infra-kit 0.1.114 → 0.1.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-1fcfe0ec-7567-41c3-9bfd-501e99e86819.jsonl +19 -0
  3. package/.omc/state/agent-replay-38382db5-3088-4b5a-aff7-2561a60827c7.jsonl +17 -0
  4. package/.omc/state/agent-replay-50c36be9-a3a2-42b9-9c85-5a06d3e22f6a.jsonl +5 -0
  5. package/.omc/state/agent-replay-8198b3e5-fad6-4ee0-9c59-ef773ad89af4.jsonl +1 -0
  6. package/.omc/state/agent-replay-8cd7652a-b8b5-49cf-9b1f-bee8e5972458.jsonl +3 -0
  7. package/.omc/state/agent-replay-9bea518f-a9ee-4d14-bb53-a936ebaf647c.jsonl +7 -0
  8. package/.omc/state/agent-replay-b52d89fb-0e24-4901-bc66-27e3c1697126.jsonl +2 -0
  9. package/.omc/state/agent-replay-f581a1ae-0ee4-48a3-96f3-ce31a0633bb0.jsonl +1 -0
  10. package/.omc/state/agent-replay-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
  11. package/.omc/state/idle-notif-cooldown.json +1 -1
  12. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/last-tool-error-state.json +7 -0
  13. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/mission-state.json +109 -0
  14. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/pre-tool-advisory-throttle.json +42 -0
  15. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ralph-state.json +13 -0
  16. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/skill-active-state.json +15 -0
  17. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/subagent-tracking-state.json +62 -0
  18. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ultrawork-state.json +11 -0
  19. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/last-tool-error-state.json +7 -0
  20. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/pre-tool-advisory-throttle.json +18 -0
  21. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/last-tool-error-state.json +7 -0
  22. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/mission-state.json +145 -0
  23. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/pre-tool-advisory-throttle.json +38 -0
  24. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/ralplan-state.json +11 -0
  25. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/subagent-tracking-state.json +98 -0
  26. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  27. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  28. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/mission-state.json +61 -0
  29. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/pre-tool-advisory-throttle.json +22 -0
  30. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/skill-active-state.json +15 -0
  31. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/subagent-tracking-state.json +26 -0
  32. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/pre-tool-advisory-throttle.json +14 -0
  33. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/subagent-tracking-state.json +17 -0
  34. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/pre-tool-advisory-throttle.json +18 -0
  35. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/ralplan-state.json +11 -0
  36. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/subagent-tracking-state.json +35 -0
  37. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/last-tool-error-state.json +7 -0
  38. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/mission-state.json +79 -0
  39. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/pre-tool-advisory-throttle.json +22 -0
  40. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/progress.txt +7 -0
  41. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/skill-active-state.json +15 -0
  42. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/subagent-tracking-state.json +26 -0
  43. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/pre-tool-advisory-throttle.json +14 -0
  44. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/subagent-tracking-state.json +17 -0
  45. package/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +18 -0
  46. package/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  47. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/pre-tool-advisory-throttle.json +18 -0
  48. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/subagent-tracking-state.json +17 -0
  49. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/last-tool-error-state.json +7 -0
  50. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/mission-state.json +61 -0
  51. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/prd.json +22 -0
  52. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/pre-tool-advisory-throttle.json +22 -0
  53. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/progress.txt +22 -0
  54. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/skill-active-state.json +15 -0
  55. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/subagent-tracking-state.json +26 -0
  56. package/.turbo/turbo-build.log +3 -3
  57. package/.turbo/turbo-test.log +92 -44
  58. package/dist/boot-K4QUAFSP.js +2 -0
  59. package/dist/boot-K4QUAFSP.js.map +7 -0
  60. package/dist/chunk-RTSP2FCA.js +2 -0
  61. package/dist/chunk-RTSP2FCA.js.map +7 -0
  62. package/dist/chunk-VPBZR2PY.js +166 -0
  63. package/dist/chunk-VPBZR2PY.js.map +7 -0
  64. package/dist/cli.js +11 -107
  65. package/dist/cli.js.map +4 -4
  66. package/dist/entry/index.d.ts +2 -0
  67. package/dist/index.js +1 -1
  68. package/dist/index.js.map +4 -4
  69. package/dist/lib/vendor/config-schema.d.ts +58 -0
  70. package/dist/mcp.js +1 -54
  71. package/dist/mcp.js.map +4 -4
  72. package/eslint.config.js +33 -1
  73. package/package.json +5 -1
  74. package/scripts/build.js +11 -1
  75. package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
  76. package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
  77. package/src/.omc/state/idle-notif-cooldown.json +1 -1
  78. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
  79. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
  80. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
  81. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  82. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
  83. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
  84. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  85. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  86. package/src/commands/config/config.ts +7 -37
  87. package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
  88. package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
  89. package/src/commands/doctor/doctor.ts +104 -44
  90. package/src/commands/env-autoload/env-autoload.ts +12 -0
  91. package/src/commands/env-autoload/index.ts +1 -0
  92. package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
  93. package/src/commands/env-clear/env-clear.ts +22 -8
  94. package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
  95. package/src/commands/env-load/env-load.ts +152 -35
  96. package/src/commands/env-load/index.ts +2 -1
  97. package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
  98. package/src/commands/env-status/env-status.ts +19 -7
  99. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
  100. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
  101. package/src/commands/init/__tests__/agent-files.test.ts +145 -27
  102. package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
  103. package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
  104. package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
  105. package/src/commands/init/__tests__/shell-body.test.ts +30 -0
  106. package/src/commands/init/agent-files.ts +65 -45
  107. package/src/commands/init/init.ts +140 -31
  108. package/src/commands/init/migrate-config.ts +160 -32
  109. package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
  110. package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
  111. package/src/commands/vendor-check/index.ts +1 -0
  112. package/src/commands/vendor-check/vendor-check.ts +127 -0
  113. package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
  114. package/src/commands/vendor-config/index.ts +1 -0
  115. package/src/commands/vendor-config/vendor-config.ts +162 -0
  116. package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
  117. package/src/commands/vendor-diff/index.ts +1 -0
  118. package/src/commands/vendor-diff/vendor-diff.ts +77 -0
  119. package/src/commands/vendor-manifest/index.ts +1 -0
  120. package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
  121. package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
  122. package/src/commands/vendor-sync/index.ts +1 -0
  123. package/src/commands/vendor-sync/vendor-sync.ts +90 -0
  124. package/src/commands/worktrees-add/worktrees-add.ts +1 -1
  125. package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
  126. package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
  127. package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
  128. package/src/entry/cli.ts +417 -210
  129. package/src/entry/index.ts +7 -1
  130. package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
  131. package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
  132. package/src/integrations/cmux/list-workspace-titles.ts +1 -1
  133. package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
  134. package/src/integrations/cursor/open-cursor-workspace.ts +6 -11
  135. package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
  136. package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
  137. package/src/integrations/doppler/doppler-errors.ts +88 -0
  138. package/src/integrations/doppler/doppler-project.ts +80 -0
  139. package/src/integrations/doppler/index.ts +3 -1
  140. package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
  141. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
  142. package/src/integrations/ide/__tests__/ide-facade.test.ts +49 -10
  143. package/src/integrations/ide/open-ide-workspace.ts +6 -6
  144. package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
  145. package/src/integrations/ide/types.ts +10 -1
  146. package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
  147. package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
  148. package/src/integrations/zed/index.ts +1 -0
  149. package/src/integrations/zed/open-zed-workspace.ts +4 -10
  150. package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
  151. package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
  152. package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
  153. package/src/lib/command-catalog/command-catalog.ts +141 -0
  154. package/src/lib/command-catalog/index.ts +2 -0
  155. package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
  156. package/src/lib/constants/constants.ts +13 -0
  157. package/src/lib/constants/index.ts +2 -0
  158. package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
  159. package/src/lib/env-autoload/env-autoload.ts +294 -0
  160. package/src/lib/env-autoload/index.ts +9 -0
  161. package/src/lib/errors/operation-error.ts +1 -1
  162. package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
  163. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
  164. package/src/lib/infra-kit-config/index.ts +9 -1
  165. package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
  166. package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
  167. package/src/lib/json-output/index.ts +1 -0
  168. package/src/lib/json-output/json-output.ts +47 -0
  169. package/src/lib/managed-block/managed-block.ts +4 -4
  170. package/src/lib/path-display/index.ts +1 -0
  171. package/src/lib/path-display/path-display.ts +42 -0
  172. package/src/lib/render/__tests__/render.test.ts +48 -0
  173. package/src/lib/render/index.ts +1 -0
  174. package/src/lib/render/render.ts +24 -0
  175. package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
  176. package/src/lib/vendor/__tests__/config.test.ts +63 -0
  177. package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
  178. package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
  179. package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
  180. package/src/lib/vendor/config-schema.ts +78 -0
  181. package/src/lib/vendor/config.ts +58 -0
  182. package/src/lib/vendor/factory-config-schema.ts +35 -0
  183. package/src/lib/vendor/factory-config.ts +91 -0
  184. package/src/lib/vendor/hash.ts +16 -0
  185. package/src/lib/vendor/index.ts +23 -0
  186. package/src/lib/vendor/manifest.ts +151 -0
  187. package/src/lib/vendor/skip-sets.ts +48 -0
  188. package/src/lib/vendor/sync-ops.ts +290 -0
  189. package/src/lib/vendor/walk.ts +56 -0
  190. package/src/mcp/tools/index.ts +5 -42
  191. package/src/sanity.test.tsx +1 -0
  192. package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
  193. package/src/tui/boot.tsx +45 -0
  194. package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
  195. package/src/tui/screens/command-palette.tsx +159 -0
  196. package/src/tui/types.ts +8 -0
  197. package/tsconfig.json +1 -0
  198. package/tsconfig.tsbuildinfo +1 -1
  199. package/vitest.config.ts +8 -0
  200. package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
  201. package/src/commands/worktrees-open/index.ts +0 -1
  202. package/src/commands/worktrees-open/worktrees-open.ts +0 -195
