@seamapi/cli 0.11.0 → 0.12.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 (39) hide show
  1. package/README.md +60 -0
  2. package/bin/cli.js +38 -98
  3. package/bin/cli.js.map +1 -1
  4. package/completions/seam.bash +11 -0
  5. package/completions/seam.fish +11 -0
  6. package/completions/seam.zsh +13 -0
  7. package/lib/command-spec.d.ts +53 -0
  8. package/lib/command-spec.js +290 -0
  9. package/lib/command-spec.js.map +1 -0
  10. package/lib/completion/describe.d.ts +10 -0
  11. package/lib/completion/describe.js +16 -0
  12. package/lib/completion/describe.js.map +1 -0
  13. package/lib/completion/index.d.ts +20 -0
  14. package/lib/completion/index.js +65 -0
  15. package/lib/completion/index.js.map +1 -0
  16. package/lib/completion/render-bash.d.ts +2 -0
  17. package/lib/completion/render-bash.js +98 -0
  18. package/lib/completion/render-bash.js.map +1 -0
  19. package/lib/completion/render-fish.d.ts +2 -0
  20. package/lib/completion/render-fish.js +58 -0
  21. package/lib/completion/render-fish.js.map +1 -0
  22. package/lib/completion/render-zsh.d.ts +2 -0
  23. package/lib/completion/render-zsh.js +121 -0
  24. package/lib/completion/render-zsh.js.map +1 -0
  25. package/lib/render-help.d.ts +8 -0
  26. package/lib/render-help.js +150 -0
  27. package/lib/render-help.js.map +1 -0
  28. package/lib/version.d.ts +1 -1
  29. package/lib/version.js +1 -1
  30. package/package.json +2 -1
  31. package/src/bin/cli.ts +54 -104
  32. package/src/lib/command-spec.ts +400 -0
  33. package/src/lib/completion/describe.ts +21 -0
  34. package/src/lib/completion/index.ts +82 -0
  35. package/src/lib/completion/render-bash.ts +125 -0
  36. package/src/lib/completion/render-fish.ts +80 -0
  37. package/src/lib/completion/render-zsh.ts +157 -0
  38. package/src/lib/render-help.ts +197 -0
  39. package/src/lib/version.ts +1 -1
