@cluesmith/codev 2.0.2 → 2.0.6
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/dashboard/dist/assets/index-B-s8BA2l.js +135 -0
- package/dashboard/dist/assets/index-B-s8BA2l.js.map +1 -0
- package/dashboard/dist/assets/index-DB2AxRP7.css +32 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +32 -14
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts +1 -1
- package/dist/agent-farm/commands/architect.js +3 -3
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/attach.d.ts +19 -0
- package/dist/agent-farm/commands/attach.d.ts.map +1 -1
- package/dist/agent-farm/commands/attach.js +172 -12
- package/dist/agent-farm/commands/attach.js.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +6 -6
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/open.js +5 -5
- package/dist/agent-farm/commands/open.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts +22 -2
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +100 -181
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/shell.js +5 -5
- package/dist/agent-farm/commands/shell.js.map +1 -1
- package/dist/agent-farm/commands/spawn-roles.d.ts +3 -9
- package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-roles.js +14 -53
- package/dist/agent-farm/commands/spawn-roles.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts +11 -17
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +32 -13
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts +8 -6
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +183 -69
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts +4 -4
- package/dist/agent-farm/commands/start.js +16 -16
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts +1 -1
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +15 -26
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts +4 -4
- package/dist/agent-farm/commands/stop.js +9 -9
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +82 -7
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +2 -2
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +21 -4
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts +36 -26
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +50 -25
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts +12 -2
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +59 -1
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +111 -0
- package/dist/agent-farm/servers/overview.d.ts.map +1 -0
- package/dist/agent-farm/servers/overview.js +385 -0
- package/dist/agent-farm/servers/overview.js.map +1 -0
- package/dist/agent-farm/servers/tower-instances.d.ts +18 -20
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +97 -100
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-messages.d.ts +87 -0
- package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-messages.js +202 -0
- package/dist/agent-farm/servers/tower-messages.js.map +1 -0
- package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +343 -174
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +50 -21
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +35 -31
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +208 -184
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.d.ts +2 -2
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.js +12 -12
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
- package/dist/agent-farm/servers/tower-types.d.ts +8 -12
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.d.ts +9 -9
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.js +18 -18
- package/dist/agent-farm/servers/tower-utils.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts +2 -2
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +39 -18
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +5 -6
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/agent-names.d.ts +85 -0
- package/dist/agent-farm/utils/agent-names.d.ts.map +1 -0
- package/dist/agent-farm/utils/agent-names.js +140 -0
- package/dist/agent-farm/utils/agent-names.js.map +1 -0
- package/dist/agent-farm/utils/config.d.ts +1 -1
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +16 -16
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/file-tabs.d.ts +3 -3
- package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -1
- package/dist/agent-farm/utils/file-tabs.js +9 -9
- package/dist/agent-farm/utils/file-tabs.js.map +1 -1
- package/dist/agent-farm/utils/index.d.ts +0 -1
- package/dist/agent-farm/utils/index.d.ts.map +1 -1
- package/dist/agent-farm/utils/index.js +0 -1
- package/dist/agent-farm/utils/index.js.map +1 -1
- package/dist/agent-farm/utils/message-format.d.ts +17 -0
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -0
- package/dist/agent-farm/utils/message-format.js +41 -0
- package/dist/agent-farm/utils/message-format.js.map +1 -0
- package/dist/agent-farm/utils/notifications.d.ts +4 -4
- package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
- package/dist/agent-farm/utils/notifications.js +18 -18
- package/dist/agent-farm/utils/notifications.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +2 -2
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +13 -15
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +26 -2
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +296 -83
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/consult/metrics.d.ts +90 -0
- package/dist/commands/consult/metrics.d.ts.map +1 -0
- package/dist/commands/consult/metrics.js +203 -0
- package/dist/commands/consult/metrics.js.map +1 -0
- package/dist/commands/consult/stats.d.ts +18 -0
- package/dist/commands/consult/stats.d.ts.map +1 -0
- package/dist/commands/consult/stats.js +150 -0
- package/dist/commands/consult/stats.js.map +1 -0
- package/dist/commands/consult/usage-extractor.d.ts +38 -0
- package/dist/commands/consult/usage-extractor.d.ts.map +1 -0
- package/dist/commands/consult/usage-extractor.js +99 -0
- package/dist/commands/consult/usage-extractor.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +11 -9
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/import.js +4 -4
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -15
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/porch/index.d.ts +6 -6
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +37 -37
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts +1 -1
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +86 -92
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/commands/porch/notify.d.ts +11 -0
- package/dist/commands/porch/notify.d.ts.map +1 -0
- package/dist/commands/porch/notify.js +30 -0
- package/dist/commands/porch/notify.js.map +1 -0
- package/dist/commands/porch/plan.d.ts +1 -1
- package/dist/commands/porch/plan.d.ts.map +1 -1
- package/dist/commands/porch/plan.js +3 -3
- package/dist/commands/porch/plan.js.map +1 -1
- package/dist/commands/porch/prompts.d.ts +10 -1
- package/dist/commands/porch/prompts.d.ts.map +1 -1
- package/dist/commands/porch/prompts.js +59 -35
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/commands/porch/protocol.d.ts +1 -1
- package/dist/commands/porch/protocol.d.ts.map +1 -1
- package/dist/commands/porch/protocol.js +8 -8
- package/dist/commands/porch/protocol.js.map +1 -1
- package/dist/commands/porch/state.d.ts +6 -6
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +14 -12
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +10 -11
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/github.d.ts +81 -0
- package/dist/lib/github.d.ts.map +1 -0
- package/dist/lib/github.js +141 -0
- package/dist/lib/github.js.map +1 -0
- package/dist/lib/scaffold.d.ts +13 -21
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +34 -57
- package/dist/lib/scaffold.js.map +1 -1
- package/dist/lib/skeleton.d.ts +7 -7
- package/dist/lib/skeleton.d.ts.map +1 -1
- package/dist/lib/skeleton.js +10 -10
- package/dist/lib/skeleton.js.map +1 -1
- package/dist/terminal/index.d.ts +14 -0
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +12 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/pty-manager.d.ts +1 -1
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +10 -7
- package/dist/terminal/pty-manager.js.map +1 -1
- package/dist/terminal/pty-session.js +3 -3
- package/dist/terminal/pty-session.js.map +1 -1
- package/dist/terminal/session-manager.d.ts +64 -0
- package/dist/terminal/session-manager.d.ts.map +1 -1
- package/dist/terminal/session-manager.js +299 -10
- package/dist/terminal/session-manager.js.map +1 -1
- package/dist/terminal/shellper-client.d.ts +2 -1
- package/dist/terminal/shellper-client.d.ts.map +1 -1
- package/dist/terminal/shellper-client.js +4 -2
- package/dist/terminal/shellper-client.js.map +1 -1
- package/dist/terminal/shellper-main.js +33 -4
- package/dist/terminal/shellper-main.js.map +1 -1
- package/dist/terminal/shellper-process.d.ts +24 -7
- package/dist/terminal/shellper-process.d.ts.map +1 -1
- package/dist/terminal/shellper-process.js +139 -36
- package/dist/terminal/shellper-process.js.map +1 -1
- package/dist/terminal/shellper-protocol.d.ts +1 -0
- package/dist/terminal/shellper-protocol.d.ts.map +1 -1
- package/dist/terminal/shellper-protocol.js.map +1 -1
- package/package.json +4 -1
- package/skeleton/.claude/skills/af/SKILL.md +7 -7
- package/skeleton/.claude/skills/consult/SKILL.md +1 -1
- package/skeleton/builders.md +2 -2
- package/skeleton/maintain/.gitkeep +1 -1
- package/skeleton/porch/prompts/specify.md +1 -1
- package/skeleton/protocols/bugfix/prompts/pr.md +15 -4
- package/skeleton/protocols/experiment/protocol.md +17 -17
- package/skeleton/protocols/maintain/prompts/audit.md +2 -2
- package/skeleton/protocols/maintain/prompts/sync.md +1 -1
- package/skeleton/protocols/maintain/prompts/verify.md +1 -1
- package/skeleton/protocols/maintain/protocol.md +8 -9
- package/skeleton/protocols/maintain/templates/maintenance-run.md +2 -2
- package/skeleton/protocols/spir/protocol.json +5 -5
- package/skeleton/protocols/spir/protocol.md +8 -8
- package/skeleton/protocols/tick/protocol.md +31 -31
- package/skeleton/resources/commands/agent-farm.md +14 -14
- package/skeleton/resources/commands/codev.md +0 -1
- package/skeleton/resources/commands/consult.md +3 -3
- package/skeleton/resources/spikes.md +3 -3
- package/skeleton/resources/workflow-reference.md +14 -14
- package/skeleton/roles/architect.md +25 -25
- package/skeleton/roles/builder.md +1 -1
- package/skeleton/roles/consultant.md +6 -0
- package/skeleton/templates/AGENTS.md +5 -5
- package/skeleton/templates/CLAUDE.md +5 -5
- package/skeleton/templates/lifecycle.md +9 -9
- package/templates/open.html +19 -16
- package/templates/tower.html +54 -94
- package/templates/vendor/marked.min.js +6 -0
- package/templates/vendor/prism-bash.min.js +1 -0
- package/templates/vendor/prism-css.min.js +1 -0
- package/templates/vendor/prism-javascript.min.js +1 -0
- package/templates/vendor/prism-json.min.js +1 -0
- package/templates/vendor/prism-markdown.min.js +1 -0
- package/templates/vendor/prism-markup.min.js +1 -0
- package/templates/vendor/prism-python.min.js +1 -0
- package/templates/vendor/prism-tomorrow.min.css +1 -0
- package/templates/vendor/prism-typescript.min.js +1 -0
- package/templates/vendor/prism-yaml.min.js +1 -0
- package/templates/vendor/prism.min.js +1 -0
- package/templates/vendor/purify.min.js +3 -0
- package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
- package/dashboard/dist/assets/index-b38SaXk5.js +0 -136
- package/dashboard/dist/assets/index-b38SaXk5.js.map +0 -1
- package/dist/agent-farm/hq-connector.d.ts +0 -19
- package/dist/agent-farm/hq-connector.d.ts.map +0 -1
- package/dist/agent-farm/hq-connector.js +0 -351
- package/dist/agent-farm/hq-connector.js.map +0 -1
- package/dist/agent-farm/utils/deps.d.ts +0 -51
- package/dist/agent-farm/utils/deps.d.ts.map +0 -1
- package/dist/agent-farm/utils/deps.js +0 -162
- package/dist/agent-farm/utils/deps.js.map +0 -1
- package/dist/agent-farm/utils/gate-status.d.ts +0 -16
- package/dist/agent-farm/utils/gate-status.d.ts.map +0 -1
- package/dist/agent-farm/utils/gate-status.js +0 -79
- package/dist/agent-farm/utils/gate-status.js.map +0 -1
- package/dist/agent-farm/utils/gate-watcher.d.ts +0 -38
- package/dist/agent-farm/utils/gate-watcher.d.ts.map +0 -1
- package/dist/agent-farm/utils/gate-watcher.js +0 -122
- package/dist/agent-farm/utils/gate-watcher.js.map +0 -1
- package/dist/agent-farm/utils/session.d.ts +0 -32
- package/dist/agent-farm/utils/session.d.ts.map +0 -1
- package/dist/agent-farm/utils/session.js +0 -57
- package/dist/agent-farm/utils/session.js.map +0 -1
- package/dist/lib/projectlist-parser.d.ts +0 -70
- package/dist/lib/projectlist-parser.d.ts.map +0 -1
- package/dist/lib/projectlist-parser.js +0 -200
- package/dist/lib/projectlist-parser.js.map +0 -1
- package/skeleton/templates/projectlist-archive.md +0 -21
- package/skeleton/templates/projectlist.md +0 -147
- package/templates/dashboard/css/dialogs.css +0 -149
- package/templates/dashboard/css/files.css +0 -558
- package/templates/dashboard/css/layout.css +0 -133
- package/templates/dashboard/css/projects.css +0 -501
- package/templates/dashboard/css/statusbar.css +0 -23
- package/templates/dashboard/css/tabs.css +0 -314
- package/templates/dashboard/css/utilities.css +0 -50
- package/templates/dashboard/css/variables.css +0 -45
- package/templates/dashboard/index.html +0 -149
- package/templates/dashboard/js/dialogs.js +0 -368
- package/templates/dashboard/js/files.js +0 -448
- package/templates/dashboard/js/main.js +0 -476
- package/templates/dashboard/js/projects.js +0 -544
- package/templates/dashboard/js/state.js +0 -91
- package/templates/dashboard/js/tabs.js +0 -518
- package/templates/dashboard/js/utils.js +0 -191
|
@@ -14,16 +14,24 @@
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import crypto from 'node:crypto';
|
|
17
|
-
import {
|
|
17
|
+
import { exec } from 'node:child_process';
|
|
18
|
+
import { promisify } from 'node:util';
|
|
18
19
|
import { homedir, tmpdir } from 'node:os';
|
|
19
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
+
const execAsync = promisify(exec);
|
|
22
|
+
import { DEFAULT_COLS, defaultSessionOptions } from '../../terminal/index.js';
|
|
20
23
|
import { parseJsonBody, isRequestAllowed } from '../utils/server-utils.js';
|
|
21
|
-
import { isRateLimited,
|
|
24
|
+
import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
|
|
22
25
|
import { handleTunnelEndpoint } from './tower-tunnel.js';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
26
|
+
import { resolveTarget, broadcastMessage, isResolveError } from './tower-messages.js';
|
|
27
|
+
import { formatArchitectMessage, formatBuilderMessage } from '../utils/message-format.js';
|
|
28
|
+
import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
|
|
29
|
+
import { OverviewCache } from './overview.js';
|
|
30
|
+
import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, removeTerminalFromRegistry, deleteWorkspaceTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForWorkspace, } from './tower-terminals.js';
|
|
25
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
32
|
const __dirname = path.dirname(__filename);
|
|
33
|
+
// Singleton cache for overview endpoint (Spec 0126 Phase 4)
|
|
34
|
+
const overviewCache = new OverviewCache();
|
|
27
35
|
// ============================================================================
|
|
28
36
|
// Helper: read raw request body
|
|
29
37
|
// ============================================================================
|
|
@@ -36,16 +44,19 @@ async function readBody(req) {
|
|
|
36
44
|
}
|
|
37
45
|
const ROUTES = {
|
|
38
46
|
'GET /health': (_req, res) => handleHealthCheck(res),
|
|
39
|
-
'GET /api/
|
|
47
|
+
'GET /api/workspaces': (_req, res) => handleListWorkspaces(res),
|
|
40
48
|
'POST /api/terminals': (req, res, _url, ctx) => handleTerminalCreate(req, res, ctx),
|
|
41
49
|
'GET /api/terminals': (_req, res) => handleTerminalList(res),
|
|
42
50
|
'GET /api/status': (_req, res) => handleStatus(res),
|
|
51
|
+
'GET /api/overview': (_req, res, url) => handleOverview(res, url),
|
|
52
|
+
'POST /api/overview/refresh': (_req, res) => handleOverviewRefresh(res),
|
|
43
53
|
'GET /api/events': (req, res, _url, ctx) => handleSSEEvents(req, res, ctx),
|
|
44
54
|
'POST /api/notify': (req, res, _url, ctx) => handleNotify(req, res, ctx),
|
|
45
55
|
'GET /api/browse': (_req, res, url) => handleBrowse(res, url),
|
|
46
|
-
'POST /api/create': (req, res, _url, ctx) =>
|
|
56
|
+
'POST /api/create': (req, res, _url, ctx) => handleCreateWorkspace(req, res, ctx),
|
|
47
57
|
'POST /api/launch': (req, res) => handleLaunchInstance(req, res),
|
|
48
58
|
'POST /api/stop': (req, res) => handleStopInstance(req, res),
|
|
59
|
+
'POST /api/send': (req, res, _url, ctx) => handleSend(req, res, ctx),
|
|
49
60
|
'GET /': (_req, res, _url, ctx) => handleDashboard(res, ctx),
|
|
50
61
|
'GET /index.html': (_req, res, _url, ctx) => handleDashboard(res, ctx),
|
|
51
62
|
};
|
|
@@ -89,19 +100,19 @@ export async function handleRequest(req, res, ctx) {
|
|
|
89
100
|
await handleTunnelEndpoint(req, res, tunnelSub);
|
|
90
101
|
return;
|
|
91
102
|
}
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
return await
|
|
103
|
+
// Workspace API: /api/workspaces/:encodedPath/activate|deactivate|status (Spec 0090 Phase 1)
|
|
104
|
+
const workspaceApiMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)\/(activate|deactivate|status)$/);
|
|
105
|
+
if (workspaceApiMatch) {
|
|
106
|
+
return await handleWorkspaceAction(req, res, ctx, workspaceApiMatch);
|
|
96
107
|
}
|
|
97
108
|
// Terminal-specific routes: /api/terminals/:id/* (Spec 0090 Phase 2)
|
|
98
109
|
const terminalRouteMatch = url.pathname.match(/^\/api\/terminals\/([^/]+)(\/.*)?$/);
|
|
99
110
|
if (terminalRouteMatch) {
|
|
100
111
|
return await handleTerminalRoutes(req, res, url, terminalRouteMatch);
|
|
101
112
|
}
|
|
102
|
-
//
|
|
103
|
-
if (url.pathname.startsWith('/
|
|
104
|
-
return await
|
|
113
|
+
// Workspace routes: /workspace/:base64urlPath/* (Spec 0090 Phase 4)
|
|
114
|
+
if (url.pathname.startsWith('/workspace/')) {
|
|
115
|
+
return await handleWorkspaceRoutes(req, res, ctx, url);
|
|
105
116
|
}
|
|
106
117
|
// 404 for everything else
|
|
107
118
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
@@ -123,59 +134,58 @@ async function handleHealthCheck(res) {
|
|
|
123
134
|
res.end(JSON.stringify({
|
|
124
135
|
status: 'healthy',
|
|
125
136
|
uptime: process.uptime(),
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
activeWorkspaces: activeCount,
|
|
138
|
+
totalWorkspaces: instances.length,
|
|
128
139
|
memoryUsage: process.memoryUsage().heapUsed,
|
|
129
140
|
timestamp: new Date().toISOString(),
|
|
130
141
|
}));
|
|
131
142
|
}
|
|
132
|
-
async function
|
|
143
|
+
async function handleListWorkspaces(res) {
|
|
133
144
|
const instances = await getInstances();
|
|
134
|
-
const
|
|
135
|
-
path: i.
|
|
136
|
-
name: i.
|
|
145
|
+
const workspaces = instances.map((i) => ({
|
|
146
|
+
path: i.workspacePath,
|
|
147
|
+
name: i.workspaceName,
|
|
137
148
|
active: i.running,
|
|
138
149
|
proxyUrl: i.proxyUrl,
|
|
139
150
|
terminals: i.terminals.length,
|
|
140
151
|
}));
|
|
141
152
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
142
|
-
res.end(JSON.stringify({
|
|
153
|
+
res.end(JSON.stringify({ workspaces }));
|
|
143
154
|
}
|
|
144
|
-
async function
|
|
155
|
+
async function handleWorkspaceAction(req, res, ctx, match) {
|
|
145
156
|
const [, encodedPath, action] = match;
|
|
146
|
-
let
|
|
157
|
+
let workspacePath;
|
|
147
158
|
try {
|
|
148
|
-
|
|
149
|
-
if (!
|
|
159
|
+
workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
160
|
+
if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
|
|
150
161
|
throw new Error('Invalid path');
|
|
151
162
|
}
|
|
152
|
-
|
|
163
|
+
workspacePath = normalizeWorkspacePath(workspacePath);
|
|
153
164
|
}
|
|
154
165
|
catch {
|
|
155
166
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
156
|
-
res.end(JSON.stringify({ error: 'Invalid
|
|
167
|
+
res.end(JSON.stringify({ error: 'Invalid workspace path encoding' }));
|
|
157
168
|
return;
|
|
158
169
|
}
|
|
159
|
-
// GET /api/
|
|
170
|
+
// GET /api/workspaces/:path/status
|
|
160
171
|
if (req.method === 'GET' && action === 'status') {
|
|
161
172
|
const instances = await getInstances();
|
|
162
|
-
const instance = instances.find((i) => i.
|
|
173
|
+
const instance = instances.find((i) => i.workspacePath === workspacePath);
|
|
163
174
|
if (!instance) {
|
|
164
175
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
165
|
-
res.end(JSON.stringify({ error: '
|
|
176
|
+
res.end(JSON.stringify({ error: 'Workspace not found' }));
|
|
166
177
|
return;
|
|
167
178
|
}
|
|
168
179
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
169
180
|
res.end(JSON.stringify({
|
|
170
|
-
path: instance.
|
|
171
|
-
name: instance.
|
|
181
|
+
path: instance.workspacePath,
|
|
182
|
+
name: instance.workspaceName,
|
|
172
183
|
active: instance.running,
|
|
173
184
|
terminals: instance.terminals,
|
|
174
|
-
gateStatus: instance.gateStatus,
|
|
175
185
|
}));
|
|
176
186
|
return;
|
|
177
187
|
}
|
|
178
|
-
// POST /api/
|
|
188
|
+
// POST /api/workspaces/:path/activate
|
|
179
189
|
if (req.method === 'POST' && action === 'activate') {
|
|
180
190
|
// Rate limiting: 10 activations per minute per client
|
|
181
191
|
const clientIp = req.socket.remoteAddress || '127.0.0.1';
|
|
@@ -184,7 +194,7 @@ async function handleProjectAction(req, res, ctx, match) {
|
|
|
184
194
|
res.end(JSON.stringify({ error: 'Too many activations, try again later' }));
|
|
185
195
|
return;
|
|
186
196
|
}
|
|
187
|
-
const result = await launchInstance(
|
|
197
|
+
const result = await launchInstance(workspacePath);
|
|
188
198
|
if (result.success) {
|
|
189
199
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
190
200
|
res.end(JSON.stringify({ success: true, adopted: result.adopted }));
|
|
@@ -195,17 +205,17 @@ async function handleProjectAction(req, res, ctx, match) {
|
|
|
195
205
|
}
|
|
196
206
|
return;
|
|
197
207
|
}
|
|
198
|
-
// POST /api/
|
|
208
|
+
// POST /api/workspaces/:path/deactivate
|
|
199
209
|
if (req.method === 'POST' && action === 'deactivate') {
|
|
200
|
-
const knownPaths =
|
|
201
|
-
const resolvedPath = fs.existsSync(
|
|
202
|
-
const isKnown = knownPaths.some((p) => p ===
|
|
210
|
+
const knownPaths = getKnownWorkspacePaths();
|
|
211
|
+
const resolvedPath = fs.existsSync(workspacePath) ? fs.realpathSync(workspacePath) : workspacePath;
|
|
212
|
+
const isKnown = knownPaths.some((p) => p === workspacePath || p === resolvedPath);
|
|
203
213
|
if (!isKnown) {
|
|
204
214
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
205
|
-
res.end(JSON.stringify({ ok: false, error: '
|
|
215
|
+
res.end(JSON.stringify({ ok: false, error: 'Workspace not found' }));
|
|
206
216
|
return;
|
|
207
217
|
}
|
|
208
|
-
const result = await stopInstance(
|
|
218
|
+
const result = await stopInstance(workspacePath);
|
|
209
219
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
210
220
|
res.end(JSON.stringify(result));
|
|
211
221
|
return;
|
|
@@ -224,7 +234,7 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
224
234
|
const env = typeof body.env === 'object' && body.env !== null ? body.env : undefined;
|
|
225
235
|
const label = typeof body.label === 'string' ? body.label : undefined;
|
|
226
236
|
// Optional session persistence via shellper
|
|
227
|
-
const
|
|
237
|
+
const workspacePath = typeof body.workspacePath === 'string' ? body.workspacePath : null;
|
|
228
238
|
const termType = typeof body.type === 'string' && ['builder', 'shell'].includes(body.type) ? body.type : null;
|
|
229
239
|
const roleId = typeof body.roleId === 'string' ? body.roleId : null;
|
|
230
240
|
const requestPersistence = body.persistent === true;
|
|
@@ -244,9 +254,8 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
244
254
|
args: args || [],
|
|
245
255
|
cwd,
|
|
246
256
|
env: sessionEnv,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
restartOnExit: false,
|
|
257
|
+
...defaultSessionOptions(),
|
|
258
|
+
cols: cols || DEFAULT_COLS,
|
|
250
259
|
});
|
|
251
260
|
const replayData = client.getReplayData() ?? Buffer.alloc(0);
|
|
252
261
|
const shellperInfo = shellperManager.getSessionInfo(sessionId);
|
|
@@ -260,16 +269,16 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
260
269
|
}
|
|
261
270
|
info = session;
|
|
262
271
|
persistent = true;
|
|
263
|
-
if (
|
|
264
|
-
const entry =
|
|
272
|
+
if (workspacePath && termType && roleId) {
|
|
273
|
+
const entry = getWorkspaceTerminalsEntry(normalizeWorkspacePath(workspacePath));
|
|
265
274
|
if (termType === 'builder') {
|
|
266
275
|
entry.builders.set(roleId, session.id);
|
|
267
276
|
}
|
|
268
277
|
else {
|
|
269
278
|
entry.shells.set(roleId, session.id);
|
|
270
279
|
}
|
|
271
|
-
saveTerminalSession(session.id,
|
|
272
|
-
ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for
|
|
280
|
+
saveTerminalSession(session.id, workspacePath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
|
|
281
|
+
ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for workspace ${workspacePath}`);
|
|
273
282
|
}
|
|
274
283
|
}
|
|
275
284
|
catch (shellperErr) {
|
|
@@ -281,16 +290,16 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
281
290
|
if (!info) {
|
|
282
291
|
info = await manager.createSession({ command, args, cols, rows, cwd, env, label });
|
|
283
292
|
persistent = false;
|
|
284
|
-
if (
|
|
285
|
-
const entry =
|
|
293
|
+
if (workspacePath && termType && roleId) {
|
|
294
|
+
const entry = getWorkspaceTerminalsEntry(normalizeWorkspacePath(workspacePath));
|
|
286
295
|
if (termType === 'builder') {
|
|
287
296
|
entry.builders.set(roleId, info.id);
|
|
288
297
|
}
|
|
289
298
|
else {
|
|
290
299
|
entry.shells.set(roleId, info.id);
|
|
291
300
|
}
|
|
292
|
-
saveTerminalSession(info.id,
|
|
293
|
-
ctx.log('WARN', `Terminal ${info.id} for ${
|
|
301
|
+
saveTerminalSession(info.id, workspacePath, termType, roleId, info.pid);
|
|
302
|
+
ctx.log('WARN', `Terminal ${info.id} for ${workspacePath} is non-persistent (shellper unavailable)`);
|
|
294
303
|
}
|
|
295
304
|
}
|
|
296
305
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
@@ -333,6 +342,9 @@ async function handleTerminalRoutes(req, res, url, match) {
|
|
|
333
342
|
}
|
|
334
343
|
// TICK-001: Delete from SQLite
|
|
335
344
|
deleteTerminalSession(terminalId);
|
|
345
|
+
// Bugfix #290: Also remove from in-memory registry so dashboard
|
|
346
|
+
// stops showing tabs for cleaned-up builders
|
|
347
|
+
removeTerminalFromRegistry(terminalId);
|
|
336
348
|
res.writeHead(204);
|
|
337
349
|
res.end();
|
|
338
350
|
return;
|
|
@@ -406,6 +418,36 @@ async function handleStatus(res) {
|
|
|
406
418
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
407
419
|
res.end(JSON.stringify({ instances }));
|
|
408
420
|
}
|
|
421
|
+
async function handleOverview(res, url, workspaceOverride) {
|
|
422
|
+
// Accept workspace from: explicit override (workspace-scoped route), ?workspace= param, or first known path.
|
|
423
|
+
let workspaceRoot = workspaceOverride || url.searchParams.get('workspace');
|
|
424
|
+
if (!workspaceRoot) {
|
|
425
|
+
const knownPaths = getKnownWorkspacePaths();
|
|
426
|
+
workspaceRoot = knownPaths.find(p => !p.includes('/.builders/')) || null;
|
|
427
|
+
}
|
|
428
|
+
if (!workspaceRoot) {
|
|
429
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
430
|
+
res.end(JSON.stringify({ builders: [], pendingPRs: [], backlog: [] }));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
// Build set of active builder role_ids (lowercased) from live terminal sessions
|
|
434
|
+
const wsTerminals = getWorkspaceTerminals();
|
|
435
|
+
const entry = wsTerminals.get(normalizeWorkspacePath(workspaceRoot));
|
|
436
|
+
const activeBuilderRoleIds = new Set();
|
|
437
|
+
if (entry) {
|
|
438
|
+
for (const key of entry.builders.keys()) {
|
|
439
|
+
activeBuilderRoleIds.add(key.toLowerCase());
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const data = await overviewCache.getOverview(workspaceRoot, activeBuilderRoleIds);
|
|
443
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
444
|
+
res.end(JSON.stringify(data));
|
|
445
|
+
}
|
|
446
|
+
function handleOverviewRefresh(res) {
|
|
447
|
+
overviewCache.invalidate();
|
|
448
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
449
|
+
res.end(JSON.stringify({ ok: true }));
|
|
450
|
+
}
|
|
409
451
|
function handleSSEEvents(req, res, ctx) {
|
|
410
452
|
const clientId = crypto.randomBytes(8).toString('hex');
|
|
411
453
|
res.writeHead(200, {
|
|
@@ -429,7 +471,7 @@ async function handleNotify(req, res, ctx) {
|
|
|
429
471
|
const type = typeof body.type === 'string' ? body.type : 'info';
|
|
430
472
|
const title = typeof body.title === 'string' ? body.title : '';
|
|
431
473
|
const messageBody = typeof body.body === 'string' ? body.body : '';
|
|
432
|
-
const
|
|
474
|
+
const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
|
|
433
475
|
if (!title || !messageBody) {
|
|
434
476
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
435
477
|
res.end(JSON.stringify({ success: false, error: 'Missing title or body' }));
|
|
@@ -440,12 +482,113 @@ async function handleNotify(req, res, ctx) {
|
|
|
440
482
|
type,
|
|
441
483
|
title,
|
|
442
484
|
body: messageBody,
|
|
443
|
-
|
|
485
|
+
workspace,
|
|
444
486
|
});
|
|
445
487
|
ctx.log('INFO', `Notification broadcast: ${title}`);
|
|
446
488
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
447
489
|
res.end(JSON.stringify({ success: true }));
|
|
448
490
|
}
|
|
491
|
+
// ============================================================================
|
|
492
|
+
// POST /api/send — send a message to a resolved agent terminal
|
|
493
|
+
// ============================================================================
|
|
494
|
+
async function handleSend(req, res, ctx) {
|
|
495
|
+
const body = await parseJsonBody(req);
|
|
496
|
+
// Validate required fields
|
|
497
|
+
const to = typeof body.to === 'string' ? body.to.trim() : '';
|
|
498
|
+
const message = typeof body.message === 'string' ? body.message.trim() : '';
|
|
499
|
+
if (!to) {
|
|
500
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
501
|
+
res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Missing or empty "to" field' }));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!message) {
|
|
505
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
506
|
+
res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Missing or empty "message" field' }));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
// Optional fields
|
|
510
|
+
const from = typeof body.from === 'string' ? body.from : undefined;
|
|
511
|
+
const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
|
|
512
|
+
const fromWorkspace = typeof body.fromWorkspace === 'string' ? body.fromWorkspace : undefined;
|
|
513
|
+
const options = typeof body.options === 'object' && body.options !== null
|
|
514
|
+
? body.options
|
|
515
|
+
: {};
|
|
516
|
+
const raw = options.raw === true;
|
|
517
|
+
const noEnter = options.noEnter === true;
|
|
518
|
+
const interrupt = options.interrupt === true;
|
|
519
|
+
// Resolve the target address to a terminal ID
|
|
520
|
+
const result = resolveTarget(to, workspace);
|
|
521
|
+
if (isResolveError(result)) {
|
|
522
|
+
const statusCode = result.code === 'AMBIGUOUS' ? 409
|
|
523
|
+
: result.code === 'NO_CONTEXT' ? 400
|
|
524
|
+
: 404;
|
|
525
|
+
// Map NO_CONTEXT to INVALID_PARAMS per plan's error contract
|
|
526
|
+
const errorCode = result.code === 'NO_CONTEXT' ? 'INVALID_PARAMS' : result.code;
|
|
527
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
528
|
+
res.end(JSON.stringify({ error: errorCode, message: result.message }));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
// Get the terminal session
|
|
532
|
+
const manager = getTerminalManager();
|
|
533
|
+
const session = manager.getSession(result.terminalId);
|
|
534
|
+
if (!session) {
|
|
535
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
536
|
+
res.end(JSON.stringify({
|
|
537
|
+
error: 'NOT_FOUND',
|
|
538
|
+
message: `Terminal session ${result.terminalId} not found (agent '${result.agent}' resolved but terminal is gone).`,
|
|
539
|
+
}));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
// Format the message based on sender/target
|
|
543
|
+
const isArchitectTarget = result.agent === 'architect';
|
|
544
|
+
let formattedMessage;
|
|
545
|
+
if (isArchitectTarget && from) {
|
|
546
|
+
// Builder → Architect
|
|
547
|
+
formattedMessage = formatBuilderMessage(from, message, undefined, raw);
|
|
548
|
+
}
|
|
549
|
+
else if (!isArchitectTarget) {
|
|
550
|
+
// Architect → Builder (or any → builder)
|
|
551
|
+
formattedMessage = formatArchitectMessage(message, undefined, raw);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
// Unknown sender to architect — use raw
|
|
555
|
+
formattedMessage = raw ? message : formatArchitectMessage(message, undefined, false);
|
|
556
|
+
}
|
|
557
|
+
// Optionally interrupt first
|
|
558
|
+
if (interrupt) {
|
|
559
|
+
session.write('\x03'); // Ctrl+C
|
|
560
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
561
|
+
}
|
|
562
|
+
// Write the message to the terminal
|
|
563
|
+
session.write(formattedMessage);
|
|
564
|
+
// Send Enter to submit (unless noEnter)
|
|
565
|
+
if (!noEnter) {
|
|
566
|
+
session.write('\r');
|
|
567
|
+
}
|
|
568
|
+
// Broadcast structured message to WebSocket subscribers
|
|
569
|
+
const senderWorkspace = fromWorkspace ?? workspace ?? 'unknown';
|
|
570
|
+
broadcastMessage({
|
|
571
|
+
type: 'message',
|
|
572
|
+
from: {
|
|
573
|
+
project: path.basename(senderWorkspace),
|
|
574
|
+
agent: from ?? 'unknown',
|
|
575
|
+
},
|
|
576
|
+
to: {
|
|
577
|
+
project: path.basename(result.workspacePath),
|
|
578
|
+
agent: result.agent,
|
|
579
|
+
},
|
|
580
|
+
content: message,
|
|
581
|
+
metadata: { raw, source: 'api' },
|
|
582
|
+
timestamp: new Date().toISOString(),
|
|
583
|
+
});
|
|
584
|
+
ctx.log('INFO', `Message sent: ${from ?? 'unknown'} → ${result.agent} (terminal ${result.terminalId.slice(0, 8)}...)`);
|
|
585
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
586
|
+
res.end(JSON.stringify({
|
|
587
|
+
ok: true,
|
|
588
|
+
terminalId: result.terminalId,
|
|
589
|
+
resolvedTo: result.agent,
|
|
590
|
+
}));
|
|
591
|
+
}
|
|
449
592
|
async function handleBrowse(res, url) {
|
|
450
593
|
const inputPath = url.searchParams.get('path') || '';
|
|
451
594
|
try {
|
|
@@ -458,19 +601,19 @@ async function handleBrowse(res, url) {
|
|
|
458
601
|
res.end(JSON.stringify({ suggestions: [], error: err.message }));
|
|
459
602
|
}
|
|
460
603
|
}
|
|
461
|
-
async function
|
|
604
|
+
async function handleCreateWorkspace(req, res, ctx) {
|
|
462
605
|
const body = await parseJsonBody(req);
|
|
463
606
|
const parentPath = body.parent;
|
|
464
|
-
const
|
|
465
|
-
if (!parentPath || !
|
|
607
|
+
const workspaceName = body.name;
|
|
608
|
+
if (!parentPath || !workspaceName) {
|
|
466
609
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
467
610
|
res.end(JSON.stringify({ success: false, error: 'Missing parent or name' }));
|
|
468
611
|
return;
|
|
469
612
|
}
|
|
470
|
-
// Validate
|
|
471
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(
|
|
613
|
+
// Validate workspace name
|
|
614
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(workspaceName)) {
|
|
472
615
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
473
|
-
res.end(JSON.stringify({ success: false, error: 'Invalid
|
|
616
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid workspace name' }));
|
|
474
617
|
return;
|
|
475
618
|
}
|
|
476
619
|
// Expand ~ to home directory
|
|
@@ -484,77 +627,76 @@ async function handleCreateProject(req, res, ctx) {
|
|
|
484
627
|
res.end(JSON.stringify({ success: false, error: `Parent directory does not exist: ${parentPath}` }));
|
|
485
628
|
return;
|
|
486
629
|
}
|
|
487
|
-
const
|
|
488
|
-
// Check if
|
|
489
|
-
if (fs.existsSync(
|
|
630
|
+
const workspacePath = path.join(expandedParent, workspaceName);
|
|
631
|
+
// Check if workspace already exists
|
|
632
|
+
if (fs.existsSync(workspacePath)) {
|
|
490
633
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
491
|
-
res.end(JSON.stringify({ success: false, error: `Directory already exists: ${
|
|
634
|
+
res.end(JSON.stringify({ success: false, error: `Directory already exists: ${workspacePath}` }));
|
|
492
635
|
return;
|
|
493
636
|
}
|
|
494
637
|
try {
|
|
495
638
|
// Run codev init (it creates the directory)
|
|
496
|
-
|
|
639
|
+
await execAsync(`codev init --yes "${workspaceName}"`, {
|
|
497
640
|
cwd: expandedParent,
|
|
498
|
-
stdio: 'pipe',
|
|
499
641
|
timeout: 60000,
|
|
500
642
|
});
|
|
501
643
|
// Launch the instance
|
|
502
|
-
const launchResult = await launchInstance(
|
|
644
|
+
const launchResult = await launchInstance(workspacePath);
|
|
503
645
|
if (!launchResult.success) {
|
|
504
646
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
505
647
|
res.end(JSON.stringify({ success: false, error: launchResult.error }));
|
|
506
648
|
return;
|
|
507
649
|
}
|
|
508
650
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
509
|
-
res.end(JSON.stringify({ success: true,
|
|
651
|
+
res.end(JSON.stringify({ success: true, workspacePath }));
|
|
510
652
|
}
|
|
511
653
|
catch (err) {
|
|
512
654
|
// Clean up on failure
|
|
513
655
|
try {
|
|
514
|
-
if (fs.existsSync(
|
|
515
|
-
fs.rmSync(
|
|
656
|
+
if (fs.existsSync(workspacePath)) {
|
|
657
|
+
fs.rmSync(workspacePath, { recursive: true });
|
|
516
658
|
}
|
|
517
659
|
}
|
|
518
660
|
catch {
|
|
519
661
|
// Ignore cleanup errors
|
|
520
662
|
}
|
|
521
663
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
522
|
-
res.end(JSON.stringify({ success: false, error: `Failed to create
|
|
664
|
+
res.end(JSON.stringify({ success: false, error: `Failed to create workspace: ${err.message}` }));
|
|
523
665
|
}
|
|
524
666
|
}
|
|
525
667
|
async function handleLaunchInstance(req, res) {
|
|
526
668
|
const body = await parseJsonBody(req);
|
|
527
|
-
let
|
|
528
|
-
if (!
|
|
669
|
+
let workspacePath = body.workspacePath;
|
|
670
|
+
if (!workspacePath) {
|
|
529
671
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
530
|
-
res.end(JSON.stringify({ success: false, error: 'Missing
|
|
672
|
+
res.end(JSON.stringify({ success: false, error: 'Missing workspacePath' }));
|
|
531
673
|
return;
|
|
532
674
|
}
|
|
533
675
|
// Expand ~ to home directory
|
|
534
|
-
if (
|
|
535
|
-
|
|
676
|
+
if (workspacePath.startsWith('~')) {
|
|
677
|
+
workspacePath = workspacePath.replace('~', homedir());
|
|
536
678
|
}
|
|
537
679
|
// Reject relative paths — tower daemon CWD is unpredictable
|
|
538
|
-
if (!path.isAbsolute(
|
|
680
|
+
if (!path.isAbsolute(workspacePath)) {
|
|
539
681
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
540
682
|
res.end(JSON.stringify({
|
|
541
683
|
success: false,
|
|
542
|
-
error: `Relative paths are not supported. Use an absolute path (e.g., /Users/.../
|
|
684
|
+
error: `Relative paths are not supported. Use an absolute path (e.g., /Users/.../workspace or ~/Development/workspace).`,
|
|
543
685
|
}));
|
|
544
686
|
return;
|
|
545
687
|
}
|
|
546
688
|
// Normalize path (resolve .. segments, trailing slashes)
|
|
547
|
-
|
|
548
|
-
const result = await launchInstance(
|
|
689
|
+
workspacePath = path.resolve(workspacePath);
|
|
690
|
+
const result = await launchInstance(workspacePath);
|
|
549
691
|
res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
550
692
|
res.end(JSON.stringify(result));
|
|
551
693
|
}
|
|
552
694
|
async function handleStopInstance(req, res) {
|
|
553
695
|
const body = await parseJsonBody(req);
|
|
554
|
-
const targetPath = body.
|
|
696
|
+
const targetPath = body.workspacePath;
|
|
555
697
|
if (!targetPath) {
|
|
556
698
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
557
|
-
res.end(JSON.stringify({ success: false, error: 'Missing
|
|
699
|
+
res.end(JSON.stringify({ success: false, error: 'Missing workspacePath' }));
|
|
558
700
|
return;
|
|
559
701
|
}
|
|
560
702
|
const result = await stopInstance(targetPath);
|
|
@@ -578,52 +720,52 @@ function handleDashboard(res, ctx) {
|
|
|
578
720
|
}
|
|
579
721
|
}
|
|
580
722
|
// ============================================================================
|
|
581
|
-
//
|
|
723
|
+
// Workspace-scoped route handler
|
|
582
724
|
// ============================================================================
|
|
583
|
-
async function
|
|
725
|
+
async function handleWorkspaceRoutes(req, res, ctx, url) {
|
|
584
726
|
const pathParts = url.pathname.split('/');
|
|
585
|
-
// ['', '
|
|
727
|
+
// ['', 'workspace', base64urlPath, ...rest]
|
|
586
728
|
const encodedPath = pathParts[2];
|
|
587
729
|
const subPath = pathParts.slice(3).join('/');
|
|
588
730
|
if (!encodedPath) {
|
|
589
731
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
590
|
-
res.end(JSON.stringify({ error: 'Missing
|
|
732
|
+
res.end(JSON.stringify({ error: 'Missing workspace path' }));
|
|
591
733
|
return;
|
|
592
734
|
}
|
|
593
735
|
// Decode Base64URL (RFC 4648)
|
|
594
|
-
let
|
|
736
|
+
let workspacePath;
|
|
595
737
|
try {
|
|
596
|
-
|
|
738
|
+
workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
597
739
|
// Support both POSIX (/) and Windows (C:\) paths
|
|
598
|
-
if (!
|
|
599
|
-
throw new Error('Invalid
|
|
740
|
+
if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
|
|
741
|
+
throw new Error('Invalid workspace path');
|
|
600
742
|
}
|
|
601
743
|
// Normalize to resolve symlinks (e.g. /var/folders → /private/var/folders on macOS)
|
|
602
|
-
|
|
744
|
+
workspacePath = normalizeWorkspacePath(workspacePath);
|
|
603
745
|
}
|
|
604
746
|
catch {
|
|
605
747
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
606
|
-
res.end(JSON.stringify({ error: 'Invalid
|
|
748
|
+
res.end(JSON.stringify({ error: 'Invalid workspace path encoding' }));
|
|
607
749
|
return;
|
|
608
750
|
}
|
|
609
751
|
// Phase 4 (Spec 0090): Tower handles everything directly
|
|
610
752
|
const isApiCall = subPath.startsWith('api/') || subPath === 'api';
|
|
611
753
|
const isWsPath = subPath.startsWith('ws/') || subPath === 'ws';
|
|
612
|
-
// Tunnel endpoints are tower-level, not
|
|
754
|
+
// Tunnel endpoints are tower-level, not workspace-scoped, but the React
|
|
613
755
|
// dashboard uses relative paths (./api/tunnel/...) which resolve to
|
|
614
|
-
// /
|
|
756
|
+
// /workspace/<encoded>/api/tunnel/... in workspace context. Handle here by
|
|
615
757
|
// extracting the tunnel sub-path and dispatching to handleTunnelEndpoint().
|
|
616
758
|
if (subPath.startsWith('api/tunnel/')) {
|
|
617
759
|
const tunnelSub = subPath.slice('api/tunnel/'.length); // e.g. "status", "connect", "disconnect"
|
|
618
760
|
await handleTunnelEndpoint(req, res, tunnelSub);
|
|
619
761
|
return;
|
|
620
762
|
}
|
|
621
|
-
// GET /file?path=<relative-path> — Read
|
|
763
|
+
// GET /file?path=<relative-path> — Read workspace file by path
|
|
622
764
|
if (req.method === 'GET' && subPath === 'file' && url.searchParams.has('path')) {
|
|
623
765
|
const relPath = url.searchParams.get('path');
|
|
624
|
-
const fullPath = path.resolve(
|
|
625
|
-
// Security: ensure resolved path stays within
|
|
626
|
-
if (!fullPath.startsWith(
|
|
766
|
+
const fullPath = path.resolve(workspacePath, relPath);
|
|
767
|
+
// Security: ensure resolved path stays within workspace directory
|
|
768
|
+
if (!fullPath.startsWith(workspacePath + path.sep) && fullPath !== workspacePath) {
|
|
627
769
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
628
770
|
res.end('Forbidden');
|
|
629
771
|
return;
|
|
@@ -643,7 +785,7 @@ async function handleProjectRoutes(req, res, ctx, url) {
|
|
|
643
785
|
// 1. Not an API call
|
|
644
786
|
// 2. Not a WebSocket path
|
|
645
787
|
// 3. React dashboard is available
|
|
646
|
-
// 4.
|
|
788
|
+
// 4. Workspace doesn't need to be running for static files
|
|
647
789
|
if (!isApiCall && !isWsPath && ctx.hasReactDashboard) {
|
|
648
790
|
// Determine which static file to serve
|
|
649
791
|
let staticPath;
|
|
@@ -664,61 +806,61 @@ async function handleProjectRoutes(req, res, ctx, url) {
|
|
|
664
806
|
return;
|
|
665
807
|
}
|
|
666
808
|
}
|
|
667
|
-
// Phase 4 (Spec 0090): Handle
|
|
809
|
+
// Phase 4 (Spec 0090): Handle workspace APIs directly instead of proxying to dashboard-server
|
|
668
810
|
if (isApiCall) {
|
|
669
811
|
const apiPath = subPath.replace(/^api\/?/, '');
|
|
670
|
-
// GET /api/state - Return
|
|
812
|
+
// GET /api/state - Return workspace state (architect, builders, shells)
|
|
671
813
|
if (req.method === 'GET' && (apiPath === 'state' || apiPath === '')) {
|
|
672
|
-
return
|
|
814
|
+
return handleWorkspaceState(res, workspacePath);
|
|
673
815
|
}
|
|
674
816
|
// POST /api/tabs/shell - Create a new shell terminal
|
|
675
817
|
if (req.method === 'POST' && apiPath === 'tabs/shell') {
|
|
676
|
-
return
|
|
818
|
+
return handleWorkspaceShellCreate(res, ctx, workspacePath);
|
|
677
819
|
}
|
|
678
820
|
// POST /api/tabs/file - Create a file tab (Spec 0092)
|
|
679
821
|
if (req.method === 'POST' && apiPath === 'tabs/file') {
|
|
680
|
-
return
|
|
822
|
+
return handleWorkspaceFileTabCreate(req, res, ctx, workspacePath);
|
|
681
823
|
}
|
|
682
824
|
// GET /api/file/:id - Get file content as JSON (Spec 0092)
|
|
683
825
|
const fileGetMatch = apiPath.match(/^file\/([^/]+)$/);
|
|
684
826
|
if (req.method === 'GET' && fileGetMatch) {
|
|
685
|
-
return
|
|
827
|
+
return handleWorkspaceFileGet(res, ctx, workspacePath, fileGetMatch[1]);
|
|
686
828
|
}
|
|
687
829
|
// GET /api/file/:id/raw - Get raw file content (for images/video) (Spec 0092)
|
|
688
830
|
const fileRawMatch = apiPath.match(/^file\/([^/]+)\/raw$/);
|
|
689
831
|
if (req.method === 'GET' && fileRawMatch) {
|
|
690
|
-
return
|
|
832
|
+
return handleWorkspaceFileRaw(res, ctx, workspacePath, fileRawMatch[1]);
|
|
691
833
|
}
|
|
692
834
|
// POST /api/file/:id/save - Save file content (Spec 0092)
|
|
693
835
|
const fileSaveMatch = apiPath.match(/^file\/([^/]+)\/save$/);
|
|
694
836
|
if (req.method === 'POST' && fileSaveMatch) {
|
|
695
|
-
return
|
|
837
|
+
return handleWorkspaceFileSave(req, res, ctx, workspacePath, fileSaveMatch[1]);
|
|
696
838
|
}
|
|
697
839
|
// DELETE /api/tabs/:id - Delete a terminal or file tab
|
|
698
840
|
const deleteMatch = apiPath.match(/^tabs\/(.+)$/);
|
|
699
841
|
if (req.method === 'DELETE' && deleteMatch) {
|
|
700
|
-
return
|
|
842
|
+
return handleWorkspaceTabDelete(res, ctx, workspacePath, deleteMatch[1]);
|
|
701
843
|
}
|
|
702
|
-
// POST /api/stop - Stop all terminals for
|
|
844
|
+
// POST /api/stop - Stop all terminals for workspace
|
|
703
845
|
if (req.method === 'POST' && apiPath === 'stop') {
|
|
704
|
-
return
|
|
846
|
+
return handleWorkspaceStopAll(res, workspacePath);
|
|
705
847
|
}
|
|
706
|
-
// GET /api/files - Return
|
|
848
|
+
// GET /api/files - Return workspace directory tree for file browser (Spec 0092)
|
|
707
849
|
if (req.method === 'GET' && apiPath === 'files') {
|
|
708
|
-
return
|
|
850
|
+
return handleWorkspaceFiles(res, url, workspacePath);
|
|
709
851
|
}
|
|
710
852
|
// GET /api/git/status - Return git status for file browser (Spec 0092)
|
|
711
853
|
if (req.method === 'GET' && apiPath === 'git/status') {
|
|
712
|
-
return
|
|
854
|
+
return handleWorkspaceGitStatus(res, ctx, workspacePath);
|
|
713
855
|
}
|
|
714
856
|
// GET /api/files/recent - Return recently opened file tabs (Spec 0092)
|
|
715
857
|
if (req.method === 'GET' && apiPath === 'files/recent') {
|
|
716
|
-
return
|
|
858
|
+
return handleWorkspaceRecentFiles(res, workspacePath);
|
|
717
859
|
}
|
|
718
860
|
// GET /api/annotate/:tabId/* — Serve rich annotator template and sub-APIs
|
|
719
861
|
const annotateMatch = apiPath.match(/^annotate\/([^/]+)(\/(.*))?$/);
|
|
720
862
|
if (annotateMatch) {
|
|
721
|
-
return
|
|
863
|
+
return handleWorkspaceAnnotate(req, res, ctx, url, workspacePath, annotateMatch);
|
|
722
864
|
}
|
|
723
865
|
// POST /api/paste-image - Upload pasted image to temp file (Issue #252)
|
|
724
866
|
if (req.method === 'POST' && apiPath === 'paste-image') {
|
|
@@ -771,6 +913,14 @@ async function handleProjectRoutes(req, res, ctx, url) {
|
|
|
771
913
|
});
|
|
772
914
|
return;
|
|
773
915
|
}
|
|
916
|
+
// GET /api/overview - Work view overview data (Spec 0126 Phase 4)
|
|
917
|
+
if (req.method === 'GET' && apiPath === 'overview') {
|
|
918
|
+
return handleOverview(res, url, workspacePath);
|
|
919
|
+
}
|
|
920
|
+
// POST /api/overview/refresh - Invalidate overview cache (Spec 0126 Phase 4)
|
|
921
|
+
if (req.method === 'POST' && apiPath === 'overview/refresh') {
|
|
922
|
+
return handleOverviewRefresh(res);
|
|
923
|
+
}
|
|
774
924
|
// Unhandled API route
|
|
775
925
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
776
926
|
res.end(JSON.stringify({ error: 'API endpoint not found', path: apiPath }));
|
|
@@ -794,24 +944,23 @@ async function handleProjectRoutes(req, res, ctx, url) {
|
|
|
794
944
|
res.end('Not found');
|
|
795
945
|
}
|
|
796
946
|
// ============================================================================
|
|
797
|
-
//
|
|
947
|
+
// Workspace API sub-handlers
|
|
798
948
|
// ============================================================================
|
|
799
|
-
async function
|
|
800
|
-
// Refresh cache via
|
|
949
|
+
async function handleWorkspaceState(res, workspacePath) {
|
|
950
|
+
// Refresh cache via getTerminalsForWorkspace (handles SQLite sync
|
|
801
951
|
// and shellper reconnection in one place)
|
|
802
|
-
const encodedPath = Buffer.from(
|
|
803
|
-
const proxyUrl = `/
|
|
804
|
-
|
|
952
|
+
const encodedPath = Buffer.from(workspacePath).toString('base64url');
|
|
953
|
+
const proxyUrl = `/workspace/${encodedPath}/`;
|
|
954
|
+
await getTerminalsForWorkspace(workspacePath, proxyUrl);
|
|
805
955
|
// Now read from the refreshed cache
|
|
806
|
-
const entry =
|
|
956
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
807
957
|
const manager = getTerminalManager();
|
|
808
958
|
const state = {
|
|
809
959
|
architect: null,
|
|
810
960
|
builders: [],
|
|
811
961
|
utils: [],
|
|
812
962
|
annotations: [],
|
|
813
|
-
|
|
814
|
-
gateStatus,
|
|
963
|
+
workspaceName: path.basename(workspacePath),
|
|
815
964
|
};
|
|
816
965
|
// Add architect if exists
|
|
817
966
|
if (entry.architect) {
|
|
@@ -845,7 +994,7 @@ async function handleProjectState(res, projectPath) {
|
|
|
845
994
|
if (session) {
|
|
846
995
|
state.builders.push({
|
|
847
996
|
id: builderId,
|
|
848
|
-
name:
|
|
997
|
+
name: builderId,
|
|
849
998
|
port: 0,
|
|
850
999
|
pid: session.pid || 0,
|
|
851
1000
|
status: 'running',
|
|
@@ -870,10 +1019,10 @@ async function handleProjectState(res, projectPath) {
|
|
|
870
1019
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
871
1020
|
res.end(JSON.stringify(state));
|
|
872
1021
|
}
|
|
873
|
-
async function
|
|
1022
|
+
async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
|
|
874
1023
|
try {
|
|
875
1024
|
const manager = getTerminalManager();
|
|
876
|
-
const shellId = getNextShellId(
|
|
1025
|
+
const shellId = getNextShellId(workspacePath);
|
|
877
1026
|
const shellCmd = process.env.SHELL || '/bin/bash';
|
|
878
1027
|
const shellArgs = [];
|
|
879
1028
|
let shellCreated = false;
|
|
@@ -889,25 +1038,23 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
|
|
|
889
1038
|
sessionId,
|
|
890
1039
|
command: shellCmd,
|
|
891
1040
|
args: shellArgs,
|
|
892
|
-
cwd:
|
|
1041
|
+
cwd: workspacePath,
|
|
893
1042
|
env: shellEnv,
|
|
894
|
-
|
|
895
|
-
rows: 50,
|
|
896
|
-
restartOnExit: false,
|
|
1043
|
+
...defaultSessionOptions(),
|
|
897
1044
|
});
|
|
898
1045
|
const replayData = client.getReplayData() ?? Buffer.alloc(0);
|
|
899
1046
|
const shellperInfo = shellperManager.getSessionInfo(sessionId);
|
|
900
1047
|
const session = manager.createSessionRaw({
|
|
901
1048
|
label: `Shell ${shellId.replace('shell-', '')}`,
|
|
902
|
-
cwd:
|
|
1049
|
+
cwd: workspacePath,
|
|
903
1050
|
});
|
|
904
1051
|
const ptySession = manager.getSession(session.id);
|
|
905
1052
|
if (ptySession) {
|
|
906
1053
|
ptySession.attachShellper(client, replayData, shellperInfo.pid, sessionId);
|
|
907
1054
|
}
|
|
908
|
-
const entry =
|
|
1055
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
909
1056
|
entry.shells.set(shellId, session.id);
|
|
910
|
-
saveTerminalSession(session.id,
|
|
1057
|
+
saveTerminalSession(session.id, workspacePath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
|
|
911
1058
|
shellCreated = true;
|
|
912
1059
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
913
1060
|
res.end(JSON.stringify({
|
|
@@ -928,14 +1075,14 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
|
|
|
928
1075
|
const session = await manager.createSession({
|
|
929
1076
|
command: shellCmd,
|
|
930
1077
|
args: shellArgs,
|
|
931
|
-
cwd:
|
|
1078
|
+
cwd: workspacePath,
|
|
932
1079
|
label: `Shell ${shellId.replace('shell-', '')}`,
|
|
933
1080
|
env: process.env,
|
|
934
1081
|
});
|
|
935
|
-
const entry =
|
|
1082
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
936
1083
|
entry.shells.set(shellId, session.id);
|
|
937
|
-
saveTerminalSession(session.id,
|
|
938
|
-
ctx.log('WARN', `Shell ${shellId} for ${
|
|
1084
|
+
saveTerminalSession(session.id, workspacePath, 'shell', shellId, session.pid);
|
|
1085
|
+
ctx.log('WARN', `Shell ${shellId} for ${workspacePath} is non-persistent (shellper unavailable)`);
|
|
939
1086
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
940
1087
|
res.end(JSON.stringify({
|
|
941
1088
|
id: shellId,
|
|
@@ -952,7 +1099,7 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
|
|
|
952
1099
|
res.end(JSON.stringify({ error: err.message }));
|
|
953
1100
|
}
|
|
954
1101
|
}
|
|
955
|
-
async function
|
|
1102
|
+
async function handleWorkspaceFileTabCreate(req, res, ctx, workspacePath) {
|
|
956
1103
|
try {
|
|
957
1104
|
const body = await readBody(req);
|
|
958
1105
|
const { path: filePath, line, terminalId } = JSON.parse(body || '{}');
|
|
@@ -973,12 +1120,12 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
|
|
|
973
1120
|
fullPath = path.join(session.cwd, filePath);
|
|
974
1121
|
}
|
|
975
1122
|
else {
|
|
976
|
-
ctx.log('WARN', `Terminal session ${terminalId} not found, falling back to
|
|
977
|
-
fullPath = path.join(
|
|
1123
|
+
ctx.log('WARN', `Terminal session ${terminalId} not found, falling back to workspace root`);
|
|
1124
|
+
fullPath = path.join(workspacePath, filePath);
|
|
978
1125
|
}
|
|
979
1126
|
}
|
|
980
1127
|
else {
|
|
981
|
-
fullPath = path.join(
|
|
1128
|
+
fullPath = path.join(workspacePath, filePath);
|
|
982
1129
|
}
|
|
983
1130
|
// Security: symlink-aware containment check
|
|
984
1131
|
// For non-existent files, resolve the parent directory to handle
|
|
@@ -995,23 +1142,23 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
|
|
|
995
1142
|
resolvedPath = path.resolve(fullPath);
|
|
996
1143
|
}
|
|
997
1144
|
}
|
|
998
|
-
let
|
|
1145
|
+
let normalizedWorkspace;
|
|
999
1146
|
try {
|
|
1000
|
-
|
|
1147
|
+
normalizedWorkspace = fs.realpathSync(workspacePath);
|
|
1001
1148
|
}
|
|
1002
1149
|
catch {
|
|
1003
|
-
|
|
1150
|
+
normalizedWorkspace = path.resolve(workspacePath);
|
|
1004
1151
|
}
|
|
1005
|
-
const
|
|
1006
|
-
|| resolvedPath ===
|
|
1007
|
-
if (!
|
|
1152
|
+
const isWithinWorkspace = resolvedPath.startsWith(normalizedWorkspace + path.sep)
|
|
1153
|
+
|| resolvedPath === normalizedWorkspace;
|
|
1154
|
+
if (!isWithinWorkspace) {
|
|
1008
1155
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1009
|
-
res.end(JSON.stringify({ error: 'Path outside
|
|
1156
|
+
res.end(JSON.stringify({ error: 'Path outside workspace' }));
|
|
1010
1157
|
return;
|
|
1011
1158
|
}
|
|
1012
1159
|
// Non-existent files still create a tab (spec 0101: file viewer shows "File not found")
|
|
1013
1160
|
const fileExists = fs.existsSync(fullPath);
|
|
1014
|
-
const entry =
|
|
1161
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1015
1162
|
// Check if already open
|
|
1016
1163
|
for (const [id, tab] of entry.fileTabs) {
|
|
1017
1164
|
if (tab.path === fullPath) {
|
|
@@ -1024,7 +1171,7 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
|
|
|
1024
1171
|
const id = `file-${crypto.randomUUID()}`;
|
|
1025
1172
|
const createdAt = Date.now();
|
|
1026
1173
|
entry.fileTabs.set(id, { id, path: fullPath, createdAt });
|
|
1027
|
-
saveFileTab(id,
|
|
1174
|
+
saveFileTab(id, workspacePath, fullPath, createdAt);
|
|
1028
1175
|
ctx.log('INFO', `Created file tab: ${id} for ${path.basename(fullPath)}`);
|
|
1029
1176
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1030
1177
|
res.end(JSON.stringify({ id, existing: false, line, notFound: !fileExists }));
|
|
@@ -1035,8 +1182,8 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
|
|
|
1035
1182
|
res.end(JSON.stringify({ error: err.message }));
|
|
1036
1183
|
}
|
|
1037
1184
|
}
|
|
1038
|
-
function
|
|
1039
|
-
const entry =
|
|
1185
|
+
function handleWorkspaceFileGet(res, ctx, workspacePath, tabId) {
|
|
1186
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1040
1187
|
const tab = entry.fileTabs.get(tabId);
|
|
1041
1188
|
if (!tab) {
|
|
1042
1189
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -1083,8 +1230,8 @@ function handleProjectFileGet(res, ctx, projectPath, tabId) {
|
|
|
1083
1230
|
res.end(JSON.stringify({ error: err.message }));
|
|
1084
1231
|
}
|
|
1085
1232
|
}
|
|
1086
|
-
function
|
|
1087
|
-
const entry =
|
|
1233
|
+
function handleWorkspaceFileRaw(res, ctx, workspacePath, tabId) {
|
|
1234
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1088
1235
|
const tab = entry.fileTabs.get(tabId);
|
|
1089
1236
|
if (!tab) {
|
|
1090
1237
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -1107,8 +1254,8 @@ function handleProjectFileRaw(res, ctx, projectPath, tabId) {
|
|
|
1107
1254
|
res.end(JSON.stringify({ error: err.message }));
|
|
1108
1255
|
}
|
|
1109
1256
|
}
|
|
1110
|
-
async function
|
|
1111
|
-
const entry =
|
|
1257
|
+
async function handleWorkspaceFileSave(req, res, ctx, workspacePath, tabId) {
|
|
1258
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1112
1259
|
const tab = entry.fileTabs.get(tabId);
|
|
1113
1260
|
if (!tab) {
|
|
1114
1261
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -1134,8 +1281,8 @@ async function handleProjectFileSave(req, res, ctx, projectPath, tabId) {
|
|
|
1134
1281
|
res.end(JSON.stringify({ error: err.message }));
|
|
1135
1282
|
}
|
|
1136
1283
|
}
|
|
1137
|
-
async function
|
|
1138
|
-
const entry =
|
|
1284
|
+
async function handleWorkspaceTabDelete(res, ctx, workspacePath, tabId) {
|
|
1285
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1139
1286
|
const manager = getTerminalManager();
|
|
1140
1287
|
// Check if it's a file tab first (Spec 0092, write-through: in-memory + SQLite)
|
|
1141
1288
|
if (tabId.startsWith('file-')) {
|
|
@@ -1185,8 +1332,8 @@ async function handleProjectTabDelete(res, ctx, projectPath, tabId) {
|
|
|
1185
1332
|
res.end(JSON.stringify({ error: 'Tab not found' }));
|
|
1186
1333
|
}
|
|
1187
1334
|
}
|
|
1188
|
-
async function
|
|
1189
|
-
const entry =
|
|
1335
|
+
async function handleWorkspaceStopAll(res, workspacePath) {
|
|
1336
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1190
1337
|
const manager = getTerminalManager();
|
|
1191
1338
|
// Kill all terminals (disable shellper auto-restart if applicable)
|
|
1192
1339
|
if (entry.architect) {
|
|
@@ -1199,13 +1346,13 @@ async function handleProjectStopAll(res, projectPath) {
|
|
|
1199
1346
|
await killTerminalWithShellper(manager, terminalId);
|
|
1200
1347
|
}
|
|
1201
1348
|
// Clear registry
|
|
1202
|
-
|
|
1349
|
+
getWorkspaceTerminals().delete(workspacePath);
|
|
1203
1350
|
// TICK-001: Delete all terminal sessions from SQLite
|
|
1204
|
-
|
|
1351
|
+
deleteWorkspaceTerminalSessions(workspacePath);
|
|
1205
1352
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1206
1353
|
res.end(JSON.stringify({ ok: true }));
|
|
1207
1354
|
}
|
|
1208
|
-
function
|
|
1355
|
+
function handleWorkspaceFiles(res, url, workspacePath) {
|
|
1209
1356
|
const maxDepth = parseInt(url.searchParams.get('depth') || '3', 10);
|
|
1210
1357
|
const ignore = new Set(['.git', 'node_modules', '.builders', 'dist', '.agent-farm', '.next', '.cache', '__pycache__']);
|
|
1211
1358
|
function readTree(dir, depth) {
|
|
@@ -1226,7 +1373,7 @@ function handleProjectFiles(res, url, projectPath) {
|
|
|
1226
1373
|
})
|
|
1227
1374
|
.map(e => {
|
|
1228
1375
|
const fullPath = path.join(dir, e.name);
|
|
1229
|
-
const relativePath = path.relative(
|
|
1376
|
+
const relativePath = path.relative(workspacePath, fullPath);
|
|
1230
1377
|
if (e.isDirectory()) {
|
|
1231
1378
|
return { name: e.name, path: relativePath, type: 'directory', children: readTree(fullPath, depth - 1) };
|
|
1232
1379
|
}
|
|
@@ -1237,15 +1384,15 @@ function handleProjectFiles(res, url, projectPath) {
|
|
|
1237
1384
|
return [];
|
|
1238
1385
|
}
|
|
1239
1386
|
}
|
|
1240
|
-
const tree = readTree(
|
|
1387
|
+
const tree = readTree(workspacePath, maxDepth);
|
|
1241
1388
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1242
1389
|
res.end(JSON.stringify(tree));
|
|
1243
1390
|
}
|
|
1244
|
-
function
|
|
1391
|
+
async function handleWorkspaceGitStatus(res, ctx, workspacePath) {
|
|
1245
1392
|
try {
|
|
1246
1393
|
// Get git status in porcelain format for parsing
|
|
1247
|
-
const result =
|
|
1248
|
-
cwd:
|
|
1394
|
+
const { stdout: result } = await execAsync('git status --porcelain', {
|
|
1395
|
+
cwd: workspacePath,
|
|
1249
1396
|
encoding: 'utf-8',
|
|
1250
1397
|
timeout: 5000,
|
|
1251
1398
|
});
|
|
@@ -1282,8 +1429,8 @@ function handleProjectGitStatus(res, ctx, projectPath) {
|
|
|
1282
1429
|
res.end(JSON.stringify({ modified: [], staged: [], untracked: [], error: err.message }));
|
|
1283
1430
|
}
|
|
1284
1431
|
}
|
|
1285
|
-
function
|
|
1286
|
-
const entry =
|
|
1432
|
+
function handleWorkspaceRecentFiles(res, workspacePath) {
|
|
1433
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1287
1434
|
// Get all file tabs sorted by creation time (most recent first)
|
|
1288
1435
|
const recentFiles = Array.from(entry.fileTabs.values())
|
|
1289
1436
|
.sort((a, b) => b.createdAt - a.createdAt)
|
|
@@ -1292,15 +1439,15 @@ function handleProjectRecentFiles(res, projectPath) {
|
|
|
1292
1439
|
id: tab.id,
|
|
1293
1440
|
path: tab.path,
|
|
1294
1441
|
name: path.basename(tab.path),
|
|
1295
|
-
relativePath: path.relative(
|
|
1442
|
+
relativePath: path.relative(workspacePath, tab.path),
|
|
1296
1443
|
}));
|
|
1297
1444
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1298
1445
|
res.end(JSON.stringify(recentFiles));
|
|
1299
1446
|
}
|
|
1300
|
-
function
|
|
1447
|
+
function handleWorkspaceAnnotate(req, res, ctx, url, workspacePath, annotateMatch) {
|
|
1301
1448
|
const tabId = annotateMatch[1];
|
|
1302
1449
|
const subRoute = annotateMatch[3] || '';
|
|
1303
|
-
const entry =
|
|
1450
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1304
1451
|
const tab = entry.fileTabs.get(tabId);
|
|
1305
1452
|
if (!tab) {
|
|
1306
1453
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -1387,6 +1534,28 @@ function handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch) {
|
|
|
1387
1534
|
}
|
|
1388
1535
|
return;
|
|
1389
1536
|
}
|
|
1537
|
+
// Sub-route: GET /vendor/* — serve bundled vendor libraries (PrismJS, marked, DOMPurify)
|
|
1538
|
+
if (req.method === 'GET' && subRoute.startsWith('vendor/')) {
|
|
1539
|
+
const vendorFile = subRoute.slice('vendor/'.length);
|
|
1540
|
+
// Security: only allow known file extensions and no path traversal
|
|
1541
|
+
if (vendorFile.includes('..') || vendorFile.includes('/') || !/\.(js|css)$/.test(vendorFile)) {
|
|
1542
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1543
|
+
res.end('Bad request');
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
const vendorPath = path.resolve(__dirname, `../../../templates/vendor/${vendorFile}`);
|
|
1547
|
+
try {
|
|
1548
|
+
const content = fs.readFileSync(vendorPath);
|
|
1549
|
+
const contentType = vendorFile.endsWith('.css') ? 'text/css' : 'application/javascript';
|
|
1550
|
+
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=86400' });
|
|
1551
|
+
res.end(content);
|
|
1552
|
+
}
|
|
1553
|
+
catch {
|
|
1554
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1555
|
+
res.end('Not found');
|
|
1556
|
+
}
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1390
1559
|
// Default: serve the annotator HTML template
|
|
1391
1560
|
if (req.method === 'GET' && (subRoute === '' || subRoute === undefined)) {
|
|
1392
1561
|
try {
|