Skip to content

Commit 82762b4

Browse files
tcconnallytcconnallyclaude
authored
feat(#427): default on-disk paths to ~/.perseus-vault (precedence-only, no data moved) (#437)
The DB *filename* was already `perseus-vault.db` (#421); the remaining brand artifact was the `~/.mimir/` support directory (holds `data/`, `secret.key`), so operators saw "Perseus Vault stored under ~/.mimir". This is a precedence-only rename (no data is ever moved): * resolve_default_db: new canonical `~/.perseus-vault/data/perseus-vault.db` is precedence #1; the whole `~/.mimir/data/*` chain and `~/mimir.db` stay below it, so an existing ~/.mimir install is still adopted via the fallback chain and upgraders are never orphaned. Fresh installs create `~/.perseus-vault/`. * PERSEUS_VAULT_DB_PATH is the current-brand env override; MIMIR_DB_PATH stays honored (checked second) for back-compat. Both suppress the split-brain refinement in normalize_default_db. * default_key_file: prefers whichever secret.key already exists (~/.perseus-vault first, else ~/.mimir), so an existing ENCRYPTED install never loses its key to a changed default; fresh installs use the new dir. * Help text updated to the new default paths + env var. Deliberately NOT done (per maintainer decision): physically moving/symlinking ~/.mimir -> ~/.perseus-vault. Relocating a live data dir (open DB + secret.key) is risky in this split-brain-sensitive resolver, so we take the safe route: fresh installs get the clean path; existing installs keep working untouched. The opt-in ObsidianSync `~/.mimir/vault` default is left for a follow-up. Tests: fresh install resolves to the new ~/.perseus-vault canonical; new dir preferred when both present; legacy ~/.mimir adopted on upgrade (not shadowed). All 12 resolve_default_db tests pass (cargo test, MSVC toolchain). Co-authored-by: tcconnally <hermes@perseus.observer> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a18c606 commit 82762b4

1 file changed

Lines changed: 80 additions & 20 deletions

File tree

src/main.rs

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ struct Cli {
2727
#[command(subcommand)]
2828
command: Option<Commands>,
2929

30-
/// SQLite database path (default: $MIMIR_DB_PATH or ~/.mimir/data/perseus-vault.db,
31-
/// falling back to an existing ~/.mimir/data/mneme.db or ~/.mimir/data/mimir.db
32-
/// from before the Perseus Vault rename). Used when running the server directly
30+
/// SQLite database path (default: $PERSEUS_VAULT_DB_PATH / $MIMIR_DB_PATH or
31+
/// ~/.perseus-vault/data/perseus-vault.db, falling back to an existing
32+
/// ~/.mimir/data/{perseus-vault,mneme,mimir}.db from before the rename).
33+
/// Used when running the server directly
3334
/// without the `serve` subcommand — matches the documented MCP host config:
3435
/// `perseus-vault --db /path/to/perseus-vault.db`.
3536
#[arg(long)]
@@ -238,7 +239,8 @@ enum Commands {
238239

239240
/// Generate a new AES-256-GCM encryption key and write it to a file
240241
Keygen {
241-
/// Path to write the key file (default: ~/.mimir/secret.key)
242+
/// Path to write the key file (default: ~/.perseus-vault/secret.key, or
243+
/// an existing ~/.mimir/secret.key from before the rename)
242244
#[arg(long, default_value_t = default_key_file())]
243245
key_file: String,
244246
},
@@ -356,7 +358,7 @@ enum Commands {
356358
ObsidianSync {
357359
/// Target Obsidian vault directory (created if needed)
358360
vault_path: String,
359-
/// SQLite database path (defaults to $MIMIR_DB_PATH or ~/.mimir/data/perseus-vault.db)
361+
/// SQLite database path (defaults to $PERSEUS_VAULT_DB_PATH / $MIMIR_DB_PATH or ~/.perseus-vault/data/perseus-vault.db)
360362
#[arg(long)]
361363
db: Option<String>,
362364
/// Continuously re-export whenever memory changes
@@ -509,12 +511,18 @@ struct DbResolution {
509511
/// pass `--db` or set `$MIMIR_DB_PATH`.
510512
///
511513
/// Precedence (first existing wins):
512-
/// 1. `~/.mimir/data/perseus-vault.db` (canonical, current name)
513-
/// 2. `~/.mimir/data/mneme.db` (pre-rename)
514-
/// 3. `~/.mimir/data/mimir.db` (pre-rename)
515-
/// 4. `~/mimir.db` (legacy single-user install location)
514+
/// 1. `~/.perseus-vault/data/perseus-vault.db` (canonical, current brand)
515+
/// 2. `~/.mimir/data/perseus-vault.db` (pre-dir-rename, #427)
516+
/// 3. `~/.mimir/data/mneme.db` (pre-rename)
517+
/// 4. `~/.mimir/data/mimir.db` (pre-rename)
518+
/// 5. `~/mimir.db` (legacy single-user install location)
516519
/// If none exist, fall back to creating (1), the canonical path.
517520
///
521+
/// #427 is a *precedence-only* directory rename: fresh installs land in
522+
/// `~/.perseus-vault/`, while any existing `~/.mimir/` install keeps being
523+
/// adopted via the fallback chain — no data is moved. `~/.mimir/` stays in the
524+
/// chain indefinitely so upgraders are never orphaned.
525+
///
518526
/// Crucially `~/mimir.db` is chosen *before* falling through to create a fresh
519527
/// canonical DB, so an existing single-user install is picked up instead of
520528
/// silently starting empty. `other_candidates` reports every *other* database
@@ -535,15 +543,18 @@ fn resolve_default_db(
535543
exists: &dyn Fn(&str) -> bool,
536544
entity_count: &dyn Fn(&str) -> Option<i64>,
537545
) -> DbResolution {
538-
let dir = format!("{}/.mimir/data", home);
539-
let vault_path = format!("{}/perseus-vault.db", dir);
540-
let mneme_path = format!("{}/mneme.db", dir);
541-
let mimir_path = format!("{}/mimir.db", dir);
546+
let new_dir = format!("{}/.perseus-vault/data", home);
547+
let legacy_dir = format!("{}/.mimir/data", home);
548+
let vault_path = format!("{}/perseus-vault.db", new_dir); // #427 canonical
549+
let legacy_vault_path = format!("{}/perseus-vault.db", legacy_dir);
550+
let mneme_path = format!("{}/mneme.db", legacy_dir);
551+
let mimir_path = format!("{}/mimir.db", legacy_dir);
542552
let home_legacy_path = format!("{}/mimir.db", home);
543553

544554
// Ordered candidate list; the first that exists is chosen.
545555
let candidates = [
546556
vault_path.clone(),
557+
legacy_vault_path,
547558
mneme_path,
548559
mimir_path,
549560
home_legacy_path,
@@ -619,16 +630,24 @@ fn probe_entity_count(path: &str) -> Option<i64> {
619630
/// emitted separately by `normalize_default_db`, which runs once at real
620631
/// startup and only when the default path was actually used.
621632
fn default_db_path() -> String {
633+
// #427: PERSEUS_VAULT_DB_PATH is the current-brand override; MIMIR_DB_PATH
634+
// stays honored for back-compat (checked second).
635+
if let Ok(explicit) = std::env::var("PERSEUS_VAULT_DB_PATH") {
636+
return explicit;
637+
}
622638
if let Ok(explicit) = std::env::var("MIMIR_DB_PATH") {
623639
return explicit;
624640
}
625641
let home = std::env::var("HOME")
626642
.or_else(|_| std::env::var("USERPROFILE"))
627643
.unwrap_or_else(|_| {
628-
eprintln!("perseus-vault: could not determine home directory. Set MIMIR_DB_PATH or HOME/USERPROFILE.");
644+
eprintln!("perseus-vault: could not determine home directory. Set PERSEUS_VAULT_DB_PATH or HOME/USERPROFILE.");
629645
std::process::exit(1);
630646
});
631-
let dir = format!("{}/.mimir/data", home);
647+
// Create the current-brand canonical data dir for fresh installs. Existing
648+
// ~/.mimir installs are still adopted by resolve_default_db via the fallback
649+
// chain (this only ever creates an empty dir alongside them).
650+
let dir = format!("{}/.perseus-vault/data", home);
632651
let _ = std::fs::create_dir_all(&dir);
633652

634653
// Path-only here: clap evaluates this eagerly for *every* invocation (even
@@ -652,7 +671,10 @@ fn default_db_path() -> String {
652671
/// rather than only the handful of sites that used to call `check_legacy_db`.
653672
fn normalize_default_db(cli: &mut Cli) {
654673
// Explicit selection (env or top-level `--db`) is never second-guessed.
655-
if std::env::var_os("MIMIR_DB_PATH").is_some() || cli.db.is_some() {
674+
if std::env::var_os("PERSEUS_VAULT_DB_PATH").is_some()
675+
|| std::env::var_os("MIMIR_DB_PATH").is_some()
676+
|| cli.db.is_some()
677+
{
656678
return;
657679
}
658680
let Ok(home) = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")) else {
@@ -698,7 +720,7 @@ fn normalize_default_db(cli: &mut Cli) {
698720
eprintln!("perseus-vault: also present (ignored): {}", other);
699721
}
700722
eprintln!(
701-
"perseus-vault: pass --db <path> or set MIMIR_DB_PATH to choose explicitly and silence this warning."
723+
"perseus-vault: pass --db <path> or set PERSEUS_VAULT_DB_PATH to choose explicitly and silence this warning."
702724
);
703725
}
704726

@@ -717,7 +739,18 @@ fn default_key_file() -> String {
717739
let home = std::env::var("HOME")
718740
.or_else(|_| std::env::var("USERPROFILE"))
719741
.unwrap_or_else(|_| "/root".to_string());
720-
format!("{}/.mimir/secret.key", home)
742+
// #427 precedence-only: prefer whichever secret.key already exists so an
743+
// existing encrypted install NEVER loses its key (a wrong default would
744+
// silently make the vault undecryptable). Fresh installs use the new dir.
745+
let new_key = format!("{}/.perseus-vault/secret.key", home);
746+
let legacy_key = format!("{}/.mimir/secret.key", home);
747+
if std::path::Path::new(&new_key).exists() {
748+
new_key
749+
} else if std::path::Path::new(&legacy_key).exists() {
750+
legacy_key
751+
} else {
752+
new_key
753+
}
721754
}
722755

723756
/// Open a database for a CLI maintenance command, or exit(1) with a message.
@@ -2028,14 +2061,41 @@ mod tests {
20282061

20292062
#[test]
20302063
fn resolve_default_db_falls_back_to_canonical_when_none_exist() {
2031-
// Fresh install: nothing exists -> create canonical path, no warning.
2064+
// Fresh install: nothing exists -> create the #427 canonical path under
2065+
// ~/.perseus-vault/, no warning.
20322066
let home = "/home/tester";
2033-
let vault = format!("{}/.mimir/data/perseus-vault.db", home);
2067+
let vault = format!("{}/.perseus-vault/data/perseus-vault.db", home);
20342068
let r = resolve_default_db(home, &present(&[]), &unknown);
20352069
assert_eq!(r.chosen, vault);
20362070
assert!(r.other_candidates.is_empty());
20372071
}
20382072

2073+
#[test]
2074+
fn resolve_default_db_427_prefers_new_dir_when_present() {
2075+
// Both the new ~/.perseus-vault and a legacy ~/.mimir DB exist: the new
2076+
// canonical dir wins; the legacy one is reported as an also-present.
2077+
let home = "/home/tester";
2078+
let new_vault = format!("{}/.perseus-vault/data/perseus-vault.db", home);
2079+
let legacy_vault = format!("{}/.mimir/data/perseus-vault.db", home);
2080+
let existing = vec![new_vault.clone(), legacy_vault.clone()];
2081+
let r = resolve_default_db(home, &present(&existing), &unknown);
2082+
assert_eq!(r.chosen, new_vault);
2083+
assert_eq!(r.other_candidates, vec![legacy_vault]);
2084+
}
2085+
2086+
#[test]
2087+
fn resolve_default_db_427_adopts_legacy_mimir_dir_on_upgrade() {
2088+
// Upgrade path: only the legacy ~/.mimir DB exists (no ~/.perseus-vault
2089+
// yet). It must be adopted, NOT shadowed by a fresh empty new-dir DB —
2090+
// no data is moved.
2091+
let home = "/home/tester";
2092+
let legacy_vault = format!("{}/.mimir/data/perseus-vault.db", home);
2093+
let existing = vec![legacy_vault.clone()];
2094+
let r = resolve_default_db(home, &present(&existing), &unknown);
2095+
assert_eq!(r.chosen, legacy_vault);
2096+
assert!(r.other_candidates.is_empty());
2097+
}
2098+
20392099
#[test]
20402100
fn resolve_default_db_reports_multiple_candidates() {
20412101
// Multiple candidate DBs -> chosen is highest-precedence, others named

0 commit comments

Comments
 (0)