@guanghechen/commander 4.5.0 → 4.6.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.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add preset-root and command-level preset resolution for commander, align control/preset pipeline
8
+ behavior with spec, and include updated preset parsing and validation semantics.
9
+
10
+ ## 4.5.1
11
+
12
+ ### Patch Changes
13
+
14
+ - Add built-in host and network validators (ip, domain, host), expose is helpers, and extend coerce
15
+ factories with port and choice support.
16
+
3
17
  ## 4.5.0
4
18
 
5
19
  ### Minor 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
@@ -76,7 +78,7 @@ import { Command } from '@guanghechen/commander'
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
 
@@ -120,25 +124,25 @@ import { Command } from '@guanghechen/commander'
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)
@@ -154,7 +158,7 @@ import { Command, CompletionCommand } from '@guanghechen/commander'
154
158
  const root = new Command({
155
159
  name: 'mycli',
156
160
  version: '1.0.0',
157
- description: 'My CLI with completion support',
161
+ desc: 'My CLI with completion support',
158
162
  })
159
163
 
160
164
  // Add completion subcommand
@@ -171,37 +175,89 @@ root.subcommand('completion', new CompletionCommand(root))
171
175
  ```typescript
172
176
  import { Command } from '@guanghechen/commander'
173
177
 
174
- new Command({ name: 'example', description: 'Option types demo' })
178
+ new Command({ name: 'example', desc: 'Option types demo' })
175
179
  // Boolean (flags)
176
- .option({ long: 'debug', type: 'boolean', description: 'Enable debug mode' })
180
+ .option({ long: 'debug', type: 'boolean', args: 'none', desc: 'Enable debug mode' })
177
181
 
178
182
  // String with choices
179
183
  .option({
180
184
  long: 'format',
181
185
  type: 'string',
186
+ args: 'required',
182
187
  choices: ['json', 'yaml', 'toml'],
183
188
  default: 'json',
184
- description: 'Output format'
189
+ desc: 'Output format'
185
190
  })
186
191
 
187
192
  // Number
188
- .option({ long: 'port', type: 'number', default: 3000, description: 'Server port' })
193
+ .option({ long: 'port', type: 'number', args: 'required', default: 3000, desc: 'Server port' })
189
194
 
190
- // Array (can be specified multiple times)
191
- .option({ long: 'include', type: 'string[]', description: 'Files to include' })
195
+ // Array (generated by variadic args, not a standalone type)
196
+ .option({ long: 'include', type: 'string', args: 'variadic', desc: 'Files to include' })
192
197
 
193
198
  // Required option
194
- .option({ long: 'config', type: 'string', required: true, description: 'Config file' })
199
+ .option({ long: 'config', type: 'string', args: 'required', required: true, desc: 'Config file' })
195
200
 
196
201
  // Custom coercion
197
202
  .option({
198
203
  long: 'date',
199
204
  type: 'string',
205
+ args: 'required',
200
206
  coerce: (value) => new Date(value),
201
- description: 'Date value',
207
+ desc: 'Date value',
202
208
  })
203
209
  ```
204
210
 
211
+ ### Planned: Preset Input Files
212
+
213
+ > This is a proposed feature and not released yet.
214
+ > README is an overview only. Authoritative semantics are defined in `packages/commander/spec/*.md`.
215
+
216
+ The proposed `--preset-opts=<file>` and `--preset-envs=<file>` flags allow injecting preset argv and
217
+ env inputs before normal CLI parsing.
218
+
219
+ ```bash
220
+ mycli --preset-opts=./options.argv --preset-envs=./preset.env --log-level debug --color
221
+ ```
222
+
223
+ Proposed behavior:
224
+
225
+ 1. Route command chain from user argv (name/alias only, no argv rewrite), then store route tokens in `sources.user.cmds`.
226
+ 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.
227
+ 3. In `run()`, execute `run-control` before preset merge: short-circuit by `help > version`. If short-circuit hits, preset files are not loaded.
228
+ 4. Scan preset directives before `--`, remove them from control-tail argv, and store cleaned tokens in `sources.user.argv`.
229
+ 5. Read options preset file(s) and tokenize by whitespace to `sources.preset.argv`.
230
+ 6. Read env preset file(s) and parse via `@guanghechen/env.parse` to `sources.preset.envs`.
231
+ 7. Build `effectiveTailArgv = [...sources.preset.argv, ...sources.user.argv]`.
232
+ 8. Build `ctx.envs = { ...sources.user.envs, ...sources.preset.envs }`.
233
+ 9. Expose source snapshots through `ctx.sources` and reuse existing tokenize/resolve/parse pipeline.
234
+
235
+ Proposed precedence for same option key:
236
+
237
+ 1. User CLI tokens (highest)
238
+ 2. Tokens loaded from `--preset-opts`
239
+ 3. Option `default` / implicit defaults
240
+ 4. `NO_COLOR` fallback for color rendering only (applies only when no explicit `--color/--no-color` token appears)
241
+
242
+ Proposed precedence for same env key:
243
+
244
+ 1. Key-values loaded from `--preset-envs` (highest)
245
+ 2. User envs (e.g. `process.env`)
246
+
247
+ Additional notes:
248
+
249
+ 1. `variadic` options append in appearance order.
250
+ 2. `NO_COLOR` is evaluated from `ctx.envs` and remains a fallback only when no color token is explicitly provided.
251
+ 3. The `--preset-opts` file is expected to contain option fragments (`-x`/`--xxx` and their values), not command-route tokens.
252
+ 4. The `--preset-envs` file must be parseable by `@guanghechen/env`.
253
+ 5. Only preset flags before `--` are processed; after `--` they are treated as normal args.
254
+ 6. Repeated preset flags are processed in appearance order.
255
+ 7. Built-in control semantics recognize `--help` / `help` / `--version` only (no short aliases).
256
+ 8. `long: 'help'` and `long: 'version'` are reserved and must not be user-defined in `.option()`.
257
+ 9. `--help` / `help` / `--version` are forbidden in `--preset-opts` files; loading should fail fast.
258
+ 10. `--` is forbidden inside `--preset-opts` files; loading should fail fast.
259
+ 11. `parse()` never executes control handlers; it only records control hits in `ctx.controls`.
260
+
205
261
  ### Built-in Coerce Factories
