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.
- package/.eslintcache +1 -1
- package/.omc/state/agent-replay-1fcfe0ec-7567-41c3-9bfd-501e99e86819.jsonl +19 -0
- package/.omc/state/agent-replay-38382db5-3088-4b5a-aff7-2561a60827c7.jsonl +17 -0
- package/.omc/state/agent-replay-50c36be9-a3a2-42b9-9c85-5a06d3e22f6a.jsonl +5 -0
- package/.omc/state/agent-replay-8198b3e5-fad6-4ee0-9c59-ef773ad89af4.jsonl +1 -0
- package/.omc/state/agent-replay-8cd7652a-b8b5-49cf-9b1f-bee8e5972458.jsonl +3 -0
- package/.omc/state/agent-replay-9bea518f-a9ee-4d14-bb53-a936ebaf647c.jsonl +7 -0
- package/.omc/state/agent-replay-b52d89fb-0e24-4901-bc66-27e3c1697126.jsonl +2 -0
- package/.omc/state/agent-replay-f581a1ae-0ee4-48a3-96f3-ce31a0633bb0.jsonl +1 -0
- package/.omc/state/agent-replay-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
- package/.omc/state/idle-notif-cooldown.json +1 -1
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/mission-state.json +109 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/pre-tool-advisory-throttle.json +42 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ralph-state.json +13 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/skill-active-state.json +15 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/subagent-tracking-state.json +62 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ultrawork-state.json +11 -0
- package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/mission-state.json +145 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/pre-tool-advisory-throttle.json +38 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/ralplan-state.json +11 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/subagent-tracking-state.json +98 -0
- package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/mission-state.json +61 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/skill-active-state.json +15 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/subagent-tracking-state.json +26 -0
- package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/pre-tool-advisory-throttle.json +14 -0
- package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/ralplan-state.json +11 -0
- package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/subagent-tracking-state.json +35 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/mission-state.json +79 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/progress.txt +7 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/skill-active-state.json +15 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/subagent-tracking-state.json +26 -0
- package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/pre-tool-advisory-throttle.json +14 -0
- package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/mission-state.json +61 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/prd.json +22 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/progress.txt +22 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/skill-active-state.json +15 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/subagent-tracking-state.json +26 -0
- package/.turbo/turbo-build.log +3 -3
- package/.turbo/turbo-test.log +92 -44
- package/dist/boot-K4QUAFSP.js +2 -0
- package/dist/boot-K4QUAFSP.js.map +7 -0
- package/dist/chunk-RTSP2FCA.js +2 -0
- package/dist/chunk-RTSP2FCA.js.map +7 -0
- package/dist/chunk-VPBZR2PY.js +166 -0
- package/dist/chunk-VPBZR2PY.js.map +7 -0
- package/dist/cli.js +11 -107
- package/dist/cli.js.map +4 -4
- package/dist/entry/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +4 -4
- package/dist/lib/vendor/config-schema.d.ts +58 -0
- package/dist/mcp.js +1 -54
- package/dist/mcp.js.map +4 -4
- package/eslint.config.js +33 -1
- package/package.json +5 -1
- package/scripts/build.js +11 -1
- package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
- package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
- package/src/.omc/state/idle-notif-cooldown.json +1 -1
- package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
- package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
- package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
- package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
- package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
- package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
- package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
- package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
- package/src/commands/config/config.ts +7 -37
- package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
- package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
- package/src/commands/doctor/doctor.ts +104 -44
- package/src/commands/env-autoload/env-autoload.ts +12 -0
- package/src/commands/env-autoload/index.ts +1 -0
- package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
- package/src/commands/env-clear/env-clear.ts +22 -8
- package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
- package/src/commands/env-load/env-load.ts +152 -35
- package/src/commands/env-load/index.ts +2 -1
- package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
- package/src/commands/env-status/env-status.ts +19 -7
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
- package/src/commands/init/__tests__/agent-files.test.ts +145 -27
- package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
- package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
- package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
- package/src/commands/init/__tests__/shell-body.test.ts +30 -0
- package/src/commands/init/agent-files.ts +65 -45
- package/src/commands/init/init.ts +140 -31
- package/src/commands/init/migrate-config.ts +160 -32
- package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
- package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
- package/src/commands/vendor-check/index.ts +1 -0
- package/src/commands/vendor-check/vendor-check.ts +127 -0
- package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
- package/src/commands/vendor-config/index.ts +1 -0
- package/src/commands/vendor-config/vendor-config.ts +162 -0
- package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
- package/src/commands/vendor-diff/index.ts +1 -0
- package/src/commands/vendor-diff/vendor-diff.ts +77 -0
- package/src/commands/vendor-manifest/index.ts +1 -0
- package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
- package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
- package/src/commands/vendor-sync/index.ts +1 -0
- package/src/commands/vendor-sync/vendor-sync.ts +90 -0
- package/src/commands/worktrees-add/worktrees-add.ts +1 -1
- package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
- package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
- package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
- package/src/entry/cli.ts +417 -210
- package/src/entry/index.ts +7 -1
- package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
- package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
- package/src/integrations/cmux/list-workspace-titles.ts +1 -1
- package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
- package/src/integrations/cursor/open-cursor-workspace.ts +6 -11
- package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
- package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
- package/src/integrations/doppler/doppler-errors.ts +88 -0
- package/src/integrations/doppler/doppler-project.ts +80 -0
- package/src/integrations/doppler/index.ts +3 -1
- package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
- package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
- package/src/integrations/ide/__tests__/ide-facade.test.ts +49 -10
- package/src/integrations/ide/open-ide-workspace.ts +6 -6
- package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
- package/src/integrations/ide/types.ts +10 -1
- package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
- package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
- package/src/integrations/zed/index.ts +1 -0
- package/src/integrations/zed/open-zed-workspace.ts +4 -10
- package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
- package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
- package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
- package/src/lib/command-catalog/command-catalog.ts +141 -0
- package/src/lib/command-catalog/index.ts +2 -0
- package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
- package/src/lib/constants/constants.ts +13 -0
- package/src/lib/constants/index.ts +2 -0
- package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
- package/src/lib/env-autoload/env-autoload.ts +294 -0
- package/src/lib/env-autoload/index.ts +9 -0
- package/src/lib/errors/operation-error.ts +1 -1
- package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
- package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
- package/src/lib/infra-kit-config/index.ts +9 -1
- package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
- package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
- package/src/lib/json-output/index.ts +1 -0
- package/src/lib/json-output/json-output.ts +47 -0
- package/src/lib/managed-block/managed-block.ts +4 -4
- package/src/lib/path-display/index.ts +1 -0
- package/src/lib/path-display/path-display.ts +42 -0
- package/src/lib/render/__tests__/render.test.ts +48 -0
- package/src/lib/render/index.ts +1 -0
- package/src/lib/render/render.ts +24 -0
- package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
- package/src/lib/vendor/__tests__/config.test.ts +63 -0
- package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
- package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
- package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
- package/src/lib/vendor/config-schema.ts +78 -0
- package/src/lib/vendor/config.ts +58 -0
- package/src/lib/vendor/factory-config-schema.ts +35 -0
- package/src/lib/vendor/factory-config.ts +91 -0
- package/src/lib/vendor/hash.ts +16 -0
- package/src/lib/vendor/index.ts +23 -0
- package/src/lib/vendor/manifest.ts +151 -0
- package/src/lib/vendor/skip-sets.ts +48 -0
- package/src/lib/vendor/sync-ops.ts +290 -0
- package/src/lib/vendor/walk.ts +56 -0
- package/src/mcp/tools/index.ts +5 -42
- package/src/sanity.test.tsx +1 -0
- package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
- package/src/tui/boot.tsx +45 -0
- package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
- package/src/tui/screens/command-palette.tsx +159 -0
- package/src/tui/types.ts +8 -0
- package/tsconfig.json +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +8 -0
- package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
- package/src/commands/worktrees-open/index.ts +0 -1
- package/src/commands/worktrees-open/worktrees-open.ts +0 -195
package/src/tui/boot.tsx
ADDED
|
@@ -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('[A') // 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('[A') // arrow up from first -> wraps to last (env-status)
|
|
91
|
+
await settle()
|
|
92
|
+
stdin.write('[B') // 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
|
+
}
|
package/src/tui/types.ts
ADDED
|
@@ -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
|
+
}
|