summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-10-31 13:08:08 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-10-31 13:08:08 -0500
commita80df869a92c330fbad9695057720b11a18af901 (patch)
tree5307272821cb3adc3f0348413a3b904290890044 /src/main.rs
parent9139398d465f5899cc31bc4cdd54daa7392ca550 (diff)
Forbid Rust lints and enforce strict typed TypeScript linting
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs324
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()
}