@hashicorp/kits 0.1.11 → 0.1.14

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 (64) hide show
  1. package/README.md +14 -1
  2. package/dist/adapters/base-adapter.d.ts.map +1 -1
  3. package/dist/adapters/base-adapter.js +6 -1
  4. package/dist/adapters/base-adapter.js.map +1 -1
  5. package/dist/adapters/gemini-cli/index.d.ts.map +1 -1
  6. package/dist/adapters/gemini-cli/index.js +2 -1
  7. package/dist/adapters/gemini-cli/index.js.map +1 -1
  8. package/dist/adapters/github-copilot/index.d.ts.map +1 -1
  9. package/dist/adapters/github-copilot/index.js +2 -1
  10. package/dist/adapters/github-copilot/index.js.map +1 -1
  11. package/dist/adapters/github-copilot/installer.js +1 -1
  12. package/dist/adapters/github-copilot/installer.js.map +1 -1
  13. package/dist/adapters/opencode/index.d.ts.map +1 -1
  14. package/dist/adapters/opencode/index.js +2 -1
  15. package/dist/adapters/opencode/index.js.map +1 -1
  16. package/dist/cli/install.d.ts.map +1 -1
  17. package/dist/cli/install.js +101 -3
  18. package/dist/cli/install.js.map +1 -1
  19. package/dist/cli/upgrade.d.ts.map +1 -1
  20. package/dist/cli/upgrade.js +476 -1
  21. package/dist/cli/upgrade.js.map +1 -1
  22. package/dist/core/hook-input.d.ts +28 -0
  23. package/dist/core/hook-input.d.ts.map +1 -0
  24. package/dist/core/hook-input.js +86 -0
  25. package/dist/core/hook-input.js.map +1 -0
  26. package/dist/core/hook-instance.d.ts +4 -0
  27. package/dist/core/hook-instance.d.ts.map +1 -1
  28. package/dist/core/hook-instance.js +32 -0
  29. package/dist/core/hook-instance.js.map +1 -1
  30. package/dist/core/mcp-instance.d.ts +4 -0
  31. package/dist/core/mcp-instance.d.ts.map +1 -1
  32. package/dist/core/mcp-instance.js +33 -0
  33. package/dist/core/mcp-instance.js.map +1 -1
  34. package/dist/core/types.d.ts +1 -1
  35. package/dist/core/types.d.ts.map +1 -1
  36. package/dist/core/upgrade-executor.d.ts +6 -1
  37. package/dist/core/upgrade-executor.d.ts.map +1 -1
  38. package/dist/core/upgrade-executor.js +107 -7
  39. package/dist/core/upgrade-executor.js.map +1 -1
  40. package/dist/lockfile/types.d.ts +2 -0
  41. package/dist/lockfile/types.d.ts.map +1 -1
  42. package/dist/lockfile/types.js.map +1 -1
  43. package/dist/tui/hook-prompt.d.ts +23 -0
  44. package/dist/tui/hook-prompt.d.ts.map +1 -0
  45. package/dist/tui/hook-prompt.js +239 -0
  46. package/dist/tui/hook-prompt.js.map +1 -0
  47. package/dist/tui/index.d.ts +2 -1
  48. package/dist/tui/index.d.ts.map +1 -1
  49. package/dist/tui/index.js +3 -1
  50. package/dist/tui/index.js.map +1 -1
  51. package/dist/tui/upgrade.d.ts +9 -0
  52. package/dist/tui/upgrade.d.ts.map +1 -1
  53. package/dist/tui/upgrade.js +29 -0
  54. package/dist/tui/upgrade.js.map +1 -1
  55. package/dist/validation/validate-subagents.d.ts +0 -1
  56. package/dist/validation/validate-subagents.d.ts.map +1 -1
  57. package/dist/validation/validate-subagents.js +0 -8
  58. package/dist/validation/validate-subagents.js.map +1 -1
  59. package/package.json +1 -1
  60. package/schemas/hook-program.schema.v1.0.0.json +50 -3
  61. package/schemas/kit.schema.v1.0.0.json +2 -2
  62. package/schemas/kits-lock.schema.v1.0.0.json +8 -0
  63. package/schemas/kits-registry.schema.v1.0.0.json +3 -3
  64. package/schemas/subagent.schema.v1.0.0.json +4 -0
