214 lines
7.4 KiB
Rust
214 lines
7.4 KiB
Rust
|
|
//! 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<PathBuf>,
|
||
|
|
cwd: &Path,
|
||
|
|
app_state: &Arc<SessionState>,
|
||
|
|
store: &Arc<JsonFileStore>,
|
||
|
|
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<SessionState>, 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 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<SessionState>,
|
||
|
|
is_agent: bool,
|
||
|
|
agent_rendezvous: Option<String>,
|
||
|
|
crdt_join_token: Option<String>,
|
||
|
|
) {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|