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
@@ -1,5 +1,11 @@
1
1
  // Public library entry for `import { defineConfig } from 'infra-kit'`. Uses
2
2
  // relative imports (no `src/*` alias) so the emitted .d.ts stays portable for
3
- // external consumers. Keep this surface minimal — only the package-config API.
3
+ // external consumers. Keep this surface minimal.
4
4
  export { defineConfig } from '../lib/package-config/package-config'
5
5
  export type { InfraKitPackageConfig, InfraKitPackageConfigInput } from '../lib/package-config/package-config'
6
+ // Vendor sync config authoring (`import { defineVendorConfig } from 'infra-kit'`).
7
+ // Imported from the node-free schema module so the emitted .d.ts needs no node types.
8
+ export { defineVendorConfig } from '../lib/vendor/config-schema'
9
+ export type { VendorConfig, VendorCopyItem } from '../lib/vendor/config-schema'
10
+ // The machine-local factory registry is now static JSON (~/.infra-kit/vendor.json),
11
+ // not authored in TypeScript — so there is no `defineFactoryConfig` to export.
@@ -0,0 +1,127 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { $ } from 'zx'
3
+
4
+ import { getInfraKitConfig } from 'src/lib/infra-kit-config'
5
+ import type { CmuxLayout } from 'src/lib/infra-kit-config'
6
+
7
+ import { openCmuxWorkspaceWithLayout } from '../open-workspace-with-layout'
8
+
9
+ vi.mock('src/lib/infra-kit-config', async (importOriginal) => {
10
+ const actual = await importOriginal<typeof import('src/lib/infra-kit-config')>()
11
+
12
+ return {
13
+ ...actual,
14
+ getInfraKitConfig: vi.fn(),
15
+ }
16
+ })
17
+
18
+ vi.mock('zx', () => {
19
+ function makeResult(stdout: string) {
20
+ return Object.assign(Promise.resolve({ stdout }), {
21
+ quiet: () => {
22
+ return Promise.resolve({ stdout })
23
+ },
24
+ })
25
+ }
26
+
27
+ return {
28
+ $: vi.fn((strings: TemplateStringsArray) => {
29
+ const cmd = strings.join('')
30
+
31
+ if (cmd.includes('workspace create')) return makeResult('created workspace:7\n')
32
+ if (cmd.includes('list-pane-surfaces')) return makeResult('surface:12 (active)\n')
33
+
34
+ return makeResult('')
35
+ }),
36
+ }
37
+ })
38
+
39
+ // A worktree cwd that is not under a publicly writable directory (avoids the
40
+ // sonarjs/publicly-writable-directories lint that flags /tmp paths).
41
+ const WORKTREE_CWD = '/home/dev/repo/wt'
42
+
43
+ type CmuxCall = [TemplateStringsArray, ...string[]]
44
+
45
+ const allCommands = (): string[] => {
46
+ const calls = vi.mocked($).mock.calls as unknown as CmuxCall[]
47
+
48
+ return calls.map((call) => {
49
+ return call[0].join('')
50
+ })
51
+ }
52
+
53
+ const splitCommands = (): string[] => {
54
+ return allCommands().filter((cmd) => {
55
+ return cmd.includes('new-split')
56
+ })
57
+ }
58
+
59
+ const mockLayout = (layout: CmuxLayout): void => {
60
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
61
+ environments: ['dev'],
62
+ envManagement: { provider: 'doppler', config: { name: 'p' } },
63
+ worktrees: { cmux: { layout } },
64
+ } as Awaited<ReturnType<typeof getInfraKitConfig>>)
65
+ }
66
+
67
+ describe('openCmuxWorkspaceWithLayout', () => {
68
+ beforeEach(() => {
69
+ vi.clearAllMocks()
70
+ })
71
+
72
+ it('two-columns layout issues a single right split (no down split)', async () => {
73
+ mockLayout('two-columns')
74
+
75
+ await openCmuxWorkspaceWithLayout({ cwd: WORKTREE_CWD })
76
+
77
+ const splits = splitCommands()
78
+
79
+ expect(
80
+ splits.some((cmd) => {
81
+ return cmd.includes('new-split right')
82
+ }),
83
+ ).toBe(true)
84
+ expect(
85
+ splits.some((cmd) => {
86
+ return cmd.includes('new-split down')
87
+ }),
88
+ ).toBe(false)
89
+ expect(splits).toHaveLength(1)
90
+ })
91
+
92
+ it('three-pane layout issues both a right and a down split', async () => {
93
+ mockLayout('three-pane')
94
+
95
+ await openCmuxWorkspaceWithLayout({ cwd: WORKTREE_CWD })
96
+
97
+ const splits = splitCommands()
98
+
99
+ expect(
100
+ splits.some((cmd) => {
101
+ return cmd.includes('new-split right')
102
+ }),
103
+ ).toBe(true)
104
+ expect(
105
+ splits.some((cmd) => {
106
+ return cmd.includes('new-split down')
107
+ }),
108
+ ).toBe(true)
109
+ expect(splits).toHaveLength(2)
110
+ })
111
+
112
+ it('renames the workspace when a title is provided', async () => {
113
+ mockLayout('two-columns')
114
+
115
+ await openCmuxWorkspaceWithLayout({ cwd: WORKTREE_CWD, title: 'my-title' })
116
+
117
+ const calls = vi.mocked($).mock.calls as unknown as CmuxCall[]
118
+ const renameCall = calls.find((call) => {
119
+ return call[0].join('').includes('workspace rename')
120
+ })
121
+
122
+ expect(renameCall).toBeDefined()
123
+ // The title is the last interpolated value of `cmux workspace rename … --title ${title}`,
124
+ // so it lands in the final tuple slot — assert on that value, not tuple membership.
125
+ expect(renameCall?.at(-1)).toBe('my-title')
126
+ })
127
+ })
@@ -30,7 +30,7 @@ export const closeCmuxWorkspaceByTitle = async (title: string): Promise<void> =>
30
30
  * title matches `title`, or undefined if no match. Both sides are compared via
