@optique/core 0.5.0 → 0.6.0-dev.102

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.
@@ -74,6 +74,33 @@ function or(...args) {
74
74
  success: false
75
75
  };
76
76
  },
77
+ suggest(context, prefix) {
78
+ const suggestions = [];
79
+ if (context.state == null) for (const parser of parsers) {
80
+ const parserSuggestions = parser.suggest({
81
+ ...context,
82
+ state: parser.initialState
83
+ }, prefix);
84
+ suggestions.push(...parserSuggestions);
85
+ }
86
+ else {
87
+ const [index, parserResult] = context.state;
88
+ if (parserResult.success) {
89
+ const parserSuggestions = parsers[index].suggest({
90
+ ...context,
91
+ state: parserResult.next.state
92
+ }, prefix);
93
+ suggestions.push(...parserSuggestions);
94
+ }
95
+ }
96
+ const seen = /* @__PURE__ */ new Set();
97
+ return suggestions.filter((suggestion) => {
98
+ const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
99
+ if (seen.has(key)) return false;
100
+ seen.add(key);
101
+ return true;
102
+ });
103
+ },
77
104
  getDocFragments(state, _defaultValue) {
78
105
  let description;
79
106
  let fragments;
@@ -182,6 +209,33 @@ function longestMatch(...args) {
182
209
  success: false
183
210
  };
184
211
  },
212
+ suggest(context, prefix) {
213
+ const suggestions = [];
214
+ if (context.state == null) for (const parser of parsers) {
215
+ const parserSuggestions = parser.suggest({
216
+ ...context,
217
+ state: parser.initialState
218
+ }, prefix);
219
+ suggestions.push(...parserSuggestions);
220
+ }
221
+ else {
222
+ const [index, parserResult] = context.state;
223
+ if (parserResult.success) {
224
+ const parserSuggestions = parsers[index].suggest({
225
+ ...context,
226
+ state: parserResult.next.state
227
+ }, prefix);
228
+ suggestions.push(...parserSuggestions);
229
+ }
230
+ }
231
+ const seen = /* @__PURE__ */ new Set();
232
+ return suggestions.filter((suggestion) => {
233
+ const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
234
+ if (seen.has(key)) return false;
235
+ seen.add(key);
236
+ return true;
237
+ });
238
+ },
185
239
  getDocFragments(state, _defaultValue) {
186
240
  let description;
187
241
  let fragments;
@@ -302,6 +356,24 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
302
356
  value: result
303
357
  };
304
358
  },
