mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
- Add Remove-OldManifestsForPath function to automatically clean old manifests - Modify Save-InstallManifest to remove old manifests before saving new one - Update Get-AllInstallManifests to return only latest manifest per installation path - Apply same strategy to both Install-Claude.ps1 (PowerShell) and Install-Claude.sh (Bash) Benefits: - Each installation location registers only once - Only latest version manifest is retained - Uninstall UI shows only latest version per location - Prevents manifest file accumulation
1896 lines
72 KiB
PowerShell
1896 lines
72 KiB
PowerShell
#Requires -Version 5.1
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Claude Code Workflow System Interactive Installer
|
|
|
|
.DESCRIPTION
|
|
Installation script for Claude Code Workflow System with Agent coordination and distributed memory system.
|
|
Installs globally to user profile directory (~/.claude) by default.
|
|
|
|
.PARAMETER InstallMode
|
|
Installation mode: "Global" (default and only supported mode)
|
|
|
|
.PARAMETER TargetPath
|
|
Target path for Custom 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
|
|
#>
|
|
|
|
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.2.0" # Installer script version
|
|
|
|
# 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
|
|
)
|
|
|
|
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
|
|
|
|
Write-ColorOutput "Backed up: $fileName" $ColorInfo
|
|
return $true
|
|
} catch {
|
|
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
|
|
$mergedCount = 0
|
|
$skippedCount = 0
|
|
|
|
foreach ($item in $sourceItems) {
|
|
# 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)) {
|
|
Write-ColorOutput "Auto-backed up: $fileName" $ColorInfo
|
|
}
|
|
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 {
|
|
Write-ColorOutput "Skipped $fileName (no backup)" $ColorWarning
|
|
$skippedCount++
|
|
}
|
|
} elseif (Confirm-Action "File '$relativePath' already exists. Replace it?" -DefaultYes:$false) {
|
|
if ($BackupFolder -and (Backup-FileToFolder -FilePath $destinationPath -BackupFolder $BackupFolder)) {
|
|
Write-ColorOutput "Backed up existing $fileName" $ColorInfo
|
|
}
|
|
Copy-Item -Path $item.FullName -Destination $destinationPath -Force
|
|
$mergedCount++
|
|
} else {
|
|
Write-ColorOutput "Skipped $fileName" $ColorWarning
|
|
$skippedCount++
|
|
}
|
|
} else {
|
|
Copy-Item -Path $item.FullName -Destination $destinationPath -Force
|
|
$mergedCount++
|
|
}
|
|
}
|
|
|
|
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
|
|
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|
$manifestId = "install-$InstallationMode-$timestamp"
|
|
|
|
$manifest = @{
|
|
manifest_id = $manifestId
|
|
version = "1.0"
|
|
installation_mode = $InstallationMode
|
|
installation_path = $InstallationPath
|
|
installation_date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
installer_version = $ScriptVersion
|
|
files = @()
|
|
directories = @()
|
|
}
|
|
|
|
return $manifest
|
|
}
|
|
|
|
function Add-ManifestEntry {
|
|
<#
|
|
.SYNOPSIS
|
|
Add a file or directory entry to the manifest
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[hashtable]$Manifest,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateSet("File", "Directory")]
|
|
[string]$Type
|
|
)
|
|
|
|
$entry = @{
|
|
path = $Path
|
|
type = $Type
|
|
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
}
|
|
|
|
if ($Type -eq "File") {
|
|
$Manifest.files += $entry
|
|
} else {
|
|
$Manifest.directories += $entry
|
|
}
|
|
}
|
|
|
|
function Remove-OldManifestsForPath {
|
|
<#
|
|
.SYNOPSIS
|
|
Remove old manifest files for the same installation path
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$InstallationPath
|
|
)
|
|
|
|
if (-not (Test-Path $script:ManifestDir)) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
$manifestFiles = Get-ChildItem -Path $script:ManifestDir -Filter "install-*.json" -File
|
|
$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()
|
|
|
|
# If paths match, remove this old manifest
|
|
if ($manifestPath -eq $targetPath) {
|
|
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
|
|
if ($Manifest.installation_path) {
|
|
Remove-OldManifestsForPath -InstallationPath $Manifest.installation_path
|
|
}
|
|
|
|
# Use manifest ID to create unique file name
|
|
$manifestFileName = "$($Manifest.manifest_id).json"
|
|
$manifestPath = Join-Path $script:ManifestDir $manifestFileName
|
|
|
|
$Manifest | ConvertTo-Json -Depth 10 | Out-File -FilePath $manifestPath -Encoding utf8 -Force
|
|
Write-ColorOutput "Installation manifest saved: $manifestPath" $ColorSuccess
|
|
return $true
|
|
} catch {
|
|
Write-ColorOutput "WARNING: Failed to save installation manifest: $($_.Exception.Message)" $ColorWarning
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Migrate-LegacyManifest {
|
|
<#
|
|
.SYNOPSIS
|
|
Migrate old single manifest file to new multi-manifest system
|
|
#>
|
|
|
|
$legacyManifestPath = Join-Path ([Environment]::GetFolderPath("UserProfile")) ".claude-install-manifest.json"
|
|
|
|
if (-not (Test-Path $legacyManifestPath)) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
Write-ColorOutput "Found legacy manifest file, migrating to new system..." $ColorInfo
|
|
|
|
# Create manifest directory if it doesn't exist
|
|
if (-not (Test-Path $script:ManifestDir)) {
|
|
New-Item -ItemType Directory -Path $script:ManifestDir -Force | Out-Null
|
|
}
|
|
|
|
# Read legacy manifest
|
|
$legacyJson = Get-Content -Path $legacyManifestPath -Raw -Encoding utf8
|
|
$legacy = $legacyJson | ConvertFrom-Json
|
|
|
|
# Generate new manifest ID
|
|
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|
$mode = if ($legacy.installation_mode) { $legacy.installation_mode } else { "Global" }
|
|
$manifestId = "install-$mode-$timestamp-migrated"
|
|
|
|
# Create new manifest with all fields
|
|
$newManifest = @{
|
|
manifest_id = $manifestId
|
|
version = if ($legacy.version) { $legacy.version } else { "1.0" }
|
|
installation_mode = $mode
|
|
installation_path = if ($legacy.installation_path) { $legacy.installation_path } else { [Environment]::GetFolderPath("UserProfile") }
|
|
installation_date = if ($legacy.installation_date) { $legacy.installation_date } else { (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") }
|
|
installer_version = if ($legacy.installer_version) { $legacy.installer_version } else { "unknown" }
|
|
files = if ($legacy.files) { @($legacy.files) } else { @() }
|
|
directories = if ($legacy.directories) { @($legacy.directories) } else { @() }
|
|
}
|
|
|
|
# Save to new location
|
|
$newManifestPath = Join-Path $script:ManifestDir "$manifestId.json"
|
|
$newManifest | ConvertTo-Json -Depth 10 | Out-File -FilePath $newManifestPath -Encoding utf8 -Force
|
|
|
|
# Rename old manifest (don't delete, keep as backup)
|
|
$backupPath = "$legacyManifestPath.migrated"
|
|
Move-Item -Path $legacyManifestPath -Destination $backupPath -Force
|
|
|
|
Write-ColorOutput "Legacy manifest migrated successfully" $ColorSuccess
|
|
Write-ColorOutput "Old manifest backed up to: $backupPath" $ColorInfo
|
|
} catch {
|
|
Write-ColorOutput "WARNING: Failed to migrate legacy manifest: $($_.Exception.Message)" $ColorWarning
|
|
}
|
|
}
|
|
|
|
function Get-AllInstallManifests {
|
|
<#
|
|
.SYNOPSIS
|
|
Get all installation manifests (only latest per installation path)
|
|
#>
|
|
|
|
# Migrate legacy manifest if exists
|
|
Migrate-LegacyManifest
|
|
|
|
if (-not (Test-Path $script:ManifestDir)) {
|
|
return @()
|
|
}
|
|
|
|
try {
|
|
$manifestFiles = Get-ChildItem -Path $script:ManifestDir -Filter "install-*.json" -File | Sort-Object LastWriteTime -Descending
|
|
$allManifests = [System.Collections.ArrayList]::new()
|
|
|
|
foreach ($file in $manifestFiles) {
|
|
try {
|
|
$manifestJson = Get-Content -Path $file.FullName -Raw -Encoding utf8
|
|
$manifest = $manifestJson | ConvertFrom-Json
|
|
|
|
# Convert to hashtable for easier manipulation
|
|
# Handle both old and new manifest formats
|
|
|
|
# Safely get array counts
|
|
$filesCount = 0
|
|
$dirsCount = 0
|
|
|
|
if ($manifest.files) {
|
|
if ($manifest.files -is [System.Array]) {
|
|
$filesCount = $manifest.files.Count
|
|
} else {
|
|
$filesCount = 1
|
|
}
|
|
}
|
|
|
|
if ($manifest.directories) {
|
|
if ($manifest.directories -is [System.Array]) {
|
|
$dirsCount = $manifest.directories.Count
|
|
} else {
|
|
$dirsCount = 1
|
|
}
|
|
}
|
|
|
|
$manifestHash = @{
|
|
manifest_id = if ($manifest.manifest_id) { $manifest.manifest_id } else { $file.BaseName }
|
|
manifest_file = $file.FullName
|
|
version = if ($manifest.version) { $manifest.version } else { "1.0" }
|
|
installation_mode = if ($manifest.installation_mode) { $manifest.installation_mode } else { "Unknown" }
|
|
installation_path = if ($manifest.installation_path) { $manifest.installation_path } else { "" }
|
|
installation_date = if ($manifest.installation_date) { $manifest.installation_date } else { $file.LastWriteTime.ToString("yyyy-MM-ddTHH:mm:ssZ") }
|
|
installer_version = if ($manifest.installer_version) { $manifest.installer_version } else { "unknown" }
|
|
files = if ($manifest.files) { @($manifest.files) } else { @() }
|
|
directories = if ($manifest.directories) { @($manifest.directories) } else { @() }
|
|
files_count = $filesCount
|
|
directories_count = $dirsCount
|
|
}
|
|
|
|
$null = $allManifests.Add($manifestHash)
|
|
} catch {
|
|
Write-ColorOutput "WARNING: Failed to load manifest $($file.Name): $($_.Exception.Message)" $ColorWarning
|
|
}
|
|
}
|
|
|
|
# Group by installation_path (normalized) and keep only the latest per path
|
|
$pathGroups = @{}
|
|
foreach ($manifest in $allManifests) {
|
|
$normalizedPath = $manifest.installation_path.TrimEnd('\', '/').ToLower()
|
|
|
|
if (-not $pathGroups.ContainsKey($normalizedPath)) {
|
|
$pathGroups[$normalizedPath] = @()
|
|
}
|
|
$pathGroups[$normalizedPath] += $manifest
|
|
}
|
|
|
|
# Select the latest manifest for each path (based on installation_date)
|
|
$latestManifests = [System.Collections.ArrayList]::new()
|
|
foreach ($pathKey in $pathGroups.Keys) {
|
|
$groupManifests = $pathGroups[$pathKey]
|
|
|
|
# Sort by installation_date descending and take the first (latest)
|
|
$latest = $groupManifests | Sort-Object {
|
|
try {
|
|
[DateTime]::Parse($_.installation_date)
|
|
} catch {
|
|
[DateTime]::MinValue
|
|
}
|
|
} -Descending | Select-Object -First 1
|
|
|
|
if ($latest) {
|
|
$null = $latestManifests.Add($latest)
|
|
}
|
|
}
|
|
|
|
# Sort final results by installation_date descending
|
|
$sortedManifests = $latestManifests | Sort-Object {
|
|
try {
|
|
[DateTime]::Parse($_.installation_date)
|
|
} catch {
|
|
[DateTime]::MinValue
|
|
}
|
|
} -Descending
|
|
|
|
return ,$sortedManifests
|
|
} catch {
|
|
Write-ColorOutput "ERROR: Failed to list installation manifests: $($_.Exception.Message)" $ColorError
|
|
return @()
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# UNINSTALLATION FUNCTIONS
|
|
# ============================================================================
|
|
|
|
function Uninstall-ClaudeWorkflow {
|
|
<#
|
|
.SYNOPSIS
|
|
Uninstall Claude Code Workflow based on installation manifest
|
|
#>
|
|
|
|
Write-ColorOutput "Claude Code Workflow System Uninstaller" $ColorInfo
|
|
Write-ColorOutput "========================================" $ColorInfo
|
|
Write-Host ""
|
|
|
|
# Load all manifests
|
|
$manifests = Get-AllInstallManifests
|
|
|
|
if (-not $manifests -or $manifests.Count -eq 0) {
|
|
Write-ColorOutput "ERROR: No installation manifests found in: $script:ManifestDir" $ColorError
|
|
Write-ColorOutput "Cannot proceed with uninstallation without manifest." $ColorError
|
|
Write-Host ""
|
|
Write-ColorOutput "Manual uninstallation instructions:" $ColorInfo
|
|
Write-Host "For Global installation, remove these directories:"
|
|
Write-Host " - ~/.claude/agents"
|
|
Write-Host " - ~/.claude/commands"
|
|
Write-Host " - ~/.claude/output-styles"
|
|
Write-Host " - ~/.claude/workflows"
|
|
Write-Host " - ~/.claude/scripts"
|
|
Write-Host " - ~/.claude/prompt-templates"
|
|
Write-Host " - ~/.claude/python_script"
|
|
Write-Host " - ~/.claude/skills"
|
|
Write-Host " - ~/.claude/version.json"
|
|
Write-Host " - ~/.claude/CLAUDE.md"
|
|
Write-Host " - ~/.codex"
|
|
Write-Host " - ~/.gemini"
|
|
Write-Host " - ~/.qwen"
|
|
return $false
|
|
}
|
|
|
|
# Display available installations
|
|
Write-ColorOutput "Found $($manifests.Count) installation(s):" $ColorInfo
|
|
Write-Host ""
|
|
|
|
# If only one manifest, use it directly
|
|
$selectedManifest = $null
|
|
if ($manifests.Count -eq 1) {
|
|
$selectedManifest = $manifests[0]
|
|
Write-ColorOutput "Only one installation found, will uninstall:" $ColorInfo
|
|
} else {
|
|
# Multiple manifests - let user choose
|
|
$options = @()
|
|
for ($i = 0; $i -lt $manifests.Count; $i++) {
|
|
$m = $manifests[$i]
|
|
|
|
# Safely extract date string
|
|
$dateStr = "unknown date"
|
|
if ($m.installation_date) {
|
|
try {
|
|
if ($m.installation_date.Length -ge 10) {
|
|
$dateStr = $m.installation_date.Substring(0, 10)
|
|
} else {
|
|
$dateStr = $m.installation_date
|
|
}
|
|
} catch {
|
|
$dateStr = "unknown date"
|
|
}
|
|
}
|
|
|
|
# Build option string with safe counts
|
|
$filesCount = if ($m.files_count) { $m.files_count } else { 0 }
|
|
$dirsCount = if ($m.directories_count) { $m.directories_count } else { 0 }
|
|
$pathInfo = if ($m.installation_path) { " ($($m.installation_path))" } else { "" }
|
|
$option = "$($i + 1). [$($m.installation_mode)] $dateStr - $filesCount files, $dirsCount dirs$pathInfo"
|
|
$options += $option
|
|
}
|
|
$options += "Cancel - Don't uninstall anything"
|
|
|
|
Write-Host ""
|
|
$selection = Get-UserChoiceWithArrows -Prompt "Select installation to uninstall:" -Options $options -DefaultIndex 0
|
|
|
|
if ($selection -like "Cancel*") {
|
|
Write-ColorOutput "Uninstallation cancelled." $ColorWarning
|
|
return $false
|
|
}
|
|
|
|
# Parse selection to get index
|
|
$selectedIndex = [int]($selection.Split('.')[0]) - 1
|
|
$selectedManifest = $manifests[$selectedIndex]
|
|
}
|
|
|
|
# Display selected installation info
|
|
Write-Host ""
|
|
Write-ColorOutput "Installation Information:" $ColorInfo
|
|
Write-Host " Manifest ID: $($selectedManifest.manifest_id)"
|
|
Write-Host " Mode: $($selectedManifest.installation_mode)"
|
|
Write-Host " Path: $($selectedManifest.installation_path)"
|
|
Write-Host " Date: $($selectedManifest.installation_date)"
|
|
Write-Host " Installer Version: $($selectedManifest.installer_version)"
|
|
|
|
# Use pre-calculated counts
|
|
$filesCount = if ($selectedManifest.files_count) { $selectedManifest.files_count } else { 0 }
|
|
$dirsCount = if ($selectedManifest.directories_count) { $selectedManifest.directories_count } else { 0 }
|
|
Write-Host " Files tracked: $filesCount"
|
|
Write-Host " Directories tracked: $dirsCount"
|
|
Write-Host ""
|
|
|
|
# Confirm uninstallation
|
|
if (-not (Confirm-Action "Do you want to uninstall this installation?" -DefaultYes:$false)) {
|
|
Write-ColorOutput "Uninstallation cancelled." $ColorWarning
|
|
return $false
|
|
}
|
|
|
|
# Use the selected manifest for uninstallation
|
|
$manifest = $selectedManifest
|
|
|
|
$removedFiles = 0
|
|
$removedDirs = 0
|
|
$failedItems = @()
|
|
|
|
# Remove files first
|
|
Write-ColorOutput "Removing installed files..." $ColorInfo
|
|
foreach ($fileEntry in $manifest.files) {
|
|
$filePath = $fileEntry.path
|
|
|
|
if (Test-Path $filePath) {
|
|
try {
|
|
Remove-Item -Path $filePath -Force -ErrorAction Stop
|
|
Write-ColorOutput " Removed file: $filePath" $ColorSuccess
|
|
$removedFiles++
|
|
} catch {
|
|
Write-ColorOutput " WARNING: Failed to remove file: $filePath" $ColorWarning
|
|
$failedItems += $filePath
|
|
}
|
|
} else {
|
|
Write-ColorOutput " File not found (already removed): $filePath" $ColorInfo
|
|
}
|
|
}
|
|
|
|
# Remove directories (in reverse order to handle nested directories)
|
|
Write-ColorOutput "Removing installed directories..." $ColorInfo
|
|
$sortedDirs = $manifest.directories | Sort-Object { $_.path.Length } -Descending
|
|
|
|
foreach ($dirEntry in $sortedDirs) {
|
|
$dirPath = $dirEntry.path
|
|
|
|
if (Test-Path $dirPath) {
|
|
try {
|
|
# Check if directory is empty or only contains files we installed
|
|
$dirContents = Get-ChildItem -Path $dirPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
if (-not $dirContents -or ($dirContents | Measure-Object).Count -eq 0) {
|
|
Remove-Item -Path $dirPath -Recurse -Force -ErrorAction Stop
|
|
Write-ColorOutput " Removed directory: $dirPath" $ColorSuccess
|
|
$removedDirs++
|
|
} else {
|
|
Write-ColorOutput " Directory not empty (preserved): $dirPath" $ColorWarning
|
|
}
|
|
} catch {
|
|
Write-ColorOutput " WARNING: Failed to remove directory: $dirPath" $ColorWarning
|
|
$failedItems += $dirPath
|
|
}
|
|
} else {
|
|
Write-ColorOutput " Directory not found (already removed): $dirPath" $ColorInfo
|
|
}
|
|
}
|
|
|
|
# Remove manifest file
|
|
if (Test-Path $manifest.manifest_file) {
|
|
try {
|
|
Remove-Item -Path $manifest.manifest_file -Force
|
|
Write-ColorOutput "Removed installation manifest: $($manifest.manifest_id)" $ColorSuccess
|
|
} catch {
|
|
Write-ColorOutput "WARNING: Failed to remove manifest file" $ColorWarning
|
|
}
|
|
}
|
|
|
|
# Show summary
|
|
Write-Host ""
|
|
Write-ColorOutput "========================================" $ColorInfo
|
|
Write-ColorOutput "Uninstallation Summary:" $ColorInfo
|
|
Write-Host " Files removed: $removedFiles"
|
|
Write-Host " Directories removed: $removedDirs"
|
|
|
|
if ($failedItems.Count -gt 0) {
|
|
Write-Host ""
|
|
Write-ColorOutput "Failed to remove the following items:" $ColorWarning
|
|
foreach ($item in $failedItems) {
|
|
Write-Host " - $item"
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
if ($failedItems.Count -eq 0) {
|
|
Write-ColorOutput "Claude Code Workflow has been successfully uninstalled!" $ColorSuccess
|
|
} else {
|
|
Write-ColorOutput "Uninstallation completed with warnings." $ColorWarning
|
|
Write-ColorOutput "Please manually remove the failed items listed above." $ColorInfo
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Create-VersionJson {
|
|
param(
|
|
[string]$TargetClaudeDir,
|
|
[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
|
|
|
|
# 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
|
|
if ($claudeInstalled) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $globalClaudeDir -Type "Directory"
|
|
|
|
# Track files from SOURCE directory, not destination
|
|
Get-ChildItem -Path $sourceClaudeDir -Recurse -File | ForEach-Object {
|
|
# Calculate target path where this file will be installed
|
|
$relativePath = $_.FullName.Substring($sourceClaudeDir.Length)
|
|
$targetPath = $globalClaudeDir + $relativePath
|
|
Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File"
|
|
}
|
|
}
|
|
|
|
# Handle CLAUDE.md file in .claude directory
|
|
Write-ColorOutput "Installing CLAUDE.md to global .claude directory..." $ColorInfo
|
|
$claudeMdInstalled = Copy-FileToDestination -Source $sourceClaudeMd -Destination $globalClaudeMd -Description "CLAUDE.md" -BackupFolder $backupFolder
|
|
|
|
# Track CLAUDE.md in manifest
|
|
if ($claudeMdInstalled) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $globalClaudeMd -Type "File"
|
|
}
|
|
|
|
# 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
|
|
if ($codexInstalled) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $globalCodexDir -Type "Directory"
|
|
# Track files from SOURCE directory
|
|
Get-ChildItem -Path $sourceCodexDir -Recurse -File | ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($sourceCodexDir.Length)
|
|
$targetPath = $globalCodexDir + $relativePath
|
|
Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File"
|
|
}
|
|
}
|
|
|
|
# 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
|
|
if ($geminiInstalled) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $globalGeminiDir -Type "Directory"
|
|
# Track files from SOURCE directory
|
|
Get-ChildItem -Path $sourceGeminiDir -Recurse -File | ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($sourceGeminiDir.Length)
|
|
$targetPath = $globalGeminiDir + $relativePath
|
|
Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File"
|
|
}
|
|
}
|
|
|
|
# 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
|
|
if ($qwenInstalled) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $globalQwenDir -Type "Directory"
|
|
# Track files from SOURCE directory
|
|
Get-ChildItem -Path $sourceQwenDir -Recurse -File | ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($sourceQwenDir.Length)
|
|
$targetPath = $globalQwenDir + $relativePath
|
|
Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File"
|
|
}
|
|
}
|
|
|
|
# Create version.json in global .claude directory
|
|
Write-ColorOutput "Creating version.json..." $ColorInfo
|
|
Create-VersionJson -TargetClaudeDir $globalClaudeDir -InstallationMode "Global"
|
|
|
|
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
|
|
|
|
# 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)
|
|
Write-ColorOutput "Installing local folder: $folder (incremental merge)..." $ColorInfo
|
|
$folderInstalled = Merge-DirectoryContents -Source $sourceFolderPath -Destination $destFolderPath -Description "$folder folder" -BackupFolder $backupFolder
|
|
Write-ColorOutput "Installed local folder: $folder" $ColorSuccess
|
|
|
|
# Track local folder in manifest
|
|
if ($folderInstalled) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $destFolderPath -Type "Directory"
|
|
# Track files from SOURCE directory
|
|
Get-ChildItem -Path $sourceFolderPath -Recurse -File | ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($sourceFolderPath.Length)
|
|
$targetPath = $destFolderPath + $relativePath
|
|
Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File"
|
|
}
|
|
}
|
|
} else {
|
|
Write-ColorOutput "WARNING: Source folder not found: $folder" $ColorWarning
|
|
}
|
|
}
|
|
|
|
# Global components - exclude local folders
|
|
Write-ColorOutput "Installing global components to $globalClaudeDir..." $ColorInfo
|
|
|
|
# Get all items from source, excluding local folders
|
|
$sourceItems = Get-ChildItem -Path $sourceClaudeDir -Recurse -File | Where-Object {
|
|
$relativePath = $_.FullName.Substring($sourceClaudeDir.Length + 1)
|
|
$topFolder = $relativePath.Split([System.IO.Path]::DirectorySeparatorChar)[0]
|
|
$topFolder -notin $localFolders
|
|
}
|
|
|
|
$mergedCount = 0
|
|
foreach ($item in $sourceItems) {
|
|
$relativePath = $item.FullName.Substring($sourceClaudeDir.Length + 1)
|
|
$destinationPath = Join-Path $globalClaudeDir $relativePath
|
|
|
|
# Ensure destination directory exists
|
|
$destinationDir = Split-Path $destinationPath -Parent
|
|
if (-not (Test-Path $destinationDir)) {
|
|
New-Item -ItemType Directory -Path $destinationDir -Force | Out-Null
|
|
}
|
|
|
|
# Handle file merging
|
|
if (Test-Path $destinationPath) {
|
|
if ($BackupAll -and -not $NoBackup) {
|
|
if ($backupFolder) {
|
|
Backup-FileToFolder -FilePath $destinationPath -BackupFolder $backupFolder
|
|
}
|
|
Copy-Item -Path $item.FullName -Destination $destinationPath -Force
|
|
$mergedCount++
|
|
} elseif ($NoBackup) {
|
|
if (Confirm-Action "File '$relativePath' already exists in global location. Replace it? (NO BACKUP)" -DefaultYes:$false) {
|
|
Copy-Item -Path $item.FullName -Destination $destinationPath -Force
|
|
$mergedCount++
|
|
}
|
|
} elseif (Confirm-Action "File '$relativePath' already exists in global location. Replace it?" -DefaultYes:$false) {
|
|
if ($backupFolder) {
|
|
Backup-FileToFolder -FilePath $destinationPath -BackupFolder $backupFolder
|
|
}
|
|
Copy-Item -Path $item.FullName -Destination $destinationPath -Force
|
|
$mergedCount++
|
|
}
|
|
} else {
|
|
Copy-Item -Path $item.FullName -Destination $destinationPath -Force
|
|
$mergedCount++
|
|
}
|
|
}
|
|
|
|
Write-ColorOutput "Merged $mergedCount files to global location" $ColorSuccess
|
|
|
|
# Track global files in manifest
|
|
$globalClaudeFiles = Get-ChildItem -Path $globalClaudeDir -Recurse -File | Where-Object {
|
|
$relativePath = $_.FullName.Substring($globalClaudeDir.Length + 1)
|
|
$topFolder = $relativePath.Split([System.IO.Path]::DirectorySeparatorChar)[0]
|
|
$topFolder -notin $localFolders
|
|
}
|
|
foreach ($file in $globalClaudeFiles) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $file.FullName -Type "File"
|
|
}
|
|
|
|
# Handle CLAUDE.md file in global .claude directory
|
|
$globalClaudeMd = Join-Path $globalClaudeDir "CLAUDE.md"
|
|
Write-ColorOutput "Installing CLAUDE.md to global .claude directory..." $ColorInfo
|
|
$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
|
|
if ($codexInstalled) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $localCodexDir -Type "Directory"
|
|
# Track files from SOURCE directory
|
|
Get-ChildItem -Path $sourceCodexDir -Recurse -File | ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($sourceCodexDir.Length)
|
|
$targetPath = $localCodexDir + $relativePath
|
|
Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File"
|
|
}
|
|
}
|
|
|
|
# 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
|
|
if ($geminiInstalled) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $localGeminiDir -Type "Directory"
|
|
# Track files from SOURCE directory
|
|
Get-ChildItem -Path $sourceGeminiDir -Recurse -File | ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($sourceGeminiDir.Length)
|
|
$targetPath = $localGeminiDir + $relativePath
|
|
Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File"
|
|
}
|
|
}
|
|
|
|
# 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
|
|
if ($qwenInstalled) {
|
|
Add-ManifestEntry -Manifest $manifest -Path $localQwenDir -Type "Directory"
|
|
# Track files from SOURCE directory
|
|
Get-ChildItem -Path $sourceQwenDir -Recurse -File | ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($sourceQwenDir.Length)
|
|
$targetPath = $localQwenDir + $relativePath
|
|
Add-ManifestEntry -Manifest $manifest -Path $targetPath -Type "File"
|
|
}
|
|
}
|
|
|
|
# Create version.json in local .claude directory
|
|
Write-ColorOutput "Creating version.json in local directory..." $ColorInfo
|
|
Create-VersionJson -TargetClaudeDir $localClaudeDir -InstallationMode "Path"
|
|
|
|
# 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) |