copillm 0.2.4 → 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.
- package/dist/cli/commands/daemon.js +4 -3
- package/dist/cli/copillmFlags.js +11 -1
- package/dist/cli/daemon/ensureRunning.js +3 -4
- package/dist/cli/daemon/selfSpawn.js +15 -0
- package/dist/cli/index.js +6 -8
- package/dist/cli/launchAgent.js +22 -6
- package/dist/cli/packageInfo.js +29 -0
- package/dist/cli/resolveAgent.js +6 -2
- package/dist/cli/updateNotifier.js +223 -0
- package/dist/cli/windowsSpawn.js +71 -0
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ import { refreshPiHome } from "../integrations/refreshPi.js";
|
|
|
16
16
|
import { writeAuthStatusLine } from "../shared/backends.js";
|
|
17
17
|
import { currentDebugLogPath, enableRuntimeDebug, getRootLogger, resolveCopillmDebug } from "../shared/debug.js";
|
|
18
18
|
import { writeCommandOutput, writeHealthOutput } from "../shared/output.js";
|
|
19
|
+
import { buildSelfSpawnCommand } from "../daemon/selfSpawn.js";
|
|
19
20
|
export function register(program) {
|
|
20
21
|
program
|
|
21
22
|
.command("start")
|
|
@@ -71,11 +72,11 @@ export function register(program) {
|
|
|
71
72
|
});
|
|
72
73
|
return;
|
|
73
74
|
}
|
|
74
|
-
const
|
|
75
|
+
const daemonCommand = buildSelfSpawnCommand("daemon");
|
|
75
76
|
if (debug) {
|
|
76
|
-
|
|
77
|
+
daemonCommand.args.push("--debug");
|
|
77
78
|
}
|
|
78
|
-
const child = spawn(
|
|
79
|
+
const child = spawn(daemonCommand.command, daemonCommand.args, {
|
|
79
80
|
detached: true,
|
|
80
81
|
stdio: "ignore",
|
|
81
82
|
env: daemonSpawnEnv(debug)
|
package/dist/cli/copillmFlags.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
export const COPILLM_FLAGS = [
|
|
19
19
|
{
|
|
20
20
|
flag: "--copillm-use",
|
|
21
|
+
aliases: ["--use"],
|
|
21
22
|
takesValue: true,
|
|
22
23
|
dest: "copillmUse",
|
|
23
24
|
kind: "swallow",
|
|
@@ -25,6 +26,7 @@ export const COPILLM_FLAGS = [
|
|
|
25
26
|
},
|
|
26
27
|
{
|
|
27
28
|
flag: "--copillm-debug",
|
|
29
|
+
aliases: ["--debug"],
|
|
28
30
|
takesValue: false,
|
|
29
31
|
dest: "copillmDebug",
|
|
30
32
|
kind: "swallow",
|
|
@@ -32,6 +34,7 @@ export const COPILLM_FLAGS = [
|
|
|
32
34
|
},
|
|
33
35
|
{
|
|
34
36
|
flag: "--copillm-profile",
|
|
37
|
+
aliases: ["--profile"],
|
|
35
38
|
takesValue: true,
|
|
36
39
|
dest: "copillmProfile",
|
|
37
40
|
kind: "swallow",
|
|
@@ -39,6 +42,7 @@ export const COPILLM_FLAGS = [
|
|
|
39
42
|
},
|
|
40
43
|
{
|
|
41
44
|
flag: "--copillm-no-config",
|
|
45
|
+
aliases: ["--no-config"],
|
|
42
46
|
takesValue: false,
|
|
43
47
|
dest: "copillmNoConfig",
|
|
44
48
|
kind: "swallow",
|
|
@@ -52,7 +56,13 @@ export const COPILLM_FLAGS = [
|
|
|
52
56
|
description: "Skip approvals (translated per-agent)"
|
|
53
57
|
}
|
|
54
58
|
];
|
|
55
|
-
const SPEC_BY_FLAG = new Map(
|
|
59
|
+
const SPEC_BY_FLAG = new Map();
|
|
60
|
+
for (const spec of COPILLM_FLAGS) {
|
|
61
|
+
SPEC_BY_FLAG.set(spec.flag, spec);
|
|
62
|
+
for (const alias of spec.aliases ?? []) {
|
|
63
|
+
SPEC_BY_FLAG.set(alias, spec);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
56
66
|
/**
|
|
57
67
|
* Extract copillm-owned flags from a raw arg tail, returning the parsed opts
|
|
58
68
|
* plus everything else to forward to the agent.
|
|
@@ -6,6 +6,7 @@ import { displayHomePath } from "../integrations/banner.js";
|
|
|
6
6
|
import { isPidAlive } from "./lifecycle.js";
|
|
7
7
|
import { readLiveLock, waitForDaemonReady, warnIfDebugRequestedButInactive } from "./probes.js";
|
|
8
8
|
import { daemonSpawnEnv } from "./spawnEnv.js";
|
|
9
|
+
import { buildSelfSpawnCommand } from "./selfSpawn.js";
|
|
9
10
|
export async function ensureDaemonRunningForLauncher(opts) {
|
|
10
11
|
const live = await readLiveLock();
|
|
11
12
|
if (live) {
|
|
@@ -22,10 +23,8 @@ export async function ensureDaemonRunningForLauncher(opts) {
|
|
|
22
23
|
process.stderr.write(opts.debug && debugLog
|
|
23
24
|
? `Starting copillm in background with debug logging at ${displayHomePath(debugLog)}...\n`
|
|
24
25
|
: `Starting copillm in background...\n`);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
daemonArgs.push("--debug");
|
|
28
|
-
const child = spawn(process.execPath, daemonArgs, {
|
|
26
|
+
const daemonCommand = buildSelfSpawnCommand("daemon", opts.debug ? ["--debug"] : []);
|
|
27
|
+
const child = spawn(daemonCommand.command, daemonCommand.args, {
|
|
29
28
|
detached: true,
|
|
30
29
|
stdio: ["ignore", "ignore", "pipe"],
|
|
31
30
|
env: daemonSpawnEnv(opts.debug)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function buildSelfSpawnCommand(subcommand, extraArgs = [], runtime = process) {
|
|
3
|
+
const entryPoint = runtime.argv[1];
|
|
4
|
+
if (!entryPoint || sameExecutable(entryPoint, runtime.execPath)) {
|
|
5
|
+
return { command: runtime.execPath, args: [subcommand, ...extraArgs] };
|
|
6
|
+
}
|
|
7
|
+
return { command: runtime.execPath, args: [entryPoint, subcommand, ...extraArgs] };
|
|
8
|
+
}
|
|
9
|
+
function sameExecutable(left, right) {
|
|
10
|
+
const normalizedLeft = path.resolve(left);
|
|
11
|
+
const normalizedRight = path.resolve(right);
|
|
12
|
+
return process.platform === "win32"
|
|
13
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
14
|
+
: normalizedLeft === normalizedRight;
|
|
15
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import { Command } from "commander";
|
|
3
2
|
import { createLogger } from "../config/logging.js";
|
|
4
3
|
import { registerConfigCommands } from "./configCommands.js";
|
|
@@ -11,19 +10,18 @@ import * as claudeCmd from "./commands/agents/claude.js";
|
|
|
11
10
|
import * as piCmd from "./commands/agents/pi.js";
|
|
12
11
|
import * as copilotCmd from "./commands/agents/copilot.js";
|
|
13
12
|
import { setRootLogger, setRootProgram } from "./shared/debug.js";
|
|
13
|
+
import { getPackageInfo } from "./packageInfo.js";
|
|
14
|
+
import { maybeNotifyAboutUpdate } from "./updateNotifier.js";
|
|
14
15
|
const logger = createLogger();
|
|
15
16
|
const program = new Command();
|
|
16
17
|
setRootProgram(program);
|
|
17
18
|
setRootLogger(logger);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// resolves the same file in both `dist/cli.js` (one level deep) and `src/cli.ts`
|
|
22
|
-
// when invoked via tsx.
|
|
23
|
-
const pkgVersion = createRequire(import.meta.url)("../../package.json").version;
|
|
24
|
-
program.name("copillm").description("Local Copilot proxy").version(pkgVersion);
|
|
19
|
+
const pkg = getPackageInfo();
|
|
20
|
+
await maybeNotifyAboutUpdate({ packageInfo: pkg });
|
|
21
|
+
program.name("copillm").description("Local Copilot proxy").version(pkg.version);
|
|
25
22
|
program.enablePositionalOptions();
|
|
26
23
|
program.option("--debug", "Enable copillm debug mode (debug endpoint plus verbose daemon diagnostics)");
|
|
24
|
+
program.option("--no-update-notifier", "Skip the npm registry update check for this run");
|
|
27
25
|
authCmd.register(program);
|
|
28
26
|
daemonCmd.register(program);
|
|
29
27
|
modelsCmd.register(program);
|
package/dist/cli/launchAgent.js
CHANGED
|
@@ -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, {
|
|
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
|
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const FALLBACK_PACKAGE_INFO = {
|
|
3
|
+
name: "copillm",
|
|
4
|
+
version: "0.2.6"
|
|
5
|
+
};
|
|
6
|
+
export function getPackageInfo() {
|
|
7
|
+
const envName = cleanPackageValue(process.env.COPILLM_PACKAGE_NAME);
|
|
8
|
+
const envVersion = cleanPackageValue(process.env.COPILLM_PACKAGE_VERSION);
|
|
9
|
+
if (envName && envVersion) {
|
|
10
|
+
return { name: envName, version: envVersion };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const pkg = createRequire(import.meta.url)("../../package.json");
|
|
14
|
+
if (typeof pkg.name === "string" && pkg.name.length > 0 && typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
15
|
+
return { name: pkg.name, version: pkg.version };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Standalone bundles may not have package.json on disk.
|
|
20
|
+
}
|
|
21
|
+
return FALLBACK_PACKAGE_INFO;
|
|
22
|
+
}
|
|
23
|
+
export function fallbackPackageInfo() {
|
|
24
|
+
return FALLBACK_PACKAGE_INFO;
|
|
25
|
+
}
|
|
26
|
+
function cleanPackageValue(value) {
|
|
27
|
+
const trimmed = value?.trim();
|
|
28
|
+
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
29
|
+
}
|
package/dist/cli/resolveAgent.js
CHANGED
|
@@ -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 (
|
|
43
|
-
|
|
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,223 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { writeFileSecureAtomic } from "../config/fsSecurity.js";
|
|
5
|
+
import { getCopillmHome } from "../config/home.js";
|
|
6
|
+
const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
7
|
+
const UPDATE_CHECK_TIMEOUT_MS = 3_000;
|
|
8
|
+
export async function maybeNotifyAboutUpdate(options) {
|
|
9
|
+
const argv = options.argv ?? process.argv;
|
|
10
|
+
const env = options.env ?? process.env;
|
|
11
|
+
const stderr = options.stderr ?? process.stderr;
|
|
12
|
+
const now = options.now ?? Date.now;
|
|
13
|
+
const packageInfo = options.packageInfo;
|
|
14
|
+
const cacheFile = options.cacheFilePath ?? updateCachePath();
|
|
15
|
+
if (!shouldRunUpdateCheck({ argv, env, moduleUrl: options.moduleUrl ?? import.meta.url, packageInfo, stderr })) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const cache = readUpdateCache(cacheFile, packageInfo.name);
|
|
19
|
+
const checkedAt = now();
|
|
20
|
+
const latestVersion = await fetchLatestNpmVersion(packageInfo.name, {
|
|
21
|
+
fetchImpl: options.fetchImpl,
|
|
22
|
+
registryUrl: env.COPILLM_UPDATE_REGISTRY_URL,
|
|
23
|
+
timeoutMs: UPDATE_CHECK_TIMEOUT_MS
|
|
24
|
+
});
|
|
25
|
+
if (latestVersion) {
|
|
26
|
+
writeUpdateCache(cacheFile, {
|
|
27
|
+
version: 1,
|
|
28
|
+
packageName: packageInfo.name,
|
|
29
|
+
latestVersion,
|
|
30
|
+
checkedAt
|
|
31
|
+
});
|
|
32
|
+
notifyIfNewer(stderr, packageInfo, latestVersion);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
writeUpdateCache(cacheFile, {
|
|
36
|
+
version: 1,
|
|
37
|
+
packageName: packageInfo.name,
|
|
38
|
+
latestVersion: cache?.latestVersion ?? null,
|
|
39
|
+
checkedAt
|
|
40
|
+
});
|
|
41
|
+
notifyIfNewer(stderr, packageInfo, cache?.latestVersion ?? null);
|
|
42
|
+
}
|
|
43
|
+
export async function fetchLatestNpmVersion(packageName, options = {}) {
|
|
44
|
+
const registryUrl = options.registryUrl && options.registryUrl.trim().length > 0
|
|
45
|
+
? options.registryUrl.trim()
|
|
46
|
+
: DEFAULT_REGISTRY_URL;
|
|
47
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
48
|
+
const timeoutMs = options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS;
|
|
49
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetchImpl(distTagsUrl(packageName, registryUrl), {
|
|
52
|
+
headers: { accept: "application/json" },
|
|
53
|
+
signal
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return latestFromDistTags(await response.json());
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function distTagsUrl(packageName, registryUrl = DEFAULT_REGISTRY_URL) {
|
|
65
|
+
return `${registryUrl.replace(/\/+$/, "")}/-/package/${encodeURIComponent(packageName)}/dist-tags`;
|
|
66
|
+
}
|
|
67
|
+
export function isNewerVersion(candidate, current) {
|
|
68
|
+
return compareSemver(candidate, current) > 0;
|
|
69
|
+
}
|
|
70
|
+
function shouldRunUpdateCheck(opts) {
|
|
71
|
+
if (opts.stderr.isTTY !== true)
|
|
72
|
+
return false;
|
|
73
|
+
if (isTruthyCi(opts.env.CI) || isTruthyCi(opts.env.CONTINUOUS_INTEGRATION))
|
|
74
|
+
return false;
|
|
75
|
+
if (opts.env.NODE_ENV === "test")
|
|
76
|
+
return false;
|
|
77
|
+
if ("NO_UPDATE_NOTIFIER" in opts.env)
|
|
78
|
+
return false;
|
|
79
|
+
if (hasArg(opts.argv, "--no-update-notifier"))
|
|
80
|
+
return false;
|
|
81
|
+
if (hasArg(opts.argv, "--version") || hasArg(opts.argv, "-V") || hasArg(opts.argv, "--help") || hasArg(opts.argv, "-h"))
|
|
82
|
+
return false;
|
|
83
|
+
if (hasArg(opts.argv, "--json"))
|
|
84
|
+
return false;
|
|
85
|
+
if (opts.argv.slice(2).includes("daemon"))
|
|
86
|
+
return false;
|
|
87
|
+
const override = parseBooleanOverride(opts.env.COPILLM_UPDATE_CHECK);
|
|
88
|
+
if (override !== null) {
|
|
89
|
+
return override;
|
|
90
|
+
}
|
|
91
|
+
return isNpmInstalledRuntime(opts.moduleUrl, opts.packageInfo.name);
|
|
92
|
+
}
|
|
93
|
+
function isNpmInstalledRuntime(moduleUrl, packageName) {
|
|
94
|
+
let modulePath;
|
|
95
|
+
try {
|
|
96
|
+
modulePath = fileURLToPath(moduleUrl);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const normalized = modulePath.split(path.sep).join("/").toLowerCase();
|
|
102
|
+
const marker = `/node_modules/${packageName.toLowerCase()}/`;
|
|
103
|
+
return normalized.includes(marker);
|
|
104
|
+
}
|
|
105
|
+
function updateCachePath() {
|
|
106
|
+
return path.join(getCopillmHome(), "update-check.json");
|
|
107
|
+
}
|
|
108
|
+
function readUpdateCache(filePath, packageName) {
|
|
109
|
+
if (!fs.existsSync(filePath)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
114
|
+
return parseUpdateCache(parsed, packageName);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function writeUpdateCache(filePath, cache) {
|
|
121
|
+
try {
|
|
122
|
+
writeFileSecureAtomic(filePath, `${JSON.stringify(cache, null, 2)}\n`, 0o600);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Update checks are advisory and must never prevent the CLI from starting.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function parseUpdateCache(value, packageName) {
|
|
129
|
+
if (!value || typeof value !== "object") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const candidate = value;
|
|
133
|
+
if (candidate.version !== 1 || candidate.packageName !== packageName) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (candidate.latestVersion !== null && typeof candidate.latestVersion !== "string") {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (typeof candidate.checkedAt !== "number" || !Number.isFinite(candidate.checkedAt)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
version: 1,
|
|
144
|
+
packageName,
|
|
145
|
+
latestVersion: candidate.latestVersion,
|
|
146
|
+
checkedAt: candidate.checkedAt
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function latestFromDistTags(value) {
|
|
150
|
+
if (!value || typeof value !== "object") {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const latest = value.latest;
|
|
154
|
+
return typeof latest === "string" && latest.trim().length > 0 ? latest.trim() : null;
|
|
155
|
+
}
|
|
156
|
+
function notifyIfNewer(stderr, packageInfo, latestVersion) {
|
|
157
|
+
if (!latestVersion || !isNewerVersion(latestVersion, packageInfo.version)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
stderr.write([
|
|
161
|
+
"",
|
|
162
|
+
`copillm ${latestVersion} is available (current ${packageInfo.version}).`,
|
|
163
|
+
"Update with: npm install -g copillm",
|
|
164
|
+
"Release notes: https://github.com/jcjc-dev/copillm/releases/latest",
|
|
165
|
+
""
|
|
166
|
+
].join("\n"));
|
|
167
|
+
}
|
|
168
|
+
function hasArg(argv, arg) {
|
|
169
|
+
return argv.slice(2).includes(arg);
|
|
170
|
+
}
|
|
171
|
+
function parseBooleanOverride(value) {
|
|
172
|
+
if (value === undefined) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const normalized = value.trim().toLowerCase();
|
|
176
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function isTruthyCi(value) {
|
|
185
|
+
if (value === undefined) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const normalized = value.trim().toLowerCase();
|
|
189
|
+
return normalized !== "" && normalized !== "0" && normalized !== "false";
|
|
190
|
+
}
|
|
191
|
+
function compareSemver(left, right) {
|
|
192
|
+
const parsedLeft = parseSemver(left);
|
|
193
|
+
const parsedRight = parseSemver(right);
|
|
194
|
+
if (!parsedLeft || !parsedRight) {
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
for (let i = 0; i < 3; i += 1) {
|
|
198
|
+
const delta = parsedLeft.core[i] - parsedRight.core[i];
|
|
199
|
+
if (delta !== 0) {
|
|
200
|
+
return delta;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (parsedLeft.prerelease === parsedRight.prerelease) {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
if (parsedLeft.prerelease === null) {
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
if (parsedRight.prerelease === null) {
|
|
210
|
+
return -1;
|
|
211
|
+
}
|
|
212
|
+
return parsedLeft.prerelease.localeCompare(parsedRight.prerelease);
|
|
213
|
+
}
|
|
214
|
+
function parseSemver(value) {
|
|
215
|
+
const match = value.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
216
|
+
if (!match) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
221
|
+
prerelease: match[4] ?? null
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -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
|
+
}
|