Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,29 @@ app.Context("client", client =>
return app.Run(args);
```

For stdio protocol handlers (MCP/LSP/JSON-RPC, DAP, CGI-style gateways), mark the route as protocol passthrough:

```csharp
app.Context("mcp", mcp =>
{
mcp.Map("start", async (IMcpServer server, CancellationToken ct) =>
{
var exitCode = await server.RunAsync(ct);
return Results.Exit(exitCode);
})
.AsProtocolPassthrough();
});
```

In passthrough mode, repl keeps `stdout` available for protocol messages and sends framework diagnostics to `stderr`.
If the handler requests `IReplIoContext`, write protocol payloads through `io.Output` (stdout in local CLI passthrough).
Requesting `IReplIoContext` is optional for local CLI handlers that already use `Console.*` directly, but recommended for explicit stream control, better testability, and hosted-session support.
Framework-rendered handler return payloads (if any) are also written to `stderr` in passthrough mode, so protocol handlers should usually return `Results.Exit(code)` after writing protocol output.
This is a strong fit for MCP-style stdio servers where the protocol stream must stay pristine.
Typical shape is `mytool mcp start` in passthrough mode, while `mytool start` stays a normal CLI command.
It also maps well to DAP/CGI-style stdio flows; socket-first variants (for example FastCGI) usually do not require passthrough.
Use this mode directly in local CLI/console runs. For hosted terminal sessions (`IReplHost` / remote transports), handlers should request `IReplIoContext`; console-bound toolings that use `Console.*` directly remain CLI-only.

One-shot CLI:

```text
Expand Down Expand Up @@ -213,6 +236,7 @@ ws-7c650a64 websocket [::1]:60288 301x31 xterm-256color 1m 34s 1s
- **Output pipeline** with transformers and aliases
(`--output:<format>`, `--json`, `--yaml`, `--markdown`, …)
- **Typed result model** (`Results.Ok/Error/Validation/NotFound/Cancelled`, etc.)
- **Protocol passthrough mode** for stdio transports (`AsProtocolPassthrough()`), keeping `stdout` reserved for protocol payloads
- **Typed interactions**: prompts, progress, status, timeouts, cancellation
- **Session model + metadata** (transport, terminal identity, window size, ANSI capabilities, etc.)
- **Hosting primitives** for running sessions over streams, sockets, or custom carriers
Expand Down Expand Up @@ -254,6 +278,7 @@ Package details:
## Getting started

- Architecture blueprint: [`docs/architecture.md`](docs/architecture.md)
- Command reference: [`docs/commands.md`](docs/commands.md)
- Terminal/session metadata: [`docs/terminal-metadata.md`](docs/terminal-metadata.md)
- Testing toolkit: [`docs/testing-toolkit.md`](docs/testing-toolkit.md)
- Conditional module presence: [`docs/module-presence.md`](docs/module-presence.md)
Expand Down
109 changes: 109 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Command Reference

This page documents **framework-level commands and flags** provided by Repl Toolkit.
Your application commands are defined by your own route mappings.

## Discover commands

- CLI help at current scope: `myapp --help`
- Help for a scoped path: `myapp contact --help`
- Interactive help: `help` or `?`

## Global flags

These flags are parsed before route execution:

- `--help`
- `--interactive`
- `--no-interactive`
- `--no-logo`
- `--output:<format>`
- output aliases mapped by `OutputOptions.Aliases` (defaults include `--json`, `--xml`, `--yaml`, `--yml`, `--markdown`)
- `--answer:<name>[=value]` for non-interactive prompt answers

## Ambient commands

These commands are handled by the runtime (not by your mapped routes):

- `help` or `?`
- `..` (interactive scope navigation; interactive mode only)
- `exit` (leave interactive session when enabled)
- `history [--limit <n>]` (interactive mode only)
- `complete <command path> --target <name> [--input <text>]`
- `autocomplete [show]`
- `autocomplete mode <off|auto|basic|rich>` (interactive mode only)

Notes:

- `history` and `autocomplete` return explicit errors outside interactive mode.
- `complete` requires a terminal route and a registered `WithCompletion(...)` provider for the selected target.

## Shell completion management commands

When shell completion is enabled, the `completion` context is available in CLI mode:

- `completion install [--shell bash|powershell|zsh|fish|nu] [--force] [--silent]`
- `completion uninstall [--shell bash|powershell|zsh|fish|nu] [--silent]`
- `completion status`
- `completion detect-shell`
- `completion __complete --shell <...> --line <input> --cursor <position>` (internal protocol bridge, hidden)

See full setup and profile snippets: [shell-completion.md](shell-completion.md)

## Optional documentation export command

If your app calls `UseDocumentationExport()`, it adds:

- `doc export [<path...>]`

By default this command is hidden from help/discovery unless you configure `HiddenByDefault = false`.

## Protocol passthrough commands

For stdio protocols (MCP/LSP/JSON-RPC), mark routes with `AsProtocolPassthrough()`.
This mode is especially well-suited for **MCP servers over stdio**, where the handler owns `stdin/stdout` end-to-end.

Example command surface:

```text
mytool mcp start # protocol mode over stdin/stdout
mytool start # normal CLI command
mytool status --json # normal structured output
```

In this model, only `mcp start` should be marked as protocol passthrough.

Common protocol families that fit this mode:

- MCP over stdio
- LSP / JSON-RPC over stdio
- DAP over stdio
- CGI-style process protocols (stdin/stdout contract)

Not typical passthrough targets:

- socket-first variants such as FastCGI (their protocol stream is on TCP, not app `stdout`)

Execution scope note:

- protocol passthrough works out of the box for **local CLI/console execution**
- hosted terminal sessions (`IReplHost` / remote transports) require handlers to request `IReplIoContext`; console-bound toolings that use `Console.*` directly remain CLI-only

Why `IReplIoContext` is optional:

- many protocol SDKs (for example some MCP/JSON-RPC stacks) read/write `Console.*` directly; these handlers can still work in local CLI passthrough without extra plumbing
- requesting `IReplIoContext` is the recommended low-level path when you want explicit stream control, easier testing, or hosted-session support
- in local CLI passthrough, `io.Output` is the protocol stream (`stdout`), while framework diagnostics remain on `stderr`

In protocol passthrough mode:

- global and command banners are suppressed
- repl/framework diagnostics are written to `stderr`
- framework-rendered handler return payloads (if any) are also written to `stderr`
- `stdout` remains reserved for protocol payloads
- interactive follow-up is skipped after command execution

Practical guidance:

- for protocol commands, prefer writing protocol bytes/messages directly to `io.Output` (or `Console.Out` when SDK-bound)
- return `Results.Exit(code)` to keep framework rendering silent
7 changes: 7 additions & 0 deletions docs/shell-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ completion __complete --shell <bash|powershell|zsh|fish|nu> --line <input> --cur

The shell passes current line + cursor, and Repl returns candidates on `stdout` (one per line).
`completion __complete` is mapped in the regular command graph through the shell-completion module (CLI channel only).
The bridge route is marked as protocol passthrough, so repl suppresses banners and routes framework diagnostics to `stderr`.
The bridge handler writes candidates through `IReplIoContext.Output`, which remains bound to the protocol stream (`stdout`) in local CLI passthrough.

`IReplIoContext` is optional in general protocol commands:

- optional for local CLI commands that already use `Console.*` directly
- recommended when handlers need explicit stream injection, deterministic tests, or hosted-session compatibility

The module exposes a real `completion` context scope:

Expand Down
4 changes: 2 additions & 2 deletions src/Repl.Core/CancelKeyHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
e.Cancel = true;
_commandCts.Cancel();
_lastCancelPress = now;
Console.Error.WriteLine();
Console.Error.WriteLine("Press Ctrl+C again to exit.");
ReplSessionIO.Error.WriteLine();
ReplSessionIO.Error.WriteLine("Press Ctrl+C again to exit.");
return;
}

