@proagentstore/cli 0.4.114 → 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 { accessSync, constants, statSync } from "node:fs";
2
+ import { statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { delimiter, isAbsolute, join, resolve } from "node:path";
4
+ import { join } from "node:path";
5
5
  import { handlerFor } from "./handlers.js";
6
6
  import { parseCommand } from "./headless.js";
7
- function defaultIsExecutable(p) {
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 = resolveBinary(bin, deps);
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 proc = spawn(this.cmdBin, args, {
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: this.spawnEnv,
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 proc = spawn(this.cmdBin, turnArgs, {
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: this.spawnEnv,
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.114",
3
+ "version": "0.4.115",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",