infra-kit 0.1.114 → 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 (202) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-1fcfe0ec-7567-41c3-9bfd-501e99e86819.jsonl +19 -0
  3. package/.omc/state/agent-replay-38382db5-3088-4b5a-aff7-2561a60827c7.jsonl +17 -0
  4. package/.omc/state/agent-replay-50c36be9-a3a2-42b9-9c85-5a06d3e22f6a.jsonl +5 -0
  5. package/.omc/state/agent-replay-8198b3e5-fad6-4ee0-9c59-ef773ad89af4.jsonl +1 -0
  6. package/.omc/state/agent-replay-8cd7652a-b8b5-49cf-9b1f-bee8e5972458.jsonl +3 -0
  7. package/.omc/state/agent-replay-9bea518f-a9ee-4d14-bb53-a936ebaf647c.jsonl +7 -0
  8. package/.omc/state/agent-replay-b52d89fb-0e24-4901-bc66-27e3c1697126.jsonl +2 -0
  9. package/.omc/state/agent-replay-f581a1ae-0ee4-48a3-96f3-ce31a0633bb0.jsonl +1 -0
  10. package/.omc/state/agent-replay-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
  11. package/.omc/state/idle-notif-cooldown.json +1 -1
  12. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/last-tool-error-state.json +7 -0
  13. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/mission-state.json +109 -0
  14. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/pre-tool-advisory-throttle.json +42 -0
  15. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ralph-state.json +13 -0
  16. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/skill-active-state.json +15 -0
  17. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/subagent-tracking-state.json +62 -0
  18. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ultrawork-state.json +11 -0
  19. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/last-tool-error-state.json +7 -0
  20. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/pre-tool-advisory-throttle.json +18 -0
  21. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/last-tool-error-state.json +7 -0
  22. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/mission-state.json +145 -0
  23. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/pre-tool-advisory-throttle.json +38 -0
  24. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/ralplan-state.json +11 -0
  25. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/subagent-tracking-state.json +98 -0
  26. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  27. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  28. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/mission-state.json +61 -0
  29. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/pre-tool-advisory-throttle.json +22 -0
  30. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/skill-active-state.json +15 -0
  31. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/subagent-tracking-state.json +26 -0
  32. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/pre-tool-advisory-throttle.json +14 -0
  33. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/subagent-tracking-state.json +17 -0
  34. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/pre-tool-advisory-throttle.json +18 -0
  35. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/ralplan-state.json +11 -0
  36. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/subagent-tracking-state.json +35 -0
  37. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/last-tool-error-state.json +7 -0
  38. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/mission-state.json +79 -0
  39. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/pre-tool-advisory-throttle.json +22 -0
  40. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/progress.txt +7 -0
  41. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/skill-active-state.json +15 -0
  42. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/subagent-tracking-state.json +26 -0
  43. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/pre-tool-advisory-throttle.json +14 -0
  44. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/subagent-tracking-state.json +17 -0
  45. package/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +18 -0
  46. package/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  47. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/pre-tool-advisory-throttle.json +18 -0
  48. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/subagent-tracking-state.json +17 -0
  49. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/last-tool-error-state.json +7 -0
  50. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/mission-state.json +61 -0
  51. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/prd.json +22 -0
  52. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/pre-tool-advisory-throttle.json +22 -0
  53. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/progress.txt +22 -0
  54. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/skill-active-state.json +15 -0
  55. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/subagent-tracking-state.json +26 -0
  56. package/.turbo/turbo-build.log +3 -3
  57. package/.turbo/turbo-test.log +92 -44
  58. package/dist/boot-K4QUAFSP.js +2 -0
  59. package/dist/boot-K4QUAFSP.js.map +7 -0
  60. package/dist/chunk-RTSP2FCA.js +2 -0
  61. package/dist/chunk-RTSP2FCA.js.map +7 -0
  62. package/dist/chunk-VPBZR2PY.js +166 -0
  63. package/dist/chunk-VPBZR2PY.js.map +7 -0
  64. package/dist/cli.js +11 -107
  65. package/dist/cli.js.map +4 -4
  66. package/dist/entry/index.d.ts +2 -0
  67. package/dist/index.js +1 -1
  68. package/dist/index.js.map +4 -4
  69. package/dist/lib/vendor/config-schema.d.ts +58 -0
  70. package/dist/mcp.js +1 -54
  71. package/dist/mcp.js.map +4 -4
  72. package/eslint.config.js +33 -1
  73. package/package.json +5 -1
  74. package/scripts/build.js +11 -1
  75. package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
  76. package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
  77. package/src/.omc/state/idle-notif-cooldown.json +1 -1
  78. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
  79. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
  80. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
  81. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  82. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
  83. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
  84. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  85. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  86. package/src/commands/config/config.ts +7 -37
  87. package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
  88. package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
  89. package/src/commands/doctor/doctor.ts +104 -44
  90. package/src/commands/env-autoload/env-autoload.ts +12 -0
  91. package/src/commands/env-autoload/index.ts +1 -0
  92. package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
  93. package/src/commands/env-clear/env-clear.ts +22 -8
  94. package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
  95. package/src/commands/env-load/env-load.ts +152 -35
  96. package/src/commands/env-load/index.ts +2 -1
  97. package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
  98. package/src/commands/env-status/env-status.ts +19 -7
  99. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
  100. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
  101. package/src/commands/init/__tests__/agent-files.test.ts +145 -27
  102. package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
  103. package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
  104. package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
  105. package/src/commands/init/__tests__/shell-body.test.ts +30 -0
  106. package/src/commands/init/agent-files.ts +65 -45
  107. package/src/commands/init/init.ts +140 -31
  108. package/src/commands/init/migrate-config.ts +160 -32
  109. package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
  110. package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
  111. package/src/commands/vendor-check/index.ts +1 -0
  112. package/src/commands/vendor-check/vendor-check.ts +127 -0
  113. package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
  114. package/src/commands/vendor-config/index.ts +1 -0
  115. package/src/commands/vendor-config/vendor-config.ts +162 -0
  116. package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
  117. package/src/commands/vendor-diff/index.ts +1 -0
  118. package/src/commands/vendor-diff/vendor-diff.ts +77 -0
  119. package/src/commands/vendor-manifest/index.ts +1 -0
  120. package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
  121. package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
  122. package/src/commands/vendor-sync/index.ts +1 -0
  123. package/src/commands/vendor-sync/vendor-sync.ts +90 -0
  124. package/src/commands/worktrees-add/worktrees-add.ts +1 -1
  125. package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
  126. package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
  127. package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
  128. package/src/entry/cli.ts +417 -210
  129. package/src/entry/index.ts +7 -1
  130. package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
  131. package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
  132. package/src/integrations/cmux/list-workspace-titles.ts +1 -1
  133. package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
  134. package/src/integrations/cursor/open-cursor-workspace.ts +6 -11
  135. package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
  136. package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
  137. package/src/integrations/doppler/doppler-errors.ts +88 -0
  138. package/src/integrations/doppler/doppler-project.ts +80 -0
  139. package/src/integrations/doppler/index.ts +3 -1
  140. package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
  141. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
  142. package/src/integrations/ide/__tests__/ide-facade.test.ts +49 -10
  143. package/src/integrations/ide/open-ide-workspace.ts +6 -6
  144. package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
  145. package/src/integrations/ide/types.ts +10 -1
  146. package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
  147. package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
  148. package/src/integrations/zed/index.ts +1 -0
  149. package/src/integrations/zed/open-zed-workspace.ts +4 -10
  150. package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
  151. package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
  152. package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
  153. package/src/lib/command-catalog/command-catalog.ts +141 -0
  154. package/src/lib/command-catalog/index.ts +2 -0
  155. package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
  156. package/src/lib/constants/constants.ts +13 -0
  157. package/src/lib/constants/index.ts +2 -0
  158. package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
  159. package/src/lib/env-autoload/env-autoload.ts +294 -0
  160. package/src/lib/env-autoload/index.ts +9 -0
  161. package/src/lib/errors/operation-error.ts +1 -1
  162. package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
  163. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
  164. package/src/lib/infra-kit-config/index.ts +9 -1
  165. package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
  166. package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
  167. package/src/lib/json-output/index.ts +1 -0
  168. package/src/lib/json-output/json-output.ts +47 -0
  169. package/src/lib/managed-block/managed-block.ts +4 -4
  170. package/src/lib/path-display/index.ts +1 -0
  171. package/src/lib/path-display/path-display.ts +42 -0
  172. package/src/lib/render/__tests__/render.test.ts +48 -0
  173. package/src/lib/render/index.ts +1 -0
  174. package/src/lib/render/render.ts +24 -0
  175. package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
  176. package/src/lib/vendor/__tests__/config.test.ts +63 -0
  177. package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
  178. package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
  179. package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
  180. package/src/lib/vendor/config-schema.ts +78 -0
  181. package/src/lib/vendor/config.ts +58 -0
  182. package/src/lib/vendor/factory-config-schema.ts +35 -0
  183. package/src/lib/vendor/factory-config.ts +91 -0
  184. package/src/lib/vendor/hash.ts +16 -0
  185. package/src/lib/vendor/index.ts +23 -0
  186. package/src/lib/vendor/manifest.ts +151 -0
  187. package/src/lib/vendor/skip-sets.ts +48 -0
  188. package/src/lib/vendor/sync-ops.ts +290 -0
  189. package/src/lib/vendor/walk.ts +56 -0
  190. package/src/mcp/tools/index.ts +5 -42
  191. package/src/sanity.test.tsx +1 -0
  192. package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
  193. package/src/tui/boot.tsx +45 -0
  194. package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
  195. package/src/tui/screens/command-palette.tsx +159 -0
  196. package/src/tui/types.ts +8 -0
  197. package/tsconfig.json +1 -0
  198. package/tsconfig.tsbuildinfo +1 -1
  199. package/vitest.config.ts +8 -0
  200. package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
  201. package/src/commands/worktrees-open/index.ts +0 -1
  202. package/src/commands/worktrees-open/worktrees-open.ts +0 -195
