Skip to content

Commit 39a775b

Browse files
authored
Merge pull request #41 from acunniffe/bug/fix-install-hooks-windows
Bug/fix install hooks windows
2 parents 098b297 + 455679b commit 39a775b

File tree

2 files changed

+137
-60
lines changed

2 files changed

+137
-60
lines changed

install.ps1

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -50,31 +50,84 @@ function Get-StdGitPath {
5050
return $null
5151
}
5252

53-
function Add-ToUserPath {
53+
# Ensure $PathToAdd is inserted before any PATH entry that contains "git" (case-insensitive)
54+
# Updates Machine (system) PATH; if not elevated, emits a prominent error with instructions
55+
function Set-PathPrependBeforeGit {
5456
param(
5557
[Parameter(Mandatory = $true)][string]$PathToAdd
5658
)
57-
$current = [Environment]::GetEnvironmentVariable('Path', 'User')
59+
5860
$sep = ';'
59-
$entries = @()
60-
if ($current) { $entries = ($current -split $sep) | Where-Object { $_ -and $_.Trim() -ne '' } }
61-
62-
$exists = $false
63-
foreach ($entry in $entries) {
64-
try {
65-
if ([IO.Path]::GetFullPath($entry.TrimEnd('\')) -ieq [IO.Path]::GetFullPath($PathToAdd.TrimEnd('\'))) {
66-
$exists = $true
67-
break
61+
62+
function NormalizePath([string]$p) {
63+
try { return ([IO.Path]::GetFullPath($p.Trim())).TrimEnd('\\').ToLowerInvariant() }
64+
catch { return ($p.Trim()).TrimEnd('\\').ToLowerInvariant() }
65+
}
66+
67+
$normalizedAdd = NormalizePath $PathToAdd
68+
69+
# Helper to build new PATH string with PathToAdd inserted before first 'git' entry
70+
function BuildPathWithInsert([string]$existingPath, [string]$toInsert) {
71+
$entries = @()
72+
if ($existingPath) { $entries = ($existingPath -split $sep) | Where-Object { $_ -and $_.Trim() -ne '' } }
73+
74+
# De-duplicate and remove any existing instance of $toInsert
75+
$list = New-Object System.Collections.Generic.List[string]
76+
$seen = New-Object 'System.Collections.Generic.HashSet[string]'
77+
foreach ($e in $entries) {
78+
$n = NormalizePath $e
79+
if (-not $seen.Contains($n) -and $n -ne $normalizedAdd) {
80+
$seen.Add($n) | Out-Null
81+
$list.Add($e) | Out-Null
6882
}
69-
} catch { }
83+
}
84+
85+
# Find first index that matches 'git' anywhere (case-insensitive)
86+
$insertIndex = 0
87+
for ($i = 0; $i -lt $list.Count; $i++) {
88+
if ($list[$i] -match '(?i)git') { $insertIndex = $i; break }
89+
}
90+
91+
$list.Insert($insertIndex, $toInsert)
92+
return ($list -join $sep)
7093
}
7194

72-
if (-not $exists) {
73-
$newPath = if ($current) { ($current.TrimEnd($sep) + $sep + $PathToAdd) } else { $PathToAdd }
74-
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
75-
return $true
95+
# Try to update Machine PATH
96+
$updatedScope = $null
97+
try {
98+
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
99+
$newMachinePath = BuildPathWithInsert -existingPath $machinePath -toInsert $PathToAdd
100+
if ($newMachinePath -ne $machinePath) {
101+
[Environment]::SetEnvironmentVariable('Path', $newMachinePath, 'Machine')
102+
$updatedScope = 'Machine'
103+
} else {
104+
# Nothing changed at Machine scope; still treat as Machine for reporting
105+
$updatedScope = 'Machine'
106+
}
107+
} catch {
108+
# Access denied or not elevated; do NOT modify User PATH. Print big red error with instructions.
109+
$origGit = $null
110+
try { $origGit = Get-StdGitPath } catch { }
111+
$origGitDir = if ($origGit) { (Split-Path $origGit -Parent) } else { 'your Git installation directory' }
112+
Write-Host ''
113+
Write-Host 'ERROR: Unable to update the SYSTEM PATH (administrator rights required).' -ForegroundColor Red
114+
Write-Host 'Your PATH was NOT changed. To ensure git-ai takes precedence over Git:' -ForegroundColor Red
115+
Write-Host (" 1) Run PowerShell as Administrator and re-run this installer; OR") -ForegroundColor Red
116+
Write-Host (" 2) Manually edit the SYSTEM Path and move '{0}' before any entries containing 'Git' (e.g. '{1}')." -f $PathToAdd, $origGitDir) -ForegroundColor Red
117+
Write-Host " Steps: Start → type 'Environment Variables' → 'Edit the system environment variables' → Environment Variables →" -ForegroundColor Red
118+
Write-Host " Under 'System variables', select 'Path' → Edit → Move '{0}' to the top (before Git) → OK." -f $PathToAdd -ForegroundColor Red
119+
Write-Host ''
120+
$updatedScope = 'Error'
76121
}
77-
return $false
122+
123+
# Update current process PATH immediately for this session
124+
try {
125+
$procPath = $env:PATH
126+
$newProcPath = BuildPathWithInsert -existingPath $procPath -toInsert $PathToAdd
127+
if ($newProcPath -ne $procPath) { $env:PATH = $newProcPath }
128+
} catch { }
129+
130+
return $updatedScope
78131
}
79132

80133
# Detect architecture and OS
@@ -141,26 +194,22 @@ if ($stdGitPath) {
141194
$env:GIT_AI_GIT_PATH = $stdGitPath
142195
}
143196

144-
# TODO Install hooks
145-
# Write-Host 'Setting up IDE/agent hooks...'
146-
# try {
147-
# & $finalExe install-hooks | Out-Host
148-
# Write-Success 'Successfully set up IDE/agent hooks'
149-
# } catch {
150-
# Write-Host 'Warning: Failed to set up IDE/agent hooks; continuing without IDE/agent hooks.' -ForegroundColor Yellow
151-
# }
152-
153-
Write-Success "Successfully installed git-ai into $installDir"
154-
Write-Success "You can now run 'git-ai' from your terminal"
155-
156-
# Update PATH for the user if needed
157-
$added = Add-ToUserPath -PathToAdd $installDir
158-
if ($added) {
159-
if ($env:PATH -notmatch [Regex]::Escape($installDir)) {
160-
$env:PATH = "$installDir;" + $env:PATH
161-
}
162-
Write-Success "Updated your user PATH to include $installDir"
163-
Write-Host 'Restart your terminal for the change to take effect.'
197+
# Install hooks
198+
Write-Host 'Setting up IDE/agent hooks...'
199+
try {
200+
& $finalExe install-hooks | Out-Host
201+
Write-Success 'Successfully set up IDE/agent hooks'
202+
} catch {
203+
Write-Host 'Warning: Failed to set up IDE/agent hooks; continuing without IDE/agent hooks.' -ForegroundColor Yellow
164204
}
165205

206+
# Update PATH so our shim takes precedence over any Git entries
207+
$scope = Set-PathPrependBeforeGit -PathToAdd $installDir
208+
if ($scope -eq 'Machine') {
209+
Write-Success 'Successfully added git-ai to the system PATH.'
210+
} elseif ($scope -eq 'Error') {
211+
Write-Host 'PATH update failed: system PATH unchanged.' -ForegroundColor Red
212+
}
166213

214+
Write-Success "Successfully installed git-ai into $installDir"
215+
Write-Success "You can now run 'git-ai' from your terminal"

src/commands/install_hooks.rs

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub fn run(_args: &[String]) -> Result<(), GitAiError> {
1111
}
1212

1313
async fn async_run() -> Result<(), GitAiError> {
14+
let mut any_installed = false;
15+
1416
if check_claude_code() {
1517
// Install/update Claude Code hooks
1618
let spinner = Spinner::new("Claude code: installing hooks");
@@ -24,6 +26,7 @@ async fn async_run() -> Result<(), GitAiError> {
2426
} else {
2527
spinner.success("Claude code: Hooks installed");
2628
}
29+
any_installed = true;
2730
}
2831

2932
if check_cursor() {
@@ -38,6 +41,11 @@ async fn async_run() -> Result<(), GitAiError> {
3841
} else {
3942
spinner.success("Cursor: Hooks installed");
4043
}
44+
any_installed = true;
45+
}
46+
47+
if !any_installed {
48+
println!("No compatible IDEs or agent configurations detected. Nothing to install.");
4149
}
4250

4351
Ok(())
@@ -49,8 +57,8 @@ fn check_claude_code() -> bool {
4957
}
5058

5159
// Sometimes the binary won't be in the PATH, but the dotfiles will be
52-
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
53-
return Path::new(&home).join(".claude").exists();
60+
let home = home_dir();
61+
return home.join(".claude").exists();
5462
}
5563

5664
fn check_cursor() -> bool {
@@ -62,20 +70,38 @@ fn check_cursor() -> bool {
6270
// TODO Approach for Windows?
6371

6472
// Sometimes the binary won't be in the PATH, but the dotfiles will be
65-
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
66-
return Path::new(&home).join(".cursor").exists();
73+
let home = home_dir();
74+
return home.join(".cursor").exists();
6775
}
6876

6977
// Shared utilities
7078

7179
/// Check if a binary with the given name exists in the system PATH
7280
fn binary_exists(name: &str) -> bool {
73-
if let Ok(path) = std::env::var("PATH") {
74-
for dir in path.split(':') {
75-
let binary_path = std::path::Path::new(dir).join(name);
76-
if binary_path.exists() && binary_path.is_file() {
81+
if let Ok(path_var) = std::env::var("PATH") {
82+
for dir in std::env::split_paths(&path_var) {
83+
// First check exact name as provided
84+
let candidate = dir.join(name);
85+
if candidate.exists() && candidate.is_file() {
7786
return true;
7887
}
88+
89+
// On Windows, executables usually have extensions listed in PATHEXT
90+
#[cfg(windows)]
91+
{
92+
let pathext = std::env::var("PATHEXT").unwrap_or_else(|_| ".EXE;.BAT;.CMD;.COM".to_string());
93+
for ext in pathext.split(';') {
94+
let ext = ext.trim();
95+
if ext.is_empty() {
96+
continue;
97+
}
98+
let ext = if ext.starts_with('.') { ext.to_string() } else { format!(".{}", ext) };
99+
let candidate = dir.join(format!("{}{}", name, ext));
100+
if candidate.exists() && candidate.is_file() {
101+
return true;
102+
}
103+
}
104+
}
79105
}
80106
}
81107
false
@@ -326,13 +352,11 @@ fn merge_hooks(existing: Option<&Value>, desired: Option<&Value>) -> Option<Valu
326352
}
327353

328354
fn claude_settings_path() -> PathBuf {
329-
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
330-
Path::new(&home).join(".claude").join("settings.json")
355+
home_dir().join(".claude").join("settings.json")
331356
}
332357

333358
fn cursor_hooks_path() -> PathBuf {
334-
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
335-
Path::new(&home).join(".cursor").join("hooks.json")
359+
home_dir().join(".cursor").join("hooks.json")
336360
}
337361

338362
fn write_atomic(path: &Path, data: &[u8]) -> Result<(), GitAiError> {
@@ -346,10 +370,22 @@ fn write_atomic(path: &Path, data: &[u8]) -> Result<(), GitAiError> {
346370
Ok(())
347371
}
348372

373+
fn home_dir() -> PathBuf {
374+
if let Ok(home) = std::env::var("HOME") {
375+
return PathBuf::from(home);
376+
}
377+
#[cfg(windows)]
378+
{
379+
if let Ok(userprofile) = std::env::var("USERPROFILE") {
380+
return PathBuf::from(userprofile);
381+
}
382+
}
383+
PathBuf::from(".")
384+
}
385+
349386
// Loader
350387
struct Spinner {
351388
pb: ProgressBar,
352-
_handle: smol::Task<()>,
353389
}
354390

355391
impl Spinner {
@@ -362,17 +398,9 @@ impl Spinner {
362398
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
363399
);
364400
pb.set_message(message.to_string());
401+
pb.enable_steady_tick(std::time::Duration::from_millis(100));
365402

366-
// Start the auto-ticking task
367-
let pb_clone = pb.clone();
368-
let _handle = smol::spawn(async move {
369-
loop {
370-
pb_clone.tick();
371-
smol::Timer::after(std::time::Duration::from_millis(100)).await;
372-
}
373-
});
374-
375-
Self { pb, _handle }
403+
Self { pb }
376404
}
377405

378406
fn start(&self) {

0 commit comments

Comments
 (0)