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:
2026-08-24 11:15:13 -06:00
parent 4324d551d6
commit 80b082423c
8 changed files with 239 additions and 44 deletions
+50 -9
View File
@@ -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();