@seamapi/cli 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/bin/cli.js +7 -4
  2. package/bin/cli.js.map +1 -1
  3. package/lib/interact-for-action-attempt-poll.js +3 -5
  4. package/lib/interact-for-action-attempt-poll.js.map +1 -1
  5. package/lib/interact-for-array.js +11 -18
  6. package/lib/interact-for-array.js.map +1 -1
  7. package/lib/interact-for-blueprint-object.js +19 -31
  8. package/lib/interact-for-blueprint-object.js.map +1 -1
  9. package/lib/interact-for-command-selection.js +6 -11
  10. package/lib/interact-for-command-selection.js.map +1 -1
  11. package/lib/interact-for-custom-metadata.js +10 -21
  12. package/lib/interact-for-custom-metadata.js.map +1 -1
  13. package/lib/interact-for-login.js +3 -5
  14. package/lib/interact-for-login.js.map +1 -1
  15. package/lib/interact-for-resource.js +6 -6
  16. package/lib/interact-for-resource.js.map +1 -1
  17. package/lib/interact-for-server-selection.js +9 -17
  18. package/lib/interact-for-server-selection.js.map +1 -1
  19. package/lib/interact-for-timestamp.d.ts +1 -1
  20. package/lib/interact-for-timestamp.js +14 -5
  21. package/lib/interact-for-timestamp.js.map +1 -1
  22. package/lib/interact-for-use-remote-api-defs.js +14 -18
  23. package/lib/interact-for-use-remote-api-defs.js.map +1 -1
  24. package/lib/interact-for-workspace-id.d.ts +1 -1
  25. package/lib/interact-for-workspace-id.js +8 -13
  26. package/lib/interact-for-workspace-id.js.map +1 -1
  27. package/lib/util/prompt.d.ts +42 -13
  28. package/lib/util/prompt.js +79 -28
  29. package/lib/util/prompt.js.map +1 -1
  30. package/lib/version.d.ts +1 -1
  31. package/lib/version.js +1 -1
  32. package/package.json +2 -3
  33. package/src/bin/cli.ts +12 -4
  34. package/src/lib/interact-for-action-attempt-poll.ts +3 -5
  35. package/src/lib/interact-for-array.ts +11 -19
  36. package/src/lib/interact-for-blueprint-object.ts +35 -46
  37. package/src/lib/interact-for-command-selection.ts +6 -12
  38. package/src/lib/interact-for-custom-metadata.ts +10 -22
  39. package/src/lib/interact-for-login.ts +3 -5
  40. package/src/lib/interact-for-resource.ts +6 -7
  41. package/src/lib/interact-for-server-selection.ts +10 -18
  42. package/src/lib/interact-for-timestamp.ts +14 -5
  43. package/src/lib/interact-for-use-remote-api-defs.ts +14 -18
  44. package/src/lib/interact-for-workspace-id.ts +8 -14
  45. package/src/lib/util/prompt.ts +138 -38
  46. package/src/lib/version.ts +1 -1
@@ -14,7 +14,14 @@ import { getOutput } from './output/get-output.js'
14
14
  import type { ContextHelpers } from './types.js'
15
15
  import { NonInteractiveError, toArgName } from './util/cli-args.js'
16
16
  import { ellipsis } from './util/ellipsis.js'
17
- import { prompt } from './util/prompt.js'
17
+ import {
18
+ promptAutocomplete,
19
+ promptAutocompleteMultiselect,
20
+ promptConfirm,
21
+ promptNumber,
22
+ promptSelect,
23
+ promptText,
24
+ } from './util/prompt.js'
18
25
 
