@proagentstore/cli 0.4.113 → 0.4.115
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.
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import { handlerFor } from "./handlers.js";
|
|
6
6
|
import { parseCommand } from "./headless.js";
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
if (!statSync(p).isFile())
|
|
10
|
-
return false;
|
|
11
|
-
accessSync(p, constants.X_OK);
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
7
|
+
import { resolveEngineSpawn } from "./engine-spawn.js";
|
|
18
8
|
function defaultExists(p) {
|
|
19
9
|
try {
|
|
20
10
|
statSync(p);
|
|
@@ -29,28 +19,6 @@ function defaultKeychainItemStatus(service) {
|
|
|
29
19
|
const r = spawnSync("security", ["find-generic-password", "-s", service], { stdio: "ignore", timeout: 5_000 });
|
|
30
20
|
return r.error ? null : r.status;
|
|
31
21
|
}
|
|
32
|
-
/** The same resolution `spawn(bin)` does: a path is taken as-is, a bare name is searched on PATH. */
|
|
33
|
-
export function resolveBinary(bin, deps = {}) {
|
|
34
|
-
const isExec = deps.isExecutable ?? defaultIsExecutable;
|
|
35
|
-
if (!bin)
|
|
36
|
-
return null;
|
|
37
|
-
if (bin.includes("/") || isAbsolute(bin)) {
|
|
38
|
-
const p = resolve(bin);
|
|
39
|
-
return isExec(p) ? p : null;
|
|
40
|
-
}
|
|
41
|
-
const env = deps.env ?? process.env;
|
|
42
|
-
const exts = (deps.platform ?? process.platform) === "win32" ? (env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
|
|
43
|
-
for (const dir of (env.PATH || "").split(delimiter)) {
|
|
44
|
-
if (!dir)
|
|
45
|
-
continue;
|
|
46
|
-
for (const ext of exts) {
|
|
47
|
-
const p = join(dir, bin + ext);
|
|
48
|
-
if (isExec(p))
|
|
49
|
-
return p;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
22
|
/** Does this engine have a stored interactive login on the machine? */
|
|
55
23
|
export function detectEngineLogin(engine, deps = {}) {
|
|
56
24
|
const env = deps.env ?? process.env;
|
|
@@ -100,14 +68,14 @@ export function checkEngine(input, deps = {}) {
|
|
|
100
68
|
const engine = (["claude", "gemini", "codex", "grok", "generic"].includes(input.clientType ?? "") ? input.clientType : "claude");
|
|
101
69
|
const parsed = parseCommand(input.command);
|
|
102
70
|
const bin = parsed.bin || parseCommand(handlerFor(engine).cliCommand).bin || engine;
|
|
103
|
-
const binaryPath =
|
|
71
|
+
const { binaryPath, via } = resolveEngineSpawn(bin, [], deps);
|
|
104
72
|
const login = input.credentialInjected ? { login: "found", where: "platform" } : detectEngineLogin(engine, deps);
|
|
105
73
|
return {
|
|
106
74
|
checked: true,
|
|
107
75
|
engine,
|
|
108
76
|
bin,
|
|
109
77
|
binaryFound: binaryPath !== null,
|
|
110
|
-
...(binaryPath ? { binaryPath } : {}),
|
|
78
|
+
...(binaryPath ? { binaryPath, binaryVia: via } : {}),
|
|
111
79
|
login: login.login,
|
|
112
80
|
...(login.where ? { loginWhere: login.where } : {}),
|
|
113
81
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { accessSync, constants, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
function defaultIsExecutable(p) {
|
|
4
|
+
try {
|
|
5
|
+
if (!statSync(p).isFile())
|
|
6
|
+
return false;
|
|
7
|
+
accessSync(p, constants.X_OK);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function defaultReadText(p) {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(p, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Windows env names are case-insensitive, but a copied env object is not (`Path`, `ComSpec`). */
|
|
23
|
+
function envVar(env, name, windows) {
|
|
24
|
+
if (!windows)
|
|
25
|
+
return env[name];
|
|
26
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === name);
|
|
27
|
+
return key ? env[key] : undefined;
|
|
28
|
+
}
|
|
29
|
+
const isPathLike = (bin, windows) => bin.includes("/") || (windows ? bin.includes("\\") || win32.isAbsolute(bin) : posix.isAbsolute(bin));
|
|
30
|
+
/** The same resolution `spawn(bin)` does: a path is taken as-is, a bare name is searched on PATH (with PATHEXT on Windows). */
|
|
31
|
+
export function resolveBinary(bin, deps = {}) {
|
|
32
|
+
const isExec = deps.isExecutable ?? defaultIsExecutable;
|
|
33
|
+
if (!bin)
|
|
34
|
+
return null;
|
|
35
|
+
const windows = (deps.platform ?? process.platform) === "win32";
|
|
36
|
+
const path = windows ? win32 : posix;
|
|
37
|
+
if (isPathLike(bin, windows)) {
|
|
38
|
+
const p = path.resolve(bin);
|
|
39
|
+
return isExec(p) ? p : null;
|
|
40
|
+
}
|
|
41
|
+
const env = deps.env ?? process.env;
|
|
42
|
+
const exts = windows ? (envVar(env, "PATHEXT", true) || ".EXE;.CMD;.BAT").split(";") : [""];
|
|
43
|
+
for (const dir of (envVar(env, "PATH", windows) || "").split(path.delimiter)) {
|
|
44
|
+
if (!dir)
|
|
45
|
+
continue;
|
|
46
|
+
for (const ext of exts) {
|
|
47
|
+
const p = path.join(dir, bin + ext);
|
|
48
|
+
if (isExec(p))
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/** The first `%dp0%\node_modules\…` target an npm (cmd-shim) `.cmd` launches, absolute. */
|
|
55
|
+
export function shimTarget(shimPath, text) {
|
|
56
|
+
const m = /"%~?dp0%?\\(node_modules\\[^"]+)"/i.exec(text);
|
|
57
|
+
return m ? win32.join(win32.dirname(shimPath), m[1]) : null;
|
|
58
|
+
}
|
|
59
|
+
// cmd.exe metacharacters (cross-spawn's set). A caret makes the next one literal for one parse.
|
|
60
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
61
|
+
/**
|
|
62
|
+
* One argument for `cmd.exe /s /c "<shim> <args>"`: quoted for the CRT's argv parser, then
|
|
63
|
+
* caret-escaped twice — once for cmd's own parse, once more for the shim's `%*` re-parse.
|
|
64
|
+
*/
|
|
65
|
+
export function quoteCmdArg(arg) {
|
|
66
|
+
let a = arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
|
|
67
|
+
a = `"${a}"`;
|
|
68
|
+
return a.replace(CMD_META, "^$1").replace(CMD_META, "^$1");
|
|
69
|
+
}
|
|
70
|
+
export function resolveEngineSpawn(bin, args, deps = {}) {
|
|
71
|
+
const windows = (deps.platform ?? process.platform) === "win32";
|
|
72
|
+
if (!windows)
|
|
73
|
+
return { command: bin, args, binaryPath: resolveBinary(bin, deps), via: "direct" };
|
|
74
|
+
const env = deps.env ?? process.env;
|
|
75
|
+
// Real executables first: PATHEXT order would otherwise pick `.CMD` over a later `.EXE` in no order we chose.
|
|
76
|
+
const exe = resolveBinary(bin, { ...deps, env: { ...env, PATHEXT: ".exe;.com" } });
|
|
77
|
+
const found = exe ?? resolveBinary(bin, { ...deps, env: { ...env, PATHEXT: ".cmd;.bat" } });
|
|
78
|
+
if (!found)
|
|
79
|
+
return { command: bin, args, binaryPath: null, via: "direct" };
|
|
80
|
+
if (!/\.(cmd|bat)$/i.test(found))
|
|
81
|
+
return { command: found, args, binaryPath: found, via: "direct" };
|
|
82
|
+
const isExec = deps.isExecutable ?? defaultIsExecutable;
|
|
83
|
+
const text = (deps.readText ?? defaultReadText)(found);
|
|
84
|
+
const target = text ? shimTarget(found, text) : null;
|
|
85
|
+
if (target && isExec(target)) {
|
|
86
|
+
if (/\.(exe|com)$/i.test(target))
|
|
87
|
+
return { command: target, args, binaryPath: target, via: "direct" };
|
|
88
|
+
if (/\.[cm]?js$/i.test(target))
|
|
89
|
+
return { command: deps.nodePath ?? process.execPath, args: [target, ...args], binaryPath: target, via: "node" };
|
|
90
|
+
}
|
|
91
|
+
const line = [found.replace(CMD_META, "^$1"), ...args.map(quoteCmdArg)].join(" ");
|
|
92
|
+
return {
|
|
93
|
+
command: envVar(env, "COMSPEC", true) || "cmd.exe",
|
|
94
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
95
|
+
windowsVerbatimArguments: true,
|
|
96
|
+
binaryPath: found,
|
|
97
|
+
via: "cmd",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -7,6 +7,7 @@ import { appendTurnTail, turnReportFromExit, turnReportFromResult } from "./engi
|
|
|
7
7
|
import { renderToolResult, shortInput, stripAnsi } from "./transcript-lines.js";
|
|
8
8
|
import { authoredTurn, authorTag } from "./turn-author.js";
|
|
9
9
|
import { engineSpawnEnv, mergeEnv } from "./engine-env.js";
|
|
10
|
+
import { resolveEngineSpawn } from "./engine-spawn.js";
|
|
10
11
|
import { ghGuardStatus } from "./gh-guard.js";
|
|
11
12
|
import { handlerFor } from "./handlers.js";
|
|
12
13
|
import { resolveEngineAuth } from "./engine-auth.js";
|
|
@@ -426,10 +427,13 @@ export class HeadlessSession {
|
|
|
426
427
|
this.inputs.newEngine();
|
|
427
428
|
this.buf = "";
|
|
428
429
|
const args = this.adapter.buildLaunchArgs(this.cmdArgs, this.claudeSessionId);
|
|
429
|
-
const
|
|
430
|
+
const env = this.spawnEnv;
|
|
431
|
+
const launch = resolveEngineSpawn(this.cmdBin, args, { env });
|
|
432
|
+
const proc = spawn(launch.command, launch.args, {
|
|
430
433
|
cwd: this.config.workDir,
|
|
431
|
-
env
|
|
434
|
+
env,
|
|
432
435
|
stdio: ["pipe", "pipe", "pipe"],
|
|
436
|
+
windowsVerbatimArguments: launch.windowsVerbatimArguments,
|
|
433
437
|
});
|
|
434
438
|
this.proc = proc;
|
|
435
439
|
this.run = "idle";
|
|
@@ -582,10 +586,13 @@ export class HeadlessSession {
|
|
|
582
586
|
const turnArgs = resumedCodexThreadId
|
|
583
587
|
? buildCodexResumeArgs(this.cmdArgs, resumedCodexThreadId, text)
|
|
584
588
|
: this.adapter.buildTurnArgs(this.cmdArgs, text);
|
|
585
|
-
const
|
|
589
|
+
const env = this.spawnEnv;
|
|
590
|
+
const launch = resolveEngineSpawn(this.cmdBin, turnArgs, { env });
|
|
591
|
+
const proc = spawn(launch.command, launch.args, {
|
|
586
592
|
cwd: this.config.workDir,
|
|
587
|
-
env
|
|
593
|
+
env,
|
|
588
594
|
stdio: ["ignore", "pipe", "pipe"],
|
|
595
|
+
windowsVerbatimArguments: launch.windowsVerbatimArguments,
|
|
589
596
|
});
|
|
590
597
|
// A turn already running is aborted before its replacement starts: `input()` accepts a turn
|
|
591
598
|
// at any time — a human typing in the console, a takeover, a Loop that stopped waiting —
|
package/dist/index.js
CHANGED
|
@@ -1260,6 +1260,7 @@ import { execFile, spawn as spawn3 } from "child_process";
|
|
|
1260
1260
|
import { promisify } from "util";
|
|
1261
1261
|
var run = promisify(execFile);
|
|
1262
1262
|
var RUNNER_UPDATE_PATH = "/pags/runner/update";
|
|
1263
|
+
var RUNNER_RESTART_PATH = "/pags/runner/restart";
|
|
1263
1264
|
var RUNNER_RESTART_EXIT_CODE = 75;
|
|
1264
1265
|
var SUPERVISED_ENV = "PAGS_UP_SUPERVISED";
|
|
1265
1266
|
var SUPERVISOR_RESTARTS = "2";
|
|
@@ -1317,6 +1318,28 @@ function planRunnerUpdate(f) {
|
|
|
1317
1318
|
if (f.busy.length > 0) return { action: "wait", current: f.current, latest: f.latest, waitingFor: f.busy };
|
|
1318
1319
|
return { action: "update", current: f.current, latest: f.latest, restarter: f.restarter };
|
|
1319
1320
|
}
|
|
1321
|
+
function planRunnerRestart(f) {
|
|
1322
|
+
if (!f.restarter) {
|
|
1323
|
+
return {
|
|
1324
|
+
action: "refused",
|
|
1325
|
+
current: f.current,
|
|
1326
|
+
reason: `Nothing would start this runner again after a restart: it was not started by \`pags up\`, and neither ${SERVICE_ENV}=1 nor ${RESTART_COMMAND_ENV} is set. Restart it at the machine.`
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
if (!f.force) {
|
|
1330
|
+
if (f.busy === null || f.busy.includes("runner-work-observation-unavailable")) {
|
|
1331
|
+
return {
|
|
1332
|
+
action: "refused",
|
|
1333
|
+
current: f.current,
|
|
1334
|
+
reason: "The local runtime did not answer the work check, so this machine cannot be shown idle \u2014 it may be the part that is wedged. Restart with force to restart anyway; any run in progress on this machine is lost."
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
if (f.busy.length > 0) {
|
|
1338
|
+
return { action: "refused", current: f.current, waitingFor: f.busy, reason: `${f.busy.length} piece(s) of work are running here. Restart when they finish, or with force (that work is lost).` };
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return { action: "restart", current: f.current, restarter: f.restarter };
|
|
1342
|
+
}
|
|
1320
1343
|
async function installVersion(version2) {
|
|
1321
1344
|
if (process.env[BOOTSTRAP_ENV]) {
|
|
1322
1345
|
await installPayload(version2);
|
|
@@ -2222,7 +2245,7 @@ function createInstallBridge(deps) {
|
|
|
2222
2245
|
|
|
2223
2246
|
// src/commands/runner/relay.ts
|
|
2224
2247
|
var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
|
|
2225
|
-
var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH]);
|
|
2248
|
+
var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH, RUNNER_RESTART_PATH]);
|
|
2226
2249
|
async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false, watchInstances = false) {
|
|
2227
2250
|
const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
|
|
2228
2251
|
const pagsToken = clean(opts.pagsToken) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
|
|
@@ -2279,7 +2302,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2279
2302
|
writeLine(formatStatusLine({ registration: state, agents, reason: state === "ok" ? void 0 : lastRegisterError }));
|
|
2280
2303
|
};
|
|
2281
2304
|
for (const id of instanceIds) await registerRuntime(id);
|
|
2282
|
-
const
|
|
2305
|
+
const workLook = async (signal) => {
|
|
2283
2306
|
const [sessionsResult, healthResult] = await Promise.allSettled([
|
|
2284
2307
|
requestRunner("GET", "/coding/sessions", {
|
|
2285
2308
|
url: localUrl,
|
|
@@ -2288,7 +2311,10 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2288
2311
|
}, void 0, signal),
|
|
2289
2312
|
requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] }, void 0, signal)
|
|
2290
2313
|
]);
|
|
2291
|
-
|
|
2314
|
+
return readWorkLook(sessionsResult, healthResult, Date.now());
|
|
2315
|
+
};
|
|
2316
|
+
const updateFacts = async (signal) => {
|
|
2317
|
+
const { busy, observations } = await workLook(signal);
|
|
2292
2318
|
return {
|
|
2293
2319
|
observations,
|
|
2294
2320
|
current: CLI_VERSION,
|
|
@@ -2389,8 +2415,21 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2389
2415
|
setTimeout(() => leaveForRestart(plan.restarter), 25).unref();
|
|
2390
2416
|
return { status: 200, result: { action: "restarting", current: plan.current, latest: plan.latest, restartedBy: plan.restarter, ...supervisor ? { supervisor } : {} } };
|
|
2391
2417
|
};
|
|
2418
|
+
const answerRestart = async (body) => {
|
|
2419
|
+
const request = body;
|
|
2420
|
+
const dryRun = request?.dryRun === true;
|
|
2421
|
+
const force2 = request?.force === true;
|
|
2422
|
+
if (bridge.inFlight()) return { status: 409, result: { error: "A runner update is installing or restarting; nothing else restarted." } };
|
|
2423
|
+
const look = force2 ? null : await boundedWork((signal) => workLook(signal));
|
|
2424
|
+
const plan = planRunnerRestart({ current: CLI_VERSION, restarter: restarterFrom(process.env), busy: force2 ? [] : look?.busy ?? null, force: force2 });
|
|
2425
|
+
if (dryRun || plan.action === "refused") return { status: 200, result: { ...plan, dryRun } };
|
|
2426
|
+
setTimeout(() => leaveForRestart(plan.restarter), 25).unref();
|
|
2427
|
+
const supervisor = supervisorNote(plan.restarter);
|
|
2428
|
+
return { status: 200, result: { action: "restarting", current: plan.current, latest: plan.current, restartedBy: plan.restarter, forced: force2, ...supervisor ? { supervisor } : {} } };
|
|
2429
|
+
};
|
|
2392
2430
|
const answerControl = async (path, body) => {
|
|
2393
2431
|
if (path === RUNNER_UPDATE_PATH) return answerUpdate(body);
|
|
2432
|
+
if (path === RUNNER_RESTART_PATH) return answerRestart(body);
|
|
2394
2433
|
if (path !== MEMBERSHIP_SYNC_PATH) return { status: 404, result: { error: `Unknown runner control ${path}` } };
|
|
2395
2434
|
const request = body;
|
|
2396
2435
|
const named = typeof request?.attach === "string" ? request.attach : "";
|