fix: address code review findings on the bundle lifecycle
The user-origin marker on replaced mcp.json entries is now sticky: re-records and cross-bundle transfers only upgrade replaced to transferred when the prior record proves bundle origin, so updating a bundle can no longer make uninstall delete a key the user had before the bundle replaced it. Canonical source URLs lowercase only the host, since self-hosted forges treat repository paths as case-sensitive and collapsing distinct repos misdirects updates and uninstalls. git clone invocations pass '--' before the URL so a crafted source cannot be parsed as a git flag. Lifecycle flags (--install, --install-builtins, --update-bundle, --uninstall) and their companions now conflict explicitly instead of first-match dispatch silently dropping actions. --install-from returns as a hidden tombstone that errors with the replacement instead of feeding the flag to the LLM as prompt text. --list-bundles dispatches before config load so a pure read no longer boots MCP servers. write_file_atomic fsyncs before the rename so a crash cannot persist a truncated store. REPL: .uninstall accepts --yes, .install rejects trailing tokens after a category, and .install remote gets a migration hint. Plus polish: host validation rejects '#' and '?', renamed_to no longer serializes null, derived names get a debug assert against the validator, completions share DEFAULT_GIT_HOST, README mentions skills.
This commit is contained in:
+50
-9
@@ -875,6 +875,9 @@ pub async fn run_repl_command(
|
||||
ReplInstallDispatch::Unified(value) => {
|
||||
config::install_or_update_from_repl_args(value)?;
|
||||
}
|
||||
ReplInstallDispatch::RemovedRemote => println!(
|
||||
"'.install remote <git-url>' was removed; use '.install <git-url>' directly."
|
||||
),
|
||||
ReplInstallDispatch::Usage => println!(
|
||||
"Usage: .install <{}> | .install <git-url|owner/repo|installed-bundle> \
|
||||
[--git-host <host>] [--filter <cat>] [--force]",
|
||||
@@ -1197,12 +1200,20 @@ pub async fn run_repl_command(
|
||||
println!("Usage: .delete <role|session|rag|macro|skill|agent-data>")
|
||||
}
|
||||
},
|
||||
".uninstall" => match args {
|
||||
Some(args) => {
|
||||
config::uninstall_bundle(args.trim(), false)?;
|
||||
".uninstall" => {
|
||||
let mut assume_yes = false;
|
||||
let mut names = Vec::new();
|
||||
for token in args.unwrap_or("").split_whitespace() {
|
||||
match token {
|
||||
"--yes" | "-y" => assume_yes = true,
|
||||
other => names.push(other),
|
||||
}
|
||||
}
|
||||
_ => println!("Usage: .uninstall <bundle-name>"),
|
||||
},
|
||||
match names.as_slice() {
|
||||
[name] => config::uninstall_bundle(name, assume_yes)?,
|
||||
_ => println!("Usage: .uninstall <bundle-name> [--yes]"),
|
||||
}
|
||||
}
|
||||
".list" => match args {
|
||||
Some(args) => {
|
||||
ctx.list_assets(args.trim())?;
|
||||
@@ -1575,6 +1586,7 @@ fn unknown_command() -> Result<()> {
|
||||
enum ReplInstallDispatch<'a> {
|
||||
Builtins(AssetCategory),
|
||||
Unified(&'a str),
|
||||
RemovedRemote,
|
||||
Usage,
|
||||
}
|
||||
|
||||
@@ -1582,10 +1594,15 @@ fn parse_repl_install(args: Option<&str>) -> ReplInstallDispatch<'_> {
|
||||
let trimmed = args.map(str::trim).unwrap_or("");
|
||||
let mut parts = trimmed.splitn(2, char::is_whitespace);
|
||||
match parts.next() {
|
||||
Some(name) if !name.is_empty() => match AssetCategory::parse(name) {
|
||||
Some(category) => ReplInstallDispatch::Builtins(category),
|
||||
None => ReplInstallDispatch::Unified(trimmed),
|
||||
},
|
||||
Some(name) if !name.is_empty() => {
|
||||
let rest = parts.next().map(str::trim).unwrap_or("");
|
||||
match AssetCategory::parse(name) {
|
||||
Some(category) if rest.is_empty() => ReplInstallDispatch::Builtins(category),
|
||||
Some(_) => ReplInstallDispatch::Usage,
|
||||
None if name == "remote" && !rest.is_empty() => ReplInstallDispatch::RemovedRemote,
|
||||
None => ReplInstallDispatch::Unified(trimmed),
|
||||
}
|
||||
}
|
||||
_ => ReplInstallDispatch::Usage,
|
||||
}
|
||||
}
|
||||
@@ -1841,6 +1858,30 @@ mod tests {
|
||||
assert_eq!(parse_repl_install(Some(" ")), ReplInstallDispatch::Usage);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_repl_install_rejects_extra_tokens_after_a_category() {
|
||||
assert_eq!(
|
||||
parse_repl_install(Some("agents --force")),
|
||||
ReplInstallDispatch::Usage
|
||||
);
|
||||
assert_eq!(
|
||||
parse_repl_install(Some("agents extra")),
|
||||
ReplInstallDispatch::Usage
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_repl_install_hints_on_removed_remote_form() {
|
||||
assert_eq!(
|
||||
parse_repl_install(Some("remote https://github.com/x/y")),
|
||||
ReplInstallDispatch::RemovedRemote
|
||||
);
|
||||
assert_eq!(
|
||||
parse_repl_install(Some("remote")),
|
||||
ReplInstallDispatch::Unified("remote")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_command_names_are_sorted_deduped_first_words_without_dots() {
|
||||
let names = builtin_command_names();
|
||||
|
||||
Reference in New Issue
Block a user