cli-engine 0.1.2

Rust CLI framework for consistent command modules
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# cli-engine Rust Design

`cli_engine` is a Rust library for building consistent, domain-oriented command-line applications.
It is not a binary and does not assume one product's command set. Consumer CLIs provide their own
`main`, register domain modules, and let the framework handle shared CLI concerns.

The design priorities are:

- Make new commands easy to add by copying a nearby command and filling in command-specific details.
- Keep domain behavior close to the command that owns it.
- Centralize cross-cutting behavior: authentication, authorization, audit, activity, output rendering,
  schemas, guides, search, command trees, and transport helpers.
- Preserve stable user-facing contracts such as command names, flag names, output envelopes, auth
  provider JSON shapes, and colon-separated command paths.
- Follow normal Rust library and CLI practices: `clap` for argument parsing, `tokio` for async work,
  `serde` for data, `schemars` for JSON Schema, `thiserror` for framework errors, and `reqwest`
  for HTTP transport.

## Crate Shape

The repository root is the Rust crate:

```text
Cargo.toml
AGENTS.md
CLAUDE.md
docs/
  auth.md
  concepts.md
  design.md
examples/
  basic.rs
  typed.rs
src/
  lib.rs
  cli.rs
  command.rs
  module.rs
  middleware.rs
  flags.rs
  guide.rs
  search.rs
  tree.rs
  tier.rs
  error.rs
  auth/
    mod.rs
    exec.rs
    pkce.rs
    ...
  output/
  transport/
tests/
  foundation.rs
  derive_bridge.rs
```

The root module re-exports the common authoring surface so consumer modules can usually import from
`cli_engine::{...}` without knowing the internal file layout.

## Consumer Application Model

A consumer CLI should keep its binary entrypoint small:

```rust
use std::process::ExitCode;

use cli_engine::{BuildInfo, Cli, CliConfig};

mod modules;

#[tokio::main]
async fn main() -> ExitCode {
    let cli = Cli::new(
        CliConfig::new("my-cli", "Team CLI", "my-cli")
            .with_build(BuildInfo::new(env!("CARGO_PKG_VERSION")))
            .with_default_auth_provider("primary")
            .with_modules(modules::all()),
    );

    cli.execute().await
}
```

Application code should be organized by domain or team ownership:

```text
src/
  main.rs
  modules/
    mod.rs
    project.rs
    certificate.rs
```

Each module owns its command group, leaf commands, response types, output schemas, human views, and
module-local guides.

## CLI Assembly

`CliConfig` is the declarative root configuration. It contains:

- Root command name, short help, and optional long help.
- Build/version metadata.
- Application id.
- Default auth provider.
- Domain modules.
- Top-level commands.
- Guides and human output views.
- Auth providers.
- Lifecycle hooks for dependency initialization, custom global flags, pre-run behavior, metadata
  resolution, shutdown, and extra search documents.

`Cli::new(config)` builds the `clap::Command` tree, registers framework global flags, mounts domain
modules, registers built-in commands, seeds schema and human-view registries, and prepares
middleware.

`Cli::execute()` is the normal binary entrypoint helper. Tests and generated integration harnesses
should prefer `Cli::run(args)` or `Cli::execute_from(args, stdout, stderr)` so stdout, stderr, and
exit status are asserted separately.

## Modules

Modules are domain-bounded collections of CLI functionality. Small modules can use a closure:

```rust
use cli_engine::{GroupSpec, Module, RuntimeGroupSpec};

pub fn module() -> Module {
    Module::new("Platform Systems", |_context| {
        RuntimeGroupSpec::new(GroupSpec::new("project", "Manage projects"))
    })
}
```

Larger modules can implement `CommandModule` when named dependency ownership is clearer than a
closure. Both forms register through `ModuleContext`, which exposes middleware, schema registration,
human-view registration, and guide registration without exposing parser internals.

## Commands And Groups

Groups are noun-based containers. Commands are leaf actions. The framework derives colon-separated
paths from the command tree:

```text
my-cli project list  ->  project:list
```

Those colon paths are stable identifiers for policy, authorization, audit, activity, schemas,
search, and tree output.

Command definitions use `CommandSpec`; executable commands use `RuntimeCommandSpec`:

```rust
use clap::Arg;
use cli_engine::{CommandResult, CommandSpec, RuntimeCommandSpec};
use serde_json::json;

fn list_projects() -> RuntimeCommandSpec {
    RuntimeCommandSpec::new(
        CommandSpec::new("list", "List projects")
            .with_system("projects-api")
            .with_default_fields("id,name,status")
            .with_arg(Arg::new("team").long("team").required(true)),
        async |_credential, args| {
            let team = args
                .get("team")
                .and_then(|value| value.as_str())
                .unwrap_or_default();

            Ok(CommandResult::new(json!([{ "id": "project-1", "team": team }])))
        },
    )
}
```

