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_models::Profile;
11use systemprompt_models::validators::WebConfigRaw;
12
13const DEFAULT_TEMPLATES_PATH: &str = "web/templates";
14const DEFAULT_ASSETS_PATH: &str = "web/assets";
15
16#[derive(Debug)]
17pub struct WebPaths {
18    pub templates: PathBuf,
19    pub assets: PathBuf,
20}
21
22impl WebPaths {
23    pub fn resolve() -> Result<Self> {
24        let profile = ProfileBootstrap::get().context("Failed to get profile")?;
25        Self::resolve_from_profile(profile)
26    }
27
28    pub fn resolve_from_profile(profile: &Profile) -> Result<Self> {
29        let web_config_path = profile.paths.web_config();
30        let services_path = &profile.paths.services;
31
32        let (templates_path, assets_path) = match fs::read_to_string(&web_config_path) {
33            Ok(content) => {
34                let web_config: WebConfigRaw =
35                    serde_yaml::from_str(&content).with_context(|| {
36                        format!("Failed to parse web config at {}", web_config_path)
37                    })?;
38
39                match web_config.paths {
40                    Some(paths) => (
41                        paths
42                            .templates
43                            .unwrap_or_else(|| DEFAULT_TEMPLATES_PATH.to_owned()),
44                        paths
45                            .assets
46                            .unwrap_or_else(|| DEFAULT_ASSETS_PATH.to_owned()),
47                    ),
48                    None => (
49                        DEFAULT_TEMPLATES_PATH.to_owned(),
50                        DEFAULT_ASSETS_PATH.to_owned(),
51                    ),
52                }
53            },
54            Err(e) if e.kind() == std::io::ErrorKind::NotFound => (
55                DEFAULT_TEMPLATES_PATH.to_owned(),
56                DEFAULT_ASSETS_PATH.to_owned(),
57            ),
58            Err(e) => {
59                return Err(e)
60                    .with_context(|| format!("Failed to read web config at {}", web_config_path));
61            },
62        };
63
64        let base = Path::new(services_path);
65        let templates = normalize_under_services(&templates_path, base);
66        let assets = normalize_under_services(&assets_path, base);
67
68        Ok(Self { templates, assets })
69    }
70}
71
72fn normalize_under_services(raw: &str, base: &Path) -> PathBuf {
73    let candidate = Path::new(raw);
74    if candidate.is_absolute() {
75        return candidate.to_path_buf();
76    }
77    let stripped = candidate.strip_prefix("services").unwrap_or(candidate);
78    base.join(stripped)
79}