systemprompt_models/bootstrap/
mod.rs

1//! Bootstrap sequence orchestration.
2//!
3//! This module provides type-safe bootstrap sequencing that enforces
4//! initialization dependencies at compile time. The sequence ensures
5//! that secrets cannot be initialized without a profile, and paths
6//! cannot be initialized without secrets.
7
8use std::marker::PhantomData;
9use std::path::Path;
10
11use anyhow::{Context, Result};
12
13use crate::profile_bootstrap::ProfileBootstrap;
14use crate::secrets::SecretsBootstrap;
15use crate::{AppPaths, Config, PathsConfig};
16
17/// Marker trait for bootstrap states.
18pub trait BootstrapState {}
19
20/// Initial state - nothing initialized.
21#[derive(Debug, Clone, Copy)]
22pub struct Uninitialized;
23impl BootstrapState for Uninitialized {}
24
25/// Profile has been initialized.
26#[derive(Debug, Clone, Copy)]
27pub struct ProfileInitialized;
28impl BootstrapState for ProfileInitialized {}
29
30/// Secrets have been initialized (requires profile).
31#[derive(Debug, Clone, Copy)]
32pub struct SecretsInitialized;
33impl BootstrapState for SecretsInitialized {}
34
35/// Paths have been initialized (requires secrets).
36#[derive(Debug, Clone, Copy)]
37pub struct PathsInitialized;
38impl BootstrapState for PathsInitialized {}
39
40/// Type-safe bootstrap sequence builder.
41///
42/// Uses the type state pattern to ensure initialization happens in the
43/// correct order: Profile -> Secrets -> Paths
44#[derive(Debug)]
45pub struct BootstrapSequence<S: BootstrapState> {
46    _state: PhantomData<S>,
47}
48
49impl Default for BootstrapSequence<Uninitialized> {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl BootstrapSequence<Uninitialized> {
56    /// Creates a new bootstrap sequence.
57    #[must_use]
58    pub const fn new() -> Self {
59        Self {
60            _state: PhantomData,
61        }
62    }
63
64    /// Initializes the profile from a path.
65    ///
66    /// This must be called first before secrets or paths can be initialized.
67    #[allow(clippy::unused_self)]
68    pub fn with_profile(self, path: &Path) -> Result<BootstrapSequence<ProfileInitialized>> {
69        ProfileBootstrap::init_from_path(path)
70            .with_context(|| format!("Profile initialization failed from: {}", path.display()))?;
71
72        Ok(BootstrapSequence {
73            _state: PhantomData,
74        })
75    }
76
77    /// Skips profile initialization (for commands that don't need it).
78    #[must_use]
79    pub const fn skip_profile(self) -> Self {
80        self
81    }
82}
83
84impl BootstrapSequence<ProfileInitialized> {
85    /// Initializes secrets from the loaded profile.
86    ///
87    /// Requires profile to be initialized first.
88    #[allow(clippy::unused_self)]
89    pub fn with_secrets(self) -> Result<BootstrapSequence<SecretsInitialized>> {
90        SecretsBootstrap::init().context("Secrets initialization failed")?;
91
92        Ok(BootstrapSequence {
93            _state: PhantomData,
94        })
95    }
96
97    /// Skips secrets initialization but allows moving forward.
98    ///
99    /// Useful for commands that need profile but not secrets.
100    #[must_use]
101    pub const fn skip_secrets(self) -> Self {
102        self
103    }
104}
105
106impl BootstrapSequence<SecretsInitialized> {
107    /// Initializes application paths from the profile configuration.
108    ///
109    /// Requires secrets to be initialized first.
110    #[allow(clippy::unused_self)]
111    pub fn with_paths(self) -> Result<BootstrapSequence<PathsInitialized>> {
112        let profile = ProfileBootstrap::get()?;
113        AppPaths::init(&profile.paths).context("Failed to initialize paths")?;
114        Config::try_init().context("Failed to initialize configuration")?;
115
116        Ok(BootstrapSequence {
117            _state: PhantomData,
118        })
119    }
120
121    /// Initializes paths with custom configuration.
122    #[allow(clippy::unused_self)]
123    pub fn with_paths_config(
124        self,
125        paths_config: &PathsConfig,
126    ) -> Result<BootstrapSequence<PathsInitialized>> {
127        AppPaths::init(paths_config).context("Failed to initialize paths")?;
128        Config::try_init().context("Failed to initialize configuration")?;
129
130        Ok(BootstrapSequence {
131            _state: PhantomData,
132        })
133    }
134
135    /// Skips paths initialization.
136    #[must_use]
137    pub const fn skip_paths(self) -> Self {
138        self
139    }
140}
141
142impl BootstrapSequence<PathsInitialized> {
143    /// Returns a reference to indicate bootstrap is complete.
144    #[must_use]
145    #[allow(clippy::unused_self)]
146    pub const fn complete(&self) -> BootstrapComplete {
147        BootstrapComplete { _private: () }
148    }
149}
150
151/// Proof that bootstrap completed successfully.
152#[derive(Debug, Clone, Copy)]
153pub struct BootstrapComplete {
154    _private: (),
155}
156
157/// Convenience functions for common bootstrap patterns.
158pub mod presets {
159    use std::path::Path;
160
161    use anyhow::Result;
162
163    use super::{
164        BootstrapComplete, BootstrapSequence, ProfileInitialized, SecretsInitialized, Uninitialized,
165    };
166
167    /// Full bootstrap: profile + secrets + paths.
168    pub fn full(profile_path: &Path) -> Result<BootstrapComplete> {
169        Ok(BootstrapSequence::<Uninitialized>::new()
170            .with_profile(profile_path)?
171            .with_secrets()?
172            .with_paths()?
173            .complete())
174    }
175
176    /// Profile and secrets only (no paths).
177    pub fn profile_and_secrets(
178        profile_path: &Path,
179    ) -> Result<BootstrapSequence<SecretsInitialized>> {
180        BootstrapSequence::<Uninitialized>::new()
181            .with_profile(profile_path)?
182            .with_secrets()
183    }
184
185    /// Profile only.
186    pub fn profile_only(profile_path: &Path) -> Result<BootstrapSequence<ProfileInitialized>> {
187        BootstrapSequence::<Uninitialized>::new().with_profile(profile_path)
188    }
189}