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