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,506 @@
|
|
|
1
|
+
// Which install this running copy is, and what would update it.
|
|
2
|
+
//
|
|
3
|
+
// These lived in src/server/self-update.mjs, between the version comparison and
|
|
4
|
+
// the registry check: the three sections that ask a package directory who it is
|
|
5
|
+
// — its own manifest, the npx cache it may sit in, the package it may be nested
|
|
6
|
+
// inside — and the three answers built on them: the package an upgrade installs,
|
|
7
|
+
// the one npm is asked about, and the line the user can paste. None of it talks
|
|
8
|
+
// to the network or keeps any state, so it moved to a leaf of its own.
|
|
9
|
+
// self-update.mjs imports what it calls and re-exports every name, so
|
|
10
|
+
// bin/agent-dag.js, invoked-as.mjs and the login item still import them from
|
|
11
|
+
// there. The code is unchanged.
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
// ── what is on disk ──────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** A package directory's own manifest, or null when there is not one worth
|
|
18
|
+
* reading there.
|
|
19
|
+
*
|
|
20
|
+
* Shared by everything below that asks a directory who it is — the npx cache
|
|
21
|
+
* directory's record of the spec included — so its callers cannot drift on
|
|
22
|
+
* what a missing, truncated or non-object package.json means. A JSON document
|
|
23
|
+
* is not necessarily an object — `null`, `"ccdeck"` and `[]` all parse — and a
|
|
24
|
+
* manifest that is not an object has no fields to read, so it is refused here
|
|
25
|
+
* once rather than guarded against at every caller. */
|
|
26
|
+
function readManifest(dir) {
|
|
27
|
+
try {
|
|
28
|
+
const meta = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
29
|
+
// An array is an object to `typeof`, so it is refused by name.
|
|
30
|
+
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Version currently written in the package's own package.json. Deliberately
|
|
37
|
+
* read fresh on every call: that is the whole point — it changes under a
|
|
38
|
+
* running process when npm replaces the install.
|
|
39
|
+
*
|
|
40
|
+
* And sometimes the directory changes with it. A `npm i -g ccdeck` performed
|
|
41
|
+
* before #340 left the deck nested inside a launcher package; upgrading such an
|
|
42
|
+
* install now writes the flat tarball over that launcher, and npm's reify takes
|
|
43
|
+
* the nested copy — the one this process is running out of — with it. Reading
|
|
44
|
+
* our own manifest then answers null, which pickNotice reads as "nothing is
|
|
45
|
+
* installed" and turns into the same upgrade offered forever, with the restart
|
|
46
|
+
* notice this module exists for never firing at all.
|
|
47
|
+
*
|
|
48
|
+
* The version is not lost, it moved one directory up. successorRoot is where
|
|
49
|
+
* to, and it answers null in every layout where nothing moved — so the normal
|
|
50
|
+
* case still reads exactly one manifest. */
|
|
51
|
+
export function installedVersion(pkgRoot) {
|
|
52
|
+
const own = readManifest(pkgRoot);
|
|
53
|
+
if (typeof own?.version === "string") return own.version;
|
|
54
|
+
const moved = successorRoot(pkgRoot);
|
|
55
|
+
const v = moved ? readManifest(moved)?.version : null;
|
|
56
|
+
return typeof v === "string" ? v : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** npx keeps each package in its own content-addressed cache directory, so
|
|
60
|
+
* `npm i -g` is the wrong advice there — nothing global exists to upgrade. */
|
|
61
|
+
export function isNpxInstall(pkgRoot) {
|
|
62
|
+
// Split on both separators, not `path.sep`: Windows paths reach here with
|
|
63
|
+
// backslashes but a POSIX-style path is still a valid input there, and a
|
|
64
|
+
// separator-agnostic test is what keeps this identical on all three
|
|
65
|
+
// platforms. Segment-wise, so a directory merely named "my_npx-tools" is not
|
|
66
|
+
// mistaken for the npx cache.
|
|
67
|
+
return typeof pkgRoot === "string" && pkgRoot.split(/[\\/]/).includes("_npx");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A git checkout is the maintainer's own tree. Its version routinely sits
|
|
71
|
+
* ahead of npm, and telling someone to `npm i -g` over their working copy is
|
|
72
|
+
* actively wrong, so the registry side of the check is skipped there.
|
|
73
|
+
*
|
|
74
|
+
* Exported for one caller outside the self-update modules: the login item,
|
|
75
|
+
* which must not be installed from a checkout for the same reason it must not
|
|
76
|
+
* be installed from an npx run — the path is not one anybody promised to keep.
|
|
77
|
+
* Everything else it gates stays internal to them, and each of those is already
|
|
78
|
+
* asserted against a directory with a real `.git` in it — `upgradeCommand`
|
|
79
|
+
* returning "git pull && npm run build", `upgradeName` refusing to move a
|
|
80
|
+
* checkout onto a published alias, and `startUpgrade` refusing with
|
|
81
|
+
* `git_checkout`. A test importing the predicate would restate what those three
|
|
82
|
+
* already prove, one level further from the behaviour a user can see.
|
|
83
|
+
*
|
|
84
|
+
* `existsSync` and not a directory test, deliberately (#587). Git writes `.git`
|
|
85
|
+
* as a directory only for an ordinary clone; a linked worktree and a submodule
|
|
86
|
+
* each get a FILE whose whole content is one `gitdir:` line, and all three are
|
|
87
|
+
* checkouts nobody may install over. Nothing here reads that line, which is
|
|
88
|
+
* also what keeps the rule identical on Windows, where the path inside it
|
|
89
|
+
* carries a drive letter and backslashes. Both shapes are covered in
|
|
90
|
+
* worktree-git-file-587.test.ts and beside every checkout fixture in the
|
|
91
|
+
* suite — narrowing this to `.isDirectory()` fails them. */
|
|
92
|
+
export function isGitCheckout(pkgRoot) {
|
|
93
|
+
try { return existsSync(join(pkgRoot, ".git")); } catch { return false; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── npx ──────────────────────────────────────────────────────────────────────
|
|
97
|
+
//
|
|
98
|
+
// An npx run lives in ~/.npm/_npx/<hash>/node_modules/<pkg>. The hash is over
|
|
99
|
+
// the SPEC the user typed, so upgrading means fetching a different directory —
|
|
100
|
+
// there is nothing to install over. What there IS, is the spec itself: npm
|
|
101
|
+
// writes it into <hash>/package.json as `_npx.packages`, which is the only
|
|
102
|
+
// record of whether the user typed `ccdeck`, `agent-dag` or `agents-deck`.
|
|
103
|
+
// Re-running the wrong one would work but would leave them on a package they
|
|
104
|
+
// never asked for, so it is worth reading rather than guessing.
|
|
105
|
+
|
|
106
|
+
/** The separator to join a path's segments back with: the one the input used,
|
|
107
|
+
* because a Windows path must come back as one. Backslashes only when the
|
|
108
|
+
* path has no forward slash at all — POSIX-style input is valid on Windows
|
|
109
|
+
* too. Shared by npxRoot and hostRoot, the two that cut a path apart and
|
|
110
|
+
* hand a piece of it back. */
|
|
111
|
+
function sepOf(path) {
|
|
112
|
+
return path.includes("\\") && !path.includes("/") ? "\\" : "/";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The `_npx/<hash>` directory this package was unpacked into, or null. Pure —
|
|
116
|
+
* path arithmetic only, so both platforms' separators can be tested. */
|
|
117
|
+
export function npxRoot(pkgRoot) {
|
|
118
|
+
if (typeof pkgRoot !== "string") return null;
|
|
119
|
+
const parts = pkgRoot.split(/[\\/]/);
|
|
120
|
+
const i = parts.lastIndexOf("_npx");
|
|
121
|
+
if (i === -1 || i + 1 >= parts.length) return null;
|
|
122
|
+
return parts.slice(0, i + 2).join(sepOf(pkgRoot));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Package name out of an npm spec, scope intact: `ccdeck@1.2.3` → `ccdeck`,
|
|
126
|
+
* `@scope/pkg` → `@scope/pkg`. Null for anything that is not a plain name —
|
|
127
|
+
* a tarball URL or a git spec is not something to re-run with `@latest`. */
|
|
128
|
+
export function bareSpecName(spec) {
|
|
129
|
+
if (typeof spec !== "string") return null;
|
|
130
|
+
const s = spec.trim();
|
|
131
|
+
if (!s) return null;
|
|
132
|
+
const at = s.lastIndexOf("@");
|
|
133
|
+
const name = at > 0 ? s.slice(0, at) : s;
|
|
134
|
+
return /^@?[a-z0-9][a-z0-9._-]*(\/[a-z0-9][a-z0-9._-]*)?$/i.test(name) ? name : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** What to hand `npx -y`, read from the cache directory's own metadata.
|
|
138
|
+
*
|
|
139
|
+
* `fallback` is what to answer when that metadata names nothing usable — the
|
|
140
|
+
* package name, for the caller whose job is to re-run SOMETHING. Pass null and
|
|
141
|
+
* the answer is null instead, which is what invoked-as.mjs needs: it asks which
|
|
142
|
+
* name the user typed, and there the package name is not a lesser answer, it is
|
|
143
|
+
* a wrong one. */
|
|
144
|
+
export function npxSpecFromMeta(meta, fallback = PUBLISHED_NAME) {
|
|
145
|
+
const list = meta && meta._npx && Array.isArray(meta._npx.packages) ? meta._npx.packages : [];
|
|
146
|
+
for (const entry of list) {
|
|
147
|
+
const name = bareSpecName(entry);
|
|
148
|
+
if (name) return `${name}@latest`;
|
|
149
|
+
}
|
|
150
|
+
return fallback ? `${fallback}@latest` : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The same, answered against the filesystem. Null when this is not an npx run,
|
|
154
|
+
* and — for a caller that passed no fallback — when the metadata cannot be
|
|
155
|
+
* read. */
|
|
156
|
+
export function npxRestartSpec(pkgRoot, name = PUBLISHED_NAME) {
|
|
157
|
+
const root = npxRoot(pkgRoot);
|
|
158
|
+
if (!root) return null;
|
|
159
|
+
return npxSpecFromMeta(readManifest(root), name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── the package that installed us ────────────────────────────────────────────
|
|
163
|
+
//
|
|
164
|
+
// Two of the three names on npm are this tarball republished; the third is not
|
|
165
|
+
// this tarball at all. `ccdeck` is a stub that ships nothing but bin/ and
|
|
166
|
+
// depends on `agents-deck`, so `npm i -g ccdeck` produces a layout no other
|
|
167
|
+
// install does: npm stopped hoisting a global package's dependencies in v7, so
|
|
168
|
+
// the deck lands at `<prefix>/lib/node_modules/ccdeck/node_modules/agents-deck`
|
|
169
|
+
// and the process runs out of a directory owned by a package it is not named
|
|
170
|
+
// after.
|
|
171
|
+
//
|
|
172
|
+
// #351 taught the stub to FIND the deck there. Nothing downstream learned about
|
|
173
|
+
// it, so every self-update surface went on naming `agents-deck` — which in that
|
|
174
|
+
// layout resolves to `<prefix>/lib/node_modules/agents-deck`, a directory this
|
|
175
|
+
// process never reads and a `npm i -g ccdeck` user usually does not have at
|
|
176
|
+
// all. "Update now" therefore installed a second, unrelated tree: the running
|
|
177
|
+
// install never moved, so installedVersion stayed put, the restart notice never
|
|
178
|
+
// came, and the banner offered the identical update forever while the upgrade
|
|
179
|
+
// reported "done".
|
|
180
|
+
//
|
|
181
|
+
// So the name to install is not the name this build was PUBLISHED under, it is
|
|
182
|
+
// the name the install was REACHED under — and under the stub layout that is
|
|
183
|
+
// written on disk one directory up. Read from there rather than from argv[1]
|
|
184
|
+
// (invoked-as.mjs answers the neighbouring question of which command the user
|
|
185
|
+
// TYPED, and cannot answer this one): the stub spawns the deck by absolute
|
|
186
|
+
// path, so argv[1] reads `agent-dag.js` under all three names, and on Windows
|
|
187
|
+
// npm's .cmd/.ps1 shims never pass the typed name on at all. The directory npm
|
|
188
|
+
// built is the one carrier that survives every platform.
|
|
189
|
+
//
|
|
190
|
+
// The other two names need no layout at all, and that is why the first fix
|
|
191
|
+
// missed one of them. `agents-deck` and `agent-dag` are the same tarball
|
|
192
|
+
// published twice with the manifest renamed between the two, so the deck IS the
|
|
193
|
+
// whole package under both and sits directly under the global node_modules with
|
|
194
|
+
// nothing above it — hostPackage correctly finds no host, and the answer falls
|
|
195
|
+
// through to a default that is right for one of the two and wrong for the
|
|
196
|
+
// other. What both of them do carry is their own package.json, which npm's
|
|
197
|
+
// rename made authoritative: see installedName.
|
|
198
|
+
|
|
199
|
+
/** Every name an install of this deck can have been REACHED under.
|
|
200
|
+
*
|
|
201
|
+
* Only `ccdeck` is published now. `agents-deck` and `agent-dag` were this
|
|
202
|
+
* tarball published twice more, then (from 3.22.3) small packages that
|
|
203
|
+
* depended on ccdeck, and both stay on the registry at their last version — so
|
|
204
|
+
* a deck can still be running inside one of them, or out of an npx run of one
|
|
205
|
+
* of them, and this list is what lets it recognise that. It is not the list of
|
|
206
|
+
* names to ask npm about: see PUBLISHED_NAME. The same three strings as
|
|
207
|
+
* invoked-as.mjs's COMMANDS, and deliberately not that list: this is the set
|
|
208
|
+
* of npm PACKAGES an install may belong to, that is the set of commands a user
|
|
209
|
+
* may have typed. A test pins them against each other rather than either side
|
|
210
|
+
* assuming it. */
|
|
211
|
+
export const ALIAS_PACKAGES = ["agents-deck", "agent-dag", "ccdeck"];
|
|
212
|
+
|
|
213
|
+
/** The one name the deck is published under, and so the only package whose
|
|
214
|
+
* dist-tag still moves. */
|
|
215
|
+
export const PUBLISHED_NAME = "ccdeck";
|
|
216
|
+
|
|
217
|
+
/** The names it is no longer published under. */
|
|
218
|
+
export const RETIRED_NAMES = ["agents-deck", "agent-dag"];
|
|
219
|
+
|
|
220
|
+
/** The published name in place of a retired one; any other name unchanged. */
|
|
221
|
+
export function currentName(name) {
|
|
222
|
+
return RETIRED_NAMES.includes(name) ? PUBLISHED_NAME : name;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Which of those three THIS build was published as, out of its own manifest.
|
|
226
|
+
*
|
|
227
|
+
* The other half of the same bug, and the easier half. `agent-dag` is not a
|
|
228
|
+
* stub and is not nested inside anything — `npm i -g agent-dag` puts the deck
|
|
229
|
+
* straight into `<prefix>/lib/node_modules/agent-dag` — so hostPackage finds no
|
|
230
|
+
* host above it and there is no layout to read the answer out of. There does
|
|
231
|
+
* not need to be: CI publishes that name by renaming the manifest in the
|
|
232
|
+
* tarball (`npm pkg set name=agent-dag` in .github/workflows/publish.yml), so
|
|
233
|
+
* the running copy's own package.json says `agent-dag` and has all along.
|
|
234
|
+
*
|
|
235
|
+
* It was never read. `name` is a parameter with a default in every exported
|
|
236
|
+
* function here, and not one caller in the deck passes it — index.mjs and
|
|
237
|
+
* bin/deck.js both call versionReport and startUpgrade with a pkgRoot and
|
|
238
|
+
* nothing else — so the default WAS the answer, and the default is a guess
|
|
239
|
+
* about which of three names this build carries. For `npm i -g agent-dag` it
|
|
240
|
+
* guessed wrong: the user was told to run `npm i -g agents-deck@latest`, which
|
|
241
|
+
* installs a second, unrelated global package while their `agent-dag` binary
|
|
242
|
+
* stays exactly where it was, and the version check went on caching under a
|
|
243
|
+
* package this install is not.
|
|
244
|
+
*
|
|
245
|
+
* Confined to the three names, for the reason hostNameFromMeta is: the answer
|
|
246
|
+
* becomes an argument in the `npm i -g` this process spawns, and startUpgrade
|
|
247
|
+
* promises that vector can only ever name this deck. A fork that republishes
|
|
248
|
+
* under a fourth name adds it to the list above, which is one line and a
|
|
249
|
+
* deliberate one — rather than having the deck hand npm a package name it has
|
|
250
|
+
* never heard of because a directory on disk said so.
|
|
251
|
+
*
|
|
252
|
+
* `fallback` is what to answer when the manifest is missing, unreadable, or
|
|
253
|
+
* names something that is not one of ours — the caller's own `name`, so every
|
|
254
|
+
* shape that cannot prove which alias it is keeps the behaviour it had. */
|
|
255
|
+
export function installedName(pkgRoot, fallback = PUBLISHED_NAME) {
|
|
256
|
+
const self = readManifest(pkgRoot)?.name;
|
|
257
|
+
return typeof self === "string" && ALIAS_PACKAGES.includes(self) ? self : fallback;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The directory of the package this one is installed INSIDE, or null when it
|
|
261
|
+
* is not inside one. Pure — path arithmetic only, so a Windows layout can be
|
|
262
|
+
* tested on a POSIX box and both separators are split on, exactly as npxRoot
|
|
263
|
+
* does it and for the same reason. */
|
|
264
|
+
export function hostRoot(pkgRoot) {
|
|
265
|
+
if (typeof pkgRoot !== "string") return null;
|
|
266
|
+
const parts = pkgRoot.split(/[\\/]/);
|
|
267
|
+
// `<host>/node_modules/<us>` and nothing else: the segment directly above us
|
|
268
|
+
// has to be the node_modules npm nested us into, and the one above that is
|
|
269
|
+
// the host. A scoped package would sit one level deeper, under `@scope`, and
|
|
270
|
+
// is refused here rather than guessed at — none of the three names is scoped.
|
|
271
|
+
if (parts.length < 3 || parts[parts.length - 2] !== "node_modules") return null;
|
|
272
|
+
return parts.slice(0, -2).join(sepOf(pkgRoot)) || null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The host package's name, out of its own manifest — but only when it is one
|
|
276
|
+
* of this deck's aliases AND it declares a dependency on us.
|
|
277
|
+
*
|
|
278
|
+
* The alias half is the load-bearing one. Any project that lists `agents-deck`
|
|
279
|
+
* in its dependencies and runs it out of its own node_modules — a workspace, a
|
|
280
|
+
* CI job, a tool that embeds the deck — is in exactly the same shape on disk
|
|
281
|
+
* as the stub, and answering with THAT name would have the deck offering to
|
|
282
|
+
* `npm i -g their-app@latest`: a package it has no business installing, and on
|
|
283
|
+
* a private name one that does not exist. Confining the answer to the three
|
|
284
|
+
* published names leaves every such install with the fallback it has today. */
|
|
285
|
+
export function hostNameFromMeta(meta, name = PUBLISHED_NAME) {
|
|
286
|
+
const host = typeof meta?.name === "string" ? meta.name : null;
|
|
287
|
+
if (!host || !ALIAS_PACKAGES.includes(host)) return null;
|
|
288
|
+
return typeof meta?.dependencies?.[name] === "string" ? host : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The same, answered against the filesystem: `{ root, name }` for the alias
|
|
292
|
+
* package this copy was installed as a dependency of, or null. */
|
|
293
|
+
export function hostPackage(pkgRoot, name = PUBLISHED_NAME) {
|
|
294
|
+
const root = hostRoot(pkgRoot);
|
|
295
|
+
if (!root) return null;
|
|
296
|
+
const host = hostNameFromMeta(readManifest(root), name);
|
|
297
|
+
return host ? { root, name: host } : null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The directory that holds this install's code AFTER an upgrade replaced it,
|
|
302
|
+
* or null when nothing has been replaced.
|
|
303
|
+
*
|
|
304
|
+
* One layout produces this and it is a transitional one. Before #340, `npm i -g
|
|
305
|
+
* ccdeck` installed a launcher package with the deck nested inside it:
|
|
306
|
+
*
|
|
307
|
+
* <prefix>/lib/node_modules/ccdeck/ the launcher
|
|
308
|
+
* <prefix>/lib/node_modules/ccdeck/node_modules/agents-deck/ pkgRoot
|
|
309
|
+
*
|
|
310
|
+
* upgradeName reads the host's declared dependency and correctly answers
|
|
311
|
+
* `ccdeck`, so the upgrade runs `npm i -g ccdeck@latest` — which since #340
|
|
312
|
+
* installs the deck itself over the host directory and removes everything that
|
|
313
|
+
* was under it, including pkgRoot. The process keeps running (POSIX keeps an
|
|
314
|
+
* open inode alive, and the modules are already loaded) out of a directory that
|
|
315
|
+
* no longer exists.
|
|
316
|
+
*
|
|
317
|
+
* Deliberately NOT hostPackage. That function recognises a host by the
|
|
318
|
+
* dependency it declares on us, and the whole point here is that the host has
|
|
319
|
+
* just stopped declaring one — it is no longer a launcher, it is the deck. What
|
|
320
|
+
* identifies it instead is its name, confined to the three we publish, for the
|
|
321
|
+
* same reason installedName confines its answer: this decides what a version
|
|
322
|
+
* report says about the user's machine, and a directory that merely happens to
|
|
323
|
+
* sit above us is not evidence.
|
|
324
|
+
*
|
|
325
|
+
* Guarded on our own manifest being unreadable, so nothing changes for an
|
|
326
|
+
* install that is intact — except for the one replacement that leaves a
|
|
327
|
+
* readable manifest behind, which is nestedDeckRoot below.
|
|
328
|
+
*/
|
|
329
|
+
export function successorRoot(pkgRoot) {
|
|
330
|
+
if (readManifest(pkgRoot)) return nestedDeckRoot(pkgRoot);
|
|
331
|
+
const root = hostRoot(pkgRoot);
|
|
332
|
+
if (!root) return null;
|
|
333
|
+
const name = readManifest(root)?.name;
|
|
334
|
+
return typeof name === "string" && ALIAS_PACKAGES.includes(name) ? root : null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The other direction the same replacement can go: the deck moved DOWN, into a
|
|
339
|
+
* node_modules underneath the directory it used to be.
|
|
340
|
+
*
|
|
341
|
+
* `npm i -g agents-deck` before 3.22.3 installed the deck flat — that directory
|
|
342
|
+
* WAS the deck, bin/ and src/ and hook/ and all. What the registry serves for
|
|
343
|
+
* that name now is a 5 KB pointer package: a shim.js, a manifest, and a
|
|
344
|
+
* dependency on `ccdeck`. So reinstalling it replaces the deck with the pointer
|
|
345
|
+
* and puts the real deck one level down, in `<pkgRoot>/node_modules/ccdeck`.
|
|
346
|
+
*
|
|
347
|
+
* The guard above cannot see that, and that is the whole of #975. npm's reify
|
|
348
|
+
* deleted bin/ and src/ but WROTE a package.json in their place, so
|
|
349
|
+
* `readManifest(pkgRoot)` answers — with the shim's manifest — and every reader
|
|
350
|
+
* downstream reads "intact". successorRoot returned null, replacedNote was
|
|
351
|
+
* handed `moved: null` and stayed silent, and the supervisor went on spawning a
|
|
352
|
+
* bin/deck.js npm had just removed: five crash restarts in ten minutes and then
|
|
353
|
+
* a deck that stops for good, with nothing on screen about an upgrade.
|
|
354
|
+
*
|
|
355
|
+
* Three facts together, because none of them alone is evidence. Our name is one
|
|
356
|
+
* of the retired ones; our manifest declares a dependency on the published name,
|
|
357
|
+
* which a deck's own manifest never does; and the package that dependency names
|
|
358
|
+
* is really sitting under us. A deck that merely vendors something, or a
|
|
359
|
+
* retired-name package from before the pointer, matches none of them.
|
|
360
|
+
*/
|
|
361
|
+
function nestedDeckRoot(pkgRoot) {
|
|
362
|
+
const self = readManifest(pkgRoot);
|
|
363
|
+
if (!RETIRED_NAMES.includes(self?.name)) return null;
|
|
364
|
+
if (typeof self?.dependencies?.[PUBLISHED_NAME] !== "string") return null;
|
|
365
|
+
const root = join(pkgRoot, "node_modules", PUBLISHED_NAME);
|
|
366
|
+
return readManifest(root)?.name === PUBLISHED_NAME ? root : null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The retired package this install IS, when reinstalling that package would
|
|
371
|
+
* replace a working deck with the pointer npm now serves for it — or null,
|
|
372
|
+
* which is every other install shape on the planet.
|
|
373
|
+
*
|
|
374
|
+
* `npm i -g agents-deck` and `npm i -g agent-dag` at 3.22.1 or earlier put the
|
|
375
|
+
* deck straight into `<prefix>/lib/node_modules/<name>`: no host above it, no
|
|
376
|
+
* nested copy under it, that directory is the deck. registryName's exception
|
|
377
|
+
* asks npm about `ccdeck` for such an install and is right to — a retired name's
|
|
378
|
+
* dist-tag stopped moving — so the deck correctly learns that a newer version
|
|
379
|
+
* exists. What it then offers is `npm i -g agents-deck@latest`, and since 3.22.8
|
|
380
|
+
* that command fetches 5 KB of shim and deletes the deck to make room for it.
|
|
381
|
+
*
|
|
382
|
+
* The nested layout is the case registryName's comment describes and it is
|
|
383
|
+
* unaffected: there the retired package is the host, the deck is its dependency,
|
|
384
|
+
* and reinstalling the host re-resolves `ccdeck@^3` to the newest deck. The
|
|
385
|
+
* difference is entirely whether the retired name is the wrapper or the thing
|
|
386
|
+
* inside it, and a wrapper is a package — so the directory test below answers
|
|
387
|
+
* that too, before it is asked whose name is on it.
|
|
388
|
+
*
|
|
389
|
+
* Confined to the global layout — `<prefix>/lib/node_modules/<us>` on POSIX,
|
|
390
|
+
* `<prefix>/node_modules/<us>` on Windows — because that is the only tree
|
|
391
|
+
* `npm i -g` rewrites. The test for it is that the directory holding our
|
|
392
|
+
* node_modules is not a package: a global prefix's `lib` has no manifest, while
|
|
393
|
+
* a workspace, a CI job or any tool that vendors the deck as a dependency does.
|
|
394
|
+
* Reinstalling a retired name from inside one of THOSE writes a global tree the
|
|
395
|
+
* project never reads and leaves its copy exactly where it is — a different
|
|
396
|
+
* complaint, and not one to answer by telling somebody to uninstall.
|
|
397
|
+
*
|
|
398
|
+
* A checkout and an npx run are refused before this in every caller, and are
|
|
399
|
+
* refused here too, so the predicate is true on its own terms rather than only
|
|
400
|
+
* in the order it happens to be asked.
|
|
401
|
+
*/
|
|
402
|
+
export function frozenNameInstall(pkgRoot, name = PUBLISHED_NAME) {
|
|
403
|
+
if (isGitCheckout(pkgRoot) || isNpxInstall(pkgRoot)) return null;
|
|
404
|
+
const above = hostRoot(pkgRoot);
|
|
405
|
+
if (!above || readManifest(above)) return null;
|
|
406
|
+
const self = installedName(pkgRoot, name);
|
|
407
|
+
return RETIRED_NAMES.includes(self) ? self : null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** The package an upgrade would actually install here — and, but for a retired
|
|
411
|
+
* name, the one worth asking npm about (see registryName).
|
|
412
|
+
*
|
|
413
|
+
* The check used to ask about `agents-deck` no matter what the upgrade
|
|
414
|
+
* command installed, so a deck started with `npx ccdeck` compared its version
|
|
415
|
+
* against `agents-deck`'s dist-tag and then handed back `npx -y ccdeck@latest`.
|
|
416
|
+
* Nothing tied the two together. CI publishes the three names one after
|
|
417
|
+
* another, so between the first and the last publish they genuinely disagree,
|
|
418
|
+
* and inside that window the deck offered a version the command could not
|
|
419
|
+
* install — the ETARGET isPublished exists for, with a window measured in
|
|
420
|
+
* publishes rather than in seconds of propagation.
|
|
421
|
+
*
|
|
422
|
+
* Deriving the name from what would actually be installed keeps the two halves
|
|
423
|
+
* consistent by construction: whatever the command will install is what gets
|
|
424
|
+
* asked about, and the per-name marker follows the same name. Each install
|
|
425
|
+
* shape carries that answer somewhere different — npx in the spec it recorded,
|
|
426
|
+
* a stub install in the layout npm built, a plain global install in the
|
|
427
|
+
* manifest of the package it is. All three are read; none is assumed. */
|
|
428
|
+
export function upgradeName(pkgRoot, name = PUBLISHED_NAME) {
|
|
429
|
+
// The published name this build actually carries, which outranks `name` in
|
|
430
|
+
// every branch below because `name` is a default at every call site in the
|
|
431
|
+
// deck and the manifest on disk is not a guess. It replaces the parameter
|
|
432
|
+
// rather than sitting beside it: this one function is where all four of the
|
|
433
|
+
// registry-shaped answers meet — the dist-tag that is fetched, the marker it
|
|
434
|
+
// is cached in, the command the user is shown, and the argv npm is spawned
|
|
435
|
+
// with — so resolving the name once here is what keeps those four naming one
|
|
436
|
+
// package. registryName is the one exception, for a retired name, and it
|
|
437
|
+
// starts from this answer.
|
|
438
|
+
const self = installedName(pkgRoot, name);
|
|
439
|
+
// A checkout installs nothing at all, so the published name is the only
|
|
440
|
+
// sensible subject for the version question — and the only one whose
|
|
441
|
+
// dist-tag says anything about the branch the maintainer is sitting on.
|
|
442
|
+
if (isGitCheckout(pkgRoot)) return self;
|
|
443
|
+
// npx recorded the spec the user typed, which is a better answer than the
|
|
444
|
+
// manifest — except that a retired name answers `ccdeck`. npx reinstalls only
|
|
445
|
+
// when a spec resolves to a different version, and `agents-deck@latest`
|
|
446
|
+
// resolves to the same last-published package forever, so re-running it
|
|
447
|
+
// would reuse the cached copy, and the ccdeck inside it, and never move. The
|
|
448
|
+
// manifest is what is left when the record cannot be read.
|
|
449
|
+
if (isNpxInstall(pkgRoot)) return currentName(bareSpecName(npxRestartSpec(pkgRoot, self)) ?? self);
|
|
450
|
+
// Left: a global install, where the answer is whichever package owns the
|
|
451
|
+
// directory npm would rewrite. That is the stub for `npm i -g ccdeck`, where
|
|
452
|
+
// the deck is nested one level down inside a package it is not named after,
|
|
453
|
+
// and this build's own name for `npm i -g agents-deck` and `npm i -g
|
|
454
|
+
// agent-dag`, where the deck IS the whole package and nothing is above it.
|
|
455
|
+
//
|
|
456
|
+
// successorRoot is the third case, and it is the second half of the same
|
|
457
|
+
// question. Once that stub install HAS been upgraded, the host has stopped
|
|
458
|
+
// declaring a dependency on us — which is the only thing hostPackage
|
|
459
|
+
// recognises a host by — so this fell through to `self`, and `self` is the
|
|
460
|
+
// fallback `agents-deck` because our own manifest went with the directory.
|
|
461
|
+
// The user was then shown `npm i -g agents-deck@latest`: a different package,
|
|
462
|
+
// a second global tree, and their `ccdeck` binary left exactly where it was.
|
|
463
|
+
// That is #358 verbatim, arriving through the upgrade that was supposed to be
|
|
464
|
+
// the end of it. The successor's own manifest names it, and that name is what
|
|
465
|
+
// the next upgrade has to install.
|
|
466
|
+
const host = hostPackage(pkgRoot, self)?.name;
|
|
467
|
+
if (host) return host;
|
|
468
|
+
const moved = successorRoot(pkgRoot);
|
|
469
|
+
return moved ? installedName(moved, self) : self;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** The exact line the user can paste, for the way THIS copy was installed. */
|
|
473
|
+
export function upgradeCommand(pkgRoot, name = PUBLISHED_NAME) {
|
|
474
|
+
// A checkout is updated by pulling, and the bundle is built, not shipped —
|
|
475
|
+
// so `npm run build` is part of the answer rather than an afterthought.
|
|
476
|
+
if (isGitCheckout(pkgRoot)) return "git pull && npm run build";
|
|
477
|
+
if (isNpxInstall(pkgRoot)) return `npx -y ${upgradeName(pkgRoot, name)}@latest`;
|
|
478
|
+
// A flat install of a retired name cannot be updated by reinstalling itself —
|
|
479
|
+
// that fetches the pointer package and deletes the deck (frozenNameInstall).
|
|
480
|
+
// The command has to move them onto the published name, and the removal is
|
|
481
|
+
// not optional: the old package owns `<prefix>/bin/ccdeck` as well as its own
|
|
482
|
+
// command, so installing ccdeck alongside it is npm linking a bin over a file
|
|
483
|
+
// another package still claims.
|
|
484
|
+
const frozen = frozenNameInstall(pkgRoot, name);
|
|
485
|
+
if (frozen) return `npm rm -g ${frozen} && npm i -g ${PUBLISHED_NAME}`;
|
|
486
|
+
// Through upgradeName for the same reason the npx line above it is: the
|
|
487
|
+
// printed command is the user's escape hatch when the button fails or is not
|
|
488
|
+
// offered, and one that names a package this install cannot be replaced by is
|
|
489
|
+
// worse than none — it looks like it worked.
|
|
490
|
+
return `npm i -g ${upgradeName(pkgRoot, name)}@latest`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** The package to ask npm about: the one an upgrade installs, except that a
|
|
494
|
+
* retired name is asked about as `ccdeck`.
|
|
495
|
+
*
|
|
496
|
+
* One install reaches that exception. A deck sitting inside the old
|
|
497
|
+
* `agents-deck` or `agent-dag` package upgrades by reinstalling THAT package —
|
|
498
|
+
* installing `ccdeck` would write a tree this process never reads, #358 again —
|
|
499
|
+
* but that package's dist-tag stopped moving when it stopped being published.
|
|
500
|
+
* The reinstall is still the right act: the old package depends on
|
|
501
|
+
* `ccdeck@^3`, and npm resolves that to the newest ccdeck on every reinstall,
|
|
502
|
+
* even of the same version. So the version asked about is ccdeck's and the
|
|
503
|
+
* command names the old package — the one place the two differ, on purpose. */
|
|
504
|
+
export function registryName(pkgRoot, name = PUBLISHED_NAME) {
|
|
505
|
+
return currentName(upgradeName(pkgRoot, name));
|
|
506
|
+
}
|