@@ -0,0 +1,45 @@
1
+ import { render } from 'ink'
2
+ import process from 'node:process'
3
+
4
+ import { CommandPalette } from './screens/command-palette'
5
+ import type { PaletteItem } from './types'
6
+
7
+ /**
8
+ * Entry point for the Ink TUI. This module (and everything under `src/tui/`) is
9
+ * the ONLY place allowed to import `ink`/`react`; it is reached exclusively via a
10
+ * dynamic `await import('src/tui/boot')` from the TTY branch of entry/cli.ts, so
11
+ * React never loads on the MCP / `--json` / non-TTY paths. Build splitting keeps
12
+ * it in a separate lazy chunk (see scripts/build.js).
13
+ */
14
+
15
+ /**
16
+ * Render the command palette and resolve to the chosen command name, or `null`
17
+ * if the user cancels. Frames are written to stderr so stdout stays clean for
18
+ * the command that runs afterwards (mirrors the previous Inquirer menu).
19
+ */
20
+ export const runCommandPalette = async (items: PaletteItem[]): Promise<string | null> => {
21
+ return new Promise((resolve) => {
22
+ let selected: string | null = null
23
+
24
+ const { waitUntilExit } = render(
25
+ <CommandPalette
26
+ items={items}
27
+ onSelect={(name) => {
28
+ selected = name
29
+ }}
30
+ onCancel={() => {
31
+ selected = null
32
+ }}
33
+ />,
34
+ { stdout: process.stderr as unknown as NodeJS.WriteStream },
35
+ )
36
+
37
+ waitUntilExit()
38
+ .then(() => {
39
+ resolve(selected)
40
+ })
41
+ .catch(() => {
42
+ resolve(null)
43
+ })
44
+ })
45
+ }
@@ -0,0 +1,136 @@
1
+ import { render } from 'ink-testing-library'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import type { PaletteItem } from '../../types'
5
+ import { CommandPalette } from '../command-palette'
6
+
7
+ const items: PaletteItem[] = [
8
+ { name: 'release-list', description: 'List all release branches', group: 'Release Management' },
9
+ { name: 'worktrees-list', description: 'List all git worktrees', group: 'Worktrees' },
10
+ { name: 'env-status', description: 'Show Doppler authentication status', group: 'Environment' },
11
+ ]
12
+
13
+ const settle = () => {
14
+ return new Promise((resolve) => {
15
+ setTimeout(resolve, 50)
16
+ })
17
+ }
18
+
19
+ describe('commandPalette', () => {
20
+ it('renders every command under its group header', () => {
21
+ const { lastFrame } = render(<CommandPalette items={items} onSelect={vi.fn()} onCancel={vi.fn()} />)
22
+ const frame = lastFrame() ?? ''
23
+
24
+ expect(frame).toContain('release-list')
25
+ expect(frame).toContain('worktrees-list')
26
+ expect(frame).toContain('env-status')
27
+ expect(frame).toContain('— Release Management —')
28
+ expect(frame).toContain('— Worktrees —')
29
+ })
30
+
31
+ it('filters the list as the user types', async () => {
32
+ const { lastFrame, stdin } = render(<CommandPalette items={items} onSelect={vi.fn()} onCancel={vi.fn()} />)
33
+
34
+ stdin.write('env')
35
+ await settle()
36
+
37
+ const frame = lastFrame() ?? ''
38
+
39
+ expect(frame).toContain('env-status')
40
+ expect(frame).not.toContain('release-list')
41
+ })
42
+
43
+ it('shows an empty-state message when nothing matches', async () => {
44
+ const { lastFrame, stdin } = render(<CommandPalette items={items} onSelect={vi.fn()} onCancel={vi.fn()} />)
45
+
46
+ stdin.write('zzzznomatch')
47
+ await settle()
48
+
49
+ expect(lastFrame() ?? '').toContain('No matching commands')
50
+ })
51
+
52
+ it('enter selects the active (first by default) command', async () => {
53
+ const onSelect = vi.fn()
54
+ const { stdin } = render(<CommandPalette items={items} onSelect={onSelect} onCancel={vi.fn()} />)
55
+
56
+ stdin.write('\r')
57
+ await settle()
58
+
59
+ expect(onSelect).toHaveBeenCalledWith('release-list')
60
+ })
61
+
62
+ it('arrow-down then Enter selects the second command', async () => {
63
+ const onSelect = vi.fn()
64
+ const { stdin } = render(<CommandPalette items={items} onSelect={onSelect} onCancel={vi.fn()} />)
65
+
66
+ stdin.write('\u001B[B') // arrow down
67
+ await settle()
68
+ stdin.write('\r')
69
+ await settle()
70
+
71
+ expect(onSelect).toHaveBeenCalledWith('worktrees-list')
72
+ })
73
+
74
+ it('arrow-up from the first command wraps to the last', async () => {
75
+ const onSelect = vi.fn()
76
+ const { stdin } = render(<CommandPalette items={items} onSelect={onSelect} onCancel={vi.fn()} />)
77
+
78
+ stdin.write('') // arrow up from first
79
+ await settle()
80
+ stdin.write('\r')
81
+ await settle()
82
+
83
+ expect(onSelect).toHaveBeenCalledWith('env-status')
84
+ })
85
+
86
+ it('arrow-down from the last command wraps to the first', async () => {
87
+ const onSelect = vi.fn()
88
+ const { stdin } = render(<CommandPalette items={items} onSelect={onSelect} onCancel={vi.fn()} />)
89
+
90
+ stdin.write('') // arrow up from first -> wraps to last (env-status)
91
+ await settle()
92
+ stdin.write('') // arrow down from last -> wraps to first (release-list)
93
+ await settle()
94
+ stdin.write('\r')
95
+ await settle()
96
+
97
+ expect(onSelect).toHaveBeenCalledWith('release-list')
98
+ })
99
+
100
+ it('typing then Enter selects the filtered command', async () => {
101
+ const onSelect = vi.fn()
102
+ const { stdin } = render(<CommandPalette items={items} onSelect={onSelect} onCancel={vi.fn()} />)
103
+
104
+ stdin.write('env')
105
+ await settle()
106
+ stdin.write('\r')
107
+ await settle()
108
+
109
+ expect(onSelect).toHaveBeenCalledWith('env-status')
110
+ })
111
+
112
+ it('clears the rendered list after a command is selected', async () => {
113
+ const { lastFrame, stdin } = render(<CommandPalette items={items} onSelect={vi.fn()} onCancel={vi.fn()} />)
114
+
115
+ expect(lastFrame() ?? '').toContain('release-list')
116
+
117
+ stdin.write('\r')
118
+ await settle()
119
+
120
+ // Final frame renders empty so Ink erases the list instead of freezing it
121
+ // into the scrollback above the command's own output.
122
+ expect(lastFrame() ?? '').not.toContain('release-list')
123
+ })
124
+
125
+ it('esc cancels without selecting', async () => {
126
+ const onSelect = vi.fn()
127
+ const onCancel = vi.fn()
128
+ const { stdin } = render(<CommandPalette items={items} onSelect={onSelect} onCancel={onCancel} />)
129
+
130
+ stdin.write('\u001B') // escape
131
+ await settle()
132
+
133
+ expect(onCancel).toHaveBeenCalled()
134
+ expect(onSelect).not.toHaveBeenCalled()
135
+ })
136
+ })
@@ -0,0 +1,159 @@
1
+ import { Box, Text, useApp, useInput } from 'ink'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+
4
+ import type { PaletteItem } from '../types'
5
+
6
+ interface CommandPaletteProps {
7
+ items: PaletteItem[]
8
+ /** Called with the chosen command name; the app then exits. */
9
+ onSelect: (name: string) => void
10
+ /** Called when the user cancels (Esc / Ctrl-C); the app then exits. */
11
+ onCancel: () => void
12
+ }
13
+
14
+ /**
15
+ * Fuzzy-ish command picker rendered with Ink. Pure presentation: it receives a
16
+ * flat list of commands (already grouped/ordered by the catalog) and returns the
17
+ * selected name. It never executes anything — the caller runs the command via
18
+ * the existing Commander path.
19
+ */
20
+ export const CommandPalette = (props: CommandPaletteProps) => {
21
+ const { items, onSelect, onCancel } = props
22
+
23
+ const T = {
24
+ hint: 'Select a command — type to filter, ↑↓ to move, Enter to run, Esc to cancel',
25
+ prompt: '❯ ',
26
+ empty: 'No matching commands',
27
+ }
28
+ const nameWidth = 24
29
+
30
+ const { exit } = useApp()
31
+ const [query, setQuery] = useState('')
32
+ const [index, setIndex] = useState(0)
33
+ // When set, the component renders `null` for one frame so Ink erases the list
34
+ // before unmount commits its final frame (Ink's `log.done()` would otherwise
35
+ // freeze the last drawn frame — the command list — into the scrollback).
36
+ const [submitted, setSubmitted] = useState(false)
37
+
38
+ // Exit only after the empty frame has committed, so the list is wiped first.
39
+ useEffect(() => {
40
+ if (submitted) {
41
+ exit()
42
+ }
43
+ }, [submitted, exit])
44
+
45
+ const filtered = useMemo(() => {
46
+ const q = query.trim().toLowerCase()
47
+
48
+ if (!q) {
49
+ return items
50
+ }
51
+
52
+ return items.filter((item) => {
53
+ return `${item.name} ${item.description}`.toLowerCase().includes(q)
54
+ })
55
+ }, [items, query])
56
+
57
+ const activeIndex = Math.min(index, Math.max(0, filtered.length - 1))
58
+
59
+ useInput((input, key) => {
60
+ if (submitted) {
61
+ return
62
+ }
63
+
64
+ if (key.escape || (key.ctrl && input === 'c')) {
65
+ onCancel()
66
+ setSubmitted(true)
67
+
68
+ return
69
+ }
70
+
71
+ if (key.return) {
72
+ const selected = filtered[activeIndex]
73
+
74
+ if (selected) {
75
+ onSelect(selected.name)
76
+ setSubmitted(true)
77
+ }
78
+
79
+ return
80
+ }
81
+
82
+ if (key.upArrow) {
83
+ const n = filtered.length
84
+
85
+ // Wrap to the bottom when stepping up past the first item. The `+ n`
86
+ // sidesteps JS negative modulo (e.g. -1 % 5 === -1, not 4).
87
+ if (n > 0) {
88
+ setIndex((activeIndex - 1 + n) % n)
89
+ }
90
+
91
+ return
92
+ }
93
+
94
+ if (key.downArrow) {
95
+ const n = filtered.length
96
+
97
+ // Wrap to the top when stepping down past the last item.
98
+ if (n > 0) {
99
+ setIndex((activeIndex + 1) % n)
100
+ }
101
+
102
+ return
103
+ }
104
+
105
+ if (key.backspace || key.delete) {
106
+ setQuery(query.slice(0, -1))
107
+ setIndex(0)
108
+
109
+ return
110
+ }
111
+
112
+ // Printable character: append to the filter.
113
+ if (input && !key.ctrl && !key.meta) {
114
+ setQuery(query + input)
115
+ setIndex(0)
116
+ }
117
+ })
118
+
119
+ // Final frame: render nothing so Ink erases the list on its way out.
120
+ if (submitted) {
121
+ return null
122
+ }
123
+
124
+ let lastGroup = ''
125
+
126
+ return (
127
+ <Box flexDirection="column">
128
+ <Text dimColor>{T.hint}</Text>
129
+ <Box>
130
+ <Text color="cyan">{T.prompt}</Text>
131
+ <Text>{query}</Text>
132
+ </Box>
133
+ <Box flexDirection="column" marginTop={1}>
134
+ {filtered.length === 0 ? (
135
+ <Text dimColor>{T.empty}</Text>
136
+ ) : (
137
+ filtered.map((item, position) => {
138
+ const isActive = position === activeIndex
139
+ const showGroup = item.group !== lastGroup
140
+
141
+ lastGroup = item.group
142
+
143
+ return (
144
+ <Box flexDirection="column" key={item.name}>
145
+ {/* Blank line between groups (not before the very first group). */}
146
+ {showGroup && position > 0 ? <Text> </Text> : null}
147
+ {showGroup ? <Text color="yellow">{`— ${item.group} —`}</Text> : null}
148
+ <Text color={isActive ? 'green' : undefined}>
149
+ {isActive ? '› ' : ' '}
150
+ {item.name.padEnd(nameWidth)} <Text dimColor>{item.description}</Text>
151
+ </Text>
152
+ </Box>
153
+ )
154
+ })
155
+ )}
156
+ </Box>
157
+ </Box>
158
+ )
159
+ }
@@ -0,0 +1,8 @@
1
+ export interface PaletteItem {
2
+ /** Command name (the token passed to the CLI, e.g. `release-list`). */
3
+ name: string
4
+ /** One-line description (sourced from Commander). */
5
+ description: string
6
+ /** Display group header (e.g. "Release Management"). */
7
+ group: string
8
+ }
package/tsconfig.json CHANGED
@@ -8,6 +8,7 @@
8
8
  "lib": ["ESNext"],
9
9
  "module": "ESNext",
10
10
  "moduleResolution": "bundler",
11
+ "jsx": "react-jsx",
11
12
  "paths": {
12
13
  "src/*": ["./src/*"]
13
14
  },