@optique/core 1.4.0-dev.2892 → 1.4.0-dev.2895

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
@@ -818,7 +818,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
818
818
  options = optionsParam ?? {};
819
819
  }
820
820
  require_validate.validateProgramName(programName);
821
- const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", onError = () => {
821
+ const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", errorUsage = "root", onError = () => {
822
822
  throw new RunParserError("Failed to parse command line arguments.");
823
823
  }, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
824
824
  let maxWidth = requestedMaxWidth;
@@ -1117,8 +1117,15 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1117
1117
  })));
1118
1118
  }
1119
1119
  if (effectiveAboveError === "usage") {
1120
- resolveUsageWidth(augmentedParser.usage);
1121
- stderr(`${usageLabel()}${indentLines(require_usage.formatUsage(programName, augmentedParser.usage, {
1120
+ const rootUsage = augmentedParser.usage;
1121
+ const optionCount = Number(helpParsers.helpOption != null) + Number(versionParsers.versionOption != null);
1122
+ const alternativeCount = optionCount + Number(helpParsers.helpCommand != null) + Number(versionParsers.versionCommand != null) + Number(completionParsers.completionCommand != null);
1123
+ const errorUsageTerms = aboveError === "usage" && errorUsage === "command" ? require_usage_internals.projectCommandUsage(rootUsage, classified.commandPath, alternativeCount === 0 ? void 0 : {
1124
+ optionCount,
1125
+ alternativeCount
1126
+ }) ?? rootUsage : rootUsage;
1127
+ resolveUsageWidth(errorUsageTerms);
1128
+ stderr(`${usageLabel()}${indentLines(require_usage.formatUsage(programName, errorUsageTerms, {
1122
1129
  theme,
1123
1130
  colors,
1124
1131
  maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),
package/dist/facade.d.cts CHANGED
@@ -326,6 +326,17 @@ interface RunOptions<THelp, TError> {
326
326
  * @default `"usage"`
327
327
  */
328
328
  readonly aboveError?: "usage" | "help" | "none";
329
+ /**
330
+ * Scope of the compact preamble when `aboveError` is `"usage"`.
331
+ * `"command"` uses the failed parse's canonical command path and usage
332
+ * metadata, retaining parent options. Hidden or unresolved paths and
333
+ * unsupported compositions fall back to root usage. Neither mode applies
334
+ * help-only `usageLine` overrides or evaluates the parser again.
335
+ *
336
+ * @default `"root"`
337
+ * @since 1.4.0
338
+ */
339
+ readonly errorUsage?: "root" | "command";
329
340
  /**
330
341
  * Callback invoked after error output for a parse failure, an invalid
331
342
  * command path before help, or a missing or unsupported completion shell.
package/dist/facade.d.ts CHANGED
@@ -326,6 +326,17 @@ interface RunOptions<THelp, TError> {
326
326
  * @default `"usage"`
327
327
  */
328
328
  readonly aboveError?: "usage" | "help" | "none";
329
+ /**
330
+ * Scope of the compact preamble when `aboveError` is `"usage"`.
331
+ * `"command"` uses the failed parse's canonical command path and usage
332
+ * metadata, retaining parent options. Hidden or unresolved paths and
333
+ * unsupported compositions fall back to root usage. Neither mode applies
334
+ * help-only `usageLine` overrides or evaluates the parser again.
335
+ *
336
+ * @default `"root"`
337
+ * @since 1.4.0
338
+ */
339
+ readonly errorUsage?: "root" | "command";
329
340
  /**
330
341
  * Callback invoked after error output for a parse failure, an invalid
331
342
  * command path before help, or a missing or unsupported completion shell.
package/dist/facade.js CHANGED
@@ -12,7 +12,7 @@ import { collectExplicitSourceValues, collectExplicitSourceValuesAsync, createDe
12
12
  import { createInputTrace } from "./input-trace.js";
13
13
  import { attemptParseAsync, attemptParseSync, createEffectfulCompletionSession, createParserContext, getDocPage } from "./internal/parser.js";
14
14
  import { bash, fish, nu, pwsh, zsh } from "./completion.js";
15
- import { collectActiveOptionNames, collectRootOptionNames } from "./usage-internals.js";
15
+ import { collectActiveOptionNames, collectRootOptionNames, projectCommandUsage } from "./usage-internals.js";
16
16
  import { allowDuplicateLeadingCommandNamesKey, hiddenCommandAliasesKey } from "./internal/command-alias.js";
17
17
  import { completeOrExtractPhase2Seed } from "./phase2-seed.js";
18
18
  import { multiple, optional, withDefault } from "./modifiers.js";
@@ -818,7 +818,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
818
818
  options = optionsParam ?? {};
819
819
  }
820
820
  validateProgramName(programName);
821
- const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", onError = () => {
821
+ const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", errorUsage = "root", onError = () => {
822
822
  throw new RunParserError("Failed to parse command line arguments.");
823
823
  }, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
824
824
  let maxWidth = requestedMaxWidth;
@@ -1117,8 +1117,15 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1117
1117
  })));
1118
1118
  }
1119
1119
  if (effectiveAboveError === "usage") {
1120
- resolveUsageWidth(augmentedParser.usage);
1121
- stderr(`${usageLabel()}${indentLines(formatUsage(programName, augmentedParser.usage, {
1120
+ const rootUsage = augmentedParser.usage;
1121
+ const optionCount = Number(helpParsers.helpOption != null) + Number(versionParsers.versionOption != null);
1122
+ const alternativeCount = optionCount + Number(helpParsers.helpCommand != null) + Number(versionParsers.versionCommand != null) + Number(completionParsers.completionCommand != null);
1123
+ const errorUsageTerms = aboveError === "usage" && errorUsage === "command" ? projectCommandUsage(rootUsage, classified.commandPath, alternativeCount === 0 ? void 0 : {
1124
+ optionCount,
1125
+ alternativeCount
1126
+ }) ?? rootUsage : rootUsage;
1127
+ resolveUsageWidth(errorUsageTerms);
1128
+ stderr(`${usageLabel()}${indentLines(formatUsage(programName, errorUsageTerms, {
1122
1129
  theme,
1123
1130
  colors,
1124
1131
  maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),
@@ -2,6 +2,103 @@ const require_usage = require('./usage.cjs');
2
2
 
3
3
  //#region src/usage-internals.ts
4
4
  /**
5
+ * Projects augmented runner usage onto a recorded canonical command path.
6
+ * Unsupported or ambiguous metadata returns undefined, preserving root usage.
7
+ * No parser, documentation, or usage-override callbacks are evaluated.
8
+ * @param usage The runner's original augmented usage.
9
+ * @param path The canonical path recorded by the failed parse.
10
+ * @param meta The known augmentation boundary, absent without alternatives.
11
+ * @returns The selected grammar, or undefined when projection is uncertain.
12
+ * @internal
13
+ */
14
+ function projectCommandUsage(usage, path, meta) {
15
+ if (path.length === 0) return void 0;
16
+ if (meta == null) return projectCommandTerms(usage, path, 0);
17
+ const top = usage[0];
18
+ if (usage.length !== 1 || top.type !== "exclusive" || top.terms.length !== meta.alternativeCount + 1) return void 0;
19
+ const selected = projectCommandTerms(top.terms[meta.optionCount], path, 0);
20
+ if (selected == null) return void 0;
21
+ const options = top.terms.slice(0, meta.optionCount).filter((branch) => branch.some((term) => term.type === "option" && !require_usage.isUsageHidden(term.hidden)));
22
+ return options.length === 0 ? selected : [{
23
+ type: "exclusive",
24
+ terms: [...options, selected]
25
+ }];
26
+ }
27
+ function containsCommand(usage) {
28
+ return usage.some((term) => {
29
+ switch (term.type) {
30
+ case "command": return true;
31
+ case "exclusive": return term.terms.some(containsCommand);
32
+ case "optional":
33
+ case "multiple":
34
+ case "sequence": return containsCommand(term.terms);
35
+ default: return false;
36
+ }
37
+ });
38
+ }
39
+ /** Undefined distinguishes unsupported metadata from a command-free branch. */
40
+ function firstCommandNames(usage) {
41
+ const hosts = usage.filter((term) => containsCommand([term]));
42
+ const first = hosts[0];
43
+ if (first == null) return [];
44
+ if (first.type === "command") return [first.name];
45
+ if (hosts.length !== 1) return void 0;
46
+ if (first.type === "sequence") return firstCommandNames(first.terms);
47
+ if (first.type !== "exclusive") return void 0;
48
+ const names = [];
49
+ for (const branch of first.terms) {
50
+ const found = firstCommandNames(branch);
51
+ if (found == null) return void 0;
52
+ names.push(...found);
53
+ }
54
+ return names;
55
+ }
56
+ function projectCommandTerms(usage, path, offset) {
57
+ if (offset === path.length) return usage;
58
+ const index = usage.findIndex((term) => containsCommand([term]));
59
+ if (index < 0) return void 0;
60
+ const host = usage[index];
61
+ const prefix = usage.slice(0, index);
62
+ const suffix = usage.slice(index + 1);
63
+ if (host.type === "command") {
64
+ if (host.name !== path[offset] || require_usage.isUsageHidden(host.hidden)) return void 0;
65
+ const rest = projectCommandTerms(suffix, path, offset + 1);
66
+ return rest == null ? void 0 : [
67
+ ...prefix,
68
+ host,
69
+ ...rest
70
+ ];
71
+ }
72
+ if (containsCommand(suffix)) return void 0;
73
+ if (host.type === "sequence") {
74
+ const terms$1 = projectCommandTerms(host.terms, path, offset);
75
+ return terms$1 == null ? void 0 : [
76
+ ...prefix,
77
+ {
78
+ ...host,
79
+ terms: terms$1
80
+ },
81
+ ...suffix
82
+ ];
83
+ }
84
+ if (host.type !== "exclusive") return void 0;
85
+ let selected;
86
+ for (const branch of host.terms) {
87
+ const names = firstCommandNames(branch);
88
+ if (names == null) return void 0;
89
+ if (!names.includes(path[offset])) continue;
90
+ if (selected != null) return void 0;
91
+ selected = branch;
92
+ }
93
+ if (selected == null) return void 0;
94
+ const terms = projectCommandTerms(selected, path, offset);
95
+ return terms == null ? void 0 : [
96
+ ...prefix,
97
+ ...terms,
98
+ ...suffix
99
+ ];
100
+ }
101
+ /**
5
102
  * Collects option names and command names that are valid as the *immediate*
6
103
  * next token at the current parse position ("leading candidates").
7
104
  *
@@ -152,4 +249,5 @@ exports.collectActiveOptionNames = collectActiveOptionNames;
152
249
  exports.collectLeadingCandidates = collectLeadingCandidates;
153
250
  exports.collectOptionNamesAtCurrentCommandDepth = collectOptionNamesAtCurrentCommandDepth;
154
251
  exports.collectRootOptionNames = collectRootOptionNames;
155
- exports.extractLeadingCommandNames = extractLeadingCommandNames;
252
+ exports.extractLeadingCommandNames = extractLeadingCommandNames;
253
+ exports.projectCommandUsage = projectCommandUsage;
@@ -1,7 +1,104 @@
1
- import { isSuggestionHidden } from "./usage.js";
1
+ import { isSuggestionHidden, isUsageHidden } from "./usage.js";
2
2
 
3
3
  //#region src/usage-internals.ts
4
4
  /**
5
+ * Projects augmented runner usage onto a recorded canonical command path.
6
+ * Unsupported or ambiguous metadata returns undefined, preserving root usage.
7
+ * No parser, documentation, or usage-override callbacks are evaluated.
8
+ * @param usage The runner's original augmented usage.
9
+ * @param path The canonical path recorded by the failed parse.
10
+ * @param meta The known augmentation boundary, absent without alternatives.
11
+ * @returns The selected grammar, or undefined when projection is uncertain.
12
+ * @internal
13
+ */
14
+ function projectCommandUsage(usage, path, meta) {
15
+ if (path.length === 0) return void 0;
16
+ if (meta == null) return projectCommandTerms(usage, path, 0);
17
+ const top = usage[0];
18
+ if (usage.length !== 1 || top.type !== "exclusive" || top.terms.length !== meta.alternativeCount + 1) return void 0;
19
+ const selected = projectCommandTerms(top.terms[meta.optionCount], path, 0);
20
+ if (selected == null) return void 0;
21
+ const options = top.terms.slice(0, meta.optionCount).filter((branch) => branch.some((term) => term.type === "option" && !isUsageHidden(term.hidden)));
22
+ return options.length === 0 ? selected : [{
23
+ type: "exclusive",
24
+ terms: [...options, selected]
25
+ }];
26
+ }
27
+ function containsCommand(usage) {
28
+ return usage.some((term) => {
29
+ switch (term.type) {
30
+ case "command": return true;
31
+ case "exclusive": return term.terms.some(containsCommand);
32
+ case "optional":
33
+ case "multiple":
34
+ case "sequence": return containsCommand(term.terms);
35
+ default: return false;
36
+ }
37
+ });
38
+ }
39
+ /** Undefined distinguishes unsupported metadata from a command-free branch. */
40
+ function firstCommandNames(usage) {
41
+ const hosts = usage.filter((term) => containsCommand([term]));
42
+ const first = hosts[0];
43
+ if (first == null) return [];
44
+ if (first.type === "command") return [first.name];
45
+ if (hosts.length !== 1) return void 0;
46
+ if (first.type === "sequence") return firstCommandNames(first.terms);
47
+ if (first.type !== "exclusive") return void 0;
48
+ const names = [];
49
+ for (const branch of first.terms) {
50
+ const found = firstCommandNames(branch);
51
+ if (found == null) return void 0;
52
+ names.push(...found);
53
+ }
54
+ return names;
55
+ }
56
+ function projectCommandTerms(usage, path, offset) {
57
+ if (offset === path.length) return usage;
58
+ const index = usage.findIndex((term) => containsCommand([term]));
59
+ if (index < 0) return void 0;
60
+ const host = usage[index];
61
+ const prefix = usage.slice(0, index);
62
+ const suffix = usage.slice(index + 1);
63
+ if (host.type === "command") {
64
+ if (host.name !== path[offset] || isUsageHidden(host.hidden)) return void 0;
65
+ const rest = projectCommandTerms(suffix, path, offset + 1);
66
+ return rest == null ? void 0 : [
67
+ ...prefix,
68
+ host,
69
+ ...rest
70
+ ];
71
+ }
72
+ if (containsCommand(suffix)) return void 0;
73
+ if (host.type === "sequence") {
74
+ const terms$1 = projectCommandTerms(host.terms, path, offset);
75
+ return terms$1 == null ? void 0 : [
76
+ ...prefix,
77
+ {
78
+ ...host,
79
+ terms: terms$1
80
+ },
81
+ ...suffix
82
+ ];
83
+ }
84
+ if (host.type !== "exclusive") return void 0;
85
+ let selected;
86
+ for (const branch of host.terms) {
87
+ const names = firstCommandNames(branch);
88
+ if (names == null) return void 0;
89
+ if (!names.includes(path[offset])) continue;
90
+ if (selected != null) return void 0;
91
+ selected = branch;
92
+ }
93
+ if (selected == null) return void 0;
94
+ const terms = projectCommandTerms(selected, path, offset);
95
+ return terms == null ? void 0 : [
96
+ ...prefix,
97
+ ...terms,
98
+ ...suffix
99
+ ];
100
+ }
101
+ /**
5
102
  * Collects option names and command names that are valid as the *immediate*
6
103
  * next token at the current parse position ("leading candidates").
7
104
  *
@@ -148,4 +245,4 @@ function collectRootOptionNamesFromTerm(term, names, rootLeadingNames) {
148
245
  }
149
246
 
150
247
  //#endregion
151
- export { collectActiveOptionNames, collectLeadingCandidates, collectOptionNamesAtCurrentCommandDepth, collectRootOptionNames, extractLeadingCommandNames };
248
+ export { collectActiveOptionNames, collectLeadingCandidates, collectOptionNamesAtCurrentCommandDepth, collectRootOptionNames, extractLeadingCommandNames, projectCommandUsage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.4.0-dev.2892",
3
+ "version": "1.4.0-dev.2895",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -261,7 +261,7 @@
261
261
  },
262
262
  "sideEffects": false,
263
263
  "devDependencies": {
264
- "@optique/env": "1.4.0-dev.2892+c7a6d04d",
264
+ "@optique/env": "1.4.0-dev.2895+fb7758c4",
265
265
  "@types/node": "^24.0.0",
266
266
  "fast-check": "^4.7.0",
267
267
  "tsdown": "^0.13.0",
@@ -80,9 +80,9 @@ Core rules
80
80
  Show hidden `aliases` in help with `showAliases` on `command()` or runner.
81
81
  - Use `run(parser, { completion: "both" })` for completion, or the object form
82
82
  with `completion.errors` for custom shell errors. Do not hand-write scripts.
83
- - Use `usageLine: [{ type: "ellipsis" }]` in runner options when a large root
84
- synopsis should become a compact `Usage: myapp ...` line. This applies only
85
- to root full help; use `command()`'s `usageLine` for subcommand help.
83
+ - Use `usageLine: [{ type: "ellipsis" }]` for compact root full help and the
84
+ override on `command()` for subcommand help. `errorUsage: "command"` scopes
85
+ usage above errors with root fallback, ignoring help-only overrides.
86
86
  - Use `showUsage: false` in runner options when full help should show the
87
87
  brief and sections without `Usage:`. Set `command()`'s `showUsage: true`
88
88
  to restore it; children inherit the nearest explicit ancestor setting.