diff --git a/src/commands/api-command/create-command.ts b/src/commands/api-command/create-command.ts index 9d9da93f..9583b8f8 100644 --- a/src/commands/api-command/create-command.ts +++ b/src/commands/api-command/create-command.ts @@ -491,7 +491,9 @@ export async function handleApiCreateCommand(args: string[]): Promise { if (hasClaudeMappings) { details += `\nLongCtx: ${ - shouldEnableClaudeLongContext ? 'compatible Claude mappings use [1m]' : 'standard Claude context' + shouldEnableClaudeLongContext + ? 'compatible Claude mappings use [1m]' + : 'standard Claude context' }`; } diff --git a/src/commands/api-command/help.ts b/src/commands/api-command/help.ts index b2fb7f94..37ddfccf 100644 --- a/src/commands/api-command/help.ts +++ b/src/commands/api-command/help.ts @@ -8,6 +8,8 @@ import { import { color, dim, fail, header, initUI, subheader } from '../../utils/ui'; import { sanitizeHelpText } from './shared'; +type HelpWriter = (line: string) => void; + function renderPresetHelpLine(preset: ProviderPreset, idWidth: number): string { const presetId = sanitizeHelpText(preset.id) || 'unknown'; const paddedId = presetId.padEnd(idWidth); @@ -16,7 +18,7 @@ function renderPresetHelpLine(preset: ProviderPreset, idWidth: number): string { return ` ${color(paddedId, 'command')} ${presetName} - ${presetDescription}`; } -export async function showApiCommandHelp(): Promise { +export async function showApiCommandHelp(writeLine: HelpWriter = console.log): Promise { await initUI(); const presetIds = getPresetIds() .map((id) => sanitizeHelpText(id)) @@ -25,112 +27,108 @@ export async function showApiCommandHelp(): Promise { const presetAliases = getPresetAliases(); const presetIdWidth = Math.max(0, ...presetIds.map((id) => id.length)) + 2; - console.log(header('CCS API Management')); - console.log(''); - console.log(subheader('Usage')); - console.log(` ${color('ccs api', 'command')} [options]`); - console.log(''); - console.log(subheader('Commands')); - console.log(` ${color('create [name]', 'command')} Create new API profile (interactive)`); - console.log(` ${color('list', 'command')} List all API profiles`); - console.log( + writeLine(header('CCS API Management')); + writeLine(''); + writeLine(subheader('Usage')); + writeLine(` ${color('ccs api', 'command')} [options]`); + writeLine(''); + writeLine(subheader('Commands')); + writeLine(` ${color('create [name]', 'command')} Create new API profile (interactive)`); + writeLine(` ${color('list', 'command')} List all API profiles`); + writeLine( ` ${color('discover', 'command')} Discover orphan *.settings.json and register` ); - console.log(` ${color('copy ', 'command')} Duplicate API profile settings + config`); - console.log( + writeLine(` ${color('copy ', 'command')} Duplicate API profile settings + config`); + writeLine( ` ${color('export ', 'command')} Export profile bundle for cross-device transfer` ); - console.log( - ` ${color('import ', 'command')} Import profile bundle and register profile` - ); - console.log(` ${color('remove ', 'command')} Remove an API profile`); - console.log(''); - console.log(subheader('Options')); - console.log( + writeLine(` ${color('import ', 'command')} Import profile bundle and register profile`); + writeLine(` ${color('remove ', 'command')} Remove an API profile`); + writeLine(''); + writeLine(subheader('Options')); + writeLine( ` ${color('--preset ', 'command')} Use provider preset (${presetIds.join(', ')})` ); - console.log( + writeLine( ` ${color('--cliproxy-provider ', 'command')} Use routed CLIProxy provider (${cliproxyProviderIds.join(', ')})` ); - console.log(` ${color('--base-url ', 'command')} API base URL (create)`); - console.log(` ${color('--api-key ', 'command')} API key (create)`); - console.log(` ${color('--model ', 'command')} Default model (create)`); - console.log( + writeLine(` ${color('--base-url ', 'command')} API base URL (create)`); + writeLine(` ${color('--api-key ', 'command')} API key (create)`); + writeLine(` ${color('--model ', 'command')} Default model (create)`); + writeLine( ` ${color('--1m / --no-1m', 'command')} Write or clear [1m] on compatible Claude mappings` ); - console.log( + writeLine( ` ${color('--target ', 'command')} Default target: claude or droid (create)` ); - console.log(` ${color('--register', 'command')} Register discovered orphan settings`); - console.log(` ${color('--json', 'command')} JSON output for discover command`); - console.log(` ${color('--out ', 'command')} Export bundle output path`); - console.log(` ${color('--include-secrets', 'command')} Include token in export bundle`); - console.log(` ${color('--name ', 'command')} Override profile name during import`); - console.log( + writeLine(` ${color('--register', 'command')} Register discovered orphan settings`); + writeLine(` ${color('--json', 'command')} JSON output for discover command`); + writeLine(` ${color('--out ', 'command')} Export bundle output path`); + writeLine(` ${color('--include-secrets', 'command')} Include token in export bundle`); + writeLine(` ${color('--name ', 'command')} Override profile name during import`); + writeLine( ` ${color('--force', 'command')} Overwrite existing or bypass validation (create/discover/copy/import)` ); - console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts`); - console.log(''); - console.log(subheader('Provider Presets')); - PROVIDER_PRESETS.forEach((preset) => console.log(renderPresetHelpLine(preset, presetIdWidth))); + writeLine(` ${color('--yes, -y', 'command')} Skip confirmation prompts`); + writeLine(''); + writeLine(subheader('Provider Presets')); + PROVIDER_PRESETS.forEach((preset) => writeLine(renderPresetHelpLine(preset, presetIdWidth))); Object.entries(presetAliases).forEach(([alias, canonical]) => { const safeAlias = sanitizeHelpText(alias); const safeCanonical = sanitizeHelpText(canonical); - console.log( - ` ${dim(`Legacy alias: --preset ${safeAlias} (auto-mapped to ${safeCanonical})`)}` - ); + writeLine(` ${dim(`Legacy alias: --preset ${safeAlias} (auto-mapped to ${safeCanonical})`)}`); }); - console.log(''); - console.log(subheader('Examples')); - console.log(` ${dim('# Interactive wizard')}`); - console.log(` ${color('ccs api create', 'command')}`); - console.log(''); - console.log(` ${dim('# Quick setup with preset')}`); - console.log(` ${color('ccs api create --preset anthropic', 'command')}`); - console.log( + writeLine(''); + writeLine(subheader('Examples')); + writeLine(` ${dim('# Interactive wizard')}`); + writeLine(` ${color('ccs api create', 'command')}`); + writeLine(''); + writeLine(` ${dim('# Quick setup with preset')}`); + writeLine(` ${color('ccs api create --preset anthropic', 'command')}`); + writeLine( ` ${color('ccs api create --preset anthropic --1m', 'command')} ${dim('# explicit Claude [1m] opt-in')}` ); - console.log(` ${color('ccs api create --preset openrouter', 'command')}`); - console.log(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`); - console.log(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`); - console.log(` ${color('ccs api create --preset glm', 'command')}`); - console.log(''); - console.log(subheader('Claude Long Context')); - console.log(` ${dim('Plain Claude model IDs stay on standard context by default.')}`); - console.log( + writeLine(` ${color('ccs api create --preset openrouter', 'command')}`); + writeLine(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`); + writeLine(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`); + writeLine(` ${color('ccs api create --preset glm', 'command')}`); + writeLine(''); + writeLine(subheader('Claude Long Context')); + writeLine(` ${dim('Plain Claude model IDs stay on standard context by default.')}`); + writeLine( ` ${dim('Use --1m during create to append [1m] to compatible Claude mappings, or --no-1m to force plain IDs.')}` ); - console.log( + writeLine( ` ${dim('CCS controls only the saved [1m] suffix. Provider pricing/entitlement stay upstream, and some accounts can still return 429 for long-context requests.')}` ); - console.log(''); - console.log(` ${dim('# Create routed profile from existing CLIProxy provider config')}`); - console.log(` ${color('ccs api create --cliproxy-provider gemini', 'command')}`); - console.log( + writeLine(''); + writeLine(` ${dim('# Create routed profile from existing CLIProxy provider config')}`); + writeLine(` ${color('ccs api create --cliproxy-provider gemini', 'command')}`); + writeLine( ` ${color('ccs api create gemini-droid --cliproxy-provider gemini --target droid', 'command')}` ); - console.log(''); - console.log(` ${dim('# Create with name')}`); - console.log(` ${color('ccs api create myapi', 'command')}`); - console.log(` ${color('ccs api create mydroid --preset glm --target droid', 'command')}`); - console.log(''); - console.log(` ${dim('# Remove API profile')}`); - console.log(` ${color('ccs api remove myapi', 'command')}`); - console.log(''); - console.log(` ${dim('# Discover and register orphan settings files')}`); - console.log(` ${color('ccs api discover', 'command')}`); - console.log(` ${color('ccs api discover --register', 'command')}`); - console.log(''); - console.log(` ${dim('# Duplicate an existing API profile')}`); - console.log(` ${color('ccs api copy glm glm-backup', 'command')}`); - console.log(''); - console.log(` ${dim('# Export and import across devices')}`); - console.log(` ${color('ccs api export glm --out ./glm.ccs-profile.json', 'command')}`); - console.log(` ${color('ccs api import ./glm.ccs-profile.json', 'command')}`); - console.log(''); - console.log(` ${dim('# Show all API profiles')}`); - console.log(` ${color('ccs api list', 'command')}`); - console.log(''); + writeLine(''); + writeLine(` ${dim('# Create with name')}`); + writeLine(` ${color('ccs api create myapi', 'command')}`); + writeLine(` ${color('ccs api create mydroid --preset glm --target droid', 'command')}`); + writeLine(''); + writeLine(` ${dim('# Remove API profile')}`); + writeLine(` ${color('ccs api remove myapi', 'command')}`); + writeLine(''); + writeLine(` ${dim('# Discover and register orphan settings files')}`); + writeLine(` ${color('ccs api discover', 'command')}`); + writeLine(` ${color('ccs api discover --register', 'command')}`); + writeLine(''); + writeLine(` ${dim('# Duplicate an existing API profile')}`); + writeLine(` ${color('ccs api copy glm glm-backup', 'command')}`); + writeLine(''); + writeLine(` ${dim('# Export and import across devices')}`); + writeLine(` ${color('ccs api export glm --out ./glm.ccs-profile.json', 'command')}`); + writeLine(` ${color('ccs api import ./glm.ccs-profile.json', 'command')}`); + writeLine(''); + writeLine(` ${dim('# Show all API profiles')}`); + writeLine(` ${color('ccs api list', 'command')}`); + writeLine(''); } export async function showUnknownApiCommand(command: string): Promise { diff --git a/src/commands/api-command/index.ts b/src/commands/api-command/index.ts index 79e6b560..2d9fe526 100644 --- a/src/commands/api-command/index.ts +++ b/src/commands/api-command/index.ts @@ -10,22 +10,68 @@ import { handleApiRemoveCommand } from './remove-command'; export { parseApiCommandArgs } from './shared'; -const API_COMMAND_ROUTES: readonly NamedCommandRoute[] = [ - { name: 'create', handle: handleApiCreateCommand }, - { name: 'list', handle: handleApiListCommand }, - { name: 'discover', handle: handleApiDiscoverCommand }, - { name: 'copy', handle: handleApiCopyCommand }, - { name: 'export', handle: handleApiExportCommand }, - { name: 'import', handle: handleApiImportCommand }, - { name: 'remove', aliases: ['delete', 'rm'], handle: handleApiRemoveCommand }, -]; +type ApiCommandHandler = (args: string[]) => Promise; +type ApiCommandHelpHandler = () => Promise; +type ApiCommandUnknownHandler = (command: string) => Promise; + +export interface ApiCommandDependencies { + help: ApiCommandHelpHandler; + unknown: ApiCommandUnknownHandler; + create: ApiCommandHandler; + list: ApiCommandHandler; + discover: ApiCommandHandler; + copy: ApiCommandHandler; + export: ApiCommandHandler; + import: ApiCommandHandler; + remove: ApiCommandHandler; +} + +const DEFAULT_API_COMMAND_DEPENDENCIES: ApiCommandDependencies = { + help: showApiCommandHelp, + unknown: showUnknownApiCommand, + create: handleApiCreateCommand, + list: handleApiListCommand, + discover: handleApiDiscoverCommand, + copy: handleApiCopyCommand, + export: handleApiExportCommand, + import: handleApiImportCommand, + remove: handleApiRemoveCommand, +}; + +function createApiCommandRoutes( + dependencies: ApiCommandDependencies +): readonly NamedCommandRoute[] { + return [ + { name: 'create', handle: dependencies.create }, + { name: 'list', handle: dependencies.list }, + { name: 'discover', handle: dependencies.discover }, + { name: 'copy', handle: dependencies.copy }, + { name: 'export', handle: dependencies.export }, + { name: 'import', handle: dependencies.import }, + { name: 'remove', aliases: ['delete', 'rm'], handle: dependencies.remove }, + ]; +} + +export function createApiCommandHandler( + overrides: Partial = {} +): (args: string[]) => Promise { + const dependencies: ApiCommandDependencies = { + ...DEFAULT_API_COMMAND_DEPENDENCIES, + ...overrides, + }; + const routes = createApiCommandRoutes(dependencies); + + return async (args: string[]) => { + await dispatchNamedCommand({ + args, + routes, + onHelp: dependencies.help, + onUnknown: dependencies.unknown, + allowEmptyHelp: true, + }); + }; +} export async function handleApiCommand(args: string[]): Promise { - await dispatchNamedCommand({ - args, - routes: API_COMMAND_ROUTES, - onHelp: showApiCommandHelp, - onUnknown: showUnknownApiCommand, - allowEmptyHelp: true, - }); + await createApiCommandHandler()(args); } diff --git a/src/commands/api-command/shared.ts b/src/commands/api-command/shared.ts index 2e725dd0..06ae02c1 100644 --- a/src/commands/api-command/shared.ts +++ b/src/commands/api-command/shared.ts @@ -294,9 +294,10 @@ export function applyClaudeExtendedContextPreference( continue; } - nextModels[key] = enabled && likelySupportsClaudeExtendedContext(value) - ? applyExtendedContextSuffix(value) - : stripExtendedContextSuffix(value); + nextModels[key] = + enabled && likelySupportsClaudeExtendedContext(value) + ? applyExtendedContextSuffix(value) + : stripExtendedContextSuffix(value); } return nextModels; diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 80e203a8..e3bef4e6 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -5,6 +5,8 @@ import { isUnifiedMode } from '../config/unified-config-loader'; import { getCcsDir, getCcsDirSource } from '../utils/config-manager'; import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager'; +type HelpWriter = (line: string) => void; + // Get version from package.json (same as version-command.ts) const VERSION = JSON.parse( fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8') @@ -19,28 +21,33 @@ const VERSION = JSON.parse( * * command Description */ -function printMajorSection(title: string, subtitles: string[], items: [string, string][]): void { +function printMajorSection( + title: string, + subtitles: string[], + items: [string, string][], + writeLine: HelpWriter = console.log +): void { // Section header with ═══ borders - console.log(sectionHeader(title)); + writeLine(sectionHeader(title)); // Subtitles on separate lines (dim) for (const subtitle of subtitles) { - console.log(` ${dim(subtitle)}`); + writeLine(` ${dim(subtitle)}`); } // Empty line before items - console.log(''); + writeLine(''); // Calculate max command length for alignment const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length)); for (const [cmd, desc] of items) { const paddedCmd = cmd.padEnd(maxCmdLen + 2); - console.log(` ${color(paddedCmd, 'command')} ${desc}`); + writeLine(` ${color(paddedCmd, 'command')} ${desc}`); } // Extra spacing after section - console.log(''); + writeLine(''); } /** @@ -49,20 +56,24 @@ function printMajorSection(title: string, subtitles: string[], items: [string, s * Title (context): * command Description */ -function printSubSection(title: string, items: [string, string][]): void { +function printSubSection( + title: string, + items: [string, string][], + writeLine: HelpWriter = console.log +): void { // Sub-section header (colored, no borders) - console.log(subheader(`${title}:`)); + writeLine(subheader(`${title}:`)); // Calculate max command length for alignment const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length)); for (const [cmd, desc] of items) { const paddedCmd = cmd.padEnd(maxCmdLen + 2); - console.log(` ${color(paddedCmd, 'command')} ${desc}`); + writeLine(` ${color(paddedCmd, 'command')} ${desc}`); } // Spacing after section - console.log(''); + writeLine(''); } /** @@ -71,24 +82,28 @@ function printSubSection(title: string, items: [string, string][]): void { * Title: * Label: path */ -function printConfigSection(title: string, items: [string, string][]): void { - console.log(subheader(`${title}:`)); +function printConfigSection( + title: string, + items: [string, string][], + writeLine: HelpWriter = console.log +): void { + writeLine(subheader(`${title}:`)); // Calculate max label length for alignment const maxLabelLen = Math.max(...items.map(([label]) => label.length)); for (const [label, path] of items) { const paddedLabel = label.padEnd(maxLabelLen); - console.log(` ${paddedLabel} ${color(path, 'path')}`); + writeLine(` ${paddedLabel} ${color(path, 'path')}`); } - console.log(''); + writeLine(''); } /** * Display comprehensive help information for CCS (Claude Code Switch) */ -export async function handleHelpCommand(): Promise { +export async function handleHelpCommand(writeLine: HelpWriter = console.log): Promise { // Initialize UI (if not already) await initUI(); @@ -103,24 +118,24 @@ Claude Code Profile & Model Switcher Run ${color('ccs config', 'command')} for web dashboard`.trim(); - console.log( + writeLine( box(logo, { padding: 1, borderStyle: 'round', titleAlignment: 'center', }) ); - console.log(''); + writeLine(''); // Resolve display path for dynamic sections const [dirSource] = getCcsDirSource(); const dirDisplay = dirSource === 'default' ? '~/.ccs' : getCcsDir(); // Usage section - console.log(subheader('Usage:')); - console.log(` ${color('ccs', 'command')} [profile] [claude-args...]`); - console.log(` ${color('ccs', 'command')} [flags]`); - console.log(''); + writeLine(subheader('Usage:')); + writeLine(` ${color('ccs', 'command')} [profile] [claude-args...]`); + writeLine(` ${color('ccs', 'command')} [flags]`); + writeLine(''); // ═══════════════════════════════════════════════════════════════════════════ // MAJOR SECTION 1: API Key Profiles @@ -152,7 +167,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs api import ', 'Import profile bundle'], ['ccs api remove', 'Remove an API profile'], ['ccs api list', 'List all API profiles'], - ] + ], + writeLine ); // ═══════════════════════════════════════════════════════════════════════════ @@ -176,7 +192,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs auth default ', 'Set default profile'], ['ccs auth reset-default', 'Restore original CCS default'], ['ccs cliproxy auth claude', 'Alternative: authenticate Claude account pool via CLIProxy'], - ] + ], + writeLine ); // ═══════════════════════════════════════════════════════════════════════════ @@ -232,7 +249,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs kiro --import', 'Import token from Kiro IDE'], ['ccs kiro --incognito', 'Use incognito browser (default: normal)'], ['ccs codex "explain code"', 'Use with prompt'], - ] + ], + writeLine ); // ═══════════════════════════════════════════════════════════════════════════ @@ -255,7 +273,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs copilot stop', 'Stop copilot-api daemon'], ['ccs copilot enable', 'Enable integration'], ['ccs copilot disable', 'Disable integration'], - ] + ], + writeLine ); // ═══════════════════════════════════════════════════════════════════════════ @@ -277,7 +296,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs cursor stop', 'Stop proxy daemon'], ['ccs cursor enable', 'Enable cursor integration'], ['ccs cursor disable', 'Disable cursor integration'], - ] + ], + writeLine ); // ═══════════════════════════════════════════════════════════════════════════ @@ -285,281 +305,351 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); // ═══════════════════════════════════════════════════════════════════════════ // Delegation - printSubSection('Delegation (inside Claude Code CLI)', [ - ['/ccs "task"', 'Delegate task (auto-selects profile)'], - ['/ccs --glm "task"', 'Force GLM-5 for simple tasks'], - ['/ccs --kimi "task"', 'Force Kimi OAuth for long context'], - ['/ccs --km "task"', 'Force Kimi API key for long context'], - ['/ccs:continue "follow-up"', 'Continue last delegation session'], - ]); + printSubSection( + 'Delegation (inside Claude Code CLI)', + [ + ['/ccs "task"', 'Delegate task (auto-selects profile)'], + ['/ccs --glm "task"', 'Force GLM-5 for simple tasks'], + ['/ccs --kimi "task"', 'Force Kimi OAuth for long context'], + ['/ccs --km "task"', 'Force Kimi API key for long context'], + ['/ccs:continue "follow-up"', 'Continue last delegation session'], + ], + writeLine + ); // Delegation CLI Flags (Claude Code passthrough) - printSubSection('Delegation Flags (Claude Code passthrough)', [ - ['--max-turns ', 'Limit agentic turns (prevents loops)'], - ['--fallback-model ', 'Auto-fallback on overload (sonnet)'], - ['--agents ', 'Inject dynamic subagents'], - ['--betas ', 'Enable experimental features'], - ['--allowedTools ', 'Restrict available tools'], - ['--disallowedTools ', 'Block specific tools'], - ]); + printSubSection( + 'Delegation Flags (Claude Code passthrough)', + [ + ['--max-turns ', 'Limit agentic turns (prevents loops)'], + ['--fallback-model ', 'Auto-fallback on overload (sonnet)'], + ['--agents ', 'Inject dynamic subagents'], + ['--betas ', 'Enable experimental features'], + ['--allowedTools ', 'Restrict available tools'], + ['--disallowedTools ', 'Block specific tools'], + ], + writeLine + ); // Diagnostics - printSubSection('Diagnostics', [ - ['ccs setup', 'First-time setup wizard'], - ['ccs doctor', 'Run health check and diagnostics'], - ['ccs cleanup', 'Remove old CLIProxy logs'], - ['ccs config', 'Open web dashboard (includes Claude IDE Extension setup page)'], - ['ccs config auth setup', 'Configure dashboard login'], - ['ccs config auth show', 'Show dashboard auth status'], - ['ccs config channels', 'Show Official Channels status'], + printSubSection( + 'Diagnostics', [ - 'ccs config channels --set telegram,discord', - 'Auto-add Telegram + Discord on supported native Claude runs', + ['ccs setup', 'First-time setup wizard'], + ['ccs doctor', 'Run health check and diagnostics'], + ['ccs cleanup', 'Remove old CLIProxy logs'], + ['ccs config', 'Open web dashboard (includes Claude IDE Extension setup page)'], + ['ccs config auth setup', 'Configure dashboard login'], + ['ccs config auth show', 'Show dashboard auth status'], + ['ccs config channels', 'Show Official Channels status'], + [ + 'ccs config channels --set telegram,discord', + 'Auto-add Telegram + Discord on supported native Claude runs', + ], + ['ccs config channels --set-token telegram=', 'Save TELEGRAM_BOT_TOKEN'], + ['ccs config image-analysis', 'Show image analysis settings'], + ['ccs config image-analysis --enable', 'Enable image analysis'], + ['ccs config thinking', 'Show thinking/reasoning settings'], + ['ccs config thinking --mode auto', 'Set thinking mode'], + ['ccs config thinking --clear-provider-override codex', 'Clear provider overrides'], + ['ccs config --port 3000', 'Use specific port'], + ['ccs config --host 0.0.0.0', 'Force all-interface binding for remote devices'], + ['ccs persist ', 'Write profile setup to ~/.claude/settings.json'], + ['ccs persist --list-backups', 'List available settings.json backups'], + ['ccs persist --restore', 'Restore settings.json from latest backup'], + ['ccs sync', 'Sync delegation commands and skills'], + ['ccs update', 'Update CCS to latest version'], + ['ccs update --force', 'Force reinstall current version'], + ['ccs update --beta', 'Install from dev channel (unstable)'], ], - ['ccs config channels --set-token telegram=', 'Save TELEGRAM_BOT_TOKEN'], - ['ccs config image-analysis', 'Show image analysis settings'], - ['ccs config image-analysis --enable', 'Enable image analysis'], - ['ccs config thinking', 'Show thinking/reasoning settings'], - ['ccs config thinking --mode auto', 'Set thinking mode'], - ['ccs config thinking --clear-provider-override codex', 'Clear provider overrides'], - ['ccs config --port 3000', 'Use specific port'], - ['ccs config --host 0.0.0.0', 'Force all-interface binding for remote devices'], - ['ccs persist ', 'Write profile setup to ~/.claude/settings.json'], - ['ccs persist --list-backups', 'List available settings.json backups'], - ['ccs persist --restore', 'Restore settings.json from latest backup'], - ['ccs sync', 'Sync delegation commands and skills'], - ['ccs update', 'Update CCS to latest version'], - ['ccs update --force', 'Force reinstall current version'], - ['ccs update --beta', 'Install from dev channel (unstable)'], - ]); + writeLine + ); // Environment export - printSubSection('Environment Export', [ - ['ccs env ', 'Export env vars for third-party tools'], - ['ccs env --format openai', 'OpenAI-compatible vars (OpenCode/Cursor)'], - ['ccs env --format anthropic', 'Anthropic vars (default)'], - ['ccs env --format raw', 'All effective env vars'], + printSubSection( + 'Environment Export', [ - 'ccs env --format claude-extension --ide vscode', - 'VS Code/Cursor Claude extension settings JSON', + ['ccs env ', 'Export env vars for third-party tools'], + ['ccs env --format openai', 'OpenAI-compatible vars (OpenCode/Cursor)'], + ['ccs env --format anthropic', 'Anthropic vars (default)'], + ['ccs env --format raw', 'All effective env vars'], + [ + 'ccs env --format claude-extension --ide vscode', + 'VS Code/Cursor Claude extension settings JSON', + ], + [ + 'ccs env --format claude-extension --ide windsurf', + 'Windsurf Claude extension settings JSON', + ], + ['ccs env --shell fish', 'Fish shell syntax'], ], - [ - 'ccs env --format claude-extension --ide windsurf', - 'Windsurf Claude extension settings JSON', - ], - ['ccs env --shell fish', 'Fish shell syntax'], - ]); + writeLine + ); // Flags - printSubSection('Flags', [ - ['--config-dir ', 'Use custom CCS config directory'], - ['--target ', 'Target CLI: claude (default), droid'], - ['-h, --help', 'Show this help message'], - ['-v, --version', 'Show version and installation info'], - ['-sc, --shell-completion', 'Install shell auto-completion'], - ]); + printSubSection( + 'Flags', + [ + ['--config-dir ', 'Use custom CCS config directory'], + ['--target ', 'Target CLI: claude (default), droid'], + ['-h, --help', 'Show this help message'], + ['-v, --version', 'Show version and installation info'], + ['-sc, --shell-completion', 'Install shell auto-completion'], + ], + writeLine + ); // Aliases - printSubSection('Aliases', [ - ['ccs-droid [args]', 'Explicit Droid runtime alias'], - ['ccsd [args]', 'Legacy shortcut for: ccs-droid [args]'], - ]); + printSubSection( + 'Aliases', + [ + ['ccs-droid [args]', 'Explicit Droid runtime alias'], + ['ccsd [args]', 'Legacy shortcut for: ccs-droid [args]'], + ], + writeLine + ); // Multi-target examples - printSubSection('Multi-Target', [ - ['ccs glm --target droid', 'Run GLM profile on Droid CLI'], - ['ccs-droid glm', 'Same as above (explicit alias)'], - ['ccsd glm', 'Legacy shortcut for ccs-droid'], - ['ccs-droid codex', 'Run built-in CLIProxy Codex profile on Droid'], - ['ccs-droid agy', 'Run built-in CLIProxy Antigravity profile on Droid'], + printSubSection( + 'Multi-Target', [ - 'ccs-droid codex exec --skip-permissions-unsafe "fix failing tests"', - 'Pass through Droid exec mode', + ['ccs glm --target droid', 'Run GLM profile on Droid CLI'], + ['ccs-droid glm', 'Same as above (explicit alias)'], + ['ccsd glm', 'Legacy shortcut for ccs-droid'], + ['ccs-droid codex', 'Run built-in CLIProxy Codex profile on Droid'], + ['ccs-droid agy', 'Run built-in CLIProxy Antigravity profile on Droid'], + [ + 'ccs-droid codex exec --skip-permissions-unsafe "fix failing tests"', + 'Pass through Droid exec mode', + ], + [ + 'ccs-droid codex -m custom:gpt-5.3-codex "fix failing tests"', + 'Auto-routes short exec flags', + ], + [ + 'ccs-droid codex --skip-permissions-unsafe "fix failing tests"', + 'Auto-routes to Droid exec when exec-only flags are detected', + ], + [ + 'ccs cliproxy create my-codex --provider codex --target droid', + 'Create CLIProxy variant with Droid as default target', + ], + ['ccs glm', 'Run GLM profile on Claude Code (default)'], ], - ['ccs-droid codex -m custom:gpt-5.3-codex "fix failing tests"', 'Auto-routes short exec flags'], - [ - 'ccs-droid codex --skip-permissions-unsafe "fix failing tests"', - 'Auto-routes to Droid exec when exec-only flags are detected', - ], - [ - 'ccs cliproxy create my-codex --provider codex --target droid', - 'Create CLIProxy variant with Droid as default target', - ], - ['ccs glm', 'Run GLM profile on Claude Code (default)'], - ]); + writeLine + ); // Configuration - printConfigSection('Configuration', [ - ['Config File:', isUnifiedMode() ? `${dirDisplay}/config.yaml` : `${dirDisplay}/config.json`], - ['Profiles:', `${dirDisplay}/profiles.json`], - ['Instances:', `${dirDisplay}/instances/`], - ['Settings:', `${dirDisplay}/*.settings.json`], - ]); + printConfigSection( + 'Configuration', + [ + ['Config File:', isUnifiedMode() ? `${dirDisplay}/config.yaml` : `${dirDisplay}/config.json`], + ['Profiles:', `${dirDisplay}/profiles.json`], + ['Instances:', `${dirDisplay}/instances/`], + ['Settings:', `${dirDisplay}/*.settings.json`], + ], + writeLine + ); // CLI Proxy management - printSubSection('CLI Proxy Plus Management', [ - ['ccs cliproxy', 'Show CLIProxy Plus status and version'], - ['ccs cliproxy --help', 'Full CLIProxy Plus management help'], - ['ccs cliproxy doctor', 'Quota diagnostics (Antigravity)'], - ['ccs cliproxy --install ', 'Install specific version (e.g., 6.6.6)'], - ['ccs cliproxy --latest', 'Update to latest version'], - ['', ''], // Spacer - ['ccs cliproxy pause

', 'Pause account from rotation'], - ['ccs cliproxy resume

', 'Resume paused account'], - ['ccs cliproxy status', 'Show CLIProxy process status'], - ['ccs cliproxy quota', 'Show quota/tier/pause status for all providers'], - ['ccs cliproxy quota --provider ', 'Show quota/tier/pause status for one provider'], - ]); + printSubSection( + 'CLI Proxy Plus Management', + [ + ['ccs cliproxy', 'Show CLIProxy Plus status and version'], + ['ccs cliproxy --help', 'Full CLIProxy Plus management help'], + ['ccs cliproxy doctor', 'Quota diagnostics (Antigravity)'], + ['ccs cliproxy --install ', 'Install specific version (e.g., 6.6.6)'], + ['ccs cliproxy --latest', 'Update to latest version'], + ['', ''], // Spacer + ['ccs cliproxy pause

', 'Pause account from rotation'], + ['ccs cliproxy resume

', 'Resume paused account'], + ['ccs cliproxy status', 'Show CLIProxy process status'], + ['ccs cliproxy quota', 'Show quota/tier/pause status for all providers'], + ['ccs cliproxy quota --provider ', 'Show quota/tier/pause status for one provider'], + ], + writeLine + ); // CLI Proxy configuration flags (new) - printSubSection('CLI Proxy Configuration', [ - ['--proxy-host ', 'Remote proxy hostname/IP'], - ['--proxy-port ', `Proxy port (default: ${CLIPROXY_DEFAULT_PORT})`], - ['--proxy-protocol ', 'Protocol: http or https (default: http)'], - ['--proxy-auth-token ', 'Auth token for remote proxy'], - ['--proxy-timeout ', 'Connection timeout in ms (default: 2000)'], - ['--local-proxy', 'Force local mode, ignore remote config'], - ['--remote-only', 'Fail if remote unreachable (no fallback)'], - ['--allow-self-signed', 'Allow self-signed certs (for dev proxies)'], - ]); + printSubSection( + 'CLI Proxy Configuration', + [ + ['--proxy-host ', 'Remote proxy hostname/IP'], + ['--proxy-port ', `Proxy port (default: ${CLIPROXY_DEFAULT_PORT})`], + ['--proxy-protocol ', 'Protocol: http or https (default: http)'], + ['--proxy-auth-token ', 'Auth token for remote proxy'], + ['--proxy-timeout ', 'Connection timeout in ms (default: 2000)'], + ['--local-proxy', 'Force local mode, ignore remote config'], + ['--remote-only', 'Fail if remote unreachable (no fallback)'], + ['--allow-self-signed', 'Allow self-signed certs (for dev proxies)'], + ], + writeLine + ); // W3: Thinking Budget explanation - printSubSection('Extended Thinking / Reasoning', [ - ['--thinking off', 'Disable extended thinking'], - ['--thinking auto', 'Let model decide dynamically'], - ['--thinking low', '1K tokens - Quick responses'], - ['--thinking medium', '8K tokens - Standard analysis'], - ['--thinking high', '24K tokens - Deep reasoning'], - ['--thinking xhigh', '32K tokens - Maximum depth'], - ['--thinking ', 'Custom token budget (512-100000)'], - ['', ''], - ['--effort ', 'Codex alias for reasoning effort (medium/high/xhigh)'], - ['--effort xhigh', 'Pin Codex effort to xhigh for this run'], - ['', ''], - ['Droid exec:', 'Use native Droid flag: --reasoning-effort '], - ['', 'CCS auto-maps --thinking/--effort to --reasoning-effort in droid exec mode.'], - ['', 'For interactive droid sessions, CCS applies reasoning via Droid BYOK model config.'], - ['', 'When multiple reasoning flags are provided, the first flag wins.'], - ['', ''], - ['Note:', 'Extended thinking allocates compute for step-by-step reasoning'], - ['', 'before responding.'], - ['', 'Providers: agy/gemini use --thinking, codex uses --effort (or --thinking alias).'], - ['', 'Codex model suffixes also pin effort: -medium / -high / -xhigh.'], - ]); + printSubSection( + 'Extended Thinking / Reasoning', + [ + ['--thinking off', 'Disable extended thinking'], + ['--thinking auto', 'Let model decide dynamically'], + ['--thinking low', '1K tokens - Quick responses'], + ['--thinking medium', '8K tokens - Standard analysis'], + ['--thinking high', '24K tokens - Deep reasoning'], + ['--thinking xhigh', '32K tokens - Maximum depth'], + ['--thinking ', 'Custom token budget (512-100000)'], + ['', ''], + ['--effort ', 'Codex alias for reasoning effort (medium/high/xhigh)'], + ['--effort xhigh', 'Pin Codex effort to xhigh for this run'], + ['', ''], + ['Droid exec:', 'Use native Droid flag: --reasoning-effort '], + ['', 'CCS auto-maps --thinking/--effort to --reasoning-effort in droid exec mode.'], + ['', 'For interactive droid sessions, CCS applies reasoning via Droid BYOK model config.'], + ['', 'When multiple reasoning flags are provided, the first flag wins.'], + ['', ''], + ['Note:', 'Extended thinking allocates compute for step-by-step reasoning'], + ['', 'before responding.'], + ['', 'Providers: agy/gemini use --thinking, codex uses --effort (or --thinking alias).'], + ['', 'Codex model suffixes also pin effort: -medium / -high / -xhigh.'], + ], + writeLine + ); // Extended Context (1M) - printSubSection('Extended Context (--1m)', [ - ['--1m', 'Request 1M token context when the selected model supports [1m]'], - ['--no-1m', 'Force standard context (Claude default stays plain)'], - ['', ''], - ['Auto behavior:', 'Gemini models: CCS auto-adds [1m] when supported'], - ['', 'Claude models: plain by default, opt-in with --1m or saved [1m]'], - ['', ''], - ['Note:', 'CCS only controls the saved [1m] suffix.'], - ['', 'Provider pricing and entitlement stay upstream.'], - ['', 'Some accounts/providers can still return 429 extra-usage errors for long-context requests.'], - ]); + printSubSection( + 'Extended Context (--1m)', + [ + ['--1m', 'Request 1M token context when the selected model supports [1m]'], + ['--no-1m', 'Force standard context (Claude default stays plain)'], + ['', ''], + ['Auto behavior:', 'Gemini models: CCS auto-adds [1m] when supported'], + ['', 'Claude models: plain by default, opt-in with --1m or saved [1m]'], + ['', ''], + ['Note:', 'CCS only controls the saved [1m] suffix.'], + ['', 'Provider pricing and entitlement stay upstream.'], + [ + '', + 'Some accounts/providers can still return 429 extra-usage errors for long-context requests.', + ], + ], + writeLine + ); // Image Analysis - printSubSection('Image Analysis (CLIProxy vision)', [ - ['ccs config image-analysis', 'Show current settings'], - ['ccs config image-analysis --enable', 'Enable for CLIProxy providers'], - ['ccs config image-analysis --disable', 'Disable (use native Read)'], - ['ccs config image-analysis --timeout 120', 'Set analysis timeout'], - ['ccs config image-analysis --set-model

', 'Set provider model'], - ['', ''], - ['Note:', 'When enabled, images/PDFs are analyzed via vision models'], - ['', 'instead of passing raw data to Claude. Works with CLIProxy'], - ['', 'providers (agy, gemini, codex, kiro, ghcp).'], - ]); - - printSubSection('Official Channels (official Claude plugins)', [ - ['ccs config', 'Dashboard -> Settings -> Channels (fastest path)'], - ['ccs config channels', 'Show current status'], + printSubSection( + 'Image Analysis (CLIProxy vision)', [ - 'ccs config channels --set telegram,discord', - 'Auto-add selected channels on native Claude default/account sessions', + ['ccs config image-analysis', 'Show current settings'], + ['ccs config image-analysis --enable', 'Enable for CLIProxy providers'], + ['ccs config image-analysis --disable', 'Disable (use native Read)'], + ['ccs config image-analysis --timeout 120', 'Set analysis timeout'], + ['ccs config image-analysis --set-model

', 'Set provider model'], + ['', ''], + ['Note:', 'When enabled, images/PDFs are analyzed via vision models'], + ['', 'instead of passing raw data to Claude. Works with CLIProxy'], + ['', 'providers (agy, gemini, codex, kiro, ghcp).'], ], - ['ccs config channels --set all', 'Enable Telegram, Discord, and iMessage'], - ['ccs config channels --unattended', 'Also add --dangerously-skip-permissions'], - ['ccs config channels --set-token telegram=', 'Save TELEGRAM_BOT_TOKEN'], - ['ccs config channels --set-token discord=', 'Save DISCORD_BOT_TOKEN'], - ['ccs config channels --clear-token [channel]', 'Remove one or all saved channel tokens'], - ['', ''], - ['', 'Fastest path: turn on the channel, save the token if needed, then run ccs.'], - ['Note:', 'Runtime-only. Applies to native Claude default/account sessions only.'], - ['', 'Not supported for ccs glm, other API/OAuth profiles, or Droid targets.'], - ['', 'Telegram/Discord tokens live in ~/.claude/channels//.env.'], - ['', 'Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work for that launch.'], - ['', 'iMessage is macOS-only and requires local OS permissions instead of a bot token.'], - ]); + writeLine + ); + + printSubSection( + 'Official Channels (official Claude plugins)', + [ + ['ccs config', 'Dashboard -> Settings -> Channels (fastest path)'], + ['ccs config channels', 'Show current status'], + [ + 'ccs config channels --set telegram,discord', + 'Auto-add selected channels on native Claude default/account sessions', + ], + ['ccs config channels --set all', 'Enable Telegram, Discord, and iMessage'], + ['ccs config channels --unattended', 'Also add --dangerously-skip-permissions'], + ['ccs config channels --set-token telegram=', 'Save TELEGRAM_BOT_TOKEN'], + ['ccs config channels --set-token discord=', 'Save DISCORD_BOT_TOKEN'], + ['ccs config channels --clear-token [channel]', 'Remove one or all saved channel tokens'], + ['', ''], + ['', 'Fastest path: turn on the channel, save the token if needed, then run ccs.'], + ['Note:', 'Runtime-only. Applies to native Claude default/account sessions only.'], + ['', 'Not supported for ccs glm, other API/OAuth profiles, or Droid targets.'], + ['', 'Telegram/Discord tokens live in ~/.claude/channels//.env.'], + ['', 'Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work for that launch.'], + ['', 'iMessage is macOS-only and requires local OS permissions instead of a bot token.'], + ], + writeLine + ); // CCS Environment Variables - printSubSection('Environment Variables', [ - ['CCS_DIR', 'Override CCS config directory (default: ~/.ccs)'], - ['CCS_HOME', 'Override home directory (legacy, appends .ccs)'], - ['CCS_DEBUG', 'Enable debug logging'], - ['CCS_THINKING', 'Override thinking level (flag > env > config)'], - ]); + printSubSection( + 'Environment Variables', + [ + ['CCS_DIR', 'Override CCS config directory (default: ~/.ccs)'], + ['CCS_HOME', 'Override home directory (legacy, appends .ccs)'], + ['CCS_DEBUG', 'Enable debug logging'], + ['CCS_THINKING', 'Override thinking level (flag > env > config)'], + ], + writeLine + ); // CLI Proxy env vars - printSubSection('CLI Proxy Environment Variables', [ - ['CCS_PROXY_HOST', 'Remote proxy hostname'], - ['CCS_PROXY_PORT', 'Proxy port'], - ['CCS_PROXY_PROTOCOL', 'Protocol (http/https)'], - ['CCS_PROXY_AUTH_TOKEN', 'Auth token'], - ['CCS_PROXY_TIMEOUT', 'Connection timeout in ms'], - ['CCS_PROXY_FALLBACK_ENABLED', 'Enable local fallback (1/0)'], - ['CCS_ALLOW_SELF_SIGNED', 'Allow self-signed certs (1/0)'], - ]); + printSubSection( + 'CLI Proxy Environment Variables', + [ + ['CCS_PROXY_HOST', 'Remote proxy hostname'], + ['CCS_PROXY_PORT', 'Proxy port'], + ['CCS_PROXY_PROTOCOL', 'Protocol (http/https)'], + ['CCS_PROXY_AUTH_TOKEN', 'Auth token'], + ['CCS_PROXY_TIMEOUT', 'Connection timeout in ms'], + ['CCS_PROXY_FALLBACK_ENABLED', 'Enable local fallback (1/0)'], + ['CCS_ALLOW_SELF_SIGNED', 'Allow self-signed certs (1/0)'], + ], + writeLine + ); // CLI Proxy paths - console.log(subheader('CLI Proxy:')); - console.log(` Binary: ${color(`${dirDisplay}/cliproxy/bin/cli-proxy-api-plus`, 'path')}`); - console.log(` Config: ${color(`${dirDisplay}/cliproxy/config.yaml`, 'path')}`); - console.log(` Auth: ${color(`${dirDisplay}/cliproxy/auth/`, 'path')}`); - console.log(` ${dim(`Port: ${CLIPROXY_DEFAULT_PORT} (default)`)}`); - console.log(''); + writeLine(subheader('CLI Proxy:')); + writeLine(` Binary: ${color(`${dirDisplay}/cliproxy/bin/cli-proxy-api-plus`, 'path')}`); + writeLine(` Config: ${color(`${dirDisplay}/cliproxy/config.yaml`, 'path')}`); + writeLine(` Auth: ${color(`${dirDisplay}/cliproxy/auth/`, 'path')}`); + writeLine(` ${dim(`Port: ${CLIPROXY_DEFAULT_PORT} (default)`)}`); + writeLine(''); // Shared Data - console.log(subheader('Shared Data:')); - console.log(` Commands: ${color(`${dirDisplay}/shared/commands/`, 'path')}`); - console.log(` Skills: ${color(`${dirDisplay}/shared/skills/`, 'path')}`); - console.log(` Agents: ${color(`${dirDisplay}/shared/agents/`, 'path')}`); - console.log(` ${dim('Note: Symlinked across all profiles')}`); - console.log(''); + writeLine(subheader('Shared Data:')); + writeLine(` Commands: ${color(`${dirDisplay}/shared/commands/`, 'path')}`); + writeLine(` Skills: ${color(`${dirDisplay}/shared/skills/`, 'path')}`); + writeLine(` Agents: ${color(`${dirDisplay}/shared/agents/`, 'path')}`); + writeLine(` ${dim('Note: Symlinked across all profiles')}`); + writeLine(''); // Examples (aligned with consistent spacing) - console.log(subheader('Examples:')); - console.log(` $ ${color('ccs', 'command')} ${dim('# Use default account')}`); - console.log( + writeLine(subheader('Examples:')); + writeLine(` $ ${color('ccs', 'command')} ${dim('# Use default account')}`); + writeLine( ` $ ${color('ccs gemini', 'command')} ${dim('# OAuth (browser opens first time)')}` ); - console.log(` $ ${color('ccs glm "implement API"', 'command')} ${dim('# API key model')}`); - console.log(` $ ${color('ccs config', 'command')} ${dim('# Open web dashboard')}`); - console.log(''); + writeLine(` $ ${color('ccs glm "implement API"', 'command')} ${dim('# API key model')}`); + writeLine(` $ ${color('ccs config', 'command')} ${dim('# Open web dashboard')}`); + writeLine(''); // Update examples - console.log(subheader('Update:')); - console.log( + writeLine(subheader('Update:')); + writeLine( ` $ ${color('ccs update', 'command')} ${dim('# Update to latest stable')}` ); - console.log( + writeLine( ` $ ${color('ccs update --force', 'command')} ${dim('# Force reinstall current')}` ); - console.log(` $ ${color('ccs update --beta', 'command')} ${dim('# Install dev channel')}`); - console.log(''); + writeLine(` $ ${color('ccs update --beta', 'command')} ${dim('# Install dev channel')}`); + writeLine(''); // Docs link - console.log(` ${dim('Docs: https://github.com/kaitranntt/ccs')}`); - console.log(''); + writeLine(` ${dim('Docs: https://github.com/kaitranntt/ccs')}`); + writeLine(''); // Uninstall - console.log(subheader('Uninstall:')); - console.log(` ${color('npm uninstall -g @kaitranntt/ccs', 'command')}`); - console.log(''); + writeLine(subheader('Uninstall:')); + writeLine(` ${color('npm uninstall -g @kaitranntt/ccs', 'command')}`); + writeLine(''); // License - console.log(dim('License: MIT')); - console.log(''); + writeLine(dim('License: MIT')); + writeLine(''); } diff --git a/tests/unit/commands/api-command-router.test.ts b/tests/unit/commands/api-command-router.test.ts index 25d33812..450a7326 100644 --- a/tests/unit/commands/api-command-router.test.ts +++ b/tests/unit/commands/api-command-router.test.ts @@ -1,74 +1,48 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; +import { beforeEach, describe, expect, it } from 'bun:test'; + +import { createApiCommandHandler } from '../../../src/commands/api-command'; let calls: string[] = []; beforeEach(() => { calls = []; +}); - mock.module('../../../src/commands/api-command/help', () => ({ - showApiCommandHelp: async () => { +function buildHandleApiCommand() { + return createApiCommandHandler({ + help: async () => { calls.push('help'); }, - showUnknownApiCommand: async (command: string) => { + unknown: async (command: string) => { calls.push(`unknown:${command}`); }, - })); - - mock.module('../../../src/commands/api-command/create-command', () => ({ - handleApiCreateCommand: async (args: string[]) => { + create: async (args: string[]) => { calls.push(`create:${args.join(' ')}`); }, - })); - - mock.module('../../../src/commands/api-command/list-command', () => ({ - handleApiListCommand: async (args: string[]) => { + list: async (args: string[]) => { calls.push(`list:${args.join(' ')}`); }, - })); - - mock.module('../../../src/commands/api-command/remove-command', () => ({ - handleApiRemoveCommand: async (args: string[]) => { - calls.push(`remove:${args.join(' ')}`); - }, - })); - - mock.module('../../../src/commands/api-command/discover-command', () => ({ - handleApiDiscoverCommand: async (args: string[]) => { + discover: async (args: string[]) => { calls.push(`discover:${args.join(' ')}`); }, - })); - - mock.module('../../../src/commands/api-command/copy-command', () => ({ - handleApiCopyCommand: async (args: string[]) => { + copy: async (args: string[]) => { calls.push(`copy:${args.join(' ')}`); }, - })); - - mock.module('../../../src/commands/api-command/export-command', () => ({ - handleApiExportCommand: async (args: string[]) => { + export: async (args: string[]) => { calls.push(`export:${args.join(' ')}`); }, - })); - - mock.module('../../../src/commands/api-command/import-command', () => ({ - handleApiImportCommand: async (args: string[]) => { + import: async (args: string[]) => { calls.push(`import:${args.join(' ')}`); }, - })); -}); - -afterEach(() => { - mock.restore(); -}); - -async function loadHandleApiCommand() { - const mod = await import(`../../../src/commands/api-command?test=${Date.now()}-${Math.random()}`); - return mod.handleApiCommand; + remove: async (args: string[]) => { + calls.push(`remove:${args.join(' ')}`); + }, + }); } describe('api-command router', () => { it('defaults to help when no subcommand is provided', async () => { - const handleApiCommand = await loadHandleApiCommand(); + const handleApiCommand = buildHandleApiCommand(); await handleApiCommand([]); @@ -76,7 +50,7 @@ describe('api-command router', () => { }); it('routes remove aliases through the named command dispatcher', async () => { - const handleApiCommand = await loadHandleApiCommand(); + const handleApiCommand = buildHandleApiCommand(); await handleApiCommand(['rm', 'profile-a']); @@ -84,7 +58,7 @@ describe('api-command router', () => { }); it('forwards list arguments to the handler for validation', async () => { - const handleApiCommand = await loadHandleApiCommand(); + const handleApiCommand = buildHandleApiCommand(); await handleApiCommand(['list', 'unexpected']); @@ -92,7 +66,7 @@ describe('api-command router', () => { }); it('routes hardened subcommands through their handlers', async () => { - const handleApiCommand = await loadHandleApiCommand(); + const handleApiCommand = buildHandleApiCommand(); await handleApiCommand(['discover', '--json']); await handleApiCommand(['copy', 'source', 'dest']); @@ -108,7 +82,7 @@ describe('api-command router', () => { }); it('delegates unknown commands to the shared unknown handler', async () => { - const handleApiCommand = await loadHandleApiCommand(); + const handleApiCommand = buildHandleApiCommand(); await handleApiCommand(['bogus']); diff --git a/tests/unit/commands/config-auth-command.test.ts b/tests/unit/commands/config-auth-command.test.ts index 3f3182a7..5674b652 100644 --- a/tests/unit/commands/config-auth-command.test.ts +++ b/tests/unit/commands/config-auth-command.test.ts @@ -15,20 +15,6 @@ beforeEach(() => { logLines.push(args.map(String).join(' ')); }; - const uiModule = { - initUI: async () => {}, - header: (message: string) => message, - subheader: (message: string) => message, - color: (message: string) => message, - dim: (message: string) => message, - ok: (message: string) => message, - info: (message: string) => message, - warn: (message: string) => message, - fail: (message: string) => message, - }; - mock.module('../../../src/utils/ui', () => uiModule); - mock.module('../../../src/utils/ui.ts', () => uiModule); - mock.module('../../../src/commands/config-auth/setup-command', () => ({ handleSetup: async () => { calls.push('setup'); @@ -55,9 +41,7 @@ afterEach(() => { }); async function loadHandleConfigAuthCommand() { - const mod = await import( - `../../../src/commands/config-auth?test=${Date.now()}-${Math.random()}` - ); + const mod = await import(`../../../src/commands/config-auth?test=${Date.now()}-${Math.random()}`); return mod.handleConfigAuthCommand; } diff --git a/tests/unit/commands/help-command-parity.test.ts b/tests/unit/commands/help-command-parity.test.ts index 451ea81a..512bb5f8 100644 --- a/tests/unit/commands/help-command-parity.test.ts +++ b/tests/unit/commands/help-command-parity.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test } from 'bun:test'; +import { describe, expect, test } from 'bun:test'; import { showApiCommandHelp } from '../../../src/commands/api-command/help'; import { handleHelpCommand } from '../../../src/commands/help-command'; @@ -8,19 +8,9 @@ function stripAnsi(input: string): string { } describe('help command parity', () => { - const originalLog = console.log; - - afterEach(() => { - console.log = originalLog; - }); - test('root help documents cliproxy provider filter under quota command', async () => { const lines: string[] = []; - console.log = (...args: unknown[]) => { - lines.push(args.map((arg) => String(arg)).join(' ')); - }; - - await handleHelpCommand(); + await handleHelpCommand((line) => lines.push(line)); const rendered = stripAnsi(lines.join('\n')); expect(rendered.includes('ccs cliproxy status [provider]')).toBe(false); @@ -31,11 +21,7 @@ describe('help command parity', () => { test('root help documents llama.cpp as a local API profile', async () => { const lines: string[] = []; - console.log = (...args: unknown[]) => { - lines.push(args.map((arg) => String(arg)).join(' ')); - }; - - await handleHelpCommand(); + await handleHelpCommand((line) => lines.push(line)); const rendered = stripAnsi(lines.join('\n')); expect(rendered.includes('ccs llamacpp')).toBe(true); @@ -44,11 +30,7 @@ describe('help command parity', () => { test('root help no longer markets glmt as a supported profile', async () => { const lines: string[] = []; - console.log = (...args: unknown[]) => { - lines.push(args.map((arg) => String(arg)).join(' ')); - }; - - await handleHelpCommand(); + await handleHelpCommand((line) => lines.push(line)); const rendered = stripAnsi(lines.join('\n')); expect(rendered.includes('ccs glmt')).toBe(false); @@ -57,11 +39,7 @@ describe('help command parity', () => { test('root help documents Claude IDE extension setup surfaces', async () => { const lines: string[] = []; - console.log = (...args: unknown[]) => { - lines.push(args.map((arg) => String(arg)).join(' ')); - }; - - await handleHelpCommand(); + await handleHelpCommand((line) => lines.push(line)); const rendered = stripAnsi(lines.join('\n')); expect(rendered.includes('Claude IDE Extension setup page')).toBe(true); @@ -75,11 +53,7 @@ describe('help command parity', () => { test('root help documents dashboard host binding example', async () => { const lines: string[] = []; - console.log = (...args: unknown[]) => { - lines.push(args.map((arg) => String(arg)).join(' ')); - }; - - await handleHelpCommand(); + await handleHelpCommand((line) => lines.push(line)); const rendered = stripAnsi(lines.join('\n')); expect(rendered.includes('ccs config --host 0.0.0.0')).toBe(true); @@ -88,48 +62,36 @@ describe('help command parity', () => { test('root help documents official channels native-only scope and process-env tokens', async () => { const lines: string[] = []; - console.log = (...args: unknown[]) => { - lines.push(args.map((arg) => String(arg)).join(' ')); - }; - - await handleHelpCommand(); + await handleHelpCommand((line) => lines.push(line)); const rendered = stripAnsi(lines.join('\n')); expect(rendered.includes('Dashboard -> Settings -> Channels (fastest path)')).toBe(true); expect( - rendered.includes('Fastest path: turn on the channel, save the token if needed, then run ccs.') + rendered.includes( + 'Fastest path: turn on the channel, save the token if needed, then run ccs.' + ) ).toBe(true); expect(rendered.includes('Not supported for ccs glm')).toBe(true); - expect(rendered.includes('Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work')).toBe( - true - ); + expect( + rendered.includes('Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work') + ).toBe(true); }); test('root help explains Claude [1m] as an explicit CCS suffix with upstream limits', async () => { const lines: string[] = []; - console.log = (...args: unknown[]) => { - lines.push(args.map((arg) => String(arg)).join(' ')); - }; - - await handleHelpCommand(); + await handleHelpCommand((line) => lines.push(line)); const rendered = stripAnsi(lines.join('\n')); - expect(rendered.includes('Claude models: plain by default, opt-in with --1m or saved [1m]')).toBe( - true - ); + expect( + rendered.includes('Claude models: plain by default, opt-in with --1m or saved [1m]') + ).toBe(true); expect(rendered.includes('CCS only controls the saved [1m] suffix.')).toBe(true); - expect(rendered.includes('return 429 extra-usage errors for long-context requests')).toBe( - true - ); + expect(rendered.includes('return 429 extra-usage errors for long-context requests')).toBe(true); }); test('api help documents create-time Claude [1m] flags and entitlement warning', async () => { const lines: string[] = []; - console.log = (...args: unknown[]) => { - lines.push(args.map((arg) => String(arg)).join(' ')); - }; - - await showApiCommandHelp(); + await showApiCommandHelp((line) => lines.push(line)); const rendered = stripAnsi(lines.join('\n')); expect(rendered.includes('--1m / --no-1m')).toBe(true);