@seamapi/cli 0.8.0 → 0.10.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.
@@ -5,15 +5,19 @@ import { getServer } from './get-server.js'
5
5
 
6
6
  export type ApiBlueprint = Blueprint
7
7
 
8
+ export interface GetApiBlueprintOptions {
9
+ update?: boolean
10
+ }
11
+
8
12
  export const getApiBlueprint = async (
9
13
  useRemoteDefinitions: boolean,
14
+ options: GetApiBlueprintOptions = {},
10
15
  ): Promise<ApiBlueprint> => {
11
16
  // Remote definitions describe whatever the server is currently running, so
12
- // build them directly from the server's OpenAPI document. This runtime path
13
- // does not load @seamapi/types.
17
+ // build them directly from the server's OpenAPI document.
14
18
  if (useRemoteDefinitions) return await createRemoteBlueprint()
15
19
 
16
- return await getBlueprint()
20
+ return await getBlueprint(options)
17
21
  }
18
22
 
19
23
  const createRemoteBlueprint = async (): Promise<Blueprint> => {
@@ -12,6 +12,7 @@ import { interactForDevice } from './interact-for-device.js'
12
12
  import { interactForTimestamp } from './interact-for-timestamp.js'
13
13
  import { interactForUserIdentity } from './interact-for-user-identity.js'
14
14
  import type { ContextHelpers } from './types.js'
15
+ import { NonInteractiveError, toArgName } from './util/cli-args.js'
15
16
  import { ellipsis } from './util/ellipsis.js'
16
17
 
17
18
  const ergonomicPropOrder = [
@@ -47,12 +48,28 @@ export const interactForBlueprintObject = async (
47
48
 
48
49
  const haveAllRequiredParams = required.every((k) => args.params[k])
49
50
 
51
+ const cmdPath = `/${args.command.join('/').replace(/-/g, '_')}`
52
+
50
53
  const should_auto_submit =
51
- !ctx.is_interactive && haveAllRequiredParams && !args.isSubProperty
54
+ ctx.interactivity !== 'interactive' &&
55
+ haveAllRequiredParams &&
56
+ !args.isSubProperty
52
57
  if (should_auto_submit) {
53
58
  return args.params
54
59
  }
55
60
 
61
+ if (ctx.interactivity === 'non-interactive') {
62
+ const missing = required.filter((k) => !args.params[k])
63
+ const target = args.isSubProperty ? `"${args.subPropertyPath}"` : cmdPath
64
+ throw new NonInteractiveError(
65
+ missing.length > 0
66
+ ? `Missing required ${
67
+ missing.length === 1 ? 'parameter' : 'parameters'
68
+ } for ${target}: ${missing.map(toArgName).join(' ')}`
69
+ : `Cannot prompt for ${target} in non-interactive mode`,
70
+ )
71
+ }
72
+
56
73
  const propSortScore = (prop: string) => {
57
74
  if (required.includes(prop)) return 100 - ergonomicPropOrder.indexOf(prop)
58
75
  if (args.params[prop] !== undefined) {
@@ -61,7 +78,6 @@ export const interactForBlueprintObject = async (
61
78
  return ergonomicPropOrder.indexOf(prop)
62
79
  }
63
80
 
64
- const cmdPath = `/${args.command.join('/').replace(/-/g, '_')}`
65
81
  const parameterSelectionMessage = args.isSubProperty
66
82
  ? `Editing "${args.subPropertyPath}"`
67
83
  : `[${cmdPath}] Parameters`
@@ -3,6 +3,7 @@ import { isDeepStrictEqual as isEqual } from 'node:util'
3
3
  import prompts from 'prompts'
4
4
 
5
5
  import type { ContextHelpers } from './types.js'
6
+ import { NonInteractiveError } from './util/cli-args.js'
6
7
 
7
8
  const uniqBy = <T>(items: T[], keyOf: (item: T) => unknown): T[] => {
8
9
  const seen = new Set<unknown>()
@@ -64,6 +65,26 @@ export async function interactForCommandSelection(
64
65
  return commandPath
65
66
  }
66
67
 
68
+ if (helpers.interactivity === 'non-interactive') {
69
+ // The command path is itself a command, so call it directly rather than
70
+ // prompting to select one of its sub-commands.
71
+ if (possibleCommands.some((cmd) => cmd.length === commandPath.length)) {
72
+ return commandPath
73
+ }
74
+
75
+ const subcommands = possibleCommands
76
+ .map((cmd) => cmd[commandPath.length])
77
+ .filter((subcommand) => subcommand != null)
78
+ .sort(ergonomicSort)
79
+ throw new NonInteractiveError(
80
+ `${
81
+ commandPath.length === 0
82
+ ? 'Missing command'
83
+ : `Incomplete command "seam ${commandPath.join(' ')}"`
84
+ }: expected one of ${subcommands.join(', ')}`,
85
+ )
86
+ }
87
+
67
88
  // Add dynamic 'back' command for sub-commands to allow returning
68
89
  // to previous level.
69
90
  if (commandPath.length > 0) {
package/src/lib/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ApiBlueprint } from './get-api-blueprint.js'
2
+ import type { Interactivity } from './util/cli-args.js'
2
3
 
3
4
  export interface ContextHelpers {
4
5
  blueprint: ApiBlueprint
5
- is_interactive: boolean
6
+ interactivity: Interactivity
6
7
  }
@@ -0,0 +1,62 @@
1
+ import parseArgs, { type ParsedArgs } from 'minimist'
2
+
3
+ /**
4
+ * How the CLI should behave when properties are not given as arguments.
5
+ *
6
+ * - `auto`: make the request as soon as every required property is given,
7
+ * otherwise prompt for what is missing. This is the default.
8
+ * - `interactive`: always prompt to review and edit properties, prefilled
9
+ * with the given arguments. Selected with `--interactive` or `-i`.
10
+ * - `non-interactive`: never prompt: anything missing is an error.
11
+ * Selected with `--non-interactive` or `-y`.
12
+ */
13
+ export type Interactivity = 'auto' | 'interactive' | 'non-interactive'
14
+
15
+ /**
16
+ * Argument keys that select the interactivity
17
+ * and are therefore not command parameters.
18
+ */
19
+ export const interactivityFlags: string[] = [
20
+ 'non_interactive',
21
+ 'y',
22
+ 'interactive',
23
+ 'i',
24
+ ]
25
+
26
+ /**
27
+ * Thrown when the CLI needs input it cannot prompt for.
28
+ */
29
+ export class NonInteractiveError extends Error {
30
+ override name = 'NonInteractiveError'
31
+ }
32
+
33
+ export const parseCliArgs = (argv: string[]): ParsedArgs =>
34
+ parseArgs(argv, {
35
+ string: ['code'],
36
+ boolean: ['non-interactive', 'interactive'],
37
+ // Deliberately not aliased to -n, which is reserved for a future
38
+ // --dry-run flag.
39
+ alias: { 'non-interactive': 'y', interactive: 'i' },
40
+ })
41
+
42
+ export const getInteractivity = (args: ParsedArgs): Interactivity => {
43
+ const isNonInteractive =
44
+ args['non_interactive'] === true || args['y'] === true
45
+ const isInteractive = args['interactive'] === true || args['i'] === true
46
+
47
+ if (isNonInteractive && isInteractive) {
48
+ throw new Error(
49
+ 'The --interactive and --non-interactive flags cannot be used together',
50
+ )
51
+ }
52
+ if (isNonInteractive) return 'non-interactive'
53
+ if (isInteractive) return 'interactive'
54
+ return 'auto'
55
+ }
56
+
57
+ /**
58
+ * Render a parameter name as the argument used to set it,
59
+ * e.g., `device_id` as `--device-id`.
60
+ */
61
+ export const toArgName = (parameterName: string): string =>
62
+ `--${parameterName.replace(/_/g, '-')}`
@@ -1,3 +1,6 @@
1
- const seamapiCliVersion = '0.8.0'
1
+ // Versions are replaced with generated values when the package is packed.
2
+ const seamapiCliVersion = '0.10.0'
3
+ const seamapiBlueprintVersion = '1.2.0'
2
4
 
5
+ export { seamapiBlueprintVersion }
3
6
  export default seamapiCliVersion