Skip to main content

mesh_llm_analytics/
install_id.rs

1//! The anonymous install identifier.
2//!
3//! This is a random v4 UUID with no derivation from anything about the
4//! machine: not the hostname, not a MAC address, not a disk serial. It is
5//! deliberately **not** the mesh node identity from `mesh-llm-identity` —
6//! that key is published to the public mesh, and reusing it would tie every
7//! analytics event to a publicly visible node. Deleting the file resets the
8//! identifier, and the user is told so in `mesh-llm analytics status`.
9
10use anyhow::{Context, Result};
11use std::fs;
12use std::path::{Path, PathBuf};
13use uuid::Uuid;
14
15/// File name inside the mesh-llm state directory.
16pub const INSTALL_ID_FILE: &str = "analytics-id";
17
18/// An install identifier, and whether this process created it.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct InstallId {
21    id: String,
22    first_run: bool,
23}
24
25impl InstallId {
26    #[must_use]
27    pub fn as_str(&self) -> &str {
28        &self.id
29    }
30
31    /// True when this process generated the identifier, i.e. a first run.
32    #[must_use]
33    pub const fn is_first_run(&self) -> bool {
34        self.first_run
35    }
36}
37
38/// The default state directory, `~/.mesh-llm`.
39pub fn state_dir() -> Result<PathBuf> {
40    let home = dirs::home_dir().context("cannot determine home directory")?;
41    Ok(home.join(".mesh-llm"))
42}
43
44/// Read the install identifier without creating one.
45///
46/// `mesh-llm analytics status` uses this: asking what is collected must not
47/// itself write state, and creating the identifier here would consume the
48/// first-run signal so the real first run never reports as one.
49pub fn load(dir: &Path) -> Option<InstallId> {
50    let raw = fs::read_to_string(dir.join(INSTALL_ID_FILE)).ok()?;
51    let trimmed = raw.trim();
52    Uuid::parse_str(trimmed).ok()?;
53    Some(InstallId {
54        id: trimmed.to_owned(),
55        first_run: false,
56    })
57}
58
59/// Load the install identifier from `dir`, generating it on first use.
60///
61/// A malformed or empty file is replaced rather than treated as an error: a
62/// corrupted identifier should not be able to break command dispatch.
63pub fn load_or_create(dir: &Path) -> Result<InstallId> {
64    let path = dir.join(INSTALL_ID_FILE);
65
66    if let Ok(existing) = fs::read_to_string(&path) {
67        let trimmed = existing.trim();
68        if Uuid::parse_str(trimmed).is_ok() {
69            return Ok(InstallId {
70                id: trimmed.to_owned(),
71                first_run: false,
72            });
73        }
74    }
75
76    fs::create_dir_all(dir)
77        .with_context(|| format!("failed to create state directory {}", dir.display()))?;
78    let id = Uuid::new_v4().to_string();
79    fs::write(&path, format!("{id}\n"))
80        .with_context(|| format!("failed to write install identifier {}", path.display()))?;
81    restrict_permissions(&path);
82
83    Ok(InstallId {
84        id,
85        first_run: true,
86    })
87}
88
89/// Best-effort owner-only permissions. A failure here is not worth failing on.
90#[cfg(unix)]
91fn restrict_permissions(path: &Path) {
92    use std::os::unix::fs::PermissionsExt;
93    let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o600));
94}
95
96#[cfg(not(unix))]
97fn restrict_permissions(_path: &Path) {}
98
99#[cfg(test)]
100#[path = "install_id/tests.rs"]
101mod tests;