mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
docs: add demo guided tour implementation plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6e2a6b7611
commit
e52713ca1e
1 changed files with 813 additions and 0 deletions
813
docs/superpowers/plans/2026-05-11-demo-guided-tour.md
Normal file
813
docs/superpowers/plans/2026-05-11-demo-guided-tour.md
Normal file
|
|
@ -0,0 +1,813 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue