@optique/core 0.5.1 → 0.6.0-dev.103

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/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,53 @@ 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, availableShells, 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 shell = availableShells[shellName];
276
+ if (!shell) {
277
+ const available = [];
278
+ for (const shell$1 in availableShells) {
279
+ if (available.length > 0) available.push(require_message.text(", "));
280
+ available.push(require_message.value(shell$1));
281
+ }
282
+ stderr(require_message.formatMessage(require_message.message`Error: Unsupported shell ${shellName}. Available shells: ${available}.`, {
283
+ colors,
284
+ quotes: !colors
285
+ }));
286
+ return onError(1);
287
+ }
288
+ if (args.length === 0) {
289
+ const script = shell.generateScript(programName, ["completion", shellName]);
290
+ stdout(script);
291
+ } else {
292
+ const suggestions = require_parser.suggest(parser, args);
293
+ for (const chunk of shell.encodeSuggestions(suggestions)) stdout(chunk);
294
+ }
295
+ try {
296
+ return onCompletion(0);
297
+ } catch {
298
+ return onCompletion();
299
+ }
300
+ }
301
+ /**
249
302
  * Runs a parser against command-line arguments with built-in help and error
250
303
  * handling.
251
304
  *
@@ -275,14 +328,41 @@ function classifyResult(result, args) {
275
328
  * @throws {RunError} When parsing fails and no `onError` callback is provided.
276
329
  */
277
330
  function run(parser, programName, args, options = {}) {
331
+ let { colors, maxWidth, showDefault, aboveError = "usage", onError = () => {
332
+ throw new RunError("Failed to parse command line arguments.");
333
+ }, stderr = console.error, stdout = console.log, brief, description, footer } = options;
334
+ const completionMode = options.completion?.mode ?? "both";
335
+ const onCompletion = options.completion?.onShow ?? (() => ({}));
336
+ if (options.completion) {
337
+ const defaultShells = {
338
+ bash: require_completion.bash,
339
+ fish: require_completion.fish,
340
+ pwsh: require_completion.pwsh,
341
+ zsh: require_completion.zsh
342
+ };
343
+ const availableShells = options.completion.shells ? {
344
+ ...defaultShells,
345
+ ...options.completion.shells
346
+ } : defaultShells;
347
+ if ((completionMode === "command" || completionMode === "both") && args.length >= 1 && args[0] === "completion") return handleCompletion(args.slice(1), programName, parser, stdout, stderr, onCompletion, onError, availableShells, colors);
348
+ if (completionMode === "option" || completionMode === "both") for (let i = 0; i < args.length; i++) {
349
+ const arg = args[i];
350
+ if (arg.startsWith("--completion=")) {
351
+ const shell = arg.slice(13);
352
+ const completionArgs = args.slice(i + 1);
353
+ return handleCompletion([shell, ...completionArgs], programName, parser, stdout, stderr, onCompletion, onError, availableShells, colors);
354
+ } else if (arg === "--completion" && i + 1 < args.length) {
355
+ const shell = args[i + 1];
356
+ const completionArgs = args.slice(i + 2);
357
+ return handleCompletion([shell, ...completionArgs], programName, parser, stdout, stderr, onCompletion, onError, availableShells, colors);
358
+ }
359
+ }
360
+ }
278
361
  const helpMode = options.help?.mode ?? "option";
279
362
  const onHelp = options.help?.onShow ?? (() => ({}));
280
363
  const versionMode = options.version?.mode ?? "option";
281
364
  const versionValue = options.version?.value ?? "";
282
365
  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
366
  const help = options.help ? helpMode : "none";
287
367
  const version = options.version ? versionMode : "none";
288
368
  const helpParsers = help === "none" ? {
@@ -345,36 +425,38 @@ function run(parser, programName, args, options = {}) {
345
425
  return onHelp();
346
426
  }
347
427
  }
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, {
428
+ case "error": {
429
+ if (aboveError === "help") {
430
+ const doc = require_parser.getDocPage(args.length < 1 ? augmentedParser : parser, args);
431
+ if (doc == null) aboveError = "usage";
432
+ else {
433
+ const augmentedDoc = {
434
+ ...doc,
435
+ brief: brief ?? doc.brief,
436
+ description: description ?? doc.description,
437
+ footer: footer ?? doc.footer
438
+ };
439
+ stderr(require_doc.formatDocPage(programName, augmentedDoc, {
440
+ colors,
441
+ maxWidth,
442
+ showDefault
443
+ }));
444
+ }
445
+ }
446
+ if (aboveError === "usage") stderr(`Usage: ${indentLines(require_usage.formatUsage(programName, augmentedParser.usage, {
447
+ colors,
448
+ maxWidth: maxWidth == null ? void 0 : maxWidth - 7,
449
+ expandCommands: true
450
+ }), 7)}`);
451
+ const errorMessage = require_message.formatMessage(classified.error, {
361
452
  colors,
362
- maxWidth,
363
- showDefault
364
- }));
453
+ quotes: !colors
454
+ });
455
+ stderr(`Error: ${errorMessage}`);
456
+ return onError(1);
365
457
  }
