@hashicorp/kits 0.1.3 → 0.1.5
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/README.md +175 -34
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +29 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/install.js +820 -161
- package/dist/cli/install.js.map +1 -1
- package/dist/cli/list.d.ts.map +1 -1
- package/dist/cli/list.js +8 -6
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/types.d.ts +17 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js +1 -0
- package/dist/cli/types.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +300 -38
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/upgrade.d.ts.map +1 -1
- package/dist/cli/upgrade.js +334 -105
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli/validate.js +5 -5
- package/dist/cli/validate.js.map +1 -1
- package/dist/core/types.d.ts +4 -4
- package/dist/core/upgrade-executor.d.ts +7 -7
- package/dist/core/upgrade-executor.d.ts.map +1 -1
- package/dist/core/upgrade-executor.js +17 -17
- package/dist/core/upgrade-executor.js.map +1 -1
- package/dist/discovery/kit-scanner.d.ts +3 -3
- package/dist/discovery/kit-scanner.js +10 -10
- package/dist/discovery/kit-scanner.js.map +1 -1
- package/dist/discovery/types.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lockfile/index.d.ts +79 -0
- package/dist/lockfile/index.d.ts.map +1 -0
- package/dist/lockfile/index.js +203 -0
- package/dist/lockfile/index.js.map +1 -0
- package/dist/lockfile/read.d.ts +32 -0
- package/dist/lockfile/read.d.ts.map +1 -0
- package/dist/lockfile/read.js +88 -0
- package/dist/lockfile/read.js.map +1 -0
- package/dist/lockfile/types.d.ts +126 -0
- package/dist/lockfile/types.d.ts.map +1 -0
- package/dist/lockfile/types.js +44 -0
- package/dist/lockfile/types.js.map +1 -0
- package/dist/{manifest → lockfile}/upgrade-check.d.ts +5 -5
- package/dist/lockfile/upgrade-check.d.ts.map +1 -0
- package/dist/{manifest → lockfile}/upgrade-check.js +11 -9
- package/dist/lockfile/upgrade-check.js.map +1 -0
- package/dist/lockfile/utils.d.ts +35 -0
- package/dist/lockfile/utils.d.ts.map +1 -0
- package/dist/lockfile/utils.js +57 -0
- package/dist/lockfile/utils.js.map +1 -0
- package/dist/lockfile/write.d.ts +44 -0
- package/dist/lockfile/write.d.ts.map +1 -0
- package/dist/lockfile/write.js +77 -0
- package/dist/lockfile/write.js.map +1 -0
- package/dist/manifest/hash.d.ts +7 -0
- package/dist/manifest/hash.d.ts.map +1 -0
- package/dist/manifest/hash.js +30 -0
- package/dist/manifest/hash.js.map +1 -0
- package/dist/manifest/index.d.ts +8 -77
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +6 -197
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/read.d.ts +4 -24
- package/dist/manifest/read.d.ts.map +1 -1
- package/dist/manifest/read.js +22 -46
- package/dist/manifest/read.js.map +1 -1
- package/dist/manifest/types.d.ts +17 -109
- package/dist/manifest/types.d.ts.map +1 -1
- package/dist/manifest/types.js +8 -37
- package/dist/manifest/types.js.map +1 -1
- package/dist/manifest/utils.d.ts +1 -27
- package/dist/manifest/utils.d.ts.map +1 -1
- package/dist/manifest/utils.js +1 -28
- package/dist/manifest/utils.js.map +1 -1
- package/dist/manifest/write.d.ts +3 -34
- package/dist/manifest/write.d.ts.map +1 -1
- package/dist/manifest/write.js +5 -45
- package/dist/manifest/write.js.map +1 -1
- package/dist/resolution/index.d.ts +1 -1
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +23 -13
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/primitives-registry.d.ts +1 -1
- package/dist/resolution/primitives-registry.js +2 -2
- package/dist/resolution/primitives-registry.js.map +1 -1
- package/dist/tui/kit-select.d.ts.map +1 -1
- package/dist/tui/kit-select.js +4 -5
- package/dist/tui/kit-select.js.map +1 -1
- package/dist/tui/types.d.ts +1 -0
- package/dist/tui/types.d.ts.map +1 -1
- package/dist/tui/upgrade-select.d.ts +1 -1
- package/dist/tui/upgrade-select.d.ts.map +1 -1
- package/dist/tui/upgrade-select.js +15 -4
- package/dist/tui/upgrade-select.js.map +1 -1
- package/dist/validation/validate-kits.d.ts +2 -2
- package/dist/validation/validate-kits.js +4 -4
- package/dist/validation/validate-kits.js.map +1 -1
- package/package.json +1 -1
- package/schemas/kit.schema.json +2 -2
- package/schemas/{manifest.schema.json → kits-lock.schema.json} +34 -8
- package/schemas/kits-manifest.schema.json +95 -0
- package/schemas/{kits.schema.json → kits-registry.schema.json} +3 -3
- package/schemas/{primitives.schema.json → primitives-registry.schema.json} +2 -2
- package/dist/manifest/upgrade-check.d.ts.map +0 -1
- package/dist/manifest/upgrade-check.js.map +0 -1
package/dist/cli/install.js
CHANGED
|
@@ -6,18 +6,21 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { ExitCode } from "./types.js";
|
|
8
8
|
import * as clack from "@clack/prompts";
|
|
9
|
+
import fs from "node:fs/promises";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import pc from "picocolors";
|
|
11
12
|
import { fetchSource, scanKits, filterKitsByName as filterDiscoveryKitsByName, getMissingKitNames as getDiscoveryMissingKitNames, NoFetcherError, SourceParseError, } from "../discovery/index.js";
|
|
12
13
|
import { resolvePrimitiveReferences, PrimitivesRegistryLoader, validateCliEnvFlags, mergeEnvDefs, resolveEnvVarsFromConfig, } from "../resolution/index.js";
|
|
13
14
|
import { hashMcpConfig } from "../core/mcp-instance.js";
|
|
14
15
|
import { hashHookConfig } from "../core/hook-instance.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { readLockfile } from "../lockfile/read.js";
|
|
17
|
+
import { writeLockfile } from "../lockfile/write.js";
|
|
18
|
+
import { createEmptyLockfile, } from "../lockfile/types.js";
|
|
19
|
+
import { readManifestFile } from "../manifest/read.js";
|
|
20
|
+
import { hashManifest } from "../manifest/hash.js";
|
|
18
21
|
import { getAdapterRegistry } from "../adapters/index.js";
|
|
19
22
|
import { debugLog, enableDebugLogging, getDebugLogPath, checkDebugLogWritable, } from "../core/debug.js";
|
|
20
|
-
import { expandPath } from "../adapters/file-operations.js";
|
|
23
|
+
import { expandPath, remove } from "../adapters/file-operations.js";
|
|
21
24
|
import {
|
|
22
25
|
// TUI components
|
|
23
26
|
intro, outroSuccess, outroError, outroCancel, logInfo, logError, confirmInstallPlan, displayInstallPlanSummary,
|
|
@@ -28,7 +31,7 @@ selectHarnesses, warnMissingHarnesses, displayNoHarnessesDetected, filterHarness
|
|
|
28
31
|
// Kit selection
|
|
29
32
|
selectKits, displayAvailableKits, displayKitsJson, displayNoKitsFound, displayMissingKits, filterKitsByName,
|
|
30
33
|
// Compatibility
|
|
31
|
-
runCompatibilityCheck, buildCompatibilityMatrix, hasIncompatibilities,
|
|
34
|
+
runCompatibilityCheck, buildCompatibilityMatrix, hasIncompatibilities, getCompatiblePairs,
|
|
32
35
|
// Scope selection
|
|
33
36
|
selectScope,
|
|
34
37
|
// Summary
|
|
@@ -77,12 +80,88 @@ function getScopeFromOptions(options) {
|
|
|
77
80
|
}
|
|
78
81
|
return null;
|
|
79
82
|
}
|
|
83
|
+
async function fileExists(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
await fs.access(filePath);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function deriveProjectRootFromManifestPath(manifestPath) {
|
|
93
|
+
const resolved = path.resolve(manifestPath);
|
|
94
|
+
const parentDir = path.basename(path.dirname(resolved));
|
|
95
|
+
if (parentDir === ".agents") {
|
|
96
|
+
return { projectRoot: path.dirname(path.dirname(resolved)) };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
projectRoot: path.dirname(resolved),
|
|
100
|
+
warning: "Project-scope kits.json is not under a .agents/ directory; using the manifest's parent directory as the project root.",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function resolveManifestTarget(source) {
|
|
104
|
+
const expanded = expandPath(source);
|
|
105
|
+
try {
|
|
106
|
+
const stat = await fs.stat(expanded);
|
|
107
|
+
if (stat.isFile()) {
|
|
108
|
+
if (path.basename(expanded) !== "kits.json") {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const manifest = await readManifestFile(expanded);
|
|
112
|
+
return { manifestPath: expanded, manifest };
|
|
113
|
+
}
|
|
114
|
+
if (stat.isDirectory()) {
|
|
115
|
+
const nestedPath = path.join(expanded, ".agents", "kits.json");
|
|
116
|
+
if (await fileExists(nestedPath)) {
|
|
117
|
+
const manifest = await readManifestFile(nestedPath);
|
|
118
|
+
return { manifestPath: nestedPath, manifest };
|
|
119
|
+
}
|
|
120
|
+
const directPath = path.join(expanded, "kits.json");
|
|
121
|
+
if (await fileExists(directPath)) {
|
|
122
|
+
const manifest = await readManifestFile(directPath);
|
|
123
|
+
return { manifestPath: directPath, manifest };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Not a local path; treat as repository source.
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
function buildLockedVersionMap(lockfile) {
|
|
133
|
+
const locked = new Map();
|
|
134
|
+
for (const harnessEntry of Object.values(lockfile.harnesses)) {
|
|
135
|
+
if (!harnessEntry)
|
|
136
|
+
continue;
|
|
137
|
+
for (const kit of Object.values(harnessEntry.kits)) {
|
|
138
|
+
for (const primitive of kit.primitives) {
|
|
139
|
+
if (!primitive.version)
|
|
140
|
+
continue;
|
|
141
|
+
const key = `${primitive.type}:${primitive.name}`;
|
|
142
|
+
if (!locked.has(key)) {
|
|
143
|
+
locked.set(key, primitive.version);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return locked;
|
|
149
|
+
}
|
|
150
|
+
function mergeMcpInstanceOverrides(base, overrides) {
|
|
151
|
+
const merged = { ...(base ?? {}) };
|
|
152
|
+
if (overrides) {
|
|
153
|
+
for (const [kitName, kitOverrides] of Object.entries(overrides)) {
|
|
154
|
+
merged[kitName] = { ...(merged[kitName] ?? {}), ...kitOverrides };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return merged;
|
|
158
|
+
}
|
|
80
159
|
/**
|
|
81
160
|
* Run the install command.
|
|
82
161
|
*/
|
|
83
162
|
export async function runInstall(source, options) {
|
|
84
163
|
const isInteractive = !options.yes && !options.json;
|
|
85
|
-
|
|
164
|
+
const fetchResults = [];
|
|
86
165
|
enableDebugLogging(options.debug ?? false);
|
|
87
166
|
if (options.debug) {
|
|
88
167
|
const debugCheck = checkDebugLogWritable();
|
|
@@ -104,11 +183,13 @@ export async function runInstall(source, options) {
|
|
|
104
183
|
options: {
|
|
105
184
|
agent: options.agent,
|
|
106
185
|
kit: options.kit,
|
|
186
|
+
alias: options.alias,
|
|
107
187
|
list: options.list ?? false,
|
|
108
188
|
yes: options.yes ?? false,
|
|
109
189
|
dryRun: options.dryRun ?? false,
|
|
110
190
|
json: options.json ?? false,
|
|
111
191
|
verbose: options.verbose ?? false,
|
|
192
|
+
overwrite: options.overwrite ?? false,
|
|
112
193
|
global: options.global ?? false,
|
|
113
194
|
project: options.project ?? false,
|
|
114
195
|
envKeys: options.env ? Object.keys(options.env) : [],
|
|
@@ -116,12 +197,82 @@ export async function runInstall(source, options) {
|
|
|
116
197
|
},
|
|
117
198
|
},
|
|
118
199
|
});
|
|
200
|
+
let manifestTarget = null;
|
|
201
|
+
try {
|
|
202
|
+
manifestTarget = await resolveManifestTarget(source);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
206
|
+
if (options.json) {
|
|
207
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.error(`Error: ${message}`);
|
|
211
|
+
}
|
|
212
|
+
return { success: false, exitCode: ExitCode.ManifestError, error: message };
|
|
213
|
+
}
|
|
214
|
+
const manifestInstall = !!manifestTarget;
|
|
215
|
+
if (options.ci && options.ignoreLock) {
|
|
216
|
+
const message = "--ci and --ignore-lock cannot be used together.";
|
|
217
|
+
if (options.json) {
|
|
218
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.error(`Error: ${message}`);
|
|
222
|
+
}
|
|
223
|
+
return { success: false, exitCode: ExitCode.InvalidArguments, error: message };
|
|
224
|
+
}
|
|
225
|
+
if (options.alias && manifestInstall) {
|
|
226
|
+
const message = "--alias cannot be used with kits.json installs. Set installAs in the manifest instead.";
|
|
227
|
+
if (options.json) {
|
|
228
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
console.error(`Error: ${message}`);
|
|
232
|
+
}
|
|
233
|
+
return { success: false, exitCode: ExitCode.InvalidArguments, error: message };
|
|
234
|
+
}
|
|
235
|
+
if (options.ci && !manifestInstall) {
|
|
236
|
+
const message = "--ci is only valid for manifest-driven installs (kits.json).";
|
|
237
|
+
if (options.json) {
|
|
238
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.error(`Error: ${message}`);
|
|
242
|
+
}
|
|
243
|
+
return { success: false, exitCode: ExitCode.InvalidArguments, error: message };
|
|
244
|
+
}
|
|
245
|
+
if (options.ignoreLock && !manifestInstall) {
|
|
246
|
+
const message = "--ignore-lock is only valid for manifest-driven installs (kits.json).";
|
|
247
|
+
if (options.json) {
|
|
248
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
console.error(`Error: ${message}`);
|
|
252
|
+
}
|
|
253
|
+
return { success: false, exitCode: ExitCode.InvalidArguments, error: message };
|
|
254
|
+
}
|
|
119
255
|
// Parse scope from options
|
|
120
256
|
let scope = getScopeFromOptions(options);
|
|
121
|
-
|
|
257
|
+
let projectRoot = process.cwd();
|
|
258
|
+
let manifest = null;
|
|
259
|
+
let manifestPath = null;
|
|
260
|
+
let manifestHash = null;
|
|
261
|
+
let manifestWarning;
|
|
262
|
+
if (manifestInstall && manifestTarget) {
|
|
263
|
+
manifest = manifestTarget.manifest;
|
|
264
|
+
manifestPath = manifestTarget.manifestPath;
|
|
265
|
+
scope = manifest.scope;
|
|
266
|
+
if (scope === "project") {
|
|
267
|
+
const derived = deriveProjectRootFromManifestPath(manifestPath);
|
|
268
|
+
projectRoot = derived.projectRoot;
|
|
269
|
+
manifestWarning = derived.warning;
|
|
270
|
+
}
|
|
271
|
+
manifestHash = hashManifest(manifest);
|
|
272
|
+
}
|
|
122
273
|
// In non-interactive mode, scope must be specified
|
|
123
274
|
// Exception: --list flag doesn't require scope
|
|
124
|
-
if (!isInteractive && !scope && !options.list) {
|
|
275
|
+
if (!manifestInstall && !isInteractive && !scope && !options.list) {
|
|
125
276
|
const message = "Scope is required in non-interactive mode. Use --global (-g) or --project (-p).";
|
|
126
277
|
if (options.json) {
|
|
127
278
|
console.log(JSON.stringify({ success: false, error: message }));
|
|
@@ -136,91 +287,222 @@ export async function runInstall(source, options) {
|
|
|
136
287
|
if (isInteractive) {
|
|
137
288
|
intro("@hashicorp/kits");
|
|
138
289
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
290
|
+
if (manifestInstall && (options.global || options.project)) {
|
|
291
|
+
const message = "Ignoring --global/--project flags because kits.json defines scope.";
|
|
292
|
+
if (isInteractive) {
|
|
293
|
+
logInfo(message);
|
|
294
|
+
}
|
|
295
|
+
else if (options.verbose) {
|
|
296
|
+
console.error(`Warning: ${message}`);
|
|
297
|
+
}
|
|
145
298
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
299
|
+
if (manifestInstall && options.kit && options.kit.length > 0) {
|
|
300
|
+
const message = "Ignoring --kit flags because kits.json defines kits to install.";
|
|
301
|
+
if (isInteractive) {
|
|
302
|
+
logInfo(message);
|
|
150
303
|
}
|
|
151
304
|
else if (options.verbose) {
|
|
152
|
-
console.error(`
|
|
305
|
+
console.error(`Warning: ${message}`);
|
|
153
306
|
}
|
|
154
|
-
void debugLog({
|
|
155
|
-
level: "info",
|
|
156
|
-
event: "install.fetch.success",
|
|
157
|
-
data: { localPath: fetchResult.localPath },
|
|
158
|
-
});
|
|
159
307
|
}
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
308
|
+
if (manifestWarning) {
|
|
309
|
+
if (isInteractive) {
|
|
310
|
+
logInfo(manifestWarning);
|
|
163
311
|
}
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
312
|
+
else if (options.verbose) {
|
|
313
|
+
console.error(`Warning: ${manifestWarning}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const sourceDiscoveries = [];
|
|
317
|
+
const availableKits = [];
|
|
318
|
+
const registryBySource = new Map();
|
|
319
|
+
let selectedKits = [];
|
|
320
|
+
const sourcesToFetch = manifestInstall ? (manifest?.sources ?? []) : [{ source }];
|
|
321
|
+
if (manifestInstall && sourcesToFetch.length === 0) {
|
|
322
|
+
const message = "kits.json must include at least one source.";
|
|
323
|
+
if (isInteractive) {
|
|
324
|
+
logError(message);
|
|
325
|
+
outroError("Installation failed");
|
|
326
|
+
}
|
|
327
|
+
else if (options.json) {
|
|
328
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
console.error(`Error: ${message}`);
|
|
332
|
+
}
|
|
333
|
+
return { success: false, exitCode: ExitCode.ManifestError, error: message };
|
|
334
|
+
}
|
|
335
|
+
for (const sourceEntry of sourcesToFetch) {
|
|
336
|
+
const sourceValue = "source" in sourceEntry ? sourceEntry.source : sourceEntry;
|
|
337
|
+
const fetchSpinner = isInteractive
|
|
338
|
+
? startSpinner(`Fetching ${formatSource(sourceValue)}...`)
|
|
339
|
+
: undefined;
|
|
340
|
+
if (!fetchSpinner && options.verbose) {
|
|
341
|
+
console.error(`Fetching ${sourceValue}...`);
|
|
342
|
+
}
|
|
343
|
+
let localFetch;
|
|
344
|
+
try {
|
|
345
|
+
localFetch = await fetchSource(sourceValue);
|
|
346
|
+
fetchResults.push(localFetch);
|
|
347
|
+
if (fetchSpinner) {
|
|
348
|
+
stopSpinnerSuccess(fetchSpinner, `Fetched ${formatSource(sourceValue)}`);
|
|
169
349
|
}
|
|
170
|
-
else if (options.
|
|
171
|
-
console.
|
|
350
|
+
else if (options.verbose) {
|
|
351
|
+
console.error(`Fetched to ${localFetch.localPath}`);
|
|
172
352
|
}
|
|
173
|
-
|
|
174
|
-
|
|
353
|
+
void debugLog({
|
|
354
|
+
level: "info",
|
|
355
|
+
event: "install.fetch.success",
|
|
356
|
+
data: { source: sourceValue, localPath: localFetch.localPath },
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (fetchSpinner) {
|
|
361
|
+
stopSpinnerError(fetchSpinner, "Failed to fetch source");
|
|
362
|
+
}
|
|
363
|
+
if (error instanceof NoFetcherError || error instanceof SourceParseError) {
|
|
364
|
+
const message = error.message;
|
|
365
|
+
if (isInteractive) {
|
|
366
|
+
logError(message);
|
|
367
|
+
outroError("Installation failed");
|
|
368
|
+
}
|
|
369
|
+
else if (options.json) {
|
|
370
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
console.error(`Error: ${message}`);
|
|
374
|
+
}
|
|
375
|
+
return { success: false, exitCode: ExitCode.SourceNotFound, error: message };
|
|
175
376
|
}
|
|
176
|
-
|
|
377
|
+
throw error;
|
|
177
378
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
379
|
+
const scanSpinner = isInteractive
|
|
380
|
+
? startSpinner(`Scanning kits from ${formatSource(sourceValue)}...`)
|
|
381
|
+
: undefined;
|
|
382
|
+
const discovery = await scanKits(localFetch.localPath, sourceValue);
|
|
383
|
+
if (scanSpinner) {
|
|
384
|
+
const kitCount = discovery.kits.length;
|
|
385
|
+
stopSpinnerSuccess(scanSpinner, `Found ${formatCount(kitCount, "kit")}`);
|
|
386
|
+
}
|
|
387
|
+
void debugLog({
|
|
388
|
+
level: "info",
|
|
389
|
+
event: "install.scan.success",
|
|
390
|
+
data: { source: sourceValue, kits: discovery.kits.map((kit) => kit.name) },
|
|
391
|
+
});
|
|
392
|
+
const discoveredKits = discovery.kits.map((kit) => ({
|
|
393
|
+
...kit,
|
|
394
|
+
source: sourceValue,
|
|
395
|
+
localPath: localFetch.localPath,
|
|
396
|
+
}));
|
|
397
|
+
availableKits.push(...discoveredKits);
|
|
398
|
+
let selectedForSource = [];
|
|
399
|
+
if (manifestInstall && "kits" in sourceEntry && sourceEntry.kits && sourceEntry.kits.length > 0) {
|
|
400
|
+
const requestedEntries = sourceEntry.kits.map(normalizeManifestKitEntry);
|
|
401
|
+
const requestedNames = requestedEntries.map((entry) => entry.name);
|
|
402
|
+
const missing = getDiscoveryMissingKitNames(discovery, requestedNames);
|
|
403
|
+
if (missing.length > 0) {
|
|
404
|
+
const message = `Kit${missing.length === 1 ? "" : "s"} not found: ${missing.join(", ")}`;
|
|
405
|
+
if (isInteractive) {
|
|
406
|
+
displayMissingKits(missing, discovery.kits);
|
|
407
|
+
outroError("Installation failed");
|
|
408
|
+
}
|
|
409
|
+
else if (options.json) {
|
|
410
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
console.error(`Error: ${message}`);
|
|
414
|
+
console.error("Available kits: " + discovery.kits.map((k) => k.name).join(", "));
|
|
415
|
+
}
|
|
416
|
+
return { success: false, exitCode: ExitCode.KitNotFound, error: message };
|
|
200
417
|
}
|
|
201
|
-
|
|
202
|
-
|
|
418
|
+
const kitIndex = new Map();
|
|
419
|
+
for (const kit of discovery.kits) {
|
|
420
|
+
kitIndex.set(kit.name.toLowerCase(), kit);
|
|
203
421
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
422
|
+
selectedForSource = requestedEntries.map((entry) => {
|
|
423
|
+
const kit = kitIndex.get(entry.name.toLowerCase());
|
|
424
|
+
if (!kit) {
|
|
425
|
+
throw new Error(`Kit not found: ${entry.name}`);
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
...kit,
|
|
429
|
+
source: sourceValue,
|
|
430
|
+
localPath: localFetch.localPath,
|
|
431
|
+
installAs: entry.installAs ?? kit.name,
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
let kitsForSource = discovery.kits;
|
|
437
|
+
if (!manifestInstall && options.kit && options.kit.length > 0) {
|
|
438
|
+
const missing = getDiscoveryMissingKitNames(discovery, options.kit);
|
|
439
|
+
if (missing.length > 0) {
|
|
440
|
+
const message = `Kit${missing.length === 1 ? "" : "s"} not found: ${missing.join(", ")}`;
|
|
441
|
+
if (isInteractive) {
|
|
442
|
+
displayMissingKits(missing, discovery.kits);
|
|
443
|
+
outroError("Installation failed");
|
|
444
|
+
}
|
|
445
|
+
else if (options.json) {
|
|
446
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
console.error(`Error: ${message}`);
|
|
450
|
+
console.error("Available kits: " + discovery.kits.map((k) => k.name).join(", "));
|
|
451
|
+
}
|
|
452
|
+
return { success: false, exitCode: ExitCode.KitNotFound, error: message };
|
|
453
|
+
}
|
|
454
|
+
kitsForSource = filterDiscoveryKitsByName(discovery, options.kit).kits;
|
|
207
455
|
}
|
|
208
|
-
|
|
456
|
+
selectedForSource = kitsForSource.map((kit) => ({
|
|
457
|
+
...kit,
|
|
458
|
+
source: sourceValue,
|
|
459
|
+
localPath: localFetch.localPath,
|
|
460
|
+
installAs: kit.name,
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
selectedKits.push(...selectedForSource);
|
|
464
|
+
if (discovery.primitivesRegistry) {
|
|
465
|
+
const loader = new PrimitivesRegistryLoader(localFetch.localPath);
|
|
466
|
+
await loader.load();
|
|
467
|
+
registryBySource.set(sourceValue, loader);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
registryBySource.set(sourceValue, null);
|
|
209
471
|
}
|
|
210
|
-
|
|
472
|
+
sourceDiscoveries.push({ source: sourceValue, localPath: localFetch.localPath, discovery });
|
|
211
473
|
}
|
|
212
474
|
// Handle --list flag
|
|
213
475
|
if (options.list) {
|
|
214
476
|
if (options.json) {
|
|
215
|
-
|
|
477
|
+
if (sourceDiscoveries.length === 1) {
|
|
478
|
+
const discovery = sourceDiscoveries[0].discovery;
|
|
479
|
+
displayKitsJson(discovery.kits, discovery.source, discovery.repository);
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
const output = {
|
|
483
|
+
sources: sourceDiscoveries.map((entry) => ({
|
|
484
|
+
source: entry.discovery.source,
|
|
485
|
+
repository: entry.discovery.repository,
|
|
486
|
+
kits: entry.discovery.kits.map((kit) => ({
|
|
487
|
+
name: kit.name,
|
|
488
|
+
version: kit.manifest.version,
|
|
489
|
+
description: kit.manifest.description,
|
|
490
|
+
products: kit.manifest.products,
|
|
491
|
+
requiredPrimitives: kit.requiredPrimitives,
|
|
492
|
+
primitiveCounts: kit.primitiveCounts,
|
|
493
|
+
})),
|
|
494
|
+
})),
|
|
495
|
+
};
|
|
496
|
+
console.log(JSON.stringify(output, null, 2));
|
|
497
|
+
}
|
|
216
498
|
}
|
|
217
499
|
else {
|
|
218
|
-
displayAvailableKits(
|
|
500
|
+
displayAvailableKits(availableKits);
|
|
219
501
|
}
|
|
220
502
|
return { success: true, exitCode: ExitCode.Success };
|
|
221
503
|
}
|
|
222
504
|
// Check if any kits were found
|
|
223
|
-
if (
|
|
505
|
+
if (selectedKits.length === 0) {
|
|
224
506
|
const message = "No kits found in repository";
|
|
225
507
|
if (isInteractive) {
|
|
226
508
|
displayNoKitsFound();
|
|
@@ -235,21 +517,56 @@ export async function runInstall(source, options) {
|
|
|
235
517
|
return { success: false, exitCode: ExitCode.KitNotFound, error: message };
|
|
236
518
|
}
|
|
237
519
|
// Interactive kit selection (stage 1)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const result = await selectKits(discovery.kits, {
|
|
520
|
+
if (isInteractive && !manifestInstall && !options.kit) {
|
|
521
|
+
const result = await selectKits(selectedKits, {
|
|
241
522
|
showAvailable: false,
|
|
242
523
|
});
|
|
243
524
|
if (result.cancelled) {
|
|
244
525
|
outroCancel("Installation cancelled");
|
|
245
526
|
return { success: false, exitCode: ExitCode.InstallationFailed };
|
|
246
527
|
}
|
|
247
|
-
selectedKits = filterKitsByName(
|
|
528
|
+
selectedKits = filterKitsByName(selectedKits, result.selected);
|
|
529
|
+
}
|
|
530
|
+
if (options.alias) {
|
|
531
|
+
if (selectedKits.length !== 1) {
|
|
532
|
+
const message = "--alias can only be used when installing a single kit.";
|
|
533
|
+
if (isInteractive) {
|
|
534
|
+
logError(message);
|
|
535
|
+
outroError("Installation failed");
|
|
536
|
+
}
|
|
537
|
+
else if (options.json) {
|
|
538
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
console.error(`Error: ${message}`);
|
|
542
|
+
}
|
|
543
|
+
return { success: false, exitCode: ExitCode.InvalidArguments, error: message };
|
|
544
|
+
}
|
|
545
|
+
selectedKits[0] = { ...selectedKits[0], installAs: options.alias };
|
|
546
|
+
}
|
|
547
|
+
const installAsIndex = new Map();
|
|
548
|
+
for (const kit of selectedKits) {
|
|
549
|
+
const existingSource = installAsIndex.get(kit.installAs);
|
|
550
|
+
if (existingSource && existingSource !== kit.source) {
|
|
551
|
+
const message = `Kit instance name "${kit.installAs}" is used by multiple selected kits (${existingSource}, ${kit.source}).`;
|
|
552
|
+
if (isInteractive) {
|
|
553
|
+
logError(message);
|
|
554
|
+
outroError("Installation failed");
|
|
555
|
+
}
|
|
556
|
+
else if (options.json) {
|
|
557
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
console.error(`Error: ${message}`);
|
|
561
|
+
}
|
|
562
|
+
return { success: false, exitCode: ExitCode.InstallationFailed, error: message };
|
|
563
|
+
}
|
|
564
|
+
installAsIndex.set(kit.installAs, kit.source);
|
|
248
565
|
}
|
|
249
566
|
void debugLog({
|
|
250
567
|
level: "info",
|
|
251
568
|
event: "install.kits.selected",
|
|
252
|
-
data: { kits: selectedKits.map((kit) => kit.name) },
|
|
569
|
+
data: { kits: selectedKits.map((kit) => ({ name: kit.name, installAs: kit.installAs })) },
|
|
253
570
|
});
|
|
254
571
|
// Detect harnesses
|
|
255
572
|
const harnessSpinner = isInteractive ? startSpinner("Detecting AI harnesses...") : undefined;
|
|
@@ -269,7 +586,29 @@ export async function runInstall(source, options) {
|
|
|
269
586
|
});
|
|
270
587
|
// Filter by requested harnesses
|
|
271
588
|
let targetHarnesses = detectedHarnesses;
|
|
272
|
-
if (
|
|
589
|
+
if (manifestInstall && manifest?.harnesses && manifest.harnesses.length > 0) {
|
|
590
|
+
if (options.agent && options.agent.length > 0) {
|
|
591
|
+
const message = "Ignoring --agent flags because kits.json defines harnesses.";
|
|
592
|
+
if (isInteractive) {
|
|
593
|
+
logInfo(message);
|
|
594
|
+
}
|
|
595
|
+
else if (options.verbose) {
|
|
596
|
+
console.error(`Warning: ${message}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const missing = getMissingHarnesses(detectedHarnesses, manifest.harnesses);
|
|
600
|
+
if (missing.length > 0) {
|
|
601
|
+
if (isInteractive) {
|
|
602
|
+
warnMissingHarnesses(missing);
|
|
603
|
+
}
|
|
604
|
+
else if (options.verbose) {
|
|
605
|
+
const label = missing.length === 1 ? "Harness" : "Harnesses";
|
|
606
|
+
console.error(`Warning: ${label} not detected: ${missing.join(", ")}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
targetHarnesses = filterHarnessesByName(detectedHarnesses, manifest.harnesses);
|
|
610
|
+
}
|
|
611
|
+
else if (options.agent && options.agent.length > 0) {
|
|
273
612
|
const missing = getMissingHarnesses(detectedHarnesses, options.agent);
|
|
274
613
|
if (missing.length > 0) {
|
|
275
614
|
if (isInteractive) {
|
|
@@ -299,7 +638,7 @@ export async function runInstall(source, options) {
|
|
|
299
638
|
}
|
|
300
639
|
// Interactive harness selection
|
|
301
640
|
let selectedHarnesses = targetHarnesses;
|
|
302
|
-
if (isInteractive && !options.agent) {
|
|
641
|
+
if (isInteractive && !manifestInstall && !options.agent) {
|
|
303
642
|
const result = await selectHarnesses(detectedHarnesses, {
|
|
304
643
|
showDetected: true,
|
|
305
644
|
includeAllOption: true,
|
|
@@ -340,20 +679,137 @@ export async function runInstall(source, options) {
|
|
|
340
679
|
event: "install.scope.selected",
|
|
341
680
|
data: { scope: effectiveScope, projectRoot },
|
|
342
681
|
});
|
|
682
|
+
const lockfileRoot = effectiveScope === "project" ? projectRoot : undefined;
|
|
683
|
+
let lockfile = await readLockfile(effectiveScope, lockfileRoot);
|
|
684
|
+
const manifestInfo = manifestInstall && manifestPath && manifestHash
|
|
685
|
+
? { path: manifestPath, hash: manifestHash }
|
|
686
|
+
: undefined;
|
|
687
|
+
const lockfileInSync = !!manifestInfo && !!lockfile && lockfile.manifest?.hash === manifestInfo.hash;
|
|
688
|
+
if (manifestInstall && options.ci && (!lockfile || !lockfileInSync)) {
|
|
689
|
+
const message = "Lockfile missing or out of sync with kits.json.";
|
|
690
|
+
if (options.json) {
|
|
691
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
console.error(`Error: ${message}`);
|
|
695
|
+
}
|
|
696
|
+
return { success: false, exitCode: ExitCode.LockfileOutOfSync, error: message };
|
|
697
|
+
}
|
|
698
|
+
if (!lockfile) {
|
|
699
|
+
lockfile = createEmptyLockfile(effectiveScope, lockfileRoot);
|
|
700
|
+
}
|
|
701
|
+
if (manifestInfo) {
|
|
702
|
+
lockfile.manifest = manifestInfo;
|
|
703
|
+
}
|
|
704
|
+
const lockedVersions = manifestInstall && lockfileInSync && !options.ignoreLock
|
|
705
|
+
? buildLockedVersionMap(lockfile)
|
|
706
|
+
: undefined;
|
|
707
|
+
const outputSource = manifestInstall && manifestPath ? manifestPath : source;
|
|
708
|
+
const overwriteTargets = new Map();
|
|
709
|
+
const existingInstances = new Set();
|
|
710
|
+
for (const harness of selectedHarnesses) {
|
|
711
|
+
const harnessEntry = lockfile.harnesses[harness.name];
|
|
712
|
+
if (!harnessEntry)
|
|
713
|
+
continue;
|
|
714
|
+
for (const instanceName of Object.keys(harnessEntry.kits)) {
|
|
715
|
+
existingInstances.add(instanceName);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const usedInstanceNames = new Set(selectedKits.map((kit) => kit.installAs));
|
|
719
|
+
for (const kit of selectedKits) {
|
|
720
|
+
const collidingHarnesses = [];
|
|
721
|
+
const existingKits = [];
|
|
722
|
+
for (const harness of selectedHarnesses) {
|
|
723
|
+
const existing = lockfile.harnesses[harness.name]?.kits[kit.installAs];
|
|
724
|
+
if (!existing)
|
|
725
|
+
continue;
|
|
726
|
+
collidingHarnesses.push(harness.name);
|
|
727
|
+
existingKits.push(existing);
|
|
728
|
+
}
|
|
729
|
+
if (collidingHarnesses.length === 0) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (!isInteractive) {
|
|
733
|
+
if (options.overwrite) {
|
|
734
|
+
overwriteTargets.set(kit.installAs, new Set(collidingHarnesses));
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const message = `Kit instance "${kit.installAs}" already exists in the selected scope. Use --overwrite or choose a different --alias.`;
|
|
738
|
+
if (options.json) {
|
|
739
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
console.error(`Error: ${message}`);
|
|
743
|
+
}
|
|
744
|
+
return { success: false, exitCode: ExitCode.InstallationFailed, error: message };
|
|
745
|
+
}
|
|
746
|
+
const decision = await promptKitCollisionResolution({
|
|
747
|
+
kit,
|
|
748
|
+
harnesses: collidingHarnesses,
|
|
749
|
+
existing: existingKits,
|
|
750
|
+
});
|
|
751
|
+
if (decision === "cancel") {
|
|
752
|
+
outroCancel("Installation cancelled");
|
|
753
|
+
return { success: false, exitCode: ExitCode.InstallationFailed };
|
|
754
|
+
}
|
|
755
|
+
if (decision === "overwrite") {
|
|
756
|
+
overwriteTargets.set(kit.installAs, new Set(collidingHarnesses));
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const newName = await promptForKitAlias(kit.installAs, usedInstanceNames, existingInstances);
|
|
760
|
+
if (!newName) {
|
|
761
|
+
outroCancel("Installation cancelled");
|
|
762
|
+
return { success: false, exitCode: ExitCode.InstallationFailed };
|
|
763
|
+
}
|
|
764
|
+
usedInstanceNames.delete(kit.installAs);
|
|
765
|
+
kit.installAs = newName;
|
|
766
|
+
usedInstanceNames.add(newName);
|
|
767
|
+
}
|
|
343
768
|
// Compatibility check (after scope selection)
|
|
344
769
|
const compatibilityHarnesses = selectedHarnesses.map((h) => ({
|
|
345
770
|
...h,
|
|
346
771
|
supportedPrimitives: h.adapter.getSupportedPrimitives(effectiveScope),
|
|
347
772
|
}));
|
|
348
|
-
|
|
773
|
+
const compatibilityKits = selectedKits.map((kit) => ({
|
|
774
|
+
...kit,
|
|
775
|
+
name: kit.installAs,
|
|
776
|
+
}));
|
|
777
|
+
let matrices = buildCompatibilityMatrix(compatibilityKits, compatibilityHarnesses);
|
|
349
778
|
let compatResult = {
|
|
350
779
|
cancelled: false,
|
|
351
780
|
choice: "proceed",
|
|
352
781
|
};
|
|
353
782
|
let compatiblePairs = [];
|
|
354
783
|
let allowCompatibleOnly = false;
|
|
355
|
-
if (
|
|
356
|
-
const
|
|
784
|
+
if (manifestInstall) {
|
|
785
|
+
const mode = manifest?.compatibility ?? "strict";
|
|
786
|
+
if (mode === "compatible-only") {
|
|
787
|
+
allowCompatibleOnly = true;
|
|
788
|
+
compatiblePairs = getCompatiblePairs(matrices);
|
|
789
|
+
if (compatiblePairs.length === 0) {
|
|
790
|
+
const message = "No compatible kit-harness combinations for the selected scope";
|
|
791
|
+
if (options.json) {
|
|
792
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
console.error(`Error: ${message}`);
|
|
796
|
+
}
|
|
797
|
+
return { success: false, exitCode: ExitCode.KitIncompatible, error: message };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else if (hasIncompatibilities(matrices)) {
|
|
801
|
+
const message = "No compatible kit-harness combinations for the selected scope";
|
|
802
|
+
if (options.json) {
|
|
803
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
console.error(`Error: ${message}`);
|
|
807
|
+
}
|
|
808
|
+
return { success: false, exitCode: ExitCode.KitIncompatible, error: message };
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
else if (isInteractive) {
|
|
812
|
+
const compat = await runCompatibilityCheck(compatibilityKits, compatibilityHarnesses, effectiveScope, {
|
|
357
813
|
showMatrix: options.verbose ?? false,
|
|
358
814
|
promptOnIncompatible: true,
|
|
359
815
|
});
|
|
@@ -362,7 +818,7 @@ export async function runInstall(source, options) {
|
|
|
362
818
|
compatiblePairs = compat.compatiblePairs;
|
|
363
819
|
allowCompatibleOnly = compat.result.choice === "install-compatible";
|
|
364
820
|
}
|
|
365
|
-
if (hasIncompatibilities(matrices)) {
|
|
821
|
+
if (!manifestInstall && hasIncompatibilities(matrices)) {
|
|
366
822
|
void debugLog({
|
|
367
823
|
level: "warn",
|
|
368
824
|
event: "install.compatibility.failed",
|
|
@@ -405,13 +861,13 @@ export async function runInstall(source, options) {
|
|
|
405
861
|
if (isInteractive && !options.dryRun) {
|
|
406
862
|
const planKitNames = allowCompatibleOnly
|
|
407
863
|
? new Set(compatiblePairs.map((pair) => pair.kit))
|
|
408
|
-
: new Set(selectedKits.map((kit) => kit.
|
|
864
|
+
: new Set(selectedKits.map((kit) => kit.installAs));
|
|
409
865
|
const planHarnessNames = allowCompatibleOnly
|
|
410
866
|
? new Set(compatiblePairs.map((pair) => pair.harness))
|
|
411
867
|
: new Set(selectedHarnesses.map((h) => h.name));
|
|
412
|
-
const planKits = selectedKits.filter((kit) => planKitNames.has(kit.
|
|
868
|
+
const planKits = selectedKits.filter((kit) => planKitNames.has(kit.installAs));
|
|
413
869
|
const planHarnesses = selectedHarnesses.filter((h) => planHarnessNames.has(h.name));
|
|
414
|
-
displayInstallPlanSummary(planKits.map((kit) => ({ name: kit
|
|
870
|
+
displayInstallPlanSummary(planKits.map((kit) => ({ name: getKitDisplayName(kit), version: kit.manifest.version })), planHarnesses.map((h) => ({ displayName: h.displayName })), effectiveScope);
|
|
415
871
|
const proceed = await confirmInstallPlan();
|
|
416
872
|
if (proceed.cancelled || !proceed.proceed) {
|
|
417
873
|
outroCancel("Installation cancelled");
|
|
@@ -423,13 +879,14 @@ export async function runInstall(source, options) {
|
|
|
423
879
|
if (options.json) {
|
|
424
880
|
const output = {
|
|
425
881
|
success: true,
|
|
426
|
-
source,
|
|
882
|
+
source: outputSource,
|
|
427
883
|
scope: effectiveScope,
|
|
428
884
|
harnesses: selectedHarnesses.map((h) => ({
|
|
429
885
|
name: h.name,
|
|
430
886
|
configPath: h.configPath || "",
|
|
431
887
|
kits: selectedKits.map((kit) => ({
|
|
432
888
|
name: kit.name,
|
|
889
|
+
installAs: kit.installAs,
|
|
433
890
|
version: kit.manifest.version,
|
|
434
891
|
installed: [],
|
|
435
892
|
})),
|
|
@@ -442,21 +899,27 @@ export async function runInstall(source, options) {
|
|
|
442
899
|
console.log(JSON.stringify(output, null, 2));
|
|
443
900
|
}
|
|
444
901
|
else {
|
|
445
|
-
displayDryRunSummary(selectedKits.map((k) => ({ name: k
|
|
902
|
+
displayDryRunSummary(selectedKits.map((k) => ({ name: getKitDisplayName(k), version: k.manifest.version })), selectedHarnesses.map((h) => ({ name: h.name, displayName: h.displayName })), { scope: effectiveScope, projectRoot });
|
|
446
903
|
}
|
|
447
904
|
return { success: true, exitCode: ExitCode.Success };
|
|
448
905
|
}
|
|
449
906
|
// Execute installation
|
|
450
|
-
const
|
|
907
|
+
const mergedMcpInstance = mergeMcpInstanceOverrides(manifest?.mcpInstanceOverrides, options.mcpInstance ?? {});
|
|
908
|
+
const installResults = await executeInstallation(selectedKits, selectedHarnesses, registryBySource, {
|
|
451
909
|
isInteractive,
|
|
452
910
|
verbose: options.verbose ?? false,
|
|
453
911
|
json: options.json ?? false,
|
|
454
912
|
scope: effectiveScope,
|
|
455
913
|
projectRoot,
|
|
456
914
|
env: options.env ?? {},
|
|
457
|
-
mcpInstance:
|
|
915
|
+
mcpInstance: mergedMcpInstance,
|
|
916
|
+
overwriteTargets,
|
|
458
917
|
allowCompatibleOnly,
|
|
459
918
|
compatiblePairs: allowCompatibleOnly ? compatiblePairs : [],
|
|
919
|
+
lockfile,
|
|
920
|
+
outputSource,
|
|
921
|
+
...(lockedVersions ? { lockedVersions } : {}),
|
|
922
|
+
...(manifestInstall ? {} : { lockfileSource: source }),
|
|
460
923
|
});
|
|
461
924
|
if (!installResults.success) {
|
|
462
925
|
const errorMsg = installResults.error || "Installation failed";
|
|
@@ -484,7 +947,7 @@ export async function runInstall(source, options) {
|
|
|
484
947
|
data: {
|
|
485
948
|
scope: effectiveScope,
|
|
486
949
|
harnesses: selectedHarnesses.map((h) => h.name),
|
|
487
|
-
kits: selectedKits.map((k) => k.name),
|
|
950
|
+
kits: selectedKits.map((k) => ({ name: k.name, installAs: k.installAs })),
|
|
488
951
|
},
|
|
489
952
|
});
|
|
490
953
|
if (options.json) {
|
|
@@ -514,10 +977,12 @@ export async function runInstall(source, options) {
|
|
|
514
977
|
return { success: false, exitCode: ExitCode.InstallationFailed, error: message };
|
|
515
978
|
}
|
|
516
979
|
finally {
|
|
517
|
-
// Clean up fetched
|
|
518
|
-
|
|
980
|
+
// Clean up fetched sources
|
|
981
|
+
for (const fetched of fetchResults) {
|
|
982
|
+
if (!fetched.cleanup)
|
|
983
|
+
continue;
|
|
519
984
|
try {
|
|
520
|
-
await
|
|
985
|
+
await fetched.cleanup();
|
|
521
986
|
}
|
|
522
987
|
catch {
|
|
523
988
|
// Ignore cleanup errors
|
|
@@ -728,9 +1193,73 @@ function resolveHookInstanceName(programName, primitiveOverride, kitName) {
|
|
|
728
1193
|
}
|
|
729
1194
|
return programName;
|
|
730
1195
|
}
|
|
1196
|
+
function normalizeManifestKitEntry(entry) {
|
|
1197
|
+
if (typeof entry === "string") {
|
|
1198
|
+
return { name: entry };
|
|
1199
|
+
}
|
|
1200
|
+
const normalized = { name: entry.name };
|
|
1201
|
+
if (entry.installAs) {
|
|
1202
|
+
normalized.installAs = entry.installAs;
|
|
1203
|
+
}
|
|
1204
|
+
return normalized;
|
|
1205
|
+
}
|
|
1206
|
+
function getKitDisplayName(kit) {
|
|
1207
|
+
return kit.installAs === kit.name ? kit.name : `${kit.name} (as ${kit.installAs})`;
|
|
1208
|
+
}
|
|
731
1209
|
function buildPrimitiveKey(kitName, sourcePath) {
|
|
732
1210
|
return `${kitName}::${sourcePath}`;
|
|
733
1211
|
}
|
|
1212
|
+
function filterOverwriteKits(overwriteKits, kits) {
|
|
1213
|
+
if (!overwriteKits || overwriteKits.size === 0) {
|
|
1214
|
+
return new Set();
|
|
1215
|
+
}
|
|
1216
|
+
const allowed = new Set(kits.map((kit) => kit.installAs));
|
|
1217
|
+
return new Set(Array.from(overwriteKits).filter((name) => allowed.has(name)));
|
|
1218
|
+
}
|
|
1219
|
+
async function promptKitCollisionResolution(options) {
|
|
1220
|
+
const existingNames = Array.from(new Set(options.existing.map((entry) => entry.name)));
|
|
1221
|
+
const existingHint = existingNames.length > 0 ? `Existing kit${existingNames.length === 1 ? "" : "s"}: ${existingNames.join(", ")}` : "";
|
|
1222
|
+
const harnessLabel = options.harnesses.join(", ");
|
|
1223
|
+
const choice = await clack.select({
|
|
1224
|
+
message: `Kit instance "${options.kit.installAs}" already exists on ${harnessLabel}. ` +
|
|
1225
|
+
(existingHint ? `${existingHint}. ` : "") +
|
|
1226
|
+
"How would you like to proceed?",
|
|
1227
|
+
options: [
|
|
1228
|
+
{ value: "overwrite", label: "Overwrite existing kit instance" },
|
|
1229
|
+
{ value: "rename", label: "Rename this install (alias)" },
|
|
1230
|
+
{ value: "cancel", label: "Cancel installation" },
|
|
1231
|
+
],
|
|
1232
|
+
});
|
|
1233
|
+
if (clack.isCancel(choice)) {
|
|
1234
|
+
return "cancel";
|
|
1235
|
+
}
|
|
1236
|
+
return choice;
|
|
1237
|
+
}
|
|
1238
|
+
async function promptForKitAlias(currentName, usedNames, existingNames) {
|
|
1239
|
+
while (true) {
|
|
1240
|
+
const value = await clack.text({
|
|
1241
|
+
message: `Enter a new instance name for "${currentName}":`,
|
|
1242
|
+
placeholder: `${currentName}-alt`,
|
|
1243
|
+
});
|
|
1244
|
+
if (clack.isCancel(value)) {
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
const trimmed = String(value).trim();
|
|
1248
|
+
if (!trimmed) {
|
|
1249
|
+
clack.log.warn("Instance name cannot be empty.");
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
if (usedNames.has(trimmed)) {
|
|
1253
|
+
clack.log.warn(`Instance name "${trimmed}" is already selected for this install.`);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
if (existingNames.has(trimmed)) {
|
|
1257
|
+
clack.log.warn(`Instance name "${trimmed}" is already installed for this scope.`);
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
return trimmed;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
734
1263
|
async function buildMcpPrimitiveInfoBySourcePath(resolvedByKit, cliEnvFlags, promptResults, instanceOverrides) {
|
|
735
1264
|
const infoBySourcePath = new Map();
|
|
736
1265
|
for (const [kitName, primitives] of resolvedByKit.entries()) {
|
|
@@ -857,38 +1386,144 @@ async function promptForMcpInstanceName(instanceName, usedNames) {
|
|
|
857
1386
|
return trimmed;
|
|
858
1387
|
}
|
|
859
1388
|
}
|
|
1389
|
+
async function removeOverwrittenKitsForHarness(harness, kitsToOverwrite, lockfile, scope, projectRoot, options) {
|
|
1390
|
+
const harnessEntry = lockfile.harnesses[harness.name];
|
|
1391
|
+
if (!harnessEntry || kitsToOverwrite.size === 0) {
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const mcpInstances = harnessEntry.mcpInstances;
|
|
1395
|
+
const hookInstances = harnessEntry.hookInstances;
|
|
1396
|
+
const mcpCandidates = new Set();
|
|
1397
|
+
const hookCandidates = new Set();
|
|
1398
|
+
for (const kitName of kitsToOverwrite) {
|
|
1399
|
+
const kit = harnessEntry.kits[kitName];
|
|
1400
|
+
if (!kit)
|
|
1401
|
+
continue;
|
|
1402
|
+
for (const primitive of kit.primitives) {
|
|
1403
|
+
if (primitive.type === "mcp") {
|
|
1404
|
+
const instanceName = primitive.instanceName ?? primitive.namespacedName;
|
|
1405
|
+
if (instanceName) {
|
|
1406
|
+
mcpCandidates.add(instanceName);
|
|
1407
|
+
}
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (primitive.type === "hooks") {
|
|
1411
|
+
const instanceName = primitive.instanceName ?? primitive.namespacedName;
|
|
1412
|
+
if (instanceName) {
|
|
1413
|
+
hookCandidates.add(instanceName);
|
|
1414
|
+
}
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
const filePath = expandPath(primitive.installedPath);
|
|
1418
|
+
try {
|
|
1419
|
+
await remove(filePath);
|
|
1420
|
+
if (options.verbose) {
|
|
1421
|
+
if (options.isInteractive) {
|
|
1422
|
+
clack.log.info(` ✓ Removed ${primitive.namespacedName}`);
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
console.error(` Removed: ${filePath}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
catch (error) {
|
|
1430
|
+
if (options.verbose) {
|
|
1431
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1432
|
+
if (options.isInteractive) {
|
|
1433
|
+
clack.log.warn(` ⚠ Could not remove ${primitive.namespacedName}: ${msg}`);
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
console.error(` Warning: Could not remove ${filePath}: ${msg}`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const mcpInstancesToRemove = [];
|
|
1443
|
+
if (mcpInstances) {
|
|
1444
|
+
for (const instanceName of mcpCandidates) {
|
|
1445
|
+
const entry = mcpInstances[instanceName];
|
|
1446
|
+
if (!entry)
|
|
1447
|
+
continue;
|
|
1448
|
+
entry.usedBy = entry.usedBy.filter((name) => !kitsToOverwrite.has(name));
|
|
1449
|
+
if (entry.usedBy.length === 0) {
|
|
1450
|
+
delete mcpInstances[instanceName];
|
|
1451
|
+
mcpInstancesToRemove.push(instanceName);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (mcpInstancesToRemove.length > 0) {
|
|
1456
|
+
if (harness.adapter.removeMcpServers) {
|
|
1457
|
+
await harness.adapter.removeMcpServers(mcpInstancesToRemove, scope, scope === "project" ? projectRoot : undefined);
|
|
1458
|
+
}
|
|
1459
|
+
else if (options.verbose) {
|
|
1460
|
+
const warning = ` ⚠ Unable to remove MCP instances for ${harness.displayName} (adapter unavailable)`;
|
|
1461
|
+
if (options.isInteractive) {
|
|
1462
|
+
clack.log.warn(warning);
|
|
1463
|
+
}
|
|
1464
|
+
else {
|
|
1465
|
+
console.error(warning);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const hookInstancesToRemove = [];
|
|
1470
|
+
if (hookInstances) {
|
|
1471
|
+
for (const instanceName of hookCandidates) {
|
|
1472
|
+
const entry = hookInstances[instanceName];
|
|
1473
|
+
if (!entry)
|
|
1474
|
+
continue;
|
|
1475
|
+
entry.usedBy = entry.usedBy.filter((name) => !kitsToOverwrite.has(name));
|
|
1476
|
+
if (entry.usedBy.length === 0) {
|
|
1477
|
+
delete hookInstances[instanceName];
|
|
1478
|
+
hookInstancesToRemove.push(instanceName);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (hookInstancesToRemove.length > 0) {
|
|
1483
|
+
if (harness.adapter.removeHooks) {
|
|
1484
|
+
await harness.adapter.removeHooks(hookInstancesToRemove, scope, scope === "project" ? projectRoot : undefined);
|
|
1485
|
+
}
|
|
1486
|
+
else if (options.verbose) {
|
|
1487
|
+
const warning = ` ⚠ Unable to remove hook instances for ${harness.displayName} (adapter unavailable)`;
|
|
1488
|
+
if (options.isInteractive) {
|
|
1489
|
+
clack.log.warn(warning);
|
|
1490
|
+
}
|
|
1491
|
+
else {
|
|
1492
|
+
console.error(warning);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
for (const kitName of kitsToOverwrite) {
|
|
1497
|
+
delete harnessEntry.kits[kitName];
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
860
1500
|
/**
|
|
861
1501
|
* Execute the actual installation of kits to harnesses.
|
|
862
1502
|
*/
|
|
863
|
-
async function executeInstallation(kits, harnesses,
|
|
864
|
-
const { isInteractive, verbose, scope, projectRoot, env, mcpInstance, allowCompatibleOnly = false, compatiblePairs = [], } = options;
|
|
865
|
-
// Load or create primitives registry loader
|
|
866
|
-
let registryLoader = null;
|
|
867
|
-
if (discovery.primitivesRegistry) {
|
|
868
|
-
registryLoader = new PrimitivesRegistryLoader(discovery.localPath);
|
|
869
|
-
await registryLoader.load();
|
|
870
|
-
}
|
|
1503
|
+
async function executeInstallation(kits, harnesses, registryBySource, options) {
|
|
1504
|
+
const { isInteractive, verbose, scope, projectRoot, env, mcpInstance, overwriteTargets, allowCompatibleOnly = false, compatiblePairs = [], lockfile, lockedVersions, outputSource, lockfileSource, } = options;
|
|
871
1505
|
// Pre-pass: resolve all primitives to collect MCP env var definitions for validation
|
|
872
1506
|
const allResolvedPrimitives = [];
|
|
873
1507
|
const resolvedByKit = new Map();
|
|
874
1508
|
const resolutionFailures = [];
|
|
875
1509
|
for (const kit of kits) {
|
|
1510
|
+
const registryLoader = registryBySource.get(kit.source) ?? null;
|
|
876
1511
|
if (registryLoader) {
|
|
877
|
-
const kitBasePath = path.join(
|
|
878
|
-
const resolution = await resolvePrimitiveReferences(kit.manifest, registryLoader, kitBasePath);
|
|
1512
|
+
const kitBasePath = path.join(kit.localPath, kit.path);
|
|
1513
|
+
const resolution = await resolvePrimitiveReferences(kit.manifest, registryLoader, kitBasePath, lockedVersions, kit.installAs);
|
|
879
1514
|
if (resolution.errors.length > 0) {
|
|
880
1515
|
resolutionFailures.push({
|
|
881
|
-
kit: kit
|
|
1516
|
+
kit: getKitDisplayName(kit),
|
|
882
1517
|
errors: resolution.errors.map((e) => e.message),
|
|
883
1518
|
});
|
|
884
1519
|
continue;
|
|
885
1520
|
}
|
|
886
|
-
resolvedByKit.set(kit.
|
|
1521
|
+
resolvedByKit.set(kit.installAs, resolution.primitives);
|
|
887
1522
|
allResolvedPrimitives.push(...resolution.primitives);
|
|
888
1523
|
}
|
|
889
1524
|
else {
|
|
890
|
-
const inlinePrimitives = resolveInlinePrimitivesOnly(kit,
|
|
891
|
-
resolvedByKit.set(kit.
|
|
1525
|
+
const inlinePrimitives = resolveInlinePrimitivesOnly(kit, kit.localPath, kit.installAs);
|
|
1526
|
+
resolvedByKit.set(kit.installAs, inlinePrimitives);
|
|
892
1527
|
allResolvedPrimitives.push(...inlinePrimitives);
|
|
893
1528
|
}
|
|
894
1529
|
}
|
|
@@ -994,12 +1629,19 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
994
1629
|
applyPromptResultsToResolutions(envResolutions, promptResults);
|
|
995
1630
|
}
|
|
996
1631
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1632
|
+
if (lockfileSource) {
|
|
1633
|
+
lockfile.source = lockfileSource;
|
|
1634
|
+
}
|
|
1635
|
+
const overwriteKitsByHarness = new Map();
|
|
1636
|
+
if (overwriteTargets) {
|
|
1637
|
+
for (const [kitName, harnessNames] of overwriteTargets.entries()) {
|
|
1638
|
+
for (const harnessName of harnessNames) {
|
|
1639
|
+
const existing = overwriteKitsByHarness.get(harnessName) ?? new Set();
|
|
1640
|
+
existing.add(kitName);
|
|
1641
|
+
overwriteKitsByHarness.set(harnessName, existing);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1001
1644
|
}
|
|
1002
|
-
manifest.source = source;
|
|
1003
1645
|
const harnessResults = [];
|
|
1004
1646
|
const skippedResults = [];
|
|
1005
1647
|
const compatibleKitsByHarness = new Map();
|
|
@@ -1021,18 +1663,18 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1021
1663
|
const hookInstancesToInstallByHarness = new Map();
|
|
1022
1664
|
for (const harness of harnesses) {
|
|
1023
1665
|
const kitsToCheck = allowCompatibleOnly
|
|
1024
|
-
? kits.filter((kit) => compatibleKitsByHarness.get(harness.name)?.has(kit.
|
|
1666
|
+
? kits.filter((kit) => compatibleKitsByHarness.get(harness.name)?.has(kit.installAs))
|
|
1025
1667
|
: kits;
|
|
1026
1668
|
if (kitsToCheck.length === 0 || mcpInfoByPrimitiveKey.size === 0) {
|
|
1027
1669
|
continue;
|
|
1028
1670
|
}
|
|
1029
1671
|
const desiredByName = new Map();
|
|
1030
1672
|
for (const kit of kitsToCheck) {
|
|
1031
|
-
const primitives = resolvedByKit.get(kit.
|
|
1673
|
+
const primitives = resolvedByKit.get(kit.installAs) ?? [];
|
|
1032
1674
|
for (const primitive of primitives) {
|
|
1033
1675
|
if (primitive.type !== "mcp")
|
|
1034
1676
|
continue;
|
|
1035
|
-
const key = buildPrimitiveKey(kit.
|
|
1677
|
+
const key = buildPrimitiveKey(kit.installAs, primitive.sourcePath);
|
|
1036
1678
|
const info = mcpInfoByPrimitiveKey.get(key);
|
|
1037
1679
|
if (!info)
|
|
1038
1680
|
continue;
|
|
@@ -1042,8 +1684,8 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1042
1684
|
kits: new Set(),
|
|
1043
1685
|
info,
|
|
1044
1686
|
};
|
|
1045
|
-
entry.primitives.push({ primitive, kitName: kit.
|
|
1046
|
-
entry.kits.add(kit.
|
|
1687
|
+
entry.primitives.push({ primitive, kitName: kit.installAs });
|
|
1688
|
+
entry.kits.add(kit.installAs);
|
|
1047
1689
|
byHash.set(info.configHash, entry);
|
|
1048
1690
|
desiredByName.set(info.instanceName, byHash);
|
|
1049
1691
|
}
|
|
@@ -1059,8 +1701,11 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1059
1701
|
};
|
|
1060
1702
|
}
|
|
1061
1703
|
}
|
|
1062
|
-
const existingInstances =
|
|
1063
|
-
const
|
|
1704
|
+
const existingInstances = lockfile.harnesses[harness.name]?.mcpInstances ?? {};
|
|
1705
|
+
const overwriteKits = filterOverwriteKits(overwriteKitsByHarness.get(harness.name), kitsToCheck);
|
|
1706
|
+
const usedInstanceNames = new Set(Object.entries(existingInstances)
|
|
1707
|
+
.filter(([, entry]) => entry.usedBy.some((name) => !overwriteKits.has(name)))
|
|
1708
|
+
.map(([name]) => name));
|
|
1064
1709
|
const assignments = new Map();
|
|
1065
1710
|
const forkedInstances = new Map();
|
|
1066
1711
|
const instancesToInstall = new Set();
|
|
@@ -1070,7 +1715,8 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1070
1715
|
let finalHash = candidate.info.configHash;
|
|
1071
1716
|
let action = "install";
|
|
1072
1717
|
const existing = existingInstances[instanceName];
|
|
1073
|
-
|
|
1718
|
+
const existingUsers = existing?.usedBy.filter((name) => !overwriteKits.has(name)) ?? [];
|
|
1719
|
+
if (existing && existingUsers.length > 0) {
|
|
1074
1720
|
if (existing.configHash === candidate.info.configHash) {
|
|
1075
1721
|
action = "reuse";
|
|
1076
1722
|
finalHash = existing.configHash;
|
|
@@ -1161,18 +1807,18 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1161
1807
|
}
|
|
1162
1808
|
for (const harness of harnesses) {
|
|
1163
1809
|
const kitsToCheck = allowCompatibleOnly
|
|
1164
|
-
? kits.filter((kit) => compatibleKitsByHarness.get(harness.name)?.has(kit.
|
|
1810
|
+
? kits.filter((kit) => compatibleKitsByHarness.get(harness.name)?.has(kit.installAs))
|
|
1165
1811
|
: kits;
|
|
1166
1812
|
if (kitsToCheck.length === 0 || hookInfoByPrimitiveKey.size === 0) {
|
|
1167
1813
|
continue;
|
|
1168
1814
|
}
|
|
1169
1815
|
const desiredByName = new Map();
|
|
1170
1816
|
for (const kit of kitsToCheck) {
|
|
1171
|
-
const primitives = resolvedByKit.get(kit.
|
|
1817
|
+
const primitives = resolvedByKit.get(kit.installAs) ?? [];
|
|
1172
1818
|
for (const primitive of primitives) {
|
|
1173
1819
|
if (primitive.type !== "hooks")
|
|
1174
1820
|
continue;
|
|
1175
|
-
const key = buildPrimitiveKey(kit.
|
|
1821
|
+
const key = buildPrimitiveKey(kit.installAs, primitive.sourcePath);
|
|
1176
1822
|
const info = hookInfoByPrimitiveKey.get(key);
|
|
1177
1823
|
if (!info)
|
|
1178
1824
|
continue;
|
|
@@ -1182,8 +1828,8 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1182
1828
|
kits: new Set(),
|
|
1183
1829
|
info,
|
|
1184
1830
|
};
|
|
1185
|
-
entry.primitives.push({ primitive, kitName: kit.
|
|
1186
|
-
entry.kits.add(kit.
|
|
1831
|
+
entry.primitives.push({ primitive, kitName: kit.installAs });
|
|
1832
|
+
entry.kits.add(kit.installAs);
|
|
1187
1833
|
byHash.set(info.configHash, entry);
|
|
1188
1834
|
desiredByName.set(info.instanceName, byHash);
|
|
1189
1835
|
}
|
|
@@ -1199,7 +1845,8 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1199
1845
|
};
|
|
1200
1846
|
}
|
|
1201
1847
|
}
|
|
1202
|
-
const existingInstances =
|
|
1848
|
+
const existingInstances = lockfile.harnesses[harness.name]?.hookInstances ?? {};
|
|
1849
|
+
const overwriteKits = filterOverwriteKits(overwriteKitsByHarness.get(harness.name), kitsToCheck);
|
|
1203
1850
|
const assignments = new Map();
|
|
1204
1851
|
const instancesToInstall = new Set();
|
|
1205
1852
|
for (const [instanceName, byHash] of desiredByName) {
|
|
@@ -1207,7 +1854,8 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1207
1854
|
let action = "install";
|
|
1208
1855
|
let finalHash = candidate.info.configHash;
|
|
1209
1856
|
const existing = existingInstances[instanceName];
|
|
1210
|
-
|
|
1857
|
+
const existingUsers = existing?.usedBy.filter((name) => !overwriteKits.has(name)) ?? [];
|
|
1858
|
+
if (existing && existingUsers.length > 0) {
|
|
1211
1859
|
if (existing.configHash !== candidate.info.configHash) {
|
|
1212
1860
|
return {
|
|
1213
1861
|
success: false,
|
|
@@ -1240,7 +1888,7 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1240
1888
|
// Install to each harness
|
|
1241
1889
|
for (const harness of harnesses) {
|
|
1242
1890
|
const kitsToInstall = allowCompatibleOnly
|
|
1243
|
-
? kits.filter((kit) => compatibleKitsByHarness.get(harness.name)?.has(kit.
|
|
1891
|
+
? kits.filter((kit) => compatibleKitsByHarness.get(harness.name)?.has(kit.installAs))
|
|
1244
1892
|
: kits;
|
|
1245
1893
|
if (kitsToInstall.length === 0) {
|
|
1246
1894
|
continue;
|
|
@@ -1256,6 +1904,10 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1256
1904
|
console.error(`Installing to ${harness.displayName}...`);
|
|
1257
1905
|
}
|
|
1258
1906
|
const kitResults = [];
|
|
1907
|
+
const overwriteKits = filterOverwriteKits(overwriteKitsByHarness.get(harness.name), kitsToInstall);
|
|
1908
|
+
if (overwriteKits && overwriteKits.size > 0) {
|
|
1909
|
+
await removeOverwrittenKitsForHarness(harness, overwriteKits, lockfile, scope, projectRoot, { isInteractive, verbose });
|
|
1910
|
+
}
|
|
1259
1911
|
for (const kit of kitsToInstall) {
|
|
1260
1912
|
// Check compatibility with scope (should already be validated)
|
|
1261
1913
|
const compatibility = harness.adapter.checkCompatibility(kit.manifest, scope);
|
|
@@ -1270,12 +1922,12 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1270
1922
|
};
|
|
1271
1923
|
}
|
|
1272
1924
|
// Use resolved primitives from pre-pass
|
|
1273
|
-
const resolvedPrimitives = resolvedByKit.get(kit.
|
|
1925
|
+
const resolvedPrimitives = resolvedByKit.get(kit.installAs);
|
|
1274
1926
|
if (!resolvedPrimitives) {
|
|
1275
1927
|
return {
|
|
1276
1928
|
success: false,
|
|
1277
1929
|
exitCode: ExitCode.ResolutionFailed,
|
|
1278
|
-
error: `Resolution failed: no primitives resolved for ${kit
|
|
1930
|
+
error: `Resolution failed: no primitives resolved for ${getKitDisplayName(kit)}`,
|
|
1279
1931
|
};
|
|
1280
1932
|
}
|
|
1281
1933
|
const mcpConfigPath = getMcpConfigPath(harness.adapter, scope, projectRoot);
|
|
@@ -1287,7 +1939,7 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1287
1939
|
const installedHookInstanceNames = new Set();
|
|
1288
1940
|
for (const primitive of resolvedPrimitives) {
|
|
1289
1941
|
if (primitive.type === "mcp" && mcpAssignments) {
|
|
1290
|
-
const assignment = mcpAssignments.get(buildPrimitiveKey(kit.
|
|
1942
|
+
const assignment = mcpAssignments.get(buildPrimitiveKey(kit.installAs, primitive.sourcePath));
|
|
1291
1943
|
if (!assignment) {
|
|
1292
1944
|
primitivesForAdapter.push(primitive);
|
|
1293
1945
|
continue;
|
|
@@ -1304,7 +1956,7 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1304
1956
|
continue;
|
|
1305
1957
|
}
|
|
1306
1958
|
if (primitive.type === "hooks" && hookAssignments) {
|
|
1307
|
-
const assignment = hookAssignments.get(buildPrimitiveKey(kit.
|
|
1959
|
+
const assignment = hookAssignments.get(buildPrimitiveKey(kit.installAs, primitive.sourcePath));
|
|
1308
1960
|
if (!assignment) {
|
|
1309
1961
|
primitivesForAdapter.push(primitive);
|
|
1310
1962
|
continue;
|
|
@@ -1322,6 +1974,9 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1322
1974
|
}
|
|
1323
1975
|
primitivesForAdapter.push(primitive);
|
|
1324
1976
|
}
|
|
1977
|
+
const installManifest = kit.installAs === kit.manifest.name
|
|
1978
|
+
? kit.manifest
|
|
1979
|
+
: { ...kit.manifest, name: kit.installAs };
|
|
1325
1980
|
// Install the kit
|
|
1326
1981
|
const installOptions = {
|
|
1327
1982
|
backup: true,
|
|
@@ -1338,10 +1993,11 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1338
1993
|
if (scope === "project") {
|
|
1339
1994
|
installOptions.projectRoot = projectRoot;
|
|
1340
1995
|
}
|
|
1341
|
-
const installResult = await harness.adapter.install(
|
|
1996
|
+
const installResult = await harness.adapter.install(installManifest, primitivesForAdapter, installOptions);
|
|
1342
1997
|
if (!installResult.success) {
|
|
1343
1998
|
skippedResults.push({
|
|
1344
|
-
kit: kit.
|
|
1999
|
+
kit: kit.installAs,
|
|
2000
|
+
name: kit.name,
|
|
1345
2001
|
harness: harness.name,
|
|
1346
2002
|
reason: installResult.error || "Installation failed",
|
|
1347
2003
|
});
|
|
@@ -1355,7 +2011,7 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1355
2011
|
}
|
|
1356
2012
|
if (skippedMcpPrimitives.length > 0) {
|
|
1357
2013
|
for (const primitive of skippedMcpPrimitives) {
|
|
1358
|
-
const namespacedName = harness.adapter.getNamespacedName(kit.
|
|
2014
|
+
const namespacedName = harness.adapter.getNamespacedName(kit.installAs, primitive.name);
|
|
1359
2015
|
const status = {
|
|
1360
2016
|
type: primitive.type,
|
|
1361
2017
|
name: primitive.name,
|
|
@@ -1374,7 +2030,7 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1374
2030
|
}
|
|
1375
2031
|
if (skippedHookPrimitives.length > 0) {
|
|
1376
2032
|
for (const primitive of skippedHookPrimitives) {
|
|
1377
|
-
const namespacedName = harness.adapter.getNamespacedName(kit.
|
|
2033
|
+
const namespacedName = harness.adapter.getNamespacedName(kit.installAs, primitive.name);
|
|
1378
2034
|
const status = {
|
|
1379
2035
|
type: primitive.type,
|
|
1380
2036
|
name: primitive.name,
|
|
@@ -1391,9 +2047,9 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1391
2047
|
installResult.installedPrimitives.push(status);
|
|
1392
2048
|
}
|
|
1393
2049
|
}
|
|
1394
|
-
// Record in
|
|
1395
|
-
if (!
|
|
1396
|
-
|
|
2050
|
+
// Record in lockfile
|
|
2051
|
+
if (!lockfile.harnesses[harness.name]) {
|
|
2052
|
+
lockfile.harnesses[harness.name] = { kits: {} };
|
|
1397
2053
|
}
|
|
1398
2054
|
const statusByKey = new Map();
|
|
1399
2055
|
for (const status of installResult.installedPrimitives) {
|
|
@@ -1403,7 +2059,7 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1403
2059
|
const key = `${primitive.type}:${primitive.name}`;
|
|
1404
2060
|
const status = statusByKey.get(key);
|
|
1405
2061
|
const namespacedName = status?.namespacedName
|
|
1406
|
-
?? harness.adapter.getNamespacedName(kit.
|
|
2062
|
+
?? harness.adapter.getNamespacedName(kit.installAs, primitive.name);
|
|
1407
2063
|
const installedPath = status?.destination
|
|
1408
2064
|
?? (primitive.type === "mcp" ? mcpConfigPath : "");
|
|
1409
2065
|
const installed = {
|
|
@@ -1424,14 +2080,14 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1424
2080
|
}
|
|
1425
2081
|
}
|
|
1426
2082
|
if (primitive.type === "mcp") {
|
|
1427
|
-
const assignment = mcpAssignments?.get(buildPrimitiveKey(kit.
|
|
2083
|
+
const assignment = mcpAssignments?.get(buildPrimitiveKey(kit.installAs, primitive.sourcePath));
|
|
1428
2084
|
if (assignment) {
|
|
1429
2085
|
installed.instanceName = assignment.instanceName;
|
|
1430
2086
|
installed.configHash = assignment.configHash;
|
|
1431
2087
|
}
|
|
1432
2088
|
}
|
|
1433
2089
|
if (primitive.type === "hooks") {
|
|
1434
|
-
const assignment = hookAssignments?.get(buildPrimitiveKey(kit.
|
|
2090
|
+
const assignment = hookAssignments?.get(buildPrimitiveKey(kit.installAs, primitive.sourcePath));
|
|
1435
2091
|
if (assignment) {
|
|
1436
2092
|
installed.instanceName = assignment.instanceName;
|
|
1437
2093
|
installed.configHash = assignment.configHash;
|
|
@@ -1440,69 +2096,71 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1440
2096
|
return installed;
|
|
1441
2097
|
});
|
|
1442
2098
|
const installedKit = {
|
|
2099
|
+
name: kit.name,
|
|
2100
|
+
installAs: kit.installAs,
|
|
1443
2101
|
version: kit.manifest.version,
|
|
1444
2102
|
installedAt: new Date().toISOString(),
|
|
1445
|
-
source,
|
|
2103
|
+
source: kit.source,
|
|
1446
2104
|
primitives: installedPrimitives,
|
|
1447
2105
|
};
|
|
1448
|
-
|
|
2106
|
+
lockfile.harnesses[harness.name].kits[kit.installAs] = installedKit;
|
|
1449
2107
|
if (mcpAssignments && mcpInfoByPrimitiveKey.size > 0) {
|
|
1450
|
-
const harnessEntry =
|
|
2108
|
+
const harnessEntry = lockfile.harnesses[harness.name];
|
|
1451
2109
|
if (!harnessEntry.mcpInstances) {
|
|
1452
2110
|
harnessEntry.mcpInstances = {};
|
|
1453
2111
|
}
|
|
1454
2112
|
for (const primitive of resolvedPrimitives) {
|
|
1455
2113
|
if (primitive.type !== "mcp")
|
|
1456
2114
|
continue;
|
|
1457
|
-
const assignment = mcpAssignments.get(buildPrimitiveKey(kit.
|
|
2115
|
+
const assignment = mcpAssignments.get(buildPrimitiveKey(kit.installAs, primitive.sourcePath));
|
|
1458
2116
|
if (!assignment)
|
|
1459
2117
|
continue;
|
|
1460
2118
|
const instanceName = assignment.instanceName;
|
|
1461
2119
|
const existingInstance = harnessEntry.mcpInstances[instanceName];
|
|
1462
2120
|
if (existingInstance) {
|
|
1463
|
-
if (!existingInstance.usedBy.includes(kit.
|
|
1464
|
-
existingInstance.usedBy.push(kit.
|
|
2121
|
+
if (!existingInstance.usedBy.includes(kit.installAs)) {
|
|
2122
|
+
existingInstance.usedBy.push(kit.installAs);
|
|
1465
2123
|
}
|
|
1466
2124
|
continue;
|
|
1467
2125
|
}
|
|
1468
|
-
const info = mcpInfoByPrimitiveKey.get(buildPrimitiveKey(kit.
|
|
2126
|
+
const info = mcpInfoByPrimitiveKey.get(buildPrimitiveKey(kit.installAs, primitive.sourcePath));
|
|
1469
2127
|
if (!info)
|
|
1470
2128
|
continue;
|
|
1471
2129
|
harnessEntry.mcpInstances[instanceName] = {
|
|
1472
2130
|
version: info.version,
|
|
1473
2131
|
configHash: assignment.configHash,
|
|
1474
|
-
usedBy: [kit.
|
|
2132
|
+
usedBy: [kit.installAs],
|
|
1475
2133
|
sourcePrimitive: info.primitiveName,
|
|
1476
2134
|
installedAt: new Date().toISOString(),
|
|
1477
2135
|
};
|
|
1478
2136
|
}
|
|
1479
2137
|
}
|
|
1480
2138
|
if (hookAssignments && hookInfoByPrimitiveKey.size > 0) {
|
|
1481
|
-
const harnessEntry =
|
|
2139
|
+
const harnessEntry = lockfile.harnesses[harness.name];
|
|
1482
2140
|
if (!harnessEntry.hookInstances) {
|
|
1483
2141
|
harnessEntry.hookInstances = {};
|
|
1484
2142
|
}
|
|
1485
2143
|
for (const primitive of resolvedPrimitives) {
|
|
1486
2144
|
if (primitive.type !== "hooks")
|
|
1487
2145
|
continue;
|
|
1488
|
-
const assignment = hookAssignments.get(buildPrimitiveKey(kit.
|
|
2146
|
+
const assignment = hookAssignments.get(buildPrimitiveKey(kit.installAs, primitive.sourcePath));
|
|
1489
2147
|
if (!assignment)
|
|
1490
2148
|
continue;
|
|
1491
2149
|
const instanceName = assignment.instanceName;
|
|
1492
2150
|
const existingInstance = harnessEntry.hookInstances[instanceName];
|
|
1493
2151
|
if (existingInstance) {
|
|
1494
|
-
if (!existingInstance.usedBy.includes(kit.
|
|
1495
|
-
existingInstance.usedBy.push(kit.
|
|
2152
|
+
if (!existingInstance.usedBy.includes(kit.installAs)) {
|
|
2153
|
+
existingInstance.usedBy.push(kit.installAs);
|
|
1496
2154
|
}
|
|
1497
2155
|
continue;
|
|
1498
2156
|
}
|
|
1499
|
-
const info = hookInfoByPrimitiveKey.get(buildPrimitiveKey(kit.
|
|
2157
|
+
const info = hookInfoByPrimitiveKey.get(buildPrimitiveKey(kit.installAs, primitive.sourcePath));
|
|
1500
2158
|
if (!info)
|
|
1501
2159
|
continue;
|
|
1502
2160
|
harnessEntry.hookInstances[instanceName] = {
|
|
1503
2161
|
version: info.version,
|
|
1504
2162
|
configHash: assignment.configHash,
|
|
1505
|
-
usedBy: [kit.
|
|
2163
|
+
usedBy: [kit.installAs],
|
|
1506
2164
|
sourcePrimitive: info.primitiveName,
|
|
1507
2165
|
installedAt: new Date().toISOString(),
|
|
1508
2166
|
};
|
|
@@ -1511,6 +2169,7 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1511
2169
|
// Build result for output
|
|
1512
2170
|
kitResults.push({
|
|
1513
2171
|
name: kit.name,
|
|
2172
|
+
installAs: kit.installAs,
|
|
1514
2173
|
version: kit.manifest.version,
|
|
1515
2174
|
installed: installResult.installedPrimitives.map((p) => {
|
|
1516
2175
|
const primitiveOutput = {
|
|
@@ -1526,7 +2185,7 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1526
2185
|
}),
|
|
1527
2186
|
});
|
|
1528
2187
|
if (isInteractive) {
|
|
1529
|
-
logInfo(`${formatKit(kit
|
|
2188
|
+
logInfo(`${formatKit(getKitDisplayName(kit), kit.manifest.version)} ${pc.green("✓")}`);
|
|
1530
2189
|
}
|
|
1531
2190
|
}
|
|
1532
2191
|
if (kitResults.length > 0) {
|
|
@@ -1537,20 +2196,20 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1537
2196
|
});
|
|
1538
2197
|
}
|
|
1539
2198
|
}
|
|
1540
|
-
// Write
|
|
2199
|
+
// Write lockfile
|
|
1541
2200
|
try {
|
|
1542
|
-
await
|
|
2201
|
+
await writeLockfile(lockfile, scope, scope === "project" ? projectRoot : undefined);
|
|
1543
2202
|
}
|
|
1544
2203
|
catch (error) {
|
|
1545
2204
|
return {
|
|
1546
2205
|
success: false,
|
|
1547
|
-
error: `Failed to write
|
|
2206
|
+
error: `Failed to write lockfile: ${error instanceof Error ? error.message : String(error)}`,
|
|
1548
2207
|
};
|
|
1549
2208
|
}
|
|
1550
2209
|
// Build output
|
|
1551
2210
|
const output = {
|
|
1552
2211
|
success: true,
|
|
1553
|
-
source,
|
|
2212
|
+
source: outputSource,
|
|
1554
2213
|
scope,
|
|
1555
2214
|
harnesses: harnessResults,
|
|
1556
2215
|
skipped: skippedResults,
|
|
@@ -1598,7 +2257,7 @@ async function executeInstallation(kits, harnesses, discovery, source, options)
|
|
|
1598
2257
|
/**
|
|
1599
2258
|
* Resolve inline primitives only (when no primitives registry exists).
|
|
1600
2259
|
*/
|
|
1601
|
-
function resolveInlinePrimitivesOnly(kit, localPath) {
|
|
2260
|
+
function resolveInlinePrimitivesOnly(kit, localPath, kitInstanceName) {
|
|
1602
2261
|
const resolved = [];
|
|
1603
2262
|
const kitBasePath = path.join(localPath, kit.path);
|
|
1604
2263
|
for (const [type, primitives] of Object.entries(kit.manifest.primitives)) {
|
|
@@ -1608,7 +2267,7 @@ function resolveInlinePrimitivesOnly(kit, localPath) {
|
|
|
1608
2267
|
// Only handle inline primitives
|
|
1609
2268
|
if ("name" in primitive && "entrypoint" in primitive) {
|
|
1610
2269
|
resolved.push({
|
|
1611
|
-
kitName:
|
|
2270
|
+
kitName: kitInstanceName,
|
|
1612
2271
|
name: primitive.name,
|
|
1613
2272
|
type: type,
|
|
1614
2273
|
sourcePath: path.resolve(kitBasePath, primitive.entrypoint),
|