#!/usr/bin/env node import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const taskmasterPath = path.join(repoRoot, ".taskmaster", "tasks", "tasks.json"); const outputDir = path.join(repoRoot, "Taskmaster"); const taskDir = path.join(outputDir, "Tasks"); const STATUS_CHECKBOX = { done: "x", completed: "x", }; const PRIORITY_TAG = { high: "#priority/high", medium: "#priority/medium", low: "#priority/low", }; const PRIORITY_EMOJI = { highest: "🔺", high: "⏫", medium: "🔼", low: "🔽", lowest: "⏬", }; const source = JSON.parse(await readFile(taskmasterPath, "utf8")); const tasks = source.master?.tasks ?? source.tasks ?? []; const generatedAt = new Date().toISOString(); await rm(outputDir, { recursive: true, force: true }); await mkdir(taskDir, { recursive: true }); const flatTasks = tasks.flatMap((task) => { const parent = normalizeTask(task); const subtasks = (task.subtasks ?? []).map((subtask) => normalizeTask({ ...subtask, id: `${task.id}.${subtask.id}`, parentId: task.id, parentTitle: task.title, priority: subtask.priority ?? task.priority, }), ); return [parent, ...subtasks]; }); for (const task of flatTasks) { await writeFile(path.join(taskDir, `${slugTaskId(task.id)}.md`), renderTaskNote(task)); } await writeFile(path.join(outputDir, "README.md"), renderDashboard(flatTasks)); await writeFile(path.join(outputDir, "tasks.md"), renderTasksFile(flatTasks)); console.log(`Exported ${flatTasks.length} Taskmaster tasks to ${path.relative(repoRoot, outputDir)}`); function normalizeTask(task) { return { id: String(task.id), title: task.title ?? "Untitled task", description: task.description ?? "", details: task.details ?? "", testStrategy: task.testStrategy ?? "", status: task.status ?? "pending", priority: task.priority ?? "medium", dependencies: (task.dependencies ?? []).map(String), parentId: task.parentId && task.parentId !== "undefined" ? String(task.parentId) : "", parentTitle: task.parentTitle ?? "", }; } function renderTaskNote(task) { return `--- taskmaster_id: "${yamlEscape(task.id)}" status: "${yamlEscape(task.status)}" priority: "${yamlEscape(task.priority)}" depends_on: [${task.dependencies.map((id) => `"${yamlEscape(id)}"`).join(", ")}] parent_id: "${yamlEscape(task.parentId)}" source: "taskmaster" generated_at: "${generatedAt}" --- # ${task.id} - ${task.title} ${renderObsidianTaskLine(task)} ## Metadata | Field | Value | | --- | --- | | Taskmaster ID | ${task.id} | | Status | ${task.status} | | Priority | ${task.priority} | | Dependencies | ${task.dependencies.length ? task.dependencies.join(", ") : "None"} | | Parent | ${task.parentId ? `${task.parentId} - ${task.parentTitle}` : "None"} | ## Description ${task.description || "_No description._"} ## Details ${task.details || "_No details._"} ## Verification ${task.testStrategy || "_No verification strategy._"} `; } function renderDashboard(tasks) { const rows = tasks .map((task) => `| [[Tasks/${slugTaskId(task.id)}|${task.id}]] | ${escapeTable(task.title)} | ${task.status} | ${task.priority} | ${task.dependencies.length ? task.dependencies.join(", ") : "None"} |`) .join("\n"); return `# Taskmaster Dashboard Generated from \`.taskmaster/tasks/tasks.json\` at ${generatedAt}. Taskmaster remains the canonical source of truth. Re-run: \`\`\`sh node scripts/export-taskmaster-to-obsidian.mjs \`\`\` ## Status Summary ${renderStatusSummary(tasks)} ## Task Index | ID | Title | Status | Priority | Dependencies | | --- | --- | --- | --- | --- | ${rows} ## Obsidian Tasks Query \`\`\`tasks not done tag includes #taskmaster sort by priority sort by description \`\`\` `; } function renderTasksFile(tasks) { const lines = tasks.map(renderObsidianTaskLine).join("\n"); return `# Taskmaster Tasks Generated from \`.taskmaster/tasks/tasks.json\` at ${generatedAt}. These lines use the Obsidian Tasks emoji format: - standard Markdown checkbox syntax - \`#taskmaster\` tag for filtering - priority emoji where available - \`🆔\` task IDs - \`⛔\` dependency IDs ${lines} `; } function renderObsidianTaskLine(task) { const checkbox = STATUS_CHECKBOX[task.status] ?? " "; const priorityTag = PRIORITY_TAG[task.priority] ?? "#priority/none"; const priorityEmoji = PRIORITY_EMOJI[task.priority] ?? ""; const dependencyMarkers = task.dependencies.map((id) => `⛔ tm-${safeId(id)}`).join(" "); const fields = [ "#taskmaster", priorityTag, `#status/${tagValue(task.status)}`, priorityEmoji, `🆔 tm-${safeId(task.id)}`, dependencyMarkers, ].filter(Boolean); return `- [${checkbox}] ${task.id} - ${task.title} ${fields.join(" ")}`; } function renderStatusSummary(tasks) { const counts = tasks.reduce((acc, task) => { acc[task.status] = (acc[task.status] ?? 0) + 1; return acc; }, {}); return Object.entries(counts) .sort(([a], [b]) => a.localeCompare(b)) .map(([status, count]) => `- ${status}: ${count}`) .join("\n"); } function slugTaskId(id) { return `task-${safeId(id)}`; } function safeId(id) { return String(id).replace(/[^A-Za-z0-9_-]+/g, "-"); } function tagValue(value) { return String(value || "none").toLowerCase().replace(/[^a-z0-9_-]+/g, "-"); } function yamlEscape(value) { return String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"'); } function escapeTable(value) { return String(value ?? "").replace(/\|/g, "\\|").replace(/\n/g, " "); }