From ae1847d9011c8bff9caff206cf1d7082c61faf40 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 00:00:58 -0500 Subject: [PATCH 1/9] fix(validation): add Windows reserved name validation and version format edge cases - Fixed version regex to accept -N suffix (e.g., 6.6.80-0) - Added .trim() to version input for whitespace handling - Added Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) validation - Updated binary-service, variant-service, and validation-service with checks --- src/api/services/validation-service.ts | 5 ++- src/cliproxy/services/binary-service.ts | 4 +-- src/cliproxy/services/variant-service.ts | 5 ++- src/commands/cliproxy-command.ts | 6 ++-- src/config/reserved-names.ts | 41 ++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/api/services/validation-service.ts b/src/api/services/validation-service.ts index 0973ca13..e9b830ce 100644 --- a/src/api/services/validation-service.ts +++ b/src/api/services/validation-service.ts @@ -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; } diff --git a/src/cliproxy/services/binary-service.ts b/src/cliproxy/services/binary-service.ts index c044aca4..ff769e65 100644 --- a/src/cliproxy/services/binary-service.ts +++ b/src/cliproxy/services/binary-service.ts @@ -84,10 +84,10 @@ export async function checkLatestVersion(): Promise { } /** - * 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); } /** diff --git a/src/cliproxy/services/variant-service.ts b/src/cliproxy/services/variant-service.ts index 58e4e05d..41a5dcfb 100644 --- a/src/cliproxy/services/variant-service.ts +++ b/src/cliproxy/services/variant-service.ts @@ -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; } diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index e0e93e35..bce8a3b9 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -619,13 +619,15 @@ export async function handleCliproxyCommand(args: string[]): Promise { 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 '); - 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; } diff --git a/src/config/reserved-names.ts b/src/config/reserved-names.ts index 25262265..7e53f4c3 100644 --- a/src/config/reserved-names.ts +++ b/src/config/reserved-names.ts @@ -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 From 001a1890ca85c10d38541d1e95d5b75e7f67f139 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Jan 2026 05:02:06 +0000 Subject: [PATCH 2/9] chore(release): 7.13.0-dev.4 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a3800ff5..75aa643e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.13.0-dev.3", + "version": "7.13.0-dev.4", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From 42e6ed72da30aebc98db3701903ae2f284c5c290 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 5 Jan 2026 05:09:23 +0000 Subject: [PATCH 3/9] chore(release): 7.13.1 [skip ci] ## [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)) --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c5e06b..f734c5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index 75aa643e..2efabc7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.13.0-dev.4", + "version": "7.13.1", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From 981cef82119359384e7385aff1791b0afa4f4fc1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 3 Jan 2026 01:09:49 +0000 Subject: [PATCH 4/9] feat(dev): add symlink setup for testing dev version Add dev:symlink and dev:unlink scripts to enable seamless testing of development changes using the global 'ccs' command without needing to pack/install globally each time. - scripts/dev-symlink.sh: New script that safely creates symlinks from global ccs to dev dist/ccs.js with backup/restore functionality - package.json: Added dev:symlink and dev:unlink npm scripts - CONTRIBUTING.md: Updated development setup documentation with symlink workflow option This improves developer experience by allowing immediate testing of changes with 'ccs ' instead of './dist/ccs.js '. --- CLAUDE.md | 4 +- CONTRIBUTING.md | 16 +++++- package.json | 2 + scripts/dev-symlink.sh | 115 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100755 scripts/dev-symlink.sh diff --git a/CLAUDE.md b/CLAUDE.md index e85547cf..1f41c7bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe0a7c50..e45bcb1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 + +# When done developing: +bun run dev:unlink # Restores original global ccs + +# Run tests +# Test with: ccs # Run tests bun run test # All tests diff --git a/package.json b/package.json index 2efabc7e..e95ba759 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/dev-symlink.sh b/scripts/dev-symlink.sh new file mode 100755 index 00000000..7f6ada59 --- /dev/null +++ b/scripts/dev-symlink.sh @@ -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 " +echo "To restore original: $0 --restore" +echo "" +echo "Test with: ccs --version" From 3a40a0d015c42c25d155153cb124da8e6518305b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Jan 2026 07:47:13 +0000 Subject: [PATCH 5/9] chore(release): 7.13.1-dev.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e95ba759..1f3c0d63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.13.1", + "version": "7.13.1-dev.1", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From c9cdfd98792cc6d272aa22e2317a3a3bb32105de Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 11:40:32 -0500 Subject: [PATCH 6/9] feat(agy): promote gemini-claude-sonnet-4-5 as default Haiku model Closes #270 --- config/base-agy.settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/base-agy.settings.json b/config/base-agy.settings.json index 0b08be0b..398841bc 100644 --- a/config/base-agy.settings.json +++ b/config/base-agy.settings.json @@ -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" } } From 96ef62f4ce659dbb1bf33c44739ab638c105a3d6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Jan 2026 16:44:50 +0000 Subject: [PATCH 7/9] chore(release): 7.13.1-dev.2 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f3c0d63..66e1f48c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.13.1-dev.1", + "version": "7.13.1-dev.2", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From e03d9b77437575a39af0dca14c2a6b5967ae4f09 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 11:59:59 -0500 Subject: [PATCH 8/9] fix(websearch): use 'where' command on Windows for CLI detection Fixes #273 The websearch-transformer hook used hardcoded 'which' command which doesn't exist on Windows. Now uses process.platform detection to choose 'where' on Windows and 'which' on Unix systems. --- lib/hooks/websearch-transformer.cjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/hooks/websearch-transformer.cjs b/lib/hooks/websearch-transformer.cjs index b0270543..7a2af2a6 100644 --- a/lib/hooks/websearch-transformer.cjs +++ b/lib/hooks/websearch-transformer.cjs @@ -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'], From 6ff5c79fe1123ebf30bbda5fbc1bfaa522d16845 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Jan 2026 17:41:00 +0000 Subject: [PATCH 9/9] chore(release): 7.13.1-dev.3 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66e1f48c..f9657cd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.13.1-dev.2", + "version": "7.13.1-dev.3", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli",