fix(cli): replace duplicate directory prompt with direct path options

Extract confirmProjectDir helper and split the "Create a new project
folder" option into "New subfolder (./ktx-project)" and "Custom path"
so users reach their target directory with fewer prompts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Luca Martial 2026-05-12 16:59:30 -07:00
parent 17a2fee69a
commit bdca6d0f04
2 changed files with 123 additions and 93 deletions

View file

@ -140,10 +140,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' }),
],
}),
@ -156,7 +157,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(
@ -168,21 +169,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();
@ -194,7 +190,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 },
@ -217,7 +213,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 },
@ -235,7 +231,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',
});
@ -248,7 +244,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: [
@ -259,15 +255,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');
});
@ -292,7 +288,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 },
@ -303,7 +299,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([