@kilogent/runner-dev 0.1.4 → 0.1.5
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 +29 -3
- package/dist/cli.js +1360 -516
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/daemon.ts
|
|
7
|
-
import
|
|
7
|
+
import fs14 from "node:fs";
|
|
8
8
|
import os5 from "node:os";
|
|
9
|
-
import
|
|
9
|
+
import path15 from "node:path";
|
|
10
10
|
import {
|
|
11
11
|
collection as collection8,
|
|
12
12
|
deleteField as deleteField2,
|
|
@@ -242,7 +242,18 @@ var ASSISTANT_FROZEN_ARGS = Object.freeze({
|
|
|
242
242
|
// ../shared/dist/assistantConfig.js
|
|
243
243
|
var CONFIG_DOCS = {
|
|
244
244
|
/** `crewConfig/assistant` — the workspace assistant's prompt, model, budget and prices. */
|
|
245
|
-
assistant: "assistant"
|
|
245
|
+
assistant: "assistant",
|
|
246
|
+
/**
|
|
247
|
+
* `crewConfig/engines` — which version of each engine's CLI the fleet believes works, and which
|
|
248
|
+
* versions a runner proved broken (PRD §15.82).
|
|
249
|
+
*
|
|
250
|
+
* Read by every RUNNER with the client SDK, written by nobody but the Admin SDK, exactly like
|
|
251
|
+
* the assistant contract beside it. The shipped defaults live in the runner
|
|
252
|
+
* (`engines/install/spec.ts`) rather than here on purpose: the runner is the only thing that
|
|
253
|
+
* installs, and keeping them out of a package the app also reads keeps a surface a captain can
|
|
254
|
+
* edit from ever naming a version.
|
|
255
|
+
*/
|
|
256
|
+
engines: "engines"
|
|
246
257
|
};
|
|
247
258
|
var DEFAULT_ASSISTANT_MODEL = "gemini-3.1-flash-lite";
|
|
248
259
|
var DEFAULT_DAILY_CAP_USD = 0.1;
|
|
@@ -377,10 +388,10 @@ function rates(value) {
|
|
|
377
388
|
}
|
|
378
389
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
379
390
|
}
|
|
380
|
-
function resolveAssistantConfig(raw,
|
|
391
|
+
function resolveAssistantConfig(raw, engine2) {
|
|
381
392
|
const doc13 = raw ?? {};
|
|
382
393
|
const shared = text(doc13.contract) ?? DEFAULT_ASSISTANT_CONTRACT;
|
|
383
|
-
const perEngine =
|
|
394
|
+
const perEngine = engine2 === "free" ? text(doc13.freeContract) ?? DEFAULT_FREE_CONTRACT : text(doc13.agenticContract) ?? DEFAULT_AGENTIC_CONTRACT;
|
|
384
395
|
return {
|
|
385
396
|
contract: `${shared}
|
|
386
397
|
|
|
@@ -806,11 +817,11 @@ function machineLoginPending(c, now) {
|
|
|
806
817
|
return now - c.login.requestedAt < MACHINE_LOGIN_TTL_MS ? "start" : "expire";
|
|
807
818
|
}
|
|
808
819
|
function resolveAgentCredential(agent, credentials, defaultEngine) {
|
|
809
|
-
const
|
|
820
|
+
const engine2 = agent.engine ?? defaultEngine;
|
|
810
821
|
if (agent.credentialId) {
|
|
811
822
|
return credentials.find((c) => c.id === agent.credentialId) ?? null;
|
|
812
823
|
}
|
|
813
|
-
const forEngine = credentials.filter((c) => c.engine ===
|
|
824
|
+
const forEngine = credentials.filter((c) => c.engine === engine2 && isCredentialReady(c));
|
|
814
825
|
const oldest = [...forEngine].sort((a, b) => a.createdAt - b.createdAt)[0];
|
|
815
826
|
return oldest ?? void 0;
|
|
816
827
|
}
|
|
@@ -887,9 +898,9 @@ function str(input, key) {
|
|
|
887
898
|
const value = input[key];
|
|
888
899
|
return typeof value === "string" && value.trim() ? value : void 0;
|
|
889
900
|
}
|
|
890
|
-
function basename(
|
|
891
|
-
const parts =
|
|
892
|
-
return parts[parts.length - 1] ??
|
|
901
|
+
function basename(path17) {
|
|
902
|
+
const parts = path17.split(/[\\/]/).filter(Boolean);
|
|
903
|
+
return parts[parts.length - 1] ?? path17;
|
|
893
904
|
}
|
|
894
905
|
function hostOf(url) {
|
|
895
906
|
try {
|
|
@@ -977,8 +988,8 @@ function builtinDetail(tool, input) {
|
|
|
977
988
|
case "Write":
|
|
978
989
|
case "Edit":
|
|
979
990
|
case "MultiEdit": {
|
|
980
|
-
const
|
|
981
|
-
return
|
|
991
|
+
const path17 = str(input, "file_path");
|
|
992
|
+
return path17 ? basename(path17) : void 0;
|
|
982
993
|
}
|
|
983
994
|
case "Glob":
|
|
984
995
|
case "Grep":
|
|
@@ -1246,7 +1257,7 @@ import os from "node:os";
|
|
|
1246
1257
|
import path from "node:path";
|
|
1247
1258
|
|
|
1248
1259
|
// src/version.ts
|
|
1249
|
-
var RUNNER_VERSION = true ? "0.1.
|
|
1260
|
+
var RUNNER_VERSION = true ? "0.1.5" : "0.0.0-dev";
|
|
1250
1261
|
var RUNNER_PACKAGE = true ? "@kilogent/runner-dev" : "@kilogent/runner-dev";
|
|
1251
1262
|
var RUNNER_BIN = true ? "kilogent-runner-dev" : "kilogent-runner-dev";
|
|
1252
1263
|
|
|
@@ -1358,11 +1369,15 @@ function allowsLocalMcp(config2, shipId) {
|
|
|
1358
1369
|
var TOGGLE_DEFAULTS = {
|
|
1359
1370
|
notifications: false,
|
|
1360
1371
|
keepAwake: true,
|
|
1361
|
-
autoUpdate: true
|
|
1372
|
+
autoUpdate: true,
|
|
1373
|
+
autoInstallEngines: true
|
|
1362
1374
|
};
|
|
1363
1375
|
function notificationsEnabled(config2) {
|
|
1364
1376
|
return config2?.notifications ?? TOGGLE_DEFAULTS.notifications;
|
|
1365
1377
|
}
|
|
1378
|
+
function engineInstallAllowed(config2) {
|
|
1379
|
+
return config2?.autoInstallEngines ?? TOGGLE_DEFAULTS.autoInstallEngines;
|
|
1380
|
+
}
|
|
1366
1381
|
function mcpUrl(config2) {
|
|
1367
1382
|
return process.env.CREW_MCP_URL || config2.mcpUrl || (config2.projectId === DEFAULT_PROJECT_ID ? DEFAULT_MCP_URL : `https://us-central1-${config2.projectId}.cloudfunctions.net/workspaceMcp`);
|
|
1368
1383
|
}
|
|
@@ -2480,12 +2495,12 @@ async function resolveAssistantCredential(db, shipId, machine) {
|
|
|
2480
2495
|
const credentialId = settings?.assistant?.credentialId ?? settings?.defaultCredentialId;
|
|
2481
2496
|
const credentials = await loadShipCredentials(db, shipId).catch(() => []);
|
|
2482
2497
|
const credential = credentialId ? credentials.find((c) => c.id === credentialId) : void 0;
|
|
2483
|
-
const
|
|
2498
|
+
const engine2 = credential?.engine ?? DEFAULT_ENGINE_ID;
|
|
2484
2499
|
const secrets = await resolveJobSecrets({
|
|
2485
2500
|
db,
|
|
2486
2501
|
shipId,
|
|
2487
2502
|
// The synthetic agent: two fields, both of which this module has just decided.
|
|
2488
|
-
agent: { engine, credentialId },
|
|
2503
|
+
agent: { engine: engine2, credentialId },
|
|
2489
2504
|
credentials,
|
|
2490
2505
|
machine
|
|
2491
2506
|
});
|
|
@@ -2494,22 +2509,220 @@ async function resolveAssistantCredential(db, shipId, machine) {
|
|
|
2494
2509
|
// A MODEL, ALWAYS. `engines/claude.ts` puts this straight onto `--model`, and an empty string
|
|
2495
2510
|
// is an empty flag — a failure that reads as the model misbehaving rather than as a missing
|
|
2496
2511
|
// value. The engine's own default is the right answer when nothing has been chosen.
|
|
2497
|
-
model: getEngine(
|
|
2512
|
+
model: getEngine(engine2)?.defaultModelId ?? ""
|
|
2498
2513
|
};
|
|
2499
2514
|
}
|
|
2500
2515
|
|
|
2501
2516
|
// src/engines/claude.ts
|
|
2502
2517
|
import { spawn as spawn3 } from "node:child_process";
|
|
2503
|
-
import
|
|
2518
|
+
import fs5 from "node:fs";
|
|
2504
2519
|
import os2 from "node:os";
|
|
2520
|
+
import path6 from "node:path";
|
|
2521
|
+
|
|
2522
|
+
// src/engines/binary.ts
|
|
2523
|
+
import fs4 from "node:fs";
|
|
2524
|
+
|
|
2525
|
+
// src/engines/install/spec.ts
|
|
2505
2526
|
import path3 from "node:path";
|
|
2527
|
+
var ENGINE_PACKAGES = {
|
|
2528
|
+
codex: { npmPackage: "@openai/codex", binName: "codex" },
|
|
2529
|
+
claude: { npmPackage: "@anthropic-ai/claude-code", binName: "claude" }
|
|
2530
|
+
};
|
|
2531
|
+
function enginePackageFor(engineId) {
|
|
2532
|
+
return Object.prototype.hasOwnProperty.call(ENGINE_PACKAGES, engineId) ? ENGINE_PACKAGES[engineId] : null;
|
|
2533
|
+
}
|
|
2534
|
+
function installableEngineIds() {
|
|
2535
|
+
return Object.keys(ENGINE_PACKAGES);
|
|
2536
|
+
}
|
|
2537
|
+
var EXACT_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[A-Za-z0-9.-]+)?$/;
|
|
2538
|
+
function isExactVersion(version) {
|
|
2539
|
+
return typeof version === "string" && version.length <= 64 && EXACT_VERSION.test(version);
|
|
2540
|
+
}
|
|
2541
|
+
var SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
2542
|
+
function assertPathSegment(segment) {
|
|
2543
|
+
if (!SEGMENT.test(segment)) throw new Error(`not a valid path segment: ${JSON.stringify(segment)}`);
|
|
2544
|
+
}
|
|
2545
|
+
function enginePrefixDir(root, engineId) {
|
|
2546
|
+
assertPathSegment(engineId);
|
|
2547
|
+
return path3.join(root, "tools", engineId);
|
|
2548
|
+
}
|
|
2549
|
+
function engineBinPath(root, engineId, binName, platform = process.platform) {
|
|
2550
|
+
assertPathSegment(engineId);
|
|
2551
|
+
assertPathSegment(binName);
|
|
2552
|
+
const prefix = enginePrefixDir(root, engineId);
|
|
2553
|
+
return platform === "win32" ? path3.join(prefix, binName) : path3.join(prefix, "bin", binName);
|
|
2554
|
+
}
|
|
2555
|
+
function engineMarkerPath(root, engineId) {
|
|
2556
|
+
return path3.join(enginePrefixDir(root, engineId), "installed.json");
|
|
2557
|
+
}
|
|
2558
|
+
var DEFAULT_ENGINE_POLICIES = {
|
|
2559
|
+
codex: { knownGood: "0.152.0", blocked: [], minAgeHours: 24 },
|
|
2560
|
+
claude: { knownGood: "2.1.258", blocked: [], minAgeHours: 24 }
|
|
2561
|
+
};
|
|
2562
|
+
var MAX_BLOCKED_VERSIONS = 20;
|
|
2563
|
+
var ENGINE_FIRST_CHECK_MS = 6e4;
|
|
2564
|
+
var ENGINE_CHECK_MS = 6 * 36e5;
|
|
2565
|
+
function resolveEnginePolicy(engineId, doc13) {
|
|
2566
|
+
const shipped = DEFAULT_ENGINE_POLICIES[engineId] ?? { knownGood: "", blocked: [], minAgeHours: 24 };
|
|
2567
|
+
const raw = doc13?.[engineId];
|
|
2568
|
+
if (!raw || typeof raw !== "object") return shipped;
|
|
2569
|
+
const entry = raw;
|
|
2570
|
+
const blocked = Array.isArray(entry.blocked) ? entry.blocked.filter(isExactVersion) : shipped.blocked;
|
|
2571
|
+
const age = typeof entry.minAgeHours === "number" && Number.isFinite(entry.minAgeHours) && entry.minAgeHours >= 0 ? Math.min(entry.minAgeHours, 24 * 30) : shipped.minAgeHours;
|
|
2572
|
+
return {
|
|
2573
|
+
knownGood: isExactVersion(entry.knownGood) ? entry.knownGood : shipped.knownGood,
|
|
2574
|
+
blocked: blocked.slice(-MAX_BLOCKED_VERSIONS),
|
|
2575
|
+
minAgeHours: age
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function chooseEngineVersion(input) {
|
|
2579
|
+
const { policy, latest, installed, firstSeen, now } = input;
|
|
2580
|
+
const blocked = new Set(policy.blocked);
|
|
2581
|
+
if (installed && blocked.has(installed)) {
|
|
2582
|
+
return isExactVersion(policy.knownGood) && !blocked.has(policy.knownGood) ? { version: policy.knownGood, reason: "known-good" } : { version: null, reason: "no-policy" };
|
|
2583
|
+
}
|
|
2584
|
+
if (!installed) {
|
|
2585
|
+
if (!isExactVersion(policy.knownGood) || blocked.has(policy.knownGood)) {
|
|
2586
|
+
return { version: null, reason: "no-policy" };
|
|
2587
|
+
}
|
|
2588
|
+
return { version: policy.knownGood, reason: "known-good" };
|
|
2589
|
+
}
|
|
2590
|
+
if (!latest || !isExactVersion(latest)) return { version: null, reason: "no-registry" };
|
|
2591
|
+
if (latest === installed) return { version: null, reason: "up-to-date" };
|
|
2592
|
+
if (blocked.has(latest)) return { version: null, reason: "blocked-latest" };
|
|
2593
|
+
if (!firstSeen || firstSeen.version !== latest) return { version: null, reason: "soaking" };
|
|
2594
|
+
if (now - firstSeen.at < policy.minAgeHours * 36e5) return { version: null, reason: "soaking" };
|
|
2595
|
+
return { version: latest, reason: "upgrade" };
|
|
2596
|
+
}
|
|
2597
|
+
var MAX_ENGINE_INSTALL_ATTEMPTS = 3;
|
|
2598
|
+
var ENGINE_BACKOFF_MS = [15 * 6e4, 2 * 36e5, 12 * 36e5];
|
|
2599
|
+
function decideEngineInstall(facts) {
|
|
2600
|
+
if (!enginePackageFor(facts.engineId)) return { install: false, reason: "unknown-engine" };
|
|
2601
|
+
if (!facts.autoInstall) return { install: false, reason: "disabled" };
|
|
2602
|
+
if (facts.platform === "win32") return { install: false, reason: "windows" };
|
|
2603
|
+
if (!isExactVersion(facts.target)) return { install: false, reason: "unknown-engine" };
|
|
2604
|
+
const pkg = enginePackageFor(facts.engineId);
|
|
2605
|
+
if (facts.marker && facts.marker.npmPackage === pkg.npmPackage && facts.marker.version === facts.target) {
|
|
2606
|
+
return { install: false, reason: "already-installed" };
|
|
2607
|
+
}
|
|
2608
|
+
if (!facts.npmPresent()) return { install: false, reason: "no-npm" };
|
|
2609
|
+
if (facts.locked) return { install: false, reason: "locked" };
|
|
2610
|
+
const record = facts.record;
|
|
2611
|
+
if (record && record.target === facts.target && record.outcome === "failed") {
|
|
2612
|
+
if (record.attempts >= MAX_ENGINE_INSTALL_ATTEMPTS) return { install: false, reason: "exhausted" };
|
|
2613
|
+
const wait = ENGINE_BACKOFF_MS[Math.min(record.attempts - 1, ENGINE_BACKOFF_MS.length - 1)] ?? 0;
|
|
2614
|
+
if (facts.now - record.at < wait) return { install: false, reason: "backoff" };
|
|
2615
|
+
}
|
|
2616
|
+
return { install: true, target: facts.target };
|
|
2617
|
+
}
|
|
2618
|
+
function describeEngineInstallRefusal(reason, engineId, runnerBin) {
|
|
2619
|
+
switch (reason) {
|
|
2620
|
+
case "unknown-engine":
|
|
2621
|
+
return `This runner has no installer for "${engineId}" \u2014 update it, or install that CLI yourself and point ${engineBinEnvHint(engineId)} at it.`;
|
|
2622
|
+
case "disabled":
|
|
2623
|
+
return `Installing engine CLIs is turned off on this machine (\`${runnerBin} config set autoInstallEngines on\` to allow it).`;
|
|
2624
|
+
case "windows":
|
|
2625
|
+
return `This runner does not install engine CLIs on Windows yet \u2014 install "${engineId}" yourself and point ${engineBinEnvHint(engineId)} at it.`;
|
|
2626
|
+
case "no-npm":
|
|
2627
|
+
return "`npm` is not on this machine's PATH, so there is nothing to install with.";
|
|
2628
|
+
case "already-installed":
|
|
2629
|
+
return `"${engineId}" is already installed at the version this machine wants.`;
|
|
2630
|
+
case "locked":
|
|
2631
|
+
return `Another install of "${engineId}" is already running on this machine.`;
|
|
2632
|
+
case "backoff":
|
|
2633
|
+
return `Waiting before trying "${engineId}" again \u2014 the last install failed (\`${runnerBin} engine install ${engineId}\` to try now).`;
|
|
2634
|
+
case "exhausted":
|
|
2635
|
+
return `Installing "${engineId}" failed ${MAX_ENGINE_INSTALL_ATTEMPTS} times on this machine \u2014 see \`${runnerBin} engine list\`.`;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
function engineBinEnvHint(engineId) {
|
|
2639
|
+
return `CREW_${engineId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_BIN`;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
// src/engines/install/state.ts
|
|
2643
|
+
import fs3 from "node:fs";
|
|
2644
|
+
import path4 from "node:path";
|
|
2645
|
+
var ENGINE_STATE_FILE = "engines.json";
|
|
2646
|
+
function engineStatePath(root) {
|
|
2647
|
+
return path4.join(root, ENGINE_STATE_FILE);
|
|
2648
|
+
}
|
|
2649
|
+
function readEngineState(root) {
|
|
2650
|
+
try {
|
|
2651
|
+
const parsed = JSON.parse(fs3.readFileSync(engineStatePath(root), "utf8"));
|
|
2652
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
2653
|
+
return parsed;
|
|
2654
|
+
} catch {
|
|
2655
|
+
return {};
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
function recordEngineInstall(root, engineId, patch) {
|
|
2659
|
+
try {
|
|
2660
|
+
const state = readEngineState(root);
|
|
2661
|
+
const previous = state[engineId];
|
|
2662
|
+
state[engineId] = { ...previous ?? { target: "", npmPackage: "", attempts: 0, at: 0, outcome: "failed" }, ...patch };
|
|
2663
|
+
fs3.mkdirSync(root, { recursive: true, mode: 448 });
|
|
2664
|
+
fs3.writeFileSync(engineStatePath(root), `${JSON.stringify(state, null, 2)}
|
|
2665
|
+
`, { mode: 384 });
|
|
2666
|
+
return true;
|
|
2667
|
+
} catch {
|
|
2668
|
+
return false;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
function readEngineMarker(root, engineId) {
|
|
2672
|
+
try {
|
|
2673
|
+
const parsed = JSON.parse(fs3.readFileSync(engineMarkerPath(root, engineId), "utf8"));
|
|
2674
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2675
|
+
const marker = parsed;
|
|
2676
|
+
if (typeof marker.npmPackage !== "string" || typeof marker.version !== "string") return null;
|
|
2677
|
+
if (typeof marker.bin !== "string" || !marker.bin) return null;
|
|
2678
|
+
return { npmPackage: marker.npmPackage, version: marker.version, bin: marker.bin, at: typeof marker.at === "number" ? marker.at : 0 };
|
|
2679
|
+
} catch {
|
|
2680
|
+
return null;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
function writeEngineMarker(root, engineId, marker) {
|
|
2684
|
+
try {
|
|
2685
|
+
fs3.writeFileSync(engineMarkerPath(root, engineId), `${JSON.stringify(marker, null, 2)}
|
|
2686
|
+
`, { mode: 384 });
|
|
2687
|
+
return true;
|
|
2688
|
+
} catch {
|
|
2689
|
+
return false;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
function clearEngineMarker(root, engineId) {
|
|
2693
|
+
try {
|
|
2694
|
+
fs3.rmSync(engineMarkerPath(root, engineId), { force: true });
|
|
2695
|
+
} catch {
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2506
2698
|
|
|
2507
2699
|
// src/engines/binary.ts
|
|
2508
2700
|
function engineBinaryEnvVar(engineId) {
|
|
2509
2701
|
return `CREW_${engineId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_BIN`;
|
|
2510
2702
|
}
|
|
2511
|
-
function
|
|
2512
|
-
|
|
2703
|
+
function managedEngineBin(engineId, options = {}) {
|
|
2704
|
+
const pkg = enginePackageFor(engineId);
|
|
2705
|
+
if (!pkg) return null;
|
|
2706
|
+
let root;
|
|
2707
|
+
try {
|
|
2708
|
+
root = options.root ?? configDir();
|
|
2709
|
+
} catch {
|
|
2710
|
+
return null;
|
|
2711
|
+
}
|
|
2712
|
+
const marker = readEngineMarker(root, engineId);
|
|
2713
|
+
if (!marker || marker.npmPackage !== pkg.npmPackage) return null;
|
|
2714
|
+
try {
|
|
2715
|
+
fs4.accessSync(marker.bin, fs4.constants.X_OK);
|
|
2716
|
+
} catch {
|
|
2717
|
+
return null;
|
|
2718
|
+
}
|
|
2719
|
+
return marker.bin;
|
|
2720
|
+
}
|
|
2721
|
+
function engineBinary(engineId, defaultName, options = {}) {
|
|
2722
|
+
const env = options.env ?? process.env;
|
|
2723
|
+
const override = env[engineBinaryEnvVar(engineId)];
|
|
2724
|
+
if (override) return override;
|
|
2725
|
+
return managedEngineBin(engineId, options) ?? defaultName;
|
|
2513
2726
|
}
|
|
2514
2727
|
|
|
2515
2728
|
// src/engines/claudeEvents.ts
|
|
@@ -2576,6 +2789,30 @@ function mcpCallsFromClaudeEvent(event, pairing) {
|
|
|
2576
2789
|
return calls;
|
|
2577
2790
|
}
|
|
2578
2791
|
|
|
2792
|
+
// src/engines/engineFault.ts
|
|
2793
|
+
var ARGV_REJECTED = [
|
|
2794
|
+
/unexpected argument/i,
|
|
2795
|
+
/unrecognized (?:subcommand|option)/i,
|
|
2796
|
+
/unknown (?:option|argument|command)/i,
|
|
2797
|
+
/invalid value .* for/i,
|
|
2798
|
+
/error: unexpected/i
|
|
2799
|
+
];
|
|
2800
|
+
function detectEngineFault(input) {
|
|
2801
|
+
if (input.explained) return void 0;
|
|
2802
|
+
if (input.exitCode === 0) return void 0;
|
|
2803
|
+
const stderr = input.stderr.slice(-4e3);
|
|
2804
|
+
for (const pattern of ARGV_REJECTED) {
|
|
2805
|
+
if (pattern.test(stderr)) {
|
|
2806
|
+
const line = stderr.split("\n").find((l) => pattern.test(l))?.trim() ?? "";
|
|
2807
|
+
return `The CLI rejected the command line this runner builds: ${line.slice(0, 200)}`;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
if (input.events === 0 && stderr.trim().length === 0) {
|
|
2811
|
+
return `The CLI produced no output and exited ${input.exitCode ?? "on a signal"}.`;
|
|
2812
|
+
}
|
|
2813
|
+
return void 0;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2579
2816
|
// src/engines/githubEnv.ts
|
|
2580
2817
|
function githubSessionEnv(token) {
|
|
2581
2818
|
if (!token) return {};
|
|
@@ -2588,6 +2825,15 @@ function githubSessionEnv(token) {
|
|
|
2588
2825
|
};
|
|
2589
2826
|
}
|
|
2590
2827
|
|
|
2828
|
+
// src/engines/spawnEnv.ts
|
|
2829
|
+
import path5 from "node:path";
|
|
2830
|
+
function withNodeOnPath(env = process.env) {
|
|
2831
|
+
const nodeDir = path5.dirname(process.execPath);
|
|
2832
|
+
const current = env.PATH ?? "";
|
|
2833
|
+
if (current === nodeDir || current.startsWith(nodeDir + path5.delimiter)) return { ...env };
|
|
2834
|
+
return { ...env, PATH: current ? `${nodeDir}${path5.delimiter}${current}` : nodeDir };
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2591
2837
|
// src/engines/limitWindow.ts
|
|
2592
2838
|
var MAX_LIMIT_MS = 7 * 24 * 60 * 60 * 1e3 + 60 * 60 * 1e3;
|
|
2593
2839
|
function saneResetAt(reported, now, maxMs = MAX_LIMIT_MS) {
|
|
@@ -2713,9 +2959,9 @@ function allowedTools(agent, extraServers = []) {
|
|
|
2713
2959
|
return [...new Set(tools)];
|
|
2714
2960
|
}
|
|
2715
2961
|
function createSessionDirs(jobId) {
|
|
2716
|
-
const workdir =
|
|
2717
|
-
const configDir2 =
|
|
2718
|
-
return { workdir, configDir: configDir2, mcpConfigPath:
|
|
2962
|
+
const workdir = fs5.mkdtempSync(path6.join(os2.tmpdir(), `crew-job-${jobId}-`));
|
|
2963
|
+
const configDir2 = fs5.mkdtempSync(path6.join(os2.tmpdir(), `crew-cfg-${jobId}-`));
|
|
2964
|
+
return { workdir, configDir: configDir2, mcpConfigPath: path6.join(configDir2, "mcp.json") };
|
|
2719
2965
|
}
|
|
2720
2966
|
function buildMcpConfig(input) {
|
|
2721
2967
|
const mcpServers = {
|
|
@@ -2762,11 +3008,11 @@ function buildMcpConfig(input) {
|
|
|
2762
3008
|
return { mcpServers };
|
|
2763
3009
|
}
|
|
2764
3010
|
function writeMcpConfig(dirs, input) {
|
|
2765
|
-
|
|
3011
|
+
fs5.writeFileSync(dirs.mcpConfigPath, JSON.stringify(buildMcpConfig(input)), { mode: 384 });
|
|
2766
3012
|
}
|
|
2767
3013
|
function cleanupSessionDirs(dirs) {
|
|
2768
|
-
|
|
2769
|
-
|
|
3014
|
+
fs5.rmSync(dirs.workdir, { recursive: true, force: true });
|
|
3015
|
+
fs5.rmSync(dirs.configDir, { recursive: true, force: true });
|
|
2770
3016
|
}
|
|
2771
3017
|
async function runClaudeSession(input) {
|
|
2772
3018
|
const bin = claudeBinary();
|
|
@@ -2803,7 +3049,9 @@ async function runSession(input, bin, dirs) {
|
|
|
2803
3049
|
];
|
|
2804
3050
|
const claudeToken = engineSecret(input.secrets, CLAUDE_SECRET_KEY);
|
|
2805
3051
|
const env = {
|
|
2806
|
-
|
|
3052
|
+
// Claude ships a native launcher and does not need this today; every engine gets it anyway,
|
|
3053
|
+
// because which engines do is the vendor's packaging and changes in a release (spawnEnv.ts).
|
|
3054
|
+
...withNodeOnPath(),
|
|
2807
3055
|
// THE INIT EVENT MUST BE TRUTHFUL, and one inherited variable is enough to make it lie.
|
|
2808
3056
|
//
|
|
2809
3057
|
// `MCP_CONNECTION_NONBLOCKING` lets the CLI start before its MCP servers have settled. The init
|
|
@@ -2921,6 +3169,12 @@ ${stderrLines.slice(-20).join("\n")}`,
|
|
|
2921
3169
|
Date.now(),
|
|
2922
3170
|
engineUsageWindows(CLAUDE_DRIVER_ID)?.fallbackMs ?? 5 * 60 * 60 * 1e3
|
|
2923
3171
|
);
|
|
3172
|
+
const engineFault = ok2 ? void 0 : detectEngineFault({
|
|
3173
|
+
exitCode,
|
|
3174
|
+
stderr: stderrLines.join("\n"),
|
|
3175
|
+
events: lines.length,
|
|
3176
|
+
explained: Boolean(limit4 || mcpProblem || resultEvent)
|
|
3177
|
+
});
|
|
2924
3178
|
return {
|
|
2925
3179
|
ok: ok2,
|
|
2926
3180
|
transcript: `${lines.join("\n")}
|
|
@@ -2928,7 +3182,8 @@ ${stderrLines.slice(-20).join("\n")}`,
|
|
|
2928
3182
|
usage,
|
|
2929
3183
|
resultText: resultText3,
|
|
2930
3184
|
...limit4 ? { limit: limit4 } : {},
|
|
2931
|
-
...mcpFailed.length ? { mcpFailed } : {}
|
|
3185
|
+
...mcpFailed.length ? { mcpFailed } : {},
|
|
3186
|
+
...engineFault ? { engineFault } : {}
|
|
2932
3187
|
};
|
|
2933
3188
|
}
|
|
2934
3189
|
var CLAUDE_DRIVER_ID = "claude";
|
|
@@ -2948,8 +3203,8 @@ function emptyUsage() {
|
|
|
2948
3203
|
durationS: 0
|
|
2949
3204
|
};
|
|
2950
3205
|
}
|
|
2951
|
-
async function claudeHealthCheck() {
|
|
2952
|
-
const bin = claudeBinary();
|
|
3206
|
+
async function claudeHealthCheck(binOverride) {
|
|
3207
|
+
const bin = binOverride ?? claudeBinary();
|
|
2953
3208
|
return new Promise((resolve) => {
|
|
2954
3209
|
let settled = false;
|
|
2955
3210
|
const done = (health) => {
|
|
@@ -2958,7 +3213,7 @@ async function claudeHealthCheck() {
|
|
|
2958
3213
|
resolve({ ...health, binary: bin });
|
|
2959
3214
|
};
|
|
2960
3215
|
let stdout = "";
|
|
2961
|
-
const child = spawn3(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
3216
|
+
const child = spawn3(bin, ["--version"], { env: withNodeOnPath(), stdio: ["ignore", "pipe", "ignore"] });
|
|
2962
3217
|
const timer = setTimeout(() => {
|
|
2963
3218
|
child.kill("SIGKILL");
|
|
2964
3219
|
done({ ok: false, detail: `\`${bin} --version\` timed out`, fix: "Check the Claude CLI install." });
|
|
@@ -3001,9 +3256,9 @@ var claudeDriver = {
|
|
|
3001
3256
|
|
|
3002
3257
|
// src/engines/codex.ts
|
|
3003
3258
|
import { spawn as spawn4 } from "node:child_process";
|
|
3004
|
-
import
|
|
3259
|
+
import fs6 from "node:fs";
|
|
3005
3260
|
import os3 from "node:os";
|
|
3006
|
-
import
|
|
3261
|
+
import path7 from "node:path";
|
|
3007
3262
|
|
|
3008
3263
|
// src/engines/ansi.ts
|
|
3009
3264
|
var ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
|
|
@@ -3266,7 +3521,7 @@ var codexLogin = {
|
|
|
3266
3521
|
logoutArgs: () => ["logout"],
|
|
3267
3522
|
statusArgs: () => ["login", "status"],
|
|
3268
3523
|
env: (home) => ({ CODEX_HOME: home }),
|
|
3269
|
-
isSignedIn: (home) =>
|
|
3524
|
+
isSignedIn: (home) => fs6.existsSync(path7.join(home, AUTH_FILE)),
|
|
3270
3525
|
parseGuidance(text2) {
|
|
3271
3526
|
const clean = stripAnsi(text2);
|
|
3272
3527
|
const url = GUIDANCE_URL_RE.exec(clean)?.[0];
|
|
@@ -3282,7 +3537,7 @@ var codexLogin = {
|
|
|
3282
3537
|
readAccount(home) {
|
|
3283
3538
|
let parsed;
|
|
3284
3539
|
try {
|
|
3285
|
-
parsed = JSON.parse(
|
|
3540
|
+
parsed = JSON.parse(fs6.readFileSync(path7.join(home, AUTH_FILE), "utf8"));
|
|
3286
3541
|
} catch {
|
|
3287
3542
|
return null;
|
|
3288
3543
|
}
|
|
@@ -3330,17 +3585,18 @@ async function runCodexSession(input) {
|
|
|
3330
3585
|
{ credentialLost: "The sign-in directory on this machine no longer holds a login." }
|
|
3331
3586
|
);
|
|
3332
3587
|
}
|
|
3333
|
-
const workdir =
|
|
3588
|
+
const workdir = fs6.mkdtempSync(path7.join(os3.tmpdir(), `crew-job-${input.job.id}-`));
|
|
3334
3589
|
try {
|
|
3335
3590
|
return await runSession2(input, bin, home, workdir);
|
|
3336
3591
|
} finally {
|
|
3337
|
-
|
|
3592
|
+
fs6.rmSync(workdir, { recursive: true, force: true });
|
|
3338
3593
|
}
|
|
3339
3594
|
}
|
|
3340
3595
|
async function runSession2(input, bin, home, workdir) {
|
|
3341
3596
|
const args = codexArgs({ ...input, extraServers: input.extraMcpServers, workdir });
|
|
3342
3597
|
const env = {
|
|
3343
|
-
|
|
3598
|
+
// The CLI's command is a `#!/usr/bin/env node` script — see engines/spawnEnv.ts.
|
|
3599
|
+
...withNodeOnPath(),
|
|
3344
3600
|
...codexLogin.env(home),
|
|
3345
3601
|
[WORKSPACE_TOKEN_ENV]: input.idToken,
|
|
3346
3602
|
...githubSessionEnv(input.githubToken)
|
|
@@ -3456,6 +3712,12 @@ async function runSession2(input, bin, home, workdir) {
|
|
|
3456
3712
|
const limit4 = !ok2 && windows ? detectCodexLimit(failureText, Date.now(), windows.fallbackMs) : void 0;
|
|
3457
3713
|
const credentialLost = !ok2 && !limit4 ? detectLoginLost(stderr) : void 0;
|
|
3458
3714
|
const resultText3 = ok2 ? seen.lastMessage : failedMessage || stderr.trim().split("\n").slice(-3).join("\n") || `Codex exited ${exitCode}`;
|
|
3715
|
+
const engineFault = ok2 ? void 0 : detectEngineFault({
|
|
3716
|
+
exitCode,
|
|
3717
|
+
stderr,
|
|
3718
|
+
events: lines.length,
|
|
3719
|
+
explained: Boolean(limit4 || credentialLost || mcpProblem || failedMessage)
|
|
3720
|
+
});
|
|
3459
3721
|
return {
|
|
3460
3722
|
ok: ok2,
|
|
3461
3723
|
transcript,
|
|
@@ -3463,11 +3725,12 @@ async function runSession2(input, bin, home, workdir) {
|
|
|
3463
3725
|
resultText: resultText3.slice(0, 2e4),
|
|
3464
3726
|
...limit4 ? { limit: limit4 } : {},
|
|
3465
3727
|
...credentialLost ? { credentialLost } : {},
|
|
3466
|
-
...mcpFailed.length ? { mcpFailed } : {}
|
|
3728
|
+
...mcpFailed.length ? { mcpFailed } : {},
|
|
3729
|
+
...engineFault ? { engineFault } : {}
|
|
3467
3730
|
};
|
|
3468
3731
|
}
|
|
3469
|
-
async function codexHealthCheck() {
|
|
3470
|
-
const bin = codexBinary();
|
|
3732
|
+
async function codexHealthCheck(binOverride) {
|
|
3733
|
+
const bin = binOverride ?? codexBinary();
|
|
3471
3734
|
return new Promise((resolve) => {
|
|
3472
3735
|
let settled = false;
|
|
3473
3736
|
const done = (health) => {
|
|
@@ -3476,7 +3739,7 @@ async function codexHealthCheck() {
|
|
|
3476
3739
|
resolve({ ...health, binary: bin });
|
|
3477
3740
|
};
|
|
3478
3741
|
let stdout = "";
|
|
3479
|
-
const child = spawn4(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
3742
|
+
const child = spawn4(bin, ["--version"], { env: withNodeOnPath(), stdio: ["ignore", "pipe", "ignore"] });
|
|
3480
3743
|
const timer = setTimeout(() => {
|
|
3481
3744
|
child.kill("SIGKILL");
|
|
3482
3745
|
done({ ok: false, detail: `\`${bin} --version\` timed out`, fix: "Check the Codex CLI install." });
|
|
@@ -3885,19 +4148,16 @@ function credentialGate(agent, credentials, runnerId, busyCredentialIds, default
|
|
|
3885
4148
|
}
|
|
3886
4149
|
|
|
3887
4150
|
// src/jobs/engineHome.ts
|
|
3888
|
-
import
|
|
3889
|
-
var SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
4151
|
+
import path8 from "node:path";
|
|
3890
4152
|
function engineCredentialHome(root, engineId, shipId, credentialId) {
|
|
3891
|
-
for (const segment of [engineId, shipId, credentialId])
|
|
3892
|
-
|
|
3893
|
-
}
|
|
3894
|
-
return path5.join(root, "engines", engineId, shipId, credentialId);
|
|
4153
|
+
for (const segment of [engineId, shipId, credentialId]) assertPathSegment(segment);
|
|
4154
|
+
return path8.join(root, "engines", engineId, shipId, credentialId);
|
|
3895
4155
|
}
|
|
3896
4156
|
|
|
3897
4157
|
// src/jobs/engineLogin.ts
|
|
3898
4158
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
3899
|
-
import
|
|
3900
|
-
import
|
|
4159
|
+
import fs7 from "node:fs";
|
|
4160
|
+
import path9 from "node:path";
|
|
3901
4161
|
var DEVICE_CODE_TTL_MS = 15 * 60 * 1e3;
|
|
3902
4162
|
function machineLoginTimeoutMs() {
|
|
3903
4163
|
const override = Number(process.env.CREW_MACHINE_LOGIN_TIMEOUT_MS);
|
|
@@ -3920,39 +4180,39 @@ function credentialsToForget(previouslyHeld, current) {
|
|
|
3920
4180
|
return [...previouslyHeld].filter((id) => !live.has(id));
|
|
3921
4181
|
}
|
|
3922
4182
|
function orphanHomes(root, shipId, liveIds) {
|
|
3923
|
-
const enginesDir =
|
|
4183
|
+
const enginesDir = path9.join(root, "engines");
|
|
3924
4184
|
let engines = [];
|
|
3925
4185
|
try {
|
|
3926
|
-
engines =
|
|
4186
|
+
engines = fs7.readdirSync(enginesDir);
|
|
3927
4187
|
} catch {
|
|
3928
4188
|
return [];
|
|
3929
4189
|
}
|
|
3930
4190
|
const out = [];
|
|
3931
4191
|
for (const engineId of engines) {
|
|
3932
|
-
const shipDir =
|
|
4192
|
+
const shipDir = path9.join(enginesDir, engineId, shipId);
|
|
3933
4193
|
let creds = [];
|
|
3934
4194
|
try {
|
|
3935
|
-
creds =
|
|
4195
|
+
creds = fs7.readdirSync(shipDir);
|
|
3936
4196
|
} catch {
|
|
3937
4197
|
continue;
|
|
3938
4198
|
}
|
|
3939
|
-
for (const credId of creds) if (!liveIds.has(credId)) out.push(
|
|
4199
|
+
for (const credId of creds) if (!liveIds.has(credId)) out.push(path9.join(shipDir, credId));
|
|
3940
4200
|
}
|
|
3941
4201
|
return out;
|
|
3942
4202
|
}
|
|
3943
4203
|
async function runMachineLogin(input) {
|
|
3944
|
-
const
|
|
4204
|
+
const spawn9 = input.spawn ?? nodeSpawn;
|
|
3945
4205
|
if (input.signal?.aborted) return { phase: "failed", error: "The runner was stopping." };
|
|
3946
4206
|
try {
|
|
3947
|
-
|
|
4207
|
+
fs7.mkdirSync(input.home, { recursive: true, mode: 448 });
|
|
3948
4208
|
} catch (e) {
|
|
3949
4209
|
return { phase: "failed", error: `Could not create the sign-in directory: ${message2(e)}` };
|
|
3950
4210
|
}
|
|
3951
4211
|
return new Promise((resolve) => {
|
|
3952
4212
|
let child;
|
|
3953
4213
|
try {
|
|
3954
|
-
child =
|
|
3955
|
-
env: { ...
|
|
4214
|
+
child = spawn9(input.bin, input.driver.spawnArgs(), {
|
|
4215
|
+
env: { ...withNodeOnPath(), ...input.driver.env(input.home) },
|
|
3956
4216
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3957
4217
|
});
|
|
3958
4218
|
} catch (e) {
|
|
@@ -4036,13 +4296,13 @@ function notInstalled(engineId, bin, detail) {
|
|
|
4036
4296
|
return `\`${bin}\` is not installed on this machine (${detail}). Install the engine's CLI, or set ${engineBinaryEnvVar(engineId)} to its path.`;
|
|
4037
4297
|
}
|
|
4038
4298
|
async function logoutAndRemove(input) {
|
|
4039
|
-
const
|
|
4040
|
-
if (
|
|
4299
|
+
const spawn9 = input.spawn ?? nodeSpawn;
|
|
4300
|
+
if (fs7.existsSync(input.home)) {
|
|
4041
4301
|
await new Promise((resolve) => {
|
|
4042
4302
|
let child;
|
|
4043
4303
|
try {
|
|
4044
|
-
child =
|
|
4045
|
-
env: { ...
|
|
4304
|
+
child = spawn9(input.bin, input.driver.logoutArgs(), {
|
|
4305
|
+
env: { ...withNodeOnPath(), ...input.driver.env(input.home) },
|
|
4046
4306
|
stdio: "ignore"
|
|
4047
4307
|
});
|
|
4048
4308
|
} catch {
|
|
@@ -4064,7 +4324,7 @@ async function logoutAndRemove(input) {
|
|
|
4064
4324
|
});
|
|
4065
4325
|
}
|
|
4066
4326
|
try {
|
|
4067
|
-
|
|
4327
|
+
fs7.rmSync(input.home, { recursive: true, force: true });
|
|
4068
4328
|
} catch (e) {
|
|
4069
4329
|
input.log(`Could not remove the sign-in directory ${input.home}: ${message2(e)}`);
|
|
4070
4330
|
}
|
|
@@ -4095,6 +4355,13 @@ async function machineLoginAndReport(input) {
|
|
|
4095
4355
|
await say2({ phase: "failed", error: message2(e) });
|
|
4096
4356
|
return;
|
|
4097
4357
|
}
|
|
4358
|
+
if (input.ensureEngine) {
|
|
4359
|
+
const ready = await input.ensureEngine(credential.engine);
|
|
4360
|
+
if (!ready.ok) {
|
|
4361
|
+
await say2({ phase: "failed", error: ready.detail });
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4098
4365
|
input.log(`Signing this machine in for "${credential.label}" (${credential.engine}) \u2014 a captain asked.`);
|
|
4099
4366
|
const outcome = await runMachineLogin({
|
|
4100
4367
|
driver: driver.login,
|
|
@@ -4128,7 +4395,7 @@ async function machineLoginAndReport(input) {
|
|
|
4128
4395
|
if (outcome.phase === "expired") {
|
|
4129
4396
|
input.log(`The sign-in for "${credential.label}" expired before it was approved.`);
|
|
4130
4397
|
try {
|
|
4131
|
-
|
|
4398
|
+
fs7.rmSync(home, { recursive: true, force: true });
|
|
4132
4399
|
} catch {
|
|
4133
4400
|
}
|
|
4134
4401
|
await say2({ phase: "expired" });
|
|
@@ -4136,7 +4403,7 @@ async function machineLoginAndReport(input) {
|
|
|
4136
4403
|
}
|
|
4137
4404
|
input.log(`The sign-in for "${credential.label}" failed: ${outcome.error}`);
|
|
4138
4405
|
try {
|
|
4139
|
-
|
|
4406
|
+
fs7.rmSync(home, { recursive: true, force: true });
|
|
4140
4407
|
} catch {
|
|
4141
4408
|
}
|
|
4142
4409
|
await say2({ phase: "failed", error: outcome.error });
|
|
@@ -4303,11 +4570,11 @@ function redactTranscript(transcript, knownSecrets, patterns = []) {
|
|
|
4303
4570
|
return out;
|
|
4304
4571
|
}
|
|
4305
4572
|
async function uploadTranscript(storage, shipId, jobId, redacted) {
|
|
4306
|
-
const
|
|
4307
|
-
await uploadBytes(storageRef(storage,
|
|
4573
|
+
const path17 = `crew/${shipId}/transcripts/${jobId}.jsonl`;
|
|
4574
|
+
await uploadBytes(storageRef(storage, path17), new TextEncoder().encode(redacted), {
|
|
4308
4575
|
contentType: "application/x-ndjson"
|
|
4309
4576
|
});
|
|
4310
|
-
return
|
|
4577
|
+
return path17;
|
|
4311
4578
|
}
|
|
4312
4579
|
function utcDay(millis) {
|
|
4313
4580
|
return new Date(millis).toISOString().slice(0, 10);
|
|
@@ -4665,118 +4932,658 @@ function createProgressWriter(deps) {
|
|
|
4665
4932
|
};
|
|
4666
4933
|
}
|
|
4667
4934
|
|
|
4668
|
-
// src/
|
|
4669
|
-
import
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
import
|
|
4673
|
-
import
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
var
|
|
4679
|
-
var
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
};
|
|
4686
|
-
function run(command, args) {
|
|
4687
|
-
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
4688
|
-
return {
|
|
4689
|
-
ok: result.status === 0,
|
|
4690
|
-
out: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
|
|
4691
|
-
};
|
|
4935
|
+
// src/engines/install/index.ts
|
|
4936
|
+
import fs10 from "node:fs";
|
|
4937
|
+
|
|
4938
|
+
// src/engines/install/lock.ts
|
|
4939
|
+
import fs9 from "node:fs";
|
|
4940
|
+
import path10 from "node:path";
|
|
4941
|
+
|
|
4942
|
+
// src/engines/install/npm.ts
|
|
4943
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
4944
|
+
import fs8 from "node:fs";
|
|
4945
|
+
var ENGINE_INSTALL_TIMEOUT_MS = 5 * 6e4;
|
|
4946
|
+
var STDERR_KEEP = 600;
|
|
4947
|
+
function engineInstallArgs(pkg, version, prefix) {
|
|
4948
|
+
if (!isExactVersion(version)) throw new Error(`refusing to install a version that is not exact: ${JSON.stringify(version)}`);
|
|
4949
|
+
if (enginePackageFor_reverse(pkg.npmPackage) === null) {
|
|
4950
|
+
throw new Error(`refusing to install a package that is not on the allowlist: ${JSON.stringify(pkg.npmPackage)}`);
|
|
4951
|
+
}
|
|
4952
|
+
return ["install", "--global", "--prefix", prefix, "--no-fund", "--no-audit", `${pkg.npmPackage}@${version}`];
|
|
4692
4953
|
}
|
|
4693
|
-
function
|
|
4694
|
-
|
|
4954
|
+
function enginePackageFor_reverse(npmPackage) {
|
|
4955
|
+
for (const id of ["codex", "claude"]) {
|
|
4956
|
+
if (enginePackageFor(id)?.npmPackage === npmPackage) return id;
|
|
4957
|
+
}
|
|
4958
|
+
return null;
|
|
4695
4959
|
}
|
|
4696
|
-
function
|
|
4697
|
-
|
|
4960
|
+
function clearEnginePrefix(root, engineId) {
|
|
4961
|
+
fs8.rmSync(enginePrefixDir(root, engineId), { recursive: true, force: true });
|
|
4698
4962
|
}
|
|
4699
|
-
function
|
|
4700
|
-
|
|
4963
|
+
async function installEnginePackage(pkg, version, prefix, options = {}) {
|
|
4964
|
+
const args = engineInstallArgs(pkg, version, prefix);
|
|
4965
|
+
return new Promise((resolve) => {
|
|
4966
|
+
let child;
|
|
4967
|
+
try {
|
|
4968
|
+
child = spawn6("npm", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
4969
|
+
} catch (e) {
|
|
4970
|
+
resolve({ ok: false, detail: e instanceof Error ? e.message : String(e) });
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
let stderr = "";
|
|
4974
|
+
let settled = false;
|
|
4975
|
+
const finish = (result) => {
|
|
4976
|
+
if (settled) return;
|
|
4977
|
+
settled = true;
|
|
4978
|
+
clearTimeout(deadline);
|
|
4979
|
+
resolve(result);
|
|
4980
|
+
};
|
|
4981
|
+
const deadline = setTimeout(() => {
|
|
4982
|
+
child.kill("SIGTERM");
|
|
4983
|
+
setTimeout(() => child.kill("SIGKILL"), 5e3).unref();
|
|
4984
|
+
finish({ ok: false, detail: `npm install timed out after ${Math.round((options.timeoutMs ?? ENGINE_INSTALL_TIMEOUT_MS) / 6e4)} min` });
|
|
4985
|
+
}, options.timeoutMs ?? ENGINE_INSTALL_TIMEOUT_MS);
|
|
4986
|
+
deadline.unref();
|
|
4987
|
+
child.stderr?.on("data", (chunk) => {
|
|
4988
|
+
stderr = (stderr + chunk.toString("utf8")).slice(-STDERR_KEEP);
|
|
4989
|
+
});
|
|
4990
|
+
child.on("error", (e) => finish({ ok: false, detail: e.message }));
|
|
4991
|
+
child.on("close", (code) => {
|
|
4992
|
+
finish(code === 0 ? { ok: true, detail: "" } : { ok: false, detail: stderr.trim() || `npm exited ${code}` });
|
|
4993
|
+
});
|
|
4994
|
+
});
|
|
4701
4995
|
}
|
|
4702
|
-
|
|
4703
|
-
|
|
4996
|
+
|
|
4997
|
+
// src/engines/install/lock.ts
|
|
4998
|
+
var OWNER_FILE = "owner.json";
|
|
4999
|
+
var STALE_AFTER_MS = 2 * ENGINE_INSTALL_TIMEOUT_MS;
|
|
5000
|
+
function lockDir(root, engineId) {
|
|
5001
|
+
return `${enginePrefixDir(root, engineId)}.lock`;
|
|
4704
5002
|
}
|
|
4705
|
-
function
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
5003
|
+
function readOwner(dir) {
|
|
5004
|
+
try {
|
|
5005
|
+
const parsed = JSON.parse(fs9.readFileSync(path10.join(dir, OWNER_FILE), "utf8"));
|
|
5006
|
+
const owner = parsed;
|
|
5007
|
+
if (typeof owner?.pid !== "number" || typeof owner?.at !== "number") return null;
|
|
5008
|
+
return { pid: owner.pid, at: owner.at };
|
|
5009
|
+
} catch {
|
|
5010
|
+
return null;
|
|
4709
5011
|
}
|
|
4710
|
-
return env;
|
|
4711
5012
|
}
|
|
4712
|
-
function
|
|
4713
|
-
|
|
4714
|
-
|
|
5013
|
+
function alive(pid) {
|
|
5014
|
+
try {
|
|
5015
|
+
process.kill(pid, 0);
|
|
5016
|
+
return true;
|
|
5017
|
+
} catch (e) {
|
|
5018
|
+
return e.code === "EPERM";
|
|
5019
|
+
}
|
|
4715
5020
|
}
|
|
4716
|
-
function
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
}
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
removed.push(unitPath);
|
|
4736
|
-
}
|
|
4737
|
-
continue;
|
|
5021
|
+
function isStale(owner, now) {
|
|
5022
|
+
if (!owner) return true;
|
|
5023
|
+
return !alive(owner.pid) || now - owner.at > STALE_AFTER_MS;
|
|
5024
|
+
}
|
|
5025
|
+
async function withEngineInstallLock(root, engineId, fn, now = Date.now()) {
|
|
5026
|
+
const dir = lockDir(root, engineId);
|
|
5027
|
+
fs9.mkdirSync(path10.dirname(dir), { recursive: true, mode: 448 });
|
|
5028
|
+
let held = false;
|
|
5029
|
+
try {
|
|
5030
|
+
fs9.mkdirSync(dir);
|
|
5031
|
+
held = true;
|
|
5032
|
+
} catch {
|
|
5033
|
+
if (!isStale(readOwner(dir), now)) return { ran: false };
|
|
5034
|
+
try {
|
|
5035
|
+
fs9.rmSync(dir, { recursive: true, force: true });
|
|
5036
|
+
fs9.mkdirSync(dir);
|
|
5037
|
+
held = true;
|
|
5038
|
+
} catch {
|
|
5039
|
+
return { ran: false };
|
|
4738
5040
|
}
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
5041
|
+
}
|
|
5042
|
+
try {
|
|
5043
|
+
fs9.writeFileSync(path10.join(dir, OWNER_FILE), `${JSON.stringify({ pid: process.pid, at: now })}
|
|
5044
|
+
`, { mode: 384 });
|
|
5045
|
+
} catch {
|
|
5046
|
+
}
|
|
5047
|
+
try {
|
|
5048
|
+
return { ran: true, value: await fn() };
|
|
5049
|
+
} finally {
|
|
5050
|
+
if (held) {
|
|
5051
|
+
try {
|
|
5052
|
+
fs9.rmSync(dir, { recursive: true, force: true });
|
|
5053
|
+
} catch {
|
|
4743
5054
|
}
|
|
4744
5055
|
}
|
|
4745
5056
|
}
|
|
4746
|
-
return removed;
|
|
4747
5057
|
}
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
5058
|
+
|
|
5059
|
+
// src/engines/install/index.ts
|
|
5060
|
+
async function installEngine(input) {
|
|
5061
|
+
const { root, engineId, version, log: log2 } = input;
|
|
5062
|
+
const now = input.now ?? Date.now();
|
|
5063
|
+
const pkg = enginePackageFor(engineId);
|
|
5064
|
+
if (!pkg) return { ok: false, ran: false, detail: `no installer for "${engineId}"` };
|
|
5065
|
+
if (!isExactVersion(version)) return { ok: false, ran: false, detail: `not an exact version: ${version}` };
|
|
5066
|
+
const held = await withEngineInstallLock(
|
|
5067
|
+
root,
|
|
5068
|
+
engineId,
|
|
5069
|
+
async () => {
|
|
5070
|
+
const record = readEngineState(root)[engineId];
|
|
5071
|
+
const spent = record && record.target === version ? record.attempts : 0;
|
|
5072
|
+
recordEngineInstall(root, engineId, {
|
|
5073
|
+
target: version,
|
|
5074
|
+
npmPackage: pkg.npmPackage,
|
|
5075
|
+
attempts: spent + 1,
|
|
5076
|
+
at: now,
|
|
5077
|
+
outcome: "failed",
|
|
5078
|
+
detail: "install started"
|
|
5079
|
+
});
|
|
5080
|
+
clearEngineMarker(root, engineId);
|
|
5081
|
+
clearEnginePrefix(root, engineId);
|
|
5082
|
+
const prefix = enginePrefixDir(root, engineId);
|
|
5083
|
+
fs10.mkdirSync(prefix, { recursive: true, mode: 448 });
|
|
5084
|
+
log2(`Installing ${pkg.npmPackage}@${version} for "${engineId}" (this is a few hundred MB).`);
|
|
5085
|
+
const installed = await installEnginePackage(pkg, version, prefix, { timeoutMs: input.timeoutMs ?? ENGINE_INSTALL_TIMEOUT_MS });
|
|
5086
|
+
if (!installed.ok) {
|
|
5087
|
+
recordEngineInstall(root, engineId, { outcome: "failed", at: Date.now(), detail: installed.detail.slice(0, 400) });
|
|
5088
|
+
return { ok: false, ran: true, detail: installed.detail };
|
|
5089
|
+
}
|
|
5090
|
+
const bin = engineBinPath(root, engineId, pkg.binName);
|
|
5091
|
+
const health = await input.healthCheck(bin);
|
|
5092
|
+
if (!health.ok || health.binary !== bin) {
|
|
5093
|
+
const detail = health.ok ? `npm reported success, but the check ran ${health.binary ?? "something else"} rather than ${bin} \u2014 the install did not land where this runner looks.` : health.detail;
|
|
5094
|
+
recordEngineInstall(root, engineId, { outcome: "failed", at: Date.now(), detail: detail.slice(0, 400) });
|
|
5095
|
+
return { ok: false, ran: true, detail };
|
|
5096
|
+
}
|
|
5097
|
+
writeEngineMarker(root, engineId, { npmPackage: pkg.npmPackage, version, bin, at: Date.now() });
|
|
5098
|
+
recordEngineInstall(root, engineId, { outcome: "installed", at: Date.now(), attempts: 0, detail: void 0 });
|
|
5099
|
+
log2(`"${engineId}" is ready \u2014 ${pkg.npmPackage}@${version}.`);
|
|
5100
|
+
return { ok: true, ran: true, detail: "", bin };
|
|
5101
|
+
},
|
|
5102
|
+
now
|
|
4756
5103
|
);
|
|
5104
|
+
return held.ran ? held.value : { ok: false, ran: false, detail: "another install of this engine is already running" };
|
|
4757
5105
|
}
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
function
|
|
4771
|
-
|
|
4772
|
-
const
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
<
|
|
4777
|
-
<
|
|
4778
|
-
|
|
4779
|
-
|
|
5106
|
+
|
|
5107
|
+
// src/update/install.ts
|
|
5108
|
+
import { spawn as spawn7, spawnSync } from "node:child_process";
|
|
5109
|
+
import fs11 from "node:fs";
|
|
5110
|
+
import path12 from "node:path";
|
|
5111
|
+
|
|
5112
|
+
// src/update/plan.ts
|
|
5113
|
+
import path11 from "node:path";
|
|
5114
|
+
|
|
5115
|
+
// src/update/semver.ts
|
|
5116
|
+
var DEV_VERSION = "0.0.0-dev";
|
|
5117
|
+
var NUMERIC = /^(0|[1-9]\d*)$/;
|
|
5118
|
+
function parseVersion(raw) {
|
|
5119
|
+
if (typeof raw !== "string") return null;
|
|
5120
|
+
const trimmed = raw.trim();
|
|
5121
|
+
if (!trimmed) return null;
|
|
5122
|
+
const [withoutBuild] = trimmed.split("+", 1);
|
|
5123
|
+
const dash = withoutBuild.indexOf("-");
|
|
5124
|
+
const core = dash < 0 ? withoutBuild : withoutBuild.slice(0, dash);
|
|
5125
|
+
const pre = dash < 0 ? "" : withoutBuild.slice(dash + 1);
|
|
5126
|
+
const parts = core.split(".");
|
|
5127
|
+
if (parts.length !== 3) return null;
|
|
5128
|
+
if (!parts.every((p) => NUMERIC.test(p))) return null;
|
|
5129
|
+
const [major, minor, patch] = parts.map(Number);
|
|
5130
|
+
if (dash >= 0 && pre === "") return null;
|
|
5131
|
+
const prerelease = pre === "" ? [] : pre.split(".");
|
|
5132
|
+
if (prerelease.some((id) => id === "")) return null;
|
|
5133
|
+
return {
|
|
5134
|
+
major,
|
|
5135
|
+
minor,
|
|
5136
|
+
patch,
|
|
5137
|
+
prerelease: prerelease.map((id) => NUMERIC.test(id) ? Number(id) : id)
|
|
5138
|
+
};
|
|
5139
|
+
}
|
|
5140
|
+
function comparePrerelease(a, b) {
|
|
5141
|
+
if (a.length === 0) return b.length === 0 ? 0 : 1;
|
|
5142
|
+
if (b.length === 0) return -1;
|
|
5143
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
5144
|
+
const x = a[i];
|
|
5145
|
+
const y = b[i];
|
|
5146
|
+
if (x === y) continue;
|
|
5147
|
+
const xNum = typeof x === "number";
|
|
5148
|
+
const yNum = typeof y === "number";
|
|
5149
|
+
if (xNum !== yNum) return xNum ? -1 : 1;
|
|
5150
|
+
if (xNum && yNum) return x < y ? -1 : 1;
|
|
5151
|
+
return x < y ? -1 : 1;
|
|
5152
|
+
}
|
|
5153
|
+
if (a.length === b.length) return 0;
|
|
5154
|
+
return a.length < b.length ? -1 : 1;
|
|
5155
|
+
}
|
|
5156
|
+
function compareVersions(a, b) {
|
|
5157
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
5158
|
+
if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
|
|
5159
|
+
}
|
|
5160
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
5161
|
+
}
|
|
5162
|
+
function isNewer(candidate, current) {
|
|
5163
|
+
if (current === DEV_VERSION) return false;
|
|
5164
|
+
const a = parseVersion(candidate);
|
|
5165
|
+
const b = parseVersion(current);
|
|
5166
|
+
if (!a || !b) return false;
|
|
5167
|
+
return compareVersions(a, b) === 1;
|
|
5168
|
+
}
|
|
5169
|
+
|
|
5170
|
+
// src/update/plan.ts
|
|
5171
|
+
var PACKAGE_NAME = RUNNER_PACKAGE;
|
|
5172
|
+
var UPDATE_DIST_TAG = "latest";
|
|
5173
|
+
var UPDATE_CHECK_MS = 30 * 6e4;
|
|
5174
|
+
var UPDATE_FIRST_CHECK_MS = 2 * 6e4;
|
|
5175
|
+
var UPDATE_JITTER_MS = 10 * 6e4;
|
|
5176
|
+
var CHECK_MINUTES_MIN = 5;
|
|
5177
|
+
var CHECK_MINUTES_MAX = 24 * 60;
|
|
5178
|
+
var UPDATE_DRAIN_MAX_MS = 30 * 6e4;
|
|
5179
|
+
var MAX_UPDATE_ATTEMPTS = 3;
|
|
5180
|
+
var UPDATE_BACKOFF_MS = [15 * 6e4, 2 * 36e5, 12 * 36e5];
|
|
5181
|
+
function checkIntervalMs(config2) {
|
|
5182
|
+
const raw = config2.updateCheckMinutes;
|
|
5183
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return UPDATE_CHECK_MS;
|
|
5184
|
+
const clamped2 = Math.min(CHECK_MINUTES_MAX, Math.max(CHECK_MINUTES_MIN, Math.floor(raw)));
|
|
5185
|
+
return clamped2 * 6e4;
|
|
5186
|
+
}
|
|
5187
|
+
function nextAttemptAllowed(state, target, now) {
|
|
5188
|
+
if (!state || typeof state !== "object") return { allowed: true };
|
|
5189
|
+
if (state.target !== target) return { allowed: true };
|
|
5190
|
+
const attempts = typeof state.attempts === "number" && state.attempts > 0 ? state.attempts : 0;
|
|
5191
|
+
if (attempts === 0) return { allowed: true };
|
|
5192
|
+
if (attempts >= MAX_UPDATE_ATTEMPTS) return { allowed: false, reason: "exhausted" };
|
|
5193
|
+
if (typeof state.at !== "number" || !Number.isFinite(state.at)) return { allowed: true };
|
|
5194
|
+
const wait = UPDATE_BACKOFF_MS[Math.min(attempts - 1, UPDATE_BACKOFF_MS.length - 1)];
|
|
5195
|
+
return now - state.at >= wait ? { allowed: true } : { allowed: false, reason: "backoff" };
|
|
5196
|
+
}
|
|
5197
|
+
function reconcile(state, current) {
|
|
5198
|
+
if (!state || typeof state.target !== "string") return { action: "none" };
|
|
5199
|
+
if (state.target === current) return { action: "clear", target: state.target };
|
|
5200
|
+
return { action: "keep", state };
|
|
5201
|
+
}
|
|
5202
|
+
function packageRootFrom(cliPath2) {
|
|
5203
|
+
return path11.dirname(path11.dirname(cliPath2));
|
|
5204
|
+
}
|
|
5205
|
+
function globalPrefixFrom(cliPath2) {
|
|
5206
|
+
const root = packageRootFrom(cliPath2);
|
|
5207
|
+
const parts = root.split(path11.sep);
|
|
5208
|
+
const i = parts.lastIndexOf("node_modules");
|
|
5209
|
+
if (i < 1) return null;
|
|
5210
|
+
const before = parts.slice(0, i);
|
|
5211
|
+
if (before[before.length - 1] === "lib") before.pop();
|
|
5212
|
+
const prefix = before.join(path11.sep);
|
|
5213
|
+
return prefix || null;
|
|
5214
|
+
}
|
|
5215
|
+
function decideUpdate(f) {
|
|
5216
|
+
if (f.platform === "win32") return { update: false, reason: "windows" };
|
|
5217
|
+
if (f.current === DEV_VERSION) return { update: false, reason: "dev-build" };
|
|
5218
|
+
if (!f.autoUpdate) return { update: false, reason: "disabled" };
|
|
5219
|
+
const service2 = f.service();
|
|
5220
|
+
if (service2.state !== "running") return { update: false, reason: "service-not-running" };
|
|
5221
|
+
if (!service2.execPath || service2.execPath !== f.cliPath) {
|
|
5222
|
+
return { update: false, reason: "foreign-unit" };
|
|
5223
|
+
}
|
|
5224
|
+
if (!f.npmPresent()) return { update: false, reason: "no-npm" };
|
|
5225
|
+
if (f.distTags === null) return { update: false, reason: "registry-unreachable" };
|
|
5226
|
+
const target = f.distTags[UPDATE_DIST_TAG];
|
|
5227
|
+
if (!target) return { update: false, reason: "no-such-tag" };
|
|
5228
|
+
if (!isNewer(target, f.current)) return { update: false, reason: "up-to-date" };
|
|
5229
|
+
const attempt = nextAttemptAllowed(f.state, target, f.now);
|
|
5230
|
+
if (!attempt.allowed) return { update: false, reason: attempt.reason };
|
|
5231
|
+
return { update: true, target };
|
|
5232
|
+
}
|
|
5233
|
+
function describeRefusal(reason, f) {
|
|
5234
|
+
switch (reason) {
|
|
5235
|
+
case "windows":
|
|
5236
|
+
return `Auto-update is not available on Windows \u2014 run \`${RUNNER_BIN} update\` to upgrade.`;
|
|
5237
|
+
case "dev-build":
|
|
5238
|
+
return "Development build \u2014 auto-update is off.";
|
|
5239
|
+
case "disabled":
|
|
5240
|
+
return `Auto-update is off (\`${RUNNER_BIN} config set autoUpdate on\` to enable).`;
|
|
5241
|
+
case "service-not-running":
|
|
5242
|
+
return `Auto-update needs the background service (this daemon would have nothing to restart it). Run \`${RUNNER_BIN} service install\`, or \`${RUNNER_BIN} update\` by hand.`;
|
|
5243
|
+
case "foreign-unit":
|
|
5244
|
+
return `This process is not the one the installed service runs, so auto-update is off for it. Run \`${RUNNER_BIN} update\` if you meant to upgrade the installed copy.`;
|
|
5245
|
+
case "no-npm":
|
|
5246
|
+
return "Auto-update needs `npm` on the service PATH and could not find it.";
|
|
5247
|
+
case "registry-unreachable":
|
|
5248
|
+
return "Could not reach the npm registry to check for updates \u2014 will retry.";
|
|
5249
|
+
case "no-such-tag":
|
|
5250
|
+
return `The npm registry has no release of ${PACKAGE_NAME} yet.`;
|
|
5251
|
+
case "up-to-date":
|
|
5252
|
+
return `Up to date (${f.current}).`;
|
|
5253
|
+
case "backoff":
|
|
5254
|
+
return "A recent update attempt did not take \u2014 waiting before trying again.";
|
|
5255
|
+
case "exhausted":
|
|
5256
|
+
return `Gave up updating after ${MAX_UPDATE_ATTEMPTS} attempts. Still on ${f.current} \u2014 run \`${RUNNER_BIN} update\` to see why, or wait for the next release.`;
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
|
|
5260
|
+
// src/update/install.ts
|
|
5261
|
+
var INSTALL_TIMEOUT_MS = 5 * 6e4;
|
|
5262
|
+
var STDERR_KEEP2 = 400;
|
|
5263
|
+
function npmPresent() {
|
|
5264
|
+
const probe2 = process.platform === "win32" ? "where" : "which";
|
|
5265
|
+
try {
|
|
5266
|
+
return spawnSync(probe2, ["npm"], { encoding: "utf8" }).status === 0;
|
|
5267
|
+
} catch {
|
|
5268
|
+
return false;
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
function installArgs(target, cliPath2) {
|
|
5272
|
+
const args = ["install", "--global", "--no-fund", "--no-audit", `${PACKAGE_NAME}@${target}`];
|
|
5273
|
+
const prefix = globalPrefixFrom(cliPath2);
|
|
5274
|
+
if (prefix) args.push("--prefix", prefix);
|
|
5275
|
+
return args;
|
|
5276
|
+
}
|
|
5277
|
+
async function installGlobal(target, cliPath2, options = {}) {
|
|
5278
|
+
const args = installArgs(target, cliPath2);
|
|
5279
|
+
return new Promise((resolve) => {
|
|
5280
|
+
let child;
|
|
5281
|
+
try {
|
|
5282
|
+
child = spawn7("npm", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
5283
|
+
} catch (e) {
|
|
5284
|
+
resolve({ ok: false, detail: e instanceof Error ? e.message : String(e) });
|
|
5285
|
+
return;
|
|
5286
|
+
}
|
|
5287
|
+
let stderr = "";
|
|
5288
|
+
child.stderr?.on("data", (chunk) => {
|
|
5289
|
+
stderr = `${stderr}${chunk.toString()}`.slice(-STDERR_KEEP2);
|
|
5290
|
+
});
|
|
5291
|
+
let settled = false;
|
|
5292
|
+
const finish = (result) => {
|
|
5293
|
+
if (settled) return;
|
|
5294
|
+
settled = true;
|
|
5295
|
+
clearTimeout(timer);
|
|
5296
|
+
resolve(result);
|
|
5297
|
+
};
|
|
5298
|
+
const timeoutMs = options.timeoutMs ?? INSTALL_TIMEOUT_MS;
|
|
5299
|
+
const timer = setTimeout(() => {
|
|
5300
|
+
child.kill("SIGTERM");
|
|
5301
|
+
setTimeout(() => child.kill("SIGKILL"), 5e3).unref();
|
|
5302
|
+
finish({ ok: false, detail: `npm install timed out after ${timeoutMs / 6e4} minutes.` });
|
|
5303
|
+
}, timeoutMs);
|
|
5304
|
+
timer.unref();
|
|
5305
|
+
child.on("error", (e) => finish({ ok: false, detail: e.message }));
|
|
5306
|
+
child.on(
|
|
5307
|
+
"close",
|
|
5308
|
+
(code) => finish({ ok: code === 0, detail: stderr.trim() || `npm exited ${code}` })
|
|
5309
|
+
);
|
|
5310
|
+
});
|
|
5311
|
+
}
|
|
5312
|
+
function installedVersionAt(cliPath2) {
|
|
5313
|
+
try {
|
|
5314
|
+
const pkg = JSON.parse(
|
|
5315
|
+
fs11.readFileSync(path12.join(packageRootFrom(cliPath2), "package.json"), "utf8")
|
|
5316
|
+
);
|
|
5317
|
+
const version = pkg?.version;
|
|
5318
|
+
return typeof version === "string" ? version : null;
|
|
5319
|
+
} catch {
|
|
5320
|
+
return null;
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5324
|
+
// src/update/registry.ts
|
|
5325
|
+
var DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
5326
|
+
var REGISTRY_TIMEOUT_MS = 1e4;
|
|
5327
|
+
function registryBase(env = process.env) {
|
|
5328
|
+
return (env.LUMI_RUNNER_REGISTRY || DEFAULT_REGISTRY).replace(/\/+$/, "");
|
|
5329
|
+
}
|
|
5330
|
+
async function fetchDistTags(options = {}) {
|
|
5331
|
+
const base = options.registry ?? registryBase();
|
|
5332
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
5333
|
+
const url = `${base}/${encodeURIComponent(options.packageName ?? PACKAGE_NAME)}`;
|
|
5334
|
+
const controller = new AbortController();
|
|
5335
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? REGISTRY_TIMEOUT_MS);
|
|
5336
|
+
try {
|
|
5337
|
+
const response = await doFetch(url, {
|
|
5338
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
5339
|
+
signal: controller.signal
|
|
5340
|
+
});
|
|
5341
|
+
if (!response.ok) return null;
|
|
5342
|
+
const body = await response.json();
|
|
5343
|
+
const tags = body?.["dist-tags"];
|
|
5344
|
+
if (!tags || typeof tags !== "object" || Array.isArray(tags)) return null;
|
|
5345
|
+
const out = {};
|
|
5346
|
+
for (const [tag, version] of Object.entries(tags)) {
|
|
5347
|
+
if (typeof version === "string") out[tag] = version;
|
|
5348
|
+
}
|
|
5349
|
+
return out;
|
|
5350
|
+
} catch {
|
|
5351
|
+
return null;
|
|
5352
|
+
} finally {
|
|
5353
|
+
clearTimeout(timer);
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
// src/jobs/engineTools.ts
|
|
5358
|
+
async function refreshEngineTools(input) {
|
|
5359
|
+
const now = input.now ?? Date.now();
|
|
5360
|
+
const out = [];
|
|
5361
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5362
|
+
for (const engineId of input.engineIds) {
|
|
5363
|
+
if (seen.has(engineId)) continue;
|
|
5364
|
+
seen.add(engineId);
|
|
5365
|
+
out.push(await refreshOne(engineId, input, now));
|
|
5366
|
+
}
|
|
5367
|
+
return out;
|
|
5368
|
+
}
|
|
5369
|
+
async function refreshOne(engineId, input, now) {
|
|
5370
|
+
const { root, log: log2 } = input;
|
|
5371
|
+
const pkg = enginePackageFor(engineId);
|
|
5372
|
+
if (!pkg) {
|
|
5373
|
+
return { engineId, state: "missing", detail: describeEngineInstallRefusal("unknown-engine", engineId, input.runnerBin) };
|
|
5374
|
+
}
|
|
5375
|
+
const policy = resolveEnginePolicy(engineId, input.policyDoc);
|
|
5376
|
+
const marker = readEngineMarker(root, engineId);
|
|
5377
|
+
const installed = managedEngineBin(engineId, { root }) ? marker?.version ?? null : null;
|
|
5378
|
+
let latest = null;
|
|
5379
|
+
if (installed) {
|
|
5380
|
+
const ask = input.fetchLatest ?? (async (name) => (await fetchDistTags({ packageName: name }))?.latest ?? null);
|
|
5381
|
+
latest = await ask(pkg.npmPackage).catch(() => null);
|
|
5382
|
+
if (latest) rememberFirstSeen(root, engineId, latest, now);
|
|
5383
|
+
}
|
|
5384
|
+
const record = readEngineState(root)[engineId];
|
|
5385
|
+
const choice = chooseEngineVersion({ policy, latest, installed, firstSeen: record?.firstSeen, now });
|
|
5386
|
+
if (!choice.version) return describeCurrent(engineId, input, marker, record, choice.reason);
|
|
5387
|
+
const decision = decideEngineInstall({
|
|
5388
|
+
engineId,
|
|
5389
|
+
target: choice.version,
|
|
5390
|
+
platform: process.platform,
|
|
5391
|
+
autoInstall: input.autoInstall,
|
|
5392
|
+
npmPresent: npmPresentOnce(),
|
|
5393
|
+
marker,
|
|
5394
|
+
locked: false,
|
|
5395
|
+
record: record ?? null,
|
|
5396
|
+
now
|
|
5397
|
+
});
|
|
5398
|
+
if (!decision.install) {
|
|
5399
|
+
if (decision.reason === "already-installed") return { engineId, state: "ready", version: marker?.version };
|
|
5400
|
+
return describeRefused(engineId, input, marker, decision.reason);
|
|
5401
|
+
}
|
|
5402
|
+
input.onState?.({ engineId, state: "installing", version: choice.version });
|
|
5403
|
+
const outcome = await installEngine({
|
|
5404
|
+
root,
|
|
5405
|
+
engineId,
|
|
5406
|
+
version: choice.version,
|
|
5407
|
+
healthCheck: (bin) => getDriver(engineId).healthCheck(bin),
|
|
5408
|
+
log: log2,
|
|
5409
|
+
now
|
|
5410
|
+
});
|
|
5411
|
+
if (outcome.ok) return { engineId, state: "ready", version: choice.version };
|
|
5412
|
+
log2(`Could not install "${engineId}": ${outcome.detail.slice(0, 200)}`);
|
|
5413
|
+
const fallback = await unmanagedVersion(engineId);
|
|
5414
|
+
if (fallback && !policy.blocked.includes(fallback)) {
|
|
5415
|
+
return { engineId, state: "unmanaged", version: fallback, detail: "Using a copy already on this machine \u2014 this runner cannot update or roll it back." };
|
|
5416
|
+
}
|
|
5417
|
+
return { engineId, state: "failed", detail: outcome.detail.slice(0, 300) };
|
|
5418
|
+
}
|
|
5419
|
+
async function ensureEngineReady(engineId, input) {
|
|
5420
|
+
const [state] = await refreshEngineTools({ ...input, engineIds: [engineId] });
|
|
5421
|
+
if (!state) return { ok: false, detail: `This runner does not know the engine "${engineId}".` };
|
|
5422
|
+
if (state.state === "ready" || state.state === "unmanaged") return { ok: true, detail: state.detail ?? "" };
|
|
5423
|
+
return { ok: false, detail: state.detail ?? `The "${engineId}" CLI is not installed on this machine.` };
|
|
5424
|
+
}
|
|
5425
|
+
function describeCurrent(engineId, input, marker, record, reason) {
|
|
5426
|
+
if (marker && managedEngineBin(engineId, { root: input.root })) {
|
|
5427
|
+
return { engineId, state: "ready", version: marker.version };
|
|
5428
|
+
}
|
|
5429
|
+
if (reason === "no-policy") {
|
|
5430
|
+
return { engineId, state: "missing", detail: `This runner has no version recorded for "${engineId}".` };
|
|
5431
|
+
}
|
|
5432
|
+
if (record?.outcome === "failed") return { engineId, state: "failed", detail: record.detail };
|
|
5433
|
+
return { engineId, state: "missing" };
|
|
5434
|
+
}
|
|
5435
|
+
function describeRefused(engineId, input, marker, reason) {
|
|
5436
|
+
const detail = describeEngineInstallRefusal(reason, engineId, input.runnerBin);
|
|
5437
|
+
if (marker && managedEngineBin(engineId, { root: input.root })) {
|
|
5438
|
+
return { engineId, state: "ready", version: marker.version, detail };
|
|
5439
|
+
}
|
|
5440
|
+
return { engineId, state: "missing", detail };
|
|
5441
|
+
}
|
|
5442
|
+
function rememberFirstSeen(root, engineId, version, now) {
|
|
5443
|
+
const record = readEngineState(root)[engineId];
|
|
5444
|
+
if (record?.firstSeen?.version === version) return;
|
|
5445
|
+
recordEngineInstall(root, engineId, { firstSeen: { version, at: now } });
|
|
5446
|
+
}
|
|
5447
|
+
function npmPresentOnce() {
|
|
5448
|
+
let answer = null;
|
|
5449
|
+
return () => {
|
|
5450
|
+
if (answer === null) answer = npmPresent();
|
|
5451
|
+
return answer;
|
|
5452
|
+
};
|
|
5453
|
+
}
|
|
5454
|
+
async function unmanagedVersion(engineId) {
|
|
5455
|
+
try {
|
|
5456
|
+
const health = await getDriver(engineId).healthCheck();
|
|
5457
|
+
if (!health.ok) return null;
|
|
5458
|
+
const match = /(\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?)/.exec(health.detail ?? "");
|
|
5459
|
+
return match?.[1] ?? null;
|
|
5460
|
+
} catch {
|
|
5461
|
+
return null;
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
|
|
5465
|
+
// src/jobs/shipEngines.ts
|
|
5466
|
+
function enginesForAgents(agents) {
|
|
5467
|
+
return engineSet(agents.map((agent) => agentEngine(agent)));
|
|
5468
|
+
}
|
|
5469
|
+
function engineSet(engineIds) {
|
|
5470
|
+
const engines = new Set(engineIds);
|
|
5471
|
+
if (engines.size === 0) engines.add(DEFAULT_ENGINE_ID);
|
|
5472
|
+
return engines;
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5475
|
+
// src/service.ts
|
|
5476
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5477
|
+
import fs12 from "node:fs";
|
|
5478
|
+
import os4 from "node:os";
|
|
5479
|
+
import path13 from "node:path";
|
|
5480
|
+
import { fileURLToPath } from "node:url";
|
|
5481
|
+
var SERVICE_LABEL = `com.kilogent.${RUNNER_BIN.replace(/^kilogent-/, "")}`;
|
|
5482
|
+
var LINUX_UNIT = `${RUNNER_BIN}.service`;
|
|
5483
|
+
var WINDOWS_TASK = RUNNER_BIN.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("");
|
|
5484
|
+
var LEGACY_SERVICE_LABEL = "com.lumi.crew-runner";
|
|
5485
|
+
var LEGACY_LINUX_UNIT = "crew-runner.service";
|
|
5486
|
+
var LEGACY_WINDOWS_TASK = "CrewRunner";
|
|
5487
|
+
var RETIRED_SERVICES = [
|
|
5488
|
+
{ label: LEGACY_SERVICE_LABEL, unit: LEGACY_LINUX_UNIT, task: LEGACY_WINDOWS_TASK },
|
|
5489
|
+
{ label: "com.lumi.runner", unit: "lumi-runner.service", task: "LumiRunner" }
|
|
5490
|
+
];
|
|
5491
|
+
var ServiceError = class extends Error {
|
|
5492
|
+
};
|
|
5493
|
+
function run(command, args) {
|
|
5494
|
+
const result = spawnSync2(command, args, { encoding: "utf8" });
|
|
5495
|
+
return {
|
|
5496
|
+
ok: result.status === 0,
|
|
5497
|
+
out: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
|
|
5498
|
+
};
|
|
5499
|
+
}
|
|
5500
|
+
function cliPath() {
|
|
5501
|
+
return fileURLToPath(import.meta.url);
|
|
5502
|
+
}
|
|
5503
|
+
function uid() {
|
|
5504
|
+
return String(process.getuid?.() ?? 0);
|
|
5505
|
+
}
|
|
5506
|
+
function launchAgentPath() {
|
|
5507
|
+
return path13.join(os4.homedir(), "Library/LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
5508
|
+
}
|
|
5509
|
+
function systemdUnitPath() {
|
|
5510
|
+
return path13.join(os4.homedir(), ".config/systemd/user", LINUX_UNIT);
|
|
5511
|
+
}
|
|
5512
|
+
function serviceEnv() {
|
|
5513
|
+
const env = { PATH: process.env.PATH ?? "" };
|
|
5514
|
+
if (process.env.LUMI_RUNNER_HOME || process.env.CREW_RUNNER_HOME) {
|
|
5515
|
+
env.LUMI_RUNNER_HOME = configDir();
|
|
5516
|
+
}
|
|
5517
|
+
return env;
|
|
5518
|
+
}
|
|
5519
|
+
function serviceEnvDrift(current, installed) {
|
|
5520
|
+
if (!installed) return [];
|
|
5521
|
+
return Object.keys(current).filter((name) => current[name] !== installed[name]).sort();
|
|
5522
|
+
}
|
|
5523
|
+
function removeLegacyService() {
|
|
5524
|
+
const removed = [];
|
|
5525
|
+
for (const old of RETIRED_SERVICES) {
|
|
5526
|
+
if (old.label === SERVICE_LABEL) continue;
|
|
5527
|
+
if (process.platform === "darwin") {
|
|
5528
|
+
const unitPath = path13.join(os4.homedir(), "Library/LaunchAgents", `${old.label}.plist`);
|
|
5529
|
+
if (fs12.existsSync(unitPath)) {
|
|
5530
|
+
run("launchctl", ["bootout", `gui/${uid()}/${old.label}`]);
|
|
5531
|
+
fs12.rmSync(unitPath, { force: true });
|
|
5532
|
+
removed.push(unitPath);
|
|
5533
|
+
}
|
|
5534
|
+
continue;
|
|
5535
|
+
}
|
|
5536
|
+
if (process.platform === "linux") {
|
|
5537
|
+
const unitPath = path13.join(os4.homedir(), ".config/systemd/user", old.unit);
|
|
5538
|
+
if (fs12.existsSync(unitPath)) {
|
|
5539
|
+
run("systemctl", ["--user", "disable", "--now", old.unit]);
|
|
5540
|
+
fs12.rmSync(unitPath, { force: true });
|
|
5541
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
5542
|
+
removed.push(unitPath);
|
|
5543
|
+
}
|
|
5544
|
+
continue;
|
|
5545
|
+
}
|
|
5546
|
+
if (process.platform === "win32") {
|
|
5547
|
+
if (run("schtasks", ["/Query", "/TN", old.task]).ok) {
|
|
5548
|
+
run("schtasks", ["/Delete", "/TN", old.task, "/F"]);
|
|
5549
|
+
removed.push(old.task);
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
return removed;
|
|
5554
|
+
}
|
|
5555
|
+
var BOOTSTRAP_RETRY_DELAYS_MS = [250, 500, 1e3, 2e3, 2e3];
|
|
5556
|
+
function isTransientBootstrapError(out) {
|
|
5557
|
+
const text2 = out.toLowerCase();
|
|
5558
|
+
return (
|
|
5559
|
+
// The observed one: `Bootstrap failed: 5: Input/output error`.
|
|
5560
|
+
text2.includes("input/output error") || // Same cause, different report: the old generation is still registered.
|
|
5561
|
+
text2.includes("service already loaded") || text2.includes("service is already loaded") || text2.includes("eexist") || // launchd is mid-teardown and says so.
|
|
5562
|
+
text2.includes("operation already in progress") || text2.includes("operation now in progress")
|
|
5563
|
+
);
|
|
5564
|
+
}
|
|
5565
|
+
function retryWhileTransient(attempt, wait, delays = BOOTSTRAP_RETRY_DELAYS_MS) {
|
|
5566
|
+
let result = attempt();
|
|
5567
|
+
for (const delay of delays) {
|
|
5568
|
+
if (result.ok || !isTransientBootstrapError(result.out)) return result;
|
|
5569
|
+
wait(delay);
|
|
5570
|
+
result = attempt();
|
|
5571
|
+
}
|
|
5572
|
+
return result;
|
|
5573
|
+
}
|
|
5574
|
+
function sleepSync(ms) {
|
|
5575
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5576
|
+
}
|
|
5577
|
+
function plistXml() {
|
|
5578
|
+
const env = serviceEnv();
|
|
5579
|
+
const envEntries = Object.entries(env).map(([k, v]) => ` <key>${k}</key>
|
|
5580
|
+
<string>${escapeXml(v)}</string>`).join("\n");
|
|
5581
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
5582
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
5583
|
+
<plist version="1.0">
|
|
5584
|
+
<dict>
|
|
5585
|
+
<key>Label</key>
|
|
5586
|
+
<string>${SERVICE_LABEL}</string>
|
|
4780
5587
|
<key>ProgramArguments</key>
|
|
4781
5588
|
<array>
|
|
4782
5589
|
<string>${escapeXml(process.execPath)}</string>
|
|
@@ -4794,9 +5601,9 @@ function plistXml() {
|
|
|
4794
5601
|
${envEntries}
|
|
4795
5602
|
</dict>
|
|
4796
5603
|
<key>StandardOutPath</key>
|
|
4797
|
-
<string>${escapeXml(
|
|
5604
|
+
<string>${escapeXml(path13.join(logDir(), "service.out.log"))}</string>
|
|
4798
5605
|
<key>StandardErrorPath</key>
|
|
4799
|
-
<string>${escapeXml(
|
|
5606
|
+
<string>${escapeXml(path13.join(logDir(), "service.err.log"))}</string>
|
|
4800
5607
|
</dict>
|
|
4801
5608
|
</plist>
|
|
4802
5609
|
`;
|
|
@@ -4842,14 +5649,14 @@ function parseSystemdCliPath(unit) {
|
|
|
4842
5649
|
function execFacts(unitPath, parse, parseEnv) {
|
|
4843
5650
|
let text2;
|
|
4844
5651
|
try {
|
|
4845
|
-
text2 =
|
|
5652
|
+
text2 = fs12.readFileSync(unitPath, "utf8");
|
|
4846
5653
|
} catch {
|
|
4847
5654
|
return {};
|
|
4848
5655
|
}
|
|
4849
5656
|
const unitEnv = parseEnv(text2);
|
|
4850
5657
|
const execPath = parse(text2);
|
|
4851
|
-
if (!execPath || !
|
|
4852
|
-
return { execPath, execMissing: !
|
|
5658
|
+
if (!execPath || !path13.isAbsolute(execPath)) return { ...unitEnv ? { unitEnv } : {} };
|
|
5659
|
+
return { execPath, execMissing: !fs12.existsSync(execPath), ...unitEnv ? { unitEnv } : {} };
|
|
4853
5660
|
}
|
|
4854
5661
|
function systemdUnit() {
|
|
4855
5662
|
const env = serviceEnv();
|
|
@@ -4875,7 +5682,7 @@ WantedBy=default.target
|
|
|
4875
5682
|
function serviceStatus() {
|
|
4876
5683
|
if (process.platform === "darwin") {
|
|
4877
5684
|
const unitPath = launchAgentPath();
|
|
4878
|
-
if (!
|
|
5685
|
+
if (!fs12.existsSync(unitPath)) return { state: "not-installed", detail: "No LaunchAgent installed." };
|
|
4879
5686
|
const exec = execFacts(unitPath, parsePlistCliPath, parsePlistEnv);
|
|
4880
5687
|
const printed = run("launchctl", ["print", `gui/${uid()}/${SERVICE_LABEL}`]);
|
|
4881
5688
|
if (!printed.ok) {
|
|
@@ -4891,7 +5698,7 @@ function serviceStatus() {
|
|
|
4891
5698
|
}
|
|
4892
5699
|
if (process.platform === "linux") {
|
|
4893
5700
|
const unitPath = systemdUnitPath();
|
|
4894
|
-
if (!
|
|
5701
|
+
if (!fs12.existsSync(unitPath)) return { state: "not-installed", detail: "No systemd user unit installed." };
|
|
4895
5702
|
const active = run("systemctl", ["--user", "is-active", LINUX_UNIT]);
|
|
4896
5703
|
return {
|
|
4897
5704
|
state: active.out === "active" ? "running" : "installed",
|
|
@@ -4913,14 +5720,14 @@ function serviceStatus() {
|
|
|
4913
5720
|
}
|
|
4914
5721
|
function installService() {
|
|
4915
5722
|
const notes = [];
|
|
4916
|
-
|
|
5723
|
+
fs12.mkdirSync(logDir(), { recursive: true, mode: 448 });
|
|
4917
5724
|
for (const unit of removeLegacyService()) {
|
|
4918
5725
|
notes.push(`Removed the previous crew-runner service (${unit}).`);
|
|
4919
5726
|
}
|
|
4920
5727
|
if (process.platform === "darwin") {
|
|
4921
5728
|
const unitPath = launchAgentPath();
|
|
4922
|
-
|
|
4923
|
-
|
|
5729
|
+
fs12.mkdirSync(path13.dirname(unitPath), { recursive: true });
|
|
5730
|
+
fs12.writeFileSync(unitPath, plistXml());
|
|
4924
5731
|
run("launchctl", ["bootout", `gui/${uid()}/${SERVICE_LABEL}`]);
|
|
4925
5732
|
const boot = retryWhileTransient(
|
|
4926
5733
|
() => run("launchctl", ["bootstrap", `gui/${uid()}`, unitPath]),
|
|
@@ -4931,272 +5738,91 @@ function installService() {
|
|
|
4931
5738
|
}
|
|
4932
5739
|
if (process.platform === "linux") {
|
|
4933
5740
|
const unitPath = systemdUnitPath();
|
|
4934
|
-
|
|
4935
|
-
|
|
5741
|
+
fs12.mkdirSync(path13.dirname(unitPath), { recursive: true });
|
|
5742
|
+
fs12.writeFileSync(unitPath, systemdUnit());
|
|
4936
5743
|
const reload = run("systemctl", ["--user", "daemon-reload"]);
|
|
4937
5744
|
if (!reload.ok) throw new ServiceError(`systemctl daemon-reload failed: ${reload.out}`);
|
|
4938
5745
|
const enable = run("systemctl", ["--user", "enable", "--now", LINUX_UNIT]);
|
|
4939
5746
|
if (!enable.ok) throw new ServiceError(`systemctl enable failed: ${enable.out}`);
|
|
4940
5747
|
const linger = run("loginctl", ["show-user", os4.userInfo().username, "--property=Linger"]);
|
|
4941
5748
|
if (!linger.out.includes("Linger=yes")) {
|
|
4942
|
-
notes.push(
|
|
4943
|
-
`Run \`sudo loginctl enable-linger ${os4.userInfo().username}\` so the daemon survives logout and starts at boot.`
|
|
4944
|
-
);
|
|
4945
|
-
}
|
|
4946
|
-
return { unitPath, notes };
|
|
4947
|
-
}
|
|
4948
|
-
if (process.platform === "win32") {
|
|
4949
|
-
const command = `"${process.execPath}" "${cliPath()}" start`;
|
|
4950
|
-
const create = run("schtasks", [
|
|
4951
|
-
"/Create",
|
|
4952
|
-
"/TN",
|
|
4953
|
-
WINDOWS_TASK,
|
|
4954
|
-
"/TR",
|
|
4955
|
-
command,
|
|
4956
|
-
"/SC",
|
|
4957
|
-
"ONLOGON",
|
|
4958
|
-
"/RL",
|
|
4959
|
-
"LIMITED",
|
|
4960
|
-
"/F"
|
|
4961
|
-
]);
|
|
4962
|
-
if (!create.ok) throw new ServiceError(`schtasks /Create failed: ${create.out}`);
|
|
4963
|
-
notes.push(
|
|
4964
|
-
"Task Scheduler starts the daemon at logon but does not restart it if it exits. For a true always-on box, run the daemon under pm2 or a Windows service wrapper instead."
|
|
4965
|
-
);
|
|
4966
|
-
return { unitPath: WINDOWS_TASK, notes };
|
|
4967
|
-
}
|
|
4968
|
-
throw new ServiceError(`Service install is not supported on ${process.platform}.`);
|
|
4969
|
-
}
|
|
4970
|
-
function uninstallService() {
|
|
4971
|
-
removeLegacyService();
|
|
4972
|
-
if (process.platform === "darwin") {
|
|
4973
|
-
run("launchctl", ["bootout", `gui/${uid()}/${SERVICE_LABEL}`]);
|
|
4974
|
-
fs6.rmSync(launchAgentPath(), { force: true });
|
|
4975
|
-
return;
|
|
4976
|
-
}
|
|
4977
|
-
if (process.platform === "linux") {
|
|
4978
|
-
run("systemctl", ["--user", "disable", "--now", LINUX_UNIT]);
|
|
4979
|
-
fs6.rmSync(systemdUnitPath(), { force: true });
|
|
4980
|
-
run("systemctl", ["--user", "daemon-reload"]);
|
|
4981
|
-
return;
|
|
4982
|
-
}
|
|
4983
|
-
if (process.platform === "win32") {
|
|
4984
|
-
run("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
|
|
4985
|
-
return;
|
|
4986
|
-
}
|
|
4987
|
-
throw new ServiceError(`Service uninstall is not supported on ${process.platform}.`);
|
|
4988
|
-
}
|
|
4989
|
-
function restartService() {
|
|
4990
|
-
if (process.platform === "darwin") {
|
|
4991
|
-
const result = run("launchctl", ["kickstart", "-k", `gui/${uid()}/${SERVICE_LABEL}`]);
|
|
4992
|
-
if (!result.ok) throw new ServiceError(`launchctl kickstart failed: ${result.out}`);
|
|
4993
|
-
return;
|
|
4994
|
-
}
|
|
4995
|
-
if (process.platform === "linux") {
|
|
4996
|
-
const result = run("systemctl", ["--user", "restart", LINUX_UNIT]);
|
|
4997
|
-
if (!result.ok) throw new ServiceError(`systemctl restart failed: ${result.out}`);
|
|
4998
|
-
return;
|
|
4999
|
-
}
|
|
5000
|
-
if (process.platform === "win32") {
|
|
5001
|
-
run("schtasks", ["/End", "/TN", WINDOWS_TASK]);
|
|
5002
|
-
const result = run("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
|
|
5003
|
-
if (!result.ok) throw new ServiceError(`schtasks /Run failed: ${result.out}`);
|
|
5004
|
-
return;
|
|
5005
|
-
}
|
|
5006
|
-
throw new ServiceError(`Service restart is not supported on ${process.platform}.`);
|
|
5007
|
-
}
|
|
5008
|
-
|
|
5009
|
-
// src/update/plan.ts
|
|
5010
|
-
import path8 from "node:path";
|
|
5011
|
-
|
|
5012
|
-
// src/update/semver.ts
|
|
5013
|
-
var DEV_VERSION = "0.0.0-dev";
|
|
5014
|
-
var NUMERIC = /^(0|[1-9]\d*)$/;
|
|
5015
|
-
function parseVersion(raw) {
|
|
5016
|
-
if (typeof raw !== "string") return null;
|
|
5017
|
-
const trimmed = raw.trim();
|
|
5018
|
-
if (!trimmed) return null;
|
|
5019
|
-
const [withoutBuild] = trimmed.split("+", 1);
|
|
5020
|
-
const dash = withoutBuild.indexOf("-");
|
|
5021
|
-
const core = dash < 0 ? withoutBuild : withoutBuild.slice(0, dash);
|
|
5022
|
-
const pre = dash < 0 ? "" : withoutBuild.slice(dash + 1);
|
|
5023
|
-
const parts = core.split(".");
|
|
5024
|
-
if (parts.length !== 3) return null;
|
|
5025
|
-
if (!parts.every((p) => NUMERIC.test(p))) return null;
|
|
5026
|
-
const [major, minor, patch] = parts.map(Number);
|
|
5027
|
-
if (dash >= 0 && pre === "") return null;
|
|
5028
|
-
const prerelease = pre === "" ? [] : pre.split(".");
|
|
5029
|
-
if (prerelease.some((id) => id === "")) return null;
|
|
5030
|
-
return {
|
|
5031
|
-
major,
|
|
5032
|
-
minor,
|
|
5033
|
-
patch,
|
|
5034
|
-
prerelease: prerelease.map((id) => NUMERIC.test(id) ? Number(id) : id)
|
|
5035
|
-
};
|
|
5036
|
-
}
|
|
5037
|
-
function comparePrerelease(a, b) {
|
|
5038
|
-
if (a.length === 0) return b.length === 0 ? 0 : 1;
|
|
5039
|
-
if (b.length === 0) return -1;
|
|
5040
|
-
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
5041
|
-
const x = a[i];
|
|
5042
|
-
const y = b[i];
|
|
5043
|
-
if (x === y) continue;
|
|
5044
|
-
const xNum = typeof x === "number";
|
|
5045
|
-
const yNum = typeof y === "number";
|
|
5046
|
-
if (xNum !== yNum) return xNum ? -1 : 1;
|
|
5047
|
-
if (xNum && yNum) return x < y ? -1 : 1;
|
|
5048
|
-
return x < y ? -1 : 1;
|
|
5049
|
-
}
|
|
5050
|
-
if (a.length === b.length) return 0;
|
|
5051
|
-
return a.length < b.length ? -1 : 1;
|
|
5052
|
-
}
|
|
5053
|
-
function compareVersions(a, b) {
|
|
5054
|
-
for (const key of ["major", "minor", "patch"]) {
|
|
5055
|
-
if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
|
|
5056
|
-
}
|
|
5057
|
-
return comparePrerelease(a.prerelease, b.prerelease);
|
|
5058
|
-
}
|
|
5059
|
-
function isNewer(candidate, current) {
|
|
5060
|
-
if (current === DEV_VERSION) return false;
|
|
5061
|
-
const a = parseVersion(candidate);
|
|
5062
|
-
const b = parseVersion(current);
|
|
5063
|
-
if (!a || !b) return false;
|
|
5064
|
-
return compareVersions(a, b) === 1;
|
|
5065
|
-
}
|
|
5066
|
-
|
|
5067
|
-
// src/update/plan.ts
|
|
5068
|
-
var PACKAGE_NAME = RUNNER_PACKAGE;
|
|
5069
|
-
var UPDATE_DIST_TAG = "latest";
|
|
5070
|
-
var UPDATE_CHECK_MS = 30 * 6e4;
|
|
5071
|
-
var UPDATE_FIRST_CHECK_MS = 2 * 6e4;
|
|
5072
|
-
var UPDATE_JITTER_MS = 10 * 6e4;
|
|
5073
|
-
var CHECK_MINUTES_MIN = 5;
|
|
5074
|
-
var CHECK_MINUTES_MAX = 24 * 60;
|
|
5075
|
-
var UPDATE_DRAIN_MAX_MS = 30 * 6e4;
|
|
5076
|
-
var MAX_UPDATE_ATTEMPTS = 3;
|
|
5077
|
-
var UPDATE_BACKOFF_MS = [15 * 6e4, 2 * 36e5, 12 * 36e5];
|
|
5078
|
-
function checkIntervalMs(config2) {
|
|
5079
|
-
const raw = config2.updateCheckMinutes;
|
|
5080
|
-
if (typeof raw !== "number" || !Number.isFinite(raw)) return UPDATE_CHECK_MS;
|
|
5081
|
-
const clamped2 = Math.min(CHECK_MINUTES_MAX, Math.max(CHECK_MINUTES_MIN, Math.floor(raw)));
|
|
5082
|
-
return clamped2 * 6e4;
|
|
5083
|
-
}
|
|
5084
|
-
function nextAttemptAllowed(state, target, now) {
|
|
5085
|
-
if (!state || typeof state !== "object") return { allowed: true };
|
|
5086
|
-
if (state.target !== target) return { allowed: true };
|
|
5087
|
-
const attempts = typeof state.attempts === "number" && state.attempts > 0 ? state.attempts : 0;
|
|
5088
|
-
if (attempts === 0) return { allowed: true };
|
|
5089
|
-
if (attempts >= MAX_UPDATE_ATTEMPTS) return { allowed: false, reason: "exhausted" };
|
|
5090
|
-
if (typeof state.at !== "number" || !Number.isFinite(state.at)) return { allowed: true };
|
|
5091
|
-
const wait = UPDATE_BACKOFF_MS[Math.min(attempts - 1, UPDATE_BACKOFF_MS.length - 1)];
|
|
5092
|
-
return now - state.at >= wait ? { allowed: true } : { allowed: false, reason: "backoff" };
|
|
5093
|
-
}
|
|
5094
|
-
function reconcile(state, current) {
|
|
5095
|
-
if (!state || typeof state.target !== "string") return { action: "none" };
|
|
5096
|
-
if (state.target === current) return { action: "clear", target: state.target };
|
|
5097
|
-
return { action: "keep", state };
|
|
5098
|
-
}
|
|
5099
|
-
function packageRootFrom(cliPath2) {
|
|
5100
|
-
return path8.dirname(path8.dirname(cliPath2));
|
|
5101
|
-
}
|
|
5102
|
-
function globalPrefixFrom(cliPath2) {
|
|
5103
|
-
const root = packageRootFrom(cliPath2);
|
|
5104
|
-
const parts = root.split(path8.sep);
|
|
5105
|
-
const i = parts.lastIndexOf("node_modules");
|
|
5106
|
-
if (i < 1) return null;
|
|
5107
|
-
const before = parts.slice(0, i);
|
|
5108
|
-
if (before[before.length - 1] === "lib") before.pop();
|
|
5109
|
-
const prefix = before.join(path8.sep);
|
|
5110
|
-
return prefix || null;
|
|
5111
|
-
}
|
|
5112
|
-
function decideUpdate(f) {
|
|
5113
|
-
if (f.platform === "win32") return { update: false, reason: "windows" };
|
|
5114
|
-
if (f.current === DEV_VERSION) return { update: false, reason: "dev-build" };
|
|
5115
|
-
if (!f.autoUpdate) return { update: false, reason: "disabled" };
|
|
5116
|
-
const service2 = f.service();
|
|
5117
|
-
if (service2.state !== "running") return { update: false, reason: "service-not-running" };
|
|
5118
|
-
if (!service2.execPath || service2.execPath !== f.cliPath) {
|
|
5119
|
-
return { update: false, reason: "foreign-unit" };
|
|
5749
|
+
notes.push(
|
|
5750
|
+
`Run \`sudo loginctl enable-linger ${os4.userInfo().username}\` so the daemon survives logout and starts at boot.`
|
|
5751
|
+
);
|
|
5752
|
+
}
|
|
5753
|
+
return { unitPath, notes };
|
|
5120
5754
|
}
|
|
5121
|
-
if (
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
case "foreign-unit":
|
|
5141
|
-
return `This process is not the one the installed service runs, so auto-update is off for it. Run \`${RUNNER_BIN} update\` if you meant to upgrade the installed copy.`;
|
|
5142
|
-
case "no-npm":
|
|
5143
|
-
return "Auto-update needs `npm` on the service PATH and could not find it.";
|
|
5144
|
-
case "registry-unreachable":
|
|
5145
|
-
return "Could not reach the npm registry to check for updates \u2014 will retry.";
|
|
5146
|
-
case "no-such-tag":
|
|
5147
|
-
return `The npm registry has no release of ${PACKAGE_NAME} yet.`;
|
|
5148
|
-
case "up-to-date":
|
|
5149
|
-
return `Up to date (${f.current}).`;
|
|
5150
|
-
case "backoff":
|
|
5151
|
-
return "A recent update attempt did not take \u2014 waiting before trying again.";
|
|
5152
|
-
case "exhausted":
|
|
5153
|
-
return `Gave up updating after ${MAX_UPDATE_ATTEMPTS} attempts. Still on ${f.current} \u2014 run \`${RUNNER_BIN} update\` to see why, or wait for the next release.`;
|
|
5755
|
+
if (process.platform === "win32") {
|
|
5756
|
+
const command = `"${process.execPath}" "${cliPath()}" start`;
|
|
5757
|
+
const create = run("schtasks", [
|
|
5758
|
+
"/Create",
|
|
5759
|
+
"/TN",
|
|
5760
|
+
WINDOWS_TASK,
|
|
5761
|
+
"/TR",
|
|
5762
|
+
command,
|
|
5763
|
+
"/SC",
|
|
5764
|
+
"ONLOGON",
|
|
5765
|
+
"/RL",
|
|
5766
|
+
"LIMITED",
|
|
5767
|
+
"/F"
|
|
5768
|
+
]);
|
|
5769
|
+
if (!create.ok) throw new ServiceError(`schtasks /Create failed: ${create.out}`);
|
|
5770
|
+
notes.push(
|
|
5771
|
+
"Task Scheduler starts the daemon at logon but does not restart it if it exits. For a true always-on box, run the daemon under pm2 or a Windows service wrapper instead."
|
|
5772
|
+
);
|
|
5773
|
+
return { unitPath: WINDOWS_TASK, notes };
|
|
5154
5774
|
}
|
|
5775
|
+
throw new ServiceError(`Service install is not supported on ${process.platform}.`);
|
|
5155
5776
|
}
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5777
|
+
function uninstallService() {
|
|
5778
|
+
removeLegacyService();
|
|
5779
|
+
if (process.platform === "darwin") {
|
|
5780
|
+
run("launchctl", ["bootout", `gui/${uid()}/${SERVICE_LABEL}`]);
|
|
5781
|
+
fs12.rmSync(launchAgentPath(), { force: true });
|
|
5782
|
+
return;
|
|
5783
|
+
}
|
|
5784
|
+
if (process.platform === "linux") {
|
|
5785
|
+
run("systemctl", ["--user", "disable", "--now", LINUX_UNIT]);
|
|
5786
|
+
fs12.rmSync(systemdUnitPath(), { force: true });
|
|
5787
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
5788
|
+
return;
|
|
5789
|
+
}
|
|
5790
|
+
if (process.platform === "win32") {
|
|
5791
|
+
run("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
|
|
5792
|
+
return;
|
|
5793
|
+
}
|
|
5794
|
+
throw new ServiceError(`Service uninstall is not supported on ${process.platform}.`);
|
|
5162
5795
|
}
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
const
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
const
|
|
5177
|
-
if (!
|
|
5178
|
-
|
|
5179
|
-
for (const [tag, version] of Object.entries(tags)) {
|
|
5180
|
-
if (typeof version === "string") out[tag] = version;
|
|
5181
|
-
}
|
|
5182
|
-
return out;
|
|
5183
|
-
} catch {
|
|
5184
|
-
return null;
|
|
5185
|
-
} finally {
|
|
5186
|
-
clearTimeout(timer);
|
|
5796
|
+
function restartService() {
|
|
5797
|
+
if (process.platform === "darwin") {
|
|
5798
|
+
const result = run("launchctl", ["kickstart", "-k", `gui/${uid()}/${SERVICE_LABEL}`]);
|
|
5799
|
+
if (!result.ok) throw new ServiceError(`launchctl kickstart failed: ${result.out}`);
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
if (process.platform === "linux") {
|
|
5803
|
+
const result = run("systemctl", ["--user", "restart", LINUX_UNIT]);
|
|
5804
|
+
if (!result.ok) throw new ServiceError(`systemctl restart failed: ${result.out}`);
|
|
5805
|
+
return;
|
|
5806
|
+
}
|
|
5807
|
+
if (process.platform === "win32") {
|
|
5808
|
+
run("schtasks", ["/End", "/TN", WINDOWS_TASK]);
|
|
5809
|
+
const result = run("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
|
|
5810
|
+
if (!result.ok) throw new ServiceError(`schtasks /Run failed: ${result.out}`);
|
|
5811
|
+
return;
|
|
5187
5812
|
}
|
|
5813
|
+
throw new ServiceError(`Service restart is not supported on ${process.platform}.`);
|
|
5188
5814
|
}
|
|
5189
5815
|
|
|
5190
5816
|
// src/update/state.ts
|
|
5191
|
-
import
|
|
5192
|
-
import
|
|
5817
|
+
import fs13 from "node:fs";
|
|
5818
|
+
import path14 from "node:path";
|
|
5193
5819
|
var UPDATE_STATE_FILE = "update.json";
|
|
5194
5820
|
function updateStatePath() {
|
|
5195
|
-
return
|
|
5821
|
+
return path14.join(configDir(), UPDATE_STATE_FILE);
|
|
5196
5822
|
}
|
|
5197
5823
|
function readUpdateState() {
|
|
5198
5824
|
try {
|
|
5199
|
-
const parsed = JSON.parse(
|
|
5825
|
+
const parsed = JSON.parse(fs13.readFileSync(updateStatePath(), "utf8"));
|
|
5200
5826
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
5201
5827
|
return parsed;
|
|
5202
5828
|
} catch {
|
|
@@ -5205,8 +5831,8 @@ function readUpdateState() {
|
|
|
5205
5831
|
}
|
|
5206
5832
|
function writeUpdateState(state) {
|
|
5207
5833
|
try {
|
|
5208
|
-
|
|
5209
|
-
|
|
5834
|
+
fs13.mkdirSync(configDir(), { recursive: true, mode: 448 });
|
|
5835
|
+
fs13.writeFileSync(updateStatePath(), `${JSON.stringify(state, null, 2)}
|
|
5210
5836
|
`, { mode: 384 });
|
|
5211
5837
|
return true;
|
|
5212
5838
|
} catch {
|
|
@@ -5215,75 +5841,8 @@ function writeUpdateState(state) {
|
|
|
5215
5841
|
}
|
|
5216
5842
|
function clearUpdateState() {
|
|
5217
5843
|
try {
|
|
5218
|
-
|
|
5219
|
-
} catch {
|
|
5220
|
-
}
|
|
5221
|
-
}
|
|
5222
|
-
|
|
5223
|
-
// src/update/install.ts
|
|
5224
|
-
import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
|
|
5225
|
-
import fs8 from "node:fs";
|
|
5226
|
-
import path10 from "node:path";
|
|
5227
|
-
var INSTALL_TIMEOUT_MS = 5 * 6e4;
|
|
5228
|
-
var STDERR_KEEP = 400;
|
|
5229
|
-
function npmPresent() {
|
|
5230
|
-
const probe2 = process.platform === "win32" ? "where" : "which";
|
|
5231
|
-
try {
|
|
5232
|
-
return spawnSync2(probe2, ["npm"], { encoding: "utf8" }).status === 0;
|
|
5233
|
-
} catch {
|
|
5234
|
-
return false;
|
|
5235
|
-
}
|
|
5236
|
-
}
|
|
5237
|
-
function installArgs(target, cliPath2) {
|
|
5238
|
-
const args = ["install", "--global", "--no-fund", "--no-audit", `${PACKAGE_NAME}@${target}`];
|
|
5239
|
-
const prefix = globalPrefixFrom(cliPath2);
|
|
5240
|
-
if (prefix) args.push("--prefix", prefix);
|
|
5241
|
-
return args;
|
|
5242
|
-
}
|
|
5243
|
-
async function installGlobal(target, cliPath2, options = {}) {
|
|
5244
|
-
const args = installArgs(target, cliPath2);
|
|
5245
|
-
return new Promise((resolve) => {
|
|
5246
|
-
let child;
|
|
5247
|
-
try {
|
|
5248
|
-
child = spawn6("npm", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
5249
|
-
} catch (e) {
|
|
5250
|
-
resolve({ ok: false, detail: e instanceof Error ? e.message : String(e) });
|
|
5251
|
-
return;
|
|
5252
|
-
}
|
|
5253
|
-
let stderr = "";
|
|
5254
|
-
child.stderr?.on("data", (chunk) => {
|
|
5255
|
-
stderr = `${stderr}${chunk.toString()}`.slice(-STDERR_KEEP);
|
|
5256
|
-
});
|
|
5257
|
-
let settled = false;
|
|
5258
|
-
const finish = (result) => {
|
|
5259
|
-
if (settled) return;
|
|
5260
|
-
settled = true;
|
|
5261
|
-
clearTimeout(timer);
|
|
5262
|
-
resolve(result);
|
|
5263
|
-
};
|
|
5264
|
-
const timeoutMs = options.timeoutMs ?? INSTALL_TIMEOUT_MS;
|
|
5265
|
-
const timer = setTimeout(() => {
|
|
5266
|
-
child.kill("SIGTERM");
|
|
5267
|
-
setTimeout(() => child.kill("SIGKILL"), 5e3).unref();
|
|
5268
|
-
finish({ ok: false, detail: `npm install timed out after ${timeoutMs / 6e4} minutes.` });
|
|
5269
|
-
}, timeoutMs);
|
|
5270
|
-
timer.unref();
|
|
5271
|
-
child.on("error", (e) => finish({ ok: false, detail: e.message }));
|
|
5272
|
-
child.on(
|
|
5273
|
-
"close",
|
|
5274
|
-
(code) => finish({ ok: code === 0, detail: stderr.trim() || `npm exited ${code}` })
|
|
5275
|
-
);
|
|
5276
|
-
});
|
|
5277
|
-
}
|
|
5278
|
-
function installedVersionAt(cliPath2) {
|
|
5279
|
-
try {
|
|
5280
|
-
const pkg = JSON.parse(
|
|
5281
|
-
fs8.readFileSync(path10.join(packageRootFrom(cliPath2), "package.json"), "utf8")
|
|
5282
|
-
);
|
|
5283
|
-
const version = pkg?.version;
|
|
5284
|
-
return typeof version === "string" ? version : null;
|
|
5844
|
+
fs13.rmSync(updateStatePath(), { force: true });
|
|
5285
5845
|
} catch {
|
|
5286
|
-
return null;
|
|
5287
5846
|
}
|
|
5288
5847
|
}
|
|
5289
5848
|
|
|
@@ -5406,7 +5965,12 @@ async function startDaemon() {
|
|
|
5406
5965
|
// published: `hasOnly()` fails the whole write for one unlisted key, which would
|
|
5407
5966
|
// take every heartbeat down, not just the badge.
|
|
5408
5967
|
updateAvailable: updateAvailableVersion,
|
|
5409
|
-
updatePhase
|
|
5968
|
+
updatePhase,
|
|
5969
|
+
// §15.82, and the same rule the paragraph above states: `engineTools` must be in
|
|
5970
|
+
// `liveFields()` in the DEPLOYED rules before this version is published. It shipped
|
|
5971
|
+
// in its own change ahead of this one for exactly that reason. `null` when the
|
|
5972
|
+
// machine has nothing to say, so a badge clears itself.
|
|
5973
|
+
engineTools: engineToolStates.size > 0 ? [...engineToolStates.values()] : null
|
|
5410
5974
|
},
|
|
5411
5975
|
{ merge: true }
|
|
5412
5976
|
);
|
|
@@ -5578,8 +6142,8 @@ async function startDaemon() {
|
|
|
5578
6142
|
held.delete(id);
|
|
5579
6143
|
loginsInFlight.get(id)?.abort();
|
|
5580
6144
|
for (const dir of orphanHomes(configDir(), shipId, new Set(list.map((c) => c.id)))) {
|
|
5581
|
-
if (!dir.endsWith(`${
|
|
5582
|
-
const engineId =
|
|
6145
|
+
if (!dir.endsWith(`${path15.sep}${id}`)) continue;
|
|
6146
|
+
const engineId = path15.basename(path15.dirname(path15.dirname(dir)));
|
|
5583
6147
|
const driver = getDriver(engineId);
|
|
5584
6148
|
if (!driver.login) continue;
|
|
5585
6149
|
void logoutAndRemove({ driver: driver.login, bin: driver.binary(), home: dir, log: log2 }).then(
|
|
@@ -5589,7 +6153,7 @@ async function startDaemon() {
|
|
|
5589
6153
|
}
|
|
5590
6154
|
if (!heldCredentialIds.has(shipId)) {
|
|
5591
6155
|
for (const dir of orphanHomes(configDir(), shipId, new Set(list.map((c) => c.id)))) {
|
|
5592
|
-
const engineId =
|
|
6156
|
+
const engineId = path15.basename(path15.dirname(path15.dirname(dir)));
|
|
5593
6157
|
const driver = getDriver(engineId);
|
|
5594
6158
|
if (!driver.login) continue;
|
|
5595
6159
|
void logoutAndRemove({ driver: driver.login, bin: driver.binary(), home: dir, log: log2 }).then(
|
|
@@ -5623,7 +6187,9 @@ async function startDaemon() {
|
|
|
5623
6187
|
credential,
|
|
5624
6188
|
root: configDir(),
|
|
5625
6189
|
signal: abort.signal,
|
|
5626
|
-
log: log2
|
|
6190
|
+
log: log2,
|
|
6191
|
+
// §15.82 — a captain asking a machine to sign in is also asking it to have the CLI.
|
|
6192
|
+
ensureEngine: (engineId) => ensureEngineForCaller(engineId)
|
|
5627
6193
|
}).catch((e) => log2(`Sign-in for ${credential.id} ended abnormally: ${e instanceof Error ? e.message : e}`)).finally(() => loginsInFlight.delete(credential.id));
|
|
5628
6194
|
}
|
|
5629
6195
|
poke();
|
|
@@ -5678,12 +6244,12 @@ async function startDaemon() {
|
|
|
5678
6244
|
heldCredentialIds.delete(shipId);
|
|
5679
6245
|
for (const engineId of (() => {
|
|
5680
6246
|
try {
|
|
5681
|
-
return
|
|
6247
|
+
return fs14.readdirSync(path15.join(configDir(), "engines"));
|
|
5682
6248
|
} catch {
|
|
5683
6249
|
return [];
|
|
5684
6250
|
}
|
|
5685
6251
|
})()) {
|
|
5686
|
-
|
|
6252
|
+
fs14.rmSync(path15.join(configDir(), "engines", engineId, shipId), { recursive: true, force: true });
|
|
5687
6253
|
}
|
|
5688
6254
|
approved.delete(shipId);
|
|
5689
6255
|
warnedUnapproved.delete(shipId);
|
|
@@ -5722,6 +6288,11 @@ async function startDaemon() {
|
|
|
5722
6288
|
DEFAULT_ENGINE_ID
|
|
5723
6289
|
);
|
|
5724
6290
|
};
|
|
6291
|
+
function engineUsable(engineId) {
|
|
6292
|
+
const state = engineToolStates.get(engineId);
|
|
6293
|
+
if (!state) return true;
|
|
6294
|
+
return state.state !== "installing";
|
|
6295
|
+
}
|
|
5725
6296
|
let dispatching = false;
|
|
5726
6297
|
let pokeRequested = false;
|
|
5727
6298
|
let shuttingDown = false;
|
|
@@ -5730,6 +6301,10 @@ async function startDaemon() {
|
|
|
5730
6301
|
let nextUpdateCheckAt = Date.now() + UPDATE_FIRST_CHECK_MS + Math.floor(Math.random() * UPDATE_JITTER_MS);
|
|
5731
6302
|
let updateAvailableVersion = null;
|
|
5732
6303
|
let updatePhase = null;
|
|
6304
|
+
const engineToolStates = /* @__PURE__ */ new Map();
|
|
6305
|
+
let checkingEngines = false;
|
|
6306
|
+
let nextEngineCheckAt = Date.now() + ENGINE_FIRST_CHECK_MS + Math.floor(Math.random() * UPDATE_JITTER_MS);
|
|
6307
|
+
const loggedEngineRefusals = /* @__PURE__ */ new Set();
|
|
5733
6308
|
function poke() {
|
|
5734
6309
|
if (dispatching || shuttingDown || updating) {
|
|
5735
6310
|
pokeRequested = !shuttingDown && !updating;
|
|
@@ -5798,7 +6373,13 @@ async function startDaemon() {
|
|
|
5798
6373
|
// `pending`, which is what lets a reset resume them with only a poke.
|
|
5799
6374
|
eligible: (p) => approved.get(p.shipId) === true && !isLimited(engineLimits, p.shipId, engineForJob(p.shipId, p.job), now) && // §15.81: a job on a machine-held credential is that machine's job. Skipped entries
|
|
5800
6375
|
// STAY in `pending`; the credentials listener pokes when the sign-in lands.
|
|
5801
|
-
credentialVerdict(p.shipId, p.job) === "ok" && //
|
|
6376
|
+
credentialVerdict(p.shipId, p.job) === "ok" && // §15.82: a machine that cannot run this engine should not take work it will fail.
|
|
6377
|
+
// Skipped entries STAY in `pending` and the engine check pokes when an install lands
|
|
6378
|
+
// — but only while the install is still going to happen. Once it is refused for good
|
|
6379
|
+
// the job IS claimed and fails with the driver's own message, because a job that never
|
|
6380
|
+
// runs and never fails is invisible, while a failed one at least names the missing CLI
|
|
6381
|
+
// (`E10`, `A21`).
|
|
6382
|
+
engineUsable(engineForJob(p.shipId, p.job)) && // The connection cooldown. Same "skip, don't stop" shape as the limit gate above, and
|
|
5802
6383
|
// skipped entries STAY in `pending` for the same reason — `sweepBackoff` resumes them
|
|
5803
6384
|
// with a poke. Without this the `transient` release below re-claims what it just put
|
|
5804
6385
|
// down, every three seconds, forever (jobs/claimBackoff.ts).
|
|
@@ -6087,7 +6668,7 @@ async function startDaemon() {
|
|
|
6087
6668
|
// `mcpSecretsToRedact` for why one of them is not enough.
|
|
6088
6669
|
...mcpSecretsToRedact(extraMcpServers)
|
|
6089
6670
|
];
|
|
6090
|
-
const
|
|
6671
|
+
const runSession3 = () => getDriver(engineId).run({
|
|
6091
6672
|
prompt,
|
|
6092
6673
|
agent,
|
|
6093
6674
|
job,
|
|
@@ -6105,6 +6686,21 @@ async function startDaemon() {
|
|
|
6105
6686
|
// swallows everything.
|
|
6106
6687
|
onStep: (step2) => progress.push(step2)
|
|
6107
6688
|
});
|
|
6689
|
+
let session = await runSession3();
|
|
6690
|
+
if (session.engineFault && !slot.abort.signal.aborted) {
|
|
6691
|
+
const rolledBackTo = await rollBackEngine(engineId, session.engineFault, log2);
|
|
6692
|
+
if (rolledBackTo) {
|
|
6693
|
+
log2(`Re-running this job on ${engineId} ${rolledBackTo.goodVersion} to find out whether ${rolledBackTo.badVersion} was the cause.`);
|
|
6694
|
+
const second = await runSession3();
|
|
6695
|
+
if (second.ok) {
|
|
6696
|
+
log2(`It was: ${engineId} ${rolledBackTo.badVersion} is broken, and this machine is reporting it.`);
|
|
6697
|
+
void reportBrokenEngineVersion(shipId, engineId, rolledBackTo, job.id);
|
|
6698
|
+
} else {
|
|
6699
|
+
log2(`It was not the version \u2014 ${engineId} ${rolledBackTo.badVersion} is left alone.`);
|
|
6700
|
+
}
|
|
6701
|
+
session = second;
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6108
6704
|
transcript = redactTranscript(
|
|
6109
6705
|
session.transcript,
|
|
6110
6706
|
knownSecrets,
|
|
@@ -6303,6 +6899,7 @@ async function startDaemon() {
|
|
|
6303
6899
|
void heartbeat();
|
|
6304
6900
|
sweepLimits();
|
|
6305
6901
|
void maybeCheckForUpdate();
|
|
6902
|
+
void maybeCheckEngines();
|
|
6306
6903
|
}, HEARTBEAT_MS);
|
|
6307
6904
|
let stopping = false;
|
|
6308
6905
|
const shutdown = async (reason) => {
|
|
@@ -6403,6 +7000,104 @@ async function startDaemon() {
|
|
|
6403
7000
|
nextUpdateCheckAt = Date.now() + checkIntervalMs(config2);
|
|
6404
7001
|
}
|
|
6405
7002
|
}
|
|
7003
|
+
async function ensureEngineForCaller(engineId) {
|
|
7004
|
+
const shipIds = [...byShip.keys()];
|
|
7005
|
+
const policySnap = shipIds.length ? await getDoc8(doc9(sess(shipIds[0]).fb.db, COLLECTIONS.crewConfig, CONFIG_DOCS.engines)).catch(() => null) : null;
|
|
7006
|
+
const result = await ensureEngineReady(engineId, {
|
|
7007
|
+
root: configDir(),
|
|
7008
|
+
runnerBin: RUNNER_BIN,
|
|
7009
|
+
autoInstall: engineInstallAllowed(config2),
|
|
7010
|
+
policyDoc: policySnap?.exists() ? policySnap.data() : null,
|
|
7011
|
+
log: (line) => log2(line),
|
|
7012
|
+
onState: (state) => {
|
|
7013
|
+
engineToolStates.set(state.engineId, state);
|
|
7014
|
+
void heartbeat();
|
|
7015
|
+
}
|
|
7016
|
+
});
|
|
7017
|
+
void maybeCheckEnginesSoon();
|
|
7018
|
+
return result;
|
|
7019
|
+
}
|
|
7020
|
+
function maybeCheckEnginesSoon() {
|
|
7021
|
+
nextEngineCheckAt = 0;
|
|
7022
|
+
}
|
|
7023
|
+
async function rollBackEngine(engineId, fault, say2) {
|
|
7024
|
+
const root = configDir();
|
|
7025
|
+
const marker = readEngineMarker(root, engineId);
|
|
7026
|
+
if (!marker || !managedEngineBin(engineId, { root })) return null;
|
|
7027
|
+
const shipIds = [...byShip.keys()];
|
|
7028
|
+
const policySnap = shipIds.length ? await getDoc8(doc9(sess(shipIds[0]).fb.db, COLLECTIONS.crewConfig, CONFIG_DOCS.engines)).catch(() => null) : null;
|
|
7029
|
+
const policy = resolveEnginePolicy(engineId, policySnap?.exists() ? policySnap.data() : null);
|
|
7030
|
+
if (!policy.knownGood || policy.knownGood === marker.version) return null;
|
|
7031
|
+
say2(`A ${engineId} session failed before the model did anything: ${fault}`);
|
|
7032
|
+
say2(`Putting ${engineId} ${policy.knownGood} back to check whether ${marker.version} is at fault.`);
|
|
7033
|
+
const outcome = await installEngine({
|
|
7034
|
+
root,
|
|
7035
|
+
engineId,
|
|
7036
|
+
version: policy.knownGood,
|
|
7037
|
+
healthCheck: (bin) => getDriver(engineId).healthCheck(bin),
|
|
7038
|
+
log: say2
|
|
7039
|
+
});
|
|
7040
|
+
if (!outcome.ok) {
|
|
7041
|
+
say2(`Could not put ${engineId} ${policy.knownGood} back: ${outcome.detail.slice(0, 200)}`);
|
|
7042
|
+
return null;
|
|
7043
|
+
}
|
|
7044
|
+
maybeCheckEnginesSoon();
|
|
7045
|
+
return { badVersion: marker.version, goodVersion: policy.knownGood };
|
|
7046
|
+
}
|
|
7047
|
+
async function reportBrokenEngineVersion(shipId, engineId, versions, jobId) {
|
|
7048
|
+
try {
|
|
7049
|
+
const idToken = await sess(shipId).user.getIdToken();
|
|
7050
|
+
await callFunction(functionsBaseUrl(config2), idToken, "crewShips", "reportEngineVersion", {
|
|
7051
|
+
shipId,
|
|
7052
|
+
engineId,
|
|
7053
|
+
badVersion: versions.badVersion,
|
|
7054
|
+
goodVersion: versions.goodVersion,
|
|
7055
|
+
jobId
|
|
7056
|
+
});
|
|
7057
|
+
} catch (e) {
|
|
7058
|
+
log2(`Could not report ${engineId} ${versions.badVersion} as broken: ${e instanceof Error ? e.message : e}`);
|
|
7059
|
+
}
|
|
7060
|
+
}
|
|
7061
|
+
async function maybeCheckEngines() {
|
|
7062
|
+
if (checkingEngines || shuttingDown || Date.now() < nextEngineCheckAt) return;
|
|
7063
|
+
const shipIds = [...byShip.keys()];
|
|
7064
|
+
if (shipIds.length === 0) return;
|
|
7065
|
+
checkingEngines = true;
|
|
7066
|
+
try {
|
|
7067
|
+
const engineIds = engineSet(agentEngines.values());
|
|
7068
|
+
const firstShip = shipIds[0];
|
|
7069
|
+
const policySnap = await getDoc8(
|
|
7070
|
+
doc9(sess(firstShip).fb.db, COLLECTIONS.crewConfig, CONFIG_DOCS.engines)
|
|
7071
|
+
).catch(() => null);
|
|
7072
|
+
const states = await refreshEngineTools({
|
|
7073
|
+
engineIds,
|
|
7074
|
+
root: configDir(),
|
|
7075
|
+
runnerBin: RUNNER_BIN,
|
|
7076
|
+
autoInstall: engineInstallAllowed(config2),
|
|
7077
|
+
policyDoc: policySnap?.exists() ? policySnap.data() : null,
|
|
7078
|
+
log: (line) => log2(line),
|
|
7079
|
+
onState: (state) => {
|
|
7080
|
+
engineToolStates.set(state.engineId, state);
|
|
7081
|
+
void heartbeat();
|
|
7082
|
+
}
|
|
7083
|
+
});
|
|
7084
|
+
for (const state of states) {
|
|
7085
|
+
const key = `${state.engineId}:${state.state}:${state.detail ?? ""}`;
|
|
7086
|
+
if ((state.state === "missing" || state.state === "failed") && !loggedEngineRefusals.has(key)) {
|
|
7087
|
+
loggedEngineRefusals.add(key);
|
|
7088
|
+
log2(`Engine "${state.engineId}": ${state.detail ?? "not installed on this machine"}`);
|
|
7089
|
+
}
|
|
7090
|
+
if (state.state === "ready") loggedEngineRefusals.delete(key);
|
|
7091
|
+
engineToolStates.set(state.engineId, state);
|
|
7092
|
+
}
|
|
7093
|
+
poke();
|
|
7094
|
+
} catch (e) {
|
|
7095
|
+
log2(`Engine check failed: ${e instanceof Error ? e.message : e}`);
|
|
7096
|
+
} finally {
|
|
7097
|
+
checkingEngines = false;
|
|
7098
|
+
nextEngineCheckAt = Date.now() + ENGINE_CHECK_MS;
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
6406
7101
|
function beginUpdate(target) {
|
|
6407
7102
|
updating = { target };
|
|
6408
7103
|
updateAvailableVersion = target;
|
|
@@ -6629,7 +7324,8 @@ function glyph(level) {
|
|
|
6629
7324
|
var TOGGLES = {
|
|
6630
7325
|
notifications: "Desktop notifications when a job starts, finishes or fails (off by default)",
|
|
6631
7326
|
keepAwake: "Keep this machine awake while a job is running",
|
|
6632
|
-
autoUpdate: "Install new versions of the runner by itself and restart (PRD \xA715.37)"
|
|
7327
|
+
autoUpdate: "Install new versions of the runner by itself and restart (PRD \xA715.37)",
|
|
7328
|
+
autoInstallEngines: "Install the engine CLIs this machine's crews need \u2014 a few hundred MB each (PRD \xA715.82)"
|
|
6633
7329
|
};
|
|
6634
7330
|
var PARALLEL = "parallel";
|
|
6635
7331
|
var PARALLEL_HELP = "How many jobs to run at once (1 = one at a time, as before)";
|
|
@@ -6821,8 +7517,8 @@ function setParallel(config2, value, ship2) {
|
|
|
6821
7517
|
|
|
6822
7518
|
// src/cli/commands/doctor.ts
|
|
6823
7519
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
6824
|
-
import
|
|
6825
|
-
import
|
|
7520
|
+
import fs15 from "node:fs";
|
|
7521
|
+
import path16 from "node:path";
|
|
6826
7522
|
import { collection as collection9, doc as doc10, getDoc as getDoc9, getDocs as getDocs8 } from "firebase/firestore";
|
|
6827
7523
|
|
|
6828
7524
|
// src/cli/session.ts
|
|
@@ -6898,7 +7594,7 @@ function serviceBinaryCheckFrom(input) {
|
|
|
6898
7594
|
if (status.state === "not-installed" || status.state === "unsupported") return null;
|
|
6899
7595
|
const pathValue = status.unitEnv?.PATH;
|
|
6900
7596
|
if (pathValue === void 0) return null;
|
|
6901
|
-
const relative = [...new Set(binaries)].filter((b) => !
|
|
7597
|
+
const relative = [...new Set(binaries)].filter((b) => !path16.isAbsolute(b));
|
|
6902
7598
|
if (relative.length === 0) return null;
|
|
6903
7599
|
const missing = relative.filter((b) => !resolves(b, pathValue));
|
|
6904
7600
|
const id = "service:path";
|
|
@@ -6914,10 +7610,10 @@ function serviceBinaryCheckFrom(input) {
|
|
|
6914
7610
|
);
|
|
6915
7611
|
}
|
|
6916
7612
|
function resolvesOnPath(binary, pathValue) {
|
|
6917
|
-
for (const dir of pathValue.split(
|
|
7613
|
+
for (const dir of pathValue.split(path16.delimiter)) {
|
|
6918
7614
|
if (!dir) continue;
|
|
6919
7615
|
try {
|
|
6920
|
-
|
|
7616
|
+
fs15.accessSync(path16.join(dir, binary), fs15.constants.X_OK);
|
|
6921
7617
|
return true;
|
|
6922
7618
|
} catch {
|
|
6923
7619
|
}
|
|
@@ -7037,8 +7733,7 @@ async function checkShips(config2) {
|
|
|
7037
7733
|
agents = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
7038
7734
|
} catch {
|
|
7039
7735
|
}
|
|
7040
|
-
const shipEngines =
|
|
7041
|
-
if (shipEngines.size === 0) shipEngines.add(DEFAULT_ENGINE_ID);
|
|
7736
|
+
const shipEngines = enginesForAgents(agents);
|
|
7042
7737
|
for (const id of shipEngines) engines.add(id);
|
|
7043
7738
|
if (agents.some((agent) => effectiveAgentTools(agent).github.enabled)) needsGithub = true;
|
|
7044
7739
|
try {
|
|
@@ -7173,9 +7868,20 @@ async function runDoctor() {
|
|
|
7173
7868
|
for (const engineId of engines) {
|
|
7174
7869
|
const health = await getDriver(engineId).healthCheck();
|
|
7175
7870
|
if (health.binary) jobBinaries.push(health.binary);
|
|
7871
|
+
const managed = managedEngineBin(engineId, {});
|
|
7872
|
+
const version = managed ? readEngineMarker(configDir(), engineId)?.version : null;
|
|
7873
|
+
const origin = managed ? ` \u2014 installed by this runner${version ? ` (${version})` : ""}` : health.binary && health.binary !== engineId ? " \u2014 from CREW_*_BIN" : " \u2014 already on this machine";
|
|
7176
7874
|
checks.push(
|
|
7177
|
-
health.ok ? ok(`engine:${engineId}`, `Engine "${engineId}"`, health.detail) : fail(
|
|
7875
|
+
health.ok ? ok(`engine:${engineId}`, `Engine "${engineId}"`, `${health.detail}${origin}`) : fail(
|
|
7876
|
+
`engine:${engineId}`,
|
|
7877
|
+
`Engine "${engineId}"`,
|
|
7878
|
+
health.detail,
|
|
7879
|
+
// A way forward the operator can type, ahead of the vendor's download page: this
|
|
7880
|
+
// runner installs it itself, and the old message never said so.
|
|
7881
|
+
enginePackageFor(engineId) ? `Run \`${RUNNER_BIN} engine install ${engineId}\` (a few hundred MB), or ${health.fix ?? "install the CLI yourself"}` : health.fix
|
|
7882
|
+
)
|
|
7178
7883
|
);
|
|
7884
|
+
if (managed) jobBinaries.push("node");
|
|
7179
7885
|
}
|
|
7180
7886
|
if (needsGithub) {
|
|
7181
7887
|
for (const binary of ["git", "gh"]) {
|
|
@@ -7231,8 +7937,142 @@ function report2(checks) {
|
|
|
7231
7937
|
return failed.length === 0 ? 0 : 1;
|
|
7232
7938
|
}
|
|
7233
7939
|
|
|
7940
|
+
// src/cli/commands/engine.ts
|
|
7941
|
+
import fs16 from "node:fs";
|
|
7942
|
+
function originOf(engineId) {
|
|
7943
|
+
const override = process.env[`CREW_${engineId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_BIN`];
|
|
7944
|
+
if (override) return { origin: "override", bin: override, version: null };
|
|
7945
|
+
const managed = managedEngineBin(engineId, {});
|
|
7946
|
+
if (managed) return { origin: "runner", bin: managed, version: readEngineMarker(configDir(), engineId)?.version ?? null };
|
|
7947
|
+
return { origin: "none", bin: null, version: null };
|
|
7948
|
+
}
|
|
7949
|
+
async function runEngineList() {
|
|
7950
|
+
const root = configDir();
|
|
7951
|
+
const state = readEngineState(root);
|
|
7952
|
+
const rows = [];
|
|
7953
|
+
for (const engineId of installableEngineIds()) {
|
|
7954
|
+
const pkg = enginePackageFor(engineId);
|
|
7955
|
+
const { origin, bin, version } = originOf(engineId);
|
|
7956
|
+
const health = await getDriver(engineId).healthCheck();
|
|
7957
|
+
const record = state[engineId];
|
|
7958
|
+
rows.push({
|
|
7959
|
+
engine: engineId,
|
|
7960
|
+
label: getEngine(engineId)?.label ?? engineId,
|
|
7961
|
+
package: pkg?.npmPackage ?? null,
|
|
7962
|
+
shipped: DEFAULT_ENGINE_POLICIES[engineId]?.knownGood ?? null,
|
|
7963
|
+
installed: version,
|
|
7964
|
+
origin: origin === "none" && health.ok ? "path" : origin,
|
|
7965
|
+
binary: bin ?? health.binary ?? null,
|
|
7966
|
+
ok: health.ok,
|
|
7967
|
+
detail: health.ok ? void 0 : health.detail,
|
|
7968
|
+
lastAttempt: record ? { target: record.target, outcome: record.outcome, attempts: record.attempts, detail: record.detail } : null,
|
|
7969
|
+
diskBytes: dirSize(enginePrefixDir(root, engineId))
|
|
7970
|
+
});
|
|
7971
|
+
}
|
|
7972
|
+
if (isJson()) {
|
|
7973
|
+
emitJson({ autoInstall: engineInstallAllowed(loadConfig()), engines: rows });
|
|
7974
|
+
return 0;
|
|
7975
|
+
}
|
|
7976
|
+
say.line(`Engine CLIs (${engineInstallAllowed(loadConfig()) ? "installed automatically" : "automatic installs are OFF"})`);
|
|
7977
|
+
for (const row of rows) {
|
|
7978
|
+
const where6 = row.origin === "runner" ? "installed by this runner" : row.origin === "override" ? "from CREW_*_BIN" : row.origin === "path" ? "already on this machine \u2014 this runner cannot update or roll it back" : "not installed";
|
|
7979
|
+
say.line("");
|
|
7980
|
+
say.line(` ${row.label} (${row.engine})`);
|
|
7981
|
+
say.line(` ${row.ok ? "\u2713" : "\u2717"} ${row.binary ?? row.package ?? row.engine} \u2014 ${where6}`);
|
|
7982
|
+
if (row.installed) say.line(` version ${row.installed}${row.diskBytes ? ` \xB7 ${mb(row.diskBytes)}` : ""}`);
|
|
7983
|
+
if (!row.ok && row.detail) say.line(` ${row.detail}`);
|
|
7984
|
+
if (row.lastAttempt?.outcome === "failed" && row.lastAttempt.detail) {
|
|
7985
|
+
say.line(` last attempt (${row.lastAttempt.target}, ${row.lastAttempt.attempts}x): ${row.lastAttempt.detail}`);
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
say.line("");
|
|
7989
|
+
say.line(` A CLI is a few hundred MB. \`${RUNNER_BIN} config set autoInstallEngines off\` declines them.`);
|
|
7990
|
+
return 0;
|
|
7991
|
+
}
|
|
7992
|
+
async function runEngineInstall(engineIds, options = {}) {
|
|
7993
|
+
const root = configDir();
|
|
7994
|
+
const wanted = engineIds.length > 0 ? engineIds : installableEngineIds();
|
|
7995
|
+
const results = [];
|
|
7996
|
+
for (const engineId of wanted) {
|
|
7997
|
+
const pkg = enginePackageFor(engineId);
|
|
7998
|
+
if (!pkg) throw new CliError(`This runner has no installer for "${engineId}". Known: ${installableEngineIds().join(", ")}.`);
|
|
7999
|
+
const version = DEFAULT_ENGINE_POLICIES[engineId]?.knownGood;
|
|
8000
|
+
if (!version) throw new CliError(`This runner has no version recorded for "${engineId}".`);
|
|
8001
|
+
const marker = readEngineMarker(root, engineId);
|
|
8002
|
+
const already = Boolean(marker && marker.npmPackage === pkg.npmPackage && marker.version === version && managedEngineBin(engineId, {}));
|
|
8003
|
+
if (options.check || options.dryRun) {
|
|
8004
|
+
results.push({
|
|
8005
|
+
engine: engineId,
|
|
8006
|
+
package: pkg.npmPackage,
|
|
8007
|
+
version,
|
|
8008
|
+
installed: false,
|
|
8009
|
+
alreadyInstalled: already,
|
|
8010
|
+
command: `npm ${engineInstallArgs(pkg, version, enginePrefixDir(root, engineId)).join(" ")}`
|
|
8011
|
+
});
|
|
8012
|
+
continue;
|
|
8013
|
+
}
|
|
8014
|
+
if (already && !options.force) {
|
|
8015
|
+
say.info(`${engineId} is already at ${version} \u2014 pass --force to reinstall.`);
|
|
8016
|
+
results.push({ engine: engineId, package: pkg.npmPackage, version, installed: false, alreadyInstalled: true });
|
|
8017
|
+
continue;
|
|
8018
|
+
}
|
|
8019
|
+
const outcome = await installEngine({
|
|
8020
|
+
root,
|
|
8021
|
+
engineId,
|
|
8022
|
+
version,
|
|
8023
|
+
healthCheck: (bin) => getDriver(engineId).healthCheck(bin),
|
|
8024
|
+
// An install takes minutes, so it narrates — but `--json` promises that stdout is ONE
|
|
8025
|
+
// document, and a caller piping this into a parser must not have to strip progress out of
|
|
8026
|
+
// it. Under `--json` the same lines go to stderr, where a person still sees them.
|
|
8027
|
+
log: (line) => isJson() ? console.error(line) : say.line(` ${line}`)
|
|
8028
|
+
});
|
|
8029
|
+
if (!outcome.ok) throw new CliError(`Could not install ${pkg.npmPackage}@${version}: ${outcome.detail}`);
|
|
8030
|
+
results.push({ engine: engineId, package: pkg.npmPackage, version, installed: true, alreadyInstalled: false, binary: outcome.bin });
|
|
8031
|
+
}
|
|
8032
|
+
if (isJson()) {
|
|
8033
|
+
emitJson({ engines: results });
|
|
8034
|
+
return 0;
|
|
8035
|
+
}
|
|
8036
|
+
for (const r of results) {
|
|
8037
|
+
if (r.command) say.info(`Would run: ${r.command}`);
|
|
8038
|
+
else if (r.installed) say.line(`\u2713 ${r.engine} \u2014 ${r.package}@${r.version}`);
|
|
8039
|
+
}
|
|
8040
|
+
return 0;
|
|
8041
|
+
}
|
|
8042
|
+
async function runEngineUninstall(engineId) {
|
|
8043
|
+
if (!enginePackageFor(engineId)) throw new CliError(`This runner has no installer for "${engineId}".`);
|
|
8044
|
+
const dir = enginePrefixDir(configDir(), engineId);
|
|
8045
|
+
const existed = fs16.existsSync(dir);
|
|
8046
|
+
fs16.rmSync(dir, { recursive: true, force: true });
|
|
8047
|
+
if (isJson()) {
|
|
8048
|
+
emitJson({ engine: engineId, removed: existed, directory: dir });
|
|
8049
|
+
return 0;
|
|
8050
|
+
}
|
|
8051
|
+
say.line(existed ? `Removed this runner's copy of ${engineId} (${dir}).` : `This runner had no copy of ${engineId}.`);
|
|
8052
|
+
say.line("A copy you installed yourself is untouched.");
|
|
8053
|
+
return 0;
|
|
8054
|
+
}
|
|
8055
|
+
function dirSize(dir) {
|
|
8056
|
+
try {
|
|
8057
|
+
let total = 0;
|
|
8058
|
+
for (const entry of fs16.readdirSync(dir, { recursive: true, withFileTypes: true })) {
|
|
8059
|
+
if (!entry.isFile()) continue;
|
|
8060
|
+
try {
|
|
8061
|
+
total += fs16.statSync(`${entry.parentPath ?? entry.path}/${entry.name}`).size;
|
|
8062
|
+
} catch {
|
|
8063
|
+
}
|
|
8064
|
+
}
|
|
8065
|
+
return total || null;
|
|
8066
|
+
} catch {
|
|
8067
|
+
return null;
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
8070
|
+
function mb(bytes) {
|
|
8071
|
+
return `${Math.round(bytes / 1e6)} MB`;
|
|
8072
|
+
}
|
|
8073
|
+
|
|
7234
8074
|
// src/cli/commands/login.ts
|
|
7235
|
-
import { spawn as
|
|
8075
|
+
import { spawn as spawn8 } from "node:child_process";
|
|
7236
8076
|
import os6 from "node:os";
|
|
7237
8077
|
import { signInWithCustomToken as signInWithCustomToken2 } from "firebase/auth";
|
|
7238
8078
|
function buildLoginResult(approved, existing, fallbackProjectId, mcpUrl2) {
|
|
@@ -7274,7 +8114,7 @@ function buildLoginResult(approved, existing, fallbackProjectId, mcpUrl2) {
|
|
|
7274
8114
|
function openBrowser(url) {
|
|
7275
8115
|
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
7276
8116
|
try {
|
|
7277
|
-
const child =
|
|
8117
|
+
const child = spawn8(command, args, { stdio: "ignore", detached: true });
|
|
7278
8118
|
child.on("error", () => {
|
|
7279
8119
|
});
|
|
7280
8120
|
child.unref();
|
|
@@ -7542,20 +8382,20 @@ async function runServiceStatus() {
|
|
|
7542
8382
|
}
|
|
7543
8383
|
|
|
7544
8384
|
// src/cli/commands/uninstall.ts
|
|
7545
|
-
import
|
|
8385
|
+
import fs17 from "node:fs";
|
|
7546
8386
|
async function runUninstall(options) {
|
|
7547
8387
|
const before = serviceStatus();
|
|
7548
8388
|
const dir = configDir();
|
|
7549
8389
|
const hadService = before.state !== "not-installed" && before.state !== "unsupported";
|
|
7550
8390
|
if (hadService) uninstallService();
|
|
7551
8391
|
let purged = false;
|
|
7552
|
-
if (options.purge &&
|
|
8392
|
+
if (options.purge && fs17.existsSync(dir)) {
|
|
7553
8393
|
const confirmed = await promptConfirm({
|
|
7554
8394
|
message: `Delete ${dir}? This machine loses its identity \u2014 a captain has to approve it again after reinstalling.`,
|
|
7555
8395
|
initialValue: false
|
|
7556
8396
|
});
|
|
7557
8397
|
if (confirmed) {
|
|
7558
|
-
|
|
8398
|
+
fs17.rmSync(dir, { recursive: true, force: true });
|
|
7559
8399
|
purged = true;
|
|
7560
8400
|
}
|
|
7561
8401
|
}
|
|
@@ -8022,6 +8862,10 @@ program.command("start").description("Run the daemon in the foreground").action(
|
|
|
8022
8862
|
return 0;
|
|
8023
8863
|
})
|
|
8024
8864
|
);
|
|
8865
|
+
var engine = program.command("engine").description("The engine CLIs this machine runs agents on");
|
|
8866
|
+
engine.command("list").description("What each engine needs, what this machine has, and where it came from").action(action(runEngineList));
|
|
8867
|
+
engine.command("install [engineIds...]").description("Install an engine's CLI into this runner's own folder (a few hundred MB each)").option("--check", "report what would be installed, and install nothing").option("--dry-run", "print the npm command that would run").option("--force", "reinstall even when this runner already has that version").action(action((engineIds, options) => runEngineInstall(engineIds ?? [], options)));
|
|
8868
|
+
engine.command("uninstall <engineId>").description("Remove this runner's own copy \u2014 never one you installed yourself").action(action((engineId) => runEngineUninstall(engineId)));
|
|
8025
8869
|
program.command("doctor").description("Check whether this machine can run jobs").action(action(runDoctor));
|
|
8026
8870
|
program.command("status").description("Show what this machine is doing").action(action(runStatus));
|
|
8027
8871
|
program.command("update").description("Update this machine to the latest published version").option("--check", "report whether a newer version exists, and install nothing").option("--dry-run", "print the npm command that would run").action(action((options) => runUpdate(options)));
|