@proagentstore/cli 0.4.149 → 0.4.150

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/bootstrap.js CHANGED
@@ -2,13 +2,21 @@
2
2
  import {
3
3
  BOOTSTRAP_ENV,
4
4
  NO_SELF_UPDATE_ENV,
5
+ TRIAL_MS,
6
+ badVersions,
7
+ beginTrial,
5
8
  cachedPayloads,
6
9
  installPayload,
7
10
  latestPublishedVersion,
11
+ logUpdateStep,
12
+ markBad,
8
13
  newestPayload,
14
+ noteTrialStart,
9
15
  olderThan,
10
- payloadAt
11
- } from "./chunk-NNA7VZ6W.js";
16
+ payloadAt,
17
+ payloadRoot,
18
+ readTrial
19
+ } from "./chunk-PKMXQJEJ.js";
12
20
 
13
21
  // src/bootstrap.ts
14
22
  import { existsSync } from "fs";
@@ -17,20 +25,41 @@ import { fileURLToPath } from "url";
17
25
 
18
26
  // src/bootstrap/stub.ts
19
27
  import { pathToFileURL } from "url";
28
+ var STUB_RESTART_EXIT_CODE = 75;
29
+ var RESTARTED_BY = ["PAGS_UP_SUPERVISED", "PAGS_UP_LAUNCHER", "PAGS_SERVICE"];
30
+ function trialApplies(args) {
31
+ const command = args.find((a) => !a.startsWith("-"));
32
+ return command === "up" || command === "runner";
33
+ }
20
34
  function checksForUpdate(args) {
21
35
  return args.find((a) => !a.startsWith("-")) === "up";
22
36
  }
