Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
145 commits
Select commit Hold shift + click to select a range
dc1afef
feat(completion): add in-process JS resolver for dynamic value comple…
dqn Apr 28, 2026
632ef21
fix(completion): close gaps in dynamic resolver delivery
dqn Apr 28, 2026
be5905f
fix(completion): honor resolver directives in zsh and fish
dqn Apr 28, 2026
9ba073b
fix(completion): preserve globals across descent and forward token to…
dqn Apr 28, 2026
a714d05
fix(completion): protect colon-prefixed candidates and route shadowed…
dqn Apr 28, 2026
1cb097e
refactor(completion): collapse resolver context indirection and dedup…
dqn Apr 28, 2026
e8d80ea
test(completion): collapse __complete boilerplate into a runComplete …
dqn Apr 28, 2026
d7261b9
refactor(completion): drop the dynamic-completion shim and unused typ…
dqn Apr 28, 2026
827b9a3
test(completion): pin that the trailing currentWord is not absorbed a…
dqn Apr 28, 2026
8f573a0
docs(api): document the globalArgsSchema parameter and CompletionCont…
dqn Apr 28, 2026
6391faa
Merge remote-tracking branch 'origin/main' into feat/dynamic-completi…
dqn May 9, 2026
7b299db
Merge remote-tracking branch 'origin/main' into feat/dynamic-completi…
dqn May 13, 2026
466ed19
Merge remote-tracking branch 'origin/main' into feat/dynamic-completi…
dqn May 18, 2026
bbbad4f
feat(completion): add `expand` for pre-enumerated value completion
dqn May 19, 2026
e04f668
feat(completion): dedup repeated `-f key=value` slots in expand
dqn May 19, 2026
ec08d13
fix(completion): address codex-review feedback for expand under aliases
dqn May 19, 2026
09939d3
fix(completion): cover empty expand keys and boolean siblings
dqn May 19, 2026
cca2404
fix(completion): bound zsh delegate context and harden fish case escapes
dqn May 19, 2026
14fa244
fix(completion): close more codex-review gaps in dynamic + expand paths
dqn May 19, 2026
8553fcd
fix(completion): align resolver `parsedArgs` with runtime negation rules
dqn May 19, 2026
75f74ea
fix(completion): skip array-expand dedup tracking on the cursor token
dqn May 19, 2026
792fee0
fix(completion): isolate expand state per frame and keep zsh resolver…
dqn May 20, 2026
c68cc6d
fix(completion): preserve global expand state across subcommand descent
dqn May 20, 2026
0d409ac
fix(completion): isolate dep buckets per scope and accept short -X=val
dqn May 20, 2026
666cca8
fix(completion): sanitize fish tracker variable names
dqn May 20, 2026
16d00e5
fix(completion): keep bash expand specs from leaking into file comple…
dqn May 20, 2026
511db0a
fix(completion): valid digit-prefixed BIN var name and frame-bounded …
dqn May 20, 2026
a4232c0
fix(completion): preserve global expand deps when locals shadow the name
dqn May 20, 2026
c4fa0db
fix(completion): match runtime per-frame array merge and dir-only bas…
dqn May 20, 2026
a8077c7
fix(completion): do not consume the next option as a value
dqn May 20, 2026
7277aec
fix(completion): per-frame global expand dedup and combined short flags
dqn May 20, 2026
1607650
fix(completion): merge resolver candidates with file directives in bash
dqn May 20, 2026
9a46321
fix(completion): skip global expand tracker on frames where a local s…
dqn May 20, 2026
db95df7
fix(completion): index alias-expanded paths so global trackers surviv…
dqn May 20, 2026
cad3d8a
fix(completion): keep __complete reachable without globals and prefix…
dqn May 20, 2026
6ed8d3c
fix(completion): suppress global effects during __complete and stop s…
dqn May 20, 2026
3f96d21
fix(completion): only strip inline prefix when completing an option v…
dqn May 20, 2026
380ab75
fix(completion): consider cliName and aliases when shadowing global deps
dqn May 20, 2026
495dbf6
refactor: simplify (extract shared shell-completion helpers)
dqn May 20, 2026
ac56cfb
refactor: simplify (store TrackedFieldRef tokens pre-rendered)
dqn May 20, 2026
f9dc2f9
refactor: simplify (collapse repeated context-parser/extractor helpers)
dqn May 20, 2026
a9048ad
refactor: simplify (trim restated comments in __complete dispatch)
dqn May 20, 2026
ffc0633
refactor: simplify (collapse bash skip-next tracking branches)
dqn May 20, 2026
746a925
refactor: simplify (collapse zsh/fish skip-next tracking branches)
dqn May 20, 2026
41c7ea6
refactor: simplify (collapse hasExpand branches in main scan loops)
dqn May 20, 2026
ef4d7dd
refactor: simplify (drop dead defaults and StaticSibling wrapper)
dqn May 20, 2026
1e8b035
refactor: simplify (collapse extractor value-completion assignment)
dqn May 20, 2026
fc4a33a
refactor: simplify (trim redundant __complete-skip comments in runner)
dqn May 20, 2026
a90a70b
refactor: simplify (collapse bash file/dir/cmd inline branches)
dqn May 20, 2026
926d513
refactor: simplify (share bash/zsh tracker case-line builders)
dqn May 20, 2026
aee84fb
refactor: simplify (fold bash choices inline branches)
dqn May 20, 2026
d3675c2
refactor: simplify (share bash/zsh subcommand dispatch builder)
dqn May 20, 2026
e58fc96
refactor: simplify (collapse availableOption negation branches)
dqn May 20, 2026
05bbb4c
refactor: simplify (drop redundant fish valueCompletionBlock wrapper)
dqn May 20, 2026
fae59dd
refactor: simplify (share collectOptionTokens for bash/zsh option-val…
dqn May 20, 2026
8be4998
refactor: simplify (share quoted availability token list across shells)
dqn May 20, 2026
4fb0b0d
fix(completion): emit global expand trackers at intermediate frames
dqn May 20, 2026
942ca3b
fix(completion): hoist fish array index to a local before quoted subs…
dqn May 20, 2026
e6593bf
Merge remote-tracking branch 'origin/main' into feat/dynamic-completi…
dqn May 20, 2026
1522b85
feat(completion): make generated bash scripts run on bash 3.2
dqn May 20, 2026
d6b8c71
ci: drop macOS bash 3.2 job in favor of snapshot guards
dqn May 20, 2026
7d33f8b
fix: address codex-review feedback (bash expand key encoding, inline …
dqn May 20, 2026
b59d536
fix: address codex-review feedback (parent-frame globals, fish multi-…
dqn May 20, 2026
26f66f8
fix(completion): use fish `bitand()` for directive bit checks
dqn May 20, 2026
fcb2120
fix(completion): drop only the colliding global tokens at a shadowed …
dqn May 20, 2026
1478d5c
fix: address codex-review feedback (fish printf, bash 3.2 docs)
dqn May 20, 2026
bc865a8
fix(completion): recognize alias-based implicit negation in parsedArgs
dqn May 20, 2026
c7f5ebb
fix(completion): match single-char custom negation in matchesExplicit
dqn May 20, 2026
b0aa833
fix(completion): migrate token-colliding local values to globals on d…
dqn May 20, 2026
9c42de3
fix(completion): align global option routing with runtime scan preced…
dqn May 20, 2026
284d2e9
fix(completion): mirror runtime last-wins and seed bash 3.2 no-file s…
dqn May 20, 2026
6ba516a
fix(completion): seed bash 3.2 sentinel when expand candidates filter…
dqn May 20, 2026
b068c1a
fix(completion): recognize short form of single-character cliNames
dqn May 20, 2026
923c672
fix(completion): align option matching with runtime alias / short / c…
dqn May 20, 2026
ec523bc
fix(completion): give global short aliases precedence over bare 1-cha…
dqn May 20, 2026
13eee68
fix(completion): accept short forms (`-f=value`, `-x`) in bash + fish…
dqn May 20, 2026
eddd8d0
fix(completion): classify short inline `-f=value` as option-value in …
dqn May 20, 2026
566c7b6
fix(completion): emit every alias form runtime aliasMap accepts
dqn May 20, 2026
7bf60ad
fix(completion): unify takes-value / value-trigger tokens across bash…
dqn May 20, 2026
9151a14
fix(completion): drop local short tokens stolen by globals and gate i…
dqn May 20, 2026
b165983
fix(completion): resolve expand after globals are known, mirror spell…
dqn May 20, 2026
fc36efc
fix(completion): resolve per-dep scope, drop shadowed global tokens, …
dqn May 20, 2026
70db3b3
refactor: simplify (drop no-op shell-ternary in trackArrayExpandCaseL…
dqn May 20, 2026
1fed5c8
refactor: simplify (reuse collectOptionTokens in dynamic context-parser)
dqn May 20, 2026
a787a24
refactor: simplify (drop duplicate doc comment on effectiveOptionTokens)
dqn May 20, 2026
dc4acaf
fix(completion): two-stage key=value UX for resolve/expand candidates
dqn May 20, 2026
a3542af
refactor: simplify (tighten two-stage key=value collapse helpers)
dqn May 20, 2026
738ec0b
refactor: simplify (unify option/positional value candidate paths)
dqn May 20, 2026
3cd4137
refactor: simplify (collapse normalise/subcommand-lookup branches)
dqn May 20, 2026
ff7b10b
fix(completion): honor NoSpace directive in zsh dynamic apply
dqn May 20, 2026
d9ec385
refactor: simplify (extract shared trackedFieldAssign helper)
dqn May 20, 2026
5e04738
refactor: simplify (share alias-token loop in extractor)
dqn May 20, 2026
dad24e7
refactor: simplify (extract matchesCamelCase helper in context-parser)
dqn May 20, 2026
9c2cef1
refactor: simplify (extract globalShortTokens helper)
dqn May 20, 2026
307abfb
fix(completion): drop bare key= from expand value-stage candidates
dqn May 20, 2026
4f3c955
fix(completion): drop `--` before -S '' in zsh _describe calls
dqn May 20, 2026
ae1e747
chore(completion): clean up PR 353 artifacts
dqn May 20, 2026
732ccb1
fix: address codex-review feedback (ancestor global tokens, bash expa…
dqn May 20, 2026
fef7505
fix: address codex-review feedback (full local-owned spellings)
dqn May 20, 2026
f9e1954
fix: address codex-review feedback (skip empty case patterns)
dqn May 20, 2026
a3346b7
fix: address codex-review feedback (pre-sub global pre-pass, local-pr…
dqn May 20, 2026
5cece1f
fix: address codex-review feedback (local shadow, hidden alias, bash …
dqn May 20, 2026
b8cfe23
fix: address codex-review feedback (explicit globals before implicit …
dqn May 20, 2026
92d3a19
fix: address codex-review feedback (preserve bash default fallback)
dqn May 20, 2026
e76f762
fix: address codex-review feedback (nested global pre-scan, ancestor …
dqn May 20, 2026
91a24ce
fix: address codex-review feedback (unescape zsh expand dedup key)
dqn May 20, 2026
7676d6e
fix: address codex-review feedback (shadowed global emission, zsh exp…
dqn May 20, 2026
add3218
fix: address codex-review feedback (drop bare key= candidates in valu…
dqn May 20, 2026
4947a89
fix: address codex-review feedback (precise key= filter, zsh desc-awa…
dqn May 20, 2026
3cfadaf
refactor: simplify (extract groupGlobalFramesByTokenSet helper)
dqn May 20, 2026
30bb0d3
refactor: simplify (collapse resolveExpandDepGlobality call sites)
dqn May 20, 2026
6a9ec07
refactor: simplify (extract isNegationOf helper in context-parser)
dqn May 20, 2026
92eca9f
refactor: simplify (split key=value stages into helper functions)
dqn May 20, 2026
3d27aeb
refactor: simplify (extract writeOptionValue helper)
dqn May 20, 2026
5a61ecb
refactor: simplify (dedupe localShadowingTokens across shell modules)
dqn May 20, 2026
ef4cd1c
refactor: simplify (centralize collectOptionTokens in shell-shared)
dqn May 20, 2026
70ef386
refactor: simplify (share BaseExpandLocation builders across bash/zsh…
dqn May 20, 2026
01b249f
refactor: simplify (reuse findOption for pre-sub global scan)
dqn May 20, 2026
2c9d412
refactor: simplify (extract schema fields once per subcommand frame)
dqn May 20, 2026
05d1e8b
refactor: simplify (inline thin candidate-generator switch wrappers)
dqn May 20, 2026
b8a0147
refactor: simplify (extract clampToVariadic helper, drop string[] cast)
dqn May 20, 2026
3116b2d
refactor: simplify (reuse fieldsToOptions for global extraction)
dqn May 20, 2026
520dc32
refactor: simplify (share opt-takes-value tree walk across shells)
dqn May 20, 2026
2fbb0f7
refactor: simplify (share expandTableVarName across bash/zsh)
dqn May 20, 2026
be5e5eb
refactor: simplify (extract trackerVar helper in fish trackers)
dqn May 20, 2026
578d0e4
refactor: simplify (share positional-target resolution in candidate gen)
dqn May 20, 2026
141f986
refactor: simplify (drop unused let-directive in dynamic candidate gen)
dqn May 20, 2026
3800bf6
refactor: simplify (share dynamic __invoke_complete body across bash/…
dqn May 20, 2026
1ee0f7b
refactor: simplify (extract unsetPrefixed helper in bash generator)
dqn May 20, 2026
67bd9d8
refactor: share FuncSuffixedExpandLocation across bash/zsh
dqn May 21, 2026
a0f88cf
refactor: rename dynamicInvokeCompleteLines, lift pushUnique, reuse j…
dqn May 21, 2026
36758e5
refactor: drop redundant positional re-resolution in resolver context
dqn May 21, 2026
deb9b87
refactor: return processed candidates directly from value resolution
dqn May 21, 2026
17fcad1
refactor: unify canonical/alias recursion in extractor walks
dqn May 21, 2026
f9e6ce8
refactor: inline single-use bash formatter helpers
dqn May 21, 2026
e36bef1
fix(completion): populate bash DirectoryCompletion via compgen for 3.…
dqn May 21, 2026
2768efd
fix(completion): isolate key=value rewrite and respect `--` for help
dqn May 21, 2026
fbd6745
fix(completion): seed empty COMPREPLY for bash 3.2 dir-only directive
dqn May 21, 2026
8c1fd3b
fix(completion): normalize inline option-value prefix inside generate…
dqn May 21, 2026
19c3658
test(shell-completion): raise beforeAll hook timeout for shell projects
dqn May 21, 2026
5b44a52
fix(completion): tag bash dynamic file/dir candidates with -o filenames
dqn May 21, 2026
219f942
fix(completion): preserve NoSpace and avoid double-escape in zsh
dqn May 21, 2026
56f9c62
refactor: tidy candidate-generator precedence and fish apply hot path
dqn May 21, 2026
20e8771
refactor: parenthesize resolver directive fallback in candidate-gener…
dqn May 21, 2026
612e5c3
Merge remote-tracking branch 'origin/main' into feat/dynamic-completi…
dqn May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/dynamic-completion-resolver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"politty": minor
---

Add `completion.custom.resolve` for in-process JS dynamic completion. The resolver receives a `DynamicCompletionContext` (current word, shell, other parsed arg values, previously supplied values) and returns candidates synchronously or via Promise. Static shell scripts (bash/zsh/fish) now delegate to `<program> __complete --shell <shell>` whenever a field uses `resolve`; the generated bash delegate stays compatible with Bash 3.2. Specifying more than one of `choices`, `shellCommand`, `resolve`, or `expand` on the same field throws.

Type-level note: `generateCandidates(context, { shell })` now returns `Promise<CandidateResult>` and takes a required second argument. `__complete`'s internal `run` is async. Callers using only the high-level `withCompletionCommand` flow are unaffected.
5 changes: 5 additions & 0 deletions .changeset/expand-completion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"politty": patch
---

Add `completion.custom.expand` for value completion that is pre-enumerated at script-generation time and baked into the static shell script. The user supplies `dependsOn` (sibling arg names that must have static `choices` or an enum schema) and `enumerate(deps)`; politty walks the cartesian product of the dependsOn values, calls `enumerate` for each combination, and emits Bash 3.2-compatible scalar variables, a hoisted associative array (zsh), or an inline switch (fish) keyed on those values. No Node process is spawned at TAB time — the shell dispatches via a case lookup or indirect-expansion lookup, taking the same `<10ms` path as static `choices`. Specifying more than one of `choices`, `shellCommand`, `resolve`, or `expand` on the same field throws.
96 changes: 89 additions & 7 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ const mainCommand = withCompletionCommand(

// Now includes:
// - mycli completion bash|zsh|fish
// - mycli __complete -- <args>
// - mycli __complete --shell <shell> -- <args>

runMain(mainCommand);
```
Expand Down Expand Up @@ -346,7 +346,11 @@ console.log(result.script);
Creates the hidden `__complete` command for dynamic completion.

```typescript
function createDynamicCompleteCommand(rootCommand: AnyCommand, programName?: string): Command;
function createDynamicCompleteCommand(
rootCommand: AnyCommand,
programName?: string,
globalArgsSchema?: ArgsSchema,
): Command;
```

#### Usage
Expand All @@ -355,7 +359,7 @@ The `__complete` command is automatically added by `withCompletionCommand`. It c

```bash
# Get completions for "mycli build --"
mycli __complete -- build --
mycli __complete --shell fish -- build --

# Output (tab-separated: value\tdescription)
--watch Watch mode
Expand All @@ -377,7 +381,11 @@ The last line (`:N`) is a directive that tells the shell how to handle completio
Parses a partial command line to determine what kind of completion is needed.

```typescript
function parseCompletionContext(argv: string[], rootCommand: AnyCommand): CompletionContext;
function parseCompletionContext(
argv: string[],
rootCommand: AnyCommand,
globalArgsSchema?: ArgsSchema,
): CompletionContext;
```

#### Return Value
Expand All @@ -395,6 +403,8 @@ interface CompletionContext {
subcommands: string[]; // Available subcommands
positionals: CompletablePositional[];
usedOptions: Set<string>; // Already used options
parsedArgs: Record<string, unknown>; // Other arg values (for dynamic resolvers)
previousValues: string[]; // Prior values for the option/positional being completed
}

type CompletionType = "subcommand" | "option-name" | "option-value" | "positional";
Expand All @@ -404,10 +414,13 @@ type CompletionType = "subcommand" | "option-name" | "option-value" | "positiona

### `generateCandidates`

Generates completion candidates based on context.
Generates completion candidates based on context. Async because dynamic resolvers may return promises.

```typescript
function generateCandidates(context: CompletionContext): CandidateResult;
function generateCandidates(
context: CompletionContext,
options: { shell: "bash" | "zsh" | "fish" },
): Promise<CandidateResult>;
```

#### Return Value
Expand Down Expand Up @@ -435,18 +448,87 @@ Completion configuration for arguments.
interface CompletionMeta {
/** Completion type */
type?: "file" | "directory" | "none";
/** Custom completion */
/** Custom completion (mutually exclusive: choices | shellCommand | resolve | expand) */
custom?: {
/** Static choices */
choices?: string[];
/** Shell command for dynamic values */
shellCommand?: string;
/** In-process JS resolver (see DynamicCompletionResolver) */
resolve?: DynamicCompletionResolver;
/** Pre-enumerated completion baked into the static shell script (see ExpandCompletion) */
expand?: ExpandCompletion;
};
/** File extension filters (for type: "file") */
extensions?: string[];
}
```

---

### `DynamicCompletionResolver`

Callback invoked at completion time inside the `__complete` command.

```typescript
type DynamicCompletionResolver = (
ctx: DynamicCompletionContext,
) => DynamicCompletionResult | Promise<DynamicCompletionResult>;

interface DynamicCompletionContext {
/** Word being completed (`--field=` inline prefix is stripped before this is set). */
currentWord: string;
/** Target shell formatting requested by the caller. */
shell: "bash" | "zsh" | "fish";
/** Best-effort parsed values of OTHER args (camelCase keys, raw strings). */
parsedArgs: Readonly<Record<string, unknown>>;
/** Values already supplied for the same option/positional being completed. */
previousValues: readonly string[];
/** Subcommand path from root (e.g. ["api"]). */
subcommandPath: readonly string[];
}

interface DynamicCompletionResult {
candidates: Array<string | { value: string; description?: string }>;
/** Optional override; defaults to FilterPrefix | NoFileCompletion. */
directive?: number;
}
```

Specifying more than one of `choices`, `shellCommand`, `resolve`, or `expand` on the same field throws at command-definition time. Static shell scripts automatically delegate to `<program> __complete --shell <shell>` when a field uses `resolve`. With `expand`, the candidate table is computed at script-generation time and inlined into the script — TAB completion stays in-shell.

---

### `ExpandCompletion`

Pre-enumerated value completion. Use when candidates can be computed up front from a finite set of sibling arg values (each must have a static `choices` or enum schema). politty walks the cartesian product of the `dependsOn` values at script-generation time, calls `enumerate` for each combination, and bakes the resulting table into the static shell script.

```typescript
interface ExpandCompletion {
/**
* Sibling arg names (camelCase, same command) whose runtime values
* drive the candidate set. Each must have a static `choices` or enum
* schema. Chaining `expand` specs is not supported.
*/
dependsOn: readonly string[];
/**
* Pure function called once per combination of dependsOn values at
* script-generation time. Returns the candidates for that combination.
* Strings imply no description; objects carry an optional description
* surfaced by zsh and fish.
*/
enumerate: (
deps: Readonly<Record<string, string>>,
) => ReadonlyArray<string | { value: string; description?: string }>;
}
```

Properties and constraints:

- `dependsOn` must be non-empty and may not reference the field itself or any sibling without a static value set. Validation errors throw at command-definition time with the offending field name.
- `enumerate` runs synchronously at the time the user invokes `<program> completion <shell>`. politty does not retain it for runtime use; if it throws, the error is wrapped with the field name and the offending `deps` snapshot.
- bash emits one prefix-scalar variable per table entry (`<base>__<encKey>=<candidates>`) so the generated script runs on Bash 3.2 (macOS default `/bin/bash`) without associative arrays; zsh emits one global associative array per spec (`typeset -gA`); fish emits an inline `switch` (no associative arrays). bash drops descriptions; zsh uses `value:description` for `_describe`; fish uses tab-separated `value\tdescription`.

#### Example

```typescript
Expand Down
142 changes: 138 additions & 4 deletions docs/shell-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@ For quick setup, see the [README](../README.md#shell-completion). For type signa
`withCompletionCommand` adds two subcommands to your CLI:

- **`completion <shell>`** — Generates a shell script that users source in their shell config
- **`__complete`** (hidden) — The dynamic completion engine, called on every TAB press
- **`__complete`** (hidden) — The dynamic completion engine used by `completion.custom.resolve`

The generated shell scripts are thin wrappers. When a user presses TAB, the shell calls:
The generated shell scripts embed static metadata for subcommands, options,
`choices`, file/directory completion, and `expand` tables. These paths stay
inside the shell at TAB time.

When a field uses `completion.custom.resolve`, the generated script delegates
that value completion to the hidden command:

```
mycli __complete --shell bash -- <partial-tokens>
```

All logic runs in JavaScript: parsing the command line context, resolving candidates, and returning results with directives that tell the shell how to present them.
That command runs in JavaScript: it parses the partial command line, calls the
resolver, and returns candidates with directives that tell the shell how to
present them.

Command aliases defined via `aliases` in `defineCommand()` are automatically included in both static completion scripts and dynamic completion candidates.

Expand Down Expand Up @@ -61,6 +68,127 @@ branch: arg(z.string().optional(), {

The command has a 5-second timeout. If it fails or times out, no candidates are shown (stderr is suppressed).

### Resolve (in-process JS)

Compute candidates in the same process from a TS callback that has access to **other arg values typed so far**. Useful when completion depends on prior context — e.g. fields valid for the chosen endpoint, or columns in the chosen table.

```typescript
field: arg(z.array(z.string()).default([]), {
alias: "f",
completion: {
custom: {
resolve: ({ parsedArgs, previousValues }) => {
const endpoint = parsedArgs.endpoint as string | undefined;
if (!endpoint) return { candidates: [] };
const all = lookupFieldsFor(endpoint);
// De-dup keys already supplied via earlier `--field key=value` flags.
const used = new Set(previousValues.map((v) => v.split("=")[0]));
return { candidates: all.filter((k) => !used.has(k)) };
},
},
},
});
```

The callback receives a `DynamicCompletionContext`:

| Field | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `currentWord` | Word being completed. Inline `--field=` prefix is stripped before this is set. |
| `shell` | `"bash" \| "zsh" \| "fish"` — useful when output should differ between shells. |
| `parsedArgs` | Best-effort parsed values of OTHER args on the same command, keyed by camelCase name. Includes positionals (string, or string[] for variadic) and options (string for scalars, string[] for array options). Zod validation is **NOT** applied. |
| `previousValues` | Values already supplied for the option/positional being completed (for de-duping array options). |
| `subcommandPath` | Subcommand path leading here, e.g. `["api"]`. |

Return `{ candidates, directive? }` where `candidates` is an array of strings or `{ value, description }` objects. When `directive` is omitted it defaults to `FilterPrefix | NoFileCompletion` (matches `choices` behaviour).

The resolver runs **inside the `__complete` command**, so:

- Static shell scripts delegate to `<program> __complete --shell <shell>` whenever a field uses `resolve`. This is automatic — call `withCompletionCommand` and politty wires it up.
- The resolver may be async (returning `Promise<DynamicCompletionResult>`).
- If the resolver throws, completion silently returns no candidates (with `CompletionDirective.Error` set).
- Dot-notation key descent (`labels.foo.bar`) and oneof exclusivity are the resolver's responsibility — politty just passes `currentWord` through and strips the `--field=` inline prefix.
- `console.log` from inside the resolver pollutes the candidate stream; use `console.error` or a logger that writes to stderr instead.

For local dev, set `MYCLI_BIN` (uppercase program name) to override the binary the static script invokes — useful when the CLI hasn't been installed on PATH yet.

### Expand (pre-enumerated)

When all of the candidates can be computed up front from a small, known set
of sibling arg values, use `expand` instead of `resolve`. politty walks the
cartesian product of the `dependsOn` values at script-generation time, calls
`enumerate(deps)` once per combination, and bakes the resulting table into
the shell script. At TAB time the shell dispatches via a case lookup keyed
on the runtime values of those args — **no Node process is spawned**, so
the latency matches static `choices` (typically <10ms).

```typescript
field: arg(z.array(z.string()).default([]), {
alias: "f",
completion: {
custom: {
expand: {
dependsOn: ["endpoint"],
enumerate: ({ endpoint }) => {
return getFieldsFor(endpoint).map((k) => ({
value: `${k}=`,
description: `Set ${k}`,
}));
},
},
},
},
});
```

Requirements:

- Every name in `dependsOn` must be a **sibling arg on the same command**
with a static value set (an explicit `completion.custom.choices` or an
enum schema). Chaining `expand` specs is not supported.
- `enumerate` must be a pure function of `deps`. politty calls it once per
combination at the time the user runs `<program> completion <shell>`. If
it throws, the error is wrapped with the offending field name and the
`deps` snapshot.
- Mixing `expand` with `choices`, `shellCommand`, or `resolve` on the same
field throws at command-definition time.
- For multi-dimensional `dependsOn`, the runtime lookup key is the
concatenation of dep values joined by U+001F. Avoid sibling choices that
contain that byte (none in practice).

Use this whenever the dependency graph collapses cleanly to a finite,
build-time-known set. Reach for `resolve` when the candidates depend on
process-local state the shell cannot observe (filesystem reads, network
calls, parsing the schema-of-the-day, etc.).

#### Array option deduplication (`-f key=value` repeats)

When `expand` is attached to a repeatable **array option** (`z.array(...)`),
the generated shell script automatically drops any candidate whose `key=`
prefix has already been consumed on the same command line. That is, for the
example above:

```
$ mycli api GetApplication -f workspaceId=foo -f <TAB>
applicationName= # workspaceId= is filtered out
```

The dedup logic:

- Splits both the user-typed value and each candidate on the first `=`
and treats everything to the left of `=` as the slot key. There is no
configurable delimiter — `key=value` is the assumed shape.
- Only fires for option fields with `valueType === "array"`. Scalar
options and positionals are not deduped (repeating them has different
semantics).
- Candidates that contain no `=` pass through untouched (e.g. plain enum
values used as a repeatable list keep duplicating, since they don't
carry a slot key).

If your CLI uses a different separator (e.g. `key:value`), this dedup
won't engage — the candidates are still emitted correctly, you just
won't get the automatic filtering.

### File Completion

Delegate to the shell's native file completion. Optionally filter by extension:
Expand Down Expand Up @@ -105,7 +233,7 @@ This is useful for secrets or tokens where file suggestions would be noise.

When multiple sources could provide completion values, the following priority applies:

1. **Explicit `custom`** — `choices` or `shellCommand`
1. **Explicit `custom`** — exactly one of `expand`, `resolve`, `choices`, or `shellCommand`. Specifying more than one throws at command-definition time.
2. **Explicit `type`** — `file`, `directory`, or `none`
3. **Auto-detected** — enum values from `z.enum()`

Expand Down Expand Up @@ -160,6 +288,12 @@ mycli completion bash > ~/.local/share/bash-completion/completions/mycli

Reload with `source ~/.bashrc`.

The generated bash script runs on **Bash 3.2 or newer**, including the
default `/bin/bash` shipped with macOS. The completion machinery (both
`completion.custom.expand` and `completion.custom.resolve`) avoids
bash 4 builtins — associative arrays are replaced with prefix-scalar
variables, and `mapfile` is replaced with a portable `while read` loop.

### Zsh

```bash
Expand Down
Loading
Loading