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,213 @@
|
|
|
1
|
+
// Whether a batch candidate's output is cmd.exe saying "no such command".
|
|
2
|
+
//
|
|
3
|
+
// A `.cmd` or `.bat` is launched THROUGH cmd.exe (see viaCmd in exec.mjs),
|
|
4
|
+
// and cmd.exe is always there — so a missing tool is not a spawn error at all.
|
|
5
|
+
// It is a healthy shell exiting non-zero after printing a sentence, and telling
|
|
6
|
+
// that sentence apart from a tool that ran and failed is the whole of what this
|
|
7
|
+
// module does. It decides two things for exec.mjs's candidate loop: whether to
|
|
8
|
+
// try the next spelling, and whether to answer `code: "ENOENT"` — and the first
|
|
9
|
+
// of those re-runs the entire command, which for `cswap remove 3` is not a
|
|
10
|
+
// retry anybody wants made on a guess.
|
|
11
|
+
//
|
|
12
|
+
// A module of its own because it is pure: text and an exit status in, a verdict
|
|
13
|
+
// out, no process and no filesystem. exec.mjs re-exports both names, so every
|
|
14
|
+
// caller that imported them from there still does.
|
|
15
|
+
// The three lines an ENGLISH cmd.exe prints when it cannot find what it was
|
|
16
|
+
// asked to run, anchored to a whole line each. First the two-line pair for a
|
|
17
|
+
// bare name, then the one it uses when a directory in an explicit path does not
|
|
18
|
+
// exist.
|
|
19
|
+
//
|
|
20
|
+
// These are one signal out of three rather than the whole answer — see
|
|
21
|
+
// looksMissing. Windows ships cmd.exe in every language it ships in, and these
|
|
22
|
+
// sentences are translated with it.
|
|
23
|
+
const CMD_UNKNOWN = /^'(.+)' is not recognized as an internal or external command,?$/i;
|
|
24
|
+
const CMD_UNKNOWN_TAIL = /^operable program or batch file\.?$/i;
|
|
25
|
+
const CMD_NO_PATH = /^the system cannot find the (?:path|file) specified\.?$/i;
|
|
26
|
+
|
|
27
|
+
// cmd.exe's own errorlevel for a command token it could not resolve — the one
|
|
28
|
+
// signal here that is not a human sentence, and therefore the only one that is
|
|
29
|
+
// the same on a German install as on an English one. It is the number every CI
|
|
30
|
+
// log in the world prints beside "is not recognized".
|
|
31
|
+
//
|
|
32
|
+
// Not every path reports it: some Windows builds answer a bare `cmd /c missing`
|
|
33
|
+
// with a plain 1 instead, which is why this is a sufficient signal and never a
|
|
34
|
+
// necessary one. A number this large cannot arrive from POSIX at all — a process
|
|
35
|
+
// exit status there is masked to 0-255 before Node ever sees it — so nothing
|
|
36
|
+
// outside cmd.exe can reach it by accident.
|
|
37
|
+
const CMD_NOT_FOUND_EXIT = 9009;
|
|
38
|
+
|
|
39
|
+
/** True when an exit status is cmd.exe saying "no such command", in any locale. */
|
|
40
|
+
export const notFoundExit = (code) => Number(code) === CMD_NOT_FOUND_EXIT;
|
|
41
|
+
|
|
42
|
+
// Every run of text this line wrapped in quotes. cmd.exe quotes the command
|
|
43
|
+
// token it could not find in EVERY locale, and the quote character is the only
|
|
44
|
+
// part of the message that is not a translation: `'…'` in English and French,
|
|
45
|
+
// `"…"` in German, Spanish, Italian, Portuguese, Polish and Russian.
|
|
46
|
+
const quotedRuns = (line) =>
|
|
47
|
+
[...String(line).matchAll(/'([^']+)'|"([^"]+)"/g)].map(m => m[1] ?? m[2]);
|
|
48
|
+
|
|
49
|
+
// cmd.exe echoes the command token exactly as it was given, so an exact match
|
|
50
|
+
// is what we expect; the comparison is only case- and quote-insensitive because
|
|
51
|
+
// Windows paths are.
|
|
52
|
+
const sameCommand = (quoted, name) => {
|
|
53
|
+
const norm = (s) => String(s).trim().replace(/^"+|"+$/g, "").toLowerCase();
|
|
54
|
+
return norm(quoted) === norm(name);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* cmd.exe's way of saying ENOENT — and only cmd.exe's.
|
|
59
|
+
*
|
|
60
|
+
* A .cmd or .bat candidate is launched THROUGH cmd.exe, and cmd.exe exists — so
|
|
61
|
+
* a missing tool is not a spawn error at all. It is a healthy shell exiting 1
|
|
62
|
+
* after printing two lines:
|
|
63
|
+
*
|
|
64
|
+
* 'cswap' is not recognized as an internal or external command,
|
|
65
|
+
* operable program or batch file.
|
|
66
|
+
*
|
|
67
|
+
* Read as a real failure, that stops the candidate loop early AND puts the
|
|
68
|
+
* second line — on its own, meaningless — in front of the user. Reported from
|
|
69
|
+
* Windows on 2026-08-14: the accounts panel said only "operable program or
|
|
70
|
+
* batch file." when sharing an account.
|
|
71
|
+
*
|
|
72
|
+
* The fussiness is about the other direction, which is worse. "The system
|
|
73
|
+
* cannot find the file specified" is Windows' generic text for ENOENT, and it
|
|
74
|
+
* appears INSIDE the output of tools that ran perfectly well — cswap is a
|
|
75
|
+
* Python CLI, and a FileNotFoundError traceback ends in that exact sentence.
|
|
76
|
+
* Believed there, it threw the real error away, told the user the tool was not
|
|
77
|
+
* on PATH when PATH was fine, and — the dangerous half — sent the candidate
|
|
78
|
+
* loop on to re-run the entire command, which for `cswap remove 3` means asking
|
|
79
|
+
* to delete an account a second time.
|
|
80
|
+
*
|
|
81
|
+
* So the output has to be cmd.exe's message and nothing else: cmd.exe prints it
|
|
82
|
+
* INSTEAD of running anything, so any other line, or any prefix on the line,
|
|
83
|
+
* means something ran and this is its report. And when cmd.exe names the
|
|
84
|
+
* command it could not find, that name must be the candidate we asked for — a
|
|
85
|
+
* tool that shells out itself can forward the message about some other command.
|
|
86
|
+
*
|
|
87
|
+
* `name` is the spelling cmd.exe was ACTUALLY GIVEN — which since #457 is the
|
|
88
|
+
* shim's absolute path whenever shimPath found one, not the bare candidate the
|
|
89
|
+
* loop started from. That distinction is the coupling #456 named as its reason
|
|
90
|
+
* for stopping short, and it is a one-way trap rather than a detail: cmd.exe
|
|
91
|
+
* echoes the command token back exactly as it received it, so a check against
|
|
92
|
+
* the bare name stops matching the instant the token becomes a path. What that
|
|
93
|
+
* costs is not a cosmetic mismatch — it is the honesty of the whole answer. A
|
|
94
|
+
* shim that shimPath saw and that is gone by the time cmd.exe looks for it (a
|
|
95
|
+
* stale memo, an uninstall mid-session, a network drive that dropped, a roaming
|
|
96
|
+
* profile still syncing) would come back as an ordinary exit 1 whose stderr is
|
|
97
|
+
* cmd.exe's two-line "is not recognized / operable program or batch file."
|
|
98
|
+
* instead of the `code: "ENOENT"` every panel keys its "not installed" message
|
|
99
|
+
* off. The user would be told their CLI failed, and shown half a sentence about
|
|
100
|
+
* batch files, when the truthful answer is that it is not there.
|
|
101
|
+
*
|
|
102
|
+
* Feeding it the launch spelling keeps the comparison EXACT, which is what the
|
|
103
|
+
* paragraph above is protecting: matching loosely — on the basename, say —
|
|
104
|
+
* would let a tool that shells out itself have its own child's "is not
|
|
105
|
+
* recognized" read as the tool's absence, re-running a command that may be
|
|
106
|
+
* `cswap remove 3`. So the rule is not "compare less", it is "compare against
|
|
107
|
+
* what was actually asked for".
|
|
108
|
+
*
|
|
109
|
+
* Callers that only have the text (failureText) omit it and get the shape rules
|
|
110
|
+
* alone.
|
|
111
|
+
*
|
|
112
|
+
* ── AND NOT ONLY IN ENGLISH (#552) ──────────────────────────────────────────
|
|
113
|
+
*
|
|
114
|
+
* Everything above was written against three English sentences, and cmd.exe is
|
|
115
|
+
* translated. On a German install with cswap genuinely absent it prints
|
|
116
|
+
*
|
|
117
|
+
* Der Befehl "cswap" ist entweder falsch geschrieben oder konnte nicht
|
|
118
|
+
* gefunden werden.
|
|
119
|
+
*
|
|
120
|
+
* — so this answered false, `run` resolved `{ ok: false, code: 1 }`,
|
|
121
|
+
* claude-accounts.mjs picked `reason: "switch_failed"` over `"no_cswap"` and the
|
|
122
|
+
* panel's install affordance never appeared. failureText then fell through to
|
|
123
|
+
* firstUseful, which puts the LAST line of a localized sentence on screen by
|
|
124
|
+
* itself: #457's symptom reproduced for every non-English locale. It also
|
|
125
|
+
* stopped the candidate loop early, so a `.bat` installed after a missing `.cmd`
|
|
126
|
+
* was never reached.
|
|
127
|
+
*
|
|
128
|
+
* Adding the German sentence, and then the French and the Japanese ones, is not
|
|
129
|
+
* a fix — it is the same defect with a longer list. So two signals that are not
|
|
130
|
+
* sentences carry the answer instead, and the English text is what remains when
|
|
131
|
+
* neither is available:
|
|
132
|
+
*
|
|
133
|
+
* 1. THE EXIT STATUS. `exitCode` 9009 is cmd.exe's own errorlevel for a
|
|
134
|
+
* command token it could not resolve, identical in every language. See
|
|
135
|
+
* CMD_NOT_FOUND_EXIT for why it is not required, and the paragraph below
|
|
136
|
+
* for the two cases where it is not believed either. It is subject to the
|
|
137
|
+
* same "cmd.exe printed nothing else" cap as rule 2, because a status is
|
|
138
|
+
* forwarded as easily as a sentence is.
|
|
139
|
+
*
|
|
140
|
+
* 2. THE SHAPE. cmd.exe quotes the command it could not find, in every locale,
|
|
141
|
+
* and prints that INSTEAD of running anything — so the whole output is at
|
|
142
|
+
* most the two lines of one wrapped sentence. Text of at most two lines
|
|
143
|
+
* whose FIRST line quotes exactly the spelling we launched is cmd.exe's
|
|
144
|
+
* verdict about our command whatever the words around it say.
|
|
145
|
+
*
|
|
146
|
+
* Rule 2 needs `name`, and refuses without it. That is the same principle the
|
|
147
|
+
* paragraphs above argue for and not a limitation bolted on: `Error: "account-9"
|
|
148
|
+
* does not exist` is one line with a quoted token in it, and read as an absence
|
|
149
|
+
* it would send the candidate loop back round to re-run `cswap remove 3`. What
|
|
150
|
+
* makes the rule safe is that the quoted token has to be the exact spelling
|
|
151
|
+
* cmd.exe was handed — `cswap.cmd`, or the absolute path shimPath found — which
|
|
152
|
+
* is a string the tool underneath has no reason to print. The two-line cap is
|
|
153
|
+
* the other half: a Python traceback ending in "The system cannot find the file
|
|
154
|
+
* specified" is four lines and can never qualify.
|
|
155
|
+
*
|
|
156
|
+
* THE SAME TRAP THE ENGLISH RULE ALREADY AVOIDS, NOW FOR THE EXIT STATUS. A
|
|
157
|
+
* `.cmd` shim is itself a batch file, so a shim that EXISTS and whose payload
|
|
158
|
+
* interpreter does not — a scoop or npm-style `cswap.cmd` in front of a python
|
|
159
|
+
* that was uninstalled — has cmd.exe print "is not recognized" about PYTHON and
|
|
160
|
+
* hands the shim's caller that same 9009. Believed on its own, the deck would
|
|
161
|
+
* call the tool absent and re-run the command under the next spelling. So a text
|
|
162
|
+
* that positively names a command OTHER than ours vetoes every rule here,
|
|
163
|
+
* including the status: the check that made #457 safe, applied one level up.
|
|
164
|
+
*
|
|
165
|
+
* What is deliberately still missed: a LOCALIZED "the system cannot find the
|
|
166
|
+
* path specified", which carries no quoted token and no structure to key off.
|
|
167
|
+
* That case only arises when shimPath found a shim that then vanished, and it
|
|
168
|
+
* fails in the safe direction — an honest exit 1 rather than a wrong ENOENT.
|
|
169
|
+
*/
|
|
170
|
+
export function looksMissing(text, name = "", exitCode = null) {
|
|
171
|
+
const lines = String(text ?? "").split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
172
|
+
// Whatever cmd.exe named, in whatever language it said the rest — the quoting
|
|
173
|
+
// is the part that is not a translation.
|
|
174
|
+
const named = lines.length ? quotedRuns(lines[0]) : [];
|
|
175
|
+
const namesUs = named.some(q => sameCommand(q, name));
|
|
176
|
+
|
|
177
|
+
// The veto, before anything is believed: a message about somebody else's
|
|
178
|
+
// command is not evidence about ours, and neither is the status that came
|
|
179
|
+
// with it. See the header — a `.cmd` shim in front of a missing interpreter
|
|
180
|
+
// forwards both.
|
|
181
|
+
if (name && named.length > 0 && !namesUs) return false;
|
|
182
|
+
|
|
183
|
+
// cmd.exe says this INSTEAD of running anything, so anything longer than one
|
|
184
|
+
// wrapped sentence came from something that DID run — and that outranks both
|
|
185
|
+
// signals below. A shim can forward its child's 9009 after printing pages of
|
|
186
|
+
// its own; a Python traceback is four lines and one of them quotes the very
|
|
187
|
+
// shim we launched.
|
|
188
|
+
const saidNothingElse = lines.length <= 2;
|
|
189
|
+
|
|
190
|
+
// The signal that is not a sentence. It does not need to READ the text, which
|
|
191
|
+
// is the whole reason it exists: on a non-English install there may be nothing
|
|
192
|
+
// in the text this can read.
|
|
193
|
+
if (saidNothingElse && notFoundExit(exitCode)) return true;
|
|
194
|
+
if (!lines.length) return false;
|
|
195
|
+
|
|
196
|
+
// The English shapes, whole-line anchored, exactly as before.
|
|
197
|
+
let english = true;
|
|
198
|
+
for (const line of lines) {
|
|
199
|
+
const unknown = CMD_UNKNOWN.exec(line);
|
|
200
|
+
if (unknown) {
|
|
201
|
+
if (name && !sameCommand(unknown[1], name)) return false;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (CMD_UNKNOWN_TAIL.test(line) || CMD_NO_PATH.test(line)) continue;
|
|
205
|
+
english = false;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (english) return true;
|
|
209
|
+
|
|
210
|
+
// Otherwise: cmd.exe in some other language, recognised by its shape and by
|
|
211
|
+
// the one word in it that is ours.
|
|
212
|
+
return Boolean(name) && saidNothingElse && namesUs;
|
|
213
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// What a command that failed said about it, short enough to keep.
|
|
2
|
+
//
|
|
3
|
+
// claude-accounts.mjs, cswap-install.mjs and cswap-auto.mjs read a failed
|
|
4
|
+
// `run` the same way — stderr when the tool wrote any, stdout otherwise,
|
|
5
|
+
// trimmed, and cut to a length a panel can print — and each wrote that out in
|
|
6
|
+
// full, seven times over. One rule spelled seven times is seven places to fix
|
|
7
|
+
// the day a tool starts putting its reason somewhere else. The auto-switch tick
|
|
8
|
+
// (cswap-auto-loop.mjs) had an eighth spelling — stderr or nothing, on the one
|
|
9
|
+
// path where stdout is already known to be empty — and asks here too.
|
|
10
|
+
//
|
|
11
|
+
// A module of its own rather than a line in exec.mjs, which owns `run`: more
|
|
12
|
+
// than a dozen suites replace exec.mjs with a mock that carries `run` and
|
|
13
|
+
// nothing else, and a helper exported from there would be missing from every
|
|
14
|
+
// module those suites load.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What a failed command said, trimmed and cut to `max` characters: its stderr
|
|
18
|
+
* when it wrote any, its stdout otherwise, because tools differ on which one a
|
|
19
|
+
* failure goes to. `r` is exec.mjs's `run` answer.
|
|
20
|
+
*/
|
|
21
|
+
export function failureDetail(r, max) {
|
|
22
|
+
return (r.stderr || r.stdout).trim().slice(0, max);
|
|
23
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// Which file to run for a command name, and what to hand spawn for it — on
|
|
2
|
+
// Linux, macOS and Windows alike.
|
|
3
|
+
//
|
|
4
|
+
// Everything exec.mjs's header argues for is decided here: the spellings a bare
|
|
5
|
+
// name is tried under (candidates), which of them are batch files that must go
|
|
6
|
+
// through cmd.exe (isBatch, viaCmd), how one argument is quoted for a shell that
|
|
7
|
+
// will parse it (shellQuoteArg), and the absolute path a `.cmd` shim is
|
|
8
|
+
// launched under so it finds its own payload (shimPath, pathLookup,
|
|
9
|
+
// candidateSpec). Nothing here spawns anything. The platform is a parameter
|
|
10
|
+
// throughout, so the Windows answers can be checked from a machine that is not
|
|
11
|
+
// Windows.
|
|
12
|
+
//
|
|
13
|
+
// The one piece of state is the memo of which spelling worked, per command
|
|
14
|
+
// name: `candidates` reads it, and `rememberSpelling` is its one write, which
|
|
15
|
+
// exec.mjs's runners call once a spelling has proved itself.
|
|
16
|
+
//
|
|
17
|
+
// exec.mjs re-exports every public name here, so its callers are unchanged.
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
// Extensions Windows will execute, most specific first. `.com` is omitted —
|
|
21
|
+
// nothing ships one, and every extra candidate costs a failed spawn.
|
|
22
|
+
const WIN_EXTS = [".exe", ".cmd", ".bat", ""];
|
|
23
|
+
|
|
24
|
+
// Which spelling worked, per command name. A failed spawn is cheap but not
|
|
25
|
+
// free, and these run on a poll.
|
|
26
|
+
const resolved = new Map();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Remember that `raw` is the spelling of `cmd` that worked, so `candidates`
|
|
30
|
+
* offers it alone from now on.
|
|
31
|
+
*
|
|
32
|
+
* The memo's one write, named. Five places confirm a spelling — a clean `run`,
|
|
33
|
+
* a direct spawn and a clean exit in `runInteractive`, the same two in
|
|
34
|
+
* `runDetached` — and each has its own rule for WHEN a spelling counts as
|
|
35
|
+
* confirmed; see the notes at each. What they record is always the candidate
|
|
36
|
+
* and never a resolved path, for the reason given in `run`.
|
|
37
|
+
*/
|
|
38
|
+
export function rememberSpelling(cmd, raw) {
|
|
39
|
+
resolved.set(cmd, raw);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The spellings to try for `cmd`, best first.
|
|
44
|
+
*
|
|
45
|
+
* Exported with the platform as a parameter for the reason everything else in
|
|
46
|
+
* this file is: the Windows answer decides which candidate becomes a batch one,
|
|
47
|
+
* and a batch candidate is the only kind #457 touches — so a test that wants to
|
|
48
|
+
* follow a caller's bare name all the way to the command line cmd.exe receives
|
|
49
|
+
* has to be able to ask for the Windows list from a machine that is not Windows.
|
|
50
|
+
*/
|
|
51
|
+
export function candidates(cmd, platform = process.platform) {
|
|
52
|
+
if (platform !== "win32") return [cmd];
|
|
53
|
+
// An explicit extension is respected as given.
|
|
54
|
+
if (/\.[a-z]+$/i.test(cmd)) return [cmd];
|
|
55
|
+
const known = resolved.get(cmd);
|
|
56
|
+
return known ? [known] : WIN_EXTS.map(ext => cmd + ext);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Exported for tests: the platform is a parameter so both can be checked. */
|
|
60
|
+
export const isBatch = (file, platform = process.platform) =>
|
|
61
|
+
platform === "win32" && /\.(cmd|bat)$/i.test(file);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rewrite a batch-file invocation as a cmd.exe one.
|
|
65
|
+
*
|
|
66
|
+
* Mirrors what Node does internally for `shell: true` on Windows — comspec,
|
|
67
|
+
* /d /s /c, the whole command line as a single quoted argument, and
|
|
68
|
+
* windowsVerbatimArguments so Node does not quote it a second time. Each
|
|
69
|
+
* argument is quoted by shellQuoteArg below, which has to satisfy cmd.exe AND
|
|
70
|
+
* the argv parser of whatever it launches — see the note there, and #624 for
|
|
71
|
+
* the half of that rule this file was missing until a real cmd.exe was asked.
|
|
72
|
+
*/
|
|
73
|
+
export function viaCmd(file, args) {
|
|
74
|
+
const line = [file, ...args].map(a => shellQuoteArg(a, "win32")).join(" ");
|
|
75
|
+
return {
|
|
76
|
+
file: process.env.comspec || process.env.ComSpec || "cmd.exe",
|
|
77
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
78
|
+
opts: { windowsVerbatimArguments: true },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Quote ONE argument into a command line a shell will parse.
|
|
84
|
+
*
|
|
85
|
+
* Everything else in this module exists to avoid needing this: an argument
|
|
86
|
+
* vector is handed to spawn untouched and nothing in it is ever read as syntax.
|
|
87
|
+
* Two callers cannot take that route, and they are the reason this is exported.
|
|
88
|
+
* A Claude Code hook is registered as `{type:"command", command:"<string>"}` —
|
|
89
|
+
* the host CLI runs that string THROUGH A SHELL on every tool call, and the
|
|
90
|
+
* format has no argv form to emit instead. So the string has to be built here,
|
|
91
|
+
* correctly, once.
|
|
92
|
+
*
|
|
93
|
+
* The inputs are not user input in the web sense, but they are not constants
|
|
94
|
+
* either: the hook path is built from $CLAUDE_CONFIG_DIR and homedir(), and the
|
|
95
|
+
* node path from process.execPath. Wrapping those in double quotes — what this
|
|
96
|
+
* used to do — is not escaping on POSIX at all: `$(…)`, a backtick and `\` are
|
|
97
|
+
* all still live inside them, so a config dir named `/tmp/a$(id)b` became shell
|
|
98
|
+
* code written into the user's settings.json and executed on every hook fire.
|
|
99
|
+
* A bare `$` in a path is the same bug wearing a duller hat — `$HOME` expands to
|
|
100
|
+
* nothing, the hook path silently becomes wrong, and hooks stop firing with no
|
|
101
|
+
* error anywhere.
|
|
102
|
+
*
|
|
103
|
+
* POSIX gets single quotes, inside which NOTHING is special, with the one
|
|
104
|
+
* escape that form has: close the quote, emit a backslash-quote, reopen.
|
|
105
|
+
*
|
|
106
|
+
* Windows gets the rule below — the same one viaCmd applies above, so this file
|
|
107
|
+
* has one Windows quoting rule rather than two. It has to satisfy TWO parsers,
|
|
108
|
+
* and that is the whole of its difficulty, because they disagree about the
|
|
109
|
+
* backslash:
|
|
110
|
+
*
|
|
111
|
+
* cmd.exe reads the line only far enough to find where the command ends. It
|
|
112
|
+
* has no escape for a quote at all — a `"` toggles "inside quotes", and `&`,
|
|
113
|
+
* `|`, `<`, `>`, `^`, `(` and `)` are syntax only while outside — and it does
|
|
114
|
+
* not treat `\` as anything. Then it hands the rest of the line on AS TEXT.
|
|
115
|
+
*
|
|
116
|
+
* The program on the other end splits that text into argv itself, and for
|
|
117
|
+
* node that is the UCRT parser, which DOES read `\` as an escape in front of
|
|
118
|
+
* a quote: 2n backslashes before a `"` are n backslashes and a quote that
|
|
119
|
+
* toggles, 2n+1 are n backslashes and a literal `"`.
|
|
120
|
+
*
|
|
121
|
+
* So an embedded `"` is written `""` rather than `\"` — two toggles is no net
|
|
122
|
+
* change to cmd.exe's idea of where it is, while the child's parser turns the
|
|
123
|
+
* pair back into one quote — and every run of backslashes that ends up in front
|
|
124
|
+
* of a quote, INCLUDING THE CLOSING ONE THIS ADDS, is doubled so the child does
|
|
125
|
+
* not read it as an escape.
|
|
126
|
+
*
|
|
127
|
+
* That last clause is #624 and it was missing. `"` + arg + `"` alone turns a
|
|
128
|
+
* path ending in a separator — `C:\Program Files\nodejs\` — into
|
|
129
|
+
* `"C:\Program Files\nodejs\"`, whose final `\"` is an escaped quote to the
|
|
130
|
+
* child: the quoted region never closes, and the argument swallows every
|
|
131
|
+
* argument after it on the line. `--provider claude` in the hook command is
|
|
132
|
+
* exactly what it swallowed. A backslash immediately before an embedded quote
|
|
133
|
+
* was the quieter half of the same defect — it was eaten as the escape.
|
|
134
|
+
*
|
|
135
|
+
* The one residual left, unchanged: cmd.exe expands `%VAR%` inside quotes too,
|
|
136
|
+
* and a command line has no escape for it. That is narrower than it sounds,
|
|
137
|
+
* since `%foo%` with no variable `foo` is left alone, and it is a limit of the
|
|
138
|
+
* platform rather than of this function. `!VAR!` is the same limit on a machine
|
|
139
|
+
* with delayed expansion turned on, which is not the default for `cmd /c`.
|
|
140
|
+
*
|
|
141
|
+
* no-shell-hook-commands.test.ts runs the output of this through a real cmd.exe
|
|
142
|
+
* on the Windows leg of the matrix and compares what the child RECEIVED against
|
|
143
|
+
* what was intended. Four literals said this function agreed with itself for
|
|
144
|
+
* three releases; they could not say whether it agreed with Windows.
|
|
145
|
+
*/
|
|
146
|
+
export function shellQuoteArg(arg, platform = process.platform) {
|
|
147
|
+
const s = String(arg ?? "");
|
|
148
|
+
if (platform !== "win32") return `'${s.split("'").join("'\\''")}'`;
|
|
149
|
+
let out = '"';
|
|
150
|
+
let slashes = 0;
|
|
151
|
+
for (const ch of s) {
|
|
152
|
+
if (ch === "\\") { slashes++; continue; }
|
|
153
|
+
if (ch === '"') { out += "\\".repeat(slashes * 2) + '""'; slashes = 0; continue; }
|
|
154
|
+
out += "\\".repeat(slashes) + ch;
|
|
155
|
+
slashes = 0;
|
|
156
|
+
}
|
|
157
|
+
// A run that reaches the end of the argument is in front of the closing quote,
|
|
158
|
+
// which is a quote like any other.
|
|
159
|
+
return `${out}${"\\".repeat(slashes * 2)}"`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The absolute path of a Windows command shim, or null when nothing answers to
|
|
164
|
+
* that name.
|
|
165
|
+
*
|
|
166
|
+
* Why a bare name is not good enough, which is the whole of #456. npm's `.cmd`
|
|
167
|
+
* shims locate every file they need relative to THEMSELVES:
|
|
168
|
+
*
|
|
169
|
+
* SET "NPM_PREFIX_JS=%~dp0\node_modules\npm\bin\npm-prefix.js"
|
|
170
|
+
* SET "NPX_CLI_JS=%~dp0\node_modules\npm\bin\npx-cli.js"
|
|
171
|
+
*
|
|
172
|
+
* `%~dp0` is the drive and path of `%0`, and `%0` is the command token cmd.exe
|
|
173
|
+
* was given. Handed `cmd.exe /d /s /c ""npm.cmd" "install" …`, that token
|
|
174
|
+
* carries no directory of its own, so `%~dp0` came out as the deck's WORKING
|
|
175
|
+
* DIRECTORY instead of the shim's. Reported from Windows 10 with the deck
|
|
176
|
+
* started from `C:\Users\vceban`:
|
|
177
|
+
*
|
|
178
|
+
* Error: Cannot find module 'C:\Users\vceban\node_modules\npm\bin\npm-prefix.js'
|
|
179
|
+
* Error: Cannot find module 'C:\Users\vceban\node_modules\npm\bin\npx-cli.js'
|
|
180
|
+
*
|
|
181
|
+
* — two stacks, one defect, and `C:\Users\vceban` is the cwd rather than
|
|
182
|
+
* anything to do with npm: `where npx` on that machine printed
|
|
183
|
+
* `C:\Program Files\nodejs\npx.cmd` and `npx ccdeck` ran perfectly from a
|
|
184
|
+
* prompt. It is ours, introduced when #362 replaced `shell: true` with viaCmd:
|
|
185
|
+
* the quoting was the fix and the bare name was the cost, and it broke the
|
|
186
|
+
* managed install and the npx fallback in the same stroke, which is why every
|
|
187
|
+
* diagnosis of one half kept half-fitting.
|
|
188
|
+
*
|
|
189
|
+
* Node's own directory is tried before PATH because node and npm ship together
|
|
190
|
+
* and that is where the shims are — the same preference npxCliCandidates in
|
|
191
|
+
* npx.mjs states for npm's CLI scripts, so the repo has one rule rather than
|
|
192
|
+
* two. PATH is then walked in its own order, which is what cmd.exe would have
|
|
193
|
+
* done, minus the current directory it searches first: a deck that resolves its
|
|
194
|
+
* npm out of whatever folder it happens to be sitting in is the bug above
|
|
195
|
+
* wearing a hat.
|
|
196
|
+
*
|
|
197
|
+
* The path arithmetic is spelled out rather than done through `node:path` for
|
|
198
|
+
* the reason npxCliCandidates gives: `path` is the platform running the SUITE,
|
|
199
|
+
* so a Windows layout checked from macOS would come back with forward slashes.
|
|
200
|
+
* `execPath`, `pathEnv` and `exists` are injected for the same reason — the
|
|
201
|
+
* Windows answer has to be checkable from an OS that cannot run it.
|
|
202
|
+
*
|
|
203
|
+
* The walk itself now lives in `pathLookup` below, because #433 needed the same
|
|
204
|
+
* one for a tool that is not a shim; this is that walk with the two answers a
|
|
205
|
+
* shim needs — Windows, and node's own directory first.
|
|
206
|
+
*/
|
|
207
|
+
export const shimPath = (name, deps) =>
|
|
208
|
+
pathLookup(name, "win32", { besideNode: true, ...deps });
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Where `name` actually lives on PATH, as an absolute path, or null when
|
|
212
|
+
* nothing on PATH answers to it.
|
|
213
|
+
*
|
|
214
|
+
* This is the general form of shimPath above, and it exists because #433 asked
|
|
215
|
+
* for a third way to reach ccusage: the copy a user installed themselves. The
|
|
216
|
+
* deck's own error text has told people to "put ccusage on PATH yourself" since
|
|
217
|
+
* before there was anything that looked, so the choice was to either look or
|
|
218
|
+
* stop saying it — see getRunner in ccusage.mjs for which way that went.
|
|
219
|
+
*
|
|
220
|
+
* Writing it as ONE walk rather than a second one is the point. A PATH search
|
|
221
|
+
* on Windows is not a PATH search plus a note: `ccusage` there is `ccusage.cmd`
|
|
222
|
+
* or `ccusage.exe` and never the bare name, because PATHEXT is a shell's job
|
|
223
|
+
* and spawn is not a shell — which is the whole reason `candidates` exists, so
|
|
224
|
+
* that is what supplies the spellings here. And a `.cmd` found this way is
|
|
225
|
+
* returned as a FULL PATH, which is what keeps #456 fixed: launched by its bare
|
|
226
|
+
* name through cmd.exe, a shim computes `%~dp0` from the deck's working
|
|
227
|
+
* directory and goes hunting for its payload there.
|
|
228
|
+
*
|
|
229
|
+
* `besideNode` is the one thing a shim wants and a tool does not. npm ships
|
|
230
|
+
* beside node, so looking there first is right for `npm.cmd`; for anything else
|
|
231
|
+
* it would quietly overrule the order the user put their own PATH in, which is
|
|
232
|
+
* the one statement of preference they actually made.
|
|
233
|
+
*
|
|
234
|
+
* The check is existence, not executability. That is what `run`'s candidate
|
|
235
|
+
* loop effectively asks too — it spawns and moves on if the spawn fails — and
|
|
236
|
+
* an executable bit is not a thing Windows has. The residue is a directory on
|
|
237
|
+
* PATH that happens to be named after the tool; it would be resolved here and
|
|
238
|
+
* fail on spawn, with the failure naming the path, which is a better place to
|
|
239
|
+
* find out than a silent miss.
|
|
240
|
+
*/
|
|
241
|
+
export function pathLookup(name, platform = process.platform, {
|
|
242
|
+
execPath = process.execPath,
|
|
243
|
+
pathEnv = process.env.PATH ?? process.env.Path ?? "",
|
|
244
|
+
exists = existsSync,
|
|
245
|
+
besideNode = false,
|
|
246
|
+
} = {}) {
|
|
247
|
+
// A name that already carries a directory needs no lookup, and re-rooting it
|
|
248
|
+
// would be a way to run something else entirely.
|
|
249
|
+
if (typeof name !== "string" || !name || /[\\/]/.test(name)) return null;
|
|
250
|
+
const win = platform === "win32";
|
|
251
|
+
const sep = win ? "\\" : "/";
|
|
252
|
+
const dirs = [];
|
|
253
|
+
if (besideNode) {
|
|
254
|
+
const beside = String(execPath ?? "").split(/[\\/]/).slice(0, -1).join(sep);
|
|
255
|
+
if (beside) dirs.push(beside);
|
|
256
|
+
}
|
|
257
|
+
// `;` on Windows, `:` everywhere else. Splitting on the wrong one is not a
|
|
258
|
+
// near miss: a POSIX PATH read with `;` is one enormous directory that
|
|
259
|
+
// exists nowhere, so every lookup would answer null and the feature would
|
|
260
|
+
// look like it had never been written.
|
|
261
|
+
for (const raw of String(pathEnv ?? "").split(win ? ";" : ":")) {
|
|
262
|
+
// A PATH entry may be quoted, and may end in a separator; neither is part
|
|
263
|
+
// of the directory, and both would produce a path nothing exists at.
|
|
264
|
+
const dir = raw.trim().replace(/^"|"$/g, "").replace(/[\\/]+$/, "");
|
|
265
|
+
if (dir) dirs.push(dir);
|
|
266
|
+
}
|
|
267
|
+
// On POSIX this is `[name]`, so the inner loop runs once and the cost is one
|
|
268
|
+
// stat per directory, exactly as before.
|
|
269
|
+
const spellings = candidates(name, platform);
|
|
270
|
+
for (const dir of dirs) {
|
|
271
|
+
for (const spelling of spellings) {
|
|
272
|
+
const full = `${dir}${sep}${spelling}`;
|
|
273
|
+
try {
|
|
274
|
+
if (exists(full)) return full;
|
|
275
|
+
} catch {
|
|
276
|
+
// An entry that cannot even be stat'ed — a disconnected network drive
|
|
277
|
+
// is the usual one — is a miss, not a reason to stop looking.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* What to hand `spawn`/`execFile` for `file` and `args` on this platform, with
|
|
286
|
+
* the argument vector intact and no shell.
|
|
287
|
+
*
|
|
288
|
+
* The alternative every caller reaches for first — `shell: true`, because a
|
|
289
|
+
* .cmd cannot be spawned any other way — is the one thing that must not be
|
|
290
|
+
* used: Node joins the array into a command line with a single space and no
|
|
291
|
+
* per-argument quoting, so `--workspace C:\Users\John Smith\proj` arrives as
|
|
292
|
+
* two arguments and an `&` in a path ends the command early. Batch files go
|
|
293
|
+
* through cmd.exe with each argument quoted; everything else is spawned as
|
|
294
|
+
* given. The platform is a parameter so both branches can be tested.
|
|
295
|
+
*
|
|
296
|
+
* The one thing quoting cannot cover: cmd.exe expands `%VAR%` inside quotes
|
|
297
|
+
* too, and a command line has no escape for it. Every other metacharacter —
|
|
298
|
+
* `&`, `|`, `>`, `^`, `(` — is inert once quoted.
|
|
299
|
+
*/
|
|
300
|
+
export const spawnSpec = (file, args, platform = process.platform) =>
|
|
301
|
+
isBatch(file, platform) ? viaCmd(file, args) : { file, args, opts: {} };
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The same thing for ONE CANDIDATE SPELLING out of the list above — which on
|
|
305
|
+
* Windows means deciding the NAME the shim is launched under before deciding
|
|
306
|
+
* how, because those are not the same question.
|
|
307
|
+
*
|
|
308
|
+
* `spawnSpec` answers "how". This answers what #457 put in front of it. A batch
|
|
309
|
+
* candidate runs THROUGH cmd.exe (see viaCmd), and a `.cmd` shim locates its own
|
|
310
|
+
* payload relative to `%~dp0` — the drive and path of the command token cmd.exe
|
|
311
|
+
* was handed. A bare `claude.cmd` carries no directory at all, so `%~dp0` came
|
|
312
|
+
* out as the deck's WORKING DIRECTORY and the shim went hunting for its
|
|
313
|
+
* JavaScript under whatever folder the deck happened to be started from. #456
|
|
314
|
+
* proved exactly that for ccusage's `npm.cmd` and `npx.cmd`; it left exec.mjs's
|
|
315
|
+
* three runners alone, and they carry the shims the panels depend on — the
|
|
316
|
+
* `claude.cmd` behind the quota poll and the sign-in, and whatever `.cmd` a
|
|
317
|
+
* Python installer left for cswap. Same defect, same machine, wider blast
|
|
318
|
+
* radius.
|
|
319
|
+
*
|
|
320
|
+
* `?? raw` is the whole safety story and is not optional: when shimPath can see
|
|
321
|
+
* no layout it answers null, and the candidate stays the bare name today's code
|
|
322
|
+
* already uses, so nothing that works now can start failing. It is the same
|
|
323
|
+
* `?? name` ccusage.mjs and npx.mjs spell, so the repo has one rule rather than
|
|
324
|
+
* three. shimPath also refuses any name that already carries a directory, which
|
|
325
|
+
* is what keeps a caller's own absolute candidate — quotaClaudeBin's
|
|
326
|
+
* `%APPDATA%\npm\claude.cmd`, cswapCandidates' `~/.local/bin/cswap.exe` — from
|
|
327
|
+
* being re-rooted somewhere else entirely.
|
|
328
|
+
*
|
|
329
|
+
* `launch` comes back beside the spec because looksMissing has to be told the
|
|
330
|
+
* spelling cmd.exe was ACTUALLY GIVEN rather than the one the loop started
|
|
331
|
+
* from; its own header explains what breaks otherwise, and that coupling is the
|
|
332
|
+
* reason #456 stopped short of doing this.
|
|
333
|
+
*
|
|
334
|
+
* On POSIX `isBatch` is false, so no lookup happens, no filesystem is touched,
|
|
335
|
+
* `launch` is `raw`, and the spec is the object spawnSpec always returned —
|
|
336
|
+
* byte-identical, which is the point.
|
|
337
|
+
*/
|
|
338
|
+
export function candidateSpec(raw, args, platform = process.platform, deps) {
|
|
339
|
+
const launch = isBatch(raw, platform) ? (shimPath(raw, deps) ?? raw) : raw;
|
|
340
|
+
return { ...spawnSpec(launch, args, platform), launch };
|
|
341
|
+
}
|