refactor: 設定参照をresolveConfigValueへ統一
This commit is contained in:
parent
5dc79946f2
commit
cbde7ac654
@ -17,8 +17,17 @@ import {
|
|||||||
|
|
||||||
// Mock external dependencies to isolate unit tests
|
// Mock external dependencies to isolate unit tests
|
||||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||||
getLanguage: () => 'en',
|
loadGlobalConfig: () => ({}),
|
||||||
getBuiltinPiecesEnabled: () => true,
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||||
|
loadConfig: () => ({
|
||||||
|
global: {
|
||||||
|
language: 'en',
|
||||||
|
enableBuiltinPieces: true,
|
||||||
|
},
|
||||||
|
project: {},
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockLogError = vi.fn();
|
const mockLogError = vi.fn();
|
||||||
|
|||||||
@ -76,7 +76,7 @@ vi.mock('../infra/task/index.js', () => ({
|
|||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })),
|
getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })),
|
||||||
loadConfig: vi.fn(() => ({ global: { interactivePreviewMovements: 3 }, project: {} })),
|
resolveConfigValues: vi.fn(() => ({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/constants.js', () => ({
|
vi.mock('../shared/constants.js', () => ({
|
||||||
@ -107,7 +107,7 @@ vi.mock('../app/cli/helpers.js', () => ({
|
|||||||
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
||||||
import { selectAndExecuteTask, determinePiece, createIssueFromTask, saveTaskFromInteractive } from '../features/tasks/index.js';
|
import { selectAndExecuteTask, determinePiece, createIssueFromTask, saveTaskFromInteractive } from '../features/tasks/index.js';
|
||||||
import { interactiveMode, selectRecentSession } from '../features/interactive/index.js';
|
import { interactiveMode, selectRecentSession } from '../features/interactive/index.js';
|
||||||
import { loadConfig } from '../infra/config/index.js';
|
import { resolveConfigValues } from '../infra/config/index.js';
|
||||||
import { confirm } from '../shared/prompt/index.js';
|
import { confirm } from '../shared/prompt/index.js';
|
||||||
import { isDirectTask } from '../app/cli/helpers.js';
|
import { isDirectTask } from '../app/cli/helpers.js';
|
||||||
import { executeDefaultAction } from '../app/cli/routing.js';
|
import { executeDefaultAction } from '../app/cli/routing.js';
|
||||||
@ -123,7 +123,7 @@ const mockCreateIssueFromTask = vi.mocked(createIssueFromTask);
|
|||||||
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);
|
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);
|
||||||
const mockInteractiveMode = vi.mocked(interactiveMode);
|
const mockInteractiveMode = vi.mocked(interactiveMode);
|
||||||
const mockSelectRecentSession = vi.mocked(selectRecentSession);
|
const mockSelectRecentSession = vi.mocked(selectRecentSession);
|
||||||
const mockLoadConfig = vi.mocked(loadConfig);
|
const mockResolveConfigValues = vi.mocked(resolveConfigValues);
|
||||||
const mockConfirm = vi.mocked(confirm);
|
const mockConfirm = vi.mocked(confirm);
|
||||||
const mockIsDirectTask = vi.mocked(isDirectTask);
|
const mockIsDirectTask = vi.mocked(isDirectTask);
|
||||||
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
|
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
|
||||||
@ -483,7 +483,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
describe('session selection with provider=claude', () => {
|
describe('session selection with provider=claude', () => {
|
||||||
it('should pass selected session ID to interactiveMode when provider is claude', async () => {
|
it('should pass selected session ID to interactiveMode when provider is claude', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockLoadConfig.mockReturnValue({ global: { interactivePreviewMovements: 3, provider: 'claude' }, project: {} });
|
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' });
|
||||||
mockConfirm.mockResolvedValue(true);
|
mockConfirm.mockResolvedValue(true);
|
||||||
mockSelectRecentSession.mockResolvedValue('session-xyz');
|
mockSelectRecentSession.mockResolvedValue('session-xyz');
|
||||||
|
|
||||||
@ -506,7 +506,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
|
|
||||||
it('should not call selectRecentSession when user selects no in confirmation', async () => {
|
it('should not call selectRecentSession when user selects no in confirmation', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockLoadConfig.mockReturnValue({ global: { interactivePreviewMovements: 3, provider: 'claude' }, project: {} });
|
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' });
|
||||||
mockConfirm.mockResolvedValue(false);
|
mockConfirm.mockResolvedValue(false);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
@ -525,7 +525,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
|
|
||||||
it('should not call selectRecentSession when provider is not claude', async () => {
|
it('should not call selectRecentSession when provider is not claude', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockLoadConfig.mockReturnValue({ global: { interactivePreviewMovements: 3, provider: 'openai' }, project: {} });
|
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'openai' });
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await executeDefaultAction();
|
await executeDefaultAction();
|
||||||
|
|||||||
@ -41,13 +41,13 @@ import {
|
|||||||
|
|
||||||
describe('getBuiltinPiece', () => {
|
describe('getBuiltinPiece', () => {
|
||||||
it('should return builtin piece when it exists in resources', () => {
|
it('should return builtin piece when it exists in resources', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
expect(piece).not.toBeNull();
|
expect(piece).not.toBeNull();
|
||||||
expect(piece!.name).toBe('default');
|
expect(piece!.name).toBe('default');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve builtin instruction_template without projectCwd', () => {
|
it('should resolve builtin instruction_template without projectCwd', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
expect(piece).not.toBeNull();
|
expect(piece).not.toBeNull();
|
||||||
|
|
||||||
const planMovement = piece!.movements.find((movement) => movement.name === 'plan');
|
const planMovement = piece!.movements.find((movement) => movement.name === 'plan');
|
||||||
@ -56,15 +56,15 @@ describe('getBuiltinPiece', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for non-existent piece names', () => {
|
it('should return null for non-existent piece names', () => {
|
||||||
expect(getBuiltinPiece('nonexistent-piece')).toBeNull();
|
expect(getBuiltinPiece('nonexistent-piece', process.cwd())).toBeNull();
|
||||||
expect(getBuiltinPiece('unknown')).toBeNull();
|
expect(getBuiltinPiece('unknown', process.cwd())).toBeNull();
|
||||||
expect(getBuiltinPiece('')).toBeNull();
|
expect(getBuiltinPiece('', process.cwd())).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('default piece parallel reviewers movement', () => {
|
describe('default piece parallel reviewers movement', () => {
|
||||||
it('should have a reviewers movement with parallel sub-movements', () => {
|
it('should have a reviewers movement with parallel sub-movements', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
expect(piece).not.toBeNull();
|
expect(piece).not.toBeNull();
|
||||||
|
|
||||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers');
|
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers');
|
||||||
@ -74,7 +74,7 @@ describe('default piece parallel reviewers movement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have arch-review and qa-review as parallel sub-movements', () => {
|
it('should have arch-review and qa-review as parallel sub-movements', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||||
const subMovementNames = reviewersMovement.parallel!.map((s) => s.name);
|
const subMovementNames = reviewersMovement.parallel!.map((s) => s.name);
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ describe('default piece parallel reviewers movement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have aggregate conditions on the reviewers parent movement', () => {
|
it('should have aggregate conditions on the reviewers parent movement', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||||
|
|
||||||
expect(reviewersMovement.rules).toBeDefined();
|
expect(reviewersMovement.rules).toBeDefined();
|
||||||
@ -101,7 +101,7 @@ describe('default piece parallel reviewers movement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have matching conditions on sub-movements for aggregation', () => {
|
it('should have matching conditions on sub-movements for aggregation', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||||
|
|
||||||
for (const subMovement of reviewersMovement.parallel!) {
|
for (const subMovement of reviewersMovement.parallel!) {
|
||||||
@ -113,7 +113,7 @@ describe('default piece parallel reviewers movement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have ai_review transitioning to reviewers movement', () => {
|
it('should have ai_review transitioning to reviewers movement', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
const aiReviewMovement = piece!.movements.find((s) => s.name === 'ai_review')!;
|
const aiReviewMovement = piece!.movements.find((s) => s.name === 'ai_review')!;
|
||||||
|
|
||||||
const approveRule = aiReviewMovement.rules!.find((r) => r.next === 'reviewers');
|
const approveRule = aiReviewMovement.rules!.find((r) => r.next === 'reviewers');
|
||||||
@ -121,7 +121,7 @@ describe('default piece parallel reviewers movement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have ai_fix transitioning to ai_review movement', () => {
|
it('should have ai_fix transitioning to ai_review movement', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
const aiFixMovement = piece!.movements.find((s) => s.name === 'ai_fix')!;
|
const aiFixMovement = piece!.movements.find((s) => s.name === 'ai_fix')!;
|
||||||
|
|
||||||
const fixedRule = aiFixMovement.rules!.find((r) => r.next === 'ai_review');
|
const fixedRule = aiFixMovement.rules!.find((r) => r.next === 'ai_review');
|
||||||
@ -129,7 +129,7 @@ describe('default piece parallel reviewers movement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have fix movement transitioning back to reviewers', () => {
|
it('should have fix movement transitioning back to reviewers', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
const fixMovement = piece!.movements.find((s) => s.name === 'fix')!;
|
const fixMovement = piece!.movements.find((s) => s.name === 'fix')!;
|
||||||
|
|
||||||
const fixedRule = fixMovement.rules!.find((r) => r.next === 'reviewers');
|
const fixedRule = fixMovement.rules!.find((r) => r.next === 'reviewers');
|
||||||
@ -137,7 +137,7 @@ describe('default piece parallel reviewers movement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not have old separate review/security_review/improve movements', () => {
|
it('should not have old separate review/security_review/improve movements', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
const movementNames = piece!.movements.map((s) => s.name);
|
const movementNames = piece!.movements.map((s) => s.name);
|
||||||
|
|
||||||
expect(movementNames).not.toContain('review');
|
expect(movementNames).not.toContain('review');
|
||||||
@ -147,7 +147,7 @@ describe('default piece parallel reviewers movement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have sub-movements with correct agents', () => {
|
it('should have sub-movements with correct agents', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||||
|
|
||||||
const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!;
|
const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!;
|
||||||
@ -158,7 +158,7 @@ describe('default piece parallel reviewers movement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have output contracts configured on sub-movements', () => {
|
it('should have output contracts configured on sub-movements', () => {
|
||||||
const piece = getBuiltinPiece('default');
|
const piece = getBuiltinPiece('default', process.cwd());
|
||||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||||
|
|
||||||
const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!;
|
const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!;
|
||||||
@ -290,7 +290,7 @@ describe('loadPersonaPromptFromPath (builtin paths)', () => {
|
|||||||
const personaPath = join(builtinPersonasDir, 'coder.md');
|
const personaPath = join(builtinPersonasDir, 'coder.md');
|
||||||
|
|
||||||
if (existsSync(personaPath)) {
|
if (existsSync(personaPath)) {
|
||||||
const prompt = loadPersonaPromptFromPath(personaPath);
|
const prompt = loadPersonaPromptFromPath(personaPath, process.cwd());
|
||||||
expect(prompt).toBeTruthy();
|
expect(prompt).toBeTruthy();
|
||||||
expect(typeof prompt).toBe('string');
|
expect(typeof prompt).toBe('string');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,12 +78,9 @@ describe('PieceEngine provider_options resolution', () => {
|
|||||||
engine = new PieceEngine(config, tmpDir, 'test task', {
|
engine = new PieceEngine(config, tmpDir, 'test task', {
|
||||||
projectCwd: tmpDir,
|
projectCwd: tmpDir,
|
||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
globalProviderOptions: {
|
providerOptions: {
|
||||||
codex: { networkAccess: true },
|
codex: { networkAccess: true },
|
||||||
claude: { sandbox: { allowUnsandboxedCommands: false } },
|
claude: { sandbox: { allowUnsandboxedCommands: false } },
|
||||||
},
|
|
||||||
projectProviderOptions: {
|
|
||||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
|
||||||
opencode: { networkAccess: true },
|
opencode: { networkAccess: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -96,7 +93,7 @@ describe('PieceEngine provider_options resolution', () => {
|
|||||||
opencode: { networkAccess: true },
|
opencode: { networkAccess: true },
|
||||||
claude: {
|
claude: {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
allowUnsandboxedCommands: true,
|
allowUnsandboxedCommands: false,
|
||||||
excludedCommands: ['./gradlew'],
|
excludedCommands: ['./gradlew'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -123,7 +120,7 @@ describe('PieceEngine provider_options resolution', () => {
|
|||||||
engine = new PieceEngine(config, tmpDir, 'test task', {
|
engine = new PieceEngine(config, tmpDir, 'test task', {
|
||||||
projectCwd: tmpDir,
|
projectCwd: tmpDir,
|
||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
globalProviderOptions: {
|
providerOptions: {
|
||||||
codex: { networkAccess: true },
|
codex: { networkAccess: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,14 +7,14 @@ import { tmpdir } from 'node:os';
|
|||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
const loadGlobalConfigMock = vi.hoisted(() => vi.fn());
|
const loadConfigMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
vi.mock('../infra/config/paths.js', () => ({
|
vi.mock('../infra/config/paths.js', () => ({
|
||||||
getGlobalConfigDir: () => '/tmp/.takt',
|
getGlobalConfigDir: () => '/tmp/.takt',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||||
loadGlobalConfig: loadGlobalConfigMock,
|
loadConfig: loadConfigMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { getPieceCategoriesPath, resetPieceCategories } = await import(
|
const { getPieceCategoriesPath, resetPieceCategories } = await import(
|
||||||
@ -28,17 +28,18 @@ function createTempCategoriesPath(): string {
|
|||||||
|
|
||||||
describe('getPieceCategoriesPath', () => {
|
describe('getPieceCategoriesPath', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
loadGlobalConfigMock.mockReset();
|
loadConfigMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return configured path when pieceCategoriesFile is set', () => {
|
it('should return configured path when pieceCategoriesFile is set', () => {
|
||||||
// Given
|
// Given
|
||||||
loadGlobalConfigMock.mockReturnValue({
|
loadConfigMock.mockReturnValue({
|
||||||
pieceCategoriesFile: '/custom/piece-categories.yaml',
|
global: { pieceCategoriesFile: '/custom/piece-categories.yaml' },
|
||||||
|
project: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// When
|
// When
|
||||||
const path = getPieceCategoriesPath();
|
const path = getPieceCategoriesPath(process.cwd());
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(path).toBe('/custom/piece-categories.yaml');
|
expect(path).toBe('/custom/piece-categories.yaml');
|
||||||
@ -46,10 +47,10 @@ describe('getPieceCategoriesPath', () => {
|
|||||||
|
|
||||||
it('should return default path when pieceCategoriesFile is not set', () => {
|
it('should return default path when pieceCategoriesFile is not set', () => {
|
||||||
// Given
|
// Given
|
||||||
loadGlobalConfigMock.mockReturnValue({});
|
loadConfigMock.mockReturnValue({ global: {}, project: {} });
|
||||||
|
|
||||||
// When
|
// When
|
||||||
const path = getPieceCategoriesPath();
|
const path = getPieceCategoriesPath(process.cwd());
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(path).toBe('/tmp/.takt/preferences/piece-categories.yaml');
|
expect(path).toBe('/tmp/.takt/preferences/piece-categories.yaml');
|
||||||
@ -57,12 +58,12 @@ describe('getPieceCategoriesPath', () => {
|
|||||||
|
|
||||||
it('should rethrow when global config loading fails', () => {
|
it('should rethrow when global config loading fails', () => {
|
||||||
// Given
|
// Given
|
||||||
loadGlobalConfigMock.mockImplementation(() => {
|
loadConfigMock.mockImplementation(() => {
|
||||||
throw new Error('invalid global config');
|
throw new Error('invalid global config');
|
||||||
});
|
});
|
||||||
|
|
||||||
// When / Then
|
// When / Then
|
||||||
expect(() => getPieceCategoriesPath()).toThrow('invalid global config');
|
expect(() => getPieceCategoriesPath(process.cwd())).toThrow('invalid global config');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ describe('resetPieceCategories', () => {
|
|||||||
const tempRoots: string[] = [];
|
const tempRoots: string[] = [];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
loadGlobalConfigMock.mockReset();
|
loadConfigMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -84,12 +85,13 @@ describe('resetPieceCategories', () => {
|
|||||||
// Given
|
// Given
|
||||||
const categoriesPath = createTempCategoriesPath();
|
const categoriesPath = createTempCategoriesPath();
|
||||||
tempRoots.push(dirname(dirname(categoriesPath)));
|
tempRoots.push(dirname(dirname(categoriesPath)));
|
||||||
loadGlobalConfigMock.mockReturnValue({
|
loadConfigMock.mockReturnValue({
|
||||||
pieceCategoriesFile: categoriesPath,
|
global: { pieceCategoriesFile: categoriesPath },
|
||||||
|
project: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// When
|
// When
|
||||||
resetPieceCategories();
|
resetPieceCategories(process.cwd());
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(existsSync(dirname(categoriesPath))).toBe(true);
|
expect(existsSync(dirname(categoriesPath))).toBe(true);
|
||||||
@ -102,14 +104,15 @@ describe('resetPieceCategories', () => {
|
|||||||
const categoriesDir = dirname(categoriesPath);
|
const categoriesDir = dirname(categoriesPath);
|
||||||
const tempRoot = dirname(categoriesDir);
|
const tempRoot = dirname(categoriesDir);
|
||||||
tempRoots.push(tempRoot);
|
tempRoots.push(tempRoot);
|
||||||
loadGlobalConfigMock.mockReturnValue({
|
loadConfigMock.mockReturnValue({
|
||||||
pieceCategoriesFile: categoriesPath,
|
global: { pieceCategoriesFile: categoriesPath },
|
||||||
|
project: {},
|
||||||
});
|
});
|
||||||
mkdirSync(categoriesDir, { recursive: true });
|
mkdirSync(categoriesDir, { recursive: true });
|
||||||
writeFileSync(categoriesPath, 'piece_categories:\n old:\n - stale-piece\n', 'utf-8');
|
writeFileSync(categoriesPath, 'piece_categories:\n old:\n - stale-piece\n', 'utf-8');
|
||||||
|
|
||||||
// When
|
// When
|
||||||
resetPieceCategories();
|
resetPieceCategories(process.cwd());
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(readFileSync(categoriesPath, 'utf-8')).toBe('piece_categories: {}\n');
|
expect(readFileSync(categoriesPath, 'utf-8')).toBe('piece_categories: {}\n');
|
||||||
|
|||||||
@ -118,6 +118,10 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
loadWorktreeSessions: vi.fn().mockReturnValue({}),
|
loadWorktreeSessions: vi.fn().mockReturnValue({}),
|
||||||
updateWorktreeSession: vi.fn(),
|
updateWorktreeSession: vi.fn(),
|
||||||
loadGlobalConfig: mockLoadGlobalConfig,
|
loadGlobalConfig: mockLoadGlobalConfig,
|
||||||
|
loadConfig: vi.fn().mockImplementation(() => ({
|
||||||
|
global: mockLoadGlobalConfig(),
|
||||||
|
project: {},
|
||||||
|
})),
|
||||||
saveSessionState: vi.fn(),
|
saveSessionState: vi.fn(),
|
||||||
ensureDir: vi.fn(),
|
ensureDir: vi.fn(),
|
||||||
writeFileAtomic: vi.fn(),
|
writeFileAtomic: vi.fn(),
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Tests the 3-tier piece resolution (project-local → user → builtin)
|
* Tests the 3-tier piece resolution (project-local → user → builtin)
|
||||||
* and YAML parsing including special rule syntax (ai(), all(), any()).
|
* and YAML parsing including special rule syntax (ai(), all(), any()).
|
||||||
*
|
*
|
||||||
* Mocked: globalConfig (for language/builtins)
|
* Mocked: loadConfig (for language/builtins)
|
||||||
* Not mocked: loadPiece, parsePiece, rule parsing
|
* Not mocked: loadPiece, parsePiece, rule parsing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -18,9 +18,17 @@ const languageState = vi.hoisted(() => ({ value: 'en' as 'en' | 'ja' }));
|
|||||||
|
|
||||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||||
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
||||||
getLanguage: vi.fn(() => languageState.value),
|
}));
|
||||||
getDisabledBuiltins: vi.fn().mockReturnValue([]),
|
|
||||||
getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true),
|
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||||
|
loadConfig: vi.fn(() => ({
|
||||||
|
global: {
|
||||||
|
language: languageState.value,
|
||||||
|
disabledBuiltins: [],
|
||||||
|
enableBuiltinPieces: true,
|
||||||
|
},
|
||||||
|
project: {},
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// --- Imports (after mocks) ---
|
// --- Imports (after mocks) ---
|
||||||
@ -38,6 +46,7 @@ function createTestDir(): string {
|
|||||||
|
|
||||||
describe('Piece Loader IT: builtin piece loading', () => {
|
describe('Piece Loader IT: builtin piece loading', () => {
|
||||||
let testDir: string;
|
let testDir: string;
|
||||||
|
const builtinNames = listBuiltinPieceNames(process.cwd(), { includeDisabled: true });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testDir = createTestDir();
|
testDir = createTestDir();
|
||||||
@ -48,8 +57,6 @@ describe('Piece Loader IT: builtin piece loading', () => {
|
|||||||
rmSync(testDir, { recursive: true, force: true });
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
const builtinNames = listBuiltinPieceNames({ includeDisabled: true });
|
|
||||||
|
|
||||||
for (const name of builtinNames) {
|
for (const name of builtinNames) {
|
||||||
it(`should load builtin piece: ${name}`, () => {
|
it(`should load builtin piece: ${name}`, () => {
|
||||||
const config = loadPiece(name, testDir);
|
const config = loadPiece(name, testDir);
|
||||||
@ -85,7 +92,7 @@ describe('Piece Loader IT: builtin piece loading', () => {
|
|||||||
it('should load e2e-test as a builtin piece in ja locale', () => {
|
it('should load e2e-test as a builtin piece in ja locale', () => {
|
||||||
languageState.value = 'ja';
|
languageState.value = 'ja';
|
||||||
|
|
||||||
const jaBuiltinNames = listBuiltinPieceNames({ includeDisabled: true });
|
const jaBuiltinNames = listBuiltinPieceNames(testDir, { includeDisabled: true });
|
||||||
expect(jaBuiltinNames).toContain('e2e-test');
|
expect(jaBuiltinNames).toContain('e2e-test');
|
||||||
|
|
||||||
const config = loadPiece('e2e-test', testDir);
|
const config = loadPiece('e2e-test', testDir);
|
||||||
|
|||||||
@ -57,6 +57,17 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({
|
|||||||
loadProjectConfig: vi.fn().mockReturnValue({}),
|
loadProjectConfig: vi.fn().mockReturnValue({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||||
|
loadConfig: vi.fn().mockReturnValue({
|
||||||
|
global: {
|
||||||
|
language: 'en',
|
||||||
|
enableBuiltinPieces: true,
|
||||||
|
disabledBuiltins: [],
|
||||||
|
},
|
||||||
|
project: {},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
// --- Imports (after mocks) ---
|
// --- Imports (after mocks) ---
|
||||||
|
|
||||||
import { PieceEngine } from '../core/piece/index.js';
|
import { PieceEngine } from '../core/piece/index.js';
|
||||||
|
|||||||
@ -118,7 +118,11 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
|||||||
const original = await importOriginal<typeof import('../infra/config/global/globalConfig.js')>();
|
const original = await importOriginal<typeof import('../infra/config/global/globalConfig.js')>();
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
loadGlobalConfig: vi.fn().mockReturnValue({
|
||||||
|
language: 'en',
|
||||||
|
enableBuiltinPieces: true,
|
||||||
|
disabledBuiltins: [],
|
||||||
|
}),
|
||||||
getLanguage: vi.fn().mockReturnValue('en'),
|
getLanguage: vi.fn().mockReturnValue('en'),
|
||||||
getDisabledBuiltins: vi.fn().mockReturnValue([]),
|
getDisabledBuiltins: vi.fn().mockReturnValue([]),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -100,7 +100,11 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
|||||||
const original = await importOriginal<typeof import('../infra/config/global/globalConfig.js')>();
|
const original = await importOriginal<typeof import('../infra/config/global/globalConfig.js')>();
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
loadGlobalConfig: vi.fn().mockReturnValue({
|
||||||
|
language: 'en',
|
||||||
|
enableBuiltinPieces: true,
|
||||||
|
disabledBuiltins: [],
|
||||||
|
}),
|
||||||
getLanguage: vi.fn().mockReturnValue('en'),
|
getLanguage: vi.fn().mockReturnValue('en'),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -89,6 +89,10 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
loadWorktreeSessions: vi.fn().mockReturnValue({}),
|
loadWorktreeSessions: vi.fn().mockReturnValue({}),
|
||||||
updateWorktreeSession: vi.fn(),
|
updateWorktreeSession: vi.fn(),
|
||||||
loadGlobalConfig: vi.fn().mockReturnValue({ provider: 'claude' }),
|
loadGlobalConfig: vi.fn().mockReturnValue({ provider: 'claude' }),
|
||||||
|
loadConfig: vi.fn().mockReturnValue({
|
||||||
|
global: { provider: 'claude' },
|
||||||
|
project: {},
|
||||||
|
}),
|
||||||
saveSessionState: vi.fn(),
|
saveSessionState: vi.fn(),
|
||||||
ensureDir: vi.fn(),
|
ensureDir: vi.fn(),
|
||||||
writeFileAtomic: vi.fn(),
|
writeFileAtomic: vi.fn(),
|
||||||
|
|||||||
@ -16,8 +16,8 @@ function createMovement(overrides: Partial<PieceMovement> = {}): PieceMovement {
|
|||||||
function createBuilder(step: PieceMovement, engineOverrides: Partial<PieceEngineOptions> = {}): OptionsBuilder {
|
function createBuilder(step: PieceMovement, engineOverrides: Partial<PieceEngineOptions> = {}): OptionsBuilder {
|
||||||
const engineOptions: PieceEngineOptions = {
|
const engineOptions: PieceEngineOptions = {
|
||||||
projectCwd: '/project',
|
projectCwd: '/project',
|
||||||
globalProvider: 'codex',
|
provider: 'codex',
|
||||||
globalProviderProfiles: {
|
providerProfiles: {
|
||||||
codex: {
|
codex: {
|
||||||
defaultPermissionMode: 'full',
|
defaultPermissionMode: 'full',
|
||||||
},
|
},
|
||||||
@ -60,10 +60,8 @@ describe('OptionsBuilder.buildBaseOptions', () => {
|
|||||||
it('uses default profile when provider_profiles are not provided', () => {
|
it('uses default profile when provider_profiles are not provided', () => {
|
||||||
const step = createMovement();
|
const step = createMovement();
|
||||||
const builder = createBuilder(step, {
|
const builder = createBuilder(step, {
|
||||||
globalProvider: undefined,
|
|
||||||
globalProviderProfiles: undefined,
|
|
||||||
projectProvider: undefined,
|
|
||||||
provider: undefined,
|
provider: undefined,
|
||||||
|
providerProfiles: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = builder.buildBaseOptions(step);
|
const options = builder.buildBaseOptions(step);
|
||||||
@ -78,11 +76,8 @@ describe('OptionsBuilder.buildBaseOptions', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const builder = createBuilder(step, {
|
const builder = createBuilder(step, {
|
||||||
globalProviderOptions: {
|
providerOptions: {
|
||||||
codex: { networkAccess: true },
|
codex: { networkAccess: true },
|
||||||
claude: { sandbox: { allowUnsandboxedCommands: false } },
|
|
||||||
},
|
|
||||||
projectProviderOptions: {
|
|
||||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||||
opencode: { networkAccess: true },
|
opencode: { networkAccess: true },
|
||||||
},
|
},
|
||||||
@ -105,10 +100,7 @@ describe('OptionsBuilder.buildBaseOptions', () => {
|
|||||||
it('falls back to global/project provider options when movement has none', () => {
|
it('falls back to global/project provider options when movement has none', () => {
|
||||||
const step = createMovement();
|
const step = createMovement();
|
||||||
const builder = createBuilder(step, {
|
const builder = createBuilder(step, {
|
||||||
globalProviderOptions: {
|
providerOptions: {
|
||||||
codex: { networkAccess: true },
|
|
||||||
},
|
|
||||||
projectProviderOptions: {
|
|
||||||
codex: { networkAccess: false },
|
codex: { networkAccess: false },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -17,6 +17,17 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||||
|
loadConfig: () => ({
|
||||||
|
global: {
|
||||||
|
language: 'en',
|
||||||
|
enableBuiltinPieces: false,
|
||||||
|
disabledBuiltins: [],
|
||||||
|
},
|
||||||
|
project: {},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const { listPieces } = await import('../infra/config/loaders/pieceLoader.js');
|
const { listPieces } = await import('../infra/config/loaders/pieceLoader.js');
|
||||||
|
|
||||||
const SAMPLE_PIECE = `name: test-piece
|
const SAMPLE_PIECE = `name: test-piece
|
||||||
|
|||||||
@ -22,12 +22,21 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
|||||||
const original = await importOriginal() as Record<string, unknown>;
|
const original = await importOriginal() as Record<string, unknown>;
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
getLanguage: () => languageState.value,
|
loadGlobalConfig: () => ({}),
|
||||||
getBuiltinPiecesEnabled: () => true,
|
|
||||||
getDisabledBuiltins: () => [],
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||||
|
loadConfig: () => ({
|
||||||
|
global: {
|
||||||
|
language: languageState.value,
|
||||||
|
enableBuiltinPieces: true,
|
||||||
|
disabledBuiltins: [],
|
||||||
|
},
|
||||||
|
project: {},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/resources/index.js', async (importOriginal) => {
|
vi.mock('../infra/resources/index.js', async (importOriginal) => {
|
||||||
const original = await importOriginal() as Record<string, unknown>;
|
const original = await importOriginal() as Record<string, unknown>;
|
||||||
return {
|
return {
|
||||||
@ -92,7 +101,7 @@ describe('piece category config loading', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null when builtin categories file is missing', () => {
|
it('should return null when builtin categories file is missing', () => {
|
||||||
const config = getPieceCategories();
|
const config = getPieceCategories(testDir);
|
||||||
expect(config).toBeNull();
|
expect(config).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -104,7 +113,7 @@ piece_categories:
|
|||||||
- default
|
- default
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const config = loadDefaultCategories();
|
const config = loadDefaultCategories(testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
expect(config!.pieceCategories).toEqual([
|
expect(config!.pieceCategories).toEqual([
|
||||||
{ name: 'Quick Start', pieces: ['default'], children: [] },
|
{ name: 'Quick Start', pieces: ['default'], children: [] },
|
||||||
@ -125,7 +134,7 @@ show_others_category: true
|
|||||||
others_category_name: Others
|
others_category_name: Others
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const config = getPieceCategories();
|
const config = getPieceCategories(testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
expect(config!.pieceCategories).toEqual([
|
expect(config!.pieceCategories).toEqual([
|
||||||
{ name: 'Main', pieces: ['default'], children: [] },
|
{ name: 'Main', pieces: ['default'], children: [] },
|
||||||
@ -165,7 +174,7 @@ show_others_category: false
|
|||||||
others_category_name: Unclassified
|
others_category_name: Unclassified
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const config = getPieceCategories();
|
const config = getPieceCategories(testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
expect(config!.pieceCategories).toEqual([
|
expect(config!.pieceCategories).toEqual([
|
||||||
{
|
{
|
||||||
@ -207,7 +216,7 @@ piece_categories:
|
|||||||
- e2e-test
|
- e2e-test
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const config = getPieceCategories();
|
const config = getPieceCategories(testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
expect(config!.pieceCategories).toEqual([
|
expect(config!.pieceCategories).toEqual([
|
||||||
{ name: 'レビュー', pieces: ['review-only', 'e2e-test'], children: [] },
|
{ name: 'レビュー', pieces: ['review-only', 'e2e-test'], children: [] },
|
||||||
@ -232,7 +241,7 @@ show_others_category: false
|
|||||||
others_category_name: Unclassified
|
others_category_name: Unclassified
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const config = getPieceCategories();
|
const config = getPieceCategories(testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
expect(config!.pieceCategories).toEqual([
|
expect(config!.pieceCategories).toEqual([
|
||||||
{ name: 'Main', pieces: ['default'], children: [] },
|
{ name: 'Main', pieces: ['default'], children: [] },
|
||||||
@ -278,7 +287,7 @@ describe('buildCategorizedPieces', () => {
|
|||||||
othersCategoryName: 'Others',
|
othersCategoryName: 'Others',
|
||||||
};
|
};
|
||||||
|
|
||||||
const categorized = buildCategorizedPieces(allPieces, config);
|
const categorized = buildCategorizedPieces(allPieces, config, process.cwd());
|
||||||
expect(categorized.categories).toEqual([
|
expect(categorized.categories).toEqual([
|
||||||
{
|
{
|
||||||
name: 'Main',
|
name: 'Main',
|
||||||
@ -310,7 +319,7 @@ describe('buildCategorizedPieces', () => {
|
|||||||
othersCategoryName: 'Others',
|
othersCategoryName: 'Others',
|
||||||
};
|
};
|
||||||
|
|
||||||
const categorized = buildCategorizedPieces(allPieces, config);
|
const categorized = buildCategorizedPieces(allPieces, config, process.cwd());
|
||||||
expect(categorized.categories).toEqual([
|
expect(categorized.categories).toEqual([
|
||||||
{ name: 'Main', pieces: ['default'], children: [] },
|
{ name: 'Main', pieces: ['default'], children: [] },
|
||||||
{ name: 'Others', pieces: ['extra'], children: [] },
|
{ name: 'Others', pieces: ['extra'], children: [] },
|
||||||
@ -334,7 +343,7 @@ describe('buildCategorizedPieces', () => {
|
|||||||
othersCategoryName: 'Others',
|
othersCategoryName: 'Others',
|
||||||
};
|
};
|
||||||
|
|
||||||
const categorized = buildCategorizedPieces(allPieces, config);
|
const categorized = buildCategorizedPieces(allPieces, config, process.cwd());
|
||||||
expect(categorized.categories).toEqual([
|
expect(categorized.categories).toEqual([
|
||||||
{ name: 'Main', pieces: ['default'], children: [] },
|
{ name: 'Main', pieces: ['default'], children: [] },
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -91,6 +91,10 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
loadWorktreeSessions: vi.fn().mockReturnValue({}),
|
loadWorktreeSessions: vi.fn().mockReturnValue({}),
|
||||||
updateWorktreeSession: vi.fn(),
|
updateWorktreeSession: vi.fn(),
|
||||||
loadGlobalConfig: vi.fn().mockReturnValue({ provider: 'claude' }),
|
loadGlobalConfig: vi.fn().mockReturnValue({ provider: 'claude' }),
|
||||||
|
loadConfig: vi.fn().mockReturnValue({
|
||||||
|
global: { provider: 'claude' },
|
||||||
|
project: {},
|
||||||
|
}),
|
||||||
saveSessionState: vi.fn(),
|
saveSessionState: vi.fn(),
|
||||||
ensureDir: vi.fn(),
|
ensureDir: vi.fn(),
|
||||||
writeFileAtomic: vi.fn(),
|
writeFileAtomic: vi.fn(),
|
||||||
|
|||||||
@ -31,13 +31,14 @@ describe('resetCategoriesToDefault', () => {
|
|||||||
|
|
||||||
it('should reset user category overlay and show updated message', async () => {
|
it('should reset user category overlay and show updated message', async () => {
|
||||||
// Given
|
// Given
|
||||||
|
const cwd = '/tmp/test-cwd';
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await resetCategoriesToDefault();
|
await resetCategoriesToDefault(cwd);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(mockHeader).toHaveBeenCalledWith('Reset Categories');
|
expect(mockHeader).toHaveBeenCalledWith('Reset Categories');
|
||||||
expect(mockResetPieceCategories).toHaveBeenCalledTimes(1);
|
expect(mockResetPieceCategories).toHaveBeenCalledWith(cwd);
|
||||||
expect(mockSuccess).toHaveBeenCalledWith('User category overlay reset.');
|
expect(mockSuccess).toHaveBeenCalledWith('User category overlay reset.');
|
||||||
expect(mockInfo).toHaveBeenCalledWith(' /tmp/user-piece-categories.yaml');
|
expect(mockInfo).toHaveBeenCalledWith(' /tmp/user-piece-categories.yaml');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,13 +5,12 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import type { TaskInfo } from '../infra/task/index.js';
|
import type { TaskInfo } from '../infra/task/index.js';
|
||||||
|
|
||||||
const { mockResolveTaskExecution, mockExecutePiece, mockLoadPieceByIdentifier, mockLoadGlobalConfig, mockLoadProjectConfig, mockBuildTaskResult, mockPersistTaskResult, mockPersistTaskError, mockPostExecutionFlow } =
|
const { mockResolveTaskExecution, mockExecutePiece, mockLoadPieceByIdentifier, mockResolveConfigValues, mockBuildTaskResult, mockPersistTaskResult, mockPersistTaskError, mockPostExecutionFlow } =
|
||||||
vi.hoisted(() => ({
|
vi.hoisted(() => ({
|
||||||
mockResolveTaskExecution: vi.fn(),
|
mockResolveTaskExecution: vi.fn(),
|
||||||
mockExecutePiece: vi.fn(),
|
mockExecutePiece: vi.fn(),
|
||||||
mockLoadPieceByIdentifier: vi.fn(),
|
mockLoadPieceByIdentifier: vi.fn(),
|
||||||
mockLoadGlobalConfig: vi.fn(),
|
mockResolveConfigValues: vi.fn(),
|
||||||
mockLoadProjectConfig: vi.fn(),
|
|
||||||
mockBuildTaskResult: vi.fn(),
|
mockBuildTaskResult: vi.fn(),
|
||||||
mockPersistTaskResult: vi.fn(),
|
mockPersistTaskResult: vi.fn(),
|
||||||
mockPersistTaskError: vi.fn(),
|
mockPersistTaskError: vi.fn(),
|
||||||
@ -39,10 +38,7 @@ vi.mock('../features/tasks/execute/postExecution.js', () => ({
|
|||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
||||||
isPiecePath: () => false,
|
isPiecePath: () => false,
|
||||||
loadConfig: () => ({
|
resolveConfigValues: (...args: unknown[]) => mockResolveConfigValues(...args),
|
||||||
global: mockLoadGlobalConfig(),
|
|
||||||
project: mockLoadProjectConfig(),
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
@ -87,21 +83,19 @@ describe('executeAndCompleteTask', () => {
|
|||||||
name: 'default',
|
name: 'default',
|
||||||
movements: [],
|
movements: [],
|
||||||
});
|
});
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockResolveConfigValues.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
|
model: undefined,
|
||||||
personaProviders: {},
|
personaProviders: {},
|
||||||
providerProfiles: {},
|
providerProfiles: {},
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||||
},
|
},
|
||||||
});
|
notificationSound: true,
|
||||||
mockLoadProjectConfig.mockReturnValue({
|
notificationSoundEvents: {},
|
||||||
provider: 'claude',
|
concurrency: 1,
|
||||||
providerProfiles: {},
|
taskPollIntervalMs: 500,
|
||||||
providerOptions: {
|
|
||||||
opencode: { networkAccess: true },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
mockBuildTaskResult.mockReturnValue({ success: true });
|
mockBuildTaskResult.mockReturnValue({ success: true });
|
||||||
mockResolveTaskExecution.mockResolvedValue({
|
mockResolveTaskExecution.mockResolvedValue({
|
||||||
@ -140,16 +134,12 @@ describe('executeAndCompleteTask', () => {
|
|||||||
const pieceExecutionOptions = mockExecutePiece.mock.calls[0]?.[3] as {
|
const pieceExecutionOptions = mockExecutePiece.mock.calls[0]?.[3] as {
|
||||||
taskDisplayLabel?: string;
|
taskDisplayLabel?: string;
|
||||||
taskPrefix?: string;
|
taskPrefix?: string;
|
||||||
globalProviderOptions?: unknown;
|
providerOptions?: unknown;
|
||||||
projectProviderOptions?: unknown;
|
|
||||||
};
|
};
|
||||||
expect(pieceExecutionOptions?.taskDisplayLabel).toBe(taskDisplayLabel);
|
expect(pieceExecutionOptions?.taskDisplayLabel).toBe(taskDisplayLabel);
|
||||||
expect(pieceExecutionOptions?.taskPrefix).toBe(taskDisplayLabel);
|
expect(pieceExecutionOptions?.taskPrefix).toBe(taskDisplayLabel);
|
||||||
expect(pieceExecutionOptions?.globalProviderOptions).toEqual({
|
expect(pieceExecutionOptions?.providerOptions).toEqual({
|
||||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||||
});
|
});
|
||||||
expect(pieceExecutionOptions?.projectProviderOptions).toEqual({
|
|
||||||
opencode: { networkAccess: true },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -48,7 +48,7 @@ vi.mock('../infra/task/index.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
loadGlobalConfig: vi.fn(() => ({ interactivePreviewMovements: 3, language: 'en' })),
|
resolveConfigValues: vi.fn(() => ({ interactivePreviewMovements: 3, language: 'en' })),
|
||||||
getPieceDescription: vi.fn(() => ({
|
getPieceDescription: vi.fn(() => ({
|
||||||
name: 'default',
|
name: 'default',
|
||||||
description: 'desc',
|
description: 'desc',
|
||||||
|
|||||||
@ -4,7 +4,7 @@ const {
|
|||||||
mockExistsSync,
|
mockExistsSync,
|
||||||
mockSelectPiece,
|
mockSelectPiece,
|
||||||
mockSelectOption,
|
mockSelectOption,
|
||||||
mockLoadGlobalConfig,
|
mockResolveConfigValue,
|
||||||
mockLoadPieceByIdentifier,
|
mockLoadPieceByIdentifier,
|
||||||
mockGetPieceDescription,
|
mockGetPieceDescription,
|
||||||
mockRunRetryMode,
|
mockRunRetryMode,
|
||||||
@ -16,7 +16,7 @@ const {
|
|||||||
mockExistsSync: vi.fn(() => true),
|
mockExistsSync: vi.fn(() => true),
|
||||||
mockSelectPiece: vi.fn(),
|
mockSelectPiece: vi.fn(),
|
||||||
mockSelectOption: vi.fn(),
|
mockSelectOption: vi.fn(),
|
||||||
mockLoadGlobalConfig: vi.fn(),
|
mockResolveConfigValue: vi.fn(),
|
||||||
mockLoadPieceByIdentifier: vi.fn(),
|
mockLoadPieceByIdentifier: vi.fn(),
|
||||||
mockGetPieceDescription: vi.fn(() => ({
|
mockGetPieceDescription: vi.fn(() => ({
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@ -60,7 +60,7 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
loadGlobalConfig: (...args: unknown[]) => mockLoadGlobalConfig(...args),
|
resolveConfigValue: (...args: unknown[]) => mockResolveConfigValue(...args),
|
||||||
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
||||||
getPieceDescription: (...args: unknown[]) => mockGetPieceDescription(...args),
|
getPieceDescription: (...args: unknown[]) => mockGetPieceDescription(...args),
|
||||||
}));
|
}));
|
||||||
@ -126,7 +126,7 @@ beforeEach(() => {
|
|||||||
mockExistsSync.mockReturnValue(true);
|
mockExistsSync.mockReturnValue(true);
|
||||||
|
|
||||||
mockSelectPiece.mockResolvedValue('default');
|
mockSelectPiece.mockResolvedValue('default');
|
||||||
mockLoadGlobalConfig.mockReturnValue({ defaultPiece: 'default' });
|
mockResolveConfigValue.mockReturnValue(3);
|
||||||
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
||||||
mockSelectOption.mockResolvedValue('plan');
|
mockSelectOption.mockResolvedValue('plan');
|
||||||
mockRunRetryMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
mockRunRetryMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { basename, dirname } from 'node:path';
|
import { basename, dirname } from 'node:path';
|
||||||
import { loadCustomAgents, loadAgentPrompt, loadConfig } from '../infra/config/index.js';
|
import { loadCustomAgents, loadAgentPrompt, resolveConfigValues } from '../infra/config/index.js';
|
||||||
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
||||||
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
||||||
import { createLogger } from '../shared/utils/index.js';
|
import { createLogger } from '../shared/utils/index.js';
|
||||||
@ -29,17 +29,10 @@ export class AgentRunner {
|
|||||||
agentConfig?: CustomAgentConfig,
|
agentConfig?: CustomAgentConfig,
|
||||||
): ProviderType {
|
): ProviderType {
|
||||||
if (options?.provider) return options.provider;
|
if (options?.provider) return options.provider;
|
||||||
const config = loadConfig(cwd);
|
const config = resolveConfigValues(cwd, ['provider']);
|
||||||
const projectConfig = config.project;
|
if (config.provider) return config.provider;
|
||||||
if (projectConfig.provider) return projectConfig.provider;
|
|
||||||
if (options?.stepProvider) return options.stepProvider;
|
if (options?.stepProvider) return options.stepProvider;
|
||||||
if (agentConfig?.provider) return agentConfig.provider;
|
if (agentConfig?.provider) return agentConfig.provider;
|
||||||
try {
|
|
||||||
const globalConfig = config.global;
|
|
||||||
if (globalConfig.provider) return globalConfig.provider;
|
|
||||||
} catch (error) {
|
|
||||||
log.debug('Global config not available for provider resolution', { error });
|
|
||||||
}
|
|
||||||
return 'claude';
|
return 'claude';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,14 +50,10 @@ export class AgentRunner {
|
|||||||
if (options?.stepModel) return options.stepModel;
|
if (options?.stepModel) return options.stepModel;
|
||||||
if (agentConfig?.model) return agentConfig.model;
|
if (agentConfig?.model) return agentConfig.model;
|
||||||
if (!options?.cwd) return undefined;
|
if (!options?.cwd) return undefined;
|
||||||
try {
|
const config = resolveConfigValues(options.cwd, ['provider', 'model']);
|
||||||
const globalConfig = loadConfig(options.cwd).global;
|
if (config.model) {
|
||||||
if (globalConfig.model) {
|
const defaultProvider = config.provider ?? 'claude';
|
||||||
const globalProvider = globalConfig.provider ?? 'claude';
|
if (defaultProvider === resolvedProvider) return config.model;
|
||||||
if (globalProvider === resolvedProvider) return globalConfig.model;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.debug('Global config not available for model resolution', { error });
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -133,7 +122,7 @@ export class AgentRunner {
|
|||||||
name: agentConfig.name,
|
name: agentConfig.name,
|
||||||
systemPrompt: agentConfig.claudeAgent || agentConfig.claudeSkill
|
systemPrompt: agentConfig.claudeAgent || agentConfig.claudeSkill
|
||||||
? undefined
|
? undefined
|
||||||
: loadAgentPrompt(agentConfig),
|
: loadAgentPrompt(agentConfig, options.cwd),
|
||||||
claudeAgent: agentConfig.claudeAgent,
|
claudeAgent: agentConfig.claudeAgent,
|
||||||
claudeSkill: agentConfig.claudeSkill,
|
claudeSkill: agentConfig.claudeSkill,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -104,7 +104,7 @@ reset
|
|||||||
.command('categories')
|
.command('categories')
|
||||||
.description('Reset piece categories to builtin defaults')
|
.description('Reset piece categories to builtin defaults')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
await resetCategoriesToDefault();
|
await resetCategoriesToDefault(resolvedCwd);
|
||||||
});
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { resolve } from 'node:path';
|
|||||||
import {
|
import {
|
||||||
initGlobalDirs,
|
initGlobalDirs,
|
||||||
initProjectDirs,
|
initProjectDirs,
|
||||||
loadConfig,
|
resolveConfigValues,
|
||||||
isVerboseMode,
|
isVerboseMode,
|
||||||
} from '../../infra/config/index.js';
|
} from '../../infra/config/index.js';
|
||||||
import { setQuietMode } from '../../shared/context.js';
|
import { setQuietMode } from '../../shared/context.js';
|
||||||
@ -69,7 +69,7 @@ export async function runPreActionHook(): Promise<void> {
|
|||||||
const verbose = isVerboseMode(resolvedCwd);
|
const verbose = isVerboseMode(resolvedCwd);
|
||||||
initDebugLogger(verbose ? { enabled: true } : undefined, resolvedCwd);
|
initDebugLogger(verbose ? { enabled: true } : undefined, resolvedCwd);
|
||||||
|
|
||||||
const { global: config } = loadConfig(resolvedCwd);
|
const config = resolveConfigValues(resolvedCwd, ['logLevel', 'minimalOutput']);
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
setVerboseConsole(true);
|
setVerboseConsole(true);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
dispatchConversationAction,
|
dispatchConversationAction,
|
||||||
type InteractiveModeResult,
|
type InteractiveModeResult,
|
||||||
} from '../../features/interactive/index.js';
|
} from '../../features/interactive/index.js';
|
||||||
import { getPieceDescription, loadConfig } from '../../infra/config/index.js';
|
import { getPieceDescription, resolveConfigValues } from '../../infra/config/index.js';
|
||||||
import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
|
import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
|
||||||
import { program, resolvedCwd, pipelineMode } from './program.js';
|
import { program, resolvedCwd, pipelineMode } from './program.js';
|
||||||
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
||||||
@ -137,7 +137,7 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All paths below go through interactive mode
|
// All paths below go through interactive mode
|
||||||
const { global: globalConfig } = loadConfig(resolvedCwd);
|
const globalConfig = resolveConfigValues(resolvedCwd, ['language', 'interactivePreviewMovements', 'provider']);
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
|
|
||||||
const pieceId = await determinePiece(resolvedCwd, selectOptions.piece);
|
const pieceId = await determinePiece(resolvedCwd, selectOptions.piece);
|
||||||
|
|||||||
@ -56,9 +56,7 @@ export class OptionsBuilder {
|
|||||||
|
|
||||||
const resolvedProviderForPermissions =
|
const resolvedProviderForPermissions =
|
||||||
this.engineOptions.provider
|
this.engineOptions.provider
|
||||||
?? this.engineOptions.projectProvider
|
|
||||||
?? resolved.provider
|
?? resolved.provider
|
||||||
?? this.engineOptions.globalProvider
|
|
||||||
?? 'claude';
|
?? 'claude';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -73,12 +71,11 @@ export class OptionsBuilder {
|
|||||||
movementName: step.name,
|
movementName: step.name,
|
||||||
requiredPermissionMode: step.requiredPermissionMode,
|
requiredPermissionMode: step.requiredPermissionMode,
|
||||||
provider: resolvedProviderForPermissions,
|
provider: resolvedProviderForPermissions,
|
||||||
projectProviderProfiles: this.engineOptions.projectProviderProfiles,
|
projectProviderProfiles: this.engineOptions.providerProfiles,
|
||||||
globalProviderProfiles: this.engineOptions.globalProviderProfiles ?? DEFAULT_PROVIDER_PERMISSION_PROFILES,
|
globalProviderProfiles: DEFAULT_PROVIDER_PERMISSION_PROFILES,
|
||||||
}),
|
}),
|
||||||
providerOptions: mergeProviderOptions(
|
providerOptions: mergeProviderOptions(
|
||||||
this.engineOptions.globalProviderOptions,
|
this.engineOptions.providerOptions,
|
||||||
this.engineOptions.projectProviderOptions,
|
|
||||||
step.providerOptions,
|
step.providerOptions,
|
||||||
),
|
),
|
||||||
language: this.getLanguage(),
|
language: this.getLanguage(),
|
||||||
|
|||||||
@ -179,21 +179,13 @@ export interface PieceEngineOptions {
|
|||||||
/** Language for instruction metadata. Defaults to 'en'. */
|
/** Language for instruction metadata. Defaults to 'en'. */
|
||||||
language?: Language;
|
language?: Language;
|
||||||
provider?: ProviderType;
|
provider?: ProviderType;
|
||||||
/** Project config provider (used for provider/profile resolution parity with AgentRunner) */
|
|
||||||
projectProvider?: ProviderType;
|
|
||||||
/** Global config provider (used for provider/profile resolution parity with AgentRunner) */
|
|
||||||
globalProvider?: ProviderType;
|
|
||||||
model?: string;
|
model?: string;
|
||||||
/** Project-level provider options */
|
/** Resolved provider options */
|
||||||
projectProviderOptions?: MovementProviderOptions;
|
providerOptions?: MovementProviderOptions;
|
||||||
/** Global-level provider options */
|
|
||||||
globalProviderOptions?: MovementProviderOptions;
|
|
||||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||||
personaProviders?: Record<string, ProviderType>;
|
personaProviders?: Record<string, ProviderType>;
|
||||||
/** Project-level provider permission profiles */
|
/** Resolved provider permission profiles */
|
||||||
projectProviderProfiles?: ProviderPermissionProfiles;
|
providerProfiles?: ProviderPermissionProfiles;
|
||||||
/** Global-level provider permission profiles */
|
|
||||||
globalProviderProfiles?: ProviderPermissionProfiles;
|
|
||||||
/** Enable interactive-only rules and user-input transitions */
|
/** Enable interactive-only rules and user-input transitions */
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
/** Rule tag index detector (required for rules evaluation) */
|
/** Rule tag index detector (required for rules evaluation) */
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import chalk from 'chalk';
|
|||||||
import type { PieceSource } from '../../infra/config/loaders/pieceResolver.js';
|
import type { PieceSource } from '../../infra/config/loaders/pieceResolver.js';
|
||||||
import { getLanguageResourcesDir } from '../../infra/resources/index.js';
|
import { getLanguageResourcesDir } from '../../infra/resources/index.js';
|
||||||
import { getGlobalConfigDir, getProjectConfigDir } from '../../infra/config/paths.js';
|
import { getGlobalConfigDir, getProjectConfigDir } from '../../infra/config/paths.js';
|
||||||
import { getLanguage, getBuiltinPiecesEnabled } from '../../infra/config/global/globalConfig.js';
|
import { resolveConfigValues } from '../../infra/config/index.js';
|
||||||
import { section, error as logError, info } from '../../shared/ui/index.js';
|
import { section, error as logError, info } from '../../shared/ui/index.js';
|
||||||
|
|
||||||
const FACET_TYPES = [
|
const FACET_TYPES = [
|
||||||
@ -62,10 +62,11 @@ function getFacetDirs(
|
|||||||
facetType: FacetType,
|
facetType: FacetType,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
): { dir: string; source: PieceSource }[] {
|
): { dir: string; source: PieceSource }[] {
|
||||||
|
const config = resolveConfigValues(cwd, ['enableBuiltinPieces', 'language']);
|
||||||
const dirs: { dir: string; source: PieceSource }[] = [];
|
const dirs: { dir: string; source: PieceSource }[] = [];
|
||||||
|
|
||||||
if (getBuiltinPiecesEnabled()) {
|
if (config.enableBuiltinPieces !== false) {
|
||||||
const lang = getLanguage();
|
const lang = config.language;
|
||||||
dirs.push({ dir: join(getLanguageResourcesDir(lang), facetType), source: 'builtin' });
|
dirs.push({ dir: join(getLanguageResourcesDir(lang), facetType), source: 'builtin' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,12 +5,12 @@
|
|||||||
import { resetPieceCategories, getPieceCategoriesPath } from '../../infra/config/global/pieceCategories.js';
|
import { resetPieceCategories, getPieceCategoriesPath } from '../../infra/config/global/pieceCategories.js';
|
||||||
import { header, success, info } from '../../shared/ui/index.js';
|
import { header, success, info } from '../../shared/ui/index.js';
|
||||||
|
|
||||||
export async function resetCategoriesToDefault(): Promise<void> {
|
export async function resetCategoriesToDefault(cwd: string): Promise<void> {
|
||||||
header('Reset Categories');
|
header('Reset Categories');
|
||||||
|
|
||||||
resetPieceCategories();
|
resetPieceCategories(cwd);
|
||||||
|
|
||||||
const userPath = getPieceCategoriesPath();
|
const userPath = getPieceCategoriesPath(cwd);
|
||||||
success('User category overlay reset.');
|
success('User category overlay reset.');
|
||||||
info(` ${userPath}`);
|
info(` ${userPath}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import {
|
import {
|
||||||
loadConfig,
|
resolveConfigValues,
|
||||||
loadPersonaSessions,
|
loadPersonaSessions,
|
||||||
updatePersonaSession,
|
updatePersonaSession,
|
||||||
loadSessionState,
|
loadSessionState,
|
||||||
@ -58,7 +58,7 @@ export interface SessionContext {
|
|||||||
* Initialize provider, session, and language for interactive conversation.
|
* Initialize provider, session, and language for interactive conversation.
|
||||||
*/
|
*/
|
||||||
export function initializeSession(cwd: string, personaName: string): SessionContext {
|
export function initializeSession(cwd: string, personaName: string): SessionContext {
|
||||||
const { global: globalConfig } = loadConfig(cwd);
|
const globalConfig = resolveConfigValues(cwd, ['language', 'provider', 'model']);
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
if (!globalConfig.provider) {
|
if (!globalConfig.provider) {
|
||||||
throw new Error('Provider is not configured.');
|
throw new Error('Provider is not configured.');
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
import { resolveLanguage } from './interactive.js';
|
import { resolveLanguage } from './interactive.js';
|
||||||
import { loadTemplate } from '../../shared/prompts/index.js';
|
import { loadTemplate } from '../../shared/prompts/index.js';
|
||||||
import { getLabelObject } from '../../shared/i18n/index.js';
|
import { getLabelObject } from '../../shared/i18n/index.js';
|
||||||
import { loadConfig } from '../../infra/config/index.js';
|
import { resolveConfigValues } from '../../infra/config/index.js';
|
||||||
import type { InstructModeResult, InstructUIText } from '../tasks/list/instructMode.js';
|
import type { InstructModeResult, InstructUIText } from '../tasks/list/instructMode.js';
|
||||||
|
|
||||||
/** Failure information for a retry task */
|
/** Failure information for a retry task */
|
||||||
@ -116,7 +116,7 @@ export async function runRetryMode(
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
retryContext: RetryContext,
|
retryContext: RetryContext,
|
||||||
): Promise<InstructModeResult> {
|
): Promise<InstructModeResult> {
|
||||||
const { global: globalConfig } = loadConfig(cwd);
|
const globalConfig = resolveConfigValues(cwd, ['language', 'provider']);
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
|
|
||||||
if (!globalConfig.provider) {
|
if (!globalConfig.provider) {
|
||||||
|
|||||||
@ -521,7 +521,7 @@ export async function selectPiece(
|
|||||||
options?: SelectPieceOptions,
|
options?: SelectPieceOptions,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const fallbackToDefault = options?.fallbackToDefault !== false;
|
const fallbackToDefault = options?.fallbackToDefault !== false;
|
||||||
const categoryConfig = getPieceCategories();
|
const categoryConfig = getPieceCategories(cwd);
|
||||||
const currentPiece = getCurrentPiece(cwd);
|
const currentPiece = getCurrentPiece(cwd);
|
||||||
|
|
||||||
if (categoryConfig) {
|
if (categoryConfig) {
|
||||||
@ -534,7 +534,7 @@ export async function selectPiece(
|
|||||||
info('No pieces found.');
|
info('No pieces found.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const categorized = buildCategorizedPieces(allPieces, categoryConfig);
|
const categorized = buildCategorizedPieces(allPieces, categoryConfig, cwd);
|
||||||
warnMissingPieces(categorized.missingPieces.filter((missing) => missing.source === 'user'));
|
warnMissingPieces(categorized.missingPieces.filter((missing) => missing.source === 'user'));
|
||||||
return selectPieceFromCategorizedPieces(categorized, currentPiece);
|
return selectPieceFromCategorizedPieces(categorized, currentPiece);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import {
|
|||||||
} from '../../infra/github/index.js';
|
} from '../../infra/github/index.js';
|
||||||
import { stageAndCommit, getCurrentBranch } from '../../infra/task/index.js';
|
import { stageAndCommit, getCurrentBranch } from '../../infra/task/index.js';
|
||||||
import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
||||||
import { loadConfig } from '../../infra/config/index.js';
|
import { resolveConfigValues } from '../../infra/config/index.js';
|
||||||
import { info, error, success, status, blankLine } from '../../shared/ui/index.js';
|
import { info, error, success, status, blankLine } from '../../shared/ui/index.js';
|
||||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import type { PipelineConfig } from '../../core/models/index.js';
|
import type { PipelineConfig } from '../../core/models/index.js';
|
||||||
@ -106,7 +106,7 @@ function buildPipelinePrBody(
|
|||||||
*/
|
*/
|
||||||
export async function executePipeline(options: PipelineExecutionOptions): Promise<number> {
|
export async function executePipeline(options: PipelineExecutionOptions): Promise<number> {
|
||||||
const { cwd, piece, autoPr, skipGit } = options;
|
const { cwd, piece, autoPr, skipGit } = options;
|
||||||
const { global: globalConfig } = loadConfig(cwd);
|
const globalConfig = resolveConfigValues(cwd, ['pipeline']);
|
||||||
const pipelineConfig = globalConfig.pipeline;
|
const pipelineConfig = globalConfig.pipeline;
|
||||||
let issue: GitHubIssue | undefined;
|
let issue: GitHubIssue | undefined;
|
||||||
let task: string;
|
let task: string;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* Useful for debugging and understanding what prompts agents will receive.
|
* Useful for debugging and understanding what prompts agents will receive.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadPieceByIdentifier, getCurrentPiece, loadConfig } from '../../infra/config/index.js';
|
import { loadPieceByIdentifier, getCurrentPiece, resolveConfigValue } from '../../infra/config/index.js';
|
||||||
import { InstructionBuilder } from '../../core/piece/instruction/InstructionBuilder.js';
|
import { InstructionBuilder } from '../../core/piece/instruction/InstructionBuilder.js';
|
||||||
import { ReportInstructionBuilder } from '../../core/piece/instruction/ReportInstructionBuilder.js';
|
import { ReportInstructionBuilder } from '../../core/piece/instruction/ReportInstructionBuilder.js';
|
||||||
import { StatusJudgmentBuilder } from '../../core/piece/instruction/StatusJudgmentBuilder.js';
|
import { StatusJudgmentBuilder } from '../../core/piece/instruction/StatusJudgmentBuilder.js';
|
||||||
@ -29,8 +29,7 @@ export async function previewPrompts(cwd: string, pieceIdentifier?: string): Pro
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { global: globalConfig } = loadConfig(cwd);
|
const language = resolveConfigValue(cwd, 'language') as Language;
|
||||||
const language: Language = globalConfig.language ?? 'en';
|
|
||||||
|
|
||||||
header(`Prompt Preview: ${config.name}`);
|
header(`Prompt Preview: ${config.name}`);
|
||||||
info(`Movements: ${config.movements.length}`);
|
info(`Movements: ${config.movements.length}`);
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
updatePersonaSession,
|
updatePersonaSession,
|
||||||
loadWorktreeSessions,
|
loadWorktreeSessions,
|
||||||
updateWorktreeSession,
|
updateWorktreeSession,
|
||||||
loadConfig,
|
resolveConfigValues,
|
||||||
saveSessionState,
|
saveSessionState,
|
||||||
type SessionState,
|
type SessionState,
|
||||||
} from '../../../infra/config/index.js';
|
} from '../../../infra/config/index.js';
|
||||||
@ -317,7 +317,10 @@ export async function executePiece(
|
|||||||
|
|
||||||
// Load saved agent sessions only on retry; normal runs start with empty sessions
|
// Load saved agent sessions only on retry; normal runs start with empty sessions
|
||||||
const isWorktree = cwd !== projectCwd;
|
const isWorktree = cwd !== projectCwd;
|
||||||
const { global: globalConfig } = loadConfig(projectCwd);
|
const globalConfig = resolveConfigValues(
|
||||||
|
projectCwd,
|
||||||
|
['notificationSound', 'notificationSoundEvents', 'provider', 'runtime', 'preventSleep', 'model', 'observability'],
|
||||||
|
);
|
||||||
const shouldNotify = globalConfig.notificationSound !== false;
|
const shouldNotify = globalConfig.notificationSound !== false;
|
||||||
const notificationSoundEvents = globalConfig.notificationSoundEvents;
|
const notificationSoundEvents = globalConfig.notificationSoundEvents;
|
||||||
const shouldNotifyIterationLimit = shouldNotify && notificationSoundEvents?.iterationLimit !== false;
|
const shouldNotifyIterationLimit = shouldNotify && notificationSoundEvents?.iterationLimit !== false;
|
||||||
@ -443,14 +446,10 @@ export async function executePiece(
|
|||||||
projectCwd,
|
projectCwd,
|
||||||
language: options.language,
|
language: options.language,
|
||||||
provider: options.provider,
|
provider: options.provider,
|
||||||
projectProvider: options.projectProvider,
|
|
||||||
globalProvider: options.globalProvider,
|
|
||||||
model: options.model,
|
model: options.model,
|
||||||
projectProviderOptions: options.projectProviderOptions,
|
providerOptions: options.providerOptions,
|
||||||
globalProviderOptions: options.globalProviderOptions,
|
|
||||||
personaProviders: options.personaProviders,
|
personaProviders: options.personaProviders,
|
||||||
projectProviderProfiles: options.projectProviderProfiles,
|
providerProfiles: options.providerProfiles,
|
||||||
globalProviderProfiles: options.globalProviderProfiles,
|
|
||||||
interactive: interactiveUserInput,
|
interactive: interactiveUserInput,
|
||||||
detectRuleIndex,
|
detectRuleIndex,
|
||||||
callAiJudge,
|
callAiJudge,
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* instructBranch (instruct mode from takt list).
|
* instructBranch (instruct mode from takt list).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadConfig } from '../../../infra/config/index.js';
|
import { resolveConfigValue } from '../../../infra/config/index.js';
|
||||||
import { confirm } from '../../../shared/prompt/index.js';
|
import { confirm } from '../../../shared/prompt/index.js';
|
||||||
import { autoCommitAndPush } from '../../../infra/task/index.js';
|
import { autoCommitAndPush } from '../../../infra/task/index.js';
|
||||||
import { info, error, success } from '../../../shared/ui/index.js';
|
import { info, error, success } from '../../../shared/ui/index.js';
|
||||||
@ -23,9 +23,9 @@ export async function resolveAutoPr(optionAutoPr: boolean | undefined, cwd: stri
|
|||||||
return optionAutoPr;
|
return optionAutoPr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { global: globalConfig } = loadConfig(cwd);
|
const autoPr = resolveConfigValue(cwd, 'autoPr');
|
||||||
if (typeof globalConfig.autoPr === 'boolean') {
|
if (typeof autoPr === 'boolean') {
|
||||||
return globalConfig.autoPr;
|
return autoPr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return confirm('Create pull request?', true);
|
return confirm('Create pull request?', true);
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { loadConfig } from '../../../infra/config/index.js';
|
import { resolveConfigValue } from '../../../infra/config/index.js';
|
||||||
import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
|
import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
|
||||||
import { withProgress } from '../../../shared/ui/index.js';
|
import { withProgress } from '../../../shared/ui/index.js';
|
||||||
import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js';
|
import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js';
|
||||||
@ -141,8 +141,7 @@ export async function resolveTaskExecution(
|
|||||||
if (data.auto_pr !== undefined) {
|
if (data.auto_pr !== undefined) {
|
||||||
autoPr = data.auto_pr;
|
autoPr = data.auto_pr;
|
||||||
} else {
|
} else {
|
||||||
const { global: globalConfig } = loadConfig(defaultCwd);
|
autoPr = resolveConfigValue(defaultCwd, 'autoPr') ?? false;
|
||||||
autoPr = globalConfig.autoPr ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* Session management helpers for agent execution
|
* Session management helpers for agent execution
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadPersonaSessions, updatePersonaSession, loadConfig } from '../../../infra/config/index.js';
|
import { loadPersonaSessions, updatePersonaSession, resolveConfigValue } from '../../../infra/config/index.js';
|
||||||
import type { AgentResponse } from '../../../core/models/index.js';
|
import type { AgentResponse } from '../../../core/models/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,7 +15,7 @@ export async function withPersonaSession(
|
|||||||
fn: (sessionId?: string) => Promise<AgentResponse>,
|
fn: (sessionId?: string) => Promise<AgentResponse>,
|
||||||
provider?: string
|
provider?: string
|
||||||
): Promise<AgentResponse> {
|
): Promise<AgentResponse> {
|
||||||
const resolvedProvider = provider ?? loadConfig(cwd).global.provider ?? 'claude';
|
const resolvedProvider = provider ?? resolveConfigValue(cwd, 'provider') ?? 'claude';
|
||||||
const sessions = loadPersonaSessions(cwd, resolvedProvider);
|
const sessions = loadPersonaSessions(cwd, resolvedProvider);
|
||||||
const sessionId = sessions[personaName];
|
const sessionId = sessions[personaName];
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* Task execution logic
|
* Task execution logic
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadPieceByIdentifier, isPiecePath, loadConfig } from '../../../infra/config/index.js';
|
import { loadPieceByIdentifier, isPiecePath, resolveConfigValues } from '../../../infra/config/index.js';
|
||||||
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js';
|
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js';
|
||||||
import {
|
import {
|
||||||
header,
|
header,
|
||||||
@ -86,21 +86,22 @@ async function executeTaskWithResult(options: ExecuteTaskOptions): Promise<Piece
|
|||||||
movements: pieceConfig.movements.map((s: { name: string }) => s.name),
|
movements: pieceConfig.movements.map((s: { name: string }) => s.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = loadConfig(projectCwd);
|
const config = resolveConfigValues(projectCwd, [
|
||||||
const globalConfig = config.global;
|
'language',
|
||||||
const projectConfig = config.project;
|
'provider',
|
||||||
|
'model',
|
||||||
|
'providerOptions',
|
||||||
|
'personaProviders',
|
||||||
|
'providerProfiles',
|
||||||
|
]);
|
||||||
return await executePiece(pieceConfig, task, cwd, {
|
return await executePiece(pieceConfig, task, cwd, {
|
||||||
projectCwd,
|
projectCwd,
|
||||||
language: globalConfig.language,
|
language: config.language,
|
||||||
provider: agentOverrides?.provider,
|
provider: agentOverrides?.provider ?? config.provider,
|
||||||
projectProvider: projectConfig.provider,
|
model: agentOverrides?.model ?? config.model,
|
||||||
globalProvider: globalConfig.provider,
|
providerOptions: config.providerOptions,
|
||||||
model: agentOverrides?.model,
|
personaProviders: config.personaProviders,
|
||||||
projectProviderOptions: projectConfig.providerOptions,
|
providerProfiles: config.providerProfiles,
|
||||||
globalProviderOptions: globalConfig.providerOptions,
|
|
||||||
personaProviders: globalConfig.personaProviders,
|
|
||||||
projectProviderProfiles: projectConfig.providerProfiles,
|
|
||||||
globalProviderProfiles: globalConfig.providerProfiles,
|
|
||||||
interactiveUserInput,
|
interactiveUserInput,
|
||||||
interactiveMetadata,
|
interactiveMetadata,
|
||||||
startMovement,
|
startMovement,
|
||||||
@ -237,7 +238,10 @@ export async function runAllTasks(
|
|||||||
options?: TaskExecutionOptions,
|
options?: TaskExecutionOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const taskRunner = new TaskRunner(cwd);
|
const taskRunner = new TaskRunner(cwd);
|
||||||
const { global: globalConfig } = loadConfig(cwd);
|
const globalConfig = resolveConfigValues(
|
||||||
|
cwd,
|
||||||
|
['notificationSound', 'notificationSoundEvents', 'concurrency', 'taskPollIntervalMs'],
|
||||||
|
);
|
||||||
const shouldNotifyRunComplete = globalConfig.notificationSound !== false
|
const shouldNotifyRunComplete = globalConfig.notificationSound !== false
|
||||||
&& globalConfig.notificationSoundEvents?.runComplete !== false;
|
&& globalConfig.notificationSoundEvents?.runComplete !== false;
|
||||||
const shouldNotifyRunAbort = globalConfig.notificationSound !== false
|
const shouldNotifyRunAbort = globalConfig.notificationSound !== false
|
||||||
|
|||||||
@ -33,21 +33,13 @@ export interface PieceExecutionOptions {
|
|||||||
/** Language for instruction metadata */
|
/** Language for instruction metadata */
|
||||||
language?: Language;
|
language?: Language;
|
||||||
provider?: ProviderType;
|
provider?: ProviderType;
|
||||||
/** Project config provider */
|
|
||||||
projectProvider?: ProviderType;
|
|
||||||
/** Global config provider */
|
|
||||||
globalProvider?: ProviderType;
|
|
||||||
model?: string;
|
model?: string;
|
||||||
/** Project-level provider options */
|
/** Resolved provider options */
|
||||||
projectProviderOptions?: MovementProviderOptions;
|
providerOptions?: MovementProviderOptions;
|
||||||
/** Global-level provider options */
|
|
||||||
globalProviderOptions?: MovementProviderOptions;
|
|
||||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||||
personaProviders?: Record<string, ProviderType>;
|
personaProviders?: Record<string, ProviderType>;
|
||||||
/** Project-level provider permission profiles */
|
/** Resolved provider permission profiles */
|
||||||
projectProviderProfiles?: ProviderPermissionProfiles;
|
providerProfiles?: ProviderPermissionProfiles;
|
||||||
/** Global-level provider permission profiles */
|
|
||||||
globalProviderProfiles?: ProviderPermissionProfiles;
|
|
||||||
/** Enable interactive user input during step transitions */
|
/** Enable interactive user input during step transitions */
|
||||||
interactiveUserInput?: boolean;
|
interactiveUserInput?: boolean;
|
||||||
/** Interactive mode result metadata for NDJSON logging */
|
/** Interactive mode result metadata for NDJSON logging */
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js';
|
import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js';
|
||||||
import { loadTemplate } from '../../../shared/prompts/index.js';
|
import { loadTemplate } from '../../../shared/prompts/index.js';
|
||||||
import { getLabelObject } from '../../../shared/i18n/index.js';
|
import { getLabelObject } from '../../../shared/i18n/index.js';
|
||||||
import { loadConfig } from '../../../infra/config/index.js';
|
import { resolveConfigValues } from '../../../infra/config/index.js';
|
||||||
|
|
||||||
export type InstructModeAction = 'execute' | 'save_task' | 'cancel';
|
export type InstructModeAction = 'execute' | 'save_task' | 'cancel';
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ export async function runInstructMode(
|
|||||||
pieceContext?: PieceContext,
|
pieceContext?: PieceContext,
|
||||||
runSessionContext?: RunSessionContext,
|
runSessionContext?: RunSessionContext,
|
||||||
): Promise<InstructModeResult> {
|
): Promise<InstructModeResult> {
|
||||||
const { global: globalConfig } = loadConfig(cwd);
|
const globalConfig = resolveConfigValues(cwd, ['language', 'provider']);
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
|
|
||||||
if (!globalConfig.provider) {
|
if (!globalConfig.provider) {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
TaskRunner,
|
TaskRunner,
|
||||||
detectDefaultBranch,
|
detectDefaultBranch,
|
||||||
} from '../../../infra/task/index.js';
|
} from '../../../infra/task/index.js';
|
||||||
import { loadConfig, getPieceDescription } from '../../../infra/config/index.js';
|
import { resolveConfigValues, getPieceDescription } from '../../../infra/config/index.js';
|
||||||
import { info, error as logError } from '../../../shared/ui/index.js';
|
import { info, error as logError } from '../../../shared/ui/index.js';
|
||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import { runInstructMode } from './instructMode.js';
|
import { runInstructMode } from './instructMode.js';
|
||||||
@ -93,7 +93,7 @@ export async function instructBranch(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { global: globalConfig } = loadConfig(projectDir);
|
const globalConfig = resolveConfigValues(projectDir, ['interactivePreviewMovements', 'language']);
|
||||||
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
||||||
const pieceContext: PieceContext = {
|
const pieceContext: PieceContext = {
|
||||||
name: pieceDesc.name,
|
name: pieceDesc.name,
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import type { TaskListItem } from '../../../infra/task/index.js';
|
import type { TaskListItem } from '../../../infra/task/index.js';
|
||||||
import { TaskRunner } from '../../../infra/task/index.js';
|
import { TaskRunner } from '../../../infra/task/index.js';
|
||||||
import { loadPieceByIdentifier, loadConfig, getPieceDescription } from '../../../infra/config/index.js';
|
import { loadPieceByIdentifier, resolveConfigValue, getPieceDescription } from '../../../infra/config/index.js';
|
||||||
import { selectPiece } from '../../pieceSelection/index.js';
|
import { selectPiece } from '../../pieceSelection/index.js';
|
||||||
import { selectOption } from '../../../shared/prompt/index.js';
|
import { selectOption } from '../../../shared/prompt/index.js';
|
||||||
import { info, header, blankLine, status } from '../../../shared/ui/index.js';
|
import { info, header, blankLine, status } from '../../../shared/ui/index.js';
|
||||||
@ -133,7 +133,7 @@ export async function retryFailedTask(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { global: globalConfig } = loadConfig(projectDir);
|
const previewCount = resolveConfigValue(projectDir, 'interactivePreviewMovements');
|
||||||
const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir);
|
const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir);
|
||||||
|
|
||||||
if (!pieceConfig) {
|
if (!pieceConfig) {
|
||||||
@ -145,7 +145,7 @@ export async function retryFailedTask(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
const pieceDesc = getPieceDescription(selectedPiece, projectDir, previewCount);
|
||||||
const pieceContext = {
|
const pieceContext = {
|
||||||
name: pieceDesc.name,
|
name: pieceDesc.name,
|
||||||
description: pieceDesc.description,
|
description: pieceDesc.description,
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { getGlobalConfigDir } from '../paths.js';
|
import { getGlobalConfigDir } from '../paths.js';
|
||||||
import { loadGlobalConfig } from './globalConfig.js';
|
import { loadConfig } from '../loadConfig.js';
|
||||||
|
|
||||||
const INITIAL_USER_CATEGORIES_CONTENT = 'piece_categories: {}\n';
|
const INITIAL_USER_CATEGORIES_CONTENT = 'piece_categories: {}\n';
|
||||||
|
|
||||||
@ -16,8 +16,8 @@ function getDefaultPieceCategoriesPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Get the path to the user's piece categories file. */
|
/** Get the path to the user's piece categories file. */
|
||||||
export function getPieceCategoriesPath(): string {
|
export function getPieceCategoriesPath(cwd: string): string {
|
||||||
const config = loadGlobalConfig();
|
const config = loadConfig(cwd);
|
||||||
if (config.pieceCategoriesFile) {
|
if (config.pieceCategoriesFile) {
|
||||||
return config.pieceCategoriesFile;
|
return config.pieceCategoriesFile;
|
||||||
}
|
}
|
||||||
@ -27,8 +27,8 @@ export function getPieceCategoriesPath(): string {
|
|||||||
/**
|
/**
|
||||||
* Reset user categories overlay file to initial content.
|
* Reset user categories overlay file to initial content.
|
||||||
*/
|
*/
|
||||||
export function resetPieceCategories(): void {
|
export function resetPieceCategories(cwd: string): void {
|
||||||
const userPath = getPieceCategoriesPath();
|
const userPath = getPieceCategoriesPath(cwd);
|
||||||
const dir = dirname(userPath);
|
const dir = dirname(userPath);
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
mkdirSync(dir, { recursive: true });
|
mkdirSync(dir, { recursive: true });
|
||||||
|
|||||||
@ -6,4 +6,4 @@ export * from './paths.js';
|
|||||||
export * from './loaders/index.js';
|
export * from './loaders/index.js';
|
||||||
export * from './global/index.js';
|
export * from './global/index.js';
|
||||||
export * from './project/index.js';
|
export * from './project/index.js';
|
||||||
export * from './loadConfig.js';
|
export * from './resolveConfigValue.js';
|
||||||
|
|||||||
@ -1,16 +1,108 @@
|
|||||||
import type { GlobalConfig } from '../../core/models/index.js';
|
import type { GlobalConfig } from '../../core/models/index.js';
|
||||||
import type { ProjectLocalConfig } from './project/projectConfig.js';
|
import type { MovementProviderOptions } from '../../core/models/piece-types.js';
|
||||||
|
import type { ProviderPermissionProfiles } from '../../core/models/provider-profiles.js';
|
||||||
import { loadGlobalConfig } from './global/globalConfig.js';
|
import { loadGlobalConfig } from './global/globalConfig.js';
|
||||||
import { loadProjectConfig } from './project/projectConfig.js';
|
import { loadProjectConfig } from './project/projectConfig.js';
|
||||||
|
import { envVarNameFromPath } from './env/config-env-overrides.js';
|
||||||
|
|
||||||
export interface LoadedConfig {
|
export interface LoadedConfig extends GlobalConfig {
|
||||||
global: GlobalConfig;
|
piece: string;
|
||||||
project: ProjectLocalConfig;
|
verbose: boolean;
|
||||||
|
providerOptions?: MovementProviderOptions;
|
||||||
|
providerProfiles?: ProviderPermissionProfiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadConfig(projectDir: string): LoadedConfig {
|
export function loadConfig(projectDir: string): LoadedConfig {
|
||||||
|
const global = loadGlobalConfig();
|
||||||
|
const project = loadProjectConfig(projectDir);
|
||||||
|
const provider = project.provider ?? global.provider;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
global: loadGlobalConfig(),
|
...global,
|
||||||
project: loadProjectConfig(projectDir),
|
piece: project.piece ?? 'default',
|
||||||
|
provider,
|
||||||
|
model: resolveModel(global, provider),
|
||||||
|
verbose: resolveVerbose(project.verbose, global.verbose),
|
||||||
|
providerOptions: mergeProviderOptions(global.providerOptions, project.providerOptions),
|
||||||
|
providerProfiles: mergeProviderProfiles(global.providerProfiles, project.providerProfiles),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveModel(global: GlobalConfig, provider: GlobalConfig['provider']): string | undefined {
|
||||||
|
if (!global.model) return undefined;
|
||||||
|
const globalProvider = global.provider ?? 'claude';
|
||||||
|
const resolvedProvider = provider ?? 'claude';
|
||||||
|
if (globalProvider !== resolvedProvider) return undefined;
|
||||||
|
return global.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveVerbose(projectVerbose: boolean | undefined, globalVerbose: boolean | undefined): boolean {
|
||||||
|
const envVerbose = loadEnvBooleanSetting('verbose');
|
||||||
|
if (envVerbose !== undefined) return envVerbose;
|
||||||
|
if (projectVerbose !== undefined) return projectVerbose;
|
||||||
|
if (globalVerbose !== undefined) return globalVerbose;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEnvBooleanSetting(configKey: string): boolean | undefined {
|
||||||
|
const envKey = envVarNameFromPath(configKey);
|
||||||
|
const raw = process.env[envKey];
|
||||||
|
if (raw === undefined) return undefined;
|
||||||
|
|
||||||
|
const normalized = raw.trim().toLowerCase();
|
||||||
|
if (normalized === 'true') return true;
|
||||||
|
if (normalized === 'false') return false;
|
||||||
|
|
||||||
|
throw new Error(`${envKey} must be one of: true, false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeProviderOptions(
|
||||||
|
globalOptions: MovementProviderOptions | undefined,
|
||||||
|
projectOptions: MovementProviderOptions | undefined,
|
||||||
|
): MovementProviderOptions | undefined {
|
||||||
|
if (!globalOptions && !projectOptions) return undefined;
|
||||||
|
|
||||||
|
const result: MovementProviderOptions = {};
|
||||||
|
if (globalOptions?.codex || projectOptions?.codex) {
|
||||||
|
result.codex = { ...globalOptions?.codex, ...projectOptions?.codex };
|
||||||
|
}
|
||||||
|
if (globalOptions?.opencode || projectOptions?.opencode) {
|
||||||
|
result.opencode = { ...globalOptions?.opencode, ...projectOptions?.opencode };
|
||||||
|
}
|
||||||
|
if (globalOptions?.claude?.sandbox || projectOptions?.claude?.sandbox) {
|
||||||
|
result.claude = {
|
||||||
|
sandbox: {
|
||||||
|
...globalOptions?.claude?.sandbox,
|
||||||
|
...projectOptions?.claude?.sandbox,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(result).length > 0 ? result : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeProviderProfiles(
|
||||||
|
globalProfiles: ProviderPermissionProfiles | undefined,
|
||||||
|
projectProfiles: ProviderPermissionProfiles | undefined,
|
||||||
|
): ProviderPermissionProfiles | undefined {
|
||||||
|
if (!globalProfiles && !projectProfiles) return undefined;
|
||||||
|
|
||||||
|
const merged: ProviderPermissionProfiles = { ...(globalProfiles ?? {}) };
|
||||||
|
for (const [provider, profile] of Object.entries(projectProfiles ?? {})) {
|
||||||
|
const key = provider as keyof ProviderPermissionProfiles;
|
||||||
|
const existing = merged[key];
|
||||||
|
if (!existing) {
|
||||||
|
merged[key] = profile;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
merged[key] = {
|
||||||
|
defaultPermissionMode: profile.defaultPermissionMode,
|
||||||
|
movementPermissionOverrides: {
|
||||||
|
...(existing.movementPermissionOverrides ?? {}),
|
||||||
|
...(profile.movementPermissionOverrides ?? {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||||
|
}
|
||||||
|
|||||||
@ -16,11 +16,11 @@ import {
|
|||||||
getBuiltinPiecesDir,
|
getBuiltinPiecesDir,
|
||||||
isPathSafe,
|
isPathSafe,
|
||||||
} from '../paths.js';
|
} from '../paths.js';
|
||||||
import { getLanguage } from '../global/globalConfig.js';
|
import { loadConfig } from '../loadConfig.js';
|
||||||
|
|
||||||
/** Get all allowed base directories for persona prompt files */
|
/** Get all allowed base directories for persona prompt files */
|
||||||
function getAllowedPromptBases(): string[] {
|
function getAllowedPromptBases(cwd: string): string[] {
|
||||||
const lang = getLanguage();
|
const lang = loadConfig(cwd).language;
|
||||||
return [
|
return [
|
||||||
getGlobalPersonasDir(),
|
getGlobalPersonasDir(),
|
||||||
getGlobalPiecesDir(),
|
getGlobalPiecesDir(),
|
||||||
@ -63,14 +63,14 @@ export function listCustomAgents(): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Load agent prompt content. */
|
/** Load agent prompt content. */
|
||||||
export function loadAgentPrompt(agent: CustomAgentConfig): string {
|
export function loadAgentPrompt(agent: CustomAgentConfig, cwd: string): string {
|
||||||
if (agent.prompt) {
|
if (agent.prompt) {
|
||||||
return agent.prompt;
|
return agent.prompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agent.promptFile) {
|
if (agent.promptFile) {
|
||||||
const promptFile = agent.promptFile;
|
const promptFile = agent.promptFile;
|
||||||
const isValid = getAllowedPromptBases().some((base) => isPathSafe(base, promptFile));
|
const isValid = getAllowedPromptBases(cwd).some((base) => isPathSafe(base, promptFile));
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new Error(`Agent prompt file path is not allowed: ${agent.promptFile}`);
|
throw new Error(`Agent prompt file path is not allowed: ${agent.promptFile}`);
|
||||||
}
|
}
|
||||||
@ -86,8 +86,8 @@ export function loadAgentPrompt(agent: CustomAgentConfig): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Load persona prompt from a resolved path. */
|
/** Load persona prompt from a resolved path. */
|
||||||
export function loadPersonaPromptFromPath(personaPath: string): string {
|
export function loadPersonaPromptFromPath(personaPath: string, cwd: string): string {
|
||||||
const isValid = getAllowedPromptBases().some((base) => isPathSafe(base, personaPath));
|
const isValid = getAllowedPromptBases(cwd).some((base) => isPathSafe(base, personaPath));
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new Error(`Persona prompt file path is not allowed: ${personaPath}`);
|
throw new Error(`Persona prompt file path is not allowed: ${personaPath}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,10 +10,10 @@ import { existsSync, readFileSync } from 'node:fs';
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { parse as parseYaml } from 'yaml';
|
import { parse as parseYaml } from 'yaml';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
import { getLanguage, getBuiltinPiecesEnabled, getDisabledBuiltins } from '../global/globalConfig.js';
|
|
||||||
import { getPieceCategoriesPath } from '../global/pieceCategories.js';
|
import { getPieceCategoriesPath } from '../global/pieceCategories.js';
|
||||||
import { getLanguageResourcesDir } from '../../resources/index.js';
|
import { getLanguageResourcesDir } from '../../resources/index.js';
|
||||||
import { listBuiltinPieceNames } from './pieceResolver.js';
|
import { listBuiltinPieceNames } from './pieceResolver.js';
|
||||||
|
import { loadConfig } from '../loadConfig.js';
|
||||||
import type { PieceWithSource } from './pieceResolver.js';
|
import type { PieceWithSource } from './pieceResolver.js';
|
||||||
|
|
||||||
const CategoryConfigSchema = z.object({
|
const CategoryConfigSchema = z.object({
|
||||||
@ -232,8 +232,8 @@ function resolveOthersCategoryName(defaultConfig: ParsedCategoryConfig, userConf
|
|||||||
* Load default categories from builtin resource file.
|
* Load default categories from builtin resource file.
|
||||||
* Returns null if file doesn't exist or has no piece_categories.
|
* Returns null if file doesn't exist or has no piece_categories.
|
||||||
*/
|
*/
|
||||||
export function loadDefaultCategories(): CategoryConfig | null {
|
export function loadDefaultCategories(cwd: string): CategoryConfig | null {
|
||||||
const lang = getLanguage();
|
const lang = loadConfig(cwd).language;
|
||||||
const filePath = join(getLanguageResourcesDir(lang), 'piece-categories.yaml');
|
const filePath = join(getLanguageResourcesDir(lang), 'piece-categories.yaml');
|
||||||
const parsed = loadCategoryConfigFromPath(filePath, filePath);
|
const parsed = loadCategoryConfigFromPath(filePath, filePath);
|
||||||
|
|
||||||
@ -255,8 +255,8 @@ export function loadDefaultCategories(): CategoryConfig | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Get the path to the builtin default categories file. */
|
/** Get the path to the builtin default categories file. */
|
||||||
export function getDefaultCategoriesPath(): string {
|
export function getDefaultCategoriesPath(cwd: string): string {
|
||||||
const lang = getLanguage();
|
const lang = loadConfig(cwd).language;
|
||||||
return join(getLanguageResourcesDir(lang), 'piece-categories.yaml');
|
return join(getLanguageResourcesDir(lang), 'piece-categories.yaml');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,14 +264,14 @@ export function getDefaultCategoriesPath(): string {
|
|||||||
* Get effective piece categories configuration.
|
* Get effective piece categories configuration.
|
||||||
* Built from builtin categories and optional user overlay.
|
* Built from builtin categories and optional user overlay.
|
||||||
*/
|
*/
|
||||||
export function getPieceCategories(): CategoryConfig | null {
|
export function getPieceCategories(cwd: string): CategoryConfig | null {
|
||||||
const defaultPath = getDefaultCategoriesPath();
|
const defaultPath = getDefaultCategoriesPath(cwd);
|
||||||
const defaultConfig = loadCategoryConfigFromPath(defaultPath, defaultPath);
|
const defaultConfig = loadCategoryConfigFromPath(defaultPath, defaultPath);
|
||||||
if (!defaultConfig?.pieceCategories) {
|
if (!defaultConfig?.pieceCategories) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userPath = getPieceCategoriesPath();
|
const userPath = getPieceCategoriesPath(cwd);
|
||||||
const userConfig = loadCategoryConfigFromPath(userPath, userPath);
|
const userConfig = loadCategoryConfigFromPath(userPath, userPath);
|
||||||
|
|
||||||
const merged = userConfig?.pieceCategories
|
const merged = userConfig?.pieceCategories
|
||||||
@ -376,14 +376,16 @@ function appendOthersCategory(
|
|||||||
export function buildCategorizedPieces(
|
export function buildCategorizedPieces(
|
||||||
allPieces: Map<string, PieceWithSource>,
|
allPieces: Map<string, PieceWithSource>,
|
||||||
config: CategoryConfig,
|
config: CategoryConfig,
|
||||||
|
cwd: string,
|
||||||
): CategorizedPieces {
|
): CategorizedPieces {
|
||||||
|
const globalConfig = loadConfig(cwd);
|
||||||
const ignoreMissing = new Set<string>();
|
const ignoreMissing = new Set<string>();
|
||||||
if (!getBuiltinPiecesEnabled()) {
|
if (globalConfig.enableBuiltinPieces === false) {
|
||||||
for (const name of listBuiltinPieceNames({ includeDisabled: true })) {
|
for (const name of listBuiltinPieceNames(cwd, { includeDisabled: true })) {
|
||||||
ignoreMissing.add(name);
|
ignoreMissing.add(name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const name of getDisabledBuiltins()) {
|
for (const name of (globalConfig.disabledBuiltins ?? [])) {
|
||||||
ignoreMissing.add(name);
|
ignoreMissing.add(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { parse as parseYaml } from 'yaml';
|
|||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
import { PieceConfigRawSchema, PieceMovementRawSchema } from '../../../core/models/index.js';
|
import { PieceConfigRawSchema, PieceMovementRawSchema } from '../../../core/models/index.js';
|
||||||
import type { PieceConfig, PieceMovement, PieceRule, OutputContractEntry, OutputContractItem, LoopMonitorConfig, LoopMonitorJudge, ArpeggioMovementConfig, ArpeggioMergeMovementConfig, TeamLeaderConfig } from '../../../core/models/index.js';
|
import type { PieceConfig, PieceMovement, PieceRule, OutputContractEntry, OutputContractItem, LoopMonitorConfig, LoopMonitorJudge, ArpeggioMovementConfig, ArpeggioMergeMovementConfig, TeamLeaderConfig } from '../../../core/models/index.js';
|
||||||
import { getLanguage } from '../global/globalConfig.js';
|
import { loadConfig } from '../loadConfig.js';
|
||||||
import {
|
import {
|
||||||
type PieceSections,
|
type PieceSections,
|
||||||
type FacetResolutionContext,
|
type FacetResolutionContext,
|
||||||
@ -428,9 +428,9 @@ export function normalizePieceConfig(
|
|||||||
/**
|
/**
|
||||||
* Load a piece from a YAML file.
|
* Load a piece from a YAML file.
|
||||||
* @param filePath Path to the piece YAML file
|
* @param filePath Path to the piece YAML file
|
||||||
* @param projectDir Optional project directory for 3-layer facet resolution
|
* @param projectDir Project directory for 3-layer facet resolution
|
||||||
*/
|
*/
|
||||||
export function loadPieceFromFile(filePath: string, projectDir?: string): PieceConfig {
|
export function loadPieceFromFile(filePath: string, projectDir: string): PieceConfig {
|
||||||
if (!existsSync(filePath)) {
|
if (!existsSync(filePath)) {
|
||||||
throw new Error(`Piece file not found: ${filePath}`);
|
throw new Error(`Piece file not found: ${filePath}`);
|
||||||
}
|
}
|
||||||
@ -439,7 +439,7 @@ export function loadPieceFromFile(filePath: string, projectDir?: string): PieceC
|
|||||||
const pieceDir = dirname(filePath);
|
const pieceDir = dirname(filePath);
|
||||||
|
|
||||||
const context: FacetResolutionContext = {
|
const context: FacetResolutionContext = {
|
||||||
lang: getLanguage(),
|
lang: loadConfig(projectDir).language,
|
||||||
projectDir,
|
projectDir,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { join, resolve, isAbsolute } from 'node:path';
|
|||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import type { PieceConfig, PieceMovement, InteractiveMode } from '../../../core/models/index.js';
|
import type { PieceConfig, PieceMovement, InteractiveMode } from '../../../core/models/index.js';
|
||||||
import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir } from '../paths.js';
|
import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir } from '../paths.js';
|
||||||
import { getLanguage, getDisabledBuiltins, getBuiltinPiecesEnabled } from '../global/globalConfig.js';
|
import { loadConfig } from '../loadConfig.js';
|
||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import { loadPieceFromFile } from './pieceParser.js';
|
import { loadPieceFromFile } from './pieceParser.js';
|
||||||
|
|
||||||
@ -23,10 +23,11 @@ export interface PieceWithSource {
|
|||||||
source: PieceSource;
|
source: PieceSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listBuiltinPieceNames(options?: { includeDisabled?: boolean }): string[] {
|
export function listBuiltinPieceNames(cwd: string, options?: { includeDisabled?: boolean }): string[] {
|
||||||
const lang = getLanguage();
|
const config = loadConfig(cwd);
|
||||||
|
const lang = config.language;
|
||||||
const dir = getBuiltinPiecesDir(lang);
|
const dir = getBuiltinPiecesDir(lang);
|
||||||
const disabled = options?.includeDisabled ? undefined : getDisabledBuiltins();
|
const disabled = options?.includeDisabled ? undefined : (config.disabledBuiltins ?? []);
|
||||||
const names = new Set<string>();
|
const names = new Set<string>();
|
||||||
for (const entry of iteratePieceDir(dir, 'builtin', disabled)) {
|
for (const entry of iteratePieceDir(dir, 'builtin', disabled)) {
|
||||||
names.add(entry.name);
|
names.add(entry.name);
|
||||||
@ -35,10 +36,11 @@ export function listBuiltinPieceNames(options?: { includeDisabled?: boolean }):
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Get builtin piece by name */
|
/** Get builtin piece by name */
|
||||||
export function getBuiltinPiece(name: string, projectCwd?: string): PieceConfig | null {
|
export function getBuiltinPiece(name: string, projectCwd: string): PieceConfig | null {
|
||||||
if (!getBuiltinPiecesEnabled()) return null;
|
const config = loadConfig(projectCwd);
|
||||||
const lang = getLanguage();
|
if (config.enableBuiltinPieces === false) return null;
|
||||||
const disabled = getDisabledBuiltins();
|
const lang = config.language;
|
||||||
|
const disabled = config.disabledBuiltins ?? [];
|
||||||
if (disabled.includes(name)) return null;
|
if (disabled.includes(name)) return null;
|
||||||
|
|
||||||
const builtinDir = getBuiltinPiecesDir(lang);
|
const builtinDir = getBuiltinPiecesDir(lang);
|
||||||
@ -69,7 +71,7 @@ function resolvePath(pathInput: string, basePath: string): string {
|
|||||||
function loadPieceFromPath(
|
function loadPieceFromPath(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
basePath: string,
|
basePath: string,
|
||||||
projectCwd?: string,
|
projectCwd: string,
|
||||||
): PieceConfig | null {
|
): PieceConfig | null {
|
||||||
const resolvedPath = resolvePath(filePath, basePath);
|
const resolvedPath = resolvePath(filePath, basePath);
|
||||||
if (!existsSync(resolvedPath)) {
|
if (!existsSync(resolvedPath)) {
|
||||||
@ -371,10 +373,11 @@ function* iteratePieceDir(
|
|||||||
|
|
||||||
/** Get the 3-layer directory list (builtin → user → project-local) */
|
/** Get the 3-layer directory list (builtin → user → project-local) */
|
||||||
function getPieceDirs(cwd: string): { dir: string; source: PieceSource; disabled?: string[] }[] {
|
function getPieceDirs(cwd: string): { dir: string; source: PieceSource; disabled?: string[] }[] {
|
||||||
const disabled = getDisabledBuiltins();
|
const config = loadConfig(cwd);
|
||||||
const lang = getLanguage();
|
const disabled = config.disabledBuiltins ?? [];
|
||||||
|
const lang = config.language;
|
||||||
const dirs: { dir: string; source: PieceSource; disabled?: string[] }[] = [];
|
const dirs: { dir: string; source: PieceSource; disabled?: string[] }[] = [];
|
||||||
if (getBuiltinPiecesEnabled()) {
|
if (config.enableBuiltinPieces !== false) {
|
||||||
dirs.push({ dir: getBuiltinPiecesDir(lang), disabled, source: 'builtin' });
|
dirs.push({ dir: getBuiltinPiecesDir(lang), disabled, source: 'builtin' });
|
||||||
}
|
}
|
||||||
dirs.push({ dir: getGlobalPiecesDir(), source: 'user' });
|
dirs.push({ dir: getGlobalPiecesDir(), source: 'user' });
|
||||||
|
|||||||
@ -27,6 +27,6 @@ function loadEnvBooleanSetting(configKey: string): boolean | undefined {
|
|||||||
|
|
||||||
export function isVerboseMode(projectDir: string): boolean {
|
export function isVerboseMode(projectDir: string): boolean {
|
||||||
const envValue = loadEnvBooleanSetting('verbose');
|
const envValue = loadEnvBooleanSetting('verbose');
|
||||||
const { project, global } = loadConfig(projectDir);
|
const config = loadConfig(projectDir);
|
||||||
return resolveValue(envValue, project.verbose, global.verbose, false);
|
return resolveValue(envValue, undefined, config.verbose, false);
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/infra/config/resolveConfigValue.ts
Normal file
22
src/infra/config/resolveConfigValue.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { loadConfig, type LoadedConfig } from './loadConfig.js';
|
||||||
|
|
||||||
|
export type ConfigParameterKey = keyof LoadedConfig;
|
||||||
|
|
||||||
|
export function resolveConfigValue<K extends ConfigParameterKey>(
|
||||||
|
projectDir: string,
|
||||||
|
key: K,
|
||||||
|
): LoadedConfig[K] {
|
||||||
|
return loadConfig(projectDir)[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveConfigValues<K extends ConfigParameterKey>(
|
||||||
|
projectDir: string,
|
||||||
|
keys: readonly K[],
|
||||||
|
): Pick<LoadedConfig, K> {
|
||||||
|
const config = loadConfig(projectDir);
|
||||||
|
const result = {} as Pick<LoadedConfig, K>;
|
||||||
|
for (const key of keys) {
|
||||||
|
result[key] = config[key];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@ import * as fs from 'node:fs';
|
|||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { createLogger, slugify } from '../../shared/utils/index.js';
|
import { createLogger, slugify } from '../../shared/utils/index.js';
|
||||||
import { loadConfig } from '../config/index.js';
|
import { resolveConfigValue } from '../config/index.js';
|
||||||
import type { WorktreeOptions, WorktreeResult } from './types.js';
|
import type { WorktreeOptions, WorktreeResult } from './types.js';
|
||||||
|
|
||||||
export type { WorktreeOptions, WorktreeResult };
|
export type { WorktreeOptions, WorktreeResult };
|
||||||
@ -36,11 +36,11 @@ export class CloneManager {
|
|||||||
* Returns the configured worktree_dir (resolved to absolute), or ../
|
* Returns the configured worktree_dir (resolved to absolute), or ../
|
||||||
*/
|
*/
|
||||||
private static resolveCloneBaseDir(projectDir: string): string {
|
private static resolveCloneBaseDir(projectDir: string): string {
|
||||||
const { global: globalConfig } = loadConfig(projectDir);
|
const worktreeDir = resolveConfigValue(projectDir, 'worktreeDir');
|
||||||
if (globalConfig.worktreeDir) {
|
if (worktreeDir) {
|
||||||
return path.isAbsolute(globalConfig.worktreeDir)
|
return path.isAbsolute(worktreeDir)
|
||||||
? globalConfig.worktreeDir
|
? worktreeDir
|
||||||
: path.resolve(projectDir, globalConfig.worktreeDir);
|
: path.resolve(projectDir, worktreeDir);
|
||||||
}
|
}
|
||||||
return path.join(projectDir, '..', 'takt-worktree');
|
return path.join(projectDir, '..', 'takt-worktree');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as wanakana from 'wanakana';
|
import * as wanakana from 'wanakana';
|
||||||
import { loadConfig } from '../config/index.js';
|
import { resolveConfigValues } from '../config/index.js';
|
||||||
import { getProvider, type ProviderType } from '../providers/index.js';
|
import { getProvider, type ProviderType } from '../providers/index.js';
|
||||||
import { createLogger } from '../../shared/utils/index.js';
|
import { createLogger } from '../../shared/utils/index.js';
|
||||||
import { loadTemplate } from '../../shared/prompts/index.js';
|
import { loadTemplate } from '../../shared/prompts/index.js';
|
||||||
@ -53,7 +53,7 @@ export class TaskSummarizer {
|
|||||||
taskName: string,
|
taskName: string,
|
||||||
options: SummarizeOptions,
|
options: SummarizeOptions,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const { global: globalConfig } = loadConfig(options.cwd);
|
const globalConfig = resolveConfigValues(options.cwd, ['branchNameStrategy', 'provider', 'model']);
|
||||||
const useLLM = options.useLLM ?? (globalConfig.branchNameStrategy === 'ai');
|
const useLLM = options.useLLM ?? (globalConfig.branchNameStrategy === 'ai');
|
||||||
log.info('Summarizing task name', { taskName, useLLM });
|
log.info('Summarizing task name', { taskName, useLLM });
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user