Repository: axodotdev/cargo-dist
Pull request #230 (2023-04-28 18:12:37)

Cli cleanup
* adds flags to `init`
  * --yes auto-accepts all default/recommended values, making init non-interactive
  * --no-generate-ci disables automatically running generate-ci at the end
* removes the alias from `cargo dist` to `cargo dist build`, making bare `cargo dist` just a help page
* adds a bunch of short aliases for common flags


Patch:
diff --git a/cargo-dist/src/cli.rs b/cargo-dist/src/cli.rs
index 2af5923d..f5471f2a 100644
--- a/cargo-dist/src/cli.rs
+++ b/cargo-dist/src/cli.rs
@@ -4,7 +4,6 @@ use clap::{
     builder::{PossibleValuesParser, TypedValueParser},
     Args, Parser, Subcommand, ValueEnum,
 };
-use semver::Version;
 use tracing::level_filters::LevelFilter;
 
 #[derive(Parser)]
@@ -16,7 +15,7 @@ pub enum FakeCli {
     Dist(Cli),
 }
 
-#[derive(Args)]
+#[derive(Args, Clone, Debug)]
 #[clap(version)]
 #[clap(bin_name = "cargo dist")]
 #[clap(args_conflicts_with_subcommands = true)]
@@ -27,17 +26,17 @@ pub enum FakeCli {
 pub struct Cli {
     /// Subcommands ("no subcommand" defaults to `build`)
     #[clap(subcommand)]
-    pub command: Option<Commands>,
+    pub command: Commands,
 
     /// How verbose logging should be (log level)
-    #[clap(long)]
+    #[clap(long, short)]
     #[clap(default_value_t = LevelFilter::WARN)]
     #[clap(value_parser = PossibleValuesParser::new(["off", "error", "warn", "info", "debug", "trace"]).map(|s| s.parse::<LevelFilter>().expect("possible values are valid")))]
     #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
     pub verbose: LevelFilter,
 
     /// The format of the output
-    #[clap(long, value_enum)]
+    #[clap(long, short, value_enum)]
     #[clap(default_value_t = OutputFormat::Human)]
     #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
     pub output_format: OutputFormat,
@@ -55,7 +54,7 @@ pub struct Cli {
     ///
     /// If left unspecified we will use the values in [workspace.metadata.dist],
     /// except for `cargo dist init` which will select some "good defaults" for you.
-    #[clap(long)]
+    #[clap(long, short)]
     #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
     pub target: Vec<String>,
 
@@ -63,7 +62,7 @@ pub struct Cli {
     ///
     /// If left unspecified we will use the values in [workspace.metadata.dist].
     ///  `cargo dist init` will persist the values you pass to that location.
-    #[clap(long)]
+    #[clap(long, short)]
     #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
     pub installer: Vec<InstallerStyle>,
 
@@ -71,7 +70,7 @@ pub struct Cli {
     ///
     /// If left unspecified we will use the value in [workspace.metadata.dist].
     /// `cargo dist init` will persist the values you pass to that location.
-    #[clap(long)]
+    #[clap(long, short)]
     #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
     pub ci: Vec<CiStyle>,
 
@@ -101,18 +100,18 @@ pub struct Cli {
     #[clap(long)]
     #[clap(help_heading = "GLOBAL OPTIONS", global = true)]
     pub tag: Option<String>,
-
-    // Add the args from the "real" build command
-    #[clap(flatten)]
-    pub build_args: BuildArgs,
 }
 
-#[derive(Subcommand)]
+#[derive(Subcommand, Clone, Debug)]
 pub enum Commands {
     /// Build artifacts
     #[clap(disable_version_flag = true)]
     Build(BuildArgs),
-    /// Initialize default settings in your Cargo.toml
+    /// Setup or update cargo-dist
+    ///
+    /// This will interactively guide you through the process of selecting configuration options
+    /// and will also automatically run 'cargo dist generate-ci' afterwards as necessary. It will
+    /// also handle updating your project to a new version of cargo-dist if you're running one.
     #[clap(disable_version_flag = true)]
     Init(InitArgs),
     /// Generate CI scripts for orchestrating cargo-dist
@@ -133,18 +132,20 @@ pub enum Commands {
     #[clap(disable_version_flag = true)]
     #[clap(hide = true)]
     HelpMarkdown(HelpMarkdownArgs),
-}
-
-#[derive(Args)]
-pub struct ReleaseNotesArgs {
-    /// Get release notes for a specific version.
+    /// Get a quick summary of the status of your project
     ///
-    /// Otherwise the app's current version will be used.
-    #[clap(long)]
-    pub version: Version,
+    /// If you want to know what running your cargo-dist CI will produce,
+    /// this is the command for you! It should run the exact same logic and do some
+    /// basic integrity checks.
+    ///
+    /// This is roughly an alias for:
+    ///
+    ///     cargo dist manifest --artifacts=all --no-local-paths
+    #[clap(disable_version_flag = true)]
+    Status(StatusArgs),
 }
 
-#[derive(Args)]
+#[derive(Args, Clone, Debug)]
 pub struct BuildArgs {
     /// Which subset of the Artifacts to build
     ///
@@ -164,7 +165,7 @@ pub struct BuildArgs {
     /// The specifics of "host" mode are intentionally unspecified to enable us to provider better
     /// out-of-the-box UX for local usage. In CI environments you should always specify "global"
     /// or "local" to get consistent behaviour!
-    #[clap(long, value_enum)]
+    #[clap(long, short, value_enum)]
     #[clap(default_value_t = ArtifactMode::Host)]
     pub artifacts: ArtifactMode,
 }
@@ -194,17 +195,27 @@ impl ArtifactMode {
     }
 }
 
-#[derive(Args)]
-pub struct InitArgs {}
+#[derive(Args, Clone, Debug)]
+pub struct InitArgs {
+    /// Automatically accept all recommended/default values
+    ///
+    /// This is equivalent to just mashing ENTER over and over
+    /// during the interactive prompts.
+    #[clap(long, short)]
+    pub yes: bool,
+    /// Don't automatically invoke 'cargo dist generate-ci' at the end
+    #[clap(long)]
+    pub no_generate_ci: bool,
+}
 
-#[derive(Args)]
+#[derive(Args, Clone, Debug)]
 pub struct GenerateCiArgs {}
 
-#[derive(clap::Args)]
+#[derive(Args, Clone, Debug)]
 pub struct HelpMarkdownArgs {}
 
 /// A style of CI to generate
-#[derive(ValueEnum, Clone, Copy)]
+#[derive(ValueEnum, Clone, Copy, Debug)]
 pub enum CiStyle {
     /// Generate github CI that uploads to github releases
     Github,
@@ -241,13 +252,16 @@ impl InstallerStyle {
     }
 }
 
-#[derive(Args)]
+#[derive(Args, Clone, Debug)]
 pub struct ManifestArgs {
     // Add the args from the "real" build command
     #[clap(flatten)]
     pub build_args: BuildArgs,
 }
 
+#[derive(Args, Clone, Debug)]
+pub struct StatusArgs {}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
 pub enum OutputFormat {
     Human,
diff --git a/cargo-dist/src/init.rs b/cargo-dist/src/init.rs
new file mode 100644
index 00000000..53517da1
--- /dev/null
+++ b/cargo-dist/src/init.rs
@@ -0,0 +1,638 @@
+use std::fs::File;
+use std::ops::Not;
+
+use axoproject::errors::AxoprojectError;
+use axoproject::WorkspaceInfo;
+use miette::{Context, IntoDiagnostic};
+use semver::Version;
+
+use crate::errors::{DistError, DistResult, Result};
+use crate::{
+    do_generate_ci, tasks, CiStyle, CompressionImpl, Config, DistMetadata, GenerateCiArgs,
+    InstallerStyle, ZipStyle, METADATA_DIST, PROFILE_DIST,
+};
+
+/// Arguments for `cargo dist init` ([`do_init`][])
+#[derive(Debug)]
+pub struct InitArgs {
+    /// Whether to auto-accept the default values for interactive prompts
+    pub yes: bool,
+    /// Don't automatically generate ci
+    pub no_generate_ci: bool,
+}
+
+/// Run 'cargo dist init'
+pub fn do_init(cfg: &Config, args: &InitArgs) -> Result<()> {
+    let workspace = tasks::get_project()?;
+
+    // Load in the workspace toml to edit and write back
+    let mut workspace_toml = tasks::load_root_cargo_toml(&workspace.manifest_path)?;
+
+    let check = console::style("✔".to_string()).for_stderr().green();
+
+    // Init things
+    eprintln!("first let's setup your cargo build profile...");
+    eprintln!();
+    if init_dist_profile(cfg, &mut workspace_toml)? {
+        eprintln!("{check} added [profile.dist] to your root Cargo.toml");
+    } else {
+        eprintln!("{check} [profile.dist] already exists");
+    }
+    eprintln!();
+
+    eprintln!("next let's setup your cargo-dist config...");
+    eprintln!();
+
+    let meta = get_new_dist_metadata(cfg, args, &workspace)?;
+    update_toml_metadata(&mut workspace_toml, &meta);
+
+    eprintln!("{check} added [workspace.metadata.dist] to your root Cargo.toml");
+    eprintln!();
+
+    {
+        use std::io::Write;
+        let mut workspace_toml_file = File::options()
+            .write(true)
+            .truncate(true)
+            .open(&workspace.manifest_path)
+            .into_diagnostic()
+            .wrap_err("couldn't load root workspace Cargo.toml")?;
+        write!(&mut workspace_toml_file, "{workspace_toml}")
+            .into_diagnostic()
+            .wrap_err("failed to write to Cargo.toml")?;
+        workspace_toml_file
+            .flush()
+            .into_diagnostic()
+            .wrap_err("failed to write to Cargo.toml")?;
+    }
+
+    eprintln!("{check} cargo-dist is setup!");
+    eprintln!();
+
+    if !meta.ci.is_empty() && !args.no_generate_ci {
+        eprintln!("running 'cargo dist generate-ci' to apply any changes to your CI scripts");
+        eprintln!();
+
+        let ci_args = GenerateCiArgs {};
+        do_generate_ci(cfg, &ci_args)?;
+    }
+    Ok(())
+}
+
+fn init_dist_profile(_cfg: &Config, workspace_toml: &mut toml_edit::Document) -> Result<bool> {
+    let profiles = workspace_toml["profile"].or_insert(toml_edit::table());
+    if let Some(t) = profiles.as_table_mut() {
+        t.set_implicit(true)
+    }
+    let dist_profile = &mut profiles[PROFILE_DIST];
+    if !dist_profile.is_none() {
+        return Ok(false);
+    }
+    let mut new_profile = toml_edit::table();
+    {
+        // For some detailed discussion, see: https://github.com/axodotdev/cargo-dist/issues/118
+        let new_profile = new_profile.as_table_mut().unwrap();
+        // We're building for release, so this is a good base!
+        new_profile.insert("inherits", toml_edit::value("release"));
+        // We're building for SUPER DUPER release, so lto is a good idea to enable!
+        //
+        // There's a decent argument for lto=true (aka "fat") here but the cost-benefit
+        // is a bit complex. Fat LTO can be way more expensive to compute (to the extent
+        // that enormous applications like chromium can become unbuildable), but definitely
+        // eeks out a bit more from your binaries.
+        //
+        // In principle cargo-dist is targetting True Shippable Binaries and so it's
+        // worth it to go nuts getting every last drop out of your binaries... but a lot
+        // of people are going to build binaries that might never even be used, so really
+        // we're just burning a bunch of CI time for nothing.
+        //
+        // The user has the freedom to crank this up higher (and/or set codegen-units=1)
+        // if they think it's worth it, but we otherwise probably shouldn't set the planet
+        // on fire just because Number Theoretically Go Up.
+        new_profile.insert("lto", toml_edit::value("thin"));
+        new_profile
+            .decor_mut()
+            .set_prefix("\n# The profile that 'cargo dist' will build with\n")
+    }
+    dist_profile.or_insert(new_profile);
+
+    Ok(true)
+}
+
+/// Initialize [workspace.metadata.dist] with default values based on what was passed on the CLI
+///
+/// Returns whether the initialization was actually done
+/// and whether ci was set
+fn get_new_dist_metadata(
+    cfg: &Config,
+    args: &InitArgs,
+    workspace_info: &WorkspaceInfo,
+) -> DistResult<DistMetadata> {
+    use dialoguer::{Confirm, Input, MultiSelect};
+    // Setup [workspace.metadata.dist]
+
+    let has_config = workspace_info
+        .cargo_metadata_table
+        .as_ref()
+        .and_then(|t| t.as_object())
+        .map(|t| t.contains_key(METADATA_DIST))
+        .unwrap_or(false);
+    let mut meta = if has_config {
+        tasks::parse_metadata_table(workspace_info.cargo_metadata_table.as_ref())
+    } else {
+        DistMetadata {
+            // If they init with this version we're gonna try to stick to it!
+            cargo_dist_version: Some(std::env!("CARGO_PKG_VERSION").parse().unwrap()),
+            // latest stable release at this precise moment
+            // maybe there's something more clever we can do here, but, *shrug*
+            rust_toolchain_version: Some("1.67.1".to_owned()),
+            ci: vec![],
+            installers: None,
+            targets: cfg.targets.is_empty().not().then(|| cfg.targets.clone()),
+            dist: None,
+            include: vec![],
+            auto_includes: None,
+            windows_archive: None,
+            unix_archive: None,
+            npm_scope: None,
+        }
+    };
+
+    // Clone this to simplify checking for settings changes
+    let orig_meta = meta.clone();
+
+    // Now prompt the user interactively to initialize these...
+
+    // Tune the theming a bit
+    let theme = dialoguer::theme::ColorfulTheme {
+        checked_item_prefix: console::style("  [x]".to_string()).for_stderr().green(),
+        unchecked_item_prefix: console::style("  [ ]".to_string()).for_stderr().dim(),
+        active_item_style: console::Style::new().for_stderr().cyan().bold(),
+        ..dialoguer::theme::ColorfulTheme::default()
+    };
+    // Some indicators we'll use in a few places
+    let check = console::style("✔".to_string()).for_stderr().green();
+    let notice = console::style("⚠️".to_string()).for_stderr().yellow();
+
+    // Set cargo-dist-version
+    let current_version: Version = std::env!("CARGO_PKG_VERSION").parse().unwrap();
+    if let Some(desired_version) = &meta.cargo_dist_version {
+        if desired_version != &current_version && !desired_version.pre.starts_with("github-") {
+            let default = true;
+            let prompt = format!(
+                r#"update your project to this version of cargo-dist?
+    {} => {}"#,
+                desired_version, current_version
+            );
+            let response = if args.yes {
+                default
+            } else {
+                let res = Confirm::with_theme(&theme)
+                    .with_prompt(prompt)
+                    .default(default)
+                    .interact()?;
+                eprintln!();
+                res
+            };
+
+            if response {
+                meta.cargo_dist_version = Some(current_version);
+            } else {
+                return Err(DistError::NoUpdateVersion {
+                    project_version: desired_version.clone(),
+                    running_version: current_version,
+                })?;
+            }
+        }
+    } else {
+        let prompt = format!(
+            r#"looks like you deleted the cargo-dist-version key, add it back?
+    this is the version of cargo-dist your releases should use
+    (you're currently running {})"#,
+            current_version
+        );
+        let default = true;
+
+        let response = if args.yes {
+            default
+        } else {
+            let res = Confirm::with_theme(&theme)
+                .with_prompt(prompt)
+                .default(default)
+                .interact()?;
+            eprintln!();
+            res
+        };
+        if response {
+            meta.cargo_dist_version = Some(current_version);
+        } else {
+            // Not recommended but technically ok...
+        }
+    }
+
+    // Enable CI backends
+    {
+        // FIXME: when there is more than one option this should be a proper
+        // multiselect like the installer selector is! For now we do
+        // most of the multi-select logic and then just give a prompt.
+        let known = &[CiStyle::Github];
+        let mut defaults = vec![];
+        let mut keys = vec![];
+        let mut github_key = 0;
+        for item in known {
+            // If this CI style is in their config, keep it
+            // If they passed it on the CLI, flip it on
+            let mut default = meta.ci.contains(item) || cfg.ci.contains(item);
+
+            // If they have a well-defined repo url and it's github, default enable it
+            #[allow(irrefutable_let_patterns)]
+            if let CiStyle::Github = item {
+                github_key = 0;
+                if let Some(repo_url) = &workspace_info.repository_url {
+                    if repo_url.contains("github.com") {
+                        default = true;
+                    }
+                }
+            }
+            defaults.push(default);
+            // This match is here to remind you to add new CiStyles
+            // to `known` above!
+            keys.push(match item {
+                CiStyle::Github => "github",
+            });
+        }
+
+        // Prompt the user
+        let prompt = r#"enable Github CI integration?
+    this creates a CI action which automates creating a Github Release,
+    builds all your binaries/archives, and then uploads them to the Release
+    it also unlocks the ability to generate installers which fetch those artifacts"#;
+        let default = defaults[github_key];
+
+        let github_selected = if args.yes {
+            default
+        } else {
+            let res = Confirm::with_theme(&theme)
+                .with_prompt(prompt)
+                .default(default)
+                .interact()?;
+            eprintln!();
+            res
+        };
+
+        let selected = if github_selected {
+            vec![github_key]
+        } else {
+            vec![]
+        };
+
+        // Apply the results
+        meta.ci = selected.into_iter().map(|i| known[i]).collect();
+    }
+
+    // Enforce repository url right away
+    if meta.ci.contains(&CiStyle::Github) && workspace_info.repository_url.is_none() {
+        // If axoproject complained about inconsistency, forward that
+        // Massively jank manual implementation of "clone" here because lots of error types
+        // (like std::io::Error) don't implement Clone and so axoproject errors can't either
+        let conflict = workspace_info.warnings.iter().find_map(|w| {
+            if let AxoprojectError::InconsistentRepositoryKey {
+                file1,
+                url1,
+                file2,
+                url2,
+            } = w
+            {
+                Some(AxoprojectError::InconsistentRepositoryKey {
+                    file1: file1.clone(),
+                    url1: url1.clone(),
+                    file2: file2.clone(),
+                    url2: url2.clone(),
+                })
+            } else {
+                None
+            }
+        });
+        if let Some(inner) = conflict {
+            return Err(DistError::CantEnableGithubUrlInconsistent { inner })?;
+        } else {
+            // Otherwise assume no URL
+            return Err(DistError::CantEnableGithubNoUrl)?;
+        }
+    }
+
+    // Enable installer backends (if they have a CI backend that can provide URLs)
+    // In the future, "vendored" installers like MSIs could be enabled in this situation!
+    if !meta.ci.is_empty() {
+        let known = &[
+            InstallerStyle::Shell,
+            InstallerStyle::Powershell,
+            InstallerStyle::Npm,
+        ];
+        let mut defaults = vec![];
+        let mut keys = vec![];
+        for item in known {
+            // If this CI style is in their config, keep it
+            // If they passed it on the CLI, flip it on
+            let config_had_it = meta
+                .installers
+                .as_deref()
+                .unwrap_or_default()
+                .contains(item);
+            let cli_had_it = cfg.installers.contains(item);
+
+            let default = config_had_it || cli_had_it;
+            defaults.push(default);
+
+            // This match is here to remind you to add new InstallerStyles
+            // to `known` above!
+            keys.push(match item {
+                InstallerStyle::Shell => "shell",
+                InstallerStyle::Powershell => "powershell",
+                InstallerStyle::Npm => "npm",
+            });
+        }
+
+        // Prompt the user
+        let prompt = r#"enable generating installers?
+    installers streamline fetching your app's prebuilt artifacts
+    see the docs for details on each one
+    (select with arrow keys and space, submit with enter)"#;
+        let selected = if args.yes {
+            defaults
+                .iter()
+                .enumerate()
+                .filter_map(|(idx, enabled)| enabled.then_some(idx))
+                .collect()
+        } else {
+            let res = MultiSelect::with_theme(&theme)
+                .items(&keys)
+                .defaults(&defaults)
+                .with_prompt(prompt)
+                .interact()?;
+            eprintln!();
+            res
+        };
+
+        // Apply the results
+        meta.installers = Some(selected.into_iter().map(|i| known[i]).collect());
+    } else {
+        eprintln!("{notice} no CI backends enabled, skipping installers");
+        eprintln!();
+    }
+
+    // Special handling of the npm installer
+    if meta
+        .installers
+        .as_deref()
+        .unwrap_or_default()
+        .contains(&InstallerStyle::Npm)
+    {
+        // If npm is being newly enabled here, prompt for a @scope
+        let npm_is_new = !orig_meta
+            .installers
+            .as_deref()
+            .unwrap_or_default()
+            .contains(&InstallerStyle::Npm);
+        if npm_is_new {
+            let prompt = r#"you've enabled npm support, please enter the @scope you want to use
+    this is the "namespace" the package will be published under
+    (leave blank to publish globally)"#;
+            let default = "".to_string();
+
+            let scope: String = if args.yes {
+                default
+            } else {
+                let res = Input::with_theme(&theme)
+                    .with_prompt(prompt)
+                    .allow_empty(true)
+                    .validate_with(|v: &String| {
+                        let v = v.trim();
+                        if v.is_empty() {
+                            Ok(())
+                        } else if let Some(v) = v.strip_prefix('@') {
+                            if v.is_empty() {
+                                Err("@ must be followed by something")
+                            } else {
+                                Ok(())
+                            }
+                        } else {
+                            Err("npm scopes must start with @")
+                        }
+                    })
+                    .interact_text()?;
+                eprintln!();
+                res
+            };
+            let scope = scope.trim();
+            if scope.is_empty() {
+                eprintln!("{check} npm packages will be published globally");
+                meta.npm_scope = None;
+            } else {
+                meta.npm_scope = Some(scope.to_owned());
+                eprintln!("{check} npm packages will be published under {scope}");
+            }
+            eprintln!();
+        }
+
+        // FIXME (#226): If they have an npm installer, force on tar.gz compression
+        const TAR_GZ: Option<ZipStyle> = Some(ZipStyle::Tar(CompressionImpl::Gzip));
+        if meta.unix_archive != TAR_GZ || meta.windows_archive != TAR_GZ {
+            let prompt = r#"the npm installer requires binaries to be distributed as .tar.gz, is that ok?
+    otherwise we would distribute your binaries as .zip on windows, .tar.xz everywhere else
+    (this is a hopefully temporary limitation of the npm installer's implementation)"#;
+            let default = true;
+            let force_targz = if args.yes {
+                default
+            } else {
+                let res = Confirm::with_theme(&theme)
+                    .with_prompt(prompt)
+                    .default(default)
+                    .interact()?;
+                eprintln!();
+                res
+            };
+            if force_targz {
+                meta.unix_archive = TAR_GZ;
+                meta.windows_archive = TAR_GZ;
+            } else {
+                return Err(DistError::MustEnableTarGz)?;
+            }
+        }
+    }
+
+    Ok(meta)
+}
+
+fn update_toml_metadata(workspace_toml: &mut toml_edit::Document, meta: &DistMetadata) {
+    use toml_edit::{value, Item};
+
+    // Keys/descriptions
+    const KEY_RUST_VERSION: &str = "rust-toolchain-version";
+    const DESC_RUST_VERSION: &str =
+        "# The preferred Rust toolchain to use in CI (rustup toolchain syntax)\n";
+
+    const KEY_DIST_VERSION: &str = "cargo-dist-version";
+    const DESC_DIST_VERSION: &str =
+        "# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)\n";
+
+    const KEY_CI: &str = "ci";
+    const DESC_CI: &str = "# CI backends to support (see 'cargo dist generate-ci')\n";
+
+    const KEY_INSTALLERS: &str = "installers";
+    const DESC_INSTALLERS: &str = "# The installers to generate for each app\n";
+
+    const KEY_TARGETS: &str = "targets";
+    const DESC_TARGETS: &str = "# Target platforms to build apps for (Rust target-triple syntax)\n";
+
+    const KEY_DIST: &str = "dist";
+    const DESC_DIST: &str =
+        "# Whether to consider the binaries in a package for distribution (defaults true)\n";
+
+    const KEY_INCLUDE: &str = "include";
+    const DESC_INCLUDE: &str =
+        "# Extra static files to include in each App (path relative to this Cargo.toml's dir)\n";
+
+    const KEY_AUTO_INCLUDE: &str = "auto-includes";
+    const DESC_AUTO_INCLUDE: &str =
+        "# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)\n";
+
+    const KEY_WIN_ARCHIVE: &str = "windows-archive";
+    const DESC_WIN_ARCHIVE: &str =
+        "# The archive format to use for windows builds (defaults .zip)\n";
+
+    const KEY_UNIX_ARCHIVE: &str = "unix-archive";
+    const DESC_UNIX_ARCHIVE: &str =
+        "# The archive format to use for non-windows builds (defaults .tar.xz)\n";
+
+    const KEY_NPM_SCOPE: &str = "npm-scope";
+    const DESC_NPM_SCOPE: &str =
+        "# A namespace to use when publishing this package to the npm registry\n";
+
+    let workspace = workspace_toml["workspace"].or_insert(toml_edit::table());
+    if let Some(t) = workspace.as_table_mut() {
+        t.set_implicit(true)
+    }
+    let metadata = workspace["metadata"].or_insert(toml_edit::table());
+    if let Some(t) = metadata.as_table_mut() {
+        t.set_implicit(true)
+    }
+    let dist_metadata = &mut metadata[METADATA_DIST];
+
+    // If there's no table, make one
+    if !dist_metadata.is_table() {
+        *dist_metadata = toml_edit::table();
+    }
+
+    // Apply formatted/commented values
+    let table = dist_metadata.as_table_mut().unwrap();
+    if let Some(val) = &meta.cargo_dist_version {
+        table.insert(KEY_DIST_VERSION, value(val.to_string()));
+        table
+            .key_decor_mut(KEY_DIST_VERSION)
+            .unwrap()
+            .set_prefix(DESC_DIST_VERSION);
+    }
+
+    if let Some(val) = &meta.rust_toolchain_version {
+        table.insert(KEY_RUST_VERSION, value(val));
+        table
+            .key_decor_mut(KEY_RUST_VERSION)
+            .unwrap()
+            .set_prefix(DESC_RUST_VERSION);
+    }
+
+    if !meta.ci.is_empty() {
+        table.insert(
+            KEY_CI,
+            Item::Value(
+                meta.ci
+                    .iter()
+                    .map(|ci| match ci {
+                        CiStyle::Github => "github",
+                    })
+                    .collect(),
+            ),
+        );
+        table.key_decor_mut(KEY_CI).unwrap().set_prefix(DESC_CI);
+    }
+
+    if let Some(val) = &meta.installers {
+        table.insert(
+            KEY_INSTALLERS,
+            Item::Value(
+                val.iter()
+                    .map(|installer| match installer {
+                        InstallerStyle::Powershell => "powershell",
+                        InstallerStyle::Shell => "shell",
+                        InstallerStyle::Npm => "npm",
+                    })
+                    .collect(),
+            ),
+        );
+        table
+            .key_decor_mut(KEY_INSTALLERS)
+            .unwrap()
+            .set_prefix(DESC_INSTALLERS);
+    }
+
+    if let Some(val) = &meta.targets {
+        table.insert(KEY_TARGETS, Item::Value(val.iter().collect()));
+        table
+            .key_decor_mut(KEY_TARGETS)
+            .unwrap()
+            .set_prefix(DESC_TARGETS);
+    }
+
+    if let Some(val) = meta.dist {
+        table.insert(KEY_DIST, value(val));
+        table.key_decor_mut(KEY_DIST).unwrap().set_prefix(DESC_DIST);
+    }
+
+    if !meta.include.is_empty() {
+        table.insert(
+            KEY_INCLUDE,
+            Item::Value(meta.include.iter().map(ToString::to_string).collect()),
+        );
+        table
+            .key_decor_mut(KEY_INCLUDE)
+            .unwrap()
+            .set_prefix(DESC_INCLUDE);
+    }
+
+    if let Some(val) = meta.auto_includes {
+        table.insert(KEY_AUTO_INCLUDE, value(val));
+        table
+            .key_decor_mut(KEY_AUTO_INCLUDE)
+            .unwrap()
+            .set_prefix(DESC_AUTO_INCLUDE);
+    }
+
+    if let Some(val) = meta.windows_archive {
+        table.insert(KEY_WIN_ARCHIVE, value(val.ext()));
+        table
+            .key_decor_mut(KEY_WIN_ARCHIVE)
+            .unwrap()
+            .set_prefix(DESC_WIN_ARCHIVE);
+    }
+
+    if let Some(val) = meta.unix_archive {
+        table.insert(KEY_UNIX_ARCHIVE, value(val.ext()));
+        table
+            .key_decor_mut(KEY_UNIX_ARCHIVE)
+            .unwrap()
+            .set_prefix(DESC_UNIX_ARCHIVE);
+    }
+
+    if let Some(val) = &meta.npm_scope {
+        table.insert(KEY_NPM_SCOPE, value(val));
+        table
+            .key_decor_mut(KEY_NPM_SCOPE)
+            .unwrap()
+            .set_prefix(DESC_NPM_SCOPE);
+    }
+
+    table
+        .decor_mut()
+        .set_prefix("\n# Config for 'cargo dist'\n");
+}
diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs
index 6473c5b8..a9aec55b 100644
--- a/cargo-dist/src/lib.rs
+++ b/cargo-dist/src/lib.rs
@@ -14,11 +14,9 @@ use std::{
     collections::{BTreeMap, HashMap},
     fs::File,
     io::BufReader,
-    ops::Not,
     process::Command,
 };
 
-use axoproject::{errors::AxoprojectError, WorkspaceInfo};
 use camino::{Utf8Path, Utf8PathBuf};
 use cargo_dist_schema::{Asset, AssetKind, DistManifest, ExecutableAsset, Release};
 use flate2::{write::ZlibEncoder, Compression, GzBuilder};
@@ -28,11 +26,13 @@ use xz2::write::XzEncoder;
 use zip::ZipWriter;
 
 use errors::*;
+pub use init::{do_init, InitArgs};
 use miette::{miette, Context, IntoDiagnostic};
 pub use tasks::*;
 
 pub mod ci;
 pub mod errors;
+mod init;
 pub mod installer;
 pub mod tasks;
 #[cfg(test)]
@@ -595,564 +595,6 @@ fn really_zip_dir(src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> {
     Ok(())
 }
 
-/// Arguments for `cargo dist init` ([`do_init`][])
-#[derive(Debug)]
-pub struct InitArgs {}
-
-/// Run 'cargo dist init'
-pub fn do_init(cfg: &Config, _args: &InitArgs) -> Result<()> {
-    let workspace = tasks::get_project()?;
-
-    // Load in the workspace toml to edit and write back
-    let mut workspace_toml = tasks::load_root_cargo_toml(&workspace.manifest_path)?;
-
-    let check = console::style("✔".to_string()).for_stderr().green();
-    // Init things
-    let mut did_anything = false;
-    eprintln!("first let's setup your cargo build profile...");
-    if init_dist_profile(cfg, &mut workspace_toml)? {
-        eprintln!("{check} added [profile.dist] to your root Cargo.toml");
-        did_anything = true;
-    } else {
-        eprintln!("{check} [profile.dist] already exists");
-    }
-    eprintln!();
-
-    eprintln!("next let's setup your cargo-dist config...");
-    eprintln!();
-    let (worked, gen_ci) = init_dist_metadata(cfg, &workspace, &mut workspace_toml)?;
-    if worked {
-        eprintln!("{check} added [workspace.metadata.dist] to your root Cargo.toml");
-        did_anything = true;
-    } else {
-        eprintln!("{check} [workspace.metadata.dist] already exists");
-    }
-    eprintln!();
-
-    if did_anything {
-        use std::io::Write;
-        let mut workspace_toml_file = File::options()
-            .write(true)
-            .truncate(true)
-            .open(&workspace.manifest_path)
-            .into_diagnostic()
-            .wrap_err("couldn't load root workspace Cargo.toml")?;
-        write!(&mut workspace_toml_file, "{workspace_toml}")
-            .into_diagnostic()
-            .wrap_err("failed to write to Cargo.toml")?;
-        workspace_toml_file
-            .flush()
-            .into_diagnostic()
-            .wrap_err("failed to write to Cargo.toml")?;
-    }
-
-    eprintln!("{check} cargo-dist is setup!");
-    eprintln!();
-
-    if gen_ci {
-        eprintln!("running 'cargo dist generate-ci' to apply any changes to your CI scripts");
-        eprintln!();
-
-        let ci_args = GenerateCiArgs {};
-        do_generate_ci(cfg, &ci_args)?;
-    }
-    Ok(())
-}
-
-fn init_dist_profile(_cfg: &Config, workspace_toml: &mut toml_edit::Document) -> Result<bool> {
-    let profiles = workspace_toml["profile"].or_insert(toml_edit::table());
-    if let Some(t) = profiles.as_table_mut() {
-        t.set_implicit(true)
-    }
-    let dist_profile = &mut profiles[PROFILE_DIST];
-    if !dist_profile.is_none() {
-        return Ok(false);
-    }
-    let mut new_profile = toml_edit::table();
-    {
-        // For some detailed discussion, see: https://github.com/axodotdev/cargo-dist/issues/118
-        let new_profile = new_profile.as_table_mut().unwrap();
-        // We're building for release, so this is a good base!
-        new_profile.insert("inherits", toml_edit::value("release"));
-        // We're building for SUPER DUPER release, so lto is a good idea to enable!
-        //
-        // There's a decent argument for lto=true (aka "fat") here but the cost-benefit
-        // is a bit complex. Fat LTO can be way more expensive to compute (to the extent
-        // that enormous applications like chromium can become unbuildable), but definitely
-        // eeks out a bit more from your binaries.
-        //
-        // In principle cargo-dist is targetting True Shippable Binaries and so it's
-        // worth it to go nuts getting every last drop out of your binaries... but a lot
-        // of people are going to build binaries that might never even be used, so really
-        // we're just burning a bunch of CI time for nothing.
-        //
-        // The user has the freedom to crank this up higher (and/or set codegen-units=1)
-        // if they think it's worth it, but we otherwise probably shouldn't set the planet
-        // on fire just because Number Theoretically Go Up.
-        new_profile.insert("lto", toml_edit::value("thin"));
-        new_profile
-            .decor_mut()
-            .set_prefix("\n# The profile that 'cargo dist' will build with\n")
-    }
-    dist_profile.or_insert(new_profile);
-
-    Ok(true)
-}
-
-/// Initialize [workspace.metadata.dist] with default values based on what was passed on the CLI
-///
-/// Returns whether the initialization was actually done
-/// and whether ci was set
-fn init_dist_metadata(
-    cfg: &Config,
-    workspace_info: &WorkspaceInfo,
-    workspace_toml: &mut toml_edit::Document,
-) -> DistResult<(bool, bool)> {
-    use dialoguer::{Confirm, Input, MultiSelect};
-    use toml_edit::{value, Item};
-    // Setup [workspace.metadata.dist]
-    let workspace = workspace_toml["workspace"].or_insert(toml_edit::table());
-    if let Some(t) = workspace.as_table_mut() {
-        t.set_implicit(true)
-    }
-    let metadata = workspace["metadata"].or_insert(toml_edit::table());
-    if let Some(t) = metadata.as_table_mut() {
-        t.set_implicit(true)
-    }
-    let dist_metadata = &mut metadata[METADATA_DIST];
-    let mut meta = if dist_metadata.is_none() {
-        DistMetadata {
-            // If they init with this version we're gonna try to stick to it!
-            cargo_dist_version: Some(std::env!("CARGO_PKG_VERSION").parse().unwrap()),
-            // latest stable release at this precise moment
-            // maybe there's something more clever we can do here, but, *shrug*
-            rust_toolchain_version: Some("1.67.1".to_owned()),
-            ci: vec![],
-            installers: None,
-            targets: cfg.targets.is_empty().not().then(|| cfg.targets.clone()),
-            dist: None,
-            include: vec![],
-            auto_includes: None,
-            windows_archive: None,
-            unix_archive: None,
-            npm_scope: None,
-        }
-    } else {
-        tasks::parse_metadata_table(workspace_info.cargo_metadata_table.as_ref())
-    };
-
-    // Clone this to simplify checking for settings changes
-    let orig_meta = meta.clone();
-
-    // Keys/descriptions
-    const KEY_RUST_VERSION: &str = "rust-toolchain-version";
-    const DESC_RUST_VERSION: &str =
-        "# The preferred Rust toolchain to use in CI (rustup toolchain syntax)\n";
-
-    const KEY_DIST_VERSION: &str = "cargo-dist-version";
-    const DESC_DIST_VERSION: &str =
-        "# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)\n";
-
-    const KEY_CI: &str = "ci";
-    const DESC_CI: &str = "# CI backends to support (see 'cargo dist generate-ci')\n";
-
-    const KEY_INSTALLERS: &str = "installers";
-    const DESC_INSTALLERS: &str = "# The installers to generate for each app\n";
-
-    const KEY_TARGETS: &str = "targets";
-    const DESC_TARGETS: &str = "# Target platforms to build apps for (Rust target-triple syntax)\n";
-
-    const KEY_DIST: &str = "dist";
-    const DESC_DIST: &str =
-        "# Whether to consider the binaries in a package for distribution (defaults true)\n";
-
-    const KEY_INCLUDE: &str = "include";
-    const DESC_INCLUDE: &str =
-        "# Extra static files to include in each App (path relative to this Cargo.toml's dir)\n";
-
-    const KEY_AUTO_INCLUDE: &str = "auto-includes";
-    const DESC_AUTO_INCLUDE: &str =
-        "# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)\n";
-
-    const KEY_WIN_ARCHIVE: &str = "windows-archive";
-    const DESC_WIN_ARCHIVE: &str =
-        "# The archive format to use for windows builds (defaults .zip)\n";
-
-    const KEY_UNIX_ARCHIVE: &str = "unix-archive";
-    const DESC_UNIX_ARCHIVE: &str =
-        "# The archive format to use for non-windows builds (defaults .tar.xz)\n";
-
-    const KEY_NPM_SCOPE: &str = "npm-scope";
-    const DESC_NPM_SCOPE: &str =
-        "# A namespace to use when publishing this package to the npm registry\n";
-
-    // Now prompt the user interactively to initialize these...
-
-    // Tune the theming a bit
-    let theme = dialoguer::theme::ColorfulTheme {
-        checked_item_prefix: console::style("  [x]".to_string()).for_stderr().green(),
-        unchecked_item_prefix: console::style("  [ ]".to_string()).for_stderr().dim(),
-        active_item_style: console::Style::new().for_stderr().cyan().bold(),
-        ..dialoguer::theme::ColorfulTheme::default()
-    };
-    // Some indicators we'll use in a few places
-    let check = console::style("✔".to_string()).for_stderr().green();
-    let notice = console::style("⚠️".to_string()).for_stderr().yellow();
-
-    // Set cargo-dist-version
-    let current_version: Version = std::env!("CARGO_PKG_VERSION").parse().unwrap();
-    if let Some(desired_version) = &meta.cargo_dist_version {
-        if desired_version != &current_version && !desired_version.pre.starts_with("github-") {
-            let prompt = format!(
-                r#"update your project to this version of cargo-dist?
-    {} => {}"#,
-                desired_version, current_version
-            );
-            if Confirm::with_theme(&theme)
-                .with_prompt(prompt)
-                .default(true)
-                .interact()?
-            {
-                meta.cargo_dist_version = Some(current_version);
-            } else {
-                return Err(DistError::NoUpdateVersion {
-                    project_version: desired_version.clone(),
-                    running_version: current_version,
-                })?;
-            }
-            eprintln!();
-        }
-    } else {
-        let prompt = format!(
-            r#"looks like you deleted the cargo-dist-version key, add it back?
-    this is the version of cargo-dist your releases should use
-    (you're currently running {})"#,
-            current_version
-        );
-        if Confirm::with_theme(&theme)
-            .with_prompt(prompt)
-            .default(true)
-            .interact()?
-        {
-            meta.cargo_dist_version = Some(current_version);
-        } else {
-            // Not recommended but technically ok...
-        }
-        eprintln!();
-    }
-
-    // Enable CI backends
-    {
-        // FIXME: when there is more than one option this should be a proper
-        // multiselect like the installer selector is! For now we do
-        // most of the multi-select logic and then just give a prompt.
-        let known = &[CiStyle::Github];
-        let mut defaults = vec![];
-        let mut keys = vec![];
-        let mut github_key = 0;
-        for item in known {
-            // If this CI style is in their config, keep it
-            // If they passed it on the CLI, flip it on
-            let mut default = meta.ci.contains(item) || cfg.ci.contains(item);
-
-            // If they have a well-defined repo url and it's github, default enable it
-            #[allow(irrefutable_let_patterns)]
-            if let CiStyle::Github = item {
-                github_key = 0;
-                if let Some(repo_url) = &workspace_info.repository_url {
-                    if repo_url.contains("github.com") {
-                        default = true;
-                    }
-                }
-            }
-            defaults.push(default);
-            // This match is here to remind you to add new CiStyles
-            // to `known` above!
-            keys.push(match item {
-                CiStyle::Github => "github",
-            });
-        }
-
-        // Prompt the user
-        let prompt = r#"enable Github CI integration?
-    this creates a CI action which automates creating a Github Release,
-    builds all your binaries/archives, and then uploads them to the Release
-    it also unlocks the ability to generate installers which fetch those artifacts"#;
-        let github_selected = Confirm::with_theme(&theme)
-            .with_prompt(prompt)
-            .default(defaults[github_key])
-            .interact()?;
-
-        let selected = if github_selected {
-            vec![github_key]
-        } else {
-            vec![]
-        };
-
-        // Apply the results
-        meta.ci = selected.into_iter().map(|i| known[i]).collect();
-        eprintln!();
-    }
-
-    // Enforce repository url right away
-    if meta.ci.contains(&CiStyle::Github) && workspace_info.repository_url.is_none() {
-        // If axoproject complained about inconsistency, forward that
-        // Massively jank manual implementation of "clone" here because lots of error types
-        // (like std::io::Error) don't implement Clone and so axoproject errors can't either
-        let conflict = workspace_info.warnings.iter().find_map(|w| {
-            if let AxoprojectError::InconsistentRepositoryKey {
-                file1,
-                url1,
-                file2,
-                url2,
-            } = w
-            {
-                Some(AxoprojectError::InconsistentRepositoryKey {
-                    file1: file1.clone(),
-                    url1: url1.clone(),
-                    file2: file2.clone(),
-                    url2: url2.clone(),
-                })
-            } else {
-                None
-            }
-        });
-        if let Some(inner) = conflict {
-            return Err(DistError::CantEnableGithubUrlInconsistent { inner })?;
-        } else {
-            // Otherwise assume no URL
-            return Err(DistError::CantEnableGithubNoUrl)?;
-        }
-    }
-
-    // Enable installer backends (if they have a CI backend that can provide URLs)
-    // In the future, "vendored" installers like MSIs could be enabled in this situation!
-    if !meta.ci.is_empty() {
-        let known = &[
-            InstallerStyle::Shell,
-            InstallerStyle::Powershell,
-            InstallerStyle::Npm,
-        ];
-        let mut defaults = vec![];
-        let mut keys = vec![];
-        for item in known {
-            // If this CI style is in their config, keep it
-            // If they passed it on the CLI, flip it on
-            let config_had_it = meta
-                .installers
-                .as_deref()
-                .unwrap_or_default()
-                .contains(item);
-            let cli_had_it = cfg.installers.contains(item);
-
-            let default = config_had_it || cli_had_it;
-            defaults.push(default);
-
-            // This match is here to remind you to add new InstallerStyles
-            // to `known` above!
-            keys.push(match item {
-                InstallerStyle::Shell => "shell",
-                InstallerStyle::Powershell => "powershell",
-                InstallerStyle::Npm => "npm",
-            });
-        }
-
-        // Prompt the user
-        let prompt = r#"enable generating installers?
-    installers streamline fetching your app's prebuilt artifacts
-    see the docs for details on each one
-    (select with arrow keys and space, submit with enter)"#;
-        let selected = MultiSelect::with_theme(&theme)
-            .items(&keys)
-            .defaults(&defaults)
-            .with_prompt(prompt)
-            .interact()?;
-
-        // Apply the results
-        meta.installers = Some(selected.into_iter().map(|i| known[i]).collect());
-        eprintln!();
-    } else {
-        eprintln!("{notice} no CI backends enabled, skipping installers");
-        eprintln!();
-    }
-
-    // Special handling of the npm installer
-    if meta
-        .installers
-        .as_deref()
-        .unwrap_or_default()
-        .contains(&InstallerStyle::Npm)
-    {
-        // If npm is being newly enabled here, prompt for a @scope
-        let npm_is_new = !orig_meta
-            .installers
-            .as_deref()
-            .unwrap_or_default()
-            .contains(&InstallerStyle::Npm);
-        if npm_is_new {
-            let prompt = r#"you've enabled npm support, please enter the @scope you want to use
-    this is the "namespace" the package will be published under
-    (leave blank to publish globally)"#;
-            let scope: String = Input::with_theme(&theme)
-                .with_prompt(prompt)
-                .allow_empty(true)
-                .validate_with(|v: &String| {
-                    let v = v.trim();
-                    if v.is_empty() {
-                        Ok(())
-                    } else if let Some(v) = v.strip_prefix('@') {
-                        if v.is_empty() {
-                            Err("@ must be followed by something")
-                        } else {
-                            Ok(())
-                        }
-                    } else {
-                        Err("npm scopes must start with @")
-                    }
-                })
-                .interact_text()?;
-            let scope = scope.trim();
-            if scope.is_empty() {
-                eprintln!("{check} npm packages will be published globally");
-                meta.npm_scope = None;
-            } else {
-                meta.npm_scope = Some(scope.to_owned());
-                eprintln!("{check} npm packages will be published under {scope}");
-            }
-            eprintln!();
-        }
-
-        // FIXME (#226): If they have an npm installer, force on tar.gz compression
-        const TAR_GZ: Option<ZipStyle> = Some(ZipStyle::Tar(CompressionImpl::Gzip));
-        if meta.unix_archive != TAR_GZ || meta.windows_archive != TAR_GZ {
-            let prompt = r#"the npm installer requires binaries to be distributed as .tar.gz, is that ok?
-    otherwise we would distribute your binaries as .zip on windows, .tar.xz everywhere else
-    (this is a hopefully temporary limitation of the npm installer's implementation)"#;
-            let force_targz = Confirm::with_theme(&theme)
-                .with_prompt(prompt)
-                .default(true)
-                .interact()?;
-            if force_targz {
-                meta.unix_archive = TAR_GZ;
-                meta.windows_archive = TAR_GZ;
-            } else {
-                return Err(DistError::MustEnableTarGz)?;
-            }
-            eprintln!();
-        }
-    }
-
-    // Ok, we're done getting values, now edit the toml!!!
-
-    // If there's no table, make one
-    if !dist_metadata.is_table() {
-        *dist_metadata = toml_edit::table();
-    }
-
-    // Apply formatted/commented values
-    let table = dist_metadata.as_table_mut().unwrap();
-    if let Some(val) = meta.cargo_dist_version {
-        table.insert(KEY_DIST_VERSION, value(val.to_string()));
-        table
-            .key_decor_mut(KEY_DIST_VERSION)
-            .unwrap()
-            .set_prefix(DESC_DIST_VERSION);
-    }
-    if let Some(val) = meta.rust_toolchain_version {
-        table.insert(KEY_RUST_VERSION, value(val));
-        table
-            .key_decor_mut(KEY_RUST_VERSION)
-            .unwrap()
-            .set_prefix(DESC_RUST_VERSION);
-    }
-    if !meta.ci.is_empty() {
-        table.insert(
-            KEY_CI,
-            Item::Value(
-                meta.ci
-                    .iter()
-                    .map(|ci| match ci {
-                        CiStyle::Github => "github",
-                    })
-                    .collect(),
-            ),
-        );
-        table.key_decor_mut(KEY_CI).unwrap().set_prefix(DESC_CI);
-    }
-    if let Some(val) = meta.installers {
-        table.insert(
-            KEY_INSTALLERS,
-            Item::Value(
-                val.iter()
-                    .map(|installer| match installer {
-                        InstallerStyle::Powershell => "powershell",
-                        InstallerStyle::Shell => "shell",
-                        InstallerStyle::Npm => "npm",
-                    })
-                    .collect(),
-            ),
-        );
-        table
-            .key_decor_mut(KEY_INSTALLERS)
-            .unwrap()
-            .set_prefix(DESC_INSTALLERS);
-    }
-    if let Some(val) = meta.targets {
-        table.insert(KEY_TARGETS, Item::Value(val.into_iter().collect()));
-        table
-            .key_decor_mut(KEY_TARGETS)
-            .unwrap()
-            .set_prefix(DESC_TARGETS);
-    }
-    if let Some(val) = meta.dist {
-        table.insert(KEY_DIST, value(val));
-        table.key_decor_mut(KEY_DIST).unwrap().set_prefix(DESC_DIST);
-    }
-    if !meta.include.is_empty() {
-        table.insert(
-            KEY_INCLUDE,
-            Item::Value(meta.include.iter().map(ToString::to_string).collect()),
-        );
-        table
-            .key_decor_mut(KEY_INCLUDE)
-            .unwrap()
-            .set_prefix(DESC_INCLUDE);
-    }
-    if let Some(val) = meta.auto_includes {
-        table.insert(KEY_AUTO_INCLUDE, value(val));
-        table
-            .key_decor_mut(KEY_AUTO_INCLUDE)
-            .unwrap()
-            .set_prefix(DESC_AUTO_INCLUDE);
-    }
-    if let Some(val) = meta.windows_archive {
-        table.insert(KEY_WIN_ARCHIVE, value(val.ext()));
-        table
-            .key_decor_mut(KEY_WIN_ARCHIVE)
-            .unwrap()
-            .set_prefix(DESC_WIN_ARCHIVE);
-    }
-    if let Some(val) = meta.unix_archive {
-        table.insert(KEY_UNIX_ARCHIVE, value(val.ext()));
-        table
-            .key_decor_mut(KEY_UNIX_ARCHIVE)
-            .unwrap()
-            .set_prefix(DESC_UNIX_ARCHIVE);
-    }
-    if let Some(val) = meta.npm_scope {
-        table.insert(KEY_NPM_SCOPE, value(val));
-        table
-            .key_decor_mut(KEY_NPM_SCOPE)
-            .unwrap()
-            .set_prefix(DESC_NPM_SCOPE);
-    }
-    table
-        .decor_mut()
-        .set_prefix("\n# Config for 'cargo dist'\n");
-
-    Ok((true, !meta.ci.is_empty()))
-}
-
 /// Arguments for `cargo dist generate-ci` ([`do_generate_ci][])
 #[derive(Debug)]
 pub struct GenerateCiArgs {}
diff --git a/cargo-dist/src/main.rs b/cargo-dist/src/main.rs
index dac8c5f0..f8d77d66 100644
--- a/cargo-dist/src/main.rs
+++ b/cargo-dist/src/main.rs
@@ -9,7 +9,7 @@ use camino::Utf8PathBuf;
 use cargo_dist::*;
 use cargo_dist_schema::{AssetKind, DistManifest};
 use clap::Parser;
-use cli::{Cli, Commands, FakeCli, HelpMarkdownArgs, ManifestArgs, OutputFormat};
+use cli::{Cli, Commands, FakeCli, HelpMarkdownArgs, ManifestArgs, OutputFormat, StatusArgs};
 use console::Term;
 use miette::IntoDiagnostic;
 
@@ -28,12 +28,12 @@ fn main() {
 fn real_main(cli: &axocli::CliApp<Cli>) -> Result<(), miette::Report> {
     let config = &cli.config;
     match &config.command {
-        Some(Commands::Init(args)) => cmd_init(config, args),
-        Some(Commands::GenerateCi(args)) => cmd_generate_ci(config, args),
-        Some(Commands::Manifest(args)) => cmd_manifest(config, args),
-        Some(Commands::HelpMarkdown(args)) => cmd_help_md(config, args),
-        Some(Commands::Build(args)) => cmd_dist(config, args),
-        None => cmd_dist(config, &config.build_args),
+        Commands::Init(args) => cmd_init(config, args),
+        Commands::GenerateCi(args) => cmd_generate_ci(config, args),
+        Commands::Manifest(args) => cmd_manifest(config, args),
+        Commands::Status(args) => cmd_status(config, args),
+        Commands::HelpMarkdown(args) => cmd_help_md(config, args),
+        Commands::Build(args) => cmd_dist(config, args),
     }
 }
 
@@ -160,7 +160,21 @@ fn cmd_manifest(cli: &Cli, args: &ManifestArgs) -> Result<(), miette::Report> {
     Ok(())
 }
 
-fn cmd_init(cli: &Cli, _args: &InitArgs) -> Result<(), miette::Report> {
+fn cmd_status(cli: &Cli, _args: &StatusArgs) -> Result<(), miette::Report> {
+    // Force --no-local-paths and --artifacts=all
+    // No need to force --output-format=human
+    let mut new_cli = cli.clone();
+    new_cli.no_local_paths = true;
+    let args = &ManifestArgs {
+        build_args: BuildArgs {
+            artifacts: cli::ArtifactMode::All,
+        },
+    };
+
+    cmd_manifest(&new_cli, args)
+}
+
+fn cmd_init(cli: &Cli, args: &InitArgs) -> Result<(), miette::Report> {
     // This command is more automagic, so provide default targets if none are chosen
     let targets = if cli.target.is_empty() {
         default_desktop_targets()
@@ -176,7 +190,10 @@ fn cmd_init(cli: &Cli, _args: &InitArgs) -> Result<(), miette::Report> {
         installers: cli.installer.iter().map(|ins| ins.to_lib()).collect(),
         announcement_tag: cli.tag.clone(),
     };
-    let args = cargo_dist::InitArgs {};
+    let args = cargo_dist::InitArgs {
+        yes: args.yes,
+        no_generate_ci: args.no_generate_ci,
+    };
     do_init(&config, &args)
 }
 
@@ -277,12 +294,6 @@ fn print_help_markdown(out: &mut dyn Write) -> std::io::Result<()> {
 
                     writeln!(out, "### {heading}")?;
 
-                    if in_normal_options && is_full_command {
-                        writeln!(
-                            out,
-                            "This command is an alias for `cargo dist build`, refer to that\n"
-                        )?;
-                    }
                     if in_global_options && !is_full_command {
                         writeln!(
                             out,


Test patch:
diff --git a/cargo-dist/tests/snapshots/cli_tests__long-help.snap b/cargo-dist/tests/snapshots/cli_tests__long-help.snap
index 28b87ad6..f33c9de4 100644
--- a/cargo-dist/tests/snapshots/cli_tests__long-help.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__long-help.snap
@@ -14,34 +14,17 @@ Commands:
   build
           Build artifacts
   init
-          Initialize default settings in your Cargo.toml
+          Setup or update cargo-dist
   generate-ci
           Generate CI scripts for orchestrating cargo-dist
   manifest
           Generate the final build manifest without running any builds
+  status
+          Get a quick summary of the status of your project
   help
           Print this message or the help of the given subcommand(s)
 
 Options:
-      --artifacts <ARTIFACTS>
-          Which subset of the Artifacts to build
-          
-          Artifacts can be broken up into two major classes: "local" ones, which are made for each target system (executable-zips, symbols, MSIs...); and "global" ones, which are made once per app (curl-sh installers, npm package, metadata...).
-          
-          Having this distinction lets us run cargo-dist independently on multiple machines without collisions between the outputs.
-          
-          If let unspecified, we will pick a fuzzier "host" mode that builds "as much as possible" for the local system. This mode is appropriate for local testing/debugging/demoing. If no --target flags are passed on the CLI then "host" mode will try to intelligently guess which targets to build for, which may include building targets that aren't defined in your metadata.dist config (since that config may exclude the current machine!).
-          
-          The specifics of "host" mode are intentionally unspecified to enable us to provider better out-of-the-box UX for local usage. In CI environments you should always specify "global" or "local" to get consistent behaviour!
-          
-          [default: host]
-
-          Possible values:
-          - local:  Build target-specific artifacts like executable-zips and MSIs
-          - global: Build unique artifacts like curl-sh installers and npm packages
-          - host:   Fuzzily build "as much as possible" for the host system
-          - all:    Build all the artifacts; useful for `cargo dist manifest`
-
   -h, --help
           Print help (see a summary with '-h')
 
@@ -49,13 +32,13 @@ Options:
           Print version
 
 GLOBAL OPTIONS:
-      --verbose <VERBOSE>
+  -v, --verbose <VERBOSE>
           How verbose logging should be (log level)
           
           [default: warn]
           [possible values: off, error, warn, info, debug, trace]
 
-      --output-format <OUTPUT_FORMAT>
+  -o, --output-format <OUTPUT_FORMAT>
           The format of the output
           
           [default: human]
@@ -68,12 +51,12 @@ GLOBAL OPTIONS:
           
           `cargo dist manifest --artifacts=all --output-format=json --no-local-paths`
 
-      --target <TARGET>
+  -t, --target <TARGET>
           Target triples we want to build
           
           If left unspecified we will use the values in [workspace.metadata.dist], except for `cargo dist init` which will select some "good defaults" for you.
 
-      --installer <INSTALLER>
+  -i, --installer <INSTALLER>
           Installers we want to build
           
           If left unspecified we will use the values in [workspace.metadata.dist]. `cargo dist init` will persist the values you pass to that location.
@@ -83,7 +66,7 @@ GLOBAL OPTIONS:
           - powershell: Generates a powershell script that fetches/installs the right build
           - npm:        Generates an npm project that fetches the right build to your node_modules
 
-      --ci <CI>
+  -c, --ci <CI>
           CI we want to support
           
           If left unspecified we will use the value in [workspace.metadata.dist]. `cargo dist init` will persist the values you pass to that location.
diff --git a/cargo-dist/tests/snapshots/cli_tests__markdown-help.snap b/cargo-dist/tests/snapshots/cli_tests__markdown-help.snap
index 7e78d048..98925131 100644
--- a/cargo-dist/tests/snapshots/cli_tests__markdown-help.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__markdown-help.snap
@@ -20,22 +20,21 @@ cargo dist <COMMAND>
 
 ### Commands
 * [build](#cargo-dist-build): Build artifacts
-* [init](#cargo-dist-init): Initialize default settings in your Cargo.toml
+* [init](#cargo-dist-init): Setup or update cargo-dist
 * [generate-ci](#cargo-dist-generate-ci): Generate CI scripts for orchestrating cargo-dist
 * [manifest](#cargo-dist-manifest): Generate the final build manifest without running any builds
+* [status](#cargo-dist-status): Get a quick summary of the status of your project
 * [help](#cargo-dist-help): Print this message or the help of the given subcommand(s)
 
 ### Options
-This command is an alias for `cargo dist build`, refer to that
-
 ### GLOBAL OPTIONS
-#### `--verbose <VERBOSE>`
+#### `-v, --verbose <VERBOSE>`
 How verbose logging should be (log level)
 
 \[default: warn]  
 \[possible values: off, error, warn, info, debug, trace]  
 
-#### `--output-format <OUTPUT_FORMAT>`
+#### `-o, --output-format <OUTPUT_FORMAT>`
 The format of the output
 
 \[default: human]  
@@ -48,12 +47,12 @@ This is useful for generating a clean "full" manifest as follows:
 
 `cargo dist manifest --artifacts=all --output-format=json --no-local-paths`
 
-#### `--target <TARGET>`
+#### `-t, --target <TARGET>`
 Target triples we want to build
 
 If left unspecified we will use the values in [workspace.metadata.dist], except for `cargo dist init` which will select some "good defaults" for you.
 
-#### `--installer <INSTALLER>`
+#### `-i, --installer <INSTALLER>`
 Installers we want to build
 
 If left unspecified we will use the values in [workspace.metadata.dist]. `cargo dist init` will persist the values you pass to that location.
@@ -63,7 +62,7 @@ Possible values:
 - powershell: Generates a powershell script that fetches/installs the right build
 - npm:        Generates an npm project that fetches the right build to your node_modules
 
-#### `--ci <CI>`
+#### `-c, --ci <CI>`
 CI we want to support
 
 If left unspecified we will use the value in [workspace.metadata.dist]. `cargo dist init` will persist the values you pass to that location.
@@ -97,7 +96,7 @@ cargo dist build [OPTIONS]
 ```
 
 ### Options
-#### `--artifacts <ARTIFACTS>`
+#### `-a, --artifacts <ARTIFACTS>`
 Which subset of the Artifacts to build
 
 Artifacts can be broken up into two major classes: "local" ones, which are made for each target system (executable-zips, symbols, MSIs...); and "global" ones, which are made once per app (curl-sh installers, npm package, metadata...).
@@ -124,7 +123,9 @@ This subcommand accepts all the [global options](#global-options)
 
 <br><br><br>
 ## cargo dist init
-Initialize default settings in your Cargo.toml
+Setup or update cargo-dist
+
+This will interactively guide you through the process of selecting configuration options and will also automatically run 'cargo dist generate-ci' afterwards as necessary. It will also handle updating your project to a new version of cargo-dist if you're running one.
 
 ### Usage:
 
@@ -133,6 +134,14 @@ cargo dist init [OPTIONS]
 ```
 
 ### Options
+#### `-y, --yes`
+Automatically accept all recommended/default values
+
+This is equivalent to just mashing ENTER over and over during the interactive prompts.
+
+#### `--no-generate-ci`
+Don't automatically invoke 'cargo dist generate-ci' at the end
+
 #### `-h, --help`
 Print help (see a summary with '-h')
 
@@ -171,7 +180,7 @@ cargo dist manifest [OPTIONS]
 ```
 
 ### Options
-#### `--artifacts <ARTIFACTS>`
+#### `-a, --artifacts <ARTIFACTS>`
 Which subset of the Artifacts to build
 
 Artifacts can be broken up into two major classes: "local" ones, which are made for each target system (executable-zips, symbols, MSIs...); and "global" ones, which are made once per app (curl-sh installers, npm package, metadata...).
@@ -196,6 +205,29 @@ Print help (see a summary with '-h')
 ### GLOBAL OPTIONS
 This subcommand accepts all the [global options](#global-options)
 
+<br><br><br>
+## cargo dist status
+Get a quick summary of the status of your project
+
+If you want to know what running your cargo-dist CI will produce, this is the command for you! It should run the exact same logic and do some basic integrity checks.
+
+### This is roughly an alias for
+
+cargo dist manifest --artifacts=all --no-local-paths
+
+### Usage:
+
+```
+cargo dist status [OPTIONS]
+```
+
+### Options
+#### `-h, --help`
+Print help (see a summary with '-h')
+
+### GLOBAL OPTIONS
+This subcommand accepts all the [global options](#global-options)
+
 <br><br><br>
 ## cargo dist help
 Print this message or the help of the given subcommand(s)
@@ -208,9 +240,10 @@ cargo dist help [COMMAND]
 
 ### Commands
 * [build](#cargo-dist-build): Build artifacts
-* [init](#cargo-dist-init): Initialize default settings in your Cargo.toml
+* [init](#cargo-dist-init): Setup or update cargo-dist
 * [generate-ci](#cargo-dist-generate-ci): Generate CI scripts for orchestrating cargo-dist
 * [manifest](#cargo-dist-manifest): Generate the final build manifest without running any builds
+* [status](#cargo-dist-status): Get a quick summary of the status of your project
 * [help](#cargo-dist-help): Print this message or the help of the given subcommand(s)
 
 
diff --git a/cargo-dist/tests/snapshots/cli_tests__short-help.snap b/cargo-dist/tests/snapshots/cli_tests__short-help.snap
index af069161..06279068 100644
--- a/cargo-dist/tests/snapshots/cli_tests__short-help.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__short-help.snap
@@ -10,23 +10,23 @@ Usage: cargo dist [OPTIONS]
 
 Commands:
   build        Build artifacts
-  init         Initialize default settings in your Cargo.toml
+  init         Setup or update cargo-dist
   generate-ci  Generate CI scripts for orchestrating cargo-dist
   manifest     Generate the final build manifest without running any builds
+  status       Get a quick summary of the status of your project
   help         Print this message or the help of the given subcommand(s)
 
 Options:
-      --artifacts <ARTIFACTS>  Which subset of the Artifacts to build [default: host] [possible values: local, global, host, all]
-  -h, --help                   Print help (see more with '--help')
-  -V, --version                Print version
+  -h, --help     Print help (see more with '--help')
+  -V, --version  Print version
 
 GLOBAL OPTIONS:
-      --verbose <VERBOSE>              How verbose logging should be (log level) [default: warn] [possible values: off, error, warn, info, debug, trace]
-      --output-format <OUTPUT_FORMAT>  The format of the output [default: human] [possible values: human, json]
+  -v, --verbose <VERBOSE>              How verbose logging should be (log level) [default: warn] [possible values: off, error, warn, info, debug, trace]
+  -o, --output-format <OUTPUT_FORMAT>  The format of the output [default: human] [possible values: human, json]
       --no-local-paths                 Strip local paths from output (e.g. in the dist manifest json)
-      --target <TARGET>                Target triples we want to build
-      --installer <INSTALLER>          Installers we want to build [possible values: shell, powershell, npm]
-      --ci <CI>                        CI we want to support [possible values: github]
+  -t, --target <TARGET>                Target triples we want to build
+  -i, --installer <INSTALLER>          Installers we want to build [possible values: shell, powershell, npm]
+  -c, --ci <CI>                        CI we want to support [possible values: github]
       --tag <TAG>                      The (git) tag to use for the Announcement that each invocation of cargo-dist is performing
 
 stderr: