takt/src/app/cli/routing.ts

154 lines
5.2 KiB
TypeScript

/**
* Default action routing
*
* Handles the default (no subcommand) action: task execution,
* pipeline mode, or interactive mode.
*/
import { info, error } from '../../shared/ui/index.js';
import { getErrorMessage } from '../../shared/utils/index.js';
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers, type GitHubIssue } from '../../infra/github/index.js';
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueFromTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
import { executePipeline } from '../../features/pipeline/index.js';
import { interactiveMode } from '../../features/interactive/index.js';
import { getPieceDescription } from '../../infra/config/index.js';
import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
import { program, resolvedCwd, pipelineMode } from './program.js';
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
/**
* Resolve issue references from CLI input.
*
* Handles two sources:
* - --issue N option (numeric issue number)
* - Positional argument containing issue references (#N or "#1 #2")
*
* Returns resolved issues and the formatted task text for interactive mode.
* Throws on gh CLI unavailability or fetch failure.
*/
function resolveIssueInput(
issueOption: number | undefined,
task: string | undefined,
): { issues: GitHubIssue[]; initialInput: string } | null {
if (issueOption) {
info('Fetching GitHub Issue...');
const ghStatus = checkGhCli();
if (!ghStatus.available) {
throw new Error(ghStatus.error);
}
const issue = fetchIssue(issueOption);
return { issues: [issue], initialInput: formatIssueAsTask(issue) };
}
if (task && isDirectTask(task)) {
info('Fetching GitHub Issue...');
const ghStatus = checkGhCli();
if (!ghStatus.available) {
throw new Error(ghStatus.error);
}
const tokens = task.trim().split(/\s+/);
const issueNumbers = parseIssueNumbers(tokens);
if (issueNumbers.length === 0) {
throw new Error(`Invalid issue reference: ${task}`);
}
const issues = issueNumbers.map((n) => fetchIssue(n));
return { issues, initialInput: issues.map(formatIssueAsTask).join('\n\n---\n\n') };
}
return null;
}
/**
* Execute default action: handle task execution, pipeline mode, or interactive mode.
* Exported for use in slash-command fallback logic.
*/
export async function executeDefaultAction(task?: string): Promise<void> {
const opts = program.opts();
const agentOverrides = resolveAgentOverrides(program);
const createWorktreeOverride = parseCreateWorktreeOption(opts.createWorktree as string | undefined);
const selectOptions: SelectAndExecuteOptions = {
autoPr: opts.autoPr === true,
repo: opts.repo as string | undefined,
piece: opts.piece as string | undefined,
createWorktree: createWorktreeOverride,
};
// --- Pipeline mode (non-interactive): triggered by --pipeline ---
if (pipelineMode) {
const exitCode = await executePipeline({
issueNumber: opts.issue as number | undefined,
task: opts.task as string | undefined,
piece: (opts.piece as string | undefined) ?? DEFAULT_PIECE_NAME,
branch: opts.branch as string | undefined,
autoPr: opts.autoPr === true,
repo: opts.repo as string | undefined,
skipGit: opts.skipGit === true,
cwd: resolvedCwd,
provider: agentOverrides?.provider,
model: agentOverrides?.model,
});
if (exitCode !== 0) {
process.exit(exitCode);
}
return;
}
// --- Normal (interactive) mode ---
// Resolve --task option to task text (direct execution, no interactive mode)
const taskFromOption = opts.task as string | undefined;
if (taskFromOption) {
await selectAndExecuteTask(resolvedCwd, taskFromOption, selectOptions, agentOverrides);
return;
}
// Resolve issue references (--issue N or #N positional arg) before interactive mode
let initialInput: string | undefined = task;
try {
const issueResult = resolveIssueInput(opts.issue as number | undefined, task);
if (issueResult) {
selectOptions.issues = issueResult.issues;
initialInput = issueResult.initialInput;
}
} catch (e) {
error(getErrorMessage(e));
process.exit(1);
}
// All paths below go through interactive mode
const pieceId = await determinePiece(resolvedCwd, selectOptions.piece);
if (pieceId === null) {
info('Cancelled');
return;
}
const pieceContext = getPieceDescription(pieceId, resolvedCwd);
const result = await interactiveMode(resolvedCwd, initialInput, pieceContext);
switch (result.action) {
case 'execute':
selectOptions.interactiveUserInput = true;
selectOptions.piece = pieceId;
selectOptions.interactiveMetadata = { confirmed: true, task: result.task };
await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides);
break;
case 'create_issue':
createIssueFromTask(result.task);
break;
case 'save_task':
await saveTaskFromInteractive(resolvedCwd, result.task, pieceId);
break;
case 'cancel':
break;
}
}
program
.argument('[task]', 'Task to execute (or GitHub issue reference like "#6")')
.action(executeDefaultAction);