ccdeck 3.22.8 → 3.22.10
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 +26 -26
- package/bin/agent-dag.js +8 -2
- package/bin/deck.js +90 -62
- package/dist/web/assets/index-CEo8A2T-.js +274 -0
- package/dist/web/assets/index-DIsEfUjc.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -3
- package/release-notes.json +10 -0
- package/src/server/boot-lock.mjs +137 -0
- package/src/server/installer.mjs +2 -2
- package/src/server/invoked-as.mjs +4 -2
- package/src/server/lan-sync.mjs +21 -1
- package/src/server/login-service.mjs +24 -0
- package/src/server/running-deck.mjs +110 -101
- package/src/server/self-update.mjs +70 -36
- package/dist/web/assets/index-BJnVANQi.css +0 -1
- package/dist/web/assets/index-BjQA8le6.js +0 -274
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ The deck opens on these eight pictures the first time it runs — they are the w
|
|
|
96
96
|
## Quick start
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
|
-
npx ccdeck
|
|
99
|
+
npx ccdeck
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
Opens **http://127.0.0.1:4317**, shows an eight-picture tour the first time, and registers the Claude Code hook on first run. If something else already holds 4317, the deck takes a port between 4318 and 4400 instead and prints the address it ended up on — that line in the terminal is the one to trust. Start any Claude Code or Codex session and the graph fills in live.
|
|
@@ -241,9 +241,9 @@ ccdeck [options]
|
|
|
241
241
|
--no-open Don't open the browser automatically
|
|
242
242
|
--foreground Hold the terminal, the way every version before 3.20
|
|
243
243
|
did (Ctrl+C stops the deck again)
|
|
244
|
-
--new
|
|
245
|
-
--stop Stop the deck
|
|
246
|
-
(--port <n> stops
|
|
244
|
+
--new Replace the running deck with a fresh one
|
|
245
|
+
--stop Stop the running deck
|
|
246
|
+
(--port <n> stops only the one on that port)
|
|
247
247
|
--status What is running on this machine, and on which ports
|
|
248
248
|
--logs What a backgrounded deck wrote where a terminal would
|
|
249
249
|
have shown it
|
|
@@ -272,10 +272,8 @@ dashboard.
|
|
|
272
272
|
|
|
273
273
|
`--foreground` is there for anything that was relying on the old behaviour — a
|
|
274
274
|
wrapper script, a CI step, a supervisor of your own that starts the deck and
|
|
275
|
-
waits on it. On the first run after upgrading
|
|
276
|
-
|
|
277
|
-
check, so it starts beside them rather than attaching to something it cannot
|
|
278
|
-
identify, and `ccdeck --stop --all` clears them.
|
|
275
|
+
waits on it. On the first run after upgrading, the deck from the older version
|
|
276
|
+
is stopped and the new one takes its place; the boot report says so.
|
|
279
277
|
|
|
280
278
|
`ccdeck` runs in the background. The boot report prints in your terminal exactly
|
|
281
279
|
as it always has — the hooks, the port, the URL — and then the prompt comes back
|
|
@@ -317,9 +315,16 @@ The port fallback is still there — 4317 is also the standard OTLP collector
|
|
|
317
315
|
port, and Windows reserves whole TCP ranges for Hyper-V, WSL2 and Docker — but
|
|
318
316
|
it now runs only when the thing holding the port is *not* one of your decks. The
|
|
319
317
|
deck on the port has to prove it is yours, with the same token handshake the
|
|
320
|
-
hooks use, before its tab is opened.
|
|
321
|
-
|
|
322
|
-
|
|
318
|
+
hooks use, before its tab is opened.
|
|
319
|
+
|
|
320
|
+
**There is only ever one.** A start that asks for a different deck — a port, a
|
|
321
|
+
workspace, a log, either `--codex` or `--claude` pair — or that is a newer
|
|
322
|
+
version than the deck running, stops that deck and takes its place; `--new` does
|
|
323
|
+
so unconditionally. Two starts at the same moment, such as the login item and a
|
|
324
|
+
terminal opened at login, wait for each other instead of both starting. The rule
|
|
325
|
+
holds per Claude config directory: pointing `CLAUDE_CONFIG_DIR` somewhere else
|
|
326
|
+
is a second deck with its own identity, as it always was, and the login item is
|
|
327
|
+
given the same directory as the shell that installed it.
|
|
323
328
|
|
|
324
329
|
ccdeck looks for each CLI before it does anything on that CLI's behalf. Claude
|
|
325
330
|
Code counts as present when its binary is on `PATH` (or in one of the places its
|
|
@@ -380,7 +385,7 @@ What the banner offers depends on how this copy was installed:
|
|
|
380
385
|
|
|
381
386
|
| Installed as | Offer |
|
|
382
387
|
|---|---|
|
|
383
|
-
| global npm install | **Update now** — runs `npm install -g` on the
|
|
388
|
+
| global npm install | **Update now** — runs `npm install -g` on the package you installed, then restarts once nothing is running |
|
|
384
389
|
| `npx` | **Update & restart** — re-runs the spec through npx, which fetches a fresh copy and takes over the same port |
|
|
385
390
|
| git checkout | the command, because your working copy leads npm: `git pull && npm run build` |
|
|
386
391
|
| directory not writable | the command — a root-owned prefix is declined up front rather than failing inside npm |
|
|
@@ -403,24 +408,19 @@ It restarts on its own only after 30 seconds with nothing running, because hook
|
|
|
403
408
|
|
|
404
409
|
## Names
|
|
405
410
|
|
|
406
|
-
**ccdeck** is the name — of this repo
|
|
407
|
-
|
|
411
|
+
**ccdeck** is the name — of this repo, of the npm package and of the command.
|
|
412
|
+
|
|
413
|
+
The deck used to be published as `agents-deck` and `agent-dag` as well. Neither
|
|
414
|
+
is published any more; both stay on npm at their last version, 3.22.8. An
|
|
415
|
+
install under one of them keeps running, but it may never update itself again,
|
|
416
|
+
and the only command it puts on your `PATH` is the old one. To move to `ccdeck`:
|
|
408
417
|
|
|
409
418
|
```bash
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
419
|
+
npm rm -g agents-deck agent-dag
|
|
420
|
+
npm i -g ccdeck
|
|
421
|
+
ccdeck --install-service # only if the deck started when you logged in
|
|
413
422
|
```
|
|
414
423
|
|
|
415
|
-
The three are one build published three times, so they behave identically:
|
|
416
|
-
`npx` on any of them fetches a single package, and `npm i -g` on any of them
|
|
417
|
-
puts all three commands on your `PATH`.
|
|
418
|
-
|
|
419
|
-
`ccdeck` was the exception until recently — a thin package that depended on
|
|
420
|
-
`agents-deck` and spawned its binary. It worked, but `npx ccdeck` downloaded two
|
|
421
|
-
packages instead of one and a global `ccdeck` ran under the old package's name.
|
|
422
|
-
It is now the same build as the other two.
|
|
423
|
-
|
|
424
424
|
The repository was previously named `agents-deck`; the old URL redirects here,
|
|
425
425
|
so existing clones, links and bookmarks keep working.
|
|
426
426
|
|
package/bin/agent-dag.js
CHANGED
|
@@ -40,7 +40,7 @@ import { invokedAs } from "../src/server/invoked-as.mjs";
|
|
|
40
40
|
import { isOneShot, parseArgs } from "../src/server/args.mjs";
|
|
41
41
|
import { npxFailureHint, npxFailureSummary, npxLaunch, npxPrefetch } from "../src/server/npx.mjs";
|
|
42
42
|
import {
|
|
43
|
-
bareSpecName, claimRestartFailureKey, clearRestartFailure, installedName, installedVersion,
|
|
43
|
+
bareSpecName, claimRestartFailureKey, clearRestartFailure, currentName, installedName, installedVersion,
|
|
44
44
|
isNpxInstall, lastKnownLatest, npxRestartSpec, readRestartFailure, recordRestartFailure,
|
|
45
45
|
successorRoot,
|
|
46
46
|
} from "../src/server/self-update.mjs";
|
|
@@ -354,7 +354,13 @@ function withoutPortAndOpen(args) {
|
|
|
354
354
|
function npxUpgrade() {
|
|
355
355
|
const self = installedName(PKG_ROOT);
|
|
356
356
|
const spec = npxRestartSpec(PKG_ROOT, self);
|
|
357
|
-
|
|
357
|
+
if (!spec) return null;
|
|
358
|
+
// A retired name relaunches as ccdeck, which is how upgradeName answers it on
|
|
359
|
+
// the worker's side: `npx -y agents-deck@latest` resolves to the same
|
|
360
|
+
// last-published package every time, so npx would reuse its cached copy and
|
|
361
|
+
// the deck would never move.
|
|
362
|
+
const pkgName = currentName(bareSpecName(spec) ?? self);
|
|
363
|
+
return { spec: `${pkgName}@latest`, pkgName };
|
|
358
364
|
}
|
|
359
365
|
|
|
360
366
|
/** The note the browser reads, written by the only process that knows why an
|
package/bin/deck.js
CHANGED
|
@@ -21,7 +21,10 @@ import { budget, bootDeadlineMs } from "../src/server/boot-deadline.mjs";
|
|
|
21
21
|
// with the rest rather than fetched later. Deliberately NOT the other way
|
|
22
22
|
// round: it takes the handshake as a parameter precisely so that it never has
|
|
23
23
|
// to import the server. See the note at the top of that file.
|
|
24
|
-
import {
|
|
24
|
+
import { deckRegistryDir, liveDecks, olderVersion, secondStart, versionNote } from "../src/server/running-deck.mjs";
|
|
25
|
+
// The same kind of leaf — fs, path, crypto and deck-probe.mjs — for the same
|
|
26
|
+
// reason: it is taken before anything else in the boot has run.
|
|
27
|
+
import { takeBootLock } from "../src/server/boot-lock.mjs";
|
|
25
28
|
|
|
26
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
30
|
const PKG_ROOT = resolve(__dirname, "..");
|
|
@@ -178,12 +181,11 @@ if (flags.uninstall) {
|
|
|
178
181
|
// block below gives (#797): those are declared two hundred lines further down
|
|
179
182
|
// and this runs at module top level.
|
|
180
183
|
//
|
|
181
|
-
// WHICH DECK.
|
|
182
|
-
//
|
|
183
|
-
// one
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// person reaches for.)
|
|
184
|
+
// WHICH DECK. Every one, unless `--port <n>` names one. A start keeps at most
|
|
185
|
+
// one deck now, so a second is a leftover from before that rule, and an off
|
|
186
|
+
// switch that ended one of two would leave the machine running. (`--all` is the
|
|
187
|
+
// legacy capture flag, a no-op since it became the default; beside `--stop` it
|
|
188
|
+
// always meant "every deck", and that is what the bare command means now.)
|
|
187
189
|
if (flags.stop || flags.status || flags.logs || flags.install || flags.installService || flags.uninstallService) {
|
|
188
190
|
const { dash, ok: gOk, warn: gWarn, bullet, arrow, ellipsis: gEllipsis } = glyphs(unicodeOK());
|
|
189
191
|
const tone = palette(colorProfile({ isTTY: Boolean(process.stdout.isTTY) }));
|
|
@@ -391,21 +393,18 @@ if (flags.stop || flags.status || flags.logs || flags.install || flags.installSe
|
|
|
391
393
|
// default SHAPE, so the matcher finds it without help. It is also declared
|
|
392
394
|
// below this block, which would make reading it here a ReferenceError (#797).
|
|
393
395
|
const named = flags.port != null && isPortValue(flags.port) ? Number(flags.port) : null;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
// EVERY DECK, unless one is named. There is meant to be one — a start keeps
|
|
397
|
+
// at most one now — so a second here is a leftover from before that rule, and
|
|
398
|
+
// an off switch that ended one of two would leave the machine running. `--all`
|
|
399
|
+
// is still accepted, and now means what the bare command means.
|
|
400
|
+
const wanted = named !== null ? decks.filter(d => d.port === named) : decks;
|
|
399
401
|
|
|
400
402
|
if (!wanted.length) {
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
: named !== null
|
|
407
|
-
? `no deck is listening on ${named} ${dash} \`${INVOKED_AS ?? PRODUCT} --status\` lists them`
|
|
408
|
-
: `no deck of this shape is running ${dash} \`${INVOKED_AS ?? PRODUCT} --status\` lists the ${decks.length} that ${decks.length === 1 ? "is" : "are"}`;
|
|
403
|
+
// Two different silences, and saying the wrong one sends the reader looking
|
|
404
|
+
// in the wrong place.
|
|
405
|
+
const why = named !== null && decks.length
|
|
406
|
+
? `no deck is listening on ${named} ${dash} \`${INVOKED_AS ?? PRODUCT} --status\` lists them`
|
|
407
|
+
: `no deck is running`;
|
|
409
408
|
say(`\n ${tone.muted}${dash} ${why}${tone.reset}\n`);
|
|
410
409
|
process.exit(0);
|
|
411
410
|
}
|
|
@@ -1180,6 +1179,10 @@ function restartTarget() {
|
|
|
1180
1179
|
let server = null;
|
|
1181
1180
|
let discovery = null;
|
|
1182
1181
|
let discoveryFile = null;
|
|
1182
|
+
// The boot lock, for the same reason: taken at the gate below, given back once
|
|
1183
|
+
// this deck is registered, and given back by the exit handler on every way out
|
|
1184
|
+
// before that. See src/server/boot-lock.mjs.
|
|
1185
|
+
let bootLock = null;
|
|
1183
1186
|
|
|
1184
1187
|
// This worker does not outlive the supervisor that started it (#702).
|
|
1185
1188
|
//
|
|
@@ -1235,9 +1238,11 @@ dieWithParent(() => shutdown(0));
|
|
|
1235
1238
|
// ── is one of ours already up? ────────────────────────────────────────────────
|
|
1236
1239
|
// A bare `ccdeck` typed beside a deck that is already running used to build a
|
|
1237
1240
|
// second everything on a random port, and neither half mentioned the other.
|
|
1238
|
-
//
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
+
// Then it attached only to a deck of exactly its own shape and built a second
|
|
1242
|
+
// one beside anything else — a different flag, an older version, or a login
|
|
1243
|
+
// item whose environment decided `codex` or the log path differently from the
|
|
1244
|
+
// shell's. src/server/running-deck.mjs carries the whole argument, the registry
|
|
1245
|
+
// read, the handshake and the rule; this is only where it is asked and acted on.
|
|
1241
1246
|
//
|
|
1242
1247
|
// ASKED EXACTLY HERE, and the position is the point. Everything the answer
|
|
1243
1248
|
// depends on is resolved above — the workspace, the canonical log path, which
|
|
@@ -1246,16 +1251,52 @@ dieWithParent(() => shutdown(0));
|
|
|
1246
1251
|
// discovery file written. An attach therefore leaves the machine precisely as
|
|
1247
1252
|
// it found it, which is what makes it safe to do without asking.
|
|
1248
1253
|
//
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
|
|
1255
|
-
|
|
1254
|
+
// UNDER THE BOOT LOCK, held until this deck's own record is on disk — see the
|
|
1255
|
+
// release beside discovery.check below, and boot-lock.mjs for why a registry
|
|
1256
|
+
// read alone could not close the window two starts at login fell through.
|
|
1257
|
+
//
|
|
1258
|
+
// A RESPAWN IS ASKED TOO, and answers differently. A restart is THIS deck
|
|
1259
|
+
// coming back, so there is nothing to attach to — but in the gap a crash
|
|
1260
|
+
// leaves, a `ccdeck` typed by hand finds no deck and starts one, and the
|
|
1261
|
+
// respawn that followed used to take a random port beside it. Now it finds
|
|
1262
|
+
// that deck and exits 0, and the supervisor, reading a clean exit, ends too.
|
|
1263
|
+
bootLock = await takeBootLock({ dir: deckRegistryDir() }).catch(() => null);
|
|
1264
|
+
{
|
|
1265
|
+
// `--port` alone, not AGENT_DAG_PORT: the variable is how somebody RUNS a
|
|
1266
|
+
// deck rather than which one they mean — the line `--stop` draws above.
|
|
1267
|
+
const askedPort = flags.port != null && isPortValue(flags.port) ? Number(flags.port) : null;
|
|
1268
|
+
const plan = secondStart({
|
|
1269
|
+
live: await liveDecks().catch(() => []),
|
|
1256
1270
|
want: { workspace, persist, codex: wantCodex, claude: wantClaude },
|
|
1257
|
-
|
|
1258
|
-
|
|
1271
|
+
port: askedPort,
|
|
1272
|
+
ours: PKG_VERSION,
|
|
1273
|
+
fresh: flags.new === true,
|
|
1274
|
+
respawn: RESPAWN,
|
|
1275
|
+
});
|
|
1276
|
+
if (plan.act === "yield") {
|
|
1277
|
+
console.error(`${PRODUCT}: a deck started on ${plan.deck.port} while this one was coming back ${G.dash} leaving it to that one.`);
|
|
1278
|
+
process.exit(0);
|
|
1279
|
+
}
|
|
1280
|
+
// THE NEWEST START WINS. What is stopped here is either the deck this start
|
|
1281
|
+
// replaces or, on an attach, a second deck left over from before this rule —
|
|
1282
|
+
// the duplicate the rule exists to end. Each one is said, with why, because a
|
|
1283
|
+
// deck that vanishes without a word is a mystery of its own.
|
|
1284
|
+
if (plan.stop.length) {
|
|
1285
|
+
const { stopDeck } = await import(pathToFileURL(join(PKG_ROOT, "src/server/stop-deck.mjs")).href);
|
|
1286
|
+
for (const d of plan.stop) {
|
|
1287
|
+
const why = plan.act === "replace" && flags.new === true
|
|
1288
|
+
? "you asked for a fresh one"
|
|
1289
|
+
: olderVersion(d.version, PKG_VERSION)
|
|
1290
|
+
? (d.version ? `it was v${d.version}` : "it was an older version")
|
|
1291
|
+
: plan.act === "attach" ? "it was a second deck" : "it was started with different settings";
|
|
1292
|
+
const out = await stopDeck(d).catch(() => ({ ok: false, reason: "unreachable" }));
|
|
1293
|
+
write(out.ok
|
|
1294
|
+
? `\n ${P.ok}${G.ok}${P.reset} stopped the deck on ${d.port}${P.muted} ${G.bullet} pid ${d.pid} ${G.bullet} ${why}${P.reset}\n`
|
|
1295
|
+
: `\n ${P.warn}${G.warn} could not stop the deck on ${d.port} (pid ${d.pid}) ${G.dash} ${out.reason ?? out.how}${P.reset}\n`);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (plan.act === "attach") {
|
|
1299
|
+
const live = plan.deck;
|
|
1259
1300
|
const liveUrl = `http://127.0.0.1:${live.port}`;
|
|
1260
1301
|
const note = versionNote(live.version, PKG_VERSION);
|
|
1261
1302
|
// Not the startup report's rows. That report has a label column because it
|
|
@@ -1275,7 +1316,7 @@ if (!RESPAWN && !asksForOwnDeck(flags)) {
|
|
|
1275
1316
|
if (note) write(` ${P.warn}${G.warn} ${note}${P.reset}\n`);
|
|
1276
1317
|
// THE TYPO'S WARNING, on the path that has no startup report to carry it.
|
|
1277
1318
|
//
|
|
1278
|
-
//
|
|
1319
|
+
// The gate used to answer `true` for these two so the report would
|
|
1279
1320
|
// run and print them, and that is how `ccdeck --stpo` — a misspelling of
|
|
1280
1321
|
// the flag that STOPS a deck — came to build a second one. The warning was
|
|
1281
1322
|
// the requirement; the extra process never was. Printed here, in the same
|
|
@@ -1284,8 +1325,8 @@ if (!RESPAWN && !asksForOwnDeck(flags)) {
|
|
|
1284
1325
|
reportIncompleteFlags(flags.incomplete);
|
|
1285
1326
|
// The line that says a second deck was NOT started. Without it the command
|
|
1286
1327
|
// looks like it did nothing at all, which is the other way to be confusing
|
|
1287
|
-
// about this — and it names the flag for the person who
|
|
1288
|
-
write(`\n ${P.muted}${G.dash} no second deck was started ${G.dash} \`${INVOKED_AS ?? PRODUCT} --new\`
|
|
1328
|
+
// about this — and it names the flag for the person who wanted a fresh one.
|
|
1329
|
+
write(`\n ${P.muted}${G.dash} no second deck was started ${G.dash} \`${INVOKED_AS ?? PRODUCT} --new\` replaces it with a fresh one${P.reset}\n`);
|
|
1289
1330
|
if (openBrowser) {
|
|
1290
1331
|
write(`\n ${P.ok}${P.bold}${G.play} opening browser${G.ellipsis}${P.reset}\n\n`);
|
|
1291
1332
|
try {
|
|
@@ -1420,6 +1461,10 @@ discoveryFile = discovery.file;
|
|
|
1420
1461
|
// Now, not in five seconds: nothing should reach the pulse line below without
|
|
1421
1462
|
// the deck knowing whether the hooks can see it.
|
|
1422
1463
|
await discovery.check();
|
|
1464
|
+
// Registered, so the next start can find this deck for itself and the boot
|
|
1465
|
+
// lock has done its job. Given back here rather than at exit, or a second
|
|
1466
|
+
// `ccdeck` would wait out this deck's whole life before attaching to it.
|
|
1467
|
+
bootLock?.release();
|
|
1423
1468
|
|
|
1424
1469
|
// Never on a respawn: the tab that asked for the restart is still open and
|
|
1425
1470
|
// reconnecting on its own. A second one would be the deck talking over itself.
|
|
@@ -1435,31 +1480,6 @@ if (openBrowser && !RESPAWN) {
|
|
|
1435
1480
|
} catch {}
|
|
1436
1481
|
}
|
|
1437
1482
|
|
|
1438
|
-
// ── the deck from before this version that is still running ──────────────────
|
|
1439
|
-
//
|
|
1440
|
-
// UPGRADE DAY, and without this it is a mystery. A deck older than the attach
|
|
1441
|
-
// publishes no `claude` and no `version` in its discovery record, so sameShape
|
|
1442
|
-
// cannot match it — deliberately, because a record that cannot be compared is
|
|
1443
|
-
// not one to attach to. The consequence is that the first `ccdeck` after an
|
|
1444
|
-
// upgrade starts a SECOND deck beside the one already running, opens a tab on
|
|
1445
|
-
// it, and says nothing; and `ccdeck --stop` will not find the old one either,
|
|
1446
|
-
// because it has the same shape problem.
|
|
1447
|
-
//
|
|
1448
|
-
// So it is named, once, with the command that actually clears it. Only for
|
|
1449
|
-
// decks with no version: a deck started with `--new`, or one scoped to another
|
|
1450
|
-
// workspace, is somebody's deliberate second deck and needs no explaining.
|
|
1451
|
-
if (!RESPAWN) {
|
|
1452
|
-
try {
|
|
1453
|
-
const { liveDecks } = await import(pathToFileURL(join(PKG_ROOT, "src/server/running-deck.mjs")).href);
|
|
1454
|
-
const older = (await liveDecks()).filter(d => !d.version);
|
|
1455
|
-
if (older.length) {
|
|
1456
|
-
const where = older.map(d => d.port).join(", ");
|
|
1457
|
-
write(` ${P.warn}${G.warn}${P.reset} ${P.muted}${older.length === 1 ? "a deck" : `${older.length} decks`} from an older ${PRODUCT} ${older.length === 1 ? "is" : "are"} still running on ${where}${P.reset}\n`);
|
|
1458
|
-
write(` ${P.muted}too old to be recognised, so this one started beside ${older.length === 1 ? "it" : "them"} ${G.dash} \`${INVOKED_AS ?? PRODUCT} --stop --all\` clears the lot${P.reset}\n\n`);
|
|
1459
|
-
}
|
|
1460
|
-
} catch { /* a question about other decks is never a reason to fail a boot */ }
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
1483
|
// ── starting at login ─────────────────────────────────────────────────────────
|
|
1464
1484
|
//
|
|
1465
1485
|
// ONCE PER MACHINE, EVER. The record in the deck's own data directory is what
|
|
@@ -1487,6 +1507,9 @@ if (!RESPAWN) {
|
|
|
1487
1507
|
script: join(PKG_ROOT, "bin", "agent-dag.js"),
|
|
1488
1508
|
logPath: join(deckLogDir(), "deck.log"),
|
|
1489
1509
|
product: PRODUCT,
|
|
1510
|
+
// The directories that decide WHICH deck this is, so the one started at
|
|
1511
|
+
// login is the one started from this shell — see scopeEnv.
|
|
1512
|
+
serviceEnv: svc.scopeEnv(process.env),
|
|
1490
1513
|
});
|
|
1491
1514
|
svc.writeServiceRecord(deckDataDir(), out.ok
|
|
1492
1515
|
? { installed: PKG_VERSION, at: new Date().toISOString(), path: out.path }
|
|
@@ -1673,6 +1696,11 @@ async function shutdown(code = 0) {
|
|
|
1673
1696
|
process.on("SIGINT", () => shutdown(0));
|
|
1674
1697
|
process.on("SIGTERM", () => shutdown(0));
|
|
1675
1698
|
process.on("beforeExit", () => { discovery?.stop(); if (discoveryFile) removeDiscovery(discoveryFile); });
|
|
1699
|
+
// Every way out, including the attach and the boot that fails before it
|
|
1700
|
+
// registers: a lock left behind would hold the next start for thirty seconds
|
|
1701
|
+
// before it could judge the holder gone. Synchronous, because `exit` allows
|
|
1702
|
+
// nothing else, and a no-op once the lock has been given back.
|
|
1703
|
+
process.on("exit", () => { bootLock?.release(); });
|
|
1676
1704
|
|
|
1677
1705
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
1678
1706
|
|
|
@@ -1762,11 +1790,11 @@ Options:
|
|
|
1762
1790
|
--no-open Don't open the browser automatically
|
|
1763
1791
|
--foreground Hold the terminal, the way every version before 3.20
|
|
1764
1792
|
did. Ctrl+C stops the deck again
|
|
1765
|
-
--new
|
|
1793
|
+
--new Replace the running deck with a fresh one.
|
|
1766
1794
|
Without it, a bare \`${PRODUCT}\` beside a deck that is
|
|
1767
1795
|
already up opens that deck's tab instead of building
|
|
1768
1796
|
a rival on another port
|
|
1769
|
-
--stop Stop the deck
|
|
1797
|
+
--stop Stop the running deck.
|
|
1770
1798
|
With --port <n>, stop that one; with --all, stop every
|
|
1771
1799
|
deck on this machine
|
|
1772
1800
|
--status What is running on this machine, and on which ports
|