mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-28 08:49:38 +02:00
813 lines
26 KiB
Markdown
813 lines
26 KiB
Markdown
# Demo Guided Tour Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Replace the disconnected "Try KTX with packaged demo data" flow with a guided tour that walks users through the same setup wizard steps using pre-filled, read-only selections, then connects their agent to the populated demo project.
|
|
|
|
**Architecture:** A new `setup-demo-tour.ts` module owns the demo tour flow. It renders read-only cards (database, sources), a simulated context build replay using the existing `renderContextBuildView` + `createRepainter` pipeline from `context-build-view.ts`, then hands off to the real `runKtxSetupAgentsStep`. The entry point in `setup.ts` (`runKtxSetupDemoFromEntryMenu`) is rewired to call this new module instead of `runKtxDemo`.
|
|
|
|
**Tech Stack:** TypeScript (ESM), Node.js raw stdin for keypress handling, existing `@clack/prompts` visual patterns, vitest for tests.
|
|
|
|
---
|
|
|
|
### Task 1: Create `setup-demo-tour.ts` with keypress utility and banner
|
|
|
|
**Files:**
|
|
- Create: `packages/cli/src/setup-demo-tour.ts`
|
|
- Test: `packages/cli/src/setup-demo-tour.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test for `renderDemoBanner`**
|
|
|
|
```typescript
|
|
// packages/cli/src/setup-demo-tour.test.ts
|
|
import { describe, expect, it } from 'vitest';
|
|
import { renderDemoBanner } from './setup-demo-tour.js';
|
|
|
|
describe('renderDemoBanner', () => {
|
|
it('includes demo mode explanation', () => {
|
|
const output = renderDemoBanner();
|
|
expect(output).toContain('Demo mode');
|
|
expect(output).toContain('pre-processed');
|
|
expect(output).toContain('read-only');
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run the test to verify it fails**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: FAIL — module not found
|
|
|
|
- [ ] **Step 3: Implement `renderDemoBanner` and `waitForDemoNavigation`**
|
|
|
|
```typescript
|
|
// packages/cli/src/setup-demo-tour.ts
|
|
import type { KtxCliIo } from './cli-runtime.js';
|
|
import { KtxSetupExitError } from './setup-interrupt.js';
|
|
|
|
const ESC = String.fromCharCode(0x1b);
|
|
|
|
function cyan(text: string): string {
|
|
return `${ESC}[36m${text}${ESC}[39m`;
|
|
}
|
|
|
|
function dim(text: string): string {
|
|
return `${ESC}[2m${text}${ESC}[22m`;
|
|
}
|
|
|
|
export function renderDemoBanner(): 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.`,
|
|
'',
|
|
];
|
|
return lines.join('\n');
|
|
}
|
|
|
|
export async function waitForDemoNavigation(
|
|
stdin: NodeJS.ReadStream = process.stdin,
|
|
): Promise<'forward' | 'back'> {
|
|
return new Promise((resolve, reject) => {
|
|
const wasRaw = stdin.isRaw;
|
|
if (stdin.setRawMode) stdin.setRawMode(true);
|
|
stdin.resume();
|
|
|
|
const onData = (data: Buffer) => {
|
|
const key = data.toString();
|
|
if (key === '\r' || key === '\n') {
|
|
cleanup();
|
|
resolve('forward');
|
|
} else if (key === '\x1b') {
|
|
cleanup();
|
|
resolve('back');
|
|
} else if (key === '\x03') {
|
|
cleanup();
|
|
reject(new KtxSetupExitError());
|
|
}
|
|
};
|
|
|
|
const cleanup = () => {
|
|
stdin.off('data', onData);
|
|
if (stdin.setRawMode) stdin.setRawMode(wasRaw ?? false);
|
|
};
|
|
|
|
stdin.on('data', onData);
|
|
});
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run the test to verify it passes**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts
|
|
git commit -m "feat(cli): add demo tour banner and keypress navigation utility"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Add `renderDemoCard` function
|
|
|
|
**Files:**
|
|
- Modify: `packages/cli/src/setup-demo-tour.ts`
|
|
- Modify: `packages/cli/src/setup-demo-tour.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test for `renderDemoCard`**
|
|
|
|
Append to the test file:
|
|
|
|
```typescript
|
|
import { renderDemoCardContent } from './setup-demo-tour.js';
|
|
|
|
describe('renderDemoCardContent', () => {
|
|
it('renders a card with title and selections', () => {
|
|
const output = renderDemoCardContent('Database connection', ['PostgreSQL (demo warehouse)']);
|
|
expect(output).toContain('Database connection');
|
|
expect(output).toContain('PostgreSQL (demo warehouse)');
|
|
expect(output).toContain('Press Enter to continue');
|
|
expect(output).toContain('Escape to go back');
|
|
});
|
|
|
|
it('renders multiple selections', () => {
|
|
const output = renderDemoCardContent('Context sources', ['dbt', 'Metabase', 'Notion']);
|
|
expect(output).toContain('dbt');
|
|
expect(output).toContain('Metabase');
|
|
expect(output).toContain('Notion');
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run the test to verify it fails**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: FAIL — `renderDemoCardContent` not exported
|
|
|
|
- [ ] **Step 3: Implement `renderDemoCardContent` and `renderDemoCard`**
|
|
|
|
Add to `setup-demo-tour.ts`:
|
|
|
|
```typescript
|
|
export function renderDemoCardContent(title: string, selections: string[]): string {
|
|
const lines = [
|
|
`┌ ${title}`,
|
|
'│',
|
|
...selections.map((s) => `│ ${cyan('▸')} ${s}`),
|
|
'│',
|
|
`│ ${dim('Press Enter to continue, Escape to go back')}`,
|
|
'└',
|
|
'',
|
|
];
|
|
return lines.join('\n');
|
|
}
|
|
|
|
export async function renderDemoCard(
|
|
title: string,
|
|
selections: string[],
|
|
io: KtxCliIo,
|
|
stdin?: NodeJS.ReadStream,
|
|
waitNav?: (stdin?: NodeJS.ReadStream) => Promise<'forward' | 'back'>,
|
|
): Promise<'forward' | 'back'> {
|
|
io.stdout.write(renderDemoBanner());
|
|
io.stdout.write(renderDemoCardContent(title, selections));
|
|
const nav = waitNav ?? waitForDemoNavigation;
|
|
return nav(stdin);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run the test to verify it passes**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts
|
|
git commit -m "feat(cli): add demo tour read-only card rendering"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Add demo context build replay animation
|
|
|
|
**Files:**
|
|
- Modify: `packages/cli/src/setup-demo-tour.ts`
|
|
- Modify: `packages/cli/src/setup-demo-tour.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test for demo replay event sequence**
|
|
|
|
Append to the test file:
|
|
|
|
```typescript
|
|
import { buildDemoReplayTimeline, DEMO_REPLAY_TARGETS } from './setup-demo-tour.js';
|
|
|
|
describe('buildDemoReplayTimeline', () => {
|
|
it('produces events for all four demo targets', () => {
|
|
const events = buildDemoReplayTimeline();
|
|
const connectionIds = new Set(events.map((e) => e.connectionId));
|
|
expect(connectionIds).toEqual(new Set(['demo-warehouse', 'dbt', 'metabase', 'notion']));
|
|
});
|
|
|
|
it('ends with all targets done', () => {
|
|
const events = buildDemoReplayTimeline();
|
|
const lastByConnection = new Map<string, string>();
|
|
for (const e of events) {
|
|
lastByConnection.set(e.connectionId, e.status);
|
|
}
|
|
for (const status of lastByConnection.values()) {
|
|
expect(status).toBe('done');
|
|
}
|
|
});
|
|
|
|
it('events are sorted by delayMs', () => {
|
|
const events = buildDemoReplayTimeline();
|
|
for (let i = 1; i < events.length; i++) {
|
|
expect(events[i]!.delayMs).toBeGreaterThanOrEqual(events[i - 1]!.delayMs);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('DEMO_REPLAY_TARGETS', () => {
|
|
it('has one primary source and three context sources', () => {
|
|
expect(DEMO_REPLAY_TARGETS.primarySources).toHaveLength(1);
|
|
expect(DEMO_REPLAY_TARGETS.contextSources).toHaveLength(3);
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run the test to verify it fails**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: FAIL — exports not found
|
|
|
|
- [ ] **Step 3: Implement replay timeline and target definitions**
|
|
|
|
Add to `setup-demo-tour.ts`:
|
|
|
|
```typescript
|
|
import type { KtxPublicIngestPlanTarget } from './public-ingest.js';
|
|
import type { ContextBuildTargetState, ContextBuildViewState } from './context-build-view.js';
|
|
|
|
export interface DemoReplayEvent {
|
|
delayMs: number;
|
|
connectionId: string;
|
|
status: 'running' | 'done';
|
|
detailLine: string | null;
|
|
summaryText: string | null;
|
|
}
|
|
|
|
function createDemoTarget(connectionId: string, operation: 'scan' | 'source-ingest', driver: string): KtxPublicIngestPlanTarget {
|
|
return {
|
|
connectionId,
|
|
driver,
|
|
operation,
|
|
debugCommand: `ktx ${operation === 'scan' ? 'scan' : 'ingest'} ${connectionId}`,
|
|
steps: operation === 'scan' ? ['scan'] : ['source-ingest'],
|
|
};
|
|
}
|
|
|
|
const primaryTarget = createDemoTarget('demo-warehouse', 'scan', 'postgres');
|
|
const dbtTarget = createDemoTarget('dbt', 'source-ingest', 'dbt');
|
|
const metabaseTarget = createDemoTarget('metabase', 'source-ingest', 'metabase');
|
|
const notionTarget = createDemoTarget('notion', 'source-ingest', 'notion');
|
|
|
|
function createTargetState(target: KtxPublicIngestPlanTarget): ContextBuildTargetState {
|
|
return {
|
|
target,
|
|
status: 'queued',
|
|
detailLine: null,
|
|
summaryText: null,
|
|
startedAt: null,
|
|
elapsedMs: 0,
|
|
};
|
|
}
|
|
|
|
export const DEMO_REPLAY_TARGETS = {
|
|
primarySources: [primaryTarget],
|
|
contextSources: [dbtTarget, metabaseTarget, notionTarget],
|
|
};
|
|
|
|
export function buildDemoReplayTimeline(): DemoReplayEvent[] {
|
|
return [
|
|
{ delayMs: 0, connectionId: 'demo-warehouse', status: 'running', detailLine: 'scanning...', summaryText: null },
|
|
{ delayMs: 600, connectionId: 'demo-warehouse', status: 'running', detailLine: '[50%] scanning...', summaryText: null },
|
|
{ delayMs: 1200, connectionId: 'demo-warehouse', status: 'done', detailLine: null, summaryText: 'completed' },
|
|
{ delayMs: 1200, connectionId: 'dbt', status: 'running', detailLine: 'ingesting...', summaryText: null },
|
|
{ delayMs: 1800, connectionId: 'dbt', status: 'running', detailLine: '[60%] ingesting...', summaryText: null },
|
|
{ delayMs: 2200, connectionId: 'dbt', status: 'done', detailLine: null, summaryText: 'completed' },
|
|
{ delayMs: 2200, connectionId: 'metabase', status: 'running', detailLine: 'ingesting...', summaryText: null },
|
|
{ delayMs: 2800, connectionId: 'metabase', status: 'done', detailLine: null, summaryText: 'completed' },
|
|
{ delayMs: 2800, connectionId: 'notion', status: 'running', detailLine: 'ingesting...', summaryText: null },
|
|
{ delayMs: 3400, connectionId: 'notion', status: 'done', detailLine: null, summaryText: 'completed' },
|
|
];
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run the test to verify it passes**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Implement `runDemoContextReplay` animation driver**
|
|
|
|
Add to `setup-demo-tour.ts`:
|
|
|
|
```typescript
|
|
import { renderContextBuildView, createRepainter } from './context-build-view.js';
|
|
|
|
export async function runDemoContextReplay(
|
|
io: KtxCliIo,
|
|
stdin?: NodeJS.ReadStream,
|
|
): Promise<'forward' | 'back'> {
|
|
const repainter = createRepainter(io);
|
|
const timeline = buildDemoReplayTimeline();
|
|
|
|
const state: ContextBuildViewState = {
|
|
primarySources: DEMO_REPLAY_TARGETS.primarySources.map((t) => createTargetState(t)),
|
|
contextSources: DEMO_REPLAY_TARGETS.contextSources.map((t) => createTargetState(t)),
|
|
frame: 0,
|
|
startedAt: Date.now(),
|
|
totalElapsedMs: 0,
|
|
};
|
|
|
|
const allTargets = [...state.primarySources, ...state.contextSources];
|
|
const targetMap = new Map(allTargets.map((t) => [t.target.connectionId, t]));
|
|
let eventIndex = 0;
|
|
const startTime = Date.now();
|
|
const FRAME_MS = 120;
|
|
|
|
await new Promise<void>((resolve) => {
|
|
const interval = setInterval(() => {
|
|
const elapsed = Date.now() - startTime;
|
|
state.frame += 1;
|
|
state.totalElapsedMs = elapsed;
|
|
|
|
while (eventIndex < timeline.length && timeline[eventIndex]!.delayMs <= elapsed) {
|
|
const event = timeline[eventIndex]!;
|
|
const target = targetMap.get(event.connectionId);
|
|
if (target) {
|
|
target.status = event.status;
|
|
target.detailLine = event.detailLine;
|
|
target.summaryText = event.summaryText;
|
|
if (event.status === 'running' && target.startedAt === null) {
|
|
target.startedAt = Date.now();
|
|
}
|
|
if (event.status === 'done') {
|
|
target.elapsedMs = target.startedAt ? Date.now() - target.startedAt : 0;
|
|
}
|
|
}
|
|
eventIndex += 1;
|
|
}
|
|
|
|
for (const t of allTargets) {
|
|
if (t.status === 'running' && t.startedAt !== null) {
|
|
t.elapsedMs = Date.now() - t.startedAt;
|
|
}
|
|
}
|
|
|
|
repainter.paint(renderContextBuildView(state, { styled: io.stdout.isTTY ?? false, showHint: false }));
|
|
|
|
if (eventIndex >= timeline.length && allTargets.every((t) => t.status === 'done')) {
|
|
clearInterval(interval);
|
|
resolve();
|
|
}
|
|
}, FRAME_MS);
|
|
});
|
|
|
|
io.stdout.write(renderDemoContextCompletionSummary());
|
|
return waitForDemoNavigation(stdin);
|
|
}
|
|
|
|
function renderDemoContextCompletionSummary(): string {
|
|
const lines = [
|
|
'',
|
|
`${cyan('★')} KTX finished ingesting demo data`,
|
|
'',
|
|
' Placeholder — final counts will come from pre-packaged demo results.',
|
|
'',
|
|
` ${dim('Press Enter to continue, Escape to go back')}`,
|
|
'',
|
|
];
|
|
return lines.join('\n');
|
|
}
|
|
```
|
|
|
|
Note: `renderDemoContextCompletionSummary` is a placeholder that will be updated when
|
|
the user provides the real pre-packaged demo data. The summary counts (business areas,
|
|
query definitions, knowledge pages) will be populated from those assets.
|
|
|
|
- [ ] **Step 6: Run tests and type-check**
|
|
|
|
Run: `pnpm --filter @ktx/cli run type-check && pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```bash
|
|
git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts
|
|
git commit -m "feat(cli): add demo context build replay animation"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Add transition message and completion summary
|
|
|
|
**Files:**
|
|
- Modify: `packages/cli/src/setup-demo-tour.ts`
|
|
- Modify: `packages/cli/src/setup-demo-tour.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing tests**
|
|
|
|
Append to test file:
|
|
|
|
```typescript
|
|
import { renderDemoAgentTransition, renderDemoCompletionSummary } from './setup-demo-tour.js';
|
|
|
|
describe('renderDemoAgentTransition', () => {
|
|
it('includes transition message about connecting agent', () => {
|
|
const output = renderDemoAgentTransition();
|
|
expect(output).toContain('Demo project is ready');
|
|
expect(output).toContain('connect your agent');
|
|
});
|
|
});
|
|
|
|
describe('renderDemoCompletionSummary', () => {
|
|
it('includes project path and temp warning', () => {
|
|
const output = renderDemoCompletionSummary('/tmp/ktx-demo-abc123', true);
|
|
expect(output).toContain('/tmp/ktx-demo-abc123');
|
|
expect(output).toContain('temporary');
|
|
expect(output).toContain('ktx setup');
|
|
});
|
|
|
|
it('shows manual agent instructions when agent not installed', () => {
|
|
const output = renderDemoCompletionSummary('/tmp/ktx-demo-abc123', false);
|
|
expect(output).toContain('ktx setup --agents');
|
|
});
|
|
|
|
it('shows success message when agent installed', () => {
|
|
const output = renderDemoCompletionSummary('/tmp/ktx-demo-abc123', true);
|
|
expect(output).toContain('agent is connected');
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run the test to verify it fails**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: FAIL — exports not found
|
|
|
|
- [ ] **Step 3: Implement transition and completion rendering**
|
|
|
|
Add to `setup-demo-tour.ts`:
|
|
|
|
```typescript
|
|
export function renderDemoAgentTransition(): string {
|
|
const lines = [
|
|
'',
|
|
`┌ Demo project is ready — let's connect your agent`,
|
|
'│',
|
|
'│ Your KTX context has been built with demo data.',
|
|
'│ Select an agent to start using it.',
|
|
'└',
|
|
'',
|
|
];
|
|
return lines.join('\n');
|
|
}
|
|
|
|
export function renderDemoCompletionSummary(projectDir: string, agentInstalled: boolean): string {
|
|
const lines = [
|
|
'',
|
|
`${cyan('★')} KTX demo is ready`,
|
|
'',
|
|
];
|
|
|
|
if (agentInstalled) {
|
|
lines.push(' Your agent is connected to a demo KTX project.');
|
|
} else {
|
|
lines.push(' Demo project created. Connect an agent to start using it:');
|
|
lines.push(` $ ktx setup --agents --project-dir ${projectDir}`);
|
|
}
|
|
|
|
lines.push(
|
|
'',
|
|
` ${dim('⚠')} This project is in a temporary directory and will be`,
|
|
` cleaned up by your system. To set up KTX with your own`,
|
|
' data, run: ktx setup',
|
|
'',
|
|
` Project: ${projectDir}`,
|
|
'',
|
|
);
|
|
return lines.join('\n');
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run the test to verify it passes**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts
|
|
git commit -m "feat(cli): add demo tour transition and completion summary"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Implement `runDemoTour` orchestrator
|
|
|
|
**Files:**
|
|
- Modify: `packages/cli/src/setup-demo-tour.ts`
|
|
- Modify: `packages/cli/src/setup-demo-tour.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test for the orchestrator**
|
|
|
|
Append to test file:
|
|
|
|
```typescript
|
|
import { vi } from 'vitest';
|
|
import type { KtxSetupAgentsResult } from './setup-agents.js';
|
|
import { runDemoTour } from './setup-demo-tour.js';
|
|
|
|
describe('runDemoTour', () => {
|
|
function createMockIo() {
|
|
const chunks: string[] = [];
|
|
return {
|
|
io: {
|
|
stdout: { isTTY: true, columns: 80, write: (chunk: string) => { chunks.push(chunk); } },
|
|
stderr: { write: () => {} },
|
|
},
|
|
chunks,
|
|
};
|
|
}
|
|
|
|
it('returns 0 on successful tour with agent installed', async () => {
|
|
const { io } = createMockIo();
|
|
const mockAgents = vi.fn<() => Promise<KtxSetupAgentsResult>>().mockResolvedValue({
|
|
status: 'ready',
|
|
projectDir: '/tmp/test',
|
|
installs: [{ target: 'claude-code' as const, scope: 'project' as const, mode: 'both' as const }],
|
|
});
|
|
|
|
const navigation = vi.fn<() => Promise<'forward' | 'back'>>().mockResolvedValue('forward');
|
|
|
|
const result = await runDemoTour(
|
|
{ inputMode: 'auto' },
|
|
io,
|
|
{ agents: mockAgents, waitForNavigation: navigation, skipReplayAnimation: true },
|
|
);
|
|
expect(result).toBe(0);
|
|
expect(mockAgents).toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles back navigation from first step', async () => {
|
|
const { io } = createMockIo();
|
|
const navigation = vi.fn<() => Promise<'forward' | 'back'>>().mockResolvedValue('back');
|
|
|
|
const result = await runDemoTour(
|
|
{ inputMode: 'auto' },
|
|
io,
|
|
{ waitForNavigation: navigation, skipReplayAnimation: true },
|
|
);
|
|
expect(result).toBe(0);
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run the test to verify it fails**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: FAIL — `runDemoTour` not exported or wrong signature
|
|
|
|
- [ ] **Step 3: Implement `runDemoTour`**
|
|
|
|
Add to `setup-demo-tour.ts`:
|
|
|
|
```typescript
|
|
import { defaultDemoProjectDir, ensureSeededDemoProject } from './demo-assets.js';
|
|
import type { KtxSetupAgentsResult } from './setup-agents.js';
|
|
import { runKtxSetupAgentsStep } from './setup-agents.js';
|
|
|
|
type DemoStep = 'databases' | 'sources' | 'context' | 'agents';
|
|
|
|
const DEMO_STEPS: DemoStep[] = ['databases', 'sources', 'context', 'agents'];
|
|
|
|
export interface DemoTourDeps {
|
|
agents?: (args: Parameters<typeof runKtxSetupAgentsStep>[0], io: KtxCliIo) => Promise<KtxSetupAgentsResult>;
|
|
waitForNavigation?: (stdin?: NodeJS.ReadStream) => Promise<'forward' | 'back'>;
|
|
ensureProject?: typeof ensureSeededDemoProject;
|
|
skipReplayAnimation?: boolean;
|
|
}
|
|
|
|
export async function runDemoTour(
|
|
args: { inputMode: 'auto' | 'disabled' },
|
|
io: KtxCliIo,
|
|
deps: DemoTourDeps = {},
|
|
): Promise<number> {
|
|
const waitNav = deps.waitForNavigation ?? waitForDemoNavigation;
|
|
const ensureProject = deps.ensureProject ?? ensureSeededDemoProject;
|
|
|
|
const projectDir = defaultDemoProjectDir();
|
|
await ensureProject({ projectDir });
|
|
|
|
let stepIndex = 0;
|
|
|
|
while (stepIndex < DEMO_STEPS.length) {
|
|
const step = DEMO_STEPS[stepIndex]!;
|
|
let direction: 'forward' | 'back';
|
|
|
|
if (step === 'databases') {
|
|
direction = await renderDemoCard('Database connection', ['PostgreSQL (demo warehouse)'], io, undefined, waitNav);
|
|
} else if (step === 'sources') {
|
|
direction = await renderDemoCard('Context sources', ['dbt', 'Metabase', 'Notion'], io, undefined, waitNav);
|
|
} else if (step === 'context') {
|
|
io.stdout.write(renderDemoBanner());
|
|
if (deps.skipReplayAnimation) {
|
|
direction = await waitNav();
|
|
} else {
|
|
direction = await runDemoContextReplay(io);
|
|
}
|
|
} else {
|
|
io.stdout.write(renderDemoAgentTransition());
|
|
const agentsRunner = deps.agents ?? runKtxSetupAgentsStep;
|
|
const agentsResult = await agentsRunner(
|
|
{
|
|
projectDir,
|
|
inputMode: args.inputMode,
|
|
yes: false,
|
|
agents: true,
|
|
scope: 'project',
|
|
mode: 'both',
|
|
skipAgents: false,
|
|
},
|
|
io,
|
|
);
|
|
const agentInstalled = agentsResult.status === 'ready';
|
|
if (agentsResult.status === 'back') {
|
|
direction = 'back';
|
|
} else {
|
|
io.stdout.write(renderDemoCompletionSummary(projectDir, agentInstalled));
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (direction === 'back') {
|
|
if (stepIndex === 0) return 0;
|
|
stepIndex -= 1;
|
|
} else {
|
|
stepIndex += 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run the test to verify it passes**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Run type-check**
|
|
|
|
Run: `pnpm --filter @ktx/cli run type-check`
|
|
Expected: PASS — all types align with existing interfaces
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts
|
|
git commit -m "feat(cli): add runDemoTour orchestrator with step navigation"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Wire up in `setup.ts`
|
|
|
|
**Files:**
|
|
- Modify: `packages/cli/src/setup.ts`
|
|
|
|
- [ ] **Step 1: Read the current `runKtxSetupDemoFromEntryMenu` function**
|
|
|
|
Read `packages/cli/src/setup.ts` and locate `runKtxSetupDemoFromEntryMenu` (around lines 218-233).
|
|
|
|
Current implementation:
|
|
```typescript
|
|
async function runKtxSetupDemoFromEntryMenu(
|
|
args: Extract<KtxSetupArgs, { command: 'run' }>,
|
|
io: KtxCliIo,
|
|
deps: KtxSetupDeps,
|
|
): Promise<number> {
|
|
const runner = deps.demo ?? (await import('./demo.js')).runKtxDemo;
|
|
return await runner(
|
|
{
|
|
command: 'seeded',
|
|
projectDir: defaultDemoProjectDir(),
|
|
outputMode: 'viz',
|
|
inputMode: args.inputMode,
|
|
},
|
|
io,
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Replace with demo tour call**
|
|
|
|
Replace the function body to call `runDemoTour`:
|
|
|
|
```typescript
|
|
async function runKtxSetupDemoFromEntryMenu(
|
|
args: Extract<KtxSetupArgs, { command: 'run' }>,
|
|
io: KtxCliIo,
|
|
deps: KtxSetupDeps,
|
|
): Promise<number> {
|
|
const { runDemoTour } = await import('./setup-demo-tour.js');
|
|
return await runDemoTour(
|
|
{ inputMode: args.inputMode },
|
|
io,
|
|
{ agents: deps.agents },
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Update imports — remove unused `defaultDemoProjectDir` import if no longer needed elsewhere in setup.ts**
|
|
|
|
Check if `defaultDemoProjectDir` is used elsewhere in `setup.ts`. If it's only used
|
|
in `runKtxSetupDemoFromEntryMenu`, remove the import. If used elsewhere, keep it.
|
|
|
|
Also check if the `KtxDemoArgs` import is still needed. If `runKtxSetupDemoFromEntryMenu`
|
|
was the only consumer of `deps.demo` with that type, it may now be unused. Keep the
|
|
`demo` slot in `KtxSetupDeps` for backwards compatibility but it will no longer be
|
|
called from the entry menu path.
|
|
|
|
- [ ] **Step 4: Run type-check and tests**
|
|
|
|
Run: `pnpm --filter @ktx/cli run type-check && pnpm --filter @ktx/cli run test`
|
|
Expected: PASS — existing tests continue to work, demo tour is now wired in
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/cli/src/setup.ts
|
|
git commit -m "feat(cli): wire demo tour into setup entry menu"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: End-to-end verification
|
|
|
|
**Files:**
|
|
- None (verification only)
|
|
|
|
- [ ] **Step 1: Run full test suite**
|
|
|
|
Run: `pnpm --filter @ktx/cli run test 2>&1 | tee /tmp/ktx-demo-tour-test.log`
|
|
Expected: All tests pass. Check the output for any regressions.
|
|
|
|
- [ ] **Step 2: Run type-check across workspace**
|
|
|
|
Run: `pnpm run type-check`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 3: Run pre-commit checks if available**
|
|
|
|
Run: `pnpm run check` (if configured)
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 4: Manual smoke test (if TTY available)**
|
|
|
|
Run: `pnpm --filter @ktx/cli run build && node packages/cli/dist/cli.js setup`
|
|
|
|
1. Select "Try KTX with packaged demo data"
|
|
2. Verify demo banner appears with full explanation text
|
|
3. Verify "Database connection" card shows with "PostgreSQL (demo warehouse)"
|
|
4. Press Enter → verify "Context sources" card shows with dbt, Metabase, Notion
|
|
5. Press Escape → verify you go back to database card
|
|
6. Press Enter twice → verify context build replay animation runs
|
|
7. Verify completion summary appears after replay
|
|
8. Press Enter → verify agents step prompt appears (interactive)
|
|
9. Press Escape all the way back → verify you return to entry menu
|
|
|
|
- [ ] **Step 5: Final commit if any adjustments needed**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "fix(cli): demo tour adjustments from smoke test"
|
|
```
|
|
|
|
---
|
|
|
|
## Open Seams for Demo Data
|
|
|
|
When the user provides the real pre-packaged demo results, update these locations:
|
|
|
|
1. **`renderDemoContextCompletionSummary()`** in `setup-demo-tour.ts` — replace placeholder text with actual counts (business areas, query definitions, knowledge pages) from the demo data
|
|
2. **`buildDemoReplayTimeline()`** in `setup-demo-tour.ts` — adjust timing and progress details to match the real ingestion profile
|
|
3. **`demo-assets.ts`** — update `REQUIRED_SEEDED_ASSET_PATHS` and `demoConfig()` if the demo dataset changes from SQLite/Orbit to Postgres/dbt/Metabase/Notion
|
|
4. **Pre-packaged asset files** in `packages/cli/assets/demo/` — replace with the new demo dataset
|