ccdeck 1.43.0 → 1.45.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 +210 -12
- package/bin/agent-dag.js +505 -0
- package/bin/deck.js +1051 -0
- package/dist/web/assets/index-Bdl1LX0-.css +1 -0
- package/dist/web/assets/index-jXBjwwZC.js +89 -0
- package/dist/web/index.html +49 -0
- package/hook/hook.js +496 -0
- package/package.json +42 -9
- package/src/server/args.mjs +156 -0
- package/src/server/brand.mjs +16 -0
- package/src/server/ccusage.mjs +1052 -0
- package/src/server/claude-accounts.mjs +653 -0
- package/src/server/claude-dir.mjs +213 -0
- package/src/server/codex-auth.mjs +388 -0
- package/src/server/codex-dir.mjs +171 -0
- package/src/server/codex-quota.mjs +445 -0
- package/src/server/codex-usage.mjs +411 -0
- package/src/server/cswap-admin.mjs +1034 -0
- package/src/server/cswap-auto.mjs +595 -0
- package/src/server/cswap-install.mjs +583 -0
- package/src/server/exec.mjs +938 -0
- package/src/server/index.mjs +5109 -0
- package/src/server/installer.mjs +763 -0
- package/src/server/invoked-as.mjs +144 -0
- package/src/server/log-writer.mjs +283 -0
- package/src/server/npx.mjs +259 -0
- package/src/server/quota.mjs +684 -0
- package/src/server/reset-label.mjs +78 -0
- package/src/server/retire-sound-hook.mjs +315 -0
- package/src/server/self-update.mjs +1331 -0
- package/src/server/supervisor.mjs +302 -0
- package/src/server/system-metrics.mjs +619 -0
- package/src/server/term.mjs +570 -0
- package/src/server/uv-bootstrap.mjs +301 -0
- package/bin/ccdeck.js +0 -194
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// How to run npx from a supervisor that cannot afford npx to be broken.
|
|
2
|
+
//
|
|
3
|
+
// The supervisor's upgrade path is `npx -y <spec>@latest`, and it used to
|
|
4
|
+
// resolve npx by bare name — `npx.cmd` on Windows — letting the OS find it on
|
|
5
|
+
// PATH. That trusts a batch shim to be sitting in a real npm install root,
|
|
6
|
+
// because the shim computes everything else from its own directory:
|
|
7
|
+
//
|
|
8
|
+
// SET "NPX_CLI_JS=%~dp0\node_modules\npm\bin\npx-cli.js"
|
|
9
|
+
// SET "NPM_PREFIX_JS=%~dp0\node_modules\npm\bin\npm-prefix.js"
|
|
10
|
+
//
|
|
11
|
+
// Reported from a Windows machine on Node 24 where `%~dp0` was the user's home
|
|
12
|
+
// directory and no `node_modules\npm` existed under it. Clicking
|
|
13
|
+
// "Update & restart" printed a raw MODULE_NOT_FOUND stack trace and came back
|
|
14
|
+
// on the same version.
|
|
15
|
+
//
|
|
16
|
+
// The cause was guessed at here as "an npm global prefix pointed somewhere the
|
|
17
|
+
// shim did not follow", and #456 established it was not: `%~dp0` was the DECK'S
|
|
18
|
+
// WORKING DIRECTORY, because a shim asked for by bare name has no directory in
|
|
19
|
+
// its `%0` to be the drive-and-path of. Nothing about that machine's npm was
|
|
20
|
+
// wrong. The correction lives in shimPath in exec.mjs, and the shim fallback
|
|
21
|
+
// below now asks for the full path.
|
|
22
|
+
//
|
|
23
|
+
// Node ships npm, so npx's real entry point can be reached from
|
|
24
|
+
// `process.execPath` with no PATH lookup, no batch file, and no shim-relative
|
|
25
|
+
// arithmetic. That is what this prefers. The shim stays as the fallback for
|
|
26
|
+
// installs whose npm lives somewhere else entirely, and it still goes through
|
|
27
|
+
// spawnSpec — the cmd.exe quoting there is load-bearing and unchanged.
|
|
28
|
+
//
|
|
29
|
+
// The pre-flight fetch below goes through the very same launcher, for the same
|
|
30
|
+
// reason stated the other way round: a supervisor that cannot afford npx to be
|
|
31
|
+
// broken cannot afford to discover that it is only after it has stopped
|
|
32
|
+
// serving. So the fetch happens first, and only a fetch that worked costs the
|
|
33
|
+
// deck its listener.
|
|
34
|
+
import { spawn } from "node:child_process";
|
|
35
|
+
import { existsSync } from "node:fs";
|
|
36
|
+
import { killTree, shimPath, spawnSpec } from "./exec.mjs";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Where npm's own `npx-cli.js` sits relative to the running Node binary, most
|
|
40
|
+
* likely first. Pure: the platform and the executable path are parameters, so
|
|
41
|
+
* the Windows layout can be checked from any OS.
|
|
42
|
+
*
|
|
43
|
+
* Windows keeps npm beside node.exe (`C:\Program Files\nodejs\node_modules\npm`,
|
|
44
|
+
* and the same shape under nvm-windows). POSIX puts it in a `lib` next to the
|
|
45
|
+
* `bin` — usually the parent (`/usr/local/bin/node` →
|
|
46
|
+
* `/usr/local/lib/node_modules/npm`, as for nvm, fnm and Volta), but not
|
|
47
|
+
* always: Homebrew's node lives in `/usr/local/Cellar/node/<v>/bin` while its
|
|
48
|
+
* npm stays at the prefix, four levels up. So the `lib` spelling is tried at
|
|
49
|
+
* each of the first four ancestors rather than only at the parent. Every
|
|
50
|
+
* candidate is confirmed by the exact file existing, so a wider search cannot
|
|
51
|
+
* pick something that is not npm.
|
|
52
|
+
*
|
|
53
|
+
* The path arithmetic is done on the string rather than through `node:path`,
|
|
54
|
+
* for the same reason npxRoot in self-update.mjs does: `path` here is the
|
|
55
|
+
* platform running the SUITE, so a Windows layout checked from macOS would come
|
|
56
|
+
* back with forward slashes and a `..` nothing resolves.
|
|
57
|
+
*/
|
|
58
|
+
export function npxCliCandidates(execPath = process.execPath, platform = process.platform) {
|
|
59
|
+
if (typeof execPath !== "string" || !execPath) return [];
|
|
60
|
+
const sep = platform === "win32" ? "\\" : "/";
|
|
61
|
+
const dir = execPath.split(/[\\/]/);
|
|
62
|
+
dir.pop(); // the binary itself
|
|
63
|
+
if (!dir.length) return [];
|
|
64
|
+
const beside = [...dir, "node_modules", "npm", "bin", "npx-cli.js"].join(sep);
|
|
65
|
+
const above = [];
|
|
66
|
+
for (let up = 1; up <= 4 && dir.length - up > 0; up++) {
|
|
67
|
+
above.push([...dir.slice(0, -up), "lib", "node_modules", "npm", "bin", "npx-cli.js"].join(sep));
|
|
68
|
+
}
|
|
69
|
+
return platform === "win32" ? [beside, ...above] : [...above, beside];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What to hand `spawn` to run `npx <args>`.
|
|
74
|
+
*
|
|
75
|
+
* Returns `{ file, args, opts, via }`, where `via` is "node" for the bundled
|
|
76
|
+
* CLI and "shim" for the PATH lookup. `exists` is injected so the choice can be
|
|
77
|
+
* tested without a matching Node install on the machine running the suite.
|
|
78
|
+
*/
|
|
79
|
+
export function npxLaunch(args, {
|
|
80
|
+
platform = process.platform,
|
|
81
|
+
execPath = process.execPath,
|
|
82
|
+
exists = existsSync,
|
|
83
|
+
pathEnv,
|
|
84
|
+
} = {}) {
|
|
85
|
+
for (const cli of npxCliCandidates(execPath, platform)) {
|
|
86
|
+
let found = false;
|
|
87
|
+
try { found = exists(cli); } catch { found = false; }
|
|
88
|
+
// Same node, same npm, spawned directly: nothing here reads PATH and
|
|
89
|
+
// nothing resolves a path relative to a shim's own directory.
|
|
90
|
+
if (found) return { file: execPath, args: [cli, ...args], opts: {}, via: "node", cli };
|
|
91
|
+
}
|
|
92
|
+
// The shim, and on Windows by its FULL path. A bare `npx.cmd` is what made
|
|
93
|
+
// the shim compute `%~dp0` — where it looks for npx-cli.js — from the deck's
|
|
94
|
+
// working directory rather than from its own, which is #456; shimPath in
|
|
95
|
+
// exec.mjs carries the account. The bare name stays as the last resort, so a
|
|
96
|
+
// layout the lookup cannot see is exactly as well off as it was before.
|
|
97
|
+
const shim = platform === "win32"
|
|
98
|
+
? (shimPath("npx.cmd", { execPath, exists, ...(pathEnv === undefined ? {} : { pathEnv }) }) ?? "npx.cmd")
|
|
99
|
+
: "npx";
|
|
100
|
+
return { ...spawnSpec(shim, args, platform), via: "shim", cli: null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Long enough for a cold tarball on a slow line; short enough that a connection
|
|
104
|
+
// hanging on a dead proxy does not hold the deck's Update button for the three
|
|
105
|
+
// minutes the tab waits before giving up on its own.
|
|
106
|
+
//
|
|
107
|
+
// Not exported (#383): it is the default of `prefetch`'s `timeoutMs` parameter
|
|
108
|
+
// and nothing else. A caller that wants a different budget passes one, and a
|
|
109
|
+
// caller that wants this one passes nothing — there is no third caller for the
|
|
110
|
+
// number to be worth naming across the module boundary.
|
|
111
|
+
const PREFETCH_TIMEOUT_MS = 120_000;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* What to hand npx to DOWNLOAD a spec without running it.
|
|
115
|
+
*
|
|
116
|
+
* The reason this is not simply `["-y", spec]`: that form is the upgrade
|
|
117
|
+
* itself. npx resolves, unpacks AND executes the package's bin in one command,
|
|
118
|
+
* so asking it "can you fetch this?" the obvious way starts a second deck —
|
|
119
|
+
* which either binds the port the running one holds or, worse, does not, and
|
|
120
|
+
* registers itself with the hooks anyway. Both are outages of their own.
|
|
121
|
+
*
|
|
122
|
+
* `--package` is the half that installs; `--call` is the half that runs, and
|
|
123
|
+
* pointing it at a shell builtin is what makes the install the only effect. The
|
|
124
|
+
* spec never appears as a positional argument, so no bin of ours is ever named,
|
|
125
|
+
* let alone started.
|
|
126
|
+
*
|
|
127
|
+
* `exit 0` rather than anything more informative on purpose: npm runs the call
|
|
128
|
+
* through its script shell — `sh -c` on POSIX, cmd.exe on Windows — and both
|
|
129
|
+
* spell that builtin the same way. Anything that needed a program on PATH would
|
|
130
|
+
* fail exactly where PATH is broken, which is the environment from #184 this
|
|
131
|
+
* pre-flight most needs to answer honestly.
|
|
132
|
+
*/
|
|
133
|
+
export function npxPrefetchArgs(spec) {
|
|
134
|
+
return ["-y", "--package", spec, "--call", "exit 0"];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve, download and unpack `spec` while the current deck keeps serving.
|
|
139
|
+
*
|
|
140
|
+
* Answers `{ ok, error, hint }` — never throws, because the caller's whole
|
|
141
|
+
* reason for asking is that it must survive the answer being "no". A refusal
|
|
142
|
+
* here costs the click; the alternative, which is what shipped before, was to
|
|
143
|
+
* exit the worker first and discover offline, a proxy, an ETARGET or a broken
|
|
144
|
+
* npx shim with the port already given up and the SSE stream already dropped.
|
|
145
|
+
*
|
|
146
|
+
* Nothing is echoed: the deck this runs alongside owns the terminal and is
|
|
147
|
+
* still drawing its pulse line over the last row, so npm's progress output
|
|
148
|
+
* would smear across it. The tail is kept only as evidence for a failure that
|
|
149
|
+
* may not happen, and reduced to one line by npxFailureSummary when it does.
|
|
150
|
+
*
|
|
151
|
+
* `spawnFn` and `launch` are injected so the choice of launcher and the stdio
|
|
152
|
+
* it is given can be asserted without a Node install to match, and `onChild`
|
|
153
|
+
* hands the process out so a Ctrl+C arriving mid-fetch has something to kill.
|
|
154
|
+
*/
|
|
155
|
+
export function npxPrefetch(spec, {
|
|
156
|
+
launch = npxLaunch,
|
|
157
|
+
spawnFn = spawn,
|
|
158
|
+
timeoutMs = PREFETCH_TIMEOUT_MS,
|
|
159
|
+
onChild = null,
|
|
160
|
+
} = {}) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const { file, args, opts } = launch(npxPrefetchArgs(spec));
|
|
163
|
+
let child;
|
|
164
|
+
try {
|
|
165
|
+
child = spawnFn(file, args, { stdio: ["ignore", "pipe", "pipe"], ...opts });
|
|
166
|
+
} catch (err) {
|
|
167
|
+
resolve({ ok: false, error: `could not run npx: ${err?.message ?? err}`, hint: null });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
onChild?.(child);
|
|
171
|
+
|
|
172
|
+
let tail = "";
|
|
173
|
+
const keep = (d) => { tail = (tail + String(d)).slice(-8000); };
|
|
174
|
+
// Both streams, one buffer: npm writes its errors to stderr and its
|
|
175
|
+
// resolution notices to stdout, and which of the two carries the sentence
|
|
176
|
+
// worth quoting depends on the failure.
|
|
177
|
+
child.stdout?.on("data", keep);
|
|
178
|
+
child.stderr?.on("data", keep);
|
|
179
|
+
|
|
180
|
+
let settled = false;
|
|
181
|
+
const settle = (result) => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
settled = true;
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
resolve(result);
|
|
186
|
+
};
|
|
187
|
+
const timer = setTimeout(() => {
|
|
188
|
+
// killTree, not kill: on Windows the shim path puts cmd.exe between us
|
|
189
|
+
// and npm, so a plain kill stops the shell and leaves the download
|
|
190
|
+
// running — writing into the very cache directory the next attempt reads.
|
|
191
|
+
killTree(child);
|
|
192
|
+
settle({ ok: false, error: `fetching ${spec} timed out after ${Math.round(timeoutMs / 1000)}s`, hint: null });
|
|
193
|
+
}, timeoutMs);
|
|
194
|
+
timer.unref?.();
|
|
195
|
+
|
|
196
|
+
child.on("error", err => settle({ ok: false, error: `could not run npx: ${err?.message ?? err}`, hint: null }));
|
|
197
|
+
child.on("exit", (code, signal) => {
|
|
198
|
+
if (code === 0) { settle({ ok: true, error: null, hint: null }); return; }
|
|
199
|
+
settle({
|
|
200
|
+
ok: false,
|
|
201
|
+
error: npxFailureSummary(tail) ?? `npx could not fetch ${spec} — exited ${code ?? signal}`,
|
|
202
|
+
hint: npxFailureHint(tail),
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Lines that are structure rather than content: a Node crash prints a stack,
|
|
209
|
+
// the source line it threw on, a caret under it, then the error object's own
|
|
210
|
+
// fields and the runtime version. None of that tells a user of a DAG dashboard
|
|
211
|
+
// what went wrong.
|
|
212
|
+
const NOISE = [
|
|
213
|
+
/^\s*at\s/,
|
|
214
|
+
/^\s*\^+\s*$/,
|
|
215
|
+
/^node:internal\//,
|
|
216
|
+
/^\s*throw\s/,
|
|
217
|
+
/^Node\.js v/,
|
|
218
|
+
/^\s*[{}]\s*$/,
|
|
219
|
+
/^\s*(code|errno|syscall|path|requireStack|stack):/,
|
|
220
|
+
/^A complete log/,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
// The line worth quoting, when there is one. npm's failures and Node's both
|
|
224
|
+
// lead with the sentence a person can act on; everything after it is detail.
|
|
225
|
+
const SIGNAL = /(Error:|error code|ERR_|No matching version|ENOENT|EACCES|EPERM|not found|permission denied)/i;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* One line summarising why an npx run failed, or null when its output said
|
|
229
|
+
* nothing. Pure, and separate from the printing, because "do not dump a stack
|
|
230
|
+
* trace at the user" is the rule and a rule worth a bug is worth a test.
|
|
231
|
+
*/
|
|
232
|
+
export function npxFailureSummary(output) {
|
|
233
|
+
const lines = String(output ?? "")
|
|
234
|
+
.split(/\r?\n/)
|
|
235
|
+
// A warning is by definition not the reason this failed, and npm emits one
|
|
236
|
+
// for every ordinary exec ("the following package will be installed") whose
|
|
237
|
+
// wording would otherwise outrank the real error below it.
|
|
238
|
+
.filter(l => !/^npm (WARN|warn)\b/.test(l))
|
|
239
|
+
.map(l => l.replace(/^npm (ERR!|error)\s*/i, "").trimEnd())
|
|
240
|
+
.filter(l => l.trim() && !NOISE.some(re => re.test(l)));
|
|
241
|
+
if (!lines.length) return null;
|
|
242
|
+
const pick = lines.find(l => SIGNAL.test(l)) ?? lines[lines.length - 1];
|
|
243
|
+
return pick.trim().slice(0, 300);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The actionable half, for the one failure shape that is a broken environment
|
|
248
|
+
* rather than a broken network: npx exists, runs, and cannot find the npm it is
|
|
249
|
+
* supposed to load. `npm config get prefix` is the thing to check — a prefix
|
|
250
|
+
* pointing at a directory with no `node_modules/npm` under it leaves exactly
|
|
251
|
+
* this trace.
|
|
252
|
+
*/
|
|
253
|
+
export function npxFailureHint(output) {
|
|
254
|
+
const s = String(output ?? "");
|
|
255
|
+
if (/MODULE_NOT_FOUND/.test(s) && /(npx-cli\.js|npm-cli\.js|npm-prefix\.js)/.test(s)) {
|
|
256
|
+
return "the npx on PATH is a shim whose npm install is missing — check `npm config get prefix`";
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|