Library usage: README.md. Doc guides: docs/.
- Rust stable (edition 2021; MSRV typically 1.70+)
rustfmt+clippy(rustup component add rustfmt clippy)
git clone https://github.com/aroff/cli-framework.git
cd cli-framework
cargo buildA pre-commit hook runs cargo fmt --check and cargo clippy --all-features -- -D warnings before every commit, matching CI exactly.
cp scripts/hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit| Path | Role |
|---|---|
src/ |
Library |
skill/ |
Bundled Agent Skill (SKILL.md, skill-project.toml, references/) |
skill/examples/ |
Runnable samples — cargo run --example <name> |
tests/ |
[[test]] targets in Cargo.toml |
docs/ |
adr/ (architecture decision records), telemetry.md, testing.md |
specs/ |
In-flight design notes |
tools-cli-framework/ |
Superseded — see skill/ |
| Module(s) | Role |
|---|---|
app |
AppBuilder, App::run, dispatch |
command, command::chat |
Command, registry, categorized root-help metadata and ordering, chat |
command_surface, command_surface::tool_bridge |
Command→tool schemas + shared tool invocation bridge (chat / MCP) |
parser, spec |
argv → args; CommandPath, CommandSpec |
plugin |
registry TOML / manifests |
security |
output sanitize, command risk policy, handle-validated private-file creation/open/atomic replacement |
http_retry, retry |
HTTP retry, secure_reqwest_client |
cli_output, cli_mode, message, environment |
help, environment-variable registry, tables, JSON, modes |
api (feature api-server) |
Built-in Axum host for versioned APIs (/api/{version}/...) plus /healthz + /readyz; build() may apply a root fallback_service (via root_fallback()) as its final composition step. ApiServer::serve(addr) binds and serves; serve_with_listener(listener) is the same path for a listener the caller already bound (an ephemeral :0 bind whose address must be published before serving) |
mcp (feature mcp-server) |
rmcp ServerHandler (tools + resources). Generic, concept-free: per-command with_meta (opaque serde_json::Value) → tools/list top-level _meta; with_visibility (cli-framework acts on it) → _meta.visibility; mcp::resources::ResourceRegistry serves resources via resources/list / resources/read with opaque per-resource with_meta at contents[]._meta. All UI/MCP-Apps semantics live in the consumer (ADR 0066). Optional per-request identity seam: McpRequestAuthenticator turns HTTP request headers into an opaque Arc<dyn Any + Send + Sync>, and McpDynamicToolProvider (McpToolRegistry::with_dynamic_tools, AppBuilder::with_mcp_dynamic_tools) maps that identity to extra Vec<McpDynamicTool> consumed by both tools/list and tools/call so the advertised and callable sets cannot drift — a discovery surface only, static registrations always win, nothing cached across requests (ADR 0081). An McpDynamicTool is { name, command, presentation: Option<McpToolPresentation> }; (String, Command) converts in with .into(). A presentation supplies an owned description + inputSchema for tools whose shape is runtime data rather than a &'static str CommandSpec; it is read by tools/list only (replacing, never merging with, build_input_schema) and never by dispatch, validation or risk classification. mcp serve binds --host/--port itself unless the app handed it a bound listener with AppBuilder::with_mcp_http_listener (then transport_http::start_streamable_http_with_listener serves it, never releasing the port) |
telemetry (feature telemetry) |
OpenTelemetry via the tracing→OTLP bridge (ADR 0068, spec 017/019/020). TelemetryConfig, init_simple/init_batch/init_batch_without_subscriber, TelemetryGuard, Telemetry handle + always-compiled NoopTelemetry. Auto cli.command spans (CLI dispatch, version, and dispatch_tool_call/MCP seams) + cli.command.invocations/cli.command.duration_ms metrics. cli_framework::telemetry::propagation extracts inbound W3C context in ApiServerBuilder and exposes TracedRequestBuilder for outbound injection. Spec 025 adds the telemetry level/attribution/probe axes (axes, probe, probes), deterministic policy resolution with kill switches and the end-user clamp (policy), the framework-owned settings store (<config_dir>/<app>/telemetry.<json|toml>, ADR 0077 — store), the generated telemetry manifest section and <APP>_TELEMETRY_* env layer (manifest, env), the redacting export boundary (ADR 0079 — redact, exporter, resource, init::init_from_policy, init::sampler_for_policy), subscriber composition through a write-once attach slot with a foreign-subscriber warning (ADR 0078 — subscriber::install_default_logging, LoggingGuard), the panic hook (panic), the first-run notice (notice::notice_decision, startup), six doctor checks (doctor::telemetry_checks: telemetry.subscriber, telemetry.store, telemetry.endpoint, telemetry.policy, telemetry.identity, telemetry.env), the bounded end-user flush budget (guard::flush_within) and the end-user Built-in telemetry command group (status/info/set/enable/disable/reset — commands), registered only for an EndUser deployment. App authors configure it through AppBuilder: with_deployment, with_telemetry_defaults (author::TelemetryDefaults), with_telemetry_ops, with_telemetry_identity, with_telemetry_attrs, with_telemetry_never; with_telemetry(TelemetryConfig) and TelemetryConfig::from_env are deprecated in v0.6.0 and removed in v0.8.0. End-user and operator documentation lives in docs/telemetry.md. |
self_install (feature self-install) |
ADR 0080. InstallEnv snapshots the process (OS, home, XDG/known-folder roots, PATH, env vars, TTY) so every operation is testable without touching the real machine. options (SelfInstallOptions), layout (bin dir, receipt path, asset names), receipt (InstallReceipt, versioned JSON), method (script / unmanaged / package-manager inference), place (same-directory temp + rename, Windows .exe.old swap, startup_cleanup), shell_path (shared env/env.fish, rc lines in existing files only), windows_path (HKCU\Environment Path + WM_SETTINGCHANGE), ops (install/uninstall/status, --system, --from), release (GitHub and HTTP sources, channels, downloads with the https/loopback rule), archive (SHA256SUMS, minisign over the sums, safe tar.gz/zip extraction, --version probe), update (self update/self rollback, bin-dir lock and work dir, .prev), policy (self_update.* enforced keys), notice (post-command update notice, daily cache; App::run_with_args runs it after the command so the telemetry level is known), apps_features (Windows HKCU Uninstall entry), commands (the self group, registered by AppBuilder::with_self_install for EndUser only) and doctor (install.* checks). Installer script templates live in skill/references/install-templates/; scripts/ci/self-install-e2e.{sh,ps1} test them against a loopback release, then update to a second demo build (CFW_DEMO_VERSION) and roll back. |
Also: auth (feature auth) — TokenProvider trait, AccessToken, AuthError, AuthenticatedHttpClient, four auto-registered auth commands; companion crate cli-framework-oidc provides OidcClient (feature client) and oidc_validation_layer / oidc_validation_layer_multi (feature server, one or several trusted issuers). Its client can route discovery and token requests through validated deployment-owned endpoints while preserving the public issuer as token identity; data_source; observability, testkit behind features; config (feature config) — ConfigBackend/ConfigStore/ConfigHandle (spec 016); FileBackend always, RegistryBackend on Windows only; config::manifest/Policy/resolution (feature config, spec 021) — the plain-JSON config manifest (+ #[derive(ConfigManifest)]), the org Policy document, and the enforced-veto resolver, all networking-free; config::managed (feature config-managed, implies config+auth) — PolicyClient/RoamingConfigClient over AuthenticatedHttpClient; four auto-registered config commands (show/manifest/profile/refresh, feature config-managed) — spec 021's "Command surface", wired via AppBuilder::with_config_manifest/with_policy_client and AppContext::opt_config_manifest/opt_policy_client; config::service (feature config-service, implies config+api-server, spec 022) — the server side: config_service_router (self-authenticating via a crate-local CallerIdentity trait, never ApiServerBuilder::auth()), PolicyStore/UserConfigStore traits with FsPolicyStore/InMemoryUserConfigStore (tests/dev) and PgPolicyStore/PgUserConfigStore (sqlx-core+sqlx-postgres directly, never the sqlx facade) implementations, plus a hand-rolled SQL migration runner.
Flow: AppBuilder registers commands, groups, help ordering, and built-ins into one registry → run resolves id + CommandArgs → await execute on AppContext. Help, completion, command-spec export, chat, and MCP read the same frozen registry. Tool surfaces (chat / MCP) adapt inputs into command_surface::tool_bridge for shared parsing/validation/gating/dispatch.
Externals (summary): Cargo.toml — e.g. tokio, reqwest, clap, serde, aikit-agent; optional comfy-table, indicatif.
Security (summary): sanitize untrusted terminal output; command risk tier policy; plugin paths rooted (no traversal).
The OIDC client's loopback callback lives in cli-framework-oidc/src/client/callback.rs:
bounded asynchronous reception owns its sockets for cancellation, validates the
request path/state/code, and constructs unambiguous PKCE authorization queries.
Browser launch is optional and never blocks callback reception or runtime shutdown.
Features: default ["clap-dispatch", "chat"]; see [features] in Cargo.toml. The clap-dispatch flag is now a no-op default — all dispatch goes through the Clap path. User-visible behavior → update README in the same PR.
cargo testTests run in parallel within a single binary and share the process environment.
Any test that mutates env vars (std::env::set_var / remove_var) MUST use a
unique, test-scoped name (e.g. CFW_TEST_<TEST>_<PURPOSE>) — generic names like
TEST_VAR cause flaky races between tests that touch the same key.
./scripts/run-ci-tests.shRequires cargo install cargo-audit. Matches fmt, clippy -D warnings, audit, release build, tests.
CI runs cargo audit. If an upstream dependency has no compatible patched
release available for a RustSec advisory, this repo may temporarily ignore the
advisory via .cargo/audit.toml and document why. Any ignores must be treated
as temporary and removed once upstream is updated.
cargo fmt --all- Clippy clean (CI uses
RUSTFLAGS="-D warnings") - Conventional Commits
Use these inline tags in README.md, docs/, and specs/ when a documented
feature does not match the shipped behavior. Tags MUST appear as the first
token of a blockquote so they stand out:
| Tag | Meaning |
|---|---|
[PLANNED] |
On the roadmap, not yet implemented. Surrounding prose describes the intended shape; the code does not yet do this. |
[PARTIAL] |
Implemented in part. Quote which part works and which does not. |
[DEPRECATED] |
Still works but slated for removal. Link to the replacement. |
Every [PLANNED] or [PARTIAL] tag SHOULD link to an ADR or spec that
records why the gap exists.
- Describe change; link issues.
- API or behavior change → README and/or
docs/in the same PR when it affects integrators.