infra-kit 0.1.113 → 0.1.115

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 (218) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-0d1515ce-4ebe-4ce8-a865-2897f68de168.jsonl +1 -0
  3. package/.omc/state/agent-replay-1fcfe0ec-7567-41c3-9bfd-501e99e86819.jsonl +19 -0
  4. package/.omc/state/agent-replay-38382db5-3088-4b5a-aff7-2561a60827c7.jsonl +17 -0
  5. package/.omc/state/agent-replay-50c36be9-a3a2-42b9-9c85-5a06d3e22f6a.jsonl +5 -0
  6. package/.omc/state/agent-replay-8198b3e5-fad6-4ee0-9c59-ef773ad89af4.jsonl +1 -0
  7. package/.omc/state/agent-replay-8cd7652a-b8b5-49cf-9b1f-bee8e5972458.jsonl +3 -0
  8. package/.omc/state/agent-replay-9bea518f-a9ee-4d14-bb53-a936ebaf647c.jsonl +7 -0
  9. package/.omc/state/agent-replay-b52d89fb-0e24-4901-bc66-27e3c1697126.jsonl +2 -0
  10. package/.omc/state/agent-replay-f581a1ae-0ee4-48a3-96f3-ce31a0633bb0.jsonl +1 -0
  11. package/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
  12. package/.omc/state/agent-replay-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
  13. package/.omc/state/idle-notif-cooldown.json +1 -1
  14. package/.omc/state/sessions/0d1515ce-4ebe-4ce8-a865-2897f68de168/pre-tool-advisory-throttle.json +10 -0
  15. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/last-tool-error-state.json +7 -0
  16. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/mission-state.json +109 -0
  17. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/pre-tool-advisory-throttle.json +42 -0
  18. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ralph-state.json +13 -0
  19. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/skill-active-state.json +15 -0
  20. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/subagent-tracking-state.json +62 -0
  21. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ultrawork-state.json +11 -0
  22. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/last-tool-error-state.json +7 -0
  23. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/pre-tool-advisory-throttle.json +18 -0
  24. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/last-tool-error-state.json +7 -0
  25. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/mission-state.json +145 -0
  26. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/pre-tool-advisory-throttle.json +38 -0
  27. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/ralplan-state.json +11 -0
  28. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/subagent-tracking-state.json +98 -0
  29. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  30. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  31. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/mission-state.json +61 -0
  32. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/pre-tool-advisory-throttle.json +22 -0
  33. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/skill-active-state.json +15 -0
  34. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/subagent-tracking-state.json +26 -0
  35. package/.omc/state/sessions/560ea519-574a-4713-b134-5bf6cfaf3c33/pre-tool-advisory-throttle.json +18 -0
  36. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/pre-tool-advisory-throttle.json +14 -0
  37. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/subagent-tracking-state.json +17 -0
  38. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/pre-tool-advisory-throttle.json +18 -0
  39. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/ralplan-state.json +11 -0
  40. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/subagent-tracking-state.json +35 -0
  41. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/last-tool-error-state.json +7 -0
  42. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/mission-state.json +79 -0
  43. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/pre-tool-advisory-throttle.json +22 -0
  44. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/progress.txt +7 -0
  45. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/skill-active-state.json +15 -0
  46. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/subagent-tracking-state.json +26 -0
  47. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/pre-tool-advisory-throttle.json +14 -0
  48. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/subagent-tracking-state.json +17 -0
  49. package/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +18 -0
  50. package/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  51. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/pre-tool-advisory-throttle.json +18 -0
  52. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/subagent-tracking-state.json +17 -0
  53. package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
  54. package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ralph-state.json +13 -0
  55. package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ultrawork-state.json +11 -0
  56. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/last-tool-error-state.json +7 -0
  57. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/mission-state.json +61 -0
  58. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/prd.json +22 -0
  59. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/pre-tool-advisory-throttle.json +22 -0
  60. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/progress.txt +22 -0
  61. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/skill-active-state.json +15 -0
  62. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/subagent-tracking-state.json +26 -0
  63. package/.turbo/turbo-build.log +3 -3
  64. package/.turbo/turbo-test.log +92 -44
  65. package/dist/boot-K4QUAFSP.js +2 -0
  66. package/dist/boot-K4QUAFSP.js.map +7 -0
  67. package/dist/chunk-RTSP2FCA.js +2 -0
  68. package/dist/chunk-RTSP2FCA.js.map +7 -0
  69. package/dist/chunk-VPBZR2PY.js +166 -0
  70. package/dist/chunk-VPBZR2PY.js.map +7 -0
  71. package/dist/cli.js +11 -107
  72. package/dist/cli.js.map +4 -4
  73. package/dist/entry/index.d.ts +2 -0
  74. package/dist/index.js +1 -1
  75. package/dist/index.js.map +4 -4
  76. package/dist/lib/vendor/config-schema.d.ts +58 -0
  77. package/dist/mcp.js +1 -54
  78. package/dist/mcp.js.map +4 -4
  79. package/eslint.config.js +33 -1
  80. package/package.json +5 -1
  81. package/scripts/build.js +11 -1
  82. package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
  83. package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
  84. package/src/.omc/state/idle-notif-cooldown.json +1 -1
  85. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
  86. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
  87. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
  88. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  89. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
  90. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
  91. package/src/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
  92. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  93. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  94. package/src/commands/config/config.ts +7 -37
  95. package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
  96. package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
  97. package/src/commands/doctor/doctor.ts +110 -44
  98. package/src/commands/env-autoload/env-autoload.ts +12 -0
  99. package/src/commands/env-autoload/index.ts +1 -0
  100. package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
  101. package/src/commands/env-clear/env-clear.ts +22 -8
  102. package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
  103. package/src/commands/env-load/env-load.ts +152 -35
  104. package/src/commands/env-load/index.ts +2 -1
  105. package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
  106. package/src/commands/env-status/env-status.ts +19 -7
  107. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
  108. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
  109. package/src/commands/init/__tests__/agent-files.test.ts +145 -27
  110. package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
  111. package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
  112. package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
  113. package/src/commands/init/__tests__/shell-body.test.ts +30 -0
  114. package/src/commands/init/agent-files.ts +65 -45
  115. package/src/commands/init/init.ts +140 -31
  116. package/src/commands/init/migrate-config.ts +160 -32
  117. package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
  118. package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
  119. package/src/commands/vendor-check/index.ts +1 -0
  120. package/src/commands/vendor-check/vendor-check.ts +127 -0
  121. package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
  122. package/src/commands/vendor-config/index.ts +1 -0
  123. package/src/commands/vendor-config/vendor-config.ts +162 -0
  124. package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
  125. package/src/commands/vendor-diff/index.ts +1 -0
  126. package/src/commands/vendor-diff/vendor-diff.ts +77 -0
  127. package/src/commands/vendor-manifest/index.ts +1 -0
  128. package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
  129. package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
  130. package/src/commands/vendor-sync/index.ts +1 -0
  131. package/src/commands/vendor-sync/vendor-sync.ts +90 -0
  132. package/src/commands/worktrees-add/worktrees-add.ts +1 -1
  133. package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
  134. package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
  135. package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
  136. package/src/entry/cli.ts +417 -210
  137. package/src/entry/index.ts +7 -1
  138. package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
  139. package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
  140. package/src/integrations/cmux/list-workspace-titles.ts +1 -1
  141. package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
  142. package/src/integrations/cursor/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
  143. package/src/integrations/cursor/.omc/state/idle-notif-cooldown.json +3 -0
  144. package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +18 -0
  145. package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/subagent-tracking-state.json +17 -0
  146. package/src/integrations/cursor/index.ts +1 -0
  147. package/src/integrations/cursor/launch-cursor.ts +12 -0
  148. package/src/integrations/cursor/open-cursor-workspace.ts +8 -14
  149. package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
  150. package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
  151. package/src/integrations/doppler/doppler-errors.ts +88 -0
  152. package/src/integrations/doppler/doppler-project.ts +80 -0
  153. package/src/integrations/doppler/index.ts +3 -1
  154. package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
  155. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
  156. package/src/integrations/ide/__tests__/ide-facade.test.ts +57 -10
  157. package/src/integrations/ide/add-ide-worktree-folders.ts +2 -4
  158. package/src/integrations/ide/open-ide-workspace.ts +6 -6
  159. package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
  160. package/src/integrations/ide/types.ts +10 -1
  161. package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
  162. package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
  163. package/src/integrations/zed/index.ts +1 -0
  164. package/src/integrations/zed/open-zed-workspace.ts +4 -10
  165. package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
  166. package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
  167. package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
  168. package/src/lib/command-catalog/command-catalog.ts +141 -0
  169. package/src/lib/command-catalog/index.ts +2 -0
  170. package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
  171. package/src/lib/constants/constants.ts +13 -0
  172. package/src/lib/constants/index.ts +2 -0
  173. package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
  174. package/src/lib/env-autoload/env-autoload.ts +294 -0
  175. package/src/lib/env-autoload/index.ts +9 -0
  176. package/src/lib/errors/operation-error.ts +1 -1
  177. package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
  178. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
  179. package/src/lib/infra-kit-config/index.ts +9 -1
  180. package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
  181. package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
  182. package/src/lib/json-output/index.ts +1 -0
  183. package/src/lib/json-output/json-output.ts +47 -0
  184. package/src/lib/managed-block/managed-block.ts +4 -4
  185. package/src/lib/path-display/index.ts +1 -0
  186. package/src/lib/path-display/path-display.ts +42 -0
  187. package/src/lib/render/__tests__/render.test.ts +48 -0
  188. package/src/lib/render/index.ts +1 -0
  189. package/src/lib/render/render.ts +24 -0
  190. package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
  191. package/src/lib/vendor/__tests__/config.test.ts +63 -0
  192. package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
  193. package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
  194. package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
  195. package/src/lib/vendor/config-schema.ts +78 -0
  196. package/src/lib/vendor/config.ts +58 -0
  197. package/src/lib/vendor/factory-config-schema.ts +35 -0
  198. package/src/lib/vendor/factory-config.ts +91 -0
  199. package/src/lib/vendor/hash.ts +16 -0
  200. package/src/lib/vendor/index.ts +23 -0
  201. package/src/lib/vendor/manifest.ts +151 -0
  202. package/src/lib/vendor/skip-sets.ts +48 -0
  203. package/src/lib/vendor/sync-ops.ts +290 -0
  204. package/src/lib/vendor/walk.ts +56 -0
  205. package/src/lib/worktrees/remove-worktrees.ts +15 -0
  206. package/src/mcp/tools/index.ts +5 -42
  207. package/src/sanity.test.tsx +1 -0
  208. package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
  209. package/src/tui/boot.tsx +45 -0
  210. package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
  211. package/src/tui/screens/command-palette.tsx +159 -0
  212. package/src/tui/types.ts +8 -0
  213. package/tsconfig.json +1 -0
  214. package/tsconfig.tsbuildinfo +1 -1
  215. package/vitest.config.ts +8 -0
  216. package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
  217. package/src/commands/worktrees-open/index.ts +0 -1
  218. package/src/commands/worktrees-open/worktrees-open.ts +0 -195
