diff --git a/.changeset/tricky-states-tease.md b/.changeset/tricky-states-tease.md new file mode 100644 index 00000000..03551af9 --- /dev/null +++ b/.changeset/tricky-states-tease.md @@ -0,0 +1,5 @@ +--- +"@clack/prompts": patch +--- + +Fix `path` directory mode so pressing Enter with an existing directory `initialValue` submits that current directory instead of the first child option, and add regression coverage for immediate submit and child-directory navigation. diff --git a/packages/prompts/src/path.ts b/packages/prompts/src/path.ts index d575ab33..abdf7f26 100644 --- a/packages/prompts/src/path.ts +++ b/packages/prompts/src/path.ts @@ -18,6 +18,7 @@ export const path = (opts: PathOptions) => { ...opts, initialUserInput: opts.initialValue ?? opts.root ?? process.cwd(), maxItems: 5, + filter: () => true, validate(value) { if (Array.isArray(value)) { // Shouldn't ever happen since we don't enable `multiple: true` @@ -44,13 +45,17 @@ export const path = (opts: PathOptions) => { searchPath = dirname(userInput); } else { const stat = lstatSync(userInput); - if (stat.isDirectory()) { + if (stat.isDirectory() && !opts.directory) { searchPath = userInput; } else { searchPath = dirname(userInput); } } + // Strip trailing slash so startsWith matches the directory itself among its siblings + const prefix = + userInput.length > 1 && userInput.endsWith('/') ? userInput.slice(0, -1) : userInput; + const items = readdirSync(searchPath) .map((item) => { const path = join(searchPath, item); @@ -63,8 +68,9 @@ export const path = (opts: PathOptions) => { }) .filter( ({ path, isDirectory }) => - path.startsWith(userInput) && (isDirectory || !opts.directory) + path.startsWith(prefix) && (isDirectory || !opts.directory) ); + return items.map((item) => ({ value: item.path, })); diff --git a/packages/prompts/test/path.test.ts b/packages/prompts/test/path.test.ts index 24f7d76d..90c4ef41 100644 --- a/packages/prompts/test/path.test.ts +++ b/packages/prompts/test/path.test.ts @@ -163,6 +163,41 @@ describe.each(['true', 'false'])('text (isCI = %s)', (isCI) => { expect(value).toBe('/tmp/foo'); }); + test('directory mode submits initial directory value on enter', async () => { + const result = prompts.path({ + message: 'foo', + root: '/tmp/', + initialValue: '/tmp/', + directory: true, + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('/tmp'); + }); + + test('directory mode can navigate from initial directory to child directory', async () => { + const result = prompts.path({ + message: 'foo', + root: '/tmp/', + initialValue: '/tmp/', + directory: true, + input, + output, + }); + + input.emit('keypress', 'f', { name: 'f' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('/tmp/foo'); + }); + test('default mode allows selecting files', async () => { const result = prompts.path({ message: 'foo',