mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
Merge pull request #48 from Kaelio/fix-duplicate-directory-prompt
fix(cli): replace duplicate directory prompt with direct path options
This commit is contained in:
commit
e7dbf91b96
2 changed files with 123 additions and 93 deletions
|
|
@ -142,10 +142,11 @@ describe('setup project step', () => {
|
|||
expect(result.projectDir).toBe(projectDir);
|
||||
expect(prompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Which KTX project should setup use?',
|
||||
message: 'Where should KTX create the project?',
|
||||
options: [
|
||||
expect.objectContaining({ value: 'current', label: 'Use current directory' }),
|
||||
expect.objectContaining({ value: 'new', label: 'Create a new project folder' }),
|
||||
expect.objectContaining({ value: 'current', label: 'Current directory' }),
|
||||
expect.objectContaining({ value: 'new-default', label: 'New subfolder (./ktx-project)' }),
|
||||
expect.objectContaining({ value: 'new-custom', label: 'Custom path' }),
|
||||
expect.objectContaining({ value: 'exit', label: 'Exit' }),
|
||||
],
|
||||
}),
|
||||
|
|
@ -159,7 +160,7 @@ describe('setup project step', () => {
|
|||
it('offers an absolute default destination for a new project folder', async () => {
|
||||
const startDir = join(tempDir, 'start');
|
||||
const projectDir = join(startDir, 'ktx-project');
|
||||
const prompts = makePromptAdapter({ choices: ['new', 'default', 'create'] });
|
||||
const prompts = makePromptAdapter({ choices: ['new-default', 'create'] });
|
||||
const testIo = makeIo({ stdoutIsTty: true });
|
||||
|
||||
const result = await runKtxSetupProjectStep(
|
||||
|
|
@ -171,21 +172,16 @@ describe('setup project step', () => {
|
|||
expect(result.status).toBe('ready');
|
||||
expect(result.projectDir).toBe(projectDir);
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
1,
|
||||
expect.objectContaining({
|
||||
message: 'Where should KTX create the project?',
|
||||
options: [
|
||||
expect.objectContaining({
|
||||
value: 'default',
|
||||
label: `Create the default project folder: ${projectDir}`,
|
||||
}),
|
||||
expect.objectContaining({ value: 'custom', label: 'Enter a custom path' }),
|
||||
expect.objectContaining({ value: 'back', label: 'Back' }),
|
||||
],
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({ value: 'new-default', label: 'New subfolder (./ktx-project)' }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
2,
|
||||
expect.objectContaining({ message: `Create KTX project at ${projectDir}?` }),
|
||||
);
|
||||
expect(prompts.text).not.toHaveBeenCalled();
|
||||
|
|
@ -197,7 +193,7 @@ describe('setup project step', () => {
|
|||
it('prompts for a custom path and resolves it inside the current setup directory', async () => {
|
||||
const startDir = join(tempDir, 'start');
|
||||
const projectDir = join(startDir, 'analytics-ktx');
|
||||
const prompts = makePromptAdapter({ choices: ['new', 'custom', 'create'], textValue: 'analytics-ktx' });
|
||||
const prompts = makePromptAdapter({ choices: ['new-custom', 'create'], textValue: 'analytics-ktx' });
|
||||
|
||||
const result = await runKtxSetupProjectStep(
|
||||
{ projectDir: startDir, mode: 'auto', inputMode: 'auto', yes: false },
|
||||
|
|
@ -220,7 +216,7 @@ describe('setup project step', () => {
|
|||
const startDir = join(tempDir, 'start');
|
||||
const homeDir = join(tempDir, 'home');
|
||||
const projectDir = join(homeDir, 'analytics-ktx');
|
||||
const prompts = makePromptAdapter({ choices: ['new', 'custom', 'create'], textValue: '~/analytics-ktx' });
|
||||
const prompts = makePromptAdapter({ choices: ['new-custom', 'create'], textValue: '~/analytics-ktx' });
|
||||
|
||||
const result = await runKtxSetupProjectStep(
|
||||
{ projectDir: startDir, mode: 'auto', inputMode: 'auto', yes: false },
|
||||
|
|
@ -238,7 +234,7 @@ describe('setup project step', () => {
|
|||
const homeDir = join(tempDir, 'home');
|
||||
const customProjectDir = join(homeDir, 'analytics-ktx');
|
||||
const prompts = makePromptAdapter({
|
||||
choices: ['new', 'custom', 'back', 'exit'],
|
||||
choices: ['new-custom', 'back', 'exit'],
|
||||
textValue: '~/analytics-ktx',
|
||||
});
|
||||
|
||||
|
|
@ -251,7 +247,7 @@ describe('setup project step', () => {
|
|||
expect(result.status).toBe('cancelled');
|
||||
expect(result.projectDir).toBe(startDir);
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
2,
|
||||
expect.objectContaining({
|
||||
message: `Create KTX project at ${customProjectDir}?`,
|
||||
options: [
|
||||
|
|
@ -262,15 +258,15 @@ describe('setup project step', () => {
|
|||
}),
|
||||
);
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
expect.objectContaining({ message: 'Which KTX project should setup use?' }),
|
||||
3,
|
||||
expect.objectContaining({ message: 'Where should KTX create the project?' }),
|
||||
);
|
||||
await expect(stat(join(customProjectDir, 'ktx.yaml'))).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('rejects an empty new folder path without creating a project in the process cwd', async () => {
|
||||
const startDir = join(tempDir, 'start');
|
||||
const prompts = makePromptAdapter({ choices: ['new', 'custom'], textValue: ' ' });
|
||||
const prompts = makePromptAdapter({ choices: ['new-custom'], textValue: ' ' });
|
||||
const initProject = vi.fn(async () => {
|
||||
throw new Error('initProject should not run for an empty path');
|
||||
});
|
||||
|
|
@ -295,7 +291,7 @@ describe('setup project step', () => {
|
|||
const projectDir = join(startDir, 'analytics-ktx');
|
||||
await mkdir(projectDir, { recursive: true });
|
||||
await writeFile(join(projectDir, 'README.md'), 'Existing project notes\n', 'utf-8');
|
||||
const prompts = makePromptAdapter({ choices: ['new', 'custom', 'use-existing'], textValue: 'analytics-ktx' });
|
||||
const prompts = makePromptAdapter({ choices: ['new-custom', 'use-existing'], textValue: 'analytics-ktx' });
|
||||
|
||||
const result = await runKtxSetupProjectStep(
|
||||
{ projectDir: startDir, mode: 'auto', inputMode: 'auto', yes: false },
|
||||
|
|
@ -306,7 +302,7 @@ describe('setup project step', () => {
|
|||
expect(result.status).toBe('ready');
|
||||
expect(result.projectDir).toBe(projectDir);
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
2,
|
||||
expect.objectContaining({
|
||||
message: `That folder already exists and is not empty: ${projectDir}`,
|
||||
options: expect.arrayContaining([
|
||||
|
|
|
|||
|
|
@ -113,6 +113,55 @@ async function existingFolderState(
|
|||
}
|
||||
}
|
||||
|
||||
type ConfirmProjectDirResult =
|
||||
| { status: 'confirmed'; confirmedCreation: boolean }
|
||||
| { status: 'choose-another' }
|
||||
| { status: 'back' }
|
||||
| { status: 'cancelled' }
|
||||
| { status: 'not-directory' };
|
||||
|
||||
async function confirmProjectDir(
|
||||
selectedDir: string,
|
||||
io: KtxCliIo,
|
||||
prompts: KtxSetupProjectPromptAdapter,
|
||||
): Promise<ConfirmProjectDirResult> {
|
||||
const state = await existingFolderState(selectedDir);
|
||||
|
||||
if (state === 'not-directory') {
|
||||
io.stderr.write(`Project folder path exists and is not a directory: ${selectedDir}\n`);
|
||||
return { status: 'not-directory' };
|
||||
}
|
||||
|
||||
if (state === 'non-empty-directory') {
|
||||
const action = await prompts.select({
|
||||
message: `That folder already exists and is not empty: ${selectedDir}`,
|
||||
options: [
|
||||
{ value: 'use-existing', label: 'Yes, create KTX files there' },
|
||||
{ value: 'choose-another', label: 'Choose another folder' },
|
||||
{ value: 'back', label: 'Back' },
|
||||
],
|
||||
});
|
||||
if (action === 'choose-another') return { status: 'choose-another' };
|
||||
if (action === 'back') return { status: 'back' };
|
||||
if (action !== 'use-existing') return { status: 'cancelled' };
|
||||
return { status: 'confirmed', confirmedCreation: true };
|
||||
}
|
||||
|
||||
io.stdout.write(`│ KTX will create:\n│ ${selectedDir}\n`);
|
||||
const action = await prompts.select({
|
||||
message: `Create KTX project at ${selectedDir}?`,
|
||||
options: [
|
||||
{ value: 'create', label: 'Create project' },
|
||||
{ value: 'choose-another', label: 'Choose another folder' },
|
||||
{ value: 'back', label: 'Back' },
|
||||
],
|
||||
});
|
||||
if (action === 'choose-another') return { status: 'choose-another' };
|
||||
if (action === 'back') return { status: 'back' };
|
||||
if (action !== 'create') return { status: 'cancelled' };
|
||||
return { status: 'confirmed', confirmedCreation: true };
|
||||
}
|
||||
|
||||
async function normalizeSetupGitignore(projectDir: string): Promise<void> {
|
||||
const gitignorePath = join(projectDir, '.ktx/.gitignore');
|
||||
await mkdir(join(projectDir, '.ktx'), { recursive: true });
|
||||
|
|
@ -193,55 +242,12 @@ async function promptForNewProjectDir(
|
|||
return { status: 'cancelled', projectDir };
|
||||
}
|
||||
|
||||
const state = await existingFolderState(selectedDir);
|
||||
let confirmedCreation = false;
|
||||
if (state === 'not-directory') {
|
||||
io.stderr.write(`Project folder path exists and is not a directory: ${selectedDir}\n`);
|
||||
return { status: 'missing-input', projectDir };
|
||||
}
|
||||
if (state === 'non-empty-directory') {
|
||||
const existingAction = await prompts.select({
|
||||
message: `That folder already exists and is not empty: ${selectedDir}`,
|
||||
options: [
|
||||
{ value: 'use-existing', label: 'Yes, create KTX files there' },
|
||||
{ value: 'choose-another', label: 'Choose another folder' },
|
||||
{ value: 'back', label: 'Back' },
|
||||
],
|
||||
});
|
||||
if (existingAction === 'choose-another') {
|
||||
continue;
|
||||
}
|
||||
if (existingAction === 'back') {
|
||||
return { status: 'back', projectDir };
|
||||
}
|
||||
if (existingAction !== 'use-existing') {
|
||||
return { status: 'cancelled', projectDir };
|
||||
}
|
||||
confirmedCreation = true;
|
||||
}
|
||||
|
||||
io.stdout.write(`│ KTX will create:\n│ ${selectedDir}\n`);
|
||||
if (state !== 'non-empty-directory') {
|
||||
const createAction = await prompts.select({
|
||||
message: `Create KTX project at ${selectedDir}?`,
|
||||
options: [
|
||||
{ value: 'create', label: 'Create project' },
|
||||
{ value: 'choose-another', label: 'Choose another folder' },
|
||||
{ value: 'back', label: 'Back' },
|
||||
],
|
||||
});
|
||||
if (createAction === 'choose-another') {
|
||||
continue;
|
||||
}
|
||||
if (createAction === 'back') {
|
||||
return { status: 'back', projectDir };
|
||||
}
|
||||
if (createAction !== 'create') {
|
||||
return { status: 'cancelled', projectDir };
|
||||
}
|
||||
confirmedCreation = true;
|
||||
}
|
||||
return { status: 'selected', projectDir: selectedDir, confirmedCreation };
|
||||
const confirmed = await confirmProjectDir(selectedDir, io, prompts);
|
||||
if (confirmed.status === 'not-directory') return { status: 'missing-input', projectDir };
|
||||
if (confirmed.status === 'choose-another') continue;
|
||||
if (confirmed.status === 'back') return { status: 'back', projectDir };
|
||||
if (confirmed.status === 'cancelled') return { status: 'cancelled', projectDir };
|
||||
return { status: 'selected', projectDir: selectedDir, confirmedCreation: confirmed.confirmedCreation };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -323,15 +329,17 @@ export async function runKtxSetupProjectStep(
|
|||
}
|
||||
|
||||
const prompts = deps.prompts ?? createClackSetupProjectPromptAdapter();
|
||||
const defaultProjectDir = join(projectDir, DEFAULT_NEW_PROJECT_FOLDER_NAME);
|
||||
io.stdout.write(
|
||||
'│ Use Up/Down to move, Enter to confirm the current selection, choose Back to return to the previous step, Ctrl+C to exit.\n',
|
||||
);
|
||||
while (true) {
|
||||
const choice = await prompts.select({
|
||||
message: 'Which KTX project should setup use?',
|
||||
message: 'Where should KTX create the project?',
|
||||
options: [
|
||||
{ value: 'current', label: 'Use current directory' },
|
||||
{ value: 'new', label: 'Create a new project folder' },
|
||||
{ value: 'current', label: 'Current directory' },
|
||||
{ value: 'new-default', label: 'New subfolder (./ktx-project)' },
|
||||
{ value: 'new-custom', label: 'Custom path' },
|
||||
...(args.allowBack ? [{ value: 'back', label: 'Back' }] : []),
|
||||
...(args.allowBack ? [] : [{ value: 'exit', label: 'Exit' }]),
|
||||
],
|
||||
|
|
@ -346,27 +354,53 @@ export async function runKtxSetupProjectStep(
|
|||
return { status: 'cancelled', projectDir };
|
||||
}
|
||||
|
||||
let selectedDir = projectDir;
|
||||
let confirmedCreation = false;
|
||||
if (choice === 'new') {
|
||||
const selected = await promptForNewProjectDir(projectDir, homeDir, io, prompts);
|
||||
if (selected.status === 'back') {
|
||||
continue;
|
||||
}
|
||||
if (selected.status !== 'selected') {
|
||||
return selected;
|
||||
}
|
||||
selectedDir = selected.projectDir;
|
||||
confirmedCreation = selected.confirmedCreation;
|
||||
if (choice === 'current') {
|
||||
const project = await createProject(projectDir, deps);
|
||||
printProjectSummary(io, projectDir);
|
||||
return { status: 'ready', projectDir, project };
|
||||
}
|
||||
|
||||
if (choice !== 'current' && choice !== 'new') {
|
||||
prompts.cancel('Setup cancelled.');
|
||||
return { status: 'cancelled', projectDir };
|
||||
if (choice === 'new-default') {
|
||||
const confirmed = await confirmProjectDir(defaultProjectDir, io, prompts);
|
||||
if (confirmed.status === 'choose-another' || confirmed.status === 'back') continue;
|
||||
if (confirmed.status === 'not-directory') return { status: 'missing-input', projectDir };
|
||||
if (confirmed.status === 'cancelled') return { status: 'cancelled', projectDir };
|
||||
const project = await createProject(defaultProjectDir, deps);
|
||||
printProjectSummary(io, defaultProjectDir);
|
||||
return {
|
||||
status: 'ready',
|
||||
projectDir: defaultProjectDir,
|
||||
project,
|
||||
confirmedCreation: confirmed.confirmedCreation,
|
||||
};
|
||||
}
|
||||
|
||||
const project = await createProject(selectedDir, deps);
|
||||
printProjectSummary(io, selectedDir);
|
||||
return { status: 'ready', projectDir: selectedDir, project, confirmedCreation };
|
||||
if (choice === 'new-custom') {
|
||||
io.stdout.write(`│ Relative paths are resolved from:\n│ ${projectDir}\n`);
|
||||
io.stdout.write(`│ Home paths are resolved from:\n│ ${homeDir}\n`);
|
||||
const rawPath = await prompts.text({
|
||||
message: withTextInputNavigation('Project folder path'),
|
||||
placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx',
|
||||
});
|
||||
if (rawPath === undefined) continue;
|
||||
const trimmed = rawPath.trim();
|
||||
if (trimmed.length === 0) {
|
||||
io.stderr.write(
|
||||
'Enter a relative path like ./analytics-ktx, a home path like ~/analytics-ktx, or an absolute path.\n',
|
||||
);
|
||||
return { status: 'missing-input', projectDir };
|
||||
}
|
||||
const customDir = resolveFromProjectDir(projectDir, trimmed, homeDir);
|
||||
const confirmed = await confirmProjectDir(customDir, io, prompts);
|
||||
if (confirmed.status === 'choose-another' || confirmed.status === 'back') continue;
|
||||
if (confirmed.status === 'not-directory') return { status: 'missing-input', projectDir };
|
||||
if (confirmed.status === 'cancelled') return { status: 'cancelled', projectDir };
|
||||
const project = await createProject(customDir, deps);
|
||||
printProjectSummary(io, customDir);
|
||||
return { status: 'ready', projectDir: customDir, project, confirmedCreation: confirmed.confirmedCreation };
|
||||
}
|
||||
|
||||
prompts.cancel('Setup cancelled.');
|
||||
return { status: 'cancelled', projectDir };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue