@optique/core 1.3.5 → 1.3.7-dev.3001

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.
@@ -6,6 +6,7 @@ import { dispatchByMode, dispatchIterableByMode, wrapForMode } from "./internal/
6
6
  import { getDefaultValuesFunction, getDependencyIds, getSnapshottedDefaultDependencyValues, isDerivedValueParser, suggestWithDependency } from "./internal/dependency.js";
7
7
  import { effectfulSchedulingNodesKey, exclusiveSourceScopeKey, includeSourceFailureChain, recordDerivedRawInput, replayDerivedParser, replayDerivedParserAsync, sourceCollectionExpansionKey } from "./dependency-runtime.js";
8
8
  import { getWrappedChildParseState, getWrappedChildState, isAnnotationWrappedInitialState, normalizeInjectedAnnotationState } from "./annotation-state.js";
9
+ import { joinedFileSuggestion } from "./internal/joined-file-suggestion.js";
9
10
  import { getCommandDocMetadata, withCommandDocMetadata } from "./internal/command-doc-metadata.js";
10
11
  import { defineKnownCompletion, defineOptionMatch, definePassThroughPriority, getOptionMatch, getPassThroughFailure, getPassThroughPriority } from "./internal/passthrough.js";
11
12
  import { hiddenCommandAliasesKey } from "./internal/command-alias.js";
@@ -253,33 +254,33 @@ function* getSuggestionsWithDependency(valueParser, prefix, dependencyRegistry,
253
254
  }
254
255
  yield* valueParser.suggest(prefix);
255
256
  }
257
+ /** Returns the joined prefix accepted by option parsing. */
258
+ function joinedOptionPrefix(names, input) {
259
+ return names.filter((name) => name.startsWith("--") || name.startsWith("/") || name.startsWith("-") && name.length > 2).map((name) => name + (name.startsWith("/") ? ":" : "=")).find((prefix) => input.startsWith(prefix));
260
+ }
256
261
  /**
257
262
  * Internal sync helper for option suggest functionality.
258
263
  * @internal
259
264
  */
