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.
This commit is contained in:
11
.cw-preview.yml
Normal file
11
.cw-preview.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: 1
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
port: 3000
|
||||
healthcheck:
|
||||
path: /health
|
||||
interval: 3s
|
||||
retries: 30
|
||||
seed:
|
||||
- sh /app/scripts/seed-preview.sh
|
||||
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
.cw/
|
||||
workdir/
|
||||
.git/
|
||||
apps/server/dist/
|
||||
apps/web/dist/
|
||||
coverage/
|
||||
*.log
|
||||
.env*
|
||||
.DS_Store
|
||||
.screenshots/
|
||||
.cw-previews/
|
||||
48
Dockerfile
Normal file
48
Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
||||
# Stage 1: Install dependencies (native addons need build tools)
|
||||
FROM node:20-alpine AS deps
|
||||
RUN apk add --no-cache python3 make g++
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
COPY apps/web/package.json apps/web/package.json
|
||||
COPY packages/shared/package.json packages/shared/package.json
|
||||
RUN npm ci
|
||||
|
||||
# Stage 2: Build everything (web needs server types via packages/shared)
|
||||
FROM deps AS build
|
||||
COPY tsconfig.json ./
|
||||
COPY apps/server/ apps/server/
|
||||
COPY apps/web/ apps/web/
|
||||
COPY packages/shared/ packages/shared/
|
||||
# Server build (tsc → apps/server/dist/)
|
||||
RUN npm run build
|
||||
# Web build (vite bundle → apps/web/dist/)
|
||||
RUN npx --workspace=apps/web vite build
|
||||
|
||||
# Stage 3: Production image
|
||||
FROM node:20-alpine AS prod
|
||||
RUN apk add --no-cache git curl jq
|
||||
COPY --from=caddy:2-alpine /usr/bin/caddy /usr/bin/caddy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Node modules (includes native better-sqlite3)
|
||||
COPY --from=deps /app/node_modules/ node_modules/
|
||||
COPY package.json ./
|
||||
|
||||
# Server build output + drizzle migrations
|
||||
COPY --from=build /app/apps/server/dist/ apps/server/dist/
|
||||
COPY apps/server/drizzle/ apps/server/drizzle/
|
||||
|
||||
# Web build output
|
||||
COPY --from=build /app/apps/web/dist/ apps/web/dist/
|
||||
|
||||
# Scripts and config
|
||||
COPY scripts/ scripts/
|
||||
COPY scripts/Caddyfile /etc/caddy/Caddyfile
|
||||
RUN chmod +x scripts/*.sh
|
||||
|
||||
# Workspace volume
|
||||
RUN mkdir -p /workspace
|
||||
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["sh", "/app/scripts/entrypoint.sh"]
|
||||
482
apps/server/preview-seed.ts
Normal file
482
apps/server/preview-seed.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* 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);
|
||||
@@ -358,6 +358,47 @@ Polls `getPreviewStatus` with `refetchInterval: 3000` while active.
|
||||
- `GracefulShutdown` calls `previewManager.stopAll()` during shutdown
|
||||
- `requirePreviewManager(ctx)` helper in `apps/server/trpc/routers/_helpers.ts`
|
||||
|
||||
## Codewalkers Self-Preview
|
||||
|
||||
Codewalkers itself has a Dockerfile, `.cw-preview.yml`, and seed script for preview deployments. This lets you demo the full app — initiatives, phases, tasks, agents with output, pages, and review tab with real git diffs.
|
||||
|
||||
### Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Dockerfile` | Multi-stage: deps → build (server+web) → production (Node + Caddy) |
|
||||
| `.cw-preview.yml` | Preview config: build, healthcheck, seed |
|
||||
| `scripts/Caddyfile` | Caddy config: SPA file server + tRPC reverse proxy |
|
||||
| `scripts/entrypoint.sh` | Init workspace, start backend, run Caddy |
|
||||
| `scripts/seed-preview.sh` | Create demo git repo + run Node.js seed |
|
||||
| `apps/server/preview-seed.ts` | Populate DB with demo initiative, phases, tasks, agents, log chunks, pages, review comments |
|
||||
|
||||
### Container Architecture
|
||||
|
||||
Single container running two processes:
|
||||
- **Node.js backend** on port 3847 (tRPC API + health endpoint)
|
||||
- **Caddy** on port 3000 (SPA file server, reverse proxy `/trpc` and `/health` to backend)
|
||||
|
||||
### Seed Data
|
||||
|
||||
The seed creates a "Task Manager Redesign" initiative with:
|
||||
- 3 phases (completed, pending_review, in_progress)
|
||||
- 9 tasks across phases
|
||||
- 3 agents with JSONL log output
|
||||
- Root page with Tiptap content
|
||||
- 3 review comments on the pending_review phase
|
||||
- Git repo with 3 branches and real commit diffs for the review tab
|
||||
|
||||
### Manual Testing
|
||||
|
||||
```sh
|
||||
docker build -t cw-preview .
|
||||
docker run -p 3000:3000 cw-preview
|
||||
# Wait for startup, then seed:
|
||||
docker exec <container> sh /app/scripts/seed-preview.sh
|
||||
# Browse http://localhost:3000
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `js-yaml` + `@types/js-yaml` — for parsing `.cw-preview.yml`
|
||||
|
||||
15
scripts/Caddyfile
Normal file
15
scripts/Caddyfile
Normal file
@@ -0,0 +1,15 @@
|
||||
:3000 {
|
||||
handle /trpc/* {
|
||||
reverse_proxy localhost:3847
|
||||
}
|
||||
|
||||
handle /health {
|
||||
reverse_proxy localhost:3847
|
||||
}
|
||||
|
||||
handle {
|
||||
root * /app/apps/web/dist
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
34
scripts/entrypoint.sh
Executable file
34
scripts/entrypoint.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# 1. Initialize workspace
|
||||
mkdir -p /workspace/.cw
|
||||
echo '{"version":1}' > /workspace/.cwrc
|
||||
|
||||
# 2. Git globals
|
||||
git config --global user.email "preview@codewalkers.dev"
|
||||
git config --global user.name "Codewalkers Preview"
|
||||
git config --global protocol.file.allow always
|
||||
git config --global init.defaultBranch main
|
||||
|
||||
# 3. Start backend
|
||||
cd /workspace
|
||||
CW_DB_PATH=/workspace/.cw/cw.db node /app/apps/server/dist/bin/cw.js --server &
|
||||
BACKEND_PID=$!
|
||||
|
||||
# 4. Wait for health
|
||||
echo "Waiting for backend to start..."
|
||||
TRIES=0
|
||||
MAX_TRIES=60
|
||||
until curl -sf http://localhost:3847/health > /dev/null 2>&1; do
|
||||
TRIES=$((TRIES + 1))
|
||||
if [ "$TRIES" -ge "$MAX_TRIES" ]; then
|
||||
echo "Backend failed to start after ${MAX_TRIES}s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "Backend is healthy"
|
||||
|
||||
# 5. Run Caddy in foreground (receives signals)
|
||||
exec caddy run --config /etc/caddy/Caddyfile
|
||||
577
scripts/seed-preview.sh
Executable file
577
scripts/seed-preview.sh
Executable file
@@ -0,0 +1,577 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "=== Creating demo git repository ==="
|
||||
|
||||
BARE_REPO="/workspace/demo-repo.git"
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
|
||||
# Initialize bare repo
|
||||
git init --bare "$BARE_REPO"
|
||||
|
||||
cd "$TEMP_DIR"
|
||||
git init -b main
|
||||
git remote add origin "$BARE_REPO"
|
||||
|
||||
# ─── main branch: initial task manager app ───
|
||||
|
||||
cat > README.md << 'READMEEOF'
|
||||
# Task Manager
|
||||
|
||||
A modern task management application built with React and TypeScript.
|
||||
|
||||
## Features
|
||||
|
||||
- Create, update, and delete tasks
|
||||
- Filter by status and priority
|
||||
- Real-time updates
|
||||
- Responsive design
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
READMEEOF
|
||||
|
||||
cat > package.json << 'PKGEOF'
|
||||
{
|
||||
"name": "task-manager",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
PKGEOF
|
||||
|
||||
mkdir -p src/components src/hooks src/lib
|
||||
|
||||
cat > src/app.tsx << 'APPEOF'
|
||||
import { useState } from 'react';
|
||||
import { TaskList } from './components/TaskList';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
|
||||
export function App() {
|
||||
const [filter, setFilter] = useState('all');
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Sidebar filter={filter} onFilterChange={setFilter} />
|
||||
<main>
|
||||
<h1>Task Manager</h1>
|
||||
<TaskList filter={filter} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
APPEOF
|
||||
|
||||
cat > src/components/TaskList.tsx << 'TLEOF'
|
||||
import { useTasks } from '../hooks/useTasks';
|
||||
|
||||
interface TaskListProps {
|
||||
filter: string;
|
||||
}
|
||||
|
||||
export function TaskList({ filter }: TaskListProps) {
|
||||
const { tasks, loading } = useTasks(filter);
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<ul className="task-list">
|
||||
{tasks.map(task => (
|
||||
<li key={task.id}>
|
||||
<span>{task.title}</span>
|
||||
<span className="status">{task.status}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
TLEOF
|
||||
|
||||
cat > src/components/Sidebar.tsx << 'SBEOF'
|
||||
interface SidebarProps {
|
||||
filter: string;
|
||||
onFilterChange: (filter: string) => void;
|
||||
}
|
||||
|
||||
export function Sidebar({ filter, onFilterChange }: SidebarProps) {
|
||||
const filters = ['all', 'active', 'completed'];
|
||||
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<h2>Filters</h2>
|
||||
<ul>
|
||||
{filters.map(f => (
|
||||
<li key={f}>
|
||||
<button
|
||||
className={f === filter ? 'active' : ''}
|
||||
onClick={() => onFilterChange(f)}
|
||||
>
|
||||
{f}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
SBEOF
|
||||
|
||||
cat > src/hooks/useTasks.ts << 'HTEOF'
|
||||
import { useState, useEffect } from 'react';
|
||||
import { fetchTasks } from '../lib/api';
|
||||
|
||||
export function useTasks(filter: string) {
|
||||
const [tasks, setTasks] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks(filter).then(data => {
|
||||
setTasks(data);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [filter]);
|
||||
|
||||
return { tasks, loading };
|
||||
}
|
||||
HTEOF
|
||||
|
||||
cat > src/lib/api.ts << 'APIEOF'
|
||||
const API_URL = '/api';
|
||||
|
||||
export async function fetchTasks(filter: string) {
|
||||
const res = await fetch(`${API_URL}/tasks?filter=${filter}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createTask(title: string) {
|
||||
const res = await fetch(`${API_URL}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title }),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
APIEOF
|
||||
|
||||
git add -A
|
||||
git commit -m "feat: initial task manager setup"
|
||||
git push origin main
|
||||
|
||||
# ─── initiative branch (same as main, no extra commits) ───
|
||||
|
||||
git checkout -b cw/task-manager-redesign
|
||||
git push origin cw/task-manager-redesign
|
||||
|
||||
# ─── phase branch: UI overhaul with 3 commits ───
|
||||
|
||||
git checkout -b cw/task-manager-redesign-phase-ui-overhaul
|
||||
|
||||
# Commit 1: responsive header with search and notifications
|
||||
mkdir -p src/components
|
||||
|
||||
cat > src/components/Header.tsx << 'HDREOF'
|
||||
import { useState } from 'react';
|
||||
|
||||
interface HeaderProps {
|
||||
onSearch: (query: string) => void;
|
||||
}
|
||||
|
||||
export function Header({ onSearch }: HeaderProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [notifications] = useState([
|
||||
{ id: 1, text: 'Task "Deploy v2" completed', unread: true },
|
||||
{ id: 2, text: 'New comment on "API redesign"', unread: true },
|
||||
{ id: 3, text: 'Sprint review tomorrow', unread: false },
|
||||
]);
|
||||
|
||||
const unreadCount = notifications.filter(n => n.unread).length;
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header-brand">
|
||||
<h1>TaskFlow</h1>
|
||||
</div>
|
||||
<div className="header-search">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search tasks..."
|
||||
value={query}
|
||||
onChange={e => {
|
||||
setQuery(e.target.value);
|
||||
onSearch(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<button className="notification-bell" aria-label="Notifications">
|
||||
🔔 {unreadCount > 0 && <span className="badge">{unreadCount}</span>}
|
||||
</button>
|
||||
<div className="avatar">JD</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
HDREOF
|
||||
|
||||
cat > src/components/TaskFilters.tsx << 'TFEOF'
|
||||
interface TaskFiltersProps {
|
||||
activeFilter: string;
|
||||
onFilterChange: (filter: string) => void;
|
||||
activePriority: string;
|
||||
onPriorityChange: (priority: string) => void;
|
||||
}
|
||||
|
||||
export function TaskFilters({
|
||||
activeFilter,
|
||||
onFilterChange,
|
||||
activePriority,
|
||||
onPriorityChange,
|
||||
}: TaskFiltersProps) {
|
||||
const statuses = ['all', 'active', 'completed', 'blocked'];
|
||||
const priorities = ['all', 'high', 'medium', 'low'];
|
||||
|
||||
return (
|
||||
<div className="task-filters">
|
||||
<div className="filter-group">
|
||||
<label>Status</label>
|
||||
<div className="filter-buttons">
|
||||
{statuses.map(s => (
|
||||
<button
|
||||
key={s}
|
||||
className={s === activeFilter ? 'active' : ''}
|
||||
onClick={() => onFilterChange(s)}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Priority</label>
|
||||
<div className="filter-buttons">
|
||||
{priorities.map(p => (
|
||||
<button
|
||||
key={p}
|
||||
className={p === activePriority ? 'active' : ''}
|
||||
onClick={() => onPriorityChange(p)}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
TFEOF
|
||||
|
||||
# Update app.tsx to use Header
|
||||
cat > src/app.tsx << 'APPEOF2'
|
||||
import { useState } from 'react';
|
||||
import { Header } from './components/Header';
|
||||
import { TaskList } from './components/TaskList';
|
||||
import { TaskFilters } from './components/TaskFilters';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
|
||||
export function App() {
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [priority, setPriority] = useState('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
return (
|
||||
<div className="app-layout">
|
||||
<Header onSearch={setSearchQuery} />
|
||||
<div className="app-body">
|
||||
<Sidebar filter={filter} onFilterChange={setFilter} />
|
||||
<main className="main-content">
|
||||
<TaskFilters
|
||||
activeFilter={filter}
|
||||
onFilterChange={setFilter}
|
||||
activePriority={priority}
|
||||
onPriorityChange={setPriority}
|
||||
/>
|
||||
<TaskList filter={filter} />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
APPEOF2
|
||||
|
||||
git add -A
|
||||
git commit -m "feat: add responsive header with search and notifications"
|
||||
|
||||
# Commit 2: enhance task list with status icons and priorities
|
||||
cat > src/components/TaskList.tsx << 'TL2EOF'
|
||||
import { useTasks } from '../hooks/useTasks';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'active' | 'completed' | 'blocked';
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
assignee?: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
interface TaskListProps {
|
||||
filter: string;
|
||||
}
|
||||
|
||||
const STATUS_ICONS: Record<string, string> = {
|
||||
active: '🔵',
|
||||
completed: '✅',
|
||||
blocked: '🔴',
|
||||
};
|
||||
|
||||
const PRIORITY_COLORS: Record<string, string> = {
|
||||
high: 'priority-high',
|
||||
medium: 'priority-medium',
|
||||
low: 'priority-low',
|
||||
};
|
||||
|
||||
export function TaskList({ filter }: TaskListProps) {
|
||||
const { tasks, loading } = useTasks(filter);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="task-list-skeleton">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="skeleton-row" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="empty-state">
|
||||
<p>No tasks found</p>
|
||||
<button className="btn-primary">Create your first task</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-list">
|
||||
<div className="task-list-header">
|
||||
<span>Task</span>
|
||||
<span>Priority</span>
|
||||
<span>Assignee</span>
|
||||
<span>Due Date</span>
|
||||
<span>Status</span>
|
||||
</div>
|
||||
{(tasks as Task[]).map(task => (
|
||||
<div key={task.id} className="task-row">
|
||||
<div className="task-title">
|
||||
<span className="status-icon">{STATUS_ICONS[task.status]}</span>
|
||||
<span>{task.title}</span>
|
||||
</div>
|
||||
<span className={`priority-badge ${PRIORITY_COLORS[task.priority]}`}>
|
||||
{task.priority}
|
||||
</span>
|
||||
<span className="assignee">
|
||||
{task.assignee ? (
|
||||
<span className="avatar-small">{task.assignee.slice(0, 2).toUpperCase()}</span>
|
||||
) : (
|
||||
<span className="unassigned">—</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="due-date">{task.dueDate ?? '—'}</span>
|
||||
<span className={`status-badge status-${task.status}`}>
|
||||
{task.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
TL2EOF
|
||||
|
||||
git add -A
|
||||
git commit -m "refactor: enhance task list with status icons and priorities"
|
||||
|
||||
# Commit 3: type-safe API client with error handling
|
||||
cat > src/lib/api.ts << 'API2EOF'
|
||||
interface ApiError {
|
||||
message: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'active' | 'completed' | 'blocked';
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
assignee?: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
interface CreateTaskInput {
|
||||
title: string;
|
||||
priority?: Task['priority'];
|
||||
assignee?: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl = '/api') {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
private async request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error: ApiError = {
|
||||
message: `Request failed: ${response.statusText}`,
|
||||
status: response.status,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getTasks(filter?: string): Promise<Task[]> {
|
||||
const params = filter && filter !== 'all' ? `?filter=${filter}` : '';
|
||||
return this.request<Task[]>(`/tasks${params}`);
|
||||
}
|
||||
|
||||
async getTask(id: string): Promise<Task> {
|
||||
return this.request<Task>(`/tasks/${id}`);
|
||||
}
|
||||
|
||||
async createTask(input: CreateTaskInput): Promise<Task> {
|
||||
return this.request<Task>('/tasks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
async updateTask(id: string, updates: Partial<Task>): Promise<Task> {
|
||||
return this.request<Task>(`/tasks/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteTask(id: string): Promise<void> {
|
||||
await this.request(`/tasks/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ApiClient();
|
||||
API2EOF
|
||||
|
||||
cat > src/hooks/useTasks.ts << 'HT2EOF'
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { api } from '../lib/api';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'active' | 'completed' | 'blocked';
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
assignee?: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
interface UseTasksResult {
|
||||
tasks: Task[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => void;
|
||||
createTask: (title: string) => Promise<void>;
|
||||
deleteTask: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useTasks(filter: string): UseTasksResult {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadTasks = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await api.getTasks(filter);
|
||||
setTasks(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load tasks');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, [loadTasks]);
|
||||
|
||||
const createTask = useCallback(async (title: string) => {
|
||||
await api.createTask({ title });
|
||||
await loadTasks();
|
||||
}, [loadTasks]);
|
||||
|
||||
const deleteTask = useCallback(async (id: string) => {
|
||||
await api.deleteTask(id);
|
||||
await loadTasks();
|
||||
}, [loadTasks]);
|
||||
|
||||
return { tasks, loading, error, refresh: loadTasks, createTask, deleteTask };
|
||||
}
|
||||
HT2EOF
|
||||
|
||||
git add -A
|
||||
git commit -m "refactor: type-safe API client with error handling"
|
||||
|
||||
# Push all branches
|
||||
git push origin cw/task-manager-redesign-phase-ui-overhaul
|
||||
|
||||
# Clean up
|
||||
cd /
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo "=== Demo git repository created at $BARE_REPO ==="
|
||||
|
||||
# ─── Step 2: Run Node.js seed ───
|
||||
|
||||
echo "=== Running database seed ==="
|
||||
PROJECT_ID=$(CW_DB_PATH=/workspace/.cw/cw.db node /app/apps/server/dist/preview-seed.js)
|
||||
echo "=== Seed complete, project ID: $PROJECT_ID ==="
|
||||
|
||||
# ─── Step 3: Set up local branches in the server's clone dir ───
|
||||
|
||||
CLONE_DIR="/workspace/repos/demo-app-${PROJECT_ID}"
|
||||
echo "=== Cloning demo repo to $CLONE_DIR ==="
|
||||
|
||||
mkdir -p /workspace/repos
|
||||
git clone "$BARE_REPO" "$CLONE_DIR"
|
||||
|
||||
echo "=== Setting up local branches in $CLONE_DIR ==="
|
||||
|
||||
cd "$CLONE_DIR"
|
||||
git checkout -b cw/task-manager-redesign origin/cw/task-manager-redesign
|
||||
git checkout -b cw/task-manager-redesign-phase-ui-overhaul origin/cw/task-manager-redesign-phase-ui-overhaul
|
||||
git checkout main
|
||||
|
||||
echo "=== Preview seed complete ==="
|
||||
Reference in New Issue
Block a user