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