Expand Down
46 changes: 46 additions & 0 deletions src/Repl.Core/CommandBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal CommandBuilder(string route, Delegate handler)
{
Route = route;
Handler = handler;
SupportsHostedProtocolPassthrough = ComputeSupportsHostedProtocolPassthrough(handler);
}

/// <summary>
Expand Down Expand Up @@ -49,6 +50,16 @@ internal CommandBuilder(string route, Delegate handler)
/// </summary>
public IReadOnlyDictionary<string, CompletionDelegate> Completions => _completions;

/// <summary>
/// Gets a value indicating whether this command reserves stdin/stdout for a protocol handler.
/// </summary>
public bool IsProtocolPassthrough { get; private set; }

/// <summary>
/// Gets a value indicating whether the handler can run protocol passthrough in hosted sessions.
/// </summary>
internal bool SupportsHostedProtocolPassthrough { get; }

/// <summary>
/// Gets the banner delegate rendered before command execution.
/// </summary>
Expand Down Expand Up @@ -145,4 +156,39 @@ public CommandBuilder Hidden(bool isHidden = true)
IsHidden = isHidden;
return this;
}

/// <summary>
/// Marks this command as protocol passthrough.
/// In this mode, repl diagnostics are routed to stderr and interactive stdin reads are skipped.
/// When handlers request <see cref="IReplIoContext"/>, <see cref="IReplIoContext.Output"/> remains the protocol stream
/// (stdout in local CLI passthrough), while framework output stays on stderr.
/// For hosted sessions, handlers should request <see cref="IReplIoContext"/> to access transport streams explicitly.
/// </summary>
/// <returns>The same builder instance.</returns>
public CommandBuilder AsProtocolPassthrough()
{
IsProtocolPassthrough = true;
return this;
}

private static bool ComputeSupportsHostedProtocolPassthrough(Delegate handler)
{
foreach (var parameter in handler.Method.GetParameters())
{
if (parameter.ParameterType != typeof(IReplIoContext))
{
continue;
}

// [FromContext] binds route/context values and is not stream injection.
if (parameter.GetCustomAttributes(typeof(FromContextAttribute), inherit: true).Length > 0)
{
continue;
}

return true;
}

return false;
}
}
1 change: 1 addition & 0 deletions src/Repl.Core/CoreReplApp.Documentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ private static bool IsFrameworkInjectedParameter(Type parameterType) =>
|| parameterType == typeof(CoreReplApp)
|| parameterType == typeof(IReplSessionState)
|| parameterType == typeof(IReplInteractionChannel)
|| parameterType == typeof(IReplIoContext)
|| parameterType == typeof(IReplKeyReader);

private static bool IsRequiredParameter(ParameterInfo parameter)
Expand Down
5 changes: 5 additions & 0 deletions src/Repl.Core/CoreReplApp.Routing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ private async ValueTask TryRenderCommandBannerAsync(
IServiceProvider serviceProvider,
CancellationToken cancellationToken)
{
if (command.IsProtocolPassthrough)
{
return;
}

if (command.Banner is { } banner && ShouldRenderBanner(outputFormat))
{
await InvokeBannerAsync(banner, serviceProvider, cancellationToken).ConfigureAwait(false);
Expand Down
Loading