@@ -1,12 +1,18 @@
1
1
  import fs from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
+ import process from 'node:process'
4
5
 
5
6
  import { logger } from 'src/lib/logger'
6
7
  import { removeManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'
7
8
 
8
9
  import { writeAgentFiles } from './agent-files'
9
- import { migrateLegacyConfig, normalizeLegacyIdeStructures } from './migrate-config'
10
+ import {
11
+ migrateFactoryConfigToJson,
12
+ migrateLegacyConfig,
13
+ migrateUserGlobalConfigFilename,
14
+ normalizeLegacyIdeStructures,
15
+ } from './migrate-config'
10
16
 
11
17
  export const MARKER_START = '# -- infra-kit:begin --'
12
18
  export const MARKER_END = '# -- infra-kit:end --'
@@ -18,22 +24,34 @@ const LEGACY_SINGLE = '# infra-kit shell functions'
18
24
  const USER_GLOBAL_CONFIG_STUB = '{}\n'
19
25
 
20
26
  // …and the annotated guidance lives next to it in a non-loaded .example.jsonc
21
- // (the loader only reads the three exact `infra-kit.json` / `config.json` files).
22
- const USER_GLOBAL_CONFIG_EXAMPLE = `// infra-kit user-global config — ~/.infra-kit/config.json
27
+ // (the loader only reads the three exact `infra-kit.json` files).
28
+ const USER_GLOBAL_CONFIG_EXAMPLE = `// infra-kit user-global config — ~/.infra-kit/infra-kit.json
23
29
  //
24
30
  // Merge chain (later layers override earlier ones at top-level keys):
25
31
  // 1. <repo>/infra-kit.json — committed project config (required)
26
- // 2. ~/.infra-kit/config.json — user-global (the sibling of this file)
32
+ // 2. ~/.infra-kit/infra-kit.json — user-global (the sibling of this file)
27
33
  // 3. ~/.infra-kit/projects/<repo-name>/infra-kit.json — user-scope per-project override
28
34
  //
29
35
  // Merge is shallow: setting a top-level key replaces that whole section from
30
36
  // layer 1. Arrays do not concatenate. Top-level keys recognized:
31
- // environments, envManagement, ide, taskManager, worktrees.
37
+ // environments, envManagement, ide, taskManager, worktrees, envAutoLoad.
32
38
  //
33
39
  // This .example.jsonc is reference only — it is NOT loaded. Put real global
34
- // overrides in the sibling config.json (strict JSON: no comments, double-quoted
40
+ // overrides in the sibling infra-kit.json (strict JSON: no comments, double-quoted
35
41
  // keys). Per-project tweaks belong in layer 3 — run \`infra-kit config edit\`.
42
+ //
43
+ // Every recognized key is documented below. NOTE: \`environments\` and
44
+ // \`envManagement\` are REQUIRED in the committed project infra-kit.json (layer 1)
45
+ // and are usually NOT set in this user-global layer — they are shown here only to
46
+ // document the full, valid config shape.
36
47
  {
48
+ // "environments": ["dev", "staging", "prod"], // string[] (>=1) — required in layer 1
49
+ //
50
+ // "envManagement": { // required in layer 1; provider-tagged
51
+ // "provider": "doppler",
52
+ // "config": { "name": "my-doppler-project" }
53
+ // },
54
+ //
37
55
  // "ide": {
38
56
  // "provider": "cursor",
39
57
  // "config": { "workspaceConfigPath": "/path/to/your.code-workspace" }
@@ -45,23 +63,60 @@ const USER_GLOBAL_CONFIG_EXAMPLE = `// infra-kit user-global config — ~/.infra
45
63
  // { "provider": "cursor", "config": { "workspaceConfigPath": "/path/to/your.code-workspace" } },
46
64
  // { "provider": "zed", "config": {} }
47
65
  // ],
48
- // "worktrees": { "openInGithubDesktop": false, "openInCmux": true }
66
+ //
67
+ // "taskManager": {
68
+ // "provider": "jira",
69
+ // "config": { "baseUrl": "https://acme.atlassian.net", "projectId": 123 }
70
+ // },
71
+ //
72
+ // "worktrees": {
73
+ // "openInGithubDesktop": false,
74
+ // "openInCmux": true,
75
+ // // cmux pane layout for opened worktrees: "two-columns" (default, left | right)
76
+ // // or "three-pane" (left split top/bottom + full-height right).
77
+ // "cmux": { "layout": "two-columns" }
78
+ // },
79
+ //
80
+ // // Auto-load Doppler env when working inside this project/worktree. Omit to
81
+ // // disable. "trigger" (pick one): "shell-startup" (new shells) | "cli-invocation"
82
+ // // (before each infra-kit command, primes subsequent commands). "config" is the
83
+ // // environment to load (must be one of "environments"). Requires the committed
84
+ // // infra-kit.json at the git repo root, and the zsh shell integration
85
+ // // (infra-kit init + a new shell). zsh only.
86
+ // "envAutoLoad": { "trigger": "shell-startup", "config": "dev" }
87
+ }
88
+ `
89
+
90
+ // The machine-local factory registry lives at ~/.infra-kit/vendor.json (strict
91
+ // JSON). This annotated sibling documents every property; the real file is
92
+ // scaffolded by \`infra-kit vendor config --init\`, NOT by init.
93
+ const VENDOR_CONFIG_EXAMPLE = `// infra-kit factory registry — ~/.infra-kit/vendor.json
94
+ //
95
+ // Machine-local registry the vendor commands (sync/manifest/diff) read to know
96
+ // where your project repos live and which ones to stamp. This .example.jsonc is
97
+ // reference only — it is NOT loaded. The real file is the strict-JSON sibling
98
+ // vendor.json (no comments, double-quoted keys); run \`infra-kit vendor config --init\`
99
+ // to scaffold it.
100
+ {
101
+ // "workspaceDir": "~/projects", // string (absolute or ~-prefixed) — where target repos are cloned
102
+ // "targets": ["my-repo-a", "my-repo-b"] // string[] (>=1) — repo dir names resolved under workspaceDir
49
103
  }
50
104
  `
51
105
 
52
106
  /**
53
- * Append infra-kit shell functions to .zshrc, migrate any legacy
54
- * `infra-kit.yml` config layers to JSON, normalize existing JSON configs from
55
- * the old IDE structure to the new one (strip the removed `ide.config.mode`),
56
- * and seed the user-global config at ~/.infra-kit/config.json on first run.
57
- * Idempotent: a subsequent run replaces the existing zshrc block in place, has
58
- * nothing left to migrate or normalize, and leaves the user-global config
59
- * untouched.
107
+ * Append infra-kit shell functions to .zshrc, migrate any legacy `infra-kit.yml`
108
+ * config layers to JSON, normalize existing JSON configs from the old IDE structure
109
+ * (strip the removed `ide.config.mode`), convert a legacy factory `vendor.config.ts`
110
+ * to `vendor.json`, and seed the user-global config at ~/.infra-kit/infra-kit.json on
111
+ * first run. Idempotent: a subsequent run replaces the existing zshrc block in place,
112
+ * has nothing left to migrate or normalize, and leaves the REAL config files
113
+ * (infra-kit.json / vendor.json) untouched — but the annotated `.example.jsonc`
114
+ * reference files are refreshed on every run so they always reflect the current schema.
60
115
  *
61
116
  * @example
62
117
  * // CLI: `infra-kit init` (or via the `pnpm dx-init` alias)
63
118
  * // INFO: Added infra-kit shell functions to /Users/me/.zshrc
64
- * // INFO: Wrote user-global config to /Users/me/.infra-kit/config.json (see …/config.example.jsonc …)
119
+ * // INFO: Wrote user-global config to /Users/me/.infra-kit/infra-kit.json (see …/infra-kit.example.jsonc …)
65
120
  * // INFO: Run `source ~/.zshrc` or open a new terminal to activate.
66
121
  */
67
122
  export const init = async (): Promise<void> => {
@@ -85,50 +140,79 @@ export const init = async (): Promise<void> => {
85
140
  logger.info(`Added infra-kit shell functions to ${zshrcPath}`)
86
141
 
87
142
  // Convert any legacy infra-kit.yml config layers to JSON before seeding, so a
88
- // migrated config.json is not re-seeded as an empty stub.
143
+ // migrated infra-kit.json is not re-seeded as an empty stub.
89
144
  await migrateLegacyConfig()
90
145
 
91
146
  // Migrate existing JSON configs from the old IDE structure to the new one
92
147
  // (strip the removed `ide.config.mode` field). No-op for already-clean configs.
93
148
  await normalizeLegacyIdeStructures()
94
149
 
150
+ // Rename a legacy user-global config.json → infra-kit.json (single canonical
151
+ // filename). MUST run before seeding: otherwise the seeder checks the new name,
152
+ // doesn't find it, and writes an empty stub that shadows the user's real config.
153
+ await migrateUserGlobalConfigFilename()
154
+
155
+ // Convert a legacy machine-local factory config from executable TS
156
+ // (~/.infra-kit/vendor.config.ts) to static JSON (~/.infra-kit/vendor.json).
157
+ // Independent of the infra-kit.json layers; grouped with the other migrations.
158
+ await migrateFactoryConfigToJson()
159
+
95
160
  seedUserGlobalConfig()
96
161
 
97
162
  // Best-effort, non-fatal, repo-gated: keep the agent-instruction files in sync
98
163
  // with the CLI surface. A no-op outside an infra-kit repo.
99
164
  await writeAgentFiles()
100
165
 
166
+ // The shell integration is zsh-only (zmodload zsh/stat, add-zsh-hook, ${dir:h}).
167
+ // Warn non-fatally if the user's login shell isn't zsh so the block isn't a
168
+ // silent no-op for them.
169
+ const shell = process.env.SHELL ?? ''
170
+
171
+ if (!shell.includes('zsh')) {
172
+ logger.warn(
173
+ `Your login shell ($SHELL=${shell || 'unset'}) is not zsh. The infra-kit shell integration (env-load/env-clear/auto-load) is zsh-only and won't activate in bash/fish.`,
174
+ )
175
+ }
176
+
101
177
  logger.info('Run `source ~/.zshrc` or open a new terminal to activate.')
102
178
  }
103
179
 
104
180
  /**
105
- * Create `~/.infra-kit/config.json` (empty `{}`) plus an annotated
106
- * `~/.infra-kit/config.example.jsonc` reference when absent. Skips silently if
107
- * the config already exists so user edits are preserved.
181
+ * Seed the user-global config on first run and (re)write the annotated reference
182
+ * files. The real `~/.infra-kit/infra-kit.json` (empty `{}`) is written only when
183
+ * absent so user edits are preserved. The non-loaded `.example.jsonc` reference
184
+ * files (`infra-kit.example.jsonc` + `vendor.example.jsonc`) are rewritten on EVERY
185
+ * run so existing users always get the current, complete schema documentation.
186
+ *
187
+ * Deliberately seeds NO real `vendor.json` — that is scaffolded by
188
+ * `infra-kit vendor config --init` (a stub here would block `--init`, trip the
189
+ * factory migration's no-overwrite guard, and fail the schema's `targets.min(1)`).
108
190
  *
109
191
  * @example
110
192
  * seedUserGlobalConfig()
111
- * // first call: writes ~/.infra-kit/config.json ({}) + config.example.jsonc
112
- * // later calls: leaves the config alone, logs that it is already present
193
+ * // first call: writes ~/.infra-kit/infra-kit.json ({}) + both .example.jsonc files
194
+ * // later calls: leaves infra-kit.json alone, refreshes both .example.jsonc files
113
195
  */
114
- const seedUserGlobalConfig = (): void => {
196
+ export const seedUserGlobalConfig = (): void => {
115
197
  const userConfigDir = path.join(os.homedir(), '.infra-kit')
116
- const userConfigPath = path.join(userConfigDir, 'config.json')
117
- const userConfigExamplePath = path.join(userConfigDir, 'config.example.jsonc')
198
+ const userConfigPath = path.join(userConfigDir, 'infra-kit.json')
199
+
200
+ fs.mkdirSync(userConfigDir, { recursive: true })
201
+
202
+ // Reference examples are non-loaded docs — always refresh them so re-running init
203
+ // delivers the current schema (and the newly-added vendor example) to existing users.
204
+ fs.writeFileSync(path.join(userConfigDir, 'infra-kit.example.jsonc'), USER_GLOBAL_CONFIG_EXAMPLE, 'utf-8')
205
+ fs.writeFileSync(path.join(userConfigDir, 'vendor.example.jsonc'), VENDOR_CONFIG_EXAMPLE, 'utf-8')
118
206
 
119
207
  if (fs.existsSync(userConfigPath)) {
120
- logger.info(`User-global config already present at ${userConfigPath}`)
208
+ logger.info(`User-global config already present at ${userConfigPath} (refreshed reference examples)`)
121
209
 
122
210
  return
123
211
  }
124
212
 
125
- fs.mkdirSync(userConfigDir, { recursive: true })
126
213
  fs.writeFileSync(userConfigPath, USER_GLOBAL_CONFIG_STUB, 'utf-8')
127
- fs.writeFileSync(userConfigExamplePath, USER_GLOBAL_CONFIG_EXAMPLE, 'utf-8')
128
214
 
129
- logger.info(
130
- `Wrote user-global config to ${userConfigPath} (see ${userConfigExamplePath} for the annotated reference)`,
131
- )
215
+ logger.info(`Wrote user-global config to ${userConfigPath} (see the sibling .example.jsonc files for reference)`)
132
216
  }
133
217
 
134
218
  const isBlockLine = (line: string): boolean => {
@@ -168,7 +252,7 @@ const removeExistingBlock = (content: string): string => {
168
252
 
169
253
  if (legacyIdx === -1) return content
170
254
 
171
- // eslint-disable-next-line sonarjs/slow-regex
255
+ // eslint-disable-next-line sonarjs/super-linear-regex
172
256
  const before = content.slice(0, legacyIdx).replace(/\n+$/, '')
173
257
  const afterLines = content.slice(legacyIdx).split('\n')
174
258
 
@@ -238,6 +322,31 @@ export const buildShellBody = (): string => {
238
322
  'if (( _INFRA_KIT_SHELL_STARTED > 0 )); then',
239
323
  ' add-zsh-hook precmd _infra_kit_autoload',
240
324
  'fi',
325
+ // One-shot env auto-load when a NEW shell opens inside an infra-kit project
326
+ // or worktree (config: envAutoLoad.trigger "shell-startup"). Cheap pure-zsh
327
+ // project gate (walk up for infra-kit.json) avoids spawning node in unrelated
328
+ // shells; the INFRA_KIT_ENV_CONFIG guard skips subshells that already inherited
329
+ // a loaded env. The spawn is DETACHED+backgrounded ( ... & ) so it never blocks
330
+ // the prompt; it only WRITES env-load.sh and the precmd hook above is the sole
331
+ // sourcer (picked up on a subsequent prompt). Trade-off: the gate can't read the
332
+ // merged JSON config, so a fresh shell in a project WITHOUT envAutoLoad still
333
+ // spawns one background process that resolves config, finds nothing, and exits.
334
+ '_infra_kit_startup_autoload() {',
335
+ ' [[ -z "$INFRA_KIT_SESSION" ]] && return',
336
+ ' [[ -n "$INFRA_KIT_ENV_CONFIG" ]] && return',
337
+ ' local dir="$PWD"',
338
+ ' while [[ "$dir" != / ]]; do',
339
+ ' if [[ -f "$dir/infra-kit.json" ]]; then',
340
+ ` ( ${runCmd} env-autoload & ) >/dev/null 2>&1`,
341
+ ' return',
342
+ ' fi',
343
+ // eslint-disable-next-line no-template-curly-in-string
344
+ ' dir="${dir:h}"',
345
+ ' done',
346
+ '}',
347
+ 'if (( _INFRA_KIT_SHELL_STARTED > 0 )); then',
348
+ ' _infra_kit_startup_autoload',
349
+ 'fi',
241
350
  ].join('\n')
242
351
  }
243
352
 
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises'
2
- import os from 'node:os'
3
2
  import path from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
4
  import yaml from 'yaml'
5
5
  import { z } from 'zod'
6
6
 
@@ -11,6 +11,9 @@ import {
11
11
  resetInfraKitConfigCache,
12
12
  } from 'src/lib/infra-kit-config'
13
13
  import { logger } from 'src/lib/logger'
14
+ import { fileExists, tildify } from 'src/lib/path-display'
15
+ import { getFactoryConfigPath } from 'src/lib/vendor/factory-config'
16
+ import { factoryConfigSchema } from 'src/lib/vendor/factory-config-schema'
14
17
 
15
18
  interface MigrateLayer {
16
19
  label: string
@@ -22,35 +25,6 @@ interface MigrateLayer {
22
25
  schema: z.ZodType
23
26
  }
24
27
 
25
- /**
26
- * Replace the user's home prefix with `~` so logged paths stay short.
27
- *
28
- * @example
29
- * // os.homedir() === '/Users/arthur'
30
- * tildify('/Users/arthur/.infra-kit/config.yml') // => '~/.infra-kit/config.yml'
31
- */
32
- const tildify = (filePath: string): string => {
33
- const home = os.homedir()
34
-
35
- return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath
36
- }
37
-
38
- /**
39
- * `fs.access` reduced to a boolean, swallowing ENOENT.
40
- *
41
- * @example
42
- * await fileExists('/etc/hosts') // => true
43
- */
44
- const fileExists = async (filePath: string): Promise<boolean> => {
45
- try {
46
- await fs.access(filePath)
47
-
48
- return true
49
- } catch {
50
- return false
51
- }
52
- }
53
-
54
28
  /**
55
29
  * Swap a resolved `.json` config path back to its legacy `.yml` sibling.
56
30
  *
@@ -87,8 +61,13 @@ export const migrateLegacyConfig = async (): Promise<void> => {
87
61
  const layers: MigrateLayer[] = [
88
62
  { label: 'infra-kit.json', yml: legacyYmlPath(paths.main), json: paths.main, schema: infraKitConfigSchema },
89
63
  {
90
- label: '~/.infra-kit/config.json',
91
- yml: legacyYmlPath(paths.userGlobal),
64
+ label: '~/.infra-kit/infra-kit.json',
65
+ // The user-global JSON filename changed (config.json → infra-kit.json), but
66
+ // the legacy YAML it converts from kept the old `config.yml` name. Pin it
67
+ // here so it does not drift to `infra-kit.yml` via legacyYmlPath(); the
68
+ // main/userProject layers keep deriving their `.yml` sibling normally
69
+ // because their `.json` filename never changed.
70
+ yml: path.join(path.dirname(paths.userGlobal), 'config.yml'),
92
71
  json: paths.userGlobal,
93
72
  schema: infraKitOverrideConfigSchema,
94
73
  },
@@ -145,6 +124,155 @@ export const migrateLegacyConfig = async (): Promise<void> => {
145
124
  }
146
125
  }
147
126
 
127
+ /**
128
+ * Rename the user-global config from the legacy `config.json` filename to the
129
+ * canonical `infra-kit.json` (and its `config.example.jsonc` reference to
130
+ * `infra-kit.example.jsonc`), so every merge-chain layer shares one filename.
131
+ * Run by `infra-kit init` before the user-global config is seeded, so a user's
132
+ * existing overrides are carried forward instead of being shadowed by a fresh
133
+ * `{}` stub written under the new name.
134
+ *
135
+ * Best-effort and non-fatal per file: a missing source is skipped silently
136
+ * (idempotent — nothing to rename on a second run), and an already-present
137
+ * target is never overwritten (the stale `config.json` is left in place so no
138
+ * bytes are lost — the user reconciles it manually). Resets the config cache
139
+ * when anything was renamed.
140
+ *
141
+ * @example
142
+ * await migrateUserGlobalConfigFilename()
143
+ * // ✓ Renamed ~/.infra-kit/config.json → ~/.infra-kit/infra-kit.json
144
+ * // (no output when there is nothing legacy to rename)
145
+ */
146
+ export const migrateUserGlobalConfigFilename = async (): Promise<void> => {
147
+ let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>
148
+
149
+ try {
150
+ paths = await getInfraKitConfigPaths()
151
+ } catch {
152
+ // No resolvable project (e.g. init run outside a repo) — nothing to rename.
153
+ return
154
+ }
155
+
156
+ const dir = path.dirname(paths.userGlobal)
157
+ // The config.json → infra-kit.json rename carries forward a user's REAL config and
158
+ // is load-bearing. The example-file rename is now largely redundant: seedUserGlobalConfig
159
+ // rewrites infra-kit.example.jsonc unconditionally moments later, so a renamed legacy
160
+ // example is overwritten anyway (and if infra-kit.example.jsonc already exists, the
161
+ // no-overwrite guard simply leaves an orphaned config.example.jsonc behind — harmless).
162
+ const pairs: { from: string; to: string }[] = [
163
+ { from: path.join(dir, 'config.json'), to: paths.userGlobal },
164
+ { from: path.join(dir, 'config.example.jsonc'), to: path.join(dir, 'infra-kit.example.jsonc') },
165
+ ]
166
+
167
+ let renamed = 0
168
+
169
+ for (const { from, to } of pairs) {
170
+ const [fromExists, toExists] = await Promise.all([fileExists(from), fileExists(to)])
171
+
172
+ if (!fromExists) continue
173
+
174
+ if (toExists) {
175
+ logger.info(`⚠ Skipped ${tildify(from)} — ${tildify(to)} already exists (remove the stale file manually)`)
176
+
177
+ continue
178
+ }
179
+
180
+ // Same-directory rename is atomic; per-file try/catch keeps a TOCTOU race or
181
+ // I/O error (EACCES, read-only FS) non-fatal so one bad file never aborts init.
182
+ try {
183
+ await fs.mkdir(path.dirname(to), { recursive: true })
184
+ await fs.rename(from, to)
185
+
186
+ logger.info(`✓ Renamed ${tildify(from)} → ${tildify(to)}`)
187
+ renamed++
188
+ } catch (err) {
189
+ logger.info(`⚠ Skipped ${tildify(from)} — ${(err as Error).message}`)
190
+ }
191
+ }
192
+
193
+ if (renamed > 0) {
194
+ resetInfraKitConfigCache()
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Convert a legacy machine-local factory config from executable TypeScript
200
+ * (`~/.infra-kit/vendor.config.ts`) to static JSON (`~/.infra-kit/vendor.json`) as
201
+ * part of `infra-kit init`. The factory config used to be a `.ts` module loaded via
202
+ * dynamic `import()`; it is now strict JSON read with `JSON.parse`. This one-shot
203
+ * migration dynamic-imports the old `.ts` (the ONLY remaining dynamic import of the
204
+ * factory file), resolves a factory-function or object default export, validates it,
205
+ * writes `vendor.json`, and removes the old `.ts`.
206
+ *
207
+ * Best-effort, non-fatal, idempotent (no old `.ts` → no-op), and never overwrites an
208
+ * existing `vendor.json`. When the old export was a FUNCTION, its resolved output is
209
+ * frozen into static JSON — so a DISTINCT warning is emitted (not a plain
210
+ * `✓ Migrated`) because dynamic re-evaluation (env vars, directory globbing) no
211
+ * longer happens. No `resetInfraKitConfigCache()` — the factory loader shares no
212
+ * cache with the `infra-kit.json` layers.
213
+ *
214
+ * @example
215
+ * await migrateFactoryConfigToJson()
216
+ * // ✓ Migrated ~/.infra-kit/vendor.config.ts → ~/.infra-kit/vendor.json
217
+ * // (no output when there is nothing legacy to convert)
218
+ */
219
+ export const migrateFactoryConfigToJson = async (): Promise<void> => {
220
+ const newJson = getFactoryConfigPath() // ~/.infra-kit/vendor.json
221
+ const dir = path.dirname(newJson)
222
+ const oldTs = path.join(dir, 'vendor.config.ts')
223
+
224
+ const [oldExists, newExists] = await Promise.all([fileExists(oldTs), fileExists(newJson)])
225
+
226
+ if (!oldExists) return // idempotent: nothing to migrate (incl. after a prior successful run)
227
+
228
+ if (newExists) {
229
+ logger.info(
230
+ `⚠ Skipped ${tildify(oldTs)} — ${tildify(newJson)} already exists (remove the stale vendor.config.ts manually)`,
231
+ )
232
+
233
+ return
234
+ }
235
+
236
+ // The factory `.ts` is executable; read it once via dynamic import (Node native
237
+ // type stripping) with an mtime cache-bust, resolving a factory-function or object
238
+ // default export. Per-file try/catch keeps a malformed module or I/O error
239
+ // non-fatal so one bad file never aborts init.
240
+ try {
241
+ const stat = await fs.stat(oldTs)
242
+ const moduleUrl = `${pathToFileURL(oldTs).href}?mtime=${Number(stat.mtimeMs)}`
243
+ const imported = (await import(moduleUrl)) as { default?: unknown }
244
+ const raw = imported.default
245
+ const wasFunction = typeof raw === 'function'
246
+ const resolved = wasFunction ? await (raw as () => unknown)() : raw
247
+
248
+ const result = factoryConfigSchema.safeParse(resolved)
249
+
250
+ if (!result.success) {
251
+ logger.info(`⚠ Skipped ${tildify(oldTs)} — invalid factory config: ${z.prettifyError(result.error)}`)
252
+
253
+ return // leave the old .ts in place so the user can fix and re-run; do not delete unconverted data
254
+ }
255
+
256
+ await fs.mkdir(dir, { recursive: true })
257
+ await fs.writeFile(newJson, `${JSON.stringify(result.data, null, 2)}\n`, 'utf-8')
258
+ await fs.rm(oldTs, { force: true })
259
+
260
+ if (wasFunction) {
261
+ // The old export was a function: its values were resolved ONCE and frozen into
262
+ // static JSON. Disclose the lost dynamism instead of reporting a clean success.
263
+ logger.info(
264
+ `⚠ Migrated ${tildify(oldTs)} → ${tildify(newJson)} as a STATIC SNAPSHOT — the old file was a ` +
265
+ `function (export default () => ({...})); computed values (env vars, directory globbing) were ` +
266
+ `frozen at their current values and no longer re-evaluate. Edit ${tildify(newJson)} directly to change them.`,
267
+ )
268
+ } else {
269
+ logger.info(`✓ Migrated ${tildify(oldTs)} → ${tildify(newJson)}`)
270
+ }
271
+ } catch (err) {
272
+ logger.info(`⚠ Skipped ${tildify(oldTs)} — ${(err as Error).message}`)
273
+ }
274
+ }
275
+
148
276
  /**
149
277
  * Surgically strip the removed `ide.config.mode` field from a parsed config
150
278
  * object (single-provider or array form). The windows-removal made `mode` a dead
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { writeManifest } from 'src/lib/vendor'
7
+ import { execMock, zxModuleMock } from 'src/lib/vendor/__tests__/zx-mock'
8
+
9
+ import { vendorCheck } from '../vendor-check'
10
+
11
+ // Spy on every subprocess-spawning and config-loading collaborator. The
12
+ // invariant under test is RUNTIME: invoking `vendor check` must spawn zero
13
+ // subprocess and load zero vendor.config.ts — regardless of what the single
14
+ // bundled CLI artifact happens to contain.
15
+ vi.mock('zx', () => {
16
+ return zxModuleMock()
17
+ })
18
+
19
+ const loadVendorConfigMock = vi.fn()
20
+
21
+ vi.mock('src/lib/vendor/config', async (importActual) => {
22
+ const actual = await importActual<typeof import('src/lib/vendor/config')>()
23
+
24
+ return { ...actual, loadVendorConfig: loadVendorConfigMock }
25
+ })
26
+
27
+ const loadFactoryConfigMock = vi.fn()
28
+
29
+ vi.mock('src/lib/vendor/factory-config', async (importActual) => {
30
+ const actual = await importActual<typeof import('src/lib/vendor/factory-config')>()
31
+
32
+ return { ...actual, loadFactoryConfig: loadFactoryConfigMock }
33
+ })
34
+
35
+ let root: string
36
+ let vendorRoot: string
37
+
38
+ beforeEach(() => {
39
+ vi.clearAllMocks()
40
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-isolation-test-'))
41
+ vendorRoot = path.join(root, 'vendor')
42
+ fs.mkdirSync(path.join(vendorRoot, 'configs'), { recursive: true })
43
+ fs.writeFileSync(path.join(vendorRoot, 'configs', 'a.js'), 'aaa')
44
+ writeManifest(vendorRoot, { source: 's', commit: 'c' })
45
+ })
46
+
47
+ afterEach(() => {
48
+ fs.rmSync(root, { recursive: true, force: true })
49
+ })
50
+
51
+ describe('vendor check runtime isolation', () => {
52
+ it('spawns no subprocess and loads no vendor.config.ts on a clean check', async () => {
53
+ const { structuredContent } = await vendorCheck({ cwd: root })
54
+
55
+ expect(structuredContent.ok).toBe(true)
56
+ expect(execMock).not.toHaveBeenCalled()
57
+ expect(loadVendorConfigMock).not.toHaveBeenCalled()
58
+ expect(loadFactoryConfigMock).not.toHaveBeenCalled()
59
+ })
60
+
61
+ it('spawns no subprocess even when reporting drift', async () => {
62
+ fs.writeFileSync(path.join(vendorRoot, 'configs', 'a.js'), 'CHANGED')
63
+
64
+ const { structuredContent } = await vendorCheck({ cwd: root })
65
+
66
+ expect(structuredContent.ok).toBe(false)
67
+ expect(execMock).not.toHaveBeenCalled()
68
+ expect(loadVendorConfigMock).not.toHaveBeenCalled()
69
+ expect(loadFactoryConfigMock).not.toHaveBeenCalled()
70
+ })
71
+ })
@@ -0,0 +1,87 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
+
6
+ import { writeManifest } from 'src/lib/vendor'
7
+
8
+ import { vendorCheck } from '../vendor-check'
9
+
10
+ let root: string
11
+ let vendorRoot: string
12
+
13
+ const writeVendor = (rel: string, content: string): void => {
14
+ const full = path.join(vendorRoot, rel)
15
+
16
+ fs.mkdirSync(path.dirname(full), { recursive: true })
17
+ fs.writeFileSync(full, content, 'utf8')
18
+ }
19
+
20
+ beforeEach(() => {
21
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-check-test-'))
22
+ vendorRoot = path.join(root, 'vendor')
23
+ })
24
+
25
+ afterEach(() => {
26
+ fs.rmSync(root, { recursive: true, force: true })
27
+ })
28
+
29
+ describe('vendorCheck exit-code matrix', () => {
30
+ it('missing vendor/ → ok (skip)', async () => {
31
+ const { structuredContent } = await vendorCheck({ cwd: root })
32
+
33
+ expect(structuredContent.status).toBe('skipped')
34
+ expect(structuredContent.ok).toBe(true)
35
+ })
36
+
37
+ it('missing manifest → not ok', async () => {
38
+ writeVendor('a.js', 'a')
39
+ const { structuredContent } = await vendorCheck({ cwd: root })
40
+
41
+ expect(structuredContent.status).toBe('missing-manifest')
42
+ expect(structuredContent.ok).toBe(false)
43
+ })
44
+
45
+ it('clean tree → ok', async () => {
46
+ writeVendor('configs/a.js', 'a')
47
+ writeVendor('packages/p/b.ts', 'b')
48
+ writeManifest(vendorRoot, { source: 's', commit: 'c' })
49
+
50
+ const { structuredContent } = await vendorCheck({ cwd: root })
51
+
52
+ expect(structuredContent.status).toBe('clean')
53
+ expect(structuredContent.ok).toBe(true)
54
+ expect(structuredContent.fileCount).toBe(2)
55
+ })
56
+
57
+ it('drifted tree → not ok with categorized paths', async () => {
58
+ writeVendor('a.js', 'a')
59
+ writeVendor('b.js', 'b')
60
+ writeManifest(vendorRoot, { source: 's', commit: 'c' })
61
+
62
+ fs.writeFileSync(path.join(vendorRoot, 'a.js'), 'CHANGED')
63
+ writeVendor('c.js', 'c')
64
+ fs.rmSync(path.join(vendorRoot, 'b.js'))
65
+
66
+ const { structuredContent } = await vendorCheck({ cwd: root })
67
+
68
+ expect(structuredContent.status).toBe('drift')
69
+ expect(structuredContent.ok).toBe(false)
70
+ expect(structuredContent.modified).toEqual(['a.js'])
71
+ expect(structuredContent.added).toEqual(['c.js'])
72
+ expect(structuredContent.removed).toEqual(['b.js'])
73
+ })
74
+
75
+ it('unknown future schemaVersion → not ok (fail-closed)', async () => {
76
+ writeVendor('a.js', 'a')
77
+ const manifest = writeManifest(vendorRoot, { source: 's', commit: 'c' })
78
+ const bumped = { ...manifest, schemaVersion: (manifest.schemaVersion ?? 1) + 1 }
79
+
80
+ fs.writeFileSync(path.join(vendorRoot, '.sync-manifest.json'), JSON.stringify(bumped))
81
+
82
+ const { structuredContent } = await vendorCheck({ cwd: root })
83
+
84
+ expect(structuredContent.status).toBe('unknown-schema')
85
+ expect(structuredContent.ok).toBe(false)
86
+ })
87
+ })
@@ -0,0 +1 @@
1
+ export { vendorCheck, vendorCheckMcpTool } from './vendor-check'