Skip to main content

apr_cli/
lib.rs

1//! apr-cli library
2//!
3//! This library is the foundation for the apr CLI binary.
4//! Exports CLI structures for testing and reuse.
5
6// APR-MONO: Clippy pedantic allows for monorepo transition.
7// unwrap() eliminated (524 → expect()). Style lints from 20 merged crates
8// are suppressed at crate level. Will be incrementally addressed.
9#![allow(clippy::all, clippy::pedantic, clippy::disallowed_methods)]
10#![allow(
11    unreachable_code,
12    unused_variables,
13    unused_imports,
14    dead_code,
15    unused_assignments
16)]
17
18use clap::{Parser, Subcommand, ValueEnum};
19use std::path::{Path, PathBuf};
20
21// Contract assertions from YAML (pv codegen)
22#[macro_use]
23#[allow(unused_macros, clippy::duplicated_attributes)]
24mod generated_contracts;
25
26mod commands;
27pub mod error;
28mod output;
29pub mod pipe;
30
31pub use error::CliError;
32
33// Public re-exports for integration tests
34pub mod qa_types {
35    pub use crate::commands::qa::{GateResult, QaReport, SystemInfo};
36}
37
38// Public re-exports for downstream crates (whisper-apr proxies these)
39pub mod model_pull {
40    pub use crate::commands::pull::{list, run};
41}
42
43// HELIX-IDEA-009: API key auth re-exported so integration tests in
44// `tests/falsify_auth_*.rs` can construct gates and middleware without
45// reaching into the private `commands` tree.
46pub mod serve_auth {
47    #[cfg(feature = "inference")]
48    pub use crate::commands::serve::auth::layer;
49    pub use crate::commands::serve::auth::{apply, AuthGate};
50}
51
52// PMAT-923: e2e seam so `tests/ollama_api_serve_compat.rs` can build the REAL
53// `apr serve` APR-CPU router (the one mounted for a `.apr` model) and prove the
54// Ollama `/api/chat` + `/api/generate` routes are wired — not realizar's
55// `create_router`.
56#[cfg(feature = "inference")]
57pub mod serve_test_support {
58    pub use crate::commands::serve::handlers::build_demo_apr_cpu_router_for_test;
59    // PMAT-928: streaming-capable demo router whose NDJSON path is driven by a
60    // scripted token sequence, so the streaming falsifier observes real
61    // multi-chunk NDJSON without loading a model.
62    pub use crate::commands::serve::handlers::build_demo_streaming_apr_cpu_router_for_test;
63}
64
65#[cfg(feature = "inference")]
66pub mod federation;
67
68// Commands are crate-private, used internally by execute_command
69use commands::{
70    bench, canary, canary::CanaryCommands, cbtop, chat, compare_hf, compile, convert, data, debug,
71    diagnose, diff, distill, eval, explain, export, flow, hex, import, inspect, lint, mcp, merge,
72    oracle, pipeline, probar, profile, prune, publish, pull, qa, qualify, quantize, rosetta,
73    rosetta::RosettaCommands, run, serve, showcase, stamp, tensors, tokenize, trace, tree, tui,
74    validate, validate_manifest,
75};
76#[cfg(feature = "training")]
77use commands::{finetune, gpu, train, tune};
78
79#[cfg(feature = "training")]
80pub use commands::pretrain::PretrainMode;
81
82/// apr - APR Model Operations Tool
83///
84/// Inspect, debug, and manage .apr model files.
85/// Toyota Way: Genchi Genbutsu - Go and see the actual data.
86#[derive(Parser, Debug)]
87#[command(name = "apr")]
88#[command(author, version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("APR_GIT_SHA"), ")"), about, long_about = None)]
89#[command(propagate_version = true)]
90pub struct Cli {
91    #[command(subcommand)]
92    pub command: Box<Commands>,
93
94    /// Output as JSON
95    #[arg(long, global = true)]
96    pub json: bool,
97
98    /// Verbose output
99    #[arg(short, long, global = true)]
100    pub verbose: bool,
101
102    /// Quiet mode (errors only)
103    #[arg(short, long, global = true)]
104    pub quiet: bool,
105
106    /// Disable network access (Sovereign AI compliance, Section 9)
107    #[arg(long, global = true)]
108    pub offline: bool,
109
110    /// Skip tensor contract validation (PMAT-237: use with diagnostic tooling)
111    #[arg(long, global = true)]
112    pub skip_contract: bool,
113}
114
115include!("commands_enum.rs");
116include!("model_ops_commands.rs");
117include!("extended_commands.rs");
118include!("tool_commands.rs");
119include!("data_commands.rs");
120#[cfg(feature = "training")]
121include!("train_commands.rs");
122include!("serve_commands.rs");
123include!("tokenize_commands.rs");
124include!("pipeline_commands.rs");
125include!("validate.rs");
126include!("dispatch_run.rs");
127include!("dispatch.rs");
128include!("dispatch_analysis.rs");
129include!("lib_07.rs");
130
131/// Full CLI entry point for `cargo install aprender`.
132///
133/// This function encapsulates the complete `apr` binary logic so that
134/// the `aprender` facade crate can produce the same binary via
135/// `cargo install aprender` (in addition to `cargo install apr-cli`).
136pub fn cli_main() -> std::process::ExitCode {
137    // GH-667: Reset SIGPIPE to default so piping to head/less doesn't panic.
138    #[cfg(unix)]
139    #[allow(unsafe_code)]
140    unsafe {
141        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
142    }
143
144    // GH-646: Clear FPCR.FZ16 on aarch64 so f16 subnormals work.
145    #[cfg(target_arch = "aarch64")]
146    #[allow(unsafe_code)]
147    unsafe {
148        let fpcr: u64;
149        core::arch::asm!("mrs {}, fpcr", out(reg) fpcr);
150        if fpcr & (1 << 19) != 0 {
151            let new_fpcr = fpcr & !(1 << 19);
152            core::arch::asm!("msr fpcr, {}", in(reg) new_fpcr);
153        }
154    }
155
156    // GH-662: Respect NO_COLOR env var and non-TTY output.
157    let no_color = std::env::var("NO_COLOR").is_ok();
158    let is_tty = std::io::IsTerminal::is_terminal(&std::io::stdout());
159    if no_color || !is_tty {
160        colored::control::set_override(false);
161    }
162
163    // FALSIFY-GPUTRAIN-007 / INV-GPUTRAIN-007 — `apr --version --json` MUST
164    // emit a machine-parseable object containing at least the three keys
165    // { cuda_feature, cuda_runtime_available, visible_devices }. Intercept
166    // this flag combo BEFORE clap's default `--version` handler exits the
167    // process with a plain string. Bound by `gputrain_007.rs` — see
168    // `AC_GPUTRAIN_007_REQUIRED_VERSION_JSON_KEYS`.
169    let raw: Vec<String> = std::env::args().collect();
170    if raw.iter().any(|a| a == "--version") && raw.iter().any(|a| a == "--json") {
171        emit_version_json();
172        return std::process::ExitCode::SUCCESS;
173    }
174
175    let cli = Cli::parse();
176    match execute_command(&cli) {
177        Ok(()) => std::process::ExitCode::SUCCESS,
178        Err(e) => {
179            eprintln!("error: {e}");
180            e.exit_code()
181        }
182    }
183}
184
185/// FALSIFY-GPUTRAIN-007 — emit `apr --version --json` output with the
186/// 3-key schema required by `AC_GPUTRAIN_007_REQUIRED_VERSION_JSON_KEYS`.
187///
188/// Schema:
189/// ```json
190/// {
191///   "name":    "apr",
192///   "version": "<semver>",
193///   "git_sha": "<commit>",
194///   "cuda_feature":           <bool>,   // was the binary built --features cuda?
195///   "cuda_runtime_available": <bool>,   // does cudaRuntimeGetVersion succeed?
196///   "visible_devices":        ["0", "1", ...]  // nvidia-smi -L indices, empty if no runtime
197/// }
198/// ```
199///
200/// Consumers (`entrenar::train::gputrain_007::verdict_from_version_json_keys`
201/// and `verdict_from_version_json_fields`) parse this and assert schema
202/// completeness + field invariants (`visible_devices.len() <= 16`, no
203/// `cuda_feature && !cuda_runtime_available` inconsistency).
204pub fn emit_version_json() {
205    let cuda_feature = cfg!(feature = "cuda");
206
207    // cuda_runtime_available: try nvidia-smi -L. Present-and-exits-0 ⇒ true.
208    // This matches how gputrain_003 queries nvidia-smi — keep the probe
209    // consistent with the residency check.
210    let cuda_runtime_available = std::process::Command::new("nvidia-smi")
211        .arg("-L")
212        .output()
213        .map(|o| o.status.success())
214        .unwrap_or(false);
215
216    // visible_devices: if the runtime is available, parse nvidia-smi -L
217    // output (one GPU per line, "GPU 0: ...", "GPU 1: ..."). Emit the
218    // index strings to match INV-GPUTRAIN-001 grammar (:0..:15).
219    let visible_devices: Vec<String> = if cuda_runtime_available {
220        std::process::Command::new("nvidia-smi")
221            .arg("-L")
222            .output()
223            .ok()
224            .and_then(|o| String::from_utf8(o.stdout).ok())
225            .map(|s| {
226                s.lines()
227                    .filter_map(|line| {
228                        // Expect "GPU <idx>: <name> (UUID: <uuid>)"
229                        line.strip_prefix("GPU ").and_then(|rest| {
230                            rest.split_once(':').map(|(idx, _)| idx.trim().to_string())
231                        })
232                    })
233                    .collect()
234            })
235            .unwrap_or_default()
236    } else {
237        Vec::new()
238    };
239
240    let body = serde_json::json!({
241        "name": "apr",
242        "version": env!("CARGO_PKG_VERSION"),
243        "git_sha": env!("APR_GIT_SHA"),
244        "cuda_feature": cuda_feature,
245        "cuda_runtime_available": cuda_runtime_available,
246        "visible_devices": visible_devices,
247    });
248
249    // Emit pretty-printed JSON on stdout so it's grep-friendly and
250    // round-trippable through `| jq .cuda_feature`.
251    println!(
252        "{}",
253        serde_json::to_string_pretty(&body).expect("build version json")
254    );
255}