//! Project-root discovery, subsystem initialisation (log, identity, DB, CRDT), //! and CRDT-sync configuration. use crate::config; use crate::crdt_state; use crate::crdt_sync; use crate::db; use crate::io::fs::find_story_kit_root; use crate::log_buffer; use crate::node_identity; use crate::state::SessionState; use crate::store::JsonFileStore; use crate::worktree; use std::path::{Path, PathBuf}; use std::sync::Arc; /// Open (or scaffold) the project root according to the CLI flags and CWD. /// /// Handles `--init`, an explicit path argument, and the default auto-detect /// behaviour. Modifies `app_state.project_root` as a side effect. pub(crate) async fn open_project_root( is_init: bool, explicit_path: Option, cwd: &Path, app_state: &Arc, store: &Arc, port: u16, ) { if is_init { let init_root = explicit_path.unwrap_or_else(|| cwd.to_path_buf()); if !init_root.exists() { std::fs::create_dir_all(&init_root).unwrap_or_else(|e| { eprintln!( "error: cannot create directory {}: {e}", init_root.display() ); std::process::exit(1); }); } match crate::io::fs::open_project( init_root.to_string_lossy().to_string(), app_state, store.as_ref(), port, ) .await { Ok(_) => { if let Some(root) = app_state.project_root.lock().unwrap().as_ref() { config::ProjectConfig::load(root) .unwrap_or_else(|e| panic!("Invalid project.toml: {e}")); crate::io::wizard::WizardState::init_if_missing(root); } } Err(e) => { eprintln!("error: {e}"); std::process::exit(1); } } } else if let Some(explicit_root) = explicit_path { match crate::io::fs::open_project( explicit_root.to_string_lossy().to_string(), app_state, store.as_ref(), port, ) .await { Ok(_) => { if let Some(root) = app_state.project_root.lock().unwrap().as_ref() { config::ProjectConfig::load(root) .unwrap_or_else(|e| panic!("Invalid project.toml: {e}")); } } Err(e) => { eprintln!("error: {e}"); std::process::exit(1); } } } else if let Some(project_root) = find_story_kit_root(cwd) { crate::io::fs::open_project( project_root.to_string_lossy().to_string(), app_state, store.as_ref(), port, ) .await .unwrap_or_else(|e| { crate::slog!("Warning: failed to auto-open project at {project_root:?}: {e}"); project_root.to_string_lossy().to_string() }); config::ProjectConfig::load(&project_root) .unwrap_or_else(|e| panic!("Invalid project.toml: {e}")); } else { crate::io::fs::open_project( cwd.to_string_lossy().to_string(), app_state, store.as_ref(), port, ) .await .unwrap_or_else(|e| { crate::slog!("Warning: failed to scaffold project at {cwd:?}: {e}"); cwd.to_string_lossy().to_string() }); } } /// Set up the server log file, node identity keypair, pipeline DB, and CRDT state. pub(crate) async fn init_subsystems(app_state: &Arc, cwd: &Path) { // Enable persistent server log file now that the project root is known. if let Some(ref root) = *app_state.project_root.lock().unwrap() { let log_dir = root.join(".huskies").join("logs"); let _ = std::fs::create_dir_all(&log_dir); log_buffer::global().set_log_file(log_dir.join("server.log")); } // Initialise the node's Ed25519 identity keypair (file-based, mode 0600). // The key is stored at .huskies/node_identity.key and persisted across restarts. { let key_path = app_state .project_root .lock() .unwrap() .as_ref() .map(|root| root.join(".huskies").join("node_identity.key")) .unwrap_or_else(|| cwd.join(".huskies").join("node_identity.key")); if let Err(e) = node_identity::init_identity(&key_path) { crate::slog!("[identity] Failed to initialise node identity keypair: {e}"); } else if let Some(id) = node_identity::get_identity() { crate::slog!("[identity] Node ID: {}", id.node_id); } } // Initialise the SQLite pipeline shadow-write database and CRDT state layer. // Clone the path out before the await so we don't hold the MutexGuard across // an await point. let pipeline_db_path = app_state .project_root .lock() .unwrap() .as_ref() .map(|root| root.join(".huskies").join("pipeline.db")); if let Some(ref db_path) = pipeline_db_path { if let Err(e) = db::init(db_path).await { crate::slog!("[db] Failed to initialise pipeline.db: {e}"); } if let Err(e) = crdt_state::init(db_path).await { crate::slog!("[crdt] Failed to initialise CRDT state layer: {e}"); } else { crdt_state::migrate_names_from_slugs(); let id_migrations = crdt_state::migrate_story_ids_to_numeric(); if !id_migrations.is_empty() && let Some(project_root) = db_path.parent().and_then(|p| p.parent()) { worktree::migrate_slug_paths(project_root, &id_migrations); } // Story 865: one-shot strip of legacy YAML front-matter from // every stored body. Idempotent — bodies without `---` are // skipped on subsequent runs. db::yaml_migration::run(); } } } /// Wire up CRDT sync: trusted keys, token auth, and the rendezvous client. /// /// In agent mode the rendezvous URL comes from the CLI; otherwise it is read /// from `project.toml`. pub(crate) fn configure_crdt_sync( app_state: &Arc, is_agent: bool, agent_rendezvous: Option, crdt_join_token: Option, ) { let sync_config = if is_agent { agent_rendezvous .clone() .map(|url| (url, Vec::new(), false, Vec::new())) } else { app_state .project_root .lock() .unwrap() .as_ref() .and_then(|root| config::ProjectConfig::load(root).ok()) .and_then(|cfg| { cfg.rendezvous.map(|url| { ( url, cfg.trusted_keys, cfg.crdt_require_token, cfg.crdt_tokens, ) }) }) }; if let Some((rendezvous_url, trusted_keys, require_token, crdt_tokens)) = sync_config { crdt_sync::init_trusted_keys(trusted_keys); crdt_sync::init_token_auth(require_token, crdt_tokens); crdt_sync::spawn_rendezvous_client(rendezvous_url, crdt_join_token); } else { let (keys, require_token, crdt_tokens) = app_state .project_root .lock() .unwrap() .as_ref() .and_then(|root| config::ProjectConfig::load(root).ok()) .map(|cfg| (cfg.trusted_keys, cfg.crdt_require_token, cfg.crdt_tokens)) .unwrap_or_default(); crdt_sync::init_trusted_keys(keys); crdt_sync::init_token_auth(require_token, crdt_tokens); } }