@optique/core 0.10.0-dev.370 → 0.10.0-dev.375

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/dist/facade.cjs CHANGED
@@ -113,10 +113,7 @@ function createCompletionParser(mode, programName, availableShells, name = "both
113
113
  };
114
114
  }
115
115
  }
116
- /**
117
- * Systematically combines the original parser with help, version, and completion parsers.
118
- */
119
- function combineWithHelpVersion(originalParser, helpParsers, versionParsers, completionParsers) {
116
+ function combineWithHelpVersion(originalParser, helpParsers, versionParsers, completionParsers, groups) {
120
117
  const parsers = [];
121
118
  if (helpParsers.helpOption) {
122
119
  const lenientHelpParser = {
@@ -262,35 +259,61 @@ function combineWithHelpVersion(originalParser, helpParsers, versionParsers, com
262
259
  };
263
260
  parsers.push(lenientVersionParser);
264
261
  }
265
- if (versionParsers.versionCommand) parsers.push(require_constructs.object({
266
- help: require_primitives.constant(false),
267
- version: require_primitives.constant(true),
268
- completion: require_primitives.constant(false),
269
- result: versionParsers.versionCommand,
270
- helpFlag: helpParsers.helpOption ? require_modifiers.optional(helpParsers.helpOption) : require_primitives.constant(false)
271
- }));
272
- if (completionParsers.completionCommand) parsers.push(require_constructs.object({
273
- help: require_primitives.constant(false),
274
- version: require_primitives.constant(false),
275
- completion: require_primitives.constant(true),
276
- completionData: completionParsers.completionCommand,
277
- helpFlag: helpParsers.helpOption ? require_modifiers.optional(helpParsers.helpOption) : require_primitives.constant(false)
278
- }));
279
- if (helpParsers.helpCommand) parsers.push(require_constructs.object({
280
- help: require_primitives.constant(true),
281
- version: require_primitives.constant(false),
282
- completion: require_primitives.constant(false),
283
- commands: helpParsers.helpCommand
284
- }));
262
+ if (versionParsers.versionCommand) {
263
+ const versionParser = require_constructs.object({
264
+ help: require_primitives.constant(false),
265
+ version: require_primitives.constant(true),
266
+ completion: require_primitives.constant(false),
267
+ result: versionParsers.versionCommand,
268
+ helpFlag: helpParsers.helpOption ? require_modifiers.optional(helpParsers.helpOption) : require_primitives.constant(false)
269
+ });
270
+ parsers.push(groups?.versionGroup ? require_constructs.group(groups.versionGroup, versionParser) : versionParser);
271
+ }
272
+ if (completionParsers.completionCommand) {
273
+ const completionParser = require_constructs.object({
274
+ help: require_primitives.constant(false),
275
+ version: require_primitives.constant(false),
276
+ completion: require_primitives.constant(true),
277
+ completionData: completionParsers.completionCommand,
278
+ helpFlag: helpParsers.helpOption ? require_modifiers.optional(helpParsers.helpOption) : require_primitives.constant(false)
279
+ });
280
+ parsers.push(groups?.completionGroup ? require_constructs.group(groups.completionGroup, completionParser) : completionParser);
281
+ }
282
+ if (helpParsers.helpCommand) {
283
+ const helpParser = require_constructs.object({
284
+ help: require_primitives.constant(true),
285
+ version: require_primitives.constant(false),
286
+ completion: require_primitives.constant(false),
287
+ commands: helpParsers.helpCommand
288
+ });
289
+ parsers.push(groups?.helpGroup ? require_constructs.group(groups.helpGroup, helpParser) : helpParser);
290
+ }
285
291
  parsers.push(require_constructs.object({
286
292
  help: require_primitives.constant(false),
287
293
  version: require_primitives.constant(false),
288
294
  completion: require_primitives.constant(false),
289
295
  result: originalParser
290
296
  }));
297
+ const mainParserIndex = parsers.length - 1;
291
298
  if (parsers.length === 1) return parsers[0];
292
- else if (parsers.length === 2) return require_constructs.longestMatch(parsers[0], parsers[1]);
293
- else return require_constructs.longestMatch(...parsers);
299
+ let combined;
300
+ if (parsers.length === 2) combined = require_constructs.longestMatch(parsers[0], parsers[1]);
301
+ else combined = require_constructs.longestMatch(...parsers);
302
+ const topUsage = combined.usage[0];
303
+ if (topUsage?.type === "exclusive" && mainParserIndex > 0) {
304
+ const terms = [...topUsage.terms];
305
+ const [mainTerm] = terms.splice(mainParserIndex, 1);
306
+ const lenientCount = (helpParsers.helpOption ? 1 : 0) + (versionParsers.versionOption ? 1 : 0);
307
+ terms.splice(lenientCount, 0, mainTerm);
308
+ combined = {
309
+ ...combined,
310
+ usage: [{
311
+ ...topUsage,
312
+ terms
313
+ }]
314
+ };
315
+ }
316
+ return combined;
294
317
  }
295
318
  /**
296
319
  * Classifies the parsing result into a discriminated union for cleaner handling.
@@ -439,13 +462,16 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
439
462
  }, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
440
463
  const helpMode = options.help?.mode ?? "option";
441
464
  const onHelp = options.help?.onShow ?? (() => ({}));
465
+ const helpGroup = options.help?.group;
442
466
  const versionMode = options.version?.mode ?? "option";
443
467
  const versionValue = options.version?.value ?? "";
444
468
  const onVersion = options.version?.onShow ?? (() => ({}));
469
+ const versionGroup = options.version?.group;
445
470
  const completionMode = options.completion?.mode ?? "both";
446
471
  const completionName = options.completion?.name ?? "both";
447
472
  const completionHelpVisibility = options.completion?.helpVisibility ?? completionName;
448
473
  const onCompletion = options.completion?.onShow ?? (() => ({}));
474
+ const completionGroup = options.completion?.group;
449
475
  const defaultShells = {
450
476
  bash: require_completion.bash,
451
477
  fish: require_completion.fish,
@@ -494,7 +520,11 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
494
520
  }
495
521
  }
496
522
  }
497
- const augmentedParser = help === "none" && version === "none" && completion === "none" ? parser : combineWithHelpVersion(parser, helpParsers, versionParsers, completionParsers);
523
+ const augmentedParser = help === "none" && version === "none" && completion === "none" ? parser : combineWithHelpVersion(parser, helpParsers, versionParsers, completionParsers, {
524
+ helpGroup,
525
+ versionGroup,
526
+ completionGroup
527
+ });
498
528
  const handleResult = (result) => {
499
529
  const classified = classifyResult(result, args);
500
530
  switch (classified.type) {
@@ -518,15 +548,18 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
518
548
  else if (requestedCommand === "version" && versionAsCommand && versionParsers.versionCommand) helpGeneratorParser = versionParsers.versionCommand;
519
549
  else {
520
550
  const commandParsers = [parser];
521
- if (helpAsCommand) {
522
- if (helpParsers.helpCommand) commandParsers.push(helpParsers.helpCommand);
523
- }
524
- if (versionAsCommand) {
525
- if (versionParsers.versionCommand) commandParsers.push(versionParsers.versionCommand);
526
- }
527
- if (completionAsCommand) {
528
- if (completionParsers.completionCommand) commandParsers.push(completionParsers.completionCommand);
529
- }
551
+ const groupedMeta = {};
552
+ const ungroupedMeta = [];
553
+ const addMeta = (p, groupLabel) => {
554
+ if (groupLabel) (groupedMeta[groupLabel] ??= []).push(p);
555
+ else ungroupedMeta.push(p);
556
+ };
557
+ if (helpAsCommand && helpParsers.helpCommand) addMeta(helpParsers.helpCommand, helpGroup);
558
+ if (versionAsCommand && versionParsers.versionCommand) addMeta(versionParsers.versionCommand, versionGroup);
559
+ if (completionAsCommand && completionParsers.completionCommand) addMeta(completionParsers.completionCommand, completionGroup);
560
+ commandParsers.push(...ungroupedMeta);
561
+ for (const [label, parsers] of Object.entries(groupedMeta)) if (parsers.length === 1) commandParsers.push(require_constructs.group(label, parsers[0]));
562
+ else commandParsers.push(require_constructs.group(label, require_constructs.longestMatch(...parsers)));
530
563
  if (commandParsers.length === 1) helpGeneratorParser = commandParsers[0];
531
564
  else if (commandParsers.length === 2) helpGeneratorParser = require_constructs.longestMatch(commandParsers[0], commandParsers[1]);
532
565
  else helpGeneratorParser = require_constructs.longestMatch(...commandParsers);
package/dist/facade.d.cts CHANGED
@@ -24,30 +24,51 @@ type CompletionConfigBase<THelp> = {
24
24
  * Determines how completion is made available:
25
25
  *
26
26
  * - `"command"`: Only the `completion` subcommand is available
27
- * - `"option"`: Only the `--completion` option is available
28
- * - `"both"`: Both `completion` subcommand and `--completion` option are available
27
+ * - `"both"`: Both `completion` subcommand and `--completion` option
28
+ * are available
29
29
  *
30
30
  * @default `"both"`
31
31
  */
32
- readonly mode?: "command" | "option" | "both";
32
+ readonly mode?: "command" | "both";
33
+ /**
34
+ * Group label for the completion command in help output. When specified,
35
+ * the completion command appears under a titled section with this name
36
+ * instead of alongside user-defined commands.
37
+ *
38
+ * @since 0.10.0
39
+ */
40
+ readonly group?: string;
33
41
  /**
34
42
  * Available shell completions. By default, includes `bash`, `fish`, `nu`,
35
- * `pwsh`, and `zsh`. You can provide additional custom shell completions or
36
- * override the defaults.
43
+ * `pwsh`, and `zsh`. You can provide additional custom shell completions
44
+ * or override the defaults.
37
45
  *
38
46
  * @default `{ bash, fish, nu, pwsh, zsh }`
39
47
  */
40
48
  readonly shells?: Record<string, ShellCompletion>;
41
49
  /**
42
- * Callback function invoked when completion is requested. The function can
43
- * optionally receive an exit code parameter.
50
+ * Callback function invoked when completion is requested. The function
51
+ * can optionally receive an exit code parameter.
44
52
  *
45
- * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
46
- * on Deno to this option.
53
+ * You usually want to pass `process.exit` on Node.js or Bun and
54
+ * `Deno.exit` on Deno to this option.
47
55
  *
48
56
  * @default Returns `void` when completion is shown.
49
57
  */
50
58
  readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
59
+ } | {
60
+ /**
61
+ * Determines how completion is made available:
62
+ *
63
+ * - `"option"`: Only the `--completion` option is available
64
+ */
65
+ readonly mode: "option";
66
+ /** @since 0.10.0 */
67
+ readonly group?: never;
68
+ /** @default `{ bash, fish, nu, pwsh, zsh }` */
69
+ readonly shells?: Record<string, ShellCompletion>;
70
+ /** @default Returns `void` when completion is shown. */
71
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
51
72
  };
52
73
  type CompletionConfigBoth<THelp> = CompletionConfigBase<THelp> & {
53
74
  /**
@@ -135,18 +156,46 @@ interface RunOptions<THelp, TError> {
135
156
  * Determines how help is made available:
136
157
  *
137
158
  * - `"command"`: Only the `help` subcommand is available
138
- * - `"option"`: Only the `--help` option is available
139
159
  * - `"both"`: Both `help` subcommand and `--help` option are available
140
160
  *
141
161
  * @default `"option"`
142
162
  */
143
- readonly mode?: "command" | "option" | "both";
163
+ readonly mode: "command" | "both";
164
+ /**
165
+ * Group label for the help command in help output. When specified,
166
+ * the help command appears under a titled section with this name
167
+ * instead of alongside user-defined commands.
168
+ *
169
+ * @since 0.10.0
170
+ */
171
+ readonly group?: string;
144
172
  /**
145
173
  * Callback function invoked when help is requested. The function can
146
174
  * optionally receive an exit code parameter.
147
175
  *
148
- * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
149
- * on Deno to this option.
176
+ * You usually want to pass `process.exit` on Node.js or Bun and
177
+ * `Deno.exit` on Deno to this option.
178
+ *
179
+ * @default Returns `void` when help is shown.
180
+ */
181
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
182
+ } | {
183
+ /**
184
+ * Determines how help is made available:
185
+ *
186
+ * - `"option"`: Only the `--help` option is available
187
+ *
188
+ * @default `"option"`
189
+ */
190
+ readonly mode?: "option";
191
+ /** @since 0.10.0 */
192
+ readonly group?: never;
193
+ /**
194
+ * Callback function invoked when help is requested. The function can
195
+ * optionally receive an exit code parameter.
196
+ *
197
+ * You usually want to pass `process.exit` on Node.js or Bun and
198
+ * `Deno.exit` on Deno to this option.
150
199
  *
151
200
  * @default Returns `void` when help is shown.
152
201
  */
@@ -160,22 +209,55 @@ interface RunOptions<THelp, TError> {
160
209
  * Determines how version is made available:
161
210
  *
162
211
  * - `"command"`: Only the `version` subcommand is available
212
+ * - `"both"`: Both `version` subcommand and `--version` option are
213
+ * available
214
+ *
215
+ * @default `"option"`
216
+ */
217
+ readonly mode: "command" | "both";
218
+ /**
219
+ * The version string to display when version is requested.
220
+ */
221
+ readonly value: string;
222
+ /**
223
+ * Group label for the version command in help output. When specified,
224
+ * the version command appears under a titled section with this name
225
+ * instead of alongside user-defined commands.
226
+ *
227
+ * @since 0.10.0
228
+ */
229
+ readonly group?: string;
230
+ /**
231
+ * Callback function invoked when version is requested. The function can
232
+ * optionally receive an exit code parameter.
233
+ *
234
+ * You usually want to pass `process.exit` on Node.js or Bun and
235
+ * `Deno.exit` on Deno to this option.
236
+ *
237
+ * @default Returns `void` when version is shown.
238
+ */
239
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
240
+ } | {
241
+ /**
242
+ * Determines how version is made available:
243
+ *
163
244
  * - `"option"`: Only the `--version` option is available
164
- * - `"both"`: Both `version` subcommand and `--version` option are available
165
245
  *
166
246
  * @default `"option"`
167
247
  */
168
- readonly mode?: "command" | "option" | "both";
248
+ readonly mode?: "option";
169
249
  /**
170
250
  * The version string to display when version is requested.
171
251
  */
172
252
  readonly value: string;
253
+ /** @since 0.10.0 */
254
+ readonly group?: never;
173
255
  /**
174
256
  * Callback function invoked when version is requested. The function can
175
257
  * optionally receive an exit code parameter.
176
258
  *
177
- * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
178
- * on Deno to this option.
259
+ * You usually want to pass `process.exit` on Node.js or Bun and
260
+ * `Deno.exit` on Deno to this option.
179
261
  *
180
262
  * @default Returns `void` when version is shown.
181
263
  */
package/dist/facade.d.ts CHANGED
@@ -24,30 +24,51 @@ type CompletionConfigBase<THelp> = {
24
24
  * Determines how completion is made available:
25
25
  *
26
26
  * - `"command"`: Only the `completion` subcommand is available
27
- * - `"option"`: Only the `--completion` option is available
28
- * - `"both"`: Both `completion` subcommand and `--completion` option are available
27
+ * - `"both"`: Both `completion` subcommand and `--completion` option
28
+ * are available
29
29
  *
30
30
  * @default `"both"`
31
31
  */
32
- readonly mode?: "command" | "option" | "both";
32
+ readonly mode?: "command" | "both";
33
+ /**
34
+ * Group label for the completion command in help output. When specified,
35
+ * the completion command appears under a titled section with this name
36
+ * instead of alongside user-defined commands.
37
+ *
38
+ * @since 0.10.0
39
+ */
40
+ readonly group?: string;
33
41
  /**
34
42
  * Available shell completions. By default, includes `bash`, `fish`, `nu`,
35
- * `pwsh`, and `zsh`. You can provide additional custom shell completions or
36
- * override the defaults.
43
+ * `pwsh`, and `zsh`. You can provide additional custom shell completions
44
+ * or override the defaults.
37
45
  *
38
46
  * @default `{ bash, fish, nu, pwsh, zsh }`
39
47
  */
40
48
  readonly shells?: Record<string, ShellCompletion>;
41
49
  /**
42
- * Callback function invoked when completion is requested. The function can
43
- * optionally receive an exit code parameter.
50
+ * Callback function invoked when completion is requested. The function
51
+ * can optionally receive an exit code parameter.
44
52
  *
45
- * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
46
- * on Deno to this option.
53
+ * You usually want to pass `process.exit` on Node.js or Bun and
54
+ * `Deno.exit` on Deno to this option.
47
55
  *
48
56
  * @default Returns `void` when completion is shown.
49
57
  */
50
58
  readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
59
+ } | {
60
+ /**
61
+ * Determines how completion is made available:
62
+ *
63
+ * - `"option"`: Only the `--completion` option is available
64
+ */
65
+ readonly mode: "option";
66
+ /** @since 0.10.0 */
67
+ readonly group?: never;
68
+ /** @default `{ bash, fish, nu, pwsh, zsh }` */
69
+ readonly shells?: Record<string, ShellCompletion>;
70
+ /** @default Returns `void` when completion is shown. */
71
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
51
72
  };
52
73
  type CompletionConfigBoth<THelp> = CompletionConfigBase<THelp> & {
53
74
  /**
@@ -135,18 +156,46 @@ interface RunOptions<THelp, TError> {
135
156
  * Determines how help is made available:
136
157
  *
137
158
  * - `"command"`: Only the `help` subcommand is available
138
- * - `"option"`: Only the `--help` option is available
139
159
  * - `"both"`: Both `help` subcommand and `--help` option are available
140
160
  *
141
161
  * @default `"option"`
142
162
  */
143
- readonly mode?: "command" | "option" | "both";
163
+ readonly mode: "command" | "both";
164
+ /**
165
+ * Group label for the help command in help output. When specified,
166
+ * the help command appears under a titled section with this name
167
+ * instead of alongside user-defined commands.
168
+ *
169
+ * @since 0.10.0
170
+ */
171
+ readonly group?: string;
144
172
  /**
145
173
  * Callback function invoked when help is requested. The function can
146
174
  * optionally receive an exit code parameter.
147
175
  *
148
- * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
149
- * on Deno to this option.
176
+ * You usually want to pass `process.exit` on Node.js or Bun and
177
+ * `Deno.exit` on Deno to this option.
178
+ *
179
+ * @default Returns `void` when help is shown.
180
+ */
181
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
182
+ } | {
183
+ /**
184
+ * Determines how help is made available:
185
+ *
186
+ * - `"option"`: Only the `--help` option is available
187
+ *
188
+ * @default `"option"`
189
+ */
190
+ readonly mode?: "option";
191
+ /** @since 0.10.0 */
192
+ readonly group?: never;
193
+ /**
194
+ * Callback function invoked when help is requested. The function can
195
+ * optionally receive an exit code parameter.
196
+ *
197
+ * You usually want to pass `process.exit` on Node.js or Bun and
198
+ * `Deno.exit` on Deno to this option.
150
199
  *
151
200
  * @default Returns `void` when help is shown.
152
201
  */
@@ -160,22 +209,55 @@ interface RunOptions<THelp, TError> {
160
209
  * Determines how version is made available:
161
210
  *
162
211
  * - `"command"`: Only the `version` subcommand is available
212
+ * - `"both"`: Both `version` subcommand and `--version` option are
213
+ * available
214
+ *
215
+ * @default `"option"`
216
+ */
217
+ readonly mode: "command" | "both";
218
+ /**
219
+ * The version string to display when version is requested.
220
+ */
221
+ readonly value: string;
222
+ /**
223
+ * Group label for the version command in help output. When specified,
224
+ * the version command appears under a titled section with this name
225
+ * instead of alongside user-defined commands.
226
+ *
227
+ * @since 0.10.0
228
+ */
229
+ readonly group?: string;
230
+ /**
231
+ * Callback function invoked when version is requested. The function can
232
+ * optionally receive an exit code parameter.
233
+ *
234
+ * You usually want to pass `process.exit` on Node.js or Bun and
235
+ * `Deno.exit` on Deno to this option.
236
+ *
237
+ * @default Returns `void` when version is shown.
238
+ */
239
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
240
+ } | {
241
+ /**
242
+ * Determines how version is made available:
243
+ *
163
244
  * - `"option"`: Only the `--version` option is available
164
- * - `"both"`: Both `version` subcommand and `--version` option are available
165
245
  *
166
246
  * @default `"option"`
167
247
  */
168
- readonly mode?: "command" | "option" | "both";
248
+ readonly mode?: "option";
169
249
  /**
170
250
  * The version string to display when version is requested.
171
251
  */
172
252
  readonly value: string;
253
+ /** @since 0.10.0 */
254
+ readonly group?: never;
173
255
  /**
174
256
  * Callback function invoked when version is requested. The function can
175
257
  * optionally receive an exit code parameter.
176
258
  *
177
- * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
178
- * on Deno to this option.
259
+ * You usually want to pass `process.exit` on Node.js or Bun and
260
+ * `Deno.exit` on Deno to this option.
179
261
  *
180
262
  * @default Returns `void` when version is shown.
181
263
  */
package/dist/facade.js CHANGED
@@ -2,7 +2,7 @@ import { annotationKey } from "./annotations.js";
2
2
  import { commandLine, formatMessage, lineBreak, message, optionName, text, value } from "./message.js";
3
3
  import { bash, fish, nu, pwsh, zsh } from "./completion.js";
4
4
  import { formatUsage } from "./usage.js";
5
- import { longestMatch, object } from "./constructs.js";
5
+ import { group, longestMatch, object } from "./constructs.js";
6
6
  import { formatDocPage } from "./doc.js";
7
7
  import { multiple, optional, withDefault } from "./modifiers.js";
8
8
  import { string } from "./valueparser.js";
@@ -113,10 +113,7 @@ function createCompletionParser(mode, programName, availableShells, name = "both
113
113
  };
114
114
  }
115
115
  }
116
- /**
117
- * Systematically combines the original parser with help, version, and completion parsers.
118
- */
119
- function combineWithHelpVersion(originalParser, helpParsers, versionParsers, completionParsers) {
116
+ function combineWithHelpVersion(originalParser, helpParsers, versionParsers, completionParsers, groups) {
120
117
  const parsers = [];
121
118
  if (helpParsers.helpOption) {
122
119
  const lenientHelpParser = {
@@ -262,35 +259,61 @@ function combineWithHelpVersion(originalParser, helpParsers, versionParsers, com
262
259
  };
263
260
  parsers.push(lenientVersionParser);
264
261
  }
265
- if (versionParsers.versionCommand) parsers.push(object({
266
- help: constant(false),
267
- version: constant(true),
268
- completion: constant(false),
269
- result: versionParsers.versionCommand,
270
- helpFlag: helpParsers.helpOption ? optional(helpParsers.helpOption) : constant(false)
271
- }));
272
- if (completionParsers.completionCommand) parsers.push(object({
273
- help: constant(false),
274
- version: constant(false),
275
- completion: constant(true),
276
- completionData: completionParsers.completionCommand,
277
- helpFlag: helpParsers.helpOption ? optional(helpParsers.helpOption) : constant(false)
278
- }));
279
- if (helpParsers.helpCommand) parsers.push(object({
280
- help: constant(true),
281
- version: constant(false),
282
- completion: constant(false),
283
- commands: helpParsers.helpCommand
284
- }));
262
+ if (versionParsers.versionCommand) {
263
+ const versionParser = object({
264
+ help: constant(false),
265
+ version: constant(true),
266
+ completion: constant(false),
267
+ result: versionParsers.versionCommand,
268
+ helpFlag: helpParsers.helpOption ? optional(helpParsers.helpOption) : constant(false)
269
+ });
270
+ parsers.push(groups?.versionGroup ? group(groups.versionGroup, versionParser) : versionParser);
271
+ }
272
+ if (completionParsers.completionCommand) {
273
+ const completionParser = object({
274
+ help: constant(false),
275
+ version: constant(false),
276
+ completion: constant(true),
277
+ completionData: completionParsers.completionCommand,
278
+ helpFlag: helpParsers.helpOption ? optional(helpParsers.helpOption) : constant(false)
279
+ });
280
+ parsers.push(groups?.completionGroup ? group(groups.completionGroup, completionParser) : completionParser);
281
+ }
282
+ if (helpParsers.helpCommand) {
283
+ const helpParser = object({
284
+ help: constant(true),
285
+ version: constant(false),
286
+ completion: constant(false),
287
+ commands: helpParsers.helpCommand
288
+ });
289
+ parsers.push(groups?.helpGroup ? group(groups.helpGroup, helpParser) : helpParser);
290
+ }
285
291
  parsers.push(object({
286
292
  help: constant(false),
287
293
  version: constant(false),
288
294
  completion: constant(false),
289
295
  result: originalParser
290
296
  }));
297
+ const mainParserIndex = parsers.length - 1;
291
298
  if (parsers.length === 1) return parsers[0];
292
- else if (parsers.length === 2) return longestMatch(parsers[0], parsers[1]);
293
- else return longestMatch(...parsers);
299
+ let combined;
300
+ if (parsers.length === 2) combined = longestMatch(parsers[0], parsers[1]);
301
+ else combined = longestMatch(...parsers);
302
+ const topUsage = combined.usage[0];
303
+ if (topUsage?.type === "exclusive" && mainParserIndex > 0) {
304
+ const terms = [...topUsage.terms];
305
+ const [mainTerm] = terms.splice(mainParserIndex, 1);
306
+ const lenientCount = (helpParsers.helpOption ? 1 : 0) + (versionParsers.versionOption ? 1 : 0);
307
+ terms.splice(lenientCount, 0, mainTerm);
308
+ combined = {
309
+ ...combined,
310
+ usage: [{
311
+ ...topUsage,
312
+ terms
313
+ }]
314
+ };
315
+ }
316
+ return combined;
294
317
  }
295
318
  /**
296
319
  * Classifies the parsing result into a discriminated union for cleaner handling.
@@ -439,13 +462,16 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
439
462
  }, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
440
463
  const helpMode = options.help?.mode ?? "option";
441
464
  const onHelp = options.help?.onShow ?? (() => ({}));
465
+ const helpGroup = options.help?.group;
442
466
  const versionMode = options.version?.mode ?? "option";
443
467
  const versionValue = options.version?.value ?? "";
444
468
  const onVersion = options.version?.onShow ?? (() => ({}));
469
+ const versionGroup = options.version?.group;
445
470
  const completionMode = options.completion?.mode ?? "both";
446
471
  const completionName = options.completion?.name ?? "both";
447
472
  const completionHelpVisibility = options.completion?.helpVisibility ?? completionName;
448
473
  const onCompletion = options.completion?.onShow ?? (() => ({}));
474
+ const completionGroup = options.completion?.group;
449
475
  const defaultShells = {
450
476
  bash,
451
477
  fish,
@@ -494,7 +520,11 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
494
520
  }
495
521
  }
496
522
  }
497
- const augmentedParser = help === "none" && version === "none" && completion === "none" ? parser : combineWithHelpVersion(parser, helpParsers, versionParsers, completionParsers);
523
+ const augmentedParser = help === "none" && version === "none" && completion === "none" ? parser : combineWithHelpVersion(parser, helpParsers, versionParsers, completionParsers, {
524
+ helpGroup,
525
+ versionGroup,
526
+ completionGroup
527
+ });
498
528
  const handleResult = (result) => {
499
529
  const classified = classifyResult(result, args);
500
530
  switch (classified.type) {
@@ -518,15 +548,18 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
518
548
  else if (requestedCommand === "version" && versionAsCommand && versionParsers.versionCommand) helpGeneratorParser = versionParsers.versionCommand;
519
549
  else {
520
550
  const commandParsers = [parser];
521
- if (helpAsCommand) {
522
- if (helpParsers.helpCommand) commandParsers.push(helpParsers.helpCommand);
523
- }
524
- if (versionAsCommand) {
525
- if (versionParsers.versionCommand) commandParsers.push(versionParsers.versionCommand);
526
- }
527
- if (completionAsCommand) {
528
- if (completionParsers.completionCommand) commandParsers.push(completionParsers.completionCommand);
529
- }
551
+ const groupedMeta = {};
552
+ const ungroupedMeta = [];
553
+ const addMeta = (p, groupLabel) => {
554
+ if (groupLabel) (groupedMeta[groupLabel] ??= []).push(p);
555
+ else ungroupedMeta.push(p);
556
+ };
557
+ if (helpAsCommand && helpParsers.helpCommand) addMeta(helpParsers.helpCommand, helpGroup);
558
+ if (versionAsCommand && versionParsers.versionCommand) addMeta(versionParsers.versionCommand, versionGroup);
559
+ if (completionAsCommand && completionParsers.completionCommand) addMeta(completionParsers.completionCommand, completionGroup);
560
+ commandParsers.push(...ungroupedMeta);
561
+ for (const [label, parsers] of Object.entries(groupedMeta)) if (parsers.length === 1) commandParsers.push(group(label, parsers[0]));
562
+ else commandParsers.push(group(label, longestMatch(...parsers)));
530
563
  if (commandParsers.length === 1) helpGeneratorParser = commandParsers[0];
531
564
  else if (commandParsers.length === 2) helpGeneratorParser = longestMatch(commandParsers[0], commandParsers[1]);
532
565
  else helpGeneratorParser = longestMatch(...commandParsers);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "0.10.0-dev.370+bae705cb",
3
+ "version": "0.10.0-dev.375+fffd225e",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",