Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0fac901
feat: add fluent hidden options
autocarl Jul 25, 2026
93b4620
ci: skip test-reporter on fork pull requests
carldebilly Jul 25, 2026
78f51a3
fix(tests): trim stray CR from shell-completion candidates on Windows
carldebilly Jul 25, 2026
c38fc46
ci: add windows-latest to the cross-platform test matrix
carldebilly Jul 25, 2026
8c44dfe
test(mcp): pin the tools/call rejection for hidden options; correct t…
carldebilly Jul 25, 2026
b25e557
refactor(core): carry option visibility on the immutable option schema
carldebilly Jul 25, 2026
6bd6948
fix(core): invalidate routing when command or option visibility changes
carldebilly Jul 25, 2026
04805c4
fix(core): validate hidden-required options at configuration time
carldebilly Jul 25, 2026
b29b683
test(mcp): make the fixture observe MCP server start failures
carldebilly Jul 25, 2026
0f3fbe0
test(core): prove the legacy global-option descriptor forwards, and s…
carldebilly Jul 25, 2026
0e05458
feat(core): give global options their own builder type
carldebilly Jul 25, 2026
fa3e1e0
feat(core): model hidden options in the documentation model instead o…
carldebilly Jul 25, 2026
a785395
feat: add option-level AutomationHidden, mirroring the command axis
carldebilly Jul 25, 2026
c100259
feat(mcp): honour option-level AutomationHidden at one chokepoint; ad…
carldebilly Jul 25, 2026
e2eed19
refactor(core)!: make WithOption the only option-configuration entry …
carldebilly Jul 25, 2026
349d290
fix: honest diagnostics, and tests that fail when their guard is removed
carldebilly Jul 25, 2026
ba9bc7e
docs: document the option visibility axes, and that neither is access…
carldebilly Jul 25, 2026
671a756
test: cover yaml and xml documentation export
carldebilly Jul 25, 2026
9e5b3f9
fix: address the automated review — four real defects, one narrowed
carldebilly Jul 25, 2026
d7656e7
fix(help): decide global option visibility per token in help too
carldebilly Jul 25, 2026
60d105e
fix: complete the visibility guarantees the review found still open
carldebilly Jul 25, 2026
29d7aa4
fix: share global option token ownership across discovery
autocarl Jul 25, 2026
6d48be5
fix: honor service fallbacks in hidden option discovery
autocarl Jul 25, 2026
6933168
fix: use active services for programmatic documentation
autocarl Jul 25, 2026
72afa8c
fix: emit the owning global token spelling
autocarl Jul 25, 2026
7e106fd
fix: retry MCP snapshot refreshes safely
autocarl Jul 25, 2026
336b7e2
fix: include synthetic progress in option requiredness
autocarl Jul 25, 2026
bc08f09
test: use production services in MCP fixtures
autocarl Jul 25, 2026
138f61e
fix: keep documentation from freezing services
autocarl Jul 25, 2026
2b4e663
fix: suppress route tokens owned by globals
autocarl Jul 25, 2026
c231310
fix: model MCP progress channel during discovery
autocarl Jul 25, 2026
9e79af0
fix: honor global token ownership in documentation
autocarl Jul 25, 2026
29a9cb7
Support hidden legacy option aliases
autocarl Jul 25, 2026
a59794b
Document hidden legacy option aliases
autocarl Jul 25, 2026
5834f89
Cover hidden aliases on option groups
autocarl Jul 25, 2026
61bfbbd
Fix case-sensitive alias discovery and MCP mapping
autocarl Jul 25, 2026
786a93a
Fail closed on hidden discovery changes
autocarl Jul 25, 2026
d2b801d
Preserve exact MCP argument identity
autocarl Jul 25, 2026
87c9306
Retry snapshots after visibility retractions
autocarl Jul 25, 2026
8e98f72
Preserve MCP field provenance
autocarl Jul 25, 2026
ad388e4
Hide parser-equivalent fluent aliases
autocarl Jul 25, 2026
73feafa
Cover case-varied MCP answer prefills
autocarl Jul 25, 2026
0838355
Publish snapshot retractions atomically
autocarl Jul 26, 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
14 changes: 12 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
# Windows is a first-class target for a CLI/REPL framework, and its absence let a
# CRLF-only test failure ship green (see the TrimEntries fix in Given_Completions).
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Expand Down Expand Up @@ -194,7 +196,15 @@ jobs:
--coverage-output-format cobertura

