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.
- package/README.md +1 -1
- 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-DP3YMtEi.js +1 -0
- package/dist/web/assets/UsageHistoryModal-CcdPVXAw.js +1 -0
- package/dist/web/assets/index-3vS6A0vt.css +1 -0
- package/dist/web/assets/index-BVTWHDta.js +298 -0
- package/dist/web/index.html +2 -2
- package/hook/hook.js +15 -14
- package/package.json +1 -1
- package/release-notes.json +28 -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 +2 -2
- 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 +1 -1
- package/src/server/deck-prefs.mjs +51 -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 +172 -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 +108 -0
- package/src/server/prefs-path.mjs +14 -0
- package/src/server/prefs-refusal.mjs +77 -0
- package/src/server/prefs-routes.mjs +76 -0
- package/src/server/prefs-set-aside.mjs +86 -0
- package/src/server/prefs-state.mjs +51 -0
- package/src/server/process-command.mjs +104 -0
- package/src/server/process-list.mjs +547 -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 +93 -0
- package/src/server/uv-bootstrap.mjs +1 -1
- package/src/server/wordmark.mjs +127 -0
- package/dist/web/assets/BrowserWatchModal-CMn28FHb.js +0 -1
- package/dist/web/assets/UsageHistoryModal-C-sPhsKT.js +0 -1
- package/dist/web/assets/index-C1YFI9f4.js +0 -298
- package/dist/web/assets/index-Ck2RAla3.css +0 -1
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// The once-per-session work, and the startup report that says how it went.
|
|
2
|
+
//
|
|
3
|
+
// Lifted out of bin/deck.js, which starts the work beside the server, prints
|
|
4
|
+
// the wordmark over it and then calls reportStartup, exactly as before. What the
|
|
5
|
+
// report needs to know about this boot — the workspace, which CLIs it serves,
|
|
6
|
+
// where the Codex watcher looks — comes in as arguments; the modules the jobs
|
|
7
|
+
// load are still imported off PKG_ROOT at the moment each job starts.
|
|
8
|
+
//
|
|
9
|
+
// The two flag rows are here too: they are the end of this report, printed by
|
|
10
|
+
// deck.js once the server row and the log row have landed under it.
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { budget, bootDeadlineMs } from "../../src/server/boot-deadline.mjs";
|
|
14
|
+
import { PRODUCT } from "../../src/server/brand.mjs";
|
|
15
|
+
import { renameNotice } from "../../src/server/invoked-as.mjs";
|
|
16
|
+
import { link } from "../../src/server/term.mjs";
|
|
17
|
+
import { wayBackNote } from "../../src/server/way-back.mjs";
|
|
18
|
+
import { COMMAND, INVOKED_AS, PKG_ROOT, PKG_VERSION } from "./package.js";
|
|
19
|
+
import { G, LINKS, P, cols, fileLink, row, step, write } from "./screen.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What is still happening after the report ended, in three words or so.
|
|
23
|
+
*
|
|
24
|
+
* #742 left one job able to outlive the boot — an install of claude-swap that
|
|
25
|
+
* the report stopped waiting for — and the terminal said nothing about it once
|
|
26
|
+
* the rows were done. That is the wrong way round: the row that had finished
|
|
27
|
+
* was the one blinking, and the thing that really was working sat still. Now
|
|
28
|
+
* the pulse line carries the label while the job runs and drops it when the job
|
|
29
|
+
* settles, which is also what decides whether the line moves at all. Kept
|
|
30
|
+
* here, beside the report that sets it; the pulse line in bin/deck.js reads it
|
|
31
|
+
* through busyLabel.
|
|
32
|
+
*/
|
|
33
|
+
let pulseBusy = null;
|
|
34
|
+
|
|
35
|
+
/** The pulse line's label for work still running after the report, or null. */
|
|
36
|
+
export const busyLabel = () => pulseBusy;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The once-per-session work, all of it started at once and none of it awaited.
|
|
40
|
+
*
|
|
41
|
+
* Hook install, the claude-swap probe and the registry lookup have nothing to
|
|
42
|
+
* do with each other and nothing to do with the wordmark, so they run underneath
|
|
43
|
+
* the reveal instead of queueing behind it — the animation then costs the boot
|
|
44
|
+
* nothing and the deck is ready about when the last art row lands. Every one of
|
|
45
|
+
* them is given its rejection handler here, at the moment it is created, since a
|
|
46
|
+
* promise that settles before anything awaits it is otherwise an unhandled
|
|
47
|
+
* rejection.
|
|
48
|
+
*/
|
|
49
|
+
export function startupWork({ wantClaude, installHooks, leftoverCodexHooks }) {
|
|
50
|
+
// Every job below this line serves Claude Code and only Claude Code: the
|
|
51
|
+
// hooks go in Claude Code's settings.json, claude-swap switches Claude
|
|
52
|
+
// accounts, and ccusage reads Claude Code's own session logs. On a machine
|
|
53
|
+
// without Claude Code all three are work done for a CLI that is not there —
|
|
54
|
+
// two of them installs the user did not ask for — so they are not started at
|
|
55
|
+
// all rather than started and then reported as failures. `null` is how each
|
|
56
|
+
// one says "not attempted", which reportStartup tells apart from "tried and
|
|
57
|
+
// could not".
|
|
58
|
+
|
|
59
|
+
// Settings the installer cannot parse are settings it cannot rewrite without
|
|
60
|
+
// losing them, so it refuses — and that refusal is reported rather than
|
|
61
|
+
// thrown, because it is the only thing the user can act on.
|
|
62
|
+
const hooks = wantClaude
|
|
63
|
+
? installHooks({ provider: "claude" }).then(v => ({ ok: true, v }), err => ({ ok: false, err }))
|
|
64
|
+
: Promise.resolve(null);
|
|
65
|
+
|
|
66
|
+
// claude-swap backs the multi-account panel, and an empty store leaves that
|
|
67
|
+
// panel useless even when the tool is there — so the account already signed
|
|
68
|
+
// in is registered once. Bounded inside seedFirstAccount: empty store only,
|
|
69
|
+
// once ever, never with NO_INSTALL set.
|
|
70
|
+
//
|
|
71
|
+
// `cswapInstalling` is the other half, and it is what keeps a first run from
|
|
72
|
+
// spending the boot's whole deadline on a question that has already been
|
|
73
|
+
// answered: ensureCswap resolves it the moment it commits to an install, so
|
|
74
|
+
// the report can stop waiting then rather than eight seconds later. It never
|
|
75
|
+
// settles on the machines where there is nothing to install, which is every
|
|
76
|
+
// machine after the first run.
|
|
77
|
+
let sayInstalling;
|
|
78
|
+
const cswapInstalling = new Promise(r => { sayInstalling = r; });
|
|
79
|
+
|
|
80
|
+
const cswap = (async () => {
|
|
81
|
+
if (!wantClaude) return null;
|
|
82
|
+
const { ensureCswap } = await import(pathToFileURL(join(PKG_ROOT, "src/server/cswap-install.mjs")).href);
|
|
83
|
+
const cs = await ensureCswap({ onInstalling: () => sayInstalling() });
|
|
84
|
+
const usable = cs.state === "present" || cs.state === "installed" || cs.state === "upgrading";
|
|
85
|
+
if (!usable) return { cs, seed: null };
|
|
86
|
+
const { seedFirstAccount } = await import(pathToFileURL(join(PKG_ROOT, "src/server/claude-accounts.mjs")).href);
|
|
87
|
+
return { cs, seed: await seedFirstAccount().catch(() => ({ state: "failed" })) };
|
|
88
|
+
})().catch(() => null);
|
|
89
|
+
|
|
90
|
+
// WHEN THE TOOL IS QUIET, which is not the same as when the job above settles
|
|
91
|
+
// (#1043). The auto-switch drives this same claude-swap — every tick is
|
|
92
|
+
// `cswap auto --once`, which moves the user's live Claude credentials — and
|
|
93
|
+
// the server arms it the moment the port binds, while this job is still in
|
|
94
|
+
// ensureCswap. So the server holds every tick until this resolves, and it has
|
|
95
|
+
// two halves because the job covers only the first: ensureCswap answers
|
|
96
|
+
// "upgrading" as soon as it has FIRED an upgrade it deliberately does not
|
|
97
|
+
// await, and that upgrade is a uv or pipx environment being rewritten for
|
|
98
|
+
// tens of seconds after this job has returned. upgradeSettled() is the handle
|
|
99
|
+
// on it, and null when nothing was started.
|
|
100
|
+
//
|
|
101
|
+
// Not markDeckReady, the other place "the boot is over" gets said:
|
|
102
|
+
// reportStartup stops waiting for claude-swap the moment the install
|
|
103
|
+
// announces itself, so the deck is marked ready with the install still
|
|
104
|
+
// running. Never rejects — a job that failed has nothing left running.
|
|
105
|
+
const cswapQuiet = cswap.then(async () => {
|
|
106
|
+
if (!wantClaude) return;
|
|
107
|
+
const { upgradeSettled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/cswap-install.mjs")).href);
|
|
108
|
+
await upgradeSettled();
|
|
109
|
+
}).catch(() => {});
|
|
110
|
+
|
|
111
|
+
// ccusage backs the usage-history modal. Primed at boot rather than on first
|
|
112
|
+
// open so a cold machine pays the install while the deck is still starting.
|
|
113
|
+
// Nothing is lost by skipping the prime: runCcusage falls back to npx, so the
|
|
114
|
+
// modal still answers if it is ever opened — it just pays the wait itself.
|
|
115
|
+
const ccusage = (async () => {
|
|
116
|
+
if (!wantClaude) return null;
|
|
117
|
+
if (process.env.AGENTS_DECK_NO_INSTALL === "1") return null;
|
|
118
|
+
const { primeCcusage } = await import(pathToFileURL(join(PKG_ROOT, "src/server/ccusage.mjs")).href);
|
|
119
|
+
return primeCcusage();
|
|
120
|
+
})().catch(() => null);
|
|
121
|
+
|
|
122
|
+
// A newer release on npm, said once, in the place the upgrade gets typed.
|
|
123
|
+
// Hard-capped so a slow registry cannot delay the server — the answer is
|
|
124
|
+
// usually already cached in ~/.agents-deck/.self-update-check anyway. It has
|
|
125
|
+
// to resolve BEFORE the pulse indicator starts writing over the last line.
|
|
126
|
+
const update = Promise.race([
|
|
127
|
+
import(pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href)
|
|
128
|
+
.then(m => m.versionReport({ running: PKG_VERSION, pkgRoot: PKG_ROOT }))
|
|
129
|
+
.then(r => (r?.notice?.kind === "upgrade" ? r : null))
|
|
130
|
+
.catch(() => null),
|
|
131
|
+
new Promise(r => setTimeout(() => r(null), 1200)),
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// An older deck's Codex forwarders, looked for and never touched — see
|
|
135
|
+
// leftoverCodexHooks for why a boot names them rather than removing them.
|
|
136
|
+
// Asked with --no-codex too: the entries fire whether or not this deck is
|
|
137
|
+
// watching the rollouts, and one file read is the whole cost.
|
|
138
|
+
const codexHooks = leftoverCodexHooks().catch(() => null);
|
|
139
|
+
|
|
140
|
+
return { hooks, cswap, cswapInstalling, cswapQuiet, ccusage, update, codexHooks };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The same work, said out loud, in a fixed order — a boot whose rows arrive in
|
|
144
|
+
* whatever order the network settled is a boot nobody can scan twice. */
|
|
145
|
+
export async function reportStartup(jobs, { workspace, wantClaude, wantCodex, CODEX_SESSIONS_DIR }) {
|
|
146
|
+
// What the whole report may spend waiting, shared by every job under it
|
|
147
|
+
// rather than granted to each — four jobs at eight seconds each is a
|
|
148
|
+
// thirty-two second boot that no individual deadline would object to.
|
|
149
|
+
const left = budget(bootDeadlineMs());
|
|
150
|
+
|
|
151
|
+
write(row({
|
|
152
|
+
mark: G.ok, label: "workspace",
|
|
153
|
+
detail: workspace === "" ? "(all)" : workspace,
|
|
154
|
+
detailTone: workspace === "" ? P.warn : P.muted,
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
const hooks = await step(`installing Claude hooks${G.ellipsis}`, jobs.hooks);
|
|
158
|
+
if (hooks === null) {
|
|
159
|
+
// Said in the same shape as the Codex row below, because it is the same
|
|
160
|
+
// sentence: this deck is not watching that CLI, and here is why. It also
|
|
161
|
+
// retires the one boot failure a Codex-only machine could hit — an
|
|
162
|
+
// unparseable or unwritable settings.json used to exit(1) below, killing a
|
|
163
|
+
// deck over a file belonging to a CLI the user does not run.
|
|
164
|
+
write(row({ label: "Claude hooks", detail: `skipped ${G.dash} no Claude Code found, or --no-claude` }));
|
|
165
|
+
} else if (!hooks.ok) {
|
|
166
|
+
// The file it names is one only the user can repair, and every Claude Code
|
|
167
|
+
// session on this machine is reading it too.
|
|
168
|
+
write(row({ mark: G.fail, tone: P.err, label: "Claude hooks", detail: "not installed" }));
|
|
169
|
+
// THE WAY OUT, printed where the wall is. The installer's message is good —
|
|
170
|
+
// the path, the reason, "fix the file or move it aside" — and it is a file
|
|
171
|
+
// the user may not be able to edit: root-owned, on read-only media, or
|
|
172
|
+
// simply not theirs. This deck knows the remedy and used to keep it in a
|
|
173
|
+
// comment: --no-claude runs everything else, which on a Codex-only machine
|
|
174
|
+
// is the whole deck, over a settings.json belonging to a CLI they do not
|
|
175
|
+
// use.
|
|
176
|
+
console.error(`\n ${PRODUCT}: ${hooks.err.message}`);
|
|
177
|
+
console.error(` Or start with --no-claude to run without Claude hooks.\n`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
} else {
|
|
180
|
+
write(row({ mark: G.ok, label: "Claude hooks", detail: fileLink(hooks.v.hookPath) }));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Codex CLI hooks never fire on Windows (sandbox refuses to spawn the hook
|
|
184
|
+
// command). Instead the server tails Codex's rollout JSONL files directly, so
|
|
185
|
+
// there's nothing to install and no /hooks trust step. We just confirm Codex
|
|
186
|
+
// is present and let the watcher pick up sessions.
|
|
187
|
+
if (wantCodex) {
|
|
188
|
+
// The directory the watcher actually tails, imported from the module that
|
|
189
|
+
// owns it rather than rebuilt from homedir() here. CODEX_HOME relocates the
|
|
190
|
+
// whole tree, and this row is the only diagnostic the deck prints about
|
|
191
|
+
// Codex — so when sessions do not show up, a path computed a second way
|
|
192
|
+
// sends the user to inspect a directory the deck never opened.
|
|
193
|
+
write(row({ mark: G.ok, label: "Codex sessions", detail: `watching ${fileLink(CODEX_SESSIONS_DIR)}` }));
|
|
194
|
+
} else {
|
|
195
|
+
write(row({ label: "Codex sessions", detail: `skipped ${G.dash} no ~/.codex/, or --no-codex` }));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// #983. A machine that ran a deck from before #253 still has that deck's
|
|
199
|
+
// forwarders in Codex's hooks.json, and nothing but `--uninstall` ever looked.
|
|
200
|
+
// Where Codex honours the file, each one posts the session to /api/event while
|
|
201
|
+
// the watcher above reads the same session off disk, so it arrives twice. Both
|
|
202
|
+
// copies go into the ring and events.jsonl; the reducer's two-second
|
|
203
|
+
// redelivery windows fold some of them on the card and not others, and a
|
|
204
|
+
// duplicate that shows only sometimes reads as a reducer bug, which is where
|
|
205
|
+
// it would otherwise be chased. Saying nothing was the one answer the issue
|
|
206
|
+
// ruled out. So: one row, in the place the deck already talks about Codex,
|
|
207
|
+
// naming the file and the command. `keep`, because a remedy cut off by an
|
|
208
|
+
// ellipsis is no remedy. The clause about arriving twice is said only while
|
|
209
|
+
// the watcher runs, since without it the forwarders are the only copy.
|
|
210
|
+
const leftover = await jobs.codexHooks;
|
|
211
|
+
if (leftover) {
|
|
212
|
+
const twice = wantCodex ? ", so Codex sessions can arrive twice" : "";
|
|
213
|
+
write(row({
|
|
214
|
+
mark: G.warn, tone: P.warn, label: "Codex hooks", keep: true,
|
|
215
|
+
detail: `left by an older deck in ${fileLink(leftover.settingsPath)}${twice} ${G.dash} \`${COMMAND} --uninstall\` takes them out`,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Bounded, because this is the job that made a first boot look hung: on a
|
|
220
|
+
// machine with neither claude-swap nor a Python toolchain it fetches a uv
|
|
221
|
+
// binary and then builds an environment with it, and the report used to wait
|
|
222
|
+
// out both. See src/server/boot-deadline.mjs. Nothing is cancelled — the
|
|
223
|
+
// install carries on and says how it went when it knows.
|
|
224
|
+
const swapWait = await step(`checking claude-swap${G.ellipsis}`, Promise.race([
|
|
225
|
+
left.within(jobs.cswap),
|
|
226
|
+
// The install announcing itself. Not a timeout — a decision, arriving in
|
|
227
|
+
// about a second on the boot that would otherwise have paid the full
|
|
228
|
+
// deadline for news it already had.
|
|
229
|
+
jobs.cswapInstalling.then(() => ({ done: false, installing: true })),
|
|
230
|
+
]));
|
|
231
|
+
if (swapWait.done) writeSwapRows(swapWait.value, { wantClaude });
|
|
232
|
+
else {
|
|
233
|
+
write(row({
|
|
234
|
+
label: "claude-swap",
|
|
235
|
+
// Both halves earn their place, and both have to survive an 80-column
|
|
236
|
+
// terminal: what the job is doing, and that waiting for it is not the
|
|
237
|
+
// user's problem. The row that only said the first is the row this
|
|
238
|
+
// replaces — a spinner at "checking claude-swap…" says that much.
|
|
239
|
+
detail: swapWait.installing
|
|
240
|
+
? `installing in the background ${G.dash} the deck is ready`
|
|
241
|
+
: `still setting up ${G.dash} the deck is ready`,
|
|
242
|
+
}));
|
|
243
|
+
// Handed to the pulse line, which is the only thing still on screen once
|
|
244
|
+
// the rows are done — and taken back the moment the job settles, whichever
|
|
245
|
+
// way it settled.
|
|
246
|
+
pulseBusy = swapWait.installing ? "installing claude-swap" : "setting up claude-swap";
|
|
247
|
+
jobs.cswap.then(
|
|
248
|
+
late => { pulseBusy = null; writeSwapRows(late, { wantClaude, late: true }); },
|
|
249
|
+
() => { pulseBusy = null; },
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const cu = await left.within(jobs.ccusage);
|
|
254
|
+
writeCcusageRow(cu.done ? cu.value : null);
|
|
255
|
+
|
|
256
|
+
const upgrade = await jobs.update;
|
|
257
|
+
if (upgrade) {
|
|
258
|
+
write(row({
|
|
259
|
+
mark: G.up, tone: P.warn, label: "update",
|
|
260
|
+
detail: `v${upgrade.notice.to} available ${G.dash} ${upgrade.command}`,
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Which name this deck was started under, when that is knowable — a notice,
|
|
265
|
+
// never a refusal. 95% of installs are on the two old names and the update
|
|
266
|
+
// path runs through this very process, so a build that declined to boot under
|
|
267
|
+
// one of them would kill the deck on the machine where the deck is what would
|
|
268
|
+
// have explained why. Nothing at all is printed wherever the typed name
|
|
269
|
+
// cannot be proven (a Windows global install, a git checkout): telling
|
|
270
|
+
// somebody who already types `ccdeck` to type `ccdeck` is the one failure
|
|
271
|
+
// that would make this row worth ignoring. See src/server/invoked-as.mjs.
|
|
272
|
+
const rename = renameNotice({ invoked: INVOKED_AS, pkgRoot: PKG_ROOT, dash: G.dash });
|
|
273
|
+
if (rename) {
|
|
274
|
+
write(row({ mark: G.warn, tone: P.warn, label: "name", detail: rename.said }));
|
|
275
|
+
// The line that carries the value: for a global install there is nothing to
|
|
276
|
+
// install and nothing to download, only six different characters to type.
|
|
277
|
+
write(row({ label: "", detail: rename.fix }));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The claude-swap rows, wherever in the boot they end up being printed.
|
|
283
|
+
*
|
|
284
|
+
* `late` is the one difference, and it is not cosmetic: by the time a late row
|
|
285
|
+
* arrives the pulse indicator owns the last line and repaints it with `\r`, so
|
|
286
|
+
* a row written without a newline first would be drawn over on the next beat.
|
|
287
|
+
* Every other thing that speaks after boot — reportUnregistered,
|
|
288
|
+
* reportReregistered — opens with the same newline for the same reason.
|
|
289
|
+
*/
|
|
290
|
+
function writeSwapRows(swap, { wantClaude, late = false }) {
|
|
291
|
+
const cs = swap?.cs;
|
|
292
|
+
// Collected rather than written one at a time, because a late report opens
|
|
293
|
+
// with a newline and there is exactly one of those however many rows follow.
|
|
294
|
+
let out = "";
|
|
295
|
+
if (!wantClaude) {
|
|
296
|
+
// claude-swap is a Python tool that switches Claude Code accounts, and the
|
|
297
|
+
// deck used to fetch a uv binary to install it on machines with no Claude
|
|
298
|
+
// Code at all. Saying so is the point of the row: it is the one place a
|
|
299
|
+
// user can learn that the accounts panel is missing on purpose.
|
|
300
|
+
out += row({ label: "claude-swap", detail: `skipped ${G.dash} accounts are Claude-only` });
|
|
301
|
+
} else if (cs?.state === "present") {
|
|
302
|
+
out += row({ mark: G.ok, label: "claude-swap", detail: `v${cs.version} (accounts panel enabled)` });
|
|
303
|
+
} else if (cs?.state === "installed") {
|
|
304
|
+
out += row({ mark: G.ok, label: "claude-swap", detail: `installed v${cs.version} via ${cs.via}` });
|
|
305
|
+
} else if (cs?.state === "upgrading") {
|
|
306
|
+
out += row({ mark: G.ok, label: "claude-swap", detail: `v${cs.version}, upgrading to v${cs.latest} in background` });
|
|
307
|
+
} else if (cs?.state === "skipped") {
|
|
308
|
+
out += row({ mark: G.ok, label: "claude-swap", detail: "not installed (AGENTS_DECK_NO_INSTALL=1)" });
|
|
309
|
+
} else {
|
|
310
|
+
const how = cs?.reason === "no_installer"
|
|
311
|
+
? `not installed ${G.dash} the accounts panel needs it`
|
|
312
|
+
: cs?.reason === "not_on_path"
|
|
313
|
+
? `installed via ${cs.via} but not on PATH ${G.dash} add ${
|
|
314
|
+
process.platform === "win32" ? "%USERPROFILE%\\.local\\bin" : "~/.local/bin"
|
|
315
|
+
}`
|
|
316
|
+
// The deck asked for one version and a different one answered, so it
|
|
317
|
+
// declines to drive it. Naming both is the whole content of the row:
|
|
318
|
+
// "install failed" would be a lie about an install that succeeded, and
|
|
319
|
+
// the number that arrived is the thing a person needs to see.
|
|
320
|
+
: cs?.reason === "unexpected_version"
|
|
321
|
+
? `refused v${cs.version} ${G.dash} asked for ${cs.want}, not driving it`
|
|
322
|
+
: `install failed${cs?.via ? ` via ${cs.via}` : ""}`;
|
|
323
|
+
out += row({ mark: G.fail, tone: P.warn, label: "claude-swap", detail: how });
|
|
324
|
+
// A URL is not an answer when someone just wants the panel to work. Print
|
|
325
|
+
// the command for THIS machine, picked from what is already on it.
|
|
326
|
+
if (cs?.hint) out += row({ label: "", detail: cs.hint });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (swap?.seed?.state === "added") {
|
|
330
|
+
out += row({ mark: G.ok, label: "accounts", detail: "registered the signed-in account (cswap add)" });
|
|
331
|
+
} else if (swap?.seed?.state === "failed" || swap?.seed?.state === "nothing-to-add") {
|
|
332
|
+
out += row({ label: "accounts", detail: `panel empty ${G.dash} sign in to Claude Code, then run cswap add` });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
write(late ? "\n" + out : out);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The ccusage row.
|
|
340
|
+
*
|
|
341
|
+
* `null` covers both of the ways there is nothing to say — the job was not
|
|
342
|
+
* attempted, and the job had not answered by the time the boot's deadline ran
|
|
343
|
+
* out. Neither deserves a row: unlike claude-swap there is no install to wait
|
|
344
|
+
* for here, because primeCcusage starts one and returns without it, so a
|
|
345
|
+
* ccusage that is slow to answer is slow at resolving a path and will be
|
|
346
|
+
* resolved again the first time the usage modal is opened.
|
|
347
|
+
*/
|
|
348
|
+
function writeCcusageRow(cu) {
|
|
349
|
+
if (cu?.state === "present") write(row({ mark: G.ok, label: "ccusage", detail: `v${cu.version}` }));
|
|
350
|
+
else if (cu?.state === "updating") write(row({ mark: G.ok, label: "ccusage", detail: `v${cu.version}, checking for update` }));
|
|
351
|
+
// A ccusage the user provided, named rather than versioned — reading a
|
|
352
|
+
// version out of it means running it, and a status row is not worth a spawn.
|
|
353
|
+
// Naming the file is the more useful half anyway: it is the answer to "which
|
|
354
|
+
// ccusage is this deck actually going to run", which is a question a machine
|
|
355
|
+
// with a managed install AND a PATH copy could not answer before #433.
|
|
356
|
+
else if (cu?.state === "user") write(row({ mark: G.ok, label: "ccusage", detail: `your own copy ${G.dash} ${cu.bin}` }));
|
|
357
|
+
else if (cu?.state === "installing") write(row({ mark: G.ok, label: "ccusage", detail: "installing in background" }));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Every token the parser did not recognise, named, one row each.
|
|
362
|
+
*
|
|
363
|
+
* Said rather than acted on: the deck goes on booting and still exits 0. It is
|
|
364
|
+
* not a one-shot command that can afford the usual contract. `bin/agent-dag.js`
|
|
365
|
+
* hands its own argv to every worker it spawns — including the npx relaunch,
|
|
366
|
+
* which starts a NEWER version of the package on the argv the user typed
|
|
367
|
+
* against an older one — and the README recommends running it from a wrapper.
|
|
368
|
+
* Refusing to boot over one token would turn a typo into a dark dashboard, and
|
|
369
|
+
* an argument the newer build no longer knows into a failed upgrade that costs
|
|
370
|
+
* the port and the session. The deck already holds that position once, in the
|
|
371
|
+
* `--all` flag: a flag the deck stopped needing is still accepted rather than
|
|
372
|
+
* made fatal.
|
|
373
|
+
*
|
|
374
|
+
* So it goes where the deck puts everything else it decided on your behalf —
|
|
375
|
+
* the startup report — and it goes at the END of it. reportStartup writes its
|
|
376
|
+
* rows in a fixed order and three more land underneath them (the server, the
|
|
377
|
+
* log, the browser), so a warning printed among those rows is a warning the
|
|
378
|
+
* rows scroll over. Here it is the last line before the pulse indicator takes
|
|
379
|
+
* the bottom of the screen and stops repainting anything above it.
|
|
380
|
+
*/
|
|
381
|
+
export function reportUnknownFlags(unknown) {
|
|
382
|
+
for (const token of unknown) {
|
|
383
|
+
write(row({
|
|
384
|
+
mark: G.warn, tone: P.warn, label: "unknown option",
|
|
385
|
+
detail: `${token} ${G.dash} see \`${COMMAND} --help\``,
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Every value-taking flag that was given no value it could use, named, one row
|
|
392
|
+
* each — and printed beside the unknown ones because it is the same failure
|
|
393
|
+
* wearing a different hat.
|
|
394
|
+
*
|
|
395
|
+
* #697: `--workspace`, `--history` and `--port` used to consume the following
|
|
396
|
+
* token whatever it was, so `ccdeck --workspace $PROJ --no-persist` with `PROJ`
|
|
397
|
+
* unset scoped the deck to a directory called `--no-persist`, kept persisting to
|
|
398
|
+
* the shared log, and reported neither. Nothing landed in `unknown`, because the
|
|
399
|
+
* token that belonged there had been eaten. The parser refuses that value now
|
|
400
|
+
* and lists the flag here instead.
|
|
401
|
+
*
|
|
402
|
+
* Said rather than acted on, under exactly the argument reportUnknownFlags makes
|
|
403
|
+
* above: the flag falls back to its documented default and the deck still boots.
|
|
404
|
+
* The row is what makes the fallback a decision the user can see, and the rows
|
|
405
|
+
* around it show its consequence — `workspace (all)` and the `log` line are
|
|
406
|
+
* printed by the same report.
|
|
407
|
+
*/
|
|
408
|
+
export function reportIncompleteFlags(incomplete) {
|
|
409
|
+
for (const { flag, expects } of incomplete ?? []) {
|
|
410
|
+
write(row({
|
|
411
|
+
mark: G.warn, tone: P.warn, label: "missing value",
|
|
412
|
+
detail: `${flag} ${G.dash} expected ${expects}; using the default`,
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* The one line a respawn prints instead of the whole report — the same session
|
|
419
|
+
* continuing — and the flag rows, which it does not skip.
|
|
420
|
+
*/
|
|
421
|
+
export function reportRestarted({ url, flags }) {
|
|
422
|
+
write(` ${P.ok}${G.restart}${P.reset} ${P.muted}restarted ${G.arrow} ${P.reset}v${PKG_VERSION}${P.muted} ${G.bullet} ${link(url, url, LINKS)}${P.reset}\n`);
|
|
423
|
+
// A respawn skips the whole startup report, but not this: the argv is the
|
|
424
|
+
// same argv, the typo in it is still there, and a deck that mentioned it once
|
|
425
|
+
// and then went quiet for every restart afterwards is back to hiding it from
|
|
426
|
+
// anyone who was not watching the first boot.
|
|
427
|
+
reportUnknownFlags(flags.unknown);
|
|
428
|
+
reportIncompleteFlags(flags.incomplete);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The end of the startup report, once the port is bound: the server and log
|
|
433
|
+
* rows, the flag rows under them, the way back, and whether a browser opens.
|
|
434
|
+
*/
|
|
435
|
+
export function reportReady({ url, persist, openBrowser, flags }) {
|
|
436
|
+
// The URL is the one detail an ellipsis would destroy — half an address is
|
|
437
|
+
// not a shorter address — so it keeps its own line when the terminal is too
|
|
438
|
+
// narrow to hold it beside the label. See statusLine's `keep`.
|
|
439
|
+
write(row({
|
|
440
|
+
mark: G.ok, label: "server ready",
|
|
441
|
+
detail: link(url, url, LINKS), detailTone: `${P.accent}${P.bold}`, keep: true,
|
|
442
|
+
}));
|
|
443
|
+
if (persist) write(row({ label: "log", detail: fileLink(persist) }));
|
|
444
|
+
// Last of the rows, on purpose — see reportUnknownFlags.
|
|
445
|
+
reportUnknownFlags(flags.unknown);
|
|
446
|
+
reportIncompleteFlags(flags.incomplete);
|
|
447
|
+
// AFTER the warnings and outside the rows, because it is neither. It is the
|
|
448
|
+
// one thing on this screen that is about next week rather than about this
|
|
449
|
+
// boot — see way-back.mjs — and putting it above a typo warning would be
|
|
450
|
+
// spending the reader's last line of attention on the calmer of the two.
|
|
451
|
+
write(`\n ${P.muted}${G.dash} ${wayBackNote({
|
|
452
|
+
command: COMMAND, dash: G.dash, columns: cols(),
|
|
453
|
+
})}${P.reset}\n`);
|
|
454
|
+
// Only when one is actually being opened. Under --no-open — which is how an
|
|
455
|
+
// npx update relaunches, with a tab already waiting — this was announcing
|
|
456
|
+
// something that never happened.
|
|
457
|
+
if (openBrowser) write(`\n ${P.ok}${P.bold}${G.play} opening browser${G.ellipsis}${P.reset}\n\n`);
|
|
458
|
+
else write("\n");
|
|
459
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// `--uninstall`, and `--purge` with it: every trace of the deck that is not
|
|
2
|
+
// somebody's data, taken off the machine, and every one that could not be,
|
|
3
|
+
// named.
|
|
4
|
+
//
|
|
5
|
+
// Lifted out of bin/deck.js, which still answers it before the migration and
|
|
6
|
+
// well before the heavy imports, exactly where the block used to sit. Every
|
|
7
|
+
// module the uninstall touches is still imported off PKG_ROOT at the moment it
|
|
8
|
+
// is needed, so `--uninstall` loads only what it uses and starts nothing.
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
import { PRODUCT } from "../../src/server/brand.mjs";
|
|
12
|
+
import { COMMAND, PKG_ROOT } from "./package.js";
|
|
13
|
+
import { G } from "./screen.js";
|
|
14
|
+
import { removedRecord } from "./login-item.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Remove this deck's hooks, its login item and, with `flags.purge`, the files
|
|
18
|
+
* holding its LAN private key — and say what happened to each.
|
|
19
|
+
*
|
|
20
|
+
* Resolves to the exit code rather than exiting, so the caller owns the
|
|
21
|
+
* process: 1 when any half of it refused, 0 otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export async function uninstall(flags) {
|
|
24
|
+
const { uninstallHooks, hasCodexInstalled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
|
|
25
|
+
// Anything that could not be taken out. An uninstall that removed nothing
|
|
26
|
+
// because it could not read the file has not uninstalled anything, and both
|
|
27
|
+
// the wording and the exit code have to say so: a user who is told it worked
|
|
28
|
+
// and still has our hooks firing on every event is worse off than one who is
|
|
29
|
+
// told it failed, because they have stopped looking.
|
|
30
|
+
let refused = false;
|
|
31
|
+
// Files already reported as unparseable. The Claude hooks and the sound hook
|
|
32
|
+
// live in the SAME settings.json, so a stray comma refuses both, and printing
|
|
33
|
+
// the whole path-plus-parser-error twice buries the one line that differs —
|
|
34
|
+
// which of our two installations is still in there.
|
|
35
|
+
const named = new Set();
|
|
36
|
+
// The screen's dash, by the name every line below uses: bin/cli/screen.js
|
|
37
|
+
// answers it when it loads, so it is there before the boot draws anything.
|
|
38
|
+
const { dash: gDash } = G;
|
|
39
|
+
/** Report one provider's outcome. `ok` first — see uninstallHooks. */
|
|
40
|
+
const report = (res, label) => {
|
|
41
|
+
if (res.ok === false) {
|
|
42
|
+
refused = true;
|
|
43
|
+
named.add(res.settingsPath);
|
|
44
|
+
console.error(`${PRODUCT}: ${label} hooks NOT removed ${gDash} ${res.settingsPath} could not be read as JSON (${res.why}).`);
|
|
45
|
+
console.error(`${PRODUCT}: the __agent-dag hook entries are still in that file and keep firing on every ${label} event.`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(res.changed
|
|
49
|
+
? `${PRODUCT}: hooks removed from ${res.settingsPath}`
|
|
50
|
+
: `${PRODUCT}: no ${label} hooks to remove`);
|
|
51
|
+
};
|
|
52
|
+
report(await uninstallHooks({ provider: "claude" }), "Claude");
|
|
53
|
+
// The old finish sound was a second entry in the same file, marked
|
|
54
|
+
// __agent-dag-sound rather than __agent-dag, and uninstallHooks does not know
|
|
55
|
+
// that mark — so it used to be left behind, playing on every turn after the
|
|
56
|
+
// deck was supposedly gone. #704 retired the mechanism outright, but the entry
|
|
57
|
+
// is still on every machine that had it, so removing it is still this
|
|
58
|
+
// command's job. So is the other half: turning the sound on parked the user's
|
|
59
|
+
// own afplay/PowerShell Stop hooks, and once the deck is uninstalled nothing
|
|
60
|
+
// else on the machine knows where they went.
|
|
61
|
+
// The login item goes too, and this is the one place the uninstall's
|
|
62
|
+
// documented narrowness has to bend. "Hook entries only" is right for the
|
|
63
|
+
// event log and the port registry, which are data somebody may still want —
|
|
64
|
+
// but a login item left behind after an uninstall is not data, it is a
|
|
65
|
+
// machine that keeps starting a deck whose hooks were just removed.
|
|
66
|
+
{
|
|
67
|
+
const svc = await import(pathToFileURL(join(PKG_ROOT, "src/server/login-service.mjs")).href);
|
|
68
|
+
const { deckDataDir: dataDir } = await import(pathToFileURL(join(PKG_ROOT, "src/server/deck-home.mjs")).href);
|
|
69
|
+
if (svc.readServiceRecord(dataDir()) !== null) {
|
|
70
|
+
const gone = svc.uninstallService();
|
|
71
|
+
svc.writeServiceRecord(dataDir(), removedRecord());
|
|
72
|
+
// Silent when there was nothing on the machine: an uninstall that reports
|
|
73
|
+
// removing a login item this deck never had is the same lie the explicit
|
|
74
|
+
// command used to tell, in the place a reader is least able to check it.
|
|
75
|
+
if (!gone.ok) {
|
|
76
|
+
// `gDash`, not an em dash: this runs before the boot, on a console that
|
|
77
|
+
// may be the legacy Windows one, and #797 is the rule that a printed
|
|
78
|
+
// string never carries punctuation the terminal may not have.
|
|
79
|
+
console.error(`${PRODUCT}: could NOT remove the login item ${gDash} ${gone.reason} (${gone.path})`);
|
|
80
|
+
} else if (gone.existed) {
|
|
81
|
+
console.log(`${PRODUCT}: no longer starts at login`);
|
|
82
|
+
}
|
|
83
|
+
if (!gone.ok) refused = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { retireSoundHook } = await import(pathToFileURL(join(PKG_ROOT, "src/server/retire-sound-hook.mjs")).href);
|
|
88
|
+
const sound = await retireSoundHook();
|
|
89
|
+
if (sound.removed) console.log(`${PRODUCT}: sound hook removed`);
|
|
90
|
+
if (sound.restored) console.log(`${PRODUCT}: restored ${sound.restored} of your own sound hook(s)`);
|
|
91
|
+
if (sound.ok === false) {
|
|
92
|
+
refused = true;
|
|
93
|
+
// Two different refusals, and saying the wrong one sends the user to the
|
|
94
|
+
// wrong file. `settings_unreadable` means nothing was touched at all — and
|
|
95
|
+
// when the forwarders already named that same file, the whole path and
|
|
96
|
+
// parser error would only bury the one line that differs. `parked_unreadable`
|
|
97
|
+
// is the other file: our entry IS out (the lines above said so), and what is
|
|
98
|
+
// still owed is the user's own hooks, which stay parked until they repair it.
|
|
99
|
+
if (sound.reason === "settings_unreadable") {
|
|
100
|
+
console.error(named.has(sound.settingsPath)
|
|
101
|
+
? `${PRODUCT}: the sound hook is still in that file too.`
|
|
102
|
+
// `gDash`, like every other line here (#797, #1431).
|
|
103
|
+
: `${PRODUCT}: sound hook left in place ${gDash} ${sound.message}`);
|
|
104
|
+
named.add(sound.settingsPath);
|
|
105
|
+
} else {
|
|
106
|
+
console.error(`${PRODUCT}: your own sound hooks were NOT restored ${gDash} ${sound.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (hasCodexInstalled()) {
|
|
110
|
+
report(await uninstallHooks({ provider: "codex" }), "Codex");
|
|
111
|
+
}
|
|
112
|
+
// ── THE PRIVATE KEY ────────────────────────────────────────────────────────
|
|
113
|
+
//
|
|
114
|
+
// The one thing left on the disk that is a CREDENTIAL rather than data. Every
|
|
115
|
+
// deck paired with this one has pinned the key in prefs.json, and until #959
|
|
116
|
+
// nothing in this command — or in the `--help` text, or in the README's
|
|
117
|
+
// uninstall paragraph — named the file, so somebody who followed the
|
|
118
|
+
// instructions to the letter believed the machine was clean and it was not.
|
|
119
|
+
//
|
|
120
|
+
// NAMED, NOT REMOVED, unless somebody asked. prefs.json also holds the
|
|
121
|
+
// pairings, the aliases and which accounts this deck offers, and deleting a
|
|
122
|
+
// user's settings out from under a command documented as "hook entries only"
|
|
123
|
+
// is a different complaint of the same size. So the default is to say exactly
|
|
124
|
+
// where the key is — resolved for THIS machine, both copies, because
|
|
125
|
+
// `migrateDeckFiles` copies rather than moves and the old one is in a
|
|
126
|
+
// directory nothing calls the deck's state — and `--purge` is the sentence
|
|
127
|
+
// somebody types when they mean it.
|
|
128
|
+
{
|
|
129
|
+
const { keyDirs, findKeyFiles, purgeKeyFiles } =
|
|
130
|
+
await import(pathToFileURL(join(PKG_ROOT, "src/server/purge-key.mjs")).href);
|
|
131
|
+
const found = await findKeyFiles(keyDirs());
|
|
132
|
+
if (flags.purge) {
|
|
133
|
+
const { removed, failed } = await purgeKeyFiles(found);
|
|
134
|
+
for (const path of removed) console.log(`${PRODUCT}: removed ${path}`);
|
|
135
|
+
for (const f of failed) {
|
|
136
|
+
refused = true;
|
|
137
|
+
// `gDash`, not an em dash: before the boot, possibly the legacy Windows
|
|
138
|
+
// console (#797).
|
|
139
|
+
console.error(`${PRODUCT}: could NOT remove ${f.path} ${gDash} ${f.why}`);
|
|
140
|
+
console.error(`${PRODUCT}: this deck's LAN private key is still in that file.`);
|
|
141
|
+
}
|
|
142
|
+
if (removed.length === 0 && failed.length === 0) {
|
|
143
|
+
console.log(`${PRODUCT}: no ${PRODUCT} state to purge`);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// "no-key" files are not mentioned. Telling somebody their private key is
|
|
147
|
+
// on the machine when the file has none is the same defect as the silence,
|
|
148
|
+
// pointing the other way. "unknown" IS mentioned: an unparseable file is
|
|
149
|
+
// the one nobody can rule a key out of.
|
|
150
|
+
const holding = found.filter(f => f.holds !== "no-key");
|
|
151
|
+
if (holding.length > 0) {
|
|
152
|
+
console.log(`${PRODUCT}: this deck's LAN private key is still on this machine:`);
|
|
153
|
+
for (const f of holding) {
|
|
154
|
+
console.log(`${PRODUCT}: ${f.path}${f.holds === "unknown" ? ` (unreadable ${gDash} ${f.why})` : ""}`);
|
|
155
|
+
}
|
|
156
|
+
console.log(`${PRODUCT}: every deck you paired with has pinned that key. \`${COMMAND} --uninstall --purge\` removes the file(s) above.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The remedy last and once, after every symptom above it, rather than once
|
|
162
|
+
// per refusal in the middle of the list.
|
|
163
|
+
if (named.size > 0) {
|
|
164
|
+
console.error(`${PRODUCT}: repair the JSON (or move the file aside), then run \`${COMMAND} --uninstall\` again.`);
|
|
165
|
+
}
|
|
166
|
+
// Non-zero when any half of it refused, so `ccdeck --uninstall && …` and every
|
|
167
|
+
// CI step that runs this stops on the failure instead of continuing past it.
|
|
168
|
+
return refused ? 1 : 0;
|
|
169
|
+
}
|