458
+ default: throw new RunError("Unexpected parse result type");
366
459
  }
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
460
  }
379
461
  /**
380
462
  * An error class used to indicate that the command line arguments
@@ -386,8 +468,8 @@ var RunError = class extends Error {
386
468
  this.name = "RunError";
387
469
  }
388
470
  };
389
- function indentLines(text, indent) {
390
- return text.split("\n").join("\n" + " ".repeat(indent));
471
+ function indentLines(text$1, indent) {
472
+ return text$1.split("\n").join("\n" + " ".repeat(indent));
391
473
  }
392
474
 
393
475
  //#endregion
package/dist/facade.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Message } from "./message.cjs";
2
2
  import { ShowDefaultOptions } from "./doc.cjs";
3
3
  import { InferValue, Parser } from "./parser.cjs";
4
+ import { ShellCompletion } from "./completion.cjs";
4
5
 
5
6
  //#region src/facade.d.ts
6
7
 
@@ -88,6 +89,40 @@ interface RunOptions<THelp, TError> {
88
89
  */
89
90
  readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
90
91
  };
92
+ /**
93
+ * Completion configuration. When provided, enables shell completion functionality.
94
+ * @since 0.6.0
95
+ */
96
+ readonly completion?: {
97
+ /**
98
+ * Determines how completion is made available:
99
+ *
100
+ * - `"command"`: Only the `completion` subcommand is available
101
+ * - `"option"`: Only the `--completion` option is available
102
+ * - `"both"`: Both `completion` subcommand and `--completion` option are available
103
+ *
104
+ * @default `"both"`
105
+ */
106
+ readonly mode?: "command" | "option" | "both";
107
+ /**
108
+ * Available shell completions. By default, includes `bash`, `fish`, `pwsh`,
109
+ * and `zsh`. You can provide additional custom shell completions or override
110
+ * the defaults.
111
+ *
112
+ * @default `{ bash, fish, pwsh, zsh }`
113
+ */
114
+ readonly shells?: Record<string, ShellCompletion>;
115
+ /**
116
+ * Callback function invoked when completion is requested. The function can
117
+ * optionally receive an exit code parameter.
118
+ *
119
+ * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
120
+ * on Deno to this option.
121
+ *
122
+ * @default Returns `void` when completion is shown.
123
+ */
124
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
125
+ };
91
126
  /**
92
127
  * What to display above error messages:
93
128
  * - `"usage"`: Show usage information
package/dist/facade.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Message } from "./message.js";
2
2
  import { ShowDefaultOptions } from "./doc.js";
3
3
  import { InferValue, Parser } from "./parser.js";
4
+ import { ShellCompletion } from "./completion.js";
4
5
 
5
6
  //#region src/facade.d.ts
6
7
 
@@ -88,6 +89,40 @@ interface RunOptions<THelp, TError> {
88
89
  */
89
90
  readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
90
91
  };
92
+ /**
93
+ * Completion configuration. When provided, enables shell completion functionality.
94
+ * @since 0.6.0
95
+ */
96
+ readonly completion?: {
97
+ /**
98
+ * Determines how completion is made available:
99
+ *
100
+ * - `"command"`: Only the `completion` subcommand is available
101
+ * - `"option"`: Only the `--completion` option is available
102
+ * - `"both"`: Both `completion` subcommand and `--completion` option are available
103
+ *
104
+ * @default `"both"`
105
+ */
106
+ readonly mode?: "command" | "option" | "both";
107
+ /**
108
+ * Available shell completions. By default, includes `bash`, `fish`, `pwsh`,
109
+ * and `zsh`. You can provide additional custom shell completions or override
110
+ * the defaults.
111
+ *
112
+ * @default `{ bash, fish, pwsh, zsh }`
113
+ */
114
+ readonly shells?: Record<string, ShellCompletion>;
115
+ /**
116
+ * Callback function invoked when completion is requested. The function can
117
+ * optionally receive an exit code parameter.
118
+ *
119
+ * You usually want to pass `process.exit` on Node.js or Bun and `Deno.exit`
120
+ * on Deno to this option.
121
+ *
122
+ * @default Returns `void` when completion is shown.
123
+ */
124
+ readonly onShow?: (() => THelp) | ((exitCode: number) => THelp);
125
+ };
91
126
  /**
92
127
  * What to display above error messages:
93
128
  * - `"usage"`: Show usage information