use std::path::PathBuf;
use std::sync::Mutex;
use rusqlite::Connection;
use crate::error::{BosunError, Result};
pub mod recents;
pub use recents::Recent;
pub struct Store {
conn: Mutex<Connection>,
}
impl Store {
pub fn open_default() -> Result<Self> {
let path = default_db_path()?;
Self::open_at(&path)
}
pub fn open_at(path: &std::path::Path) -> Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(BosunError::Io)?;
}
let conn = Connection::open(path).map_err(map_sql_err)?;
let mut s = Self {
conn: Mutex::new(conn),
};
s.migrate()?;
Ok(s)
}
#[allow(dead_code)]
pub fn in_memory() -> Result<Self> {
let conn = Connection::open_in_memory().map_err(map_sql_err)?;
let mut s = Self {
conn: Mutex::new(conn),
};
s.migrate()?;
Ok(s)
}
fn migrate(&mut self) -> Result<()> {
let conn = self.conn.get_mut().expect("store mutex poisoned");
conn.pragma_update(None, "journal_mode", "WAL")
.map_err(map_sql_err)?;
conn.pragma_update(None, "synchronous", "NORMAL")
.map_err(map_sql_err)?;
conn.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS recents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
path TEXT NOT NULL,
agent TEXT NOT NULL,
args TEXT NOT NULL DEFAULT '',
claude_session_mode TEXT NOT NULL DEFAULT 'New',
claude_skip_permissions INTEGER NOT NULL DEFAULT 0,
codex_yolo INTEGER NOT NULL DEFAULT 0,
last_used_at INTEGER NOT NULL,
use_count INTEGER NOT NULL DEFAULT 1,
UNIQUE(name, path, agent) ON CONFLICT IGNORE
);
CREATE INDEX IF NOT EXISTS idx_recents_last_used
ON recents(last_used_at DESC);
CREATE INDEX IF NOT EXISTS idx_recents_path
ON recents(path);
"#,
)
.map_err(map_sql_err)?;
Ok(())
}
}
fn default_db_path() -> Result<PathBuf> {
let dirs = directories::ProjectDirs::from("dev", "yetidevworks", "bosun")
.ok_or_else(|| BosunError::Store("could not determine user data directory".to_string()))?;
Ok(dirs.data_dir().join("bosun.db"))
}
pub(crate) fn map_sql_err(e: rusqlite::Error) -> BosunError {
BosunError::Store(e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn in_memory_open_runs_migrations() {
let s = Store::in_memory().expect("in-memory open");
let count = s
.conn
.lock()
.unwrap()
.query_row("SELECT COUNT(*) FROM recents", [], |row| {
row.get::<_, i64>(0)
})
.expect("select from recents");
assert_eq!(count, 0);
}
}