Skip to main content

nexus_core/
space.rs

1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4
5use crate::config;
6use crate::db::DEFAULT_SPACE;
7
8/// Resolves the shared data directory (holding `nexus.db`) and per-space
9/// directories (`spaces/<name>/`, each with `memory.md` + `instructions.md`).
10pub struct Space {
11    pub root: PathBuf,
12}
13
14impl Space {
15    /// Resolve the data dir, running the one-time layout migration from the old
16    /// "single global space" layout (`spaces/global/nexus.db`) if present.
17    pub fn open() -> Result<Self> {
18        let root = config::project_dirs()?.data_dir().to_path_buf();
19        let space = Self { root };
20        space.migrate_legacy_layout()?;
21        std::fs::create_dir_all(space.root.join("spaces"))
22            .with_context(|| format!("creating {}", space.root.join("spaces").display()))?;
23        Ok(space)
24    }
25
26    /// Old layout: db and the sole space both lived under `spaces/global/`.
27    /// Move the db up to the shared root and rename the space dir to `default`.
28    fn migrate_legacy_layout(&self) -> Result<()> {
29        let legacy_dir = self.root.join("spaces").join("global");
30        let legacy_db = legacy_dir.join("nexus.db");
31        let new_db = self.root.join("nexus.db");
32        if legacy_db.exists() && !new_db.exists() {
33            std::fs::create_dir_all(&self.root)
34                .with_context(|| format!("creating {}", self.root.display()))?;
35            std::fs::rename(&legacy_db, &new_db).with_context(|| {
36                format!("moving {} to {}", legacy_db.display(), new_db.display())
37            })?;
38        }
39        let default_dir = self.root.join("spaces").join(DEFAULT_SPACE);
40        if legacy_dir.exists() && !default_dir.exists() {
41            std::fs::rename(&legacy_dir, &default_dir).with_context(|| {
42                format!(
43                    "moving {} to {}",
44                    legacy_dir.display(),
45                    default_dir.display()
46                )
47            })?;
48        }
49        Ok(())
50    }
51
52    pub fn db_path(&self) -> PathBuf {
53        self.root.join("nexus.db")
54    }
55
56    /// Ensure `spaces/<name>/` exists, for a space just created in the db.
57    pub fn ensure_space_dir(&self, name: &str) -> Result<()> {
58        let dir = self.space_dir(name);
59        std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))
60    }
61
62    /// `spaces/<name>/` — crate-internal (the sync engine renames space
63    /// dirs when a rename syncs across).
64    pub(crate) fn space_dir(&self, name: &str) -> PathBuf {
65        self.root.join("spaces").join(name)
66    }
67
68    pub fn memory_path(&self, name: &str) -> PathBuf {
69        self.space_dir(name).join("memory.md")
70    }
71
72    pub fn instructions_path(&self, name: &str) -> PathBuf {
73        self.space_dir(name).join("instructions.md")
74    }
75
76    /// Comma-separated domains `search(mode=web)` always excludes in this space.
77    pub fn blocked_domains_path(&self, name: &str) -> PathBuf {
78        self.space_dir(name).join("blocked_domains.txt")
79    }
80
81    /// Directory holding a space's imported fileset (created on demand).
82    pub fn files_dir(&self, name: &str) -> PathBuf {
83        self.space_dir(name).join("files")
84    }
85
86    /// Directory holding a space's model-created apps (created on demand).
87    pub fn apps_dir(&self, name: &str) -> PathBuf {
88        self.space_dir(name).join("apps")
89    }
90
91    /// Directory holding a space's reusable scripts.
92    pub fn scripts_dir(&self, name: &str) -> PathBuf {
93        self.space_dir(name).join("scripts")
94    }
95
96    /// Root of all spaces — what the app server serves from.
97    pub fn spaces_root(&self) -> PathBuf {
98        self.root.join("spaces")
99    }
100
101    /// Rename a space's directory (its db row is renamed separately).
102    pub fn rename_space_dir(&self, old: &str, new: &str) -> Result<()> {
103        let from = self.space_dir(old);
104        let to = self.space_dir(new);
105        if from.exists() {
106            std::fs::rename(&from, &to)
107                .with_context(|| format!("renaming {} to {}", from.display(), to.display()))?;
108        }
109        Ok(())
110    }
111
112    /// Remove a space's directory (memory + instructions gone with it).
113    pub fn remove_space_dir(&self, name: &str) -> Result<()> {
114        let dir = self.space_dir(name);
115        if dir.exists() {
116            std::fs::remove_dir_all(&dir).with_context(|| format!("removing {}", dir.display()))?;
117        }
118        Ok(())
119    }
120}