359
+ suggest(context, prefix) {
360
+ const suggestions = [];
361
+ for (const [field, parser] of parserPairs) {
362
+ const fieldState = context.state && typeof context.state === "object" && field in context.state ? context.state[field] : parser.initialState;
363
+ const fieldSuggestions = parser.suggest({
364
+ ...context,
365
+ state: fieldState
366
+ }, prefix);
367
+ suggestions.push(...fieldSuggestions);
368
+ }
369
+ const seen = /* @__PURE__ */ new Set();
370
+ return suggestions.filter((suggestion) => {
371
+ const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
372
+ if (seen.has(key)) return false;
373
+ seen.add(key);
374
+ return true;
375
+ });
376
+ },
305
377
  getDocFragments(state, defaultValue) {
306
378
  const fragments = parserPairs.flatMap(([field, p]) => {
307
379
  const fieldState = state.kind === "unavailable" ? { kind: "unavailable" } : {
@@ -412,6 +484,25 @@ function tuple(labelOrParsers, maybeParsers) {
412
484
  value: result
413
485
  };
414
486
  },
487
+ suggest(context, prefix) {
488
+ const suggestions = [];
489
+ for (let i = 0; i < parsers.length; i++) {
490
+ const parser = parsers[i];
491
+ const parserState = context.state && Array.isArray(context.state) ? context.state[i] : parser.initialState;
492
+ const parserSuggestions = parser.suggest({
493
+ ...context,
494
+ state: parserState
495
+ }, prefix);
496
+ suggestions.push(...parserSuggestions);
497
+ }
498
+ const seen = /* @__PURE__ */ new Set();
499
+ return suggestions.filter((suggestion) => {
500
+ const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
501
+ if (seen.has(key)) return false;
502
+ seen.add(key);
503
+ return true;
504
+ });
505
+ },
415
506
  getDocFragments(state, defaultValue) {
416
507
  const fragments = parsers.flatMap((p, i) => {
417
508
  const indexState = state.kind === "unavailable" ? { kind: "unavailable" } : {
@@ -523,6 +614,35 @@ function merge(...args) {
523
614
  value: object$1
524
615
  };
525
616
  },
617
+ suggest(context, prefix) {
618
+ const suggestions = [];
619
+ for (let i = 0; i < parsers.length; i++) {
620
+ const parser = parsers[i];
621
+ let parserState;
622
+ if (parser.initialState === void 0) {
623
+ const key = `__parser_${i}`;
624
+ if (context.state && typeof context.state === "object" && key in context.state) parserState = context.state[key];
625
+ else parserState = void 0;
626
+ } else if (parser.initialState && typeof parser.initialState === "object") if (context.state && typeof context.state === "object") {
627
+ const extractedState = {};
628
+ for (const field in parser.initialState) extractedState[field] = field in context.state ? context.state[field] : parser.initialState[field];
629
+ parserState = extractedState;
630
+ } else parserState = parser.initialState;
631
+ else parserState = parser.initialState;
632
+ const parserSuggestions = parser.suggest({
633
+ ...context,
634
+ state: parserState
635
+ }, prefix);
636
+ suggestions.push(...parserSuggestions);
637
+ }
638
+ const seen = /* @__PURE__ */ new Set();
639
+ return suggestions.filter((suggestion) => {
640
+ const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
641
+ if (seen.has(key)) return false;
642
+ seen.add(key);
643
+ return true;
644
+ });
645
+ },
526
646
  getDocFragments(state, _defaultValue) {
527
647
  const fragments = parsers.flatMap((p) => {
528
648
  const parserState = p.initialState === void 0 ? { kind: "unavailable" } : state.kind === "unavailable" ? { kind: "unavailable" } : {
@@ -641,6 +761,25 @@ function concat(...parsers) {
641
761
  value: results
642
762
  };
643
763
  },
764
+ suggest(context, prefix) {
765
+ const suggestions = [];
766
+ for (let i = 0; i < parsers.length; i++) {
767
+ const parser = parsers[i];
768
+ const parserState = context.state && Array.isArray(context.state) ? context.state[i] : parser.initialState;
769
+ const parserSuggestions = parser.suggest({
770
+ ...context,
771
+ state: parserState
772
+ }, prefix);
773
+ suggestions.push(...parserSuggestions);
774
+ }
775
+ const seen = /* @__PURE__ */ new Set();
776
+ return suggestions.filter((suggestion) => {
777
+ const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
778
+ if (seen.has(key)) return false;
779
+ seen.add(key);
780
+ return true;
781
+ });
782
+ },
644
783
  getDocFragments(state, _defaultValue) {
645
784
  const fragments = parsers.flatMap((p, index) => {
646
785
  const indexState = state.kind === "unavailable" ? { kind: "unavailable" } : {
@@ -719,6 +858,7 @@ function group(label, parser) {
719
858
  initialState: parser.initialState,
720
859
  parse: (context) => parser.parse(context),
721
860
  complete: (state) => parser.complete(state),
861
+ suggest: (context, prefix) => parser.suggest(context, prefix),
722
862
  getDocFragments: (state, defaultValue) => {
723
863
  const { description, fragments } = parser.getDocFragments(state, defaultValue);
724
864
  const allEntries = [];
package/dist/doc.cjs CHANGED
@@ -65,6 +65,7 @@ function formatDocPage(programName, page, options = {}) {
65
65
  }
66
66
  const sections = page.sections.toSorted((a, b) => a.title == null && b.title == null ? 0 : a.title == null ? -1 : 1);
67
67
  for (const section of sections) {
68
+ if (section.entries.length < 1) continue;
68
69
  output += "\n";
69
70
  if (section.title != null) output += `${section.title}:\n`;
70
71
  for (const entry of section.entries) {
package/dist/doc.js CHANGED
@@ -65,6 +65,7 @@ function formatDocPage(programName, page, options = {}) {
65
65
  }
66
66
  const sections = page.sections.toSorted((a, b) => a.title == null && b.title == null ? 0 : a.title == null ? -1 : 1);
67
67
  for (const section of sections) {
68
+ if (section.entries.length < 1) continue;
68
69
  output += "\n";
69
70
  if (section.title != null) output += `${section.title}:\n`;
70
71
  for (const entry of section.entries) {
package/dist/facade.cjs CHANGED
@@ -2,6 +2,7 @@ const require_message = require('./message.cjs');
2
2
  const require_constructs = require('./constructs.cjs');
3
3
  const require_usage = require('./usage.cjs');
4
4
  const require_doc = require('./doc.cjs');
5
+ const require_completion = require('./completion.cjs');
5
6
  const require_modifiers = require('./modifiers.cjs');
6
7
  const require_valueparser = require('./valueparser.cjs');
7
8
  const require_primitives = require('./primitives.cjs');
@@ -118,6 +119,13 @@ function combineWithHelpVersion(originalParser, helpParsers, versionParsers) {
118
119
  value: state
119
120
  };
120
121
  },
122
+ suggest(_context, prefix) {
123
+ if ("--help".startsWith(prefix)) return [{
124
+ kind: "literal",
125
+ text: "--help"
126
+ }];
127
+ return [];
128
+ },
121
129
  getDocFragments(state) {
122
130
  return helpParsers.helpOption?.getDocFragments(state) ?? { fragments: [] };
123
131
  }
@@ -179,6 +187,13 @@ function combineWithHelpVersion(originalParser, helpParsers, versionParsers) {
179
187
  value: state
180
188
  };
181
189
  },
190
+ suggest(_context, prefix) {
191
+ if ("--version".startsWith(prefix)) return [{
192
+ kind: "literal",
193
+ text: "--version"
194
+ }];
195
+ return [];
196
+ },
182
197
  getDocFragments(state) {
183
198
  return versionParsers.versionOption?.getDocFragments(state) ?? { fragments: [] };
184
199
  }
@@ -213,9 +228,9 @@ function classifyResult(result, args) {
213
228
  type: "error",
214
229
  error: result.error
215
230
  };
216
- const value = result.value;
217
- if (typeof value === "object" && value != null && "help" in value && "version" in value) {
218
- const parsedValue = value;
231
+ const value$1 = result.value;
232
+ if (typeof value$1 === "object" && value$1 != null && "help" in value$1 && "version" in value$1) {
233
+ const parsedValue = value$1;
219
234
  const hasVersionOption = args.includes("--version");
220
235
  const hasVersionCommand = args.length > 0 && args[0] === "version";
221
236
  const hasHelpOption = args.includes("--help");
@@ -237,15 +252,59 @@ function classifyResult(result, args) {
237
252
  if ((hasVersionOption || hasVersionCommand) && (parsedValue.version || parsedValue.versionFlag)) return { type: "version" };
238
253
  return {
239
254
  type: "success",
240
- value: parsedValue.result ?? value
255
+ value: parsedValue.result ?? value$1
241
256
  };
242
257
  }
243
258
  return {
244
259
  type: "success",
245
- value
260
+ value: value$1
246
261
  };
247
262
  }
248
263
  /**
264
+ * Handles shell completion requests.
265
+ * @since 0.6.0
266
+ */
267
+ function handleCompletion(completionArgs, programName, parser, stdout, stderr, onCompletion, onError, colors) {
268
+ if (completionArgs.length === 0) {
269
+ stderr("Error: Missing shell name for completion.\n");
270
+ stderr("Usage: " + programName + " completion <shell> [args...]\n");
271
+ return onError(1);
272
+ }
273
+ const shellName = completionArgs[0];
274
+ const args = completionArgs.slice(1);
275
+ const availableShells = {
276
+ bash: require_completion.bash,
277
+ fish: require_completion.fish,
278
+ pwsh: require_completion.pwsh,
279
+ zsh: require_completion.zsh
280
+ };
281
+ const shell = availableShells[shellName];
282
+ if (!shell) {
283
+ const available = [];
284
+ for (const shell$1 in availableShells) {
285
+ if (available.length > 0) available.push(require_message.text(", "));
286
+ available.push(require_message.value(shell$1));
287
+ }
288
+ stderr(require_message.formatMessage(require_message.message`Error: Unsupported shell ${shellName}. Available shells: ${available}.`, {
289
+ colors,
290
+ quotes: !colors
291
+ }));
292
+ return onError(1);
293
+ }
294
+ if (args.length === 0) {
295
+ const script = shell.generateScript(programName, ["completion", shellName]);
296
+ stdout(script);
297
+ } else {
298
+ const suggestions = require_parser.suggest(parser, args);
299
+ for (const chunk of shell.encodeSuggestions(suggestions)) stdout(chunk);
300
+ }
301
+ try {
302
+ return onCompletion(0);
303
+ } catch {
304
+ return onCompletion();
305
+ }
306
+ }
307
+ /**
249
308
  * Runs a parser against command-line arguments with built-in help and error
250
309
  * handling.
251
310
  *
@@ -275,14 +334,31 @@ function classifyResult(result, args) {
275
334
  * @throws {RunError} When parsing fails and no `onError` callback is provided.
276
335
  */
277
336
  function run(parser, programName, args, options = {}) {
337
+ let { colors, maxWidth, showDefault, aboveError = "usage", onError = () => {
338
+ throw new RunError("Failed to parse command line arguments.");
339
+ }, stderr = console.error, stdout = console.log, brief, description, footer } = options;
340
+ const completionMode = options.completion?.mode ?? "both";
341
+ const onCompletion = options.completion?.onShow ?? (() => ({}));
342
+ if (options.completion) {
343
+ if ((completionMode === "command" || completionMode === "both") && args.length >= 1 && args[0] === "completion") return handleCompletion(args.slice(1), programName, parser, stdout, stderr, onCompletion, onError, colors);
344
+ if (completionMode === "option" || completionMode === "both") for (let i = 0; i < args.length; i++) {
345
+ const arg = args[i];
346
+ if (arg.startsWith("--completion=")) {
347
+ const shell = arg.slice(13);
348
+ const completionArgs = args.slice(i + 1);
349
+ return handleCompletion([shell, ...completionArgs], programName, parser, stdout, stderr, onCompletion, onError, colors);
350
+ } else if (arg === "--completion" && i + 1 < args.length) {
351
+ const shell = args[i + 1];
352
+ const completionArgs = args.slice(i + 2);
353
+ return handleCompletion([shell, ...completionArgs], programName, parser, stdout, stderr, onCompletion, onError, colors);
354
+ }
355
+ }
356
+ }
278
357
  const helpMode = options.help?.mode ?? "option";
279
358
  const onHelp = options.help?.onShow ?? (() => ({}));
280
359
  const versionMode = options.version?.mode ?? "option";
281
360
  const versionValue = options.version?.value ?? "";
282
361
  const onVersion = options.version?.onShow ?? (() => ({}));
283
- let { colors, maxWidth, showDefault, aboveError = "usage", onError = () => {
284
- throw new RunError("Failed to parse command line arguments.");
285
- }, stderr = console.error, stdout = console.log, brief, description, footer } = options;
286
362
  const help = options.help ? helpMode : "none";
287
363
  const version = options.version ? versionMode : "none";
288
364
  const helpParsers = help === "none" ? {
@@ -345,36 +421,38 @@ function run(parser, programName, args, options = {}) {
345
421
  return onHelp();
346
422
  }
347
423
  }
348
- case "error": break;
349
- }
350
- if (aboveError === "help") {
351
- const doc = require_parser.getDocPage(args.length < 1 ? augmentedParser : parser, args);
352
- if (doc == null) aboveError = "usage";
353
- else {
354
- const augmentedDoc = {
355
- ...doc,
356
- brief: brief ?? doc.brief,
357
- description: description ?? doc.description,
358
- footer: footer ?? doc.footer
359
- };
360
- stderr(require_doc.formatDocPage(programName, augmentedDoc, {
424
+ case "error": {
425
+ if (aboveError === "help") {
426
+ const doc = require_parser.getDocPage(args.length < 1 ? augmentedParser : parser, args);
427
+ if (doc == null) aboveError = "usage";
428
+ else {
429
+ const augmentedDoc = {
430
+ ...doc,
431
+ brief: brief ?? doc.brief,
432
+ description: description ?? doc.description,
433
+ footer: footer ?? doc.footer
434
+ };
435
+ stderr(require_doc.formatDocPage(programName, augmentedDoc, {
436
+ colors,
437
+ maxWidth,
438
+ showDefault
439
+ }));
440
+ }
441
+ }
442
+ if (aboveError === "usage") stderr(`Usage: ${indentLines(require_usage.formatUsage(programName, augmentedParser.usage, {
443
+ colors,
444
+ maxWidth: maxWidth == null ? void 0 : maxWidth - 7,
445
+ expandCommands: true
446
+ }), 7)}`);
447
+ const errorMessage = require_message.formatMessage(classified.error, {
361
448
  colors,
362
- maxWidth,
363
- showDefault
364
- }));
449
+ quotes: !colors
450
+ });
451
+ stderr(`Error: ${errorMessage}`);
452
+ return onError(1);
365
453
  }
454
+ default: throw new RunError("Unexpected parse result type");
366
455
  }
367
- if (aboveError === "usage") stderr(`Usage: ${indentLines(require_usage.formatUsage(programName, augmentedParser.usage, {
368
- colors,
369
- maxWidth: maxWidth == null ? void 0 : maxWidth - 7,
370
- expandCommands: true
371
- }), 7)}`);
372
- const errorMessage = require_message.formatMessage(classified.error, {
373
- colors,
374
- quotes: !colors
375
- });
376
- stderr(`Error: ${errorMessage}`);
377
- return onError(1);
378
456
  }
379
457
  /**
380
458
  * An error class used to indicate that the command line arguments
@@ -386,8 +464,8 @@ var RunError = class extends Error {
386
464
  this.name = "RunError";
387
465
  }
388
466
  };
389
- function indentLines(text, indent) {
390
- return text.split("\n").join("\n" + " ".repeat(indent));
467
+ function indentLines(text$1, indent) {
468
+ return text$1.split("\n").join("\n" + " ".repeat(indent));
391
469
  }
392
470
 
393
471
  //#endregion
package/dist/facade.d.cts CHANGED
@@ -88,6 +88,32 @@ interface RunOptions<THelp, TError> {
88
88
  */
89
89
  readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
90
90
  };
91
+ /**
92
+ * Completion configuration. When provided, enables shell completion functionality.
93
+ * @since 0.6.0
94
+ */
95
+ readonly completion?: {
96
+ /**
97
+ * Determines how completion is made available:
98
+ *
99
+ * - `"command"`: Only the `completion` subcommand is available
100
+ * - `"option"`: Only the `--completion` option is available
101
+ * - `"both"`: Both `completion` subcommand and `--completion` option are available
102
+ *
103
+ * @default `"both"`
104
+ */
105
+ readonly mode?: "command" | "option" | "both";
106
+ /**
107
+ * Callback function invoked when completion is requested. The function can
108
+ * optionally receive an exit code parameter.
109
+ *
110
+ * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
111
+ * on Deno to this option.
112
+ *
113
+ * @default Returns `void` when completion is shown.
114
+ */
115
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
116
+ };
91
117
  /**
92
118
  * What to display above error messages:
93
119
  * - `"usage"`: Show usage information
package/dist/facade.d.ts CHANGED
@@ -88,6 +88,32 @@ interface RunOptions<THelp, TError> {
88
88
  */
89
89
  readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
90
90
  };
91
+ /**
92
+ * Completion configuration. When provided, enables shell completion functionality.
93
+ * @since 0.6.0
94
+ */
95
+ readonly completion?: {
96
+ /**
97
+ * Determines how completion is made available:
98
+ *
99
+ * - `"command"`: Only the `completion` subcommand is available
100
+ * - `"option"`: Only the `--completion` option is available
101
+ * - `"both"`: Both `completion` subcommand and `--completion` option are available
102
+ *
103
+ * @default `"both"`
104
+ */
105
+ readonly mode?: "command" | "option" | "both";
106
+ /**
107
+ * Callback function invoked when completion is requested. The function can
108
+ * optionally receive an exit code parameter.
109
+ *
110
+ * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
111
+ * on Deno to this option.
112
+ *
113
+ * @default Returns `void` when completion is shown.
114
+ */
115
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
116
+ };
91
117
  /**
92
118
  * What to display above error messages:
93
119
  * - `"usage"`: Show usage information