ccdeck 3.29.11 → 3.31.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 +4 -3
- package/bin/agent-dag.js +67 -118
- package/bin/cli/help.js +67 -0
- package/bin/cli/login-item.js +182 -0
- package/bin/cli/one-shot.js +254 -0
- package/bin/cli/package.js +39 -0
- package/bin/cli/pulse.js +98 -0
- package/bin/cli/restart.js +179 -0
- package/bin/cli/screen.js +166 -0
- package/bin/cli/second-start.js +120 -0
- package/bin/cli/startup.js +459 -0
- package/bin/cli/uninstall.js +169 -0
- package/bin/deck.js +112 -1559
- package/dist/web/assets/BrowserWatchModal-n63GjLhv.js +1 -0
- package/dist/web/assets/UsageHistoryModal-DEwBX4Fk.js +1 -0
- package/dist/web/assets/index-CZJf1Cpp.js +299 -0
- package/dist/web/assets/index-D4q1ElXU.css +1 -0
- package/dist/web/index.html +2 -2
- package/hook/hook.js +15 -14
- package/package.json +1 -1
- package/release-notes.json +40 -0
- package/src/server/account-projects.mjs +25 -11
- package/src/server/account-routes.mjs +254 -0
- package/src/server/activity.mjs +3 -3
- package/src/server/append-budget.mjs +185 -0
- package/src/server/append-failures.mjs +147 -0
- package/src/server/args.mjs +37 -5
- package/src/server/atomic-write.mjs +300 -0
- package/src/server/block-notify.mjs +0 -5
- package/src/server/boot-lock.mjs +1 -1
- package/src/server/brand.mjs +7 -6
- package/src/server/browser-react.mjs +10 -2
- package/src/server/browser-watch-decks.mjs +146 -0
- package/src/server/browser-watch-feed.mjs +69 -0
- package/src/server/browser-watch-log.mjs +173 -0
- package/src/server/browser-watch-mtime.mjs +18 -0
- package/src/server/browser-watch-relay.mjs +112 -0
- package/src/server/browser-watch-routes.mjs +172 -0
- package/src/server/browser-watch-seen.mjs +127 -0
- package/src/server/browser-watch-store.mjs +14 -143
- package/src/server/browser-watch.mjs +278 -626
- package/src/server/canonical-path.mjs +150 -0
- package/src/server/ccusage-failure.mjs +64 -0
- package/src/server/ccusage-install.mjs +459 -0
- package/src/server/ccusage-runner.mjs +271 -0
- package/src/server/ccusage.mjs +21 -756
- package/src/server/claude-accounts.mjs +220 -464
- package/src/server/claude-collector.mjs +189 -0
- package/src/server/claude-dir.mjs +7 -3
- package/src/server/claude-identity.mjs +5 -4
- package/src/server/claude-reset-credits.mjs +130 -0
- package/src/server/claude-verdicts.mjs +174 -0
- package/src/server/clear-route.mjs +191 -0
- package/src/server/codex-auth.mjs +4 -4
- package/src/server/codex-dir.mjs +52 -8
- package/src/server/codex-enrichment.mjs +310 -0
- package/src/server/codex-quota.mjs +5 -3
- package/src/server/codex-translate.mjs +407 -0
- package/src/server/codex-usage.mjs +8 -5
- package/src/server/codex-watch.mjs +496 -0
- package/src/server/cswap-auto-loop.mjs +342 -0
- package/src/server/cswap-auto-readers.mjs +303 -0
- package/src/server/cswap-auto.mjs +51 -609
- package/src/server/cswap-install.mjs +69 -265
- package/src/server/cswap-layout.mjs +242 -0
- package/src/server/deck-home.mjs +2 -2
- package/src/server/deck-prefs.mjs +58 -184
- package/src/server/deck-scope.mjs +52 -0
- package/src/server/desktop-update-routes.mjs +75 -0
- package/src/server/detach.mjs +78 -0
- package/src/server/discovery.mjs +298 -0
- package/src/server/event-log.mjs +328 -0
- package/src/server/event-pipeline.mjs +235 -0
- package/src/server/event-ring.mjs +214 -0
- package/src/server/event-routes.mjs +352 -0
- package/src/server/event-sink.mjs +26 -0
- package/src/server/exec-children.mjs +128 -0
- package/src/server/exec-not-found.mjs +213 -0
- package/src/server/exec-output.mjs +23 -0
- package/src/server/exec-spec.mjs +341 -0
- package/src/server/exec.mjs +74 -661
- package/src/server/health-route.mjs +42 -0
- package/src/server/hook-challenge.mjs +37 -0
- package/src/server/http-io.mjs +104 -0
- package/src/server/index.mjs +174 -7620
- package/src/server/install-layout.mjs +506 -0
- package/src/server/installer.mjs +79 -608
- package/src/server/jsonl-chunks.mjs +139 -0
- package/src/server/lan-accounts.mjs +27 -0
- package/src/server/lan-beacon.mjs +457 -0
- package/src/server/lan-call.mjs +239 -0
- package/src/server/lan-copies.mjs +270 -0
- package/src/server/lan-deck.mjs +583 -0
- package/src/server/lan-dials.mjs +161 -0
- package/src/server/lan-engine.mjs +475 -1024
- package/src/server/lan-hearing.mjs +49 -0
- package/src/server/lan-inbound.mjs +100 -0
- package/src/server/lan-invite-offer.mjs +77 -0
- package/src/server/lan-invite.mjs +207 -0
- package/src/server/lan-lines.mjs +73 -0
- package/src/server/lan-manifest.mjs +74 -0
- package/src/server/lan-reach.mjs +82 -69
- package/src/server/lan-requests.mjs +249 -0
- package/src/server/lan-round-record.mjs +45 -0
- package/src/server/lan-round-timer.mjs +62 -0
- package/src/server/lan-routes.mjs +143 -0
- package/src/server/lan-serve.mjs +112 -0
- package/src/server/lan-socket.mjs +243 -925
- package/src/server/lan-sync.mjs +58 -1026
- package/src/server/lan-tailnet-poll.mjs +57 -0
- package/src/server/lan-turns.mjs +80 -0
- package/src/server/lan-wire.mjs +620 -0
- package/src/server/lifecycle.mjs +421 -0
- package/src/server/listen.mjs +110 -0
- package/src/server/live-decks.mjs +138 -0
- package/src/server/load-average.mjs +32 -0
- package/src/server/log-election.mjs +235 -0
- package/src/server/log-replay.mjs +374 -0
- package/src/server/log-writer.mjs +72 -557
- package/src/server/login-service.mjs +3 -2
- package/src/server/macmon.mjs +1 -1
- package/src/server/memory-files.mjs +163 -0
- package/src/server/memory-metrics.mjs +178 -0
- package/src/server/metrics-history.mjs +94 -0
- package/src/server/metrics-run.mjs +100 -0
- package/src/server/music-routes.mjs +134 -0
- package/src/server/network-metrics.mjs +5 -3
- package/src/server/network-sampler.mjs +256 -0
- package/src/server/npm-latest.mjs +385 -0
- package/src/server/npm-upgrade.mjs +352 -0
- package/src/server/npx.mjs +41 -7
- package/src/server/output-watch.mjs +5 -4
- package/src/server/pinned-build.mjs +110 -0
- package/src/server/prefs-give-back.mjs +206 -0
- package/src/server/prefs-path.mjs +14 -0
- package/src/server/prefs-refusal.mjs +86 -0
- package/src/server/prefs-routes.mjs +99 -0
- package/src/server/prefs-set-aside.mjs +86 -0
- package/src/server/prefs-state.mjs +70 -0
- package/src/server/process-command.mjs +104 -0
- package/src/server/process-list.mjs +547 -0
- package/src/server/provider-status.mjs +307 -0
- package/src/server/pulse-line.mjs +132 -0
- package/src/server/purge-key.mjs +7 -7
- package/src/server/quota-cli.mjs +174 -0
- package/src/server/quota-oauth.mjs +190 -0
- package/src/server/quota-shape.mjs +229 -0
- package/src/server/quota-store-resets.mjs +206 -0
- package/src/server/quota.mjs +123 -509
- package/src/server/relay-guard.mjs +3 -3
- package/src/server/request-gates.mjs +441 -0
- package/src/server/restart-note.mjs +180 -0
- package/src/server/retire-sound-hook.mjs +3 -3
- package/src/server/ring-bounds.mjs +215 -0
- package/src/server/running-deck.mjs +1 -1
- package/src/server/running-version.mjs +24 -0
- package/src/server/self-update.mjs +39 -1371
- package/src/server/session-enrichment.mjs +622 -0
- package/src/server/session-read-gate.mjs +49 -0
- package/src/server/session-tracking.mjs +242 -0
- package/src/server/sse-clients.mjs +222 -0
- package/src/server/stale-discovery.mjs +30 -0
- package/src/server/static-serve.mjs +81 -0
- package/src/server/supervisor.mjs +73 -12
- package/src/server/system-metrics.mjs +169 -1935
- package/src/server/term.mjs +11 -249
- package/src/server/thermal-metrics.mjs +520 -0
- package/src/server/thermal-sampler.mjs +204 -0
- package/src/server/token-redact.mjs +123 -0
- package/src/server/transcript-gate.mjs +107 -0
- package/src/server/transcript-scan.mjs +563 -0
- package/src/server/usage-routes.mjs +108 -0
- package/src/server/uv-bootstrap.mjs +1 -1
- package/src/server/wordmark.mjs +127 -0
- package/dist/web/assets/BrowserWatchModal-CyAFTvAY.js +0 -1
- package/dist/web/assets/UsageHistoryModal-KJfli0oa.js +0 -1
- package/dist/web/assets/index-Ck2RAla3.css +0 -1
- package/dist/web/assets/index-jmrcEsCj.js +0 -298
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// The one-shot commands: `--status`, `--logs` and `--stop`, and the way in for
|
|
2
|
+
// the three login-item commands, which bin/cli/login-item.js answers.
|
|
3
|
+
//
|
|
4
|
+
// One-shot commands in the shape `--uninstall` already established: do the
|
|
5
|
+
// thing, print, exit, and never start a server. bin/deck.js asks them ABOVE the
|
|
6
|
+
// migration and well above the heavy imports, because a command that ends a
|
|
7
|
+
// deck has no business moving that deck's files on the way past, and because
|
|
8
|
+
// asking a server to stop should not require starting one.
|
|
9
|
+
//
|
|
10
|
+
// The glyphs and the palette are the boot's own, from bin/cli/screen.js, which
|
|
11
|
+
// answers them when it loads — so these lines can use them even though they run
|
|
12
|
+
// before the boot has drawn anything (#797).
|
|
13
|
+
import { readFileSync as readLog, statSync as statLog } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { isPortValue } from "../../src/server/args.mjs";
|
|
17
|
+
import { sinceLabel } from "../../src/server/term.mjs";
|
|
18
|
+
import { installGlobally, loginItemCommand } from "./login-item.js";
|
|
19
|
+
import { COMMAND, PKG_ROOT } from "./package.js";
|
|
20
|
+
import { G, P } from "./screen.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Answer one of the six one-shot flags and resolve to the exit code. The
|
|
24
|
+
* caller exits with it; nothing here starts a server.
|
|
25
|
+
*/
|
|
26
|
+
export async function oneShot(flags) {
|
|
27
|
+
const voice = oneShotVoice();
|
|
28
|
+
reportFlagRows(flags, voice);
|
|
29
|
+
if (flags.stop && refusesPort(flags, voice)) return 1;
|
|
30
|
+
|
|
31
|
+
const { deckDataDir, deckLogDir } = await import(pathToFileURL(join(PKG_ROOT, "src/server/deck-home.mjs")).href);
|
|
32
|
+
|
|
33
|
+
// --install, --install-service and --uninstall-service: the login item, in
|
|
34
|
+
// bin/cli/login-item.js. Answered before the registry is read, since neither
|
|
35
|
+
// is about a deck that is running.
|
|
36
|
+
if (flags.install) return installGlobally({ ...voice, deckDataDir, deckLogDir });
|
|
37
|
+
if (flags.installService || flags.uninstallService) return loginItemCommand(flags, { ...voice, deckDataDir, deckLogDir });
|
|
38
|
+
if (flags.logs) return printLogs(deckLogDir, voice);
|
|
39
|
+
|
|
40
|
+
const { liveDecks } = await import(pathToFileURL(join(PKG_ROOT, "src/server/running-deck.mjs")).href);
|
|
41
|
+
const decks = await liveDecks().catch(() => []);
|
|
42
|
+
if (flags.status) return printStatus(decks, await defaultShape(deckLogDir), voice);
|
|
43
|
+
return stopDecks(flags, decks, voice);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The screen's glyphs and palette, by the names the one-shots use, and `say`. */
|
|
47
|
+
function oneShotVoice() {
|
|
48
|
+
const { dash, ok: gOk, warn: gWarn, bullet, arrow, ellipsis: gEllipsis } = G;
|
|
49
|
+
const tone = P;
|
|
50
|
+
const say = (line) => process.stdout.write(`${line}\n`);
|
|
51
|
+
return { dash, gOk, gWarn, bullet, arrow, gEllipsis, tone, say };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const age = (d) => sinceLabel(Date.now() - Date.parse(d.startedAt ?? ""));
|
|
55
|
+
const where = (d) => (d.workspace ? d.workspace : "(all)");
|
|
56
|
+
const url = (d) => `http://127.0.0.1:${d.port}`;
|
|
57
|
+
|
|
58
|
+
// WHAT THE ONE-SHOTS USED TO SWALLOW.
|
|
59
|
+
//
|
|
60
|
+
// The boot's reportUnknownFlags and reportIncompleteFlags live in bin/deck.js,
|
|
61
|
+
// which is the script itself and cannot be imported, and they write the boot's
|
|
62
|
+
// label-column rows to stdout. So a one-shot's are written here, to stderr, in
|
|
63
|
+
// the one-shots' own shape.
|
|
64
|
+
//
|
|
65
|
+
// Without them `--help`'s closing promise — "Anything else on the command
|
|
66
|
+
// line is reported as an unknown option and then ignored" — held for a boot
|
|
67
|
+
// and for none of the nine one-shots: `ccdeck --status --prot 4317` printed
|
|
68
|
+
// the status and never mentioned the flag.
|
|
69
|
+
function reportFlagRows(flags, { tone, gWarn, dash }) {
|
|
70
|
+
for (const token of flags.unknown ?? []) {
|
|
71
|
+
process.stderr.write(` ${tone.warn}${gWarn} unknown option${tone.reset} ${token} ${dash} see \`${COMMAND} --help\`\n`);
|
|
72
|
+
}
|
|
73
|
+
for (const { flag, expects } of flags.incomplete ?? []) {
|
|
74
|
+
process.stderr.write(` ${tone.warn}${gWarn} missing value${tone.reset} ${flag} ${dash} expected ${expects}\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// AND `--stop` FAILS CLOSED ON A PORT IT CANNOT USE.
|
|
79
|
+
//
|
|
80
|
+
// The selector below reads "no usable port" and "no port asked for" as the
|
|
81
|
+
// same thing, and the second means every deck — so `ccdeck --stop --port
|
|
82
|
+
// 431x`, or `--port $UNSET`, ended every deck on the machine and exited 0.
|
|
83
|
+
// The guard that refuses a bad port is in bin/deck.js, below the line that
|
|
84
|
+
// brings a one-shot here, and a one-shot always exits before reaching it.
|
|
85
|
+
//
|
|
86
|
+
// A narrowing flag that fails open to "everything" is the wrong default for
|
|
87
|
+
// an off switch: failing closed costs a retype, failing open costs a deck
|
|
88
|
+
// somebody else was watching. So this answers true, having said why, when
|
|
89
|
+
// `--stop` was pointed at a port it cannot use.
|
|
90
|
+
function refusesPort(flags, { dash }) {
|
|
91
|
+
const askedPort = (flags.incomplete ?? []).some(x => x.flag === "--port" || x.flag === "-p");
|
|
92
|
+
const badPort = flags.port != null && !isPortValue(flags.port);
|
|
93
|
+
if (badPort || askedPort) {
|
|
94
|
+
const shown = badPort ? ` ${flags.port}` : "";
|
|
95
|
+
console.error(`${COMMAND}: --port${shown}: not a port number ${dash} expected 0-65535.`);
|
|
96
|
+
console.error(`${COMMAND}: refusing to stop every deck when you asked for one.`);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── --logs ────────────────────────────────────────────────────────────────
|
|
103
|
+
// What the deck wrote where a terminal would have shown it. Printed RAW,
|
|
104
|
+
// escapes and all: the launcher passes its own colour tier down to a detached
|
|
105
|
+
// deck, so this file is a mirror of the terminal the deck was started from
|
|
106
|
+
// and re-rendering it is the whole point. The path goes last, because that is
|
|
107
|
+
// the line somebody copies into `tail -f`.
|
|
108
|
+
function printLogs(deckLogDir, { say, tone, dash, bullet }) {
|
|
109
|
+
const logPath = join(deckLogDir(), "deck.log");
|
|
110
|
+
let text = null;
|
|
111
|
+
try { text = readLog(logPath, "utf8"); } catch { /* never started, or swept */ }
|
|
112
|
+
if (text === null) {
|
|
113
|
+
say(`\n ${tone.muted}${dash} nothing logged yet ${dash} ${logPath}${tone.reset}\n`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
// The tail, not the file. It is truncated at the first start on an idle
|
|
117
|
+
// machine, so it is normally small — but an attach appends to a running
|
|
118
|
+
// deck's log every time, and a long-lived deck's log is somebody's week.
|
|
119
|
+
const lines = text.split("\n");
|
|
120
|
+
const shown = lines.slice(Math.max(0, lines.length - 200));
|
|
121
|
+
if (shown.length < lines.length) {
|
|
122
|
+
say(` ${tone.muted}${dash} showing the last ${shown.length} of ${lines.length} lines${tone.reset}`);
|
|
123
|
+
}
|
|
124
|
+
process.stdout.write(shown.join("\n"));
|
|
125
|
+
if (!text.endsWith("\n")) say("");
|
|
126
|
+
const size = (() => { try { return statLog(logPath).size; } catch { return 0; } })();
|
|
127
|
+
say(` ${tone.muted}${dash} ${logPath} ${bullet} ${size} bytes${tone.reset}\n`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The shape a bare start would have, for the deck `--status` marks as the one
|
|
133
|
+
* it opens.
|
|
134
|
+
*/
|
|
135
|
+
async function defaultShape(deckLogDir) {
|
|
136
|
+
const { canonicalLogPath } = await import(pathToFileURL(join(PKG_ROOT, "src/server/log-election.mjs")).href);
|
|
137
|
+
const { hasCodexInstalled, codexHomeField } = await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
|
|
138
|
+
const { hasClaudeInstalled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/claude-dir.mjs")).href);
|
|
139
|
+
|
|
140
|
+
// The default shape, spelled the same way deck.js's boot spells it. NOT the
|
|
141
|
+
// shape of the flags on THIS command line: `--stop --no-codex` is not a
|
|
142
|
+
// request to stop a Codex-less deck, it is a flag that means nothing here,
|
|
143
|
+
// and reading it as a selector would make `--stop` miss the deck it was
|
|
144
|
+
// pointed at and say "nothing is running".
|
|
145
|
+
const mine = {
|
|
146
|
+
workspace: "",
|
|
147
|
+
persist: canonicalLogPath(join(deckLogDir(), "events.jsonl")),
|
|
148
|
+
codex: hasCodexInstalled(),
|
|
149
|
+
claude: hasClaudeInstalled(),
|
|
150
|
+
};
|
|
151
|
+
// And the Codex tree, the one field of a start's shape that is not a flag. A
|
|
152
|
+
// bare start has passed it since #1123 and replaces a deck on another tree, so
|
|
153
|
+
// without it here every tree matched, and `--status` could mark as "opens this
|
|
154
|
+
// one" exactly the deck the next `ccdeck` stops (#1134). Worked out by the
|
|
155
|
+
// function the start and the discovery record use, so all three agree.
|
|
156
|
+
mine.codexHome = codexHomeField(mine.codex);
|
|
157
|
+
return mine;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** `--status`: every deck on this machine, and which one a bare start would open. */
|
|
161
|
+
async function printStatus(decks, mine, { say, tone, dash, gOk, bullet, arrow }) {
|
|
162
|
+
const { sameShape } = await import(pathToFileURL(join(PKG_ROOT, "src/server/running-deck.mjs")).href);
|
|
163
|
+
if (!decks.length) {
|
|
164
|
+
say(`\n ${tone.muted}${dash} no deck is running ${dash} \`${COMMAND}\` starts one${tone.reset}\n`);
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
// The one a bare `ccdeck` would open is marked, because with two decks up
|
|
168
|
+
// that is the only question this command is really being asked.
|
|
169
|
+
const opens = decks.find(d => sameShape(d, mine)) ?? null;
|
|
170
|
+
say("");
|
|
171
|
+
for (const d of decks) {
|
|
172
|
+
// The version chunk is dropped rather than printed as "v?" for a deck too
|
|
173
|
+
// old to publish one — same rule as the attach line, and for the same
|
|
174
|
+
// reason: a question mark beside two real facts reads as a fault.
|
|
175
|
+
const head = [d.version ? `v${d.version}` : "", `pid ${d.pid}`, `up ${age(d)}`]
|
|
176
|
+
.filter(Boolean).join(` ${bullet} `);
|
|
177
|
+
const mark = d === opens ? `${tone.ok}${gOk}${tone.reset}` : `${tone.muted}${bullet}${tone.reset}`;
|
|
178
|
+
const tail = d === opens ? `${tone.muted} ${arrow} \`${COMMAND}\` opens this one${tone.reset}` : "";
|
|
179
|
+
say(` ${mark} ${tone.muted}${head}${tone.reset}${tail}`);
|
|
180
|
+
say(` ${tone.accent}${tone.bold}${url(d)}${tone.reset}`);
|
|
181
|
+
say(` ${tone.muted}${where(d)} ${bullet} ${d.persist ?? "no log (--no-persist)"}${tone.reset}`);
|
|
182
|
+
}
|
|
183
|
+
say("");
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── --stop ────────────────────────────────────────────────────────────────
|
|
188
|
+
//
|
|
189
|
+
// WHICH DECK. Every one, unless `--port <n>` names one. A start keeps at most
|
|
190
|
+
// one deck now, so a second is a leftover from before that rule, and an off
|
|
191
|
+
// switch that ended one of two would leave the machine running. (`--all` is the
|
|
192
|
+
// legacy capture flag, a no-op since it became the default; beside `--stop` it
|
|
193
|
+
// always meant "every deck", and that is what the bare command means now.)
|
|
194
|
+
async function stopDecks(flags, decks, { say, tone, dash, gOk, gWarn, bullet }) {
|
|
195
|
+
const { stopDeck } = await import(pathToFileURL(join(PKG_ROOT, "src/server/stop-deck.mjs")).href);
|
|
196
|
+
// `flags.port`, not the `rawPort` deck.js computes: that folds in
|
|
197
|
+
// AGENT_DAG_PORT, which is how somebody RUNS a deck rather than which deck
|
|
198
|
+
// they mean to stop — and a deck started on a custom port still has the
|
|
199
|
+
// default SHAPE, so the matcher finds it without help. It is also a binding
|
|
200
|
+
// deck.js declares after it asks this, so it is not there to read (#797).
|
|
201
|
+
const named = flags.port != null && isPortValue(flags.port) ? Number(flags.port) : null;
|
|
202
|
+
// EVERY DECK, unless one is named. There is meant to be one — a start keeps
|
|
203
|
+
// at most one now — so a second here is a leftover from before that rule, and
|
|
204
|
+
// an off switch that ended one of two would leave the machine running. `--all`
|
|
205
|
+
// is still accepted, and now means what the bare command means.
|
|
206
|
+
const wanted = named !== null ? decks.filter(d => d.port === named) : decks;
|
|
207
|
+
|
|
208
|
+
if (!wanted.length) {
|
|
209
|
+
// Two different silences, and saying the wrong one sends the reader looking
|
|
210
|
+
// in the wrong place.
|
|
211
|
+
const why = named !== null && decks.length
|
|
212
|
+
? `no deck is listening on ${named} ${dash} \`${COMMAND} --status\` lists them`
|
|
213
|
+
: `no deck is running`;
|
|
214
|
+
say(`\n ${tone.muted}${dash} ${why}${tone.reset}\n`);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
say("");
|
|
219
|
+
let refused = false;
|
|
220
|
+
for (const d of wanted) {
|
|
221
|
+
const was = age(d);
|
|
222
|
+
const out = await stopDeck(d);
|
|
223
|
+
if (!out.ok) {
|
|
224
|
+
refused = true;
|
|
225
|
+
say(` ${tone.err}${gWarn} could not stop pid ${d.pid} on ${d.port} ${dash} ${out.reason}${tone.reset}`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// HOW it went out, not just that it did. "asked" means the deck closed its
|
|
229
|
+
// listener, unlinked its registration and left the LAN cleanly; anything
|
|
230
|
+
// else means none of that happened and the next boot has litter to sweep.
|
|
231
|
+
const how = out.how === "asked"
|
|
232
|
+
? ""
|
|
233
|
+
: out.old
|
|
234
|
+
? ` ${tone.muted}(${out.how} ${dash} that deck predates \`--stop\`)${tone.reset}`
|
|
235
|
+
: ` ${tone.muted}(${out.how} ${dash} it did not answer)${tone.reset}`;
|
|
236
|
+
say(` ${tone.ok}${gOk}${tone.reset} stopped${tone.muted} ${bullet} pid ${d.pid} ${bullet} port ${d.port} ${bullet} was up ${was}${tone.reset}${how}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// What is still up, named. A command that ends one of three decks and says
|
|
240
|
+
// only "stopped" leaves the reader believing the machine is clear.
|
|
241
|
+
const left = decks.filter(d => !wanted.includes(d));
|
|
242
|
+
if (left.length) {
|
|
243
|
+
say("");
|
|
244
|
+
say(` ${tone.muted}${dash} ${left.length} other deck${left.length === 1 ? "" : "s"} still running:${tone.reset}`);
|
|
245
|
+
for (const d of left) say(` ${tone.muted}pid ${d.pid} ${bullet} ${d.port} ${bullet} ${where(d)}${tone.reset}`);
|
|
246
|
+
// `--stop --all` used to be named here as the way to reach every deck. It
|
|
247
|
+
// is not: `--all` is a parsed no-op, and a bare `--stop` already ends every
|
|
248
|
+
// deck — which is why this line is only reachable after `--stop --port <n>`
|
|
249
|
+
// narrowed one. Naming the narrowing form is the half that is true.
|
|
250
|
+
say(` ${tone.muted}\`${COMMAND} --stop\` ends every deck ${bullet} \`--stop --port <n>\` ends one${tone.reset}`);
|
|
251
|
+
}
|
|
252
|
+
say("");
|
|
253
|
+
return refused ? 1 : 0;
|
|
254
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Which package this worker belongs to, and the name it was started under.
|
|
2
|
+
//
|
|
3
|
+
// Worked out from where THIS file is, so a copy of bin/ in another tree — an npx
|
|
4
|
+
// cache, a package a test assembled by hand — answers for that tree and not for
|
|
5
|
+
// the one it was copied from. Lifted out of bin/deck.js so that every file in
|
|
6
|
+
// bin/cli/ imports the three answers instead of having them handed down.
|
|
7
|
+
import { resolve, dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { PRODUCT } from "../../src/server/brand.mjs";
|
|
11
|
+
import { invokedName } from "../../src/server/invoked-as.mjs";
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
export const PKG_ROOT = resolve(__dirname, "../..");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The version package.json names right now, or null when it cannot be read.
|
|
18
|
+
*
|
|
19
|
+
* Asked twice in a deck's life, and the second answer is only worth having
|
|
20
|
+
* because it can differ from the first: once at boot, for the version this
|
|
21
|
+
* process runs, and again as a restart is about to land, for the version it
|
|
22
|
+
* will land on — whatever an upgrade has installed underneath it since.
|
|
23
|
+
*/
|
|
24
|
+
export function versionOnDisk() {
|
|
25
|
+
try { return JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version ?? null; }
|
|
26
|
+
catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const PKG_VERSION = versionOnDisk() ?? "0.0.0";
|
|
30
|
+
|
|
31
|
+
// The command the user typed, handed down by the supervisor — the worker's own
|
|
32
|
+
// argv[1] is bin/deck.js under every one of the three names. Null when it cannot be
|
|
33
|
+
// proven, and null is the answer that prints nothing.
|
|
34
|
+
export const INVOKED_AS = invokedName({ pkgRoot: PKG_ROOT });
|
|
35
|
+
|
|
36
|
+
// The command a hint tells somebody to type: the one they typed, or ours when
|
|
37
|
+
// that cannot be told (#1501). INVOKED_AS stays null in that case for the one
|
|
38
|
+
// caller that must say nothing rather than guess — the rename notice.
|
|
39
|
+
export const COMMAND = INVOKED_AS ?? PRODUCT;
|
package/bin/cli/pulse.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The pulse line: the last line of the boot's screen, and the only one that can
|
|
2
|
+
// still move once the report is done.
|
|
3
|
+
//
|
|
4
|
+
// Lifted out of bin/deck.js, which starts it at the same point in the boot.
|
|
5
|
+
//
|
|
6
|
+
// The whole line is rewritten each beat rather than just the dot: anything else
|
|
7
|
+
// on this deck that has something to say writes a newline first, and after that
|
|
8
|
+
// the line under the cursor is no longer the one we drew — a partial repaint
|
|
9
|
+
// would leave the message behind and pulse into empty space. Sized to the real
|
|
10
|
+
// terminal, because at 40 columns the old fixed 61-character line wrapped, and
|
|
11
|
+
// from then on \r only ever reached its second row.
|
|
12
|
+
//
|
|
13
|
+
// AND IT STOPS MOVING (#742). The dot alternated green and grey every 800ms for
|
|
14
|
+
// as long as the deck ran, and a blinking indicator beside a status line is the
|
|
15
|
+
// vocabulary of "working on it" — so a boot that finished in a second read as
|
|
16
|
+
// one that never finished, which is what people reported. Motion is now spent
|
|
17
|
+
// on the two states where something really is outstanding, and the frame is
|
|
18
|
+
// compared against what is already on screen so a deck at rest paints once and
|
|
19
|
+
// then leaves the terminal alone. See pulseMoves.
|
|
20
|
+
import { pulseDot, pulseText } from "../../src/server/pulse-line.mjs";
|
|
21
|
+
import { G, MOTION, P, UNICODE, cols, write } from "./screen.js";
|
|
22
|
+
import { busyLabel } from "./startup.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Put the pulse line up, when anything on this terminal is allowed to move.
|
|
26
|
+
*
|
|
27
|
+
* `registered` is asked on every beat rather than passed once: the discovery
|
|
28
|
+
* check changes it for as long as the deck runs, and the line says so.
|
|
29
|
+
*/
|
|
30
|
+
export function startPulse({ registered: isRegistered, wantClaude }) {
|
|
31
|
+
if (!MOTION) return;
|
|
32
|
+
let pi = 0;
|
|
33
|
+
let painted = null;
|
|
34
|
+
// The line is on screen and is the last thing written to this terminal.
|
|
35
|
+
let ours = false;
|
|
36
|
+
// We are the one writing right now, so the guard below leaves us alone.
|
|
37
|
+
let writing = false;
|
|
38
|
+
|
|
39
|
+
// The pulse line's tenancy, enforced rather than agreed.
|
|
40
|
+
//
|
|
41
|
+
// The convention was that anything with something to say writes a newline
|
|
42
|
+
// first, so the pulse's `\r` never lands on somebody else's text. The boot
|
|
43
|
+
// keeps it everywhere. src/server/quota.mjs does not — it calls console.error
|
|
44
|
+
// directly — and a Windows user with no Claude Code sent a screenshot of the
|
|
45
|
+
// result: `listening — Ctrl+C to stop ccdeck quota: claude CLI failed`
|
|
46
|
+
// on one row, three times over, the pulse and the complaint interleaved.
|
|
47
|
+
//
|
|
48
|
+
// An invariant every writer has to remember is one a writer will forget, and
|
|
49
|
+
// the writers here are server modules that know nothing about a terminal. So
|
|
50
|
+
// it is enforced at the stream instead: while our line is the last thing on
|
|
51
|
+
// screen, anything else that speaks gets a newline first, and the memo is
|
|
52
|
+
// dropped so the next beat repaints the line under whatever was said.
|
|
53
|
+
//
|
|
54
|
+
// Both streams, because console.error goes to stderr and lands on the same
|
|
55
|
+
// screen. Only when MOTION is on — with no pulse there is no line to defend,
|
|
56
|
+
// and a piped deck must not have its output rewritten.
|
|
57
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
58
|
+
const real = stream.write.bind(stream);
|
|
59
|
+
stream.write = (chunk, ...rest) => {
|
|
60
|
+
if (!writing && ours) {
|
|
61
|
+
ours = false;
|
|
62
|
+
// Dropped, not kept: the line is no longer where we left it, so the
|
|
63
|
+
// next beat has to draw it again even though the frame is unchanged.
|
|
64
|
+
painted = null;
|
|
65
|
+
// Unless the speaker already did it. Every late message the boot writes
|
|
66
|
+
// opens with one, and two blank lines is its own kind of mess.
|
|
67
|
+
if (!String(chunk).startsWith("\n")) real("\n");
|
|
68
|
+
}
|
|
69
|
+
return real(chunk, ...rest);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setInterval(() => {
|
|
74
|
+
const registered = isRegistered();
|
|
75
|
+
// The colour follows the words. A Codex-only deck keeps saying "listening"
|
|
76
|
+
// when it is unregistered — see pulseText — and painting that sentence in
|
|
77
|
+
// the warning tone would restore the alarm the sentence just retired.
|
|
78
|
+
const alarm = !registered && wantClaude;
|
|
79
|
+
const state = { registered, claude: wantClaude, busy: busyLabel() };
|
|
80
|
+
const text = pulseText({ ...state, columns: cols(), unicode: UNICODE });
|
|
81
|
+
// At rest every beat is lit, which is what makes the line still: the frame
|
|
82
|
+
// is then identical to the one already on screen and the write below is
|
|
83
|
+
// skipped. See pulseDot.
|
|
84
|
+
const dot = pulseDot(pi++, state) === "on" ? (alarm ? P.warn : P.ok) : P.muted;
|
|
85
|
+
const tone = alarm ? P.warn : P.muted;
|
|
86
|
+
const frame = `\r ${dot}${G.pulse}${P.reset} ${tone}${text}${P.reset}`;
|
|
87
|
+
// Unchanged frames are not written at all. That is what makes "at rest"
|
|
88
|
+
// visible: one paint, and then a still line for as long as nothing happens.
|
|
89
|
+
// `painted` is dropped by the guard above whenever somebody else writes, so
|
|
90
|
+
// this can only skip a beat while the line is genuinely still where we left
|
|
91
|
+
// it.
|
|
92
|
+
if (frame === painted) return;
|
|
93
|
+
painted = frame;
|
|
94
|
+
writing = true;
|
|
95
|
+
try { write(frame); } finally { writing = false; }
|
|
96
|
+
ours = true;
|
|
97
|
+
}, 800).unref();
|
|
98
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// The restart latch: one restart at a time, held until the boot can take it,
|
|
2
|
+
// and let go of on every way it can fail.
|
|
3
|
+
//
|
|
4
|
+
// Lifted out of bin/deck.js. The latch is the only state here, and the two
|
|
5
|
+
// things it acts through are handed in rather than reached for: `shutdown`,
|
|
6
|
+
// deck.js's own way out, which a restart ends in, and `releaseRestart`, the
|
|
7
|
+
// server's half of the same latch. What comes back is requestRestart, for
|
|
8
|
+
// startServer's onRestart, and the two calls deck.js makes when its boot ends.
|
|
9
|
+
import { oneLine } from "../../src/server/term.mjs";
|
|
10
|
+
import { RESTART_CODE, UPGRADE_CODE } from "../../src/server/supervisor.mjs";
|
|
11
|
+
import { PKG_VERSION, versionOnDisk } from "./package.js";
|
|
12
|
+
import { G, P, cols, write } from "./screen.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Arm the latch, and the supervisor's answer to an upgrade with it. Called once,
|
|
16
|
+
* before the server can take a request.
|
|
17
|
+
*/
|
|
18
|
+
export function restartLatch({ shutdown, releaseRestart }) {
|
|
19
|
+
// Asking the supervisor to bring us back. It is the only party that can, and
|
|
20
|
+
// only after this process is gone — which is precisely what keeps the
|
|
21
|
+
// replacement from racing this listener onto a random fallback port.
|
|
22
|
+
let restarting = false;
|
|
23
|
+
// Outer bound on the supervisor's answer below. It cannot be reached today —
|
|
24
|
+
// the fetch has a deadline of its own and every path through it replies — but
|
|
25
|
+
// `restarting` is a latch, and a latch with no way out is how a deck ends up
|
|
26
|
+
// silently refusing every restart for the rest of its life.
|
|
27
|
+
const UPGRADE_ANSWER_MS = 150_000;
|
|
28
|
+
let upgradeTimer = null;
|
|
29
|
+
|
|
30
|
+
// Whether the boot has finished: markBooted, called by deck.js at its end.
|
|
31
|
+
//
|
|
32
|
+
// The server starts accepting connections from inside startServer, before
|
|
33
|
+
// that call has returned — so /api/restart is reachable for the whole of the
|
|
34
|
+
// boot that follows it: the startup report, the port report to the supervisor,
|
|
35
|
+
// the discovery file and its first fsynced write, and on a cold start the
|
|
36
|
+
// browser spawn. A restart landing in that window used to reach `shutdown`
|
|
37
|
+
// before the binding holding it was initialised and die of a ReferenceError,
|
|
38
|
+
// having already set the latch above, with nothing left to clear it — after
|
|
39
|
+
// which every restart from every tab was answered "ok" and did nothing, for the
|
|
40
|
+
// life of the process (#448).
|
|
41
|
+
//
|
|
42
|
+
// So an ask that arrives too early is held rather than run: the user asked for
|
|
43
|
+
// something this deck can genuinely give a moment later, and refusing outright
|
|
44
|
+
// would put back the same silence in a politer form. BOOT_RESTART_MS is the
|
|
45
|
+
// outer bound, for the reason UPGRADE_ANSWER_MS above is one. It used to be a
|
|
46
|
+
// bound that got used — before #742 the report waited out a real `uv tool
|
|
47
|
+
// install`, so the window it covers was minutes wide. It is now the boot
|
|
48
|
+
// deadline plus the browser spawn, comfortably inside ten seconds, and this
|
|
49
|
+
// stays as the thing that makes that a fact rather than a belief. Ten seconds
|
|
50
|
+
// in, the ask is run; the respawn skips the report entirely and is up in about
|
|
51
|
+
// a second.
|
|
52
|
+
let booted = false;
|
|
53
|
+
let heldRestart = null;
|
|
54
|
+
let bootTimer = null;
|
|
55
|
+
const BOOT_RESTART_MS = 10_000;
|
|
56
|
+
|
|
57
|
+
const requestRestart = (mode) => {
|
|
58
|
+
if (restarting) return;
|
|
59
|
+
restarting = true;
|
|
60
|
+
if (!booted) {
|
|
61
|
+
heldRestart = { mode };
|
|
62
|
+
bootTimer = setTimeout(() => { bootTimer = null; runHeldRestart(); }, BOOT_RESTART_MS);
|
|
63
|
+
bootTimer.unref?.();
|
|
64
|
+
// Said out loud for the same reason abandonUpgrade below is: the tab has
|
|
65
|
+
// already been told its restart was accepted, and a second of nothing
|
|
66
|
+
// happening on this terminal is otherwise indistinguishable from the bug
|
|
67
|
+
// this replaces.
|
|
68
|
+
write(`\n ${P.warn}${G.restart}${P.reset} ${P.muted}restart queued ${G.dash} still starting up${P.reset}\n`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
beginRestart(mode);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// The restart itself, once there is a booted deck to end. Split out of
|
|
75
|
+
// requestRestart so the held ask above can re-enter it without tripping the
|
|
76
|
+
// latch it is already holding.
|
|
77
|
+
//
|
|
78
|
+
// Everything here runs inside one try: the whole point of #448 is that a throw
|
|
79
|
+
// on this path is not merely a failed restart but a permanent one, because the
|
|
80
|
+
// latch it leaves behind outlives it. There is no line in here worth dying for.
|
|
81
|
+
function beginRestart(mode) {
|
|
82
|
+
try {
|
|
83
|
+
// "npx" means the newer code is not on this disk at all, so it has to be
|
|
84
|
+
// fetched — and this process keeps serving while that happens. Exiting first
|
|
85
|
+
// is what made every failed upgrade an outage: the SSE stream dropped, hook
|
|
86
|
+
// events fired into the gap were lost outright (hook/hook.js is
|
|
87
|
+
// fire-and-forget with a 1s timeout and no retry), and the canvas came back
|
|
88
|
+
// with whatever was in flight stuck until the stale sweeper reaped it — all
|
|
89
|
+
// of it paid before anyone knew whether npm could even resolve the version.
|
|
90
|
+
// Nothing is torn down here now; the supervisor answers when it knows.
|
|
91
|
+
if (mode === "npx") {
|
|
92
|
+
upgradeTimer = setTimeout(() => abandonUpgrade("no answer from the supervisor"), UPGRADE_ANSWER_MS);
|
|
93
|
+
upgradeTimer.unref?.();
|
|
94
|
+
// Armed before the ask, not after: a send that throws is a supervisor that
|
|
95
|
+
// can no longer answer, and the deck has to come back out of the latch on
|
|
96
|
+
// its own rather than wait out an answer that cannot arrive.
|
|
97
|
+
try { process.send({ type: "upgrade" }); }
|
|
98
|
+
catch (err) { abandonUpgrade(err?.message ?? "the supervisor is no longer listening"); }
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// What a restart would land on. Read from disk now rather than remembered
|
|
102
|
+
// from boot, because the whole point is that the two differ.
|
|
103
|
+
const to = versionOnDisk();
|
|
104
|
+
write(`\n ${P.warn}${G.restart}${P.reset} ${P.muted}restarting${to ? ` ${G.arrow} v${to}` : ""}${G.ellipsis}${P.reset}\n`);
|
|
105
|
+
shutdown(RESTART_CODE);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
abandonRestart(err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The ask that was waiting for the boot to finish, now that it has. Safe to
|
|
112
|
+
// call when nothing is waiting, which is every ordinary boot.
|
|
113
|
+
function runHeldRestart() {
|
|
114
|
+
if (!heldRestart) return;
|
|
115
|
+
const { mode } = heldRestart;
|
|
116
|
+
heldRestart = null;
|
|
117
|
+
clearTimeout(bootTimer);
|
|
118
|
+
bootTimer = null;
|
|
119
|
+
beginRestart(mode);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A restart that could not be started, said out loud and then let go of.
|
|
123
|
+
//
|
|
124
|
+
// Both halves of the latch have to come down — this one's and the server's —
|
|
125
|
+
// because a latch nothing clears is precisely how one failed request turned
|
|
126
|
+
// into a deck that refused every restart afterwards while answering "ok" to
|
|
127
|
+
// each one (#448). The reason is folded onto one line by oneLine: the terminal
|
|
128
|
+
// under this is repainted every 800ms by the pulse, and a stack written into
|
|
129
|
+
// that is a stack nobody can read (#432).
|
|
130
|
+
//
|
|
131
|
+
// A declaration rather than a const, like deck.js's `shutdown` and for the same
|
|
132
|
+
// reason: this is the handler for a binding that was not there yet, and it must
|
|
133
|
+
// not be capable of becoming the next one.
|
|
134
|
+
function abandonRestart(err) {
|
|
135
|
+
clearTimeout(bootTimer);
|
|
136
|
+
bootTimer = null;
|
|
137
|
+
heldRestart = null;
|
|
138
|
+
restarting = false;
|
|
139
|
+
releaseRestart();
|
|
140
|
+
write(
|
|
141
|
+
`\n ${P.err}${G.fail}${P.reset} ${P.muted}restart failed ${G.dash} still on ${P.reset}v${PKG_VERSION}\n` +
|
|
142
|
+
` ${P.muted}${oneLine(err?.stack ?? err, Math.max(20, cols() - 6), G.ellipsis)}${P.reset}\n`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The upgrade did not happen and this deck is still the deck. Said out loud
|
|
147
|
+
// because the terminal has just printed that a fetch was starting, and left
|
|
148
|
+
// unsaid it reads as a restart that hung.
|
|
149
|
+
const abandonUpgrade = (why) => {
|
|
150
|
+
clearTimeout(upgradeTimer);
|
|
151
|
+
restarting = false;
|
|
152
|
+
// The server's own latch, which no longer has an exiting process to clear it.
|
|
153
|
+
releaseRestart();
|
|
154
|
+
write(
|
|
155
|
+
`\n ${P.warn}${G.cancel}${P.reset} ${P.muted}update not applied ${G.dash} still on ${P.reset}v${PKG_VERSION}\n` +
|
|
156
|
+
(why ? ` ${P.muted}${why}${P.reset}\n` : ""),
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// The supervisor's verdict on the fetch it was asked for. Only it can answer:
|
|
161
|
+
// the fetch is its child, and it is the process that will still be here when
|
|
162
|
+
// this one exits.
|
|
163
|
+
process.on("message", (m) => {
|
|
164
|
+
if (!restarting || !m || typeof m !== "object") return;
|
|
165
|
+
if (m.type === "upgrade-ready") {
|
|
166
|
+
clearTimeout(upgradeTimer);
|
|
167
|
+
// The replacement is on the machine now, so this is the last moment the
|
|
168
|
+
// port is worth holding: exiting hands it straight over.
|
|
169
|
+
write(`\n ${P.warn}${G.restart}${P.reset} ${P.muted}updating via npx${G.ellipsis}${P.reset}\n`);
|
|
170
|
+
shutdown(UPGRADE_CODE);
|
|
171
|
+
} else if (m.type === "upgrade-refused") {
|
|
172
|
+
abandonUpgrade(m.error);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/** The boot is over, so an ask that arrives from now on is run, not held. */
|
|
177
|
+
const markBooted = () => { booted = true; };
|
|
178
|
+
return { requestRestart, markBooted, runHeldRestart };
|
|
179
|
+
}
|