import { existsSync, readFileSync } from "node:fs"; import { isAbsolute, join, relative } from "node:path"; import { rootPath } from "./config"; import { CliInputError } from "./output"; import { parseEnvFile, readYamlRecord } from "./platform-infra-ops-library"; interface TaskTreeRuntimeConfig { defaults?: { node?: unknown; backup?: unknown; }; targets?: unknown; native?: { targetRef?: unknown; }; } interface NodeLanesConfig { lanes?: unknown; } interface SecretsDistributionConfig { sources?: { root?: unknown; }; } export function runTaskTreeCommand(args: string[]): number { const runtime = readYamlRecord( rootPath("config", "hwlab-tasktree.yaml"), "hwlab-tasktree-runtime", ); const targetName = nativeTargetName(runtime.native?.targetRef); const target = recordField(recordField(runtime, "targets"), targetName); const laneName = stringField(target, "lane", `targets.${targetName}.lane`); const nodeName = stringValue(runtime.defaults?.node, "defaults.node"); const workspace = taskTreeWorkspace(laneName, nodeName); const sourceRef = stringField( recordField(target, "database", `targets.${targetName}.database`), "sourceRef", `targets.${targetName}.database.sourceRef`, ); const secretRoot = taskTreeSecretRoot(); const secretPath = containedPath(secretRoot, sourceRef, "database.sourceRef"); if (!existsSync(secretPath)) { throw new CliInputError("TaskTree 数据库 Secret source 不存在", { code: "tasktree_database_secret_missing", argument: sourceRef, hint: "使用 bun scripts/cli.ts secrets sync/status 核对 YAML 声明的 Secret source。", }); } const databaseUrl = parseEnvFile(readFileSync(secretPath, "utf8")).DATABASE_URL; if (!databaseUrl) { throw new CliInputError("TaskTree 数据库 Secret source 缺少 DATABASE_URL", { code: "tasktree_database_url_missing", argument: sourceRef, }); } const cliPath = join(workspace, "tools", "hwlab-cli", "bin", "hwlab-cli.ts"); if (!existsSync(cliPath)) { throw new CliInputError("YAML 选中的 HWLAB workspace 缺少 TaskTree CLI", { code: "tasktree_cli_missing", argument: cliPath, }); } const childEnv: Record = { ...process.env, TASKTREE_DATABASE_URL: databaseUrl, }; const overapiEndpoint = optionalNestedString(target, ["runtime", "cli", "overapiEndpoint"]); if (overapiEndpoint !== undefined) childEnv.HWLAB_TASKTREE_API_URL = overapiEndpoint; if (args[0] === "backup") Object.assign(childEnv, taskTreeBackupEnv(runtime, target)); const result = Bun.spawnSync({ cmd: ["bun", cliPath, "tasktree", ...args], cwd: workspace, env: childEnv, stdin: "inherit", stdout: "inherit", stderr: "inherit", }); return result.exitCode; } function nativeTargetName(value: unknown): string { const targetRef = stringValue(value, "native.targetRef"); const match = /^targets\.([A-Za-z0-9_-]+)$/u.exec(targetRef); if (match === null) { throw new CliInputError("TaskTree native.targetRef 必须指向 targets.", { code: "tasktree_invalid_target_ref", argument: targetRef, }); } return match[1]!; } function taskTreeWorkspace(laneName: string, nodeName: string): string { const config = readYamlRecord( rootPath("config", "hwlab-node-lanes.yaml"), "HwlabNodeLaneConfig", ); const lane = recordField(recordField(config, "lanes"), laneName, `lanes.${laneName}`); const targets = recordField(lane, "targets", `lanes.${laneName}.targets`); const target = recordField(targets, nodeName, `lanes.${laneName}.targets.${nodeName}`); const workspace = stringField(target, "workspace", `lanes.${laneName}.targets.${nodeName}.workspace`); if (!isAbsolute(workspace)) { throw new CliInputError("HWLAB workspace 必须是绝对路径", { code: "tasktree_invalid_workspace", argument: workspace, }); } return workspace; } function taskTreeSecretRoot(): string { const config = readYamlRecord( rootPath("config", "secrets-distribution.yaml"), "unidesk-secret-distribution", ); const secretRoot = stringValue(config.sources?.root, "sources.root"); if (!isAbsolute(secretRoot)) { throw new CliInputError("统一 Secret root 必须是绝对路径", { code: "tasktree_invalid_secret_root", argument: secretRoot, }); } return secretRoot; } function taskTreeBackupEnv( runtime: TaskTreeRuntimeConfig, target: Record, ): Record { const backup = optionalRecord(runtime.defaults?.backup); const targetBackup = optionalRecord(target.backup); if (backup === undefined || targetBackup === undefined) { throw new CliInputError("TaskTree owning YAML 尚未声明 native 备份配置", { code: "tasktree_backup_config_missing", hint: "先将 defaults.backup 与当前 target.backup 合入 config/hwlab-tasktree.yaml。", }); } return { TASKTREE_BACKUP_ENABLED: String(booleanField(backup, "enabled", "defaults.backup.enabled")), TASKTREE_BACKUP_REPO_SSH_URL: stringField(backup, "sshUrl", "defaults.backup.sshUrl"), TASKTREE_BACKUP_BRANCH: stringField(backup, "branch", "defaults.backup.branch"), TASKTREE_BACKUP_BASE_PATH: stringField(targetBackup, "basePath", "target.backup.basePath"), TASKTREE_BACKUP_WORKTREE: stringField(backup, "worktreePath", "defaults.backup.worktreePath"), TASKTREE_BACKUP_TARGET: stringField(targetBackup, "target", "target.backup.target"), TASKTREE_BACKUP_INTERVAL_MS: String(durationMilliseconds(stringField(backup, "interval", "defaults.backup.interval"))), TASKTREE_BACKUP_GIT_AUTHOR_NAME: stringField(recordField(backup, "author", "defaults.backup.author"), "name", "defaults.backup.author.name"), TASKTREE_BACKUP_GIT_AUTHOR_EMAIL: stringField(recordField(backup, "author", "defaults.backup.author"), "email", "defaults.backup.author.email"), TASKTREE_BACKUP_GIT_COMMIT_PREFIX: stringField(backup, "commitMessagePrefix", "defaults.backup.commitMessagePrefix"), }; } function containedPath(root: string, sourceRef: string, label: string): string { if (isAbsolute(sourceRef)) { throw new CliInputError(`${label} 必须相对统一 Secret root`, { code: "tasktree_invalid_secret_source_ref", argument: sourceRef, }); } const path = join(root, sourceRef); const rel = relative(root, path); if (rel.startsWith("..") || isAbsolute(rel)) { throw new CliInputError(`${label} 超出统一 Secret root`, { code: "tasktree_invalid_secret_source_ref", argument: sourceRef, }); } return path; } function durationMilliseconds(value: string): number { const match = /^(\d+)(ms|s|m|h)$/u.exec(value); if (match === null) { throw new CliInputError("TaskTree backup interval 格式无效", { code: "tasktree_invalid_backup_interval", argument: value, }); } const multiplier = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 }[match[2]!]!; return Number(match[1]) * multiplier; } function optionalNestedString(record: Record, path: string[]): string | undefined { let value: unknown = record; for (const key of path) { const current = optionalRecord(value); if (current === undefined) return undefined; value = current[key]; } return typeof value === "string" && value.length > 0 ? value : undefined; } function optionalRecord(value: unknown): Record | undefined { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : undefined; } function recordField(value: unknown, key: string, label = key): Record { const record = optionalRecord(value); const nested = record === undefined ? undefined : optionalRecord(record[key]); if (nested === undefined) throw configError(label); return nested; } function stringField(record: Record, key: string, label: string): string { return stringValue(record[key], label); } function stringValue(value: unknown, label: string): string { if (typeof value !== "string" || value.length === 0) throw configError(label); return value; } function booleanField(record: Record, key: string, label: string): boolean { if (typeof record[key] !== "boolean") throw configError(label); return record[key]; } function configError(label: string): CliInputError { return new CliInputError(`TaskTree owning YAML 缺少 ${label}`, { code: "tasktree_config_invalid", argument: label, }); }