@proagentstore/cli 0.4.124 → 0.4.126

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.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Native desktop / Maestro runtime (`local_native`, #1057) — the wire contract between the API
3
+ * Worker and the runner.
4
+ *
5
+ * This file is copied BYTE-FOR-BYTE between `packages/browser-runner/src/native/contract.ts` and
6
+ * `workers/api/src/lib/native/contract.ts` (the two packages cannot import each other), and
7
+ * `workers/api/src/lib/native/contract.test.ts` fails if they differ. It imports nothing.
8
+ *
9
+ * Slice 1 is the FOUNDATION only: a read-only preflight that reports what this machine could do,
10
+ * plus the run/event shapes the later slices will use. Nothing here drives a screen, a mouse, a
11
+ * keyboard or a Maestro flow yet.
12
+ */
13
+ /** The runner route the API calls (over the relay) to probe a machine. Read-only. */
14
+ export const NATIVE_PREFLIGHT_PATH = "/native/preflight";
15
+ /** The capability a runner advertises once it can answer {@link NATIVE_PREFLIGHT_PATH}. */
16
+ export const NATIVE_PREFLIGHT_CAPABILITY = "native.preflight";
17
+ export const NATIVE_BACKENDS = ["desktop", "maestro"];
@@ -0,0 +1,172 @@
1
+ /**
2
+ * `/native/preflight` (#1057, slice 1): what this machine could do for the `local_native` runtime
3
+ * — desktop control permissions and Maestro devices — WITHOUT doing any of it.
4
+ *
5
+ * Rules this module keeps:
6
+ * - READ-ONLY. It runs fixed probe commands and installs nothing. It never prompts for an OS
7
+ * permission either: on macOS the two checks are the "preflight" APIs that only report state
8
+ * (`CGPreflightScreenCaptureAccess`, `AXIsProcessTrusted`), not the "request" ones that open a
9
+ * system dialog. (`screencapture` is NOT used as the probe: without Screen Recording it still
10
+ * exits 0 and captures only the wallpaper, so its exit code proves nothing.)
11
+ * - NEVER THROWS. A missing tool, a timeout or garbage output becomes a state (`unknown`, not
12
+ * installed, no devices) and a stable error code with a sentence the owner can act on.
13
+ * - Parsing is pure and exported, so it is tested on recorded outputs — no live device needed.
14
+ *
15
+ * `adb devices` starts the adb server if it is not already running; that is adb's own behaviour
16
+ * when asked to list devices, not something installed or configured here.
17
+ */
18
+ import { execFile } from "node:child_process";
19
+ import { arch as osArch, platform as osPlatform } from "node:os";
20
+ /** Run one probe command; never rejects. */
21
+ export const execProbe = (cmd, args, timeoutMs = 8000) => new Promise((resolve) => {
22
+ execFile(cmd, args, { timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
23
+ const e = err;
24
+ if (e?.code === "ENOENT")
25
+ return resolve({ found: false, code: null, stdout: "", stderr: "" });
26
+ resolve({
27
+ found: true,
28
+ code: e ? (typeof e.code === "number" ? e.code : 1) : 0,
29
+ stdout: String(stdout ?? ""),
30
+ stderr: String(stderr ?? ""),
31
+ ...(e?.killed ? { timedOut: true } : {}),
32
+ });
33
+ });
34
+ });
35
+ // ── Pure parsers ────────────────────────────────────────────────────────────
36
+ /** `osascript -l JavaScript` printing `true`/`false` → a permission state. */
37
+ export function parseBooleanProbe(r) {
38
+ if (!r.found || r.timedOut || r.code !== 0)
39
+ return "unknown";
40
+ const out = r.stdout.trim().toLowerCase();
41
+ return out === "true" ? "granted" : out === "false" ? "denied" : "unknown";
42
+ }
43
+ /** `maestro --version` → its version, or null when it printed nothing recognisable. */
44
+ export function parseMaestroVersion(r) {
45
+ if (!r.found || r.code !== 0)
46
+ return null;
47
+ const m = r.stdout.match(/\d+\.\d+(?:\.\d+)?/);
48
+ if (m)
49
+ return m[0];
50
+ // No version number: keep a short first line verbatim, and refuse a long one rather than cut it.
51
+ const first = r.stdout.trim().split("\n")[0] ?? "";
52
+ return first && first.length <= 40 ? first : null;
53
+ }
54
+ /** `adb devices -l` → attached devices (any state; `device` is the usable one). */
55
+ export function parseAdbDevices(stdout) {
56
+ const out = [];
57
+ for (const line of stdout.split("\n")) {
58
+ const t = line.trim();
59
+ if (!t || t.startsWith("List of devices") || t.startsWith("*"))
60
+ continue;
61
+ const [id, state, ...rest] = t.split(/\s+/);
62
+ if (!id || !state)
63
+ continue;
64
+ const model = rest.find((p) => p.startsWith("model:"))?.slice("model:".length);
65
+ out.push({ id, kind: "android", name: model ? model.replace(/_/g, " ") : id, state });
66
+ }
67
+ return out;
68
+ }
69
+ /** `xcrun simctl list devices booted --json` → booted simulators. */
70
+ export function parseSimctlBooted(stdout) {
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(stdout);
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ const devices = parsed?.devices;
79
+ if (!devices || typeof devices !== "object")
80
+ return [];
81
+ const out = [];
82
+ for (const list of Object.values(devices)) {
83
+ if (!Array.isArray(list))
84
+ continue;
85
+ for (const d of list) {
86
+ if (typeof d?.udid !== "string" || d.state !== "Booted")
87
+ continue;
88
+ out.push({ id: d.udid, kind: "ios_simulator", name: typeof d.name === "string" ? d.name : d.udid, state: "Booted" });
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+ /** Combine the parts into one result with its error codes. Pure. */
94
+ export function summarizePreflight(input) {
95
+ const { screenRecording, accessibility } = input;
96
+ const states = [screenRecording, accessibility];
97
+ const permissionState = states.includes("unsupported")
98
+ ? "unsupported"
99
+ : states.includes("denied")
100
+ ? "denied"
101
+ : states.every((s) => s === "granted")
102
+ ? "granted"
103
+ : "unknown";
104
+ const errors = [];
105
+ const add = (code, message) => errors.push({ code, message });
106
+ if (permissionState === "unsupported")
107
+ add("NATIVE_UNSUPPORTED_OS", `Desktop control is not supported on ${input.os} yet (macOS and Windows only).`);
108
+ if (permissionState === "denied") {
109
+ const missing = [screenRecording === "denied" ? "Screen Recording" : null, accessibility === "denied" ? "Accessibility" : null].filter(Boolean).join(" and ");
110
+ add("NATIVE_PERMISSIONS_MISSING", `This machine has not granted ${missing}. Grant it to the app that runs \`pags up\` (e.g. Terminal) in System Settings → Privacy & Security, then restart \`pags up\`.`);
111
+ }
112
+ if (!input.maestroInstalled)
113
+ add("NATIVE_MAESTRO_NOT_INSTALLED", "Maestro is not installed on this machine. The runner never installs it; install it yourself (https://maestro.mobile.dev) if you want phone or emulator automation.");
114
+ const usable = input.devices.filter((d) => d.state === "device" || d.state === "Booted");
115
+ if (input.maestroInstalled && usable.length === 0) {
116
+ add("NATIVE_DEVICE_NOT_FOUND", input.devices.length ? `No usable device: ${input.devices.map((d) => `${d.name} (${d.state})`).join(", ")}. Authorise or boot one.` : "No Android device/emulator or booted iOS simulator is connected.");
117
+ }
118
+ return {
119
+ os: input.os,
120
+ osVersion: input.osVersion,
121
+ arch: input.arch,
122
+ desktop: { screenRecording, accessibility, permissionState },
123
+ maestro: { installed: input.maestroInstalled, version: input.maestroVersion, adbInstalled: input.adbInstalled, simctlAvailable: input.simctlAvailable, connectedDevices: input.devices },
124
+ errors,
125
+ checkedAt: input.checkedAt,
126
+ };
127
+ }
128
+ // ── The probe ───────────────────────────────────────────────────────────────
129
+ // CGPreflightScreenCaptureAccess is not in JXA's bridge metadata, so it is bound explicitly —
130
+ // without the bind the call is "not a function" and the state would read `unknown`.
131
+ const JXA_SCREEN = 'ObjC.import("CoreGraphics"); ObjC.bindFunction("CGPreflightScreenCaptureAccess", ["bool", []]); $.CGPreflightScreenCaptureAccess()';
132
+ const JXA_AX = 'ObjC.import("ApplicationServices"); $.AXIsProcessTrusted()';
133
+ const PS_UIA = "Add-Type -AssemblyName UIAutomationClient; if ([System.Windows.Automation.AutomationElement]::RootElement) { 'true' } else { 'false' }";
134
+ export async function runNativePreflight(deps = {}) {
135
+ const exec = deps.exec ?? execProbe;
136
+ const platform = deps.platform ?? osPlatform();
137
+ const safe = (p) => p.catch(() => ({ found: false, code: null, stdout: "", stderr: "" }));
138
+ let osVersion = null;
139
+ let screenRecording = "unsupported";
140
+ let accessibility = "unsupported";
141
+ if (platform === "darwin") {
142
+ const [ver, screen, ax] = await Promise.all([safe(exec("sw_vers", ["-productVersion"])), safe(exec("osascript", ["-l", "JavaScript", "-e", JXA_SCREEN])), safe(exec("osascript", ["-l", "JavaScript", "-e", JXA_AX]))]);
143
+ osVersion = ver.code === 0 ? ver.stdout.trim() || null : null;
144
+ screenRecording = parseBooleanProbe(screen);
145
+ accessibility = parseBooleanProbe(ax);
146
+ }
147
+ else if (platform === "win32") {
148
+ // Windows has no per-app permission for screen capture or UI Automation: one availability probe.
149
+ const uia = parseBooleanProbe(await safe(exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_UIA], 15000)));
150
+ screenRecording = uia;
151
+ accessibility = uia;
152
+ }
153
+ const [maestro, adb, simctl] = await Promise.all([
154
+ safe(exec("maestro", ["--version"], 15000)),
155
+ safe(exec("adb", ["devices", "-l"])),
156
+ platform === "darwin" ? safe(exec("xcrun", ["simctl", "list", "devices", "booted", "--json"])) : Promise.resolve({ found: false, code: null, stdout: "", stderr: "" }),
157
+ ]);
158
+ const devices = [...(adb.found && adb.code === 0 ? parseAdbDevices(adb.stdout) : []), ...(simctl.found && simctl.code === 0 ? parseSimctlBooted(simctl.stdout) : [])];
159
+ return summarizePreflight({
160
+ os: platform,
161
+ osVersion,
162
+ arch: deps.arch ?? osArch(),
163
+ screenRecording,
164
+ accessibility,
165
+ maestroVersion: parseMaestroVersion(maestro),
166
+ maestroInstalled: maestro.found && maestro.code === 0,
167
+ adbInstalled: adb.found,
168
+ simctlAvailable: simctl.found && simctl.code === 0,
169
+ devices,
170
+ checkedAt: (deps.now ?? Date.now)(),
171
+ });
172
+ }
@@ -39,6 +39,7 @@ const CAPABILITIES = [
39
39
  "human.approval",
40
40
  "human.takeover",
41
41
  "tmux.control",
42
+ "native.preflight",
42
43
  ];
43
44
  /** How many times a task may attempt an action before handing off to a human. */
44
45
  export const MAX_AUTONOMOUS_ATTEMPTS = 3;
@@ -5,6 +5,7 @@ import { probeGitSshIdentity } from "./coding/repo.js";
5
5
  import { checkEngine } from "./coding/engine-check.js";
6
6
  import { listGithubOrgs, listGithubRepos, searchGithubRepos, getGithubRepoDetail, getGithubCredentialScope } from "./coding/github-browse.js";
7
7
  import { routeLocalApplyHandoff } from "./local-apply/handoff-routes.js";
8
+ import { runNativePreflight } from "./native/preflight.js";
8
9
  import { routeLocalApplyReconciliation } from "./local-apply/reconciliation-routes.js";
9
10
  export function createRunnerServer(runner) {
10
11
  return createServer(async (req, res) => {
@@ -105,6 +106,11 @@ async function route(runner, req, res) {
105
106
  if ((req.method === "GET" || req.method === "POST") && path === "/capabilities") {
106
107
  return json(res, 200, runner.capabilities());
107
108
  }
109
+ // #1057 slice 1: a READ-ONLY probe of what native desktop / Maestro control could do on this
110
+ // machine. Runs fixed probe commands, installs nothing, prompts for nothing, never throws.
111
+ if ((req.method === "GET" || req.method === "POST") && path === "/native/preflight") {
112
+ return json(res, 200, await runNativePreflight());
113
+ }
108
114
  if (req.method === "GET" && path === "/sessions") {
109
115
  return json(res, 200, { sessions: runner.store.listSessions() });
110
116
  }
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  // src/index.ts
13
13
  import { createRequire as createRequire3 } from "module";
14
- import { Command as Command10 } from "commander";
14
+ import { Command as Command11 } from "commander";
15
15
 
16
16
  // src/commands/check.ts
17
17
  import { existsSync, readFileSync } from "fs";
@@ -1548,6 +1548,7 @@ var isTakeable = (verdict) => verdict === "dead";
1548
1548
  function holderWhere(lock) {
1549
1549
  if (lock.launch === "tmux") return `tmux session ${lock.launchDetail || "?"}`;
1550
1550
  if (lock.launch === "service") return "a background service (launchd/systemd)";
1551
+ if (lock.launch === "background") return `the background (\`pags up --background\`${lock.launchDetail ? `, log ${lock.launchDetail}` : ""})`;
1551
1552
  if (lock.launch === "headless") return "headless";
1552
1553
  return lock.launchDetail ? `terminal ${lock.launchDetail}` : "a terminal";
1553
1554
  }
@@ -1565,6 +1566,7 @@ function holderMessage(lock, verdict) {
1565
1566
  }
1566
1567
  lines.push("", " What you can do:");
1567
1568
  if (lock.launch === "tmux" && lock.launchDetail) lines.push(` tmux attach -t ${lock.launchDetail} \u2014 go to the one that is running`);
1569
+ if (lock.launch === "background") lines.push(` pags down \u2014 stop it (it has no window to close)`);
1568
1570
  lines.push(
1569
1571
  ` pags status \u2014 what it is doing, and anything else running here`,
1570
1572
  lock.launch === "service" ? ` (this one is a service \u2014 stop its launchd/systemd unit rather than replacing it)` : ` pags up --replace \u2014 stop that one gracefully, then take over`
@@ -1573,6 +1575,7 @@ function holderMessage(lock, verdict) {
1573
1575
  }
1574
1576
  function detectLaunch(env = process.env, isTty = Boolean(process.stdout.isTTY)) {
1575
1577
  if (env.PAGS_SERVICE === "1") return { launch: "service", detail: env.PAGS_SERVICE_NAME?.trim() || "" };
1578
+ if (env.PAGS_BACKGROUND === "1") return { launch: "background", detail: env.PAGS_BACKGROUND_LOG?.trim() || "" };
1576
1579
  if (env.TMUX) return { launch: "tmux", detail: env.TMUX_PANE?.trim() || env.TMUX.split(",")[0]?.split("/").pop() || "" };
1577
1580
  if (!isTty) return { launch: "headless", detail: "" };
1578
1581
  return { launch: "tty", detail: env.TERM_SESSION_ID?.trim() || env.WINDOWID?.trim() || "" };
@@ -3094,9 +3097,148 @@ function createRunnerCommand() {
3094
3097
  // src/commands/runner.ts
3095
3098
  var runnerCommand = createRunnerCommand();
3096
3099
 
3100
+ // src/commands/status.ts
3101
+ import { readFileSync as readFileSync9, statSync as statSync2, watchFile } from "fs";
3102
+ import { Command as Command9 } from "commander";
3103
+
3104
+ // src/background.ts
3105
+ import { spawn as nodeSpawn } from "child_process";
3106
+ import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync8, renameSync as renameSync3, statSync, writeFileSync as writeFileSync6 } from "fs";
3107
+ import { dirname as dirname4, join as join9 } from "path";
3108
+ var BACKGROUND_ENV = "PAGS_BACKGROUND";
3109
+ var BACKGROUND_LOG_ENV = "PAGS_BACKGROUND_LOG";
3110
+ var BACKGROUND_RESTART_ENV = "PAGS_BACKGROUND_RESTART";
3111
+ var LOG_ROTATE_BYTES = 5 * 1024 * 1024;
3112
+ var backgroundLogPath = (account, env = process.env) => join9(configDir(env), "logs", `runner-${accountKey(account)}.log`);
3113
+ var backgroundStatePath = (account, env = process.env) => join9(configDir(env), `background-${accountKey(account)}.json`);
3114
+ function writeBackgroundState(path, state) {
3115
+ mkdirSync7(dirname4(path), { recursive: true });
3116
+ writeFileSync6(path, `${JSON.stringify(state)}
3117
+ `, { mode: 384 });
3118
+ }
3119
+ function readBackgroundState(path) {
3120
+ try {
3121
+ const parsed = JSON.parse(readFileSync8(path, "utf-8"));
3122
+ return parsed?.v === 1 && Number.isInteger(parsed.pid) ? parsed : null;
3123
+ } catch {
3124
+ return null;
3125
+ }
3126
+ }
3127
+ function rotateLog(path, maxBytes = LOG_ROTATE_BYTES) {
3128
+ try {
3129
+ if (statSync(path).size <= maxBytes) return false;
3130
+ } catch {
3131
+ return false;
3132
+ }
3133
+ renameSync3(path, `${path}.1`);
3134
+ return true;
3135
+ }
3136
+ function shellQuote(arg, platform = process.platform) {
3137
+ if (platform === "win32") return `"${arg.replace(/"/g, '""')}"`;
3138
+ return /^[A-Za-z0-9_./:=@%+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
3139
+ }
3140
+ function backgroundRestartCommand(execPath, script, upArgs, platform = process.platform) {
3141
+ const argv = [execPath, script, ...upArgs, "--background"].map((a) => shellQuote(a, platform)).join(" ");
3142
+ return platform === "win32" ? `set ${BACKGROUND_RESTART_ENV}=1&& ${argv}` : `${BACKGROUND_RESTART_ENV}=1 ${argv}`;
3143
+ }
3144
+ function spawnDetached(command, args, env, logPath, spawn5 = nodeSpawn) {
3145
+ mkdirSync7(dirname4(logPath), { recursive: true });
3146
+ rotateLog(logPath);
3147
+ const fd = openSync2(logPath, "a", 384);
3148
+ try {
3149
+ const child = spawn5(command, args, { detached: true, stdio: ["ignore", fd, fd], env, windowsHide: true });
3150
+ child.unref();
3151
+ if (!child.pid) throw new Error("the runner process did not start");
3152
+ return child.pid;
3153
+ } finally {
3154
+ closeSync2(fd);
3155
+ }
3156
+ }
3157
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3158
+ async function waitForBackgroundHolder(childPid, read, alive, opts = {}) {
3159
+ const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
3160
+ while (Date.now() < deadline) {
3161
+ if (!alive(childPid)) return "exited";
3162
+ const held = await read();
3163
+ if (held.lock?.pid === childPid && held.verdict === "live") return "live";
3164
+ await sleep(opts.intervalMs ?? 500);
3165
+ }
3166
+ return alive(childPid) ? "timeout" : "exited";
3167
+ }
3168
+ async function waitUntilTakeable(read, opts = {}) {
3169
+ const deadline = Date.now() + (opts.timeoutMs ?? 15e3);
3170
+ for (; ; ) {
3171
+ const held = await read();
3172
+ if (!held.lock || isTakeable(held.verdict)) return true;
3173
+ if (Date.now() >= deadline) return false;
3174
+ await sleep(opts.intervalMs ?? 500);
3175
+ }
3176
+ }
3177
+ function tailLines(path, n) {
3178
+ if (!existsSync8(path)) return null;
3179
+ const lines = readFileSync8(path, "utf-8").split("\n");
3180
+ if (lines.at(-1) === "") lines.pop();
3181
+ return lines.slice(Math.max(0, lines.length - n));
3182
+ }
3183
+
3184
+ // src/commands/status.ts
3185
+ function describeStatus(lock, verdict, bg, now = Date.now()) {
3186
+ if (!lock || verdict === "dead") {
3187
+ const lines2 = ["Not running \u2014 no runner of yours is connected from this machine."];
3188
+ if (bg) lines2.push(`The last background runner (pid ${bg.pid}, started ${new Date(bg.startedAt).toLocaleString()}) has stopped. Its log: ${bg.logPath} (\`pags logs\`).`);
3189
+ lines2.push("", "Start one with `pags up` (this terminal) or `pags up --background` (no terminal).");
3190
+ return { running: false, lines: lines2 };
3191
+ }
3192
+ const mins = Math.max(0, Math.round((now - lock.startedAt) / 6e4));
3193
+ const state = verdict === "live" ? "Running" : verdict === "unresponsive" ? "Running, but NOT answering its health check (asleep, stopped or overloaded)" : "Probably running (its lock could not be fully read)";
3194
+ const lines = [
3195
+ `${state} \u2014 pid ${lock.pid}, up ${mins < 120 ? `${mins} min` : `${Math.round(mins / 60)} h`}, CLI ${lock.version || "?"}, machine ${lock.node}.`,
3196
+ `Launched from ${holderWhere(lock)}.`
3197
+ ];
3198
+ if (lock.launch === "background" && bg && bg.pid === lock.pid) {
3199
+ lines.push(`Started with ${bg.instances.length} agent${bg.instances.length === 1 ? "" : "s"}${bg.watching ? " (newly eligible ones attach as they appear)" : " (scoped: only these)"}:`);
3200
+ for (const i of bg.instances) lines.push(` ${i.name} (${i.id.slice(0, 8)})`);
3201
+ lines.push("", `Logs: \`pags logs\` (${bg.logPath}). Stop: \`pags down\`.`);
3202
+ } else if (lock.launch === "background") {
3203
+ lines.push("", "Logs: `pags logs`. Stop: `pags down`.");
3204
+ }
3205
+ return { running: true, lines };
3206
+ }
3207
+ var statusCommand = new Command9("status").description("Is a runner running on this machine for your account \u2014 and how was it started").action(async () => {
3208
+ const session = requireSession();
3209
+ const held = await inspectHolder(lockPath(session.user.login));
3210
+ const { running, lines } = describeStatus(held.lock, held.lock ? held.verdict : null, readBackgroundState(backgroundStatePath(session.user.login)));
3211
+ writeLine("");
3212
+ for (const line of lines) writeLine(` ${line}`);
3213
+ writeLine("");
3214
+ if (!running) process.exitCode = 1;
3215
+ });
3216
+ var logsCommand = new Command9("logs").description("Show the background runner's log (`pags up --background`)").option("-n, --lines <n>", "How many recent lines to show", "50").option("-f, --follow", "Keep printing new lines as they are written").action((opts) => {
3217
+ const session = requireSession();
3218
+ const path = backgroundLogPath(session.user.login);
3219
+ const n = Math.max(1, Number.parseInt(opts.lines, 10) || 50);
3220
+ const lines = tailLines(path, n);
3221
+ if (!lines) {
3222
+ writeLine("");
3223
+ writeLine(" No background runner log here yet \u2014 one is written by `pags up --background`.");
3224
+ writeLine(" A foreground `pags up` shows its output in its own window (press `l`).");
3225
+ writeLine("");
3226
+ return;
3227
+ }
3228
+ for (const line of lines) writeLine(line);
3229
+ if (!opts.follow) return;
3230
+ let offset = statSync2(path).size;
3231
+ watchFile(path, { interval: 1e3 }, (cur) => {
3232
+ if (cur.size < offset) offset = 0;
3233
+ if (cur.size === offset) return;
3234
+ process.stdout.write(readFileSync9(path).subarray(offset, cur.size));
3235
+ offset = cur.size;
3236
+ });
3237
+ });
3238
+
3097
3239
  // src/commands/up.ts
3098
3240
  import { createRequire as createRequire2 } from "module";
3099
- import { Command as Command9 } from "commander";
3241
+ import { Command as Command10 } from "commander";
3100
3242
  import { hostname as hostname7 } from "os";
3101
3243
 
3102
3244
  // src/tui.ts
@@ -3217,7 +3359,7 @@ async function waitForKey(keys, onInterrupt) {
3217
3359
  }
3218
3360
 
3219
3361
  // src/commands/up-restart.ts
3220
- import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
3362
+ import { spawn as nodeSpawn2, spawnSync as nodeSpawnSync } from "child_process";
3221
3363
  var UP_LAUNCHER_ENV = "PAGS_UP_LAUNCHER";
3222
3364
  function upRestartMode(env, platform, canExecve) {
3223
3365
  if (env[SERVICE_ENV] === "1") return "service";
@@ -3234,7 +3376,7 @@ function processRestartDeps() {
3234
3376
  execArgv: process.execArgv,
3235
3377
  script: process.argv[1],
3236
3378
  execve: typeof execve === "function" ? execve.bind(process) : void 0,
3237
- spawn: nodeSpawn,
3379
+ spawn: nodeSpawn2,
3238
3380
  spawnSync: nodeSpawnSync,
3239
3381
  exit: (code) => process.exit(code),
3240
3382
  log: () => {
@@ -3323,7 +3465,7 @@ async function holderWork(lock, deps = {}) {
3323
3465
  }
3324
3466
  async function replaceHolder(lock, opts = {}, deps = {}) {
3325
3467
  const fetchImpl = deps.fetchImpl ?? fetch;
3326
- const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3468
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3327
3469
  const alive = deps.alive ?? pidAlive;
3328
3470
  const now = deps.now ?? Date.now;
3329
3471
  const work = await holderWork(lock, deps);
@@ -3348,7 +3490,7 @@ async function replaceHolder(lock, opts = {}, deps = {}) {
3348
3490
  const until = now() + REPLACE_EXIT_TIMEOUT_MS;
3349
3491
  while (now() < until) {
3350
3492
  if (!alive(lock.pid)) return { ok: true, lines: [`Replaced the \`pags up\` on pid ${lock.pid}`] };
3351
- await sleep(500);
3493
+ await sleep2(500);
3352
3494
  }
3353
3495
  return {
3354
3496
  ok: false,
@@ -3359,10 +3501,67 @@ async function replaceHolder(lock, opts = {}, deps = {}) {
3359
3501
  };
3360
3502
  }
3361
3503
 
3504
+ // src/commands/up-background.ts
3505
+ import { spawn as nodeSpawn3 } from "child_process";
3506
+ var processDeps = () => ({
3507
+ env: process.env,
3508
+ execPath: process.execPath,
3509
+ platform: process.platform,
3510
+ spawn: nodeSpawn3,
3511
+ alive: pidAlive,
3512
+ inspect: inspectHolder,
3513
+ log: (line) => writeLine(line),
3514
+ step: printStep
3515
+ });
3516
+ async function startInBackground(start, deps = processDeps()) {
3517
+ const account = start.session.user.login;
3518
+ const logPath = backgroundLogPath(account, deps.env);
3519
+ const upArgs = restartUpArgs(start.opts);
3520
+ const env = {
3521
+ ...deps.env,
3522
+ PAGS_TOKEN: start.session.token,
3523
+ PAGS_LOCK_ACCOUNT: account,
3524
+ [BACKGROUND_ENV]: "1",
3525
+ [BACKGROUND_LOG_ENV]: logPath,
3526
+ [RESTART_COMMAND_ENV]: backgroundRestartCommand(deps.execPath, start.runnerArgs[0], upArgs, deps.platform)
3527
+ };
3528
+ delete env.PAGS_UP_SUPERVISED;
3529
+ deps.step("Starting the runner in the background\u2026", "wait");
3530
+ const pid = spawnDetached(deps.execPath, start.runnerArgs, env, logPath, deps.spawn);
3531
+ writeBackgroundState(backgroundStatePath(account, deps.env), {
3532
+ v: 1,
3533
+ pid,
3534
+ startedAt: Date.now(),
3535
+ logPath,
3536
+ args: [...upArgs, "--background"],
3537
+ instances: start.instances,
3538
+ watching: start.watching
3539
+ });
3540
+ const outcome = await waitForBackgroundHolder(pid, () => deps.inspect(start.lockFile), deps.alive, { timeoutMs: deps.timeoutMs });
3541
+ if (outcome === "live") {
3542
+ deps.step(`Running in the background (pid ${pid}) \u2014 this terminal can be closed`, "ok");
3543
+ deps.log("");
3544
+ deps.log(` Serving ${start.instances.length} agent${start.instances.length === 1 ? "" : "s"}${start.watching ? ", and attaching newly eligible ones" : ""}.`);
3545
+ deps.log(" It keeps running until `pags down`, a reboot, or logging out \u2014 it does not start by itself.");
3546
+ deps.log("");
3547
+ deps.log(" pags status \u2014 is it running, and what is it serving");
3548
+ deps.log(` pags logs \u2014 its output (${logPath})`);
3549
+ deps.log(" pags down \u2014 stop it");
3550
+ deps.log("");
3551
+ return 0;
3552
+ }
3553
+ deps.step(outcome === "exited" ? "The background runner stopped before it was ready" : "The background runner did not confirm it was ready", "fail");
3554
+ for (const line of tailLines(logPath, 10) ?? []) deps.log(` ${line}`);
3555
+ deps.log("");
3556
+ deps.log(outcome === "exited" ? " Fix what the log says, then `pags up --background` again." : ` It may still be starting (pid ${pid}): check \`pags status\`, or stop it with \`pags down\`.`);
3557
+ deps.log("");
3558
+ return 1;
3559
+ }
3560
+
3362
3561
  // src/commands/up.ts
3363
3562
  var API_BASE3 = "https://api.proagentstore.online";
3364
3563
  var CLI_VERSION2 = createRequire2(import.meta.url)("../package.json").version;
3365
- var upCommand = new Command9("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)").action(async (opts) => {
3564
+ 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) => {
3366
3565
  const session = requireSession();
3367
3566
  const state = {
3368
3567
  user: session.user.login,
@@ -3439,6 +3638,7 @@ var upCommand = new Command9("up").description("Start the browser runner for all
3439
3638
  state.activeInstance = instances.length === 1 ? instances[0].name || instances[0].slug || instances[0].id.slice(0, 8) : `${instances.length} agents`;
3440
3639
  printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
3441
3640
  const lockFile = lockPath(session.user.login);
3641
+ if (opts.background && process.env[BACKGROUND_RESTART_ENV] === "1") await waitUntilTakeable(() => inspectHolder(lockFile));
3442
3642
  const held = await inspectHolder(lockFile);
3443
3643
  if (held.lock && !isTakeable(held.verdict)) {
3444
3644
  if (!opts.replace) {
@@ -3464,6 +3664,10 @@ var upCommand = new Command9("up").description("Start the browser runner for all
3464
3664
  if (opts.headless) args.push("--headless");
3465
3665
  if (opts.force) args.push("--force");
3466
3666
  if (!opts.instance) args.push("--watch-instances");
3667
+ if (opts.background) {
3668
+ const code = await startInBackground({ session, opts, runnerArgs: args, instances: state.instances, lockFile, watching: !opts.instance });
3669
+ process.exit(code);
3670
+ }
3467
3671
  const child = spawn5(process.execPath, args, {
3468
3672
  stdio: ["ignore", "pipe", "pipe"],
3469
3673
  // The child is the lock HOLDER (#896): it owns the relay sockets, so it is what a
@@ -3600,7 +3804,7 @@ var upCommand = new Command9("up").description("Start the browser runner for all
3600
3804
  }
3601
3805
  }
3602
3806
  });
3603
- var downCommand = new Command9("down").description("Stop the browser runner and disconnect").option("--now", "Stop it even though work is running on it (that work is lost)").action(async (opts) => {
3807
+ var downCommand = new Command10("down").description("Stop the browser runner and disconnect").option("--now", "Stop it even though work is running on it (that work is lost)").action(async (opts) => {
3604
3808
  clearScreen();
3605
3809
  printLogo(CLI_VERSION2);
3606
3810
  const session = requireSession();
@@ -3625,7 +3829,7 @@ var downCommand = new Command9("down").description("Stop the browser runner and
3625
3829
  // src/index.ts
3626
3830
  var require2 = createRequire3(import.meta.url);
3627
3831
  var { version } = require2("../package.json");
3628
- var program = new Command10();
3832
+ var program = new Command11();
3629
3833
  program.name("pags").description(
3630
3834
  "ProAgentStore CLI \u2014 create and publish server-powered AI agents"
3631
3835
  ).version(version);
@@ -3634,6 +3838,8 @@ program.addCommand(logoutCommand);
3634
3838
  program.addCommand(whoamiCommand);
3635
3839
  program.addCommand(upCommand);
3636
3840
  program.addCommand(downCommand);
3841
+ program.addCommand(statusCommand);
3842
+ program.addCommand(logsCommand);
3637
3843
  program.addCommand(initCommand);
3638
3844
  program.addCommand(checkCommand);
3639
3845
  program.addCommand(publishCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.124",
3
+ "version": "0.4.126",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",