Synced from monorepo

Changes:
- Gate session-lifecycle heap steady state with a dhat soak
- Unbreak merge lifecycle e2e after default model → grok-4.5
- Scan home-scope rules dirs at <root>/rules
- Complete text-input paste and terminal parity
- Gate project roles and personas
- Use canonical editing in dialogs
- Use canonical editing in search bars
- Reject ambiguous MCP tool IDs
- Harden Git operands for plugins
- Simplify queue drain API
- Pass RFC 9207 iss through MCP OAuth token exchange
- Show leader roster when local agents map is empty
- Use canonical editing in Persona views
- Remove marketplace default-skills auto-install and purge old installs
- Use canonical editing in extension forms
- Add canonical dashboard text editing
- Use canonical editing in settings
- Add /summarize as a /recap alias
- Restore previous agent when exiting dashboard
- Use tool_choice auto for compaction
- Settings toggle for snap-prompt-to-top on send
- Update default models to grok-4.5
- Source login shell once for local bash (env + alias/function snapshot)
- Template hardcoded param names in server-native tool descriptions
- Fix System-Reminder XML tag injection in CLAUDE.md via agents_md
- Fix remote workspace-server hardcoding LSP trust (repo code execution risk)
- Clear orphaned tool-call updates at turn end
- Suppress task wake after cancel
- Send x-grok-client-identifier on direct API tool calls
- Harden dashboard peek lease transitions
- Host /btw side panel in live region (minimal mode)
- Bound scroll presentation latency
- Highlight multi-line constructs correctly in diffs and the file viewer
- Block web_fetch non-public IPs; local opt-in is explicit-host only
- Seed coding_data_retention_opt_out=false for OAuth e2es in pty-harness
- Follow up clipboard delivery feedback
- Use canonical editing in pickers
- Route TextArea through canonical editor
- Persistent "watching" status row; quieter turn markers
- Gate sensitive edit targets
- Expose agent registry counts and gate session churn on them
- Default coding data sharing to opt-out until server preference applies
- Wire chat attachment ids through gateway prompts
- On auth refresh failure, issue retry
- Forward preview provenance and computer lifecycle state
- Document independent privacy controls and scope /privacy output
- Strip SamplingError Display prefix on rate-limit UI copy
- Stop dumping Cloudflare HTML into Retry failed
- Disable in-place prompt edit (scroll jank on enter)
- Strip forced ANSI color from gh pr view JSON
- Plumb bash tool description onto ToolUsageCard wire
This commit is contained in:
grokkybara[bot] 2026-07-18 19:48:28 +01:00
commit 7cfcb20d2b
292 changed files with 23315 additions and 9209 deletions

View file

