aboutsummaryrefslogtreecommitdiff
path: root/src/integration
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 00:07:24 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 16:29:44 +0300
commit3c2b46d24aa6c8011a1e198143752f2aa414d585 (patch)
treea52280bccc824ca656f8bc986a756f30901974e4 /src/integration
parent5d7e56737ef30d9de5802b0684a9428c40131dda (diff)
feat: add native windows beta support
Diffstat (limited to 'src/integration')
-rw-r--r--src/integration/mod.rs503
1 files changed, 453 insertions, 50 deletions
diff --git a/src/integration/mod.rs b/src/integration/mod.rs
index b668998d..2989ba90 100644
--- a/src/integration/mod.rs
+++ b/src/integration/mod.rs
@@ -656,29 +656,84 @@ pub(crate) fn integration_target_label(
}
fn integration_target_command(target: crate::api::schema::IntegrationTarget) -> &'static str {
+ integration_target_command_names(target)[0]
+}
+
+fn integration_target_command_names(
+ target: crate::api::schema::IntegrationTarget,
+) -> &'static [&'static str] {
match target {
- crate::api::schema::IntegrationTarget::Pi => "pi",
- crate::api::schema::IntegrationTarget::Omp => "omp",
- crate::api::schema::IntegrationTarget::Claude => "claude",
- crate::api::schema::IntegrationTarget::Codex => "codex",
- crate::api::schema::IntegrationTarget::Copilot => "copilot",
- crate::api::schema::IntegrationTarget::Droid => "droid",
- crate::api::schema::IntegrationTarget::Kimi => "kimi",
- crate::api::schema::IntegrationTarget::Opencode => "opencode",
- crate::api::schema::IntegrationTarget::Hermes => "hermes",
- crate::api::schema::IntegrationTarget::Qodercli => "qodercli",
+ crate::api::schema::IntegrationTarget::Pi => &["pi"],
+ crate::api::schema::IntegrationTarget::Omp => &["omp"],
+ crate::api::schema::IntegrationTarget::Claude => &["claude"],
+ crate::api::schema::IntegrationTarget::Codex => &["codex"],
+ crate::api::schema::IntegrationTarget::Copilot => &["copilot"],
+ crate::api::schema::IntegrationTarget::Droid => &["droid"],
+ crate::api::schema::IntegrationTarget::Kimi => &["kimi"],
+ crate::api::schema::IntegrationTarget::Opencode => &["opencode"],
+ crate::api::schema::IntegrationTarget::Hermes => &["hermes"],
+ crate::api::schema::IntegrationTarget::Qodercli => qodercli_command_names(),
}
}
fn integration_target_available(target: crate::api::schema::IntegrationTarget) -> bool {
- command_available(integration_target_command(target))
+ integration_target_command_names(target)
+ .iter()
+ .any(|command| command_available(command))
+ || integration_target_install_layout_available(target)
+}
+
+#[cfg(windows)]
+fn qodercli_command_names() -> &'static [&'static str] {
+ &["qodercli", "qoder", "qoderclicn", "qodercn"]
+}
+
+#[cfg(not(windows))]
+fn qodercli_command_names() -> &'static [&'static str] {
+ &["qodercli"]
+}
+
+fn integration_target_install_layout_available(
+ target: crate::api::schema::IntegrationTarget,
+) -> bool {
+ match target {
+ crate::api::schema::IntegrationTarget::Codex => codex_standalone_binary_available(),
+ crate::api::schema::IntegrationTarget::Hermes => hermes_install_layout_available(),
+ _ => false,
+ }
}
fn command_available(command: &str) -> bool {
let Some(paths) = std::env::var_os("PATH") else {
return false;
};
- std::env::split_paths(&paths).any(|dir| executable_file_exists(&dir.join(command)))
+ std::env::split_paths(&paths).any(|dir| {
+ command_path_candidates(&dir, command)
+ .into_iter()
+ .any(|path| executable_file_exists(&path))
+ })
+}
+
+fn command_path_candidates(dir: &Path, command: &str) -> Vec<PathBuf> {
+ let base = dir.join(command);
+
+ #[cfg(not(windows))]
+ {
+ vec![base]
+ }
+
+ #[cfg(windows)]
+ {
+ if Path::new(command).extension().is_some() {
+ return vec![base];
+ }
+
+ let mut candidates = vec![base];
+ for extension in [".exe", ".cmd", ".bat", ".ps1"] {
+ candidates.push(dir.join(format!("{command}{extension}")));
+ }
+ candidates
+ }
}
fn executable_file_exists(path: &Path) -> bool {
@@ -701,6 +756,53 @@ fn executable_file_exists(path: &Path) -> bool {
}
}
+fn codex_standalone_binary_available() -> bool {
+ let Ok(releases_dir) =
+ codex_dir().map(|dir| dir.join("packages").join("standalone").join("releases"))
+ else {
+ return false;
+ };
+ let Ok(entries) = fs::read_dir(releases_dir) else {
+ return false;
+ };
+
+ entries.filter_map(Result::ok).any(|entry| {
+ executable_file_exists(&entry.path().join("bin").join(codex_executable_name()))
+ })
+}
+
+fn codex_executable_name() -> &'static str {
+ if cfg!(windows) {
+ "codex.exe"
+ } else {
+ "codex"
+ }
+}
+
+fn hermes_install_layout_available() -> bool {
+ #[cfg(windows)]
+ {
+ let Some(local_app_data) =
+ std::env::var_os("LOCALAPPDATA").filter(|value| !value.is_empty())
+ else {
+ return false;
+ };
+ let dir = PathBuf::from(local_app_data).join("hermes");
+ [
+ dir.join("hermes.exe"),
+ dir.join("bin").join("hermes.exe"),
+ dir.join("Scripts").join("hermes.exe"),
+ ]
+ .into_iter()
+ .any(|path| executable_file_exists(&path))
+ }
+
+ #[cfg(not(windows))]
+ {
+ false
+ }
+}
+
pub(crate) fn installed_integration_statuses() -> Vec<IntegrationStatus> {
integration_specs()
.into_iter()
@@ -897,6 +999,17 @@ 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()
@@ -2537,14 +2650,14 @@ fn shell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\"'\"'"))
}
-fn make_executable(path: &Path) -> io::Result<()> {
+fn make_executable(_path: &Path) -> io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
- let mut perms = fs::metadata(path)?.permissions();
+ let mut perms = fs::metadata(_path)?.permissions();
perms.set_mode(0o755);
- fs::set_permissions(path, perms)?;
+ fs::set_permissions(_path, perms)?;
}
Ok(())
@@ -2638,9 +2751,28 @@ fn qodercli_dir() -> io::Result<PathBuf> {
}
fn home_dir() -> io::Result<PathBuf> {
- std::env::var("HOME")
- .map(PathBuf::from)
- .map_err(|_| io::Error::other("HOME is not set; cannot locate home directory"))
+ if let Some(home) = std::env::var_os("HOME").filter(|value| !value.is_empty()) {
+ return Ok(PathBuf::from(home));
+ }
+
+ #[cfg(windows)]
+ {
+ if let Some(profile) = std::env::var_os("USERPROFILE").filter(|value| !value.is_empty()) {
+ return Ok(PathBuf::from(profile));
+ }
+ if let (Some(drive), Some(path)) = (
+ std::env::var_os("HOMEDRIVE").filter(|value| !value.is_empty()),
+ std::env::var_os("HOMEPATH").filter(|value| !value.is_empty()),
+ ) {
+ let mut home = PathBuf::from(drive);
+ home.push(path);
+ return Ok(home);
+ }
+ }
+
+ Err(io::Error::other(
+ "home directory is not set; cannot locate home directory",
+ ))
}
#[cfg(test)]
@@ -2703,6 +2835,28 @@ mod tests {
))
}
+ #[cfg(windows)]
+ #[test]
+ fn home_dir_uses_userprofile_when_home_is_missing() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let previous_home = std::env::var_os("HOME");
+ let previous_userprofile = std::env::var_os("USERPROFILE");
+ std::env::remove_var("HOME");
+ std::env::set_var("USERPROFILE", &base);
+
+ assert_eq!(home_dir().unwrap(), base);
+
+ if let Some(home) = previous_home {
+ std::env::set_var("HOME", home);
+ }
+ if let Some(userprofile) = previous_userprofile {
+ std::env::set_var("USERPROFILE", userprofile);
+ } else {
+ std::env::remove_var("USERPROFILE");
+ }
+ }
+
#[test]
#[cfg(unix)]
fn command_available_requires_executable_file_on_path() {
@@ -2732,6 +2886,162 @@ mod tests {
}
#[test]
+ #[cfg(windows)]
+ fn command_available_finds_windows_command_shims_on_path() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let bin = base.join("bin");
+ fs::create_dir_all(&bin).unwrap();
+ let original_path = std::env::var_os("PATH");
+ std::env::set_var("PATH", &bin);
+
+ fs::write(bin.join("claude.cmd"), "@echo off\r\n").unwrap();
+ assert!(command_available("claude"));
+
+ fs::write(bin.join("codex.exe"), "").unwrap();
+ assert!(command_available("codex"));
+
+ assert!(!command_available("missing-agent"));
+
+ if let Some(path) = original_path {
+ std::env::set_var("PATH", path);
+ } else {
+ std::env::remove_var("PATH");
+ }
+ let _ = fs::remove_dir_all(base);
+ }
+
+ #[test]
+ #[cfg(windows)]
+ fn qodercli_availability_checks_windows_aliases() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let bin = base.join("bin");
+ fs::create_dir_all(&bin).unwrap();
+ let original_path = std::env::var_os("PATH");
+ std::env::set_var("PATH", &bin);
+
+ fs::write(bin.join("qoder.cmd"), "@echo off\r\n").unwrap();
+
+ assert!(integration_target_available(
+ crate::api::schema::IntegrationTarget::Qodercli
+ ));
+
+ if let Some(path) = original_path {
+ std::env::set_var("PATH", path);
+ } else {
+ std::env::remove_var("PATH");
+ }
+ let _ = fs::remove_dir_all(base);
+ }
+
+ #[test]
+ #[cfg(windows)]
+ fn hermes_availability_checks_windows_install_layout() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let local_app_data = base.join("local-app-data");
+ let hermes_bin = local_app_data.join("hermes").join("bin");
+ fs::create_dir_all(&hermes_bin).unwrap();
+ fs::write(hermes_bin.join("hermes.exe"), "").unwrap();
+ let original_local_app_data = std::env::var_os("LOCALAPPDATA");
+ let original_path = std::env::var_os("PATH");
+ std::env::set_var("LOCALAPPDATA", &local_app_data);
+ std::env::set_var("PATH", "");
+
+ assert!(integration_target_available(
+ crate::api::schema::IntegrationTarget::Hermes
+ ));
+
+ if let Some(local_app_data) = original_local_app_data {
+ std::env::set_var("LOCALAPPDATA", local_app_data);
+ } else {
+ std::env::remove_var("LOCALAPPDATA");
+ }
+ if let Some(path) = original_path {
+ std::env::set_var("PATH", path);
+ } else {
+ std::env::remove_var("PATH");
+ }
+ let _ = fs::remove_dir_all(base);
+ }
+
+ #[test]
+ fn codex_availability_finds_standalone_binary_under_codex_home() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let home = base.join("home");
+ let bin = home
+ .join(".codex/packages/standalone/releases/0.137.0-test")
+ .join("bin");
+ fs::create_dir_all(&bin).unwrap();
+ let binary = bin.join(codex_executable_name());
+ fs::write(&binary, "").unwrap();
+ make_executable(&binary).unwrap();
+ let original_home = std::env::var_os("HOME");
+ let original_path = std::env::var_os("PATH");
+ std::env::set_var("HOME", &home);
+ std::env::set_var("PATH", "");
+
+ assert!(integration_target_available(
+ crate::api::schema::IntegrationTarget::Codex
+ ));
+
+ if let Some(home) = original_home {
+ std::env::set_var("HOME", home);
+ } else {
+ std::env::remove_var("HOME");
+ }
+ if let Some(path) = original_path {
+ std::env::set_var("PATH", path);
+ } else {
+ std::env::remove_var("PATH");
+ }
+ let _ = fs::remove_dir_all(base);
+ }
+
+ #[test]
+ fn integration_recommendations_mark_standalone_codex_available() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let home = base.join("home");
+ let bin = home
+ .join(".codex/packages/standalone/releases/0.137.0-test")
+ .join("bin");
+ fs::create_dir_all(&bin).unwrap();
+ let binary = bin.join(codex_executable_name());
+ fs::write(&binary, "").unwrap();
+ make_executable(&binary).unwrap();
+ let original_home = std::env::var_os("HOME");
+ let original_path = std::env::var_os("PATH");
+ std::env::set_var("HOME", &home);
+ std::env::set_var("PATH", "");
+
+ let codex = integration_recommendations()
+ .into_iter()
+ .find(|recommendation| {
+ recommendation.target == crate::api::schema::IntegrationTarget::Codex
+ })
+ .expect("codex recommendation should be present");
+
+ assert!(codex.available);
+ assert_eq!(codex.state, IntegrationStatusKind::NotInstalled);
+ assert!(codex.needs_install());
+
+ if let Some(home) = original_home {
+ std::env::set_var("HOME", home);
+ } else {
+ std::env::remove_var("HOME");
+ }
+ if let Some(path) = original_path {
+ std::env::set_var("PATH", path);
+ } else {
+ std::env::remove_var("PATH");
+ }
+ let _ = fs::remove_dir_all(base);
+ }
+
+ #[test]
fn integration_recommendation_installs_available_or_outdated_targets() {
let mut recommendation = IntegrationRecommendation {
target: crate::api::schema::IntegrationTarget::Claude,
@@ -2906,6 +3216,29 @@ mod tests {
}
#[test]
+ fn install_omp_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(".omp/agent");
+ let ext_dir = agent_dir.join("extensions");
+ fs::create_dir_all(&agent_dir).unwrap();
+ std::env::set_var("HOME", &home);
+
+ let installed = install_omp().unwrap();
+
+ assert_eq!(
+ installed.extension_path,
+ ext_dir.join(OMP_EXTENSION_INSTALL_NAME)
+ );
+ assert!(ext_dir.is_dir());
+ assert!(!installed.removed_legacy_pi_extension);
+
+ std::env::remove_var("HOME");
+ let _ = fs::remove_dir_all(base);
+ }
+
+ #[test]
fn uninstall_omp_removes_embedded_extension_when_present() {
let _lock = integration_env_lock();
let base = unique_base();
@@ -3133,15 +3466,41 @@ mod tests {
let hooks_dir = claude_dir.join("hooks");
fs::create_dir_all(&hooks_dir).unwrap();
let hook_path = hooks_dir.join(CLAUDE_HOOK_INSTALL_NAME);
+ let settings = serde_json::json!({
+ "hooks": {
+ "PostToolUse": [{
+ "matcher": "*",
+ "hooks": [
+ {"type": "command", "command": format!("bash '{}' working", hook_path.display()), "timeout": 10},
+ {"type": "command", "command": "echo keep-post", "timeout": 10}
+ ]
+ }],
+ "PostToolUseFailure": [{
+ "matcher": "*",
+ "hooks": [
+ {"type": "command", "command": format!("bash '{}' working", hook_path.display()), "timeout": 10},
+ {"type": "command", "command": "echo keep-failure", "timeout": 10}
+ ]
+ }],
+ "SubagentStop": [{
+ "matcher": "*",
+ "hooks": [
+ {"type": "command", "command": format!("bash '{}' working", hook_path.display()), "timeout": 10},
+ {"type": "command", "command": "echo keep-subagent", "timeout": 10}
+ ]
+ }],
+ "SessionEnd": [{
+ "matcher": "*",
+ "hooks": [
+ {"type": "command", "command": format!("bash '{}' release", hook_path.display()), "timeout": 10},
+ {"type": "command", "command": "echo keep-session-end", "timeout": 10}
+ ]
+ }]
+ }
+ });
fs::write(
claude_dir.join("settings.json"),
- format!(
- r#"{{"hooks":{{"PostToolUse":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep-post","timeout":10}}]}}],"PostToolUseFailure":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep-failure","timeout":10}}]}}],"SubagentStop":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep-subagent","timeout":10}}]}}],"SessionEnd":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' release","timeout":10}},{{"type":"command","command":"echo keep-session-end","timeout":10}}]}}]}}}}"#,
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- ),
+ serde_json::to_string(&settings).unwrap(),
)
.unwrap();
std::env::set_var("HOME", &home);
@@ -3245,19 +3604,48 @@ mod tests {
fs::create_dir_all(&hooks_dir).unwrap();
let hook_path = hooks_dir.join(CLAUDE_HOOK_INSTALL_NAME);
fs::write(&hook_path, CLAUDE_HOOK_ASSET).unwrap();
+ let settings = serde_json::json!({
+ "hooks": {
+ "SessionStart": [{
+ "matcher": "*",
+ "hooks": [{"type": "command", "command": format!("bash '{}' idle", hook_path.display()), "timeout": 10}]
+ }],
+ "UserPromptSubmit": [{
+ "matcher": "*",
+ "hooks": [
+ {"type": "command", "command": format!("bash '{}' working", hook_path.display()), "timeout": 10},
+ {"type": "command", "command": "echo keep", "timeout": 10}
+ ]
+ }],
+ "PermissionRequest": [{
+ "matcher": "*",
+ "hooks": [{"type": "command", "command": format!("bash '{}' blocked", hook_path.display()), "timeout": 10}]
+ }],
+ "PostToolUse": [{
+ "matcher": "*",
+ "hooks": [{"type": "command", "command": format!("bash '{}' working", hook_path.display()), "timeout": 10}]
+ }],
+ "PostToolUseFailure": [{
+ "matcher": "*",
+ "hooks": [{"type": "command", "command": format!("bash '{}' working", hook_path.display()), "timeout": 10}]
+ }],
+ "SubagentStop": [{
+ "matcher": "*",
+ "hooks": [{"type": "command", "command": format!("bash '{}' working", hook_path.display()), "timeout": 10}]
+ }],
+ "Stop": [{
+ "matcher": "*",
+ "hooks": [{"type": "command", "command": format!("bash '{}' idle", hook_path.display()), "timeout": 10}]
+ }],
+ "SessionEnd": [{
+ "matcher": "*",
+ "hooks": [{"type": "command", "command": format!("bash '{}' release", hook_path.display()), "timeout": 10}]
+ }]
+ }
+ });
fs::write(
claude_dir.join("settings.json"),
- format!(
- r#"{{"hooks":{{"SessionStart":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}],"UserPromptSubmit":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep","timeout":10}}]}}],"PermissionRequest":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' blocked","timeout":10}}]}}],"PostToolUse":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}}]}}],"PostToolUseFailure":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}}]}}],"SubagentStop":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}}]}}],"Stop":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}],"SessionEnd":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' release","timeout":10}}]}}]}}}}"#,
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- ),
+ serde_json::to_string(&settings).unwrap(),
)
.unwrap();
std::env::set_var("HOME", &home);
@@ -3464,16 +3852,21 @@ mod tests {
fs::create_dir_all(&codex_dir).unwrap();
let hook_path = codex_dir.join(CODEX_HOOK_INSTALL_NAME);
fs::write(&hook_path, CODEX_HOOK_ASSET).unwrap();
+ let hooks = serde_json::json!({
+ "hooks": {
+ "SessionStart": [{"hooks": [{"type": "command", "command": format!("bash '{}' idle", hook_path.display()), "timeout": 10}]}],
+ "UserPromptSubmit": [{"hooks": [
+ {"type": "command", "command": format!("bash '{}' working", hook_path.display()), "timeout": 10},
+ {"type": "command", "command": "echo keep", "timeout": 10}
+ ]}],
+ "PreToolUse": [{"hooks": [{"type": "command", "command": format!("bash '{}' working", hook_path.display()), "timeout": 10}]}],
+ "PermissionRequest": [{"hooks": [{"type": "command", "command": format!("bash '{}' blocked", hook_path.display()), "timeout": 10}]}],
+ "Stop": [{"hooks": [{"type": "command", "command": format!("bash '{}' idle", hook_path.display()), "timeout": 10}]}]
+ }
+ });
fs::write(
codex_dir.join("hooks.json"),
- format!(
- r#"{{"hooks":{{"SessionStart":[{{"hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}],"UserPromptSubmit":[{{"hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep","timeout":10}}]}}],"PreToolUse":[{{"hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}}]}}],"PermissionRequest":[{{"hooks":[{{"type":"command","command":"bash '{}' blocked","timeout":10}}]}}],"Stop":[{{"hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}]}}}}"#,
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- hook_path.display(),
- ),
+ serde_json::to_string(&hooks).unwrap(),
)
.unwrap();
fs::write(
@@ -3808,14 +4201,24 @@ mod tests {
"bash {}",
shell_single_quote(&hook_path.display().to_string())
);
+ let settings = serde_json::json!({
+ "hooks": {
+ "PreToolUse": [
+ {"type": "command", "command": command, "timeoutSec": 10},
+ {"type": "command", "command": "echo keep", "timeoutSec": 10}
+ ],
+ "PostToolUse": [{"type": "command", "command": command, "timeoutSec": 10}],
+ "notification": [{
+ "type": "command",
+ "matcher": "permission_prompt|elicitation_dialog|agent_idle",
+ "command": command,
+ "timeoutSec": 10
+ }]
+ }
+ });
fs::write(
copilot_dir.join("settings.json"),
- format!(
- r#"{{"hooks":{{"PreToolUse":[{{"type":"command","command":"{}","timeoutSec":10}},{{"type":"command","command":"echo keep","timeoutSec":10}}],"PostToolUse":[{{"type":"command","command":"{}","timeoutSec":10}}],"notification":[{{"type":"command","matcher":"permission_prompt|elicitation_dialog|agent_idle","command":"{}","timeoutSec":10}}]}}}}"#,
- command,
- command,
- command,
- ),
+ serde_json::to_string(&settings).unwrap(),
)
.unwrap();
std::env::set_var("HOME", &home);