// Copyright (C) 2026 Doan Luu Duc Tai // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #![doc = "Rust commands for personal web infrastructure."] mod archive; mod chat; mod cli; mod files; mod images; mod process; mod project; mod release; mod render; use std::path::{Path, PathBuf}; use std::process::ExitCode; use crate::app::cli::{Action, Cli}; use anyhow::Result; use crate::app::project::{Mode, Project}; #[doc = "Runs the checked command-line entry point and returns its process status."] #[must_use] pub fn run() -> ExitCode { let arguments = match Cli::try_parse() { Ok(arguments) => arguments, Err(error) => { let status = if error.use_stderr() { ExitCode::FAILURE } else { ExitCode::SUCCESS }; drop(error.print()); return status; } }; match execute(arguments) { Ok(()) => ExitCode::SUCCESS, Err(error) => { drop(process::error(format_args!("Error: {error:#}"))); ExitCode::FAILURE } } } fn execute(arguments: Cli) -> Result<()> { if let Action::Restore { archive, destination, } = &arguments.command { return archive::restore(&absolute(archive)?, &absolute(destination)?); } let project = Project::discover(arguments.root.as_deref())?; if let Action::Chat { init, reset_owner, owner_fingerprint, } = arguments.command { return chat(&project, init, reset_owner.as_deref(), owner_fingerprint); } let _operation = project.lock()?; match arguments.command { Action::Dev { port } => { let mut settings = project.settings()?; settings.mode = Mode::Local; if let Some(selected_port) = port { settings.port = selected_port; } images::prepare(&project, &mut settings)?; project.up(&settings) } Action::Up { production, local, port, online, } => { let mut settings = project.settings()?; if production { settings.mode = Mode::Production; } if local { settings.mode = Mode::Local; } if let Some(selected_port) = port { settings.port = selected_port; } if online { settings.online = true; } images::prepare(&project, &mut settings)?; project.up(&settings) } Action::Config { production, port } => { let mut settings = project.settings()?; settings.mode = if production { Mode::Production } else { Mode::Local }; if let Some(selected_port) = port { settings.port = selected_port; } stack_config::initialize_public(&project.root)?; render::prepare(&project, &settings)?; process::line(format_args!( "Public configuration: {}", render::directory(&project, &settings).display() )) } Action::Status => project.status(&project.settings()?), Action::Down => project.down(&project.settings()?), Action::Backup { destination, include_images, recipient, key, } => { let output = destination.map_or_else( || Ok(project.root.join("var/exports")), |path| absolute(&path), )?; let configuration = stack_config::Config::load(&project.root)?; let selected_recipient = recipient.unwrap_or(configuration.value("BACKUP_RECIPIENT")?.to_owned()); archive::backup( &project, &output, include_images, &selected_recipient, key.as_deref(), ) } Action::Release { destination } => { let output = destination .map_or_else(|| Ok(project.root.join("dist")), |path| absolute(&path))?; release::create(&project, &output) } Action::Activate { archive } => release::activate(&absolute(&archive)?, &project), Action::Check => check(&project), Action::Licenses => licenses(&project), Action::Chat { .. } => anyhow::bail!("Chat action was not handled"), Action::Restore { .. } => anyhow::bail!("Restore action was not handled"), } } fn chat( project: &Project, init: bool, reset_owner: Option<&str>, owner_fingerprint: bool, ) -> Result<()> { if project.settings()?.mode == Mode::Production { return chat::admin(project, init, reset_owner, owner_fingerprint); } let mut command = project.command("cargo")?; command .args(["run", "--locked", "-p", "stack-chat-server", "--", "--root"]) .arg(&project.root); if init { command.arg("--init"); } if let Some(fingerprint) = reset_owner { command.arg("--reset-owner").arg(fingerprint); } if owner_fingerprint { command.arg("--owner-fingerprint"); } process::run(&mut command) } fn absolute(path: &Path) -> Result { Ok(if path.is_absolute() { path.to_path_buf() } else { std::env::current_dir()?.join(path) }) } fn licenses(project: &Project) -> Result<()> { process::run(project.command("cargo")?.args([ "about", "generate", "--workspace", "--locked", "about.hbs", "--output-file", "web/THIRD-PARTY-LICENSES.html", ])) } fn check(project: &Project) -> Result<()> { process::run(project.command("sh")?.arg("infra/chat/build-typescript.sh"))?; process::run( project .command("cargo")? .args(["fmt", "--all", "--", "--check"]), )?; process::run(project.command("cargo")?.args([ "clippy", "--locked", "--workspace", "--all-targets", "--all-features", "--", "-D", "warnings", ]))?; process::run(project.command("cargo")?.args([ "test", "--locked", "--workspace", "--all-targets", "--all-features", ]))?; process::run( project .command("cargo")? .env("RUSTDOCFLAGS", "-D warnings") .args([ "doc", "--locked", "--workspace", "--no-deps", "--document-private-items", ]), )?; process::run(project.command("cargo")?.args([ "deny", "--workspace", "--all-features", "check", ]))?; licenses(project) }