@mrpatronz/nexusflow 0.2.11 โ 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/create.d.ts.map +1 -1
- package/dist/commands/create.js +9 -2
- package/dist/commands/create.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 +21 -9
- 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/antigravity.d.ts +15 -0
- package/dist/generators/antigravity.d.ts.map +1 -0
- package/dist/generators/antigravity.js +27 -0
- package/dist/generators/antigravity.js.map +1 -0
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +17 -1
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/skills-generator.d.ts.map +1 -1
- package/dist/generators/skills-generator.js +6 -1
- package/dist/generators/skills-generator.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 +57 -2
- package/dist/server.js.map +1 -1
- package/dist/utils/detect-ai.d.ts.map +1 -1
- package/dist/utils/detect-ai.js +4 -1
- package/dist/utils/detect-ai.js.map +1 -1
- package/dist/utils/detect-editors.d.ts.map +1 -1
- package/dist/utils/detect-editors.js +5 -2
- package/dist/utils/detect-editors.js.map +1 -1
- package/dist/utils/detect-editors.test.js +7 -6
- 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/dist/utils/update-check.d.ts.map +1 -1
- package/dist/utils/update-check.js +16 -4
- package/dist/utils/update-check.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 +3 -2
- package/src/commands/add-repo.ts +30 -14
- package/src/commands/commit.ts +1 -1
- package/src/commands/create.ts +9 -2
- 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 +24 -9
- 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/antigravity.ts +32 -0
- package/src/generators/index.ts +23 -1
- package/src/generators/skills-generator.ts +5 -1
- package/src/orchestration/runner.ts +112 -108
- package/src/server.ts +62 -2
- package/src/utils/detect-ai.ts +4 -1
- package/src/utils/detect-editors.test.ts +7 -6
- package/src/utils/detect-editors.ts +5 -2
- package/src/utils/prompts.ts +88 -14
- package/src/utils/update-check.ts +16 -4
- package/dist/gui/assets/index-Ca2VRMrQ.js +0 -22
- package/dist/gui/assets/index-fj0RJiOb.css +0 -2
|
@@ -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 {
|
|
@@ -978,7 +1035,10 @@ app.post('/api/updates/install', async (c) => {
|
|
|
978
1035
|
return c.json({ error: 'Tool not found' }, 404);
|
|
979
1036
|
}
|
|
980
1037
|
|
|
981
|
-
const result = await execa(target.cmd, target.args, {
|
|
1038
|
+
const result = await execa(target.cmd, target.args, {
|
|
1039
|
+
reject: false,
|
|
1040
|
+
shell: process.platform === 'win32',
|
|
1041
|
+
});
|
|
982
1042
|
if (result.exitCode === 0) {
|
|
983
1043
|
return c.json({ success: true, output: result.stdout });
|
|
984
1044
|
} else {
|
package/src/utils/detect-ai.ts
CHANGED
|
@@ -13,7 +13,10 @@ import type { AIAssistant, DetectedAI } from '../types.js';
|
|
|
13
13
|
*/
|
|
14
14
|
async function commandExists(command: string): Promise<boolean> {
|
|
15
15
|
try {
|
|
16
|
-
const result = await execa(command, ['--version'], {
|
|
16
|
+
const result = await execa(command, ['--version'], {
|
|
17
|
+
reject: false,
|
|
18
|
+
shell: process.platform === 'win32',
|
|
19
|
+
});
|
|
17
20
|
return result.exitCode === 0;
|
|
18
21
|
} catch {
|
|
19
22
|
// The command could not be spawned at all (not in PATH).
|
|
@@ -23,13 +23,14 @@ describe('detectEditors', () => {
|
|
|
23
23
|
{ name: 'VS Code', command: 'code', detected: true },
|
|
24
24
|
{ name: 'VS Code Insiders', command: 'code-insiders', detected: false },
|
|
25
25
|
{ name: 'Cursor', command: 'cursor', detected: true },
|
|
26
|
-
{ name: 'Antigravity', command: '
|
|
26
|
+
{ name: 'Antigravity', command: 'antigravity', detected: false },
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
expect(execa).toHaveBeenCalledWith('code
|
|
31
|
-
expect(execa).toHaveBeenCalledWith('
|
|
32
|
-
expect(execa).toHaveBeenCalledWith('
|
|
29
|
+
const isWin = process.platform === 'win32';
|
|
30
|
+
expect(execa).toHaveBeenCalledWith('code', ['--version'], { reject: false, shell: isWin });
|
|
31
|
+
expect(execa).toHaveBeenCalledWith('code-insiders', ['--version'], { reject: false, shell: isWin });
|
|
32
|
+
expect(execa).toHaveBeenCalledWith('cursor', ['--version'], { reject: false, shell: isWin });
|
|
33
|
+
expect(execa).toHaveBeenCalledWith('antigravity', ['--version'], { reject: false, shell: isWin });
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
it('should return detected = false for all editors if execa throws an error', async () => {
|
|
@@ -41,7 +42,7 @@ describe('detectEditors', () => {
|
|
|
41
42
|
{ name: 'VS Code', command: 'code', detected: false },
|
|
42
43
|
{ name: 'VS Code Insiders', command: 'code-insiders', detected: false },
|
|
43
44
|
{ name: 'Cursor', command: 'cursor', detected: false },
|
|
44
|
-
{ name: 'Antigravity', command: '
|
|
45
|
+
{ name: 'Antigravity', command: 'antigravity', detected: false },
|
|
45
46
|
]);
|
|
46
47
|
});
|
|
47
48
|
});
|
|
@@ -12,7 +12,7 @@ const EDITOR_CANDIDATES: ReadonlyArray<{ name: string; command: string }> = [
|
|
|
12
12
|
{ name: 'VS Code', command: 'code' },
|
|
13
13
|
{ name: 'VS Code Insiders', command: 'code-insiders' },
|
|
14
14
|
{ name: 'Cursor', command: 'cursor' },
|
|
15
|
-
{ name: 'Antigravity', command: '
|
|
15
|
+
{ name: 'Antigravity', command: 'antigravity' },
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -21,7 +21,10 @@ const EDITOR_CANDIDATES: ReadonlyArray<{ name: string; command: string }> = [
|
|
|
21
21
|
*/
|
|
22
22
|
async function commandExists(command: string): Promise<boolean> {
|
|
23
23
|
try {
|
|
24
|
-
const result = await execa(command, ['--version'], {
|
|
24
|
+
const result = await execa(command, ['--version'], {
|
|
25
|
+
reject: false,
|
|
26
|
+
shell: process.platform === 'win32',
|
|
27
|
+
});
|
|
25
28
|
return result.exitCode === 0;
|
|
26
29
|
} catch {
|
|
27
30
|
return false;
|
package/src/utils/prompts.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Interactive CLI prompts for NexusFlow using @inquirer/prompts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { input, checkbox, confirm } from '@inquirer/prompts';
|
|
6
|
+
import { input, checkbox, confirm, select, search } from '@inquirer/prompts';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
|
|
9
9
|
import type { AIAssistant, DetectedAI, DetectedEditor, RepoInfo } from '../types.js';
|
|
@@ -57,26 +57,100 @@ export async function promptDescription(): Promise<string> {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Prompts the user to select repos from a list of discovered repos.
|
|
60
|
+
* Supports searching/filtering by term, viewing all, and incremental selection.
|
|
60
61
|
*/
|
|
61
62
|
export async function promptSelectRepos(repos: RepoInfo[]): Promise<RepoInfo[]> {
|
|
62
63
|
if (repos.length === 0) {
|
|
63
64
|
return [];
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
const selectedPaths = new Set<string>();
|
|
68
|
+
|
|
69
|
+
// If there are only a few repos (e.g. <= 10), we can just show a checkbox prompt directly to save time
|
|
70
|
+
if (repos.length <= 10) {
|
|
71
|
+
const toggled = await checkbox({
|
|
72
|
+
message: 'Select repositories to include in workspace:',
|
|
73
|
+
choices: repos.map((repo) => ({
|
|
74
|
+
name: `${repo.name} ${chalk.dim(`(${repo.path})`)}`,
|
|
75
|
+
value: repo.path,
|
|
76
|
+
checked: false,
|
|
77
|
+
})),
|
|
78
|
+
});
|
|
79
|
+
return repos.filter((r) => toggled.includes(r.path));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Otherwise, use a loop with the search prompt to make it extremely easy to filter and toggle selection
|
|
83
|
+
while (true) {
|
|
84
|
+
console.log(chalk.bold(`\n๐ Selected Repositories (${selectedPaths.size}/${repos.length}):`));
|
|
85
|
+
if (selectedPaths.size === 0) {
|
|
86
|
+
console.log(chalk.dim(' (None selected yet)'));
|
|
87
|
+
} else {
|
|
88
|
+
repos.forEach((r) => {
|
|
89
|
+
if (selectedPaths.has(r.path)) {
|
|
90
|
+
console.log(` ${chalk.green('โ')} ${r.name} ${chalk.dim(`(${r.path})`)}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
console.log();
|
|
95
|
+
|
|
96
|
+
const result = await search({
|
|
97
|
+
message: 'Search and select repositories (type to filter, select to toggle, choose Done to finish):',
|
|
98
|
+
source: async (input) => {
|
|
99
|
+
const query = (input || '').toLowerCase();
|
|
100
|
+
const filtered = repos.filter(
|
|
101
|
+
(r) =>
|
|
102
|
+
r.name.toLowerCase().includes(query) ||
|
|
103
|
+
r.path.toLowerCase().includes(query)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const choices = [
|
|
107
|
+
{
|
|
108
|
+
name: chalk.green(`โ
Done selecting (${selectedPaths.size} repo(s) selected)`),
|
|
109
|
+
value: 'done',
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
if (selectedPaths.size > 0) {
|
|
114
|
+
choices.push({
|
|
115
|
+
name: chalk.yellow('๐งน Clear all selections'),
|
|
116
|
+
value: 'clear',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
filtered.forEach((r) => {
|
|
121
|
+
const isSelected = selectedPaths.has(r.path);
|
|
122
|
+
choices.push({
|
|
123
|
+
name: `${isSelected ? chalk.green('โ') : ' '} ${r.name} ${chalk.dim(`(${r.path})`)}`,
|
|
124
|
+
value: r.path,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return choices;
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (result === 'done') {
|
|
133
|
+
if (selectedPaths.size === 0) {
|
|
134
|
+
console.log(chalk.red(' Please select at least one repository.'));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (result === 'clear') {
|
|
141
|
+
selectedPaths.clear();
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Toggle selected state
|
|
146
|
+
if (selectedPaths.has(result)) {
|
|
147
|
+
selectedPaths.delete(result);
|
|
148
|
+
} else {
|
|
149
|
+
selectedPaths.add(result);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
78
152
|
|
|
79
|
-
return repos.filter((r) =>
|
|
153
|
+
return repos.filter((r) => selectedPaths.has(r.path));
|
|
80
154
|
}
|
|
81
155
|
|
|
82
156
|
/**
|
|
@@ -178,11 +178,17 @@ export async function getToolsStatus(force = false): Promise<ToolUpdateStatus[]>
|
|
|
178
178
|
updateCmd: 'npm install -g repomix',
|
|
179
179
|
getCurrent: async () => {
|
|
180
180
|
try {
|
|
181
|
-
const res = await execa('repomix', ['--version'], {
|
|
181
|
+
const res = await execa('repomix', ['--version'], {
|
|
182
|
+
reject: false,
|
|
183
|
+
shell: process.platform === 'win32',
|
|
184
|
+
});
|
|
182
185
|
if (res.exitCode === 0) return res.stdout.trim();
|
|
183
186
|
} catch {}
|
|
184
187
|
try {
|
|
185
|
-
const res = await execa('npx', ['repomix', '--version'], {
|
|
188
|
+
const res = await execa('npx', ['repomix', '--version'], {
|
|
189
|
+
reject: false,
|
|
190
|
+
shell: process.platform === 'win32',
|
|
191
|
+
});
|
|
186
192
|
if (res.exitCode === 0) return res.stdout.trim();
|
|
187
193
|
} catch {}
|
|
188
194
|
return '';
|
|
@@ -196,7 +202,10 @@ export async function getToolsStatus(force = false): Promise<ToolUpdateStatus[]>
|
|
|
196
202
|
updateCmd: 'agy update',
|
|
197
203
|
getCurrent: async () => {
|
|
198
204
|
try {
|
|
199
|
-
const res = await execa('agy', ['--version'], {
|
|
205
|
+
const res = await execa('agy', ['--version'], {
|
|
206
|
+
reject: false,
|
|
207
|
+
shell: process.platform === 'win32',
|
|
208
|
+
});
|
|
200
209
|
if (res.exitCode === 0) return res.stdout.trim();
|
|
201
210
|
} catch {}
|
|
202
211
|
return '';
|
|
@@ -210,7 +219,10 @@ export async function getToolsStatus(force = false): Promise<ToolUpdateStatus[]>
|
|
|
210
219
|
updateCmd: 'npm install -g @anthropic-ai/claude-code',
|
|
211
220
|
getCurrent: async () => {
|
|
212
221
|
try {
|
|
213
|
-
const res = await execa('claude', ['--version'], {
|
|
222
|
+
const res = await execa('claude', ['--version'], {
|
|
223
|
+
reject: false,
|
|
224
|
+
shell: process.platform === 'win32',
|
|
225
|
+
});
|
|
214
226
|
if (res.exitCode === 0) return res.stdout.trim();
|
|
215
227
|
} catch {}
|
|
216
228
|
return '';
|