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}
56
57/// Serializable worktree context snapshot for reports.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
59pub struct WorktreeSnapshot {
60    /// Source checkout used for file operations.
61    pub root_path: PathBuf,
62    /// Current worktree root where targets and commands are anchored.
63    pub worktree_path: PathBuf,
64    /// Best-effort default branch name.
65    pub default_branch: String,
66}
67
68impl From<&Worktree> for WorktreeSnapshot {
69    fn from(context: &Worktree) -> Self {
70        Self {
71            root_path: context.root_path.clone(),
72            worktree_path: context.worktree_path.clone(),
73            default_branch: context.default_branch.clone(),
74        }
75    }
76}
77
78/// Checks treeboot bootstrap behavior without side effects.
79///
80/// # Errors
81///
82/// Returns an error when context discovery fails, strict mode treats the
83/// current state as invalid, config loading fails, or declarative validation
84/// fails.
85pub fn check(options: CheckOptions) -> Result<CheckReport> {
86    let runtime_policy = RuntimePolicy::from_environment(&options.environment, options.strict)?;
87    let pre_config_strict = runtime_policy.pre_config_strict();
88    let context = context::resolve(&WorktreeOptions {
89        cwd: options.cwd.clone(),
90        root: options.root.clone(),
91        environment: options.environment.clone(),
92    })?;
93
94    if context.root_path == context.worktree_path {
95        if pre_config_strict {
96            return Err(Error::RootWorktreeStrict);
97        }
98
99        return Ok(CheckReport {
100            context: WorktreeSnapshot::from(&context),
101            action: CheckAction::RootWorktreeSkipped,
102        });
103    }
104
105    if options.config.is_none() && !options.no_init_script {
106        let scripts = InitScriptDiscovery::discover(&context);
107
108        if let Some(path) = scripts.executable {
109            return Ok(CheckReport {
110                context: WorktreeSnapshot::from(&context),
111                action: CheckAction::InitScript { path },
112            });
113        }
114    }
115
116    match Config::discover_path(&context, options.config.as_deref())? {
117        Some(path) => {
118            let config = Config::load(&path, &context)?;
119            let plan_options = runtime_policy.resolve(&config.options);
120            ActionPlan::from_manifest(
121                &path,
122                &config,
123                &context,
124                plan_options.into_action_plan_options(),
125            )?;
126
127            Ok(CheckReport {
128                context: WorktreeSnapshot::from(&context),
129                action: CheckAction::Config { path },
130            })
131        }
132        None => {
133            if pre_config_strict {
134                Err(Error::NoConfigDetectedStrict)
135            } else {
136                Ok(CheckReport {
137                    context: WorktreeSnapshot::from(&context),
138                    action: CheckAction::MissingConfig,
139                })
140            }
141        }
142    }
143}