Skip to main content

linkmarks_cli/cmd/
init.rs

1//! `linkmarks init` — initialize the LinkMarks store and config.
2//!
3//! What it does:
4//! 1. Creates the XDG data directory (`~/.local/share/linkmarks/`).
5//! 2. Creates the XDG config directory (`~/.config/linkmarks/`).
6//! 3. Writes a default `config.toml` if none exists.
7//! 4. Opens the store (which runs the migrator and stamps
8//!    `PRAGMA user_version`).
9//!
10//! Idempotent: re-running is a no-op when both dirs already exist and
11//! `config.toml` is present.
12
13use anyhow::Result;
14use clap::Args;
15use linkmarks_core::config as core_config;
16use linkmarks_core::paths;
17use linkmarks_core::store;
18
19#[derive(Args, Debug)]
20pub struct InitArgs {
21    /// Override the data directory (defaults to XDG).
22    #[arg(long)]
23    pub data_dir: Option<std::path::PathBuf>,
24
25    /// Override the config directory (defaults to XDG).
26    #[arg(long)]
27    pub config_dir: Option<std::path::PathBuf>,
28
29    /// Overwrite an existing config file with the bundled defaults.
30    /// Off by default; the operator decides when to discard their
31    /// hand-written rules.
32    #[arg(long)]
33    pub force: bool,
34}
35
36pub fn run(args: InitArgs, _format: crate::Format, paths: crate::Paths) -> Result<i32> {
37    // The CLI resolves --store / --config (or LINKMARKS_STORE / LINKMARKS_CONFIG)
38    // into concrete file paths. We derive the directory from the file's parent
39    // so init honors the same env vars every other subcommand uses.
40    let store_path = paths.store.clone();
41    let cfg_path = paths.config.clone();
42
43    let data_dir = args.data_dir.clone().unwrap_or_else(|| {
44        store_path
45            .parent()
46            .map(|p| p.to_path_buf())
47            .unwrap_or_else(paths::linkmarks_data_dir)
48    });
49    let config_dir = args.config_dir.clone().unwrap_or_else(|| {
50        cfg_path
51            .parent()
52            .map(|p| p.to_path_buf())
53            .unwrap_or_else(paths::linkmarks_config_dir)
54    });
55
56    // 1+2. Create directories.
57    std::fs::create_dir_all(&data_dir)
58        .map_err(|e| anyhow::anyhow!("create data dir {}: {e}", data_dir.display()))?;
59    std::fs::create_dir_all(&config_dir)
60        .map_err(|e| anyhow::anyhow!("create config dir {}: {e}", config_dir.display()))?;
61
62    // 3. Default config (skip when the file exists, unless --force).
63    let wrote_config = if args.force || !cfg_path.exists() {
64        if args.force && cfg_path.exists() {
65            // Best-effort rename to a `.bak` so the operator can recover.
66            let backup = cfg_path.with_extension("toml.bak");
67            let _ = std::fs::rename(&cfg_path, &backup);
68        }
69        std::fs::write(&cfg_path, core_config::DEFAULT_CONFIG_BODY)
70            .map_err(|e| anyhow::anyhow!("write default config: {e}"))?;
71        true
72    } else {
73        false
74    };
75
76    // 4. Open the store (runs the migrator).
77    let _store = store::open(&store_path)
78        .map_err(|e| anyhow::anyhow!("open store {}: {e}", store_path.display()))?;
79    let _cfg = core_config::load_from(&cfg_path)
80        .map_err(|e| anyhow::anyhow!("parse config {}: {e}", cfg_path.display()))?;
81
82    println!(
83        "data_dir={}\nconfig_dir={}\nstore={}\nconfig_file={}\nconfig_written={}",
84        data_dir.display(),
85        config_dir.display(),
86        store_path.display(),
87        cfg_path.display(),
88        wrote_config,
89    );
90    Ok(crate::exit_codes::OK)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use tempfile::tempdir;
97
98    #[test]
99    fn default_args_parse() {
100        let _ = InitArgs {
101            data_dir: None,
102            config_dir: None,
103            force: false,
104        };
105    }
106
107    #[test]
108    fn store_opens_against_arbitrary_data_dir() {
109        // Smoke check: `store::open` succeeds against a fresh directory.
110        let dir = tempdir().unwrap();
111        let s = store::open(&dir.path().join("store.db")).unwrap();
112        assert_eq!(s.count().unwrap(), 0);
113    }
114}