refactor: remove deprecated native shell installers

- Remove installers/ directory (install.sh, install.ps1, uninstall.sh, uninstall.ps1)
- Update CloudFlare worker to 301 redirect /install* to npm docs
- Remove detectInstallationMethod() - npm is now only install method
- Simplify update-command.ts to npm-only updates
- Clean up tests to remove direct install references

BREAKING CHANGE: Native shell installers (curl/irm) no longer work.
Use `npm install -g @kaitranntt/ccs` instead.
This commit is contained in:
kaitranntt
2025-12-18 16:52:57 -05:00
parent be2b87ba98
commit 126cffc6dc
10 changed files with 51 additions and 2385 deletions
-808
View File
@@ -1,808 +0,0 @@
# CCS Installation Script (v4.5.0) - Windows PowerShell - DEPRECATED
# DEPRECATED: This installer is deprecated. Use npm instead.
# Bootstrap-based: Installs lightweight shell wrappers (LEGACY)
# Requires: Node.js 14+ (npm recommended)
# https://github.com/kaitranntt/ccs
param(
[string]$InstallDir = "$env:USERPROFILE\.ccs"
)
$ErrorActionPreference = "Stop"
# --- Deprecation Notice ---
Write-Host ""
Write-Host "=======================================================================" -ForegroundColor Yellow
Write-Host " " -ForegroundColor Yellow
Write-Host " [!] DEPRECATION NOTICE " -ForegroundColor Yellow
Write-Host " " -ForegroundColor Yellow
Write-Host " Native shell installers are deprecated and will be removed " -ForegroundColor Yellow
Write-Host " in a future version. Please use npm installation instead: " -ForegroundColor Yellow
Write-Host " " -ForegroundColor Yellow
Write-Host " npm install -g @kaitranntt/ccs " -ForegroundColor Yellow
Write-Host " " -ForegroundColor Yellow
Write-Host " Proceeding with legacy install (auto-runs npm if available)... " -ForegroundColor Yellow
Write-Host " " -ForegroundColor Yellow
Write-Host "=======================================================================" -ForegroundColor Yellow
Write-Host ""
Start-Sleep -Seconds 3
# --- Auto-redirect to npm installation ---
if (Get-Command npm -ErrorAction SilentlyContinue) {
Write-Host "[i] Node.js detected, using npm installation (recommended)..." -ForegroundColor Cyan
Write-Host ""
npm install -g "@kaitranntt/ccs"
if ($LASTEXITCODE -eq 0) {
Write-Host ""
Write-Host "[OK] CCS installed via npm successfully!" -ForegroundColor Green
Write-Host ""
Write-Host "Quick start:"
Write-Host " ccs # Use Claude (default)"
Write-Host " ccs glm # Use GLM"
Write-Host " ccs --help # Show all commands"
Write-Host ""
exit 0
} else {
Write-Host ""
Write-Host "[!] npm installation failed. Falling back to legacy install..." -ForegroundColor Yellow
Write-Host ""
Start-Sleep -Seconds 2
}
} else {
Write-Host "[!] npm not found. Falling back to legacy install..." -ForegroundColor Yellow
Write-Host "[!] Install Node.js from https://nodejs.org for the recommended method." -ForegroundColor Yellow
Write-Host ""
Start-Sleep -Seconds 2
}
# Continue with legacy PowerShell installation...
# Configuration
$CcsDir = "$env:USERPROFILE\.ccs"
$ClaudeDir = "$env:USERPROFILE\.claude"
$GlmModel = "glm-4.6"
$KimiModel = "kimi-for-coding"
# Detect if running from git repository or standalone
$ScriptDir = if ($MyInvocation.MyCommand.Path) {
Split-Path -Parent $MyInvocation.MyCommand.Path
} else {
# Running via irm | iex (in-memory, no file path)
$null
}
$InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or (Test-Path "$ScriptDir\..\lib\ccs.ps1"))) {
"git"
} else {
"standalone"
}
# Version configuration
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (irm | iex)
# For git installations, VERSION file is read if available
$CcsVersion = "6.5.0"
# Try to read VERSION file for git installations
if ($ScriptDir) {
$VersionFile = if (Test-Path "$ScriptDir\VERSION") {
"$ScriptDir\VERSION"
} elseif (Test-Path "$ScriptDir\..\VERSION") {
"$ScriptDir\..\VERSION"
} else {
$null
}
if ($VersionFile -and (Test-Path $VersionFile)) {
$CcsVersion = (Get-Content $VersionFile -Raw).Trim()
}
}
# --- Color/Format Functions ---
function Write-Critical {
param([string]$Message)
Write-Host ""
Write-Host "╔═════════════════════════════════════════════╗" -ForegroundColor Red
Write-Host "║ ACTION REQUIRED ║" -ForegroundColor Red
Write-Host "╚═════════════════════════════════════════════╝" -ForegroundColor Red
Write-Host ""
Write-Host $Message -ForegroundColor Red
Write-Host ""
}
function Write-WarningMsg {
param([string]$Message)
Write-Host ""
Write-Host "[!] WARNING" -ForegroundColor Yellow
Write-Host $Message -ForegroundColor Yellow
Write-Host ""
}
function Write-Success {
param([string]$Message)
Write-Host "[OK] $Message" -ForegroundColor Green
}
function Write-Info {
param([string]$Message)
Write-Host "[i] $Message"
}
function Write-Section {
param([string]$Title)
Write-Host ""
Write-Host "===== $Title =====" -ForegroundColor Cyan
Write-Host ""
}
# --- Node.js Detection (v4.5) ---
function Test-NodeJs {
$MIN_VERSION = 14
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
Write-WarningMsg @"
Node.js not found
CCS v4.5+ requires Node.js 14+ to run.
The bootstrap scripts will check and install the npm package on first use.
Install Node.js: https://nodejs.org (LTS recommended)
Installation will continue, but 'ccs' will not work until Node.js is installed.
"@
return $false
}
$nodeVersion = (node -v) -replace 'v', ''
$nodeMajor = [int]($nodeVersion -split '\.')[0]
if ($nodeMajor -lt $MIN_VERSION) {
Write-WarningMsg @"
Node.js 14+ required (found: $(node -v))
CCS v4.5+ requires Node.js 14 or newer.
Upgrade from: https://nodejs.org
Installation will continue, but 'ccs' may not work correctly.
"@
return $false
}
Write-Success "Node.js $(node -v) detected"
return $true
}
# Helper Functions
function Detect-CurrentProvider {
$SettingsFile = "$ClaudeDir\settings.json"
if (-not (Test-Path $SettingsFile)) {
return "unknown"
}
try {
$Content = Get-Content $SettingsFile -Raw
if ($Content -match "api\.kimi\.com|kimi-for-coding") {
return "kimi"
} elseif ($Content -match "api\.z\.ai|glm-4") {
return "glm"
} elseif ($Content -match "ANTHROPIC_BASE_URL" -and $Content -notmatch "api\.z\.ai|api\.kimi\.com") {
return "custom"
} else {
return "claude"
}
} catch {
return "unknown"
}
}
function New-GlmTemplate {
$Template = @{
env = @{
ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic"
ANTHROPIC_AUTH_TOKEN = "YOUR_GLM_API_KEY_HERE"
ANTHROPIC_MODEL = $GlmModel
ANTHROPIC_DEFAULT_OPUS_MODEL = $GlmModel
ANTHROPIC_DEFAULT_SONNET_MODEL = $GlmModel
ANTHROPIC_DEFAULT_HAIKU_MODEL = $GlmModel
}
}
return $Template | ConvertTo-Json -Depth 10
}
function New-GlmProfile {
param([string]$Provider)
$CurrentSettings = "$ClaudeDir\settings.json"
$GlmSettings = "$CcsDir\glm.settings.json"
if ($Provider -eq "glm" -and (Test-Path $CurrentSettings)) {
Write-Host "[OK] Copying current GLM config to profile..."
try {
$Config = Get-Content $CurrentSettings -Raw | ConvertFrom-Json
if (-not $Config.env) {
$Config | Add-Member -NotePropertyName env -NotePropertyValue @{} -Force
}
$Config.env | Add-Member -NotePropertyName ANTHROPIC_DEFAULT_OPUS_MODEL -NotePropertyValue $GlmModel -Force
$Config.env | Add-Member -NotePropertyName ANTHROPIC_DEFAULT_SONNET_MODEL -NotePropertyValue $GlmModel -Force
$Config.env | Add-Member -NotePropertyName ANTHROPIC_DEFAULT_HAIKU_MODEL -NotePropertyValue $GlmModel -Force
$Config | ConvertTo-Json -Depth 10 | Set-Content $GlmSettings
Write-Host " Created: $GlmSettings with your existing API key + enhanced settings"
} catch {
Write-Host " [i] Copying current settings failed, using template"
New-GlmTemplate | Set-Content $GlmSettings
}
} else {
Write-Host "Creating GLM profile template at $GlmSettings"
New-GlmTemplate | Set-Content $GlmSettings
Write-Host " Created: $GlmSettings"
Write-Host " [!] Edit this file and replace YOUR_GLM_API_KEY_HERE with your actual GLM API key"
}
}
function New-KimiTemplate {
$Template = @{
env = @{
ANTHROPIC_BASE_URL = "https://api.kimi.com/coding/"
ANTHROPIC_AUTH_TOKEN = "YOUR_KIMI_API_KEY_HERE"
ANTHROPIC_MODEL = $KimiModel
ANTHROPIC_SMALL_FAST_MODEL = $KimiModel
ANTHROPIC_DEFAULT_OPUS_MODEL = $KimiModel
ANTHROPIC_DEFAULT_SONNET_MODEL = $KimiModel
ANTHROPIC_DEFAULT_HAIKU_MODEL = $KimiModel
}
alwaysThinkingEnabled = $true
}
return $Template | ConvertTo-Json -Depth 10
}
function New-KimiProfile {
param([string]$Provider)
$CurrentSettings = "$ClaudeDir\settings.json"
$KimiSettings = "$CcsDir\kimi.settings.json"
if ($Provider -eq "kimi" -and (Test-Path $CurrentSettings)) {
Write-Host "[OK] Copying current Kimi config to profile..."
try {
$Config = Get-Content $CurrentSettings -Raw | ConvertFrom-Json
if (-not $Config.env) {
$Config | Add-Member -NotePropertyName env -NotePropertyValue @{} -Force
}
$Config.env | Add-Member -NotePropertyName ANTHROPIC_SMALL_FAST_MODEL -NotePropertyValue $KimiModel -Force
$Config.env | Add-Member -NotePropertyName ANTHROPIC_DEFAULT_OPUS_MODEL -NotePropertyValue $KimiModel -Force
$Config.env | Add-Member -NotePropertyName ANTHROPIC_DEFAULT_SONNET_MODEL -NotePropertyValue $KimiModel -Force
$Config.env | Add-Member -NotePropertyName ANTHROPIC_DEFAULT_HAIKU_MODEL -NotePropertyValue $KimiModel -Force
$Config | Add-Member -NotePropertyName alwaysThinkingEnabled -NotePropertyValue $true -Force
$Config | ConvertTo-Json -Depth 10 | Set-Content $KimiSettings
Write-Host " Created: $KimiSettings with your existing API key + enhanced settings"
} catch {
Write-Host " [i] Copying current settings failed, using template"
New-KimiTemplate | Set-Content $KimiSettings
}
} else {
Write-Host "Creating Kimi profile template at $KimiSettings"
New-KimiTemplate | Set-Content $KimiSettings
Write-Host " Created: $KimiSettings"
Write-Host " [!] Edit this file and replace YOUR_KIMI_API_KEY_HERE with your actual Kimi API key"
}
}
function Install-ClaudeFolder {
param(
[string]$SourceDir
)
$TargetDir = "$CcsDir\.claude"
# Check if already exists
if (Test-Path $TargetDir) {
Write-Host "| [i] .claude/ folder already exists, skipping"
return $true
}
# Create directory structure
$null = New-Item -ItemType Directory -Force -Path "$TargetDir\commands"
$null = New-Item -ItemType Directory -Force -Path "$TargetDir\skills\ccs-delegation\references"
if ($InstallMethod -eq "git" -and $SourceDir) {
# Copy from local git repo
$SourceClaudeDir = Join-Path $SourceDir ".claude"
if (Test-Path $SourceClaudeDir) {
try {
Copy-Item -Path "$SourceClaudeDir\*" -Destination $TargetDir -Recurse -Force
Write-Host "| [OK] Installed .claude/ folder"
return $true
} catch {
Write-Host "| [!] Failed to copy .claude/ folder"
return $false
}
} else {
Write-Host "| [!] .claude/ folder not found in source"
return $false
}
} else {
# Standalone: download from GitHub
try {
$BaseUrl = "https://raw.githubusercontent.com/kaitranntt/ccs/main/.claude"
Invoke-WebRequest -Uri "$BaseUrl/commands/ccs.md" `
-OutFile "$TargetDir\commands\ccs.md" -UseBasicParsing
Invoke-WebRequest -Uri "$BaseUrl/skills/ccs-delegation/SKILL.md" `
-OutFile "$TargetDir\skills\ccs-delegation\SKILL.md" -UseBasicParsing
Invoke-WebRequest -Uri "$BaseUrl/skills/ccs-delegation/references/delegation-patterns.md" `
-OutFile "$TargetDir\skills\ccs-delegation\references\delegation-patterns.md" -UseBasicParsing
Write-Host "| [OK] Downloaded .claude/ folder"
return $true
} catch {
Write-Host "| [!] Failed to download .claude/ folder"
return $false
}
}
}
# Main Installation
# Check Node.js requirement (warn if missing, continue anyway)
$null = Test-NodeJs
Write-Host '===== Installing CCS (Windows) ====='
# Create directories
New-Item -ItemType Directory -Force -Path $CcsDir | Out-Null
# Install main executable
if ($InstallMethod -eq "standalone") {
# Standalone install - download from GitHub
Write-Host "| Downloading CCS from GitHub..."
try {
$BaseUrl = "https://raw.githubusercontent.com/kaitranntt/ccs/main"
Invoke-WebRequest -Uri "$BaseUrl/lib/ccs.ps1" -OutFile "$CcsDir\ccs.ps1" -UseBasicParsing
Write-Host "| [OK] Downloaded ccs.ps1"
# Note: Shell dependencies (error-codes.ps1, progress-indicator.ps1, prompt.ps1) no longer needed
# Bootstrap delegates all functionality to Node.js via npx
# Download shell completion files
$CompletionsDir = "$CcsDir\completions"
if (-not (Test-Path $CompletionsDir)) {
New-Item -ItemType Directory -Path $CompletionsDir -Force | Out-Null
}
try {
Invoke-WebRequest -Uri "$BaseUrl/scripts/completion/ccs.ps1" -OutFile "$CompletionsDir\ccs.ps1" -UseBasicParsing
Write-Host "| [OK] Downloaded completion files"
} catch {
Write-Host "| [!] Warning: Failed to download completion files"
}
} catch {
Write-Host "|"
Write-Host "[X] Error: Failed to download ccs.ps1 from GitHub" -ForegroundColor Red
Write-Host " $_"
return
}
} else {
# Git install - copy local file
$CcsPs1Path = if (Test-Path "$ScriptDir\lib\ccs.ps1") {
"$ScriptDir\lib\ccs.ps1"
} elseif (Test-Path "$ScriptDir\..\lib\ccs.ps1") {
"$ScriptDir\..\lib\ccs.ps1"
} else {
throw "lib\ccs.ps1 not found"
}
Copy-Item $CcsPs1Path "$CcsDir\ccs.ps1" -Force
Write-Host "| [OK] Installed ccs.ps1"
# Note: Shell dependencies (error-codes.ps1, progress-indicator.ps1, prompt.ps1) no longer needed
# Bootstrap delegates all functionality to Node.js via npx
# Copy shell completion files
$CompletionsDir = "$CcsDir\completions"
if (-not (Test-Path $CompletionsDir)) {
New-Item -ItemType Directory -Path $CompletionsDir -Force | Out-Null
}
$SourceCompletionDir = if (Test-Path "$ScriptDir\scripts\completion") {
"$ScriptDir\scripts\completion"
} elseif (Test-Path "$ScriptDir\..\scripts\completion") {
"$ScriptDir\..\scripts\completion"
} else {
$null
}
if ($SourceCompletionDir -and (Test-Path "$SourceCompletionDir\ccs.ps1")) {
Copy-Item "$SourceCompletionDir\ccs.ps1" "$CompletionsDir\ccs.ps1" -Force -ErrorAction SilentlyContinue
Write-Host "| [OK] Copied completion files"
}
}
# Install uninstall script as ccs-uninstall.ps1
if ($ScriptDir -and (Test-Path "$ScriptDir\uninstall.ps1")) {
# Copy uninstall.ps1 as ccs-uninstall.ps1 (similar to Linux symlink approach)
if ($ScriptDir -ne $CcsDir) {
Copy-Item "$ScriptDir\uninstall.ps1" "$CcsDir\ccs-uninstall.ps1" -Force
}
# Clean up old uninstall.ps1 from previous installations
if (Test-Path "$CcsDir\uninstall.ps1") {
Remove-Item "$CcsDir\uninstall.ps1" -Force -ErrorAction SilentlyContinue
}
Write-Host "| [OK] Installed uninstaller"
} elseif ($InstallMethod -eq "standalone") {
try {
$BaseUrl = "https://raw.githubusercontent.com/kaitranntt/ccs/main"
# Download uninstall.ps1 as ccs-uninstall.ps1
Invoke-WebRequest -Uri "$BaseUrl/installers/uninstall.ps1" -OutFile "$CcsDir\ccs-uninstall.ps1" -UseBasicParsing
# Clean up old uninstall.ps1 from previous installations
if (Test-Path "$CcsDir\uninstall.ps1") {
Remove-Item "$CcsDir\uninstall.ps1" -Force -ErrorAction SilentlyContinue
}
Write-Host "| [OK] Installed uninstaller"
} catch {
Write-Host "| [!] Could not download uninstaller (optional)"
}
}
Write-Host "| [OK] Created directories"
# Install .claude/ folder
if ($InstallMethod -eq "git" -and $ScriptDir) {
$ParentDir = Split-Path -Parent $ScriptDir
$null = Install-ClaudeFolder -SourceDir $ParentDir
} else {
$null = Install-ClaudeFolder -SourceDir ""
}
Write-Host "========================================="
Write-Host ""
# Profile Setup
$CurrentProvider = Detect-CurrentProvider
$ProviderLabel = switch ($CurrentProvider) {
"glm" { ' (detected: GLM)' }
"kimi" { ' (detected: Kimi)' }
"claude" { ' (detected: Claude)' }
"custom" { ' (detected: custom)' }
default { "" }
}
Write-Host "===== Configuring Profiles v$CcsVersion$ProviderLabel"
# Backup existing config (single backup, no timestamp)
$ConfigFile = "$CcsDir\config.json"
$BackupFile = "$CcsDir\config.json.backup"
if (Test-Path $ConfigFile) {
Copy-Item $ConfigFile $BackupFile -Force
}
$NeedsGlmKey = $false
$GlmSettings = "$CcsDir\glm.settings.json"
# Create GLM profile if missing
if (-not (Test-Path $GlmSettings)) {
New-GlmProfile -Provider $CurrentProvider
if ($CurrentProvider -ne "glm") {
$NeedsGlmKey = $true
}
} else {
Write-Host '| [OK] GLM profile exists'
}
$NeedsKimiKey = $false
$KimiSettings = "$CcsDir\kimi.settings.json"
# Create Kimi profile if missing
if (-not (Test-Path $KimiSettings)) {
New-KimiProfile -Provider $CurrentProvider
if ($CurrentProvider -ne "kimi") {
$NeedsKimiKey = $true
}
} else {
Write-Host '| [OK] Kimi profile exists'
}
# Create config if missing
if (-not (Test-Path $ConfigFile)) {
$ConfigContent = @{
profiles = @{
glm = "~/.ccs/glm.settings.json"
kimi = "~/.ccs/kimi.settings.json"
default = "~/.claude/settings.json"
}
}
$ConfigContent | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile
Write-Host ('| OK: Config created at {0}\.ccs\config.json' -f $env:USERPROFILE)
}
# Validate config JSON
if (Test-Path $ConfigFile) {
try {
$null = Get-Content $ConfigFile -Raw | ConvertFrom-Json
} catch {
Write-Host '| [!] Warning: Invalid JSON in config.json' -ForegroundColor Yellow
if (Test-Path $BackupFile) {
Write-Host ('| Restore from: {0}' -f $BackupFile)
}
}
}
# Validate GLM settings JSON
if (Test-Path $GlmSettings) {
try {
$null = Get-Content $GlmSettings -Raw | ConvertFrom-Json
} catch {
Write-Host '| [!] Warning: Invalid JSON in glm.settings.json' -ForegroundColor Yellow
}
}
Write-Host "========================================="
Write-Host ""
# Detect circular symlink
function Test-CircularSymlink {
param(
[string]$Target,
[string]$LinkPath
)
# Check if target exists and is symlink
if (-not (Test-Path $Target)) {
return $false
}
try {
$Item = Get-Item $Target -ErrorAction Stop
if ($Item.LinkType -ne "SymbolicLink") {
return $false
}
# Resolve target's link
$TargetLink = $Item.Target
$SharedDir = "$env:USERPROFILE\.ccs\shared"
# Check if target points back to our shared dir
if ($TargetLink -like "$SharedDir*" -or $TargetLink -eq $LinkPath) {
Write-Host "[!] Circular symlink detected: $Target$TargetLink"
return $true
}
} catch {
return $false
}
return $false
}
# Setup shared directories as symlinks to ~/.claude/ (v3.2.0)
function Initialize-SharedSymlinks {
$SharedDir = "$CcsDir\shared"
$ClaudeDir = "$env:USERPROFILE\.claude"
# Create ~/.claude/ if missing
if (-not (Test-Path $ClaudeDir)) {
Write-Host "[i] Creating ~/.claude/ directory structure"
New-Item -ItemType Directory -Path $ClaudeDir -Force | Out-Null
@('commands', 'skills', 'agents') | ForEach-Object {
New-Item -ItemType Directory -Path "$ClaudeDir\$_" -Force | Out-Null
}
}
# Create shared directory
if (-not (Test-Path $SharedDir)) {
New-Item -ItemType Directory -Path $SharedDir -Force | Out-Null
}
# Create symlinks ~/.ccs/shared/* → ~/.claude/*
foreach ($Dir in @('commands', 'skills', 'agents')) {
$ClaudePath = "$ClaudeDir\$Dir"
$SharedPath = "$SharedDir\$Dir"
# Create directory in ~/.claude/ if missing
if (-not (Test-Path $ClaudePath)) {
New-Item -ItemType Directory -Path $ClaudePath -Force | Out-Null
}
# Check for circular symlink
if (Test-CircularSymlink -Target $ClaudePath -LinkPath $SharedPath) {
Write-Host "[!] Skipping $Dir`: circular symlink detected"
continue
}
# If already correct symlink, skip
if (Test-Path $SharedPath) {
try {
$Item = Get-Item $SharedPath -ErrorAction Stop
if ($Item.LinkType -eq "SymbolicLink") {
$CurrentTarget = $Item.Target
if ($CurrentTarget -eq $ClaudePath) {
continue # Already correct
}
}
# Backup existing data before replacing
if ((Get-ChildItem $SharedPath -ErrorAction SilentlyContinue).Count -gt 0) {
Write-Host "[i] Migrating existing $Dir to ~/.claude/$Dir"
Get-ChildItem $SharedPath -ErrorAction SilentlyContinue | ForEach-Object {
$DestPath = Join-Path $ClaudePath $_.Name
if (-not (Test-Path $DestPath)) {
Copy-Item $_.FullName $DestPath -Recurse -ErrorAction SilentlyContinue
}
}
}
Remove-Item $SharedPath -Recurse -Force -ErrorAction SilentlyContinue
} catch {
# Continue to recreate
}
}
# Create symlink (requires Developer Mode or admin)
try {
New-Item -ItemType SymbolicLink -Path $SharedPath -Target $ClaudePath -Force -ErrorAction Stop | Out-Null
} catch {
Write-Host "[!] Symlink failed for $Dir, copying instead (enable Developer Mode)"
if (-not (Test-Path $SharedPath)) {
New-Item -ItemType Directory -Path $SharedPath -Force | Out-Null
}
if (Test-Path $ClaudePath) {
Copy-Item "$ClaudePath\*" $SharedPath -Recurse -ErrorAction SilentlyContinue
}
}
}
}
Write-Host "[i] Setting up shared directories..."
Initialize-SharedSymlinks
Write-Host ""
# Install CCS items to ~/.claude/ via symlinks (v4.1.0)
Write-Host "[i] Installing CCS items to ~/.claude/..."
if (Get-Command node -ErrorAction SilentlyContinue) {
# Check if .claude/ was successfully installed
if (Test-Path "$CcsDir\.claude") {
# Download or copy claude-symlink-manager.js
$UtilsDir = "$CcsDir\bin\utils"
if (-not (Test-Path $UtilsDir)) {
New-Item -ItemType Directory -Path $UtilsDir -Force | Out-Null
}
if ($InstallMethod -eq "git" -and $ScriptDir) {
# Git install - copy from local repo
$SourcePath = $null
if (Test-Path "$ScriptDir\..\bin\utils\claude-symlink-manager.js") {
$SourcePath = "$ScriptDir\..\bin\utils\claude-symlink-manager.js"
} elseif (Test-Path "$ScriptDir\bin\utils\claude-symlink-manager.js") {
$SourcePath = "$ScriptDir\bin\utils\claude-symlink-manager.js"
}
if ($SourcePath) {
Copy-Item $SourcePath "$UtilsDir\claude-symlink-manager.js" -Force
}
} else {
# Standalone install - download from GitHub
try {
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/kaitranntt/ccs/main/bin/utils/claude-symlink-manager.js" `
-OutFile "$UtilsDir\claude-symlink-manager.js" -UseBasicParsing
} catch {
Write-Host "[!] Failed to download claude-symlink-manager.js"
}
}
# Call ClaudeSymlinkManager if available
if (Test-Path "$UtilsDir\claude-symlink-manager.js") {
try {
$scriptBlock = @"
try {
const ClaudeSymlinkManager = require('$($UtilsDir -replace '\\', '/')/claude-symlink-manager.js');
const manager = new ClaudeSymlinkManager();
manager.install();
} catch (err) {
console.log('[!] CCS item installation warning: ' + err.message);
console.log(' Run "ccs sync" to retry');
}
"@
node -e $scriptBlock 2>$null
if (-not $?) {
Write-Host "[!] CCS item installation skipped (run 'ccs sync' later)"
}
} catch {
Write-Host "[!] CCS item installation failed: $($_.Exception.Message)"
Write-Host " Run 'ccs sync' after installation to complete setup"
}
} else {
Write-Host "[!] claude-symlink-manager.js not found, skipping"
Write-Host " Run 'ccs sync' after installation to complete setup"
}
} else {
Write-Host "[!] .claude/ folder not found, skipping CCS item installation"
}
} else {
Write-Host "[!] Node.js not found, skipping CCS item installation"
Write-Host " Install Node.js and run 'ccs sync' to complete setup"
}
Write-Host ""
Write-Host "[i] Note: Windows symlink support requires Developer Mode (v4.2 will add fallback)"
Write-Host ""
# Check and update PATH
$UserPath = [Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
if ($UserPath -notlike "*$CcsDir*") {
Write-Host "[!] PATH Configuration Required"
Write-Host ""
Write-Host " Adding $CcsDir to your PATH..."
try {
$NewPath = if ($UserPath) { "$UserPath;$CcsDir" } else { $CcsDir }
[Environment]::SetEnvironmentVariable("Path", $NewPath, [System.EnvironmentVariableTarget]::User)
Write-Host " [OK] PATH updated. Restart your terminal for changes to take effect."
Write-Host ""
} catch {
Write-Host " [X] Could not update PATH automatically." -ForegroundColor Yellow
Write-Host " Please add manually: $CcsDir"
Write-Host ""
}
}
# Show API key warning if needed
if ($NeedsGlmKey) {
Write-Critical @"
Configure GLM API Key:
1. Get API key from: https://api.z.ai
2. Edit: $env:USERPROFILE\.ccs\glm.settings.json
3. Replace: YOUR_GLM_API_KEY_HERE
With your actual API key
4. Test: ccs glm --version
"@
}
# Show API key warning for Kimi if needed
if ($NeedsKimiKey) {
Write-Critical @"
Configure Kimi API Key:
1. Get API key from: https://www.kimi.com/coding
2. Edit: $env:USERPROFILE\.ccs\kimi.settings.json
3. Replace: YOUR_KIMI_API_KEY_HERE
With your actual API key
4. Test: ccs kimi --version
"@
}
Write-Success "CCS installed successfully!"
Write-Host ""
Write-Host " Installed components:"
Write-Host " * ccs command -> $CcsDir\ccs.ps1"
Write-Host " * config -> $CcsDir\config.json"
Write-Host " * glm profile -> $CcsDir\glm.settings.json"
Write-Host " * kimi profile -> $CcsDir\kimi.settings.json"
Write-Host " * .claude/ folder -> $CcsDir\.claude\"
Write-Host ""
Write-Host " Requirements:"
$nodeVer = if (Get-Command node -ErrorAction SilentlyContinue) { node -v } else { "NOT FOUND" }
Write-Host " * Node.js 14+ (detected: $nodeVer)"
Write-Host " * npm 5.2+ (for npx, comes with Node.js 8.2+)"
Write-Host ""
Write-Host " First Run:"
Write-Host " The first time you run 'ccs', it will automatically install"
Write-Host " the @kaitranntt/ccs npm package globally via npx."
Write-Host ""
Write-Host " Quick start:"
Write-Host " ccs # Use Claude subscription (default)"
Write-Host " ccs glm # Use GLM fallback"
Write-Host " ccs kimi # Use Kimi for Coding"
Write-Host ""
Write-Host " To uninstall: ccs-uninstall"
Write-Host ""
-927
View File
@@ -1,927 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# ============================================================================
# CCS Installation Script (v4.5.0) - DEPRECATED
# DEPRECATED: This installer is deprecated. Use npm instead.
# Bootstrap-based: Installs lightweight shell wrappers (LEGACY)
# Requires: Node.js 14+ (npm recommended)
# ============================================================================
# --- Deprecation Notice ---
echo ""
echo "======================================================================="
echo " "
echo " [!] DEPRECATION NOTICE "
echo " "
echo " Native shell installers are deprecated and will be removed "
echo " in a future version. Please use npm installation instead: "
echo " "
echo " npm install -g @kaitranntt/ccs "
echo " "
echo " Proceeding with legacy install (auto-runs npm if available)... "
echo " "
echo "======================================================================="
echo ""
sleep 3 # Give users time to read
# --- Auto-redirect to npm installation ---
if command -v npm &> /dev/null; then
echo "[i] Node.js detected, using npm installation (recommended)..."
echo ""
npm install -g @kaitranntt/ccs
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo ""
echo "[OK] CCS installed via npm successfully!"
echo ""
echo "Quick start:"
echo " ccs # Use Claude (default)"
echo " ccs glm # Use GLM"
echo " ccs --help # Show all commands"
echo ""
exit 0
else
echo ""
echo "[!] npm installation failed. Falling back to legacy install..."
echo ""
sleep 2
fi
else
echo "[!] npm not found. Falling back to legacy install..."
echo "[!] Install Node.js from https://nodejs.org for the recommended method."
echo ""
sleep 2
fi
# Continue with legacy bash installation...
# --- Configuration ---
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
CCS_DIR="$HOME/.ccs"
CLAUDE_DIR="$HOME/.claude"
GLM_MODEL="glm-4.6"
KIMI_MODEL="kimi-k2-thinking-turbo"
# Resolve script directory (handles both file-based and piped execution)
if [[ -n "${BASH_SOURCE[0]:-}" ]]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
else
SCRIPT_DIR="$(cd "$(dirname "${0:-$PWD}")" && pwd)"
fi
# Detect installation method (git vs standalone)
# Check if ccs executable exists in SCRIPT_DIR or parent (real git install)
# Don't just check .git (user might run curl | bash inside their own git repo)
if [[ -f "$SCRIPT_DIR/lib/ccs" ]] || [[ -f "$SCRIPT_DIR/../lib/ccs" ]]; then
INSTALL_METHOD="git"
else
INSTALL_METHOD="standalone"
fi
# Version configuration
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (curl | bash)
# For git installations, VERSION file is read if available
CCS_VERSION="6.5.0"
# Try to read VERSION file for git installations
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
CCS_VERSION="$(cat "$SCRIPT_DIR/VERSION" | tr -d '\n' | tr -d '\r')"
elif [[ -f "$SCRIPT_DIR/../VERSION" ]]; then
CCS_VERSION="$(cat "$SCRIPT_DIR/../VERSION" | tr -d '\n' | tr -d '\r')"
fi
# --- Platform Detection ---
# Detect platform and redirect to Windows installer if needed
detect_platform() {
case "$OSTYPE" in
msys*|mingw*|cygwin*|win32*)
echo "windows"
;;
*)
echo "unix"
;;
esac
}
PLATFORM=$(detect_platform)
if [[ "$PLATFORM" == "windows" ]]; then
echo "Windows detected. Using PowerShell installer..."
if [[ -f "$SCRIPT_DIR/install.ps1" ]]; then
powershell.exe -ExecutionPolicy Bypass -File "$SCRIPT_DIR/install.ps1"
exit $?
else
echo "Error: install.ps1 not found."
echo "Please download the full CCS package from:"
echo " https://github.com/kaitranntt/ccs"
exit 1
fi
fi
# Continue with Unix installation...
# --- Helper Functions ---
detect_current_provider() {
local settings="$CLAUDE_DIR/settings.json"
if [[ ! -f "$settings" ]]; then
echo "unknown"
return
fi
if grep -q "api.kimi.com\|kimi-for-coding" "$settings" 2>/dev/null; then
echo "kimi"
elif grep -q "api.z.ai\|glm-4" "$settings" 2>/dev/null; then
echo "glm"
elif grep -q "ANTHROPIC_BASE_URL" "$settings" 2>/dev/null && ! grep -q "api.z.ai\|api.kimi.com" "$settings" 2>/dev/null; then
echo "custom"
else
echo "claude"
fi
}
# --- Color/Format Functions (ANSI) ---
setup_colors() {
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED='' GREEN='' YELLOW='' CYAN='' BOLD='' RESET=''
fi
}
msg_critical() {
echo "" >&2
echo -e "${RED}${BOLD}╔═════════════════════════════════════════════╗${RESET}" >&2
echo -e "${RED}${BOLD}║ ACTION REQUIRED ║${RESET}" >&2
echo -e "${RED}${BOLD}╚═════════════════════════════════════════════╝${RESET}" >&2
echo "" >&2
echo -e "${RED}$1${RESET}" >&2
echo "" >&2
}
msg_warning() {
echo "" >&2
echo -e "${YELLOW}${BOLD}[!] WARNING${RESET}" >&2
echo -e "${YELLOW}$1${RESET}" >&2
echo "" >&2
}
msg_success() {
echo -e "${GREEN}[OK] $1${RESET}"
}
msg_info() {
echo -e "[i] $1"
}
msg_section() {
echo ""
echo -e "${BOLD}===== $1 =====${RESET}"
echo ""
}
setup_colors
# --- Node.js Detection (v4.5) ---
check_nodejs() {
if ! command -v node &> /dev/null; then
msg_warning "Node.js not found
CCS v4.5+ requires Node.js 14+ to run.
The bootstrap scripts will check and install the npm package on first use.
Install Node.js: https://nodejs.org (LTS recommended)
Installation will continue, but 'ccs' will not work until Node.js is installed."
return 1
fi
local node_major
node_major=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [[ $node_major -lt 14 ]]; then
msg_warning "Node.js 14+ required (found: $(node -v))
CCS v4.5+ requires Node.js 14 or newer.
Upgrade from: https://nodejs.org
Installation will continue, but 'ccs' may not work correctly."
return 1
fi
msg_success "Node.js $(node -v) detected"
return 0
}
# --- Shell Profile Management ---
detect_shell_profile() {
# Safe extraction of shell name (no command substitution)
local shell_path="${SHELL:-/bin/bash}"
local shell_name="${shell_path##*/}"
# Validate shell_name is alphanumeric (defense in depth)
if [[ ! "$shell_name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
shell_name="bash"
fi
case "$shell_name" in
zsh)
echo "$HOME/.zshrc"
;;
bash)
if [[ "$OSTYPE" == darwin* ]]; then
# macOS prefers bash_profile
[[ -f "$HOME/.bash_profile" ]] && echo "$HOME/.bash_profile" || echo "$HOME/.bashrc"
else
echo "$HOME/.bashrc"
fi
;;
fish)
echo "$HOME/.config/fish/config.fish"
;;
*)
# Default to bashrc
echo "$HOME/.bashrc"
;;
esac
}
check_path_configured() {
[[ ":$PATH:" == *":$HOME/.local/bin:"* ]]
}
add_to_path() {
local profile_file="$1"
local dir_to_add="$HOME/.local/bin"
# Create profile file if doesn't exist
if [[ ! -f "$profile_file" ]]; then
local profile_dir="$(dirname "$profile_file")"
if ! mkdir -p "$profile_dir" 2>/dev/null; then
echo "[!] Failed to create directory: $profile_dir" >&2
return 1
fi
if ! touch "$profile_file" 2>/dev/null; then
echo "[!] Failed to create profile file: $profile_file" >&2
return 1
fi
fi
# Check if already in profile (avoid duplicates)
if grep -q "# CCS: Added by Claude Code Switch installer" "$profile_file" 2>/dev/null; then
return 0 # Already added
fi
# Check for fish shell (different syntax)
if [[ "$profile_file" == *"config.fish" ]]; then
cat >> "$profile_file" << 'EOF'
# CCS: Added by Claude Code Switch installer
set -gx PATH $HOME/.local/bin $PATH
EOF
else
# Bash/Zsh syntax
cat >> "$profile_file" << 'EOF'
# CCS: Added by Claude Code Switch installer
export PATH="$HOME/.local/bin:$PATH"
EOF
fi
return 0
}
configure_shell_path() {
if check_path_configured; then
msg_info "PATH already configured for ~/.local/bin"
return 0
fi
local profile_file=$(detect_shell_profile)
echo ""
msg_section "Configuring Shell PATH"
msg_info "Detected shell profile: $profile_file"
if add_to_path "$profile_file"; then
msg_success "Added ~/.local/bin to PATH in $profile_file"
echo ""
# Show reload instructions
msg_critical "Reload your shell to use 'ccs' command:
Option 1 (current session):
source $profile_file
Option 2 (new session):
Open a new terminal window
Then verify:
ccs --version"
return 0
else
msg_warning "Could not auto-configure PATH
Manually add this line to $profile_file:
export PATH=\"\$HOME/.local/bin:\$PATH\"
Then reload:
source $profile_file"
return 1
fi
}
create_glm_template() {
cat << EOF
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"ANTHROPIC_AUTH_TOKEN": "YOUR_GLM_API_KEY_HERE",
"ANTHROPIC_MODEL": "$GLM_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "$GLM_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "$GLM_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "$GLM_MODEL"
}
}
EOF
}
create_kimi_template() {
cat << EOF
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.kimi.com/coding/",
"ANTHROPIC_AUTH_TOKEN": "YOUR_KIMI_API_KEY_HERE",
"ANTHROPIC_MODEL": "$KIMI_MODEL",
"ANTHROPIC_SMALL_FAST_MODEL": "$KIMI_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "$KIMI_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "$KIMI_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "$KIMI_MODEL"
},
"alwaysThinkingEnabled": true
}
EOF
}
atomic_mv() {
local src="$1"
local dest="$2"
if mv "$src" "$dest" 2>/dev/null; then
return 0
else
rm -f "$src"
echo " [X] Error: Failed to create $dest (check permissions)"
exit 1
fi
}
download_file() {
local url="$1"
local dest="$2"
if ! curl -fsSL "$url" -o "$dest"; then
echo " [!] Failed to download: $(basename "$dest")"
return 1
fi
return 0
}
install_claude_folder() {
local source_dir="$1"
local target_dir="$CCS_DIR/.claude"
# Check if already exists
if [[ -d "$target_dir" ]]; then
echo "| [i] .claude/ folder already exists, skipping"
return 0
fi
mkdir -p "$target_dir/commands" "$target_dir/skills/ccs-delegation/references"
if [[ "$INSTALL_METHOD" == "git" ]]; then
# Copy from local git repo
if [[ -d "$source_dir/.claude" ]]; then
cp -r "$source_dir/.claude"/* "$target_dir/" 2>/dev/null || {
echo "| [!] Failed to copy .claude/ folder"
return 1
}
echo "| [OK] Installed .claude/ folder"
else
echo "| [!] .claude/ folder not found in source"
return 1
fi
else
# Standalone: download from GitHub
local base_url="https://raw.githubusercontent.com/kaitranntt/ccs/main/.claude"
download_file "$base_url/commands/ccs.md" "$target_dir/commands/ccs.md" || return 1
download_file "$base_url/skills/ccs-delegation/SKILL.md" "$target_dir/skills/ccs-delegation/SKILL.md" || return 1
download_file "$base_url/skills/ccs-delegation/references/delegation-patterns.md" "$target_dir/skills/ccs-delegation/references/delegation-patterns.md" || return 1
echo "| [OK] Downloaded .claude/ folder"
fi
return 0
}
create_glm_profile() {
local current_settings="$CLAUDE_DIR/settings.json"
local glm_settings="$CCS_DIR/glm.settings.json"
local provider="$1"
if [[ "$provider" == "glm" ]]; then
echo "[OK] Copying current GLM config to profile..."
if command -v jq &> /dev/null; then
if jq '.env |= (. // {}) + {
"ANTHROPIC_DEFAULT_OPUS_MODEL": "'"$GLM_MODEL"'",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "'"$GLM_MODEL"'",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "'"$GLM_MODEL"'"
}' "$current_settings" > "$glm_settings.tmp" 2>/dev/null; then
atomic_mv "$glm_settings.tmp" "$glm_settings"
echo " Created: $glm_settings (with your existing API key + enhanced settings)"
else
rm -f "$glm_settings.tmp"
cp "$current_settings" "$glm_settings"
echo " Created: $glm_settings (copied as-is, jq enhancement failed)"
fi
else
cp "$current_settings" "$glm_settings"
echo " Created: $glm_settings (copied as-is, jq not available)"
fi
else
echo "Creating GLM profile template at $glm_settings"
if [[ -f "$current_settings" ]] && command -v jq &> /dev/null; then
if jq '.env |= (. // {}) + {
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"ANTHROPIC_AUTH_TOKEN": "YOUR_GLM_API_KEY_HERE",
"ANTHROPIC_MODEL": "'"$GLM_MODEL"'",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "'"$GLM_MODEL"'",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "'"$GLM_MODEL"'",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "'"$GLM_MODEL"'"
}' "$current_settings" > "$glm_settings.tmp" 2>/dev/null; then
atomic_mv "$glm_settings.tmp" "$glm_settings"
else
rm -f "$glm_settings.tmp"
echo " [i] jq failed, using basic template"
create_glm_template > "$glm_settings"
fi
else
create_glm_template > "$glm_settings"
fi
echo " Created: $glm_settings"
echo " [!] Edit this file and replace YOUR_GLM_API_KEY_HERE with your actual GLM API key"
fi
}
create_kimi_profile() {
local current_settings="$CLAUDE_DIR/settings.json"
local kimi_settings="$CCS_DIR/kimi.settings.json"
local provider="$1"
if [[ "$provider" == "kimi" ]]; then
echo "[OK] Copying current Kimi config to profile..."
if command -v jq &> /dev/null; then
if jq '.env |= (. // {}) + {
"ANTHROPIC_SMALL_FAST_MODEL": "'"$KIMI_MODEL"'",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "'"$KIMI_MODEL"'",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "'"$KIMI_MODEL"'",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "'"$KIMI_MODEL"'"
}' "$current_settings" > "$kimi_settings.tmp" 2>/dev/null; then
atomic_mv "$kimi_settings.tmp" "$kimi_settings"
echo " Created: $kimi_settings (with your existing API key + enhanced settings)"
else
rm -f "$kimi_settings.tmp"
cp "$current_settings" "$kimi_settings"
echo " Created: $kimi_settings (copied as-is, jq enhancement failed)"
fi
else
cp "$current_settings" "$kimi_settings"
echo " Created: $kimi_settings (copied as-is, jq not available)"
fi
else
echo "Creating Kimi profile template at $kimi_settings"
if [[ -f "$current_settings" ]] && command -v jq &> /dev/null; then
if jq '.env |= (. // {}) + {
"ANTHROPIC_BASE_URL": "https://api.kimi.com/coding/",
"ANTHROPIC_AUTH_TOKEN": "YOUR_KIMI_API_KEY_HERE",
"ANTHROPIC_MODEL": "'"$KIMI_MODEL"'",
"ANTHROPIC_SMALL_FAST_MODEL": "'"$KIMI_MODEL"'",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "'"$KIMI_MODEL"'",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "'"$KIMI_MODEL"'",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "'"$KIMI_MODEL"'"
} | . + {"alwaysThinkingEnabled": true}' "$current_settings" > "$kimi_settings.tmp" 2>/dev/null; then
atomic_mv "$kimi_settings.tmp" "$kimi_settings"
else
rm -f "$kimi_settings.tmp"
echo " [i] jq failed, using basic template"
create_kimi_template > "$kimi_settings"
fi
else
create_kimi_template > "$kimi_settings"
fi
echo " Created: $kimi_settings"
echo " [!] Edit this file and replace YOUR_KIMI_API_KEY_HERE with your actual Kimi API key"
fi
}
# --- Main Installation ---
# Check Node.js requirement (warn if missing, continue anyway)
check_nodejs || true
echo "┌─ Installing CCS"
# Create directories
mkdir -p "$INSTALL_DIR" "$CCS_DIR"
# Install main executable
if [[ "$INSTALL_METHOD" == "standalone" ]]; then
# Standalone install - download ccs from GitHub
if ! command -v curl &> /dev/null; then
echo "[X] Error: curl is required for standalone installation"
exit 1
fi
BASE_URL="https://raw.githubusercontent.com/kaitranntt/ccs/main"
# Download main executable
if curl -fsSL "$BASE_URL/lib/ccs" -o "$CCS_DIR/ccs"; then
chmod +x "$CCS_DIR/ccs"
ln -sf "$CCS_DIR/ccs" "$INSTALL_DIR/ccs"
echo "| [OK] Downloaded executable"
else
echo "|"
echo "[X] Error: Failed to download ccs from GitHub"
exit 1
fi
# Note: Shell dependencies (error-codes.sh, progress-indicator.sh, prompt.sh) no longer needed
# Bootstrap delegates all functionality to Node.js via npx
# Download shell completion files
mkdir -p "$CCS_DIR/completions"
if curl -fsSL "$BASE_URL/scripts/completion/ccs.bash" -o "$CCS_DIR/completions/ccs.bash" 2>/dev/null; then
echo "| [OK] Downloaded completion files"
fi
curl -fsSL "$BASE_URL/scripts/completion/ccs.zsh" -o "$CCS_DIR/completions/ccs.zsh" 2>/dev/null || true
curl -fsSL "$BASE_URL/scripts/completion/ccs.fish" -o "$CCS_DIR/completions/ccs.fish" 2>/dev/null || true
else
# Git install - use local ccs file
# Handle both running from root or from installers/ subdirectory
local LIB_DIR=""
if [[ -f "$SCRIPT_DIR/lib/ccs" ]]; then
chmod +x "$SCRIPT_DIR/lib/ccs"
ln -sf "$SCRIPT_DIR/lib/ccs" "$INSTALL_DIR/ccs"
LIB_DIR="$SCRIPT_DIR/lib"
elif [[ -f "$SCRIPT_DIR/../lib/ccs" ]]; then
chmod +x "$SCRIPT_DIR/../lib/ccs"
ln -sf "$SCRIPT_DIR/../lib/ccs" "$INSTALL_DIR/ccs"
LIB_DIR="$SCRIPT_DIR/../lib"
else
echo "|"
echo "[X] Error: lib/ccs executable not found"
exit 1
fi
echo "| [OK] Installed executable"
# Note: Shell dependencies (error-codes.sh, progress-indicator.sh, prompt.sh) no longer needed
# Bootstrap delegates all functionality to Node.js via npx
# Copy shell completion files
mkdir -p "$CCS_DIR/completions"
local COMPLETION_DIR=""
if [[ -d "$SCRIPT_DIR/scripts/completion" ]]; then
COMPLETION_DIR="$SCRIPT_DIR/scripts/completion"
elif [[ -d "$SCRIPT_DIR/../scripts/completion" ]]; then
COMPLETION_DIR="$SCRIPT_DIR/../scripts/completion"
fi
if [[ -n "$COMPLETION_DIR" ]]; then
cp "$COMPLETION_DIR/ccs.bash" "$CCS_DIR/completions/ccs.bash" 2>/dev/null || true
cp "$COMPLETION_DIR/ccs.zsh" "$CCS_DIR/completions/ccs.zsh" 2>/dev/null || true
cp "$COMPLETION_DIR/ccs.fish" "$CCS_DIR/completions/ccs.fish" 2>/dev/null || true
echo "| [OK] Copied completion files"
fi
fi
if [[ ! -L "$INSTALL_DIR/ccs" ]]; then
echo "|"
echo "[X] Error: Failed to create symlink at $INSTALL_DIR/ccs"
echo " Check directory permissions and try again."
exit 1
fi
# Install uninstall script (with idempotency check)
if [[ -f "$SCRIPT_DIR/uninstall.sh" ]]; then
# Only copy if source and destination are different
if [[ "$SCRIPT_DIR/uninstall.sh" != "$CCS_DIR/uninstall.sh" ]]; then
cp "$SCRIPT_DIR/uninstall.sh" "$CCS_DIR/uninstall.sh"
fi
chmod +x "$CCS_DIR/uninstall.sh"
ln -sf "$CCS_DIR/uninstall.sh" "$INSTALL_DIR/ccs-uninstall"
echo "| [OK] Installed uninstaller"
elif [[ "$INSTALL_METHOD" == "standalone" ]] && command -v curl &> /dev/null; then
if curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/uninstall.sh -o "$CCS_DIR/uninstall.sh"; then
chmod +x "$CCS_DIR/uninstall.sh"
ln -sf "$CCS_DIR/uninstall.sh" "$INSTALL_DIR/ccs-uninstall"
echo "| [OK] Installed uninstaller"
fi
fi
echo "| [OK] Created directories"
# Install .claude/ folder
if [[ "$INSTALL_METHOD" == "git" ]]; then
install_claude_folder "$SCRIPT_DIR/.." || echo "| [!] Optional .claude/ installation skipped"
else
install_claude_folder "" || echo "| [!] Optional .claude/ installation skipped"
fi
echo "└─"
echo ""
# --- Profile Setup ---
CURRENT_PROVIDER=$(detect_current_provider)
GLM_SETTINGS="$CCS_DIR/glm.settings.json"
KIMI_SETTINGS="$CCS_DIR/kimi.settings.json"
# Build provider label
PROVIDER_LABEL=""
[[ "$CURRENT_PROVIDER" == "glm" ]] && PROVIDER_LABEL=" (detected: GLM)"
[[ "$CURRENT_PROVIDER" == "kimi" ]] && PROVIDER_LABEL=" (detected: Kimi)"
[[ "$CURRENT_PROVIDER" == "claude" ]] && PROVIDER_LABEL=" (detected: Claude)"
[[ "$CURRENT_PROVIDER" == "custom" ]] && PROVIDER_LABEL=" (detected: custom)"
echo "┌─ Configuring Profiles (v${CCS_VERSION})${PROVIDER_LABEL}"
# Backup existing config if present (single backup, no timestamp)
BACKUP_FILE="$CCS_DIR/config.json.backup"
if [[ -f "$CCS_DIR/config.json" ]]; then
cp "$CCS_DIR/config.json" "$BACKUP_FILE"
fi
# Track if GLM needs API key
NEEDS_GLM_KEY=false
# Create GLM profile if missing
if [[ ! -f "$GLM_SETTINGS" ]]; then
create_glm_profile "$CURRENT_PROVIDER" >/dev/null 2>&1
echo "| [OK] GLM profile -> ~/.ccs/glm.settings.json"
[[ "$CURRENT_PROVIDER" != "glm" ]] && NEEDS_GLM_KEY=true
fi
# Track if Kimi needs API key
NEEDS_KIMI_KEY=false
# Create Kimi profile if missing
if [[ ! -f "$KIMI_SETTINGS" ]]; then
create_kimi_profile "$CURRENT_PROVIDER" >/dev/null 2>&1
echo "| [OK] Kimi profile -> ~/.ccs/kimi.settings.json"
[[ "$CURRENT_PROVIDER" != "kimi" ]] && NEEDS_KIMI_KEY=true
fi
# Create config if missing
if [[ ! -f "$CCS_DIR/config.json" ]]; then
cat > "$CCS_DIR/config.json.tmp" << 'EOF'
{
"profiles": {
"glm": "~/.ccs/glm.settings.json",
"kimi": "~/.ccs/kimi.settings.json",
"default": "~/.claude/settings.json"
}
}
EOF
atomic_mv "$CCS_DIR/config.json.tmp" "$CCS_DIR/config.json"
echo "| [OK] Config -> ~/.ccs/config.json"
fi
# Validate config JSON
if [[ -f "$CCS_DIR/config.json" ]]; then
if command -v jq &> /dev/null; then
if ! jq -e . "$CCS_DIR/config.json" &>/dev/null; then
echo "| [!] Warning: Invalid JSON in config.json"
if [[ -f "$BACKUP_FILE" ]]; then
echo "| Restore from: $BACKUP_FILE"
fi
fi
fi
fi
# Validate GLM settings JSON
if [[ -f "$GLM_SETTINGS" ]]; then
if command -v jq &> /dev/null; then
if ! jq -e . "$GLM_SETTINGS" &>/dev/null; then
echo "| [!] Warning: Invalid JSON in glm.settings.json"
fi
fi
fi
echo "└─"
echo ""
# Detect circular symlink
detect_circular_symlink() {
local target="$1"
local link_path="$2"
# Check if target exists and is symlink
if [[ ! -L "$target" ]]; then
return 1 # Not circular
fi
# Resolve target's link
local target_link=$(readlink "$target" 2>/dev/null || echo "")
local shared_dir="$HOME/.ccs/shared"
# Check if target points back to our shared dir
if [[ "$target_link" == "$shared_dir"* ]] || [[ "$target_link" == "$link_path" ]]; then
echo "[!] Circular symlink detected: $target$target_link"
return 0 # Circular
fi
return 1 # Not circular
}
# Setup shared directories as symlinks to ~/.claude/ (v3.2.0)
setup_shared_symlinks() {
local shared_dir="$CCS_DIR/shared"
local claude_dir="$HOME/.claude"
# Create ~/.claude/ if missing
if [[ ! -d "$claude_dir" ]]; then
echo "[i] Creating ~/.claude/ directory structure"
mkdir -p "$claude_dir"/{commands,skills,agents}
fi
# Create shared directory
mkdir -p "$shared_dir"
# Create symlinks ~/.ccs/shared/* → ~/.claude/*
for dir in commands skills agents; do
local claude_path="$claude_dir/$dir"
local shared_path="$shared_dir/$dir"
# Create directory in ~/.claude/ if missing
if [[ ! -d "$claude_path" ]]; then
mkdir -p "$claude_path"
fi
# Check for circular symlink
if detect_circular_symlink "$claude_path" "$shared_path"; then
echo "[!] Skipping $dir: circular symlink detected"
continue
fi
# If already correct symlink, skip
if [[ -L "$shared_path" ]]; then
local current_target=$(readlink "$shared_path" 2>/dev/null || echo "")
if [[ "$current_target" == "$claude_path" ]]; then
continue # Already correct
fi
rm -rf "$shared_path"
elif [[ -e "$shared_path" ]]; then
# Backup existing data before replacing
if [[ -d "$shared_path" ]] && [[ -n "$(ls -A "$shared_path" 2>/dev/null)" ]]; then
echo "[i] Migrating existing $dir to ~/.claude/$dir"
# Copy to claude dir (preserve user modifications)
for item in "$shared_path"/*; do
[[ -e "$item" ]] || continue
local basename=$(basename "$item")
if [[ ! -e "$claude_path/$basename" ]]; then
cp -r "$item" "$claude_path/" 2>/dev/null
fi
done
fi
rm -rf "$shared_path"
fi
# Create symlink
ln -s "$claude_path" "$shared_path" 2>/dev/null || {
echo "[!] Failed to create symlink for $dir, copying instead"
mkdir -p "$shared_path"
if [[ -d "$claude_path" ]]; then
cp -r "$claude_path"/* "$shared_path/" 2>/dev/null || true
fi
}
done
}
echo "[i] Setting up shared directories..."
setup_shared_symlinks
echo ""
# Install CCS items to ~/.claude/ via symlinks (v4.1.0)
echo "[i] Installing CCS items to ~/.claude/..."
if command -v node &> /dev/null; then
# Check if .claude/ was successfully installed
if [[ -d "$CCS_DIR/.claude" ]]; then
# Download or copy claude-symlink-manager.js
mkdir -p "$CCS_DIR/bin/utils"
if [[ "$INSTALL_METHOD" == "git" ]]; then
# Git install - copy from local repo
if [[ -f "$SCRIPT_DIR/../bin/utils/claude-symlink-manager.js" ]]; then
cp "$SCRIPT_DIR/../bin/utils/claude-symlink-manager.js" "$CCS_DIR/bin/utils/claude-symlink-manager.js"
elif [[ -f "$SCRIPT_DIR/bin/utils/claude-symlink-manager.js" ]]; then
cp "$SCRIPT_DIR/bin/utils/claude-symlink-manager.js" "$CCS_DIR/bin/utils/claude-symlink-manager.js"
fi
else
# Standalone install - download from GitHub
if ! curl -fsSL "https://raw.githubusercontent.com/kaitranntt/ccs/main/bin/utils/claude-symlink-manager.js" -o "$CCS_DIR/bin/utils/claude-symlink-manager.js" 2>/dev/null; then
echo "[!] Failed to download claude-symlink-manager.js"
fi
fi
# Call ClaudeSymlinkManager if available
if [[ -f "$CCS_DIR/bin/utils/claude-symlink-manager.js" ]]; then
node -e "
try {
const ClaudeSymlinkManager = require('$CCS_DIR/bin/utils/claude-symlink-manager.js');
const manager = new ClaudeSymlinkManager();
manager.install();
} catch (err) {
console.log('[!] CCS item installation warning: ' + err.message);
console.log(' Run \"ccs sync\" to retry');
}
" 2>/dev/null || echo "[!] CCS item installation skipped (run 'ccs sync' later)"
else
echo "[!] claude-symlink-manager.js not found, skipping"
echo " Run 'ccs sync' after installation to complete setup"
fi
else
echo "[!] .claude/ folder not found, skipping CCS item installation"
fi
else
echo "[!] Node.js not found, skipping CCS item installation"
echo " Install Node.js and run 'ccs sync' to complete setup"
fi
echo ""
# Auto-configure PATH if needed (all Unix platforms)
configure_shell_path
# Show API key warning if needed
if [[ "$NEEDS_GLM_KEY" == "true" ]]; then
msg_critical "Configure GLM API Key:
1. Get API key from: https://api.z.ai
2. Edit: ~/.ccs/glm.settings.json
3. Replace: YOUR_GLM_API_KEY_HERE
With your actual API key
4. Test: ccs glm --version"
fi
# Show API key warning for Kimi if needed
if [[ "$NEEDS_KIMI_KEY" == "true" ]]; then
msg_critical "Configure Kimi API Key:
1. Get API key from: https://www.kimi.com/coding
2. Edit: ~/.ccs/kimi.settings.json
3. Replace: YOUR_KIMI_API_KEY_HERE
With your actual API key
4. Test: ccs kimi --version"
fi
msg_success "CCS installed successfully!"
echo ""
echo " Installed components:"
echo " * ccs command -> ~/.local/bin/ccs"
echo " * config -> ~/.ccs/config.json"
echo " * glm profile -> ~/.ccs/glm.settings.json"
echo " * kimi profile -> ~/.ccs/kimi.settings.json"
echo " * .claude/ folder -> ~/.ccs/.claude/"
echo ""
echo " Requirements:"
echo " * Node.js 14+ (detected: $(node -v 2>/dev/null || echo 'NOT FOUND'))"
echo " * npm 5.2+ (for npx, comes with Node.js 8.2+)"
echo ""
echo " First Run:"
echo " The first time you run 'ccs', it will automatically install"
echo " the @kaitranntt/ccs npm package globally via npx."
echo ""
echo " Quick start:"
echo " ccs # Use Claude subscription (default)"
echo " ccs glm # Use GLM fallback"
echo " ccs kimi # Use Kimi for Coding"
echo ""
echo " To uninstall: ccs-uninstall"
echo ""
-99
View File
@@ -1,99 +0,0 @@
# CCS Uninstallation Script (Windows PowerShell)
# https://github.com/kaitranntt/ccs
$ErrorActionPreference = "Stop"
# --- Color/Format Functions ---
function Write-Success {
param([string]$Message)
Write-Host "[OK] $Message" -ForegroundColor Green
}
function Write-Info {
param([string]$Message)
Write-Host "[i] $Message" -ForegroundColor Cyan
}
# --- Selective Cleanup Function ---
function Invoke-SelectiveCleanup {
param([string]$CcsDir)
$Removed = @()
$Kept = @()
# Remove executables and version metadata
$FilesToRemove = @("ccs.ps1", "VERSION")
# Also remove the uninstall script itself
$UninstallScript = $PSCommandPath
if ($UninstallScript -and (Test-Path $UninstallScript)) {
$FilesToRemove += $UninstallScript
}
foreach ($File in $FilesToRemove) {
$FilePath = if ([System.IO.Path]::IsPathRooted($File)) { $File } else { Join-Path $CcsDir $File }
if (Test-Path $FilePath) {
Remove-Item $FilePath -Force
$Removed += Split-Path $FilePath -Leaf
}
}
# Remove .claude folder
if (Test-Path "$CcsDir\.claude") {
Remove-Item "$CcsDir\.claude" -Recurse -Force
$Removed += ".claude/"
}
# Track kept files
if (Test-Path "$CcsDir\config.json") { $Kept += "config.json" }
if (Test-Path "$CcsDir\config.json.backup") { $Kept += "config.json.backup" }
Get-ChildItem "$CcsDir\*.settings.json" -ErrorAction SilentlyContinue | ForEach-Object {
$Kept += $_.Name
}
# Report results
if ($Removed.Count -gt 0) {
Write-Info "Cleaned up: $($Removed -join ', ')"
}
if ($Kept.Count -gt 0) {
Write-Info "Kept config files: $($Kept -join ', ')"
}
}
Write-Host "Uninstalling ccs..."
Write-Host ""
$CcsDir = "$env:USERPROFILE\.ccs"
# Remove from PATH
$UserPath = [Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
if ($UserPath -like "*$CcsDir*") {
try {
$NewPath = ($UserPath -split ';' | Where-Object { $_ -ne $CcsDir }) -join ';'
[Environment]::SetEnvironmentVariable("Path", $NewPath, [System.EnvironmentVariableTarget]::User)
Write-Success "Removed from PATH: $CcsDir"
Write-Host " Restart your terminal for changes to take effect."
} catch {
Write-Host "[!] Could not remove from PATH automatically. Please remove manually: $CcsDir" -ForegroundColor Yellow
}
}
# Ask about ~/.ccs directory
if (Test-Path $CcsDir) {
Write-Host ""
$Response = Read-Host "Remove CCS directory $CcsDir`? This includes config and profiles. (y/N)"
if ($Response -match '^[Yy]$') {
Remove-Item $CcsDir -Recurse -Force
Write-Success "Removed: $CcsDir"
} else {
Write-Host ""
Invoke-SelectiveCleanup -CcsDir $CcsDir
}
} else {
Write-Info "No CCS directory found at $CcsDir"
}
Write-Host ""
Write-Success "Uninstall complete!"
Write-Host ""
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# --- Color/Format Functions ---
setup_colors() {
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
GREEN='\033[0;32m'
CYAN='\033[0;36m'
RESET='\033[0m'
else
GREEN='' CYAN='' RESET=''
fi
}
msg_success() {
echo -e "${GREEN}[OK] $1${RESET}"
}
msg_info() {
echo -e "${CYAN}[i] $1${RESET}"
}
# --- Selective Cleanup Function ---
selective_cleanup() {
local ccs_dir="$1"
local removed=()
local kept=()
# Remove executables, version metadata, and .claude folder
for file in "ccs" "uninstall.sh" "VERSION"; do
if [[ -f "$ccs_dir/$file" ]]; then
rm "$ccs_dir/$file"
removed+=("$file")
fi
done
# Remove .claude folder
if [[ -d "$ccs_dir/.claude" ]]; then
rm -rf "$ccs_dir/.claude"
removed+=(".claude/")
fi
# Track kept files
[[ -f "$ccs_dir/config.json" ]] && kept+=("config.json")
[[ -f "$ccs_dir/config.json.backup" ]] && kept+=("config.json.backup")
for settings in "$ccs_dir"/*.settings.json; do
[[ -f "$settings" ]] && kept+=("$(basename "$settings")")
done
# Report results
if [[ ${#removed[@]} -gt 0 ]]; then
msg_info "Cleaned up: ${removed[*]}"
fi
if [[ ${#kept[@]} -gt 0 ]]; then
msg_info "Kept config files: ${kept[*]}"
fi
}
setup_colors
echo "Uninstalling ccs..."
echo ""
# Remove from ~/.local/bin (standard location)
if [[ -L "$HOME/.local/bin/ccs" ]]; then
rm "$HOME/.local/bin/ccs"
msg_success "Removed: $HOME/.local/bin/ccs"
elif [[ -f "$HOME/.local/bin/ccs" ]]; then
rm "$HOME/.local/bin/ccs"
msg_success "Removed: $HOME/.local/bin/ccs"
fi
if [[ -L "$HOME/.local/bin/ccs-uninstall" ]]; then
rm "$HOME/.local/bin/ccs-uninstall"
msg_success "Removed: $HOME/.local/bin/ccs-uninstall"
fi
# Ask about ~/.ccs directory
if [[ -d "$HOME/.ccs" ]]; then
read -p "Remove CCS directory ~/.ccs? This includes config and profiles. (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf "$HOME/.ccs"
msg_success "Removed: $HOME/.ccs"
else
echo ""
selective_cleanup "$HOME/.ccs"
fi
else
msg_info "No CCS directory found at $HOME/.ccs"
fi
echo ""
msg_success "Uninstall complete!"
+18 -38
View File
@@ -1,46 +1,26 @@
/**
* CCS CloudFlare Worker - Redirect to npm Installation
*
* Legacy shell installers are deprecated. This worker now redirects
* all /install* and /uninstall* requests to the npm installation docs.
*/
export default {
async fetch(request) {
const url = new URL(request.url);
const docsUrl = 'https://docs.ccs.kaitran.ca/getting-started/installation';
// Detect platform from User-Agent header
const userAgent = request.headers.get('user-agent') || '';
const isWindows = userAgent.includes('Windows') || userAgent.includes('Win32');
const isPowerShell = userAgent.includes('PowerShell') || userAgent.includes('pwsh');
// Smart routing with platform detection
let filePath;
if (url.pathname === '/install' || url.pathname === '/install.sh') {
filePath = (isWindows && isPowerShell) ? 'installers/install.ps1' : 'installers/install.sh';
} else if (url.pathname === '/install.ps1') {
filePath = 'installers/install.ps1';
} else if (url.pathname === '/uninstall' || url.pathname === '/uninstall.sh') {
filePath = (isWindows && isPowerShell) ? 'installers/uninstall.ps1' : 'installers/uninstall.sh';
} else if (url.pathname === '/uninstall.ps1') {
filePath = 'installers/uninstall.ps1';
} else {
return new Response('Not Found', { status: 404 });
// Redirect all install/uninstall paths to npm installation docs
if (
url.pathname === '/install' ||
url.pathname === '/install.sh' ||
url.pathname === '/install.ps1' ||
url.pathname === '/uninstall' ||
url.pathname === '/uninstall.sh' ||
url.pathname === '/uninstall.ps1'
) {
return Response.redirect(docsUrl, 301);
}
try {
const githubUrl = `https://raw.githubusercontent.com/kaitranntt/ccs/main/${filePath}`;
const response = await fetch(githubUrl);
if (!response.ok) {
return new Response('File not found on GitHub', { status: 404 });
}
const contentType = filePath.endsWith('.ps1')
? 'text/plain; charset=utf-8'
: 'text/x-shellscript; charset=utf-8';
return new Response(response.body, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=300'
}
});
} catch (error) {
return new Response('Server Error', { status: 500 });
}
return new Response('Not Found', { status: 404 });
}
};
+3 -4
View File
@@ -32,7 +32,7 @@ import {
checkCachedUpdate,
isCacheStale,
} from './utils/update-checker';
import { detectInstallationMethod } from './utils/package-manager-detector';
// Note: npm is now the only supported installation method
// ========== Profile Detection ==========
@@ -219,9 +219,8 @@ interface ProfileError extends Error {
async function refreshUpdateCache(): Promise<void> {
try {
const currentVersion = getVersion();
const installMethod = detectInstallationMethod();
// Force=true to always fetch fresh data
await checkForUpdates(currentVersion, true, installMethod);
// npm is now the only supported installation method
await checkForUpdates(currentVersion, true, 'npm');
} catch (_e) {
// Silently fail - update check shouldn't crash main CLI
}
+26 -139
View File
@@ -2,12 +2,12 @@
* Update Command Handler
*
* Handles `ccs update` command - checks for updates and installs latest version.
* Supports both npm and direct installation methods.
* Uses npm/yarn/pnpm/bun package managers exclusively.
*/
import { spawn } from 'child_process';
import { initUI, header, ok, fail, warn, info, color } from '../utils/ui';
import { detectInstallationMethod, detectPackageManager } from '../utils/package-manager-detector';
import { detectPackageManager } from '../utils/package-manager-detector';
import { compareVersionsWithPrerelease } from '../utils/update-checker';
import { getVersion } from '../utils/version';
@@ -35,33 +35,20 @@ export async function handleUpdateCommand(options: UpdateOptions = {}): Promise<
console.log(header('Checking for updates...'));
console.log('');
const installMethod = detectInstallationMethod();
const isNpmInstall = installMethod === 'npm';
// Force reinstall - skip update check
if (force) {
console.log(info(`Force reinstall from @${targetTag} channel...`));
console.log('');
if (isNpmInstall) {
await performNpmUpdate(targetTag, true);
} else {
// Direct install doesn't support --beta
if (beta) {
handleDirectBetaNotSupported();
return;
}
await performDirectUpdate();
}
await performNpmUpdate(targetTag, true);
return;
}
const { checkForUpdates } = await import('../utils/update-checker');
const updateResult = await checkForUpdates(CCS_VERSION, true, installMethod, targetTag);
const updateResult = await checkForUpdates(CCS_VERSION, true, 'npm', targetTag);
if (updateResult.status === 'check_failed') {
handleCheckFailed(updateResult.message ?? 'Update check failed', isNpmInstall, targetTag);
handleCheckFailed(updateResult.message ?? 'Update check failed', targetTag);
return;
}
@@ -102,21 +89,13 @@ export async function handleUpdateCommand(options: UpdateOptions = {}): Promise<
console.log('');
}
if (isNpmInstall) {
await performNpmUpdate(targetTag);
} else {
await performDirectUpdate();
}
await performNpmUpdate(targetTag);
}
/**
* Handle failed update check
*/
function handleCheckFailed(
message: string,
isNpmInstall: boolean,
targetTag: string = 'latest'
): void {
function handleCheckFailed(message: string, targetTag: string = 'latest'): void {
console.log(fail(message));
console.log('');
console.log(warn('Possible causes:'));
@@ -126,36 +105,27 @@ function handleCheckFailed(
console.log('');
console.log('Try again later or update manually:');
if (isNpmInstall) {
const packageManager = detectPackageManager();
let manualCommand: string;
const packageManager = detectPackageManager();
let manualCommand: string;
switch (packageManager) {
case 'npm':
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
break;
case 'yarn':
manualCommand = `yarn global add @kaitranntt/ccs@${targetTag}`;
break;
case 'pnpm':
manualCommand = `pnpm add -g @kaitranntt/ccs@${targetTag}`;
break;
case 'bun':
manualCommand = `bun add -g @kaitranntt/ccs@${targetTag}`;
break;
default:
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
}
console.log(color(` ${manualCommand}`, 'command'));
} else {
const isWindows = process.platform === 'win32';
if (isWindows) {
console.log(color(' irm ccs.kaitran.ca/install | iex', 'command'));
} else {
console.log(color(' curl -fsSL ccs.kaitran.ca/install | bash', 'command'));
}
switch (packageManager) {
case 'npm':
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
break;
case 'yarn':
manualCommand = `yarn global add @kaitranntt/ccs@${targetTag}`;
break;
case 'pnpm':
manualCommand = `pnpm add -g @kaitranntt/ccs@${targetTag}`;
break;
case 'bun':
manualCommand = `bun add -g @kaitranntt/ccs@${targetTag}`;
break;
default:
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
}
console.log(color(` ${manualCommand}`, 'command'));
console.log('');
process.exit(1);
}
@@ -300,86 +270,3 @@ async function performNpmUpdate(
performUpdate();
}
}
/**
* Handle direct install beta not supported error
*/
function handleDirectBetaNotSupported(): void {
console.log(fail('--beta flag requires npm installation'));
console.log('');
console.log('Current installation method: direct installer');
console.log('To use beta releases, install via npm:');
console.log('');
console.log(color(' npm install -g @kaitranntt/ccs', 'command'));
console.log(color(' ccs update --beta', 'command'));
console.log('');
console.log('Or continue using stable releases via direct installer.');
console.log('');
process.exit(1);
}
/**
* Perform update via direct installer (curl/irm)
*/
async function performDirectUpdate(): Promise<void> {
console.log(info('Updating via installer...'));
console.log('');
const isWindows = process.platform === 'win32';
let command: string;
let args: string[];
if (isWindows) {
command = 'powershell.exe';
args = [
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-Command',
'irm ccs.kaitran.ca/install | iex',
];
} else {
command = '/bin/bash';
args = ['-c', 'curl -fsSL ccs.kaitran.ca/install | bash'];
}
const child = spawn(command, args, {
stdio: 'inherit',
});
child.on('exit', (code) => {
if (code === 0) {
console.log('');
console.log(ok('Update successful!'));
console.log('');
console.log(`Run ${color('ccs --version', 'command')} to verify`);
console.log('');
} else {
console.log('');
console.log(fail('Update failed'));
console.log('');
console.log('Try manually:');
if (isWindows) {
console.log(color(' irm ccs.kaitran.ca/install | iex', 'command'));
} else {
console.log(color(' curl -fsSL ccs.kaitran.ca/install | bash', 'command'));
}
console.log('');
}
process.exit(code || 0);
});
child.on('error', () => {
console.log('');
console.log(fail('Failed to run installer'));
console.log('');
console.log('Try manually:');
if (isWindows) {
console.log(color(' irm ccs.kaitran.ca/install | iex', 'command'));
} else {
console.log(color(' curl -fsSL ccs.kaitran.ca/install | bash', 'command'));
}
console.log('');
process.exit(1);
});
}
+1 -65
View File
@@ -2,77 +2,13 @@
* Package Manager Detector Utilities
*
* Cross-platform package manager detection utilities for CCS.
* Now only supports npm-based installation (npm/yarn/pnpm/bun).
*/
import * as path from 'path';
import * as fs from 'fs';
import { spawnSync } from 'child_process';
/**
* Detect installation method
*/
export function detectInstallationMethod(): 'npm' | 'direct' {
const scriptPath = process.argv[1];
// Method 1: Check if script is inside node_modules
if (scriptPath.includes('node_modules')) {
return 'npm';
}
// Method 2: Check if script is in npm global bin directory
const npmGlobalBinPatterns = [
/\.npm\/global\/bin\//,
/\/\.nvm\/versions\/node\/[^/]+\/bin\//,
/\/usr\/local\/bin\//,
/\/usr\/bin\//,
];
for (const pattern of npmGlobalBinPatterns) {
if (pattern.test(scriptPath)) {
try {
const binDir = path.dirname(scriptPath);
const nodeModulesDir = path.join(binDir, '..', 'lib', 'node_modules', '@kaitranntt', 'ccs');
const globalModulesDir = path.join(binDir, '..', 'node_modules', '@kaitranntt', 'ccs');
if (fs.existsSync(nodeModulesDir) || fs.existsSync(globalModulesDir)) {
return 'npm';
}
} catch (_err) {
// Continue checking other patterns
}
}
}
// Method 3: Check if package.json exists in parent directory
const packageJsonPath = path.join(__dirname, '..', 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (pkg.name === '@kaitranntt/ccs') {
return 'npm';
}
} catch (_err) {
// Ignore parse errors
}
}
// Method 4: Check if script is a symlink pointing to node_modules
try {
const stats = fs.lstatSync(scriptPath);
if (stats.isSymbolicLink()) {
const targetPath = fs.readlinkSync(scriptPath);
if (targetPath.includes('node_modules') || targetPath.includes('@kaitranntt/ccs')) {
return 'npm';
}
}
} catch (_err) {
// Continue to default
}
return 'direct';
}
/**
* Detect which package manager was used for installation
*/
@@ -118,9 +118,7 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
describe('Beta stability warning display', function () {
it('should show beta warning when installing from dev channel', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
// Mock update checker to return update available
@@ -166,7 +164,6 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
assert(returnStable, 'should show return to stable instruction');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
updateCheckerModule.checkForUpdates = originalCheckForUpdates;
require('child_process').spawn = originalSpawn;
@@ -175,9 +172,7 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
it('should NOT show beta warning for stable channel', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
// Mock update checker to return update available
@@ -204,7 +199,6 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
assert(!unstableWarning, 'should not show production warning for stable channel');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
updateCheckerModule.checkForUpdates = originalCheckForUpdates;
}
@@ -212,9 +206,7 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
it('should show beta warning even with force flag', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
@@ -228,7 +220,6 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
assert(betaWarning, 'should show beta warning even with force');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
@@ -237,9 +228,7 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
describe('handleCheckFailed with targetTag parameter', function () {
it('should show manual update command with dev tag for npm install', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
@@ -265,9 +254,7 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
it('should show manual update command with latest tag for stable', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
@@ -304,9 +291,7 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
consoleOutput = [];
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => name;
try {
@@ -330,96 +315,13 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
assert(manualCommand, `should show manual ${name} command with dev tag`);
// Restore functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
});
});
it('should show direct install commands when npm detection fails', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'direct';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
// Mock checkForUpdates to return failed
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
updateCheckerModule.checkForUpdates = async () => ({
status: 'check_failed',
message: 'Failed to check for updates'
});
// Call with beta: false (beta not supported for direct)
updateCommandModule.handleUpdateCommand({ beta: false });
} catch (e) {
// Expected to exit
}
// Should show direct install commands
if (process.platform === 'win32') {
const powershellCmd = consoleOutput.find(output =>
output[0] && output[0].includes('irm ccs.kaitran.ca/install | iex')
);
assert(powershellCmd, 'should show PowerShell command for Windows');
} else {
const curlCmd = consoleOutput.find(output =>
output[0] && output[0].includes('curl -fsSL ccs.kaitran.ca/install | bash')
);
assert(curlCmd, 'should show curl command for Unix');
}
});
it('should show beta not supported message for direct install with beta', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
packageManagerDetectorModule.detectInstallationMethod = () => 'direct';
try {
// Mock checkForUpdates to return beta not supported
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
updateCheckerModule.checkForUpdates = async () => ({
status: 'check_failed',
reason: 'beta_not_supported',
message: '--beta requires npm installation method'
});
// Call with beta: true
updateCommandModule.handleUpdateCommand({ beta: true });
} catch (e) {
// Expected to exit
}
// Should show beta not supported message
const betaError = consoleOutput.find(output =>
output[0] && output[0].includes('[X] --beta requires npm installation')
);
assert(betaError, 'should show beta not supported error');
const currentMethod = consoleOutput.find(output =>
output[0] && output[0].includes('Current installation method: direct installer')
);
assert(currentMethod, 'should show current installation method');
// Should show npm install instructions
const npmInstall = consoleOutput.find(output =>
output[0] && output[0].includes('npm install -g @kaitranntt/ccs')
);
assert(npmInstall, 'should show npm install instructions');
const ccsUpdateBeta = consoleOutput.find(output =>
output[0] && output[0].includes('ccs update --beta')
);
assert(ccsUpdateBeta, 'should show ccs update --beta instruction');
});
});
describe('Error handling', function () {
it('should handle checkForUpdates throwing error', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
try {
// Mock checkForUpdates to throw error
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
@@ -435,10 +337,6 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
});
it('should exit with error code 1 when check fails', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
try {
// Mock checkForUpdates to return failed
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
@@ -458,10 +356,6 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
describe('Integration with update checker', function () {
it('should pass correct targetTag to checkForUpdates', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
// Track calls to checkForUpdates
let checkForUpdatesCalls = [];
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
@@ -480,16 +374,11 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
assert.strictEqual(devCall.installMethod, 'npm');
} finally {
// Restore function
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
updateCheckerModule.checkForUpdates = originalCheckForUpdates;
}
});
it('should pass force parameter correctly', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
// Track calls to checkForUpdates
let checkForUpdatesCalls = [];
const originalCheckForUpdates = updateCheckerModule.checkForUpdates;
@@ -507,9 +396,8 @@ describe.skip('Update Command Beta Channel Implementation (Phase 3)', function (
assert.strictEqual(checkForUpdatesCalls[0].force, true, 'should pass force parameter');
} finally {
// Restore function
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
updateCheckerModule.checkForUpdates = originalCheckForUpdates;
}
});
});
});
});
@@ -6,7 +6,6 @@
* - Skip update check when force is true
* - Target tag calculation (latest vs dev) based on beta flag
* - performNpmUpdate function with targetTag parameter
* - handleDirectBetaNotSupported function for direct installs
* - Success messages showing "Reinstall" vs "Update"
*
* NOTE: These tests are currently skipped because they require proper mocking
@@ -113,9 +112,7 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
describe('Target tag calculation based on beta flag', function () {
it('should set targetTag to "latest" when beta flag is false', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
@@ -130,16 +127,13 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
assert(latestCall, 'should install latest tag when beta is false');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
it('should set targetTag to "dev" when beta flag is true', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
@@ -154,7 +148,6 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
assert(devCall, 'should install dev tag when beta is true');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
@@ -162,10 +155,6 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
describe('Force flag behavior', function () {
it('should show force reinstall message when force is true', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
try {
// Call with force: true
updateCommandModule.handleUpdateCommand({ force: true, beta: false });
@@ -176,16 +165,13 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
);
assert(forceMessage, 'should show force reinstall message');
} finally {
// Restore original function
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
// No cleanup needed
}
});
it('should bypass update check when force is true', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
@@ -201,7 +187,6 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
assert(npmCall.args.includes('install'), 'should call install command');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
@@ -210,9 +195,7 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
describe('Package manager tag syntax', function () {
it('should use correct tag syntax for npm', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
@@ -226,16 +209,13 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
assert(npmCall.args.includes('-g'), 'should use global flag for npm');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
it('should use correct tag syntax for yarn', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'yarn';
try {
@@ -249,16 +229,13 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
assert(yarnCall.args.includes('global'), 'should use global flag for yarn');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
it('should use correct tag syntax for pnpm', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'pnpm';
try {
@@ -272,16 +249,13 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
assert(pnpmCall.args.includes('-g'), 'should use global flag for pnpm');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
it('should use correct tag syntax for bun', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'bun';
try {
@@ -295,80 +269,15 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
assert(bunCall.args.includes('-g'), 'should use global flag for bun');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
});
describe('Direct install beta not supported', function () {
it('should show error for direct install with --beta', function () {
// Mock installation method detection as direct
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
packageManagerDetectorModule.detectInstallationMethod = () => 'direct';
try {
// Call with force: true, beta: true
updateCommandModule.handleUpdateCommand({ force: true, beta: true });
// Should show beta not supported error
const betaError = consoleOutput.find(output =>
output[0] && output[0].includes('--beta flag requires npm installation')
);
assert(betaError, 'should show beta not supported error');
const directInstallMsg = consoleOutput.find(output =>
output[0] && output[0].includes('Current installation method: direct installer')
);
assert(directInstallMsg, 'should show direct installer message');
// Should exit with error code
assert(processExitCalls.length > 0, 'should call process.exit');
assert(processExitCalls[0] === 1, 'should exit with error code 1');
} finally {
// Restore original function
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
}
});
it('should allow force reinstall with direct install when beta is false', function () {
// Mock installation method detection as direct
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
packageManagerDetectorModule.detectInstallationMethod = () => 'direct';
try {
// Call with force: true, beta: false
updateCommandModule.handleUpdateCommand({ force: true, beta: false });
// Should NOT show beta error
const betaError = consoleOutput.find(output =>
output[0] && output[0].includes('--beta flag requires npm installation')
);
assert(!betaError, 'should not show beta error when beta is false');
// Should call spawn for direct update
assert(spawnCalls.length > 0, 'should call spawn for direct update');
// Should call curl or powershell
const directUpdateCall = spawnCalls[0];
if (process.platform === 'win32') {
assert(directUpdateCall.command === 'powershell.exe', 'should call powershell on Windows');
} else {
assert(directUpdateCall.command === '/bin/bash', 'should call bash on Unix');
}
} finally {
// Restore original function
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
}
});
});
describe('Success messages', function () {
it('should show "Reinstalling" message when force is true', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
@@ -382,7 +291,6 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
assert(reinstallingMsg, 'should show reinstalling message');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
@@ -391,9 +299,7 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
describe('Combined force and beta behavior', function () {
it('should handle force with beta for npm install', function () {
// Mock package manager detection
const originalDetectInstallationMethod = packageManagerDetectorModule.detectInstallationMethod;
const originalDetectPackageManager = packageManagerDetectorModule.detectPackageManager;
packageManagerDetectorModule.detectInstallationMethod = () => 'npm';
packageManagerDetectorModule.detectPackageManager = () => 'npm';
try {
@@ -412,9 +318,8 @@ describe.skip('Update Command - Force Reinstall Implementation (Phase 2)', funct
assert(forceMessage, 'should show force reinstall from dev channel message');
} finally {
// Restore original functions
packageManagerDetectorModule.detectInstallationMethod = originalDetectInstallationMethod;
packageManagerDetectorModule.detectPackageManager = originalDetectPackageManager;
}
});
});
});
});