import { realpathSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { handleStartupRecovery } from "../application/recovery/handle-startup-recovery.js"; import { GenericDomainError } from "../infrastructure/errors/generic-domain-error.js"; import { NativeBindingError } from "../infrastructure/adapters/sqlite/native-binding-error.js"; import { tffLog } from "../infrastructure/adapters/logging/warn.js"; import { getProjectHome, getProjectId, resolveProjectRoot, } from "../infrastructure/home-directory.js"; import { branchGuardCheckCmd, branchGuardCheckSchema } from "./commands/branch-guard-check.cmd.js"; import { checkpointLoadCmd, checkpointLoadSchema } from "./commands/checkpoint-load.cmd.js"; import { checkpointSaveCmd, checkpointSaveSchema } from "./commands/checkpoint-save.cmd.js"; import { claimCheckStaleCmd, claimCheckStaleSchema } from "./commands/claim-check-stale.cmd.js"; import { composeDetectCmd, composeDetectSchema } from "./commands/compose-detect.cmd.js"; import { depAddCmd, depAddSchema } from "./commands/dep-add.cmd.js"; import { directEditGuardCmd, directEditGuardSchema } from "./commands/direct-edit-guard.cmd.js"; import { judgePendingClearCmd, judgePendingClearSchema, } from "./commands/judge-pending-clear.cmd.js"; import { judgePendingListCmd, judgePendingListSchema } from "./commands/judge-pending-list.cmd.js"; import { milestoneAuditStatusCmd, milestoneAuditStatusSchema, } from "./commands/milestone-audit-status.cmd.js"; import { milestoneCloseCmd, milestoneCloseSchema } from "./commands/milestone-close.cmd.js"; import { milestoneCreateCmd, milestoneCreateSchema } from "./commands/milestone-create.cmd.js"; import { milestoneListCmd, milestoneListSchema } from "./commands/milestone-list.cmd.js"; import { milestoneRecordAuditCmd, milestoneRecordAuditSchema, } from "./commands/milestone-record-audit.cmd.js"; import { observeHealthCmd, observeHealthSchema } from "./commands/observe-health.cmd.js"; import { observeRecordCmd, observeRecordSchema } from "./commands/observe-record.cmd.js"; import { plannotatorCheckCmd, plannotatorCheckSchema } from "./commands/plannotator-check.cmd.js"; import { patternsAggregateCmd, patternsAggregateSchema, } from "./commands/patterns-aggregate.cmd.js"; import { patternsExtractCmd, patternsExtractSchema } from "./commands/patterns-extract.cmd.js"; import { patternsRankCmd, patternsRankSchema } from "./commands/patterns-rank.cmd.js"; import { preOpGuardCmd, preOpGuardSchema } from "./commands/pre-op-guard.cmd.js"; import { projectGetCmd, projectGetSchema } from "./commands/project-get.cmd.js"; import { projectInitCmd, projectInitSchema } from "./commands/project-init.cmd.js"; import { getCommandSchema } from "./commands/registry.js"; import { reviewCheckFreshCmd, reviewCheckFreshSchema } from "./commands/review-check-fresh.cmd.js"; import { reviewRecordCmd, reviewRecordSchema } from "./commands/review-record.cmd.js"; import { routingCalibrateCmd, routingCalibrateSchema } from "./commands/routing-calibrate.cmd.js"; import { routingDecideCmd, routingDecideSchema } from "./commands/routing-decide.cmd.js"; import { routingEventCmd, routingEventSchema } from "./commands/routing-event.cmd.js"; import { routingJudgePrepareCmd, routingJudgePrepareSchema, } from "./commands/routing-judge-prepare.cmd.js"; import { routingJudgeRecordCmd, routingJudgeRecordSchema, } from "./commands/routing-judge-record.cmd.js"; import { routingOutcomeCmd, routingOutcomeSchema } from "./commands/routing-outcome.cmd.js"; import { schemaCmd, schemaCmdSchema } from "./commands/schema.cmd.js"; import { sessionRemindCmd, sessionRemindSchema } from "./commands/session-remind.cmd.js"; import { skillsApproveCmd, skillsApproveSchema } from "./commands/skills-approve.cmd.js"; import { skillsDriftCmd, skillsDriftSchema } from "./commands/skills-drift.cmd.js"; import { skillsDriftReportCmd, skillsDriftReportSchema, } from "./commands/skills-drift-report.cmd.js"; import { skillsValidateCmd, skillsValidateSchema } from "./commands/skills-validate.cmd.js"; import { sliceClassifyCmd, sliceClassifySchema } from "./commands/slice-classify.cmd.js"; import { sliceCloseCmd, sliceCloseSchema } from "./commands/slice-close.cmd.js"; import { sliceCreateCmd, sliceCreateSchema } from "./commands/slice-create.cmd.js"; import { sliceListCmd, sliceListSchema } from "./commands/slice-list.cmd.js"; import { sliceRecordMergeCmd, sliceRecordMergeSchema } from "./commands/slice-record-merge.cmd.js"; import { sliceTransitionCmd, sliceTransitionSchema } from "./commands/slice-transition.cmd.js"; import { specEditGuardCmd, specEditGuardSchema } from "./commands/spec-edit-guard.cmd.js"; import { stateDiffCmd, stateDiffSchema } from "./commands/state-diff.cmd.js"; import { syncStateCmd, syncStateSchema } from "./commands/sync-state.cmd.js"; import { taskClaimCmd, taskClaimSchema } from "./commands/task-claim.cmd.js"; import { taskCloseCmd, taskCloseSchema } from "./commands/task-close.cmd.js"; import { taskCreateCmd, taskCreateSchema } from "./commands/task-create.cmd.js"; import { taskReadyCmd, taskReadySchema } from "./commands/task-ready.cmd.js"; import { versionCmd, versionSchema } from "./commands/version.cmd.js"; import { wavesDetectCmd, wavesDetectSchema } from "./commands/waves-detect.cmd.js"; import { workflowNextCmd, workflowNextSchema } from "./commands/workflow-next.cmd.js"; import { workflowShouldAutoCmd, workflowShouldAutoSchema, } from "./commands/workflow-should-auto.cmd.js"; import { worktreeCreateCmd, worktreeCreateSchema } from "./commands/worktree-create.cmd.js"; import { worktreeDeleteCmd, worktreeDeleteSchema } from "./commands/worktree-delete.cmd.js"; import { worktreeListCmd, worktreeListSchema } from "./commands/worktree-list.cmd.js"; import type { CommandSchema } from "./utils/flag-parser.js"; import { withMutatingCommand } from "./utils/with-mutating-command.js"; type CommandFn = (args: string[]) => Promise; export interface CommandEntry { schema: CommandSchema; dispatcher: CommandFn; } const wrap = (handler: CommandFn, schema: CommandSchema): CommandFn => schema.mutates === true ? withMutatingCommand(handler, { commandName: schema.name }) : handler; export const COMMAND_REGISTRY: Record = (() => { const branchGuardCheckHandler = branchGuardCheckCmd(); return { "branch-guard:check": { schema: branchGuardCheckSchema, dispatcher: wrap(branchGuardCheckHandler, branchGuardCheckSchema), }, "project:init": { schema: projectInitSchema, dispatcher: wrap(projectInitCmd, projectInitSchema), }, "project:get": { schema: projectGetSchema, dispatcher: wrap(projectGetCmd, projectGetSchema), }, "milestone:create": { schema: milestoneCreateSchema, dispatcher: wrap(milestoneCreateCmd, milestoneCreateSchema), }, "milestone:list": { schema: milestoneListSchema, dispatcher: wrap(milestoneListCmd, milestoneListSchema), }, "milestone:close": { schema: milestoneCloseSchema, dispatcher: wrap(milestoneCloseCmd, milestoneCloseSchema), }, "milestone:record-audit": { schema: milestoneRecordAuditSchema, dispatcher: wrap(milestoneRecordAuditCmd, milestoneRecordAuditSchema), }, "milestone:audit-status": { schema: milestoneAuditStatusSchema, dispatcher: wrap(milestoneAuditStatusCmd, milestoneAuditStatusSchema), }, "slice:create": { schema: sliceCreateSchema, dispatcher: wrap(sliceCreateCmd, sliceCreateSchema), }, "slice:list": { schema: sliceListSchema, dispatcher: wrap(sliceListCmd, sliceListSchema), }, "slice:transition": { schema: sliceTransitionSchema, dispatcher: wrap(sliceTransitionCmd, sliceTransitionSchema), }, "slice:close": { schema: sliceCloseSchema, dispatcher: wrap(sliceCloseCmd, sliceCloseSchema), }, "slice:record-merge": { schema: sliceRecordMergeSchema, dispatcher: wrap(sliceRecordMergeCmd, sliceRecordMergeSchema), }, "slice:classify": { schema: sliceClassifySchema, dispatcher: wrap(sliceClassifyCmd, sliceClassifySchema), }, "task:create": { schema: taskCreateSchema, dispatcher: wrap(taskCreateCmd, taskCreateSchema), }, "task:claim": { schema: taskClaimSchema, dispatcher: wrap(taskClaimCmd, taskClaimSchema), }, "task:close": { schema: taskCloseSchema, dispatcher: wrap(taskCloseCmd, taskCloseSchema), }, "task:ready": { schema: taskReadySchema, dispatcher: wrap(taskReadyCmd, taskReadySchema), }, "dep:add": { schema: depAddSchema, dispatcher: wrap(depAddCmd, depAddSchema), }, "direct-edit:guard": { schema: directEditGuardSchema, dispatcher: wrap(directEditGuardCmd, directEditGuardSchema), }, "pre-op:guard": { schema: preOpGuardSchema, dispatcher: wrap(preOpGuardCmd, preOpGuardSchema), }, "spec-edit:guard": { schema: specEditGuardSchema, dispatcher: wrap(specEditGuardCmd, specEditGuardSchema), }, "waves:detect": { schema: wavesDetectSchema, dispatcher: wrap(wavesDetectCmd, wavesDetectSchema), }, "state:diff": { schema: stateDiffSchema, dispatcher: wrap(stateDiffCmd, stateDiffSchema), }, "sync:state": { schema: syncStateSchema, dispatcher: wrap(syncStateCmd, syncStateSchema), }, "worktree:create": { schema: worktreeCreateSchema, dispatcher: wrap(worktreeCreateCmd, worktreeCreateSchema), }, "worktree:delete": { schema: worktreeDeleteSchema, dispatcher: wrap(worktreeDeleteCmd, worktreeDeleteSchema), }, "worktree:list": { schema: worktreeListSchema, dispatcher: wrap(worktreeListCmd, worktreeListSchema), }, "review:check-fresh": { schema: reviewCheckFreshSchema, dispatcher: wrap(reviewCheckFreshCmd, reviewCheckFreshSchema), }, "review:record": { schema: reviewRecordSchema, dispatcher: wrap(reviewRecordCmd, reviewRecordSchema), }, "routing:decide": { schema: routingDecideSchema, dispatcher: wrap(routingDecideCmd, routingDecideSchema), }, "routing:event": { schema: routingEventSchema, dispatcher: wrap(routingEventCmd, routingEventSchema), }, "routing:outcome": { schema: routingOutcomeSchema, dispatcher: wrap(routingOutcomeCmd, routingOutcomeSchema), }, "routing:calibrate": { schema: routingCalibrateSchema, dispatcher: wrap(routingCalibrateCmd, routingCalibrateSchema), }, "routing:judge-prepare": { schema: routingJudgePrepareSchema, dispatcher: wrap(routingJudgePrepareCmd, routingJudgePrepareSchema), }, "routing:judge-record": { schema: routingJudgeRecordSchema, dispatcher: wrap(routingJudgeRecordCmd, routingJudgeRecordSchema), }, "judge:pending:list": { schema: judgePendingListSchema, dispatcher: wrap(judgePendingListCmd, judgePendingListSchema), }, "judge:pending:clear": { schema: judgePendingClearSchema, dispatcher: wrap(judgePendingClearCmd, judgePendingClearSchema), }, "checkpoint:save": { schema: checkpointSaveSchema, dispatcher: wrap(checkpointSaveCmd, checkpointSaveSchema), }, "checkpoint:load": { schema: checkpointLoadSchema, dispatcher: wrap(checkpointLoadCmd, checkpointLoadSchema), }, "observe:health": { schema: observeHealthSchema, dispatcher: wrap(observeHealthCmd, observeHealthSchema), }, "plannotator:check": { schema: plannotatorCheckSchema, dispatcher: wrap(plannotatorCheckCmd, plannotatorCheckSchema), }, "observe:record": { schema: observeRecordSchema, dispatcher: wrap(observeRecordCmd, observeRecordSchema), }, "patterns:extract": { schema: patternsExtractSchema, dispatcher: wrap(patternsExtractCmd, patternsExtractSchema), }, "patterns:aggregate": { schema: patternsAggregateSchema, dispatcher: wrap(patternsAggregateCmd, patternsAggregateSchema), }, "patterns:rank": { schema: patternsRankSchema, dispatcher: wrap(patternsRankCmd, patternsRankSchema), }, "compose:detect": { schema: composeDetectSchema, dispatcher: wrap(composeDetectCmd, composeDetectSchema), }, "skills:approve": { schema: skillsApproveSchema, dispatcher: wrap(skillsApproveCmd, skillsApproveSchema), }, "skills:drift": { schema: skillsDriftSchema, dispatcher: wrap(skillsDriftCmd, skillsDriftSchema), }, "skills:drift-report": { schema: skillsDriftReportSchema, dispatcher: wrap(skillsDriftReportCmd, skillsDriftReportSchema), }, "skills:validate": { schema: skillsValidateSchema, dispatcher: wrap(skillsValidateCmd, skillsValidateSchema), }, "workflow:next": { schema: workflowNextSchema, dispatcher: wrap(workflowNextCmd, workflowNextSchema), }, "workflow:should-auto": { schema: workflowShouldAutoSchema, dispatcher: wrap(workflowShouldAutoCmd, workflowShouldAutoSchema), }, "claim:check-stale": { schema: claimCheckStaleSchema, dispatcher: wrap(claimCheckStaleCmd, claimCheckStaleSchema), }, "session:remind": { schema: sessionRemindSchema, dispatcher: wrap(sessionRemindCmd, sessionRemindSchema), }, version: { schema: versionSchema, dispatcher: wrap(versionCmd, versionSchema), }, schema: { schema: schemaCmdSchema, dispatcher: wrap(schemaCmd, schemaCmdSchema), }, }; })(); /** * Generate help output for a command */ export function generateHelp(schema: CommandSchema): string { return JSON.stringify({ ok: true, data: { name: schema.name, purpose: schema.purpose, syntax: generateSyntax(schema), requiredFlags: schema.requiredFlags.map((f) => ({ name: `--${f.name}`, type: f.type, description: f.description, enum: f.enum, pattern: f.pattern, })), optionalFlags: schema.optionalFlags.map((f) => ({ name: `--${f.name}`, type: f.type, description: f.description, enum: f.enum, pattern: f.pattern, })), examples: schema.examples, }, }); } /** * Generate syntax string from schema */ function generateSyntax(schema: CommandSchema): string { const required = schema.requiredFlags.map((f) => `--${f.name} <${f.type}>`); const optional = schema.optionalFlags.map((f) => `[--${f.name}]`); return `${schema.name} ${required.join(" ")} ${optional.join(" ")}`.trim(); } /** * Convert a CommandSchema to JSON Schema format */ export function schemaToJsonSchema(schema: CommandSchema): Record { const properties: Record> = {}; const required: string[] = []; for (const flag of schema.requiredFlags) { required.push(flag.name); properties[flag.name] = flagToJsonSchema(flag); } for (const flag of schema.optionalFlags) { properties[flag.name] = flagToJsonSchema(flag); } return { type: "object", required, properties, }; } /** * Convert a FlagDefinition to JSON Schema format */ export function flagToJsonSchema(flag: { name: string; type: string; description: string; enum?: string[]; pattern?: string; }): Record { const schema: Record = { type: flag.type === "json" ? "object" : flag.type, description: flag.description, }; if (flag.enum) { schema.enum = flag.enum; } if (flag.pattern) { schema.pattern = flag.pattern; } return schema; } /** * Resolve the directory that startup recovery should sweep. * * Preferred: the real project home (`~/.tff//`), resolved via * the project id persisted at the git toplevel. This avoids walking through * the `cwd/.tff` symlink, which — in multi-worktree setups — lives inside * the same home directory and can create cyclic descents. * * Fallback: `cwd/.tff` for repos that haven't run `project:init` yet. * These are legitimately non-cyclic (they're either missing or a plain dir), * so the legacy path remains safe. */ function resolveStartupHomeDir(): string { const cwd = process.cwd(); try { // resolveProjectRoot honors TFF_CC_HOME: when set, the id-file lives // under TFF_CC_HOME and not in cwd, keeping isolated test sandboxes // from leaking `.tff-project-id` into the surrounding worktree. const projectRoot = resolveProjectRoot(cwd); const projectId = getProjectId(projectRoot); return getProjectHome(projectId); } catch (err) { process.stderr.write( `tff-cc: could not resolve project home, falling back to cwd/.tff — ${String(err)}\n`, ); return join(cwd, ".tff"); } } export function handleEntryPointError(err: unknown): string { if (err instanceof NativeBindingError) { return JSON.stringify({ ok: false, error: err.toJSON() }); } return JSON.stringify({ ok: false, error: { code: "INTERNAL_ERROR", message: String(err) }, }); } export const main = async () => { const [command, ...args] = process.argv.slice(2); await handleStartupRecovery({ homeDir: resolveStartupHomeDir() }); if (!command || command === "--help" || command === "-h") { tffLog( JSON.stringify({ ok: true, data: { name: "tff-tools", version: typeof __TFF_VERSION__ !== "undefined" ? __TFF_VERSION__ : "0.0.0-dev", commands: Object.keys(COMMAND_REGISTRY), }, }), ); return; } if (command === "--version" || command === "-v") { tffLog(await versionCmd(args)); return; } // Handle --help flag for any command if (args.includes("--help") || args.includes("-h")) { const schema = getCommandSchema(command); if (schema) { // Check for --json flag - output schema format instead of help format if (args.includes("--json")) { tffLog( JSON.stringify({ ok: true, data: { command: schema.name, flags: schemaToJsonSchema(schema), }, }), ); return; } tffLog(generateHelp(schema)); return; } tffLog( JSON.stringify({ ok: false, error: new GenericDomainError( "UNKNOWN_COMMAND", `Unknown command "${command}". Run --help for available commands.`, { availableCommands: Object.keys(COMMAND_REGISTRY).filter((c) => c !== "schema") }, ), }), ); return; } const entry = COMMAND_REGISTRY[command]; if (!entry) { tffLog( JSON.stringify({ ok: false, error: new GenericDomainError( "UNKNOWN_COMMAND", `Unknown command "${command}". Run --help for available commands.`, { availableCommands: Object.keys(COMMAND_REGISTRY).filter((c) => c !== "schema") }, ), }), ); return; } const output = await entry.dispatcher(args); tffLog(output); }; // Compare via realpath to handle platforms where argv[1] and the canonical // module URL disagree on symlinks (e.g. macOS /var -> /private/var). const isEntryPoint = (): boolean => { if (!process.argv[1]) return false; try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); } catch { return process.argv[1] === fileURLToPath(import.meta.url); } }; if (isEntryPoint()) { main().catch((err) => { tffLog(handleEntryPointError(err)); process.exit(1); }); } export { runMigrations, Milestone } from "@tff/core";