ccdeck 3.29.10 → 3.30.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.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/bin/agent-dag.js +67 -118
  3. package/bin/cli/help.js +67 -0
  4. package/bin/cli/login-item.js +182 -0
  5. package/bin/cli/one-shot.js +254 -0
  6. package/bin/cli/package.js +39 -0
  7. package/bin/cli/pulse.js +98 -0
  8. package/bin/cli/restart.js +179 -0
  9. package/bin/cli/screen.js +166 -0
  10. package/bin/cli/second-start.js +120 -0
  11. package/bin/cli/startup.js +459 -0
  12. package/bin/cli/uninstall.js +169 -0
  13. package/bin/deck.js +112 -1559
  14. package/dist/web/assets/BrowserWatchModal-DP3YMtEi.js +1 -0
  15. package/dist/web/assets/UsageHistoryModal-CcdPVXAw.js +1 -0
  16. package/dist/web/assets/index-3vS6A0vt.css +1 -0
  17. package/dist/web/assets/index-BVTWHDta.js +298 -0
  18. package/dist/web/index.html +2 -2
  19. package/hook/hook.js +15 -14
  20. package/package.json +1 -1
  21. package/release-notes.json +28 -0
  22. package/src/server/account-projects.mjs +25 -11
  23. package/src/server/account-routes.mjs +254 -0
  24. package/src/server/activity.mjs +3 -3
  25. package/src/server/append-budget.mjs +185 -0
  26. package/src/server/append-failures.mjs +147 -0
  27. package/src/server/args.mjs +37 -5
  28. package/src/server/atomic-write.mjs +300 -0
  29. package/src/server/block-notify.mjs +0 -5
  30. package/src/server/boot-lock.mjs +1 -1
  31. package/src/server/brand.mjs +7 -6
  32. package/src/server/browser-react.mjs +10 -2
  33. package/src/server/browser-watch-decks.mjs +146 -0
  34. package/src/server/browser-watch-feed.mjs +69 -0
  35. package/src/server/browser-watch-log.mjs +173 -0
  36. package/src/server/browser-watch-mtime.mjs +18 -0
  37. package/src/server/browser-watch-relay.mjs +112 -0
  38. package/src/server/browser-watch-routes.mjs +172 -0
  39. package/src/server/browser-watch-seen.mjs +127 -0
  40. package/src/server/browser-watch-store.mjs +14 -143
  41. package/src/server/browser-watch.mjs +278 -626
  42. package/src/server/canonical-path.mjs +150 -0
  43. package/src/server/ccusage-failure.mjs +64 -0
  44. package/src/server/ccusage-install.mjs +459 -0
  45. package/src/server/ccusage-runner.mjs +271 -0
  46. package/src/server/ccusage.mjs +21 -756
  47. package/src/server/claude-accounts.mjs +220 -464
  48. package/src/server/claude-collector.mjs +189 -0
  49. package/src/server/claude-dir.mjs +2 -2
  50. package/src/server/claude-identity.mjs +5 -4
  51. package/src/server/claude-reset-credits.mjs +130 -0
  52. package/src/server/claude-verdicts.mjs +174 -0
  53. package/src/server/clear-route.mjs +191 -0
  54. package/src/server/codex-auth.mjs +4 -4
  55. package/src/server/codex-dir.mjs +52 -8
  56. package/src/server/codex-enrichment.mjs +310 -0
  57. package/src/server/codex-quota.mjs +5 -3
  58. package/src/server/codex-translate.mjs +407 -0
  59. package/src/server/codex-usage.mjs +8 -5
  60. package/src/server/codex-watch.mjs +496 -0
  61. package/src/server/cswap-auto-loop.mjs +342 -0
  62. package/src/server/cswap-auto-readers.mjs +303 -0
  63. package/src/server/cswap-auto.mjs +51 -609
  64. package/src/server/cswap-install.mjs +69 -265
  65. package/src/server/cswap-layout.mjs +242 -0
  66. package/src/server/deck-home.mjs +1 -1
  67. package/src/server/deck-prefs.mjs +51 -184
  68. package/src/server/deck-scope.mjs +52 -0
  69. package/src/server/desktop-update-routes.mjs +75 -0
  70. package/src/server/detach.mjs +78 -0
  71. package/src/server/discovery.mjs +298 -0
  72. package/src/server/event-log.mjs +328 -0
  73. package/src/server/event-pipeline.mjs +235 -0
  74. package/src/server/event-ring.mjs +214 -0
  75. package/src/server/event-routes.mjs +352 -0
  76. package/src/server/event-sink.mjs +26 -0
  77. package/src/server/exec-children.mjs +128 -0
  78. package/src/server/exec-not-found.mjs +213 -0
  79. package/src/server/exec-output.mjs +23 -0
  80. package/src/server/exec-spec.mjs +341 -0
  81. package/src/server/exec.mjs +74 -661
  82. package/src/server/health-route.mjs +42 -0
  83. package/src/server/hook-challenge.mjs +37 -0
  84. package/src/server/http-io.mjs +104 -0
  85. package/src/server/index.mjs +172 -7620
  86. package/src/server/install-layout.mjs +506 -0
  87. package/src/server/installer.mjs +79 -608
  88. package/src/server/jsonl-chunks.mjs +139 -0
  89. package/src/server/lan-accounts.mjs +27 -0
  90. package/src/server/lan-beacon.mjs +457 -0
  91. package/src/server/lan-call.mjs +239 -0
  92. package/src/server/lan-copies.mjs +270 -0
  93. package/src/server/lan-deck.mjs +583 -0
  94. package/src/server/lan-dials.mjs +161 -0
  95. package/src/server/lan-engine.mjs +475 -1024
  96. package/src/server/lan-hearing.mjs +49 -0
  97. package/src/server/lan-inbound.mjs +100 -0
  98. package/src/server/lan-invite-offer.mjs +77 -0
  99. package/src/server/lan-invite.mjs +207 -0
  100. package/src/server/lan-lines.mjs +73 -0
  101. package/src/server/lan-manifest.mjs +74 -0
  102. package/src/server/lan-reach.mjs +82 -69
  103. package/src/server/lan-requests.mjs +249 -0
  104. package/src/server/lan-round-record.mjs +45 -0
  105. package/src/server/lan-round-timer.mjs +62 -0
  106. package/src/server/lan-routes.mjs +143 -0
  107. package/src/server/lan-serve.mjs +112 -0
  108. package/src/server/lan-socket.mjs +243 -925
  109. package/src/server/lan-sync.mjs +58 -1026
  110. package/src/server/lan-tailnet-poll.mjs +57 -0
  111. package/src/server/lan-turns.mjs +80 -0
  112. package/src/server/lan-wire.mjs +620 -0
  113. package/src/server/lifecycle.mjs +421 -0
  114. package/src/server/listen.mjs +110 -0
  115. package/src/server/live-decks.mjs +138 -0
  116. package/src/server/load-average.mjs +32 -0
  117. package/src/server/log-election.mjs +235 -0
  118. package/src/server/log-replay.mjs +374 -0
  119. package/src/server/log-writer.mjs +72 -557
  120. package/src/server/login-service.mjs +3 -2
  121. package/src/server/macmon.mjs +1 -1
  122. package/src/server/memory-files.mjs +163 -0
  123. package/src/server/memory-metrics.mjs +178 -0
  124. package/src/server/metrics-history.mjs +94 -0
  125. package/src/server/metrics-run.mjs +100 -0
  126. package/src/server/music-routes.mjs +134 -0
  127. package/src/server/network-metrics.mjs +5 -3
  128. package/src/server/network-sampler.mjs +256 -0
  129. package/src/server/npm-latest.mjs +385 -0
  130. package/src/server/npm-upgrade.mjs +352 -0
  131. package/src/server/npx.mjs +41 -7
  132. package/src/server/output-watch.mjs +5 -4
  133. package/src/server/pinned-build.mjs +108 -0
  134. package/src/server/prefs-path.mjs +14 -0
  135. package/src/server/prefs-refusal.mjs +77 -0
  136. package/src/server/prefs-routes.mjs +76 -0
  137. package/src/server/prefs-set-aside.mjs +86 -0
  138. package/src/server/prefs-state.mjs +51 -0
  139. package/src/server/process-command.mjs +104 -0
  140. package/src/server/process-list.mjs +547 -0
  141. package/src/server/pulse-line.mjs +132 -0
  142. package/src/server/purge-key.mjs +7 -7
  143. package/src/server/quota-cli.mjs +174 -0
  144. package/src/server/quota-oauth.mjs +190 -0
  145. package/src/server/quota-shape.mjs +229 -0
  146. package/src/server/quota-store-resets.mjs +206 -0
  147. package/src/server/quota.mjs +123 -509
  148. package/src/server/relay-guard.mjs +3 -3
  149. package/src/server/request-gates.mjs +441 -0
  150. package/src/server/restart-note.mjs +180 -0
  151. package/src/server/retire-sound-hook.mjs +3 -3
  152. package/src/server/ring-bounds.mjs +215 -0
  153. package/src/server/running-deck.mjs +1 -1
  154. package/src/server/running-version.mjs +24 -0
  155. package/src/server/self-update.mjs +39 -1371
  156. package/src/server/session-enrichment.mjs +622 -0
  157. package/src/server/session-read-gate.mjs +49 -0
  158. package/src/server/session-tracking.mjs +242 -0
  159. package/src/server/sse-clients.mjs +222 -0
  160. package/src/server/stale-discovery.mjs +30 -0
  161. package/src/server/static-serve.mjs +81 -0
  162. package/src/server/supervisor.mjs +73 -12
  163. package/src/server/system-metrics.mjs +169 -1935
  164. package/src/server/term.mjs +11 -249
  165. package/src/server/thermal-metrics.mjs +520 -0
  166. package/src/server/thermal-sampler.mjs +204 -0
  167. package/src/server/token-redact.mjs +123 -0
  168. package/src/server/transcript-gate.mjs +107 -0
  169. package/src/server/transcript-scan.mjs +563 -0
  170. package/src/server/usage-routes.mjs +93 -0
  171. package/src/server/uv-bootstrap.mjs +1 -1
  172. package/src/server/wordmark.mjs +127 -0
  173. package/dist/web/assets/BrowserWatchModal-CMn28FHb.js +0 -1
  174. package/dist/web/assets/UsageHistoryModal-C-sPhsKT.js +0 -1
  175. package/dist/web/assets/index-C1YFI9f4.js +0 -298
  176. package/dist/web/assets/index-Ck2RAla3.css +0 -1
