Skip to main content

run_stack/
workspace.rs

1//! Finding the workspace a command belongs to.
2
3use std::env;
4use std::path::{Path, PathBuf};
5
6use anyhow::{bail, Context, Result};
7
8use crate::config::Config;
9
10#[derive(Debug, Clone)]
11pub struct Workspace {
12    /// The project root: the directory holding .run/.
13    pub root: PathBuf,
14    /// .run/ — config, generated compose overlays, the merged env file.
15    pub run_dir: PathBuf,
16}
17
18impl Workspace {
19    /// The nearest workspace at or above `start`, the way the shell walks up.
20    pub fn find(start: &Path) -> Result<Self> {
21        // Set but empty means "not set": a shell exports it that way when a
22        // caller clears it, and treating "" as a path fails uselessly.
23        if let Some(forced) = env::var("RUN_WORKSPACE_DIR").ok().filter(|v| !v.is_empty()) {
24            let root = PathBuf::from(&forced)
25                .canonicalize()
26                .with_context(|| format!("RUN_WORKSPACE_DIR is not a directory: {forced}"))?;
27            return Ok(Self::at(root));
28        }
29        let mut dir = start
30            .canonicalize()
31            .with_context(|| format!("reading {}", start.display()))?;
32        loop {
33            if dir.join(".run/run.config.json").is_file() || dir.join("run.config.json").is_file() {
34                return Ok(Self::at(dir));
35            }
36            // The layout before .run/: a run/ directory holding the scripts.
37            if dir.join("run/run.sh").is_file() {
38                bail!(
39                    "{} uses the older run/ layout — convert it with `run-stack migrate` \
40                     (the shell version), then this one can read it",
41                    dir.display()
42                );
43            }
44            match dir.parent() {
45                Some(parent) if parent != dir => dir = parent.to_path_buf(),
46                _ => break,
47            }
48        }
49        bail!(
50            "no workspace here — nothing above {} has .run/run.config.json",
51            start.display()
52        )
53    }
54
55    fn at(root: PathBuf) -> Self {
56        let run_dir = root.join(".run");
57        Self { root, run_dir }
58    }
59
60    pub fn config_path(&self) -> PathBuf {
61        let preferred = self.run_dir.join("run.config.json");
62        if preferred.is_file() {
63            return preferred;
64        }
65        // The old layout kept it at the project root.
66        let legacy = self.root.join("run.config.json");
67        if legacy.is_file() {
68            return legacy;
69        }
70        preferred
71    }
72
73    pub fn env_path(&self) -> PathBuf {
74        self.run_dir.join(".env")
75    }
76
77    pub fn config(&self) -> Result<Config> {
78        let path = self.config_path();
79        let mut config = Config::load(&path)?;
80        // Older workspaces have no essential block; fill it in once so
81        // `rst up --essential` and self-update both land on the same shape.
82        if config.ensure_essential() {
83            config.save(&path)?;
84        }
85        Ok(config)
86    }
87}