From afe918d1468300067a4d44362d5e64353b35016a Mon Sep 17 00:00:00 2001 From: catlog22 Date: Wed, 1 Oct 2025 17:14:17 +0800 Subject: [PATCH] feat: Enhanced installer with ASCII art banner and arrow key navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major improvements: - Add colorful ASCII art banner (CLAUDE/CODE/WORKFLOW in Cyan/Green/Yellow) - Implement arrow key navigation (↑/↓) for installation mode selection - Add new Path installation mode (hybrid: local agents/commands/output-styles + global workflows/scripts) - Fix parameter type conversion error for $success variable - Improve console capability detection with graceful fallback to numbered menu - Use single-quoted strings to properly escape $ symbols in ASCII art Technical enhancements: - New Get-UserChoiceWithArrows function with keyboard input handling - New Install-Path function for hybrid installation - Enhanced Show-Banner with three-section colored ASCII art - Better error handling and stack trace output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Install-Claude.ps1 | 446 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 405 insertions(+), 41 deletions(-) diff --git a/Install-Claude.ps1 b/Install-Claude.ps1 index 3e1cb4c5..29f1b307 100644 --- a/Install-Claude.ps1 +++ b/Install-Claude.ps1 @@ -48,15 +48,17 @@ #> param( - [ValidateSet("Global")] - [string]$InstallMode = "Global", - + [ValidateSet("Global", "Path")] + [string]$InstallMode = "", + + [string]$TargetPath = "", + [switch]$Force, - + [switch]$NonInteractive, - + [switch]$BackupAll, - + [switch]$NoBackup ) @@ -95,13 +97,54 @@ function Write-ColorOutput { 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 { - Write-ColorOutput "==== $ScriptName v$Version ====" $ColorInfo - Write-ColorOutput "========================================================" $ColorInfo + Show-Banner + Write-ColorOutput " $ScriptName v$Version" $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 without backup!" $ColorWarning + Write-ColorOutput "WARNING: Backup disabled - existing files will be overwritten!" $ColorWarning } else { - Write-ColorOutput "Auto-backup enabled - existing files will be backed up before replacement" $ColorSuccess + Write-ColorOutput "Auto-backup enabled - existing files will be backed up" $ColorSuccess } Write-Host "" } @@ -133,18 +176,130 @@ function Test-Prerequisites { 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) { @@ -154,18 +309,18 @@ function Get-UserChoice { } 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) } @@ -457,19 +612,19 @@ function Merge-DirectoryContents { 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" - + Write-ColorOutput "Global installation path: $userProfile" $ColorInfo - + # Source paths $sourceDir = $PSScriptRoot $sourceClaudeDir = Join-Path $sourceDir ".claude" $sourceClaudeMd = Join-Path $sourceDir "CLAUDE.md" - + # Create backup folder if needed (default behavior unless NoBackup is specified) $backupFolder = $null if (-not $NoBackup) { @@ -485,15 +640,15 @@ function Install-Global { Write-ColorOutput "Backup folder created: $backupFolder" $ColorInfo } } - + # Merge .claude directory contents (don't replace entire directory) Write-ColorOutput "Merging .claude directory contents..." $ColorInfo $claudeMerged = Merge-DirectoryContents -Source $sourceClaudeDir -Destination $globalClaudeDir -Description ".claude directory contents" -BackupFolder $backupFolder - + # 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 - + if ($backupFolder -and (Test-Path $backupFolder)) { $backupFiles = Get-ChildItem $backupFolder -Recurse -File -ErrorAction SilentlyContinue if (-not $backupFiles -or ($backupFiles | Measure-Object).Count -eq 0) { @@ -502,16 +657,207 @@ function Install-Global { Write-ColorOutput "Removed empty backup folder" $ColorInfo } } - + + 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 + + # Source paths + $sourceDir = $PSScriptRoot + $sourceClaudeDir = Join-Path $sourceDir ".claude" + $sourceClaudeMd = Join-Path $sourceDir "CLAUDE.md" + + # Local paths - only for agents, commands, output-styles + $localClaudeDir = Join-Path $TargetDirectory ".claude" + + # Create backup folder if needed + $backupFolder = $null + if (-not $NoBackup) { + if ((Test-Path $localClaudeDir) -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) { + if (Test-Path $destFolderPath) { + if ($backupFolder) { + Backup-DirectoryToFolder -DirectoryPath $destFolderPath -BackupFolder $backupFolder + } + } + + Copy-DirectoryRecursive -Source $sourceFolderPath -Destination $destFolderPath + Write-ColorOutput "Installed local folder: $folder" $ColorSuccess + } else { + Write-ColorOutput "WARNING: Source folder not found: $folder" $ColorWarning + } + } + + # Global components - exclude local folders + Write-ColorOutput "Installing global components to $globalClaudeDir..." $ColorInfo + + # Get all items from source, excluding local folders + $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 + } + + $mergedCount = 0 + foreach ($item in $sourceItems) { + $relativePath = $item.FullName.Substring($sourceClaudeDir.Length + 1) + $destinationPath = Join-Path $globalClaudeDir $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) { + if ($BackupAll -and -not $NoBackup) { + if ($backupFolder) { + Backup-FileToFolder -FilePath $destinationPath -BackupFolder $backupFolder + } + Copy-Item -Path $item.FullName -Destination $destinationPath -Force + $mergedCount++ + } elseif ($NoBackup) { + if (Confirm-Action "File '$relativePath' already exists in global location. Replace it? (NO BACKUP)" -DefaultYes:$false) { + Copy-Item -Path $item.FullName -Destination $destinationPath -Force + $mergedCount++ + } + } elseif (Confirm-Action "File '$relativePath' already exists in global location. Replace it?" -DefaultYes:$false) { + if ($backupFolder) { + Backup-FileToFolder -FilePath $destinationPath -BackupFolder $backupFolder + } + Copy-Item -Path $item.FullName -Destination $destinationPath -Force + $mergedCount++ + } + } else { + Copy-Item -Path $item.FullName -Destination $destinationPath -Force + $mergedCount++ + } + } + + Write-ColorOutput "Merged $mergedCount files to global location" $ColorSuccess + + # 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 + + 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 + } + } + return $true } function Get-InstallationMode { - Write-ColorOutput "Installation mode: Global (installing to user profile ~/.claude/)" $ColorInfo + 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( @@ -519,17 +865,26 @@ function Show-Summary { [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" - Write-Host " Path: $Path" + + if ($Mode -eq "Path") { + Write-Host " Local Path: $Path" + Write-Host " Global Path: $([Environment]::GetFolderPath('UserProfile'))" + Write-Host " Local Components: agents, commands, output-styles" + Write-Host " Global Components: workflows, scripts, python_script, etc." + } else { + Write-Host " Path: $Path" + } + if ($NoBackup) { Write-Host " Backup: Disabled (no backup created)" } elseif ($BackupAll) { @@ -537,7 +892,7 @@ function Show-Summary { } 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" @@ -545,7 +900,7 @@ function Show-Summary { Write-Host "3. Start using Claude Code with Agent workflow coordination!" Write-Host "4. Use /workflow commands for task execution" Write-Host "5. 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 @@ -553,48 +908,57 @@ function Show-Summary { function Main { Show-Header - + # 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 - - $installPath = [Environment]::GetFolderPath("UserProfile") - $success = Install-Global - - Show-Summary -Mode $mode -Path $installPath -Success $success - + + 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 } }