Skip to main content

smbcloud_model/
runner.rs

1use {
2    crate::error_codes::{ErrorCode::UnsupportedRunner, ErrorResponse},
3    serde::{Deserialize, Serialize},
4    serde_repr::{Deserialize_repr, Serialize_repr},
5    std::{
6        fmt::{self, Display, Formatter},
7        fs,
8        path::PathBuf,
9    },
10};
11
12#[derive(Debug, Deserialize_repr, Serialize_repr, Clone, Copy, PartialEq, Default)]
13#[repr(u8)]
14#[tsync::tsync]
15pub enum Runner {
16    #[default]
17    NodeJs = 0,
18    /// A pure static site: no app process on the server, nginx serves files
19    /// directly. Always deployed via rsync — git push has no build step to run.
20    Static = 1,
21    Ruby = 2,
22    Swift = 3,
23    Monorepo = 255,
24}
25
26impl Display for Runner {
27    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
28        match self {
29            Runner::NodeJs => write!(f, "NodeJs"),
30            Runner::Static => write!(f, "Static"),
31            Runner::Ruby => write!(f, "Ruby"),
32            Runner::Swift => write!(f, "Swift"),
33            Runner::Monorepo => write!(f, "Monorepo"),
34        }
35    }
36}
37
38#[derive(Debug, Serialize, Deserialize)]
39#[tsync::tsync]
40pub enum NodeJsFramework {
41    NextJs,
42    Astro,
43}
44
45#[derive(Debug, Serialize, Deserialize)]
46#[tsync::tsync]
47pub enum RubyFramework {
48    Rails,
49}
50
51#[derive(Debug, Serialize, Deserialize)]
52#[tsync::tsync]
53pub enum SwiftFramework {
54    Vapor,
55}
56
57impl Runner {
58    pub fn from(repo_path: &PathBuf) -> Result<Runner, ErrorResponse> {
59        // See if we have a framework-based config.
60        if repo_path.join("package.json").exists()
61            && (next_config_exists(repo_path) || astro_config_exists(repo_path))
62        {
63            return Ok(Runner::NodeJs);
64        }
65
66        if repo_path.join("Gemfile").exists() {
67            return Ok(Runner::Ruby);
68        }
69        if repo_path.join("Package.swift").exists() {
70            return Ok(Runner::Swift);
71        }
72        // See if we have a monorepo setup.
73        non_framework_runner()
74    }
75
76    pub fn git_host(&self) -> String {
77        format!("git@{}.smbcloud.xyz", self.api())
78    }
79
80    /// Returns the explicit hostname used for rsync SSH connections.
81    /// e.g. `api.smbcloud.xyz` or `api-1.smbcloud.xyz`
82    pub fn rsync_host(&self) -> String {
83        format!("{}.smbcloud.xyz", self.api())
84    }
85
86    fn api(&self) -> &str {
87        match self {
88            Runner::Monorepo => "monorepo",
89            // Static sites and NodeJs projects share the same lightweight tier
90            Runner::NodeJs | Runner::Static => "api",
91            Runner::Ruby | Runner::Swift => "api-1",
92        }
93    }
94}
95
96fn non_framework_runner() -> Result<Runner, ErrorResponse> {
97    Err(ErrorResponse::Error {
98        error_code: UnsupportedRunner,
99        message: UnsupportedRunner.message(None).to_string(),
100    })
101}
102
103// Helper function to detect any next.config.* file
104fn next_config_exists(repo_path: &PathBuf) -> bool {
105    if let Ok(entries) = fs::read_dir(repo_path) {
106        for entry in entries.flatten() {
107            let filename = entry.file_name();
108            let filename_str = filename.to_string_lossy();
109            if filename_str.starts_with("next.config.") {
110                return true;
111            }
112        }
113    }
114    false
115}
116
117// Helper function to detect any astro.config.* file
118fn astro_config_exists(repo_path: &PathBuf) -> bool {
119    if let Ok(entries) = fs::read_dir(repo_path) {
120        for entry in entries.flatten() {
121            let filename = entry.file_name();
122            let filename_str = filename.to_string_lossy();
123            if filename_str.starts_with("astro.config.") {
124                return true;
125            }
126        }
127    }
128    false
129}