From fbd20ad6e488fb3b9efc1612fdffee6e672976c1 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sun, 14 Jun 2026 02:43:45 +0300 Subject: feat: add plugin v1 system Add manifest-driven plugin actions, event hooks, pane entrypoints, link handlers, and host APIs. Support plugin link, GitHub install, build commands, installed plugin registry, pane movement, runtime context, and plugin docs. --- src/cli.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) (limited to 'src/cli.rs') diff --git a/src/cli.rs b/src/cli.rs index 276462b7..3facba8b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,20 +4,34 @@ use serde::Serialize; use crate::api::client::{ApiClient, ApiClientError}; use crate::api::schema::{ - AgentStatus, Method, OutputMatch, PaneAgentState, PaneWaitForOutputParams, ReadFormat, - ReadSource, Request, SplitDirection, Subscription, + AgentStatus, ClientWindowTitleSetParams, EmptyParams, Method, OutputMatch, PaneAgentState, + PaneWaitForOutputParams, ReadFormat, ReadSource, Request, SplitDirection, Subscription, }; mod agent; mod integration; mod notification; mod pane; +mod plugin; mod server; mod status; mod tab; mod workspace; mod worktree; +pub(crate) fn parse_env_assignment(raw: &str) -> Result<(String, String), String> { + let Some((key, value)) = raw.split_once('=') else { + return Err("env must use KEY=VALUE".into()); + }; + if key.is_empty() { + return Err("env key must not be empty".into()); + } + if key.contains('\0') || value.contains('\0') { + return Err("env must not contain NUL bytes".into()); + } + Ok((key.to_string(), value.to_string())) +} + pub enum CommandOutcome { Handled(i32), NotCli, @@ -45,6 +59,7 @@ pub fn maybe_run(args: &[String]) -> std::io::Result { "agent" => agent::run_agent_command(&args[2..])?, "terminal" => run_terminal_command(&args[2..])?, "pane" => pane::run_pane_command(&args[2..])?, + "plugin" => plugin::run_plugin_command(&args[2..])?, "wait" => run_wait_command(&args[2..])?, "integration" => integration::run_integration_command(&args[2..])?, "session" => run_session_command(&args[2..])?, @@ -305,6 +320,7 @@ fn run_terminal_command(args: &[String]) -> std::io::Result { match subcommand { "attach" => terminal_attach(&args[1..]), + "title" => terminal_title(&args[1..]), "help" | "--help" | "-h" => { print_terminal_help(); Ok(0) @@ -458,6 +474,43 @@ fn terminal_attach(args: &[String]) -> std::io::Result { Ok(0) } +fn terminal_title(args: &[String]) -> std::io::Result { + match args.first().map(|arg| arg.as_str()) { + Some("set") => { + if args.len() != 2 { + eprintln!("usage: herdr terminal title set "); + return Ok(2); + } + print_response(&send_request(&Request { + id: "cli:terminal:title:set".into(), + method: Method::ClientWindowTitleSet(ClientWindowTitleSetParams { + title: args[1].clone(), + }), + })?) + } + Some("clear") => { + if args.len() != 1 { + eprintln!("usage: herdr terminal title clear"); + return Ok(2); + } + print_response(&send_request(&Request { + id: "cli:terminal:title:clear".into(), + method: Method::ClientWindowTitleClear(EmptyParams::default()), + })?) + } + Some("help" | "--help" | "-h") => { + eprintln!("usage: herdr terminal title set <title>"); + eprintln!(" herdr terminal title clear"); + Ok(0) + } + _ => { + eprintln!("usage: herdr terminal title set <title>"); + eprintln!(" herdr terminal title clear"); + Ok(2) + } + } +} + pub(super) fn parse_attach_target(args: &[String], usage: &str) -> Result<(String, bool), i32> { let Some(target) = args.first() else { eprintln!("{usage}"); @@ -861,6 +914,8 @@ fn print_config_help() { fn print_terminal_help() { eprintln!("herdr terminal commands:"); eprintln!(" herdr terminal attach <terminal_id> [--takeover]"); + eprintln!(" herdr terminal title set <title>"); + eprintln!(" herdr terminal title clear"); eprintln!(" detach from direct attach with ctrl+b q; send literal ctrl+b with ctrl+b ctrl+b"); } @@ -948,4 +1003,20 @@ mod tests { super::ChannelSetInstallAction::RunSelfUpdate ); } + + #[test] + fn parse_env_assignment_accepts_empty_values() { + assert_eq!( + super::parse_env_assignment("HERDR_ROLE=").unwrap(), + ("HERDR_ROLE".to_string(), String::new()) + ); + } + + #[test] + fn parse_env_assignment_requires_key_value_separator() { + assert_eq!( + super::parse_env_assignment("HERDR_ROLE").unwrap_err(), + "env must use KEY=VALUE" + ); + } } -- cgit v1.2.3