@ -59,6 +59,10 @@ pub fn sync_source_cache_with_mode(
cache_root: &Path,
mode: SyncMode,
) -> Result<SourceCacheLease, String> {
let url = xai_grok_agent::plugins::git_install::validate_git_url(url)?;
let branch = branch
.map(xai_grok_agent::plugins::git_install::validate_git_ref)
.transpose()?;
let hash = cache_hash(url);
let cache_dir = cache_root.join(&hash);
let start = Instant::now();
@ -89,6 +93,10 @@ fn sync_cache_locked(
cache_dir: &Path,
mode: SyncMode,
) -> Result<(), String> {
let url = xai_grok_agent::plugins::git_install::validate_git_url(url)?;
let branch = branch
.map(xai_grok_agent::plugins::git_install::validate_git_ref)
.transpose()?;
if cache_dir.join(".git").exists() {
if mode == SyncMode::UseTtl && is_cache_fresh(cache_dir) {
return Ok(());
@ -225,6 +233,10 @@ fn unique_reclone_suffix() -> u128 {
}
fn clone_with_git2(url: &str, branch: Option<&str>, dest: &Path) -> Result<(), String> {
let url = xai_grok_agent::plugins::git_install::validate_git_url(url)?;
let branch = branch
.map(xai_grok_agent::plugins::git_install::validate_git_ref)
.transpose()?;
let mut fetch_opts = git2::FetchOptions::new();
fetch_opts.depth(1);
@ -261,15 +273,22 @@ pub fn git_command() -> std::process::Command {
cmd
}
fn clone_with_cli(url: &str, branch: Option<&str>, dest: &Path) -> Result<(), String> {
fn clone_cli_command(url: &str, branch: Option<&str>, dest: &Path) -> std::process::Command {
let mut cmd = git_command();
cmd.args(["clone", "--depth", "1"]);
if let Some(b) = branch {
cmd.args(["--branch", b]);
}
cmd.arg(url).arg(dest.as_os_str());
cmd.arg("--").arg(url).arg(dest.as_os_str());
cmd
}
let output = cmd
fn clone_with_cli(url: &str, branch: Option<&str>, dest: &Path) -> Result<(), String> {
let url = xai_grok_agent::plugins::git_install::validate_git_url(url)?;
let branch = branch
.map(xai_grok_agent::plugins::git_install::validate_git_ref)
.transpose()?;
let output = clone_cli_command(url, branch, dest)
.output()
.map_err(|e| format!("failed to run git clone: {e}"))?;
if !output.status.success() {
@ -279,11 +298,24 @@ fn clone_with_cli(url: &str, branch: Option<&str>, dest: &Path) -> Result<(), St
Ok(())
}
fn fetch_cli_command(repo_dir: &Path, branch: Option<&str>) -> std::process::Command {
let mut cmd = git_command();
cmd.current_dir(repo_dir).args([
"fetch",
"--depth",
"1",
"--",
"origin",
branch.unwrap_or("HEAD"),
]);
cmd
}
fn fetch_reset_cached_repo(repo_dir: &Path, branch: Option<&str>) -> Result<(), String> {
let branch_arg = branch.unwrap_or("HEAD");
let fetch_output = git_command()
.current_dir(repo_dir)
.args(["fetch", "--depth", "1", "origin", branch_arg])
let branch = branch
.map(xai_grok_agent::plugins::git_install::validate_git_ref)
.transpose()?;
let fetch_output = fetch_cli_command(repo_dir, branch)
.output()
.map_err(|e| format!("failed to run git fetch: {e}"))?;
@ -343,6 +375,60 @@ mod tests {
assert!(root.to_string_lossy().contains("marketplace-cache"));
}
#[test]
fn cli_git_args_terminate_options_before_operands() {
let clone_cmd = clone_cli_command("repo", Some("main"), Path::new("dest"));
let clone_args: Vec<_> = clone_cmd
.get_args()
.map(|arg| arg.to_str().unwrap())
.collect();
assert_eq!(
clone_args,
[
"--no-optional-locks",
"clone",
"--depth",
"1",
"--branch",
"main",
"--",
"repo",
"dest",
]
);
let fetch_cmd = fetch_cli_command(Path::new("repo"), Some("main"));
let fetch_args: Vec<_> = fetch_cmd
.get_args()
.map(|arg| arg.to_str().unwrap())
.collect();
assert_eq!(
fetch_args,
[
"--no-optional-locks",
"fetch",
"--depth",
"1",
"--",
"origin",
"main",
]
);
}
#[test]
fn invalid_cache_operands_fail_before_cache_root_creation() {
for (url, branch) in [
("--upload-pack=cmd", Some("main")),
("https://example.com/repo.git", Some("--upload-pack=cmd")),
] {
let parent = tempfile::tempdir().unwrap();
let cache_root = parent.path().join("cache");
assert!(sync_source_cache(url, branch, &cache_root).is_err());
assert!(!cache_root.exists());
}
}
#[test]
fn sync_source_cache_uses_ttl_by_default() {
if !git_available() {

View file

@ -5,7 +5,6 @@
use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
use xai_grok_agent::plugins::git_install::{self, InstallSource};
use xai_grok_agent::plugins::install_registry::{
@ -56,7 +55,6 @@ pub fn install_from_marketplace(
.map_err(|e| InstallError::InstallFailed {
detail: format!("invalid marketplace plugin path: {e}"),
})?;
let plugin_relative_path = plugin_relative_path.as_str();
let source = InstallSource::Local {
path: plugin_dir,
subdir: None,
@ -67,17 +65,6 @@ pub fn install_from_marketplace(
match git_install::install_from_source(&source, registry, false) {
Ok(result) => {
let repo_key = result.repo_key.clone();
let installed_path = registry.install_dir().join(&repo_key);
// If the installed dir has no manifest but has SKILL.md files
// at the root level (e.g. default-skills/), write a synthetic
// plugin.json so the plugin discovery system finds the skills.
ensure_manifest_for_root_skills(
&installed_path,
plugin_relative_path,
&provenance.source_display_name,
);
let mut repo = git_install::build_installed_repo(&result, &source);
repo.marketplace = Some(provenance);
registry.insert(repo_key.clone(), repo);
@ -98,12 +85,6 @@ pub fn install_from_marketplace(
match git_install::install_from_source(&source, registry, false) {
Ok(result) => {
let repo_key = result.repo_key.clone();
let installed_path = registry.install_dir().join(&repo_key);
ensure_manifest_for_root_skills(
&installed_path,
plugin_relative_path,
&provenance.source_display_name,
);
let mut repo = git_install::build_installed_repo(&result, &source);
repo.marketplace = Some(provenance);
registry.insert(repo_key.clone(), repo);
@ -153,6 +134,7 @@ pub fn install_from_remote_url(
})
})
.transpose()?;
let (url, git_ref, git_sha) = git_install::clone_operands(url, git_ref, git_sha)?;
// No-fetch short-circuit before the pin gate: re-install of an already-present
// plugin must not refuse just because the catalog entry is unpinned.
if let Some((existing_key, _)) = find_installed_marketplace_plugin(
@ -166,8 +148,8 @@ pub fn install_from_remote_url(
}
let source = InstallSource::Git {
url: url.to_string(),
git_ref: git_ref.map(|s| s.to_string()),
git_sha: git_sha.map(|s| s.to_string()),
git_ref: git_ref.map(str::to_owned),
git_sha: git_sha.map(str::to_owned),
subdir,
};
@ -181,12 +163,6 @@ pub fn install_from_remote_url(
) {
Ok(result) => {
let repo_key = result.repo_key.clone();
let installed_path = registry.install_dir().join(&repo_key);
ensure_manifest_for_root_skills(
&installed_path,
plugin_name,
&provenance.source_display_name,
);
let mut repo = git_install::build_installed_repo(&result, &source);
repo.marketplace = Some(provenance);
registry.insert(repo_key.clone(), repo);
@ -209,12 +185,6 @@ pub fn install_from_remote_url(
) {
Ok(result) => {
let repo_key = result.repo_key.clone();
let installed_path = registry.install_dir().join(&repo_key);
ensure_manifest_for_root_skills(
&installed_path,
plugin_name,
&provenance.source_display_name,
);
let mut repo = git_install::build_installed_repo(&result, &source);
repo.marketplace = Some(provenance);
registry.insert(repo_key.clone(), repo);
@ -288,6 +258,21 @@ pub fn update_from_marketplace_entry_transactional(
name: provenance.plugin_subdir.clone(),
})?;
let remote_source = entry
.remote_url
.as_deref()
.map(|url| {
// Catalog pins published as `ref` still need hoisting for the verified clone path.
let (git_ref, git_sha) = git_install::hoist_pin_slots(
entry.remote_ref.as_deref(),
entry.remote_sha.as_deref(),
);
let source = git_install::clone_operands(url, git_ref, git_sha)?;
git_install::ensure_pinned(require_sha, source.2, &entry.name, source.0)?;
Ok::<_, InstallError>(source)
})
.transpose()?;
let install_dir = registry.install_dir().to_path_buf();
std::fs::create_dir_all(&install_dir).map_err(|e| InstallError::Io {
path: install_dir.clone(),
@ -302,11 +287,7 @@ pub fn update_from_marketplace_entry_transactional(
remove_path_if_exists(&staging_path)?;
remove_path_if_exists(&backup_path)?;
let stage_result = if let Some(url) = entry.remote_url.as_deref() {
// Catalog pins published as `ref` still need hoisting for the verified clone path.
let (git_ref, git_sha) =
git_install::hoist_pin_slots(entry.remote_ref.as_deref(), entry.remote_sha.as_deref());
git_install::ensure_pinned(require_sha, git_sha, &entry.name, url)?;
let stage_result = if let Some((url, git_ref, git_sha)) = remote_source {
clone_repo_to_path(url, git_ref, git_sha, &staging_path)
} else {
let source_path = plugin_relative_path
@ -330,11 +311,6 @@ pub fn update_from_marketplace_entry_transactional(
return Err(e);
}
ensure_manifest_for_root_skills(
&staging_path,
plugin_relative_path.as_str(),
&provenance.source_display_name,
);
let plugins = match discover_plugins_in_dir(&staging_path, remote_subdir.as_deref()) {
Ok(plugins) if !plugins.is_empty() => plugins,
Ok(_) => {
@ -355,16 +331,14 @@ pub fn update_from_marketplace_entry_transactional(
let new_version = first_plugin_version(&new_plugins);
let changed = old_version != new_version;
let updated_at = chrono::Utc::now().to_rfc3339();
let kind = if let Some(url) = entry.remote_url.as_ref() {
InstallKind::Git {
url: url.clone(),
git_ref: entry
.remote_sha
.clone()
.or_else(|| entry.remote_ref.clone()),
commit: read_head_commit(&staging_path).unwrap_or_default(),
subdir: remote_subdir.clone(),
}
let kind = if let Some((url, git_ref, git_sha)) = remote_source {
remote_install_kind(
url,
git_ref,
git_sha,
read_head_commit(&staging_path).unwrap_or_default(),
remote_subdir.clone(),
)
} else {
let source_path = plugin_relative_path
.join_under(marketplace_root)
@ -511,27 +485,39 @@ fn remove_path_if_exists(path: &Path) -> Result<(), InstallError> {
Ok(())
}
fn remote_install_kind(
url: &str,
git_ref: Option<&str>,
git_sha: Option<&str>,
commit: String,
subdir: Option<String>,
) -> InstallKind {
InstallKind::Git {
url: url.to_owned(),
git_ref: git_sha.or(git_ref).map(str::to_owned),
commit,
subdir,
}
}
fn clone_repo_to_path(
url: &str,
git_ref: Option<&str>,
git_sha: Option<&str>,
target: &Path,
) -> Result<(), InstallError> {
let (url, git_ref, git_sha) = git_install::clone_operands(url, git_ref, git_sha)?;
if let Some(sha) = git_sha {
return clone_repo_at_sha(url, sha, target);
}
let mut cmd = Command::new("git");
xai_tty_utils::detach_std_command(&mut cmd);
cmd.arg("clone")
.arg("--depth")
.arg("1")
.stdin(std::process::Stdio::null())
.envs(xai_tty_utils::pager_env());
// Same auth/LFS/SSH suppression as marketplace cache clones.
let mut cmd = xai_tty_utils::git_command();
cmd.arg("clone").arg("--depth").arg("1");
if let Some(r) = git_ref {
cmd.arg("--branch").arg(r);
}
cmd.arg(url).arg(target);
cmd.arg("--").arg(url).arg(target);
let output = cmd.output().map_err(|e| InstallError::InstallFailed {
detail: format!("failed to run git clone: {e}"),
})?;
@ -549,11 +535,10 @@ fn clone_repo_to_path(
}
fn clone_repo_at_sha(url: &str, sha: &str, target: &Path) -> Result<(), InstallError> {
if sha.is_empty() {
return Err(InstallError::InstallFailed {
detail: "empty SHA provided for pinned clone".into(),
});
}
let url = git_install::validate_git_url(url)
.map_err(|detail| InstallError::InstallFailed { detail })?;
let sha = git_install::validate_git_sha(sha)
.map_err(|detail| InstallError::InstallFailed { detail })?;
std::fs::create_dir_all(target).map_err(|e| InstallError::Io {
path: target.to_path_buf(),
source: e,
@ -563,8 +548,8 @@ fn clone_repo_at_sha(url: &str, sha: &str, target: &Path) -> Result<(), InstallE
InstallError::InstallFailed { detail }
};
run_git_in(target, &["init", "--quiet"]).map_err(wrap_fail)?;
run_git_in(target, &["remote", "add", "origin", url]).map_err(wrap_fail)?;
run_git_in(target, &["fetch", "--depth", "1", "origin", sha])
run_git_in(target, &git_install::remote_add_args(url)).map_err(wrap_fail)?;
run_git_in(target, &git_install::fetch_sha_args(sha))
.map_err(|d| wrap_fail(format!("fetch-by-sha failed: {d}")))?;
run_git_in(target, &["checkout", "--quiet", "FETCH_HEAD"]).map_err(wrap_fail)?;
let head = read_head_commit(target).ok_or_else(|| {
@ -588,12 +573,8 @@ fn run_git_in(cwd: &Path, args: &[&str]) -> Result<(), String> {
}
fn run_git_in_capture(cwd: &Path, args: &[&str]) -> Result<std::process::Output, String> {
let mut cmd = Command::new("git");
xai_tty_utils::detach_std_command(&mut cmd);
cmd.args(args)
.current_dir(cwd)
.stdin(std::process::Stdio::null())
.envs(xai_tty_utils::pager_env());
let mut cmd = xai_tty_utils::git_command();
cmd.args(args).current_dir(cwd);
let output = cmd
.output()
.map_err(|e| format!("failed to run git {}: {e}", args.first().unwrap_or(&"")))?;
@ -740,89 +721,47 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
Ok(())
}
/// Write a synthetic `plugin.json` for directories that have SKILL.md files
/// at the root level (not under a `skills/` subdirectory).
///
/// This handles `default-skills/` directories where each subdirectory IS a
/// skill, rather than a plugin with a `skills/` convention directory.
fn ensure_manifest_for_root_skills(
installed_path: &Path,
plugin_relative_path: &str,
source_display_name: &str,
) {
use xai_grok_agent::plugins::manifest::load_manifest;
// Skip if a manifest already exists.
if let Ok(xai_grok_agent::plugins::manifest::ManifestLoadResult::Found(_)) =
load_manifest(installed_path)
{
return;
}
// Skip if there's already a skills/ directory (convention will work).
if installed_path.join("skills").is_dir() {
return;
}
// Check if there are SKILL.md files at the root level.
let has_root_skills = std::fs::read_dir(installed_path)
.ok()
.map(|rd| {
rd.filter_map(|e| e.ok())
.any(|e| e.path().join("SKILL.md").exists())
})
.unwrap_or(false);
if !has_root_skills {
return;
}
// Build a unique name from the source display name + relative path.
// e.g. source="xAI Marketplace", path="default-skills"
// -> "xai-marketplace-default-skills"
let source_slug: String = source_display_name
.chars()
.map(|c| {
if c.is_alphanumeric() {
c.to_ascii_lowercase()
} else {
'-'
}
})
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-");
let path_slug = plugin_relative_path
.rsplit('/')
.next()
.unwrap_or(plugin_relative_path);
let name = format!("{source_slug}-{path_slug}");
// Write a minimal plugin.json with skills pointing to root.
let manifest = serde_json::json!({
"name": name,
"description": format!("Default skills from {source_display_name}"),
"skills": "./"
});
let manifest_path = installed_path.join("plugin.json");
if let Err(e) = std::fs::write(&manifest_path, manifest.to_string()) {
tracing::warn!(
path = %manifest_path.display(),
error = %e,
"failed to write synthetic plugin.json for root-level skills"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
use std::sync::{Mutex, OnceLock};
static TEST_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();
static TEST_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn transactional_sha_git_args_terminate_options_before_operands() {
assert_eq!(
git_install::remote_add_args("repo"),
["remote", "add", "--", "origin", "repo"]
);
assert_eq!(
git_install::fetch_sha_args("0123456789abcdef0123456789abcdef01234567"),
[
"fetch",
"--depth",
"1",
"--",
"origin",
"0123456789abcdef0123456789abcdef01234567",
]
);
}
#[test]
fn transactional_sha_clone_rejects_before_target_creation() {
for bad in ["deadbeef", "--upload-pack=cmd"] {
let root = tempfile::tempdir().unwrap();
let target = root.path().join("staging");
assert!(matches!(
clone_repo_to_path("file:///unused", None, Some(bad), &target),
Err(InstallError::InstallFailed { .. })
));
assert!(!target.exists());
}
}
#[test]
fn require_sha_rejects_unpinned_remote_install() {
with_test_registry(|registry| {
@ -861,10 +800,50 @@ mod tests {
true,
)
.unwrap_err();
assert!(
matches!(err, InstallError::UnpinnedRemoteRefused { .. }),
"a non-hex 'pin' must be refused up front, got: {err}"
);
match err {
InstallError::InstallFailed { detail } => assert!(
detail.contains("40 or 64 hexadecimal"),
"expected full-SHA validation detail, got: {detail}"
),
other => panic!("expected InstallFailed for malformed SHA, got: {other}"),
}
});
}
#[test]
fn already_installed_remote_still_rejects_malformed_operands() {
with_test_registry(|registry| {
let marketplace = tempfile::tempdir().unwrap();
write_plugin(marketplace.path(), "demo", "1.0.0", "old");
install_test_plugin(registry, marketplace.path(), "demo");
let provenance = provenance(marketplace.path(), "plugins/demo");
let registry_len = registry.list().len();
let installed_path = registry.list().into_iter().next().unwrap().1.path.clone();
for (url, git_ref, git_sha) in [
("--upload-pack=cmd", Some("main"), None),
(
"https://example.com/plugin.git",
Some("--upload-pack=cmd"),
None,
),
("https://example.com/plugin.git", None, Some("deadbeef")),
] {
let err = install_from_remote_url(
url,
git_ref,
git_sha,
None,
"plugins/demo",
provenance.clone(),
registry,
false,
)
.unwrap_err();
assert!(matches!(err, InstallError::InstallFailed { .. }));
assert_eq!(registry.list().len(), registry_len);
assert!(installed_path.exists());
}
});
}
@ -1078,6 +1057,39 @@ mod tests {
});
}
#[test]
fn transactional_git_kind_uses_normalized_operands() {
let sha = "a".repeat(40);
let padded_sha = format!(" {sha} ");
let (url, git_ref, git_sha) = git_install::clone_operands(
" https://example.com/plugin.git ",
Some(" v1.2.3 "),
Some(&padded_sha),
)
.unwrap();
let kind = remote_install_kind(url, git_ref, git_sha, sha.clone(), None);
let repo = InstalledRepo {
kind,
installed_at: String::new(),
updated_at: String::new(),
path: Path::new("/unused").to_path_buf(),
plugins: HashMap::new(),
marketplace: None,
};
match &repo.kind {
InstallKind::Git { url, git_ref, .. } => {
assert_eq!(url, "https://example.com/plugin.git");
assert_eq!(git_ref.as_deref(), Some(sha.as_str()));
}
InstallKind::Local { .. } => panic!("expected Git"),
}
assert!(matches!(
git_install::update_repo("repo", &repo, true),
Ok(git_install::UpdateStatus::Pinned { ref_name }) if ref_name == sha
));
}
#[test]
fn transactional_update_preserves_installed_at_and_updates_updated_at() {
with_test_registry(|registry| {

View file

@ -14,46 +14,10 @@ use crate::types::{MarketplaceEntry, MarketplaceScan};
/// Scan a marketplace directory for plugins, reporting whether a
/// `plugin-index.json` component catalog was loaded.
///
/// Tries indexed mode first, falls back to filesystem scanning.
/// Tries indexed mode first, falls back to filesystem scanning. The component
/// catalog is only consulted in indexed mode: its keys are defined as index
/// names, so the filesystem fallback ignores it.
pub fn scan_marketplace(root: &Path) -> MarketplaceScan {
let MarketplaceScan {
entries: mut plugins,
catalog_loaded,
} = scan_plugins(root);
// Also scan `default-skills/` as a virtual plugin if present.
let default_skills_dir = root.join("default-skills");
if default_skills_dir.is_dir() {
// default-skills/ has skills at root level (each subdir is a skill),
// not under a skills/ subdirectory. Count SKILL.md files directly.
let skill_count = std::fs::read_dir(&default_skills_dir)
.ok()
.map(|rd| {
rd.filter_map(|e| e.ok())
.filter(|e| e.path().join("SKILL.md").exists())
.count()
})
.unwrap_or(0);
if skill_count > 0 {
let mut entry = scan_single_plugin(&default_skills_dir, "default-skills");
// Override skill_count since scan_single_plugin looks under skills/.
entry.skill_count = skill_count;
plugins.push(entry);
}
}
MarketplaceScan {
entries: plugins,
catalog_loaded,
}
}
/// Core plugin scanning — tries indexed mode first, falls back to filesystem.
///
/// The component catalog is only consulted in indexed mode: its keys are
/// defined as index names, so the filesystem fallback ignores it.
fn scan_plugins(root: &Path) -> MarketplaceScan {
// Try indexed mode.
match index::load_index(root) {
Ok(Some(idx)) => {
tracing::debug!(
@ -738,34 +702,6 @@ mod tests {
assert_eq!(scan.entries[0].skill_count, 1);
}
#[test]
fn default_skills_virtual_plugin_has_no_components() {
let dir = tempfile::tempdir().unwrap();
write_grok_file(
dir.path(),
"marketplace.json",
r#"{"name": "m", "plugins": []}"#,
);
write_grok_file(
dir.path(),
"plugin-index.json",
r#"{
"version": 1,
"plugins": { "default-skills": { "components": { "skills": [ { "name": "s" } ] } } }
}"#,
);
let skill_dir = dir.path().join("default-skills").join("a-skill");
std::fs::create_dir_all(&skill_dir).unwrap();
std::fs::write(skill_dir.join("SKILL.md"), "# A Skill").unwrap();
let scan = scan_marketplace(dir.path());
assert!(scan.catalog_loaded);
assert_eq!(scan.entries.len(), 1);
assert_eq!(scan.entries[0].name, "default-skills");
assert_eq!(scan.entries[0].skill_count, 1);
assert!(scan.entries[0].components.is_none());
}
#[test]
fn root_plugin_json_preferred() {
let dir = tempfile::tempdir().unwrap();