use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use clap::Args;
use serde::Serialize;
use crate::exit::Exit;
use crate::output::{self, Envelope};
use crate::paths::{assert_secure_data_dir, DataLayout};
use cortex_core::SCHEMA_VERSION;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[derive(Debug, Args)]
pub struct InitArgs {
#[arg(long, value_name = "PATH")]
pub db: Option<PathBuf>,
#[arg(long = "event-log", value_name = "PATH")]
pub event_log: Option<PathBuf>,
#[arg(long = "validate-perms")]
pub validate_perms: bool,
}
#[derive(Debug, Clone)]
pub struct InitOutcome {
pub layout: DataLayout,
pub created_db: bool,
pub created_event_log: bool,
}
pub fn run(args: InitArgs) -> Exit {
match run_inner(args) {
Ok(outcome) => {
if output::json_enabled() {
let envelope =
Envelope::new("cortex.init", Exit::Ok, InitReport::from_outcome(&outcome));
output::emit(&envelope, Exit::Ok)
} else {
print_summary(&outcome);
Exit::Ok
}
}
Err(e) => {
if output::json_enabled() {
let envelope = Envelope::new("cortex.init", e, serde_json::Value::Null);
output::emit(&envelope, e)
} else {
e
}
}
}
}
#[derive(Debug, Serialize)]
struct InitReport {
data_dir: String,
db_path: String,
event_log_path: String,
schema_version: u16,
created_db: bool,
created_event_log: bool,
applied_migrations: Vec<String>,
}
impl InitReport {
fn from_outcome(outcome: &InitOutcome) -> Self {
Self {
data_dir: outcome.layout.data_dir.display().to_string(),
db_path: outcome.layout.db_path.display().to_string(),
event_log_path: outcome.layout.event_log_path.display().to_string(),
schema_version: SCHEMA_VERSION,
created_db: outcome.created_db,
created_event_log: outcome.created_event_log,
applied_migrations: Vec::new(),
}
}
}
pub fn run_inner(args: InitArgs) -> Result<InitOutcome, Exit> {
let layout = DataLayout::resolve(args.db, args.event_log)?;
if !layout.data_dir.exists() {
fs::create_dir_all(&layout.data_dir).map_err(|_| Exit::PreconditionUnmet)?;
#[cfg(unix)]
{
let perms = fs::Permissions::from_mode(0o700);
fs::set_permissions(&layout.data_dir, perms).map_err(|_| Exit::PreconditionUnmet)?;
}
}
if args.validate_perms {
assert_secure_data_dir(&layout.data_dir)?;
}
preflight_no_sqlite_sidecars(&layout.db_path)?;
let created_db = create_if_missing(&layout.db_path)?;
let created_event_log = create_if_missing(&layout.event_log_path)?;
Ok(InitOutcome {
layout,
created_db,
created_event_log,
})
}
fn create_if_missing(path: &Path) -> Result<bool, Exit> {
if path.exists() {
return Ok(false);
}
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
fs::create_dir_all(parent).map_err(|_| Exit::PreconditionUnmet)?;
}
}
let mut f = fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(path)
.map_err(|_| Exit::PreconditionUnmet)?;
f.write_all(b"").map_err(|_| Exit::PreconditionUnmet)?;
f.sync_all().map_err(|_| Exit::PreconditionUnmet)?;
Ok(true)
}
fn preflight_no_sqlite_sidecars(db_path: &Path) -> Result<(), Exit> {
for sidecar in sqlite_sidecar_paths(db_path) {
if sidecar.exists() {
eprintln!(
"cortex init: precondition unmet: sqlite sidecar {} exists before store verification; no state was changed.",
sidecar.display()
);
return Err(Exit::PreconditionUnmet);
}
}
Ok(())
}
fn sqlite_sidecar_paths(db_path: &Path) -> [PathBuf; 2] {
let raw = db_path.as_os_str().to_string_lossy();
[
PathBuf::from(format!("{raw}-wal")),
PathBuf::from(format!("{raw}-shm")),
]
}
fn print_summary(out: &InitOutcome) {
println!("cortex init: data_dir = {}", out.layout.data_dir.display());
println!(
"cortex init: db = {} ({})",
out.layout.db_path.display(),
if out.created_db { "created" } else { "exists" },
);
println!(
"cortex init: event_log = {} ({})",
out.layout.event_log_path.display(),
if out.created_event_log {
"created"
} else {
"exists"
},
);
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn args_with(tmp: &Path) -> InitArgs {
InitArgs {
db: Some(tmp.join("cortex.db")),
event_log: Some(tmp.join("events.jsonl")),
validate_perms: false,
}
}
#[test]
fn init_creates_db_and_log() {
let tmp = tempdir().unwrap();
let out = run_inner(args_with(tmp.path())).expect("init ok");
assert!(out.created_db);
assert!(out.created_event_log);
assert!(tmp.path().join("cortex.db").exists());
assert!(tmp.path().join("events.jsonl").exists());
}
#[test]
fn init_is_idempotent() {
let tmp = tempdir().unwrap();
let _first = run_inner(args_with(tmp.path())).unwrap();
let second = run_inner(args_with(tmp.path())).expect("idempotent ok");
assert!(!second.created_db);
assert!(!second.created_event_log);
}
#[test]
fn init_rejects_existing_wal_sidecar() {
let tmp = tempdir().unwrap();
std::fs::write(tmp.path().join("cortex.db-wal"), b"unverified wal").unwrap();
let err = run_inner(args_with(tmp.path())).unwrap_err();
assert_eq!(err, Exit::PreconditionUnmet);
assert!(
!tmp.path().join("cortex.db").exists(),
"init must not create db after sidecar preflight failure"
);
assert!(
!tmp.path().join("events.jsonl").exists(),
"init must not create event log after sidecar preflight failure"
);
}
#[cfg(unix)]
#[test]
fn validate_perms_rejects_0755_dir() {
use std::fs;
let tmp = tempdir().unwrap();
let dir = tmp.path().join("data");
fs::create_dir(&dir).unwrap();
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
let args = InitArgs {
db: Some(dir.join("cortex.db")),
event_log: Some(dir.join("events.jsonl")),
validate_perms: true,
};
let err = run_inner(args).unwrap_err();
assert_eq!(err, Exit::PreconditionUnmet);
}
}