1use 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 pub root: PathBuf,
14 pub run_dir: PathBuf,
16}
17
18impl Workspace {
19 pub fn find(start: &Path) -> Result<Self> {
21 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 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 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 if config.ensure_essential() {
83 config.save(&path)?;
84 }
85 Ok(config)
86 }
87}