Skip to main content

treeboot_core/
run.rs

1use std::path::PathBuf;
2use std::process::Command;
3
4use crate::context;
5use crate::{
6    ActionPlan, Config, EnvironmentInput, Error, ExecuteOptions, Executor, InitScriptDiscovery,
7    OutputEvent, Reporter, Result, RuntimePolicy, Worktree, WorktreeOptions,
8};
9
10/// Options for running worktree bootstrap.
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct RunOptions {
13    /// Directory from which the run starts. Defaults to the process cwd.
14    pub cwd: Option<PathBuf>,
15    /// Overrides the root checkout used as the file-operation source.
16    pub root: Option<PathBuf>,
17    /// Explicit environment input used for compatibility discovery and options.
18    pub environment: EnvironmentInput,
19    /// Uses one specific config file and skips init script discovery.
20    pub config: Option<PathBuf>,
21    /// Skips init script discovery and uses declarative config discovery.
22    pub no_init_script: bool,
23    /// Fails on missing config and stricter file-operation conflicts.
24    pub strict: bool,
25    /// Replaces existing file-operation targets where supported.
26    pub force: bool,
27    /// Prints planned work without changing files or running commands.
28    pub dry_run: bool,
29    /// Prints detailed file-operation actions instead of compact summaries.
30    pub verbose: bool,
31    /// Runs file operations only.
32    pub skip_commands: bool,
33}
34
35/// Completed action for a `treeboot run` invocation.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum RunAction {
38    /// No config or executable init script was detected.
39    MissingConfig,
40    /// The run started from the root checkout and had no work to do.
41    RootWorktreeSkipped,
42    /// An init script would run in dry-run mode.
43    WouldRunInitScript {
44        /// Script path.
45        path: PathBuf,
46    },
47    /// An init script was executed.
48    RanInitScript {
49        /// Script path.
50        path: PathBuf,
51    },
52    /// A declarative config was detected.
53    ConfigDetected {
54        /// Config file path.
55        path: PathBuf,
56    },
57    /// Declarative config file operations were applied.
58    ConfigApplied {
59        /// Config file path.
60        path: PathBuf,
61    },
62}
63
64/// Result summary for a `treeboot run` invocation.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct RunReport {
67    /// Runtime context used by the run.
68    pub context: Worktree,
69    /// Action taken by the run flow.
70    pub action: RunAction,
71}
72
73/// Runs worktree bootstrap according to the provided options.
74///
75/// Resolves the worktree context, discovers executable init scripts unless
76/// disabled, discovers declarative config files, reports the selected action,
77/// and executes an init script when one should run.
78///
79/// # Errors
80///
81/// Returns an error if context discovery fails, output reporting fails, an init
82/// script cannot be started or exits unsuccessfully, a configured file cannot
83/// be read, or strict mode treats a missing config as a failure.
84pub fn run(options: RunOptions, reporter: &mut dyn Reporter) -> Result<RunReport> {
85    let runtime_policy = RuntimePolicy::from_environment(&options.environment, options.strict)?;
86    let pre_config_strict = runtime_policy.pre_config_strict();
87    let context = context::resolve(&WorktreeOptions {
88        cwd: options.cwd.clone(),
89        root: options.root.clone(),
90        environment: options.environment.clone(),
91    })?;
92
93    if context.root_path == context.worktree_path {
94        report(reporter, OutputEvent::RootWorktreeDetected)?;
95
96        if pre_config_strict {
97            return Err(Error::RootWorktreeStrict);
98        }
99
100        return Ok(RunReport {
101            context,
102            action: RunAction::RootWorktreeSkipped,
103        });
104    }
105
106    if options.config.is_none() && !options.no_init_script {
107        let scripts = InitScriptDiscovery::discover(&context);
108
109        for ignored in scripts.ignored {
110            report(
111                reporter,
112                OutputEvent::IgnoredInitScript { path: ignored.path },
113            )?;
114        }
115
116        if let Some(path) = scripts.executable {
117            return run_init_script(path, context, &options, reporter);
118        }
119    }
120
121    match Config::discover_path(&context, options.config.as_deref())? {
122        Some(path) => {
123            report(reporter, OutputEvent::ConfigDetected { path: path.clone() })?;
124            let config = Config::load(&path, &context)?;
125            let plan_options = runtime_policy.resolve(&config.options);
126            let strict = plan_options.strict();
127            let plan = ActionPlan::from_manifest(
128                &path,
129                &config,
130                &context,
131                plan_options.into_action_plan_options(),
132            )?;
133            Executor::new(ExecuteOptions {
134                strict,
135                force: options.force,
136                dry_run: options.dry_run,
137                verbose: options.verbose,
138                skip_commands: options.skip_commands,
139            })
140            .execute(&plan, reporter)?;
141
142            Ok(RunReport {
143                context,
144                action: RunAction::ConfigApplied { path },
145            })
146        }
147        None => {
148            report(reporter, OutputEvent::NoConfigDetected)?;
149
150            if pre_config_strict {
151                Err(Error::NoConfigDetectedStrict)
152            } else {
153                Ok(RunReport {
154                    context,
155                    action: RunAction::MissingConfig,
156                })
157            }
158        }
159    }
160}
161
162fn run_init_script(
163    path: PathBuf,
164    context: Worktree,
165    options: &RunOptions,
166    reporter: &mut dyn Reporter,
167) -> Result<RunReport> {
168    if options.dry_run {
169        report(
170            reporter,
171            OutputEvent::WouldRunInitScript {
172                path: path.clone(),
173                root_path: context.root_path.clone(),
174            },
175        )?;
176
177        return Ok(RunReport {
178            context,
179            action: RunAction::WouldRunInitScript { path },
180        });
181    }
182
183    report(reporter, OutputEvent::RunInitScript { path: path.clone() })?;
184
185    let status = Command::new(&path)
186        .arg(&context.root_path)
187        .current_dir(&context.worktree_path)
188        .envs(&context.environment)
189        .status()
190        .map_err(|source| Error::ScriptIo {
191            path: path.clone(),
192            source,
193        })?;
194
195    if !status.success() {
196        return Err(Error::ScriptFailed { path, status });
197    }
198
199    Ok(RunReport {
200        context,
201        action: RunAction::RanInitScript { path },
202    })
203}
204
205fn report(reporter: &mut dyn Reporter, event: OutputEvent) -> Result<()> {
206    reporter
207        .report(event)
208        .map_err(|source| Error::Output { source })
209}