19
26
  const ergonomicPropOrder = [
20
27
  'name',
@@ -84,23 +91,21 @@ export const interactForBlueprintObject = async (
84
91
  : `[${cmdPath}] Parameters`
85
92
 
86
93
  getOutput().info()
87
- const { paramToEdit } = await prompt({
88
- name: 'paramToEdit',
94
+ const paramToEdit = await promptAutocomplete({
89
95
  message: parameterSelectionMessage,
90
- type: 'autocomplete',
91
96
  choices: [
92
97
  ...(haveAllRequiredParams && !args.isSubProperty
93
98
  ? [
94
99
  {
95
100
  value: 'done',
96
- title: `[Make API Call] ${cmdPath}`,
101
+ label: `[Make API Call] ${cmdPath}`,
97
102
  },
98
103
  ]
99
104
  : []),
100
105
  ...(haveAllRequiredParams && args.isSubProperty
101
106
  ? [
102
107
  {
103
- title: `[Save]`,
108
+ label: `[Save]`,
104
109
  value: 'done',
105
110
  },
106
111
  ]
@@ -108,9 +113,9 @@ export const interactForBlueprintObject = async (
108
113
  ...Object.keys(properties)
109
114
  .map((k) => {
110
115
  return {
111
- title: k + (required.includes(k) ? '*' : ''),
116
+ label: k + (required.includes(k) ? '*' : ''),
112
117
  value: k,
113
- description:
118
+ hint:
114
119
  args.params[k] !== undefined
115
120
  ? typeof args.params[k] === 'object'
116
121
  ? ellipsis(JSON.stringify(args.params[k]), 60)
@@ -122,13 +127,13 @@ export const interactForBlueprintObject = async (
122
127
  ...(args.isSubProperty
123
128
  ? [
124
129
  {
125
- title: `[Leave Empty]`,
130
+ label: `[Leave Empty]`,
126
131
  value: 'empty',
127
132
  },
128
133
  ]
129
134
  : []),
130
135
  {
131
- title: `[Back]`,
136
+ label: `[Back]`,
132
137
  value: 'back',
133
138
  },
134
139
  ],
@@ -201,36 +206,26 @@ export const interactForBlueprintObject = async (
201
206
  if (prop.format === 'datetime') {
202
207
  value = await interactForTimestamp()
203
208
  } else {
204
- value = (
205
- await prompt({
206
- name: 'value',
207
- message: `${paramToEdit}:`,
208
- type: 'text',
209
- })
210
- ).value
209
+ value = await promptText({
210
+ message: `${paramToEdit}:`,
211
+ })
211
212
  }
212
213
  args.params[paramToEdit] = value
213
214
  return interactForBlueprintObject(args, ctx)
214
215
  } else if (prop.format === 'enum') {
215
- const value = (
216
- await prompt({
217
- name: 'value',
218
- message: `${paramToEdit}:`,
219
- type: 'select',
220
- choices: prop.values.map((v) => ({
221
- title: v.name,
222
- value: v.name,
223
- })),
224
- })
225
- ).value
216
+ const value = await promptSelect({
217
+ message: `${paramToEdit}:`,
218
+ choices: prop.values.map((v) => ({
219
+ label: v.name,
220
+ value: v.name,
221
+ })),
222
+ })
226
223
  args.params[paramToEdit] = value
227
224
  return interactForBlueprintObject(args, ctx)
228
225
  } else if (prop.format === 'boolean') {
229
- const { value } = await prompt({
230
- name: 'value',
226
+ const value = await promptConfirm({
231
227
  message: `${paramToEdit}:`,
232
- type: 'toggle',
233
- initial: true,
228
+ initialValue: true,
234
229
  active: 'true',
235
230
  inactive: 'false',
236
231
  })
@@ -239,17 +234,13 @@ export const interactForBlueprintObject = async (
239
234
 
240
235
  return interactForBlueprintObject(args, ctx)
241
236
  } else if (prop.format === 'list' && prop.itemFormat === 'enum') {
242
- const value = (
243
- await prompt({
244
- name: 'value',
245
- message: `${paramToEdit}:`,
246
- type: 'autocompleteMultiselect',
247
- choices: prop.itemEnumValues.map((v) => ({
248
- title: v.name,
249
- value: v.name,
250
- })),
251
- })
252
- ).value
237
+ const value = await promptAutocompleteMultiselect({
238
+ message: `${paramToEdit}:`,
239
+ choices: prop.itemEnumValues.map((v) => ({
240
+ label: v.name,
241
+ value: v.name,
242
+ })),
243
+ })
253
244
  args.params[paramToEdit] = value
254
245
  return interactForBlueprintObject(args, ctx)
255
246
  } else if (prop.format === 'list') {
@@ -271,10 +262,8 @@ export const interactForBlueprintObject = async (
271
262
  )
272
263
  return interactForBlueprintObject(args, ctx)
273
264
  } else if (prop.format === 'number') {
274
- const { value } = await prompt({
275
- name: 'value',
265
+ const value = await promptNumber({
276
266
  message: `${paramToEdit}:`,
277
- type: 'number',
278
267
  })
279
268
 
280
269
  args.params[paramToEdit] = value
@@ -2,7 +2,7 @@ import { isDeepStrictEqual as isEqual } from 'node:util'
2
2
 
3
3
  import type { ContextHelpers } from './types.js'
4
4
  import { NonInteractiveError } from './util/cli-args.js'
5
- import { prompt } from './util/prompt.js'
5
+ import { promptAutocomplete } from './util/prompt.js'
6
6
 
7
7
  const uniqBy = <T>(items: T[], keyOf: (item: T) => unknown): T[] => {
8
8
  const seen = new Set<unknown>()
@@ -92,28 +92,22 @@ export async function interactForCommandSelection(
92
92
 
93
93
  const commandPathStr = commandPath.join('/').replace(/-/g, '_')
94
94
 
95
- const res = await prompt({
96
- name: 'Command',
97
- type: 'autocomplete',
95
+ const selectedCommand = await promptAutocomplete({
96
+ message: `Select a command: /${commandPathStr}`,
98
97
  choices: [
99
98
  ...possibleCommands.map((cmd) => ({
100
- title:
99
+ label:
101
100
  cmd?.[commandPath.length] ?? `[Call /${commandPathStr} Directly]`,
102
101
  value: cmd?.[commandPath.length] ?? '<none>',
103
102
  })),
104
103
  ].sort((a, b) => ergonomicSort(a.value, b.value)),
105
- message: `Select a command: /${commandPathStr}`,
106
104
  })
107
105
 
108
- if (res?.Command === undefined) {
109
- throw new Error('Bailed')
110
- }
111
-
112
- if (res?.Command === '<none>') {
106
+ if (selectedCommand === '<none>') {
113
107
  return commandPath
114
108
  }
115
109
 
116
- const newCommandPath = [...commandPath, res.Command]
110
+ const newCommandPath = [...commandPath, selectedCommand]
117
111
 
118
112
  const fullCommand = possibleCommands.find((cmd) =>
119
113
  isEqual(newCommandPath, cmd),
@@ -1,5 +1,5 @@
1
1
  import { getOutput } from './output/get-output.js'
2
- import { prompt } from './util/prompt.js'
2
+ import { promptSelect, promptText } from './util/prompt.js'
3
3
 
4
4
  // Structurally the CustomMetadata of @seamapi/types, spelled out here so the
5
5
  // published declarations do not depend on a development-only package.
@@ -31,29 +31,21 @@ export const interactForCustomMetadata = async (
31
31
  do {
32
32
  displayCurrentCustomMetadata()
33
33
 
34
- const response = await prompt({
35
- type: 'select',
36
- name: 'action',
34
+ action = await promptSelect({
37
35
  message: 'Choose an action:',
38
36
  choices: [
39
- { title: 'Add an item to params', value: 'add' },
40
- { title: 'Remove an item from params', value: 'remove' },
41
- { title: 'Finish editing params', value: 'done' },
37
+ { label: 'Add an item to params', value: 'add' },
38
+ { label: 'Remove an item from params', value: 'remove' },
39
+ { label: 'Finish editing params', value: 'done' },
42
40
  ],
43
41
  })
44
42
 
45
- action = response.action
46
-
47
43
  if (action === 'add') {
48
- const { newKey } = await prompt({
49
- type: 'text',
50
- name: 'newKey',
44
+ const newKey = await promptText({
51
45
  message: 'Enter a key to add or edit:',
52
46
  })
53
47
 
54
- let { newValue } = await prompt({
55
- type: 'text',
56
- name: 'newValue',
48
+ let newValue: string | boolean = await promptText({
57
49
  message: 'Enter the new value to add or edit (or null to delete):',
58
50
  })
59
51
  if (newKey) {
@@ -67,21 +59,17 @@ export const interactForCustomMetadata = async (
67
59
  }
68
60
  }
69
61
  } else if (action === 'remove') {
70
- const { customKeyToRemove } = await prompt({
71
- type: 'select',
72
- name: 'customKeyToRemove',
62
+ const customKeyToRemove = await promptSelect({
73
63
  message: 'Choose a key-value pair to remove from params:',
74
64
  choices: Object.keys(updatedCustomMetadata).map((customMetadataKey) => {
75
65
  return {
76
- title: `${customMetadataKey}: ${updatedCustomMetadata[customMetadataKey]}`,
66
+ label: `${customMetadataKey}: ${updatedCustomMetadata[customMetadataKey]}`,
77
67
  value: customMetadataKey,
78
68
  }
79
69
  }),
80
70
  })
81
71
 
82
- if (customKeyToRemove) {
83
- delete customMetadata[customKeyToRemove]
84
- }
72
+ delete customMetadata[customKeyToRemove]
85
73
  }
86
74
  } while (action !== 'done')
87
75
 
@@ -6,7 +6,7 @@ import { assertEnvVarUnset, getTokenFromEnv, tokenEnvVar } from './env.js'
6
6
  import { getServer } from './get-server.js'
7
7
  import { interactForWorkspaceId } from './interact-for-workspace-id.js'
8
8
  import { getOutput } from './output/get-output.js'
9
- import { prompt } from './util/prompt.js'
9
+ import { promptText } from './util/prompt.js'
10
10
  import { withLoading } from './util/with-loading.js'
11
11
  import { validateToken } from './validate-token.js'
12
12
 
@@ -32,12 +32,10 @@ export const interactForLogin = async () => {
32
32
  ),
33
33
  )
34
34
 
35
- const { pat } = await prompt({
36
- name: 'pat',
37
- type: 'text',
35
+ const pat = await promptText({
38
36
  message: 'Personal Access Token:',
39
37
  })
40
- const token = pat?.trim()
38
+ const token = pat.trim()
41
39
 
42
40
  if (!token) {
43
41
  throw new Error('No token provided')
@@ -1,4 +1,4 @@
1
- import { prompt } from './util/prompt.js'
1
+ import { promptAutocomplete } from './util/prompt.js'
2
2
  import { withLoading } from './util/with-loading.js'
3
3
 
4
4
  export interface ResourceChoice {
@@ -22,12 +22,11 @@ export const interactForResource = async <Resource>({
22
22
  `Fetching ${resourceName.replace(/_/g, ' ')}s...`,
23
23
  fetchResources,
24
24
  )
25
- const { resourceId } = await prompt({
26
- name: 'resourceId',
27
- type: 'autocomplete',
25
+ return await promptAutocomplete({
28
26
  message,
29
- choices: resources.map(toChoice),
27
+ choices: resources.map((resource) => {
28
+ const { title, value, description } = toChoice(resource)
29
+ return { label: title, value, hint: description }
30
+ }),
30
31
  })
31
-
32
- return resourceId as string
33
32
  }
@@ -10,7 +10,7 @@ import {
10
10
  } from './env.js'
11
11
  import { getServer } from './get-server.js'
12
12
  import { getOutput } from './output/get-output.js'
13
- import { prompt } from './util/prompt.js'
13
+ import { promptAutocomplete, promptText } from './util/prompt.js'
14
14
 
15
15
  export async function interactForServerSelection() {
16
16
  assertEnvVarUnset(endpointEnvVar, getEndpointFromEnv(), 'select a server')
@@ -21,27 +21,19 @@ export async function interactForServerSelection() {
21
21
  'https://fakeseamconnect.seam.vc',
22
22
  ]
23
23
 
24
- const { server } = await prompt([
25
- {
26
- // Searchable, as selecting a device or a command is.
27
- type: 'autocomplete',
28
- name: 'server',
29
- message: 'Select a server:',
30
- choices: servers.map((server) => ({ title: server, value: server })),
31
- },
32
- ])
24
+ // Searchable, as selecting a device or a command is.
25
+ const server = await promptAutocomplete({
26
+ message: 'Select a server:',
27
+ choices: servers.map((server) => ({ label: server, value: server })),
28
+ })
33
29
 
34
30
  const config = getConfigStore()
35
31
  const output = getOutput()
36
32
  if (server === servers[2]) {
37
- let { userUrlSeed } = await prompt([
38
- {
39
- type: 'text',
40
- name: 'userUrlSeed',
41
- message:
42
- 'You can input a custom server URL or leave this field empty to use a new fakeserver.',
43
- },
44
- ])
33
+ let userUrlSeed = await promptText({
34
+ message:
35
+ 'You can input a custom server URL or leave this field empty to use a new fakeserver.',
36
+ })
45
37
 
46
38
  if (userUrlSeed.trim().length === 0) {
47
39
  userUrlSeed = randomBytes(5).toString('hex')
@@ -1,10 +1,19 @@
1
- import { prompt } from './util/prompt.js'
1
+ import { promptText } from './util/prompt.js'
2
+
2
3
  export const interactForTimestamp = async () => {
3
- const { timestamp } = await prompt({
4
- name: 'timestamp',
5
- type: 'date',
4
+ const now = new Date().toISOString()
5
+ const timestamp = await promptText({
6
6
  message: 'Enter a timestamp:',
7
+ placeholder: now,
8
+ defaultValue: now,
9
+ validate: (value) => {
10
+ if (value == null || value === '') return undefined
11
+ if (Number.isNaN(new Date(value).getTime())) {
12
+ return `Enter a valid timestamp, e.g. ${now}`
13
+ }
14
+ return undefined
15
+ },
7
16
  })
8
17
 
9
- return timestamp.toISOString()
18
+ return new Date(timestamp).toISOString()
10
19
  }
@@ -1,25 +1,21 @@
1
1
  import { getConfigStore } from './config/index.js'
2
2
  import { getOutput } from './output/get-output.js'
3
- import { prompt } from './util/prompt.js'
3
+ import { promptSelect } from './util/prompt.js'
4
4
 
5
5
  export async function interactForUseRemoteApiDefs() {
6
- const { useRemoteApiDefs } = await prompt([
7
- {
8
- type: 'select',
9
- name: 'useRemoteApiDefs',
10
- message: 'Always use remote API Definitions?',
11
- choices: [
12
- {
13
- title: 'Yes',
14
- value: true,
15
- },
16
- {
17
- title: 'No',
18
- value: false,
19
- },
20
- ],
21
- },
22
- ])
6
+ const useRemoteApiDefs = await promptSelect({
7
+ message: 'Always use remote API Definitions?',
8
+ choices: [
9
+ {
10
+ label: 'Yes',
11
+ value: true,
12
+ },
13
+ {
14
+ label: 'No',
15
+ value: false,
16
+ },
17
+ ],
18
+ })
23
19
 
24
20
  const config = getConfigStore()
25
21
  config.set('use_remote_api_defs', useRemoteApiDefs)
@@ -8,7 +8,7 @@ import {
8
8
  } from './env.js'
9
9
  import { getSeamMultiWorkspace } from './get-seam.js'
10
10
  import { getServer } from './get-server.js'
11
- import { prompt } from './util/prompt.js'
11
+ import { promptAutocomplete } from './util/prompt.js'
12
12
  import { withLoading } from './util/with-loading.js'
13
13
 
14
14
  export const interactForWorkspaceId = async (personalAccessToken?: string) => {
@@ -29,23 +29,17 @@ export const interactForWorkspaceId = async (personalAccessToken?: string) => {
29
29
  const workspaces = await withLoading('Fetching workspaces...', () =>
30
30
  seam.workspaces.list(),
31
31
  )
32
- const { workspaceId } = await prompt({
33
- name: 'workspaceId',
34
- // Searchable, as selecting a device or a command is: an account may have
35
- // more workspaces than fit on a screen.
36
- type: 'autocomplete',
32
+ // Searchable, as selecting a device or a command is: an account may have
33
+ // more workspaces than fit on a screen.
34
+ const workspaceId = await promptAutocomplete<string>({
37
35
  message: 'Select a workspace:',
38
36
  choices: workspaces.map((workspace: any) => ({
39
- title: workspace.name,
37
+ label: workspace.name,
40
38
  value: workspace.workspace_id,
41
- description: workspace.workspace_id,
39
+ hint: workspace.workspace_id,
42
40
  })),
43
41
  })
44
42
 
45
- if (workspaceId) {
46
- config.set('current_workspace_id', workspaceId)
47
- return workspaceId
48
- }
49
-
50
- throw new Error('Bailed')
43
+ config.set('current_workspace_id', workspaceId)
44
+ return workspaceId
51
45
  }
@@ -1,4 +1,12 @@
1
- import prompts, { type Answers, type Options, type PromptObject } from 'prompts'
1
+ import {
2
+ autocomplete,
3
+ autocompleteMultiselect,
4
+ confirm,
5
+ isCancel,
6
+ type Option,
7
+ select,
8
+ text,
9
+ } from '@clack/prompts'
2
10
 
3
11
  import { NonInteractiveError } from './cli-args.js'
4
12
 
@@ -13,62 +21,154 @@ import { NonInteractiveError } from './cli-args.js'
13
21
  export const canPrompt = (): boolean =>
14
22
  process.stdin.isTTY === true && process.stderr.isTTY === true
15
23
 
16
- /**
17
- * Ask the user a question.
18
- *
19
- * Prompts are rendered to stderr: a selection is not a command result,
20
- * so it must not end up in stdout when the CLI is piped.
21
- */
22
- export const prompt = async <T extends string = string>(
23
- questions: PromptObject<T> | Array<PromptObject<T>>,
24
- options?: Options,
25
- ): Promise<Answers<T>> => {
24
+ /** The user dismissed a prompt with ctrl-c or escape instead of answering. */
25
+ export class PromptCancelledError extends Error {
26
+ constructor() {
27
+ super('Cancelled')
28
+ }
29
+ }
30
+
31
+ export interface PromptChoice<Value> {
32
+ label: string
33
+ value: Value
34
+ hint?: string | undefined
35
+ }
36
+
37
+ const ensureInteractive = (): void => {
26
38
  if (!canPrompt()) {
27
39
  throw new NonInteractiveError(
28
40
  'Cannot prompt without a terminal: pass the missing arguments, or pipe them in as JSON',
29
41
  )
30
42
  }
43
+ }
44
+
45
+ const unwrap = <Value>(value: Value | symbol): Value => {
46
+ if (isCancel(value)) throw new PromptCancelledError()
47
+ return value as Value
48
+ }
49
+
50
+ // Prompts are rendered to stderr: a selection is not a command result,
51
+ // so it must not end up in stdout when the CLI is piped.
52
+ const output = process.stderr
53
+
54
+ const toOptions = <Value>(
55
+ choices: Array<PromptChoice<Value>>,
56
+ ): Array<Option<Value>> =>
57
+ choices.map(
58
+ ({ label, value, hint }) =>
59
+ (hint === undefined
60
+ ? { label, value }
61
+ : { label, value, hint }) as Option<Value>,
62
+ )
31
63
 
32
- const questionList = Array.isArray(questions) ? questions : [questions]
33
-
34
- return await prompts(
35
- questionList.map((question) => ({
36
- ...question,
37
- // Search a list by any part of a name, rather than only by what it
38
- // starts with, which is all prompts does for itself.
39
- ...(question.type === 'autocomplete' && question.suggest == null
40
- ? { suggest: searchChoices }
41
- : {}),
42
- stdout: process.stderr,
43
- })),
44
- options,
64
+ export const promptText = async (options: {
65
+ message: string
66
+ placeholder?: string
67
+ defaultValue?: string
68
+ validate?: (value: string | undefined) => string | undefined
69
+ }): Promise<string> => {
70
+ ensureInteractive()
71
+ return unwrap(await text({ ...options, output }))
72
+ }
73
+
74
+ export const promptNumber = async (options: {
75
+ message: string
76
+ validate?: (value: number) => string | undefined
77
+ }): Promise<number> => {
78
+ ensureInteractive()
79
+ const value = unwrap(
80
+ await text({
81
+ message: options.message,
82
+ validate: (value) => {
83
+ if (value == null || value.trim() === '') return 'Enter a number'
84
+ const parsed = Number(value)
85
+ if (Number.isNaN(parsed)) return 'Enter a number'
86
+ return options.validate?.(parsed)
87
+ },
88
+ output,
89
+ }),
90
+ )
91
+ return Number(value)
92
+ }
93
+
94
+ export const promptConfirm = async (options: {
95
+ message: string
96
+ initialValue?: boolean
97
+ active?: string
98
+ inactive?: string
99
+ }): Promise<boolean> => {
100
+ ensureInteractive()
101
+ return unwrap(await confirm({ ...options, output }))
102
+ }
103
+
104
+ export const promptSelect = async <Value>(options: {
105
+ message: string
106
+ choices: Array<PromptChoice<Value>>
107
+ }): Promise<Value> => {
108
+ ensureInteractive()
109
+ return unwrap(
110
+ await select<Value>({
111
+ message: options.message,
112
+ options: toOptions(options.choices),
113
+ output,
114
+ }),
115
+ )
116
+ }
117
+
118
+ export const promptAutocomplete = async <Value>(options: {
119
+ message: string
120
+ choices: Array<PromptChoice<Value>>
121
+ }): Promise<Value> => {
122
+ ensureInteractive()
123
+ return unwrap(
124
+ await autocomplete<Value>({
125
+ message: options.message,
126
+ options: toOptions(options.choices),
127
+ // Search a list by any part of a name or hint, rather than only by
128
+ // the label, which is all clack matches for itself.
129
+ filter: searchChoices,
130
+ output,
131
+ }),
132
+ )
133
+ }
134
+
135
+ export const promptAutocompleteMultiselect = async <Value>(options: {
136
+ message: string
137
+ choices: Array<PromptChoice<Value>>
138
+ }): Promise<Value[]> => {
139
+ ensureInteractive()
140
+ return unwrap(
141
+ await autocompleteMultiselect<Value>({
142
+ message: options.message,
143
+ options: toOptions(options.choices),
144
+ filter: searchChoices,
145
+ output,
146
+ }),
45
147
  )
46
148
  }
47
149
 
48
150
  export interface SearchableChoice {
49
- title?: string | undefined
50
- description?: string | undefined
151
+ label?: string | undefined
152
+ hint?: string | undefined
51
153
  }
52
154
 
53
155
  /**
54
- * Filter choices by every whitespace separated term of the input, matched
55
- * case insensitively against the title and the description.
156
+ * Match a choice by every whitespace separated term of the input, matched
157
+ * case insensitively against the label and the hint.
56
158
  */
57
- export const searchChoices = async <Choice extends SearchableChoice>(
159
+ export const searchChoices = (
58
160
  input: string,
59
- choices: Choice[],
60
- ): Promise<Choice[]> => {
161
+ choice: SearchableChoice,
162
+ ): boolean => {
61
163
  const terms = input
62
164
  .toLowerCase()
63
165
  .split(/\s+/)
64
166
  .filter((term) => term.length > 0)
65
167
 
66
- if (terms.length === 0) return choices
168
+ if (terms.length === 0) return true
67
169
 
68
- return choices.filter((choice) => {
69
- const searchable = `${choice.title ?? ''} ${choice.description ?? ''}`
70
- .toLowerCase()
71
- .trim()
72
- return terms.every((term) => searchable.includes(term))
73
- })
170
+ const searchable = `${choice.label ?? ''} ${choice.hint ?? ''}`
171
+ .toLowerCase()
172
+ .trim()
173
+ return terms.every((term) => searchable.includes(term))
74
174
  }