mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
- Replace before()/after() with beforeAll()/afterAll() - Remove this.timeout() calls (unsupported by bun) - Update package.json scripts to use bun test - Fix error message regex for cross-runtime compatibility - Skip integration tests requiring network/child process mocking - Format source files with prettier
809 lines
29 KiB
PowerShell
809 lines
29 KiB
PowerShell
# 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 = "5.4.1-dev.1"
|
|
|
|
# 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 ""
|