31
31
  * {@link canonicalizeCmuxTitle} so a workspace stored under a drifted title
32
32
  * (whitespace, or an older CLI's `v`-prefixed semver) is still found — keeping
33
- * close symmetric with the dedup in `worktrees-open`.
33
+ * close symmetric with the cmux open dedup in `worktrees-reload`.
34
34
  *
35
35
  * Each line looks like:
36
36
  * " workspace:8 hulyo-monorepo 1.48.0"
@@ -40,7 +40,7 @@ const findWorkspaceRefByTitle = (output: string, title: string): string | undefi
40
40
  const target = canonicalizeCmuxTitle(title)
41
41
 
42
42
  for (const rawLine of output.split('\n')) {
43
- // eslint-disable-next-line sonarjs/slow-regex, regexp/no-super-linear-backtracking
43
+ // eslint-disable-next-line sonarjs/super-linear-regex, regexp/no-super-linear-backtracking
44
44
  const match = rawLine.match(/^[* ]\s*(workspace:\d+)\s+(.+?)(?:\s+\[selected\])?\s*$/)
45
45
 
46
46
  if (!match) {
@@ -24,7 +24,7 @@ export const listCmuxWorkspaceTitles = async (): Promise<Set<string>> => {
24
24
  const titles = new Set<string>()
25
25
 
26
26
  for (const rawLine of output.split('\n')) {
27
- // eslint-disable-next-line sonarjs/slow-regex, regexp/no-super-linear-backtracking
27
+ // eslint-disable-next-line sonarjs/super-linear-regex, regexp/no-super-linear-backtracking
28
28
  const match = rawLine.match(/^[* ]\s*workspace:\d+\s+(.+?)(?:\s+\[selected\])?\s*$/)
29
29
 
30
30
  if (!match) {
@@ -1,19 +1,25 @@
1
1
  import { $ } from 'zx'
2
2
 
3
+ import { getInfraKitConfig, resolveCmuxLayout } from 'src/lib/infra-kit-config'
4
+
3
5
  interface OpenCmuxWorkspaceArgs {
4
6
  cwd: string
5
7
  title?: string
6
8
  }
7
9
 
8
10
  /**
9
- * Opens a new cmux workspace rooted at `cwd` with three panes:
10
- * left-top (primary) | right (full-height)
11
- * left-bottom |
11
+ * Opens a new cmux workspace rooted at `cwd`, with panes arranged per the
12
+ * configured `worktrees.cmux.layout` (resolved via {@link resolveCmuxLayout},
13
+ * default `two-columns`):
14
+ * two-columns — left | right, both full-height (two panes)
15
+ * three-pane — left-top / left-bottom | full-height right (three panes)
12
16
  * All panes inherit `cwd` from the workspace.
13
17
  */
14
18
  export const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs): Promise<void> => {
15
19
  const { cwd, title } = args
16
20
 
21
+ const layout = resolveCmuxLayout(await getInfraKitConfig())
22
+
17
23
  const newWorkspaceOutput = (await $`cmux workspace create --cwd ${cwd}`).stdout
18
24
 
19
25
  const workspaceRef = parseWorkspaceRef(newWorkspaceOutput)
@@ -22,8 +28,13 @@ export const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs):
22
28
 
23
29
  const leftTopRef = parseFirstSurfaceRef(surfacesOutput)
24
30
 
31
+ // Both layouts share the vertical split into left | right columns; only the
32
+ // legacy three-pane layout additionally splits the left column top/bottom.
25
33
  await $`cmux new-split right --workspace ${workspaceRef} --surface ${leftTopRef}`
26
- await $`cmux new-split down --workspace ${workspaceRef} --surface ${leftTopRef}`
34
+
35
+ if (layout === 'three-pane') {
36
+ await $`cmux new-split down --workspace ${workspaceRef} --surface ${leftTopRef}`
37
+ }
27
38
 
28
39
  if (title) {
29
40
  await $`cmux workspace rename --workspace ${workspaceRef} --title ${title}`
@@ -8,13 +8,6 @@ interface OpenCursorWorkspaceArgs {
8
8
  projectRoot: string
9
9
  worktreeDir: string
10
10
  currentBranches: string[]
11
- /**
12
- * When true, skip the `cursor <workspace>` relaunch if there are no release
13
- * worktrees (folder reconcile still runs to strip dangling entries). Lets
14
- * `worktrees-reload` avoid popping an empty Cursor window while keeping the
15
- * cold-start `worktrees-open` behaviour (relaunch unconditionally) intact.
16
- */
17
- skipRelaunchWhenEmpty: boolean
18
11
  /**
19
12
  * The Cursor entry's config, selected by the facade from the (possibly
20
13
  * multi-IDE) infra-kit config. Passed in rather than re-read here so this
@@ -32,14 +25,16 @@ interface OpenCursorWorkspaceOutcome {
32
25
 
33
26
  /**
34
27
  * Reconciles the configured Cursor `.code-workspace` `folders` array against the
35
- * set of release worktrees on disk, then launches Cursor against it. Shared by
36
- * `worktrees-open` (cold-start restore) and `worktrees-reload` (close + reopen).
28
+ * set of release worktrees on disk, then launches Cursor against it (the folder
29
+ * reconcile still runs when there are no worktrees, but the relaunch is skipped
30
+ * so `worktrees-reload` never pops an empty Cursor window). Used by
31
+ * `worktrees-reload`.
37
32
  *
38
33
  * No-ops (returns `ran: false`) when the Cursor entry has no `workspaceConfigPath`.
39
34
  * All failures are swallowed into a warning — opening Cursor is best-effort.
40
35
  */
41
36
  export const openCursorWorkspace = async (args: OpenCursorWorkspaceArgs): Promise<OpenCursorWorkspaceOutcome> => {
42
- const { projectRoot, worktreeDir, currentBranches, skipRelaunchWhenEmpty, cursorConfig } = args
37
+ const { projectRoot, worktreeDir, currentBranches, cursorConfig } = args
43
38
 
44
39
  if (!cursorConfig.workspaceConfigPath) {
45
40
  logger.warn('⚠️ Skipping Cursor: workspaceConfigPath is not set.')
@@ -56,7 +51,7 @@ export const openCursorWorkspace = async (args: OpenCursorWorkspaceArgs): Promis
56
51
  currentBranches,
57
52
  })
58
53
 
59
- if (!(skipRelaunchWhenEmpty && currentBranches.length === 0)) {
54
+ if (currentBranches.length > 0) {
60
55
  await launchCursor(workspacePath)
61
56
  }
62
57
 
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { buildDopplerNotFoundMessage, classifyDopplerDownloadError } from '../doppler-errors'
4
+
5
+ // Verbatim Doppler stderr observed on a not-found download (doppler v3.76.0,
6
+ // 2026-06-27). The "" prefix is the real ANSI color on "Doppler Error:".
7
+ const PROJECT_NOT_FOUND_STDERR =
8
+ "Unable to download secrets\nDoppler Error: Could not find requested project 'infra-kit'\n"
9
+ const CONFIG_NOT_FOUND_STDERR =
10
+ "Unable to download secrets\nDoppler Error: Could not find requested config 'bogus-cfg'\n"
11
+
12
+ describe('classifyDopplerDownloadError', () => {
13
+ it('classifies a project-not-found stderr as "project"', () => {
14
+ expect(classifyDopplerDownloadError(PROJECT_NOT_FOUND_STDERR)).toBe('project')
15
+ })
16
+
17
+ it('classifies a config-not-found stderr as "config"', () => {
18
+ expect(classifyDopplerDownloadError(CONFIG_NOT_FOUND_STDERR)).toBe('config')
19
+ })
20
+
21
+ it('is color-safe — the markers are matched despite ANSI codes', () => {
22
+ expect(classifyDopplerDownloadError('Doppler Error: Could not find requested project')).toBe('project')
23
+ })
24
+
25
+ it('returns "unknown" for unrelated failures (auth, network, timeout)', () => {
26
+ expect(classifyDopplerDownloadError('Doppler Error: you must be logged in')).toBe('unknown')
27
+ expect(classifyDopplerDownloadError('connect ETIMEDOUT')).toBe('unknown')
28
+ expect(classifyDopplerDownloadError('')).toBe('unknown')
29
+ })
30
+ })
31
+
32
+ describe('buildDopplerNotFoundMessage', () => {
33
+ it('names the missing project and points at envManagement.config.name', () => {
34
+ const msg = buildDopplerNotFoundMessage({ kind: 'project', project: 'infra-kit', config: 'dev', available: null })
35
+
36
+ expect(msg).toContain('Doppler project "infra-kit" not found')
37
+ expect(msg).toContain('envManagement.config.name')
38
+ expect(msg).toContain('Fix:')
39
+ })
40
+
41
+ it('lists available projects when the enrichment list is non-empty', () => {
42
+ const msg = buildDopplerNotFoundMessage({
43
+ kind: 'project',
44
+ project: 'infra-kit',
45
+ config: 'dev',
46
+ available: ['example-project', 'nomadream'],
47
+ })
48
+
49
+ expect(msg).toContain('Available projects: example-project, nomadream.')
50
+ })
51
+
52
+ it('omits the available line when the list is empty (told: none exist)', () => {
53
+ const msg = buildDopplerNotFoundMessage({ kind: 'project', project: 'infra-kit', config: 'dev', available: [] })
54
+
55
+ expect(msg).not.toContain('Available projects')
56
+ })
57
+
58
+ it('omits the available line when the list is null (lookup failed)', () => {
59
+ const msg = buildDopplerNotFoundMessage({ kind: 'project', project: 'infra-kit', config: 'dev', available: null })
60
+
61
+ expect(msg).not.toContain('Available projects')
62
+ })
63
+
64
+ it('names the missing config, its project, and points at environments', () => {
65
+ const msg = buildDopplerNotFoundMessage({
66
+ kind: 'config',
67
+ project: 'nomadream',
68
+ config: 'staging',
69
+ available: ['dev', 'arthur'],
70
+ })
71
+
72
+ expect(msg).toContain('Doppler config "staging" not found in project "nomadream"')
73
+ expect(msg).toContain('infra-kit.json → environments')
74
+ expect(msg).toContain('Available configs in "nomadream": dev, arthur.')
75
+ })
76
+ })
@@ -1,14 +1,22 @@
1
1
  import { $ } from 'zx'
2
2
 
3
+ /**
4
+ * Hard upper bound for the Doppler auth probes. `doppler me` is a network call;
5
+ * without a timeout a hung VPN/connection would stall every caller indefinitely
6
+ * (notably the env auto-load preAction hook, which runs before each CLI command).
7
+ */
8
+ const DOPPLER_AUTH_TIMEOUT_MS = 5_000
9
+
3
10
  /**
4
11
  * Validate Doppler CLI installation and authentication status. Throws on failure
5
12
  * so callers (CLI entry, MCP tool handler) can translate to the right surface —
6
13
  * CLI exits non-zero; MCP returns a structured tool error instead of tearing
7
- * down the server.
14
+ * down the server. Both probes are time-bounded so a hung Doppler/network never
15
+ * blocks the caller (a timeout surfaces as the same "not authenticated" path).
8
16
  */
9
17
  export const validateDopplerCliAndAuth = async (): Promise<void> => {
10
18
  try {
11
- await $`doppler --version`
19
+ await $`doppler --version`.timeout(DOPPLER_AUTH_TIMEOUT_MS)
12
20
  } catch (error: unknown) {
13
21
  throw new Error('Doppler CLI is not installed. Install it from: https://docs.doppler.com/docs/install-cli', {
14
22
  cause: error,
@@ -16,8 +24,8 @@ export const validateDopplerCliAndAuth = async (): Promise<void> => {
16
24
  }
17
25
 
18
26
  try {
19
- await $`doppler me`
27
+ await $`doppler me`.timeout(DOPPLER_AUTH_TIMEOUT_MS)
20
28
  } catch (error: unknown) {
21
- throw new Error('Doppler CLI is not authenticated. Run: doppler login', { cause: error })
29
+ throw new Error('Doppler CLI is not authenticated (or timed out). Run: doppler login', { cause: error })
22
30
  }
23
31
  }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Translate Doppler `secrets download` not-found failures into actionable,
3
+ * point-at-the-fix guidance. Pure (no I/O) so the classification + message policy
4
+ * is unit-testable without spawning the Doppler CLI — mirroring the pure-helper
5
+ * pattern in `commands/env-load/env-load.ts`.
6
+ *
7
+ * Observed Doppler output on a not-found download (verified 2026-06-27, doppler
8
+ * v3.76.0), exit 1, written to STDERR (stdout empty):
9
+ * Unable to download secrets
10
+ * Doppler Error: Could not find requested project 'name'
11
+ * Doppler Error: Could not find requested config 'name'
12
+ * Only the "Doppler Error:" prefix is ANSI-colored; the "Could not find requested
13
+ * ..." text is plain, so substring matching is color-safe. zx's
14
+ * `ProcessOutput.message` also concatenates stdout+stderr, so the same markers are
15
+ * matchable off `.message` when `.stderr` is unavailable.
16
+ */
17
+
18
+ /** Which kind of Doppler not-found a download failure represents (`unknown` = not a not-found error). */
19
+ export type DopplerNotFoundKind = 'project' | 'config' | 'unknown'
20
+
21
+ const PROJECT_NOT_FOUND_MARKER = 'Could not find requested project'
22
+ const CONFIG_NOT_FOUND_MARKER = 'Could not find requested config'
23
+
24
+ /**
25
+ * Classify a Doppler download failure from its stderr/message text. Returns
26
+ * `'unknown'` for anything that isn't a recognized not-found error so the caller
27
+ * rethrows the original untouched (degrades to status quo, never worse).
28
+ *
29
+ * @example
30
+ * classifyDopplerDownloadError("Doppler Error: Could not find requested project 'x'") // => 'project'
31
+ * classifyDopplerDownloadError("Doppler Error: Could not find requested config 'dev'") // => 'config'
32
+ * classifyDopplerDownloadError('network unreachable') // => 'unknown'
33
+ */
34
+ export const classifyDopplerDownloadError = (stderr: string): DopplerNotFoundKind => {
35
+ if (stderr.includes(PROJECT_NOT_FOUND_MARKER)) return 'project'
36
+
37
+ if (stderr.includes(CONFIG_NOT_FOUND_MARKER)) return 'config'
38
+
39
+ return 'unknown'
40
+ }
41
+
42
+ interface DopplerNotFoundMessageArgs {
43
+ /** Whether the missing entity is the project or the config within it. */
44
+ kind: 'project' | 'config'
45
+ project: string
46
+ config: string
47
+ /**
48
+ * Names to suggest. `null` = lookup failed/unavailable, `[]` = none exist; both
49
+ * omit the suggestion line so a failed enrichment probe is never misreported as
50
+ * a definitive "none available".
51
+ */
52
+ available?: string[] | null
53
+ }
54
+
55
+ /**
56
+ * Build the actionable, multi-line error body for a Doppler not-found failure:
57
+ * states what's missing, points at the exact infra-kit.json field, lists available
58
+ * names when known, and gives a concrete fix.
59
+ *
60
+ * @example
61
+ * buildDopplerNotFoundMessage({ kind: 'project', project: 'infra-kit', config: 'dev', available: ['nomadream'] })
62
+ * // => 'Doppler project "infra-kit" not found (set in infra-kit.json → envManagement.config.name).\nAvailable projects: nomadream.\nFix: ...'
63
+ */
64
+ export const buildDopplerNotFoundMessage = ({
65
+ kind,
66
+ project,
67
+ config,
68
+ available,
69
+ }: DopplerNotFoundMessageArgs): string => {
70
+ const lines: string[] =
71
+ kind === 'project'
72
+ ? [`Doppler project "${project}" not found (set in infra-kit.json → envManagement.config.name).`]
73
+ : [`Doppler config "${config}" not found in project "${project}" (set in infra-kit.json → environments).`]
74
+
75
+ if (available && available.length > 0) {
76
+ const label = kind === 'project' ? 'Available projects' : `Available configs in "${project}"`
77
+
78
+ lines.push(`${label}: ${available.join(', ')}.`)
79
+ }
80
+
81
+ lines.push(
82
+ kind === 'project'
83
+ ? 'Fix: update envManagement.config.name to an existing project, or create it in Doppler.'
84
+ : 'Fix: update environments to an existing config, or create it in Doppler.',
85
+ )
86
+
87
+ return lines.join('\n')
88
+ }
@@ -1,5 +1,15 @@
1
+ import { $ } from 'zx'
2
+ import type { ProcessPromise } from 'zx'
3
+
1
4
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
2
5
 
6
+ /**
7
+ * Hard upper bound for the Doppler list probes. These run only on error/diagnostic
8
+ * paths (download-failure enrichment, `doctor`), but a hung VPN/connection must not
9
+ * stall them — mirrors the auth-probe budget in `doppler-cli-auth.ts`.
10
+ */
11
+ const DOPPLER_LIST_TIMEOUT_MS = 5_000
12
+
3
13
  /**
4
14
  * Resolve Doppler project name from infra-kit.json at the project root
5
15
  */
@@ -8,3 +18,73 @@ export const getDopplerProject = async (): Promise<string> => {
8
18
 
9
19
  return envManagement.config.name
10
20
  }
21
+
22
+ /**
23
+ * List the Doppler project names for the authenticated account, or `null` when the
24
+ * lookup itself fails (CLI missing, unauthenticated, network/timeout, malformed
25
+ * JSON). NEVER throws. Callers MUST distinguish `null` ("couldn't tell") from `[]`
26
+ * ("told: none exist") so an enrichment or diagnostic message never misreports a
27
+ * failed probe as "no projects available".
28
+ *
29
+ * @example
30
+ * await listDopplerProjects() // => ['example-project', 'nomadream'] (or null on failure)
31
+ */
32
+ export const listDopplerProjects = async (): Promise<string[] | null> => {
33
+ return runDopplerNameList(() => {
34
+ return $`doppler projects --json`
35
+ })
36
+ }
37
+
38
+ /**
39
+ * List the Doppler config names for a project, or `null` when the lookup itself
40
+ * fails (see {@link listDopplerProjects} for the `null` vs `[]` contract).
41
+ *
42
+ * @example
43
+ * await listDopplerConfigs('nomadream') // => ['dev', 'oleg', 'prod', 'arthur'] (or null on failure)
44
+ */
45
+ export const listDopplerConfigs = async (project: string): Promise<string[] | null> => {
46
+ return runDopplerNameList(() => {
47
+ return $`doppler configs --project ${project} --json`
48
+ })
49
+ }
50
+
51
+ /**
52
+ * Run a `doppler ... --json` listing quietly + time-bounded and extract the `name`
53
+ * field of each entry. Returns `null` on any failure (subprocess error, timeout,
54
+ * non-array/malformed JSON) so the caller can tell a failed probe from an empty set.
55
+ */
56
+ const runDopplerNameList = async (run: () => ProcessPromise): Promise<string[] | null> => {
57
+ const prevQuiet = $.quiet
58
+
59
+ $.quiet = true
60
+ try {
61
+ const result = await run().timeout(DOPPLER_LIST_TIMEOUT_MS)
62
+
63
+ return parseDopplerNames(result.stdout)
64
+ } catch {
65
+ return null
66
+ } finally {
67
+ $.quiet = prevQuiet
68
+ }
69
+ }
70
+
71
+ /** Parse a Doppler `--json` array into its `name` strings, or `null` if the shape is unexpected. */
72
+ const parseDopplerNames = (stdout: string): string[] | null => {
73
+ let parsed: unknown
74
+
75
+ try {
76
+ parsed = JSON.parse(stdout)
77
+ } catch {
78
+ return null
79
+ }
80
+
81
+ if (!Array.isArray(parsed)) return null
82
+
83
+ return parsed
84
+ .map((entry) => {
85
+ return entry !== null && typeof entry === 'object' ? (entry as { name?: unknown }).name : undefined
86
+ })
87
+ .filter((name): name is string => {
88
+ return typeof name === 'string'
89
+ })
90
+ }
@@ -1,2 +1,4 @@
1
1
  export { validateDopplerCliAndAuth } from './doppler-cli-auth'
2
- export { getDopplerProject } from './doppler-project'
2
+ export { buildDopplerNotFoundMessage, classifyDopplerDownloadError } from './doppler-errors'
3
+ export type { DopplerNotFoundKind } from './doppler-errors'
4
+ export { getDopplerProject, listDopplerConfigs, listDopplerProjects } from './doppler-project'
@@ -1,6 +1,6 @@
1
- import process from 'node:process'
2
1
  import { $ } from 'zx'
3
2
 
3
+ import { OperationError } from 'src/lib/errors/operation-error'
4
4
  import { logger } from 'src/lib/logger'
5
5
 
6
6
  /**
@@ -11,15 +11,19 @@ export const validateGitHubCliAndAuth = async () => {
11
11
  await $`gh --version`
12
12
  } catch (error: unknown) {
13
13
  logger.error({ error }, 'Error: GitHub CLI (gh) is not installed.')
14
- logger.error('Please install it from: https://cli.github.com/')
15
- process.exit(1)
14
+ throw new OperationError(error, {
15
+ operation: 'verify GitHub CLI is installed',
16
+ remediation: 'install gh from https://cli.github.com/',
17
+ })
16
18
  }
17
19
 
18
20
  try {
19
21
  await $`gh auth status`
20
22
  } catch (error: unknown) {
21
23
  logger.error({ error }, 'Error: GitHub CLI (gh) is not authenticated.')
22
- logger.error('Please authenticate it from: https://cli.github.com/manual/gh_auth_login or type "gh auth login"')
23
- process.exit(1)
24
+ throw new OperationError(error, {
25
+ operation: 'verify GitHub CLI authentication',
26
+ remediation: 'run "gh auth login" (https://cli.github.com/manual/gh_auth_login)',
27
+ })
24
28
  }
25
29
  }
@@ -1,6 +1,6 @@
1
- import process from 'node:process'
2
1
  import { $ } from 'zx'
3
2
 
3
+ import { OperationError } from 'src/lib/errors/operation-error'
4
4
  import { logger } from 'src/lib/logger'
5
5
  import { compareReleaseIds, formatBranchName, formatPrTitle, parseBranchName } from 'src/lib/release-id'
6
6
  import type { ReleaseId } from 'src/lib/release-id'
@@ -84,18 +84,21 @@ export const getReleasePRs = async (): Promise<string[]> => {
84
84
  const prs = await fetchAllReleasePRs()
85
85
 
86
86
  if (prs.length === 0) {
87
- logger.error('❌ No release PRs found. Check the project folder for the script. Exiting...')
88
-
89
- process.exit(1)
87
+ throw new OperationError(undefined, {
88
+ operation: 'find open release PRs',
89
+ remediation: 'open a release PR first, or check you are in the right repo',
90
+ })
90
91
  }
91
92
 
92
93
  return sortReleasePRs(prs).map((pr) => {
93
94
  return pr.headRefName
94
95
  })
95
96
  } catch (error) {
97
+ if (error instanceof OperationError) throw error
98
+
96
99
  logger.error({ error }, '❌ Error fetching release PRs')
97
100
 
98
- process.exit(1)
101
+ throw new OperationError(error, { operation: 'fetch release PRs' })
99
102
  }
100
103
  }
101
104
 
@@ -110,8 +113,10 @@ export const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {
110
113
  const prs = await fetchAllReleasePRs()
111
114
 
112
115
  if (prs.length === 0) {
113
- logger.error('❌ No release PRs found. Check the project folder for the script. Exiting...')
114
- process.exit(1)
116
+ throw new OperationError(undefined, {
117
+ operation: 'find open release PRs',
118
+ remediation: 'open a release PR first, or check you are in the right repo',
119
+ })
115
120
  }
116
121
 
117
122
  return sortReleasePRs(prs).map((pr) => {
@@ -122,8 +127,11 @@ export const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {
122
127
  }
123
128
  })
124
129
  } catch (error) {
130
+ if (error instanceof OperationError) throw error
131
+
125
132
  logger.error({ error }, '❌ Error fetching release PRs')
126
- process.exit(1)
133
+
134
+ throw new OperationError(error, { operation: 'fetch release PRs' })
127
135
  }
128
136
  }
129
137