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//!
13//! Copyright (c) systemprompt.io — Business Source License 1.1.
14//! See <https://systemprompt.io> for licensing details.
15
16use std::sync::Arc;
17
18use anyhow::{Context, Result, bail};
19use systemprompt_database::DbPool;
20use systemprompt_runtime::{AppContext, DatabaseContext};
21use tokio::sync::OnceCell;
22
23use crate::cli_settings::CliConfig;
24use crate::env_overrides::EnvOverrides;
25use crate::interactive::{DialoguerPrompter, Prompter};
26
27pub struct CommandContext {
28    pub cli: CliConfig,
29    pub env: EnvOverrides,
30    prompter: Box<dyn Prompter>,
31    runtime: OnceCell<Arc<AppContext>>,
32    db: Option<DatabaseContext>,
33    database_url: Option<String>,
34}
35
36impl CommandContext {
37    #[must_use]
38    pub fn new(cli: CliConfig, env: EnvOverrides) -> Self {
39        Self {
40            cli,
41            env,
42            prompter: Box::new(DialoguerPrompter),
43            runtime: OnceCell::new(),
44            db: None,
45            database_url: None,
46        }
47    }
48
49    #[must_use]
50    pub fn with_database(
51        cli: CliConfig,
52        env: EnvOverrides,
53        db: DatabaseContext,
54        database_url: String,
55    ) -> Self {
56        Self {
57            cli,
58            env,
59            prompter: Box::new(DialoguerPrompter),
60            runtime: OnceCell::new(),
61            db: Some(db),
62            database_url: Some(database_url),
63        }
64    }
65
66    #[must_use]
67    pub fn with_app_context(cli: CliConfig, env: EnvOverrides, app: Arc<AppContext>) -> Self {
68        Self {
69            cli,
70            env,
71            prompter: Box::new(DialoguerPrompter),
72            runtime: OnceCell::new_with(Some(app)),
73            db: None,
74            database_url: None,
75        }
76    }
77
78    #[must_use]
79    pub fn with_prompter(mut self, prompter: Box<dyn Prompter>) -> Self {
80        self.prompter = prompter;
81        self
82    }
83
84    #[must_use]
85    pub fn prompter(&self) -> &dyn Prompter {
86        self.prompter.as_ref()
87    }
88
89    #[must_use]
90    pub const fn is_database_scoped(&self) -> bool {
91        self.db.is_some()
92    }
93
94    #[must_use]
95    pub fn database_url(&self) -> Option<&str> {
96        self.database_url.as_deref()
97    }
98
99    #[must_use]
100    pub const fn database_context(&self) -> Option<&DatabaseContext> {
101        self.db.as_ref()
102    }
103
104    pub async fn app_context(&self) -> Result<&Arc<AppContext>> {
105        if self.db.is_some() {
106            bail!("This command requires full profile initialization. Remove --database-url flag.");
107        }
108        self.runtime
109            .get_or_try_init(|| async { AppContext::new().await.map(Arc::new) })
110            .await
111            .context("Failed to initialize application context")
112    }
113
114    pub async fn db_pool(&self) -> Result<DbPool> {
115        if let Some(db) = &self.db {
116            return Ok(db.db_pool_arc());
117        }
118        Ok(Arc::clone(self.app_context().await?.db_pool()))
119    }
120
121    pub async fn database(&self) -> Result<DatabaseContext> {
122        if let Some(db) = &self.db {
123            return Ok(db.clone());
124        }
125        Ok(DatabaseContext::from_pool(Arc::clone(
126            self.app_context().await?.db_pool(),
127        )))
128    }
129}
130
131impl std::fmt::Debug for CommandContext {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        f.debug_struct("CommandContext")
134            .field("cli", &self.cli)
135            .field("env", &self.env)
136            .field("database_scoped", &self.db.is_some())
137            .finish_non_exhaustive()
138    }
139}