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
@@ -55,6 +55,7 @@ const zed = vi.hoisted(() => {
55
55
  return {
56
56
  openZedWorkspace: vi.fn(),
57
57
  addFoldersToZedWorkspace: vi.fn(),
58
+ reuseZedWorkspace: vi.fn(),
58
59
  }
59
60
  })
60
61
 
@@ -73,7 +74,6 @@ const baseOpenArgs = {
73
74
  projectRoot: '/repo',
74
75
  worktreeDir: '/repo.worktrees',
75
76
  currentBranches: ['release/v1.0.0'],
76
- skipRelaunchWhenEmpty: false,
77
77
  }
78
78
 
79
79
  beforeEach(() => {
@@ -191,7 +191,14 @@ describe('addIdeWorktreeFolders', () => {
191
191
  })
192
192
 
193
193
  describe('removeIdeWorktreeFolders', () => {
194
- const args = { projectRoot: '/repo', worktreeDir: '/repo.worktrees', branches: ['release/v1.0.0'] }
194
+ // currentWorktrees \ removedWorktrees => remaining = ['release/v1.1.0']
195
+ const args = {
196
+ projectRoot: '/repo',
197
+ worktreeDir: '/repo.worktrees',
198
+ currentWorktrees: ['release/v1.0.0', 'release/v1.1.0'],
199
+ removedWorktrees: ['release/v1.0.0'],
200
+ allowEditorRelaunch: false,
201
+ }
195
202
 
196
203
  it('removes from the Cursor workspace', async () => {
197
204
  config.value = cursorConfig
@@ -212,31 +219,63 @@ describe('removeIdeWorktreeFolders', () => {
212
219
  expect(outcomes).toEqual([{ provider: 'cursor', supported: true, removed: [] }])
213
220
  })
214
221
 
215
- it('reports unsupported for Zed without throwing', async () => {
222
+ it('does NOT relaunch Zed on the non-interactive path (allowEditorRelaunch: false)', async () => {
216
223
  config.value = zedConfig
217
224
 
218
- const outcomes = await removeIdeWorktreeFolders(args)
225
+ const outcomes = await removeIdeWorktreeFolders({ ...args, allowEditorRelaunch: false })
226
+
227
+ expect(zed.reuseZedWorkspace).not.toHaveBeenCalled()
228
+ expect(outcomes).toEqual([{ provider: 'zed', supported: true, removed: [] }])
229
+ })
219
230
 
220
- expect(outcomes).toEqual([{ provider: 'zed', supported: false, removed: [] }])
231
+ it('relaunches Zed with the remaining set on the interactive path (allowEditorRelaunch: true)', async () => {
232
+ config.value = zedConfig
233
+
234
+ const outcomes = await removeIdeWorktreeFolders({ ...args, allowEditorRelaunch: true })
235
+
236
+ expect(zed.reuseZedWorkspace).toHaveBeenCalledWith({
237
+ projectRoot: '/repo',
238
+ worktreeDir: '/repo.worktrees',
239
+ remainingBranches: ['release/v1.1.0'],
240
+ })
241
+ expect(outcomes).toEqual([{ provider: 'zed', supported: true, removed: [] }])
221
242
  })
222
243
 
223
- it('removes from Cursor AND reports Zed unsupported when both are configured', async () => {
244
+ it('reports Zed removed:[] when the relaunch swallowed a failure (ran:false), never throwing', async () => {
245
+ config.value = zedConfig
246
+ // reuseZedWorkspace is best-effort: it swallows launch failures internally and resolves
247
+ // { ran: false } rather than rejecting, so the facade never sees a throw.
248
+ zed.reuseZedWorkspace.mockResolvedValue({ ran: false })
249
+
250
+ const outcomes = await removeIdeWorktreeFolders({ ...args, allowEditorRelaunch: true })
251
+
252
+ expect(outcomes).toEqual([{ provider: 'zed', supported: true, removed: [] }])
253
+ })
254
+
255
+ it('removes from Cursor surgically AND relaunches Zed when both are configured (interactive)', async () => {
224
256
  config.value = bothConfig
225
257
  cursor.removeFoldersFromCursorWorkspace.mockResolvedValue({ removed: ['x'], notFound: [] })
226
258
 
227
- const outcomes = await removeIdeWorktreeFolders(args)
259
+ const outcomes = await removeIdeWorktreeFolders({ ...args, allowEditorRelaunch: true })
228
260
 
261
+ expect(cursor.removeFoldersFromCursorWorkspace).toHaveBeenCalled()
262
+ expect(zed.reuseZedWorkspace).toHaveBeenCalledWith({
263
+ projectRoot: '/repo',
264
+ worktreeDir: '/repo.worktrees',
265
+ remainingBranches: ['release/v1.1.0'],
266
+ })
229
267
  expect(outcomes).toEqual([
230
268
  { provider: 'cursor', supported: true, removed: ['x'] },
231
- { provider: 'zed', supported: false, removed: [] },
269
+ { provider: 'zed', supported: true, removed: [] },
232
270
  ])
233
271
  })
234
272
 
235
- it('short-circuits to an empty array when there are no branches (no config read needed)', async () => {
273
+ it('short-circuits to an empty array when nothing was removed (no config read needed)', async () => {
236
274
  config.value = zedConfig
237
275
 
238
- const outcomes = await removeIdeWorktreeFolders({ ...args, branches: [] })
276
+ const outcomes = await removeIdeWorktreeFolders({ ...args, removedWorktrees: [] })
239
277
 
278
+ expect(zed.reuseZedWorkspace).not.toHaveBeenCalled()
240
279
  expect(outcomes).toEqual([])
241
280
  })
242
281
 
@@ -9,15 +9,15 @@ interface OpenIdeWorkspaceArgs {
9
9
  projectRoot: string
10
10
  worktreeDir: string
11
11
  currentBranches: string[]
12
- skipRelaunchWhenEmpty: boolean
13
12
  }
14
13
 
15
14
  /**
16
- * Provider-agnostic entry point for the cold-start / reload open: for every
17
- * configured editor, reconciles (or, for Zed, simply assembles) the workspace
18
- * against the release worktrees on disk and launches it. Returns one outcome per
19
- * configured provider (empty array when no IDE is configured). Iterates
20
- * sequentially `worktrees-reload` already wraps this call in an outer
15
+ * Provider-agnostic entry point for the reload open: for every configured
16
+ * editor, reconciles (or, for Zed, simply assembles) the workspace against the
17
+ * release worktrees on disk and launches it skipping the launch when there are
18
+ * no worktrees, so `worktrees-reload` never pops a bare editor window. Returns
19
+ * one outcome per configured provider (empty array when no IDE is configured).
20
+ * Iterates sequentially — `worktrees-reload` already wraps this call in an outer
21
21
  * `Promise.all` with cmux, so a `Promise.all` here would compound editor-spawn
22
22
  * concurrency. Best-effort — every provider swallows failures into a warning.
23
23
  */
@@ -1,4 +1,5 @@
1
1
  import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
2
+ import { reuseZedWorkspace } from 'src/integrations/zed'
2
3
  import { assertNever } from 'src/lib/assert-never'
3
4
  import { getInfraKitConfig, resolveConfiguredIdes } from 'src/lib/infra-kit-config'
4
5
  import { logger } from 'src/lib/logger'
@@ -8,7 +9,17 @@ import type { RemoveIdeWorktreeFoldersOutcome } from './types'
8
9
  interface RemoveIdeWorktreeFoldersArgs {
9
10
  projectRoot: string
10
11
  worktreeDir: string
11
- branches: string[]
12
+ /** All release worktrees BEFORE removal (release-only — see Zed note below). */
13
+ currentWorktrees: string[]
14
+ /** The subset of `currentWorktrees` that was just removed. */
15
+ removedWorktrees: string[]
16
+ /**
17
+ * Whether the caller is on an interactive path where firing Zed's destructive
18
+ * `zed --reuse` is acceptable (a human is present and confirming). Interactive
19
+ * `worktrees-remove` passes `!confirmedCommand`; `worktrees-sync` and every
20
+ * non-interactive/MCP run pass `false`.
21
+ */
22
+ allowEditorRelaunch: boolean
12
23
  }
13
24
 
14
25
  /**
@@ -17,57 +28,51 @@ interface RemoveIdeWorktreeFoldersArgs {
17
28
  * provider (empty array when no IDE is configured or no worktrees were removed),
18
29
  * iterating sequentially.
19
30
  *
20
- * Cursor edits the `.code-workspace` `folders` array. Zed has no folder-remove
21
- * CLI, so it is a structural no-op (`supported: false`) with an info-level
22
- * message — never a warning, because nothing failed.
31
+ * Cursor surgically edits the `.code-workspace` `folders` array it removes
32
+ * only the named paths and leaves everything else intact.
33
+ *
34
+ * Zed has no surgical remove. The only mutation mechanism, `zed --reuse`,
35
+ * REPLACES the focused window's entire folder set, so it can only re-state the
36
+ * release worktrees we know about and silently drops any other open folder
37
+ * (`remaining` is built from release worktrees only). Because that is
38
+ * destructive and unrecoverable, it fires ONLY when `allowEditorRelaunch` is
39
+ * true (interactive `worktrees-remove`). On the non-interactive/MCP path it is a
40
+ * deliberate no-op (`supported: true`, `removed: []`) with an info message — the
41
+ * capability exists; skipping is a policy choice, not a missing capability.
42
+ *
43
+ * Zed's `removed` is ALWAYS `[]`: `--reuse` performs no diff, so it confirms no
44
+ * specific removal and we never report intended-but-unverified paths.
23
45
  */
24
46
  export const removeIdeWorktreeFolders = async (
25
47
  args: RemoveIdeWorktreeFoldersArgs,
26
48
  ): Promise<RemoveIdeWorktreeFoldersOutcome[]> => {
27
- const { projectRoot, worktreeDir, branches } = args
49
+ const { projectRoot, worktreeDir, currentWorktrees, removedWorktrees, allowEditorRelaunch } = args
28
50
 
29
- if (branches.length === 0) {
51
+ if (removedWorktrees.length === 0) {
30
52
  return []
31
53
  }
32
54
 
33
55
  const config = await getInfraKitConfig()
34
56
  const ides = resolveConfiguredIdes(config)
35
57
 
36
- const folderPaths = branches.map((branch) => {
58
+ const folderPaths = removedWorktrees.map((branch) => {
37
59
  return `${worktreeDir}/${branch}`
38
60
  })
39
61
 
62
+ const remainingBranches = currentWorktrees.filter((branch) => {
63
+ return !removedWorktrees.includes(branch)
64
+ })
65
+
40
66
  const outcomes: RemoveIdeWorktreeFoldersOutcome[] = []
41
67
 
42
68
  for (const ide of ides) {
43
69
  switch (ide.provider) {
44
70
  case 'cursor': {
45
- if (!ide.config.workspaceConfigPath) {
46
- outcomes.push({ provider: 'cursor', supported: true, removed: [] })
47
- break
48
- }
49
-
50
- const workspacePath = resolveCursorWorkspacePath(ide.config.workspaceConfigPath, projectRoot)
51
-
52
- try {
53
- const { removed } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })
54
-
55
- if (removed.length > 0) {
56
- logger.info(`✅ Removed ${removed.length} folder(s) from ${workspacePath}`)
57
- }
58
-
59
- outcomes.push({ provider: 'cursor', supported: true, removed })
60
- } catch (error) {
61
- logger.warn({ error }, `⚠️ Failed to update Cursor workspace at ${workspacePath}`)
62
-
63
- outcomes.push({ provider: 'cursor', supported: true, removed: [] })
64
- }
71
+ outcomes.push(await removeFromCursor({ ide, projectRoot, folderPaths }))
65
72
  break
66
73
  }
67
74
  case 'zed': {
68
- logger.info('ℹ️ Zed has no folder-remove CLI; close removed worktree folders in Zed manually if needed.')
69
-
70
- outcomes.push({ provider: 'zed', supported: false, removed: [] })
75
+ outcomes.push(await removeFromZed({ projectRoot, worktreeDir, remainingBranches, allowEditorRelaunch }))
71
76
  break
72
77
  }
73
78
  default: {
@@ -78,3 +83,63 @@ export const removeIdeWorktreeFolders = async (
78
83
 
79
84
  return outcomes
80
85
  }
86
+
87
+ interface RemoveFromCursorArgs {
88
+ ide: Extract<ReturnType<typeof resolveConfiguredIdes>[number], { provider: 'cursor' }>
89
+ projectRoot: string
90
+ folderPaths: string[]
91
+ }
92
+
93
+ /** Surgically strip the removed folder paths from the Cursor `.code-workspace`. */
94
+ const removeFromCursor = async (args: RemoveFromCursorArgs): Promise<RemoveIdeWorktreeFoldersOutcome> => {
95
+ const { ide, projectRoot, folderPaths } = args
96
+
97
+ if (!ide.config.workspaceConfigPath) {
98
+ return { provider: 'cursor', supported: true, removed: [] }
99
+ }
100
+
101
+ const workspacePath = resolveCursorWorkspacePath(ide.config.workspaceConfigPath, projectRoot)
102
+
103
+ try {
104
+ const { removed } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })
105
+
106
+ if (removed.length > 0) {
107
+ logger.info(`✅ Removed ${removed.length} folder(s) from ${workspacePath}`)
108
+ }
109
+
110
+ return { provider: 'cursor', supported: true, removed }
111
+ } catch (error) {
112
+ logger.warn({ error }, `⚠️ Failed to update Cursor workspace at ${workspacePath}`)
113
+
114
+ return { provider: 'cursor', supported: true, removed: [] }
115
+ }
116
+ }
117
+
118
+ interface RemoveFromZedArgs {
119
+ projectRoot: string
120
+ worktreeDir: string
121
+ remainingBranches: string[]
122
+ allowEditorRelaunch: boolean
123
+ }
124
+
125
+ /**
126
+ * Reflect the removal in Zed by relaunching the focused window onto the
127
+ * remaining set — but ONLY on an interactive path (see the module doc for why
128
+ * `zed --reuse` is destructive). Otherwise a deliberate no-op. `removed` is
129
+ * always `[]` because `--reuse` performs no diff.
130
+ */
131
+ const removeFromZed = async (args: RemoveFromZedArgs): Promise<RemoveIdeWorktreeFoldersOutcome> => {
132
+ const { projectRoot, worktreeDir, remainingBranches, allowEditorRelaunch } = args
133
+
134
+ if (!allowEditorRelaunch) {
135
+ logger.info(
136
+ 'ℹ️ Zed folder removal skipped (no interactive session); close removed worktree folders in Zed manually if needed.',
137
+ )
138
+
139
+ return { provider: 'zed', supported: true, removed: [] }
140
+ }
141
+
142
+ await reuseZedWorkspace({ projectRoot, worktreeDir, remainingBranches })
143
+
144
+ return { provider: 'zed', supported: true, removed: [] }
145
+ }
@@ -26,7 +26,16 @@ export interface AddIdeWorktreeFoldersOutcome {
26
26
 
27
27
  export interface RemoveIdeWorktreeFoldersOutcome {
28
28
  provider: IdeProvider
29
- /** False when the configured provider has no folder-remove capability (Zed). */
29
+ /**
30
+ * True when the provider can reflect a removal. Both providers now report
31
+ * `true`; `false` is reserved for a future provider with no remove capability.
32
+ */
30
33
  supported: boolean
34
+ /**
35
+ * Folders confirmed removed. For Cursor this is the real diff of the
36
+ * `.code-workspace` `folders` array. For Zed it is ALWAYS `[]`: the only
37
+ * mechanism (`zed --reuse`) replaces the whole window without producing a
38
+ * diff, so it confirms no specific removal even when it refreshed the window.
39
+ */
31
40
  removed: string[]
32
41
  }
@@ -37,7 +37,6 @@ describe('openZedWorkspace', () => {
37
37
  projectRoot: '/repo',
38
38
  worktreeDir: '/repo.worktrees',
39
39
  currentBranches: ['release/v1.0.0', 'release/v1.1.0'],
40
- skipRelaunchWhenEmpty: false,
41
40
  })
42
41
 
43
42
  expect(zx.calls).toHaveLength(1)
@@ -45,25 +44,11 @@ describe('openZedWorkspace', () => {
45
44
  expect(outcome).toEqual({ ran: true, added: 2, removed: 0 })
46
45
  })
47
46
 
48
- it('still opens the bare project root when there are worktrees but relaunch is not skipped', async () => {
47
+ it('skips launching when there are no worktrees', async () => {
49
48
  const outcome = await openZedWorkspace({
50
49
  projectRoot: '/repo',
51
50
  worktreeDir: '/repo.worktrees',
52
51
  currentBranches: [],
53
- skipRelaunchWhenEmpty: false,
54
- })
55
-
56
- expect(zx.calls).toHaveLength(1)
57
- expect(zx.calls[0]?.[0]).toEqual(['/repo'])
58
- expect(outcome).toEqual({ ran: true, added: 0, removed: 0 })
59
- })
60
-
61
- it('skips launching when skipRelaunchWhenEmpty is set and there are no worktrees', async () => {
62
- const outcome = await openZedWorkspace({
63
- projectRoot: '/repo',
64
- worktreeDir: '/repo.worktrees',
65
- currentBranches: [],
66
- skipRelaunchWhenEmpty: true,
67
52
  })
68
53
 
69
54
  expect(zx.calls).toHaveLength(0)
@@ -77,7 +62,6 @@ describe('openZedWorkspace', () => {
77
62
  projectRoot: '/repo',
78
63
  worktreeDir: '/repo.worktrees',
79
64
  currentBranches: ['release/v1.0.0'],
80
- skipRelaunchWhenEmpty: false,
81
65
  })
82
66
 
83
67
  expect(outcome).toEqual({ ran: false, added: 0, removed: 0 })
@@ -0,0 +1,86 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { reuseZedWorkspace } from '../reuse-zed-workspace'
4
+
5
+ const zx = vi.hoisted(() => {
6
+ return { calls: [] as unknown[][], shouldThrow: false }
7
+ })
8
+
9
+ vi.mock('zx', () => {
10
+ return {
11
+ $: vi.fn((_strings: TemplateStringsArray, ...values: unknown[]) => {
12
+ zx.calls.push(values)
13
+
14
+ if (zx.shouldThrow) {
15
+ return Promise.reject(new Error('zed failed to launch'))
16
+ }
17
+
18
+ return Promise.resolve({ stdout: '' })
19
+ }),
20
+ }
21
+ })
22
+
23
+ const log = vi.hoisted(() => {
24
+ return { info: vi.fn(), warn: vi.fn(), error: vi.fn() }
25
+ })
26
+
27
+ vi.mock('src/lib/logger', () => {
28
+ return { logger: log }
29
+ })
30
+
31
+ describe('reuseZedWorkspace', () => {
32
+ beforeEach(() => {
33
+ zx.calls = []
34
+ zx.shouldThrow = false
35
+ vi.clearAllMocks()
36
+ })
37
+
38
+ it('runs `zed --reuse` with the project root plus the remaining worktree paths', async () => {
39
+ const outcome = await reuseZedWorkspace({
40
+ projectRoot: '/repo',
41
+ worktreeDir: '/repo.worktrees',
42
+ remainingBranches: ['release/v1.0.0', 'release/v1.1.0'],
43
+ })
44
+
45
+ expect(zx.calls).toHaveLength(1)
46
+ expect(zx.calls[0]?.[0]).toEqual(['/repo', '/repo.worktrees/release/v1.0.0', '/repo.worktrees/release/v1.1.0'])
47
+ expect(outcome).toEqual({ ran: true })
48
+ })
49
+
50
+ it('collapses to root-only when no worktrees remain', async () => {
51
+ const outcome = await reuseZedWorkspace({
52
+ projectRoot: '/repo',
53
+ worktreeDir: '/repo.worktrees',
54
+ remainingBranches: [],
55
+ })
56
+
57
+ expect(zx.calls).toHaveLength(1)
58
+ expect(zx.calls[0]?.[0]).toEqual(['/repo'])
59
+ expect(outcome).toEqual({ ran: true })
60
+ })
61
+
62
+ it('emits a disclosure log line warning that other open folders are not preserved', async () => {
63
+ await reuseZedWorkspace({
64
+ projectRoot: '/repo',
65
+ worktreeDir: '/repo.worktrees',
66
+ remainingBranches: ['release/v1.0.0'],
67
+ })
68
+
69
+ expect(log.info).toHaveBeenCalledWith(
70
+ expect.stringContaining('other folders in the focused window are not preserved'),
71
+ )
72
+ })
73
+
74
+ it('swallows a launch failure into a best-effort warning', async () => {
75
+ zx.shouldThrow = true
76
+
77
+ const outcome = await reuseZedWorkspace({
78
+ projectRoot: '/repo',
79
+ worktreeDir: '/repo.worktrees',
80
+ remainingBranches: ['release/v1.0.0'],
81
+ })
82
+
83
+ expect(outcome).toEqual({ ran: false })
84
+ expect(log.warn).toHaveBeenCalled()
85
+ })
86
+ })
@@ -1,2 +1,3 @@
1
1
  export { addFoldersToZedWorkspace } from './add-folders-to-zed-workspace'
2
2
  export { openZedWorkspace } from './open-zed-workspace'
3
+ export { reuseZedWorkspace } from './reuse-zed-workspace'
@@ -6,12 +6,6 @@ interface OpenZedWorkspaceArgs {
6
6
  projectRoot: string
7
7
  worktreeDir: string
8
8
  currentBranches: string[]
9
- /**
10
- * When true, skip launching Zed if there are no release worktrees. Lets
11
- * `worktrees-reload` avoid popping a bare Zed window while keeping the
12
- * cold-start `worktrees-open` behaviour (launch unconditionally) intact.
13
- */
14
- skipRelaunchWhenEmpty: boolean
15
9
  }
16
10
 
17
11
  interface OpenZedWorkspaceOutcome {
@@ -23,8 +17,8 @@ interface OpenZedWorkspaceOutcome {
23
17
  /**
24
18
  * Opens a single Zed workspace containing the project root plus every release
25
19
  * worktree, via one `zed <root> <wt...>` invocation (Zed realizes a multi-folder
26
- * workspace from multiple path arguments). Shared by `worktrees-open` (cold-start
27
- * restore) and `worktrees-reload` (relaunch).
20
+ * workspace from multiple path arguments). Used by `worktrees-reload`; skips the
21
+ * launch when there are no worktrees so it never pops a bare Zed window.
28
22
  *
29
23
  * Zed has no portable workspace file and no folder-remove CLI, so there is
30
24
  * nothing to reconcile: `added` is the number of worktrees opened and `removed`
@@ -32,9 +26,9 @@ interface OpenZedWorkspaceOutcome {
32
26
  * best-effort, mirroring the Cursor integration.
33
27
  */
34
28
  export const openZedWorkspace = async (args: OpenZedWorkspaceArgs): Promise<OpenZedWorkspaceOutcome> => {
35
- const { projectRoot, worktreeDir, currentBranches, skipRelaunchWhenEmpty } = args
29
+ const { projectRoot, worktreeDir, currentBranches } = args
36
30
 
37
- if (skipRelaunchWhenEmpty && currentBranches.length === 0) {
31
+ if (currentBranches.length === 0) {
38
32
  return { ran: false, added: 0, removed: 0 }
39
33
  }
40
34
 
@@ -0,0 +1,57 @@
1
+ import { $ } from 'zx'
2
+
3
+ import { logger } from 'src/lib/logger'
4
+
5
+ interface ReuseZedWorkspaceArgs {
6
+ projectRoot: string
7
+ worktreeDir: string
8
+ remainingBranches: string[]
9
+ }
10
+
11
+ interface ReuseZedWorkspaceOutcome {
12
+ ran: boolean
13
+ }
14
+
15
+ /**
16
+ * Reflects a worktree removal in the open Zed window by running
17
+ * `zed --reuse <root> <remaining release worktrees...>`.
18
+ *
19
+ * IMPORTANT — this is destructive: `--reuse` REPLACES the focused window's
20
+ * ENTIRE folder set with exactly these paths. The remaining set is built from
21
+ * release worktrees only (`getCurrentWorktrees('release')`), so any other folder
22
+ * open in that window — feature worktrees, scratch dirs, user-added dirs — is
23
+ * DROPPED and is NOT recoverable by infra-kit (Zed offers no undo and we keep no
24
+ * record of the prior set). The caller MUST therefore only invoke this on an
25
+ * interactive path where a human is present and can re-add folders via
26
+ * `zed --add`. We emit a disclosure log line saying as much.
27
+ *
28
+ * Distinct from `openZedWorkspace`, which runs `zed <paths>` WITHOUT `--reuse`
29
+ * to realize a multi-folder workspace; that does not replace a focused window.
30
+ *
31
+ * Best-effort: any launch failure is swallowed into a warning, mirroring
32
+ * `openZedWorkspace`. Root is always included so the path list is never empty.
33
+ */
34
+ export const reuseZedWorkspace = async (args: ReuseZedWorkspaceArgs): Promise<ReuseZedWorkspaceOutcome> => {
35
+ const { projectRoot, worktreeDir, remainingBranches } = args
36
+
37
+ const paths = [
38
+ projectRoot,
39
+ ...remainingBranches.map((branch) => {
40
+ return `${worktreeDir}/${branch}`
41
+ }),
42
+ ]
43
+
44
+ try {
45
+ await $`zed --reuse ${paths}`
46
+
47
+ logger.info(
48
+ `↻ Refreshed Zed workspace to root + ${remainingBranches.length} release worktree(s) (other folders in the focused window are not preserved)`,
49
+ )
50
+
51
+ return { ran: true }
52
+ } catch (error) {
53
+ logger.warn({ error }, '⚠️ Failed to refresh Zed workspace')
54
+
55
+ return { ran: false }
56
+ }
57
+ }