@runuai/host 0.8.28 → 0.8.33
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/lib/agents/claude.ts +27 -29
- package/lib/agents/cursor.ts +11 -1
- package/lib/agents/factory.ts +26 -0
- package/lib/agents/usage.ts +37 -0
- package/lib/standard-image.ts +57 -8
- package/package.json +1 -1
- package/runner/runner.mjs +9 -3
package/lib/agents/claude.ts
CHANGED
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
import { newId } from "../ulid";
|
|
24
24
|
import { createAgentTransport, type LineTransport } from "./transport";
|
|
25
25
|
import { register } from "./registry";
|
|
26
|
+
import { extractResultUsage } from "./usage";
|
|
26
27
|
import type {
|
|
27
28
|
AgentEvent,
|
|
28
29
|
AgentEventHandler,
|
|
29
30
|
AgentKind,
|
|
30
31
|
AgentSession,
|
|
31
|
-
AgentUsage,
|
|
32
32
|
RosterAgent,
|
|
33
33
|
} from "./types";
|
|
34
34
|
|
|
@@ -60,32 +60,6 @@ function isObj(v: unknown): v is Record<string, unknown> {
|
|
|
60
60
|
return typeof v === "object" && v !== null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function num(v: unknown): number | undefined {
|
|
64
|
-
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Token + cost accounting from a Claude `result` line. Claude Code reports the
|
|
69
|
-
* exact `total_cost_usd` (no estimation needed) plus a token `usage` breakdown
|
|
70
|
-
* and per-model `modelUsage`. The billed model is the single modelUsage key
|
|
71
|
-
* (or, on a multi-model turn, joined).
|
|
72
|
-
*/
|
|
73
|
-
function claudeUsage(json: Record<string, unknown>): AgentUsage | undefined {
|
|
74
|
-
const u = isObj(json.usage) ? json.usage : {};
|
|
75
|
-
const cost = num(json.total_cost_usd);
|
|
76
|
-
const models = isObj(json.modelUsage) ? Object.keys(json.modelUsage) : [];
|
|
77
|
-
const usage: AgentUsage = {
|
|
78
|
-
model: models.length ? models.join(", ") : undefined,
|
|
79
|
-
inputTokens: num(u.input_tokens),
|
|
80
|
-
outputTokens: num(u.output_tokens),
|
|
81
|
-
cacheReadTokens: num(u.cache_read_input_tokens),
|
|
82
|
-
cacheCreateTokens: num(u.cache_creation_input_tokens),
|
|
83
|
-
costUsd: cost,
|
|
84
|
-
};
|
|
85
|
-
// Nothing usable reported → omit rather than send an empty object.
|
|
86
|
-
return Object.values(usage).some((v) => v !== undefined) ? usage : undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
63
|
/**
|
|
90
64
|
* Map one stream-json stdout line to zero or more AgentEvents.
|
|
91
65
|
*
|
|
@@ -146,7 +120,7 @@ export function mapClaudeLine(raw: string): AgentEvent[] {
|
|
|
146
120
|
// --- turn result ------------------------------------------------------
|
|
147
121
|
if (type === "result") {
|
|
148
122
|
const text = typeof json.result === "string" ? json.result : "";
|
|
149
|
-
const usage =
|
|
123
|
+
const usage = extractResultUsage(json);
|
|
150
124
|
if (json.is_error === true) {
|
|
151
125
|
// An errored turn still cost tokens — meter it.
|
|
152
126
|
return [
|
|
@@ -235,6 +209,13 @@ export class ClaudeSession implements AgentSession {
|
|
|
235
209
|
private readonly proc: LineTransport;
|
|
236
210
|
private readonly handlers = new Set<AgentEventHandler>();
|
|
237
211
|
private closed = false;
|
|
212
|
+
// Claude's `total_cost_usd` is CUMULATIVE across the persistent session
|
|
213
|
+
// process (ADR-061 keeps one claude alive across turns), so per-turn cost is
|
|
214
|
+
// the delta from the previous turn — otherwise summing turns over-counts
|
|
215
|
+
// wildly (live 2026-07-22: a 4-turn task billed ~4x). Token counts, by
|
|
216
|
+
// contrast, are per-turn already. Reset per session; a respawn makes a fresh
|
|
217
|
+
// ClaudeSession, so this starts at 0 alongside the CLI's own counter.
|
|
218
|
+
private lastCumulativeCostUsd = 0;
|
|
238
219
|
|
|
239
220
|
constructor(args: {
|
|
240
221
|
taskId: string;
|
|
@@ -288,7 +269,7 @@ export class ClaudeSession implements AgentSession {
|
|
|
288
269
|
debugLabel: `claude:${this.agentId}`,
|
|
289
270
|
});
|
|
290
271
|
this.proc.onLine((line) => {
|
|
291
|
-
for (const event of mapClaudeLine(line)) this.emit(event);
|
|
272
|
+
for (const event of mapClaudeLine(line)) this.emit(this.perTurnCost(event));
|
|
292
273
|
});
|
|
293
274
|
this.proc.onExit((code) => {
|
|
294
275
|
if (this.closed) return;
|
|
@@ -327,6 +308,23 @@ export class ClaudeSession implements AgentSession {
|
|
|
327
308
|
for (const h of this.handlers) h(event);
|
|
328
309
|
}
|
|
329
310
|
|
|
311
|
+
/** Convert Claude's cumulative session cost to this turn's delta. Tokens are
|
|
312
|
+
* already per-turn and pass through untouched. */
|
|
313
|
+
private perTurnCost(event: AgentEvent): AgentEvent {
|
|
314
|
+
if (event.type !== "turn_complete" || event.usage?.costUsd === undefined) {
|
|
315
|
+
return event;
|
|
316
|
+
}
|
|
317
|
+
const cumulative = event.usage.costUsd;
|
|
318
|
+
const delta = cumulative - this.lastCumulativeCostUsd;
|
|
319
|
+
this.lastCumulativeCostUsd = cumulative;
|
|
320
|
+
// A negative delta means the CLI's counter reset (a fresh process) — the
|
|
321
|
+
// reported value is then already this turn's cost.
|
|
322
|
+
return {
|
|
323
|
+
...event,
|
|
324
|
+
usage: { ...event.usage, costUsd: delta >= 0 ? delta : cumulative },
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
330
328
|
async send(text: string): Promise<void> {
|
|
331
329
|
if (this.closed) return;
|
|
332
330
|
this.proc.writeLine({
|
package/lib/agents/cursor.ts
CHANGED
|
@@ -31,11 +31,13 @@ import { spawn, type ChildProcess } from "node:child_process";
|
|
|
31
31
|
|
|
32
32
|
import { newId } from "../ulid";
|
|
33
33
|
import { register } from "./registry";
|
|
34
|
+
import { extractResultUsage } from "./usage";
|
|
34
35
|
import type {
|
|
35
36
|
AgentEvent,
|
|
36
37
|
AgentEventHandler,
|
|
37
38
|
AgentKind,
|
|
38
39
|
AgentSession,
|
|
40
|
+
AgentUsage,
|
|
39
41
|
RosterAgent,
|
|
40
42
|
} from "./types";
|
|
41
43
|
|
|
@@ -83,6 +85,9 @@ export interface MappedCursorLine {
|
|
|
83
85
|
finalText?: string;
|
|
84
86
|
sessionId?: string;
|
|
85
87
|
errorText?: string;
|
|
88
|
+
/** Token/cost from the result envelope — Cursor uses the Claude-compatible
|
|
89
|
+
* shape (usage + total_cost_usd + modelUsage). ADR-071 metering. */
|
|
90
|
+
usage?: AgentUsage;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
export function mapCursorLine(line: string): MappedCursorLine {
|
|
@@ -131,6 +136,7 @@ export function mapCursorLine(line: string): MappedCursorLine {
|
|
|
131
136
|
? m.result
|
|
132
137
|
: "cursor turn failed"
|
|
133
138
|
: undefined,
|
|
139
|
+
usage: extractResultUsage(m),
|
|
134
140
|
};
|
|
135
141
|
}
|
|
136
142
|
// thinking / user / other → nothing.
|
|
@@ -235,6 +241,9 @@ export class CursorSession implements AgentSession {
|
|
|
235
241
|
let acc = "";
|
|
236
242
|
let sawText = false;
|
|
237
243
|
let buf = "";
|
|
244
|
+
// Usage rides the result line but turn_complete fires on process exit;
|
|
245
|
+
// capture it here and attach it below (ADR-071 metering).
|
|
246
|
+
let turnUsage: AgentUsage | undefined;
|
|
238
247
|
const consume = (chunk: string): void => {
|
|
239
248
|
buf += chunk;
|
|
240
249
|
let nl: number;
|
|
@@ -279,6 +288,7 @@ export class CursorSession implements AgentSession {
|
|
|
279
288
|
});
|
|
280
289
|
}
|
|
281
290
|
if (m.end) {
|
|
291
|
+
if (m.usage) turnUsage = m.usage;
|
|
282
292
|
if (m.errorText) this.emit({ type: "error", message: m.errorText });
|
|
283
293
|
const finalText = m.finalText ?? acc;
|
|
284
294
|
if (sawText || finalText) {
|
|
@@ -307,7 +317,7 @@ export class CursorSession implements AgentSession {
|
|
|
307
317
|
message: `cursor exited ${code ?? "null"}${tail ? `: ${tail}` : ""}`,
|
|
308
318
|
});
|
|
309
319
|
}
|
|
310
|
-
this.emit({ type: "turn_complete" });
|
|
320
|
+
this.emit({ type: "turn_complete", usage: turnUsage });
|
|
311
321
|
resolve();
|
|
312
322
|
};
|
|
313
323
|
child.on("exit", (code) => finish(code));
|
package/lib/agents/factory.ts
CHANGED
|
@@ -21,9 +21,30 @@ import "./kimi";
|
|
|
21
21
|
import "./grok";
|
|
22
22
|
import "./cursor";
|
|
23
23
|
|
|
24
|
+
import { agentClisReady } from "../standard-image";
|
|
24
25
|
import { factoryFor } from "./registry";
|
|
25
26
|
import type { AgentSession, AgentSessionFactory } from "./types";
|
|
26
27
|
|
|
28
|
+
/** Cap the wait on agentClisReady so a spawn can never hang forever if the
|
|
29
|
+
* boot reconcile never signals (e.g. an unexpected code path). Generous —
|
|
30
|
+
* the reconcile normally settles in seconds; this only guards a stuck boot. */
|
|
31
|
+
const CLIS_READY_TIMEOUT_MS = 90_000;
|
|
32
|
+
|
|
33
|
+
/** Await the shared-volume CLI reconcile, bounded by a self-clearing timeout so
|
|
34
|
+
* neither a stuck boot nor a per-spawn timer leak can bite. */
|
|
35
|
+
async function awaitAgentClisReady(): Promise<void> {
|
|
36
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
37
|
+
const bound = new Promise<void>((resolve) => {
|
|
38
|
+
timer = setTimeout(resolve, CLIS_READY_TIMEOUT_MS);
|
|
39
|
+
timer.unref?.();
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
await Promise.race([agentClisReady, bound]);
|
|
43
|
+
} finally {
|
|
44
|
+
if (timer) clearTimeout(timer);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
export const realAgentFactory: AgentSessionFactory = {
|
|
28
49
|
create: async (args): Promise<AgentSession> => {
|
|
29
50
|
const factory = factoryFor(args.agent.kind);
|
|
@@ -32,6 +53,11 @@ export const realAgentFactory: AgentSessionFactory = {
|
|
|
32
53
|
`no agent adapter registered for kind "${args.agent.kind}"`,
|
|
33
54
|
);
|
|
34
55
|
}
|
|
56
|
+
// Don't spawn a CLI until the shared-volume agent CLIs are reconciled:
|
|
57
|
+
// at boot the CLI auto-upgrade briefly removes then reinstalls codex/claude,
|
|
58
|
+
// and a resume that races that window dies with "No codex executable found
|
|
59
|
+
// for nodejs X". Post-boot this is already resolved (instant).
|
|
60
|
+
await awaitAgentClisReady();
|
|
35
61
|
return factory.create(args);
|
|
36
62
|
},
|
|
37
63
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared token/cost extraction for the Claude-Code-compatible stream-json
|
|
3
|
+
* `result` envelope (ADR-071 metering). Claude Code AND Cursor Agent both emit
|
|
4
|
+
* this exact shape — `usage` token breakdown, an exact `total_cost_usd`, and
|
|
5
|
+
* per-model `modelUsage` — so both engines share this one extractor.
|
|
6
|
+
*
|
|
7
|
+
* Codex (app-server protocol) and Kimi/Grok (plain text streams) report no
|
|
8
|
+
* token/cost data, so they have no extractor.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AgentUsage } from "./types";
|
|
12
|
+
|
|
13
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
14
|
+
return typeof v === "object" && v !== null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function num(v: unknown): number | undefined {
|
|
18
|
+
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Pull AgentUsage from a `result` line, or undefined when none is reported. */
|
|
22
|
+
export function extractResultUsage(
|
|
23
|
+
json: Record<string, unknown>,
|
|
24
|
+
): AgentUsage | undefined {
|
|
25
|
+
const u = isObj(json.usage) ? json.usage : {};
|
|
26
|
+
const models = isObj(json.modelUsage) ? Object.keys(json.modelUsage) : [];
|
|
27
|
+
const usage: AgentUsage = {
|
|
28
|
+
model: models.length ? models.join(", ") : undefined,
|
|
29
|
+
inputTokens: num(u.input_tokens),
|
|
30
|
+
outputTokens: num(u.output_tokens),
|
|
31
|
+
cacheReadTokens: num(u.cache_read_input_tokens),
|
|
32
|
+
cacheCreateTokens: num(u.cache_creation_input_tokens),
|
|
33
|
+
costUsd: num(json.total_cost_usd),
|
|
34
|
+
};
|
|
35
|
+
// Nothing usable reported → omit rather than send an empty object.
|
|
36
|
+
return Object.values(usage).some((v) => v !== undefined) ? usage : undefined;
|
|
37
|
+
}
|
package/lib/standard-image.ts
CHANGED
|
@@ -176,17 +176,28 @@ const VOLUME_AGENT_CLIS: { bin: string; pkg: string }[] = [
|
|
|
176
176
|
*/
|
|
177
177
|
async function ensureVolumeAgentClis(): Promise<void> {
|
|
178
178
|
const bins = VOLUME_AGENT_CLIS.map((c) => c.bin).join(" ");
|
|
179
|
-
//
|
|
180
|
-
//
|
|
179
|
+
// Probe by EXECUTING each bin (`--version`), not `command -v`. A stale asdf
|
|
180
|
+
// shim — the shim file survives in the volume but the reshim DB no longer
|
|
181
|
+
// maps it to an installed exe — passes `command -v` (the file is present and
|
|
182
|
+
// executable) yet aborts at runtime with "unknown command: codex. Perhaps
|
|
183
|
+
// you have to reshim?" (exit 1). That made a broken codex/claude read as
|
|
184
|
+
// "present," so the self-heal skipped it and every task/resume spawn failed,
|
|
185
|
+
// restart after restart (live 2026-07-22). Running the bin is the only probe
|
|
186
|
+
// that catches a stale shim. `-w /home/node` so asdf resolves the node
|
|
187
|
+
// version from /home/node/.tool-versions — the same one the repair installs
|
|
188
|
+
// against — and the sentinel prefix keeps login-shell noise from being
|
|
189
|
+
// misread as a missing bin.
|
|
181
190
|
const check = await run("docker", [
|
|
182
191
|
"run",
|
|
183
192
|
"--rm",
|
|
193
|
+
"-w",
|
|
194
|
+
"/home/node",
|
|
184
195
|
"-v",
|
|
185
196
|
`${ASDF_DATA_VOLUME}:/opt/asdf-data`,
|
|
186
197
|
STANDARD_IMAGE_TAG,
|
|
187
198
|
"bash",
|
|
188
199
|
"-lc",
|
|
189
|
-
`for b in ${bins}; do
|
|
200
|
+
`for b in ${bins}; do "$b" --version >/dev/null 2>&1 || echo "UAI_MISSING:$b"; done`,
|
|
190
201
|
]);
|
|
191
202
|
if (check.code !== 0) {
|
|
192
203
|
console.warn(
|
|
@@ -207,7 +218,7 @@ async function ensureVolumeAgentClis(): Promise<void> {
|
|
|
207
218
|
.join(" ");
|
|
208
219
|
console.log(
|
|
209
220
|
`[host-agent] repairing ${ASDF_DATA_VOLUME}: agent CLI(s) [${missing.join(", ")}] ` +
|
|
210
|
-
`missing
|
|
221
|
+
`broken/missing in the shared volume — reshim + install ${pkgs}`,
|
|
211
222
|
);
|
|
212
223
|
const repair = await run("docker", [
|
|
213
224
|
"run",
|
|
@@ -225,7 +236,17 @@ async function ensureVolumeAgentClis(): Promise<void> {
|
|
|
225
236
|
STANDARD_IMAGE_TAG,
|
|
226
237
|
"bash",
|
|
227
238
|
"-lc",
|
|
228
|
-
|
|
239
|
+
// Reshim FIRST and with `;` (not `&&`): the common failure is a stale shim
|
|
240
|
+
// over an already-installed package, which a bare reshim fixes offline —
|
|
241
|
+
// gating it behind a network `npm install` (which may be down) would leave
|
|
242
|
+
// it broken. Then install to cover a genuinely-missing package, and reshim
|
|
243
|
+
// again for the freshly-installed one. Every step runs regardless of the
|
|
244
|
+
// previous one's exit. Final loop re-probes by execution so the exit code
|
|
245
|
+
// (and the log below) reflect whether the bins ACTUALLY run now, not just
|
|
246
|
+
// the trailing reshim's own exit.
|
|
247
|
+
`asdf reshim nodejs; npm install -g ${pkgs}; asdf reshim nodejs; ` +
|
|
248
|
+
`for b in ${bins}; do "$b" --version >/dev/null 2>&1 || ` +
|
|
249
|
+
`{ echo "UAI_STILL_BROKEN:$b" >&2; exit 1; }; done`,
|
|
229
250
|
]);
|
|
230
251
|
if (repair.code === 0) {
|
|
231
252
|
console.log(
|
|
@@ -313,14 +334,38 @@ export interface StandardImageResult {
|
|
|
313
334
|
error?: string;
|
|
314
335
|
}
|
|
315
336
|
|
|
337
|
+
let signalClisReady: (() => void) | null = null;
|
|
338
|
+
/**
|
|
339
|
+
* Resolves once ensureStandardImage has reconciled the shared-volume agent CLIs
|
|
340
|
+
* at least once (or bailed because docker is down / the build failed). The agent
|
|
341
|
+
* factory awaits this before spawning a CLI, so a boot-time resume never races
|
|
342
|
+
* the CLI auto-upgrade's brief remove-then-reinstall window — the "No codex
|
|
343
|
+
* executable found for nodejs X" (exit 2) resume failure. Post-boot it is
|
|
344
|
+
* already resolved, so the await is a no-op.
|
|
345
|
+
*/
|
|
346
|
+
export const agentClisReady: Promise<void> = new Promise<void>((resolve) => {
|
|
347
|
+
signalClisReady = resolve;
|
|
348
|
+
});
|
|
349
|
+
|
|
316
350
|
/**
|
|
317
351
|
* Ensure the standard image and the shared asdf data volume exist, and that the
|
|
318
352
|
* agent CLIs are present inside the volume. Builds the image only when `docker
|
|
319
353
|
* image inspect` fails or the content hash is stale. Never throws — returns
|
|
320
354
|
* `{ ok, error? }` so the caller decides (boot: fire-and-forget; taskUp:
|
|
321
|
-
* surface the reason).
|
|
355
|
+
* surface the reason). Resolves `agentClisReady` on every exit path.
|
|
322
356
|
*/
|
|
323
357
|
export async function ensureStandardImage(): Promise<StandardImageResult> {
|
|
358
|
+
try {
|
|
359
|
+
return await ensureStandardImageInner();
|
|
360
|
+
} finally {
|
|
361
|
+
// Always signal — even a docker-down bail — so agent spawns never hang
|
|
362
|
+
// waiting on a reconcile that will not happen this boot.
|
|
363
|
+
signalClisReady?.();
|
|
364
|
+
signalClisReady = null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function ensureStandardImageInner(): Promise<StandardImageResult> {
|
|
324
369
|
// 1. Shared asdf data volume — idempotent.
|
|
325
370
|
const vol = await run("docker", ["volume", "create", ASDF_DATA_VOLUME]);
|
|
326
371
|
if (vol.code !== 0) {
|
|
@@ -412,11 +457,15 @@ export async function ensureStandardImage(): Promise<StandardImageResult> {
|
|
|
412
457
|
}
|
|
413
458
|
}
|
|
414
459
|
|
|
415
|
-
// 3. Reconcile the agent CLIs
|
|
460
|
+
// 3. Reconcile the agent CLIs in the shared volume (it shadows the image's
|
|
416
461
|
// shims, so a stale volume can be missing one — the claude-127 bug).
|
|
462
|
+
// Upgrade FIRST, self-heal LAST: the upgrade's `rm -rf`+reinstall fallback
|
|
463
|
+
// can leave a stale shim (a failed reinstall over a removed package), so
|
|
464
|
+
// ensureVolumeAgentClis must be the final word — it verifies every bin
|
|
465
|
+
// actually runs and repairs whatever the upgrade left broken.
|
|
417
466
|
if (imageReady) {
|
|
418
|
-
await ensureVolumeAgentClis();
|
|
419
467
|
await upgradeVolumeAgentClis();
|
|
468
|
+
await ensureVolumeAgentClis();
|
|
420
469
|
return { ok: true };
|
|
421
470
|
}
|
|
422
471
|
return {
|
package/package.json
CHANGED
package/runner/runner.mjs
CHANGED
|
@@ -64,11 +64,17 @@ function meta(kind, extra = {}) {
|
|
|
64
64
|
// exports its resolved version (ASDF_NODEJS_VERSION et al.) into our env.
|
|
65
65
|
// Passing that through would pin the CLI's own asdf shim to the WORKSPACE's
|
|
66
66
|
// node version — "No claude executable found for nodejs X" when the agent
|
|
67
|
-
// CLIs are installed under a different one.
|
|
68
|
-
//
|
|
67
|
+
// CLIs are installed under a different one. So strip the per-tool VERSION
|
|
68
|
+
// PINS only (ASDF_NODEJS_VERSION, ASDF_<TOOL>_VERSION) — NOT the whole ASDF_*
|
|
69
|
+
// namespace: ASDF_DATA_DIR (/opt/asdf-data) and ASDF_DIR (/opt/asdf) tell the
|
|
70
|
+
// shim's `asdf exec` WHERE the tools live. Drop those and asdf falls back to
|
|
71
|
+
// $HOME/.asdf (empty), so EVERY shim aborts with "unknown command: codex.
|
|
72
|
+
// Perhaps you have to reshim?" (exit 1) — the codex/claude resume failure on
|
|
73
|
+
// every host restart, host 0.8.32. `ASDF_VERSION` (asdf's own version) has no
|
|
74
|
+
// middle segment, so the pin regex leaves it — harmless either way.
|
|
69
75
|
const cliEnv = { ...process.env };
|
|
70
76
|
for (const key of Object.keys(cliEnv)) {
|
|
71
|
-
if (key
|
|
77
|
+
if (/^ASDF_.+_VERSION$/.test(key)) delete cliEnv[key];
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], env: cliEnv });
|