@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.
Files changed (110) hide show
  1. package/README.md +175 -34
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +29 -9
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/cli/install.d.ts.map +1 -1
  6. package/dist/cli/install.js +820 -161
  7. package/dist/cli/install.js.map +1 -1
  8. package/dist/cli/list.d.ts.map +1 -1
  9. package/dist/cli/list.js +8 -6
  10. package/dist/cli/list.js.map +1 -1
  11. package/dist/cli/types.d.ts +17 -0
  12. package/dist/cli/types.d.ts.map +1 -1
  13. package/dist/cli/types.js +1 -0
  14. package/dist/cli/types.js.map +1 -1
  15. package/dist/cli/uninstall.d.ts.map +1 -1
  16. package/dist/cli/uninstall.js +300 -38
  17. package/dist/cli/uninstall.js.map +1 -1
  18. package/dist/cli/upgrade.d.ts.map +1 -1
  19. package/dist/cli/upgrade.js +334 -105
  20. package/dist/cli/upgrade.js.map +1 -1
  21. package/dist/cli/validate.js +5 -5
  22. package/dist/cli/validate.js.map +1 -1
  23. package/dist/core/types.d.ts +4 -4
  24. package/dist/core/upgrade-executor.d.ts +7 -7
  25. package/dist/core/upgrade-executor.d.ts.map +1 -1
  26. package/dist/core/upgrade-executor.js +17 -17
  27. package/dist/core/upgrade-executor.js.map +1 -1
  28. package/dist/discovery/kit-scanner.d.ts +3 -3
  29. package/dist/discovery/kit-scanner.js +10 -10
  30. package/dist/discovery/kit-scanner.js.map +1 -1
  31. package/dist/discovery/types.d.ts +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +3 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/lockfile/index.d.ts +79 -0
  37. package/dist/lockfile/index.d.ts.map +1 -0
  38. package/dist/lockfile/index.js +203 -0
  39. package/dist/lockfile/index.js.map +1 -0
  40. package/dist/lockfile/read.d.ts +32 -0
  41. package/dist/lockfile/read.d.ts.map +1 -0
  42. package/dist/lockfile/read.js +88 -0
  43. package/dist/lockfile/read.js.map +1 -0
  44. package/dist/lockfile/types.d.ts +126 -0
  45. package/dist/lockfile/types.d.ts.map +1 -0
  46. package/dist/lockfile/types.js +44 -0
  47. package/dist/lockfile/types.js.map +1 -0
  48. package/dist/{manifest → lockfile}/upgrade-check.d.ts +5 -5
  49. package/dist/lockfile/upgrade-check.d.ts.map +1 -0
  50. package/dist/{manifest → lockfile}/upgrade-check.js +11 -9
  51. package/dist/lockfile/upgrade-check.js.map +1 -0
  52. package/dist/lockfile/utils.d.ts +35 -0
  53. package/dist/lockfile/utils.d.ts.map +1 -0
  54. package/dist/lockfile/utils.js +57 -0
  55. package/dist/lockfile/utils.js.map +1 -0
  56. package/dist/lockfile/write.d.ts +44 -0
  57. package/dist/lockfile/write.d.ts.map +1 -0
  58. package/dist/lockfile/write.js +77 -0
  59. package/dist/lockfile/write.js.map +1 -0
  60. package/dist/manifest/hash.d.ts +7 -0
  61. package/dist/manifest/hash.d.ts.map +1 -0
  62. package/dist/manifest/hash.js +30 -0
  63. package/dist/manifest/hash.js.map +1 -0
  64. package/dist/manifest/index.d.ts +8 -77
  65. package/dist/manifest/index.d.ts.map +1 -1
  66. package/dist/manifest/index.js +6 -197
  67. package/dist/manifest/index.js.map +1 -1
  68. package/dist/manifest/read.d.ts +4 -24
  69. package/dist/manifest/read.d.ts.map +1 -1
  70. package/dist/manifest/read.js +22 -46
  71. package/dist/manifest/read.js.map +1 -1
  72. package/dist/manifest/types.d.ts +17 -109
  73. package/dist/manifest/types.d.ts.map +1 -1
  74. package/dist/manifest/types.js +8 -37
  75. package/dist/manifest/types.js.map +1 -1
  76. package/dist/manifest/utils.d.ts +1 -27
  77. package/dist/manifest/utils.d.ts.map +1 -1
  78. package/dist/manifest/utils.js +1 -28
  79. package/dist/manifest/utils.js.map +1 -1
  80. package/dist/manifest/write.d.ts +3 -34
  81. package/dist/manifest/write.d.ts.map +1 -1
  82. package/dist/manifest/write.js +5 -45
  83. package/dist/manifest/write.js.map +1 -1
  84. package/dist/resolution/index.d.ts +1 -1
  85. package/dist/resolution/index.d.ts.map +1 -1
  86. package/dist/resolution/index.js +23 -13
  87. package/dist/resolution/index.js.map +1 -1
  88. package/dist/resolution/primitives-registry.d.ts +1 -1
  89. package/dist/resolution/primitives-registry.js +2 -2
  90. package/dist/resolution/primitives-registry.js.map +1 -1
  91. package/dist/tui/kit-select.d.ts.map +1 -1
  92. package/dist/tui/kit-select.js +4 -5
  93. package/dist/tui/kit-select.js.map +1 -1
  94. package/dist/tui/types.d.ts +1 -0
  95. package/dist/tui/types.d.ts.map +1 -1
  96. package/dist/tui/upgrade-select.d.ts +1 -1
  97. package/dist/tui/upgrade-select.d.ts.map +1 -1
  98. package/dist/tui/upgrade-select.js +15 -4
  99. package/dist/tui/upgrade-select.js.map +1 -1
  100. package/dist/validation/validate-kits.d.ts +2 -2
  101. package/dist/validation/validate-kits.js +4 -4
  102. package/dist/validation/validate-kits.js.map +1 -1
  103. package/package.json +1 -1
  104. package/schemas/kit.schema.json +2 -2
  105. package/schemas/{manifest.schema.json → kits-lock.schema.json} +34 -8
  106. package/schemas/kits-manifest.schema.json +95 -0
  107. package/schemas/{kits.schema.json → kits-registry.schema.json} +3 -3
  108. package/schemas/{primitives.schema.json → primitives-registry.schema.json} +2 -2
  109. package/dist/manifest/upgrade-check.d.ts.map +0 -1
  110. package/dist/manifest/upgrade-check.js.map +0 -1
