// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! The command-line interface for fabrica. //! //! Every subcommand operates directly on the store and filesystem; there is no //! IPC with a running `serve`. [`run`] is the single entry point invoked by the //! `fabrica` binary. The command tree is built out over the implementation //! phases; today it covers only `config check` and `config show`. mod admin_cmd; mod config_cmd; mod context; mod group_cmd; mod hook_cmd; mod key_cmd; mod prompt; mod repo_cmd; #[cfg(test)] mod testutil; mod token_cmd; mod user_cmd; use std::path::PathBuf; use std::process::ExitCode; use clap::{Parser, Subcommand}; /// Top-level argument parser for the `fabrica` binary. #[derive(Debug, Parser)] #[command(name = "fabrica", version, about = "A self-hosted git server.")] struct Cli { /// Path to a configuration file, merged last after the standard locations. #[arg(long, global = true, value_name = "PATH")] config: Option, /// Override the configured log level (`trace|debug|info|warn|error`). #[arg(long, global = true, value_name = "LEVEL")] log_level: Option, /// Emit machine-readable JSON instead of human-formatted output. #[arg(long, global = true)] json: bool, /// The subcommand to run. #[command(subcommand)] command: Command, } /// The fabrica subcommand tree. #[derive(Debug, Subcommand)] enum Command { /// Inspect and validate configuration. Config { /// The configuration action to perform. #[command(subcommand)] command: config_cmd::ConfigCommand, }, /// Manage user accounts. User { /// The user action to perform. #[command(subcommand)] command: user_cmd::UserCommand, }, /// Manage a user's SSH and GPG keys. Key { /// The key action to perform. #[command(subcommand)] command: key_cmd::KeyCommand, }, /// Manage repositories. Repo { /// The repository action to perform. #[command(subcommand)] command: repo_cmd::RepoCommand, }, /// Manage the group hierarchy. Group { /// The group action to perform. #[command(subcommand)] command: group_cmd::GroupCommand, }, /// Manage a user's API tokens. Token { /// The token action to perform. #[command(subcommand)] command: token_cmd::TokenCommand, }, /// Run the HTTPS and SSH servers in the foreground. Serve { /// Double-fork into the background and write a pid file. (Not yet /// implemented — systemd/Docker manage the foreground process.) #[arg(long)] detach: bool, /// Skip applying pending migrations on startup. #[arg(long)] no_migrate: bool, }, /// Apply pending database migrations. Migrate, /// Check that the environment fabrica needs is present and healthy. Doctor { /// Only set the exit code; suppress the per-check report. For health checks. #[arg(long)] quiet: bool, }, /// Internal git-hook entry points (invoked by installed hooks). #[command(hide = true)] Hook { /// The hook to run. #[command(subcommand)] command: hook_cmd::HookCommand, }, } /// The parsed request to run `fabrica serve`, handed to the serve callback the /// root binary supplies. This keeps `cli` free of any dependency on `web`/`ssh` /// (no crate may depend on `web`); the binary wires the servers together. #[derive(Debug, Clone)] pub struct ServeRequest { /// The `--config` path. pub config_path: Option, /// Double-fork into the background. pub detach: bool, /// Skip startup migrations. pub no_migrate: bool, } /// Resolve the instance signing secret (generating and persisting it on first /// use), for the binary's serve wiring. /// /// # Errors /// /// Returns an error if the secret file cannot be read or written. pub fn resolve_secret(config: &config::Config) -> anyhow::Result { context::ensure_secret(config) } /// Parse arguments and dispatch to the requested subcommand. /// /// `serve` handles the `fabrica serve` command; the root binary supplies it so /// the servers (which live in `web`/`ssh`) are wired without `cli` depending on /// them. Returns the process exit code. Argument-parse failures are handled by /// clap; any other error is printed to stderr and mapped to /// [`ExitCode::FAILURE`]. pub fn run(serve: F) -> ExitCode where F: FnOnce(ServeRequest) -> anyhow::Result, { let cli = Cli::parse(); match dispatch(&cli, serve) { Ok(code) => code, Err(err) => { eprintln!("error: {err:#}"); ExitCode::FAILURE } } } /// Route a parsed [`Cli`] to its handler. fn dispatch(cli: &Cli, serve: F) -> anyhow::Result where F: FnOnce(ServeRequest) -> anyhow::Result, { match &cli.command { Command::Serve { detach, no_migrate } => serve(ServeRequest { config_path: cli.config.clone(), detach: *detach, no_migrate: *no_migrate, }), Command::Config { command } => config_cmd::run(command, cli.config.as_deref(), cli.json), Command::User { command } => user_cmd::run(command, cli.config.as_deref(), cli.json), Command::Key { command } => key_cmd::run(command, cli.config.as_deref(), cli.json), Command::Repo { command } => repo_cmd::run(command, cli.config.as_deref(), cli.json), Command::Group { command } => group_cmd::run(command, cli.config.as_deref(), cli.json), Command::Token { command } => token_cmd::run(command, cli.config.as_deref(), cli.json), Command::Migrate => admin_cmd::migrate(cli.config.as_deref()), Command::Doctor { quiet } => admin_cmd::doctor(cli.config.as_deref(), *quiet), Command::Hook { command } => Ok(hook_cmd::run(command)), } }