@guanghechen/commander 4.5.1 → 4.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add explicit runtime entry points for browser and node, and align command runtime abstractions and
8
+ tests.
9
+
10
+ ## 4.6.0
11
+
12
+ ### Minor Changes
13
+
14
+ - Add preset-root and command-level preset resolution for commander, align control/preset pipeline
15
+ behavior with spec, and include updated preset parsing and validation semantics.
16
+
3
17
  ## 4.5.1
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -52,6 +52,8 @@
52
52
  A minimal, type-safe command-line interface builder with fluent API. Supports subcommands, option
53
53
  parsing, shell completion generation (bash, fish, pwsh), and built-in help/version handling.
54
54
 
55
+ `opts` / `args` are designed for strong type inference from the current command's own declarations.
56
+
55
57
  ## Install
56
58
 
57
59
  - npm
@@ -71,12 +73,12 @@ parsing, shell completion generation (bash, fish, pwsh), and built-in help/versi
71
73
  ### Basic Command
72
74
 
73
75
  ```typescript
74
- import { Command } from '@guanghechen/commander'
76
+ import { Command } from '@guanghechen/commander/browser'
75
77
 
76
78
  const cli = new Command({
77
79
  name: 'mycli',
78
80
  version: '1.0.0',
79
- description: 'My awesome CLI tool',
81
+ desc: 'My awesome CLI tool',
80
82
  })
81
83
 
82
84
  cli
@@ -84,25 +86,27 @@ cli
84
86
  long: 'verbose',
85
87
  short: 'v',
86
88
  type: 'boolean',
87
- description: 'Enable verbose output',
89
+ args: 'none',
90
+ desc: 'Enable verbose output',
88
91
  })
89
92
  .option({
90
93
  long: 'output',
91
94
  short: 'o',
92
95
  type: 'string',
93
- description: 'Output file path',
96
+ args: 'required',
97
+ desc: 'Output file path',
94
98
  default: './output.txt',
95
99
  })
96
100
  .argument({
97
101
  name: 'file',
98
102
  kind: 'required',
99
- description: 'Input file to process',
103
+ desc: 'Input file to process',
100
104
  })
101
105
  .action(({ opts, args, ctx }) => {
102
- const [file] = args
106
+ const file = String(args.file)
103
107
  ctx.reporter.info(`Processing ${file}...`)
104
- if (opts['verbose']) {
105
- ctx.reporter.debug(`Output: ${opts['output']}`)
108
+ if (opts.verbose) {
109
+ ctx.reporter.debug(`Output: ${opts.output}`)
106
110
  }
107
111
  })
108
112
 
@@ -115,30 +119,30 @@ cli.run({
115
119
  ### Subcommands
116
120
 
117
121
  ```typescript
118
- import { Command } from '@guanghechen/commander'
122
+ import { Command } from '@guanghechen/commander/browser'
119
123
 
120
124
  const root = new Command({
121
125
  name: 'git',
122
126
  version: '1.0.0',
123
- description: 'A simple git-like CLI',
127
+ desc: 'A simple git-like CLI',
124
128
  })
125
129
 
126
130
  const clone = new Command({
127
- description: 'Clone a repository',
131
+ desc: 'Clone a repository',
128
132
  })
129
- .argument({ name: 'url', kind: 'required', description: 'Repository URL' })
130
- .option({ long: 'depth', type: 'number', description: 'Shallow clone depth' })
133
+ .argument({ name: 'url', kind: 'required', desc: 'Repository URL' })
134
+ .option({ long: 'depth', type: 'number', args: 'required', desc: 'Shallow clone depth' })
131
135
  .action(({ args, opts }) => {
132
- console.log(`Cloning ${args[0]} with depth ${opts['depth'] ?? 'full'}`)
136
+ console.log(`Cloning ${args.url} with depth ${opts.depth ?? 'full'}`)
133
137
  })
134
138
 
135
139
  const commit = new Command({
136
- description: 'Record changes to the repository',
140
+ desc: 'Record changes to the repository',
137
141
  })
