From 262276dcd73a96c08beffb336d46d3da23d34ba3 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Tue, 12 May 2026 17:54:18 -0700 Subject: [PATCH] feat(cli): add intro step and project dir to demo tour Show the target project directory in the demo banner and add an introductory screen before the first setup card so users understand where demo artifacts will land. Also simplify stdin key detection by comparing raw byte values instead of string conversions. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/setup-demo-tour.test.ts | 17 ++++++------ packages/cli/src/setup-demo-tour.ts | 34 +++++++++++++++--------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/setup-demo-tour.test.ts b/packages/cli/src/setup-demo-tour.test.ts index d3f7e3f4..a8b63974 100644 --- a/packages/cli/src/setup-demo-tour.test.ts +++ b/packages/cli/src/setup-demo-tour.test.ts @@ -209,7 +209,7 @@ describe('runDemoTour', () => { }, ); expect(result).toBe(0); - // Navigation called once for databases step, then exits + // Navigation called once for intro, then exits on back expect(navigation).toHaveBeenCalledTimes(1); }); @@ -218,10 +218,11 @@ describe('runDemoTour', () => { let callCount = 0; const navigation = vi.fn().mockImplementation(() => { callCount++; - // First call (databases): forward - // Second call (sources): back - // Third call (databases again): back (exit) - if (callCount === 1) return Promise.resolve('forward'); + // First call (intro): forward + // Second call (databases): forward + // Third call (sources): back + // Fourth call (databases again): back (exit) + if (callCount <= 2) return Promise.resolve('forward'); return Promise.resolve('back'); }); @@ -235,7 +236,7 @@ describe('runDemoTour', () => { }, ); expect(result).toBe(0); - expect(navigation).toHaveBeenCalledTimes(3); + expect(navigation).toHaveBeenCalledTimes(4); }); it('handles agent step returning back', async () => { @@ -243,10 +244,10 @@ describe('runDemoTour', () => { let navCount = 0; const navigation = vi.fn().mockImplementation(() => { navCount++; - // Forward through databases, sources, context + // Forward through intro, databases, sources, context // Then back from context (after agents returns back) // Then back from sources, then back from databases (exit) - if (navCount <= 3) return Promise.resolve('forward'); + if (navCount <= 4) return Promise.resolve('forward'); return Promise.resolve('back'); }); diff --git a/packages/cli/src/setup-demo-tour.ts b/packages/cli/src/setup-demo-tour.ts index f3d71f70..fe211e67 100644 --- a/packages/cli/src/setup-demo-tour.ts +++ b/packages/cli/src/setup-demo-tour.ts @@ -62,12 +62,15 @@ function createTargetState(target: KtxPublicIngestPlanTarget): ContextBuildTarge // Pure rendering functions // --------------------------------------------------------------------------- -export function renderDemoBanner(): string { +export function renderDemoBanner(projectDir?: string): string { const lines = [ '', `┌ ${cyan('Demo mode')} — data has been pre-processed and KTX context is already built.`, '│ This walkthrough illustrates the setup steps. Selections are pre-filled and read-only.', ]; + if (projectDir) { + lines.push(`│ Project directory: ${dim(projectDir)}`); + } return lines.join('\n'); } @@ -144,16 +147,15 @@ export async function waitForDemoNavigation( }; const onData = (data: Buffer) => { - const char = data.toString(); - if (char === '\r' || char === '\n') { - cleanup(); - resolve('forward'); - } else if (char === '\x1b') { - cleanup(); - resolve('back'); - } else if (char === '\x03') { + if (data[0] === 0x03) { cleanup(); reject(new KtxSetupExitError()); + } else if (data[0] === 0x0d || data[0] === 0x0a) { + cleanup(); + resolve('forward'); + } else if (data[0] === 0x1b) { + cleanup(); + resolve('back'); } }; @@ -171,8 +173,9 @@ export async function renderDemoCard( io: KtxCliIo, stdin?: NodeJS.ReadStream, waitNav: (stdin?: NodeJS.ReadStream) => Promise<'forward' | 'back'> = waitForDemoNavigation, + projectDir?: string, ): Promise<'forward' | 'back'> { - io.stdout.write(renderDemoBanner() + '\n\n'); + io.stdout.write(renderDemoBanner(projectDir) + '\n\n'); io.stdout.write(renderDemoCardContent(title, selections) + '\n'); return waitNav(stdin); } @@ -337,6 +340,11 @@ export async function runDemoTour( const projectDir = defaultDemoProjectDir(); await ensureProject({ projectDir, force: false }); + io.stdout.write(renderDemoBanner(projectDir) + '\n'); + io.stdout.write(`\n│ ${dim('Press Enter to continue, Escape to go back')}\n└\n`); + const introDirection = await waitNav(); + if (introDirection === 'back') return 0; + let stepIndex = 0; while (stepIndex < DEMO_STEPS.length) { @@ -344,11 +352,11 @@ export async function runDemoTour( let direction: 'forward' | 'back'; if (step === 'databases') { - direction = await renderDemoCard('Database connection', ['PostgreSQL — Orbit Analytics (56 tables, 2 schemas)'], io, undefined, waitNav); + direction = await renderDemoCard('Database connection', ['PostgreSQL — Orbit Analytics (56 tables, 2 schemas)'], io, undefined, waitNav, projectDir); } else if (step === 'sources') { - direction = await renderDemoCard('Context sources', ['dbt — 34 transformation models', 'Metabase — 80 dashboard cards', 'Notion — 9 knowledge pages'], io, undefined, waitNav); + direction = await renderDemoCard('Context sources', ['dbt — 34 transformation models', 'Metabase — 80 dashboard cards', 'Notion — 9 knowledge pages'], io, undefined, waitNav, projectDir); } else if (step === 'context') { - io.stdout.write(renderDemoBanner() + '\n\n'); + io.stdout.write(renderDemoBanner(projectDir) + '\n\n'); if (deps.skipReplayAnimation) { direction = await waitNav(); } else {