@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.
- package/dist/index.js +315 -141
- 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
|
-
|
|
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
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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] ??
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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,
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
4366
|
-
if (
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
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 = () => {
|