383 lines
14 KiB
PowerShell
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
|
|
}
|