mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-09 01:24:49 +08:00
feat: add honest plugin inspection reporting
Shift the Rust parity increment away from implying TS-style plugin UX and toward an honest inspection surface. /plugin now reports current local plugin support, checked directories, and missing runtime wiring, while /reload-plugins rebuilds the runtime and prints the same inspection snapshot.\n\nConstraint: Rust only supports local manifest-backed plugins today; marketplace/discovery parity does not exist\nRejected: Stub marketplace installer flow | would overstate current capability\nRejected: Keep /plugin as list-only output | hides important gaps and checked paths\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: Keep plugin reporting aligned with actual runtime wiring; do not advertise manifest commands/hooks as active until the runtime uses them\nTested: cargo test -p commands\nTested: cargo test -p claw-cli\nNot-tested: cargo clippy -p commands -p claw-cli --tests -- -D warnings (blocked by pre-existing workspace warnings in commands/claw-cli/lsp)
This commit is contained in:
19
PARITY.md
19
PARITY.md
@@ -84,16 +84,19 @@ Evidence:
|
|||||||
|
|
||||||
### Rust exists
|
### Rust exists
|
||||||
Evidence:
|
Evidence:
|
||||||
- No dedicated plugin subsystem appears under `rust/crates/`.
|
- Local plugin manifests, registry/state, install/update/uninstall flows, and bundled/external discovery live in `rust/crates/plugins/src/lib.rs`.
|
||||||
- Repo-wide Rust references to plugins are effectively absent beyond text/help mentions.
|
- Runtime config parses plugin settings (`enabledPlugins`, external directories, install root, registry path, bundled root) in `rust/crates/runtime/src/config.rs`.
|
||||||
|
- CLI wiring builds a `PluginManager`, exposes `/plugin` inspection/reporting, and now exposes `/reload-plugins` runtime rebuild/reporting in `rust/crates/commands/src/lib.rs` and `rust/crates/claw-cli/src/main.rs`.
|
||||||
|
- Plugin-provided tools are merged into the runtime tool registry in `rust/crates/claw-cli/src/main.rs` and `rust/crates/tools/src/lib.rs`.
|
||||||
|
|
||||||
### Missing or broken in Rust
|
### Missing or broken in Rust
|
||||||
- No plugin loader.
|
- No TS-style marketplace/discovery/editor UI; current surfaces are local manifest/reporting oriented.
|
||||||
- No marketplace install/update/enable/disable flow.
|
- Plugin-defined slash commands are validated from manifests but not exposed in the CLI runtime.
|
||||||
- No `/plugin` or `/reload-plugins` parity.
|
- Plugin hooks and lifecycle commands are validated but not wired into the conversation runtime startup/shutdown or hook runner.
|
||||||
- No plugin-provided hook/tool/command/MCP extension path.
|
- No plugin-provided MCP/server extension path.
|
||||||
|
- `/reload-plugins` only rebuilds the current local runtime; it is not a richer TS hot-reload/plugin-browser flow.
|
||||||
|
|
||||||
**Status:** missing.
|
**Status:** local plugin discovery/install/inspection exists; TS marketplace/runtime-extension parity is still partial.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -133,7 +136,7 @@ Evidence:
|
|||||||
### Rust exists
|
### Rust exists
|
||||||
Evidence:
|
Evidence:
|
||||||
- Shared slash command registry in `rust/crates/commands/src/lib.rs`.
|
- Shared slash command registry in `rust/crates/commands/src/lib.rs`.
|
||||||
- Rust slash commands currently cover `help`, `status`, `compact`, `model`, `permissions`, `clear`, `cost`, `resume`, `config`, `memory`, `init`, `diff`, `version`, `export`, `session`, `plugin`, `agents`, and `skills`.
|
- Rust slash commands currently cover `help`, `status`, `compact`, `model`, `permissions`, `clear`, `cost`, `resume`, `config`, `hooks`, `memory`, `init`, `diff`, `version`, `export`, `session`, `plugin`, `reload-plugins`, `agents`, and `skills`.
|
||||||
- Main CLI/repl/prompt handling lives in `rust/crates/claw-cli/src/main.rs`.
|
- Main CLI/repl/prompt handling lives in `rust/crates/claw-cli/src/main.rs`.
|
||||||
|
|
||||||
### Missing or broken in Rust
|
### Missing or broken in Rust
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ use api::{
|
|||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
handle_agents_slash_command, handle_hooks_slash_command, handle_plugins_slash_command,
|
handle_agents_slash_command, handle_hooks_slash_command, handle_plugins_slash_command,
|
||||||
handle_skills_slash_command, render_slash_command_help, resume_supported_slash_commands,
|
handle_skills_slash_command, render_plugin_inspection_report, render_slash_command_help,
|
||||||
slash_command_specs, suggest_slash_commands, SlashCommand,
|
resume_supported_slash_commands, slash_command_specs, suggest_slash_commands, SlashCommand,
|
||||||
};
|
};
|
||||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||||
use init::initialize_repo;
|
use init::initialize_repo;
|
||||||
@@ -1015,6 +1015,7 @@ fn run_resume_command(
|
|||||||
| SlashCommand::Permissions { .. }
|
| SlashCommand::Permissions { .. }
|
||||||
| SlashCommand::Session { .. }
|
| SlashCommand::Session { .. }
|
||||||
| SlashCommand::Plugins { .. }
|
| SlashCommand::Plugins { .. }
|
||||||
|
| SlashCommand::ReloadPlugins
|
||||||
| SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()),
|
| SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1340,6 +1341,7 @@ impl LiveCli {
|
|||||||
SlashCommand::Plugins { action, target } => {
|
SlashCommand::Plugins { action, target } => {
|
||||||
self.handle_plugins_command(action.as_deref(), target.as_deref())?
|
self.handle_plugins_command(action.as_deref(), target.as_deref())?
|
||||||
}
|
}
|
||||||
|
SlashCommand::ReloadPlugins => self.reload_plugins_command()?,
|
||||||
SlashCommand::Agents { args } => {
|
SlashCommand::Agents { args } => {
|
||||||
Self::print_agents(args.as_deref())?;
|
Self::print_agents(args.as_deref())?;
|
||||||
false
|
false
|
||||||
@@ -1671,6 +1673,22 @@ impl LiveCli {
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reload_plugins_command(&mut self) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
self.reload_runtime_features()?;
|
||||||
|
|
||||||
|
let cwd = env::current_dir()?;
|
||||||
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
|
let runtime_config = loader.load()?;
|
||||||
|
let manager = build_plugin_manager(&cwd, &loader, &runtime_config);
|
||||||
|
let inspection = manager.inspect()?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Plugin runtime reloaded from local manifests.\n{}",
|
||||||
|
render_plugin_inspection_report(&inspection)
|
||||||
|
);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn reload_runtime_features(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
fn reload_runtime_features(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
self.runtime = build_runtime(
|
self.runtime = build_runtime(
|
||||||
self.runtime.session().clone(),
|
self.runtime.session().clone(),
|
||||||
@@ -4528,8 +4546,9 @@ mod tests {
|
|||||||
assert!(help.contains("/export [file]"));
|
assert!(help.contains("/export [file]"));
|
||||||
assert!(help.contains("/session [list|switch <session-id>]"));
|
assert!(help.contains("/session [list|switch <session-id>]"));
|
||||||
assert!(help.contains(
|
assert!(help.contains(
|
||||||
"/plugin [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
|
"/plugin [inspect|list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
|
||||||
));
|
));
|
||||||
|
assert!(help.contains("/reload-plugins"));
|
||||||
assert!(help.contains("aliases: /plugins, /marketplace"));
|
assert!(help.contains("aliases: /plugins, /marketplace"));
|
||||||
assert!(help.contains("/agents"));
|
assert!(help.contains("/agents"));
|
||||||
assert!(help.contains("/skills"));
|
assert!(help.contains("/skills"));
|
||||||
@@ -4556,11 +4575,20 @@ mod tests {
|
|||||||
.expect("plugin descriptor should exist");
|
.expect("plugin descriptor should exist");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
plugin.description.as_deref(),
|
plugin.description.as_deref(),
|
||||||
Some("Manage Claw Code plugins")
|
Some("Inspect and manage local Claw Code plugins")
|
||||||
);
|
);
|
||||||
assert!(plugin.aliases.contains(&"/plugins".to_string()));
|
assert!(plugin.aliases.contains(&"/plugins".to_string()));
|
||||||
assert!(plugin.aliases.contains(&"/marketplace".to_string()));
|
assert!(plugin.aliases.contains(&"/marketplace".to_string()));
|
||||||
|
|
||||||
|
let reload = descriptors
|
||||||
|
.iter()
|
||||||
|
.find(|descriptor| descriptor.command == "/reload-plugins")
|
||||||
|
.expect("reload plugins descriptor should exist");
|
||||||
|
assert_eq!(
|
||||||
|
reload.description.as_deref(),
|
||||||
|
Some("Reload plugin-derived runtime features and print current support")
|
||||||
|
);
|
||||||
|
|
||||||
let exit = descriptors
|
let exit = descriptors
|
||||||
.iter()
|
.iter()
|
||||||
.find(|descriptor| descriptor.command == "/exit")
|
.find(|descriptor| descriptor.command == "/exit")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use plugins::{PluginError, PluginManager, PluginSummary};
|
use plugins::{PluginError, PluginInspection, PluginManager, PluginSummary};
|
||||||
use runtime::{
|
use runtime::{
|
||||||
compact_session, discover_skill_roots, CompactionConfig, ConfigLoader, ConfigSource,
|
compact_session, discover_skill_roots, CompactionConfig, ConfigLoader, ConfigSource,
|
||||||
RuntimeConfig, Session, SkillDiscoveryRoot, SkillDiscoverySource, SkillRootKind,
|
RuntimeConfig, Session, SkillDiscoveryRoot, SkillDiscoverySource, SkillRootKind,
|
||||||
@@ -285,13 +285,21 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "plugin",
|
name: "plugin",
|
||||||
aliases: &["plugins", "marketplace"],
|
aliases: &["plugins", "marketplace"],
|
||||||
summary: "Manage Claw Code plugins",
|
summary: "Inspect and manage local Claw Code plugins",
|
||||||
argument_hint: Some(
|
argument_hint: Some(
|
||||||
"[list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]",
|
"[inspect|list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]",
|
||||||
),
|
),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Automation,
|
category: SlashCommandCategory::Automation,
|
||||||
},
|
},
|
||||||
|
SlashCommandSpec {
|
||||||
|
name: "reload-plugins",
|
||||||
|
aliases: &[],
|
||||||
|
summary: "Reload plugin-derived runtime features and print current support",
|
||||||
|
argument_hint: None,
|
||||||
|
resume_supported: false,
|
||||||
|
category: SlashCommandCategory::Automation,
|
||||||
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "agents",
|
name: "agents",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -378,6 +386,7 @@ pub enum SlashCommand {
|
|||||||
action: Option<String>,
|
action: Option<String>,
|
||||||
target: Option<String>,
|
target: Option<String>,
|
||||||
},
|
},
|
||||||
|
ReloadPlugins,
|
||||||
Agents {
|
Agents {
|
||||||
args: Option<String>,
|
args: Option<String>,
|
||||||
},
|
},
|
||||||
@@ -467,6 +476,7 @@ impl SlashCommand {
|
|||||||
(!remainder.is_empty()).then_some(remainder)
|
(!remainder.is_empty()).then_some(remainder)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"reload-plugins" => Self::ReloadPlugins,
|
||||||
"agents" => Self::Agents {
|
"agents" => Self::Agents {
|
||||||
args: remainder_after_command(trimmed, command),
|
args: remainder_after_command(trimmed, command),
|
||||||
},
|
},
|
||||||
@@ -688,7 +698,11 @@ pub fn handle_plugins_slash_command(
|
|||||||
manager: &mut PluginManager,
|
manager: &mut PluginManager,
|
||||||
) -> Result<PluginsCommandResult, PluginError> {
|
) -> Result<PluginsCommandResult, PluginError> {
|
||||||
match action {
|
match action {
|
||||||
None | Some("list") => Ok(PluginsCommandResult {
|
None | Some("inspect" | "status") => Ok(PluginsCommandResult {
|
||||||
|
message: render_plugin_inspection_report(&manager.inspect()?),
|
||||||
|
reload_runtime: false,
|
||||||
|
}),
|
||||||
|
Some("list") => Ok(PluginsCommandResult {
|
||||||
message: render_plugins_report(&manager.list_installed_plugins()?),
|
message: render_plugins_report(&manager.list_installed_plugins()?),
|
||||||
reload_runtime: false,
|
reload_runtime: false,
|
||||||
}),
|
}),
|
||||||
@@ -786,7 +800,7 @@ pub fn handle_plugins_slash_command(
|
|||||||
}
|
}
|
||||||
Some(other) => Ok(PluginsCommandResult {
|
Some(other) => Ok(PluginsCommandResult {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Unknown /plugins action '{other}'. Use list, install, enable, disable, uninstall, or update."
|
"Unknown /plugins action '{other}'. Use inspect, list, install, enable, disable, uninstall, or update."
|
||||||
),
|
),
|
||||||
reload_runtime: false,
|
reload_runtime: false,
|
||||||
}),
|
}),
|
||||||
@@ -1242,6 +1256,87 @@ pub fn render_plugins_report(plugins: &[PluginSummary]) -> String {
|
|||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_plugin_inspection_report(inspection: &PluginInspection) -> String {
|
||||||
|
let mut lines = vec![
|
||||||
|
"Plugins".to_string(),
|
||||||
|
" Current support Local manifest discovery plus install/update/uninstall and enable/disable state".to_string(),
|
||||||
|
" Runtime wiring Plugin tools load on runtime rebuild; manifest-defined hooks, lifecycle, slash commands, and MCP extensions are not wired yet".to_string(),
|
||||||
|
format!(
|
||||||
|
" Discoverable {} total",
|
||||||
|
inspection.discoverable_plugins.len()
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
" Installed {} total",
|
||||||
|
inspection.installed_plugins.len()
|
||||||
|
),
|
||||||
|
" Checked locations".to_string(),
|
||||||
|
render_report_path("Install root", &inspection.install_root, inspection.install_root.exists()),
|
||||||
|
render_report_path("Bundled root", &inspection.bundled_root, inspection.bundled_root.exists()),
|
||||||
|
];
|
||||||
|
|
||||||
|
if inspection.external_dirs.is_empty() {
|
||||||
|
lines.push(" External dirs none configured".to_string());
|
||||||
|
} else {
|
||||||
|
for (index, directory) in inspection.external_dirs.iter().enumerate() {
|
||||||
|
lines.push(render_report_path(
|
||||||
|
if index == 0 {
|
||||||
|
"External dirs"
|
||||||
|
} else {
|
||||||
|
"External dir"
|
||||||
|
},
|
||||||
|
directory,
|
||||||
|
directory.exists(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(render_report_path(
|
||||||
|
"Registry file",
|
||||||
|
&inspection.registry_path,
|
||||||
|
inspection.registry_path.exists(),
|
||||||
|
));
|
||||||
|
lines.push(render_report_path(
|
||||||
|
"Settings file",
|
||||||
|
&inspection.settings_path,
|
||||||
|
inspection.settings_path.exists(),
|
||||||
|
));
|
||||||
|
|
||||||
|
lines.push(" Missing parity".to_string());
|
||||||
|
lines.push(" TS marketplace/discovery UI is not implemented.".to_string());
|
||||||
|
lines.push(
|
||||||
|
" Plugin-defined slash commands are parsed from manifests but not exposed.".to_string(),
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
" Plugin hooks/lifecycle are validated but not attached to the conversation runtime."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
" No plugin hot-swap beyond /reload-plugins rebuilding the current runtime.".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
lines.push(" Installed plugins".to_string());
|
||||||
|
if inspection.installed_plugins.is_empty() {
|
||||||
|
lines.push(" none".to_string());
|
||||||
|
} else {
|
||||||
|
for plugin in &inspection.installed_plugins {
|
||||||
|
lines.push(format!(
|
||||||
|
" - {} · {} v{} · {}",
|
||||||
|
plugin.metadata.id,
|
||||||
|
plugin.metadata.kind,
|
||||||
|
plugin.metadata.version,
|
||||||
|
if plugin.enabled {
|
||||||
|
"enabled"
|
||||||
|
} else {
|
||||||
|
"disabled"
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
fn render_plugin_install_report(plugin_id: &str, plugin: Option<&PluginSummary>) -> String {
|
fn render_plugin_install_report(plugin_id: &str, plugin: Option<&PluginSummary>) -> String {
|
||||||
let name = plugin.map_or(plugin_id, |plugin| plugin.metadata.name.as_str());
|
let name = plugin.map_or(plugin_id, |plugin| plugin.metadata.name.as_str());
|
||||||
let version = plugin.map_or("unknown", |plugin| plugin.metadata.version.as_str());
|
let version = plugin.map_or("unknown", |plugin| plugin.metadata.version.as_str());
|
||||||
@@ -1272,6 +1367,14 @@ fn resolve_plugin_target(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_report_path(label: &str, path: &Path, exists: bool) -> String {
|
||||||
|
format!(
|
||||||
|
" {label:<15} {} ({})",
|
||||||
|
path.display(),
|
||||||
|
if exists { "present" } else { "missing" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, PathBuf)> {
|
fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, PathBuf)> {
|
||||||
let mut roots = Vec::new();
|
let mut roots = Vec::new();
|
||||||
|
|
||||||
@@ -1801,6 +1904,7 @@ pub fn handle_slash_command(
|
|||||||
| SlashCommand::Export { .. }
|
| SlashCommand::Export { .. }
|
||||||
| SlashCommand::Session { .. }
|
| SlashCommand::Session { .. }
|
||||||
| SlashCommand::Plugins { .. }
|
| SlashCommand::Plugins { .. }
|
||||||
|
| SlashCommand::ReloadPlugins
|
||||||
| SlashCommand::Agents { .. }
|
| SlashCommand::Agents { .. }
|
||||||
| SlashCommand::Skills { .. }
|
| SlashCommand::Skills { .. }
|
||||||
| SlashCommand::Unknown(_) => None,
|
| SlashCommand::Unknown(_) => None,
|
||||||
@@ -2135,6 +2239,10 @@ mod tests {
|
|||||||
target: Some("demo".to_string())
|
target: Some("demo".to_string())
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
SlashCommand::parse("/reload-plugins"),
|
||||||
|
Some(SlashCommand::ReloadPlugins)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2173,12 +2281,13 @@ mod tests {
|
|||||||
assert!(help.contains("/export [file]"));
|
assert!(help.contains("/export [file]"));
|
||||||
assert!(help.contains("/session [list|switch <session-id>]"));
|
assert!(help.contains("/session [list|switch <session-id>]"));
|
||||||
assert!(help.contains(
|
assert!(help.contains(
|
||||||
"/plugin [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
|
"/plugin [inspect|list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
|
||||||
));
|
));
|
||||||
assert!(help.contains("aliases: /plugins, /marketplace"));
|
assert!(help.contains("aliases: /plugins, /marketplace"));
|
||||||
|
assert!(help.contains("/reload-plugins"));
|
||||||
assert!(help.contains("/agents"));
|
assert!(help.contains("/agents"));
|
||||||
assert!(help.contains("/skills"));
|
assert!(help.contains("/skills"));
|
||||||
assert_eq!(slash_command_specs().len(), 29);
|
assert_eq!(slash_command_specs().len(), 30);
|
||||||
assert_eq!(resume_supported_slash_commands().len(), 14);
|
assert_eq!(resume_supported_slash_commands().len(), 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2299,6 +2408,10 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
handle_slash_command("/plugins list", &session, CompactionConfig::default()).is_none()
|
handle_slash_command("/plugins list", &session, CompactionConfig::default()).is_none()
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
handle_slash_command("/reload-plugins", &session, CompactionConfig::default())
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2340,6 +2453,47 @@ mod tests {
|
|||||||
assert!(rendered.contains("disabled"));
|
assert!(rendered.contains("disabled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_plugin_action_renders_inspection_report() {
|
||||||
|
let config_home = temp_dir("plugins-inspect-home");
|
||||||
|
let bundled_root = temp_dir("plugins-inspect-bundled");
|
||||||
|
let bundled_plugin = bundled_root.join("starter");
|
||||||
|
write_bundled_plugin(&bundled_plugin, "starter", "0.1.0", false);
|
||||||
|
|
||||||
|
let mut config = PluginManagerConfig::new(&config_home);
|
||||||
|
config.bundled_root = Some(bundled_root.clone());
|
||||||
|
config.external_dirs = vec![config_home.join("external")];
|
||||||
|
let mut manager = PluginManager::new(config);
|
||||||
|
|
||||||
|
let inspection = handle_plugins_slash_command(None, None, &mut manager)
|
||||||
|
.expect("inspect command should succeed");
|
||||||
|
assert!(!inspection.reload_runtime);
|
||||||
|
assert!(inspection.message.contains("Current support"));
|
||||||
|
assert!(inspection.message.contains("Checked locations"));
|
||||||
|
assert!(inspection
|
||||||
|
.message
|
||||||
|
.contains(&manager.install_root().display().to_string()));
|
||||||
|
assert!(inspection
|
||||||
|
.message
|
||||||
|
.contains(&manager.bundled_root_path().display().to_string()));
|
||||||
|
assert!(inspection
|
||||||
|
.message
|
||||||
|
.contains(&manager.registry_path().display().to_string()));
|
||||||
|
assert!(inspection
|
||||||
|
.message
|
||||||
|
.contains(&manager.settings_path().display().to_string()));
|
||||||
|
assert!(inspection
|
||||||
|
.message
|
||||||
|
.contains("Plugin-defined slash commands are parsed from manifests but not exposed."));
|
||||||
|
assert!(inspection.message.contains(
|
||||||
|
"Plugin hooks/lifecycle are validated but not attached to the conversation runtime."
|
||||||
|
));
|
||||||
|
assert!(inspection.message.contains("starter@bundled"));
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(config_home);
|
||||||
|
let _ = fs::remove_dir_all(bundled_root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lists_agents_from_project_and_user_roots() {
|
fn lists_agents_from_project_and_user_roots() {
|
||||||
let workspace = temp_dir("agents-workspace");
|
let workspace = temp_dir("agents-workspace");
|
||||||
|
|||||||
@@ -648,6 +648,17 @@ pub struct PluginSummary {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct PluginInspection {
|
||||||
|
pub install_root: PathBuf,
|
||||||
|
pub registry_path: PathBuf,
|
||||||
|
pub settings_path: PathBuf,
|
||||||
|
pub bundled_root: PathBuf,
|
||||||
|
pub external_dirs: Vec<PathBuf>,
|
||||||
|
pub discoverable_plugins: Vec<PluginSummary>,
|
||||||
|
pub installed_plugins: Vec<PluginSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq)]
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
pub struct PluginRegistry {
|
pub struct PluginRegistry {
|
||||||
plugins: Vec<RegisteredPlugin>,
|
plugins: Vec<RegisteredPlugin>,
|
||||||
@@ -934,6 +945,31 @@ impl PluginManager {
|
|||||||
self.config.config_home.join(SETTINGS_FILE_NAME)
|
self.config.config_home.join(SETTINGS_FILE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn bundled_root_path(&self) -> PathBuf {
|
||||||
|
self.config
|
||||||
|
.bundled_root
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(Self::bundled_root)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn external_dirs(&self) -> &[PathBuf] {
|
||||||
|
&self.config.external_dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inspect(&self) -> Result<PluginInspection, PluginError> {
|
||||||
|
Ok(PluginInspection {
|
||||||
|
install_root: self.install_root(),
|
||||||
|
registry_path: self.registry_path(),
|
||||||
|
settings_path: self.settings_path(),
|
||||||
|
bundled_root: self.bundled_root_path(),
|
||||||
|
external_dirs: self.external_dirs().to_vec(),
|
||||||
|
discoverable_plugins: self.list_plugins()?,
|
||||||
|
installed_plugins: self.list_installed_plugins()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn plugin_registry(&self) -> Result<PluginRegistry, PluginError> {
|
pub fn plugin_registry(&self) -> Result<PluginRegistry, PluginError> {
|
||||||
Ok(PluginRegistry::new(
|
Ok(PluginRegistry::new(
|
||||||
self.discover_plugins()?
|
self.discover_plugins()?
|
||||||
|
|||||||
Reference in New Issue
Block a user