Skip to main content

systemprompt_cli/commands/web/
paths.rs

1//! Web template/asset path resolution under the services root.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::{Context, Result};
7use std::fs;
8use std::path::{Path, PathBuf};
9use systemprompt_config::ProfileBootstrap;
10use systemprompt_loader::ServicesRootBootstrap;
11use systemprompt_models::Profile;
12use systemprompt_models::validators::WebConfigRaw;
13
14const DEFAULT_TEMPLATES_PATH: &str = "web/templates";
15const DEFAULT_ASSETS_PATH: &str = "web/assets";
16
17#[derive(Debug)]
18pub struct WebPaths {
19    pub templates: PathBuf,
20    pub assets: PathBuf,
21}
22
23impl WebPaths {
24    pub fn resolve() -> Result<Self> {
25        let profile = ProfileBootstrap::get().context("Failed to get profile")?;
26        Self::resolve_from_profile(profile)
27    }
28
29    pub fn resolve_from_profile(profile: &Profile) -> Result<Self> {
30        let services_root = ServicesRootBootstrap::active_root_or(&profile.paths.services);
31        let web_config_path = services_root.join("web/config.yaml");
32
33        let (templates_path, assets_path) = match fs::read_to_string(&web_config_path) {
34            Ok(content) => {
35                let web_config: WebConfigRaw =
36                    serde_yaml::from_str(&content).with_context(|| {
37                        format!(
38                            "Failed to parse web config at {}",
39                            web_config_path.display()
40                        )
41                    })?;
42
43                match web_config.paths {
44                    Some(paths) => (
45                        paths
46                            .templates
47                            .unwrap_or_else(|| DEFAULT_TEMPLATES_PATH.to_owned()),
48                        paths
49                            .assets
50                            .unwrap_or_else(|| DEFAULT_ASSETS_PATH.to_owned()),
51                    ),
52                    None => (
53                        DEFAULT_TEMPLATES_PATH.to_owned(),
54                        DEFAULT_ASSETS_PATH.to_owned(),
55                    ),
56                }
57            },
58            Err(e) if e.kind() == std::io::ErrorKind::NotFound => (
59                DEFAULT_TEMPLATES_PATH.to_owned(),
60                DEFAULT_ASSETS_PATH.to_owned(),
61            ),
62            Err(e) => {
63                return Err(e).with_context(|| {
64                    format!("Failed to read web config at {}", web_config_path.display())
65                });
66            },
67        };
68
69        let base = services_root.as_path();
70        let templates = normalize_under_services(&templates_path, base);
71        let assets = normalize_under_services(&assets_path, base);
72
73        Ok(Self { templates, assets })
74    }
75}
76
77fn normalize_under_services(raw: &str, base: &Path) -> PathBuf {
78    let candidate = Path::new(raw);
79    if candidate.is_absolute() {
80        return candidate.to_path_buf();
81    }
82    let stripped = candidate.strip_prefix("services").unwrap_or(candidate);
83    base.join(stripped)
84}