diff --git a/.changeset/big-pants-invite.md b/.changeset/big-pants-invite.md new file mode 100644 index 00000000..43f13e6b --- /dev/null +++ b/.changeset/big-pants-invite.md @@ -0,0 +1,5 @@ +--- +"@clack/prompts": patch +--- + +Fix the `path` prompt so `directory: true` correctly enforces directory-only selection while still allowing directory navigation, and add regression tests for both directory and default file selection behavior. diff --git a/packages/prompts/src/path.ts b/packages/prompts/src/path.ts index b630c7cc..d575ab33 100644 --- a/packages/prompts/src/path.ts +++ b/packages/prompts/src/path.ts @@ -63,7 +63,7 @@ export const path = (opts: PathOptions) => { }) .filter( ({ path, isDirectory }) => - path.startsWith(userInput) && (opts.directory || !isDirectory) + path.startsWith(userInput) && (isDirectory || !opts.directory) ); return items.map((item) => ({ value: item.path, diff --git a/packages/prompts/test/__snapshots__/path.test.ts.snap b/packages/prompts/test/__snapshots__/path.test.ts.snap index 9d81f118..0153e166 100644 --- a/packages/prompts/test/__snapshots__/path.test.ts.snap +++ b/packages/prompts/test/__snapshots__/path.test.ts.snap @@ -8,10 +8,12 @@ exports[`text (isCI = false) > can cancel 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "■ foo @@ -30,10 +32,12 @@ exports[`text (isCI = false) > cannot submit unknown value 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/_█ @@ -57,10 +61,12 @@ exports[`text (isCI = false) > cannot submit unknown value 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/b█ @@ -107,10 +113,12 @@ exports[`text (isCI = false) > renders cancelled value if one set 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/x█ @@ -141,10 +149,12 @@ exports[`text (isCI = false) > renders message 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "◇ foo @@ -163,10 +173,12 @@ exports[`text (isCI = false) > renders submitted value 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/b█ @@ -197,10 +209,12 @@ exports[`text (isCI = false) > validation errors render and clear (using Error) │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/r█ @@ -224,10 +238,12 @@ exports[`text (isCI = false) > validation errors render and clear (using Error) │ │ Search: /tmp/█ │ ○ /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ● /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/b█ @@ -253,10 +269,12 @@ exports[`text (isCI = false) > validation errors render and clear 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/r█ @@ -280,10 +298,12 @@ exports[`text (isCI = false) > validation errors render and clear 1`] = ` │ │ Search: /tmp/█ │ ○ /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ● /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/b█ @@ -309,10 +329,12 @@ exports[`text (isCI = true) > can cancel 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "■ foo @@ -331,10 +353,12 @@ exports[`text (isCI = true) > cannot submit unknown value 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/_█ @@ -358,10 +382,12 @@ exports[`text (isCI = true) > cannot submit unknown value 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/b█ @@ -408,10 +434,12 @@ exports[`text (isCI = true) > renders cancelled value if one set 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/x█ @@ -442,10 +470,12 @@ exports[`text (isCI = true) > renders message 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "◇ foo @@ -464,10 +494,12 @@ exports[`text (isCI = true) > renders submitted value 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/b█ @@ -498,10 +530,12 @@ exports[`text (isCI = true) > validation errors render and clear (using Error) 1 │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/r█ @@ -525,10 +559,12 @@ exports[`text (isCI = true) > validation errors render and clear (using Error) 1 │ │ Search: /tmp/█ │ ○ /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ● /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/b█ @@ -554,10 +590,12 @@ exports[`text (isCI = true) > validation errors render and clear 1`] = ` │ │ Search: /tmp/█ │ ● /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ○ /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/r█ @@ -581,10 +619,12 @@ exports[`text (isCI = true) > validation errors render and clear 1`] = ` │ │ Search: /tmp/█ │ ○ /tmp/bar +│ ○ /tmp/foo +│ ○ /tmp/hello │ ● /tmp/root.zip │ ↑/↓ to select • Enter: confirm • Type: to search └", - "", + "", "", "", "│ Search: /tmp/b█ diff --git a/packages/prompts/test/path.test.ts b/packages/prompts/test/path.test.ts index 0e15ec34..24f7d76d 100644 --- a/packages/prompts/test/path.test.ts +++ b/packages/prompts/test/path.test.ts @@ -146,6 +146,42 @@ describe.each(['true', 'false'])('text (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); + test('directory mode only allows selecting directories', async () => { + const result = prompts.path({ + message: 'foo', + root: '/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', + root: '/tmp/', + input, + output, + }); + + input.emit('keypress', 'r', { name: 'r' }); + input.emit('keypress', 'o', { name: 'o' }); + input.emit('keypress', 'o', { name: 'o' }); + input.emit('keypress', 't', { name: 't' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('/tmp/root.zip'); + }); + test('validation errors render and clear', async () => { const result = prompts.path({ message: 'foo',