138
- .option({ long: 'message', short: 'm', type: 'string', required: true, description: 'Commit message' })
139
- .option({ long: 'amend', type: 'boolean', description: 'Amend previous commit' })
142
+ .option({ long: 'message', short: 'm', type: 'string', args: 'required', required: true, desc: 'Commit message' })
143
+ .option({ long: 'amend', type: 'boolean', args: 'none', desc: 'Amend previous commit' })
140
144
  .action(({ opts }) => {
141
- console.log(`Committing: ${opts['message']}`)
145
+ console.log(`Committing: ${opts.message}`)
142
146
  })
143
147
 
144
148
  root.subcommand('clone', clone).subcommand('commit', commit).subcommand('ci', commit)
@@ -149,12 +153,13 @@ root.run({ argv: process.argv.slice(2), envs: process.env })
149
153
  ### Shell Completion
150
154
 
151
155
  ```typescript
152
- import { Command, CompletionCommand } from '@guanghechen/commander'
156
+ import { Command } from '@guanghechen/commander/browser'
157
+ import { CompletionCommand } from '@guanghechen/commander/node'
153
158
 
154
159
  const root = new Command({
155
160
  name: 'mycli',
156
161
  version: '1.0.0',
157
- description: 'My CLI with completion support',
162
+ desc: 'My CLI with completion support',
158
163
  })
159
164
 
160
165
  // Add completion subcommand
@@ -169,43 +174,92 @@ root.subcommand('completion', new CompletionCommand(root))
169
174
  ### Option Types
170
175
 
171
176
  ```typescript
172
- import { Command } from '@guanghechen/commander'
177
+ import { Command } from '@guanghechen/commander/browser'
173
178
 
174
- new Command({ name: 'example', description: 'Option types demo' })
179
+ new Command({ name: 'example', desc: 'Option types demo' })
175
180
  // Boolean (flags)
176
- .option({ long: 'debug', type: 'boolean', description: 'Enable debug mode' })
181
+ .option({ long: 'debug', type: 'boolean', args: 'none', desc: 'Enable debug mode' })
177
182
 
178
183
  // String with choices
179
184
  .option({
180
185
  long: 'format',
181
186
  type: 'string',
187
+ args: 'required',
182
188
  choices: ['json', 'yaml', 'toml'],
183
189
  default: 'json',
184
- description: 'Output format'
190
+ desc: 'Output format'
185
191
  })
186
192
 
187
193
  // Number
188
- .option({ long: 'port', type: 'number', default: 3000, description: 'Server port' })
194
+ .option({ long: 'port', type: 'number', args: 'required', default: 3000, desc: 'Server port' })
189
195
 
190
- // Array (can be specified multiple times)
191
- .option({ long: 'include', type: 'string[]', description: 'Files to include' })
196
+ // Array (generated by variadic args, not a standalone type)
197
+ .option({ long: 'include', type: 'string', args: 'variadic', desc: 'Files to include' })
192
198
 
193
199
  // Required option
194
- .option({ long: 'config', type: 'string', required: true, description: 'Config file' })
200
+ .option({ long: 'config', type: 'string', args: 'required', required: true, desc: 'Config file' })
195
201
 
196
202
  // Custom coercion
197
203
  .option({
198
204
  long: 'date',
199
205
  type: 'string',
206
+ args: 'required',
200
207
  coerce: (value) => new Date(value),
201
- description: 'Date value',
208
+ desc: 'Date value',
202
209
  })
