@proagentstore/cli 0.4.149 → 0.4.152

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";
@@ -374,6 +381,7 @@ jobs:
374
381
  });
375
382
 
376
383
  // src/commands/login.ts
384
+ import { spawn } from "child_process";
377
385
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
378
386
  import { createServer } from "http";
379
387
  import { homedir } from "os";
@@ -407,9 +415,21 @@ function requireSession() {
407
415
  }
408
416
  return session;
409
417
  }
418
+ function browserCommand(url, platform = process.platform) {
419
+ if (platform === "win32") return { file: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
420
+ return { file: platform === "darwin" ? "open" : "xdg-open", args: [url] };
421
+ }
422
+ function openBrowser(url) {
423
+ const { file, args } = browserCommand(url);
424
+ try {
425
+ const child = spawn(file, args, { stdio: "ignore", detached: true, windowsHide: true });
426
+ child.on("error", () => void 0);
427
+ child.unref();
428
+ } catch {
429
+ }
430
+ }
410
431
  var loginCommand = new Command3("login").description("Sign in with Google or GitHub (opens browser)").option("--github", "Use GitHub instead of Google").action(async (opts) => {
411
432
  const provider = opts.github ? "github" : "google";
412
- writeLine(`Opening browser for ${provider} sign-in...`);
413
433
  const port = await findFreePort();
414
434
  const callbackUrl = `http://127.0.0.1:${port}/callback`;
415
435
  const tokenPromise = new Promise((resolve5, reject) => {
@@ -462,9 +482,9 @@ var loginCommand = new Command3("login").description("Sign in with Google or Git
462
482
  });
463
483
  const oauthBase = provider === "google" ? `${API_BASE}/v1/auth/google/start` : `${API_BASE}/v1/auth/github/start`;
464
484
  const oauthUrl = `${oauthBase}?return_to=${encodeURIComponent(callbackUrl)}`;
465
- const open = (await import("child_process")).exec;
466
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
467
- open(`${cmd} "${oauthUrl}"`);
485
+ writeLine(`Opening your browser for ${provider} sign-in. If it does not open, paste this URL into a browser on this machine:`);
486
+ writeLine(` ${oauthUrl}`);
487
+ openBrowser(oauthUrl);
468
488
  try {
469
489
  await tokenPromise;
470
490
  const session = loadSession();
@@ -983,14 +1003,14 @@ var unclaimCommand = new Command4("unclaim").description("Remove a mistaken mach
983
1003
  var machinesCommand = new Command4("machines").description("Show and claim the machine names ProAgentStore has for this account").addCommand(listCommand, { isDefault: true }).addCommand(claimCommand).addCommand(unclaimCommand);
984
1004
 
985
1005
  // src/commands/mcp.ts
986
- import { spawn } from "child_process";
1006
+ import { spawn as spawn2 } from "child_process";
987
1007
  import { Command as Command5 } from "commander";
988
1008
  var DEFAULT_MCP_URL = "https://mcp.proagentstore.online/mcp";
989
1009
  function buildMcpRemoteArgs(opts, extraArgs = []) {
990
1010
  return ["-y", "mcp-remote", opts.url || DEFAULT_MCP_URL, ...extraArgs];
991
1011
  }
992
1012
  async function runMcpProxy(opts, extraArgs = []) {
993
- const child = spawn("npx", buildMcpRemoteArgs(opts, extraArgs), {
1013
+ const child = spawn2("npx", buildMcpRemoteArgs(opts, extraArgs), {
994
1014
  stdio: "inherit",
995
1015
  env: process.env
996
1016
  });
@@ -1173,12 +1193,12 @@ var publishCommand = new Command7("publish").description("Publish an agent to Pr
1173
1193
  });
1174
1194
 
1175
1195
  // src/commands/runner/command.ts
1176
- import { spawn as spawn4 } from "child_process";
1196
+ import { spawn as spawn5 } from "child_process";
1177
1197
  import { randomUUID as randomUUID4 } from "crypto";
1178
1198
  import { Command as Command8 } from "commander";
1179
1199
 
1180
1200
  // src/commands/runner/process.ts
1181
- import { spawn as spawn2 } from "child_process";
1201
+ import { spawn as spawn3 } from "child_process";
1182
1202
  import { existsSync as existsSync6 } from "fs";
1183
1203
  import { resolve as resolve4, win32 } from "path";
1184
1204
  import { createServer as createServer2 } from "net";
@@ -1253,7 +1273,7 @@ function runnerSpawnSpec(opts) {
1253
1273
  function startRunnerForeground(opts) {
1254
1274
  const spec = runnerSpawnSpec(opts);
1255
1275
  return new Promise((resolvePromise, reject) => {
1256
- const child = spawn2(spec.command, spec.args, {
1276
+ const child = spawn3(spec.command, spec.args, {
1257
1277
  cwd: spec.cwd,
1258
1278
  stdio: "inherit",
1259
1279
  shell: spec.shell
@@ -1329,7 +1349,7 @@ function sampleResources(read = os, now = Date.now(), extra = {}) {
1329
1349
  }
1330
1350
 
1331
1351
  // src/commands/runner/self-update.ts
1332
- import { spawn as spawn3 } from "child_process";
1352
+ import { spawn as spawn4 } from "child_process";
1333
1353
  var RUNNER_UPDATE_PATH = "/pags/runner/update";
1334
1354
  var RUNNER_RESTART_PATH = "/pags/runner/restart";
1335
1355
  var RUNNER_RESTART_EXIT_CODE = 75;
@@ -1360,7 +1380,8 @@ function deferredUpdateLease(value, now = Date.now()) {
1360
1380
  function deferredUpdateLeaseActive(lease, now = Date.now()) {
1361
1381
  return now < lease.expiresAt;
1362
1382
  }
1363
- function leaveForRestart(restarter, deps = { spawn: spawn3, exit: (code) => process.exit(code) }) {
1383
+ function leaveForRestart(restarter, deps = { spawn: spawn4, exit: (code) => process.exit(code) }) {
1384
+ logUpdateStep(`restart: leaving for the ${restarter} restarter (${restarter === "command" ? "restart command started, exit 0" : `exit ${RUNNER_RESTART_EXIT_CODE}`})`);
1364
1385
  if (restarter === "command") {
1365
1386
  deps.spawn(process.env[RESTART_COMMAND_ENV], { shell: true, detached: true, stdio: "ignore", env: process.env }).unref();
1366
1387
  deps.exit(0);
@@ -1379,6 +1400,8 @@ function planRunnerUpdate(f) {
1379
1400
  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
1401
  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
1402
  if (!olderThan(f.current, f.latest)) return { action: "up-to-date", current: f.current };
1403
+ const rolledBack = f.rolledBack?.[f.latest];
1404
+ 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
1405
  if (!f.restarter) {
1383
1406
  return {
1384
1407
  action: "refused",
@@ -1411,17 +1434,24 @@ function planRunnerRestart(f) {
1411
1434
  }
1412
1435
  return { action: "restart", current: f.current, restarter: f.restarter };
1413
1436
  }
1414
- async function installVersion(version2) {
1415
- if (process.env[BOOTSTRAP_ENV]) {
1416
- await installPayload(version2);
1417
- return;
1418
- }
1437
+ async function installVersion(version2, current = null) {
1438
+ const bootstrapped = !!process.env[BOOTSTRAP_ENV];
1439
+ logUpdateStep(`install: ${current ?? "?"} \u2192 ${version2} starting (${bootstrapped ? "payload cache" : "npm i -g"})`);
1419
1440
  try {
1420
- await runNpm(["i", "-g", `${CLI_PACKAGE}@${version2}`], { timeout: 5 * 6e4 });
1441
+ if (bootstrapped) {
1442
+ await installPayload(version2);
1443
+ beginTrial(payloadRoot(), version2, current);
1444
+ } else {
1445
+ await runNpm(["i", "-g", `${CLI_PACKAGE}@${version2}`], { timeout: 5 * 6e4 }).catch((e) => {
1446
+ const stderr = String(e.stderr ?? "").trim();
1447
+ throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
1448
+ });
1449
+ }
1421
1450
  } catch (e) {
1422
- const stderr = String(e.stderr ?? "").trim();
1423
- throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
1451
+ logUpdateStep(`install: ${version2} FAILED \u2014 ${e instanceof Error ? e.message : String(e)}`);
1452
+ throw e;
1424
1453
  }
1454
+ logUpdateStep(`install: ${version2} installed`);
1425
1455
  }
1426
1456
 
1427
1457
  // src/commands/runner/membership.ts
@@ -1776,13 +1806,15 @@ function parseAutoUpdatePolicy(value) {
1776
1806
  const lastAttemptAt = raw.lastAttemptAt ?? raw.last_attempt_at;
1777
1807
  const reason = raw.reason;
1778
1808
  const recovery = parseRecovery(raw.recovery);
1809
+ const fleet = raw.fleetUpdating ?? raw.fleet_updating;
1779
1810
  return {
1780
1811
  autoUpdate,
1781
1812
  ...status ? { status } : {},
1782
1813
  ...typeof latestVersion === "string" ? { latestVersion } : {},
1783
1814
  ...typeof lastAttemptAt === "string" ? { lastAttemptAt } : {},
1784
1815
  ...typeof reason === "string" ? { reason } : {},
1785
- ...recovery ? { recovery } : {}
1816
+ ...recovery ? { recovery } : {},
1817
+ ...typeof fleet === "number" && Number.isInteger(fleet) && fleet >= 0 ? { fleetUpdating: fleet } : {}
1786
1818
  };
1787
1819
  }
1788
1820
  function policyFromResponse(value) {
@@ -1843,7 +1875,9 @@ function acceptPolicyResponse(local, wasAuthoritative, response) {
1843
1875
  const { recovery: _fromService, ...remote } = parsed;
1844
1876
  const action = policyScheduleAction(local, remote, wasAuthoritative);
1845
1877
  const ownerTransition = action === "cancel" || !local.autoUpdate;
1846
- return { policy: ownerTransition ? { ...local, ...remote } : { ...local, autoUpdate: remote.autoUpdate }, action };
1878
+ const { fleetUpdating: _stale, ...base } = local;
1879
+ const fleet = remote.fleetUpdating === void 0 ? {} : { fleetUpdating: remote.fleetUpdating };
1880
+ return { policy: ownerTransition ? { ...base, ...remote, ...fleet } : { ...base, autoUpdate: remote.autoUpdate, ...fleet }, action };
1847
1881
  }
1848
1882
  var ResponseFence = class {
1849
1883
  sent = 0;
@@ -2068,6 +2102,12 @@ var AutomaticUpdateController = class {
2068
2102
  this.defer();
2069
2103
  return;
2070
2104
  }
2105
+ if (policy.fleetUpdating) {
2106
+ this.blockerClass = "fleet_stagger";
2107
+ 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.` });
2108
+ this.defer(plan.latest);
2109
+ return;
2110
+ }
2071
2111
  const target = open && isVersion(open.target) ? open.target : plan.latest;
2072
2112
  const attempt = { ...plan, latest: target };
2073
2113
  const recovery = open ?? { target, attachments: [...this.deps.attachments?.() ?? []], at: new Date(this.now()).toISOString() };
@@ -2077,7 +2117,7 @@ var AutomaticUpdateController = class {
2077
2117
  this.deps.status("restarting", { latestVersion: target, reason: void 0 });
2078
2118
  return;
2079
2119
  }
2080
- this.blockerClass = "idle_wait";
2120
+ this.blockerClass = this.deps.policy().value.fleetUpdating ? "fleet_stagger" : "idle_wait";
2081
2121
  this.defer(target);
2082
2122
  }
2083
2123
  defer(latest) {
@@ -2250,7 +2290,10 @@ function createPolicyChannel(deps) {
2250
2290
  if (action === "cancel") deps.onAction("cancel");
2251
2291
  else if (action === "start" && !deps.cell.refreshing) deps.onAction("start");
2252
2292
  };
2253
- const accept = (response, sentAs) => act(deps.cell.accept(response, sentAs));
2293
+ const accept = (response, sentAs) => {
2294
+ act(deps.cell.accept(response, sentAs));
2295
+ deps.onAnswer?.();
2296
+ };
2254
2297
  const register = async (id, force = deps.force, signal) => {
2255
2298
  const sent = deps.cell.next();
2256
2299
  try {
@@ -2300,7 +2343,7 @@ function createInstallBridge(deps) {
2300
2343
  if (automatic) {
2301
2344
  const fresh = await within((signal) => deps.refresh(signal));
2302
2345
  if (fresh === null) expired(false, "The owner policy refresh before installing");
2303
- if (!fresh || !deps.cell.policy.autoUpdate) {
2346
+ if (!fresh || !deps.cell.policy.autoUpdate || deps.cell.policy.fleetUpdating) {
2304
2347
  release(false);
2305
2348
  return false;
2306
2349
  }
@@ -2344,6 +2387,122 @@ function createInstallBridge(deps) {
2344
2387
  return { run, inFlight: () => inFlight, installedPendingRestart: () => installedPendingRestart };
2345
2388
  }
2346
2389
 
2390
+ // src/commands/runner/release-trial.ts
2391
+ function releaseTrial(version2, cell, log, env = process.env, root = payloadRoot) {
2392
+ if (!env[BOOTSTRAP_ENV]) return { rollbackReason: null, proven: () => void 0, rolledBack: () => ({}) };
2393
+ const notice = takeRollbackNotice(root());
2394
+ const rollbackReason = notice ? `Rolled back from ${notice.from} to ${version2}: ${notice.reason}.` : null;
2395
+ if (rollbackReason) {
2396
+ log(`auto-update: ${rollbackReason}`);
2397
+ cell.record("failure", { reason: rollbackReason, recovery: void 0 });
2398
+ }
2399
+ let done = false;
2400
+ return {
2401
+ rollbackReason,
2402
+ proven: () => {
2403
+ if (done) return;
2404
+ done = true;
2405
+ if (markPayloadHealthy(root(), version2)) log(`auto-update: ${version2} registered \u2014 it is kept.`);
2406
+ },
2407
+ rolledBack: () => badVersions(root())
2408
+ };
2409
+ }
2410
+
2411
+ // src/commands/up-restart.ts
2412
+ import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
2413
+ var UP_LAUNCHER_ENV = "PAGS_UP_LAUNCHER";
2414
+ var UP_LAUNCHER_PID_ENV = "PAGS_UP_LAUNCHER_PID";
2415
+ var RUNNER_DIED_EXIT_CODE = 76;
2416
+ function upRestartMode(env, parent = { pid: process.ppid, alive: pidAlive }) {
2417
+ if (env[SERVICE_ENV] === "1") return "service";
2418
+ if (env[RESTART_COMMAND_ENV]?.trim()) return "command";
2419
+ const launcher = Number(env[UP_LAUNCHER_PID_ENV]);
2420
+ if (env[UP_LAUNCHER_ENV] === "1" && launcher === parent.pid && parent.alive(launcher)) return "launcher-child";
2421
+ return "launcher";
2422
+ }
2423
+ function runnerExitAction(code, stopping) {
2424
+ if (code === RUNNER_RESTART_EXIT_CODE) return "restart";
2425
+ if (stopping) return "ignore";
2426
+ return code === 0 ? "stop" : "died";
2427
+ }
2428
+ function restartOwner(env = process.env, alive = pidAlive) {
2429
+ if (env[SERVICE_ENV] === "1") return "service";
2430
+ return env[UP_LAUNCHER_ENV] === "1" && alive(Number(env[UP_LAUNCHER_PID_ENV])) ? "launcher" : "none";
2431
+ }
2432
+ function relaunchDelayMs(failures) {
2433
+ return Math.min(5 * 6e4, 1e4 * 2 ** Math.max(0, failures - 1));
2434
+ }
2435
+ var RAN_MS = 6e4;
2436
+ var RESET_MS = 10 * 6e4;
2437
+ function processRestartDeps() {
2438
+ return {
2439
+ env: process.env,
2440
+ execPath: process.execPath,
2441
+ execArgv: process.execArgv,
2442
+ script: process.argv[1],
2443
+ spawn: nodeSpawn,
2444
+ spawnSync: nodeSpawnSync,
2445
+ exit: (code) => process.exit(code),
2446
+ log: () => {
2447
+ },
2448
+ sleep: (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms),
2449
+ step: (line) => logUpdateStep(line),
2450
+ parent: { pid: process.ppid, alive: pidAlive },
2451
+ pid: process.pid,
2452
+ now: Date.now
2453
+ };
2454
+ }
2455
+ function restartPagsUp(args, extraEnv, deps, opts = {}) {
2456
+ const env = { ...deps.env, ...extraEnv };
2457
+ const argv = [...deps.execArgv, deps.script, ...args];
2458
+ const exitCode = opts.exitCode ?? RUNNER_RESTART_EXIT_CODE;
2459
+ const mode = upRestartMode(env, deps.parent);
2460
+ deps.step(`${opts.initial ? "start" : "restart"}: pags ${args.join(" ")} \u2014 ${opts.initial ? "as" : "leaving via"} ${mode}${exitCode === RUNNER_DIED_EXIT_CODE ? " (the runner died)" : ""}`);
2461
+ if (mode === "service") {
2462
+ deps.exit(exitCode);
2463
+ return;
2464
+ }
2465
+ if (mode === "command") {
2466
+ leaveForRestart(mode, { spawn: deps.spawn, exit: deps.exit });
2467
+ return;
2468
+ }
2469
+ if (mode === "launcher-child") {
2470
+ deps.exit(exitCode);
2471
+ return;
2472
+ }
2473
+ const childEnv = { ...env, [UP_LAUNCHER_ENV]: "1", [UP_LAUNCHER_PID_ENV]: String(deps.pid) };
2474
+ let failures = 0;
2475
+ for (let first = true; ; first = false) {
2476
+ const unattended = !first || env.PAGS_NO_PROMPT === "1";
2477
+ deps.step(`launcher: starting pags up${unattended ? " (unattended)" : ""}`);
2478
+ const startedAt = deps.now();
2479
+ const r = deps.spawnSync(deps.execPath, argv, { stdio: "inherit", env: first ? childEnv : { ...childEnv, PAGS_NO_PROMPT: "1" } });
2480
+ const ran = deps.now() - startedAt;
2481
+ const how = r.error ? `could not start (${r.error.message})` : r.signal ? `was killed by ${r.signal}` : `exited ${r.status}`;
2482
+ deps.step(`launcher: pags up ${how} after ${Math.round(ran / 1e3)}s`);
2483
+ if (ran >= RESET_MS) failures = 0;
2484
+ if (r.status === RUNNER_RESTART_EXIT_CODE) {
2485
+ failures = 0;
2486
+ continue;
2487
+ }
2488
+ if (!r.error && (r.status === 0 || r.signal === "SIGINT")) {
2489
+ deps.exit(0);
2490
+ return;
2491
+ }
2492
+ if (unattended || r.status === RUNNER_DIED_EXIT_CODE || ran >= RAN_MS) {
2493
+ failures++;
2494
+ const wait = relaunchDelayMs(failures);
2495
+ deps.log(`pags up ${how} \u2014 starting it again in ${Math.round(wait / 1e3)}s (attempt ${failures + 1}).`);
2496
+ deps.step(`launcher: retrying in ${Math.round(wait / 1e3)}s (failure ${failures})`);
2497
+ deps.sleep(wait);
2498
+ continue;
2499
+ }
2500
+ if (!opts.initial) deps.log(`Restart failed${typeof r.status === "number" ? ` (exit ${r.status})` : ""} \u2014 the runner is NOT running. Run 'pags up' again.`);
2501
+ deps.exit(typeof r.status === "number" && r.status !== 0 ? r.status : 1);
2502
+ return;
2503
+ }
2504
+ }
2505
+
2347
2506
  // src/commands/runner/relay.ts
2348
2507
  var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
2349
2508
  var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH, RUNNER_RESTART_PATH]);
@@ -2355,12 +2514,15 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2355
2514
  const machine = loadMachineIdentity(runnerNode);
2356
2515
  const autoUpdateKey = machine.id ? autoUpdatePolicyKey(machine.id) : "";
2357
2516
  const cell = new PolicyCell(autoUpdateKey ? loadAutoUpdatePolicy(autoUpdateKey) ?? { autoUpdate: false } : { autoUpdate: false }, (p) => saveAutoUpdatePolicy(autoUpdateKey, p), !!autoUpdateKey);
2517
+ const trial = releaseTrial(CLI_VERSION, cell, writeLine);
2358
2518
  let scheduleAutomaticUpdate = () => void 0;
2359
2519
  let autoUpdateDiagnostics = () => ({});
2520
+ const updateLog = updateLogUpload(CLI_VERSION);
2360
2521
  const autoUpdateStatusBody = () => ({
2361
2522
  status: autoUpdateStatusWire(cell.policy.status),
2362
2523
  latestVersion: cell.policy.latestVersion,
2363
2524
  error: cell.policy.reason,
2525
+ ...updateLog.body(),
2364
2526
  ...autoUpdateDiagnostics()
2365
2527
  });
2366
2528
  const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
@@ -2387,7 +2549,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2387
2549
  lastRegisterError = msg;
2388
2550
  writeError(`register ${id.slice(0, 8)}\u2026 failed: ${msg}`);
2389
2551
  },
2390
- onAction: (action) => scheduleAutomaticUpdate(action === "start", action === "cancel")
2552
+ onAction: (action) => scheduleAutomaticUpdate(action === "start", action === "cancel"),
2553
+ onAnswer: () => {
2554
+ trial.proven();
2555
+ updateLog.answered();
2556
+ }
2391
2557
  });
2392
2558
  const registered = channel.registered;
2393
2559
  const registerRuntime = (id, forceClaim = force) => channel.register(id, forceClaim);
@@ -2424,6 +2590,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2424
2590
  ..."error" in release ? { latestError: release.error } : {},
2425
2591
  fromSource: runsFromSource(),
2426
2592
  restarter: restarterFrom(process.env),
2593
+ rolledBack: trial.rolledBack(),
2427
2594
  // A restart ends browser/application work too. Give these durable names to the same
2428
2595
  // planner so neither manual nor automatic update can mistake them for an idle machine.
2429
2596
  busy
@@ -2434,7 +2601,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2434
2601
  cell,
2435
2602
  refresh: channel.refresh,
2436
2603
  facts: updateFacts,
2437
- install: installVersion,
2604
+ install: (version2) => installVersion(version2, CLI_VERSION),
2438
2605
  admission: relayMutationAdmission,
2439
2606
  leaseActive: deferredUpdateLeaseActive,
2440
2607
  detachAll: () => {
@@ -2631,7 +2798,9 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2631
2798
  // #896: who is heartbeating. Set by `runner connect` from the lock it took.
2632
2799
  ...process.env.PAGS_RUNNER_RSID ? { rsid: process.env.PAGS_RUNNER_RSID } : {},
2633
2800
  pid: process.pid,
2634
- ...process.env.PAGS_RUNNER_LAUNCH ? { launch: process.env.PAGS_RUNNER_LAUNCH } : {}
2801
+ ...process.env.PAGS_RUNNER_LAUNCH ? { launch: process.env.PAGS_RUNNER_LAUNCH } : {},
2802
+ supervisor: restartOwner()
2803
+ // #1154: what starts it again if it dies
2635
2804
  },
2636
2805
  relayRttMs,
2637
2806
  sessions: sessions ?? void 0
@@ -3198,6 +3367,8 @@ function createRunnerCommand() {
3198
3367
  process.exit(1);
3199
3368
  }
3200
3369
  lockHeld = { path: taken.path, rsid: taken.lock.rsid, nonce: taken.lock.nonce, launch: taken.lock.launch };
3370
+ process.env.PAGS_RUNNER_RSID = taken.lock.rsid;
3371
+ process.env.PAGS_RUNNER_LAUNCH = taken.lock.launch;
3201
3372
  writeLine(`STATUS runner-session=${taken.lock.rsid}`);
3202
3373
  }
3203
3374
  const releaseHeldLock = () => {
@@ -3209,7 +3380,7 @@ function createRunnerCommand() {
3209
3380
  const spec = runnerSpawnSpec(runnerOpts);
3210
3381
  const crashAccount = lockAccount || loadSession()?.user.login || "";
3211
3382
  const crashes = crashAccount ? crashPaths(configDir(), crashAccount, opts.dataDir) : null;
3212
- const runner = spawn4(spec.command, spec.args, {
3383
+ const runner = spawn5(spec.command, spec.args, {
3213
3384
  cwd: spec.cwd,
3214
3385
  stdio: ["ignore", "pipe", "pipe"],
3215
3386
  shell: spec.shell,
@@ -3394,7 +3565,7 @@ import { readFileSync as readFileSync11, statSync as statSync2, watchFile } from
3394
3565
  import { Command as Command9 } from "commander";
3395
3566
 
3396
3567
  // src/background.ts
3397
- import { spawn as nodeSpawn } from "child_process";
3568
+ import { spawn as nodeSpawn2 } from "child_process";
3398
3569
  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
3570
  import { dirname as dirname5, join as join11 } from "path";
3400
3571
  var BACKGROUND_ENV = "PAGS_BACKGROUND";
@@ -3433,12 +3604,12 @@ function backgroundRestartCommand(execPath, script, upArgs, platform = process.p
3433
3604
  const argv = [execPath, script, ...upArgs, "--background"].map((a) => shellQuote(a, platform)).join(" ");
3434
3605
  return platform === "win32" ? `set ${BACKGROUND_RESTART_ENV}=1&& ${argv}` : `${BACKGROUND_RESTART_ENV}=1 ${argv}`;
3435
3606
  }
3436
- function spawnDetached(command, args, env, logPath, spawn5 = nodeSpawn) {
3607
+ function spawnDetached(command, args, env, logPath, spawn6 = nodeSpawn2) {
3437
3608
  mkdirSync8(dirname5(logPath), { recursive: true });
3438
3609
  rotateLog(logPath);
3439
3610
  const fd = openSync3(logPath, "a", 384);
3440
3611
  try {
3441
- const child = spawn5(command, args, { detached: true, stdio: ["ignore", fd, fd], env, windowsHide: true });
3612
+ const child = spawn6(command, args, { detached: true, stdio: ["ignore", fd, fd], env, windowsHide: true });
3442
3613
  child.unref();
3443
3614
  if (!child.pid) throw new Error("the runner process did not start");
3444
3615
  return child.pid;
@@ -3531,7 +3702,7 @@ var logsCommand = new Command9("logs").description("Show the background runner's
3531
3702
  // src/commands/up.ts
3532
3703
  import { createRequire as createRequire2 } from "module";
3533
3704
  import { Command as Command10 } from "commander";
3534
- import { hostname as hostname7 } from "os";
3705
+ import { homedir as homedir5, hostname as hostname7, userInfo } from "os";
3535
3706
 
3536
3707
  // src/tui.ts
3537
3708
  import chalk from "chalk";
@@ -3650,65 +3821,6 @@ async function waitForKey(keys, onInterrupt) {
3650
3821
  });
3651
3822
  }
3652
3823
 
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
3824
  // src/runner-replace.ts
3713
3825
  var REPLACE_EXIT_TIMEOUT_MS = 3e4;
3714
3826
  function describeWork(work) {
@@ -3850,11 +3962,263 @@ async function startInBackground(start, deps = processDeps()) {
3850
3962
  return 1;
3851
3963
  }
3852
3964
 
3965
+ // src/service-install.ts
3966
+ import { execFileSync as execFileSync3 } from "child_process";
3967
+ import { mkdirSync as mkdirSync9, rmSync, writeFileSync as writeFileSync7 } from "fs";
3968
+ import { posix, win32 as win322 } from "path";
3969
+ var SERVICE_LABEL = "online.proagentstore.pags-up";
3970
+ var SYSTEMD_UNIT = "pags-up.service";
3971
+ var WINDOWS_TASK = "pags up";
3972
+ var pathFor = (ctx) => ctx.platform === "win32" ? win322 : posix;
3973
+ var supervisorPath = (ctx) => pathFor(ctx).join(ctx.configDir, "pags-up-supervisor.mjs");
3974
+ var serviceLogPath = (ctx) => pathFor(ctx).join(ctx.configDir, "pags-up.log");
3975
+ function supervisorScript(ctx) {
3976
+ const config = JSON.stringify({ node: ctx.nodePath, script: ctx.script, log: serviceLogPath(ctx), path: ctx.pathEnv });
3977
+ return `// Written by \`pags up --install-service\` (#1141). Keeps \`pags up\` running; see packages/cli/src/service-install.ts.
3978
+ import { spawn } from "node:child_process";
3979
+ import { appendFileSync, closeSync, openSync } from "node:fs";
3980
+
3981
+ const config = ${config};
3982
+ const num = (name, fallback) => { const n = Number(process.env[name]); return Number.isFinite(n) && n > 0 ? n : fallback; };
3983
+ const MIN = num("PAGS_SUPERVISOR_MIN_MS", 10_000);
3984
+ const MAX = num("PAGS_SUPERVISOR_MAX_MS", 300_000);
3985
+ const RESET = num("PAGS_SUPERVISOR_RESET_MS", 600_000);
3986
+ const RESTART_CODE = 75; // \`pags up\` asking to be started again: an update, or runner_restart
3987
+
3988
+ 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 */ } };
3989
+ let delay = MIN;
3990
+ let child = null;
3991
+ let stopping = false;
3992
+
3993
+ function start() {
3994
+ if (stopping) return;
3995
+ const startedAt = Date.now();
3996
+ let out = "ignore";
3997
+ try { out = openSync(config.log, "a"); } catch { /* run without a log rather than not at all */ }
3998
+ say("starting pags up");
3999
+ child = spawn(config.node, [config.script, "up", "--headless"], {
4000
+ stdio: ["ignore", out, out],
4001
+ env: { ...process.env, PATH: config.path || process.env.PATH, PAGS_SERVICE: "1", PAGS_SERVICE_NAME: "${SERVICE_LABEL}", PAGS_NO_PROMPT: "1" },
4002
+ windowsHide: true,
4003
+ });
4004
+ if (typeof out === "number") closeSync(out);
4005
+ child.on("error", (e) => say(\`could not start pags up: \${e.message}\`));
4006
+ child.on("exit", (code, signal) => {
4007
+ child = null;
4008
+ if (stopping) return;
4009
+ if (Date.now() - startedAt >= RESET) delay = MIN;
4010
+ const wait = code === RESTART_CODE ? 1_000 : delay;
4011
+ say(\`pags up exited (\${signal ?? code}); starting it again in \${Math.round(wait / 1000)}s\`);
4012
+ if (code !== RESTART_CODE) delay = Math.min(delay * 2, MAX);
4013
+ setTimeout(start, wait);
4014
+ });
4015
+ }
4016
+
4017
+ for (const sig of ["SIGTERM", "SIGINT"]) {
4018
+ process.on(sig, () => {
4019
+ stopping = true;
4020
+ say(\`stopping (\${sig})\`);
4021
+ if (child) child.kill("SIGTERM");
4022
+ setTimeout(() => process.exit(0), 5_000).unref();
4023
+ if (!child) process.exit(0);
4024
+ else child.once("exit", () => process.exit(0));
4025
+ });
4026
+ }
4027
+ start();
4028
+ `;
4029
+ }
4030
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4031
+ var unitWord = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
4032
+ var ps = (s) => `'${s.replace(/'/g, "''")}'`;
4033
+ function versionManagedNode(nodePath) {
4034
+ if (/[\\/]n[\\/]versions[\\/]/.test(nodePath)) return "n";
4035
+ const m = /[\\/]\.?(nvm|fnm|volta|asdf|nodenv)[\\/]/i.exec(nodePath);
4036
+ return m ? m[1].toLowerCase() : null;
4037
+ }
4038
+ var pinnedNodeNote = (ctx) => {
4039
+ const manager = versionManagedNode(ctx.nodePath);
4040
+ 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.`] : [];
4041
+ };
4042
+ function servicePlan(ctx) {
4043
+ const supervisor = supervisorPath(ctx);
4044
+ const log = serviceLogPath(ctx);
4045
+ const files = [{ path: supervisor, content: supervisorScript(ctx) }];
4046
+ if (ctx.platform === "darwin") {
4047
+ const plist = posix.join(ctx.home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
4048
+ const domain = `gui/${ctx.uid ?? 501}`;
4049
+ files.push({
4050
+ path: plist,
4051
+ content: `<?xml version="1.0" encoding="UTF-8"?>
4052
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4053
+ <plist version="1.0">
4054
+ <dict>
4055
+ <key>Label</key><string>${SERVICE_LABEL}</string>
4056
+ <key>ProgramArguments</key>
4057
+ <array>
4058
+ <string>${xml(ctx.nodePath)}</string>
4059
+ <string>${xml(supervisor)}</string>
4060
+ </array>
4061
+ <key>RunAtLoad</key><true/>
4062
+ <key>KeepAlive</key><true/>
4063
+ <key>ThrottleInterval</key><integer>30</integer>
4064
+ <key>ProcessType</key><string>Background</string>
4065
+ <key>WorkingDirectory</key><string>${xml(ctx.home)}</string>
4066
+ <key>StandardOutPath</key><string>${xml(log)}</string>
4067
+ <key>StandardErrorPath</key><string>${xml(log)}</string>
4068
+ </dict>
4069
+ </plist>
4070
+ `
4071
+ });
4072
+ return {
4073
+ kind: "launchd",
4074
+ files,
4075
+ commands: [
4076
+ { file: "launchctl", args: ["bootout", `${domain}/${SERVICE_LABEL}`], optional: true, why: "unload an earlier copy" },
4077
+ { file: "launchctl", args: ["bootstrap", domain, plist], why: "load the LaunchAgent" },
4078
+ { file: "launchctl", args: ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`], why: "start it now" }
4079
+ ],
4080
+ notes: [
4081
+ ...pinnedNodeNote(ctx),
4082
+ `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.`,
4083
+ `Remove it with: pags up --uninstall-service`
4084
+ ]
4085
+ };
4086
+ }
4087
+ if (ctx.platform === "win32") {
4088
+ const task = win322.join(ctx.configDir, "pags-up-task.ps1");
4089
+ const principal = `${ctx.domain ? `${ctx.domain}\\` : ""}${ctx.user ?? ""}`;
4090
+ files.push({
4091
+ path: task,
4092
+ content: `# Written by \`pags up --install-service\` (#1141). Registers the "${WINDOWS_TASK}" Scheduled Task; needs an elevated PowerShell.
4093
+ $ErrorActionPreference = 'Stop'
4094
+ $action = New-ScheduledTaskAction -Execute ${ps(ctx.nodePath)} -Argument ${ps(`"${supervisor}"`)}
4095
+ $trigger = New-ScheduledTaskTrigger -AtStartup
4096
+ # Task Scheduler restarts a task that failed to START; a supervisor killed later just ends. Re-trigger it every five
4097
+ # minutes, forever \u2014 MultipleInstances IgnoreNew makes a trigger that finds it running do nothing (#1154).
4098
+ $trigger.Repetition = (New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5)).Repetition
4099
+ $principal = New-ScheduledTaskPrincipal -UserId ${ps(principal)} -LogonType S4U -RunLevel Limited
4100
+ $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew
4101
+ Unregister-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Confirm:$false -ErrorAction SilentlyContinue
4102
+ Register-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
4103
+ Start-ScheduledTask -TaskName ${ps(WINDOWS_TASK)}
4104
+ `
4105
+ });
4106
+ return {
4107
+ kind: "scheduled-task",
4108
+ files,
4109
+ commands: [{ file: "powershell.exe", args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", task], why: "register and start the Scheduled Task" }],
4110
+ notes: [
4111
+ ...pinnedNodeNote(ctx),
4112
+ `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.`,
4113
+ `Remove it with: pags up --uninstall-service`
4114
+ ]
4115
+ };
4116
+ }
4117
+ const unit = posix.join(ctx.home, ".config", "systemd", "user", SYSTEMD_UNIT);
4118
+ files.push({
4119
+ path: unit,
4120
+ content: `# Written by \`pags up --install-service\` (#1141).
4121
+ [Unit]
4122
+ Description=ProAgentStore runner (pags up)
4123
+ After=network-online.target
4124
+ Wants=network-online.target
4125
+ StartLimitIntervalSec=0
4126
+
4127
+ [Service]
4128
+ ExecStart=${unitWord(ctx.nodePath)} ${unitWord(supervisor)}
4129
+ Environment=${unitWord(`PATH=${ctx.pathEnv}`)}
4130
+ Restart=always
4131
+ RestartSec=10
4132
+ KillMode=mixed
4133
+
4134
+ [Install]
4135
+ WantedBy=default.target
4136
+ `
4137
+ });
4138
+ return {
4139
+ kind: "systemd",
4140
+ files,
4141
+ commands: [
4142
+ { file: "systemctl", args: ["--user", "daemon-reload"], why: "read the unit" },
4143
+ { file: "systemctl", args: ["--user", "enable", "--now", SYSTEMD_UNIT], why: "enable and start it" },
4144
+ { file: "loginctl", args: ["enable-linger", ...ctx.user ? [ctx.user] : []], optional: true, why: "keep it running with nobody logged in" }
4145
+ ],
4146
+ notes: [...pinnedNodeNote(ctx), `systemd user unit ${unit}. Logs: ${log} (and journalctl --user -u ${SYSTEMD_UNIT}).`, `Remove it with: pags up --uninstall-service`]
4147
+ };
4148
+ }
4149
+ function serviceRemovalPlan(ctx) {
4150
+ const plan = servicePlan(ctx);
4151
+ const files = plan.files.map((f) => f.path);
4152
+ if (plan.kind === "launchd") return { files, commands: [{ file: "launchctl", args: ["bootout", `gui/${ctx.uid ?? 501}/${SERVICE_LABEL}`], optional: true, why: "unload the LaunchAgent" }] };
4153
+ if (plan.kind === "systemd") return { files, commands: [{ file: "systemctl", args: ["--user", "disable", "--now", SYSTEMD_UNIT], optional: true, why: "stop and disable the unit" }] };
4154
+ return {
4155
+ files,
4156
+ 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" }]
4157
+ };
4158
+ }
4159
+ var defaultWrite = (path, content) => {
4160
+ mkdirSync9(path.replace(/[\\/][^\\/]*$/, ""), { recursive: true });
4161
+ writeFileSync7(path, content);
4162
+ };
4163
+ var defaultRun = (file, args) => void execFileSync3(file, args, { stdio: "pipe", timeout: 6e4 });
4164
+ function installService(ctx, deps = {}) {
4165
+ const plan = servicePlan(ctx);
4166
+ const lines = [];
4167
+ for (const f of plan.files) {
4168
+ (deps.write ?? defaultWrite)(f.path, f.content);
4169
+ lines.push(`wrote ${f.path}`);
4170
+ }
4171
+ let ok = true;
4172
+ for (const c2 of plan.commands) {
4173
+ try {
4174
+ (deps.run ?? defaultRun)(c2.file, c2.args);
4175
+ lines.push(`${c2.why}: ok`);
4176
+ } catch (e) {
4177
+ const why = String(e.stderr ?? "").trim() || (e instanceof Error ? e.message : String(e));
4178
+ lines.push(`${c2.why}: ${c2.optional ? "skipped" : "FAILED"} \u2014 ${why.split(/\r?\n/)[0].slice(0, 200)}`);
4179
+ if (!c2.optional) ok = false;
4180
+ }
4181
+ }
4182
+ return { ok, lines: [...lines, ...plan.notes] };
4183
+ }
4184
+ function uninstallService(ctx, deps = {}) {
4185
+ const plan = serviceRemovalPlan(ctx);
4186
+ const lines = [];
4187
+ for (const c2 of plan.commands) {
4188
+ try {
4189
+ (deps.run ?? defaultRun)(c2.file, c2.args);
4190
+ lines.push(`${c2.why}: ok`);
4191
+ } catch (e) {
4192
+ lines.push(`${c2.why}: skipped \u2014 ${(e instanceof Error ? e.message : String(e)).split(/\r?\n/)[0].slice(0, 200)}`);
4193
+ }
4194
+ }
4195
+ for (const f of plan.files) {
4196
+ (deps.remove ?? ((p) => rmSync(p, { force: true })))(f);
4197
+ lines.push(`removed ${f}`);
4198
+ }
4199
+ return { lines };
4200
+ }
4201
+
3853
4202
  // src/commands/up.ts
3854
4203
  var API_BASE3 = "https://api.proagentstore.online";
3855
4204
  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) => {
4205
+ 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) => {
4206
+ if (opts.uninstallService) {
4207
+ for (const line of uninstallService(serviceContext()).lines) writeLine(line);
4208
+ return;
4209
+ }
4210
+ if (!opts.background && !opts.installService && upRestartMode(process.env) === "launcher") {
4211
+ restartPagsUp(process.argv.slice(2), {}, { ...processRestartDeps(), log: (line) => writeLine(` ${line}`) }, { initial: true });
4212
+ return;
4213
+ }
3857
4214
  const session = requireSession();
4215
+ if (opts.installService) {
4216
+ const result = installService(serviceContext());
4217
+ for (const line of result.lines) writeLine(line);
4218
+ 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.");
4219
+ if (!result.ok) process.exitCode = 1;
4220
+ return;
4221
+ }
3858
4222
  const state = {
3859
4223
  user: session.user.login,
3860
4224
  instances: [],
@@ -3871,18 +4235,34 @@ var upCommand = new Command10("up").description("Start the browser runner for al
3871
4235
  // as taking a few seconds, indefinitely, because nothing counted (#497).
3872
4236
  startedAt: Date.now()
3873
4237
  };
4238
+ const unattended = process.env.PAGS_NO_PROMPT === "1";
4239
+ logUpdateStep(`start: pags up ${CLI_VERSION2}${unattended ? " (unattended)" : ""}`);
3874
4240
  clearScreen();
3875
4241
  printLogo(CLI_VERSION2);
3876
4242
  printStep("Signed in as " + session.user.login, "ok");
3877
4243
  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);
4244
+ let data;
4245
+ for (let failures = 1; ; failures++) {
4246
+ let failure;
4247
+ try {
4248
+ const res = await fetch(`${API_BASE3}/v1/instances/my/instances`, { headers: { Authorization: `Bearer ${session.token}` } });
4249
+ if (res.ok) {
4250
+ data = await res.json();
4251
+ break;
4252
+ }
4253
+ failure = `HTTP ${res.status}`;
4254
+ } catch (e) {
4255
+ failure = e instanceof Error ? e.message : String(e);
4256
+ }
4257
+ logUpdateStep(`start: fetching instances failed (${failure})${unattended ? "" : " \u2014 exiting"}`);
4258
+ if (!unattended) {
4259
+ printStep("Failed to fetch instances: " + failure, "fail");
4260
+ process.exit(1);
4261
+ }
4262
+ const wait = Math.min(5 * 6e4, 5e3 * 2 ** (failures - 1));
4263
+ printStep(`Failed to fetch instances (${failure}) \u2014 retrying in ${Math.round(wait / 1e3)}s`, "wait");
4264
+ await new Promise((resolve5) => setTimeout(resolve5, wait));
3884
4265
  }
3885
- const data = await res.json();
3886
4266
  const active = (data.instances || []).filter((i) => i.status === "active");
3887
4267
  let instances = active;
3888
4268
  let elsewhere = [];
@@ -3930,9 +4310,10 @@ var upCommand = new Command10("up").description("Start the browser runner for al
3930
4310
  state.activeInstance = instances.length === 1 ? instances[0].name || instances[0].slug || instances[0].id.slice(0, 8) : `${instances.length} agents`;
3931
4311
  printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
3932
4312
  const lockFile = lockPath(session.user.login);
3933
- if (opts.background && process.env[BACKGROUND_RESTART_ENV] === "1") await waitUntilTakeable(() => inspectHolder(lockFile));
4313
+ if (unattended || opts.background && process.env[BACKGROUND_RESTART_ENV] === "1") await waitUntilTakeable(() => inspectHolder(lockFile), { timeoutMs: 6e4 });
3934
4314
  const held = await inspectHolder(lockFile);
3935
4315
  if (held.lock && !isTakeable(held.verdict)) {
4316
+ logUpdateStep(`start: the lock is held by pid ${held.lock.pid} (${held.verdict})${opts.replace ? " \u2014 replacing it" : " \u2014 exiting"}`);
3936
4317
  if (!opts.replace) {
3937
4318
  clearScreen();
3938
4319
  printLogo(CLI_VERSION2);
@@ -3950,7 +4331,7 @@ var upCommand = new Command10("up").description("Start the browser runner for al
3950
4331
  }
3951
4332
  for (const line of replaced.lines) printStep(line, "ok");
3952
4333
  }
3953
- const { spawn: spawn5 } = await import("child_process");
4334
+ const { spawn: spawn6 } = await import("child_process");
3954
4335
  const cliPath = process.argv[1];
3955
4336
  const args = [cliPath, "runner", "connect", ...instances.map((i) => i.id)];
3956
4337
  if (opts.headless) args.push("--headless");
@@ -3960,7 +4341,7 @@ var upCommand = new Command10("up").description("Start the browser runner for al
3960
4341
  const code = await startInBackground({ session, opts, runnerArgs: args, instances: state.instances, lockFile, watching: !opts.instance });
3961
4342
  process.exit(code);
3962
4343
  }
3963
- const child = spawn5(process.execPath, args, {
4344
+ const child = spawn6(process.execPath, args, {
3964
4345
  stdio: ["ignore", "pipe", "pipe"],
3965
4346
  // The child is the lock HOLDER (#896): it owns the relay sockets, so it is what a
3966
4347
  // contender must find and what `--replace` must be able to stop.
@@ -4036,31 +4417,29 @@ var upCommand = new Command10("up").description("Start the browser runner for al
4036
4417
  void deliverCrashReports({ apiBase: API_BASE3, token: session.token, account: session.user.login, paths: crashes }).catch(() => void 0);
4037
4418
  };
4038
4419
  child.on("exit", (code, signal) => {
4039
- if (code === RUNNER_RESTART_EXIT_CODE) {
4420
+ const action = runnerExitAction(code, stopping);
4421
+ if (action === "restart") {
4422
+ logUpdateStep("restart: runner connect asked for a restart (exit 75)");
4040
4423
  restartUp("Runner updated remotely \u2014 restarting pags up on the new version\u2026", { PAGS_NO_PROMPT: "1" });
4041
4424
  return;
4042
4425
  }
4043
4426
  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);
4427
+ logUpdateStep(`runner connect ${signal ? `was killed by ${signal}` : `exited ${code}`}${stopping ? " (stopping)" : ""}`);
4428
+ if (action === "ignore") return;
4429
+ if (action === "stop") {
4430
+ clearScreen();
4431
+ writeLine(" Runner stopped (pags down, or replaced by another pags up).");
4432
+ process.exit(0);
4057
4433
  }
4434
+ recordConnectExit(code, signal ?? null, logs.slice(-20));
4435
+ const recent = logs.slice(-5).at(-1);
4436
+ 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
4437
  });
4059
- function restartUp(why, env = {}) {
4438
+ function restartUp(why, env = {}, exitCode) {
4060
4439
  writeLine(` ${why}`);
4061
4440
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
4062
4441
  process.stdin.pause();
4063
- restartPagsUp(restartUpArgs(opts), env, { ...processRestartDeps(), log: (line) => writeLine(` ${line}`) });
4442
+ restartPagsUp(restartUpArgs(opts), env, { ...processRestartDeps(), log: (line) => writeLine(` ${line}`) }, { exitCode });
4064
4443
  process.exit(1);
4065
4444
  }
4066
4445
  const shutdown = () => {
@@ -4125,6 +4504,26 @@ var downCommand = new Command10("down").description("Stop the browser runner and
4125
4504
  }
4126
4505
  writeLine("");
4127
4506
  });
4507
+ function serviceContext() {
4508
+ const user = (() => {
4509
+ try {
4510
+ return userInfo().username;
4511
+ } catch {
4512
+ return process.env.USER || process.env.USERNAME || void 0;
4513
+ }
4514
+ })();
4515
+ return {
4516
+ platform: process.platform,
4517
+ home: homedir5(),
4518
+ nodePath: process.execPath,
4519
+ script: process.argv[1],
4520
+ configDir: configDir(),
4521
+ pathEnv: process.env.PATH ?? "",
4522
+ uid: process.getuid?.(),
4523
+ user,
4524
+ domain: process.env.USERDOMAIN
4525
+ };
4526
+ }
4128
4527
 
4129
4528
  // src/index.ts
4130
4529
  var require2 = createRequire3(import.meta.url);