Files
Codewalkers/apps/server/preview-seed.ts
Lukas May 66605da30d feat: Add Dockerfile, preview config, and seed script for self-preview deployments
Containerize Codewalkers with a multi-stage Docker build (Node + Caddy) and
add a seed script that populates the database with a demo initiative, 3 phases,
9 tasks, 3 agents with JSONL log output, a root page, review comments, and a
git repo with real branch diffs for the review tab.
2026-03-05 14:26:22 +01:00

483 lines
18 KiB
TypeScript

/**
* Preview Seed Script
*
* Populates the database with demo data for preview deployments.
* Run with: CW_DB_PATH=/workspace/.cw/cw.db node /app/apps/server/dist/preview-seed.js
*
* Prints the project ID to stdout so the calling shell script
* can derive the clone path.
*/
import { createDatabase, ensureSchema } from './db/index.js';
import { createRepositories } from './container.js';
const dbPath = process.env.CW_DB_PATH;
if (!dbPath) {
console.error('CW_DB_PATH environment variable is required');
process.exit(1);
}
const db = createDatabase(dbPath);
ensureSchema(db);
const repos = createRepositories(db);
// ─── Project ───
const project = await repos.projectRepository.create({
name: 'demo-app',
url: 'file:///workspace/demo-repo.git',
defaultBranch: 'main',
});
// ─── Initiative ───
const initiative = await repos.initiativeRepository.create({
name: 'Task Manager Redesign',
status: 'active',
branch: 'cw/task-manager-redesign',
executionMode: 'review_per_phase',
});
await repos.projectRepository.setInitiativeProjects(initiative.id, [project.id]);
// ─── Phases ───
const phase1 = await repos.phaseRepository.create({
initiativeId: initiative.id,
name: 'Backend API',
status: 'completed',
});
const phase2 = await repos.phaseRepository.create({
initiativeId: initiative.id,
name: 'UI Overhaul',
status: 'pending_review',
});
const phase3 = await repos.phaseRepository.create({
initiativeId: initiative.id,
name: 'Testing & Polish',
status: 'in_progress',
});
// ─── Tasks ───
// Phase 1: Backend API (all completed)
const task1a = await repos.taskRepository.create({
phaseId: phase1.id,
initiativeId: initiative.id,
name: 'Research REST vs GraphQL patterns',
category: 'research',
status: 'completed',
description: 'Evaluate REST and GraphQL approaches for the task management API. Consider developer experience, caching, and real-time requirements.',
order: 0,
summary: 'Recommended REST with WebSocket subscriptions for real-time. GraphQL adds complexity without clear benefit for this domain.',
});
const task1b = await repos.taskRepository.create({
phaseId: phase1.id,
initiativeId: initiative.id,
name: 'Implement task CRUD endpoints',
category: 'execute',
status: 'completed',
description: 'Build the core CRUD API for tasks with proper validation and error handling.',
order: 1,
summary: 'Implemented GET/POST/PUT/DELETE /api/tasks with Zod validation, proper HTTP status codes, and pagination support.',
});
const task1c = await repos.taskRepository.create({
phaseId: phase1.id,
initiativeId: initiative.id,
name: 'Add input validation middleware',
category: 'execute',
status: 'completed',
description: 'Create reusable validation middleware using Zod schemas for all API endpoints.',
order: 2,
summary: 'Added validateBody() and validateQuery() middleware with typed error responses.',
});
// Phase 2: UI Overhaul (all completed)
const task2a = await repos.taskRepository.create({
phaseId: phase2.id,
initiativeId: initiative.id,
name: 'Design component hierarchy',
category: 'plan',
status: 'completed',
description: 'Plan the component architecture for the redesigned UI, focusing on reusability and maintainability.',
order: 0,
summary: 'Designed 3-level hierarchy: Layout (Header, Sidebar) → Views (TaskList, TaskDetail) → Primitives (StatusBadge, PriorityTag).',
});
const task2b = await repos.taskRepository.create({
phaseId: phase2.id,
initiativeId: initiative.id,
name: 'Implement responsive header',
category: 'execute',
status: 'completed',
description: 'Build the responsive header component with search functionality and notification bell.',
order: 1,
summary: 'Created Header.tsx with search input, notification dropdown (3 mock items), and user avatar. Fully responsive with mobile hamburger menu.',
});
const task2c = await repos.taskRepository.create({
phaseId: phase2.id,
initiativeId: initiative.id,
name: 'Refactor task list with filters',
category: 'execute',
status: 'completed',
description: 'Rewrite the task list component with status icons, priority badges, and filter controls.',
order: 2,
summary: 'Refactored TaskList.tsx with table layout, status icons, priority badges, assignee avatars, and due dates. Added TaskFilters.tsx with status/priority filter buttons.',
});
// Phase 3: Testing & Polish (mixed statuses)
const task3a = await repos.taskRepository.create({
phaseId: phase3.id,
initiativeId: initiative.id,
name: 'Plan test strategy',
category: 'plan',
status: 'completed',
description: 'Define the testing strategy: unit tests, integration tests, and E2E test coverage targets.',
order: 0,
summary: 'Defined 3-tier strategy: Vitest unit tests (80% coverage), API integration tests with supertest, and Playwright E2E for critical flows.',
});
const task3b = await repos.taskRepository.create({
phaseId: phase3.id,
initiativeId: initiative.id,
name: 'Write integration tests',
category: 'execute',
status: 'in_progress',
description: 'Write integration tests for all API endpoints using supertest.',
order: 1,
});
const task3c = await repos.taskRepository.create({
phaseId: phase3.id,
initiativeId: initiative.id,
name: 'Add error handling',
category: 'execute',
status: 'pending',
description: 'Add global error boundary, API error toasts, and retry logic for failed requests.',
order: 2,
});
// ─── Agents ───
const agent1 = await repos.agentRepository.create({
name: 'keen-falcon',
worktreeId: 'worktrees/keen-falcon',
taskId: task1b.id,
initiativeId: initiative.id,
status: 'stopped',
mode: 'execute',
provider: 'claude',
});
const agent2 = await repos.agentRepository.create({
name: 'swift-otter',
worktreeId: 'worktrees/swift-otter',
taskId: task2c.id,
initiativeId: initiative.id,
status: 'stopped',
mode: 'execute',
provider: 'claude',
});
const agent3 = await repos.agentRepository.create({
name: 'bright-heron',
worktreeId: 'worktrees/bright-heron',
taskId: task3b.id,
initiativeId: initiative.id,
status: 'running',
mode: 'execute',
provider: 'claude',
});
// ─── Agent Log Chunks ───
// Agent 1: keen-falcon (completed backend work)
await repos.logChunkRepository.insertChunk({
agentId: agent1.id,
agentName: 'keen-falcon',
sessionNumber: 1,
content: [
JSON.stringify({ type: 'system', session_id: 'session-kf-001' }),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "I'll start by reading the existing API structure to understand the current patterns." },
{ type: 'tool_use', id: 'toolu_kf1', name: 'Read', input: { file_path: '/app/src/lib/api.ts' } },
],
},
}),
JSON.stringify({
type: 'user',
message: { content: [{ type: 'tool_result', content: "const API_URL = '/api';\n\nexport async function fetchTasks(filter: string) {\n const res = await fetch(`${API_URL}/tasks?filter=${filter}`);\n return res.json();\n}" }] },
tool_use_result: { stdout: "const API_URL = '/api';\n\nexport async function fetchTasks(filter: string) {\n const res = await fetch(`${API_URL}/tasks?filter=${filter}`);\n return res.json();\n}" },
}),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "Now I'll implement the task CRUD endpoints with proper validation. I'll create a structured API client with error handling." },
{ type: 'tool_use', id: 'toolu_kf2', name: 'Write', input: { file_path: '/app/src/server/routes/tasks.ts', content: '// CRUD endpoints implementation...' } },
],
},
}),
JSON.stringify({
type: 'user',
message: { content: [{ type: 'tool_result', content: 'File written successfully' }] },
tool_use_result: { stdout: 'File written successfully' },
}),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "Let me also add the Zod validation schemas and middleware." },
{ type: 'tool_use', id: 'toolu_kf3', name: 'Write', input: { file_path: '/app/src/server/middleware/validate.ts', content: '// Validation middleware...' } },
],
},
}),
JSON.stringify({
type: 'user',
message: { content: [{ type: 'tool_result', content: 'File written successfully' }] },
tool_use_result: { stdout: 'File written successfully' },
}),
JSON.stringify({ type: 'result', is_error: false, total_cost_usd: 0.0847, duration_ms: 32000 }),
].join('\n'),
});
// Agent 2: swift-otter (completed UI overhaul)
await repos.logChunkRepository.insertChunk({
agentId: agent2.id,
agentName: 'swift-otter',
sessionNumber: 1,
content: [
JSON.stringify({ type: 'system', session_id: 'session-so-001' }),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "I'll start by reading the current TaskList component to understand what needs to be refactored." },
{ type: 'tool_use', id: 'toolu_so1', name: 'Read', input: { file_path: '/app/src/components/TaskList.tsx' } },
],
},
}),
JSON.stringify({
type: 'user',
message: {
content: [{
type: 'tool_result',
content: "import { useTasks } from '../hooks/useTasks';\n\nexport function TaskList({ filter }) {\n const { tasks, loading } = useTasks(filter);\n if (loading) return <div>Loading...</div>;\n return (\n <ul className=\"task-list\">\n {tasks.map(task => (\n <li key={task.id}>\n <span>{task.title}</span>\n <span className=\"status\">{task.status}</span>\n </li>\n ))}\n </ul>\n );\n}",
}],
},
tool_use_result: { stdout: "import { useTasks } from '../hooks/useTasks';\n..." },
}),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "The current TaskList is very basic — just a plain unordered list with no visual hierarchy. I'll refactor it into a proper table with status icons, priority badges, assignee avatars, and a loading skeleton." },
{
type: 'tool_use', id: 'toolu_so2', name: 'Edit',
input: {
file_path: '/app/src/components/TaskList.tsx',
old_string: "import { useTasks } from '../hooks/useTasks';",
new_string: "import { useTasks } from '../hooks/useTasks';\n\ninterface Task {\n id: string;\n title: string;\n status: 'active' | 'completed' | 'blocked';\n priority: 'high' | 'medium' | 'low';\n assignee?: string;\n}",
},
},
],
},
}),
JSON.stringify({
type: 'user',
message: { content: [{ type: 'tool_result', content: 'File edited successfully' }] },
tool_use_result: { stdout: 'File edited successfully' },
}),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "Now I'll create the TaskFilters component for status and priority filtering." },
{ type: 'tool_use', id: 'toolu_so3', name: 'Write', input: { file_path: '/app/src/components/TaskFilters.tsx', content: '// TaskFilters component...' } },
],
},
}),
JSON.stringify({
type: 'user',
message: { content: [{ type: 'tool_result', content: 'File written successfully' }] },
tool_use_result: { stdout: 'File written successfully' },
}),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "Finally, let me refactor the API client to be type-safe with proper error handling." },
{ type: 'tool_use', id: 'toolu_so4', name: 'Edit', input: { file_path: '/app/src/lib/api.ts', old_string: "const API_URL = '/api';", new_string: 'class ApiClient {\n private baseUrl: string;\n constructor(baseUrl = \'/api\') { this.baseUrl = baseUrl; }' } },
],
},
}),
JSON.stringify({
type: 'user',
message: { content: [{ type: 'tool_result', content: 'File edited successfully' }] },
tool_use_result: { stdout: 'File edited successfully' },
}),
JSON.stringify({ type: 'result', is_error: false, total_cost_usd: 0.1234, duration_ms: 45000 }),
].join('\n'),
});
// Agent 3: bright-heron (in-progress, no result line)
await repos.logChunkRepository.insertChunk({
agentId: agent3.id,
agentName: 'bright-heron',
sessionNumber: 1,
content: [
JSON.stringify({ type: 'system', session_id: 'session-bh-001' }),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "I'll start writing integration tests for the task CRUD endpoints. Let me first check the existing test setup." },
{ type: 'tool_use', id: 'toolu_bh1', name: 'Bash', input: { command: 'ls src/test/', description: 'Check existing test files' } },
],
},
}),
JSON.stringify({
type: 'user',
message: { content: [{ type: 'tool_result', content: 'setup.ts\nfixtures/' }] },
tool_use_result: { stdout: 'setup.ts\nfixtures/' },
}),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "Good, there's a test setup file and fixtures directory. Let me read the setup to understand the test configuration." },
{ type: 'tool_use', id: 'toolu_bh2', name: 'Read', input: { file_path: '/app/src/test/setup.ts' } },
],
},
}),
JSON.stringify({
type: 'user',
message: { content: [{ type: 'tool_result', content: "import { beforeAll, afterAll } from 'vitest';\nimport { createTestServer } from './fixtures/server';\n\nlet server;\nbeforeAll(async () => { server = await createTestServer(); });\nafterAll(() => server?.close());" }] },
tool_use_result: { stdout: "import { beforeAll, afterAll } from 'vitest';\n..." },
}),
JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: "Now I'll write the integration tests for the tasks API. Starting with the CRUD operations..." },
{ type: 'tool_use', id: 'toolu_bh3', name: 'Write', input: { file_path: '/app/src/test/tasks.test.ts', content: '// Integration tests...' } },
],
},
}),
JSON.stringify({
type: 'user',
message: { content: [{ type: 'tool_result', content: 'File written successfully' }] },
tool_use_result: { stdout: 'File written successfully' },
}),
].join('\n'),
});
// ─── Root Page ───
const pageContent = {
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Task Manager Redesign' }],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'A comprehensive redesign of the task management application, focusing on improved user experience, type safety, and modern UI patterns.',
},
],
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Goals' }],
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Responsive layout with mobile-first design' }] }],
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Type-safe API client with proper error handling' }] }],
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Status icons and priority badges for visual clarity' }] }],
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Comprehensive test coverage (unit, integration, E2E)' }] }],
},
],
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Progress' }],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Backend API is complete with full CRUD endpoints and validation middleware. The UI overhaul phase is in review — header, task list, and filters have been refactored. Testing phase is actively in progress with integration tests being written.',
},
],
},
],
};
await repos.pageRepository.create({
initiativeId: initiative.id,
title: 'Task Manager Redesign',
content: JSON.stringify(pageContent),
sortOrder: 0,
});
// ─── Review Comments (on Phase 2) ───
await repos.reviewCommentRepository.create({
phaseId: phase2.id,
filePath: 'src/components/TaskList.tsx',
lineNumber: 12,
lineType: 'added',
body: 'Should we memoize the STATUS_ICONS and PRIORITY_COLORS objects since they are static? Moving them outside the component is good, but wrapping the component in React.memo might help with re-renders.',
author: 'you',
});
await repos.reviewCommentRepository.create({
phaseId: phase2.id,
filePath: 'src/components/Header.tsx',
lineNumber: 8,
lineType: 'added',
body: 'The notifications are hardcoded. For the demo this is fine, but we should add a TODO to wire this up to a real notification system.',
author: 'you',
});
await repos.reviewCommentRepository.create({
phaseId: phase2.id,
filePath: 'src/lib/api.ts',
lineNumber: 25,
lineType: 'added',
body: 'Nice error handling pattern. Could we also add request timeout support? Long-running requests should fail gracefully.',
author: 'you',
});
// Print project ID for the shell script
process.stdout.write(project.id);