@@ -4,6 +4,8 @@
4
4
  * Checks for and applies primitive upgrades.
5
5
  */
6
6
  import * as clack from "@clack/prompts";
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
7
9
  import pc from "picocolors";
8
10
  import { ExitCode, } from "./types.js";
9
11
  import { readManifest, writeManifest, getManifestPath } from "../manifest/index.js";
@@ -13,8 +15,14 @@ import { writeLockfile } from "../lockfile/write.js";
13
15
  import { checkForUpgrades } from "../lockfile/upgrade-check.js";
14
16
  import { fetchSource, scanKits, NoFetcherError, SourceParseError, } from "../discovery/index.js";
15
17
  import { PrimitivesRegistryLoader } from "../resolution/primitives-registry.js";
18
+ import { resolveRegistryPrimitivePath } from "../resolution/primitive-paths.js";
19
+ import { resolveEnvVarsFromConfig } from "../resolution/index.js";
20
+ import { hashMcpConfig, hashMcpInputSchema } from "../core/mcp-instance.js";
21
+ import { hashHookProgram, hashHookInputSchema } from "../core/hook-instance.js";
22
+ import { buildHookInputDefaults, buildHookDefaultArgMap, buildHookDefaultEnvMap, } from "../core/hook-input.js";
16
23
  import { toUpgradeDisplayEntry, selectUpgrades, displayUpgradeCheck, displayNoUpgrades, upgradesToJson, filterUpgradesByHarness, getUpgradeGroups, } from "../tui/upgrade-select.js";
17
24
  import { renderUpgradeApplyStart, renderUpgradeApplyResults, renderUpgradeProcessComplete, renderUpgradeError, } from "../tui/upgrade.js";
25
+ import { promptForEnvVars, promptForHookInputs, promptForUpgradeConfigUpdates, } from "../tui/index.js";
18
26
  import { globalRegistry } from "../adapters/registry.js";
19
27
  import { executeUpgrades } from "../core/upgrade-executor.js";
20
28
  import { debugLog, enableDebugLogging, getDebugLogPath, checkDebugLogWritable, } from "../core/debug.js";
@@ -48,6 +56,238 @@ function filterLockfileBySource(lockfile, source) {
48
56
  }
49
57
  return filtered;
50
58
  }
