infra-kit 0.1.114 → 0.1.117

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 (204) 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/{src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2 → .omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4}/pre-tool-advisory-throttle.json +3 -3
  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-eslint-check.log +2 -0
  58. package/.turbo/turbo-test.log +90 -42
  59. package/dist/boot-K4QUAFSP.js +2 -0
  60. package/dist/boot-K4QUAFSP.js.map +7 -0
  61. package/dist/chunk-FEGOOVZE.js +166 -0
  62. package/dist/chunk-FEGOOVZE.js.map +7 -0
  63. package/dist/chunk-WA4BQRDC.js +2 -0
  64. package/dist/chunk-WA4BQRDC.js.map +7 -0
  65. package/dist/cli.js +11 -107
  66. package/dist/cli.js.map +4 -4
  67. package/dist/entry/index.d.ts +2 -0
  68. package/dist/index.js +1 -1
  69. package/dist/index.js.map +4 -4
  70. package/dist/lib/vendor/config-schema.d.ts +58 -0
  71. package/dist/mcp.js +1 -54
  72. package/dist/mcp.js.map +4 -4
  73. package/eslint.config.js +33 -1
  74. package/package.json +5 -1
  75. package/scripts/build.js +11 -1
  76. package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
  77. package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
  78. package/src/.omc/state/idle-notif-cooldown.json +1 -1
  79. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
  80. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
  81. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
  82. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  83. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
  84. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
  85. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  86. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  87. package/src/commands/config/config.ts +7 -37
  88. package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
  89. package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
  90. package/src/commands/doctor/doctor.ts +104 -44
  91. package/src/commands/env-autoload/env-autoload.ts +12 -0
  92. package/src/commands/env-autoload/index.ts +1 -0
  93. package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
  94. package/src/commands/env-clear/env-clear.ts +22 -8
  95. package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
  96. package/src/commands/env-load/env-load.ts +152 -35
  97. package/src/commands/env-load/index.ts +2 -1
  98. package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
  99. package/src/commands/env-status/env-status.ts +19 -7
  100. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
  101. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
  102. package/src/commands/init/__tests__/agent-files.test.ts +145 -27
  103. package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
  104. package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
  105. package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
  106. package/src/commands/init/__tests__/shell-body.test.ts +30 -0
  107. package/src/commands/init/agent-files.ts +65 -45
  108. package/src/commands/init/init.ts +140 -31
  109. package/src/commands/init/migrate-config.ts +160 -32
  110. package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
  111. package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
  112. package/src/commands/vendor-check/index.ts +1 -0
  113. package/src/commands/vendor-check/vendor-check.ts +127 -0
  114. package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
  115. package/src/commands/vendor-config/index.ts +1 -0
  116. package/src/commands/vendor-config/vendor-config.ts +162 -0
  117. package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
  118. package/src/commands/vendor-diff/index.ts +1 -0
  119. package/src/commands/vendor-diff/vendor-diff.ts +77 -0
  120. package/src/commands/vendor-manifest/index.ts +1 -0
  121. package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
  122. package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
  123. package/src/commands/vendor-sync/index.ts +1 -0
  124. package/src/commands/vendor-sync/vendor-sync.ts +90 -0
  125. package/src/commands/worktrees-add/worktrees-add.ts +1 -1
  126. package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
  127. package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
  128. package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
  129. package/src/entry/cli.ts +417 -210
  130. package/src/entry/index.ts +7 -1
  131. package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
  132. package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
  133. package/src/integrations/cmux/list-workspace-titles.ts +1 -1
  134. package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
  135. package/src/integrations/cursor/open-cursor-workspace.ts +6 -11
  136. package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
  137. package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
  138. package/src/integrations/doppler/doppler-errors.ts +88 -0
  139. package/src/integrations/doppler/doppler-project.ts +80 -0
  140. package/src/integrations/doppler/index.ts +3 -1
  141. package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
  142. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
  143. package/src/integrations/ide/__tests__/ide-facade.test.ts +49 -10
  144. package/src/integrations/ide/open-ide-workspace.ts +6 -6
  145. package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
  146. package/src/integrations/ide/types.ts +10 -1
  147. package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
  148. package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
  149. package/src/integrations/zed/index.ts +1 -0
  150. package/src/integrations/zed/open-zed-workspace.ts +4 -10
  151. package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
  152. package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
  153. package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
  154. package/src/lib/command-catalog/command-catalog.ts +141 -0
  155. package/src/lib/command-catalog/index.ts +2 -0
  156. package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
  157. package/src/lib/constants/constants.ts +13 -0
  158. package/src/lib/constants/index.ts +2 -0
  159. package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
  160. package/src/lib/env-autoload/env-autoload.ts +294 -0
  161. package/src/lib/env-autoload/index.ts +9 -0
  162. package/src/lib/errors/operation-error.ts +1 -1
  163. package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
  164. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
  165. package/src/lib/infra-kit-config/index.ts +9 -1
  166. package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
  167. package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
  168. package/src/lib/json-output/index.ts +1 -0
  169. package/src/lib/json-output/json-output.ts +47 -0
  170. package/src/lib/managed-block/managed-block.ts +4 -4
  171. package/src/lib/package-config/package-config.ts +1 -3
  172. package/src/lib/path-display/index.ts +1 -0
  173. package/src/lib/path-display/path-display.ts +42 -0
  174. package/src/lib/render/__tests__/render.test.ts +48 -0
  175. package/src/lib/render/index.ts +1 -0
  176. package/src/lib/render/render.ts +24 -0
  177. package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
  178. package/src/lib/vendor/__tests__/config.test.ts +63 -0
  179. package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
  180. package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
  181. package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
  182. package/src/lib/vendor/config-schema.ts +78 -0
  183. package/src/lib/vendor/config.ts +58 -0
  184. package/src/lib/vendor/factory-config-schema.ts +35 -0
  185. package/src/lib/vendor/factory-config.ts +91 -0
  186. package/src/lib/vendor/hash.ts +16 -0
  187. package/src/lib/vendor/index.ts +23 -0
  188. package/src/lib/vendor/manifest.ts +151 -0
  189. package/src/lib/vendor/skip-sets.ts +48 -0
  190. package/src/lib/vendor/sync-ops.ts +290 -0
  191. package/src/lib/vendor/walk.ts +56 -0
  192. package/src/mcp/tools/index.ts +5 -42
  193. package/src/sanity.test.tsx +1 -0
  194. package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
  195. package/src/tui/boot.tsx +45 -0
  196. package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
  197. package/src/tui/screens/command-palette.tsx +159 -0
  198. package/src/tui/types.ts +8 -0
  199. package/tsconfig.json +1 -0
  200. package/tsconfig.tsbuildinfo +1 -1
  201. package/vitest.config.ts +8 -0
  202. package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
  203. package/src/commands/worktrees-open/index.ts +0 -1
  204. package/src/commands/worktrees-open/worktrees-open.ts +0 -195
