Skip to content

Commit c9d4361

Browse files
committed
refactor: Make -y the short flag for --non-interactive
-y differed from --non-interactive only by prompting instead of failing when something was missing, which is not worth a separate flag. Collapse them: -y is now simply the short flag for --non-interactive, and its --yes long form is dropped. This leaves -n unused so it can become --dry-run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TxQCkfVypobQVA7DpF4jvx
1 parent 131b45d commit c9d4361

9 files changed

Lines changed: 49 additions & 101 deletions

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ A command line interface (CLI) for interacting with the Seam API.
1111

1212
Every command is interactive: the CLI prompts for any missing required
1313
parameter with suggestions pulled from your workspace. Pass `--non-interactive`
14-
(or `-n`) to never be prompted: the command fails instead.
14+
(or `-y`) to never be prompted: the command fails instead.
1515

1616
## Installation
1717

@@ -35,15 +35,11 @@ $ paru -S seam-bin
3535
Every `seam` command is interactive and will prompt you for any missing
3636
required properties with helpful suggestions.
3737

38-
For scripts and CI, pass `--non-interactive` (or `-n`) to never be prompted.
38+
For scripts and CI, pass `--non-interactive` (or `-y`) to never be prompted.
3939
The command must then be complete: if the command itself is ambiguous, or any
4040
required property is missing, the CLI exits with an error naming what is
4141
missing instead of asking for it.
4242