59
+ function upgradeKey(check) {
60
+ return `${check.harness}:${check.kit}:${check.type}:${check.primitive}`;
61
+ }
62
+ async function loadJsonFile(filePath) {
63
+ try {
64
+ const content = await fs.readFile(filePath, "utf-8");
65
+ return JSON.parse(content);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ async function resolvePrimitiveConfigPath(registry, type, name, version) {
72
+ const entry = await registry.getPrimitive(type, name);
73
+ if (!entry) {
74
+ return null;
75
+ }
76
+ const versionEntry = entry.versions.find((v) => v.version === version);
77
+ if (!versionEntry) {
78
+ return null;
79
+ }
80
+ let basePath = versionEntry.path;
81
+ if (!path.isAbsolute(basePath)) {
82
+ basePath = entry.path
83
+ ? path.join(registry.getBasePath(), entry.path, basePath)
84
+ : path.join(registry.getBasePath(), basePath);
85
+ }
86
+ const configPath = await resolveRegistryPrimitivePath(type, name, basePath);
87
+ return { configPath, basePath };
88
+ }
89
+ function detectNewRequiredEnv(oldDefs = {}, newDefs = {}) {
90
+ const required = [];
91
+ for (const [name, def] of Object.entries(newDefs)) {
92
+ if (!def.required || def.sensitive || def.default !== undefined) {
93
+ continue;
94
+ }
95
+ const oldDef = oldDefs[name];
96
+ const oldRequired = oldDef?.required ?? false;
97
+ const oldDefault = oldDef?.default;
98
+ if (!oldDef || !oldRequired || oldDefault !== undefined) {
99
+ required.push(name);
100
+ }
101
+ }
102
+ return required;
103
+ }
104
+ function buildEnvVarRequirements(resolutions) {
105
+ const requirements = [];
106
+ const seen = new Set();
107
+ for (const resolution of resolutions) {
108
+ for (const resolved of resolution.resolved) {
109
+ if (seen.has(resolved.name))
110
+ continue;
111
+ seen.add(resolved.name);
112
+ requirements.push({
113
+ name: resolved.name,
114
+ description: resolved.description,
115
+ required: resolved.required,
116
+ sensitive: resolved.sensitive,
117
+ defaultValue: resolved.defaultValue,
118
+ currentValue: process.env[resolved.name],
119
+ source: resolution.mcpServer,
120
+ });
121
+ }
122
+ }
123
+ return requirements;
124
+ }
125
+ function applyPromptResultsToResolutions(resolutions, promptResults) {
126
+ for (const result of promptResults.variables) {
127
+ for (const resolution of resolutions) {
128
+ const resolved = resolution.resolved.find((r) => r.name === result.name);
129
+ if (!resolved)
130
+ continue;
131
+ switch (result.choice) {
132
+ case "use-existing": {
133
+ if ("value" in resolved) {
134
+ delete resolved.value;
135
+ }
136
+ resolved.usePassthrough = true;
137
+ resolved.source = "environment";
138
+ break;
139
+ }
140
+ case "use-default": {
141
+ if (result.value !== undefined) {
142
+ resolved.value = result.value;
143
+ }
144
+ else if ("value" in resolved) {
145
+ delete resolved.value;
146
+ }
147
+ resolved.usePassthrough = false;
148
+ resolved.source = "default";
149
+ break;
150
+ }
151
+ case "enter-new": {
152
+ if (result.value !== undefined) {
153
+ resolved.value = result.value;
154
+ }
155
+ else if ("value" in resolved) {
156
+ delete resolved.value;
157
+ }
158
+ resolved.usePassthrough = false;
159
+ resolved.source = "user-input";
160
+ break;
161
+ }
162
+ case "skip": {
163
+ if ("value" in resolved) {
164
+ delete resolved.value;
165
+ }
166
+ resolved.usePassthrough = true;
167
+ resolved.source = "passthrough";
168
+ break;
169
+ }
170
+ default:
171
+ break;
172
+ }
173
+ const idx = resolution.missingRequired.indexOf(result.name);
174
+ if (idx !== -1) {
175
+ resolution.missingRequired.splice(idx, 1);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ async function buildMcpUpgradePlan(check, lockfile, registry) {
181
+ const kitEntry = lockfile.kits[check.kit];
182
+ const harnessEntry = kitEntry?.harnesses[check.harness];
183
+ const installed = harnessEntry?.primitives.mcp?.find((p) => p.name === check.primitive);
184
+ if (!installed?.instanceRef) {
185
+ return null;
186
+ }
187
+ const instanceRef = installed.instanceRef;
188
+ const prefix = `${check.harness}:`;
189
+ const instanceKey = instanceRef.startsWith(prefix) ? instanceRef : `${check.harness}:${instanceRef}`;
190
+ const instanceName = instanceKey.startsWith(prefix)
191
+ ? instanceKey.slice(prefix.length)
192
+ : instanceRef;
193
+ const existingInstance = lockfile.mcpInstances?.[instanceKey];
194
+ const oldConfig = existingInstance?.config;
195
+ const oldInputHash = existingInstance?.inputHash ?? (oldConfig ? hashMcpInputSchema(oldConfig) : undefined);
196
+ const resolved = await resolvePrimitiveConfigPath(registry, "mcp", check.primitive, check.availableVersion);
197
+ if (!resolved) {
198
+ return null;
199
+ }
200
+ const config = await loadJsonFile(resolved.configPath);
201
+ if (!config) {
202
+ return null;
203
+ }
204
+ const inputHash = hashMcpInputSchema(config);
205
+ const needsUpdate = !oldInputHash || oldInputHash !== inputHash;
206
+ const requiredInputs = needsUpdate
207
+ ? detectNewRequiredEnv(oldConfig?.env ?? {}, config.env ?? {})
208
+ : [];
209
+ const envResolution = resolveEnvVarsFromConfig(instanceName, config, {});
210
+ const configHash = hashMcpConfig(config, envResolution.resolved, envResolution.resolvedHeaders);
211
+ return {
212
+ kind: "mcp",
213
+ upgradeKey: upgradeKey(check),
214
+ harness: check.harness,
215
+ kit: check.kit,
216
+ primitive: check.primitive,
217
+ instanceName,
218
+ config,
219
+ envResolution,
220
+ inputHash,
221
+ configHash,
222
+ needsUpdate,
223
+ requiredInputs,
224
+ };
225
+ }
226
+ async function buildHookUpgradePlan(check, lockfile, registry) {
227
+ const kitEntry = lockfile.kits[check.kit];
228
+ const harnessEntry = kitEntry?.harnesses[check.harness];
229
+ const installed = harnessEntry?.primitives.hooks?.find((p) => p.name === check.primitive);
230
+ if (!installed?.instanceRef || !installed.binding) {
231
+ return null;
232
+ }
233
+ const instanceName = installed.instanceRef;
234
+ const existingInstance = lockfile.hookInstances?.[instanceName];
235
+ let oldInputHash = existingInstance?.inputHash;
236
+ if (!oldInputHash) {
237
+ const oldResolved = await resolvePrimitiveConfigPath(registry, "hooks", check.primitive, check.currentVersion);
238
+ if (oldResolved) {
239
+ const oldProgram = await loadJsonFile(oldResolved.configPath);
240
+ if (oldProgram) {
241
+ oldInputHash = hashHookInputSchema(oldProgram);
242
+ }
243
+ }
244
+ }
245
+ const resolved = await resolvePrimitiveConfigPath(registry, "hooks", check.primitive, check.availableVersion);
246
+ if (!resolved) {
247
+ return null;
248
+ }
249
+ const program = await loadJsonFile(resolved.configPath);
250
+ if (!program) {
251
+ return null;
252
+ }
253
+ const inputHash = hashHookInputSchema(program);
254
+ const needsUpdate = !oldInputHash || oldInputHash !== inputHash;
255
+ const binding = {
256
+ ...installed.binding,
257
+ ...(installed.binding.env ? { env: { ...installed.binding.env } } : {}),
258
+ ...(installed.binding.args ? { args: { ...installed.binding.args } } : {}),
259
+ };
260
+ const defaults = buildHookInputDefaults(program, binding);
261
+ const requiredInputs = needsUpdate ? defaults.missingRequiredEnv : [];
262
+ const programDir = path.dirname(resolved.configPath);
263
+ const entryPath = path.join(programDir, program.runtime.entrypoint);
264
+ let checksum = existingInstance?.checksum ?? "";
265
+ try {
266
+ const entryContents = await fs.readFile(entryPath, "utf-8");
267
+ checksum = hashHookProgram(program, entryContents);
268
+ }
269
+ catch {
270
+ if (!checksum) {
271
+ checksum = hashHookProgram(program);
272
+ }
273
+ }
274
+ return {
275
+ kind: "hooks",
276
+ upgradeKey: upgradeKey(check),
277
+ harness: check.harness,
278
+ kit: check.kit,
279
+ primitive: check.primitive,
280
+ instanceName,
281
+ displayName: program.name ?? check.primitive,
282
+ program,
283
+ binding,
284
+ defaults,
285
+ inputHash,
286
+ checksum,
287
+ needsUpdate,
288
+ requiredInputs,
289
+ };
290
+ }
51
291
  /**
52
292
  * Run the upgrade command.
53
293
  */
@@ -221,7 +461,7 @@ export async function runUpgrade(options) {
221
461
  const kitVersionsBySource = new Map();
222
462
  const registryBySource = new Map();
223
463
  const checkSourceMap = new Map();
224
- const checkKey = (check) => `${check.harness}:${check.kit}:${check.type}:${check.primitive}`;
464
+ const checkKey = upgradeKey;
225
465
  const fetchSpinner = isInteractive ? clack.spinner() : undefined;
226
466
  const fetchedSources = [];
227
467
  if (fetchSpinner) {
@@ -552,12 +792,208 @@ export async function runUpgrade(options) {
552
792
  existing.push(check);
553
793
  upgradesBySelectedSource.set(sourceKey, existing);
554
794
  }
795
+ const configPlans = [];
796
+ const configPlansByKey = new Map();
797
+ const configPlanErrors = [];
798
+ for (const check of selectedChecks) {
799
+ if (check.type !== "mcp" && check.type !== "hooks") {
800
+ continue;
801
+ }
802
+ const sourceKey = checkSourceMap.get(checkKey(check)) ?? options.source;
803
+ if (!sourceKey) {
804
+ continue;
805
+ }
806
+ const registryLoader = registryBySource.get(sourceKey);
807
+ if (!registryLoader) {
808
+ continue;
809
+ }
810
+ if (check.type === "mcp") {
811
+ const plan = await buildMcpUpgradePlan(check, lockfile, registryLoader);
812
+ if (!plan) {
813
+ configPlanErrors.push(`Unable to resolve MCP configuration for ${check.primitive} (${check.kit})`);
814
+ continue;
815
+ }
816
+ configPlans.push(plan);
817
+ configPlansByKey.set(plan.upgradeKey, plan);
818
+ }
819
+ else if (check.type === "hooks") {
820
+ const plan = await buildHookUpgradePlan(check, lockfile, registryLoader);
821
+ if (!plan) {
822
+ configPlanErrors.push(`Unable to resolve hook configuration for ${check.primitive} (${check.kit})`);
823
+ continue;
824
+ }
825
+ configPlans.push(plan);
826
+ configPlansByKey.set(plan.upgradeKey, plan);
827
+ }
828
+ }
829
+ if (configPlanErrors.length > 0) {
830
+ const message = configPlanErrors.join("; ");
831
+ if (isInteractive) {
832
+ clack.log.error(message);
833
+ clack.outro(pc.red("Upgrade failed"));
834
+ }
835
+ else if (options.json) {
836
+ console.log(JSON.stringify({ success: false, error: message }));
837
+ }
838
+ else {
839
+ console.error(message);
840
+ }
841
+ return { success: false, exitCode: ExitCode.InstallationFailed, error: message };
842
+ }
843
+ const plansNeedingUpdate = configPlans.filter((plan) => plan.needsUpdate);
844
+ if (plansNeedingUpdate.length > 0) {
845
+ const mcpNames = Array.from(new Set(plansNeedingUpdate
846
+ .filter((plan) => plan.kind === "mcp")
847
+ .map((plan) => plan.instanceName)));
848
+ const hookNames = Array.from(new Set(plansNeedingUpdate
849
+ .filter((plan) => plan.kind === "hooks")
850
+ .map((plan) => plan.displayName)));
851
+ const hasRequiredUpdates = plansNeedingUpdate.some((plan) => plan.requiredInputs.length > 0);
852
+ if (isInteractive) {
853
+ const configPrompt = await promptForUpgradeConfigUpdates({
854
+ mcpServers: mcpNames,
855
+ hooks: hookNames,
856
+ requireUpdate: hasRequiredUpdates,
857
+ });
858
+ if (configPrompt.cancelled) {
859
+ clack.outro(pc.yellow("Upgrade cancelled"));
860
+ return { success: true, exitCode: ExitCode.UserCancelled };
861
+ }
862
+ const shouldConfigure = hasRequiredUpdates || configPrompt.configure;
863
+ if (shouldConfigure) {
864
+ const mcpResolutions = plansNeedingUpdate
865
+ .filter((plan) => plan.kind === "mcp")
866
+ .map((plan) => plan.envResolution);
867
+ if (mcpResolutions.length > 0) {
868
+ const requirements = buildEnvVarRequirements(mcpResolutions);
869
+ if (requirements.length > 0) {
870
+ const promptResults = await promptForEnvVars(requirements);
871
+ if (promptResults.cancelled) {
872
+ clack.outro(pc.yellow("Upgrade cancelled"));
873
+ return { success: true, exitCode: ExitCode.UserCancelled };
874
+ }
875
+ applyPromptResultsToResolutions(mcpResolutions, promptResults);
876
+ }
877
+ }
878
+ const hookPlans = plansNeedingUpdate.filter((plan) => plan.kind === "hooks");
879
+ if (hookPlans.length > 0) {
880
+ const hookRequests = hookPlans.map((plan) => ({
881
+ key: plan.upgradeKey,
882
+ hookName: plan.displayName,
883
+ kitName: plan.kit,
884
+ defaults: plan.defaults,
885
+ }));
886
+ const promptResults = await promptForHookInputs(hookRequests);
887
+ if (promptResults.cancelled) {
888
+ clack.outro(pc.yellow("Upgrade cancelled"));
889
+ return { success: true, exitCode: ExitCode.UserCancelled };
890
+ }
891
+ for (const plan of hookPlans) {
892
+ const overrides = promptResults.results.get(plan.upgradeKey);
893
+ const resolvedEnv = overrides?.env ?? buildHookDefaultEnvMap(plan.defaults);
894
+ const resolvedArgs = overrides?.args ?? buildHookDefaultArgMap(plan.defaults);
895
+ const nextBinding = { ...plan.binding };
896
+ if (plan.defaults.env.length > 0) {
897
+ if (Object.keys(resolvedEnv).length > 0) {
898
+ nextBinding.env = resolvedEnv;
899
+ }
900
+ else {
901
+ delete nextBinding.env;
902
+ }
903
+ }
904
+ if (plan.defaults.args.length > 0) {
905
+ nextBinding.args = resolvedArgs;
906
+ }
907
+ plan.binding = nextBinding;
908
+ }
909
+ }
910
+ }
911
+ else {
912
+ for (const plan of plansNeedingUpdate) {
913
+ if (plan.kind !== "hooks") {
914
+ continue;
915
+ }
916
+ const resolvedEnv = buildHookDefaultEnvMap(plan.defaults);
917
+ const resolvedArgs = buildHookDefaultArgMap(plan.defaults);
918
+ const nextBinding = { ...plan.binding };
919
+ if (plan.defaults.env.length > 0) {
920
+ if (Object.keys(resolvedEnv).length > 0) {
921
+ nextBinding.env = resolvedEnv;
922
+ }
923
+ else {
924
+ delete nextBinding.env;
925
+ }
926
+ }
927
+ if (plan.defaults.args.length > 0) {
928
+ nextBinding.args = resolvedArgs;
929
+ }
930
+ plan.binding = nextBinding;
931
+ }
932
+ }
933
+ }
934
+ else if (hasRequiredUpdates) {
935
+ const details = plansNeedingUpdate
936
+ .map((plan) => {
937
+ const label = plan.kind === "mcp" ? plan.instanceName : plan.displayName;
938
+ return `${label}: ${plan.requiredInputs.join(", ")}`;
939
+ })
940
+ .join("; ");
941
+ const message = `Configuration updates required for upgraded primitives: ${details}. ` +
942
+ "Run without -y to configure updated inputs.";
943
+ if (options.json) {
944
+ console.log(JSON.stringify({ success: false, error: message }));
945
+ }
946
+ else {
947
+ console.error(message);
948
+ }
949
+ return { success: false, exitCode: ExitCode.EnvVarRequired, error: message };
950
+ }
951
+ else {
952
+ for (const plan of plansNeedingUpdate) {
953
+ if (plan.kind !== "hooks") {
954
+ continue;
955
+ }
956
+ const resolvedEnv = buildHookDefaultEnvMap(plan.defaults);
957
+ const resolvedArgs = buildHookDefaultArgMap(plan.defaults);
958
+ const nextBinding = { ...plan.binding };
959
+ if (plan.defaults.env.length > 0) {
960
+ if (Object.keys(resolvedEnv).length > 0) {
961
+ nextBinding.env = resolvedEnv;
962
+ }
963
+ else {
964
+ delete nextBinding.env;
965
+ }
966
+ }
967
+ if (plan.defaults.args.length > 0) {
968
+ nextBinding.args = resolvedArgs;
969
+ }
970
+ plan.binding = nextBinding;
971
+ }
972
+ }
973
+ for (const plan of plansNeedingUpdate) {
974
+ if (plan.kind === "mcp") {
975
+ plan.configHash = hashMcpConfig(plan.config, plan.envResolution.resolved, plan.envResolution.resolvedHeaders);
976
+ }
977
+ }
978
+ }
979
+ const configOverrides = new Map();
980
+ for (const plan of configPlans) {
981
+ if (plan.kind === "mcp") {
982
+ configOverrides.set(plan.upgradeKey, {
983
+ envResolutions: [plan.envResolution],
984
+ });
985
+ }
986
+ else if (plan.kind === "hooks") {
987
+ configOverrides.set(plan.upgradeKey, { hookBinding: plan.binding });
988
+ }
989
+ }
555
990
  const aggregateResult = {
556
991
  success: true,
557
992
  upgraded: [],
558
993
  failed: [],
559
994
  backupPaths: [],
560
995
  };
996
+ let configUpdatesApplied = false;
561
997
  if (isInteractive) {
562
998
  renderUpgradeApplyStart(selectedGroups.length);
563
999
  }
@@ -599,6 +1035,7 @@ export async function runUpgrade(options) {
599
1035
  }
600
1036
  },
601
1037
  scope,
1038
+ configOverrides,
602
1039
  ...(scope === "project" && projectRoot ? { projectRoot } : {}),
603
1040
  };
604
1041
  const result = await executeUpgrades(checks, adapterMap, registryLoader, lockfile, upgradeOptions);
@@ -619,6 +1056,41 @@ export async function runUpgrade(options) {
619
1056
  aggregateResult.upgraded.push(...result.upgraded);
620
1057
  aggregateResult.failed.push(...result.failed);
621
1058
  aggregateResult.backupPaths.push(...result.backupPaths);
1059
+ for (const upgraded of result.upgraded) {
1060
+ const key = `${upgraded.harness}:${upgraded.kit}:${upgraded.type}:${upgraded.primitive}`;
1061
+ const plan = configPlansByKey.get(key);
1062
+ if (!plan) {
1063
+ continue;
1064
+ }
1065
+ if (plan.kind === "mcp") {
1066
+ const instanceKey = `${plan.harness}:${plan.instanceName}`;
1067
+ const existing = lockfile.mcpInstances?.[instanceKey];
1068
+ if (existing) {
1069
+ existing.config = plan.config;
1070
+ existing.configHash = plan.configHash;
1071
+ existing.inputHash = plan.inputHash;
1072
+ existing.version = upgraded.newVersion;
1073
+ existing.installedAt = new Date().toISOString();
1074
+ configUpdatesApplied = true;
1075
+ }
1076
+ }
1077
+ else if (plan.kind === "hooks") {
1078
+ const existing = lockfile.hookInstances?.[plan.instanceName];
1079
+ if (existing) {
1080
+ existing.checksum = plan.checksum;
1081
+ existing.inputHash = plan.inputHash;
1082
+ existing.version = upgraded.newVersion;
1083
+ configUpdatesApplied = true;
1084
+ }
1085
+ const kitEntry = lockfile.kits[plan.kit];
1086
+ const harnessEntry = kitEntry?.harnesses[plan.harness];
1087
+ const hookEntry = harnessEntry?.primitives.hooks?.find((entry) => entry.name === plan.primitive);
1088
+ if (hookEntry) {
1089
+ hookEntry.binding = plan.binding;
1090
+ configUpdatesApplied = true;
1091
+ }
1092
+ }
1093
+ }
622
1094
  }
623
1095
  void debugLog({
624
1096
  level: "info",
@@ -662,6 +1134,9 @@ export async function runUpgrade(options) {
662
1134
  await writeLockfile(lockfile, scope, scope === "project" ? projectRoot : undefined);
663
1135
  }
664
1136
  }
1137
+ if (configUpdatesApplied) {
1138
+ await writeLockfile(lockfile, scope, scope === "project" ? projectRoot : undefined);
1139
+ }
665
1140
  // Display results
666
1141
  if (isInteractive) {
667
1142
  const failuresByGroup = new Map();