- name: Test report
if: always()
# Creating a check run needs `checks: write`, but GitHub caps GITHUB_TOKEN
# to read-only for `pull_request` events raised from a fork — the
# workflow-level `permissions:` block cannot lift that ceiling. The action
# then fails with "Resource not accessible by integration", which cascades
# into skipping Pack and the package validation steps below. Fork PRs still
# gate on the Test step and publish the .trx via the test-results artifact.
# The `github.event_name` clause keeps the report alive on push builds,
# where `github.event.pull_request` is null.
if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
with:
name: Test Results
Expand Down
115 changes: 115 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,121 @@ app.Map(

Root help now includes a dedicated `Global Options:` section with built-ins plus custom options registered through `options.Parsing.AddGlobalOption<T>(...)`.

### Hiding individual options

Use `.WithOption(<targetName>, option => option.Hidden())` to hide one command option without hiding its command. The target is the CLR handler parameter or options-group property name, not the rendered `--option-name` token:

```csharp
app.Map(
"deploy",
([ReplOption(Name = "internal-mode")] bool internalMode = false) => internalMode)
.WithOption("internalMode", option => option.Hidden());
```

The callback shape is deliberate. `CommandBuilder` and `OptionBuilder` both expose `Hidden` and `AutomationHidden`, so an API returning the option builder would let `.Option("x").Hidden()` sit in a chain reading exactly like the command-level `.Hidden()` while meaning something quite different, with nothing to signal that the subject had changed. `WithOption` keeps the subject explicit and the chain on the command.

For declarative registration, set `Hidden = true` on `ReplOptionAttribute`. This works on direct parameters, options-group properties, and typed global-options properties:

```csharp
public sealed class DeploymentOptions
{
[ReplOption(Hidden = true)]
public string? InternalToken { get; set; }
}
```

Manually registered global options can be selected after registration:

```csharp
app.Options(options =>
{
options.Parsing.AddGlobalOption<string>("internal-tenant");
options.Parsing.GlobalOption("internal-tenant").Hidden();
});
```

#### Keeping a legacy alias callable but hidden

Do not hide the whole option when only an old spelling is deprecated. Declare the legacy token in `HiddenAliases`; it remains accepted by CLI and REPL parsing while the canonical token stays visible:

```csharp
app.Map(
"deploy",
([ReplOption(Name = "tenant", HiddenAliases = ["--account", "-a"])] string? tenant = null) => tenant);
```

`HiddenAliases` registers those tokens itself; they do not also need to appear in `Aliases`. If the same exact token appears in both arrays, hidden visibility wins. Tokens remain case-sensitive when the option is case-sensitive, so `--account` can be hidden without hiding a distinct `--ACCOUNT` alias.

The fluent form operates on an alias already declared in `Aliases`:

```csharp
app.Map(
"deploy",
([ReplOption(Name = "tenant", Aliases = ["--account"])] string? tenant = null) => tenant)
.WithOption("tenant", option => option.HiddenAlias("--account"));
```

The same contract applies to global options:

```csharp
app.Options(options =>
{
options.Parsing.AddGlobalOption<string>("tenant", aliases: ["--account"]);
options.Parsing.GlobalOption("tenant").HiddenAlias("--account");
});

public sealed class GlobalOptions
{
[ReplOption(HiddenAliases = ["--account"])]
public string? Tenant { get; set; }
}
```

A hidden alias is omitted from command/root help, typo suggestions, interactive and shell completion (including value completion after manually typing it), aggregate and exact-path documentation, and MCP schemas. MCP callers use the visible canonical argument name; the hidden alias is only a backwards-compatible CLI/REPL fallback. Like every hidden surface, this is discoverability metadata rather than authorization.

Hidden options are omitted from help, interactive and shell completion (including value providers), documentation export, and generated MCP schemas. They remain valid parser inputs on the command line and in the REPL, and bind normally when supplied explicitly.

Over MCP the omission is stricter. The advertised tool schema and the list of accepted arguments are built from the same option list, so a `tools/call` that supplies a hidden option is **rejected** — exactly like a call to a hidden command. A hidden option is therefore reachable from a human-driven CLI or REPL session, but not from an agent.

A hidden option must be omittable for the provider that builds a discovery surface, because that client otherwise has no valid invocation path. Repl checks the same fallbacks and precedence as the handler binder: a CLR default or nullable shape, synthetic `IProgress<double>`/`IProgress<ReplProgressEvent>` from an available `IReplInteractionChannel`, then direct `IServiceProvider.GetService` before explicit `ExactlyOne`/`OneOrMore` lower bounds are enforced. Direct handler parameters are validated when aggregate documentation or MCP discovery is built, not by `Map` or `.Hidden()`, because `Run` and MCP may receive an external provider only after mapping. If the active provider cannot supply the value, discovery fails with the required-hidden diagnostic rather than advertising an impossible command. Required options-group properties still fail immediately during mapping because that binding path never consults DI.

A fluent `.Hidden(isHidden: false)` overrides `ReplOptionAttribute.Hidden`. For direct handler parameters this can restore visibility before provider-aware discovery. A required hidden options-group property is rejected while the command is mapped, before a fluent override can run, so fix that attribute instead.

> **Hiding is not access control.** A hidden option stays a fully invocable part of the command line for anyone who knows its name — nothing about it is authenticated, authorized, or secret. Use it for deprecated switches, diagnostic escape hatches and migration aliases. Gate privileged behavior with real authorization, never with obscurity.

### Hiding an option from agents only

`.AutomationHidden()` is the option-level counterpart of `CommandBuilder.AutomationHidden()`: it withholds the option from programmatic surfaces while leaving it fully visible to people.

```csharp
app.Map("deploy", Deploy)
.WithOption("traceId", option => option.AutomationHidden());
```

The declarative form is `[ReplOption(AutomationHidden = true)]`. It is **not** supported on typed global-options properties and fails fast there, because global options never reach a programmatic surface at all — the flag would have nothing to act on. `GlobalOptionBuilder` has no `AutomationHidden` for the same reason, enforced by its type rather than by a runtime check.

The two axes are independent:

| Surface | `.Hidden()` | `.AutomationHidden()` |
|---|---|---|
| Command help | omitted | **listed** |
| Interactive and shell completion | omitted | **offered** |
| Aggregate `doc export` | omitted | **exported, flagged** |
| `doc export <command>` (exact path) | **exported, flagged** | **exported, flagged** |
| MCP tool schema and prompt arguments | omitted | omitted |
| MCP `tools/call` | rejected | rejected |
| CLI and REPL parsing and binding | unaffected | unaffected |

Both are discovery filters, and neither is an access-control boundary.

### Troubleshooting an option that will not bind

Almost always a target-name mistake. `WithOption` takes the **CLR** handler parameter or options-group property name, not the rendered `--option-name` token — `WithOption("internalMode", …)`, never `WithOption("internal-mode", …)`. An unknown target throws at configuration time and the message lists the targets that do exist, so read it rather than guessing.
Comment thread
carldebilly marked this conversation as resolved.

If the option is genuinely hidden and you want to confirm what the app thinks, export the command explicitly: `doc export <command path> --json` includes hidden options with `"isHidden": true`. The aggregate export omits them, so target the command.

Note there is no built-in signal for *use* of a hidden option. If the point is retiring a deprecated switch, record that in the handler yourself — otherwise nothing will tell you when it has become safe to remove.

### Accessing global options outside handlers

Parsed global option values are available via `IGlobalOptionsAccessor`, registered in DI automatically. This enables access from middleware, DI service factories, and handlers:
Expand Down
1 change: 1 addition & 0 deletions docs/for-coding-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Use these annotations to help agents make safer decisions:
| `.OpenWorld()` | Talks to external systems; expect latency and failures. |
| `.LongRunning()` | May take time; use call-now / poll-later patterns. |
| `.AutomationHidden()` | Do not expose this command to MCP automation. |
| `.WithOption(name, o => o.AutomationHidden())` | Keep this one option out of the tool schema; the command stays visible. |

Unannotated tools force agents to assume the worst. Annotate every command that will be visible through MCP.

Expand Down
1 change: 1 addition & 0 deletions docs/mcp-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Commands map to MCP primitives automatically:
| `.AsPrompt()` | Prompt | Reusable instruction template |
| `.AsMcpAppResource()` | Tool + `ui://` HTML resource | Interactive UI for capable hosts |
| `.AutomationHidden()` | _(nothing)_ | Excluded from MCP entirely |
| `.WithOption(name, o => o.AutomationHidden())` | Tool without that option | Option people may use but agents should not |

## Annotations

Expand Down
2 changes: 2 additions & 0 deletions docs/mcp-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ Notes:
|---|---|---|
| `.AutomationHidden()` | Per-command | Interactive-only commands |
| `.Hidden()` | Per-command | Hidden from all surfaces |
| `.WithOption(name, o => o.AutomationHidden())` | Per-option | Option people may use but agents should not |
| `.WithOption(name, o => o.Hidden())` | Per-option | Deprecated or diagnostic switches |
| `CommandFilter` | App-level | `o.CommandFilter = c => !c.Path.StartsWith("admin")` |
| Module presence + `Programmatic` | Per-module | Entire feature areas |

Expand Down
6 changes: 6 additions & 0 deletions docs/parameter-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Application-facing parameter DSL:
- explicit `Aliases` (full tokens, for example `-m`, `--mode`)
- explicit `ReverseAliases` (for example `--no-verbose`)
- `Mode` (`OptionOnly`, `ArgumentOnly`, `OptionAndPositional`)
- `Hidden` to suppress discovery without changing parsing or binding
- optional per-parameter `CaseSensitivity`
- optional `Arity`
- `ReplArgumentAttribute`
Expand All @@ -41,6 +42,8 @@ Supporting enums:
- `ReplParameterMode`
- `ReplArity`

Option visibility can also be configured fluently with `CommandBuilder.WithOption(targetName, option => option.Hidden())` and `ParsingOptions.GlobalOption(name).Hidden()`. Fluent visibility overrides the attribute value, including `.Hidden(isHidden: false)`. Unknown targets fail during configuration instead of being ignored. Hidden options must be omittable, but inferred `ExactlyOne` alone does not make an omittable nullable/defaulted CLR parameter invalid. Required options-group properties fail immediately during mapping because their binder never consults DI. Required direct handler parameters are validated later, when aggregate documentation or MCP discovery knows the active provider; discovery accepts them when the binder can synthesize progress from `IReplInteractionChannel` or resolve the parameter from DI, and otherwise emits the required-hidden diagnostic.

### Options groups

- `ReplOptionsGroupAttribute` (on a class) marks it as a reusable parameter group
Expand Down Expand Up @@ -107,6 +110,9 @@ This same schema drives:
- command help option sections
- shell option completion candidates
- exported documentation option metadata
- generated MCP tool and prompt schemas

Hidden options are filtered from those discovery surfaces, while the same schema continues to accept and bind their tokens during direct execution.

## System.CommandLine comparison

Expand Down
10 changes: 8 additions & 2 deletions src/Repl.Core/Autocomplete/AutocompleteEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ private ConsoleLineReader.AutocompleteSuggestion[] AppendInvalidAlternativeIfNee
var comparer = StringComparer.FromComparison(comparison);
var tokens = new List<string>();
var dedupe = new HashSet<string>(comparer);
OptionTokenCompletionSource.CollectGlobalOptionTokens(
var customGlobalOwnership = OptionTokenCompletionSource.CollectGlobalOptionTokens(
app.OptionsSnapshot, currentTokenPrefix, comparison, dedupe, tokens);

// Source route options from the single route this prefix resolves to (already
Expand All @@ -636,7 +636,8 @@ private ConsoleLineReader.AutocompleteSuggestion[] AppendInvalidAlternativeIfNee
&& commandPrefix.Length == match.Route.Template.Segments.Count)
{
OptionTokenCompletionSource.CollectRouteOptionTokens(
match.Route,
match.Route.OptionSchema,
customGlobalOwnership,
currentTokenPrefix,
app.OptionsSnapshot.Parsing.OptionCaseSensitivity,
dedupe,
Expand Down Expand Up @@ -1592,6 +1593,11 @@ internal static bool IsControlFreeValue(string value) =>
pendingOptionToken, app.OptionsSnapshot.Parsing.OptionCaseSensitivity);
foreach (var entry in entries)
{
if (entry.IsHidden || match.Route.OptionSchema.IsOptionHidden(entry.ParameterName))
{
continue;
}

// Same keystroke rule as the positional path: providers only run for an explicit
// completion request; live-hint refreshes fall through to the static enum fallback.
if (providersAllowed
Expand Down
Loading
Loading