| 1 | //! Legacy executable plugin-tool inventory (`[tools].plugin_dir`). |
| 2 | //! |
| 3 | //! These are scripts, not declarative bundles: they are discovered by |
| 4 | //! scanning a directory, they carry their own approval requirement, and |
| 5 | //! they never share bundle trust state. `/plugin tools` reports them |
| 6 | //! read-only — nothing here installs, trusts, or executes anything. |
| 7 | |
| 8 | use std::fmt::Write as _; |
| 9 | use std::path::{Path, PathBuf}; |
| 10 | |
| 11 | use crate::commands::CommandResult; |
| 12 | use crate::localization::{MessageId, tr}; |
| 13 | use crate::tools::plugin::{PluginMetadata, scan_plugin_dir}; |
| 14 | use crate::tools::spec::ApprovalRequirement; |
| 15 | use crate::tui::app::App; |
| 16 | |
| 17 | use super::action_error; |
| 18 | |
| 19 | pub(super) fn legacy_tools(app: &App, name: Option<&str>) -> CommandResult { |
| 20 | let Some(plugin_dir) = plugin_dir_for(app) else { |
| 21 | return action_error( |
| 22 | app, |
| 23 | "Could not resolve the legacy executable plugin-tool directory", |
| 24 | ); |
| 25 | }; |
| 26 | if !plugin_dir.exists() { |
| 27 | return CommandResult::message( |
| 28 | tr(app.ui_locale, MessageId::CmdPluginNoneFound) |
| 29 | .replace("{dir}", &plugin_dir.display().to_string()), |
| 30 | ); |
| 31 | } |
| 32 | let discovered = scan_plugin_dir(&plugin_dir); |
| 33 | match name { |
| 34 | Some(name) => show_legacy_tool_detail(app, name, &discovered), |
| 35 | None => list_legacy_tools(app, &plugin_dir, &discovered), |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | fn list_legacy_tools( |
| 40 | app: &App, |
| 41 | plugin_dir: &Path, |
| 42 | discovered: &[(PathBuf, PluginMetadata)], |
| 43 | ) -> CommandResult { |
| 44 | if discovered.is_empty() { |
| 45 | return CommandResult::message( |
| 46 | tr(app.ui_locale, MessageId::CmdPluginNoneFound) |
| 47 | .replace("{dir}", &plugin_dir.display().to_string()), |
| 48 | ); |
| 49 | } |
| 50 | let mut output = tr(app.ui_locale, MessageId::CmdPluginLegacyListHeader) |
| 51 | .replace("{count}", &discovered.len().to_string()) |
| 52 | .replace("{dir}", &plugin_dir.display().to_string()); |
| 53 | output.push('\n'); |
| 54 | for (path, metadata) in discovered { |
| 55 | let _ = writeln!( |
| 56 | output, |
| 57 | "• {} — {}\n {}", |
| 58 | metadata.name, |
| 59 | metadata.description, |
| 60 | path.display() |
| 61 | ); |
| 62 | } |
| 63 | CommandResult::message(output) |
| 64 | } |
| 65 | |
| 66 | fn show_legacy_tool_detail( |
| 67 | app: &App, |
| 68 | name: &str, |
| 69 | discovered: &[(PathBuf, PluginMetadata)], |
| 70 | ) -> CommandResult { |
| 71 | let Some((path, metadata)) = discovered |
| 72 | .iter() |
| 73 | .find(|(_, metadata)| metadata.name == name) |
| 74 | else { |
| 75 | return CommandResult::error( |
| 76 | tr(app.ui_locale, MessageId::CmdPluginNotFound).replace("{name}", name), |
| 77 | ); |
| 78 | }; |
| 79 | let schema = serde_json::to_string_pretty(&metadata.input_schema).unwrap_or_default(); |
| 80 | let mut output = format!("{}\n{:=<40}\n", metadata.name, ""); |
| 81 | let _ = writeln!( |
| 82 | output, |
| 83 | "{}", |
| 84 | tr(app.ui_locale, MessageId::CmdPluginDetailDescription) |
| 85 | .replace("{description}", &metadata.description) |
| 86 | ); |
| 87 | let _ = writeln!( |
| 88 | output, |
| 89 | "{}", |
| 90 | tr(app.ui_locale, MessageId::CmdPluginDetailSchema).replace("{schema}", &schema) |
| 91 | ); |
| 92 | let _ = writeln!( |
| 93 | output, |
| 94 | "{}", |
| 95 | tr(app.ui_locale, MessageId::CmdPluginDetailApproval) |
| 96 | .replace("{approval}", approval_label(metadata.approval)) |
| 97 | ); |
| 98 | let _ = writeln!( |
| 99 | output, |
| 100 | "{}", |
| 101 | tr(app.ui_locale, MessageId::CmdPluginDetailPath) |
| 102 | .replace("{path}", &path.display().to_string()) |
| 103 | ); |
| 104 | CommandResult::message(output) |
| 105 | } |
| 106 | |
| 107 | pub(super) fn scan_legacy_tools(app: &App) -> Option<(PathBuf, Vec<(PathBuf, PluginMetadata)>)> { |
| 108 | let dir = plugin_dir_for(app)?; |
| 109 | dir.exists().then(|| { |
| 110 | let tools = scan_plugin_dir(&dir); |
| 111 | (dir, tools) |
| 112 | }) |
| 113 | } |
| 114 | |
| 115 | fn approval_label(approval: ApprovalRequirement) -> &'static str { |
| 116 | match approval { |
| 117 | ApprovalRequirement::Auto => "auto", |
| 118 | ApprovalRequirement::Suggest => "suggest", |
| 119 | ApprovalRequirement::Required => "required", |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | fn plugin_dir_for(app: &App) -> Option<PathBuf> { |
| 124 | app.legacy_plugin_tools_dir |
| 125 | .clone() |
| 126 | .or_else(default_codewhale_tools_dir) |
| 127 | } |
| 128 | |
| 129 | fn default_codewhale_tools_dir() -> Option<PathBuf> { |
| 130 | codewhale_config::codewhale_home() |
| 131 | .ok() |
| 132 | .map(|home| home.join("tools")) |
| 133 | } |
| 134 |