Use `RuntimeCommandSpec::new_with_context` only when a handler needs the colon command path,
user-supplied args, or a middleware snapshot.

Use `RuntimeCommandSpec::new_streaming` for commands that emit a sequence of events rather than a single result. The handler receives a `StreamSender` and writes individual `serde_json::Value` events. Each event is written to stdout as a newline-delimited JSON line as it arrives. The handler and the NDJSON writer run concurrently so the handler can keep sending while the writer flushes to stdout. If stdout is under backpressure the bounded channel can fill and the handler will wait on `send` until the writer catches up.

### Typed Arguments

When commands have many flags or already use `#[derive(clap::Args)]` structs, the typed path avoids
manual `Arg` construction and `ValueMap` extraction:

```rust
use cli_engine::{CommandResult, CommandSpec, Credential, RuntimeCommandSpec};
use serde_json::json;

#[derive(Debug, Clone, clap::Args)]
struct ListArgs {
    #[arg(long)]
    team: String,

    #[arg(long, default_value = "10")]
    limit: u32,
}

fn list_projects() -> RuntimeCommandSpec {
    RuntimeCommandSpec::new_typed::<ListArgs, _, _, _>(
        CommandSpec::from_args::<ListArgs>("list", "List projects")
            .with_system("projects-api")
            .with_default_fields("id,name,status"),
        async |_credential: Option<Credential>, args: ListArgs| {
            Ok(CommandResult::new(json!([
                {"id": "p1", "name": "Portal", "team": args.team, "limit": args.limit}
            ])))
        },
    )
}
```

`CommandSpec::from_args::<T>()` calls `T::augment_args` to extract argument definitions.
`RuntimeCommandSpec::new_typed` deserializes parsed matches into the typed struct. Handlers that use
`RuntimeCommandSpec::new` or `new_with_context` can also call `context.typed_args::<T>()` for
on-demand deserialization.

The builder and derive paths are equivalent at runtime and can be mixed within a module.

Command metadata should be explicit:

- `with_system` sets backend attribution for output and errors.
- `with_default_fields` sets default field projection for list-like output.
- `with_auth_provider` and `with_auth_metadata` select provider behavior.
- `with_tier` and `mutates` mark risk and dry-run behavior.
- `with_json_schema::<T>()` publishes JSON Schema for output.
- `with_arg` adds typed `clap::Arg` values.

## Global Flags

Framework global flags populate middleware and apply consistently to every command:

| Flag | Purpose |
| --- | --- |
| `--output`, `-o` | Output format: `json`, `human`, or `toon`. |
| `--json` | Shorthand for `--output json`. |
| `--toon` | Shorthand for `--output toon`. |
| `--human` | Shorthand for `--output human`. |
| `--verbose` | Includes metadata; no value means all metadata. |
| `--dry-run` | Short-circuits mutating/destructive commands. |
| `--fields` | Selects comma-separated output fields. |
| `--filter` | Runs a JMESPath predicate against each list item. |
| `--expr` | Runs a JMESPath query against the whole result. |
| `--limit` | Client-side page size for list output. |
| `--offset` | Client-side starting offset for list output. |
| `--schema` | Renders command schema instead of running business logic. |
| `--reason` | Reason passed to authorization, audit, and activity. |
| `--timeout` | Command deadline (e.g. `60s`, `5m`); default is no timeout (`0s`). |
| `--debug` | Debug selector for integrations that use it. |
| `--search` | Searches command and guide documentation before command execution. |
| `--version`, `-v` | Prints version/build metadata. |

Applications can add their own global flags with `CliConfig::with_register_flags` and copy parsed
values into middleware with `CliConfig::with_apply_flags`.

## Middleware

Middleware owns the execution pipeline:

1. Resolve command metadata.
2. Resolve credentials unless the command is no-auth.
3. Run authorization if configured.
4. Short-circuit `--schema` or mutating `--dry-run` when applicable.
5. Run command business logic.
6. Audit and emit activity.
7. Apply the output pipeline.
8. Render success or error output.

Command handlers should not print directly. They return data or an error; middleware builds the
output envelope and renderer output. This keeps stdout machine-friendly and stderr reserved for
diagnostics in executable paths.

## Auth And Authorization

Auth providers implement `AuthProvider` and are registered with the CLI or during dependency
initialization. The dispatcher routes credential operations by provider name and supports the
built-in `auth login`, `auth status`, and `auth logout` commands.

