github-issue-255-ui (#266)
* update builtin * fix: OpenCode SDKサーバー起動タイムアウトを30秒に延長 * takt: github-issue-255-ui * 無駄な条件分岐を削除
This commit is contained in:
parent
3ff6f99152
commit
02272e595c
@ -33,7 +33,19 @@ vi.mock('../infra/config/index.js', async (importOriginal) => {
|
|||||||
return actual;
|
return actual;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { selectPieceFromEntries, selectPieceFromCategorizedPieces } = await import('../features/pieceSelection/index.js');
|
const configMock = vi.hoisted(() => ({
|
||||||
|
listPieces: vi.fn(),
|
||||||
|
listPieceEntries: vi.fn(),
|
||||||
|
loadAllPiecesWithSources: vi.fn(),
|
||||||
|
getPieceCategories: vi.fn(),
|
||||||
|
buildCategorizedPieces: vi.fn(),
|
||||||
|
getCurrentPiece: vi.fn(),
|
||||||
|
findPieceCategories: vi.fn(() => []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/config/index.js', () => configMock);
|
||||||
|
|
||||||
|
const { selectPieceFromEntries, selectPieceFromCategorizedPieces, selectPiece } = await import('../features/pieceSelection/index.js');
|
||||||
|
|
||||||
describe('selectPieceFromEntries', () => {
|
describe('selectPieceFromEntries', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -231,3 +243,93 @@ describe('selectPieceFromCategorizedPieces', () => {
|
|||||||
expect(labels.some((l) => l.includes('Dev'))).toBe(false);
|
expect(labels.some((l) => l.includes('Dev'))).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('selectPiece', () => {
|
||||||
|
const entries: PieceDirEntry[] = [
|
||||||
|
{ name: 'custom-flow', path: '/tmp/custom.yaml', source: 'user' },
|
||||||
|
{ name: 'builtin-flow', path: '/tmp/builtin.yaml', source: 'builtin' },
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
selectOptionMock.mockReset();
|
||||||
|
bookmarkState.bookmarks = [];
|
||||||
|
configMock.listPieces.mockReset();
|
||||||
|
configMock.listPieceEntries.mockReset();
|
||||||
|
configMock.loadAllPiecesWithSources.mockReset();
|
||||||
|
configMock.getPieceCategories.mockReset();
|
||||||
|
configMock.buildCategorizedPieces.mockReset();
|
||||||
|
configMock.getCurrentPiece.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default piece when no pieces found and fallbackToDefault is true', async () => {
|
||||||
|
configMock.getPieceCategories.mockReturnValue(null);
|
||||||
|
configMock.listPieces.mockReturnValue([]);
|
||||||
|
configMock.getCurrentPiece.mockReturnValue('default');
|
||||||
|
|
||||||
|
const result = await selectPiece('/cwd');
|
||||||
|
|
||||||
|
expect(result).toBe('default');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no pieces found and fallbackToDefault is false', async () => {
|
||||||
|
configMock.getPieceCategories.mockReturnValue(null);
|
||||||
|
configMock.listPieces.mockReturnValue([]);
|
||||||
|
configMock.getCurrentPiece.mockReturnValue('default');
|
||||||
|
|
||||||
|
const result = await selectPiece('/cwd', { fallbackToDefault: false });
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prompt selection even when only one piece exists', async () => {
|
||||||
|
configMock.getPieceCategories.mockReturnValue(null);
|
||||||
|
configMock.listPieces.mockReturnValue(['only-piece']);
|
||||||
|
configMock.listPieceEntries.mockReturnValue([
|
||||||
|
{ name: 'only-piece', path: '/tmp/only-piece.yaml', source: 'user' },
|
||||||
|
]);
|
||||||
|
configMock.getCurrentPiece.mockReturnValue('only-piece');
|
||||||
|
selectOptionMock.mockResolvedValueOnce('only-piece');
|
||||||
|
|
||||||
|
const result = await selectPiece('/cwd');
|
||||||
|
|
||||||
|
expect(result).toBe('only-piece');
|
||||||
|
expect(selectOptionMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use category-based selection when category config exists', async () => {
|
||||||
|
const pieceMap = createPieceMap([{ name: 'my-piece', source: 'user' }]);
|
||||||
|
const categorized: CategorizedPieces = {
|
||||||
|
categories: [{ name: 'Dev', pieces: ['my-piece'], children: [] }],
|
||||||
|
allPieces: pieceMap,
|
||||||
|
missingPieces: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
configMock.getPieceCategories.mockReturnValue({ categories: ['Dev'] });
|
||||||
|
configMock.loadAllPiecesWithSources.mockReturnValue(pieceMap);
|
||||||
|
configMock.buildCategorizedPieces.mockReturnValue(categorized);
|
||||||
|
configMock.getCurrentPiece.mockReturnValue('my-piece');
|
||||||
|
|
||||||
|
selectOptionMock.mockResolvedValueOnce('__current__');
|
||||||
|
|
||||||
|
const result = await selectPiece('/cwd');
|
||||||
|
|
||||||
|
expect(result).toBe('my-piece');
|
||||||
|
expect(configMock.buildCategorizedPieces).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use directory-based selection when no category config', async () => {
|
||||||
|
configMock.getPieceCategories.mockReturnValue(null);
|
||||||
|
configMock.listPieces.mockReturnValue(['piece-a', 'piece-b']);
|
||||||
|
configMock.listPieceEntries.mockReturnValue(entries);
|
||||||
|
configMock.getCurrentPiece.mockReturnValue('piece-a');
|
||||||
|
|
||||||
|
selectOptionMock
|
||||||
|
.mockResolvedValueOnce('custom')
|
||||||
|
.mockResolvedValueOnce('custom-flow');
|
||||||
|
|
||||||
|
const result = await selectPiece('/cwd');
|
||||||
|
|
||||||
|
expect(result).toBe('custom-flow');
|
||||||
|
expect(configMock.listPieceEntries).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -13,9 +13,6 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
listPieces: vi.fn(() => ['default']),
|
listPieces: vi.fn(() => ['default']),
|
||||||
listPieceEntries: vi.fn(() => []),
|
listPieceEntries: vi.fn(() => []),
|
||||||
isPiecePath: vi.fn(() => false),
|
isPiecePath: vi.fn(() => false),
|
||||||
loadAllPiecesWithSources: vi.fn(() => new Map()),
|
|
||||||
getPieceCategories: vi.fn(() => null),
|
|
||||||
buildCategorizedPieces: vi.fn(),
|
|
||||||
loadGlobalConfig: vi.fn(() => ({})),
|
loadGlobalConfig: vi.fn(() => ({})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -60,29 +57,25 @@ vi.mock('../features/pieceSelection/index.js', () => ({
|
|||||||
warnMissingPieces: vi.fn(),
|
warnMissingPieces: vi.fn(),
|
||||||
selectPieceFromCategorizedPieces: vi.fn(),
|
selectPieceFromCategorizedPieces: vi.fn(),
|
||||||
selectPieceFromEntries: vi.fn(),
|
selectPieceFromEntries: vi.fn(),
|
||||||
|
selectPiece: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { confirm } from '../shared/prompt/index.js';
|
import { confirm } from '../shared/prompt/index.js';
|
||||||
import {
|
import {
|
||||||
getCurrentPiece,
|
getCurrentPiece,
|
||||||
loadAllPiecesWithSources,
|
listPieces,
|
||||||
getPieceCategories,
|
|
||||||
buildCategorizedPieces,
|
|
||||||
} from '../infra/config/index.js';
|
} from '../infra/config/index.js';
|
||||||
import { createSharedClone, autoCommitAndPush, summarizeTaskName } from '../infra/task/index.js';
|
import { createSharedClone, autoCommitAndPush, summarizeTaskName } from '../infra/task/index.js';
|
||||||
import { warnMissingPieces, selectPieceFromCategorizedPieces } from '../features/pieceSelection/index.js';
|
import { selectPiece } from '../features/pieceSelection/index.js';
|
||||||
import { selectAndExecuteTask, determinePiece } from '../features/tasks/execute/selectAndExecute.js';
|
import { selectAndExecuteTask, determinePiece } from '../features/tasks/execute/selectAndExecute.js';
|
||||||
|
|
||||||
const mockConfirm = vi.mocked(confirm);
|
const mockConfirm = vi.mocked(confirm);
|
||||||
const mockGetCurrentPiece = vi.mocked(getCurrentPiece);
|
const mockGetCurrentPiece = vi.mocked(getCurrentPiece);
|
||||||
const mockLoadAllPiecesWithSources = vi.mocked(loadAllPiecesWithSources);
|
const mockListPieces = vi.mocked(listPieces);
|
||||||
const mockGetPieceCategories = vi.mocked(getPieceCategories);
|
|
||||||
const mockBuildCategorizedPieces = vi.mocked(buildCategorizedPieces);
|
|
||||||
const mockCreateSharedClone = vi.mocked(createSharedClone);
|
const mockCreateSharedClone = vi.mocked(createSharedClone);
|
||||||
const mockAutoCommitAndPush = vi.mocked(autoCommitAndPush);
|
const mockAutoCommitAndPush = vi.mocked(autoCommitAndPush);
|
||||||
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
|
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
|
||||||
const mockWarnMissingPieces = vi.mocked(warnMissingPieces);
|
const mockSelectPiece = vi.mocked(selectPiece);
|
||||||
const mockSelectPieceFromCategorizedPieces = vi.mocked(selectPieceFromCategorizedPieces);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@ -121,44 +114,12 @@ describe('resolveAutoPr default in selectAndExecuteTask', () => {
|
|||||||
expect(autoPrCall![1]).toBe(true);
|
expect(autoPrCall![1]).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn only user-origin missing pieces during interactive selection', async () => {
|
it('should call selectPiece when no override is provided', async () => {
|
||||||
// Given: category selection is enabled and both builtin/user missing pieces exist
|
mockSelectPiece.mockResolvedValue('selected-piece');
|
||||||
mockGetCurrentPiece.mockReturnValue('default');
|
|
||||||
mockLoadAllPiecesWithSources.mockReturnValue(new Map([
|
|
||||||
['default', {
|
|
||||||
source: 'builtin',
|
|
||||||
config: {
|
|
||||||
name: 'default',
|
|
||||||
movements: [],
|
|
||||||
initialMovement: 'start',
|
|
||||||
maxMovements: 1,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
]));
|
|
||||||
mockGetPieceCategories.mockReturnValue({
|
|
||||||
pieceCategories: [],
|
|
||||||
builtinPieceCategories: [],
|
|
||||||
userPieceCategories: [],
|
|
||||||
showOthersCategory: true,
|
|
||||||
othersCategoryName: 'Others',
|
|
||||||
});
|
|
||||||
mockBuildCategorizedPieces.mockReturnValue({
|
|
||||||
categories: [],
|
|
||||||
allPieces: new Map(),
|
|
||||||
missingPieces: [
|
|
||||||
{ categoryPath: ['Quick Start'], pieceName: 'default', source: 'builtin' },
|
|
||||||
{ categoryPath: ['Custom'], pieceName: 'my-missing', source: 'user' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
mockSelectPieceFromCategorizedPieces.mockResolvedValue('default');
|
|
||||||
|
|
||||||
// When
|
|
||||||
const selected = await determinePiece('/project');
|
const selected = await determinePiece('/project');
|
||||||
|
|
||||||
// Then
|
expect(selected).toBe('selected-piece');
|
||||||
expect(selected).toBe('default');
|
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
|
||||||
expect(mockWarnMissingPieces).toHaveBeenCalledWith([
|
|
||||||
{ categoryPath: ['Custom'], pieceName: 'my-missing', source: 'user' },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,19 +5,13 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
listPieceEntries: vi.fn(() => []),
|
|
||||||
loadAllPiecesWithSources: vi.fn(() => new Map()),
|
|
||||||
getPieceCategories: vi.fn(() => null),
|
|
||||||
buildCategorizedPieces: vi.fn(),
|
|
||||||
loadPiece: vi.fn(() => null),
|
loadPiece: vi.fn(() => null),
|
||||||
getCurrentPiece: vi.fn(() => 'default'),
|
getCurrentPiece: vi.fn(() => 'default'),
|
||||||
setCurrentPiece: vi.fn(),
|
setCurrentPiece: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/pieceSelection/index.js', () => ({
|
vi.mock('../features/pieceSelection/index.js', () => ({
|
||||||
warnMissingPieces: vi.fn(),
|
selectPiece: vi.fn(),
|
||||||
selectPieceFromCategorizedPieces: vi.fn(),
|
|
||||||
selectPieceFromEntries: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
@ -26,65 +20,41 @@ vi.mock('../shared/ui/index.js', () => ({
|
|||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import { getCurrentPiece, loadPiece, setCurrentPiece } from '../infra/config/index.js';
|
||||||
loadAllPiecesWithSources,
|
import { selectPiece } from '../features/pieceSelection/index.js';
|
||||||
getPieceCategories,
|
|
||||||
buildCategorizedPieces,
|
|
||||||
} from '../infra/config/index.js';
|
|
||||||
import {
|
|
||||||
warnMissingPieces,
|
|
||||||
selectPieceFromCategorizedPieces,
|
|
||||||
} from '../features/pieceSelection/index.js';
|
|
||||||
import { switchPiece } from '../features/config/switchPiece.js';
|
import { switchPiece } from '../features/config/switchPiece.js';
|
||||||
|
|
||||||
const mockLoadAllPiecesWithSources = vi.mocked(loadAllPiecesWithSources);
|
const mockGetCurrentPiece = vi.mocked(getCurrentPiece);
|
||||||
const mockGetPieceCategories = vi.mocked(getPieceCategories);
|
const mockLoadPiece = vi.mocked(loadPiece);
|
||||||
const mockBuildCategorizedPieces = vi.mocked(buildCategorizedPieces);
|
const mockSetCurrentPiece = vi.mocked(setCurrentPiece);
|
||||||
const mockWarnMissingPieces = vi.mocked(warnMissingPieces);
|
const mockSelectPiece = vi.mocked(selectPiece);
|
||||||
const mockSelectPieceFromCategorizedPieces = vi.mocked(selectPieceFromCategorizedPieces);
|
|
||||||
|
|
||||||
describe('switchPiece', () => {
|
describe('switchPiece', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn only user-origin missing pieces during interactive switch', async () => {
|
it('should call selectPiece with fallbackToDefault: false', async () => {
|
||||||
// Given
|
mockSelectPiece.mockResolvedValue(null);
|
||||||
mockLoadAllPiecesWithSources.mockReturnValue(new Map([
|
|
||||||
['default', {
|
|
||||||
source: 'builtin',
|
|
||||||
config: {
|
|
||||||
name: 'default',
|
|
||||||
movements: [],
|
|
||||||
initialMovement: 'start',
|
|
||||||
maxMovements: 1,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
]));
|
|
||||||
mockGetPieceCategories.mockReturnValue({
|
|
||||||
pieceCategories: [],
|
|
||||||
builtinPieceCategories: [],
|
|
||||||
userPieceCategories: [],
|
|
||||||
showOthersCategory: true,
|
|
||||||
othersCategoryName: 'Others',
|
|
||||||
});
|
|
||||||
mockBuildCategorizedPieces.mockReturnValue({
|
|
||||||
categories: [],
|
|
||||||
allPieces: new Map(),
|
|
||||||
missingPieces: [
|
|
||||||
{ categoryPath: ['Quick Start'], pieceName: 'default', source: 'builtin' },
|
|
||||||
{ categoryPath: ['Custom'], pieceName: 'my-missing', source: 'user' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
mockSelectPieceFromCategorizedPieces.mockResolvedValue(null);
|
|
||||||
|
|
||||||
// When
|
|
||||||
const switched = await switchPiece('/project');
|
const switched = await switchPiece('/project');
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(switched).toBe(false);
|
expect(switched).toBe(false);
|
||||||
expect(mockWarnMissingPieces).toHaveBeenCalledWith([
|
expect(mockSelectPiece).toHaveBeenCalledWith('/project', { fallbackToDefault: false });
|
||||||
{ categoryPath: ['Custom'], pieceName: 'my-missing', source: 'user' },
|
});
|
||||||
]);
|
|
||||||
|
it('should switch to selected piece', async () => {
|
||||||
|
mockSelectPiece.mockResolvedValue('new-piece');
|
||||||
|
mockLoadPiece.mockReturnValue({
|
||||||
|
name: 'new-piece',
|
||||||
|
movements: [],
|
||||||
|
initialMovement: 'start',
|
||||||
|
maxMovements: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const switched = await switchPiece('/project');
|
||||||
|
|
||||||
|
expect(switched).toBe(true);
|
||||||
|
expect(mockSetCurrentPiece).toHaveBeenCalledWith('/project', 'new-piece');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,48 +3,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
listPieceEntries,
|
|
||||||
loadAllPiecesWithSources,
|
|
||||||
getPieceCategories,
|
|
||||||
buildCategorizedPieces,
|
|
||||||
loadPiece,
|
loadPiece,
|
||||||
getCurrentPiece,
|
getCurrentPiece,
|
||||||
setCurrentPiece,
|
setCurrentPiece,
|
||||||
} from '../../infra/config/index.js';
|
} from '../../infra/config/index.js';
|
||||||
import { info, success, error } from '../../shared/ui/index.js';
|
import { info, success, error } from '../../shared/ui/index.js';
|
||||||
import {
|
import { selectPiece } from '../pieceSelection/index.js';
|
||||||
warnMissingPieces,
|
|
||||||
selectPieceFromCategorizedPieces,
|
|
||||||
selectPieceFromEntries,
|
|
||||||
} from '../pieceSelection/index.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch to a different piece
|
* Switch to a different piece
|
||||||
* @returns true if switch was successful
|
* @returns true if switch was successful
|
||||||
*/
|
*/
|
||||||
export async function switchPiece(cwd: string, pieceName?: string): Promise<boolean> {
|
export async function switchPiece(cwd: string, pieceName?: string): Promise<boolean> {
|
||||||
// No piece specified - show selection prompt
|
|
||||||
if (!pieceName) {
|
if (!pieceName) {
|
||||||
const current = getCurrentPiece(cwd);
|
const current = getCurrentPiece(cwd);
|
||||||
info(`Current piece: ${current}`);
|
info(`Current piece: ${current}`);
|
||||||
|
|
||||||
const categoryConfig = getPieceCategories();
|
const selected = await selectPiece(cwd, { fallbackToDefault: false });
|
||||||
let selected: string | null;
|
|
||||||
if (categoryConfig) {
|
|
||||||
const allPieces = loadAllPiecesWithSources(cwd);
|
|
||||||
if (allPieces.size === 0) {
|
|
||||||
info('No pieces found.');
|
|
||||||
selected = null;
|
|
||||||
} else {
|
|
||||||
const categorized = buildCategorizedPieces(allPieces, categoryConfig);
|
|
||||||
warnMissingPieces(categorized.missingPieces.filter((missing) => missing.source === 'user'));
|
|
||||||
selected = await selectPieceFromCategorizedPieces(categorized, current);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const entries = listPieceEntries(cwd);
|
|
||||||
selected = await selectPieceFromEntries(entries, current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
info('Cancelled');
|
info('Cancelled');
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -12,11 +12,18 @@ import {
|
|||||||
} from '../../infra/config/global/index.js';
|
} from '../../infra/config/global/index.js';
|
||||||
import {
|
import {
|
||||||
findPieceCategories,
|
findPieceCategories,
|
||||||
|
listPieces,
|
||||||
|
listPieceEntries,
|
||||||
|
loadAllPiecesWithSources,
|
||||||
|
getPieceCategories,
|
||||||
|
buildCategorizedPieces,
|
||||||
|
getCurrentPiece,
|
||||||
type PieceDirEntry,
|
type PieceDirEntry,
|
||||||
type PieceCategoryNode,
|
type PieceCategoryNode,
|
||||||
type CategorizedPieces,
|
type CategorizedPieces,
|
||||||
type MissingPiece,
|
type MissingPiece,
|
||||||
} from '../../infra/config/index.js';
|
} from '../../infra/config/index.js';
|
||||||
|
import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
|
||||||
|
|
||||||
/** Top-level selection item: either a piece or a category containing pieces */
|
/** Top-level selection item: either a piece or a category containing pieces */
|
||||||
export type PieceSelectionItem =
|
export type PieceSelectionItem =
|
||||||
@ -504,3 +511,44 @@ export async function selectPieceFromEntries(
|
|||||||
const entriesToUse = customEntries.length > 0 ? customEntries : builtinEntries;
|
const entriesToUse = customEntries.length > 0 ? customEntries : builtinEntries;
|
||||||
return selectPieceFromEntriesWithCategories(entriesToUse, currentPiece);
|
return selectPieceFromEntriesWithCategories(entriesToUse, currentPiece);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SelectPieceOptions {
|
||||||
|
fallbackToDefault?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function selectPiece(
|
||||||
|
cwd: string,
|
||||||
|
options?: SelectPieceOptions,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const fallbackToDefault = options?.fallbackToDefault !== false;
|
||||||
|
const categoryConfig = getPieceCategories();
|
||||||
|
const currentPiece = getCurrentPiece(cwd);
|
||||||
|
|
||||||
|
if (categoryConfig) {
|
||||||
|
const allPieces = loadAllPiecesWithSources(cwd);
|
||||||
|
if (allPieces.size === 0) {
|
||||||
|
if (fallbackToDefault) {
|
||||||
|
info(`No pieces found. Using default: ${DEFAULT_PIECE_NAME}`);
|
||||||
|
return DEFAULT_PIECE_NAME;
|
||||||
|
}
|
||||||
|
info('No pieces found.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const categorized = buildCategorizedPieces(allPieces, categoryConfig);
|
||||||
|
warnMissingPieces(categorized.missingPieces.filter((missing) => missing.source === 'user'));
|
||||||
|
return selectPieceFromCategorizedPieces(categorized, currentPiece);
|
||||||
|
}
|
||||||
|
|
||||||
|
const availablePieces = listPieces(cwd);
|
||||||
|
if (availablePieces.length === 0) {
|
||||||
|
if (fallbackToDefault) {
|
||||||
|
info(`No pieces found. Using default: ${DEFAULT_PIECE_NAME}`);
|
||||||
|
return DEFAULT_PIECE_NAME;
|
||||||
|
}
|
||||||
|
info('No pieces found.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = listPieceEntries(cwd);
|
||||||
|
return selectPieceFromEntries(entries, currentPiece);
|
||||||
|
}
|
||||||
|
|||||||
@ -7,13 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCurrentPiece,
|
|
||||||
listPieces,
|
listPieces,
|
||||||
listPieceEntries,
|
|
||||||
isPiecePath,
|
isPiecePath,
|
||||||
loadAllPiecesWithSources,
|
|
||||||
getPieceCategories,
|
|
||||||
buildCategorizedPieces,
|
|
||||||
loadGlobalConfig,
|
loadGlobalConfig,
|
||||||
} from '../../../infra/config/index.js';
|
} from '../../../infra/config/index.js';
|
||||||
import { confirm } from '../../../shared/prompt/index.js';
|
import { confirm } from '../../../shared/prompt/index.js';
|
||||||
@ -24,63 +19,12 @@ import { createLogger } from '../../../shared/utils/index.js';
|
|||||||
import { createPullRequest, buildPrBody, pushBranch } from '../../../infra/github/index.js';
|
import { createPullRequest, buildPrBody, pushBranch } from '../../../infra/github/index.js';
|
||||||
import { executeTask } from './taskExecution.js';
|
import { executeTask } from './taskExecution.js';
|
||||||
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
|
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
|
||||||
import {
|
import { selectPiece } from '../../pieceSelection/index.js';
|
||||||
warnMissingPieces,
|
|
||||||
selectPieceFromCategorizedPieces,
|
|
||||||
selectPieceFromEntries,
|
|
||||||
} from '../../pieceSelection/index.js';
|
|
||||||
|
|
||||||
export type { WorktreeConfirmationResult, SelectAndExecuteOptions };
|
export type { WorktreeConfirmationResult, SelectAndExecuteOptions };
|
||||||
|
|
||||||
const log = createLogger('selectAndExecute');
|
const log = createLogger('selectAndExecute');
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a piece interactively with directory categories and bookmarks.
|
|
||||||
*/
|
|
||||||
async function selectPieceWithDirectoryCategories(cwd: string): Promise<string | null> {
|
|
||||||
const availablePieces = listPieces(cwd);
|
|
||||||
const currentPiece = getCurrentPiece(cwd);
|
|
||||||
|
|
||||||
if (availablePieces.length === 0) {
|
|
||||||
info(`No pieces found. Using default: ${DEFAULT_PIECE_NAME}`);
|
|
||||||
return DEFAULT_PIECE_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (availablePieces.length === 1 && availablePieces[0]) {
|
|
||||||
return availablePieces[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = listPieceEntries(cwd);
|
|
||||||
return selectPieceFromEntries(entries, currentPiece);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a piece interactively with 2-stage category support.
|
|
||||||
*/
|
|
||||||
async function selectPiece(cwd: string): Promise<string | null> {
|
|
||||||
const categoryConfig = getPieceCategories();
|
|
||||||
if (categoryConfig) {
|
|
||||||
const current = getCurrentPiece(cwd);
|
|
||||||
const allPieces = loadAllPiecesWithSources(cwd);
|
|
||||||
if (allPieces.size === 0) {
|
|
||||||
info(`No pieces found. Using default: ${DEFAULT_PIECE_NAME}`);
|
|
||||||
return DEFAULT_PIECE_NAME;
|
|
||||||
}
|
|
||||||
const categorized = buildCategorizedPieces(allPieces, categoryConfig);
|
|
||||||
warnMissingPieces(categorized.missingPieces.filter((missing) => missing.source === 'user'));
|
|
||||||
return selectPieceFromCategorizedPieces(categorized, current);
|
|
||||||
}
|
|
||||||
return selectPieceWithDirectoryCategories(cwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine piece to use.
|
|
||||||
*
|
|
||||||
* - If override looks like a path (isPiecePath), return it directly (validation is done at load time).
|
|
||||||
* - If override is a name, validate it exists in available pieces.
|
|
||||||
* - If no override, prompt user to select interactively.
|
|
||||||
*/
|
|
||||||
export async function determinePiece(cwd: string, override?: string): Promise<string | null> {
|
export async function determinePiece(cwd: string, override?: string): Promise<string | null> {
|
||||||
if (override) {
|
if (override) {
|
||||||
if (isPiecePath(override)) {
|
if (isPiecePath(override)) {
|
||||||
|
|||||||
@ -24,13 +24,11 @@ import { info, success, error as logError, warn, header, blankLine } from '../..
|
|||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import { executeTask } from '../execute/taskExecution.js';
|
import { executeTask } from '../execute/taskExecution.js';
|
||||||
import type { TaskExecutionOptions } from '../execute/types.js';
|
import type { TaskExecutionOptions } from '../execute/types.js';
|
||||||
import { listPieces, getCurrentPiece } from '../../../infra/config/index.js';
|
|
||||||
import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js';
|
|
||||||
import { encodeWorktreePath } from '../../../infra/config/project/sessionStore.js';
|
import { encodeWorktreePath } from '../../../infra/config/project/sessionStore.js';
|
||||||
|
import { selectPiece } from '../../pieceSelection/index.js';
|
||||||
|
|
||||||
const log = createLogger('list-tasks');
|
const log = createLogger('list-tasks');
|
||||||
|
|
||||||
/** Actions available for a listed branch */
|
|
||||||
export type ListAction = 'diff' | 'instruct' | 'try' | 'merge' | 'delete';
|
export type ListAction = 'diff' | 'instruct' | 'try' | 'merge' | 'delete';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -254,29 +252,6 @@ export function deleteBranch(projectDir: string, item: BranchListItem): boolean
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the piece to use for instruction.
|
|
||||||
*/
|
|
||||||
async function selectPieceForInstruction(projectDir: string): Promise<string | null> {
|
|
||||||
const availablePieces = listPieces(projectDir);
|
|
||||||
const currentPiece = getCurrentPiece(projectDir);
|
|
||||||
|
|
||||||
if (availablePieces.length === 0) {
|
|
||||||
return DEFAULT_PIECE_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (availablePieces.length === 1 && availablePieces[0]) {
|
|
||||||
return availablePieces[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = availablePieces.map((name) => ({
|
|
||||||
label: name === currentPiece ? `${name} (current)` : name,
|
|
||||||
value: name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return await selectOption('Select piece:', options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get branch context: diff stat and commit log from main branch.
|
* Get branch context: diff stat and commit log from main branch.
|
||||||
*/
|
*/
|
||||||
@ -343,7 +318,7 @@ export async function instructBranch(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedPiece = await selectPieceForInstruction(projectDir);
|
const selectedPiece = await selectPiece(projectDir);
|
||||||
if (!selectedPiece) {
|
if (!selectedPiece) {
|
||||||
info('Cancelled');
|
info('Cancelled');
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user