diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 324 |
1 files changed, 5 insertions, 319 deletions
diff --git a/src/main.rs b/src/main.rs index e6323e2..942a379 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,326 +13,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -#![doc = "Rust commands for personal web infrastructure."] +#![doc = "Checked command-line entry point for the personal infrastructure stack."] -pub(crate) mod archive; -pub(crate) mod chat; -pub(crate) mod files; -pub(crate) mod images; -pub(crate) mod process; -pub(crate) mod project; -pub(crate) mod release; -pub(crate) mod render; +mod app; -use std::path::{Path, PathBuf}; -use std::process::ExitCode; +pub use app::run; -use anyhow::Result; -use clap::{Parser, Subcommand}; - -use crate::project::{Mode, Project}; - -#[derive(Debug, Parser)] -#[command( - name = "stack", - version, - about = "Develop, deploy, and back up the personal web stack" -)] -struct Cli { - #[arg( - long, - global = true, - help = "Application root; otherwise discovered from the working directory or binary" - )] - root: Option<PathBuf>, - #[command(subcommand)] - command: Action, -} - -#[derive(Debug, Subcommand)] -enum Action { - #[command(about = "Run experimental local chat or administer its packaged deployment")] - Chat { - #[arg(long, conflicts_with_all = ["reset_owner", "owner_fingerprint"])] - init: bool, - #[arg( - long, - value_name = "FINGERPRINT", - conflicts_with = "owner_fingerprint", - help = "Permanently clear owner registration and chat history after key loss" - )] - reset_owner: Option<String>, - #[arg(long, help = "Print the registered owner's public fingerprint")] - owner_fingerprint: bool, - }, - #[command(about = "Start the local development services")] - Dev { - #[arg(long)] - port: Option<u16>, - }, - #[command(about = "Start or update services in the selected mode")] - Up { - #[arg(long, conflicts_with = "local")] - production: bool, - #[arg(long)] - local: bool, - #[arg(long)] - port: Option<u16>, - #[arg(long)] - online: bool, - }, - #[command( - about = "Render public Caddy, website, and cgit configuration without starting services" - )] - Config { - #[arg(long)] - production: bool, - #[arg(long)] - port: Option<u16>, - }, - #[command(about = "Show service status")] - Status, - #[command(about = "Stop services while retaining data")] - Down, - #[command(about = "Create a GPG-encrypted application export")] - Backup { - destination: Option<PathBuf>, - #[arg(long)] - include_images: bool, - #[arg(long)] - recipient: Option<String>, - #[arg(long)] - key: Option<PathBuf>, - }, - #[command( - about = "Decrypt, validate, and restore into a new directory without starting services" - )] - Restore { - archive: PathBuf, - destination: PathBuf, - }, - #[command(about = "Run formatting, strict lints, tests, documentation, and dependency checks")] - Check, - #[command(about = "Check and build a portable production release locally")] - Release { destination: Option<PathBuf> }, - #[command(about = "Install a prebuilt release without compiling on the server")] - Activate { archive: PathBuf }, - #[command(about = "Generate the third-party license page with GNU license names")] - Licenses, -} - -fn main() -> 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<PathBuf> { - 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) +fn main() -> std::process::ExitCode { + run() } |
