diff --git a/packages/cli/src/notion-page-picker-tui.test.tsx b/packages/cli/src/notion-page-picker-tui.test.tsx index ae39d39e..2d4dffc3 100644 --- a/packages/cli/src/notion-page-picker-tui.test.tsx +++ b/packages/cli/src/notion-page-picker-tui.test.tsx @@ -378,7 +378,7 @@ describe('renderNotionPickerTui', () => { }, ), ).resolves.toEqual({ kind: 'quit' }); - expect(stderr).toContain('Use --no-input --root-page-id for scripted mode'); + expect(stderr).toContain('Use --no-input --notion-root-page-id for scripted mode'); expect(stderr).not.toContain('secret'); }); }); diff --git a/packages/cli/src/notion-page-picker-tui.tsx b/packages/cli/src/notion-page-picker-tui.tsx index fac7f339..30af7522 100644 --- a/packages/cli/src/notion-page-picker-tui.tsx +++ b/packages/cli/src/notion-page-picker-tui.tsx @@ -331,7 +331,7 @@ export async function renderNotionPickerTui( return result; } catch (error) { io.stderr.write( - `Notion picker requires a TTY. Use --no-input --root-page-id for scripted mode. ${sanitizeNotionPickerTuiError(error)}\n`, + `Notion picker requires a TTY. Use --no-input --notion-root-page-id for scripted mode. ${sanitizeNotionPickerTuiError(error)}\n`, ); return { kind: 'quit' }; } diff --git a/packages/cli/src/setup-interrupt.test.ts b/packages/cli/src/setup-interrupt.test.ts index d8e6350a..62917db6 100644 --- a/packages/cli/src/setup-interrupt.test.ts +++ b/packages/cli/src/setup-interrupt.test.ts @@ -17,9 +17,11 @@ function makeTracker(ctrlCValues: boolean[]): SetupInterruptTracker { describe('setup interrupt confirmation', () => { const originalIsTTY = process.stdin.isTTY; + const originalRef = process.stdin.ref; afterEach(() => { Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: originalIsTTY }); + Object.defineProperty(process.stdin, 'ref', { configurable: true, value: originalRef }); }); it('fails before opening a prompt when interactive setup has no tty', async () => { @@ -33,6 +35,26 @@ describe('setup interrupt confirmation', () => { expect(prompt).not.toHaveBeenCalled(); }); + it('refs stdin before opening a real interactive prompt', async () => { + const calls: string[] = []; + Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: true }); + Object.defineProperty(process.stdin, 'ref', { + configurable: true, + value: vi.fn(() => { + calls.push('ref'); + return process.stdin; + }), + }); + const prompt = vi.fn(async () => { + calls.push('prompt'); + return 'continued'; + }); + + await expect(withSetupInterruptConfirmation(prompt)).resolves.toBe('continued'); + + expect(calls).toEqual(['ref', 'prompt']); + }); + it('asks before exiting on Ctrl+C and reruns the active prompt when declined', async () => { const prompt = vi.fn(async () => (prompt.mock.calls.length === 1 ? CANCEL : 'continued')); const confirmExit = vi.fn(async () => false); diff --git a/packages/cli/src/setup-interrupt.ts b/packages/cli/src/setup-interrupt.ts index 5773c336..9baa0f1f 100644 --- a/packages/cli/src/setup-interrupt.ts +++ b/packages/cli/src/setup-interrupt.ts @@ -23,6 +23,10 @@ interface SetupInterruptOptions { const NON_INTERACTIVE_SETUP_MESSAGE = 'Interactive setup requires a terminal. Re-run this command in a TTY, or pass --no-input with the required options.'; +function refSetupInput(input: NodeJS.ReadStream = stdin): void { + input.ref?.(); +} + function createSetupInterruptTracker(input: NodeJS.ReadStream = stdin): SetupInterruptTracker { let ctrlCPressed = false; const onKeypress = (char: string | undefined, key: Key) => { @@ -73,6 +77,9 @@ export async function withSetupInterruptConfirmation( const confirmExit = options.confirmExit ?? defaultConfirmExit; while (true) { + if (!options.tracker) { + refSetupInput(); + } const value = await tracker.track(prompt); if (!isCancel(value)) { return value; diff --git a/packages/cli/src/setup-sources.test.ts b/packages/cli/src/setup-sources.test.ts index b79e43d6..93ad854b 100644 --- a/packages/cli/src/setup-sources.test.ts +++ b/packages/cli/src/setup-sources.test.ts @@ -311,6 +311,14 @@ describe('setup sources step', () => { ).resolves.toEqual({ status: 'ready', projectDir, connectionIds: ['notion-main'] }); expect(pickNotionRootPages).toHaveBeenCalledOnce(); + expect(testPrompts.select).toHaveBeenCalledWith({ + message: 'Which Notion pages should KTX ingest?', + options: [ + { value: 'selected_roots', label: 'Specific pages and their subpages (choose them in a picker)' }, + { value: 'all_accessible', label: 'All pages the integration can access' }, + { value: 'back', label: 'Back' }, + ], + }); expect((await readConfig()).connections['notion-main']).toMatchObject({ driver: 'notion', auth_token_ref: 'env:NOTION_TOKEN', diff --git a/packages/cli/src/setup-sources.ts b/packages/cli/src/setup-sources.ts index 5fa4c4af..313dfbe0 100644 --- a/packages/cli/src/setup-sources.ts +++ b/packages/cli/src/setup-sources.ts @@ -1270,7 +1270,7 @@ async function promptForInteractiveSource( const crawlMode = await prompts.select({ message: 'Which Notion pages should KTX ingest?', options: [ - { value: 'selected_roots', label: 'Specific pages and their subpages (you\'ll paste page IDs)' }, + { value: 'selected_roots', label: 'Specific pages and their subpages (choose them in a picker)' }, { value: 'all_accessible', label: 'All pages the integration can access' }, { value: 'back', label: 'Back' }, ],