copillm 0.2.5 → 0.2.6

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,10 +1,14 @@
1
- import { spawn } from "node:child_process";
2
1
  import { resolveAgent } from "./resolveAgent.js";
2
+ import { spawnAgent } from "./windowsSpawn.js";
3
3
  export async function launchAgent(opts) {
4
4
  const log = opts.log ?? ((line) => process.stderr.write(`${line}\n`));
5
5
  let resolved;
6
6
  try {
7
- resolved = await resolveAgent(opts.agent, { pinnedSpec: opts.pinnedSpec, log });
7
+ resolved = await resolveAgent(opts.agent, {
8
+ pinnedSpec: opts.pinnedSpec,
9
+ preferPath: useSystemAgentOptIn(),
10
+ log
11
+ });
8
12
  }
9
13
  catch (error) {
10
14
  const message = error instanceof Error ? error.message : String(error);
@@ -14,11 +18,9 @@ export async function launchAgent(opts) {
14
18
  }
15
19
  log(resolved.displayLine);
16
20
  const childEnv = { ...process.env, ...opts.env };
17
- const useShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(resolved.binPath);
18
- const child = spawn(resolved.binPath, opts.args, {
21
+ const child = spawnAgent(resolved.binPath, opts.args, {
19
22
  stdio: "inherit",
20
- env: childEnv,
21
- shell: useShell
23
+ env: childEnv
22
24
  });
23
25
  return new Promise((resolve, reject) => {
24
26
  child.once("error", reject);
@@ -65,3 +67,17 @@ function installHint(agent) {
65
67
  " npm i -g @anthropic-ai/claude-code"
66
68
  ].join("\n");
67
69
  }
70
+ /**
71
+ * Whether the user has opted in to letting copillm fall back to a system-installed
72
+ * coding-agent binary on PATH. Off by default — copillm uses its own cache and
73
+ * downloads on demand so the executed version is deterministic.
74
+ *
75
+ * Opt in by setting `COPILLM_USE_SYSTEM_AGENT` to `1`, `true`, or `yes`
76
+ * (case-insensitive).
77
+ */
78
+ function useSystemAgentOptIn() {
79
+ const raw = process.env.COPILLM_USE_SYSTEM_AGENT;
80
+ if (!raw)
81
+ return false;
82
+ return /^(1|true|yes)$/i.test(raw.trim());
83
+ }
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  const FALLBACK_PACKAGE_INFO = {
3
3
  name: "copillm",
4
- version: "0.2.5"
4
+ version: "0.2.6"
5
5
  };
6
6
  export function getPackageInfo() {
7
7
  const envName = cleanPackageValue(process.env.COPILLM_PACKAGE_NAME);
@@ -39,8 +39,12 @@ export async function resolveAgent(agent, opts = {}) {
39
39
  const pkg = pin.packageName;
40
40
  const binName = AGENT_REGISTRY[agent].binName;
41
41
  const agentRoot = path.join(cacheRoot, agent);
42
- // 1. PATH lookup (skipped when user pinned a specific version)
43
- if (!pin.version && opts.preferPath !== false) {
42
+ // 1. PATH lookup (opt-in only).
43
+ // PATH lookup is OFF by default so the running agent version is always the one copillm
44
+ // manages in its cache. Users who want to fall back to a system-installed binary can opt
45
+ // in via the COPILLM_USE_SYSTEM_AGENT env var (wired in launchAgent.ts) or by passing
46
+ // `preferPath: true` directly. Pinned versions always skip this branch.
47
+ if (!pin.version && opts.preferPath === true) {
44
48
  const found = findOnPath(binName);
45
49
  if (found) {
46
50
  const v = probeVersion(found) ?? "unknown";
@@ -0,0 +1,71 @@
1
+ import { spawn } from "node:child_process";
2
+ const META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
3
+ function escapeArgument(arg, doubleEscapeMetaChars) {
4
+ let escaped = `${arg}`;
5
+ escaped = escaped.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"');
6
+ escaped = escaped.replace(/(?=(\\+?)?)\1$/, "$1$1");
7
+ escaped = `"${escaped}"`;
8
+ escaped = escaped.replace(META_CHARS, "^$1");
9
+ if (doubleEscapeMetaChars) {
10
+ escaped = escaped.replace(META_CHARS, "^$1");
11
+ }
12
+ return escaped;
13
+ }
14
+ function escapeCommand(command) {
15
+ return command.replace(META_CHARS, "^$1");
16
+ }
17
+ /**
18
+ * Build the `cmd.exe /d /s /c "..."` invocation we need to run a `.cmd` /
19
+ * `.bat` file safely on Windows.
20
+ *
21
+ * Background: Node's `child_process.spawn` cannot directly exec a batch file
22
+ * (CreateProcess only understands real PE binaries), and `shell: true` is now
23
+ * deprecated when combined with an args array because Node performs no
24
+ * escaping (see Node DEP0190). The accepted alternative — long used by
25
+ * cross-spawn and npm's own bin shims — is to spawn `cmd.exe` ourselves,
26
+ * pre-quote the command line, and set `windowsVerbatimArguments: true` so
27
+ * Node hands the buffer to Windows untouched.
28
+ *
29
+ * The quoting follows the well-known two-layer algorithm:
30
+ * 1. Apply Microsoft's CommandLineToArgvW rules (backslash/quote dance) so
31
+ * that the underlying program parses each argument back into the values
32
+ * we passed in.
33
+ * 2. Escape cmd.exe metacharacters (`^ & | < > ( ) % ! ;` etc.) with `^` so
34
+ * they don't get interpreted by the shell before the program sees them.
35
+ *
36
+ * `doubleEscape` is needed when the target is an npm-generated `.cmd` shim
37
+ * (which itself spawns a nested cmd.exe via `CALL` on older npm versions, or
38
+ * via subshell composition); each cmd.exe parse strips one layer of `^`, so
39
+ * we apply it twice to survive the round trip. We default to true for
40
+ * `.cmd`/`.bat` because every agent we launch is installed via npm.
41
+ */
42
+ export function buildWindowsCmdInvocation(file, args, doubleEscape = true) {
43
+ const escapedCommand = escapeCommand(file);
44
+ const escapedArgs = args.map((a) => escapeArgument(a, doubleEscape));
45
+ const commandLine = [escapedCommand, ...escapedArgs].join(" ");
46
+ const comspec = process.env.ComSpec || process.env.comspec || "cmd.exe";
47
+ return {
48
+ command: comspec,
49
+ args: ["/d", "/s", "/c", `"${commandLine}"`]
50
+ };
51
+ }
52
+ /**
53
+ * Spawn a child process, transparently routing `.cmd` / `.bat` files through
54
+ * `cmd.exe` with safe quoting on Windows. Non-Windows platforms and real
55
+ * `.exe` / `.com` binaries go through a direct `spawn` with no shell flag.
56
+ *
57
+ * Mirrors the surface of `child_process.spawn(file, args, options)` but
58
+ * never sets `shell: true` and therefore never triggers Node's DEP0190
59
+ * deprecation warning.
60
+ */
61
+ export function spawnAgent(file, args, options) {
62
+ if (process.platform !== "win32" || !/\.(cmd|bat)$/i.test(file)) {
63
+ return spawn(file, args, { ...options, shell: false });
64
+ }
65
+ const { command, args: cmdArgs } = buildWindowsCmdInvocation(file, args);
66
+ return spawn(command, cmdArgs, {
67
+ ...options,
68
+ shell: false,
69
+ windowsVerbatimArguments: true
70
+ });
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copillm",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Local Copilot proxy CLI (OpenAI/Anthropic-compatible)",
5
5
  "license": "MIT",
6
6
  "type": "module",