@mrpatronz/nexusflow 0.2.12 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/add-repo.d.ts.map +1 -1
- package/dist/commands/add-repo.js +24 -14
- package/dist/commands/add-repo.js.map +1 -1
- package/dist/commands/commit.js +1 -1
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/diff.js +1 -1
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/handoff.js +1 -1
- package/dist/commands/handoff.js.map +1 -1
- package/dist/commands/logs.js +1 -1
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/open.d.ts.map +1 -1
- package/dist/commands/open.js +12 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/pack.js +1 -1
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/refresh.js +13 -8
- package/dist/commands/refresh.js.map +1 -1
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +12 -7
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/start.js +1 -1
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.js +1 -1
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/sync.js +13 -8
- package/dist/commands/sync.js.map +1 -1
- package/dist/core/workspace.d.ts +7 -0
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +45 -1
- package/dist/core/workspace.js.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +14 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/gui/assets/index-CFWwqFux.js +23 -0
- package/dist/gui/assets/index-Dvc9QMvl.css +2 -0
- package/dist/gui/index.html +2 -2
- package/dist/orchestration/runner.d.ts +5 -6
- package/dist/orchestration/runner.d.ts.map +1 -1
- package/dist/orchestration/runner.js +105 -102
- package/dist/orchestration/runner.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +53 -1
- package/dist/server.js.map +1 -1
- package/dist/utils/detect-editors.js +1 -1
- package/dist/utils/detect-editors.js.map +1 -1
- package/dist/utils/detect-editors.test.js +3 -3
- package/dist/utils/detect-editors.test.js.map +1 -1
- package/dist/utils/prompts.d.ts +1 -0
- package/dist/utils/prompts.d.ts.map +1 -1
- package/dist/utils/prompts.js +77 -15
- package/dist/utils/prompts.js.map +1 -1
- package/extension/package.json +9 -0
- package/extension/src/extension.ts +37 -1
- package/gui/eslint.config.js +4 -0
- package/gui/src/App.tsx +375 -151
- package/gui/src/features/changes/ChangesViewer.tsx +246 -70
- package/gui/src/features/services/ServiceConsole.tsx +162 -71
- package/gui/src/features/workspace/WorkspaceList.tsx +184 -32
- package/gui/src/index.css +95 -2
- package/package.json +2 -1
- package/src/commands/add-repo.ts +30 -14
- package/src/commands/commit.ts +1 -1
- package/src/commands/diff.ts +1 -1
- package/src/commands/doctor.ts +1 -1
- package/src/commands/handoff.ts +1 -1
- package/src/commands/logs.ts +1 -1
- package/src/commands/open.ts +15 -7
- package/src/commands/pack.ts +1 -1
- package/src/commands/refresh.ts +16 -8
- package/src/commands/remove.ts +15 -7
- package/src/commands/start.ts +1 -1
- package/src/commands/status.ts +1 -1
- package/src/commands/stop.ts +1 -1
- package/src/commands/sync.ts +16 -8
- package/src/core/workspace.ts +48 -1
- package/src/generators/index.ts +20 -0
- package/src/orchestration/runner.ts +112 -108
- package/src/server.ts +58 -1
- package/src/utils/detect-editors.test.ts +3 -3
- package/src/utils/detect-editors.ts +1 -1
- package/src/utils/prompts.ts +88 -14
- package/dist/gui/assets/index-Ca2VRMrQ.js +0 -22
- package/dist/gui/assets/index-fj0RJiOb.css +0 -2
package/src/commands/refresh.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { select } from '@inquirer/prompts';
|
|
2
|
+
import { select, search } from '@inquirer/prompts';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as fs from 'node:fs/promises';
|
|
5
5
|
|
|
@@ -117,7 +117,7 @@ async function resolveWorkspace(workspaceArg?: string): Promise<string | null> {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
const cwdFeature = await loadFeatureConfig(process.cwd());
|
|
120
|
-
if (cwdFeature) return
|
|
120
|
+
if (cwdFeature) return cwdFeature.workspacePath;
|
|
121
121
|
|
|
122
122
|
const config = await loadConfig();
|
|
123
123
|
const workspaces = await listWorkspaces(config.workspacesDir);
|
|
@@ -127,12 +127,20 @@ async function resolveWorkspace(workspaceArg?: string): Promise<string | null> {
|
|
|
127
127
|
return null;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
const selected = await
|
|
131
|
-
message: '
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
const selected = await search({
|
|
131
|
+
message: 'Search and select a workspace to refresh:',
|
|
132
|
+
source: async (input) => {
|
|
133
|
+
const query = (input || '').toLowerCase();
|
|
134
|
+
const filtered = workspaces.filter(
|
|
135
|
+
(ws) =>
|
|
136
|
+
ws.branchName.toLowerCase().includes(query) ||
|
|
137
|
+
ws.workspacePath.toLowerCase().includes(query)
|
|
138
|
+
);
|
|
139
|
+
return filtered.map((ws) => ({
|
|
140
|
+
name: `${ws.branchName} ${chalk.dim(`(${ws.repos.length} repos)`)}`,
|
|
141
|
+
value: ws.workspacePath,
|
|
142
|
+
}));
|
|
143
|
+
},
|
|
136
144
|
});
|
|
137
145
|
|
|
138
146
|
return selected;
|
package/src/commands/remove.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import ora from 'ora';
|
|
8
|
-
import { select, confirm } from '@inquirer/prompts';
|
|
8
|
+
import { select, confirm, search } from '@inquirer/prompts';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import * as fs from 'node:fs/promises';
|
|
11
11
|
|
|
@@ -52,12 +52,20 @@ export async function removeCommand(workspaceArg?: string): Promise<void> {
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const selected = await
|
|
56
|
-
message: '
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
const selected = await search({
|
|
56
|
+
message: 'Search and select a workspace to delete:',
|
|
57
|
+
source: async (input) => {
|
|
58
|
+
const query = (input || '').toLowerCase();
|
|
59
|
+
const filtered = workspaces.filter(
|
|
60
|
+
(ws) =>
|
|
61
|
+
ws.branchName.toLowerCase().includes(query) ||
|
|
62
|
+
ws.workspacePath.toLowerCase().includes(query)
|
|
63
|
+
);
|
|
64
|
+
return filtered.map((ws) => ({
|
|
65
|
+
name: `${ws.branchName} ${chalk.dim(`(${ws.repos.length} repos)`)}`,
|
|
66
|
+
value: ws,
|
|
67
|
+
}));
|
|
68
|
+
},
|
|
61
69
|
});
|
|
62
70
|
|
|
63
71
|
workspacePath = selected.workspacePath;
|
package/src/commands/start.ts
CHANGED
|
@@ -93,7 +93,7 @@ async function resolveWorkspace(workspaceArg?: string): Promise<string | null> {
|
|
|
93
93
|
|
|
94
94
|
// Check if CWD is a workspace
|
|
95
95
|
const cwdFeature = await loadFeatureConfig(process.cwd());
|
|
96
|
-
if (cwdFeature) return
|
|
96
|
+
if (cwdFeature) return cwdFeature.workspacePath;
|
|
97
97
|
|
|
98
98
|
// Otherwise, list workspaces and let user pick
|
|
99
99
|
const config = await loadConfig();
|
package/src/commands/status.ts
CHANGED
|
@@ -32,7 +32,7 @@ async function resolveWorkspace(workspaceArg?: string): Promise<string | null> {
|
|
|
32
32
|
if (workspaceArg) return workspaceArg;
|
|
33
33
|
|
|
34
34
|
const cwdFeature = await loadFeatureConfig(process.cwd());
|
|
35
|
-
if (cwdFeature) return
|
|
35
|
+
if (cwdFeature) return cwdFeature.workspacePath;
|
|
36
36
|
|
|
37
37
|
const config = await loadConfig();
|
|
38
38
|
const workspaces = await listWorkspaces(config.workspacesDir);
|
package/src/commands/stop.ts
CHANGED
|
@@ -33,7 +33,7 @@ async function resolveWorkspace(workspaceArg?: string): Promise<string | null> {
|
|
|
33
33
|
if (workspaceArg) return workspaceArg;
|
|
34
34
|
|
|
35
35
|
const cwdFeature = await loadFeatureConfig(process.cwd());
|
|
36
|
-
if (cwdFeature) return
|
|
36
|
+
if (cwdFeature) return cwdFeature.workspacePath;
|
|
37
37
|
|
|
38
38
|
const config = await loadConfig();
|
|
39
39
|
const workspaces = await listWorkspaces(config.workspacesDir);
|
package/src/commands/sync.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import { select } from '@inquirer/prompts';
|
|
7
|
+
import { select, search } from '@inquirer/prompts';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import * as fs from 'node:fs/promises';
|
|
10
10
|
|
|
@@ -123,7 +123,7 @@ async function resolveWorkspace(workspaceArg?: string): Promise<string | null> {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const cwdFeature = await loadFeatureConfig(process.cwd());
|
|
126
|
-
if (cwdFeature) return
|
|
126
|
+
if (cwdFeature) return cwdFeature.workspacePath;
|
|
127
127
|
|
|
128
128
|
const config = await loadConfig();
|
|
129
129
|
const workspaces = await listWorkspaces(config.workspacesDir);
|
|
@@ -133,12 +133,20 @@ async function resolveWorkspace(workspaceArg?: string): Promise<string | null> {
|
|
|
133
133
|
return null;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const selected = await
|
|
137
|
-
message: '
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
const selected = await search({
|
|
137
|
+
message: 'Search and select a workspace to sync:',
|
|
138
|
+
source: async (input) => {
|
|
139
|
+
const query = (input || '').toLowerCase();
|
|
140
|
+
const filtered = workspaces.filter(
|
|
141
|
+
(ws) =>
|
|
142
|
+
ws.branchName.toLowerCase().includes(query) ||
|
|
143
|
+
ws.workspacePath.toLowerCase().includes(query)
|
|
144
|
+
);
|
|
145
|
+
return filtered.map((ws) => ({
|
|
146
|
+
name: `${ws.branchName} ${chalk.dim(`(${ws.repos.length} repos)`)}`,
|
|
147
|
+
value: ws.workspacePath,
|
|
148
|
+
}));
|
|
149
|
+
},
|
|
142
150
|
});
|
|
143
151
|
|
|
144
152
|
return selected;
|
package/src/core/workspace.ts
CHANGED
|
@@ -82,6 +82,27 @@ export async function createWorkspace(
|
|
|
82
82
|
console.warn('Warning: Failed to create .vscode/settings.json:', error);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Create .cursor/mcp.json for workspace-local Cursor MCP configuration
|
|
86
|
+
try {
|
|
87
|
+
const cursorDir = path.join(workspacePath, '.cursor');
|
|
88
|
+
await fs.mkdir(cursorDir, { recursive: true });
|
|
89
|
+
const cursorMcp = {
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"nexusflow": {
|
|
92
|
+
"command": "npx",
|
|
93
|
+
"args": ["-y", "@mrpatronz/nexusflow", "mcp", "run", workspacePath]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
await fs.writeFile(
|
|
98
|
+
path.join(cursorDir, 'mcp.json'),
|
|
99
|
+
JSON.stringify(cursorMcp, null, 2) + '\n',
|
|
100
|
+
'utf-8'
|
|
101
|
+
);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn('Warning: Failed to create .cursor/mcp.json:', error);
|
|
104
|
+
}
|
|
105
|
+
|
|
85
106
|
// Create a worktree for each repo inside the workspace.
|
|
86
107
|
for (const repo of repos) {
|
|
87
108
|
const worktreeTarget = path.join(workspacePath, repo.name);
|
|
@@ -173,8 +194,10 @@ export async function saveFeatureConfig(
|
|
|
173
194
|
export async function loadFeatureConfig(
|
|
174
195
|
workspacePath: string,
|
|
175
196
|
): Promise<Feature | null> {
|
|
176
|
-
const
|
|
197
|
+
const rootDir = await findWorkspaceRoot(workspacePath);
|
|
198
|
+
if (!rootDir) return null;
|
|
177
199
|
|
|
200
|
+
const manifestPath = path.join(rootDir, MANIFEST_FILE);
|
|
178
201
|
try {
|
|
179
202
|
const raw = await fs.readFile(manifestPath, 'utf-8');
|
|
180
203
|
return JSON.parse(raw) as Feature;
|
|
@@ -183,6 +206,30 @@ export async function loadFeatureConfig(
|
|
|
183
206
|
}
|
|
184
207
|
}
|
|
185
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Traverses up parent directories to find a directory containing `nexusflow.json`.
|
|
211
|
+
*
|
|
212
|
+
* @param startPath - Path to start searching from.
|
|
213
|
+
* @returns Absolute path to the workspace root directory, or null if not found.
|
|
214
|
+
*/
|
|
215
|
+
export async function findWorkspaceRoot(startPath: string): Promise<string | null> {
|
|
216
|
+
let current = path.resolve(startPath);
|
|
217
|
+
while (true) {
|
|
218
|
+
const manifestPath = path.join(current, MANIFEST_FILE);
|
|
219
|
+
try {
|
|
220
|
+
await fs.access(manifestPath);
|
|
221
|
+
return current;
|
|
222
|
+
} catch {
|
|
223
|
+
const parent = path.dirname(current);
|
|
224
|
+
if (parent === current) {
|
|
225
|
+
break; // Reached root directory
|
|
226
|
+
}
|
|
227
|
+
current = parent;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
186
233
|
/**
|
|
187
234
|
* Resolves a repo path to a full RepoInfo object.
|
|
188
235
|
*
|
package/src/generators/index.ts
CHANGED
|
@@ -178,6 +178,7 @@ export async function generateContextFiles(
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// Generate configuration for each selected assistant
|
|
181
182
|
for (const assistant of assistants) {
|
|
182
183
|
const entry = GENERATORS[assistant];
|
|
183
184
|
|
|
@@ -196,6 +197,25 @@ export async function generateContextFiles(
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
// Ensure AGENTS.md is always generated if it is not already generated as a primary assistant file,
|
|
201
|
+
// since it is the universal standard for AI coding agents.
|
|
202
|
+
const hasAgentsGen = assistants.includes('antigravity') || assistants.includes('codex');
|
|
203
|
+
if (!hasAgentsGen) {
|
|
204
|
+
try {
|
|
205
|
+
await generateAntigravityConfig(ctx, workspacePath);
|
|
206
|
+
console.log(
|
|
207
|
+
chalk.green(' ✔'),
|
|
208
|
+
`Generated fallback ${chalk.bold('AGENTS.md')} for harness context`,
|
|
209
|
+
);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
console.error(
|
|
213
|
+
chalk.red(' ✖'),
|
|
214
|
+
`Failed to generate fallback AGENTS.md: ${message}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
199
219
|
|
|
200
220
|
// Generate implementation plan from dependency analysis (if analysis data available)
|
|
201
221
|
try {
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module orchestration/runner
|
|
3
3
|
* Manages the lifecycle of services — start, stop, status, and log streaming.
|
|
4
|
-
*
|
|
4
|
+
* Uses PM2 for process management.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as fs from 'node:fs/promises';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
-
import { spawn, type ChildProcess } from 'node:child_process';
|
|
10
9
|
import chalk from 'chalk';
|
|
10
|
+
import { execa } from 'execa';
|
|
11
11
|
|
|
12
12
|
import type { ServiceConfig, RunningService, RunningState } from '../types.js';
|
|
13
13
|
|
|
14
14
|
/** Name of the state file that tracks running services. */
|
|
15
15
|
const STATE_FILE = '.nexusflow-running.json';
|
|
16
16
|
|
|
17
|
-
/** Active child processes tracked in memory. */
|
|
18
|
-
const activeProcesses = new Map<string, ChildProcess>();
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
18
|
* Returns the path to the running-state file for a workspace.
|
|
22
19
|
*/
|
|
@@ -25,12 +22,39 @@ function getStatePath(workspacePath: string): string {
|
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
|
-
* Loads the running state from disk.
|
|
25
|
+
* Loads the running state from disk, syncing active status with PM2.
|
|
29
26
|
*/
|
|
30
27
|
export async function loadRunningState(workspacePath: string): Promise<RunningState | null> {
|
|
31
28
|
try {
|
|
32
29
|
const raw = await fs.readFile(getStatePath(workspacePath), 'utf-8');
|
|
33
|
-
|
|
30
|
+
const state = JSON.parse(raw) as RunningState;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Query current PM2 process list to verify actual running status
|
|
34
|
+
const { stdout } = await execa('npx', ['pm2', 'jlist']);
|
|
35
|
+
const pm2List = JSON.parse(stdout);
|
|
36
|
+
const workspaceId = path.basename(workspacePath);
|
|
37
|
+
const prefix = `nexusflow-${workspaceId}-`;
|
|
38
|
+
|
|
39
|
+
const activeServices = state.services.map((service) => {
|
|
40
|
+
const uniqueName = `${prefix}${service.name}`;
|
|
41
|
+
const pm2App = pm2List.find((app: any) => app.name === uniqueName);
|
|
42
|
+
const running = pm2App && pm2App.pm2_env?.status === 'online';
|
|
43
|
+
return {
|
|
44
|
+
...service,
|
|
45
|
+
pid: running ? (pm2App.pid || service.pid) : 0,
|
|
46
|
+
};
|
|
47
|
+
}).filter((service) => service.pid > 0);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...state,
|
|
51
|
+
services: activeServices,
|
|
52
|
+
updatedAt: new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
// Fallback to cached state on disk if PM2 query fails
|
|
56
|
+
return state;
|
|
57
|
+
}
|
|
34
58
|
} catch {
|
|
35
59
|
return null;
|
|
36
60
|
}
|
|
@@ -56,20 +80,7 @@ async function clearRunningState(workspacePath: string): Promise<void> {
|
|
|
56
80
|
}
|
|
57
81
|
|
|
58
82
|
/**
|
|
59
|
-
*
|
|
60
|
-
*/
|
|
61
|
-
function isProcessRunning(pid: number): boolean {
|
|
62
|
-
try {
|
|
63
|
-
process.kill(pid, 0);
|
|
64
|
-
return true;
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Starts all services in the given list.
|
|
72
|
-
* Each service is spawned as a detached child process.
|
|
83
|
+
* Starts all services in the given list using PM2.
|
|
73
84
|
*
|
|
74
85
|
* @param services - Services to start.
|
|
75
86
|
* @param workspacePath - Workspace root path.
|
|
@@ -84,63 +95,66 @@ export async function startServices(
|
|
|
84
95
|
await fs.mkdir(logDir, { recursive: true });
|
|
85
96
|
|
|
86
97
|
const runningServices: RunningService[] = [];
|
|
98
|
+
const workspaceId = path.basename(workspacePath);
|
|
87
99
|
|
|
88
100
|
for (const service of services) {
|
|
89
101
|
const logFile = path.join(logDir, `${service.name}.log`);
|
|
90
102
|
const portStr = service.port ? ` on port ${service.port}` : '';
|
|
103
|
+
const uniqueName = `nexusflow-${workspaceId}-${service.name}`;
|
|
91
104
|
|
|
92
105
|
console.log(
|
|
93
|
-
chalk.cyan(` Starting ${chalk.bold(service.name)}${portStr}...`),
|
|
106
|
+
chalk.cyan(` Starting ${chalk.bold(service.name)}${portStr} under PM2...`),
|
|
94
107
|
);
|
|
95
108
|
console.log(
|
|
96
109
|
chalk.dim(` ${service.command} ${service.args.join(' ')} (in ${service.cwd})`),
|
|
97
110
|
);
|
|
98
111
|
|
|
99
112
|
try {
|
|
100
|
-
// Ensure parent directory for this log file exists
|
|
113
|
+
// Ensure parent directory for this log file exists
|
|
101
114
|
await fs.mkdir(path.dirname(logFile), { recursive: true });
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
activeProcesses.set(service.name, child);
|
|
115
|
+
|
|
116
|
+
// Delete any existing PM2 configuration with the same name to avoid duplicate starts
|
|
117
|
+
await execa('npx', ['pm2', 'delete', uniqueName], { reject: false });
|
|
118
|
+
|
|
119
|
+
// Start the service using PM2 as a direct process execution
|
|
120
|
+
await execa('npx', [
|
|
121
|
+
'pm2',
|
|
122
|
+
'start',
|
|
123
|
+
service.command,
|
|
124
|
+
'--name',
|
|
125
|
+
uniqueName,
|
|
126
|
+
'--cwd',
|
|
127
|
+
service.cwd,
|
|
128
|
+
'-o',
|
|
129
|
+
logFile,
|
|
130
|
+
'-e',
|
|
131
|
+
logFile,
|
|
132
|
+
'--interpreter',
|
|
133
|
+
'none',
|
|
134
|
+
'--',
|
|
135
|
+
...service.args
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
// Retrieve the real PID from PM2
|
|
139
|
+
const { stdout } = await execa('npx', ['pm2', 'jlist']);
|
|
140
|
+
const pm2List = JSON.parse(stdout);
|
|
141
|
+
const pm2App = pm2List.find((app: any) => app.name === uniqueName);
|
|
142
|
+
const pid = pm2App?.pid || 0;
|
|
143
|
+
|
|
144
|
+
if (pid) {
|
|
133
145
|
runningServices.push({
|
|
134
146
|
name: service.name,
|
|
135
|
-
pid
|
|
147
|
+
pid,
|
|
136
148
|
config: service,
|
|
137
149
|
startedAt: new Date().toISOString(),
|
|
138
150
|
});
|
|
139
|
-
console.log(chalk.green(` ✔ ${service.name} started (PID: ${
|
|
151
|
+
console.log(chalk.green(` ✔ ${service.name} started under PM2 (PID: ${pid})`));
|
|
152
|
+
} else {
|
|
153
|
+
console.warn(chalk.yellow(` ⚠ Started ${service.name} but could not resolve PID from PM2.`));
|
|
140
154
|
}
|
|
141
155
|
} catch (error) {
|
|
142
156
|
const msg = error instanceof Error ? error.message : String(error);
|
|
143
|
-
console.error(chalk.red(` ✖ Failed to start ${service.name}: ${msg}`));
|
|
157
|
+
console.error(chalk.red(` ✖ Failed to start ${service.name} via PM2: ${msg}`));
|
|
144
158
|
}
|
|
145
159
|
}
|
|
146
160
|
|
|
@@ -155,79 +169,69 @@ export async function startServices(
|
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
/**
|
|
158
|
-
* Stops all services for a workspace.
|
|
172
|
+
* Stops all services for a workspace under PM2.
|
|
159
173
|
*
|
|
160
174
|
* @param workspacePath - Workspace root path.
|
|
161
175
|
*/
|
|
162
176
|
export async function stopServices(workspacePath: string): Promise<void> {
|
|
163
|
-
const
|
|
177
|
+
const workspaceId = path.basename(workspacePath);
|
|
178
|
+
const prefix = `nexusflow-${workspaceId}-`;
|
|
164
179
|
|
|
165
|
-
|
|
166
|
-
console.log(chalk.yellow(' No running services found.'));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
180
|
+
console.log(chalk.cyan(` Stopping all services under PM2 for workspace: ${workspaceId}...`));
|
|
169
181
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
// Kill the process group (negative PID kills group on Unix, taskkill on Windows)
|
|
176
|
-
if (process.platform === 'win32') {
|
|
177
|
-
spawn('taskkill', ['/pid', String(child.pid), '/f', '/t'], { stdio: 'ignore' });
|
|
178
|
-
} else {
|
|
179
|
-
process.kill(-child.pid, 'SIGTERM');
|
|
180
|
-
}
|
|
181
|
-
console.log(chalk.green(` ✔ Stopped ${service.name} (PID: ${service.pid})`));
|
|
182
|
-
activeProcesses.delete(service.name);
|
|
183
|
-
continue;
|
|
184
|
-
} catch {
|
|
185
|
-
// Fall through to PID-based kill
|
|
186
|
-
}
|
|
187
|
-
}
|
|
182
|
+
try {
|
|
183
|
+
const { stdout } = await execa('npx', ['pm2', 'jlist']);
|
|
184
|
+
const pm2List = JSON.parse(stdout);
|
|
185
|
+
const targetApps = pm2List.filter((app: any) => app.name && app.name.startsWith(prefix));
|
|
188
186
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
if (process.platform === 'win32') {
|
|
193
|
-
spawn('taskkill', ['/pid', String(service.pid), '/f', '/t'], { stdio: 'ignore' });
|
|
194
|
-
} else {
|
|
195
|
-
process.kill(service.pid, 'SIGTERM');
|
|
196
|
-
}
|
|
197
|
-
console.log(chalk.green(` ✔ Stopped ${service.name} (PID: ${service.pid})`));
|
|
198
|
-
} catch {
|
|
199
|
-
console.log(chalk.yellow(` ⚠ Could not stop ${service.name} (PID: ${service.pid})`));
|
|
200
|
-
}
|
|
187
|
+
if (targetApps.length === 0) {
|
|
188
|
+
console.log(chalk.yellow(' No running services found for this workspace.'));
|
|
201
189
|
} else {
|
|
202
|
-
|
|
190
|
+
for (const app of targetApps) {
|
|
191
|
+
console.log(chalk.dim(` Stopping PM2 process: ${app.name}`));
|
|
192
|
+
await execa('npx', ['pm2', 'delete', app.name], { reject: false });
|
|
193
|
+
}
|
|
194
|
+
console.log(chalk.green(` ✔ All services stopped.`));
|
|
203
195
|
}
|
|
196
|
+
} catch (error: any) {
|
|
197
|
+
console.error(chalk.red(` ✖ Failed to stop services via PM2: ${error.message}`));
|
|
204
198
|
}
|
|
205
199
|
|
|
206
200
|
await clearRunningState(workspacePath);
|
|
207
201
|
}
|
|
208
202
|
|
|
209
203
|
/**
|
|
210
|
-
* Shows the status of all services in a workspace.
|
|
204
|
+
* Shows the status of all services in a workspace by querying PM2.
|
|
211
205
|
*
|
|
212
206
|
* @param workspacePath - Workspace root path.
|
|
213
207
|
*/
|
|
214
208
|
export async function getServiceStatus(workspacePath: string): Promise<void> {
|
|
215
|
-
const
|
|
209
|
+
const workspaceId = path.basename(workspacePath);
|
|
210
|
+
const prefix = `nexusflow-${workspaceId}-`;
|
|
216
211
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
212
|
+
try {
|
|
213
|
+
const { stdout } = await execa('npx', ['pm2', 'jlist']);
|
|
214
|
+
const pm2List = JSON.parse(stdout);
|
|
215
|
+
const targetApps = pm2List.filter((app: any) => app.name && app.name.startsWith(prefix));
|
|
221
216
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const since = new Date(service.startedAt).toLocaleTimeString();
|
|
217
|
+
if (targetApps.length === 0) {
|
|
218
|
+
console.log(chalk.yellow(' No running services found for this workspace in PM2.'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
227
221
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
222
|
+
for (const app of targetApps) {
|
|
223
|
+
const name = app.name.substring(prefix.length);
|
|
224
|
+
const running = app.pm2_env?.status === 'online';
|
|
225
|
+
const status = running ? chalk.green('● running') : chalk.red(`● ${app.pm2_env?.status || 'stopped'}`);
|
|
226
|
+
const pid = app.pid || 'N/A';
|
|
227
|
+
const uptime = app.pm2_env?.pm_uptime ? new Date(app.pm2_env.pm_uptime).toLocaleTimeString() : 'unknown';
|
|
228
|
+
|
|
229
|
+
console.log(
|
|
230
|
+
` ${status} ${chalk.bold(name)} (PID: ${pid}, since ${uptime})`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
} catch (error: any) {
|
|
234
|
+
console.error(chalk.red(` ✖ Failed to query PM2 status: ${error.message}`));
|
|
231
235
|
}
|
|
232
236
|
}
|
|
233
237
|
|
package/src/server.ts
CHANGED
|
@@ -46,7 +46,7 @@ const guiPath = path.join(__dirname, 'gui');
|
|
|
46
46
|
export const app = new Hono();
|
|
47
47
|
|
|
48
48
|
// Allowed editor binaries/scripts to prevent command injection
|
|
49
|
-
const ALLOWED_EDITORS = new Set(['code', 'code-insiders', 'cursor', 'agy', 'idea', 'charm', 'webstorm', 'subl', 'nano', 'vim', 'nvim', 'emacs']);
|
|
49
|
+
const ALLOWED_EDITORS = new Set(['code', 'code-insiders', 'cursor', 'antigravity', 'agy', 'idea', 'charm', 'webstorm', 'subl', 'nano', 'vim', 'nvim', 'emacs']);
|
|
50
50
|
|
|
51
51
|
// Enable CORS for frontend dev server
|
|
52
52
|
app.use('/api/*', cors());
|
|
@@ -700,6 +700,63 @@ app.get('/api/workspace/:id/changes', async (c) => {
|
|
|
700
700
|
}
|
|
701
701
|
});
|
|
702
702
|
|
|
703
|
+
// 13_diff. Get git diff for a specific file in workspace sub-repositories
|
|
704
|
+
app.get('/api/workspace/:id/changes/diff', async (c) => {
|
|
705
|
+
try {
|
|
706
|
+
const id = c.req.param('id');
|
|
707
|
+
const repoName = c.req.query('repo');
|
|
708
|
+
const filePath = c.req.query('file');
|
|
709
|
+
|
|
710
|
+
if (!repoName || !filePath) {
|
|
711
|
+
return c.json({ error: 'Missing repo or file query parameter.' }, 400);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const config = await loadConfig();
|
|
715
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
716
|
+
const worktreePath = path.join(workspacePath, repoName);
|
|
717
|
+
|
|
718
|
+
// Security check: ensure path is within workspace directory
|
|
719
|
+
const resolvedPath = path.resolve(worktreePath);
|
|
720
|
+
if (!resolvedPath.startsWith(path.resolve(workspacePath))) {
|
|
721
|
+
return c.json({ error: 'Invalid repository path.' }, 400);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let diff = '';
|
|
725
|
+
|
|
726
|
+
// Check git status for the file to know if it's untracked
|
|
727
|
+
try {
|
|
728
|
+
const { stdout: statusOut } = await execa('git', ['status', '--porcelain', '--', filePath], { cwd: worktreePath });
|
|
729
|
+
const statusLine = statusOut.trim();
|
|
730
|
+
const isUntracked = statusLine.startsWith('??');
|
|
731
|
+
|
|
732
|
+
if (isUntracked) {
|
|
733
|
+
const result = await execa('git', ['diff', '--no-index', '--', '/dev/null', filePath], {
|
|
734
|
+
cwd: worktreePath,
|
|
735
|
+
reject: false
|
|
736
|
+
});
|
|
737
|
+
diff = result.stdout || result.stderr || '';
|
|
738
|
+
} else {
|
|
739
|
+
const result = await execa('git', ['diff', 'HEAD', '--', filePath], {
|
|
740
|
+
cwd: worktreePath,
|
|
741
|
+
reject: false
|
|
742
|
+
});
|
|
743
|
+
diff = result.stdout || result.stderr || '';
|
|
744
|
+
}
|
|
745
|
+
} catch (e) {
|
|
746
|
+
const result = await execa('git', ['diff', 'HEAD', '--', filePath], {
|
|
747
|
+
cwd: worktreePath,
|
|
748
|
+
reject: false
|
|
749
|
+
});
|
|
750
|
+
diff = result.stdout || result.stderr || '';
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return c.json({ diff });
|
|
754
|
+
} catch (error) {
|
|
755
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
756
|
+
return c.json({ error: msg }, 500);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
703
760
|
// 13a. Get workspace knowledge (nexusflow-knowledge.md)
|
|
704
761
|
app.get('/api/workspace/:id/knowledge', async (c) => {
|
|
705
762
|
try {
|