takt/src/features/tasks/list/listNonInteractive.ts

141 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Non-interactive list mode.
*
* Handles --non-interactive output (text/JSON) and
* non-interactive branch actions (--action, --branch).
*/
import { execFileSync } from 'node:child_process';
import type { TaskListItem, BranchListItem } from '../../../infra/task/index.js';
import {
detectDefaultBranch,
listTaktBranches,
buildListItems,
TaskRunner,
} from '../../../infra/task/index.js';
import { info } from '../../../shared/ui/index.js';
import {
type ListAction,
tryMergeBranch,
mergeBranch,
deleteBranch,
} from './taskActions.js';
export interface ListNonInteractiveOptions {
enabled: boolean;
action?: string;
branch?: string;
format?: string;
yes?: boolean;
}
function isValidAction(action: string): action is ListAction {
return action === 'diff' || action === 'try' || action === 'merge' || action === 'delete';
}
function printNonInteractiveList(
items: BranchListItem[],
pendingTasks: TaskListItem[],
failedTasks: TaskListItem[],
format?: string,
): void {
const outputFormat = format ?? 'text';
if (outputFormat === 'json') {
// stdout に直接出力JSON パース用途のため UI ヘルパーを経由しない)
console.log(JSON.stringify({
branches: items,
pendingTasks,
failedTasks,
}, null, 2));
return;
}
for (const item of items) {
const worktreeLabel = item.info.worktreePath ? ' (worktree)' : '';
const instruction = item.originalInstruction ? ` - ${item.originalInstruction}` : '';
info(`${item.info.branch}${worktreeLabel} (${item.filesChanged} files)${instruction}`);
}
for (const task of pendingTasks) {
info(`[pending] ${task.name} - ${task.content}`);
}
for (const task of failedTasks) {
info(`[failed] ${task.name} - ${task.content}`);
}
}
function showDiffStat(projectDir: string, defaultBranch: string, branch: string): void {
try {
const stat = execFileSync(
'git', ['diff', '--stat', `${defaultBranch}...${branch}`],
{ cwd: projectDir, encoding: 'utf-8', stdio: 'pipe' },
);
info(stat);
} catch {
info('Could not generate diff stat');
}
}
/**
* Run list-tasks in non-interactive mode.
*/
export async function listTasksNonInteractive(
cwd: string,
nonInteractive: ListNonInteractiveOptions,
): Promise<void> {
const defaultBranch = detectDefaultBranch(cwd);
const branches = listTaktBranches(cwd);
const runner = new TaskRunner(cwd);
const pendingTasks = runner.listPendingTaskItems();
const failedTasks = runner.listFailedTasks();
const items = buildListItems(cwd, branches, defaultBranch);
if (items.length === 0 && pendingTasks.length === 0 && failedTasks.length === 0) {
info('No tasks to list.');
return;
}
if (!nonInteractive.action) {
printNonInteractiveList(items, pendingTasks, failedTasks, nonInteractive.format);
return;
}
// Branch-targeted action (--branch)
if (!nonInteractive.branch) {
info('Missing --branch for non-interactive action.');
process.exit(1);
}
if (!isValidAction(nonInteractive.action)) {
info('Invalid --action. Use one of: diff, try, merge, delete.');
process.exit(1);
}
const item = items.find((entry) => entry.info.branch === nonInteractive.branch);
if (!item) {
info(`Branch not found: ${nonInteractive.branch}`);
process.exit(1);
}
switch (nonInteractive.action) {
case 'diff':
showDiffStat(cwd, defaultBranch, item.info.branch);
return;
case 'try':
tryMergeBranch(cwd, item);
return;
case 'merge':
mergeBranch(cwd, item);
return;
case 'delete':
if (!nonInteractive.yes) {
info('Delete requires --yes in non-interactive mode.');
process.exit(1);
}
deleteBranch(cwd, item);
return;
}
}