206
262
 
207
263
  ```typescript
@@ -229,6 +285,41 @@ new Command({ name: 'example', desc: 'Coerce demo' })
229
285
  coerce: Coerce.positiveNumber('--duration'),
230
286
  desc: 'Duration in seconds',
231
287
  })
288
+ .option({
289
+ long: 'port',
290
+ type: 'number',
291
+ args: 'required',
292
+ coerce: Coerce.port('--port'),
293
+ desc: 'Server port',
294
+ })
295
+ .option({
296
+ long: 'domain',
297
+ type: 'string',
298
+ args: 'required',
299
+ coerce: Coerce.domain('--domain'),
300
+ desc: 'Domain name',
301
+ })
302
+ .option({
303
+ long: 'ip',
304
+ type: 'string',
305
+ args: 'required',
306
+ coerce: Coerce.ip('--ip'),
307
+ desc: 'IP address',
308
+ })
309
+ .option({
310
+ long: 'host',
311
+ type: 'string',
312
+ args: 'required',
313
+ coerce: Coerce.host('--host'),
314
+ desc: 'Host (IP or domain)',
315
+ })
316
+ .option({
317
+ long: 'mode',
318
+ type: 'string',
319
+ args: 'required',
320
+ coerce: Coerce.choice('--mode', ['dev', 'test', 'prod'] as const),
321
+ desc: 'Deploy mode',
322
+ })
232
323
  .option({
233
324
  long: 'scale',
234
325
  type: 'number',
@@ -246,6 +337,17 @@ Default error message format:
246
337
 
247
338
  You can still override the message via `Coerce.xxx(name, 'custom error message')`.
248
339
 
340
+ ### Built-in Is Helpers
341
+
342
+ ```typescript
343
+ import { isDomain, isIp, isIpv4, isIpv6 } from '@guanghechen/commander'
344
+
345
+ isIpv4('127.0.0.1') // true
346
+ isIpv6('::1') // true
347
+ isIp('2001:db8::1') // true
348
+ isDomain('example.com') // true
349
+ ```
350
+
249
351
  ### Help Examples
250
352
 
251
353
  ```typescript
@@ -263,11 +365,11 @@ await cli.run({ argv: ['--help'], envs: process.env })
263
365
 
264
366
  `usage` 是相对当前 command path 的片段,help 中会自动补齐前缀,例如 `mycli build --watch`。
265
367
 
266
- `--color` / `--no-color` 仅控制 help 文本的终端着色;
267
- `--log-colorful` / `--no-log-colorful` 控制 `Reporter` 的日志着色。
368
+ `--color` / `--no-color` 仅控制 help 文本的终端着色; `--log-colorful` / `--no-log-colorful` 控制
369
+ `Reporter` 的日志着色。
268
370
 
269
- 当环境变量 `NO_COLOR` 存在时,help 渲染默认视为 `--no-color`;
270
- 显式传入 `--color` 可以覆盖这个默认值。
371
+ 当环境变量 `NO_COLOR` 存在时,help 渲染默认视为 `--no-color`;显式传入 `--color`
372
+ 可以覆盖这个默认值。
271
373
 
272
374
  ## Reference
273
375