import fs from "node:fs"; import path from "node:path"; import { all, get, run } from "@/lib/db"; import type { TaskPriority, TaskRecord, TaskStatus } from "@/lib/types"; const VALID_STATUSES: TaskStatus[] = [ "Backlog", "Todo", "In Progress", "Review", "Done", ]; const VALID_PRIORITIES: TaskPriority[] = ["Low", "Medium", "High", "Critical"]; const WIKI_DIR = process.env.WIKI_DIR || path.join(process.cwd(), "wiki"); fs.mkdirSync(WIKI_DIR, { recursive: true }); type DatabaseTaskRow = Omit & { tags: string }; export function normalizeTask(row: DatabaseTaskRow): TaskRecord { let tags: string[] = []; try { tags = JSON.parse(row.tags || "[]"); } catch { tags = []; } return { ...row, tags, }; } export async function listTasks() { const rows = await all("SELECT * FROM tasks ORDER BY id DESC"); return rows.map(normalizeTask); } export async function findTask(id: number) { const row = await get("SELECT * FROM tasks WHERE id = ?", [id]); return row ? normalizeTask(row) : null; } export function validateTaskPayload( payload: Partial & { tags?: unknown }, partial = false, ) { const errors: string[] = []; if (!partial || payload.title !== undefined) { if (typeof payload.title !== "string" || payload.title.trim().length === 0) { errors.push("title is required"); } } if (payload.status !== undefined && !VALID_STATUSES.includes(payload.status)) { errors.push(`status must be one of: ${VALID_STATUSES.join(", ")}`); } if (payload.priority !== undefined && !VALID_PRIORITIES.includes(payload.priority)) { errors.push(`priority must be one of: ${VALID_PRIORITIES.join(", ")}`); } if (payload.tags !== undefined && !Array.isArray(payload.tags)) { errors.push("tags must be an array of strings"); } return errors; } function buildWikiMarkdown(task: TaskRecord) { const renderedTags = task.tags.length ? task.tags.join(", ") : "None"; return `# ${task.title} - Task ID: ${task.id} - Assignee: ${task.assignee || "Unassigned"} - Priority: ${task.priority} - Status: ${task.status} - Tags: ${renderedTags} - Created: ${task.created_at} - Completed: ${task.completed_at || new Date().toISOString()} ## Description ${task.description || "No description provided."} `; } async function writeWikiForTask(task: TaskRecord) { const safeTitle = task.title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 80); const fileName = `${new Date().toISOString().slice(0, 10)}-task-${task.id}-${safeTitle || `task-${task.id}`}.md`; fs.writeFileSync(path.join(WIKI_DIR, fileName), buildWikiMarkdown(task), "utf8"); } export async function createTask(input: Partial) { const result = await run( `INSERT INTO tasks (title, description, assignee, priority, status, tags) VALUES (?, ?, ?, ?, ?, ?)`, [ input.title?.trim() || "", input.description || "", input.assignee || "", input.priority || "Medium", input.status || "Backlog", JSON.stringify(Array.isArray(input.tags) ? input.tags.filter((tag) => typeof tag === "string") : []), ], ); const task = await findTask(result.lastID); if (!task) { throw new Error("failed_to_fetch_created_task"); } return task; } export async function updateTask(id: number, input: Partial) { const existing = await findTask(id); if (!existing) { return null; } const nextStatus = input.status ?? existing.status; const completedAt = nextStatus === "Done" ? existing.completed_at || new Date().toISOString() : null; await run( `UPDATE tasks SET title = ?, description = ?, assignee = ?, priority = ?, status = ?, tags = ?, completed_at = ?, updated_at = datetime('now') WHERE id = ?`, [ input.title?.trim() || existing.title, input.description ?? existing.description, input.assignee ?? existing.assignee, input.priority ?? existing.priority, nextStatus, JSON.stringify(input.tags ?? existing.tags), completedAt, id, ], ); const updated = await findTask(id); if (!updated) { throw new Error("failed_to_fetch_updated_task"); } if (nextStatus === "Done" && existing.status !== "Done") { await writeWikiForTask(updated); } return updated; }