diff --git a/dev/pi-plugin.ts b/dev/pi-plugin.ts new file mode 100644 index 000000000..001a15e61 --- /dev/null +++ b/dev/pi-plugin.ts @@ -0,0 +1,36 @@ +// Worktrunk activity tracking hook for Pi / oh-my-pi. +// +// Tracks agent activity per branch, showing status markers in `wt list`: +// 🤖 — agent is working +// 💬 — agent is waiting for input +// +// Installed globally via: wt config plugins pi install + +import type { HookAPI } from "@oh-my-pi/pi-coding-agent/extensibility/hooks"; + +export default function worktrunkActivity(pi: HookAPI): void { + const run = async ( + ctx: { cwd: string }, + args: ["set", string] | ["clear"], + ): Promise => { + try { + await pi.exec("wt", ["config", "state", "marker", ...args], { + cwd: ctx.cwd, + }); + } catch { + // Activity tracking must never interrupt the host Pi session. + } + }; + + pi.on("agent_start", async (_event, ctx) => { + await run(ctx, ["set", "🤖"]); + }); + + pi.on("agent_end", async (_event, ctx) => { + await run(ctx, ["set", "💬"]); + }); + + pi.on("session_shutdown", async (_event, ctx) => { + await run(ctx, ["clear"]); + }); +} diff --git a/docs/public/llms.txt b/docs/public/llms.txt index db7c3bb4c..2754b4aaf 100644 --- a/docs/public/llms.txt +++ b/docs/public/llms.txt @@ -23,7 +23,7 @@ with many parallel changes, including hooks to automate local workflows. - [Extending Worktrunk](https://worktrunk.dev/extending.md): Three ways to add custom behavior: hooks for lifecycle automation, aliases for reusable commands, and custom subcommands for standalone tools. - [LLM Commit Messages](https://worktrunk.dev/llm-commits.md): Generate commit messages from diffs using any LLM. Integrates with wt merge, wt step commit, and wt step squash. -- [Agent Integration](https://worktrunk.dev/claude-code.md): Worktrunk plugins for Claude Code, Codex, OpenCode, and Gemini CLI: a configuration skill, wt list activity tracking, and Claude-only worktree isolation. +- [Agent Integration](https://worktrunk.dev/claude-code.md): Worktrunk plugins for Claude Code, Codex, OpenCode, Pi, and Gemini CLI: a configuration skill, wt list activity tracking, and Claude-only worktree isolation. - [Tips & Patterns](https://worktrunk.dev/tips-patterns.md): Practical recipes for Worktrunk workflows: aliases, shell integration, Zellij layouts, and parallel agent patterns. - [FAQ](https://worktrunk.dev/faq.md): Common questions about Worktrunk: comparison to git worktree and branch switching, bare repos, TUI support, and more. - [Code Signing Policy](https://worktrunk.dev/code-signing.md): How Worktrunk's Windows release binaries are code-signed: certificate provenance, the build and signing pipeline, project roles, and per-release approval. diff --git a/docs/src/content/docs/claude-code.md b/docs/src/content/docs/claude-code.md index 9e7930a6f..8d0f338db 100644 --- a/docs/src/content/docs/claude-code.md +++ b/docs/src/content/docs/claude-code.md @@ -1,19 +1,19 @@ --- title: "Agent Integration" -description: "Worktrunk plugins for Claude Code, Codex, OpenCode, and Gemini CLI: a configuration skill, wt list activity tracking, and Claude-only worktree isolation." +description: "Worktrunk plugins for Claude Code, Codex, OpenCode, Pi, and Gemini CLI: a configuration skill, wt list activity tracking, and Claude-only worktree isolation." sidebar: order: 23 --- Worktrunk ships a plugin for each supported agent CLI. What a plugin provides depends on the hooks that CLI exposes: -| Capability | Claude Code | Codex | OpenCode | Gemini CLI | -|---|:-:|:-:|:-:|:-:| -| Configuration skill | ✓ | ✓ | | ✓ | -| Activity tracking (🤖/💬 in `wt list`) | ✓ | ✓ | ✓ | ✓ | -| Worktree isolation | ✓ | | | | -| `/wt-switch-create` command | ✓ | | | | +| Capability | Claude Code | Codex | OpenCode | Pi | Gemini CLI | +|---|:-:|:-:|:-:|:-:|:-:| +| Configuration skill | ✓ | ✓ | | | ✓ | +| Activity tracking (🤖/💬 in `wt list`) | ✓ | ✓ | ✓ | ✓ | ✓ | +| Worktree isolation | ✓ | | | | | +| `/wt-switch-create` command | ✓ | | | | | -The configuration skill is documentation the agent reads to help set up LLM commits, hooks, and troubleshooting. Activity tracking shows which worktrees have running sessions. Worktree isolation needs worktree-lifecycle hooks and `/wt-switch-create` needs session working-directory switching — both Claude Code-only, so Codex, OpenCode, and Gemini users invoke `wt switch --create` and `wt remove` directly. Codex tracks activity through its own `Stop` and `SessionEnd` hooks. +The configuration skill is documentation the agent reads to help set up LLM commits, hooks, and troubleshooting. Activity tracking shows which worktrees have running sessions. Worktree isolation needs worktree-lifecycle hooks and `/wt-switch-create` needs session working-directory switching — both Claude Code-only, so Codex, OpenCode, Pi, and Gemini users invoke `wt switch --create` and `wt remove` directly. Codex tracks activity through its own `Stop` and `SessionEnd` hooks. ## Installation @@ -52,6 +52,14 @@ wt config plugins opencode install This writes the activity-tracking plugin to OpenCode's global plugins directory, `~/.config/opencode/plugins/worktrunk.ts` (honoring `$OPENCODE_CONFIG_DIR` and `$XDG_CONFIG_HOME`). `wt config plugins opencode uninstall` removes it. +### Pi + +```bash +wt config plugins pi install +``` + +This writes the activity hook to `~/.omp/agent/hooks/pre/worktrunk.ts`. Named `$OMP_PROFILE` or `$PI_PROFILE` profiles use `~/.omp/profiles//agent`, `$PI_CONFIG_DIR` changes the `.omp` config root, and `$PI_CODING_AGENT_DIR` overrides the agent directory for the default profile. `wt config plugins pi uninstall` removes the hook. + ### Gemini CLI ```bash @@ -73,7 +81,7 @@ Claude Code is designed to load the skill automatically when it detects worktrun ## Activity tracking -The Claude Code, Codex, OpenCode, and Gemini plugins track agent sessions with status markers in `wt list`: +The Claude Code, Codex, OpenCode, Pi, and Gemini plugins track agent sessions with status markers in `wt list`: diff --git a/docs/tests/built-site.test.mjs b/docs/tests/built-site.test.mjs index 3ad80d2cb..9ec80cb46 100644 --- a/docs/tests/built-site.test.mjs +++ b/docs/tests/built-site.test.mjs @@ -475,7 +475,7 @@ test('short wide tables become labeled records without capturing dense tables', /\bwt-responsive-records\b/, ); assert.doesNotMatch( - findTable('/claude-code/', ['Capability', 'Claude Code', 'Codex', 'OpenCode', 'Gemini CLI']).attributes, + findTable('/claude-code/', ['Capability', 'Claude Code', 'Codex', 'OpenCode', 'Pi', 'Gemini CLI']).attributes, /\bwt-responsive-records\b/, ); assert.doesNotMatch( diff --git a/plugins/worktrunk/skills/worktrunk/reference/claude-code.md b/plugins/worktrunk/skills/worktrunk/reference/claude-code.md index c9936e368..a05627a42 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/claude-code.md +++ b/plugins/worktrunk/skills/worktrunk/reference/claude-code.md @@ -2,14 +2,14 @@ Worktrunk ships a plugin for each supported agent CLI. What a plugin provides depends on the hooks that CLI exposes: -| Capability | Claude Code | Codex | OpenCode | Gemini CLI | -|---|:-:|:-:|:-:|:-:| -| Configuration skill | ✓ | ✓ | | ✓ | -| Activity tracking (🤖/💬 in `wt list`) | ✓ | ✓ | ✓ | ✓ | -| Worktree isolation | ✓ | | | | -| `/wt-switch-create` command | ✓ | | | | +| Capability | Claude Code | Codex | OpenCode | Pi | Gemini CLI | +|---|:-:|:-:|:-:|:-:|:-:| +| Configuration skill | ✓ | ✓ | | | ✓ | +| Activity tracking (🤖/💬 in `wt list`) | ✓ | ✓ | ✓ | ✓ | ✓ | +| Worktree isolation | ✓ | | | | | +| `/wt-switch-create` command | ✓ | | | | | -The configuration skill is documentation the agent reads to help set up LLM commits, hooks, and troubleshooting. Activity tracking shows which worktrees have running sessions. Worktree isolation needs worktree-lifecycle hooks and `/wt-switch-create` needs session working-directory switching — both Claude Code-only, so Codex, OpenCode, and Gemini users invoke `wt switch --create` and `wt remove` directly. Codex tracks activity through its own `Stop` and `SessionEnd` hooks. +The configuration skill is documentation the agent reads to help set up LLM commits, hooks, and troubleshooting. Activity tracking shows which worktrees have running sessions. Worktree isolation needs worktree-lifecycle hooks and `/wt-switch-create` needs session working-directory switching — both Claude Code-only, so Codex, OpenCode, Pi, and Gemini users invoke `wt switch --create` and `wt remove` directly. Codex tracks activity through its own `Stop` and `SessionEnd` hooks. ## Installation @@ -48,6 +48,14 @@ wt config plugins opencode install This writes the activity-tracking plugin to OpenCode's global plugins directory, `~/.config/opencode/plugins/worktrunk.ts` (honoring `$OPENCODE_CONFIG_DIR` and `$XDG_CONFIG_HOME`). `wt config plugins opencode uninstall` removes it. +### Pi + +```bash +wt config plugins pi install +``` + +This writes the activity hook to `~/.omp/agent/hooks/pre/worktrunk.ts`. Named `$OMP_PROFILE` or `$PI_PROFILE` profiles use `~/.omp/profiles//agent`, `$PI_CONFIG_DIR` changes the `.omp` config root, and `$PI_CODING_AGENT_DIR` overrides the agent directory for the default profile. `wt config plugins pi uninstall` removes the hook. + ### Gemini CLI ```bash @@ -69,7 +77,7 @@ Claude Code is designed to load the skill automatically when it detects worktrun ## Activity tracking -The Claude Code, Codex, OpenCode, and Gemini plugins track agent sessions with status markers in `wt list`: +The Claude Code, Codex, OpenCode, Pi, and Gemini plugins track agent sessions with status markers in `wt list`: ```console $ wt list diff --git a/skills/worktrunk/reference/claude-code.md b/skills/worktrunk/reference/claude-code.md index c9936e368..a05627a42 100644 --- a/skills/worktrunk/reference/claude-code.md +++ b/skills/worktrunk/reference/claude-code.md @@ -2,14 +2,14 @@ Worktrunk ships a plugin for each supported agent CLI. What a plugin provides depends on the hooks that CLI exposes: -| Capability | Claude Code | Codex | OpenCode | Gemini CLI | -|---|:-:|:-:|:-:|:-:| -| Configuration skill | ✓ | ✓ | | ✓ | -| Activity tracking (🤖/💬 in `wt list`) | ✓ | ✓ | ✓ | ✓ | -| Worktree isolation | ✓ | | | | -| `/wt-switch-create` command | ✓ | | | | +| Capability | Claude Code | Codex | OpenCode | Pi | Gemini CLI | +|---|:-:|:-:|:-:|:-:|:-:| +| Configuration skill | ✓ | ✓ | | | ✓ | +| Activity tracking (🤖/💬 in `wt list`) | ✓ | ✓ | ✓ | ✓ | ✓ | +| Worktree isolation | ✓ | | | | | +| `/wt-switch-create` command | ✓ | | | | | -The configuration skill is documentation the agent reads to help set up LLM commits, hooks, and troubleshooting. Activity tracking shows which worktrees have running sessions. Worktree isolation needs worktree-lifecycle hooks and `/wt-switch-create` needs session working-directory switching — both Claude Code-only, so Codex, OpenCode, and Gemini users invoke `wt switch --create` and `wt remove` directly. Codex tracks activity through its own `Stop` and `SessionEnd` hooks. +The configuration skill is documentation the agent reads to help set up LLM commits, hooks, and troubleshooting. Activity tracking shows which worktrees have running sessions. Worktree isolation needs worktree-lifecycle hooks and `/wt-switch-create` needs session working-directory switching — both Claude Code-only, so Codex, OpenCode, Pi, and Gemini users invoke `wt switch --create` and `wt remove` directly. Codex tracks activity through its own `Stop` and `SessionEnd` hooks. ## Installation @@ -48,6 +48,14 @@ wt config plugins opencode install This writes the activity-tracking plugin to OpenCode's global plugins directory, `~/.config/opencode/plugins/worktrunk.ts` (honoring `$OPENCODE_CONFIG_DIR` and `$XDG_CONFIG_HOME`). `wt config plugins opencode uninstall` removes it. +### Pi + +```bash +wt config plugins pi install +``` + +This writes the activity hook to `~/.omp/agent/hooks/pre/worktrunk.ts`. Named `$OMP_PROFILE` or `$PI_PROFILE` profiles use `~/.omp/profiles//agent`, `$PI_CONFIG_DIR` changes the `.omp` config root, and `$PI_CODING_AGENT_DIR` overrides the agent directory for the default profile. `wt config plugins pi uninstall` removes the hook. + ### Gemini CLI ```bash @@ -69,7 +77,7 @@ Claude Code is designed to load the skill automatically when it detects worktrun ## Activity tracking -The Claude Code, Codex, OpenCode, and Gemini plugins track agent sessions with status markers in `wt list`: +The Claude Code, Codex, OpenCode, Pi, and Gemini plugins track agent sessions with status markers in `wt list`: ```console $ wt list diff --git a/src/cli/config.rs b/src/cli/config.rs index 28b009592..f8e2f336a 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -202,6 +202,42 @@ $ wt config plugins opencode uninstall Uninstall, } +// Ordering: action + inverse adjacent (install, uninstall). +#[derive(Subcommand)] +pub enum ConfigPluginsPiCommand { + /// Install the activity tracking hook + #[command( + after_long_help = r#"Writes the Worktrunk hook to Pi's profile-aware user hook directory. + +## Examples + +```console +$ wt config plugins pi install +$ wt config plugins pi install --yes +``` + +## Plugin location + +The default location is `~/.omp/agent/hooks/pre/worktrunk.ts`. The installer +honors `$PI_CONFIG_DIR` and active `$OMP_PROFILE` / `$PI_PROFILE` profiles. +`$PI_CODING_AGENT_DIR` overrides the agent directory for the default profile +only — named profiles ignore it, matching Pi's own resolution."# + )] + Install, + + /// Remove the activity tracking hook + #[command( + after_long_help = r#"Removes the Worktrunk hook from Pi's active user hook directory. + +## Examples + +```console +$ wt config plugins pi uninstall +```"# + )] + Uninstall, +} + // Ordering: action + inverse adjacent (install, uninstall). #[derive(Subcommand)] pub enum ConfigPluginsCodexCommand { @@ -361,6 +397,26 @@ config precedence: `$OPENCODE_CONFIG_DIR` > `$XDG_CONFIG_HOME/opencode` > #[command(subcommand)] action: ConfigPluginsOpencodeCommand, }, + + /// Pi / oh-my-pi activity hook + #[command( + after_long_help = r#"Activity tracking hook — shows status markers in `wt list`: +- 🤖 — agent is working +- 💬 — agent is waiting for input + +Pi's `session_shutdown` event clears the marker when the session exits. + +## Examples + +```console +$ wt config plugins pi install +$ wt config plugins pi uninstall +```"# + )] + Pi { + #[command(subcommand)] + action: ConfigPluginsPiCommand, + }, } // Ordering: action + inverse adjacent (install, uninstall), then related @@ -670,6 +726,7 @@ $ wt config alias dry-run deploy -- --env=staging - **claude** — Claude Code plugin (activity tracking + statusline) - **codex** — Codex plugin (Worktrunk configuration skill) - **opencode** — OpenCode plugin (activity tracking) +- **pi** — Pi / oh-my-pi plugin (activity tracking) ## Examples @@ -677,6 +734,7 @@ $ wt config alias dry-run deploy -- --env=staging $ wt config plugins claude install $ wt config plugins codex install $ wt config plugins opencode install +$ wt config plugins pi install ```"# )] Plugins { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index adf6ad9b2..36800022f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -6,9 +6,9 @@ mod step; pub(crate) use config::{ ApprovalsCommand, CacheAction, CiStatusAction, ConfigAliasCommand, ConfigCommand, ConfigPluginsClaudeCommand, ConfigPluginsCodexCommand, ConfigPluginsCommand, - ConfigPluginsOpencodeCommand, ConfigShellCommand, DefaultBranchAction, GlobalFormatFlag, - HintsAction, LogsAction, MarkerAction, PreviousBranchAction, StateCommand, StateWrite, - VarsAction, + ConfigPluginsOpencodeCommand, ConfigPluginsPiCommand, ConfigShellCommand, DefaultBranchAction, + GlobalFormatFlag, HintsAction, LogsAction, MarkerAction, PreviousBranchAction, StateCommand, + StateWrite, VarsAction, }; pub(crate) use hook::{HOOK_TYPE_NAMES, HookCommand, HookOptions, parse_hook_type}; pub(crate) use list::ListSubcommand; diff --git a/src/commands/config/mod.rs b/src/commands/config/mod.rs index 0d9d21abd..c3bb9b5b2 100644 --- a/src/commands/config/mod.rs +++ b/src/commands/config/mod.rs @@ -8,6 +8,7 @@ mod codex; mod create; mod hints; pub mod opencode; +mod pi; mod plugins; mod show; mod state; @@ -20,6 +21,7 @@ pub use codex::{handle_codex_install, handle_codex_uninstall}; pub use create::handle_config_create; pub use hints::{handle_hints_clear, handle_hints_get}; pub use opencode::{handle_opencode_install, handle_opencode_uninstall}; +pub use pi::{handle_pi_install, handle_pi_uninstall}; pub use plugins::{ handle_claude_install, handle_claude_install_statusline, handle_claude_uninstall, }; @@ -30,6 +32,99 @@ pub use state::{ handle_state_show, handle_vars_clear, handle_vars_get, handle_vars_list, handle_vars_set, }; pub use update::handle_config_update; +use worktrunk::styling::{eprintln, hint_message, info_message, success_message}; + +/// Install or update a file-based agent plugin after confirmation. +fn install_file_plugin( + name: &str, + target: &std::path::Path, + source: &str, + yes: bool, +) -> anyhow::Result<()> { + use crate::output::prompt::{PromptResponse, prompt_yes_no_preview}; + use anyhow::Context; + use color_print::cformat; + use worktrunk::path::format_path_for_display; + + let target_display = format_path_for_display(target); + if target.exists() + && let Ok(existing) = std::fs::read_to_string(target) + && existing == source + { + eprintln!( + "{}", + info_message(cformat!( + "Plugin already installed @ {target_display}" + )) + ); + return Ok(()); + } + + let action = if target.exists() { "Update" } else { "Install" }; + let preview_msg = info_message(cformat!("Would write to {target_display}")); + let preview = || eprintln!("{}", preview_msg); + let confirmed = yes + || prompt_yes_no_preview( + &cformat!("{action} {name} plugin @ {target_display}?"), + preview, + )? == PromptResponse::Accepted; + if !confirmed { + return Ok(()); + } + + let parent = target + .parent() + .context("Plugin path has no parent directory")?; + std::fs::create_dir_all(parent) + .with_context(|| format!("Failed to create directory {}", parent.display()))?; + worktrunk::utils::write_atomically(target, source) + .with_context(|| format!("Failed to write plugin to {target_display}"))?; + + eprintln!( + "{}", + success_message(cformat!("Plugin installed @ {target_display}")) + ); + eprintln!( + "{}", + hint_message(cformat!( + "Activity markers (🤖/💬) will appear in wt list" + )) + ); + Ok(()) +} + +/// Remove a file-based agent plugin after confirmation. +fn uninstall_file_plugin(name: &str, target: &std::path::Path, yes: bool) -> anyhow::Result<()> { + use crate::output::prompt::{PromptResponse, prompt_yes_no_preview}; + use anyhow::Context; + use color_print::cformat; + use worktrunk::path::format_path_for_display; + + let target_display = format_path_for_display(target); + if !target.exists() { + eprintln!("{}", info_message("Plugin not installed")); + return Ok(()); + } + + let preview_msg = info_message(cformat!("Would remove {target_display}")); + let preview = || eprintln!("{}", preview_msg); + let confirmed = yes + || prompt_yes_no_preview( + &cformat!("Remove {name} plugin @ {target_display}?"), + preview, + )? == PromptResponse::Accepted; + if !confirmed { + return Ok(()); + } + + std::fs::remove_file(target) + .with_context(|| format!("Failed to remove plugin at {target_display}"))?; + eprintln!( + "{}", + success_message(cformat!("Plugin removed from {target_display}")) + ); + Ok(()) +} /// Run a plugin-CLI command (`claude` / `codex`), surfacing a non-zero exit /// as a typed [`worktrunk::git::CommandError`]. diff --git a/src/commands/config/opencode.rs b/src/commands/config/opencode.rs index 9cf47b5e4..c16fc44f6 100644 --- a/src/commands/config/opencode.rs +++ b/src/commands/config/opencode.rs @@ -8,11 +8,6 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use color_print::cformat; -use worktrunk::path::format_path_for_display; -use worktrunk::styling::{eprintln, hint_message, info_message, success_message}; - -use crate::output::prompt::{PromptResponse, prompt_yes_no_preview}; /// The plugin source, embedded at compile time. const PLUGIN_SOURCE: &str = include_str!("../../../dev/opencode-plugin.ts"); @@ -59,97 +54,14 @@ pub fn plugin_file_exists() -> bool { plugin_path().map(|p| p.exists()).unwrap_or(false) } -/// Confirm an action via prompt, or accept automatically with `--yes`. -fn confirm_or_yes(yes: bool, prompt: &str, preview: impl Fn()) -> Result { - Ok(yes || prompt_yes_no_preview(prompt, preview)? == PromptResponse::Accepted) -} - /// Handle `wt config plugins opencode install`. pub fn handle_opencode_install(yes: bool) -> Result<()> { let target = plugin_path()?; - let target_display = format_path_for_display(&target); - - // Check if already installed with current content - if target.exists() - && let Ok(existing) = std::fs::read_to_string(&target) - && existing == PLUGIN_SOURCE - { - eprintln!( - "{}", - info_message(cformat!( - "Plugin already installed @ {target_display}" - )) - ); - return Ok(()); - } - - let action = if target.exists() { "Update" } else { "Install" }; - let preview_msg = info_message(cformat!("Would write to {target_display}")); - let preview = || eprintln!("{}", preview_msg); - - let confirmed = confirm_or_yes( - yes, - &cformat!("{action} OpenCode plugin @ {target_display}?"), - preview, - ); - if !confirmed? { - return Ok(()); - } - - // Create parent directories if needed - let parent = target - .parent() - .context("Plugin path has no parent directory")?; - std::fs::create_dir_all(parent) - .with_context(|| format!("Failed to create directory {}", parent.display()))?; - - // Write the plugin file - worktrunk::utils::write_atomically(&target, PLUGIN_SOURCE) - .with_context(|| format!("Failed to write plugin to {target_display}"))?; - - eprintln!( - "{}", - success_message(cformat!("Plugin installed @ {target_display}")) - ); - eprintln!( - "{}", - hint_message(cformat!( - "Activity markers (🤖/💬) will appear in wt list" - )) - ); - - Ok(()) + super::install_file_plugin("OpenCode", &target, PLUGIN_SOURCE, yes) } /// Handle `wt config plugins opencode uninstall`. pub fn handle_opencode_uninstall(yes: bool) -> Result<()> { let target = plugin_path()?; - let target_display = format_path_for_display(&target); - - if !target.exists() { - eprintln!("{}", info_message("Plugin not installed")); - return Ok(()); - } - - let preview_msg = info_message(cformat!("Would remove {target_display}")); - let preview = || eprintln!("{}", preview_msg); - - let confirmed = confirm_or_yes( - yes, - &cformat!("Remove OpenCode plugin @ {target_display}?"), - preview, - ); - if !confirmed? { - return Ok(()); - } - - std::fs::remove_file(&target) - .with_context(|| format!("Failed to remove plugin at {target_display}"))?; - - eprintln!( - "{}", - success_message(cformat!("Plugin removed from {target_display}")) - ); - - Ok(()) + super::uninstall_file_plugin("OpenCode", &target, yes) } diff --git a/src/commands/config/pi.rs b/src/commands/config/pi.rs new file mode 100644 index 000000000..2ba9def7c --- /dev/null +++ b/src/commands/config/pi.rs @@ -0,0 +1,70 @@ +//! Pi / oh-my-pi activity-hook installation. +//! +//! Installs the embedded hook factory under Pi's profile-aware user agent +//! directory at `hooks/pre/worktrunk.ts`. + +use std::path::PathBuf; + +use anyhow::{Context, Result}; + +const PLUGIN_SOURCE: &str = include_str!("../../../dev/pi-plugin.ts"); + +fn active_profile() -> Option { + let value = std::env::var("OMP_PROFILE") + .ok() + .or_else(|| std::env::var("PI_PROFILE").ok())?; + let profile = value.trim(); + (!profile.is_empty() && profile != "default").then(|| profile.to_owned()) +} + +fn pi_agent_dir() -> Result { + if let Some(path) = std::env::var("PI_CODING_AGENT_DIR") + .ok() + .filter(|value| !value.is_empty()) + // Named profiles ignore `PI_CODING_AGENT_DIR` upstream, so the + // override applies to the default profile only. + .filter(|_| active_profile().is_none()) + { + return Ok(PathBuf::from(path)); + } + + let home = worktrunk::path::home_dir().context("Could not determine home directory")?; + let config_dir = std::env::var("PI_CONFIG_DIR") + .ok() + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| ".omp".to_owned()); + let root = home.join(config_dir); + + Ok(match active_profile() { + Some(profile) => root.join("profiles").join(profile).join("agent"), + None => root.join("agent"), + }) +} + +pub fn plugin_path() -> Result { + Ok(pi_agent_dir()? + .join("hooks") + .join("pre") + .join("worktrunk.ts")) +} + +pub fn is_plugin_installed() -> bool { + plugin_path() + .ok() + .and_then(|p| std::fs::read_to_string(p).ok()) + .is_some_and(|content| content == PLUGIN_SOURCE) +} + +pub fn plugin_file_exists() -> bool { + plugin_path().map(|p| p.exists()).unwrap_or(false) +} + +pub fn handle_pi_install(yes: bool) -> Result<()> { + let target = plugin_path()?; + super::install_file_plugin("Pi", &target, PLUGIN_SOURCE, yes) +} + +pub fn handle_pi_uninstall(yes: bool) -> Result<()> { + let target = plugin_path()?; + super::uninstall_file_plugin("Pi", &target, yes) +} diff --git a/src/commands/config/show.rs b/src/commands/config/show.rs index 7abf0a2b0..0a002d680 100644 --- a/src/commands/config/show.rs +++ b/src/commands/config/show.rs @@ -75,6 +75,12 @@ pub fn handle_config_show(full: bool, format: SwitchFormat) -> anyhow::Result<() render_opencode_status(&mut show_output)?; } + // Render Pi status (only when the Pi CLI is available) + if is_pi_available() { + show_output.push('\n'); + render_pi_status(&mut show_output)?; + } + // Render Gemini status (only when gemini CLI is available) if is_gemini_available() { show_output.push('\n'); @@ -326,6 +332,14 @@ fn is_opencode_available() -> bool { which::which("opencode").is_ok() } +/// Check if the Pi coding agent CLI is available. +fn is_pi_available() -> bool { + if let Ok(val) = std::env::var("WORKTRUNK_TEST_PI_INSTALLED") { + return val == "1"; + } + which::which("omp").is_ok() +} + /// Render OPENCODE section (plugin status). /// Caller must check `is_opencode_available()` first. fn render_opencode_status(out: &mut String) -> anyhow::Result<()> { @@ -357,6 +371,34 @@ fn render_opencode_status(out: &mut String) -> anyhow::Result<()> { Ok(()) } +/// Render PI section (plugin status). +/// Caller must check `is_pi_available()` first. +fn render_pi_status(out: &mut String) -> anyhow::Result<()> { + writeln!(out, "{}", format_heading("PI", None))?; + + if super::pi::is_plugin_installed() { + writeln!(out, "{}", success_message("Plugin installed"))?; + } else if super::pi::plugin_file_exists() { + writeln!( + out, + "{}", + hint_message(cformat!( + "Plugin outdated. To update, run wt config plugins pi install" + )) + )?; + } else { + writeln!( + out, + "{}", + hint_message(cformat!( + "Plugin not installed. To install, run wt config plugins pi install" + )) + )?; + } + + Ok(()) +} + /// Check if Gemini CLI is available fn is_gemini_available() -> bool { // Allow tests to override detection diff --git a/src/commands/mod.rs b/src/commands/mod.rs index c75d8ba1c..4e9130e4d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -40,9 +40,9 @@ pub(crate) use config::{ handle_claude_uninstall, handle_codex_install, handle_codex_uninstall, handle_config_create, handle_config_show, handle_config_update, handle_hints_clear, handle_hints_get, handle_logs_list, handle_logs_profile, handle_opencode_install, handle_opencode_uninstall, - handle_state_clear, handle_state_clear_all, handle_state_get, handle_state_set, - handle_state_show, handle_vars_clear, handle_vars_get, handle_vars_list, handle_vars_set, - list_approvals, + handle_pi_install, handle_pi_uninstall, handle_state_clear, handle_state_clear_all, + handle_state_get, handle_state_set, handle_state_show, handle_vars_clear, handle_vars_get, + handle_vars_list, handle_vars_set, list_approvals, }; pub(crate) use configure_shell::{ handle_configure_shell, handle_show_theme, handle_unconfigure_shell, diff --git a/src/main.rs b/src/main.rs index b30b02345..d7533c54b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,21 +49,21 @@ use commands::{ handle_config_create, handle_config_show, handle_config_update, handle_configure_shell, handle_custom_command, handle_hints_clear, handle_hints_get, handle_hook_show, handle_init, handle_list, handle_logs_list, handle_logs_profile, handle_merge, handle_opencode_install, - handle_opencode_uninstall, handle_promote, handle_rebase, handle_remove_command, - handle_show_theme, handle_squash, handle_state_clear, handle_state_clear_all, handle_state_get, - handle_state_set, handle_state_show, handle_switch_command, handle_unconfigure_shell, - handle_vars_clear, handle_vars_get, handle_vars_list, handle_vars_set, list_approvals, - run_hook, step_commit, step_copy_ignored, step_diff, step_eval, step_for_each, step_prune, - step_relocate, step_tether, + handle_opencode_uninstall, handle_pi_install, handle_pi_uninstall, handle_promote, + handle_rebase, handle_remove_command, handle_show_theme, handle_squash, handle_state_clear, + handle_state_clear_all, handle_state_get, handle_state_set, handle_state_show, + handle_switch_command, handle_unconfigure_shell, handle_vars_clear, handle_vars_get, + handle_vars_list, handle_vars_set, list_approvals, run_hook, step_commit, step_copy_ignored, + step_diff, step_eval, step_for_each, step_prune, step_relocate, step_tether, }; use cli::{ ApprovalsCommand, CacheAction, CiStatusAction, Cli, Commands, ConfigAliasCommand, ConfigCommand, ConfigPluginsClaudeCommand, ConfigPluginsCodexCommand, ConfigPluginsCommand, - ConfigPluginsOpencodeCommand, ConfigShellCommand, DefaultBranchAction, GlobalFormatFlag, - HintsAction, HookCommand, HookOptions, ListArgs, ListSubcommand, LogsAction, MarkerAction, - MergeArgs, PreviousBranchAction, StateCommand, StateWrite, StepCommand, SwitchFormat, - VarsAction, + ConfigPluginsOpencodeCommand, ConfigPluginsPiCommand, ConfigShellCommand, DefaultBranchAction, + GlobalFormatFlag, HintsAction, HookCommand, HookOptions, ListArgs, ListSubcommand, LogsAction, + MarkerAction, MergeArgs, PreviousBranchAction, StateCommand, StateWrite, StepCommand, + SwitchFormat, VarsAction, }; /// Render a clap error to stderr, appending a wt-specific nested-subcommand @@ -669,6 +669,10 @@ fn handle_plugins_command(action: ConfigPluginsCommand, yes: bool) -> anyhow::Re ConfigPluginsOpencodeCommand::Install => handle_opencode_install(yes), ConfigPluginsOpencodeCommand::Uninstall => handle_opencode_uninstall(yes), }, + ConfigPluginsCommand::Pi { action } => match action { + ConfigPluginsPiCommand::Install => handle_pi_install(yes), + ConfigPluginsPiCommand::Uninstall => handle_pi_uninstall(yes), + }, } } diff --git a/src/testing/mod.rs b/src/testing/mod.rs index eb250d5c9..3dd700566 100644 --- a/src/testing/mod.rs +++ b/src/testing/mod.rs @@ -1207,6 +1207,14 @@ pub fn set_temp_home_env(cmd: &mut Command, home: &Path) { // matches the setup helpers and stays hermetic against an ambient // CLAUDE_CONFIG_DIR inherited from the test runner's environment. cmd.env("CLAUDE_CONFIG_DIR", home.join(".claude")); + for var in [ + "OMP_PROFILE", + "PI_PROFILE", + "PI_CONFIG_DIR", + "PI_CODING_AGENT_DIR", + ] { + cmd.env_remove(var); + } } /// Override `WORKTRUNK_CONFIG_PATH` to point to the XDG-derived user config path @@ -2292,14 +2300,14 @@ impl TestRepo { self.gemini_installed = true; } - /// Make `claude`, `codex`, `opencode`, and `gemini` resolvable on `PATH`. + /// Make `claude`, `codex`, `opencode`, `omp`, and `gemini` resolvable on `PATH`. /// /// The `setup_mock_*_installed` helpers force detection through the /// `WORKTRUNK_TEST_*_INSTALLED` env overrides, so the `which::which` /// lookup inside each `is_*_available()` never runs under test. This /// helper instead drops those overrides and prepends real mock /// executables, exercising the production PATH-detection path for all - /// four AI CLIs at once. Call `setup_mock_ci_tools_unauthenticated()` + /// five AI CLIs at once. Call `setup_mock_ci_tools_unauthenticated()` /// first to create the mock bin directory. pub fn setup_mock_clis_on_path(&mut self) { let mock_bin = self @@ -2308,7 +2316,7 @@ impl TestRepo { .expect("call setup_mock_ci_tools_unauthenticated() first"); // `wt config show` only `which`-detects these CLIs (never runs // them), so the mocks need no command behavior. - for cli in ["claude", "codex", "opencode", "gemini"] { + for cli in ["claude", "codex", "opencode", "omp", "gemini"] { MockConfig::new(cli).write(mock_bin); } self.detect_clis_via_path = true; @@ -2886,6 +2894,7 @@ impl TestRepo { "WORKTRUNK_TEST_CLAUDE_INSTALLED", "WORKTRUNK_TEST_CODEX_INSTALLED", "WORKTRUNK_TEST_OPENCODE_INSTALLED", + "WORKTRUNK_TEST_PI_INSTALLED", "WORKTRUNK_TEST_GEMINI_INSTALLED", ] { cmd.env_remove(var); diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index 3e99364bf..dac34ac03 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -2258,6 +2258,43 @@ fn test_config_show_opencode_plugin_outdated(mut repo: TestRepo, temp_home: Temp }); } +#[rstest] +#[case(None, "Plugin not installed")] +#[case(Some("// outdated plugin content\n"), "Plugin outdated")] +#[case(Some(include_str!("../../dev/pi-plugin.ts")), "Plugin installed")] +fn test_config_show_pi_plugin_status( + mut repo: TestRepo, + temp_home: TempDir, + #[case] plugin_content: Option<&str>, + #[case] expected_status: &str, +) { + repo.setup_mock_ci_tools_unauthenticated(); + let plugin_path = temp_home.path().join(".omp/agent/hooks/pre/worktrunk.ts"); + if let Some(content) = plugin_content { + fs::create_dir_all(plugin_path.parent().unwrap()).unwrap(); + fs::write(&plugin_path, content).unwrap(); + } + + let global_config_dir = temp_home.path().join(".config").join("worktrunk"); + fs::create_dir_all(&global_config_dir).unwrap(); + fs::write(global_config_dir.join("config.toml"), "").unwrap(); + + let mut cmd = repo.wt_command(); + cmd.args(["config", "show"]).current_dir(repo.root_path()); + set_temp_home_env(&mut cmd, temp_home.path()); + set_xdg_config_path(&mut cmd, temp_home.path()); + cmd.env("WORKTRUNK_TEST_PI_INSTALLED", "1"); + + let output = cmd.output().expect("config show should run"); + assert!(output.status.success(), "config show failed: {output:?}"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("PI"), "missing Pi section: {stdout}"); + assert!( + stdout.contains(expected_status), + "missing Pi status: {stdout}" + ); +} + #[rstest] fn test_config_show_gemini_available_extension_not_installed( mut repo: TestRepo, @@ -2353,7 +2390,7 @@ fn test_config_show_gemini_extension_installed(mut repo: TestRepo, temp_home: Te /// override the harness sets, so the production `which::which` PATH lookup is /// otherwise never exercised. `setup_mock_clis_on_path()` drops the overrides /// and prepends real mock executables, so this single run covers the -/// PATH-detection path for all four AI CLIs at once. +/// PATH-detection path for all five AI CLIs at once. #[rstest] fn test_config_show_clis_detected_via_path(mut repo: TestRepo, temp_home: TempDir) { repo.setup_mock_ci_tools_unauthenticated(); @@ -2607,6 +2644,165 @@ fn test_opencode_uninstall_prompt_declined(temp_home: TempDir) { ); } +// ============================================================================= +// Pi plugin install/uninstall +// ============================================================================= + +#[rstest] +fn test_pi_install_creates_profile_aware_hook(temp_home: TempDir) { + let settings = setup_home_snapshot_settings(&temp_home); + settings.bind(|| { + let mut cmd = wt_command(); + set_temp_home_env(&mut cmd, temp_home.path()); + cmd.env("OMP_PROFILE", "research"); + cmd.args(["config", "plugins", "pi", "install", "--yes"]); + + assert_cmd_snapshot!(cmd); + }); + + let canonical_home = + crate::common::canonicalize(temp_home.path()).unwrap_or_else(|_| temp_home.path().into()); + let plugin_path = canonical_home.join(".omp/profiles/research/agent/hooks/pre/worktrunk.ts"); + let content = fs::read_to_string(&plugin_path).expect("Pi hook should be installed"); + assert!(content.contains("agent_start")); + assert!(content.contains("agent_end")); + assert!(content.contains("session_shutdown")); +} + +#[rstest] +fn test_pi_install_honors_agent_dir_override(temp_home: TempDir) { + let agent_dir = temp_home.path().join("custom-pi-agent"); + let mut cmd = wt_command(); + set_temp_home_env(&mut cmd, temp_home.path()); + cmd.env("PI_CODING_AGENT_DIR", &agent_dir); + cmd.args(["config", "plugins", "pi", "install", "--yes"]); + + let output = cmd.output().expect("install command should run"); + assert!( + output.status.success(), + "install failed: stdout={}, stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + assert!(agent_dir.join("hooks/pre/worktrunk.ts").exists()); +} + +#[rstest] +fn test_pi_named_profile_ignores_agent_dir_override(temp_home: TempDir) { + let agent_dir = temp_home.path().join("custom-pi-agent"); + let mut cmd = wt_command(); + set_temp_home_env(&mut cmd, temp_home.path()); + cmd.env("OMP_PROFILE", "research"); + cmd.env("PI_CODING_AGENT_DIR", &agent_dir); + cmd.args(["config", "plugins", "pi", "install", "--yes"]); + + let output = cmd.output().expect("install command should run"); + assert!(output.status.success(), "install failed: {output:?}"); + assert!(!agent_dir.join("hooks/pre/worktrunk.ts").exists()); + assert!( + temp_home + .path() + .join(".omp/profiles/research/agent/hooks/pre/worktrunk.ts") + .exists() + ); +} + +#[rstest] +fn test_pi_install_honors_pi_profile_and_config_dir(temp_home: TempDir) { + let mut cmd = wt_command(); + set_temp_home_env(&mut cmd, temp_home.path()); + cmd.env_remove("OMP_PROFILE"); + cmd.env("PI_PROFILE", "research"); + cmd.env("PI_CONFIG_DIR", ".pi-config"); + cmd.args(["config", "plugins", "pi", "install", "--yes"]); + + let output = cmd.output().expect("install command should run"); + assert!(output.status.success(), "install failed: {output:?}"); + assert!( + temp_home + .path() + .join(".pi-config/profiles/research/agent/hooks/pre/worktrunk.ts") + .exists() + ); +} + +#[rstest] +fn test_pi_install_is_idempotent(temp_home: TempDir) { + for _ in 0..2 { + let mut cmd = wt_command(); + set_temp_home_env(&mut cmd, temp_home.path()); + cmd.args(["config", "plugins", "pi", "install", "--yes"]); + let output = cmd.output().expect("install command should run"); + assert!(output.status.success(), "install failed: {output:?}"); + } + + let plugin_path = temp_home.path().join(".omp/agent/hooks/pre/worktrunk.ts"); + assert_eq!( + fs::read_to_string(plugin_path).unwrap(), + include_str!("../../dev/pi-plugin.ts") + ); +} + +#[rstest] +fn test_pi_install_prompt_declined(temp_home: TempDir) { + let mut cmd = wt_command(); + set_temp_home_env(&mut cmd, temp_home.path()); + cmd.args(["config", "plugins", "pi", "install"]); + + let output = cmd.output().expect("install command should run"); + assert!(output.status.success(), "install failed: {output:?}"); + assert!( + !temp_home + .path() + .join(".omp/agent/hooks/pre/worktrunk.ts") + .exists() + ); +} + +#[rstest] +fn test_pi_uninstall_removes_hook(temp_home: TempDir) { + let agent_dir = temp_home.path().join(".omp/agent"); + let plugin_path = agent_dir.join("hooks/pre/worktrunk.ts"); + fs::create_dir_all(plugin_path.parent().unwrap()).unwrap(); + fs::write(&plugin_path, include_str!("../../dev/pi-plugin.ts")).unwrap(); + + let settings = setup_home_snapshot_settings(&temp_home); + settings.bind(|| { + let mut cmd = wt_command(); + set_temp_home_env(&mut cmd, temp_home.path()); + cmd.args(["config", "plugins", "pi", "uninstall", "--yes"]); + + assert_cmd_snapshot!(cmd); + }); + + assert!(!plugin_path.exists()); +} + +#[rstest] +fn test_pi_uninstall_missing_is_a_no_op(temp_home: TempDir) { + let mut cmd = wt_command(); + set_temp_home_env(&mut cmd, temp_home.path()); + cmd.args(["config", "plugins", "pi", "uninstall", "--yes"]); + + let output = cmd.output().expect("uninstall command should run"); + assert!(output.status.success(), "uninstall failed: {output:?}"); +} + +#[rstest] +fn test_pi_uninstall_prompt_declined(temp_home: TempDir) { + let plugin_path = temp_home.path().join(".omp/agent/hooks/pre/worktrunk.ts"); + fs::create_dir_all(plugin_path.parent().unwrap()).unwrap(); + fs::write(&plugin_path, include_str!("../../dev/pi-plugin.ts")).unwrap(); + + let mut cmd = wt_command(); + set_temp_home_env(&mut cmd, temp_home.path()); + cmd.args(["config", "plugins", "pi", "uninstall"]); + + let output = cmd.output().expect("uninstall command should run"); + assert!(output.status.success(), "uninstall failed: {output:?}"); + assert!(plugin_path.exists()); +} + /// When $SHELL is not set but PSModulePath is, config show should display /// "Detected shell: powershell" in the diagnostics and show the verification hint. #[rstest] diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_clis_detected_via_path.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_clis_detected_via_path.snap index c2bb87702..7e7884bf1 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_clis_detected_via_path.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_clis_detected_via_path.snap @@ -78,6 +78,9 @@ exit_code: 0 OPENCODE ↳ Plugin not installed. To install, run wt config plugins opencode install +PI +↳ Plugin not installed. To install, run wt config plugins pi install + GEMINI CLI ↳ Extension not installed. To install, run gemini extensions install https://github.com/max-sixty/worktrunk diff --git a/tests/snapshots/integration__integration_tests__config_show__pi_install_creates_profile_aware_hook.snap b/tests/snapshots/integration__integration_tests__config_show__pi_install_creates_profile_aware_hook.snap new file mode 100644 index 000000000..e2204a3e6 --- /dev/null +++ b/tests/snapshots/integration__integration_tests__config_show__pi_install_creates_profile_aware_hook.snap @@ -0,0 +1,49 @@ +--- +source: tests/integration_tests/config_show.rs +info: + program: wt + args: + - config + - plugins + - pi + - install + - "--yes" + env: + APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" + CLICOLOR_FORCE: "1" + COLUMNS: "500" + HOME: "[TEST_HOME]" + LANG: C + LC_ALL: C + LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]" + OMP_PROFILE: research + OPENCODE_CONFIG_DIR: "[TEST_OPENCODE_CONFIG]" + TERM: alacritty + USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_BASH_INSTALLED: "0" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_CODEX_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_FISH_INSTALLED: "0" + WORKTRUNK_TEST_GEMINI_INSTALLED: "0" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_OPENCODE_INSTALLED: "0" + WORKTRUNK_TEST_PARENT_SHELL: "" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + WORKTRUNK_TEST_ZSH_INSTALLED: "0" + XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" +--- +success: true +exit_code: 0 +----- stdout ----- + +----- stderr ----- +✓ Plugin installed @ ~/.omp/profiles/research/agent/hooks/pre/worktrunk.ts +↳ Activity markers (🤖/💬) will appear in wt list diff --git a/tests/snapshots/integration__integration_tests__config_show__pi_uninstall_removes_hook.snap b/tests/snapshots/integration__integration_tests__config_show__pi_uninstall_removes_hook.snap new file mode 100644 index 000000000..094af0b41 --- /dev/null +++ b/tests/snapshots/integration__integration_tests__config_show__pi_uninstall_removes_hook.snap @@ -0,0 +1,47 @@ +--- +source: tests/integration_tests/config_show.rs +info: + program: wt + args: + - config + - plugins + - pi + - uninstall + - "--yes" + env: + APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" + CLICOLOR_FORCE: "1" + COLUMNS: "500" + HOME: "[TEST_HOME]" + LANG: C + LC_ALL: C + LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]" + OPENCODE_CONFIG_DIR: "[TEST_OPENCODE_CONFIG]" + TERM: alacritty + USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_BASH_INSTALLED: "0" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_CODEX_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_FISH_INSTALLED: "0" + WORKTRUNK_TEST_GEMINI_INSTALLED: "0" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_OPENCODE_INSTALLED: "0" + WORKTRUNK_TEST_PARENT_SHELL: "" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + WORKTRUNK_TEST_ZSH_INSTALLED: "0" + XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" +--- +success: true +exit_code: 0 +----- stdout ----- + +----- stderr ----- +✓ Plugin removed from ~/.omp/agent/hooks/pre/worktrunk.ts diff --git a/tests/snapshots/integration__integration_tests__help__help_config_plugins.snap b/tests/snapshots/integration__integration_tests__help__help_config_plugins.snap index 2e44e26e7..87fd704f3 100644 --- a/tests/snapshots/integration__integration_tests__help__help_config_plugins.snap +++ b/tests/snapshots/integration__integration_tests__help__help_config_plugins.snap @@ -41,6 +41,7 @@ Usage: wt config plugins [OPTIONS]  claude Claude Code plugin codex Codex plugin opencode OpenCode plugin + pi Pi / oh-my-pi activity hook Options: -h, --help @@ -69,11 +70,13 @@ Install and manage Worktrunk plugins for AI coding tools. - claude — Claude Code plugin (activity tracking + statusline) - codex — Codex plugin (Worktrunk configuration skill) - opencode — OpenCode plugin (activity tracking) +- pi — Pi / oh-my-pi plugin (activity tracking) Examples   wt config plugins claude install   wt config plugins codex install   wt config plugins opencode install +  wt config plugins pi install ----- stderr -----