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
@@ -0,0 +1,93 @@
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 { logger } from 'src/lib/logger'
7
+
8
+ import { migrateFactoryConfigToJson } from '../migrate-config'
9
+
10
+ let home: string
11
+ let dir: string
12
+ let oldTs: string
13
+ let newJson: string
14
+ let infoSpy: ReturnType<typeof vi.spyOn>
15
+
16
+ const messages = (): string => {
17
+ return infoSpy.mock.calls
18
+ .map((c: unknown[]) => {
19
+ return String(c[0])
20
+ })
21
+ .join('\n')
22
+ }
23
+
24
+ beforeEach(() => {
25
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'migrate-factory-test-'))
26
+ vi.spyOn(os, 'homedir').mockReturnValue(home)
27
+ dir = path.join(home, '.infra-kit')
28
+ oldTs = path.join(dir, 'vendor.config.ts')
29
+ newJson = path.join(dir, 'vendor.json')
30
+ fs.mkdirSync(dir, { recursive: true })
31
+ infoSpy = vi.spyOn(logger, 'info').mockImplementation(() => {})
32
+ })
33
+
34
+ afterEach(() => {
35
+ vi.restoreAllMocks()
36
+ fs.rmSync(home, { recursive: true, force: true })
37
+ })
38
+
39
+ describe('migrateFactoryConfigToJson', () => {
40
+ it('converts an object-form vendor.config.ts to vendor.json and removes the .ts (plain ✓, no warning)', async () => {
41
+ fs.writeFileSync(oldTs, `export default { workspaceDir: '~/projects', targets: ['repo-a'] }`)
42
+
43
+ await migrateFactoryConfigToJson()
44
+
45
+ expect(fs.existsSync(oldTs)).toBe(false)
46
+ expect(JSON.parse(fs.readFileSync(newJson, 'utf-8'))).toEqual({ workspaceDir: '~/projects', targets: ['repo-a'] })
47
+ expect(messages()).toMatch(/✓ Migrated/)
48
+ expect(messages()).not.toMatch(/STATIC SNAPSHOT/)
49
+ })
50
+
51
+ it('converts a factory-function form AND emits the STATIC SNAPSHOT warning (not a plain ✓)', async () => {
52
+ fs.writeFileSync(oldTs, `export default () => ({ workspaceDir: '/abs/repos', targets: ['repo-b'] })`)
53
+
54
+ await migrateFactoryConfigToJson()
55
+
56
+ expect(fs.existsSync(oldTs)).toBe(false)
57
+ expect(JSON.parse(fs.readFileSync(newJson, 'utf-8'))).toEqual({ workspaceDir: '/abs/repos', targets: ['repo-b'] })
58
+ expect(messages()).toMatch(/STATIC SNAPSHOT/)
59
+ expect(messages()).not.toMatch(/✓ Migrated/)
60
+ })
61
+
62
+ it('is a no-op when there is no legacy vendor.config.ts (idempotent)', async () => {
63
+ await migrateFactoryConfigToJson()
64
+
65
+ expect(fs.existsSync(newJson)).toBe(false)
66
+ expect(infoSpy).not.toHaveBeenCalled()
67
+ })
68
+
69
+ it('never overwrites an existing vendor.json; leaves the .ts in place and warns', async () => {
70
+ const existing = { workspaceDir: '~/active', targets: ['keep-me'] }
71
+
72
+ fs.writeFileSync(oldTs, `export default { workspaceDir: '~/stale', targets: ['old'] }`)
73
+ fs.writeFileSync(newJson, JSON.stringify(existing))
74
+
75
+ await migrateFactoryConfigToJson()
76
+
77
+ // Active vendor.json untouched; the stale .ts is preserved (not deleted).
78
+ expect(JSON.parse(fs.readFileSync(newJson, 'utf-8'))).toEqual(existing)
79
+ expect(fs.existsSync(oldTs)).toBe(true)
80
+ expect(messages()).toMatch(/already exists/)
81
+ })
82
+
83
+ it('leaves an invalid old .ts in place (non-fatal) and writes no vendor.json', async () => {
84
+ // Missing required `targets` → fails factoryConfigSchema.
85
+ fs.writeFileSync(oldTs, `export default { workspaceDir: '~/projects' }`)
86
+
87
+ await expect(migrateFactoryConfigToJson()).resolves.toBeUndefined()
88
+
89
+ expect(fs.existsSync(oldTs)).toBe(true)
90
+ expect(fs.existsSync(newJson)).toBe(false)
91
+ expect(messages()).toMatch(/invalid factory config/)
92
+ })
93
+ })
@@ -0,0 +1,64 @@
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 { seedUserGlobalConfig } from '../init'
7
+
8
+ let home: string
9
+ let dir: string
10
+
11
+ beforeEach(() => {
12
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-user-global-test-'))
13
+ vi.spyOn(os, 'homedir').mockReturnValue(home)
14
+ dir = path.join(home, '.infra-kit')
15
+ })
16
+
17
+ afterEach(() => {
18
+ vi.restoreAllMocks()
19
+ fs.rmSync(home, { recursive: true, force: true })
20
+ })
21
+
22
+ describe('seedUserGlobalConfig', () => {
23
+ it('seeds the infra-kit.json stub and both fully-commented example files', () => {
24
+ seedUserGlobalConfig()
25
+
26
+ expect(fs.readFileSync(path.join(dir, 'infra-kit.json'), 'utf-8')).toBe('{}\n')
27
+
28
+ const infraExample = fs.readFileSync(path.join(dir, 'infra-kit.example.jsonc'), 'utf-8')
29
+
30
+ // Every recognized infra-kit property is documented.
31
+ for (const key of ['environments', 'envManagement', 'ide', 'taskManager', 'worktrees']) {
32
+ expect(infraExample).toContain(key)
33
+ }
34
+
35
+ const vendorExample = fs.readFileSync(path.join(dir, 'vendor.example.jsonc'), 'utf-8')
36
+
37
+ expect(vendorExample).toContain('workspaceDir')
38
+ expect(vendorExample).toContain('targets')
39
+ })
40
+
41
+ it('never seeds a real vendor.json (only the example)', () => {
42
+ seedUserGlobalConfig()
43
+
44
+ expect(fs.existsSync(path.join(dir, 'vendor.example.jsonc'))).toBe(true)
45
+ expect(fs.existsSync(path.join(dir, 'vendor.json'))).toBe(false)
46
+ })
47
+
48
+ it('refreshes the example files on a re-run while leaving an existing infra-kit.json byte-unchanged', () => {
49
+ fs.mkdirSync(dir, { recursive: true })
50
+ const realConfig = '{ "environments": ["dev"] }\n'
51
+
52
+ fs.writeFileSync(path.join(dir, 'infra-kit.json'), realConfig)
53
+ // Stale example a previous version might have written.
54
+ fs.writeFileSync(path.join(dir, 'infra-kit.example.jsonc'), '// stale\n')
55
+
56
+ seedUserGlobalConfig()
57
+
58
+ // Real config preserved exactly…
59
+ expect(fs.readFileSync(path.join(dir, 'infra-kit.json'), 'utf-8')).toBe(realConfig)
60
+ // …examples refreshed to the current template (no longer "stale").
61
+ expect(fs.readFileSync(path.join(dir, 'infra-kit.example.jsonc'), 'utf-8')).not.toBe('// stale\n')
62
+ expect(fs.existsSync(path.join(dir, 'vendor.example.jsonc'))).toBe(true)
63
+ })
64
+ })
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { buildShellBody } from '../init'
4
+
5
+ describe('buildShellBody — env auto-load shell-startup block', () => {
6
+ const body = buildShellBody()
7
+
8
+ it('defines and invokes the one-shot startup autoload function', () => {
9
+ expect(body).toContain('_infra_kit_startup_autoload()')
10
+ expect(body).toContain('_infra_kit_startup_autoload\n')
11
+ })
12
+
13
+ it('gates on a session and skips subshells that already inherited an env', () => {
14
+ expect(body).toContain('[[ -z "$INFRA_KIT_SESSION" ]] && return')
15
+ expect(body).toContain('[[ -n "$INFRA_KIT_ENV_CONFIG" ]] && return')
16
+ })
17
+
18
+ it('walks up for infra-kit.json (project/worktree gate) before spawning node', () => {
19
+ expect(body).toContain('[[ -f "$dir/infra-kit.json" ]]')
20
+ expect(body).toContain('while [[ "$dir" != / ]]')
21
+ })
22
+
23
+ it('spawns env-autoload backgrounded and silenced (precmd does the sourcing)', () => {
24
+ expect(body).toContain('infra-kit env-autoload & ) >/dev/null 2>&1')
25
+ })
26
+
27
+ it('keeps the precmd hook as the sole sourcer of env-load.sh', () => {
28
+ expect(body).toContain('add-zsh-hook precmd _infra_kit_autoload')
29
+ })
30
+ })
@@ -3,7 +3,7 @@ import path from 'node:path'
3
3
 
