@mrpatronz/nexusflow 1.8.0 → 1.9.1
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/README.md +48 -15
- package/dist/commands/create.d.ts +8 -7
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +134 -65
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +0 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -72
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +2 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/project.d.ts +30 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +125 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/refresh.js +1 -1
- package/dist/commands/refresh.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +18 -2
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/stop.d.ts +1 -1
- package/dist/commands/stop.d.ts.map +1 -1
- package/dist/commands/stop.js +19 -2
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/tui.js +0 -1
- package/dist/commands/tui.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +0 -9
- package/dist/core/config.js.map +1 -1
- package/dist/core/config.test.js +0 -9
- package/dist/core/config.test.js.map +1 -1
- package/dist/core/doctor.d.ts +1 -3
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +18 -69
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/finish.d.ts.map +1 -1
- package/dist/core/finish.js +5 -0
- package/dist/core/finish.js.map +1 -1
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +2 -1
- package/dist/core/graph.js.map +1 -1
- package/dist/core/projects.d.ts +58 -0
- package/dist/core/projects.d.ts.map +1 -0
- package/dist/core/projects.js +190 -0
- package/dist/core/projects.js.map +1 -0
- package/dist/core/projects.test.d.ts +2 -0
- package/dist/core/projects.test.d.ts.map +1 -0
- package/dist/core/projects.test.js +165 -0
- package/dist/core/projects.test.js.map +1 -0
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/core/sync.js +21 -0
- package/dist/core/sync.js.map +1 -1
- package/dist/core/sync.test.js +15 -0
- package/dist/core/sync.test.js.map +1 -1
- package/dist/core/workspace.create.test.js +68 -1
- package/dist/core/workspace.create.test.js.map +1 -1
- package/dist/core/workspace.d.ts +4 -3
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +138 -64
- package/dist/core/workspace.js.map +1 -1
- package/dist/generators/antigravity.js +1 -1
- package/dist/generators/antigravity.js.map +1 -1
- package/dist/generators/base.d.ts +1 -8
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +107 -58
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/base.test.d.ts +2 -0
- package/dist/generators/base.test.d.ts.map +1 -0
- package/dist/generators/base.test.js +80 -0
- package/dist/generators/base.test.js.map +1 -0
- package/dist/generators/claude.js +1 -1
- package/dist/generators/claude.js.map +1 -1
- package/dist/generators/codex.js +1 -1
- package/dist/generators/codex.js.map +1 -1
- package/dist/generators/copilot.js +1 -1
- package/dist/generators/copilot.js.map +1 -1
- package/dist/generators/cursor.js +1 -1
- package/dist/generators/cursor.js.map +1 -1
- package/dist/generators/diff-context.d.ts.map +1 -1
- package/dist/generators/diff-context.js +3 -1
- package/dist/generators/diff-context.js.map +1 -1
- package/dist/generators/index.js +1 -1
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/map-generator.d.ts.map +1 -1
- package/dist/generators/map-generator.js +3 -1
- package/dist/generators/map-generator.js.map +1 -1
- package/dist/gui/assets/DashboardPage-C3NH97y2.js +1 -0
- package/dist/gui/assets/GettingStartedPage-og-Kv7Sm.js +1 -0
- package/dist/gui/assets/ProjectsPage-COqqadEr.js +1 -0
- package/dist/gui/assets/ScaffoldRepoInline-DRipQsos.js +1 -0
- package/dist/gui/assets/Separator-Chx6O62Y.js +1 -0
- package/dist/gui/assets/SettingsPage-CpU3V9oU.js +1 -0
- package/dist/gui/assets/StartWorkPage-DIND0hJy.js +1 -0
- package/dist/gui/assets/StrategiesPage-6LWOnKdk.js +3 -0
- package/dist/gui/assets/WorkspacesPage-BUqgOhuy.js +6 -0
- package/dist/gui/assets/badge-BnkhqVEF.js +1 -0
- package/dist/gui/assets/button-cPldH5S4.js +1 -0
- package/dist/gui/assets/card-82INhRRE.js +1 -0
- package/dist/gui/assets/circle-plus-C0PNc7h4.js +1 -0
- package/dist/gui/assets/cpu-CPUNnHRM.js +1 -0
- package/dist/gui/assets/empty-DWyDwDYB.js +1 -0
- package/dist/gui/assets/external-link-BtYhMxno.js +1 -0
- package/dist/gui/assets/git-branch-Dw55pQWa.js +1 -0
- package/dist/gui/assets/index-CsA2SueF.css +2 -0
- package/dist/gui/assets/index-CyYbQwRf.js +52 -0
- package/dist/gui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/gui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/gui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/gui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/gui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/gui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/gui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-100-normal-DlYB2XW3.woff2 +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-100-normal-DzH8uRxw.woff +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-200-normal-CHn02WOn.woff +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-200-normal-DvzYDkvL.woff2 +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-300-normal-BYcAiAh2.woff +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-300-normal-DuMDZskh.woff2 +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-800-normal-D2mQHRMK.woff2 +0 -0
- package/dist/gui/assets/jetbrains-mono-latin-800-normal-Dj9qwObk.woff +0 -0
- package/dist/gui/assets/label-JFgMNsTn.js +1 -0
- package/dist/gui/assets/search-ChVovkOL.js +1 -0
- package/dist/gui/assets/skeleton-CCPbfn1L.js +1 -0
- package/dist/gui/assets/spinner-DXt6mnJk.js +1 -0
- package/dist/gui/assets/textarea-BLU5HXmZ.js +1 -0
- package/dist/gui/assets/trash-2-Da8F2gKd.js +1 -0
- package/dist/gui/assets/useAriaLabelledBy-CjNlctb9.js +1 -0
- package/dist/gui/assets/useCompositeListItem-DWGFLKqA.js +1 -0
- package/dist/gui/index.html +16 -6
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +4 -63
- package/dist/mcp/tools.js.map +1 -1
- package/dist/orchestration/detect.d.ts.map +1 -1
- package/dist/orchestration/detect.js +72 -26
- package/dist/orchestration/detect.js.map +1 -1
- package/dist/orchestration/index.d.ts +3 -1
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +3 -1
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/log-tail.d.ts +27 -0
- package/dist/orchestration/log-tail.d.ts.map +1 -0
- package/dist/orchestration/log-tail.js +93 -0
- package/dist/orchestration/log-tail.js.map +1 -0
- package/dist/orchestration/log-tail.test.d.ts +2 -0
- package/dist/orchestration/log-tail.test.d.ts.map +1 -0
- package/dist/orchestration/log-tail.test.js +72 -0
- package/dist/orchestration/log-tail.test.js.map +1 -0
- package/dist/orchestration/orchestrator.d.ts +26 -0
- package/dist/orchestration/orchestrator.d.ts.map +1 -0
- package/dist/orchestration/orchestrator.js +102 -0
- package/dist/orchestration/orchestrator.js.map +1 -0
- package/dist/orchestration/orchestrator.test.d.ts +2 -0
- package/dist/orchestration/orchestrator.test.d.ts.map +1 -0
- package/dist/orchestration/orchestrator.test.js +85 -0
- package/dist/orchestration/orchestrator.test.js.map +1 -0
- package/dist/orchestration/runner.d.ts +62 -3
- package/dist/orchestration/runner.d.ts.map +1 -1
- package/dist/orchestration/runner.js +198 -103
- package/dist/orchestration/runner.js.map +1 -1
- package/dist/orchestration/runner.test.js +125 -1
- package/dist/orchestration/runner.test.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +286 -124
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +111 -123
- package/dist/server.test.js.map +1 -1
- package/dist/types.d.ts +79 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/feature.d.ts +42 -0
- package/dist/utils/feature.d.ts.map +1 -0
- package/dist/utils/feature.js +52 -0
- package/dist/utils/feature.js.map +1 -0
- package/dist/utils/multi-git.d.ts.map +1 -1
- package/dist/utils/multi-git.js +30 -7
- package/dist/utils/multi-git.js.map +1 -1
- package/dist/utils/slug.d.ts +12 -0
- package/dist/utils/slug.d.ts.map +1 -0
- package/dist/utils/slug.js +18 -0
- package/dist/utils/slug.js.map +1 -0
- package/dist/utils/test-command.d.ts +12 -0
- package/dist/utils/test-command.d.ts.map +1 -0
- package/dist/utils/test-command.js +22 -0
- package/dist/utils/test-command.js.map +1 -0
- package/dist/utils/update-check.d.ts +8 -0
- package/dist/utils/update-check.d.ts.map +1 -1
- package/dist/utils/update-check.js +13 -7
- package/dist/utils/update-check.js.map +1 -1
- package/dist/utils/update-check.test.d.ts +2 -0
- package/dist/utils/update-check.test.d.ts.map +1 -0
- package/dist/utils/update-check.test.js +25 -0
- package/dist/utils/update-check.test.js.map +1 -0
- package/dist/utils/workflow-advisor.d.ts +2 -2
- package/dist/utils/workflow-advisor.d.ts.map +1 -1
- package/dist/utils/workflow-advisor.js +50 -92
- package/dist/utils/workflow-advisor.js.map +1 -1
- package/dist/utils/workflows.d.ts.map +1 -1
- package/dist/utils/workflows.js +2 -4
- package/dist/utils/workflows.js.map +1 -1
- package/package.json +2 -2
- package/dist/gui/assets/index-CQTEzHj2.css +0 -1
- package/dist/gui/assets/index-DLmLdpqC.js +0 -58
- package/dist/utils/local-ai.d.ts +0 -27
- package/dist/utils/local-ai.d.ts.map +0 -1
- package/dist/utils/local-ai.js +0 -100
- package/dist/utils/local-ai.js.map +0 -1
- package/dist/utils/local-ai.test.d.ts +0 -2
- package/dist/utils/local-ai.test.d.ts.map +0 -1
- package/dist/utils/local-ai.test.js +0 -108
- package/dist/utils/local-ai.test.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -20,18 +20,18 @@ import { loadConfig, saveConfig, getConfigDir } from './core/config.js';
|
|
|
20
20
|
import { listStorageProviders } from './core/adapters/registry.js';
|
|
21
21
|
import { scanForRepos } from './core/scanner.js';
|
|
22
22
|
import { createNewRepo } from './core/new-repo.js';
|
|
23
|
+
import { loadProjects, createProject, updateProject, removeProject, slugifyProjectName } from './core/projects.js';
|
|
24
|
+
import { getSessionCwd, isInPlace, resolveFeatureRepoPath } from './utils/feature.js';
|
|
23
25
|
import { listBranches } from './utils/git.js';
|
|
24
26
|
import { createWorkspace, listWorkspaces, loadFeatureConfig, deleteWorkspace, addRepoToWorkspace } from './core/workspace.js';
|
|
25
27
|
import { loadWorkspaceState } from './core/workspace-state.js';
|
|
26
28
|
import { analyzeAllRepos } from './analyzers/index.js';
|
|
27
29
|
import { generateContextFiles } from './generators/index.js';
|
|
28
|
-
import { isOllamaModelAvailable, getOpenAiCompatibleUrl, callLocalLlm } from './utils/local-ai.js';
|
|
29
30
|
import { detectAIAssistants } from './utils/detect-ai.js';
|
|
30
31
|
import { detectEditors } from './utils/detect-editors.js';
|
|
31
32
|
import { findSessions, getSessionTranscript } from './utils/session-finder.js';
|
|
32
33
|
import { ProviderRegistry } from './agent/adapters.js';
|
|
33
34
|
import { isValidSessionUuid } from './agent/session.js';
|
|
34
|
-
import { scanSystemSpecs } from './utils/system-scanner.js';
|
|
35
35
|
import { getRepoStatus } from './utils/multi-git.js';
|
|
36
36
|
import { syncWorkspace } from './core/sync.js';
|
|
37
37
|
import { commitWorkspace } from './core/commit.js';
|
|
@@ -39,7 +39,7 @@ import { refreshWorkspace } from './core/refresh.js';
|
|
|
39
39
|
import { writeWorkspaceFile } from './core/storage.js';
|
|
40
40
|
import { readWorkspaceKnowledge, addWorkspaceKnowledge, addBaseKnowledge, } from './core/knowledge.js';
|
|
41
41
|
import { addSchedule, loadSchedules, nextDueAt, parseInterval, removeSchedule, runJob, setScheduleEnabled, startScheduler, } from './core/scheduler.js';
|
|
42
|
-
import { detectAllServices, detectOrchestrationTools, startServices, stopServices, loadRunningState, getPm2List, } from './orchestration/index.js';
|
|
42
|
+
import { detectAllServices, detectOrchestrationTools, startServices, stopServices, startService, stopService, restartService, startOrchestrator, stopOrchestrator, tailLogFile, loadRunningState, getPm2List, } from './orchestration/index.js';
|
|
43
43
|
import { checkForUpdates, getCurrentVersion, getToolsStatus } from './utils/update-check.js';
|
|
44
44
|
import { getWorkflowTemplates, saveWorkflowTemplate, deleteWorkflowTemplate } from './utils/workflows.js';
|
|
45
45
|
import { suggestWorkflow } from './utils/workflow-advisor.js';
|
|
@@ -263,35 +263,10 @@ app.get('/api/adapters', async (c) => {
|
|
|
263
263
|
return errorResponse(c, error);
|
|
264
264
|
}
|
|
265
265
|
});
|
|
266
|
-
function isSafeLocalEndpoint(urlStr) {
|
|
267
|
-
try {
|
|
268
|
-
const url = new URL(urlStr);
|
|
269
|
-
const hostname = url.hostname.toLowerCase();
|
|
270
|
-
if (url.protocol === 'https:') {
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') {
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
const ipv4Pattern = /^(?:10|127|192\.168|172\.(?:1[6-9]|2[0-9]|3[01]))\.\d+\.\d+\.\d+$/;
|
|
277
|
-
if (ipv4Pattern.test(hostname)) {
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
266
|
// 2. Save configuration
|
|
287
267
|
app.post('/api/config', async (c) => {
|
|
288
268
|
try {
|
|
289
269
|
const newConfig = await c.req.json();
|
|
290
|
-
if (newConfig?.localLlm?.enabled && newConfig.localLlm.endpoint) {
|
|
291
|
-
if (!isSafeLocalEndpoint(newConfig.localLlm.endpoint)) {
|
|
292
|
-
return c.json({ error: 'Local AI endpoint must be HTTPS, localhost, 127.0.0.1, or a private LAN IP.' }, 400);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
270
|
await saveConfig(newConfig);
|
|
296
271
|
return c.json({ success: true, config: newConfig });
|
|
297
272
|
}
|
|
@@ -342,6 +317,72 @@ app.post('/api/repos/new', async (c) => {
|
|
|
342
317
|
return errorResponse(c, error);
|
|
343
318
|
}
|
|
344
319
|
});
|
|
320
|
+
// 3c. Project registry — named groups of source repos that features start from.
|
|
321
|
+
app.get('/api/projects', async (c) => {
|
|
322
|
+
try {
|
|
323
|
+
return c.json(await loadProjects({ quiet: true }));
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
return errorResponse(c, error);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
app.post('/api/projects', async (c) => {
|
|
330
|
+
try {
|
|
331
|
+
const body = await c.req.json();
|
|
332
|
+
if (!body.name || typeof body.name !== 'string') {
|
|
333
|
+
return c.json({ error: 'Missing "name" in request body' }, 400);
|
|
334
|
+
}
|
|
335
|
+
if (!Array.isArray(body.repos) || body.repos.length === 0) {
|
|
336
|
+
return c.json({ error: 'Missing "repos" in request body' }, 400);
|
|
337
|
+
}
|
|
338
|
+
const config = await loadConfig();
|
|
339
|
+
// Only repos under devDir are offered by the scanner; refuse anything else.
|
|
340
|
+
const repos = body.repos.map((r) => assertWithin(config.devDir, r));
|
|
341
|
+
const project = await createProject(body.name, repos, body.description);
|
|
342
|
+
return c.json(project, 201);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
return errorResponse(c, error);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
app.put('/api/projects/:id', async (c) => {
|
|
349
|
+
try {
|
|
350
|
+
const body = await c.req.json();
|
|
351
|
+
let repoPaths;
|
|
352
|
+
if (body.repos !== undefined) {
|
|
353
|
+
if (!Array.isArray(body.repos) || body.repos.length === 0) {
|
|
354
|
+
return c.json({ error: '"repos" must be a non-empty array' }, 400);
|
|
355
|
+
}
|
|
356
|
+
const config = await loadConfig();
|
|
357
|
+
repoPaths = body.repos.map((r) => assertWithin(config.devDir, r));
|
|
358
|
+
}
|
|
359
|
+
const project = await updateProject(c.req.param('id'), {
|
|
360
|
+
name: body.name,
|
|
361
|
+
description: body.description,
|
|
362
|
+
repoPaths,
|
|
363
|
+
});
|
|
364
|
+
return c.json(project);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
if (error instanceof Error && error.message.startsWith('No project')) {
|
|
368
|
+
return c.json({ error: error.message }, 404);
|
|
369
|
+
}
|
|
370
|
+
return errorResponse(c, error);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
// Registry-only delete — never touches repos or workspaces on disk.
|
|
374
|
+
app.delete('/api/projects/:id', async (c) => {
|
|
375
|
+
try {
|
|
376
|
+
const removed = await removeProject(c.req.param('id'));
|
|
377
|
+
if (!removed) {
|
|
378
|
+
return c.json({ error: `No project with id "${c.req.param('id')}"` }, 404);
|
|
379
|
+
}
|
|
380
|
+
return c.json({ success: true });
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
return errorResponse(c, error);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
345
386
|
// 4. List existing workspaces
|
|
346
387
|
app.get('/api/workspaces', async (c) => {
|
|
347
388
|
try {
|
|
@@ -397,9 +438,10 @@ app.get('/api/workspaces/status', async (c) => {
|
|
|
397
438
|
pendingValidation: false,
|
|
398
439
|
};
|
|
399
440
|
try {
|
|
400
|
-
// Uncommitted changes across the workspace's
|
|
441
|
+
// Uncommitted changes across the workspace's repos: worktrees inside
|
|
442
|
+
// the workspace dir, or the source repos themselves for in-place.
|
|
401
443
|
for (const repoPath of ws.repos) {
|
|
402
|
-
const worktreePath =
|
|
444
|
+
const worktreePath = resolveFeatureRepoPath(ws, workspacePath, repoPath);
|
|
403
445
|
const repoStatus = await getRepoStatus(worktreePath);
|
|
404
446
|
if (repoStatus.hasChanges) {
|
|
405
447
|
status.dirtyRepos += 1;
|
|
@@ -409,7 +451,8 @@ app.get('/api/workspaces/status', async (c) => {
|
|
|
409
451
|
// Running services (cached running-state, PM2-verified — same source as
|
|
410
452
|
// the Services tab; only workspaces that ever started services touch PM2).
|
|
411
453
|
const runningState = await loadRunningState(workspacePath, pm2List);
|
|
412
|
-
status.runningServices =
|
|
454
|
+
status.runningServices =
|
|
455
|
+
(runningState?.services?.length ?? 0) + (runningState?.orchestrators?.length ?? 0);
|
|
413
456
|
// Sync state: worst-case classification + any repo pending validation.
|
|
414
457
|
const wsState = await loadWorkspaceState(workspacePath);
|
|
415
458
|
const repoStates = Object.values(wsState.repos);
|
|
@@ -452,63 +495,6 @@ app.get('/api/editor-detect', async (c) => {
|
|
|
452
495
|
return errorResponse(c, error);
|
|
453
496
|
}
|
|
454
497
|
});
|
|
455
|
-
// 6.5. Local LLM test & recommendation
|
|
456
|
-
app.post('/api/local-llm/test', async (c) => {
|
|
457
|
-
try {
|
|
458
|
-
const { provider, endpoint, model, apiKey, shoot } = await c.req.json();
|
|
459
|
-
if (!endpoint || !isSafeLocalEndpoint(endpoint)) {
|
|
460
|
-
return c.json({ success: false, error: 'Local AI endpoint must be HTTPS, localhost, 127.0.0.1, or a private LAN IP.' }, 400);
|
|
461
|
-
}
|
|
462
|
-
const cleanEndpoint = endpoint.replace(/\/$/, '');
|
|
463
|
-
if (shoot) {
|
|
464
|
-
const responseText = await callLocalLlm({ enabled: true, provider, endpoint, model, apiKey }, [{ role: 'user', content: 'Respond with the exact word "OK" and nothing else.' }]);
|
|
465
|
-
const cleanResponse = responseText.trim();
|
|
466
|
-
return c.json({
|
|
467
|
-
success: true,
|
|
468
|
-
modelReady: true,
|
|
469
|
-
message: `Inference test succeeded! Response from model: "${cleanResponse}"`
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
if (provider === 'ollama') {
|
|
473
|
-
const res = await fetch(`${cleanEndpoint}/api/tags`);
|
|
474
|
-
if (!res.ok)
|
|
475
|
-
throw new Error(`Ollama responded with status ${res.status}`);
|
|
476
|
-
const data = await res.json();
|
|
477
|
-
const models = data?.models || [];
|
|
478
|
-
const isModelLoaded = isOllamaModelAvailable(models, model);
|
|
479
|
-
return c.json({
|
|
480
|
-
success: true,
|
|
481
|
-
modelReady: isModelLoaded,
|
|
482
|
-
message: isModelLoaded ? 'Connected successfully! Model is ready.' : `Connected successfully, but model "${model}" is not pulled. Run "ollama pull ${model}" to install it.`
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
const testUrl = getOpenAiCompatibleUrl(cleanEndpoint, '/v1/models');
|
|
487
|
-
const res = await fetch(testUrl);
|
|
488
|
-
if (!res.ok)
|
|
489
|
-
throw new Error(`OpenAI-compatible server responded with status ${res.status}`);
|
|
490
|
-
return c.json({ success: true, modelReady: true, message: 'Connected successfully to OpenAI-compatible server!' });
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
catch (error) {
|
|
494
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
495
|
-
return c.json({ success: false, error: msg }, 400);
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
app.get('/api/local-llm/recommend', async (c) => {
|
|
499
|
-
try {
|
|
500
|
-
const specs = await scanSystemSpecs();
|
|
501
|
-
return c.json(specs);
|
|
502
|
-
}
|
|
503
|
-
catch (error) {
|
|
504
|
-
return c.json({
|
|
505
|
-
totalRamGb: 8,
|
|
506
|
-
gpuName: 'Unknown/Integrated',
|
|
507
|
-
hasHardwareAcceleration: false,
|
|
508
|
-
recommendedModel: 'qwen2.5-coder:1.5b',
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
498
|
const creationJobs = new Map();
|
|
513
499
|
// Keep a finished job around briefly so a late SSE reconnect can still read its
|
|
514
500
|
// final state, then drop it so the map doesn't grow unbounded.
|
|
@@ -565,7 +551,11 @@ function updateJobStep(jobId, stepId, status, message, extraData) {
|
|
|
565
551
|
}
|
|
566
552
|
async function runCreationJob(jobId, body, config) {
|
|
567
553
|
try {
|
|
568
|
-
const
|
|
554
|
+
const inPlace = body.mode === 'in-place';
|
|
555
|
+
// Workspace id doubles as the directory name: the branch for worktree
|
|
556
|
+
// mode, the (slugified) workspace name for in-place mode.
|
|
557
|
+
const workspaceId = inPlace ? jobId : body.branchName;
|
|
558
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, workspaceId);
|
|
569
559
|
const job = creationJobs.get(jobId);
|
|
570
560
|
if (job) {
|
|
571
561
|
job.workspacePath = workspacePath;
|
|
@@ -578,32 +568,40 @@ async function runCreationJob(jobId, body, config) {
|
|
|
578
568
|
}
|
|
579
569
|
}
|
|
580
570
|
const feature = {
|
|
581
|
-
id:
|
|
582
|
-
|
|
571
|
+
id: workspaceId,
|
|
572
|
+
mode: inPlace ? 'in-place' : 'worktree',
|
|
573
|
+
projectId: body.projectId,
|
|
574
|
+
// In-place features never create a branch; keeping branchName populated
|
|
575
|
+
// (= id) avoids breaking every consumer of the non-optional field.
|
|
576
|
+
branchName: inPlace ? workspaceId : body.branchName,
|
|
583
577
|
description: body.description,
|
|
584
|
-
repos:
|
|
578
|
+
repos: inPlace
|
|
579
|
+
? body.repos.map((r) => r.path)
|
|
580
|
+
: body.repos.map((r) => path.join(workspacePath, r.name)),
|
|
585
581
|
originalRepos: body.repos.map((r) => r.path),
|
|
586
|
-
repoBranches: Object.keys(repoBranches).length > 0 ? repoBranches : undefined,
|
|
582
|
+
repoBranches: !inPlace && Object.keys(repoBranches).length > 0 ? repoBranches : undefined,
|
|
587
583
|
assistants: body.assistants,
|
|
588
584
|
workspacePath,
|
|
589
585
|
createdAt: new Date().toISOString(),
|
|
590
586
|
resumption: body.resumption,
|
|
591
|
-
localLlmEnabled: body.localLlmEnabled,
|
|
592
587
|
teamworkInstructions: body.teamworkInstructions,
|
|
593
588
|
};
|
|
594
589
|
if (job) {
|
|
595
590
|
job.feature = feature;
|
|
596
591
|
}
|
|
597
|
-
// Step 1:
|
|
598
|
-
|
|
592
|
+
// Step 1: Materialize the workspace (worktrees, or just the lightweight
|
|
593
|
+
// dir). One stable step id for both modes — only the wording differs.
|
|
594
|
+
updateJobStep(jobId, 'workspace', 'running', inPlace ? 'Registering workspace...' : 'Creating git worktrees...');
|
|
599
595
|
await createWorkspace(feature, body.repos);
|
|
600
|
-
updateJobStep(jobId, '
|
|
601
|
-
// Step 2: Analyze repos
|
|
596
|
+
updateJobStep(jobId, 'workspace', 'completed', inPlace ? 'Workspace registered — working in-place in the source repos.' : 'Git worktrees created successfully.');
|
|
597
|
+
// Step 2: Analyze repos — against the worktrees, or the source repos in-place.
|
|
602
598
|
updateJobStep(jobId, 'analysis', 'running', 'Analyzing projects and dependencies...');
|
|
603
|
-
const workspaceRepos =
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
599
|
+
const workspaceRepos = inPlace
|
|
600
|
+
? body.repos
|
|
601
|
+
: body.repos.map((repo) => ({
|
|
602
|
+
...repo,
|
|
603
|
+
path: path.join(workspacePath, repo.name),
|
|
604
|
+
}));
|
|
607
605
|
const analysis = await analyzeAllRepos(workspaceRepos);
|
|
608
606
|
updateJobStep(jobId, 'analysis', 'completed', 'Project analysis complete.');
|
|
609
607
|
// Step 3: Generate AI context files
|
|
@@ -612,7 +610,6 @@ async function runCreationJob(jobId, body, config) {
|
|
|
612
610
|
feature,
|
|
613
611
|
repos: workspaceRepos,
|
|
614
612
|
analysis,
|
|
615
|
-
localLlm: config.localLlm,
|
|
616
613
|
};
|
|
617
614
|
await generateContextFiles(ctx, body.assistants, workspacePath);
|
|
618
615
|
updateJobStep(jobId, 'context', 'completed', 'AI context files generated.');
|
|
@@ -623,7 +620,7 @@ async function runCreationJob(jobId, body, config) {
|
|
|
623
620
|
const job = creationJobs.get(jobId);
|
|
624
621
|
if (job) {
|
|
625
622
|
const runningStep = job.steps.find((s) => s.status === 'running');
|
|
626
|
-
const failedStepId = runningStep ? runningStep.id : '
|
|
623
|
+
const failedStepId = runningStep ? runningStep.id : 'workspace';
|
|
627
624
|
updateJobStep(jobId, failedStepId, 'failed', msg);
|
|
628
625
|
}
|
|
629
626
|
}
|
|
@@ -632,8 +629,19 @@ async function runCreationJob(jobId, body, config) {
|
|
|
632
629
|
app.post('/api/workspace', async (c) => {
|
|
633
630
|
try {
|
|
634
631
|
const body = await c.req.json();
|
|
632
|
+
const inPlace = body.mode === 'in-place';
|
|
633
|
+
if (inPlace && !body.name?.trim()) {
|
|
634
|
+
return c.json({ error: 'In-place workspaces need a "name"' }, 400);
|
|
635
|
+
}
|
|
636
|
+
if (!inPlace && !body.branchName) {
|
|
637
|
+
return c.json({ error: 'Missing "branchName" in request body' }, 400);
|
|
638
|
+
}
|
|
635
639
|
const config = await loadConfig();
|
|
636
|
-
|
|
640
|
+
// The job id doubles as the workspace directory name.
|
|
641
|
+
const jobId = inPlace ? slugifyProjectName(body.name) : body.branchName;
|
|
642
|
+
if (!jobId) {
|
|
643
|
+
return c.json({ error: `Workspace name "${body.name}" contains no usable characters` }, 400);
|
|
644
|
+
}
|
|
637
645
|
if (creationJobs.has(jobId)) {
|
|
638
646
|
const existing = creationJobs.get(jobId);
|
|
639
647
|
if (existing.status === 'running') {
|
|
@@ -641,7 +649,9 @@ app.post('/api/workspace', async (c) => {
|
|
|
641
649
|
}
|
|
642
650
|
}
|
|
643
651
|
const steps = [
|
|
644
|
-
|
|
652
|
+
// Stable id 'workspace' in both modes so progress consumers never need
|
|
653
|
+
// to know the mode; only the display name differs.
|
|
654
|
+
{ id: 'workspace', name: inPlace ? 'Register Workspace' : 'Create Git Worktrees', status: 'pending', message: 'Waiting...' },
|
|
645
655
|
{ id: 'analysis', name: 'Analyze Repositories', status: 'pending', message: 'Waiting...' },
|
|
646
656
|
{ id: 'context', name: 'Generate AI Context Files', status: 'pending', message: 'Waiting...' },
|
|
647
657
|
];
|
|
@@ -710,10 +720,27 @@ app.get('/api/workspace/create-stream/:jobId', async (c) => {
|
|
|
710
720
|
}
|
|
711
721
|
};
|
|
712
722
|
job.listeners.add(listener);
|
|
713
|
-
// Guard against the job finishing between the initial state write and
|
|
723
|
+
// Guard against the job finishing between the initial state write and
|
|
724
|
+
// now: the client only saw a 'running' frame, so send the terminal
|
|
725
|
+
// state before closing — silently ending the stream here would leave
|
|
726
|
+
// the client believing the connection dropped mid-run.
|
|
714
727
|
if (job.status === 'completed' || job.status === 'failed') {
|
|
715
728
|
job.listeners.delete(listener);
|
|
716
|
-
|
|
729
|
+
stream
|
|
730
|
+
.writeSSE({
|
|
731
|
+
event: 'progress',
|
|
732
|
+
data: JSON.stringify({
|
|
733
|
+
id: job.id,
|
|
734
|
+
status: job.status,
|
|
735
|
+
progress: job.progress,
|
|
736
|
+
error: job.error,
|
|
737
|
+
steps: job.steps,
|
|
738
|
+
workspacePath: job.workspacePath,
|
|
739
|
+
feature: job.feature,
|
|
740
|
+
}),
|
|
741
|
+
})
|
|
742
|
+
.catch(() => { })
|
|
743
|
+
.finally(() => resolve());
|
|
717
744
|
}
|
|
718
745
|
});
|
|
719
746
|
});
|
|
@@ -750,7 +777,7 @@ app.post('/api/workspace/suggest-workflow', async (c) => {
|
|
|
750
777
|
try {
|
|
751
778
|
const { description, repos } = await c.req.json();
|
|
752
779
|
const config = await loadConfig();
|
|
753
|
-
const suggestion = await suggestWorkflow(description, repos
|
|
780
|
+
const suggestion = await suggestWorkflow(description, repos);
|
|
754
781
|
return c.json({
|
|
755
782
|
success: true,
|
|
756
783
|
...suggestion
|
|
@@ -811,20 +838,22 @@ app.get('/api/workspace/:id/services', async (c) => {
|
|
|
811
838
|
services,
|
|
812
839
|
orchestrationTools: tools,
|
|
813
840
|
runningState: runningState?.services || [],
|
|
841
|
+
runningOrchestrators: runningState?.orchestrators || [],
|
|
814
842
|
});
|
|
815
843
|
}
|
|
816
844
|
catch (error) {
|
|
817
845
|
return errorResponse(c, error);
|
|
818
846
|
}
|
|
819
847
|
});
|
|
820
|
-
// 10. Start services in workspace
|
|
848
|
+
// 10. Start services in workspace. Configs are re-detected server-side —
|
|
849
|
+
// the client only says "start", never what to execute.
|
|
821
850
|
app.post('/api/workspace/:id/services/start', async (c) => {
|
|
822
851
|
try {
|
|
823
852
|
const id = c.req.param('id');
|
|
824
|
-
const { services } = await c.req.json();
|
|
825
853
|
const config = await loadConfig();
|
|
826
854
|
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
827
855
|
const logDir = path.join(workspacePath, '.nexusflow-logs');
|
|
856
|
+
const services = await detectAllServices(workspacePath);
|
|
828
857
|
await startServices(services, workspacePath, logDir);
|
|
829
858
|
return c.json({ success: true });
|
|
830
859
|
}
|
|
@@ -845,19 +874,53 @@ app.post('/api/workspace/:id/services/stop', async (c) => {
|
|
|
845
874
|
return errorResponse(c, error);
|
|
846
875
|
}
|
|
847
876
|
});
|
|
848
|
-
//
|
|
877
|
+
// 11b. Per-service start / stop / restart. The service config is re-detected
|
|
878
|
+
// by name server-side; the client never supplies a command.
|
|
879
|
+
app.post('/api/workspace/:id/services/:serviceName/:action{start|stop|restart}', async (c) => {
|
|
880
|
+
try {
|
|
881
|
+
const id = c.req.param('id');
|
|
882
|
+
const serviceName = decodeURIComponent(c.req.param('serviceName'));
|
|
883
|
+
const action = c.req.param('action');
|
|
884
|
+
const config = await loadConfig();
|
|
885
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
886
|
+
const logDir = path.join(workspacePath, '.nexusflow-logs');
|
|
887
|
+
if (action === 'stop') {
|
|
888
|
+
const stopped = await stopService(workspacePath, serviceName);
|
|
889
|
+
return c.json({ success: true, stopped });
|
|
890
|
+
}
|
|
891
|
+
const services = await detectAllServices(workspacePath);
|
|
892
|
+
const service = services.find((s) => s.name === serviceName);
|
|
893
|
+
if (!service) {
|
|
894
|
+
return c.json({ error: `Unknown service "${serviceName}" in this workspace.` }, 404);
|
|
895
|
+
}
|
|
896
|
+
const running = action === 'restart'
|
|
897
|
+
? await restartService(service, workspacePath, logDir)
|
|
898
|
+
: await startService(service, workspacePath, logDir);
|
|
899
|
+
return c.json({ success: running !== null, service: running });
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
return errorResponse(c, error);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
/** Resolve + contain a service log path; names may contain '/' (repo/sub). */
|
|
906
|
+
function resolveServiceLogFile(workspacePath, serviceName) {
|
|
907
|
+
const logDir = path.join(workspacePath, '.nexusflow-logs');
|
|
908
|
+
return assertWithin(logDir, path.join(logDir, `${serviceName}.log`));
|
|
909
|
+
}
|
|
910
|
+
// 12. Get service logs (backfill). Returns the trailing 50KB and the byte
|
|
911
|
+
// offset the read ended at, so the SSE stream can resume exactly there.
|
|
849
912
|
app.get('/api/workspace/:id/services/logs/:serviceName', async (c) => {
|
|
850
913
|
try {
|
|
851
914
|
const id = c.req.param('id');
|
|
852
|
-
const serviceName = c.req.param('serviceName');
|
|
915
|
+
const serviceName = decodeURIComponent(c.req.param('serviceName'));
|
|
853
916
|
const config = await loadConfig();
|
|
854
917
|
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
855
|
-
const logFile =
|
|
918
|
+
const logFile = resolveServiceLogFile(workspacePath, serviceName);
|
|
856
919
|
let content = '';
|
|
920
|
+
let size = 0;
|
|
857
921
|
try {
|
|
858
|
-
// Read last 200 lines or 50KB
|
|
859
922
|
const stats = await fs.stat(logFile);
|
|
860
|
-
|
|
923
|
+
size = stats.size;
|
|
861
924
|
const start = Math.max(0, size - 50000);
|
|
862
925
|
const fd = await fs.open(logFile, 'r');
|
|
863
926
|
const buffer = Buffer.alloc(size - start);
|
|
@@ -868,7 +931,82 @@ app.get('/api/workspace/:id/services/logs/:serviceName', async (c) => {
|
|
|
868
931
|
catch {
|
|
869
932
|
content = 'No logs available yet.';
|
|
870
933
|
}
|
|
871
|
-
return c.json({ logs: content });
|
|
934
|
+
return c.json({ logs: content, size });
|
|
935
|
+
}
|
|
936
|
+
catch (error) {
|
|
937
|
+
return errorResponse(c, error);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
// 12b. Live log stream (SSE): tails the log file from ?offset onward,
|
|
941
|
+
// emitting 'log' events with JSON-encoded chunks (raw SSE frames would mangle
|
|
942
|
+
// embedded newlines). A 15s ping keeps idle streams alive.
|
|
943
|
+
app.get('/api/workspace/:id/services/logs/:serviceName/stream', async (c) => {
|
|
944
|
+
// Resolve + validate the log path BEFORE opening the stream so a bad name
|
|
945
|
+
// (e.g. a traversal attempt) returns a clean error, mirroring the backfill
|
|
946
|
+
// route, rather than a raw 500 from an uncaught throw.
|
|
947
|
+
let logFile;
|
|
948
|
+
let startOffset;
|
|
949
|
+
try {
|
|
950
|
+
const id = c.req.param('id');
|
|
951
|
+
const serviceName = decodeURIComponent(c.req.param('serviceName'));
|
|
952
|
+
const config = await loadConfig();
|
|
953
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
954
|
+
logFile = resolveServiceLogFile(workspacePath, serviceName);
|
|
955
|
+
const offsetParam = Number.parseInt(c.req.query('offset') ?? '', 10);
|
|
956
|
+
startOffset = Number.isFinite(offsetParam) && offsetParam >= 0 ? offsetParam : undefined;
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
return errorResponse(c, error);
|
|
960
|
+
}
|
|
961
|
+
c.header('Content-Type', 'text/event-stream');
|
|
962
|
+
c.header('Cache-Control', 'no-cache');
|
|
963
|
+
c.header('Connection', 'keep-alive');
|
|
964
|
+
return streamSSE(c, async (stream) => {
|
|
965
|
+
await stream.writeSSE({ event: 'init', data: JSON.stringify({ offset: startOffset ?? null }) });
|
|
966
|
+
await new Promise((resolve) => {
|
|
967
|
+
let done = false;
|
|
968
|
+
const cleanup = () => {
|
|
969
|
+
if (done)
|
|
970
|
+
return;
|
|
971
|
+
done = true;
|
|
972
|
+
tail.stop();
|
|
973
|
+
clearInterval(heartbeat);
|
|
974
|
+
resolve();
|
|
975
|
+
};
|
|
976
|
+
const tail = tailLogFile(logFile, (chunk) => {
|
|
977
|
+
stream.writeSSE({ event: 'log', data: JSON.stringify({ chunk }) }).catch(cleanup);
|
|
978
|
+
}, { startOffset });
|
|
979
|
+
const heartbeat = setInterval(() => {
|
|
980
|
+
stream.writeSSE({ event: 'ping', data: '{}' }).catch(cleanup);
|
|
981
|
+
}, 15_000);
|
|
982
|
+
stream.onAbort(cleanup);
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
// 12c. Orchestration tools: start/stop by detection id only — the tool is
|
|
987
|
+
// re-detected server-side and the client can never supply a command.
|
|
988
|
+
app.post('/api/workspace/:id/orchestrators/:action{start|stop}', async (c) => {
|
|
989
|
+
try {
|
|
990
|
+
const id = c.req.param('id');
|
|
991
|
+
const action = c.req.param('action');
|
|
992
|
+
const body = await c.req.json();
|
|
993
|
+
if (!body.id || typeof body.id !== 'string') {
|
|
994
|
+
return c.json({ error: 'Missing orchestrator "id" in request body' }, 400);
|
|
995
|
+
}
|
|
996
|
+
const config = await loadConfig();
|
|
997
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
998
|
+
const logDir = path.join(workspacePath, '.nexusflow-logs');
|
|
999
|
+
const tools = await detectOrchestrationTools(workspacePath);
|
|
1000
|
+
const detection = tools.find((t) => t.id === body.id);
|
|
1001
|
+
if (!detection) {
|
|
1002
|
+
return c.json({ error: `Unknown orchestration tool "${body.id}" in this workspace.` }, 404);
|
|
1003
|
+
}
|
|
1004
|
+
if (action === 'start') {
|
|
1005
|
+
const running = await startOrchestrator(detection, workspacePath, logDir);
|
|
1006
|
+
return c.json({ success: true, orchestrator: running });
|
|
1007
|
+
}
|
|
1008
|
+
await stopOrchestrator(detection, workspacePath);
|
|
1009
|
+
return c.json({ success: true });
|
|
872
1010
|
}
|
|
873
1011
|
catch (error) {
|
|
874
1012
|
return errorResponse(c, error);
|
|
@@ -886,10 +1024,10 @@ app.get('/api/workspace/:id/changes', async (c) => {
|
|
|
886
1024
|
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
887
1025
|
}
|
|
888
1026
|
const results = [];
|
|
889
|
-
// Check git status in each repo
|
|
1027
|
+
// Check git status in each repo (worktree, or source repo for in-place)
|
|
890
1028
|
for (const repoPath of feature.repos) {
|
|
891
1029
|
const repoName = path.basename(repoPath);
|
|
892
|
-
const worktreePath =
|
|
1030
|
+
const worktreePath = resolveFeatureRepoPath(feature, workspacePath, repoPath);
|
|
893
1031
|
try {
|
|
894
1032
|
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: worktreePath });
|
|
895
1033
|
const lines = stdout.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
@@ -963,8 +1101,25 @@ app.get('/api/workspace/:id/changes/diff', async (c) => {
|
|
|
963
1101
|
}
|
|
964
1102
|
const config = await loadConfig();
|
|
965
1103
|
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
966
|
-
//
|
|
967
|
-
|
|
1104
|
+
// Resolve by mode, manifest first: an in-place repo name may only resolve
|
|
1105
|
+
// to an exact manifest entry (its repos live outside the workspace), and
|
|
1106
|
+
// must never be shadowed by a stray same-named subdirectory inside the
|
|
1107
|
+
// workspace dir. Worktree mode keeps the path-containment guard.
|
|
1108
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
1109
|
+
let worktreePath;
|
|
1110
|
+
if (feature && isInPlace(feature)) {
|
|
1111
|
+
const matches = feature.repos.filter((r) => path.basename(r) === repoName);
|
|
1112
|
+
if (matches.length === 0) {
|
|
1113
|
+
return c.json({ error: `Unknown repo "${repoName}" in this workspace.` }, 404);
|
|
1114
|
+
}
|
|
1115
|
+
if (matches.length > 1) {
|
|
1116
|
+
return c.json({ error: `Repo name "${repoName}" is ambiguous in this workspace.` }, 400);
|
|
1117
|
+
}
|
|
1118
|
+
worktreePath = matches[0];
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
worktreePath = resolveRepoPath(workspacePath, repoName);
|
|
1122
|
+
}
|
|
968
1123
|
let diff = '';
|
|
969
1124
|
// Check git status for the file to know if it's untracked
|
|
970
1125
|
try {
|
|
@@ -1196,7 +1351,9 @@ app.post('/api/workspace/:id/resume', async (c) => {
|
|
|
1196
1351
|
console.error(`Failed to launch editor ${body.command} for path ${workspacePath}:`, err);
|
|
1197
1352
|
});
|
|
1198
1353
|
}
|
|
1199
|
-
|
|
1354
|
+
// Where the resume command should be run: the workspace dir, or the repo
|
|
1355
|
+
// root for single-repo in-place features.
|
|
1356
|
+
return c.json({ success: true, resumeCommand, workspacePath, sessionCwd: getSessionCwd(feature) });
|
|
1200
1357
|
}
|
|
1201
1358
|
catch (error) {
|
|
1202
1359
|
return errorResponse(c, error);
|
|
@@ -1325,9 +1482,14 @@ app.post('/api/updates/apply', async (c) => {
|
|
|
1325
1482
|
try {
|
|
1326
1483
|
const isWin = process.platform === 'win32';
|
|
1327
1484
|
if (isWin) {
|
|
1328
|
-
//
|
|
1485
|
+
// The desktop app ships an electron-builder NSIS installer (build target
|
|
1486
|
+
// 'nsis'), whose silent-install switch is `/S` — NOT Inno Setup's
|
|
1487
|
+
// /VERYSILENT. With the wrong flags the one-click installer waits for UI
|
|
1488
|
+
// that a detached (stdio: 'ignore') process can never provide, so the
|
|
1489
|
+
// update silently fails to apply. `/S` runs it unattended and relaunches
|
|
1490
|
+
// the app on finish (electron-builder default).
|
|
1329
1491
|
console.log(`Applying update: Spawning detached installer at: ${downloadedInstallerPath}`);
|
|
1330
|
-
const child = spawn(downloadedInstallerPath, ['/
|
|
1492
|
+
const child = spawn(downloadedInstallerPath, ['/S'], {
|
|
1331
1493
|
detached: true,
|
|
1332
1494
|
stdio: 'ignore',
|
|
1333
1495
|
});
|