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
@@ -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'
@@ -0,0 +1,162 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ import { getProjectRoot } from 'src/lib/git-utils'
7
+ import { logger } from 'src/lib/logger'
8
+ import { fileExists, tildify } from 'src/lib/path-display'
9
+ import { VENDOR_CONFIG_FILE } from 'src/lib/vendor/config-schema'
10
+ import { expandTilde, getFactoryConfigPath, loadFactoryConfig } from 'src/lib/vendor/factory-config'
11
+
12
+ interface VendorConfigOptions {
13
+ /** Scaffold `~/.infra-kit/vendor.json` instead of printing the current one. */
14
+ init?: boolean
15
+ /** Source repo root used for legacy `targets` auto-seeding. Defaults to the git toplevel. */
16
+ cwd?: string
17
+ }
18
+
19
+ /** Placeholder workspace dir written by `--init`; the user edits it to their layout. */
20
+ const PLACEHOLDER_WORKSPACE_DIR = '~/projects'
21
+
22
+ /**
23
+ * Surface or scaffold the machine-local factory config
24
+ * (`~/.infra-kit/vendor.json`). CLI-only β€” NOT an MCP tool; returns nothing
25
+ * and signals problems via `process.exitCode`.
26
+ *
27
+ * Without `--init`: prints the factory file path + existence, the resolved
28
+ * `workspaceDir` + existence, and per-target reachability (`[βœ“]`/`[ ]`). Exits
29
+ * non-zero if the file is missing, the workspace dir is missing, or any target
30
+ * is unreachable, so it is usable as a doctor check.
31
+ *
32
+ * With `--init`: scaffolds the file (skipping if it already exists), seeding
33
+ * `targets` from a legacy source `vendor.config.ts` when one is readable.
34
+ */
35
+ export const vendorConfig = async (options: VendorConfigOptions = {}): Promise<void> => {
36
+ if (options.init) {
37
+ await initFactoryConfig(options.cwd)
38
+
39
+ return
40
+ }
41
+
42
+ await printFactoryConfig()
43
+ }
44
+
45
+ /** Render the factory config chain with `[βœ“]`/`[ ]` reachability markers. */
46
+ const printFactoryConfig = async (): Promise<void> => {
47
+ const factoryPath = getFactoryConfigPath()
48
+ const exists = await fileExists(factoryPath)
49
+
50
+ logger.info(`Factory config: ${tildify(factoryPath)} ${exists ? '[βœ“]' : '[ ]'}`)
51
+
52
+ if (!exists) {
53
+ logger.info('\nNot found β€” run `infra-kit vendor-config --init` to scaffold it.')
54
+ process.exitCode = 1
55
+
56
+ return
57
+ }
58
+
59
+ const { workspaceDir, targets } = await loadFactoryConfig()
60
+ const resolvedWorkspace = expandTilde(workspaceDir)
61
+ const workspaceExists = await fileExists(resolvedWorkspace)
62
+
63
+ logger.info(
64
+ `workspaceDir: ${workspaceDir} (resolved: ${resolvedWorkspace}) ${workspaceExists ? '[βœ“ exists]' : '[ ] not found'}`,
65
+ )
66
+ logger.info('Targets:')
67
+
68
+ let allReachable = workspaceExists
69
+
70
+ for (const repo of targets) {
71
+ const targetPath = path.join(resolvedWorkspace, repo)
72
+ const reachable = await fileExists(targetPath)
73
+
74
+ if (!reachable) {
75
+ allReachable = false
76
+ }
77
+
78
+ const marker = reachable ? '[βœ“]' : '[ ]'
79
+ const suffix = reachable ? '' : ' (not found β€” clone or remove)'
80
+
81
+ logger.info(` ${marker} ${repo} ${tildify(targetPath)}${suffix}`)
82
+ }
83
+
84
+ if (!allReachable) {
85
+ process.exitCode = 1
86
+ }
87
+ }
88
+
89
+ /** Scaffold `~/.infra-kit/vendor.json`, skipping if it already exists. */
90
+ const initFactoryConfig = async (cwd?: string): Promise<void> => {
91
+ const factoryPath = getFactoryConfigPath()
92
+
93
+ if (await fileExists(factoryPath)) {
94
+ logger.info(`Factory config already exists at ${tildify(factoryPath)} β€” leaving it untouched.`)
95
+
96
+ return
97
+ }
98
+
99
+ const sourceRoot = cwd ?? (await getProjectRoot())
100
+ const seededTargets = await readLegacyTargets(sourceRoot)
101
+
102
+ await fs.mkdir(path.dirname(factoryPath), { recursive: true })
103
+ await fs.writeFile(factoryPath, buildScaffold(seededTargets), 'utf-8')
104
+
105
+ logger.info(`βœ“ Created ${tildify(factoryPath)}`)
106
+
107
+ if (seededTargets.length > 0) {
108
+ logger.info(` Seeded ${seededTargets.length} target(s) from the source ${VENDOR_CONFIG_FILE}.`)
109
+ }
110
+
111
+ logger.info(` Edit \`workspaceDir\` (placeholder: ${PLACEHOLDER_WORKSPACE_DIR}) to point at where your repos live.`)
112
+
113
+ if (seededTargets.length === 0) {
114
+ logger.info(' Add at least one repo name to `targets` before running vendor sync/manifest/diff.')
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Best-effort read of a legacy `targets` array from the source repo's
120
+ * `vendor.config.ts`. The current schema no longer accepts `targets`, so this
121
+ * reads the raw default export directly (bypassing validation). Returns `[]` on
122
+ * any failure β€” seeding is a convenience, never a hard requirement.
123
+ */
124
+ const readLegacyTargets = async (sourceRoot: string): Promise<string[]> => {
125
+ try {
126
+ const configPath = path.join(sourceRoot, VENDOR_CONFIG_FILE)
127
+ const stat = await fs.stat(configPath)
128
+ const moduleUrl = `${pathToFileURL(configPath).href}?mtime=${Number(stat.mtimeMs)}`
129
+ const imported = (await import(moduleUrl)) as { default?: unknown }
130
+ const raw = imported.default
131
+ const resolved = typeof raw === 'function' ? await (raw as () => unknown)() : raw
132
+
133
+ if (resolved && typeof resolved === 'object' && 'targets' in resolved) {
134
+ const targets = (resolved as { targets?: unknown }).targets
135
+
136
+ if (
137
+ Array.isArray(targets) &&
138
+ targets.every((t) => {
139
+ return typeof t === 'string'
140
+ })
141
+ ) {
142
+ return targets as string[]
143
+ }
144
+ }
145
+ } catch {
146
+ // Absent or unreadable source config β€” fall through to an empty placeholder.
147
+ }
148
+
149
+ return []
150
+ }
151
+
152
+ /**
153
+ * Render the scaffold file body as strict JSON (`vendor.json`). The factory config
154
+ * is static JSON, loaded with `JSON.parse` β€” no comments, no executable code. When
155
+ * `targets` is empty the stub writes `"targets": []`, which fails the schema's
156
+ * `targets.min(1)` on load: this is intentional β€” `--init` produces an incomplete
157
+ * stub the user must edit before running vendor sync/manifest/diff. The annotated
158
+ * guidance lives in the sibling `vendor.example.jsonc` (seeded by `infra-kit init`).
159
+ */
160
+ const buildScaffold = (targets: string[]): string => {
161
+ return `${JSON.stringify({ workspaceDir: PLACEHOLDER_WORKSPACE_DIR, targets }, null, 2)}\n`
162
+ }
@@ -0,0 +1,74 @@
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 { execMock, zxModuleMock } from 'src/lib/vendor/__tests__/zx-mock'
7
+
8
+ import { vendorDiff } from '../vendor-diff'
9
+
10
+ vi.mock('zx', () => {
11
+ return zxModuleMock()
12
+ })
13
+
14
+ let parent: string
15
+ let home: string
16
+ let sourceRoot: string
17
+ let targetRoot: string
18
+
19
+ const write = (root: string, rel: string, content: string): void => {
20
+ const full = path.join(root, rel)
21
+
22
+ fs.mkdirSync(path.dirname(full), { recursive: true })
23
+ fs.writeFileSync(full, content, 'utf8')
24
+ }
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks()
28
+
29
+ parent = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-diff-test-'))
30
+ sourceRoot = path.join(parent, 'source-repo')
31
+ targetRoot = path.join(parent, 'target-repo')
32
+
33
+ // Machine-local factory config (workspaceDir + targets) lives under ~/.infra-kit;
34
+ // point os.homedir() at a fixture so the loader finds it.
35
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-diff-home-'))
36
+ vi.spyOn(os, 'homedir').mockReturnValue(home)
37
+ write(home, '.infra-kit/vendor.json', JSON.stringify({ workspaceDir: parent, targets: ['target-repo'] }))
38
+
39
+ write(sourceRoot, 'vendor/configs/a.js', 'aaa')
40
+ write(
41
+ sourceRoot,
42
+ 'vendor.config.ts',
43
+ `export default { copy: [{ name: 'Configs', source: 'vendor/configs', target: 'vendor/configs', type: 'directory', vendored: true }] }`,
44
+ )
45
+ write(targetRoot, 'vendor/configs/a.js', 'DIFFERENT')
46
+ })
47
+
48
+ afterEach(() => {
49
+ vi.restoreAllMocks()
50
+ fs.rmSync(parent, { recursive: true, force: true })
51
+ fs.rmSync(home, { recursive: true, force: true })
52
+ })
53
+
54
+ describe('vendorDiff', () => {
55
+ it('reports drift when rsync dry-run returns changes', async () => {
56
+ execMock.mockResolvedValue({ stdout: '>f.st..... configs/a.js\n' })
57
+
58
+ const { structuredContent } = await vendorDiff({ cwd: sourceRoot })
59
+
60
+ expect(structuredContent.ok).toBe(false)
61
+ expect(structuredContent.drifted).toBe(1)
62
+ expect(structuredContent.entries[0]?.repo).toBe('target-repo')
63
+ expect(structuredContent.entries[0]?.changes).toContain('>f.st..... configs/a.js')
64
+ })
65
+
66
+ it('reports no drift when rsync dry-run is empty', async () => {
67
+ execMock.mockResolvedValue({ stdout: '' })
68
+
69
+ const { structuredContent } = await vendorDiff({ cwd: sourceRoot })
70
+
71
+ expect(structuredContent.ok).toBe(true)
72
+ expect(structuredContent.drifted).toBe(0)
73
+ })
74
+ })
@@ -0,0 +1 @@
1
+ export { vendorDiff, vendorDiffMcpTool } from './vendor-diff'
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod'
2
+
3
+ import { getProjectRoot } from 'src/lib/git-utils'
4
+ import { logger } from 'src/lib/logger'
5
+ import { loadVendorConfig } from 'src/lib/vendor/config'
6
+ import { loadFactoryConfig } from 'src/lib/vendor/factory-config'
7
+ import { runDiff, selectTargets } from 'src/lib/vendor/sync-ops'
8
+ import { defineMcpTool, textContent } from 'src/types'
9
+
10
+ /** Max itemized changes to print per drifted path before truncating. */
11
+ const MAX_REPORTED = 20
12
+
13
+ interface VendorDiffOptions {
14
+ cwd?: string
15
+ repos?: string[]
16
+ }
17
+
18
+ /**
19
+ * Source-aware drift check: compare each target repo's `vendored:true` subtree
20
+ * against the source via `rsync --dry-run --delete`. Read-only (does NOT extend
21
+ * RequiredConfirmedOptionArg).
22
+ */
23
+ export const vendorDiff = async (options: VendorDiffOptions = {}) => {
24
+ const sourceRoot = options.cwd ?? (await getProjectRoot())
25
+
26
+ const config = await loadVendorConfig(sourceRoot)
27
+ const factory = await loadFactoryConfig()
28
+ const targets = selectTargets(factory.targets, options.repos)
29
+
30
+ const entries = await runDiff(config, sourceRoot, factory.workspaceDir, targets)
31
+
32
+ if (entries.length === 0) {
33
+ logger.info('πŸŽ‰ No drift detected.')
34
+ } else {
35
+ for (const entry of entries) {
36
+ logger.info(`❌ ${entry.repo} β†’ ${entry.target} (${entry.changes.length} change(s)):`)
37
+
38
+ for (const change of entry.changes.slice(0, MAX_REPORTED)) {
39
+ logger.info(` ${change}`)
40
+ }
41
+
42
+ if (entry.changes.length > MAX_REPORTED) {
43
+ logger.info(` …and ${entry.changes.length - MAX_REPORTED} more`)
44
+ }
45
+ }
46
+ }
47
+
48
+ const structuredContent = { ok: entries.length === 0, drifted: entries.length, entries }
49
+
50
+ return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
51
+ }
52
+
53
+ const vendorDiffOutputSchema = {
54
+ ok: z.boolean().describe('Whether every target is in sync with the source'),
55
+ drifted: z.number(),
56
+ entries: z.array(
57
+ z.object({
58
+ repo: z.string(),
59
+ target: z.string(),
60
+ changes: z.array(z.string()),
61
+ }),
62
+ ),
63
+ }
64
+
65
+ // MCP Tool Registration (read-only; safe to expose).
66
+ export const vendorDiffMcpTool = defineMcpTool({
67
+ name: 'vendor-diff',
68
+ description:
69
+ 'Source-aware drift check: compare each target repo vendored subtree against the source via rsync dry-run. Read-only; run from the source repo root. ok=false when any target has drifted.',
70
+ inputSchema: {
71
+ repos: z.array(z.string()).optional().describe('Restrict to these target repo names'),
72
+ },
73
+ outputSchema: vendorDiffOutputSchema,
74
+ handler: (params) => {
75
+ return vendorDiff({ repos: params.repos })
76
+ },
77
+ })
@@ -0,0 +1 @@
1
+ export { vendorManifest, vendorManifestMcpTool } from './vendor-manifest'
@@ -0,0 +1,60 @@
1
+ import path from 'node:path'
2
+ import { z } from 'zod'
3
+
4
+ import { getProjectRoot } from 'src/lib/git-utils'
5
+ import { logger } from 'src/lib/logger'
6
+ import { loadFactoryConfig } from 'src/lib/vendor/factory-config'
7
+ import { getSourceCommit, runManifest, selectTargets } from 'src/lib/vendor/sync-ops'
8
+ import { defineMcpTool, textContent } from 'src/types'
9
+ import type { RequiredConfirmedOptionArg } from 'src/types'
10
+
11
+ interface VendorManifestOptions extends RequiredConfirmedOptionArg {
12
+ cwd?: string
13
+ repos?: string[]
14
+ }
15
+
16
+ /**
17
+ * Regenerate each target repo's `vendor/.sync-manifest.json` + README from the
18
+ * CURRENT vendor content, without copying anything. Mutating (writes manifests).
19
+ */
20
+ export const vendorManifest = async (options: VendorManifestOptions) => {
21
+ const sourceRoot = options.cwd ?? (await getProjectRoot())
22
+
23
+ const factory = await loadFactoryConfig()
24
+ const targets = selectTargets(factory.targets, options.repos)
25
+
26
+ const commit = await getSourceCommit(sourceRoot)
27
+ const source = path.basename(sourceRoot)
28
+
29
+ const repos = await runManifest(factory.workspaceDir, targets, { source, commit })
30
+
31
+ for (const repo of repos) {
32
+ logger.info(
33
+ repo.skipped ? `⚠️ ${repo.repo}: target does not exist β€” skipped` : `πŸ“ ${repo.repo}: manifest written`,
34
+ )
35
+ }
36
+
37
+ const structuredContent = { source, commit, repos }
38
+
39
+ return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
40
+ }
41
+
42
+ const vendorManifestOutputSchema = {
43
+ source: z.string(),
44
+ commit: z.string(),
45
+ repos: z.array(z.object({ repo: z.string(), skipped: z.boolean() })),
46
+ }
47
+
48
+ // MCP Tool Registration (mutating).
49
+ export const vendorManifestMcpTool = defineMcpTool({
50
+ name: 'vendor-manifest',
51
+ description:
52
+ 'Regenerate each target repo vendor/.sync-manifest.json + README from current content without copying. Mutating; run from the source repo root.',
53
+ inputSchema: {
54
+ repos: z.array(z.string()).optional().describe('Restrict to these target repo names'),
55
+ },
56
+ outputSchema: vendorManifestOutputSchema,
57
+ handler: (params) => {
58
+ return vendorManifest({ repos: params.repos, confirmedCommand: params.confirmedCommand })
59
+ },
60
+ })