@mrpatronz/nexusflow 2.4.0 → 2.5.0
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/isolate.d.ts +12 -0
- package/dist/commands/isolate.d.ts.map +1 -0
- package/dist/commands/isolate.js +78 -0
- package/dist/commands/isolate.js.map +1 -0
- package/dist/commands/isolate.test.d.ts +2 -0
- package/dist/commands/isolate.test.d.ts.map +1 -0
- package/dist/commands/isolate.test.js +59 -0
- package/dist/commands/isolate.test.js.map +1 -0
- package/dist/commands/open.d.ts.map +1 -1
- package/dist/commands/open.js +66 -21
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/ui.d.ts +1 -1
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +26 -6
- package/dist/commands/ui.js.map +1 -1
- package/dist/core/config-schema.d.ts +17 -0
- package/dist/core/config-schema.d.ts.map +1 -0
- package/dist/core/config-schema.js +24 -0
- package/dist/core/config-schema.js.map +1 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +19 -5
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/isolate.d.ts +31 -0
- package/dist/core/isolate.d.ts.map +1 -0
- package/dist/core/isolate.js +175 -0
- package/dist/core/isolate.js.map +1 -0
- package/dist/core/isolate.test.d.ts +2 -0
- package/dist/core/isolate.test.d.ts.map +1 -0
- package/dist/core/isolate.test.js +290 -0
- package/dist/core/isolate.test.js.map +1 -0
- package/dist/core/refresh.d.ts.map +1 -1
- package/dist/core/refresh.js +35 -1
- package/dist/core/refresh.js.map +1 -1
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/core/sync.js +21 -7
- package/dist/core/sync.js.map +1 -1
- package/dist/core/workspace.d.ts +6 -0
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +61 -17
- package/dist/core/workspace.js.map +1 -1
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +7 -0
- package/dist/core/worktree.js.map +1 -1
- package/dist/desktop-server.js +2 -1
- package/dist/desktop-server.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +13 -5
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/base.test.js +1 -1
- package/dist/generators/base.test.js.map +1 -1
- package/dist/gui/antigravity-logo.png +0 -0
- package/dist/gui/assets/AgentsPage-BNHR9qux.js +3 -0
- package/dist/gui/assets/CSPContext-BG7no5S_.js +1 -0
- package/dist/gui/assets/DashboardPage-BScH3Cxp.js +1 -0
- package/dist/gui/assets/{GettingStartedPage-DQ5kCj0q.js → GettingStartedPage-AWlI-176.js} +1 -1
- package/dist/gui/assets/ProjectsPage-Bojj7gxI.js +1 -0
- package/dist/gui/assets/ScaffoldRepoInline-DJMbeHVM.js +1 -0
- package/dist/gui/assets/SettingsPage-BAdXO8X0.js +1 -0
- package/dist/gui/assets/SkillsPage-CF6Db1FA.js +6 -0
- package/dist/gui/assets/StartWorkPage-BQid3uIw.js +1 -0
- package/dist/gui/assets/{StrategiesPage-CkpJI4Q3.js → StrategiesPage-nGbR9TAc.js} +2 -2
- package/dist/gui/assets/WorkspacesPage-7XRtVJNF.js +2 -0
- package/dist/gui/assets/{alert-dialog-Cjwogh-W.js → alert-dialog-67uzXJi4.js} +1 -1
- package/dist/gui/assets/antigravity-logo-DRKQga2U.png +0 -0
- package/dist/gui/assets/{badge-BnkhqVEF.js → badge-CAohJGj6.js} +1 -1
- package/dist/gui/assets/{bot-wH0FKlic.js → bot-C2HWrAft.js} +1 -1
- package/dist/gui/assets/{button-cPldH5S4.js → button-BDDZnIHx.js} +1 -1
- package/dist/gui/assets/card-BKXzd_5L.js +1 -0
- package/dist/gui/assets/chevron-down-qTL8HPug.js +1 -0
- package/dist/gui/assets/{circle-plus-C0PNc7h4.js → circle-plus-lmU1yNJd.js} +1 -1
- package/dist/gui/assets/code-xml-jFAa3-TT.js +1 -0
- package/dist/gui/assets/{cpu-CPUNnHRM.js → cpu-Cdwd84Jz.js} +1 -1
- package/dist/gui/assets/{empty-DWyDwDYB.js → empty-BdlgbifD.js} +1 -1
- package/dist/gui/assets/{git-branch-Dw55pQWa.js → git-branch-Cfj9D8yW.js} +1 -1
- package/dist/gui/assets/index-CZoZQkeC.css +2 -0
- package/dist/gui/assets/index-V1dDX1O_.js +50 -0
- package/dist/gui/assets/isElementDisabled-D_fu2zfc.js +1 -0
- package/dist/gui/assets/{label-JFgMNsTn.js → label-CG5exjag.js} +1 -1
- package/dist/gui/assets/{terminal-D1awFRPr.js → square-DlIYnlSq.js} +1 -1
- package/dist/gui/assets/store-D7ixdVck.js +3 -0
- package/dist/gui/assets/styles-CVwNuFCx.js +1 -0
- package/dist/gui/assets/textarea-BNRuIXNs.js +1 -0
- package/dist/gui/assets/{trash-2-Da8F2gKd.js → trash-2-CtR35_eF.js} +1 -1
- package/dist/gui/assets/useAriaLabelledBy-DfRdwH8a.js +1 -0
- package/dist/gui/assets/useValueChanged-CStSGSsD.js +1 -0
- package/dist/gui/assets/vsc-Di05BTNr.js +1 -0
- package/dist/gui/assets/zap-CWG_Lvki.js +1 -0
- package/dist/gui/index.html +9 -7
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +47 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/tools.test.d.ts +2 -0
- package/dist/mcp/tools.test.d.ts.map +1 -0
- package/dist/mcp/tools.test.js +70 -0
- package/dist/mcp/tools.test.js.map +1 -0
- package/dist/orchestration/orchestrator.js +2 -2
- package/dist/orchestration/orchestrator.js.map +1 -1
- package/dist/orchestration/orchestrator.test.js +1 -1
- package/dist/orchestration/orchestrator.test.js.map +1 -1
- package/dist/orchestration/runner.d.ts +5 -1
- package/dist/orchestration/runner.d.ts.map +1 -1
- package/dist/orchestration/runner.js +14 -4
- package/dist/orchestration/runner.js.map +1 -1
- package/dist/orchestration/runner.test.js +22 -16
- package/dist/orchestration/runner.test.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +189 -27
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +220 -7
- package/dist/server.test.js.map +1 -1
- package/dist/types.d.ts +17 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/detect-editors.d.ts +1 -7
- package/dist/utils/detect-editors.d.ts.map +1 -1
- package/dist/utils/detect-editors.js +12 -10
- package/dist/utils/detect-editors.js.map +1 -1
- package/dist/utils/detect-editors.test.js +58 -25
- package/dist/utils/detect-editors.test.js.map +1 -1
- package/dist/utils/feature.d.ts +10 -1
- package/dist/utils/feature.d.ts.map +1 -1
- package/dist/utils/feature.js +36 -4
- package/dist/utils/feature.js.map +1 -1
- package/dist/utils/open-editor.d.ts +1 -1
- package/dist/utils/open-editor.d.ts.map +1 -1
- package/dist/utils/open-editor.js +11 -2
- package/dist/utils/open-editor.js.map +1 -1
- package/dist/utils/session-finder.d.ts +11 -0
- package/dist/utils/session-finder.d.ts.map +1 -1
- package/dist/utils/session-finder.js +121 -48
- package/dist/utils/session-finder.js.map +1 -1
- package/dist/utils/session-finder.test.js +105 -1
- package/dist/utils/session-finder.test.js.map +1 -1
- package/dist/utils/terminal-launch.d.ts +39 -0
- package/dist/utils/terminal-launch.d.ts.map +1 -0
- package/dist/utils/terminal-launch.js +205 -0
- package/dist/utils/terminal-launch.js.map +1 -0
- package/dist/utils/terminal-launch.test.d.ts +2 -0
- package/dist/utils/terminal-launch.test.d.ts.map +1 -0
- package/dist/utils/terminal-launch.test.js +117 -0
- package/dist/utils/terminal-launch.test.js.map +1 -0
- package/dist/utils/workspace-launch.d.ts.map +1 -1
- package/dist/utils/workspace-launch.js +3 -1
- package/dist/utils/workspace-launch.js.map +1 -1
- package/package.json +5 -3
- package/dist/gui/assets/AgentsPage-B_3Vqn2y.js +0 -3
- package/dist/gui/assets/DashboardPage-W7KE2gn1.js +0 -1
- package/dist/gui/assets/ProjectsPage-CrZhKj85.js +0 -1
- package/dist/gui/assets/ScaffoldRepoInline-BSpQbVe7.js +0 -1
- package/dist/gui/assets/Separator-Chx6O62Y.js +0 -1
- package/dist/gui/assets/SettingsPage-Zvq-P6Hj.js +0 -1
- package/dist/gui/assets/SkillsPage-DR-Y5ZM1.js +0 -6
- package/dist/gui/assets/StartWorkPage-DO6t_zFb.js +0 -1
- package/dist/gui/assets/WorkspaceLauncher-CDtnv1IK.js +0 -1
- package/dist/gui/assets/WorkspacesPage-B9QOLnq_.js +0 -6
- package/dist/gui/assets/card-82INhRRE.js +0 -1
- package/dist/gui/assets/chevron-down-DDmhvRLr.js +0 -1
- package/dist/gui/assets/index-ofoYKpCG.js +0 -52
- package/dist/gui/assets/index-w-iQhEQv.css +0 -2
- package/dist/gui/assets/search-ChVovkOL.js +0 -1
- package/dist/gui/assets/skeleton-CCPbfn1L.js +0 -1
- package/dist/gui/assets/spinner-BCEaGbib.js +0 -1
- package/dist/gui/assets/textarea-D3-elOAa.js +0 -1
- package/dist/gui/assets/useAriaLabelledBy-DmwmZIqb.js +0 -1
- package/dist/gui/assets/useCompositeListItem-CZnlZ-eF.js +0 -1
- package/dist/gui/assets/useValueChanged-BfI2s7NX.js +0 -1
- package/dist/gui/assets/zap-DhYfYRlq.js +0 -1
package/dist/server.js
CHANGED
|
@@ -17,19 +17,22 @@ import * as os from 'node:os';
|
|
|
17
17
|
import { pipeline } from 'node:stream/promises';
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
19
|
import { loadConfig, saveConfig, getConfigDir } from './core/config.js';
|
|
20
|
+
import { configPatchSchema } from './core/config-schema.js';
|
|
20
21
|
import { listStorageProviders } from './core/adapters/registry.js';
|
|
21
22
|
import { scanForRepos } from './core/scanner.js';
|
|
22
|
-
import { createNewRepo } from './core/new-repo.js';
|
|
23
|
+
import { createNewRepo, isValidProjectName } from './core/new-repo.js';
|
|
23
24
|
import { loadProjects, createProject, updateProject, removeProject, slugifyProjectName } from './core/projects.js';
|
|
24
25
|
import { getSessionCwd, isInPlace, resolveFeatureRepoPath } from './utils/feature.js';
|
|
25
26
|
import { listBranches } from './utils/git.js';
|
|
26
|
-
import { createWorkspace, listWorkspaces, loadFeatureConfig, loadWorkspaceManifest, deleteWorkspace, addRepoToWorkspace } from './core/workspace.js';
|
|
27
|
+
import { createWorkspace, listWorkspaces, loadFeatureConfig, loadWorkspaceManifest, deleteWorkspace, addRepoToWorkspace, isolateWorkspaceRepo } from './core/workspace.js';
|
|
27
28
|
import { loadWorkspaceState } from './core/workspace-state.js';
|
|
28
29
|
import { analyzeAllRepos } from './analyzers/index.js';
|
|
29
30
|
import { generateContextFiles } from './generators/index.js';
|
|
30
31
|
import { detectAIAssistants } from './utils/detect-ai.js';
|
|
31
32
|
import { detectEditors } from './utils/detect-editors.js';
|
|
32
33
|
import { buildWorkspaceLaunchPrompt, detectWorkspaceLaunchTargets, launchTargetIdForEditorCommand, launchWorkspaceTarget, } from './utils/workspace-launch.js';
|
|
34
|
+
import { isBinaryOnPath, launchWorkspaceTerminal, SUPPORTED_ASSISTANTS } from './utils/terminal-launch.js';
|
|
35
|
+
import { openInEditor } from './utils/open-editor.js';
|
|
33
36
|
import { canOpenCodexSessionInWorkspace, canTransferClaudeSessionInWorkspace, findSessions, getSessionTranscript, } from './utils/session-finder.js';
|
|
34
37
|
import { ProviderRegistry } from './agent/adapters.js';
|
|
35
38
|
import { isValidSessionUuid } from './agent/session.js';
|
|
@@ -291,7 +294,15 @@ const DESKTOP_HANDOFF_SCAN_LIMIT = 20;
|
|
|
291
294
|
* Claude's documented `/desktop` transfer is narrower than its URI handler:
|
|
292
295
|
* it requires macOS or x64 Windows, a subscription login, and a usable CLI.
|
|
293
296
|
*/
|
|
294
|
-
export function canOfferClaudeDesktopTransfer(platform = process.platform, architecture = process.arch, env = process.env, isCliConfigured = () =>
|
|
297
|
+
export function canOfferClaudeDesktopTransfer(platform = process.platform, architecture = process.arch, env = process.env, isCliConfigured = () => {
|
|
298
|
+
try {
|
|
299
|
+
const provider = ProviderRegistry.getProvider('claude-cli');
|
|
300
|
+
if (provider?.isConfigured())
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
catch { }
|
|
304
|
+
return isBinaryOnPath('claude');
|
|
305
|
+
}) {
|
|
295
306
|
const supportedPlatform = platform === 'darwin' || (platform === 'win32' && architecture === 'x64');
|
|
296
307
|
const usesUnsupportedAuth = Boolean(env.ANTHROPIC_API_KEY
|
|
297
308
|
|| env.ANTHROPIC_AUTH_TOKEN
|
|
@@ -425,6 +436,33 @@ app.use('/api/*', cors({
|
|
|
425
436
|
}
|
|
426
437
|
},
|
|
427
438
|
}));
|
|
439
|
+
// --- host guard: DNS rebinding protection ---
|
|
440
|
+
function isAllowedHost(hostHeader) {
|
|
441
|
+
if (!hostHeader)
|
|
442
|
+
return true; // Internal Hono programmatic invocations / test requests
|
|
443
|
+
const hostname = hostHeader.startsWith('[')
|
|
444
|
+
? hostHeader.slice(1, hostHeader.indexOf(']'))
|
|
445
|
+
: hostHeader.split(':')[0]?.toLowerCase() ?? '';
|
|
446
|
+
return ['localhost', '127.0.0.1', '::1'].includes(hostname);
|
|
447
|
+
}
|
|
448
|
+
app.use('*', async (c, next) => {
|
|
449
|
+
const host = c.req.header('host');
|
|
450
|
+
if (!isAllowedHost(host)) {
|
|
451
|
+
return c.text('Forbidden: invalid Host header', 403);
|
|
452
|
+
}
|
|
453
|
+
await next();
|
|
454
|
+
});
|
|
455
|
+
// Enforce trusted local origin on all mutating HTTP methods across /api/* to defend against
|
|
456
|
+
// cross-site request forgery and browser form posts from untrusted web pages.
|
|
457
|
+
app.use('/api/*', async (c, next) => {
|
|
458
|
+
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(c.req.method)) {
|
|
459
|
+
const origin = c.req.header('origin');
|
|
460
|
+
if (origin && !hasTrustedLocalOrigin(origin)) {
|
|
461
|
+
return c.json({ error: 'Forbidden cross-origin request.' }, 403);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
await next();
|
|
465
|
+
});
|
|
428
466
|
// ─── API Endpoints ────────────────────────────────────────────────────────
|
|
429
467
|
// Status checks may execute a trusted local CLI from PATH. Keep that work
|
|
430
468
|
// behind a non-simple, same-machine POST so a remote page cannot trigger it
|
|
@@ -479,9 +517,15 @@ app.get('/api/adapters', async (c) => {
|
|
|
479
517
|
// 2. Save configuration
|
|
480
518
|
app.post('/api/config', async (c) => {
|
|
481
519
|
try {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
520
|
+
const body = await c.req.json().catch(() => null);
|
|
521
|
+
const parsed = configPatchSchema.safeParse(body);
|
|
522
|
+
if (!parsed.success) {
|
|
523
|
+
return c.json({ error: 'Invalid config payload', issues: parsed.error.flatten() }, 400);
|
|
524
|
+
}
|
|
525
|
+
const current = await loadConfig();
|
|
526
|
+
const next = { ...current, ...parsed.data };
|
|
527
|
+
await saveConfig(next);
|
|
528
|
+
return c.json({ success: true, config: next });
|
|
485
529
|
}
|
|
486
530
|
catch (error) {
|
|
487
531
|
return errorResponse(c, error);
|
|
@@ -596,11 +640,16 @@ app.delete('/api/projects/:id', async (c) => {
|
|
|
596
640
|
return errorResponse(c, error);
|
|
597
641
|
}
|
|
598
642
|
});
|
|
599
|
-
// 4. List existing workspaces
|
|
643
|
+
// 4. List existing workspaces (ordered by newest creation date first)
|
|
600
644
|
app.get('/api/workspaces', async (c) => {
|
|
601
645
|
try {
|
|
602
646
|
const config = await loadConfig();
|
|
603
647
|
const workspaces = await listWorkspaces(config.workspacesDir);
|
|
648
|
+
workspaces.sort((a, b) => {
|
|
649
|
+
const aTime = new Date(a.createdAt || 0).getTime();
|
|
650
|
+
const bTime = new Date(b.createdAt || 0).getTime();
|
|
651
|
+
return bTime - aTime;
|
|
652
|
+
});
|
|
604
653
|
return c.json(workspaces);
|
|
605
654
|
}
|
|
606
655
|
catch (error) {
|
|
@@ -857,6 +906,19 @@ app.post('/api/workspace', async (c) => {
|
|
|
857
906
|
return c.json({ error: 'Missing "branchName" in request body' }, 400);
|
|
858
907
|
}
|
|
859
908
|
const config = await loadConfig();
|
|
909
|
+
// Validate repo names and paths synchronously before starting the job.
|
|
910
|
+
for (const r of body.repos || []) {
|
|
911
|
+
if (inPlace) {
|
|
912
|
+
if (config.devDir && r.path) {
|
|
913
|
+
assertWithin(config.devDir, path.resolve(r.path));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
if (r.name && !isValidProjectName(r.name)) {
|
|
918
|
+
return c.json({ error: `Invalid repository name: ${JSON.stringify(r.name)}` }, 400);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
860
922
|
// The job id doubles as the workspace directory name.
|
|
861
923
|
const jobId = inPlace ? slugifyProjectName(body.name) : body.branchName;
|
|
862
924
|
if (!jobId) {
|
|
@@ -984,6 +1046,9 @@ app.post('/api/workspace/:id/repo', async (c) => {
|
|
|
984
1046
|
const id = decodeURIComponent(c.req.param('id'));
|
|
985
1047
|
const { repoPath } = await c.req.json();
|
|
986
1048
|
const config = await loadConfig();
|
|
1049
|
+
if (config.devDir) {
|
|
1050
|
+
assertWithin(config.devDir, path.resolve(repoPath));
|
|
1051
|
+
}
|
|
987
1052
|
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
988
1053
|
await addRepoToWorkspace(workspacePath, repoPath);
|
|
989
1054
|
return c.json({ success: true });
|
|
@@ -992,6 +1057,29 @@ app.post('/api/workspace/:id/repo', async (c) => {
|
|
|
992
1057
|
return errorResponse(c, error);
|
|
993
1058
|
}
|
|
994
1059
|
});
|
|
1060
|
+
// 7.7b. Isolate repo in in-place workspace
|
|
1061
|
+
app.post('/api/workspace/:id/isolate', async (c) => {
|
|
1062
|
+
try {
|
|
1063
|
+
const id = decodeURIComponent(c.req.param('id'));
|
|
1064
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1065
|
+
const repo = typeof body.repo === 'string' ? body.repo.trim() : '';
|
|
1066
|
+
if (!repo) {
|
|
1067
|
+
return c.json({ error: 'Missing "repo" parameter in request body' }, 400);
|
|
1068
|
+
}
|
|
1069
|
+
const branchName = typeof body.branchName === 'string' && body.branchName.trim() ? body.branchName.trim() : undefined;
|
|
1070
|
+
const baseBranch = typeof body.baseBranch === 'string' && body.baseBranch.trim() ? body.baseBranch.trim() : undefined;
|
|
1071
|
+
const config = await loadConfig();
|
|
1072
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
1073
|
+
const result = await isolateWorkspaceRepo(workspacePath, repo, {
|
|
1074
|
+
branchName,
|
|
1075
|
+
baseBranch,
|
|
1076
|
+
});
|
|
1077
|
+
return c.json({ success: true, ...result });
|
|
1078
|
+
}
|
|
1079
|
+
catch (error) {
|
|
1080
|
+
return errorResponse(c, error);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
995
1083
|
// 7.8. Suggest workflow strategy based on feature description & selected repos
|
|
996
1084
|
app.post('/api/workspace/suggest-workflow', async (c) => {
|
|
997
1085
|
try {
|
|
@@ -1018,6 +1106,10 @@ app.get('/api/workspace-launch-targets', async (c) => {
|
|
|
1018
1106
|
});
|
|
1019
1107
|
// 8a. Open a real NexusFlow workspace in a selected desktop app or editor.
|
|
1020
1108
|
app.post('/api/workspace/:id/launch', async (c) => {
|
|
1109
|
+
const origin = c.req.header('origin');
|
|
1110
|
+
if (origin && !hasTrustedLocalOrigin(origin)) {
|
|
1111
|
+
return c.json({ error: 'Forbidden cross-origin request.' }, 403);
|
|
1112
|
+
}
|
|
1021
1113
|
try {
|
|
1022
1114
|
const id = decodeURIComponent(c.req.param('id'));
|
|
1023
1115
|
const { targetId, action = 'new', sessionId } = await c.req.json().catch(() => ({}));
|
|
@@ -1079,12 +1171,74 @@ app.post('/api/workspace/:id/launch', async (c) => {
|
|
|
1079
1171
|
return errorResponse(c, error);
|
|
1080
1172
|
}
|
|
1081
1173
|
});
|
|
1082
|
-
// 8b.
|
|
1174
|
+
// 8b. Launch an external interactive terminal for a workspace or AI session.
|
|
1175
|
+
app.post('/api/workspace/:id/terminal', async (c) => {
|
|
1176
|
+
const origin = c.req.header('origin');
|
|
1177
|
+
if (origin && !hasTrustedLocalOrigin(origin)) {
|
|
1178
|
+
return c.json({ error: 'Forbidden cross-origin request.' }, 403);
|
|
1179
|
+
}
|
|
1180
|
+
try {
|
|
1181
|
+
const id = decodeURIComponent(c.req.param('id'));
|
|
1182
|
+
const { command, assistant, sessionId, title } = await c.req.json().catch(() => ({}));
|
|
1183
|
+
if (assistant !== undefined) {
|
|
1184
|
+
if (typeof assistant !== 'string' || !SUPPORTED_ASSISTANTS.has(assistant.trim().toLowerCase())) {
|
|
1185
|
+
return c.json({ error: 'Invalid or unsupported assistant parameter.' }, 400);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (sessionId !== undefined) {
|
|
1189
|
+
if (typeof sessionId !== 'string' || !isValidSessionUuid(sessionId)) {
|
|
1190
|
+
return c.json({ error: 'Invalid session UUID format.' }, 400);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (command !== undefined && (typeof command !== 'string' || !command.trim())) {
|
|
1194
|
+
return c.json({ error: 'Command must be a non-empty string.' }, 400);
|
|
1195
|
+
}
|
|
1196
|
+
if (title !== undefined && typeof title !== 'string') {
|
|
1197
|
+
return c.json({ error: 'Title must be a string.' }, 400);
|
|
1198
|
+
}
|
|
1199
|
+
const config = await loadConfig();
|
|
1200
|
+
const requestedWorkspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
1201
|
+
let workspacePath;
|
|
1202
|
+
try {
|
|
1203
|
+
workspacePath = await resolveExactLaunchWorkspace(config.workspacesDir, requestedWorkspacePath);
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
if (error instanceof PathAccessError)
|
|
1207
|
+
throw error;
|
|
1208
|
+
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
1209
|
+
}
|
|
1210
|
+
if (!workspacePath) {
|
|
1211
|
+
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
1212
|
+
}
|
|
1213
|
+
const res = await launchWorkspaceTerminal(workspacePath, {
|
|
1214
|
+
command: typeof command === 'string' ? command : undefined,
|
|
1215
|
+
assistant: typeof assistant === 'string' ? assistant : undefined,
|
|
1216
|
+
sessionId: typeof sessionId === 'string' ? sessionId : undefined,
|
|
1217
|
+
title: typeof title === 'string' ? title : undefined,
|
|
1218
|
+
});
|
|
1219
|
+
return c.json({ success: true, command: res.command });
|
|
1220
|
+
}
|
|
1221
|
+
catch (error) {
|
|
1222
|
+
return errorResponse(c, error);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
// 8c. Legacy editor route retained for older GUI clients using recognized
|
|
1083
1226
|
// graphical editors. Interactive terminal editors are intentionally rejected:
|
|
1084
1227
|
// a detached HTTP request cannot safely provide their required TTY.
|
|
1085
1228
|
app.post('/api/open-editor', async (c) => {
|
|
1229
|
+
const origin = c.req.header('origin');
|
|
1230
|
+
if (origin && !hasTrustedLocalOrigin(origin)) {
|
|
1231
|
+
return c.json({ error: 'Forbidden cross-origin request.' }, 403);
|
|
1232
|
+
}
|
|
1086
1233
|
try {
|
|
1087
|
-
const
|
|
1234
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1235
|
+
const { workspacePath, command } = body;
|
|
1236
|
+
if (typeof workspacePath !== 'string' || !workspacePath.trim()) {
|
|
1237
|
+
return c.json({ error: 'Workspace path does not exist' }, 400);
|
|
1238
|
+
}
|
|
1239
|
+
if (typeof command !== 'string' || !command.trim()) {
|
|
1240
|
+
return c.json({ error: 'Forbidden editor command' }, 400);
|
|
1241
|
+
}
|
|
1088
1242
|
const targetId = launchTargetIdForEditorCommand(command);
|
|
1089
1243
|
if (!ALLOWED_EDITORS.has(command) || !targetId) {
|
|
1090
1244
|
return c.json({ error: 'Forbidden editor command' }, 400);
|
|
@@ -1329,10 +1483,10 @@ app.get('/api/workspace/:id/changes', async (c) => {
|
|
|
1329
1483
|
});
|
|
1330
1484
|
const numstatLines = numstatRaw.split('\n').filter(Boolean);
|
|
1331
1485
|
for (const numLine of numstatLines) {
|
|
1332
|
-
const
|
|
1333
|
-
if (
|
|
1334
|
-
const [add, del, file] =
|
|
1335
|
-
numstatMap.set(file, {
|
|
1486
|
+
const match = numLine.trim().match(/^(\d+|-)\s+(\d+|-)\s+(.*)$/);
|
|
1487
|
+
if (match) {
|
|
1488
|
+
const [, add, del, file] = match;
|
|
1489
|
+
numstatMap.set(file.trim(), {
|
|
1336
1490
|
additions: add === '-' ? 0 : parseInt(add, 10) || 0,
|
|
1337
1491
|
deletions: del === '-' ? 0 : parseInt(del, 10) || 0,
|
|
1338
1492
|
});
|
|
@@ -1567,6 +1721,10 @@ app.post('/api/workspace/:id/commit', async (c) => {
|
|
|
1567
1721
|
});
|
|
1568
1722
|
// 14. Resume session in workspace (copies CLI resume command and opens editor)
|
|
1569
1723
|
app.post('/api/workspace/:id/resume', async (c) => {
|
|
1724
|
+
const origin = c.req.header('origin');
|
|
1725
|
+
if (origin && !hasTrustedLocalOrigin(origin)) {
|
|
1726
|
+
return c.json({ error: 'Forbidden cross-origin request.' }, 403);
|
|
1727
|
+
}
|
|
1570
1728
|
try {
|
|
1571
1729
|
const id = decodeURIComponent(c.req.param('id'));
|
|
1572
1730
|
const config = await loadConfig();
|
|
@@ -1577,8 +1735,14 @@ app.post('/api/workspace/:id/resume', async (c) => {
|
|
|
1577
1735
|
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
1578
1736
|
}
|
|
1579
1737
|
const body = await c.req.json().catch(() => ({}));
|
|
1580
|
-
const targetSessionId = body.sessionId;
|
|
1581
|
-
|
|
1738
|
+
const targetSessionId = typeof body.sessionId === 'string' && isValidSessionUuid(body.sessionId) ? body.sessionId : undefined;
|
|
1739
|
+
if (body.sessionId !== undefined && targetSessionId === undefined) {
|
|
1740
|
+
return c.json({ error: 'Invalid session UUID format.' }, 400);
|
|
1741
|
+
}
|
|
1742
|
+
if (body.command !== undefined && (typeof body.command !== 'string' || !ALLOWED_EDITORS.has(body.command))) {
|
|
1743
|
+
return c.json({ error: 'Forbidden editor command' }, 400);
|
|
1744
|
+
}
|
|
1745
|
+
const targetAssistant = typeof body.assistant === 'string' ? body.assistant : undefined;
|
|
1582
1746
|
// Find the session to resume
|
|
1583
1747
|
let resumeCommand = '';
|
|
1584
1748
|
let selectedAssistant = targetAssistant || feature.assistants[0] || 'antigravity';
|
|
@@ -1628,17 +1792,12 @@ app.post('/api/workspace/:id/resume', async (c) => {
|
|
|
1628
1792
|
if (!ALLOWED_EDITORS.has(body.command)) {
|
|
1629
1793
|
return c.json({ error: 'Forbidden editor command' }, 400);
|
|
1630
1794
|
}
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
shell: isWin,
|
|
1636
|
-
cleanup: false,
|
|
1637
|
-
});
|
|
1638
|
-
child.unref();
|
|
1639
|
-
child.catch((err) => {
|
|
1795
|
+
try {
|
|
1796
|
+
await openInEditor(body.command, workspacePath);
|
|
1797
|
+
}
|
|
1798
|
+
catch (err) {
|
|
1640
1799
|
console.error(`Failed to launch editor ${body.command} for path ${workspacePath}:`, err);
|
|
1641
|
-
}
|
|
1800
|
+
}
|
|
1642
1801
|
}
|
|
1643
1802
|
// Where the resume command should be run: the workspace dir, or the repo
|
|
1644
1803
|
// root for single-repo in-place features.
|
|
@@ -1726,8 +1885,11 @@ app.get('/api/session/:assistant/:sessionId/transcript', async (c) => {
|
|
|
1726
1885
|
const assistant = c.req.param('assistant');
|
|
1727
1886
|
const sessionId = c.req.param('sessionId');
|
|
1728
1887
|
try {
|
|
1729
|
-
if (
|
|
1730
|
-
return c.json({ error:
|
|
1888
|
+
if (!SUPPORTED_ASSISTANTS.has(assistant.trim().toLowerCase())) {
|
|
1889
|
+
return c.json({ error: `Unsupported assistant: "${assistant}".` }, 400);
|
|
1890
|
+
}
|
|
1891
|
+
if (!isValidSessionUuid(sessionId)) {
|
|
1892
|
+
return c.json({ error: 'Invalid session UUID format.' }, 400);
|
|
1731
1893
|
}
|
|
1732
1894
|
const messages = await getSessionTranscript(assistant, sessionId);
|
|
1733
1895
|
return c.json({ messages });
|