@@ -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 { readManifest } from "../manifest/read.js";
16
- import { writeManifest } from "../manifest/write.js";
17
- import { createEmptyManifest, } from "../manifest/types.js";
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
- let fetchResult;
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
- const projectRoot = process.cwd();
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
- // Fetch source
140
- const fetchSpinner = isInteractive
141
- ? startSpinner(`Fetching ${formatSource(source)}...`)
142
- : undefined;
143
- if (!fetchSpinner && options.verbose) {
144
- console.error(`Fetching ${source}...`);
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
- try {
147
- fetchResult = await fetchSource(source);
148
- if (fetchSpinner) {
149
- stopSpinnerSuccess(fetchSpinner, `Fetched ${formatSource(source)}`);
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(`Fetched to ${fetchResult.localPath}`);
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
- catch (error) {
161
- if (fetchSpinner) {
162
- stopSpinnerError(fetchSpinner, "Failed to fetch source");
308
+ if (manifestWarning) {
309
+ if (isInteractive) {
310
+ logInfo(manifestWarning);
163
311
  }
164
- if (error instanceof NoFetcherError || error instanceof SourceParseError) {
165
- const message = error.message;
166
- if (isInteractive) {
167
- logError(message);
168
- outroError("Installation failed");
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.json) {
171
- console.log(JSON.stringify({ success: false, error: message }));
350
+ else if (options.verbose) {
351
+ console.error(`Fetched to ${localFetch.localPath}`);
172
352
  }
173
- else {
174
- console.error(`Error: ${message}`);
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
- return { success: false, exitCode: ExitCode.SourceNotFound, error: message };
377
+ throw error;
177
378
  }
178
- throw error;
179
- }
180
- // Scan for kits
181
- const scanSpinner = isInteractive ? startSpinner("Scanning for kits...") : undefined;
182
- let discovery = await scanKits(fetchResult.localPath, source);
183
- if (scanSpinner) {
184
- const kitCount = discovery.kits.length;
185
- stopSpinnerSuccess(scanSpinner, `Found ${formatCount(kitCount, "kit")}`);
186
- }
187
- void debugLog({
188
- level: "info",
189
- event: "install.scan.success",
190
- data: { kits: discovery.kits.map((kit) => kit.name) },
191
- });
192
- // Filter by requested kit names
193
- if (options.kit && options.kit.length > 0) {
194
- const missing = getDiscoveryMissingKitNames(discovery, options.kit);
195
- if (missing.length > 0) {
196
- const message = `Kit${missing.length === 1 ? "" : "s"} not found: ${missing.join(", ")}`;
197
- if (isInteractive) {
198
- displayMissingKits(missing, discovery.kits);
199
- outroError("Installation failed");
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
- else if (options.json) {
202
- console.log(JSON.stringify({ success: false, error: message }));
418
+ const kitIndex = new Map();
419
+ for (const kit of discovery.kits) {
420
+ kitIndex.set(kit.name.toLowerCase(), kit);
203
421
  }
204
- else {
205
- console.error(`Error: ${message}`);
206
- console.error("Available kits: " + discovery.kits.map((k) => k.name).join(", "));
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
- return { success: false, exitCode: ExitCode.KitNotFound, error: message };
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
- discovery = filterDiscoveryKitsByName(discovery, options.kit);
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
- displayKitsJson(discovery.kits, discovery.source, discovery.repository);
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(discovery.kits);
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 (discovery.kits.length === 0) {
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
- let selectedKits = discovery.kits;
239
- if (isInteractive && !options.kit) {
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(discovery.kits, result.selected);
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 (options.agent && options.agent.length > 0) {
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
- let matrices = buildCompatibilityMatrix(selectedKits, compatibilityHarnesses);
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 (isInteractive) {
356
- const compat = await runCompatibilityCheck(selectedKits, compatibilityHarnesses, effectiveScope, {
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.name));
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.name));
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.name, version: kit.manifest.version })), planHarnesses.map((h) => ({ displayName: h.displayName })), effectiveScope);
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.name, version: k.manifest.version })), selectedHarnesses.map((h) => ({ name: h.name, displayName: h.displayName })), { scope: effectiveScope, projectRoot });
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 installResults = await executeInstallation(selectedKits, selectedHarnesses, discovery, source, {
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: options.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 source
518
- if (fetchResult?.cleanup) {
980
+ // Clean up fetched sources
981
+ for (const fetched of fetchResults) {
982
+ if (!fetched.cleanup)
983
+ continue;
519
984
  try {
520
- await fetchResult.cleanup();
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, discovery, source, options) {
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(discovery.localPath, kit.path);
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.name,
1516
+ kit: getKitDisplayName(kit),
882
1517
  errors: resolution.errors.map((e) => e.message),
883
1518
  });
884
1519
  continue;
885
1520
  }
886
- resolvedByKit.set(kit.name, resolution.primitives);
1521
+ resolvedByKit.set(kit.installAs, resolution.primitives);
887
1522
  allResolvedPrimitives.push(...resolution.primitives);
888
1523
  }
889
1524
  else {
890
- const inlinePrimitives = resolveInlinePrimitivesOnly(kit, discovery.localPath);
891
- resolvedByKit.set(kit.name, inlinePrimitives);
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
- // Read or create manifest for the specified scope
998
- let manifest = await readManifest(scope, scope === "project" ? projectRoot : undefined);
999
- if (!manifest) {
1000
- manifest = createEmptyManifest(scope, scope === "project" ? projectRoot : undefined);
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.name))
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.name) ?? [];
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.name, primitive.sourcePath);
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.name });
1046
- entry.kits.add(kit.name);
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 = manifest.harnesses[harness.name]?.mcpInstances ?? {};
1063
- const usedInstanceNames = new Set(Object.keys(existingInstances));
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
- if (existing) {
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.name))
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.name) ?? [];
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.name, primitive.sourcePath);
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.name });
1186
- entry.kits.add(kit.name);
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 = manifest.harnesses[harness.name]?.hookInstances ?? {};
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
- if (existing) {
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.name))
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.name);
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.name}`,
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.name, primitive.sourcePath));
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.name, primitive.sourcePath));
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(kit.manifest, primitivesForAdapter, installOptions);
1996
+ const installResult = await harness.adapter.install(installManifest, primitivesForAdapter, installOptions);
1342
1997
  if (!installResult.success) {
1343
1998
  skippedResults.push({
1344
- kit: kit.name,
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.name, primitive.name);
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.name, primitive.name);
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 manifest
1395
- if (!manifest.harnesses[harness.name]) {
1396
- manifest.harnesses[harness.name] = { kits: {} };
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.name, primitive.name);
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.name, primitive.sourcePath));
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.name, primitive.sourcePath));
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
- manifest.harnesses[harness.name].kits[kit.name] = installedKit;
2106
+ lockfile.harnesses[harness.name].kits[kit.installAs] = installedKit;
1449
2107
  if (mcpAssignments && mcpInfoByPrimitiveKey.size > 0) {
1450
- const harnessEntry = manifest.harnesses[harness.name];
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.name, primitive.sourcePath));
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.name)) {
1464
- existingInstance.usedBy.push(kit.name);
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.name, primitive.sourcePath));
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.name],
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 = manifest.harnesses[harness.name];
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.name, primitive.sourcePath));
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.name)) {
1495
- existingInstance.usedBy.push(kit.name);
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.name, primitive.sourcePath));
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.name],
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.name, kit.manifest.version)} ${pc.green("✓")}`);
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 manifest
2199
+ // Write lockfile
1541
2200
  try {
1542
- await writeManifest(manifest, scope, scope === "project" ? projectRoot : undefined);
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 manifest: ${error instanceof Error ? error.message : String(error)}`,
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: kit.name,
2270
+ kitName: kitInstanceName,
1612
2271
  name: primitive.name,
1613
2272
  type: type,
1614
2273
  sourcePath: path.resolve(kitBasePath, primitive.entrypoint),