`PkceAuthProvider` (behind the `pkce-auth` feature) is a built-in provider that implements the full browser-based OAuth 2.0 PKCE flow. It stores tokens in the system keychain and refreshes them automatically. Consumer CLIs that need a first-party browser login flow can use it directly without writing a provider binary.

Credential fields are serialized as provider-contract JSON and are used by transport injectors,
authorization, audit, and activity.

The provider process contract and transport injectors are described in
[Authentication and Transport](auth.md).

Authorization is optional and supplied by an `Authorizer` attached to middleware. The authorizer
receives command path, effective args, optional credential, reason, and tier.

Auditors and activity emitters are also pluggable traits. They receive enough context to record
success, auth failures, authorization denials, dry-runs, command errors, and command duration.

## Output

Handlers return JSON-serializable data and a system id. Middleware wraps the result in an envelope:

- `data`
- `metadata`
- `error`
- `warnings`
- `next_actions`

Metadata is omitted unless `--verbose` is requested. Selective metadata is supported with
comma-separated verbose fields.

The output pipeline runs in this order:

1. `--filter`
2. `--limit` and `--offset`
3. `--expr`
4. `--fields`
5. `--output`

JSON is the default and preferred machine-readable format. Human output is designed for terminal
reading. TOON remains an optional output format.

Human views are keyed by schema id or command path:

```rust
use cli_engine::{HumanViewDef, TableColumn};

let view = HumanViewDef::new(
    "project:list",
    vec![
        TableColumn::new("id", "ID"),
        TableColumn::new("name", "Name"),
        TableColumn::new("status", "Status"),
    ],
);
```

Custom human renderers can be registered when column output is not expressive enough.

## Schemas

Schemas exist for help output and agent comprehension. The preferred schema path is JSON Schema
from Rust types:

```rust
use schemars::JsonSchema;
use serde::Serialize;

#[derive(Debug, Serialize, JsonSchema)]
struct Project {
    id: String,
    name: String,
    status: String,
}
```

Attach schemas with `CommandSpec::with_json_schema::<Project>()`. The framework also derives a
compact field summary for help text. Manual `OutputSchema` and `OutputField` definitions remain
available for simple or dynamic cases.

## Guides And Search

Guides are markdown documents registered globally or by module. They can come from filesystem paths,
embedded `(path, bytes)` pairs, or explicit `GuideEntry` values.

`--search` indexes command metadata, aliases, guide content, and extra registered search documents.
Search bypasses normal command execution so users and agents can discover commands without
satisfying required command flags.

## Transport

`transport::HttpClient` wraps `reqwest` for command implementations. It provides:

- Auth injection.
- Default headers and user-agent configuration.
- JSON request/response helpers.
- Raw response helpers.
- ETag and `If-Match` helpers.
- Multipart helpers.
- GraphQL helpers.
- Retry behavior.
- Structured transport errors that preserve code, system, and request id in output envelopes.

Auth injectors cover bearer tokens, provider-backed bearer tokens, cookies, basic auth, API keys,
OAuth2 client credentials, and no-op requests.

## Error Model

Framework code returns `cli_engine::Result<T>`. `CliCoreError` is the shared error enum for framework
failures, output failures, transport failures, and wrapped domain errors.

Use:

- `CliCoreError::message` for simple framework messages.
- `CliCoreError::message_for_system` for direct system-attributed messages.
- `CliCoreError::with_system` to wrap a source error with backend attribution.
- `CliCoreError::with_detailed_error` when a source error has structured code/system/request id.
- `CliCoreError::with_exit_code` when a specific process exit code must survive error wrapping.

## Testing Design

The Rust crate uses integration tests in `tests/foundation.rs` to exercise the public
framework surface:

- CLI construction and built-ins.
- Command and group dispatch.
- Middleware sequencing.
- Auth provider routing.
- Output envelopes and renderers.
- Output pipeline behavior.
- Schemas and human views.
- Guides, search, and tree rendering.
- Transport clients and auth injectors.

Consumer CLIs should add their own integration tests around generated command trees. Prefer tests
that assert exit code, stdout, stderr, rendered JSON shape, and important command side effects.

Before handoff, run:

```sh
cargo fmt --all --check
cargo clippy --all-targets -- -D warnings
RUSTDOCFLAGS='-D warnings' cargo doc --no-deps
cargo test --all-targets
cargo test --doc
```

## Non-Goals

- This crate does not define product-specific commands.
- This crate does not own consumer binary entrypoints.
- This crate does not prescribe one guide-embedding crate.
- This crate does not require exact human table bytes across all implementations; the contract is
  readable, stable terminal output.