Files
Claude-Code-Workflow/Install-Claude.ps1
catlog22 b0b99a4217 Enhance installation and uninstallation processes
- Added optional quiet mode for backup notifications to reduce output clutter.
- Improved file merging with progress reporting and summary of backed up files.
- Implemented a cleanup function to move old installations to a backup directory before new installations.
- Enhanced manifest handling to distinguish between Global and Path installations.
- Updated uninstallation process to preserve global files if a Global installation exists.
- Improved error handling and user feedback during file operations.
2025-11-22 23:20:39 +08:00

2132 lines
80 KiB
PowerShell

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