@proagentstore/cli 0.4.149 → 0.4.151

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/dist/index.js CHANGED
@@ -2,12 +2,19 @@
2
2
  import {
3
3
  BOOTSTRAP_ENV,
4
4
  CLI_PACKAGE,
5
+ badVersions,
6
+ beginTrial,
5
7
  installPayload,
6
8
  isVersion,
7
9
  latestPublishedRelease,
10
+ logUpdateStep,
11
+ markPayloadHealthy,
8
12
  olderThan,
9
- runNpm
10
- } from "./chunk-NNA7VZ6W.js";
13
+ payloadRoot,
14
+ runNpm,
15
+ takeRollbackNotice,
16
+ updateLogUpload
17
+ } from "./chunk-PKMXQJEJ.js";
11
18
 
12
19
  // src/index.ts
13
20
  import { createRequire as createRequire3 } from "module";
@@ -1361,6 +1368,7 @@ function deferredUpdateLeaseActive(lease, now = Date.now()) {
1361
1368
  return now < lease.expiresAt;
1362
1369
  }
1363
1370
  function leaveForRestart(restarter, deps = { spawn: spawn3, exit: (code) => process.exit(code) }) {
1371
+ logUpdateStep(`restart: leaving for the ${restarter} restarter (${restarter === "command" ? "restart command started, exit 0" : `exit ${RUNNER_RESTART_EXIT_CODE}`})`);
1364
1372
  if (restarter === "command") {
1365
1373
  deps.spawn(process.env[RESTART_COMMAND_ENV], { shell: true, detached: true, stdio: "ignore", env: process.env }).unref();
1366
1374
  deps.exit(0);
@@ -1379,6 +1387,8 @@ function planRunnerUpdate(f) {
1379
1387
  if (f.fromSource) return { action: "refused", current: f.current, reason: "This runner runs from a source checkout \u2014 update it with `git pull` there, not npm." };
1380
1388
  if (!f.latest) return { action: "refused", current: f.current, reason: `npm could not be asked for the latest ${CLI_PACKAGE} from this machine${f.latestError ? `: ${f.latestError}` : "."}`, retryable: true };
1381
1389
  if (!olderThan(f.current, f.latest)) return { action: "up-to-date", current: f.current };
1390
+ const rolledBack = f.rolledBack?.[f.latest];
1391
+ if (rolledBack) return { action: "refused", current: f.current, reason: `${f.latest} was installed here and rolled back: ${rolledBack}. This machine stays on ${f.current} until a newer release is published.`, retryable: true };
1382
1392
  if (!f.restarter) {
1383
1393
  return {
1384
1394
  action: "refused",
@@ -1411,17 +1421,24 @@ function planRunnerRestart(f) {
1411
1421
  }
1412
1422
  return { action: "restart", current: f.current, restarter: f.restarter };
1413
1423
  }
1414
- async function installVersion(version2) {
1415
- if (process.env[BOOTSTRAP_ENV]) {
1416
- await installPayload(version2);
1417
- return;
1418
- }
1424
+ async function installVersion(version2, current = null) {
1425
+ const bootstrapped = !!process.env[BOOTSTRAP_ENV];
1426
+ logUpdateStep(`install: ${current ?? "?"} \u2192 ${version2} starting (${bootstrapped ? "payload cache" : "npm i -g"})`);
1419
1427
  try {
1420
- await runNpm(["i", "-g", `${CLI_PACKAGE}@${version2}`], { timeout: 5 * 6e4 });
1428
+ if (bootstrapped) {
1429
+ await installPayload(version2);
1430
+ beginTrial(payloadRoot(), version2, current);
1431
+ } else {
1432
+ await runNpm(["i", "-g", `${CLI_PACKAGE}@${version2}`], { timeout: 5 * 6e4 }).catch((e) => {
1433
+ const stderr = String(e.stderr ?? "").trim();
1434
+ throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
1435
+ });
1436
+ }
1421
1437
  } catch (e) {
1422
- const stderr = String(e.stderr ?? "").trim();
1423
- throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
1438
+ logUpdateStep(`install: ${version2} FAILED \u2014 ${e instanceof Error ? e.message : String(e)}`);
1439
+ throw e;
1424
1440
  }
1441
+ logUpdateStep(`install: ${version2} installed`);
1425
1442
  }
1426
1443
 
1427
1444
  // src/commands/runner/membership.ts
@@ -1776,13 +1793,15 @@ function parseAutoUpdatePolicy(value) {
1776
1793
  const lastAttemptAt = raw.lastAttemptAt ?? raw.last_attempt_at;
1777
1794
  const reason = raw.reason;
1778
1795
  const recovery = parseRecovery(raw.recovery);
1796
+ const fleet = raw.fleetUpdating ?? raw.fleet_updating;
1779
1797
  return {
1780
1798
  autoUpdate,
1781
1799
  ...status ? { status } : {},
1782
1800
  ...typeof latestVersion === "string" ? { latestVersion } : {},
1783
1801
  ...typeof lastAttemptAt === "string" ? { lastAttemptAt } : {},
1784
1802
  ...typeof reason === "string" ? { reason } : {},
1785
- ...recovery ? { recovery } : {}
1803
+ ...recovery ? { recovery } : {},
1804
+ ...typeof fleet === "number" && Number.isInteger(fleet) && fleet >= 0 ? { fleetUpdating: fleet } : {}
1786
1805
  };
1787
1806
  }
1788
1807
  function policyFromResponse(value) {
@@ -1843,7 +1862,9 @@ function acceptPolicyResponse(local, wasAuthoritative, response) {
1843
1862
  const { recovery: _fromService, ...remote } = parsed;
1844
1863
  const action = policyScheduleAction(local, remote, wasAuthoritative);
1845
1864
  const ownerTransition = action === "cancel" || !local.autoUpdate;
1846
- return { policy: ownerTransition ? { ...local, ...remote } : { ...local, autoUpdate: remote.autoUpdate }, action };
1865
+ const { fleetUpdating: _stale, ...base } = local;
1866
+ const fleet = remote.fleetUpdating === void 0 ? {} : { fleetUpdating: remote.fleetUpdating };
1867
+ return { policy: ownerTransition ? { ...base, ...remote, ...fleet } : { ...base, autoUpdate: remote.autoUpdate, ...fleet }, action };
1847
1868
  }
1848
1869
  var ResponseFence = class {
1849
1870
  sent = 0;
@@ -2068,6 +2089,12 @@ var AutomaticUpdateController = class {
2068
2089
  this.defer();
2069
2090
  return;
2070
2091
  }
2092
+ if (policy.fleetUpdating) {
2093
+ this.blockerClass = "fleet_stagger";
2094
+ this.deps.status("waiting-for-idle", { latestVersion: plan.latest, reason: `${policy.fleetUpdating} other machine(s) on this account are mid-update; this one updates after them.` });
2095
+ this.defer(plan.latest);
2096
+ return;
2097
+ }
2071
2098
  const target = open && isVersion(open.target) ? open.target : plan.latest;
2072
2099
  const attempt = { ...plan, latest: target };
2073
2100
  const recovery = open ?? { target, attachments: [...this.deps.attachments?.() ?? []], at: new Date(this.now()).toISOString() };
@@ -2077,7 +2104,7 @@ var AutomaticUpdateController = class {
2077
2104
  this.deps.status("restarting", { latestVersion: target, reason: void 0 });
2078
2105
  return;
2079
2106
  }
2080
- this.blockerClass = "idle_wait";
2107
+ this.blockerClass = this.deps.policy().value.fleetUpdating ? "fleet_stagger" : "idle_wait";
2081
2108
  this.defer(target);
2082
2109
  }
2083
2110
  defer(latest) {
@@ -2250,7 +2277,10 @@ function createPolicyChannel(deps) {
2250
2277
  if (action === "cancel") deps.onAction("cancel");
2251
2278
  else if (action === "start" && !deps.cell.refreshing) deps.onAction("start");
2252
2279
  };
2253
- const accept = (response, sentAs) => act(deps.cell.accept(response, sentAs));
2280
+ const accept = (response, sentAs) => {
2281
+ act(deps.cell.accept(response, sentAs));
2282
+ deps.onAnswer?.();
2283
+ };
2254
2284
  const register = async (id, force = deps.force, signal) => {
2255
2285
  const sent = deps.cell.next();
2256
2286
  try {
@@ -2300,7 +2330,7 @@ function createInstallBridge(deps) {
2300
2330
  if (automatic) {
2301
2331
  const fresh = await within((signal) => deps.refresh(signal));
2302
2332
  if (fresh === null) expired(false, "The owner policy refresh before installing");
2303
- if (!fresh || !deps.cell.policy.autoUpdate) {
2333
+ if (!fresh || !deps.cell.policy.autoUpdate || deps.cell.policy.fleetUpdating) {
2304
2334
  release(false);
2305
2335
  return false;
2306
2336
  }
@@ -2344,6 +2374,122 @@ function createInstallBridge(deps) {
2344
2374
  return { run, inFlight: () => inFlight, installedPendingRestart: () => installedPendingRestart };
2345
2375
  }
2346
2376
 
2377
+ // src/commands/runner/release-trial.ts
2378
+ function releaseTrial(version2, cell, log, env = process.env, root = payloadRoot) {
2379
+ if (!env[BOOTSTRAP_ENV]) return { rollbackReason: null, proven: () => void 0, rolledBack: () => ({}) };
2380
+ const notice = takeRollbackNotice(root());
2381
+ const rollbackReason = notice ? `Rolled back from ${notice.from} to ${version2}: ${notice.reason}.` : null;
2382
+ if (rollbackReason) {
2383
+ log(`auto-update: ${rollbackReason}`);
2384
+ cell.record("failure", { reason: rollbackReason, recovery: void 0 });
2385
+ }
2386
+ let done = false;
2387
+ return {
2388
+ rollbackReason,
2389
+ proven: () => {
2390
+ if (done) return;
2391
+ done = true;
2392
+ if (markPayloadHealthy(root(), version2)) log(`auto-update: ${version2} registered \u2014 it is kept.`);
2393
+ },
2394
+ rolledBack: () => badVersions(root())
2395
+ };
2396
+ }
2397
+
2398
+ // src/commands/up-restart.ts
2399
+ import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
2400
+ var UP_LAUNCHER_ENV = "PAGS_UP_LAUNCHER";
2401
+ var UP_LAUNCHER_PID_ENV = "PAGS_UP_LAUNCHER_PID";
2402
+ var RUNNER_DIED_EXIT_CODE = 76;
2403
+ function upRestartMode(env, parent = { pid: process.ppid, alive: pidAlive }) {
2404
+ if (env[SERVICE_ENV] === "1") return "service";
2405
+ if (env[RESTART_COMMAND_ENV]?.trim()) return "command";
2406
+ const launcher = Number(env[UP_LAUNCHER_PID_ENV]);
2407
+ if (env[UP_LAUNCHER_ENV] === "1" && launcher === parent.pid && parent.alive(launcher)) return "launcher-child";
2408
+ return "launcher";
2409
+ }
2410
+ function runnerExitAction(code, stopping) {
2411
+ if (code === RUNNER_RESTART_EXIT_CODE) return "restart";
2412
+ if (stopping) return "ignore";
2413
+ return code === 0 ? "stop" : "died";
2414
+ }
2415
+ function restartOwner(env = process.env, alive = pidAlive) {
2416
+ if (env[SERVICE_ENV] === "1") return "service";
2417
+ return env[UP_LAUNCHER_ENV] === "1" && alive(Number(env[UP_LAUNCHER_PID_ENV])) ? "launcher" : "none";
2418
+ }
2419
+ function relaunchDelayMs(failures) {
2420
+ return Math.min(5 * 6e4, 1e4 * 2 ** Math.max(0, failures - 1));
2421
+ }
2422
+ var RAN_MS = 6e4;
2423
+ var RESET_MS = 10 * 6e4;
2424
+ function processRestartDeps() {
2425
+ return {
2426
+ env: process.env,
2427
+ execPath: process.execPath,
2428
+ execArgv: process.execArgv,
2429
+ script: process.argv[1],
2430
+ spawn: nodeSpawn,
2431
+ spawnSync: nodeSpawnSync,
2432
+ exit: (code) => process.exit(code),
2433
+ log: () => {
2434
+ },
2435
+ sleep: (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms),
2436
+ step: (line) => logUpdateStep(line),
2437
+ parent: { pid: process.ppid, alive: pidAlive },
2438
+ pid: process.pid,
2439
+ now: Date.now
2440
+ };
2441
+ }
2442
+ function restartPagsUp(args, extraEnv, deps, opts = {}) {
2443
+ const env = { ...deps.env, ...extraEnv };
2444
+ const argv = [...deps.execArgv, deps.script, ...args];
2445
+ const exitCode = opts.exitCode ?? RUNNER_RESTART_EXIT_CODE;
2446
+ const mode = upRestartMode(env, deps.parent);
2447
+ deps.step(`${opts.initial ? "start" : "restart"}: pags ${args.join(" ")} \u2014 ${opts.initial ? "as" : "leaving via"} ${mode}${exitCode === RUNNER_DIED_EXIT_CODE ? " (the runner died)" : ""}`);
2448
+ if (mode === "service") {
2449
+ deps.exit(exitCode);
2450
+ return;
2451
+ }
2452
+ if (mode === "command") {
2453
+ leaveForRestart(mode, { spawn: deps.spawn, exit: deps.exit });
2454
+ return;
2455
+ }
2456
+ if (mode === "launcher-child") {
2457
+ deps.exit(exitCode);
2458
+ return;
2459
+ }
2460
+ const childEnv = { ...env, [UP_LAUNCHER_ENV]: "1", [UP_LAUNCHER_PID_ENV]: String(deps.pid) };
2461
+ let failures = 0;
2462
+ for (let first = true; ; first = false) {
2463
+ const unattended = !first || env.PAGS_NO_PROMPT === "1";
2464
+ deps.step(`launcher: starting pags up${unattended ? " (unattended)" : ""}`);
2465
+ const startedAt = deps.now();
2466
+ const r = deps.spawnSync(deps.execPath, argv, { stdio: "inherit", env: first ? childEnv : { ...childEnv, PAGS_NO_PROMPT: "1" } });
2467
+ const ran = deps.now() - startedAt;
2468
+ const how = r.error ? `could not start (${r.error.message})` : r.signal ? `was killed by ${r.signal}` : `exited ${r.status}`;
2469
+ deps.step(`launcher: pags up ${how} after ${Math.round(ran / 1e3)}s`);
2470
+ if (ran >= RESET_MS) failures = 0;
2471
+ if (r.status === RUNNER_RESTART_EXIT_CODE) {
2472
+ failures = 0;
2473
+ continue;
2474
+ }
2475
+ if (!r.error && (r.status === 0 || r.signal === "SIGINT")) {
2476
+ deps.exit(0);
2477
+ return;
2478
+ }
2479
+ if (unattended || r.status === RUNNER_DIED_EXIT_CODE || ran >= RAN_MS) {
2480
+ failures++;
2481
+ const wait = relaunchDelayMs(failures);
2482
+ deps.log(`pags up ${how} \u2014 starting it again in ${Math.round(wait / 1e3)}s (attempt ${failures + 1}).`);
2483
+ deps.step(`launcher: retrying in ${Math.round(wait / 1e3)}s (failure ${failures})`);
2484
+ deps.sleep(wait);
2485
+ continue;
2486
+ }
2487
+ if (!opts.initial) deps.log(`Restart failed${typeof r.status === "number" ? ` (exit ${r.status})` : ""} \u2014 the runner is NOT running. Run 'pags up' again.`);
2488
+ deps.exit(typeof r.status === "number" && r.status !== 0 ? r.status : 1);
2489
+ return;
2490
+ }
2491
+ }
2492
+
2347
2493
  // src/commands/runner/relay.ts
2348
2494
  var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
2349
2495
  var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH, RUNNER_RESTART_PATH]);
@@ -2355,12 +2501,15 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2355
2501
  const machine = loadMachineIdentity(runnerNode);
2356
2502
  const autoUpdateKey = machine.id ? autoUpdatePolicyKey(machine.id) : "";
2357
2503
  const cell = new PolicyCell(autoUpdateKey ? loadAutoUpdatePolicy(autoUpdateKey) ?? { autoUpdate: false } : { autoUpdate: false }, (p) => saveAutoUpdatePolicy(autoUpdateKey, p), !!autoUpdateKey);
2504
+ const trial = releaseTrial(CLI_VERSION, cell, writeLine);
2358
2505
  let scheduleAutomaticUpdate = () => void 0;
2359
2506
  let autoUpdateDiagnostics = () => ({});
2507
+ const updateLog = updateLogUpload(CLI_VERSION);
2360
2508
  const autoUpdateStatusBody = () => ({
2361
2509
  status: autoUpdateStatusWire(cell.policy.status),
2362
2510
  latestVersion: cell.policy.latestVersion,
2363
2511
  error: cell.policy.reason,
2512
+ ...updateLog.body(),
2364
2513
  ...autoUpdateDiagnostics()
2365
2514
  });
2366
2515
  const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
@@ -2387,7 +2536,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2387
2536
  lastRegisterError = msg;
2388
2537
  writeError(`register ${id.slice(0, 8)}\u2026 failed: ${msg}`);
2389
2538
  },
2390
- onAction: (action) => scheduleAutomaticUpdate(action === "start", action === "cancel")
2539
+ onAction: (action) => scheduleAutomaticUpdate(action === "start", action === "cancel"),
2540
+ onAnswer: () => {
2541
+ trial.proven();
2542
+ updateLog.answered();
2543
+ }
2391
2544
  });
2392
2545
  const registered = channel.registered;
2393
2546
  const registerRuntime = (id, forceClaim = force) => channel.register(id, forceClaim);
@@ -2424,6 +2577,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2424
2577
  ..."error" in release ? { latestError: release.error } : {},
2425
2578
  fromSource: runsFromSource(),
2426
2579
  restarter: restarterFrom(process.env),
2580
+ rolledBack: trial.rolledBack(),
2427
2581
  // A restart ends browser/application work too. Give these durable names to the same
2428
2582
  // planner so neither manual nor automatic update can mistake them for an idle machine.
2429
2583
  busy
@@ -2434,7 +2588,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2434
2588
  cell,
2435
2589
  refresh: channel.refresh,
2436
2590
  facts: updateFacts,
2437
- install: installVersion,
2591
+ install: (version2) => installVersion(version2, CLI_VERSION),
2438
2592
  admission: relayMutationAdmission,
2439
2593
  leaseActive: deferredUpdateLeaseActive,
2440
2594
  detachAll: () => {
@@ -2631,7 +2785,9 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2631
2785
  // #896: who is heartbeating. Set by `runner connect` from the lock it took.
2632
2786
  ...process.env.PAGS_RUNNER_RSID ? { rsid: process.env.PAGS_RUNNER_RSID } : {},
2633
2787
  pid: process.pid,
2634
- ...process.env.PAGS_RUNNER_LAUNCH ? { launch: process.env.PAGS_RUNNER_LAUNCH } : {}
2788
+ ...process.env.PAGS_RUNNER_LAUNCH ? { launch: process.env.PAGS_RUNNER_LAUNCH } : {},
2789
+ supervisor: restartOwner()
2790
+ // #1154: what starts it again if it dies
2635
2791
  },
2636
2792
  relayRttMs,
2637
2793
  sessions: sessions ?? void 0
@@ -3198,6 +3354,8 @@ function createRunnerCommand() {
3198
3354
  process.exit(1);
3199
3355
  }
3200
3356
  lockHeld = { path: taken.path, rsid: taken.lock.rsid, nonce: taken.lock.nonce, launch: taken.lock.launch };
3357
+ process.env.PAGS_RUNNER_RSID = taken.lock.rsid;
3358
+ process.env.PAGS_RUNNER_LAUNCH = taken.lock.launch;
3201
3359
  writeLine(`STATUS runner-session=${taken.lock.rsid}`);
3202
3360
  }
3203
3361
  const releaseHeldLock = () => {
@@ -3394,7 +3552,7 @@ import { readFileSync as readFileSync11, statSync as statSync2, watchFile } from
3394
3552
  import { Command as Command9 } from "commander";
3395
3553
 
3396
3554
  // src/background.ts
3397
- import { spawn as nodeSpawn } from "child_process";
3555
+ import { spawn as nodeSpawn2 } from "child_process";
3398
3556
  import { closeSync as closeSync3, existsSync as existsSync10, mkdirSync as mkdirSync8, openSync as openSync3, readFileSync as readFileSync10, renameSync as renameSync4, statSync, writeFileSync as writeFileSync6 } from "fs";
3399
3557
  import { dirname as dirname5, join as join11 } from "path";
3400
3558
  var BACKGROUND_ENV = "PAGS_BACKGROUND";
@@ -3433,7 +3591,7 @@ function backgroundRestartCommand(execPath, script, upArgs, platform = process.p
3433
3591
  const argv = [execPath, script, ...upArgs, "--background"].map((a) => shellQuote(a, platform)).join(" ");
3434
3592
  return platform === "win32" ? `set ${BACKGROUND_RESTART_ENV}=1&& ${argv}` : `${BACKGROUND_RESTART_ENV}=1 ${argv}`;
3435
3593
  }
3436
- function spawnDetached(command, args, env, logPath, spawn5 = nodeSpawn) {
3594
+ function spawnDetached(command, args, env, logPath, spawn5 = nodeSpawn2) {
3437
3595
  mkdirSync8(dirname5(logPath), { recursive: true });
3438
3596
  rotateLog(logPath);
3439
3597
  const fd = openSync3(logPath, "a", 384);
@@ -3531,7 +3689,7 @@ var logsCommand = new Command9("logs").description("Show the background runner's
3531
3689
  // src/commands/up.ts
3532
3690
  import { createRequire as createRequire2 } from "module";
3533
3691
  import { Command as Command10 } from "commander";
3534
- import { hostname as hostname7 } from "os";
3692
+ import { homedir as homedir5, hostname as hostname7, userInfo } from "os";
3535
3693
 
3536
3694
  // src/tui.ts
3537
3695
  import chalk from "chalk";
@@ -3650,65 +3808,6 @@ async function waitForKey(keys, onInterrupt) {
3650
3808
  });
3651
3809
  }
3652
3810
 
3653
- // src/commands/up-restart.ts
3654
- import { spawn as nodeSpawn2, spawnSync as nodeSpawnSync } from "child_process";
3655
- var UP_LAUNCHER_ENV = "PAGS_UP_LAUNCHER";
3656
- function upRestartMode(env, platform, canExecve) {
3657
- if (env[SERVICE_ENV] === "1") return "service";
3658
- if (env[RESTART_COMMAND_ENV]?.trim()) return "command";
3659
- if (env[UP_LAUNCHER_ENV] === "1") return "launcher-child";
3660
- return platform !== "win32" && canExecve ? "exec" : "launcher";
3661
- }
3662
- function processRestartDeps() {
3663
- const execve = process.execve;
3664
- return {
3665
- env: process.env,
3666
- platform: process.platform,
3667
- execPath: process.execPath,
3668
- execArgv: process.execArgv,
3669
- script: process.argv[1],
3670
- execve: typeof execve === "function" ? execve.bind(process) : void 0,
3671
- spawn: nodeSpawn2,
3672
- spawnSync: nodeSpawnSync,
3673
- exit: (code) => process.exit(code),
3674
- log: () => {
3675
- }
3676
- };
3677
- }
3678
- function restartPagsUp(args, extraEnv, deps) {
3679
- const env = { ...deps.env, ...extraEnv };
3680
- const argv = [...deps.execArgv, deps.script, ...args];
3681
- const mode = upRestartMode(env, deps.platform, !!deps.execve);
3682
- if (mode === "service" || mode === "command") {
3683
- leaveForRestart(mode, { spawn: deps.spawn, exit: deps.exit });
3684
- return;
3685
- }
3686
- if (mode === "launcher-child") {
3687
- deps.exit(RUNNER_RESTART_EXIT_CODE);
3688
- return;
3689
- }
3690
- if (mode === "exec") {
3691
- try {
3692
- deps.execve(deps.execPath, [deps.execPath, ...argv], env);
3693
- } catch (e) {
3694
- deps.log(`Restart failed (${e instanceof Error ? e.message : String(e)}) \u2014 the runner is NOT running. Run 'pags up' again.`);
3695
- deps.exit(1);
3696
- }
3697
- return;
3698
- }
3699
- for (let first = true; ; first = false) {
3700
- const r = deps.spawnSync(deps.execPath, argv, { stdio: "inherit", env: { ...env, [UP_LAUNCHER_ENV]: "1", ...first ? {} : { PAGS_NO_PROMPT: "1" } } });
3701
- if (r.status === RUNNER_RESTART_EXIT_CODE) continue;
3702
- if (r.error || r.status !== 0) {
3703
- deps.log(`Restart failed${typeof r.status === "number" ? ` (exit ${r.status})` : ""} \u2014 the runner is NOT running. Run 'pags up' again.`);
3704
- deps.exit(typeof r.status === "number" && r.status !== 0 ? r.status : 1);
3705
- return;
3706
- }
3707
- deps.exit(0);
3708
- return;
3709
- }
3710
- }
3711
-
3712
3811
  // src/runner-replace.ts
3713
3812
  var REPLACE_EXIT_TIMEOUT_MS = 3e4;
3714
3813
  function describeWork(work) {
@@ -3850,11 +3949,263 @@ async function startInBackground(start, deps = processDeps()) {
3850
3949
  return 1;
3851
3950
  }
3852
3951
 
3952
+ // src/service-install.ts
3953
+ import { execFileSync as execFileSync3 } from "child_process";
3954
+ import { mkdirSync as mkdirSync9, rmSync, writeFileSync as writeFileSync7 } from "fs";
3955
+ import { posix, win32 as win322 } from "path";
3956
+ var SERVICE_LABEL = "online.proagentstore.pags-up";
3957
+ var SYSTEMD_UNIT = "pags-up.service";
3958
+ var WINDOWS_TASK = "pags up";
3959
+ var pathFor = (ctx) => ctx.platform === "win32" ? win322 : posix;
3960
+ var supervisorPath = (ctx) => pathFor(ctx).join(ctx.configDir, "pags-up-supervisor.mjs");
3961
+ var serviceLogPath = (ctx) => pathFor(ctx).join(ctx.configDir, "pags-up.log");
3962
+ function supervisorScript(ctx) {
3963
+ const config = JSON.stringify({ node: ctx.nodePath, script: ctx.script, log: serviceLogPath(ctx), path: ctx.pathEnv });
3964
+ return `// Written by \`pags up --install-service\` (#1141). Keeps \`pags up\` running; see packages/cli/src/service-install.ts.
3965
+ import { spawn } from "node:child_process";
3966
+ import { appendFileSync, closeSync, openSync } from "node:fs";
3967
+
3968
+ const config = ${config};
3969
+ const num = (name, fallback) => { const n = Number(process.env[name]); return Number.isFinite(n) && n > 0 ? n : fallback; };
3970
+ const MIN = num("PAGS_SUPERVISOR_MIN_MS", 10_000);
3971
+ const MAX = num("PAGS_SUPERVISOR_MAX_MS", 300_000);
3972
+ const RESET = num("PAGS_SUPERVISOR_RESET_MS", 600_000);
3973
+ const RESTART_CODE = 75; // \`pags up\` asking to be started again: an update, or runner_restart
3974
+
3975
+ const say = (line) => { try { appendFileSync(config.log, \`[\${new Date().toISOString()}] supervisor: \${line}\\n\`); } catch { /* a log that cannot be written must not stop the runner */ } };
3976
+ let delay = MIN;
3977
+ let child = null;
3978
+ let stopping = false;
3979
+
3980
+ function start() {
3981
+ if (stopping) return;
3982
+ const startedAt = Date.now();
3983
+ let out = "ignore";
3984
+ try { out = openSync(config.log, "a"); } catch { /* run without a log rather than not at all */ }
3985
+ say("starting pags up");
3986
+ child = spawn(config.node, [config.script, "up", "--headless"], {
3987
+ stdio: ["ignore", out, out],
3988
+ env: { ...process.env, PATH: config.path || process.env.PATH, PAGS_SERVICE: "1", PAGS_SERVICE_NAME: "${SERVICE_LABEL}", PAGS_NO_PROMPT: "1" },
3989
+ windowsHide: true,
3990
+ });
3991
+ if (typeof out === "number") closeSync(out);
3992
+ child.on("error", (e) => say(\`could not start pags up: \${e.message}\`));
3993
+ child.on("exit", (code, signal) => {
3994
+ child = null;
3995
+ if (stopping) return;
3996
+ if (Date.now() - startedAt >= RESET) delay = MIN;
3997
+ const wait = code === RESTART_CODE ? 1_000 : delay;
3998
+ say(\`pags up exited (\${signal ?? code}); starting it again in \${Math.round(wait / 1000)}s\`);
3999
+ if (code !== RESTART_CODE) delay = Math.min(delay * 2, MAX);
4000
+ setTimeout(start, wait);
4001
+ });
4002
+ }
4003
+
4004
+ for (const sig of ["SIGTERM", "SIGINT"]) {
4005
+ process.on(sig, () => {
4006
+ stopping = true;
4007
+ say(\`stopping (\${sig})\`);
4008
+ if (child) child.kill("SIGTERM");
4009
+ setTimeout(() => process.exit(0), 5_000).unref();
4010
+ if (!child) process.exit(0);
4011
+ else child.once("exit", () => process.exit(0));
4012
+ });
4013
+ }
4014
+ start();
4015
+ `;
4016
+ }
4017
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4018
+ var unitWord = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
4019
+ var ps = (s) => `'${s.replace(/'/g, "''")}'`;
4020
+ function versionManagedNode(nodePath) {
4021
+ if (/[\\/]n[\\/]versions[\\/]/.test(nodePath)) return "n";
4022
+ const m = /[\\/]\.?(nvm|fnm|volta|asdf|nodenv)[\\/]/i.exec(nodePath);
4023
+ return m ? m[1].toLowerCase() : null;
4024
+ }
4025
+ var pinnedNodeNote = (ctx) => {
4026
+ const manager = versionManagedNode(ctx.nodePath);
4027
+ return manager ? [`It runs Node ${ctx.nodePath}, installed by ${manager}. Removing or switching away from that Node version stops the service; after changing Node, run \`pags up --install-service\` again.`] : [];
4028
+ };
4029
+ function servicePlan(ctx) {
4030
+ const supervisor = supervisorPath(ctx);
4031
+ const log = serviceLogPath(ctx);
4032
+ const files = [{ path: supervisor, content: supervisorScript(ctx) }];
4033
+ if (ctx.platform === "darwin") {
4034
+ const plist = posix.join(ctx.home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
4035
+ const domain = `gui/${ctx.uid ?? 501}`;
4036
+ files.push({
4037
+ path: plist,
4038
+ content: `<?xml version="1.0" encoding="UTF-8"?>
4039
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4040
+ <plist version="1.0">
4041
+ <dict>
4042
+ <key>Label</key><string>${SERVICE_LABEL}</string>
4043
+ <key>ProgramArguments</key>
4044
+ <array>
4045
+ <string>${xml(ctx.nodePath)}</string>
4046
+ <string>${xml(supervisor)}</string>
4047
+ </array>
4048
+ <key>RunAtLoad</key><true/>
4049
+ <key>KeepAlive</key><true/>
4050
+ <key>ThrottleInterval</key><integer>30</integer>
4051
+ <key>ProcessType</key><string>Background</string>
4052
+ <key>WorkingDirectory</key><string>${xml(ctx.home)}</string>
4053
+ <key>StandardOutPath</key><string>${xml(log)}</string>
4054
+ <key>StandardErrorPath</key><string>${xml(log)}</string>
4055
+ </dict>
4056
+ </plist>
4057
+ `
4058
+ });
4059
+ return {
4060
+ kind: "launchd",
4061
+ files,
4062
+ commands: [
4063
+ { file: "launchctl", args: ["bootout", `${domain}/${SERVICE_LABEL}`], optional: true, why: "unload an earlier copy" },
4064
+ { file: "launchctl", args: ["bootstrap", domain, plist], why: "load the LaunchAgent" },
4065
+ { file: "launchctl", args: ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`], why: "start it now" }
4066
+ ],
4067
+ notes: [
4068
+ ...pinnedNodeNote(ctx),
4069
+ `LaunchAgent ${plist} \u2014 runs while this user is logged in. On a machine that must survive a reboot unattended, turn on automatic login for this user.`,
4070
+ `Remove it with: pags up --uninstall-service`
4071
+ ]
4072
+ };
4073
+ }
4074
+ if (ctx.platform === "win32") {
4075
+ const task = win322.join(ctx.configDir, "pags-up-task.ps1");
4076
+ const principal = `${ctx.domain ? `${ctx.domain}\\` : ""}${ctx.user ?? ""}`;
4077
+ files.push({
4078
+ path: task,
4079
+ content: `# Written by \`pags up --install-service\` (#1141). Registers the "${WINDOWS_TASK}" Scheduled Task; needs an elevated PowerShell.
4080
+ $ErrorActionPreference = 'Stop'
4081
+ $action = New-ScheduledTaskAction -Execute ${ps(ctx.nodePath)} -Argument ${ps(`"${supervisor}"`)}
4082
+ $trigger = New-ScheduledTaskTrigger -AtStartup
4083
+ # Task Scheduler restarts a task that failed to START; a supervisor killed later just ends. Re-trigger it every five
4084
+ # minutes, forever \u2014 MultipleInstances IgnoreNew makes a trigger that finds it running do nothing (#1154).
4085
+ $trigger.Repetition = (New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5)).Repetition
4086
+ $principal = New-ScheduledTaskPrincipal -UserId ${ps(principal)} -LogonType S4U -RunLevel Limited
4087
+ $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew
4088
+ Unregister-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Confirm:$false -ErrorAction SilentlyContinue
4089
+ Register-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
4090
+ Start-ScheduledTask -TaskName ${ps(WINDOWS_TASK)}
4091
+ `
4092
+ });
4093
+ return {
4094
+ kind: "scheduled-task",
4095
+ files,
4096
+ commands: [{ file: "powershell.exe", args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", task], why: "register and start the Scheduled Task" }],
4097
+ notes: [
4098
+ ...pinnedNodeNote(ctx),
4099
+ `Scheduled Task "${WINDOWS_TASK}" \u2014 starts at boot as ${principal || "this user"}, with nobody logged in. Registering it needs an elevated PowerShell: if it was refused, run ${task} from one.`,
4100
+ `Remove it with: pags up --uninstall-service`
4101
+ ]
4102
+ };
4103
+ }
4104
+ const unit = posix.join(ctx.home, ".config", "systemd", "user", SYSTEMD_UNIT);
4105
+ files.push({
4106
+ path: unit,
4107
+ content: `# Written by \`pags up --install-service\` (#1141).
4108
+ [Unit]
4109
+ Description=ProAgentStore runner (pags up)
4110
+ After=network-online.target
4111
+ Wants=network-online.target
4112
+ StartLimitIntervalSec=0
4113
+
4114
+ [Service]
4115
+ ExecStart=${unitWord(ctx.nodePath)} ${unitWord(supervisor)}
4116
+ Environment=${unitWord(`PATH=${ctx.pathEnv}`)}
4117
+ Restart=always
4118
+ RestartSec=10
4119
+ KillMode=mixed
4120
+
4121
+ [Install]
4122
+ WantedBy=default.target
4123
+ `
4124
+ });
4125
+ return {
4126
+ kind: "systemd",
4127
+ files,
4128
+ commands: [
4129
+ { file: "systemctl", args: ["--user", "daemon-reload"], why: "read the unit" },
4130
+ { file: "systemctl", args: ["--user", "enable", "--now", SYSTEMD_UNIT], why: "enable and start it" },
4131
+ { file: "loginctl", args: ["enable-linger", ...ctx.user ? [ctx.user] : []], optional: true, why: "keep it running with nobody logged in" }
4132
+ ],
4133
+ notes: [...pinnedNodeNote(ctx), `systemd user unit ${unit}. Logs: ${log} (and journalctl --user -u ${SYSTEMD_UNIT}).`, `Remove it with: pags up --uninstall-service`]
4134
+ };
4135
+ }
4136
+ function serviceRemovalPlan(ctx) {
4137
+ const plan = servicePlan(ctx);
4138
+ const files = plan.files.map((f) => f.path);
4139
+ if (plan.kind === "launchd") return { files, commands: [{ file: "launchctl", args: ["bootout", `gui/${ctx.uid ?? 501}/${SERVICE_LABEL}`], optional: true, why: "unload the LaunchAgent" }] };
4140
+ if (plan.kind === "systemd") return { files, commands: [{ file: "systemctl", args: ["--user", "disable", "--now", SYSTEMD_UNIT], optional: true, why: "stop and disable the unit" }] };
4141
+ return {
4142
+ files,
4143
+ commands: [{ file: "powershell.exe", args: ["-NoProfile", "-Command", `Stop-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Confirm:$false`], optional: true, why: "remove the Scheduled Task" }]
4144
+ };
4145
+ }
4146
+ var defaultWrite = (path, content) => {
4147
+ mkdirSync9(path.replace(/[\\/][^\\/]*$/, ""), { recursive: true });
4148
+ writeFileSync7(path, content);
4149
+ };
4150
+ var defaultRun = (file, args) => void execFileSync3(file, args, { stdio: "pipe", timeout: 6e4 });
4151
+ function installService(ctx, deps = {}) {
4152
+ const plan = servicePlan(ctx);
4153
+ const lines = [];
4154
+ for (const f of plan.files) {
4155
+ (deps.write ?? defaultWrite)(f.path, f.content);
4156
+ lines.push(`wrote ${f.path}`);
4157
+ }
4158
+ let ok = true;
4159
+ for (const c2 of plan.commands) {
4160
+ try {
4161
+ (deps.run ?? defaultRun)(c2.file, c2.args);
4162
+ lines.push(`${c2.why}: ok`);
4163
+ } catch (e) {
4164
+ const why = String(e.stderr ?? "").trim() || (e instanceof Error ? e.message : String(e));
4165
+ lines.push(`${c2.why}: ${c2.optional ? "skipped" : "FAILED"} \u2014 ${why.split(/\r?\n/)[0].slice(0, 200)}`);
4166
+ if (!c2.optional) ok = false;
4167
+ }
4168
+ }
4169
+ return { ok, lines: [...lines, ...plan.notes] };
4170
+ }
4171
+ function uninstallService(ctx, deps = {}) {
4172
+ const plan = serviceRemovalPlan(ctx);
4173
+ const lines = [];
4174
+ for (const c2 of plan.commands) {
4175
+ try {
4176
+ (deps.run ?? defaultRun)(c2.file, c2.args);
4177
+ lines.push(`${c2.why}: ok`);
4178
+ } catch (e) {
4179
+ lines.push(`${c2.why}: skipped \u2014 ${(e instanceof Error ? e.message : String(e)).split(/\r?\n/)[0].slice(0, 200)}`);
4180
+ }
4181
+ }
4182
+ for (const f of plan.files) {
4183
+ (deps.remove ?? ((p) => rmSync(p, { force: true })))(f);
4184
+ lines.push(`removed ${f}`);
4185
+ }
4186
+ return { lines };
4187
+ }
4188
+
3853
4189
  // src/commands/up.ts
3854
4190
  var API_BASE3 = "https://api.proagentstore.online";
3855
4191
  var CLI_VERSION2 = createRequire2(import.meta.url)("../package.json").version;
3856
- var upCommand = new Command10("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").option("--force", "Take over from another connected machine").option("--replace", "Stop the `pags up` already running here for this account, then take over").option("--now", "With --replace: stop it even though work is running on it (that work is lost)").option("--background", "Run the runner in the background, with no terminal open (stop it with `pags down`)").action(async (opts) => {
4192
+ var upCommand = new Command10("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").option("--force", "Take over from another connected machine").option("--replace", "Stop the `pags up` already running here for this account, then take over").option("--now", "With --replace: stop it even though work is running on it (that work is lost)").option("--background", "Run the runner in the background, with no terminal open (stop it with `pags down`)").option("--install-service", "Install an OS service that keeps `pags up` running (launchd, systemd or a Scheduled Task), then exit").option("--uninstall-service", "Remove the service --install-service installed, then exit").action(async (opts) => {
4193
+ if (opts.uninstallService) {
4194
+ for (const line of uninstallService(serviceContext()).lines) writeLine(line);
4195
+ return;
4196
+ }
4197
+ if (!opts.background && !opts.installService && upRestartMode(process.env) === "launcher") {
4198
+ restartPagsUp(process.argv.slice(2), {}, { ...processRestartDeps(), log: (line) => writeLine(` ${line}`) }, { initial: true });
4199
+ return;
4200
+ }
3857
4201
  const session = requireSession();
4202
+ if (opts.installService) {
4203
+ const result = installService(serviceContext());
4204
+ for (const line of result.lines) writeLine(line);
4205
+ writeLine(result.ok ? "The service starts `pags up` now and whenever it exits. A `pags up` already running here keeps the lock; the service takes over once it stops." : "The service was not fully installed \u2014 see the FAILED line above.");
4206
+ if (!result.ok) process.exitCode = 1;
4207
+ return;
4208
+ }
3858
4209
  const state = {
3859
4210
  user: session.user.login,
3860
4211
  instances: [],
@@ -3871,18 +4222,34 @@ var upCommand = new Command10("up").description("Start the browser runner for al
3871
4222
  // as taking a few seconds, indefinitely, because nothing counted (#497).
3872
4223
  startedAt: Date.now()
3873
4224
  };
4225
+ const unattended = process.env.PAGS_NO_PROMPT === "1";
4226
+ logUpdateStep(`start: pags up ${CLI_VERSION2}${unattended ? " (unattended)" : ""}`);
3874
4227
  clearScreen();
3875
4228
  printLogo(CLI_VERSION2);
3876
4229
  printStep("Signed in as " + session.user.login, "ok");
3877
4230
  printStep("Fetching instances...", "wait");
3878
- const res = await fetch(`${API_BASE3}/v1/instances/my/instances`, {
3879
- headers: { Authorization: `Bearer ${session.token}` }
3880
- });
3881
- if (!res.ok) {
3882
- printStep("Failed to fetch instances: " + res.status, "fail");
3883
- process.exit(1);
4231
+ let data;
4232
+ for (let failures = 1; ; failures++) {
4233
+ let failure;
4234
+ try {
4235
+ const res = await fetch(`${API_BASE3}/v1/instances/my/instances`, { headers: { Authorization: `Bearer ${session.token}` } });
4236
+ if (res.ok) {
4237
+ data = await res.json();
4238
+ break;
4239
+ }
4240
+ failure = `HTTP ${res.status}`;
4241
+ } catch (e) {
4242
+ failure = e instanceof Error ? e.message : String(e);
4243
+ }
4244
+ logUpdateStep(`start: fetching instances failed (${failure})${unattended ? "" : " \u2014 exiting"}`);
4245
+ if (!unattended) {
4246
+ printStep("Failed to fetch instances: " + failure, "fail");
4247
+ process.exit(1);
4248
+ }
4249
+ const wait = Math.min(5 * 6e4, 5e3 * 2 ** (failures - 1));
4250
+ printStep(`Failed to fetch instances (${failure}) \u2014 retrying in ${Math.round(wait / 1e3)}s`, "wait");
4251
+ await new Promise((resolve5) => setTimeout(resolve5, wait));
3884
4252
  }
3885
- const data = await res.json();
3886
4253
  const active = (data.instances || []).filter((i) => i.status === "active");
3887
4254
  let instances = active;
3888
4255
  let elsewhere = [];
@@ -3930,9 +4297,10 @@ var upCommand = new Command10("up").description("Start the browser runner for al
3930
4297
  state.activeInstance = instances.length === 1 ? instances[0].name || instances[0].slug || instances[0].id.slice(0, 8) : `${instances.length} agents`;
3931
4298
  printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
3932
4299
  const lockFile = lockPath(session.user.login);
3933
- if (opts.background && process.env[BACKGROUND_RESTART_ENV] === "1") await waitUntilTakeable(() => inspectHolder(lockFile));
4300
+ if (unattended || opts.background && process.env[BACKGROUND_RESTART_ENV] === "1") await waitUntilTakeable(() => inspectHolder(lockFile), { timeoutMs: 6e4 });
3934
4301
  const held = await inspectHolder(lockFile);
3935
4302
  if (held.lock && !isTakeable(held.verdict)) {
4303
+ logUpdateStep(`start: the lock is held by pid ${held.lock.pid} (${held.verdict})${opts.replace ? " \u2014 replacing it" : " \u2014 exiting"}`);
3936
4304
  if (!opts.replace) {
3937
4305
  clearScreen();
3938
4306
  printLogo(CLI_VERSION2);
@@ -4036,31 +4404,29 @@ var upCommand = new Command10("up").description("Start the browser runner for al
4036
4404
  void deliverCrashReports({ apiBase: API_BASE3, token: session.token, account: session.user.login, paths: crashes }).catch(() => void 0);
4037
4405
  };
4038
4406
  child.on("exit", (code, signal) => {
4039
- if (code === RUNNER_RESTART_EXIT_CODE) {
4407
+ const action = runnerExitAction(code, stopping);
4408
+ if (action === "restart") {
4409
+ logUpdateStep("restart: runner connect asked for a restart (exit 75)");
4040
4410
  restartUp("Runner updated remotely \u2014 restarting pags up on the new version\u2026", { PAGS_NO_PROMPT: "1" });
4041
4411
  return;
4042
4412
  }
4043
4413
  childDead = true;
4044
- if (code !== 0 && !stopping) recordConnectExit(code, signal ?? null, logs.slice(-20));
4045
- if (code === null) {
4046
- state.runner = "error";
4047
- state.lastEvent = "Runner stopped by another process (signal) \u2014 it is not connected any more";
4048
- printStatus(state);
4049
- return;
4050
- }
4051
- if (code !== 0) {
4052
- state.runner = "error";
4053
- state.lastEvent = `Runner exited (code ${code})`;
4054
- const recent = logs.slice(-5);
4055
- if (recent.length) state.lastEvent += ": " + recent[recent.length - 1].slice(0, 60);
4056
- printStatus(state);
4414
+ logUpdateStep(`runner connect ${signal ? `was killed by ${signal}` : `exited ${code}`}${stopping ? " (stopping)" : ""}`);
4415
+ if (action === "ignore") return;
4416
+ if (action === "stop") {
4417
+ clearScreen();
4418
+ writeLine(" Runner stopped (pags down, or replaced by another pags up).");
4419
+ process.exit(0);
4057
4420
  }
4421
+ recordConnectExit(code, signal ?? null, logs.slice(-20));
4422
+ const recent = logs.slice(-5).at(-1);
4423
+ restartUp(`Runner ${signal ? `was killed by ${signal}` : `exited (code ${code})`}${recent ? `: ${recent.slice(0, 60)}` : ""} \u2014 starting it again\u2026`, { PAGS_NO_PROMPT: "1" }, RUNNER_DIED_EXIT_CODE);
4058
4424
  });
4059
- function restartUp(why, env = {}) {
4425
+ function restartUp(why, env = {}, exitCode) {
4060
4426
  writeLine(` ${why}`);
4061
4427
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
4062
4428
  process.stdin.pause();
4063
- restartPagsUp(restartUpArgs(opts), env, { ...processRestartDeps(), log: (line) => writeLine(` ${line}`) });
4429
+ restartPagsUp(restartUpArgs(opts), env, { ...processRestartDeps(), log: (line) => writeLine(` ${line}`) }, { exitCode });
4064
4430
  process.exit(1);
4065
4431
  }
4066
4432
  const shutdown = () => {
@@ -4125,6 +4491,26 @@ var downCommand = new Command10("down").description("Stop the browser runner and
4125
4491
  }
4126
4492
  writeLine("");
4127
4493
  });
4494
+ function serviceContext() {
4495
+ const user = (() => {
4496
+ try {
4497
+ return userInfo().username;
4498
+ } catch {
4499
+ return process.env.USER || process.env.USERNAME || void 0;
4500
+ }
4501
+ })();
4502
+ return {
4503
+ platform: process.platform,
4504
+ home: homedir5(),
4505
+ nodePath: process.execPath,
4506
+ script: process.argv[1],
4507
+ configDir: configDir(),
4508
+ pathEnv: process.env.PATH ?? "",
4509
+ uid: process.getuid?.(),
4510
+ user,
4511
+ domain: process.env.USERDOMAIN
4512
+ };
4513
+ }
4128
4514
 
4129
4515
  // src/index.ts
4130
4516
  var require2 = createRequire3(import.meta.url);