@mrpatronz/nexusflow 0.2.19 → 1.3.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 +37 -7
- package/dist/analyzers/index.d.ts +23 -0
- package/dist/analyzers/index.d.ts.map +1 -1
- package/dist/analyzers/index.js +62 -0
- package/dist/analyzers/index.js.map +1 -1
- package/dist/commands/add-repo.d.ts +9 -0
- package/dist/commands/add-repo.d.ts.map +1 -1
- package/dist/commands/add-repo.js +12 -1
- package/dist/commands/add-repo.js.map +1 -1
- package/dist/commands/add-repo.test.d.ts +2 -0
- package/dist/commands/add-repo.test.d.ts.map +1 -0
- package/dist/commands/add-repo.test.js +30 -0
- package/dist/commands/add-repo.test.js.map +1 -0
- package/dist/commands/commands.test.js +26 -6
- package/dist/commands/commands.test.js.map +1 -1
- package/dist/commands/commit.d.ts +7 -1
- package/dist/commands/commit.d.ts.map +1 -1
- package/dist/commands/commit.js +31 -8
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/commit.test.d.ts +2 -0
- package/dist/commands/commit.test.d.ts.map +1 -0
- package/dist/commands/commit.test.js +54 -0
- package/dist/commands/commit.test.js.map +1 -0
- package/dist/commands/diff.d.ts +9 -2
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +36 -12
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +16 -20
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/handoff.d.ts.map +1 -1
- package/dist/commands/handoff.js +11 -15
- package/dist/commands/handoff.js.map +1 -1
- package/dist/commands/refresh.d.ts +4 -2
- package/dist/commands/refresh.d.ts.map +1 -1
- package/dist/commands/refresh.js +20 -36
- package/dist/commands/refresh.js.map +1 -1
- package/dist/commands/schedule.d.ts +35 -0
- package/dist/commands/schedule.d.ts.map +1 -0
- package/dist/commands/schedule.js +151 -0
- package/dist/commands/schedule.js.map +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +28 -7
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/tui.js +1 -1
- package/dist/commands/tui.js.map +1 -1
- package/dist/commands/ui.d.ts +1 -0
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +5 -2
- package/dist/commands/ui.js.map +1 -1
- package/dist/core/adapters/local-storage.d.ts +7 -0
- package/dist/core/adapters/local-storage.d.ts.map +1 -1
- package/dist/core/adapters/local-storage.js +13 -4
- package/dist/core/adapters/local-storage.js.map +1 -1
- package/dist/core/adapters/storage.test.js +18 -0
- package/dist/core/adapters/storage.test.js.map +1 -1
- package/dist/core/analysis-cache.d.ts +58 -0
- package/dist/core/analysis-cache.d.ts.map +1 -0
- package/dist/core/analysis-cache.js +116 -0
- package/dist/core/analysis-cache.js.map +1 -0
- package/dist/core/analysis-cache.test.d.ts +2 -0
- package/dist/core/analysis-cache.test.d.ts.map +1 -0
- package/dist/core/analysis-cache.test.js +108 -0
- package/dist/core/analysis-cache.test.js.map +1 -0
- package/dist/core/refresh.d.ts +41 -0
- package/dist/core/refresh.d.ts.map +1 -0
- package/dist/core/refresh.js +56 -0
- package/dist/core/refresh.js.map +1 -0
- package/dist/core/scheduler.d.ts +117 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +361 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/scheduler.test.d.ts +2 -0
- package/dist/core/scheduler.test.d.ts.map +1 -0
- package/dist/core/scheduler.test.js +163 -0
- package/dist/core/scheduler.test.js.map +1 -0
- package/dist/core/sync.d.ts +6 -0
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/core/sync.js +17 -12
- 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 +32 -15
- package/dist/core/workspace.js.map +1 -1
- package/dist/core/workspace.test.d.ts +2 -0
- package/dist/core/workspace.test.d.ts.map +1 -0
- package/dist/core/workspace.test.js +42 -0
- package/dist/core/workspace.test.js.map +1 -0
- package/dist/generators/index.d.ts +7 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +14 -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 +4 -3
- package/dist/generators/map-generator.js.map +1 -1
- package/dist/generators/map-generator.test.js +4 -2
- package/dist/generators/map-generator.test.js.map +1 -1
- package/dist/gui/assets/index-ElroPpSP.js +26 -0
- package/dist/gui/index.html +1 -1
- package/dist/index.js +108 -4
- package/dist/index.js.map +1 -1
- package/dist/orchestration/index.d.ts +1 -1
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/runner.d.ts +16 -1
- package/dist/orchestration/runner.d.ts.map +1 -1
- package/dist/orchestration/runner.js +41 -5
- package/dist/orchestration/runner.js.map +1 -1
- package/dist/orchestration/runner.test.d.ts +2 -0
- package/dist/orchestration/runner.test.d.ts.map +1 -0
- package/dist/orchestration/runner.test.js +71 -0
- package/dist/orchestration/runner.test.js.map +1 -0
- package/dist/server.d.ts +17 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +275 -114
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +158 -1
- package/dist/server.test.js.map +1 -1
- package/dist/utils/multi-git.d.ts +21 -0
- package/dist/utils/multi-git.d.ts.map +1 -1
- package/dist/utils/multi-git.js +40 -4
- package/dist/utils/multi-git.js.map +1 -1
- package/dist/utils/prompts.d.ts.map +1 -1
- package/dist/utils/prompts.js +8 -2
- package/dist/utils/prompts.js.map +1 -1
- package/package.json +13 -1
- package/.agents/skills/ccc/SKILL.md +0 -71
- package/.agents/skills/ccc/references/management.md +0 -110
- package/.agents/skills/ccc/references/settings.md +0 -126
- package/.github/dependabot.yml +0 -52
- package/.github/workflows/ci.yml +0 -44
- package/.github/workflows/dependabot-merge.yml +0 -53
- package/.github/workflows/release-desktop.yml +0 -67
- package/.github/workflows/release-npm.yml +0 -65
- package/.github/workflows/release-vscode.yml +0 -61
- package/.vscode/launch.json +0 -17
- package/.vscode/tasks.json +0 -17
- package/GETTING_STARTED.md +0 -114
- package/desktop/build-installer.js +0 -228
- package/desktop/build.js +0 -118
- package/desktop/installer/nexusflow.iss +0 -35
- package/desktop/neutralino.config.json +0 -39
- package/desktop/package-lock.json +0 -1060
- package/desktop/package.json +0 -14
- package/dist/gui/assets/index-D8iJPvnk.js +0 -26
- package/extension/media/icon.svg +0 -1
- package/extension/package-lock.json +0 -6044
- package/extension/package.json +0 -117
- package/extension/src/extension.ts +0 -653
- package/extension/tsconfig.json +0 -21
- package/gui/README.md +0 -73
- package/gui/e2e/dashboard.spec.ts +0 -61
- package/gui/e2e/wizard.spec.ts +0 -346
- package/gui/eslint.config.js +0 -26
- package/gui/index.html +0 -17
- package/gui/package-lock.json +0 -3137
- package/gui/package.json +0 -37
- package/gui/playwright.config.ts +0 -41
- package/gui/public/app_logo.png +0 -0
- package/gui/public/favicon.svg +0 -1
- package/gui/public/icons.svg +0 -24
- package/gui/src/App.css +0 -1
- package/gui/src/App.tsx +0 -3573
- package/gui/src/app/AppSidebar.tsx +0 -69
- package/gui/src/assets/hero.png +0 -0
- package/gui/src/assets/react.svg +0 -1
- package/gui/src/assets/vite.svg +0 -1
- package/gui/src/components/AddRepoPicker.tsx +0 -81
- package/gui/src/components/ui/Button.tsx +0 -40
- package/gui/src/components/ui/Card.tsx +0 -6
- package/gui/src/components/ui/EmptyState.tsx +0 -22
- package/gui/src/components/ui/Input.tsx +0 -21
- package/gui/src/components/ui/Menu.tsx +0 -71
- package/gui/src/components/ui/Modal.tsx +0 -39
- package/gui/src/components/ui/PageHeader.tsx +0 -21
- package/gui/src/components/ui/RepoStatusStrip.tsx +0 -33
- package/gui/src/components/ui/Skeleton.tsx +0 -5
- package/gui/src/components/ui/StatusPill.tsx +0 -36
- package/gui/src/components/ui/Tabs.tsx +0 -40
- package/gui/src/components/ui/cn.ts +0 -4
- package/gui/src/components/ui/index.ts +0 -16
- package/gui/src/features/changes/ChangesViewer.tsx +0 -388
- package/gui/src/features/knowledge/KnowledgeBase.tsx +0 -84
- package/gui/src/features/onboarding/OnboardingWizard.tsx +0 -230
- package/gui/src/features/plan/ImplementationPlan.tsx +0 -32
- package/gui/src/features/services/ServiceConsole.tsx +0 -215
- package/gui/src/features/sessions/SessionHistory.tsx +0 -195
- package/gui/src/index.css +0 -167
- package/gui/src/lib/status.ts +0 -24
- package/gui/src/main.tsx +0 -94
- package/gui/src/pages/DashboardPage.tsx +0 -129
- package/gui/src/pages/WorkspacesPage.tsx +0 -352
- package/gui/src/types.ts +0 -81
- package/gui/tsconfig.app.json +0 -25
- package/gui/tsconfig.json +0 -7
- package/gui/tsconfig.node.json +0 -24
- package/gui/vite.config.ts +0 -12
- package/resources/workflows/plan-implement-review.md +0 -8
- package/resources/workflows/research-verify.md +0 -6
- package/resources/workflows/solo-developer.md +0 -3
- package/scripts/simulate-workspaces.ts +0 -55
- package/skills-lock.json +0 -11
- package/src/analyzers/detect-apis.ts +0 -290
- package/src/analyzers/detect-deps.ts +0 -315
- package/src/analyzers/detect-existing.ts +0 -74
- package/src/analyzers/detect-ports.ts +0 -110
- package/src/analyzers/index.ts +0 -97
- package/src/analyzers/messaging-analyzer.ts +0 -254
- package/src/analyzers/readme-summarizer.ts +0 -102
- package/src/analyzers/run-analyzer.ts +0 -269
- package/src/analyzers/tech-stack.ts +0 -283
- package/src/commands/adapter.ts +0 -348
- package/src/commands/add-repo.ts +0 -156
- package/src/commands/commands.test.ts +0 -156
- package/src/commands/commit.ts +0 -131
- package/src/commands/config.ts +0 -52
- package/src/commands/create.ts +0 -204
- package/src/commands/desktop.ts +0 -128
- package/src/commands/diff.ts +0 -125
- package/src/commands/doctor.ts +0 -370
- package/src/commands/handoff.ts +0 -266
- package/src/commands/init.ts +0 -134
- package/src/commands/list.ts +0 -46
- package/src/commands/logs.ts +0 -63
- package/src/commands/mcp.ts +0 -94
- package/src/commands/open.ts +0 -129
- package/src/commands/refresh.ts +0 -137
- package/src/commands/remove.ts +0 -98
- package/src/commands/start.ts +0 -117
- package/src/commands/status.ts +0 -54
- package/src/commands/stop.ts +0 -55
- package/src/commands/sync.ts +0 -125
- package/src/commands/tui.ts +0 -484
- package/src/commands/ui.ts +0 -111
- package/src/core/adapters/local-storage.ts +0 -66
- package/src/core/adapters/obsidian-storage.ts +0 -151
- package/src/core/adapters/registry.ts +0 -44
- package/src/core/adapters/storage.test.ts +0 -174
- package/src/core/adapters/vault-storage.ts +0 -86
- package/src/core/config.test.ts +0 -96
- package/src/core/config.ts +0 -140
- package/src/core/graph.ts +0 -344
- package/src/core/plugins/index.ts +0 -17
- package/src/core/plugins/loader.ts +0 -36
- package/src/core/ports/storage.ts +0 -71
- package/src/core/scanner.test.ts +0 -61
- package/src/core/scanner.ts +0 -91
- package/src/core/storage.ts +0 -37
- package/src/core/sync.ts +0 -121
- package/src/core/workspace-state.test.ts +0 -108
- package/src/core/workspace-state.ts +0 -130
- package/src/core/workspace.ts +0 -487
- package/src/core/worktree.ts +0 -120
- package/src/generators/antigravity.ts +0 -32
- package/src/generators/base.ts +0 -243
- package/src/generators/claude.ts +0 -34
- package/src/generators/codex.ts +0 -48
- package/src/generators/copilot.ts +0 -56
- package/src/generators/cursor.ts +0 -44
- package/src/generators/diff-context.ts +0 -75
- package/src/generators/index.ts +0 -322
- package/src/generators/map-generator.test.ts +0 -159
- package/src/generators/map-generator.ts +0 -539
- package/src/generators/plan-generator.ts +0 -483
- package/src/generators/skills-generator.ts +0 -259
- package/src/index.ts +0 -539
- package/src/mcp/server.ts +0 -338
- package/src/orchestration/detect.ts +0 -313
- package/src/orchestration/index.ts +0 -7
- package/src/orchestration/runner.ts +0 -283
- package/src/server.test.ts +0 -618
- package/src/server.ts +0 -1402
- package/src/types.ts +0 -451
- package/src/utils/detect-ai.test.ts +0 -44
- package/src/utils/detect-ai.ts +0 -84
- package/src/utils/detect-editors.test.ts +0 -48
- package/src/utils/detect-editors.ts +0 -57
- package/src/utils/git.test.ts +0 -74
- package/src/utils/git.ts +0 -117
- package/src/utils/local-ai.test.ts +0 -130
- package/src/utils/local-ai.ts +0 -116
- package/src/utils/multi-git.test.ts +0 -158
- package/src/utils/multi-git.ts +0 -405
- package/src/utils/prompts.ts +0 -209
- package/src/utils/session-finder.ts +0 -483
- package/src/utils/system-scanner.test.ts +0 -89
- package/src/utils/system-scanner.ts +0 -96
- package/src/utils/update-check.ts +0 -286
- package/src/utils/workflow-advisor.ts +0 -118
- package/src/utils/workflows.test.ts +0 -114
- package/src/utils/workflows.ts +0 -178
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -14
package/dist/server.js
CHANGED
|
@@ -29,7 +29,9 @@ import { findSessions, getSessionTranscript } from './utils/session-finder.js';
|
|
|
29
29
|
import { scanSystemSpecs } from './utils/system-scanner.js';
|
|
30
30
|
import { getWorkspaceRepos, commitAndPush, getRepoStatus } from './utils/multi-git.js';
|
|
31
31
|
import { syncWorkspace } from './core/sync.js';
|
|
32
|
-
import {
|
|
32
|
+
import { refreshWorkspace } from './core/refresh.js';
|
|
33
|
+
import { addSchedule, loadSchedules, nextDueAt, parseInterval, removeSchedule, runJob, setScheduleEnabled, startScheduler, } from './core/scheduler.js';
|
|
34
|
+
import { detectAllServices, detectOrchestrationTools, startServices, stopServices, loadRunningState, getPm2List, } from './orchestration/index.js';
|
|
33
35
|
import { checkForUpdates, getCurrentVersion, getToolsStatus } from './utils/update-check.js';
|
|
34
36
|
import { getWorkflowTemplates, saveWorkflowTemplate, deleteWorkflowTemplate } from './utils/workflows.js';
|
|
35
37
|
import { suggestWorkflow } from './utils/workflow-advisor.js';
|
|
@@ -42,8 +44,86 @@ const guiPath = path.join(__dirname, 'gui');
|
|
|
42
44
|
export const app = new Hono();
|
|
43
45
|
// Allowed editor binaries/scripts to prevent command injection
|
|
44
46
|
const ALLOWED_EDITORS = new Set(['code', 'code-insiders', 'cursor', 'antigravity', 'agy', 'idea', 'charm', 'webstorm', 'subl', 'nano', 'vim', 'nvim', 'emacs']);
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
+
// ─── Path containment guards ──────────────────────────────────────────────
|
|
48
|
+
// The server exposes state-changing routes keyed by a workspace `:id` taken
|
|
49
|
+
// straight from the URL. Without containment checks, `..%2f..` sequences let a
|
|
50
|
+
// caller read/write/delete files outside the workspaces root. Every handler
|
|
51
|
+
// that turns an id (or repo name) into a filesystem path must go through these.
|
|
52
|
+
/** Thrown when a requested path escapes its permitted base directory. */
|
|
53
|
+
export class PathAccessError extends Error {
|
|
54
|
+
constructor(message = 'Invalid workspace path') {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'PathAccessError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Resolve `target` and assert it stays within `baseDir` (or equals it). */
|
|
60
|
+
function assertWithin(baseDir, target) {
|
|
61
|
+
const base = path.resolve(baseDir);
|
|
62
|
+
const resolved = path.resolve(target);
|
|
63
|
+
// Trailing separator prevents a sibling like `feat-secret` from passing the
|
|
64
|
+
// prefix test for base `feat`.
|
|
65
|
+
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
|
|
66
|
+
throw new PathAccessError();
|
|
67
|
+
}
|
|
68
|
+
return resolved;
|
|
69
|
+
}
|
|
70
|
+
/** Safe workspace directory for a route `:id`, contained within workspacesDir. */
|
|
71
|
+
export function resolveWorkspacePath(workspacesDir, id) {
|
|
72
|
+
return assertWithin(workspacesDir, path.join(workspacesDir, id));
|
|
73
|
+
}
|
|
74
|
+
/** Safe sub-repo path for a repo name, contained within the workspace. */
|
|
75
|
+
export function resolveRepoPath(workspacePath, repoName) {
|
|
76
|
+
return assertWithin(workspacePath, path.join(workspacePath, repoName));
|
|
77
|
+
}
|
|
78
|
+
/** Consistent error response; path-containment violations map to 400. */
|
|
79
|
+
function errorResponse(c, error) {
|
|
80
|
+
if (error instanceof PathAccessError) {
|
|
81
|
+
return c.json({ error: error.message }, 400);
|
|
82
|
+
}
|
|
83
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
84
|
+
return c.json({ error: msg }, 500);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Restrict the self-update download to GitHub release hosts over HTTPS so a
|
|
88
|
+
* cross-origin caller cannot make the server fetch and later execute an
|
|
89
|
+
* arbitrary binary.
|
|
90
|
+
*/
|
|
91
|
+
export function isAllowedUpdateUrl(candidate) {
|
|
92
|
+
try {
|
|
93
|
+
const url = new URL(candidate);
|
|
94
|
+
if (url.protocol !== 'https:')
|
|
95
|
+
return false;
|
|
96
|
+
const host = url.hostname.toLowerCase();
|
|
97
|
+
return (host === 'github.com' ||
|
|
98
|
+
host.endsWith('.github.com') ||
|
|
99
|
+
host === 'githubusercontent.com' ||
|
|
100
|
+
host.endsWith('.githubusercontent.com'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Enable CORS for the local GUI only. This server can spawn processes, run
|
|
107
|
+
// package installs and delete worktrees, so a wildcard origin would let any
|
|
108
|
+
// web page the developer has open drive it cross-origin.
|
|
109
|
+
app.use('/api/*', cors({
|
|
110
|
+
origin: (origin) => {
|
|
111
|
+
// No Origin header → same-origin or a non-browser client (CLI/desktop).
|
|
112
|
+
if (!origin)
|
|
113
|
+
return origin;
|
|
114
|
+
try {
|
|
115
|
+
const { hostname } = new URL(origin);
|
|
116
|
+
const isLocal = hostname === 'localhost' ||
|
|
117
|
+
hostname === '127.0.0.1' ||
|
|
118
|
+
hostname === '::1' ||
|
|
119
|
+
hostname === '[::1]';
|
|
120
|
+
return isLocal ? origin : null;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
}));
|
|
47
127
|
// ─── API Endpoints ────────────────────────────────────────────────────────
|
|
48
128
|
// 1. Get current configuration
|
|
49
129
|
app.get('/api/config', async (c) => {
|
|
@@ -59,8 +139,7 @@ app.get('/api/config', async (c) => {
|
|
|
59
139
|
return c.json({ config, exists });
|
|
60
140
|
}
|
|
61
141
|
catch (error) {
|
|
62
|
-
|
|
63
|
-
return c.json({ error: msg }, 500);
|
|
142
|
+
return errorResponse(c, error);
|
|
64
143
|
}
|
|
65
144
|
});
|
|
66
145
|
// Get all registered storage adapters
|
|
@@ -70,8 +149,7 @@ app.get('/api/adapters', async (c) => {
|
|
|
70
149
|
return c.json({ adapters });
|
|
71
150
|
}
|
|
72
151
|
catch (error) {
|
|
73
|
-
|
|
74
|
-
return c.json({ error: msg }, 500);
|
|
152
|
+
return errorResponse(c, error);
|
|
75
153
|
}
|
|
76
154
|
});
|
|
77
155
|
function isSafeLocalEndpoint(urlStr) {
|
|
@@ -107,8 +185,7 @@ app.post('/api/config', async (c) => {
|
|
|
107
185
|
return c.json({ success: true, config: newConfig });
|
|
108
186
|
}
|
|
109
187
|
catch (error) {
|
|
110
|
-
|
|
111
|
-
return c.json({ error: msg }, 500);
|
|
188
|
+
return errorResponse(c, error);
|
|
112
189
|
}
|
|
113
190
|
});
|
|
114
191
|
// 3. Scan development directory for repositories
|
|
@@ -119,8 +196,7 @@ app.get('/api/repos', async (c) => {
|
|
|
119
196
|
return c.json(repos);
|
|
120
197
|
}
|
|
121
198
|
catch (error) {
|
|
122
|
-
|
|
123
|
-
return c.json({ error: msg }, 500);
|
|
199
|
+
return errorResponse(c, error);
|
|
124
200
|
}
|
|
125
201
|
});
|
|
126
202
|
// 4. List existing workspaces
|
|
@@ -131,8 +207,7 @@ app.get('/api/workspaces', async (c) => {
|
|
|
131
207
|
return c.json(workspaces);
|
|
132
208
|
}
|
|
133
209
|
catch (error) {
|
|
134
|
-
|
|
135
|
-
return c.json({ error: msg }, 500);
|
|
210
|
+
return errorResponse(c, error);
|
|
136
211
|
}
|
|
137
212
|
});
|
|
138
213
|
// Severity ranking for picking the worst per-repo sync outcome in a workspace.
|
|
@@ -164,6 +239,9 @@ app.get('/api/workspaces/status', async (c) => {
|
|
|
164
239
|
try {
|
|
165
240
|
const config = await loadConfig();
|
|
166
241
|
const workspaces = await listWorkspaces(config.workspacesDir);
|
|
242
|
+
// Fetch the PM2 process list once for the whole overview instead of
|
|
243
|
+
// spawning `npx pm2 jlist` per workspace (slow, especially on Windows).
|
|
244
|
+
const pm2List = await getPm2List();
|
|
167
245
|
const entries = await Promise.all(workspaces.map(async (ws) => {
|
|
168
246
|
const workspacePath = ws.workspacePath || path.join(config.workspacesDir, ws.branchName);
|
|
169
247
|
const status = {
|
|
@@ -187,7 +265,7 @@ app.get('/api/workspaces/status', async (c) => {
|
|
|
187
265
|
}
|
|
188
266
|
// Running services (cached running-state, PM2-verified — same source as
|
|
189
267
|
// the Services tab; only workspaces that ever started services touch PM2).
|
|
190
|
-
const runningState = await loadRunningState(workspacePath);
|
|
268
|
+
const runningState = await loadRunningState(workspacePath, pm2List);
|
|
191
269
|
status.runningServices = runningState?.services?.length ?? 0;
|
|
192
270
|
// Sync state: worst-case classification + any repo pending validation.
|
|
193
271
|
const wsState = await loadWorkspaceState(workspacePath);
|
|
@@ -208,8 +286,7 @@ app.get('/api/workspaces/status', async (c) => {
|
|
|
208
286
|
return c.json(byWorkspace);
|
|
209
287
|
}
|
|
210
288
|
catch (error) {
|
|
211
|
-
|
|
212
|
-
return c.json({ error: msg }, 500);
|
|
289
|
+
return errorResponse(c, error);
|
|
213
290
|
}
|
|
214
291
|
});
|
|
215
292
|
// 5. Detect available AI assistants
|
|
@@ -219,8 +296,7 @@ app.get('/api/ai-detect', async (c) => {
|
|
|
219
296
|
return c.json(assistants);
|
|
220
297
|
}
|
|
221
298
|
catch (error) {
|
|
222
|
-
|
|
223
|
-
return c.json({ error: msg }, 500);
|
|
299
|
+
return errorResponse(c, error);
|
|
224
300
|
}
|
|
225
301
|
});
|
|
226
302
|
// 6. Detect available editors
|
|
@@ -230,8 +306,7 @@ app.get('/api/editor-detect', async (c) => {
|
|
|
230
306
|
return c.json(editors);
|
|
231
307
|
}
|
|
232
308
|
catch (error) {
|
|
233
|
-
|
|
234
|
-
return c.json({ error: msg }, 500);
|
|
309
|
+
return errorResponse(c, error);
|
|
235
310
|
}
|
|
236
311
|
});
|
|
237
312
|
// 6.5. Local LLM test & recommendation
|
|
@@ -292,6 +367,16 @@ app.get('/api/local-llm/recommend', async (c) => {
|
|
|
292
367
|
}
|
|
293
368
|
});
|
|
294
369
|
const creationJobs = new Map();
|
|
370
|
+
// Keep a finished job around briefly so a late SSE reconnect can still read its
|
|
371
|
+
// final state, then drop it so the map doesn't grow unbounded.
|
|
372
|
+
const FINISHED_JOB_TTL_MS = 5 * 60 * 1000;
|
|
373
|
+
function scheduleJobCleanup(jobId) {
|
|
374
|
+
const job = creationJobs.get(jobId);
|
|
375
|
+
if (!job || job.cleanupTimer)
|
|
376
|
+
return;
|
|
377
|
+
job.cleanupTimer = setTimeout(() => creationJobs.delete(jobId), FINISHED_JOB_TTL_MS);
|
|
378
|
+
job.cleanupTimer.unref?.();
|
|
379
|
+
}
|
|
295
380
|
function updateJobStep(jobId, stepId, status, message, extraData) {
|
|
296
381
|
const job = creationJobs.get(jobId);
|
|
297
382
|
if (!job)
|
|
@@ -331,10 +416,13 @@ function updateJobStep(jobId, stepId, status, message, extraData) {
|
|
|
331
416
|
for (const listener of job.listeners) {
|
|
332
417
|
listener(eventPayload);
|
|
333
418
|
}
|
|
419
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
420
|
+
scheduleJobCleanup(jobId);
|
|
421
|
+
}
|
|
334
422
|
}
|
|
335
423
|
async function runCreationJob(jobId, body, config) {
|
|
336
424
|
try {
|
|
337
|
-
const workspacePath =
|
|
425
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, body.branchName);
|
|
338
426
|
const job = creationJobs.get(jobId);
|
|
339
427
|
if (job) {
|
|
340
428
|
job.workspacePath = workspacePath;
|
|
@@ -420,8 +508,7 @@ app.post('/api/workspace', async (c) => {
|
|
|
420
508
|
return c.json({ success: true, jobId });
|
|
421
509
|
}
|
|
422
510
|
catch (error) {
|
|
423
|
-
|
|
424
|
-
return c.json({ error: msg }, 500);
|
|
511
|
+
return errorResponse(c, error);
|
|
425
512
|
}
|
|
426
513
|
});
|
|
427
514
|
// 7.5. Create workspace SSE stream
|
|
@@ -451,23 +538,33 @@ app.get('/api/workspace/create-stream/:jobId', async (c) => {
|
|
|
451
538
|
if (job.status === 'completed' || job.status === 'failed') {
|
|
452
539
|
return;
|
|
453
540
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
541
|
+
// Keep the connection open until the job finishes, driven by job events
|
|
542
|
+
// rather than polling.
|
|
543
|
+
await new Promise((resolve) => {
|
|
544
|
+
const listener = async (event) => {
|
|
545
|
+
try {
|
|
546
|
+
await stream.writeSSE({
|
|
547
|
+
event: event.type,
|
|
548
|
+
data: JSON.stringify(event.data),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
job.listeners.delete(listener);
|
|
553
|
+
resolve();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (event.data?.status === 'completed' || event.data?.status === 'failed') {
|
|
557
|
+
job.listeners.delete(listener);
|
|
558
|
+
resolve();
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
job.listeners.add(listener);
|
|
562
|
+
// Guard against the job finishing between the initial state write and now.
|
|
563
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
462
564
|
job.listeners.delete(listener);
|
|
565
|
+
resolve();
|
|
463
566
|
}
|
|
464
|
-
};
|
|
465
|
-
job.listeners.add(listener);
|
|
466
|
-
// Keep connection alive until job finishes
|
|
467
|
-
while (job.status === 'running') {
|
|
468
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
469
|
-
}
|
|
470
|
-
job.listeners.delete(listener);
|
|
567
|
+
});
|
|
471
568
|
});
|
|
472
569
|
});
|
|
473
570
|
// 7.6. Delete workspace
|
|
@@ -475,13 +572,12 @@ app.delete('/api/workspace/:id', async (c) => {
|
|
|
475
572
|
try {
|
|
476
573
|
const id = decodeURIComponent(c.req.param('id'));
|
|
477
574
|
const config = await loadConfig();
|
|
478
|
-
const workspacePath =
|
|
575
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
479
576
|
await deleteWorkspace(workspacePath);
|
|
480
577
|
return c.json({ success: true });
|
|
481
578
|
}
|
|
482
579
|
catch (error) {
|
|
483
|
-
|
|
484
|
-
return c.json({ error: msg }, 500);
|
|
580
|
+
return errorResponse(c, error);
|
|
485
581
|
}
|
|
486
582
|
});
|
|
487
583
|
// 7.7. Add repo to workspace
|
|
@@ -490,13 +586,12 @@ app.post('/api/workspace/:id/repo', async (c) => {
|
|
|
490
586
|
const id = decodeURIComponent(c.req.param('id'));
|
|
491
587
|
const { repoPath } = await c.req.json();
|
|
492
588
|
const config = await loadConfig();
|
|
493
|
-
const workspacePath =
|
|
589
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
494
590
|
await addRepoToWorkspace(workspacePath, repoPath);
|
|
495
591
|
return c.json({ success: true });
|
|
496
592
|
}
|
|
497
593
|
catch (error) {
|
|
498
|
-
|
|
499
|
-
return c.json({ error: msg }, 500);
|
|
594
|
+
return errorResponse(c, error);
|
|
500
595
|
}
|
|
501
596
|
});
|
|
502
597
|
// 7.8. Suggest workflow strategy based on feature description & selected repos
|
|
@@ -511,8 +606,7 @@ app.post('/api/workspace/suggest-workflow', async (c) => {
|
|
|
511
606
|
});
|
|
512
607
|
}
|
|
513
608
|
catch (error) {
|
|
514
|
-
|
|
515
|
-
return c.json({ error: msg }, 500);
|
|
609
|
+
return errorResponse(c, error);
|
|
516
610
|
}
|
|
517
611
|
});
|
|
518
612
|
// 8. Open workspace in editor
|
|
@@ -547,8 +641,7 @@ app.post('/api/open-editor', async (c) => {
|
|
|
547
641
|
return c.json({ success: true });
|
|
548
642
|
}
|
|
549
643
|
catch (error) {
|
|
550
|
-
|
|
551
|
-
return c.json({ error: msg }, 500);
|
|
644
|
+
return errorResponse(c, error);
|
|
552
645
|
}
|
|
553
646
|
});
|
|
554
647
|
// 9. Get status of running services in all/specific workspace
|
|
@@ -556,7 +649,7 @@ app.get('/api/workspace/:id/services', async (c) => {
|
|
|
556
649
|
try {
|
|
557
650
|
const id = c.req.param('id');
|
|
558
651
|
const config = await loadConfig();
|
|
559
|
-
const workspacePath =
|
|
652
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
560
653
|
// Detect all services
|
|
561
654
|
const services = await detectAllServices(workspacePath);
|
|
562
655
|
// Detect existing tools
|
|
@@ -570,8 +663,7 @@ app.get('/api/workspace/:id/services', async (c) => {
|
|
|
570
663
|
});
|
|
571
664
|
}
|
|
572
665
|
catch (error) {
|
|
573
|
-
|
|
574
|
-
return c.json({ error: msg }, 500);
|
|
666
|
+
return errorResponse(c, error);
|
|
575
667
|
}
|
|
576
668
|
});
|
|
577
669
|
// 10. Start services in workspace
|
|
@@ -580,14 +672,13 @@ app.post('/api/workspace/:id/services/start', async (c) => {
|
|
|
580
672
|
const id = c.req.param('id');
|
|
581
673
|
const { services } = await c.req.json();
|
|
582
674
|
const config = await loadConfig();
|
|
583
|
-
const workspacePath =
|
|
675
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
584
676
|
const logDir = path.join(workspacePath, '.nexusflow-logs');
|
|
585
677
|
await startServices(services, workspacePath, logDir);
|
|
586
678
|
return c.json({ success: true });
|
|
587
679
|
}
|
|
588
680
|
catch (error) {
|
|
589
|
-
|
|
590
|
-
return c.json({ error: msg }, 500);
|
|
681
|
+
return errorResponse(c, error);
|
|
591
682
|
}
|
|
592
683
|
});
|
|
593
684
|
// 11. Stop services in workspace
|
|
@@ -595,13 +686,12 @@ app.post('/api/workspace/:id/services/stop', async (c) => {
|
|
|
595
686
|
try {
|
|
596
687
|
const id = c.req.param('id');
|
|
597
688
|
const config = await loadConfig();
|
|
598
|
-
const workspacePath =
|
|
689
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
599
690
|
await stopServices(workspacePath);
|
|
600
691
|
return c.json({ success: true });
|
|
601
692
|
}
|
|
602
693
|
catch (error) {
|
|
603
|
-
|
|
604
|
-
return c.json({ error: msg }, 500);
|
|
694
|
+
return errorResponse(c, error);
|
|
605
695
|
}
|
|
606
696
|
});
|
|
607
697
|
// 12. Get service logs
|
|
@@ -610,7 +700,7 @@ app.get('/api/workspace/:id/services/logs/:serviceName', async (c) => {
|
|
|
610
700
|
const id = c.req.param('id');
|
|
611
701
|
const serviceName = c.req.param('serviceName');
|
|
612
702
|
const config = await loadConfig();
|
|
613
|
-
const workspacePath =
|
|
703
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
614
704
|
const logFile = path.join(workspacePath, '.nexusflow-logs', `${serviceName}.log`);
|
|
615
705
|
let content = '';
|
|
616
706
|
try {
|
|
@@ -630,8 +720,7 @@ app.get('/api/workspace/:id/services/logs/:serviceName', async (c) => {
|
|
|
630
720
|
return c.json({ logs: content });
|
|
631
721
|
}
|
|
632
722
|
catch (error) {
|
|
633
|
-
|
|
634
|
-
return c.json({ error: msg }, 500);
|
|
723
|
+
return errorResponse(c, error);
|
|
635
724
|
}
|
|
636
725
|
});
|
|
637
726
|
// 13. Get git changes in workspace sub-repositories
|
|
@@ -639,7 +728,7 @@ app.get('/api/workspace/:id/changes', async (c) => {
|
|
|
639
728
|
try {
|
|
640
729
|
const id = c.req.param('id');
|
|
641
730
|
const config = await loadConfig();
|
|
642
|
-
const workspacePath =
|
|
731
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
643
732
|
// Load feature config to get repo paths
|
|
644
733
|
const feature = await loadFeatureConfig(workspacePath);
|
|
645
734
|
if (!feature) {
|
|
@@ -709,8 +798,7 @@ app.get('/api/workspace/:id/changes', async (c) => {
|
|
|
709
798
|
return c.json({ changes: results });
|
|
710
799
|
}
|
|
711
800
|
catch (error) {
|
|
712
|
-
|
|
713
|
-
return c.json({ error: msg }, 500);
|
|
801
|
+
return errorResponse(c, error);
|
|
714
802
|
}
|
|
715
803
|
});
|
|
716
804
|
// 13_diff. Get git diff for a specific file in workspace sub-repositories
|
|
@@ -723,13 +811,9 @@ app.get('/api/workspace/:id/changes/diff', async (c) => {
|
|
|
723
811
|
return c.json({ error: 'Missing repo or file query parameter.' }, 400);
|
|
724
812
|
}
|
|
725
813
|
const config = await loadConfig();
|
|
726
|
-
const workspacePath =
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const resolvedPath = path.resolve(worktreePath);
|
|
730
|
-
if (!resolvedPath.startsWith(path.resolve(workspacePath))) {
|
|
731
|
-
return c.json({ error: 'Invalid repository path.' }, 400);
|
|
732
|
-
}
|
|
814
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
815
|
+
// Contained within the workspace; rejects `..` and sibling-prefix escapes.
|
|
816
|
+
const worktreePath = resolveRepoPath(workspacePath, repoName);
|
|
733
817
|
let diff = '';
|
|
734
818
|
// Check git status for the file to know if it's untracked
|
|
735
819
|
try {
|
|
@@ -761,8 +845,7 @@ app.get('/api/workspace/:id/changes/diff', async (c) => {
|
|
|
761
845
|
return c.json({ diff });
|
|
762
846
|
}
|
|
763
847
|
catch (error) {
|
|
764
|
-
|
|
765
|
-
return c.json({ error: msg }, 500);
|
|
848
|
+
return errorResponse(c, error);
|
|
766
849
|
}
|
|
767
850
|
});
|
|
768
851
|
// 13a. Get workspace knowledge (nexusflow-knowledge.md)
|
|
@@ -770,7 +853,7 @@ app.get('/api/workspace/:id/knowledge', async (c) => {
|
|
|
770
853
|
try {
|
|
771
854
|
const id = c.req.param('id');
|
|
772
855
|
const config = await loadConfig();
|
|
773
|
-
const workspacePath =
|
|
856
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
774
857
|
const knowledgeFile = path.join(workspacePath, 'nexusflow-knowledge.md');
|
|
775
858
|
let content = '';
|
|
776
859
|
try {
|
|
@@ -782,8 +865,7 @@ app.get('/api/workspace/:id/knowledge', async (c) => {
|
|
|
782
865
|
return c.json({ content });
|
|
783
866
|
}
|
|
784
867
|
catch (error) {
|
|
785
|
-
|
|
786
|
-
return c.json({ error: msg }, 500);
|
|
868
|
+
return errorResponse(c, error);
|
|
787
869
|
}
|
|
788
870
|
});
|
|
789
871
|
// 13b. Update workspace knowledge (nexusflow-knowledge.md)
|
|
@@ -792,14 +874,13 @@ app.put('/api/workspace/:id/knowledge', async (c) => {
|
|
|
792
874
|
const id = c.req.param('id');
|
|
793
875
|
const { content } = await c.req.json();
|
|
794
876
|
const config = await loadConfig();
|
|
795
|
-
const workspacePath =
|
|
877
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
796
878
|
const knowledgeFile = path.join(workspacePath, 'nexusflow-knowledge.md');
|
|
797
879
|
await fs.writeFile(knowledgeFile, content, 'utf-8');
|
|
798
880
|
return c.json({ success: true });
|
|
799
881
|
}
|
|
800
882
|
catch (error) {
|
|
801
|
-
|
|
802
|
-
return c.json({ error: msg }, 500);
|
|
883
|
+
return errorResponse(c, error);
|
|
803
884
|
}
|
|
804
885
|
});
|
|
805
886
|
// 13c. Get workspace plan (nexusflow-plan.md)
|
|
@@ -807,7 +888,7 @@ app.get('/api/workspace/:id/plan', async (c) => {
|
|
|
807
888
|
try {
|
|
808
889
|
const id = c.req.param('id');
|
|
809
890
|
const config = await loadConfig();
|
|
810
|
-
const workspacePath =
|
|
891
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
811
892
|
const planFile = path.join(workspacePath, 'nexusflow-plan.md');
|
|
812
893
|
let content = '';
|
|
813
894
|
try {
|
|
@@ -819,8 +900,7 @@ app.get('/api/workspace/:id/plan', async (c) => {
|
|
|
819
900
|
return c.json({ content });
|
|
820
901
|
}
|
|
821
902
|
catch (error) {
|
|
822
|
-
|
|
823
|
-
return c.json({ error: msg }, 500);
|
|
903
|
+
return errorResponse(c, error);
|
|
824
904
|
}
|
|
825
905
|
});
|
|
826
906
|
// 13d. Sync all repositories in workspace
|
|
@@ -828,7 +908,7 @@ app.post('/api/workspace/:id/sync', async (c) => {
|
|
|
828
908
|
try {
|
|
829
909
|
const id = c.req.param('id');
|
|
830
910
|
const config = await loadConfig();
|
|
831
|
-
const workspacePath =
|
|
911
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
832
912
|
const report = await syncWorkspace(workspacePath);
|
|
833
913
|
const results = report.repos.map((repo) => ({
|
|
834
914
|
repoName: repo.name,
|
|
@@ -845,8 +925,7 @@ app.post('/api/workspace/:id/sync', async (c) => {
|
|
|
845
925
|
});
|
|
846
926
|
}
|
|
847
927
|
catch (error) {
|
|
848
|
-
|
|
849
|
-
return c.json({ error: msg }, 500);
|
|
928
|
+
return errorResponse(c, error);
|
|
850
929
|
}
|
|
851
930
|
});
|
|
852
931
|
// 13e. Commit changes in all repositories in workspace
|
|
@@ -855,7 +934,7 @@ app.post('/api/workspace/:id/commit', async (c) => {
|
|
|
855
934
|
const id = c.req.param('id');
|
|
856
935
|
const { message } = await c.req.json();
|
|
857
936
|
const config = await loadConfig();
|
|
858
|
-
const workspacePath =
|
|
937
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
859
938
|
const repos = await getWorkspaceRepos(workspacePath);
|
|
860
939
|
const results = [];
|
|
861
940
|
for (const repo of repos) {
|
|
@@ -874,8 +953,7 @@ app.post('/api/workspace/:id/commit', async (c) => {
|
|
|
874
953
|
return c.json({ results });
|
|
875
954
|
}
|
|
876
955
|
catch (error) {
|
|
877
|
-
|
|
878
|
-
return c.json({ error: msg }, 500);
|
|
956
|
+
return errorResponse(c, error);
|
|
879
957
|
}
|
|
880
958
|
});
|
|
881
959
|
// 14. Resume session in workspace (copies CLI resume command and opens editor)
|
|
@@ -883,7 +961,7 @@ app.post('/api/workspace/:id/resume', async (c) => {
|
|
|
883
961
|
try {
|
|
884
962
|
const id = decodeURIComponent(c.req.param('id'));
|
|
885
963
|
const config = await loadConfig();
|
|
886
|
-
const workspacePath =
|
|
964
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
887
965
|
// Load feature config to get repo paths
|
|
888
966
|
const feature = await loadFeatureConfig(workspacePath);
|
|
889
967
|
if (!feature) {
|
|
@@ -956,8 +1034,7 @@ app.post('/api/workspace/:id/resume', async (c) => {
|
|
|
956
1034
|
return c.json({ success: true, resumeCommand, workspacePath });
|
|
957
1035
|
}
|
|
958
1036
|
catch (error) {
|
|
959
|
-
|
|
960
|
-
return c.json({ error: msg }, 500);
|
|
1037
|
+
return errorResponse(c, error);
|
|
961
1038
|
}
|
|
962
1039
|
});
|
|
963
1040
|
// 15. List past AI sessions for a workspace
|
|
@@ -965,7 +1042,7 @@ app.get('/api/workspace/:id/sessions', async (c) => {
|
|
|
965
1042
|
try {
|
|
966
1043
|
const id = decodeURIComponent(c.req.param('id'));
|
|
967
1044
|
const config = await loadConfig();
|
|
968
|
-
const workspacePath =
|
|
1045
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
969
1046
|
const feature = await loadFeatureConfig(workspacePath);
|
|
970
1047
|
if (!feature) {
|
|
971
1048
|
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
@@ -974,8 +1051,7 @@ app.get('/api/workspace/:id/sessions', async (c) => {
|
|
|
974
1051
|
return c.json({ sessions });
|
|
975
1052
|
}
|
|
976
1053
|
catch (error) {
|
|
977
|
-
|
|
978
|
-
return c.json({ error: msg }, 500);
|
|
1054
|
+
return errorResponse(c, error);
|
|
979
1055
|
}
|
|
980
1056
|
});
|
|
981
1057
|
// 16. Fetch transcript for a specific AI session
|
|
@@ -987,8 +1063,7 @@ app.get('/api/session/:assistant/:sessionId/transcript', async (c) => {
|
|
|
987
1063
|
return c.json({ messages });
|
|
988
1064
|
}
|
|
989
1065
|
catch (error) {
|
|
990
|
-
|
|
991
|
-
return c.json({ error: msg }, 500);
|
|
1066
|
+
return errorResponse(c, error);
|
|
992
1067
|
}
|
|
993
1068
|
});
|
|
994
1069
|
// 17. Check for NexusFlow updates
|
|
@@ -1002,8 +1077,7 @@ app.get('/api/update-status', async (c) => {
|
|
|
1002
1077
|
return c.json(status);
|
|
1003
1078
|
}
|
|
1004
1079
|
catch (error) {
|
|
1005
|
-
|
|
1006
|
-
return c.json({ error: msg }, 500);
|
|
1080
|
+
return errorResponse(c, error);
|
|
1007
1081
|
}
|
|
1008
1082
|
});
|
|
1009
1083
|
// 17.5. Check tools updates status
|
|
@@ -1014,8 +1088,7 @@ app.get('/api/updates/tools', async (c) => {
|
|
|
1014
1088
|
return c.json(status);
|
|
1015
1089
|
}
|
|
1016
1090
|
catch (error) {
|
|
1017
|
-
|
|
1018
|
-
return c.json({ error: msg }, 500);
|
|
1091
|
+
return errorResponse(c, error);
|
|
1019
1092
|
}
|
|
1020
1093
|
});
|
|
1021
1094
|
// 17.6. Install updates for a specific tool
|
|
@@ -1043,8 +1116,7 @@ app.post('/api/updates/install', async (c) => {
|
|
|
1043
1116
|
}
|
|
1044
1117
|
}
|
|
1045
1118
|
catch (error) {
|
|
1046
|
-
|
|
1047
|
-
return c.json({ error: msg }, 500);
|
|
1119
|
+
return errorResponse(c, error);
|
|
1048
1120
|
}
|
|
1049
1121
|
});
|
|
1050
1122
|
let downloadedInstallerPath = null;
|
|
@@ -1055,6 +1127,9 @@ app.post('/api/updates/download', async (c) => {
|
|
|
1055
1127
|
if (!downloadUrl) {
|
|
1056
1128
|
return c.json({ error: 'Download URL is required' }, 400);
|
|
1057
1129
|
}
|
|
1130
|
+
if (!isAllowedUpdateUrl(downloadUrl)) {
|
|
1131
|
+
return c.json({ error: 'Download URL is not an allowed update host' }, 400);
|
|
1132
|
+
}
|
|
1058
1133
|
const tempDir = os.tmpdir();
|
|
1059
1134
|
const fileName = 'NexusFlowSetup_Update.exe';
|
|
1060
1135
|
const targetPath = path.join(tempDir, fileName);
|
|
@@ -1112,8 +1187,7 @@ app.get('/api/workflows/templates', async (c) => {
|
|
|
1112
1187
|
return c.json({ templates });
|
|
1113
1188
|
}
|
|
1114
1189
|
catch (error) {
|
|
1115
|
-
|
|
1116
|
-
return c.json({ error: msg }, 500);
|
|
1190
|
+
return errorResponse(c, error);
|
|
1117
1191
|
}
|
|
1118
1192
|
});
|
|
1119
1193
|
// Save or update custom teamwork template
|
|
@@ -1127,8 +1201,7 @@ app.post('/api/workflows/templates', async (c) => {
|
|
|
1127
1201
|
return c.json({ success: true, template });
|
|
1128
1202
|
}
|
|
1129
1203
|
catch (error) {
|
|
1130
|
-
|
|
1131
|
-
return c.json({ error: msg }, 500);
|
|
1204
|
+
return errorResponse(c, error);
|
|
1132
1205
|
}
|
|
1133
1206
|
});
|
|
1134
1207
|
// Delete custom teamwork template
|
|
@@ -1147,8 +1220,7 @@ app.delete('/api/workflows/templates/:id', async (c) => {
|
|
|
1147
1220
|
return c.json({ success: true });
|
|
1148
1221
|
}
|
|
1149
1222
|
catch (error) {
|
|
1150
|
-
|
|
1151
|
-
return c.json({ error: msg }, 500);
|
|
1223
|
+
return errorResponse(c, error);
|
|
1152
1224
|
}
|
|
1153
1225
|
});
|
|
1154
1226
|
// Analyze teamwork template rules via selected AI coding assistant harness
|
|
@@ -1218,8 +1290,83 @@ and suffix it with:
|
|
|
1218
1290
|
return c.json({ analysis, suggestedImprovement });
|
|
1219
1291
|
}
|
|
1220
1292
|
catch (error) {
|
|
1221
|
-
|
|
1222
|
-
|
|
1293
|
+
return errorResponse(c, error);
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
// ─── Schedules: recurring workspace jobs (sync/refresh) ─────────────────
|
|
1297
|
+
// List all scheduled jobs (with computed next-due time)
|
|
1298
|
+
app.get('/api/schedules', async (c) => {
|
|
1299
|
+
const store = await loadSchedules();
|
|
1300
|
+
const jobs = store.jobs.map((job) => ({
|
|
1301
|
+
...job,
|
|
1302
|
+
nextDueAt: nextDueAt(job)?.toISOString() ?? null,
|
|
1303
|
+
}));
|
|
1304
|
+
return c.json({ jobs });
|
|
1305
|
+
});
|
|
1306
|
+
// Create a scheduled job
|
|
1307
|
+
app.post('/api/schedules', async (c) => {
|
|
1308
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1309
|
+
const task = body.task === 'refresh' ? 'refresh' : body.task === 'sync' ? 'sync' : null;
|
|
1310
|
+
if (!task) {
|
|
1311
|
+
return c.json({ error: 'task must be "sync" or "refresh"' }, 400);
|
|
1312
|
+
}
|
|
1313
|
+
const intervalMinutes = typeof body.intervalMinutes === 'number' && body.intervalMinutes > 0
|
|
1314
|
+
? Math.floor(body.intervalMinutes)
|
|
1315
|
+
: parseInterval(String(body.every ?? ''));
|
|
1316
|
+
if (!intervalMinutes) {
|
|
1317
|
+
return c.json({ error: 'Provide intervalMinutes (> 0) or every (e.g. "30m", "2h", "1d")' }, 400);
|
|
1318
|
+
}
|
|
1319
|
+
const config = await loadConfig();
|
|
1320
|
+
const workspacePath = body.workspacePath
|
|
1321
|
+
? String(body.workspacePath)
|
|
1322
|
+
: body.workspaceId
|
|
1323
|
+
? path.join(config.workspacesDir, String(body.workspaceId))
|
|
1324
|
+
: null;
|
|
1325
|
+
if (!workspacePath) {
|
|
1326
|
+
return c.json({ error: 'Provide workspacePath or workspaceId' }, 400);
|
|
1327
|
+
}
|
|
1328
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
1329
|
+
if (!feature) {
|
|
1330
|
+
return c.json({ error: `No NexusFlow workspace found at ${workspacePath}` }, 404);
|
|
1331
|
+
}
|
|
1332
|
+
const job = await addSchedule({ workspacePath, task, intervalMinutes });
|
|
1333
|
+
return c.json({ job }, 201);
|
|
1334
|
+
});
|
|
1335
|
+
// Enable/disable a scheduled job
|
|
1336
|
+
app.post('/api/schedules/:id/enabled', async (c) => {
|
|
1337
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1338
|
+
const job = await setScheduleEnabled(c.req.param('id'), Boolean(body.enabled));
|
|
1339
|
+
if (!job)
|
|
1340
|
+
return c.json({ error: 'Schedule not found' }, 404);
|
|
1341
|
+
return c.json({ job });
|
|
1342
|
+
});
|
|
1343
|
+
// Run a scheduled job immediately
|
|
1344
|
+
app.post('/api/schedules/:id/run', async (c) => {
|
|
1345
|
+
const store = await loadSchedules();
|
|
1346
|
+
const job = store.jobs.find((j) => j.id === c.req.param('id'));
|
|
1347
|
+
if (!job)
|
|
1348
|
+
return c.json({ error: 'Schedule not found' }, 404);
|
|
1349
|
+
const result = await runJob(job);
|
|
1350
|
+
return c.json({ result });
|
|
1351
|
+
});
|
|
1352
|
+
// Delete a scheduled job
|
|
1353
|
+
app.delete('/api/schedules/:id', async (c) => {
|
|
1354
|
+
const removed = await removeSchedule(c.req.param('id'));
|
|
1355
|
+
if (!removed)
|
|
1356
|
+
return c.json({ error: 'Schedule not found' }, 404);
|
|
1357
|
+
return c.json({ ok: true });
|
|
1358
|
+
});
|
|
1359
|
+
// Refresh a workspace's context files on demand (cache-aware)
|
|
1360
|
+
app.post('/api/workspace/:id/refresh', async (c) => {
|
|
1361
|
+
try {
|
|
1362
|
+
const config = await loadConfig();
|
|
1363
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, c.req.param('id'));
|
|
1364
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1365
|
+
const report = await refreshWorkspace(workspacePath, { force: Boolean(body.force) });
|
|
1366
|
+
return c.json({ report });
|
|
1367
|
+
}
|
|
1368
|
+
catch (error) {
|
|
1369
|
+
return errorResponse(c, error);
|
|
1223
1370
|
}
|
|
1224
1371
|
});
|
|
1225
1372
|
// Legacy pack endpoint removed.
|
|
@@ -1233,16 +1380,30 @@ app.get('/', async (c) => {
|
|
|
1233
1380
|
return c.text('GUI dashboard built assets not found. Run "npm run build" first.', 404);
|
|
1234
1381
|
}
|
|
1235
1382
|
});
|
|
1236
|
-
// Serve static assets from GUI build folder
|
|
1237
|
-
|
|
1238
|
-
|
|
1383
|
+
// Serve static assets from GUI build folder. Use the absolute build path
|
|
1384
|
+
// (serveStatic joins root + request path) so asset serving does not depend on
|
|
1385
|
+
// the server's cwd — a cwd on another drive made the old cwd-relative path
|
|
1386
|
+
// resolve wrong and serve a blank GUI, e.g. under `ui --daemon`.
|
|
1387
|
+
app.use('/*', serveStatic({ root: guiPath }));
|
|
1388
|
+
export function startServer(port = 3000, opts = {}) {
|
|
1239
1389
|
return new Promise((resolve, reject) => {
|
|
1240
|
-
const server = serve({ fetch: app.fetch, port }, (info) => {
|
|
1390
|
+
const server = serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, (info) => {
|
|
1391
|
+
// The dashboard server doubles as the host for recurring workspace
|
|
1392
|
+
// jobs (nexusflow schedule ...); jobs are re-read from disk each tick.
|
|
1393
|
+
startScheduler({ log: (message) => console.log(`[scheduler] ${message}`) });
|
|
1241
1394
|
resolve({ port: info.port, server });
|
|
1242
1395
|
});
|
|
1243
1396
|
server.on('error', (e) => {
|
|
1244
1397
|
if (e.code === 'EADDRINUSE') {
|
|
1245
|
-
|
|
1398
|
+
// Callers that own their backend (desktop/extension) pass strictPort so
|
|
1399
|
+
// they can rely on the port they requested instead of chasing a silent
|
|
1400
|
+
// increment they'd never find.
|
|
1401
|
+
if (opts.strictPort) {
|
|
1402
|
+
reject(new Error(`Port ${port} is already in use.`));
|
|
1403
|
+
}
|
|
1404
|
+
else {
|
|
1405
|
+
resolve(startServer(port + 1, opts));
|
|
1406
|
+
}
|
|
1246
1407
|
}
|
|
1247
1408
|
else {
|
|
1248
1409
|
reject(e);
|