|
|
@@ -1,4 +1,11 @@
|
|
|
-#![allow(dead_code, unused_imports, unused_variables, clippy::unneeded_struct_pattern, clippy::unnecessary_wraps, clippy::unused_self)]
|
|
|
+#![allow(
|
|
|
+ dead_code,
|
|
|
+ unused_imports,
|
|
|
+ unused_variables,
|
|
|
+ clippy::unneeded_struct_pattern,
|
|
|
+ clippy::unnecessary_wraps,
|
|
|
+ clippy::unused_self
|
|
|
+)]
|
|
|
mod init;
|
|
|
mod input;
|
|
|
mod render;
|
|
|
@@ -8,6 +15,7 @@ use std::env;
|
|
|
use std::fs;
|
|
|
use std::io::{self, Read, Write};
|
|
|
use std::net::TcpListener;
|
|
|
+use std::ops::{Deref, DerefMut};
|
|
|
use std::path::{Path, PathBuf};
|
|
|
use std::process::Command;
|
|
|
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
|
|
|
@@ -28,7 +36,7 @@ use commands::{
|
|
|
};
|
|
|
use compat_harness::{extract_manifest, UpstreamPaths};
|
|
|
use init::initialize_repo;
|
|
|
-use plugins::{PluginManager, PluginManagerConfig};
|
|
|
+use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
|
|
|
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
|
|
use runtime::{
|
|
|
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
|
|
|
@@ -1475,10 +1483,76 @@ struct LiveCli {
|
|
|
allowed_tools: Option<AllowedToolSet>,
|
|
|
permission_mode: PermissionMode,
|
|
|
system_prompt: Vec<String>,
|
|
|
- runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
|
|
+ runtime: BuiltRuntime,
|
|
|
session: SessionHandle,
|
|
|
}
|
|
|
|
|
|
+struct RuntimePluginState {
|
|
|
+ feature_config: runtime::RuntimeFeatureConfig,
|
|
|
+ tool_registry: GlobalToolRegistry,
|
|
|
+ plugin_registry: PluginRegistry,
|
|
|
+}
|
|
|
+
|
|
|
+struct BuiltRuntime {
|
|
|
+ runtime: Option<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>>,
|
|
|
+ plugin_registry: PluginRegistry,
|
|
|
+ plugins_active: bool,
|
|
|
+}
|
|
|
+
|
|
|
+impl BuiltRuntime {
|
|
|
+ fn new(
|
|
|
+ runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
|
|
+ plugin_registry: PluginRegistry,
|
|
|
+ ) -> Self {
|
|
|
+ Self {
|
|
|
+ runtime: Some(runtime),
|
|
|
+ plugin_registry,
|
|
|
+ plugins_active: true,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fn with_hook_abort_signal(mut self, hook_abort_signal: runtime::HookAbortSignal) -> Self {
|
|
|
+ let runtime = self
|
|
|
+ .runtime
|
|
|
+ .take()
|
|
|
+ .expect("runtime should exist before installing hook abort signal");
|
|
|
+ self.runtime = Some(runtime.with_hook_abort_signal(hook_abort_signal));
|
|
|
+ self
|
|
|
+ }
|
|
|
+
|
|
|
+ fn shutdown_plugins(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
+ if self.plugins_active {
|
|
|
+ self.plugin_registry.shutdown()?;
|
|
|
+ self.plugins_active = false;
|
|
|
+ }
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Deref for BuiltRuntime {
|
|
|
+ type Target = ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>;
|
|
|
+
|
|
|
+ fn deref(&self) -> &Self::Target {
|
|
|
+ self.runtime
|
|
|
+ .as_ref()
|
|
|
+ .expect("runtime should exist while built runtime is alive")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl DerefMut for BuiltRuntime {
|
|
|
+ fn deref_mut(&mut self) -> &mut Self::Target {
|
|
|
+ self.runtime
|
|
|
+ .as_mut()
|
|
|
+ .expect("runtime should exist while built runtime is alive")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Drop for BuiltRuntime {
|
|
|
+ fn drop(&mut self) {
|
|
|
+ let _ = self.shutdown_plugins();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
struct HookAbortMonitor {
|
|
|
stop_tx: Option<Sender<()>>,
|
|
|
join_handle: Option<JoinHandle<()>>,
|
|
|
@@ -1625,13 +1699,7 @@ impl LiveCli {
|
|
|
fn prepare_turn_runtime(
|
|
|
&self,
|
|
|
emit_output: bool,
|
|
|
- ) -> Result<
|
|
|
- (
|
|
|
- ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
|
|
- HookAbortMonitor,
|
|
|
- ),
|
|
|
- Box<dyn std::error::Error>,
|
|
|
- > {
|
|
|
+ ) -> Result<(BuiltRuntime, HookAbortMonitor), Box<dyn std::error::Error>> {
|
|
|
let hook_abort_signal = runtime::HookAbortSignal::new();
|
|
|
let runtime = build_runtime(
|
|
|
self.runtime.session().clone(),
|
|
|
@@ -1650,6 +1718,12 @@ impl LiveCli {
|
|
|
Ok((runtime, hook_abort_monitor))
|
|
|
}
|
|
|
|
|
|
+ fn replace_runtime(&mut self, runtime: BuiltRuntime) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
+ self.runtime.shutdown_plugins()?;
|
|
|
+ self.runtime = runtime;
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(true)?;
|
|
|
let mut spinner = Spinner::new();
|
|
|
@@ -1662,9 +1736,9 @@ impl LiveCli {
|
|
|
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
|
|
let result = runtime.run_turn(input, Some(&mut permission_prompter));
|
|
|
hook_abort_monitor.stop();
|
|
|
- self.runtime = runtime;
|
|
|
match result {
|
|
|
Ok(summary) => {
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
spinner.finish(
|
|
|
"✨ Done",
|
|
|
TerminalRenderer::new().color_theme(),
|
|
|
@@ -1681,6 +1755,7 @@ impl LiveCli {
|
|
|
Ok(())
|
|
|
}
|
|
|
Err(error) => {
|
|
|
+ runtime.shutdown_plugins()?;
|
|
|
spinner.fail(
|
|
|
"❌ Request failed",
|
|
|
TerminalRenderer::new().color_theme(),
|
|
|
@@ -1708,7 +1783,7 @@ impl LiveCli {
|
|
|
let result = runtime.run_turn(input, Some(&mut permission_prompter));
|
|
|
hook_abort_monitor.stop();
|
|
|
let summary = result?;
|
|
|
- self.runtime = runtime;
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
self.persist_session()?;
|
|
|
println!(
|
|
|
"{}",
|
|
|
@@ -1903,7 +1978,7 @@ impl LiveCli {
|
|
|
let previous = self.model.clone();
|
|
|
let session = self.runtime.session().clone();
|
|
|
let message_count = session.messages.len();
|
|
|
- self.runtime = build_runtime(
|
|
|
+ let runtime = build_runtime(
|
|
|
session,
|
|
|
&self.session.id,
|
|
|
model.clone(),
|
|
|
@@ -1914,6 +1989,7 @@ impl LiveCli {
|
|
|
self.permission_mode,
|
|
|
None,
|
|
|
)?;
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
self.model.clone_from(&model);
|
|
|
println!(
|
|
|
"{}",
|
|
|
@@ -1948,7 +2024,7 @@ impl LiveCli {
|
|
|
let previous = self.permission_mode.as_str().to_string();
|
|
|
let session = self.runtime.session().clone();
|
|
|
self.permission_mode = permission_mode_from_label(normalized);
|
|
|
- self.runtime = build_runtime(
|
|
|
+ let runtime = build_runtime(
|
|
|
session,
|
|
|
&self.session.id,
|
|
|
self.model.clone(),
|
|
|
@@ -1959,6 +2035,7 @@ impl LiveCli {
|
|
|
self.permission_mode,
|
|
|
None,
|
|
|
)?;
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
println!(
|
|
|
"{}",
|
|
|
format_permissions_switch_report(&previous, normalized)
|
|
|
@@ -1976,7 +2053,7 @@ impl LiveCli {
|
|
|
|
|
|
let session_state = Session::new();
|
|
|
self.session = create_managed_session_handle(&session_state.session_id)?;
|
|
|
- self.runtime = build_runtime(
|
|
|
+ let runtime = build_runtime(
|
|
|
session_state.with_persistence_path(self.session.path.clone()),
|
|
|
&self.session.id,
|
|
|
self.model.clone(),
|
|
|
@@ -1987,6 +2064,7 @@ impl LiveCli {
|
|
|
self.permission_mode,
|
|
|
None,
|
|
|
)?;
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
println!(
|
|
|
"Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}",
|
|
|
self.model,
|
|
|
@@ -2014,7 +2092,7 @@ impl LiveCli {
|
|
|
let session = Session::load_from_path(&handle.path)?;
|
|
|
let message_count = session.messages.len();
|
|
|
let session_id = session.session_id.clone();
|
|
|
- self.runtime = build_runtime(
|
|
|
+ let runtime = build_runtime(
|
|
|
session,
|
|
|
&handle.id,
|
|
|
self.model.clone(),
|
|
|
@@ -2025,6 +2103,7 @@ impl LiveCli {
|
|
|
self.permission_mode,
|
|
|
None,
|
|
|
)?;
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
self.session = SessionHandle {
|
|
|
id: session_id,
|
|
|
path: handle.path,
|
|
|
@@ -2104,7 +2183,7 @@ impl LiveCli {
|
|
|
let session = Session::load_from_path(&handle.path)?;
|
|
|
let message_count = session.messages.len();
|
|
|
let session_id = session.session_id.clone();
|
|
|
- self.runtime = build_runtime(
|
|
|
+ let runtime = build_runtime(
|
|
|
session,
|
|
|
&handle.id,
|
|
|
self.model.clone(),
|
|
|
@@ -2115,6 +2194,7 @@ impl LiveCli {
|
|
|
self.permission_mode,
|
|
|
None,
|
|
|
)?;
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
self.session = SessionHandle {
|
|
|
id: session_id,
|
|
|
path: handle.path,
|
|
|
@@ -2138,7 +2218,7 @@ impl LiveCli {
|
|
|
let forked = forked.with_persistence_path(handle.path.clone());
|
|
|
let message_count = forked.messages.len();
|
|
|
forked.save_to_path(&handle.path)?;
|
|
|
- self.runtime = build_runtime(
|
|
|
+ let runtime = build_runtime(
|
|
|
forked,
|
|
|
&handle.id,
|
|
|
self.model.clone(),
|
|
|
@@ -2149,6 +2229,7 @@ impl LiveCli {
|
|
|
self.permission_mode,
|
|
|
None,
|
|
|
)?;
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
self.session = handle;
|
|
|
println!(
|
|
|
"Session forked\n Parent session {}\n Active session {}\n Branch {}\n File {}\n Messages {}",
|
|
|
@@ -2187,7 +2268,7 @@ impl LiveCli {
|
|
|
}
|
|
|
|
|
|
fn reload_runtime_features(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
- self.runtime = build_runtime(
|
|
|
+ let runtime = build_runtime(
|
|
|
self.runtime.session().clone(),
|
|
|
&self.session.id,
|
|
|
self.model.clone(),
|
|
|
@@ -2198,6 +2279,7 @@ impl LiveCli {
|
|
|
self.permission_mode,
|
|
|
None,
|
|
|
)?;
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
self.persist_session()
|
|
|
}
|
|
|
|
|
|
@@ -2206,7 +2288,7 @@ impl LiveCli {
|
|
|
let removed = result.removed_message_count;
|
|
|
let kept = result.compacted_session.messages.len();
|
|
|
let skipped = removed == 0;
|
|
|
- self.runtime = build_runtime(
|
|
|
+ let runtime = build_runtime(
|
|
|
result.compacted_session,
|
|
|
&self.session.id,
|
|
|
self.model.clone(),
|
|
|
@@ -2217,6 +2299,7 @@ impl LiveCli {
|
|
|
self.permission_mode,
|
|
|
None,
|
|
|
)?;
|
|
|
+ self.replace_runtime(runtime)?;
|
|
|
self.persist_session()?;
|
|
|
println!("{}", format_compact_report(removed, kept, skipped));
|
|
|
Ok(())
|
|
|
@@ -2242,7 +2325,9 @@ impl LiveCli {
|
|
|
)?;
|
|
|
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
|
|
let summary = runtime.run_turn(prompt, Some(&mut permission_prompter))?;
|
|
|
- Ok(final_assistant_text(&summary).trim().to_string())
|
|
|
+ let text = final_assistant_text(&summary).trim().to_string();
|
|
|
+ runtime.shutdown_plugins()?;
|
|
|
+ Ok(text)
|
|
|
}
|
|
|
|
|
|
fn run_internal_prompt_text(
|
|
|
@@ -3270,14 +3355,32 @@ fn build_system_prompt() -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
|
|
)?)
|
|
|
}
|
|
|
|
|
|
-fn build_runtime_plugin_state(
|
|
|
-) -> Result<(runtime::RuntimeFeatureConfig, GlobalToolRegistry), Box<dyn std::error::Error>> {
|
|
|
+fn build_runtime_plugin_state() -> Result<RuntimePluginState, Box<dyn std::error::Error>> {
|
|
|
let cwd = env::current_dir()?;
|
|
|
let loader = ConfigLoader::default_for(&cwd);
|
|
|
let runtime_config = loader.load()?;
|
|
|
+ build_runtime_plugin_state_with_loader(&cwd, &loader, &runtime_config)
|
|
|
+}
|
|
|
+
|
|
|
+fn build_runtime_plugin_state_with_loader(
|
|
|
+ cwd: &Path,
|
|
|
+ loader: &ConfigLoader,
|
|
|
+ runtime_config: &runtime::RuntimeConfig,
|
|
|
+) -> Result<RuntimePluginState, Box<dyn std::error::Error>> {
|
|
|
let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
|
|
|
- let tool_registry = GlobalToolRegistry::with_plugin_tools(plugin_manager.aggregated_tools()?)?;
|
|
|
- Ok((runtime_config.feature_config().clone(), tool_registry))
|
|
|
+ let plugin_registry = plugin_manager.plugin_registry()?;
|
|
|
+ let plugin_hook_config =
|
|
|
+ runtime_hook_config_from_plugin_hooks(plugin_registry.aggregated_hooks()?);
|
|
|
+ let feature_config = runtime_config
|
|
|
+ .feature_config()
|
|
|
+ .clone()
|
|
|
+ .with_hooks(runtime_config.hooks().merged(&plugin_hook_config));
|
|
|
+ let tool_registry = GlobalToolRegistry::with_plugin_tools(plugin_registry.aggregated_tools()?)?;
|
|
|
+ Ok(RuntimePluginState {
|
|
|
+ feature_config,
|
|
|
+ tool_registry,
|
|
|
+ plugin_registry,
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
fn build_plugin_manager(
|
|
|
@@ -3316,6 +3419,14 @@ fn resolve_plugin_path(cwd: &Path, config_home: &Path, value: &str) -> PathBuf {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+fn runtime_hook_config_from_plugin_hooks(hooks: PluginHooks) -> runtime::RuntimeHookConfig {
|
|
|
+ runtime::RuntimeHookConfig::new(
|
|
|
+ hooks.pre_tool_use,
|
|
|
+ hooks.post_tool_use,
|
|
|
+ hooks.post_tool_use_failure,
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
struct InternalPromptProgressState {
|
|
|
command_label: &'static str,
|
|
|
@@ -3656,9 +3767,42 @@ fn build_runtime(
|
|
|
allowed_tools: Option<AllowedToolSet>,
|
|
|
permission_mode: PermissionMode,
|
|
|
progress_reporter: Option<InternalPromptProgressReporter>,
|
|
|
-) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
|
|
-{
|
|
|
- let (feature_config, tool_registry) = build_runtime_plugin_state()?;
|
|
|
+) -> Result<BuiltRuntime, Box<dyn std::error::Error>> {
|
|
|
+ let runtime_plugin_state = build_runtime_plugin_state()?;
|
|
|
+ build_runtime_with_plugin_state(
|
|
|
+ session,
|
|
|
+ session_id,
|
|
|
+ model,
|
|
|
+ system_prompt,
|
|
|
+ enable_tools,
|
|
|
+ emit_output,
|
|
|
+ allowed_tools,
|
|
|
+ permission_mode,
|
|
|
+ progress_reporter,
|
|
|
+ runtime_plugin_state,
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+#[allow(clippy::needless_pass_by_value)]
|
|
|
+#[allow(clippy::too_many_arguments)]
|
|
|
+fn build_runtime_with_plugin_state(
|
|
|
+ session: Session,
|
|
|
+ session_id: &str,
|
|
|
+ model: String,
|
|
|
+ system_prompt: Vec<String>,
|
|
|
+ enable_tools: bool,
|
|
|
+ emit_output: bool,
|
|
|
+ allowed_tools: Option<AllowedToolSet>,
|
|
|
+ permission_mode: PermissionMode,
|
|
|
+ progress_reporter: Option<InternalPromptProgressReporter>,
|
|
|
+ runtime_plugin_state: RuntimePluginState,
|
|
|
+) -> Result<BuiltRuntime, Box<dyn std::error::Error>> {
|
|
|
+ let RuntimePluginState {
|
|
|
+ feature_config,
|
|
|
+ tool_registry,
|
|
|
+ plugin_registry,
|
|
|
+ } = runtime_plugin_state;
|
|
|
+ plugin_registry.initialize()?;
|
|
|
let mut runtime = ConversationRuntime::new_with_features(
|
|
|
session,
|
|
|
AnthropicRuntimeClient::new(
|
|
|
@@ -3679,7 +3823,7 @@ fn build_runtime(
|
|
|
if emit_output {
|
|
|
runtime = runtime.with_hook_progress_reporter(Box::new(CliHookProgressReporter));
|
|
|
}
|
|
|
- Ok(runtime)
|
|
|
+ Ok(BuiltRuntime::new(runtime, plugin_registry))
|
|
|
}
|
|
|
|
|
|
struct CliHookProgressReporter;
|
|
|
@@ -4847,6 +4991,7 @@ fn print_help() {
|
|
|
#[cfg(test)]
|
|
|
mod tests {
|
|
|
use super::{
|
|
|
+ build_runtime_plugin_state_with_loader, build_runtime_with_plugin_state,
|
|
|
create_managed_session_handle, describe_tool_progress, filter_tool_specs,
|
|
|
format_bughunter_report, format_commit_preflight_report, format_commit_skipped_report,
|
|
|
format_compact_report, format_cost_report, format_internal_prompt_progress_line,
|
|
|
@@ -4865,9 +5010,12 @@ mod tests {
|
|
|
InternalPromptProgressState, LiveCli, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
|
|
};
|
|
|
use api::{MessageResponse, OutputContentBlock, Usage};
|
|
|
- use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
|
|
|
+ use plugins::{
|
|
|
+ PluginManager, PluginManagerConfig, PluginTool, PluginToolDefinition, PluginToolPermission,
|
|
|
+ };
|
|
|
use runtime::{
|
|
|
- AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode, Session,
|
|
|
+ AssistantEvent, ConfigLoader, ContentBlock, ConversationMessage, MessageRole,
|
|
|
+ PermissionMode, Session,
|
|
|
};
|
|
|
use serde_json::json;
|
|
|
use std::fs;
|
|
|
@@ -4936,6 +5084,49 @@ mod tests {
|
|
|
std::env::set_current_dir(previous).expect("cwd should restore");
|
|
|
result
|
|
|
}
|
|
|
+
|
|
|
+ fn write_plugin_fixture(root: &Path, name: &str, include_hooks: bool, include_lifecycle: bool) {
|
|
|
+ fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
|
|
|
+ if include_hooks {
|
|
|
+ fs::create_dir_all(root.join("hooks")).expect("hooks dir");
|
|
|
+ fs::write(
|
|
|
+ root.join("hooks").join("pre.sh"),
|
|
|
+ "#!/bin/sh\nprintf 'plugin pre hook'\n",
|
|
|
+ )
|
|
|
+ .expect("write hook");
|
|
|
+ }
|
|
|
+ if include_lifecycle {
|
|
|
+ fs::create_dir_all(root.join("lifecycle")).expect("lifecycle dir");
|
|
|
+ fs::write(
|
|
|
+ root.join("lifecycle").join("init.sh"),
|
|
|
+ "#!/bin/sh\nprintf 'init\\n' >> lifecycle.log\n",
|
|
|
+ )
|
|
|
+ .expect("write init lifecycle");
|
|
|
+ fs::write(
|
|
|
+ root.join("lifecycle").join("shutdown.sh"),
|
|
|
+ "#!/bin/sh\nprintf 'shutdown\\n' >> lifecycle.log\n",
|
|
|
+ )
|
|
|
+ .expect("write shutdown lifecycle");
|
|
|
+ }
|
|
|
+
|
|
|
+ let hooks = if include_hooks {
|
|
|
+ ",\n \"hooks\": {\n \"PreToolUse\": [\"./hooks/pre.sh\"]\n }"
|
|
|
+ } else {
|
|
|
+ ""
|
|
|
+ };
|
|
|
+ let lifecycle = if include_lifecycle {
|
|
|
+ ",\n \"lifecycle\": {\n \"Init\": [\"./lifecycle/init.sh\"],\n \"Shutdown\": [\"./lifecycle/shutdown.sh\"]\n }"
|
|
|
+ } else {
|
|
|
+ ""
|
|
|
+ };
|
|
|
+ fs::write(
|
|
|
+ root.join(".claude-plugin").join("plugin.json"),
|
|
|
+ format!(
|
|
|
+ "{{\n \"name\": \"{name}\",\n \"version\": \"1.0.0\",\n \"description\": \"runtime plugin fixture\"{hooks}{lifecycle}\n}}"
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .expect("write plugin manifest");
|
|
|
+ }
|
|
|
#[test]
|
|
|
fn defaults_to_repl_when_no_args() {
|
|
|
assert_eq!(
|
|
|
@@ -6384,6 +6575,89 @@ UU conflicted.rs",
|
|
|
));
|
|
|
assert!(!String::from_utf8(out).expect("utf8").contains("step 1"));
|
|
|
}
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn build_runtime_plugin_state_merges_plugin_hooks_into_runtime_features() {
|
|
|
+ let config_home = temp_dir();
|
|
|
+ let workspace = temp_dir();
|
|
|
+ let source_root = temp_dir();
|
|
|
+ fs::create_dir_all(&config_home).expect("config home");
|
|
|
+ fs::create_dir_all(&workspace).expect("workspace");
|
|
|
+ fs::create_dir_all(&source_root).expect("source root");
|
|
|
+ write_plugin_fixture(&source_root, "hook-runtime-demo", true, false);
|
|
|
+
|
|
|
+ let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
|
|
|
+ manager
|
|
|
+ .install(source_root.to_str().expect("utf8 source path"))
|
|
|
+ .expect("plugin install should succeed");
|
|
|
+ let loader = ConfigLoader::new(&workspace, &config_home);
|
|
|
+ let runtime_config = loader.load().expect("runtime config should load");
|
|
|
+ let state = build_runtime_plugin_state_with_loader(&workspace, &loader, &runtime_config)
|
|
|
+ .expect("plugin state should load");
|
|
|
+ let pre_hooks = state.feature_config.hooks().pre_tool_use();
|
|
|
+ assert_eq!(pre_hooks.len(), 1);
|
|
|
+ assert!(
|
|
|
+ pre_hooks[0].ends_with("hooks/pre.sh"),
|
|
|
+ "expected installed plugin hook path, got {pre_hooks:?}"
|
|
|
+ );
|
|
|
+
|
|
|
+ let _ = fs::remove_dir_all(config_home);
|
|
|
+ let _ = fs::remove_dir_all(workspace);
|
|
|
+ let _ = fs::remove_dir_all(source_root);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn build_runtime_runs_plugin_lifecycle_init_and_shutdown() {
|
|
|
+ let config_home = temp_dir();
|
|
|
+ let workspace = temp_dir();
|
|
|
+ let source_root = temp_dir();
|
|
|
+ fs::create_dir_all(&config_home).expect("config home");
|
|
|
+ fs::create_dir_all(&workspace).expect("workspace");
|
|
|
+ fs::create_dir_all(&source_root).expect("source root");
|
|
|
+ write_plugin_fixture(&source_root, "lifecycle-runtime-demo", false, true);
|
|
|
+
|
|
|
+ let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
|
|
|
+ let install = manager
|
|
|
+ .install(source_root.to_str().expect("utf8 source path"))
|
|
|
+ .expect("plugin install should succeed");
|
|
|
+ let log_path = install.install_path.join("lifecycle.log");
|
|
|
+ let loader = ConfigLoader::new(&workspace, &config_home);
|
|
|
+ let runtime_config = loader.load().expect("runtime config should load");
|
|
|
+ let runtime_plugin_state =
|
|
|
+ build_runtime_plugin_state_with_loader(&workspace, &loader, &runtime_config)
|
|
|
+ .expect("plugin state should load");
|
|
|
+ let mut runtime = build_runtime_with_plugin_state(
|
|
|
+ Session::new(),
|
|
|
+ "runtime-plugin-lifecycle",
|
|
|
+ DEFAULT_MODEL.to_string(),
|
|
|
+ vec!["test system prompt".to_string()],
|
|
|
+ true,
|
|
|
+ false,
|
|
|
+ None,
|
|
|
+ PermissionMode::DangerFullAccess,
|
|
|
+ None,
|
|
|
+ runtime_plugin_state,
|
|
|
+ )
|
|
|
+ .expect("runtime should build");
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ fs::read_to_string(&log_path).expect("init log should exist"),
|
|
|
+ "init\n"
|
|
|
+ );
|
|
|
+
|
|
|
+ runtime
|
|
|
+ .shutdown_plugins()
|
|
|
+ .expect("plugin shutdown should succeed");
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ fs::read_to_string(&log_path).expect("shutdown log should exist"),
|
|
|
+ "init\nshutdown\n"
|
|
|
+ );
|
|
|
+
|
|
|
+ let _ = fs::remove_dir_all(config_home);
|
|
|
+ let _ = fs::remove_dir_all(workspace);
|
|
|
+ let _ = fs::remove_dir_all(source_root);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
#[cfg(test)]
|