feat: workflowやconfig指定をオプションで受け入れpath対応にする (#81)
This commit is contained in:
parent
05bf51cfbb
commit
14130ee958
@ -150,6 +150,7 @@ describe('executePipeline', () => {
|
||||
'Fix the bug',
|
||||
'/tmp/test',
|
||||
'default',
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
@ -172,6 +173,7 @@ describe('executePipeline', () => {
|
||||
'Fix the bug',
|
||||
'/tmp/test',
|
||||
'default',
|
||||
false,
|
||||
undefined,
|
||||
{ provider: 'codex', model: 'codex-model' },
|
||||
);
|
||||
@ -229,6 +231,7 @@ describe('executePipeline', () => {
|
||||
'From --task flag',
|
||||
'/tmp/test',
|
||||
'magi',
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
@ -389,6 +392,7 @@ describe('executePipeline', () => {
|
||||
'Fix the bug',
|
||||
'/tmp/test',
|
||||
'default',
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
189
src/__tests__/workflow-path-loading.test.ts
Normal file
189
src/__tests__/workflow-path-loading.test.ts
Normal file
@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Tests for path-based workflow loading
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
import { loadWorkflow, loadWorkflowFromPath } from '../config/workflowLoader.js';
|
||||
|
||||
describe('Path-based workflow loading', () => {
|
||||
let tempDir: string;
|
||||
let projectDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create temporary directories for testing
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'takt-test-'));
|
||||
projectDir = mkdtempSync(join(tmpdir(), 'takt-project-'));
|
||||
|
||||
// Create a test workflow in temp directory
|
||||
writeFileSync(
|
||||
join(tempDir, 'test-workflow.yaml'),
|
||||
`name: test-path-workflow
|
||||
description: Test workflow for path-based loading
|
||||
initial_step: plan
|
||||
max_iterations: 5
|
||||
|
||||
steps:
|
||||
- name: plan
|
||||
agent: planner
|
||||
instruction: "Plan the task"
|
||||
rules:
|
||||
- condition: ai("Ready?")
|
||||
next: implement
|
||||
|
||||
- name: implement
|
||||
agent: coder
|
||||
instruction: "Implement"
|
||||
`,
|
||||
);
|
||||
|
||||
// Create project-local workflow directory
|
||||
const projectWorkflowsDir = join(projectDir, '.takt', 'workflows');
|
||||
rmSync(projectWorkflowsDir, { recursive: true, force: true });
|
||||
writeFileSync(
|
||||
join(tempDir, 'project-local.yaml'),
|
||||
`name: project-local-workflow
|
||||
description: Project-local workflow
|
||||
initial_step: test
|
||||
max_iterations: 1
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
agent: tester
|
||||
instruction: "Run tests"
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temporary directories
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
rmSync(projectDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should load workflow by absolute path', () => {
|
||||
const absolutePath = join(tempDir, 'test-workflow.yaml');
|
||||
const workflow = loadWorkflowFromPath(absolutePath);
|
||||
|
||||
expect(workflow).not.toBeNull();
|
||||
expect(workflow!.name).toBe('test-path-workflow');
|
||||
expect(workflow!.description).toBe('Test workflow for path-based loading');
|
||||
});
|
||||
|
||||
it('should load workflow by relative path', () => {
|
||||
const originalCwd = process.cwd();
|
||||
try {
|
||||
process.chdir(tempDir);
|
||||
const relativePath = './test-workflow.yaml';
|
||||
const workflow = loadWorkflowFromPath(relativePath, tempDir);
|
||||
|
||||
expect(workflow).not.toBeNull();
|
||||
expect(workflow!.name).toBe('test-path-workflow');
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
|
||||
it('should load workflow with .yaml extension in name', () => {
|
||||
const pathWithExtension = join(tempDir, 'test-workflow.yaml');
|
||||
const workflow = loadWorkflowFromPath(pathWithExtension);
|
||||
|
||||
expect(workflow).not.toBeNull();
|
||||
expect(workflow!.name).toBe('test-path-workflow');
|
||||
});
|
||||
|
||||
it('should return null for non-existent path', () => {
|
||||
const nonExistentPath = join(tempDir, 'non-existent.yaml');
|
||||
const workflow = loadWorkflowFromPath(nonExistentPath);
|
||||
|
||||
expect(workflow).toBeNull();
|
||||
});
|
||||
|
||||
it('should maintain backward compatibility with name-based loading', () => {
|
||||
// Load builtin workflow by name
|
||||
const workflow = loadWorkflow('default');
|
||||
|
||||
expect(workflow).not.toBeNull();
|
||||
expect(workflow!.name).toBe('default');
|
||||
});
|
||||
|
||||
it('should prioritize project-local workflows over global when loading by name', () => {
|
||||
// Create project-local workflow directory
|
||||
const projectWorkflowsDir = join(projectDir, '.takt', 'workflows');
|
||||
mkdirSync(projectWorkflowsDir, { recursive: true });
|
||||
|
||||
// Create project-local workflow with same name as builtin
|
||||
writeFileSync(
|
||||
join(projectWorkflowsDir, 'default.yaml'),
|
||||
`name: project-override
|
||||
description: Project-local override of default workflow
|
||||
initial_step: custom
|
||||
max_iterations: 1
|
||||
|
||||
steps:
|
||||
- name: custom
|
||||
agent: custom
|
||||
instruction: "Custom step"
|
||||
`,
|
||||
);
|
||||
|
||||
// Load by name with projectCwd - should get project-local version
|
||||
const workflow = loadWorkflow('default', projectDir);
|
||||
|
||||
expect(workflow).not.toBeNull();
|
||||
expect(workflow!.name).toBe('project-override');
|
||||
expect(workflow!.description).toBe('Project-local override of default workflow');
|
||||
});
|
||||
|
||||
it('should load workflows via loadWorkflowFromPath function', () => {
|
||||
// Absolute paths
|
||||
const pathWithSlash = join(tempDir, 'test-workflow.yaml');
|
||||
const workflow1 = loadWorkflowFromPath(pathWithSlash);
|
||||
expect(workflow1).not.toBeNull();
|
||||
|
||||
// Relative paths
|
||||
const workflow2 = loadWorkflowFromPath('./test-workflow.yaml', tempDir);
|
||||
expect(workflow2).not.toBeNull();
|
||||
|
||||
// Explicit path loading
|
||||
const yamlFile = join(tempDir, 'test-workflow.yaml');
|
||||
const workflow3 = loadWorkflowFromPath(yamlFile);
|
||||
expect(workflow3).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should handle workflow files with .yml extension', () => {
|
||||
// Create workflow with .yml extension
|
||||
const ymlPath = join(tempDir, 'test-yml.yml');
|
||||
writeFileSync(
|
||||
ymlPath,
|
||||
`name: yml-workflow
|
||||
description: Workflow with .yml extension
|
||||
initial_step: start
|
||||
max_iterations: 1
|
||||
|
||||
steps:
|
||||
- name: start
|
||||
agent: starter
|
||||
instruction: "Start"
|
||||
`,
|
||||
);
|
||||
|
||||
const workflow = loadWorkflowFromPath(ymlPath);
|
||||
|
||||
expect(workflow).not.toBeNull();
|
||||
expect(workflow!.name).toBe('yml-workflow');
|
||||
});
|
||||
|
||||
it('should resolve relative paths against provided base directory', () => {
|
||||
const relativePath = 'test-workflow.yaml';
|
||||
const workflow = loadWorkflowFromPath(relativePath, tempDir);
|
||||
|
||||
expect(workflow).not.toBeNull();
|
||||
expect(workflow!.name).toBe('test-path-workflow');
|
||||
});
|
||||
});
|
||||
|
||||
// Import for test setup
|
||||
import { mkdirSync } from 'node:fs';
|
||||
69
src/cli.ts
69
src/cli.ts
@ -111,6 +111,7 @@ export interface SelectAndExecuteOptions {
|
||||
autoPr?: boolean;
|
||||
repo?: string;
|
||||
workflow?: string;
|
||||
workflowPath?: string;
|
||||
createWorktree?: boolean | undefined;
|
||||
}
|
||||
|
||||
@ -120,9 +121,24 @@ async function selectAndExecuteTask(
|
||||
options?: SelectAndExecuteOptions,
|
||||
agentOverrides?: TaskExecutionOptions,
|
||||
): Promise<void> {
|
||||
const selectedWorkflow = await determineWorkflow(cwd, options?.workflow);
|
||||
// Validate that only one workflow option is specified
|
||||
if (options?.workflow && options?.workflowPath) {
|
||||
error('Cannot specify both --workflow and --workflow-path');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (selectedWorkflow === null) {
|
||||
let workflowIdentifier: string | null;
|
||||
let isPathBased = false;
|
||||
|
||||
if (options?.workflowPath) {
|
||||
workflowIdentifier = await determineWorkflowPath(cwd, options.workflowPath);
|
||||
isPathBased = true;
|
||||
} else {
|
||||
workflowIdentifier = await determineWorkflow(cwd, options?.workflow);
|
||||
isPathBased = false;
|
||||
}
|
||||
|
||||
if (workflowIdentifier === null) {
|
||||
info('Cancelled');
|
||||
return;
|
||||
}
|
||||
@ -133,8 +149,8 @@ async function selectAndExecuteTask(
|
||||
options?.createWorktree,
|
||||
);
|
||||
|
||||
log.info('Starting task execution', { workflow: selectedWorkflow, worktree: isWorktree });
|
||||
const taskSuccess = await executeTask(task, execCwd, selectedWorkflow, cwd, agentOverrides);
|
||||
log.info('Starting task execution', { workflow: workflowIdentifier, worktree: isWorktree, pathBased: isPathBased });
|
||||
const taskSuccess = await executeTask(task, execCwd, workflowIdentifier, isPathBased, cwd, agentOverrides);
|
||||
|
||||
if (taskSuccess && isWorktree) {
|
||||
const commitResult = autoCommitAndPush(execCwd, task, cwd);
|
||||
@ -149,7 +165,7 @@ async function selectAndExecuteTask(
|
||||
const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false);
|
||||
if (shouldCreatePr) {
|
||||
info('Creating pull request...');
|
||||
const prBody = buildPrBody(undefined, `Workflow \`${selectedWorkflow}\` completed successfully.`);
|
||||
const prBody = buildPrBody(undefined, `Workflow \`${workflowIdentifier}\` completed successfully.`);
|
||||
const prResult = createPullRequest(execCwd, {
|
||||
branch,
|
||||
title: task.length > 100 ? `${task.slice(0, 97)}...` : task,
|
||||
@ -171,12 +187,17 @@ async function selectAndExecuteTask(
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user whether to create a shared clone, and create one if confirmed.
|
||||
* Returns the execution directory and whether a clone was created.
|
||||
* Task name is summarized to English by AI for use in branch/clone names.
|
||||
* Determine workflow to use.
|
||||
* If override is provided, validate it (either as a name or path).
|
||||
* Otherwise, prompt user to select interactively.
|
||||
*/
|
||||
/**
|
||||
* Determine workflow to use (name-based only).
|
||||
* For path-based loading, use determineWorkflowPath() instead.
|
||||
*/
|
||||
async function determineWorkflow(cwd: string, override?: string): Promise<string | null> {
|
||||
if (override) {
|
||||
// Validate workflow name exists
|
||||
const availableWorkflows = listWorkflows();
|
||||
const knownWorkflows = availableWorkflows.length === 0 ? [DEFAULT_WORKFLOW_NAME] : availableWorkflows;
|
||||
if (!knownWorkflows.includes(override)) {
|
||||
@ -188,6 +209,34 @@ async function determineWorkflow(cwd: string, override?: string): Promise<string
|
||||
return selectWorkflow(cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine workflow path (path-based loading).
|
||||
* Validates that the file exists.
|
||||
*/
|
||||
async function determineWorkflowPath(cwd: string, workflowPath: string): Promise<string | null> {
|
||||
const { existsSync } = await import('node:fs');
|
||||
const { resolve: resolvePath, isAbsolute } = await import('node:path');
|
||||
const { homedir } = await import('node:os');
|
||||
|
||||
let resolvedPath = workflowPath;
|
||||
|
||||
// Handle home directory
|
||||
if (workflowPath.startsWith('~')) {
|
||||
const home = homedir();
|
||||
resolvedPath = resolvePath(home, workflowPath.slice(1).replace(/^\//, ''));
|
||||
} else if (!isAbsolute(workflowPath)) {
|
||||
// Relative path
|
||||
resolvedPath = resolvePath(cwd, workflowPath);
|
||||
}
|
||||
|
||||
if (!existsSync(resolvedPath)) {
|
||||
error(`Workflow file not found: ${workflowPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return workflowPath; // Return original path (loader will resolve it)
|
||||
}
|
||||
|
||||
export async function confirmAndCreateWorktree(
|
||||
cwd: string,
|
||||
task: string,
|
||||
@ -254,7 +303,8 @@ program
|
||||
// --- Global options ---
|
||||
program
|
||||
.option('-i, --issue <number>', 'GitHub issue number (equivalent to #N)', (val: string) => parseInt(val, 10))
|
||||
.option('-w, --workflow <name>', 'Workflow to use')
|
||||
.option('-w, --workflow <name>', 'Workflow name to use')
|
||||
.option('--workflow-path <path>', 'Path to workflow file (alternative to --workflow)')
|
||||
.option('-b, --branch <name>', 'Branch name (auto-generated if omitted)')
|
||||
.option('--auto-pr', 'Create PR after successful execution')
|
||||
.option('--repo <owner/repo>', 'Repository (defaults to current)')
|
||||
@ -388,6 +438,7 @@ program
|
||||
autoPr: opts.autoPr === true,
|
||||
repo: opts.repo as string | undefined,
|
||||
workflow: opts.workflow as string | undefined,
|
||||
workflowPath: opts.workflowPath as string | undefined,
|
||||
createWorktree: createWorktreeOverride,
|
||||
};
|
||||
|
||||
|
||||
@ -324,7 +324,7 @@ export async function instructBranch(
|
||||
: instruction;
|
||||
|
||||
// 5. Execute task on temp clone
|
||||
const taskSuccess = await executeTask(fullInstruction, clone.path, selectedWorkflow, projectDir, options);
|
||||
const taskSuccess = await executeTask(fullInstruction, clone.path, selectedWorkflow, false, projectDir, options);
|
||||
|
||||
// 6. Auto-commit+push if successful
|
||||
if (taskSuccess) {
|
||||
|
||||
@ -191,7 +191,7 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
|
||||
? { provider: options.provider, model: options.model }
|
||||
: undefined;
|
||||
|
||||
const taskSuccess = await executeTask(task, cwd, workflow, undefined, agentOverrides);
|
||||
const taskSuccess = await executeTask(task, cwd, workflow, false, undefined, agentOverrides);
|
||||
|
||||
if (!taskSuccess) {
|
||||
error(`Workflow '${workflow}' failed`);
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Task execution logic
|
||||
*/
|
||||
|
||||
import { loadWorkflow, loadGlobalConfig } from '../config/index.js';
|
||||
import { loadWorkflow, loadWorkflowFromPath, loadGlobalConfig } from '../config/index.js';
|
||||
import { TaskRunner, type TaskInfo } from '../task/index.js';
|
||||
import { createSharedClone } from '../task/clone.js';
|
||||
import { autoCommitAndPush } from '../task/autoCommit.js';
|
||||
@ -31,28 +31,38 @@ export interface TaskExecutionOptions {
|
||||
* Execute a single task with workflow
|
||||
* @param task - Task content
|
||||
* @param cwd - Working directory (may be a clone path)
|
||||
* @param workflowName - Workflow to use
|
||||
* @param workflowIdentifier - Workflow name or path
|
||||
* @param isPathBased - True if workflowIdentifier is a file path, false if it's a name
|
||||
* @param projectCwd - Project root (where .takt/ lives). Defaults to cwd.
|
||||
*/
|
||||
export async function executeTask(
|
||||
task: string,
|
||||
cwd: string,
|
||||
workflowName: string = DEFAULT_WORKFLOW_NAME,
|
||||
workflowIdentifier: string = DEFAULT_WORKFLOW_NAME,
|
||||
isPathBased: boolean = false,
|
||||
projectCwd?: string,
|
||||
options?: TaskExecutionOptions
|
||||
): Promise<boolean> {
|
||||
const workflowConfig = loadWorkflow(workflowName);
|
||||
const effectiveProjectCwd = projectCwd || cwd;
|
||||
|
||||
const workflowConfig = isPathBased
|
||||
? loadWorkflowFromPath(workflowIdentifier, effectiveProjectCwd)
|
||||
: loadWorkflow(workflowIdentifier, effectiveProjectCwd);
|
||||
|
||||
if (!workflowConfig) {
|
||||
error(`Workflow "${workflowName}" not found.`);
|
||||
info('Available workflows are in ~/.takt/workflows/');
|
||||
info('Use "takt switch" to select a workflow.');
|
||||
if (isPathBased) {
|
||||
error(`Workflow file not found: ${workflowIdentifier}`);
|
||||
} else {
|
||||
error(`Workflow "${workflowIdentifier}" not found.`);
|
||||
info('Available workflows are in ~/.takt/workflows/ or .takt/workflows/');
|
||||
info('Use "takt switch" to select a workflow.');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug('Running workflow', {
|
||||
name: workflowConfig.name,
|
||||
steps: workflowConfig.steps.map(s => s.name),
|
||||
steps: workflowConfig.steps.map((s: { name: string }) => s.name),
|
||||
});
|
||||
|
||||
const globalConfig = loadGlobalConfig();
|
||||
@ -87,7 +97,7 @@ export async function executeAndCompleteTask(
|
||||
const { execCwd, execWorkflow, isWorktree } = await resolveTaskExecution(task, cwd, workflowName);
|
||||
|
||||
// cwd is always the project root; pass it as projectCwd so reports/sessions go there
|
||||
const taskSuccess = await executeTask(task.content, execCwd, execWorkflow, cwd, options);
|
||||
const taskSuccess = await executeTask(task.content, execCwd, execWorkflow, false, cwd, options);
|
||||
const completedAt = new Date().toISOString();
|
||||
|
||||
if (taskSuccess && isWorktree) {
|
||||
|
||||
@ -48,7 +48,7 @@ export async function switchWorkflow(cwd: string, workflowName?: string): Promis
|
||||
}
|
||||
|
||||
// Check if workflow exists
|
||||
const config = getBuiltinWorkflow(workflowName) || loadWorkflow(workflowName);
|
||||
const config = getBuiltinWorkflow(workflowName) || loadWorkflow(workflowName, cwd);
|
||||
|
||||
if (!config) {
|
||||
error(`Workflow "${workflowName}" not found`);
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
export {
|
||||
getBuiltinWorkflow,
|
||||
loadWorkflowFromFile,
|
||||
loadWorkflowFromPath,
|
||||
loadWorkflow,
|
||||
loadAllWorkflows,
|
||||
listWorkflows,
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
/**
|
||||
* Workflow configuration loader
|
||||
*
|
||||
* Loads workflows with user → builtin fallback:
|
||||
* 1. User workflows: ~/.takt/workflows/{name}.yaml
|
||||
* 2. Builtin workflows: resources/global/{lang}/workflows/{name}.yaml
|
||||
* Loads workflows with the following priority:
|
||||
* 1. Path-based input (absolute, relative, or home-dir) → load directly from file
|
||||
* 2. Project-local workflows: .takt/workflows/{name}.yaml
|
||||
* 3. User workflows: ~/.takt/workflows/{name}.yaml
|
||||
* 4. Builtin workflows: resources/global/{lang}/workflows/{name}.yaml
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
||||
import { join, dirname, basename } from 'node:path';
|
||||
import { join, dirname, basename, resolve, isAbsolute } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { WorkflowConfigRawSchema } from '../models/schemas.js';
|
||||
import type { WorkflowConfig, WorkflowStep, WorkflowRule, ReportConfig, ReportObjectConfig } from '../models/types.js';
|
||||
import { getGlobalWorkflowsDir, getBuiltinWorkflowsDir } from './paths.js';
|
||||
import { getGlobalWorkflowsDir, getBuiltinWorkflowsDir, getProjectConfigDir } from './paths.js';
|
||||
import { getLanguage, getDisabledBuiltins } from './globalConfig.js';
|
||||
|
||||
/** Get builtin workflow by name */
|
||||
@ -241,18 +244,78 @@ export function loadWorkflowFromFile(filePath: string): WorkflowConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load workflow by name.
|
||||
* Priority: user (~/.takt/workflows/) → builtin (resources/global/{lang}/workflows/)
|
||||
* Resolve a path that may be relative, absolute, or home-directory-relative.
|
||||
* @param pathInput Path to resolve
|
||||
* @param basePath Base directory for relative paths (defaults to cwd)
|
||||
* @returns Absolute resolved path
|
||||
*/
|
||||
export function loadWorkflow(name: string): WorkflowConfig | null {
|
||||
// 1. User workflow
|
||||
function resolvePath(pathInput: string, basePath: string = process.cwd()): string {
|
||||
// Home directory expansion
|
||||
if (pathInput.startsWith('~')) {
|
||||
const home = homedir();
|
||||
return resolve(home, pathInput.slice(1).replace(/^\//, ''));
|
||||
}
|
||||
|
||||
// Absolute path
|
||||
if (isAbsolute(pathInput)) {
|
||||
return pathInput;
|
||||
}
|
||||
|
||||
// Relative path
|
||||
return resolve(basePath, pathInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load workflow from a file path (explicit path-based loading).
|
||||
* Use this when user explicitly specifies a workflow file path via --workflow-path.
|
||||
*
|
||||
* @param filePath Path to workflow file (absolute, relative, or home-dir prefixed with ~)
|
||||
* @param basePath Base directory for resolving relative paths (default: cwd)
|
||||
* @returns WorkflowConfig or null if file not found
|
||||
*/
|
||||
export function loadWorkflowFromPath(
|
||||
filePath: string,
|
||||
basePath: string = process.cwd()
|
||||
): WorkflowConfig | null {
|
||||
const resolvedPath = resolvePath(filePath, basePath);
|
||||
|
||||
if (!existsSync(resolvedPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return loadWorkflowFromFile(resolvedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load workflow by name (name-based loading only, no path detection).
|
||||
*
|
||||
* Priority:
|
||||
* 1. Project-local workflows → .takt/workflows/{name}.yaml
|
||||
* 2. User workflows → ~/.takt/workflows/{name}.yaml
|
||||
* 3. Builtin workflows → resources/global/{lang}/workflows/{name}.yaml
|
||||
*
|
||||
* @param name Workflow name (not a file path)
|
||||
* @param projectCwd Project root directory (default: cwd, for project-local workflow resolution)
|
||||
*/
|
||||
export function loadWorkflow(
|
||||
name: string,
|
||||
projectCwd: string = process.cwd()
|
||||
): WorkflowConfig | null {
|
||||
// 1. Project-local workflow (.takt/workflows/{name}.yaml)
|
||||
const projectWorkflowsDir = join(getProjectConfigDir(projectCwd), 'workflows');
|
||||
const projectWorkflowPath = join(projectWorkflowsDir, `${name}.yaml`);
|
||||
if (existsSync(projectWorkflowPath)) {
|
||||
return loadWorkflowFromFile(projectWorkflowPath);
|
||||
}
|
||||
|
||||
// 2. User workflow (~/.takt/workflows/{name}.yaml)
|
||||
const globalWorkflowsDir = getGlobalWorkflowsDir();
|
||||
const workflowYamlPath = join(globalWorkflowsDir, `${name}.yaml`);
|
||||
if (existsSync(workflowYamlPath)) {
|
||||
return loadWorkflowFromFile(workflowYamlPath);
|
||||
}
|
||||
|
||||
// 2. Builtin fallback
|
||||
// 3. Builtin fallback
|
||||
return getBuiltinWorkflow(name);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user