@proagentstore/cli 0.4.150 → 0.4.153

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.
Files changed (2) hide show
  1. package/dist/index.js +315 -141
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -381,11 +381,61 @@ jobs:
381
381
  });
382
382
 
383
383
  // src/commands/login.ts
384
+ import { spawn } from "child_process";
384
385
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
385
386
  import { createServer } from "http";
386
- import { homedir } from "os";
387
+ import { homedir, hostname } from "os";
387
388
  import { join as join3 } from "path";
388
389
  import { Command as Command3 } from "commander";
390
+
391
+ // src/commands/login-device.ts
392
+ function chooseLoginFlow(opts, env = process.env, platform = process.platform) {
393
+ if (opts.device) return { flow: "device", why: "--device" };
394
+ if (opts.browser) return { flow: "browser", why: "--browser" };
395
+ if (env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT) return { flow: "device", why: "this is an SSH session" };
396
+ if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) return { flow: "device", why: "there is no display here" };
397
+ return { flow: "browser", why: "" };
398
+ }
399
+ function sessionExpiresAt(token) {
400
+ try {
401
+ const payload = JSON.parse(Buffer.from(token.split(".")[0] ?? "", "base64url").toString("utf8"));
402
+ return typeof payload.exp === "number" && Number.isFinite(payload.exp) ? payload.exp * 1e3 : null;
403
+ } catch {
404
+ return null;
405
+ }
406
+ }
407
+ async function deviceLogin(deps) {
408
+ const fetchFn = deps.fetch ?? fetch;
409
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
410
+ const now = deps.now ?? Date.now;
411
+ const post = (path, body) => fetchFn(`${deps.apiBase}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
412
+ const res = await post("/v1/auth/device/start", { node: deps.node });
413
+ if (!res.ok) throw new Error(`Could not start a device sign-in (HTTP ${res.status}).`);
414
+ const started = await res.json();
415
+ deps.log("To sign this machine in, open this link on your phone or any device where you are signed in to ProAgentStore:");
416
+ deps.log(` ${started.verification_uri_complete}`);
417
+ deps.log(`or go to ${started.verification_uri} and enter the code: ${started.user_code}`);
418
+ deps.log(`Check that the page shows this machine (${deps.node}) and this code. Waiting for approval \u2014 the code lasts ${Math.round(started.expires_in / 60)} minutes\u2026`);
419
+ let interval = Math.max(1, started.interval) * 1e3;
420
+ const deadline = now() + started.expires_in * 1e3;
421
+ while (now() < deadline) {
422
+ await sleep2(interval);
423
+ let answer;
424
+ try {
425
+ const r = await post("/v1/auth/device/token", { device_code: started.device_code });
426
+ answer = await r.json().catch(() => ({}));
427
+ if (r.ok && answer.session) return answer.session;
428
+ } catch {
429
+ continue;
430
+ }
431
+ if (answer.error === "slow_down") interval += 5e3;
432
+ else if (answer.error === "access_denied") throw new Error("The sign-in was denied on the approval page.");
433
+ else if (answer.error === "expired_token") break;
434
+ }
435
+ throw new Error("The code expired before it was approved. Run `pags login --device` again for a new one.");
436
+ }
437
+
438
+ // src/commands/login.ts
389
439
  var CONFIG_DIR = join3(homedir(), ".config", "proagentstore");
390
440
  var TOKEN_FILE = join3(CONFIG_DIR, "session.json");
391
441
  var API_BASE = "https://api.proagentstore.online";
@@ -406,6 +456,14 @@ function saveSession(session) {
406
456
  mkdirSync2(CONFIG_DIR, { recursive: true });
407
457
  writeFileSync2(TOKEN_FILE, JSON.stringify(session, null, 2));
408
458
  }
459
+ async function storeSession(token) {
460
+ const meRes = await fetch(`${API_BASE}/v1/auth/me`, { headers: { Authorization: `Bearer ${token}` } });
461
+ if (!meRes.ok) throw new Error("Session validation failed");
462
+ const me = await meRes.json();
463
+ const session = { token, user: { id: me.id, login: me.login, avatar: me.avatar }, expiresAt: sessionExpiresAt(token) ?? Date.now() + 29 * 24 * 36e5 };
464
+ saveSession(session);
465
+ return session;
466
+ }
409
467
  function requireSession() {
410
468
  const session = loadSession();
411
469
  if (!session) {
@@ -414,9 +472,33 @@ function requireSession() {
414
472
  }
415
473
  return session;
416
474
  }
417
- var loginCommand = new Command3("login").description("Sign in with Google or GitHub (opens browser)").option("--github", "Use GitHub instead of Google").action(async (opts) => {
475
+ function browserCommand(url, platform = process.platform) {
476
+ if (platform === "win32") return { file: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
477
+ return { file: platform === "darwin" ? "open" : "xdg-open", args: [url] };
478
+ }
479
+ function openBrowser(url) {
480
+ const { file, args } = browserCommand(url);
481
+ try {
482
+ const child = spawn(file, args, { stdio: "ignore", detached: true, windowsHide: true });
483
+ child.on("error", () => void 0);
484
+ child.unref();
485
+ } catch {
486
+ }
487
+ }
488
+ var loginCommand = new Command3("login").description("Sign in with Google or GitHub \u2014 in a browser here, or approved from another device over SSH/headless").option("--github", "Use GitHub instead of Google").option("--device", "Sign in from another device: show a link and a code to approve on your phone (SSH, no display)").option("--browser", "Open a browser on this machine, even over SSH").action(async (opts) => {
489
+ const choice = chooseLoginFlow(opts);
490
+ if (choice.flow === "device") {
491
+ if (choice.why !== "--device") writeLine(`Signing in from another device, because ${choice.why} (use --browser to open one here instead).`);
492
+ try {
493
+ const session = await storeSession(await deviceLogin({ apiBase: API_BASE, node: hostname(), log: writeLine }));
494
+ writeLine(`Signed in as ${session.user.login}. Token stored at ${TOKEN_FILE}`);
495
+ } catch (err) {
496
+ writeError(err instanceof Error ? err.message : String(err));
497
+ process.exit(1);
498
+ }
499
+ return;
500
+ }
418
501
  const provider = opts.github ? "github" : "google";
419
- writeLine(`Opening browser for ${provider} sign-in...`);
420
502
  const port = await findFreePort();
421
503
  const callbackUrl = `http://127.0.0.1:${port}/callback`;
422
504
  const tokenPromise = new Promise((resolve5, reject) => {
@@ -441,17 +523,7 @@ var loginCommand = new Command3("login").description("Sign in with Google or Git
441
523
  return;
442
524
  }
443
525
  try {
444
- const meRes = await fetch(`${API_BASE}/v1/auth/me`, {
445
- headers: { Authorization: `Bearer ${session}` }
446
- });
447
- if (!meRes.ok) throw new Error("Session validation failed");
448
- const me = await meRes.json();
449
- saveSession({
450
- token: session,
451
- user: { id: me.id, login: me.login, avatar: me.avatar },
452
- expiresAt: Date.now() + 29 * 24 * 60 * 60 * 1e3
453
- // ~29 days
454
- });
526
+ const me = (await storeSession(session)).user;
455
527
  res.writeHead(200, { "Content-Type": "text/html" });
456
528
  res.end(`<h1>Signed in as ${me.login}</h1><p>You can close this tab and return to the terminal.</p><script>window.close()</script>`);
457
529
  clearTimeout(timeout);
@@ -469,9 +541,9 @@ var loginCommand = new Command3("login").description("Sign in with Google or Git
469
541
  });
470
542
  const oauthBase = provider === "google" ? `${API_BASE}/v1/auth/google/start` : `${API_BASE}/v1/auth/github/start`;
471
543
  const oauthUrl = `${oauthBase}?return_to=${encodeURIComponent(callbackUrl)}`;
472
- const open = (await import("child_process")).exec;
473
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
474
- open(`${cmd} "${oauthUrl}"`);
544
+ writeLine(`Opening your browser for ${provider} sign-in. If it does not open, paste this URL into a browser on this machine:`);
545
+ writeLine(` ${oauthUrl}`);
546
+ openBrowser(oauthUrl);
475
547
  try {
476
548
  await tokenPromise;
477
549
  const session = loadSession();
@@ -519,7 +591,7 @@ import { Command as Command4 } from "commander";
519
591
  // src/machine.ts
520
592
  import { randomUUID } from "crypto";
521
593
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
522
- import { homedir as homedir2, hostname } from "os";
594
+ import { homedir as homedir2, hostname as hostname2 } from "os";
523
595
  import { join as join4 } from "path";
524
596
  var CONFIG_DIR2 = join4(homedir2(), ".config", "proagentstore");
525
597
  var MACHINE_FILE = join4(CONFIG_DIR2, "machine.json");
@@ -594,7 +666,7 @@ function saveMachineIdentity(identity2) {
594
666
  function sameIdentity(a, b) {
595
667
  return a.id === b.id && a.names.join("\0") === b.names.join("\0") && (a.declined ?? []).join("\0") === (b.declined ?? []).join("\0");
596
668
  }
597
- function loadMachineIdentity(now = hostname()) {
669
+ function loadMachineIdentity(now = hostname2()) {
598
670
  let stored = null;
599
671
  try {
600
672
  if (existsSync4(MACHINE_FILE)) stored = parseMachineFile(readFileSync3(MACHINE_FILE, "utf-8"));
@@ -802,7 +874,7 @@ async function defaultAsk(question) {
802
874
  }
803
875
 
804
876
  // src/commands/runner/http.ts
805
- import { hostname as hostname2 } from "os";
877
+ import { hostname as hostname3 } from "os";
806
878
  function clean(value) {
807
879
  const trimmed = value?.trim();
808
880
  return trimmed || void 0;
@@ -828,7 +900,7 @@ function apiPathSegment(value) {
828
900
  return encodeURIComponent(value);
829
901
  }
830
902
  function buildRuntimeRegistrationBody(opts, capabilities = []) {
831
- const node = hostname2();
903
+ const node = hostname3();
832
904
  const machine = loadMachineIdentity(node);
833
905
  return {
834
906
  endpointUrl: clean(opts.endpointUrl) || opts.endpointUrl,
@@ -990,14 +1062,14 @@ var unclaimCommand = new Command4("unclaim").description("Remove a mistaken mach
990
1062
  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);
991
1063
 
992
1064
  // src/commands/mcp.ts
993
- import { spawn } from "child_process";
1065
+ import { spawn as spawn2 } from "child_process";
994
1066
  import { Command as Command5 } from "commander";
995
1067
  var DEFAULT_MCP_URL = "https://mcp.proagentstore.online/mcp";
996
1068
  function buildMcpRemoteArgs(opts, extraArgs = []) {
997
1069
  return ["-y", "mcp-remote", opts.url || DEFAULT_MCP_URL, ...extraArgs];
998
1070
  }
999
1071
  async function runMcpProxy(opts, extraArgs = []) {
1000
- const child = spawn("npx", buildMcpRemoteArgs(opts, extraArgs), {
1072
+ const child = spawn2("npx", buildMcpRemoteArgs(opts, extraArgs), {
1001
1073
  stdio: "inherit",
1002
1074
  env: process.env
1003
1075
  });
@@ -1015,7 +1087,7 @@ var mcpCommand = new Command5("mcp").description("Run a local stdio proxy for th
1015
1087
 
1016
1088
  // src/commands/remote-auth.ts
1017
1089
  import { Command as Command6 } from "commander";
1018
- import { hostname as hostname3 } from "os";
1090
+ import { hostname as hostname4 } from "os";
1019
1091
  var CONSOLE = "https://proagentstore.online/console";
1020
1092
  var FIELDS = ["request_id", "agent", "machine", "resource", "scopes", "approval_url", "expires_at"];
1021
1093
  var SECRET_KEY = /verifier|proof|private|secret|token|password|credential|ciphertext|session|cookie|^code$/i;
@@ -1041,7 +1113,7 @@ function buildRelayBody(input, identity2) {
1041
1113
  }
1042
1114
  function identity() {
1043
1115
  const id = loadMachineIdentity();
1044
- return { id: id.id, node: id.names[0] ?? hostname3() };
1116
+ return { id: id.id, node: id.names[0] ?? hostname4() };
1045
1117
  }
1046
1118
  async function readStdin() {
1047
1119
  const chunks = [];
@@ -1180,12 +1252,12 @@ var publishCommand = new Command7("publish").description("Publish an agent to Pr
1180
1252
  });
1181
1253
 
1182
1254
  // src/commands/runner/command.ts
1183
- import { spawn as spawn4 } from "child_process";
1255
+ import { spawn as spawn5 } from "child_process";
1184
1256
  import { randomUUID as randomUUID4 } from "crypto";
1185
1257
  import { Command as Command8 } from "commander";
1186
1258
 
1187
1259
  // src/commands/runner/process.ts
1188
- import { spawn as spawn2 } from "child_process";
1260
+ import { spawn as spawn3 } from "child_process";
1189
1261
  import { existsSync as existsSync6 } from "fs";
1190
1262
  import { resolve as resolve4, win32 } from "path";
1191
1263
  import { createServer as createServer2 } from "net";
@@ -1260,7 +1332,7 @@ function runnerSpawnSpec(opts) {
1260
1332
  function startRunnerForeground(opts) {
1261
1333
  const spec = runnerSpawnSpec(opts);
1262
1334
  return new Promise((resolvePromise, reject) => {
1263
- const child = spawn2(spec.command, spec.args, {
1335
+ const child = spawn3(spec.command, spec.args, {
1264
1336
  cwd: spec.cwd,
1265
1337
  stdio: "inherit",
1266
1338
  shell: spec.shell
@@ -1288,7 +1360,7 @@ async function waitForLocalRunner(opts, timeoutMs = 15e3) {
1288
1360
  }
1289
1361
 
1290
1362
  // src/commands/runner/relay.ts
1291
- import { hostname as hostname4 } from "os";
1363
+ import { hostname as hostname5 } from "os";
1292
1364
 
1293
1365
  // src/commands/runner/resources.ts
1294
1366
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, statfsSync, writeFileSync as writeFileSync4 } from "fs";
@@ -1336,7 +1408,7 @@ function sampleResources(read = os, now = Date.now(), extra = {}) {
1336
1408
  }
1337
1409
 
1338
1410
  // src/commands/runner/self-update.ts
1339
- import { spawn as spawn3 } from "child_process";
1411
+ import { spawn as spawn4 } from "child_process";
1340
1412
  var RUNNER_UPDATE_PATH = "/pags/runner/update";
1341
1413
  var RUNNER_RESTART_PATH = "/pags/runner/restart";
1342
1414
  var RUNNER_RESTART_EXIT_CODE = 75;
@@ -1367,7 +1439,7 @@ function deferredUpdateLease(value, now = Date.now()) {
1367
1439
  function deferredUpdateLeaseActive(lease, now = Date.now()) {
1368
1440
  return now < lease.expiresAt;
1369
1441
  }
1370
- function leaveForRestart(restarter, deps = { spawn: spawn3, exit: (code) => process.exit(code) }) {
1442
+ function leaveForRestart(restarter, deps = { spawn: spawn4, exit: (code) => process.exit(code) }) {
1371
1443
  logUpdateStep(`restart: leaving for the ${restarter} restarter (${restarter === "command" ? "restart command started, exit 0" : `exit ${RUNNER_RESTART_EXIT_CODE}`})`);
1372
1444
  if (restarter === "command") {
1373
1445
  deps.spawn(process.env[RESTART_COMMAND_ENV], { shell: true, detached: true, stdio: "ignore", env: process.env }).unref();
@@ -2395,14 +2467,109 @@ function releaseTrial(version2, cell, log, env = process.env, root = payloadRoot
2395
2467
  };
2396
2468
  }
2397
2469
 
2470
+ // src/commands/up-restart.ts
2471
+ import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
2472
+ var UP_LAUNCHER_ENV = "PAGS_UP_LAUNCHER";
2473
+ var UP_LAUNCHER_PID_ENV = "PAGS_UP_LAUNCHER_PID";
2474
+ var RUNNER_DIED_EXIT_CODE = 76;
2475
+ function upRestartMode(env, parent = { pid: process.ppid, alive: pidAlive }) {
2476
+ if (env[SERVICE_ENV] === "1") return "service";
2477
+ if (env[RESTART_COMMAND_ENV]?.trim()) return "command";
2478
+ const launcher = Number(env[UP_LAUNCHER_PID_ENV]);
2479
+ if (env[UP_LAUNCHER_ENV] === "1" && launcher === parent.pid && parent.alive(launcher)) return "launcher-child";
2480
+ return "launcher";
2481
+ }
2482
+ function runnerExitAction(code, stopping) {
2483
+ if (code === RUNNER_RESTART_EXIT_CODE) return "restart";
2484
+ if (stopping) return "ignore";
2485
+ return code === 0 ? "stop" : "died";
2486
+ }
2487
+ function restartOwner(env = process.env, alive = pidAlive) {
2488
+ if (env[SERVICE_ENV] === "1") return "service";
2489
+ return env[UP_LAUNCHER_ENV] === "1" && alive(Number(env[UP_LAUNCHER_PID_ENV])) ? "launcher" : "none";
2490
+ }
2491
+ function relaunchDelayMs(failures) {
2492
+ return Math.min(5 * 6e4, 1e4 * 2 ** Math.max(0, failures - 1));
2493
+ }
2494
+ var RAN_MS = 6e4;
2495
+ var RESET_MS = 10 * 6e4;
2496
+ function processRestartDeps() {
2497
+ return {
2498
+ env: process.env,
2499
+ execPath: process.execPath,
2500
+ execArgv: process.execArgv,
2501
+ script: process.argv[1],
2502
+ spawn: nodeSpawn,
2503
+ spawnSync: nodeSpawnSync,
2504
+ exit: (code) => process.exit(code),
2505
+ log: () => {
2506
+ },
2507
+ sleep: (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms),
2508
+ step: (line) => logUpdateStep(line),
2509
+ parent: { pid: process.ppid, alive: pidAlive },
2510
+ pid: process.pid,
2511
+ now: Date.now
2512
+ };
2513
+ }
2514
+ function restartPagsUp(args, extraEnv, deps, opts = {}) {
2515
+ const env = { ...deps.env, ...extraEnv };
2516
+ const argv = [...deps.execArgv, deps.script, ...args];
2517
+ const exitCode = opts.exitCode ?? RUNNER_RESTART_EXIT_CODE;
2518
+ const mode = upRestartMode(env, deps.parent);
2519
+ deps.step(`${opts.initial ? "start" : "restart"}: pags ${args.join(" ")} \u2014 ${opts.initial ? "as" : "leaving via"} ${mode}${exitCode === RUNNER_DIED_EXIT_CODE ? " (the runner died)" : ""}`);
2520
+ if (mode === "service") {
2521
+ deps.exit(exitCode);
2522
+ return;
2523
+ }
2524
+ if (mode === "command") {
2525
+ leaveForRestart(mode, { spawn: deps.spawn, exit: deps.exit });
2526
+ return;
2527
+ }
2528
+ if (mode === "launcher-child") {
2529
+ deps.exit(exitCode);
2530
+ return;
2531
+ }
2532
+ const childEnv = { ...env, [UP_LAUNCHER_ENV]: "1", [UP_LAUNCHER_PID_ENV]: String(deps.pid) };
2533
+ let failures = 0;
2534
+ for (let first = true; ; first = false) {
2535
+ const unattended = !first || env.PAGS_NO_PROMPT === "1";
2536
+ deps.step(`launcher: starting pags up${unattended ? " (unattended)" : ""}`);
2537
+ const startedAt = deps.now();
2538
+ const r = deps.spawnSync(deps.execPath, argv, { stdio: "inherit", env: first ? childEnv : { ...childEnv, PAGS_NO_PROMPT: "1" } });
2539
+ const ran = deps.now() - startedAt;
2540
+ const how = r.error ? `could not start (${r.error.message})` : r.signal ? `was killed by ${r.signal}` : `exited ${r.status}`;
2541
+ deps.step(`launcher: pags up ${how} after ${Math.round(ran / 1e3)}s`);
2542
+ if (ran >= RESET_MS) failures = 0;
2543
+ if (r.status === RUNNER_RESTART_EXIT_CODE) {
2544
+ failures = 0;
2545
+ continue;
2546
+ }
2547
+ if (!r.error && (r.status === 0 || r.signal === "SIGINT")) {
2548
+ deps.exit(0);
2549
+ return;
2550
+ }
2551
+ if (unattended || r.status === RUNNER_DIED_EXIT_CODE || ran >= RAN_MS) {
2552
+ failures++;
2553
+ const wait = relaunchDelayMs(failures);
2554
+ deps.log(`pags up ${how} \u2014 starting it again in ${Math.round(wait / 1e3)}s (attempt ${failures + 1}).`);
2555
+ deps.step(`launcher: retrying in ${Math.round(wait / 1e3)}s (failure ${failures})`);
2556
+ deps.sleep(wait);
2557
+ continue;
2558
+ }
2559
+ if (!opts.initial) deps.log(`Restart failed${typeof r.status === "number" ? ` (exit ${r.status})` : ""} \u2014 the runner is NOT running. Run 'pags up' again.`);
2560
+ deps.exit(typeof r.status === "number" && r.status !== 0 ? r.status : 1);
2561
+ return;
2562
+ }
2563
+ }
2564
+
2398
2565
  // src/commands/runner/relay.ts
2399
2566
  var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
2400
2567
  var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH, RUNNER_RESTART_PATH]);
2401
2568
  async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false, watchInstances = false) {
2402
2569
  const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
2403
- const pagsToken = clean(opts.pagsToken) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
2404
- if (!pagsToken) throw new Error("PAGS token required for WebSocket relay");
2405
- const runnerNode = hostname4();
2570
+ const pagsToken = () => clean(opts.pagsToken) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
2571
+ if (!pagsToken()) throw new Error("PAGS token required for WebSocket relay");
2572
+ const runnerNode = hostname5();
2406
2573
  const machine = loadMachineIdentity(runnerNode);
2407
2574
  const autoUpdateKey = machine.id ? autoUpdatePolicyKey(machine.id) : "";
2408
2575
  const cell = new PolicyCell(autoUpdateKey ? loadAutoUpdatePolicy(autoUpdateKey) ?? { autoUpdate: false } : { autoUpdate: false }, (p) => saveAutoUpdatePolicy(autoUpdateKey, p), !!autoUpdateKey);
@@ -2598,7 +2765,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2598
2765
  const plan = reattachPlan(request, { held: attached.has(named), blocked: blocked.has(named), watching: watchInstances, scope: instanceIds });
2599
2766
  if (plan.refuse) return { status: 409, result: { error: plan.refuse } };
2600
2767
  if (plan.release) {
2601
- const res = await requestPags("GET", "/v1/instances/my/instances", { ...opts, pagsToken });
2768
+ const res = await requestPags("GET", "/v1/instances/my/instances", { ...opts, pagsToken: pagsToken() });
2602
2769
  const released = pinnedAway(attached.keys(), res.instances ?? [], runnerNode, machine.names);
2603
2770
  for (const id of released) detach(id, `${id.slice(0, 8)}\u2026 (pinned to another machine now)`);
2604
2771
  if (released.length && attached.size === 0) writeLine("This `pags up --instance` holds no agent now \u2014 close it, or restart it without --instance.");
@@ -2619,7 +2786,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2619
2786
  };
2620
2787
  const attach = (id, label = `${id.slice(0, 8)}\u2026`) => {
2621
2788
  if (attached.has(id)) return;
2622
- const mintToken = () => requestPags("POST", `/v1/relay/${apiPathSegment(id)}/token`, { ...opts, pagsToken }, {}).then((r) => r.token);
2789
+ const mintToken = () => requestPags("POST", `/v1/relay/${apiPathSegment(id)}/token`, { ...opts, pagsToken: pagsToken() }, {}).then((r) => r.token);
2623
2790
  attached.set(
2624
2791
  id,
2625
2792
  openRelaySocket(
@@ -2670,7 +2837,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2670
2837
  writeLine(startup.state === "ok" ? `Runtime registered with PAGS \u2713 (${startup.agents} agents)` : `Runtime registration incomplete: ${startup.agents} agents \u2014 retried on each relay (re)connect${watchInstances ? " and every 20s while this runs" : ""}.`);
2671
2838
  writeLine("");
2672
2839
  writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2673
- writeLine(` \u2705 CONNECTED \u2014 WebSocket relay \xB7 ${hostname4()}`);
2840
+ writeLine(` \u2705 CONNECTED \u2014 WebSocket relay \xB7 ${hostname5()}`);
2674
2841
  writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"}`);
2675
2842
  writeLine(" No cloudflared needed. Ctrl+C to disconnect.");
2676
2843
  writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
@@ -2690,7 +2857,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2690
2857
  // #896: who is heartbeating. Set by `runner connect` from the lock it took.
2691
2858
  ...process.env.PAGS_RUNNER_RSID ? { rsid: process.env.PAGS_RUNNER_RSID } : {},
2692
2859
  pid: process.pid,
2693
- ...process.env.PAGS_RUNNER_LAUNCH ? { launch: process.env.PAGS_RUNNER_LAUNCH } : {}
2860
+ ...process.env.PAGS_RUNNER_LAUNCH ? { launch: process.env.PAGS_RUNNER_LAUNCH } : {},
2861
+ supervisor: restartOwner(),
2862
+ // #1154: what starts it again if it dies
2863
+ sessionExpiresAt: sessionExpiresAt(pagsToken() ?? "") ?? void 0
2864
+ // #1157: when this runner signs out
2694
2865
  },
2695
2866
  relayRttMs,
2696
2867
  sessions: sessions ?? void 0
@@ -2729,7 +2900,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2729
2900
  const free = await requestPags(
2730
2901
  "GET",
2731
2902
  `/v1/relay/${apiPathSegment(id)}/status`,
2732
- { ...opts, pagsToken }
2903
+ { ...opts, pagsToken: pagsToken() }
2733
2904
  ).then((r) => r.connected === false).catch(() => false);
2734
2905
  if (!free) continue;
2735
2906
  blocked.delete(id);
@@ -2742,7 +2913,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2742
2913
  const res = await requestPags(
2743
2914
  "GET",
2744
2915
  "/v1/instances/my/instances",
2745
- { ...opts, pagsToken }
2916
+ { ...opts, pagsToken: pagsToken() }
2746
2917
  );
2747
2918
  const { attach: toAttach, detach: toDetach } = diffMembership(
2748
2919
  attached.keys(),
@@ -2793,7 +2964,7 @@ async function readSessionResources(localUrl, runnerToken) {
2793
2964
  }
2794
2965
  var RTT_PROBE_PREFIX = "rtt:";
2795
2966
  var RTT_ECHO_PREFIX = "rtt-echo:";
2796
- function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen, onControl, runnerNode = hostname4(), beginMutation = () => () => void 0) {
2967
+ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen, onControl, runnerNode = hostname5(), beginMutation = () => () => void 0) {
2797
2968
  let backoffMs = 1e3;
2798
2969
  let reconnecting = false;
2799
2970
  let closed = false;
@@ -3232,8 +3403,50 @@ function recordChildExit(o) {
3232
3403
  return report;
3233
3404
  }
3234
3405
 
3406
+ // src/session-refresh.ts
3407
+ var REFRESH_WITHIN_MS = 7 * 24 * 36e5;
3408
+ var WARN_WITHIN_MS = 3 * 24 * 36e5;
3409
+ var CHECK_EVERY_MS = 6 * 36e5;
3410
+ function refreshDecision(expiresAt, now) {
3411
+ if (expiresAt === null) return "unreadable";
3412
+ if (expiresAt <= now) return "expired";
3413
+ return expiresAt - now <= REFRESH_WITHIN_MS ? "refresh" : "fresh";
3414
+ }
3415
+ var hoursLeft = (ms) => ms >= 48 * 36e5 ? `${Math.round(ms / (24 * 36e5))} days` : `${Math.max(1, Math.round(ms / 36e5))} hours`;
3416
+ async function checkSession(deps) {
3417
+ const now = (deps.now ?? Date.now)();
3418
+ const token = deps.token();
3419
+ if (!token) return { decision: "unreadable" };
3420
+ const expiresAt = sessionExpiresAt(token);
3421
+ const decision = refreshDecision(expiresAt, now);
3422
+ if (decision === "expired") {
3423
+ deps.warn("Session expired: this runner is signed out. Sign in again with `pags login` (`pags login --device` over SSH).");
3424
+ return { decision };
3425
+ }
3426
+ if (decision !== "refresh") return { decision };
3427
+ const left = expiresAt - now;
3428
+ try {
3429
+ const res = await (deps.fetch ?? fetch)(`${deps.apiBase}/v1/auth/session/refresh`, { method: "POST", headers: { Authorization: `Bearer ${token}` } });
3430
+ const body = await res.json().catch(() => ({}));
3431
+ if (!res.ok || !body.session) throw new Error(body.error || `HTTP ${res.status}`);
3432
+ deps.onRefreshed(body.session, token);
3433
+ const until = sessionExpiresAt(body.session);
3434
+ deps.log(`Session refreshed \u2014 signed in until ${until ? new Date(until).toISOString().slice(0, 10) : "the next 30 days"}.`);
3435
+ return { decision, refreshed: true };
3436
+ } catch (e) {
3437
+ const why = e instanceof Error ? e.message : String(e);
3438
+ if (left <= WARN_WITHIN_MS) deps.warn(`Session refresh failed (${why}): this runner signs out in ${hoursLeft(left)}. Sign in again with \`pags login\` (\`pags login --device\` over SSH).`);
3439
+ return { decision, refreshed: false };
3440
+ }
3441
+ }
3442
+ function startSessionRefresh(deps, every = setInterval) {
3443
+ const run = () => void checkSession(deps).catch(() => void 0);
3444
+ run();
3445
+ every(run, CHECK_EVERY_MS).unref?.();
3446
+ }
3447
+
3235
3448
  // src/commands/runner/command.ts
3236
- import { hostname as hostname5 } from "os";
3449
+ import { hostname as hostname6 } from "os";
3237
3450
  function collectCapability(value, previous = []) {
3238
3451
  return [...previous, value];
3239
3452
  }
@@ -3251,12 +3464,14 @@ function createRunnerCommand() {
3251
3464
  const lockAccount = clean(process.env.PAGS_LOCK_ACCOUNT) || "";
3252
3465
  let lockHeld = null;
3253
3466
  if (lockAccount) {
3254
- const taken = await acquireLock({ account: lockAccount, node: hostname5(), machineId: loadMachineIdentity().id || null, version: CLI_VERSION, port: Number(port), runnerToken });
3467
+ const taken = await acquireLock({ account: lockAccount, node: hostname6(), machineId: loadMachineIdentity().id || null, version: CLI_VERSION, port: Number(port), runnerToken });
3255
3468
  if (!taken.ok) {
3256
3469
  for (const line of taken.lock ? holderMessage(taken.lock, taken.verdict) : ["Another `pags up` holds this machine for this account."]) writeError(line);
3257
3470
  process.exit(1);
3258
3471
  }
3259
3472
  lockHeld = { path: taken.path, rsid: taken.lock.rsid, nonce: taken.lock.nonce, launch: taken.lock.launch };
3473
+ process.env.PAGS_RUNNER_RSID = taken.lock.rsid;
3474
+ process.env.PAGS_RUNNER_LAUNCH = taken.lock.launch;
3260
3475
  writeLine(`STATUS runner-session=${taken.lock.rsid}`);
3261
3476
  }
3262
3477
  const releaseHeldLock = () => {
@@ -3268,7 +3483,7 @@ function createRunnerCommand() {
3268
3483
  const spec = runnerSpawnSpec(runnerOpts);
3269
3484
  const crashAccount = lockAccount || loadSession()?.user.login || "";
3270
3485
  const crashes = crashAccount ? crashPaths(configDir(), crashAccount, opts.dataDir) : null;
3271
- const runner = spawn4(spec.command, spec.args, {
3486
+ const runner = spawn5(spec.command, spec.args, {
3272
3487
  cwd: spec.cwd,
3273
3488
  stdio: ["ignore", "pipe", "pipe"],
3274
3489
  shell: spec.shell,
@@ -3328,6 +3543,19 @@ function createRunnerCommand() {
3328
3543
  await connectViaRelay(instanceIds, localUrl, runnerToken, opts, Boolean(opts.force), Boolean(opts.watchInstances));
3329
3544
  void deliver().catch(() => void 0);
3330
3545
  setInterval(() => void deliver().catch(() => void 0), 5 * 6e4).unref();
3546
+ if (!clean(opts.pagsToken)) {
3547
+ startSessionRefresh({
3548
+ apiBase: pagsApiBase(opts.apiBase),
3549
+ token: () => clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token) || null,
3550
+ onRefreshed: (token, previous) => {
3551
+ process.env.PAGS_TOKEN = token;
3552
+ const stored = loadSession();
3553
+ if (stored?.token === previous) saveSession({ ...stored, token, expiresAt: sessionExpiresAt(token) ?? stored.expiresAt });
3554
+ },
3555
+ log: writeLine,
3556
+ warn: writeError
3557
+ });
3558
+ }
3331
3559
  await new Promise((resolvePromise) => {
3332
3560
  runner.on("exit", () => resolvePromise());
3333
3561
  });
@@ -3453,7 +3681,7 @@ import { readFileSync as readFileSync11, statSync as statSync2, watchFile } from
3453
3681
  import { Command as Command9 } from "commander";
3454
3682
 
3455
3683
  // src/background.ts
3456
- import { spawn as nodeSpawn } from "child_process";
3684
+ import { spawn as nodeSpawn2 } from "child_process";
3457
3685
  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";
3458
3686
  import { dirname as dirname5, join as join11 } from "path";
3459
3687
  var BACKGROUND_ENV = "PAGS_BACKGROUND";
@@ -3492,12 +3720,12 @@ function backgroundRestartCommand(execPath, script, upArgs, platform = process.p
3492
3720
  const argv = [execPath, script, ...upArgs, "--background"].map((a) => shellQuote(a, platform)).join(" ");
3493
3721
  return platform === "win32" ? `set ${BACKGROUND_RESTART_ENV}=1&& ${argv}` : `${BACKGROUND_RESTART_ENV}=1 ${argv}`;
3494
3722
  }
3495
- function spawnDetached(command, args, env, logPath, spawn5 = nodeSpawn) {
3723
+ function spawnDetached(command, args, env, logPath, spawn6 = nodeSpawn2) {
3496
3724
  mkdirSync8(dirname5(logPath), { recursive: true });
3497
3725
  rotateLog(logPath);
3498
3726
  const fd = openSync3(logPath, "a", 384);
3499
3727
  try {
3500
- const child = spawn5(command, args, { detached: true, stdio: ["ignore", fd, fd], env, windowsHide: true });
3728
+ const child = spawn6(command, args, { detached: true, stdio: ["ignore", fd, fd], env, windowsHide: true });
3501
3729
  child.unref();
3502
3730
  if (!child.pid) throw new Error("the runner process did not start");
3503
3731
  return child.pid;
@@ -3590,11 +3818,11 @@ var logsCommand = new Command9("logs").description("Show the background runner's
3590
3818
  // src/commands/up.ts
3591
3819
  import { createRequire as createRequire2 } from "module";
3592
3820
  import { Command as Command10 } from "commander";
3593
- import { homedir as homedir5, hostname as hostname7, userInfo } from "os";
3821
+ import { homedir as homedir5, hostname as hostname8, userInfo } from "os";
3594
3822
 
3595
3823
  // src/tui.ts
3596
3824
  import chalk from "chalk";
3597
- import { hostname as hostname6 } from "os";
3825
+ import { hostname as hostname7 } from "os";
3598
3826
  import readline from "readline";
3599
3827
  var ACCENT = "#7c3aed";
3600
3828
  var c = chalk.hex(ACCENT);
@@ -3647,7 +3875,7 @@ function printStatus(state) {
3647
3875
  clearScreen();
3648
3876
  printLogo(state.version);
3649
3877
  const connected = state.runner === "online" && state.tunnel === "online" && state.registration === "registered";
3650
- console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(hostname6()));
3878
+ console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(hostname7()));
3651
3879
  console.log("");
3652
3880
  const row = (kind, s, override) => {
3653
3881
  const { icon, label, note } = describe(kind, s);
@@ -3709,75 +3937,6 @@ async function waitForKey(keys, onInterrupt) {
3709
3937
  });
3710
3938
  }
3711
3939
 
3712
- // src/commands/up-restart.ts
3713
- import { spawn as nodeSpawn2, spawnSync as nodeSpawnSync } from "child_process";
3714
- var UP_LAUNCHER_ENV = "PAGS_UP_LAUNCHER";
3715
- function upRestartMode(env) {
3716
- if (env[SERVICE_ENV] === "1") return "service";
3717
- if (env[RESTART_COMMAND_ENV]?.trim()) return "command";
3718
- if (env[UP_LAUNCHER_ENV] === "1") return "launcher-child";
3719
- return "launcher";
3720
- }
3721
- function relaunchDelayMs(failures) {
3722
- return Math.min(5 * 6e4, 1e4 * 2 ** Math.max(0, failures - 1));
3723
- }
3724
- function processRestartDeps() {
3725
- return {
3726
- env: process.env,
3727
- execPath: process.execPath,
3728
- execArgv: process.execArgv,
3729
- script: process.argv[1],
3730
- spawn: nodeSpawn2,
3731
- spawnSync: nodeSpawnSync,
3732
- exit: (code) => process.exit(code),
3733
- log: () => {
3734
- },
3735
- sleep: (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms),
3736
- step: (line) => logUpdateStep(line)
3737
- };
3738
- }
3739
- function restartPagsUp(args, extraEnv, deps) {
3740
- const env = { ...deps.env, ...extraEnv };
3741
- const argv = [...deps.execArgv, deps.script, ...args];
3742
- const mode = upRestartMode(env);
3743
- deps.step(`restart: pags ${args.join(" ")} \u2014 leaving via ${mode}`);
3744
- if (mode === "service" || mode === "command") {
3745
- leaveForRestart(mode, { spawn: deps.spawn, exit: deps.exit });
3746
- return;
3747
- }
3748
- if (mode === "launcher-child") {
3749
- deps.exit(RUNNER_RESTART_EXIT_CODE);
3750
- return;
3751
- }
3752
- let failures = 0;
3753
- for (let first = true; ; first = false) {
3754
- const unattended = !first || env.PAGS_NO_PROMPT === "1";
3755
- deps.step(`launcher: starting pags up${unattended ? " (unattended)" : ""}`);
3756
- const r = deps.spawnSync(deps.execPath, argv, { stdio: "inherit", env: { ...env, [UP_LAUNCHER_ENV]: "1", ...first ? {} : { PAGS_NO_PROMPT: "1" } } });
3757
- const how = r.error ? `could not start (${r.error.message})` : r.signal ? `was killed by ${r.signal}` : `exited ${r.status}`;
3758
- deps.step(`launcher: pags up ${how}`);
3759
- if (r.status === RUNNER_RESTART_EXIT_CODE) {
3760
- failures = 0;
3761
- continue;
3762
- }
3763
- if (!r.error && r.status === 0) {
3764
- deps.exit(0);
3765
- return;
3766
- }
3767
- if (unattended) {
3768
- failures++;
3769
- const wait = relaunchDelayMs(failures);
3770
- deps.log(`pags up ${how} \u2014 starting it again in ${Math.round(wait / 1e3)}s (attempt ${failures + 1}).`);
3771
- deps.step(`launcher: retrying in ${Math.round(wait / 1e3)}s (failure ${failures})`);
3772
- deps.sleep(wait);
3773
- continue;
3774
- }
3775
- deps.log(`Restart failed${typeof r.status === "number" ? ` (exit ${r.status})` : ""} \u2014 the runner is NOT running. Run 'pags up' again.`);
3776
- deps.exit(typeof r.status === "number" && r.status !== 0 ? r.status : 1);
3777
- return;
3778
- }
3779
- }
3780
-
3781
3940
  // src/runner-replace.ts
3782
3941
  var REPLACE_EXIT_TIMEOUT_MS = 3e4;
3783
3942
  function describeWork(work) {
@@ -3987,6 +4146,15 @@ start();
3987
4146
  var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3988
4147
  var unitWord = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
3989
4148
  var ps = (s) => `'${s.replace(/'/g, "''")}'`;
4149
+ function versionManagedNode(nodePath) {
4150
+ if (/[\\/]n[\\/]versions[\\/]/.test(nodePath)) return "n";
4151
+ const m = /[\\/]\.?(nvm|fnm|volta|asdf|nodenv)[\\/]/i.exec(nodePath);
4152
+ return m ? m[1].toLowerCase() : null;
4153
+ }
4154
+ var pinnedNodeNote = (ctx) => {
4155
+ const manager = versionManagedNode(ctx.nodePath);
4156
+ 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.`] : [];
4157
+ };
3990
4158
  function servicePlan(ctx) {
3991
4159
  const supervisor = supervisorPath(ctx);
3992
4160
  const log = serviceLogPath(ctx);
@@ -4010,6 +4178,7 @@ function servicePlan(ctx) {
4010
4178
  <key>KeepAlive</key><true/>
4011
4179
  <key>ThrottleInterval</key><integer>30</integer>
4012
4180
  <key>ProcessType</key><string>Background</string>
4181
+ <key>WorkingDirectory</key><string>${xml(ctx.home)}</string>
4013
4182
  <key>StandardOutPath</key><string>${xml(log)}</string>
4014
4183
  <key>StandardErrorPath</key><string>${xml(log)}</string>
4015
4184
  </dict>
@@ -4025,6 +4194,7 @@ function servicePlan(ctx) {
4025
4194
  { file: "launchctl", args: ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`], why: "start it now" }
4026
4195
  ],
4027
4196
  notes: [
4197
+ ...pinnedNodeNote(ctx),
4028
4198
  `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.`,
4029
4199
  `Remove it with: pags up --uninstall-service`
4030
4200
  ]
@@ -4039,8 +4209,11 @@ function servicePlan(ctx) {
4039
4209
  $ErrorActionPreference = 'Stop'
4040
4210
  $action = New-ScheduledTaskAction -Execute ${ps(ctx.nodePath)} -Argument ${ps(`"${supervisor}"`)}
4041
4211
  $trigger = New-ScheduledTaskTrigger -AtStartup
4212
+ # Task Scheduler restarts a task that failed to START; a supervisor killed later just ends. Re-trigger it every five
4213
+ # minutes, forever \u2014 MultipleInstances IgnoreNew makes a trigger that finds it running do nothing (#1154).
4214
+ $trigger.Repetition = (New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5)).Repetition
4042
4215
  $principal = New-ScheduledTaskPrincipal -UserId ${ps(principal)} -LogonType S4U -RunLevel Limited
4043
- $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
4216
+ $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew
4044
4217
  Unregister-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Confirm:$false -ErrorAction SilentlyContinue
4045
4218
  Register-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
4046
4219
  Start-ScheduledTask -TaskName ${ps(WINDOWS_TASK)}
@@ -4051,6 +4224,7 @@ Start-ScheduledTask -TaskName ${ps(WINDOWS_TASK)}
4051
4224
  files,
4052
4225
  commands: [{ file: "powershell.exe", args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", task], why: "register and start the Scheduled Task" }],
4053
4226
  notes: [
4227
+ ...pinnedNodeNote(ctx),
4054
4228
  `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.`,
4055
4229
  `Remove it with: pags up --uninstall-service`
4056
4230
  ]
@@ -4085,7 +4259,7 @@ WantedBy=default.target
4085
4259
  { file: "systemctl", args: ["--user", "enable", "--now", SYSTEMD_UNIT], why: "enable and start it" },
4086
4260
  { file: "loginctl", args: ["enable-linger", ...ctx.user ? [ctx.user] : []], optional: true, why: "keep it running with nobody logged in" }
4087
4261
  ],
4088
- notes: [`systemd user unit ${unit}. Logs: ${log} (and journalctl --user -u ${SYSTEMD_UNIT}).`, `Remove it with: pags up --uninstall-service`]
4262
+ notes: [...pinnedNodeNote(ctx), `systemd user unit ${unit}. Logs: ${log} (and journalctl --user -u ${SYSTEMD_UNIT}).`, `Remove it with: pags up --uninstall-service`]
4089
4263
  };
4090
4264
  }
4091
4265
  function serviceRemovalPlan(ctx) {
@@ -4149,6 +4323,10 @@ var upCommand = new Command10("up").description("Start the browser runner for al
4149
4323
  for (const line of uninstallService(serviceContext()).lines) writeLine(line);
4150
4324
  return;
4151
4325
  }
4326
+ if (!opts.background && !opts.installService && upRestartMode(process.env) === "launcher") {
4327
+ restartPagsUp(process.argv.slice(2), {}, { ...processRestartDeps(), log: (line) => writeLine(` ${line}`) }, { initial: true });
4328
+ return;
4329
+ }
4152
4330
  const session = requireSession();
4153
4331
  if (opts.installService) {
4154
4332
  const result = installService(serviceContext());
@@ -4226,7 +4404,7 @@ var upCommand = new Command10("up").description("Start the browser runner for al
4226
4404
  headless: opts.headless
4227
4405
  }).catch(() => void 0);
4228
4406
  if (!opts.instance) {
4229
- const split = partitionByPin(instances, hostname7(), loadMachineIdentity(hostname7()).names);
4407
+ const split = partitionByPin(instances, hostname8(), loadMachineIdentity(hostname8()).names);
4230
4408
  instances = split.here;
4231
4409
  elsewhere = split.elsewhere;
4232
4410
  }
@@ -4269,7 +4447,7 @@ var upCommand = new Command10("up").description("Start the browser runner for al
4269
4447
  }
4270
4448
  for (const line of replaced.lines) printStep(line, "ok");
4271
4449
  }
4272
- const { spawn: spawn5 } = await import("child_process");
4450
+ const { spawn: spawn6 } = await import("child_process");
4273
4451
  const cliPath = process.argv[1];
4274
4452
  const args = [cliPath, "runner", "connect", ...instances.map((i) => i.id)];
4275
4453
  if (opts.headless) args.push("--headless");
@@ -4279,7 +4457,7 @@ var upCommand = new Command10("up").description("Start the browser runner for al
4279
4457
  const code = await startInBackground({ session, opts, runnerArgs: args, instances: state.instances, lockFile, watching: !opts.instance });
4280
4458
  process.exit(code);
4281
4459
  }
4282
- const child = spawn5(process.execPath, args, {
4460
+ const child = spawn6(process.execPath, args, {
4283
4461
  stdio: ["ignore", "pipe", "pipe"],
4284
4462
  // The child is the lock HOLDER (#896): it owns the relay sockets, so it is what a
4285
4463
  // contender must find and what `--replace` must be able to stop.
@@ -4339,7 +4517,7 @@ var upCommand = new Command10("up").description("Start the browser runner for al
4339
4517
  printStatus(state);
4340
4518
  continue;
4341
4519
  }
4342
- if (/error|Error|EADDRINUSE|ECONNREFUSED|failed/i.test(trimmed)) {
4520
+ if (/error|Error|EADDRINUSE|ECONNREFUSED|failed|expired/i.test(trimmed)) {
4343
4521
  state.lastEvent = trimmed.slice(0, 80);
4344
4522
  printStatus(state);
4345
4523
  }
@@ -4355,33 +4533,29 @@ var upCommand = new Command10("up").description("Start the browser runner for al
4355
4533
  void deliverCrashReports({ apiBase: API_BASE3, token: session.token, account: session.user.login, paths: crashes }).catch(() => void 0);
4356
4534
  };
4357
4535
  child.on("exit", (code, signal) => {
4358
- if (code === RUNNER_RESTART_EXIT_CODE) {
4536
+ const action = runnerExitAction(code, stopping);
4537
+ if (action === "restart") {
4359
4538
  logUpdateStep("restart: runner connect asked for a restart (exit 75)");
4360
4539
  restartUp("Runner updated remotely \u2014 restarting pags up on the new version\u2026", { PAGS_NO_PROMPT: "1" });
4361
4540
  return;
4362
4541
  }
4363
4542
  childDead = true;
4364
4543
  logUpdateStep(`runner connect ${signal ? `was killed by ${signal}` : `exited ${code}`}${stopping ? " (stopping)" : ""}`);
4365
- if (code !== 0 && !stopping) recordConnectExit(code, signal ?? null, logs.slice(-20));
4366
- if (code === null) {
4367
- state.runner = "error";
4368
- state.lastEvent = "Runner stopped by another process (signal) \u2014 it is not connected any more";
4369
- printStatus(state);
4370
- return;
4371
- }
4372
- if (code !== 0) {
4373
- state.runner = "error";
4374
- state.lastEvent = `Runner exited (code ${code})`;
4375
- const recent = logs.slice(-5);
4376
- if (recent.length) state.lastEvent += ": " + recent[recent.length - 1].slice(0, 60);
4377
- printStatus(state);
4544
+ if (action === "ignore") return;
4545
+ if (action === "stop") {
4546
+ clearScreen();
4547
+ writeLine(" Runner stopped (pags down, or replaced by another pags up).");
4548
+ process.exit(0);
4378
4549
  }
4550
+ recordConnectExit(code, signal ?? null, logs.slice(-20));
4551
+ const recent = logs.slice(-5).at(-1);
4552
+ 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);
4379
4553
  });
4380
- function restartUp(why, env = {}) {
4554
+ function restartUp(why, env = {}, exitCode) {
4381
4555
  writeLine(` ${why}`);
4382
4556
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
4383
4557
  process.stdin.pause();
4384
- restartPagsUp(restartUpArgs(opts), env, { ...processRestartDeps(), log: (line) => writeLine(` ${line}`) });
4558
+ restartPagsUp(restartUpArgs(opts), env, { ...processRestartDeps(), log: (line) => writeLine(` ${line}`) }, { exitCode });
4385
4559
  process.exit(1);
4386
4560
  }
4387
4561
  const shutdown = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.150",
3
+ "version": "0.4.153",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",