systemprompt_cli/commands/core/plugins/generate/
mod.rs1pub mod agents;
12pub mod marketplace;
13pub mod mcp;
14pub mod skills;
15
16use anyhow::{Context, Result, anyhow};
17use clap::Args;
18use std::path::{Path, PathBuf};
19
20use crate::CliConfig;
21use crate::shared::CommandOutput;
22use systemprompt_models::PluginConfigFile;
23
24use super::types::{PluginGenerateAllOutput, PluginGenerateOutput};
25
26const DEFAULT_AGENT_TOOLS: &str = "Read, Grep, Glob, Bash, Write, Edit, WebFetch, WebSearch";
27
28#[derive(Debug, Clone, Args)]
29pub struct GenerateArgs {
30 #[arg(long, help = "Plugin ID to generate (generates all if omitted)")]
31 pub id: Option<String>,
32
33 #[arg(long, help = "Output directory (defaults to plugin directory)")]
34 pub output_dir: Option<String>,
35}
36
37#[derive(Debug, Clone, Copy)]
38pub struct PluginGenerateContext<'a> {
39 pub plugins_path: &'a Path,
40 pub skills_path: &'a Path,
41 pub services_path: &'a Path,
42 pub output_dir_override: Option<&'a str>,
43}
44
45pub(super) fn execute(args: &GenerateArgs, _config: &CliConfig) -> Result<CommandOutput> {
46 let profile = systemprompt_config::ProfileBootstrap::get().context("Failed to get profile")?;
47 let plugins_path = PathBuf::from(profile.paths.plugins());
48 let skills_path = PathBuf::from(profile.paths.skills());
49 let services_path = PathBuf::from(&profile.paths.services);
50
51 let plugin_ids = match &args.id {
52 Some(id) => {
53 let plugin_dir = plugins_path.join(id);
54 if !plugin_dir.exists() {
55 return Err(anyhow!("Plugin '{}' not found", id));
56 }
57 vec![id.clone()]
58 },
59 None => collect_plugin_ids(&plugins_path)?,
60 };
61
62 let ctx = PluginGenerateContext {
63 plugins_path: &plugins_path,
64 skills_path: &skills_path,
65 services_path: &services_path,
66 output_dir_override: args.output_dir.as_deref(),
67 };
68
69 let mut results = Vec::new();
70
71 for plugin_id in &plugin_ids {
72 let result = generate_plugin(plugin_id, &ctx)?;
73 results.push(result);
74 }
75
76 let plugins_output_path = services_path
77 .join("..")
78 .join("storage")
79 .join("files")
80 .join("plugins");
81 marketplace::generate_marketplace_json(&plugins_path, &plugins_output_path)?;
82
83 let install_hint = extract_install_command(profile);
84
85 let output = PluginGenerateAllOutput {
86 results,
87 install_command: install_hint,
88 };
89
90 Ok(CommandOutput::card_value(
91 "Plugin Generation Complete",
92 &output,
93 ))
94}
95
96pub fn collect_plugin_ids(plugins_path: &Path) -> Result<Vec<String>> {
97 if !plugins_path.exists() {
98 return Ok(Vec::new());
99 }
100
101 let mut ids = Vec::new();
102 for entry in std::fs::read_dir(plugins_path)? {
103 let entry = entry?;
104 if entry.path().is_dir()
105 && entry.path().join("config.yaml").exists()
106 && let Some(name) = entry.file_name().to_str()
107 {
108 ids.push(name.to_owned());
109 }
110 }
111 ids.sort();
112 Ok(ids)
113}
114
115pub fn generate_plugin(
116 plugin_id: &str,
117 ctx: &PluginGenerateContext<'_>,
118) -> Result<PluginGenerateOutput> {
119 let config_path = ctx.plugins_path.join(plugin_id).join("config.yaml");
120 let content = std::fs::read_to_string(&config_path)
121 .with_context(|| format!("Failed to read {}", config_path.display()))?;
122 let plugin_file: PluginConfigFile = serde_yaml::from_str(&content)
123 .with_context(|| format!("Failed to parse {}", config_path.display()))?;
124 let plugin = &plugin_file.plugin;
125
126 let output_dir = ctx.output_dir_override.map_or_else(
127 || {
128 ctx.services_path
129 .join("..")
130 .join("storage")
131 .join("files")
132 .join("plugins")
133 .join(plugin_id)
134 },
135 PathBuf::from,
136 );
137
138 let mut files_generated = Vec::new();
139
140 skills::generate_skills(plugin, ctx.skills_path, &output_dir, &mut files_generated)?;
141 agents::generate_agents(plugin, ctx.services_path, &output_dir, &mut files_generated)?;
142 mcp::generate_mcp_json(plugin, ctx.services_path, &output_dir, &mut files_generated)?;
143 marketplace::copy_scripts(
144 plugin,
145 ctx.plugins_path,
146 plugin_id,
147 &output_dir,
148 &mut files_generated,
149 )?;
150 marketplace::generate_plugin_json(plugin, &output_dir, &mut files_generated)?;
151
152 Ok(PluginGenerateOutput {
153 plugin_id: systemprompt_identifiers::PluginId::new(plugin_id),
154 files_generated,
155 marketplace_path: output_dir.to_string_lossy().to_string(),
156 })
157}
158
159pub fn extract_install_command(profile: &systemprompt_models::Profile) -> Option<String> {
160 let github_link = profile.site.github_link.as_deref()?;
161 let repo_path = github_link
162 .trim_end_matches('/')
163 .trim_end_matches(".git")
164 .rsplit("github.com/")
165 .next()?;
166
167 if repo_path.contains('/') {
168 Some(format!("/plugin marketplace add {}", repo_path))
169 } else {
170 None
171 }
172}