43-
Pass `--yes` (or `-y`) to only skip the prompt to review properties before the
44-
API call is made. Unlike `--non-interactive`, anything still missing is
45-
prompted for.
46-
4743
```bash
4844
# Login to Seam
4945
seam login

src/bin/cli.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ import { interactForUseRemoteApiDefs } from 'lib/interact-for-use-remote-api-def
1919
import { interactForWorkspaceId } from 'lib/interact-for-workspace-id.js'
2020
import type { ContextHelpers } from 'lib/types.js'
2121
import {
22-
getInteractivity,
23-
type Interactivity,
24-
interactivityFlags,
22+
isInteractive,
2523
NonInteractiveError,
24+
nonInteractiveFlags,
2625
parseCliArgs,
2726
} from 'lib/util/cli-args.js'
2827
import { RequestSeamApi } from 'lib/util/request-seam-api.js'
@@ -33,7 +32,7 @@ const sections = [
3332
{
3433
header: 'Seam CLI',
3534
content:
36-
'Every seam command is interactive and will prompt you for any missing required properties with helpful suggestions. To never be prompted, pass -n ',
35+
'Every seam command is interactive and will prompt you for any missing required properties with helpful suggestions. To never be prompted, pass -y ',
3736
},
3837
{
3938
header: 'Options',
@@ -48,13 +47,6 @@ const sections = [
4847
name: 'non-interactive',
4948
description:
5049
'Never prompt: exit with an error if the command or any required property is missing.',
51-
alias: 'n',
52-
type: Boolean,
53-
},
54-
{
55-
name: 'yes',
56-
description:
57-
'Do not prompt to review properties when every required property was already given.',
5850
alias: 'y',
5951
type: Boolean,
6052
},
@@ -144,7 +136,7 @@ async function cli(args: ParsedArgs) {
144136

145137
const ctx: ContextHelpers = {
146138
blueprint,
147-
interactivity: getInteractivity(args),
139+
is_interactive: isInteractive(args),
148140
}
149141

150142
for (const k in args) {
@@ -153,7 +145,7 @@ async function cli(args: ParsedArgs) {
153145
delete args[k]
154146
const key = k.replace(/-/g, '_')
155147
args[key] = v
156-
if (interactivityFlags.includes(key)) continue
148+
if (nonInteractiveFlags.includes(key)) continue
157149
commandParams[key] = v
158150
}
159151

@@ -256,28 +248,22 @@ async function cli(args: ParsedArgs) {
256248
if (response.data?.connect_webview) {
257249
await handleConnectWebviewResponse(
258250
response.data.connect_webview,
259-
ctx.interactivity,
251+
ctx.is_interactive,
260252
)
261253
}
262254

263-
if (
264-
response.data?.action_attempt &&
265-
ctx.interactivity !== 'non-interactive'
266-
) {
255+
if (response.data?.action_attempt && ctx.is_interactive) {
267256
interactForActionAttemptPoll(response.data.action_attempt)
268257
}
269258
}
270259

271260
const handleConnectWebviewResponse = async (
272261
connect_webview: any,
273-
interactivity: Interactivity,
262+
is_interactive: boolean,
274263
) => {
275264
const url = connect_webview.url
276265

277-
if (
278-
interactivity !== 'non-interactive' &&
279-
process.env['INSIDE_WEB_BROWSER'] !== '1'
280-
) {
266+
if (is_interactive && process.env['INSIDE_WEB_BROWSER'] !== '1') {
281267
const { action } = await prompts({
282268
type: 'confirm',
283269
name: 'action',

src/lib/interact-for-blueprint-object.test.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ const parameters = [
99
{ name: 'name', isRequired: false, format: 'string' },
1010
] as unknown as Parameter[]
1111

12-
const ctx = (interactivity: ContextHelpers['interactivity']): ContextHelpers =>
13-
({ interactivity, blueprint: {} }) as unknown as ContextHelpers
12+
const nonInteractiveCtx = {
13+
is_interactive: false,
14+
blueprint: {},
15+
} as unknown as ContextHelpers
1416

1517
const args = (params: Record<string, any>) => ({
1618
command: ['devices', 'get'],
@@ -22,26 +24,14 @@ test('interactForBlueprintObject: submits given parameters when non-interactive'
2224
await expect(
2325
interactForBlueprintObject(
2426
args({ device_id: 'device1' }),
25-
ctx('non-interactive'),
26-
),
27-
).resolves.toEqual({ device_id: 'device1' })
28-
})
29-
30-
test('interactForBlueprintObject: submits given parameters with -y', async () => {
31-
await expect(
32-
interactForBlueprintObject(
33-
args({ device_id: 'device1' }),
34-
ctx('auto-submit'),
27+
nonInteractiveCtx,
3528
),
3629
).resolves.toEqual({ device_id: 'device1' })
3730
})
3831

3932
test('interactForBlueprintObject: rejects missing required parameters when non-interactive', async () => {
4033
await expect(
41-
interactForBlueprintObject(
42-
args({ name: 'Front Door' }),
43-
ctx('non-interactive'),
44-
),
34+
interactForBlueprintObject(args({ name: 'Front Door' }), nonInteractiveCtx),
4535
).rejects.toThrowError(
4636
'Missing required parameter for /devices/get: --device-id',
4737
)

src/lib/interact-for-blueprint-object.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,12 @@ export const interactForBlueprintObject = async (
5050

5151
const cmdPath = `/${args.command.join('/').replace(/-/g, '_')}`
5252

53-
const should_auto_submit =
54-
ctx.interactivity !== 'interactive' &&
55-
haveAllRequiredParams &&
56-
!args.isSubProperty
57-
if (should_auto_submit) {
58-
return args.params
59-
}
53+
if (!ctx.is_interactive) {
54+
const should_auto_submit = haveAllRequiredParams && !args.isSubProperty
55+
if (should_auto_submit) {
56+
return args.params
57+
}
6058

61-
if (ctx.interactivity === 'non-interactive') {
6259
const missing = required.filter((k) => !args.params[k])
6360
const target = args.isSubProperty ? `"${args.subPropertyPath}"` : cmdPath
6461
throw new NonInteractiveError(

src/lib/interact-for-command-selection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { interactForCommandSelection } from './interact-for-command-selection.js
44
import type { ContextHelpers } from './types.js'
55

66
const ctx = {
7-
interactivity: 'non-interactive',
7+
is_interactive: false,
88
blueprint: {
99
routes: [
1010
{

src/lib/interact-for-command-selection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export async function interactForCommandSelection(
6565
return commandPath
6666
}
6767

68-
if (helpers.interactivity === 'non-interactive') {
68+
if (!helpers.is_interactive) {
6969
// The command path is itself a command, so call it directly rather than
7070
// prompting to select one of its sub-commands.
7171
if (possibleCommands.some((cmd) => cmd.length === commandPath.length)) {

src/lib/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { ApiBlueprint } from './get-api-blueprint.js'
2-
import type { Interactivity } from './util/cli-args.js'
32

43
export interface ContextHelpers {
54
blueprint: ApiBlueprint
6-
interactivity: Interactivity
5+
is_interactive: boolean
76
}

src/lib/util/cli-args.test.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ParsedArgs } from 'minimist'
22
import { expect, test } from 'vitest'
33

4-
import { getInteractivity, parseCliArgs, toArgName } from './cli-args.js'
4+
import { isInteractive, parseCliArgs, toArgName } from './cli-args.js'
55

66
// The CLI normalizes argument keys before checking them.
77
const parse = (argv: string[]): ParsedArgs => {
@@ -13,34 +13,23 @@ const parse = (argv: string[]): ParsedArgs => {
1313
return args
1414
}
1515

16-
test('getInteractivity: interactive by default', () => {
17-
expect(getInteractivity(parse(['devices', 'list']))).toBe('interactive')
16+
test('isInteractive: interactive by default', () => {
17+
expect(isInteractive(parse(['devices', 'list']))).toBe(true)
1818
})
1919

20-
test('getInteractivity: --non-interactive and -n never prompt', () => {
21-
expect(
22-
getInteractivity(parse(['devices', 'list', '--non-interactive'])),
23-
).toBe('non-interactive')
24-
expect(getInteractivity(parse(['devices', 'list', '-n']))).toBe(
25-
'non-interactive',
20+
test('isInteractive: --non-interactive and -y never prompt', () => {
21+
expect(isInteractive(parse(['devices', 'list', '--non-interactive']))).toBe(
22+
false,
2623
)
24+
expect(isInteractive(parse(['devices', 'list', '-y']))).toBe(false)
2725
})
2826

29-
test('getInteractivity: --yes and -y only skip the parameter prompt', () => {
30-
expect(getInteractivity(parse(['devices', 'list', '--yes']))).toBe(
31-
'auto-submit',
32-
)
33-
expect(getInteractivity(parse(['devices', 'list', '-y']))).toBe('auto-submit')
34-
})
35-
36-
test('getInteractivity: --non-interactive wins over -y', () => {
37-
expect(getInteractivity(parse(['devices', 'list', '-y', '-n']))).toBe(
38-
'non-interactive',
39-
)
27+
test('isInteractive: -n is reserved and does not affect interactivity', () => {
28+
expect(isInteractive(parse(['devices', 'list', '-n']))).toBe(true)
4029
})
4130

4231
test('parseCliArgs: --non-interactive does not consume the next argument', () => {
43-
const args = parse(['devices', 'get', '-n', '--device-id', 'foo'])
32+
const args = parse(['devices', 'get', '-y', '--device-id', 'foo'])
4433
expect(args['device_id']).toBe('foo')
4534
expect(args._).toEqual(['devices', 'get'])
4635
})

src/lib/util/cli-args.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
11
import parseArgs, { type ParsedArgs } from 'minimist'
22

33
/**
4-
* How the CLI should behave when it needs input that was not given as an
5-
* argument.
6-
*
7-
* - `interactive`: prompt for it. This is the default.
8-
* - `auto-submit`: skip the parameter prompt when everything required
9-
* was already given, otherwise prompt. Selected with `--yes` or `-y`.
10-
* - `non-interactive`: never prompt: missing input is an error.
11-
* Selected with `--non-interactive` or `-n`.
12-
*/
13-
export type Interactivity = 'interactive' | 'auto-submit' | 'non-interactive'
14-
15-
/**
16-
* Argument keys that affect interactivity
4+
* Argument keys that disable interactive prompts
175
* and are therefore not command parameters.
186
*/
19-
export const interactivityFlags = ['non_interactive', 'n', 'yes', 'y']
7+
export const nonInteractiveFlags = ['non_interactive', 'y']
208

219
/**
2210
* Thrown when the CLI needs input it cannot prompt for.
@@ -28,17 +16,20 @@ export class NonInteractiveError extends Error {
2816
export const parseCliArgs = (argv: string[]): ParsedArgs =>
2917
parseArgs(argv, {
3018
string: ['code'],
31-
boolean: ['non-interactive', 'yes'],
32-
alias: { 'non-interactive': 'n', yes: 'y' },
19+
boolean: ['non-interactive'],
20+
// Deliberately not aliased to -n, which is reserved for a future
21+
// --dry-run flag.
22+
alias: { 'non-interactive': 'y' },
3323
})
3424

35-
export const getInteractivity = (args: ParsedArgs): Interactivity => {
36-
if (args['non_interactive'] === true || args['n'] === true) {
37-
return 'non-interactive'
38-
}
39-
if (args['yes'] === true || args['y'] === true) return 'auto-submit'
40-
return 'interactive'
41-
}
25+
/**
26+
* Whether or not the CLI may prompt for input.
27+
*
28+
* When false, the command must be given in full:
29+
* anything missing is an error instead of a prompt.
30+
*/
31+
export const isInteractive = (args: ParsedArgs): boolean =>
32+
!nonInteractiveFlags.some((flag) => args[flag] === true)
4233

4334
/**
4435
* Render a parameter name as the argument used to set it,

0 commit comments

Comments
 (0)