@cluesmith/codev 2.0.0-rc.61 → 2.0.0-rc.63
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-C7FtNK6Y.css +32 -0
- package/dashboard/dist/assets/index-D6VqWAaI.js +131 -0
- package/dashboard/dist/assets/index-D6VqWAaI.js.map +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +75 -50
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts.map +1 -1
- package/dist/agent-farm/commands/architect.js +38 -48
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/attach.d.ts.map +1 -1
- package/dist/agent-farm/commands/attach.js +14 -35
- package/dist/agent-farm/commands/attach.js.map +1 -1
- package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +17 -18
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/consult.d.ts +3 -4
- package/dist/agent-farm/commands/consult.d.ts.map +1 -1
- package/dist/agent-farm/commands/consult.js +27 -37
- package/dist/agent-farm/commands/consult.js.map +1 -1
- package/dist/agent-farm/commands/open.d.ts.map +1 -1
- package/dist/agent-farm/commands/open.js +17 -31
- package/dist/agent-farm/commands/open.js.map +1 -1
- package/dist/agent-farm/commands/shell.d.ts.map +1 -1
- package/dist/agent-farm/commands/shell.js +27 -38
- package/dist/agent-farm/commands/shell.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +113 -90
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts +7 -20
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +3 -242
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +22 -29
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts.map +1 -1
- package/dist/agent-farm/commands/stop.js +43 -172
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/commands/tower-cloud.d.ts +47 -0
- package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -0
- package/dist/agent-farm/commands/tower-cloud.js +316 -0
- package/dist/agent-farm/commands/tower-cloud.js.map +1 -0
- package/dist/agent-farm/db/index.d.ts +6 -2
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +56 -31
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/migrate.d.ts +0 -4
- package/dist/agent-farm/db/migrate.d.ts.map +1 -1
- package/dist/agent-farm/db/migrate.js +0 -46
- package/dist/agent-farm/db/migrate.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +3 -3
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +3 -17
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/db/types.d.ts +0 -10
- package/dist/agent-farm/db/types.d.ts.map +1 -1
- package/dist/agent-farm/db/types.js +0 -8
- package/dist/agent-farm/db/types.js.map +1 -1
- package/dist/agent-farm/hq-connector.d.ts +1 -1
- package/dist/agent-farm/hq-connector.js +1 -1
- package/dist/agent-farm/lib/cloud-config.d.ts +46 -0
- package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -0
- package/dist/agent-farm/lib/cloud-config.js +106 -0
- package/dist/agent-farm/lib/cloud-config.js.map +1 -0
- package/dist/agent-farm/lib/tower-client.d.ts +7 -5
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts +117 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -0
- package/dist/agent-farm/lib/tunnel-client.js +502 -0
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -0
- package/dist/agent-farm/servers/tower-server.js +559 -341
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/state.d.ts +2 -2
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +6 -16
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/types.d.ts +1 -18
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.d.ts +0 -5
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +0 -31
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/file-tabs.d.ts +27 -0
- package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -0
- package/dist/agent-farm/utils/file-tabs.js +46 -0
- package/dist/agent-farm/utils/file-tabs.js.map +1 -0
- package/dist/agent-farm/utils/gate-status.d.ts +16 -0
- package/dist/agent-farm/utils/gate-status.d.ts.map +1 -0
- package/dist/agent-farm/utils/gate-status.js +79 -0
- package/dist/agent-farm/utils/gate-status.js.map +1 -0
- package/dist/agent-farm/utils/gate-watcher.d.ts +38 -0
- package/dist/agent-farm/utils/gate-watcher.d.ts.map +1 -0
- package/dist/agent-farm/utils/gate-watcher.js +122 -0
- package/dist/agent-farm/utils/gate-watcher.js.map +1 -0
- 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.js +1 -1
- package/dist/agent-farm/utils/notifications.js.map +1 -1
- package/dist/agent-farm/utils/server-utils.d.ts +1 -1
- package/dist/agent-farm/utils/server-utils.js +1 -1
- package/dist/agent-farm/utils/session.d.ts +10 -0
- package/dist/agent-farm/utils/session.d.ts.map +1 -0
- package/dist/agent-farm/utils/session.js +12 -0
- package/dist/agent-farm/utils/session.js.map +1 -0
- package/dist/commands/adopt.js +1 -1
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +23 -14
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +35 -12
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.js +11 -3
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/commands/porch/verdict.d.ts +8 -0
- package/dist/commands/porch/verdict.d.ts.map +1 -1
- package/dist/commands/porch/verdict.js +13 -0
- package/dist/commands/porch/verdict.js.map +1 -1
- package/dist/terminal/pty-session.d.ts +2 -0
- package/dist/terminal/pty-session.d.ts.map +1 -1
- package/dist/terminal/pty-session.js +4 -0
- package/dist/terminal/pty-session.js.map +1 -1
- package/package.json +1 -1
- package/skeleton/.claude/skills/af/SKILL.md +15 -0
- package/skeleton/protocols/spir/prompts/review.md +15 -16
- package/skeleton/protocols/spir/protocol.json +4 -0
- package/skeleton/protocols/spir/templates/review.md +81 -199
- package/skeleton/resources/commands/agent-farm.md +38 -2
- package/templates/tower.html +7 -150
- package/dashboard/dist/assets/index-CXloFYpB.css +0 -32
- package/dashboard/dist/assets/index-Ca2fjOJf.js +0 -131
- package/dashboard/dist/assets/index-Ca2fjOJf.js.map +0 -1
- package/dist/agent-farm/utils/orphan-handler.d.ts +0 -27
- package/dist/agent-farm/utils/orphan-handler.d.ts.map +0 -1
- package/dist/agent-farm/utils/orphan-handler.js +0 -149
- package/dist/agent-farm/utils/orphan-handler.js.map +0 -1
- package/dist/agent-farm/utils/port-registry.d.ts +0 -57
- package/dist/agent-farm/utils/port-registry.d.ts.map +0 -1
- package/dist/agent-farm/utils/port-registry.js +0 -166
- package/dist/agent-farm/utils/port-registry.js.map +0 -1
- package/dist/agent-farm/utils/terminal-ports.d.ts +0 -18
- package/dist/agent-farm/utils/terminal-ports.d.ts.map +0 -1
- package/dist/agent-farm/utils/terminal-ports.js +0 -35
- package/dist/agent-farm/utils/terminal-ports.js.map +0 -1
|
@@ -8,7 +8,6 @@ import { execFile } from 'node:child_process';
|
|
|
8
8
|
import { promisify } from 'node:util';
|
|
9
9
|
import { loadState, clearState } from '../state.js';
|
|
10
10
|
import { logger } from '../utils/logger.js';
|
|
11
|
-
import { killProcess, killProcessTree, isProcessRunning, run } from '../utils/shell.js';
|
|
12
11
|
import { getConfig } from '../utils/config.js';
|
|
13
12
|
import { TowerClient } from '../lib/tower-client.js';
|
|
14
13
|
const execFileAsync = promisify(execFile);
|
|
@@ -16,79 +15,10 @@ const execFileAsync = promisify(execFile);
|
|
|
16
15
|
* Default tower port
|
|
17
16
|
*/
|
|
18
17
|
const DEFAULT_TOWER_PORT = 4100;
|
|
19
|
-
/** Kill a tmux session by name. Uses execFile (no shell) to avoid injection.
|
|
20
|
-
|
|
21
|
-
* to prevent cross-project kills when two projects share the same basename.
|
|
22
|
-
*/
|
|
23
|
-
async function killTmuxSession(sessionName, expectedPid) {
|
|
24
|
-
if (expectedPid && expectedPid > 0) {
|
|
25
|
-
// Verify the session belongs to this project by checking its PID
|
|
26
|
-
try {
|
|
27
|
-
const { stdout } = await execFileAsync('tmux', [
|
|
28
|
-
'list-sessions', '-F', '#{session_name} #{session_pid}',
|
|
29
|
-
]);
|
|
30
|
-
const line = stdout.trim().split('\n').find(l => l.startsWith(sessionName + ' '));
|
|
31
|
-
if (line) {
|
|
32
|
-
const sessionPid = parseInt(line.split(' ')[1], 10);
|
|
33
|
-
if (sessionPid !== expectedPid && !isNaN(sessionPid)) {
|
|
34
|
-
// PID mismatch — this session belongs to a different project
|
|
35
|
-
throw new Error(`Session ${sessionName} PID ${sessionPid} != expected ${expectedPid}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
if (err.message?.includes('PID'))
|
|
41
|
-
throw err;
|
|
42
|
-
// tmux command failed — fall through to kill attempt
|
|
43
|
-
}
|
|
44
|
-
}
|
|
18
|
+
/** Kill a tmux session by name. Uses execFile (no shell) to avoid injection. */
|
|
19
|
+
async function killTmuxSession(sessionName) {
|
|
45
20
|
await execFileAsync('tmux', ['kill-session', '-t', sessionName]);
|
|
46
21
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Find orphan agent-farm processes for this project that aren't in state
|
|
49
|
-
* Returns PIDs of orphaned processes
|
|
50
|
-
*/
|
|
51
|
-
async function findOrphanProcesses(trackedPids) {
|
|
52
|
-
const config = getConfig();
|
|
53
|
-
const projectRoot = config.projectRoot;
|
|
54
|
-
// Pattern to match agent-farm server processes for this project
|
|
55
|
-
// Matches: node .../dist/agent-farm/servers/dashboard-server.js
|
|
56
|
-
// node .../dist/agent-farm/servers/tower-server.js
|
|
57
|
-
// Note: open-server.js removed in Spec 0092 - files served through Tower
|
|
58
|
-
const orphans = [];
|
|
59
|
-
try {
|
|
60
|
-
// Use ps to find node processes, then filter by our project path
|
|
61
|
-
const result = await run('ps -eo pid,command');
|
|
62
|
-
const lines = result.stdout.split('\n');
|
|
63
|
-
for (const line of lines) {
|
|
64
|
-
// Skip tower-server entirely - it's a global service, not per-project
|
|
65
|
-
if (line.includes('tower-server')) {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
// Only match processes that belong to THIS project
|
|
69
|
-
// Must contain projectRoot to be considered for orphan cleanup
|
|
70
|
-
const isProjectProcess = line.includes(projectRoot) && line.includes('agent-farm');
|
|
71
|
-
if (!isProjectProcess) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
// Extract PID (first number in the line)
|
|
75
|
-
const match = line.trim().match(/^(\d+)/);
|
|
76
|
-
if (!match)
|
|
77
|
-
continue;
|
|
78
|
-
const pid = parseInt(match[1], 10);
|
|
79
|
-
// Skip if this PID is tracked in state
|
|
80
|
-
if (trackedPids.has(pid)) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
// This is an orphan
|
|
84
|
-
orphans.push(pid);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// ps command failed - ignore
|
|
89
|
-
}
|
|
90
|
-
return orphans;
|
|
91
|
-
}
|
|
92
22
|
/**
|
|
93
23
|
* Stop all agent farm processes
|
|
94
24
|
*
|
|
@@ -123,126 +53,67 @@ export async function stop() {
|
|
|
123
53
|
// Legacy cleanup for processes not managed by tower
|
|
124
54
|
const state = loadState();
|
|
125
55
|
let stopped = 0;
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
trackedPids.add(state.architect.pid);
|
|
130
|
-
for (const builder of state.builders)
|
|
131
|
-
trackedPids.add(builder.pid);
|
|
132
|
-
for (const util of state.utils)
|
|
133
|
-
trackedPids.add(util.pid);
|
|
134
|
-
for (const annotation of state.annotations)
|
|
135
|
-
trackedPids.add(annotation.pid);
|
|
136
|
-
// Stop architect — kill tmux session by name (safer than tree-kill)
|
|
137
|
-
if (state.architect) {
|
|
138
|
-
logger.info(`Stopping architect (PID: ${state.architect.pid})`);
|
|
56
|
+
// Stop architect — kill tmux session by name, then Tower terminal
|
|
57
|
+
if (state.architect?.tmuxSession) {
|
|
58
|
+
logger.info('Stopping architect...');
|
|
139
59
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
await killTmuxSession(state.architect.tmuxSession, state.architect.pid);
|
|
144
|
-
stopped++;
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
// Session may already be gone, try PID fallback
|
|
148
|
-
if (await isProcessRunning(state.architect.pid)) {
|
|
149
|
-
await killProcess(state.architect.pid);
|
|
150
|
-
stopped++;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
else if (await isProcessRunning(state.architect.pid)) {
|
|
155
|
-
await killProcess(state.architect.pid);
|
|
156
|
-
stopped++;
|
|
157
|
-
}
|
|
60
|
+
await killTmuxSession(state.architect.tmuxSession);
|
|
61
|
+
stopped++;
|
|
158
62
|
}
|
|
159
|
-
catch
|
|
160
|
-
|
|
63
|
+
catch {
|
|
64
|
+
// Session may already be gone
|
|
161
65
|
}
|
|
162
66
|
}
|
|
163
|
-
|
|
164
|
-
for (const builder of state.builders) {
|
|
165
|
-
logger.info(`Stopping builder ${builder.id} (PID: ${builder.pid})`);
|
|
67
|
+
if (towerRunning && state.architect?.terminalId) {
|
|
166
68
|
try {
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
await killTmuxSession(builder.tmuxSession, builder.pid);
|
|
170
|
-
stopped++;
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
if (await isProcessRunning(builder.pid)) {
|
|
174
|
-
await killProcess(builder.pid);
|
|
175
|
-
stopped++;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
else if (await isProcessRunning(builder.pid)) {
|
|
180
|
-
await killProcess(builder.pid);
|
|
181
|
-
stopped++;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
logger.warn(`Failed to stop builder ${builder.id}: ${error}`);
|
|
69
|
+
await client.killTerminal(state.architect.terminalId);
|
|
186
70
|
}
|
|
71
|
+
catch { /* best-effort */ }
|
|
187
72
|
}
|
|
188
|
-
// Stop all
|
|
189
|
-
for (const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
await killTmuxSession(util.tmuxSession, util.pid);
|
|
195
|
-
stopped++;
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
if (await isProcessRunning(util.pid)) {
|
|
199
|
-
await killProcess(util.pid);
|
|
200
|
-
stopped++;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
else if (await isProcessRunning(util.pid)) {
|
|
205
|
-
await killProcess(util.pid);
|
|
73
|
+
// Stop all builders — kill tmux sessions, then Tower terminals
|
|
74
|
+
for (const builder of state.builders) {
|
|
75
|
+
if (builder.tmuxSession) {
|
|
76
|
+
logger.info(`Stopping builder ${builder.id}...`);
|
|
77
|
+
try {
|
|
78
|
+
await killTmuxSession(builder.tmuxSession);
|
|
206
79
|
stopped++;
|
|
207
80
|
}
|
|
81
|
+
catch {
|
|
82
|
+
// Session may already be gone
|
|
83
|
+
}
|
|
208
84
|
}
|
|
209
|
-
|
|
210
|
-
|
|
85
|
+
if (towerRunning && builder.terminalId) {
|
|
86
|
+
try {
|
|
87
|
+
await client.killTerminal(builder.terminalId);
|
|
88
|
+
if (!builder.tmuxSession)
|
|
89
|
+
stopped++;
|
|
90
|
+
}
|
|
91
|
+
catch { /* best-effort */ }
|
|
211
92
|
}
|
|
212
93
|
}
|
|
213
|
-
// Stop all
|
|
214
|
-
for (const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
await
|
|
94
|
+
// Stop all utils — kill tmux sessions, then Tower terminals
|
|
95
|
+
for (const util of state.utils) {
|
|
96
|
+
if (util.tmuxSession) {
|
|
97
|
+
logger.info(`Stopping util ${util.id}...`);
|
|
98
|
+
try {
|
|
99
|
+
await killTmuxSession(util.tmuxSession);
|
|
219
100
|
stopped++;
|
|
220
101
|
}
|
|
102
|
+
catch {
|
|
103
|
+
// Session may already be gone
|
|
104
|
+
}
|
|
221
105
|
}
|
|
222
|
-
|
|
223
|
-
logger.warn(`Failed to stop annotation ${annotation.id}: ${error}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
// Clear state
|
|
227
|
-
clearState();
|
|
228
|
-
// Find and kill orphan processes (not in state but running for this project)
|
|
229
|
-
const orphans = await findOrphanProcesses(trackedPids);
|
|
230
|
-
if (orphans.length > 0) {
|
|
231
|
-
logger.blank();
|
|
232
|
-
logger.info(`Found ${orphans.length} orphan process(es)`);
|
|
233
|
-
for (const pid of orphans) {
|
|
106
|
+
if (towerRunning && util.terminalId) {
|
|
234
107
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await killProcessTree(pid);
|
|
108
|
+
await client.killTerminal(util.terminalId);
|
|
109
|
+
if (!util.tmuxSession)
|
|
238
110
|
stopped++;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
catch (error) {
|
|
242
|
-
logger.warn(` Failed to kill orphan ${pid}: ${error}`);
|
|
243
111
|
}
|
|
112
|
+
catch { /* best-effort */ }
|
|
244
113
|
}
|
|
245
114
|
}
|
|
115
|
+
// Clear state
|
|
116
|
+
clearState();
|
|
246
117
|
logger.blank();
|
|
247
118
|
if (stopped > 0) {
|
|
248
119
|
logger.success(`Stopped ${stopped} process(es)`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stop.js","sourceRoot":"","sources":["../../../src/agent-farm/commands/stop.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"stop.js","sourceRoot":"","sources":["../../../src/agent-farm/commands/stop.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Tower Registration Commands (Spec 0097 Phase 5)
|
|
3
|
+
*
|
|
4
|
+
* Implements `af tower register`, `af tower register --reauth`,
|
|
5
|
+
* `af tower deregister`, and cloud status display for `af tower status`.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Get tunnel status from the running tower daemon.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getTunnelStatus(port?: number): Promise<{
|
|
11
|
+
registered: boolean;
|
|
12
|
+
state: string;
|
|
13
|
+
uptime: number | null;
|
|
14
|
+
towerId: string | null;
|
|
15
|
+
towerName: string | null;
|
|
16
|
+
serverUrl: string | null;
|
|
17
|
+
accessUrl: string | null;
|
|
18
|
+
} | null>;
|
|
19
|
+
export interface TowerRegisterOptions {
|
|
20
|
+
reauth?: boolean;
|
|
21
|
+
port?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Register this tower with codevos.ai.
|
|
25
|
+
*
|
|
26
|
+
* Flow:
|
|
27
|
+
* 1. Check existing registration
|
|
28
|
+
* 2. Start ephemeral HTTP server for browser callback
|
|
29
|
+
* 3. Open browser to codevos.ai registration page
|
|
30
|
+
* 4. Wait for callback (2 min timeout), fallback to manual token paste
|
|
31
|
+
* 5. Prompt for tower name (skip on --reauth)
|
|
32
|
+
* 6. Exchange token for API key
|
|
33
|
+
* 7. Write cloud-config.json
|
|
34
|
+
* 8. Signal tower daemon if running
|
|
35
|
+
*/
|
|
36
|
+
export declare function towerRegister(options?: TowerRegisterOptions): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Deregister this tower from codevos.ai.
|
|
39
|
+
*/
|
|
40
|
+
export declare function towerDeregister(options?: {
|
|
41
|
+
port?: number;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Display cloud connection status.
|
|
45
|
+
*/
|
|
46
|
+
export declare function towerCloudStatus(port?: number): Promise<void>;
|
|
47
|
+
//# sourceMappingURL=tower-cloud.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tower-cloud.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/commands/tower-cloud.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgGH;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5D,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,GAAG,IAAI,CAAC,CAYR;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FrF;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CpF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgCnE"}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Tower Registration Commands (Spec 0097 Phase 5)
|
|
3
|
+
*
|
|
4
|
+
* Implements `af tower register`, `af tower register --reauth`,
|
|
5
|
+
* `af tower deregister`, and cloud status display for `af tower status`.
|
|
6
|
+
*/
|
|
7
|
+
import http from 'node:http';
|
|
8
|
+
import { hostname } from 'node:os';
|
|
9
|
+
import { createInterface } from 'node:readline';
|
|
10
|
+
import { logger, fatal } from '../utils/logger.js';
|
|
11
|
+
import { openBrowser } from '../utils/shell.js';
|
|
12
|
+
import { readCloudConfig, writeCloudConfig, deleteCloudConfig, maskApiKey, } from '../lib/cloud-config.js';
|
|
13
|
+
const CODEVOS_URL = process.env.CODEVOS_URL || 'https://codevos.ai';
|
|
14
|
+
const DEFAULT_TOWER_PORT = 4100;
|
|
15
|
+
const CALLBACK_TIMEOUT_MS = 120_000; // 2 minutes
|
|
16
|
+
/**
|
|
17
|
+
* Prompt the user for input via stdin.
|
|
18
|
+
*/
|
|
19
|
+
function prompt(question) {
|
|
20
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
rl.question(question, (answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(answer.trim());
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Prompt for yes/no confirmation. Returns true if user answers y/yes.
|
|
30
|
+
*/
|
|
31
|
+
async function confirm(question) {
|
|
32
|
+
const answer = await prompt(question);
|
|
33
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Generate a stable machine ID from hostname + platform.
|
|
37
|
+
*/
|
|
38
|
+
function getMachineId() {
|
|
39
|
+
const { platform, arch } = process;
|
|
40
|
+
return `${hostname()}-${platform}-${arch}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Exchange a registration token for API key and tower ID.
|
|
44
|
+
*/
|
|
45
|
+
async function redeemToken(serverUrl, token, towerName, machineId) {
|
|
46
|
+
const url = `${serverUrl}/api/towers/register/redeem`;
|
|
47
|
+
const body = JSON.stringify({ token, name: towerName, machineId });
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body,
|
|
52
|
+
signal: AbortSignal.timeout(30_000),
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const text = await response.text().catch(() => '');
|
|
56
|
+
throw new Error(`Registration failed (${response.status}): ${text || response.statusText}`);
|
|
57
|
+
}
|
|
58
|
+
const data = (await response.json());
|
|
59
|
+
if (!data.towerId || !data.apiKey) {
|
|
60
|
+
throw new Error('Invalid response from registration server: missing towerId or apiKey');
|
|
61
|
+
}
|
|
62
|
+
return { towerId: data.towerId, apiKey: data.apiKey };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Signal the running tower daemon to connect/disconnect the tunnel.
|
|
66
|
+
*/
|
|
67
|
+
async function signalTower(endpoint, port) {
|
|
68
|
+
const towerPort = port || DEFAULT_TOWER_PORT;
|
|
69
|
+
try {
|
|
70
|
+
await fetch(`http://127.0.0.1:${towerPort}/api/tunnel/${endpoint}`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
signal: AbortSignal.timeout(5_000),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Tower may not be running — that's fine
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get tunnel status from the running tower daemon.
|
|
81
|
+
*/
|
|
82
|
+
export async function getTunnelStatus(port) {
|
|
83
|
+
const towerPort = port || DEFAULT_TOWER_PORT;
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(`http://127.0.0.1:${towerPort}/api/tunnel/status`, { signal: AbortSignal.timeout(3_000) });
|
|
86
|
+
if (!response.ok)
|
|
87
|
+
return null;
|
|
88
|
+
return (await response.json());
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Register this tower with codevos.ai.
|
|
96
|
+
*
|
|
97
|
+
* Flow:
|
|
98
|
+
* 1. Check existing registration
|
|
99
|
+
* 2. Start ephemeral HTTP server for browser callback
|
|
100
|
+
* 3. Open browser to codevos.ai registration page
|
|
101
|
+
* 4. Wait for callback (2 min timeout), fallback to manual token paste
|
|
102
|
+
* 5. Prompt for tower name (skip on --reauth)
|
|
103
|
+
* 6. Exchange token for API key
|
|
104
|
+
* 7. Write cloud-config.json
|
|
105
|
+
* 8. Signal tower daemon if running
|
|
106
|
+
*/
|
|
107
|
+
export async function towerRegister(options = {}) {
|
|
108
|
+
const existing = readCloudConfig();
|
|
109
|
+
// Check existing registration
|
|
110
|
+
if (existing && !options.reauth) {
|
|
111
|
+
const proceed = await confirm(`This tower is already registered as '${existing.tower_name}'. Re-register? (y/N) `);
|
|
112
|
+
if (!proceed) {
|
|
113
|
+
logger.info('Registration cancelled.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
logger.header('Tower Registration');
|
|
118
|
+
// Start ephemeral callback server
|
|
119
|
+
const callbackServer = await startCallbackServer();
|
|
120
|
+
const callbackUrl = `http://localhost:${callbackServer.port}/callback`;
|
|
121
|
+
const callbackParam = `callback=${encodeURIComponent(callbackUrl)}`;
|
|
122
|
+
const browserUrl = options.reauth
|
|
123
|
+
? `${CODEVOS_URL}/towers/register?reauth=true&${callbackParam}`
|
|
124
|
+
: `${CODEVOS_URL}/towers/register?${callbackParam}`;
|
|
125
|
+
logger.info('Opening browser for authentication...');
|
|
126
|
+
logger.kv('URL', browserUrl);
|
|
127
|
+
try {
|
|
128
|
+
await openBrowser(browserUrl);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
logger.warn('Could not open browser automatically.');
|
|
132
|
+
logger.info(`Open this URL manually: ${browserUrl}`);
|
|
133
|
+
}
|
|
134
|
+
logger.info('Waiting for authentication (2 minute timeout)...');
|
|
135
|
+
// Wait for callback or timeout
|
|
136
|
+
let token = await callbackServer.waitForToken(CALLBACK_TIMEOUT_MS);
|
|
137
|
+
if (!token) {
|
|
138
|
+
// Fallback: manual token paste
|
|
139
|
+
logger.warn('Browser callback timed out.');
|
|
140
|
+
token = await prompt('Paste registration token from browser: ');
|
|
141
|
+
if (!token) {
|
|
142
|
+
fatal('No token provided. Registration cancelled.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Prompt for tower name (skip on reauth)
|
|
146
|
+
let towerName;
|
|
147
|
+
if (options.reauth && existing) {
|
|
148
|
+
towerName = existing.tower_name;
|
|
149
|
+
logger.kv('Tower name', towerName);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const defaultName = hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
153
|
+
towerName = await prompt(`Tower name (default: ${defaultName}): `);
|
|
154
|
+
if (!towerName)
|
|
155
|
+
towerName = defaultName;
|
|
156
|
+
}
|
|
157
|
+
// Exchange token for API key
|
|
158
|
+
// Explicit CODEVOS_URL env var takes priority over existing config (allows migration)
|
|
159
|
+
const serverUrl = process.env.CODEVOS_URL || existing?.server_url || 'https://codevos.ai';
|
|
160
|
+
logger.info('Exchanging token...');
|
|
161
|
+
let towerId;
|
|
162
|
+
let apiKey;
|
|
163
|
+
try {
|
|
164
|
+
({ towerId, apiKey } = await redeemToken(serverUrl, token, towerName, getMachineId()));
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
fatal(`Token exchange failed: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
// Write config
|
|
170
|
+
const config = {
|
|
171
|
+
tower_id: towerId,
|
|
172
|
+
tower_name: towerName,
|
|
173
|
+
api_key: apiKey,
|
|
174
|
+
server_url: serverUrl,
|
|
175
|
+
};
|
|
176
|
+
writeCloudConfig(config);
|
|
177
|
+
// Signal tower daemon if running
|
|
178
|
+
await signalTower('connect', options.port);
|
|
179
|
+
// Print success
|
|
180
|
+
const accessUrl = `${serverUrl}/t/${towerName}/`;
|
|
181
|
+
logger.blank();
|
|
182
|
+
logger.success(`Tower '${towerName}' registered successfully.`);
|
|
183
|
+
logger.kv('Tower ID', towerId);
|
|
184
|
+
logger.kv('API Key', maskApiKey(apiKey));
|
|
185
|
+
logger.kv('Access URL', accessUrl);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Deregister this tower from codevos.ai.
|
|
189
|
+
*/
|
|
190
|
+
export async function towerDeregister(options = {}) {
|
|
191
|
+
const config = readCloudConfig();
|
|
192
|
+
if (!config) {
|
|
193
|
+
fatal('Tower is not registered. Nothing to deregister.');
|
|
194
|
+
}
|
|
195
|
+
const proceed = await confirm(`Deregister tower '${config.tower_name}' from codevos.ai? (y/N) `);
|
|
196
|
+
if (!proceed) {
|
|
197
|
+
logger.info('Deregistration cancelled.');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Call server to deregister
|
|
201
|
+
try {
|
|
202
|
+
const response = await fetch(`${config.server_url}/api/towers/${config.tower_id}`, {
|
|
203
|
+
method: 'DELETE',
|
|
204
|
+
headers: {
|
|
205
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
206
|
+
},
|
|
207
|
+
signal: AbortSignal.timeout(30_000),
|
|
208
|
+
});
|
|
209
|
+
if (!response.ok && response.status !== 404) {
|
|
210
|
+
const text = await response.text().catch(() => '');
|
|
211
|
+
logger.warn(`Server deregistration returned ${response.status}: ${text || response.statusText}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
logger.warn(`Could not reach codevos.ai: ${err.message}. Removing local config anyway.`);
|
|
216
|
+
}
|
|
217
|
+
// Delete local config
|
|
218
|
+
deleteCloudConfig();
|
|
219
|
+
// Signal tower daemon to disconnect
|
|
220
|
+
await signalTower('disconnect', options.port);
|
|
221
|
+
logger.blank();
|
|
222
|
+
logger.success('Tower deregistered successfully.');
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Display cloud connection status.
|
|
226
|
+
*/
|
|
227
|
+
export async function towerCloudStatus(port) {
|
|
228
|
+
const config = readCloudConfig();
|
|
229
|
+
if (!config) {
|
|
230
|
+
logger.blank();
|
|
231
|
+
logger.info('Cloud Registration: not registered. Run \'af tower register\' to connect to codevos.ai.');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
logger.blank();
|
|
235
|
+
logger.header('Cloud Connection');
|
|
236
|
+
logger.kv('Registration', 'registered');
|
|
237
|
+
logger.kv('Tower Name', config.tower_name);
|
|
238
|
+
logger.kv('Tower ID', config.tower_id);
|
|
239
|
+
logger.kv('Server', config.server_url);
|
|
240
|
+
logger.kv('API Key', maskApiKey(config.api_key));
|
|
241
|
+
// Try to get live tunnel status from daemon
|
|
242
|
+
const status = await getTunnelStatus(port);
|
|
243
|
+
if (status) {
|
|
244
|
+
logger.kv('Connection', status.state);
|
|
245
|
+
if (status.uptime !== null) {
|
|
246
|
+
logger.kv('Uptime', formatUptime(status.uptime));
|
|
247
|
+
}
|
|
248
|
+
if (status.accessUrl) {
|
|
249
|
+
logger.kv('Access URL', status.accessUrl);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
logger.kv('Connection', 'unknown (tower not running)');
|
|
254
|
+
logger.kv('Access URL', `${config.server_url}/t/${config.tower_name}/`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Format uptime in milliseconds to a human-readable string.
|
|
259
|
+
*/
|
|
260
|
+
function formatUptime(ms) {
|
|
261
|
+
const seconds = Math.floor(ms / 1000);
|
|
262
|
+
if (seconds < 60)
|
|
263
|
+
return `${seconds}s`;
|
|
264
|
+
if (seconds < 3600)
|
|
265
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
266
|
+
const hours = Math.floor(seconds / 3600);
|
|
267
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
268
|
+
return `${hours}h ${mins}m`;
|
|
269
|
+
}
|
|
270
|
+
function startCallbackServer() {
|
|
271
|
+
return new Promise((resolve) => {
|
|
272
|
+
let tokenResolve = null;
|
|
273
|
+
const server = http.createServer((req, res) => {
|
|
274
|
+
const url = new URL(req.url || '/', `http://localhost`);
|
|
275
|
+
if (url.pathname === '/callback') {
|
|
276
|
+
const token = url.searchParams.get('token');
|
|
277
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
278
|
+
res.end('<html><body><h1>Registration received!</h1><p>You can close this tab.</p></body></html>');
|
|
279
|
+
if (token && tokenResolve) {
|
|
280
|
+
const r = tokenResolve;
|
|
281
|
+
tokenResolve = null;
|
|
282
|
+
r(token);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
res.writeHead(404);
|
|
287
|
+
res.end();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
server.listen(0, '127.0.0.1', () => {
|
|
291
|
+
const addr = server.address();
|
|
292
|
+
resolve({
|
|
293
|
+
port: addr.port,
|
|
294
|
+
waitForToken(timeoutMs) {
|
|
295
|
+
return new Promise((r) => {
|
|
296
|
+
tokenResolve = (token) => {
|
|
297
|
+
server.close();
|
|
298
|
+
r(token);
|
|
299
|
+
};
|
|
300
|
+
setTimeout(() => {
|
|
301
|
+
if (tokenResolve) {
|
|
302
|
+
tokenResolve = null;
|
|
303
|
+
server.close();
|
|
304
|
+
r(null);
|
|
305
|
+
}
|
|
306
|
+
}, timeoutMs);
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
close() {
|
|
310
|
+
server.close();
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
//# sourceMappingURL=tower-cloud.js.map
|