260
265
  function* suggestOptionSync(optionNames$1, valueParser, hidden, description, context, prefix) {
261
266
  if (hidden || context.optionsTerminated) return;
262
- const equalsIndex = prefix.indexOf("=");
263
- if (equalsIndex >= 0) {
264
- const optionPart = prefix.slice(0, equalsIndex);
265
- const valuePart = prefix.slice(equalsIndex + 1);
267
+ const expectingValue = context.buffer.length > 0 && optionNames$1.includes(context.buffer[context.buffer.length - 1]);
268
+ const joinedPrefix = joinedOptionPrefix(optionNames$1, prefix);
269
+ if (joinedPrefix !== void 0 && !expectingValue) {
270
+ const optionPart = joinedPrefix.slice(0, -1);
271
+ const valuePart = prefix.slice(joinedPrefix.length);
266
272
  if (optionNames$1.includes(optionPart)) {
267
273
  if (valueParser && valueParser.suggest) {
268
274
  const valueSuggestions = getSuggestionsWithDependency(valueParser, valuePart, context.dependencyRegistry, context.exec);
269
275
  for (const suggestion of valueSuggestions) if (suggestion.kind === "literal") yield {
270
276
  kind: "literal",
271
- text: `${optionPart}=${suggestion.text}`,
272
- description: suggestion.description
273
- };
274
- else yield {
275
- kind: "literal",
276
- text: `${optionPart}=${suggestion.pattern || ""}`,
277
+ text: `${joinedPrefix}${suggestion.text}`,
277
278
  description: suggestion.description
278
279
  };
280
+ else yield joinedFileSuggestion(joinedPrefix, suggestion, valuePart);
279
281
  }
280
282
  }
281
283
  } else {
282
- const expectingValue = context.buffer.length > 0 && optionNames$1.includes(context.buffer[context.buffer.length - 1]);
283
284
  if (!expectingValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
284
285
  for (const optionName$1 of optionNames$1) if (optionName$1.startsWith(prefix)) {
285
286
  if (prefix === "-" && optionName$1.length !== 2) continue;
@@ -344,27 +345,23 @@ async function* getSuggestionsWithDependencyAsync(valueParser, prefix, dependenc
344
345
  */
345
346
  async function* suggestOptionAsync(optionNames$1, valueParser, hidden, description, context, prefix) {
346
347
  if (hidden || context.optionsTerminated) return;
347
- const equalsIndex = prefix.indexOf("=");
348
- if (equalsIndex >= 0) {
349
- const optionPart = prefix.slice(0, equalsIndex);
350
- const valuePart = prefix.slice(equalsIndex + 1);
348
+ const expectingValue = context.buffer.length > 0 && optionNames$1.includes(context.buffer[context.buffer.length - 1]);
349
+ const joinedPrefix = joinedOptionPrefix(optionNames$1, prefix);
350
+ if (joinedPrefix !== void 0 && !expectingValue) {
351
+ const optionPart = joinedPrefix.slice(0, -1);
352
+ const valuePart = prefix.slice(joinedPrefix.length);
351
353
  if (optionNames$1.includes(optionPart)) {
352
354
  if (valueParser && valueParser.suggest) {
353
355
  const valueSuggestions = getSuggestionsWithDependencyAsync(valueParser, valuePart, context.dependencyRegistry, context.exec);
354
356
  for await (const suggestion of valueSuggestions) if (suggestion.kind === "literal") yield {
355
357
  kind: "literal",
356
- text: `${optionPart}=${suggestion.text}`,
357
- description: suggestion.description
358
- };
359
- else yield {
360
- kind: "literal",
361
- text: `${optionPart}=${suggestion.pattern || ""}`,
358
+ text: `${joinedPrefix}${suggestion.text}`,
362
359
  description: suggestion.description
363
360
  };
361
+ else yield joinedFileSuggestion(joinedPrefix, suggestion, valuePart);
364
362
  }
365
363
  }
366
364
  } else {
367
- const expectingValue = context.buffer.length > 0 && optionNames$1.includes(context.buffer[context.buffer.length - 1]);
368
365
  if (!expectingValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
369
366
  for (const optionName$1 of optionNames$1) if (optionName$1.startsWith(prefix)) {
370
367
  if (prefix === "-" && optionName$1.length !== 2) continue;
@@ -1806,16 +1803,29 @@ function command(name, parser, options = {}) {
1806
1803
  * // mycli --debug --foo bar
1807
1804
  * // → { debug: true, extra: ["--foo", "bar"] }
1808
1805
  *
1809
- * // greedy format: captures all remaining tokens
1810
- * const parser = command("exec", object({
1811
- * container: argument(string()),
1812
- * args: passThrough({ format: "greedy" }),
1813
- * }));
1806
+ * // greedy format: require a program name before capturing the rest
1807
+ * const parser = command("exec", map(
1808
+ * seq(
1809
+ * argument(string({ metavar: "PROGRAM" })),
1810
+ * passThrough({ format: "greedy" }),
1811
+ * ),
1812
+ * ([program, args]) => ({ program, args }),
1813
+ * ));
1814
1814
  *
1815
- * // myproxy exec mycontainer --verbose -it bash
1816
- * // → { container: "mycontainer", args: ["--verbose", "-it", "bash"] }
1815
+ * // myproxy exec my-program --help
1816
+ * // → { program: "my-program", args: ["--help"] }
1817
1817
  * ```
1818
1818
  *
1819
+ * With built-in help enabled, `myproxy exec --help` displays help because `seq()`
1820
+ * does not reach pass-through until the program name is parsed. After the
1821
+ * program name, greedy capture can consume remaining tokens, including
1822
+ * `--help`, so they can be forwarded. Putting this parser last in an `object()`
1823
+ * does not establish the same boundary: fields are tried by priority, not
1824
+ * declaration order.
1825
+ * An immediate `--` after the program name is consumed by `seq()` as an
1826
+ * options terminator before greedy capture starts. Once capture starts,
1827
+ * subsequent `--` tokens are kept.
1828
+ *
1819
1829
  * @since 0.8.0
1820
1830
  */
1821
1831
  function passThrough(options = {}) {
@@ -1,5 +1,6 @@
1
1
  const require_usage = require('./usage.cjs');
2
2
  const require_message = require('./message.cjs');
3
+ const require_joined_file_suggestion = require('./internal/joined-file-suggestion.cjs');
3
4
 
4
5
  //#region src/suggestion.ts
5
6
  /**
@@ -322,22 +323,27 @@ function appendValueHint(base, input, candidates, options) {
322
323
  *
323
324
  * For literal suggestions, the text itself is used as the key.
324
325
  * For file suggestions, a composite key is created from the type,
325
- * extensions, and pattern.
326
+ * extensions, pattern, and insertion prefix.
326
327
  *
327
328
  * @param suggestion The suggestion to create a key for
328
329
  * @returns A string key that uniquely identifies this suggestion
329
330
  * @internal
330
331
  */
331
332
  function getSuggestionKey(suggestion) {
332
- if (suggestion.kind === "literal") return suggestion.text;
333
- return `__FILE__:${suggestion.type}:${suggestion.extensions?.toSorted().join(",") ?? ""}:${suggestion.pattern ?? ""}`;
333
+ if (suggestion.kind === "literal") return `literal:${suggestion.text}`;
334
+ return `file:${JSON.stringify([
335
+ suggestion.type,
336
+ suggestion.extensions?.toSorted().join(",") ?? "",
337
+ suggestion.pattern ?? "",
338
+ suggestion.prefix ?? ""
339
+ ])}`;
334
340
  }
335
341
  /**
336
342
  * Removes duplicate suggestions from an array while preserving order.
337
343
  *
338
344
  * Suggestions are considered duplicates if they have the same key:
339
345
  * - Literal suggestions: same text
340
- * - File suggestions: same type, extensions, and pattern
346
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
341
347
  *
342
348
  * The first occurrence of each unique suggestion is kept. For file
343
349
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -370,7 +376,8 @@ function deduplicateSuggestions(suggestions) {
370
376
  if (existing == null) {
371
377
  entries.set(key, suggestion);
372
378
  order.push(key);
373
- } else if (suggestion.kind === "file" && existing.kind === "file" && suggestion.includeHidden === true && existing.includeHidden !== true) entries.set(key, {
379
+ } else if (suggestion.kind === "literal" && existing.kind === "literal") require_joined_file_suggestion.mergeJoinedFileSuggestions(existing, suggestion);
380
+ else if (suggestion.kind === "file" && existing.kind === "file" && suggestion.includeHidden === true && existing.includeHidden !== true) entries.set(key, {
374
381
  ...existing,
375
382
  includeHidden: true
376
383
  });
@@ -202,7 +202,7 @@ declare function appendValueHint(base: Message, input: string, candidates: reado
202
202
  *
203
203
  * Suggestions are considered duplicates if they have the same key:
204
204
  * - Literal suggestions: same text
205
- * - File suggestions: same type, extensions, and pattern
205
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
206
206
  *
207
207
  * The first occurrence of each unique suggestion is kept. For file
208
208
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -202,7 +202,7 @@ declare function appendValueHint(base: Message, input: string, candidates: reado
202
202
  *
203
203
  * Suggestions are considered duplicates if they have the same key:
204
204
  * - Literal suggestions: same text
205
- * - File suggestions: same type, extensions, and pattern
205
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
206
206
  *
207
207
  * The first occurrence of each unique suggestion is kept. For file
208
208
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -1,5 +1,6 @@
1
1
  import { extractCommandNames, extractOptionNames, isSuggestionHidden } from "./usage.js";
2
2
  import { lineBreak, message, optionName, text, value } from "./message.js";
3
+ import { mergeJoinedFileSuggestions } from "./internal/joined-file-suggestion.js";
3
4
 
4
5
  //#region src/suggestion.ts
5
6
  /**
@@ -322,22 +323,27 @@ function appendValueHint(base, input, candidates, options) {
322
323
  *
323
324
  * For literal suggestions, the text itself is used as the key.
324
325
  * For file suggestions, a composite key is created from the type,
325
- * extensions, and pattern.
326
+ * extensions, pattern, and insertion prefix.
326
327
  *
327
328
  * @param suggestion The suggestion to create a key for
328
329
  * @returns A string key that uniquely identifies this suggestion
329
330
  * @internal
330
331
  */
331
332
  function getSuggestionKey(suggestion) {
332
- if (suggestion.kind === "literal") return suggestion.text;
333
- return `__FILE__:${suggestion.type}:${suggestion.extensions?.toSorted().join(",") ?? ""}:${suggestion.pattern ?? ""}`;
333
+ if (suggestion.kind === "literal") return `literal:${suggestion.text}`;
334
+ return `file:${JSON.stringify([
335
+ suggestion.type,
336
+ suggestion.extensions?.toSorted().join(",") ?? "",
337
+ suggestion.pattern ?? "",
338
+ suggestion.prefix ?? ""
339
+ ])}`;
334
340
  }
335
341
  /**
336
342
  * Removes duplicate suggestions from an array while preserving order.
337
343
  *
338
344
  * Suggestions are considered duplicates if they have the same key:
339
345
  * - Literal suggestions: same text
340
- * - File suggestions: same type, extensions, and pattern
346
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
341
347
  *
342
348
  * The first occurrence of each unique suggestion is kept. For file
343
349
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -370,7 +376,8 @@ function deduplicateSuggestions(suggestions) {
370
376
  if (existing == null) {
371
377
  entries.set(key, suggestion);
372
378
  order.push(key);
373
- } else if (suggestion.kind === "file" && existing.kind === "file" && suggestion.includeHidden === true && existing.includeHidden !== true) entries.set(key, {
379
+ } else if (suggestion.kind === "literal" && existing.kind === "literal") mergeJoinedFileSuggestions(existing, suggestion);
380
+ else if (suggestion.kind === "file" && existing.kind === "file" && suggestion.includeHidden === true && existing.includeHidden !== true) entries.set(key, {
374
381
  ...existing,
375
382
  includeHidden: true
376
383
  });
@@ -960,7 +960,8 @@ function prefixKeyValueSuggestion(suggestion, prefix, suffix = "") {
960
960
  const text$1 = `${prefix}${textValue}${suffix}`;
961
961
  if (suggestion.kind === "file" && suffix === "") return {
962
962
  ...suggestion,
963
- pattern: text$1
963
+ pattern: text$1,
964
+ prefix: `${prefix}${suggestion.prefix ?? ""}`
964
965
  };
965
966
  return suggestion.description == null ? {
966
967
  kind: "literal",
@@ -960,7 +960,8 @@ function prefixKeyValueSuggestion(suggestion, prefix, suffix = "") {
960
960
  const text$1 = `${prefix}${textValue}${suffix}`;
961
961
  if (suggestion.kind === "file" && suffix === "") return {
962
962
  ...suggestion,
963
- pattern: text$1
963
+ pattern: text$1,
964
+ prefix: `${prefix}${suggestion.prefix ?? ""}`
964
965
  };
965
966
  return suggestion.description == null ? {
966
967
  kind: "literal",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.3.5",
3
+ "version": "1.3.7-dev.3001",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -237,7 +237,7 @@
237
237
  },
238
238
  "sideEffects": false,
239
239
  "devDependencies": {
240
- "@optique/env": "1.3.5",
240
+ "@optique/env": "1.3.7-dev.3001+a1175c5b",
241
241
  "@types/node": "^24.0.0",
242
242
  "fast-check": "^4.7.0",
243
243
  "tsdown": "^0.13.0",
@@ -243,8 +243,8 @@ Common mistakes checklist
243
243
  `withDefault(..., false)` for ordinary optional flags.
244
244
  - Do not expect `multiple(p)` to fail when absent; it returns `[]`. Wrap with
245
245
  `nonEmpty()` when at least one value is required.
246
- - Do not confuse free-order parsing with `seq()`. Most constructs let child
247
- parsers compete by priority; use `seq()` only for truly ordered grammars.
246
+ - For ordered grammars, use `seq()`; `object()` fields compete by priority.
247
+ In wrappers, require the program name before greedy `passThrough()`.
248
248
  - Do not concatenate plain strings for errors or descriptions. Use structured
249
249
  `message` values.
250
250
  - Register contexts for `bindEnv()`, `bindConfig()`, `bindDerivedDefault()`,