agent-dag 1.43.0 → 1.45.0
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/README.md +13 -7
- package/bin/agent-dag.js +87 -9
- package/bin/deck.js +133 -22
- package/dist/web/assets/index-Bdl1LX0-.css +1 -0
- package/dist/web/assets/index-jXBjwwZC.js +89 -0
- package/dist/web/index.html +2 -2
- package/hook/hook.js +120 -31
- package/package.json +2 -2
- package/src/server/args.mjs +113 -15
- package/src/server/ccusage.mjs +105 -1
- package/src/server/claude-accounts.mjs +145 -1
- package/src/server/codex-auth.mjs +9 -4
- package/src/server/codex-quota.mjs +95 -3
- package/src/server/codex-usage.mjs +163 -9
- package/src/server/cswap-admin.mjs +346 -40
- package/src/server/cswap-auto.mjs +365 -12
- package/src/server/cswap-install.mjs +238 -17
- package/src/server/exec.mjs +233 -26
- package/src/server/index.mjs +1994 -157
- package/src/server/installer.mjs +173 -11
- package/src/server/invoked-as.mjs +16 -14
- package/src/server/quota.mjs +131 -34
- package/src/server/retire-sound-hook.mjs +315 -0
- package/src/server/self-update.mjs +262 -21
- package/src/server/supervisor.mjs +103 -0
- package/src/server/system-metrics.mjs +105 -7
- package/src/server/uv-bootstrap.mjs +43 -11
- package/dist/web/assets/index-BxAZQc7O.css +0 -1
- package/dist/web/assets/index-DRgZVqF-.js +0 -78
- package/hook/notify.js +0 -60
- package/src/server/sound-hook.mjs +0 -390
package/README.md
CHANGED
|
@@ -135,7 +135,9 @@ them, the other one's hooks, installs and panels are skipped rather than shown
|
|
|
135
135
|
empty — the boot banner says which way it went, and `--claude` / `--codex`
|
|
136
136
|
override it if the guess is wrong.
|
|
137
137
|
|
|
138
|
-
`--workspace` is a filter this deck applies to itself, not a claim on the sessions it matches: **every** running deck whose workspace contains a session's directory draws that session, so a machine-wide deck and one scoped to `~/proj` both show the agents working inside `~/proj`. It reads the same way on
|
|
138
|
+
`--workspace` is a filter this deck applies to itself, not a claim on the sessions it matches: **every** running deck whose workspace contains a session's directory draws that session, so a machine-wide deck and one scoped to `~/proj` both show the agents working inside `~/proj`. It reads the same way on all three paths a session can reach the canvas by — Claude Code's hook, Codex's rollout files, and the boot replay of the events log — and the events log still gets exactly one copy of each event, whichever decks are up. A relative path is resolved against the directory you start the deck in, and once, so every path scopes to the same tree. The log is machine-wide by default and shared by every deck on the box, so a scoped deck replays only the part of it that is inside its own workspace: it comes up showing what it will go on to capture, and nothing else.
|
|
139
|
+
|
|
140
|
+
That one events log is also the reason Clear is not quite the per-deck button it looks like. The decks elect a single writer for each log file, and only that deck may empty it: Clear on any other deck wipes its own canvas and leaves the file to the deck that writes it. The confirmation says which of the two you are about to do, and how many decks share the log when it is yours to empty — so `--history` or `--no-persist` gives a deck a log of its own if you want Clear to answer to nobody else.
|
|
139
141
|
|
|
140
142
|
Environment:
|
|
141
143
|
|
|
@@ -180,7 +182,7 @@ What the banner offers depends on how this copy was installed:
|
|
|
180
182
|
|
|
181
183
|
| Installed as | Offer |
|
|
182
184
|
|---|---|
|
|
183
|
-
| global npm install | **Update now** — runs `npm install -g agents-deck
|
|
185
|
+
| global npm install | **Update now** — runs `npm install -g` on the name you installed (`ccdeck`, `agents-deck` or `agent-dag`), then restarts once nothing is running |
|
|
184
186
|
| `npx` | **Update & restart** — re-runs the spec through npx, which fetches a fresh copy and takes over the same port |
|
|
185
187
|
| git checkout | the command, because your working copy leads npm: `git pull && npm run build` |
|
|
186
188
|
| directory not writable | the command — a root-owned prefix is declined up front rather than failing inside npm |
|
|
@@ -208,14 +210,18 @@ under three, and `npx` runs the same deck from any of them.
|
|
|
208
210
|
|
|
209
211
|
```bash
|
|
210
212
|
npx ccdeck # this repo's name — the short one
|
|
211
|
-
npx agents-deck # the
|
|
213
|
+
npx agents-deck # the name it shipped under before the rename
|
|
212
214
|
npx agent-dag # the original name; existing installs and scripts keep working
|
|
213
215
|
```
|
|
214
216
|
|
|
215
|
-
|
|
216
|
-
`
|
|
217
|
-
three commands on your `PATH`.
|
|
218
|
-
|
|
217
|
+
The three are one build published three times, so they behave identically:
|
|
218
|
+
`npx` on any of them fetches a single package, and `npm i -g` on any of them
|
|
219
|
+
puts all three commands on your `PATH`.
|
|
220
|
+
|
|
221
|
+
`ccdeck` was the exception until recently — a thin package that depended on
|
|
222
|
+
`agents-deck` and spawned its binary. It worked, but `npx ccdeck` downloaded two
|
|
223
|
+
packages instead of one and a global `ccdeck` ran under the old package's name.
|
|
224
|
+
It is now the same build as the other two.
|
|
219
225
|
|
|
220
226
|
The repository was previously named `agents-deck`; the old URL redirects here,
|
|
221
227
|
so existing clones, links and bookmarks keep working.
|
package/bin/agent-dag.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
// Everything else the deck does still lives in bin/deck.js. This file must stay
|
|
32
32
|
// boring: it is the one process that is never replaced.
|
|
33
33
|
import { spawn } from "node:child_process";
|
|
34
|
+
import { existsSync } from "node:fs";
|
|
34
35
|
import { connect } from "node:net";
|
|
35
36
|
import { dirname, join } from "node:path";
|
|
36
37
|
import { fileURLToPath } from "node:url";
|
|
@@ -38,10 +39,10 @@ import { killTree } from "../src/server/exec.mjs";
|
|
|
38
39
|
import { invokedAs } from "../src/server/invoked-as.mjs";
|
|
39
40
|
import { npxFailureHint, npxFailureSummary, npxLaunch, npxPrefetch } from "../src/server/npx.mjs";
|
|
40
41
|
import {
|
|
41
|
-
bareSpecName, claimRestartFailureKey, clearRestartFailure,
|
|
42
|
-
npxRestartSpec, readRestartFailure, recordRestartFailure,
|
|
42
|
+
bareSpecName, claimRestartFailureKey, clearRestartFailure, installedName, installedVersion,
|
|
43
|
+
lastKnownLatest, npxRestartSpec, readRestartFailure, recordRestartFailure, successorRoot,
|
|
43
44
|
} from "../src/server/self-update.mjs";
|
|
44
|
-
import { dieOfSignal, upgradeAttempt, upgradeRefusalText, workerExitAction } from "../src/server/supervisor.mjs";
|
|
45
|
+
import { dieOfSignal, dieWithParent, replacedNote, upgradeAttempt, upgradeRefusalText, workerExitAction } from "../src/server/supervisor.mjs";
|
|
45
46
|
import { colorProfile, glyphs, palette, unicodeOK } from "../src/server/term.mjs";
|
|
46
47
|
import { PRODUCT } from "../src/server/brand.mjs";
|
|
47
48
|
|
|
@@ -101,6 +102,19 @@ let attempting = null;
|
|
|
101
102
|
let stopping = false;
|
|
102
103
|
|
|
103
104
|
function launch(respawn) {
|
|
105
|
+
// Asked on every spawn rather than once at boot: the deletion this catches
|
|
106
|
+
// happens while the deck is running, so a constant read at import time would
|
|
107
|
+
// be the one answer that cannot see it. See supervisor.mjs for the whole case.
|
|
108
|
+
const replaced = replacedNote({
|
|
109
|
+
workerExists: existsSync(WORKER),
|
|
110
|
+
moved: successorRoot(PKG_ROOT),
|
|
111
|
+
product: PRODUCT,
|
|
112
|
+
command: INVOKED_AS ?? PRODUCT,
|
|
113
|
+
});
|
|
114
|
+
if (replaced) {
|
|
115
|
+
console.error(replaced);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
104
118
|
const args = [WORKER, ...process.argv.slice(2)];
|
|
105
119
|
// Appended last so it wins: the worker's parser keeps the final --port.
|
|
106
120
|
if (respawn && boundPort != null) args.push("--port", String(boundPort));
|
|
@@ -188,6 +202,43 @@ function withoutPortAndOpen(args) {
|
|
|
188
202
|
return out;
|
|
189
203
|
}
|
|
190
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Which package an npx upgrade here names, and the spec that installs it — or
|
|
207
|
+
* null when this deck was not started by npx and there is nothing to re-run.
|
|
208
|
+
*
|
|
209
|
+
* One function because there is one answer, and the worker above us reaches it
|
|
210
|
+
* independently: `upgradeName` in self-update.mjs resolves the npx case as
|
|
211
|
+
* `bareSpecName(npxRestartSpec(pkgRoot, installedName(pkgRoot)))`, and
|
|
212
|
+
* everything registry-shaped on that side — the dist-tag it fetched, the marker
|
|
213
|
+
* it cached the answer in, the failure note the browser reads — is keyed by it.
|
|
214
|
+
* This supervisor keyed the same four things off `npxRestartSpec(PKG_ROOT)`
|
|
215
|
+
* with the module's own default, which is the literal string "agents-deck".
|
|
216
|
+
*
|
|
217
|
+
* Both read `_npx/<hash>/package.json`, so they agreed for as long as that
|
|
218
|
+
* metadata was readable, and #340 made the disagreement matter when it is not:
|
|
219
|
+
* the three names are one tarball now, so the manifest under a `npx ccdeck` run
|
|
220
|
+
* genuinely says `ccdeck` where it once always said `agents-deck`. A cache
|
|
221
|
+
* directory written by an npm whose `_npx.packages` layout differs — or a
|
|
222
|
+
* truncated file — split the two apart: the worker asked npm about `ccdeck` and
|
|
223
|
+
* read `.restart-failed-ccdeck-<pid>` while this process wrote
|
|
224
|
+
* `.restart-failed-agents-deck-<pid>`, took `lastKnownLatest("agents-deck")`
|
|
225
|
+
* off a marker nobody had written — so `target` was null and upgradeAttempt's
|
|
226
|
+
* per-target cap degraded to "any failure counts" — and relaunched
|
|
227
|
+
* `npx -y agents-deck@latest`, moving a `npx ccdeck` user onto a different
|
|
228
|
+
* published name. The note went where the browser never looks, which is the
|
|
229
|
+
* exact class of bug NOTE_PREFIX's naming exists to prevent.
|
|
230
|
+
*
|
|
231
|
+
* So the fallback is the name npm's rename wrote into this build's own manifest,
|
|
232
|
+
* which is what the worker falls back to as well. Read on each call rather than
|
|
233
|
+
* once at import for the reason installedVersion gives: the manifest is a file
|
|
234
|
+
* on disk, and this process outlives more than one of them.
|
|
235
|
+
*/
|
|
236
|
+
function npxUpgrade() {
|
|
237
|
+
const self = installedName(PKG_ROOT);
|
|
238
|
+
const spec = npxRestartSpec(PKG_ROOT, self);
|
|
239
|
+
return spec ? { spec, pkgName: bareSpecName(spec) ?? self } : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
191
242
|
/** The note the browser reads, written by the only process that knows why an
|
|
192
243
|
* upgrade did not happen. `failedAt` is when the FETCH failed, which a refusal
|
|
193
244
|
* restating an earlier failure carries forward — see upgradeAttempt. */
|
|
@@ -227,11 +278,11 @@ async function prefetchUpgrade(worker) {
|
|
|
227
278
|
if (stopping || fetching || attempting) return;
|
|
228
279
|
const reply = (msg) => { try { worker.send?.(msg); } catch { /* the worker is gone */ } };
|
|
229
280
|
|
|
230
|
-
const
|
|
281
|
+
const upgrade = npxUpgrade();
|
|
231
282
|
// Not an npx run after all, so there is nothing to fetch and the files on
|
|
232
283
|
// disk are already the newest this deck can reach.
|
|
233
|
-
if (!
|
|
234
|
-
const pkgName =
|
|
284
|
+
if (!upgrade) { reply({ type: "upgrade-refused", error: "this deck was not started by npx" }); return; }
|
|
285
|
+
const { spec, pkgName } = upgrade;
|
|
235
286
|
// What the banner offered, straight off the marker the version check writes.
|
|
236
287
|
// The note is keyed by it so that "this exact version already failed here" is
|
|
237
288
|
// a question anything can answer — and so a newer release clears the slate.
|
|
@@ -284,8 +335,12 @@ async function prefetchUpgrade(worker) {
|
|
|
284
335
|
* down), and someone has to bring the working copy back when it does.
|
|
285
336
|
*/
|
|
286
337
|
function launchNpx() {
|
|
287
|
-
const
|
|
288
|
-
if (!
|
|
338
|
+
const upgrade = npxUpgrade();
|
|
339
|
+
if (!upgrade) { launch(true); return; } // not an npx run after all
|
|
340
|
+
// The same package prefetchUpgrade fetched and the worker asked npm about —
|
|
341
|
+
// one answer, so the spec that is run, the note that records it and the
|
|
342
|
+
// marker its target came from all name one package. See npxUpgrade.
|
|
343
|
+
const { spec, pkgName } = upgrade;
|
|
289
344
|
// Our two are appended, so the originals are dropped rather than left to be
|
|
290
345
|
// overridden — `--port 4317 --no-open --port 4317 --no-open` works, but it is
|
|
291
346
|
// what the next person reads in `ps`.
|
|
@@ -298,7 +353,6 @@ function launchNpx() {
|
|
|
298
353
|
// A retry answers for itself: whatever the last attempt left on disk is about
|
|
299
354
|
// to be replaced by this attempt's outcome, and leaving it there would keep
|
|
300
355
|
// the browser showing an old failure over a running fetch.
|
|
301
|
-
const pkgName = bareSpecName(spec) ?? "agents-deck";
|
|
302
356
|
clearRestartFailure(pkgName);
|
|
303
357
|
|
|
304
358
|
// No "fetching…" line here any more: prefetchUpgrade printed it and the
|
|
@@ -424,4 +478,28 @@ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
|
424
478
|
});
|
|
425
479
|
}
|
|
426
480
|
|
|
481
|
+
// And the same leash the worker is on, one link up (#702).
|
|
482
|
+
//
|
|
483
|
+
// Inert for every published way of starting the deck: `ccdeck`, `npx ccdeck` and
|
|
484
|
+
// the npx relaunch in launchNpx all spawn this file with no IPC channel, so
|
|
485
|
+
// dieWithParent declines to arm and this line costs nothing. It is armed by a
|
|
486
|
+
// harness that DOES hand one over — the suite's spawnSupervised — and what it
|
|
487
|
+
// buys there is the case no teardown can cover, because the teardown is not
|
|
488
|
+
// running: a vitest worker killed outright leaves this supervisor orphaned, and
|
|
489
|
+
// an orphaned supervisor keeps a worker alive under it.
|
|
490
|
+
//
|
|
491
|
+
// Taking the worker with us is the whole of the stop. `stopping` first, so the
|
|
492
|
+
// worker's exit is not read as a request to bring it back; the kill is the
|
|
493
|
+
// signal on POSIX and, through killTree, `taskkill /T /F` on Windows, which has
|
|
494
|
+
// neither SIGTERM nor a process group to aim at.
|
|
495
|
+
dieWithParent(() => {
|
|
496
|
+
stopping = true;
|
|
497
|
+
if (fetching) { killTree(fetching, "SIGTERM"); fetching = null; }
|
|
498
|
+
if (child) killTree(child, "SIGTERM");
|
|
499
|
+
// Long enough for the worker to unregister its discovery file and let the
|
|
500
|
+
// port go, short enough that nothing is waiting on us. Unref'd: if the worker
|
|
501
|
+
// leaves first its exit handler ends this process and the timer never fires.
|
|
502
|
+
setTimeout(() => process.exit(0), 3000).unref();
|
|
503
|
+
});
|
|
504
|
+
|
|
427
505
|
launch(false);
|
package/bin/deck.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import { resolve, dirname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
-
import { dieOfSignal } from "../src/server/supervisor.mjs";
|
|
11
|
-
import { parseArgs } from "../src/server/args.mjs";
|
|
10
|
+
import { dieOfSignal, dieWithParent } from "../src/server/supervisor.mjs";
|
|
11
|
+
import { isPortValue, parseArgs } from "../src/server/args.mjs";
|
|
12
12
|
import {
|
|
13
13
|
CURSOR_HIDE, CURSOR_SHOW, colorProfile, fit, glyphs, labelColumn, link, motionOK, oneLine,
|
|
14
14
|
palette, pulseText, spinnerFrames, statusLine, supportsHyperlinks, termColumns, unicodeOK,
|
|
@@ -64,34 +64,94 @@ if (flags.version) {
|
|
|
64
64
|
|
|
65
65
|
if (flags.uninstall) {
|
|
66
66
|
const { uninstallHooks, hasCodexInstalled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
67
|
+
// Anything that could not be taken out. An uninstall that removed nothing
|
|
68
|
+
// because it could not read the file has not uninstalled anything, and both
|
|
69
|
+
// the wording and the exit code have to say so: a user who is told it worked
|
|
70
|
+
// and still has our hooks firing on every event is worse off than one who is
|
|
71
|
+
// told it failed, because they have stopped looking.
|
|
72
|
+
let refused = false;
|
|
73
|
+
// Files already reported as unparseable. The Claude hooks and the sound hook
|
|
74
|
+
// live in the SAME settings.json, so a stray comma refuses both, and printing
|
|
75
|
+
// the whole path-plus-parser-error twice buries the one line that differs —
|
|
76
|
+
// which of our two installations is still in there.
|
|
77
|
+
const named = new Set();
|
|
78
|
+
/** Report one provider's outcome. `ok` first — see uninstallHooks. */
|
|
79
|
+
const report = (res, label) => {
|
|
80
|
+
if (res.ok === false) {
|
|
81
|
+
refused = true;
|
|
82
|
+
named.add(res.settingsPath);
|
|
83
|
+
console.error(`${PRODUCT}: ${label} hooks NOT removed — ${res.settingsPath} could not be read as JSON (${res.why}).`);
|
|
84
|
+
console.error(`${PRODUCT}: the __agent-dag hook entries are still in that file and keep firing on every ${label} event.`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.log(res.changed
|
|
88
|
+
? `${PRODUCT}: hooks removed from ${res.settingsPath}`
|
|
89
|
+
: `${PRODUCT}: no ${label} hooks to remove`);
|
|
90
|
+
};
|
|
91
|
+
report(await uninstallHooks({ provider: "claude" }), "Claude");
|
|
92
|
+
// The old finish sound was a second entry in the same file, marked
|
|
72
93
|
// __agent-dag-sound rather than __agent-dag, and uninstallHooks does not know
|
|
73
94
|
// that mark — so it used to be left behind, playing on every turn after the
|
|
74
|
-
// deck was supposedly gone.
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
// deck was supposedly gone. #704 retired the mechanism outright, but the entry
|
|
96
|
+
// is still on every machine that had it, so removing it is still this
|
|
97
|
+
// command's job. So is the other half: turning the sound on parked the user's
|
|
98
|
+
// own afplay/PowerShell Stop hooks, and once the deck is uninstalled nothing
|
|
99
|
+
// else on the machine knows where they went.
|
|
100
|
+
const { retireSoundHook } = await import(pathToFileURL(join(PKG_ROOT, "src/server/retire-sound-hook.mjs")).href);
|
|
101
|
+
const sound = await retireSoundHook();
|
|
102
|
+
if (sound.removed) console.log(`${PRODUCT}: sound hook removed`);
|
|
103
|
+
if (sound.restored) console.log(`${PRODUCT}: restored ${sound.restored} of your own sound hook(s)`);
|
|
79
104
|
if (sound.ok === false) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
refused = true;
|
|
106
|
+
// Two different refusals, and saying the wrong one sends the user to the
|
|
107
|
+
// wrong file. `settings_unreadable` means nothing was touched at all — and
|
|
108
|
+
// when the forwarders already named that same file, the whole path and
|
|
109
|
+
// parser error would only bury the one line that differs. `parked_unreadable`
|
|
110
|
+
// is the other file: our entry IS out (the lines above said so), and what is
|
|
111
|
+
// still owed is the user's own hooks, which stay parked until they repair it.
|
|
112
|
+
if (sound.reason === "settings_unreadable") {
|
|
113
|
+
console.error(named.has(sound.settingsPath)
|
|
114
|
+
? `${PRODUCT}: the sound hook is still in that file too.`
|
|
115
|
+
: `${PRODUCT}: sound hook left in place — ${sound.message}`);
|
|
116
|
+
named.add(sound.settingsPath);
|
|
117
|
+
} else {
|
|
118
|
+
console.error(`${PRODUCT}: your own sound hooks were NOT restored — ${sound.message}`);
|
|
119
|
+
}
|
|
84
120
|
}
|
|
85
121
|
if (hasCodexInstalled()) {
|
|
86
|
-
|
|
87
|
-
console.log(codex.changed
|
|
88
|
-
? `${PRODUCT}: hooks removed from ${codex.settingsPath}`
|
|
89
|
-
: `${PRODUCT}: no Codex hooks to remove`);
|
|
122
|
+
report(await uninstallHooks({ provider: "codex" }), "Codex");
|
|
90
123
|
}
|
|
91
|
-
|
|
124
|
+
// The remedy last and once, after every symptom above it, rather than once
|
|
125
|
+
// per refusal in the middle of the list.
|
|
126
|
+
if (named.size > 0) {
|
|
127
|
+
console.error(`${PRODUCT}: repair the JSON (or move the file aside), then run \`${PRODUCT} --uninstall\` again.`);
|
|
128
|
+
}
|
|
129
|
+
// Non-zero when any half of it refused, so `ccdeck --uninstall && …` and every
|
|
130
|
+
// CI step that runs this stops on the failure instead of continuing past it.
|
|
131
|
+
process.exit(refused ? 1 : 0);
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
|
|
134
|
+
// The port, and the one piece of argv the deck really does refuse to boot over.
|
|
135
|
+
//
|
|
136
|
+
// It refused before too — `--port banana` and `--port --no-open` both became
|
|
137
|
+
// `Number(…)` → `NaN`, which survived the whole startup (hooks installed,
|
|
138
|
+
// claude-swap installed, ccusage probed) and then killed the process from inside
|
|
139
|
+
// `listen` with Node's own wording: "options.port should be >= 0 and < 65536.
|
|
140
|
+
// Received type number (NaN)." That names neither the flag nor the value the
|
|
141
|
+
// user typed, and arrives after a page of green ticks. Same outcome, said here:
|
|
142
|
+
// early, in the deck's own voice, quoting the flag and the value back.
|
|
143
|
+
//
|
|
144
|
+
// An empty `AGENT_DAG_PORT` is an unset one — a variable that did not expand is
|
|
145
|
+
// not a request for port zero. `--port ""` never reaches this, because the
|
|
146
|
+
// parser records an empty value as `incomplete` and leaves the flag unset.
|
|
147
|
+
const envPort = process.env.AGENT_DAG_PORT?.trim();
|
|
148
|
+
const rawPort = flags.port ?? (envPort ? envPort : null);
|
|
149
|
+
if (rawPort != null && !isPortValue(rawPort)) {
|
|
150
|
+
const named = flags.port != null ? "--port" : "AGENT_DAG_PORT";
|
|
151
|
+
console.error(`${PRODUCT}: ${named} ${rawPort}: not a port number — expected 0–65535.`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const port = rawPort == null ? 4317 : Number(rawPort);
|
|
95
155
|
// Default = machine-wide (capture every CC session on this box). Pass
|
|
96
156
|
// `--workspace <path>` (or `--scope`) to restrict to a single tree. Canonicalized
|
|
97
157
|
// just below, once the module that owns that rule is loaded.
|
|
@@ -213,6 +273,7 @@ process.on("SIGHUP", () => { showCursor(); dieOfSignal("SIGHUP"); });
|
|
|
213
273
|
const LABELS = [
|
|
214
274
|
"workspace", "Claude hooks", "Codex sessions", "claude-swap", "accounts",
|
|
215
275
|
"ccusage", "update", "name", "server ready", "log", "unknown option",
|
|
276
|
+
"missing value",
|
|
216
277
|
];
|
|
217
278
|
const LABEL_W = labelColumn(LABELS);
|
|
218
279
|
|
|
@@ -617,6 +678,22 @@ let server = null;
|
|
|
617
678
|
let discovery = null;
|
|
618
679
|
let discoveryFile = null;
|
|
619
680
|
|
|
681
|
+
// This worker does not outlive the supervisor that started it (#702).
|
|
682
|
+
//
|
|
683
|
+
// Armed HERE rather than beside the signal handlers at the bottom of the file,
|
|
684
|
+
// for the reason the three `let`s above are where they are: this is the first
|
|
685
|
+
// line from which `shutdown` can be called without dying of a temporal dead
|
|
686
|
+
// zone, and the window that matters is the boot — the supervisor can be killed
|
|
687
|
+
// while the report is still printing, and a worker orphaned in there is a
|
|
688
|
+
// worker that binds its port a moment later and then holds it for a day.
|
|
689
|
+
//
|
|
690
|
+
// A no-op with nothing supervising us, which is `node bin/deck.js` run by hand:
|
|
691
|
+
// dieWithParent arms only when there is a channel, the same question SUPERVISED
|
|
692
|
+
// asks above. Shutdown code 0 because there is nobody left to read it — the
|
|
693
|
+
// point is only that discovery is unregistered and the port let go of on the
|
|
694
|
+
// way out, rather than left for the next boot's stale sweep.
|
|
695
|
+
dieWithParent(() => shutdown(0));
|
|
696
|
+
|
|
620
697
|
// The listen, begun HERE and awaited below the startup report rather than after
|
|
621
698
|
// it. The report is a narration; the port is the product, and it was queued
|
|
622
699
|
// behind three tool probes for no reason but the order these two statements
|
|
@@ -697,6 +774,7 @@ if (RESPAWN) {
|
|
|
697
774
|
// and then went quiet for every restart afterwards is back to hiding it from
|
|
698
775
|
// anyone who was not watching the first boot.
|
|
699
776
|
reportUnknownFlags(flags.unknown);
|
|
777
|
+
reportIncompleteFlags(flags.incomplete);
|
|
700
778
|
} else {
|
|
701
779
|
// The URL is the one detail an ellipsis would destroy — half an address is
|
|
702
780
|
// not a shorter address — so it keeps its own line when the terminal is too
|
|
@@ -708,6 +786,7 @@ if (RESPAWN) {
|
|
|
708
786
|
if (persist) write(row({ label: "log", detail: fileLink(persist) }));
|
|
709
787
|
// Last of the rows, on purpose — see reportUnknownFlags.
|
|
710
788
|
reportUnknownFlags(flags.unknown);
|
|
789
|
+
reportIncompleteFlags(flags.incomplete);
|
|
711
790
|
// Only when one is actually being opened. Under --no-open — which is how an
|
|
712
791
|
// npx update relaunches, with a tab already waiting — this was announcing
|
|
713
792
|
// something that never happened.
|
|
@@ -909,6 +988,33 @@ function reportUnknownFlags(unknown) {
|
|
|
909
988
|
}
|
|
910
989
|
}
|
|
911
990
|
|
|
991
|
+
/**
|
|
992
|
+
* Every value-taking flag that was given no value it could use, named, one row
|
|
993
|
+
* each — and printed beside the unknown ones because it is the same failure
|
|
994
|
+
* wearing a different hat.
|
|
995
|
+
*
|
|
996
|
+
* #697: `--workspace`, `--history` and `--port` used to consume the following
|
|
997
|
+
* token whatever it was, so `ccdeck --workspace $PROJ --no-persist` with `PROJ`
|
|
998
|
+
* unset scoped the deck to a directory called `--no-persist`, kept persisting to
|
|
999
|
+
* the shared log, and reported neither. Nothing landed in `unknown`, because the
|
|
1000
|
+
* token that belonged there had been eaten. The parser refuses that value now
|
|
1001
|
+
* and lists the flag here instead.
|
|
1002
|
+
*
|
|
1003
|
+
* Said rather than acted on, under exactly the argument reportUnknownFlags makes
|
|
1004
|
+
* above: the flag falls back to its documented default and the deck still boots.
|
|
1005
|
+
* The row is what makes the fallback a decision the user can see, and the rows
|
|
1006
|
+
* around it show its consequence — `workspace (all)` and the `log` line are
|
|
1007
|
+
* printed by the same report.
|
|
1008
|
+
*/
|
|
1009
|
+
function reportIncompleteFlags(incomplete) {
|
|
1010
|
+
for (const { flag, expects } of incomplete ?? []) {
|
|
1011
|
+
write(row({
|
|
1012
|
+
mark: G.warn, tone: P.warn, label: "missing value",
|
|
1013
|
+
detail: `${flag} ${G.dash} expected ${expects}; using the default`,
|
|
1014
|
+
}));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
912
1018
|
function printHelp() {
|
|
913
1019
|
process.stdout.write(`${PRODUCT} — live deck of Claude Code + Codex agents
|
|
914
1020
|
|
|
@@ -936,5 +1042,10 @@ Options:
|
|
|
936
1042
|
|
|
937
1043
|
Anything else on the command line is reported as an unknown option and then
|
|
938
1044
|
ignored: the deck still starts.
|
|
1045
|
+
|
|
1046
|
+
A flag that takes a value never swallows the next flag. If the value is missing,
|
|
1047
|
+
empty, or itself looks like a flag — \`${PRODUCT} --workspace \$UNSET --no-persist\`
|
|
1048
|
+
after the shell has dropped an unset variable — the flag is reported, left on its
|
|
1049
|
+
default, and the token it would have eaten is parsed as the flag it is.
|
|
939
1050
|
`);
|
|
940
1051
|
}
|