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/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/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" } } 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'], diff --git a/package.json b/package.json index a3800ff5..f9657cd7 100644 --- a/package.json +++ b/package.json @@ -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", 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" 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 16fc1b18..c8af0f25 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -732,13 +732,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