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.
578 lines
14 KiB
Bash
Executable File
578 lines
14 KiB
Bash
Executable File
#!/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 ==="
|