Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/daemon/telemetry_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ use std::time::Duration;
const DAEMON_SOCKET_IO_TIMEOUT: Duration = Duration::from_secs(2);

/// Maximum time to wait for the daemon socket on process start.
#[cfg(not(any(test, feature = "test-support")))]
///
/// Windows Named Pipes require a listening server instance before `connect_ms`
/// returns, and process startup on Windows is meaningfully slower than on Unix
/// (AV scanning, loader lock, etc.). The daemon startup timeout in
/// `daemon_startup_timeout()` already grants 5 s on Windows for this reason;
/// we mirror that budget here so the git wrapper does not time-out connecting
/// to the control socket before the daemon is ready.
#[cfg(all(not(any(test, feature = "test-support")), windows))]
const DAEMON_TELEMETRY_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);

#[cfg(all(not(any(test, feature = "test-support")), not(windows)))]
const DAEMON_TELEMETRY_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);

/// Global handle to the daemon control socket for telemetry submission.
Expand Down
36 changes: 32 additions & 4 deletions src/mdm/agents/codex.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::error::GitAiError;
use crate::mdm::hook_installer::{HookCheckResult, HookInstaller, HookInstallerParams};
use crate::mdm::utils::{
binary_exists, generate_diff, home_dir, is_git_ai_checkpoint_command, write_atomic,
binary_exists, generate_diff, home_dir, is_git_ai_checkpoint_command,
to_agent_hook_command_path, to_git_bash_path, write_atomic,
};
use serde_json::{Value as JsonValue, json};
use sha2::{Digest, Sha256};
Expand All @@ -25,7 +26,15 @@ impl CodexInstaller {
}

fn desired_command(binary_path: &Path) -> String {
format!("{} {}", binary_path.display(), CODEX_CHECKPOINT_CMD)
format!(
"{} {}",
to_agent_hook_command_path(binary_path),
CODEX_CHECKPOINT_CMD
)
}

fn legacy_msys_desired_command(binary_path: &Path) -> String {
format!("{} {}", to_git_bash_path(binary_path), CODEX_CHECKPOINT_CMD)
}

