// large_futures: stack futures on hot paths by design. // too_many_lines: style gate only (the harness fns are intentionally linear). // Casts: 64-bit targets; narrowing sits at bounded OS/protocol boundaries. #![allow( clippy::large_futures, clippy::too_many_lines, clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss, clippy::cast_precision_loss )] //! pa-cli: the `prime-agent` binary. The argument surface, command routing, //! help output, and validation are faithful ports of the TypeScript product. // Internal ported modules are crate-private: the only public API is the // runtime boundary below (see crates/pa-cli/README.md). // A test binary's own TMPDIR: fixtures the suite leaves behind die with the process. #[cfg(test)] use pa_test_scratch as _; #[cfg(test)] pub(crate) mod test_support { /// The faux provider registers process-globally. Every pa-cli test module /// that drives it must hold this one lock: per-module locks still allow /// another module's registration to replace the provider mid-test. pub(crate) static FAUX_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); } pub(crate) mod args; pub(crate) mod client_settings; pub(crate) mod client_update; pub(crate) mod codex_subscription_login; pub(crate) mod command_registry; pub(crate) mod config; pub(crate) mod config_command; pub(crate) mod daemon_client; pub(crate) mod daemon_command; pub(crate) mod daemon_discovery; pub(crate) mod daemon_mode; pub(crate) mod daemon_session_list; pub(crate) mod file_processor; pub(crate) mod global_flags; pub(crate) mod incident; pub(crate) mod initial_message; pub(crate) mod installer_update; pub(crate) mod interactive_mode; pub(crate) mod list_models; pub(crate) mod mcp_command; pub(crate) mod mcp_login; pub(crate) mod mode; pub(crate) mod package_command; pub(crate) mod prime_inference_login; pub(crate) mod prompt_command; pub(crate) mod provider_login; pub(crate) mod public_command; pub(crate) mod refresh_models; pub(crate) mod self_update; pub(crate) mod session_export; pub(crate) mod sessions_table_format; pub(crate) mod subscription_login; /// The runtime boundary: everything a mode-runner crate implements to plug /// into the `prime-agent` binary, plus the entry point that drives it. pub use mode::{AppMode, MissingSubsystem, RunOptions, Runtime, UnavailableRuntime}; pub(crate) mod piped_stdin; pub(crate) mod print_boundary; pub(crate) mod print_goal; pub(crate) mod print_rlm; pub mod print_runtime; pub(crate) mod print_session_command; pub mod update_flow; pub mod util_time; pub use print_runtime::PrintRuntime; /// Daemon wiring shared by the interactive runtime and the integration /// tests: spawn/probe the supervisor on a socket, and map `--daemon-socket`. pub use interactive_mode::{ ensure_daemon_running, ensure_daemon_running_with, resolve_socket_path, }; /// Entry point shared by the binary and the integration tests. Returns the /// process exit code. pub fn main_with_runtime(args: &[String], runtime: &dyn mode::Runtime) -> i32 { match main_impl(args, runtime) { Ok(code) => code, Err(error) => { eprintln!("Error: {error}"); 1 } } } fn main_impl(args: &[String], runtime: &dyn mode::Runtime) -> Result { use std::io::IsTerminal; pa_daemon::protocol::configure_app_version(crate::config::version()) .map_err(|error| error.to_string())?; let offline_mode = args.iter().any(|arg| arg == "--offline") || crate::config::is_truthy_env_flag( std::env::var(crate::config::ENV_OFFLINE).ok().as_deref(), ); if offline_mode { std::env::set_var(crate::config::ENV_OFFLINE, "1"); } // Install-time kernel preparation: the installer invokes `--prime-agent-bootstrap` // after extracting a release, so the venv is ready before the first session. if args.len() == 1 && args[0] == "--prime-agent-bootstrap" { return run_runtime_bootstrap(); } // Public command routing: help requests, removed commands, management // commands, and the model/session rewrites. let public_command = public_command::handle_public_command(args); if public_command.handled { return Ok(public_command.exit_code.unwrap_or(0)); } let args = public_command.args; if args.first().map(String::as_str) == Some("config") { return Ok(crate::config_command::run()); } let parsed = args::parse_args(&args); if !parsed.diagnostics.is_empty() { for diagnostic in &parsed.diagnostics { let label = if diagnostic.is_error { "Error" } else { "Warning" }; eprintln!("{label}: {}", diagnostic.message); } if parsed.diagnostics.iter().any(|d| d.is_error) { return Ok(1); } } let app_mode = mode::AppMode::resolve(&parsed, std::io::stdin().is_terminal()); // Enable Windows VT processing before raw ANSI mode writes. pa_types::platform::console_init(); if public_command.attach_agent.is_some() && app_mode != mode::AppMode::Interactive { return Err("attach requires an interactive terminal".to_string()); } if parsed.resume_bare && app_mode != mode::AppMode::Interactive { return Err( "--resume without a session selector requires an interactive terminal".to_string(), ); } if parsed.version { println!("{}", crate::config::version()); return Ok(0); } if parsed.help { println!("{}", command_registry::format_top_level_help()); return Ok(0); } if parsed.refresh { return refresh_models::run(&args, &parsed); } if parsed.export.is_some() { // The TS product only reaches the export subsystem through the `session // export [output]` rewrite (a standalone `--export` exits above). return session_export::run(&parsed, &crate::config::get_agent_dir()); } if matches!( parsed.mode, Some(args::Mode::Rpc | args::Mode::Daemon | args::Mode::Acp) ) && !parsed.file_args.is_empty() { return Err("@file arguments are not supported in RPC, daemon, or ACP mode".to_string()); } // Daemon worker processes start with the worker role env var set: route // straight into the worker runtime. The binary entry keeps the worker // handshake env only for the supervisor-spawned `worker` subcommand (it // strips an inherited one from every other invocation), so the routing // reads that subcommand alone. let is_worker_process = std::env::var(pa_daemon::worker::WORKER_ROLE_ENV).unwrap_or_default() == "1"; if is_worker_process && args.first().map(String::as_str) == Some("worker") { return run_worker_mode(); } if let Some(fork) = &parsed.fork { let mut conflicting_flags: Vec<&str> = Vec::new(); if parsed.continue_ { conflicting_flags.push("--continue"); } if parsed.has_resume() { conflicting_flags.push("--resume"); } if parsed.no_session { conflicting_flags.push("--no-session"); } if !conflicting_flags.is_empty() { return Err(format!( "--fork cannot be combined with {}", conflicting_flags.join(", ") )); } let _ = fork; } let cwd = match &parsed.cwd { Some(cwd) => { let cwd = crate::config::expand_tilde_path(cwd); let from = std::env::current_dir().map_err(|e| e.to_string())?; if let Err(error) = std::env::set_current_dir(&cwd) { return Err(format!( "Cannot use cwd {}: {}", cwd.display(), node_style_error( &error, "chdir", &from.display().to_string(), &cwd.display().to_string() ) )); } std::env::current_dir().map_err(|e| e.to_string())? } None => std::env::current_dir().map_err(|e| e.to_string())?, }; if crate::config::is_truthy_env_flag( std::env::var(crate::config::ENV_STARTUP_BENCHMARK) .ok() .as_deref(), ) && app_mode != mode::AppMode::Interactive { return Err("PI_STARTUP_BENCHMARK only supports interactive mode".to_string()); } let agent_dir = crate::config::get_agent_dir(); let session_dir = parsed .session_dir .as_deref() .map(crate::config::expand_tilde_path) .or_else(crate::config::get_session_dir_env_override) // sessionDir resolution: the cwd-scoped settings manager is // consulted after the flag and env overrides, before the default. .or_else(|| pa_core::settings::SettingsManager::create(&cwd, &agent_dir).get_session_dir()); let mut cli_messages = parsed.messages.clone(); // Startup branches read piped stdin for every mode but the stdio-protocol ones // (rpc/acp/daemon keep stdin); the idle window keeps a silent pipe from hanging. let stdin_content = if matches!(app_mode, AppMode::Rpc | AppMode::Acp | AppMode::Daemon) { None } else { piped_stdin::read_piped_stdin(piped_stdin::resolve_stdin_idle_timeout_ms( std::env::var(piped_stdin::STDIN_IDLE_TIMEOUT_MS_ENV) .ok() .as_deref(), )) }; // `@file` arguments expand into text and image attachments for the initial // prompt; the file blocks ride every mode's first message. let (file_text, initial_images) = if parsed.file_args.is_empty() { (None, Vec::new()) } else { let auto_resize = pa_core::settings::SettingsManager::create(&cwd, &agent_dir).get_image_auto_resize(); match file_processor::process_file_arguments(&parsed.file_args, &cwd, auto_resize) { Ok(processed) => ( Some(processed.text).filter(|text| !text.is_empty()), processed.images, ), Err(error) => { eprintln!("{}", error.message); return Ok(1); } } }; let initial_message = initial_message::build_initial_message( &mut cli_messages, file_text.as_deref(), stdin_content.as_deref(), ) .initial_message; let options = mode::RunOptions { app_mode, config: mode::runtime_config_from_args(&parsed, cwd, agent_dir, session_dir.clone()), session: mode::SessionOptions { continue_recent: parsed.continue_, resume_bare: parsed.resume_bare, resume: parsed.resume.clone(), fork: parsed.fork.clone(), no_session: parsed.no_session, session_dir, cwd_from_flag: parsed.cwd.is_some(), }, messages: cli_messages, initial_message, initial_images, file_args: parsed.file_args.clone(), daemon_socket: parsed.daemon_socket.clone(), list_models: parsed.list_models, verbose: parsed.verbose, offline: parsed.offline, agents_view_requested: public_command.explicit_agents_view, attach_agent: public_command.attach_agent, }; match runtime.run(&options) { Ok(exit_code) => Ok(exit_code), Err(missing) => Err(missing.error_message()), } } /// Prepare the kernel runtime at install time: resolve or bootstrap the kernel /// Python and print its path. Failures print the error and exit 1. fn run_runtime_bootstrap() -> Result { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .map_err(|error| error.to_string())?; match runtime.block_on(pa_core::kernel::ensure_kernel_python( pa_core::kernel::EnsureKernelPythonOptions::default(), )) { Ok(python) => { println!("kernel python: {}", python.display()); Ok(0) } Err(error) => Err(format!("{error:#}")), } } /// Run the daemon session-worker runtime (the process the supervisor spawns /// for each live session). fn run_worker_mode() -> Result { let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .map_err(|error| error.to_string())?; match runtime.block_on(pa_daemon::worker::run_worker()) { Ok(()) => Ok(0), Err(error) => Err(format!("{error:#}")), } } /// Format an io error the way Node.js prints it for the same syscall, so the /// `Error: Cannot use cwd : ` text matches the TS product. fn node_style_error(error: &std::io::Error, syscall: &str, from: &str, to: &str) -> String { match error.kind() { std::io::ErrorKind::NotFound => { format!("ENOENT: no such file or directory, {syscall} '{from}' -> '{to}'") } std::io::ErrorKind::NotADirectory => { format!("ENOTDIR: not a directory, {syscall} '{from}' -> '{to}'") } std::io::ErrorKind::PermissionDenied => { format!("EACCES: permission denied, {syscall} '{from}' -> '{to}'") } _ => error.to_string(), } }