@@ -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'
@@ -0,0 +1,127 @@
1
+ import { existsSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { z } from 'zod'
5
+
6
+ import { getProjectRoot } from 'src/lib/git-utils'
7
+ import { logger } from 'src/lib/logger'
8
+ import { MANIFEST_FILE, VENDOR_DIR, compareToManifest, isUnknownSchema, readManifest } from 'src/lib/vendor'
9
+ import { defineMcpTool, textContent } from 'src/types'
10
+
11
+ /** Max drifted paths to print per category before truncating. */
12
+ const MAX_REPORTED = 30
13
+
14
+ type VendorCheckStatus = 'clean' | 'drift' | 'skipped' | 'missing-manifest' | 'unknown-schema'
15
+
16
+ interface VendorCheckOptions {
17
+ /** Repo root to check. Defaults to the git toplevel. */
18
+ cwd?: string
19
+ }
20
+
21
+ const report = (label: string, list: string[]): void => {
22
+ if (list.length === 0) {
23
+ return
24
+ }
25
+
26
+ logger.info(`\n${label} (${list.length}):`)
27
+
28
+ for (const rel of list.slice(0, MAX_REPORTED)) {
29
+ logger.info(` ${rel}`)
30
+ }
31
+
32
+ if (list.length > MAX_REPORTED) {
33
+ logger.info(` …and ${list.length - MAX_REPORTED} more`)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Verify that files under `vendor/` still match the checksums recorded in
39
+ * `vendor/.sync-manifest.json`. Config-free and self-contained (no source repo,
40
+ * no `vendor.config.ts`, no rsync) so it runs in any consumer's CI. Read-only —
41
+ * never calls `process.exit`; the CLI action maps `structuredContent.ok` to the
42
+ * exit code, and the MCP tool reuses the same handler.
43
+ *
44
+ * Exit-code contract (preserved from the legacy `vendor-check.mjs`):
45
+ * clean → ok; drift → not ok; missing `vendor/` → ok (skip);
46
+ * missing manifest → not ok; unknown (newer) schemaVersion → not ok (fail-closed).
47
+ *
48
+ * @example
49
+ * await vendorCheck() // checks the current repo's vendor/
50
+ * await vendorCheck({ cwd: '/repo' }) // checks a specific root (tests)
51
+ */
52
+ export const vendorCheck = async (options: VendorCheckOptions = {}) => {
53
+ const root = options.cwd ?? (await getProjectRoot())
54
+ const vendorRoot = path.join(root, VENDOR_DIR)
55
+
56
+ const base = { modified: [] as string[], added: [] as string[], removed: [] as string[], fileCount: 0 }
57
+
58
+ if (!existsSync(vendorRoot)) {
59
+ logger.info(`ℹ️ No ${VENDOR_DIR}/ folder found — nothing to check.`)
60
+ const structuredContent = { status: 'skipped' as VendorCheckStatus, ok: true, ...base }
61
+
62
+ return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
63
+ }
64
+
65
+ if (!existsSync(path.join(vendorRoot, MANIFEST_FILE))) {
66
+ logger.error(`❌ Missing ${VENDOR_DIR}/${MANIFEST_FILE}. Re-run the vendor sync to generate it.`)
67
+ const structuredContent = { status: 'missing-manifest' as VendorCheckStatus, ok: false, ...base }
68
+
69
+ return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
70
+ }
71
+
72
+ const manifest = readManifest(vendorRoot)
73
+
74
+ if (isUnknownSchema(manifest)) {
75
+ logger.error(
76
+ `❌ ${VENDOR_DIR}/${MANIFEST_FILE} has a newer schemaVersion (${String(manifest.schemaVersion)}) than this CLI supports. Upgrade infra-kit.`,
77
+ )
78
+ const structuredContent = { status: 'unknown-schema' as VendorCheckStatus, ok: false, ...base }
79
+
80
+ return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
81
+ }
82
+
83
+ const { modified, added, removed } = compareToManifest(vendorRoot, manifest)
84
+ const drifted = modified.length > 0 || added.length > 0 || removed.length > 0
85
+
86
+ if (drifted) {
87
+ logger.error(`❌ ${VENDOR_DIR}/ has drifted from ${VENDOR_DIR}/${MANIFEST_FILE}.`)
88
+ logger.error(' These files are mirrored from the source repo — edit them upstream, not here.')
89
+ report('Modified', modified)
90
+ report('Added (not in manifest)', added)
91
+ report('Removed (in manifest, now missing)', removed)
92
+ } else {
93
+ logger.info(`✅ ${VENDOR_DIR}/ matches manifest (${manifest.fileCount} files).`)
94
+ }
95
+
96
+ const structuredContent = {
97
+ status: (drifted ? 'drift' : 'clean') as VendorCheckStatus,
98
+ ok: !drifted,
99
+ fileCount: manifest.fileCount,
100
+ modified,
101
+ added,
102
+ removed,
103
+ }
104
+
105
+ return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
106
+ }
107
+
108
+ const vendorCheckOutputSchema = {
109
+ status: z.enum(['clean', 'drift', 'skipped', 'missing-manifest', 'unknown-schema']),
110
+ ok: z.boolean().describe('Whether the vendor tree matches its manifest (false → CI should fail)'),
111
+ fileCount: z.number(),
112
+ modified: z.array(z.string()),
113
+ added: z.array(z.string()),
114
+ removed: z.array(z.string()),
115
+ }
116
+
117
+ // MCP Tool Registration (read-only; safe to expose).
118
+ export const vendorCheckMcpTool = defineMcpTool({
119
+ name: 'vendor-check',
120
+ description:
121
+ 'Verify that files under vendor/ match the checksums in vendor/.sync-manifest.json. Self-contained (no source repo or config needed). Returns ok=false on drift, missing manifest, or an unknown future schemaVersion; ok=true when clean or when no vendor/ folder exists.',
122
+ inputSchema: {},
123
+ outputSchema: vendorCheckOutputSchema,
124
+ handler: () => {
125
+ return vendorCheck({ cwd: process.cwd() })
126
+ },
127
+ })
@@ -0,0 +1,103 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import process from 'node:process'
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6
+
7
+ import { getFactoryConfigPath } from 'src/lib/vendor/factory-config'
8
+
9
+ import { vendorConfig } from '../vendor-config'
10
+
11
+ let home: string
12
+ let source: string
13
+ let factoryPath: string
14
+
15
+ const writeFactory = (workspaceDir: string, targets: string[]): void => {
16
+ fs.mkdirSync(path.dirname(factoryPath), { recursive: true })
17
+ fs.writeFileSync(factoryPath, JSON.stringify({ workspaceDir, targets }), 'utf8')
18
+ }
19
+
20
+ beforeEach(() => {
21
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-config-home-'))
22
+ source = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-config-src-'))
23
+ vi.spyOn(os, 'homedir').mockReturnValue(home)
24
+ factoryPath = getFactoryConfigPath()
25
+ process.exitCode = 0
26
+ })
27
+
28
+ afterEach(() => {
29
+ vi.restoreAllMocks()
30
+ fs.rmSync(home, { recursive: true, force: true })
31
+ fs.rmSync(source, { recursive: true, force: true })
32
+ process.exitCode = 0
33
+ })
34
+
35
+ describe('vendorConfig (print)', () => {
36
+ it('exits non-zero when the factory config is absent', async () => {
37
+ await vendorConfig()
38
+
39
+ expect(process.exitCode).toBe(1)
40
+ })
41
+
42
+ it('exits zero when workspaceDir and every target are reachable', async () => {
43
+ const workspaceDir = path.join(home, 'projects')
44
+
45
+ fs.mkdirSync(path.join(workspaceDir, 'repo-a'), { recursive: true })
46
+ writeFactory(workspaceDir, ['repo-a'])
47
+
48
+ await vendorConfig()
49
+
50
+ expect(process.exitCode).toBe(0)
51
+ })
52
+
53
+ it('exits non-zero when a target is missing', async () => {
54
+ const workspaceDir = path.join(home, 'projects')
55
+
56
+ fs.mkdirSync(workspaceDir, { recursive: true })
57
+ writeFactory(workspaceDir, ['missing-repo'])
58
+
59
+ await vendorConfig()
60
+
61
+ expect(process.exitCode).toBe(1)
62
+ })
63
+ })
64
+
65
+ describe('vendorConfig --init', () => {
66
+ it('scaffolds the factory config as strict JSON with a placeholder workspaceDir', async () => {
67
+ await vendorConfig({ init: true, cwd: source })
68
+
69
+ const written = fs.readFileSync(factoryPath, 'utf8')
70
+ const parsed = JSON.parse(written)
71
+
72
+ // Strict JSON — no executable export, no `infra-kit` import.
73
+ expect(written).not.toContain('export default')
74
+ expect(written).not.toContain(`from 'infra-kit'`)
75
+ expect(parsed.workspaceDir).toBe('~/projects')
76
+ })
77
+
78
+ it('seeds targets from a legacy source vendor.config.ts', async () => {
79
+ // The SOURCE config stays TypeScript (only the machine-local factory file moved to JSON).
80
+ fs.writeFileSync(
81
+ path.join(source, 'vendor.config.ts'),
82
+ `export default { targets: ['travelist-monorepo', 'hulyo-monorepo'], copy: [] }`,
83
+ 'utf8',
84
+ )
85
+
86
+ await vendorConfig({ init: true, cwd: source })
87
+
88
+ const parsed = JSON.parse(fs.readFileSync(factoryPath, 'utf8'))
89
+
90
+ expect(parsed.targets).toEqual(['travelist-monorepo', 'hulyo-monorepo'])
91
+ })
92
+
93
+ it('leaves an existing factory config untouched', async () => {
94
+ const workspaceDir = path.join(home, 'projects')
95
+
96
+ writeFactory(workspaceDir, ['original-repo'])
97
+ const before = fs.readFileSync(factoryPath, 'utf8')
98
+
99
+ await vendorConfig({ init: true, cwd: source })
100
+
101
+ expect(fs.readFileSync(factoryPath, 'utf8')).toBe(before)
102
+ })
103
+ })
@@ -0,0 +1 @@
1
+ export { vendorConfig } from './vendor-config'