@@ -0,0 +1,166 @@
1
+ // The boot's terminal: the palette, the glyphs, the cursor, the status rows,
2
+ // the wordmark and the spinner — everything bin/deck.js draws with.
3
+ //
4
+ // Lifted out of deck.js, where it was a block in the middle of the boot. The
5
+ // answers are the same ones asked of the same environment, only asked when this
6
+ // module loads instead of halfway down that file; nothing here has a side effect
7
+ // until the boot calls takeCursor, at the point where the block used to hide it.
8
+ import { pathToFileURL } from "node:url";
9
+ import { dieOfSignal } from "../../src/server/supervisor.mjs";
10
+ import {
11
+ CURSOR_HIDE, CURSOR_SHOW, colorProfile, elapsedSuffix, fit, glyphs, labelColumn, link, motionOK,
12
+ palette, spinnerFrames, statusLine, supportsHyperlinks, termColumns, unicodeOK, visibleWidth,
13
+ } from "../../src/server/term.mjs";
14
+ import { wordmark } from "../../src/server/wordmark.mjs";
15
+ import { PKG_VERSION } from "./package.js";
16
+
17
+ // ── the terminal we are printing into ─────────────────────────────────────────
18
+ // Asked once, degraded from there — see src/server/term.mjs, which is where all
19
+ // of this is decided and asserted. Below this point the deck writes no escape of
20
+ // its own: colour comes from `P`, glyphs from `G`, layout from statusLine. That
21
+ // is what makes NO_COLOR, a pipe, a CI log and a legacy Windows console one
22
+ // question rather than thirty separate ones nobody remembers to ask.
23
+ export const tty = Boolean(process.stdout.isTTY);
24
+ export const PROFILE = colorProfile({ isTTY: tty });
25
+ export const P = palette(PROFILE);
26
+ export const UNICODE = unicodeOK();
27
+ export const G = glyphs(UNICODE);
28
+ export const LINKS = supportsHyperlinks({ profile: PROFILE });
29
+ // The terminal's prefers-reduced-motion: nothing sleeps, spins or repaints in a
30
+ // pipe, under CI, or with NO_COLOR set.
31
+ export const MOTION = motionOK({ isTTY: tty, profile: PROFILE });
32
+ export const write = (s) => process.stdout.write(s);
33
+ // Read per line, never cached: a terminal can be resized while the deck runs,
34
+ // and the pulse below is still on screen hours later.
35
+ export const cols = () => termColumns(process.stdout);
36
+ export const sleep = ms => new Promise(r => setTimeout(r, ms));
37
+ export const fileLink = (path) => link(path, pathToFileURL(path).href, LINKS);
38
+
39
+ // ── the cursor ────────────────────────────────────────────────────────────────
40
+ // Hidden for as long as anything of ours is moving — the reveal, the spinner,
41
+ // the pulse — and put back on every way out of this process: the ordinary exit,
42
+ // all three signals, and an uncaught throw, which reaches 'exit' after Node has
43
+ // printed it. Half of this is worse than none: a deck that dies with the cursor
44
+ // hidden leaves the user's shell with no cursor and nothing to do about it but
45
+ // `reset`.
46
+ let cursorHidden = false;
47
+ export const showCursor = () => {
48
+ if (!cursorHidden) return;
49
+ cursorHidden = false;
50
+ try { write(CURSOR_SHOW); } catch { /* stdout is gone; nothing left to restore */ }
51
+ };
52
+ /**
53
+ * Hide the cursor, if anything is going to move, and arm every way of putting
54
+ * it back. Called once, by the boot, at the point it starts drawing.
55
+ */
56
+ export function takeCursor() {
57
+ if (MOTION) { cursorHidden = true; write(CURSOR_HIDE); }
58
+ process.on("exit", showCursor);
59
+ // SIGHUP is the one signal this process does not otherwise handle, so its
60
+ // default action would end us before 'exit' could run. Handled only to put the
61
+ // cursor back and then die of it exactly as before — the supervisor reads the
62
+ // signal, not an exit code.
63
+ process.on("SIGHUP", () => { showCursor(); dieOfSignal("SIGHUP"); });
64
+ }
65
+
66
+ // ── rows ──────────────────────────────────────────────────────────────────────
67
+ // The status column is computed from the longest label. It used to be counted
68
+ // into each string as trailing spaces, so any new row, or any label a character
69
+ // longer, silently broke the alignment of every other one.
70
+ const LABELS = [
71
+ "workspace", "Claude hooks", "Codex sessions", "Codex hooks", "claude-swap", "accounts",
72
+ "ccusage", "update", "name", "server ready", "log", "unknown option",
73
+ "missing value",
74
+ ];
75
+ const LABEL_W = labelColumn(LABELS);
76
+
77
+ export function row({ mark = " ", tone = P.ok, label = "", detail = "", detailTone = P.muted, keep = false }) {
78
+ return statusLine({
79
+ mark, label, detail, keep, labelWidth: LABEL_W, columns: cols(), ellipsis: G.ellipsis,
80
+ paint: {
81
+ mark: (s) => `${tone}${s}${P.reset}`,
82
+ detail: (s) => `${detailTone}${s}${P.reset}`,
83
+ },
84
+ }) + "\n";
85
+ }
86
+
87
+ // ── the wordmark ──────────────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Hold anything else that wants to speak until the art is finished.
91
+ *
92
+ * #742, found while watching a first run through a pty: the startup jobs run
93
+ * UNDER the reveal on purpose, and one of them failing writes to console.error
94
+ * the moment it fails — which put
95
+ *
96
+ * ccdeck ccusage: install failed: npm install ccusage failed: spawn npm ENOENT
97
+ *
98
+ * between the second and third rows of the logo. A wordmark with a stack of
99
+ * someone else's bad news through the middle of it is the first thing a new
100
+ * user sees, and it reads as a crash rather than as a note.
101
+ *
102
+ * The window is the reveal and nothing else — about 180ms — so at worst a
103
+ * message arrives a fifth of a second later than it would have, on the one
104
+ * stretch of the boot where there is nowhere for it to go. Restored in a
105
+ * `finally`, so a throw inside the reveal cannot leave the process mute.
106
+ */
107
+ function holdConsole() {
108
+ const held = [];
109
+ const real = { warn: console.warn, error: console.error, log: console.log };
110
+ for (const k of Object.keys(real)) console[k] = (...args) => { held.push([k, args]); };
111
+ return () => {
112
+ Object.assign(console, real);
113
+ for (const [k, args] of held) real[k](...args);
114
+ };
115
+ }
116
+
117
+ export async function printBanner() {
118
+ const { lines } = wordmark({ columns: cols(), version: PKG_VERSION, profile: PROFILE, unicode: UNICODE, pal: P });
119
+ const release = holdConsole();
120
+ try {
121
+ for (const line of lines) {
122
+ write(line + "\n");
123
+ // A reveal, not a wait. The once-per-session work is already running under
124
+ // it (see startupWork), so the art costs the boot nothing and the deck is
125
+ // ready about when the last row lands. What used to be here — 560ms of
126
+ // spinner at "loading…" before a single art line — was dead time in a tool
127
+ // whose documented entry point is `npx ccdeck`.
128
+ if (MOTION && line) await sleep(45);
129
+ }
130
+ } finally {
131
+ release();
132
+ }
133
+ }
134
+
135
+ // ── a step, with a spinner only if it is slow enough to need one ──────────────
136
+ // The interval's first frame is 80ms away, so anything already settled when we
137
+ // get here paints nothing at all and the row below is the only trace of it.
138
+
139
+ export async function step(label, work) {
140
+ if (!MOTION) return work;
141
+ const frames = spinnerFrames(UNICODE);
142
+ // Kept inside the terminal: a label that wraps is a label the \r below can
143
+ // only half erase, and what is left of it stays under the row that follows.
144
+ // Six columns for the indent and the spinner, four more so the elapsed
145
+ // seconds have somewhere to go without pushing the label off the edge.
146
+ const text = fit(label, cols() - 10, G.ellipsis);
147
+ const started = Date.now();
148
+ let i = 0;
149
+ let widest = 0;
150
+ const iv = setInterval(() => {
151
+ const line = ` ${P.accent}${frames[i++ % frames.length]}${P.reset} ${P.muted}${text}${elapsedSuffix(Date.now() - started)}${P.reset}`;
152
+ widest = Math.max(widest, visibleWidth(line));
153
+ write(`\r${line}`);
154
+ }, 80);
155
+ try {
156
+ return await work;
157
+ } finally {
158
+ clearInterval(iv);
159
+ // Cleared rather than overwritten: the row that follows is a different
160
+ // length, and relying on it to be the longer of the two is how a spinner
161
+ // leaves its own tail on screen. Measured rather than computed, because the
162
+ // line grows when the elapsed seconds appear and again when they reach two
163
+ // digits. Nothing to clear if it never painted.
164
+ if (i) write("\r" + " ".repeat(widest) + "\r");
165
+ }
166
+ }
@@ -0,0 +1,120 @@
1
+ // A second start, settled: attach to the deck that is already up, yield to one
2
+ // that came up while this one was coming back, or stop what this start
3
+ // replaces and go on.
4
+ //
5
+ // A bare `ccdeck` typed beside a deck that is already running used to build a
6
+ // second everything on a random port, and neither half mentioned the other.
7
+ // Then it attached only to a deck of exactly its own shape and built a second
8
+ // one beside anything else — a different flag, an older version, or a login
9
+ // item whose environment decided `codex` or the log path differently from the
10
+ // shell's. src/server/running-deck.mjs carries the whole argument, the registry
11
+ // read, the handshake and the rule; this is where the answer is acted on, and
12
+ // bin/deck.js is where — and when — it is asked.
13
+ //
14
+ // A RESPAWN IS ASKED TOO, and answers differently. A restart is THIS deck
15
+ // coming back, so there is nothing to attach to — but in the gap a crash
16
+ // leaves, a `ccdeck` typed by hand finds no deck and starts one, and the
17
+ // respawn that followed used to take a random port beside it. Now it finds
18
+ // that deck and exits 0, and the supervisor, reading a clean exit, ends too.
19
+ import { join } from "node:path";
20
+ import { pathToFileURL } from "node:url";
21
+ import { isPortValue } from "../../src/server/args.mjs";
22
+ import { PRODUCT } from "../../src/server/brand.mjs";
23
+ import { liveDecks, olderVersion, secondStart, versionNote } from "../../src/server/running-deck.mjs";
24
+ import { link } from "../../src/server/term.mjs";
25
+ import { COMMAND, PKG_ROOT, PKG_VERSION } from "./package.js";
26
+ import { G, LINKS, P, sleep, write } from "./screen.js";
27
+ import { reportIncompleteFlags, reportUnknownFlags } from "./startup.js";
28
+
29
+ /**
30
+ * Ask the rule, and do what it says. Resolves to the exit code when this start
31
+ * ends here — a yield, or an attach — having said why, and to null when it is
32
+ * to go on and boot. Nothing here exits the process: bin/deck.js does, so every
33
+ * way out is one its exit handlers already cover (#980).
34
+ */
35
+ export async function settleSecondStart({ flags, workspace, persist, wantCodex, wantClaude, codexHome, openBrowser, RESPAWN }) {
36
+ // `--port` alone, not AGENT_DAG_PORT: the variable is how somebody RUNS a
37
+ // deck rather than which one they mean — the line `--stop` draws in
38
+ // bin/cli/one-shot.js.
39
+ const askedPort = flags.port != null && isPortValue(flags.port) ? Number(flags.port) : null;
40
+ const plan = secondStart({
41
+ live: await liveDecks().catch(() => []),
42
+ want: { workspace, persist, codex: wantCodex, claude: wantClaude, codexHome },
43
+ port: askedPort,
44
+ ours: PKG_VERSION,
45
+ fresh: flags.new === true,
46
+ respawn: RESPAWN,
47
+ });
48
+ if (plan.act === "yield") {
49
+ console.error(`${PRODUCT}: a deck started on ${plan.deck.port} while this one was coming back ${G.dash} leaving it to that one.`);
50
+ return 0;
51
+ }
52
+ // THE NEWEST START WINS. What is stopped here is either the deck this start
53
+ // replaces or, on an attach, a second deck left over from before this rule —
54
+ // the duplicate the rule exists to end. Each one is said, with why, because a
55
+ // deck that vanishes without a word is a mystery of its own.
56
+ if (plan.stop.length) {
57
+ const { stopDeck } = await import(pathToFileURL(join(PKG_ROOT, "src/server/stop-deck.mjs")).href);
58
+ for (const d of plan.stop) {
59
+ const why = plan.act === "replace" && flags.new === true
60
+ ? "you asked for a fresh one"
61
+ : olderVersion(d.version, PKG_VERSION)
62
+ ? (d.version ? `it was v${d.version}` : "it was an older version")
63
+ : plan.act === "attach" ? "it was a second deck" : "it was started with different settings";
64
+ const out = await stopDeck(d).catch(() => ({ ok: false, reason: "unreachable" }));
65
+ write(out.ok
66
+ ? `\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`
67
+ : `\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`);
68
+ }
69
+ }
70
+ if (plan.act === "attach") {
71
+ const live = plan.deck;
72
+ const liveUrl = `http://127.0.0.1:${live.port}`;
73
+ const note = versionNote(live.version, PKG_VERSION);
74
+ // Not the startup report's rows. That report has a label column because it
75
+ // has twelve lines to align; this has two, and borrowing the column would
76
+ // indent a three-line message behind a gutter sized for "Codex sessions".
77
+ //
78
+ // The version chunk is dropped rather than printed as "v?" when the running
79
+ // deck is too old to report one — see versionNote, which says nothing in the
80
+ // same case for the same reason.
81
+ const ident = [live.version ? `v${live.version}` : "", `pid ${live.pid}`]
82
+ .filter(Boolean).join(` ${G.bullet} `);
83
+ write(`\n ${P.ok}${G.ok}${P.reset} deck already running${P.muted} ${G.bullet} ${ident}${P.reset}\n`);
84
+ // Its own line, always. The URL is the one detail an ellipsis would destroy
85
+ // — half an address is not a shorter address — and this message has no
86
+ // report to hand it to. Same rule as statusLine's `keep`.
87
+ write(` ${P.accent}${P.bold}${link(liveUrl, liveUrl, LINKS)}${P.reset}\n`);
88
+ if (note) write(` ${P.warn}${G.warn} ${note}${P.reset}\n`);
89
+ // THE TYPO'S WARNING, on the path that has no startup report to carry it.
90
+ //
91
+ // The gate used to answer `true` for these two so the report would
92
+ // run and print them, and that is how `ccdeck --stpo` — a misspelling of
93
+ // the flag that STOPS a deck — came to build a second one. The warning was
94
+ // the requirement; the extra process never was. Printed here, in the same
95
+ // rows the report uses, so nothing is lost and nothing is started.
96
+ reportUnknownFlags(flags.unknown);
97
+ reportIncompleteFlags(flags.incomplete);
98
+ // The line that says a second deck was NOT started. Without it the command
99
+ // looks like it did nothing at all, which is the other way to be confusing
100
+ // about this — and it names the flag for the person who wanted a fresh one.
101
+ write(`\n ${P.muted}${G.dash} no second deck was started ${G.dash} \`${COMMAND} --new\` replaces it with a fresh one${P.reset}\n`);
102
+ if (openBrowser) {
103
+ write(`\n ${P.ok}${P.bold}${G.play} opening browser${G.ellipsis}${P.reset}\n\n`);
104
+ try {
105
+ const { openUrl, LAUNCH_GRACE_MS } = await import(pathToFileURL(join(PKG_ROOT, "src/server/open-url.mjs")).href);
106
+ openUrl(liveUrl);
107
+ // Held for exactly as long as openUrl needs to fall through to its next
108
+ // launcher. Every child it spawns is unref'd, so an immediate exit ends
109
+ // this process before a missing xdg-open has been answered by gio — and
110
+ // then no browser opens and nothing says why. The boot path never had
111
+ // to think about this because it stays alive forever.
112
+ await sleep(LAUNCH_GRACE_MS);
113
+ } catch { /* the URL is on screen; it can be clicked or pasted */ }
114
+ } else {
115
+ write("\n");
116
+ }
117
+ return 0;
118
+ }
119
+ return null;
120
+ }