@gunshi/docs 0.27.6 → 0.28.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 CHANGED
@@ -33,6 +33,7 @@ Robust, modular, flexible, and localizable CLI library
33
33
  - [Internationalization](src/guide/advanced/internationalization)
34
34
  - [Documentation Generation](src/guide/advanced/docs-gen)
35
35
  - [Advanced Lazy Loading and Sub-Commands](src/guide/advanced/advanced-lazy-loading)
36
+ - [Nested Sub-Commands](src/guide/advanced/nested-sub-commands)
36
37
 
37
38
 
38
39
  ### API References
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gunshi/docs",
3
3
  "description": "Documentation for gunshi",
4
- "version": "0.27.6",
4
+ "version": "0.28.0",
5
5
  "author": {
6
6
  "name": "kazuya kawaguchi",
7
7
  "email": "kawakazu80@gmail.com"
@@ -22,6 +22,7 @@ Parameters of [createCommandContext](../functions/createCommandContext.md)
22
22
  | <a id="callmode"></a> `callMode?` | [`CommandCallMode`](../../default/type-aliases/CommandCallMode.md) | Command call mode. |
23
23
  | <a id="clioptions"></a> `cliOptions?` | [`CliOptions`](../../default/interfaces/CliOptions.md)\<`G`\> | A command options, which is spicialized from `cli` function |
24
24
  | <a id="command"></a> `command?` | `C` | A target command |
25
+ | <a id="commandpath"></a> `commandPath?` | `string`[] | The path of nested sub-commands resolved to reach the current command. |
25
26
  | <a id="explicit"></a> `explicit?` | `ArgExplicitlyProvided`\<`ExtractArgs`\<`G`\>\> | Explicitly provided arguments |
26
27
  | <a id="extensions"></a> `extensions?` | `E` | Plugin extensions to apply as the command context extension. |
27
28
  | <a id="omitted"></a> `omitted?` | `boolean` | Whether the command is omitted |
@@ -22,4 +22,5 @@ Command interface.
22
22
  | <a id="name"></a> `name?` | `string` | Command name. It's used to find command line arguments to execute from sub commands, and it's recommended to specify. |
23
23
  | <a id="rendering"></a> `rendering?` | [`RenderingOptions`](RenderingOptions.md)\<`G`\> | Rendering control options **Since** v0.27.0 |
24
24
  | <a id="run"></a> `run?` | [`CommandRunner`](../type-aliases/CommandRunner.md)\<`G`\> | Command runner. it's the command to be executed |
25
+ | <a id="subcommands"></a> `subCommands?` | \| `Record`\<`string`, [`SubCommandable`](SubCommandable.md)\> \| `Map`\<`string`, [`SubCommandable`](SubCommandable.md)\> | Nested sub-commands for this command. Allows building command trees like `git remote add`. Each key is the sub-command name, and the value is a command or lazy command. **Since** v0.28.0 |
25
26
  | <a id="tokebab"></a> `toKebab?` | `boolean` | Whether to convert the camel-case style argument name to kebab-case. If you will set to `true`, All [`Command.args`](#args) names will be converted to kebab-case. |
@@ -19,6 +19,7 @@ Command context is the context of the command execution.
19
19
  | <a id="_"></a> `_` | `string`[] | Original command line arguments. This argument is passed from `cli` function. |
20
20
  | <a id="args"></a> `args` | `ExtractArgs`\<`G`\> | Command arguments, that is the arguments of the command that is executed. The command arguments is same [`Command.args`](Command.md#args). |
21
21
  | <a id="callmode"></a> `callMode` | [`CommandCallMode`](../type-aliases/CommandCallMode.md) | Command call mode. The command call mode is `entry` when the command is executed as an entry command, and `subCommand` when the command is executed as a sub-command. |
22
+ | <a id="commandpath"></a> `commandPath` | `string`[] | The path of nested sub-commands that were resolved to reach the current command. For example, if the user runs `git remote add`, `commandPath` would be `['remote', 'add']`. For the entry command, this is an empty array. **Since** v0.28.0 |
22
23
  | <a id="description"></a> `description` | `string` \| `undefined` | Command description, that is the description of the command that is executed. The command description is same [`CommandEnvironment.description`](CommandEnvironment.md#description). |
23
24
  | <a id="env"></a> `env` | `Readonly`\<[`CommandEnvironment`](CommandEnvironment.md)\<`G`\>\> | Command environment, that is the environment of the command that is executed. The command environment is same [`CommandEnvironment`](CommandEnvironment.md). |
24
25
  | <a id="explicit"></a> `explicit` | `ExtractArgExplicitlyProvided`\<`G`\> | Whether arguments were explicitly provided by the user. - `true`: The argument was explicitly provided via command line - `false`: The argument was not explicitly provided. This means either: - The value comes from a default value defined in the argument schema - The value is `undefined` (no explicit input and no default value) |
@@ -32,4 +32,5 @@ Index signature to allow additional properties
32
32
  | <a id="name"></a> `name?` | `string` | see [Command.name](Command.md#name) |
33
33
  | <a id="rendering"></a> `rendering?` | `any` | see [Command.rendering](Command.md#rendering) |
34
34
  | <a id="run"></a> `run?` | (...`args`) => `any` | see [Command.run](Command.md#run) |
35
+ | <a id="subcommands"></a> `subCommands?` | `Record`\<`string`, `any`\> \| `Map`\<`string`, `any`\> | Nested sub-commands for this command. **See** [Command.subCommands](Command.md#subcommands) **Since** v0.28.0 |
35
36
  | <a id="tokebab"></a> `toKebab?` | `boolean` | see [Command.toKebab](Command.md#tokebab) |
@@ -3,7 +3,7 @@
3
3
  # Function: define()
4
4
 
5
5
  ```ts
6
- function define<G, A, C>(definition): { [K in string | number | symbol]: (Pick<C, keyof C> & Partial<Pick<Command<G>, Exclude<"name", keyof C> | Exclude<"description", keyof C> | Exclude<"run", keyof C> | Exclude<"args", keyof C> | Exclude<"examples", keyof C> | Exclude<"toKebab", keyof C> | Exclude<"internal", keyof C> | Exclude<"entry", keyof C> | Exclude<"rendering", keyof C>>>)[K] };
6
+ function define<G, A, C>(definition): { [K in string | number | symbol]: (Pick<C, keyof C> & Partial<Pick<Command<G>, Exclude<"name", keyof C> | Exclude<"description", keyof C> | Exclude<"subCommands", keyof C> | Exclude<"run", keyof C> | Exclude<"args", keyof C> | Exclude<"examples", keyof C> | Exclude<"toKebab", keyof C> | Exclude<"internal", keyof C> | Exclude<"entry", keyof C> | Exclude<"rendering", keyof C>>>)[K] };
7
7
  ```
8
8
 
9
9
  Define a [command](../../default/interfaces/Command.md).
@@ -24,7 +24,7 @@ Define a [command](../../default/interfaces/Command.md).
24
24
 
25
25
  ## Returns
26
26
 
27
- \{ \[K in string \| number \| symbol\]: (Pick\<C, keyof C\> & Partial\<Pick\<Command\<G\>, Exclude\<"name", keyof C\> \| Exclude\<"description", keyof C\> \| Exclude\<"run", keyof C\> \| Exclude\<"args", keyof C\> \| Exclude\<"examples", keyof C\> \| Exclude\<"toKebab", keyof C\> \| Exclude\<"internal", keyof C\> \| Exclude\<"entry", keyof C\> \| Exclude\<"rendering", keyof C\>\>\>)\[K\] \}
27
+ \{ \[K in string \| number \| symbol\]: (Pick\<C, keyof C\> & Partial\<Pick\<Command\<G\>, Exclude\<"name", keyof C\> \| Exclude\<"description", keyof C\> \| Exclude\<"subCommands", keyof C\> \| Exclude\<"run", keyof C\> \| Exclude\<"args", keyof C\> \| Exclude\<"examples", keyof C\> \| Exclude\<"toKebab", keyof C\> \| Exclude\<"internal", keyof C\> \| Exclude\<"entry", keyof C\> \| Exclude\<"rendering", keyof C\>\>\>)\[K\] \}
28
28
 
29
29
  A defined [command](../../default/interfaces/Command.md)
30
30
 
@@ -21,7 +21,7 @@ Generate the command usage.
21
21
 
22
22
  | Parameter | Type | Description |
23
23
  | ------ | ------ | ------ |
24
- | `command` | `string` \| `null` | usage generate command, if you want to generate the usage of the default command where there are target commands and sub-commands, specify `null`. |
24
+ | `command` | `string` \| `string`[] \| `null` | usage generate command, if you want to generate the usage of the default command where there are target commands and sub-commands, specify `null`. |
25
25
  | `entry` | \| [`Command`](../../default/interfaces/Command.md)\<`G`\> \| [`LazyCommand`](../../default/type-aliases/LazyCommand.md)\<`G`\> | A [`entry command`](../../default/interfaces/Command.md) |
26
26
  | `options` | [`GenerateOptions`](../type-aliases/GenerateOptions.md)\<`G`\> | A [`cli options`](../type-aliases/GenerateOptions.md) |
27
27
 
@@ -0,0 +1,238 @@
1
+ # Nested Sub-Commands
2
+
3
+ Gunshi supports nested sub-commands, allowing you to build hierarchical command trees similar to tools like Git (`git remote add`) or Docker (`docker container ls`).
4
+
5
+ ## Why Use Nested Sub-Commands?
6
+
7
+ Nested sub-commands are useful when your CLI has command groups with related operations:
8
+
9
+ - **Organization**: Group related operations under a parent command (e.g., `remote add`, `remote remove`)
10
+ - **Discoverability**: Users can explore available operations at each level with `--help`
11
+ - **Scalability**: Add new nested commands without cluttering the top-level command list
12
+
13
+ ## Basic Nested Sub-Commands
14
+
15
+ You can nest sub-commands by adding a `subCommands` property to any command definition:
16
+
17
+ ```ts [cli.ts]
18
+ import { cli, define } from 'gunshi'
19
+
20
+ // Define leaf commands
21
+ const addCommand = define({
22
+ name: 'add',
23
+ description: 'Add a remote',
24
+ args: {
25
+ url: { type: 'string', required: true, description: 'Remote URL' }
26
+ },
27
+ run: ctx => {
28
+ console.log(`Adding remote: ${ctx.values.url}`)
29
+ }
30
+ })
31
+
32
+ const removeCommand = define({
33
+ name: 'remove',
34
+ description: 'Remove a remote',
35
+ args: {
36
+ name: { type: 'positional', description: 'Remote name' }
37
+ },
38
+ run: ctx => {
39
+ console.log(`Removing remote: ${ctx.values.name}`)
40
+ }
41
+ })
42
+
43
+ // Define an intermediate command with nested sub-commands
44
+ const remoteCommand = define({
45
+ name: 'remote',
46
+ description: 'Manage remotes',
47
+ subCommands: {
48
+ add: addCommand,
49
+ remove: removeCommand
50
+ },
51
+ run: () => {
52
+ console.log('Use: git remote add|remove')
53
+ }
54
+ })
55
+
56
+ // Define the entry command
57
+ const entry = define({
58
+ name: 'main',
59
+ description: 'Git-like CLI',
60
+ run: () => {
61
+ console.log('Run --help for available commands')
62
+ }
63
+ })
64
+
65
+ await cli(process.argv.slice(2), entry, {
66
+ name: 'git',
67
+ version: '1.0.0',
68
+ subCommands: {
69
+ remote: remoteCommand
70
+ }
71
+ })
72
+ ```
73
+
74
+ <!-- eslint-disable markdown/no-missing-label-refs -->
75
+
76
+ > [!TIP]
77
+ > The complete example code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/nested-sub-commands).
78
+
79
+ <!-- eslint-enable markdown/no-missing-label-refs -->
80
+
81
+ Now users can run:
82
+
83
+ ```sh
84
+ # Execute nested sub-command
85
+ $ npx tsx cli.ts remote add --url https://example.com
86
+ Adding remote: https://example.com
87
+
88
+ # Show help for intermediate command
89
+ $ npx tsx cli.ts remote --help
90
+ Manage remotes
91
+
92
+ USAGE:
93
+ git remote [COMMANDS] <OPTIONS>
94
+
95
+ COMMANDS:
96
+ [remote] <OPTIONS> Manage remotes
97
+ add <OPTIONS> Add a remote
98
+ remove <OPTIONS> Remove a remote
99
+
100
+ For more info, run any command with the `--help` flag:
101
+ git remote --help
102
+ git remote add --help
103
+ git remote remove --help
104
+
105
+ # Show help for leaf command
106
+ $ npx tsx cli.ts remote add --help
107
+ Add a remote
108
+
109
+ USAGE:
110
+ git remote add <OPTIONS>
111
+
112
+ OPTIONS:
113
+ --url <url> Remote URL
114
+ ```
115
+
116
+ ## Three or More Levels
117
+
118
+ You can nest commands to any depth:
119
+
120
+ ```ts
121
+ const subSubCommand = define({
122
+ name: 'sub-sub',
123
+ description: 'A deeply nested command',
124
+ run: ctx => {
125
+ // ctx.commandPath will be ['level1', 'level2', 'sub-sub']
126
+ console.log(`Command path: ${ctx.commandPath.join(' > ')}`)
127
+ }
128
+ })
129
+
130
+ const level2Command = define({
131
+ name: 'level2',
132
+ description: 'Second level',
133
+ subCommands: { 'sub-sub': subSubCommand },
134
+ run: () => {}
135
+ })
136
+
137
+ const level1Command = define({
138
+ name: 'level1',
139
+ description: 'First level',
140
+ subCommands: { level2: level2Command },
141
+ run: () => {}
142
+ })
143
+ ```
144
+
145
+ ## Using `commandPath`
146
+
147
+ The `CommandContext` includes a `commandPath` property that tells you the full path of commands that were resolved:
148
+
149
+ ```ts
150
+ const addCommand = define({
151
+ name: 'add',
152
+ description: 'Add a remote',
153
+ run: ctx => {
154
+ console.log(ctx.commandPath) // ['remote', 'add']
155
+ console.log(ctx.callMode) // 'subCommand'
156
+ }
157
+ })
158
+ ```
159
+
160
+ | Invocation | `commandPath` | `callMode` |
161
+ | ---------------- | ------------------- | -------------- |
162
+ | `cli` | `[]` | `'entry'` |
163
+ | `cli remote` | `['remote']` | `'subCommand'` |
164
+ | `cli remote add` | `['remote', 'add']` | `'subCommand'` |
165
+
166
+ ## Intermediate Commands
167
+
168
+ When a user invokes an intermediate command (one that has nested sub-commands) without specifying a child, the intermediate command's `run` function is called with `omitted: true`. In this case, the built-in help system automatically shows a COMMANDS section listing the available nested sub-commands:
169
+
170
+ ```ts
171
+ const remoteCommand = define({
172
+ name: 'remote',
173
+ description: 'Manage remotes',
174
+ subCommands: { add: addCommand },
175
+ run: ctx => {
176
+ if (ctx.omitted) {
177
+ // User ran `cli remote` without specifying a sub-command
178
+ // Help is shown automatically with COMMANDS section
179
+ console.log('Please specify a sub-command: add, remove')
180
+ }
181
+ }
182
+ })
183
+ ```
184
+
185
+ ## Nested Sub-Commands with Lazy Loading
186
+
187
+ For large CLIs, you can combine nested sub-commands with lazy loading:
188
+
189
+ ```ts
190
+ import { cli, define, lazy } from 'gunshi'
191
+
192
+ // Lazy-load the leaf command
193
+ const addCommand = lazy(() => import('./commands/remote-add.ts'), {
194
+ name: 'add',
195
+ description: 'Add a remote',
196
+ args: {
197
+ url: { type: 'string', required: true, description: 'Remote URL' }
198
+ }
199
+ })
200
+
201
+ // The parent command can also be lazy-loaded
202
+ const remoteCommand = lazy(() => import('./commands/remote.ts'), {
203
+ name: 'remote',
204
+ description: 'Manage remotes',
205
+ subCommands: {
206
+ add: addCommand
207
+ }
208
+ })
209
+
210
+ await cli(process.argv.slice(2), entry, {
211
+ name: 'git',
212
+ subCommands: { remote: remoteCommand }
213
+ })
214
+ ```
215
+
216
+ <!-- eslint-disable markdown/no-missing-label-refs -->
217
+
218
+ > [!IMPORTANT]
219
+ > When using `lazy()` with nested sub-commands, include `args` in the lazy definition (the second argument) if you want argument parsing to work before the command is loaded. The `subCommands` property is automatically carried over from the definition to the lazy command.
220
+
221
+ <!-- eslint-enable markdown/no-missing-label-refs -->
222
+
223
+ ## Generating Documentation for Nested Commands
224
+
225
+ The `generate()` function supports nested command paths:
226
+
227
+ ```ts
228
+ import { generate } from 'gunshi/generator'
229
+
230
+ // Generate help for a nested command using array or space-separated string
231
+ const help = await generate(['remote', 'add'], entry, {
232
+ name: 'git',
233
+ subCommands: { remote: remoteCommand }
234
+ })
235
+
236
+ // Or using space-separated string
237
+ const help2 = await generate('remote add', entry, { ... })
238
+ ```
@@ -319,6 +319,41 @@ This approach is particularly useful for CLIs that:
319
319
  - Want to provide a default action when no sub-command matches
320
320
  - Implement dynamic command resolution based on context
321
321
 
322
+ ## Nested Sub-Commands
323
+
324
+ Gunshi also supports nested sub-commands for building hierarchical command trees like `git remote add`. You can add a `subCommands` property to any command definition:
325
+
326
+ ```ts [cli.ts]
327
+ import { cli, define } from 'gunshi'
328
+
329
+ const addCommand = define({
330
+ name: 'add',
331
+ description: 'Add a remote',
332
+ args: { url: { type: 'string', required: true } },
333
+ run: ctx => console.log(`Adding: ${ctx.values.url}`)
334
+ })
335
+
336
+ const remoteCommand = define({
337
+ name: 'remote',
338
+ description: 'Manage remotes',
339
+ subCommands: { add: addCommand },
340
+ run: () => console.log('Use: remote add')
341
+ })
342
+
343
+ const entry = define({
344
+ name: 'main',
345
+ description: 'Git-like CLI',
346
+ run: () => console.log('Run --help for available commands')
347
+ })
348
+
349
+ await cli(process.argv.slice(2), entry, {
350
+ name: 'git',
351
+ subCommands: { remote: remoteCommand }
352
+ })
353
+ ```
354
+
355
+ For more details on nested sub-commands, including lazy loading, intermediate command handling, and `commandPath`, see the [Nested Sub-Commands](../advanced/nested-sub-commands.md) guide.
356
+
322
357
  ## Next Steps
323
358
 
324
359
  Throughout this guide, you've learned how to build composable sub-commands that scale from simple to complex CLI applications.