@cluesmith/codev 2.0.1 → 2.0.3
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-b38SaXk5.js → index-UsH9ixz1.js} +20 -20
- package/dashboard/dist/assets/index-UsH9ixz1.js.map +1 -0
- package/dashboard/dist/index.html +1 -1
- package/dist/agent-farm/cli.js +1 -1
- 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.js +1 -1
- 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.js +5 -5
- 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-worktree.d.ts +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +8 -8
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/spawn.js +3 -3
- 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.js +16 -16
- 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 +20 -20
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +25 -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/tower-instances.d.ts +18 -18
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +89 -89
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- 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 +184 -162
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +23 -19
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +27 -29
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +95 -116
- 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 -10
- 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.js +14 -14
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +2 -2
- package/dist/agent-farm/types.d.ts.map +1 -1
- 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/gate-status.d.ts +2 -2
- package/dist/agent-farm/utils/gate-status.d.ts.map +1 -1
- package/dist/agent-farm/utils/gate-status.js +3 -3
- package/dist/agent-farm/utils/gate-status.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/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/commands/adopt.d.ts +2 -2
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +13 -3
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +1 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +52 -51
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.js +6 -6
- 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 -3
- 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 +43 -40
- 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 +1 -1
- package/dist/commands/porch/prompts.d.ts.map +1 -1
- package/dist/commands/porch/prompts.js +13 -13
- 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 +6 -6
- 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 +11 -11
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +10 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/scaffold.d.ts +13 -0
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +34 -0
- 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/pty-manager.d.ts +1 -1
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +3 -3
- package/dist/terminal/pty-manager.js.map +1 -1
- package/package.json +1 -1
- package/templates/open.html +13 -13
- package/templates/tower.html +63 -72
- 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-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-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/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
|
@@ -18,10 +18,10 @@ import { execSync } from 'node:child_process';
|
|
|
18
18
|
import { homedir, tmpdir } from 'node:os';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { parseJsonBody, isRequestAllowed } from '../utils/server-utils.js';
|
|
21
|
-
import { isRateLimited,
|
|
21
|
+
import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
|
|
22
22
|
import { handleTunnelEndpoint } from './tower-tunnel.js';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
23
|
+
import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
|
|
24
|
+
import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, deleteWorkspaceTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForWorkspace, } from './tower-terminals.js';
|
|
25
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
26
|
const __dirname = path.dirname(__filename);
|
|
27
27
|
// ============================================================================
|
|
@@ -36,14 +36,14 @@ async function readBody(req) {
|
|
|
36
36
|
}
|
|
37
37
|
const ROUTES = {
|
|
38
38
|
'GET /health': (_req, res) => handleHealthCheck(res),
|
|
39
|
-
'GET /api/
|
|
39
|
+
'GET /api/workspaces': (_req, res) => handleListWorkspaces(res),
|
|
40
40
|
'POST /api/terminals': (req, res, _url, ctx) => handleTerminalCreate(req, res, ctx),
|
|
41
41
|
'GET /api/terminals': (_req, res) => handleTerminalList(res),
|
|
42
42
|
'GET /api/status': (_req, res) => handleStatus(res),
|
|
43
43
|
'GET /api/events': (req, res, _url, ctx) => handleSSEEvents(req, res, ctx),
|
|
44
44
|
'POST /api/notify': (req, res, _url, ctx) => handleNotify(req, res, ctx),
|
|
45
45
|
'GET /api/browse': (_req, res, url) => handleBrowse(res, url),
|
|
46
|
-
'POST /api/create': (req, res, _url, ctx) =>
|
|
46
|
+
'POST /api/create': (req, res, _url, ctx) => handleCreateWorkspace(req, res, ctx),
|
|
47
47
|
'POST /api/launch': (req, res) => handleLaunchInstance(req, res),
|
|
48
48
|
'POST /api/stop': (req, res) => handleStopInstance(req, res),
|
|
49
49
|
'GET /': (_req, res, _url, ctx) => handleDashboard(res, ctx),
|
|
@@ -89,19 +89,19 @@ export async function handleRequest(req, res, ctx) {
|
|
|
89
89
|
await handleTunnelEndpoint(req, res, tunnelSub);
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
return await
|
|
92
|
+
// Workspace API: /api/workspaces/:encodedPath/activate|deactivate|status (Spec 0090 Phase 1)
|
|
93
|
+
const workspaceApiMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)\/(activate|deactivate|status)$/);
|
|
94
|
+
if (workspaceApiMatch) {
|
|
95
|
+
return await handleWorkspaceAction(req, res, ctx, workspaceApiMatch);
|
|
96
96
|
}
|
|
97
97
|
// Terminal-specific routes: /api/terminals/:id/* (Spec 0090 Phase 2)
|
|
98
98
|
const terminalRouteMatch = url.pathname.match(/^\/api\/terminals\/([^/]+)(\/.*)?$/);
|
|
99
99
|
if (terminalRouteMatch) {
|
|
100
100
|
return await handleTerminalRoutes(req, res, url, terminalRouteMatch);
|
|
101
101
|
}
|
|
102
|
-
//
|
|
103
|
-
if (url.pathname.startsWith('/
|
|
104
|
-
return await
|
|
102
|
+
// Workspace routes: /workspace/:base64urlPath/* (Spec 0090 Phase 4)
|
|
103
|
+
if (url.pathname.startsWith('/workspace/')) {
|
|
104
|
+
return await handleWorkspaceRoutes(req, res, ctx, url);
|
|
105
105
|
}
|
|
106
106
|
// 404 for everything else
|
|
107
107
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
@@ -123,59 +123,59 @@ async function handleHealthCheck(res) {
|
|
|
123
123
|
res.end(JSON.stringify({
|
|
124
124
|
status: 'healthy',
|
|
125
125
|
uptime: process.uptime(),
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
activeWorkspaces: activeCount,
|
|
127
|
+
totalWorkspaces: instances.length,
|
|
128
128
|
memoryUsage: process.memoryUsage().heapUsed,
|
|
129
129
|
timestamp: new Date().toISOString(),
|
|
130
130
|
}));
|
|
131
131
|
}
|
|
132
|
-
async function
|
|
132
|
+
async function handleListWorkspaces(res) {
|
|
133
133
|
const instances = await getInstances();
|
|
134
|
-
const
|
|
135
|
-
path: i.
|
|
136
|
-
name: i.
|
|
134
|
+
const workspaces = instances.map((i) => ({
|
|
135
|
+
path: i.workspacePath,
|
|
136
|
+
name: i.workspaceName,
|
|
137
137
|
active: i.running,
|
|
138
138
|
proxyUrl: i.proxyUrl,
|
|
139
139
|
terminals: i.terminals.length,
|
|
140
140
|
}));
|
|
141
141
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
142
|
-
res.end(JSON.stringify({
|
|
142
|
+
res.end(JSON.stringify({ workspaces }));
|
|
143
143
|
}
|
|
144
|
-
async function
|
|
144
|
+
async function handleWorkspaceAction(req, res, ctx, match) {
|
|
145
145
|
const [, encodedPath, action] = match;
|
|
146
|
-
let
|
|
146
|
+
let workspacePath;
|
|
147
147
|
try {
|
|
148
|
-
|
|
149
|
-
if (!
|
|
148
|
+
workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
149
|
+
if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
|
|
150
150
|
throw new Error('Invalid path');
|
|
151
151
|
}
|
|
152
|
-
|
|
152
|
+
workspacePath = normalizeWorkspacePath(workspacePath);
|
|
153
153
|
}
|
|
154
154
|
catch {
|
|
155
155
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
156
|
-
res.end(JSON.stringify({ error: 'Invalid
|
|
156
|
+
res.end(JSON.stringify({ error: 'Invalid workspace path encoding' }));
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
|
-
// GET /api/
|
|
159
|
+
// GET /api/workspaces/:path/status
|
|
160
160
|
if (req.method === 'GET' && action === 'status') {
|
|
161
161
|
const instances = await getInstances();
|
|
162
|
-
const instance = instances.find((i) => i.
|
|
162
|
+
const instance = instances.find((i) => i.workspacePath === workspacePath);
|
|
163
163
|
if (!instance) {
|
|
164
164
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
165
|
-
res.end(JSON.stringify({ error: '
|
|
165
|
+
res.end(JSON.stringify({ error: 'Workspace not found' }));
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
169
169
|
res.end(JSON.stringify({
|
|
170
|
-
path: instance.
|
|
171
|
-
name: instance.
|
|
170
|
+
path: instance.workspacePath,
|
|
171
|
+
name: instance.workspaceName,
|
|
172
172
|
active: instance.running,
|
|
173
173
|
terminals: instance.terminals,
|
|
174
174
|
gateStatus: instance.gateStatus,
|
|
175
175
|
}));
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
|
-
// POST /api/
|
|
178
|
+
// POST /api/workspaces/:path/activate
|
|
179
179
|
if (req.method === 'POST' && action === 'activate') {
|
|
180
180
|
// Rate limiting: 10 activations per minute per client
|
|
181
181
|
const clientIp = req.socket.remoteAddress || '127.0.0.1';
|
|
@@ -184,7 +184,7 @@ async function handleProjectAction(req, res, ctx, match) {
|
|
|
184
184
|
res.end(JSON.stringify({ error: 'Too many activations, try again later' }));
|
|
185
185
|
return;
|
|
186
186
|
}
|
|
187
|
-
const result = await launchInstance(
|
|
187
|
+
const result = await launchInstance(workspacePath);
|
|
188
188
|
if (result.success) {
|
|
189
189
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
190
190
|
res.end(JSON.stringify({ success: true, adopted: result.adopted }));
|
|
@@ -195,17 +195,17 @@ async function handleProjectAction(req, res, ctx, match) {
|
|
|
195
195
|
}
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
|
-
// POST /api/
|
|
198
|
+
// POST /api/workspaces/:path/deactivate
|
|
199
199
|
if (req.method === 'POST' && action === 'deactivate') {
|
|
200
|
-
const knownPaths =
|
|
201
|
-
const resolvedPath = fs.existsSync(
|
|
202
|
-
const isKnown = knownPaths.some((p) => p ===
|
|
200
|
+
const knownPaths = getKnownWorkspacePaths();
|
|
201
|
+
const resolvedPath = fs.existsSync(workspacePath) ? fs.realpathSync(workspacePath) : workspacePath;
|
|
202
|
+
const isKnown = knownPaths.some((p) => p === workspacePath || p === resolvedPath);
|
|
203
203
|
if (!isKnown) {
|
|
204
204
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
205
|
-
res.end(JSON.stringify({ ok: false, error: '
|
|
205
|
+
res.end(JSON.stringify({ ok: false, error: 'Workspace not found' }));
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
|
-
const result = await stopInstance(
|
|
208
|
+
const result = await stopInstance(workspacePath);
|
|
209
209
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
210
210
|
res.end(JSON.stringify(result));
|
|
211
211
|
return;
|
|
@@ -224,7 +224,7 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
224
224
|
const env = typeof body.env === 'object' && body.env !== null ? body.env : undefined;
|
|
225
225
|
const label = typeof body.label === 'string' ? body.label : undefined;
|
|
226
226
|
// Optional session persistence via shellper
|
|
227
|
-
const
|
|
227
|
+
const workspacePath = typeof body.workspacePath === 'string' ? body.workspacePath : null;
|
|
228
228
|
const termType = typeof body.type === 'string' && ['builder', 'shell'].includes(body.type) ? body.type : null;
|
|
229
229
|
const roleId = typeof body.roleId === 'string' ? body.roleId : null;
|
|
230
230
|
const requestPersistence = body.persistent === true;
|
|
@@ -260,16 +260,16 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
260
260
|
}
|
|
261
261
|
info = session;
|
|
262
262
|
persistent = true;
|
|
263
|
-
if (
|
|
264
|
-
const entry =
|
|
263
|
+
if (workspacePath && termType && roleId) {
|
|
264
|
+
const entry = getWorkspaceTerminalsEntry(normalizeWorkspacePath(workspacePath));
|
|
265
265
|
if (termType === 'builder') {
|
|
266
266
|
entry.builders.set(roleId, session.id);
|
|
267
267
|
}
|
|
268
268
|
else {
|
|
269
269
|
entry.shells.set(roleId, session.id);
|
|
270
270
|
}
|
|
271
|
-
saveTerminalSession(session.id,
|
|
272
|
-
ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for
|
|
271
|
+
saveTerminalSession(session.id, workspacePath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
|
|
272
|
+
ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for workspace ${workspacePath}`);
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
catch (shellperErr) {
|
|
@@ -281,16 +281,16 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
281
281
|
if (!info) {
|
|
282
282
|
info = await manager.createSession({ command, args, cols, rows, cwd, env, label });
|
|
283
283
|
persistent = false;
|
|
284
|
-
if (
|
|
285
|
-
const entry =
|
|
284
|
+
if (workspacePath && termType && roleId) {
|
|
285
|
+
const entry = getWorkspaceTerminalsEntry(normalizeWorkspacePath(workspacePath));
|
|
286
286
|
if (termType === 'builder') {
|
|
287
287
|
entry.builders.set(roleId, info.id);
|
|
288
288
|
}
|
|
289
289
|
else {
|
|
290
290
|
entry.shells.set(roleId, info.id);
|
|
291
291
|
}
|
|
292
|
-
saveTerminalSession(info.id,
|
|
293
|
-
ctx.log('WARN', `Terminal ${info.id} for ${
|
|
292
|
+
saveTerminalSession(info.id, workspacePath, termType, roleId, info.pid);
|
|
293
|
+
ctx.log('WARN', `Terminal ${info.id} for ${workspacePath} is non-persistent (shellper unavailable)`);
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
@@ -429,7 +429,7 @@ async function handleNotify(req, res, ctx) {
|
|
|
429
429
|
const type = typeof body.type === 'string' ? body.type : 'info';
|
|
430
430
|
const title = typeof body.title === 'string' ? body.title : '';
|
|
431
431
|
const messageBody = typeof body.body === 'string' ? body.body : '';
|
|
432
|
-
const
|
|
432
|
+
const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
|
|
433
433
|
if (!title || !messageBody) {
|
|
434
434
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
435
435
|
res.end(JSON.stringify({ success: false, error: 'Missing title or body' }));
|
|
@@ -440,7 +440,7 @@ async function handleNotify(req, res, ctx) {
|
|
|
440
440
|
type,
|
|
441
441
|
title,
|
|
442
442
|
body: messageBody,
|
|
443
|
-
|
|
443
|
+
workspace,
|
|
444
444
|
});
|
|
445
445
|
ctx.log('INFO', `Notification broadcast: ${title}`);
|
|
446
446
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -458,19 +458,19 @@ async function handleBrowse(res, url) {
|
|
|
458
458
|
res.end(JSON.stringify({ suggestions: [], error: err.message }));
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
|
-
async function
|
|
461
|
+
async function handleCreateWorkspace(req, res, ctx) {
|
|
462
462
|
const body = await parseJsonBody(req);
|
|
463
463
|
const parentPath = body.parent;
|
|
464
|
-
const
|
|
465
|
-
if (!parentPath || !
|
|
464
|
+
const workspaceName = body.name;
|
|
465
|
+
if (!parentPath || !workspaceName) {
|
|
466
466
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
467
467
|
res.end(JSON.stringify({ success: false, error: 'Missing parent or name' }));
|
|
468
468
|
return;
|
|
469
469
|
}
|
|
470
|
-
// Validate
|
|
471
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(
|
|
470
|
+
// Validate workspace name
|
|
471
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(workspaceName)) {
|
|
472
472
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
473
|
-
res.end(JSON.stringify({ success: false, error: 'Invalid
|
|
473
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid workspace name' }));
|
|
474
474
|
return;
|
|
475
475
|
}
|
|
476
476
|
// Expand ~ to home directory
|
|
@@ -484,77 +484,77 @@ async function handleCreateProject(req, res, ctx) {
|
|
|
484
484
|
res.end(JSON.stringify({ success: false, error: `Parent directory does not exist: ${parentPath}` }));
|
|
485
485
|
return;
|
|
486
486
|
}
|
|
487
|
-
const
|
|
488
|
-
// Check if
|
|
489
|
-
if (fs.existsSync(
|
|
487
|
+
const workspacePath = path.join(expandedParent, workspaceName);
|
|
488
|
+
// Check if workspace already exists
|
|
489
|
+
if (fs.existsSync(workspacePath)) {
|
|
490
490
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
491
|
-
res.end(JSON.stringify({ success: false, error: `Directory already exists: ${
|
|
491
|
+
res.end(JSON.stringify({ success: false, error: `Directory already exists: ${workspacePath}` }));
|
|
492
492
|
return;
|
|
493
493
|
}
|
|
494
494
|
try {
|
|
495
495
|
// Run codev init (it creates the directory)
|
|
496
|
-
execSync(`codev init --yes "${
|
|
496
|
+
execSync(`codev init --yes "${workspaceName}"`, {
|
|
497
497
|
cwd: expandedParent,
|
|
498
498
|
stdio: 'pipe',
|
|
499
499
|
timeout: 60000,
|
|
500
500
|
});
|
|
501
501
|
// Launch the instance
|
|
502
|
-
const launchResult = await launchInstance(
|
|
502
|
+
const launchResult = await launchInstance(workspacePath);
|
|
503
503
|
if (!launchResult.success) {
|
|
504
504
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
505
505
|
res.end(JSON.stringify({ success: false, error: launchResult.error }));
|
|
506
506
|
return;
|
|
507
507
|
}
|
|
508
508
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
509
|
-
res.end(JSON.stringify({ success: true,
|
|
509
|
+
res.end(JSON.stringify({ success: true, workspacePath }));
|
|
510
510
|
}
|
|
511
511
|
catch (err) {
|
|
512
512
|
// Clean up on failure
|
|
513
513
|
try {
|
|
514
|
-
if (fs.existsSync(
|
|
515
|
-
fs.rmSync(
|
|
514
|
+
if (fs.existsSync(workspacePath)) {
|
|
515
|
+
fs.rmSync(workspacePath, { recursive: true });
|
|
516
516
|
}
|
|
517
517
|
}
|
|
518
518
|
catch {
|
|
519
519
|
// Ignore cleanup errors
|
|
520
520
|
}
|
|
521
521
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
522
|
-
res.end(JSON.stringify({ success: false, error: `Failed to create
|
|
522
|
+
res.end(JSON.stringify({ success: false, error: `Failed to create workspace: ${err.message}` }));
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
525
|
async function handleLaunchInstance(req, res) {
|
|
526
526
|
const body = await parseJsonBody(req);
|
|
527
|
-
let
|
|
528
|
-
if (!
|
|
527
|
+
let workspacePath = body.workspacePath;
|
|
528
|
+
if (!workspacePath) {
|
|
529
529
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
530
|
-
res.end(JSON.stringify({ success: false, error: 'Missing
|
|
530
|
+
res.end(JSON.stringify({ success: false, error: 'Missing workspacePath' }));
|
|
531
531
|
return;
|
|
532
532
|
}
|
|
533
533
|
// Expand ~ to home directory
|
|
534
|
-
if (
|
|
535
|
-
|
|
534
|
+
if (workspacePath.startsWith('~')) {
|
|
535
|
+
workspacePath = workspacePath.replace('~', homedir());
|
|
536
536
|
}
|
|
537
537
|
// Reject relative paths — tower daemon CWD is unpredictable
|
|
538
|
-
if (!path.isAbsolute(
|
|
538
|
+
if (!path.isAbsolute(workspacePath)) {
|
|
539
539
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
540
540
|
res.end(JSON.stringify({
|
|
541
541
|
success: false,
|
|
542
|
-
error: `Relative paths are not supported. Use an absolute path (e.g., /Users/.../
|
|
542
|
+
error: `Relative paths are not supported. Use an absolute path (e.g., /Users/.../workspace or ~/Development/workspace).`,
|
|
543
543
|
}));
|
|
544
544
|
return;
|
|
545
545
|
}
|
|
546
546
|
// Normalize path (resolve .. segments, trailing slashes)
|
|
547
|
-
|
|
548
|
-
const result = await launchInstance(
|
|
547
|
+
workspacePath = path.resolve(workspacePath);
|
|
548
|
+
const result = await launchInstance(workspacePath);
|
|
549
549
|
res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
550
550
|
res.end(JSON.stringify(result));
|
|
551
551
|
}
|
|
552
552
|
async function handleStopInstance(req, res) {
|
|
553
553
|
const body = await parseJsonBody(req);
|
|
554
|
-
const targetPath = body.
|
|
554
|
+
const targetPath = body.workspacePath;
|
|
555
555
|
if (!targetPath) {
|
|
556
556
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
557
|
-
res.end(JSON.stringify({ success: false, error: 'Missing
|
|
557
|
+
res.end(JSON.stringify({ success: false, error: 'Missing workspacePath' }));
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
560
|
const result = await stopInstance(targetPath);
|
|
@@ -578,52 +578,52 @@ function handleDashboard(res, ctx) {
|
|
|
578
578
|
}
|
|
579
579
|
}
|
|
580
580
|
// ============================================================================
|
|
581
|
-
//
|
|
581
|
+
// Workspace-scoped route handler
|
|
582
582
|
// ============================================================================
|
|
583
|
-
async function
|
|
583
|
+
async function handleWorkspaceRoutes(req, res, ctx, url) {
|
|
584
584
|
const pathParts = url.pathname.split('/');
|
|
585
|
-
// ['', '
|
|
585
|
+
// ['', 'workspace', base64urlPath, ...rest]
|
|
586
586
|
const encodedPath = pathParts[2];
|
|
587
587
|
const subPath = pathParts.slice(3).join('/');
|
|
588
588
|
if (!encodedPath) {
|
|
589
589
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
590
|
-
res.end(JSON.stringify({ error: 'Missing
|
|
590
|
+
res.end(JSON.stringify({ error: 'Missing workspace path' }));
|
|
591
591
|
return;
|
|
592
592
|
}
|
|
593
593
|
// Decode Base64URL (RFC 4648)
|
|
594
|
-
let
|
|
594
|
+
let workspacePath;
|
|
595
595
|
try {
|
|
596
|
-
|
|
596
|
+
workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
597
597
|
// Support both POSIX (/) and Windows (C:\) paths
|
|
598
|
-
if (!
|
|
599
|
-
throw new Error('Invalid
|
|
598
|
+
if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
|
|
599
|
+
throw new Error('Invalid workspace path');
|
|
600
600
|
}
|
|
601
601
|
// Normalize to resolve symlinks (e.g. /var/folders → /private/var/folders on macOS)
|
|
602
|
-
|
|
602
|
+
workspacePath = normalizeWorkspacePath(workspacePath);
|
|
603
603
|
}
|
|
604
604
|
catch {
|
|
605
605
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
606
|
-
res.end(JSON.stringify({ error: 'Invalid
|
|
606
|
+
res.end(JSON.stringify({ error: 'Invalid workspace path encoding' }));
|
|
607
607
|
return;
|
|
608
608
|
}
|
|
609
609
|
// Phase 4 (Spec 0090): Tower handles everything directly
|
|
610
610
|
const isApiCall = subPath.startsWith('api/') || subPath === 'api';
|
|
611
611
|
const isWsPath = subPath.startsWith('ws/') || subPath === 'ws';
|
|
612
|
-
// Tunnel endpoints are tower-level, not
|
|
612
|
+
// Tunnel endpoints are tower-level, not workspace-scoped, but the React
|
|
613
613
|
// dashboard uses relative paths (./api/tunnel/...) which resolve to
|
|
614
|
-
// /
|
|
614
|
+
// /workspace/<encoded>/api/tunnel/... in workspace context. Handle here by
|
|
615
615
|
// extracting the tunnel sub-path and dispatching to handleTunnelEndpoint().
|
|
616
616
|
if (subPath.startsWith('api/tunnel/')) {
|
|
617
617
|
const tunnelSub = subPath.slice('api/tunnel/'.length); // e.g. "status", "connect", "disconnect"
|
|
618
618
|
await handleTunnelEndpoint(req, res, tunnelSub);
|
|
619
619
|
return;
|
|
620
620
|
}
|
|
621
|
-
// GET /file?path=<relative-path> — Read
|
|
621
|
+
// GET /file?path=<relative-path> — Read workspace file by path (for StatusPanel workspace list)
|
|
622
622
|
if (req.method === 'GET' && subPath === 'file' && url.searchParams.has('path')) {
|
|
623
623
|
const relPath = url.searchParams.get('path');
|
|
624
|
-
const fullPath = path.resolve(
|
|
625
|
-
// Security: ensure resolved path stays within
|
|
626
|
-
if (!fullPath.startsWith(
|
|
624
|
+
const fullPath = path.resolve(workspacePath, relPath);
|
|
625
|
+
// Security: ensure resolved path stays within workspace directory
|
|
626
|
+
if (!fullPath.startsWith(workspacePath + path.sep) && fullPath !== workspacePath) {
|
|
627
627
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
628
628
|
res.end('Forbidden');
|
|
629
629
|
return;
|
|
@@ -643,7 +643,7 @@ async function handleProjectRoutes(req, res, ctx, url) {
|
|
|
643
643
|
// 1. Not an API call
|
|
644
644
|
// 2. Not a WebSocket path
|
|
645
645
|
// 3. React dashboard is available
|
|
646
|
-
// 4.
|
|
646
|
+
// 4. Workspace doesn't need to be running for static files
|
|
647
647
|
if (!isApiCall && !isWsPath && ctx.hasReactDashboard) {
|
|
648
648
|
// Determine which static file to serve
|
|
649
649
|
let staticPath;
|
|
@@ -664,61 +664,61 @@ async function handleProjectRoutes(req, res, ctx, url) {
|
|
|
664
664
|
return;
|
|
665
665
|
}
|
|
666
666
|
}
|
|
667
|
-
// Phase 4 (Spec 0090): Handle
|
|
667
|
+
// Phase 4 (Spec 0090): Handle workspace APIs directly instead of proxying to dashboard-server
|
|
668
668
|
if (isApiCall) {
|
|
669
669
|
const apiPath = subPath.replace(/^api\/?/, '');
|
|
670
|
-
// GET /api/state - Return
|
|
670
|
+
// GET /api/state - Return workspace state (architect, builders, shells)
|
|
671
671
|
if (req.method === 'GET' && (apiPath === 'state' || apiPath === '')) {
|
|
672
|
-
return
|
|
672
|
+
return handleWorkspaceState(res, workspacePath);
|
|
673
673
|
}
|
|
674
674
|
// POST /api/tabs/shell - Create a new shell terminal
|
|
675
675
|
if (req.method === 'POST' && apiPath === 'tabs/shell') {
|
|
676
|
-
return
|
|
676
|
+
return handleWorkspaceShellCreate(res, ctx, workspacePath);
|
|
677
677
|
}
|
|
678
678
|
// POST /api/tabs/file - Create a file tab (Spec 0092)
|
|
679
679
|
if (req.method === 'POST' && apiPath === 'tabs/file') {
|
|
680
|
-
return
|
|
680
|
+
return handleWorkspaceFileTabCreate(req, res, ctx, workspacePath);
|
|
681
681
|
}
|
|
682
682
|
// GET /api/file/:id - Get file content as JSON (Spec 0092)
|
|
683
683
|
const fileGetMatch = apiPath.match(/^file\/([^/]+)$/);
|
|
684
684
|
if (req.method === 'GET' && fileGetMatch) {
|
|
685
|
-
return
|
|
685
|
+
return handleWorkspaceFileGet(res, ctx, workspacePath, fileGetMatch[1]);
|
|
686
686
|
}
|
|
687
687
|
// GET /api/file/:id/raw - Get raw file content (for images/video) (Spec 0092)
|
|
688
688
|
const fileRawMatch = apiPath.match(/^file\/([^/]+)\/raw$/);
|
|
689
689
|
if (req.method === 'GET' && fileRawMatch) {
|
|
690
|
-
return
|
|
690
|
+
return handleWorkspaceFileRaw(res, ctx, workspacePath, fileRawMatch[1]);
|
|
691
691
|
}
|
|
692
692
|
// POST /api/file/:id/save - Save file content (Spec 0092)
|
|
693
693
|
const fileSaveMatch = apiPath.match(/^file\/([^/]+)\/save$/);
|
|
694
694
|
if (req.method === 'POST' && fileSaveMatch) {
|
|
695
|
-
return
|
|
695
|
+
return handleWorkspaceFileSave(req, res, ctx, workspacePath, fileSaveMatch[1]);
|
|
696
696
|
}
|
|
697
697
|
// DELETE /api/tabs/:id - Delete a terminal or file tab
|
|
698
698
|
const deleteMatch = apiPath.match(/^tabs\/(.+)$/);
|
|
699
699
|
if (req.method === 'DELETE' && deleteMatch) {
|
|
700
|
-
return
|
|
700
|
+
return handleWorkspaceTabDelete(res, ctx, workspacePath, deleteMatch[1]);
|
|
701
701
|
}
|
|
702
|
-
// POST /api/stop - Stop all terminals for
|
|
702
|
+
// POST /api/stop - Stop all terminals for workspace
|
|
703
703
|
if (req.method === 'POST' && apiPath === 'stop') {
|
|
704
|
-
return
|
|
704
|
+
return handleWorkspaceStopAll(res, workspacePath);
|
|
705
705
|
}
|
|
706
|
-
// GET /api/files - Return
|
|
706
|
+
// GET /api/files - Return workspace directory tree for file browser (Spec 0092)
|
|
707
707
|
if (req.method === 'GET' && apiPath === 'files') {
|
|
708
|
-
return
|
|
708
|
+
return handleWorkspaceFiles(res, url, workspacePath);
|
|
709
709
|
}
|
|
710
710
|
// GET /api/git/status - Return git status for file browser (Spec 0092)
|
|
711
711
|
if (req.method === 'GET' && apiPath === 'git/status') {
|
|
712
|
-
return
|
|
712
|
+
return handleWorkspaceGitStatus(res, ctx, workspacePath);
|
|
713
713
|
}
|
|
714
714
|
// GET /api/files/recent - Return recently opened file tabs (Spec 0092)
|
|
715
715
|
if (req.method === 'GET' && apiPath === 'files/recent') {
|
|
716
|
-
return
|
|
716
|
+
return handleWorkspaceRecentFiles(res, workspacePath);
|
|
717
717
|
}
|
|
718
718
|
// GET /api/annotate/:tabId/* — Serve rich annotator template and sub-APIs
|
|
719
719
|
const annotateMatch = apiPath.match(/^annotate\/([^/]+)(\/(.*))?$/);
|
|
720
720
|
if (annotateMatch) {
|
|
721
|
-
return
|
|
721
|
+
return handleWorkspaceAnnotate(req, res, ctx, url, workspacePath, annotateMatch);
|
|
722
722
|
}
|
|
723
723
|
// POST /api/paste-image - Upload pasted image to temp file (Issue #252)
|
|
724
724
|
if (req.method === 'POST' && apiPath === 'paste-image') {
|
|
@@ -794,23 +794,23 @@ async function handleProjectRoutes(req, res, ctx, url) {
|
|
|
794
794
|
res.end('Not found');
|
|
795
795
|
}
|
|
796
796
|
// ============================================================================
|
|
797
|
-
//
|
|
797
|
+
// Workspace API sub-handlers
|
|
798
798
|
// ============================================================================
|
|
799
|
-
async function
|
|
800
|
-
// Refresh cache via
|
|
799
|
+
async function handleWorkspaceState(res, workspacePath) {
|
|
800
|
+
// Refresh cache via getTerminalsForWorkspace (handles SQLite sync
|
|
801
801
|
// and shellper reconnection in one place)
|
|
802
|
-
const encodedPath = Buffer.from(
|
|
803
|
-
const proxyUrl = `/
|
|
804
|
-
const { gateStatus } = await
|
|
802
|
+
const encodedPath = Buffer.from(workspacePath).toString('base64url');
|
|
803
|
+
const proxyUrl = `/workspace/${encodedPath}/`;
|
|
804
|
+
const { gateStatus } = await getTerminalsForWorkspace(workspacePath, proxyUrl);
|
|
805
805
|
// Now read from the refreshed cache
|
|
806
|
-
const entry =
|
|
806
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
807
807
|
const manager = getTerminalManager();
|
|
808
808
|
const state = {
|
|
809
809
|
architect: null,
|
|
810
810
|
builders: [],
|
|
811
811
|
utils: [],
|
|
812
812
|
annotations: [],
|
|
813
|
-
|
|
813
|
+
workspaceName: path.basename(workspacePath),
|
|
814
814
|
gateStatus,
|
|
815
815
|
};
|
|
816
816
|
// Add architect if exists
|
|
@@ -870,10 +870,10 @@ async function handleProjectState(res, projectPath) {
|
|
|
870
870
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
871
871
|
res.end(JSON.stringify(state));
|
|
872
872
|
}
|
|
873
|
-
async function
|
|
873
|
+
async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
|
|
874
874
|
try {
|
|
875
875
|
const manager = getTerminalManager();
|
|
876
|
-
const shellId = getNextShellId(
|
|
876
|
+
const shellId = getNextShellId(workspacePath);
|
|
877
877
|
const shellCmd = process.env.SHELL || '/bin/bash';
|
|
878
878
|
const shellArgs = [];
|
|
879
879
|
let shellCreated = false;
|
|
@@ -889,7 +889,7 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
|
|
|
889
889
|
sessionId,
|
|
890
890
|
command: shellCmd,
|
|
891
891
|
args: shellArgs,
|
|
892
|
-
cwd:
|
|
892
|
+
cwd: workspacePath,
|
|
893
893
|
env: shellEnv,
|
|
894
894
|
cols: 200,
|
|
895
895
|
rows: 50,
|
|
@@ -899,15 +899,15 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
|
|
|
899
899
|
const shellperInfo = shellperManager.getSessionInfo(sessionId);
|
|
900
900
|
const session = manager.createSessionRaw({
|
|
901
901
|
label: `Shell ${shellId.replace('shell-', '')}`,
|
|
902
|
-
cwd:
|
|
902
|
+
cwd: workspacePath,
|
|
903
903
|
});
|
|
904
904
|
const ptySession = manager.getSession(session.id);
|
|
905
905
|
if (ptySession) {
|
|
906
906
|
ptySession.attachShellper(client, replayData, shellperInfo.pid, sessionId);
|
|
907
907
|
}
|
|
908
|
-
const entry =
|
|
908
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
909
909
|
entry.shells.set(shellId, session.id);
|
|
910
|
-
saveTerminalSession(session.id,
|
|
910
|
+
saveTerminalSession(session.id, workspacePath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
|
|
911
911
|
shellCreated = true;
|
|
912
912
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
913
913
|
res.end(JSON.stringify({
|
|
@@ -928,14 +928,14 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
|
|
|
928
928
|
const session = await manager.createSession({
|
|
929
929
|
command: shellCmd,
|
|
930
930
|
args: shellArgs,
|
|
931
|
-
cwd:
|
|
931
|
+
cwd: workspacePath,
|
|
932
932
|
label: `Shell ${shellId.replace('shell-', '')}`,
|
|
933
933
|
env: process.env,
|
|
934
934
|
});
|
|
935
|
-
const entry =
|
|
935
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
936
936
|
entry.shells.set(shellId, session.id);
|
|
937
|
-
saveTerminalSession(session.id,
|
|
938
|
-
ctx.log('WARN', `Shell ${shellId} for ${
|
|
937
|
+
saveTerminalSession(session.id, workspacePath, 'shell', shellId, session.pid);
|
|
938
|
+
ctx.log('WARN', `Shell ${shellId} for ${workspacePath} is non-persistent (shellper unavailable)`);
|
|
939
939
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
940
940
|
res.end(JSON.stringify({
|
|
941
941
|
id: shellId,
|
|
@@ -952,7 +952,7 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
|
|
|
952
952
|
res.end(JSON.stringify({ error: err.message }));
|
|
953
953
|
}
|
|
954
954
|
}
|
|
955
|
-
async function
|
|
955
|
+
async function handleWorkspaceFileTabCreate(req, res, ctx, workspacePath) {
|
|
956
956
|
try {
|
|
957
957
|
const body = await readBody(req);
|
|
958
958
|
const { path: filePath, line, terminalId } = JSON.parse(body || '{}');
|
|
@@ -973,12 +973,12 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
|
|
|
973
973
|
fullPath = path.join(session.cwd, filePath);
|
|
974
974
|
}
|
|
975
975
|
else {
|
|
976
|
-
ctx.log('WARN', `Terminal session ${terminalId} not found, falling back to
|
|
977
|
-
fullPath = path.join(
|
|
976
|
+
ctx.log('WARN', `Terminal session ${terminalId} not found, falling back to workspace root`);
|
|
977
|
+
fullPath = path.join(workspacePath, filePath);
|
|
978
978
|
}
|
|
979
979
|
}
|
|
980
980
|
else {
|
|
981
|
-
fullPath = path.join(
|
|
981
|
+
fullPath = path.join(workspacePath, filePath);
|
|
982
982
|
}
|
|
983
983
|
// Security: symlink-aware containment check
|
|
984
984
|
// For non-existent files, resolve the parent directory to handle
|
|
@@ -995,23 +995,23 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
|
|
|
995
995
|
resolvedPath = path.resolve(fullPath);
|
|
996
996
|
}
|
|
997
997
|
}
|
|
998
|
-
let
|
|
998
|
+
let normalizedWorkspace;
|
|
999
999
|
try {
|
|
1000
|
-
|
|
1000
|
+
normalizedWorkspace = fs.realpathSync(workspacePath);
|
|
1001
1001
|
}
|
|
1002
1002
|
catch {
|
|
1003
|
-
|
|
1003
|
+
normalizedWorkspace = path.resolve(workspacePath);
|
|
1004
1004
|
}
|
|
1005
|
-
const
|
|
1006
|
-
|| resolvedPath ===
|
|
1007
|
-
if (!
|
|
1005
|
+
const isWithinWorkspace = resolvedPath.startsWith(normalizedWorkspace + path.sep)
|
|
1006
|
+
|| resolvedPath === normalizedWorkspace;
|
|
1007
|
+
if (!isWithinWorkspace) {
|
|
1008
1008
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1009
|
-
res.end(JSON.stringify({ error: 'Path outside
|
|
1009
|
+
res.end(JSON.stringify({ error: 'Path outside workspace' }));
|
|
1010
1010
|
return;
|
|
1011
1011
|
}
|
|
1012
1012
|
// Non-existent files still create a tab (spec 0101: file viewer shows "File not found")
|
|
1013
1013
|
const fileExists = fs.existsSync(fullPath);
|
|
1014
|
-
const entry =
|
|
1014
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1015
1015
|
// Check if already open
|
|
1016
1016
|
for (const [id, tab] of entry.fileTabs) {
|
|
1017
1017
|
if (tab.path === fullPath) {
|
|
@@ -1024,7 +1024,7 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
|
|
|
1024
1024
|
const id = `file-${crypto.randomUUID()}`;
|
|
1025
1025
|
const createdAt = Date.now();
|
|
1026
1026
|
entry.fileTabs.set(id, { id, path: fullPath, createdAt });
|
|
1027
|
-
saveFileTab(id,
|
|
1027
|
+
saveFileTab(id, workspacePath, fullPath, createdAt);
|
|
1028
1028
|
ctx.log('INFO', `Created file tab: ${id} for ${path.basename(fullPath)}`);
|
|
1029
1029
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1030
1030
|
res.end(JSON.stringify({ id, existing: false, line, notFound: !fileExists }));
|
|
@@ -1035,8 +1035,8 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
|
|
|
1035
1035
|
res.end(JSON.stringify({ error: err.message }));
|
|
1036
1036
|
}
|
|
1037
1037
|
}
|
|
1038
|
-
function
|
|
1039
|
-
const entry =
|
|
1038
|
+
function handleWorkspaceFileGet(res, ctx, workspacePath, tabId) {
|
|
1039
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1040
1040
|
const tab = entry.fileTabs.get(tabId);
|
|
1041
1041
|
if (!tab) {
|
|
1042
1042
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -1083,8 +1083,8 @@ function handleProjectFileGet(res, ctx, projectPath, tabId) {
|
|
|
1083
1083
|
res.end(JSON.stringify({ error: err.message }));
|
|
1084
1084
|
}
|
|
1085
1085
|
}
|
|
1086
|
-
function
|
|
1087
|
-
const entry =
|
|
1086
|
+
function handleWorkspaceFileRaw(res, ctx, workspacePath, tabId) {
|
|
1087
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1088
1088
|
const tab = entry.fileTabs.get(tabId);
|
|
1089
1089
|
if (!tab) {
|
|
1090
1090
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -1107,8 +1107,8 @@ function handleProjectFileRaw(res, ctx, projectPath, tabId) {
|
|
|
1107
1107
|
res.end(JSON.stringify({ error: err.message }));
|
|
1108
1108
|
}
|
|
1109
1109
|
}
|
|
1110
|
-
async function
|
|
1111
|
-
const entry =
|
|
1110
|
+
async function handleWorkspaceFileSave(req, res, ctx, workspacePath, tabId) {
|
|
1111
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1112
1112
|
const tab = entry.fileTabs.get(tabId);
|
|
1113
1113
|
if (!tab) {
|
|
1114
1114
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -1134,8 +1134,8 @@ async function handleProjectFileSave(req, res, ctx, projectPath, tabId) {
|
|
|
1134
1134
|
res.end(JSON.stringify({ error: err.message }));
|
|
1135
1135
|
}
|
|
1136
1136
|
}
|
|
1137
|
-
async function
|
|
1138
|
-
const entry =
|
|
1137
|
+
async function handleWorkspaceTabDelete(res, ctx, workspacePath, tabId) {
|
|
1138
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1139
1139
|
const manager = getTerminalManager();
|
|
1140
1140
|
// Check if it's a file tab first (Spec 0092, write-through: in-memory + SQLite)
|
|
1141
1141
|
if (tabId.startsWith('file-')) {
|
|
@@ -1185,8 +1185,8 @@ async function handleProjectTabDelete(res, ctx, projectPath, tabId) {
|
|
|
1185
1185
|
res.end(JSON.stringify({ error: 'Tab not found' }));
|
|
1186
1186
|
}
|
|
1187
1187
|
}
|
|
1188
|
-
async function
|
|
1189
|
-
const entry =
|
|
1188
|
+
async function handleWorkspaceStopAll(res, workspacePath) {
|
|
1189
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1190
1190
|
const manager = getTerminalManager();
|
|
1191
1191
|
// Kill all terminals (disable shellper auto-restart if applicable)
|
|
1192
1192
|
if (entry.architect) {
|
|
@@ -1199,13 +1199,13 @@ async function handleProjectStopAll(res, projectPath) {
|
|
|
1199
1199
|
await killTerminalWithShellper(manager, terminalId);
|
|
1200
1200
|
}
|
|
1201
1201
|
// Clear registry
|
|
1202
|
-
|
|
1202
|
+
getWorkspaceTerminals().delete(workspacePath);
|
|
1203
1203
|
// TICK-001: Delete all terminal sessions from SQLite
|
|
1204
|
-
|
|
1204
|
+
deleteWorkspaceTerminalSessions(workspacePath);
|
|
1205
1205
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1206
1206
|
res.end(JSON.stringify({ ok: true }));
|
|
1207
1207
|
}
|
|
1208
|
-
function
|
|
1208
|
+
function handleWorkspaceFiles(res, url, workspacePath) {
|
|
1209
1209
|
const maxDepth = parseInt(url.searchParams.get('depth') || '3', 10);
|
|
1210
1210
|
const ignore = new Set(['.git', 'node_modules', '.builders', 'dist', '.agent-farm', '.next', '.cache', '__pycache__']);
|
|
1211
1211
|
function readTree(dir, depth) {
|
|
@@ -1226,7 +1226,7 @@ function handleProjectFiles(res, url, projectPath) {
|
|
|
1226
1226
|
})
|
|
1227
1227
|
.map(e => {
|
|
1228
1228
|
const fullPath = path.join(dir, e.name);
|
|
1229
|
-
const relativePath = path.relative(
|
|
1229
|
+
const relativePath = path.relative(workspacePath, fullPath);
|
|
1230
1230
|
if (e.isDirectory()) {
|
|
1231
1231
|
return { name: e.name, path: relativePath, type: 'directory', children: readTree(fullPath, depth - 1) };
|
|
1232
1232
|
}
|
|
@@ -1237,15 +1237,15 @@ function handleProjectFiles(res, url, projectPath) {
|
|
|
1237
1237
|
return [];
|
|
1238
1238
|
}
|
|
1239
1239
|
}
|
|
1240
|
-
const tree = readTree(
|
|
1240
|
+
const tree = readTree(workspacePath, maxDepth);
|
|
1241
1241
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1242
1242
|
res.end(JSON.stringify(tree));
|
|
1243
1243
|
}
|
|
1244
|
-
function
|
|
1244
|
+
function handleWorkspaceGitStatus(res, ctx, workspacePath) {
|
|
1245
1245
|
try {
|
|
1246
1246
|
// Get git status in porcelain format for parsing
|
|
1247
1247
|
const result = execSync('git status --porcelain', {
|
|
1248
|
-
cwd:
|
|
1248
|
+
cwd: workspacePath,
|
|
1249
1249
|
encoding: 'utf-8',
|
|
1250
1250
|
timeout: 5000,
|
|
1251
1251
|
});
|
|
@@ -1282,8 +1282,8 @@ function handleProjectGitStatus(res, ctx, projectPath) {
|
|
|
1282
1282
|
res.end(JSON.stringify({ modified: [], staged: [], untracked: [], error: err.message }));
|
|
1283
1283
|
}
|
|
1284
1284
|
}
|
|
1285
|
-
function
|
|
1286
|
-
const entry =
|
|
1285
|
+
function handleWorkspaceRecentFiles(res, workspacePath) {
|
|
1286
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1287
1287
|
// Get all file tabs sorted by creation time (most recent first)
|
|
1288
1288
|
const recentFiles = Array.from(entry.fileTabs.values())
|
|
1289
1289
|
.sort((a, b) => b.createdAt - a.createdAt)
|
|
@@ -1292,15 +1292,15 @@ function handleProjectRecentFiles(res, projectPath) {
|
|
|
1292
1292
|
id: tab.id,
|
|
1293
1293
|
path: tab.path,
|
|
1294
1294
|
name: path.basename(tab.path),
|
|
1295
|
-
relativePath: path.relative(
|
|
1295
|
+
relativePath: path.relative(workspacePath, tab.path),
|
|
1296
1296
|
}));
|
|
1297
1297
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1298
1298
|
res.end(JSON.stringify(recentFiles));
|
|
1299
1299
|
}
|
|
1300
|
-
function
|
|
1300
|
+
function handleWorkspaceAnnotate(req, res, ctx, url, workspacePath, annotateMatch) {
|
|
1301
1301
|
const tabId = annotateMatch[1];
|
|
1302
1302
|
const subRoute = annotateMatch[3] || '';
|
|
1303
|
-
const entry =
|
|
1303
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1304
1304
|
const tab = entry.fileTabs.get(tabId);
|
|
1305
1305
|
if (!tab) {
|
|
1306
1306
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -1387,6 +1387,28 @@ function handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch) {
|
|
|
1387
1387
|
}
|
|
1388
1388
|
return;
|
|
1389
1389
|
}
|
|
1390
|
+
// Sub-route: GET /vendor/* — serve bundled vendor libraries (PrismJS, marked, DOMPurify)
|
|
1391
|
+
if (req.method === 'GET' && subRoute.startsWith('vendor/')) {
|
|
1392
|
+
const vendorFile = subRoute.slice('vendor/'.length);
|
|
1393
|
+
// Security: only allow known file extensions and no path traversal
|
|
1394
|
+
if (vendorFile.includes('..') || vendorFile.includes('/') || !/\.(js|css)$/.test(vendorFile)) {
|
|
1395
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1396
|
+
res.end('Bad request');
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const vendorPath = path.resolve(__dirname, `../../../templates/vendor/${vendorFile}`);
|
|
1400
|
+
try {
|
|
1401
|
+
const content = fs.readFileSync(vendorPath);
|
|
1402
|
+
const contentType = vendorFile.endsWith('.css') ? 'text/css' : 'application/javascript';
|
|
1403
|
+
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=86400' });
|
|
1404
|
+
res.end(content);
|
|
1405
|
+
}
|
|
1406
|
+
catch {
|
|
1407
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1408
|
+
res.end('Not found');
|
|
1409
|
+
}
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1390
1412
|
// Default: serve the annotator HTML template
|
|
1391
1413
|
if (req.method === 'GET' && (subRoute === '' || subRoute === undefined)) {
|
|
1392
1414
|
try {
|