#Requires -Version 5.1 <# .SYNOPSIS Claude Code Workflow System Interactive Installer with Version Management .DESCRIPTION Installation script for Claude Code Workflow System with Agent coordination and distributed memory system. Features automatic version management: - Tracks all installed files and directories in manifest - Automatically moves old installation to backup folder before new installation - Preserves version history in backup folders - Supports clean uninstallation based on manifest records Installs globally to user profile directory (~/.claude) by default. .PARAMETER InstallMode Installation mode: "Global" (default) or "Path" (hybrid local + global) .PARAMETER TargetPath Target path for Path installation mode .PARAMETER Force Skip confirmation prompts .PARAMETER NonInteractive Run in non-interactive mode with default options .PARAMETER BackupAll Automatically backup all existing files without confirmation prompts (enabled by default) .PARAMETER NoBackup Disable automatic backup functionality .PARAMETER Uninstall Uninstall Claude Code Workflow System based on installation manifest .EXAMPLE .\Install-Claude.ps1 Interactive installation with mode selection .EXAMPLE .\Install-Claude.ps1 -InstallMode Global -Force Global installation without prompts .EXAMPLE .\Install-Claude.ps1 -Force -NonInteractive Global installation without prompts .EXAMPLE .\Install-Claude.ps1 -BackupAll Global installation with automatic backup of all existing files .EXAMPLE .\Install-Claude.ps1 -NoBackup Installation without any backup (overwrite existing files) .EXAMPLE .\Install-Claude.ps1 -Uninstall Uninstall Claude Code Workflow System .EXAMPLE .\Install-Claude.ps1 -Uninstall -Force Uninstall without confirmation prompts .NOTES Version Management: - First installation: Creates manifest and installs files - Subsequent installations: Automatically moves old files to backup folder (claude-backup-old-TIMESTAMP) - Manifest location: ~/.claude-manifests/ - Each installation path maintains its own manifest - Uninstall uses manifest to remove only installed files Backup Folders: - Old version backups: claude-backup-old-YYYYMMDD-HHMMSS (automatic on reinstall) - File conflict backups: claude-backup-YYYYMMDD-HHMMSS (when BackupAll enabled) #> param( [ValidateSet("Global", "Path")] [string]$InstallMode = "", [string]$TargetPath = "", [switch]$Force, [switch]$NonInteractive, [switch]$BackupAll, [switch]$NoBackup, [switch]$Uninstall, [string]$SourceVersion = "", [string]$SourceBranch = "", [string]$SourceCommit = "" ) # Set encoding for proper Unicode support if ($PSVersionTable.PSVersion.Major -ge 6) { $OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8 } else { # For Windows PowerShell 5.1 chcp 65001 | Out-Null } # Script metadata $ScriptName = "Claude Code Workflow System Installer" $ScriptVersion = "2.3.0" # Installer script version - Added automatic version cleanup # Default version (will be overridden by -SourceVersion from install-remote.ps1) $DefaultVersion = "unknown" # Initialize backup behavior - backup is enabled by default unless NoBackup is specified if (-not $BackupAll -and -not $NoBackup) { $BackupAll = $true Write-Verbose "Auto-backup enabled by default. Use -NoBackup to disable." } # Colors for output $ColorSuccess = "Green" $ColorInfo = "Cyan" $ColorWarning = "Yellow" $ColorError = "Red" $ColorPrompt = "Magenta" # Global manifest directory location $script:ManifestDir = Join-Path ([Environment]::GetFolderPath("UserProfile")) ".claude-manifests" function Write-ColorOutput { param( [string]$Message, [string]$Color = "White" ) Write-Host $Message -ForegroundColor $Color } function Show-Banner { Write-Host "" # CLAUDE - Cyan color Write-Host ' ______ __ __ ' -ForegroundColor Cyan Write-Host ' / \ | \ | \ ' -ForegroundColor Cyan Write-Host '| $$$$$$\| $$ ______ __ __ ____| $$ ______ ' -ForegroundColor Cyan Write-Host '| $$ \$$| $$ | \ | \ | \ / $$ / \ ' -ForegroundColor Cyan Write-Host '| $$ | $$ \$$$$$$\| $$ | $$| $$$$$$$| $$$$$$\ ' -ForegroundColor Cyan Write-Host '| $$ __ | $$ / $$| $$ | $$| $$ | $$| $$ $$ ' -ForegroundColor Cyan Write-Host '| $$__/ \| $$| $$$$$$$| $$__/ $$| $$__| $$| $$$$$$$$ ' -ForegroundColor Cyan Write-Host ' \$$ $$| $$ \$$ $$ \$$ $$ \$$ $$ \$$ \ ' -ForegroundColor Cyan Write-Host ' \$$$$$$ \$$ \$$$$$$$ \$$$$$$ \$$$$$$$ \$$$$$$$ ' -ForegroundColor Cyan Write-Host "" # CODE - Green color Write-Host ' ______ __ ' -ForegroundColor Green Write-Host '/ \ | \ ' -ForegroundColor Green Write-Host '| $$$$$$\ ______ ____| $$ ______ ' -ForegroundColor Green Write-Host '| $$ \$$ / \ / $$ / \ ' -ForegroundColor Green Write-Host '| $$ | $$$$$$\| $$$$$$$| $$$$$$\ ' -ForegroundColor Green Write-Host '| $$ __ | $$ | $$| $$ | $$| $$ $$ ' -ForegroundColor Green Write-Host '| $$__/ \| $$__/ $$| $$__| $$| $$$$$$$$ ' -ForegroundColor Green Write-Host ' \$$ $$ \$$ $$ \$$ $$ \$$ \ ' -ForegroundColor Green Write-Host ' \$$$$$$ \$$$$$$ \$$$$$$$ \$$$$$$$ ' -ForegroundColor Green Write-Host "" # WORKFLOW - Yellow color Write-Host '__ __ __ ______ __ ' -ForegroundColor Yellow Write-Host '| \ _ | \ | \ / \ | \ ' -ForegroundColor Yellow Write-Host '| $$ / \ | $$ ______ ______ | $$ __ | $$$$$$\| $$ ______ __ __ __ ' -ForegroundColor Yellow Write-Host '| $$/ $\| $$ / \ / \ | $$ / \| $$_ \$$| $$ / \ | \ | \ | \' -ForegroundColor Yellow Write-Host '| $$ $$$\ $$| $$$$$$\| $$$$$$\| $$_/ $$| $$ \ | $$| $$$$$$\| $$ | $$ | $$' -ForegroundColor Yellow Write-Host '| $$ $$\$$\$$| $$ | $$| $$ \$$| $$ $$ | $$$$ | $$| $$ | $$| $$ | $$ | $$' -ForegroundColor Yellow Write-Host '| $$$$ \$$$$| $$__/ $$| $$ | $$$$$$\ | $$ | $$| $$__/ $$| $$_/ $$_/ $$' -ForegroundColor Yellow Write-Host '| $$$ \$$$ \$$ $$| $$ | $$ \$$\| $$ | $$ \$$ $$ \$$ $$ $$' -ForegroundColor Yellow Write-Host ' \$$ \$$ \$$$$$$ \$$ \$$ \$$ \$$ \$$ \$$$$$$ \$$$$$\$$$$' -ForegroundColor Yellow Write-Host "" } function Show-Header { param( [string]$InstallVersion = $DefaultVersion ) Show-Banner Write-ColorOutput " $ScriptName v$ScriptVersion" $ColorInfo if ($InstallVersion -ne "unknown") { Write-ColorOutput " Installing Claude Code Workflow v$InstallVersion" $ColorInfo } Write-ColorOutput " Unified workflow system with comprehensive coordination" $ColorInfo Write-ColorOutput "========================================================================" $ColorInfo if ($NoBackup) { Write-ColorOutput "WARNING: Backup disabled - existing files will be overwritten!" $ColorWarning } else { Write-ColorOutput "Auto-backup enabled - existing files will be backed up" $ColorSuccess } Write-Host "" } function Test-Prerequisites { # Test PowerShell version if ($PSVersionTable.PSVersion.Major -lt 5) { Write-ColorOutput "ERROR: PowerShell 5.1 or higher is required" $ColorError Write-ColorOutput "Current version: $($PSVersionTable.PSVersion)" $ColorError return $false } # Test source files exist $sourceDir = $PSScriptRoot $claudeDir = Join-Path $sourceDir ".claude" $claudeMd = Join-Path $sourceDir "CLAUDE.md" $codexDir = Join-Path $sourceDir ".codex" $geminiDir = Join-Path $sourceDir ".gemini" $qwenDir = Join-Path $sourceDir ".qwen" if (-not (Test-Path $claudeDir)) { Write-ColorOutput "ERROR: .claude directory not found in $sourceDir" $ColorError return $false } if (-not (Test-Path $claudeMd)) { Write-ColorOutput "ERROR: CLAUDE.md file not found in $sourceDir" $ColorError return $false } if (-not (Test-Path $codexDir)) { Write-ColorOutput "ERROR: .codex directory not found in $sourceDir" $ColorError return $false } if (-not (Test-Path $geminiDir)) { Write-ColorOutput "ERROR: .gemini directory not found in $sourceDir" $ColorError return $false } if (-not (Test-Path $qwenDir)) { Write-ColorOutput "ERROR: .qwen directory not found in $sourceDir" $ColorError return $false } Write-ColorOutput "Prerequisites check passed" $ColorSuccess return $true } function Get-UserChoiceWithArrows { param( [string]$Prompt, [string[]]$Options, [int]$DefaultIndex = 0 ) if ($NonInteractive) { Write-ColorOutput "Non-interactive mode: Using default '$($Options[$DefaultIndex])'" $ColorInfo return $Options[$DefaultIndex] } # Test if we can use console features (interactive terminal) $canUseConsole = $true try { $null = [Console]::CursorVisible $null = $Host.UI.RawUI.ReadKey } catch { $canUseConsole = $false } # Fallback to simple numbered menu if console not available if (-not $canUseConsole) { Write-ColorOutput "Arrow navigation not available in this environment. Using numbered menu." $ColorWarning return Get-UserChoice -Prompt $Prompt -Options $Options -Default $Options[$DefaultIndex] } $selectedIndex = $DefaultIndex $cursorVisible = $true try { $cursorVisible = [Console]::CursorVisible [Console]::CursorVisible = $false } catch { # Silently continue if cursor control fails } try { Write-Host "" Write-ColorOutput $Prompt $ColorPrompt Write-Host "" while ($true) { # Display options for ($i = 0; $i -lt $Options.Count; $i++) { $prefix = if ($i -eq $selectedIndex) { " > " } else { " " } $color = if ($i -eq $selectedIndex) { $ColorSuccess } else { "White" } # Clear line and write option Write-Host "`r$prefix$($Options[$i])".PadRight(80) -ForegroundColor $color } Write-Host "" Write-Host " Use " -NoNewline -ForegroundColor DarkGray Write-Host "UP/DOWN" -NoNewline -ForegroundColor Yellow Write-Host " arrows to navigate, " -NoNewline -ForegroundColor DarkGray Write-Host "ENTER" -NoNewline -ForegroundColor Yellow Write-Host " to select, or type " -NoNewline -ForegroundColor DarkGray Write-Host "1-$($Options.Count)" -NoNewline -ForegroundColor Yellow Write-Host "" -ForegroundColor DarkGray # Read key $key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") # Handle arrow keys if ($key.VirtualKeyCode -eq 38) { # Up arrow $selectedIndex = if ($selectedIndex -gt 0) { $selectedIndex - 1 } else { $Options.Count - 1 } } elseif ($key.VirtualKeyCode -eq 40) { # Down arrow $selectedIndex = if ($selectedIndex -lt ($Options.Count - 1)) { $selectedIndex + 1 } else { 0 } } elseif ($key.VirtualKeyCode -eq 13) { # Enter key Write-Host "" return $Options[$selectedIndex] } elseif ($key.Character -match '^\d$') { # Number key $num = [int]::Parse($key.Character) if ($num -ge 1 -and $num -le $Options.Count) { Write-Host "" return $Options[$num - 1] } } # Move cursor back up to redraw menu $linesToMove = $Options.Count + 2 try { for ($i = 0; $i -lt $linesToMove; $i++) { [Console]::SetCursorPosition(0, [Console]::CursorTop - 1) } } catch { # If cursor positioning fails, just continue break } } } finally { try { [Console]::CursorVisible = $cursorVisible } catch { # Silently continue if cursor control fails } } } function Get-UserChoice { param( [string]$Prompt, [string[]]$Options, [string]$Default = $null ) if ($NonInteractive -and $Default) { Write-ColorOutput "Non-interactive mode: Using default '$Default'" $ColorInfo return $Default } Write-ColorOutput $Prompt $ColorPrompt for ($i = 0; $i -lt $Options.Count; $i++) { if ($Default -and $Options[$i] -eq $Default) { $marker = " (default)" } else { $marker = "" } Write-Host " $($i + 1). $($Options[$i])$marker" } do { $input = Read-Host "Please select (1-$($Options.Count))" if ([string]::IsNullOrWhiteSpace($input) -and $Default) { return $Default } $index = $null if ([int]::TryParse($input, [ref]$index) -and $index -ge 1 -and $index -le $Options.Count) { return $Options[$index - 1] } Write-ColorOutput "Invalid selection. Please enter a number between 1 and $($Options.Count)" $ColorWarning } while ($true) } function Confirm-Action { param( [string]$Message, [switch]$DefaultYes ) if ($Force) { Write-ColorOutput "Force mode: Proceeding with '$Message'" $ColorInfo return $true } if ($NonInteractive) { if ($DefaultYes) { $result = $true } else { $result = $false } if ($result) { $resultText = 'Yes' } else { $resultText = 'No' } Write-ColorOutput "Non-interactive mode: $Message - $resultText" $ColorInfo return $result } if ($DefaultYes) { $defaultChar = "Y" $prompt = "(Y/n)" } else { $defaultChar = "N" $prompt = "(y/N)" } do { $response = Read-Host "$Message $prompt" if ([string]::IsNullOrWhiteSpace($response)) { return $DefaultYes } switch ($response.ToLower()) { { $_ -in @('y', 'yes') } { return $true } { $_ -in @('n', 'no') } { return $false } default { Write-ColorOutput "Please answer 'y' or 'n'" $ColorWarning } } } while ($true) } function Get-BackupDirectory { param( [string]$TargetDirectory ) $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $backupDirName = "claude-backup-$timestamp" $backupPath = Join-Path $TargetDirectory $backupDirName # Ensure backup directory exists if (-not (Test-Path $backupPath)) { New-Item -ItemType Directory -Path $backupPath -Force | Out-Null } return $backupPath } function Backup-FileToFolder { param( [string]$FilePath, [string]$BackupFolder, [switch]$Quiet ) if (-not (Test-Path $FilePath)) { return $false } try { $fileName = Split-Path $FilePath -Leaf $relativePath = "" # Try to determine relative path structure for better organization $fileDir = Split-Path $FilePath -Parent if ($fileDir -match '\.claude') { # Extract path relative to .claude directory $claudeIndex = $fileDir.LastIndexOf('.claude') if ($claudeIndex -ge 0) { $relativePath = $fileDir.Substring($claudeIndex + 7) # +7 for ".claude\" if ($relativePath.StartsWith('\')) { $relativePath = $relativePath.Substring(1) } } } # Create subdirectory structure in backup if needed $backupSubDir = $BackupFolder if (-not [string]::IsNullOrEmpty($relativePath)) { $backupSubDir = Join-Path $BackupFolder $relativePath if (-not (Test-Path $backupSubDir)) { New-Item -ItemType Directory -Path $backupSubDir -Force | Out-Null } } $backupFilePath = Join-Path $backupSubDir $fileName Copy-Item -Path $FilePath -Destination $backupFilePath -Force if (-not $Quiet) { Write-ColorOutput "Backed up: $fileName" $ColorInfo } return $true } catch { if (-not $Quiet) { Write-ColorOutput "WARNING: Failed to backup file $FilePath`: $($_.Exception.Message)" $ColorWarning } return $false } } function Backup-DirectoryToFolder { param( [string]$DirectoryPath, [string]$BackupFolder ) if (-not (Test-Path $DirectoryPath)) { return $false } try { $dirName = Split-Path $DirectoryPath -Leaf $backupDirPath = Join-Path $BackupFolder $dirName Copy-Item -Path $DirectoryPath -Destination $backupDirPath -Recurse -Force Write-ColorOutput "Backed up directory: $dirName" $ColorInfo return $true } catch { Write-ColorOutput "WARNING: Failed to backup directory $DirectoryPath`: $($_.Exception.Message)" $ColorWarning return $false } } function Copy-DirectoryRecursive { param( [string]$Source, [string]$Destination ) if (-not (Test-Path $Source)) { throw "Source directory does not exist: $Source" } # Create destination directory if it doesn't exist if (-not (Test-Path $Destination)) { New-Item -ItemType Directory -Path $Destination -Force | Out-Null } try { # Copy all items recursively Copy-Item -Path "$Source\*" -Destination $Destination -Recurse -Force Write-ColorOutput "Directory copied: $Source -> $Destination" $ColorSuccess } catch { throw "Failed to copy directory: $($_.Exception.Message)" } } function Copy-FileToDestination { param( [string]$Source, [string]$Destination, [string]$Description = "file", [string]$BackupFolder = $null ) if (Test-Path $Destination) { # Use BackupAll mode for automatic backup without confirmation (default behavior) if ($BackupAll -and -not $NoBackup) { if ($BackupFolder -and (Backup-FileToFolder -FilePath $Destination -BackupFolder $BackupFolder)) { Write-ColorOutput "Auto-backed up: $Description" $ColorSuccess } Copy-Item -Path $Source -Destination $Destination -Force Write-ColorOutput "$Description updated (with backup)" $ColorSuccess return $true } elseif ($NoBackup) { # No backup mode - ask for confirmation if (Confirm-Action "$Description already exists. Replace it? (NO BACKUP)" -DefaultYes:$false) { Copy-Item -Path $Source -Destination $Destination -Force Write-ColorOutput "$Description updated (no backup)" $ColorWarning return $true } else { Write-ColorOutput "Skipping $Description installation" $ColorWarning return $false } } elseif (Confirm-Action "$Description already exists. Replace it?" -DefaultYes:$false) { if ($BackupFolder -and (Backup-FileToFolder -FilePath $Destination -BackupFolder $BackupFolder)) { Write-ColorOutput "Existing $Description backed up" $ColorSuccess } Copy-Item -Path $Source -Destination $Destination -Force Write-ColorOutput "$Description updated" $ColorSuccess return $true } else { Write-ColorOutput "Skipping $Description installation" $ColorWarning return $false } } else { # Ensure destination directory exists $destinationDir = Split-Path $Destination -Parent if (-not (Test-Path $destinationDir)) { New-Item -ItemType Directory -Path $destinationDir -Force | Out-Null } Copy-Item -Path $Source -Destination $Destination -Force Write-ColorOutput "$Description installed" $ColorSuccess return $true } } function Backup-AndReplaceDirectory { param( [string]$Source, [string]$Destination, [string]$Description = "directory", [string]$BackupFolder = $null ) if (-not (Test-Path $Source)) { Write-ColorOutput "WARNING: Source $Description not found: $Source" $ColorWarning return $false } # Backup destination if it exists if (Test-Path $Destination) { Write-ColorOutput "Found existing $Description at: $Destination" $ColorInfo # Backup entire directory if backup is enabled if (-not $NoBackup -and $BackupFolder) { Write-ColorOutput "Backing up entire $Description..." $ColorInfo if (Backup-DirectoryToFolder -DirectoryPath $Destination -BackupFolder $BackupFolder) { Write-ColorOutput "Backed up $Description to: $BackupFolder" $ColorSuccess } } elseif ($NoBackup) { if (-not (Confirm-Action "Replace existing $Description without backup?" -DefaultYes:$false)) { Write-ColorOutput "Skipping $Description installation" $ColorWarning return $false } } # Get all items from source to determine what to clear in destination Write-ColorOutput "Clearing conflicting items in destination $Description..." $ColorInfo $sourceItems = Get-ChildItem -Path $Source -Force foreach ($sourceItem in $sourceItems) { $destItemPath = Join-Path $Destination $sourceItem.Name if (Test-Path $destItemPath) { Write-ColorOutput "Removing existing: $($sourceItem.Name)" $ColorInfo Remove-Item -Path $destItemPath -Recurse -Force -ErrorAction SilentlyContinue } } Write-ColorOutput "Cleared conflicting items in destination" $ColorSuccess } else { # Create destination directory if it doesn't exist New-Item -ItemType Directory -Path $Destination -Force | Out-Null Write-ColorOutput "Created destination directory: $Destination" $ColorInfo } # Copy all items from source to destination Write-ColorOutput "Copying $Description from $Source to $Destination..." $ColorInfo $sourceItems = Get-ChildItem -Path $Source -Force foreach ($item in $sourceItems) { $destPath = Join-Path $Destination $item.Name Copy-Item -Path $item.FullName -Destination $destPath -Recurse -Force } Write-ColorOutput "$Description installed successfully" $ColorSuccess return $true } function Backup-CriticalConfigFiles { param( [string]$TargetDirectory, [string]$BackupFolder, [string[]]$FileNames ) if (-not $BackupFolder -or $NoBackup) { return } if (-not (Test-Path $TargetDirectory)) { return } $backedUpCount = 0 foreach ($fileName in $FileNames) { $filePath = Join-Path $TargetDirectory $fileName if (Test-Path $filePath) { if (Backup-FileToFolder -FilePath $filePath -BackupFolder $BackupFolder) { Write-ColorOutput "Critical config backed up: $fileName" $ColorSuccess $backedUpCount++ } } } if ($backedUpCount -gt 0) { Write-ColorOutput "Backed up $backedUpCount critical configuration file(s)" $ColorInfo } } function Merge-DirectoryContents { param( [string]$Source, [string]$Destination, [string]$Description = "directory contents", [string]$BackupFolder = $null ) if (-not (Test-Path $Source)) { Write-ColorOutput "WARNING: Source $Description not found: $Source" $ColorWarning return $false } # Create destination directory if it doesn't exist if (-not (Test-Path $Destination)) { New-Item -ItemType Directory -Path $Destination -Force | Out-Null Write-ColorOutput "Created destination directory: $Destination" $ColorInfo } # Get all items in source directory $sourceItems = Get-ChildItem -Path $Source -Recurse -File $totalFiles = $sourceItems.Count $mergedCount = 0 $skippedCount = 0 $backedUpCount = 0 $processedCount = 0 Write-ColorOutput "Processing $totalFiles files in $Description..." $ColorInfo foreach ($item in $sourceItems) { $processedCount++ # Calculate relative path from source $relativePath = $item.FullName.Substring($Source.Length + 1) $destinationPath = Join-Path $Destination $relativePath # Ensure destination directory exists $destinationDir = Split-Path $destinationPath -Parent if (-not (Test-Path $destinationDir)) { New-Item -ItemType Directory -Path $destinationDir -Force | Out-Null } # Handle file merging if (Test-Path $destinationPath) { $fileName = Split-Path $relativePath -Leaf # Use BackupAll mode for automatic backup without confirmation (default behavior) if ($BackupAll -and -not $NoBackup) { if ($BackupFolder -and (Backup-FileToFolder -FilePath $destinationPath -BackupFolder $BackupFolder -Quiet)) { $backedUpCount++ } Copy-Item -Path $item.FullName -Destination $destinationPath -Force $mergedCount++ } elseif ($NoBackup) { # No backup mode - ask for confirmation if (Confirm-Action "File '$relativePath' already exists. Replace it? (NO BACKUP)" -DefaultYes:$false) { Copy-Item -Path $item.FullName -Destination $destinationPath -Force $mergedCount++ } else { $skippedCount++ } } elseif (Confirm-Action "File '$relativePath' already exists. Replace it?" -DefaultYes:$false) { if ($BackupFolder -and (Backup-FileToFolder -FilePath $destinationPath -BackupFolder $BackupFolder -Quiet)) { $backedUpCount++ } Copy-Item -Path $item.FullName -Destination $destinationPath -Force $mergedCount++ } else { $skippedCount++ } } else { Copy-Item -Path $item.FullName -Destination $destinationPath -Force $mergedCount++ } # Show progress every 20 files if ($processedCount % 20 -eq 0 -or $processedCount -eq $totalFiles) { Write-Progress -Activity "Merging $Description" -Status "$processedCount/$totalFiles files processed" -PercentComplete (($processedCount / $totalFiles) * 100) } } Write-Progress -Activity "Merging $Description" -Completed if ($backedUpCount -gt 0) { Write-ColorOutput "Merged $mergedCount files ($backedUpCount backed up), skipped $skippedCount files" $ColorSuccess } else { Write-ColorOutput "Merged $mergedCount files, skipped $skippedCount files" $ColorSuccess } return $true } # ============================================================================ # INSTALLATION MANIFEST MANAGEMENT # ============================================================================ function New-InstallManifest { <# .SYNOPSIS Create a new installation manifest to track installed files #> param( [string]$InstallationMode, [string]$InstallationPath ) # Create manifest directory if it doesn't exist if (-not (Test-Path $script:ManifestDir)) { New-Item -ItemType Directory -Path $script:ManifestDir -Force | Out-Null } # Generate unique manifest ID based on timestamp and mode # Distinguish between Global and Path installations with clear naming $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $modePrefix = if ($InstallationMode -eq "Global") { "manifest-global" } else { "manifest-path" } $manifestId = "$modePrefix-$timestamp" $manifest = @{ manifest_id = $manifestId version = "1.0" installation_mode = $InstallationMode installation_path = $InstallationPath installation_date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") installer_version = $ScriptVersion files = @() directories = @() } return $manifest } function Add-ManifestEntry { <# .SYNOPSIS Add a file or directory entry to the manifest #> param( [Parameter(Mandatory=$true)] [hashtable]$Manifest, [Parameter(Mandatory=$true)] [string]$Path, [Parameter(Mandatory=$true)] [ValidateSet("File", "Directory")] [string]$Type ) $entry = @{ path = $Path type = $Type timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") } if ($Type -eq "File") { $Manifest.files += $entry } else { $Manifest.directories += $entry } } function Add-ManifestEntriesBulk { <# .SYNOPSIS Bulk add file entries to manifest for better performance #> param( [Parameter(Mandatory=$true)] [hashtable]$Manifest, [Parameter(Mandatory=$true)] [string]$SourceDirectory, [Parameter(Mandatory=$true)] [string]$TargetDirectory, [string]$Description = "files" ) if (-not (Test-Path $SourceDirectory)) { Write-ColorOutput "WARNING: Source directory not found: $SourceDirectory" $ColorWarning return 0 } $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") # Add directory entry Add-ManifestEntry -Manifest $Manifest -Path $TargetDirectory -Type "Directory" # Get all source files $sourceFiles = Get-ChildItem -Path $SourceDirectory -Recurse -File $fileEntries = [System.Collections.ArrayList]::new() Write-ColorOutput "Adding $($sourceFiles.Count) $Description to manifest..." $ColorInfo foreach ($file in $sourceFiles) { $relativePath = $file.FullName.Substring($SourceDirectory.Length) $targetPath = $TargetDirectory + $relativePath $entry = @{ path = $targetPath type = "File" timestamp = $timestamp } $null = $fileEntries.Add($entry) } # Bulk add to manifest $Manifest.files += $fileEntries Write-ColorOutput "Added $($fileEntries.Count) $Description to manifest" $ColorSuccess return $fileEntries.Count } function Remove-OldManifestsForPath { <# .SYNOPSIS Remove old manifest files for the same installation path and mode #> param( [Parameter(Mandatory=$true)] [string]$InstallationPath, [Parameter(Mandatory=$true)] [string]$InstallationMode ) if (-not (Test-Path $script:ManifestDir)) { return } try { # Get both new (manifest-*) and old (install-*) format manifest files $manifestFiles = Get-ChildItem -Path $script:ManifestDir -Filter "*-*.json" -File | Where-Object { $_.Name -match '^(manifest|install)-' } $removedCount = 0 foreach ($file in $manifestFiles) { try { $manifestJson = Get-Content -Path $file.FullName -Raw -Encoding utf8 $manifest = $manifestJson | ConvertFrom-Json # Normalize paths for comparison (case-insensitive, handle trailing slashes) $manifestPath = if ($manifest.installation_path) { $manifest.installation_path.TrimEnd('\', '/').ToLower() } else { "" } $targetPath = $InstallationPath.TrimEnd('\', '/').ToLower() # Get installation mode $manifestMode = if ($manifest.installation_mode) { $manifest.installation_mode } else { "Global" } # Only remove if BOTH path and mode match if ($manifestPath -eq $targetPath -and $manifestMode -eq $InstallationMode) { Remove-Item -Path $file.FullName -Force Write-ColorOutput "Removed old manifest: $($file.Name)" $ColorInfo $removedCount++ } } catch { Write-ColorOutput "WARNING: Failed to process manifest $($file.Name): $($_.Exception.Message)" $ColorWarning } } if ($removedCount -gt 0) { Write-ColorOutput "Removed $removedCount old manifest(s) for installation path: $InstallationPath" $ColorSuccess } } catch { Write-ColorOutput "WARNING: Failed to clean old manifests: $($_.Exception.Message)" $ColorWarning } } function Save-InstallManifest { <# .SYNOPSIS Save the installation manifest to disk #> param( [Parameter(Mandatory=$true)] [hashtable]$Manifest ) try { # Remove old manifests for the same installation path and mode if ($Manifest.installation_path -and $Manifest.installation_mode) { Remove-OldManifestsForPath -InstallationPath $Manifest.installation_path -InstallationMode $Manifest.installation_mode } # Use manifest ID to create unique file name $manifestFileName = "$($Manifest.manifest_id).json" $manifestPath = Join-Path $script:ManifestDir $manifestFileName $Manifest | ConvertTo-Json -Depth 10 | Out-File -FilePath $manifestPath -Encoding utf8 -Force Write-ColorOutput "Installation manifest saved: $manifestPath" $ColorSuccess return $true } catch { Write-ColorOutput "WARNING: Failed to save installation manifest: $($_.Exception.Message)" $ColorWarning return $false } } function Migrate-LegacyManifest { <# .SYNOPSIS Migrate old single manifest file to new multi-manifest system #> $legacyManifestPath = Join-Path ([Environment]::GetFolderPath("UserProfile")) ".claude-install-manifest.json" if (-not (Test-Path $legacyManifestPath)) { return } try { Write-ColorOutput "Found legacy manifest file, migrating to new system..." $ColorInfo # Create manifest directory if it doesn't exist if (-not (Test-Path $script:ManifestDir)) { New-Item -ItemType Directory -Path $script:ManifestDir -Force | Out-Null } # Read legacy manifest $legacyJson = Get-Content -Path $legacyManifestPath -Raw -Encoding utf8 $legacy = $legacyJson | ConvertFrom-Json # Generate new manifest ID with new naming convention $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $mode = if ($legacy.installation_mode) { $legacy.installation_mode } else { "Global" } $modePrefix = if ($mode -eq "Global") { "manifest-global" } else { "manifest-path" } $manifestId = "$modePrefix-$timestamp-migrated" # Create new manifest with all fields $newManifest = @{ manifest_id = $manifestId version = if ($legacy.version) { $legacy.version } else { "1.0" } installation_mode = $mode installation_path = if ($legacy.installation_path) { $legacy.installation_path } else { [Environment]::GetFolderPath("UserProfile") } installation_date = if ($legacy.installation_date) { $legacy.installation_date } else { (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") } installer_version = if ($legacy.installer_version) { $legacy.installer_version } else { "unknown" } files = if ($legacy.files) { @($legacy.files) } else { @() } directories = if ($legacy.directories) { @($legacy.directories) } else { @() } } # Save to new location $newManifestPath = Join-Path $script:ManifestDir "$manifestId.json" $newManifest | ConvertTo-Json -Depth 10 | Out-File -FilePath $newManifestPath -Encoding utf8 -Force # Rename old manifest (don't delete, keep as backup) $backupPath = "$legacyManifestPath.migrated" Move-Item -Path $legacyManifestPath -Destination $backupPath -Force Write-ColorOutput "Legacy manifest migrated successfully" $ColorSuccess Write-ColorOutput "Old manifest backed up to: $backupPath" $ColorInfo } catch { Write-ColorOutput "WARNING: Failed to migrate legacy manifest: $($_.Exception.Message)" $ColorWarning } } function Move-OldInstallation { <# .SYNOPSIS Move old installation files to backup folder based on manifest .DESCRIPTION This function finds the previous installation manifest for the given path, moves (cuts) the old files to a backup folder, and removes empty directories. #> param( [Parameter(Mandatory=$true)] [string]$InstallationPath, [Parameter(Mandatory=$true)] [string]$InstallationMode ) # Find existing manifest for this installation path $manifests = Get-AllInstallManifests if (-not $manifests -or $manifests.Count -eq 0) { Write-ColorOutput "No previous installation found - proceeding with fresh installation" $ColorInfo return $null } # Normalize paths for comparison $targetPath = $InstallationPath.TrimEnd('\', '/').ToLower() # Find manifest matching this installation path and mode $oldManifest = $manifests | Where-Object { $manifestPath = $_.installation_path.TrimEnd('\', '/').ToLower() $manifestPath -eq $targetPath -and $_.installation_mode -eq $InstallationMode } | Select-Object -First 1 if (-not $oldManifest) { Write-ColorOutput "No previous $InstallationMode installation found at this path" $ColorInfo return $null } Write-ColorOutput "Found previous installation from $($oldManifest.installation_date)" $ColorInfo Write-ColorOutput "Files: $($oldManifest.files_count), Directories: $($oldManifest.directories_count)" $ColorInfo # Create backup folder $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $backupDirName = "claude-backup-old-$timestamp" $backupPath = Join-Path $InstallationPath $backupDirName if (-not (Test-Path $backupPath)) { New-Item -ItemType Directory -Path $backupPath -Force | Out-Null Write-ColorOutput "Created backup folder: $backupPath" $ColorSuccess } $movedFiles = 0 $movedDirs = 0 $failedItems = @() # Move files first (from manifest) Write-ColorOutput "Moving old installation files to backup..." $ColorInfo foreach ($fileEntry in $oldManifest.files) { $filePath = $fileEntry.path if (Test-Path $filePath) { try { # Calculate relative path from installation root (generic approach) # This works for any directory structure without hardcoding $normalizedFilePath = $filePath.ToLower() $normalizedInstallPath = $InstallationPath.ToLower() $relativeSubPath = "" $fileName = Split-Path $filePath -Leaf $relativeDir = "" # Check if file is under installation path if ($normalizedFilePath.StartsWith($normalizedInstallPath)) { # Get path relative to installation root $relativeSubPath = $filePath.Substring($InstallationPath.Length).TrimStart('\', '/') # Separate directory from filename if ($relativeSubPath -ne $fileName) { $relativeDir = Split-Path -Path $relativeSubPath -Parent } } # Create backup subdirectory structure $backupSubDir = $backupPath if (-not [string]::IsNullOrEmpty($relativeDir)) { $backupSubDir = Join-Path $backupPath $relativeDir if (-not (Test-Path $backupSubDir)) { New-Item -ItemType Directory -Path $backupSubDir -Force | Out-Null } } $backupFilePath = Join-Path $backupSubDir $fileName # Move (cut) instead of copy Move-Item -Path $filePath -Destination $backupFilePath -Force -ErrorAction Stop $movedFiles++ } catch { Write-ColorOutput " WARNING: Failed to move file: $filePath - $($_.Exception.Message)" $ColorWarning $failedItems += $filePath } } } # Remove empty directories (in reverse order to handle nested dirs) Write-ColorOutput "Cleaning up empty directories..." $ColorInfo $sortedDirs = $oldManifest.directories | Sort-Object { $_.path.Length } -Descending foreach ($dirEntry in $sortedDirs) { $dirPath = $dirEntry.path if (Test-Path $dirPath) { try { # Check if directory is empty $dirContents = Get-ChildItem -Path $dirPath -Force -ErrorAction SilentlyContinue if (-not $dirContents -or ($dirContents | Measure-Object).Count -eq 0) { Remove-Item -Path $dirPath -Force -ErrorAction Stop Write-ColorOutput " Removed empty directory: $dirPath" $ColorInfo $movedDirs++ } else { Write-ColorOutput " Directory not empty (preserved): $dirPath" $ColorInfo } } catch { # Silently continue if directory removal fails } } } # Note: Old manifest will be automatically removed by Save-InstallManifest # via Remove-OldManifestsForPath to ensure robust cleanup Write-Host "" Write-ColorOutput "Old installation cleanup summary:" $ColorInfo Write-Host " Files moved: $movedFiles" Write-Host " Directories removed: $movedDirs" Write-Host " Backup location: $backupPath" if ($failedItems.Count -gt 0) { Write-ColorOutput " Failed items: $($failedItems.Count)" $ColorWarning } Write-Host "" # Return backup path for reference return $backupPath } function Get-AllInstallManifests { <# .SYNOPSIS Get all installation manifests (only latest per installation path) #> # Migrate legacy manifest if exists Migrate-LegacyManifest if (-not (Test-Path $script:ManifestDir)) { return @() } try { # Get both new (manifest-*) and old (install-*) format manifest files for backward compatibility $manifestFiles = @(Get-ChildItem -Path $script:ManifestDir -Filter "*-*.json" -File | Where-Object { $_.Name -match '^(manifest|install)-' } | Sort-Object LastWriteTime -Descending) # Simple array to hold results $results = @() foreach ($file in $manifestFiles) { try { $manifestJson = Get-Content -Path $file.FullName -Raw -Encoding utf8 $manifest = $manifestJson | ConvertFrom-Json # Safely get array counts $filesCount = 0 $dirsCount = 0 if ($manifest.files) { if ($manifest.files -is [System.Array]) { $filesCount = $manifest.files.Count } else { $filesCount = 1 } } if ($manifest.directories) { if ($manifest.directories -is [System.Array]) { $dirsCount = $manifest.directories.Count } else { $dirsCount = 1 } } # Create PSCustomObject instead of hashtable for better compatibility $manifestObj = [PSCustomObject]@{ manifest_id = if ($manifest.manifest_id) { $manifest.manifest_id } else { $file.BaseName } manifest_file = $file.FullName version = if ($manifest.version) { $manifest.version } else { "1.0" } installation_mode = if ($manifest.installation_mode) { $manifest.installation_mode } else { "Unknown" } installation_path = if ($manifest.installation_path) { $manifest.installation_path } else { "" } installation_date = if ($manifest.installation_date) { $manifest.installation_date } else { $file.LastWriteTime.ToString("yyyy-MM-ddTHH:mm:ssZ") } installer_version = if ($manifest.installer_version) { $manifest.installer_version } else { "unknown" } files = if ($manifest.files) { @($manifest.files) } else { @() } directories = if ($manifest.directories) { @($manifest.directories) } else { @() } files_count = $filesCount directories_count = $dirsCount application_version = 'unknown' } # Read application version from version.json file try { $installPath = $manifestObj.installation_path if ($installPath) { $versionJsonPath = Join-Path (Join-Path $installPath ".claude") "version.json" if (Test-Path $versionJsonPath) { $versionJsonContent = Get-Content -Path $versionJsonPath -Raw -Encoding utf8 $versionInfo = $versionJsonContent | ConvertFrom-Json if ($versionInfo.version) { $manifestObj.application_version = $versionInfo.version } } } } catch { # Silently fail - 'unknown' will be used } # Add to results array $results += $manifestObj } catch { Write-ColorOutput "WARNING: Failed to load manifest $($file.Name): $($_.Exception.Message)" $ColorWarning } } # Group by installation_path and keep only the latest per path $pathGroups = @{} foreach ($m in $results) { $normalizedPath = if ($m.installation_path) { $m.installation_path.TrimEnd('\', '/').ToLower() } else { "" } if (-not $pathGroups.ContainsKey($normalizedPath)) { $pathGroups[$normalizedPath] = @() } $pathGroups[$normalizedPath] += $m } # Select the latest manifest for each path $finalResults = @() foreach ($pathKey in $pathGroups.Keys) { $groupManifests = $pathGroups[$pathKey] # Sort by installation_date descending and take the first $latest = $groupManifests | Sort-Object { try { [DateTime]::Parse($_.installation_date) } catch { [DateTime]::MinValue } } -Descending | Select-Object -First 1 if ($latest) { $finalResults += $latest } } # Sort final results by installation_date descending if ($finalResults.Count -eq 0) { return @() } # Force array output from Sort-Object $sortedResults = @($finalResults | Sort-Object { try { [DateTime]::Parse($_.installation_date) } catch { [DateTime]::MinValue } } -Descending) # Use comma operator to prevent PowerShell from unwrapping single-element array return ,$sortedResults } catch { Write-ColorOutput "ERROR: Failed to list installation manifests: $($_.Exception.Message)" $ColorError return @() } } # ============================================================================ # UNINSTALLATION FUNCTIONS # ============================================================================ function Uninstall-ClaudeWorkflow { <# .SYNOPSIS Uninstall Claude Code Workflow based on installation manifest #> Write-ColorOutput "Claude Code Workflow System Uninstaller" $ColorInfo Write-ColorOutput "========================================" $ColorInfo Write-Host "" # Load all manifests $manifests = Get-AllInstallManifests if (-not $manifests -or $manifests.Count -eq 0) { Write-ColorOutput "ERROR: No installation manifests found in: $script:ManifestDir" $ColorError Write-ColorOutput "Cannot proceed with uninstallation without manifest." $ColorError Write-Host "" Write-ColorOutput "Manual uninstallation instructions:" $ColorInfo Write-Host "For Global installation, remove these directories:" Write-Host " - ~/.claude/agents" Write-Host " - ~/.claude/commands" Write-Host " - ~/.claude/output-styles" Write-Host " - ~/.claude/workflows" Write-Host " - ~/.claude/scripts" Write-Host " - ~/.claude/prompt-templates" Write-Host " - ~/.claude/python_script" Write-Host " - ~/.claude/skills" Write-Host " - ~/.claude/version.json" Write-Host " - ~/.claude/CLAUDE.md" Write-Host " - ~/.codex" Write-Host " - ~/.gemini" Write-Host " - ~/.qwen" return $false } # Display available installations Write-ColorOutput "Found $($manifests.Count) installation(s):" $ColorInfo Write-Host "" # If only one manifest, use it directly $selectedManifest = $null if ($manifests.Count -eq 1) { $selectedManifest = $manifests[0] # Display simplified info for single installation $versionStr = if ($selectedManifest.application_version -and $selectedManifest.application_version -ne 'unknown') { "v$($selectedManifest.application_version)" } else { "Version Unknown" } Write-ColorOutput "Found installation: $versionStr - $($selectedManifest.installation_path)" $ColorInfo } else { # Multiple manifests - let user choose (simplified: only version and path) $options = @() for ($i = 0; $i -lt $manifests.Count; $i++) { $m = $manifests[$i] # Get version string $versionStr = if ($m.application_version -and $m.application_version -ne 'unknown') { "v$($m.application_version)" } else { "Version Unknown" } # Get path string $pathStr = if ($m.installation_path) { $m.installation_path } else { "Path Unknown" } # Simplified format: only version and path $option = "$($i + 1). $versionStr - $pathStr" $options += $option } $options += "Cancel - Don't uninstall anything" Write-Host "" $selection = Get-UserChoiceWithArrows -Prompt "Select installation to uninstall:" -Options $options -DefaultIndex 0 if ($selection -like "Cancel*") { Write-ColorOutput "Uninstallation cancelled." $ColorWarning return $false } # Parse selection to get index $selectedIndex = [int]($selection.Split('.')[0]) - 1 $selectedManifest = $manifests[$selectedIndex] } # Display selected installation info (simplified - only path and version) Write-Host "" Write-ColorOutput "Uninstallation Target:" $ColorInfo # Display application version (handle unknown version) $displayVersion = if ($selectedManifest.application_version -and $selectedManifest.application_version -ne 'unknown') { "v$($selectedManifest.application_version)" } else { "Version: Unknown" } Write-Host " $displayVersion" Write-Host " Path: $($selectedManifest.installation_path)" Write-Host "" # Confirm uninstallation if (-not (Confirm-Action "Do you want to uninstall this installation?" -DefaultYes:$false)) { Write-ColorOutput "Uninstallation cancelled." $ColorWarning return $false } # Use the selected manifest for uninstallation $manifest = $selectedManifest $removedFiles = 0 $failedItems = @() $skippedFiles = 0 # Check if this is a Path mode uninstallation and if Global installation exists $isPathMode = ($manifest.installation_mode -eq "Path") $hasGlobalInstallation = $false if ($isPathMode) { # Check if any Global installation manifest exists if (Test-Path $script:ManifestDir) { $globalManifestFiles = Get-ChildItem -Path $script:ManifestDir -Filter "manifest-global-*.json" -File if ($globalManifestFiles -and $globalManifestFiles.Count -gt 0) { $hasGlobalInstallation = $true Write-ColorOutput "Found Global installation, global files will be preserved" $ColorWarning Write-Host "" } } } # Only remove files listed in manifest - do NOT remove directories Write-ColorOutput "Removing installed files..." $ColorInfo foreach ($fileEntry in $manifest.files) { $filePath = $fileEntry.path # For Path mode uninstallation, skip global files if Global installation exists if ($isPathMode -and $hasGlobalInstallation) { $userProfile = [Environment]::GetFolderPath("UserProfile") $globalClaudeDir = Join-Path $userProfile ".claude" $normalizedFilePath = $filePath.ToLower() $normalizedGlobalDir = $globalClaudeDir.ToLower() # Skip files under global .claude directory if ($normalizedFilePath.StartsWith($normalizedGlobalDir)) { Write-ColorOutput " Skipping global file (Global installation exists): $filePath" $ColorInfo $skippedFiles++ continue } } if (Test-Path $filePath) { try { Remove-Item -Path $filePath -Force -ErrorAction Stop $removedFiles++ } catch { Write-ColorOutput " WARNING: Failed to remove: $filePath" $ColorWarning $failedItems += $filePath } } } # Display removal summary if ($skippedFiles -gt 0) { Write-ColorOutput "Removed $removedFiles files, skipped $skippedFiles global files" $ColorSuccess } else { Write-ColorOutput "Removed $removedFiles files" $ColorSuccess } # Remove manifest file if (Test-Path $manifest.manifest_file) { try { Remove-Item -Path $manifest.manifest_file -Force Write-ColorOutput "Removed installation manifest: $($manifest.manifest_id)" $ColorSuccess } catch { Write-ColorOutput "WARNING: Failed to remove manifest file" $ColorWarning } } # Show summary Write-Host "" if ($failedItems.Count -gt 0) { Write-ColorOutput "Failed to remove $($failedItems.Count) files" $ColorWarning } if ($skippedFiles -gt 0) { Write-ColorOutput "Note: $skippedFiles global files were preserved due to existing Global installation" $ColorInfo } if ($failedItems.Count -eq 0) { $summaryMsg = if ($skippedFiles -gt 0) { "Uninstallation complete! Removed $removedFiles files, preserved $skippedFiles global files." } else { "Uninstallation complete! Removed $removedFiles files." } Write-ColorOutput $summaryMsg $ColorSuccess } else { Write-ColorOutput "Uninstallation completed with warnings." $ColorWarning } return $true } function Create-VersionJson { param( [string]$TargetClaudeDir, [string]$InstallationMode ) # Determine version from source parameter (passed from install-remote.ps1) $versionNumber = if ($SourceVersion) { $SourceVersion } else { $DefaultVersion } $sourceBranch = if ($SourceBranch) { $SourceBranch } else { "unknown" } $commitSha = if ($SourceCommit) { $SourceCommit } else { "unknown" } # Create version.json content $versionInfo = @{ version = $versionNumber commit_sha = $commitSha installation_mode = $InstallationMode installation_path = $TargetClaudeDir installation_date_utc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") source_branch = $sourceBranch installer_version = $ScriptVersion } $versionJsonPath = Join-Path $TargetClaudeDir "version.json" try { $versionInfo | ConvertTo-Json | Out-File -FilePath $versionJsonPath -Encoding utf8 -Force Write-ColorOutput "Created version.json: $versionNumber ($commitSha) - $InstallationMode" $ColorSuccess return $true } catch { Write-ColorOutput "WARNING: Failed to create version.json: $($_.Exception.Message)" $ColorWarning return $false } } function Install-Global { Write-ColorOutput "Installing Claude Code Workflow System globally..." $ColorInfo # Determine user profile directory $userProfile = [Environment]::GetFolderPath("UserProfile") $globalClaudeDir = Join-Path $userProfile ".claude" $globalClaudeMd = Join-Path $globalClaudeDir "CLAUDE.md" $globalCodexDir = Join-Path $userProfile ".codex" $globalGeminiDir = Join-Path $userProfile ".gemini" $globalQwenDir = Join-Path $userProfile ".qwen" Write-ColorOutput "Global installation path: $userProfile" $ColorInfo # Clean up old installation before proceeding Write-Host "" Write-ColorOutput "Checking for previous installation..." $ColorInfo $oldBackupPath = Move-OldInstallation -InstallationPath $userProfile -InstallationMode "Global" if ($oldBackupPath) { Write-ColorOutput "Previous installation moved to: $oldBackupPath" $ColorSuccess } # Initialize manifest $manifest = New-InstallManifest -InstallationMode "Global" -InstallationPath $userProfile # Source paths $sourceDir = $PSScriptRoot $sourceClaudeDir = Join-Path $sourceDir ".claude" $sourceClaudeMd = Join-Path $sourceDir "CLAUDE.md" $sourceCodexDir = Join-Path $sourceDir ".codex" $sourceGeminiDir = Join-Path $sourceDir ".gemini" $sourceQwenDir = Join-Path $sourceDir ".qwen" # Create backup folder if needed (default behavior unless NoBackup is specified) $backupFolder = $null if (-not $NoBackup) { if ((Test-Path $globalClaudeDir) -or (Test-Path $globalCodexDir) -or (Test-Path $globalGeminiDir) -or (Test-Path $globalQwenDir)) { $existingFiles = @() if (Test-Path $globalClaudeDir) { $existingFiles += Get-ChildItem $globalClaudeDir -Recurse -File -ErrorAction SilentlyContinue } if (Test-Path $globalCodexDir) { $existingFiles += Get-ChildItem $globalCodexDir -Recurse -File -ErrorAction SilentlyContinue } if (Test-Path $globalGeminiDir) { $existingFiles += Get-ChildItem $globalGeminiDir -Recurse -File -ErrorAction SilentlyContinue } if (Test-Path $globalQwenDir) { $existingFiles += Get-ChildItem $globalQwenDir -Recurse -File -ErrorAction SilentlyContinue } if (($existingFiles -and ($existingFiles | Measure-Object).Count -gt 0)) { $backupFolder = Get-BackupDirectory -TargetDirectory $userProfile Write-ColorOutput "Backup folder created: $backupFolder" $ColorInfo } } elseif (Test-Path $globalClaudeMd) { # Create backup folder even if .claude directory doesn't exist but CLAUDE.md does $backupFolder = Get-BackupDirectory -TargetDirectory $userProfile Write-ColorOutput "Backup folder created: $backupFolder" $ColorInfo } } # Merge .claude directory (incremental overlay - preserves user files) Write-ColorOutput "Installing .claude directory (incremental merge)..." $ColorInfo $claudeInstalled = Merge-DirectoryContents -Source $sourceClaudeDir -Destination $globalClaudeDir -Description ".claude directory" -BackupFolder $backupFolder # Track .claude directory in manifest (bulk add) if ($claudeInstalled) { Add-ManifestEntriesBulk -Manifest $manifest -SourceDirectory $sourceClaudeDir -TargetDirectory $globalClaudeDir -Description ".claude files" } # Handle CLAUDE.md file in .claude directory Write-ColorOutput "Installing CLAUDE.md to global .claude directory..." $ColorInfo $claudeMdInstalled = Copy-FileToDestination -Source $sourceClaudeMd -Destination $globalClaudeMd -Description "CLAUDE.md" -BackupFolder $backupFolder # Track CLAUDE.md in manifest if ($claudeMdInstalled) { Add-ManifestEntry -Manifest $manifest -Path $globalClaudeMd -Type "File" } # Backup critical config files in .codex directory before installation Backup-CriticalConfigFiles -TargetDirectory $globalCodexDir -BackupFolder $backupFolder -FileNames @("AGENTS.md") # Merge .codex directory (incremental overlay - preserves user files) Write-ColorOutput "Installing .codex directory (incremental merge)..." $ColorInfo $codexInstalled = Merge-DirectoryContents -Source $sourceCodexDir -Destination $globalCodexDir -Description ".codex directory" -BackupFolder $backupFolder # Track .codex directory in manifest (bulk add) if ($codexInstalled) { Add-ManifestEntriesBulk -Manifest $manifest -SourceDirectory $sourceCodexDir -TargetDirectory $globalCodexDir -Description ".codex files" } # Backup critical config files in .gemini directory before installation Backup-CriticalConfigFiles -TargetDirectory $globalGeminiDir -BackupFolder $backupFolder -FileNames @("GEMINI.md", "CLAUDE.md") # Merge .gemini directory (incremental overlay - preserves user files) Write-ColorOutput "Installing .gemini directory (incremental merge)..." $ColorInfo $geminiInstalled = Merge-DirectoryContents -Source $sourceGeminiDir -Destination $globalGeminiDir -Description ".gemini directory" -BackupFolder $backupFolder # Track .gemini directory in manifest (bulk add) if ($geminiInstalled) { Add-ManifestEntriesBulk -Manifest $manifest -SourceDirectory $sourceGeminiDir -TargetDirectory $globalGeminiDir -Description ".gemini files" } # Backup critical config files in .qwen directory before installation Backup-CriticalConfigFiles -TargetDirectory $globalQwenDir -BackupFolder $backupFolder -FileNames @("QWEN.md") # Merge .qwen directory (incremental overlay - preserves user files) Write-ColorOutput "Installing .qwen directory (incremental merge)..." $ColorInfo $qwenInstalled = Merge-DirectoryContents -Source $sourceQwenDir -Destination $globalQwenDir -Description ".qwen directory" -BackupFolder $backupFolder # Track .qwen directory in manifest (bulk add) if ($qwenInstalled) { Add-ManifestEntriesBulk -Manifest $manifest -SourceDirectory $sourceQwenDir -TargetDirectory $globalQwenDir -Description ".qwen files" } # Create version.json in global .claude directory Write-ColorOutput "Creating version.json..." $ColorInfo Create-VersionJson -TargetClaudeDir $globalClaudeDir -InstallationMode "Global" if ($backupFolder -and (Test-Path $backupFolder)) { $backupFiles = Get-ChildItem $backupFolder -Recurse -File -ErrorAction SilentlyContinue if (-not $backupFiles -or ($backupFiles | Measure-Object).Count -eq 0) { # Remove empty backup folder Remove-Item -Path $backupFolder -Force Write-ColorOutput "Removed empty backup folder" $ColorInfo } } # Save installation manifest Save-InstallManifest -Manifest $manifest return $true } function Install-Path { param( [string]$TargetDirectory ) Write-ColorOutput "Installing Claude Code Workflow System in hybrid mode..." $ColorInfo Write-ColorOutput "Local path: $TargetDirectory" $ColorInfo # Determine user profile directory for global files $userProfile = [Environment]::GetFolderPath("UserProfile") $globalClaudeDir = Join-Path $userProfile ".claude" Write-ColorOutput "Global path: $userProfile" $ColorInfo # Clean up old installation before proceeding Write-Host "" Write-ColorOutput "Checking for previous installation..." $ColorInfo $oldBackupPath = Move-OldInstallation -InstallationPath $TargetDirectory -InstallationMode "Path" if ($oldBackupPath) { Write-ColorOutput "Previous installation moved to: $oldBackupPath" $ColorSuccess } # Initialize manifest $manifest = New-InstallManifest -InstallationMode "Path" -InstallationPath $TargetDirectory # Source paths $sourceDir = $PSScriptRoot $sourceClaudeDir = Join-Path $sourceDir ".claude" $sourceClaudeMd = Join-Path $sourceDir "CLAUDE.md" $sourceCodexDir = Join-Path $sourceDir ".codex" $sourceGeminiDir = Join-Path $sourceDir ".gemini" $sourceQwenDir = Join-Path $sourceDir ".qwen" # Local paths - for agents, commands, output-styles, .codex, .gemini, .qwen $localClaudeDir = Join-Path $TargetDirectory ".claude" $localCodexDir = Join-Path $TargetDirectory ".codex" $localGeminiDir = Join-Path $TargetDirectory ".gemini" $localQwenDir = Join-Path $TargetDirectory ".qwen" # Create backup folder if needed $backupFolder = $null if (-not $NoBackup) { if ((Test-Path $localClaudeDir) -or (Test-Path $localCodexDir) -or (Test-Path $localGeminiDir) -or (Test-Path $localQwenDir) -or (Test-Path $globalClaudeDir)) { $backupFolder = Get-BackupDirectory -TargetDirectory $TargetDirectory Write-ColorOutput "Backup folder created: $backupFolder" $ColorInfo } } # Create local .claude directory if (-not (Test-Path $localClaudeDir)) { New-Item -ItemType Directory -Path $localClaudeDir -Force | Out-Null Write-ColorOutput "Created local .claude directory" $ColorSuccess } # Local folders to install (agents, commands, output-styles) $localFolders = @("agents", "commands", "output-styles") Write-ColorOutput "Installing local components (agents, commands, output-styles)..." $ColorInfo foreach ($folder in $localFolders) { $sourceFolderPath = Join-Path $sourceClaudeDir $folder $destFolderPath = Join-Path $localClaudeDir $folder if (Test-Path $sourceFolderPath) { # Use incremental merge for local folders (preserves user customizations) $folderInstalled = Merge-DirectoryContents -Source $sourceFolderPath -Destination $destFolderPath -Description "$folder folder" -BackupFolder $backupFolder # Track local folder in manifest (bulk add) if ($folderInstalled) { Add-ManifestEntriesBulk -Manifest $manifest -SourceDirectory $sourceFolderPath -TargetDirectory $destFolderPath -Description "$folder files" } } else { Write-ColorOutput "WARNING: Source folder not found: $folder" $ColorWarning } } # Global components - exclude local folders (use same efficient method as Global mode) Write-ColorOutput "Installing global components to $globalClaudeDir..." $ColorInfo # Create temporary directory for global files only $tempGlobalDir = Join-Path ([System.IO.Path]::GetTempPath()) "claude-global-$((Get-Date).Ticks)" New-Item -ItemType Directory -Path $tempGlobalDir -Force | Out-Null try { # Copy global files to temp directory (excluding local folders) Write-ColorOutput "Preparing global components..." $ColorInfo $sourceItems = Get-ChildItem -Path $sourceClaudeDir -Recurse -File | Where-Object { $relativePath = $_.FullName.Substring($sourceClaudeDir.Length + 1) $topFolder = $relativePath.Split([System.IO.Path]::DirectorySeparatorChar)[0] $topFolder -notin $localFolders } foreach ($item in $sourceItems) { $relativePath = $item.FullName.Substring($sourceClaudeDir.Length + 1) $tempDestPath = Join-Path $tempGlobalDir $relativePath $tempDestDir = Split-Path $tempDestPath -Parent if (-not (Test-Path $tempDestDir)) { New-Item -ItemType Directory -Path $tempDestDir -Force | Out-Null } Copy-Item -Path $item.FullName -Destination $tempDestPath -Force } # Use bulk merge method (same as Global mode - fast!) $globalInstalled = Merge-DirectoryContents -Source $tempGlobalDir -Destination $globalClaudeDir -Description "global components" -BackupFolder $backupFolder # Track global files in manifest using bulk method (fast!) if ($globalInstalled) { Add-ManifestEntriesBulk -Manifest $manifest -SourceDirectory $tempGlobalDir -TargetDirectory $globalClaudeDir -Description "global files" } } finally { # Clean up temp directory if (Test-Path $tempGlobalDir) { Remove-Item -Path $tempGlobalDir -Recurse -Force -ErrorAction SilentlyContinue } } # Handle CLAUDE.md file in global .claude directory $globalClaudeMd = Join-Path $globalClaudeDir "CLAUDE.md" Write-ColorOutput "Installing CLAUDE.md to global .claude directory..." $ColorInfo $claudeMdInstalled = Copy-FileToDestination -Source $sourceClaudeMd -Destination $globalClaudeMd -Description "CLAUDE.md" -BackupFolder $backupFolder # Track CLAUDE.md in manifest if ($claudeMdInstalled) { Add-ManifestEntry -Manifest $manifest -Path $globalClaudeMd -Type "File" } # Backup critical config files in .codex directory before installation Backup-CriticalConfigFiles -TargetDirectory $localCodexDir -BackupFolder $backupFolder -FileNames @("AGENTS.md") # Merge .codex directory to local location (incremental overlay - preserves user files) Write-ColorOutput "Installing .codex directory to local location (incremental merge)..." $ColorInfo $codexInstalled = Merge-DirectoryContents -Source $sourceCodexDir -Destination $localCodexDir -Description ".codex directory" -BackupFolder $backupFolder # Track .codex directory in manifest (bulk add) if ($codexInstalled) { Add-ManifestEntriesBulk -Manifest $manifest -SourceDirectory $sourceCodexDir -TargetDirectory $localCodexDir -Description ".codex files" } # Backup critical config files in .gemini directory before installation Backup-CriticalConfigFiles -TargetDirectory $localGeminiDir -BackupFolder $backupFolder -FileNames @("GEMINI.md", "CLAUDE.md") # Merge .gemini directory to local location (incremental overlay - preserves user files) Write-ColorOutput "Installing .gemini directory to local location (incremental merge)..." $ColorInfo $geminiInstalled = Merge-DirectoryContents -Source $sourceGeminiDir -Destination $localGeminiDir -Description ".gemini directory" -BackupFolder $backupFolder # Track .gemini directory in manifest (bulk add) if ($geminiInstalled) { Add-ManifestEntriesBulk -Manifest $manifest -SourceDirectory $sourceGeminiDir -TargetDirectory $localGeminiDir -Description ".gemini files" } # Backup critical config files in .qwen directory before installation Backup-CriticalConfigFiles -TargetDirectory $localQwenDir -BackupFolder $backupFolder -FileNames @("QWEN.md") # Merge .qwen directory to local location (incremental overlay - preserves user files) Write-ColorOutput "Installing .qwen directory to local location (incremental merge)..." $ColorInfo $qwenInstalled = Merge-DirectoryContents -Source $sourceQwenDir -Destination $localQwenDir -Description ".qwen directory" -BackupFolder $backupFolder # Track .qwen directory in manifest (bulk add) if ($qwenInstalled) { Add-ManifestEntriesBulk -Manifest $manifest -SourceDirectory $sourceQwenDir -TargetDirectory $localQwenDir -Description ".qwen files" } # Create version.json in local .claude directory Write-ColorOutput "Creating version.json in local directory..." $ColorInfo Create-VersionJson -TargetClaudeDir $localClaudeDir -InstallationMode "Path" # Also create version.json in global .claude directory Write-ColorOutput "Creating version.json in global directory..." $ColorInfo Create-VersionJson -TargetClaudeDir $globalClaudeDir -InstallationMode "Global" if ($backupFolder -and (Test-Path $backupFolder)) { $backupFiles = Get-ChildItem $backupFolder -Recurse -File -ErrorAction SilentlyContinue if (-not $backupFiles -or ($backupFiles | Measure-Object).Count -eq 0) { Remove-Item -Path $backupFolder -Force Write-ColorOutput "Removed empty backup folder" $ColorInfo } } # Save installation manifest Save-InstallManifest -Manifest $manifest return $true } function Get-InstallationMode { if ($InstallMode) { Write-ColorOutput "Installation mode: $InstallMode" $ColorInfo return $InstallMode } $modes = @( "Global - Install to user profile (~/.claude/)", "Path - Install to custom directory (partial local + global)" ) Write-Host "" $selection = Get-UserChoiceWithArrows -Prompt "Choose installation mode:" -Options $modes -DefaultIndex 0 if ($selection -like "Global*") { return "Global" } elseif ($selection -like "Path*") { return "Path" } return "Global" } function Get-InstallationPath { param( [string]$Mode ) if ($Mode -eq "Global") { return [Environment]::GetFolderPath("UserProfile") } if ($TargetPath) { if (Test-Path $TargetPath) { return $TargetPath } Write-ColorOutput "WARNING: Specified target path does not exist: $TargetPath" $ColorWarning } # Interactive path selection do { Write-Host "" Write-ColorOutput "Enter the target directory path for installation:" $ColorPrompt Write-ColorOutput "(This will install agents, commands, output-styles locally, other files globally)" $ColorInfo $path = Read-Host "Path" if ([string]::IsNullOrWhiteSpace($path)) { Write-ColorOutput "Path cannot be empty" $ColorWarning continue } # Expand environment variables and relative paths $expandedPath = [System.Environment]::ExpandEnvironmentVariables($path) $expandedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($expandedPath) if (Test-Path $expandedPath) { return $expandedPath } Write-ColorOutput "Path does not exist: $expandedPath" $ColorWarning if (Confirm-Action "Create this directory?" -DefaultYes) { try { New-Item -ItemType Directory -Path $expandedPath -Force | Out-Null Write-ColorOutput "Directory created successfully" $ColorSuccess return $expandedPath } catch { Write-ColorOutput "Failed to create directory: $($_.Exception.Message)" $ColorError } } } while ($true) } function Show-Summary { param( [string]$Mode, [string]$Path, [bool]$Success ) Write-Host "" if ($Success) { Write-ColorOutput "Installation completed successfully!" $ColorSuccess } else { Write-ColorOutput "Installation completed with warnings" $ColorWarning } Write-ColorOutput "Installation Details:" $ColorInfo Write-Host " Mode: $Mode" if ($Mode -eq "Path") { Write-Host " Local Path: $Path" Write-Host " Global Path: $([Environment]::GetFolderPath('UserProfile'))" Write-Host " Local Components: agents, commands, output-styles, .codex, .gemini, .qwen" Write-Host " Global Components: workflows, scripts, python_script, etc." } else { Write-Host " Path: $Path" Write-Host " Global Components: .claude, .codex, .gemini, .qwen" } if ($NoBackup) { Write-Host " Backup: Disabled (no backup created)" } elseif ($BackupAll) { Write-Host " Backup: Enabled (automatic backup of all existing files)" } else { Write-Host " Backup: Enabled (default behavior)" } Write-Host "" Write-ColorOutput "Next steps:" $ColorInfo Write-Host "1. Review CLAUDE.md - Customize guidelines for your project" Write-Host "2. Review .codex/Agent.md - Codex agent execution protocol" Write-Host "3. Review .gemini/CLAUDE.md - Gemini agent execution protocol" Write-Host "4. Review .qwen/QWEN.md - Qwen agent execution protocol" Write-Host "5. Configure settings - Edit .claude/settings.local.json as needed" Write-Host "6. Start using Claude Code with Agent workflow coordination!" Write-Host "7. Use /workflow commands for task execution" Write-Host "8. Use /update-memory commands for memory system management" Write-Host "" Write-ColorOutput "Documentation: https://github.com/catlog22/Claude-CCW" $ColorInfo Write-ColorOutput "Features: Unified workflow system with comprehensive file output generation" $ColorInfo } function Main { # Use SourceVersion parameter if provided, otherwise use default $installVersion = if ($SourceVersion) { $SourceVersion } else { $DefaultVersion } # Show banner first Show-Banner # Check for uninstall mode from parameter or ask user interactively $operationMode = "Install" if ($Uninstall) { $operationMode = "Uninstall" } elseif (-not $NonInteractive -and -not $InstallMode) { # Interactive mode selection Write-Host "" $operations = @( "Install - Install Claude Code Workflow System", "Uninstall - Remove Claude Code Workflow System" ) $selection = Get-UserChoiceWithArrows -Prompt "Choose operation:" -Options $operations -DefaultIndex 0 if ($selection -like "Uninstall*") { $operationMode = "Uninstall" } } # Handle uninstall mode if ($operationMode -eq "Uninstall") { $result = Uninstall-ClaudeWorkflow if (-not $NonInteractive) { Write-Host "" Write-ColorOutput "Press any key to exit..." $ColorPrompt $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } return $(if ($result) { 0 } else { 1 }) } # Continue with installation Show-Header -InstallVersion $installVersion # Test prerequisites Write-ColorOutput "Checking system requirements..." $ColorInfo if (-not (Test-Prerequisites)) { Write-ColorOutput "Prerequisites check failed!" $ColorError return 1 } try { # Get installation mode $mode = Get-InstallationMode $installPath = "" $success = $false if ($mode -eq "Global") { $installPath = [Environment]::GetFolderPath("UserProfile") $result = Install-Global $success = $result -eq $true } elseif ($mode -eq "Path") { $installPath = Get-InstallationPath -Mode $mode $result = Install-Path -TargetDirectory $installPath $success = $result -eq $true } Show-Summary -Mode $mode -Path $installPath -Success ([bool]$success) # Wait for user confirmation before exit in interactive mode if (-not $NonInteractive) { Write-Host "" Write-ColorOutput "Installation completed. Press any key to exit..." $ColorPrompt $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } if ($success) { return 0 } else { return 1 } } catch { Write-ColorOutput "CRITICAL ERROR: $($_.Exception.Message)" $ColorError Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" $ColorError # Wait for user confirmation before exit in interactive mode if (-not $NonInteractive) { Write-Host "" Write-ColorOutput "An error occurred. Press any key to exit..." $ColorPrompt $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } return 1 } } # Run main function exit (Main)