@seamapi/cli 0.11.0 → 0.13.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.
- package/README.md +71 -0
- package/bin/cli.js +38 -98
- package/bin/cli.js.map +1 -1
- package/completions/seam.bash +16 -0
- package/completions/seam.fish +15 -0
- package/completions/seam.zsh +19 -0
- package/lib/command-spec.d.ts +53 -0
- package/lib/command-spec.js +290 -0
- package/lib/command-spec.js.map +1 -0
- package/lib/completion/describe.d.ts +10 -0
- package/lib/completion/describe.js +16 -0
- package/lib/completion/describe.js.map +1 -0
- package/lib/completion/index.d.ts +28 -0
- package/lib/completion/index.js +92 -0
- package/lib/completion/index.js.map +1 -0
- package/lib/completion/render-bash.d.ts +2 -0
- package/lib/completion/render-bash.js +98 -0
- package/lib/completion/render-bash.js.map +1 -0
- package/lib/completion/render-fish.d.ts +2 -0
- package/lib/completion/render-fish.js +58 -0
- package/lib/completion/render-fish.js.map +1 -0
- package/lib/completion/render-zsh.d.ts +2 -0
- package/lib/completion/render-zsh.js +121 -0
- package/lib/completion/render-zsh.js.map +1 -0
- package/lib/render-help.d.ts +8 -0
- package/lib/render-help.js +150 -0
- package/lib/render-help.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +2 -1
- package/src/bin/cli.ts +54 -104
- package/src/lib/command-spec.ts +400 -0
- package/src/lib/completion/describe.ts +21 -0
- package/src/lib/completion/index.ts +110 -0
- package/src/lib/completion/render-bash.ts +125 -0
- package/src/lib/completion/render-fish.ts +80 -0
- package/src/lib/completion/render-zsh.ts +157 -0
- package/src/lib/render-help.ts +197 -0
- package/src/lib/version.ts +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { CommandFlag, CommandSpec } from '../command-spec.js'
|
|
2
|
+
import { describeForShell } from './describe.js'
|
|
3
|
+
|
|
4
|
+
export const renderFishCompletion = (spec: CommandSpec): string =>
|
|
5
|
+
`${[
|
|
6
|
+
header,
|
|
7
|
+
helpers,
|
|
8
|
+
['complete -c seam -f', ...subcommandCompletions(spec)].join('\n'),
|
|
9
|
+
flagCompletions(spec).join('\n'),
|
|
10
|
+
globalFlagCompletions(spec).join('\n'),
|
|
11
|
+
].join('\n\n')}\n`
|
|
12
|
+
|
|
13
|
+
const header = `# fish completion for the seam command.
|
|
14
|
+
#
|
|
15
|
+
# Generated by @seamapi/cli from the Seam API definitions.
|
|
16
|
+
# Do not edit: regenerate with 'seam completion fish'.
|
|
17
|
+
#
|
|
18
|
+
# Install it with
|
|
19
|
+
#
|
|
20
|
+
# seam completion fish > ~/.config/fish/completions/seam.fish`
|
|
21
|
+
|
|
22
|
+
const helpers = `function __seam_command --description 'Print the seam command path on the command line'
|
|
23
|
+
set -l tokens (commandline -opc)
|
|
24
|
+
set -l command
|
|
25
|
+
if test (count $tokens) -gt 1
|
|
26
|
+
# The command path is the run of words before the first flag.
|
|
27
|
+
for token in $tokens[2..-1]
|
|
28
|
+
if string match -q -- '-*' $token
|
|
29
|
+
break
|
|
30
|
+
end
|
|
31
|
+
set -a command $token
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
string join ' ' -- $command
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
function __seam_using --description 'Test whether the command line names the given seam command'
|
|
38
|
+
set -l command (__seam_command)
|
|
39
|
+
test "$command" = "$argv[1]"
|
|
40
|
+
end`
|
|
41
|
+
|
|
42
|
+
const subcommandCompletions = (spec: CommandSpec): string[] =>
|
|
43
|
+
spec.groups.flatMap((group) =>
|
|
44
|
+
group.subcommands.map(({ name, description }) =>
|
|
45
|
+
complete([
|
|
46
|
+
`-n '__seam_using "${group.path.join(' ')}"'`,
|
|
47
|
+
`-a '${name}'`,
|
|
48
|
+
describe(description),
|
|
49
|
+
]),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const flagCompletions = (spec: CommandSpec): string[] =>
|
|
54
|
+
spec.commands.flatMap((command) =>
|
|
55
|
+
command.flags.map((flag) =>
|
|
56
|
+
complete([
|
|
57
|
+
`-n '__seam_using "${command.path.join(' ')}"'`,
|
|
58
|
+
...flagOptions(flag),
|
|
59
|
+
]),
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const globalFlagCompletions = (spec: CommandSpec): string[] =>
|
|
64
|
+
spec.globalFlags.map((flag) => complete(flagOptions(flag)))
|
|
65
|
+
|
|
66
|
+
const flagOptions = (flag: CommandFlag): string[] => [
|
|
67
|
+
...(flag.short == null ? [] : [`-s ${flag.short}`]),
|
|
68
|
+
...(flag.long == null ? [] : [`-l ${flag.long}`]),
|
|
69
|
+
...(flag.takesValue ? ['-r'] : []),
|
|
70
|
+
...(flag.values.length === 0 ? [] : [`-a '${flag.values.join(' ')}'`]),
|
|
71
|
+
describe(flag.description),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
const describe = (description: string): string => {
|
|
75
|
+
const summary = describeForShell(description)
|
|
76
|
+
return summary === '' ? '' : `-d '${summary}'`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const complete = (options: string[]): string =>
|
|
80
|
+
['complete -c seam', ...options.filter((option) => option !== '')].join(' ')
|
|
@@ -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
|
+
}
|
package/src/lib/version.ts
CHANGED