Files
pikasTech-HWLAB/tools/src/tasktree-cli.ts
T

86 lines
5.7 KiB
TypeScript

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<string, string | undefined> = 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|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<TaskTreeCommand> {
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 === "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<string, string | undefined>, 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<string, string>; flags: Set<string>; 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; }