@@ -0,0 +1,157 @@
1
+ import {
2
+ type CommandFlag,
3
+ type CommandSpec,
4
+ flagTokens,
5
+ } from '../command-spec.js'
6
+ import { describeForShell } from './describe.js'
7
+
8
+ export const renderZshCompletion = (spec: CommandSpec): string => {
9
+ const valuelessTokens = spec.globalFlags
10
+ .filter(({ takesValue }) => !takesValue)
11
+ .flatMap(flagTokens)
12
+ .sort()
13
+
14
+ return `${[
15
+ header,
16
+ renderCase('_seam_subcommands', subcommandBranches(spec)),
17
+ renderCase('_seam_flags', flagBranches(spec)),
18
+ renderCase('_seam_flag_values', flagValueBranches(spec)),
19
+ `_seam_global_flags() {\n _seam_reply+=(${describeFlags(spec.globalFlags)})\n}`,
20
+ completionFunction(valuelessTokens),
21
+ dispatch,
22
+ ].join('\n\n')}\n`
23
+ }
24
+
25
+ const header = `#compdef seam
26
+
27
+ # zsh completion for the seam command.
28
+ #
29
+ # Generated by @seamapi/cli from the Seam API definitions.
30
+ # Do not edit: regenerate with 'seam completion zsh'.
31
+ #
32
+ # Load it for the current shell with
33
+ #
34
+ # source <(seam completion zsh)
35
+ #
36
+ # or install it for every shell with
37
+ #
38
+ # seam completion zsh > "\${fpath[1]}/_seam"`
39
+
40
+ const completionFunction = (valuelessTokens: string[]): string =>
41
+ `_seam() {
42
+ local -a _seam_reply
43
+ local -a valueless
44
+ local command previous word
45
+ local -i index
46
+
47
+ valueless=(${valuelessTokens.join(' ')})
48
+
49
+ # The command path is the run of words before the first flag.
50
+ command=''
51
+ for (( index = 2; index < CURRENT; index++ )); do
52
+ word="\${words[index]}"
53
+ if [[ "$word" == -* ]]; then
54
+ break
55
+ fi
56
+ command="\${command:+$command }$word"
57
+ done
58
+
59
+ previous=''
60
+ if (( CURRENT > 1 )); then
61
+ previous="\${words[CURRENT - 1]}"
62
+ fi
63
+
64
+ # Completing the value of a flag that takes one.
65
+ if [[ "$previous" == -* ]] && (( \${valueless[(Ie)$previous]} == 0 )); then
66
+ _seam_flag_values "$command $previous"
67
+ if (( \${#_seam_reply} )); then
68
+ _describe -t values 'value' _seam_reply
69
+ fi
70
+ return
71
+ fi
72
+
73
+ if [[ "\${words[CURRENT]}" == -* ]]; then
74
+ _seam_flags "$command"
75
+ _seam_global_flags
76
+ _describe -t options 'option' _seam_reply
77
+ return
78
+ fi
79
+
80
+ _seam_subcommands "$command"
81
+ if (( \${#_seam_reply} )); then
82
+ _describe -t commands 'command' _seam_reply
83
+ return
84
+ fi
85
+
86
+ _seam_flags "$command"
87
+ _seam_global_flags
88
+ _describe -t options 'option' _seam_reply
89
+ }`
90
+
91
+ // The script runs in three ways. Autoloaded from fpath as _seam, it must
92
+ // complete the in-flight request: funcstack holds _seam. Evaluated by the
93
+ // loader stub inside the autoloaded _seam, the same applies, but eval pushes
94
+ // '(eval)' onto funcstack, so search the whole stack rather than the top.
95
+ // Sourced into a shell, funcstack holds no _seam: register with compdef.
96
+ const dispatch = `if (( \${funcstack[(I)_seam]} )); then
97
+ _seam "$@"
98
+ else
99
+ compdef _seam seam
100
+ fi`
101
+
102
+ interface Branch {
103
+ pattern: string
104
+ entries: string[]
105
+ }
106
+
107
+ const subcommandBranches = (spec: CommandSpec): Branch[] =>
108
+ spec.groups.map((group) => ({
109
+ pattern: group.path.join(' '),
110
+ entries: group.subcommands.map(({ name, description }) =>
111
+ describe(name, description),
112
+ ),
113
+ }))
114
+
115
+ const flagBranches = (spec: CommandSpec): Branch[] =>
116
+ spec.commands
117
+ .filter(({ flags }) => flags.length > 0)
118
+ .map((command) => ({
119
+ pattern: command.path.join(' '),
120
+ entries: [describeFlags(command.flags)],
121
+ }))
122
+
123
+ const flagValueBranches = (spec: CommandSpec): Branch[] =>
124
+ spec.commands.flatMap((command) =>
125
+ command.flags
126
+ .filter(({ values }) => values.length > 0)
127
+ .flatMap((flag) =>
128
+ flagTokens(flag).map((token) => ({
129
+ pattern: `${command.path.join(' ')} ${token}`,
130
+ entries: flag.values.map((value) => `'${value}'`),
131
+ })),
132
+ ),
133
+ )
134
+
135
+ const describeFlags = (flags: CommandFlag[]): string =>
136
+ flags
137
+ .flatMap((flag) =>
138
+ flagTokens(flag).map((token) => describe(token, flag.description)),
139
+ )
140
+ .join(' ')
141
+
142
+ const describe = (value: string, description: string): string => {
143
+ const summary = describeForShell(description)
144
+ return summary === '' ? `'${value}'` : `'${value}:${summary}'`
145
+ }
146
+
147
+ const renderCase = (name: string, branches: Branch[]): string =>
148
+ [
149
+ `${name}() {`,
150
+ ` case "$1" in`,
151
+ ...branches.map(
152
+ ({ pattern, entries }) =>
153
+ ` ('${pattern}') _seam_reply+=(${entries.join(' ')}) ;;`,
154
+ ),
155
+ ` esac`,
156
+ `}`,
157
+ ].join('\n')
@@ -0,0 +1,197 @@
1
+ import commandLineUsage, { type Section } from 'command-line-usage'
2
+
3
+ import {
4
+ type CommandDefinition,
5
+ type CommandFlag,
6
+ type CommandGroup,
7
+ type CommandSpec,
8
+ findCommand,
9
+ findGroup,
10
+ } from './command-spec.js'
11
+
12
+ /**
13
+ * Render the help guide for a command path, or `null` when no command or
14
+ * group goes by that path.
15
+ *
16
+ * An empty path is the guide for `seam` itself.
17
+ */
18
+ export const renderHelp = (
19
+ path: string[],
20
+ spec: CommandSpec,
21
+ ): string | null => {
22
+ const group = findGroup(spec, path)
23
+ if (group != null) return commandLineUsage(groupSections(group, spec))
24
+
25
+ const command = findCommand(spec, path)
26
+ if (command != null) return commandLineUsage(commandSections(command, spec))
27
+
28
+ return null
29
+ }
30
+
31
+ const overview =
32
+ 'Every seam command runs as soon as every required property is given, and otherwise prompts you for what is missing with helpful suggestions. Pass -i to always review properties first, or -y to never be prompted.'
33
+
34
+ const outputSection = {
35
+ header: 'Output',
36
+ content: [
37
+ 'Only the response is written to stdout, so it is safe to pipe. Prompts, progress, and other information are written to stderr.',
38
+ 'The response is trimmed to the response key and pagination.',
39
+ 'Request params may be piped or redirected in as a JSON object. Params given as arguments win over params read from stdin.',
40
+ ],
41
+ }
42
+
43
+ const examples = [
44
+ { name: 'seam', summary: 'Interactively select commands to execute.' },
45
+ { name: 'seam login', summary: 'Login to Seam.' },
46
+ { name: 'seam wizard', summary: 'Set up Seam in the current project.' },
47
+ { name: 'seam select workspace', summary: 'Select your workspace.' },
48
+ {
49
+ name: 'seam connect-webviews create',
50
+ summary: 'Create a connect webview to connect devices.',
51
+ },
52
+ { name: 'seam devices list', summary: 'List devices in your workspace.' },
53
+ {
54
+ name: 'seam devices list {bold --interactive}',
55
+ summary: 'Review and edit filters before listing devices.',
56
+ },
57
+ {
58
+ name: 'seam devices list {bold --non-interactive}',
59
+ summary: 'List devices, failing instead of prompting.',
60
+ },
61
+ {
62
+ name: 'seam locks unlock-door {bold --device-id} $MY_DOOR',
63
+ summary: 'Unlock a lock.',
64
+ },
65
+ {
66
+ name: "seam access-codes create {bold --code} '1234' {bold --name} 'My Code'",
67
+ summary: 'Create an access code.',
68
+ },
69
+ {
70
+ name: 'seam devices list > devices.json',
71
+ summary: 'Write the response to a file as JSON.',
72
+ },
73
+ {
74
+ name: 'cat params.json | seam locks unlock-door',
75
+ summary: 'Pipe request params in as JSON.',
76
+ },
77
+ {
78
+ name: 'seam completion bash',
79
+ summary: 'Print a shell completion script for bash, fish, or zsh.',
80
+ },
81
+ ]
82
+
83
+ const groupSections = (group: CommandGroup, spec: CommandSpec): Section[] => {
84
+ const isRoot = group.path.length === 0
85
+ const name = ['seam', ...group.path].join(' ')
86
+
87
+ return [
88
+ isRoot
89
+ ? { header: 'Seam CLI', content: overview }
90
+ : { header: name, content: `Commands under ${name}.` },
91
+ { header: 'Usage', content: `${name} <command> [options]` },
92
+ ...commandSectionsForGroup(group, isRoot),
93
+ optionSection(spec.globalFlags),
94
+ ...(isRoot
95
+ ? [outputSection, { header: 'Command List Examples', content: examples }]
96
+ : []),
97
+ { content: `Run '${name} <command> --help' to see a command in detail.` },
98
+ ]
99
+ }
100
+
101
+ const commandSectionsForGroup = (
102
+ group: CommandGroup,
103
+ isRoot: boolean,
104
+ ): Section[] => {
105
+ const content = (subcommands: CommandGroup['subcommands']) =>
106
+ subcommands.map(({ name, description }) => ({ name, summary: description }))
107
+
108
+ // The root guide separates the commands of the CLI itself from the commands
109
+ // that call the Seam API. Anywhere deeper the split adds nothing: a group
110
+ // holds commands of one kind.
111
+ if (!isRoot) {
112
+ return [{ header: 'Commands', content: content(group.subcommands) }]
113
+ }
114
+
115
+ const cli = group.subcommands.filter(({ kind }) => kind === 'cli')
116
+ const api = group.subcommands.filter(({ kind }) => kind === 'api')
117
+
118
+ return [
119
+ { header: 'Commands', content: content(cli) },
120
+ { header: 'API Commands', content: content(api) },
121
+ ].filter((section) => section.content.length > 0)
122
+ }
123
+
124
+ const commandSections = (
125
+ command: CommandDefinition,
126
+ spec: CommandSpec,
127
+ ): Section[] => {
128
+ const name = ['seam', ...command.path].join(' ')
129
+ const hasFlags = command.flags.length > 0
130
+
131
+ return [
132
+ {
133
+ header: name,
134
+ content: [command.title, command.description].filter(
135
+ (line) => line !== '',
136
+ ),
137
+ },
138
+ { header: 'Usage', content: `${name} [options]` },
139
+ // The command's own parameters are what the request is made of, so keep
140
+ // them apart from the options every seam command takes.
141
+ ...(hasFlags ? [optionSection(command.flags, 'Parameters')] : []),
142
+ optionSection(spec.globalFlags),
143
+ ...(hasFlags
144
+ ? [
145
+ {
146
+ content:
147
+ 'Any required parameter left out is prompted for interactively.',
148
+ },
149
+ ]
150
+ : []),
151
+ ]
152
+ }
153
+
154
+ const optionSection = (flags: CommandFlag[], header = 'Options'): Section => ({
155
+ header,
156
+ optionList: flags.map(toOptionDefinition),
157
+ })
158
+
159
+ const maxDocumentedValues = 8
160
+
161
+ interface OptionDefinition {
162
+ name: string
163
+ alias?: string
164
+ description: string
165
+ type: typeof Boolean | typeof String
166
+ typeLabel?: string
167
+ }
168
+
169
+ const toOptionDefinition = (flag: CommandFlag): OptionDefinition => {
170
+ const description = [
171
+ flag.isRequired ? '{bold [required]}' : '',
172
+ flag.description,
173
+ describeValues(flag),
174
+ ]
175
+ .filter((part) => part !== '')
176
+ .join(' ')
177
+
178
+ return {
179
+ // command-line-usage renders a nameless option as the short form alone.
180
+ name: flag.long ?? '',
181
+ ...(flag.short == null ? {} : { alias: flag.short }),
182
+ // A flag with no value must be typed as a boolean, or the guide labels it
183
+ // as taking a string.
184
+ type: flag.takesValue ? String : Boolean,
185
+ ...(flag.takesValue ? { typeLabel: '{underline value}' } : {}),
186
+ description,
187
+ }
188
+ }
189
+
190
+ const describeValues = (flag: CommandFlag): string => {
191
+ if (flag.values.length === 0) return ''
192
+
193
+ const shown = flag.values.slice(0, maxDocumentedValues).join(', ')
194
+ const rest = flag.values.length - maxDocumentedValues
195
+
196
+ return rest > 0 ? `One of: ${shown}, and ${rest} more.` : `One of: ${shown}.`
197
+ }
@@ -1,5 +1,5 @@
1
1
  // Versions are replaced with generated values when the package is packed.
2
- const seamapiCliVersion = '0.11.0'
2
+ const seamapiCliVersion = '0.12.0'
3
3
  const seamapiBlueprintVersion = '1.2.0'
4
4
 
5
5
  export { seamapiBlueprintVersion }