4
4
  import { getInfraKitConfigPaths } from 'src/lib/infra-kit-config'
5
5
  import { logger } from 'src/lib/logger'
6
- import { hasManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'
6
+ import { hasManagedBlock, removeManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'
7
7
 
8
8
  import packageJson from '../../../package.json' with { type: 'json' }
9
9
 
@@ -13,23 +13,18 @@ export const AGENTS_MARKER_END = '<!-- infra-kit:end -->'
13
13
  /** The version line lives on its own line (OMC `OMC:VERSION:` precedent) so the markers stay constant-matchable. */
14
14
  const AGENTS_VERSION_PREFIX = '<!-- infra-kit:version '
15
15
 
16
- /** Separate marker pair for the small `@AGENTS.md` import region injected into CLAUDE.md. */
16
+ /**
17
+ * Marker pair for the legacy `@AGENTS.md` import region once injected into CLAUDE.md.
18
+ * Migration-only now: `writeAgentFiles` strips this region so the full guidance body
19
+ * can take its place. TODO(remove after ~2 release cycles once repos have re-run init).
20
+ */
17
21
  export const AGENTS_IMPORT_START = '<!-- infra-kit:import:begin -->'
18
22
  export const AGENTS_IMPORT_END = '<!-- infra-kit:import:end -->'
19
23
 
20
24
  const AGENTS_FILE = 'AGENTS.md'
21
25
  const CLAUDE_FILE = 'CLAUDE.md'
22
- const CURSOR_RULE_REL = path.join('.cursor', 'rules', 'infra-kit.mdc')
23
26
 
24
- const CURSOR_FRONTMATTER = [
25
- '---',
26
- 'description: infra-kit CLI usage and conventions',
27
- 'alwaysApply: true',
28
- '---',
29
- '',
30
- ].join('\n')
31
-
32
- export type WriteAction = 'created' | 'updated' | 'unchanged'
27
+ export type WriteAction = 'created' | 'updated' | 'unchanged' | 'removed'
33
28
 
34
29
  export interface AgentFileWrite {
35
30
  path: string
@@ -66,9 +61,9 @@ const buildAgentsBody = (version: string): string => {
66
61
  '## Commands (`ik` = `pnpm exec infra-kit`)',
67
62
  '',
68
63
  '- `ik env-load -c <config>` / `ik env-clear` / `ik env-status` — load, clear, or inspect Doppler env vars for a config (e.g. `dev`). Source the returned file to apply.',
69
- '- `ik worktrees-add` / `worktrees-list` / `worktrees-open` / `worktrees-remove` / `worktrees-sync` — manage release and feature git worktrees.',
70
- '- `ik release-create` / `release-list` / `release-deploy-all` / `release-deploy-selected` / `release-deliver` / `release-desc-edit` — release-branch and deploy flow.',
71
- '- `ik merge-dev` — merge the dev branch into every release branch.',
64
+ '- `ik worktrees add` / `worktrees list` / `worktrees reload` / `worktrees remove` / `worktrees sync` — manage release and feature git worktrees.',
65
+ '- `ik release create` / `release list` / `release deploy-all` / `release deploy-selected` / `release deliver` / `release desc-edit` — release-branch and deploy flow.',
66
+ '- `ik release merge-dev` — merge the dev branch into every release branch.',
72
67
  '- `ik audit` — audit packages against `infra-kit.config.ts` rules.',
73
68
  '- `ik doctor` — check gh / doppler / aws / rtk install + auth and repo setup.',
74
69
  '- `ik init` — (re)install shell integration and regenerate these agent-instruction files.',
@@ -86,6 +81,13 @@ const assertNotSymlink = (filePath: string): void => {
86
81
  }
87
82
  }
88
83
 
84
+ /** Copy a file to a timestamped `<file>.backup.<timestamp>` sibling. */
85
+ const backupFile = (filePath: string): void => {
86
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
87
+
88
+ fs.copyFileSync(filePath, `${filePath}.backup.${stamp}`)
89
+ }
90
+
89
91
  /**
90
92
  * Write `nextContent` to `filePath` with OMC-style safety: refuse symlinks,
91
93
  * back up the prior file with a timestamp before overwriting, and skip the
@@ -99,11 +101,7 @@ const writeManaged = (filePath: string, nextContent: string): WriteAction => {
99
101
 
100
102
  if (previous === nextContent) return 'unchanged'
101
103
 
102
- if (existed) {
103
- const stamp = new Date().toISOString().replace(/[:.]/g, '-')
104
-
105
- fs.copyFileSync(filePath, `${filePath}.backup.${stamp}`)
106
- }
104
+ if (existed) backupFile(filePath)
107
105
 
108
106
  fs.mkdirSync(path.dirname(filePath), { recursive: true })
109
107
  fs.writeFileSync(filePath, nextContent, 'utf-8')
@@ -124,10 +122,39 @@ const assertBlockPresent = (filePath: string, start: string, end: string): void
124
122
  }
125
123
 
126
124
  /**
127
- * Generate (or refresh) the repo agent-instruction files: the `AGENTS.md`
128
- * guidance block, a managed `@AGENTS.md` import region in `CLAUDE.md`
129
- * (preserving existing content), and a `.cursor/rules` rule file. Repo-gated:
130
- * a no-op outside an infra-kit repo. Idempotent and non-destructive.
125
+ * Migrate a legacy `AGENTS.md` now that the guidance lives solely in `CLAUDE.md`.
126
+ * Strips the infra-kit managed block, then asymmetrically:
127
+ * - no infra-kit block at all (hand-authored, not ours) leave untouched (`unchanged`),
128
+ * - file was purely generated (nothing but whitespace remains) → back up + delete (`removed`),
129
+ * - hand-authored content surrounds the block → back up + write the block-free remainder (`updated`).
130
+ * Every destructive path leaves a timestamped `.backup.` first.
131
+ */
132
+ const migrateLegacyAgentsFile = (agentsPath: string): WriteAction => {
133
+ if (!fs.existsSync(agentsPath)) return 'unchanged'
134
+
135
+ assertNotSymlink(agentsPath)
136
+
137
+ const content = fs.readFileSync(agentsPath, 'utf-8')
138
+ const stripped = removeManagedBlock(content, AGENTS_MARKER_START, AGENTS_MARKER_END)
139
+
140
+ if (stripped === null) return 'unchanged'
141
+
142
+ if (stripped.trim() === '') {
143
+ backupFile(agentsPath)
144
+ fs.rmSync(agentsPath)
145
+
146
+ return 'removed'
147
+ }
148
+
149
+ return writeManaged(agentsPath, stripped)
150
+ }
151
+
152
+ /**
153
+ * Generate (or refresh) the repo agent-instruction guidance, now hosted solely in
154
+ * `CLAUDE.md` (preserving hand-authored content outside the markers). Also migrates
155
+ * legacy setups — stripping the old `@AGENTS.md` import region from `CLAUDE.md` and
156
+ * backing up/removing a now-redundant `AGENTS.md`. Repo-gated: a no-op outside an
157
+ * infra-kit repo. Idempotent (within a CLI version) and non-destructive.
131
158
  *
132
159
  * @example
133
160
  * await writeAgentFiles()
@@ -156,40 +183,33 @@ export const writeAgentFiles = async (): Promise<WriteAgentFilesResult> => {
156
183
 
157
184
  const agentsPath = path.join(root, AGENTS_FILE)
158
185
  const claudePath = path.join(root, CLAUDE_FILE)
159
- const cursorPath = path.join(root, CURSOR_RULE_REL)
160
186
 
161
- const agentsContent = upsertManagedBlock({
162
- content: readOr(agentsPath, ''),
163
- body,
164
- startMarker: AGENTS_MARKER_START,
165
- endMarker: AGENTS_MARKER_END,
166
- })
187
+ // CLAUDE.md now hosts the full guidance body. Strip the legacy `@AGENTS.md`
188
+ // import region first (if present), then upsert the body under the begin/end
189
+ // markers — disjoint marker pairs, so the two operations never collide.
190
+ let claudeContent = readOr(claudePath, '')
167
191
 
168
- const claudeContent = upsertManagedBlock({
169
- content: readOr(claudePath, ''),
170
- body: `@${AGENTS_FILE}`,
171
- startMarker: AGENTS_IMPORT_START,
172
- endMarker: AGENTS_IMPORT_END,
173
- })
174
-
175
- const cursorContent = upsertManagedBlock({
176
- content: readOr(cursorPath, CURSOR_FRONTMATTER),
192
+ claudeContent = removeManagedBlock(claudeContent, AGENTS_IMPORT_START, AGENTS_IMPORT_END) ?? claudeContent
193
+ claudeContent = upsertManagedBlock({
194
+ content: claudeContent,
177
195
  body,
178
196
  startMarker: AGENTS_MARKER_START,
179
197
  endMarker: AGENTS_MARKER_END,
198
+ placement: 'replace-in-place',
180
199
  })
181
200
 
201
+ // CLAUDE.md is the only file we manage. A legacy AGENTS.md is migrated away.
182
202
  const written: AgentFileWrite[] = [
183
- { path: agentsPath, action: writeManaged(agentsPath, agentsContent) },
184
203
  { path: claudePath, action: writeManaged(claudePath, claudeContent) },
185
- { path: cursorPath, action: writeManaged(cursorPath, cursorContent) },
204
+ { path: agentsPath, action: migrateLegacyAgentsFile(agentsPath) },
186
205
  ]
187
206
 
188
- assertBlockPresent(agentsPath, AGENTS_MARKER_START, AGENTS_MARKER_END)
189
- assertBlockPresent(claudePath, AGENTS_IMPORT_START, AGENTS_IMPORT_END)
190
- assertBlockPresent(cursorPath, AGENTS_MARKER_START, AGENTS_MARKER_END)
207
+ assertBlockPresent(claudePath, AGENTS_MARKER_START, AGENTS_MARKER_END)
191
208
 
192
209
  for (const file of written) {
210
+ // Always report CLAUDE.md; only mention a migrated file when it actually changed.
211
+ if (file.action === 'unchanged' && file.path !== claudePath) continue
212
+
193
213
  logger.info(` ${file.action.padEnd(9)} ${path.relative(root, file.path)}`)
194
214
  }
195
215
 
@@ -1,12 +1,18 @@
1
1
  import fs from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
+ import process from 'node:process'
4
5
 
5
6
  import { logger } from 'src/lib/logger'
6
7
  import { removeManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'
7
8
 
8
9
  import { writeAgentFiles } from './agent-files'
9
- import { migrateLegacyConfig, normalizeLegacyIdeStructures } from './migrate-config'
10
+ import {
11
+ migrateFactoryConfigToJson,
12
+ migrateLegacyConfig,
13
+ migrateUserGlobalConfigFilename,
14
+ normalizeLegacyIdeStructures,
15
+ } from './migrate-config'
10
16
 
11
17
  export const MARKER_START = '# -- infra-kit:begin --'
12
18
  export const MARKER_END = '# -- infra-kit:end --'
@@ -18,22 +24,34 @@ const LEGACY_SINGLE = '# infra-kit shell functions'
18
24
  const USER_GLOBAL_CONFIG_STUB = '{}\n'
19
25
 
20
26
  // …and the annotated guidance lives next to it in a non-loaded .example.jsonc
21
- // (the loader only reads the three exact `infra-kit.json` / `config.json` files).
22
- const USER_GLOBAL_CONFIG_EXAMPLE = `// infra-kit user-global config — ~/.infra-kit/config.json
27
+ // (the loader only reads the three exact `infra-kit.json` files).
28
+ const USER_GLOBAL_CONFIG_EXAMPLE = `// infra-kit user-global config — ~/.infra-kit/infra-kit.json
23
29
  //
24
30
  // Merge chain (later layers override earlier ones at top-level keys):
25
31
  // 1. <repo>/infra-kit.json — committed project config (required)
26
- // 2. ~/.infra-kit/config.json — user-global (the sibling of this file)
32
+ // 2. ~/.infra-kit/infra-kit.json — user-global (the sibling of this file)
27
33
  // 3. ~/.infra-kit/projects/<repo-name>/infra-kit.json — user-scope per-project override
28
34
  //
29
35
  // Merge is shallow: setting a top-level key replaces that whole section from
30
36
  // layer 1. Arrays do not concatenate. Top-level keys recognized:
31
- // environments, envManagement, ide, taskManager, worktrees.
37
+ // environments, envManagement, ide, taskManager, worktrees, envAutoLoad.
32
38
  //
33
39
  // This .example.jsonc is reference only — it is NOT loaded. Put real global
34
- // overrides in the sibling config.json (strict JSON: no comments, double-quoted
40
+ // overrides in the sibling infra-kit.json (strict JSON: no comments, double-quoted
35
41
  // keys). Per-project tweaks belong in layer 3 — run \`infra-kit config edit\`.
42
+ //
43
+ // Every recognized key is documented below. NOTE: \`environments\` and
44
+ // \`envManagement\` are REQUIRED in the committed project infra-kit.json (layer 1)
45
+ // and are usually NOT set in this user-global layer — they are shown here only to
46
+ // document the full, valid config shape.
36
47
  {
48
+ // "environments": ["dev", "staging", "prod"], // string[] (>=1) — required in layer 1
49
+ //
50
+ // "envManagement": { // required in layer 1; provider-tagged
51
+ // "provider": "doppler",
52
+ // "config": { "name": "my-doppler-project" }
53
+ // },
54
+ //
37
55
  // "ide": {
38
56
  // "provider": "cursor",
39
57
  // "config": { "workspaceConfigPath": "/path/to/your.code-workspace" }
@@ -45,23 +63,60 @@ const USER_GLOBAL_CONFIG_EXAMPLE = `// infra-kit user-global config — ~/.infra
45
63
  // { "provider": "cursor", "config": { "workspaceConfigPath": "/path/to/your.code-workspace" } },
46
64
  // { "provider": "zed", "config": {} }
47
65
  // ],
48
- // "worktrees": { "openInGithubDesktop": false, "openInCmux": true }
66
+ //
67
+ // "taskManager": {
68
+ // "provider": "jira",
69
+ // "config": { "baseUrl": "https://acme.atlassian.net", "projectId": 123 }
70
+ // },
71
+ //
72
+ // "worktrees": {
73
+ // "openInGithubDesktop": false,
74
+ // "openInCmux": true,
75
+ // // cmux pane layout for opened worktrees: "two-columns" (default, left | right)
76
+ // // or "three-pane" (left split top/bottom + full-height right).
77
+ // "cmux": { "layout": "two-columns" }
78
+ // },
79
+ //
80
+ // // Auto-load Doppler env when working inside this project/worktree. Omit to
81
+ // // disable. "trigger" (pick one): "shell-startup" (new shells) | "cli-invocation"
82
+ // // (before each infra-kit command, primes subsequent commands). "config" is the
83
+ // // environment to load (must be one of "environments"). Requires the committed
84
+ // // infra-kit.json at the git repo root, and the zsh shell integration
85
+ // // (infra-kit init + a new shell). zsh only.
86
+ // "envAutoLoad": { "trigger": "shell-startup", "config": "dev" }
87
+ }
88
+ `
89
+
90
+ // The machine-local factory registry lives at ~/.infra-kit/vendor.json (strict
91
+ // JSON). This annotated sibling documents every property; the real file is
92
+ // scaffolded by \`infra-kit vendor config --init\`, NOT by init.
93
+ const VENDOR_CONFIG_EXAMPLE = `// infra-kit factory registry — ~/.infra-kit/vendor.json
94
+ //
95
+ // Machine-local registry the vendor commands (sync/manifest/diff) read to know
96
+ // where your project repos live and which ones to stamp. This .example.jsonc is
97
+ // reference only — it is NOT loaded. The real file is the strict-JSON sibling
98
+ // vendor.json (no comments, double-quoted keys); run \`infra-kit vendor config --init\`
99
+ // to scaffold it.
100
+ {
101
+ // "workspaceDir": "~/projects", // string (absolute or ~-prefixed) — where target repos are cloned
102
+ // "targets": ["my-repo-a", "my-repo-b"] // string[] (>=1) — repo dir names resolved under workspaceDir
49
103
  }
50
104
  `
51
105
 
52
106
  /**
53
- * Append infra-kit shell functions to .zshrc, migrate any legacy
54
- * `infra-kit.yml` config layers to JSON, normalize existing JSON configs from
55
- * the old IDE structure to the new one (strip the removed `ide.config.mode`),
56
- * and seed the user-global config at ~/.infra-kit/config.json on first run.
57
- * Idempotent: a subsequent run replaces the existing zshrc block in place, has
58
- * nothing left to migrate or normalize, and leaves the user-global config
59
- * untouched.
107
+ * Append infra-kit shell functions to .zshrc, migrate any legacy `infra-kit.yml`
108
+ * config layers to JSON, normalize existing JSON configs from the old IDE structure
109
+ * (strip the removed `ide.config.mode`), convert a legacy factory `vendor.config.ts`
110
+ * to `vendor.json`, and seed the user-global config at ~/.infra-kit/infra-kit.json on
111
+ * first run. Idempotent: a subsequent run replaces the existing zshrc block in place,
112
+ * has nothing left to migrate or normalize, and leaves the REAL config files
113
+ * (infra-kit.json / vendor.json) untouched — but the annotated `.example.jsonc`
114
+ * reference files are refreshed on every run so they always reflect the current schema.
60
115
  *
61
116
  * @example
62
117
  * // CLI: `infra-kit init` (or via the `pnpm dx-init` alias)
63
118
  * // INFO: Added infra-kit shell functions to /Users/me/.zshrc
64
- * // INFO: Wrote user-global config to /Users/me/.infra-kit/config.json (see …/config.example.jsonc …)
119
+ * // INFO: Wrote user-global config to /Users/me/.infra-kit/infra-kit.json (see …/infra-kit.example.jsonc …)
65
120
  * // INFO: Run `source ~/.zshrc` or open a new terminal to activate.
66
121
  */
67
122
  export const init = async (): Promise<void> => {
@@ -85,50 +140,79 @@ export const init = async (): Promise<void> => {
85
140
  logger.info(`Added infra-kit shell functions to ${zshrcPath}`)
86
141
 
87
142
  // Convert any legacy infra-kit.yml config layers to JSON before seeding, so a
88
- // migrated config.json is not re-seeded as an empty stub.
143
+ // migrated infra-kit.json is not re-seeded as an empty stub.
89
144
  await migrateLegacyConfig()
90
145
 
91
146
  // Migrate existing JSON configs from the old IDE structure to the new one
92
147
  // (strip the removed `ide.config.mode` field). No-op for already-clean configs.
93
148
  await normalizeLegacyIdeStructures()
94
149
 
150
+ // Rename a legacy user-global config.json → infra-kit.json (single canonical
151
+ // filename). MUST run before seeding: otherwise the seeder checks the new name,
152
+ // doesn't find it, and writes an empty stub that shadows the user's real config.
153
+ await migrateUserGlobalConfigFilename()
154
+
155
+ // Convert a legacy machine-local factory config from executable TS
156
+ // (~/.infra-kit/vendor.config.ts) to static JSON (~/.infra-kit/vendor.json).
157
+ // Independent of the infra-kit.json layers; grouped with the other migrations.
158
+ await migrateFactoryConfigToJson()
159
+
95
160
  seedUserGlobalConfig()
96
161
 
97
162
  // Best-effort, non-fatal, repo-gated: keep the agent-instruction files in sync
98
163
  // with the CLI surface. A no-op outside an infra-kit repo.
99
164
  await writeAgentFiles()
100
165
 
166
+ // The shell integration is zsh-only (zmodload zsh/stat, add-zsh-hook, ${dir:h}).
167
+ // Warn non-fatally if the user's login shell isn't zsh so the block isn't a
168
+ // silent no-op for them.
169
+ const shell = process.env.SHELL ?? ''
170
+
171
+ if (!shell.includes('zsh')) {
172
+ logger.warn(
173
+ `Your login shell ($SHELL=${shell || 'unset'}) is not zsh. The infra-kit shell integration (env-load/env-clear/auto-load) is zsh-only and won't activate in bash/fish.`,
174
+ )
175
+ }
176
+
101
177
  logger.info('Run `source ~/.zshrc` or open a new terminal to activate.')
102
178
  }
103
179
 
104
180
  /**
105
- * Create `~/.infra-kit/config.json` (empty `{}`) plus an annotated
106
- * `~/.infra-kit/config.example.jsonc` reference when absent. Skips silently if
107
- * the config already exists so user edits are preserved.
181
+ * Seed the user-global config on first run and (re)write the annotated reference
182
+ * files. The real `~/.infra-kit/infra-kit.json` (empty `{}`) is written only when
183
+ * absent so user edits are preserved. The non-loaded `.example.jsonc` reference
184
+ * files (`infra-kit.example.jsonc` + `vendor.example.jsonc`) are rewritten on EVERY
185
+ * run so existing users always get the current, complete schema documentation.
186
+ *
187
+ * Deliberately seeds NO real `vendor.json` — that is scaffolded by
188
+ * `infra-kit vendor config --init` (a stub here would block `--init`, trip the
189
+ * factory migration's no-overwrite guard, and fail the schema's `targets.min(1)`).
108
190
  *
109
191
  * @example
110
192
  * seedUserGlobalConfig()
111
- * // first call: writes ~/.infra-kit/config.json ({}) + config.example.jsonc
112
- * // later calls: leaves the config alone, logs that it is already present
193
+ * // first call: writes ~/.infra-kit/infra-kit.json ({}) + both .example.jsonc files
194
+ * // later calls: leaves infra-kit.json alone, refreshes both .example.jsonc files
113
195
  */
114
- const seedUserGlobalConfig = (): void => {
196
+ export const seedUserGlobalConfig = (): void => {
115
197
  const userConfigDir = path.join(os.homedir(), '.infra-kit')
116
- const userConfigPath = path.join(userConfigDir, 'config.json')
117
- const userConfigExamplePath = path.join(userConfigDir, 'config.example.jsonc')
198
+ const userConfigPath = path.join(userConfigDir, 'infra-kit.json')
199
+
200
+ fs.mkdirSync(userConfigDir, { recursive: true })
201
+
202
+ // Reference examples are non-loaded docs — always refresh them so re-running init
203
+ // delivers the current schema (and the newly-added vendor example) to existing users.
204
+ fs.writeFileSync(path.join(userConfigDir, 'infra-kit.example.jsonc'), USER_GLOBAL_CONFIG_EXAMPLE, 'utf-8')
205
+ fs.writeFileSync(path.join(userConfigDir, 'vendor.example.jsonc'), VENDOR_CONFIG_EXAMPLE, 'utf-8')
118
206
 
119
207
  if (fs.existsSync(userConfigPath)) {
120
- logger.info(`User-global config already present at ${userConfigPath}`)
208
+ logger.info(`User-global config already present at ${userConfigPath} (refreshed reference examples)`)
121
209
 
122
210
  return
123
211
  }
124
212
 
125
- fs.mkdirSync(userConfigDir, { recursive: true })
126
213
  fs.writeFileSync(userConfigPath, USER_GLOBAL_CONFIG_STUB, 'utf-8')
127
- fs.writeFileSync(userConfigExamplePath, USER_GLOBAL_CONFIG_EXAMPLE, 'utf-8')
128
214
 
129
- logger.info(
130
- `Wrote user-global config to ${userConfigPath} (see ${userConfigExamplePath} for the annotated reference)`,
131
- )
215
+ logger.info(`Wrote user-global config to ${userConfigPath} (see the sibling .example.jsonc files for reference)`)
132
216
  }
133
217
 
134
218
  const isBlockLine = (line: string): boolean => {
@@ -168,7 +252,7 @@ const removeExistingBlock = (content: string): string => {
168
252
 
169
253
  if (legacyIdx === -1) return content
170
254
 
171
- // eslint-disable-next-line sonarjs/slow-regex
255
+ // eslint-disable-next-line sonarjs/super-linear-regex
172
256
  const before = content.slice(0, legacyIdx).replace(/\n+$/, '')
173
257
  const afterLines = content.slice(legacyIdx).split('\n')
174
258
 
@@ -238,6 +322,31 @@ export const buildShellBody = (): string => {
238
322
  'if (( _INFRA_KIT_SHELL_STARTED > 0 )); then',
239
323
  ' add-zsh-hook precmd _infra_kit_autoload',
240
324
  'fi',
325
+ // One-shot env auto-load when a NEW shell opens inside an infra-kit project
326
+ // or worktree (config: envAutoLoad.trigger "shell-startup"). Cheap pure-zsh
327
+ // project gate (walk up for infra-kit.json) avoids spawning node in unrelated
328
+ // shells; the INFRA_KIT_ENV_CONFIG guard skips subshells that already inherited
329
+ // a loaded env. The spawn is DETACHED+backgrounded ( ... & ) so it never blocks
330
+ // the prompt; it only WRITES env-load.sh and the precmd hook above is the sole
331
+ // sourcer (picked up on a subsequent prompt). Trade-off: the gate can't read the
332
+ // merged JSON config, so a fresh shell in a project WITHOUT envAutoLoad still
333
+ // spawns one background process that resolves config, finds nothing, and exits.
334
+ '_infra_kit_startup_autoload() {',
335
+ ' [[ -z "$INFRA_KIT_SESSION" ]] && return',
336
+ ' [[ -n "$INFRA_KIT_ENV_CONFIG" ]] && return',
337
+ ' local dir="$PWD"',
338
+ ' while [[ "$dir" != / ]]; do',
339
+ ' if [[ -f "$dir/infra-kit.json" ]]; then',
340
+ ` ( ${runCmd} env-autoload & ) >/dev/null 2>&1`,
341
+ ' return',
342
+ ' fi',
343
+ // eslint-disable-next-line no-template-curly-in-string
344
+ ' dir="${dir:h}"',
345
+ ' done',
346
+ '}',
347
+ 'if (( _INFRA_KIT_SHELL_STARTED > 0 )); then',
348
+ ' _infra_kit_startup_autoload',
349
+ 'fi',
241
350
  ].join('\n')
242
351
  }
243
352