Skip to main content

smbcloud_model/
project.rs

1use {
2    crate::{app_auth::AuthApp, ar_date_format, runner::Runner, tenant::Tenant},
3    chrono::{DateTime, Utc},
4    serde::{Deserialize, Serialize},
5    serde_repr::{Deserialize_repr, Serialize_repr},
6    std::fmt::Display,
7    tsync::tsync,
8};
9
10fn default_datetime() -> DateTime<Utc> {
11    DateTime::UNIX_EPOCH
12}
13
14/// How the project's files are delivered to the server.
15///
16/// `Git`   — the classic smbCloud flow: push to a remote git repo, the server
17///           builds and restarts the process.
18/// `Rsync` — files are transferred directly with rsync over SSH; no build step
19///           runs on the server. Ideal for pre-built static sites or assets.
20#[derive(Deserialize_repr, Serialize_repr, Debug, Clone, Copy, PartialEq, Eq, Default)]
21#[repr(u8)]
22#[tsync]
23pub enum DeploymentMethod {
24    #[default]
25    Git = 0,
26    Rsync = 1,
27}
28
29impl Display for DeploymentMethod {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            DeploymentMethod::Git => write!(f, "Git"),
33            DeploymentMethod::Rsync => write!(f, "Rsync"),
34        }
35    }
36}
37
38#[derive(Deserialize, Debug, Serialize, Clone, Default)]
39pub struct Config {
40    /// Legacy project field — kept for backward compatibility during migration.
41    pub current_project: Option<Project>,
42    /// The active FrontendApp for CLI deploy operations.
43    #[serde(default)]
44    pub current_frontend_app: Option<crate::frontend_app::FrontendApp>,
45    pub current_auth_app: Option<AuthApp>,
46    /// The tenant selected with `smb tenant use`. Determines which workspace
47    /// tenant-scoped operations (e.g. `smb project new`) target — the API
48    /// falls back to the user's personal tenant when this isn't set.
49    #[serde(default)]
50    pub current_tenant: Option<Tenant>,
51}
52
53#[derive(Deserialize, Serialize, Debug, Clone)]
54#[tsync]
55pub struct Project {
56    /// Umbrella smbCloud workspace ID.
57    pub id: i32,
58    /// The tenant this project belongs to. Optional for older API responses
59    /// that predate the tenant/workspace split.
60    #[serde(default)]
61    pub tenant_id: Option<i64>,
62    #[serde(default)]
63    pub name: String,
64    #[serde(default)]
65    pub runner: Runner,
66    /// Defaults to `Git` when absent (older API responses won't include the field).
67    #[serde(default)]
68    pub deployment_method: DeploymentMethod,
69    pub path: Option<String>,
70    pub repository: Option<String>,
71    pub description: Option<String>,
72    /// Deployable app ID for precise deployment tracking. Optional during the
73    /// migration away from project-as-app semantics.
74    pub frontend_app_id: Option<String>,
75    /// Repo ID backing this deploy target. Optional until the API exposes it
76    /// consistently to the CLI.
77    pub deploy_repo_id: Option<i64>,
78    /// Approved rsync host override for deployments that do not use the
79    /// runner's default static tier.
80    #[serde(default)]
81    pub rsync_host: Option<String>,
82    /// Repo-relative app path for monorepo targets, e.g. "apps/web/console".
83    pub source_path: Option<String>,
84    #[serde(default = "default_datetime")]
85    pub created_at: DateTime<Utc>,
86    #[serde(default = "default_datetime")]
87    pub updated_at: DateTime<Utc>,
88    /// Deployment kind, e.g. "vite-spa", "sveltekit", "nextjs-ssr", or "rust".
89    pub kind: Option<String>,
90    /// Local source directory to build from, e.g. "frontend/connected-devices"
91    /// or a Rust crate root like ".".
92    /// Used by local-build deploys such as vite-spa, sveltekit, nextjs-ssr, and rust.
93    /// Distinct from `path`, which is the remote destination on the server.
94    pub source: Option<String>,
95    /// Build output directory relative to `source`, e.g. "dist".
96    pub output: Option<String>,
97    /// Package manager to use for the build step, e.g. "pnpm".
98    pub package_manager: Option<String>,
99    /// PM2 process name to restart after a nextjs-ssr deploy, e.g. "my-app".
100    /// Matches the name passed to `pm2 start` on the server.
101    pub pm2_app: Option<String>,
102    /// Environment variables to seed into the PM2 ecosystem `env_production` block.
103    /// Populated from the server-side App record; not written to `.smb/config.toml`.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub pm2_env: Option<std::collections::HashMap<String, serde_json::Value>>,
106    /// Runtime supervisor. For `nextjs-ssr` apps: `"pm2"` (default) or
107    /// `"systemd"`. When `"systemd"`, the deploy restart step drives a git-owned
108    /// `systemctl --user restart <pm2_app>.service` (created by the
109    /// pm2-to-systemd migration) instead of `pm2 delete`/`pm2 start`.
110    /// `pm2_app` is still used as the unit name.
111    /// For `rust` apps: `"nohup"` (default) or `"systemd"`, with
112    /// `<binary_name>.service` as the user unit name.
113    pub process_manager: Option<String>,
114    /// Port the standalone server binds to (default: 3000). Must match nginx upstream configuration.
115    #[serde(default)]
116    pub port: Option<u16>,
117    /// HTTP path polled after a `rust` deploy restart, e.g. `"/health"`. Used
118    /// together with `port`: the deploy requests
119    /// `http://127.0.0.1:<port><health_path>` until it succeeds, and rolls back
120    /// to the previous binary if it does not.
121    #[serde(default)]
122    pub health_path: Option<String>,
123    /// Path to a shared lib directory to rsync to the server before deploying,
124    /// e.g. "lib". Used by Rails apps that depend on native gems built from
125    /// monorepo-level source. Relative to the repo root.
126    pub shared_lib: Option<String>,
127    /// SSH command to run on the server after rsyncing the shared lib,
128    /// e.g. "cd ~/lib/gems/gem_error_codes && rbenv local 3.4.2 && bundle install && bundle exec rake compile".
129    pub compile_cmd: Option<String>,
130    /// Install command override, e.g. "pnpm install --frozen-lockfile".
131    #[serde(default)]
132    pub install_command: Option<String>,
133    /// Rust binary filename to upload and restart, e.g. "onde-cloud".
134    /// When absent, the CLI falls back to the Cargo package name.
135    pub binary_name: Option<String>,
136    /// Rust target triple used for local cross-compilation before upload,
137    /// e.g. "x86_64-unknown-linux-gnu".
138    pub rust_target: Option<String>,
139    /// Swift SDK identifier used to cross-compile a Swift/Vapor app for Linux,
140    /// e.g. "x86_64-swift-linux-musl" (the Static Linux SDK). Defaults to
141    /// "x86_64-swift-linux-musl" when absent. Built natively on the host with
142    /// `swift build --swift-sdk <id>` — no Docker, no emulation.
143    pub swift_sdk: Option<String>,
144    /// Optional toolchain identifier passed via the `TOOLCHAINS` env var when
145    /// cross-compiling Swift. Needed on macOS where the default `swift` is
146    /// Apple's Xcode toolchain (which lacks `lld`); point it at an installed
147    /// swift.org toolchain, e.g. "swift" (resolves to swift-latest) or a bundle
148    /// id like "org.swift.632202605101a". Unnecessary when `swift` is already a
149    /// swift.org toolchain (e.g. via swiftly).
150    pub swift_toolchain: Option<String>,
151}
152
153impl Display for Project {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        write!(f, "ID: {}, Name: {}", self.id, self.name,)
156    }
157}
158/// Payload for creating the umbrella workspace. Deploy concerns (runner,
159/// repository, deployment method) live on the App, not the Project.
160#[derive(Serialize, Debug, Deserialize, Clone)]
161#[tsync]
162pub struct ProjectCreate {
163    pub name: String,
164    pub description: String,
165}
166
167#[derive(Deserialize, Serialize, Debug)]
168#[tsync]
169pub struct Deployment {
170    pub id: i32,
171    pub project_id: i32,
172    pub frontend_app_id: Option<String>,
173    pub frontend_app_name: Option<String>,
174    pub commit_hash: String,
175    pub status: DeploymentStatus,
176    #[serde(with = "ar_date_format")]
177    pub created_at: DateTime<Utc>,
178    #[serde(with = "ar_date_format")]
179    pub updated_at: DateTime<Utc>,
180}
181
182#[derive(Deserialize, Serialize, Debug)]
183pub struct DeploymentPayload {
184    pub commit_hash: String,
185    pub status: DeploymentStatus,
186    pub frontend_app_id: Option<String>,
187}
188
189#[derive(Deserialize_repr, Serialize_repr, Debug, Clone, Copy)] // Added Clone, Copy
190#[repr(u8)]
191#[tsync]
192pub enum DeploymentStatus {
193    Started = 0,
194    Failed,
195    Done,
196}
197
198impl Display for DeploymentStatus {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        match self {
201            DeploymentStatus::Started => write!(f, "🚀"),
202            DeploymentStatus::Failed => write!(f, "❌"),
203            DeploymentStatus::Done => write!(f, "✅"),
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use serde_json::json;
212    #[test]
213    fn test_project_create() {
214        let project_create = ProjectCreate {
215            name: "test".to_owned(),
216            description: "test".to_owned(),
217        };
218        let json = json!({
219            "name": "test",
220            "description": "test"
221        });
222        assert_eq!(serde_json::to_value(project_create).unwrap(), json);
223    }
224
225    #[test]
226    fn test_deployment_status_display() {
227        assert_eq!(format!("{}", DeploymentStatus::Started), "🚀");
228        assert_eq!(DeploymentStatus::Started.to_string(), "🚀");
229
230        assert_eq!(format!("{}", DeploymentStatus::Failed), "❌");
231        assert_eq!(DeploymentStatus::Failed.to_string(), "❌");
232
233        assert_eq!(format!("{}", DeploymentStatus::Done), "✅");
234        assert_eq!(DeploymentStatus::Done.to_string(), "✅");
235    }
236}