use std::path::PathBuf; use std::process::ExitCode; use clap::Parser; use pod::{Pod, PodController, PodFactory, PodManifestConfig, PodMetaConfig}; use session_store::FsStore; #[derive(Parser)] #[command( name = "pod", about = "Spawn a Pod process from cascaded manifest layers" )] struct Cli { /// User manifest TOML. Defaults to /// `$XDG_CONFIG_HOME/insomnia/manifest.toml`. #[arg(long, value_name = "PATH")] user_manifest: Option, /// Start the project-manifest walk from this directory. When /// omitted, the factory walks up from the current working /// directory looking for `.insomnia/manifest.toml`. #[arg(long, value_name = "PATH")] project: Option, /// Inline TOML string applied as the highest-priority overlay /// layer. Example: `--overlay 'pod.name = "dbg"'`. #[arg(long, value_name = "TOML")] overlay: Option, /// Shorthand that injects `pod.pwd = ` into the overlay /// layer. `--pwd .` uses the current working directory. #[arg(long, value_name = "PATH")] pwd: Option, /// Directory for session persistence. Defaults to /// `~/.insomnia/sessions/`. #[arg(short, long)] store: Option, } fn default_store_dir() -> Result { let home = std::env::var("HOME") .map_err(|_| std::io::Error::new(std::io::ErrorKind::NotFound, "HOME is not set"))?; Ok(PathBuf::from(home).join(".insomnia").join("sessions")) } fn default_runtime_dir() -> Result { if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { Ok(PathBuf::from(runtime_dir).join("insomnia")) } else if let Ok(home) = std::env::var("HOME") { Ok(PathBuf::from(home).join(".insomnia").join("run")) } else { Err(std::io::Error::new( std::io::ErrorKind::NotFound, "neither XDG_RUNTIME_DIR nor HOME is set", )) } } /// Construct a programmatic overlay [`PodManifestConfig`] that carries /// `pod.pwd` derived from the `--pwd` shorthand. Relative CLI paths /// are canonicalized here so the cascade always sees an absolute path. fn pwd_overlay(pwd: &PathBuf) -> PodManifestConfig { let absolute = std::fs::canonicalize(pwd).unwrap_or_else(|_| pwd.clone()); PodManifestConfig { pod: PodMetaConfig { pwd: Some(absolute), ..Default::default() }, ..Default::default() } } async fn build_factory(cli: &Cli) -> Result { let mut factory = PodFactory::new(); factory = match &cli.user_manifest { Some(path) => factory .with_user_manifest(path) .map_err(|e| format!("failed to load user manifest: {e}"))?, None => factory .with_user_manifest_auto() .map_err(|e| format!("failed to auto-load user manifest: {e}"))?, }; factory = match &cli.project { Some(path) => factory .with_project_manifest_from(path) .map_err(|e| format!("failed to load project manifest: {e}"))?, None => factory .with_project_manifest_auto() .map_err(|e| format!("failed to auto-load project manifest: {e}"))?, }; // `--pwd` goes in as a typed config so path strings never have to // pass through TOML escaping. `--overlay` keeps its inline-TOML // interface (that is its entire reason for existing). Both feed // the same overlay slot and merge in call order. if let Some(pwd) = cli.pwd.as_ref() { factory = factory.with_overlay_config(pwd_overlay(pwd)); } if let Some(overlay) = cli.overlay.as_deref() { factory = factory .with_overlay_toml(overlay) .map_err(|e| format!("failed to parse overlay TOML: {e}"))?; } Ok(factory) } #[tokio::main] async fn main() -> ExitCode { let cli = Cli::parse(); let factory = match build_factory(&cli).await { Ok(f) => f, Err(e) => { eprintln!("error: {e}"); return ExitCode::FAILURE; } }; let (manifest, loader) = match factory.resolve() { Ok(pair) => pair, Err(e) => { eprintln!("error: failed to resolve manifest cascade: {e}"); return ExitCode::FAILURE; } }; // Initialize persistent store let store_dir = cli.store.clone().unwrap_or_else(|| { default_store_dir().unwrap_or_else(|_| PathBuf::from(".insomnia/sessions")) }); let store = match FsStore::new(&store_dir).await { Ok(s) => s, Err(e) => { eprintln!("error: failed to initialize store at {store_dir:?}: {e}"); return ExitCode::FAILURE; } }; let pod = match Pod::from_manifest(manifest, store, loader).await { Ok(p) => p, Err(e) => { eprintln!("error: failed to create pod: {e}"); return ExitCode::FAILURE; } }; let pod_name = pod.manifest().pod.name.clone(); // Spawn the controller (starts socket server) let runtime_base = match default_runtime_dir() { Ok(d) => d, Err(e) => { eprintln!("error: {e}"); return ExitCode::FAILURE; } }; let handle = match PodController::spawn(pod, &runtime_base).await { Ok(h) => h, Err(e) => { eprintln!("error: failed to start pod controller: {e}"); return ExitCode::FAILURE; } }; eprintln!( "pod: {pod_name} listening on {:?}", handle.runtime_dir.socket_path() ); // Wait for shutdown signal match tokio::signal::ctrl_c().await { Ok(()) => { eprintln!("pod: {pod_name} shutting down"); } Err(e) => { eprintln!("error: failed to listen for signal: {e}"); } } drop(handle); ExitCode::SUCCESS }