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", "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, 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 `"systemd"`.
107 /// 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 #[serde(default)]
112 pub process_manager: Option<String>,
113 /// Port the standalone server binds to (default: 3000). Must match nginx upstream configuration.
114 #[serde(default)]
115 pub port: Option<u16>,
116 /// Path to a shared lib directory to rsync to the server before deploying,
117 /// e.g. "lib". Used by Rails apps that depend on native gems built from
118 /// monorepo-level source. Relative to the repo root.
119 pub shared_lib: Option<String>,
120 /// SSH command to run on the server after rsyncing the shared lib,
121 /// e.g. "cd ~/lib/gems/gem_error_codes && rbenv local 3.4.2 && bundle install && bundle exec rake compile".
122 pub compile_cmd: Option<String>,
123 /// Install command override, e.g. "pnpm install --frozen-lockfile".
124 #[serde(default)]
125 pub install_command: Option<String>,
126 /// Rust binary filename to upload and restart, e.g. "onde-cloud".
127 /// When absent, the CLI falls back to the Cargo package name.
128 pub binary_name: Option<String>,
129 /// Rust target triple used for local cross-compilation before upload,
130 /// e.g. "x86_64-unknown-linux-gnu".
131 pub rust_target: Option<String>,
132 /// Swift SDK identifier used to cross-compile a Swift/Vapor app for Linux,
133 /// e.g. "x86_64-swift-linux-musl" (the Static Linux SDK). Defaults to
134 /// "x86_64-swift-linux-musl" when absent. Built natively on the host with
135 /// `swift build --swift-sdk <id>` — no Docker, no emulation.
136 pub swift_sdk: Option<String>,
137 /// Optional toolchain identifier passed via the `TOOLCHAINS` env var when
138 /// cross-compiling Swift. Needed on macOS where the default `swift` is
139 /// Apple's Xcode toolchain (which lacks `lld`); point it at an installed
140 /// swift.org toolchain, e.g. "swift" (resolves to swift-latest) or a bundle
141 /// id like "org.swift.632202605101a". Unnecessary when `swift` is already a
142 /// swift.org toolchain (e.g. via swiftly).
143 pub swift_toolchain: Option<String>,
144}
145
146impl Display for Project {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 write!(f, "ID: {}, Name: {}", self.id, self.name,)
149 }
150}
151/// Payload for creating the umbrella workspace. Deploy concerns (runner,
152/// repository, deployment method) live on the App, not the Project.
153#[derive(Serialize, Debug, Deserialize, Clone)]
154#[tsync]
155pub struct ProjectCreate {
156 pub name: String,
157 pub description: String,
158}
159
160#[derive(Deserialize, Serialize, Debug)]
161#[tsync]
162pub struct Deployment {
163 pub id: i32,
164 pub project_id: i32,
165 pub frontend_app_id: Option<String>,
166 pub frontend_app_name: Option<String>,
167 pub commit_hash: String,
168 pub status: DeploymentStatus,
169 #[serde(with = "ar_date_format")]
170 pub created_at: DateTime<Utc>,
171 #[serde(with = "ar_date_format")]
172 pub updated_at: DateTime<Utc>,
173}
174
175#[derive(Deserialize, Serialize, Debug)]
176pub struct DeploymentPayload {
177 pub commit_hash: String,
178 pub status: DeploymentStatus,
179 pub frontend_app_id: Option<String>,
180}
181
182#[derive(Deserialize_repr, Serialize_repr, Debug, Clone, Copy)] // Added Clone, Copy
183#[repr(u8)]
184#[tsync]
185pub enum DeploymentStatus {
186 Started = 0,
187 Failed,
188 Done,
189}
190
191impl Display for DeploymentStatus {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 match self {
194 DeploymentStatus::Started => write!(f, "🚀"),
195 DeploymentStatus::Failed => write!(f, "❌"),
196 DeploymentStatus::Done => write!(f, "✅"),
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use serde_json::json;
205 #[test]
206 fn test_project_create() {
207 let project_create = ProjectCreate {
208 name: "test".to_owned(),
209 description: "test".to_owned(),
210 };
211 let json = json!({
212 "name": "test",
213 "description": "test"
214 });
215 assert_eq!(serde_json::to_value(project_create).unwrap(), json);
216 }
217
218 #[test]
219 fn test_deployment_status_display() {
220 assert_eq!(format!("{}", DeploymentStatus::Started), "🚀");
221 assert_eq!(DeploymentStatus::Started.to_string(), "🚀");
222
223 assert_eq!(format!("{}", DeploymentStatus::Failed), "❌");
224 assert_eq!(DeploymentStatus::Failed.to_string(), "❌");
225
226 assert_eq!(format!("{}", DeploymentStatus::Done), "✅");
227 assert_eq!(DeploymentStatus::Done.to_string(), "✅");
228 }
229}