aboutsummaryrefslogtreecommitdiff
path: root/src/integration
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-13 03:00:35 +0300
committerGitHub <noreply@github.com>2026-08-13 03:00:35 +0300
commita4d52ab60282b2ad021cb90978a7ed4d3898704b (patch)
treecdd5f55d6d392d54541b86da7f1a00a48c211d03 /src/integration
parent8b635c918066dd184bacc29b4c06c40a3efea3f5 (diff)
feat: add qwen code detection and restore (#2743)
refs #2730
Diffstat (limited to 'src/integration')
-rw-r--r--src/integration/actions.rs44
-rw-r--r--src/integration/assets/qwen/herdr-agent-session.ps137
-rw-r--r--src/integration/assets/qwen/herdr-agent-session.sh48
-rw-r--r--src/integration/env.rs5
-rw-r--r--src/integration/mod.rs12
-rw-r--r--src/integration/registry.rs10
-rw-r--r--src/integration/targets.rs97
-rw-r--r--src/integration/tests.rs104
-rw-r--r--src/integration/types.rs14
9 files changed, 367 insertions, 4 deletions
diff --git a/src/integration/actions.rs b/src/integration/actions.rs
index 00bf8c8f..24d1f9e3 100644
--- a/src/integration/actions.rs
+++ b/src/integration/actions.rs
@@ -4,11 +4,11 @@ use super::registry::{integration_target_label, integration_target_supported};
use super::targets::{
install_antigravity_cli, install_claude, install_codex, install_copilot, install_cursor,
install_devin, install_droid, install_grok, install_hermes, install_kilo, install_kimi,
- install_mastracode, install_omp, install_opencode, install_pi, install_qodercli,
+ install_mastracode, install_omp, install_opencode, install_pi, install_qodercli, install_qwen,
uninstall_antigravity_cli, uninstall_claude, uninstall_codex, uninstall_copilot,
uninstall_cursor, uninstall_devin, uninstall_droid, uninstall_grok, uninstall_hermes,
uninstall_kilo, uninstall_kimi, uninstall_mastracode, uninstall_omp, uninstall_opencode,
- uninstall_pi, uninstall_qodercli,
+ uninstall_pi, uninstall_qodercli, uninstall_qwen,
};
use super::version::{agent_version_requirement, enforce_agent_version};
use super::{KIMI_MIN_VERSION, PI_EXTENSION_INSTALL_NAME};
@@ -192,6 +192,19 @@ fn install_target_inner(target: crate::api::schema::IntegrationTarget) -> io::Re
),
]
}
+ crate::api::schema::IntegrationTarget::Qwen => {
+ let installed = install_qwen()?;
+ vec![
+ format!(
+ "installed qwen integration hook to {}",
+ installed.hook_path.display()
+ ),
+ format!(
+ "ensured qwen settings at {}",
+ installed.settings_path.display()
+ ),
+ ]
+ }
crate::api::schema::IntegrationTarget::Cursor => {
let installed = install_cursor()?;
vec![
@@ -559,6 +572,33 @@ pub(crate) fn uninstall_target(
}
messages
}
+ crate::api::schema::IntegrationTarget::Qwen => {
+ let result = uninstall_qwen()?;
+ let mut messages = Vec::new();
+ if result.removed_hook_file {
+ messages.push(format!(
+ "removed qwen hook at {}",
+ result.hook_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no qwen hook found at {}",
+ result.hook_path.display()
+ ));
+ }
+ if result.updated_settings {
+ messages.push(format!(
+ "removed herdr qwen hook entries from {}",
+ result.settings_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no herdr qwen hook entries found in {}",
+ result.settings_path.display()
+ ));
+ }
+ messages
+ }
crate::api::schema::IntegrationTarget::Cursor => {
let result = uninstall_cursor()?;
let mut messages = Vec::new();
diff --git a/src/integration/assets/qwen/herdr-agent-session.ps1 b/src/integration/assets/qwen/herdr-agent-session.ps1
new file mode 100644
index 00000000..882caf86
--- /dev/null
+++ b/src/integration/assets/qwen/herdr-agent-session.ps1
@@ -0,0 +1,37 @@
+# installed by herdr
+# managed by herdr; reinstalling or updating the integration overwrites this file.
+# add custom hooks beside this file instead of editing it.
+# HERDR_INTEGRATION_ID=qwen
+# HERDR_INTEGRATION_VERSION=1
+
+param([string]$Action = "")
+
+if ($Action -ne "session") { exit 0 }
+if ($env:HERDR_ENV -ne "1") { exit 0 }
+if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
+if ([string]::IsNullOrWhiteSpace($env:HERDR_SOCKET_PATH)) { exit 0 }
+
+$inputText = [Console]::In.ReadToEnd()
+try {
+ $payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
+} catch {
+ $payload = $null
+}
+
+if ($null -eq $payload -or [string]::IsNullOrWhiteSpace($payload.session_id)) { exit 0 }
+
+$seq = [DateTime]::UtcNow.Ticks
+$herdr = if ([string]::IsNullOrWhiteSpace($env:HERDR_BIN_PATH)) { "herdr" } else { $env:HERDR_BIN_PATH }
+$commandArgs = @(
+ "pane", "report-agent-session", $env:HERDR_PANE_ID,
+ "--source", "herdr:qwen", "--agent", "qwen",
+ "--agent-session-id", [string]$payload.session_id,
+ "--seq", [string]$seq
+)
+if ($payload.source -in @("startup", "resume", "clear", "compact", "branch")) {
+ $commandArgs += @("--session-start-source", [string]$payload.source)
+}
+try {
+ & $herdr @commandArgs 2>$null | Out-Null
+} catch {
+}
diff --git a/src/integration/assets/qwen/herdr-agent-session.sh b/src/integration/assets/qwen/herdr-agent-session.sh
new file mode 100644
index 00000000..226c2a97
--- /dev/null
+++ b/src/integration/assets/qwen/herdr-agent-session.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# managed by herdr; reinstalling the integration replaces this file.
+# HERDR_INTEGRATION_ID=qwen
+# HERDR_INTEGRATION_VERSION=1
+
+[ "${1:-}" = "session" ] || exit 0
+[ "${HERDR_ENV:-}" = "1" ] || exit 0
+[ -n "${HERDR_PANE_ID:-}" ] || exit 0
+[ -n "${HERDR_SOCKET_PATH:-}" ] || exit 0
+if [ -n "${HERDR_BIN_PATH:-}" ]; then
+ [ -x "$HERDR_BIN_PATH" ] || exit 0
+else
+ command -v herdr >/dev/null 2>&1 || exit 0
+fi
+command -v python3 >/dev/null 2>&1 || exit 0
+
+python3 -c '
+import json
+import os
+import subprocess
+import sys
+import time
+
+try:
+ payload = json.load(sys.stdin)
+ session_id = payload.get("session_id")
+ source = payload.get("source")
+ if not isinstance(session_id, str) or not session_id:
+ raise ValueError
+ command = os.environ.get("HERDR_BIN_PATH") or "herdr"
+ args = [
+ command, "pane", "report-agent-session", os.environ["HERDR_PANE_ID"],
+ "--source", "herdr:qwen", "--agent", "qwen",
+ "--agent-session-id", session_id, "--seq", str(time.time_ns()),
+ ]
+ if source in ("startup", "resume", "clear", "compact", "branch"):
+ args.extend(["--session-start-source", source])
+ subprocess.run(
+ args,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ timeout=1,
+ check=False,
+ )
+except Exception:
+ pass
+' 2>/dev/null || true
diff --git a/src/integration/env.rs b/src/integration/env.rs
index e86f6ee8..8f3a9e65 100644
--- a/src/integration/env.rs
+++ b/src/integration/env.rs
@@ -16,6 +16,7 @@ pub(crate) const CODEX_HOME_ENV_VAR: &str = "CODEX_HOME";
pub(crate) const KIMI_CODE_HOME_ENV_VAR: &str = "KIMI_CODE_HOME";
pub(crate) const COPILOT_HOME_ENV_VAR: &str = "COPILOT_HOME";
pub(crate) const QODERCLI_CONFIG_DIR_ENV_VAR: &str = "QODER_CONFIG_DIR";
+pub(crate) const QWEN_HOME_ENV_VAR: &str = "QWEN_HOME";
pub(crate) const CURSOR_CONFIG_DIR_ENV_VAR: &str = "CURSOR_CONFIG_DIR";
pub(crate) const ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR: &str = "ANTIGRAVITY_CLI_CONFIG_DIR";
pub(crate) const GROK_CONFIG_DIR_ENV_VAR: &str = "GROK_CONFIG_DIR";
@@ -157,6 +158,10 @@ pub(crate) fn qodercli_dir() -> io::Result<PathBuf> {
config_dir_from_env_or_home(QODERCLI_CONFIG_DIR_ENV_VAR, &[".qoder"])
}
+pub(crate) fn qwen_dir() -> io::Result<PathBuf> {
+ config_dir_from_env_or_home(QWEN_HOME_ENV_VAR, &[".qwen"])
+}
+
pub(crate) fn cursor_dir() -> io::Result<PathBuf> {
config_dir_from_env_or_home(CURSOR_CONFIG_DIR_ENV_VAR, &[".cursor"])
}
diff --git a/src/integration/mod.rs b/src/integration/mod.rs
index 597aabc5..c87ccac6 100644
--- a/src/integration/mod.rs
+++ b/src/integration/mod.rs
@@ -192,6 +192,18 @@ const QODERCLI_HOOK_ASSET: &str = if cfg!(windows) {
};
const QODERCLI_INTEGRATION_VERSION: u32 = 3;
const QODERCLI_HOOK_EVENTS: [(&str, &str); 1] = [("SessionStart", "session")];
+const QWEN_HOOK_INSTALL_NAME: &str = if cfg!(windows) {
+ "herdr-agent-session.ps1"
+} else {
+ "herdr-agent-session.sh"
+};
+const QWEN_HOOK_ASSET: &str = if cfg!(windows) {
+ include_str!("assets/qwen/herdr-agent-session.ps1")
+} else {
+ include_str!("assets/qwen/herdr-agent-session.sh")
+};
+const QWEN_INTEGRATION_VERSION: u32 = 1;
+const QWEN_HOOK_EVENTS: [(&str, &str); 1] = [("SessionStart", "session")];
const QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS: [(&str, &str); 12] = [
("SessionStart", "idle"),
("UserPromptSubmit", "working"),
diff --git a/src/integration/registry.rs b/src/integration/registry.rs
index b7bb3bf2..46251970 100644
--- a/src/integration/registry.rs
+++ b/src/integration/registry.rs
@@ -20,6 +20,7 @@ pub(crate) fn integration_target_label(
crate::api::schema::IntegrationTarget::Kilo => "kilo",
crate::api::schema::IntegrationTarget::Hermes => "hermes",
crate::api::schema::IntegrationTarget::Qodercli => "qodercli",
+ crate::api::schema::IntegrationTarget::Qwen => "qwen",
crate::api::schema::IntegrationTarget::Cursor => "cursor",
crate::api::schema::IntegrationTarget::Mastracode => "mastracode",
crate::api::schema::IntegrationTarget::AntigravityCli => "antigravity-cli",
@@ -49,6 +50,7 @@ pub(crate) fn integration_target_command_names(
crate::api::schema::IntegrationTarget::Kilo => &["kilo", "kilo-code"],
crate::api::schema::IntegrationTarget::Hermes => &["hermes"],
crate::api::schema::IntegrationTarget::Qodercli => qodercli_command_names(),
+ crate::api::schema::IntegrationTarget::Qwen => &["qwen"],
crate::api::schema::IntegrationTarget::Cursor => cursor_command_names(),
crate::api::schema::IntegrationTarget::Mastracode => &["mastracode"],
crate::api::schema::IntegrationTarget::AntigravityCli => &["agy"],
@@ -75,6 +77,7 @@ pub(crate) fn integration_target_supported(target: crate::api::schema::Integrati
| crate::api::schema::IntegrationTarget::Droid
| crate::api::schema::IntegrationTarget::Kimi
| crate::api::schema::IntegrationTarget::Qodercli
+ | crate::api::schema::IntegrationTarget::Qwen
| crate::api::schema::IntegrationTarget::AntigravityCli
| crate::api::schema::IntegrationTarget::Devin
| crate::api::schema::IntegrationTarget::Hermes
@@ -264,7 +267,7 @@ fn integration_specs() -> [(
crate::api::schema::IntegrationTarget,
io::Result<PathBuf>,
u32,
-); 16] {
+); 17] {
[
(
crate::api::schema::IntegrationTarget::Pi,
@@ -330,6 +333,11 @@ fn integration_specs() -> [(
super::QODERCLI_INTEGRATION_VERSION,
),
(
+ crate::api::schema::IntegrationTarget::Qwen,
+ qwen_dir().map(|dir| dir.join("hooks").join(super::QWEN_HOOK_INSTALL_NAME)),
+ super::QWEN_INTEGRATION_VERSION,
+ ),
+ (
crate::api::schema::IntegrationTarget::Cursor,
cursor_dir().map(|dir| dir.join(super::CURSOR_HOOK_INSTALL_NAME)),
super::CURSOR_INTEGRATION_VERSION,
diff --git a/src/integration/targets.rs b/src/integration/targets.rs
index 375d2713..0150ccaa 100644
--- a/src/integration/targets.rs
+++ b/src/integration/targets.rs
@@ -20,7 +20,7 @@ use super::config_edit::{
use super::env::{
antigravity_cli_dir, claude_dir, codex_dir, copilot_dir, cursor_dir, devin_dir, droid_dir,
grok_dir, hermes_dir, hermes_plugin_dir, kilo_dir, kimi_dir, mastracode_dir, omp_extension_dir,
- opencode_dir, pi_extension_dir, qodercli_dir,
+ opencode_dir, pi_extension_dir, qodercli_dir, qwen_dir,
};
use super::file_ops::{
make_executable, remove_dir_all_if_exists, remove_file_if_exists, remove_legacy_bash_hook_file,
@@ -37,6 +37,7 @@ use super::types::{
KiloUninstallResult, KimiInstallPaths, KimiUninstallResult, MastracodeInstallPaths,
MastracodeUninstallResult, OmpInstallPaths, OmpUninstallResult, OpenCodeInstallPaths,
OpenCodeUninstallResult, PiUninstallResult, QodercliInstallPaths, QodercliUninstallResult,
+ QwenInstallPaths, QwenUninstallResult,
};
use super::{
ANTIGRAVITY_CLI_HOOK_ASSET, ANTIGRAVITY_CLI_HOOK_BLOCK_NAME, ANTIGRAVITY_CLI_HOOK_EVENTS,
@@ -55,6 +56,7 @@ use super::{
OPENCODE_PLUGIN_INSTALL_NAME, OPENCODE_TUI_PLUGIN_ASSET, OPENCODE_TUI_PLUGIN_INSTALL_NAME,
OPENCODE_TUI_PLUGIN_SPEC, PI_EXTENSION_ASSET, PI_EXTENSION_INSTALL_NAME, QODERCLI_HOOK_ASSET,
QODERCLI_HOOK_EVENTS, QODERCLI_HOOK_INSTALL_NAME, QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS,
+ QWEN_HOOK_ASSET, QWEN_HOOK_EVENTS, QWEN_HOOK_INSTALL_NAME,
};
fn ensure_extension_dir(dir: &Path, agent: &str) -> io::Result<()> {
@@ -945,6 +947,59 @@ pub(crate) fn install_qodercli() -> io::Result<QodercliInstallPaths> {
})
}
+pub(crate) fn install_qwen() -> io::Result<QwenInstallPaths> {
+ let dir = qwen_dir()?;
+ if !dir.is_dir() {
+ return Err(io::Error::other(format!(
+ "qwen code config directory not found at {}. install qwen code first",
+ dir.display()
+ )));
+ }
+
+ let hooks_dir = dir.join("hooks");
+ fs::create_dir_all(&hooks_dir)?;
+
+ let hook_path = hooks_dir.join(QWEN_HOOK_INSTALL_NAME);
+ fs::write(&hook_path, QWEN_HOOK_ASSET)?;
+ make_executable(&hook_path)?;
+
+ let settings_path = dir.join("settings.json");
+ let mut settings = if settings_path.is_file() {
+ serde_json::from_str::<Value>(&fs::read_to_string(&settings_path)?).map_err(|err| {
+ io::Error::other(format!(
+ "failed to parse {}: {err}",
+ settings_path.display()
+ ))
+ })?
+ } else {
+ json!({})
+ };
+
+ let hooks = ensure_hooks_object(
+ &mut settings,
+ &settings_path,
+ "qwen settings",
+ "qwen settings hooks",
+ )?;
+ for (event, action) in QWEN_HOOK_EVENTS {
+ remove_hook_commands(hooks, event, &hook_path, Some(action))?;
+ ensure_command_hook(
+ hooks,
+ event,
+ hook_command(&hook_path, Some(action)),
+ 10_000,
+ Some("*"),
+ )?;
+ }
+
+ fs::write(&settings_path, serde_json::to_string_pretty(&settings)?)?;
+
+ Ok(QwenInstallPaths {
+ hook_path,
+ settings_path,
+ })
+}
+
pub(crate) fn install_cursor() -> io::Result<CursorInstallPaths> {
let dir = cursor_dir()?;
if !dir.is_dir() {
@@ -1047,6 +1102,46 @@ pub(crate) fn uninstall_qodercli() -> io::Result<QodercliUninstallResult> {
})
}
+pub(crate) fn uninstall_qwen() -> io::Result<QwenUninstallResult> {
+ let hook_path = qwen_dir()?.join("hooks").join(QWEN_HOOK_INSTALL_NAME);
+ let settings_path = qwen_dir()?.join("settings.json");
+ let mut updated_settings = false;
+
+ if settings_path.is_file() {
+ let mut settings = serde_json::from_str::<Value>(&fs::read_to_string(&settings_path)?)
+ .map_err(|err| {
+ io::Error::other(format!(
+ "failed to parse {}: {err}",
+ settings_path.display()
+ ))
+ })?;
+
+ if let Some(hooks) = hooks_object_if_present(
+ &mut settings,
+ &settings_path,
+ "qwen settings",
+ "qwen settings hooks",
+ )? {
+ for (event, action) in QWEN_HOOK_EVENTS {
+ updated_settings |= remove_hook_commands(hooks, event, &hook_path, Some(action))?;
+ }
+ }
+
+ if updated_settings {
+ fs::write(&settings_path, serde_json::to_string_pretty(&settings)?)?;
+ }
+ }
+
+ let removed_hook_file = remove_file_if_exists(&hook_path)?;
+
+ Ok(QwenUninstallResult {
+ hook_path,
+ settings_path,
+ removed_hook_file,
+ updated_settings,
+ })
+}
+
pub(crate) fn uninstall_cursor() -> io::Result<CursorUninstallResult> {
let cursor_home = cursor_dir()?;
let hook_path = cursor_home.join(CURSOR_HOOK_INSTALL_NAME);
diff --git a/src/integration/tests.rs b/src/integration/tests.rs
index 324ae6ce..d54183f2 100644
--- a/src/integration/tests.rs
+++ b/src/integration/tests.rs
@@ -101,6 +101,7 @@ fn clear_integration_path_env() {
std::env::remove_var(KIMI_CODE_HOME_ENV_VAR);
std::env::remove_var("XDG_CONFIG_HOME");
std::env::remove_var(QODERCLI_CONFIG_DIR_ENV_VAR);
+ std::env::remove_var(QWEN_HOME_ENV_VAR);
std::env::remove_var(CURSOR_CONFIG_DIR_ENV_VAR);
std::env::remove_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR);
std::env::remove_var(GROK_CONFIG_DIR_ENV_VAR);
@@ -195,6 +196,7 @@ fn windows_supports_portable_integrations() {
assert!(integration_target_supported(IntegrationTarget::Droid));
assert!(integration_target_supported(IntegrationTarget::Kimi));
assert!(integration_target_supported(IntegrationTarget::Qodercli));
+ assert!(integration_target_supported(IntegrationTarget::Qwen));
}
#[cfg(windows)]
@@ -3187,6 +3189,108 @@ fn install_qodercli_errors_when_config_dir_missing() {
}
#[test]
+fn install_qwen_writes_session_hook_and_preserves_settings() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let qwen_dir = base.join(".qwen");
+ fs::create_dir_all(&qwen_dir).unwrap();
+ fs::write(
+ qwen_dir.join("settings.json"),
+ r#"{"permissions":{"allow":["Read"]},"hooks":{}}"#,
+ )
+ .unwrap();
+ std::env::set_var(QWEN_HOME_ENV_VAR, &qwen_dir);
+
+ let installed = install_qwen().unwrap();
+
+ assert_eq!(
+ installed.hook_path,
+ qwen_dir.join("hooks").join(QWEN_HOOK_INSTALL_NAME)
+ );
+ assert_eq!(installed.settings_path, qwen_dir.join("settings.json"));
+ assert!(installed.hook_path.is_file());
+
+ let settings: Value =
+ serde_json::from_str(&fs::read_to_string(&installed.settings_path).unwrap()).unwrap();
+ let entries = settings["hooks"]["SessionStart"].as_array().unwrap();
+ assert_eq!(entries.len(), 1);
+ assert_eq!(entries[0]["matcher"], "*");
+ assert_eq!(entries[0]["hooks"][0]["timeout"], 10_000);
+ let command = entries[0]["hooks"][0]["command"].as_str().unwrap();
+ assert!(command.contains(QWEN_HOOK_INSTALL_NAME));
+ assert!(command.ends_with("session"));
+ assert!(settings.get("permissions").is_some());
+ let hook_asset = fs::read_to_string(&installed.hook_path).unwrap();
+ assert!(hook_asset.contains("HERDR_INTEGRATION_ID=qwen"));
+ assert!(hook_asset.contains("HERDR_INTEGRATION_VERSION=1"));
+ assert!(hook_asset.contains("herdr:qwen"));
+
+ install_qwen().unwrap();
+ let settings: Value =
+ serde_json::from_str(&fs::read_to_string(&installed.settings_path).unwrap()).unwrap();
+ assert_eq!(
+ settings["hooks"]["SessionStart"].as_array().unwrap().len(),
+ 1
+ );
+
+ std::env::remove_var(QWEN_HOME_ENV_VAR);
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
+fn uninstall_qwen_removes_only_herdr_hook() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let qwen_dir = base.join(".qwen");
+ fs::create_dir_all(&qwen_dir).unwrap();
+ std::env::set_var(QWEN_HOME_ENV_VAR, &qwen_dir);
+
+ install_qwen().unwrap();
+ let settings_path = qwen_dir.join("settings.json");
+ let mut settings: Value =
+ serde_json::from_str(&fs::read_to_string(&settings_path).unwrap()).unwrap();
+ settings["hooks"]["SessionStart"]
+ .as_array_mut()
+ .unwrap()
+ .push(json!({
+ "matcher": "resume",
+ "hooks": [{"type": "command", "command": "echo user-defined"}],
+ }));
+ fs::write(
+ &settings_path,
+ serde_json::to_string_pretty(&settings).unwrap(),
+ )
+ .unwrap();
+
+ let result = uninstall_qwen().unwrap();
+ assert!(result.removed_hook_file);
+ assert!(result.updated_settings);
+
+ let settings: Value =
+ serde_json::from_str(&fs::read_to_string(&settings_path).unwrap()).unwrap();
+ let remaining = settings["hooks"]["SessionStart"].as_array().unwrap();
+ assert_eq!(remaining.len(), 1);
+ assert_eq!(remaining[0]["hooks"][0]["command"], "echo user-defined");
+
+ std::env::remove_var(QWEN_HOME_ENV_VAR);
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
+fn install_qwen_errors_when_config_dir_missing() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let missing = base.join(".qwen");
+ std::env::set_var(QWEN_HOME_ENV_VAR, &missing);
+
+ let err = install_qwen().unwrap_err().to_string();
+ assert!(err.contains("qwen code config directory not found"));
+
+ std::env::remove_var(QWEN_HOME_ENV_VAR);
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
fn install_cursor_writes_hook_and_updates_hooks_json() {
let _lock = integration_env_lock();
let base = unique_base();
diff --git a/src/integration/types.rs b/src/integration/types.rs
index c5c24267..c7eaf795 100644
--- a/src/integration/types.rs
+++ b/src/integration/types.rs
@@ -70,6 +70,12 @@ pub(crate) struct QodercliInstallPaths {
}
#[derive(Debug)]
+pub(crate) struct QwenInstallPaths {
+ pub hook_path: PathBuf,
+ pub settings_path: PathBuf,
+}
+
+#[derive(Debug)]
pub(crate) struct CursorInstallPaths {
pub hook_path: PathBuf,
pub hooks_path: PathBuf,
@@ -119,6 +125,14 @@ pub(crate) struct QodercliUninstallResult {
pub updated_settings: bool,
}
+#[derive(Debug)]
+pub(crate) struct QwenUninstallResult {
+ pub hook_path: PathBuf,
+ pub settings_path: PathBuf,
+ pub removed_hook_file: bool,
+ pub updated_settings: bool,
+}
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct IntegrationStatus {
pub target: crate::api::schema::IntegrationTarget,