From fc8f8f1725dbc839c09997c7ccc89921014e49f3 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 10 Jun 2026 23:45:30 +0000 Subject: [PATCH 01/19] consolidate cot cli commands into one --- Cargo.lock | 2 + cot-cli/Cargo.toml | 2 + cot-cli/src/args.rs | 25 +++++ cot-cli/src/handlers.rs | 84 +++++++++++++++ cot-cli/src/lib.rs | 1 + cot-cli/src/main.rs | 16 ++- cot-cli/src/project.rs | 232 ++++++++++++++++++++++++++++++++++++++++ cot-cli/src/utils.rs | 6 +- cot/src/cli.rs | 4 + cot/src/lib.rs | 1 + cot/src/metadata.rs | 43 ++++++++ cot/src/project.rs | 6 ++ 12 files changed, 419 insertions(+), 3 deletions(-) create mode 100644 cot-cli/src/project.rs create mode 100644 cot/src/metadata.rs diff --git a/Cargo.lock b/Cargo.lock index 3eb78ef6b..d4a94936c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -987,6 +987,8 @@ dependencies = [ "proc-macro2", "quote", "rand 0.10.1", + "serde", + "serde_json", "syn", "tempfile", "tracing", diff --git a/cot-cli/Cargo.toml b/cot-cli/Cargo.toml index 084349e49..c858279c2 100644 --- a/cot-cli/Cargo.toml +++ b/cot-cli/Cargo.toml @@ -42,6 +42,8 @@ quote.workspace = true syn.workspace = true tracing.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } +serde = { workspace = true, features = ["derive"] } +serde_json = {workspace = true} [dev-dependencies] cot-cli = { path = ".", features = ["test_utils"] } diff --git a/cot-cli/src/args.rs b/cot-cli/src/args.rs index 1e35ceec8..a2383a5c6 100644 --- a/cot-cli/src/args.rs +++ b/cot-cli/src/args.rs @@ -1,3 +1,4 @@ +use std::ffi::OsString; use std::path::PathBuf; use clap::{Args, Parser, Subcommand}; @@ -11,6 +12,11 @@ use clap_verbosity_flag::Verbosity; long_about = None )] pub struct Cli { + #[arg(long, global = true)] + release: bool, + /// Package to use, in case you're running this in a workspace + #[arg(short = 'p', long, global = true, value_name = "PACKAGE")] + pub package: Option, #[command(flatten)] pub verbose: Verbosity, #[command(subcommand)] @@ -29,6 +35,9 @@ pub enum Commands { /// Manage Cot CLI #[command(subcommand)] Cli(CliCommands), + + #[command(external_subcommand)] + External(Vec), } #[derive(Debug, Args)] @@ -119,3 +128,19 @@ pub struct CompletionsArgs { /// Shell to generate completions for pub shell: clap_complete::Shell, } + +/// Pulls `-p ` / `--package ` / `--package=` out of raw +/// argv, before clap has parsed anything. Needed because `project::load` +/// must run before `Cli::parse()` for the `--help` interception path. +pub fn extract_package_arg(raw: &[String]) -> Option { + let mut iter = raw.iter(); + while let Some(arg) = iter.next() { + if let Some(value) = arg.strip_prefix("--package=") { + return Some(value.to_string()); + } + if arg == "--package" || arg == "-p" { + return iter.next().cloned(); + } + } + None +} diff --git a/cot-cli/src/handlers.rs b/cot-cli/src/handlers.rs index 23b34fb90..ea8f5c084 100644 --- a/cot-cli/src/handlers.rs +++ b/cot-cli/src/handlers.rs @@ -1,7 +1,10 @@ +use std::ffi::OsString; +use std::os::unix::process::CommandExt; use std::path::PathBuf; use anyhow::Context; use clap::CommandFactory; +use cot::metadata::CommandMeta; use crate::args::{ Cli, CompletionsArgs, ManpagesArgs, MigrationListArgs, MigrationMakeArgs, MigrationNewArgs, @@ -11,6 +14,7 @@ use crate::migration_generator::{ MigrationGeneratorOptions, create_new_migration, list_migrations, make_migrations, }; use crate::new_project::{CotSource, new_project}; +use crate::project::ProjectBinary; pub fn handle_new_project( ProjectNewArgs { path, name, source }: ProjectNewArgs, @@ -95,6 +99,86 @@ pub fn handle_cli_completions(CompletionsArgs { shell }: CompletionsArgs) -> any Ok(()) } +pub fn handle_external( + args: Vec, + project: Option, + _release: bool, +) -> anyhow::Result<()> { + let subcmd = args[0].to_string_lossy(); + + let Some(proj) = project else { + anyhow::bail!( + "Unknown command `{subcmd}` and no project binary was found in target/.\n\ + Hint: run `cargo build` first, or `cargo build --release` with --release." + ); + }; + + let known = proj + .metadata + .commands + .iter() + .any(|c| c.name == subcmd.as_ref() || c.aliases.iter().any(|a| a == subcmd.as_ref())); + + if !known { + anyhow::bail!( + "Unknown command `{subcmd}`.\n\ + Run `cot --help` to see all available commands." + ); + } + + exec(proj, args) +} + +fn exec(proj: ProjectBinary, args: Vec) -> anyhow::Result<()> { + #[cfg(unix)] + { + let err = std::process::Command::new(&proj.path).args(&args).exec(); + anyhow::bail!("Failed to exec {}: {err}", proj.path.display()); + } + + #[cfg(not(unix))] + { + let status = std::process::Command::new(&proj.path) + .args(&args) + .status()?; + std::process::exit(status.code().unwrap_or(1)); + } +} + +/// Build a fresh [`clap::Command`] and inject the project's subcommands into +/// it before printing. +pub fn handle_combined_help(project: Option<&ProjectBinary>) -> anyhow::Result<()> { + let mut cmd = Cli::command(); + + if let Some(proj) = project { + for meta_cmd in &proj.metadata.commands { + cmd = cmd.subcommand(build_clap_subcommand(meta_cmd)); + } + } + + cmd.print_long_help()?; + println!(); + Ok(()) +} + +fn build_clap_subcommand(meta: &CommandMeta) -> clap::Command { + let mut cmd = clap::Command::new(&meta.name); + + if let Some(about) = &meta.about { + cmd = cmd.about(about.clone()); + } + + for alias in &meta.aliases { + cmd = cmd.visible_alias(alias.clone()); + } + + for sub in &meta.subcommands { + cmd = cmd.subcommand(build_clap_subcommand(sub)); + } + + cmd +} + fn generate_completions(shell: clap_complete::Shell, writer: &mut impl std::io::Write) { clap_complete::generate(shell, &mut Cli::command(), "cot", writer); } diff --git a/cot-cli/src/lib.rs b/cot-cli/src/lib.rs index 5c23e7383..c76021490 100644 --- a/cot-cli/src/lib.rs +++ b/cot-cli/src/lib.rs @@ -4,6 +4,7 @@ pub mod args; pub mod handlers; pub mod migration_generator; pub mod new_project; +pub mod project; #[cfg(feature = "test_utils")] pub mod test_utils; mod utils; diff --git a/cot-cli/src/main.rs b/cot-cli/src/main.rs index c80f98271..6f14b6ce7 100644 --- a/cot-cli/src/main.rs +++ b/cot-cli/src/main.rs @@ -1,11 +1,22 @@ #![allow(unreachable_pub)] // triggers false positives because we have both a binary and library use clap::Parser; -use cot_cli::args::{Cli, CliCommands, Commands, MigrationCommands}; -use cot_cli::handlers; +use cot_cli::args::{Cli, CliCommands, Commands, MigrationCommands, extract_package_arg}; +use cot_cli::{handlers, project}; use tracing_subscriber::util::SubscriberInitExt; fn main() -> anyhow::Result<()> { + let raw: Vec = std::env::args().collect(); + let release = raw.iter().any(|a| a == "--release"); + let package = extract_package_arg(&raw); + + let project = project::load(&std::env::current_dir()?, release, package.as_deref())?; + + if matches!(raw.as_slice(), [_, flag] if flag == "--help" || flag == "-h") { + handlers::handle_combined_help(project.as_ref())?; + return Ok(()); + } + let cli = Cli::parse(); tracing_subscriber::fmt() @@ -27,5 +38,6 @@ fn main() -> anyhow::Result<()> { MigrationCommands::Make(args) => handlers::handle_migration_make(args), MigrationCommands::New(args) => handlers::handle_migration_new(args), }, + Commands::External(args) => handlers::handle_external(args, project, release), } } diff --git a/cot-cli/src/project.rs b/cot-cli/src/project.rs new file mode 100644 index 000000000..4d4bc7cb4 --- /dev/null +++ b/cot-cli/src/project.rs @@ -0,0 +1,232 @@ +use std::path::{Path, PathBuf}; +use std::time::SystemTime; + +use anyhow::{Context, bail}; +use cargo_toml::Manifest; +use cot::metadata::{METADATA_FLAG, ProjectMetadata}; +use serde::{Deserialize, Serialize}; + +use crate::utils::{CargoTomlManager, PackageManager, WorkspaceManager}; + +#[derive(Serialize, Deserialize)] +struct Cache { + binary_mtime_secs: u64, + metadata: ProjectMetadata, +} + +const CACHE_FILE_NAME: &str = ".command-cache.json"; + +pub struct ProjectBinary { + pub path: PathBuf, + pub metadata: ProjectMetadata, +} + +/// Find and load the project binary and its metadata. +/// +/// `package` corresponds to `cot -p ...` or `--package `, +/// mirroring `cargo`'s flag. It's required when run from a workspace root +/// (or any directory that doesn't unambiguously belong to one package) and +/// the workspace has more than one member. +pub fn load( + path: &Path, + release: bool, + package: Option<&str>, +) -> anyhow::Result> { + let Some(manager) = CargoTomlManager::from_path(path)? else { + return Ok(None); + }; + + let (package_manager, target_dir_root): (&PackageManager, PathBuf) = match &manager { + CargoTomlManager::Package(pm) => { + let dir = pm.get_package_path().to_path_buf(); + (pm, dir) + } + CargoTomlManager::Workspace(wm) => { + let pm = resolve_workspace_package(wm, package)?; + (pm, wm.get_workspace_root().to_path_buf()) + } + }; + + let project_dir = package_manager.get_package_path(); + let binary_name = resolve_binary_name(package_manager)?; + let target_dir = resolve_target_dir(&target_dir_root); + let profile = if release { "release" } else { "debug" }; + + #[cfg(target_os = "windows")] + let binary_name = format!("{binary_name}.exe"); + + let binary_path = target_dir.join(profile).join(binary_name); + + if !binary_path.exists() { + return Ok(None); + } + + let cache_path = project_dir.join(CACHE_FILE_NAME); + let metadata = load_or_refresh_metadata(&binary_path, &cache_path).with_context(|| { + format!( + "unable to load metadata from binary `{}`", + binary_path.display() + ) + })?; + + Ok(Some(ProjectBinary { + path: binary_path, + metadata, + })) +} + +fn resolve_workspace_package<'a>( + wm: &'a WorkspaceManager, + package: Option<&str>, +) -> anyhow::Result<&'a PackageManager> { + if let Some(name) = package { + return wm.get_package_manager(name).with_context(|| { + format!( + "package `{name}` not found in workspace.\nAvailable packages: {}", + available_packages(wm) + ) + }); + } + + if let Some(pm) = wm.get_current_package_manager() { + return Ok(pm); + } + + bail!( + "multiple packages found in the workspace; specify which one to use with `-p `.\n\ + Available packages: {}", + available_packages(wm) + ) +} + +fn available_packages(wm: &WorkspaceManager) -> String { + wm.get_packages() + .iter() + .map(|p| p.get_package_name()) + .collect::>() + .join(", ") +} + +/// Resolve the binary name for a package: +/// +/// 1. `[package.metadata.cot] binary = "..."` — explicit override, useful when +/// a crate has multiple `[[bin]]` targets +/// 2. A single `[[bin]]` entry — use its name +/// 3. Fall back to the package name (cargo's default when there's no explicit +/// `[[bin]]` and `src/main.rs` exists) +fn resolve_binary_name(package_manager: &PackageManager) -> anyhow::Result { + let manifest: &Manifest = package_manager.get_manifest(); + + if let Some(package) = &manifest.package { + if let Some(metadata) = &package.metadata { + if let Some(name) = metadata + .get("cot") + .and_then(|c| c.get("binary")) + .and_then(|b| b.as_str()) + { + return Ok(name.to_string()); + } + } + } + + let named_bins: Vec<&str> = manifest + .bin + .iter() + .filter_map(|b| b.name.as_deref()) + .collect(); + + match named_bins.len() { + 0 => {} + 1 => return Ok(named_bins[0].to_string()), + _ => bail!( + "package `{}` has multiple [[bin]] targets.\n\ + Specify which one `cot` should use by adding to its Cargo.toml:\n\ + \n\ + [package.metadata.cot]\n\ + binary = \"your-binary-name\"", + package_manager.get_package_name(), + ), + } + + manifest + .package + .as_ref() + .map(|p| p.name.clone()) + .context("Cargo.toml has no [package] section and no [[bin]] targets") +} + +fn resolve_target_dir(start_dir: &Path) -> PathBuf { + let mut dir = start_dir; + loop { + let candidate = dir.join("target"); + if candidate.exists() { + return candidate; + } + match dir.parent() { + Some(parent) => dir = parent, + None => break, + } + } + start_dir.join("target") +} + +fn load_or_refresh_metadata( + binary_path: &Path, + cache_path: &Path, +) -> anyhow::Result { + let current_mtime_secs = mtime_secs(binary_path)?; + + if let Ok(bytes) = std::fs::read(cache_path) { + if let Ok(cache) = serde_json::from_slice::(&bytes) { + if cache.binary_mtime_secs == current_mtime_secs { + return Ok(cache.metadata); + } + } + } + + let output = std::process::Command::new(binary_path) + .arg(METADATA_FLAG) + .output() + .with_context(|| format!("Failed to spawn {}", binary_path.display()))?; + + if !output.status.success() { + bail!( + "Binary `{}` exited with status {} when queried for metadata.", + binary_path.display(), + output.status, + ); + } + + let metadata: ProjectMetadata = serde_json::from_slice(&output.stdout).with_context(|| { + format!( + "Binary `{}` returned invalid JSON for {METADATA_FLAG}", + binary_path.display() + ) + })?; + + write_cache( + cache_path, + &Cache { + binary_mtime_secs: current_mtime_secs, + metadata: metadata.clone(), + }, + )?; + + Ok(metadata) +} + +fn mtime_secs(path: &Path) -> anyhow::Result { + let metadata = path.metadata()?; + Ok(metadata + .modified()? + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs()) +} + +fn write_cache(cache_path: &Path, cache: &Cache) -> anyhow::Result<()> { + if let Some(parent) = cache_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(cache_path, serde_json::to_string(cache)?)?; + Ok(()) +} diff --git a/cot-cli/src/utils.rs b/cot-cli/src/utils.rs index 6ec4cbcf8..dbbc65ebe 100644 --- a/cot-cli/src/utils.rs +++ b/cot-cli/src/utils.rs @@ -258,6 +258,10 @@ impl WorkspaceManager { self.package_manifests.get(package_name) } + pub(crate) fn get_workspace_root(&self) -> &Path { + self.workspace_root.as_path() + } + #[cfg(test)] pub(crate) fn get_package_manager_by_path( &self, @@ -295,7 +299,7 @@ impl PackageManager { path.to_owned() } - #[cfg(test)] + // #[cfg(test)] pub(crate) fn get_manifest(&self) -> &Manifest { &self.manifest } diff --git a/cot/src/cli.rs b/cot/src/cli.rs index 97652a2e1..e36b0905e 100644 --- a/cot/src/cli.rs +++ b/cot/src/cli.rs @@ -175,6 +175,10 @@ impl Cli { self.tasks.insert(Some(name), Box::new(task)); } + pub fn command(&self) -> &Command { + &self.command + } + #[must_use] pub(crate) fn common_options(&mut self) -> CommonOptions { let matches = self.command.get_matches_mut(); diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 1479cf2df..1d5f10e49 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -69,6 +69,7 @@ pub mod config; #[cfg(feature = "email")] pub mod email; mod error_page; +pub mod metadata; pub mod middleware; #[cfg(feature = "openapi")] pub mod openapi; diff --git a/cot/src/metadata.rs b/cot/src/metadata.rs new file mode 100644 index 000000000..a5a7362f2 --- /dev/null +++ b/cot/src/metadata.rs @@ -0,0 +1,43 @@ +use clap::Command; +use serde::{Deserialize, Serialize}; + +pub const METADATA_FLAG: &str = "--metadata"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ProjectMetadata { + // pub version: u32, + pub binary_name: String, + pub commands: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CommandMeta { + pub name: String, + pub about: Option, + pub aliases: Vec, + pub subcommands: Vec, +} + +pub fn extract(cmd: &Command) -> ProjectMetadata { + ProjectMetadata { + binary_name: cmd.get_name().to_string(), + commands: cmd + .get_subcommands() + .filter(|subcmd| !subcmd.is_hide_set()) + .map(extract_command) + .collect(), + } +} + +fn extract_command(cmd: &Command) -> CommandMeta { + CommandMeta { + name: cmd.get_name().to_string(), + about: cmd.get_about().map(|s| s.to_string()), + aliases: cmd.get_all_aliases().map(|s| s.to_string()).collect(), + subcommands: cmd + .get_subcommands() + .filter(|subcmd| !subcmd.is_hide_set()) + .map(extract_command) + .collect(), + } +} diff --git a/cot/src/project.rs b/cot/src/project.rs index e71f57597..860f64b16 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -939,6 +939,12 @@ impl Bootstrapper { cli.set_metadata(self.project.cli_metadata()); self.project.register_tasks(&mut cli); + if std::env::args().any(|arg| arg == cot::metadata::METADATA_FLAG) { + let meta = cot::metadata::extract(cli.command()); + println!("{}", serde_json::to_string_pretty(&meta).unwrap()); + std::process::exit(0); + } + let common_options = cli.common_options(); let self_with_context = self.with_config_name(common_options.config())?; From 683b2a651f287f0bb6407233b459a856542352b7 Mon Sep 17 00:00:00 2001 From: Elijah Date: Thu, 11 Jun 2026 00:16:11 +0000 Subject: [PATCH 02/19] remove dead code --- cot-cli/src/utils.rs | 1 - cot/src/metadata.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/cot-cli/src/utils.rs b/cot-cli/src/utils.rs index dbbc65ebe..e0a42abd1 100644 --- a/cot-cli/src/utils.rs +++ b/cot-cli/src/utils.rs @@ -299,7 +299,6 @@ impl PackageManager { path.to_owned() } - // #[cfg(test)] pub(crate) fn get_manifest(&self) -> &Manifest { &self.manifest } diff --git a/cot/src/metadata.rs b/cot/src/metadata.rs index a5a7362f2..31327e351 100644 --- a/cot/src/metadata.rs +++ b/cot/src/metadata.rs @@ -5,7 +5,6 @@ pub const METADATA_FLAG: &str = "--metadata"; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ProjectMetadata { - // pub version: u32, pub binary_name: String, pub commands: Vec, } From 8a0c6b8b77935ad482ab2eaaacb70f0a8af99785 Mon Sep 17 00:00:00 2001 From: Elijah Date: Thu, 18 Jun 2026 23:09:42 +0000 Subject: [PATCH 03/19] - improve error messages. - help for workspaces and packages now dispatch to the custom help handler --- cot-cli/src/args.rs | 10 ++++++++-- cot-cli/src/main.rs | 31 +++++++++++++++++++++++++++---- cot-cli/src/project.rs | 39 +++++++++++++++++++++++++++++---------- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/cot-cli/src/args.rs b/cot-cli/src/args.rs index a2383a5c6..67bf61bb5 100644 --- a/cot-cli/src/args.rs +++ b/cot-cli/src/args.rs @@ -4,6 +4,12 @@ use std::path::PathBuf; use clap::{Args, Parser, Subcommand}; use clap_verbosity_flag::Verbosity; +pub const PACKAGE_LONG_FLAG: &str = "--package"; +pub const PACKAGE_SHORT_FLAG: &str = "-p"; +pub const RELEASE_FLAG: &str = "--release"; +pub const HELP_LONG_FLAG: &str = "--help"; +pub const HELP_SHORT_FLAG: &str = "-h"; + #[derive(Debug, Parser)] #[command( name = "cot", @@ -135,10 +141,10 @@ pub struct CompletionsArgs { pub fn extract_package_arg(raw: &[String]) -> Option { let mut iter = raw.iter(); while let Some(arg) = iter.next() { - if let Some(value) = arg.strip_prefix("--package=") { + if let Some(value) = arg.strip_prefix(&format!("{PACKAGE_LONG_FLAG}=")) { return Some(value.to_string()); } - if arg == "--package" || arg == "-p" { + if arg == PACKAGE_LONG_FLAG || arg == PACKAGE_SHORT_FLAG { return iter.next().cloned(); } } diff --git a/cot-cli/src/main.rs b/cot-cli/src/main.rs index 6f14b6ce7..b87f5367d 100644 --- a/cot-cli/src/main.rs +++ b/cot-cli/src/main.rs @@ -1,18 +1,41 @@ #![allow(unreachable_pub)] // triggers false positives because we have both a binary and library use clap::Parser; -use cot_cli::args::{Cli, CliCommands, Commands, MigrationCommands, extract_package_arg}; +use cot_cli::args::{ + Cli, CliCommands, Commands, HELP_LONG_FLAG, HELP_SHORT_FLAG, MigrationCommands, + PACKAGE_LONG_FLAG, PACKAGE_SHORT_FLAG, RELEASE_FLAG, extract_package_arg, +}; use cot_cli::{handlers, project}; use tracing_subscriber::util::SubscriberInitExt; +fn is_top_level_help(args: &[String]) -> bool { + if !args + .iter() + .any(|a| a == HELP_LONG_FLAG || a == HELP_SHORT_FLAG) + { + return false; + } + + let mut rest = args.iter().skip(1).peekable(); + while let Some(arg) = rest.next() { + match arg.as_str() { + HELP_LONG_FLAG | HELP_SHORT_FLAG | RELEASE_FLAG => {} + PACKAGE_SHORT_FLAG | PACKAGE_LONG_FLAG => { + rest.next(); + } + _ => return false, + } + } + true +} + fn main() -> anyhow::Result<()> { let raw: Vec = std::env::args().collect(); - let release = raw.iter().any(|a| a == "--release"); + let release = raw.iter().any(|a| a == RELEASE_FLAG); let package = extract_package_arg(&raw); - let project = project::load(&std::env::current_dir()?, release, package.as_deref())?; - if matches!(raw.as_slice(), [_, flag] if flag == "--help" || flag == "-h") { + if is_top_level_help(&raw) { handlers::handle_combined_help(project.as_ref())?; return Ok(()); } diff --git a/cot-cli/src/project.rs b/cot-cli/src/project.rs index 4d4bc7cb4..dea721c77 100644 --- a/cot-cli/src/project.rs +++ b/cot-cli/src/project.rs @@ -8,6 +8,9 @@ use serde::{Deserialize, Serialize}; use crate::utils::{CargoTomlManager, PackageManager, WorkspaceManager}; +const RELEASE_PROFILE: &str = "release"; +const DEBUG_PROFILE: &str = "debug"; + #[derive(Serialize, Deserialize)] struct Cache { binary_mtime_secs: u64, @@ -50,7 +53,11 @@ pub fn load( let project_dir = package_manager.get_package_path(); let binary_name = resolve_binary_name(package_manager)?; let target_dir = resolve_target_dir(&target_dir_root); - let profile = if release { "release" } else { "debug" }; + let profile = if release { + RELEASE_PROFILE + } else { + DEBUG_PROFILE + }; #[cfg(target_os = "windows")] let binary_name = format!("{binary_name}.exe"); @@ -62,12 +69,10 @@ pub fn load( } let cache_path = project_dir.join(CACHE_FILE_NAME); - let metadata = load_or_refresh_metadata(&binary_path, &cache_path).with_context(|| { - format!( - "unable to load metadata from binary `{}`", - binary_path.display() - ) - })?; + let metadata = load_or_refresh_metadata(&binary_path, &cache_path).context(format!( + "unable to load metadata from binary `{}`", + binary_path.display() + ))?; Ok(Some(ProjectBinary { path: binary_path, @@ -190,17 +195,31 @@ fn load_or_refresh_metadata( .with_context(|| format!("Failed to spawn {}", binary_path.display()))?; if !output.status.success() { - bail!( + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + let mut msg = format!( "Binary `{}` exited with status {} when queried for metadata.", binary_path.display(), output.status, ); + + if !stderr.trim().is_empty() { + msg.push_str(&format!("\n\nstderr:\n{}", stderr.trim())); + } + + if !stdout.trim().is_empty() { + msg.push_str(&format!("\n\nstdout:\n{}", stdout.trim())); + } + bail!(msg); } let metadata: ProjectMetadata = serde_json::from_slice(&output.stdout).with_context(|| { + let raw = String::from_utf8_lossy(&output.stdout); format!( - "Binary `{}` returned invalid JSON for {METADATA_FLAG}", - binary_path.display() + "Binary `{}` returned invalid JSON for {METADATA_FLAG}.\n\nGot:\n{}", + binary_path.display(), + raw.trim(), ) })?; From d73cd1086411333c2b2c48bbedddae5bf08869b1 Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 23 Jun 2026 01:12:55 +0000 Subject: [PATCH 04/19] unit tests initial --- cot-cli/src/args.rs | 51 +++ cot-cli/src/handlers.rs | 108 ++++- cot-cli/src/main.rs | 57 ++- cot-cli/src/project.rs | 388 ++++++++++++++++++ ...apshot_testing__cli__completions_bash.snap | 90 +++- ...shot_testing__cli__completions_elvish.snap | 27 ++ ...apshot_testing__cli__completions_fish.snap | 20 +- ..._testing__cli__completions_powershell.snap | 27 ++ ...napshot_testing__cli__completions_zsh.snap | 27 ++ .../cli__snapshot_testing__cli__no_args.snap | 8 +- .../cli__snapshot_testing__help__help.snap | 10 +- ...t_testing__help__help_cli_completions.snap | 8 +- ...shot_testing__help__help_cli_manpages.snap | 4 +- ...napshot_testing__help__help_migration.snap | 8 +- ...ot_testing__help__help_migration_list.snap | 8 +- ...ot_testing__help__help_migration_make.snap | 4 +- ...cli__snapshot_testing__help__help_new.snap | 6 +- ...li__snapshot_testing__help__long_help.snap | 22 +- .../cli__snapshot_testing__help__no_args.snap | 10 +- ...i__snapshot_testing__help__short_help.snap | 22 +- cot/src/cli.rs | 1 + cot/src/metadata.rs | 62 +++ 22 files changed, 919 insertions(+), 49 deletions(-) diff --git a/cot-cli/src/args.rs b/cot-cli/src/args.rs index 67bf61bb5..72170dd9c 100644 --- a/cot-cli/src/args.rs +++ b/cot-cli/src/args.rs @@ -150,3 +150,54 @@ pub fn extract_package_arg(raw: &[String]) -> Option { } None } + +#[cfg(test)] +mod tests { + use super::*; + + fn args(raw: &[&str]) -> Vec { + raw.iter().map(|arg| (*arg).to_string()).collect() + } + + #[test] + fn extract_package_arg_long_with_separate_value() { + let raw = args(&["cot", "--release", "--package", "blog", "check"]); + + assert_eq!(extract_package_arg(&raw), Some("blog".to_string())); + } + + #[test] + fn extract_package_arg_long_with_equals_value() { + let raw = args(&["cot", "--package=blog", "check"]); + + assert_eq!(extract_package_arg(&raw), Some("blog".to_string())); + } + + #[test] + fn extract_package_arg_short_with_value() { + let raw = args(&["cot", "-p", "blog", "check"]); + + assert_eq!(extract_package_arg(&raw), Some("blog".to_string())); + } + + #[test] + fn extract_package_arg_returns_first_package_flag() { + let raw = args(&["cot", "-p", "first", "--package", "second", "check"]); + + assert_eq!(extract_package_arg(&raw), Some("first".to_string())); + } + + #[test] + fn extract_package_arg_missing_value_returns_none() { + let raw = args(&["cot", "check", "-p"]); + + assert_eq!(extract_package_arg(&raw), None); + } + + #[test] + fn extract_package_arg_absent_returns_none() { + let raw = args(&["cot", "--release", "check"]); + + assert_eq!(extract_package_arg(&raw), None); + } +} diff --git a/cot-cli/src/handlers.rs b/cot-cli/src/handlers.rs index ea8f5c084..b2a2a8758 100644 --- a/cot-cli/src/handlers.rs +++ b/cot-cli/src/handlers.rs @@ -109,7 +109,7 @@ pub fn handle_external( let Some(proj) = project else { anyhow::bail!( "Unknown command `{subcmd}` and no project binary was found in target/.\n\ - Hint: run `cargo build` first, or `cargo build --release` with --release." + Hint: run `cargo build` first, or `cargo build --release`." ); }; @@ -148,6 +148,14 @@ fn exec(proj: ProjectBinary, args: Vec) -> anyhow::Result<()> { /// Build a fresh [`clap::Command`] and inject the project's subcommands into /// it before printing. pub fn handle_combined_help(project: Option<&ProjectBinary>) -> anyhow::Result<()> { + let mut cmd = combined_help_command(project); + + cmd.print_long_help()?; + println!(); + Ok(()) +} + +fn combined_help_command(project: Option<&ProjectBinary>) -> clap::Command { let mut cmd = Cli::command(); if let Some(proj) = project { @@ -156,9 +164,7 @@ pub fn handle_combined_help(project: Option<&ProjectBinary>) -> anyhow::Result<( } } - cmd.print_long_help()?; - println!(); - Ok(()) + cmd } fn build_clap_subcommand(meta: &CommandMeta) -> clap::Command { @@ -264,4 +270,98 @@ mod tests { assert!(!output.is_empty()); } + + #[test] + fn external_command_without_project_reports_build_hint() { + let result = handle_external(vec![OsString::from("serve")], None, false); + + assert!(result.is_err()); + let message = result.unwrap_err().to_string(); + assert!(message.contains("Unknown command `serve`")); + assert!(message.contains("run `cargo build` first")); + } + + #[test] + fn external_command_unknown_to_project_reports_unknown_command() { + let project = ProjectBinary { + path: PathBuf::from("target/debug/example"), + metadata: cot::metadata::ProjectMetadata { + binary_name: "example".to_string(), + commands: vec![CommandMeta { + name: "check".to_string(), + about: None, + aliases: vec![], + subcommands: vec![], + }], + }, + }; + + let result = handle_external(vec![OsString::from("foo")], Some(project), false); + + assert!(result.is_err()); + let message = result.unwrap_err().to_string(); + assert!(message.contains("Unknown command `foo`")); + assert!(message.contains("cot --help")); + } + + #[test] + fn build_clap_subcommand_preserves_about_aliases_and_nested_subcommands() { + let meta = CommandMeta { + name: "migration".to_string(), + about: Some("Migration commands".to_string()), + aliases: vec!["database".to_string()], + subcommands: vec![CommandMeta { + name: "rollback".to_string(), + about: Some("Rollback migrations".to_string()), + aliases: vec!["rbk".to_string()], + subcommands: vec![], + }], + }; + + let cmd = build_clap_subcommand(&meta); + + assert_eq!(cmd.get_name(), "migration"); + assert_eq!(cmd.get_about().unwrap().to_string(), "Migration commands"); + assert!(cmd.get_all_aliases().any(|alias| alias == "database")); + let nested = cmd + .get_subcommands() + .find(|subcommand| subcommand.get_name() == "rollback") + .unwrap(); + assert_eq!( + nested.get_about().unwrap().to_string(), + "Rollback migrations" + ); + assert!(nested.get_all_aliases().any(|alias| alias == "rbk")); + } + + #[test] + fn combined_help_command_includes_project_commands_and_builtin_commands() { + let project = ProjectBinary { + path: PathBuf::from("target/debug/example"), + metadata: cot::metadata::ProjectMetadata { + binary_name: "example".to_string(), + commands: vec![CommandMeta { + name: "health".to_string(), + about: Some("Check the server health".to_string()), + aliases: vec![], + subcommands: vec![], + }], + }, + }; + + let cmd = combined_help_command(Some(&project)); + + assert!( + cmd.get_subcommands() + .any(|subcommand| subcommand.get_name() == "new") + ); + let health = cmd + .get_subcommands() + .find(|subcommand| subcommand.get_name() == "health") + .unwrap(); + assert_eq!( + health.get_about().unwrap().to_string(), + "Check the server health" + ); + } } diff --git a/cot-cli/src/main.rs b/cot-cli/src/main.rs index b87f5367d..8c9085662 100644 --- a/cot-cli/src/main.rs +++ b/cot-cli/src/main.rs @@ -21,7 +21,12 @@ fn is_top_level_help(args: &[String]) -> bool { match arg.as_str() { HELP_LONG_FLAG | HELP_SHORT_FLAG | RELEASE_FLAG => {} PACKAGE_SHORT_FLAG | PACKAGE_LONG_FLAG => { - rest.next(); + let Some(value) = rest.next() else { + return false; + }; + if value.starts_with('-') { + return false; + } } _ => return false, } @@ -33,9 +38,9 @@ fn main() -> anyhow::Result<()> { let raw: Vec = std::env::args().collect(); let release = raw.iter().any(|a| a == RELEASE_FLAG); let package = extract_package_arg(&raw); - let project = project::load(&std::env::current_dir()?, release, package.as_deref())?; if is_top_level_help(&raw) { + let project = project::load(&std::env::current_dir()?, release, package.as_deref())?; handlers::handle_combined_help(project.as_ref())?; return Ok(()); } @@ -61,6 +66,52 @@ fn main() -> anyhow::Result<()> { MigrationCommands::Make(args) => handlers::handle_migration_make(args), MigrationCommands::New(args) => handlers::handle_migration_new(args), }, - Commands::External(args) => handlers::handle_external(args, project, release), + Commands::External(args) => { + let project = project::load(&std::env::current_dir()?, release, package.as_deref())?; + handlers::handle_external(args, project, release) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn args(raw: &[&str]) -> Vec { + raw.iter().map(|arg| (*arg).to_string()).collect() + } + + #[test] + fn top_level_help_accepts_only_global_flags() { + assert!(is_top_level_help(&args(&["cot", "--help"]))); + assert!(is_top_level_help(&args(&["cot", "-h"]))); + assert!(is_top_level_help(&args(&[ + "cot", + "--release", + "-p", + "blog", + "--help" + ]))); + assert!(is_top_level_help(&args(&[ + "cot", + "--package", + "blog", + "-h", + "--release" + ]))); + } + + #[test] + fn top_level_help_rejects_subcommands_and_non_help_invocations() { + assert!(!is_top_level_help(&args(&["cot"]))); + assert!(!is_top_level_help(&args(&["cot", "migration", "--help"]))); + assert!(!is_top_level_help(&args(&["cot", "serve", "-h"]))); + assert!(!is_top_level_help(&args(&["cot", "--version"]))); + } + + #[test] + fn top_level_help_treats_missing_package_value_as_not_top_level_help() { + assert!(!is_top_level_help(&args(&["cot", "-p", "--help"]))); + assert!(!is_top_level_help(&args(&["cot", "--package", "-h"]))); } } diff --git a/cot-cli/src/project.rs b/cot-cli/src/project.rs index dea721c77..2e9d0e07f 100644 --- a/cot-cli/src/project.rs +++ b/cot-cli/src/project.rs @@ -19,6 +19,7 @@ struct Cache { const CACHE_FILE_NAME: &str = ".command-cache.json"; +#[derive(Debug)] pub struct ProjectBinary { pub path: PathBuf, pub metadata: ProjectMetadata, @@ -68,6 +69,10 @@ pub fn load( return Ok(None); } + if is_current_executable(&binary_path) { + return Ok(None); + } + let cache_path = project_dir.join(CACHE_FILE_NAME); let metadata = load_or_refresh_metadata(&binary_path, &cache_path).context(format!( "unable to load metadata from binary `{}`", @@ -80,6 +85,21 @@ pub fn load( })) } +fn is_current_executable(binary_path: &Path) -> bool { + let Ok(current_exe) = std::env::current_exe() else { + return false; + }; + + let Ok(binary_path) = binary_path.canonicalize() else { + return false; + }; + let Ok(current_exe) = current_exe.canonicalize() else { + return false; + }; + + binary_path == current_exe +} + fn resolve_workspace_package<'a>( wm: &'a WorkspaceManager, package: Option<&str>, @@ -249,3 +269,371 @@ fn write_cache(cache_path: &Path, cache: &Cache) -> anyhow::Result<()> { std::fs::write(cache_path, serde_json::to_string(cache)?)?; Ok(()) } + +#[cfg(test)] +mod tests { + use std::fs; + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + + use cot::metadata::CommandMeta; + use tempfile::TempDir; + + use super::*; + + fn write_package_manifest(package_dir: &Path, package_name: &str, extra: &str) { + fs::create_dir_all(package_dir).unwrap(); + fs::write( + package_dir.join("Cargo.toml"), + format!( + r#"[package] +name = "{package_name}" +version = "0.1.0" +edition = "2024" + +{extra}"# + ), + ) + .unwrap(); + } + + fn write_workspace_manifest(workspace_dir: &Path, members: &[&str]) { + fs::write( + workspace_dir.join("Cargo.toml"), + format!( + "[workspace]\nresolver = \"3\"\nmembers = [{}]\n", + members + .iter() + .map(|member| format!("\"{member}\"")) + .collect::>() + .join(", ") + ), + ) + .unwrap(); + } + + fn command(name: &str) -> CommandMeta { + CommandMeta { + name: name.to_string(), + about: None, + aliases: vec![], + subcommands: vec![], + } + } + + fn metadata(binary_name: &str, command_names: &[&str]) -> ProjectMetadata { + ProjectMetadata { + binary_name: binary_name.to_string(), + commands: command_names.iter().map(|name| command(name)).collect(), + } + } + + #[cfg(unix)] + fn write_metadata_script(path: &Path, metadata: &ProjectMetadata) { + let json = serde_json::to_string(metadata).unwrap(); + write_shell_script(path, &format!("printf '%s\\n' '{json}'\n")); + } + + #[cfg(unix)] + fn write_shell_script(path: &Path, body: &str) { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, format!("#!/bin/sh\n{body}")).unwrap(); + let mut permissions = fs::metadata(path).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions).unwrap(); + } + + #[test] + fn load_returns_none_without_cargo_manifest() { + let temp_dir = TempDir::new().unwrap(); + + let result = load(temp_dir.path(), false, None).unwrap(); + + assert!(result.is_none()); + } + + #[test] + fn load_errors_when_start_path_does_not_exist() { + let temp_dir = TempDir::new().unwrap(); + + let result = load(&temp_dir.path().join("missing"), false, None); + + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("path does not exist") + ); + } + + #[test] + fn load_returns_none_when_expected_binary_is_missing() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest(temp_dir.path(), "demo", ""); + + let result = load(temp_dir.path(), false, None).unwrap(); + + assert!(result.is_none()); + } + + #[test] + #[cfg(unix)] + fn load_reads_debug_binary_metadata_and_writes_cache() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest(temp_dir.path(), "demo", ""); + let binary_path = temp_dir.path().join("target/debug/demo"); + write_metadata_script(&binary_path, &metadata("demo", &["serve"])); + + let project = load(temp_dir.path(), false, None).unwrap().unwrap(); + + assert_eq!(project.path, binary_path); + assert_eq!(project.metadata.binary_name, "demo"); + assert_eq!(project.metadata.commands[0].name, "serve"); + assert!(temp_dir.path().join(CACHE_FILE_NAME).exists()); + } + + #[test] + #[cfg(unix)] + fn load_uses_release_profile_when_requested() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest(temp_dir.path(), "demo", ""); + let binary_path = temp_dir.path().join("target/release/demo"); + write_metadata_script(&binary_path, &metadata("demo", &["serve"])); + + let project = load(temp_dir.path(), true, None).unwrap().unwrap(); + + assert_eq!(project.path, binary_path); + } + + #[test] + #[cfg(unix)] + fn load_uses_single_named_bin_target() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest( + temp_dir.path(), + "demo", + r#"[[bin]] +name = "server" +path = "src/server.rs" +"#, + ); + let binary_path = temp_dir.path().join("target/debug/server"); + write_metadata_script(&binary_path, &metadata("server", &["serve"])); + + let project = load(temp_dir.path(), false, None).unwrap().unwrap(); + + assert_eq!(project.path, binary_path); + assert_eq!(project.metadata.binary_name, "server"); + } + + #[test] + #[cfg(unix)] + fn load_uses_metadata_binary_override_before_bin_targets() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest( + temp_dir.path(), + "demo", + r#"[package.metadata.cot] +binary = "api" + +[[bin]] +name = "api" +path = "src/api.rs" + +[[bin]] +name = "worker" +path = "src/worker.rs" +"#, + ); + let binary_path = temp_dir.path().join("target/debug/api"); + write_metadata_script(&binary_path, &metadata("api", &["serve"])); + + let project = load(temp_dir.path(), false, None).unwrap().unwrap(); + + assert_eq!(project.path, binary_path); + assert_eq!(project.metadata.binary_name, "api"); + } + + #[test] + fn load_errors_on_multiple_bin_targets_without_override() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest( + temp_dir.path(), + "demo", + r#"[[bin]] +name = "api" +path = "src/api.rs" + +[[bin]] +name = "worker" +path = "src/worker.rs" +"#, + ); + + let result = load(temp_dir.path(), false, None); + + assert!(result.is_err()); + let message = result.unwrap_err().to_string(); + assert!(message.contains("multiple [[bin]] targets")); + assert!(message.contains("[package.metadata.cot]")); + } + + #[test] + fn workspace_root_requires_package_when_ambiguous() { + let temp_dir = TempDir::new().unwrap(); + write_workspace_manifest(temp_dir.path(), &["api", "web"]); + write_package_manifest(&temp_dir.path().join("api"), "api", ""); + write_package_manifest(&temp_dir.path().join("web"), "web", ""); + + let result = load(temp_dir.path(), false, None); + + assert!(result.is_err()); + let message = result.unwrap_err().to_string(); + assert!(message.contains("multiple packages found")); + assert!(message.contains("api")); + assert!(message.contains("web")); + } + + #[test] + fn workspace_package_flag_must_match_member() { + let temp_dir = TempDir::new().unwrap(); + write_workspace_manifest(temp_dir.path(), &["api", "web"]); + write_package_manifest(&temp_dir.path().join("api"), "api", ""); + write_package_manifest(&temp_dir.path().join("web"), "web", ""); + + let result = load(temp_dir.path(), false, Some("missing")); + + assert!(result.is_err()); + let message = result.unwrap_err().to_string(); + assert!(message.contains("package `missing` not found")); + assert!(message.contains("api")); + assert!(message.contains("web")); + } + + #[test] + #[cfg(unix)] + fn workspace_root_uses_selected_package_and_workspace_target_dir() { + let temp_dir = TempDir::new().unwrap(); + write_workspace_manifest(temp_dir.path(), &["api", "web"]); + write_package_manifest(&temp_dir.path().join("api"), "api", ""); + write_package_manifest(&temp_dir.path().join("web"), "web", ""); + let binary_path = temp_dir.path().join("target/debug/api"); + write_metadata_script(&binary_path, &metadata("api", &["check"])); + + let project = load(temp_dir.path(), false, Some("api")).unwrap().unwrap(); + + assert_eq!(project.path, binary_path); + assert!(temp_dir.path().join("api").join(CACHE_FILE_NAME).exists()); + } + + #[test] + #[cfg(unix)] + fn workspace_member_directory_uses_current_package_without_flag() { + let temp_dir = TempDir::new().unwrap(); + write_workspace_manifest(temp_dir.path(), &["api", "web"]); + write_package_manifest(&temp_dir.path().join("api"), "api", ""); + write_package_manifest(&temp_dir.path().join("web"), "web", ""); + let binary_path = temp_dir.path().join("target/debug/web"); + write_metadata_script(&binary_path, &metadata("web", &["check"])); + + let project = load(&temp_dir.path().join("web"), false, None) + .unwrap() + .unwrap(); + + assert_eq!(project.path, binary_path); + assert_eq!(project.metadata.binary_name, "web"); + } + + #[test] + #[cfg(unix)] + fn load_reuses_valid_cache_without_spawning_binary() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest(temp_dir.path(), "demo", ""); + let binary_path = temp_dir.path().join("target/debug/demo"); + write_shell_script( + &binary_path, + "echo 'binary should not be queried' >&2\nexit 42\n", + ); + let cache = Cache { + binary_mtime_secs: mtime_secs(&binary_path).unwrap(), + metadata: metadata("demo", &["cached"]), + }; + write_cache(&temp_dir.path().join(CACHE_FILE_NAME), &cache).unwrap(); + + let project = load(temp_dir.path(), false, None).unwrap().unwrap(); + + assert_eq!(project.metadata.commands[0].name, "cached"); + } + + #[test] + #[cfg(unix)] + fn load_refreshes_stale_cache() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest(temp_dir.path(), "demo", ""); + let binary_path = temp_dir.path().join("target/debug/demo"); + write_metadata_script(&binary_path, &metadata("demo", &["fresh"])); + let cache = Cache { + binary_mtime_secs: 0, + metadata: metadata("demo", &["stale"]), + }; + write_cache(&temp_dir.path().join(CACHE_FILE_NAME), &cache).unwrap(); + + let project = load(temp_dir.path(), false, None).unwrap().unwrap(); + + assert_eq!(project.metadata.commands[0].name, "fresh"); + } + + #[test] + #[cfg(unix)] + fn load_reports_metadata_command_failure_with_output() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest(temp_dir.path(), "demo", ""); + let binary_path = temp_dir.path().join("target/debug/demo"); + write_shell_script( + &binary_path, + "echo stdout message\necho stderr message >&2\nexit 42\n", + ); + + let result = load(temp_dir.path(), false, None); + + assert!(result.is_err()); + let message = format!("{:#}", result.unwrap_err()); + assert!(message.contains("unable to load metadata")); + assert!(message.contains("exited with status")); + assert!(message.contains("stdout message")); + assert!(message.contains("stderr message")); + } + + #[test] + #[cfg(unix)] + fn load_reports_invalid_metadata_json() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest(temp_dir.path(), "demo", ""); + let binary_path = temp_dir.path().join("target/debug/demo"); + write_shell_script(&binary_path, "echo 'not json'\n"); + + let result = load(temp_dir.path(), false, None); + + assert!(result.is_err()); + let message = format!("{:#}", result.unwrap_err()); + assert!(message.contains(METADATA_FLAG)); + assert!(message.contains("not json")); + } + + #[test] + fn current_executable_matches_current_process() { + let current_exe = std::env::current_exe().unwrap(); + + assert!(is_current_executable(¤t_exe)); + } + + #[test] + fn current_executable_does_not_match_missing_path() { + let missing = std::env::temp_dir().join("cot-cli-missing-test-binary"); + + assert!(!is_current_executable(&missing)); + } +} diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap index ac47430ce..ea299e473 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap @@ -116,12 +116,20 @@ _cot() { case "${cmd}" in cot) - opts="-v -q -h -V --verbose --quiet --help --version new migration cli help" + opts="-p -v -q -h -V --release --package --verbose --quiet --help --version new migration cli help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -130,12 +138,20 @@ _cot() { return 0 ;; cot__subcmd__cli) - opts="-v -q -h --verbose --quiet --help manpages completions help" + opts="-p -v -q -h --release --package --verbose --quiet --help manpages completions help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -144,12 +160,20 @@ _cot() { return 0 ;; cot__subcmd__cli__subcmd__completions) - opts="-v -q -h --verbose --quiet --help bash elvish fish powershell zsh" + opts="-p -v -q -h --release --package --verbose --quiet --help bash elvish fish powershell zsh" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -214,7 +238,7 @@ _cot() { return 0 ;; cot__subcmd__cli__subcmd__manpages) - opts="-o -c -v -q -h --output-dir --create --verbose --quiet --help" + opts="-o -c -p -v -q -h --output-dir --create --release --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -228,6 +252,14 @@ _cot() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -376,12 +408,20 @@ _cot() { return 0 ;; cot__subcmd__migration) - opts="-v -q -h --verbose --quiet --help list make new help" + opts="-p -v -q -h --release --package --verbose --quiet --help list make new help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -460,12 +500,20 @@ _cot() { return 0 ;; cot__subcmd__migration__subcmd__list) - opts="-v -q -h --verbose --quiet --help [PATH]" + opts="-p -v -q -h --release --package --verbose --quiet --help [PATH]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -474,7 +522,7 @@ _cot() { return 0 ;; cot__subcmd__migration__subcmd__make) - opts="-v -q -h --app-name --output-dir --verbose --quiet --help [PATH]" + opts="-p -v -q -h --app-name --output-dir --release --package --verbose --quiet --help [PATH]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -488,6 +536,14 @@ _cot() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -496,7 +552,7 @@ _cot() { return 0 ;; cot__subcmd__migration__subcmd__new) - opts="-v -q -h --app-name --verbose --quiet --help [PATH]" + opts="-p -v -q -h --app-name --release --package --verbose --quiet --help [PATH]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -506,6 +562,14 @@ _cot() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -514,7 +578,7 @@ _cot() { return 0 ;; cot__subcmd__new) - opts="-v -q -h --name --use-git --cot-path --verbose --quiet --help " + opts="-p -v -q -h --name --use-git --cot-path --release --package --verbose --quiet --help " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -528,6 +592,14 @@ _cot() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap index 66217fe41..61c353c57 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap @@ -30,6 +30,9 @@ set edit:completion:arg-completer[cot] = {|@words| } var completions = [ &'cot'= { + cand -p 'Package to use, in case you''re running this in a workspace' + cand --package 'Package to use, in case you''re running this in a workspace' + cand --release 'release' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -46,7 +49,10 @@ set edit:completion:arg-completer[cot] = {|@words| &'cot;new'= { cand --name 'Set the resulting crate name [default: the directory name]' cand --cot-path 'Use `cot` from the specified path instead of a published crate' + cand -p 'Package to use, in case you''re running this in a workspace' + cand --package 'Package to use, in case you''re running this in a workspace' cand --use-git 'Use the latest `cot` version from git instead of a published crate' + cand --release 'release' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -55,6 +61,9 @@ set edit:completion:arg-completer[cot] = {|@words| cand --help 'Print help' } &'cot;migration'= { + cand -p 'Package to use, in case you''re running this in a workspace' + cand --package 'Package to use, in case you''re running this in a workspace' + cand --release 'release' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -67,6 +76,9 @@ set edit:completion:arg-completer[cot] = {|@words| cand help 'Print this message or the help of the given subcommand(s)' } &'cot;migration;list'= { + cand -p 'Package to use, in case you''re running this in a workspace' + cand --package 'Package to use, in case you''re running this in a workspace' + cand --release 'release' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -77,6 +89,9 @@ set edit:completion:arg-completer[cot] = {|@words| &'cot;migration;make'= { cand --app-name 'Name of the app to use in the migration [default: crate name]' cand --output-dir 'Directory to write the migrations to [default: the migrations/ directory in the crate''s src/ directory]' + cand -p 'Package to use, in case you''re running this in a workspace' + cand --package 'Package to use, in case you''re running this in a workspace' + cand --release 'release' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -86,6 +101,9 @@ set edit:completion:arg-completer[cot] = {|@words| } &'cot;migration;new'= { cand --app-name 'Name of the app to use in the migration (default: crate name)' + cand -p 'Package to use, in case you''re running this in a workspace' + cand --package 'Package to use, in case you''re running this in a workspace' + cand --release 'release' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -108,6 +126,9 @@ set edit:completion:arg-completer[cot] = {|@words| &'cot;migration;help;help'= { } &'cot;cli'= { + cand -p 'Package to use, in case you''re running this in a workspace' + cand --package 'Package to use, in case you''re running this in a workspace' + cand --release 'release' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -121,8 +142,11 @@ set edit:completion:arg-completer[cot] = {|@words| &'cot;cli;manpages'= { cand -o 'Directory to write the manpages to [default: current directory]' cand --output-dir 'Directory to write the manpages to [default: current directory]' + cand -p 'Package to use, in case you''re running this in a workspace' + cand --package 'Package to use, in case you''re running this in a workspace' cand -c 'Create the directory if it doesn''t exist' cand --create 'Create the directory if it doesn''t exist' + cand --release 'release' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -131,6 +155,9 @@ set edit:completion:arg-completer[cot] = {|@words| cand --help 'Print help' } &'cot;cli;completions'= { + cand -p 'Package to use, in case you''re running this in a workspace' + cand --package 'Package to use, in case you''re running this in a workspace' + cand --release 'release' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap index 3ee746569..6efd16476 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap @@ -12,7 +12,7 @@ exit_code: 0 ----- stdout ----- # Print an optspec for argparse to handle cmd's options that are independent of any subcommand. function __fish_cot_global_optspecs - string join \n v/verbose q/quiet h/help V/version + string join \n release p/package= v/verbose q/quiet h/help V/version end function __fish_cot_needs_command @@ -36,6 +36,8 @@ function __fish_cot_using_subcommand contains -- $cmd[1] $argv end +complete -c cot -n "__fish_cot_needs_command" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_needs_command" -l release complete -c cot -n "__fish_cot_needs_command" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_needs_command" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_needs_command" -s h -l help -d 'Print help' @@ -46,10 +48,14 @@ complete -c cot -n "__fish_cot_needs_command" -f -a "cli" -d 'Manage Cot CLI' complete -c cot -n "__fish_cot_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c cot -n "__fish_cot_using_subcommand new" -l name -d 'Set the resulting crate name [default: the directory name]' -r complete -c cot -n "__fish_cot_using_subcommand new" -l cot-path -d 'Use `cot` from the specified path instead of a published crate' -r -F +complete -c cot -n "__fish_cot_using_subcommand new" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand new" -l use-git -d 'Use the latest `cot` version from git instead of a published crate' +complete -c cot -n "__fish_cot_using_subcommand new" -l release complete -c cot -n "__fish_cot_using_subcommand new" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand new" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand new" -s h -l help -d 'Print help' +complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -l release complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s h -l help -d 'Print help' @@ -57,15 +63,21 @@ complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_s complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -f -a "make" -d 'Generate migrations for a Cot project' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -f -a "new" -d 'Create a new empty migration' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -l release complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l app-name -d 'Name of the app to use in the migration [default: crate name]' -r complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l output-dir -d 'Directory to write the migrations to [default: the migrations/ directory in the crate\'s src/ directory]' -r -F +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l release complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l app-name -d 'Name of the app to use in the migration (default: crate name)' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l release complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s h -l help -d 'Print help' @@ -73,6 +85,8 @@ complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subco complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from help" -f -a "make" -d 'Generate migrations for a Cot project' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from help" -f -a "new" -d 'Create a new empty migration' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -l release complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s h -l help -d 'Print help' @@ -80,10 +94,14 @@ complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcomm complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -f -a "completions" -d 'Generate completions for the Cot CLI' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s o -l output-dir -d 'Directory to write the manpages to [default: current directory]' -r -F +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s c -l create -d 'Create the directory if it doesn\'t exist' +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -l release complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s h -l help -d 'Print help' +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -l release complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s h -l help -d 'Print help' diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap index fc9bf6565..623bbf5df 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap @@ -33,6 +33,9 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { $completions = @(switch ($command) { 'cot' { + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -50,7 +53,10 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { 'cot;new' { [CompletionResult]::new('--name', '--name', [CompletionResultType]::ParameterName, 'Set the resulting crate name [default: the directory name]') [CompletionResult]::new('--cot-path', '--cot-path', [CompletionResultType]::ParameterName, 'Use `cot` from the specified path instead of a published crate') + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--use-git', '--use-git', [CompletionResultType]::ParameterName, 'Use the latest `cot` version from git instead of a published crate') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -60,6 +66,9 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { break } 'cot;migration' { + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -73,6 +82,9 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { break } 'cot;migration;list' { + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -84,6 +96,9 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { 'cot;migration;make' { [CompletionResult]::new('--app-name', '--app-name', [CompletionResultType]::ParameterName, 'Name of the app to use in the migration [default: crate name]') [CompletionResult]::new('--output-dir', '--output-dir', [CompletionResultType]::ParameterName, 'Directory to write the migrations to [default: the migrations/ directory in the crate''s src/ directory]') + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -94,6 +109,9 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { } 'cot;migration;new' { [CompletionResult]::new('--app-name', '--app-name', [CompletionResultType]::ParameterName, 'Name of the app to use in the migration (default: crate name)') + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -122,6 +140,9 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { break } 'cot;cli' { + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -136,8 +157,11 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { 'cot;cli;manpages' { [CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'Directory to write the manpages to [default: current directory]') [CompletionResult]::new('--output-dir', '--output-dir', [CompletionResultType]::ParameterName, 'Directory to write the manpages to [default: current directory]') + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Create the directory if it doesn''t exist') [CompletionResult]::new('--create', '--create', [CompletionResultType]::ParameterName, 'Create the directory if it doesn''t exist') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -147,6 +171,9 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { break } 'cot;cli;completions' { + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap index 1332d6345..01eb4963e 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap @@ -27,6 +27,9 @@ _cot() { local context curcontext="$curcontext" state line _arguments "${_arguments_options[@]}" : \ +'-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--release[]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -48,7 +51,10 @@ _cot() { _arguments "${_arguments_options[@]}" : \ '--name=[Set the resulting crate name \[default\: the directory name\]]:NAME:_default' \ '--cot-path=[Use \`cot\` from the specified path instead of a published crate]:COT_PATH:_files' \ +'-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--use-git[Use the latest \`cot\` version from git instead of a published crate]' \ +'--release[]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -60,6 +66,9 @@ _arguments "${_arguments_options[@]}" : \ ;; (migration) _arguments "${_arguments_options[@]}" : \ +'-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--release[]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -78,6 +87,9 @@ _arguments "${_arguments_options[@]}" : \ case $line[1] in (list) _arguments "${_arguments_options[@]}" : \ +'-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--release[]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -91,6 +103,9 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ '--app-name=[Name of the app to use in the migration \[default\: crate name\]]:APP_NAME:_default' \ '--output-dir=[Directory to write the migrations to \[default\: the migrations/ directory in the crate'\''s src/ directory\]]:OUTPUT_DIR:_files' \ +'-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--release[]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -103,6 +118,9 @@ _arguments "${_arguments_options[@]}" : \ (new) _arguments "${_arguments_options[@]}" : \ '--app-name=[Name of the app to use in the migration (default\: crate name)]:APP_NAME:_default' \ +'-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--release[]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -151,6 +169,9 @@ esac ;; (cli) _arguments "${_arguments_options[@]}" : \ +'-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--release[]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -171,8 +192,11 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ '-o+[Directory to write the manpages to \[default\: current directory\]]:OUTPUT_DIR:_files' \ '--output-dir=[Directory to write the manpages to \[default\: current directory\]]:OUTPUT_DIR:_files' \ +'-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '-c[Create the directory if it doesn'\''t exist]' \ '--create[Create the directory if it doesn'\''t exist]' \ +'--release[]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -183,6 +207,9 @@ _arguments "${_arguments_options[@]}" : \ ;; (completions) _arguments "${_arguments_options[@]}" : \ +'-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ +'--release[]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap index 9fc482ef1..d650de4ef 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap @@ -20,6 +20,8 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -v, --verbose... Increase logging verbosity - -q, --quiet... Decrease logging verbosity - -h, --help Print help + --release + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap index 710c81f20..25f36fe68 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap @@ -19,9 +19,11 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -v, --verbose... Increase logging verbosity - -q, --quiet... Decrease logging verbosity - -h, --help Print help - -V, --version Print version + --release + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help + -V, --version Print version ----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap index 8e362c49a..d049221e8 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap @@ -18,8 +18,10 @@ Arguments: Shell to generate completions for [possible values: bash, elvish, fish, powershell, zsh] Options: - -v, --verbose... Increase logging verbosity - -q, --quiet... Decrease logging verbosity - -h, --help Print help + --release + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help ----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap index 8229dced9..ac7c44e74 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap @@ -16,8 +16,10 @@ Usage: cot cli manpages [OPTIONS] Options: -o, --output-dir Directory to write the manpages to [default: current directory] - -v, --verbose... Increase logging verbosity + --release -c, --create Create the directory if it doesn't exist + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity -h, --help Print help diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap index eeceba2ac..f0c885564 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap @@ -20,8 +20,10 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -v, --verbose... Increase logging verbosity - -q, --quiet... Decrease logging verbosity - -h, --help Print help + --release + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help ----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap index f87813e2e..a1b61ca57 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap @@ -18,8 +18,10 @@ Arguments: [PATH] Path to the crate directory to list migrations for [default: current directory] Options: - -v, --verbose... Increase logging verbosity - -q, --quiet... Decrease logging verbosity - -h, --help Print help + --release + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help ----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap index 90eb57e0e..1e6177bee 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap @@ -19,9 +19,11 @@ Arguments: Options: --app-name Name of the app to use in the migration [default: crate name] - -v, --verbose... Increase logging verbosity + --release --output-dir Directory to write the migrations to [default: the migrations/ directory in the crate's src/ directory] + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity -h, --help Print help diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap index 4e4d857e8..885334017 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap @@ -18,10 +18,12 @@ Arguments: Options: --name Set the resulting crate name [default: the directory name] - -v, --verbose... Increase logging verbosity - -q, --quiet... Decrease logging verbosity + --release + -p, --package Package to use, in case you're running this in a workspace --use-git Use the latest `cot` version from git instead of a published crate --cot-path Use `cot` from the specified path instead of a published crate + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity -h, --help Print help ----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap index 0f46b54d1..1fa6e45fd 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap @@ -19,9 +19,23 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -v, --verbose... Increase logging verbosity - -q, --quiet... Decrease logging verbosity - -h, --help Print help - -V, --version Print version + --release + + + -p, --package + Package to use, in case you're running this in a workspace + + -v, --verbose... + Increase logging verbosity + + -q, --quiet... + Decrease logging verbosity + + -h, --help + Print help + + -V, --version + Print version + ----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap index 6cd3549f4..deb310322 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap @@ -20,7 +20,9 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -v, --verbose... Increase logging verbosity - -q, --quiet... Decrease logging verbosity - -h, --help Print help - -V, --version Print version + --release + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help + -V, --version Print version diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap index d2cc816d2..04a1ffe5e 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap @@ -19,9 +19,23 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -v, --verbose... Increase logging verbosity - -q, --quiet... Decrease logging verbosity - -h, --help Print help - -V, --version Print version + --release + + + -p, --package + Package to use, in case you're running this in a workspace + + -v, --verbose... + Increase logging verbosity + + -q, --quiet... + Decrease logging verbosity + + -h, --help + Print help + + -V, --version + Print version + ----- stderr ----- diff --git a/cot/src/cli.rs b/cot/src/cli.rs index e36b0905e..0f001c780 100644 --- a/cot/src/cli.rs +++ b/cot/src/cli.rs @@ -175,6 +175,7 @@ impl Cli { self.tasks.insert(Some(name), Box::new(task)); } + /// Returns the underlying clap command definition. pub fn command(&self) -> &Command { &self.command } diff --git a/cot/src/metadata.rs b/cot/src/metadata.rs index 31327e351..b98714945 100644 --- a/cot/src/metadata.rs +++ b/cot/src/metadata.rs @@ -1,22 +1,34 @@ +//! Metadata exported by Cot project binaries for the proxying `cot` CLI. + use clap::Command; use serde::{Deserialize, Serialize}; +/// Flag used to ask a Cot project binary to print its CLI metadata as JSON. pub const METADATA_FLAG: &str = "--metadata"; +/// Metadata describing the commands exposed by a Cot project binary. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ProjectMetadata { + /// Name of the project binary that produced the metadata. pub binary_name: String, + /// Top-level commands exposed by the project binary. pub commands: Vec, } +/// Metadata for a single CLI command. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct CommandMeta { + /// Command name. pub name: String, + /// Optional command description. pub about: Option, + /// Visible aliases accepted by the command. pub aliases: Vec, + /// Nested subcommands exposed by this command. pub subcommands: Vec, } +/// Extract proxyable command metadata from a clap command definition. pub fn extract(cmd: &Command) -> ProjectMetadata { ProjectMetadata { binary_name: cmd.get_name().to_string(), @@ -40,3 +52,53 @@ fn extract_command(cmd: &Command) -> CommandMeta { .collect(), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract() { + let command = Command::new("demo") + .subcommand(Command::new("serve").about("Serve requests")) + .subcommand(Command::new("secret").hide(true)); + + let metadata = extract(&command); + + assert_eq!(metadata.binary_name, "demo"); + assert_eq!(metadata.commands.len(), 1); + assert_eq!(metadata.commands[0].name, "serve"); + assert_eq!( + metadata.commands[0].about.as_deref(), + Some("Serve requests") + ); + } + + #[test] + fn test_extract_command_with_visible_aliases() { + let command = Command::new("demo").subcommand( + Command::new("database") + .visible_alias("db") + .subcommand(Command::new("migrate").visible_alias("mig")) + .subcommand(Command::new("internal").hide(true)), + ); + + let metadata = extract(&command); + let database = &metadata.commands[0]; + + assert_eq!(database.name, "database"); + assert_eq!(database.aliases, vec!["db"]); + assert_eq!(database.subcommands.len(), 1); + assert_eq!(database.subcommands[0].name, "migrate"); + assert_eq!(database.subcommands[0].aliases, vec!["mig"]); + } + + #[test] + fn test_extract_command_with_no_about() { + let command = Command::new("demo").subcommand(Command::new("plain")); + + let metadata = extract(&command); + + assert_eq!(metadata.commands[0].about, None); + } +} From 0cfd155652387ff19a8563ae698905511ca077e4 Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 23 Jun 2026 15:07:07 +0000 Subject: [PATCH 05/19] some minor refactor --- cot-cli/src/project.rs | 26 ++++++++++++++----- cot-cli/src/project_template/.gitignore | 3 +++ ...li__snapshot_testing__help__long_help.snap | 1 - ...i__snapshot_testing__help__short_help.snap | 1 - 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/cot-cli/src/project.rs b/cot-cli/src/project.rs index 2e9d0e07f..dc0bb5124 100644 --- a/cot-cli/src/project.rs +++ b/cot-cli/src/project.rs @@ -17,7 +17,12 @@ struct Cache { metadata: ProjectMetadata, } -const CACHE_FILE_NAME: &str = ".command-cache.json"; +const COT_DIR_NAME: &str = ".cot"; +const CACHE_FILE_NAME: &str = "command-cache.json"; + +fn command_cache_path(project_dir: &Path) -> PathBuf { + project_dir.join(COT_DIR_NAME).join(CACHE_FILE_NAME) +} #[derive(Debug)] pub struct ProjectBinary { @@ -69,11 +74,18 @@ pub fn load( return Ok(None); } + // When `cot` command is run from the same directory/package as the binary + // (typically the `cot-cli` package), or any workspace package whose binary + // resolves to the current executable, the discovered project binary can be the + // CLI itself. Do not query it for the project metadata: `--metadata` is + // handled by Cot application binaries, not by the `cot` proxy CLI. + // Treat this as "no project binary found" so the help output and command + // dispatch do not recurse into, or fail on, the current running CLI. if is_current_executable(&binary_path) { return Ok(None); } - let cache_path = project_dir.join(CACHE_FILE_NAME); + let cache_path = command_cache_path(project_dir); let metadata = load_or_refresh_metadata(&binary_path, &cache_path).context(format!( "unable to load metadata from binary `{}`", binary_path.display() @@ -118,7 +130,7 @@ fn resolve_workspace_package<'a>( } bail!( - "multiple packages found in the workspace; specify which one to use with `-p `.\n\ + "multiple packages found in the workspace; specify which one to use with `-p `.\n\n\ Available packages: {}", available_packages(wm) ) @@ -392,7 +404,7 @@ edition = "2024" assert_eq!(project.path, binary_path); assert_eq!(project.metadata.binary_name, "demo"); assert_eq!(project.metadata.commands[0].name, "serve"); - assert!(temp_dir.path().join(CACHE_FILE_NAME).exists()); + assert!(command_cache_path(temp_dir.path()).exists()); } #[test] @@ -526,7 +538,7 @@ path = "src/worker.rs" let project = load(temp_dir.path(), false, Some("api")).unwrap().unwrap(); assert_eq!(project.path, binary_path); - assert!(temp_dir.path().join("api").join(CACHE_FILE_NAME).exists()); + assert!(temp_dir.path().join("api").exists()); } #[test] @@ -561,7 +573,7 @@ path = "src/worker.rs" binary_mtime_secs: mtime_secs(&binary_path).unwrap(), metadata: metadata("demo", &["cached"]), }; - write_cache(&temp_dir.path().join(CACHE_FILE_NAME), &cache).unwrap(); + write_cache(&command_cache_path(temp_dir.path()), &cache).unwrap(); let project = load(temp_dir.path(), false, None).unwrap().unwrap(); @@ -579,7 +591,7 @@ path = "src/worker.rs" binary_mtime_secs: 0, metadata: metadata("demo", &["stale"]), }; - write_cache(&temp_dir.path().join(CACHE_FILE_NAME), &cache).unwrap(); + write_cache(&command_cache_path(temp_dir.path()), &cache).unwrap(); let project = load(temp_dir.path(), false, None).unwrap().unwrap(); diff --git a/cot-cli/src/project_template/.gitignore b/cot-cli/src/project_template/.gitignore index a611abcd7..6e99f71d2 100644 --- a/cot-cli/src/project_template/.gitignore +++ b/cot-cli/src/project_template/.gitignore @@ -3,6 +3,9 @@ debug/ target/ +# Cot related auto generated files +.cot/ + # These are backup files generated by rustfmt **/*.rs.bk diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap index 1fa6e45fd..70d8d39a0 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap @@ -37,5 +37,4 @@ Options: -V, --version Print version - ----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap index 04a1ffe5e..c7817220b 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap @@ -37,5 +37,4 @@ Options: -V, --version Print version - ----- stderr ----- From 9beb8eaf296fe97da604035cc3c64684a35e8d50 Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 23 Jun 2026 15:54:20 +0000 Subject: [PATCH 06/19] fix clippy --- cot-cli/src/args.rs | 1 + cot-cli/src/handlers.rs | 12 +++++------ cot-cli/src/main.rs | 2 +- cot-cli/src/project.rs | 47 +++++++++++++++++++---------------------- cot/src/cli.rs | 3 +-- cot/src/metadata.rs | 4 ++-- 6 files changed, 33 insertions(+), 36 deletions(-) diff --git a/cot-cli/src/args.rs b/cot-cli/src/args.rs index 72170dd9c..559c330b1 100644 --- a/cot-cli/src/args.rs +++ b/cot-cli/src/args.rs @@ -138,6 +138,7 @@ pub struct CompletionsArgs { /// Pulls `-p ` / `--package ` / `--package=` out of raw /// argv, before clap has parsed anything. Needed because `project::load` /// must run before `Cli::parse()` for the `--help` interception path. +#[must_use] pub fn extract_package_arg(raw: &[String]) -> Option { let mut iter = raw.iter(); while let Some(arg) = iter.next() { diff --git a/cot-cli/src/handlers.rs b/cot-cli/src/handlers.rs index b2a2a8758..5e9fc2005 100644 --- a/cot-cli/src/handlers.rs +++ b/cot-cli/src/handlers.rs @@ -100,7 +100,7 @@ pub fn handle_cli_completions(CompletionsArgs { shell }: CompletionsArgs) -> any } pub fn handle_external( - args: Vec, + args: &[OsString], project: Option, _release: bool, ) -> anyhow::Result<()> { @@ -126,13 +126,13 @@ pub fn handle_external( ); } - exec(proj, args) + exec(&proj, args) } -fn exec(proj: ProjectBinary, args: Vec) -> anyhow::Result<()> { +fn exec(proj: &ProjectBinary, args: &[OsString]) -> anyhow::Result<()> { #[cfg(unix)] { - let err = std::process::Command::new(&proj.path).args(&args).exec(); + let err = std::process::Command::new(&proj.path).args(args).exec(); anyhow::bail!("Failed to exec {}: {err}", proj.path.display()); } @@ -273,7 +273,7 @@ mod tests { #[test] fn external_command_without_project_reports_build_hint() { - let result = handle_external(vec![OsString::from("serve")], None, false); + let result = handle_external(&[OsString::from("serve")], None, false); assert!(result.is_err()); let message = result.unwrap_err().to_string(); @@ -296,7 +296,7 @@ mod tests { }, }; - let result = handle_external(vec![OsString::from("foo")], Some(project), false); + let result = handle_external(&[OsString::from("foo")], Some(project), false); assert!(result.is_err()); let message = result.unwrap_err().to_string(); diff --git a/cot-cli/src/main.rs b/cot-cli/src/main.rs index 8c9085662..73aa1006a 100644 --- a/cot-cli/src/main.rs +++ b/cot-cli/src/main.rs @@ -68,7 +68,7 @@ fn main() -> anyhow::Result<()> { }, Commands::External(args) => { let project = project::load(&std::env::current_dir()?, release, package.as_deref())?; - handlers::handle_external(args, project, release) + handlers::handle_external(&args, project, release) } } } diff --git a/cot-cli/src/project.rs b/cot-cli/src/project.rs index dc0bb5124..f14235c4e 100644 --- a/cot-cli/src/project.rs +++ b/cot-cli/src/project.rs @@ -1,3 +1,4 @@ +use std::fmt::Write; use std::path::{Path, PathBuf}; use std::time::SystemTime; @@ -32,8 +33,8 @@ pub struct ProjectBinary { /// Find and load the project binary and its metadata. /// -/// `package` corresponds to `cot -p ...` or `--package `, -/// mirroring `cargo`'s flag. It's required when run from a workspace root +/// `package` corresponds to `cot -p ...` or `--package `. +/// It's required when run from a workspace root /// (or any directory that doesn't unambiguously belong to one package) and /// the workspace has more than one member. pub fn load( @@ -146,24 +147,21 @@ fn available_packages(wm: &WorkspaceManager) -> String { /// Resolve the binary name for a package: /// -/// 1. `[package.metadata.cot] binary = "..."` — explicit override, useful when -/// a crate has multiple `[[bin]]` targets -/// 2. A single `[[bin]]` entry — use its name -/// 3. Fall back to the package name (cargo's default when there's no explicit -/// `[[bin]]` and `src/main.rs` exists) +/// 1. If the package has a `[package.metadata.cot.binary]` entry (typically as +/// a result of disambiguating multiple binaries), use that. +/// 2. If the package has a single `[[bin]]` target, use that. +/// 3. Otherwise, use the package name. fn resolve_binary_name(package_manager: &PackageManager) -> anyhow::Result { let manifest: &Manifest = package_manager.get_manifest(); - if let Some(package) = &manifest.package { - if let Some(metadata) = &package.metadata { - if let Some(name) = metadata - .get("cot") - .and_then(|c| c.get("binary")) - .and_then(|b| b.as_str()) - { - return Ok(name.to_string()); - } - } + if let Some(package) = &manifest.package + && let Some(metadata) = &package.metadata + && let Some(name) = metadata + .get("cot") + .and_then(|c| c.get("binary")) + .and_then(|b| b.as_str()) + { + return Ok(name.to_string()); } let named_bins: Vec<&str> = manifest @@ -213,12 +211,11 @@ fn load_or_refresh_metadata( ) -> anyhow::Result { let current_mtime_secs = mtime_secs(binary_path)?; - if let Ok(bytes) = std::fs::read(cache_path) { - if let Ok(cache) = serde_json::from_slice::(&bytes) { - if cache.binary_mtime_secs == current_mtime_secs { - return Ok(cache.metadata); - } - } + if let Ok(bytes) = std::fs::read(cache_path) + && let Ok(cache) = serde_json::from_slice::(&bytes) + && cache.binary_mtime_secs == current_mtime_secs + { + return Ok(cache.metadata); } let output = std::process::Command::new(binary_path) @@ -237,11 +234,11 @@ fn load_or_refresh_metadata( ); if !stderr.trim().is_empty() { - msg.push_str(&format!("\n\nstderr:\n{}", stderr.trim())); + let _ = write!(msg, "\n\nstderr:\n{}", stderr.trim()); } if !stdout.trim().is_empty() { - msg.push_str(&format!("\n\nstdout:\n{}", stdout.trim())); + let _ = write!(msg, "\n\nstdout:\n{}", stdout.trim()); } bail!(msg); } diff --git a/cot/src/cli.rs b/cot/src/cli.rs index 0f001c780..ca1836916 100644 --- a/cot/src/cli.rs +++ b/cot/src/cli.rs @@ -175,8 +175,7 @@ impl Cli { self.tasks.insert(Some(name), Box::new(task)); } - /// Returns the underlying clap command definition. - pub fn command(&self) -> &Command { + pub(crate) fn command(&self) -> &Command { &self.command } diff --git a/cot/src/metadata.rs b/cot/src/metadata.rs index b98714945..c4d2498ac 100644 --- a/cot/src/metadata.rs +++ b/cot/src/metadata.rs @@ -43,8 +43,8 @@ pub fn extract(cmd: &Command) -> ProjectMetadata { fn extract_command(cmd: &Command) -> CommandMeta { CommandMeta { name: cmd.get_name().to_string(), - about: cmd.get_about().map(|s| s.to_string()), - aliases: cmd.get_all_aliases().map(|s| s.to_string()).collect(), + about: cmd.get_about().map(ToString::to_string), + aliases: cmd.get_all_aliases().map(ToString::to_string).collect(), subcommands: cmd .get_subcommands() .filter(|subcmd| !subcmd.is_hide_set()) From 48353cbcc4d3716288ffb86ffce7516543e9c7dc Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 23 Jun 2026 16:27:26 +0000 Subject: [PATCH 07/19] fix the snapshot tests --- cot-cli/src/args.rs | 2 ++ ...pshot_testing__cli__completions_elvish.snap | 18 +++++++++--------- ...napshot_testing__cli__completions_fish.snap | 18 +++++++++--------- ...t_testing__cli__completions_powershell.snap | 18 +++++++++--------- ...snapshot_testing__cli__completions_zsh.snap | 18 +++++++++--------- .../cli__snapshot_testing__cli__no_args.snap | 3 ++- .../cli__snapshot_testing__help__help.snap | 3 ++- ...ot_testing__help__help_cli_completions.snap | 3 ++- ...pshot_testing__help__help_cli_manpages.snap | 3 ++- ...snapshot_testing__help__help_migration.snap | 3 ++- ...hot_testing__help__help_migration_list.snap | 3 ++- ...hot_testing__help__help_migration_make.snap | 3 ++- .../cli__snapshot_testing__help__help_new.snap | 3 ++- ...cli__snapshot_testing__help__long_help.snap | 3 ++- .../cli__snapshot_testing__help__no_args.snap | 3 ++- ...li__snapshot_testing__help__short_help.snap | 3 ++- 16 files changed, 60 insertions(+), 47 deletions(-) diff --git a/cot-cli/src/args.rs b/cot-cli/src/args.rs index 559c330b1..3deb7fb44 100644 --- a/cot-cli/src/args.rs +++ b/cot-cli/src/args.rs @@ -18,6 +18,8 @@ pub const HELP_SHORT_FLAG: &str = "-h"; long_about = None )] pub struct Cli { + /// Use target/release instead of target/debug when looking for the project + /// binary #[arg(long, global = true)] release: bool, /// Package to use, in case you're running this in a workspace diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap index 61c353c57..1bb6e0952 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap @@ -32,7 +32,7 @@ set edit:completion:arg-completer[cot] = {|@words| &'cot'= { cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' - cand --release 'release' + cand --release 'Use target/release instead of target/debug when looking for the project binary' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -52,7 +52,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' cand --use-git 'Use the latest `cot` version from git instead of a published crate' - cand --release 'release' + cand --release 'Use target/release instead of target/debug when looking for the project binary' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -63,7 +63,7 @@ set edit:completion:arg-completer[cot] = {|@words| &'cot;migration'= { cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' - cand --release 'release' + cand --release 'Use target/release instead of target/debug when looking for the project binary' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -78,7 +78,7 @@ set edit:completion:arg-completer[cot] = {|@words| &'cot;migration;list'= { cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' - cand --release 'release' + cand --release 'Use target/release instead of target/debug when looking for the project binary' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -91,7 +91,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand --output-dir 'Directory to write the migrations to [default: the migrations/ directory in the crate''s src/ directory]' cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' - cand --release 'release' + cand --release 'Use target/release instead of target/debug when looking for the project binary' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -103,7 +103,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand --app-name 'Name of the app to use in the migration (default: crate name)' cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' - cand --release 'release' + cand --release 'Use target/release instead of target/debug when looking for the project binary' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -128,7 +128,7 @@ set edit:completion:arg-completer[cot] = {|@words| &'cot;cli'= { cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' - cand --release 'release' + cand --release 'Use target/release instead of target/debug when looking for the project binary' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -146,7 +146,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand --package 'Package to use, in case you''re running this in a workspace' cand -c 'Create the directory if it doesn''t exist' cand --create 'Create the directory if it doesn''t exist' - cand --release 'release' + cand --release 'Use target/release instead of target/debug when looking for the project binary' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -157,7 +157,7 @@ set edit:completion:arg-completer[cot] = {|@words| &'cot;cli;completions'= { cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' - cand --release 'release' + cand --release 'Use target/release instead of target/debug when looking for the project binary' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap index 6efd16476..11a724a14 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap @@ -37,7 +37,7 @@ function __fish_cot_using_subcommand end complete -c cot -n "__fish_cot_needs_command" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r -complete -c cot -n "__fish_cot_needs_command" -l release +complete -c cot -n "__fish_cot_needs_command" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_needs_command" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_needs_command" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_needs_command" -s h -l help -d 'Print help' @@ -50,12 +50,12 @@ complete -c cot -n "__fish_cot_using_subcommand new" -l name -d 'Set the resulti complete -c cot -n "__fish_cot_using_subcommand new" -l cot-path -d 'Use `cot` from the specified path instead of a published crate' -r -F complete -c cot -n "__fish_cot_using_subcommand new" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand new" -l use-git -d 'Use the latest `cot` version from git instead of a published crate' -complete -c cot -n "__fish_cot_using_subcommand new" -l release +complete -c cot -n "__fish_cot_using_subcommand new" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand new" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand new" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand new" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r -complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -l release +complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s h -l help -d 'Print help' @@ -64,20 +64,20 @@ complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_s complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -f -a "new" -d 'Create a new empty migration' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r -complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -l release +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l app-name -d 'Name of the app to use in the migration [default: crate name]' -r complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l output-dir -d 'Directory to write the migrations to [default: the migrations/ directory in the crate\'s src/ directory]' -r -F complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r -complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l release +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l app-name -d 'Name of the app to use in the migration (default: crate name)' -r complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r -complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l release +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s h -l help -d 'Print help' @@ -86,7 +86,7 @@ complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subco complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from help" -f -a "new" -d 'Create a new empty migration' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r -complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -l release +complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s h -l help -d 'Print help' @@ -96,12 +96,12 @@ complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcomm complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s o -l output-dir -d 'Directory to write the manpages to [default: current directory]' -r -F complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s c -l create -d 'Create the directory if it doesn\'t exist' -complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -l release +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r -complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -l release +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s h -l help -d 'Print help' diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap index 623bbf5df..5a5803e53 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap @@ -35,7 +35,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { 'cot' { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') - [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -56,7 +56,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--use-git', '--use-git', [CompletionResultType]::ParameterName, 'Use the latest `cot` version from git instead of a published crate') - [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -68,7 +68,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { 'cot;migration' { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') - [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -84,7 +84,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { 'cot;migration;list' { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') - [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -98,7 +98,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('--output-dir', '--output-dir', [CompletionResultType]::ParameterName, 'Directory to write the migrations to [default: the migrations/ directory in the crate''s src/ directory]') [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') - [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -111,7 +111,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('--app-name', '--app-name', [CompletionResultType]::ParameterName, 'Name of the app to use in the migration (default: crate name)') [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') - [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -142,7 +142,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { 'cot;cli' { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') - [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -161,7 +161,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Create the directory if it doesn''t exist') [CompletionResult]::new('--create', '--create', [CompletionResultType]::ParameterName, 'Create the directory if it doesn''t exist') - [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -173,7 +173,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { 'cot;cli;completions' { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') - [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'release') + [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap index 01eb4963e..9dd5f2a37 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap @@ -29,7 +29,7 @@ _cot() { _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ -'--release[]' \ +'--release[Use target/release instead of target/debug when looking for the project binary]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -54,7 +54,7 @@ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--use-git[Use the latest \`cot\` version from git instead of a published crate]' \ -'--release[]' \ +'--release[Use target/release instead of target/debug when looking for the project binary]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -68,7 +68,7 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ -'--release[]' \ +'--release[Use target/release instead of target/debug when looking for the project binary]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -89,7 +89,7 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ -'--release[]' \ +'--release[Use target/release instead of target/debug when looking for the project binary]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -105,7 +105,7 @@ _arguments "${_arguments_options[@]}" : \ '--output-dir=[Directory to write the migrations to \[default\: the migrations/ directory in the crate'\''s src/ directory\]]:OUTPUT_DIR:_files' \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ -'--release[]' \ +'--release[Use target/release instead of target/debug when looking for the project binary]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -120,7 +120,7 @@ _arguments "${_arguments_options[@]}" : \ '--app-name=[Name of the app to use in the migration (default\: crate name)]:APP_NAME:_default' \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ -'--release[]' \ +'--release[Use target/release instead of target/debug when looking for the project binary]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -171,7 +171,7 @@ esac _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ -'--release[]' \ +'--release[Use target/release instead of target/debug when looking for the project binary]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -196,7 +196,7 @@ _arguments "${_arguments_options[@]}" : \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '-c[Create the directory if it doesn'\''t exist]' \ '--create[Create the directory if it doesn'\''t exist]' \ -'--release[]' \ +'--release[Use target/release instead of target/debug when looking for the project binary]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -209,7 +209,7 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ -'--release[]' \ +'--release[Use target/release instead of target/debug when looking for the project binary]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap index d650de4ef..43d02d9c6 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap @@ -20,7 +20,8 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --release + --release Use target/release instead of target/debug when looking for the project + binary -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap index 25f36fe68..9ac2a4d0e 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap @@ -19,7 +19,8 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --release + --release Use target/release instead of target/debug when looking for the project + binary -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap index d049221e8..94e49a54f 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap @@ -18,7 +18,8 @@ Arguments: Shell to generate completions for [possible values: bash, elvish, fish, powershell, zsh] Options: - --release + --release Use target/release instead of target/debug when looking for the project + binary -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap index ac7c44e74..1422eb943 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap @@ -16,7 +16,8 @@ Usage: cot cli manpages [OPTIONS] Options: -o, --output-dir Directory to write the manpages to [default: current directory] - --release + --release Use target/release instead of target/debug when looking for the + project binary -c, --create Create the directory if it doesn't exist -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap index f0c885564..b98d57787 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap @@ -20,7 +20,8 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --release + --release Use target/release instead of target/debug when looking for the project + binary -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap index a1b61ca57..4ae1b261c 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap @@ -18,7 +18,8 @@ Arguments: [PATH] Path to the crate directory to list migrations for [default: current directory] Options: - --release + --release Use target/release instead of target/debug when looking for the project + binary -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap index 1e6177bee..224fbd85b 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap @@ -19,7 +19,8 @@ Arguments: Options: --app-name Name of the app to use in the migration [default: crate name] - --release + --release Use target/release instead of target/debug when looking for the + project binary --output-dir Directory to write the migrations to [default: the migrations/ directory in the crate's src/ directory] -p, --package Package to use, in case you're running this in a workspace diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap index 885334017..c526649d7 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap @@ -18,7 +18,8 @@ Arguments: Options: --name Set the resulting crate name [default: the directory name] - --release + --release Use target/release instead of target/debug when looking for the project + binary -p, --package Package to use, in case you're running this in a workspace --use-git Use the latest `cot` version from git instead of a published crate --cot-path Use `cot` from the specified path instead of a published crate diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap index 70d8d39a0..972d94aad 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap @@ -20,7 +20,7 @@ Commands: Options: --release - + Use target/release instead of target/debug when looking for the project binary -p, --package Package to use, in case you're running this in a workspace @@ -37,4 +37,5 @@ Options: -V, --version Print version + ----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap index deb310322..c192bef20 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap @@ -20,7 +20,8 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --release + --release Use target/release instead of target/debug when looking for the project + binary -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap index c7817220b..1e24032b2 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap @@ -20,7 +20,7 @@ Commands: Options: --release - + Use target/release instead of target/debug when looking for the project binary -p, --package Package to use, in case you're running this in a workspace @@ -37,4 +37,5 @@ Options: -V, --version Print version + ----- stderr ----- From 00016d382a8802182df10fa9db76eacf24578bce Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 23 Jun 2026 16:40:42 +0000 Subject: [PATCH 08/19] gate unix::process::CommandExt behind unix flag --- cot-cli/src/handlers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cot-cli/src/handlers.rs b/cot-cli/src/handlers.rs index 5e9fc2005..115cc6eed 100644 --- a/cot-cli/src/handlers.rs +++ b/cot-cli/src/handlers.rs @@ -1,4 +1,5 @@ use std::ffi::OsString; +#[cfg(unix)] use std::os::unix::process::CommandExt; use std::path::PathBuf; From 7f85e45a7ba922bf1828158561b09c7da4e6fcd1 Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 23 Jun 2026 17:04:29 +0000 Subject: [PATCH 09/19] fix exec error on windows --- cot-cli/src/handlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cot-cli/src/handlers.rs b/cot-cli/src/handlers.rs index 115cc6eed..cb130daec 100644 --- a/cot-cli/src/handlers.rs +++ b/cot-cli/src/handlers.rs @@ -140,7 +140,7 @@ fn exec(proj: &ProjectBinary, args: &[OsString]) -> anyhow::Result<()> { #[cfg(not(unix))] { let status = std::process::Command::new(&proj.path) - .args(&args) + .args(args) .status()?; std::process::exit(status.code().unwrap_or(1)); } From 86cfd370cfff6cab8cea1b1918d3fd3e28597c12 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:04:45 +0000 Subject: [PATCH 10/19] chore(pre-commit.ci): auto fixes from pre-commit hooks --- cot-cli/src/handlers.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cot-cli/src/handlers.rs b/cot-cli/src/handlers.rs index cb130daec..2ee7ad303 100644 --- a/cot-cli/src/handlers.rs +++ b/cot-cli/src/handlers.rs @@ -139,9 +139,7 @@ fn exec(proj: &ProjectBinary, args: &[OsString]) -> anyhow::Result<()> { #[cfg(not(unix))] { - let status = std::process::Command::new(&proj.path) - .args(args) - .status()?; + let status = std::process::Command::new(&proj.path).args(args).status()?; std::process::exit(status.code().unwrap_or(1)); } } From 9c9b467125783a40f0ca81356978e44159570438 Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 23 Jun 2026 19:05:39 +0000 Subject: [PATCH 11/19] make serde_json a required dep --- cot/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 936529294..518d7e5c3 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -53,7 +53,7 @@ schemars = { workspace = true, optional = true, features = ["derive"] } sea-query = { workspace = true, optional = true } sea-query-sqlx = { workspace = true, features = ["with-chrono"], optional = true } serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, optional = true } +serde_json.workspace = true sqlx = { workspace = true, features = ["runtime-tokio", "chrono"], optional = true } subtle = { workspace = true, features = ["std"] } swagger-ui-redist = { workspace = true, optional = true } @@ -110,7 +110,7 @@ sqlite = ["db", "sea-query/backend-sqlite", "sea-query-sqlx/sqlx-sqlite", "sqlx/ postgres = ["db", "sea-query/backend-postgres", "sea-query-sqlx/sqlx-postgres", "sqlx/postgres"] mysql = ["db", "sea-query/backend-mysql", "sea-query-sqlx/sqlx-mysql", "sqlx/mysql"] redis = ["cache", "dep:deadpool-redis", "dep:redis", "json"] -json = ["dep:serde_json", "cot_core/json"] +json = ["cot_core/json"] openapi = ["json", "cot_core/schemars", "dep:aide", "dep:schemars"] swagger-ui = ["openapi", "dep:swagger-ui-redist"] live-reload = ["dep:tower-livereload"] From 4e7b5d69ecd5fe388a168c8ecd58fe649c3cf09d Mon Sep 17 00:00:00 2001 From: Elijah Date: Thu, 25 Jun 2026 11:52:15 +0000 Subject: [PATCH 12/19] comment improve --- cot-cli/src/project.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cot-cli/src/project.rs b/cot-cli/src/project.rs index f14235c4e..56c83f016 100644 --- a/cot-cli/src/project.rs +++ b/cot-cli/src/project.rs @@ -75,13 +75,11 @@ pub fn load( return Ok(None); } - // When `cot` command is run from the same directory/package as the binary - // (typically the `cot-cli` package), or any workspace package whose binary - // resolves to the current executable, the discovered project binary can be the - // CLI itself. Do not query it for the project metadata: `--metadata` is - // handled by Cot application binaries, not by the `cot` proxy CLI. - // Treat this as "no project binary found" so the help output and command - // dispatch do not recurse into, or fail on, the current running CLI. + // Guard against the `cot` CLI resolving to itself. This can happen when + // running from within the `cot-cli` package or a workspace package whose + // binary is the current executable. Querying it for `--metadata` would + // either recurse or fail: only cot application binaries implement that + // flag, not the CLI proxy. if is_current_executable(&binary_path) { return Ok(None); } @@ -149,7 +147,8 @@ fn available_packages(wm: &WorkspaceManager) -> String { /// /// 1. If the package has a `[package.metadata.cot.binary]` entry (typically as /// a result of disambiguating multiple binaries), use that. -/// 2. If the package has a single `[[bin]]` target, use that. +/// 2. If the package has a single `[[bin]]` explicitly in `Cargo.toml`, use +/// that. /// 3. Otherwise, use the package name. fn resolve_binary_name(package_manager: &PackageManager) -> anyhow::Result { let manifest: &Manifest = package_manager.get_manifest(); From 8001727597e0457061672ee5e08b1c867253142b Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 21 Jul 2026 16:42:54 +0000 Subject: [PATCH 13/19] update lock files --- Cargo.lock | 292 ++++++++++++++++++++++++++++------------------------- 1 file changed, 153 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9134f0604..e27c21833 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -135,9 +135,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arcstr" @@ -193,7 +193,7 @@ dependencies = [ "proc-macro2", "quote", "rustc-hash", - "syn", + "syn 2.0.119", ] [[package]] @@ -394,7 +394,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -405,13 +405,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -628,9 +628,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.67" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "shlex", @@ -706,9 +706,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.2" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +checksum = "0fb99565819980999fb7b4a1796046a5c949e6d4ff132cf5fadf5a641e20d776" dependencies = [ "clap_builder", "clap_derive", @@ -748,14 +748,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "32f2392eae7f16557a3d727ef3a12e57b2b2ca6f98566a5f4fb41ffe305df077" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -959,7 +959,7 @@ dependencies = [ "subtle", "swagger-ui-redist", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tokio", "toml", @@ -999,7 +999,9 @@ dependencies = [ "proc-macro2", "quote", "rand 0.10.2", - "syn", + "serde", + "serde_json", + "syn 2.0.119", "tempfile", "tracing", "tracing-subscriber", @@ -1015,7 +1017,7 @@ dependencies = [ "cot-cli", "glob", "libtest-mimic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1026,7 +1028,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "tracing", ] @@ -1057,7 +1059,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "sync_wrapper", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tower", "tower-sessions", @@ -1076,7 +1078,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.119", "trybuild", ] @@ -1248,7 +1250,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] @@ -1261,7 +1263,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] @@ -1272,7 +1274,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1283,7 +1285,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1343,7 +1345,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1353,7 +1355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.119", ] [[package]] @@ -1375,7 +1377,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.119", "unicode-xid", ] @@ -1421,7 +1423,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1451,7 +1453,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1522,7 +1524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1676,9 +1678,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" @@ -1771,9 +1773,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1785,9 +1787,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1795,15 +1797,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1823,9 +1825,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-lite" @@ -1842,32 +1844,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-io", @@ -1934,9 +1936,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "gloo-timers" @@ -2128,9 +2130,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -2341,7 +2343,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2476,7 +2478,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -2491,7 +2493,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.119", ] [[package]] @@ -2510,7 +2512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2578,9 +2580,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.186" +version = "0.2.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "22053b6a34f84abc97f9129e61334f40174659a1b9bd18c970b83db6a9a6348b" [[package]] name = "libm" @@ -2737,7 +2739,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2866,7 +2868,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3035,7 +3037,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3194,7 +3196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.119", ] [[package]] @@ -3208,18 +3210,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3360,22 +3362,22 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -3489,7 +3491,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3546,7 +3548,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3619,7 +3621,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.119", ] [[package]] @@ -3693,9 +3695,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -3703,22 +3705,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -3729,7 +3731,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3745,9 +3747,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -3949,7 +3951,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "tracing", @@ -3966,7 +3968,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.119", ] [[package]] @@ -3989,7 +3991,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.119", "tokio", "url", ] @@ -4017,7 +4019,7 @@ dependencies = [ "sha1", "sha2 0.11.0", "sqlx-core", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -4052,7 +4054,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "whoami", ] @@ -4077,7 +4079,7 @@ dependencies = [ "percent-encoding", "serde", "sqlx-core", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "url", ] @@ -4141,6 +4143,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -4158,14 +4171,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "target-triple" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" [[package]] name = "tempfile" @@ -4177,7 +4190,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4216,11 +4229,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -4231,18 +4244,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -4256,9 +4269,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "num-conv", @@ -4276,9 +4289,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -4330,9 +4343,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -4346,13 +4359,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4388,13 +4401,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -4557,7 +4571,7 @@ dependencies = [ "rand 0.9.5", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tokio", "tracing", @@ -4595,7 +4609,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4655,7 +4669,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4776,9 +4790,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd4ec1eb1d240636e354a30110a1dfcb37047169a4d9bd6d9d3469df574b5c4" +checksum = "ef73bfbaf3216cb59c205d7176bee1194e0d84348979da31f4a71fefe3c2054e" [[package]] name = "vcpkg" @@ -4886,7 +4900,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -4931,9 +4945,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -4966,7 +4980,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4996,7 +5010,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5007,7 +5021,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5145,9 +5159,9 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xxhash-rust" -version = "0.8.17" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985eec839aaf2a1270af8f4ebcf63cf9401cfd90f0902f97c28d9f104ffbde72" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" [[package]] name = "yoke" @@ -5180,7 +5194,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -5192,28 +5206,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5233,7 +5247,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -5284,7 +5298,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5295,7 +5309,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] From 0412722b0f53296af0fed8313e52c485895ad927 Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 21 Jul 2026 17:15:58 +0000 Subject: [PATCH 14/19] update snapshots --- ...apshot_testing__cli__completions_bash.snap | 90 +++++++++++++++++-- ...apshot_testing__cli__completions_fish.snap | 20 ++++- 2 files changed, 100 insertions(+), 10 deletions(-) diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap index 7b57c92dd..a407b0db0 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap @@ -116,12 +116,20 @@ _cot() { case "${cmd}" in cot) - opts="-v -q -h -V --verbose --quiet --help --version new migration cli help" + opts="-p -v -q -h -V --release --package --verbose --quiet --help --version new migration cli help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -130,12 +138,20 @@ _cot() { return 0 ;; cot__subcmd__cli) - opts="-v -q -h --verbose --quiet --help manpages completions help" + opts="-p -v -q -h --release --package --verbose --quiet --help manpages completions help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -144,12 +160,20 @@ _cot() { return 0 ;; cot__subcmd__cli__subcmd__completions) - opts="-v -q -h --verbose --quiet --help bash elvish fish powershell zsh" + opts="-p -v -q -h --release --package --verbose --quiet --help bash elvish fish powershell zsh" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -214,7 +238,7 @@ _cot() { return 0 ;; cot__subcmd__cli__subcmd__manpages) - opts="-o -c -v -q -h --output-dir --create --verbose --quiet --help" + opts="-o -c -p -v -q -h --output-dir --create --release --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -228,6 +252,14 @@ _cot() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -376,12 +408,20 @@ _cot() { return 0 ;; cot__subcmd__migration) - opts="-v -q -h --verbose --quiet --help list make new help" + opts="-p -v -q -h --release --package --verbose --quiet --help list make new help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -460,12 +500,20 @@ _cot() { return 0 ;; cot__subcmd__migration__subcmd__list) - opts="-v -q -h --verbose --quiet --help" + opts="-p -v -q -h --release --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -474,7 +522,7 @@ _cot() { return 0 ;; cot__subcmd__migration__subcmd__make) - opts="-v -q -h --app-name --output-dir --verbose --quiet --help" + opts="-p -v -q -h --app-name --output-dir --release --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -488,6 +536,14 @@ _cot() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -496,7 +552,7 @@ _cot() { return 0 ;; cot__subcmd__migration__subcmd__new) - opts="-v -q -h --app-name --verbose --quiet --help" + opts="-p -v -q -h --app-name --release --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -506,6 +562,14 @@ _cot() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -514,7 +578,7 @@ _cot() { return 0 ;; cot__subcmd__new) - opts="-v -q -h --name --use-git --cot-path --verbose --quiet --help" + opts="-p -v -q -h --name --use-git --cot-path --release --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -528,6 +592,14 @@ _cot() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --package) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -p) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap index bfd28c5cf..1d6cc2ba2 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap @@ -12,7 +12,7 @@ exit_code: 0 ----- stdout ----- # Print an optspec for argparse to handle cmd's options that are independent of any subcommand. function __fish_cot_global_optspecs - string join \n v/verbose q/quiet h/help V/version + string join \n release p/package= v/verbose q/quiet h/help V/version end function __fish_cot_needs_command @@ -36,6 +36,8 @@ function __fish_cot_using_subcommand contains -- $cmd[1] $argv end +complete -c cot -n "__fish_cot_needs_command" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_needs_command" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_needs_command" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_needs_command" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_needs_command" -s h -l help -d 'Print help' @@ -46,10 +48,14 @@ complete -c cot -n "__fish_cot_needs_command" -f -a "cli" -d 'Manage Cot CLI' complete -c cot -n "__fish_cot_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c cot -n "__fish_cot_using_subcommand new" -l name -d 'Set the resulting crate name [default: the directory name]' -r complete -c cot -n "__fish_cot_using_subcommand new" -l cot-path -d 'Use `cot` from the specified path instead of a published crate' -r -F +complete -c cot -n "__fish_cot_using_subcommand new" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand new" -l use-git -d 'Use the latest `cot` version from git instead of a published crate' +complete -c cot -n "__fish_cot_using_subcommand new" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand new" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand new" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand new" -s h -l help -d 'Print help' +complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s h -l help -d 'Print help' @@ -57,15 +63,21 @@ complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_s complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -f -a "make" -d 'Generate migrations for a Cot project' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -f -a "new" -d 'Create a new empty migration' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l app-name -d 'Name of the app to use in the migration [default: crate name]' -r complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l output-dir -d 'Directory to write the migrations to [default: the migrations/ directory in the crate\'s src/ directory]' -r -F +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l app-name -d 'Name of the app to use in the migration (default: crate name)' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s h -l help -d 'Print help' @@ -73,6 +85,8 @@ complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subco complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from help" -f -a "make" -d 'Generate migrations for a Cot project' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from help" -f -a "new" -d 'Create a new empty migration' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s h -l help -d 'Print help' @@ -80,10 +94,14 @@ complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcomm complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -f -a "completions" -d 'Generate completions for the Cot CLI' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s o -l output-dir -d 'Directory to write the manpages to [default: current directory]' -r -F +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s c -l create -d 'Create the directory if it doesn\'t exist' +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s h -l help -d 'Print help' +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -l release -d 'Use target/release instead of target/debug when looking for the project binary' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s h -l help -d 'Print help' From 29ade27b714fdd59fc1ee1b0e98586169c0bb88f Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 4 Aug 2026 17:26:25 +0000 Subject: [PATCH 15/19] fix merge conflicts --- Cargo.lock | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70b752fd3..aacdc86b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -504,9 +504,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "base64ct" @@ -1006,6 +1006,8 @@ dependencies = [ "proc-macro2", "quote", "rand 0.10.2", + "serde", + "serde_json", "syn 3.0.3", "tempfile", "tracing", @@ -1482,7 +1484,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420b9da095f052ea597503e39073b5b3c522f7db933fbac202d91d24492693fd" dependencies = [ - "base64 0.23.0", + "base64 0.23.1", "memchr", ] @@ -1602,7 +1604,7 @@ dependencies = [ name = "example-file-upload" version = "0.1.0" dependencies = [ - "base64 0.23.0", + "base64 0.23.1", "cot", ] @@ -2439,9 +2441,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_terminal_polyfill" @@ -2556,13 +2558,13 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lettre" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" +checksum = "f2c646bd5cc763b1087b15493e29a64be6147ba8f19342004fa52048ee596eae" dependencies = [ "async-std", "async-trait", - "base64 0.22.1", + "base64 0.23.1", "email-encoding", "email_address", "fastrand", @@ -3398,9 +3400,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -4684,9 +4686,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.118" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06649c6f63d86604ba0c8950d5a1829fc9a17afd70fc6629f481d75b6a624c78" +checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" dependencies = [ "dissimilar", "glob", From 51b6c20343f4d408c7bb97a8903fa8e1764ce7b2 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 12 Aug 2026 02:49:53 +0000 Subject: [PATCH 16/19] address PR comments. Also some refactor and fixed loads of bugs to improve UX --- Cargo.lock | 1 + Cargo.toml | 1 + cot-cli/Cargo.toml | 3 +- cot-cli/src/args.rs | 10 +- cot-cli/src/handlers.rs | 485 ++++++++++++++++-- cot-cli/src/main.rs | 221 ++++++-- cot-cli/src/project.rs | 426 +++++++++++---- ...apshot_testing__cli__completions_bash.snap | 18 +- ...shot_testing__cli__completions_elvish.snap | 9 + ...apshot_testing__cli__completions_fish.snap | 11 +- ..._testing__cli__completions_powershell.snap | 9 + ...napshot_testing__cli__completions_zsh.snap | 9 + .../cli__snapshot_testing__cli__no_args.snap | 1 + .../cli__snapshot_testing__help__help.snap | 1 + ...t_testing__help__help_cli_completions.snap | 1 + ...shot_testing__help__help_cli_manpages.snap | 1 + ...napshot_testing__help__help_migration.snap | 1 + ...ot_testing__help__help_migration_list.snap | 1 + ...ot_testing__help__help_migration_make.snap | 1 + ...cli__snapshot_testing__help__help_new.snap | 3 +- ...li__snapshot_testing__help__long_help.snap | 25 +- .../cli__snapshot_testing__help__no_args.snap | 1 + ...i__snapshot_testing__help__short_help.snap | 25 +- cot/src/metadata.rs | 201 ++++++-- cot/src/project.rs | 6 +- 25 files changed, 1222 insertions(+), 249 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aacdc86b2..781f1fe24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1013,6 +1013,7 @@ dependencies = [ "tracing", "tracing-subscriber", "trybuild", + "wait-timeout", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 55471725c..e8b010e45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -157,6 +157,7 @@ tracing-subscriber = "0.3" tracing-test = "0.2" trybuild = { version = "1", features = ["diff"] } url = "2" +wait-timeout = { version = "0.2", default-features = false } [profile.dev.package] insta.opt-level = 3 diff --git a/cot-cli/Cargo.toml b/cot-cli/Cargo.toml index d0b830764..4080d79b1 100644 --- a/cot-cli/Cargo.toml +++ b/cot-cli/Cargo.toml @@ -42,7 +42,8 @@ syn.workspace = true tracing.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } serde = { workspace = true, features = ["derive"] } -serde_json = {workspace = true} +serde_json = { workspace = true} +wait-timeout = { workspace = true } [dev-dependencies] cot-cli = { path = ".", features = ["test_utils"] } diff --git a/cot-cli/src/args.rs b/cot-cli/src/args.rs index 3deb7fb44..e5a2011b0 100644 --- a/cot-cli/src/args.rs +++ b/cot-cli/src/args.rs @@ -9,6 +9,8 @@ pub const PACKAGE_SHORT_FLAG: &str = "-p"; pub const RELEASE_FLAG: &str = "--release"; pub const HELP_LONG_FLAG: &str = "--help"; pub const HELP_SHORT_FLAG: &str = "-h"; +pub const BINARY_FLAG: &str = "--bin"; +pub const BUILD_FLAG: &str = "--build"; #[derive(Debug, Parser)] #[command( @@ -22,6 +24,9 @@ pub struct Cli { /// binary #[arg(long, global = true)] release: bool, + /// Build the binary if it does not exist + #[arg(long, global = true)] + build: bool, /// Package to use, in case you're running this in a workspace #[arg(short = 'p', long, global = true, value_name = "PACKAGE")] pub package: Option, @@ -67,6 +72,9 @@ pub enum MigrationCommands { Make(MigrationMakeArgs), /// Create a new empty migration New(MigrationNewArgs), + /// External migration subcommands shipped with the cot binary + #[command(external_subcommand)] + External(Vec), } #[derive(Debug, Args)] @@ -139,7 +147,7 @@ pub struct CompletionsArgs { /// Pulls `-p ` / `--package ` / `--package=` out of raw /// argv, before clap has parsed anything. Needed because `project::load` -/// must run before `Cli::parse()` for the `--help` interception path. +/// must run before `Cli::parse` for the `--help` interception path. #[must_use] pub fn extract_package_arg(raw: &[String]) -> Option { let mut iter = raw.iter(); diff --git a/cot-cli/src/handlers.rs b/cot-cli/src/handlers.rs index 2ee7ad303..7bc737268 100644 --- a/cot-cli/src/handlers.rs +++ b/cot-cli/src/handlers.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::ffi::OsString; #[cfg(unix)] use std::os::unix::process::CommandExt; @@ -5,7 +6,8 @@ use std::path::PathBuf; use anyhow::Context; use clap::CommandFactory; -use cot::metadata::CommandMeta; +use cot::metadata::{ArgMeta, CommandMeta}; +use cot::utils::cli::{StatusType, print_status_msg}; use crate::args::{ Cli, CompletionsArgs, ManpagesArgs, MigrationListArgs, MigrationMakeArgs, MigrationNewArgs, @@ -101,33 +103,68 @@ pub fn handle_cli_completions(CompletionsArgs { shell }: CompletionsArgs) -> any } pub fn handle_external( - args: &[OsString], + command_path: &[String], + remaining_args: &[OsString], project: Option, _release: bool, ) -> anyhow::Result<()> { - let subcmd = args[0].to_string_lossy(); + let subcmd = command_path.join(" "); let Some(proj) = project else { anyhow::bail!( - "Unknown command `{subcmd}` and no project binary was found in target/.\n\ - Hint: run `cargo build` first, or `cargo build --release`." + "unknown command `{subcmd}` and no project binary was found in the `target` dir.\n\ + Hint: run `cargo build` first, or pass `cot --build {subcmd}` to build it automatically." ); }; - let known = proj - .metadata - .commands + match &proj.metadata { + Some(meta) if command_path_exists(&meta.commands, command_path) => { + // command is known, proceed to exec + } + Some(_) => { + // metadata found but command is not known + anyhow::bail!( + "unknown command `{subcmd}`. Run `cot --help` to see available commands." + ); + } + None => { + // The metadata retrieval from the binary most likely failed or didnt exist so + // theres no way to validate the command exists here. We forward the command + // unconditionally and let the binary handle it. + print_status_msg( + StatusType::Warning, + &format!( + "could not obtain metadata for `{}`; forwarding `{subcmd}` command directly", + proj.path.display() + ), + ); + } + } + + let full_args: Vec = command_path .iter() - .any(|c| c.name == subcmd.as_ref() || c.aliases.iter().any(|a| a == subcmd.as_ref())); + .map(OsString::from) + .chain(remaining_args.iter().cloned()) + .collect(); - if !known { - anyhow::bail!( - "Unknown command `{subcmd}`.\n\ - Run `cot --help` to see all available commands." - ); + exec(&proj, &full_args) +} + +fn command_path_exists(commands: &[CommandMeta], path: &[String]) -> bool { + let mut current: &[CommandMeta] = commands; + + for segment in path { + let found = current + .iter() + .find(|c| c.name == *segment || c.aliases.iter().any(|a| a == segment)); + + match found { + Some(cmd) => current = &cmd.subcommands, + None => return false, + } } - exec(&proj, args) + true } fn exec(proj: &ProjectBinary, args: &[OsString]) -> anyhow::Result<()> { @@ -139,6 +176,9 @@ fn exec(proj: &ProjectBinary, args: &[OsString]) -> anyhow::Result<()> { #[cfg(not(unix))] { + // Windows has no equivalent of POSIX `execve` that replaces the current + // process in place. The best we can do is spawn the binary as a + // child and block here until it exits let status = std::process::Command::new(&proj.path).args(args).status()?; std::process::exit(status.code().unwrap_or(1)); } @@ -146,20 +186,58 @@ fn exec(proj: &ProjectBinary, args: &[OsString]) -> anyhow::Result<()> { /// Build a fresh [`clap::Command`] and inject the project's subcommands into /// it before printing. -pub fn handle_combined_help(project: Option<&ProjectBinary>) -> anyhow::Result<()> { - let mut cmd = combined_help_command(project); - - cmd.print_long_help()?; +pub fn handle_combined_help( + project: Option<&ProjectBinary>, + path: &[String], +) -> anyhow::Result<()> { + let cmd = combined_help_command(project); + let mut target = navigate_to(cmd, path); + target.print_help()?; println!(); Ok(()) } +fn navigate_to(mut cmd: clap::Command, path: &[String]) -> clap::Command { + let mut bin_name = cmd.get_name().to_string(); + + for segment in path { + match cmd.find_subcommand(segment) { + Some(sub) => { + bin_name = format!("{bin_name} {segment}"); + cmd = sub.clone(); + } + None => break, + } + } + + cmd.bin_name(bin_name) +} + fn combined_help_command(project: Option<&ProjectBinary>) -> clap::Command { let mut cmd = Cli::command(); - if let Some(proj) = project { - for meta_cmd in &proj.metadata.commands { - cmd = cmd.subcommand(build_clap_subcommand(meta_cmd)); + if let Some(proj) = project + && let Some(meta) = &proj.metadata + { + let mut cmd_set: HashSet = cmd + .get_subcommands() + .map(|sc| sc.get_name().to_string()) + .collect(); + + for meta_cmd in &meta.commands { + if cmd_set.insert(meta_cmd.name.clone()) { + cmd = cmd.subcommand(build_clap_subcommand(meta_cmd)); + } else { + // there's an existing command, let's merge them into one. For command + // collisions, metadata(such as name and about) of the command + // present in `cot-cli` will take precedence. + cmd = cmd.mut_subcommand(&meta_cmd.name, |mut sc| { + for sub in &meta_cmd.subcommands { + sc = sc.subcommand(build_clap_subcommand(sub)); + } + sc + }); + } } } @@ -177,6 +255,10 @@ fn build_clap_subcommand(meta: &CommandMeta) -> clap::Command { cmd = cmd.visible_alias(alias.clone()); } + for arg_meta in &meta.args { + cmd = cmd.arg(build_clap_arg(arg_meta)); + } + for sub in &meta.subcommands { cmd = cmd.subcommand(build_clap_subcommand(sub)); } @@ -184,6 +266,30 @@ fn build_clap_subcommand(meta: &CommandMeta) -> clap::Command { cmd } +fn build_clap_arg(meta: &ArgMeta) -> clap::Arg { + let mut arg = clap::Arg::new(&meta.name).required(meta.required); + + if meta.is_positional + && let Some(vn) = &meta.value_name + { + arg = arg.value_name(vn.clone()); + } else { + if let Some(long) = &meta.long { + arg = arg.long(long.clone()); + } + if let Some(short) = meta.short { + arg = arg.short(short); + } + if !meta.takes_value { + arg = arg.action(clap::ArgAction::SetTrue); + } + } + if let Some(help) = &meta.help { + arg = arg.help(help.clone()); + } + arg +} + fn generate_completions(shell: clap_complete::Shell, writer: &mut impl std::io::Write) { clap_complete::generate(shell, &mut Cli::command(), "cot", writer); } @@ -272,11 +378,11 @@ mod tests { #[test] fn external_command_without_project_reports_build_hint() { - let result = handle_external(&[OsString::from("serve")], None, false); + let result = handle_external(&["serve".to_string()], &[], None, false); assert!(result.is_err()); let message = result.unwrap_err().to_string(); - assert!(message.contains("Unknown command `serve`")); + assert!(message.contains("unknown command `serve`")); assert!(message.contains("run `cargo build` first")); } @@ -284,25 +390,173 @@ mod tests { fn external_command_unknown_to_project_reports_unknown_command() { let project = ProjectBinary { path: PathBuf::from("target/debug/example"), - metadata: cot::metadata::ProjectMetadata { + metadata: Some(cot::metadata::ProjectMetadata { + version: cot::metadata::METADATA_SCHEMA_VERSION, binary_name: "example".to_string(), commands: vec![CommandMeta { name: "check".to_string(), about: None, aliases: vec![], subcommands: vec![], + args: vec![], }], - }, + }), }; - let result = handle_external(&[OsString::from("foo")], Some(project), false); + let result = handle_external(&["foo".to_string()], &[], Some(project), false); assert!(result.is_err()); let message = result.unwrap_err().to_string(); - assert!(message.contains("Unknown command `foo`")); + assert!(message.contains("unknown command `foo`")); assert!(message.contains("cot --help")); } + #[test] + fn external_command_nested_path_unknown_reports_unknown_command() { + let project = ProjectBinary { + path: PathBuf::from("target/debug/example"), + metadata: Some(cot::metadata::ProjectMetadata { + version: cot::metadata::METADATA_SCHEMA_VERSION, + binary_name: "example".to_string(), + commands: vec![CommandMeta { + name: "migration".to_string(), + about: None, + aliases: vec![], + subcommands: vec![CommandMeta { + name: "rollback".to_string(), + about: None, + aliases: vec![], + subcommands: vec![], + args: vec![], + }], + args: vec![], + }], + }), + }; + + let result = handle_external( + &["migration".to_string(), "nonexistent".to_string()], + &[], + Some(project), + false, + ); + + assert!(result.is_err()); + let message = result.unwrap_err().to_string(); + assert!(message.contains("unknown command `migration nonexistent`")); + } + + #[test] + #[cfg(unix)] + fn known_nested_command_attempts_exec_and_fails_when_binary_missing() { + let project = ProjectBinary { + path: PathBuf::from("/nonexistent/binary/path"), + metadata: Some(cot::metadata::ProjectMetadata { + version: cot::metadata::METADATA_SCHEMA_VERSION, + binary_name: "example".to_string(), + commands: vec![CommandMeta { + name: "migration".to_string(), + about: None, + aliases: vec![], + subcommands: vec![CommandMeta { + name: "rollback".to_string(), + about: None, + aliases: vec![], + subcommands: vec![], + args: vec![], + }], + args: vec![], + }], + }), + }; + + let result = handle_external( + &["migration".to_string(), "rollback".to_string()], + &[OsString::from("my_migration"), OsString::from("--dry-run")], + Some(project), + false, + ); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Failed to exec")); + } + + #[test] + #[cfg(unix)] + fn missing_metadata_forwards_blindly_and_attempts_exec() { + let project = ProjectBinary { + path: PathBuf::from("/nonexistent/binary/path"), + metadata: None, + }; + + let result = handle_external(&["anything".to_string()], &[], Some(project), false); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Failed to exec")); + } + + #[test] + fn command_path_exists_finds_nested_command() { + let commands = vec![CommandMeta { + name: "migration".to_string(), + about: None, + aliases: vec![], + subcommands: vec![CommandMeta { + name: "rollback".to_string(), + about: None, + aliases: vec![], + subcommands: vec![], + args: vec![], + }], + args: vec![], + }]; + + assert!(command_path_exists( + &commands, + &["migration".to_string(), "rollback".to_string()] + )); + } + + #[test] + fn command_path_exists_matches_via_alias() { + let commands = vec![CommandMeta { + name: "migration".to_string(), + about: None, + aliases: vec!["mig".to_string()], + subcommands: vec![], + args: vec![], + }]; + + assert!(command_path_exists(&commands, &["mig".to_string()])); + } + + #[test] + fn command_path_exists_rejects_missing_nested_command() { + let commands = vec![CommandMeta { + name: "migration".to_string(), + about: None, + aliases: vec![], + subcommands: vec![CommandMeta { + name: "rollback".to_string(), + about: None, + aliases: vec![], + subcommands: vec![], + args: vec![], + }], + args: vec![], + }]; + + assert!(!command_path_exists( + &commands, + &["migration".to_string(), "nonexistent".to_string()] + )); + } + + #[test] + fn command_path_exists_empty_path_is_true() { + assert!(command_path_exists(&[], &[])); + } + #[test] fn build_clap_subcommand_preserves_about_aliases_and_nested_subcommands() { let meta = CommandMeta { @@ -314,7 +568,9 @@ mod tests { about: Some("Rollback migrations".to_string()), aliases: vec!["rbk".to_string()], subcommands: vec![], + args: vec![], }], + args: vec![], }; let cmd = build_clap_subcommand(&meta); @@ -333,19 +589,78 @@ mod tests { assert!(nested.get_all_aliases().any(|alias| alias == "rbk")); } + #[test] + fn build_clap_arg_positional_required() { + let meta = ArgMeta { + name: "migration_name".to_string(), + long: None, + short: None, + help: Some("Migration to roll back to".to_string()), + required: true, + is_positional: true, + takes_value: true, + value_name: Some("MIGRATION_NAME".to_string()), + }; + + let arg = build_clap_arg(&meta); + + assert!(arg.is_required_set()); + assert!(arg.is_positional()); + assert_eq!(arg.get_value_names().unwrap()[0].as_str(), "MIGRATION_NAME"); + } + + #[test] + fn build_clap_arg_boolean_flag_sets_true_action() { + let meta = ArgMeta { + name: "dry-run".to_string(), + long: Some("dry-run".to_string()), + short: None, + help: None, + required: false, + is_positional: false, + takes_value: false, + value_name: None, + }; + + let arg = build_clap_arg(&meta); + + assert_eq!(arg.get_long(), Some("dry-run")); + } + + #[test] + fn build_clap_arg_valued_flag_with_short_and_long() { + let meta = ArgMeta { + name: "app".to_string(), + long: Some("app".to_string()), + short: Some('a'), + help: Some("App name".to_string()), + required: false, + is_positional: false, + takes_value: true, + value_name: None, + }; + + let arg = build_clap_arg(&meta); + + assert_eq!(arg.get_long(), Some("app")); + assert_eq!(arg.get_short(), Some('a')); + } + #[test] fn combined_help_command_includes_project_commands_and_builtin_commands() { let project = ProjectBinary { path: PathBuf::from("target/debug/example"), - metadata: cot::metadata::ProjectMetadata { + metadata: Some(cot::metadata::ProjectMetadata { + version: cot::metadata::METADATA_SCHEMA_VERSION, binary_name: "example".to_string(), commands: vec![CommandMeta { name: "health".to_string(), about: Some("Check the server health".to_string()), aliases: vec![], subcommands: vec![], + args: vec![], }], - }, + }), }; let cmd = combined_help_command(Some(&project)); @@ -363,4 +678,114 @@ mod tests { "Check the server health" ); } + + #[test] + fn combined_help_command_merges_duplicate_subcommand_preserving_builtin_about() { + let project = ProjectBinary { + path: PathBuf::from("target/debug/example"), + metadata: Some(cot::metadata::ProjectMetadata { + version: cot::metadata::METADATA_SCHEMA_VERSION, + binary_name: "example".to_string(), + commands: vec![CommandMeta { + name: "migration".to_string(), + about: Some("Should not override cot-cli's about".to_string()), + aliases: vec![], + subcommands: vec![CommandMeta { + name: "rollback".to_string(), + about: Some("Rollback migrations".to_string()), + aliases: vec![], + subcommands: vec![], + args: vec![], + }], + args: vec![], + }], + }), + }; + + let cmd = combined_help_command(Some(&project)); + + let matches: Vec<_> = cmd + .get_subcommands() + .filter(|sc| sc.get_name() == "migration") + .collect(); + assert_eq!(matches.len(), 1, "migration should not be duplicated"); + + let migration = matches[0]; + assert_eq!( + migration.get_about().unwrap().to_string(), + "Manage migrations for a Cot project" + ); + assert!( + migration + .get_subcommands() + .any(|sc| sc.get_name() == "rollback") + ); + assert!( + migration + .get_subcommands() + .any(|sc| sc.get_name() == "list") + ); + } + + #[test] + fn navigate_to_returns_root_for_empty_path() { + let cmd = combined_help_command(None); + let target = navigate_to(cmd, &[]); + assert_eq!(target.get_name(), "cot"); + } + + #[test] + fn navigate_to_descends_into_known_subcommand() { + let cmd = combined_help_command(None); + let target = navigate_to(cmd, &["migration".to_string()]); + assert_eq!(target.get_name(), "migration"); + assert_eq!(target.get_bin_name(), Some("cot migration")); + } + + #[test] + fn navigate_to_stops_at_first_unknown_segment() { + let cmd = combined_help_command(None); + let target = navigate_to(cmd, &["migration".to_string(), "nonexistent".to_string()]); + assert_eq!(target.get_name(), "migration"); + } + + #[test] + fn navigate_to_descends_into_merged_binary_subcommand_with_args() { + let project = ProjectBinary { + path: PathBuf::from("target/debug/example"), + metadata: Some(cot::metadata::ProjectMetadata { + version: cot::metadata::METADATA_SCHEMA_VERSION, + binary_name: "example".to_string(), + commands: vec![CommandMeta { + name: "migration".to_string(), + about: None, + aliases: vec![], + subcommands: vec![CommandMeta { + name: "rollback".to_string(), + about: Some("Rollback migrations".to_string()), + aliases: vec![], + subcommands: vec![], + args: vec![ArgMeta { + name: "dry-run".to_string(), + long: Some("dry-run".to_string()), + short: None, + help: Some("Print the rollback plan".to_string()), + required: false, + is_positional: false, + takes_value: false, + value_name: None, + }], + }], + args: vec![], + }], + }), + }; + + let cmd = combined_help_command(Some(&project)); + let target = navigate_to(cmd, &["migration".to_string(), "rollback".to_string()]); + + assert_eq!(target.get_name(), "rollback"); + assert_eq!(target.get_bin_name(), Some("cot migration rollback")); + assert!(target.get_arguments().any(|a| a.get_id() == "dry-run")); + } } diff --git a/cot-cli/src/main.rs b/cot-cli/src/main.rs index 73aa1006a..19d53654d 100644 --- a/cot-cli/src/main.rs +++ b/cot-cli/src/main.rs @@ -1,51 +1,80 @@ #![allow(unreachable_pub)] // triggers false positives because we have both a binary and library +use std::ffi::OsString; + use clap::Parser; use cot_cli::args::{ - Cli, CliCommands, Commands, HELP_LONG_FLAG, HELP_SHORT_FLAG, MigrationCommands, + BUILD_FLAG, Cli, CliCommands, Commands, HELP_LONG_FLAG, HELP_SHORT_FLAG, MigrationCommands, PACKAGE_LONG_FLAG, PACKAGE_SHORT_FLAG, RELEASE_FLAG, extract_package_arg, }; use cot_cli::{handlers, project}; use tracing_subscriber::util::SubscriberInitExt; -fn is_top_level_help(args: &[String]) -> bool { +fn resolve_help_request(args: &[String]) -> Option> { if !args .iter() .any(|a| a == HELP_LONG_FLAG || a == HELP_SHORT_FLAG) { - return false; + return None; } - let mut rest = args.iter().skip(1).peekable(); - while let Some(arg) = rest.next() { + let mut path = Vec::new(); + let mut iter = args.iter().skip(1).peekable(); + + while let Some(arg) = iter.next() { match arg.as_str() { - HELP_LONG_FLAG | HELP_SHORT_FLAG | RELEASE_FLAG => {} - PACKAGE_SHORT_FLAG | PACKAGE_LONG_FLAG => { - let Some(value) = rest.next() else { - return false; - }; - if value.starts_with('-') { - return false; + HELP_LONG_FLAG | HELP_SHORT_FLAG => return Some(path), + RELEASE_FLAG | BUILD_FLAG => {} + PACKAGE_SHORT_FLAG | PACKAGE_LONG_FLAG => match iter.peek() { + Some(v) if !v.starts_with('-') => { + iter.next(); } - } - _ => return false, + _ => return None, + }, + other if other.starts_with('-') => return None, + other => path.push(other.to_string()), } } - true + + None +} + +fn forwarded_args(clap_captured: &[OsString], after_dash_delimiter: &[String]) -> Vec { + clap_captured + .iter() + .cloned() + .chain(after_dash_delimiter.iter().map(OsString::from)) + .collect() +} + +fn split_on_double_dash(raw: &[String]) -> (&[String], &[String]) { + match raw.iter().position(|a| a == "--") { + Some(i) => (&raw[..i], &raw[i + 1..]), + None => (raw, &[]), + } } fn main() -> anyhow::Result<()> { let raw: Vec = std::env::args().collect(); - let release = raw.iter().any(|a| a == RELEASE_FLAG); - let package = extract_package_arg(&raw); - if is_top_level_help(&raw) { - let project = project::load(&std::env::current_dir()?, release, package.as_deref())?; - handlers::handle_combined_help(project.as_ref())?; + let (cot_args, forwarded_tail_args) = split_on_double_dash(&raw); + + let release = cot_args.iter().any(|a| a == RELEASE_FLAG); + let build = cot_args.iter().any(|b| b == BUILD_FLAG); + let package = extract_package_arg(cot_args); + + if let Some(path) = resolve_help_request(cot_args) { + let project = project::load( + &std::env::current_dir()?, + release, + package.as_deref(), + build, + )?; + handlers::handle_combined_help(project.as_ref(), &path)?; return Ok(()); } - let cli = Cli::parse(); + let cli = Cli::parse_from(cot_args); tracing_subscriber::fmt() .with_env_filter( @@ -65,10 +94,31 @@ fn main() -> anyhow::Result<()> { MigrationCommands::List(args) => handlers::handle_migration_list(args), MigrationCommands::Make(args) => handlers::handle_migration_make(args), MigrationCommands::New(args) => handlers::handle_migration_new(args), + MigrationCommands::External(args) => { + let project = project::load( + &std::env::current_dir()?, + release, + package.as_deref(), + build, + )?; + let path = vec![ + "migration".to_string(), + args[0].to_string_lossy().into_owned(), + ]; + let remaining = forwarded_args(&args[1..], forwarded_tail_args); + handlers::handle_external(&path, &remaining, project, release) + } }, Commands::External(args) => { - let project = project::load(&std::env::current_dir()?, release, package.as_deref())?; - handlers::handle_external(&args, project, release) + let project = project::load( + &std::env::current_dir()?, + release, + package.as_deref(), + build, + )?; + let path = vec![args[0].to_string_lossy().into_owned()]; + let remaining = forwarded_args(&args[1..], forwarded_tail_args); + handlers::handle_external(&path, &remaining, project, release) } } } @@ -82,36 +132,111 @@ mod tests { } #[test] - fn top_level_help_accepts_only_global_flags() { - assert!(is_top_level_help(&args(&["cot", "--help"]))); - assert!(is_top_level_help(&args(&["cot", "-h"]))); - assert!(is_top_level_help(&args(&[ - "cot", - "--release", - "-p", - "blog", - "--help" - ]))); - assert!(is_top_level_help(&args(&[ - "cot", - "--package", - "blog", - "-h", - "--release" - ]))); + fn top_level_help_returns_empty_path() { + assert_eq!( + resolve_help_request(&args(&["cot", "--help"])), + Some(vec![]) + ); + assert_eq!(resolve_help_request(&args(&["cot", "-h"])), Some(vec![])); + } + + #[test] + fn top_level_help_accepts_global_flags_before_help() { + assert_eq!( + resolve_help_request(&args(&["cot", "--release", "-p", "blog", "--help"])), + Some(vec![]) + ); + assert_eq!( + resolve_help_request(&args(&["cot", "--package", "blog", "-h", "--release"])), + Some(vec![]) + ); } #[test] - fn top_level_help_rejects_subcommands_and_non_help_invocations() { - assert!(!is_top_level_help(&args(&["cot"]))); - assert!(!is_top_level_help(&args(&["cot", "migration", "--help"]))); - assert!(!is_top_level_help(&args(&["cot", "serve", "-h"]))); - assert!(!is_top_level_help(&args(&["cot", "--version"]))); + fn help_flag_short_circuits_ignoring_trailing_tokens() { + assert_eq!( + resolve_help_request(&args(&["cot", "--help", "foo"])), + Some(vec![]) + ); + assert_eq!( + resolve_help_request(&args(&["cot", "migration", "-h", "rollback"])), + Some(vec!["migration".to_string()]) + ); } #[test] - fn top_level_help_treats_missing_package_value_as_not_top_level_help() { - assert!(!is_top_level_help(&args(&["cot", "-p", "--help"]))); - assert!(!is_top_level_help(&args(&["cot", "--package", "-h"]))); + fn subcommand_help_returns_path() { + assert_eq!( + resolve_help_request(&args(&["cot", "migration", "--help"])), + Some(vec!["migration".to_string()]) + ); + assert_eq!( + resolve_help_request(&args(&["cot", "migration", "rollback", "-h"])), + Some(vec!["migration".to_string(), "rollback".to_string()]) + ); + } + + #[test] + fn non_help_invocations_return_none() { + assert_eq!(resolve_help_request(&args(&["cot"])), None); + assert_eq!(resolve_help_request(&args(&["cot", "serve"])), None); + assert_eq!(resolve_help_request(&args(&["cot", "--version"])), None); + } + + #[test] + fn missing_package_value_returns_none() { + assert_eq!(resolve_help_request(&args(&["cot", "-p", "--help"])), None); + assert_eq!( + resolve_help_request(&args(&["cot", "--package", "-h"])), + None + ); + } + + #[test] + fn unknown_flag_before_help_returns_none() { + assert_eq!( + resolve_help_request(&args(&["cot", "--unknown", "--help"])), + None + ); + } + + #[test] + fn forwarded_args_combines_captured_and_double_dash_tail() { + let captured = vec![OsString::from("--dry-run")]; + let tail = vec!["--app".to_string(), "blog".to_string()]; + + let result = forwarded_args(&captured, &tail); + + assert_eq!( + result, + vec![ + OsString::from("--dry-run"), + OsString::from("--app"), + OsString::from("blog"), + ] + ); + } + + #[test] + fn forwarded_args_empty_inputs_produce_empty_vec() { + assert!(forwarded_args(&[], &[]).is_empty()); + } + + #[test] + fn split_on_double_dash_splits_at_delimiter() { + let raw = args(&["cot", "check", "--", "--dry-run", "x"]); + let (before, after) = split_on_double_dash(&raw); + + assert_eq!(before, &args(&["cot", "check"])[..]); + assert_eq!(after, &args(&["--dry-run", "x"])[..]); + } + + #[test] + fn split_on_double_dash_without_delimiter_returns_all_before() { + let raw = args(&["cot", "check"]); + let (before, after) = split_on_double_dash(&raw); + + assert_eq!(before, &raw[..]); + assert!(after.is_empty()); } } diff --git a/cot-cli/src/project.rs b/cot-cli/src/project.rs index 56c83f016..eab3c459e 100644 --- a/cot-cli/src/project.rs +++ b/cot-cli/src/project.rs @@ -1,16 +1,22 @@ use std::fmt::Write; +use std::io::Read; use std::path::{Path, PathBuf}; +use std::process::Stdio; use std::time::SystemTime; use anyhow::{Context, bail}; use cargo_toml::Manifest; use cot::metadata::{METADATA_FLAG, ProjectMetadata}; +use cot::utils::cli::{StatusType, print_status_msg}; use serde::{Deserialize, Serialize}; +use wait_timeout::ChildExt; +use crate::args::{BINARY_FLAG, PACKAGE_SHORT_FLAG, RELEASE_FLAG}; use crate::utils::{CargoTomlManager, PackageManager, WorkspaceManager}; const RELEASE_PROFILE: &str = "release"; const DEBUG_PROFILE: &str = "debug"; +const METADATA_TIMEOUT: core::time::Duration = core::time::Duration::from_secs(5); #[derive(Serialize, Deserialize)] struct Cache { @@ -28,7 +34,7 @@ fn command_cache_path(project_dir: &Path) -> PathBuf { #[derive(Debug)] pub struct ProjectBinary { pub path: PathBuf, - pub metadata: ProjectMetadata, + pub metadata: Option, } /// Find and load the project binary and its metadata. @@ -41,6 +47,7 @@ pub fn load( path: &Path, release: bool, package: Option<&str>, + build: bool, ) -> anyhow::Result> { let Some(manager) = CargoTomlManager::from_path(path)? else { return Ok(None); @@ -69,10 +76,21 @@ pub fn load( #[cfg(target_os = "windows")] let binary_name = format!("{binary_name}.exe"); - let binary_path = target_dir.join(profile).join(binary_name); + let binary_path = target_dir.join(profile).join(&binary_name); if !binary_path.exists() { - return Ok(None); + if !build { + return Ok(None); + } + + build_binary(package_manager.get_package_name(), &binary_name, release)?; + if !binary_path.exists() { + bail!( + "`cargo build` succeeded but `{}` still wasn't found at the expected path, \ + this may mean the binary name `cot` resolved doesn't match what cargo built.", + binary_path.display(), + ); + } } // Guard against the `cot` CLI resolving to itself. This can happen when @@ -85,10 +103,20 @@ pub fn load( } let cache_path = command_cache_path(project_dir); - let metadata = load_or_refresh_metadata(&binary_path, &cache_path).context(format!( - "unable to load metadata from binary `{}`", - binary_path.display() - ))?; + let metadata = match load_or_refresh_metadata(&binary_path, &cache_path) { + Ok(meta) => meta, + Err(e) => { + print_status_msg( + StatusType::Warning, + &format!( + "could not determine `{}`'s cli commands, so they won't be \ + listed when you run `cot --help`: {e:#}", + binary_path.display(), + ), + ); + None + } + }; Ok(Some(ProjectBinary { path: binary_path, @@ -96,6 +124,35 @@ pub fn load( })) } +fn build_binary(package_name: &str, binary_name: &str, release: bool) -> anyhow::Result<()> { + print_status_msg( + StatusType::Notice, + &format!("no existing binary found for `{binary_name}`, building it now"), + ); + + let mut cmd = std::process::Command::new("cargo"); + cmd.args([ + "build", + PACKAGE_SHORT_FLAG, + package_name, + BINARY_FLAG, + binary_name, + ]); + if release { + cmd.arg(RELEASE_FLAG); + } + + // Inherit stdio so the user sees cargo's normal build output and any + // compile errors directly — we don't want to capture/reformat that. + let status = cmd.status().context("failed to spawn `cargo build`")?; + + anyhow::ensure!( + status.success(), + "`cargo build` failed for `{package_name}`" + ); + Ok(()) +} + fn is_current_executable(binary_path: &Path) -> bool { let Ok(current_exe) = std::env::current_exe() else { return false; @@ -172,14 +229,26 @@ fn resolve_binary_name(package_manager: &PackageManager) -> anyhow::Result {} 1 => return Ok(named_bins[0].to_string()), - _ => bail!( - "package `{}` has multiple [[bin]] targets.\n\ + _ => { + // if a default-run field exists lets use that + // https://doc.rust-lang.org/cargo/reference/manifest.html#the-default-run-field + if let Some(default_run) = manifest + .package + .as_ref() + .and_then(|p| p.default_run.as_deref()) + { + return Ok(default_run.to_string()); + } + + bail!( + "package `{}` has multiple [[bin]] targets.\n\ Specify which one `cot` should use by adding to its Cargo.toml:\n\ \n\ [package.metadata.cot]\n\ binary = \"your-binary-name\"", - package_manager.get_package_name(), - ), + package_manager.get_package_name(), + ) + } } manifest @@ -190,6 +259,10 @@ fn resolve_binary_name(package_manager: &PackageManager) -> anyhow::Result PathBuf { + if let Ok(dir) = std::env::var("CARGO_TARGET_DIR") { + return PathBuf::from(dir); + } + let mut dir = start_dir; loop { let candidate = dir.join("target"); @@ -207,49 +280,106 @@ fn resolve_target_dir(start_dir: &Path) -> PathBuf { fn load_or_refresh_metadata( binary_path: &Path, cache_path: &Path, -) -> anyhow::Result { +) -> anyhow::Result> { let current_mtime_secs = mtime_secs(binary_path)?; + // Fast path if we hit the cache if let Ok(bytes) = std::fs::read(cache_path) && let Ok(cache) = serde_json::from_slice::(&bytes) && cache.binary_mtime_secs == current_mtime_secs { - return Ok(cache.metadata); + return Ok(Some(cache.metadata)); } - let output = std::process::Command::new(binary_path) + // slow path + let mut child = std::process::Command::new(binary_path) .arg(METADATA_FLAG) - .output() + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() .with_context(|| format!("Failed to spawn {}", binary_path.display()))?; - if !output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - let mut msg = format!( - "Binary `{}` exited with status {} when queried for metadata.", + let mut std_err_piped = child.stderr.take().expect("Stderr should be piped"); + let mut std_out_piped = child.stdout.take().expect("Stdout should be piped"); + + let std_err_thread = std::thread::spawn(move || { + let mut buf = Vec::new(); + std_err_piped + .read_to_end(&mut buf) + .expect("reading to buffer should not fail"); + buf + }); + + let std_out_thread = std::thread::spawn(move || { + let mut buf = Vec::new(); + std_out_piped + .read_to_end(&mut buf) + .expect("reading to buffer should not fail"); + buf + }); + + let Some(status) = child + .wait_timeout(METADATA_TIMEOUT) + .with_context(|| format!("Failed to wait on {}", binary_path.display()))? + else { + let _ = child.kill(); + let _ = child.wait(); + bail!( + "the `{}` binary did not respond within {:?} when queried for metadata.", binary_path.display(), - output.status, + METADATA_TIMEOUT ); + }; - if !stderr.trim().is_empty() { - let _ = write!(msg, "\n\nstderr:\n{}", stderr.trim()); + let stdout = std_out_thread + .join() + .expect("joining thread handle should not fail"); + let stderr = std_err_thread + .join() + .expect("joining stderr thread should not fail"); + + if !status.success() { + let stderr_str = String::from_utf8_lossy(&stderr); + + let is_legacy_binary = status.code() == Some(2) + && stderr_str.contains(&format!("unexpected argument '{METADATA_FLAG}'")); + + if is_legacy_binary { + print_status_msg( + StatusType::Warning, + &format!( + "the `{}` binary doesn't recognize a flag `cot` uses to discover the binary's cli commands, \ + so they won't be listed in `cot --help`. This usually means the binary \ + was built against an older version of `cot`. To fix this, update your `cot`version", + binary_path.display(), + ), + ); + return Ok(None); } - if !stdout.trim().is_empty() { - let _ = write!(msg, "\n\nstdout:\n{}", stdout.trim()); + let mut msg = format!( + "the `{}` binary exited unexpectedly while `cot` was trying to determine the binary's cli commands.", + binary_path.display(), + ); + if !stderr_str.trim().is_empty() { + let _ = write!(msg, "\n\nstderr:\n{}", stderr_str.trim()); + } + let stdout_str = String::from_utf8_lossy(&stdout); + if !stdout_str.trim().is_empty() { + let _ = write!(msg, "\n\nstdout:\n{}", stdout_str.trim()); } bail!(msg); } - let metadata: ProjectMetadata = serde_json::from_slice(&output.stdout).with_context(|| { - let raw = String::from_utf8_lossy(&output.stdout); - format!( - "Binary `{}` returned invalid JSON for {METADATA_FLAG}.\n\nGot:\n{}", + if stdout.is_empty() { + // The binary ran but the metadata flag was ignored + bail!( + "the `{}` binary produced no output for {METADATA_FLAG}", binary_path.display(), - raw.trim(), - ) - })?; + ); + } + + let metadata = parse_metadata(&stdout, binary_path)?; write_cache( cache_path, @@ -259,7 +389,41 @@ fn load_or_refresh_metadata( }, )?; - Ok(metadata) + Ok(Some(metadata)) +} + +#[derive(Deserialize)] +struct MetadataVersionProbe { + version: u32, +} + +fn parse_metadata(bytes: &[u8], binary_path: &Path) -> anyhow::Result { + // check the version first before attempting to deserialize so we can show a + // clearer error message instead of the generic serde error message + let probe: MetadataVersionProbe = serde_json::from_slice(bytes).with_context(|| { + format!( + "the `{}` binary returned metadata with no readable version field.", + binary_path.display() + ) + })?; + + anyhow::ensure!( + probe.version == cot::metadata::METADATA_SCHEMA_VERSION, + "the `{}` binary was built against a `cot` version with metadata schema v{}, \ + but this `cot-cli` expects v{}. Try updating cot-cli (`cargo install --locked cot-cli`) \ + or rebuilding the project.", + binary_path.display(), + probe.version, + cot::metadata::METADATA_SCHEMA_VERSION, + ); + + serde_json::from_slice(bytes).with_context(|| { + format!( + "Binary `{}` returned invalid JSON for {METADATA_FLAG}\n\nstdout:\n{}", + binary_path.display(), + String::from_utf8_lossy(bytes).trim(), + ) + }) } fn mtime_secs(path: &Path) -> anyhow::Result { @@ -326,11 +490,13 @@ edition = "2024" about: None, aliases: vec![], subcommands: vec![], + args: vec![], } } fn metadata(binary_name: &str, command_names: &[&str]) -> ProjectMetadata { ProjectMetadata { + version: cot::metadata::METADATA_SCHEMA_VERSION, binary_name: binary_name.to_string(), commands: command_names.iter().map(|name| command(name)).collect(), } @@ -357,7 +523,7 @@ edition = "2024" fn load_returns_none_without_cargo_manifest() { let temp_dir = TempDir::new().unwrap(); - let result = load(temp_dir.path(), false, None).unwrap(); + let result = load(temp_dir.path(), false, None, false).unwrap(); assert!(result.is_none()); } @@ -366,7 +532,7 @@ edition = "2024" fn load_errors_when_start_path_does_not_exist() { let temp_dir = TempDir::new().unwrap(); - let result = load(&temp_dir.path().join("missing"), false, None); + let result = load(&temp_dir.path().join("missing"), false, None, false); assert!(result.is_err()); assert!( @@ -382,7 +548,7 @@ edition = "2024" let temp_dir = TempDir::new().unwrap(); write_package_manifest(temp_dir.path(), "demo", ""); - let result = load(temp_dir.path(), false, None).unwrap(); + let result = load(temp_dir.path(), false, None, false).unwrap(); assert!(result.is_none()); } @@ -395,11 +561,15 @@ edition = "2024" let binary_path = temp_dir.path().join("target/debug/demo"); write_metadata_script(&binary_path, &metadata("demo", &["serve"])); - let project = load(temp_dir.path(), false, None).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); assert_eq!(project.path, binary_path); - assert_eq!(project.metadata.binary_name, "demo"); - assert_eq!(project.metadata.commands[0].name, "serve"); + assert!(project.metadata.is_some()); + + let metadata = project.metadata.unwrap(); + + assert_eq!(metadata.binary_name, "demo"); + assert_eq!(metadata.commands[0].name, "serve"); assert!(command_cache_path(temp_dir.path()).exists()); } @@ -411,7 +581,7 @@ edition = "2024" let binary_path = temp_dir.path().join("target/release/demo"); write_metadata_script(&binary_path, &metadata("demo", &["serve"])); - let project = load(temp_dir.path(), true, None).unwrap().unwrap(); + let project = load(temp_dir.path(), true, None, false).unwrap().unwrap(); assert_eq!(project.path, binary_path); } @@ -431,10 +601,11 @@ path = "src/server.rs" let binary_path = temp_dir.path().join("target/debug/server"); write_metadata_script(&binary_path, &metadata("server", &["serve"])); - let project = load(temp_dir.path(), false, None).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); assert_eq!(project.path, binary_path); - assert_eq!(project.metadata.binary_name, "server"); + assert!(project.metadata.is_some()); + assert_eq!(project.metadata.unwrap().binary_name, "server"); } #[test] @@ -459,10 +630,11 @@ path = "src/worker.rs" let binary_path = temp_dir.path().join("target/debug/api"); write_metadata_script(&binary_path, &metadata("api", &["serve"])); - let project = load(temp_dir.path(), false, None).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); assert_eq!(project.path, binary_path); - assert_eq!(project.metadata.binary_name, "api"); + assert!(project.metadata.is_some()); + assert_eq!(project.metadata.unwrap().binary_name, "api"); } #[test] @@ -481,7 +653,7 @@ path = "src/worker.rs" "#, ); - let result = load(temp_dir.path(), false, None); + let result = load(temp_dir.path(), false, None, false); assert!(result.is_err()); let message = result.unwrap_err().to_string(); @@ -489,6 +661,108 @@ path = "src/worker.rs" assert!(message.contains("[package.metadata.cot]")); } + #[test] + #[cfg(unix)] + fn load_falls_back_to_no_metadata_on_command_failure() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest(temp_dir.path(), "demo", ""); + let binary_path = temp_dir.path().join("target/debug/demo"); + write_shell_script( + &binary_path, + "echo stdout message\necho stderr message >&2\nexit 42\n", + ); + + let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); + + assert!(project.metadata.is_none()); + } + + #[test] + #[cfg(unix)] + fn load_falls_back_to_no_metadata_on_invalid_json() { + let temp_dir = TempDir::new().unwrap(); + write_package_manifest(temp_dir.path(), "demo", ""); + let binary_path = temp_dir.path().join("target/debug/demo"); + write_shell_script(&binary_path, "echo 'not json'\n"); + + let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); + + assert!(project.metadata.is_none()); + } + + #[test] + #[cfg(unix)] + fn load_or_refresh_metadata_reports_command_failure_with_output() { + let temp_dir = TempDir::new().unwrap(); + let binary_path = temp_dir.path().join("demo"); + write_shell_script( + &binary_path, + "echo stdout message\necho stderr message >&2\nexit 42\n", + ); + let cache_path = command_cache_path(temp_dir.path()); + + let result = load_or_refresh_metadata(&binary_path, &cache_path); + + assert!(result.is_err()); + let message = format!("{:#}", result.unwrap_err()); + assert!(message.contains("exited unexpectedly")); + assert!(message.contains("stdout message")); + assert!(message.contains("stderr message")); + } + + #[test] + #[cfg(unix)] + fn load_or_refresh_metadata_reports_invalid_json() { + let temp_dir = TempDir::new().unwrap(); + let binary_path = temp_dir.path().join("demo"); + write_shell_script(&binary_path, "echo 'not json'\n"); + let cache_path = command_cache_path(temp_dir.path()); + + let result = load_or_refresh_metadata(&binary_path, &cache_path); + + assert!(result.is_err()); + let message = format!("{:#}", result.unwrap_err()); + assert!(message.contains("no readable version field")); + } + + #[test] + #[cfg(unix)] + fn load_or_refresh_metadata_returns_none_for_legacy_binary() { + let temp_dir = TempDir::new().unwrap(); + let binary_path = temp_dir.path().join("demo"); + write_shell_script( + &binary_path, + &format!("echo \"error: unexpected argument '{METADATA_FLAG}'\" >&2\nexit 2\n"), + ); + let cache_path = command_cache_path(temp_dir.path()); + + let result = load_or_refresh_metadata(&binary_path, &cache_path).unwrap(); + + assert!(result.is_none()); + } + + #[test] + fn parse_metadata_reports_schema_version_mismatch() { + let bytes = br#"{"version":999,"binary_name":"demo","commands":[]}"#; + + let result = parse_metadata(bytes, &PathBuf::from("target/debug/demo")); + + assert!(result.is_err()); + let message = result.unwrap_err().to_string(); + assert!(message.contains("metadata schema v999")); + assert!(message.contains("cargo install --locked cot-cli")); + } + + #[test] + fn parse_metadata_succeeds_on_matching_shape() { + let meta = metadata("demo", &["serve"]); + let bytes = serde_json::to_vec(&meta).unwrap(); + + let result = parse_metadata(&bytes, &PathBuf::from("target/debug/demo")); + + assert!(result.is_ok()); + } + #[test] fn workspace_root_requires_package_when_ambiguous() { let temp_dir = TempDir::new().unwrap(); @@ -496,7 +770,7 @@ path = "src/worker.rs" write_package_manifest(&temp_dir.path().join("api"), "api", ""); write_package_manifest(&temp_dir.path().join("web"), "web", ""); - let result = load(temp_dir.path(), false, None); + let result = load(temp_dir.path(), false, None, false); assert!(result.is_err()); let message = result.unwrap_err().to_string(); @@ -512,7 +786,7 @@ path = "src/worker.rs" write_package_manifest(&temp_dir.path().join("api"), "api", ""); write_package_manifest(&temp_dir.path().join("web"), "web", ""); - let result = load(temp_dir.path(), false, Some("missing")); + let result = load(temp_dir.path(), false, Some("missing"), false); assert!(result.is_err()); let message = result.unwrap_err().to_string(); @@ -531,7 +805,9 @@ path = "src/worker.rs" let binary_path = temp_dir.path().join("target/debug/api"); write_metadata_script(&binary_path, &metadata("api", &["check"])); - let project = load(temp_dir.path(), false, Some("api")).unwrap().unwrap(); + let project = load(temp_dir.path(), false, Some("api"), false) + .unwrap() + .unwrap(); assert_eq!(project.path, binary_path); assert!(temp_dir.path().join("api").exists()); @@ -547,12 +823,13 @@ path = "src/worker.rs" let binary_path = temp_dir.path().join("target/debug/web"); write_metadata_script(&binary_path, &metadata("web", &["check"])); - let project = load(&temp_dir.path().join("web"), false, None) + let project = load(&temp_dir.path().join("web"), false, None, false) .unwrap() .unwrap(); assert_eq!(project.path, binary_path); - assert_eq!(project.metadata.binary_name, "web"); + assert!(project.metadata.is_some()); + assert_eq!(project.metadata.unwrap().binary_name, "web"); } #[test] @@ -571,9 +848,10 @@ path = "src/worker.rs" }; write_cache(&command_cache_path(temp_dir.path()), &cache).unwrap(); - let project = load(temp_dir.path(), false, None).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); - assert_eq!(project.metadata.commands[0].name, "cached"); + assert!(project.metadata.is_some()); + assert_eq!(project.metadata.unwrap().commands[0].name, "cached"); } #[test] @@ -589,46 +867,10 @@ path = "src/worker.rs" }; write_cache(&command_cache_path(temp_dir.path()), &cache).unwrap(); - let project = load(temp_dir.path(), false, None).unwrap().unwrap(); - - assert_eq!(project.metadata.commands[0].name, "fresh"); - } - - #[test] - #[cfg(unix)] - fn load_reports_metadata_command_failure_with_output() { - let temp_dir = TempDir::new().unwrap(); - write_package_manifest(temp_dir.path(), "demo", ""); - let binary_path = temp_dir.path().join("target/debug/demo"); - write_shell_script( - &binary_path, - "echo stdout message\necho stderr message >&2\nexit 42\n", - ); + let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); - let result = load(temp_dir.path(), false, None); - - assert!(result.is_err()); - let message = format!("{:#}", result.unwrap_err()); - assert!(message.contains("unable to load metadata")); - assert!(message.contains("exited with status")); - assert!(message.contains("stdout message")); - assert!(message.contains("stderr message")); - } - - #[test] - #[cfg(unix)] - fn load_reports_invalid_metadata_json() { - let temp_dir = TempDir::new().unwrap(); - write_package_manifest(temp_dir.path(), "demo", ""); - let binary_path = temp_dir.path().join("target/debug/demo"); - write_shell_script(&binary_path, "echo 'not json'\n"); - - let result = load(temp_dir.path(), false, None); - - assert!(result.is_err()); - let message = format!("{:#}", result.unwrap_err()); - assert!(message.contains(METADATA_FLAG)); - assert!(message.contains("not json")); + assert!(project.metadata.is_some()); + assert_eq!(project.metadata.unwrap().commands[0].name, "fresh"); } #[test] diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap index a407b0db0..bd54c3cfb 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_bash.snap @@ -116,7 +116,7 @@ _cot() { case "${cmd}" in cot) - opts="-p -v -q -h -V --release --package --verbose --quiet --help --version new migration cli help" + opts="-p -v -q -h -V --release --build --package --verbose --quiet --help --version new migration cli help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -138,7 +138,7 @@ _cot() { return 0 ;; cot__subcmd__cli) - opts="-p -v -q -h --release --package --verbose --quiet --help manpages completions help" + opts="-p -v -q -h --release --build --package --verbose --quiet --help manpages completions help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -160,7 +160,7 @@ _cot() { return 0 ;; cot__subcmd__cli__subcmd__completions) - opts="-p -v -q -h --release --package --verbose --quiet --help bash elvish fish powershell zsh" + opts="-p -v -q -h --release --build --package --verbose --quiet --help bash elvish fish powershell zsh" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -238,7 +238,7 @@ _cot() { return 0 ;; cot__subcmd__cli__subcmd__manpages) - opts="-o -c -p -v -q -h --output-dir --create --release --package --verbose --quiet --help" + opts="-o -c -p -v -q -h --output-dir --create --release --build --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -408,7 +408,7 @@ _cot() { return 0 ;; cot__subcmd__migration) - opts="-p -v -q -h --release --package --verbose --quiet --help list make new help" + opts="-p -v -q -h --release --build --package --verbose --quiet --help list make new help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -500,7 +500,7 @@ _cot() { return 0 ;; cot__subcmd__migration__subcmd__list) - opts="-p -v -q -h --release --package --verbose --quiet --help" + opts="-p -v -q -h --release --build --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -522,7 +522,7 @@ _cot() { return 0 ;; cot__subcmd__migration__subcmd__make) - opts="-p -v -q -h --app-name --output-dir --release --package --verbose --quiet --help" + opts="-p -v -q -h --app-name --output-dir --release --build --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -552,7 +552,7 @@ _cot() { return 0 ;; cot__subcmd__migration__subcmd__new) - opts="-p -v -q -h --app-name --release --package --verbose --quiet --help" + opts="-p -v -q -h --app-name --release --build --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -578,7 +578,7 @@ _cot() { return 0 ;; cot__subcmd__new) - opts="-p -v -q -h --name --use-git --cot-path --release --package --verbose --quiet --help" + opts="-p -v -q -h --name --use-git --cot-path --release --build --package --verbose --quiet --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap index 1bb6e0952..cb2f5c200 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_elvish.snap @@ -33,6 +33,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' cand --release 'Use target/release instead of target/debug when looking for the project binary' + cand --build 'Build the binary if it does not exist' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -53,6 +54,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand --package 'Package to use, in case you''re running this in a workspace' cand --use-git 'Use the latest `cot` version from git instead of a published crate' cand --release 'Use target/release instead of target/debug when looking for the project binary' + cand --build 'Build the binary if it does not exist' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -64,6 +66,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' cand --release 'Use target/release instead of target/debug when looking for the project binary' + cand --build 'Build the binary if it does not exist' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -79,6 +82,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' cand --release 'Use target/release instead of target/debug when looking for the project binary' + cand --build 'Build the binary if it does not exist' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -92,6 +96,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' cand --release 'Use target/release instead of target/debug when looking for the project binary' + cand --build 'Build the binary if it does not exist' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -104,6 +109,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' cand --release 'Use target/release instead of target/debug when looking for the project binary' + cand --build 'Build the binary if it does not exist' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -129,6 +135,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' cand --release 'Use target/release instead of target/debug when looking for the project binary' + cand --build 'Build the binary if it does not exist' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -147,6 +154,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand -c 'Create the directory if it doesn''t exist' cand --create 'Create the directory if it doesn''t exist' cand --release 'Use target/release instead of target/debug when looking for the project binary' + cand --build 'Build the binary if it does not exist' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' @@ -158,6 +166,7 @@ set edit:completion:arg-completer[cot] = {|@words| cand -p 'Package to use, in case you''re running this in a workspace' cand --package 'Package to use, in case you''re running this in a workspace' cand --release 'Use target/release instead of target/debug when looking for the project binary' + cand --build 'Build the binary if it does not exist' cand -v 'Increase logging verbosity' cand --verbose 'Increase logging verbosity' cand -q 'Decrease logging verbosity' diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap index 1d6cc2ba2..a3b63c6e3 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_fish.snap @@ -12,7 +12,7 @@ exit_code: 0 ----- stdout ----- # Print an optspec for argparse to handle cmd's options that are independent of any subcommand. function __fish_cot_global_optspecs - string join \n release p/package= v/verbose q/quiet h/help V/version + string join \n release build p/package= v/verbose q/quiet h/help V/version end function __fish_cot_needs_command @@ -38,6 +38,7 @@ end complete -c cot -n "__fish_cot_needs_command" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_needs_command" -l release -d 'Use target/release instead of target/debug when looking for the project binary' +complete -c cot -n "__fish_cot_needs_command" -l build -d 'Build the binary if it does not exist' complete -c cot -n "__fish_cot_needs_command" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_needs_command" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_needs_command" -s h -l help -d 'Print help' @@ -51,11 +52,13 @@ complete -c cot -n "__fish_cot_using_subcommand new" -l cot-path -d 'Use `cot` f complete -c cot -n "__fish_cot_using_subcommand new" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand new" -l use-git -d 'Use the latest `cot` version from git instead of a published crate' complete -c cot -n "__fish_cot_using_subcommand new" -l release -d 'Use target/release instead of target/debug when looking for the project binary' +complete -c cot -n "__fish_cot_using_subcommand new" -l build -d 'Build the binary if it does not exist' complete -c cot -n "__fish_cot_using_subcommand new" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand new" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand new" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -l release -d 'Use target/release instead of target/debug when looking for the project binary' +complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -l build -d 'Build the binary if it does not exist' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -s h -l help -d 'Print help' @@ -65,6 +68,7 @@ complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_s complete -c cot -n "__fish_cot_using_subcommand migration; and not __fish_seen_subcommand_from list make new help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -l release -d 'Use target/release instead of target/debug when looking for the project binary' +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -l build -d 'Build the binary if it does not exist' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' @@ -72,12 +76,14 @@ complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subco complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l output-dir -d 'Directory to write the migrations to [default: the migrations/ directory in the crate\'s src/ directory]' -r -F complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l release -d 'Use target/release instead of target/debug when looking for the project binary' +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -l build -d 'Build the binary if it does not exist' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from make" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l app-name -d 'Name of the app to use in the migration (default: crate name)' -r complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l release -d 'Use target/release instead of target/debug when looking for the project binary' +complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -l build -d 'Build the binary if it does not exist' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from new" -s h -l help -d 'Print help' @@ -87,6 +93,7 @@ complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subco complete -c cot -n "__fish_cot_using_subcommand migration; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -l release -d 'Use target/release instead of target/debug when looking for the project binary' +complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -l build -d 'Build the binary if it does not exist' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and not __fish_seen_subcommand_from manpages completions help" -s h -l help -d 'Print help' @@ -97,11 +104,13 @@ complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_ complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s c -l create -d 'Create the directory if it doesn\'t exist' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -l release -d 'Use target/release instead of target/debug when looking for the project binary' +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -l build -d 'Build the binary if it does not exist' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from manpages" -s h -l help -d 'Print help' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s p -l package -d 'Package to use, in case you\'re running this in a workspace' -r complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -l release -d 'Use target/release instead of target/debug when looking for the project binary' +complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -l build -d 'Build the binary if it does not exist' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s v -l verbose -d 'Increase logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s q -l quiet -d 'Decrease logging verbosity' complete -c cot -n "__fish_cot_using_subcommand cli; and __fish_seen_subcommand_from completions" -s h -l help -d 'Print help' diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap index 5a5803e53..39ad874ea 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_powershell.snap @@ -36,6 +36,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') + [CompletionResult]::new('--build', '--build', [CompletionResultType]::ParameterName, 'Build the binary if it does not exist') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -57,6 +58,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--use-git', '--use-git', [CompletionResultType]::ParameterName, 'Use the latest `cot` version from git instead of a published crate') [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') + [CompletionResult]::new('--build', '--build', [CompletionResultType]::ParameterName, 'Build the binary if it does not exist') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -69,6 +71,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') + [CompletionResult]::new('--build', '--build', [CompletionResultType]::ParameterName, 'Build the binary if it does not exist') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -85,6 +88,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') + [CompletionResult]::new('--build', '--build', [CompletionResultType]::ParameterName, 'Build the binary if it does not exist') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -99,6 +103,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') + [CompletionResult]::new('--build', '--build', [CompletionResultType]::ParameterName, 'Build the binary if it does not exist') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -112,6 +117,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') + [CompletionResult]::new('--build', '--build', [CompletionResultType]::ParameterName, 'Build the binary if it does not exist') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -143,6 +149,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') + [CompletionResult]::new('--build', '--build', [CompletionResultType]::ParameterName, 'Build the binary if it does not exist') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -162,6 +169,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Create the directory if it doesn''t exist') [CompletionResult]::new('--create', '--create', [CompletionResultType]::ParameterName, 'Create the directory if it doesn''t exist') [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') + [CompletionResult]::new('--build', '--build', [CompletionResultType]::ParameterName, 'Build the binary if it does not exist') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') @@ -174,6 +182,7 @@ Register-ArgumentCompleter -Native -CommandName 'cot' -ScriptBlock { [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--package', '--package', [CompletionResultType]::ParameterName, 'Package to use, in case you''re running this in a workspace') [CompletionResult]::new('--release', '--release', [CompletionResultType]::ParameterName, 'Use target/release instead of target/debug when looking for the project binary') + [CompletionResult]::new('--build', '--build', [CompletionResultType]::ParameterName, 'Build the binary if it does not exist') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Increase logging verbosity') [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Decrease logging verbosity') diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap index 9dd5f2a37..aaa92e885 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__completions_zsh.snap @@ -30,6 +30,7 @@ _cot() { '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--release[Use target/release instead of target/debug when looking for the project binary]' \ +'--build[Build the binary if it does not exist]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -55,6 +56,7 @@ _arguments "${_arguments_options[@]}" : \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--use-git[Use the latest \`cot\` version from git instead of a published crate]' \ '--release[Use target/release instead of target/debug when looking for the project binary]' \ +'--build[Build the binary if it does not exist]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -69,6 +71,7 @@ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--release[Use target/release instead of target/debug when looking for the project binary]' \ +'--build[Build the binary if it does not exist]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -90,6 +93,7 @@ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--release[Use target/release instead of target/debug when looking for the project binary]' \ +'--build[Build the binary if it does not exist]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -106,6 +110,7 @@ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--release[Use target/release instead of target/debug when looking for the project binary]' \ +'--build[Build the binary if it does not exist]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -121,6 +126,7 @@ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--release[Use target/release instead of target/debug when looking for the project binary]' \ +'--build[Build the binary if it does not exist]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -172,6 +178,7 @@ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--release[Use target/release instead of target/debug when looking for the project binary]' \ +'--build[Build the binary if it does not exist]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -197,6 +204,7 @@ _arguments "${_arguments_options[@]}" : \ '-c[Create the directory if it doesn'\''t exist]' \ '--create[Create the directory if it doesn'\''t exist]' \ '--release[Use target/release instead of target/debug when looking for the project binary]' \ +'--build[Build the binary if it does not exist]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ @@ -210,6 +218,7 @@ _arguments "${_arguments_options[@]}" : \ '-p+[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--package=[Package to use, in case you'\''re running this in a workspace]:PACKAGE:_default' \ '--release[Use target/release instead of target/debug when looking for the project binary]' \ +'--build[Build the binary if it does not exist]' \ '*-v[Increase logging verbosity]' \ '*--verbose[Increase logging verbosity]' \ '(-v --verbose)*-q[Decrease logging verbosity]' \ diff --git a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap index 43d02d9c6..361ac6d40 100644 --- a/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap +++ b/cot-cli/tests/snapshot_testing/cli/snapshots/cli__snapshot_testing__cli__no_args.snap @@ -22,6 +22,7 @@ Commands: Options: --release Use target/release instead of target/debug when looking for the project binary + --build Build the binary if it does not exist -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap index 9ac2a4d0e..0bcc121f5 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help.snap @@ -21,6 +21,7 @@ Commands: Options: --release Use target/release instead of target/debug when looking for the project binary + --build Build the binary if it does not exist -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap index 94e49a54f..2fd713769 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_completions.snap @@ -20,6 +20,7 @@ Arguments: Options: --release Use target/release instead of target/debug when looking for the project binary + --build Build the binary if it does not exist -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap index 1422eb943..afb4b5e63 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_cli_manpages.snap @@ -18,6 +18,7 @@ Options: -o, --output-dir Directory to write the manpages to [default: current directory] --release Use target/release instead of target/debug when looking for the project binary + --build Build the binary if it does not exist -c, --create Create the directory if it doesn't exist -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap index b98d57787..58c707f51 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration.snap @@ -22,6 +22,7 @@ Commands: Options: --release Use target/release instead of target/debug when looking for the project binary + --build Build the binary if it does not exist -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap index 4ae1b261c..20f2358ea 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_list.snap @@ -20,6 +20,7 @@ Arguments: Options: --release Use target/release instead of target/debug when looking for the project binary + --build Build the binary if it does not exist -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap index 224fbd85b..1c002cb34 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_migration_make.snap @@ -21,6 +21,7 @@ Options: --app-name Name of the app to use in the migration [default: crate name] --release Use target/release instead of target/debug when looking for the project binary + --build Build the binary if it does not exist --output-dir Directory to write the migrations to [default: the migrations/ directory in the crate's src/ directory] -p, --package Package to use, in case you're running this in a workspace diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap index c526649d7..d86092bd9 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__help_new.snap @@ -20,9 +20,10 @@ Options: --name Set the resulting crate name [default: the directory name] --release Use target/release instead of target/debug when looking for the project binary - -p, --package Package to use, in case you're running this in a workspace + --build Build the binary if it does not exist --use-git Use the latest `cot` version from git instead of a published crate --cot-path Use `cot` from the specified path instead of a published crate + -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity -h, --help Print help diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap index 972d94aad..0a8a6c660 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__long_help.snap @@ -19,23 +19,14 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --release - Use target/release instead of target/debug when looking for the project binary - - -p, --package - Package to use, in case you're running this in a workspace - - -v, --verbose... - Increase logging verbosity - - -q, --quiet... - Decrease logging verbosity - - -h, --help - Print help - - -V, --version - Print version + --release Use target/release instead of target/debug when looking for the project + binary + --build Build the binary if it does not exist + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help + -V, --version Print version ----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap index c192bef20..8518a72c3 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__no_args.snap @@ -22,6 +22,7 @@ Commands: Options: --release Use target/release instead of target/debug when looking for the project binary + --build Build the binary if it does not exist -p, --package Package to use, in case you're running this in a workspace -v, --verbose... Increase logging verbosity -q, --quiet... Decrease logging verbosity diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap index 1e24032b2..0aaa4d151 100644 --- a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__short_help.snap @@ -19,23 +19,14 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --release - Use target/release instead of target/debug when looking for the project binary - - -p, --package - Package to use, in case you're running this in a workspace - - -v, --verbose... - Increase logging verbosity - - -q, --quiet... - Decrease logging verbosity - - -h, --help - Print help - - -V, --version - Print version + --release Use target/release instead of target/debug when looking for the project + binary + --build Build the binary if it does not exist + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help + -V, --version Print version ----- stderr ----- diff --git a/cot/src/metadata.rs b/cot/src/metadata.rs index c4d2498ac..9ef9079d8 100644 --- a/cot/src/metadata.rs +++ b/cot/src/metadata.rs @@ -1,20 +1,85 @@ //! Metadata exported by Cot project binaries for the proxying `cot` CLI. -use clap::Command; +use clap::{Arg, Command}; use serde::{Deserialize, Serialize}; +/// The current version of the `ProjectMetadata` JSON schema. +pub const METADATA_SCHEMA_VERSION: u32 = 1; + /// Flag used to ask a Cot project binary to print its CLI metadata as JSON. -pub const METADATA_FLAG: &str = "--metadata"; +pub const METADATA_FLAG: &str = "--cot-internal-cli-metadata"; /// Metadata describing the commands exposed by a Cot project binary. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ProjectMetadata { + /// Schema version this metadata was serialized with. + pub version: u32, /// Name of the project binary that produced the metadata. pub binary_name: String, /// Top-level commands exposed by the project binary. pub commands: Vec, } +impl ProjectMetadata { + /// Create new Project metadata + pub fn new(cmd: &Command) -> Self { + ProjectMetadata { + version: METADATA_SCHEMA_VERSION, + binary_name: cmd.get_name().to_string(), + commands: cmd + .get_subcommands() + .filter(|subcmd| !subcmd.is_hide_set()) + .map(CommandMeta::from) + .collect(), + } + } +} + +impl From<&Command> for ProjectMetadata { + fn from(cmd: &Command) -> Self { + Self::new(cmd) + } +} + +/// Arguments for a CLI command +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ArgMeta { + /// Argument Name. + pub name: String, + /// long option name. + pub long: Option, + /// short option name. + pub short: Option, + /// Help text for the argument. + pub help: Option, + /// Whether the argument is required. + pub required: bool, + /// Whether the argument is a positional argument. + pub is_positional: bool, + /// Whether the argument takes a value. + pub takes_value: bool, + /// The value name for this argument. + pub value_name: Option, +} + +impl From<&Arg> for ArgMeta { + fn from(arg: &Arg) -> Self { + Self { + name: arg.get_id().to_string(), + long: arg.get_long().map(str::to_string), + short: arg.get_short(), + help: arg.get_help().map(ToString::to_string), + required: arg.is_required_set(), + is_positional: arg.is_positional(), + takes_value: arg.get_num_args().is_some_and(|n| n.takes_values()), + value_name: arg + .get_value_names() + .and_then(|v| v.first()) + .map(ToString::to_string), + } + } +} + /// Metadata for a single CLI command. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct CommandMeta { @@ -26,30 +91,27 @@ pub struct CommandMeta { pub aliases: Vec, /// Nested subcommands exposed by this command. pub subcommands: Vec, + /// Arguments supported by the command. + pub args: Vec, } -/// Extract proxyable command metadata from a clap command definition. -pub fn extract(cmd: &Command) -> ProjectMetadata { - ProjectMetadata { - binary_name: cmd.get_name().to_string(), - commands: cmd - .get_subcommands() - .filter(|subcmd| !subcmd.is_hide_set()) - .map(extract_command) - .collect(), - } -} - -fn extract_command(cmd: &Command) -> CommandMeta { - CommandMeta { - name: cmd.get_name().to_string(), - about: cmd.get_about().map(ToString::to_string), - aliases: cmd.get_all_aliases().map(ToString::to_string).collect(), - subcommands: cmd - .get_subcommands() - .filter(|subcmd| !subcmd.is_hide_set()) - .map(extract_command) - .collect(), +impl From<&Command> for CommandMeta { + fn from(cmd: &Command) -> Self { + CommandMeta { + name: cmd.get_name().to_string(), + about: cmd.get_about().map(ToString::to_string), + aliases: cmd.get_all_aliases().map(ToString::to_string).collect(), + subcommands: cmd + .get_subcommands() + .filter(|subcmd| !subcmd.is_hide_set()) + .map(CommandMeta::from) + .collect(), + args: cmd + .get_arguments() + .filter(|a| a.get_id() != "help" && a.get_id() != "version") + .map(ArgMeta::from) + .collect(), + } } } @@ -58,12 +120,12 @@ mod tests { use super::*; #[test] - fn test_extract() { + fn test_project_metadata_from() { let command = Command::new("demo") .subcommand(Command::new("serve").about("Serve requests")) .subcommand(Command::new("secret").hide(true)); - let metadata = extract(&command); + let metadata = ProjectMetadata::from(&command); assert_eq!(metadata.binary_name, "demo"); assert_eq!(metadata.commands.len(), 1); @@ -75,7 +137,7 @@ mod tests { } #[test] - fn test_extract_command_with_visible_aliases() { + fn test_from_command_with_visible_aliases() { let command = Command::new("demo").subcommand( Command::new("database") .visible_alias("db") @@ -83,7 +145,7 @@ mod tests { .subcommand(Command::new("internal").hide(true)), ); - let metadata = extract(&command); + let metadata = ProjectMetadata::from(&command); let database = &metadata.commands[0]; assert_eq!(database.name, "database"); @@ -94,11 +156,90 @@ mod tests { } #[test] - fn test_extract_command_with_no_about() { + fn test_from_command_with_no_about() { let command = Command::new("demo").subcommand(Command::new("plain")); - let metadata = extract(&command); + let metadata = ProjectMetadata::from(&command); assert_eq!(metadata.commands[0].about, None); } + + #[test] + fn command_meta_from_captures_args() { + let command = Command::new("demo").subcommand( + Command::new("rollback") + .arg( + Arg::new("migration_name") + .value_name("MIGRATION_NAME") + .required(true), + ) + .arg( + Arg::new("dry-run") + .long("dry-run") + .action(clap::ArgAction::SetTrue), + ), + ); + + let metadata = ProjectMetadata::from(&command); + let rollback = &metadata.commands[0]; + + assert_eq!(rollback.args.len(), 2); + + let positional = rollback + .args + .iter() + .find(|a| a.name == "migration_name") + .unwrap(); + assert!(positional.is_positional); + assert!(positional.required); + assert_eq!(positional.value_name.as_deref(), Some("MIGRATION_NAME")); + + let flag = rollback.args.iter().find(|a| a.name == "dry-run").unwrap(); + assert!(!flag.is_positional); + assert_eq!(flag.long.as_deref(), Some("dry-run")); + assert!(!flag.takes_value); + } + + #[test] + fn command_meta_from_excludes_help_and_version_ids() { + let command = Command::new("demo").subcommand( + Command::new("sub") + .arg(Arg::new("help").long("help")) + .arg(Arg::new("version").long("version")) + .arg(Arg::new("real").long("real")), + ); + + let metadata = ProjectMetadata::from(&command); + let sub = &metadata.commands[0]; + + assert_eq!(sub.args.len(), 1); + assert_eq!(sub.args[0].name, "real"); + } + + #[test] + fn arg_meta_from_flag_arg() { + let arg = Arg::new("verbose") + .short('v') + .long("verbose") + .action(clap::ArgAction::SetTrue); + + let meta = ArgMeta::from(&arg); + + assert_eq!(meta.name, "verbose"); + assert_eq!(meta.short, Some('v')); + assert_eq!(meta.long.as_deref(), Some("verbose")); + assert!(!meta.takes_value); + assert!(!meta.is_positional); + } + + #[test] + fn arg_meta_from_positional_arg() { + let arg = Arg::new("path").value_name("PATH").required(true); + + let meta = ArgMeta::from(&arg); + + assert!(meta.is_positional); + assert!(meta.required); + assert_eq!(meta.value_name.as_deref(), Some("PATH")); + } } diff --git a/cot/src/project.rs b/cot/src/project.rs index 860f64b16..ef573effc 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -60,6 +60,7 @@ use crate::error::UncaughtPanic; use crate::error::handler::{DynErrorPageHandler, RequestOuterError}; use crate::error_page::Diagnostics; use crate::html::Html; +use crate::metadata::{METADATA_FLAG, ProjectMetadata}; use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer}; use crate::request::{Request, RequestExt, RequestHead}; use crate::response::{IntoResponse, Response}; @@ -939,8 +940,9 @@ impl Bootstrapper { cli.set_metadata(self.project.cli_metadata()); self.project.register_tasks(&mut cli); - if std::env::args().any(|arg| arg == cot::metadata::METADATA_FLAG) { - let meta = cot::metadata::extract(cli.command()); + if std::env::args().any(|arg| arg == METADATA_FLAG) { + let meta = ProjectMetadata::from(cli.command()); + println!("{}", serde_json::to_string_pretty(&meta).unwrap()); std::process::exit(0); } From b02da44e0bd44ac8deb55286bc0178a90751a29b Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 12 Aug 2026 03:13:13 +0000 Subject: [PATCH 17/19] comment improve --- cot-cli/src/project.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cot-cli/src/project.rs b/cot-cli/src/project.rs index eab3c459e..c511e4f1d 100644 --- a/cot-cli/src/project.rs +++ b/cot-cli/src/project.rs @@ -142,8 +142,6 @@ fn build_binary(package_name: &str, binary_name: &str, release: bool) -> anyhow: cmd.arg(RELEASE_FLAG); } - // Inherit stdio so the user sees cargo's normal build output and any - // compile errors directly — we don't want to capture/reformat that. let status = cmd.status().context("failed to spawn `cargo build`")?; anyhow::ensure!( @@ -292,6 +290,13 @@ fn load_or_refresh_metadata( } // slow path + // Both stderr and stdout are piped, to avoid deadlock. Pipe buffers + // are a fixed OS size, so if the child fills one while we're still + // waiting to read the other, its write call blocks and it can never + // finish producing output (or exit) for us to read. To avoid this we + // drain stdout and stderr on separate threads concurrently. + // https://doc.rust-lang.org/std/process/index.html#handling-io + // https://docs.rs/os_pipe/latest/os_pipe/#common-deadlocks-related-to-pipes let mut child = std::process::Command::new(binary_path) .arg(METADATA_FLAG) .stdout(Stdio::piped()) From 545e976f564df9036431d36742bb35ba3229f3c3 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 12 Aug 2026 19:46:08 +0000 Subject: [PATCH 18/19] Add test harness --- cot-cli/Cargo.toml | 8 +- cot-cli/src/lib.rs | 2 + cot-cli/src/main.rs | 16 +- cot-cli/src/project.rs | 30 +- cot-cli/src/test_harness.rs | 887 ++++++++++++++++++ .../tests/snapshot_testing/external/check.rs | 40 + .../tests/snapshot_testing/external/mod.rs | 1 + ...eck__check_forwards_to_project_binary.snap | 13 + ..._no_project_binary_reports_build_hint.snap | 14 + ...iter_fails_with_unsupported_flag_name.snap | 19 + ...nized_command_reports_unknown_command.snap | 13 + .../tests/snapshot_testing/help/external.rs | 76 ++ cot-cli/tests/snapshot_testing/help/mod.rs | 2 + ...ing__help__check_help_shows_real_task.snap | 20 + ...stered_task_appears_in_top_level_help.snap | 37 + ...om_task_help_shows_reconstructed_args.snap | 17 + ..._external__check_help_shows_real_task.snap | 20 + ...stered_task_appears_in_top_level_help.snap | 37 + ...om_task_help_shows_reconstructed_args.snap | 17 + ..._help_merges_real_rollback_subcommand.snap | 27 + ...succeeds_proving_command_is_reachable.snap | 26 + ...tion_unknown_subcommand_fails_cleanly.snap | 28 + ...ed_custom_group_help_merges_correctly.snap | 24 + ...vel_help_merges_real_project_commands.snap | 37 + ..._help_merges_real_rollback_subcommand.snap | 27 + ...succeeds_proving_command_is_reachable.snap | 26 + ...tion_unknown_subcommand_fails_cleanly.snap | 28 + ...ed_custom_group_help_merges_correctly.snap | 24 + ...vel_help_merges_real_project_commands.snap | 37 + cot-cli/tests/snapshot_testing/mod.rs | 25 +- 30 files changed, 1550 insertions(+), 28 deletions(-) create mode 100644 cot-cli/src/test_harness.rs create mode 100644 cot-cli/tests/snapshot_testing/external/check.rs create mode 100644 cot-cli/tests/snapshot_testing/external/mod.rs create mode 100644 cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__check_forwards_to_project_binary.snap create mode 100644 cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__check_with_no_project_binary_reports_build_hint.snap create mode 100644 cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__double_dash_delimiter_fails_with_unsupported_flag_name.snap create mode 100644 cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__unrecognized_command_reports_unknown_command.snap create mode 100644 cot-cli/tests/snapshot_testing/help/external.rs create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__check_help_shows_real_task.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__custom_registered_task_appears_in_top_level_help.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__custom_task_help_shows_reconstructed_args.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__check_help_shows_real_task.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__custom_registered_task_appears_in_top_level_help.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__custom_task_help_shows_reconstructed_args.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_help_merges_real_rollback_subcommand.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_rollback_help_succeeds_proving_command_is_reachable.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_unknown_subcommand_fails_cleanly.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__nested_custom_group_help_merges_correctly.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__top_level_help_merges_real_project_commands.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_help_merges_real_rollback_subcommand.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_rollback_help_succeeds_proving_command_is_reachable.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_unknown_subcommand_fails_cleanly.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__nested_custom_group_help_merges_correctly.snap create mode 100644 cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__top_level_help_merges_real_project_commands.snap diff --git a/cot-cli/Cargo.toml b/cot-cli/Cargo.toml index 4080d79b1..c805ef30a 100644 --- a/cot-cli/Cargo.toml +++ b/cot-cli/Cargo.toml @@ -21,6 +21,7 @@ workspace = true [dependencies] anyhow.workspace = true +assert_cmd = {workspace = true, optional = true} cargo_toml.workspace = true chrono.workspace = true clap = { workspace = true, features = ["derive", "env", "wrap_help", "string"] } @@ -44,14 +45,17 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true} wait-timeout = { workspace = true } +tempfile = {workspace = true, optional = true} [dev-dependencies] cot-cli = { path = ".", features = ["test_utils"] } assert_cmd.workspace = true insta.workspace = true insta-cmd.workspace = true -tempfile.workspace = true + trybuild.workspace = true [features] -test_utils = [] +test_utils = [ + "dep:tempfile" +] diff --git a/cot-cli/src/lib.rs b/cot-cli/src/lib.rs index c76021490..07364dac1 100644 --- a/cot-cli/src/lib.rs +++ b/cot-cli/src/lib.rs @@ -5,6 +5,8 @@ pub mod handlers; pub mod migration_generator; pub mod new_project; pub mod project; +#[cfg(any(test, feature = "test_utils"))] +pub mod test_harness; #[cfg(feature = "test_utils")] pub mod test_utils; mod utils; diff --git a/cot-cli/src/main.rs b/cot-cli/src/main.rs index 19d53654d..999354f0b 100644 --- a/cot-cli/src/main.rs +++ b/cot-cli/src/main.rs @@ -23,6 +23,7 @@ fn resolve_help_request(args: &[String]) -> Option> { while let Some(arg) = iter.next() { match arg.as_str() { + // short-circuit once we find a help flag HELP_LONG_FLAG | HELP_SHORT_FLAG => return Some(path), RELEASE_FLAG | BUILD_FLAG => {} PACKAGE_SHORT_FLAG | PACKAGE_LONG_FLAG => match iter.peek() { @@ -39,11 +40,14 @@ fn resolve_help_request(args: &[String]) -> Option> { None } -fn forwarded_args(clap_captured: &[OsString], after_dash_delimiter: &[String]) -> Vec { - clap_captured +fn forwarded_args( + clap_captured_args: &[OsString], + args_after_double_dash: &[String], +) -> Vec { + clap_captured_args .iter() .cloned() - .chain(after_dash_delimiter.iter().map(OsString::from)) + .chain(args_after_double_dash.iter().map(OsString::from)) .collect() } @@ -57,7 +61,7 @@ fn split_on_double_dash(raw: &[String]) -> (&[String], &[String]) { fn main() -> anyhow::Result<()> { let raw: Vec = std::env::args().collect(); - let (cot_args, forwarded_tail_args) = split_on_double_dash(&raw); + let (cot_args, forwarded_remaining_args) = split_on_double_dash(&raw); let release = cot_args.iter().any(|a| a == RELEASE_FLAG); let build = cot_args.iter().any(|b| b == BUILD_FLAG); @@ -105,7 +109,7 @@ fn main() -> anyhow::Result<()> { "migration".to_string(), args[0].to_string_lossy().into_owned(), ]; - let remaining = forwarded_args(&args[1..], forwarded_tail_args); + let remaining = forwarded_args(&args[1..], forwarded_remaining_args); handlers::handle_external(&path, &remaining, project, release) } }, @@ -117,7 +121,7 @@ fn main() -> anyhow::Result<()> { build, )?; let path = vec![args[0].to_string_lossy().into_owned()]; - let remaining = forwarded_args(&args[1..], forwarded_tail_args); + let remaining = forwarded_args(&args[1..], forwarded_remaining_args); handlers::handle_external(&path, &remaining, project, release) } } diff --git a/cot-cli/src/project.rs b/cot-cli/src/project.rs index c511e4f1d..2513bef06 100644 --- a/cot-cli/src/project.rs +++ b/cot-cli/src/project.rs @@ -528,7 +528,7 @@ edition = "2024" fn load_returns_none_without_cargo_manifest() { let temp_dir = TempDir::new().unwrap(); - let result = load(temp_dir.path(), false, None, false).unwrap(); + let result = load(temp_dir.path(), false, None, true).unwrap(); assert!(result.is_none()); } @@ -537,7 +537,7 @@ edition = "2024" fn load_errors_when_start_path_does_not_exist() { let temp_dir = TempDir::new().unwrap(); - let result = load(&temp_dir.path().join("missing"), false, None, false); + let result = load(&temp_dir.path().join("missing"), false, None, true); assert!(result.is_err()); assert!( @@ -566,7 +566,7 @@ edition = "2024" let binary_path = temp_dir.path().join("target/debug/demo"); write_metadata_script(&binary_path, &metadata("demo", &["serve"])); - let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, true).unwrap().unwrap(); assert_eq!(project.path, binary_path); assert!(project.metadata.is_some()); @@ -586,7 +586,7 @@ edition = "2024" let binary_path = temp_dir.path().join("target/release/demo"); write_metadata_script(&binary_path, &metadata("demo", &["serve"])); - let project = load(temp_dir.path(), true, None, false).unwrap().unwrap(); + let project = load(temp_dir.path(), true, None, true).unwrap().unwrap(); assert_eq!(project.path, binary_path); } @@ -606,7 +606,7 @@ path = "src/server.rs" let binary_path = temp_dir.path().join("target/debug/server"); write_metadata_script(&binary_path, &metadata("server", &["serve"])); - let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, true).unwrap().unwrap(); assert_eq!(project.path, binary_path); assert!(project.metadata.is_some()); @@ -635,7 +635,7 @@ path = "src/worker.rs" let binary_path = temp_dir.path().join("target/debug/api"); write_metadata_script(&binary_path, &metadata("api", &["serve"])); - let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, true).unwrap().unwrap(); assert_eq!(project.path, binary_path); assert!(project.metadata.is_some()); @@ -658,7 +658,7 @@ path = "src/worker.rs" "#, ); - let result = load(temp_dir.path(), false, None, false); + let result = load(temp_dir.path(), false, None, true); assert!(result.is_err()); let message = result.unwrap_err().to_string(); @@ -677,7 +677,7 @@ path = "src/worker.rs" "echo stdout message\necho stderr message >&2\nexit 42\n", ); - let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, true).unwrap().unwrap(); assert!(project.metadata.is_none()); } @@ -690,7 +690,7 @@ path = "src/worker.rs" let binary_path = temp_dir.path().join("target/debug/demo"); write_shell_script(&binary_path, "echo 'not json'\n"); - let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, true).unwrap().unwrap(); assert!(project.metadata.is_none()); } @@ -775,7 +775,7 @@ path = "src/worker.rs" write_package_manifest(&temp_dir.path().join("api"), "api", ""); write_package_manifest(&temp_dir.path().join("web"), "web", ""); - let result = load(temp_dir.path(), false, None, false); + let result = load(temp_dir.path(), false, None, true); assert!(result.is_err()); let message = result.unwrap_err().to_string(); @@ -791,7 +791,7 @@ path = "src/worker.rs" write_package_manifest(&temp_dir.path().join("api"), "api", ""); write_package_manifest(&temp_dir.path().join("web"), "web", ""); - let result = load(temp_dir.path(), false, Some("missing"), false); + let result = load(temp_dir.path(), false, Some("missing"), true); assert!(result.is_err()); let message = result.unwrap_err().to_string(); @@ -810,7 +810,7 @@ path = "src/worker.rs" let binary_path = temp_dir.path().join("target/debug/api"); write_metadata_script(&binary_path, &metadata("api", &["check"])); - let project = load(temp_dir.path(), false, Some("api"), false) + let project = load(temp_dir.path(), false, Some("api"), true) .unwrap() .unwrap(); @@ -828,7 +828,7 @@ path = "src/worker.rs" let binary_path = temp_dir.path().join("target/debug/web"); write_metadata_script(&binary_path, &metadata("web", &["check"])); - let project = load(&temp_dir.path().join("web"), false, None, false) + let project = load(&temp_dir.path().join("web"), false, None, true) .unwrap() .unwrap(); @@ -853,7 +853,7 @@ path = "src/worker.rs" }; write_cache(&command_cache_path(temp_dir.path()), &cache).unwrap(); - let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, true).unwrap().unwrap(); assert!(project.metadata.is_some()); assert_eq!(project.metadata.unwrap().commands[0].name, "cached"); @@ -872,7 +872,7 @@ path = "src/worker.rs" }; write_cache(&command_cache_path(temp_dir.path()), &cache).unwrap(); - let project = load(temp_dir.path(), false, None, false).unwrap().unwrap(); + let project = load(temp_dir.path(), false, None, true).unwrap().unwrap(); assert!(project.metadata.is_some()); assert_eq!(project.metadata.unwrap().commands[0].name, "fresh"); diff --git a/cot-cli/src/test_harness.rs b/cot-cli/src/test_harness.rs new file mode 100644 index 000000000..9ce14bfc7 --- /dev/null +++ b/cot-cli/src/test_harness.rs @@ -0,0 +1,887 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::OnceLock; + +use anyhow::{Context, Result, bail}; +use tempfile::TempDir; + +pub const FROBNICATE_TASK_SOURCE: &str = r#" +struct Frobnicate; + +#[async_trait(?Send)] +impl CliTask for Frobnicate { + fn subcommand(&self) -> Command { + Command::new("frobnicate") + .about("Frobnicates the target") + .arg(Arg::new("target").required(true).help("What to frobnicate")) + .arg(Arg::new("intensity").long("intensity").help("How hard to frobnicate")) + .arg( + Arg::new("build") + .long("build") + .action(ArgAction::SetTrue) + .help("Simulated flag colliding with cot-cli's own --build"), + ) + } + + async fn execute( + &mut self, + matches: &ArgMatches, + _bootstrapper: Bootstrapper, + ) -> cot::Result<()> { + let target = matches.get_one::("target").expect("required"); + println!("frobnicating {target}"); + if matches.get_flag("build") { + println!("(received forwarded --build flag)"); + } + Ok(()) + } +} +"#; + +pub const FROBNICATE_REGISTER: &str = "cli.add_task(Frobnicate);"; + +pub const GROUPED_TASK_SOURCE: &str = r#" +struct SubA; + +#[async_trait(?Send)] +impl CliTask for SubA { + fn subcommand(&self) -> Command { + Command::new("sub-a").about("Fixture sub-task A") + } + + async fn execute( + &mut self, + _matches: &ArgMatches, + _bootstrapper: Bootstrapper, + ) -> cot::Result<()> { + println!("ran sub-a"); + Ok(()) + } +} +"#; + +pub const GROUPED_REGISTER: &str = r#" + let mut group = cot::cli::CliTaskGroup::new("fixture-group").about("Fixture task group"); + group.add_task(SubA); + cli.add_task(group); +"#; + +fn workspace() -> &'static Path { + static ROOT: OnceLock = OnceLock::new(); + ROOT.get_or_init(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("cot-cli should be in a workspace") + .to_path_buf() + }) +} + +fn cot_crate_path() -> PathBuf { + workspace().join("cot") +} + +fn workspace_target_dir() -> PathBuf { + workspace().join("target") +} + +fn default_main_rs( + project_name: &str, + extra_code: &str, + register_calls: &[String], + apps: &[CotApp], +) -> String { + let struct_name = to_pascal_case(project_name); + let register_tasks_body = register_calls.join("\n\t\t"); + let app_definitions = apps + .iter() + .map(CotApp::render) + .collect::>() + .join("\n"); + + let register_apps_body = apps + .iter() + .map(CotApp::render_registration) + .collect::>() + .join("\n"); + + format!( + r"mod migrations; + +use cot::Project; +use cot::Bootstrapper; +use cot::db::{{Auto, Model, model}}; +use cot::cli::{{Cli, CliMetadata, CliTask}}; +use cot::cli::clap::{{Arg, ArgAction, ArgMatches, Command}}; +use cot::config::ProjectConfig; +use cot::project::{{AppBuilder, RegisterAppsContext, WithConfig}}; +use async_trait::async_trait; + +#[model] +#[derive(Debug, Clone)] +struct DefaultTestModel {{ + #[model(primary_key)] + id: Auto, + title: String, +}} + +{app_definitions} + +{extra_code} + +struct {struct_name}Project; + +impl Project for {struct_name}Project {{ + fn cli_metadata(&self) -> CliMetadata {{ + cot::cli::metadata!() + }} + + fn config(&self, _config_name: &str) -> cot::Result {{ + Ok(ProjectConfig::dev_default()) + }} + + fn register_tasks(&self, cli: &mut Cli) {{ + {register_tasks_body} + }} + + fn register_apps( + &self, + apps: &mut AppBuilder, + _context: &RegisterAppsContext, + ) {{ +{register_apps_body} + }} +}} + +#[cot::main] +fn main() -> impl Project {{ + {struct_name}Project +}} +" + ) +} + +fn default_migrations_rs() -> String { + r"pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[];".to_string() +} + +fn render_cargo_toml(project_name: &str, features: &[String], extra: &str) -> String { + let features_str = if features.is_empty() { + r#"["db", "json", "sqlite"]"#.to_owned() + } else { + format!( + "[{}]", + features + .iter() + .map(|f| format!(r#""{f}""#)) + .collect::>() + .join(", ") + ) + }; + format!( + r#"[package] +name = "{project_name}" +version = "0.1.0" +edition = "2024" + +[dependencies] +cot = {{ path = "{cot_path}", features = {features_str} }} +async-trait = "0.1" +{extra} +"#, + cot_path = cot_crate_path().display(), + ) +} + +fn unique_project_name() -> String { + use std::sync::atomic::{AtomicU32, Ordering}; + static COUNTER: AtomicU32 = AtomicU32::new(0); + let count = COUNTER.fetch_add(1, Ordering::Relaxed); + // Use process ID + counter so parallel test processes don't collide. + format!("cot-test-{}-{count}", std::process::id()) +} + +/// Builder for a generated Cot application. +/// +/// The builder mirrors the methods available on Cot's [`cot::App`] trait. +#[derive(Debug, Clone)] +pub struct CotAppBuilder { + name: String, + init: Option, + router: Option, + migrations: Option, + admin_model_managers: Option, + static_files: Option, +} + +impl CotAppBuilder { + /// Creates a new App builder. + #[must_use] + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + init: None, + router: None, + migrations: None, + admin_model_managers: None, + static_files: None, + } + } + + /// Sets the implementation of `App::init`. + #[must_use] + pub fn init(mut self, body: impl Into) -> Self { + self.init = Some(body.into()); + self + } + + /// Sets the implementation of `App::router`. + #[must_use] + pub fn router(mut self, code_block: impl Into) -> Self { + self.router = Some(code_block.into()); + self + } + + /// Sets the implementation of `App::migrations`. + #[must_use] + pub fn migrations(mut self, code_block: impl Into) -> Self { + self.migrations = Some(code_block.into()); + self + } + + /// Sets the implementation of `App::admin_model_managers`. + #[must_use] + pub fn admin_model_managers(mut self, code_block: impl Into) -> Self { + self.admin_model_managers = Some(code_block.into()); + self + } + + /// Sets the implementation of `App::static_files`. + #[must_use] + pub fn static_files(mut self, code_block: impl Into) -> Self { + self.static_files = Some(code_block.into()); + self + } + + /// Builds the application definition. + /// + /// The returned `CotApp` is what gets registered with a project builder. + #[must_use] + pub fn build(self) -> CotApp { + assert!(!self.name.trim().is_empty(), "Cot app name cannot be empty"); + + CotApp { + name: self.name, + init: self.init, + router: self.router, + migrations: self.migrations, + admin_model_managers: self.admin_model_managers, + static_files: self.static_files, + } + } +} + +/// A fully-built generated Cot App +#[derive(Debug, Clone)] +pub struct CotApp { + name: String, + init: Option, + router: Option, + migrations: Option, + admin_model_managers: Option, + static_files: Option, +} + +impl CotApp { + /// Returns the app's name. + #[must_use] + pub fn name(&self) -> &str { + &self.name + } + + /// Render this app as Rust source implementing `cot::App`. + #[must_use] + pub fn render(&self) -> String { + let struct_name = format!("{}App", to_pascal_case(&self.name)); + + let init = self.render_init(); + let router = self.render_router(); + let migrations = self.render_migrations(); + let admin_model_managers = self.render_admin_model_managers(); + let static_files = self.render_static_files(); + + format!( + r" +struct {struct_name}; + +#[async_trait] +impl cot::App for {struct_name} {{ + fn name(&self) -> &str {{ + {name:?} + }} + +{init} + +{router} + +{migrations} + +{admin_model_managers} + +{static_files} +}} +", + name = self.name, + ) + } + + fn render_init(&self) -> String { + match &self.init { + Some(body) => format!( + r" async fn init( + &self, + _context: &mut cot::project::ProjectContext, + ) -> cot::Result<()> {{ + {body} + }}" + ), + + None => r" async fn init( + &self, + _context: &mut cot::project::ProjectContext, + ) -> cot::Result<()> { + Ok(()) + }" + .to_owned(), + } + } + + fn render_router(&self) -> String { + match &self.router { + Some(code_block) => { + format!( + r" fn router(&self) -> cot::router::Router {{ + {code_block} + }}" + ) + } + + None => r" fn router(&self) -> cot::router::Router { + cot::router::Router::empty() + }" + .to_owned(), + } + } + + fn render_migrations(&self) -> String { + match &self.migrations { + Some(code_block) => { + format!( + r#" #[cfg(feature = "db")] + fn migrations(&self) -> Vec> {{ + {code_block} + }}"# + ) + } + + None => r#" #[cfg(feature = "db")] + fn migrations(&self) -> Vec> { + vec![] + }"# + .to_owned(), + } + } + + fn render_admin_model_managers(&self) -> String { + match &self.admin_model_managers { + Some(code_block) => { + format!( + r" fn admin_model_managers(&self) -> Vec> {{ + {code_block} + }}" + ) + } + + None => r" fn admin_model_managers(&self) -> Vec> { + vec![] + }" + .to_owned(), + } + } + + fn render_static_files(&self) -> String { + match &self.static_files { + Some(code_block) => { + format!( + r" fn static_files(&self) -> Vec {{ + {code_block} + }}" + ) + } + + None => r" fn static_files(&self) -> Vec { + vec![] + }" + .to_owned(), + } + } + + /// Returns the code string used to register this app with the generated + /// project. + #[must_use] + pub fn render_registration(&self) -> String { + let struct_name = format!("{}App", to_pascal_case(&self.name)); + + format!("\t\tapps.register({struct_name});") + } +} + +#[derive(Debug)] +pub struct CotProjectHarness { + project_name: String, + cot_binary: PathBuf, + features: Vec, + main_rs: Option, + migrations_rs: Option, + extra_files: Vec<(PathBuf, String)>, + extra_cargo_toml: String, + extra_code: String, + register_calls: Vec, + apps: Vec, +} + +impl CotProjectHarness { + #[must_use] + pub fn new(cot_binary: PathBuf) -> Self { + Self { + project_name: unique_project_name(), + features: Vec::new(), + main_rs: None, + migrations_rs: None, + extra_files: Vec::new(), + extra_cargo_toml: String::new(), + extra_code: String::new(), + register_calls: Vec::new(), + apps: Vec::new(), + cot_binary, + } + } + + #[must_use] + pub fn project_name(mut self, name: impl Into) -> Self { + self.project_name = name.into(); + self + } + + #[must_use] + pub fn features(mut self, features: impl IntoIterator>) -> Self { + self.features = features.into_iter().map(Into::into).collect(); + self + } + + #[must_use] + pub fn main_rs(mut self, content: impl Into) -> Self { + self.main_rs = Some(content.into()); + self + } + + #[must_use] + pub fn migrations_rs(mut self, content: impl Into) -> Self { + self.migrations_rs = Some(content.into()); + self + } + + /// Append raw TOML to the generated `Cargo.toml`. + #[must_use] + pub fn cargo_toml_extra(mut self, toml: impl Into) -> Self { + self.extra_cargo_toml = toml.into(); + self + } + + /// Insert raw Rust code at the top level of the generated `main.rs`, + /// above the `Project` impl. + #[must_use] + pub fn extra_code(mut self, code: impl Into) -> Self { + self.extra_code.push_str(&code.into()); + self.extra_code.push('\n'); + self + } + + /// Add a code block`Project::register_tasks` body. + #[must_use] + pub fn register_task(mut self, code_block: impl Into) -> Self { + self.register_calls.push(code_block.into()); + self + } + + /// Register an already-built app with this project. + #[must_use] + pub fn app(mut self, app: CotApp) -> Self { + self.apps.push(app); + self + } + + /// Register multiple already-built apps with this project. + #[must_use] + pub fn apps(mut self, apps: impl IntoIterator) -> Self { + self.apps.extend(apps); + self + } + + /// Add a file to the project, relative to the project root. + #[must_use] + pub fn with_file( + mut self, + relative_path: impl Into, + content: impl Into, + ) -> Self { + self.extra_files + .push((relative_path.into(), content.into())); + self + } + + /// Write all project files to a temporary directory. + /// + /// Returns a [`CotTestProject`] that can be used to run commands which + /// don't require a compiled binary (e.g. `cot migration list`), or can + /// be compiled via [`CotTestProject::compile`]. + pub fn build(self) -> Result { + let tempdir = TempDir::with_prefix("cot-test-harness-") + .context("failed to create temporary directory for test project")?; + + let project_dir = tempdir.path().join(&self.project_name); + std::fs::create_dir_all(project_dir.join("src")) + .context("failed to create project src/ directory")?; + + std::fs::write( + project_dir.join("Cargo.toml"), + render_cargo_toml(&self.project_name, &self.features, &self.extra_cargo_toml), + ) + .context("failed to write Cargo.toml")?; + + let main_rs = self.main_rs.clone().unwrap_or_else(|| { + default_main_rs( + &self.project_name, + &self.extra_code, + &self.register_calls, + &self.apps, + ) + }); + std::fs::write(project_dir.join("src").join("main.rs"), main_rs) + .context("failed to write src/main.rs")?; + + let migrations_rs = self + .migrations_rs + .clone() + .unwrap_or_else(default_migrations_rs); + std::fs::write(project_dir.join("src").join("migrations.rs"), migrations_rs) + .context("failed to write src/migrations.rs")?; + + for (rel, content) in &self.extra_files { + let abs = project_dir.join(rel); + if let Some(parent) = abs.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("failed to create directory for {}", rel.display()))?; + } + std::fs::write(&abs, content) + .with_context(|| format!("failed to write {}", rel.display()))?; + } + + Ok(CotTestProject { + _tempdir: tempdir, + project_dir, + project_name: self.project_name, + cot_binary: self.cot_binary, + }) + } +} + +/// A temporary Cot project with all files written to disk, but no binary built. +/// +/// Suitable for testing CLI commands that operate on source code +/// +/// Call [`CotTestProject::compile`] to build the binary and unlock proxy +/// command testing. +#[derive(Debug)] +pub struct CotTestProject { + _tempdir: TempDir, + project_dir: PathBuf, + project_name: String, + cot_binary: PathBuf, +} + +impl CotTestProject { + /// The absolute path to the project root directory. + #[must_use] + pub fn path(&self) -> &Path { + &self.project_dir + } + + /// The project name (also the Cargo package name and binary name). + #[must_use] + pub fn name(&self) -> &str { + &self.project_name + } + + /// Build a `cot` CLI command configured to run in this project's directory. + /// + /// Uses the test binary (respects `COT_CLI_TEST_CMD`) and does not require + /// a compiled project binary. + #[must_use] + pub fn cot_cmd(&self, args: &[&str]) -> Command { + let mut cmd = Command::new(&self.cot_binary); + cmd.current_dir(&self.project_dir); + cmd.args(args); + cmd + } + + /// Build a raw `cargo` command configured to run in this project's + /// directory. + /// + /// The `CARGO_TARGET_DIR` is set to the workspace target so dependencies + /// are shared across all test project builds. + #[must_use] + pub fn cargo_cmd(&self, subcommand: &str, args: &[&str]) -> Command { + let mut cmd = cargo_bin_command(); + cmd.current_dir(&self.project_dir) + .env("CARGO_TARGET_DIR", workspace_target_dir()) + .arg(subcommand) + .args(args); + cmd + } + + /// Compile the project binary in debug mode. + pub fn compile(self) -> Result { + self.compile_inner(false) + } + + /// Compile the project binary in release mode. + pub fn compile_release(self) -> Result { + self.compile_inner(true) + } + + fn compile_inner(self, release: bool) -> Result { + let mut extra_args = vec![]; + if release { + extra_args.push("--release"); + } + + let status = self + .cargo_cmd("build", &extra_args) + .status() + .context("failed to spawn `cargo build`")?; + + if !status.success() { + bail!( + "`cargo build` failed for project `{}` at `{}`", + self.project_name, + self.project_dir.display() + ); + } + + let profile = if release { "release" } else { "debug" }; + let binary_name = platform_binary_name(&self.project_name); + + // The binary was compiled into the workspace target dir. + let workspace_binary = workspace_target_dir().join(profile).join(&binary_name); + + if !workspace_binary.exists() { + bail!( + "expected compiled binary at `{}` but it was not found", + workspace_binary.display() + ); + } + + // Bridge the binary into the project's own target tree so that + // `cot-cli`'s `resolve_target_dir` (which walks up from CWD) can find + // it. On Unix we symlink (zero-cost); on Windows we copy. + let project_target_dir = self.project_dir.join("target").join(profile); + std::fs::create_dir_all(&project_target_dir) + .context("failed to create project target directory")?; + + let project_binary = project_target_dir.join(&binary_name); + link_or_copy(&workspace_binary, &project_binary) + .context("failed to link binary into project target dir")?; + + Ok(CompiledCotProject { + inner: self, + binary_path: project_binary, + release, + }) + } +} + +/// A temporary Cot project with a compiled binary. +#[derive(Debug)] +pub struct CompiledCotProject { + inner: CotTestProject, + binary_path: PathBuf, + release: bool, +} + +impl CompiledCotProject { + /// The absolute path to the project root directory. + #[must_use] + pub fn path(&self) -> &Path { + self.inner.path() + } + + /// The project name. + #[must_use] + pub fn name(&self) -> &str { + self.inner.name() + } + + /// The absolute path to the compiled binary. + #[must_use] + pub fn binary_path(&self) -> &Path { + &self.binary_path + } + + /// Whether this is a release build. + #[must_use] + pub fn is_release(&self) -> bool { + self.release + } + + /// Build a `cot` CLI proxy command configured to run in this project's + /// directory. + /// + /// Automatically appends `--release` if the project was compiled in release + /// mode so `cot-cli` resolves the correct binary. + #[must_use] + pub fn cot_cmd(&self, args: &[&str]) -> Command { + let mut cmd = self.inner.cot_cmd(args); + if self.release { + cmd.arg("--release"); + } + cmd + } + + /// Build a `cot` CLI command *without* any automatic flags. + /// + /// Use this when you want to control `--release` manually or test + /// the error path where the wrong profile binary is specified. + #[must_use] + pub fn cot_cmd_raw(&self, args: &[&str]) -> Command { + self.inner.cot_cmd(args) + } + + /// Run the project binary directly, bypassing the `cot` CLI proxy. + /// + /// Useful for verifying that the binary itself behaves correctly, + /// independent of proxy machinery. + #[must_use] + pub fn binary_cmd(&self, args: &[&str]) -> Command { + let mut cmd = Command::new(&self.binary_path); + cmd.current_dir(self.path()).args(args); + cmd + } + + /// Build a `cargo` command in the project directory. + #[must_use] + pub fn cargo_cmd(&self, subcommand: &str, args: &[&str]) -> Command { + self.inner.cargo_cmd(subcommand, args) + } +} + +/// A lazily-compiled standard project shared across all tests in a process. +/// +/// Compiling the same project for every test function would be prohibitively +/// slow. For tests that don't need a custom project structure, use this +/// instead. +/// +/// # Usage +/// +/// ```no_run +/// # use cot_cli::test_harness::standard_project; +/// let project = standard_project().unwrap(); +/// let output = project.cot_cmd(&["check"]).output().unwrap(); +/// ``` +pub fn standard_project(cot_binary: PathBuf) -> Result<&'static CompiledCotProject> { + static PROJECT: OnceLock = OnceLock::new(); + static ERROR: OnceLock = OnceLock::new(); + + if let Some(err) = ERROR.get() { + bail!("standard project failed to compile: {err}"); + } + + if let Some(proj) = PROJECT.get() { + return Ok(proj); + } + + let extra_code = format!("{FROBNICATE_TASK_SOURCE}\n{GROUPED_TASK_SOURCE}"); + + let standard_app = CotAppBuilder::new("cot_test_standard") + .migrations("cot::db::migrations::wrap_migrations(migrations::MIGRATIONS)") + .build(); + + match CotProjectHarness::new(cot_binary) + .project_name("cot_test_standard") + .app(standard_app) + .extra_code(extra_code) + .register_task(FROBNICATE_REGISTER) + .register_task(GROUPED_REGISTER) + .build() + .and_then(CotTestProject::compile) + { + Ok(proj) => { + let _ = PROJECT.set(proj); + Ok(PROJECT.get().unwrap()) + } + + Err(e) => { + let msg = format!("{e:#}"); + let _ = ERROR.set(msg.clone()); + bail!("standard project failed to compile: {msg}"); + } + } +} + +fn cargo_bin_command() -> Command { + let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + let mut cmd = Command::new(cargo); + // Strip RUSTFLAGS that may have been set by the outer cargo invocation + // (e.g. instrument-coverage flags), they may conflict with the inner build. + cmd.env_remove("RUSTFLAGS").env("CARGO_INCREMENTAL", "0"); + cmd +} + +fn platform_binary_name(name: &str) -> String { + if cfg!(windows) { + format!("{name}.exe") + } else { + name.to_string() + } +} + +fn link_or_copy(src: &Path, dst: &Path) -> Result<()> { + // Remove stale link/copy from a previous test run. + if dst.exists() || dst.symlink_metadata().is_ok() { + std::fs::remove_file(dst).context("failed to remove stale binary")?; + } + + #[cfg(unix)] + { + std::os::unix::fs::symlink(src, dst) + .with_context(|| format!("failed to symlink {} → {}", src.display(), dst.display())) + } + + #[cfg(not(unix))] + { + std::fs::copy(src, dst) + .with_context(|| format!("failed to copy {} → {}", src.display(), dst.display())) + .map(|_| ()) + } +} + +fn to_pascal_case(s: &str) -> String { + s.split(['-', '_']) + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + } + }) + .collect() +} diff --git a/cot-cli/tests/snapshot_testing/external/check.rs b/cot-cli/tests/snapshot_testing/external/check.rs new file mode 100644 index 000000000..e4186733a --- /dev/null +++ b/cot-cli/tests/snapshot_testing/external/check.rs @@ -0,0 +1,40 @@ +use cot_cli::test_harness::standard_project; +use insta_cmd::assert_cmd_snapshot; + +use crate::snapshot_testing::{GENERIC_FILTERS, TEMP_PATH_FILTERS, cot_cli_path, cot_cmd_in}; + +#[test] +fn check_forwards_to_project_binary() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["check"])) } + ); +} + +#[test] +fn double_dash_delimiter_fails_with_unsupported_flag_name() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["check", "--", "--build"])) } + ); +} + +#[test] +fn unrecognized_command_reports_unknown_command() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["banana"])) } + ); +} + +#[test] +fn check_with_no_project_binary_reports_build_hint() { + let tempdir = tempfile::TempDir::new().unwrap(); + insta::with_settings!( + { filters => [GENERIC_FILTERS, TEMP_PATH_FILTERS].concat() }, + { assert_cmd_snapshot!(cot_cmd_in(&["check"], tempdir.path())) } + ); +} diff --git a/cot-cli/tests/snapshot_testing/external/mod.rs b/cot-cli/tests/snapshot_testing/external/mod.rs new file mode 100644 index 000000000..be0c6a3ea --- /dev/null +++ b/cot-cli/tests/snapshot_testing/external/mod.rs @@ -0,0 +1 @@ +mod check; diff --git a/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__check_forwards_to_project_binary.snap b/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__check_forwards_to_project_binary.snap new file mode 100644 index 000000000..36b3422f7 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__check_forwards_to_project_binary.snap @@ -0,0 +1,13 @@ +--- +source: cot-cli/tests/snapshot_testing/external/check.rs +info: + program: cot + args: + - check +--- +success: true +exit_code: 0 +----- stdout ----- +Success verifying the configuration + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__check_with_no_project_binary_reports_build_hint.snap b/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__check_with_no_project_binary_reports_build_hint.snap new file mode 100644 index 000000000..bc4b2ae4e --- /dev/null +++ b/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__check_with_no_project_binary_reports_build_hint.snap @@ -0,0 +1,14 @@ +--- +source: cot-cli/tests/snapshot_testing/external/check.rs +info: + program: cot + args: + - check +--- +success: false +exit_code: 1 +----- stdout ----- + +----- stderr ----- +Error: unknown command `check` and no project binary was found in the `target` dir. +Hint: run `cargo build` first, or pass `cot --build check` to build it automatically. diff --git a/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__double_dash_delimiter_fails_with_unsupported_flag_name.snap b/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__double_dash_delimiter_fails_with_unsupported_flag_name.snap new file mode 100644 index 000000000..1de74f97e --- /dev/null +++ b/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__double_dash_delimiter_fails_with_unsupported_flag_name.snap @@ -0,0 +1,19 @@ +--- +source: cot-cli/tests/snapshot_testing/external/check.rs +info: + program: cot + args: + - check + - "--" + - "--build" +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: unexpected argument '--build' found + +Usage: cot_test_standard check + +For more information, try '--help'. diff --git a/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__unrecognized_command_reports_unknown_command.snap b/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__unrecognized_command_reports_unknown_command.snap new file mode 100644 index 000000000..b3b8803a5 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/external/snapshots/cli__snapshot_testing__external__check__unrecognized_command_reports_unknown_command.snap @@ -0,0 +1,13 @@ +--- +source: cot-cli/tests/snapshot_testing/external/check.rs +info: + program: cot + args: + - banana +--- +success: false +exit_code: 1 +----- stdout ----- + +----- stderr ----- +Error: unknown command `banana`. Run `cot --help` to see available commands. diff --git a/cot-cli/tests/snapshot_testing/help/external.rs b/cot-cli/tests/snapshot_testing/help/external.rs new file mode 100644 index 000000000..edd777578 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/external.rs @@ -0,0 +1,76 @@ +use cot_cli::test_harness::standard_project; +use insta_cmd::assert_cmd_snapshot; + +use crate::snapshot_testing::{GENERIC_FILTERS, cot_cli_path}; + +#[test] +fn top_level_help_merges_real_project_commands() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["--help"])) } + ); +} + +#[test] +fn migration_help_merges_real_rollback_subcommand() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["migration", "--help"])) } + ); +} + +#[test] +fn migration_rollback_help_succeeds_proving_command_is_reachable() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["migration", "rollback", "--help"])) } + ); +} + +#[test] +fn migration_unknown_subcommand_fails_cleanly() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["migration", "unknown", "--help"])) } + ); +} + +#[test] +fn check_help_shows_real_task() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["check", "--help"])) } + ); +} + +#[test] +fn custom_registered_task_appears_in_top_level_help() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["--help"])) } + ); +} + +#[test] +fn custom_task_help_shows_reconstructed_args() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["frobnicate", "--help"])) } + ); +} + +#[test] +fn nested_custom_group_help_merges_correctly() { + let project = standard_project(cot_cli_path()).unwrap(); + insta::with_settings!( + { filters => GENERIC_FILTERS.to_owned() }, + { assert_cmd_snapshot!(project.cot_cmd(&["fixture-group", "--help"])) } + ); +} diff --git a/cot-cli/tests/snapshot_testing/help/mod.rs b/cot-cli/tests/snapshot_testing/help/mod.rs index 1a4d05202..cd0b00640 100644 --- a/cot-cli/tests/snapshot_testing/help/mod.rs +++ b/cot-cli/tests/snapshot_testing/help/mod.rs @@ -1,3 +1,5 @@ +mod external; + use super::*; #[test] diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__check_help_shows_real_task.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__check_help_shows_real_task.snap new file mode 100644 index 000000000..10c40dc24 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__check_help_shows_real_task.snap @@ -0,0 +1,20 @@ +--- +source: cot-cli/tests/snapshot_testing/help/mod.rs +info: + program: cot + args: + - check + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Verifies the configuration, including connections to the database and other services + +Usage: cot check + +Options: + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__custom_registered_task_appears_in_top_level_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__custom_registered_task_appears_in_top_level_help.snap new file mode 100644 index 000000000..ead6201b2 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__custom_registered_task_appears_in_top_level_help.snap @@ -0,0 +1,37 @@ +--- +source: cot-cli/tests/snapshot_testing/help/mod.rs +info: + program: cot + args: + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Command-line interface for the Cot web framework + +Usage: cot [OPTIONS] + +Commands: + new Create a new Cot project + migration Manage migrations for a Cot project + cli Manage Cot CLI + check Verifies the configuration, including connections to the database and other + services + collect-static Collects all static files into a static directory + frobnicate Frobnicates the target + fixture-group Fixture task group + help Print this message or the help of the given subcommand(s) + +Options: + --release Use target/release instead of target/debug when looking for the project + binary + --build Build the binary if it does not exist + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help + -V, --version Print version + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__custom_task_help_shows_reconstructed_args.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__custom_task_help_shows_reconstructed_args.snap new file mode 100644 index 000000000..4b98aed1c --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__custom_task_help_shows_reconstructed_args.snap @@ -0,0 +1,17 @@ +--- +source: cot-cli/tests/snapshot_testing/help/mod.rs +info: + program: cot + args: + - frobnicate + - "--help" +--- +success: false +exit_code: 101 +----- stdout ----- + +----- stderr ----- + +thread 'main' (6047256) panicked at /Users/eli/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_builder-4.6.5/src/builder/debug_asserts.rs:746:9: +Argument 'target' is positional and it must take a value but action is SetTrue +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__check_help_shows_real_task.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__check_help_shows_real_task.snap new file mode 100644 index 000000000..cf24cbf35 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__check_help_shows_real_task.snap @@ -0,0 +1,20 @@ +--- +source: cot-cli/tests/snapshot_testing/help/external.rs +info: + program: cot + args: + - check + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Verifies the configuration, including connections to the database and other services + +Usage: cot check + +Options: + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__custom_registered_task_appears_in_top_level_help.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__custom_registered_task_appears_in_top_level_help.snap new file mode 100644 index 000000000..bb4d92252 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__custom_registered_task_appears_in_top_level_help.snap @@ -0,0 +1,37 @@ +--- +source: cot-cli/tests/snapshot_testing/help/external.rs +info: + program: cot + args: + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Command-line interface for the Cot web framework + +Usage: cot [OPTIONS] + +Commands: + new Create a new Cot project + migration Manage migrations for a Cot project + cli Manage Cot CLI + check Verifies the configuration, including connections to the database and other + services + collect-static Collects all static files into a static directory + frobnicate Frobnicates the target + fixture-group Fixture task group + help Print this message or the help of the given subcommand(s) + +Options: + --release Use target/release instead of target/debug when looking for the project + binary + --build Build the binary if it does not exist + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help + -V, --version Print version + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__custom_task_help_shows_reconstructed_args.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__custom_task_help_shows_reconstructed_args.snap new file mode 100644 index 000000000..31c85fbc0 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__custom_task_help_shows_reconstructed_args.snap @@ -0,0 +1,17 @@ +--- +source: cot-cli/tests/snapshot_testing/help/external.rs +info: + program: cot + args: + - frobnicate + - "--help" +--- +success: false +exit_code: 101 +----- stdout ----- + +----- stderr ----- + +thread 'main' (6739824) panicked at /Users/eli/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_builder-4.6.5/src/builder/debug_asserts.rs:746:9: +Argument 'target' is positional and it must take a value but action is SetTrue +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_help_merges_real_rollback_subcommand.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_help_merges_real_rollback_subcommand.snap new file mode 100644 index 000000000..96fdec830 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_help_merges_real_rollback_subcommand.snap @@ -0,0 +1,27 @@ +--- +source: cot-cli/tests/snapshot_testing/help/external.rs +info: + program: cot + args: + - migration + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Manage migrations for a Cot project + +Usage: cot migration + +Commands: + list List all migrations for a Cot project + make Generate migrations for a Cot project + new Create a new empty migration + rollback Rollback migrations up to the specified migration file + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_rollback_help_succeeds_proving_command_is_reachable.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_rollback_help_succeeds_proving_command_is_reachable.snap new file mode 100644 index 000000000..e063df720 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_rollback_help_succeeds_proving_command_is_reachable.snap @@ -0,0 +1,26 @@ +--- +source: cot-cli/tests/snapshot_testing/help/external.rs +info: + program: cot + args: + - migration + - rollback + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Rollback migrations up to the specified migration file + +Usage: cot migration rollback [OPTIONS] + +Arguments: + The migration name to roll back to (e.g. m_0001_initial, 0001, or zero) + +Options: + --app The name of the app to rollback migrations for + --dry-run Print the Rollback Plan without changing the database + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_unknown_subcommand_fails_cleanly.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_unknown_subcommand_fails_cleanly.snap new file mode 100644 index 000000000..c3abd2057 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__migration_unknown_subcommand_fails_cleanly.snap @@ -0,0 +1,28 @@ +--- +source: cot-cli/tests/snapshot_testing/help/external.rs +info: + program: cot + args: + - migration + - unknown + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Manage migrations for a Cot project + +Usage: cot migration + +Commands: + list List all migrations for a Cot project + make Generate migrations for a Cot project + new Create a new empty migration + rollback Rollback migrations up to the specified migration file + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__nested_custom_group_help_merges_correctly.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__nested_custom_group_help_merges_correctly.snap new file mode 100644 index 000000000..7c951463f --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__nested_custom_group_help_merges_correctly.snap @@ -0,0 +1,24 @@ +--- +source: cot-cli/tests/snapshot_testing/help/external.rs +info: + program: cot + args: + - fixture-group + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Fixture task group + +Usage: cot fixture-group [COMMAND] + +Commands: + sub-a Fixture sub-task A + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__top_level_help_merges_real_project_commands.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__top_level_help_merges_real_project_commands.snap new file mode 100644 index 000000000..bb4d92252 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__external__top_level_help_merges_real_project_commands.snap @@ -0,0 +1,37 @@ +--- +source: cot-cli/tests/snapshot_testing/help/external.rs +info: + program: cot + args: + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Command-line interface for the Cot web framework + +Usage: cot [OPTIONS] + +Commands: + new Create a new Cot project + migration Manage migrations for a Cot project + cli Manage Cot CLI + check Verifies the configuration, including connections to the database and other + services + collect-static Collects all static files into a static directory + frobnicate Frobnicates the target + fixture-group Fixture task group + help Print this message or the help of the given subcommand(s) + +Options: + --release Use target/release instead of target/debug when looking for the project + binary + --build Build the binary if it does not exist + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help + -V, --version Print version + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_help_merges_real_rollback_subcommand.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_help_merges_real_rollback_subcommand.snap new file mode 100644 index 000000000..832bdf66e --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_help_merges_real_rollback_subcommand.snap @@ -0,0 +1,27 @@ +--- +source: cot-cli/tests/snapshot_testing/help/mod.rs +info: + program: cot + args: + - migration + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Manage migrations for a Cot project + +Usage: cot migration + +Commands: + list List all migrations for a Cot project + make Generate migrations for a Cot project + new Create a new empty migration + rollback Rollback migrations up to the specified migration file + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_rollback_help_succeeds_proving_command_is_reachable.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_rollback_help_succeeds_proving_command_is_reachable.snap new file mode 100644 index 000000000..58065d28e --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_rollback_help_succeeds_proving_command_is_reachable.snap @@ -0,0 +1,26 @@ +--- +source: cot-cli/tests/snapshot_testing/help/mod.rs +info: + program: cot + args: + - migration + - rollback + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Rollback migrations up to the specified migration file + +Usage: cot migration rollback [OPTIONS] + +Arguments: + The migration name to roll back to (e.g. m_0001_initial, 0001, or zero) + +Options: + --app The name of the app to rollback migrations for + --dry-run Print the Rollback Plan without changing the database + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_unknown_subcommand_fails_cleanly.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_unknown_subcommand_fails_cleanly.snap new file mode 100644 index 000000000..9484586d4 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__migration_unknown_subcommand_fails_cleanly.snap @@ -0,0 +1,28 @@ +--- +source: cot-cli/tests/snapshot_testing/help/mod.rs +info: + program: cot + args: + - migration + - unknown + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Manage migrations for a Cot project + +Usage: cot migration + +Commands: + list List all migrations for a Cot project + make Generate migrations for a Cot project + new Create a new empty migration + rollback Rollback migrations up to the specified migration file + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__nested_custom_group_help_merges_correctly.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__nested_custom_group_help_merges_correctly.snap new file mode 100644 index 000000000..a7d08c7c2 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__nested_custom_group_help_merges_correctly.snap @@ -0,0 +1,24 @@ +--- +source: cot-cli/tests/snapshot_testing/help/mod.rs +info: + program: cot + args: + - fixture-group + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Fixture task group + +Usage: cot fixture-group [COMMAND] + +Commands: + sub-a Fixture sub-task A + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__top_level_help_merges_real_project_commands.snap b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__top_level_help_merges_real_project_commands.snap new file mode 100644 index 000000000..ead6201b2 --- /dev/null +++ b/cot-cli/tests/snapshot_testing/help/snapshots/cli__snapshot_testing__help__top_level_help_merges_real_project_commands.snap @@ -0,0 +1,37 @@ +--- +source: cot-cli/tests/snapshot_testing/help/mod.rs +info: + program: cot + args: + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Command-line interface for the Cot web framework + +Usage: cot [OPTIONS] + +Commands: + new Create a new Cot project + migration Manage migrations for a Cot project + cli Manage Cot CLI + check Verifies the configuration, including connections to the database and other + services + collect-static Collects all static files into a static directory + frobnicate Frobnicates the target + fixture-group Fixture task group + help Print this message or the help of the given subcommand(s) + +Options: + --release Use target/release instead of target/debug when looking for the project + binary + --build Build the binary if it does not exist + -p, --package Package to use, in case you're running this in a workspace + -v, --verbose... Increase logging verbosity + -q, --quiet... Decrease logging verbosity + -h, --help Print help + -V, --version Print version + + +----- stderr ----- diff --git a/cot-cli/tests/snapshot_testing/mod.rs b/cot-cli/tests/snapshot_testing/mod.rs index 86dcee554..640791349 100644 --- a/cot-cli/tests/snapshot_testing/mod.rs +++ b/cot-cli/tests/snapshot_testing/mod.rs @@ -1,3 +1,4 @@ +use std::path::{Path, PathBuf}; use std::process::Command; pub(crate) use insta_cmd::assert_cmd_snapshot; @@ -5,6 +6,7 @@ pub(crate) use insta_cmd::assert_cmd_snapshot; pub(crate) use crate::cot_cli; mod cli; +mod external; mod help; mod migration; mod new; @@ -46,6 +48,14 @@ macro_rules! cot_cli { } } +pub(crate) fn cot_cli_path() -> PathBuf { + if let Ok(path) = std::env::var("COT_CLI_TEST_CMD") { + PathBuf::from(path) + } else { + assert_cmd::cargo::cargo_bin!("cot").to_path_buf() + } +} + /// Get the command for the Cot CLI binary under test. /// /// By default, this is the binary defined in this crate. @@ -59,11 +69,16 @@ macro_rules! cot_cli { /// /// COT_CLI_TEST_CMD="$PWD"/custom-cot-cli cargo test --test cli pub(crate) fn cot_cli_cmd() -> Command { - if let Ok(np) = std::env::var("COT_CLI_TEST_CMD") { - Command::new(np) - } else { - Command::new(assert_cmd::cargo::cargo_bin!("cot")) - } + Command::new(cot_cli_path()) +} + +/// Convenience: build a `cot` command in an arbitrary directory. +/// +/// Useful for testing behaviour outside any Cot project. +pub(crate) fn cot_cmd_in(args: &[&str], dir: &Path) -> Command { + let mut cmd = cot_cli_cmd(); + cmd.current_dir(dir).args(args); + cmd } const GENERIC_FILTERS: &[(&str, &str)] = &[ From ca2542564f349e8d27c1c6283a590aab974de8d7 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 12 Aug 2026 19:52:19 +0000 Subject: [PATCH 19/19] rename project harness for consistency --- cot-cli/src/test_harness.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cot-cli/src/test_harness.rs b/cot-cli/src/test_harness.rs index 9ce14bfc7..9a94a97bb 100644 --- a/cot-cli/src/test_harness.rs +++ b/cot-cli/src/test_harness.rs @@ -436,7 +436,7 @@ impl cot::App for {struct_name} {{ } #[derive(Debug)] -pub struct CotProjectHarness { +pub struct CotProjectBuilder { project_name: String, cot_binary: PathBuf, features: Vec, @@ -449,7 +449,7 @@ pub struct CotProjectHarness { apps: Vec, } -impl CotProjectHarness { +impl CotProjectBuilder { #[must_use] pub fn new(cot_binary: PathBuf) -> Self { Self { @@ -541,10 +541,10 @@ impl CotProjectHarness { /// Write all project files to a temporary directory. /// - /// Returns a [`CotTestProject`] that can be used to run commands which + /// Returns a [`CotProject`] that can be used to run commands which /// don't require a compiled binary (e.g. `cot migration list`), or can - /// be compiled via [`CotTestProject::compile`]. - pub fn build(self) -> Result { + /// be compiled via [`CotProject::compile`]. + pub fn build(self) -> Result { let tempdir = TempDir::with_prefix("cot-test-harness-") .context("failed to create temporary directory for test project")?; @@ -586,7 +586,7 @@ impl CotProjectHarness { .with_context(|| format!("failed to write {}", rel.display()))?; } - Ok(CotTestProject { + Ok(CotProject { _tempdir: tempdir, project_dir, project_name: self.project_name, @@ -599,17 +599,17 @@ impl CotProjectHarness { /// /// Suitable for testing CLI commands that operate on source code /// -/// Call [`CotTestProject::compile`] to build the binary and unlock proxy +/// Call [`CotProject::compile`] to build the binary and unlock proxy /// command testing. #[derive(Debug)] -pub struct CotTestProject { +pub struct CotProject { _tempdir: TempDir, project_dir: PathBuf, project_name: String, cot_binary: PathBuf, } -impl CotTestProject { +impl CotProject { /// The absolute path to the project root directory. #[must_use] pub fn path(&self) -> &Path { @@ -713,7 +713,7 @@ impl CotTestProject { /// A temporary Cot project with a compiled binary. #[derive(Debug)] pub struct CompiledCotProject { - inner: CotTestProject, + inner: CotProject, binary_path: PathBuf, release: bool, } @@ -784,15 +784,15 @@ impl CompiledCotProject { } } -/// A lazily-compiled standard project shared across all tests in a process. +/// A lazily-compiled standard project cot project. /// /// Compiling the same project for every test function would be prohibitively /// slow. For tests that don't need a custom project structure, use this /// instead. /// -/// # Usage +/// # Examples /// -/// ```no_run +/// ``` /// # use cot_cli::test_harness::standard_project; /// let project = standard_project().unwrap(); /// let output = project.cot_cmd(&["check"]).output().unwrap(); @@ -815,14 +815,14 @@ pub fn standard_project(cot_binary: PathBuf) -> Result<&'static CompiledCotProje .migrations("cot::db::migrations::wrap_migrations(migrations::MIGRATIONS)") .build(); - match CotProjectHarness::new(cot_binary) + match CotProjectBuilder::new(cot_binary) .project_name("cot_test_standard") .app(standard_app) .extra_code(extra_code) .register_task(FROBNICATE_REGISTER) .register_task(GROUPED_REGISTER) .build() - .and_then(CotTestProject::compile) + .and_then(CotProject::compile) { Ok(proj) => { let _ = PROJECT.set(proj);