refactor
This commit is contained in:
parent
b648a8ea6b
commit
482fa51266
@ -71,9 +71,13 @@ vi.mock('../config/workflowLoader.js', () => ({
|
||||
listWorkflows: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
vi.mock('../constants.js', () => ({
|
||||
DEFAULT_WORKFLOW_NAME: 'default',
|
||||
}));
|
||||
vi.mock('../constants.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../constants.js')>();
|
||||
return {
|
||||
...actual,
|
||||
DEFAULT_WORKFLOW_NAME: 'default',
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../github/issue.js', () => ({
|
||||
isIssueReference: vi.fn((s: string) => /^#\d+$/.test(s)),
|
||||
@ -88,7 +92,7 @@ import { confirm } from '../prompt/index.js';
|
||||
import { createSharedClone } from '../task/clone.js';
|
||||
import { summarizeTaskName } from '../task/summarize.js';
|
||||
import { info } from '../utils/ui.js';
|
||||
import { confirmAndCreateWorktree } from '../cli.js';
|
||||
import { confirmAndCreateWorktree } from '../commands/selectAndExecute.js';
|
||||
|
||||
const mockConfirm = vi.mocked(confirm);
|
||||
const mockCreateSharedClone = vi.mocked(createSharedClone);
|
||||
|
||||
@ -20,7 +20,7 @@ vi.mock('../utils/debug.js', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../cli.js', () => ({
|
||||
vi.mock('../context.js', () => ({
|
||||
isQuietMode: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ vi.mock('../config/projectConfig.js', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../cli.js', () => ({
|
||||
vi.mock('../context.js', () => ({
|
||||
isQuietMode: vi.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
|
||||
@ -104,7 +104,7 @@ vi.mock('../config/projectConfig.js', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../cli.js', () => ({
|
||||
vi.mock('../context.js', () => ({
|
||||
isQuietMode: vi.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
|
||||
@ -53,7 +53,7 @@ vi.mock('./workflowExecution.js', () => ({
|
||||
executeWorkflow: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../cli.js', () => ({
|
||||
vi.mock('../context.js', () => ({
|
||||
isQuietMode: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
|
||||
180
src/cli.ts
180
src/cli.ts
@ -27,10 +27,10 @@ import {
|
||||
getEffectiveDebugConfig,
|
||||
} from './config/index.js';
|
||||
import { clearAgentSessions, getCurrentWorkflow, isVerboseMode } from './config/paths.js';
|
||||
import { setQuietMode } from './context.js';
|
||||
import { info, error, success, setLogLevel } from './utils/ui.js';
|
||||
import { initDebugLogger, createLogger, setVerboseConsole } from './utils/debug.js';
|
||||
import {
|
||||
executeTask,
|
||||
runAllTasks,
|
||||
switchWorkflow,
|
||||
switchConfig,
|
||||
@ -41,16 +41,14 @@ import {
|
||||
interactiveMode,
|
||||
executePipeline,
|
||||
} from './commands/index.js';
|
||||
import { listWorkflows, isWorkflowPath } from './config/workflowLoader.js';
|
||||
import { selectOptionWithDefault, confirm } from './prompt/index.js';
|
||||
import { createSharedClone } from './task/clone.js';
|
||||
import { autoCommitAndPush } from './task/autoCommit.js';
|
||||
import { summarizeTaskName } from './task/summarize.js';
|
||||
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
||||
import { checkForUpdates } from './utils/updateNotifier.js';
|
||||
import { getErrorMessage } from './utils/error.js';
|
||||
import { resolveIssueTask, isIssueReference } from './github/issue.js';
|
||||
import { createPullRequest, buildPrBody } from './github/pr.js';
|
||||
import {
|
||||
selectAndExecuteTask,
|
||||
type SelectAndExecuteOptions,
|
||||
} from './commands/selectAndExecute.js';
|
||||
import type { TaskExecutionOptions } from './commands/taskExecution.js';
|
||||
import type { ProviderType } from './providers/index.js';
|
||||
|
||||
@ -70,168 +68,6 @@ let pipelineMode = false;
|
||||
/** Whether quiet mode is active (--quiet flag or config, set in preAction) */
|
||||
let quietMode = false;
|
||||
|
||||
export interface WorktreeConfirmationResult {
|
||||
execCwd: string;
|
||||
isWorktree: boolean;
|
||||
branch?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a workflow interactively.
|
||||
* Returns the selected workflow name, or null if cancelled.
|
||||
*/
|
||||
async function selectWorkflow(cwd: string): Promise<string | null> {
|
||||
const availableWorkflows = listWorkflows(cwd);
|
||||
const currentWorkflow = getCurrentWorkflow(cwd);
|
||||
|
||||
if (availableWorkflows.length === 0) {
|
||||
info(`No workflows found. Using default: ${DEFAULT_WORKFLOW_NAME}`);
|
||||
return DEFAULT_WORKFLOW_NAME;
|
||||
}
|
||||
|
||||
if (availableWorkflows.length === 1 && availableWorkflows[0]) {
|
||||
return availableWorkflows[0];
|
||||
}
|
||||
|
||||
const options = availableWorkflows.map((name) => ({
|
||||
label: name === currentWorkflow ? `${name} (current)` : name,
|
||||
value: name,
|
||||
}));
|
||||
|
||||
const defaultWorkflow = availableWorkflows.includes(currentWorkflow)
|
||||
? currentWorkflow
|
||||
: (availableWorkflows.includes(DEFAULT_WORKFLOW_NAME)
|
||||
? DEFAULT_WORKFLOW_NAME
|
||||
: availableWorkflows[0] || DEFAULT_WORKFLOW_NAME);
|
||||
|
||||
return selectOptionWithDefault('Select workflow:', options, defaultWorkflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a task with workflow selection, optional worktree, and auto-commit.
|
||||
* Shared by direct task execution and interactive mode.
|
||||
*/
|
||||
export interface SelectAndExecuteOptions {
|
||||
autoPr?: boolean;
|
||||
repo?: string;
|
||||
workflow?: string;
|
||||
createWorktree?: boolean | undefined;
|
||||
}
|
||||
|
||||
async function selectAndExecuteTask(
|
||||
cwd: string,
|
||||
task: string,
|
||||
options?: SelectAndExecuteOptions,
|
||||
agentOverrides?: TaskExecutionOptions,
|
||||
): Promise<void> {
|
||||
const workflowIdentifier = await determineWorkflow(cwd, options?.workflow);
|
||||
|
||||
if (workflowIdentifier === null) {
|
||||
info('Cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const { execCwd, isWorktree, branch } = await confirmAndCreateWorktree(
|
||||
cwd,
|
||||
task,
|
||||
options?.createWorktree,
|
||||
);
|
||||
|
||||
log.info('Starting task execution', { workflow: workflowIdentifier, worktree: isWorktree });
|
||||
const taskSuccess = await executeTask({
|
||||
task,
|
||||
cwd: execCwd,
|
||||
workflowIdentifier,
|
||||
projectCwd: cwd,
|
||||
agentOverrides,
|
||||
});
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
// PR creation: --auto-pr → create automatically, otherwise ask
|
||||
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, `Workflow \`${workflowIdentifier}\` 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine workflow to use.
|
||||
*
|
||||
* - If override looks like a path (isWorkflowPath), return it directly (validation is done at load time).
|
||||
* - If override is a name, validate it exists in available workflows.
|
||||
* - If no override, prompt user to select interactively.
|
||||
*/
|
||||
async function determineWorkflow(cwd: string, override?: string): Promise<string | null> {
|
||||
if (override) {
|
||||
// Path-based: skip name validation (loader handles existence check)
|
||||
if (isWorkflowPath(override)) {
|
||||
return override;
|
||||
}
|
||||
// Name-based: validate workflow name exists
|
||||
const availableWorkflows = listWorkflows(cwd);
|
||||
const knownWorkflows = availableWorkflows.length === 0 ? [DEFAULT_WORKFLOW_NAME] : availableWorkflows;
|
||||
if (!knownWorkflows.includes(override)) {
|
||||
error(`Workflow not found: ${override}`);
|
||||
return null;
|
||||
}
|
||||
return override;
|
||||
}
|
||||
return selectWorkflow(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 };
|
||||
}
|
||||
|
||||
// Summarize task name to English slug using AI
|
||||
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 };
|
||||
}
|
||||
|
||||
const program = new Command();
|
||||
|
||||
function resolveAgentOverrides(): TaskExecutionOptions | undefined {
|
||||
@ -315,14 +151,12 @@ program.hook('preAction', async () => {
|
||||
|
||||
// Quiet mode: CLI flag takes precedence over config
|
||||
quietMode = rootOpts.quiet === true || config.minimalOutput === true;
|
||||
setQuietMode(quietMode);
|
||||
|
||||
log.info('TAKT CLI starting', { version: cliVersion, cwd: resolvedCwd, verbose, pipelineMode, quietMode });
|
||||
});
|
||||
|
||||
/** Get whether quiet mode is active (CLI flag or config, resolved in preAction) */
|
||||
export function isQuietMode(): boolean {
|
||||
return quietMode;
|
||||
}
|
||||
// isQuietMode is now exported from context.ts to avoid circular dependencies
|
||||
|
||||
// --- Subcommands ---
|
||||
|
||||
|
||||
@ -13,3 +13,9 @@ export { switchConfig, getCurrentPermissionMode, setPermissionMode, type Permiss
|
||||
export { listTasks } from './listTasks.js';
|
||||
export { interactiveMode } from './interactive.js';
|
||||
export { executePipeline, type PipelineExecutionOptions } from './pipelineExecution.js';
|
||||
export {
|
||||
selectAndExecuteTask,
|
||||
confirmAndCreateWorktree,
|
||||
type SelectAndExecuteOptions,
|
||||
type WorktreeConfirmationResult,
|
||||
} from './selectAndExecute.js';
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
import * as readline from 'node:readline';
|
||||
import chalk from 'chalk';
|
||||
import { loadGlobalConfig } from '../config/globalConfig.js';
|
||||
import { isQuietMode } from '../cli.js';
|
||||
import { isQuietMode } from '../context.js';
|
||||
import { loadAgentSessions, updateAgentSession } from '../config/paths.js';
|
||||
import { getProvider, type ProviderType } from '../providers/index.js';
|
||||
import { createLogger } from '../utils/debug.js';
|
||||
|
||||
184
src/commands/selectAndExecute.ts
Normal file
184
src/commands/selectAndExecute.ts
Normal file
@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Task execution orchestration.
|
||||
*
|
||||
* Coordinates workflow selection, worktree creation, task execution,
|
||||
* auto-commit, and PR creation. Extracted from cli.ts to avoid
|
||||
* mixing CLI parsing with business logic.
|
||||
*/
|
||||
|
||||
import { getCurrentWorkflow } from '../config/paths.js';
|
||||
import { listWorkflows, isWorkflowPath } from '../config/workflowLoader.js';
|
||||
import { selectOptionWithDefault, confirm } from '../prompt/index.js';
|
||||
import { createSharedClone } from '../task/clone.js';
|
||||
import { autoCommitAndPush } from '../task/autoCommit.js';
|
||||
import { summarizeTaskName } from '../task/summarize.js';
|
||||
import { DEFAULT_WORKFLOW_NAME } from '../constants.js';
|
||||
import { info, error, success } from '../utils/ui.js';
|
||||
import { createLogger } from '../utils/debug.js';
|
||||
import { createPullRequest, buildPrBody } from '../github/pr.js';
|
||||
import { executeTask } from './taskExecution.js';
|
||||
import type { TaskExecutionOptions } from './taskExecution.js';
|
||||
|
||||
const log = createLogger('selectAndExecute');
|
||||
|
||||
export interface WorktreeConfirmationResult {
|
||||
execCwd: string;
|
||||
isWorktree: boolean;
|
||||
branch?: string;
|
||||
}
|
||||
|
||||
export interface SelectAndExecuteOptions {
|
||||
autoPr?: boolean;
|
||||
repo?: string;
|
||||
workflow?: string;
|
||||
createWorktree?: boolean | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a workflow interactively.
|
||||
* Returns the selected workflow name, or null if cancelled.
|
||||
*/
|
||||
async function selectWorkflow(cwd: string): Promise<string | null> {
|
||||
const availableWorkflows = listWorkflows(cwd);
|
||||
const currentWorkflow = getCurrentWorkflow(cwd);
|
||||
|
||||
if (availableWorkflows.length === 0) {
|
||||
info(`No workflows found. Using default: ${DEFAULT_WORKFLOW_NAME}`);
|
||||
return DEFAULT_WORKFLOW_NAME;
|
||||
}
|
||||
|
||||
if (availableWorkflows.length === 1 && availableWorkflows[0]) {
|
||||
return availableWorkflows[0];
|
||||
}
|
||||
|
||||
const options = availableWorkflows.map((name) => ({
|
||||
label: name === currentWorkflow ? `${name} (current)` : name,
|
||||
value: name,
|
||||
}));
|
||||
|
||||
const defaultWorkflow = availableWorkflows.includes(currentWorkflow)
|
||||
? currentWorkflow
|
||||
: (availableWorkflows.includes(DEFAULT_WORKFLOW_NAME)
|
||||
? DEFAULT_WORKFLOW_NAME
|
||||
: availableWorkflows[0] || DEFAULT_WORKFLOW_NAME);
|
||||
|
||||
return selectOptionWithDefault('Select workflow:', options, defaultWorkflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine workflow to use.
|
||||
*
|
||||
* - If override looks like a path (isWorkflowPath), return it directly (validation is done at load time).
|
||||
* - If override is a name, validate it exists in available workflows.
|
||||
* - If no override, prompt user to select interactively.
|
||||
*/
|
||||
async function determineWorkflow(cwd: string, override?: string): Promise<string | null> {
|
||||
if (override) {
|
||||
// Path-based: skip name validation (loader handles existence check)
|
||||
if (isWorkflowPath(override)) {
|
||||
return override;
|
||||
}
|
||||
// Name-based: validate workflow name exists
|
||||
const availableWorkflows = listWorkflows(cwd);
|
||||
const knownWorkflows = availableWorkflows.length === 0 ? [DEFAULT_WORKFLOW_NAME] : availableWorkflows;
|
||||
if (!knownWorkflows.includes(override)) {
|
||||
error(`Workflow not found: ${override}`);
|
||||
return null;
|
||||
}
|
||||
return override;
|
||||
}
|
||||
return selectWorkflow(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 };
|
||||
}
|
||||
|
||||
// Summarize task name to English slug using AI
|
||||
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 workflow 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 workflowIdentifier = await determineWorkflow(cwd, options?.workflow);
|
||||
|
||||
if (workflowIdentifier === null) {
|
||||
info('Cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const { execCwd, isWorktree, branch } = await confirmAndCreateWorktree(
|
||||
cwd,
|
||||
task,
|
||||
options?.createWorktree,
|
||||
);
|
||||
|
||||
log.info('Starting task execution', { workflow: workflowIdentifier, worktree: isWorktree });
|
||||
const taskSuccess = await executeTask({
|
||||
task,
|
||||
cwd: execCwd,
|
||||
workflowIdentifier,
|
||||
projectCwd: cwd,
|
||||
agentOverrides,
|
||||
});
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
// PR creation: --auto-pr → create automatically, otherwise ask
|
||||
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, `Workflow \`${workflowIdentifier}\` 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);
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ import {
|
||||
updateWorktreeSession,
|
||||
} from '../config/paths.js';
|
||||
import { loadGlobalConfig } from '../config/globalConfig.js';
|
||||
import { isQuietMode } from '../cli.js';
|
||||
import { isQuietMode } from '../context.js';
|
||||
import {
|
||||
header,
|
||||
info,
|
||||
|
||||
19
src/context.ts
Normal file
19
src/context.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Runtime context shared across modules.
|
||||
*
|
||||
* Holds process-wide state (quiet mode, etc.) that would otherwise
|
||||
* create circular dependencies if exported from cli.ts.
|
||||
*/
|
||||
|
||||
/** Whether quiet mode is active (set during CLI initialization) */
|
||||
let quietMode = false;
|
||||
|
||||
/** Get whether quiet mode is active (CLI flag or config, resolved in preAction) */
|
||||
export function isQuietMode(): boolean {
|
||||
return quietMode;
|
||||
}
|
||||
|
||||
/** Set quiet mode state. Called from CLI preAction hook. */
|
||||
export function setQuietMode(value: boolean): void {
|
||||
quietMode = value;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user