Skip to main content

nika_cli/machine/
status.rs

1//! Machine status checks and marker file management.
2
3use std::path::PathBuf;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6/// Result of a single setup action.
7#[derive(Debug)]
8pub struct SetupResult {
9    pub name: String,
10    pub success: bool,
11    #[allow(dead_code)]
12    pub message: String,
13}
14
15/// Distinguishes "never set up" from "version mismatch" from "ready".
16#[derive(Debug, PartialEq)]
17pub enum MachineStatus {
18    /// Never set up (no marker file)
19    NeverSetup,
20    /// Set up but version doesn't match (user upgraded)
21    NeedsUpdate,
22    /// Fully set up and current
23    Ready,
24}
25
26/// Return the machine setup status: NeverSetup, NeedsUpdate, or Ready.
27pub fn machine_setup_status() -> MachineStatus {
28    let marker = machine_toml_path();
29    let content = match std::fs::read_to_string(&marker) {
30        Ok(c) => c,
31        Err(_) => return MachineStatus::NeverSetup,
32    };
33    for line in content.lines() {
34        let trimmed = line.trim();
35        if let Some(rest) = trimmed.strip_prefix("version") {
36            let rest = rest.trim().strip_prefix('=').unwrap_or("").trim();
37            let version = rest.trim_matches('"');
38            if version == env!("CARGO_PKG_VERSION") {
39                return MachineStatus::Ready;
40            } else {
41                return MachineStatus::NeedsUpdate;
42            }
43        }
44    }
45    MachineStatus::NeedsUpdate
46}
47
48/// Check if machine setup has been done (marker file exists + version current).
49pub fn is_machine_setup() -> bool {
50    machine_setup_status() == MachineStatus::Ready
51}
52
53/// Returns true in CI/CD or automated environments where machine setup must not run.
54pub fn is_ci() -> bool {
55    use std::env;
56    if env::var("NIKA_NO_SETUP").map(|v| v == "1").unwrap_or(false) {
57        return true;
58    }
59    if env::var("CI").is_ok() {
60        return true;
61    }
62    let ci_vars = [
63        "GITHUB_ACTIONS",
64        "GITLAB_CI",
65        "CIRCLECI",
66        "JENKINS_URL",
67        "BUILDKITE",
68        "TRAVIS",
69        "CODEBUILD_BUILD_ID",
70        "TF_BUILD",
71    ];
72    if ci_vars.iter().any(|v| env::var(v).is_ok()) {
73        return true;
74    }
75    if dirs::home_dir().is_none() {
76        return true;
77    }
78    #[cfg(not(target_os = "windows"))]
79    {
80        let dumb = env::var("TERM").map(|t| t == "dumb").unwrap_or(false);
81        let no_display = env::var("DISPLAY").is_err() && env::var("WAYLAND_DISPLAY").is_err();
82        if dumb && no_display {
83            return true;
84        }
85    }
86    false
87}
88
89/// Path to the machine marker file.
90pub(crate) fn machine_toml_path() -> PathBuf {
91    dirs::home_dir()
92        .unwrap_or_else(|| PathBuf::from("/tmp"))
93        .join(".nika")
94        .join("machine.toml")
95}
96
97/// Write the machine.toml marker file after setup completes.
98pub(super) fn write_marker(results: &[SetupResult]) {
99    let editors = super::install::detect_editors();
100    write_marker_with_editors(results, &editors);
101}
102
103fn write_marker_with_editors(results: &[SetupResult], editors: &[&str]) {
104    let marker_path = machine_toml_path();
105    let Some(dir) = marker_path.parent() else {
106        return;
107    };
108    std::fs::create_dir_all(dir).ok();
109
110    let ai_tools: Vec<&str> = results
111        .iter()
112        .filter(|r| {
113            r.success
114                && [
115                    "Claude Code",
116                    "Cursor",
117                    "Copilot",
118                    "Windsurf",
119                    "Roo Code",
120                    "Agent Skills",
121                ]
122                .contains(&r.name.as_str())
123        })
124        .map(|r| r.name.as_str())
125        .collect();
126
127    let secs = SystemTime::now()
128        .duration_since(UNIX_EPOCH)
129        .unwrap_or_default()
130        .as_secs();
131
132    // Format editors as TOML array: editors = ["vscode", "claude", "cursor"]
133    let editors_toml: Vec<String> = editors.iter().map(|e| format!("\"{}\"", e)).collect();
134
135    let content = format!(
136        "[machine]\nsetup_at = \"{}\"\nversion = \"{}\"\neditors = [{}]\nai_tools = {:?}\n",
137        secs,
138        env!("CARGO_PKG_VERSION"),
139        editors_toml.join(", "),
140        ai_tools,
141    );
142
143    // Don't fail init if marker write fails
144    std::fs::write(&marker_path, content).ok();
145}