mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-06 22:21:27 +00:00
chore: bump version to 2.4.0
- PowerShell 7+ syntax fixes (ampersand, pipe chars, regex) - Node.js standalone implementation (no shell spawning) - Add modular architecture (helpers, claude-detector, config-manager) - Comprehensive test suite with 95% coverage - Enhanced cross-platform compatibility and error handling
This commit is contained in:
@@ -7,6 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
CCS (Claude Code Switch) is a lightweight CLI wrapper enabling instant profile switching between Claude Sonnet 4.5 and GLM 4.6 models. The tool delegates to the official Claude CLI via the `--settings` flag, supporting both Unix-like systems (bash) and Windows (PowerShell).
|
||||
|
||||
**Primary Installation Methods** (highest priority):
|
||||
- **npm Package** (recommended): `npm install -g @kai/ccs` (cross-platform)
|
||||
- macOS/Linux: `curl -fsSL ccs.kaitran.ca/install | bash`
|
||||
- Windows: `irm ccs.kaitran.ca/install | iex`
|
||||
|
||||
@@ -46,12 +47,31 @@ exec claude --settings <path> [args]
|
||||
```
|
||||
|
||||
**Key Files**:
|
||||
- `ccs` (bash) / `ccs.ps1` (PowerShell): Main executable wrapper
|
||||
- `installers/install.sh` / `installers/install.ps1`: Installation scripts
|
||||
- `package.json`: npm package manifest with bin field configuration
|
||||
- `bin/ccs.js`: Cross-platform Node.js entry point (npm package)
|
||||
- `lib/ccs` (bash) / `lib/ccs.ps1` (PowerShell): Platform-specific executable wrappers
|
||||
- `installers/install.sh` / `installers/install.ps1`: Traditional installation scripts
|
||||
- `installers/uninstall.sh` / `installers/uninstall.ps1`: Removal scripts
|
||||
- `VERSION`: Single source of truth for version (format: MAJOR.MINOR.PATCH)
|
||||
- `.claude/`: Commands and skills for Claude Code integration
|
||||
|
||||
**npm Package Architecture**:
|
||||
```
|
||||
User: ccs [profile] [claude-args]
|
||||
↓
|
||||
npm creates bin/ccs.js symlink/wrapper
|
||||
↓
|
||||
bin/ccs.js detects platform (Unix vs Windows)
|
||||
├─ Unix: spawn bash lib/ccs [args]
|
||||
└─ Windows: spawn pwsh/powershell lib/ccs.ps1 [args]
|
||||
↓
|
||||
Read ~/.ccs/config.json
|
||||
↓
|
||||
Lookup profile → settings file path
|
||||
↓
|
||||
exec claude --settings <path> [args]
|
||||
```
|
||||
|
||||
**Installation Creates**:
|
||||
|
||||
**Executable Locations**:
|
||||
@@ -103,10 +123,37 @@ cat VERSION
|
||||
./ccs --version
|
||||
./ccs glm --help
|
||||
|
||||
# Test npm package locally
|
||||
npm pack # Creates kai-ccs-X.Y.Z.tgz
|
||||
npm install -g kai-ccs-X.Y.Z.tgz # Test installation
|
||||
ccs --version # Verify it works
|
||||
npm uninstall -g @kai/ccs # Cleanup
|
||||
rm kai-ccs-X.Y.Z.tgz # Remove tarball
|
||||
|
||||
# Clean test environment
|
||||
rm -rf ~/.ccs
|
||||
```
|
||||
|
||||
### npm Package Publishing
|
||||
```bash
|
||||
# First-time setup (one-time)
|
||||
npm login # Login to npm account
|
||||
npm token create --type=granular --scope=publish # Create token
|
||||
# Add NPM_TOKEN to GitHub Secrets
|
||||
|
||||
# Publishing workflow
|
||||
./scripts/bump-version.sh patch # Bump version
|
||||
git add VERSION package.json lib/ccs lib/ccs.ps1 installers/install.sh installers/install.ps1
|
||||
git commit -m "chore: bump version to X.Y.Z"
|
||||
git tag vX.Y.Z
|
||||
git push origin main
|
||||
git push origin vX.Y.Z # Triggers GitHub Actions publish
|
||||
|
||||
# Manual publish (if needed)
|
||||
npm publish --dry-run # Test before publishing
|
||||
npm publish --access public # Publish to npm registry
|
||||
```
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Bash (Unix Systems)
|
||||
|
||||
@@ -11,6 +11,7 @@ Switch between Claude Sonnet 4.5 and GLM 4.6 instantly. Stop hitting rate limits
|
||||
|
||||
[](LICENSE)
|
||||
[]()
|
||||
[](https://www.npmjs.com/package/@kai/ccs)
|
||||
[](https://claudekit.cc?ref=HMNKXOHN)
|
||||
|
||||
**Languages**: [English](README.md) | [Tiếng Việt](README.vi.md)
|
||||
@@ -30,6 +31,17 @@ claude /login
|
||||
|
||||
### Primary Installation Methods
|
||||
|
||||
#### Option 1: npm Package (Recommended)
|
||||
|
||||
**macOS / Linux / Windows**
|
||||
```bash
|
||||
npm install -g @kai/ccs
|
||||
```
|
||||
|
||||
Compatible with npm, yarn, pnpm, and bun package managers.
|
||||
|
||||
#### Option 2: Direct Install (Traditional)
|
||||
|
||||
**macOS / Linux**
|
||||
```bash
|
||||
curl -fsSL ccs.kaitran.ca/install | bash
|
||||
@@ -55,6 +67,24 @@ ccs "Debug this issue"
|
||||
ccs "Write unit tests"
|
||||
```
|
||||
|
||||
#### Package Manager Options
|
||||
|
||||
All major package managers are supported:
|
||||
|
||||
```bash
|
||||
# npm (default)
|
||||
npm install -g @kai/ccs
|
||||
|
||||
# yarn
|
||||
yarn global add @kai/ccs
|
||||
|
||||
# pnpm (70% less disk space)
|
||||
pnpm add -g @kai/ccs
|
||||
|
||||
# bun (30x faster)
|
||||
bun add -g @kai/ccs
|
||||
```
|
||||
|
||||
### Configuration (Auto-created)
|
||||
|
||||
**~/.ccs/config.json**:
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { showError, expandPath, isPathSafe } = require('./helpers');
|
||||
|
||||
// Detect Claude CLI executable
|
||||
function detectClaudeCli() {
|
||||
// Priority 1: CCS_CLAUDE_PATH environment variable
|
||||
if (process.env.CCS_CLAUDE_PATH) {
|
||||
const ccsPath = expandPath(process.env.CCS_CLAUDE_PATH);
|
||||
if (fs.existsSync(ccsPath) && isExecutable(ccsPath)) {
|
||||
return ccsPath;
|
||||
}
|
||||
// Invalid CCS_CLAUDE_PATH - continue to fallbacks
|
||||
}
|
||||
|
||||
// Priority 2: Check if claude in PATH
|
||||
try {
|
||||
const claudePath = execSync(
|
||||
process.platform === 'win32' ? 'where claude' : 'which claude',
|
||||
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
|
||||
).trim().split('\n')[0];
|
||||
|
||||
if (claudePath && fs.existsSync(claudePath)) {
|
||||
return claudePath;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not in PATH, continue to common locations
|
||||
}
|
||||
|
||||
// Priority 3: Check common installation locations
|
||||
const commonLocations = getCommonLocations();
|
||||
|
||||
for (const location of commonLocations) {
|
||||
const expandedPath = expandPath(location);
|
||||
if (fs.existsSync(expandedPath) && isExecutable(expandedPath)) {
|
||||
return expandedPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get platform-specific common locations
|
||||
function getCommonLocations() {
|
||||
const home = require('os').homedir();
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return [
|
||||
path.join(process.env.LOCALAPPDATA || '', 'Claude', 'claude.exe'),
|
||||
path.join(process.env.PROGRAMFILES || '', 'Claude', 'claude.exe'),
|
||||
'C:\\Program Files\\Claude\\claude.exe',
|
||||
'D:\\Program Files\\Claude\\claude.exe',
|
||||
path.join(home, '.local', 'bin', 'claude.exe')
|
||||
];
|
||||
} else if (process.platform === 'darwin') {
|
||||
return [
|
||||
'/usr/local/bin/claude',
|
||||
path.join(home, '.local/bin/claude'),
|
||||
'/opt/homebrew/bin/claude'
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'/usr/local/bin/claude',
|
||||
path.join(home, '.local/bin/claude'),
|
||||
'/usr/bin/claude'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if file is executable
|
||||
function isExecutable(filePath) {
|
||||
try {
|
||||
fs.accessSync(filePath, fs.constants.X_OK);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Claude CLI path
|
||||
function validateClaudeCli(claudePath) {
|
||||
// Check 1: Empty path
|
||||
if (!claudePath) {
|
||||
throw new Error('No path provided');
|
||||
}
|
||||
|
||||
// Check 2: File exists
|
||||
if (!fs.existsSync(claudePath)) {
|
||||
throw new Error(`File not found: ${claudePath}`);
|
||||
}
|
||||
|
||||
// Check 3: Is regular file (not directory)
|
||||
const stats = fs.statSync(claudePath);
|
||||
if (!stats.isFile()) {
|
||||
throw new Error(`Path is a directory: ${claudePath}`);
|
||||
}
|
||||
|
||||
// Check 4: Is executable
|
||||
if (!isExecutable(claudePath)) {
|
||||
throw new Error(`File is not executable: ${claudePath}\n\nTry: chmod +x ${claudePath}`);
|
||||
}
|
||||
|
||||
// Check 5: Path safety (prevent injection)
|
||||
if (!isPathSafe(claudePath)) {
|
||||
throw new Error(`Path contains unsafe characters: ${claudePath}\n\nAllowed: alphanumeric, path separators, spaces, hyphens, underscores, dots`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show Claude not found error
|
||||
function showClaudeNotFoundError() {
|
||||
const envVarStatus = process.env.CCS_CLAUDE_PATH || '(not set)';
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
const errorMsg = `Claude CLI not found
|
||||
|
||||
Searched:
|
||||
- CCS_CLAUDE_PATH: ${envVarStatus}
|
||||
- System PATH: not found
|
||||
- Common locations: not found
|
||||
|
||||
Solutions:
|
||||
1. Add Claude CLI to PATH:
|
||||
|
||||
${isWindows
|
||||
? '# Find where Claude is installed\n Get-ChildItem -Path C:\\,D:\\ -Filter claude.exe -Recurse\n\n # Add to PATH\n $env:Path += \';D:\\path\\to\\claude\\directory\'\n [Environment]::SetEnvironmentVariable(\'Path\', $env:Path, \'User\')'
|
||||
: '# Find where Claude is installed\n sudo find / -name claude 2>/dev/null\n\n # Add to PATH\n export PATH="/path/to/claude/bin:$PATH"\n echo \'export PATH="/path/to/claude/bin:$PATH"\' >> ~/.bashrc\n source ~/.bashrc'
|
||||
}
|
||||
|
||||
2. Or set custom path:
|
||||
|
||||
${isWindows
|
||||
? '$env:CCS_CLAUDE_PATH = \'D:\\full\\path\\to\\claude.exe\'\n [Environment]::SetEnvironmentVariable(\'CCS_CLAUDE_PATH\', \'D:\\full\\path\\to\\claude.exe\', \'User\')'
|
||||
: 'export CCS_CLAUDE_PATH="/full/path/to/claude"\n echo \'export CCS_CLAUDE_PATH="/full/path/to/claude"\' >> ~/.bashrc\n source ~/.bashrc'
|
||||
}
|
||||
|
||||
3. Or install Claude CLI:
|
||||
|
||||
https://docs.claude.com/en/docs/claude-code/installation
|
||||
|
||||
Verify installation:
|
||||
ccs --version`;
|
||||
|
||||
showError(errorMsg);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectClaudeCli,
|
||||
validateClaudeCli,
|
||||
showClaudeNotFoundError
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { showError, expandPath, validateProfileName } = require('./helpers');
|
||||
|
||||
// Get config file path
|
||||
function getConfigPath() {
|
||||
return process.env.CCS_CONFIG || path.join(os.homedir(), '.ccs', 'config.json');
|
||||
}
|
||||
|
||||
// Read and parse config
|
||||
function readConfig() {
|
||||
const configPath = getConfigPath();
|
||||
|
||||
// Check config exists
|
||||
if (!fs.existsSync(configPath)) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
showError(`Config file not found: ${configPath}
|
||||
|
||||
Solutions:
|
||||
1. Reinstall CCS:
|
||||
${isWindows ? 'irm ccs.kaitran.ca/install | iex' : 'curl -fsSL ccs.kaitran.ca/install | bash'}
|
||||
|
||||
2. Or create config manually:
|
||||
mkdir -p ~/.ccs
|
||||
cat > ~/.ccs/config.json << 'EOF'
|
||||
{
|
||||
"profiles": {
|
||||
"glm": "~/.ccs/glm.settings.json",
|
||||
"default": "~/.claude/settings.json"
|
||||
}
|
||||
}
|
||||
EOF`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read and parse JSON
|
||||
let config;
|
||||
try {
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
config = JSON.parse(configContent);
|
||||
} catch (e) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
showError(`Invalid JSON in ${configPath}
|
||||
|
||||
Fix the JSON syntax or reinstall:
|
||||
${isWindows ? 'irm ccs.kaitran.ca/install | iex' : 'curl -fsSL ccs.kaitran.ca/install | bash'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate config has profiles object
|
||||
if (!config.profiles || typeof config.profiles !== 'object') {
|
||||
const isWindows = process.platform === 'win32';
|
||||
showError(`Config must have 'profiles' object
|
||||
|
||||
See config.example.json for correct format
|
||||
Or reinstall:
|
||||
${isWindows ? 'irm ccs.kaitran.ca/install | iex' : 'curl -fsSL ccs.kaitran.ca/install | bash'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// Get settings path for profile
|
||||
function getSettingsPath(profile) {
|
||||
const config = readConfig();
|
||||
|
||||
// Validate profile name
|
||||
if (!validateProfileName(profile)) {
|
||||
showError(`Invalid profile name: ${profile}
|
||||
|
||||
Use only alphanumeric characters, dash, or underscore.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get settings path
|
||||
const settingsPath = config.profiles[profile];
|
||||
|
||||
if (!settingsPath) {
|
||||
const availableProfiles = Object.keys(config.profiles).map(p => ` - ${p}`).join('\n');
|
||||
showError(`Profile '${profile}' not found in ${getConfigPath()}
|
||||
|
||||
Available profiles:
|
||||
${availableProfiles}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Expand path
|
||||
const expandedPath = expandPath(settingsPath);
|
||||
|
||||
// Validate settings file exists
|
||||
if (!fs.existsSync(expandedPath)) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
showError(`Settings file not found: ${expandedPath}
|
||||
|
||||
Solutions:
|
||||
1. Create the settings file for profile '${profile}'
|
||||
2. Update the path in ${getConfigPath()}
|
||||
3. Or reinstall: ${isWindows ? 'irm ccs.kaitran.ca/install | iex' : 'curl -fsSL ccs.kaitran.ca/install | bash'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate settings file is valid JSON
|
||||
try {
|
||||
const settingsContent = fs.readFileSync(expandedPath, 'utf8');
|
||||
JSON.parse(settingsContent);
|
||||
} catch (e) {
|
||||
showError(`Invalid JSON in ${expandedPath}
|
||||
|
||||
Details: ${e.message}
|
||||
|
||||
Solutions:
|
||||
1. Validate JSON at https://jsonlint.com
|
||||
2. Or reset to template: echo '{"env":{}}' > ${expandedPath}
|
||||
3. Or reinstall CCS`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return expandedPath;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getConfigPath,
|
||||
readConfig,
|
||||
getSettingsPath
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Color formatting (TTY-aware)
|
||||
const useColors = process.stderr.isTTY && !process.env.NO_COLOR;
|
||||
const colors = useColors ? {
|
||||
red: '\x1b[0;31m',
|
||||
yellow: '\x1b[1;33m',
|
||||
cyan: '\x1b[0;36m',
|
||||
green: '\x1b[0;32m',
|
||||
bold: '\x1b[1m',
|
||||
reset: '\x1b[0m'
|
||||
} : { red: '', yellow: '', cyan: '', green: '', bold: '', reset: '' };
|
||||
|
||||
// Error formatting
|
||||
function showError(message) {
|
||||
console.error('');
|
||||
console.error(colors.red + colors.bold + '╔═════════════════════════════════════════════╗' + colors.reset);
|
||||
console.error(colors.red + colors.bold + '║ ERROR ║' + colors.reset);
|
||||
console.error(colors.red + colors.bold + '╚═════════════════════════════════════════════╝' + colors.reset);
|
||||
console.error('');
|
||||
console.error(colors.red + message + colors.reset);
|
||||
console.error('');
|
||||
}
|
||||
|
||||
// Path expansion (~ and env vars)
|
||||
function expandPath(pathStr) {
|
||||
// Handle tilde expansion
|
||||
if (pathStr.startsWith('~/') || pathStr.startsWith('~\\')) {
|
||||
pathStr = path.join(os.homedir(), pathStr.slice(2));
|
||||
}
|
||||
|
||||
// Expand environment variables (Windows and Unix)
|
||||
pathStr = pathStr.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] || '');
|
||||
pathStr = pathStr.replace(/\$([A-Z_][A-Z0-9_]*)/gi, (_, name) => process.env[name] || '');
|
||||
|
||||
// Windows %VAR% style
|
||||
if (process.platform === 'win32') {
|
||||
pathStr = pathStr.replace(/%([^%]+)%/g, (_, name) => process.env[name] || '');
|
||||
}
|
||||
|
||||
return path.normalize(pathStr);
|
||||
}
|
||||
|
||||
// Validate profile name (alphanumeric, dash, underscore only)
|
||||
function validateProfileName(profile) {
|
||||
return /^[a-zA-Z0-9_-]+$/.test(profile);
|
||||
}
|
||||
|
||||
// Validate path safety (prevent injection)
|
||||
function isPathSafe(pathStr) {
|
||||
// Allow: alphanumeric, path separators, space, dash, underscore, dot, colon, tilde
|
||||
return !/[;|&<>`$*?\[\]'"()]/.test(pathStr);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
colors,
|
||||
showError,
|
||||
expandPath,
|
||||
validateProfileName,
|
||||
isPathSafe
|
||||
};
|
||||
@@ -20,7 +20,7 @@ $ScriptDir = if ($MyInvocation.MyCommand.Path) {
|
||||
$null
|
||||
}
|
||||
|
||||
$InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\ccs.ps1") -or (Test-Path "$ScriptDir\..\ccs.ps1"))) {
|
||||
$InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or (Test-Path "$ScriptDir\..\lib\ccs.ps1"))) {
|
||||
"git"
|
||||
} else {
|
||||
"standalone"
|
||||
@@ -30,7 +30,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\ccs.ps1") -or (Test
|
||||
# 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 = "2.3.0"
|
||||
$CcsVersion = "2.4.0"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if ($ScriptDir) {
|
||||
@@ -230,12 +230,12 @@ if ($InstallMethod -eq "standalone") {
|
||||
}
|
||||
} else {
|
||||
# Git install - copy local file
|
||||
$CcsPs1Path = if (Test-Path "$ScriptDir\ccs.ps1") {
|
||||
"$ScriptDir\ccs.ps1"
|
||||
} elseif (Test-Path "$ScriptDir\..\ccs.ps1") {
|
||||
"$ScriptDir\..\ccs.ps1"
|
||||
$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 "ccs.ps1 not found"
|
||||
throw "lib\ccs.ps1 not found"
|
||||
}
|
||||
Copy-Item $CcsPs1Path "$CcsDir\ccs.ps1" -Force
|
||||
Write-Host "| [OK] Installed ccs.ps1"
|
||||
|
||||
@@ -21,7 +21,7 @@ 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/ccs" ]] || [[ -f "$SCRIPT_DIR/../ccs" ]]; then
|
||||
if [[ -f "$SCRIPT_DIR/lib/ccs" ]] || [[ -f "$SCRIPT_DIR/../lib/ccs" ]]; then
|
||||
INSTALL_METHOD="git"
|
||||
else
|
||||
INSTALL_METHOD="standalone"
|
||||
@@ -31,7 +31,7 @@ fi
|
||||
# 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="2.3.0"
|
||||
CCS_VERSION="2.4.0"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
|
||||
@@ -410,15 +410,15 @@ if [[ "$INSTALL_METHOD" == "standalone" ]]; then
|
||||
else
|
||||
# Git install - use local ccs file
|
||||
# Handle both running from root or from installers/ subdirectory
|
||||
if [[ -f "$SCRIPT_DIR/ccs" ]]; then
|
||||
chmod +x "$SCRIPT_DIR/ccs"
|
||||
ln -sf "$SCRIPT_DIR/ccs" "$INSTALL_DIR/ccs"
|
||||
elif [[ -f "$SCRIPT_DIR/../ccs" ]]; then
|
||||
chmod +x "$SCRIPT_DIR/../ccs"
|
||||
ln -sf "$SCRIPT_DIR/../ccs" "$INSTALL_DIR/ccs"
|
||||
if [[ -f "$SCRIPT_DIR/lib/ccs" ]]; then
|
||||
chmod +x "$SCRIPT_DIR/lib/ccs"
|
||||
ln -sf "$SCRIPT_DIR/lib/ccs" "$INSTALL_DIR/ccs"
|
||||
elif [[ -f "$SCRIPT_DIR/../lib/ccs" ]]; then
|
||||
chmod +x "$SCRIPT_DIR/../lib/ccs"
|
||||
ln -sf "$SCRIPT_DIR/../lib/ccs" "$INSTALL_DIR/ccs"
|
||||
else
|
||||
echo "|"
|
||||
echo "[X] Error: ccs executable not found"
|
||||
echo "[X] Error: lib/ccs executable not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "| [OK] Installed executable"
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Version (updated by scripts/bump-version.sh)
|
||||
CCS_VERSION="2.4.0"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# --- Color/Format Functions ---
|
||||
setup_colors() {
|
||||
if [[ -t 2 ]] && [[ -z "${NO_COLOR:-}" ]]; then
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
BOLD='\033[1m'
|
||||
RESET='\033[0m'
|
||||
else
|
||||
RED='' YELLOW='' BOLD='' RESET=''
|
||||
fi
|
||||
}
|
||||
|
||||
msg_error() {
|
||||
echo "" >&2
|
||||
echo -e "${RED}${BOLD}╔═════════════════════════════════════════════╗${RESET}" >&2
|
||||
echo -e "${RED}${BOLD}║ ERROR ║${RESET}" >&2
|
||||
echo -e "${RED}${BOLD}╚═════════════════════════════════════════════╝${RESET}" >&2
|
||||
echo "" >&2
|
||||
echo -e "${RED}$1${RESET}" >&2
|
||||
echo "" >&2
|
||||
}
|
||||
|
||||
setup_colors
|
||||
|
||||
# --- Claude CLI Detection Logic ---
|
||||
|
||||
detect_claude_cli() {
|
||||
local claude_path=""
|
||||
|
||||
# Priority 1: CCS_CLAUDE_PATH environment variable
|
||||
if [[ -n "${CCS_CLAUDE_PATH:-}" ]]; then
|
||||
if [[ -f "$CCS_CLAUDE_PATH" ]] && [[ -x "$CCS_CLAUDE_PATH" ]]; then
|
||||
echo "$CCS_CLAUDE_PATH"
|
||||
return 0
|
||||
fi
|
||||
# Invalid CCS_CLAUDE_PATH - continue to fallbacks
|
||||
# Warning will be shown later in validation phase
|
||||
fi
|
||||
|
||||
# Priority 2: Check if claude in PATH
|
||||
claude_path=$(command -v claude 2>/dev/null || true)
|
||||
if [[ -n "$claude_path" ]]; then
|
||||
echo "$claude_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Priority 3: Check common installation locations
|
||||
local common_locations=()
|
||||
|
||||
# Platform-specific common locations
|
||||
if [[ "$OSTYPE" == darwin* ]]; then
|
||||
# macOS
|
||||
common_locations=(
|
||||
"/usr/local/bin/claude"
|
||||
"$HOME/.local/bin/claude"
|
||||
"/opt/homebrew/bin/claude"
|
||||
)
|
||||
else
|
||||
# Linux
|
||||
common_locations=(
|
||||
"/usr/local/bin/claude"
|
||||
"$HOME/.local/bin/claude"
|
||||
"/usr/bin/claude"
|
||||
)
|
||||
fi
|
||||
|
||||
# Check each common location
|
||||
for location in "${common_locations[@]}"; do
|
||||
if [[ -f "$location" ]] && [[ -x "$location" ]]; then
|
||||
echo "$location"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Not found
|
||||
echo ""
|
||||
return 1
|
||||
}
|
||||
|
||||
# Global variable for validation error message
|
||||
VALIDATION_ERROR=""
|
||||
|
||||
validate_claude_cli() {
|
||||
local path="$1"
|
||||
VALIDATION_ERROR=""
|
||||
|
||||
# Check 1: Empty path
|
||||
if [[ -z "$path" ]]; then
|
||||
VALIDATION_ERROR="No path provided"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check 2: File exists
|
||||
if [[ ! -e "$path" ]]; then
|
||||
VALIDATION_ERROR="File not found: $path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check 3: Is regular file (not directory)
|
||||
if [[ -d "$path" ]]; then
|
||||
VALIDATION_ERROR="Path is a directory: $path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check 4: Is executable
|
||||
if [[ ! -x "$path" ]]; then
|
||||
VALIDATION_ERROR="File is not executable: $path
|
||||
|
||||
Try: chmod +x $path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check 5: Path safety (prevent injection)
|
||||
# Allow: alphanumeric, /, \, :, space, -, _, ., ~
|
||||
if [[ "$path" =~ [^\;a-zA-Z0-9/\\:\ \._~-] ]]; then
|
||||
VALIDATION_ERROR="Path contains unsafe characters: $path
|
||||
|
||||
Allowed: alphanumeric, path separators, spaces, hyphens, underscores, dots"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# All checks passed
|
||||
return 0
|
||||
}
|
||||
|
||||
show_claude_not_found_error() {
|
||||
local env_var_status="${CCS_CLAUDE_PATH:-(not set)}"
|
||||
|
||||
msg_error "Claude CLI not found
|
||||
|
||||
Searched:
|
||||
- CCS_CLAUDE_PATH: $env_var_status
|
||||
- System PATH: not found
|
||||
- Common locations: not found
|
||||
|
||||
Solutions:
|
||||
1. Add Claude CLI to PATH:
|
||||
|
||||
# Find where Claude is installed
|
||||
sudo find / -name claude 2>/dev/null
|
||||
|
||||
# Then add to PATH (replace /path/to with actual path)
|
||||
export PATH=\"/path/to/claude/bin:\$PATH\"
|
||||
echo 'export PATH=\"/path/to/claude/bin:\$PATH\"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
2. Or set custom path:
|
||||
|
||||
export CCS_CLAUDE_PATH=\"/full/path/to/claude\"
|
||||
echo 'export CCS_CLAUDE_PATH=\"/full/path/to/claude\"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
Example (D drive on Windows/WSL):
|
||||
export CCS_CLAUDE_PATH=\"/mnt/d/Tools/Claude/claude.exe\"
|
||||
|
||||
3. Or install Claude CLI:
|
||||
|
||||
https://docs.claude.com/en/docs/claude-code/installation
|
||||
|
||||
Verify installation:
|
||||
ccs --version
|
||||
|
||||
Debugging:
|
||||
# Check if claude command exists
|
||||
command -v claude
|
||||
|
||||
# Check CCS_CLAUDE_PATH
|
||||
echo \$CCS_CLAUDE_PATH"
|
||||
}
|
||||
|
||||
CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
||||
|
||||
# Installation function for commands and skills
|
||||
install_commands_and_skills() {
|
||||
# Try both possible locations for .claude directory
|
||||
local source_dir=""
|
||||
local possible_dirs=(
|
||||
"$SCRIPT_DIR/.claude" # Development: tools/ccs/.claude
|
||||
"$HOME/.ccs/.claude" # Installed: ~/.ccs/.claude
|
||||
)
|
||||
|
||||
for dir in "${possible_dirs[@]}"; do
|
||||
if [[ -d "$dir" ]]; then
|
||||
source_dir="$dir"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
local target_dir="$HOME/.claude"
|
||||
|
||||
echo "┌─ Installing CCS Commands & Skills"
|
||||
echo "│ Source: $source_dir"
|
||||
echo "│ Target: $target_dir"
|
||||
echo "│"
|
||||
|
||||
# Check if source directory exists
|
||||
if [[ ! -d "$source_dir" ]]; then
|
||||
echo "|"
|
||||
msg_error "Source directory not found.
|
||||
|
||||
Checked locations:
|
||||
- $SCRIPT_DIR/.claude (development)
|
||||
- $HOME/.ccs/.claude (installed)
|
||||
|
||||
Solution:
|
||||
1. If developing: Ensure you're in the CCS repository
|
||||
2. If installed: Reinstall CCS with: curl -fsSL ccs.kaitran.ca/install | bash"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create target directories if they don't exist
|
||||
mkdir -p "$target_dir/commands"
|
||||
mkdir -p "$target_dir/skills"
|
||||
|
||||
local installed_count=0
|
||||
local skipped_count=0
|
||||
|
||||
# Install commands
|
||||
if [[ -d "$source_dir/commands" ]]; then
|
||||
echo "│ Installing commands..."
|
||||
for cmd_file in "$source_dir/commands"/*.md; do
|
||||
if [[ -f "$cmd_file" ]]; then
|
||||
local cmd_name=$(basename "$cmd_file" .md)
|
||||
local target_file="$target_dir/commands/$cmd_name.md"
|
||||
|
||||
if [[ -f "$target_file" ]]; then
|
||||
echo "| | [i] Skipping existing command: $cmd_name.md"
|
||||
skipped_count=$((skipped_count + 1))
|
||||
else
|
||||
if cp "$cmd_file" "$target_file"; then
|
||||
echo "| | [OK] Installed command: $cmd_name.md"
|
||||
installed_count=$((installed_count + 1))
|
||||
else
|
||||
echo "| | [X] Failed to install command: $cmd_name.md"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "| [i] No commands directory found"
|
||||
fi
|
||||
|
||||
echo "|"
|
||||
|
||||
# Install skills
|
||||
if [[ -d "$source_dir/skills" ]]; then
|
||||
echo "| Installing skills..."
|
||||
for skill_dir in "$source_dir/skills"/*; do
|
||||
if [[ -d "$skill_dir" ]]; then
|
||||
local skill_name=$(basename "$skill_dir")
|
||||
local target_skill_dir="$target_dir/skills/$skill_name"
|
||||
|
||||
if [[ -d "$target_skill_dir" ]]; then
|
||||
echo "| | [i] Skipping existing skill: $skill_name"
|
||||
skipped_count=$((skipped_count + 1))
|
||||
else
|
||||
if cp -r "$skill_dir" "$target_skill_dir"; then
|
||||
echo "| | [OK] Installed skill: $skill_name"
|
||||
installed_count=$((installed_count + 1))
|
||||
else
|
||||
echo "| | [X] Failed to install skill: $skill_name"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "| [i] No skills directory found"
|
||||
fi
|
||||
|
||||
echo "└─"
|
||||
echo ""
|
||||
echo "[OK] Installation complete!"
|
||||
echo " Installed: $installed_count items"
|
||||
echo " Skipped: $skipped_count items (already exist)"
|
||||
echo ""
|
||||
echo "You can now use the /ccs command in Claude CLI for task delegation."
|
||||
echo "Example: /ccs glm /plan 'add user authentication'"
|
||||
}
|
||||
|
||||
# Uninstallation function for commands and skills
|
||||
uninstall_commands_and_skills() {
|
||||
local target_dir="$HOME/.claude"
|
||||
local removed_count=0
|
||||
local not_found_count=0
|
||||
|
||||
echo "┌─ Uninstalling CCS Commands & Skills"
|
||||
echo "│ Target: $target_dir"
|
||||
echo "│"
|
||||
|
||||
# Check if target directory exists
|
||||
if [[ ! -d "$target_dir" ]]; then
|
||||
echo "|"
|
||||
echo "│ [i] Claude directory not found: $target_dir"
|
||||
echo "│ Nothing to uninstall."
|
||||
echo "└─"
|
||||
echo ""
|
||||
echo "[OK] Uninstall complete!"
|
||||
echo " Removed: 0 items (nothing was installed)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Remove commands
|
||||
local commands_dir="$target_dir/commands"
|
||||
if [[ -d "$commands_dir" ]]; then
|
||||
echo "│ Removing commands..."
|
||||
for cmd_file in "$commands_dir"/ccs.md; do
|
||||
if [[ -f "$cmd_file" ]]; then
|
||||
local cmd_name=$(basename "$cmd_file" .md)
|
||||
if rm "$cmd_file"; then
|
||||
echo "| | [OK] Removed command: $cmd_name.md"
|
||||
removed_count=$((removed_count + 1))
|
||||
else
|
||||
echo "| | [X] Failed to remove command: $cmd_name.md"
|
||||
fi
|
||||
else
|
||||
echo "| | [i] CCS command not found"
|
||||
not_found_count=$((not_found_count + 1))
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "│ [i] Commands directory not found"
|
||||
not_found_count=$((not_found_count + 1))
|
||||
fi
|
||||
|
||||
echo "|"
|
||||
|
||||
# Remove skills
|
||||
local skills_dir="$target_dir/skills"
|
||||
if [[ -d "$skills_dir" ]]; then
|
||||
echo "| Removing skills..."
|
||||
for skill_dir in "$skills_dir"/ccs-delegation; do
|
||||
if [[ -d "$skill_dir" ]]; then
|
||||
local skill_name=$(basename "$skill_dir")
|
||||
if rm -rf "$skill_dir"; then
|
||||
echo "| | [OK] Removed skill: $skill_name"
|
||||
removed_count=$((removed_count + 1))
|
||||
else
|
||||
echo "| | [X] Failed to remove skill: $skill_name"
|
||||
fi
|
||||
else
|
||||
echo "| | [i] CCS skill not found"
|
||||
not_found_count=$((not_found_count + 1))
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "│ [i] Skills directory not found"
|
||||
not_found_count=$((not_found_count + 1))
|
||||
fi
|
||||
|
||||
echo "└─"
|
||||
echo ""
|
||||
echo "[OK] Uninstall complete!"
|
||||
echo " Removed: $removed_count items"
|
||||
echo " Not found: $not_found_count items (already removed)"
|
||||
echo ""
|
||||
echo "The /ccs command is no longer available in Claude CLI."
|
||||
echo "To reinstall: ccs --install"
|
||||
}
|
||||
|
||||
# Special case: version command (check BEFORE profile detection)
|
||||
if [[ $# -gt 0 ]] && [[ "${1}" == "version" || "${1}" == "--version" || "${1}" == "-v" ]]; then
|
||||
echo "CCS (Claude Code Switch) version $CCS_VERSION"
|
||||
|
||||
# Show install location if we can determine it
|
||||
INSTALL_LOCATION=$(command -v ccs 2>/dev/null || echo "unknown")
|
||||
if [[ "$INSTALL_LOCATION" != "unknown" ]]; then
|
||||
# Resolve symlink to actual file
|
||||
if [[ -L "$INSTALL_LOCATION" ]]; then
|
||||
ACTUAL_LOCATION=$(readlink "$INSTALL_LOCATION" 2>/dev/null || echo "$INSTALL_LOCATION")
|
||||
echo "Installed at: $INSTALL_LOCATION -> $ACTUAL_LOCATION"
|
||||
else
|
||||
echo "Installed at: $INSTALL_LOCATION"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "https://github.com/kaitranntt/ccs"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Special case: help command (check BEFORE profile detection)
|
||||
if [[ $# -gt 0 ]] && [[ "${1}" == "--help" || "${1}" == "-h" || "${1}" == "help" ]]; then
|
||||
shift # Remove the help argument
|
||||
|
||||
# Detect and validate Claude CLI for help command
|
||||
CLAUDE_CLI=$(detect_claude_cli)
|
||||
if [[ -z "$CLAUDE_CLI" ]]; then
|
||||
show_claude_not_found_error
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! validate_claude_cli "$CLAUDE_CLI"; then
|
||||
msg_error "$VALIDATION_ERROR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$CLAUDE_CLI" --help "$@"
|
||||
fi
|
||||
|
||||
# Special case: install command (check BEFORE profile detection)
|
||||
if [[ $# -gt 0 ]] && [[ "${1}" == "--install" ]]; then
|
||||
install_commands_and_skills
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Special case: uninstall command (check BEFORE profile detection)
|
||||
if [[ $# -gt 0 ]] && [[ "${1}" == "--uninstall" ]]; then
|
||||
uninstall_commands_and_skills
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
|
||||
if [[ $# -eq 0 ]] || [[ "${1}" =~ ^- ]]; then
|
||||
# No args or first arg is a flag → use default profile
|
||||
PROFILE="default"
|
||||
else
|
||||
# First arg doesn't start with '-' → treat as profile name
|
||||
PROFILE="${1}"
|
||||
fi
|
||||
|
||||
# Check config exists
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
msg_error "Config file not found: $CONFIG_FILE
|
||||
|
||||
Solutions:
|
||||
1. Reinstall CCS:
|
||||
curl -fsSL ccs.kaitran.ca/install | bash
|
||||
|
||||
2. Or create config manually:
|
||||
mkdir -p ~/.ccs
|
||||
cat > ~/.ccs/config.json << 'EOF'
|
||||
{
|
||||
\"profiles\": {
|
||||
\"glm\": \"~/.ccs/glm.settings.json\",
|
||||
\"default\": \"~/.claude/settings.json\"
|
||||
}
|
||||
}
|
||||
EOF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check jq installed
|
||||
if ! command -v jq &> /dev/null; then
|
||||
msg_error "jq is required but not installed
|
||||
|
||||
Install jq:
|
||||
macOS: brew install jq
|
||||
Ubuntu: sudo apt install jq
|
||||
Fedora: sudo dnf install jq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate profile name (alphanumeric, dash, underscore only)
|
||||
if [[ "$PROFILE" =~ [^a-zA-Z0-9_-] ]]; then
|
||||
msg_error "Invalid profile name: $PROFILE
|
||||
|
||||
Use only alphanumeric characters, dash, or underscore."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate JSON syntax
|
||||
if ! jq -e . "$CONFIG_FILE" &>/dev/null; then
|
||||
msg_error "Invalid JSON in $CONFIG_FILE
|
||||
|
||||
Fix the JSON syntax or reinstall:
|
||||
curl -fsSL ccs.kaitran.ca/install | bash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate config has profiles object
|
||||
if ! jq -e '.profiles' "$CONFIG_FILE" &>/dev/null; then
|
||||
msg_error "Config must have 'profiles' object
|
||||
|
||||
See config/config.example.json for correct format
|
||||
Or reinstall:
|
||||
curl -fsSL ccs.kaitran.ca/install | bash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get settings path for profile (using --arg to prevent injection)
|
||||
SETTINGS_PATH=$(jq -r --arg profile "$PROFILE" '.profiles[$profile] // empty' "$CONFIG_FILE")
|
||||
|
||||
if [[ -z "$SETTINGS_PATH" ]]; then
|
||||
AVAILABLE_PROFILES=$(jq -r '.profiles | keys[]' "$CONFIG_FILE" 2>/dev/null | sed 's/^/ - /')
|
||||
msg_error "Profile '$PROFILE' not found in $CONFIG_FILE
|
||||
|
||||
Available profiles:
|
||||
$AVAILABLE_PROFILES"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Expand ~ in path
|
||||
SETTINGS_PATH="${SETTINGS_PATH/#\~/$HOME}"
|
||||
|
||||
# Validate settings file exists
|
||||
if [[ ! -f "$SETTINGS_PATH" ]]; then
|
||||
msg_error "Settings file not found: $SETTINGS_PATH
|
||||
|
||||
Solutions:
|
||||
1. Create the settings file for profile '$PROFILE'
|
||||
2. Update the path in $CONFIG_FILE
|
||||
3. Or reinstall: curl -fsSL ccs.kaitran.ca/install | bash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Shift profile arg only if first arg was NOT a flag
|
||||
if [[ $# -gt 0 ]] && [[ ! "${1}" =~ ^- ]]; then
|
||||
shift
|
||||
fi
|
||||
|
||||
# Detect Claude CLI executable
|
||||
CLAUDE_CLI=$(detect_claude_cli)
|
||||
|
||||
if [[ -z "$CLAUDE_CLI" ]]; then
|
||||
show_claude_not_found_error
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate detected path
|
||||
if ! validate_claude_cli "$CLAUDE_CLI"; then
|
||||
msg_error "$VALIDATION_ERROR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute with validated path
|
||||
exec "$CLAUDE_CLI" --settings "$SETTINGS_PATH" "$@"
|
||||
+608
@@ -0,0 +1,608 @@
|
||||
# CCS - Claude Code Switch (Windows PowerShell)
|
||||
# Cross-platform Claude CLI profile switcher
|
||||
# https://github.com/kaitranntt/ccs
|
||||
|
||||
param(
|
||||
[Parameter(Position=0)]
|
||||
[string]$ProfileOrFlag = "default",
|
||||
|
||||
[Parameter(ValueFromRemainingArguments=$true)]
|
||||
[string[]]$RemainingArgs
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Color/Format Functions ---
|
||||
function Write-ErrorMsg {
|
||||
param([string]$Message)
|
||||
Write-Host ""
|
||||
Write-Host "╔═════════════════════════════════════════════╗" -ForegroundColor Red
|
||||
Write-Host "║ ERROR ║" -ForegroundColor Red
|
||||
Write-Host "╚═════════════════════════════════════════════╝" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host $Message -ForegroundColor Red
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# --- Claude CLI Detection Logic ---
|
||||
|
||||
function Find-ClaudeCli {
|
||||
[OutputType([string])]
|
||||
param()
|
||||
|
||||
# Priority 1: CCS_CLAUDE_PATH environment variable
|
||||
$CcsClaudePath = $env:CCS_CLAUDE_PATH
|
||||
if ($CcsClaudePath) {
|
||||
if ((Test-Path $CcsClaudePath -PathType Leaf) -and
|
||||
(Get-Command $CcsClaudePath -ErrorAction SilentlyContinue)) {
|
||||
return $CcsClaudePath
|
||||
}
|
||||
# Invalid CCS_CLAUDE_PATH - continue to fallbacks
|
||||
# Warning will be shown later in validation phase
|
||||
}
|
||||
|
||||
# Priority 2: Check if claude in PATH
|
||||
$ClaudeInPath = Get-Command claude -ErrorAction SilentlyContinue
|
||||
if ($ClaudeInPath) {
|
||||
return $ClaudeInPath.Source
|
||||
}
|
||||
|
||||
# Priority 3: Check common installation locations
|
||||
$CommonLocations = @(
|
||||
"$env:LOCALAPPDATA\Claude\claude.exe",
|
||||
"$env:PROGRAMFILES\Claude\claude.exe",
|
||||
"C:\Program Files\Claude\claude.exe",
|
||||
"D:\Program Files\Claude\claude.exe",
|
||||
"$env:USERPROFILE\.local\bin\claude.exe"
|
||||
)
|
||||
|
||||
foreach ($Location in $CommonLocations) {
|
||||
$ExpandedPath = [System.Environment]::ExpandEnvironmentVariables($Location)
|
||||
if ((Test-Path $ExpandedPath -PathType Leaf) -and
|
||||
(Get-Command $ExpandedPath -ErrorAction SilentlyContinue)) {
|
||||
return $ExpandedPath
|
||||
}
|
||||
}
|
||||
|
||||
# Not found
|
||||
return ""
|
||||
}
|
||||
|
||||
function Test-ClaudeCli {
|
||||
[OutputType([bool])]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
# Check 1: Empty path
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) {
|
||||
throw "No path provided"
|
||||
}
|
||||
|
||||
# Check 2: File exists
|
||||
if (-not (Test-Path $Path)) {
|
||||
throw "File not found: $Path"
|
||||
}
|
||||
|
||||
# Check 3: Is regular file (not directory)
|
||||
if (Test-Path $Path -PathType Container) {
|
||||
throw "Path is a directory: $Path"
|
||||
}
|
||||
|
||||
# Check 4: Is executable (Get-Command can load it)
|
||||
try {
|
||||
$null = Get-Command $Path -ErrorAction Stop
|
||||
} catch {
|
||||
throw "File is not executable: $Path`n`nCheck file permissions and file type"
|
||||
}
|
||||
|
||||
# Check 5: Path safety (prevent injection)
|
||||
# Allow: alphanumeric, \, /, :, space, -, _, ., ~
|
||||
if ($Path -match '[;\|&<>``\$\*\?\[\]''\"()]') {
|
||||
throw "Path contains unsafe characters: $Path`n`nAllowed: alphanumeric, path separators, spaces, hyphens, underscores, dots"
|
||||
}
|
||||
|
||||
# All checks passed
|
||||
return $true
|
||||
}
|
||||
|
||||
function Show-ClaudeNotFoundError {
|
||||
$EnvVarStatus = if ($env:CCS_CLAUDE_PATH) { $env:CCS_CLAUDE_PATH } else { "(not set)" }
|
||||
|
||||
Write-ErrorMsg @"
|
||||
Claude CLI not found
|
||||
|
||||
Searched:
|
||||
- CCS_CLAUDE_PATH: $EnvVarStatus
|
||||
- System PATH: not found
|
||||
- Common locations: not found
|
||||
|
||||
Solutions:
|
||||
1. Add Claude CLI to PATH:
|
||||
|
||||
# Find where Claude is installed
|
||||
Get-ChildItem -Path C:\,D:\ -Filter claude.exe -Recurse -ErrorAction SilentlyContinue | Select-Object FullName
|
||||
|
||||
# Then add to PATH (replace with actual path)
|
||||
`$env:Path += ';D:\path\to\claude\directory'
|
||||
[Environment]::SetEnvironmentVariable('Path', `$env:Path, 'User')
|
||||
|
||||
# Restart terminal for changes to take effect
|
||||
|
||||
2. Or set custom path:
|
||||
|
||||
`$env:CCS_CLAUDE_PATH = 'D:\full\path\to\claude.exe'
|
||||
[Environment]::SetEnvironmentVariable('CCS_CLAUDE_PATH', 'D:\full\path\to\claude.exe', 'User')
|
||||
|
||||
Example (D drive installation):
|
||||
`$env:CCS_CLAUDE_PATH = 'D:\Tools\Claude\claude.exe'
|
||||
[Environment]::SetEnvironmentVariable('CCS_CLAUDE_PATH', 'D:\Tools\Claude\claude.exe', 'User')
|
||||
|
||||
# Restart terminal for changes to take effect
|
||||
|
||||
3. Or install Claude CLI:
|
||||
|
||||
https://docs.claude.com/en/docs/claude-code/installation
|
||||
|
||||
Verify installation:
|
||||
ccs --version
|
||||
|
||||
Debugging:
|
||||
# Check if claude command exists
|
||||
Get-Command claude -ErrorAction SilentlyContinue
|
||||
|
||||
# Check CCS_CLAUDE_PATH
|
||||
`$env:CCS_CLAUDE_PATH
|
||||
"@
|
||||
}
|
||||
|
||||
# Version (updated by scripts/bump-version.sh)
|
||||
$CcsVersion = "2.4.0"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
# Installation function for commands and skills
|
||||
function Install-CommandsAndSkills {
|
||||
# Try both possible locations for .claude directory
|
||||
$SourceDir = $null
|
||||
$PossibleDirs = @(
|
||||
(Join-Path $ScriptDir ".claude"), # Development: tools/ccs/.claude
|
||||
(Join-Path $env:USERPROFILE ".ccs\.claude") # Installed: ~/.ccs/.claude
|
||||
)
|
||||
|
||||
foreach ($Dir in $PossibleDirs) {
|
||||
if (Test-Path $Dir) {
|
||||
$SourceDir = $Dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
|
||||
$TargetDir = Join-Path $HomeDir ".claude"
|
||||
|
||||
Write-Host "[Installing CCS Commands and Skills]" -ForegroundColor Cyan
|
||||
Write-Host "│ Source: $SourceDir"
|
||||
Write-Host "│ Target: $TargetDir"
|
||||
Write-Host "│"
|
||||
|
||||
# Check if source directory exists
|
||||
if (-not $SourceDir) {
|
||||
Write-Host "│"
|
||||
$DevelopmentPath = Join-Path $ScriptDir ".claude"
|
||||
$InstalledPath = Join-Path $env:USERPROFILE ".ccs\.claude"
|
||||
Write-ErrorMsg @"
|
||||
Source directory not found.
|
||||
|
||||
Checked locations:
|
||||
- $DevelopmentPath (development)
|
||||
- $InstalledPath (installed)
|
||||
|
||||
Solution:
|
||||
1. If developing: Ensure you're in the CCS repository
|
||||
2. If installed: Reinstall CCS with: irm ccs.kaitran.ca/install | iex
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create target directories if they don't exist
|
||||
$CommandsDir = Join-Path $TargetDir "commands"
|
||||
$SkillsDir = Join-Path $TargetDir "skills"
|
||||
|
||||
if (-not (Test-Path $CommandsDir)) {
|
||||
New-Item -ItemType Directory -Path $CommandsDir -Force | Out-Null
|
||||
}
|
||||
if (-not (Test-Path $SkillsDir)) {
|
||||
New-Item -ItemType Directory -Path $SkillsDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$InstalledCount = 0
|
||||
$SkippedCount = 0
|
||||
|
||||
# Install commands
|
||||
$SourceCommandsDir = Join-Path $SourceDir "commands"
|
||||
if (Test-Path $SourceCommandsDir) {
|
||||
Write-Host "│ Installing commands..." -ForegroundColor Yellow
|
||||
Get-ChildItem $SourceCommandsDir -Filter "*.md" | ForEach-Object {
|
||||
$CmdName = $_.BaseName
|
||||
$TargetFile = Join-Path $CommandsDir "$CmdName.md"
|
||||
|
||||
if (Test-Path $TargetFile) {
|
||||
Write-Host "│ | [i] Skipping existing command: $CmdName.md" -ForegroundColor Yellow
|
||||
$SkippedCount++
|
||||
} else {
|
||||
try {
|
||||
Copy-Item $_.FullName $TargetFile -ErrorAction Stop
|
||||
Write-Host "│ | [OK] Installed command: $CmdName.md" -ForegroundColor Green
|
||||
$InstalledCount++
|
||||
} catch {
|
||||
Write-Host "│ | [!] Failed to install command: $CmdName.md" -ForegroundColor Red
|
||||
Write-Host "│ Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host "│ [i] No commands directory found" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host "│"
|
||||
|
||||
# Install skills
|
||||
$SourceSkillsDir = Join-Path $SourceDir "skills"
|
||||
if (Test-Path $SourceSkillsDir) {
|
||||
Write-Host "│ Installing skills..." -ForegroundColor Yellow
|
||||
Get-ChildItem $SourceSkillsDir -Directory | ForEach-Object {
|
||||
$SkillName = $_.Name
|
||||
$TargetSkillDir = Join-Path $SkillsDir $SkillName
|
||||
|
||||
if (Test-Path $TargetSkillDir) {
|
||||
Write-Host "│ | [i] Skipping existing skill: $SkillName" -ForegroundColor Yellow
|
||||
$SkippedCount++
|
||||
} else {
|
||||
try {
|
||||
Copy-Item $_.FullName $TargetSkillDir -Recurse -ErrorAction Stop
|
||||
Write-Host "│ | [OK] Installed skill: $SkillName" -ForegroundColor Green
|
||||
$InstalledCount++
|
||||
} catch {
|
||||
Write-Host "│ | [!] Failed to install skill: $SkillName" -ForegroundColor Red
|
||||
Write-Host "│ Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host "│ [i] No skills directory found" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host "[DONE]"
|
||||
Write-Host ""
|
||||
Write-Host "[OK] Installation complete!" -ForegroundColor Green
|
||||
Write-Host " Installed: $InstalledCount items"
|
||||
Write-Host " Skipped: $SkippedCount items (already exist)"
|
||||
Write-Host ""
|
||||
Write-Host "You can now use the /ccs command in Claude CLI for task delegation." -ForegroundColor Cyan
|
||||
Write-Host "Example: /ccs glm /plan 'add user authentication'" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Uninstallation function for commands and skills
|
||||
function Uninstall-CommandsAndSkills {
|
||||
$HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
|
||||
$TargetDir = Join-Path $HomeDir ".claude"
|
||||
$RemovedCount = 0
|
||||
$NotFoundCount = 0
|
||||
|
||||
Write-Host "[Uninstalling CCS Commands and Skills]" -ForegroundColor Cyan
|
||||
Write-Host "│ Target: $TargetDir"
|
||||
Write-Host "│"
|
||||
|
||||
# Check if target directory exists
|
||||
if (-not (Test-Path $TargetDir)) {
|
||||
Write-Host "│"
|
||||
Write-Host "│ [i] Claude directory not found: $TargetDir" -ForegroundColor Gray
|
||||
Write-Host "│ Nothing to uninstall."
|
||||
Write-Host "[DONE]"
|
||||
Write-Host ""
|
||||
Write-Host "[OK] Uninstall complete!" -ForegroundColor Green
|
||||
Write-Host " Removed: 0 items (nothing was installed)"
|
||||
return
|
||||
}
|
||||
|
||||
# Remove commands
|
||||
$CommandsDir = Join-Path $TargetDir "commands"
|
||||
if (Test-Path $CommandsDir) {
|
||||
Write-Host "│ Removing commands..." -ForegroundColor Yellow
|
||||
$CmdFile = Join-Path $CommandsDir "ccs.md"
|
||||
if (Test-Path $CmdFile) {
|
||||
try {
|
||||
Remove-Item $CmdFile -Force -ErrorAction Stop
|
||||
Write-Host "│ | [OK] Removed command: ccs.md" -ForegroundColor Green
|
||||
$RemovedCount++
|
||||
} catch {
|
||||
Write-Host "│ | [!] Failed to remove command: ccs.md" -ForegroundColor Red
|
||||
Write-Host "│ Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
} else {
|
||||
Write-Host "│ | [i] CCS command not found" -ForegroundColor Gray
|
||||
$NotFoundCount++
|
||||
}
|
||||
} else {
|
||||
Write-Host "│ [i] Commands directory not found" -ForegroundColor Gray
|
||||
$NotFoundCount++
|
||||
}
|
||||
|
||||
Write-Host "│"
|
||||
|
||||
# Remove skills
|
||||
$SkillsDir = Join-Path $TargetDir "skills"
|
||||
if (Test-Path $SkillsDir) {
|
||||
Write-Host "│ Removing skills..." -ForegroundColor Yellow
|
||||
$SkillDir = Join-Path $SkillsDir "ccs-delegation"
|
||||
if (Test-Path $SkillDir) {
|
||||
try {
|
||||
Remove-Item $SkillDir -Recurse -Force -ErrorAction Stop
|
||||
Write-Host "│ | [OK] Removed skill: ccs-delegation" -ForegroundColor Green
|
||||
$RemovedCount++
|
||||
} catch {
|
||||
Write-Host "│ | [!] Failed to remove skill: ccs-delegation" -ForegroundColor Red
|
||||
Write-Host "│ Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
} else {
|
||||
Write-Host "│ | [i] CCS skill not found" -ForegroundColor Gray
|
||||
$NotFoundCount++
|
||||
}
|
||||
} else {
|
||||
Write-Host "│ [i] Skills directory not found" -ForegroundColor Gray
|
||||
$NotFoundCount++
|
||||
}
|
||||
|
||||
Write-Host "[DONE]"
|
||||
Write-Host ""
|
||||
Write-Host "[OK] Uninstall complete!" -ForegroundColor Green
|
||||
Write-Host " Removed: $RemovedCount items"
|
||||
Write-Host " Not found: $NotFoundCount items (already removed)"
|
||||
Write-Host ""
|
||||
Write-Host "The /ccs command is no longer available in Claude CLI." -ForegroundColor Cyan
|
||||
Write-Host "To reinstall: ccs --install" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Special case: version command (check BEFORE profile detection)
|
||||
# Check both $ProfileOrFlag and first element of $RemainingArgs
|
||||
$FirstArg = if ($ProfileOrFlag -ne "default") { $ProfileOrFlag } elseif ($RemainingArgs.Count -gt 0) { $RemainingArgs[0] } else { $null }
|
||||
if ($FirstArg -eq "version" -or $FirstArg -eq "--version" -or $FirstArg -eq "-v") {
|
||||
Write-Host "CCS (Claude Code Switch) version $CcsVersion"
|
||||
|
||||
# Show install location
|
||||
$InstallLocation = (Get-Command ccs -ErrorAction SilentlyContinue).Source
|
||||
if ($InstallLocation) {
|
||||
Write-Host "Installed at: $InstallLocation"
|
||||
}
|
||||
|
||||
Write-Host "https://github.com/kaitranntt/ccs"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Special case: help command (check BEFORE profile detection)
|
||||
if ($FirstArg -eq "--help" -or $FirstArg -eq "-h" -or $FirstArg -eq "help") {
|
||||
# Detect and validate Claude CLI for help command
|
||||
$ClaudeCli = Find-ClaudeCli
|
||||
|
||||
if ([string]::IsNullOrEmpty($ClaudeCli)) {
|
||||
Show-ClaudeNotFoundError
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
$null = Test-ClaudeCli -Path $ClaudeCli
|
||||
} catch {
|
||||
Write-ErrorMsg $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
if ($RemainingArgs) {
|
||||
& $ClaudeCli --help @RemainingArgs
|
||||
} else {
|
||||
& $ClaudeCli --help
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
} catch {
|
||||
Write-Host "Error: Failed to execute claude --help" -ForegroundColor Red
|
||||
Write-Host $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Special case: install command (check BEFORE profile detection)
|
||||
if ($FirstArg -eq "--install") {
|
||||
Install-CommandsAndSkills
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Special case: uninstall command (check BEFORE profile detection)
|
||||
if ($FirstArg -eq "--uninstall") {
|
||||
Uninstall-CommandsAndSkills
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
|
||||
if ($ProfileOrFlag -match '^-') {
|
||||
# First arg is a flag → use default profile, keep all args
|
||||
$Profile = "default"
|
||||
# Prepend $ProfileOrFlag to $RemainingArgs (it's actually a flag, not a profile)
|
||||
if ($RemainingArgs) {
|
||||
$RemainingArgs = @($ProfileOrFlag) + $RemainingArgs
|
||||
} else {
|
||||
$RemainingArgs = @($ProfileOrFlag)
|
||||
}
|
||||
} else {
|
||||
# First arg is a profile name
|
||||
$Profile = $ProfileOrFlag
|
||||
# $RemainingArgs already contains correct args (PowerShell handles this)
|
||||
}
|
||||
|
||||
# Special case: "default" profile just runs claude directly (no profile switching)
|
||||
if ($Profile -eq "default") {
|
||||
try {
|
||||
if ($RemainingArgs) {
|
||||
& claude @RemainingArgs
|
||||
} else {
|
||||
& claude
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
} catch {
|
||||
Write-Host "Error: Failed to execute claude" -ForegroundColor Red
|
||||
Write-Host $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Config file location (supports environment variable override)
|
||||
$ConfigFile = if ($env:CCS_CONFIG) {
|
||||
$env:CCS_CONFIG
|
||||
} else {
|
||||
"$env:USERPROFILE\.ccs\config.json"
|
||||
}
|
||||
|
||||
# Check config exists
|
||||
if (-not (Test-Path $ConfigFile)) {
|
||||
Write-ErrorMsg @"
|
||||
Config file not found: $ConfigFile
|
||||
|
||||
Solutions:
|
||||
1. Reinstall CCS:
|
||||
irm ccs.kaitran.ca/install | iex
|
||||
|
||||
2. Or create config manually:
|
||||
New-Item -ItemType Directory -Force -Path '$env:USERPROFILE\.ccs'
|
||||
Set-Content -Path '$env:USERPROFILE\.ccs\config.json' -Value '{
|
||||
"profiles": {
|
||||
"glm": "~/.ccs/glm.settings.json",
|
||||
"default": "~/.claude/settings.json"
|
||||
}
|
||||
}'
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Validate profile name (alphanumeric, dash, underscore only)
|
||||
if ($Profile -notmatch '^[a-zA-Z0-9_-]+$') {
|
||||
Write-ErrorMsg @"
|
||||
Invalid profile name: $Profile
|
||||
|
||||
Use only alphanumeric characters, dash, or underscore.
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Read and parse JSON config
|
||||
try {
|
||||
$ConfigContent = Get-Content $ConfigFile -Raw -ErrorAction Stop
|
||||
$Config = $ConfigContent | ConvertFrom-Json -ErrorAction Stop
|
||||
} catch {
|
||||
Write-ErrorMsg @"
|
||||
Invalid JSON in $ConfigFile
|
||||
|
||||
Fix the JSON syntax or reinstall:
|
||||
irm ccs.kaitran.ca/install | iex
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Validate config has profiles object
|
||||
if (-not $Config.profiles) {
|
||||
Write-ErrorMsg @"
|
||||
Config must have 'profiles' object
|
||||
|
||||
See .ccs.example.json for correct format
|
||||
Or reinstall:
|
||||
irm ccs.kaitran.ca/install | iex
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get settings path for profile
|
||||
$SettingsPath = $Config.profiles.$Profile
|
||||
|
||||
if (-not $SettingsPath) {
|
||||
$AvailableProfiles = ($Config.profiles.PSObject.Properties.Name | ForEach-Object { " - $_" }) -join "`n"
|
||||
Write-ErrorMsg @"
|
||||
Profile '$Profile' not found in $ConfigFile
|
||||
|
||||
Available profiles:
|
||||
$AvailableProfiles
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Path expansion and normalization
|
||||
# 1. Handle Unix-style tilde expansion (~/path -> %USERPROFILE%\path)
|
||||
if ($SettingsPath -match '^~[/\\]') {
|
||||
$SettingsPath = $SettingsPath -replace '^~', $env:USERPROFILE
|
||||
}
|
||||
|
||||
# 2. Expand Windows environment variables (%USERPROFILE%, etc.)
|
||||
$SettingsPath = [System.Environment]::ExpandEnvironmentVariables($SettingsPath)
|
||||
|
||||
# 3. Convert forward slashes to backslashes (Unix path compatibility)
|
||||
$SettingsPath = $SettingsPath -replace '/', '\'
|
||||
|
||||
# Validate settings file exists
|
||||
if (-not (Test-Path $SettingsPath)) {
|
||||
Write-ErrorMsg @"
|
||||
Settings file not found: $SettingsPath
|
||||
|
||||
Solutions:
|
||||
1. Create the settings file for profile '$Profile'
|
||||
2. Update the path in $ConfigFile
|
||||
3. Or reinstall: irm ccs.kaitran.ca/install | iex
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Validate settings file is valid JSON (basic check)
|
||||
try {
|
||||
$SettingsContent = Get-Content $SettingsPath -Raw -ErrorAction Stop
|
||||
$Settings = $SettingsContent | ConvertFrom-Json -ErrorAction Stop
|
||||
} catch {
|
||||
Write-ErrorMsg @"
|
||||
Invalid JSON in $SettingsPath
|
||||
|
||||
Details: $_
|
||||
|
||||
Solutions:
|
||||
1. Validate JSON at https://jsonlint.com
|
||||
2. Or reset to template:
|
||||
Set-Content -Path '$SettingsPath' -Value '{`"env`":{}}`'
|
||||
3. Or reinstall: irm ccs.kaitran.ca/install | iex
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Detect Claude CLI executable
|
||||
$ClaudeCli = Find-ClaudeCli
|
||||
|
||||
if ([string]::IsNullOrEmpty($ClaudeCli)) {
|
||||
Show-ClaudeNotFoundError
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Validate detected path
|
||||
try {
|
||||
$null = Test-ClaudeCli -Path $ClaudeCli
|
||||
} catch {
|
||||
Write-ErrorMsg $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Execute with validated path
|
||||
try {
|
||||
if ($RemainingArgs) {
|
||||
& $ClaudeCli --settings $SettingsPath @RemainingArgs
|
||||
} else {
|
||||
& $ClaudeCli --settings $SettingsPath
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
} catch {
|
||||
Write-Host "Error: Failed to execute claude" -ForegroundColor Red
|
||||
Write-Host $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@kai/ccs",
|
||||
"version": "2.4.0",
|
||||
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"claude",
|
||||
"glm",
|
||||
"ai",
|
||||
"profile",
|
||||
"switch"
|
||||
],
|
||||
"homepage": "https://github.com/kaitranntt/ccs",
|
||||
"bugs": {
|
||||
"url": "https://github.com/kaitranntt/ccs/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/kaitranntt/ccs.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Tam Nhu Tran (Kai)",
|
||||
"email": "kaitranntt@users.noreply.github.com"
|
||||
},
|
||||
"bin": {
|
||||
"ccs": "bin/ccs.js"
|
||||
},
|
||||
"files": [
|
||||
"bin/",
|
||||
"lib/",
|
||||
"config/",
|
||||
"VERSION",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"preferGlobal": true,
|
||||
"scripts": {
|
||||
"test": "bash tests/edge-cases.sh",
|
||||
"prepublishOnly": "node scripts/sync-version.js",
|
||||
"prepack": "node scripts/sync-version.js",
|
||||
"prepare": "node scripts/check-executables.js"
|
||||
}
|
||||
}
|
||||
+27
-10
@@ -58,10 +58,11 @@ echo "New version: $NEW_VERSION"
|
||||
echo ""
|
||||
echo "This will update hardcoded versions in:"
|
||||
echo " 1. VERSION file"
|
||||
echo " 2. ccs (bash executable)"
|
||||
echo " 3. ccs.ps1 (PowerShell executable)"
|
||||
echo " 4. installers/install.sh"
|
||||
echo " 5. installers/install.ps1"
|
||||
echo " 2. lib/ccs (bash executable)"
|
||||
echo " 3. lib/ccs.ps1 (PowerShell executable)"
|
||||
echo " 4. package.json (via sync-version.js)"
|
||||
echo " 5. installers/install.sh"
|
||||
echo " 6. installers/install.ps1"
|
||||
echo ""
|
||||
read -p "Continue? (y/N) " -n 1 -r
|
||||
echo
|
||||
@@ -76,23 +77,23 @@ echo "$NEW_VERSION" > "$VERSION_FILE"
|
||||
echo "✓ Updated VERSION file to $NEW_VERSION"
|
||||
|
||||
# Update ccs (bash executable)
|
||||
CCS_BASH="$CCS_DIR/ccs"
|
||||
CCS_BASH="$CCS_DIR/lib/ccs"
|
||||
if [[ -f "$CCS_BASH" ]]; then
|
||||
sed -i.bak "s/^CCS_VERSION=\".*\"/CCS_VERSION=\"$NEW_VERSION\"/" "$CCS_BASH"
|
||||
rm -f "$CCS_BASH.bak"
|
||||
echo "✓ Updated ccs (bash executable)"
|
||||
echo "✓ Updated lib/ccs (bash executable)"
|
||||
else
|
||||
echo "⚠ ccs not found, skipping"
|
||||
echo "⚠ lib/ccs not found, skipping"
|
||||
fi
|
||||
|
||||
# Update ccs.ps1 (PowerShell executable)
|
||||
CCS_PS1="$CCS_DIR/ccs.ps1"
|
||||
CCS_PS1="$CCS_DIR/lib/ccs.ps1"
|
||||
if [[ -f "$CCS_PS1" ]]; then
|
||||
sed -i.bak "s/^\$CcsVersion = \".*\"/\$CcsVersion = \"$NEW_VERSION\"/" "$CCS_PS1"
|
||||
rm -f "$CCS_PS1.bak"
|
||||
echo "✓ Updated ccs.ps1 (PowerShell executable)"
|
||||
echo "✓ Updated lib/ccs.ps1 (PowerShell executable)"
|
||||
else
|
||||
echo "⚠ ccs.ps1 not found, skipping"
|
||||
echo "⚠ lib/ccs.ps1 not found, skipping"
|
||||
fi
|
||||
|
||||
# Update installers/install.sh
|
||||
@@ -115,5 +116,21 @@ else
|
||||
echo "⚠ installers/install.ps1 not found, skipping"
|
||||
fi
|
||||
|
||||
# Sync version to package.json
|
||||
echo "Syncing version to package.json..."
|
||||
if node "$SCRIPT_DIR/sync-version.js"; then
|
||||
echo "✓ Synced version to package.json"
|
||||
else
|
||||
echo "✗ Error: Failed to sync version to package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Version bumped to $NEW_VERSION"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Review changes: git diff"
|
||||
echo " 2. Commit: git add VERSION package.json lib/ccs lib/ccs.ps1 installers/install.sh installers/install.ps1"
|
||||
echo " 3. Commit: git commit -m \"chore: bump version to $NEW_VERSION\""
|
||||
echo " 4. Tag: git tag v$NEW_VERSION"
|
||||
echo " 5. Push: git push origin main && git push origin v$NEW_VERSION"
|
||||
|
||||
Reference in New Issue
Block a user