agent-dag 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 +13 -7
- package/bin/agent-dag.js +87 -9
- package/bin/deck.js +133 -22
- package/dist/web/assets/index-Bdl1LX0-.css +1 -0
- package/dist/web/assets/index-jXBjwwZC.js +89 -0
- package/dist/web/index.html +2 -2
- package/hook/hook.js +120 -31
- package/package.json +2 -2
- package/src/server/args.mjs +113 -15
- package/src/server/ccusage.mjs +105 -1
- package/src/server/claude-accounts.mjs +145 -1
- package/src/server/codex-auth.mjs +9 -4
- package/src/server/codex-quota.mjs +95 -3
- package/src/server/codex-usage.mjs +163 -9
- package/src/server/cswap-admin.mjs +346 -40
- package/src/server/cswap-auto.mjs +365 -12
- package/src/server/cswap-install.mjs +238 -17
- package/src/server/exec.mjs +233 -26
- package/src/server/index.mjs +1994 -157
- package/src/server/installer.mjs +173 -11
- package/src/server/invoked-as.mjs +16 -14
- package/src/server/quota.mjs +131 -34
- package/src/server/retire-sound-hook.mjs +315 -0
- package/src/server/self-update.mjs +262 -21
- package/src/server/supervisor.mjs +103 -0
- package/src/server/system-metrics.mjs +105 -7
- package/src/server/uv-bootstrap.mjs +43 -11
- package/dist/web/assets/index-BxAZQc7O.css +0 -1
- package/dist/web/assets/index-DRgZVqF-.js +0 -78
- package/hook/notify.js +0 -60
- package/src/server/sound-hook.mjs +0 -390
package/src/server/exec.mjs
CHANGED
|
@@ -63,8 +63,9 @@ export const isBatch = (file, platform = process.platform) =>
|
|
|
63
63
|
* Mirrors what Node does internally for `shell: true` on Windows — comspec,
|
|
64
64
|
* /d /s /c, the whole command line as a single quoted argument, and
|
|
65
65
|
* windowsVerbatimArguments so Node does not quote it a second time. Each
|
|
66
|
-
* argument is quoted
|
|
67
|
-
*
|
|
66
|
+
* argument is quoted by shellQuoteArg below, which has to satisfy cmd.exe AND
|
|
67
|
+
* the argv parser of whatever it launches — see the note there, and #624 for
|
|
68
|
+
* the half of that rule this file was missing until a real cmd.exe was asked.
|
|
68
69
|
*/
|
|
69
70
|
export function viaCmd(file, args) {
|
|
70
71
|
const line = [file, ...args].map(a => shellQuoteArg(a, "win32")).join(" ");
|
|
@@ -98,17 +99,61 @@ export function viaCmd(file, args) {
|
|
|
98
99
|
*
|
|
99
100
|
* POSIX gets single quotes, inside which NOTHING is special, with the one
|
|
100
101
|
* escape that form has: close the quote, emit a backslash-quote, reopen.
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
102
|
+
*
|
|
103
|
+
* Windows gets the rule below — the same one viaCmd applies above, so this file
|
|
104
|
+
* has one Windows quoting rule rather than two. It has to satisfy TWO parsers,
|
|
105
|
+
* and that is the whole of its difficulty, because they disagree about the
|
|
106
|
+
* backslash:
|
|
107
|
+
*
|
|
108
|
+
* cmd.exe reads the line only far enough to find where the command ends. It
|
|
109
|
+
* has no escape for a quote at all — a `"` toggles "inside quotes", and `&`,
|
|
110
|
+
* `|`, `<`, `>`, `^`, `(` and `)` are syntax only while outside — and it does
|
|
111
|
+
* not treat `\` as anything. Then it hands the rest of the line on AS TEXT.
|
|
112
|
+
*
|
|
113
|
+
* The program on the other end splits that text into argv itself, and for
|
|
114
|
+
* node that is the UCRT parser, which DOES read `\` as an escape in front of
|
|
115
|
+
* a quote: 2n backslashes before a `"` are n backslashes and a quote that
|
|
116
|
+
* toggles, 2n+1 are n backslashes and a literal `"`.
|
|
117
|
+
*
|
|
118
|
+
* So an embedded `"` is written `""` rather than `\"` — two toggles is no net
|
|
119
|
+
* change to cmd.exe's idea of where it is, while the child's parser turns the
|
|
120
|
+
* pair back into one quote — and every run of backslashes that ends up in front
|
|
121
|
+
* of a quote, INCLUDING THE CLOSING ONE THIS ADDS, is doubled so the child does
|
|
122
|
+
* not read it as an escape.
|
|
123
|
+
*
|
|
124
|
+
* That last clause is #624 and it was missing. `"` + arg + `"` alone turns a
|
|
125
|
+
* path ending in a separator — `C:\Program Files\nodejs\` — into
|
|
126
|
+
* `"C:\Program Files\nodejs\"`, whose final `\"` is an escaped quote to the
|
|
127
|
+
* child: the quoted region never closes, and the argument swallows every
|
|
128
|
+
* argument after it on the line. `--provider claude` in the hook command is
|
|
129
|
+
* exactly what it swallowed. A backslash immediately before an embedded quote
|
|
130
|
+
* was the quieter half of the same defect — it was eaten as the escape.
|
|
131
|
+
*
|
|
132
|
+
* The one residual left, unchanged: cmd.exe expands `%VAR%` inside quotes too,
|
|
133
|
+
* and a command line has no escape for it. That is narrower than it sounds,
|
|
134
|
+
* since `%foo%` with no variable `foo` is left alone, and it is a limit of the
|
|
135
|
+
* platform rather than of this function. `!VAR!` is the same limit on a machine
|
|
136
|
+
* with delayed expansion turned on, which is not the default for `cmd /c`.
|
|
137
|
+
*
|
|
138
|
+
* no-shell-hook-commands.test.ts runs the output of this through a real cmd.exe
|
|
139
|
+
* on the Windows leg of the matrix and compares what the child RECEIVED against
|
|
140
|
+
* what was intended. Four literals said this function agreed with itself for
|
|
141
|
+
* three releases; they could not say whether it agreed with Windows.
|
|
107
142
|
*/
|
|
108
143
|
export function shellQuoteArg(arg, platform = process.platform) {
|
|
109
144
|
const s = String(arg ?? "");
|
|
110
|
-
if (platform
|
|
111
|
-
|
|
145
|
+
if (platform !== "win32") return `'${s.split("'").join("'\\''")}'`;
|
|
146
|
+
let out = '"';
|
|
147
|
+
let slashes = 0;
|
|
148
|
+
for (const ch of s) {
|
|
149
|
+
if (ch === "\\") { slashes++; continue; }
|
|
150
|
+
if (ch === '"') { out += "\\".repeat(slashes * 2) + '""'; slashes = 0; continue; }
|
|
151
|
+
out += "\\".repeat(slashes) + ch;
|
|
152
|
+
slashes = 0;
|
|
153
|
+
}
|
|
154
|
+
// A run that reaches the end of the argument is in front of the closing quote,
|
|
155
|
+
// which is a quote like any other.
|
|
156
|
+
return `${out}${"\\".repeat(slashes * 2)}"`;
|
|
112
157
|
}
|
|
113
158
|
|
|
114
159
|
/**
|
|
@@ -340,13 +385,40 @@ export const tryNext = (err) =>
|
|
|
340
385
|
Boolean(err) && (err.code === "ENOENT" || err.code === "EACCES" ||
|
|
341
386
|
err.code === "EINVAL" || err.code === "UNKNOWN");
|
|
342
387
|
|
|
343
|
-
// The three lines cmd.exe prints when it cannot find what it was
|
|
344
|
-
// anchored to a whole line each. First the two-line pair for a
|
|
345
|
-
// the one it uses when a directory in an explicit path does not
|
|
388
|
+
// The three lines an ENGLISH cmd.exe prints when it cannot find what it was
|
|
389
|
+
// asked to run, anchored to a whole line each. First the two-line pair for a
|
|
390
|
+
// bare name, then the one it uses when a directory in an explicit path does not
|
|
391
|
+
// exist.
|
|
392
|
+
//
|
|
393
|
+
// These are one signal out of three rather than the whole answer — see
|
|
394
|
+
// looksMissing. Windows ships cmd.exe in every language it ships in, and these
|
|
395
|
+
// sentences are translated with it.
|
|
346
396
|
const CMD_UNKNOWN = /^'(.+)' is not recognized as an internal or external command,?$/i;
|
|
347
397
|
const CMD_UNKNOWN_TAIL = /^operable program or batch file\.?$/i;
|
|
348
398
|
const CMD_NO_PATH = /^the system cannot find the (?:path|file) specified\.?$/i;
|
|
349
399
|
|
|
400
|
+
// cmd.exe's own errorlevel for a command token it could not resolve — the one
|
|
401
|
+
// signal here that is not a human sentence, and therefore the only one that is
|
|
402
|
+
// the same on a German install as on an English one. It is the number every CI
|
|
403
|
+
// log in the world prints beside "is not recognized".
|
|
404
|
+
//
|
|
405
|
+
// Not every path reports it: some Windows builds answer a bare `cmd /c missing`
|
|
406
|
+
// with a plain 1 instead, which is why this is a sufficient signal and never a
|
|
407
|
+
// necessary one. A number this large cannot arrive from POSIX at all — a process
|
|
408
|
+
// exit status there is masked to 0-255 before Node ever sees it — so nothing
|
|
409
|
+
// outside cmd.exe can reach it by accident.
|
|
410
|
+
const CMD_NOT_FOUND_EXIT = 9009;
|
|
411
|
+
|
|
412
|
+
/** True when an exit status is cmd.exe saying "no such command", in any locale. */
|
|
413
|
+
export const notFoundExit = (code) => Number(code) === CMD_NOT_FOUND_EXIT;
|
|
414
|
+
|
|
415
|
+
// Every run of text this line wrapped in quotes. cmd.exe quotes the command
|
|
416
|
+
// token it could not find in EVERY locale, and the quote character is the only
|
|
417
|
+
// part of the message that is not a translation: `'…'` in English and French,
|
|
418
|
+
// `"…"` in German, Spanish, Italian, Portuguese, Polish and Russian.
|
|
419
|
+
const quotedRuns = (line) =>
|
|
420
|
+
[...String(line).matchAll(/'([^']+)'|"([^"]+)"/g)].map(m => m[1] ?? m[2]);
|
|
421
|
+
|
|
350
422
|
// cmd.exe echoes the command token exactly as it was given, so an exact match
|
|
351
423
|
// is what we expect; the comparison is only case- and quote-insensitive because
|
|
352
424
|
// Windows paths are.
|
|
@@ -409,20 +481,108 @@ const sameCommand = (quoted, name) => {
|
|
|
409
481
|
*
|
|
410
482
|
* Callers that only have the text (failureText) omit it and get the shape rules
|
|
411
483
|
* alone.
|
|
484
|
+
*
|
|
485
|
+
* ── AND NOT ONLY IN ENGLISH (#552) ──────────────────────────────────────────
|
|
486
|
+
*
|
|
487
|
+
* Everything above was written against three English sentences, and cmd.exe is
|
|
488
|
+
* translated. On a German install with cswap genuinely absent it prints
|
|
489
|
+
*
|
|
490
|
+
* Der Befehl "cswap" ist entweder falsch geschrieben oder konnte nicht
|
|
491
|
+
* gefunden werden.
|
|
492
|
+
*
|
|
493
|
+
* — so this answered false, `run` resolved `{ ok: false, code: 1 }`,
|
|
494
|
+
* claude-accounts.mjs picked `reason: "switch_failed"` over `"no_cswap"` and the
|
|
495
|
+
* panel's install affordance never appeared. failureText then fell through to
|
|
496
|
+
* firstUseful, which puts the LAST line of a localized sentence on screen by
|
|
497
|
+
* itself: #457's symptom reproduced for every non-English locale. It also
|
|
498
|
+
* stopped the candidate loop early, so a `.bat` installed after a missing `.cmd`
|
|
499
|
+
* was never reached.
|
|
500
|
+
*
|
|
501
|
+
* Adding the German sentence, and then the French and the Japanese ones, is not
|
|
502
|
+
* a fix — it is the same defect with a longer list. So two signals that are not
|
|
503
|
+
* sentences carry the answer instead, and the English text is what remains when
|
|
504
|
+
* neither is available:
|
|
505
|
+
*
|
|
506
|
+
* 1. THE EXIT STATUS. `exitCode` 9009 is cmd.exe's own errorlevel for a
|
|
507
|
+
* command token it could not resolve, identical in every language. See
|
|
508
|
+
* CMD_NOT_FOUND_EXIT for why it is not required, and the paragraph below
|
|
509
|
+
* for the two cases where it is not believed either. It is subject to the
|
|
510
|
+
* same "cmd.exe printed nothing else" cap as rule 2, because a status is
|
|
511
|
+
* forwarded as easily as a sentence is.
|
|
512
|
+
*
|
|
513
|
+
* 2. THE SHAPE. cmd.exe quotes the command it could not find, in every locale,
|
|
514
|
+
* and prints that INSTEAD of running anything — so the whole output is at
|
|
515
|
+
* most the two lines of one wrapped sentence. Text of at most two lines
|
|
516
|
+
* whose FIRST line quotes exactly the spelling we launched is cmd.exe's
|
|
517
|
+
* verdict about our command whatever the words around it say.
|
|
518
|
+
*
|
|
519
|
+
* Rule 2 needs `name`, and refuses without it. That is the same principle the
|
|
520
|
+
* paragraphs above argue for and not a limitation bolted on: `Error: "account-9"
|
|
521
|
+
* does not exist` is one line with a quoted token in it, and read as an absence
|
|
522
|
+
* it would send the candidate loop back round to re-run `cswap remove 3`. What
|
|
523
|
+
* makes the rule safe is that the quoted token has to be the exact spelling
|
|
524
|
+
* cmd.exe was handed — `cswap.cmd`, or the absolute path shimPath found — which
|
|
525
|
+
* is a string the tool underneath has no reason to print. The two-line cap is
|
|
526
|
+
* the other half: a Python traceback ending in "The system cannot find the file
|
|
527
|
+
* specified" is four lines and can never qualify.
|
|
528
|
+
*
|
|
529
|
+
* THE SAME TRAP THE ENGLISH RULE ALREADY AVOIDS, NOW FOR THE EXIT STATUS. A
|
|
530
|
+
* `.cmd` shim is itself a batch file, so a shim that EXISTS and whose payload
|
|
531
|
+
* interpreter does not — a scoop or npm-style `cswap.cmd` in front of a python
|
|
532
|
+
* that was uninstalled — has cmd.exe print "is not recognized" about PYTHON and
|
|
533
|
+
* hands the shim's caller that same 9009. Believed on its own, the deck would
|
|
534
|
+
* call the tool absent and re-run the command under the next spelling. So a text
|
|
535
|
+
* that positively names a command OTHER than ours vetoes every rule here,
|
|
536
|
+
* including the status: the check that made #457 safe, applied one level up.
|
|
537
|
+
*
|
|
538
|
+
* What is deliberately still missed: a LOCALIZED "the system cannot find the
|
|
539
|
+
* path specified", which carries no quoted token and no structure to key off.
|
|
540
|
+
* That case only arises when shimPath found a shim that then vanished, and it
|
|
541
|
+
* fails in the safe direction — an honest exit 1 rather than a wrong ENOENT.
|
|
412
542
|
*/
|
|
413
|
-
export function looksMissing(text, name = "") {
|
|
543
|
+
export function looksMissing(text, name = "", exitCode = null) {
|
|
414
544
|
const lines = String(text ?? "").split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
545
|
+
// Whatever cmd.exe named, in whatever language it said the rest — the quoting
|
|
546
|
+
// is the part that is not a translation.
|
|
547
|
+
const named = lines.length ? quotedRuns(lines[0]) : [];
|
|
548
|
+
const namesUs = named.some(q => sameCommand(q, name));
|
|
549
|
+
|
|
550
|
+
// The veto, before anything is believed: a message about somebody else's
|
|
551
|
+
// command is not evidence about ours, and neither is the status that came
|
|
552
|
+
// with it. See the header — a `.cmd` shim in front of a missing interpreter
|
|
553
|
+
// forwards both.
|
|
554
|
+
if (name && named.length > 0 && !namesUs) return false;
|
|
555
|
+
|
|
556
|
+
// cmd.exe says this INSTEAD of running anything, so anything longer than one
|
|
557
|
+
// wrapped sentence came from something that DID run — and that outranks both
|
|
558
|
+
// signals below. A shim can forward its child's 9009 after printing pages of
|
|
559
|
+
// its own; a Python traceback is four lines and one of them quotes the very
|
|
560
|
+
// shim we launched.
|
|
561
|
+
const saidNothingElse = lines.length <= 2;
|
|
562
|
+
|
|
563
|
+
// The signal that is not a sentence. It does not need to READ the text, which
|
|
564
|
+
// is the whole reason it exists: on a non-English install there may be nothing
|
|
565
|
+
// in the text this can read.
|
|
566
|
+
if (saidNothingElse && notFoundExit(exitCode)) return true;
|
|
415
567
|
if (!lines.length) return false;
|
|
568
|
+
|
|
569
|
+
// The English shapes, whole-line anchored, exactly as before.
|
|
570
|
+
let english = true;
|
|
416
571
|
for (const line of lines) {
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
419
|
-
if (name && !sameCommand(
|
|
572
|
+
const unknown = CMD_UNKNOWN.exec(line);
|
|
573
|
+
if (unknown) {
|
|
574
|
+
if (name && !sameCommand(unknown[1], name)) return false;
|
|
420
575
|
continue;
|
|
421
576
|
}
|
|
422
577
|
if (CMD_UNKNOWN_TAIL.test(line) || CMD_NO_PATH.test(line)) continue;
|
|
423
|
-
|
|
578
|
+
english = false;
|
|
579
|
+
break;
|
|
424
580
|
}
|
|
425
|
-
return true;
|
|
581
|
+
if (english) return true;
|
|
582
|
+
|
|
583
|
+
// Otherwise: cmd.exe in some other language, recognised by its shape and by
|
|
584
|
+
// the one word in it that is ours.
|
|
585
|
+
return Boolean(name) && saidNothingElse && namesUs;
|
|
426
586
|
}
|
|
427
587
|
|
|
428
588
|
// How much of a hung child's output the deadline keeps. The full buffers belong
|
|
@@ -485,7 +645,11 @@ export function run(cmd, args, { timeout = 20_000, maxBuffer = 4 << 20, env } =
|
|
|
485
645
|
// be a shell's verdict rather than the tool's own words — spawned
|
|
486
646
|
// directly, a missing file is a plain ENOENT and anything printed came
|
|
487
647
|
// from a tool that ran.
|
|
488
|
-
|
|
648
|
+
// `err.code` is the exit STATUS for a child that ran and failed, which
|
|
649
|
+
// is where cmd.exe's language-independent 9009 arrives; for a spawn
|
|
650
|
+
// failure it is an errno string, and Number() of that is NaN. Either
|
|
651
|
+
// way looksMissing is handed what the attempt actually reported.
|
|
652
|
+
const missing = Boolean(err) && tree && looksMissing(`${stderr ?? ""}\n${stdout ?? ""}`, launch, err.code);
|
|
489
653
|
if (err && (tryNext(err) || missing) && i + 1 < tries.length) return attempt(i + 1);
|
|
490
654
|
// The CANDIDATE is what gets remembered, never the resolved path. The
|
|
491
655
|
// memo is the only entry `candidates` offers afterwards, so recording an
|
|
@@ -561,14 +725,17 @@ export function run(cmd, args, { timeout = 20_000, maxBuffer = 4 << 20, env } =
|
|
|
561
725
|
*
|
|
562
726
|
* Returns immediately with a handle:
|
|
563
727
|
* write(text) — into the child's stdin
|
|
564
|
-
* kill() — give up; `done`
|
|
728
|
+
* kill() — give up; `done` settles within killGrace either way
|
|
565
729
|
* onLine(cb) — every complete stdout/stderr line as it arrives
|
|
566
730
|
* done — Promise<{ok, code, killed, timedOut, stdout, stderr}>
|
|
567
731
|
*
|
|
568
|
-
* Never rejects, for the same reason `run` never does.
|
|
569
|
-
*
|
|
732
|
+
* Never rejects, for the same reason `run` never does. And never stays pending:
|
|
733
|
+
* the deadline answers with `code: "ETIMEDOUT", timedOut: true` at the moment
|
|
734
|
+
* it expires rather than waiting on a 'close' a surviving descendant can hold
|
|
735
|
+
* back forever — see the timer below, and #614 for what that cost. Same Windows
|
|
736
|
+
* candidate resolution, since `claude` and `cswap` are `.cmd` shims there.
|
|
570
737
|
*/
|
|
571
|
-
export function runInteractive(cmd, args, { timeout = 300_000, maxOutput = 256 << 10 } = {}) {
|
|
738
|
+
export function runInteractive(cmd, args, { timeout = 300_000, maxOutput = 256 << 10, killGrace = 2_000 } = {}) {
|
|
572
739
|
const tries = candidates(cmd);
|
|
573
740
|
const lineSubs = [];
|
|
574
741
|
let child = null;
|
|
@@ -596,15 +763,39 @@ export function runInteractive(cmd, args, { timeout = 300_000, maxOutput = 256 <
|
|
|
596
763
|
}
|
|
597
764
|
};
|
|
598
765
|
|
|
766
|
+
let graceTimer = null;
|
|
767
|
+
|
|
599
768
|
const finish = (code, err) => {
|
|
600
769
|
if (!settle) return;
|
|
601
770
|
const s = settle; settle = null;
|
|
602
771
|
clearTimeout(timer);
|
|
772
|
+
clearTimeout(graceTimer);
|
|
603
773
|
s({ ok: code === 0 && !err && !timedOut, code: err?.code ?? code ?? -1, killed, timedOut, stdout, stderr });
|
|
604
774
|
};
|
|
605
775
|
|
|
776
|
+
// The deadline states the outcome and only then kills — the order `run` uses
|
|
777
|
+
// forty lines above, for the reason its own header already spells out.
|
|
778
|
+
//
|
|
779
|
+
// This used to set the flag, kill, and leave `done` to the child's 'close'.
|
|
780
|
+
// 'close' waits for the stdio pipes, not merely for the exit, so ONE
|
|
781
|
+
// descendant that outlives the kill holding the inherited stdout keeps it
|
|
782
|
+
// from ever arriving: the child is dead, the promise is pending, and it stays
|
|
783
|
+
// pending for the life of the process. On Windows that is the ordinary shape
|
|
784
|
+
// rather than a corner — a `.cmd` shim runs the real tool as a grandchild
|
|
785
|
+
// under cmd.exe, so a taskkill that cannot run reaches only the wrapper — and
|
|
786
|
+
// on macOS/Linux any cswap subprocess still alive when SIGTERM lands does it.
|
|
787
|
+
//
|
|
788
|
+
// What it cost: both callers await this INSIDE withStoreLock, so the pending
|
|
789
|
+
// promise is the accounts mutex. Every later mutation — login, cancel,
|
|
790
|
+
// import, remove, alias, reorder — queued behind a link that would never
|
|
791
|
+
// settle, the HTTP request was never answered, and spawnLogin's
|
|
792
|
+
// process-on-exit handler leaked with the promise. Reported as #614.
|
|
606
793
|
const timer = setTimeout(() => {
|
|
607
794
|
timedOut = true;
|
|
795
|
+
killed = true;
|
|
796
|
+
// Whatever the child managed to print rides along, the way run()'s tail
|
|
797
|
+
// does: it had not finished, but it is all a caller has to go on.
|
|
798
|
+
finish(-1, { code: "ETIMEDOUT" });
|
|
608
799
|
killTree(child);
|
|
609
800
|
}, timeout);
|
|
610
801
|
timer.unref?.();
|
|
@@ -658,12 +849,18 @@ export function runInteractive(cmd, args, { timeout = 300_000, maxOutput = 256 <
|
|
|
658
849
|
proc.stderr?.on("data", (d) => { if (stale()) return; const t = String(d); stderr = keep(stderr, t); emitLines(t); });
|
|
659
850
|
proc.on("close", (code) => {
|
|
660
851
|
if (stale()) return;
|
|
852
|
+
// Already answered — by the deadline above, or by kill()'s grace below.
|
|
853
|
+
// A late 'close' has nothing left to report, and this is not merely
|
|
854
|
+
// tidiness: the retry underneath re-runs the WHOLE command, and re-running
|
|
855
|
+
// `cswap remove` on behalf of a promise nobody is waiting for any more is
|
|
856
|
+
// exactly the thing that must not happen.
|
|
857
|
+
if (!settle) return;
|
|
661
858
|
// Same cmd.exe case as in `run`: exit 1 with "is not recognized" means
|
|
662
859
|
// this spelling does not exist, not that the tool failed. Restricted to a
|
|
663
860
|
// batch candidate, which is the only kind launched through a shell, and
|
|
664
861
|
// to output that is cmd.exe's message alone — everything below re-runs
|
|
665
862
|
// the whole command, and these commands remove accounts.
|
|
666
|
-
if (code !== 0 && isBatch(raw) && looksMissing(`${stderr}\n${stdout}`, launch)) {
|
|
863
|
+
if (code !== 0 && isBatch(raw) && looksMissing(`${stderr}\n${stdout}`, launch, code)) {
|
|
667
864
|
if (i + 1 < tries.length) {
|
|
668
865
|
stdout = ""; stderr = ""; pending = "";
|
|
669
866
|
child = null;
|
|
@@ -689,10 +886,20 @@ export function runInteractive(cmd, args, { timeout = 300_000, maxOutput = 256 <
|
|
|
689
886
|
try { child?.stdin?.end(); } catch { /* already closed */ }
|
|
690
887
|
},
|
|
691
888
|
/** Stop the run. On Windows that means the tool under the cmd.exe wrapper
|
|
692
|
-
* too — see killTree; a cancelled sign-in used to leave it running.
|
|
889
|
+
* too — see killTree; a cancelled sign-in used to leave it running.
|
|
890
|
+
*
|
|
891
|
+
* `done` settles either way. The kill cannot promise the pipes close —
|
|
892
|
+
* that is the deadline's problem reached from the other side — so if the
|
|
893
|
+
* child's own 'close' has not arrived within killGrace, the handle answers
|
|
894
|
+
* without it. A cancelled sign-in must not be able to wedge the accounts
|
|
895
|
+
* mutex any more than an expired one. */
|
|
693
896
|
kill() {
|
|
694
897
|
killed = true;
|
|
695
898
|
killTree(child);
|
|
899
|
+
if (settle && !graceTimer) {
|
|
900
|
+
graceTimer = setTimeout(() => finish(-1, null), killGrace);
|
|
901
|
+
graceTimer.unref?.();
|
|
902
|
+
}
|
|
696
903
|
},
|
|
697
904
|
onLine(cb) { lineSubs.push(cb); },
|
|
698
905
|
done,
|