@proagentstore/cli 0.4.87 → 0.4.88
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/browser-runner/coding/runtime.js +13 -0
- package/dist/browser-runner/index.js +6 -0
- package/dist/browser-runner/local-apply/runtime.js +4 -0
- package/dist/browser-runner/local-artifact/runtime.js +4 -0
- package/dist/browser-runner/local-browser/runtime.js +9 -0
- package/dist/browser-runner/runner.js +25 -0
- package/dist/browser-runner/server.js +34 -0
- package/dist/index.js +374 -39
- package/package.json +1 -1
|
@@ -12,6 +12,19 @@ const MAX_PANE = 64 * 1024;
|
|
|
12
12
|
export class CodingRuntime {
|
|
13
13
|
reposBaseDir;
|
|
14
14
|
sessions = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Coding sessions that are running here (#896).
|
|
17
|
+
*
|
|
18
|
+
* A Claude session survives a restart — it is re-spawned with `--resume <session_id>` and only
|
|
19
|
+
* the turn in flight is cut — but a Codex, Grok or raw-engine session restarts cold and its turn
|
|
20
|
+
* is gone. Both are named, because the owner is the one deciding whether to take the machine.
|
|
21
|
+
*/
|
|
22
|
+
liveWork() {
|
|
23
|
+
return [...this.sessions.values()].map((s) => {
|
|
24
|
+
const engine = s.clientType ?? "";
|
|
25
|
+
return engine === "claude" ? "a Claude coding session (resumes after a restart)" : `a ${engine || "coding"} turn (lost on a restart)`;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
15
28
|
/** Background cold-start clones, one per folder (#858). */
|
|
16
29
|
cloneJobs = new CloneJobs();
|
|
17
30
|
/**
|
|
@@ -38,6 +38,12 @@ function configFromArgs() {
|
|
|
38
38
|
token: arg("--token", process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`,
|
|
39
39
|
instanceId: arg("--instance-id", process.env.PAGS_INSTANCE_ID),
|
|
40
40
|
headless: flag("--headless") || process.env.PAGS_RUNNER_HEADLESS === "1",
|
|
41
|
+
// #896: the single-instance lock's identity and shutdown credential, passed by the parent
|
|
42
|
+
// (`runner connect`) that took the lock. Absent when nothing holds a lock — a bare
|
|
43
|
+
// `pags runner start`, or a CLI older than the lock — and then `/control/shutdown` refuses.
|
|
44
|
+
rsid: process.env.PAGS_RUNNER_RSID || undefined,
|
|
45
|
+
launch: process.env.PAGS_RUNNER_LAUNCH || undefined,
|
|
46
|
+
controlNonce: process.env.PAGS_RUNNER_CONTROL_NONCE || undefined,
|
|
41
47
|
};
|
|
42
48
|
}
|
|
43
49
|
if (flag("--help") || flag("-h")) {
|
|
@@ -167,6 +167,10 @@ function bridgeUnusedHelp(engine, d) {
|
|
|
167
167
|
export class LocalApplyRuntime {
|
|
168
168
|
deps;
|
|
169
169
|
runs = new Map();
|
|
170
|
+
/** Application fills that a restart would lose (#896) — their state is in this process. */
|
|
171
|
+
liveWork() {
|
|
172
|
+
return [...this.runs.values()].filter((r) => r.state !== "ended").map((r) => `an application fill (${r.envelope.applicationId.slice(0, 8)})`);
|
|
173
|
+
}
|
|
170
174
|
root;
|
|
171
175
|
now;
|
|
172
176
|
spawn;
|
|
@@ -111,6 +111,10 @@ class Pause extends Error {
|
|
|
111
111
|
export class LocalArtifactRuntime {
|
|
112
112
|
deps;
|
|
113
113
|
runs = new Map();
|
|
114
|
+
/** Tailoring runs that a restart would lose (#896) — their state is in this process. */
|
|
115
|
+
liveWork() {
|
|
116
|
+
return [...this.runs.values()].filter((r) => r.state !== "ended").map((r) => `a tailoring run (${r.envelope.runId.slice(0, 8)})`);
|
|
117
|
+
}
|
|
114
118
|
root;
|
|
115
119
|
now;
|
|
116
120
|
spawn;
|
|
@@ -138,6 +138,15 @@ export function resolveWorkspacePath(path, home) {
|
|
|
138
138
|
export class LocalBrowserRuntime {
|
|
139
139
|
deps;
|
|
140
140
|
runs = new Map();
|
|
141
|
+
/**
|
|
142
|
+
* Research runs that would be LOST if this process stopped (#896).
|
|
143
|
+
*
|
|
144
|
+
* Their state is in this process's memory: a restart does not resume them, and the cloud only
|
|
145
|
+
* gives up on them minutes later. So `--replace` asks before taking the machine.
|
|
146
|
+
*/
|
|
147
|
+
liveWork() {
|
|
148
|
+
return [...this.runs.values()].filter((r) => r.state !== "ended").map((r) => `a browser research run (${r.envelope.instanceId.slice(0, 8)})`);
|
|
149
|
+
}
|
|
141
150
|
root;
|
|
142
151
|
now;
|
|
143
152
|
spawn;
|
|
@@ -112,6 +112,20 @@ export class LocalRunner {
|
|
|
112
112
|
if (expired > 0)
|
|
113
113
|
console.log(`[runner] expired ${expired} orphaned in-flight task(s) from a previous session`);
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* What this process is doing that a restart would destroy (#896).
|
|
117
|
+
*
|
|
118
|
+
* `close()` stops every local runtime it owns, so "restart the runner" is not a free action: a
|
|
119
|
+
* Claude coding session resumes, but a Codex or Grok turn, a browser research run, a tailoring
|
|
120
|
+
* run and an application fill are each lost, and the cloud only reports them `runner_lost`
|
|
121
|
+
* minutes later. `pags up --replace` reads this and refuses while anything is live unless the
|
|
122
|
+
* owner passes `--now`, which is the whole reason it is reported rather than counted silently.
|
|
123
|
+
*/
|
|
124
|
+
liveWork() {
|
|
125
|
+
const coding = this.coding.liveWork();
|
|
126
|
+
const local = [...this.localBrowser.liveWork(), ...this.localArtifact.liveWork(), ...this.localApply.liveWork()];
|
|
127
|
+
return { codingTurns: coding.length, localRuns: local.length, detail: [...coding, ...local].slice(0, 10) };
|
|
128
|
+
}
|
|
115
129
|
capabilities() {
|
|
116
130
|
return {
|
|
117
131
|
runtime: "pags-browser-runtime",
|
|
@@ -255,6 +269,17 @@ export class LocalRunner {
|
|
|
255
269
|
* holding a live browser until someone `kill -9`ed it. That kill is exactly
|
|
256
270
|
* what orphans the browser, so an unswallowed error here MADE the leak.
|
|
257
271
|
*/
|
|
272
|
+
/**
|
|
273
|
+
* Stop because somebody with the lock asked (#896) — the same teardown a signal runs.
|
|
274
|
+
*
|
|
275
|
+
* Deliberately not a second shutdown path: `index.ts`'s handler is idempotent and time-boxed
|
|
276
|
+
* (#274), so raising SIGTERM on ourselves reuses it rather than re-implementing it, which is
|
|
277
|
+
* what keeps "asked to stop" and "signalled to stop" from drifting apart.
|
|
278
|
+
*/
|
|
279
|
+
async requestShutdown(reason) {
|
|
280
|
+
console.log(`[runner] stopping on request (${reason.slice(0, 60)})`);
|
|
281
|
+
process.kill(process.pid, "SIGTERM");
|
|
282
|
+
}
|
|
258
283
|
async close() {
|
|
259
284
|
this.localBrowser.closeAll();
|
|
260
285
|
this.localArtifact.closeAll();
|
|
@@ -57,6 +57,12 @@ async function route(runner, req, res) {
|
|
|
57
57
|
const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
|
|
58
58
|
const path = url.pathname.replace(/\/$/, "") || "/";
|
|
59
59
|
if ((req.method === "GET" || req.method === "POST") && path === "/health") {
|
|
60
|
+
// #896: a health check that cannot say WHO answered is not enough to decide anything with.
|
|
61
|
+
// A contender for the single-instance lock has a pid and a port from the lock file; what it
|
|
62
|
+
// needs is proof that the process on that port is the one the lock names (`rsid`), and what
|
|
63
|
+
// `--replace` needs on top of that is what would be destroyed by stopping it (`work`).
|
|
64
|
+
// Without the id, a recycled port answering "ok" reads as a live holder; without the work
|
|
65
|
+
// counts, replacing silently ends a Codex turn, a research run or an application fill.
|
|
60
66
|
return json(res, 200, {
|
|
61
67
|
ok: true,
|
|
62
68
|
service: "proagentstore-browser-runtime",
|
|
@@ -64,8 +70,36 @@ async function route(runner, req, res) {
|
|
|
64
70
|
controlPlane: "pags",
|
|
65
71
|
runtimePlane: "pags",
|
|
66
72
|
instanceId: runner.config.instanceId,
|
|
73
|
+
rsid: runner.config.rsid ?? null,
|
|
74
|
+
pid: process.pid,
|
|
75
|
+
launch: runner.config.launch ?? null,
|
|
76
|
+
work: runner.liveWork(),
|
|
67
77
|
});
|
|
68
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* `POST /control/shutdown` — stop gracefully, when asked by somebody holding the lock (#896).
|
|
81
|
+
*
|
|
82
|
+
* This is what makes `pags up --replace` and `pags down` an ASK rather than a kill. The caller
|
|
83
|
+
* proves it is entitled by quoting the nonce from the mode-600 lock file, which only a process
|
|
84
|
+
* able to read the owner's own lock can do; `pkill`, which this replaced, proved nothing and
|
|
85
|
+
* stopped anything matching a name — including another account's runner and a service's.
|
|
86
|
+
*
|
|
87
|
+
* The drain itself is `close()`, the same teardown a signal runs, so there is one shutdown path
|
|
88
|
+
* and not a second one that forgets something.
|
|
89
|
+
*/
|
|
90
|
+
if (req.method === "POST" && path === "/control/shutdown") {
|
|
91
|
+
const expected = runner.config.controlNonce ?? "";
|
|
92
|
+
const body = (await readJson(req).catch(() => null));
|
|
93
|
+
const given = typeof body?.nonce === "string" ? body.nonce : "";
|
|
94
|
+
if (!expected || !given || given !== expected)
|
|
95
|
+
return json(res, 403, { error: "This runner only stops for a request carrying its own lock's nonce." });
|
|
96
|
+
const work = runner.liveWork();
|
|
97
|
+
json(res, 202, { stopping: true, pid: process.pid, work });
|
|
98
|
+
// Answer first, then stop: the caller is waiting for the pid to go, and a reply written
|
|
99
|
+
// after teardown never arrives.
|
|
100
|
+
setTimeout(() => void runner.requestShutdown(typeof body?.reason === "string" ? body.reason : "requested"), 10);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
69
103
|
if ((req.method === "GET" || req.method === "POST") && path === "/capabilities") {
|
|
70
104
|
return json(res, 200, runner.capabilities());
|
|
71
105
|
}
|
package/dist/index.js
CHANGED
|
@@ -1098,7 +1098,7 @@ var publishCommand = new Command6("publish").description("Publish an agent to Pr
|
|
|
1098
1098
|
|
|
1099
1099
|
// src/commands/runner/command.ts
|
|
1100
1100
|
import { spawn as spawn4 } from "child_process";
|
|
1101
|
-
import { randomUUID as
|
|
1101
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1102
1102
|
import { Command as Command7 } from "commander";
|
|
1103
1103
|
|
|
1104
1104
|
// src/commands/runner/process.ts
|
|
@@ -1597,7 +1597,16 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1597
1597
|
const [relayRttMs, sessions] = await Promise.all([first ? first.probeRtt() : Promise.resolve(null), readSessionResources(localUrl, runnerToken)]);
|
|
1598
1598
|
const resources = sampleResources(void 0, void 0, {
|
|
1599
1599
|
disk: sampleDisk() ?? void 0,
|
|
1600
|
-
runner: {
|
|
1600
|
+
runner: {
|
|
1601
|
+
startedAt: processStartedAt,
|
|
1602
|
+
uptimeSec: Math.round(process.uptime()),
|
|
1603
|
+
starts24h,
|
|
1604
|
+
relayReconnects,
|
|
1605
|
+
// #896: who is heartbeating. Set by `runner connect` from the lock it took.
|
|
1606
|
+
...process.env.PAGS_RUNNER_RSID ? { rsid: process.env.PAGS_RUNNER_RSID } : {},
|
|
1607
|
+
pid: process.pid,
|
|
1608
|
+
...process.env.PAGS_RUNNER_LAUNCH ? { launch: process.env.PAGS_RUNNER_LAUNCH } : {}
|
|
1609
|
+
},
|
|
1601
1610
|
relayRttMs,
|
|
1602
1611
|
sessions: sessions ?? void 0
|
|
1603
1612
|
});
|
|
@@ -1853,7 +1862,204 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
|
|
|
1853
1862
|
};
|
|
1854
1863
|
}
|
|
1855
1864
|
|
|
1865
|
+
// src/runner-lock.ts
|
|
1866
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
1867
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1868
|
+
import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync6, renameSync, unlinkSync as unlinkSync2, writeSync } from "fs";
|
|
1869
|
+
import { homedir as homedir3 } from "os";
|
|
1870
|
+
import { join as join7 } from "path";
|
|
1871
|
+
function configDir(env = process.env) {
|
|
1872
|
+
return env.PAGS_CONFIG_DIR?.trim() || join7(homedir3(), ".config", "proagentstore");
|
|
1873
|
+
}
|
|
1874
|
+
var accountKey = (account) => createHash("sha256").update(account.trim().toLowerCase()).digest("hex").slice(0, 12);
|
|
1875
|
+
var lockPath = (account, env = process.env) => join7(configDir(env), `up-${accountKey(account)}.lock`);
|
|
1876
|
+
function classifyHolder(facts) {
|
|
1877
|
+
const lock = facts.lock;
|
|
1878
|
+
if (!lock) return "unknown";
|
|
1879
|
+
if (lock.v !== 1) return "unknown";
|
|
1880
|
+
if (!lock.pid || !Number.isInteger(lock.pid)) return "unknown";
|
|
1881
|
+
if (!facts.pidAlive) return "dead";
|
|
1882
|
+
if (lock.pidStartedAt && facts.pidStartedAt && lock.pidStartedAt !== facts.pidStartedAt) return "dead";
|
|
1883
|
+
if (facts.healthRsid && facts.healthRsid === lock.rsid) return "live";
|
|
1884
|
+
return "unresponsive";
|
|
1885
|
+
}
|
|
1886
|
+
var isTakeable = (verdict) => verdict === "dead";
|
|
1887
|
+
function holderWhere(lock) {
|
|
1888
|
+
if (lock.launch === "tmux") return `tmux session ${lock.launchDetail || "?"}`;
|
|
1889
|
+
if (lock.launch === "service") return "a background service (launchd/systemd)";
|
|
1890
|
+
if (lock.launch === "headless") return "headless";
|
|
1891
|
+
return lock.launchDetail ? `terminal ${lock.launchDetail}` : "a terminal";
|
|
1892
|
+
}
|
|
1893
|
+
function holderMessage(lock, verdict) {
|
|
1894
|
+
const since = new Date(lock.startedAt).toLocaleString();
|
|
1895
|
+
const lines = [
|
|
1896
|
+
`A \`pags up\` is already running for this account on this machine.`,
|
|
1897
|
+
` pid ${lock.pid}, started ${since}, CLI ${lock.version || "?"}, in ${holderWhere(lock)}.`
|
|
1898
|
+
];
|
|
1899
|
+
if (verdict === "unresponsive") {
|
|
1900
|
+
lines.push(` It is NOT answering its health check \u2014 it may be asleep, stopped, or the machine may be overloaded.`);
|
|
1901
|
+
}
|
|
1902
|
+
if (verdict === "unknown") {
|
|
1903
|
+
lines.push(` Its lock file could not be read (a newer CLI may have written it), so it is assumed to be running.`);
|
|
1904
|
+
}
|
|
1905
|
+
lines.push("", " What you can do:");
|
|
1906
|
+
if (lock.launch === "tmux" && lock.launchDetail) lines.push(` tmux attach -t ${lock.launchDetail} \u2014 go to the one that is running`);
|
|
1907
|
+
lines.push(
|
|
1908
|
+
` pags status \u2014 what it is doing, and anything else running here`,
|
|
1909
|
+
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`
|
|
1910
|
+
);
|
|
1911
|
+
return lines;
|
|
1912
|
+
}
|
|
1913
|
+
function detectLaunch(env = process.env, isTty = Boolean(process.stdout.isTTY)) {
|
|
1914
|
+
if (env.PAGS_SERVICE === "1") return { launch: "service", detail: env.PAGS_SERVICE_NAME?.trim() || "" };
|
|
1915
|
+
if (env.TMUX) return { launch: "tmux", detail: env.TMUX_PANE?.trim() || env.TMUX.split(",")[0]?.split("/").pop() || "" };
|
|
1916
|
+
if (!isTty) return { launch: "headless", detail: "" };
|
|
1917
|
+
return { launch: "tty", detail: env.TERM_SESSION_ID?.trim() || env.WINDOWID?.trim() || "" };
|
|
1918
|
+
}
|
|
1919
|
+
function pidAlive(pid) {
|
|
1920
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) return false;
|
|
1921
|
+
try {
|
|
1922
|
+
process.kill(pid, 0);
|
|
1923
|
+
return true;
|
|
1924
|
+
} catch (e) {
|
|
1925
|
+
return e.code === "EPERM";
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function pidStartedAt(pid) {
|
|
1929
|
+
if (!pidAlive(pid)) return "";
|
|
1930
|
+
try {
|
|
1931
|
+
if (process.platform === "win32") {
|
|
1932
|
+
const out = execFileSync2("wmic", ["process", "where", `ProcessId=${pid}`, "get", "CreationDate", "/value"], { encoding: "utf-8", timeout: 4e3, stdio: ["ignore", "pipe", "ignore"] });
|
|
1933
|
+
return (out.match(/CreationDate=(\S+)/)?.[1] ?? "").trim();
|
|
1934
|
+
}
|
|
1935
|
+
return execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf-8", timeout: 4e3, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1936
|
+
} catch {
|
|
1937
|
+
return "";
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
async function healthRsid(port, token = "", timeoutMs = 5e3, fetchImpl = fetch) {
|
|
1941
|
+
if (!port) return null;
|
|
1942
|
+
try {
|
|
1943
|
+
const res = await fetchImpl(`http://127.0.0.1:${port}/health`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, signal: AbortSignal.timeout(timeoutMs) });
|
|
1944
|
+
if (!res.ok) return null;
|
|
1945
|
+
const body = await res.json();
|
|
1946
|
+
return typeof body.rsid === "string" && body.rsid ? body.rsid : null;
|
|
1947
|
+
} catch {
|
|
1948
|
+
return null;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function readLock(path) {
|
|
1952
|
+
try {
|
|
1953
|
+
const parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
1954
|
+
return parsed && typeof parsed === "object" && typeof parsed.rsid === "string" ? parsed : null;
|
|
1955
|
+
} catch {
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
async function inspectHolder(path, opts = {}) {
|
|
1960
|
+
const lock = readLock(path);
|
|
1961
|
+
if (!lock) return { lock: null, verdict: "unknown" };
|
|
1962
|
+
const alive = pidAlive(lock.pid);
|
|
1963
|
+
const facts = {
|
|
1964
|
+
lock,
|
|
1965
|
+
pidAlive: alive,
|
|
1966
|
+
pidStartedAt: alive ? pidStartedAt(lock.pid) : "",
|
|
1967
|
+
healthRsid: alive ? await healthRsid(lock.port, lock.runnerToken ?? "", opts.healthTimeoutMs ?? 5e3, opts.fetchImpl ?? fetch) : null
|
|
1968
|
+
};
|
|
1969
|
+
return { lock, verdict: classifyHolder(facts) };
|
|
1970
|
+
}
|
|
1971
|
+
async function acquireLock(input) {
|
|
1972
|
+
const env = input.env ?? process.env;
|
|
1973
|
+
const path = lockPath(input.account, env);
|
|
1974
|
+
const rsid = input.rsid ?? randomUUID2();
|
|
1975
|
+
const handoff = input.handoffFrom?.trim() || env.PAGS_LOCK_HANDOFF?.trim() || "";
|
|
1976
|
+
mkdirSync5(configDir(env), { recursive: true });
|
|
1977
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1978
|
+
const written = tryWrite(path, buildLock(input, rsid, env));
|
|
1979
|
+
if (written) return { ok: true, lock: written, path, tookOver: attempt === 0 ? "none" : "stale" };
|
|
1980
|
+
const { lock: lock2, verdict: verdict2 } = await inspectHolder(path);
|
|
1981
|
+
if (handoff && lock2?.rsid === handoff) {
|
|
1982
|
+
renameAside(path, lock2.pid);
|
|
1983
|
+
const taken = tryWrite(path, buildLock(input, rsid, env));
|
|
1984
|
+
if (taken) return { ok: true, lock: taken, path, tookOver: "handoff" };
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
if (!isTakeable(verdict2)) return { ok: false, lock: lock2, path, verdict: verdict2 };
|
|
1988
|
+
if (!takeAsideIfStill(path, lock2?.rsid ?? "", lock2?.pid ?? 0)) continue;
|
|
1989
|
+
}
|
|
1990
|
+
const { lock, verdict } = await inspectHolder(path);
|
|
1991
|
+
return { ok: false, lock, path, verdict };
|
|
1992
|
+
}
|
|
1993
|
+
function buildLock(input, rsid, env) {
|
|
1994
|
+
const { launch, detail } = detectLaunch(env);
|
|
1995
|
+
const now = Date.now();
|
|
1996
|
+
return {
|
|
1997
|
+
v: 1,
|
|
1998
|
+
rsid,
|
|
1999
|
+
pid: process.pid,
|
|
2000
|
+
pidStartedAt: pidStartedAt(process.pid),
|
|
2001
|
+
startedAt: now,
|
|
2002
|
+
port: input.port,
|
|
2003
|
+
node: input.node,
|
|
2004
|
+
machineId: input.machineId,
|
|
2005
|
+
account: input.account,
|
|
2006
|
+
version: input.version,
|
|
2007
|
+
launch,
|
|
2008
|
+
launchDetail: detail,
|
|
2009
|
+
childPids: [],
|
|
2010
|
+
nonce: randomUUID2(),
|
|
2011
|
+
runnerToken: input.runnerToken ?? "",
|
|
2012
|
+
heartbeatAt: now
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
function tryWrite(path, lock) {
|
|
2016
|
+
let fd = null;
|
|
2017
|
+
try {
|
|
2018
|
+
fd = openSync(path, "wx", 384);
|
|
2019
|
+
writeSync(fd, JSON.stringify(lock, null, 2));
|
|
2020
|
+
return lock;
|
|
2021
|
+
} catch (e) {
|
|
2022
|
+
if (e.code === "EEXIST") return null;
|
|
2023
|
+
throw e;
|
|
2024
|
+
} finally {
|
|
2025
|
+
if (fd !== null) closeSync(fd);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
function renameAside(path, pid) {
|
|
2029
|
+
const aside = `${path}.stale-${pid || "unknown"}`;
|
|
2030
|
+
try {
|
|
2031
|
+
renameSync(path, aside);
|
|
2032
|
+
return aside;
|
|
2033
|
+
} catch {
|
|
2034
|
+
return null;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
function takeAsideIfStill(path, deadRsid, pid) {
|
|
2038
|
+
const aside = renameAside(path, pid);
|
|
2039
|
+
if (!aside) return false;
|
|
2040
|
+
const moved = readLock(aside);
|
|
2041
|
+
if (moved && deadRsid && moved.rsid !== deadRsid) {
|
|
2042
|
+
try {
|
|
2043
|
+
renameSync(aside, path);
|
|
2044
|
+
} catch {
|
|
2045
|
+
}
|
|
2046
|
+
return false;
|
|
2047
|
+
}
|
|
2048
|
+
return true;
|
|
2049
|
+
}
|
|
2050
|
+
function releaseLock(path, rsid) {
|
|
2051
|
+
const current = readLock(path);
|
|
2052
|
+
if (!current || current.rsid !== rsid) return false;
|
|
2053
|
+
try {
|
|
2054
|
+
unlinkSync2(path);
|
|
2055
|
+
return true;
|
|
2056
|
+
} catch {
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1856
2061
|
// src/commands/runner/command.ts
|
|
2062
|
+
import { hostname as hostname4 } from "os";
|
|
1857
2063
|
function collectCapability(value, previous = []) {
|
|
1858
2064
|
return [...previous, value];
|
|
1859
2065
|
}
|
|
@@ -1865,9 +2071,23 @@ function createRunnerCommand() {
|
|
|
1865
2071
|
await startRunnerForeground(opts);
|
|
1866
2072
|
});
|
|
1867
2073
|
command.command("connect <instanceIds...>").description("Start ONE local runtime, connect via WebSocket relay, and register it for every given PAGS instance").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "49171").option("--data-dir <path>", "Runner data directory").option("--token <token>", "Runner bearer token. Defaults to PAGS_RUNNER_TOKEN or a generated token").option("--headless", "Run Playwright headless").option("--api-base <url>", "PAGS API base URL").option("--pags-token <token>", "PAGS session token. Defaults to PAGS_TOKEN").option("--runner-version <version>", "Runner version").option("--force", "Take over from another connected machine").option("--watch-instances", "Attach newly eligible agents while running, without a restart").action(async (instanceIds, opts) => {
|
|
1868
|
-
const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${
|
|
2074
|
+
const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID3()}`;
|
|
1869
2075
|
const host = clean(opts.host) || "127.0.0.1";
|
|
1870
2076
|
const port = clean(opts.port) || String(await findFreePort2(49171));
|
|
2077
|
+
const lockAccount = clean(process.env.PAGS_LOCK_ACCOUNT) || "";
|
|
2078
|
+
let lockHeld = null;
|
|
2079
|
+
if (lockAccount) {
|
|
2080
|
+
const taken = await acquireLock({ account: lockAccount, node: hostname4(), machineId: loadMachineIdentity().id || null, version: CLI_VERSION, port: Number(port), runnerToken });
|
|
2081
|
+
if (!taken.ok) {
|
|
2082
|
+
for (const line of taken.lock ? holderMessage(taken.lock, taken.verdict) : ["Another `pags up` holds this machine for this account."]) writeError(line);
|
|
2083
|
+
process.exit(1);
|
|
2084
|
+
}
|
|
2085
|
+
lockHeld = { path: taken.path, rsid: taken.lock.rsid, nonce: taken.lock.nonce, launch: taken.lock.launch };
|
|
2086
|
+
writeLine(`STATUS runner-session=${taken.lock.rsid}`);
|
|
2087
|
+
}
|
|
2088
|
+
const releaseHeldLock = () => {
|
|
2089
|
+
if (lockHeld) releaseLock(lockHeld.path, lockHeld.rsid);
|
|
2090
|
+
};
|
|
1871
2091
|
const localUrl = `http://${host}:${port}`;
|
|
1872
2092
|
const primary = instanceIds[0];
|
|
1873
2093
|
const runnerOpts = { ...opts, host, port, token: runnerToken };
|
|
@@ -1875,7 +2095,14 @@ function createRunnerCommand() {
|
|
|
1875
2095
|
const runner = spawn4(spec.command, spec.args, {
|
|
1876
2096
|
cwd: spec.cwd,
|
|
1877
2097
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1878
|
-
shell: process.platform === "win32"
|
|
2098
|
+
shell: process.platform === "win32",
|
|
2099
|
+
// #896: the runtime answers `/health` with the lock's identity and stops for a
|
|
2100
|
+
// request quoting its nonce — so a contender can prove who is on that port, and a
|
|
2101
|
+
// replace is an authorisation rather than a signal aimed at a name pattern.
|
|
2102
|
+
env: {
|
|
2103
|
+
...process.env,
|
|
2104
|
+
...lockHeld ? { PAGS_RUNNER_RSID: lockHeld.rsid, PAGS_RUNNER_CONTROL_NONCE: lockHeld.nonce, PAGS_RUNNER_LAUNCH: lockHeld.launch } : {}
|
|
2105
|
+
}
|
|
1879
2106
|
});
|
|
1880
2107
|
runner.stdout?.on("data", (data) => process.stdout.write(data));
|
|
1881
2108
|
runner.stderr?.on("data", (data) => process.stderr.write(data));
|
|
@@ -1887,6 +2114,7 @@ function createRunnerCommand() {
|
|
|
1887
2114
|
});
|
|
1888
2115
|
const shutdown = () => {
|
|
1889
2116
|
shuttingDown = true;
|
|
2117
|
+
releaseHeldLock();
|
|
1890
2118
|
if (!runner.killed) runner.kill("SIGTERM");
|
|
1891
2119
|
};
|
|
1892
2120
|
process.once("exit", shutdown);
|
|
@@ -2023,14 +2251,14 @@ function createRunnerCommand() {
|
|
|
2023
2251
|
var runnerCommand = createRunnerCommand();
|
|
2024
2252
|
|
|
2025
2253
|
// src/commands/up.ts
|
|
2026
|
-
import { execFileSync as
|
|
2254
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2027
2255
|
import { createRequire as createRequire2 } from "module";
|
|
2028
2256
|
import { Command as Command8 } from "commander";
|
|
2029
|
-
import { hostname as
|
|
2257
|
+
import { hostname as hostname6 } from "os";
|
|
2030
2258
|
|
|
2031
2259
|
// src/tui.ts
|
|
2032
2260
|
import chalk from "chalk";
|
|
2033
|
-
import { hostname as
|
|
2261
|
+
import { hostname as hostname5 } from "os";
|
|
2034
2262
|
import readline from "readline";
|
|
2035
2263
|
var ACCENT = "#7c3aed";
|
|
2036
2264
|
var c = chalk.hex(ACCENT);
|
|
@@ -2083,7 +2311,7 @@ function printStatus(state) {
|
|
|
2083
2311
|
clearScreen();
|
|
2084
2312
|
printLogo(state.version);
|
|
2085
2313
|
const connected = state.runner === "online" && state.tunnel === "online" && state.registration === "registered";
|
|
2086
|
-
console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(
|
|
2314
|
+
console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(hostname5()));
|
|
2087
2315
|
console.log("");
|
|
2088
2316
|
const row = (kind, s, override) => {
|
|
2089
2317
|
const { icon, label, note } = describe(kind, s);
|
|
@@ -2145,28 +2373,94 @@ async function waitForKey(keys, onInterrupt) {
|
|
|
2145
2373
|
});
|
|
2146
2374
|
}
|
|
2147
2375
|
|
|
2376
|
+
// src/runner-replace.ts
|
|
2377
|
+
var REPLACE_EXIT_TIMEOUT_MS = 3e4;
|
|
2378
|
+
function describeWork(work) {
|
|
2379
|
+
if (work.detail.length) return work.detail.join(", ");
|
|
2380
|
+
const parts = [];
|
|
2381
|
+
if (work.codingTurns) parts.push(`${work.codingTurns} coding turn${work.codingTurns === 1 ? "" : "s"}`);
|
|
2382
|
+
if (work.localRuns) parts.push(`${work.localRuns} local run${work.localRuns === 1 ? "" : "s"}`);
|
|
2383
|
+
return parts.join(", ") || "work";
|
|
2384
|
+
}
|
|
2385
|
+
var hasWork = (work) => !!work && work.codingTurns + work.localRuns > 0;
|
|
2386
|
+
function replaceVerdict(lock, work, opts) {
|
|
2387
|
+
if (lock.launch === "service") {
|
|
2388
|
+
return {
|
|
2389
|
+
allowed: false,
|
|
2390
|
+
lines: [
|
|
2391
|
+
`That runner is managed by a background service${lock.launchDetail ? ` (${lock.launchDetail})` : ""}, so replacing it would only make the service start it again.`,
|
|
2392
|
+
"Stop its launchd/systemd unit instead, then run `pags up` here."
|
|
2393
|
+
]
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
if (hasWork(work) && !opts.now) {
|
|
2397
|
+
return {
|
|
2398
|
+
allowed: false,
|
|
2399
|
+
lines: [
|
|
2400
|
+
`That runner is busy: ${describeWork(work)}.`,
|
|
2401
|
+
"Stopping it now would lose that work \u2014 a Codex or Grok turn, a research run, a tailoring run or an application fill do not survive a restart.",
|
|
2402
|
+
"Wait for it to finish, or run `pags up --replace --now` to stop it anyway."
|
|
2403
|
+
]
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
return { allowed: true, lines: [] };
|
|
2407
|
+
}
|
|
2408
|
+
async function holderWork(lock, deps = {}) {
|
|
2409
|
+
if (!lock.port) return null;
|
|
2410
|
+
try {
|
|
2411
|
+
const res = await (deps.fetchImpl ?? fetch)(`http://127.0.0.1:${lock.port}/health`, { headers: lock.runnerToken ? { Authorization: `Bearer ${lock.runnerToken}` } : {}, signal: AbortSignal.timeout(5e3) });
|
|
2412
|
+
if (!res.ok) return null;
|
|
2413
|
+
const body = await res.json();
|
|
2414
|
+
const w2 = body.work;
|
|
2415
|
+
if (!w2) return null;
|
|
2416
|
+
const n = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
|
|
2417
|
+
return { codingTurns: n(w2.codingTurns), localRuns: n(w2.localRuns), detail: (Array.isArray(w2.detail) ? w2.detail : []).filter((x) => typeof x === "string").slice(0, 10) };
|
|
2418
|
+
} catch {
|
|
2419
|
+
return null;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
async function replaceHolder(lock, opts = {}, deps = {}) {
|
|
2423
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
2424
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2425
|
+
const alive = deps.alive ?? pidAlive;
|
|
2426
|
+
const now = deps.now ?? Date.now;
|
|
2427
|
+
const work = await holderWork(lock, deps);
|
|
2428
|
+
const verdict = replaceVerdict(lock, work, opts);
|
|
2429
|
+
if (!verdict.allowed) return { ok: false, lines: verdict.lines };
|
|
2430
|
+
if (!lock.port) {
|
|
2431
|
+
return { ok: false, lines: [`The runner holding this machine (pid ${lock.pid}) does not say which port it is on, so it cannot be asked to stop.`, "Stop it where it is running, then try again."] };
|
|
2432
|
+
}
|
|
2433
|
+
try {
|
|
2434
|
+
const res = await fetchImpl(`http://127.0.0.1:${lock.port}/control/shutdown`, {
|
|
2435
|
+
method: "POST",
|
|
2436
|
+
// The runner authorises every request, and the nonce authorises THIS one: the token says
|
|
2437
|
+
// "I may talk to you", the nonce says "I hold your lock" (#896).
|
|
2438
|
+
headers: { "Content-Type": "application/json", ...lock.runnerToken ? { Authorization: `Bearer ${lock.runnerToken}` } : {} },
|
|
2439
|
+
body: JSON.stringify({ nonce: lock.nonce, reason: "replaced" }),
|
|
2440
|
+
signal: AbortSignal.timeout(1e4)
|
|
2441
|
+
});
|
|
2442
|
+
if (!res.ok) return { ok: false, lines: [`The runner on pid ${lock.pid} refused the stop request (HTTP ${res.status}).`, "Stop it where it is running, then try again."] };
|
|
2443
|
+
} catch (e) {
|
|
2444
|
+
return { ok: false, lines: [`The runner on pid ${lock.pid} could not be asked to stop: ${e instanceof Error ? e.message : String(e)}.`, "Stop it where it is running, then try again."] };
|
|
2445
|
+
}
|
|
2446
|
+
const until = now() + REPLACE_EXIT_TIMEOUT_MS;
|
|
2447
|
+
while (now() < until) {
|
|
2448
|
+
if (!alive(lock.pid)) return { ok: true, lines: [`Replaced the \`pags up\` on pid ${lock.pid}`] };
|
|
2449
|
+
await sleep(500);
|
|
2450
|
+
}
|
|
2451
|
+
return {
|
|
2452
|
+
ok: false,
|
|
2453
|
+
lines: [
|
|
2454
|
+
`The runner on pid ${lock.pid} was asked to stop but is still running after ${Math.round(REPLACE_EXIT_TIMEOUT_MS / 1e3)}s.`,
|
|
2455
|
+
"It may be finishing something. Wait and try again, or stop it where it is running \u2014 this command will not kill it for you."
|
|
2456
|
+
]
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2148
2460
|
// src/commands/up.ts
|
|
2149
2461
|
var API_BASE3 = "https://api.proagentstore.online";
|
|
2150
2462
|
var CLI_VERSION2 = createRequire2(import.meta.url)("../package.json").version;
|
|
2151
|
-
|
|
2152
|
-
if (process.platform === "win32") return false;
|
|
2153
|
-
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
2154
|
-
const patterns = [
|
|
2155
|
-
"dist/browser-runner/index.js",
|
|
2156
|
-
"browser-runner/src/index",
|
|
2157
|
-
"runner connect"
|
|
2158
|
-
];
|
|
2159
|
-
let stopped = false;
|
|
2160
|
-
for (const p of patterns) {
|
|
2161
|
-
try {
|
|
2162
|
-
execFileSync3("pkill", ["-f", p], { stdio: "ignore" });
|
|
2163
|
-
stopped = true;
|
|
2164
|
-
} catch {
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
return stopped;
|
|
2168
|
-
}
|
|
2169
|
-
var upCommand = new Command8("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").action(async (opts) => {
|
|
2463
|
+
var upCommand = new Command8("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) => {
|
|
2170
2464
|
const session = requireSession();
|
|
2171
2465
|
const state = {
|
|
2172
2466
|
user: session.user.login,
|
|
@@ -2221,7 +2515,7 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
2221
2515
|
headless: opts.headless
|
|
2222
2516
|
}).catch(() => void 0);
|
|
2223
2517
|
if (!opts.instance) {
|
|
2224
|
-
const split = partitionByPin(instances,
|
|
2518
|
+
const split = partitionByPin(instances, hostname6(), loadMachineIdentity(hostname6()).names);
|
|
2225
2519
|
instances = split.here;
|
|
2226
2520
|
elsewhere = split.elsewhere;
|
|
2227
2521
|
}
|
|
@@ -2242,7 +2536,26 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
2242
2536
|
}
|
|
2243
2537
|
state.activeInstance = instances.length === 1 ? instances[0].name || instances[0].slug || instances[0].id.slice(0, 8) : `${instances.length} agents`;
|
|
2244
2538
|
printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
|
|
2245
|
-
|
|
2539
|
+
const lockFile = lockPath(session.user.login);
|
|
2540
|
+
const held = await inspectHolder(lockFile);
|
|
2541
|
+
if (held.lock && !isTakeable(held.verdict)) {
|
|
2542
|
+
if (!opts.replace) {
|
|
2543
|
+
clearScreen();
|
|
2544
|
+
printLogo(CLI_VERSION2);
|
|
2545
|
+
for (const line of holderMessage(held.lock, held.verdict)) writeLine(` ${line}`);
|
|
2546
|
+
writeLine("");
|
|
2547
|
+
process.exit(1);
|
|
2548
|
+
}
|
|
2549
|
+
const replaced = await replaceHolder(held.lock, { now: opts.now === true });
|
|
2550
|
+
if (!replaced.ok) {
|
|
2551
|
+
clearScreen();
|
|
2552
|
+
printLogo(CLI_VERSION2);
|
|
2553
|
+
for (const line of replaced.lines) writeLine(` ${line}`);
|
|
2554
|
+
writeLine("");
|
|
2555
|
+
process.exit(1);
|
|
2556
|
+
}
|
|
2557
|
+
for (const line of replaced.lines) printStep(line, "ok");
|
|
2558
|
+
}
|
|
2246
2559
|
const { spawn: spawn5 } = await import("child_process");
|
|
2247
2560
|
const cliPath = process.argv[1];
|
|
2248
2561
|
const args = [cliPath, "runner", "connect", ...instances.map((i) => i.id)];
|
|
@@ -2251,9 +2564,12 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
2251
2564
|
if (!opts.instance) args.push("--watch-instances");
|
|
2252
2565
|
const child = spawn5(process.execPath, args, {
|
|
2253
2566
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2254
|
-
|
|
2567
|
+
// The child is the lock HOLDER (#896): it owns the relay sockets, so it is what a
|
|
2568
|
+
// contender must find and what `--replace` must be able to stop.
|
|
2569
|
+
env: { ...process.env, PAGS_TOKEN: session.token, [SUPERVISED_ENV]: SUPERVISOR_RESTARTS, PAGS_LOCK_ACCOUNT: session.user.login }
|
|
2255
2570
|
});
|
|
2256
2571
|
const logs = [];
|
|
2572
|
+
let childRsid = "";
|
|
2257
2573
|
const handleOutput = (data2) => {
|
|
2258
2574
|
const text = data2.toString("utf-8");
|
|
2259
2575
|
for (const line of text.split("\n")) {
|
|
@@ -2261,6 +2577,8 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
2261
2577
|
if (!trimmed) continue;
|
|
2262
2578
|
logs.push(trimmed);
|
|
2263
2579
|
if (logs.length > 200) logs.shift();
|
|
2580
|
+
const rsidLine = trimmed.match(/^STATUS runner-session=(\S+)/);
|
|
2581
|
+
if (rsidLine) childRsid = rsidLine[1];
|
|
2264
2582
|
const status = parseStatusLine(trimmed);
|
|
2265
2583
|
if (status) {
|
|
2266
2584
|
if (status.registration) {
|
|
@@ -2319,7 +2637,13 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
2319
2637
|
return;
|
|
2320
2638
|
}
|
|
2321
2639
|
childDead = true;
|
|
2322
|
-
if (code
|
|
2640
|
+
if (code === null) {
|
|
2641
|
+
state.runner = "error";
|
|
2642
|
+
state.lastEvent = "Runner stopped by another process (signal) \u2014 it is not connected any more";
|
|
2643
|
+
printStatus(state);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
if (code !== 0) {
|
|
2323
2647
|
state.runner = "error";
|
|
2324
2648
|
state.lastEvent = `Runner exited (code ${code})`;
|
|
2325
2649
|
const recent = logs.slice(-5);
|
|
@@ -2332,7 +2656,7 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
2332
2656
|
try {
|
|
2333
2657
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
2334
2658
|
process.stdin.pause();
|
|
2335
|
-
|
|
2659
|
+
execFileSync3(process.execPath, [process.argv[1], ...restartUpArgs(opts)], { stdio: "inherit", env: { ...process.env, ...env } });
|
|
2336
2660
|
} catch (e) {
|
|
2337
2661
|
const status = e.status;
|
|
2338
2662
|
writeLine(` Restart failed${typeof status === "number" ? ` (exit ${status})` : ""} \u2014 the runner is NOT running. Run 'pags up' again.`);
|
|
@@ -2367,26 +2691,37 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
2367
2691
|
printStatus(state);
|
|
2368
2692
|
}
|
|
2369
2693
|
if (key === "r") {
|
|
2370
|
-
if (childDead)
|
|
2694
|
+
if (childDead) {
|
|
2695
|
+
const other = await inspectHolder(lockFile);
|
|
2696
|
+
if (other.lock && other.lock.rsid !== childRsid && !isTakeable(other.verdict)) {
|
|
2697
|
+
state.lastEvent = `Another \`pags up\` holds this machine (pid ${other.lock.pid}) \u2014 restart refused`;
|
|
2698
|
+
printStatus(state);
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
restartUp("Restarting runner...");
|
|
2702
|
+
}
|
|
2371
2703
|
printStatus(state);
|
|
2372
2704
|
}
|
|
2373
2705
|
}
|
|
2374
2706
|
});
|
|
2375
|
-
var downCommand = new Command8("down").description("Stop the browser runner and disconnect").action(async () => {
|
|
2707
|
+
var downCommand = new Command8("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) => {
|
|
2376
2708
|
clearScreen();
|
|
2377
2709
|
printLogo(CLI_VERSION2);
|
|
2378
|
-
|
|
2379
|
-
|
|
2710
|
+
const session = requireSession();
|
|
2711
|
+
const lock = readLock(lockPath(session.user.login));
|
|
2712
|
+
if (!lock) {
|
|
2713
|
+
writeLine(" No runner of yours is running on this machine \u2014 nothing to stop.");
|
|
2380
2714
|
writeLine("");
|
|
2381
2715
|
return;
|
|
2382
2716
|
}
|
|
2383
|
-
const stopped = await
|
|
2384
|
-
if (stopped) {
|
|
2717
|
+
const stopped = await replaceHolder(lock, { now: opts.now === true });
|
|
2718
|
+
if (stopped.ok) {
|
|
2719
|
+
releaseLock(lockPath(session.user.login), lock.rsid);
|
|
2385
2720
|
writeLine(" \u2713 Runner stopped \u2014 you're disconnected.");
|
|
2386
2721
|
writeLine("");
|
|
2387
2722
|
writeLine(" Your agent won't act on the web until you run 'pags up' again.");
|
|
2388
2723
|
} else {
|
|
2389
|
-
|
|
2724
|
+
for (const line of stopped.lines) writeLine(` ${line}`);
|
|
2390
2725
|
}
|
|
2391
2726
|
writeLine("");
|
|
2392
2727
|
});
|