23
37
  async function runStub(deps) {
24
- const log = deps.log ?? ((line) => process.stderr.write(`${line}
25
- `));
26
- const local = () => newestPayload([...deps.bundled ? [deps.bundled] : [], ...(deps.cached ?? cachedPayloads)()]);
38
+ const log = deps.log ?? ((line) => {
39
+ process.stderr.write(`${line}
40
+ `);
41
+ logUpdateStep(`stub: ${line}`);
42
+ });
43
+ const root = deps.root ?? payloadRoot();
44
+ const now = deps.now ?? Date.now;
45
+ const env = deps.env ?? process.env;
46
+ const all = () => [...deps.bundled ? [deps.bundled] : [], ...(deps.cached ?? cachedPayloads)()];
47
+ const local = (without = []) => {
48
+ const skip = /* @__PURE__ */ new Set([...Object.keys(badVersions(root)), ...without]);
49
+ return newestPayload(all().filter((p) => !skip.has(p.version))) ?? (without.length ? null : newestPayload(all()));
50
+ };
27
51
  let chosen = local();
28
- if (checksForUpdate(deps.args) && !deps.fromSource && process.env[NO_SELF_UPDATE_ENV] !== "1") {
52
+ if (checksForUpdate(deps.args) && !deps.fromSource && env[NO_SELF_UPDATE_ENV] !== "1") {
29
53
  const latest = await (deps.latest ?? (() => latestPublishedVersion(15e3)))();
30
- if (latest && (!chosen || olderThan(chosen.version, latest))) {
54
+ const bad = latest ? badVersions(root)[latest] : void 0;
55
+ if (latest && bad) {
56
+ log(`pags: ${latest} is published, but this machine rolled back from it (${bad}) \u2014 staying on ${chosen?.version ?? "nothing"}`);
57
+ } else if (latest && (!chosen || olderThan(chosen.version, latest))) {
31
58
  log(`pags: updating ${chosen?.version ?? "(none)"} \u2192 ${latest}\u2026`);
32
59
  try {
60
+ const previous = chosen?.version ?? null;
33
61
  chosen = await (deps.install ?? ((v) => installPayload(v)))(latest);
62
+ beginTrial(root, chosen.version, previous, now());
34
63
  log(`pags: now on ${chosen.version}`);
35
64
  } catch (e) {
36
65
  log(`pags: could not fetch ${latest} (${e instanceof Error ? e.message : String(e)}) \u2014 starting ${chosen?.version ?? "nothing"} instead`);
@@ -38,10 +67,46 @@ async function runStub(deps) {
38
67
  }
39
68
  }
40
69
  if (!chosen) throw new Error("pags: no CLI to run \u2014 reinstall with `npm i -g @proagentstore/cli`");
41
- process.env[BOOTSTRAP_ENV] = deps.self;
42
- await (deps.load ?? ((entry) => import(pathToFileURL(entry).href)))(chosen.entry);
70
+ if (trialApplies(deps.args)) {
71
+ const verdict = noteTrialStart(root, chosen.version, now());
72
+ if (verdict.kind === "give-up") {
73
+ const fallback = local([chosen.version]);
74
+ markBad(root, chosen.version, verdict.reason, fallback?.version ?? null, now());
75
+ log(`pags: giving up on ${chosen.version} \u2014 ${verdict.reason}; rolling back to ${fallback?.version ?? "nothing"}`);
76
+ if (fallback) chosen = fallback;
77
+ } else if (verdict.kind === "trial") {
78
+ watchTrial(root, chosen.version, deps, log, now, env);
79
+ }
80
+ }
81
+ if (trialApplies(deps.args)) logUpdateStep(`stub: running ${chosen.version} for pags ${deps.args.find((a) => !a.startsWith("-"))}`);
82
+ env[BOOTSTRAP_ENV] = deps.self;
83
+ const load = deps.load ?? ((entry) => import(pathToFileURL(entry).href));
84
+ try {
85
+ await load(chosen.entry);
86
+ } catch (e) {
87
+ const fallback = readTrial(root)?.version === chosen.version ? local([chosen.version]) : null;
88
+ if (!fallback) throw e;
89
+ const why = `it crashed on start: ${e instanceof Error ? e.message : String(e)}`;
90
+ markBad(root, chosen.version, why, fallback.version, now());
91
+ log(`pags: giving up on ${chosen.version} \u2014 ${why}; rolling back to ${fallback.version}`);
92
+ chosen = fallback;
93
+ await load(chosen.entry);
94
+ }
43
95
  return chosen;
44
96
  }
97
+ function watchTrial(root, version, deps, log, now, env) {
98
+ const startedAt = readTrial(root)?.startedAt ?? now();
99
+ const timer = (deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms)))(() => {
100
+ const given = badVersions(root)[version];
101
+ if (readTrial(root)?.version !== version && !given) return;
102
+ const reason = `it did not register with the platform within ${TRIAL_MS / 6e4} minutes of first starting`;
103
+ if (!given) markBad(root, version, reason, readTrial(root)?.previous ?? null, now());
104
+ const supervised = RESTARTED_BY.some((k) => !!env[k]);
105
+ log(`pags: giving up on ${version} \u2014 ${given ?? reason}.${supervised ? " Restarting on the previous version." : ' The next "pags up" runs the previous version.'}`);
106
+ if (supervised) (deps.exit ?? ((code) => process.exit(code)))(STUB_RESTART_EXIT_CODE);
107
+ }, Math.max(0, startedAt + TRIAL_MS - now()));
108
+ timer.unref?.();
109
+ }
45
110
 
46
111
  // src/bootstrap.ts
47
112
  var self = fileURLToPath(import.meta.url);
@@ -0,0 +1,281 @@
1
+ // src/bootstrap/payload.ts
2
+ import { execFile } from "child_process";
3
+ import { existsSync, mkdirSync as mkdirSync2, mkdtempSync, readdirSync, readFileSync as readFileSync2, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
4
+ import { homedir } from "os";
5
+ import { join as join2, win32 } from "path";
6
+ import { promisify } from "util";
7
+
8
+ // src/bootstrap/trial.ts
9
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
10
+ import { join } from "path";
11
+ var TRIAL_MS = 5 * 6e4;
12
+ var MAX_TRIAL_STARTS = 6;
13
+ var trialPath = (root) => join(root, ".trial.json");
14
+ var badPath = (root) => join(root, ".bad.json");
15
+ var noticePath = (root) => join(root, ".rollback.json");
16
+ function readJson(path) {
17
+ try {
18
+ return JSON.parse(readFileSync(path, "utf-8"));
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ function writeJson(root, path, value) {
24
+ mkdirSync(root, { recursive: true });
25
+ const temp = `${path}.${process.pid}.tmp`;
26
+ writeFileSync(temp, JSON.stringify(value));
27
+ renameSync(temp, path);
28
+ }
29
+ function readTrial(root) {
30
+ const t = readJson(trialPath(root));
31
+ if (!t || typeof t.version !== "string" || typeof t.installedAt !== "number") return null;
32
+ return {
33
+ version: t.version,
34
+ previous: typeof t.previous === "string" ? t.previous : null,
35
+ installedAt: t.installedAt,
36
+ ...typeof t.startedAt === "number" ? { startedAt: t.startedAt } : {},
37
+ starts: typeof t.starts === "number" && t.starts >= 0 ? t.starts : 0
38
+ };
39
+ }
40
+ function beginTrial(root, version, previous, now = Date.now()) {
41
+ if (version === previous) return;
42
+ try {
43
+ writeJson(root, trialPath(root), { version, previous, installedAt: now, starts: 0 });
44
+ } catch {
45
+ }
46
+ }
47
+ function badVersions(root) {
48
+ const raw = readJson(badPath(root));
49
+ const out = {};
50
+ for (const [v, why] of Object.entries(raw?.versions ?? {})) if (typeof why === "string") out[v] = why;
51
+ return out;
52
+ }
53
+ function markBad(root, version, reason, to, now = Date.now()) {
54
+ try {
55
+ writeJson(root, badPath(root), { versions: { ...badVersions(root), [version]: reason.slice(0, 300) } });
56
+ writeJson(root, noticePath(root), { from: version, to, reason: reason.slice(0, 300), at: now });
57
+ if (readTrial(root)?.version === version) rmSync(trialPath(root), { force: true });
58
+ } catch {
59
+ }
60
+ }
61
+ function markPayloadHealthy(root, version) {
62
+ const trial = readTrial(root);
63
+ if (trial?.version !== version) return false;
64
+ rmSync(trialPath(root), { force: true });
65
+ return true;
66
+ }
67
+ function takeRollbackNotice(root) {
68
+ const n = readJson(noticePath(root));
69
+ rmSync(noticePath(root), { force: true });
70
+ if (!n || typeof n.from !== "string" || typeof n.reason !== "string") return null;
71
+ return { from: n.from, to: typeof n.to === "string" ? n.to : null, reason: n.reason, at: typeof n.at === "number" ? n.at : 0 };
72
+ }
73
+ function noteTrialStart(root, version, now = Date.now()) {
74
+ const trial = readTrial(root);
75
+ if (!trial || trial.version !== version) return { kind: "none" };
76
+ if (trial.startedAt !== void 0 && now - trial.startedAt >= TRIAL_MS) {
77
+ return { kind: "give-up", reason: `it did not register with the platform within ${TRIAL_MS / 6e4} minutes of first starting` };
78
+ }
79
+ if (trial.starts >= MAX_TRIAL_STARTS) {
80
+ return { kind: "give-up", reason: `it was started ${trial.starts} times without registering with the platform` };
81
+ }
82
+ const next = { ...trial, startedAt: trial.startedAt ?? now, starts: trial.starts + 1 };
83
+ try {
84
+ writeJson(root, trialPath(root), next);
85
+ } catch {
86
+ }
87
+ return { kind: "trial", trial: next };
88
+ }
89
+
90
+ // src/bootstrap/payload.ts
91
+ var run = promisify(execFile);
92
+ var CLI_PACKAGE = "@proagentstore/cli";
93
+ var BOOTSTRAP_ENV = "PAGS_BOOTSTRAP";
94
+ var NO_SELF_UPDATE_ENV = "PAGS_NO_SELF_UPDATE";
95
+ var KEEP = 2;
96
+ function payloadRoot() {
97
+ return process.env.PAGS_CLI_CACHE || join2(homedir(), ".config", "proagentstore", "cli");
98
+ }
99
+ function isVersion(v) {
100
+ return typeof v === "string" && /^(\d+)\.(\d+)\.(\d+)/.test(v.trim());
101
+ }
102
+ function olderThan(a, b) {
103
+ const parse = (v) => /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim())?.slice(1).map(Number);
104
+ const x = parse(a);
105
+ const y = parse(b);
106
+ if (!x || !y) return false;
107
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i];
108
+ return false;
109
+ }
110
+ function payloadAt(dir) {
111
+ try {
112
+ const pkg = JSON.parse(readFileSync2(join2(dir, "package.json"), "utf-8"));
113
+ if (pkg.name !== CLI_PACKAGE || !pkg.version) return null;
114
+ const entry = join2(dir, pkg.pagsPayload || "dist/index.js");
115
+ return existsSync(entry) ? { version: pkg.version, entry } : null;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+ var installedAt = (prefix) => join2(prefix, "node_modules", ...CLI_PACKAGE.split("/"));
121
+ var packageDir = (root, version) => installedAt(join2(root, version));
122
+ function cachedPayloads(root = payloadRoot()) {
123
+ let names;
124
+ try {
125
+ names = readdirSync(root);
126
+ } catch {
127
+ return [];
128
+ }
129
+ return names.flatMap((v) => v.startsWith(".") ? [] : payloadAt(packageDir(root, v)) ?? []);
130
+ }
131
+ function newestPayload(payloads) {
132
+ return payloads.reduce((best, p) => !best || olderThan(best.version, p.version) ? p : best, null);
133
+ }
134
+ var CMD_META = /([()\][%!^"`<>&|;, *?])/g;
135
+ function quoteCmdArg(arg) {
136
+ const quoted = `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
137
+ return quoted.replace(CMD_META, "^$1").replace(CMD_META, "^$1");
138
+ }
139
+ var envVar = (env, name) => env[Object.keys(env).find((k) => k.toUpperCase() === name) ?? name];
140
+ function npmInvocation(args, deps = {}) {
141
+ if ((deps.platform ?? process.platform) !== "win32") return { file: "npm", args };
142
+ const env = deps.env ?? process.env;
143
+ const exists = deps.exists ?? existsSync;
144
+ const execPath = deps.execPath ?? process.execPath;
145
+ const cli = (dir) => win32.join(dir, "node_modules", "npm", "bin", "npm-cli.js");
146
+ const onPath = (envVar(env, "PATH") ?? "").split(";").filter((dir) => dir && exists(win32.join(dir, "npm.cmd")));
147
+ for (const dir of [...onPath, win32.dirname(execPath)]) {
148
+ if (exists(cli(dir))) return { file: execPath, args: [cli(dir), ...args] };
149
+ }
150
+ const line = ["npm", ...args.map(quoteCmdArg)].join(" ");
151
+ return { file: envVar(env, "COMSPEC") || "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], windowsVerbatimArguments: true };
152
+ }
153
+ function runNpm(args, options) {
154
+ const npm = npmInvocation(args);
155
+ return run(npm.file, npm.args, { ...options, ...npm.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {} });
156
+ }
157
+ async function latestPublishedRelease(timeoutMs = 3e4, signal, deps = {}) {
158
+ try {
159
+ const { stdout } = await (deps.runNpm ?? runNpm)(["view", CLI_PACKAGE, "version"], { timeout: timeoutMs, signal });
160
+ const version = String(stdout).trim();
161
+ return version ? { version } : { error: "npm printed no version" };
162
+ } catch (e) {
163
+ return { error: npmFailure(e) };
164
+ }
165
+ }
166
+ function npmFailure(e) {
167
+ const err = e;
168
+ const stderr = String(err?.stderr ?? "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
169
+ const npmErrors = stderr.filter((l) => /npm (ERR!|error)/i.test(l));
170
+ const why = npmErrors.slice(0, 2).join(" ") || stderr.at(-1) || (err?.killed || err?.signal ? `npm did not answer in time (${String(err.signal ?? "killed")})` : "") || (typeof err?.code === "string" ? `npm could not be started (${err.code}: ${String(err.message ?? "")})` : "") || String(err?.message ?? e);
171
+ return why.replace(/\s+/g, " ").trim().slice(0, 300);
172
+ }
173
+ async function latestPublishedVersion(timeoutMs = 3e4, signal) {
174
+ const r = await latestPublishedRelease(timeoutMs, signal);
175
+ return "version" in r ? r.version : null;
176
+ }
177
+ async function npmInstallInto(dir, version) {
178
+ try {
179
+ await runNpm(["install", "--prefix", dir, "--omit=dev", "--no-audit", "--no-fund", "--no-save", `${CLI_PACKAGE}@${version}`], { timeout: 5 * 6e4 });
180
+ } catch (e) {
181
+ const stderr = String(e.stderr ?? "").trim();
182
+ throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
183
+ }
184
+ }
185
+ async function installPayload(version, root = payloadRoot(), install = npmInstallInto) {
186
+ const done = payloadAt(packageDir(root, version));
187
+ if (done) return done;
188
+ mkdirSync2(root, { recursive: true });
189
+ const scratch = mkdtempSync(join2(root, `.install-${version}-`));
190
+ try {
191
+ await install(scratch, version);
192
+ if (payloadAt(installedAt(scratch))?.version !== version) throw new Error(`npm installed no ${CLI_PACKAGE}@${version}`);
193
+ try {
194
+ renameSync2(scratch, join2(root, version));
195
+ } catch (e) {
196
+ if (!payloadAt(packageDir(root, version))) throw e;
197
+ }
198
+ } finally {
199
+ rmSync2(scratch, { recursive: true, force: true });
200
+ }
201
+ const payload = payloadAt(packageDir(root, version));
202
+ if (!payload) throw new Error(`${CLI_PACKAGE}@${version} is not usable after install`);
203
+ const bad = badVersions(root);
204
+ const others = cachedPayloads(root).filter((p) => p.version !== version).sort((a, b) => olderThan(a.version, b.version) ? 1 : -1);
205
+ const kept = new Set(others.filter((p) => !bad[p.version]).slice(0, KEEP - 1).map((p) => p.version));
206
+ for (const old of others) if (!kept.has(old.version)) rmSync2(join2(root, old.version), { recursive: true, force: true });
207
+ return payload;
208
+ }
209
+
210
+ // src/bootstrap/update-log.ts
211
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
212
+ import { homedir as homedir2 } from "os";
213
+ import { dirname, join as join3 } from "path";
214
+ var MAX_BYTES = 256 * 1024;
215
+ var UPDATE_LOG_UPLOAD_CHARS = 4e3;
216
+ function updateLogPath(env = process.env) {
217
+ return env.PAGS_UPDATE_LOG?.trim() || join3(env.PAGS_CONFIG_DIR?.trim() || join3(homedir2(), ".config", "proagentstore"), "update.log");
218
+ }
219
+ function logUpdateStep(step, env = process.env, now = /* @__PURE__ */ new Date()) {
220
+ const path = updateLogPath(env);
221
+ try {
222
+ mkdirSync3(dirname(path), { recursive: true });
223
+ if (existsSync2(path) && statSync(path).size > MAX_BYTES) {
224
+ const text = readFileSync3(path, "utf8");
225
+ writeFileSync2(path, text.slice(text.indexOf("\n", text.length / 2) + 1));
226
+ }
227
+ appendFileSync(path, `${now.toISOString()} [${process.pid}] ${step.replace(/\s*\n\s*/g, " | ")}
228
+ `);
229
+ } catch {
230
+ }
231
+ }
232
+ function updateLogTail(env = process.env, maxChars = UPDATE_LOG_UPLOAD_CHARS) {
233
+ try {
234
+ const text = readFileSync3(updateLogPath(env), "utf8").trimEnd();
235
+ if (!text) return null;
236
+ if (text.length <= maxChars) return text;
237
+ const cut = text.slice(-maxChars);
238
+ return cut.slice(cut.indexOf("\n") + 1);
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+ function updateLogUpload(version, env = process.env) {
244
+ let pending = updateLogTail(env);
245
+ return {
246
+ /** The field for a status report while the upload is pending. */
247
+ body: () => pending ? { updateLog: pending } : {},
248
+ /** The platform answered a report of this start. */
249
+ answered: () => {
250
+ if (pending === null) return;
251
+ pending = null;
252
+ logUpdateStep(`registered: ${version} answered by the platform`, env);
253
+ }
254
+ };
255
+ }
256
+
257
+ export {
258
+ TRIAL_MS,
259
+ readTrial,
260
+ beginTrial,
261
+ badVersions,
262
+ markBad,
263
+ markPayloadHealthy,
264
+ takeRollbackNotice,
265
+ noteTrialStart,
266
+ CLI_PACKAGE,
267
+ BOOTSTRAP_ENV,
268
+ NO_SELF_UPDATE_ENV,
269
+ payloadRoot,
270
+ isVersion,
271
+ olderThan,
272
+ payloadAt,
273
+ cachedPayloads,
274
+ newestPayload,
275
+ runNpm,
276
+ latestPublishedRelease,
277
+ latestPublishedVersion,
278
+ installPayload,
279
+ logUpdateStep,
280
+ updateLogUpload
281
+ };
package/dist/index.js CHANGED
@@ -2,12 +2,19 @@
2
2
  import {
3
3
  BOOTSTRAP_ENV,
4
4
  CLI_PACKAGE,
5
+ badVersions,
6
+ beginTrial,
5
7
  installPayload,
6
8
  isVersion,
7
9
  latestPublishedRelease,
10
+ logUpdateStep,
11
+ markPayloadHealthy,
8
12
  olderThan,
9
- runNpm
10
- } from "./chunk-NNA7VZ6W.js";
13
+ payloadRoot,
14
+ runNpm,
15
+ takeRollbackNotice,
16
+ updateLogUpload
17
+ } from "./chunk-PKMXQJEJ.js";
11
18
 
12
19
  // src/index.ts
13
20
  import { createRequire as createRequire3 } from "module";
@@ -1361,6 +1368,7 @@ function deferredUpdateLeaseActive(lease, now = Date.now()) {
1361
1368
  return now < lease.expiresAt;
1362
1369
  }
1363
1370
  function leaveForRestart(restarter, deps = { spawn: spawn3, exit: (code) => process.exit(code) }) {
1371
+ logUpdateStep(`restart: leaving for the ${restarter} restarter (${restarter === "command" ? "restart command started, exit 0" : `exit ${RUNNER_RESTART_EXIT_CODE}`})`);
1364
1372
  if (restarter === "command") {
1365
1373
  deps.spawn(process.env[RESTART_COMMAND_ENV], { shell: true, detached: true, stdio: "ignore", env: process.env }).unref();
1366
1374
  deps.exit(0);
@@ -1379,6 +1387,8 @@ function planRunnerUpdate(f) {
1379
1387
  if (f.fromSource) return { action: "refused", current: f.current, reason: "This runner runs from a source checkout \u2014 update it with `git pull` there, not npm." };
1380
1388
  if (!f.latest) return { action: "refused", current: f.current, reason: `npm could not be asked for the latest ${CLI_PACKAGE} from this machine${f.latestError ? `: ${f.latestError}` : "."}`, retryable: true };
1381
1389
  if (!olderThan(f.current, f.latest)) return { action: "up-to-date", current: f.current };
1390
+ const rolledBack = f.rolledBack?.[f.latest];
1391
+ if (rolledBack) return { action: "refused", current: f.current, reason: `${f.latest} was installed here and rolled back: ${rolledBack}. This machine stays on ${f.current} until a newer release is published.`, retryable: true };
1382
1392
  if (!f.restarter) {
1383
1393
  return {
1384
1394
  action: "refused",
@@ -1411,17 +1421,24 @@ function planRunnerRestart(f) {
1411
1421
  }
1412
1422
  return { action: "restart", current: f.current, restarter: f.restarter };
1413
1423
  }
1414
- async function installVersion(version2) {
1415
- if (process.env[BOOTSTRAP_ENV]) {
1416
- await installPayload(version2);
1417
- return;
1418
- }
1424
+ async function installVersion(version2, current = null) {
1425
+ const bootstrapped = !!process.env[BOOTSTRAP_ENV];
1426
+ logUpdateStep(`install: ${current ?? "?"} \u2192 ${version2} starting (${bootstrapped ? "payload cache" : "npm i -g"})`);
1419
1427
  try {
1420
- await runNpm(["i", "-g", `${CLI_PACKAGE}@${version2}`], { timeout: 5 * 6e4 });
1428
+ if (bootstrapped) {
1429
+ await installPayload(version2);
1430
+ beginTrial(payloadRoot(), version2, current);
1431
+ } else {
1432
+ await runNpm(["i", "-g", `${CLI_PACKAGE}@${version2}`], { timeout: 5 * 6e4 }).catch((e) => {
1433
+ const stderr = String(e.stderr ?? "").trim();
1434
+ throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
1435
+ });
1436
+ }
1421
1437
  } catch (e) {
1422
- const stderr = String(e.stderr ?? "").trim();
1423
- throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
1438
+ logUpdateStep(`install: ${version2} FAILED \u2014 ${e instanceof Error ? e.message : String(e)}`);
1439
+ throw e;
1424
1440
  }
1441
+ logUpdateStep(`install: ${version2} installed`);
1425
1442
  }
1426
1443
 
1427
1444
  // src/commands/runner/membership.ts
@@ -1776,13 +1793,15 @@ function parseAutoUpdatePolicy(value) {
1776
1793
  const lastAttemptAt = raw.lastAttemptAt ?? raw.last_attempt_at;
1777
1794
  const reason = raw.reason;
1778
1795
  const recovery = parseRecovery(raw.recovery);
1796
+ const fleet = raw.fleetUpdating ?? raw.fleet_updating;
1779
1797
  return {
1780
1798
  autoUpdate,
1781
1799
  ...status ? { status } : {},
1782
1800
  ...typeof latestVersion === "string" ? { latestVersion } : {},
1783
1801
  ...typeof lastAttemptAt === "string" ? { lastAttemptAt } : {},
1784
1802
  ...typeof reason === "string" ? { reason } : {},
1785
- ...recovery ? { recovery } : {}
1803
+ ...recovery ? { recovery } : {},
1804
+ ...typeof fleet === "number" && Number.isInteger(fleet) && fleet >= 0 ? { fleetUpdating: fleet } : {}
1786
1805
  };
1787
1806
  }
1788
1807
  function policyFromResponse(value) {
@@ -1843,7 +1862,9 @@ function acceptPolicyResponse(local, wasAuthoritative, response) {
1843
1862
  const { recovery: _fromService, ...remote } = parsed;
1844
1863
  const action = policyScheduleAction(local, remote, wasAuthoritative);
1845
1864
  const ownerTransition = action === "cancel" || !local.autoUpdate;
1846
- return { policy: ownerTransition ? { ...local, ...remote } : { ...local, autoUpdate: remote.autoUpdate }, action };
1865
+ const { fleetUpdating: _stale, ...base } = local;
1866
+ const fleet = remote.fleetUpdating === void 0 ? {} : { fleetUpdating: remote.fleetUpdating };
1867
+ return { policy: ownerTransition ? { ...base, ...remote, ...fleet } : { ...base, autoUpdate: remote.autoUpdate, ...fleet }, action };
1847
1868
  }
1848
1869
  var ResponseFence = class {
1849
1870
  sent = 0;
@@ -2068,6 +2089,12 @@ var AutomaticUpdateController = class {
2068
2089
  this.defer();
2069
2090
  return;
2070
2091
  }
2092
+ if (policy.fleetUpdating) {
2093
+ this.blockerClass = "fleet_stagger";
2094
+ this.deps.status("waiting-for-idle", { latestVersion: plan.latest, reason: `${policy.fleetUpdating} other machine(s) on this account are mid-update; this one updates after them.` });
2095
+ this.defer(plan.latest);
2096
+ return;
2097
+ }
2071
2098
  const target = open && isVersion(open.target) ? open.target : plan.latest;
2072
2099
  const attempt = { ...plan, latest: target };
2073
2100
  const recovery = open ?? { target, attachments: [...this.deps.attachments?.() ?? []], at: new Date(this.now()).toISOString() };
@@ -2077,7 +2104,7 @@ var AutomaticUpdateController = class {
2077
2104
  this.deps.status("restarting", { latestVersion: target, reason: void 0 });
2078
2105
  return;
2079
2106
  }
2080
- this.blockerClass = "idle_wait";
2107
+ this.blockerClass = this.deps.policy().value.fleetUpdating ? "fleet_stagger" : "idle_wait";
2081
2108
  this.defer(target);
2082
2109
  }
2083
2110
  defer(latest) {
@@ -2250,7 +2277,10 @@ function createPolicyChannel(deps) {
2250
2277
  if (action === "cancel") deps.onAction("cancel");
2251
2278
  else if (action === "start" && !deps.cell.refreshing) deps.onAction("start");
2252
2279
  };
2253
- const accept = (response, sentAs) => act(deps.cell.accept(response, sentAs));
2280
+ const accept = (response, sentAs) => {
2281
+ act(deps.cell.accept(response, sentAs));
2282
+ deps.onAnswer?.();
2283
+ };
2254
2284
  const register = async (id, force = deps.force, signal) => {
2255
2285
  const sent = deps.cell.next();
2256
2286
  try {
@@ -2300,7 +2330,7 @@ function createInstallBridge(deps) {
2300
2330
  if (automatic) {
2301
2331
  const fresh = await within((signal) => deps.refresh(signal));
2302
2332
  if (fresh === null) expired(false, "The owner policy refresh before installing");
2303
- if (!fresh || !deps.cell.policy.autoUpdate) {
2333
+ if (!fresh || !deps.cell.policy.autoUpdate || deps.cell.policy.fleetUpdating) {
2304
2334
  release(false);
2305
2335
  return false;
2306
2336
  }
@@ -2344,6 +2374,27 @@ function createInstallBridge(deps) {
2344
2374
  return { run, inFlight: () => inFlight, installedPendingRestart: () => installedPendingRestart };
2345
2375
  }
2346
2376
 
2377
+ // src/commands/runner/release-trial.ts
2378
+ function releaseTrial(version2, cell, log, env = process.env, root = payloadRoot) {
2379
+ if (!env[BOOTSTRAP_ENV]) return { rollbackReason: null, proven: () => void 0, rolledBack: () => ({}) };
2380
+ const notice = takeRollbackNotice(root());
2381
+ const rollbackReason = notice ? `Rolled back from ${notice.from} to ${version2}: ${notice.reason}.` : null;
2382
+ if (rollbackReason) {
2383
+ log(`auto-update: ${rollbackReason}`);
2384
+ cell.record("failure", { reason: rollbackReason, recovery: void 0 });
2385
+ }
2386
+ let done = false;
2387
+ return {
2388
+ rollbackReason,
2389
+ proven: () => {
2390
+ if (done) return;
2391
+ done = true;
2392
+ if (markPayloadHealthy(root(), version2)) log(`auto-update: ${version2} registered \u2014 it is kept.`);
2393
+ },
2394
+ rolledBack: () => badVersions(root())
2395
+ };
2396
+ }
2397
+
2347
2398
  // src/commands/runner/relay.ts
2348
2399
  var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
2349
2400
  var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH, RUNNER_RESTART_PATH]);
@@ -2355,12 +2406,15 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2355
2406
  const machine = loadMachineIdentity(runnerNode);
2356
2407
  const autoUpdateKey = machine.id ? autoUpdatePolicyKey(machine.id) : "";
2357
2408
  const cell = new PolicyCell(autoUpdateKey ? loadAutoUpdatePolicy(autoUpdateKey) ?? { autoUpdate: false } : { autoUpdate: false }, (p) => saveAutoUpdatePolicy(autoUpdateKey, p), !!autoUpdateKey);
2409
+ const trial = releaseTrial(CLI_VERSION, cell, writeLine);
2358
2410
  let scheduleAutomaticUpdate = () => void 0;
2359
2411
  let autoUpdateDiagnostics = () => ({});
2412
+ const updateLog = updateLogUpload(CLI_VERSION);
2360
2413
  const autoUpdateStatusBody = () => ({
2361
2414
  status: autoUpdateStatusWire(cell.policy.status),
2362
2415
  latestVersion: cell.policy.latestVersion,
2363
2416
  error: cell.policy.reason,
2417
+ ...updateLog.body(),
2364
2418
  ...autoUpdateDiagnostics()
2365
2419
  });
2366
2420
  const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
@@ -2387,7 +2441,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2387
2441
  lastRegisterError = msg;
2388
2442
  writeError(`register ${id.slice(0, 8)}\u2026 failed: ${msg}`);
2389
2443
  },
2390
- onAction: (action) => scheduleAutomaticUpdate(action === "start", action === "cancel")
2444
+ onAction: (action) => scheduleAutomaticUpdate(action === "start", action === "cancel"),
2445
+ onAnswer: () => {
2446
+ trial.proven();
2447
+ updateLog.answered();
2448
+ }
2391
2449
  });
2392
2450
  const registered = channel.registered;
2393
2451
  const registerRuntime = (id, forceClaim = force) => channel.register(id, forceClaim);
@@ -2424,6 +2482,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2424
2482
  ..."error" in release ? { latestError: release.error } : {},
2425
2483
  fromSource: runsFromSource(),
2426
2484
  restarter: restarterFrom(process.env),
2485
+ rolledBack: trial.rolledBack(),
2427
2486
  // A restart ends browser/application work too. Give these durable names to the same
2428
2487
  // planner so neither manual nor automatic update can mistake them for an idle machine.
2429
2488
  busy
@@ -2434,7 +2493,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2434
2493
  cell,
2435
2494
  refresh: channel.refresh,
2436
2495
  facts: updateFacts,
2437
- install: installVersion,
2496
+ install: (version2) => installVersion(version2, CLI_VERSION),
2438
2497
  admission: relayMutationAdmission,
2439
2498
  leaseActive: deferredUpdateLeaseActive,
2440
2499
  detachAll: () => {
@@ -3531,7 +3590,7 @@ var logsCommand = new Command9("logs").description("Show the background runner's
3531
3590
  // src/commands/up.ts
3532
3591
  import { createRequire as createRequire2 } from "module";
3533
3592
  import { Command as Command10 } from "commander";
3534
- import { hostname as hostname7 } from "os";
3593
+ import { homedir as homedir5, hostname as hostname7, userInfo } from "os";
3535
3594
 
3536
3595
  // src/tui.ts
3537
3596
  import chalk from "chalk";
@@ -3653,32 +3712,35 @@ async function waitForKey(keys, onInterrupt) {
3653
3712
  // src/commands/up-restart.ts
3654
3713
  import { spawn as nodeSpawn2, spawnSync as nodeSpawnSync } from "child_process";
3655
3714
  var UP_LAUNCHER_ENV = "PAGS_UP_LAUNCHER";
3656
- function upRestartMode(env, platform, canExecve) {
3715
+ function upRestartMode(env) {
3657
3716
  if (env[SERVICE_ENV] === "1") return "service";
3658
3717
  if (env[RESTART_COMMAND_ENV]?.trim()) return "command";
3659
3718
  if (env[UP_LAUNCHER_ENV] === "1") return "launcher-child";
3660
- return platform !== "win32" && canExecve ? "exec" : "launcher";
3719
+ return "launcher";
3720
+ }
3721
+ function relaunchDelayMs(failures) {
3722
+ return Math.min(5 * 6e4, 1e4 * 2 ** Math.max(0, failures - 1));
3661
3723
  }
3662
3724
  function processRestartDeps() {
3663
- const execve = process.execve;
3664
3725
  return {
3665
3726
  env: process.env,
3666
- platform: process.platform,
3667
3727
  execPath: process.execPath,
3668
3728
  execArgv: process.execArgv,
3669
3729
  script: process.argv[1],
3670
- execve: typeof execve === "function" ? execve.bind(process) : void 0,
3671
3730
  spawn: nodeSpawn2,
3672
3731
  spawnSync: nodeSpawnSync,
3673
3732
  exit: (code) => process.exit(code),
3674
3733
  log: () => {
3675
- }
3734
+ },
3735
+ sleep: (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms),
3736
+ step: (line) => logUpdateStep(line)
3676
3737
  };
3677
3738
  }
3678
3739
  function restartPagsUp(args, extraEnv, deps) {
3679
3740
  const env = { ...deps.env, ...extraEnv };
3680
3741
  const argv = [...deps.execArgv, deps.script, ...args];
3681
- const mode = upRestartMode(env, deps.platform, !!deps.execve);
3742
+ const mode = upRestartMode(env);
3743
+ deps.step(`restart: pags ${args.join(" ")} \u2014 leaving via ${mode}`);
3682
3744
  if (mode === "service" || mode === "command") {
3683
3745
  leaveForRestart(mode, { spawn: deps.spawn, exit: deps.exit });
3684
3746
  return;
@@ -3687,24 +3749,31 @@ function restartPagsUp(args, extraEnv, deps) {
3687
3749
  deps.exit(RUNNER_RESTART_EXIT_CODE);
3688
3750
  return;
3689
3751
  }
3690
- if (mode === "exec") {
3691
- try {
3692
- deps.execve(deps.execPath, [deps.execPath, ...argv], env);
3693
- } catch (e) {
3694
- deps.log(`Restart failed (${e instanceof Error ? e.message : String(e)}) \u2014 the runner is NOT running. Run 'pags up' again.`);
3695
- deps.exit(1);
3696
- }
3697
- return;
3698
- }
3752
+ let failures = 0;
3699
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)" : ""}`);
3700
3756
  const r = deps.spawnSync(deps.execPath, argv, { stdio: "inherit", env: { ...env, [UP_LAUNCHER_ENV]: "1", ...first ? {} : { PAGS_NO_PROMPT: "1" } } });
3701
- if (r.status === RUNNER_RESTART_EXIT_CODE) continue;
3702
- if (r.error || r.status !== 0) {
3703
- deps.log(`Restart failed${typeof r.status === "number" ? ` (exit ${r.status})` : ""} \u2014 the runner is NOT running. Run 'pags up' again.`);
3704
- deps.exit(typeof r.status === "number" && r.status !== 0 ? r.status : 1);
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);
3705
3765
  return;
3706
3766
  }
3707
- deps.exit(0);
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);
3708
3777
  return;
3709
3778
  }
3710
3779
  }
@@ -3850,11 +3919,244 @@ async function startInBackground(start, deps = processDeps()) {
3850
3919
  return 1;
3851
3920
  }
3852
3921
 
3922
+ // src/service-install.ts
3923
+ import { execFileSync as execFileSync3 } from "child_process";
3924
+ import { mkdirSync as mkdirSync9, rmSync, writeFileSync as writeFileSync7 } from "fs";
3925
+ import { posix, win32 as win322 } from "path";
3926
+ var SERVICE_LABEL = "online.proagentstore.pags-up";
3927
+ var SYSTEMD_UNIT = "pags-up.service";
3928
+ var WINDOWS_TASK = "pags up";
3929
+ var pathFor = (ctx) => ctx.platform === "win32" ? win322 : posix;
3930
+ var supervisorPath = (ctx) => pathFor(ctx).join(ctx.configDir, "pags-up-supervisor.mjs");
3931
+ var serviceLogPath = (ctx) => pathFor(ctx).join(ctx.configDir, "pags-up.log");
3932
+ function supervisorScript(ctx) {
3933
+ const config = JSON.stringify({ node: ctx.nodePath, script: ctx.script, log: serviceLogPath(ctx), path: ctx.pathEnv });
3934
+ return `// Written by \`pags up --install-service\` (#1141). Keeps \`pags up\` running; see packages/cli/src/service-install.ts.
3935
+ import { spawn } from "node:child_process";
3936
+ import { appendFileSync, closeSync, openSync } from "node:fs";
3937
+
3938
+ const config = ${config};
3939
+ const num = (name, fallback) => { const n = Number(process.env[name]); return Number.isFinite(n) && n > 0 ? n : fallback; };
3940
+ const MIN = num("PAGS_SUPERVISOR_MIN_MS", 10_000);
3941
+ const MAX = num("PAGS_SUPERVISOR_MAX_MS", 300_000);
3942
+ const RESET = num("PAGS_SUPERVISOR_RESET_MS", 600_000);
3943
+ const RESTART_CODE = 75; // \`pags up\` asking to be started again: an update, or runner_restart
3944
+
3945
+ const say = (line) => { try { appendFileSync(config.log, \`[\${new Date().toISOString()}] supervisor: \${line}\\n\`); } catch { /* a log that cannot be written must not stop the runner */ } };
3946
+ let delay = MIN;
3947
+ let child = null;
3948
+ let stopping = false;
3949
+
3950
+ function start() {
3951
+ if (stopping) return;
3952
+ const startedAt = Date.now();
3953
+ let out = "ignore";
3954
+ try { out = openSync(config.log, "a"); } catch { /* run without a log rather than not at all */ }
3955
+ say("starting pags up");
3956
+ child = spawn(config.node, [config.script, "up", "--headless"], {
3957
+ stdio: ["ignore", out, out],
3958
+ env: { ...process.env, PATH: config.path || process.env.PATH, PAGS_SERVICE: "1", PAGS_SERVICE_NAME: "${SERVICE_LABEL}", PAGS_NO_PROMPT: "1" },
3959
+ windowsHide: true,
3960
+ });
3961
+ if (typeof out === "number") closeSync(out);
3962
+ child.on("error", (e) => say(\`could not start pags up: \${e.message}\`));
3963
+ child.on("exit", (code, signal) => {
3964
+ child = null;
3965
+ if (stopping) return;
3966
+ if (Date.now() - startedAt >= RESET) delay = MIN;
3967
+ const wait = code === RESTART_CODE ? 1_000 : delay;
3968
+ say(\`pags up exited (\${signal ?? code}); starting it again in \${Math.round(wait / 1000)}s\`);
3969
+ if (code !== RESTART_CODE) delay = Math.min(delay * 2, MAX);
3970
+ setTimeout(start, wait);
3971
+ });
3972
+ }
3973
+
3974
+ for (const sig of ["SIGTERM", "SIGINT"]) {
3975
+ process.on(sig, () => {
3976
+ stopping = true;
3977
+ say(\`stopping (\${sig})\`);
3978
+ if (child) child.kill("SIGTERM");
3979
+ setTimeout(() => process.exit(0), 5_000).unref();
3980
+ if (!child) process.exit(0);
3981
+ else child.once("exit", () => process.exit(0));
3982
+ });
3983
+ }
3984
+ start();
3985
+ `;
3986
+ }
3987
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3988
+ var unitWord = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
3989
+ var ps = (s) => `'${s.replace(/'/g, "''")}'`;
3990
+ function servicePlan(ctx) {
3991
+ const supervisor = supervisorPath(ctx);
3992
+ const log = serviceLogPath(ctx);
3993
+ const files = [{ path: supervisor, content: supervisorScript(ctx) }];
3994
+ if (ctx.platform === "darwin") {
3995
+ const plist = posix.join(ctx.home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
3996
+ const domain = `gui/${ctx.uid ?? 501}`;
3997
+ files.push({
3998
+ path: plist,
3999
+ content: `<?xml version="1.0" encoding="UTF-8"?>
4000
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4001
+ <plist version="1.0">
4002
+ <dict>
4003
+ <key>Label</key><string>${SERVICE_LABEL}</string>
4004
+ <key>ProgramArguments</key>
4005
+ <array>
4006
+ <string>${xml(ctx.nodePath)}</string>
4007
+ <string>${xml(supervisor)}</string>
4008
+ </array>
4009
+ <key>RunAtLoad</key><true/>
4010
+ <key>KeepAlive</key><true/>
4011
+ <key>ThrottleInterval</key><integer>30</integer>
4012
+ <key>ProcessType</key><string>Background</string>
4013
+ <key>StandardOutPath</key><string>${xml(log)}</string>
4014
+ <key>StandardErrorPath</key><string>${xml(log)}</string>
4015
+ </dict>
4016
+ </plist>
4017
+ `
4018
+ });
4019
+ return {
4020
+ kind: "launchd",
4021
+ files,
4022
+ commands: [
4023
+ { file: "launchctl", args: ["bootout", `${domain}/${SERVICE_LABEL}`], optional: true, why: "unload an earlier copy" },
4024
+ { file: "launchctl", args: ["bootstrap", domain, plist], why: "load the LaunchAgent" },
4025
+ { file: "launchctl", args: ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`], why: "start it now" }
4026
+ ],
4027
+ notes: [
4028
+ `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
+ `Remove it with: pags up --uninstall-service`
4030
+ ]
4031
+ };
4032
+ }
4033
+ if (ctx.platform === "win32") {
4034
+ const task = win322.join(ctx.configDir, "pags-up-task.ps1");
4035
+ const principal = `${ctx.domain ? `${ctx.domain}\\` : ""}${ctx.user ?? ""}`;
4036
+ files.push({
4037
+ path: task,
4038
+ content: `# Written by \`pags up --install-service\` (#1141). Registers the "${WINDOWS_TASK}" Scheduled Task; needs an elevated PowerShell.
4039
+ $ErrorActionPreference = 'Stop'
4040
+ $action = New-ScheduledTaskAction -Execute ${ps(ctx.nodePath)} -Argument ${ps(`"${supervisor}"`)}
4041
+ $trigger = New-ScheduledTaskTrigger -AtStartup
4042
+ $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
4044
+ Unregister-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Confirm:$false -ErrorAction SilentlyContinue
4045
+ Register-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
4046
+ Start-ScheduledTask -TaskName ${ps(WINDOWS_TASK)}
4047
+ `
4048
+ });
4049
+ return {
4050
+ kind: "scheduled-task",
4051
+ files,
4052
+ commands: [{ file: "powershell.exe", args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", task], why: "register and start the Scheduled Task" }],
4053
+ notes: [
4054
+ `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
+ `Remove it with: pags up --uninstall-service`
4056
+ ]
4057
+ };
4058
+ }
4059
+ const unit = posix.join(ctx.home, ".config", "systemd", "user", SYSTEMD_UNIT);
4060
+ files.push({
4061
+ path: unit,
4062
+ content: `# Written by \`pags up --install-service\` (#1141).
4063
+ [Unit]
4064
+ Description=ProAgentStore runner (pags up)
4065
+ After=network-online.target
4066
+ Wants=network-online.target
4067
+ StartLimitIntervalSec=0
4068
+
4069
+ [Service]
4070
+ ExecStart=${unitWord(ctx.nodePath)} ${unitWord(supervisor)}
4071
+ Environment=${unitWord(`PATH=${ctx.pathEnv}`)}
4072
+ Restart=always
4073
+ RestartSec=10
4074
+ KillMode=mixed
4075
+
4076
+ [Install]
4077
+ WantedBy=default.target
4078
+ `
4079
+ });
4080
+ return {
4081
+ kind: "systemd",
4082
+ files,
4083
+ commands: [
4084
+ { file: "systemctl", args: ["--user", "daemon-reload"], why: "read the unit" },
4085
+ { file: "systemctl", args: ["--user", "enable", "--now", SYSTEMD_UNIT], why: "enable and start it" },
4086
+ { file: "loginctl", args: ["enable-linger", ...ctx.user ? [ctx.user] : []], optional: true, why: "keep it running with nobody logged in" }
4087
+ ],
4088
+ notes: [`systemd user unit ${unit}. Logs: ${log} (and journalctl --user -u ${SYSTEMD_UNIT}).`, `Remove it with: pags up --uninstall-service`]
4089
+ };
4090
+ }
4091
+ function serviceRemovalPlan(ctx) {
4092
+ const plan = servicePlan(ctx);
4093
+ const files = plan.files.map((f) => f.path);
4094
+ if (plan.kind === "launchd") return { files, commands: [{ file: "launchctl", args: ["bootout", `gui/${ctx.uid ?? 501}/${SERVICE_LABEL}`], optional: true, why: "unload the LaunchAgent" }] };
4095
+ if (plan.kind === "systemd") return { files, commands: [{ file: "systemctl", args: ["--user", "disable", "--now", SYSTEMD_UNIT], optional: true, why: "stop and disable the unit" }] };
4096
+ return {
4097
+ files,
4098
+ commands: [{ file: "powershell.exe", args: ["-NoProfile", "-Command", `Stop-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName ${ps(WINDOWS_TASK)} -Confirm:$false`], optional: true, why: "remove the Scheduled Task" }]
4099
+ };
4100
+ }
4101
+ var defaultWrite = (path, content) => {
4102
+ mkdirSync9(path.replace(/[\\/][^\\/]*$/, ""), { recursive: true });
4103
+ writeFileSync7(path, content);
4104
+ };
4105
+ var defaultRun = (file, args) => void execFileSync3(file, args, { stdio: "pipe", timeout: 6e4 });
4106
+ function installService(ctx, deps = {}) {
4107
+ const plan = servicePlan(ctx);
4108
+ const lines = [];
4109
+ for (const f of plan.files) {
4110
+ (deps.write ?? defaultWrite)(f.path, f.content);
4111
+ lines.push(`wrote ${f.path}`);
4112
+ }
4113
+ let ok = true;
4114
+ for (const c2 of plan.commands) {
4115
+ try {
4116
+ (deps.run ?? defaultRun)(c2.file, c2.args);
4117
+ lines.push(`${c2.why}: ok`);
4118
+ } catch (e) {
4119
+ const why = String(e.stderr ?? "").trim() || (e instanceof Error ? e.message : String(e));
4120
+ lines.push(`${c2.why}: ${c2.optional ? "skipped" : "FAILED"} \u2014 ${why.split(/\r?\n/)[0].slice(0, 200)}`);
4121
+ if (!c2.optional) ok = false;
4122
+ }
4123
+ }
4124
+ return { ok, lines: [...lines, ...plan.notes] };
4125
+ }
4126
+ function uninstallService(ctx, deps = {}) {
4127
+ const plan = serviceRemovalPlan(ctx);
4128
+ const lines = [];
4129
+ for (const c2 of plan.commands) {
4130
+ try {
4131
+ (deps.run ?? defaultRun)(c2.file, c2.args);
4132
+ lines.push(`${c2.why}: ok`);
4133
+ } catch (e) {
4134
+ lines.push(`${c2.why}: skipped \u2014 ${(e instanceof Error ? e.message : String(e)).split(/\r?\n/)[0].slice(0, 200)}`);
4135
+ }
4136
+ }
4137
+ for (const f of plan.files) {
4138
+ (deps.remove ?? ((p) => rmSync(p, { force: true })))(f);
4139
+ lines.push(`removed ${f}`);
4140
+ }
4141
+ return { lines };
4142
+ }
4143
+
3853
4144
  // src/commands/up.ts
3854
4145
  var API_BASE3 = "https://api.proagentstore.online";
3855
4146
  var CLI_VERSION2 = createRequire2(import.meta.url)("../package.json").version;
3856
- var upCommand = new Command10("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").option("--force", "Take over from another connected machine").option("--replace", "Stop the `pags up` already running here for this account, then take over").option("--now", "With --replace: stop it even though work is running on it (that work is lost)").option("--background", "Run the runner in the background, with no terminal open (stop it with `pags down`)").action(async (opts) => {
4147
+ var upCommand = new Command10("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").option("--force", "Take over from another connected machine").option("--replace", "Stop the `pags up` already running here for this account, then take over").option("--now", "With --replace: stop it even though work is running on it (that work is lost)").option("--background", "Run the runner in the background, with no terminal open (stop it with `pags down`)").option("--install-service", "Install an OS service that keeps `pags up` running (launchd, systemd or a Scheduled Task), then exit").option("--uninstall-service", "Remove the service --install-service installed, then exit").action(async (opts) => {
4148
+ if (opts.uninstallService) {
4149
+ for (const line of uninstallService(serviceContext()).lines) writeLine(line);
4150
+ return;
4151
+ }
3857
4152
  const session = requireSession();
4153
+ if (opts.installService) {
4154
+ const result = installService(serviceContext());
4155
+ for (const line of result.lines) writeLine(line);
4156
+ writeLine(result.ok ? "The service starts `pags up` now and whenever it exits. A `pags up` already running here keeps the lock; the service takes over once it stops." : "The service was not fully installed \u2014 see the FAILED line above.");
4157
+ if (!result.ok) process.exitCode = 1;
4158
+ return;
4159
+ }
3858
4160
  const state = {
3859
4161
  user: session.user.login,
3860
4162
  instances: [],
@@ -3871,18 +4173,34 @@ var upCommand = new Command10("up").description("Start the browser runner for al
3871
4173
  // as taking a few seconds, indefinitely, because nothing counted (#497).
3872
4174
  startedAt: Date.now()
3873
4175
  };
4176
+ const unattended = process.env.PAGS_NO_PROMPT === "1";
4177
+ logUpdateStep(`start: pags up ${CLI_VERSION2}${unattended ? " (unattended)" : ""}`);
3874
4178
  clearScreen();
3875
4179
  printLogo(CLI_VERSION2);
3876
4180
  printStep("Signed in as " + session.user.login, "ok");
3877
4181
  printStep("Fetching instances...", "wait");
3878
- const res = await fetch(`${API_BASE3}/v1/instances/my/instances`, {
3879
- headers: { Authorization: `Bearer ${session.token}` }
3880
- });
3881
- if (!res.ok) {
3882
- printStep("Failed to fetch instances: " + res.status, "fail");
3883
- process.exit(1);
4182
+ let data;
4183
+ for (let failures = 1; ; failures++) {
4184
+ let failure;
4185
+ try {
4186
+ const res = await fetch(`${API_BASE3}/v1/instances/my/instances`, { headers: { Authorization: `Bearer ${session.token}` } });
4187
+ if (res.ok) {
4188
+ data = await res.json();
4189
+ break;
4190
+ }
4191
+ failure = `HTTP ${res.status}`;
4192
+ } catch (e) {
4193
+ failure = e instanceof Error ? e.message : String(e);
4194
+ }
4195
+ logUpdateStep(`start: fetching instances failed (${failure})${unattended ? "" : " \u2014 exiting"}`);
4196
+ if (!unattended) {
4197
+ printStep("Failed to fetch instances: " + failure, "fail");
4198
+ process.exit(1);
4199
+ }
4200
+ const wait = Math.min(5 * 6e4, 5e3 * 2 ** (failures - 1));
4201
+ printStep(`Failed to fetch instances (${failure}) \u2014 retrying in ${Math.round(wait / 1e3)}s`, "wait");
4202
+ await new Promise((resolve5) => setTimeout(resolve5, wait));
3884
4203
  }
3885
- const data = await res.json();
3886
4204
  const active = (data.instances || []).filter((i) => i.status === "active");
3887
4205
  let instances = active;
3888
4206
  let elsewhere = [];
@@ -3930,9 +4248,10 @@ var upCommand = new Command10("up").description("Start the browser runner for al
3930
4248
  state.activeInstance = instances.length === 1 ? instances[0].name || instances[0].slug || instances[0].id.slice(0, 8) : `${instances.length} agents`;
3931
4249
  printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
3932
4250
  const lockFile = lockPath(session.user.login);
3933
- if (opts.background && process.env[BACKGROUND_RESTART_ENV] === "1") await waitUntilTakeable(() => inspectHolder(lockFile));
4251
+ if (unattended || opts.background && process.env[BACKGROUND_RESTART_ENV] === "1") await waitUntilTakeable(() => inspectHolder(lockFile), { timeoutMs: 6e4 });
3934
4252
  const held = await inspectHolder(lockFile);
3935
4253
  if (held.lock && !isTakeable(held.verdict)) {
4254
+ logUpdateStep(`start: the lock is held by pid ${held.lock.pid} (${held.verdict})${opts.replace ? " \u2014 replacing it" : " \u2014 exiting"}`);
3936
4255
  if (!opts.replace) {
3937
4256
  clearScreen();
3938
4257
  printLogo(CLI_VERSION2);
@@ -4037,10 +4356,12 @@ var upCommand = new Command10("up").description("Start the browser runner for al
4037
4356
  };
4038
4357
  child.on("exit", (code, signal) => {
4039
4358
  if (code === RUNNER_RESTART_EXIT_CODE) {
4359
+ logUpdateStep("restart: runner connect asked for a restart (exit 75)");
4040
4360
  restartUp("Runner updated remotely \u2014 restarting pags up on the new version\u2026", { PAGS_NO_PROMPT: "1" });
4041
4361
  return;
4042
4362
  }
4043
4363
  childDead = true;
4364
+ logUpdateStep(`runner connect ${signal ? `was killed by ${signal}` : `exited ${code}`}${stopping ? " (stopping)" : ""}`);
4044
4365
  if (code !== 0 && !stopping) recordConnectExit(code, signal ?? null, logs.slice(-20));
4045
4366
  if (code === null) {
4046
4367
  state.runner = "error";
@@ -4125,6 +4446,26 @@ var downCommand = new Command10("down").description("Stop the browser runner and
4125
4446
  }
4126
4447
  writeLine("");
4127
4448
  });
4449
+ function serviceContext() {
4450
+ const user = (() => {
4451
+ try {
4452
+ return userInfo().username;
4453
+ } catch {
4454
+ return process.env.USER || process.env.USERNAME || void 0;
4455
+ }
4456
+ })();
4457
+ return {
4458
+ platform: process.platform,
4459
+ home: homedir5(),
4460
+ nodePath: process.execPath,
4461
+ script: process.argv[1],
4462
+ configDir: configDir(),
4463
+ pathEnv: process.env.PATH ?? "",
4464
+ uid: process.getuid?.(),
4465
+ user,
4466
+ domain: process.env.USERDOMAIN
4467
+ };
4468
+ }
4128
4469
 
4129
4470
  // src/index.ts
4130
4471
  var require2 = createRequire3(import.meta.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.149",
3
+ "version": "0.4.150",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,138 +0,0 @@
1
- // src/bootstrap/payload.ts
2
- import { execFile } from "child_process";
3
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync } from "fs";
4
- import { homedir } from "os";
5
- import { join, win32 } from "path";
6
- import { promisify } from "util";
7
- var run = promisify(execFile);
8
- var CLI_PACKAGE = "@proagentstore/cli";
9
- var BOOTSTRAP_ENV = "PAGS_BOOTSTRAP";
10
- var NO_SELF_UPDATE_ENV = "PAGS_NO_SELF_UPDATE";
11
- var KEEP = 2;
12
- function payloadRoot() {
13
- return process.env.PAGS_CLI_CACHE || join(homedir(), ".config", "proagentstore", "cli");
14
- }
15
- function isVersion(v) {
16
- return typeof v === "string" && /^(\d+)\.(\d+)\.(\d+)/.test(v.trim());
17
- }
18
- function olderThan(a, b) {
19
- const parse = (v) => /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim())?.slice(1).map(Number);
20
- const x = parse(a);
21
- const y = parse(b);
22
- if (!x || !y) return false;
23
- for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i];
24
- return false;
25
- }
26
- function payloadAt(dir) {
27
- try {
28
- const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
29
- if (pkg.name !== CLI_PACKAGE || !pkg.version) return null;
30
- const entry = join(dir, pkg.pagsPayload || "dist/index.js");
31
- return existsSync(entry) ? { version: pkg.version, entry } : null;
32
- } catch {
33
- return null;
34
- }
35
- }
36
- var installedAt = (prefix) => join(prefix, "node_modules", ...CLI_PACKAGE.split("/"));
37
- var packageDir = (root, version) => installedAt(join(root, version));
38
- function cachedPayloads(root = payloadRoot()) {
39
- let names;
40
- try {
41
- names = readdirSync(root);
42
- } catch {
43
- return [];
44
- }
45
- return names.flatMap((v) => v.startsWith(".") ? [] : payloadAt(packageDir(root, v)) ?? []);
46
- }
47
- function newestPayload(payloads) {
48
- return payloads.reduce((best, p) => !best || olderThan(best.version, p.version) ? p : best, null);
49
- }
50
- var CMD_META = /([()\][%!^"`<>&|;, *?])/g;
51
- function quoteCmdArg(arg) {
52
- const quoted = `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
53
- return quoted.replace(CMD_META, "^$1").replace(CMD_META, "^$1");
54
- }
55
- var envVar = (env, name) => env[Object.keys(env).find((k) => k.toUpperCase() === name) ?? name];
56
- function npmInvocation(args, deps = {}) {
57
- if ((deps.platform ?? process.platform) !== "win32") return { file: "npm", args };
58
- const env = deps.env ?? process.env;
59
- const exists = deps.exists ?? existsSync;
60
- const execPath = deps.execPath ?? process.execPath;
61
- const cli = (dir) => win32.join(dir, "node_modules", "npm", "bin", "npm-cli.js");
62
- const onPath = (envVar(env, "PATH") ?? "").split(";").filter((dir) => dir && exists(win32.join(dir, "npm.cmd")));
63
- for (const dir of [...onPath, win32.dirname(execPath)]) {
64
- if (exists(cli(dir))) return { file: execPath, args: [cli(dir), ...args] };
65
- }
66
- const line = ["npm", ...args.map(quoteCmdArg)].join(" ");
67
- return { file: envVar(env, "COMSPEC") || "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], windowsVerbatimArguments: true };
68
- }
69
- function runNpm(args, options) {
70
- const npm = npmInvocation(args);
71
- return run(npm.file, npm.args, { ...options, ...npm.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {} });
72
- }
73
- async function latestPublishedRelease(timeoutMs = 3e4, signal, deps = {}) {
74
- try {
75
- const { stdout } = await (deps.runNpm ?? runNpm)(["view", CLI_PACKAGE, "version"], { timeout: timeoutMs, signal });
76
- const version = String(stdout).trim();
77
- return version ? { version } : { error: "npm printed no version" };
78
- } catch (e) {
79
- return { error: npmFailure(e) };
80
- }
81
- }
82
- function npmFailure(e) {
83
- const err = e;
84
- const stderr = String(err?.stderr ?? "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
85
- const npmErrors = stderr.filter((l) => /npm (ERR!|error)/i.test(l));
86
- const why = npmErrors.slice(0, 2).join(" ") || stderr.at(-1) || (err?.killed || err?.signal ? `npm did not answer in time (${String(err.signal ?? "killed")})` : "") || (typeof err?.code === "string" ? `npm could not be started (${err.code}: ${String(err.message ?? "")})` : "") || String(err?.message ?? e);
87
- return why.replace(/\s+/g, " ").trim().slice(0, 300);
88
- }
89
- async function latestPublishedVersion(timeoutMs = 3e4, signal) {
90
- const r = await latestPublishedRelease(timeoutMs, signal);
91
- return "version" in r ? r.version : null;
92
- }
93
- async function npmInstallInto(dir, version) {
94
- try {
95
- await runNpm(["install", "--prefix", dir, "--omit=dev", "--no-audit", "--no-fund", "--no-save", `${CLI_PACKAGE}@${version}`], { timeout: 5 * 6e4 });
96
- } catch (e) {
97
- const stderr = String(e.stderr ?? "").trim();
98
- throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
99
- }
100
- }
101
- async function installPayload(version, root = payloadRoot(), install = npmInstallInto) {
102
- const done = payloadAt(packageDir(root, version));
103
- if (done) return done;
104
- mkdirSync(root, { recursive: true });
105
- const scratch = mkdtempSync(join(root, `.install-${version}-`));
106
- try {
107
- await install(scratch, version);
108
- if (payloadAt(installedAt(scratch))?.version !== version) throw new Error(`npm installed no ${CLI_PACKAGE}@${version}`);
109
- try {
110
- renameSync(scratch, join(root, version));
111
- } catch (e) {
112
- if (!payloadAt(packageDir(root, version))) throw e;
113
- }
114
- } finally {
115
- rmSync(scratch, { recursive: true, force: true });
116
- }
117
- const payload = payloadAt(packageDir(root, version));
118
- if (!payload) throw new Error(`${CLI_PACKAGE}@${version} is not usable after install`);
119
- for (const old of cachedPayloads(root).sort((a, b) => olderThan(a.version, b.version) ? 1 : -1).slice(KEEP)) {
120
- rmSync(join(root, old.version), { recursive: true, force: true });
121
- }
122
- return payload;
123
- }
124
-
125
- export {
126
- CLI_PACKAGE,
127
- BOOTSTRAP_ENV,
128
- NO_SELF_UPDATE_ENV,
129
- isVersion,
130
- olderThan,
131
- payloadAt,
132
- cachedPayloads,
133
- newestPayload,
134
- runNpm,
135
- latestPublishedRelease,
136
- latestPublishedVersion,
137
- installPayload
138
- };