diff --git a/Install-Claude.ps1 b/Install-Claude.ps1 index 4cbaddf5..36723662 100644 --- a/Install-Claude.ps1 +++ b/Install-Claude.ps1 @@ -26,6 +26,9 @@ .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 @@ -45,6 +48,14 @@ .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 #> param( @@ -61,6 +72,8 @@ param( [switch]$NoBackup, + [switch]$Uninstall, + [string]$SourceVersion = "", [string]$SourceBranch = "", @@ -98,6 +111,9 @@ $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, @@ -704,6 +720,427 @@ function Merge-DirectoryContents { 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 + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $manifestId = "install-$InstallationMode-$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 Save-InstallManifest { + <# + .SYNOPSIS + Save the installation manifest to disk + #> + param( + [Parameter(Mandatory=$true)] + [hashtable]$Manifest + ) + + try { + # 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 + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $mode = if ($legacy.installation_mode) { $legacy.installation_mode } else { "Global" } + $manifestId = "install-$mode-$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 Get-AllInstallManifests { + <# + .SYNOPSIS + Get all installation manifests + #> + + # Migrate legacy manifest if exists + Migrate-LegacyManifest + + if (-not (Test-Path $script:ManifestDir)) { + return @() + } + + try { + $manifestFiles = Get-ChildItem -Path $script:ManifestDir -Filter "install-*.json" -File | Sort-Object LastWriteTime -Descending + $manifests = [System.Collections.ArrayList]::new() + + foreach ($file in $manifestFiles) { + try { + $manifestJson = Get-Content -Path $file.FullName -Raw -Encoding utf8 + $manifest = $manifestJson | ConvertFrom-Json + + # Convert to hashtable for easier manipulation + # Handle both old and new manifest formats + + # 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 + } + } + + $manifestHash = @{ + 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 + } + + $null = $manifests.Add($manifestHash) + } catch { + Write-ColorOutput "WARNING: Failed to load manifest $($file.Name): $($_.Exception.Message)" $ColorWarning + } + } + + return ,$manifests.ToArray() + } 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] + Write-ColorOutput "Only one installation found, will uninstall:" $ColorInfo + } else { + # Multiple manifests - let user choose + $options = @() + for ($i = 0; $i -lt $manifests.Count; $i++) { + $m = $manifests[$i] + + # Safely extract date string + $dateStr = "unknown date" + if ($m.installation_date) { + try { + if ($m.installation_date.Length -ge 10) { + $dateStr = $m.installation_date.Substring(0, 10) + } else { + $dateStr = $m.installation_date + } + } catch { + $dateStr = "unknown date" + } + } + + # Build option string with safe counts + $filesCount = if ($m.files_count) { $m.files_count } else { 0 } + $dirsCount = if ($m.directories_count) { $m.directories_count } else { 0 } + $pathInfo = if ($m.installation_path) { " ($($m.installation_path))" } else { "" } + $option = "$($i + 1). [$($m.installation_mode)] $dateStr - $filesCount files, $dirsCount dirs$pathInfo" + $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 + Write-Host "" + Write-ColorOutput "Installation Information:" $ColorInfo + Write-Host " Manifest ID: $($selectedManifest.manifest_id)" + Write-Host " Mode: $($selectedManifest.installation_mode)" + Write-Host " Path: $($selectedManifest.installation_path)" + Write-Host " Date: $($selectedManifest.installation_date)" + Write-Host " Installer Version: $($selectedManifest.installer_version)" + + # Use pre-calculated counts + $filesCount = if ($selectedManifest.files_count) { $selectedManifest.files_count } else { 0 } + $dirsCount = if ($selectedManifest.directories_count) { $selectedManifest.directories_count } else { 0 } + Write-Host " Files tracked: $filesCount" + Write-Host " Directories tracked: $dirsCount" + 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 + $removedDirs = 0 + $failedItems = @() + + # Remove files first + Write-ColorOutput "Removing installed files..." $ColorInfo + foreach ($fileEntry in $manifest.files) { + $filePath = $fileEntry.path + + if (Test-Path $filePath) { + try { + Remove-Item -Path $filePath -Force -ErrorAction Stop + Write-ColorOutput " Removed file: $filePath" $ColorSuccess + $removedFiles++ + } catch { + Write-ColorOutput " WARNING: Failed to remove file: $filePath" $ColorWarning + $failedItems += $filePath + } + } else { + Write-ColorOutput " File not found (already removed): $filePath" $ColorInfo + } + } + + # Remove directories (in reverse order to handle nested directories) + Write-ColorOutput "Removing installed directories..." $ColorInfo + $sortedDirs = $manifest.directories | Sort-Object { $_.path.Length } -Descending + + foreach ($dirEntry in $sortedDirs) { + $dirPath = $dirEntry.path + + if (Test-Path $dirPath) { + try { + # Check if directory is empty or only contains files we installed + $dirContents = Get-ChildItem -Path $dirPath -Recurse -Force -ErrorAction SilentlyContinue + + if (-not $dirContents -or ($dirContents | Measure-Object).Count -eq 0) { + Remove-Item -Path $dirPath -Recurse -Force -ErrorAction Stop + Write-ColorOutput " Removed directory: $dirPath" $ColorSuccess + $removedDirs++ + } else { + Write-ColorOutput " Directory not empty (preserved): $dirPath" $ColorWarning + } + } catch { + Write-ColorOutput " WARNING: Failed to remove directory: $dirPath" $ColorWarning + $failedItems += $dirPath + } + } else { + Write-ColorOutput " Directory not found (already removed): $dirPath" $ColorInfo + } + } + + # 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 "" + Write-ColorOutput "========================================" $ColorInfo + Write-ColorOutput "Uninstallation Summary:" $ColorInfo + Write-Host " Files removed: $removedFiles" + Write-Host " Directories removed: $removedDirs" + + if ($failedItems.Count -gt 0) { + Write-Host "" + Write-ColorOutput "Failed to remove the following items:" $ColorWarning + foreach ($item in $failedItems) { + Write-Host " - $item" + } + } + + Write-Host "" + if ($failedItems.Count -eq 0) { + Write-ColorOutput "Claude Code Workflow has been successfully uninstalled!" $ColorSuccess + } else { + Write-ColorOutput "Uninstallation completed with warnings." $ColorWarning + Write-ColorOutput "Please manually remove the failed items listed above." $ColorInfo + } + + return $true +} + function Create-VersionJson { param( [string]$TargetClaudeDir, @@ -751,6 +1188,9 @@ function Install-Global { Write-ColorOutput "Global installation path: $userProfile" $ColorInfo + # Initialize manifest + $manifest = New-InstallManifest -InstallationMode "Global" -InstallationPath $userProfile + # Source paths $sourceDir = $PSScriptRoot $sourceClaudeDir = Join-Path $sourceDir ".claude" @@ -791,22 +1231,73 @@ function Install-Global { Write-ColorOutput "Installing .claude directory..." $ColorInfo $claudeInstalled = Backup-AndReplaceDirectory -Source $sourceClaudeDir -Destination $globalClaudeDir -Description ".claude directory" -BackupFolder $backupFolder + # Track .claude directory in manifest + if ($claudeInstalled) { + Add-ManifestEntry -Manifest $manifest -Path $globalClaudeDir -Type "Directory" + + # Track files from SOURCE directory, not destination + Get-ChildItem -Path $sourceClaudeDir -Recurse -File | ForEach-Object { + # Calculate target path where this file will be installed + $relativePath = $_.FullName.Substring($sourceClaudeDir.Length) + $targetPath = $globalClaudeDir + $relativePath + Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File" + } + } + # 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" + } + # Replace .codex directory (backup → clear → copy entire folder) Write-ColorOutput "Installing .codex directory..." $ColorInfo $codexInstalled = Backup-AndReplaceDirectory -Source $sourceCodexDir -Destination $globalCodexDir -Description ".codex directory" -BackupFolder $backupFolder + # Track .codex directory in manifest + if ($codexInstalled) { + Add-ManifestEntry -Manifest $manifest -Path $globalCodexDir -Type "Directory" + # Track files from SOURCE directory + Get-ChildItem -Path $sourceCodexDir -Recurse -File | ForEach-Object { + $relativePath = $_.FullName.Substring($sourceCodexDir.Length) + $targetPath = $globalCodexDir + $relativePath + Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File" + } + } + # Replace .gemini directory (backup → clear → copy entire folder) Write-ColorOutput "Installing .gemini directory..." $ColorInfo $geminiInstalled = Backup-AndReplaceDirectory -Source $sourceGeminiDir -Destination $globalGeminiDir -Description ".gemini directory" -BackupFolder $backupFolder + # Track .gemini directory in manifest + if ($geminiInstalled) { + Add-ManifestEntry -Manifest $manifest -Path $globalGeminiDir -Type "Directory" + # Track files from SOURCE directory + Get-ChildItem -Path $sourceGeminiDir -Recurse -File | ForEach-Object { + $relativePath = $_.FullName.Substring($sourceGeminiDir.Length) + $targetPath = $globalGeminiDir + $relativePath + Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File" + } + } + # Replace .qwen directory (backup → clear → copy entire folder) Write-ColorOutput "Installing .qwen directory..." $ColorInfo $qwenInstalled = Backup-AndReplaceDirectory -Source $sourceQwenDir -Destination $globalQwenDir -Description ".qwen directory" -BackupFolder $backupFolder + # Track .qwen directory in manifest + if ($qwenInstalled) { + Add-ManifestEntry -Manifest $manifest -Path $globalQwenDir -Type "Directory" + # Track files from SOURCE directory + Get-ChildItem -Path $sourceQwenDir -Recurse -File | ForEach-Object { + $relativePath = $_.FullName.Substring($sourceQwenDir.Length) + $targetPath = $globalQwenDir + $relativePath + Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File" + } + } + # Create version.json in global .claude directory Write-ColorOutput "Creating version.json..." $ColorInfo Create-VersionJson -TargetClaudeDir $globalClaudeDir -InstallationMode "Global" @@ -820,6 +1311,9 @@ function Install-Global { } } + # Save installation manifest + Save-InstallManifest -Manifest $manifest + return $true } @@ -837,6 +1331,9 @@ function Install-Path { Write-ColorOutput "Global path: $userProfile" $ColorInfo + # Initialize manifest + $manifest = New-InstallManifest -InstallationMode "Path" -InstallationPath $TargetDirectory + # Source paths $sourceDir = $PSScriptRoot $sourceClaudeDir = Join-Path $sourceDir ".claude" @@ -877,8 +1374,19 @@ function Install-Path { if (Test-Path $sourceFolderPath) { # Use new backup and replace logic for local folders Write-ColorOutput "Installing local folder: $folder..." $ColorInfo - Backup-AndReplaceDirectory -Source $sourceFolderPath -Destination $destFolderPath -Description "$folder folder" -BackupFolder $backupFolder + $folderInstalled = Backup-AndReplaceDirectory -Source $sourceFolderPath -Destination $destFolderPath -Description "$folder folder" -BackupFolder $backupFolder Write-ColorOutput "Installed local folder: $folder" $ColorSuccess + + # Track local folder in manifest + if ($folderInstalled) { + Add-ManifestEntry -Manifest $manifest -Path $destFolderPath -Type "Directory" + # Track files from SOURCE directory + Get-ChildItem -Path $sourceFolderPath -Recurse -File | ForEach-Object { + $relativePath = $_.FullName.Substring($sourceFolderPath.Length) + $targetPath = $destFolderPath + $relativePath + Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File" + } + } } else { Write-ColorOutput "WARNING: Source folder not found: $folder" $ColorWarning } @@ -933,23 +1441,71 @@ function Install-Path { Write-ColorOutput "Merged $mergedCount files to global location" $ColorSuccess + # Track global files in manifest + $globalClaudeFiles = Get-ChildItem -Path $globalClaudeDir -Recurse -File | Where-Object { + $relativePath = $_.FullName.Substring($globalClaudeDir.Length + 1) + $topFolder = $relativePath.Split([System.IO.Path]::DirectorySeparatorChar)[0] + $topFolder -notin $localFolders + } + foreach ($file in $globalClaudeFiles) { + Add-ManifestEntry -Manifest $manifest -Path $file.FullName -Type "File" + } + # Handle CLAUDE.md file in global .claude directory $globalClaudeMd = Join-Path $globalClaudeDir "CLAUDE.md" Write-ColorOutput "Installing CLAUDE.md to global .claude directory..." $ColorInfo - Copy-FileToDestination -Source $sourceClaudeMd -Destination $globalClaudeMd -Description "CLAUDE.md" -BackupFolder $backupFolder + $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" + } # Replace .codex directory to local location (backup → clear → copy entire folder) Write-ColorOutput "Installing .codex directory to local location..." $ColorInfo $codexInstalled = Backup-AndReplaceDirectory -Source $sourceCodexDir -Destination $localCodexDir -Description ".codex directory" -BackupFolder $backupFolder + # Track .codex directory in manifest + if ($codexInstalled) { + Add-ManifestEntry -Manifest $manifest -Path $localCodexDir -Type "Directory" + # Track files from SOURCE directory + Get-ChildItem -Path $sourceCodexDir -Recurse -File | ForEach-Object { + $relativePath = $_.FullName.Substring($sourceCodexDir.Length) + $targetPath = $localCodexDir + $relativePath + Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File" + } + } + # Replace .gemini directory to local location (backup → clear → copy entire folder) Write-ColorOutput "Installing .gemini directory to local location..." $ColorInfo $geminiInstalled = Backup-AndReplaceDirectory -Source $sourceGeminiDir -Destination $localGeminiDir -Description ".gemini directory" -BackupFolder $backupFolder + # Track .gemini directory in manifest + if ($geminiInstalled) { + Add-ManifestEntry -Manifest $manifest -Path $localGeminiDir -Type "Directory" + # Track files from SOURCE directory + Get-ChildItem -Path $sourceGeminiDir -Recurse -File | ForEach-Object { + $relativePath = $_.FullName.Substring($sourceGeminiDir.Length) + $targetPath = $localGeminiDir + $relativePath + Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File" + } + } + # Replace .qwen directory to local location (backup → clear → copy entire folder) Write-ColorOutput "Installing .qwen directory to local location..." $ColorInfo $qwenInstalled = Backup-AndReplaceDirectory -Source $sourceQwenDir -Destination $localQwenDir -Description ".qwen directory" -BackupFolder $backupFolder + # Track .qwen directory in manifest + if ($qwenInstalled) { + Add-ManifestEntry -Manifest $manifest -Path $localQwenDir -Type "Directory" + # Track files from SOURCE directory + Get-ChildItem -Path $sourceQwenDir -Recurse -File | ForEach-Object { + $relativePath = $_.FullName.Substring($sourceQwenDir.Length) + $targetPath = $localQwenDir + $relativePath + Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File" + } + } + # Create version.json in local .claude directory Write-ColorOutput "Creating version.json in local directory..." $ColorInfo Create-VersionJson -TargetClaudeDir $localClaudeDir -InstallationMode "Path" @@ -966,6 +1522,9 @@ function Install-Path { } } + # Save installation manifest + Save-InstallManifest -Manifest $manifest + return $true } @@ -1098,6 +1657,42 @@ 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 diff --git a/Install-Claude.sh b/Install-Claude.sh index b1a8efdd..37558570 100644 --- a/Install-Claude.sh +++ b/Install-Claude.sh @@ -24,10 +24,14 @@ FORCE=false NON_INTERACTIVE=false BACKUP_ALL=true # Enabled by default NO_BACKUP=false +UNINSTALL=false # Uninstall mode SOURCE_VERSION="" # Version from remote installer SOURCE_BRANCH="" # Branch from remote installer SOURCE_COMMIT="" # Commit SHA from remote installer +# Global manifest directory location +MANIFEST_DIR="${HOME}/.claude-manifests" + # Functions function write_color() { local message="$1" @@ -474,6 +478,9 @@ function install_global() { write_color "Global installation path: $user_home" "$COLOR_INFO" + # Initialize manifest + local manifest_file=$(new_install_manifest "Global" "$user_home") + # Source paths local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local source_claude_dir="${script_dir}/.claude" @@ -507,23 +514,66 @@ function install_global() { # Replace .claude directory (backup → clear conflicting → copy) write_color "Installing .claude directory..." "$COLOR_INFO" - backup_and_replace_directory "$source_claude_dir" "$global_claude_dir" ".claude directory" "$backup_folder" + if backup_and_replace_directory "$source_claude_dir" "$global_claude_dir" ".claude directory" "$backup_folder"; then + # Track .claude directory in manifest + add_manifest_entry "$manifest_file" "$global_claude_dir" "Directory" + + # Track files from SOURCE directory, not destination + while IFS= read -r -d '' source_file; do + local relative_path="${source_file#$source_claude_dir}" + local target_path="${global_claude_dir}${relative_path}" + add_manifest_entry "$manifest_file" "$target_path" "File" + done < <(find "$source_claude_dir" -type f -print0) + fi # Handle CLAUDE.md file write_color "Installing CLAUDE.md to global .claude directory..." "$COLOR_INFO" - copy_file_to_destination "$source_claude_md" "$global_claude_md" "CLAUDE.md" "$backup_folder" + if copy_file_to_destination "$source_claude_md" "$global_claude_md" "CLAUDE.md" "$backup_folder"; then + # Track CLAUDE.md in manifest + add_manifest_entry "$manifest_file" "$global_claude_md" "File" + fi # Replace .codex directory (backup → clear conflicting → copy) write_color "Installing .codex directory..." "$COLOR_INFO" - backup_and_replace_directory "$source_codex_dir" "$global_codex_dir" ".codex directory" "$backup_folder" + if backup_and_replace_directory "$source_codex_dir" "$global_codex_dir" ".codex directory" "$backup_folder"; then + # Track .codex directory in manifest + add_manifest_entry "$manifest_file" "$global_codex_dir" "Directory" + + # Track files from SOURCE directory + while IFS= read -r -d '' source_file; do + local relative_path="${source_file#$source_codex_dir}" + local target_path="${global_codex_dir}${relative_path}" + add_manifest_entry "$manifest_file" "$target_path" "File" + done < <(find "$source_codex_dir" -type f -print0) + fi # Replace .gemini directory (backup → clear conflicting → copy) write_color "Installing .gemini directory..." "$COLOR_INFO" - backup_and_replace_directory "$source_gemini_dir" "$global_gemini_dir" ".gemini directory" "$backup_folder" + if backup_and_replace_directory "$source_gemini_dir" "$global_gemini_dir" ".gemini directory" "$backup_folder"; then + # Track .gemini directory in manifest + add_manifest_entry "$manifest_file" "$global_gemini_dir" "Directory" + + # Track files from SOURCE directory + while IFS= read -r -d '' source_file; do + local relative_path="${source_file#$source_gemini_dir}" + local target_path="${global_gemini_dir}${relative_path}" + add_manifest_entry "$manifest_file" "$target_path" "File" + done < <(find "$source_gemini_dir" -type f -print0) + fi # Replace .qwen directory (backup → clear conflicting → copy) write_color "Installing .qwen directory..." "$COLOR_INFO" - backup_and_replace_directory "$source_qwen_dir" "$global_qwen_dir" ".qwen directory" "$backup_folder" + if backup_and_replace_directory "$source_qwen_dir" "$global_qwen_dir" ".qwen directory" "$backup_folder"; then + # Track .qwen directory in manifest + add_manifest_entry "$manifest_file" "$global_qwen_dir" "Directory" + + # Track files from SOURCE directory + while IFS= read -r -d '' source_file; do + local relative_path="${source_file#$source_qwen_dir}" + local target_path="${global_qwen_dir}${relative_path}" + add_manifest_entry "$manifest_file" "$target_path" "File" + done < <(find "$source_qwen_dir" -type f -print0) + fi # Remove empty backup folder if [ -n "$backup_folder" ] && [ -d "$backup_folder" ]; then @@ -537,6 +587,9 @@ function install_global() { write_color "Creating version.json..." "$COLOR_INFO" create_version_json "$global_claude_dir" "Global" + # Save installation manifest + save_install_manifest "$manifest_file" + return 0 } @@ -550,6 +603,9 @@ function install_path() { local global_claude_dir="${user_home}/.claude" write_color "Global path: $user_home" "$COLOR_INFO" + # Initialize manifest + local manifest_file=$(new_install_manifest "Path" "$target_dir") + # Source paths local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local source_claude_dir="${script_dir}/.claude" @@ -588,7 +644,17 @@ function install_path() { if [ -d "$source_folder" ]; then # Use new backup and replace logic for local folders write_color "Installing local folder: $folder..." "$COLOR_INFO" - backup_and_replace_directory "$source_folder" "$dest_folder" "$folder folder" "$backup_folder" + if backup_and_replace_directory "$source_folder" "$dest_folder" "$folder folder" "$backup_folder"; then + # Track local folder in manifest + add_manifest_entry "$manifest_file" "$dest_folder" "Directory" + + # Track files from SOURCE directory + while IFS= read -r -d '' source_file; do + local relative_path="${source_file#$source_folder}" + local target_path="${dest_folder}${relative_path}" + add_manifest_entry "$manifest_file" "$target_path" "File" + done < <(find "$source_folder" -type f -print0) + fi write_color "✓ Installed local folder: $folder" "$COLOR_SUCCESS" else write_color "WARNING: Source folder not found: $folder" "$COLOR_WARNING" @@ -644,19 +710,52 @@ function install_path() { # Handle CLAUDE.md file in global .claude directory local global_claude_md="${global_claude_dir}/CLAUDE.md" write_color "Installing CLAUDE.md to global .claude directory..." "$COLOR_INFO" - copy_file_to_destination "$source_claude_md" "$global_claude_md" "CLAUDE.md" "$backup_folder" + if copy_file_to_destination "$source_claude_md" "$global_claude_md" "CLAUDE.md" "$backup_folder"; then + # Track CLAUDE.md in manifest + add_manifest_entry "$manifest_file" "$global_claude_md" "File" + fi # Replace .codex directory to local location (backup → clear conflicting → copy) write_color "Installing .codex directory to local location..." "$COLOR_INFO" - backup_and_replace_directory "$source_codex_dir" "$local_codex_dir" ".codex directory" "$backup_folder" + if backup_and_replace_directory "$source_codex_dir" "$local_codex_dir" ".codex directory" "$backup_folder"; then + # Track .codex directory in manifest + add_manifest_entry "$manifest_file" "$local_codex_dir" "Directory" + + # Track files from SOURCE directory + while IFS= read -r -d '' source_file; do + local relative_path="${source_file#$source_codex_dir}" + local target_path="${local_codex_dir}${relative_path}" + add_manifest_entry "$manifest_file" "$target_path" "File" + done < <(find "$source_codex_dir" -type f -print0) + fi # Replace .gemini directory to local location (backup → clear conflicting → copy) write_color "Installing .gemini directory to local location..." "$COLOR_INFO" - backup_and_replace_directory "$source_gemini_dir" "$local_gemini_dir" ".gemini directory" "$backup_folder" + if backup_and_replace_directory "$source_gemini_dir" "$local_gemini_dir" ".gemini directory" "$backup_folder"; then + # Track .gemini directory in manifest + add_manifest_entry "$manifest_file" "$local_gemini_dir" "Directory" + + # Track files from SOURCE directory + while IFS= read -r -d '' source_file; do + local relative_path="${source_file#$source_gemini_dir}" + local target_path="${local_gemini_dir}${relative_path}" + add_manifest_entry "$manifest_file" "$target_path" "File" + done < <(find "$source_gemini_dir" -type f -print0) + fi # Replace .qwen directory to local location (backup → clear conflicting → copy) write_color "Installing .qwen directory to local location..." "$COLOR_INFO" - backup_and_replace_directory "$source_qwen_dir" "$local_qwen_dir" ".qwen directory" "$backup_folder" + if backup_and_replace_directory "$source_qwen_dir" "$local_qwen_dir" ".qwen directory" "$backup_folder"; then + # Track .qwen directory in manifest + add_manifest_entry "$manifest_file" "$local_qwen_dir" "Directory" + + # Track files from SOURCE directory + while IFS= read -r -d '' source_file; do + local relative_path="${source_file#$source_qwen_dir}" + local target_path="${local_qwen_dir}${relative_path}" + add_manifest_entry "$manifest_file" "$target_path" "File" + done < <(find "$source_qwen_dir" -type f -print0) + fi # Remove empty backup folder if [ -n "$backup_folder" ] && [ -d "$backup_folder" ]; then @@ -674,6 +773,9 @@ function install_path() { write_color "Creating version.json in global directory..." "$COLOR_INFO" create_version_json "$global_claude_dir" "Global" + # Save installation manifest + save_install_manifest "$manifest_file" + return 0 } @@ -749,6 +851,357 @@ function get_installation_path() { done } +# ============================================================================ +# INSTALLATION MANIFEST MANAGEMENT +# ============================================================================ + +function new_install_manifest() { + local installation_mode="$1" + local installation_path="$2" + + # Create manifest directory if it doesn't exist + mkdir -p "$MANIFEST_DIR" + + # Generate unique manifest ID based on timestamp and mode + local timestamp=$(date +"%Y%m%d-%H%M%S") + local manifest_id="install-${installation_mode}-${timestamp}" + + # Create manifest file path + local manifest_file="${MANIFEST_DIR}/${manifest_id}.json" + + # Get current UTC timestamp + local installation_date_utc=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + # Create manifest JSON + cat > "$manifest_file" << EOF +{ + "manifest_id": "$manifest_id", + "version": "1.0", + "installation_mode": "$installation_mode", + "installation_path": "$installation_path", + "installation_date": "$installation_date_utc", + "installer_version": "$VERSION", + "files": [], + "directories": [] +} +EOF + + echo "$manifest_file" +} + +function add_manifest_entry() { + local manifest_file="$1" + local entry_path="$2" + local entry_type="$3" + + if [ ! -f "$manifest_file" ]; then + write_color "WARNING: Manifest file not found: $manifest_file" "$COLOR_WARNING" + return 1 + fi + + local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + # Escape path for JSON + local escaped_path=$(echo "$entry_path" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') + + # Create entry JSON + local entry_json=$(cat << EOF +{ + "path": "$escaped_path", + "type": "$entry_type", + "timestamp": "$timestamp" +} +EOF +) + + # Read manifest, add entry, write back + local temp_file="${manifest_file}.tmp" + + if [ "$entry_type" = "File" ]; then + jq --argjson entry "$entry_json" '.files += [$entry]' "$manifest_file" > "$temp_file" + else + jq --argjson entry "$entry_json" '.directories += [$entry]' "$manifest_file" > "$temp_file" + fi + + mv "$temp_file" "$manifest_file" +} + +function save_install_manifest() { + local manifest_file="$1" + + if [ -f "$manifest_file" ]; then + write_color "Installation manifest saved: $manifest_file" "$COLOR_SUCCESS" + return 0 + else + write_color "WARNING: Failed to save installation manifest" "$COLOR_WARNING" + return 1 + fi +} + +function migrate_legacy_manifest() { + local legacy_manifest="${HOME}/.claude-install-manifest.json" + + if [ ! -f "$legacy_manifest" ]; then + return 0 + fi + + write_color "Found legacy manifest file, migrating to new system..." "$COLOR_INFO" + + # Create manifest directory if it doesn't exist + mkdir -p "$MANIFEST_DIR" + + # Read legacy manifest + local mode=$(jq -r '.installation_mode // "Global"' "$legacy_manifest") + local timestamp=$(date +"%Y%m%d-%H%M%S") + local manifest_id="install-${mode}-${timestamp}-migrated" + + # Create new manifest file + local new_manifest="${MANIFEST_DIR}/${manifest_id}.json" + + # Copy with new manifest_id field + jq --arg id "$manifest_id" '. + {manifest_id: $id}' "$legacy_manifest" > "$new_manifest" + + # Rename old manifest (don't delete, keep as backup) + mv "$legacy_manifest" "${legacy_manifest}.migrated" + + write_color "Legacy manifest migrated successfully" "$COLOR_SUCCESS" + write_color "Old manifest backed up to: ${legacy_manifest}.migrated" "$COLOR_INFO" +} + +function get_all_install_manifests() { + # Migrate legacy manifest if exists + migrate_legacy_manifest + + if [ ! -d "$MANIFEST_DIR" ]; then + echo "[]" + return + fi + + # Check if any manifest files exist + local manifest_count=$(find "$MANIFEST_DIR" -name "install-*.json" -type f 2>/dev/null | wc -l) + + if [ "$manifest_count" -eq 0 ]; then + echo "[]" + return + fi + + # Collect all manifests into JSON array + local manifests="[" + local first=true + + while IFS= read -r -d '' file; do + if [ "$first" = true ]; then + first=false + else + manifests+="," + fi + + # Add manifest_file field + local manifest_content=$(jq --arg file "$file" '. + {manifest_file: $file}' "$file") + + # Count files and directories safely + local files_count=$(echo "$manifest_content" | jq '.files | length') + local dirs_count=$(echo "$manifest_content" | jq '.directories | length') + + # Add counts to manifest + manifest_content=$(echo "$manifest_content" | jq --argjson fc "$files_count" --argjson dc "$dirs_count" '. + {files_count: $fc, directories_count: $dc}') + + manifests+="$manifest_content" + done < <(find "$MANIFEST_DIR" -name "install-*.json" -type f -print0 | sort -z) + + manifests+="]" + + echo "$manifests" +} + +# ============================================================================ +# UNINSTALLATION FUNCTIONS +# ============================================================================ + +function uninstall_claude_workflow() { + write_color "Claude Code Workflow System Uninstaller" "$COLOR_INFO" + write_color "========================================" "$COLOR_INFO" + echo "" + + # Load all manifests + local manifests_json=$(get_all_install_manifests) + local manifests_count=$(echo "$manifests_json" | jq 'length') + + if [ "$manifests_count" -eq 0 ]; then + write_color "ERROR: No installation manifests found in: $MANIFEST_DIR" "$COLOR_ERROR" + write_color "Cannot proceed with uninstallation without manifest." "$COLOR_ERROR" + echo "" + write_color "Manual uninstallation instructions:" "$COLOR_INFO" + echo "For Global installation, remove these directories:" + echo " - ~/.claude/agents" + echo " - ~/.claude/commands" + echo " - ~/.claude/output-styles" + echo " - ~/.claude/workflows" + echo " - ~/.claude/scripts" + echo " - ~/.claude/prompt-templates" + echo " - ~/.claude/python_script" + echo " - ~/.claude/skills" + echo " - ~/.claude/version.json" + echo " - ~/.claude/CLAUDE.md" + echo " - ~/.codex" + echo " - ~/.gemini" + echo " - ~/.qwen" + return 1 + fi + + # Display available installations + write_color "Found $manifests_count installation(s):" "$COLOR_INFO" + echo "" + + # If only one manifest, use it directly + local selected_index=0 + local selected_manifest="" + + if [ "$manifests_count" -eq 1 ]; then + selected_manifest=$(echo "$manifests_json" | jq '.[0]') + write_color "Only one installation found, will uninstall:" "$COLOR_INFO" + else + # Multiple manifests - let user choose + local options=() + + for i in $(seq 0 $((manifests_count - 1))); do + local m=$(echo "$manifests_json" | jq ".[$i]") + + # Safely extract date string + local date_str=$(echo "$m" | jq -r '.installation_date // "unknown date"' | cut -c1-10) + local mode=$(echo "$m" | jq -r '.installation_mode // "Unknown"') + local files_count=$(echo "$m" | jq -r '.files_count // 0') + local dirs_count=$(echo "$m" | jq -r '.directories_count // 0') + local path_info=$(echo "$m" | jq -r '.installation_path // ""') + + if [ -n "$path_info" ]; then + path_info=" ($path_info)" + fi + + options+=("$((i + 1)). [$mode] $date_str - $files_count files, $dirs_count dirs$path_info") + done + + options+=("Cancel - Don't uninstall anything") + + echo "" + local selection=$(get_user_choice "Select installation to uninstall:" "${options[@]}") + + if [[ "$selection" == Cancel* ]]; then + write_color "Uninstallation cancelled." "$COLOR_WARNING" + return 1 + fi + + # Parse selection to get index + selected_index=$((${selection%%.*} - 1)) + selected_manifest=$(echo "$manifests_json" | jq ".[$selected_index]") + fi + + # Display selected installation info + echo "" + write_color "Installation Information:" "$COLOR_INFO" + echo " Manifest ID: $(echo "$selected_manifest" | jq -r '.manifest_id')" + echo " Mode: $(echo "$selected_manifest" | jq -r '.installation_mode')" + echo " Path: $(echo "$selected_manifest" | jq -r '.installation_path')" + echo " Date: $(echo "$selected_manifest" | jq -r '.installation_date')" + echo " Installer Version: $(echo "$selected_manifest" | jq -r '.installer_version')" + echo " Files tracked: $(echo "$selected_manifest" | jq -r '.files_count')" + echo " Directories tracked: $(echo "$selected_manifest" | jq -r '.directories_count')" + echo "" + + # Confirm uninstallation + if ! confirm_action "Do you want to uninstall this installation?" false; then + write_color "Uninstallation cancelled." "$COLOR_WARNING" + return 1 + fi + + local removed_files=0 + local removed_dirs=0 + local failed_items=() + + # Remove files first + write_color "Removing installed files..." "$COLOR_INFO" + + local files_array=$(echo "$selected_manifest" | jq -c '.files[]') + + while IFS= read -r file_entry; do + local file_path=$(echo "$file_entry" | jq -r '.path') + + if [ -f "$file_path" ]; then + if rm -f "$file_path" 2>/dev/null; then + write_color " Removed file: $file_path" "$COLOR_SUCCESS" + ((removed_files++)) + else + write_color " WARNING: Failed to remove file: $file_path" "$COLOR_WARNING" + failed_items+=("$file_path") + fi + else + write_color " File not found (already removed): $file_path" "$COLOR_INFO" + fi + done <<< "$files_array" + + # Remove directories (in reverse order by path length) + write_color "Removing installed directories..." "$COLOR_INFO" + + local dirs_array=$(echo "$selected_manifest" | jq -c '.directories[] | {path: .path, length: (.path | length)}' | sort -t: -k2 -rn | jq -c '.path') + + while IFS= read -r dir_path_json; do + local dir_path=$(echo "$dir_path_json" | jq -r '.') + + if [ -d "$dir_path" ]; then + # Check if directory is empty + if [ -z "$(ls -A "$dir_path" 2>/dev/null)" ]; then + if rmdir "$dir_path" 2>/dev/null; then + write_color " Removed directory: $dir_path" "$COLOR_SUCCESS" + ((removed_dirs++)) + else + write_color " WARNING: Failed to remove directory: $dir_path" "$COLOR_WARNING" + failed_items+=("$dir_path") + fi + else + write_color " Directory not empty (preserved): $dir_path" "$COLOR_WARNING" + fi + else + write_color " Directory not found (already removed): $dir_path" "$COLOR_INFO" + fi + done <<< "$dirs_array" + + # Remove manifest file + local manifest_file=$(echo "$selected_manifest" | jq -r '.manifest_file') + + if [ -f "$manifest_file" ]; then + if rm -f "$manifest_file" 2>/dev/null; then + write_color "Removed installation manifest: $(basename "$manifest_file")" "$COLOR_SUCCESS" + else + write_color "WARNING: Failed to remove manifest file" "$COLOR_WARNING" + fi + fi + + # Show summary + echo "" + write_color "========================================" "$COLOR_INFO" + write_color "Uninstallation Summary:" "$COLOR_INFO" + echo " Files removed: $removed_files" + echo " Directories removed: $removed_dirs" + + if [ ${#failed_items[@]} -gt 0 ]; then + echo "" + write_color "Failed to remove the following items:" "$COLOR_WARNING" + for item in "${failed_items[@]}"; do + echo " - $item" + done + fi + + echo "" + if [ ${#failed_items[@]} -eq 0 ]; then + write_color "✓ Claude Code Workflow has been successfully uninstalled!" "$COLOR_SUCCESS" + else + write_color "Uninstallation completed with warnings." "$COLOR_WARNING" + write_color "Please manually remove the failed items listed above." "$COLOR_INFO" + fi + + return 0 +} + function create_version_json() { local target_claude_dir="$1" local installation_mode="$2" @@ -863,6 +1316,10 @@ function parse_arguments() { BACKUP_ALL=false shift ;; + -Uninstall) + UNINSTALL=true + shift + ;; -SourceVersion) SOURCE_VERSION="$2" shift 2 @@ -901,6 +1358,7 @@ Options: -NonInteractive Run in non-interactive mode with default options -BackupAll Automatically backup all existing files (default) -NoBackup Disable automatic backup functionality + -Uninstall Uninstall Claude Code Workflow System based on installation manifest -SourceVersion Source version (passed from install-remote.sh) -SourceBranch Source branch (passed from install-remote.sh) -SourceCommit Source commit SHA (passed from install-remote.sh) @@ -919,6 +1377,12 @@ Examples: # Installation without backup $0 -NoBackup + # Uninstall Claude Code Workflow System + $0 -Uninstall + + # Uninstall without confirmation prompts + $0 -Uninstall -Force + # With version info (typically called by install-remote.sh) $0 -InstallMode Global -Force -SourceVersion "3.4.2" -SourceBranch "main" -SourceCommit "abc1234" @@ -926,6 +1390,46 @@ EOF } function main() { + # Show banner first + show_banner + + # Check for uninstall mode from parameter or ask user interactively + local operation_mode="Install" + + if [ "$UNINSTALL" = true ]; then + operation_mode="Uninstall" + elif [ "$NON_INTERACTIVE" != true ] && [ -z "$INSTALL_MODE" ]; then + # Interactive mode selection + echo "" + local operations=( + "Install - Install Claude Code Workflow System" + "Uninstall - Remove Claude Code Workflow System" + ) + local selection=$(get_user_choice "Choose operation:" "${operations[@]}") + + if [[ "$selection" == Uninstall* ]]; then + operation_mode="Uninstall" + fi + fi + + # Handle uninstall mode + if [ "$operation_mode" = "Uninstall" ]; then + if uninstall_claude_workflow; then + local result=0 + else + local result=1 + fi + + if [ "$NON_INTERACTIVE" != true ]; then + echo "" + write_color "Press Enter to exit..." "$COLOR_PROMPT" + read -r + fi + + return $result + fi + + # Continue with installation show_header # Test prerequisites