aboutsummaryrefslogtreecommitdiff
path: root/src/app/api/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/api/plugins')
-rw-r--r--src/app/api/plugins/manifest.rs29
-rw-r--r--src/app/api/plugins/mod.rs96
-rw-r--r--src/app/api/plugins/runtime.rs35
3 files changed, 155 insertions, 5 deletions
diff --git a/src/app/api/plugins/manifest.rs b/src/app/api/plugins/manifest.rs
index 645748e0..f6464285 100644
--- a/src/app/api/plugins/manifest.rs
+++ b/src/app/api/plugins/manifest.rs
@@ -1,7 +1,7 @@
use crate::api::schema::{
InstalledPluginInfo, PluginManifestAction, PluginManifestBuild, PluginManifestEventHook,
- PluginManifestLinkHandler, PluginManifestPane, PluginPanePlacement, PluginPlatform,
- PluginSourceInfo, PluginSourceKind,
+ PluginManifestLinkHandler, PluginManifestPane, PluginManifestStartup, PluginPanePlacement,
+ PluginPlatform, PluginSourceInfo, PluginSourceKind,
};
use crate::popup_size::PopupSize;
@@ -22,6 +22,8 @@ struct RawPluginManifest {
#[serde(default)]
build: Vec<RawPluginManifestBuild>,
#[serde(default)]
+ startup: Vec<RawPluginManifestStartup>,
+ #[serde(default)]
actions: Vec<RawPluginManifestAction>,
#[serde(default)]
events: Vec<RawPluginManifestEventHook>,
@@ -39,6 +41,13 @@ struct RawPluginManifestBuild {
}
#[derive(serde::Deserialize)]
+struct RawPluginManifestStartup {
+ #[serde(default)]
+ platforms: Option<Vec<RawPlatform>>,
+ command: Vec<String>,
+}
+
+#[derive(serde::Deserialize)]
struct RawPluginManifestAction {
id: String,
title: String,
@@ -151,6 +160,11 @@ pub(crate) fn load_plugin_manifest(
.into_iter()
.map(normalize_manifest_build)
.collect::<Result<Vec<_>, _>>()?;
+ let startup = raw
+ .startup
+ .into_iter()
+ .map(normalize_manifest_startup)
+ .collect::<Result<Vec<_>, _>>()?;
let mut actions = raw
.actions
.into_iter()
@@ -202,6 +216,7 @@ pub(crate) fn load_plugin_manifest(
enabled,
platforms,
build,
+ startup,
actions,
events,
panes,
@@ -250,6 +265,14 @@ fn normalize_manifest_build(
Ok(PluginManifestBuild { platforms, command })
}
+fn normalize_manifest_startup(
+ startup: RawPluginManifestStartup,
+) -> Result<PluginManifestStartup, (&'static str, String)> {
+ let platforms = normalize_platforms(startup.platforms)?;
+ let command = normalize_command(startup.command)?;
+ Ok(PluginManifestStartup { platforms, command })
+}
+
pub(super) fn normalize_plugin_source(
plugin: &InstalledPluginInfo,
source: PluginSourceInfo,
@@ -563,7 +586,7 @@ fn non_empty_trimmed(
}
}
-pub(super) fn normalize_plugin_id(value: &str) -> Option<String> {
+pub(crate) fn normalize_plugin_id(value: &str) -> Option<String> {
normalize_identifier(value, PLUGIN_ID_MAX_CHARS)
}
diff --git a/src/app/api/plugins/mod.rs b/src/app/api/plugins/mod.rs
index 83a6065e..e48a1506 100644
--- a/src/app/api/plugins/mod.rs
+++ b/src/app/api/plugins/mod.rs
@@ -13,9 +13,9 @@ use crate::api::schema::{
ResponseResult,
};
use crate::app::App;
+pub(super) use manifest::normalize_plugin_id;
use manifest::{
- effective_platforms, ensure_platform_supported, normalize_action_id, normalize_plugin_id,
- normalize_plugin_source,
+ effective_platforms, ensure_platform_supported, normalize_action_id, normalize_plugin_source,
};
#[cfg(test)]
@@ -110,6 +110,7 @@ impl App {
}
return encode_error(id, "plugin_registry_save_failed", err.to_string());
}
+ self.clear_agent_view_for_source(&format!("plugin:{plugin_id}"));
}
encode_success(id, ResponseResult::PluginUnlinked { plugin_id, removed })
}
@@ -593,6 +594,9 @@ impl App {
let Some(plugin) = self.state.installed_plugins.get(&plugin_id).cloned() else {
return encode_error(id, "plugin_not_found", "plugin not found");
};
+ if !enabled {
+ self.clear_agent_view_for_source(&format!("plugin:{plugin_id}"));
+ }
if enabled {
encode_success(id, ResponseResult::PluginEnabled { plugin })
} else {
@@ -1212,6 +1216,38 @@ command = ["echo", "b"]
}
#[test]
+ fn startup_hook_manifest_loads() {
+ let root = unique_temp_path("plugin-startup-manifest");
+ write_manifest_content(
+ &root,
+ r#"
+id = "example.startup-manifest"
+name = "Startup Manifest"
+version = "0.1.0"
+min_herdr_version = "0.6.10"
+platforms = ["linux", "macos", "windows"]
+
+[[startup]]
+command = ["node", "restore.js"]
+platforms = ["linux", "macos"]
+"#,
+ );
+
+ let plugin = load_plugin_manifest(&root.display().to_string(), true).unwrap();
+
+ assert_eq!(plugin.startup.len(), 1);
+ assert_eq!(plugin.startup[0].command, ["node", "restore.js"]);
+ assert_eq!(
+ plugin.startup[0].platforms,
+ Some(vec![
+ crate::api::schema::PluginPlatform::Linux,
+ crate::api::schema::PluginPlatform::Macos,
+ ])
+ );
+ let _ = std::fs::remove_dir_all(root);
+ }
+
+ #[test]
fn smoke_fixture_manifest_loads() {
let plugin = load_plugin_manifest("tests/fixtures/plugin-smoke", true)
.expect("smoke fixture should load");
@@ -1235,6 +1271,12 @@ command = ["echo", "b"]
let root = unique_temp_path("plugin-enable-disable");
write_manifest(&root);
link_manifest(&mut app, &root);
+ app.state.agent_view_override = Some(crate::api::schema::AgentViewSetParams {
+ source: "plugin:example.worktree-bootstrap".into(),
+ label: None,
+ filter: None,
+ sort: Vec::new(),
+ });
let disabled = app.handle_api_request(Request {
id: "disable".into(),
@@ -1246,6 +1288,19 @@ command = ["echo", "b"]
panic!("expected disabled response: {disabled}");
};
assert!(!plugin.enabled);
+ assert!(app.state.agent_view_override.is_none());
+ let delayed_restore = app.handle_api_request(Request {
+ id: "delayed-startup-restore".into(),
+ method: Method::AgentViewSet(crate::api::schema::AgentViewSetParams {
+ source: "plugin:example.worktree-bootstrap".into(),
+ label: None,
+ filter: None,
+ sort: Vec::new(),
+ }),
+ });
+ let delayed_restore: crate::api::schema::ErrorResponse =
+ serde_json::from_str(&delayed_restore).unwrap();
+ assert_eq!(delayed_restore.error.code, "plugin_disabled");
let enabled = app.handle_api_request(Request {
id: "enable".into(),
@@ -2266,6 +2321,43 @@ command = ["sh", "-c", "printf '%s\n%s\n%s' \"$HERDR_PLUGIN_ROOT\" \"$HERDR_PLUG
#[cfg(unix)]
#[test]
+ fn startup_hooks_run_once_with_plugin_environment() {
+ let mut app = test_app();
+ let root = unique_temp_path("plugin-startup-hook");
+ let capture = root.join("startup.txt");
+ write_manifest_content(
+ &root,
+ &format!(
+ r#"
+id = "example.startup"
+name = "Startup"
+version = "0.1.0"
+min_herdr_version = "0.6.10"
+platforms = ["linux", "macos"]
+
+[[startup]]
+command = ["sh", "-c", "printf '%s:%s' \"$HERDR_PLUGIN_ID\" \"$HERDR_PLUGIN_EVENT\" > {}"]
+"#,
+ capture.display()
+ ),
+ );
+ link_manifest(&mut app, &root);
+
+ app.run_plugin_startup_hooks();
+
+ assert_eq!(
+ read_capture_when_ready(&capture, || {
+ app.drain_all_internal_events();
+ }),
+ "example.startup:startup"
+ );
+ let plugin = app.state.installed_plugins.get("example.startup").unwrap();
+ assert_eq!(plugin.startup.len(), 1);
+ let _ = std::fs::remove_dir_all(root);
+ }
+
+ #[cfg(unix)]
+ #[test]
fn event_hooks_use_event_target_context() {
let mut app = test_app();
app.state.workspaces = vec![
diff --git a/src/app/api/plugins/runtime.rs b/src/app/api/plugins/runtime.rs
index 0008def2..71004fe5 100644
--- a/src/app/api/plugins/runtime.rs
+++ b/src/app/api/plugins/runtime.rs
@@ -180,6 +180,41 @@ impl App {
Ok(log)
}
+ pub(crate) fn run_plugin_startup_hooks(&mut self) {
+ let mut context = self.current_plugin_context("plugin.startup");
+ context.invocation_source = Some("startup".to_string());
+ let mut plugins = self
+ .state
+ .installed_plugins
+ .values()
+ .filter(|plugin| {
+ plugin.enabled && plugin_manifest_available(plugin) && !plugin.startup.is_empty()
+ })
+ .cloned()
+ .collect::<Vec<_>>();
+ plugins.sort_by(|left, right| left.plugin_id.cmp(&right.plugin_id));
+ for plugin in plugins {
+ for startup in plugin.startup.clone() {
+ if ensure_platform_supported(
+ &effective_platforms(&startup.platforms, &plugin.platforms).clone(),
+ "startup",
+ )
+ .is_err()
+ {
+ continue;
+ }
+ let _ = self.start_plugin_command(
+ &plugin,
+ None,
+ Some("startup".to_string()),
+ startup.command,
+ &context,
+ None,
+ );
+ }
+ }
+ }
+
pub(crate) fn run_plugin_event_hooks(&mut self, event: &crate::api::schema::EventEnvelope) {
let event_name = event.event.dot_name();
if !crate::api::schema::PLUGIN_HOOK_EVENT_KINDS.contains(&event.event) {