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) <noreply@anthropic.com>
This commit is contained in:
Luca Martial 2026-05-12 17:54:18 -07:00
parent 556563d654
commit 262276dcd7
2 changed files with 30 additions and 21 deletions

View file

@ -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');
});

View file

@ -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 {