mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-04 01:40:45 +08:00
- 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.
2132 lines
80 KiB
PowerShell
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) |