@agent-deck/cli 1.9.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend-runtime.d.ts +13 -0
- package/dist/backend-runtime.d.ts.map +1 -1
- package/dist/backend-runtime.js +20 -0
- package/dist/backend-runtime.js.map +1 -1
- package/dist/cli-integration-harness.d.ts +93 -0
- package/dist/cli-integration-harness.d.ts.map +1 -0
- package/dist/cli-integration-harness.js +309 -0
- package/dist/cli-integration-harness.js.map +1 -0
- package/dist/daemon-logs.d.ts +7 -0
- package/dist/daemon-logs.d.ts.map +1 -1
- package/dist/daemon-logs.js +55 -0
- package/dist/daemon-logs.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp-bridge.d.ts +182 -0
- package/dist/mcp-bridge.d.ts.map +1 -0
- package/dist/mcp-bridge.js +856 -0
- package/dist/mcp-bridge.js.map +1 -0
- package/dist/mcp-launcher.d.ts +9 -0
- package/dist/mcp-launcher.d.ts.map +1 -1
- package/dist/mcp-launcher.js +47 -0
- package/dist/mcp-launcher.js.map +1 -1
- package/dist/menubar.d.ts +13 -0
- package/dist/menubar.d.ts.map +1 -1
- package/dist/menubar.js +32 -1
- package/dist/menubar.js.map +1 -1
- package/dist/node-runtime.d.ts +14 -2
- package/dist/node-runtime.d.ts.map +1 -1
- package/dist/node-runtime.js +37 -18
- package/dist/node-runtime.js.map +1 -1
- package/dist/ports.d.ts +38 -0
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js +63 -0
- package/dist/ports.js.map +1 -1
- package/dist/setup.js +1 -1
- package/dist/setup.js.map +1 -1
- package/dist/shutdown-reason.d.ts +105 -0
- package/dist/shutdown-reason.d.ts.map +1 -0
- package/dist/shutdown-reason.js +247 -0
- package/dist/shutdown-reason.js.map +1 -0
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +361 -37
- package/dist/start.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +30 -0
- package/dist/status.js.map +1 -1
- package/dist/stop.d.ts +14 -1
- package/dist/stop.d.ts.map +1 -1
- package/dist/stop.js +72 -1
- package/dist/stop.js.map +1 -1
- package/dist/store.d.ts +9 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +41 -0
- package/dist/store.js.map +1 -1
- package/package.json +3 -3
package/dist/start.js
CHANGED
|
@@ -15,20 +15,40 @@ const version_1 = require("./version");
|
|
|
15
15
|
const defaults_1 = require("./defaults");
|
|
16
16
|
const managed_1 = require("./managed");
|
|
17
17
|
const daemon_logs_1 = require("./daemon-logs");
|
|
18
|
+
const shutdown_reason_1 = require("./shutdown-reason");
|
|
18
19
|
const dashboard_open_1 = require("./dashboard-open");
|
|
20
|
+
const backend_runtime_1 = require("./backend-runtime");
|
|
21
|
+
const store_1 = require("./store");
|
|
19
22
|
function formatStartVersionLine(version = (0, version_1.getAgentDeckVersion)()) {
|
|
20
23
|
return ` Version ${version}`;
|
|
21
24
|
}
|
|
22
25
|
function formatClaudeMcpAddCommand(host, mcpPort) {
|
|
23
26
|
return `claude mcp add --scope user agent-deck -e AGENT_DECK_MCP_PORT=${mcpPort} -e AGENT_DECK_HOST=${host} -- agent-deck mcp-launch`;
|
|
24
27
|
}
|
|
28
|
+
const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
25
29
|
const children = [];
|
|
26
30
|
let shuttingDown = false;
|
|
31
|
+
/** Only the process that wrote run.json may clear it — an aborted start must not. */
|
|
32
|
+
let ownsRunState = false;
|
|
33
|
+
/** Non-null until the deck is up: names the phase a stop interrupted. */
|
|
34
|
+
let startupPhase = null;
|
|
27
35
|
function isSupervisorMode(options) {
|
|
28
36
|
return options.supervisor === true || process.env.AGENT_DECK_SUPERVISOR === '1';
|
|
29
37
|
}
|
|
30
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Set once `runStart` knows what it is. The `--_supervisor` flag alone (no env
|
|
40
|
+
* var) still means stderr *is* supervisor.log, and printing there would write
|
|
41
|
+
* every line twice.
|
|
42
|
+
*/
|
|
43
|
+
let supervisorProcess = false;
|
|
44
|
+
function isSupervisorProcess() {
|
|
45
|
+
return supervisorProcess || isSupervisorMode({});
|
|
46
|
+
}
|
|
47
|
+
async function waitForHealth(url, attempts = 60, giveUp) {
|
|
31
48
|
for (let i = 0; i < attempts; i += 1) {
|
|
49
|
+
if (giveUp?.()) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
32
52
|
try {
|
|
33
53
|
const response = await fetch(url);
|
|
34
54
|
if (response.ok) {
|
|
@@ -49,11 +69,59 @@ function resolveServiceStdio(label, ioMode) {
|
|
|
49
69
|
const fd = (0, daemon_logs_1.openDaemonLogFd)(label);
|
|
50
70
|
return ['ignore', fd, fd];
|
|
51
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Copy the child's own last words into supervisor.log. The child already logs
|
|
74
|
+
* why it died, but the operator reading "backend exited (code 1)" is looking at
|
|
75
|
+
* a different file — so bring the reason to them.
|
|
76
|
+
*/
|
|
77
|
+
function surfaceChildLogTail(label, ioMode) {
|
|
78
|
+
if (ioMode !== 'file') {
|
|
79
|
+
// Inherit mode already printed the child's output to this terminal.
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const line of (0, daemon_logs_1.formatChildLogTail)(label, (0, daemon_logs_1.readDaemonLogTail)(label, 20))) {
|
|
83
|
+
(0, daemon_logs_1.appendDaemonLogLine)('supervisor', `${new Date().toISOString()} ${line}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* supervisor.log always, the terminal as well unless stderr already *is* that
|
|
88
|
+
* log. Used by the paths that run outside the ioMode-aware body of `runStart`.
|
|
89
|
+
*/
|
|
90
|
+
function reportSupervisor(lines) {
|
|
91
|
+
const stamp = new Date().toISOString();
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
try {
|
|
94
|
+
(0, daemon_logs_1.appendDaemonLogLine)('supervisor', `${stamp} ${line}`);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// A diagnostic never fails on its own bookkeeping.
|
|
98
|
+
}
|
|
99
|
+
if (!isSupervisorProcess()) {
|
|
100
|
+
console.error(line);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
52
104
|
function spawnNodeService(label, entry, env, ioMode) {
|
|
53
105
|
const child = (0, node_child_process_1.spawn)(process.execPath, [entry], {
|
|
54
106
|
env: { ...process.env, ...env },
|
|
55
107
|
stdio: resolveServiceStdio(label, ioMode),
|
|
56
108
|
});
|
|
109
|
+
// A spawn that never starts emits 'error', not 'exit' — and an unhandled
|
|
110
|
+
// 'error' event would take the supervisor down with Node's default trace and
|
|
111
|
+
// no record at all, which is the failure mode this ticket is about.
|
|
112
|
+
child.on('error', (error) => {
|
|
113
|
+
if (shuttingDown) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const { reason, detail } = (0, shutdown_reason_1.describeCrash)(error);
|
|
117
|
+
reportSupervisor([
|
|
118
|
+
`[agent-deck] ${label} failed to spawn: ${reason}`,
|
|
119
|
+
...detail.map((frame) => `[agent-deck] ${label} spawn| ${frame}`),
|
|
120
|
+
]);
|
|
121
|
+
if (label === 'backend') {
|
|
122
|
+
void shutdown(1, `backend failed to spawn: ${reason}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
57
125
|
child.on('exit', (code, signal) => {
|
|
58
126
|
if (shuttingDown) {
|
|
59
127
|
return;
|
|
@@ -66,8 +134,11 @@ function spawnNodeService(label, entry, env, ioMode) {
|
|
|
66
134
|
else {
|
|
67
135
|
console.error(message);
|
|
68
136
|
}
|
|
137
|
+
if (signal || (code ?? 1) !== 0) {
|
|
138
|
+
surfaceChildLogTail(label, ioMode);
|
|
139
|
+
}
|
|
69
140
|
if (label === 'backend') {
|
|
70
|
-
void shutdown(code ?? 1);
|
|
141
|
+
void shutdown(code ?? 1, `backend exited (${detail})`);
|
|
71
142
|
return;
|
|
72
143
|
}
|
|
73
144
|
const mcpWarn = '[agent-deck] MCP stopped; dashboard API remains available. Run `agent-deck stop && agent-deck start` to recover MCP.';
|
|
@@ -81,15 +152,45 @@ function spawnNodeService(label, entry, env, ioMode) {
|
|
|
81
152
|
children.push(child);
|
|
82
153
|
return child;
|
|
83
154
|
}
|
|
84
|
-
|
|
155
|
+
/**
|
|
156
|
+
* @param origin what this supervisor observed (a signal, a child exit, a failed
|
|
157
|
+
* start). Enriched with the requester's note when one was left behind, so the
|
|
158
|
+
* log names *who* stopped the deck and not just that it stopped.
|
|
159
|
+
*/
|
|
160
|
+
async function shutdown(exitCode = 0, origin = 'origin not recorded') {
|
|
85
161
|
if (shuttingDown) {
|
|
86
162
|
return;
|
|
87
163
|
}
|
|
88
164
|
shuttingDown = true;
|
|
89
|
-
|
|
90
|
-
|
|
165
|
+
const request = (0, shutdown_reason_1.consumeStopRequest)({ supervisorPid: process.pid });
|
|
166
|
+
const reason = (0, shutdown_reason_1.composeShutdownReason)(origin, request);
|
|
167
|
+
// Only a process that actually ran the deck may answer "why did it stop?".
|
|
168
|
+
// A `start` that found one already running, or died in preflight, would
|
|
169
|
+
// otherwise record a stop for a deck that is still up and serving.
|
|
170
|
+
const supervised = ownsRunState || children.length > 0;
|
|
171
|
+
if (supervised) {
|
|
172
|
+
// supervisor.log answers "why did the deck stop?" for every run, daemon or
|
|
173
|
+
// not — a Ctrl-C in inherit mode must not be the one stop with no record.
|
|
174
|
+
reportSupervisor([(0, shutdown_reason_1.formatSupervisorShutdownLine)(exitCode, reason)]);
|
|
175
|
+
(0, shutdown_reason_1.writeLastStop)({
|
|
176
|
+
at: new Date().toISOString(),
|
|
177
|
+
exitCode,
|
|
178
|
+
reason,
|
|
179
|
+
supervisorPid: process.pid,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else if (startupPhase !== null) {
|
|
183
|
+
// Nothing was ever supervised, so this is a start that did not finish —
|
|
184
|
+
// "why won't it start?", which keeps its own record.
|
|
185
|
+
(0, shutdown_reason_1.recordStartFailure)({ reason, exitCode });
|
|
186
|
+
if (!isSupervisorProcess()) {
|
|
187
|
+
console.error((0, shutdown_reason_1.formatStartFailureLine)(reason));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
reportSupervisor([(0, shutdown_reason_1.formatCommandExitLine)(exitCode, reason)]);
|
|
91
192
|
}
|
|
92
|
-
(
|
|
193
|
+
clearOwnRunState();
|
|
93
194
|
for (const child of children) {
|
|
94
195
|
if (!child.killed) {
|
|
95
196
|
child.kill('SIGTERM');
|
|
@@ -98,6 +199,117 @@ async function shutdown(exitCode = 0) {
|
|
|
98
199
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
99
200
|
process.exit(exitCode);
|
|
100
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* A start aborted before `writeRunState` never owned run.json, and deleting the
|
|
204
|
+
* running instance's state would leave `status` and `stop` blind. A record whose
|
|
205
|
+
* supervisor is gone is nobody's, though — that one is ours to clean up.
|
|
206
|
+
*/
|
|
207
|
+
function clearOwnRunState() {
|
|
208
|
+
if (ownsRunState) {
|
|
209
|
+
(0, runtime_state_1.clearRunState)();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const state = (0, runtime_state_1.readRunState)();
|
|
213
|
+
if (state && (state.cliPid === process.pid || !(0, runtime_state_1.isProcessAlive)(state.cliPid))) {
|
|
214
|
+
(0, runtime_state_1.clearRunState)();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** A signal that lands mid-startup names the phase it interrupted. */
|
|
218
|
+
function signalShutdownOrigin(signal) {
|
|
219
|
+
const origin = (0, shutdown_reason_1.describeSignalOrigin)(signal);
|
|
220
|
+
return startupPhase ? `${origin} while starting (phase: ${startupPhase})` : origin;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Every way this process can end, routed through one reporting path. Installed
|
|
224
|
+
* before any startup work — preflight, upgrade checks and port probes all await,
|
|
225
|
+
* and a stop arriving during them used to take Node's default exit path: no
|
|
226
|
+
* shutdown line, no last-stop record, no origin.
|
|
227
|
+
*/
|
|
228
|
+
function installSupervisorExitHandlers() {
|
|
229
|
+
for (const signal of SHUTDOWN_SIGNALS) {
|
|
230
|
+
process.on(signal, () => void shutdown(0, signalShutdownOrigin(signal)));
|
|
231
|
+
}
|
|
232
|
+
// The stop with the worst record of all: Node's default handler prints a
|
|
233
|
+
// trace to wherever stderr points and exits, leaving no shutdown line and no
|
|
234
|
+
// last-stop record. Route it through the same reporting as every other stop.
|
|
235
|
+
for (const [event, kind] of [
|
|
236
|
+
['uncaughtException', 'uncaught exception'],
|
|
237
|
+
['unhandledRejection', 'unhandled promise rejection'],
|
|
238
|
+
]) {
|
|
239
|
+
process.on(event, (error) => {
|
|
240
|
+
const { reason, detail } = (0, shutdown_reason_1.describeCrash)(error);
|
|
241
|
+
reportSupervisor(detail.map((frame) => `[agent-deck] supervisor stack| ${frame}`));
|
|
242
|
+
void shutdown(1, `supervisor ${kind}: ${reason}`);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* `start --daemon` launcher: it owns no run state and no children, so it records
|
|
248
|
+
* the interrupted start and leaves any supervisor it already spawned running.
|
|
249
|
+
* Once the deck is up (`startupPhase === null`) this process is only printing —
|
|
250
|
+
* interrupting it is not a failed start and must not be recorded as one.
|
|
251
|
+
*/
|
|
252
|
+
function installLauncherExitHandlers(getSupervisorPid) {
|
|
253
|
+
for (const signal of SHUTDOWN_SIGNALS) {
|
|
254
|
+
process.on(signal, () => {
|
|
255
|
+
if (startupPhase === null) {
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
const supervisorPid = getSupervisorPid();
|
|
259
|
+
(0, shutdown_reason_1.recordStartFailure)({
|
|
260
|
+
reason: `${(0, shutdown_reason_1.describeSignalOrigin)(signal)} while starting in background (phase: ${startupPhase})`,
|
|
261
|
+
detail: survivingSupervisorNote(supervisorPid),
|
|
262
|
+
});
|
|
263
|
+
process.exit(1);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// Same reasoning as the supervisor's crash handlers: the launcher is the only
|
|
267
|
+
// process watching a background start, so its own throw has to be recorded.
|
|
268
|
+
for (const [event, kind] of [
|
|
269
|
+
['uncaughtException', 'uncaught exception'],
|
|
270
|
+
['unhandledRejection', 'unhandled promise rejection'],
|
|
271
|
+
]) {
|
|
272
|
+
process.on(event, (error) => {
|
|
273
|
+
const { reason, detail } = (0, shutdown_reason_1.describeCrash)(error);
|
|
274
|
+
const frames = detail.map((frame) => `[agent-deck] launcher stack| ${frame}`);
|
|
275
|
+
if (startupPhase === null) {
|
|
276
|
+
// The deck is up and this process was only printing — not a failed
|
|
277
|
+
// start, so it must not be recorded as one.
|
|
278
|
+
reportSupervisor([
|
|
279
|
+
`[agent-deck] start --daemon ${kind} after the deck was up: ${reason}`,
|
|
280
|
+
...frames,
|
|
281
|
+
]);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
console.error(`[agent-deck] start --daemon ${kind}: ${reason}`);
|
|
285
|
+
(0, shutdown_reason_1.recordStartFailure)({
|
|
286
|
+
reason: `agent-deck start --daemon ${kind} (phase: ${startupPhase}): ${reason}`,
|
|
287
|
+
detail: [...frames, ...survivingSupervisorNote(getSupervisorPid())],
|
|
288
|
+
});
|
|
289
|
+
process.exit(1);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** A launcher that gives up has usually left a working supervisor behind. */
|
|
294
|
+
function survivingSupervisorNote(supervisorPid) {
|
|
295
|
+
return supervisorPid > 0
|
|
296
|
+
? [
|
|
297
|
+
`[agent-deck] background supervisor (pid ${supervisorPid}) was already spawned and keeps running — check agent-deck status`,
|
|
298
|
+
]
|
|
299
|
+
: [];
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Print (unless stderr already *is* supervisor.log) and persist, so the reason
|
|
303
|
+
* survives in supervisor.log and `agent-deck status`.
|
|
304
|
+
*/
|
|
305
|
+
function failStart(failure, supervisor) {
|
|
306
|
+
if (!supervisor) {
|
|
307
|
+
for (const line of failure.lines) {
|
|
308
|
+
console.error(line);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return (0, shutdown_reason_1.recordStartFailure)({ reason: failure.reason, detail: failure.lines });
|
|
312
|
+
}
|
|
101
313
|
async function printRunningEndpoints(host, backendPort, mcpPort, backendUrl) {
|
|
102
314
|
console.log('');
|
|
103
315
|
console.log('Agent Deck is running');
|
|
@@ -136,25 +348,66 @@ function buildSupervisorArgs(options) {
|
|
|
136
348
|
}
|
|
137
349
|
return args;
|
|
138
350
|
}
|
|
139
|
-
async function runDaemonLauncher(options) {
|
|
351
|
+
async function runDaemonLauncher(options, onSupervisorSpawned) {
|
|
140
352
|
const backendPort = options.backendPort ?? (0, defaults_1.readCliBackendPort)();
|
|
141
353
|
const mcpPort = options.mcpPort ?? (0, defaults_1.parseCliMcpPort)(process.env.AGENT_DECK_MCP_PORT);
|
|
142
354
|
const host = process.env.AGENT_DECK_HOST ?? '127.0.0.1';
|
|
143
355
|
const backendUrl = `http://${host}:${backendPort}`;
|
|
144
356
|
const supervisorLogFd = (0, daemon_logs_1.openDaemonLogFd)('supervisor');
|
|
145
357
|
const cliEntry = (0, daemon_logs_1.resolveCliEntry)();
|
|
358
|
+
const launchedAt = Date.now();
|
|
146
359
|
const child = (0, node_child_process_1.spawn)(process.execPath, [cliEntry, ...buildSupervisorArgs(options)], {
|
|
147
360
|
detached: true,
|
|
148
361
|
stdio: ['ignore', supervisorLogFd, supervisorLogFd],
|
|
149
362
|
env: { ...process.env, AGENT_DECK_SUPERVISOR: '1' },
|
|
150
363
|
});
|
|
364
|
+
onSupervisorSpawned(child.pid ?? 0);
|
|
365
|
+
// Object, not a `let`: the end arrives from a callback, and the reads below
|
|
366
|
+
// are all after an await. Holds a phrase, not a code, so a supervisor that
|
|
367
|
+
// never started reads as plainly as one that exited.
|
|
368
|
+
const supervisor = { ended: null };
|
|
369
|
+
child.on('exit', (code, signal) => {
|
|
370
|
+
supervisor.ended = signal ? `exited (signal ${signal})` : `exited (code ${code ?? 1})`;
|
|
371
|
+
});
|
|
372
|
+
// Without this handler a failed spawn throws an unhandled 'error' event and
|
|
373
|
+
// kills the launcher before it can write any diagnostic.
|
|
374
|
+
child.on('error', (error) => {
|
|
375
|
+
supervisor.ended = `failed to spawn (${(0, shutdown_reason_1.describeCrash)(error).reason})`;
|
|
376
|
+
});
|
|
151
377
|
child.unref();
|
|
152
|
-
|
|
378
|
+
// Waiting out the full health budget after the supervisor is already gone
|
|
379
|
+
// only delays the diagnostic it just wrote.
|
|
380
|
+
const healthy = await waitForHealth(`${backendUrl}/health`, 60, () => supervisor.ended !== null);
|
|
153
381
|
if (!healthy) {
|
|
154
|
-
|
|
382
|
+
const reason = supervisor.ended
|
|
383
|
+
? `daemon supervisor ${supervisor.ended} before the API became healthy`
|
|
384
|
+
: 'daemon supervisor never passed its API health check';
|
|
385
|
+
console.error(`[agent-deck] ${reason}`);
|
|
386
|
+
// The supervisor log already holds the reason (and the child log tail it
|
|
387
|
+
// copied in) — show it here rather than sending the operator hunting.
|
|
388
|
+
for (const line of (0, daemon_logs_1.formatChildLogTail)('supervisor', (0, daemon_logs_1.readDaemonLogTail)('supervisor', 20))) {
|
|
389
|
+
console.error(line);
|
|
390
|
+
}
|
|
155
391
|
console.error(`[agent-deck] See ${(0, daemon_logs_1.resolveDaemonLogPath)('supervisor')}`);
|
|
156
|
-
|
|
157
|
-
|
|
392
|
+
// A supervisor that is merely slow is still running and still holding the
|
|
393
|
+
// ports; saying only "start failed" sends the operator into a port conflict
|
|
394
|
+
// on their next attempt.
|
|
395
|
+
const stillRunning = supervisor.ended === null ? survivingSupervisorNote(child.pid ?? 0) : [];
|
|
396
|
+
for (const line of stillRunning) {
|
|
397
|
+
console.error(line);
|
|
398
|
+
}
|
|
399
|
+
// The supervisor child knows more than "it exited"; keep its record if it
|
|
400
|
+
// got far enough to write one for this launch.
|
|
401
|
+
const recorded = (0, shutdown_reason_1.readLastStartFailure)();
|
|
402
|
+
const supervisorRecorded = recorded !== null && Date.parse(recorded.at) >= launchedAt;
|
|
403
|
+
return (0, shutdown_reason_1.recordStartFailure)({
|
|
404
|
+
reason,
|
|
405
|
+
detail: stillRunning,
|
|
406
|
+
keepExistingRecord: supervisorRecorded,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
// The deck is up; everything past here is reporting, not starting.
|
|
410
|
+
startupPhase = null;
|
|
158
411
|
const mcpHealthy = await waitForHealth(`http://${host}:${mcpPort}/health`, 20);
|
|
159
412
|
if (!mcpHealthy) {
|
|
160
413
|
console.warn('[agent-deck] MCP not healthy yet — dashboard API is up.');
|
|
@@ -176,34 +429,51 @@ async function runDaemonLauncher(options) {
|
|
|
176
429
|
}
|
|
177
430
|
return 0;
|
|
178
431
|
}
|
|
432
|
+
function portConflictFailure(port, label, host) {
|
|
433
|
+
return {
|
|
434
|
+
reason: `port ${port} (${label}) is held by another program on ${host}`,
|
|
435
|
+
lines: (0, ports_1.formatPortConflict)(port, label, host, false)
|
|
436
|
+
.split('\n')
|
|
437
|
+
.map((line) => `[agent-deck] ${line}`),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
179
440
|
async function ensurePortsAvailable(host, backendPort, mcpPort, probe) {
|
|
180
441
|
const [backendBusy, mcpBusy] = await Promise.all([
|
|
181
442
|
(0, ports_1.isTcpPortOpen)(host, backendPort),
|
|
182
443
|
(0, ports_1.isTcpPortOpen)(host, mcpPort),
|
|
183
444
|
]);
|
|
184
445
|
if (backendBusy && !probe.backendUp) {
|
|
185
|
-
|
|
186
|
-
return 1;
|
|
446
|
+
return portConflictFailure(backendPort, 'API/dashboard', host);
|
|
187
447
|
}
|
|
188
448
|
if (mcpBusy && !probe.mcpUp) {
|
|
189
|
-
|
|
190
|
-
return 1;
|
|
449
|
+
return portConflictFailure(mcpPort, 'MCP', host);
|
|
191
450
|
}
|
|
192
451
|
return null;
|
|
193
452
|
}
|
|
194
453
|
async function runStart(options = {}) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
454
|
+
const supervisor = isSupervisorMode(options);
|
|
455
|
+
const launcher = options.daemon === true && !supervisor;
|
|
456
|
+
supervisorProcess = supervisor;
|
|
457
|
+
// Before the first await: preflight, upgrade checks and port probes all take
|
|
458
|
+
// time, and a stop landing in that window used to kill this process through
|
|
459
|
+
// Node's default signal path — no shutdown line, no origin, no record.
|
|
460
|
+
startupPhase = 'preflight';
|
|
461
|
+
let daemonSupervisorPid = 0;
|
|
462
|
+
if (launcher) {
|
|
463
|
+
installLauncherExitHandlers(() => daemonSupervisorPid);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
installSupervisorExitHandlers();
|
|
198
467
|
}
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
201
|
-
return
|
|
468
|
+
const preflight = (0, node_runtime_1.checkStartPreflight)();
|
|
469
|
+
if (preflight !== null) {
|
|
470
|
+
return failStart({ reason: preflight.reason, lines: preflight.message.split('\n') }, supervisor);
|
|
202
471
|
}
|
|
472
|
+
startupPhase = 'update check';
|
|
203
473
|
await (0, upgrade_1.maybeAutoUpgradeOnStart)();
|
|
204
474
|
await (0, upgrade_1.notifyIfUpdateAvailable)();
|
|
205
|
-
|
|
206
|
-
|
|
475
|
+
if (launcher) {
|
|
476
|
+
startupPhase = 'probing for a running instance';
|
|
207
477
|
const backendPort = options.backendPort ?? (0, defaults_1.readCliBackendPort)();
|
|
208
478
|
const mcpPort = options.mcpPort ?? (0, defaults_1.parseCliMcpPort)(process.env.AGENT_DECK_MCP_PORT);
|
|
209
479
|
const host = process.env.AGENT_DECK_HOST ?? '127.0.0.1';
|
|
@@ -211,22 +481,29 @@ async function runStart(options = {}) {
|
|
|
211
481
|
if (probe.backendUp && probe.mcpUp) {
|
|
212
482
|
if (options.force) {
|
|
213
483
|
console.log('[agent-deck] Restarting existing instance (--force) ...');
|
|
214
|
-
await (0, stop_1.runStop)();
|
|
484
|
+
await (0, stop_1.runStop)({ source: 'agent-deck start --force', detail: 'restarting existing instance' });
|
|
215
485
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
216
486
|
}
|
|
217
487
|
else {
|
|
488
|
+
// Nothing is starting any more — an interrupt from here on is a stopped
|
|
489
|
+
// printout, not a failed start, and must not be recorded as one.
|
|
490
|
+
startupPhase = null;
|
|
218
491
|
await printRunningEndpoints(host, backendPort, mcpPort, `http://${host}:${backendPort}`);
|
|
219
492
|
console.log('Already running. Use `agent-deck stop` or `agent-deck start --daemon --force` to restart.');
|
|
220
493
|
await maybeOpenDashboard(`http://${host}:${backendPort}`, options.openBrowser);
|
|
221
494
|
return 0;
|
|
222
495
|
}
|
|
223
496
|
}
|
|
497
|
+
startupPhase = 'checking ports';
|
|
224
498
|
const refreshedProbe = options.force ? await (0, ports_1.probeAgentDeck)(host, backendPort, mcpPort) : probe;
|
|
225
499
|
const portError = await ensurePortsAvailable(host, backendPort, mcpPort, refreshedProbe);
|
|
226
500
|
if (portError !== null) {
|
|
227
|
-
return portError;
|
|
501
|
+
return failStart(portError, supervisor);
|
|
228
502
|
}
|
|
229
|
-
|
|
503
|
+
startupPhase = 'launching background supervisor';
|
|
504
|
+
return runDaemonLauncher(options, (pid) => {
|
|
505
|
+
daemonSupervisorPid = pid;
|
|
506
|
+
});
|
|
230
507
|
}
|
|
231
508
|
const ioMode = supervisor ? 'file' : 'inherit';
|
|
232
509
|
const backendPort = options.backendPort ?? (0, defaults_1.readCliBackendPort)();
|
|
@@ -234,24 +511,29 @@ async function runStart(options = {}) {
|
|
|
234
511
|
const host = process.env.AGENT_DECK_HOST ?? '127.0.0.1';
|
|
235
512
|
const backendUrl = `http://${host}:${backendPort}`;
|
|
236
513
|
const uiDist = options.skipUi ? undefined : (0, paths_1.resolveUiDist)();
|
|
514
|
+
startupPhase = 'probing for a running instance';
|
|
237
515
|
const probe = await (0, ports_1.probeAgentDeck)(host, backendPort, mcpPort);
|
|
238
516
|
if (probe.backendUp && probe.mcpUp) {
|
|
239
517
|
if (options.force) {
|
|
240
518
|
console.log('[agent-deck] Restarting existing instance (--force) ...');
|
|
241
|
-
await (0, stop_1.runStop)();
|
|
519
|
+
await (0, stop_1.runStop)({ source: 'agent-deck start --force', detail: 'restarting existing instance' });
|
|
242
520
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
243
521
|
}
|
|
244
522
|
else {
|
|
523
|
+
// As above: past this point this process is only reporting on a deck it
|
|
524
|
+
// did not start, so it owns neither a stop nor a failed start.
|
|
525
|
+
startupPhase = null;
|
|
245
526
|
await printRunningEndpoints(host, backendPort, mcpPort, backendUrl);
|
|
246
527
|
console.log('Already running. Use `agent-deck stop` or `agent-deck start --force` to restart.');
|
|
247
528
|
await maybeOpenDashboard(backendUrl, options.openBrowser);
|
|
248
529
|
return 0;
|
|
249
530
|
}
|
|
250
531
|
}
|
|
532
|
+
startupPhase = 'checking ports';
|
|
251
533
|
const refreshedProbe = options.force ? await (0, ports_1.probeAgentDeck)(host, backendPort, mcpPort) : probe;
|
|
252
534
|
const portError = await ensurePortsAvailable(host, backendPort, mcpPort, refreshedProbe);
|
|
253
535
|
if (portError !== null) {
|
|
254
|
-
return portError;
|
|
536
|
+
return failStart(portError, supervisor);
|
|
255
537
|
}
|
|
256
538
|
if (!options.skipUi && !uiDist) {
|
|
257
539
|
const warnUi = '[agent-deck] Dashboard UI bundle not found (static-ui). API and MCP will still start.';
|
|
@@ -265,8 +547,17 @@ async function runStart(options = {}) {
|
|
|
265
547
|
console.warn(warnDist);
|
|
266
548
|
}
|
|
267
549
|
}
|
|
268
|
-
|
|
269
|
-
|
|
550
|
+
startupPhase = 'resolving the backend build';
|
|
551
|
+
let backendEntry;
|
|
552
|
+
let mcpEntry;
|
|
553
|
+
try {
|
|
554
|
+
backendEntry = (0, paths_1.resolveBackendEntry)('index');
|
|
555
|
+
mcpEntry = (0, paths_1.resolveBackendEntry)('mcp-index');
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
559
|
+
return failStart({ reason: message, lines: [`[agent-deck] ${message}`] }, supervisor);
|
|
560
|
+
}
|
|
270
561
|
const logStart = (line) => {
|
|
271
562
|
if (ioMode === 'file') {
|
|
272
563
|
(0, daemon_logs_1.appendDaemonLogLine)('supervisor', `${new Date().toISOString()} ${line}`);
|
|
@@ -275,6 +566,7 @@ async function runStart(options = {}) {
|
|
|
275
566
|
console.log(line);
|
|
276
567
|
}
|
|
277
568
|
};
|
|
569
|
+
startupPhase = 'starting backend';
|
|
278
570
|
logStart('[agent-deck] Starting backend ...');
|
|
279
571
|
const backendChild = spawnNodeService('backend', backendEntry, {
|
|
280
572
|
PORT: String(backendPort),
|
|
@@ -284,19 +576,33 @@ async function runStart(options = {}) {
|
|
|
284
576
|
AGENT_DECK_MCP_PORT: String(mcpPort),
|
|
285
577
|
...(uiDist ? { AGENT_DECK_UI_DIST: uiDist } : {}),
|
|
286
578
|
}, ioMode);
|
|
287
|
-
|
|
579
|
+
// A backend that exits takes the shutdown path immediately; polling out the
|
|
580
|
+
// remaining health budget would only postpone this process's own exit.
|
|
581
|
+
const healthy = await waitForHealth(`${backendUrl}/health`, 60, () => shuttingDown);
|
|
288
582
|
if (!healthy) {
|
|
583
|
+
if (shuttingDown) {
|
|
584
|
+
// Something else (the backend's own exit, or a stop) already logged the
|
|
585
|
+
// cause and is shutting down with its own exit code. Returning here would
|
|
586
|
+
// race that exit and could report a failure for a requested stop; repeating
|
|
587
|
+
// the tail would only double it in supervisor.log.
|
|
588
|
+
await new Promise(() => {
|
|
589
|
+
// shutdown() exits this process.
|
|
590
|
+
});
|
|
591
|
+
}
|
|
289
592
|
const failMsg = '[agent-deck] Backend failed health check (port conflict or crash).';
|
|
290
593
|
if (ioMode === 'file') {
|
|
291
|
-
(0, daemon_logs_1.appendDaemonLogLine)('supervisor', failMsg);
|
|
594
|
+
(0, daemon_logs_1.appendDaemonLogLine)('supervisor', `${new Date().toISOString()} ${failMsg}`);
|
|
292
595
|
}
|
|
293
596
|
else {
|
|
294
597
|
console.error(failMsg);
|
|
598
|
+
console.error(`[agent-deck] See ${(0, daemon_logs_1.resolveDaemonLogPath)('backend')}`);
|
|
295
599
|
console.error('[agent-deck] Run: agent-deck status');
|
|
296
600
|
}
|
|
297
|
-
|
|
601
|
+
surfaceChildLogTail('backend', ioMode);
|
|
602
|
+
await shutdown(1, 'backend failed health check during start');
|
|
298
603
|
return 1;
|
|
299
604
|
}
|
|
605
|
+
startupPhase = 'starting MCP server';
|
|
300
606
|
logStart('[agent-deck] Starting MCP server ...');
|
|
301
607
|
let mcpPid = 0;
|
|
302
608
|
if (refreshedProbe.mcpUp) {
|
|
@@ -315,12 +621,14 @@ async function runStart(options = {}) {
|
|
|
315
621
|
if (!mcpProbe.mcpUp) {
|
|
316
622
|
const mcpFail = '[agent-deck] MCP server failed to start (port conflict or crash). Dashboard API is still running.';
|
|
317
623
|
if (ioMode === 'file') {
|
|
318
|
-
(0, daemon_logs_1.appendDaemonLogLine)('supervisor', mcpFail);
|
|
624
|
+
(0, daemon_logs_1.appendDaemonLogLine)('supervisor', `${new Date().toISOString()} ${mcpFail}`);
|
|
319
625
|
}
|
|
320
626
|
else {
|
|
321
627
|
console.error(mcpFail);
|
|
628
|
+
console.error(`[agent-deck] See ${(0, daemon_logs_1.resolveDaemonLogPath)('mcp')}`);
|
|
322
629
|
console.error('[agent-deck] Run: agent-deck stop && agent-deck start');
|
|
323
630
|
}
|
|
631
|
+
surfaceChildLogTail('mcp', ioMode);
|
|
324
632
|
}
|
|
325
633
|
else {
|
|
326
634
|
mcpPid = mcpChild.pid ?? 0;
|
|
@@ -335,6 +643,9 @@ async function runStart(options = {}) {
|
|
|
335
643
|
cliPid: process.pid,
|
|
336
644
|
startedAt: new Date().toISOString(),
|
|
337
645
|
});
|
|
646
|
+
ownsRunState = true;
|
|
647
|
+
startupPhase = null;
|
|
648
|
+
(0, shutdown_reason_1.clearLastStartFailure)();
|
|
338
649
|
const dashboardLine = uiDist
|
|
339
650
|
? (0, dashboard_open_1.formatDashboardStatusLine)()
|
|
340
651
|
: 'Dashboard (UI bundle missing — use npm run dev:all for dev UI)';
|
|
@@ -369,8 +680,6 @@ async function runStart(options = {}) {
|
|
|
369
680
|
console.warn(`[agent-deck] ${result.message ?? 'Failed to open dashboard'}`);
|
|
370
681
|
}
|
|
371
682
|
}
|
|
372
|
-
process.on('SIGINT', () => void shutdown(0));
|
|
373
|
-
process.on('SIGTERM', () => void shutdown(0));
|
|
374
683
|
await new Promise(() => {
|
|
375
684
|
// keep alive until signal
|
|
376
685
|
});
|
|
@@ -388,7 +697,7 @@ async function runDoctor() {
|
|
|
388
697
|
console.error('FAIL: Node.js 20+ required (24 recommended — current OS default)');
|
|
389
698
|
ok = false;
|
|
390
699
|
}
|
|
391
|
-
else if (!
|
|
700
|
+
else if (!(0, node_runtime_1.isSupportedNodeMajor)(nodeMajor)) {
|
|
392
701
|
console.error('FAIL: Unsupported Node.js major for better-sqlite3 prebuilds');
|
|
393
702
|
console.error(' Use Node 20+; Node 24 is the default target');
|
|
394
703
|
ok = false;
|
|
@@ -447,6 +756,21 @@ async function runDoctor() {
|
|
|
447
756
|
else {
|
|
448
757
|
console.warn('WARN: Agent Deck is not running (agent-deck start)');
|
|
449
758
|
}
|
|
759
|
+
// A store→sqlite import that failed leaves a healthy-looking backend serving a
|
|
760
|
+
// stale snapshot, so doctor has to fail on it (NOT-123).
|
|
761
|
+
const lastReindex = (0, backend_runtime_1.readLastReindex)();
|
|
762
|
+
const [reindexHeadline, ...reindexDetail] = (0, store_1.formatLastReindex)(lastReindex);
|
|
763
|
+
if (reindexHeadline) {
|
|
764
|
+
if (lastReindex?.ok) {
|
|
765
|
+
console.log(`OK: ${reindexHeadline}`);
|
|
766
|
+
reindexDetail.forEach((line) => console.log(` ${line}`));
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
console.error(`FAIL: ${reindexHeadline}`);
|
|
770
|
+
reindexDetail.forEach((line) => console.error(` ${line}`));
|
|
771
|
+
ok = false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
450
774
|
return ok ? 0 : 1;
|
|
451
775
|
}
|
|
452
776
|
//# sourceMappingURL=start.js.map
|