# install.ps1 — Link claude-meta config into ~/.claude/ $ErrorActionPreference = "Stop" $repoRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) $sourceDir = Join-Path $repoRoot "home-claude" $claudeDir = Join-Path $env:USERPROFILE ".claude" # Verify source exists if (-not (Test-Path $sourceDir)) { Write-Host "ERROR: home-claude/ not found at $sourceDir" -ForegroundColor Red exit 1 } # Ensure ~/.claude/ is a real directory (never a symlink!) if (Test-Path $claudeDir) { $item = Get-Item $claudeDir -Force if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { Write-Host "ERROR: ~/.claude/ is a symlink/junction. It must be a real directory." -ForegroundColor Red Write-Host "Claude Code Bug #764: symlinked ~/.claude/ breaks file detection." -ForegroundColor Red exit 1 } } else { New-Item -ItemType Directory -Path $claudeDir -Force | Out-Null Write-Host "Created ~/.claude/" -ForegroundColor Cyan } # Verify same volume for junctions $sourceVolume = (Get-Item $sourceDir).PSDrive.Name $targetVolume = (Get-Item $claudeDir).PSDrive.Name if ($sourceVolume -ne $targetVolume) { Write-Host "WARNING: Source ($sourceVolume`:) and target ($targetVolume`:) are on different volumes." -ForegroundColor Yellow Write-Host "Junctions require same volume. Falling back to file copy for directories." -ForegroundColor Yellow $useJunctions = $false } else { $useJunctions = $true } Write-Host "" Write-Host "Installing claude-meta config..." -ForegroundColor Cyan Write-Host " Source: $sourceDir" -ForegroundColor Gray Write-Host " Target: $claudeDir" -ForegroundColor Gray Write-Host "" $success = $true # --- File Symlinks --- $files = @("CLAUDE.md", "settings.json") foreach ($file in $files) { $src = Join-Path $sourceDir $file $dst = Join-Path $claudeDir $file if (-not (Test-Path $src)) { Write-Host " SKIP: $file (not in repo)" -ForegroundColor Yellow continue } # Remove existing file/link if (Test-Path $dst) { Remove-Item $dst -Force } # Try symlink first try { New-Item -ItemType SymbolicLink -Path $dst -Target $src -ErrorAction Stop | Out-Null Write-Host " SYMLINK: $file -> $src" -ForegroundColor Green } catch { # Fallback: copy Write-Host " WARNING: Symlink failed for $file (need Developer Mode?). Using copy." -ForegroundColor Yellow Copy-Item $src $dst -Force Write-Host " COPY: $file (changes won't auto-sync!)" -ForegroundColor Yellow $success = $false } } # --- Directory Junctions --- $dirs = @("skills", "rules", "commands") foreach ($dir in $dirs) { $src = Join-Path $sourceDir $dir $dst = Join-Path $claudeDir $dir if (-not (Test-Path $src)) { Write-Host " SKIP: $dir/ (not in repo)" -ForegroundColor Yellow continue } # Remove existing dir/junction if (Test-Path $dst) { $item = Get-Item $dst -Force if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { # It's a junction/symlink — remove it cmd /c "rmdir `"$dst`"" 2>$null } else { Remove-Item $dst -Recurse -Force } } if ($useJunctions) { # Junction (no admin needed, same volume only) cmd /c "mklink /J `"$dst`" `"$src`"" | Out-Null if ($LASTEXITCODE -eq 0) { Write-Host " JUNCTION: $dir/ -> $src" -ForegroundColor Green } else { # Fallback: copy Copy-Item $src $dst -Recurse -Force Write-Host " COPY: $dir/ (changes won't auto-sync!)" -ForegroundColor Yellow $success = $false } } else { Copy-Item $src $dst -Recurse -Force Write-Host " COPY: $dir/ (different volume)" -ForegroundColor Yellow $success = $false } } # ============================================================ # GEMINI INSTALL # ============================================================ $geminiSourceDir = Join-Path $repoRoot "home-gemini" $geminiDir = Join-Path $env:USERPROFILE ".gemini" $claudeRulesDir = Join-Path $sourceDir "rules" Write-Host "" Write-Host "Installing Gemini config..." -ForegroundColor Cyan Write-Host " Source: $geminiSourceDir" -ForegroundColor Gray Write-Host " Target: $geminiDir" -ForegroundColor Gray Write-Host "" if (-not (Test-Path $geminiSourceDir)) { Write-Host " SKIP: home-gemini/ not found" -ForegroundColor Yellow } else { # Ensure ~/.gemini/ is a real directory if (Test-Path $geminiDir) { $item = Get-Item $geminiDir -Force if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { Write-Host " ERROR: ~/.gemini/ is a symlink/junction. Must be a real directory." -ForegroundColor Red $success = $false } } else { New-Item -ItemType Directory -Path $geminiDir -Force | Out-Null Write-Host " Created ~/.gemini/" -ForegroundColor Cyan } # Check same volume for junctions $geminiVolume = (Get-Item $geminiDir).PSDrive.Name $geminiUseJunctions = ($sourceVolume -eq $geminiVolume) # GEMINI.md — always copy (no symlink, Gemini CLI issue #11547) $geminiMdSrc = Join-Path $geminiSourceDir "GEMINI.md" $geminiMdDst = Join-Path $geminiDir "GEMINI.md" if (Test-Path $geminiMdSrc) { Copy-Item $geminiMdSrc $geminiMdDst -Force Write-Host " COPY: GEMINI.md (symlinks not supported by Gemini CLI)" -ForegroundColor Green } # settings.json — try symlink, fallback to copy $settingsSrc = Join-Path $geminiSourceDir "settings.json" $settingsDst = Join-Path $geminiDir "settings.json" if (Test-Path $settingsSrc) { if (Test-Path $settingsDst) { Remove-Item $settingsDst -Force } try { New-Item -ItemType SymbolicLink -Path $settingsDst -Target $settingsSrc -ErrorAction Stop | Out-Null Write-Host " SYMLINK: settings.json -> $settingsSrc" -ForegroundColor Green } catch { Copy-Item $settingsSrc $settingsDst -Force Write-Host " COPY: settings.json (symlink failed)" -ForegroundColor Yellow $success = $false } } # rules/ — junction to home-claude/rules/ (shared source of truth) $geminiRulesDst = Join-Path $geminiDir "rules" if (Test-Path $claudeRulesDir) { if (Test-Path $geminiRulesDst) { $item = Get-Item $geminiRulesDst -Force if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { cmd /c "rmdir `"$geminiRulesDst`"" 2>$null } else { Remove-Item $geminiRulesDst -Recurse -Force } } if ($geminiUseJunctions) { cmd /c "mklink /J `"$geminiRulesDst`" `"$claudeRulesDir`"" | Out-Null if ($LASTEXITCODE -eq 0) { Write-Host " JUNCTION: rules/ -> $claudeRulesDir" -ForegroundColor Green } else { Copy-Item $claudeRulesDir $geminiRulesDst -Recurse -Force Write-Host " COPY: rules/ (junction failed)" -ForegroundColor Yellow $success = $false } } else { Copy-Item $claudeRulesDir $geminiRulesDst -Recurse -Force Write-Host " COPY: rules/ (different volume)" -ForegroundColor Yellow $success = $false } } # skills/ — selective junctions for review/ and plan-project/ $geminiSkillsDir = Join-Path $geminiDir "skills" if (-not (Test-Path $geminiSkillsDir)) { New-Item -ItemType Directory -Path $geminiSkillsDir -Force | Out-Null } foreach ($skill in @("review", "plan-project")) { $skillSrc = Join-Path $sourceDir "skills\$skill" $skillDst = Join-Path $geminiSkillsDir $skill if (-not (Test-Path $skillSrc)) { Write-Host " SKIP: skills/$skill/ (not in repo)" -ForegroundColor Yellow continue } if (Test-Path $skillDst) { $item = Get-Item $skillDst -Force if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { cmd /c "rmdir `"$skillDst`"" 2>$null } else { Remove-Item $skillDst -Recurse -Force } } if ($geminiUseJunctions) { cmd /c "mklink /J `"$skillDst`" `"$skillSrc`"" | Out-Null if ($LASTEXITCODE -eq 0) { Write-Host " JUNCTION: skills/$skill/ -> $skillSrc" -ForegroundColor Green } else { Copy-Item $skillSrc $skillDst -Recurse -Force Write-Host " COPY: skills/$skill/ (junction failed)" -ForegroundColor Yellow $success = $false } } else { Copy-Item $skillSrc $skillDst -Recurse -Force Write-Host " COPY: skills/$skill/ (different volume)" -ForegroundColor Yellow $success = $false } } } # ============================================================ # PROJECT MEMORY JUNCTIONS # ============================================================ $projectMapFile = Join-Path $repoRoot "memory\projects\project-map.json" $projectsDir = Join-Path $claudeDir "projects" Write-Host "" Write-Host "Installing project memory junctions..." -ForegroundColor Cyan if (Test-Path $projectMapFile) { $projectMap = Get-Content $projectMapFile -Raw | ConvertFrom-Json foreach ($prop in $projectMap.PSObject.Properties) { $encodedName = $prop.Name $friendlyName = $prop.Value $memorySrc = Join-Path $repoRoot "memory\projects\$friendlyName" $projectDir = Join-Path $projectsDir $encodedName $memoryDst = Join-Path $projectDir "memory" if (-not (Test-Path $projectDir)) { Write-Host " SKIP: $encodedName (project dir not found)" -ForegroundColor Yellow continue } if (-not (Test-Path $memorySrc)) { Write-Host " SKIP: $friendlyName (repo dir not found)" -ForegroundColor Yellow continue } # Check if already a junction to the correct target if (Test-Path $memoryDst) { $item = Get-Item $memoryDst -Force if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { Write-Host " OK: $friendlyName (already linked)" -ForegroundColor Green continue } # Move existing memory files to repo before replacing $existing = Get-ChildItem $memoryDst -File -ErrorAction SilentlyContinue foreach ($f in $existing) { $destFile = Join-Path $memorySrc $f.Name if (-not (Test-Path $destFile)) { Move-Item $f.FullName $destFile Write-Host " MOVED: $($f.Name) -> $memorySrc" -ForegroundColor Cyan } } Remove-Item $memoryDst -Recurse -Force } if ($useJunctions) { cmd /c "mklink /J `"$memoryDst`" `"$memorySrc`"" | Out-Null if ($LASTEXITCODE -eq 0) { Write-Host " JUNCTION: $friendlyName -> $memorySrc" -ForegroundColor Green } else { Write-Host " FAIL: $friendlyName junction creation failed" -ForegroundColor Red $success = $false } } else { Copy-Item $memorySrc $memoryDst -Recurse -Force Write-Host " COPY: $friendlyName (different volume)" -ForegroundColor Yellow $success = $false } } } else { Write-Host " SKIP: project-map.json not found" -ForegroundColor Yellow } # --- Validation --- Write-Host "" Write-Host "Validating..." -ForegroundColor Cyan $valid = $true foreach ($file in $files) { $dst = Join-Path $claudeDir $file if (Test-Path $dst) { $content = Get-Content $dst -Raw -ErrorAction SilentlyContinue if ($content) { Write-Host " OK: $file readable" -ForegroundColor Green } else { Write-Host " FAIL: $file exists but empty/unreadable" -ForegroundColor Red $valid = $false } } else { Write-Host " FAIL: $file not found" -ForegroundColor Red $valid = $false } } foreach ($dir in $dirs) { $dst = Join-Path $claudeDir $dir if (Test-Path $dst) { $count = (Get-ChildItem $dst -Recurse -File).Count Write-Host " OK: $dir/ ($count files)" -ForegroundColor Green } else { Write-Host " FAIL: $dir/ not found" -ForegroundColor Red $valid = $false } } # Gemini validation if (Test-Path $geminiSourceDir) { $geminiMdDst = Join-Path $geminiDir "GEMINI.md" if (Test-Path $geminiMdDst) { $item = Get-Item $geminiMdDst -Force if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { Write-Host " FAIL: ~/.gemini/GEMINI.md is a symlink (Gemini CLI rejects this!)" -ForegroundColor Red $valid = $false } else { Write-Host " OK: ~/.gemini/GEMINI.md (real file)" -ForegroundColor Green } } else { Write-Host " FAIL: ~/.gemini/GEMINI.md not found" -ForegroundColor Red $valid = $false } $geminiRulesDst = Join-Path $geminiDir "rules" if (Test-Path $geminiRulesDst) { $count = (Get-ChildItem $geminiRulesDst -Recurse -File).Count Write-Host " OK: ~/.gemini/rules/ ($count files)" -ForegroundColor Green } else { Write-Host " FAIL: ~/.gemini/rules/ not found" -ForegroundColor Red $valid = $false } } # Project memory validation if (Test-Path $projectMapFile) { $projectMap = Get-Content $projectMapFile -Raw | ConvertFrom-Json foreach ($prop in $projectMap.PSObject.Properties) { $memoryDst = Join-Path $projectsDir "$($prop.Name)\memory" if (Test-Path $memoryDst) { $item = Get-Item $memoryDst -Force $isJunction = $item.Attributes -band [System.IO.FileAttributes]::ReparsePoint $label = if ($isJunction) { "junction" } else { "copy" } Write-Host " OK: $($prop.Value) memory/ ($label)" -ForegroundColor Green } } } # --- Summary --- Write-Host "" if ($valid) { Write-Host "Installation complete!" -ForegroundColor Green if (-not $success) { Write-Host "" Write-Host "NOTE: Some items were copied instead of linked." -ForegroundColor Yellow Write-Host "Enable Developer Mode (Settings > Developer) for symlinks," -ForegroundColor Yellow Write-Host "or re-run after changes with: .\scripts\install.ps1" -ForegroundColor Yellow } } else { Write-Host "Installation had errors. Check output above." -ForegroundColor Red exit 1 }