feat(plugins): redesign plugin manager

This commit is contained in:
Hakan Seven committed 2026-07-29 14:27:32 +03:00
1 parent d80637cda7
commit 4e25e88c31
11 files changed
+741 -74

No files matched your search

+34
View File
@@ -30,6 +30,10 @@ pub struct ExternalPlugin {
pub name: String,
pub version: String,
pub description: String,
/// GitHub source in `owner/repo` form. New marketplace installs persist
/// this beside the manifest so their README remains discoverable even when
/// an older `plugin.toml` does not declare `repository`.
pub repository: Option<String>,
pub api_version: u32,
pub ribbon_order: i32,
pub command_prefixes: Vec<String>,
@@ -125,6 +129,11 @@ pub fn discover() -> Vec<ExternalPlugin> {
continue;
};
if let Some(mut p) = parse_plugin_toml(&text) {
if p.repository.is_none() {
p.repository = std::fs::read_to_string(dir.join(".source_repo"))
.ok()
.and_then(|repo| normalize_repository(&repo));
}
p.lib_present = lib_present_in(&dir);
p.dir = dir;
found.push(p);
@@ -156,6 +165,7 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
let mut name = String::new();
let mut version = String::new();
let mut description = String::new();
let mut repository = None;
let mut api_version: u32 = 0;
let mut ribbon_order: i32 = 0;
let mut command_prefixes: Vec<String> = Vec::new();
@@ -175,6 +185,7 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
"name" => name = unquote(value),
"version" => version = unquote(value),
"description" => description = unquote(value),
"repository" => repository = normalize_repository(&unquote(value)),
"api_version" => api_version = value.parse().unwrap_or(0),
"ribbon_order" => ribbon_order = value.parse().unwrap_or(0),
"command_prefixes" => command_prefixes = parse_string_array(value),
@@ -187,6 +198,7 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
name,
version,
description,
repository,
api_version,
ribbon_order,
command_prefixes,
@@ -195,6 +207,26 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
})
}
/// Convert a plugin source URL or shorthand to the marketplace's canonical
/// `owner/repo` form.
pub(crate) fn normalize_repository(value: &str) -> Option<String> {
let repo = value
.trim()
.trim_start_matches("https://github.com/")
.trim_start_matches("http://github.com/")
.trim_end_matches('/')
.trim_end_matches(".git");
let mut parts = repo.split('/');
let (Some(owner), Some(name), None) = (parts.next(), parts.next(), parts.next()) else {
return None;
};
if owner.is_empty() || name.is_empty() {
None
} else {
Some(format!("{owner}/{name}"))
}
}
/// Strip surrounding single or double quotes from a TOML scalar.
fn unquote(s: &str) -> String {
let s = s.trim();
@@ -321,6 +353,7 @@ id = "opencad.my_plugin"
name = "My Plugin"
version = "0.1.0"
description = "Template plugin"
repository = "https://github.com/example/opencad-my-plugin.git"
[opencad]
api_version = 2
@@ -330,6 +363,7 @@ xdata_apps = ["MYPLUGIN_RECORD"]
"#;
let p = parse_plugin_toml(toml).expect("parsed");
assert_eq!(p.api_version, 2);
assert_eq!(p.repository.as_deref(), Some("example/opencad-my-plugin"));
assert!(p.command_prefixes.contains(&"MP_".to_string()));
assert!(p.api_compatible(), "API v2 plugins must run on API v3 host");
}
+17 -1
View File
@@ -148,6 +148,20 @@ pub fn fetch_releases(repo: &str) -> Result<Vec<Release>, String> {
Ok(out)
}
/// Fetch the repository README from its default branch as raw Markdown.
pub fn fetch_readme(repo: &str) -> Result<String, String> {
let url = format!("https://api.github.com/repos/{repo}/readme");
agent()
.get(&url)
.header("User-Agent", UA)
.header("Accept", "application/vnd.github.raw+json")
.call()
.map_err(|e| e.to_string())?
.body_mut()
.read_to_string()
.map_err(|e| e.to_string())
}
fn download_string(url: &str) -> Result<String, String> {
agent()
.get(url)
@@ -176,7 +190,7 @@ fn download_bytes(url: &str) -> Result<Vec<u8>, String> {
/// Download and install a release's package into the plugins folder. Verifies
/// the API version from the package's `plugin.toml` first. Returns the plugin
/// id on success.
pub fn install(release: &Release) -> Result<String, String> {
pub fn install(release: &Release, repository: &str) -> Result<String, String> {
let lib = release.lib_asset().ok_or("no library for this platform")?;
let toml = release.toml_asset().ok_or("release has no plugin.toml")?;
@@ -199,6 +213,8 @@ pub fn install(release: &Release) -> Result<String, String> {
let bytes = download_bytes(&lib.url)?;
replace_library(&dir, &lib.name, external_lib_ext(), &bytes)?;
std::fs::write(dir.join("plugin.toml"), toml_text).map_err(|e| e.to_string())?;
std::fs::write(dir.join(".source_repo"), format!("{repository}\n"))
.map_err(|e| e.to_string())?;
Ok(manifest.id)
}