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
@@ -2,9 +2,10 @@ import path from 'node:path'
2
2
  import process from 'node:process'
3
3
  import { z } from 'zod'
4
4
 
5
- import { validateDopplerCliAndAuth } from 'src/integrations/doppler'
6
5
  import {
7
6
  ENV_LOAD_FILE,
7
+ INFRA_KIT_ENV_AUTOLOADED_VAR,
8
+ INFRA_KIT_ENV_CLEARED_VAR,
8
9
  INFRA_KIT_ENV_CONFIG_VAR,
9
10
  INFRA_KIT_ENV_LOADED_AT_VAR,
10
11
  INFRA_KIT_ENV_PROJECT_VAR,
@@ -16,11 +17,13 @@ import { logger } from 'src/lib/logger'
16
17
  import { defineMcpTool, textContent } from 'src/types'
17
18
 
18
19
  /**
19
- * Show Doppler authentication status and detected project info
20
+ * Report which env is currently loaded in the terminal session. Pure local
21
+ * introspection: reads only `process.env` + the cached env-load.sh — it makes NO
22
+ * Doppler call (use `doctor` for auth/CLI checks), so it works offline and never
23
+ * hangs. Surfaces whether the env was auto-loaded vs manually loaded and whether a
24
+ * clear is currently suppressing auto-load.
20
25
  */
21
26
  export const envStatus = async () => {
22
- await validateDopplerCliAndAuth()
23
-
24
27
  logger.info('Environment session status:')
25
28
 
26
29
  // Check session-loaded vars — getSessionCacheDir() throws if INFRA_KIT_SESSION is unset
@@ -35,6 +38,8 @@ export const envStatus = async () => {
35
38
  const sessionConfig = process.env[INFRA_KIT_ENV_CONFIG_VAR] ?? null
36
39
  const sessionProject = process.env[INFRA_KIT_ENV_PROJECT_VAR] ?? null
37
40
  const sessionLoadedAt = process.env[INFRA_KIT_ENV_LOADED_AT_VAR] ?? null
41
+ const autoLoaded = process.env[INFRA_KIT_ENV_AUTOLOADED_VAR] === '1'
42
+ const cleared = Boolean(process.env[INFRA_KIT_ENV_CLEARED_VAR])
38
43
 
39
44
  if (sessionConfig) {
40
45
  const varNames = parseVarNamesFromEnvFile(envLoadPath)
@@ -47,9 +52,10 @@ export const envStatus = async () => {
47
52
  }
48
53
 
49
54
  const loadedAtDisplay = sessionLoadedAt?.replace(/\.\d{3}Z$/, '') ?? null
55
+ const origin = autoLoaded ? 'auto-loaded' : 'manually loaded'
50
56
 
51
57
  logger.info(
52
- ` ${sessionConfig}: ${sessionLoadedCount} of ${sessionTotalCount} vars loaded (project: ${sessionProject}, loadedAt: ${loadedAtDisplay}, session: ${sessionId})\n`,
58
+ ` ${sessionConfig}: ${sessionLoadedCount} of ${sessionTotalCount} vars loaded (${origin}, project: ${sessionProject}, loadedAt: ${loadedAtDisplay}, session: ${sessionId})\n`,
53
59
  )
54
60
 
55
61
  if (sessionTotalCount > 0 && sessionLoadedCount < sessionTotalCount) {
@@ -60,7 +66,9 @@ export const envStatus = async () => {
60
66
  )
61
67
  }
62
68
  } else {
63
- logger.info(` Session ${sessionId}: no env loaded\n`)
69
+ const clearedNote = cleared ? ' (cleared auto-load suppressed until a new shell or explicit env-load)' : ''
70
+
71
+ logger.info(` Session ${sessionId}: no env loaded${clearedNote}\n`)
64
72
  }
65
73
 
66
74
  const structuredContent = {
@@ -70,6 +78,8 @@ export const envStatus = async () => {
70
78
  sessionConfig,
71
79
  sessionProject,
72
80
  sessionLoadedAt,
81
+ autoLoaded,
82
+ cleared,
73
83
  }
74
84
 
75
85
  return {
@@ -82,7 +92,7 @@ export const envStatus = async () => {
82
92
  export const envStatusMcpTool = defineMcpTool({
83
93
  name: 'env-status',
84
94
  description:
85
- 'Report which Doppler project/config is currently loaded in the terminal session, when it was loaded, and how many variables are cached. Read-only — use env-load / env-clear to change the terminal session.',
95
+ 'Report which Doppler project/config is currently loaded in the terminal session, when it was loaded, how many variables are cached, whether it was auto-loaded, and whether a clear is suppressing auto-load. Pure local introspection — makes NO Doppler call (use doctor for auth). Read-only — use env-load / env-clear to change the terminal session.',
86
96
  inputSchema: {},
87
97
  outputSchema: {
88
98
  sessionId: z.string().describe('Current terminal session ID'),
@@ -91,6 +101,8 @@ export const envStatusMcpTool = defineMcpTool({
91
101
  sessionConfig: z.string().nullable().describe('Doppler config name of the loaded session (environment name)'),
92
102
  sessionProject: z.string().nullable().describe('Doppler project name of the loaded session'),
93
103
  sessionLoadedAt: z.string().nullable().describe('ISO 8601 timestamp of when the env was loaded'),
104
+ autoLoaded: z.boolean().describe('True when the loaded env was applied by env auto-load (not a manual env-load)'),
105
+ cleared: z.boolean().describe('True when env-clear is currently suppressing auto-load in this shell'),
94
106
  },
95
107
  handler: envStatus,
96
108
  })
@@ -1,3 +1,4 @@
1
+ import confirm from '@inquirer/confirm'
1
2
  import select from '@inquirer/select'
2
3
  import { z } from 'zod'
3
4
  import { $ } from 'zx'
@@ -21,13 +22,39 @@ interface GhReleaseDeployAllArgs {
21
22
  version: string
22
23
  env: string
23
24
  skipTerraform?: boolean
25
+ confirmedCommand?: boolean
26
+ }
27
+
28
+ interface ConfirmDeployArgs {
29
+ confirmedCommand?: boolean
30
+ branch: string
31
+ env: string
32
+ }
33
+
34
+ /**
35
+ * Gate the workflow dispatch behind an interactive confirmation. Returns true to
36
+ * proceed; `confirmedCommand` (CLI `--yes`) skips the prompt. Mirrors the
37
+ * confirm+commandEcho pattern used by worktrees-remove.
38
+ */
39
+ const confirmDeploy = async (args: ConfirmDeployArgs): Promise<boolean> => {
40
+ const { confirmedCommand, branch, env } = args
41
+
42
+ if (confirmedCommand) return true
43
+
44
+ commandEcho.setInteractive()
45
+
46
+ const answer = await confirm({ message: `Deploy ${branch} → ${env}?`, default: false })
47
+
48
+ if (answer) commandEcho.addOption('--yes', true)
49
+
50
+ return answer
24
51
  }
25
52
 
26
53
  /**
27
54
  * Deploy a release branch to an environment
28
55
  */
29
56
  export const ghReleaseDeployAll = async (args: GhReleaseDeployAllArgs) => {
30
- const { version, env, skipTerraform } = args
57
+ const { version, env, skipTerraform, confirmedCommand } = args
31
58
 
32
59
  commandEcho.start('release-deploy-all')
33
60
 
@@ -98,6 +125,23 @@ export const ghReleaseDeployAll = async (args: GhReleaseDeployAllArgs) => {
98
125
  commandEcho.addOption('--skip-terraform', true)
99
126
  }
100
127
 
128
+ if (!(await confirmDeploy({ confirmedCommand, branch: selectedReleaseBranch, env: selectedEnv }))) {
129
+ logger.info('Deployment cancelled')
130
+
131
+ const structuredContent = {
132
+ releaseBranch: selectedReleaseBranch,
133
+ version: selectedVersion,
134
+ environment: selectedEnv,
135
+ skipTerraformDeploy: shouldSkipTerraform,
136
+ success: false,
137
+ }
138
+
139
+ return {
140
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
141
+ structuredContent,
142
+ }
143
+ }
144
+
101
145
  try {
102
146
  $.quiet = true
103
147
 
@@ -1,4 +1,5 @@
1
1
  import checkbox from '@inquirer/checkbox'
2
+ import confirm from '@inquirer/confirm'
2
3
  import select from '@inquirer/select'
3
4
  import fs from 'node:fs/promises'
4
5
  import { resolve } from 'node:path'
@@ -27,6 +28,32 @@ interface GhReleaseDeploySelectedArgs {
27
28
  env: string
28
29
  services: string[]
29
30
  skipTerraform?: boolean
31
+ confirmedCommand?: boolean
32
+ }
33
+
34
+ interface ConfirmDeployArgs {
35
+ confirmedCommand?: boolean
36
+ branch: string
37
+ env: string
38
+ }
39
+
40
+ /**
41
+ * Gate the workflow dispatch behind an interactive confirmation. Returns true to
42
+ * proceed; `confirmedCommand` (CLI `--yes`) skips the prompt. Mirrors the
43
+ * confirm+commandEcho pattern used by worktrees-remove.
44
+ */
45
+ const confirmDeploy = async (args: ConfirmDeployArgs): Promise<boolean> => {
46
+ const { confirmedCommand, branch, env } = args
47
+
48
+ if (confirmedCommand) return true
49
+
50
+ commandEcho.setInteractive()
51
+
52
+ const answer = await confirm({ message: `Deploy ${branch} → ${env}?`, default: false })
53
+
54
+ if (answer) commandEcho.addOption('--yes', true)
55
+
56
+ return answer
30
57
  }
31
58
 
32
59
  /**
@@ -34,7 +61,7 @@ interface GhReleaseDeploySelectedArgs {
34
61
  */
35
62
  // eslint-disable-next-line sonarjs/cognitive-complexity
36
63
  export const ghReleaseDeploySelected = async (args: GhReleaseDeploySelectedArgs) => {
37
- const { version, env, services, skipTerraform } = args
64
+ const { version, env, services, skipTerraform, confirmedCommand } = args
38
65
 
39
66
  commandEcho.start('release-deploy-selected')
40
67
 
@@ -157,6 +184,24 @@ export const ghReleaseDeploySelected = async (args: GhReleaseDeploySelectedArgs)
157
184
  commandEcho.addOption('--skip-terraform', true)
158
185
  }
159
186
 
187
+ if (!(await confirmDeploy({ confirmedCommand, branch: selectedReleaseBranch, env: selectedEnv }))) {
188
+ logger.info('Deployment cancelled')
189
+
190
+ const structuredContent = {
191
+ releaseBranch: selectedReleaseBranch,
192
+ version: selectedVersion,
193
+ environment: selectedEnv,
194
+ services: selectedServices,
195
+ skipTerraformDeploy: shouldSkipTerraform,
196
+ success: false,
197
+ }
198
+
199
+ return {
200
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
201
+ structuredContent,
202
+ }
203
+ }
204
+
160
205
  try {
161
206
  $.quiet = true
162
207
 
@@ -5,7 +5,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
5
 
6
6
  import { getProjectRoot, getRepoName } from 'src/lib/git-utils'
7
7
 
8
- import { AGENTS_IMPORT_START, AGENTS_MARKER_END, AGENTS_MARKER_START, writeAgentFiles } from '../agent-files'
8
+ import {
9
+ AGENTS_IMPORT_END,
10
+ AGENTS_IMPORT_START,
11
+ AGENTS_MARKER_END,
12
+ AGENTS_MARKER_START,
13
+ writeAgentFiles,
14
+ } from '../agent-files'
9
15
 
10
16
  vi.mock('src/lib/git-utils', () => {
11
17
  return {
@@ -46,32 +52,30 @@ describe('writeAgentFiles', () => {
46
52
  vi.clearAllMocks()
47
53
  })
48
54
 
49
- it('creates AGENTS.md, the CLAUDE.md import, and the .cursor rule on a fresh repo', async () => {
55
+ it('writes the full guidance block only to CLAUDE.md, and creates no other files, on a fresh repo', async () => {
50
56
  await withTmpRepo(async (tmp) => {
51
57
  const result = await writeAgentFiles()
52
58
 
53
59
  expect(result.skipped).toBe(false)
54
60
  expect(result.root).toBe(tmp)
55
61
 
56
- const agents = fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')
57
-
58
- expect(agents).toContain(AGENTS_MARKER_START)
59
- expect(agents).toContain(AGENTS_MARKER_END)
60
- expect(agents).toContain('<!-- infra-kit:version ')
61
-
62
62
  const claude = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8')
63
63
 
64
- expect(claude).toContain(AGENTS_IMPORT_START)
65
- expect(claude).toContain('@AGENTS.md')
66
-
67
- const cursor = fs.readFileSync(path.join(tmp, '.cursor', 'rules', 'infra-kit.mdc'), 'utf-8')
68
-
69
- expect(cursor).toContain('alwaysApply: true')
70
- expect(cursor).toContain(AGENTS_MARKER_START)
64
+ expect(claude).toContain(AGENTS_MARKER_START)
65
+ expect(claude).toContain(AGENTS_MARKER_END)
66
+ expect(claude).toContain('<!-- infra-kit:version ')
67
+ expect(claude).toContain('# infra-kit')
68
+ // The migrated `@AGENTS.md` import region must never appear.
69
+ expect(claude).not.toContain(AGENTS_IMPORT_START)
70
+ expect(claude).not.toContain('@AGENTS.md')
71
+
72
+ // CLAUDE.md is the only output — infra-kit no longer touches AGENTS.md or .cursor.
73
+ expect(fs.existsSync(path.join(tmp, 'AGENTS.md'))).toBe(false)
74
+ expect(fs.existsSync(path.join(tmp, '.cursor'))).toBe(false)
71
75
  })
72
76
  })
73
77
 
74
- it('preserves existing hand-authored CLAUDE.md content verbatim (ticket conventions)', async () => {
78
+ it('preserves existing hand-authored CLAUDE.md content and appends the block after it', async () => {
75
79
  await withTmpRepo(async (tmp) => {
76
80
  const claudePath = path.join(tmp, 'CLAUDE.md')
77
81
  const userContent = '# CLAUDE.md\n\n## Ticket Naming Convention\n\n- `[FE]` — frontend\n- `[BE]` — backend\n'
@@ -84,17 +88,19 @@ describe('writeAgentFiles', () => {
84
88
 
85
89
  expect(updated).toContain('## Ticket Naming Convention')
86
90
  expect(updated).toContain('`[FE]` — frontend')
87
- expect(updated).toContain(AGENTS_IMPORT_START)
91
+ expect(updated).toContain(AGENTS_MARKER_START)
92
+ // The managed block lands AFTER the hand-authored content.
93
+ expect(updated.indexOf('## Ticket Naming Convention')).toBeLessThan(updated.indexOf(AGENTS_MARKER_START))
88
94
  })
89
95
  })
90
96
 
91
- it('is idempotent — a second run does not duplicate the block', async () => {
97
+ it('is idempotent — a second run does not duplicate the block and reports unchanged (same CLI version)', async () => {
92
98
  await withTmpRepo(async (tmp) => {
93
99
  await writeAgentFiles()
94
- const first = fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')
100
+ const first = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8')
95
101
 
96
102
  const second = await writeAgentFiles()
97
- const after = fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')
103
+ const after = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8')
98
104
 
99
105
  expect(after).toBe(first)
100
106
  expect(after.match(new RegExp(AGENTS_MARKER_START, 'g'))?.length).toBe(1)
@@ -113,35 +119,147 @@ describe('writeAgentFiles', () => {
113
119
 
114
120
  expect(result.skipped).toBe(true)
115
121
  expect(result.root).toBeNull()
116
- expect(fs.existsSync(path.join(tmp, 'AGENTS.md'))).toBe(false)
122
+ expect(fs.existsSync(path.join(tmp, 'CLAUDE.md'))).toBe(false)
117
123
  },
118
124
  { repo: false },
119
125
  )
120
126
  })
121
127
 
122
- it('refuses to write through a symlinked AGENTS.md', async () => {
128
+ it('refuses to write through a symlinked CLAUDE.md', async () => {
123
129
  await withTmpRepo(async (tmp) => {
124
130
  const realTarget = path.join(tmp, 'elsewhere.md')
125
131
 
126
132
  writeFile(realTarget, 'sensitive')
127
- fs.symlinkSync(realTarget, path.join(tmp, 'AGENTS.md'))
133
+ fs.symlinkSync(realTarget, path.join(tmp, 'CLAUDE.md'))
128
134
 
129
135
  await expect(writeAgentFiles()).rejects.toThrow(/symlink/)
130
136
  })
131
137
  })
132
138
 
133
- it('backs up an existing AGENTS.md before overwriting', async () => {
139
+ it('backs up an existing CLAUDE.md before overwriting', async () => {
134
140
  await withTmpRepo(async (tmp) => {
135
- writeFile(path.join(tmp, 'AGENTS.md'), 'previous content without markers\n')
141
+ writeFile(path.join(tmp, 'CLAUDE.md'), 'previous content without markers\n')
136
142
 
137
143
  await writeAgentFiles()
138
144
 
139
145
  const backups = fs.readdirSync(tmp).filter((f) => {
140
- return f.startsWith('AGENTS.md.backup.')
146
+ return f.startsWith('CLAUDE.md.backup.')
141
147
  })
142
148
 
143
- expect(backups.length).toBe(1)
149
+ expect(backups).toHaveLength(1)
144
150
  expect(fs.readFileSync(path.join(tmp, backups[0]!), 'utf-8')).toContain('previous content without markers')
145
151
  })
146
152
  })
153
+
154
+ describe('legacy AGENTS.md migration', () => {
155
+ /** Seed a repo in the pre-migration state: AGENTS.md (full block) + CLAUDE.md (@AGENTS.md import). */
156
+ const seedLegacy = async (
157
+ tmp: string,
158
+ opts: { agentsExtra?: string; claudeExtra?: string } = {},
159
+ ): Promise<void> => {
160
+ const agentsBlock = `${AGENTS_MARKER_START}\n# infra-kit\nlegacy body\n${AGENTS_MARKER_END}\n`
161
+ const importBlock = `${AGENTS_IMPORT_START}\n@AGENTS.md\n${AGENTS_IMPORT_END}\n`
162
+
163
+ writeFile(path.join(tmp, 'AGENTS.md'), `${opts.agentsExtra ?? ''}${agentsBlock}`)
164
+ writeFile(path.join(tmp, 'CLAUDE.md'), `${opts.claudeExtra ?? ''}${importBlock}`)
165
+ }
166
+
167
+ it('removes a purely-generated AGENTS.md (with a backup) and strips the @AGENTS.md import from CLAUDE.md', async () => {
168
+ await withTmpRepo(async (tmp) => {
169
+ await seedLegacy(tmp)
170
+
171
+ await writeAgentFiles()
172
+
173
+ // AGENTS.md is gone, but a backup survives.
174
+ expect(fs.existsSync(path.join(tmp, 'AGENTS.md'))).toBe(false)
175
+ const backups = fs.readdirSync(tmp).filter((f) => {
176
+ return f.startsWith('AGENTS.md.backup.')
177
+ })
178
+
179
+ expect(backups).toHaveLength(1)
180
+
181
+ // CLAUDE.md now carries the full block and no longer imports @AGENTS.md.
182
+ const claude = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8')
183
+
184
+ expect(claude).toContain(AGENTS_MARKER_START)
185
+ expect(claude).not.toContain(AGENTS_IMPORT_START)
186
+ expect(claude).not.toContain('@AGENTS.md')
187
+ })
188
+ })
189
+
190
+ it('retains an AGENTS.md that has hand-authored content (block removed, file kept, backup made)', async () => {
191
+ await withTmpRepo(async (tmp) => {
192
+ await seedLegacy(tmp, { agentsExtra: '# My own notes\n\nkeep me\n\n' })
193
+
194
+ await writeAgentFiles()
195
+
196
+ // File retained with hand content; the infra-kit block is stripped.
197
+ expect(fs.existsSync(path.join(tmp, 'AGENTS.md'))).toBe(true)
198
+ const agents = fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')
199
+
200
+ expect(agents).toContain('# My own notes')
201
+ expect(agents).toContain('keep me')
202
+ expect(agents).not.toContain(AGENTS_MARKER_START)
203
+
204
+ // A backup of the pre-strip file exists.
205
+ const backups = fs.readdirSync(tmp).filter((f) => {
206
+ return f.startsWith('AGENTS.md.backup.')
207
+ })
208
+
209
+ expect(backups).toHaveLength(1)
210
+ })
211
+ })
212
+
213
+ it('leaves an AGENTS.md with no infra-kit block untouched (not ours)', async () => {
214
+ await withTmpRepo(async (tmp) => {
215
+ const handAuthored = '# AGENTS\n\nentirely hand-written, no markers\n'
216
+
217
+ writeFile(path.join(tmp, 'AGENTS.md'), handAuthored)
218
+
219
+ await writeAgentFiles()
220
+
221
+ expect(fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')).toBe(handAuthored)
222
+ const backups = fs.readdirSync(tmp).filter((f) => {
223
+ return f.startsWith('AGENTS.md.backup.')
224
+ })
225
+
226
+ expect(backups).toHaveLength(0)
227
+ })
228
+ })
229
+
230
+ it('is idempotent after migration — a second run finds nothing left to migrate', async () => {
231
+ await withTmpRepo(async (tmp) => {
232
+ await seedLegacy(tmp)
233
+ await writeAgentFiles()
234
+
235
+ const second = await writeAgentFiles()
236
+
237
+ expect(
238
+ second.written.every((w) => {
239
+ return w.action === 'unchanged'
240
+ }),
241
+ ).toBe(true)
242
+ })
243
+ })
244
+ })
245
+
246
+ it('does not touch a pre-existing .cursor/rules file — infra-kit no longer supports Cursor rules', async () => {
247
+ await withTmpRepo(async (tmp) => {
248
+ const cursorPath = path.join(tmp, '.cursor', 'rules', 'infra-kit.mdc')
249
+ // Even a file that carries our managed block is left completely alone now.
250
+ const existing = `---\ndescription: x\n---\n${AGENTS_MARKER_START}\n# infra-kit\nold body\n${AGENTS_MARKER_END}\n`
251
+
252
+ writeFile(cursorPath, existing)
253
+
254
+ await writeAgentFiles()
255
+
256
+ // Untouched: same content, no backup, no deletion.
257
+ expect(fs.readFileSync(cursorPath, 'utf-8')).toBe(existing)
258
+ const backups = fs.readdirSync(path.join(tmp, '.cursor', 'rules')).filter((f) => {
259
+ return f.startsWith('infra-kit.mdc.backup.')
260
+ })
261
+
262
+ expect(backups).toHaveLength(0)
263
+ })
264
+ })
147
265
  })
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6
6
  // Import AFTER the mock is declared so the module picks up the mocked dep.
7
7
  import { getProjectRoot, getRepoName } from 'src/lib/git-utils'
8
8
 
9
- import { migrateLegacyConfig, normalizeLegacyIdeStructures } from '../migrate-config'
9
+ import { migrateLegacyConfig, migrateUserGlobalConfigFilename, normalizeLegacyIdeStructures } from '../migrate-config'
10
10
 
11
11
  vi.mock('src/lib/git-utils', () => {
12
12
  return {
@@ -105,8 +105,10 @@ describe('migrateLegacyConfig', () => {
105
105
  await withTmpRepo(async (tmp) => {
106
106
  const mainYml = path.join(tmp, 'infra-kit.yml')
107
107
  const mainJson = path.join(tmp, 'infra-kit.json')
108
+ // The user-global legacy YAML keeps the old `config.yml` name; its JSON
109
+ // target is the renamed canonical `infra-kit.json`.
108
110
  const userGlobalYml = path.join(tmp, '.infra-kit', 'config.yml')
109
- const userGlobalJson = path.join(tmp, '.infra-kit', 'config.json')
111
+ const userGlobalJson = path.join(tmp, '.infra-kit', 'infra-kit.json')
110
112
 
111
113
  writeFile(mainYml, MAIN_YML)
112
114
  // Invalid override: environments present but empty (min(1) fails).
@@ -151,9 +153,17 @@ describe('migrateLegacyConfig', () => {
151
153
 
152
154
  await migrateLegacyConfig()
153
155
 
154
- for (const yml of [mainYml, userGlobalYml, userProjectYml]) {
156
+ // The user-global legacy `config.yml` migrates to the renamed canonical
157
+ // `infra-kit.json`; main/userProject keep their `.yml`→`.json` sibling name.
158
+ const pairs: [yml: string, json: string][] = [
159
+ [mainYml, path.join(tmp, 'infra-kit.json')],
160
+ [userGlobalYml, path.join(tmp, '.infra-kit', 'infra-kit.json')],
161
+ [userProjectYml, path.join(tmp, '.infra-kit', 'projects', projectName, 'infra-kit.json')],
162
+ ]
163
+
164
+ for (const [yml, json] of pairs) {
155
165
  expect(fs.existsSync(yml)).toBe(false)
156
- expect(fs.existsSync(yml.replace(/\.yml$/, '.json'))).toBe(true)
166
+ expect(fs.existsSync(json)).toBe(true)
157
167
  }
158
168
  })
159
169
  })
@@ -221,7 +231,7 @@ describe('normalizeLegacyIdeStructures', () => {
221
231
 
222
232
  it('normalizes the user-global config layer too', async () => {
223
233
  await withTmpRepo(async (tmp) => {
224
- const userGlobalJson = path.join(tmp, '.infra-kit', 'config.json')
234
+ const userGlobalJson = path.join(tmp, '.infra-kit', 'infra-kit.json')
225
235
 
226
236
  writeFile(userGlobalJson, JSON.stringify({ ide: { provider: 'zed', config: { mode: 'windows' } } }))
227
237
 
@@ -289,3 +299,124 @@ describe('normalizeLegacyIdeStructures', () => {
289
299
  })
290
300
  })
291
301
  })
302
+
303
+ describe('migrateUserGlobalConfigFilename', () => {
304
+ beforeEach(() => {
305
+ vi.clearAllMocks()
306
+ })
307
+
308
+ afterEach(() => {
309
+ vi.clearAllMocks()
310
+ })
311
+
312
+ const userGlobalPaths = (tmp: string) => {
313
+ const dir = path.join(tmp, '.infra-kit')
314
+
315
+ return {
316
+ legacyConfig: path.join(dir, 'config.json'),
317
+ newConfig: path.join(dir, 'infra-kit.json'),
318
+ legacyExample: path.join(dir, 'config.example.jsonc'),
319
+ newExample: path.join(dir, 'infra-kit.example.jsonc'),
320
+ }
321
+ }
322
+
323
+ it('renames config.json → infra-kit.json (and the example) preserving content', async () => {
324
+ await withTmpRepo(async (tmp) => {
325
+ const p = userGlobalPaths(tmp)
326
+ const configBody = JSON.stringify({ worktrees: { openInCmux: true } })
327
+ const exampleBody = '// example\n{}\n'
328
+
329
+ writeFile(p.legacyConfig, configBody)
330
+ writeFile(p.legacyExample, exampleBody)
331
+
332
+ await migrateUserGlobalConfigFilename()
333
+
334
+ expect(fs.existsSync(p.legacyConfig)).toBe(false)
335
+ expect(fs.existsSync(p.legacyExample)).toBe(false)
336
+ expect(fs.readFileSync(p.newConfig, 'utf-8')).toBe(configBody)
337
+ expect(fs.readFileSync(p.newExample, 'utf-8')).toBe(exampleBody)
338
+ })
339
+ })
340
+
341
+ it('is a no-op when there is no legacy config.json (idempotent)', async () => {
342
+ await withTmpRepo(async (tmp) => {
343
+ const p = userGlobalPaths(tmp)
344
+
345
+ await expect(migrateUserGlobalConfigFilename()).resolves.toBeUndefined()
346
+
347
+ expect(fs.existsSync(p.legacyConfig)).toBe(false)
348
+ expect(fs.existsSync(p.newConfig)).toBe(false)
349
+ })
350
+ })
351
+
352
+ it('never overwrites an existing infra-kit.json; leaves the stale config.json in place', async () => {
353
+ await withTmpRepo(async (tmp) => {
354
+ const p = userGlobalPaths(tmp)
355
+ const legacyBody = JSON.stringify({ worktrees: { openInCmux: false } })
356
+ const activeBody = JSON.stringify({ worktrees: { openInCmux: true } })
357
+
358
+ writeFile(p.legacyConfig, legacyBody)
359
+ writeFile(p.newConfig, activeBody)
360
+
361
+ await migrateUserGlobalConfigFilename()
362
+
363
+ // Active config untouched (NOT clobbered); legacy file preserved for the user.
364
+ expect(fs.readFileSync(p.newConfig, 'utf-8')).toBe(activeBody)
365
+ expect(fs.readFileSync(p.legacyConfig, 'utf-8')).toBe(legacyBody)
366
+ })
367
+ })
368
+
369
+ it('migrates each file independently (example present, config absent)', async () => {
370
+ await withTmpRepo(async (tmp) => {
371
+ const p = userGlobalPaths(tmp)
372
+ const exampleBody = '// example only\n{}\n'
373
+
374
+ writeFile(p.legacyExample, exampleBody)
375
+
376
+ await migrateUserGlobalConfigFilename()
377
+
378
+ expect(fs.existsSync(p.legacyExample)).toBe(false)
379
+ expect(fs.readFileSync(p.newExample, 'utf-8')).toBe(exampleBody)
380
+ // No config.json existed → nothing created on that side.
381
+ expect(fs.existsSync(p.newConfig)).toBe(false)
382
+ })
383
+ })
384
+
385
+ it('yAML-legacy upgrade: migrateLegacyConfig + rename land config at infra-kit.json', async () => {
386
+ await withTmpRepo(async (tmp) => {
387
+ const p = userGlobalPaths(tmp)
388
+ const legacyYml = path.join(tmp, '.infra-kit', 'config.yml')
389
+
390
+ writeFile(legacyYml, 'worktrees:\n openInCmux: true\n')
391
+
392
+ // migrateLegacyConfig converts the pinned config.yml source → infra-kit.json;
393
+ // the rename then has nothing left to do for the user-global config.
394
+ await migrateLegacyConfig()
395
+ await migrateUserGlobalConfigFilename()
396
+
397
+ expect(fs.existsSync(legacyYml)).toBe(false)
398
+ expect(fs.existsSync(p.legacyConfig)).toBe(false)
399
+ expect(JSON.parse(fs.readFileSync(p.newConfig, 'utf-8')).worktrees.openInCmux).toBe(true)
400
+ })
401
+ })
402
+
403
+ it('both-legacy files: YAML wins via migrateLegacyConfig, config.json is left untouched', async () => {
404
+ await withTmpRepo(async (tmp) => {
405
+ const p = userGlobalPaths(tmp)
406
+ const legacyYml = path.join(tmp, '.infra-kit', 'config.yml')
407
+ const jsonBody = JSON.stringify({ worktrees: { openInCmux: false } })
408
+
409
+ writeFile(legacyYml, 'worktrees:\n openInCmux: true\n')
410
+ writeFile(p.legacyConfig, jsonBody)
411
+
412
+ // init() order: migrateLegacyConfig (yml→infra-kit.json) then the rename.
413
+ await migrateLegacyConfig()
414
+ await migrateUserGlobalConfigFilename()
415
+
416
+ // YAML content won at infra-kit.json; the rename's no-overwrite guard then
417
+ // skipped config.json, leaving it untouched (no data loss).
418
+ expect(JSON.parse(fs.readFileSync(p.newConfig, 'utf-8')).worktrees.openInCmux).toBe(true)
419
+ expect(fs.readFileSync(p.legacyConfig, 'utf-8')).toBe(jsonBody)
420
+ })
421
+ })
422
+ })