Merge remote-tracking branch 'origin/dev' into kai/feat/antigravity-failover

This commit is contained in:
kaitranntt
2026-01-05 13:13:41 -05:00
12 changed files with 206 additions and 11 deletions
+10
View File
@@ -1,3 +1,13 @@
## [7.13.1](https://github.com/kaitranntt/ccs/compare/v7.13.0...v7.13.1) (2026-01-05)
### Bug Fixes
* **cliproxy:** add management_key support for remote proxy auth separation ([0e58d0e](https://github.com/kaitranntt/ccs/commit/0e58d0e8b7fd07004990e99fbdc6a080380c0304))
* **cliproxy:** add missing kiro/ghcp provider mappings in remote-auth-fetcher ([dea0e87](https://github.com/kaitranntt/ccs/commit/dea0e872bd529b2c6f825dc1d9e901d0896b7f41))
* **cliproxy:** extract unique accountId from token filename for Kiro/GHCP ([7bb7ccc](https://github.com/kaitranntt/ccs/commit/7bb7ccc27fe0d9885c4dd7f23de664e2c8b4866f)), closes [#258](https://github.com/kaitranntt/ccs/issues/258)
* **cliproxy:** proactive token refresh to prevent UND_ERR_SOCKET ([a6a653f](https://github.com/kaitranntt/ccs/commit/a6a653f14580888bcdccbf6b83b90d41b6b52136)), closes [#256](https://github.com/kaitranntt/ccs/issues/256)
* **validation:** add Windows reserved name validation and version format edge cases ([ae1847d](https://github.com/kaitranntt/ccs/commit/ae1847d9011c8bff9caff206cf1d7082c61faf40))
## [7.13.0](https://github.com/kaitranntt/ccs/compare/v7.12.2...v7.13.0) (2026-01-03)
### Features
+3 -1
View File
@@ -268,7 +268,9 @@ bun run test:unit # Unit tests
### Local Development
```bash
bun run dev # Build + start config server (http://localhost:3000)
./scripts/dev-install.sh # Build, pack, install globally
bun run dev:symlink # Symlink global 'ccs' → dev dist/ccs.js (fast iteration)
bun run dev:unlink # Restore original global ccs
./scripts/dev-install.sh # Build, pack, install globally (full install)
rm -rf ~/.ccs # Clean environment
```
+15 -1
View File
@@ -265,8 +265,22 @@ cd ccs
# Create feature branch
git checkout -b your-feature-name
# Option 1: Test with built binary
# Test locally with ./dist/ccs.js
# Option 2: Symlink for seamless testing (recommended)
bun run build
bun run dev:symlink # Symlinks global 'ccs' to dev version
# Now 'ccs' command uses your dev changes!
# Make changes
# Test locally with ./ccs
# Test with: ccs <command>
# When done developing:
bun run dev:unlink # Restores original global ccs
# Run tests
# Test with: ccs <command>
# Run tests
bun run test # All tests
+1 -1
View File
@@ -5,6 +5,6 @@
"ANTHROPIC_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemini-3-flash-preview"
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemini-claude-sonnet-4-5"
}
}
+4 -1
View File
@@ -154,7 +154,10 @@ process.stdin.on('error', () => {
*/
function isCliAvailable(cmd) {
try {
const result = spawnSync('which', [cmd], {
const isWindows = process.platform === 'win32';
const whichCmd = isWindows ? 'where.exe' : 'which';
const result = spawnSync(whichCmd, [cmd], {
encoding: 'utf8',
timeout: 2000,
stdio: ['pipe', 'pipe', 'pipe'],
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kaitranntt/ccs",
"version": "7.13.0-dev.3",
"version": "7.13.1-dev.3",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"keywords": [
"cli",
@@ -72,6 +72,8 @@
"test:npm": "bun test tests/npm/",
"test:native": "bash tests/native/unix/edge-cases.sh",
"dev": "bun run build:server && bun dist/ccs.js config --dev",
"dev:symlink": "bash scripts/dev-symlink.sh",
"dev:unlink": "bash scripts/dev-symlink.sh --restore",
"ui:build": "cd ui && bun run build",
"ui:preview": "cd ui && bun run preview",
"ui:validate": "cd ui && bun run validate",
+115
View File
@@ -0,0 +1,115 @@
#!/bin/bash
# CCS Dev Symlink Setup
# Creates symlinks for testing dev version with 'ccs' command
#
# Usage: ./scripts/dev-symlink.sh [--restore]
#
# Without --restore: Creates symlink from global 'ccs' to dist/ccs.js
# With --restore: Restores original global 'ccs' from backup
set -euo pipefail
RESTORE=false
# Parse arguments
for arg in "$@"; do
case $arg in
--restore) RESTORE=true ;;
-h|--help)
echo "Usage: $0 [--restore]"
echo ""
echo "Create symlink for dev testing:"
echo " $0"
echo ""
echo "Restore original global ccs:"
echo " $0 --restore"
exit 0
;;
*)
echo "[X] Unknown option: $arg"
echo "Use --help for usage"
exit 1
;;
esac
done
# Get to the right directory
cd "$(dirname "$0")/.."
# Check if dist/ccs.js exists
if [ ! -f "dist/ccs.js" ]; then
echo "[X] ERROR: dist/ccs.js not found. Run 'bun run build' first."
exit 1
fi
# Get absolute path to dev ccs.js
DEV_CCS_PATH="$(pwd)/dist/ccs.js"
# Find global ccs installation
GLOBAL_CCS_PATH=$(which ccs 2>/dev/null || true)
if [ -z "$GLOBAL_CCS_PATH" ]; then
echo "[X] ERROR: No global 'ccs' installation found."
echo "Install CCS globally first: npm install -g @kaitranntt/ccs"
exit 1
fi
echo "[i] Found global ccs at: $GLOBAL_CCS_PATH"
if [ "$RESTORE" = true ]; then
# Restore original ccs from backup
BACKUP_PATH="${GLOBAL_CCS_PATH}.backup-dev"
if [ ! -f "$BACKUP_PATH" ] && [ ! -L "$BACKUP_PATH" ]; then
echo "[X] ERROR: No backup found at $BACKUP_PATH"
echo "Cannot restore - backup may have been deleted"
exit 1
fi
echo "[i] Restoring original ccs from backup..."
rm -f "$GLOBAL_CCS_PATH"
if [ -L "$BACKUP_PATH" ]; then
# Restore symlink
cp -P "$BACKUP_PATH" "$GLOBAL_CCS_PATH"
else
# Restore regular file
cp "$BACKUP_PATH" "$GLOBAL_CCS_PATH"
fi
chmod +x "$GLOBAL_CCS_PATH"
rm -f "$BACKUP_PATH"
echo "[OK] Restored original global ccs"
echo "Run 'ccs --version' to verify"
exit 0
fi
# Check if already symlinked to our dev version
if [ -L "$GLOBAL_CCS_PATH" ]; then
CURRENT_TARGET=$(readlink "$GLOBAL_CCS_PATH" 2>/dev/null || true)
if [ "$CURRENT_TARGET" = "$DEV_CCS_PATH" ]; then
echo "[OK] Already symlinked to dev version"
exit 0
fi
fi
# Create backup of current global ccs
BACKUP_PATH="${GLOBAL_CCS_PATH}.backup-dev"
if [ -f "$BACKUP_PATH" ] || [ -L "$BACKUP_PATH" ]; then
echo "[i] Backup already exists, skipping backup creation"
else
echo "[i] Creating backup of current global ccs..."
cp -P "$GLOBAL_CCS_PATH" "$BACKUP_PATH"
echo "[OK] Backup created at: $BACKUP_PATH"
fi
# Create symlink
echo "[i] Creating symlink to dev version..."
rm -f "$GLOBAL_CCS_PATH"
ln -s "$DEV_CCS_PATH" "$GLOBAL_CCS_PATH"
echo "[OK] Symlinked global 'ccs' to dev version"
echo ""
echo "Now you can test dev changes with: ccs <command>"
echo "To restore original: $0 --restore"
echo ""
echo "Test with: ccs --version"
+4 -1
View File
@@ -5,7 +5,7 @@
* Extracted from api-command.ts for reuse and testability.
*/
import { isReservedName } from '../../config/reserved-names';
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
/**
* Validate API profile name
@@ -24,6 +24,9 @@ export function validateApiName(name: string): string | null {
if (isReservedName(name)) {
return `'${name}' is a reserved name`;
}
if (isWindowsReservedName(name)) {
return `'${name}' is a Windows reserved device name and cannot be used`;
}
return null;
}
+2 -2
View File
@@ -84,10 +84,10 @@ export async function checkLatestVersion(): Promise<LatestVersionResult> {
}
/**
* Validate version format
* Validate version format (supports X.Y.Z or X.Y.Z-N suffix)
*/
export function isValidVersionFormat(version: string): boolean {
return /^\d+\.\d+\.\d+$/.test(version);
return /^\d+\.\d+\.\d+(-\d+)?$/.test(version);
}
/**
+4 -1
View File
@@ -9,7 +9,7 @@ import * as os from 'os';
import * as path from 'path';
import { CLIProxyProfileName } from '../../auth/profile-detector';
import { CLIProxyProvider } from '../types';
import { isReservedName } from '../../config/reserved-names';
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { deleteConfigForPort } from '../config-generator';
import { deleteSessionLockForPort } from '../session-tracker';
@@ -58,6 +58,9 @@ export function validateProfileName(name: string): string | null {
if (isReservedName(name)) {
return `'${name}' is a reserved name`;
}
if (isWindowsReservedName(name)) {
return `'${name}' is a Windows reserved device name and cannot be used`;
}
return null;
}
+4 -2
View File
@@ -732,13 +732,15 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
const installIdx = args.indexOf('--install');
if (installIdx !== -1) {
const version = args[installIdx + 1];
let version = args[installIdx + 1];
if (!version || version.startsWith('-')) {
console.error(fail('Missing version argument for --install'));
console.error(' Usage: ccs cliproxy --install <version>');
console.error(' Example: ccs cliproxy --install 6.5.53');
console.error(' Example: ccs cliproxy --install 6.6.80-0');
process.exit(1);
}
// Strip leading 'v' prefix and whitespace (user may type " v6.6.80-0 ")
version = version.trim().replace(/^v/, '');
await handleInstallVersion(version, verbose);
return;
}
+41
View File
@@ -19,6 +19,35 @@ export const RESERVED_PROFILE_NAMES = [
export type ReservedProfileName = (typeof RESERVED_PROFILE_NAMES)[number];
/**
* Windows reserved device names - cannot be used as filenames on Windows.
* Case-insensitive on Windows filesystem.
*/
export const WINDOWS_RESERVED_NAMES = [
'CON',
'PRN',
'AUX',
'NUL',
'COM1',
'COM2',
'COM3',
'COM4',
'COM5',
'COM6',
'COM7',
'COM8',
'COM9',
'LPT1',
'LPT2',
'LPT3',
'LPT4',
'LPT5',
'LPT6',
'LPT7',
'LPT8',
'LPT9',
] as const;
/**
* Check if a name is reserved and cannot be used for user profiles.
* @param name - The profile name to check
@@ -28,6 +57,18 @@ export function isReservedName(name: string): boolean {
return RESERVED_PROFILE_NAMES.includes(name.toLowerCase() as ReservedProfileName);
}
/**
* Check if a name is a Windows reserved device name.
* These cause filesystem errors on Windows systems.
* @param name - The name to check
* @returns true if the name is a Windows reserved name
*/
export function isWindowsReservedName(name: string): boolean {
return WINDOWS_RESERVED_NAMES.includes(
name.toUpperCase() as (typeof WINDOWS_RESERVED_NAMES)[number]
);
}
/**
* Validate a profile name and throw if reserved.
* @param name - The profile name to validate