Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 53 additions & 35 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
}
},
"dependencies": {
"@clack/prompts": "^1.7.0",
"@seamapi/blueprint": "1.2.0",
"@seamapi/http": "2.2.0",
"@seamapi/wizard": "0.5.2",
Expand All @@ -102,15 +103,13 @@
"minimist": "^1.2.8",
"nanospinner": "^1.2.2",
"open": "^11.0.0",
"prompts": "^2.4.2",
"tar": "^7.5.22"
},
"devDependencies": {
"@seamapi/types": "1.985.0",
"@types/command-line-usage": "^5.0.4",
"@types/minimist": "^1.2.5",
"@types/node": "^24.10.9",
"@types/prompts": "^2.4.9",
"@vitest/coverage-v8": "^4.1.10",
"concurrently": "^10.0.4",
"del-cli": "^7.0.0",
Expand Down
16 changes: 12 additions & 4 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ import {
toParameterName,
UsageError,
} from 'lib/util/cli-args.js'
import { canPrompt, prompt } from 'lib/util/prompt.js'
import {
canPrompt,
PromptCancelledError,
promptConfirm,
} from 'lib/util/prompt.js'
import { readStdinJson } from 'lib/util/read-stdin-json.js'
import { RequestSeamApi } from 'lib/util/request-seam-api.js'
import { validateToken } from 'lib/validate-token.js'
Expand Down Expand Up @@ -406,10 +410,9 @@ const handleConnectWebviewResponse = async (
interactivity !== 'non-interactive' &&
process.env['INSIDE_WEB_BROWSER'] !== '1'
) {
const { action } = await prompt({
type: 'confirm',
name: 'action',
const action = await promptConfirm({
message: 'Would you like to open the webview in your browser?',
initialValue: false,
})

if (action) {
Expand Down Expand Up @@ -458,6 +461,11 @@ run(process.argv.slice(2)).catch((e: unknown) => {
return
}

if (e instanceof PromptCancelledError) {
output.error(chalk.gray(e.message))
return
}

const error = e instanceof Error ? e : new Error(String(e))
output.error(chalk.red(`CLI Error: ${error.message}`))
if (error.stack != null) output.error(chalk.gray(error.stack))
Expand Down
8 changes: 3 additions & 5 deletions src/lib/interact-for-action-attempt-poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ import type { ActionAttemptsGetResponse } from '@seamapi/http/connect'

import { getSeam } from './get-seam.js'
import { getOutput } from './output/get-output.js'
import { prompt } from './util/prompt.js'
import { promptConfirm } from './util/prompt.js'
import { withLoading } from './util/with-loading.js'

export const interactForActionAttemptPoll = async (
actionAttempt: ActionAttemptsGetResponse['action_attempt'],
) => {
if (actionAttempt.status === 'pending') {
const { pollForActionAttempt } = await prompt({
name: 'pollForActionAttempt',
const pollForActionAttempt = await promptConfirm({
message: "Would you like to poll the action attempt until it's ready?",
type: 'toggle',
initial: true,
initialValue: true,
active: 'yes',
inactive: 'no',
})
Expand Down
30 changes: 11 additions & 19 deletions src/lib/interact-for-array.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getOutput } from './output/get-output.js'
import { prompt } from './util/prompt.js'
import { promptNumber, promptSelect, promptText } from './util/prompt.js'

export const interactForArray = async (
array: string[],
Expand All @@ -23,39 +23,31 @@ export const interactForArray = async (
do {
displayList()

const response = await prompt({
type: 'select',
name: 'action',
action = await promptSelect({
message: 'Choose an action:',
choices: [
{ title: 'Add an item', value: 'add' },
{ title: 'Remove an item', value: 'remove' },
{ title: 'Finish editing', value: 'done' },
{ label: 'Add an item', value: 'add' },
{ label: 'Remove an item', value: 'remove' },
{ label: 'Finish editing', value: 'done' },
],
})

action = response.action

if (action === 'add') {
const { newItem } = await prompt({
type: 'text',
name: 'newItem',
const newItem = await promptText({
message: 'Enter the new item:',
})
if (newItem) {
updatedArray.push(newItem)
}
} else if (action === 'remove') {
const { index } = await prompt({
type: 'number',
name: 'index',
const index = await promptNumber({
message: 'Enter the index of the item to remove:',
validate: (value) =>
value > 0 && value <= updatedArray.length ? true : 'Invalid index',
value > 0 && value <= updatedArray.length
? undefined
: 'Invalid index',
})
if (index) {
updatedArray.splice(index - 1, 1)
}
updatedArray.splice(index - 1, 1)
}
} while (action !== 'done')

Expand Down
26 changes: 16 additions & 10 deletions src/lib/interact-for-blueprint-object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import { interactForBlueprintObject } from './interact-for-blueprint-object.js'
import { createMemoryOutput } from './output/create-memory-output.js'
import { setOutput } from './output/get-output.js'
import type { ContextHelpers } from './types.js'
import { prompt } from './util/prompt.js'
import { promptAutocomplete } from './util/prompt.js'

vi.mock('./util/prompt.js', () => ({
canPrompt: vi.fn(() => true),
prompt: vi.fn(async () => ({ paramToEdit: 'done' })),
PromptCancelledError: class extends Error {},
promptText: vi.fn(),
promptNumber: vi.fn(),
promptConfirm: vi.fn(),
promptSelect: vi.fn(),
promptAutocomplete: vi.fn(async () => 'done'),
promptAutocompleteMultiselect: vi.fn(),
}))

beforeEach(() => {
vi.mocked(prompt).mockClear()
vi.mocked(promptAutocomplete).mockClear()
// Keep the interactive chrome out of the test output.
setOutput(createMemoryOutput().output)
})
Expand All @@ -36,7 +42,7 @@ test('interactForBlueprintObject: submits without prompting once every required
await expect(
interactForBlueprintObject(args({ device_id: 'device1' }), ctx('auto')),
).resolves.toEqual({ device_id: 'device1' })
expect(prompt).not.toHaveBeenCalled()
expect(promptAutocomplete).not.toHaveBeenCalled()
})

test('interactForBlueprintObject: prompts to review given parameters when interactive', async () => {
Expand All @@ -46,7 +52,7 @@ test('interactForBlueprintObject: prompts to review given parameters when intera
ctx('interactive'),
),
).resolves.toEqual({ device_id: 'device1' })
expect(prompt).toHaveBeenCalledTimes(1)
expect(promptAutocomplete).toHaveBeenCalledTimes(1)
})

test('interactForBlueprintObject: prefills the prompt with the given parameters', async () => {
Expand All @@ -55,11 +61,11 @@ test('interactForBlueprintObject: prefills the prompt with the given parameters'
ctx('interactive'),
)

const { choices } = vi.mocked(prompt).mock.calls[0]?.[0] as {
choices: Array<{ value: string; description?: string }>
const { choices } = vi.mocked(promptAutocomplete).mock.calls[0]?.[0] as {
choices: Array<{ value: string; hint?: string }>
}
expect(choices.find(({ value }) => value === 'device_id')).toMatchObject({
description: '[device1]',
hint: '[device1]',
})
})

Expand All @@ -70,7 +76,7 @@ test('interactForBlueprintObject: submits without prompting when non-interactive
ctx('non-interactive'),
),
).resolves.toEqual({ device_id: 'device1' })
expect(prompt).not.toHaveBeenCalled()
expect(promptAutocomplete).not.toHaveBeenCalled()
})

test('interactForBlueprintObject: rejects missing required parameters when non-interactive', async () => {
Expand All @@ -82,5 +88,5 @@ test('interactForBlueprintObject: rejects missing required parameters when non-i
).rejects.toThrowError(
'Missing required parameter for /devices/get: --device-id',
)
expect(prompt).not.toHaveBeenCalled()
expect(promptAutocomplete).not.toHaveBeenCalled()
})
Loading
Loading