diff --git a/crates/owlry-core/src/plugins/manifest.rs b/crates/owlry-core/src/plugins/manifest.rs index bcf7b96..ef47e6d 100644 --- a/crates/owlry-core/src/plugins/manifest.rs +++ b/crates/owlry-core/src/plugins/manifest.rs @@ -341,6 +341,70 @@ api_url = "https://api.example.com" assert_eq!(manifest.permissions.run_commands, vec!["myapp"]); } + #[test] + fn test_parse_new_format_with_providers_array() { + let toml_str = r#" +[plugin] +id = "my-plugin" +name = "My Plugin" +version = "0.1.0" +description = "Test" +entry_point = "main.rn" + +[[providers]] +id = "my-plugin" +name = "My Plugin" +type = "static" +type_id = "myplugin" +icon = "system-run" +prefix = ":mp" +"#; + let manifest: PluginManifest = toml::from_str(toml_str).unwrap(); + assert_eq!(manifest.plugin.entry, "main.rn"); + assert_eq!(manifest.providers.len(), 1); + let p = &manifest.providers[0]; + assert_eq!(p.id, "my-plugin"); + assert_eq!(p.name, "My Plugin"); + assert_eq!(p.provider_type, "static"); + assert_eq!(p.type_id.as_deref(), Some("myplugin")); + assert_eq!(p.icon.as_deref(), Some("system-run")); + assert_eq!(p.prefix.as_deref(), Some(":mp")); + } + + #[test] + fn test_parse_new_format_entry_point_alias() { + let toml_str = r#" +[plugin] +id = "test" +name = "Test" +version = "1.0.0" +entry_point = "main.lua" +"#; + let manifest: PluginManifest = toml::from_str(toml_str).unwrap(); + assert_eq!(manifest.plugin.entry, "main.lua"); + } + + #[test] + fn test_provider_spec_defaults() { + let toml_str = r#" +[plugin] +id = "test" +name = "Test" +version = "1.0.0" + +[[providers]] +id = "test" +name = "Test" +"#; + let manifest: PluginManifest = toml::from_str(toml_str).unwrap(); + assert_eq!(manifest.providers.len(), 1); + let p = &manifest.providers[0]; + assert_eq!(p.provider_type, "static"); // default + assert!(p.prefix.is_none()); + assert!(p.icon.is_none()); + assert!(p.type_id.is_none()); + } + #[test] fn test_version_compatibility() { let toml_str = r#" diff --git a/crates/owlry-lua/Cargo.toml b/crates/owlry-lua/Cargo.toml index 4a2559e..868924b 100644 --- a/crates/owlry-lua/Cargo.toml +++ b/crates/owlry-lua/Cargo.toml @@ -30,6 +30,9 @@ serde_json = "1.0" # Version compatibility semver = "1" +# Logging +log = "0.4" + # HTTP client for plugins reqwest = { version = "0.13", default-features = false, features = ["native-tls", "blocking", "json"] } diff --git a/crates/owlry-lua/src/loader.rs b/crates/owlry-lua/src/loader.rs index edee503..51578d7 100644 --- a/crates/owlry-lua/src/loader.rs +++ b/crates/owlry-lua/src/loader.rs @@ -188,11 +188,16 @@ pub fn discover_plugins( Ok(manifest) => { // Skip plugins whose entry point is not a Lua file if !manifest.plugin.entry.ends_with(".lua") { + log::debug!( + "owlry-lua: Skipping non-Lua plugin at {} (entry: {})", + path.display(), + manifest.plugin.entry + ); continue; } let id = manifest.plugin.id.clone(); if plugins.contains_key(&id) { - eprintln!( + log::warn!( "owlry-lua: Duplicate plugin ID '{}', skipping {}", id, path.display() @@ -202,7 +207,7 @@ pub fn discover_plugins( plugins.insert(id, (manifest, path)); } Err(e) => { - eprintln!( + log::warn!( "owlry-lua: Failed to load plugin at {}: {}", path.display(), e @@ -263,4 +268,79 @@ version = "1.0.0" let plugins = discover_plugins(Path::new("/nonexistent/path")).unwrap(); assert!(plugins.is_empty()); } + + #[test] + fn test_discover_skips_non_lua_plugins() { + let temp = TempDir::new().unwrap(); + let plugins_dir = temp.path(); + + // Rune plugin — should be skipped by the Lua runtime + let rune_dir = plugins_dir.join("rune-plugin"); + fs::create_dir_all(&rune_dir).unwrap(); + fs::write( + rune_dir.join("plugin.toml"), + r#" +[plugin] +id = "rune-plugin" +name = "Rune Plugin" +version = "1.0.0" +entry_point = "main.rn" + +[[providers]] +id = "rune-plugin" +name = "Rune Plugin" +"#, + ) + .unwrap(); + fs::write(rune_dir.join("main.rn"), "pub fn refresh() { [] }").unwrap(); + + // Lua plugin — should be discovered + create_test_plugin(plugins_dir, "lua-plugin"); + + let plugins = discover_plugins(plugins_dir).unwrap(); + assert_eq!(plugins.len(), 1); + assert!(plugins.contains_key("lua-plugin")); + assert!(!plugins.contains_key("rune-plugin")); + } + + #[test] + fn test_manifest_provider_fallback() { + let temp = TempDir::new().unwrap(); + let plugin_dir = temp.path().join("test-plugin"); + fs::create_dir_all(&plugin_dir).unwrap(); + + fs::write( + plugin_dir.join("plugin.toml"), + r#" +[plugin] +id = "test-plugin" +name = "Test Plugin" +version = "1.0.0" +entry_point = "main.lua" + +[[providers]] +id = "test-plugin" +name = "Test Plugin" +type = "static" +type_id = "testplugin" +icon = "system-run" +prefix = ":tp" +"#, + ) + .unwrap(); + // Script that does NOT call owlry.provider.register() + fs::write(plugin_dir.join("main.lua"), "function refresh() return {} end").unwrap(); + + let manifest = + crate::manifest::PluginManifest::load(&plugin_dir.join("plugin.toml")).unwrap(); + let mut plugin = LoadedPlugin::new(manifest, plugin_dir); + plugin.initialize().unwrap(); + + let regs = plugin.get_provider_registrations().unwrap(); + assert_eq!(regs.len(), 1, "should fall back to [[providers]] declaration"); + assert_eq!(regs[0].name, "test-plugin"); + assert_eq!(regs[0].type_id, "testplugin"); + assert_eq!(regs[0].prefix.as_deref(), Some(":tp")); + assert!(!regs[0].is_dynamic); + } } diff --git a/crates/owlry-lua/src/manifest.rs b/crates/owlry-lua/src/manifest.rs index adf23ff..5fdcc3f 100644 --- a/crates/owlry-lua/src/manifest.rs +++ b/crates/owlry-lua/src/manifest.rs @@ -151,6 +151,11 @@ impl PluginManifest { )); } + // Lua plugins must have a .lua entry point + if !self.plugin.entry.ends_with(".lua") { + return Err("Entry point must be a .lua file for Lua plugins".to_string()); + } + Ok(()) } diff --git a/crates/owlry-rune/src/loader.rs b/crates/owlry-rune/src/loader.rs index b809e09..e660f09 100644 --- a/crates/owlry-rune/src/loader.rs +++ b/crates/owlry-rune/src/loader.rs @@ -203,6 +203,7 @@ pub fn discover_rune_plugins(plugins_dir: &Path) -> Result CommandResult { "runtime": runtime.to_string(), "runtime_available": runtime_available, "path": plugin_path.display().to_string(), + "providers": manifest.providers.iter().map(|p| serde_json::json!({ + "id": p.id, + "name": p.name, + "type": p.provider_type, + "type_id": p.type_id, + "prefix": p.prefix, + "icon": p.icon, + })).collect::>(), "provides": { "providers": manifest.provides.providers, "actions": manifest.provides.actions, @@ -406,9 +414,13 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult { ); println!("Path: {}", plugin_path.display()); println!(); - println!("Provides:"); + println!("Providers:"); + for p in &manifest.providers { + let prefix = p.prefix.as_deref().map(|s| format!(" ({})", s)).unwrap_or_default(); + println!(" {} [{}]{}", p.name, p.provider_type, prefix); + } if !manifest.provides.providers.is_empty() { - println!(" Providers: {}", manifest.provides.providers.join(", ")); + println!(" {}", manifest.provides.providers.join(", ")); } if manifest.provides.actions { println!(" Actions: yes"); @@ -976,11 +988,11 @@ fn cmd_runtimes() -> CommandResult { if lua_available { println!(" ✓ Lua - Installed"); println!(" Package: owlry-lua"); - println!(" Entry point: init.lua"); + println!(" Entry point: main.lua"); } else { println!(" ✗ Lua - Not installed"); println!(" Install: yay -S owlry-lua"); - println!(" Entry point: init.lua"); + println!(" Entry point: main.lua"); } println!(); @@ -989,11 +1001,11 @@ fn cmd_runtimes() -> CommandResult { if rune_available { println!(" ✓ Rune - Installed"); println!(" Package: owlry-rune"); - println!(" Entry point: init.rn"); + println!(" Entry point: main.rn"); } else { println!(" ✗ Rune - Not installed"); println!(" Install: yay -S owlry-rune"); - println!(" Entry point: init.rn"); + println!(" Entry point: main.rn"); } println!();