Skip to main content

treeboot_core/
check.rs

1use std::path::PathBuf;
2
3use serde::Serialize;
4
5use crate::config::RuntimeOptionOverrides;
6use crate::context;
7use crate::{ActionPlan, Config, Error, InitScriptDiscovery, Result, Worktree, WorktreeOptions};
8
9/// Options for checking treeboot bootstrap behavior.
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
11pub struct CheckOptions {
12    /// Directory from which the check starts. Defaults to the process cwd.
13    pub cwd: Option<PathBuf>,
14    /// Overrides the root checkout used as the file-operation source.
15    pub root: Option<PathBuf>,
16    /// Uses one specific config file and skips init script discovery.
17    pub config: Option<PathBuf>,
18    /// Skips init script discovery and uses declarative config discovery.
19    pub no_init_script: bool,
20    /// Fails on missing config and stricter file-operation conflicts.
21    pub strict: bool,
22}
23
24/// Completed action for a `treeboot check` invocation.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26#[serde(tag = "kind", rename_all = "snake_case")]
27pub enum CheckAction {
28    /// No config or executable init script was detected.
29    MissingConfig,
30    /// The check started from the root checkout and had no work to validate.
31    RootWorktreeSkipped,
32    /// An init script would take precedence.
33    InitScript {
34        /// Script path.
35        path: PathBuf,
36    },
37    /// Declarative config was validated.
38    Config {
39        /// Config file path.
40        path: PathBuf,
41    },
42}
43
44/// Result summary for a `treeboot check` invocation.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46pub struct CheckReport {
47    /// Runtime context used by the check.
48    pub context: WorktreeSnapshot,
49    /// Action that was validated.
50    pub action: CheckAction,
51}
52
53/// Serializable worktree context snapshot for reports.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55pub struct WorktreeSnapshot {
56    /// Source checkout used for file operations.
57    pub root_path: PathBuf,
58    /// Current worktree root where targets and commands are anchored.
59    pub worktree_path: PathBuf,
60    /// Best-effort default branch name.
61    pub default_branch: String,
62}
63
64impl From<&Worktree> for WorktreeSnapshot {
65    fn from(context: &Worktree) -> Self {
66        Self {
67            root_path: context.root_path.clone(),
68            worktree_path: context.worktree_path.clone(),
69            default_branch: context.default_branch.clone(),
70        }
71    }
72}
73
74/// Checks treeboot bootstrap behavior without side effects.
75///
76/// # Errors
77///
78/// Returns an error when context discovery fails, strict mode treats the
79/// current state as invalid, config loading fails, or declarative validation
80/// fails.
81pub fn check(options: CheckOptions) -> Result<CheckReport> {
82    let env_options = RuntimeOptionOverrides::from_env()?;
83    let pre_config_strict = env_options.pre_config_strict(options.strict);
84    let context = context::resolve(&WorktreeOptions {
85        cwd: options.cwd.clone(),
86        root: options.root.clone(),
87    })?;
88
89    if context.root_path == context.worktree_path {
90        if pre_config_strict {
91            return Err(Error::RootWorktreeStrict);
92        }
93
94        return Ok(CheckReport {
95            context: WorktreeSnapshot::from(&context),
96            action: CheckAction::RootWorktreeSkipped,
97        });
98    }
99
100    if options.config.is_none() && !options.no_init_script {
101        let scripts = InitScriptDiscovery::discover(&context);
102
103        if let Some(path) = scripts.executable {
104            return Ok(CheckReport {
105                context: WorktreeSnapshot::from(&context),
106                action: CheckAction::InitScript { path },
107            });
108        }
109    }
110
111    match Config::discover_path(&context, options.config.as_deref())? {
112        Some(path) => {
113            let config = Config::load(&path, &context)?;
114            let plan_options = env_options.resolve(&config.options, options.strict);
115            ActionPlan::from_manifest(&path, &config, &context, plan_options.into())?;
116
117            Ok(CheckReport {
118                context: WorktreeSnapshot::from(&context),
119                action: CheckAction::Config { path },
120            })
121        }
122        None => {
123            if pre_config_strict {
124                Err(Error::NoConfigDetectedStrict)
125            } else {
126                Ok(CheckReport {
127                    context: WorktreeSnapshot::from(&context),
128                    action: CheckAction::MissingConfig,
129                })
130            }
131        }
132    }
133}