203
210
  ```
204
211
 
212
+ ### Preset Input Files
213
+
214
+ `--preset-opts=<file>` and `--preset-envs=<file>` allow injecting preset argv and
215
+ env inputs before normal CLI parsing.
216
+
217
+ ```bash
218
+ mycli --preset-opts=./options.argv --preset-envs=./preset.env --log-level debug --color
219
+ ```
220
+
221
+ Behavior:
222
+
223
+ 1. Route command chain from user argv (name/alias only, no argv rewrite), then store route tokens in `sources.user.cmds`.
224
+ 2. Run `control-scan` on user tail argv before preset merge: detect `--help` / `--version` by token scan (`--version` only when `supportsBuiltinVersion(leaf)`), detect `help` only when it is the first tail token, write `ctx.controls`, and strip control tokens from parse input.
225
+ 3. In `run()`, execute `run-control` before preset merge: short-circuit by `help > version`. If short-circuit hits, preset files are not loaded.
226
+ 4. Scan preset directives before `--`, remove them from control-tail argv, and store cleaned tokens in `sources.user.argv`.
227
+ 5. Read options preset file(s) and tokenize by whitespace to `sources.preset.argv`.
228
+ 6. Read env preset file(s) and parse via `@guanghechen/env.parse` to `sources.preset.envs`.
229
+ 7. Build `effectiveTailArgv = [...sources.preset.argv, ...sources.user.argv]`.
230
+ 8. Build `ctx.envs = { ...sources.user.envs, ...sources.preset.envs }`.
231
+ 9. Expose source snapshots through `ctx.sources` and reuse existing tokenize/resolve/parse pipeline.
232
+
233
+ Precedence for same option key:
234
+
235
+ 1. User CLI tokens (highest)
236
+ 2. Tokens loaded from `--preset-opts`
237
+ 3. Option `default` / implicit defaults
238
+ 4. `NO_COLOR` fallback for color rendering only (applies only when no explicit `--color/--no-color` token appears)
239
+
240
+ Precedence for same env key:
241
+
242
+ 1. Key-values loaded from `--preset-envs` (highest)
243
+ 2. User envs (e.g. `process.env`)
244
+
245
+ Additional notes:
246
+
247
+ 1. `variadic` options append in appearance order.
248
+ 2. `NO_COLOR` is evaluated from `ctx.envs` and remains a fallback only when no color token is explicitly provided.
249
+ 3. The `--preset-opts` file is expected to contain option fragments (`-x`/`--xxx` and their values), not command-route tokens.
250
+ 4. The `--preset-envs` file must be parseable by `@guanghechen/env`.
251
+ 5. Only preset flags before `--` are processed; after `--` they are treated as normal args.
252
+ 6. Repeated preset flags are processed in appearance order.
253
+ 7. Built-in control semantics recognize `--help` / `help` / `--version` only (no short aliases).
254
+ 8. `long: 'help'` and `long: 'version'` are reserved and must not be user-defined in `.option()`.
255
+ 9. `--help` / `help` / `--version` are forbidden in `--preset-opts` files; loading should fail fast.
256
+ 10. `--` is forbidden inside `--preset-opts` files; loading should fail fast.
257
+ 11. `parse()` never executes control handlers; it only records control hits in `ctx.controls`.
258
+
205
259
  ### Built-in Coerce Factories
206
260
 
207
261
  ```typescript
208
- import { Coerce, Command } from '@guanghechen/commander'
262
+ import { Coerce, Command } from '@guanghechen/commander/browser'
209
263
 
210
264
  new Command({ name: 'example', desc: 'Coerce demo' })
211
265
  .option({
@@ -284,7 +338,7 @@ You can still override the message via `Coerce.xxx(name, 'custom error message')
284
338
  ### Built-in Is Helpers
285
339
 
286
340
  ```typescript
287
- import { isDomain, isIp, isIpv4, isIpv6 } from '@guanghechen/commander'
341
+ import { isDomain, isIp, isIpv4, isIpv6 } from '@guanghechen/commander/browser'
288
342
 
289
343
  isIpv4('127.0.0.1') // true
290
344
  isIpv6('::1') // true
@@ -295,7 +349,7 @@ isDomain('example.com') // true
295
349
  ### Help Examples
296
350
 
297
351
  ```typescript
298
- import { Command } from '@guanghechen/commander'
352
+ import { Command } from '@guanghechen/commander/browser'
299
353
 
300
354
  const cli = new Command({ name: 'mycli', desc: 'My CLI tool' })
301
355