Skip to main content

systemprompt_cli/
context.rs

1//! Per-invocation command context.
2//!
3//! [`CommandContext`] is the single dependency handed to command handlers: the
4//! resolved [`CliConfig`], the [`EnvOverrides`] snapshot, the active
5//! [`Prompter`], and lazy access to the runtime. The runtime is resolved on
6//! first use — `--database-url` invocations carry a [`DatabaseContext`] and
7//! refuse to boot the full [`AppContext`]; profile-driven invocations boot the
8//! [`AppContext`] once and share it across the command. Tests construct the
9//! context with [`CommandContext::with_app_context`] or
10//! [`CommandContext::with_database`] and a scripted prompter, so no handler
11//! needs process-global state.
12
13use std::sync::Arc;
14
15use anyhow::{Context, Result, bail};
16use systemprompt_database::DbPool;
17use systemprompt_runtime::{AppContext, DatabaseContext};
18use tokio::sync::OnceCell;
19
20use crate::cli_settings::CliConfig;
21use crate::env_overrides::EnvOverrides;
22use crate::interactive::{DialoguerPrompter, Prompter};
23
24pub struct CommandContext {
25    pub cli: CliConfig,
26    pub env: EnvOverrides,
27    prompter: Box<dyn Prompter>,
28    runtime: OnceCell<Arc<AppContext>>,
29    db: Option<DatabaseContext>,
30    database_url: Option<String>,
31}
32
33impl CommandContext {
34    #[must_use]
35    pub fn new(cli: CliConfig, env: EnvOverrides) -> Self {
36        Self {
37            cli,
38            env,
39            prompter: Box::new(DialoguerPrompter),
40            runtime: OnceCell::new(),
41            db: None,
42            database_url: None,
43        }
44    }
45
46    #[must_use]
47    pub fn with_database(
48        cli: CliConfig,
49        env: EnvOverrides,
50        db: DatabaseContext,
51        database_url: String,
52    ) -> Self {
53        Self {
54            cli,
55            env,
56            prompter: Box::new(DialoguerPrompter),
57            runtime: OnceCell::new(),
58            db: Some(db),
59            database_url: Some(database_url),
60        }
61    }
62
63    #[must_use]
64    pub fn with_app_context(cli: CliConfig, env: EnvOverrides, app: Arc<AppContext>) -> Self {
65        Self {
66            cli,
67            env,
68            prompter: Box::new(DialoguerPrompter),
69            runtime: OnceCell::new_with(Some(app)),
70            db: None,
71            database_url: None,
72        }
73    }
74
75    #[must_use]
76    pub fn with_prompter(mut self, prompter: Box<dyn Prompter>) -> Self {
77        self.prompter = prompter;
78        self
79    }
80
81    #[must_use]
82    pub fn prompter(&self) -> &dyn Prompter {
83        self.prompter.as_ref()
84    }
85
86    #[must_use]
87    pub const fn is_database_scoped(&self) -> bool {
88        self.db.is_some()
89    }
90
91    #[must_use]
92    pub fn database_url(&self) -> Option<&str> {
93        self.database_url.as_deref()
94    }
95
96    #[must_use]
97    pub const fn database_context(&self) -> Option<&DatabaseContext> {
98        self.db.as_ref()
99    }
100
101    pub async fn app_context(&self) -> Result<&Arc<AppContext>> {
102        if self.db.is_some() {
103            bail!("This command requires full profile initialization. Remove --database-url flag.");
104        }
105        self.runtime
106            .get_or_try_init(|| async { AppContext::new().await.map(Arc::new) })
107            .await
108            .context("Failed to initialize application context")
109    }
110
111    pub async fn db_pool(&self) -> Result<DbPool> {
112        if let Some(db) = &self.db {
113            return Ok(db.db_pool_arc());
114        }
115        Ok(Arc::clone(self.app_context().await?.db_pool()))
116    }
117
118    pub async fn database(&self) -> Result<DatabaseContext> {
119        if let Some(db) = &self.db {
120            return Ok(db.clone());
121        }
122        Ok(DatabaseContext::from_pool(Arc::clone(
123            self.app_context().await?.db_pool(),
124        )))
125    }
126}
127
128impl std::fmt::Debug for CommandContext {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("CommandContext")
131            .field("cli", &self.cli)
132            .field("env", &self.env)
133            .field("database_scoped", &self.db.is_some())
134            .finish_non_exhaustive()
135    }
136}