Skip to main content

treeboot_core/
check.rs

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