aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-18 23:46:34 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-18 23:56:20 +0300
commita51a15914a5b29155022a1fd589d2bcc6409cadf (patch)
tree8602b6d94c811666d49e4c2aa7636bb6a9b0e8a3
parent60c040057e3949ae93ee88d495df09b4b848d22d (diff)
fix: support javascript integrations on windows
refs #1531
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--docs/next/website/src/content/docs/integrations.mdx4
-rw-r--r--docs/next/website/src/content/docs/ja/integrations.mdx4
-rw-r--r--docs/next/website/src/content/docs/zh-cn/integrations.mdx4
-rw-r--r--src/integration/assets/herdr-agent-state.test.ts78
-rw-r--r--src/integration/assets/kilo/herdr-agent-state.js7
-rw-r--r--src/integration/assets/omp/herdr-agent-state.ts8
-rw-r--r--src/integration/assets/opencode/herdr-agent-state.js5
-rw-r--r--src/integration/assets/pi/herdr-agent-state.ts8
-rw-r--r--src/integration/mod.rs6
-rw-r--r--src/integration/registry.rs6
-rw-r--r--src/integration/targets.rs38
-rw-r--r--src/integration/tests.rs45
-rw-r--r--tests/cli_wrapper.rs2
14 files changed, 160 insertions, 56 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index cbe0500d..b3d57550 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -10,6 +10,7 @@
### Fixed
- `herdr config check` now reports unknown config keys with their full paths instead of treating ignored typos as valid configuration. (#1573)
- Codex panes with customized static terminal titles now fall back to the live working footer instead of remaining idle, while OSC activity remains preferred. (#1563)
+- The Pi, OMP, OpenCode, and Kilo Code integrations can now be installed on Windows and report lifecycle state and native session identity through Herdr's named-pipe API. (#1531)
- Named agent prompts now honor live bracketed-paste mode before sending Enter, preserving OpenCode text such as `A != B` instead of triggering shell mode. (#1525)
- New panes, tabs, layouts, and workspaces using `new_cwd = "follow"` now inherit the foreground process-group leader's working directory instead of an unrelated helper process directory. (#1472)
- Noninteractive update, plugin, integration, sound, custom-command, and Git subprocesses no longer flash console windows on Windows. (#1468)
diff --git a/docs/next/website/src/content/docs/integrations.mdx b/docs/next/website/src/content/docs/integrations.mdx
index 48d3e7c9..ea26b892 100644
--- a/docs/next/website/src/content/docs/integrations.mdx
+++ b/docs/next/website/src/content/docs/integrations.mdx
@@ -76,7 +76,7 @@ Herdr writes the bundled extension to:
~/.pi/agent/extensions/herdr-agent-state.ts
```
-If `PI_CODING_AGENT_DIR` is set, Herdr writes to `$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts` instead. The extensions directory must already exist. Uninstall removes only that extension file.
+If `PI_CODING_AGENT_DIR` is set, Herdr writes to `$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts` instead. Herdr creates the extensions directory when the Pi agent directory already exists. Uninstall removes only that extension file.
## OMP
@@ -92,7 +92,7 @@ Herdr writes the bundled extension to:
~/.omp/agent/extensions/herdr-omp-agent-state.ts
```
-If `PI_CODING_AGENT_DIR` is set, Herdr writes to `$PI_CODING_AGENT_DIR/extensions/herdr-omp-agent-state.ts` instead. The extensions directory must already exist. Uninstall removes only that extension file.
+If `PI_CODING_AGENT_DIR` is set, Herdr writes to `$PI_CODING_AGENT_DIR/extensions/herdr-omp-agent-state.ts` instead. Herdr creates the extensions directory when the OMP agent directory already exists. Uninstall removes only that extension file.
The OMP integration reports `omp` as the agent label, lifecycle state, and native session identity through Herdr's socket API. It does not require native process detection for the `omp` executable, and Herdr can resume an OMP pane with `omp --resume=<session>` after a server restart.
diff --git a/docs/next/website/src/content/docs/ja/integrations.mdx b/docs/next/website/src/content/docs/ja/integrations.mdx
index de80ab39..fe4e41b1 100644
--- a/docs/next/website/src/content/docs/ja/integrations.mdx
+++ b/docs/next/website/src/content/docs/ja/integrations.mdx
@@ -76,7 +76,7 @@ Herdr はバンドルされた拡張を次の場所に書き込みます:
~/.pi/agent/extensions/herdr-agent-state.ts
```
-`PI_CODING_AGENT_DIR` が設定されている場合は、代わりに `$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts` に書き込みます。extensions ディレクトリはあらかじめ存在している必要があります。アンインストールはその拡張ファイルだけを削除します。
+`PI_CODING_AGENT_DIR` が設定されている場合は、代わりに `$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts` に書き込みます。Pi の agent ディレクトリが存在する場合、Herdr は extensions ディレクトリを作成します。アンインストールはその拡張ファイルだけを削除します。
## OMP
@@ -92,7 +92,7 @@ Herdr はバンドルされた拡張を次の場所に書き込みます:
~/.omp/agent/extensions/herdr-omp-agent-state.ts
```
-`PI_CODING_AGENT_DIR` が設定されている場合は、代わりに `$PI_CODING_AGENT_DIR/extensions/herdr-omp-agent-state.ts` に書き込みます。extensions ディレクトリはあらかじめ存在している必要があります。アンインストールはその拡張ファイルだけを削除します。
+`PI_CODING_AGENT_DIR` が設定されている場合は、代わりに `$PI_CODING_AGENT_DIR/extensions/herdr-omp-agent-state.ts` に書き込みます。OMP の agent ディレクトリが存在する場合、Herdr は extensions ディレクトリを作成します。アンインストールはその拡張ファイルだけを削除します。
OMP インテグレーションは、Herdr のソケット API を通じてエージェントラベル `omp`、ライフサイクル状態、ネイティブセッション識別を報告します。`omp` 実行ファイルのネイティブプロセス検出は不要で、Herdr はサーバー再起動後に `omp --resume=<session>` で OMP ペインを resume できます。
diff --git a/docs/next/website/src/content/docs/zh-cn/integrations.mdx b/docs/next/website/src/content/docs/zh-cn/integrations.mdx
index 2e494e12..c49f61db 100644
--- a/docs/next/website/src/content/docs/zh-cn/integrations.mdx
+++ b/docs/next/website/src/content/docs/zh-cn/integrations.mdx
@@ -76,7 +76,7 @@ Herdr 把内置扩展写入:
~/.pi/agent/extensions/herdr-agent-state.ts
```
-如果设置了 `PI_CODING_AGENT_DIR`,Herdr 会改为写入 `$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts`。extensions 目录必须已经存在。卸载只删除那个扩展文件。
+如果设置了 `PI_CODING_AGENT_DIR`,Herdr 会改为写入 `$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts`。如果 Pi agent 目录已经存在,Herdr 会创建 extensions 目录。卸载只删除那个扩展文件。
## OMP
@@ -92,7 +92,7 @@ Herdr 把内置扩展写入:
~/.omp/agent/extensions/herdr-omp-agent-state.ts
```
-如果设置了 `PI_CODING_AGENT_DIR`,Herdr 会改为写入 `$PI_CODING_AGENT_DIR/extensions/herdr-omp-agent-state.ts`。extensions 目录必须已经存在。卸载只删除那个扩展文件。
+如果设置了 `PI_CODING_AGENT_DIR`,Herdr 会改为写入 `$PI_CODING_AGENT_DIR/extensions/herdr-omp-agent-state.ts`。如果 OMP agent 目录已经存在,Herdr 会创建 extensions 目录。卸载只删除那个扩展文件。
OMP 集成通过 Herdr 的 socket API 上报智能体标签 `omp`、生命周期状态和原生会话身份。它不需要对 `omp` 可执行文件做原生进程检测,并且 Herdr 可以在服务器重启后用 `omp --resume=<session>` 恢复 OMP 窗格。
diff --git a/src/integration/assets/herdr-agent-state.test.ts b/src/integration/assets/herdr-agent-state.test.ts
index c662e132..50f1fc52 100644
--- a/src/integration/assets/herdr-agent-state.test.ts
+++ b/src/integration/assets/herdr-agent-state.test.ts
@@ -1,9 +1,11 @@
import { afterEach, expect, test } from "bun:test";
import { rm } from "node:fs/promises";
-import { createServer, type Server } from "node:net";
+import net, { createServer, type Server } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
+const originalPlatform = process.platform;
+const originalCreateConnection = net.createConnection;
const originalEnvironment = {
HERDR_ENV: process.env.HERDR_ENV,
HERDR_OMP_IDLE_DEBOUNCE_MS: process.env.HERDR_OMP_IDLE_DEBOUNCE_MS,
@@ -30,6 +32,8 @@ afterEach(async () => {
socketPath = undefined;
}
+ Object.defineProperty(process, "platform", { value: originalPlatform });
+ net.createConnection = originalCreateConnection;
for (const [name, value] of Object.entries(originalEnvironment)) {
if (value === undefined) {
delete process.env[name];
@@ -44,6 +48,15 @@ const integrations = [
{ name: "Oh My Pi", modulePath: "./omp/herdr-agent-state.ts" },
] as const;
+const socketPlugins = [
+ {
+ name: "OpenCode",
+ modulePath: "./opencode/herdr-agent-state.js",
+ sessionID: "opencode-session",
+ },
+ { name: "Kilo", modulePath: "./kilo/herdr-agent-state.js", sessionID: "kilo-session" },
+] as const;
+
function importFresh(modulePath: string) {
importCounter += 1;
return import(`${modulePath}?test=${importCounter}`);
@@ -74,6 +87,15 @@ function configureIntegrationEnvironment(recordingSocketPath: string) {
process.env.HERDR_PANE_ID = "test:p1";
}
+function captureConnectionEndpoint() {
+ let connectedEndpoint: unknown;
+ net.createConnection = ((...args: unknown[]) => {
+ connectedEndpoint = args[0];
+ return Reflect.apply(originalCreateConnection, net, args);
+ }) as typeof net.createConnection;
+ return () => connectedEndpoint;
+}
+
async function startRecordingServer(name: string): Promise<unknown[]> {
const recordingSocketPath = join(tmpdir(), `herdr-${name}-${process.pid}.sock`);
socketPath = recordingSocketPath;
@@ -102,7 +124,61 @@ async function startRecordingServer(name: string): Promise<unknown[]> {
return requests;
}
+for (const socketPlugin of socketPlugins) {
+ test(`${socketPlugin.name} maps the Windows socket marker path to a named pipe endpoint`, async () => {
+ const markerPath = `herdr-${socketPlugin.name.toLowerCase()}-${process.pid}.sock`;
+ configureIntegrationEnvironment(markerPath);
+ Object.defineProperty(process, "platform", { value: "win32" });
+ const connectedEndpoint = captureConnectionEndpoint();
+
+ const { HerdrAgentStatePlugin } = await importFresh(socketPlugin.modulePath);
+ const plugin = await HerdrAgentStatePlugin();
+ await plugin.event({
+ event: {
+ type: "session.updated",
+ properties: { sessionID: socketPlugin.sessionID },
+ },
+ });
+
+ expect(connectedEndpoint()).toBe(`\\\\.\\pipe\\${markerPath}`);
+ });
+}
+
+test("OpenCode stays disabled without the Herdr socket environment", async () => {
+ process.env.HERDR_ENV = "1";
+ process.env.HERDR_PANE_ID = "test:p1";
+ delete process.env.HERDR_SOCKET_PATH;
+
+ const { HerdrAgentStatePlugin } = await importFresh("./opencode/herdr-agent-state.js");
+
+ expect(await HerdrAgentStatePlugin()).toEqual({});
+});
+
for (const integration of integrations) {
+ test(`${integration.name} maps the Windows socket marker path to a named pipe endpoint`, async () => {
+ const markerPath = `herdr-${integration.name.toLowerCase().replaceAll(" ", "-")}-${process.pid}.sock`;
+ configureIntegrationEnvironment(markerPath);
+ Object.defineProperty(process, "platform", { value: "win32" });
+ const connectedEndpoint = captureConnectionEndpoint();
+ const { handlers, pi } = createExtensionHarness();
+
+ const { default: install } = await importFresh(integration.modulePath);
+ install(pi);
+ await handlers.get("session_start")?.(
+ { reason: "startup" },
+ {
+ hasUI: true,
+ isIdle: () => true,
+ sessionManager: {
+ getSessionFile: () => undefined,
+ getSessionId: () => "test-session",
+ },
+ },
+ );
+
+ expect(connectedEndpoint()).toBe(`\\\\.\\pipe\\${markerPath}`);
+ });
+
test(`${integration.name} reload preserves working state when the agent is active`, async () => {
const requests = await startRecordingServer(
integration.name.toLowerCase().replaceAll(" ", "-"),
diff --git a/src/integration/assets/kilo/herdr-agent-state.js b/src/integration/assets/kilo/herdr-agent-state.js
index fc12919c..e8600ab1 100644
--- a/src/integration/assets/kilo/herdr-agent-state.js
+++ b/src/integration/assets/kilo/herdr-agent-state.js
@@ -2,7 +2,7 @@
// managed by herdr; reinstalling or updating the integration overwrites this file.
// add custom hooks/plugins beside this file instead of editing it.
// HERDR_INTEGRATION_ID=kilo
-// HERDR_INTEGRATION_VERSION=2
+// HERDR_INTEGRATION_VERSION=3
import net from "node:net";
@@ -48,6 +48,9 @@ function request(method, params) {
return Promise.resolve();
}
+ const socketEndpoint =
+ process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
+
const requestId = `${SOURCE}:${Date.now()}:${Math.floor(Math.random() * 1_000_000)
.toString()
.padStart(6, "0")}`;
@@ -64,7 +67,7 @@ function request(method, params) {
};
return new Promise((resolve) => {
- const client = net.createConnection(socketPath, () => {
+ const client = net.createConnection(socketEndpoint, () => {
client.write(`${JSON.stringify(request)}\n`);
});
diff --git a/src/integration/assets/omp/herdr-agent-state.ts b/src/integration/assets/omp/herdr-agent-state.ts
index 9bb6671a..350cdea7 100644
--- a/src/integration/assets/omp/herdr-agent-state.ts
+++ b/src/integration/assets/omp/herdr-agent-state.ts
@@ -2,13 +2,15 @@
// managed by herdr; reinstalling or updating the integration overwrites this file.
// add custom hooks/plugins beside this file instead of editing it.
// HERDR_INTEGRATION_ID=omp
-// HERDR_INTEGRATION_VERSION=5
+// HERDR_INTEGRATION_VERSION=6
// @ts-nocheck
-import { createConnection } from "node:net";
+import net from "node:net";
const HERDR_ENV = process.env.HERDR_ENV;
const socketPath = process.env.HERDR_SOCKET_PATH;
+const socketEndpoint =
+ process.platform === "win32" && socketPath ? `\\\\.\\pipe\\${socketPath}` : socketPath;
const paneId = process.env.HERDR_PANE_ID;
const source = "herdr:omp";
@@ -36,7 +38,7 @@ function sendRequestAttempt(request: unknown, timeoutMs: number): Promise<boolea
resolve(delivered);
};
- const socket = createConnection(socketPath!);
+ const socket = net.createConnection(socketEndpoint!);
socket.on("error", () => finish(false));
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
socket.on("data", () => finish(true));
diff --git a/src/integration/assets/opencode/herdr-agent-state.js b/src/integration/assets/opencode/herdr-agent-state.js
index 4d59f9ec..27ea7ee5 100644
--- a/src/integration/assets/opencode/herdr-agent-state.js
+++ b/src/integration/assets/opencode/herdr-agent-state.js
@@ -62,6 +62,9 @@ function requestOnce(method, params) {
return Promise.resolve();
}
+ const socketEndpoint =
+ process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
+
const requestId = `${SOURCE}:${Date.now()}:${Math.floor(Math.random() * 1_000_000)
.toString()
.padStart(6, "0")}`;
@@ -78,7 +81,7 @@ function requestOnce(method, params) {
};
return new Promise((resolve) => {
- const client = net.createConnection(socketPath, () => {
+ const client = net.createConnection(socketEndpoint, () => {
client.write(`${JSON.stringify(request)}\n`);
});
diff --git a/src/integration/assets/pi/herdr-agent-state.ts b/src/integration/assets/pi/herdr-agent-state.ts
index 52340ebb..218cede9 100644
--- a/src/integration/assets/pi/herdr-agent-state.ts
+++ b/src/integration/assets/pi/herdr-agent-state.ts
@@ -2,13 +2,15 @@
// managed by herdr; reinstalling or updating the integration overwrites this file.
// add custom hooks/plugins beside this file instead of editing it.
// HERDR_INTEGRATION_ID=pi
-// HERDR_INTEGRATION_VERSION=5
+// HERDR_INTEGRATION_VERSION=6
// @ts-nocheck
-import { createConnection } from "node:net";
+import net from "node:net";
const HERDR_ENV = process.env.HERDR_ENV;
const socketPath = process.env.HERDR_SOCKET_PATH;
+const socketEndpoint =
+ process.platform === "win32" && socketPath ? `\\\\.\\pipe\\${socketPath}` : socketPath;
const paneId = process.env.HERDR_PANE_ID;
const source = "herdr:pi";
@@ -34,7 +36,7 @@ function sendRequestAttempt(request: unknown, timeoutMs: number): Promise<boolea
resolve(delivered);
};
- const socket = createConnection(socketPath!);
+ const socket = net.createConnection(socketEndpoint!);
socket.on("error", () => finish(false));
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
socket.on("data", () => finish(true));
diff --git a/src/integration/mod.rs b/src/integration/mod.rs
index 66c62d7a..fdc1c5bd 100644
--- a/src/integration/mod.rs
+++ b/src/integration/mod.rs
@@ -22,10 +22,10 @@ pub(crate) use types::{IntegrationRecommendation, IntegrationStatus, Integration
const PI_EXTENSION_INSTALL_NAME: &str = "herdr-agent-state.ts";
const PI_EXTENSION_ASSET: &str = include_str!("assets/pi/herdr-agent-state.ts");
-const PI_INTEGRATION_VERSION: u32 = 5;
+const PI_INTEGRATION_VERSION: u32 = 6;
const OMP_EXTENSION_INSTALL_NAME: &str = "herdr-omp-agent-state.ts";
const OMP_EXTENSION_ASSET: &str = include_str!("assets/omp/herdr-agent-state.ts");
-const OMP_INTEGRATION_VERSION: u32 = 5;
+const OMP_INTEGRATION_VERSION: u32 = 6;
const CLAUDE_HOOK_INSTALL_NAME: &str = if cfg!(windows) {
"herdr-agent-state.ps1"
} else {
@@ -143,7 +143,7 @@ const OPENCODE_PLUGIN_ASSET: &str = include_str!("assets/opencode/herdr-agent-st
const OPENCODE_INTEGRATION_VERSION: u32 = 9;
const KILO_PLUGIN_INSTALL_NAME: &str = "herdr-agent-state.js";
const KILO_PLUGIN_ASSET: &str = include_str!("assets/kilo/herdr-agent-state.js");
-const KILO_INTEGRATION_VERSION: u32 = 2;
+const KILO_INTEGRATION_VERSION: u32 = 3;
const HERMES_PLUGIN_INSTALL_NAME: &str = "herdr-agent-state";
const HERMES_PLUGIN_MANIFEST_INSTALL_NAME: &str = "plugin.yaml";
const HERMES_PLUGIN_INIT_INSTALL_NAME: &str = "__init__.py";
diff --git a/src/integration/registry.rs b/src/integration/registry.rs
index 84e42a25..bcd17220 100644
--- a/src/integration/registry.rs
+++ b/src/integration/registry.rs
@@ -61,9 +61,13 @@ pub(crate) fn integration_target_supported(target: crate::api::schema::Integrati
{
matches!(
target,
- crate::api::schema::IntegrationTarget::Claude
+ crate::api::schema::IntegrationTarget::Pi
+ | crate::api::schema::IntegrationTarget::Omp
+ | crate::api::schema::IntegrationTarget::Claude
| crate::api::schema::IntegrationTarget::Codex
| crate::api::schema::IntegrationTarget::Copilot
+ | crate::api::schema::IntegrationTarget::Opencode
+ | crate::api::schema::IntegrationTarget::Kilo
| crate::api::schema::IntegrationTarget::Droid
| crate::api::schema::IntegrationTarget::Kimi
| crate::api::schema::IntegrationTarget::Qodercli
diff --git a/src/integration/targets.rs b/src/integration/targets.rs
index 2b4c5615..99c150a3 100644
--- a/src/integration/targets.rs
+++ b/src/integration/targets.rs
@@ -45,14 +45,22 @@ use super::{
QODERCLI_HOOK_INSTALL_NAME, QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS,
};
+fn ensure_extension_dir(dir: &Path, agent: &str) -> io::Result<()> {
+ if dir.is_dir() {
+ return Ok(());
+ }
+ if dir.parent().is_some_and(|parent| parent.is_dir()) {
+ return fs::create_dir_all(dir);
+ }
+ Err(io::Error::other(format!(
+ "{agent} extension directory not found at {}. install {agent} first",
+ dir.display()
+ )))
+}
+
pub(crate) fn install_pi() -> io::Result<PathBuf> {
let dir = pi_extension_dir()?;
- if !dir.is_dir() {
- return Err(io::Error::other(format!(
- "pi extension directory not found at {}. install pi and create the extensions directory first",
- dir.display()
- )));
- }
+ ensure_extension_dir(&dir, "pi")?;
let path = dir.join(PI_EXTENSION_INSTALL_NAME);
fs::write(&path, PI_EXTENSION_ASSET)?;
@@ -61,23 +69,7 @@ pub(crate) fn install_pi() -> io::Result<PathBuf> {
pub(crate) fn install_omp() -> io::Result<OmpInstallPaths> {
let dir = omp_extension_dir()?;
- if !dir.is_dir() {
- if dir.parent().is_some_and(|parent| parent.is_dir()) {
- fs::create_dir_all(&dir)?;
- } else {
- return Err(io::Error::other(format!(
- "omp extension directory not found at {}. install omp and create the extensions directory first",
- dir.display()
- )));
- }
- }
-
- if !dir.is_dir() {
- return Err(io::Error::other(format!(
- "omp extension directory not found at {}. install omp and create the extensions directory first",
- dir.display()
- )));
- }
+ ensure_extension_dir(&dir, "omp")?;
let removed_legacy_pi_extension = remove_legacy_pi_extension_from_omp_dir(&dir)?;
let extension_path = dir.join(OMP_EXTENSION_INSTALL_NAME);
diff --git a/src/integration/tests.rs b/src/integration/tests.rs
index 3d578c53..41714ffa 100644
--- a/src/integration/tests.rs
+++ b/src/integration/tests.rs
@@ -165,21 +165,21 @@ fn home_dir_uses_userprofile_when_home_is_missing() {
#[cfg(windows)]
#[test]
-fn windows_supports_only_cli_hook_integrations() {
+fn windows_supports_portable_integrations() {
use crate::api::schema::IntegrationTarget;
- assert!(!integration_target_supported(IntegrationTarget::Pi));
- assert!(!integration_target_supported(IntegrationTarget::Omp));
- assert!(!integration_target_supported(IntegrationTarget::Opencode));
- assert!(!integration_target_supported(IntegrationTarget::Kilo));
assert!(!integration_target_supported(IntegrationTarget::Hermes));
assert!(!integration_target_supported(IntegrationTarget::Cursor));
assert!(!integration_target_supported(IntegrationTarget::Devin));
assert!(!integration_target_supported(IntegrationTarget::Mastracode));
+ assert!(integration_target_supported(IntegrationTarget::Pi));
+ assert!(integration_target_supported(IntegrationTarget::Omp));
assert!(integration_target_supported(IntegrationTarget::Claude));
assert!(integration_target_supported(IntegrationTarget::Codex));
assert!(integration_target_supported(IntegrationTarget::Copilot));
+ assert!(integration_target_supported(IntegrationTarget::Opencode));
+ assert!(integration_target_supported(IntegrationTarget::Kilo));
assert!(integration_target_supported(IntegrationTarget::Droid));
assert!(integration_target_supported(IntegrationTarget::Kimi));
assert!(integration_target_supported(IntegrationTarget::Qodercli));
@@ -187,7 +187,7 @@ fn windows_supports_only_cli_hook_integrations() {
#[cfg(windows)]
#[test]
-fn windows_does_not_offer_unsupported_integrations_even_when_commands_exist() {
+fn windows_availability_excludes_unsupported_integrations() {
use crate::api::schema::IntegrationTarget;
let _lock = integration_env_lock();
@@ -206,10 +206,10 @@ fn windows_does_not_offer_unsupported_integrations_even_when_commands_exist() {
fs::write(bin.join("devin.cmd"), "@echo off\r\n").unwrap();
fs::write(bin.join("mastracode.cmd"), "@echo off\r\n").unwrap();
- assert!(!integration_target_available(IntegrationTarget::Pi));
- assert!(!integration_target_available(IntegrationTarget::Omp));
- assert!(!integration_target_available(IntegrationTarget::Opencode));
- assert!(!integration_target_available(IntegrationTarget::Kilo));
+ assert!(integration_target_available(IntegrationTarget::Pi));
+ assert!(integration_target_available(IntegrationTarget::Omp));
+ assert!(integration_target_available(IntegrationTarget::Opencode));
+ assert!(integration_target_available(IntegrationTarget::Kilo));
assert!(!integration_target_available(IntegrationTarget::Hermes));
assert!(!integration_target_available(IntegrationTarget::Cursor));
assert!(!integration_target_available(IntegrationTarget::Devin));
@@ -238,10 +238,10 @@ fn windows_install_rejects_unsupported_integration_before_config_lookup() {
std::env::remove_var("HOMEDRIVE");
std::env::remove_var("HOMEPATH");
- let err = install_target(IntegrationTarget::Pi).unwrap_err();
+ let err = install_target(IntegrationTarget::Hermes).unwrap_err();
assert_eq!(
err.to_string(),
- "pi integration is not supported on Windows"
+ "hermes integration is not supported on Windows"
);
if let Some(home) = original_home {
@@ -487,6 +487,27 @@ fn install_pi_writes_embedded_asset_to_pi_extensions_dir() {
}
#[test]
+fn install_pi_creates_extensions_dir_when_agent_dir_exists() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let home = base.join("home");
+ let agent_dir = home.join(".pi/agent");
+ fs::create_dir_all(&agent_dir).unwrap();
+ std::env::set_var("HOME", &home);
+
+ let path = install_pi().unwrap();
+
+ assert_eq!(
+ path,
+ agent_dir.join("extensions").join(PI_EXTENSION_INSTALL_NAME)
+ );
+ assert!(path.is_file());
+
+ std::env::remove_var("HOME");
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
fn install_pi_uses_pi_coding_agent_dir_env() {
let _lock = integration_env_lock();
let base = unique_base();
diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs
index 0faac229..d218c230 100644
--- a/tests/cli_wrapper.rs
+++ b/tests/cli_wrapper.rs
@@ -1623,7 +1623,7 @@ fn integration_commands_run_locally_when_server_is_missing() {
.unwrap();
assert_eq!(integration_status.status.code(), Some(0));
let status_stdout = String::from_utf8_lossy(&integration_status.stdout);
- assert!(status_stdout.contains("pi: current (v5)"));
+ assert!(status_stdout.contains("pi: current (v6)"));
assert!(status_stdout.contains("claude: not installed"));
let integration_uninstall = Command::new(env!("CARGO_BIN_EXE_herdr"))