diff --git a/Cargo.lock b/Cargo.lock index 0eeb9ca45..93f735db2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -997,6 +997,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..559c330b1 100644 --- a/cot-cli/src/args.rs +++ b/cot-cli/src/args.rs @@ -1,8 +1,15 @@ +use std::ffi::OsString; 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", @@ -11,6 +18,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 +41,9 @@ pub enum Commands { /// Manage Cot CLI #[command(subcommand)] Cli(CliCommands), + + #[command(external_subcommand)] + External(Vec), } #[derive(Debug, Args)] @@ -119,3 +134,71 @@ 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. +#[must_use] +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(&format!("{PACKAGE_LONG_FLAG}=")) { + return Some(value.to_string()); + } + if arg == PACKAGE_LONG_FLAG || arg == PACKAGE_SHORT_FLAG { + return iter.next().cloned(); + } + } + 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 23b34fb90..5e9fc2005 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,92 @@ pub fn handle_cli_completions(CompletionsArgs { shell }: CompletionsArgs) -> any Ok(()) } +pub fn handle_external( + args: &[OsString], + 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`." + ); + }; + + 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: &[OsString]) -> 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 = 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 { + for meta_cmd in &proj.metadata.commands { + cmd = cmd.subcommand(build_clap_subcommand(meta_cmd)); + } + } + + cmd +} + +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); } @@ -180,4 +270,98 @@ mod tests { assert!(!output.is_empty()); } + + #[test] + fn external_command_without_project_reports_build_hint() { + let result = handle_external(&[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(&[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/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..73aa1006a 100644 --- a/cot-cli/src/main.rs +++ b/cot-cli/src/main.rs @@ -1,11 +1,50 @@ #![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, 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 => { + let Some(value) = rest.next() else { + return false; + }; + if value.starts_with('-') { + return false; + } + } + _ => return false, + } + } + true +} + 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())?; + return Ok(()); + } + let cli = Cli::parse(); tracing_subscriber::fmt() @@ -27,5 +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) => { + 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 new file mode 100644 index 000000000..f14235c4e --- /dev/null +++ b/cot-cli/src/project.rs @@ -0,0 +1,648 @@ +use std::fmt::Write; +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}; + +const RELEASE_PROFILE: &str = "release"; +const DEBUG_PROFILE: &str = "debug"; + +#[derive(Serialize, Deserialize)] +struct Cache { + binary_mtime_secs: u64, + metadata: ProjectMetadata, +} + +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 { + pub path: PathBuf, + pub metadata: ProjectMetadata, +} + +/// Find and load the project binary and its metadata. +/// +/// `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( + 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_PROFILE + } else { + DEBUG_PROFILE + }; + + #[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); + } + + // 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 = 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() + ))?; + + Ok(Some(ProjectBinary { + path: binary_path, + metadata, + })) +} + +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>, +) -> 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\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. 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 + && 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 + .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) + && 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) + .arg(METADATA_FLAG) + .output() + .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.", + binary_path.display(), + output.status, + ); + + if !stderr.trim().is_empty() { + let _ = write!(msg, "\n\nstderr:\n{}", stderr.trim()); + } + + if !stdout.trim().is_empty() { + let _ = write!(msg, "\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}.\n\nGot:\n{}", + binary_path.display(), + raw.trim(), + ) + })?; + + 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(()) +} + +#[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!(command_cache_path(temp_dir.path()).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").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(&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, "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(&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 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/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/src/utils.rs b/cot-cli/src/utils.rs index 6ec4cbcf8..e0a42abd1 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,6 @@ impl PackageManager { path.to_owned() } - #[cfg(test)] pub(crate) fn get_manifest(&self) -> &Manifest { &self.manifest } 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..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 @@ -19,9 +19,22 @@ 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..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 @@ -19,9 +19,22 @@ 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 97652a2e1..ca1836916 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(crate) 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..c4d2498ac --- /dev/null +++ b/cot/src/metadata.rs @@ -0,0 +1,104 @@ +//! 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(), + 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(), + } +} + +#[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); + } +} 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())?;