@cluesmith/codev 2.0.3 → 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 +31 -13
- package/dist/agent-farm/cli.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/send.d.ts +22 -2
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +97 -178
- package/dist/agent-farm/commands/send.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 +10 -16
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +24 -5
- 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 +180 -66
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +1 -12
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts +16 -6
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +25 -0
- package/dist/agent-farm/lib/tower-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 +1 -3
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +10 -13
- 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.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +164 -17
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +27 -2
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +8 -2
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +125 -80
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-types.d.ts +0 -2
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +25 -4
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +3 -4
- 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/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/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.map +1 -1
- package/dist/commands/adopt.js +1 -13
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +25 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +251 -39
- 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 +5 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -13
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +53 -62
- package/dist/commands/porch/next.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 +50 -26
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/commands/porch/protocol.js +2 -2
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +3 -1
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +0 -10
- 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 +0 -21
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +0 -57
- package/dist/lib/scaffold.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.map +1 -1
- package/dist/terminal/pty-manager.js +7 -4
- 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 +6 -3
- package/templates/tower.html +1 -41
- package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
- package/dashboard/dist/assets/index-UsH9ixz1.js +0 -136
- package/dashboard/dist/assets/index-UsH9ixz1.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/skeleton/templates/projectlist-archive.md +0 -21
- package/skeleton/templates/projectlist.md +0 -147
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message routing, address resolution, and WebSocket message bus for Tower server.
|
|
3
|
+
* Spec 0110: Messaging Infrastructure — Phases 2 & 3
|
|
4
|
+
*
|
|
5
|
+
* Resolves `[project:]agent` addresses to terminal IDs by querying
|
|
6
|
+
* the workspace terminal registry maintained by tower-terminals.ts.
|
|
7
|
+
* Manages WebSocket subscribers and broadcasts structured message frames.
|
|
8
|
+
*/
|
|
9
|
+
import type { WebSocket } from 'ws';
|
|
10
|
+
/**
|
|
11
|
+
* Structured message frame broadcast to WebSocket subscribers.
|
|
12
|
+
*/
|
|
13
|
+
export interface MessageFrame {
|
|
14
|
+
type: 'message';
|
|
15
|
+
timestamp: string;
|
|
16
|
+
from: {
|
|
17
|
+
project: string;
|
|
18
|
+
agent: string;
|
|
19
|
+
};
|
|
20
|
+
to: {
|
|
21
|
+
project: string;
|
|
22
|
+
agent: string;
|
|
23
|
+
};
|
|
24
|
+
content: string;
|
|
25
|
+
metadata: {
|
|
26
|
+
raw?: boolean;
|
|
27
|
+
source?: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Add a WebSocket subscriber to the message bus.
|
|
32
|
+
* @param ws - The WebSocket connection
|
|
33
|
+
* @param projectFilter - Optional project name to filter messages by
|
|
34
|
+
*/
|
|
35
|
+
export declare function addSubscriber(ws: WebSocket, projectFilter?: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Remove a WebSocket subscriber from the message bus.
|
|
38
|
+
* @param ws - The WebSocket connection to remove
|
|
39
|
+
*/
|
|
40
|
+
export declare function removeSubscriber(ws: WebSocket): void;
|
|
41
|
+
/**
|
|
42
|
+
* Get the count of active subscribers (for testing/monitoring).
|
|
43
|
+
*/
|
|
44
|
+
export declare function getSubscriberCount(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Result of resolving a target address to a terminal.
|
|
47
|
+
*/
|
|
48
|
+
export interface ResolveResult {
|
|
49
|
+
terminalId: string;
|
|
50
|
+
workspacePath: string;
|
|
51
|
+
agent: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Error from address resolution — distinguishes "not found" from "ambiguous".
|
|
55
|
+
*/
|
|
56
|
+
export interface ResolveError {
|
|
57
|
+
code: 'NOT_FOUND' | 'AMBIGUOUS' | 'NO_CONTEXT';
|
|
58
|
+
message: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a `[project:]agent` address to a terminal ID.
|
|
62
|
+
*
|
|
63
|
+
* Resolution logic:
|
|
64
|
+
* 1. Parse target using parseAddress() (case-insensitive)
|
|
65
|
+
* 2. If project specified: find workspace by basename match
|
|
66
|
+
* - Multiple basename matches → AMBIGUOUS error
|
|
67
|
+
* 3. If no project: use fallbackWorkspace
|
|
68
|
+
* - Missing fallbackWorkspace → NO_CONTEXT error
|
|
69
|
+
* 4. Within workspace: match agent against architect, then builders map
|
|
70
|
+
* - Exact match (case-insensitive) first, then tail match
|
|
71
|
+
* - Multiple tail matches → AMBIGUOUS error
|
|
72
|
+
*
|
|
73
|
+
* @param target - Address string: "agent" or "project:agent"
|
|
74
|
+
* @param fallbackWorkspace - Workspace path when no project: prefix is given
|
|
75
|
+
* @returns ResolveResult on success, ResolveError on failure
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveTarget(target: string, fallbackWorkspace?: string): ResolveResult | ResolveError;
|
|
78
|
+
/**
|
|
79
|
+
* Broadcast a structured message frame to all WebSocket subscribers.
|
|
80
|
+
* Filters by project if the subscriber has a projectFilter set.
|
|
81
|
+
*/
|
|
82
|
+
export declare function broadcastMessage(message: MessageFrame): void;
|
|
83
|
+
/**
|
|
84
|
+
* Helper to check if a resolve result is an error.
|
|
85
|
+
*/
|
|
86
|
+
export declare function isResolveError(result: ResolveResult | ResolveError): result is ResolveError;
|
|
87
|
+
//# sourceMappingURL=tower-messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tower-messages.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-messages.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAQpC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE;QAAE,GAAG,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAcD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAEzE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAOpD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;IAC/C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,iBAAiB,CAAC,EAAE,MAAM,GACzB,aAAa,GAAG,YAAY,CA6B9B;AA8GD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAkB5D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,YAAY,GAAG,MAAM,IAAI,YAAY,CAE3F"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message routing, address resolution, and WebSocket message bus for Tower server.
|
|
3
|
+
* Spec 0110: Messaging Infrastructure — Phases 2 & 3
|
|
4
|
+
*
|
|
5
|
+
* Resolves `[project:]agent` addresses to terminal IDs by querying
|
|
6
|
+
* the workspace terminal registry maintained by tower-terminals.ts.
|
|
7
|
+
* Manages WebSocket subscribers and broadcasts structured message frames.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { parseAddress, stripLeadingZeros } from '../utils/agent-names.js';
|
|
11
|
+
import { getWorkspaceTerminals } from './tower-terminals.js';
|
|
12
|
+
/** Active WebSocket subscribers for the message bus. */
|
|
13
|
+
const messageSubscribers = new Set();
|
|
14
|
+
/**
|
|
15
|
+
* Add a WebSocket subscriber to the message bus.
|
|
16
|
+
* @param ws - The WebSocket connection
|
|
17
|
+
* @param projectFilter - Optional project name to filter messages by
|
|
18
|
+
*/
|
|
19
|
+
export function addSubscriber(ws, projectFilter) {
|
|
20
|
+
messageSubscribers.add({ ws, projectFilter });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Remove a WebSocket subscriber from the message bus.
|
|
24
|
+
* @param ws - The WebSocket connection to remove
|
|
25
|
+
*/
|
|
26
|
+
export function removeSubscriber(ws) {
|
|
27
|
+
for (const sub of messageSubscribers) {
|
|
28
|
+
if (sub.ws === ws) {
|
|
29
|
+
messageSubscribers.delete(sub);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the count of active subscribers (for testing/monitoring).
|
|
36
|
+
*/
|
|
37
|
+
export function getSubscriberCount() {
|
|
38
|
+
return messageSubscribers.size;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a `[project:]agent` address to a terminal ID.
|
|
42
|
+
*
|
|
43
|
+
* Resolution logic:
|
|
44
|
+
* 1. Parse target using parseAddress() (case-insensitive)
|
|
45
|
+
* 2. If project specified: find workspace by basename match
|
|
46
|
+
* - Multiple basename matches → AMBIGUOUS error
|
|
47
|
+
* 3. If no project: use fallbackWorkspace
|
|
48
|
+
* - Missing fallbackWorkspace → NO_CONTEXT error
|
|
49
|
+
* 4. Within workspace: match agent against architect, then builders map
|
|
50
|
+
* - Exact match (case-insensitive) first, then tail match
|
|
51
|
+
* - Multiple tail matches → AMBIGUOUS error
|
|
52
|
+
*
|
|
53
|
+
* @param target - Address string: "agent" or "project:agent"
|
|
54
|
+
* @param fallbackWorkspace - Workspace path when no project: prefix is given
|
|
55
|
+
* @returns ResolveResult on success, ResolveError on failure
|
|
56
|
+
*/
|
|
57
|
+
export function resolveTarget(target, fallbackWorkspace) {
|
|
58
|
+
const { project, agent } = parseAddress(target);
|
|
59
|
+
// Validate: empty or whitespace-only agent is a malformed address
|
|
60
|
+
if (!agent || !agent.trim()) {
|
|
61
|
+
return {
|
|
62
|
+
code: 'NO_CONTEXT',
|
|
63
|
+
message: 'Malformed address: agent name is empty.',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Determine the workspace path
|
|
67
|
+
let workspacePath;
|
|
68
|
+
if (project) {
|
|
69
|
+
const result = findWorkspaceByBasename(project);
|
|
70
|
+
if ('code' in result)
|
|
71
|
+
return result;
|
|
72
|
+
workspacePath = result.workspacePath;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (!fallbackWorkspace) {
|
|
76
|
+
return {
|
|
77
|
+
code: 'NO_CONTEXT',
|
|
78
|
+
message: 'Cannot resolve agent without project context.',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
workspacePath = fallbackWorkspace;
|
|
82
|
+
}
|
|
83
|
+
// Resolve agent within the workspace
|
|
84
|
+
return resolveAgentInWorkspace(agent, workspacePath);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Find a workspace path by matching the basename of registered workspace paths.
|
|
88
|
+
*/
|
|
89
|
+
function findWorkspaceByBasename(projectName) {
|
|
90
|
+
const allWorkspaces = getWorkspaceTerminals();
|
|
91
|
+
const matches = [];
|
|
92
|
+
for (const wsPath of allWorkspaces.keys()) {
|
|
93
|
+
if (path.basename(wsPath).toLowerCase() === projectName) {
|
|
94
|
+
matches.push(wsPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (matches.length === 0) {
|
|
98
|
+
return {
|
|
99
|
+
code: 'NOT_FOUND',
|
|
100
|
+
message: `Project '${projectName}' not found. No workspace with that basename is registered.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (matches.length > 1) {
|
|
104
|
+
return {
|
|
105
|
+
code: 'AMBIGUOUS',
|
|
106
|
+
message: `Project '${projectName}' is ambiguous — matches ${matches.length} workspaces: ${matches.join(', ')}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { workspacePath: matches[0] };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve an agent name to a terminal ID within a specific workspace.
|
|
113
|
+
*
|
|
114
|
+
* Checks architect first, then builders by exact match, then builders by tail match.
|
|
115
|
+
*/
|
|
116
|
+
function resolveAgentInWorkspace(agent, workspacePath) {
|
|
117
|
+
const allWorkspaces = getWorkspaceTerminals();
|
|
118
|
+
const entry = allWorkspaces.get(workspacePath);
|
|
119
|
+
if (!entry) {
|
|
120
|
+
return {
|
|
121
|
+
code: 'NOT_FOUND',
|
|
122
|
+
message: `Workspace '${workspacePath}' has no registered terminals.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// Check architect
|
|
126
|
+
if (agent === 'architect' || agent === 'arch') {
|
|
127
|
+
if (!entry.architect) {
|
|
128
|
+
return {
|
|
129
|
+
code: 'NOT_FOUND',
|
|
130
|
+
message: `No architect terminal found in workspace '${path.basename(workspacePath)}'.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { terminalId: entry.architect, workspacePath, agent: 'architect' };
|
|
134
|
+
}
|
|
135
|
+
// Check builders — exact match (case-insensitive)
|
|
136
|
+
for (const [builderId, terminalId] of entry.builders) {
|
|
137
|
+
if (builderId.toLowerCase() === agent) {
|
|
138
|
+
return { terminalId, workspacePath, agent: builderId };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Check builders — tail match with leading-zero stripping
|
|
142
|
+
const strippedAgent = stripLeadingZeros(agent).toLowerCase();
|
|
143
|
+
const tailMatches = [];
|
|
144
|
+
for (const [builderId, terminalId] of entry.builders) {
|
|
145
|
+
if (builderId.toLowerCase().endsWith(`-${strippedAgent}`)) {
|
|
146
|
+
tailMatches.push({ builderId, terminalId });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (tailMatches.length === 1) {
|
|
150
|
+
return {
|
|
151
|
+
terminalId: tailMatches[0].terminalId,
|
|
152
|
+
workspacePath,
|
|
153
|
+
agent: tailMatches[0].builderId,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (tailMatches.length > 1) {
|
|
157
|
+
const candidates = tailMatches.map(m => m.builderId).join(', ');
|
|
158
|
+
return {
|
|
159
|
+
code: 'AMBIGUOUS',
|
|
160
|
+
message: `Agent '${agent}' is ambiguous — matches ${tailMatches.length} builders: ${candidates}. Use the full name.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Check shells — exact match
|
|
164
|
+
for (const [shellId, terminalId] of entry.shells) {
|
|
165
|
+
if (shellId.toLowerCase() === agent) {
|
|
166
|
+
return { terminalId, workspacePath, agent: shellId };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
code: 'NOT_FOUND',
|
|
171
|
+
message: `Agent '${agent}' not found in workspace '${path.basename(workspacePath)}'.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Broadcast a structured message frame to all WebSocket subscribers.
|
|
176
|
+
* Filters by project if the subscriber has a projectFilter set.
|
|
177
|
+
*/
|
|
178
|
+
export function broadcastMessage(message) {
|
|
179
|
+
const payload = JSON.stringify(message);
|
|
180
|
+
for (const sub of messageSubscribers) {
|
|
181
|
+
// Apply project filter: message must involve the filtered project (from or to)
|
|
182
|
+
if (sub.projectFilter) {
|
|
183
|
+
if (message.from.project !== sub.projectFilter && message.to.project !== sub.projectFilter) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
sub.ws.send(payload);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// If send fails, subscriber is likely disconnected — remove it
|
|
192
|
+
messageSubscribers.delete(sub);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Helper to check if a resolve result is an error.
|
|
198
|
+
*/
|
|
199
|
+
export function isResolveError(result) {
|
|
200
|
+
return 'code' in result;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=tower-messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tower-messages.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-messages.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tower-routes.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"tower-routes.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAU7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AA8ClD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,kBAAkB,EAAE,MAAM,cAAc,GAAG,IAAI,CAAC;IAChD,qBAAqB,EAAE,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACjH,YAAY,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IAC1C,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAgDD,wBAAsB,aAAa,CACjC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,GAAG,EAAE,YAAY,GAChB,OAAO,CAAC,IAAI,CAAC,CAuEf"}
|
|
@@ -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
24
|
import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
|
|
22
25
|
import { handleTunnelEndpoint } from './tower-tunnel.js';
|
|
26
|
+
import { resolveTarget, broadcastMessage, isResolveError } from './tower-messages.js';
|
|
27
|
+
import { formatArchitectMessage, formatBuilderMessage } from '../utils/message-format.js';
|
|
23
28
|
import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
|
|
24
|
-
import {
|
|
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
|
// ============================================================================
|
|
@@ -40,12 +48,15 @@ const ROUTES = {
|
|
|
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
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
|
};
|
|
@@ -171,7 +182,6 @@ async function handleWorkspaceAction(req, res, ctx, match) {
|
|
|
171
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
|
}
|
|
@@ -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);
|
|
@@ -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, {
|
|
@@ -446,6 +488,107 @@ async function handleNotify(req, res, ctx) {
|
|
|
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 {
|
|
@@ -493,9 +636,8 @@ async function handleCreateWorkspace(req, res, ctx) {
|
|
|
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
|
|
@@ -618,7 +760,7 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
|
|
|
618
760
|
await handleTunnelEndpoint(req, res, tunnelSub);
|
|
619
761
|
return;
|
|
620
762
|
}
|
|
621
|
-
// GET /file?path=<relative-path> — Read workspace file by path
|
|
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
766
|
const fullPath = path.resolve(workspacePath, relPath);
|
|
@@ -771,6 +913,14 @@ async function handleWorkspaceRoutes(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 }));
|
|
@@ -801,7 +951,7 @@ async function handleWorkspaceState(res, workspacePath) {
|
|
|
801
951
|
// and shellper reconnection in one place)
|
|
802
952
|
const encodedPath = Buffer.from(workspacePath).toString('base64url');
|
|
803
953
|
const proxyUrl = `/workspace/${encodedPath}/`;
|
|
804
|
-
|
|
954
|
+
await getTerminalsForWorkspace(workspacePath, proxyUrl);
|
|
805
955
|
// Now read from the refreshed cache
|
|
806
956
|
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
807
957
|
const manager = getTerminalManager();
|
|
@@ -811,7 +961,6 @@ async function handleWorkspaceState(res, workspacePath) {
|
|
|
811
961
|
utils: [],
|
|
812
962
|
annotations: [],
|
|
813
963
|
workspaceName: path.basename(workspacePath),
|
|
814
|
-
gateStatus,
|
|
815
964
|
};
|
|
816
965
|
// Add architect if exists
|
|
817
966
|
if (entry.architect) {
|
|
@@ -845,7 +994,7 @@ async function handleWorkspaceState(res, workspacePath) {
|
|
|
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',
|
|
@@ -891,9 +1040,7 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
|
|
|
891
1040
|
args: shellArgs,
|
|
892
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);
|
|
@@ -1241,10 +1388,10 @@ function handleWorkspaceFiles(res, url, workspacePath) {
|
|
|
1241
1388
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1242
1389
|
res.end(JSON.stringify(tree));
|
|
1243
1390
|
}
|
|
1244
|
-
function handleWorkspaceGitStatus(res, ctx, workspacePath) {
|
|
1391
|
+
async function handleWorkspaceGitStatus(res, ctx, workspacePath) {
|
|
1245
1392
|
try {
|
|
1246
1393
|
// Get git status in porcelain format for parsing
|
|
1247
|
-
const result =
|
|
1394
|
+
const { stdout: result } = await execAsync('git status --porcelain', {
|
|
1248
1395
|
cwd: workspacePath,
|
|
1249
1396
|
encoding: 'utf-8',
|
|
1250
1397
|
timeout: 5000,
|