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