feat: plugin system with manifest-based discovery

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 00:15:00 +05:30
co-authored by Claude Opus 4.7
parent e5eb1e151a
commit 337a6d67a0
6 changed files with 151 additions and 0 deletions
@@ -0,0 +1,7 @@
{
"name": "writing-studio",
"version": "1.0.0",
"description": "Goal tracking, manuscript panels, snapshots, and sprint engine for writers",
"author": "ConcreteInfo",
"builtin": true
}
+1
View File
@@ -4,3 +4,4 @@ pub mod git;
pub mod app;
pub mod pdf;
pub mod dialog;
pub mod plugins;
+31
View File
@@ -0,0 +1,31 @@
use crate::error::AppError;
use crate::plugin_manager::PluginMetadata;
#[tauri::command]
pub fn list_plugins() -> Vec<PluginMetadata> {
// This would be connected to the app state in a full implementation
// For now, return the built-in plugins
vec![
PluginMetadata {
name: "writing-studio".to_string(),
version: "1.0.0".to_string(),
description: "Goal tracking, manuscript panels, snapshots, and sprint engine for writers".to_string(),
author: "ConcreteInfo".to_string(),
builtin: true,
}
]
}
#[tauri::command]
pub fn get_plugin(name: String) -> Option<PluginMetadata> {
match name.as_str() {
"writing-studio" => Some(PluginMetadata {
name: "writing-studio".to_string(),
version: "1.0.0".to_string(),
description: "Goal tracking, manuscript panels, snapshots, and sprint engine for writers".to_string(),
author: "ConcreteInfo".to_string(),
builtin: true,
}),
_ => None,
}
}
+3
View File
@@ -1,6 +1,7 @@
pub mod menu;
pub mod tray;
pub mod pdf_ops;
pub mod plugin_manager;
pub fn run() {
tauri::Builder::default()
@@ -42,6 +43,8 @@ pub fn run() {
crate::commands::dialog::open_file_dialog,
crate::commands::dialog::save_file_dialog,
crate::commands::dialog::select_folder_dialog,
crate::commands::plugins::list_plugins,
crate::commands::plugins::get_plugin,
])
.setup(|app| {
log::info!("MarkdownConverter starting up");
+101
View File
@@ -0,0 +1,101 @@
use std::collections::HashMap;
use std::path::PathBuf;
use crate::error::AppError;
#[derive(Debug, Clone)]
pub struct PluginMetadata {
pub name: String,
pub version: String,
pub description: String,
pub author: String,
pub builtin: bool,
}
pub struct PluginManager {
plugins: HashMap<String, PluginMetadata>,
enabled: Vec<String>,
}
impl PluginManager {
pub fn new() -> Self {
PluginManager {
plugins: HashMap::new(),
enabled: Vec::new(),
}
}
pub fn load_plugins(&mut self, plugin_dir: PathBuf) -> Result<(), AppError> {
if !plugin_dir.exists() {
return Ok(());
}
for entry in walkdir::WalkDir::new(&plugin_dir)
.max_depth(1)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if !path.is_dir() {
continue;
}
let manifest_path = path.join("manifest.json");
if manifest_path.exists() {
let content = std::fs::read_to_string(&manifest_path)
.map_err(|e| AppError::Config(format!("Failed to read manifest: {}", e)))?;
#[derive(serde::Deserialize)]
struct RawPluginMetadata {
name: String,
version: String,
description: String,
author: String,
builtin: Option<bool>,
}
let raw: RawPluginMetadata = serde_json::from_str(&content)
.map_err(|e| AppError::Config(format!("Failed to parse manifest: {}", e)))?;
let metadata = PluginMetadata {
name: raw.name,
version: raw.version,
description: raw.description,
author: raw.author,
builtin: raw.builtin.unwrap_or(false),
};
self.plugins.insert(metadata.name.clone(), metadata);
}
}
Ok(())
}
pub fn enable_plugin(&mut self, name: &str) {
if !self.enabled.contains(&name.to_string()) {
self.enabled.push(name.to_string());
}
}
pub fn disable_plugin(&mut self, name: &str) {
self.enabled.retain(|n| n != name);
}
pub fn get_enabled(&self) -> Vec<String> {
self.enabled.clone()
}
pub fn list_plugins(&self) -> Vec<PluginMetadata> {
self.plugins.values().cloned().collect()
}
pub fn get_plugin(&self, name: &str) -> Option<PluginMetadata> {
self.plugins.get(name).cloned()
}
}
impl Default for PluginManager {
fn default() -> Self {
Self::new()
}
}
+8
View File
@@ -86,3 +86,11 @@ export const events = {
onConversionComplete: (callback) => listen('conversion-complete', callback),
onConversionStatus: (callback) => listen('conversion-status', callback),
};
// ============================================
// PLUGIN OPERATIONS
// ============================================
export const plugins = {
list: () => invoke('list_plugins', {}),
get: (name) => invoke('get_plugin', { name }),
};