agent-dag 1.33.1 → 1.33.2
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/bin/agent-dag.js +112 -5
- package/bin/deck.js +22 -7
- package/dist/web/assets/{index-BKz-oUzX.js → index-D4UiP6Xm.js} +16 -16
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/server/index.mjs +31 -5
- package/src/server/self-update.mjs +77 -4
package/bin/agent-dag.js
CHANGED
|
@@ -23,13 +23,22 @@
|
|
|
23
23
|
// Everything else the deck does still lives in bin/deck.js. This file must stay
|
|
24
24
|
// boring: it is the one process that is never replaced.
|
|
25
25
|
import { spawn } from "node:child_process";
|
|
26
|
+
import { connect } from "node:net";
|
|
26
27
|
import { dirname, join } from "node:path";
|
|
27
28
|
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { installedVersion, npxRestartSpec } from "../src/server/self-update.mjs";
|
|
28
30
|
|
|
29
|
-
// Chosen because
|
|
31
|
+
// Chosen because they mean nothing else here: the worker exits 0 normally and
|
|
30
32
|
// non-zero on failure, both of which must pass straight through.
|
|
31
|
-
const RESTART_CODE = 75;
|
|
32
|
-
const
|
|
33
|
+
const RESTART_CODE = 75; // come back running the files on disk
|
|
34
|
+
const UPGRADE_CODE = 76; // come back through npx, which fetches newer files
|
|
35
|
+
const BIN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const WORKER = join(BIN_DIR, "deck.js");
|
|
37
|
+
const PKG_ROOT = dirname(BIN_DIR);
|
|
38
|
+
|
|
39
|
+
// npx is a .cmd shim on Windows, which spawn can only launch through a shell.
|
|
40
|
+
const NPX = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
41
|
+
const VERSION = installedVersion(PKG_ROOT) ?? "?";
|
|
33
42
|
|
|
34
43
|
// The port the worker actually bound, which is not necessarily the one it was
|
|
35
44
|
// asked for — the first launch falls back to a random port when 4317 is taken.
|
|
@@ -38,6 +47,10 @@ const WORKER = join(dirname(fileURLToPath(import.meta.url)), "deck.js");
|
|
|
38
47
|
let boundPort = null;
|
|
39
48
|
let restarts = 0;
|
|
40
49
|
let child = null;
|
|
50
|
+
// Set by the signal handlers. Without it, Ctrl+C during an npx fetch looks
|
|
51
|
+
// exactly like a failed fetch, and the fallback below would resurrect the deck
|
|
52
|
+
// the user just stopped.
|
|
53
|
+
let stopping = false;
|
|
41
54
|
|
|
42
55
|
function launch(respawn) {
|
|
43
56
|
const args = [WORKER, ...process.argv.slice(2)];
|
|
@@ -71,6 +84,11 @@ function launch(respawn) {
|
|
|
71
84
|
launch(true);
|
|
72
85
|
return;
|
|
73
86
|
}
|
|
87
|
+
if (code === UPGRADE_CODE) {
|
|
88
|
+
restarts++;
|
|
89
|
+
launchNpx();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
74
92
|
// Anything else is the worker's own verdict and belongs to whoever started
|
|
75
93
|
// us — including the ccdeck wrapper, which exits with our code in turn.
|
|
76
94
|
if (signal) process.kill(process.pid, signal);
|
|
@@ -83,6 +101,85 @@ function launch(respawn) {
|
|
|
83
101
|
});
|
|
84
102
|
}
|
|
85
103
|
|
|
104
|
+
/** Drop the two flags launchNpx sets itself. `--port` takes a value, and both
|
|
105
|
+
* spellings npm's parser accepts (`--port 4317`, `--port=4317`) have to go. */
|
|
106
|
+
function withoutPortAndOpen(args) {
|
|
107
|
+
const out = [];
|
|
108
|
+
for (let i = 0; i < args.length; i++) {
|
|
109
|
+
const a = args[i];
|
|
110
|
+
if (a === "--no-open") continue;
|
|
111
|
+
if (a === "--port") { i++; continue; } // and its value
|
|
112
|
+
if (a.startsWith("--port=")) continue;
|
|
113
|
+
out.push(a);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Come back on a newer version, for a deck that npx started.
|
|
120
|
+
*
|
|
121
|
+
* There is nothing to install here: npx unpacks each spec into its own
|
|
122
|
+
* content-addressed directory under _npx, so `npm i -g` would upgrade something
|
|
123
|
+
* this process could never reach. `npx -y <spec>@latest` resolves fresh, gets a
|
|
124
|
+
* DIFFERENT directory, and starts the deck there — on the port we hand it, so
|
|
125
|
+
* the tab that asked for the update reconnects to the same URL.
|
|
126
|
+
*
|
|
127
|
+
* This process stays as the parent rather than exec-ing, for the same reasons
|
|
128
|
+
* the file's header gives, plus one more: a fetch can fail (offline, registry
|
|
129
|
+
* down), and someone has to bring the working copy back when it does.
|
|
130
|
+
*/
|
|
131
|
+
function launchNpx() {
|
|
132
|
+
const spec = npxRestartSpec(PKG_ROOT);
|
|
133
|
+
if (!spec) { launch(true); return; } // not an npx run after all
|
|
134
|
+
// Our two are appended, so the originals are dropped rather than left to be
|
|
135
|
+
// overridden — `--port 4317 --no-open --port 4317 --no-open` works, but it is
|
|
136
|
+
// what the next person reads in `ps`.
|
|
137
|
+
const args = ["-y", spec, ...withoutPortAndOpen(process.argv.slice(2))];
|
|
138
|
+
if (boundPort != null) args.push("--port", String(boundPort));
|
|
139
|
+
// The tab that asked for this is open and reconnecting; a second one would be
|
|
140
|
+
// the deck talking over itself.
|
|
141
|
+
args.push("--no-open");
|
|
142
|
+
|
|
143
|
+
process.stdout.write(`\n ↻ fetching ${spec}…\n`);
|
|
144
|
+
const started = spawn(NPX, args, { stdio: "inherit", shell: process.platform === "win32" });
|
|
145
|
+
child = started;
|
|
146
|
+
|
|
147
|
+
// Whether the replacement ever got as far as serving. An npx that cannot
|
|
148
|
+
// resolve exits in seconds having bound nothing; a deck the user stops with
|
|
149
|
+
// Ctrl+C exits non-zero too, and only this tells the two apart.
|
|
150
|
+
let served = false;
|
|
151
|
+
const probe = setInterval(() => {
|
|
152
|
+
if (boundPort == null || served) return;
|
|
153
|
+
const sock = connect({ port: boundPort, host: "127.0.0.1" });
|
|
154
|
+
sock.setTimeout(1000);
|
|
155
|
+
sock.on("connect", () => { served = true; sock.destroy(); });
|
|
156
|
+
sock.on("timeout", () => sock.destroy());
|
|
157
|
+
sock.on("error", () => { /* not up yet */ });
|
|
158
|
+
}, 1000);
|
|
159
|
+
probe.unref?.();
|
|
160
|
+
|
|
161
|
+
const giveUp = (why) => {
|
|
162
|
+
clearInterval(probe);
|
|
163
|
+
child = null;
|
|
164
|
+
if (stopping || served) return; // the user stopped it, or it ran and ended
|
|
165
|
+
console.error(`agents-deck: ${why} — staying on v${VERSION}`);
|
|
166
|
+
restarts++;
|
|
167
|
+
launch(true);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
started.on("exit", (code, signal) => {
|
|
171
|
+
clearInterval(probe);
|
|
172
|
+
child = null;
|
|
173
|
+
if (stopping || served) {
|
|
174
|
+
if (signal) process.kill(process.pid, signal);
|
|
175
|
+
else process.exit(code ?? 0);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
giveUp(`${NPX} ${spec} exited ${code ?? signal}`);
|
|
179
|
+
});
|
|
180
|
+
started.on("error", (err) => giveUp(`could not run ${NPX}: ${err.message}`));
|
|
181
|
+
}
|
|
182
|
+
|
|
86
183
|
// Ctrl+C already reaches the child directly — it shares this process group — so
|
|
87
184
|
// forwarding would deliver it twice. These handlers exist only to keep the
|
|
88
185
|
// supervisor alive long enough for the child's own graceful shutdown to run and
|
|
@@ -92,8 +189,18 @@ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
|
92
189
|
// On Windows none of these are delivered to a Node process, which is fine:
|
|
93
190
|
// there the console kills the whole tree and sweepStaleDiscovery cleans up
|
|
94
191
|
// on the next boot.
|
|
95
|
-
|
|
96
|
-
|
|
192
|
+
const second = stopping; // an impatient user pressing Ctrl+C again
|
|
193
|
+
stopping = true;
|
|
194
|
+
if (!child) { process.exit(0); return; }
|
|
195
|
+
try { child.kill(second ? "SIGKILL" : sig); } catch { /* already gone */ }
|
|
196
|
+
// The npx step is a shell that exec's the new deck, and a signal can land in
|
|
197
|
+
// the gap: the shell dies, the process replacing it never saw it. One more
|
|
198
|
+
// attempt a moment later catches that; it is a no-op when the first worked.
|
|
199
|
+
if (!second) {
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
if (stopping && child) { try { child.kill("SIGTERM"); } catch { /* gone */ } }
|
|
202
|
+
}, 2500).unref();
|
|
203
|
+
}
|
|
97
204
|
});
|
|
98
205
|
}
|
|
99
206
|
|
package/bin/deck.js
CHANGED
|
@@ -19,8 +19,12 @@ const PKG_VERSION = (() => {
|
|
|
19
19
|
const argv = process.argv.slice(2);
|
|
20
20
|
const flags = parseArgs(argv);
|
|
21
21
|
|
|
22
|
-
// Exit
|
|
22
|
+
// Exit codes the supervisor reads as "bring me back": 75 from the files on
|
|
23
|
+
// disk, 76 through npx — which is the only way an npx run reaches a newer
|
|
24
|
+
// version, since its directory is never upgraded in place. Anything else it
|
|
25
|
+
// forwards.
|
|
23
26
|
const RESTART_CODE = 75;
|
|
27
|
+
const UPGRADE_CODE = 76;
|
|
24
28
|
const RESPAWN = process.env.AGENTS_DECK_RESPAWN === "1";
|
|
25
29
|
const SUPERVISED = typeof process.send === "function";
|
|
26
30
|
|
|
@@ -241,14 +245,19 @@ if (upgrade) {
|
|
|
241
245
|
// only after this process is gone — which is precisely what keeps the
|
|
242
246
|
// replacement from racing this listener onto a random fallback port.
|
|
243
247
|
let restarting = false;
|
|
244
|
-
const requestRestart = () => {
|
|
248
|
+
const requestRestart = (mode) => {
|
|
245
249
|
if (restarting) return;
|
|
246
250
|
restarting = true;
|
|
247
|
-
|
|
251
|
+
// "npx" means the newer code is not on this disk at all — the supervisor has
|
|
252
|
+
// to fetch it — so there is no target version to name yet.
|
|
253
|
+
const viaNpx = mode === "npx";
|
|
254
|
+
const to = viaNpx ? null : restartTarget();
|
|
248
255
|
process.stdout.write(
|
|
249
|
-
|
|
256
|
+
viaNpx
|
|
257
|
+
? `\n ${C.yellow}↻${C.reset} ${C.dim}updating via npx…${C.reset}\n`
|
|
258
|
+
: `\n ${C.yellow}↻${C.reset} ${C.dim}restarting${to ? ` → v${to}` : ""}…${C.reset}\n`,
|
|
250
259
|
);
|
|
251
|
-
shutdown(RESTART_CODE);
|
|
260
|
+
shutdown(viaNpx ? UPGRADE_CODE : RESTART_CODE);
|
|
252
261
|
};
|
|
253
262
|
// What a restart would land on. Read from disk now rather than remembered from
|
|
254
263
|
// boot, because the whole point is that the two differ.
|
|
@@ -283,7 +292,11 @@ if (RESPAWN) {
|
|
|
283
292
|
} else {
|
|
284
293
|
sp.stop(true, `server ready ${C.dim}→ ${C.reset}${C.bCyan}${C.bold}${url}${C.reset}`);
|
|
285
294
|
if (persist) process.stdout.write(` ${C.dim}log : ${persist}${C.reset}\n`);
|
|
286
|
-
|
|
295
|
+
// Only when one is actually being opened. Under --no-open — which is how an
|
|
296
|
+
// npx update relaunches, with a tab already waiting — this was announcing
|
|
297
|
+
// something that never happened.
|
|
298
|
+
if (openBrowser) process.stdout.write(`\n ${C.green}${C.bold}▶ opening browser…${C.reset}\n\n`);
|
|
299
|
+
else process.stdout.write("\n");
|
|
287
300
|
}
|
|
288
301
|
|
|
289
302
|
const discoveryFile = await writeDiscovery({ port: realPort, workspace });
|
|
@@ -311,7 +324,9 @@ const shutdown = async (code = 0) => {
|
|
|
311
324
|
// on its own before either timer runs, Node would otherwise exit 0 and the
|
|
312
325
|
// supervisor would take that as "done" instead of "bring me back".
|
|
313
326
|
process.exitCode = code;
|
|
314
|
-
if (tty && code !== RESTART_CODE)
|
|
327
|
+
if (tty && code !== RESTART_CODE && code !== UPGRADE_CODE) {
|
|
328
|
+
process.stdout.write(`\n\n ${C.yellow}◉ shutting down…${C.reset}\n`);
|
|
329
|
+
}
|
|
315
330
|
await removeDiscovery(discoveryFile);
|
|
316
331
|
server.close(() => process.exit(code));
|
|
317
332
|
// SSE connections never end by themselves, so close() alone would sit out the
|