systemprompt-cloud 0.2.0

systemprompt.io Cloud infrastructure - API client, credentials, OAuth
Documentation
use std::path::{Path, PathBuf};

use crate::constants::{dir_names, paths};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProjectPath {
    Root,
    ProfilesDir,
    DockerDir,
    StorageDir,
    SessionsDir,
    Dockerfile,
    LocalCredentials,
    LocalTenants,
    LocalSession,
}

impl ProjectPath {
    #[must_use]
    pub const fn segments(&self) -> &'static [&'static str] {
        match self {
            Self::Root => &[paths::ROOT_DIR],
            Self::ProfilesDir => &[paths::ROOT_DIR, paths::PROFILES_DIR],
            Self::DockerDir => &[paths::ROOT_DIR, paths::DOCKER_DIR],
            Self::StorageDir => &[paths::STORAGE_DIR],
            Self::SessionsDir => &[paths::ROOT_DIR, dir_names::SESSIONS],
            Self::Dockerfile => &[paths::ROOT_DIR, paths::DOCKERFILE],
            Self::LocalCredentials => &[paths::ROOT_DIR, paths::CREDENTIALS_FILE],
            Self::LocalTenants => &[paths::ROOT_DIR, paths::TENANTS_FILE],
            Self::LocalSession => &[paths::ROOT_DIR, paths::SESSION_FILE],
        }
    }

    #[must_use]
    pub const fn is_dir(&self) -> bool {
        matches!(
            self,
            Self::Root | Self::ProfilesDir | Self::DockerDir | Self::StorageDir | Self::SessionsDir
        )
    }

    #[must_use]
    pub fn resolve(&self, project_root: &Path) -> PathBuf {
        let mut path = project_root.to_path_buf();
        for segment in self.segments() {
            path.push(segment);
        }
        path
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProfilePath {
    Config,
    Secrets,
    DockerDir,
    Dockerfile,
    Entrypoint,
    Dockerignore,
    Compose,
}

impl ProfilePath {
    #[must_use]
    pub const fn filename(&self) -> &'static str {
        match self {
            Self::Config => paths::PROFILE_CONFIG,
            Self::Secrets => paths::PROFILE_SECRETS,
            Self::DockerDir => paths::PROFILE_DOCKER_DIR,
            Self::Dockerfile => paths::DOCKERFILE,
            Self::Entrypoint => paths::ENTRYPOINT,
            Self::Dockerignore => paths::DOCKERIGNORE,
            Self::Compose => paths::COMPOSE_FILE,
        }
    }

    #[must_use]
    pub const fn is_docker_file(&self) -> bool {
        matches!(
            self,
            Self::Dockerfile | Self::Entrypoint | Self::Dockerignore | Self::Compose
        )
    }

    #[must_use]
    pub fn resolve(&self, profile_dir: &Path) -> PathBuf {
        match self {
            Self::Dockerfile | Self::Entrypoint | Self::Dockerignore | Self::Compose => profile_dir
                .join(paths::PROFILE_DOCKER_DIR)
                .join(self.filename()),
            _ => profile_dir.join(self.filename()),
        }
    }
}

fn is_valid_project_root(path: &Path) -> bool {
    if !path.join(paths::ROOT_DIR).is_dir() {
        return false;
    }
    path.join("Cargo.toml").exists()
        || path.join("services").is_dir()
        || path.join("storage").is_dir()
}

#[derive(Debug, Clone)]
pub struct ProjectContext {
    root: PathBuf,
}

impl ProjectContext {
    #[must_use]
    pub const fn new(root: PathBuf) -> Self {
        Self { root }
    }

    #[must_use]
    pub fn discover() -> Self {
        let cwd = match std::env::current_dir() {
            Ok(dir) => dir,
            Err(e) => {
                tracing::debug!(error = %e, "Failed to get current directory, using '.'");
                PathBuf::from(".")
            },
        };
        Self::discover_from(&cwd)
    }

    #[must_use]
    pub fn discover_from(start: &Path) -> Self {
        let mut current = start.to_path_buf();
        loop {
            if is_valid_project_root(&current) {
                return Self::new(current);
            }
            if !current.pop() {
                break;
            }
        }
        Self::new(start.to_path_buf())
    }

    #[must_use]
    pub fn root(&self) -> &Path {
        &self.root
    }

    #[must_use]
    pub fn resolve(&self, path: ProjectPath) -> PathBuf {
        path.resolve(&self.root)
    }

    #[must_use]
    pub fn systemprompt_dir(&self) -> PathBuf {
        self.resolve(ProjectPath::Root)
    }

    #[must_use]
    pub fn profiles_dir(&self) -> PathBuf {
        self.resolve(ProjectPath::ProfilesDir)
    }

    #[must_use]
    pub fn profile_dir(&self, name: &str) -> PathBuf {
        self.profiles_dir().join(name)
    }

    #[must_use]
    pub fn profile_path(&self, name: &str, path: ProfilePath) -> PathBuf {
        path.resolve(&self.profile_dir(name))
    }

    #[must_use]
    pub fn profile_docker_dir(&self, name: &str) -> PathBuf {
        self.profile_path(name, ProfilePath::DockerDir)
    }

    #[must_use]
    pub fn profile_dockerfile(&self, name: &str) -> PathBuf {
        self.profile_path(name, ProfilePath::Dockerfile)
    }

    #[must_use]
    pub fn profile_entrypoint(&self, name: &str) -> PathBuf {
        self.profile_path(name, ProfilePath::Entrypoint)
    }

    #[must_use]
    pub fn profile_dockerignore(&self, name: &str) -> PathBuf {
        self.profile_path(name, ProfilePath::Dockerignore)
    }

    #[must_use]
    pub fn profile_compose(&self, name: &str) -> PathBuf {
        self.profile_path(name, ProfilePath::Compose)
    }

    #[must_use]
    pub fn docker_dir(&self) -> PathBuf {
        self.resolve(ProjectPath::DockerDir)
    }

    #[must_use]
    pub fn storage_dir(&self) -> PathBuf {
        self.resolve(ProjectPath::StorageDir)
    }

    #[must_use]
    pub fn dockerfile(&self) -> PathBuf {
        self.resolve(ProjectPath::Dockerfile)
    }

    #[must_use]
    pub fn local_credentials(&self) -> PathBuf {
        self.resolve(ProjectPath::LocalCredentials)
    }

    #[must_use]
    pub fn local_tenants(&self) -> PathBuf {
        self.resolve(ProjectPath::LocalTenants)
    }

    #[must_use]
    pub fn sessions_dir(&self) -> PathBuf {
        self.resolve(ProjectPath::SessionsDir)
    }

    #[must_use]
    pub fn exists(&self, path: ProjectPath) -> bool {
        self.resolve(path).exists()
    }

    #[must_use]
    pub fn profile_exists(&self, name: &str) -> bool {
        self.profile_dir(name).exists()
    }
}