copillm 0.2.6 → 0.2.8
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/agentconfig/render.js +57 -8
- package/dist/cli/packageInfo.js +1 -1
- package/dist/cli/resolveAgent.js +68 -30
- package/package.json +1 -1
|
@@ -384,18 +384,67 @@ export default function activate(pi: PiApi): void {
|
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
`;
|
|
387
|
-
// ─── Copilot CLI
|
|
388
|
-
export function renderCopilot(
|
|
387
|
+
// ─── Copilot CLI ──────────────────────────────────────────────────────────
|
|
388
|
+
export function renderCopilot(input) {
|
|
389
|
+
const writes = [];
|
|
390
|
+
const notes = [];
|
|
391
|
+
const cliArgs = [];
|
|
392
|
+
const mcpConfigPath = path.join(getCopillmHome(), "copilot", "mcp-config.json");
|
|
393
|
+
const serverCount = Object.keys(input.resolved.mcpServers).length;
|
|
394
|
+
if (serverCount > 0) {
|
|
395
|
+
const content = renderCopilotMcp(input.resolved.mcpServers);
|
|
396
|
+
const existing = fs.existsSync(mcpConfigPath) ? fs.readFileSync(mcpConfigPath, "utf8") : null;
|
|
397
|
+
if (existing !== content) {
|
|
398
|
+
writes.push({
|
|
399
|
+
path: mcpConfigPath,
|
|
400
|
+
content,
|
|
401
|
+
mode: 0o600,
|
|
402
|
+
description: "Copilot CLI MCP config (copillm-managed)"
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
cliArgs.push("--additional-mcp-config", `@${mcpConfigPath}`);
|
|
406
|
+
}
|
|
407
|
+
else if (fs.existsSync(mcpConfigPath)) {
|
|
408
|
+
fs.rmSync(mcpConfigPath, { force: true });
|
|
409
|
+
notes.push(`Removed stale ${mcpConfigPath} (no MCP servers in active profile).`);
|
|
410
|
+
}
|
|
389
411
|
return {
|
|
390
|
-
writes
|
|
412
|
+
writes,
|
|
391
413
|
envOverlay: {},
|
|
392
|
-
cliArgs
|
|
393
|
-
notes
|
|
394
|
-
"Copilot CLI: native MCP config format is not yet documented publicly. " +
|
|
395
|
-
"Skipping fan-out. Track upstream and remove this stub when the path is known."
|
|
396
|
-
]
|
|
414
|
+
cliArgs,
|
|
415
|
+
notes
|
|
397
416
|
};
|
|
398
417
|
}
|
|
418
|
+
function renderCopilotMcp(servers) {
|
|
419
|
+
const out = {};
|
|
420
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
421
|
+
if (server.transport === "stdio") {
|
|
422
|
+
const entry = {
|
|
423
|
+
type: "local",
|
|
424
|
+
command: server.command,
|
|
425
|
+
tools: ["*"]
|
|
426
|
+
};
|
|
427
|
+
if (server.args)
|
|
428
|
+
entry.args = server.args;
|
|
429
|
+
if (server.env)
|
|
430
|
+
entry.env = server.env;
|
|
431
|
+
if (server.cwd)
|
|
432
|
+
entry.cwd = server.cwd;
|
|
433
|
+
out[name] = entry;
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
const entry = {
|
|
437
|
+
type: server.transport,
|
|
438
|
+
url: server.url,
|
|
439
|
+
tools: ["*"]
|
|
440
|
+
};
|
|
441
|
+
if (server.headers)
|
|
442
|
+
entry.headers = server.headers;
|
|
443
|
+
out[name] = entry;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return `${JSON.stringify({ mcpServers: out }, null, 2)}\n`;
|
|
447
|
+
}
|
|
399
448
|
export function planRender(opts, load) {
|
|
400
449
|
const baseInput = { resolved: load.resolved, cwd: opts.cwd };
|
|
401
450
|
switch (opts.agent) {
|
package/dist/cli/packageInfo.js
CHANGED
package/dist/cli/resolveAgent.js
CHANGED
|
@@ -59,16 +59,25 @@ export async function resolveAgent(agent, opts = {}) {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
// 2. Determine target version
|
|
62
|
+
// 2. Determine target version. If we can reach npm we ask for `latest`;
|
|
63
|
+
// otherwise we fall through to whatever's already cached so the user can
|
|
64
|
+
// keep working when the registry is unreachable (corp proxy, npm outage,
|
|
65
|
+
// airplane mode, etc.).
|
|
63
66
|
let target = pin.version;
|
|
67
|
+
let viewError = null;
|
|
64
68
|
if (!target && !opts.offline) {
|
|
65
|
-
|
|
69
|
+
try {
|
|
70
|
+
target = npmViewLatest(npmExe, pkg);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
viewError = err instanceof Error ? err : new Error(String(err));
|
|
74
|
+
}
|
|
66
75
|
}
|
|
67
76
|
// 3. Cache lookup
|
|
68
77
|
if (target) {
|
|
69
78
|
const cachedDir = path.join(agentRoot, target);
|
|
70
|
-
const cachedBin =
|
|
71
|
-
if (cachedBin
|
|
79
|
+
const cachedBin = findReadyCachedBin(cachedDir, binName);
|
|
80
|
+
if (cachedBin) {
|
|
72
81
|
return {
|
|
73
82
|
source: "cache",
|
|
74
83
|
binPath: cachedBin,
|
|
@@ -81,8 +90,13 @@ export async function resolveAgent(agent, opts = {}) {
|
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
else {
|
|
93
|
+
// Either --offline or we couldn't reach npm to ask "what's latest?".
|
|
94
|
+
// Use the newest known-good install on disk.
|
|
84
95
|
const last = pickLastCached(agentRoot, binName);
|
|
85
96
|
if (last) {
|
|
97
|
+
if (viewError) {
|
|
98
|
+
log(`\u26a0 could not reach npm registry to check for updates (${viewError.message}); using cached ${binName} v${last.version}`);
|
|
99
|
+
}
|
|
86
100
|
return {
|
|
87
101
|
source: "cache",
|
|
88
102
|
binPath: last.binPath,
|
|
@@ -93,12 +107,22 @@ export async function resolveAgent(agent, opts = {}) {
|
|
|
93
107
|
displayLine: `\u2192 ${binName} (cached fallback, ${displayPath(last.dir)}, v${last.version})`
|
|
94
108
|
};
|
|
95
109
|
}
|
|
110
|
+
if (viewError) {
|
|
111
|
+
throw new Error(`${binName} not installed and could not reach npm registry to download it: ${viewError.message}`);
|
|
112
|
+
}
|
|
96
113
|
throw new Error(`${binName} not installed and no cache available (offline).`);
|
|
97
114
|
}
|
|
98
115
|
if (opts.offline) {
|
|
99
116
|
throw new Error(`${binName}@${target} not in cache and --offline is set.`);
|
|
100
117
|
}
|
|
101
|
-
// 4. Install
|
|
118
|
+
// 4. Install. We install *directly* into the canonical version directory
|
|
119
|
+
// and write `version.txt` LAST as the "install complete" marker.
|
|
120
|
+
// findReadyCachedBin requires both the bin and the marker, so any crash
|
|
121
|
+
// before the marker is written leaves the tree visible as incomplete and
|
|
122
|
+
// the next run cleans it up + re-installs. Avoids the older staging+rename
|
|
123
|
+
// pattern, which had to retry rename-of-directory on Windows when AV or
|
|
124
|
+
// npm post-install workers transiently held handles on freshly-written
|
|
125
|
+
// files.
|
|
102
126
|
log(`\u2192 ${binName} (installing ${pkg}@${target} into ${displayPath(agentRoot)} \u2026)`);
|
|
103
127
|
fs.mkdirSync(agentRoot, { recursive: true });
|
|
104
128
|
const lockFile = path.join(agentRoot, ".lock");
|
|
@@ -106,8 +130,8 @@ export async function resolveAgent(agent, opts = {}) {
|
|
|
106
130
|
try {
|
|
107
131
|
// Re-check after acquiring lock — another invocation may have just installed it.
|
|
108
132
|
const finalDir = path.join(agentRoot, target);
|
|
109
|
-
const recheckBin =
|
|
110
|
-
if (recheckBin
|
|
133
|
+
const recheckBin = findReadyCachedBin(finalDir, binName);
|
|
134
|
+
if (recheckBin) {
|
|
111
135
|
return {
|
|
112
136
|
source: "cache",
|
|
113
137
|
binPath: recheckBin,
|
|
@@ -118,40 +142,37 @@ export async function resolveAgent(agent, opts = {}) {
|
|
|
118
142
|
displayLine: `\u2192 ${binName} (cached, ${displayPath(finalDir)}, v${target})`
|
|
119
143
|
};
|
|
120
144
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
// Wipe any partial state (missing marker means a previous attempt was
|
|
146
|
+
// interrupted) before we re-run npm into the same prefix.
|
|
147
|
+
if (fs.existsSync(finalDir)) {
|
|
148
|
+
fs.rmSync(finalDir, { recursive: true, force: true });
|
|
124
149
|
}
|
|
125
|
-
fs.mkdirSync(
|
|
150
|
+
fs.mkdirSync(finalDir, { recursive: true });
|
|
126
151
|
const spec = `${pkg}@${target}`;
|
|
127
|
-
const installResult = spawnSync(npmExe, ["install", "--prefix",
|
|
152
|
+
const installResult = spawnSync(npmExe, ["install", "--prefix", finalDir, "--no-audit", "--no-fund", "--omit=dev", spec], {
|
|
128
153
|
stdio: ["ignore", "inherit", "inherit"],
|
|
129
154
|
shell: process.platform === "win32"
|
|
130
155
|
});
|
|
131
156
|
if (installResult.status !== 0) {
|
|
157
|
+
cleanupFailedInstall(finalDir);
|
|
132
158
|
const msg = installResult.error ? `: ${installResult.error.message}` : "";
|
|
133
159
|
throw new Error(`npm install ${spec} failed (exit ${installResult.status})${msg}`);
|
|
134
160
|
}
|
|
135
|
-
const
|
|
136
|
-
if (!
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (probeVersion(stagedBin) === null) {
|
|
140
|
-
throw new Error(`Smoke test failed: ${stagedBin} --version did not exit 0`);
|
|
161
|
+
const installedBin = binPathInPrefix(finalDir, binName);
|
|
162
|
+
if (!installedBin || !fs.existsSync(installedBin)) {
|
|
163
|
+
cleanupFailedInstall(finalDir);
|
|
164
|
+
throw new Error(`Installed package did not produce a ${binName} bin at ${finalDir}`);
|
|
141
165
|
}
|
|
142
|
-
if (
|
|
143
|
-
|
|
166
|
+
if (probeVersion(installedBin) === null) {
|
|
167
|
+
cleanupFailedInstall(finalDir);
|
|
168
|
+
throw new Error(`Smoke test failed: ${installedBin} --version did not exit 0`);
|
|
144
169
|
}
|
|
145
|
-
|
|
170
|
+
// Marker file: MUST be the last write. Cache-hit checks key off this.
|
|
146
171
|
fs.writeFileSync(path.join(finalDir, "version.txt"), `${target}\n`);
|
|
147
172
|
const pruned = pruneSiblings(agentRoot, target);
|
|
148
|
-
const finalBin = binPathInPrefix(finalDir, binName);
|
|
149
|
-
if (!finalBin) {
|
|
150
|
-
throw new Error(`Final install missing bin at ${finalDir}`);
|
|
151
|
-
}
|
|
152
173
|
return {
|
|
153
174
|
source: "installed",
|
|
154
|
-
binPath:
|
|
175
|
+
binPath: installedBin,
|
|
155
176
|
version: target,
|
|
156
177
|
packageName: pkg,
|
|
157
178
|
cacheDir: finalDir,
|
|
@@ -163,6 +184,26 @@ export async function resolveAgent(agent, opts = {}) {
|
|
|
163
184
|
releaseFileLock(lockFile);
|
|
164
185
|
}
|
|
165
186
|
}
|
|
187
|
+
function cleanupFailedInstall(dir) {
|
|
188
|
+
try {
|
|
189
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Best-effort: a stuck handle here means the next run will retry the
|
|
193
|
+
// cleanup before reinstalling. Worst case the user gets a clearer
|
|
194
|
+
// "rmSync failed" error on the next attempt.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function findReadyCachedBin(dir, binName) {
|
|
198
|
+
const bin = binPathInPrefix(dir, binName);
|
|
199
|
+
if (!bin || !fs.existsSync(bin))
|
|
200
|
+
return null;
|
|
201
|
+
// version.txt is written LAST, after the smoke test passes. Missing
|
|
202
|
+
// marker = partial/aborted install; do not treat as a cache hit.
|
|
203
|
+
if (!fs.existsSync(path.join(dir, "version.txt")))
|
|
204
|
+
return null;
|
|
205
|
+
return bin;
|
|
206
|
+
}
|
|
166
207
|
function defaultNpmExecutable() {
|
|
167
208
|
return process.env.COPILLM_NPM_EXECUTABLE && process.env.COPILLM_NPM_EXECUTABLE.trim().length > 0
|
|
168
209
|
? process.env.COPILLM_NPM_EXECUTABLE
|
|
@@ -245,7 +286,7 @@ function pickLastCached(agentRoot, binName) {
|
|
|
245
286
|
.sort((a, b) => compareVersionsDescending(a, b));
|
|
246
287
|
for (const v of versions) {
|
|
247
288
|
const dir = path.join(agentRoot, v);
|
|
248
|
-
const bin =
|
|
289
|
+
const bin = findReadyCachedBin(dir, binName);
|
|
249
290
|
if (bin)
|
|
250
291
|
return { dir, binPath: bin, version: v };
|
|
251
292
|
}
|
|
@@ -351,6 +392,3 @@ function displayPath(p) {
|
|
|
351
392
|
}
|
|
352
393
|
return p;
|
|
353
394
|
}
|
|
354
|
-
function sanitize(s) {
|
|
355
|
-
return s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
356
|
-
}
|