@@ -0,0 +1,151 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { z } from 'zod'
4
+
5
+ import { sha256 } from './hash'
6
+ import { walkVendorTree } from './walk'
7
+
8
+ /** Filename of the integrity manifest inside a `vendor/` tree. */
9
+ export const MANIFEST_FILE = '.sync-manifest.json'
10
+
11
+ /**
12
+ * Manifest schema version the CLI understands. A manifest with a HIGHER version
13
+ * than this was written by a newer CLI and is treated as drift (fail-closed) —
14
+ * see {@link isUnknownSchema}. Legacy manifests carry no `schemaVersion` field
15
+ * and read as compatible.
16
+ */
17
+ export const CURRENT_SCHEMA_VERSION = 1
18
+
19
+ /**
20
+ * `schemaVersion` is OPTIONAL with no default: legacy manifests written by
21
+ * `writeVendorMeta` have no such field, and a required field would make
22
+ * `vendor check` fail-closed on every existing manifest. Absent is read as
23
+ * "compatible" (current). `writeManifest` always emits the current version.
24
+ */
25
+ const manifestSchema = z.object({
26
+ schemaVersion: z.number().optional(),
27
+ source: z.string(),
28
+ commit: z.string(),
29
+ syncedAt: z.string(),
30
+ fileCount: z.number(),
31
+ files: z.record(z.string(), z.string()),
32
+ })
33
+
34
+ export type VendorManifest = z.infer<typeof manifestSchema>
35
+
36
+ export interface ManifestDiff {
37
+ modified: string[]
38
+ added: string[]
39
+ removed: string[]
40
+ }
41
+
42
+ /**
43
+ * Whether a manifest's `schemaVersion` is newer than this CLI supports. Absent
44
+ * (legacy) reads as compatible; only a strictly-greater version is unknown.
45
+ */
46
+ export const isUnknownSchema = (manifest: VendorManifest): boolean => {
47
+ return (manifest.schemaVersion ?? CURRENT_SCHEMA_VERSION) > CURRENT_SCHEMA_VERSION
48
+ }
49
+
50
+ /**
51
+ * Walk the vendor tree and hash every file into a `{ relativePath: sha256 }`
52
+ * map (sorted by walk order → stable). A file that cannot be read (e.g. a
53
+ * dangling symlink) surfaces an explicit error rather than crashing with a raw
54
+ * fs stack.
55
+ */
56
+ export const buildFilesMap = (vendorRoot: string): Record<string, string> => {
57
+ const files: Record<string, string> = {}
58
+
59
+ for (const rel of walkVendorTree(vendorRoot)) {
60
+ try {
61
+ files[rel] = sha256(path.join(vendorRoot, rel))
62
+ } catch (cause) {
63
+ const reason = cause instanceof Error ? cause.message : String(cause)
64
+
65
+ throw new Error(`Cannot hash vendor file "${rel}" (broken symlink or unreadable file): ${reason}`)
66
+ }
67
+ }
68
+
69
+ return files
70
+ }
71
+
72
+ /**
73
+ * Read and validate the manifest at `<vendorRoot>/.sync-manifest.json`. Throws a
74
+ * descriptive error if the file is missing or malformed.
75
+ */
76
+ export const readManifest = (vendorRoot: string): VendorManifest => {
77
+ const manifestPath = path.join(vendorRoot, MANIFEST_FILE)
78
+
79
+ let raw: string
80
+
81
+ try {
82
+ raw = readFileSync(manifestPath, 'utf8')
83
+ } catch {
84
+ throw new Error(`Missing ${MANIFEST_FILE} at ${manifestPath}`)
85
+ }
86
+
87
+ const parsed = manifestSchema.safeParse(JSON.parse(raw))
88
+
89
+ if (!parsed.success) {
90
+ throw new Error(`Invalid ${MANIFEST_FILE} at ${manifestPath}: ${z.prettifyError(parsed.error)}`)
91
+ }
92
+
93
+ return parsed.data
94
+ }
95
+
96
+ /**
97
+ * Build and write a fresh manifest for the current content of `vendorRoot`.
98
+ * Emits the CURRENT schema version, the supplied `source`/`commit`, an ISO
99
+ * `syncedAt`, and the per-file checksum map. Returns the written manifest.
100
+ *
101
+ * The `files` map and field set (minus `schemaVersion`) match legacy
102
+ * `writeVendorMeta`, so a regenerated manifest differs from a legacy one only by
103
+ * the added `schemaVersion` and the `syncedAt` timestamp.
104
+ */
105
+ export const writeManifest = (vendorRoot: string, meta: { source: string; commit: string }): VendorManifest => {
106
+ const files = buildFilesMap(vendorRoot)
107
+
108
+ const manifest: VendorManifest = {
109
+ schemaVersion: CURRENT_SCHEMA_VERSION,
110
+ source: meta.source,
111
+ commit: meta.commit,
112
+ syncedAt: new Date().toISOString(),
113
+ fileCount: Object.keys(files).length,
114
+ files,
115
+ }
116
+
117
+ writeFileSync(path.join(vendorRoot, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`)
118
+
119
+ return manifest
120
+ }
121
+
122
+ /**
123
+ * Compare the live `vendorRoot` tree against a manifest's recorded checksums.
124
+ * Returns the modified / added (present, not in manifest) / removed (in
125
+ * manifest, now missing) relative paths.
126
+ */
127
+ export const compareToManifest = (vendorRoot: string, manifest: VendorManifest): ManifestDiff => {
128
+ const expected = manifest.files
129
+ const actualFiles = walkVendorTree(vendorRoot)
130
+
131
+ const modified: string[] = []
132
+ const added: string[] = []
133
+
134
+ for (const rel of actualFiles) {
135
+ if (!(rel in expected)) {
136
+ added.push(rel)
137
+ continue
138
+ }
139
+
140
+ if (sha256(path.join(vendorRoot, rel)) !== expected[rel]) {
141
+ modified.push(rel)
142
+ }
143
+ }
144
+
145
+ const actualSet = new Set(actualFiles)
146
+ const removed = Object.keys(expected).filter((rel) => {
147
+ return !actualSet.has(rel)
148
+ })
149
+
150
+ return { modified, added, removed }
151
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Skip rules for walking a `vendor/` tree to build or verify its integrity
3
+ * manifest. This is the SINGLE source of truth consumed by both the read path
4
+ * (`vendor check`) and the write path (`vendor sync`/`manifest`).
5
+ *
6
+ * The set below is a strict superset of the legacy read script's
7
+ * `MANIFEST_SKIP_DIRS` (`scripts/vendor-check.mjs`). It additionally skips
8
+ * `.vitest-attachments` — a git-ignored directory (see repo `.gitignore`) that
9
+ * therefore never appears in a committed vendor tree. Widening the read path's
10
+ * skip set this way is deliberate and behavior-preserving: no existing manifest
11
+ * changes, and the read and write paths share this single set so they cannot
12
+ * diverge.
13
+ */
14
+ /** Root of the mirrored tree inside each repo. Single source for read + write paths. */
15
+ export const VENDOR_DIR = 'vendor'
16
+
17
+ export const MANIFEST_SKIP_DIRS: ReadonlySet<string> = new Set([
18
+ 'node_modules',
19
+ 'dist',
20
+ '.turbo',
21
+ '.omc',
22
+ '__screenshots__',
23
+ '.vitest-attachments',
24
+ '.output',
25
+ '.source',
26
+ '.nitro',
27
+ '.tanstack',
28
+ ])
29
+
30
+ export const MANIFEST_SKIP_FILES: ReadonlySet<string> = new Set(['.sync-manifest.json', '.eslintcache', 'log.txt'])
31
+
32
+ export const MANIFEST_SKIP_SUFFIXES: readonly string[] = ['.tsbuildinfo']
33
+
34
+ /** Whether a directory name should be skipped when walking the vendor tree. */
35
+ export const isSkippedDir = (name: string): boolean => {
36
+ return MANIFEST_SKIP_DIRS.has(name)
37
+ }
38
+
39
+ /** Whether a file name should be skipped when walking the vendor tree. */
40
+ export const isSkippedFile = (name: string): boolean => {
41
+ if (MANIFEST_SKIP_FILES.has(name)) {
42
+ return true
43
+ }
44
+
45
+ return MANIFEST_SKIP_SUFFIXES.some((suffix) => {
46
+ return name.endsWith(suffix)
47
+ })
48
+ }
@@ -0,0 +1,290 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { $ } from 'zx'
4
+
5
+ import { OperationError } from 'src/lib/errors/operation-error'
6
+
7
+ import type { VendorConfig } from './config-schema'
8
+ import { expandTilde } from './factory-config'
9
+ import { writeManifest } from './manifest'
10
+ import { VENDOR_DIR } from './skip-sets'
11
+
12
+ /**
13
+ * Write-path operations for `vendor sync`/`manifest`/`diff`. This module imports
14
+ * `zx` at the top level and is NEVER re-exported from the read-path barrel
15
+ * (`./index.ts`); `vendor check` never imports it, so its runtime path stays
16
+ * free of `zx`/rsync.
17
+ */
18
+
19
+ /** rsync excludes mirrored from the legacy sync script. */
20
+ const EXCLUDE_PATTERNS = [
21
+ 'node_modules',
22
+ 'dist',
23
+ '*.tsbuildinfo',
24
+ '.turbo',
25
+ '.eslintcache',
26
+ '.omc',
27
+ '__screenshots__',
28
+ '.vitest-attachments',
29
+ '.output',
30
+ '.source',
31
+ '.nitro',
32
+ '.tanstack',
33
+ 'log.txt',
34
+ ]
35
+
36
+ const VENDOR_README = `# vendor/ — mirrored from the source repo
37
+
38
+ **DO NOT EDIT files in this folder here.**
39
+
40
+ Everything under \`vendor/\` is the single source of truth maintained in the source
41
+ repo and copied into this repo by \`infra-kit vendor sync\`. Local edits are
42
+ overwritten on the next sync and will fail \`infra-kit vendor check\` in CI.
43
+
44
+ To change a vendored package, edit it in the source repo and re-run the sync.
45
+
46
+ See \`.sync-manifest.json\` for the source commit and per-file checksums.
47
+ `
48
+
49
+ const excludeFlags = (): string[] => {
50
+ return EXCLUDE_PATTERNS.map((pattern) => {
51
+ return `--exclude=${pattern}`
52
+ })
53
+ }
54
+
55
+ /** Resolve the source repo's current HEAD commit (or `unknown` if unavailable). */
56
+ export const getSourceCommit = async (sourceRoot: string): Promise<string> => {
57
+ try {
58
+ const result = await $({ cwd: sourceRoot })`git rev-parse HEAD`
59
+
60
+ return result.stdout.trim()
61
+ } catch {
62
+ return 'unknown'
63
+ }
64
+ }
65
+
66
+ /** rsync a directory source→target (target is replaced to avoid nested copies). */
67
+ export const copyDirectory = async (source: string, target: string): Promise<boolean> => {
68
+ if (!existsSync(source)) {
69
+ return false
70
+ }
71
+
72
+ if (existsSync(target)) {
73
+ await $`rm -rf ${target}`
74
+ }
75
+
76
+ const sourceSlash = `${source}/`
77
+ const targetSlash = `${target}/`
78
+
79
+ await $`rsync -a ${excludeFlags()} ${sourceSlash} ${targetSlash}`
80
+
81
+ return true
82
+ }
83
+
84
+ /** Copy a single file source→target, creating the target dir as needed. */
85
+ export const copyFile = async (source: string, target: string): Promise<boolean> => {
86
+ if (!existsSync(source)) {
87
+ return false
88
+ }
89
+
90
+ const targetDir = path.dirname(target)
91
+
92
+ if (!existsSync(targetDir)) {
93
+ mkdirSync(targetDir, { recursive: true })
94
+ }
95
+
96
+ await $`cp ${source} ${target}`
97
+
98
+ return true
99
+ }
100
+
101
+ /**
102
+ * Dry-run rsync reporting whether `target` has drifted from `source`. Returns the
103
+ * itemized change list (empty = in sync). `--delete` surfaces consumer-only files.
104
+ */
105
+ export const diffDirectory = async (source: string, target: string): Promise<string[]> => {
106
+ if (!existsSync(source)) {
107
+ return []
108
+ }
109
+
110
+ if (!existsSync(target)) {
111
+ return [`missing target: ${target}`]
112
+ }
113
+
114
+ const sourceSlash = `${source}/`
115
+ const targetSlash = `${target}/`
116
+ const result = await $`rsync -ai --dry-run --delete ${excludeFlags()} ${sourceSlash} ${targetSlash}`
117
+
118
+ return result.stdout
119
+ .split('\n')
120
+ .map((line) => {
121
+ return line.trim()
122
+ })
123
+ .filter(Boolean)
124
+ }
125
+
126
+ /**
127
+ * Write `vendor/README.md` + `vendor/.sync-manifest.json` for a target repo,
128
+ * mirroring the legacy `writeVendorMeta`. The manifest `files` map matches legacy
129
+ * output; it differs only by the added `schemaVersion` and the `syncedAt` stamp.
130
+ */
131
+ export const writeVendorMeta = (targetRoot: string, meta: { source: string; commit: string }): void => {
132
+ const vendorRoot = path.join(targetRoot, VENDOR_DIR)
133
+
134
+ if (!existsSync(vendorRoot)) {
135
+ return
136
+ }
137
+
138
+ writeFileSync(path.join(vendorRoot, 'README.md'), VENDOR_README)
139
+ writeManifest(vendorRoot, meta)
140
+ }
141
+
142
+ /**
143
+ * Resolve a target repo name to its absolute root under the machine-local
144
+ * `workspaceDir` (from `~/.infra-kit/vendor.json`). Replaces the old
145
+ * "siblings of the source repo" assumption — the working dir is now explicit.
146
+ */
147
+ export const resolveTargetRoot = (workspaceDir: string, repo: string): string => {
148
+ return path.join(expandTilde(workspaceDir), repo)
149
+ }
150
+
151
+ /** Narrow the configured target list to an optional `--repos` allowlist. */
152
+ export const selectTargets = (allTargets: string[], repos?: string[]): string[] => {
153
+ if (!repos) {
154
+ return allTargets
155
+ }
156
+
157
+ return allTargets.filter((repo) => {
158
+ return repos.includes(repo)
159
+ })
160
+ }
161
+
162
+ export interface VendorSyncResult {
163
+ source: string
164
+ commit: string
165
+ repos: { repo: string; copied: number; total: number; skipped: boolean }[]
166
+ }
167
+
168
+ /**
169
+ * Defense-in-depth path-traversal guard: throw if `child` resolves outside
170
+ * `root`. The schema's `safeRelPath` already rejects `..`/absolute segments at
171
+ * load time; this re-checks the resolved absolute paths so a symlink or odd join
172
+ * can never escape the source/target repo root.
173
+ */
174
+ const assertContained = (root: string, child: string, label: string, rel: string, itemName: string): void => {
175
+ const resolvedRoot = path.resolve(root)
176
+ const resolved = path.resolve(child)
177
+
178
+ if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
179
+ throw new OperationError(undefined, {
180
+ operation: `sync vendor item ${itemName}`,
181
+ stderrExcerpt: `${label} escapes repo root: ${rel}`,
182
+ })
183
+ }
184
+ }
185
+
186
+ /** Copy every configured item into each existing target, then write its manifest. */
187
+ export const runSync = async (
188
+ config: VendorConfig,
189
+ sourceRoot: string,
190
+ workspaceDir: string,
191
+ targets: string[],
192
+ meta: { source: string; commit: string },
193
+ ): Promise<VendorSyncResult> => {
194
+ const repos: VendorSyncResult['repos'] = []
195
+
196
+ for (const repo of targets) {
197
+ const targetRoot = resolveTargetRoot(workspaceDir, repo)
198
+
199
+ if (!existsSync(targetRoot)) {
200
+ repos.push({ repo, copied: 0, total: config.copy.length, skipped: true })
201
+ continue
202
+ }
203
+
204
+ let copied = 0
205
+
206
+ for (const item of config.copy) {
207
+ const source = path.join(sourceRoot, item.source)
208
+ const target = path.join(targetRoot, item.target)
209
+
210
+ assertContained(sourceRoot, source, 'source', item.source, item.name)
211
+ assertContained(targetRoot, target, 'target', item.target, item.name)
212
+
213
+ const ok = item.type === 'directory' ? await copyDirectory(source, target) : await copyFile(source, target)
214
+
215
+ if (ok) {
216
+ copied++
217
+ }
218
+ }
219
+
220
+ writeVendorMeta(targetRoot, meta)
221
+ repos.push({ repo, copied, total: config.copy.length, skipped: false })
222
+ }
223
+
224
+ return { source: meta.source, commit: meta.commit, repos }
225
+ }
226
+
227
+ /**
228
+ * Regenerate the manifest+README in each existing target without copying. Needs
229
+ * neither the source `config` (no `copy[]` involved) nor `sourceRoot` (it writes
230
+ * only into targets) — just where the targets live and which ones.
231
+ */
232
+ export const runManifest = async (
233
+ workspaceDir: string,
234
+ targets: string[],
235
+ meta: { source: string; commit: string },
236
+ ): Promise<{ repo: string; skipped: boolean }[]> => {
237
+ const repos: { repo: string; skipped: boolean }[] = []
238
+
239
+ for (const repo of targets) {
240
+ const targetRoot = resolveTargetRoot(workspaceDir, repo)
241
+ const skipped = !existsSync(targetRoot)
242
+
243
+ if (!skipped) {
244
+ writeVendorMeta(targetRoot, meta)
245
+ }
246
+
247
+ repos.push({ repo, skipped })
248
+ }
249
+
250
+ return repos
251
+ }
252
+
253
+ export interface VendorDiffEntry {
254
+ repo: string
255
+ target: string
256
+ changes: string[]
257
+ }
258
+
259
+ /** Source-aware drift across every `vendored:true` item in each target. */
260
+ export const runDiff = async (
261
+ config: VendorConfig,
262
+ sourceRoot: string,
263
+ workspaceDir: string,
264
+ targets: string[],
265
+ ): Promise<VendorDiffEntry[]> => {
266
+ const entries: VendorDiffEntry[] = []
267
+ const vendoredItems = config.copy.filter((item) => {
268
+ return item.vendored === true
269
+ })
270
+
271
+ for (const repo of targets) {
272
+ const targetRoot = resolveTargetRoot(workspaceDir, repo)
273
+
274
+ if (!existsSync(targetRoot)) {
275
+ continue
276
+ }
277
+
278
+ for (const item of vendoredItems) {
279
+ const source = path.join(sourceRoot, item.source)
280
+ const target = path.join(targetRoot, item.target)
281
+ const changes = await diffDirectory(source, target)
282
+
283
+ if (changes.length > 0) {
284
+ entries.push({ repo, target: item.target, changes })
285
+ }
286
+ }
287
+ }
288
+
289
+ return entries
290
+ }
@@ -0,0 +1,56 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ import { isSkippedDir, isSkippedFile } from './skip-sets'
5
+
6
+ /**
7
+ * Convert a (possibly Windows) relative path to a POSIX-normalized form so the
8
+ * manifest is byte-identical across operating systems.
9
+ */
10
+ const toPosix = (relativePath: string): string => {
11
+ return relativePath.split(path.sep).join('/')
12
+ }
13
+
14
+ /**
15
+ * Walk a `vendor/` tree and return the relative paths of every non-skipped file,
16
+ * POSIX-normalized and sorted byte-wise. The sort makes the output independent
17
+ * of `readdirSync` order, which keeps generated manifests deterministic.
18
+ *
19
+ * Symlink handling matches the legacy scripts: only REAL directories
20
+ * (`entry.isDirectory()`) are recursed; a symlink is treated as a leaf and its
21
+ * relative path is emitted, to be hashed later by following it to target
22
+ * content (`sha256`/`readFileSync`). This preserves legacy behavior exactly —
23
+ * the manifest checksum reflects target content, never the link itself.
24
+ *
25
+ * @example
26
+ * walkVendorTree('/repo/vendor')
27
+ * // => ['README.md', 'configs/eslint-config/index.js', ...]
28
+ */
29
+ export const walkVendorTree = (vendorRoot: string): string[] => {
30
+ const acc: string[] = []
31
+
32
+ const walk = (dir: string): void => {
33
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
34
+ const full = path.join(dir, entry.name)
35
+
36
+ if (entry.isDirectory()) {
37
+ if (isSkippedDir(entry.name)) {
38
+ continue
39
+ }
40
+
41
+ walk(full)
42
+ continue
43
+ }
44
+
45
+ if (isSkippedFile(entry.name)) {
46
+ continue
47
+ }
48
+
49
+ acc.push(toPosix(path.relative(vendorRoot, full)))
50
+ }
51
+ }
52
+
53
+ walk(vendorRoot)
54
+
55
+ return acc.sort()
56
+ }
@@ -1,50 +1,13 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
 
3
- import { auditMcpTool } from 'src/commands/audit'
4
- import { envClearMcpTool } from 'src/commands/env-clear'
5
- import { envListMcpTool } from 'src/commands/env-list'
6
- import { envLoadMcpTool } from 'src/commands/env-load'
7
- import { envStatusMcpTool } from 'src/commands/env-status'
8
- import { ghMergeDevMcpTool } from 'src/commands/gh-merge-dev'
9
- import { ghReleaseDeliverMcpTool } from 'src/commands/gh-release-deliver'
10
- import { ghReleaseDeployAllMcpTool } from 'src/commands/gh-release-deploy-all'
11
- import { ghReleaseDeploySelectedMcpTool } from 'src/commands/gh-release-deploy-selected'
12
- import { ghReleaseListMcpTool } from 'src/commands/gh-release-list'
13
- import { releaseCreateMcpTool } from 'src/commands/release-create'
14
- import { releaseDescEditMcpTool } from 'src/commands/release-desc-edit'
15
- import { versionMcpTool } from 'src/commands/version'
16
- import { worktreesAddMcpTool } from 'src/commands/worktrees-add'
17
- import { worktreesListMcpTool } from 'src/commands/worktrees-list'
18
- import { worktreesOpenMcpTool } from 'src/commands/worktrees-open'
19
- import { worktreesReloadMcpTool } from 'src/commands/worktrees-reload'
20
- import { worktreesRemoveMcpTool } from 'src/commands/worktrees-remove'
21
- import { worktreesSyncMcpTool } from 'src/commands/worktrees-sync'
3
+ import { getExposedMcpTools } from 'src/lib/command-catalog'
22
4
  import { createToolHandler } from 'src/lib/tool-handler'
23
5
 
24
- const tools = [
25
- envStatusMcpTool,
26
- envListMcpTool,
27
- envLoadMcpTool,
28
- envClearMcpTool,
29
- ghMergeDevMcpTool,
30
- releaseCreateMcpTool,
31
- releaseDescEditMcpTool,
32
- ghReleaseDeliverMcpTool,
33
- ghReleaseDeployAllMcpTool,
34
- ghReleaseDeploySelectedMcpTool,
35
- ghReleaseListMcpTool,
36
- auditMcpTool,
37
- versionMcpTool,
38
- worktreesAddMcpTool,
39
- worktreesListMcpTool,
40
- worktreesOpenMcpTool,
41
- worktreesReloadMcpTool,
42
- worktreesRemoveMcpTool,
43
- worktreesSyncMcpTool,
44
- ]
45
-
46
6
  export const initializeTools = async (server: McpServer) => {
47
- for (const tool of tools) {
7
+ // The registered tool set is derived from the single command catalog, filtered
8
+ // by its explicit `mcpExposed` allowlist. doctor / vendor-sync / vendor-manifest
9
+ // are intentionally excluded there and must never be registered here.
10
+ for (const tool of getExposedMcpTools()) {
48
11
  server.registerTool(
49
12
  tool.name,
50
13
  {
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
2
2
 
3
3
  describe('sanity check', () => {
4
4
  it('should be testable', () => {
5
+ // eslint-disable-next-line sonarjs/no-trivial-assertions -- intentional test-runner sanity check
5
6
  expect(1 + 1).toBe(2)
6
7
  })
7
8
  })
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { describe, expect, it } from 'vitest'
5
+
6
+ // Guard the "no React on machine paths" invariant at the source level (the
7
+ // ESLint no-restricted-imports rule enforces it during lint; this asserts it in
8
+ // the test suite too, and documents the boundary). Only STATIC imports are
9
+ // forbidden — the TTY branch reaches the TUI via a dynamic `import('src/tui/boot')`.
10
+
11
+ const SRC_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
12
+
13
+ const MACHINE_PATH_FILES = ['entry/cli.ts', 'entry/mcp.ts']
14
+
15
+ /** Extract the module specifier from each `import ... from '<spec>'` line. */
16
+ const staticImportSpecifiers = (source: string): string[] => {
17
+ return source.split('\n').flatMap((line) => {
18
+ const match = /^\s*import\s.+from\s+['"]([^'"]+)['"]/.exec(line)
19
+
20
+ return match?.[1] ? [match[1]] : []
21
+ })
22
+ }
23
+
24
+ /** A specifier that would pull React/Ink onto a machine path if imported statically. */
25
+ const isForbidden = (specifier: string): boolean => {
26
+ if (specifier === 'ink' || specifier === 'react' || specifier.startsWith('react/')) {
27
+ return true
28
+ }
29
+
30
+ return (
31
+ specifier === 'tui' || specifier.endsWith('/tui') || specifier.includes('/tui/') || specifier.startsWith('src/tui')
32
+ )
33
+ }
34
+
35
+ describe('no-React boundary', () => {
36
+ it.each(MACHINE_PATH_FILES)('%s does not statically import ink/react/tui', (relative) => {
37
+ const source = fs.readFileSync(path.join(SRC_DIR, relative), 'utf-8')
38
+
39
+ for (const specifier of staticImportSpecifiers(source)) {
40
+ expect(isForbidden(specifier), `forbidden static import in ${relative}: ${specifier}`).toBe(false)
41
+ }
42
+ })
43
+
44
+ it('entry/cli.ts reaches the TUI only via a dynamic import', () => {
45
+ const source = fs.readFileSync(path.join(SRC_DIR, 'entry/cli.ts'), 'utf-8')
46
+
47
+ // The dynamic import is how the palette is loaded; it must be present and
48
+ // must be a dynamic import() call, never a static `import ... from`.
49
+ expect(source).toMatch(/import\(\s*['"]src\/tui\/boot['"]\s*\)/)
50
+ })
51
+ })