takt/src/features/tasks/execute/selectAndExecute.ts
nrslib 68b45abbf6 カテゴリ設定を簡素化: 自動コピー方式に変更
ユーザー/ビルトインの分離を廃止し、単一のカテゴリツリーに統一。
~/.takt/preferences/piece-categories.yaml を唯一のソースとし、
ファイルがなければ builtin デフォルトから自動コピーする。

- builtinCategories 分離と「📂 Builtin/」フォルダ表示を廃止
- appendOthersCategory で同名カテゴリへの未分類 piece マージを修正
- takt reset categories コマンドを追加
- default-categories.yaml を piece-categories.yaml にリネーム
2026-02-06 01:24:31 +09:00

191 lines
5.9 KiB
TypeScript

/**
* Task execution orchestration.
*
* Coordinates piece selection, worktree creation, task execution,
* auto-commit, and PR creation. Extracted from cli.ts to avoid
* mixing CLI parsing with business logic.
*/
import {
getCurrentPiece,
listPieces,
listPieceEntries,
isPiecePath,
loadAllPiecesWithSources,
getPieceCategories,
buildCategorizedPieces,
} from '../../../infra/config/index.js';
import { confirm } from '../../../shared/prompt/index.js';
import { createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js';
import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js';
import { info, error, success } from '../../../shared/ui/index.js';
import { createLogger } from '../../../shared/utils/index.js';
import { createPullRequest, buildPrBody } from '../../../infra/github/index.js';
import { executeTask } from './taskExecution.js';
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
import {
warnMissingPieces,
selectPieceFromCategorizedPieces,
selectPieceFromEntries,
} from '../../pieceSelection/index.js';
export type { WorktreeConfirmationResult, SelectAndExecuteOptions };
const log = createLogger('selectAndExecute');
/**
* Select a piece interactively with directory categories and bookmarks.
*/
async function selectPieceWithDirectoryCategories(cwd: string): Promise<string | null> {
const availablePieces = listPieces(cwd);
const currentPiece = getCurrentPiece(cwd);
if (availablePieces.length === 0) {
info(`No pieces found. Using default: ${DEFAULT_PIECE_NAME}`);
return DEFAULT_PIECE_NAME;
}
if (availablePieces.length === 1 && availablePieces[0]) {
return availablePieces[0];
}
const entries = listPieceEntries(cwd);
return selectPieceFromEntries(entries, currentPiece);
}
/**
* Select a piece interactively with 2-stage category support.
*/
async function selectPiece(cwd: string): Promise<string | null> {
const categoryConfig = getPieceCategories();
if (categoryConfig) {
const current = getCurrentPiece(cwd);
const allPieces = loadAllPiecesWithSources(cwd);
if (allPieces.size === 0) {
info(`No pieces found. Using default: ${DEFAULT_PIECE_NAME}`);
return DEFAULT_PIECE_NAME;
}
const categorized = buildCategorizedPieces(allPieces, categoryConfig);
warnMissingPieces(categorized.missingPieces);
return selectPieceFromCategorizedPieces(categorized, current);
}
return selectPieceWithDirectoryCategories(cwd);
}
/**
* Determine piece to use.
*
* - If override looks like a path (isPiecePath), return it directly (validation is done at load time).
* - If override is a name, validate it exists in available pieces.
* - If no override, prompt user to select interactively.
*/
export async function determinePiece(cwd: string, override?: string): Promise<string | null> {
if (override) {
if (isPiecePath(override)) {
return override;
}
const availablePieces = listPieces(cwd);
const knownPieces = availablePieces.length === 0 ? [DEFAULT_PIECE_NAME] : availablePieces;
if (!knownPieces.includes(override)) {
error(`Piece not found: ${override}`);
return null;
}
return override;
}
return selectPiece(cwd);
}
export async function confirmAndCreateWorktree(
cwd: string,
task: string,
createWorktreeOverride?: boolean | undefined,
): Promise<WorktreeConfirmationResult> {
const useWorktree =
typeof createWorktreeOverride === 'boolean'
? createWorktreeOverride
: await confirm('Create worktree?', true);
if (!useWorktree) {
return { execCwd: cwd, isWorktree: false };
}
info('Generating branch name...');
const taskSlug = await summarizeTaskName(task, { cwd });
const result = createSharedClone(cwd, {
worktree: true,
taskSlug,
});
info(`Clone created: ${result.path} (branch: ${result.branch})`);
return { execCwd: result.path, isWorktree: true, branch: result.branch };
}
/**
* Execute a task with piece selection, optional worktree, and auto-commit.
* Shared by direct task execution and interactive mode.
*/
export async function selectAndExecuteTask(
cwd: string,
task: string,
options?: SelectAndExecuteOptions,
agentOverrides?: TaskExecutionOptions,
): Promise<void> {
const pieceIdentifier = await determinePiece(cwd, options?.piece);
if (pieceIdentifier === null) {
info('Cancelled');
return;
}
const { execCwd, isWorktree, branch } = await confirmAndCreateWorktree(
cwd,
task,
options?.createWorktree,
);
log.info('Starting task execution', { piece: pieceIdentifier, worktree: isWorktree });
const taskSuccess = await executeTask({
task,
cwd: execCwd,
pieceIdentifier,
projectCwd: cwd,
agentOverrides,
interactiveUserInput: options?.interactiveUserInput === true,
interactiveMetadata: options?.interactiveMetadata,
});
if (taskSuccess && isWorktree) {
const commitResult = autoCommitAndPush(execCwd, task, cwd);
if (commitResult.success && commitResult.commitHash) {
success(`Auto-committed & pushed: ${commitResult.commitHash}`);
} else if (!commitResult.success) {
error(`Auto-commit failed: ${commitResult.message}`);
}
if (commitResult.success && commitResult.commitHash && branch) {
const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false);
if (shouldCreatePr) {
info('Creating pull request...');
const prBody = buildPrBody(undefined, `Piece \`${pieceIdentifier}\` completed successfully.`);
const prResult = createPullRequest(execCwd, {
branch,
title: task.length > 100 ? `${task.slice(0, 97)}...` : task,
body: prBody,
repo: options?.repo,
});
if (prResult.success) {
success(`PR created: ${prResult.url}`);
} else {
error(`PR creation failed: ${prResult.error}`);
}
}
}
}
if (!taskSuccess) {
process.exit(1);
}
}