import { readFile } from "node:fs/promises"; import type { TaskStatus, TaskTreeCommand } from "../../internal/tasktree/contracts.ts"; import { createTaskTreeDispatcher } from "../../internal/tasktree/dispatcher.ts"; import { loadMdtodoImportPayload } from "../../internal/tasktree/mdtodo-import.ts"; import { taskTreeRuntime } from "../../internal/tasktree/runtime.ts"; export async function runTaskTreeCli(argv: string[], env: Record = process.env) { const parsed = parse(argv); if (parsed.help || parsed.positionals.length === 0) return help(); const command = await commandFrom(parsed); if (parsed.overapi) return overApi(command, env, parsed); const runtime = taskTreeRuntime(env); try { return await createTaskTreeDispatcher(runtime)(command); } finally { await runtime.store.close(); } } function help() { return { ok: true, operation: "help", mode: "local-dispatcher-default", usage: [ "hwlab-cli tasktree health [--overapi]", "hwlab-cli tasktree group list|overview|get|create|delete [--group ID] [--name TEXT]", "hwlab-cli tasktree task create|update|delete --group ID|--task ID [--parent ID] --title TEXT --start ISO --due ISO --status STATUS", "hwlab-cli tasktree milestone create --group ID [--task ID] --title TEXT --at ISO", "hwlab-cli tasktree report create --task ID --title TEXT (--body TEXT|--body-file PATH)", "hwlab-cli tasktree import mdtodo --file PATH [--name TEXT] [--dry-run] [--overapi]", "hwlab-cli tasktree timeline --group ID [--overapi]", "hwlab-cli tasktree workflow start --task ID" ], transportContract: "--overapi only changes transport; commands and options are identical" }; } async function commandFrom(parsed: Parsed): Promise { const [group, action = ""] = parsed.positionals; if (group === "health") return { operation: "health" }; if (group === "timeline") return { operation: "timeline.get", groupId: required(parsed, "group") }; if (group === "group" && action === "list") return { operation: "group.list" }; if (group === "group" && action === "overview") return { operation: "group.overview" }; if (group === "group" && action === "get") return { operation: "group.get", groupId: required(parsed, "group") }; if (group === "group" && action === "create") return { operation: "group.create", name: required(parsed, "name"), description: parsed.values.description }; if (group === "group" && action === "delete") return { operation: "group.delete", groupId: required(parsed, "group") }; if (group === "task" && action === "create") return { operation: "task.create", groupId: required(parsed, "group"), parentId: parsed.values.parent, title: required(parsed, "title"), description: parsed.values.description, startAt: parsed.values.start, dueAt: parsed.values.due }; if (group === "task" && action === "update") return { operation: "task.update", taskId: required(parsed, "task"), title: parsed.values.title, description: parsed.values.description, status: parsed.values.status as TaskStatus | undefined, startAt: parsed.values.start, dueAt: parsed.values.due }; if (group === "task" && action === "delete") return { operation: "task.delete", taskId: required(parsed, "task") }; if (group === "milestone" && action === "create") return { operation: "milestone.create", groupId: required(parsed, "group"), taskId: parsed.values.task, title: required(parsed, "title"), occursAt: required(parsed, "at") }; if (group === "report" && action === "create") return { operation: "report.create", taskId: required(parsed, "task"), title: required(parsed, "title"), body: parsed.values.body ?? await readFile(required(parsed, "body-file"), "utf8"), status: parsed.values.status }; if (group === "import" && action === "mdtodo") return { operation: "mdtodo.import", ...await loadMdtodoImportPayload(required(parsed, "file"), { groupName: parsed.values.name, dryRun: parsed.flags.has("dry-run") }) }; if (group === "workflow" && action === "start") return { operation: "workflow.start", taskId: required(parsed, "task") }; throw new Error(`unsupported TaskTree command: ${group} ${action}`.trim()); } async function overApi(command: TaskTreeCommand, env: Record, parsed: Parsed) { const baseUrl = parsed.values["api-url"] || env.HWLAB_TASKTREE_API_URL; if (!baseUrl) throw new Error("HWLAB_TASKTREE_API_URL or --api-url is required with --overapi"); const response = await fetch(`${baseUrl.replace(/\/$/u, "")}/v1/tasktree/commands`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(command) }); const body = await response.json().catch(() => null); if (!body || typeof body !== "object") throw new Error(`TaskTree API returned invalid JSON (${response.status})`); return { ...(body as object), transport: "api", httpStatus: response.status }; } type Parsed = { positionals: string[]; values: Record; flags: Set; overapi: boolean; help: boolean }; function parse(argv: string[]): Parsed { const parsed: Parsed = { positionals: [], values: {}, flags: new Set(), overapi: false, help: false }; for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; if (token === "--overapi") parsed.overapi = true; else if (token === "--dry-run") parsed.flags.add("dry-run"); else if (token === "--help" || token === "-h") parsed.help = true; else if (token.startsWith("--")) { const key = token.slice(2); const value = argv[index + 1]; if (!value || value.startsWith("--")) throw new Error(`${token} requires a value`); parsed.values[key] = value; index += 1; } else parsed.positionals.push(token); } return parsed; } function required(parsed: Parsed, key: string) { const value = parsed.values[key]; if (!value) throw new Error(`--${key} is required`); return value; }