feat: add --help guides to the .install and .uninstall REPL commands
.install --help and .uninstall --help print a usage guide covering the owner/repo shorthand, --git-host, --filter, --force, ref pinning, and the bundle lifecycle; both usage error lines now point at --help. Tab completion offers --help for both commands and --git-host on the first .install argument, and the unified install parser accepts flags in any argument position so completed flags work wherever they are inserted. The empty .list bundles message now shows the REPL install form alongside the CLI one.
This commit is contained in:
@@ -626,7 +626,10 @@ pub fn list_installed_bundles() -> Result<()> {
|
||||
let store = BundleStore::load()?;
|
||||
let rows = bundle_list_rows(&store, &paths::config_dir());
|
||||
if rows.is_empty() {
|
||||
println!("No bundles installed. Install one with `coyote --install <git-url>`.");
|
||||
println!(
|
||||
"No bundles installed. Install one with `coyote --install <git-url|owner/repo>` \
|
||||
or, from the REPL, `.install <git-url|owner/repo>` (see `.install --help`)."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -71,10 +71,21 @@ pub fn install_remote(git_url: &str, filter: Option<InstallFilter>, force: bool)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ReplInstallArgs {
|
||||
value: Option<String>,
|
||||
filter: Option<InstallFilter>,
|
||||
force: bool,
|
||||
git_host: Option<String>,
|
||||
}
|
||||
|
||||
/// Flags may appear before or after the positional value, matching how the
|
||||
/// completer offers them at any argument position.
|
||||
fn parse_repl_install_flags(
|
||||
command: &str,
|
||||
mut iter: impl Iterator<Item = String>,
|
||||
) -> Result<(Option<InstallFilter>, bool, Option<String>)> {
|
||||
) -> Result<ReplInstallArgs> {
|
||||
let mut value: Option<String> = None;
|
||||
let mut filter: Option<InstallFilter> = None;
|
||||
let mut force = false;
|
||||
let mut git_host: Option<String> = None;
|
||||
@@ -103,11 +114,24 @@ fn parse_repl_install_flags(
|
||||
s if s.starts_with("--git-host=") => {
|
||||
git_host = Some(s["--git-host=".len()..].to_string());
|
||||
}
|
||||
other => bail!("Unexpected argument to '{command}': {other}"),
|
||||
other if other.starts_with('-') => {
|
||||
bail!("Unexpected argument to '{command}': {other}")
|
||||
}
|
||||
other => {
|
||||
if value.is_some() {
|
||||
bail!("Unexpected argument to '{command}': {other}");
|
||||
}
|
||||
value = Some(other.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((filter, force, git_host))
|
||||
Ok(ReplInstallArgs {
|
||||
value,
|
||||
filter,
|
||||
force,
|
||||
git_host,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -159,7 +183,7 @@ fn looks_like_remote_source(value: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const DEFAULT_GIT_HOST: &str = "github.com";
|
||||
pub const DEFAULT_GIT_HOST: &str = "github.com";
|
||||
|
||||
fn is_repo_shorthand(value: &str) -> bool {
|
||||
let path = strip_ref_suffix(value);
|
||||
@@ -266,17 +290,20 @@ pub fn install_or_update_from_repl_args(args: &str) -> Result<()> {
|
||||
let tokens = shell_words::split(args)
|
||||
.with_context(|| format!("failed to parse '.install' args: {args}"))?;
|
||||
|
||||
let mut iter = tokens.into_iter();
|
||||
let value = iter.next().with_context(|| {
|
||||
let parsed = parse_repl_install_flags(".install", tokens.into_iter())?;
|
||||
let value = parsed.value.with_context(|| {
|
||||
format!(
|
||||
"Usage: .install <git-url|owner/repo|installed-bundle> \
|
||||
[--git-host <host>] [--filter <{}>] [--force]",
|
||||
InstallFilter::NAMES.join("|")
|
||||
)
|
||||
})?;
|
||||
|
||||
let (filter, force, git_host) = parse_repl_install_flags(".install", iter)?;
|
||||
install_or_update(&value, git_host.as_deref(), filter, force)
|
||||
install_or_update(
|
||||
&value,
|
||||
parsed.git_host.as_deref(),
|
||||
parsed.filter,
|
||||
parsed.force,
|
||||
)
|
||||
}
|
||||
|
||||
/// The whole remote is always processed, including categories a filtered
|
||||
@@ -4194,21 +4221,50 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn repl_install_flags_parse_git_host() {
|
||||
let (filter, force, host) = parse_repl_install_flags(
|
||||
let parsed = parse_repl_install_flags(
|
||||
".install",
|
||||
vec!["--git-host".to_string(), "git.x.com".to_string()].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(host.as_deref(), Some("git.x.com"));
|
||||
assert!(filter.is_none() && !force);
|
||||
assert_eq!(parsed.git_host.as_deref(), Some("git.x.com"));
|
||||
assert!(parsed.filter.is_none() && !parsed.force);
|
||||
|
||||
let (_, force, host) = parse_repl_install_flags(
|
||||
let parsed = parse_repl_install_flags(
|
||||
".install",
|
||||
vec!["--git-host=git.y.com".to_string(), "--force".to_string()].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(host.as_deref(), Some("git.y.com"));
|
||||
assert!(force);
|
||||
assert_eq!(parsed.git_host.as_deref(), Some("git.y.com"));
|
||||
assert!(parsed.force);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repl_install_flags_accept_any_argument_order() {
|
||||
let parsed = parse_repl_install_flags(
|
||||
".install",
|
||||
vec![
|
||||
"--git-host".to_string(),
|
||||
"git.x.com".to_string(),
|
||||
"owner/repo".to_string(),
|
||||
"--force".to_string(),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parsed.value.as_deref(), Some("owner/repo"));
|
||||
assert_eq!(parsed.git_host.as_deref(), Some("git.x.com"));
|
||||
assert!(parsed.filter.is_none() && parsed.force);
|
||||
|
||||
let err = parse_repl_install_flags(
|
||||
".install",
|
||||
vec!["one".to_string(), "two".to_string()].into_iter(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("Unexpected argument"));
|
||||
|
||||
let err = parse_repl_install_flags(".install", vec!["--bogus".to_string()].into_iter())
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("Unexpected argument"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@ pub use self::app_state::AppState;
|
||||
pub use self::bundles::list_installed_bundles;
|
||||
pub use self::input::Input;
|
||||
pub use self::install_remote::{
|
||||
install_or_update, install_or_update_from_repl_args, uninstall_bundle, update_bundle,
|
||||
DEFAULT_GIT_HOST, install_or_update, install_or_update_from_repl_args, uninstall_bundle,
|
||||
update_bundle,
|
||||
};
|
||||
pub use self::macro_policy::{
|
||||
MacroAllowlistLevel, MacroPolicy, MacroSource, MacroState, RESERVED_MACRO_NAMES, ResolvedMacro,
|
||||
|
||||
@@ -3278,10 +3278,19 @@ impl RequestContext {
|
||||
.map(|(name, desc)| (name, if desc.is_empty() { None } else { Some(desc) }))
|
||||
.collect(),
|
||||
".install" => {
|
||||
let mut values: Vec<String> =
|
||||
let mut names: Vec<String> =
|
||||
AssetCategory::NAMES.iter().map(|s| s.to_string()).collect();
|
||||
values.extend(installed_bundle_names());
|
||||
super::map_completion_values(values)
|
||||
names.extend(installed_bundle_names());
|
||||
let mut values = super::map_completion_values(names);
|
||||
values.push((
|
||||
"--git-host".to_string(),
|
||||
Some("Host the owner/repo shorthand expands against".to_string()),
|
||||
));
|
||||
values.push((
|
||||
"--help".to_string(),
|
||||
Some("Show usage for .install".to_string()),
|
||||
));
|
||||
values
|
||||
}
|
||||
".uninstall" => {
|
||||
let mut values = super::map_completion_values(installed_bundle_names());
|
||||
@@ -3289,6 +3298,10 @@ impl RequestContext {
|
||||
"--yes".to_string(),
|
||||
Some("Skip the uninstall confirmation".to_string()),
|
||||
));
|
||||
values.push((
|
||||
"--help".to_string(),
|
||||
Some("Show usage for .uninstall".to_string()),
|
||||
));
|
||||
|
||||
values
|
||||
}
|
||||
@@ -3525,6 +3538,9 @@ impl RequestContext {
|
||||
if !has_git_host {
|
||||
available.push("--git-host");
|
||||
}
|
||||
if !args.contains(&"--help") {
|
||||
available.push("--help");
|
||||
}
|
||||
|
||||
values = super::map_completion_values(available);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user