claude-meta/scripts/install.ps1

383 lines
14 KiB
PowerShell

# 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
}