mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 20:17:45 +00:00
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:
@@ -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 ""
|
||||
@@ -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 ""
|
||||
@@ -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 ""
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user