Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 131 additions & 53 deletions src/commands/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::plugins::{PluginLoadError, PluginManager};
use crate::utils::print as p;
use anyhow::Result;
use clap::Subcommand;
use colored::*;
use starforge::utils::config;
use std::path::Path;
use std::path::PathBuf;

Expand Down Expand Up @@ -113,8 +113,8 @@ pub fn handle(cmd: PluginCommands) -> Result<()> {
fn install(name: String, path: Option<PathBuf>, source: Option<String>, force: bool) -> Result<()> {
let lib_path = registry::resolve_plugin_library_path(&name, path)?;
let source_str = source.as_deref().unwrap_or("");
let config = crate::utils::config::load().unwrap_or_default();
let trust = registry::classify_source(source_str);
let config = config::load().unwrap_or_default();
let trust = registry::classify_source_with_config(source_str, &config);

// Warn the user about untrusted sources and require --force to proceed.
if trust == TrustLevel::Unknown && !source_str.is_empty() && !force {
Expand All @@ -135,22 +135,39 @@ fn install(name: String, path: Option<PathBuf>, source: Option<String>, force: b

let plugin_manifest = manifest::require_compatible_manifest(&lib_path, &name)?;

let discovered_commands = discover_commands_from_library(lib_path.to_str().unwrap_or_default())
.unwrap_or_else(|e| {
p::warn(&format!(
"Could not discover commands from '{}': {}",
lib_path.display(),
e
));
Vec::new()
});
// Load the plugin to discover the commands and description it registers.
let (discovered_commands, plugin_description) = {
let mut pm = PluginManager::new();
unsafe {
pm.load_plugin(&lib_path).with_context(|| {
format!("Failed to load plugin '{}' to discover commands", name)
})?;
}
let commands: Vec<RegisteredCommand> = pm
.list_commands()
.into_iter()
.map(|c| RegisteredCommand {
name: c.name,
description: c.description,
})
.collect();
let description = pm
.list_plugins()
.into_iter()
.find(|(plugin_name, _, _)| *plugin_name == name)
.map(|(_, desc, _)| desc.to_string())
.filter(|d| !d.is_empty())
.unwrap_or_else(|| plugin_manifest.description.clone());
(commands, description)
};

registry::install_plugin(
&name,
&lib_path,
source_str,
&plugin_manifest.starforge_version,
&plugin_manifest.version,
&plugin_description,
discovered_commands.clone(),
)?;

Expand Down Expand Up @@ -187,20 +204,41 @@ fn list() -> Result<()> {

p::kv("StarForge core version", CORE_VERSION);
p::separator();
for (i, pl) in reg.plugins.iter().enumerate() {
println!(" {:>2}. {}", i + 1, pl.name);
p::kv("Path", &pl.path);
p::kv("Trust", pl.trust.label());
if !pl.source.is_empty() {
p::kv("Source", &pl.source);
}
if !pl.starforge_version.is_empty() {
p::kv("StarForge", &pl.starforge_version);
}
if i < reg.plugins.len() - 1 {
println!();
}

let entries = registry::plugin_list_entries(&reg);

let plugin_rows: Vec<Vec<String>> = entries
.iter()
.map(|entry| {
vec![
entry.name.clone(),
entry.version.clone(),
entry.trust.label().to_string(),
entry.description.clone(),
]
})
.collect();
p::table(&["Name", "Version", "Trust", "Description"], &plugin_rows);

let command_rows: Vec<Vec<String>> = entries
.iter()
.flat_map(|entry| {
entry.commands.iter().map(|cmd| {
vec![
entry.name.clone(),
cmd.name.clone(),
cmd.description.clone(),
]
})
})
.collect();

if !command_rows.is_empty() {
println!();
p::info("Commands");
p::table(&["Plugin", "Command", "Description"], &command_rows);
}

p::separator();
Ok(())
}
Expand Down Expand Up @@ -253,6 +291,8 @@ fn load() -> Result<()> {
return Ok(());
}

let config = config::load().unwrap_or_default();

// Warn about any unknown-trust plugins before loading.
for pl in reg.plugins.iter().filter(|p| {
registry::classify_source(&p.source) == TrustLevel::Unknown && !p.source.is_empty()
Expand Down Expand Up @@ -404,6 +444,8 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {
return Ok(());
}

let config = config::load().unwrap_or_default();

let to_update: Vec<_> = match &name {
Some(n) => {
let found: Vec<_> = reg.plugins.iter().filter(|p| &p.name == n).collect();
Expand Down Expand Up @@ -491,6 +533,7 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {
&pl.source,
&pl.starforge_version,
&pl.plugin_version,
&pl.description,
pl.commands.clone(),
)?;
p::success(&format!(" '{}' updated via cargo install", pl.name));
Expand Down Expand Up @@ -531,14 +574,15 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {

if modified > installed_epoch {
// Library on disk is newer — refresh the registry entry.
let cmds = discover_commands_from_library(&pl.path)
.unwrap_or_else(|_| pl.commands.clone());
let (cmds, description) = discover_plugin_metadata(&pl.path)
.unwrap_or_else(|_| (pl.commands.clone(), pl.description.clone()));
registry::install_plugin(
&pl.name,
std::path::Path::new(&pl.path),
&pl.source,
&pl.starforge_version,
&pl.plugin_version,
&description,
cmds,
)?;
p::success(&format!(
Expand Down Expand Up @@ -604,6 +648,7 @@ fn verify(name: Option<String>, deep: bool, runtime_check: bool) -> Result<()> {
None => reg.plugins.iter().collect(),
};

let config = config::load().unwrap_or_default();
let mut all_ok = true;

for pl in &to_check {
Expand Down Expand Up @@ -931,41 +976,74 @@ fn print_audit_report(report: &AuditReport) {
println!();
}

fn commands(_name: Option<String>) -> Result<()> {
fn discover_plugin_metadata(path: &str) -> Result<(Vec<RegisteredCommand>, String)> {
let mut pm = PluginManager::new();
unsafe {
pm.load_plugin(path)
.with_context(|| format!("Failed to load plugin from {}", path))?;
}
let commands = pm
.list_commands()
.into_iter()
.map(|c| RegisteredCommand {
name: c.name,
description: c.description,
})
.collect();
let description = pm
.list_plugins()
.into_iter()
.map(|(_, desc, _)| desc.to_string())
.find(|d| !d.is_empty())
.unwrap_or_default();
Ok((commands, description))
}

fn commands(name: Option<String>) -> Result<()> {
p::header("Plugin Commands");

let reg = registry::load_registry().unwrap_or_default();
if reg.plugins.is_empty() {
p::info("No plugins installed. Use: starforge plugin install <name> --path <lib>");
return Ok(());
}

p::separator();
for plugin in &reg.plugins {
p::kv_accent("Plugin", &plugin.name);
if !plugin.commands.is_empty() {
for cmd in &plugin.commands {
p::info(&format!(" • {} — {}", cmd.name, cmd.description));
let entries: Vec<_> = match &name {
Some(n) => {
let found: Vec<_> = registry::plugin_list_entries(&reg)
.into_iter()
.filter(|entry| entry.name == *n)
.collect();
if found.is_empty() {
anyhow::bail!(
"Plugin '{}' is not installed. Run `starforge plugin list`.",
n
);
}
} else {
p::info(" (no commands registered)");
found
}
println!();
}
p::separator();
Ok(())
}
None => registry::plugin_list_entries(&reg),
};

fn discover_commands_from_library(path: &str) -> Result<Vec<RegisteredCommand>> {
let mut pm = PluginManager::new();
unsafe {
pm.load_plugin(path)?;
}
Ok(pm
.list_commands()
.into_iter()
.map(|c| RegisteredCommand {
name: c.name,
description: c.description,
let rows: Vec<Vec<String>> = entries
.iter()
.flat_map(|entry| {
entry.commands.iter().map(|cmd| {
vec![
entry.name.clone(),
cmd.name.clone(),
cmd.description.clone(),
]
})
})
.collect())
.collect();

if rows.is_empty() {
p::info("No commands registered. Re-install plugins to discover their commands.");
p::info(" starforge plugin install <name> --path <lib>");
return Ok(());
}

p::table(&["Plugin", "Command", "Description"], &rows);
Ok(())
}
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
)]

mod commands;
pub use starforge::{plugins, utils};
pub use starforge::plugins;
pub use starforge::utils;

use clap::{Parser, Subcommand};
use colored::*;
Expand Down Expand Up @@ -214,6 +215,7 @@ fn handle_external_plugin(args: Vec<String>) -> anyhow::Result<()> {
let plugin_name = &args[0];
let plugin_args = &args[1..];

let cfg = starforge::utils::config::load()?;
let reg = plugins::registry::load_registry().unwrap_or_default();
if reg.plugins.is_empty() {
anyhow::bail!(
Expand Down
Loading
Loading