fn parse_config_toml(content: &str) -> Result<TomlValue, GitAiError> {
Expand Down Expand Up @@ -210,6 +219,13 @@ impl CodexInstaller {
fn config_with_installed_hooks(
config: &TomlValue,
binary_path: &Path,
) -> Result<TomlValue, GitAiError> {
Self::config_with_installed_hooks_for_command(config, Self::desired_command(binary_path))
}

fn config_with_installed_hooks_for_command(
config: &TomlValue,
desired_command: String,
) -> Result<TomlValue, GitAiError> {
let mut merged = Self::remove_notify_if_git_ai(config)?.unwrap_or(config.clone());
let root = merged
Expand All @@ -231,7 +247,6 @@ impl CodexInstaller {
}

// Add inline hooks to config.toml under [hooks] table
let desired_command = Self::desired_command(binary_path);
let hooks_table = root
.entry("hooks")
.or_insert_with(|| TomlValue::Table(Map::new()));
Expand Down Expand Up @@ -631,14 +646,27 @@ impl HookInstaller for CodexInstaller {
};

let desired_config = Self::config_with_installed_hooks(&config, &params.binary_path)?;
let legacy_msys_desired_config = if cfg!(windows) {
Some(Self::config_with_installed_hooks_for_command(
&config,
Self::legacy_msys_desired_command(&params.binary_path),
)?)
} else {
None
};
let has_inline_hooks = Self::config_has_inline_hooks(&config);
let has_legacy_hooks_json = Self::hooks_json_path().exists()
&& Self::parse_hooks_json(&fs::read_to_string(Self::hooks_json_path())?)
.map(|json| Self::hooks_json_has_git_ai_entries(&json))
.unwrap_or(false);
let hooks_installed = Self::config_hooks_feature_enabled(&config)
&& (has_inline_hooks || has_legacy_hooks_json);
let hooks_up_to_date = config == desired_config && !has_legacy_hooks_json;
let hooks_up_to_date = (config == desired_config
|| legacy_msys_desired_config
.as_ref()
.map(|legacy| &config == legacy)
.unwrap_or(false))
&& !has_legacy_hooks_json;

Ok(HookCheckResult {
tool_installed: true,
Expand Down
29 changes: 13 additions & 16 deletions src/mdm/agents/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use crate::mdm::hook_installer::{
};
use crate::mdm::utils::{
MIN_CURSOR_VERSION, generate_diff, get_editor_version, home_dir, install_vsc_editor_extension,
is_vsc_editor_extension_installed, parse_version, resolve_editor_cli,
settings_paths_for_products, should_process_settings_target, version_meets_requirement,
write_atomic,
is_vsc_editor_extension_installed, parse_version, resolve_editor_cli, settings_paths_for_products,
should_process_settings_target, to_agent_hook_command_path,
version_meets_requirement, write_atomic,
};
use serde_json::{Value, json};
use std::fs;
Expand Down Expand Up @@ -135,16 +135,9 @@ impl HookInstaller for CursorInstaller {
};

// Build commands with absolute path
let pre_tool_use_cmd = format!(
"{} {}",
params.binary_path.display(),
CURSOR_PRE_TOOL_USE_CMD
);
let post_tool_use_cmd = format!(
"{} {}",
params.binary_path.display(),
CURSOR_POST_TOOL_USE_CMD
);
let binary_path_str = to_agent_hook_command_path(&params.binary_path);
let pre_tool_use_cmd = format!("{} {}", binary_path_str, CURSOR_PRE_TOOL_USE_CMD);
let post_tool_use_cmd = format!("{} {}", binary_path_str, CURSOR_POST_TOOL_USE_CMD);

// Desired hooks payload for Cursor
let desired: Value = json!({
Expand Down Expand Up @@ -433,7 +426,7 @@ impl HookInstaller for CursorInstaller {
#[cfg(test)]
mod tests {
use super::*;
use crate::mdm::utils::clean_path;
use crate::mdm::utils::{clean_path, to_agent_hook_command_path};
use std::fs;
use tempfile::TempDir;

Expand Down Expand Up @@ -653,11 +646,15 @@ mod tests {

#[test]
fn test_cursor_hook_commands_no_windows_extended_path_prefix() {
// Simulate the production path: get_current_binary_path() calls clean_path to strip
// the \\?\ extended-length prefix, then we call to_agent_hook_command_path to
// normalize the command path for hook execution.
let raw_path = PathBuf::from(r"\\?\C:\Users\USERNAME\.git-ai\bin\git-ai.exe");
let binary_path = clean_path(raw_path);
let binary_path_str = to_agent_hook_command_path(&binary_path);

let pre_tool_use_cmd = format!("{} {}", binary_path.display(), CURSOR_PRE_TOOL_USE_CMD);
let post_tool_use_cmd = format!("{} {}", binary_path.display(), CURSOR_POST_TOOL_USE_CMD);
let pre_tool_use_cmd = format!("{} {}", binary_path_str, CURSOR_PRE_TOOL_USE_CMD);
let post_tool_use_cmd = format!("{} {}", binary_path_str, CURSOR_POST_TOOL_USE_CMD);

assert!(
!pre_tool_use_cmd.contains(r"\\?\"),
Expand Down
11 changes: 7 additions & 4 deletions src/mdm/agents/droid.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::error::GitAiError;
use crate::mdm::hook_installer::{HookCheckResult, HookInstaller, HookInstallerParams};
use crate::mdm::utils::{generate_diff, home_dir, is_git_ai_checkpoint_command, write_atomic};
use crate::mdm::utils::{
generate_diff, home_dir, is_git_ai_checkpoint_command, to_agent_hook_command_path,
write_atomic,
};
use serde_json::{Value, json};
use std::fs;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -84,9 +87,9 @@ impl DroidInstaller {
serde_json::from_str(&existing_content)?
};

let binary_path = params.binary_path.to_string_lossy().to_string();
let pre_tool_cmd = format!("{} {}", binary_path, DROID_PRE_TOOL_CMD);
let post_tool_cmd = format!("{} {}", binary_path, DROID_POST_TOOL_CMD);
let binary_path_str = to_agent_hook_command_path(&params.binary_path);
let pre_tool_cmd = format!("{} {}", binary_path_str, DROID_PRE_TOOL_CMD);
let post_tool_cmd = format!("{} {}", binary_path_str, DROID_POST_TOOL_CMD);

let mut merged = existing.clone();
let mut hooks_obj = merged.get("hooks").cloned().unwrap_or_else(|| json!({}));
Expand Down
15 changes: 4 additions & 11 deletions src/mdm/agents/firebender.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::GitAiError;
use crate::mdm::hook_installer::{HookCheckResult, HookInstaller, HookInstallerParams};
use crate::mdm::utils::{generate_diff, home_dir, write_atomic};
use crate::mdm::utils::{generate_diff, home_dir, to_agent_hook_command_path, write_atomic};
use serde_json::{Value, json};
use std::fs;
use std::path::PathBuf;
Expand Down Expand Up @@ -112,16 +112,9 @@ impl HookInstaller for FirebenderInstaller {
serde_json::from_str(&existing_content)?
};

let pre_tool_use_cmd = format!(
"{} {}",
params.binary_path.display(),
FIREBENDER_PRE_TOOL_USE_CMD
);
let post_tool_use_cmd = format!(
"{} {}",
params.binary_path.display(),
FIREBENDER_POST_TOOL_USE_CMD
);
let binary_path_str = to_agent_hook_command_path(&params.binary_path);
let pre_tool_use_cmd = format!("{} {}", binary_path_str, FIREBENDER_PRE_TOOL_USE_CMD);
let post_tool_use_cmd = format!("{} {}", binary_path_str, FIREBENDER_POST_TOOL_USE_CMD);

let desired: Value = json!({
"version": 1,
Expand Down
12 changes: 5 additions & 7 deletions src/mdm/agents/gemini.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::error::GitAiError;
use crate::mdm::hook_installer::{HookCheckResult, HookInstaller, HookInstallerParams};
use crate::mdm::utils::{
binary_exists, generate_diff, home_dir, is_git_ai_checkpoint_command, write_atomic,
binary_exists, generate_diff, home_dir, is_git_ai_checkpoint_command,
to_agent_hook_command_path, write_atomic,
};
use serde_json::{Value, json};
use std::fs;
Expand Down Expand Up @@ -87,12 +88,9 @@ impl GeminiInstaller {
serde_json::from_str(&existing_content)?
};

let before_tool_cmd = format!(
"{} {}",
params.binary_path.display(),
GEMINI_BEFORE_TOOL_CMD
);
let after_tool_cmd = format!("{} {}", params.binary_path.display(), GEMINI_AFTER_TOOL_CMD);
let binary_path_str = to_agent_hook_command_path(&params.binary_path);
let before_tool_cmd = format!("{} {}", binary_path_str, GEMINI_BEFORE_TOOL_CMD);
let after_tool_cmd = format!("{} {}", binary_path_str, GEMINI_AFTER_TOOL_CMD);

let mut merged = existing.clone();

Expand Down
33 changes: 14 additions & 19 deletions src/mdm/agents/github_copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::mdm::hook_installer::{HookCheckResult, HookInstaller, HookInstallerPa
use crate::mdm::utils::{
MIN_CODE_VERSION, generate_diff, get_editor_version, home_dir, parse_version,
resolve_editor_cli, settings_paths_for_products, should_process_settings_target,
version_meets_requirement, write_atomic,
to_agent_hook_command_path, to_git_bash_path, version_meets_requirement, write_atomic,
};
use serde_json::{Value, json};
use std::fs;
Expand Down Expand Up @@ -107,15 +107,17 @@ impl HookInstaller for GitHubCopilotInstaller {
let content = fs::read_to_string(&hooks_path)?;
let existing: Value = serde_json::from_str(&content).unwrap_or_else(|_| json!({}));

let pre_desired = format!(
let binary_path_str = to_agent_hook_command_path(&params.binary_path);
let pre_desired = format!("{} {}", binary_path_str, GITHUB_COPILOT_PRE_TOOL_CMD);
let post_desired = format!("{} {}", binary_path_str, GITHUB_COPILOT_POST_TOOL_CMD);
let legacy_msys_binary_path_str = to_git_bash_path(&params.binary_path);
let pre_legacy = format!(
"{} {}",
params.binary_path.display(),
GITHUB_COPILOT_PRE_TOOL_CMD
legacy_msys_binary_path_str, GITHUB_COPILOT_PRE_TOOL_CMD
);
let post_desired = format!(
let post_legacy = format!(
"{} {}",
params.binary_path.display(),
GITHUB_COPILOT_POST_TOOL_CMD
legacy_msys_binary_path_str, GITHUB_COPILOT_POST_TOOL_CMD
);

let has_pre_installed = existing
Expand Down Expand Up @@ -154,7 +156,7 @@ impl HookInstaller for GitHubCopilotInstaller {
arr.iter().any(|hook| {
hook.get("command")
.and_then(|c| c.as_str())
.map(|cmd| cmd == pre_desired)
.map(|cmd| cmd == pre_desired || (cfg!(windows) && cmd == pre_legacy))
.unwrap_or(false)
})
})
Expand All @@ -168,7 +170,7 @@ impl HookInstaller for GitHubCopilotInstaller {
arr.iter().any(|hook| {
hook.get("command")
.and_then(|c| c.as_str())
.map(|cmd| cmd == post_desired)
.map(|cmd| cmd == post_desired || (cfg!(windows) && cmd == post_legacy))
.unwrap_or(false)
})
})
Expand Down Expand Up @@ -204,16 +206,9 @@ impl HookInstaller for GitHubCopilotInstaller {
serde_json::from_str(&existing_content)?
};

let pre_tool_cmd = format!(
"{} {}",
params.binary_path.display(),
GITHUB_COPILOT_PRE_TOOL_CMD
);
let post_tool_cmd = format!(
"{} {}",
params.binary_path.display(),
GITHUB_COPILOT_POST_TOOL_CMD
);
let binary_path_str = to_agent_hook_command_path(&params.binary_path);
let pre_tool_cmd = format!("{} {}", binary_path_str, GITHUB_COPILOT_PRE_TOOL_CMD);
let post_tool_cmd = format!("{} {}", binary_path_str, GITHUB_COPILOT_POST_TOOL_CMD);

let desired: Value = json!({
"hooks": {
Expand Down
5 changes: 3 additions & 2 deletions src/mdm/agents/windsurf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use crate::mdm::hook_installer::{
};
use crate::mdm::utils::{
generate_diff, home_dir, install_vsc_editor_extension, is_git_ai_checkpoint_command,
is_github_codespaces, is_vsc_editor_extension_installed, resolve_editor_cli, write_atomic,
is_github_codespaces, is_vsc_editor_extension_installed, resolve_editor_cli,
to_agent_hook_command_path, write_atomic,
};

use serde_json::{Value, json};
Expand Down Expand Up @@ -271,7 +272,7 @@ impl HookInstaller for WindsurfInstaller {
) -> Result<Option<String>, GitAiError> {
let desired_cmd = format!(
"{} {}",
params.binary_path.display(),
to_agent_hook_command_path(&params.binary_path),
WINDSURF_CHECKPOINT_CMD
);

Expand Down
16 changes: 12 additions & 4 deletions src/mdm/ensure_git_symlinks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,20 @@ pub fn ensure_git_symlinks() -> Result<(), GitAiError> {

// Remove existing symlink/junction if present
if symlink_path.exists() || symlink_path.symlink_metadata().is_ok() {
// On Windows, junctions are directories, so use remove_dir
// On Windows, junctions are directories, so try remove_dir first,
// then fall back to remove_file (for regular symlinks). Both errors
// are surfaced rather than swallowed so callers get a clear diagnosis.
#[cfg(windows)]
{
// Try remove_dir first (for junctions), then remove_file (for symlinks)
if std::fs::remove_dir(&symlink_path).is_err() {
let _ = std::fs::remove_file(&symlink_path);
let dir_err = std::fs::remove_dir(&symlink_path);
if dir_err.is_err() {
std::fs::remove_file(&symlink_path).map_err(|e| {
GitAiError::Generic(format!(
"Failed to remove existing junction/symlink at {}: {}",
symlink_path.display(),
e
))
})?;
}
Comment thread
tanbazhagan marked this conversation as resolved.
}
#[cfg(unix)]
Expand Down
12 changes: 12 additions & 0 deletions src/mdm/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,18 @@ pub fn to_git_bash_path(path: &Path) -> String {
s.into_owned()
}

/// Convert a path for agent hook command execution.
/// On Windows, use `C:/...` style paths so commands can run in cmd.exe,
/// PowerShell, and shells that also accept forward slashes.
/// On non-Windows platforms, return the existing hook command path style.
pub fn to_agent_hook_command_path(path: &Path) -> String {
if cfg!(windows) {
to_windows_git_bash_style_path(path)
} else {
to_git_bash_path(path)
}
}

/// Get the absolute path to the currently running binary
pub fn get_current_binary_path() -> Result<PathBuf, GitAiError> {
let path = std::env::current_exe()?;
Expand Down