feat(tasktree): add GitHub disaster recovery backups

This commit is contained in:
root
2026-07-18 15:15:57 +02:00
parent 8b57d87ff7
commit 12ad602a63
16 changed files with 1342 additions and 9 deletions
+27 -1
View File
@@ -40,6 +40,9 @@ function help() {
"hwlab-cli tasktree report list|get --task ID|--report ID",
"hwlab-cli tasktree report write --task ID --title TEXT (--body TEXT|--body-file PATH|--stdin) [--status STATUS]",
"hwlab-cli tasktree timeline --group ID [--overapi]",
"hwlab-cli tasktree backup create [--dry-run] [--reason TEXT]",
"hwlab-cli tasktree backup status|verify",
"hwlab-cli tasktree backup restore [--confirm --expected-sha SHA]",
"hwlab-cli tasktree workflow start --task ID"
],
transportContract: "--overapi only changes transport; commands and options are identical",
@@ -105,6 +108,18 @@ async function commandFrom(parsed: Parsed, stdin?: string): Promise<TaskTreeComm
if (group === "report" && action === "list") return { operation: "report.list", taskId: required(parsed, "task") };
if (group === "report" && action === "get") return { operation: "report.get", reportId: required(parsed, "report") };
if (group === "report" && action === "write") return { operation: "report.write", taskId: required(parsed, "task"), title: required(parsed, "title"), body: await bodyInput(parsed, stdin), status: parsed.values.status };
if (group === "backup" && action === "create") return {
operation: "backup.create",
dryRun: parsed.flags.has("dry-run"),
reason: parsed.values.reason
};
if (group === "backup" && action === "status") return { operation: "backup.status" };
if (group === "backup" && action === "verify") return { operation: "backup.verify" };
if (group === "backup" && action === "restore") return {
operation: "backup.restore",
confirm: parsed.flags.has("confirm"),
expectedSha256: parsed.values["expected-sha"]
};
if (group === "workflow" && action === "start") return { operation: "workflow.start", taskId: required(parsed, "task") };
throw codedError("unsupported_command", `unsupported TaskTree command: ${group} ${action}`.trim());
}
@@ -144,7 +159,7 @@ function parse(argv: string[]): Parsed {
if (parsed.overapi) throw codedError("duplicate_option", "--overapi may only be provided once");
parsed.overapi = true;
}
else if (token === "--dry-run" || token === "--stdin") {
else if (token === "--dry-run" || token === "--stdin" || token === "--confirm") {
const flag = token.slice(2);
if (parsed.flags.has(flag)) throw codedError("duplicate_option", `${token} may only be provided once`);
parsed.flags.add(flag);
@@ -211,6 +226,10 @@ const COMMAND_SPECS: Record<string, CommandSpec> = {
"report list": { positionals: 2, options: ["task"] },
"report get": { positionals: 2, options: ["report"] },
"report write": { positionals: 2, options: ["task", "title", "body", "body-file", "status"], flags: ["stdin"] },
"backup create": { positionals: 2, options: ["reason"], flags: ["dry-run"] },
"backup status": { positionals: 2 },
"backup verify": { positionals: 2 },
"backup restore": { positionals: 2, options: ["expected-sha"], flags: ["confirm"] },
"workflow start": { positionals: 2, options: ["task"] }
};
@@ -236,4 +255,11 @@ function validateArguments(parsed: Parsed) {
for (const flag of parsed.flags) {
if (!allowedFlags.has(flag)) throw codedError("invalid_option", `--${flag} is not valid for ${key}`);
}
if (key === "backup restore") {
const confirm = parsed.flags.has("confirm");
const expectedSha = parsed.values["expected-sha"];
if (confirm !== (expectedSha !== undefined)) {
throw codedError("restore_confirmation_invalid", "--confirm and --expected-sha must be provided together");
}
}
}