Skip to main content

apcore_cli/
lib.rs

1//! apcore-cli — Command-line interface for apcore modules.
2//!
3//! Automatic MCP Server & OpenAI Tools Bridge for apcore — zero code changes required.
4//!
5//! Library root: re-exports the user-facing public API items.
6//! Protocol spec: FE-01 through FE-13 plus SEC-01 through SEC-04.
7//!
8//! See the apcore-cli docs repo for the authoritative feature spec and tech design.
9
10pub mod approval;
11pub mod builtin_group;
12pub mod cli;
13pub mod config;
14pub mod discovery;
15pub mod display_helpers;
16pub mod exposure;
17pub mod fs_discoverer;
18pub mod init_cmd;
19pub mod output;
20pub mod ref_resolver;
21pub mod schema_parser;
22pub mod security;
23pub mod shell;
24pub mod strategy;
25pub mod system_cmd;
26pub mod validate;
27
28// Internal sandbox runner — not part of the public API surface, but must be
29// pub so the binary entry point (main.rs) can invoke run_sandbox_subprocess().
30#[doc(hidden)]
31pub mod sandbox_runner;
32
33// Exit codes as defined in the API contract.
34pub const EXIT_SUCCESS: i32 = 0;
35pub const EXIT_MODULE_EXECUTE_ERROR: i32 = 1;
36pub const EXIT_INVALID_INPUT: i32 = 2;
37pub const EXIT_MODULE_NOT_FOUND: i32 = 44;
38pub const EXIT_SCHEMA_VALIDATION_ERROR: i32 = 45;
39pub const EXIT_APPROVAL_DENIED: i32 = 46;
40pub const EXIT_CONFIG_NOT_FOUND: i32 = 47;
41pub const EXIT_SCHEMA_CIRCULAR_REF: i32 = 48;
42pub const EXIT_ACL_DENIED: i32 = 77;
43// Config Bus errors (apcore >= 0.15.0)
44// All four namespace/env errors share exit code 78 per protocol spec —
45// the spec groups them into a single "config namespace error" category.
46pub const EXIT_CONFIG_NAMESPACE_RESERVED: i32 = 78;
47pub const EXIT_CONFIG_MOUNT_ERROR: i32 = 66;
48pub const EXIT_CONFIG_BIND_ERROR: i32 = 65;
49pub const EXIT_ERROR_FORMATTER_DUPLICATE: i32 = 70;
50pub const EXIT_SIGINT: i32 = 130;
51
52// ---------------------------------------------------------------------------
53// FE-13 apcli subcommand dispatcher (§4.9)
54// ---------------------------------------------------------------------------
55
56/// Subcommand names that are registered regardless of the resolved visibility
57/// mode's include/exclude filter. `exec` is the documented always-registered
58/// escape hatch (spec §4.9) so downstream callers can always invoke modules
59/// by ID even when the apcli group is configured with a minimal surface.
60pub const APCLI_ALWAYS_REGISTERED: &[&str] = &["exec"];
61
62/// Central dispatcher for the 13 canonical apcli subcommands. Walks a fixed
63/// registration table and honors [`ApcliGroup::resolve_visibility`] for
64/// include/exclude modes. Under `"all"` or `"none"` all 13 subcommands are
65/// registered (spec §4.9 registration rules table); under `"include"` only
66/// listed subcommands + [`APCLI_ALWAYS_REGISTERED`]; under `"exclude"` all
67/// except listed + [`APCLI_ALWAYS_REGISTERED`].
68///
69/// * `apcli_group` — the `apcli` clap [`Command`](clap::Command) to receive
70///   subcommands.
71/// * `cfg` — the resolved apcli visibility configuration.
72/// * `prog_name` — program name forwarded to `register_completion_command`.
73///
74/// Returns the updated command with registered subcommands attached.
75pub fn register_apcli_subcommands(
76    apcli_group: clap::Command,
77    cfg: &ApcliGroup,
78    prog_name: &str,
79) -> clap::Command {
80    type Registrar = Box<dyn FnOnce(clap::Command) -> clap::Command>;
81
82    let prog_name_for_completion = prog_name.to_string();
83    let table: Vec<(&'static str, Registrar)> = vec![
84        ("list", Box::new(discovery::register_list_command)),
85        ("describe", Box::new(discovery::register_describe_command)),
86        ("exec", Box::new(discovery::register_exec_command)),
87        ("validate", Box::new(validate::register_validate_command)),
88        ("init", Box::new(init_cmd::register_init_command)),
89        ("health", Box::new(system_cmd::register_health_command)),
90        ("usage", Box::new(system_cmd::register_usage_command)),
91        ("enable", Box::new(system_cmd::register_enable_command)),
92        ("disable", Box::new(system_cmd::register_disable_command)),
93        ("reload", Box::new(system_cmd::register_reload_command)),
94        ("config", Box::new(system_cmd::register_config_command)),
95        (
96            "completion",
97            Box::new(move |cli| shell::register_completion_command(cli, &prog_name_for_completion)),
98        ),
99        (
100            "describe-pipeline",
101            Box::new(strategy::register_pipeline_command),
102        ),
103    ];
104
105    let mode = cfg.resolve_visibility();
106    let mut cmd = apcli_group;
107    for (name, registrar) in table {
108        let should_register = match mode {
109            // mode:"none" still registers all subcommands — the group itself
110            // is hidden but subcommands remain individually reachable
111            // (spec §4.6 / §4.9 hidden-but-reachable).
112            "all" | "none" => true,
113            _ => APCLI_ALWAYS_REGISTERED.contains(&name) || cfg.is_subcommand_included(name),
114        };
115        if should_register {
116            cmd = registrar(cmd);
117        }
118    }
119    cmd
120}
121
122// ---------------------------------------------------------------------------
123// Crate-root re-exports (USER-FACING API only)
124// ---------------------------------------------------------------------------
125//
126// Per audit D9-005, the crate-root pub-use surface was trimmed from ~110 → ~40
127// items in v0.6.x. Internal command-builder helpers (register_*,
128// describe_pipeline_command, validate_command, generate_grouped_*_completion,
129// build_synopsis, generate_man_page) are now `pub(crate)` at the module level
130// — they are only consumed by lib.rs's per-subcommand registrar table and the
131// in-crate test suite. The dispatch_* fns in `system_cmd` and `strategy`
132// remain `pub` because the binary entry-point in main.rs calls them via the
133// full path (`apcore_cli::system_cmd::dispatch_health`, etc.) and main.rs is
134// a separate binary crate.
135//
136// **No `create_cli` / `run_with_config` factory in Rust** (cross-SDK parity
137// note from audit D1-005, 2026-04-26): Python exposes `apcore_cli.create_cli`
138// and TypeScript exposes `createCli` from `apcore-cli`; Rust intentionally
139// has no equivalent factory. The high-level `CliConfig` / `run_with_config`
140// embedding API was removed in v0.7.0 (D9-001/002) — it never had a working
141// dispatch loop. An embedding API will be reintroduced when actually
142// implemented; until then, downstream Rust users invoke the binary directly
143// or compose the per-subcommand registrars (e.g.,
144// `register_completion_command`) onto a `clap::Command` they own. The
145// Python `allowed_prefixes` parameter on `create_cli` is therefore a
146// Python-only safety knob with no Rust counterpart at this writing.
147
148// Approval gate (FE-04 + FE-11 §3.5)
149pub use approval::{
150    check_approval, ApprovalDeniedError, ApprovalError, ApprovalResult, ApprovalStatus,
151    ApprovalTimeoutError, CliApprovalHandler,
152};
153
154// Built-in command group (FE-13)
155pub use builtin_group::{ApcliConfig, ApcliGroup, ApcliMode};
156
157// Core dispatcher (FE-01)
158pub use cli::{
159    build_module_command, build_module_command_with_limit, collect_input,
160    collect_input_from_reader, dispatch_module, get_docs_url, is_verbose_help, set_audit_logger,
161    set_docs_url, set_executables, set_verbose_help, validate_module_id,
162};
163
164// FE-13 retires `cli::BUILTIN_COMMANDS`. Downstream consumers that pinned to
165// the old symbol continue to compile via the deprecated alias on `cli::` for
166// one MINOR cycle; the re-export at the crate root is dropped.
167pub use builtin_group::{APCLI_SUBCOMMAND_NAMES, RESERVED_GROUP_NAMES};
168
169// Config resolution (FE-07)
170pub use config::ConfigResolver;
171
172// Discovery + Registry providers (FE-03 / FE-09)
173pub use discovery::{
174    cmd_describe, cmd_list, cmd_list_enhanced, register_discovery_commands, ApCoreRegistryProvider,
175    DiscoveryError, ListOptions, RegistryProvider,
176};
177
178// Test utilities — available behind the `test-support` feature.
179// Gated behind cfg(test) for unit tests and the test-support feature for
180// integration tests. Excluded from production builds.
181#[cfg(any(test, feature = "test-support"))]
182#[doc(hidden)]
183pub use discovery::{mock_module, MockRegistry};
184
185// Display overlay helpers (FE-09)
186pub use display_helpers::{get_cli_display_fields, get_display};
187
188// Module exposure filtering (FE-12)
189pub use exposure::ExposureFilter;
190
191// Filesystem discoverer (FE-03)
192pub use fs_discoverer::FsDiscoverer;
193
194// Init command (FE-10)
195pub use init_cmd::{handle_init, init_command};
196
197// Output formatting (FE-08)
198pub use output::{format_exec_result, format_module_detail, format_module_list, resolve_format};
199
200// Schema $ref resolver (FE-02)
201pub use ref_resolver::resolve_refs;
202
203// JSON Schema → clap argument generator (FE-02)
204pub use schema_parser::{
205    extract_help_with_limit, reconvert_enum_values, schema_to_clap_args,
206    schema_to_clap_args_with_limit, BoolFlagPair, SchemaArgs, SchemaParserError, HELP_TEXT_MAX_LEN,
207    RESERVED_PROPERTY_NAMES,
208};
209
210// Security primitives (SEC-01..04)
211pub use security::{
212    AuditLogger, AuthProvider, AuthenticationError, ConfigDecryptionError, ConfigEncryptor,
213    ModuleExecutionError, ModuleNotFoundError, Sandbox, SchemaValidationError,
214};
215
216// Shell integration (FE-06): completion + man page builders.
217// build_program_man_page is the user-facing full-program man entry point;
218// per-command builders (cmd_completion, cmd_man, has_man_flag, completion_command)
219// are kept for downstream embedders that build their own root command tree.
220// Embedders compose them directly: each registers the corresponding subcommand
221// onto a clap Command. The previous register_shell_commands wrapper was a
222// 2-line passthrough with no production callers and was removed in v0.7.0
223// (D9-003) — embedders should call register_completion_command and
224// register_man_command directly.
225pub use shell::{
226    build_program_man_page, cmd_completion, cmd_man, completion_command, has_man_flag, ShellError,
227};
228
229// FE-11 system commands constant (used by downstream consumers to inspect
230// which command names are reserved by the system-management subset).
231pub use system_cmd::SYSTEM_COMMANDS;
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    /// Drift guard: the Registrar table built inside
238    /// `register_apcli_subcommands` must cover every name in
239    /// `APCLI_SUBCOMMAND_NAMES`. Adding a subcommand to one list without the
240    /// other produces a silent mismatch that was previously invisible across
241    /// three declaration sites.
242    #[test]
243    fn registrar_table_covers_all_apcli_subcommand_names() {
244        // Drive visibility to "all" so every table entry is registered.
245        let cfg = builtin_group::ApcliGroup::from_yaml(None, /*registry_injected*/ false);
246        let root = clap::Command::new("root").about("drift-guard root");
247        let built = register_apcli_subcommands(root, &cfg, "apcore-cli");
248        let registered: Vec<&str> = built.get_subcommands().map(|s| s.get_name()).collect();
249        for name in APCLI_SUBCOMMAND_NAMES {
250            assert!(
251                registered.contains(name),
252                "APCLI_SUBCOMMAND_NAMES lists '{name}' but register_apcli_subcommands \
253                 did not attach it — drift between builtin_group.rs constant and the \
254                 Registrar table in lib.rs"
255            );
256        }
257    }
258}