fix(tests): stabilize command help CI validation

This commit is contained in:
Tam Nhu Tran
2026-03-26 16:56:34 -04:00
parent 5e11bba514
commit be4ba13e96
8 changed files with 528 additions and 471 deletions
+3 -1
View File
@@ -491,7 +491,9 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
if (hasClaudeMappings) { if (hasClaudeMappings) {
details += `\nLongCtx: ${ details += `\nLongCtx: ${
shouldEnableClaudeLongContext ? 'compatible Claude mappings use [1m]' : 'standard Claude context' shouldEnableClaudeLongContext
? 'compatible Claude mappings use [1m]'
: 'standard Claude context'
}`; }`;
} }
+79 -81
View File
@@ -8,6 +8,8 @@ import {
import { color, dim, fail, header, initUI, subheader } from '../../utils/ui'; import { color, dim, fail, header, initUI, subheader } from '../../utils/ui';
import { sanitizeHelpText } from './shared'; import { sanitizeHelpText } from './shared';
type HelpWriter = (line: string) => void;
function renderPresetHelpLine(preset: ProviderPreset, idWidth: number): string { function renderPresetHelpLine(preset: ProviderPreset, idWidth: number): string {
const presetId = sanitizeHelpText(preset.id) || 'unknown'; const presetId = sanitizeHelpText(preset.id) || 'unknown';
const paddedId = presetId.padEnd(idWidth); const paddedId = presetId.padEnd(idWidth);
@@ -16,7 +18,7 @@ function renderPresetHelpLine(preset: ProviderPreset, idWidth: number): string {
return ` ${color(paddedId, 'command')} ${presetName} - ${presetDescription}`; return ` ${color(paddedId, 'command')} ${presetName} - ${presetDescription}`;
} }
export async function showApiCommandHelp(): Promise<void> { export async function showApiCommandHelp(writeLine: HelpWriter = console.log): Promise<void> {
await initUI(); await initUI();
const presetIds = getPresetIds() const presetIds = getPresetIds()
.map((id) => sanitizeHelpText(id)) .map((id) => sanitizeHelpText(id))
@@ -25,112 +27,108 @@ export async function showApiCommandHelp(): Promise<void> {
const presetAliases = getPresetAliases(); const presetAliases = getPresetAliases();
const presetIdWidth = Math.max(0, ...presetIds.map((id) => id.length)) + 2; const presetIdWidth = Math.max(0, ...presetIds.map((id) => id.length)) + 2;
console.log(header('CCS API Management')); writeLine(header('CCS API Management'));
console.log(''); writeLine('');
console.log(subheader('Usage')); writeLine(subheader('Usage'));
console.log(` ${color('ccs api', 'command')} <command> [options]`); writeLine(` ${color('ccs api', 'command')} <command> [options]`);
console.log(''); writeLine('');
console.log(subheader('Commands')); writeLine(subheader('Commands'));
console.log(` ${color('create [name]', 'command')} Create new API profile (interactive)`); writeLine(` ${color('create [name]', 'command')} Create new API profile (interactive)`);
console.log(` ${color('list', 'command')} List all API profiles`); writeLine(` ${color('list', 'command')} List all API profiles`);
console.log( writeLine(
` ${color('discover', 'command')} Discover orphan *.settings.json and register` ` ${color('discover', 'command')} Discover orphan *.settings.json and register`
); );
console.log(` ${color('copy <src> <dest>', 'command')} Duplicate API profile settings + config`); writeLine(` ${color('copy <src> <dest>', 'command')} Duplicate API profile settings + config`);
console.log( writeLine(
` ${color('export <name>', 'command')} Export profile bundle for cross-device transfer` ` ${color('export <name>', 'command')} Export profile bundle for cross-device transfer`
); );
console.log( writeLine(` ${color('import <file>', 'command')} Import profile bundle and register profile`);
` ${color('import <file>', 'command')} Import profile bundle and register profile` writeLine(` ${color('remove <name>', 'command')} Remove an API profile`);
); writeLine('');
console.log(` ${color('remove <name>', 'command')} Remove an API profile`); writeLine(subheader('Options'));
console.log(''); writeLine(
console.log(subheader('Options'));
console.log(
` ${color('--preset <id>', 'command')} Use provider preset (${presetIds.join(', ')})` ` ${color('--preset <id>', 'command')} Use provider preset (${presetIds.join(', ')})`
); );
console.log( writeLine(
` ${color('--cliproxy-provider <id>', 'command')} Use routed CLIProxy provider (${cliproxyProviderIds.join(', ')})` ` ${color('--cliproxy-provider <id>', 'command')} Use routed CLIProxy provider (${cliproxyProviderIds.join(', ')})`
); );
console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`); writeLine(` ${color('--base-url <url>', 'command')} API base URL (create)`);
console.log(` ${color('--api-key <key>', 'command')} API key (create)`); writeLine(` ${color('--api-key <key>', 'command')} API key (create)`);
console.log(` ${color('--model <model>', 'command')} Default model (create)`); writeLine(` ${color('--model <model>', 'command')} Default model (create)`);
console.log( writeLine(
` ${color('--1m / --no-1m', 'command')} Write or clear [1m] on compatible Claude mappings` ` ${color('--1m / --no-1m', 'command')} Write or clear [1m] on compatible Claude mappings`
); );
console.log( writeLine(
` ${color('--target <cli>', 'command')} Default target: claude or droid (create)` ` ${color('--target <cli>', 'command')} Default target: claude or droid (create)`
); );
console.log(` ${color('--register', 'command')} Register discovered orphan settings`); writeLine(` ${color('--register', 'command')} Register discovered orphan settings`);
console.log(` ${color('--json', 'command')} JSON output for discover command`); writeLine(` ${color('--json', 'command')} JSON output for discover command`);
console.log(` ${color('--out <file>', 'command')} Export bundle output path`); writeLine(` ${color('--out <file>', 'command')} Export bundle output path`);
console.log(` ${color('--include-secrets', 'command')} Include token in export bundle`); writeLine(` ${color('--include-secrets', 'command')} Include token in export bundle`);
console.log(` ${color('--name <name>', 'command')} Override profile name during import`); writeLine(` ${color('--name <name>', 'command')} Override profile name during import`);
console.log( writeLine(
` ${color('--force', 'command')} Overwrite existing or bypass validation (create/discover/copy/import)` ` ${color('--force', 'command')} Overwrite existing or bypass validation (create/discover/copy/import)`
); );
console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts`); writeLine(` ${color('--yes, -y', 'command')} Skip confirmation prompts`);
console.log(''); writeLine('');
console.log(subheader('Provider Presets')); writeLine(subheader('Provider Presets'));
PROVIDER_PRESETS.forEach((preset) => console.log(renderPresetHelpLine(preset, presetIdWidth))); PROVIDER_PRESETS.forEach((preset) => writeLine(renderPresetHelpLine(preset, presetIdWidth)));
Object.entries(presetAliases).forEach(([alias, canonical]) => { Object.entries(presetAliases).forEach(([alias, canonical]) => {
const safeAlias = sanitizeHelpText(alias); const safeAlias = sanitizeHelpText(alias);
const safeCanonical = sanitizeHelpText(canonical); const safeCanonical = sanitizeHelpText(canonical);
console.log( writeLine(` ${dim(`Legacy alias: --preset ${safeAlias} (auto-mapped to ${safeCanonical})`)}`);
` ${dim(`Legacy alias: --preset ${safeAlias} (auto-mapped to ${safeCanonical})`)}`
);
}); });
console.log(''); writeLine('');
console.log(subheader('Examples')); writeLine(subheader('Examples'));
console.log(` ${dim('# Interactive wizard')}`); writeLine(` ${dim('# Interactive wizard')}`);
console.log(` ${color('ccs api create', 'command')}`); writeLine(` ${color('ccs api create', 'command')}`);
console.log(''); writeLine('');
console.log(` ${dim('# Quick setup with preset')}`); writeLine(` ${dim('# Quick setup with preset')}`);
console.log(` ${color('ccs api create --preset anthropic', 'command')}`); writeLine(` ${color('ccs api create --preset anthropic', 'command')}`);
console.log( writeLine(
` ${color('ccs api create --preset anthropic --1m', 'command')} ${dim('# explicit Claude [1m] opt-in')}` ` ${color('ccs api create --preset anthropic --1m', 'command')} ${dim('# explicit Claude [1m] opt-in')}`
); );
console.log(` ${color('ccs api create --preset openrouter', 'command')}`); writeLine(` ${color('ccs api create --preset openrouter', 'command')}`);
console.log(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`); writeLine(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`);
console.log(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`); writeLine(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`);
console.log(` ${color('ccs api create --preset glm', 'command')}`); writeLine(` ${color('ccs api create --preset glm', 'command')}`);
console.log(''); writeLine('');
console.log(subheader('Claude Long Context')); writeLine(subheader('Claude Long Context'));
console.log(` ${dim('Plain Claude model IDs stay on standard context by default.')}`); writeLine(` ${dim('Plain Claude model IDs stay on standard context by default.')}`);
console.log( writeLine(
` ${dim('Use --1m during create to append [1m] to compatible Claude mappings, or --no-1m to force plain IDs.')}` ` ${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.')}` ` ${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(''); writeLine('');
console.log(` ${dim('# Create routed profile from existing CLIProxy provider config')}`); writeLine(` ${dim('# Create routed profile from existing CLIProxy provider config')}`);
console.log(` ${color('ccs api create --cliproxy-provider gemini', 'command')}`); writeLine(` ${color('ccs api create --cliproxy-provider gemini', 'command')}`);
console.log( writeLine(
` ${color('ccs api create gemini-droid --cliproxy-provider gemini --target droid', 'command')}` ` ${color('ccs api create gemini-droid --cliproxy-provider gemini --target droid', 'command')}`
); );
console.log(''); writeLine('');
console.log(` ${dim('# Create with name')}`); writeLine(` ${dim('# Create with name')}`);
console.log(` ${color('ccs api create myapi', 'command')}`); writeLine(` ${color('ccs api create myapi', 'command')}`);
console.log(` ${color('ccs api create mydroid --preset glm --target droid', 'command')}`); writeLine(` ${color('ccs api create mydroid --preset glm --target droid', 'command')}`);
console.log(''); writeLine('');
console.log(` ${dim('# Remove API profile')}`); writeLine(` ${dim('# Remove API profile')}`);
console.log(` ${color('ccs api remove myapi', 'command')}`); writeLine(` ${color('ccs api remove myapi', 'command')}`);
console.log(''); writeLine('');
console.log(` ${dim('# Discover and register orphan settings files')}`); writeLine(` ${dim('# Discover and register orphan settings files')}`);
console.log(` ${color('ccs api discover', 'command')}`); writeLine(` ${color('ccs api discover', 'command')}`);
console.log(` ${color('ccs api discover --register', 'command')}`); writeLine(` ${color('ccs api discover --register', 'command')}`);
console.log(''); writeLine('');
console.log(` ${dim('# Duplicate an existing API profile')}`); writeLine(` ${dim('# Duplicate an existing API profile')}`);
console.log(` ${color('ccs api copy glm glm-backup', 'command')}`); writeLine(` ${color('ccs api copy glm glm-backup', 'command')}`);
console.log(''); writeLine('');
console.log(` ${dim('# Export and import across devices')}`); writeLine(` ${dim('# Export and import across devices')}`);
console.log(` ${color('ccs api export glm --out ./glm.ccs-profile.json', 'command')}`); writeLine(` ${color('ccs api export glm --out ./glm.ccs-profile.json', 'command')}`);
console.log(` ${color('ccs api import ./glm.ccs-profile.json', 'command')}`); writeLine(` ${color('ccs api import ./glm.ccs-profile.json', 'command')}`);
console.log(''); writeLine('');
console.log(` ${dim('# Show all API profiles')}`); writeLine(` ${dim('# Show all API profiles')}`);
console.log(` ${color('ccs api list', 'command')}`); writeLine(` ${color('ccs api list', 'command')}`);
console.log(''); writeLine('');
} }
export async function showUnknownApiCommand(command: string): Promise<void> { export async function showUnknownApiCommand(command: string): Promise<void> {
+62 -16
View File
@@ -10,22 +10,68 @@ import { handleApiRemoveCommand } from './remove-command';
export { parseApiCommandArgs } from './shared'; export { parseApiCommandArgs } from './shared';
const API_COMMAND_ROUTES: readonly NamedCommandRoute[] = [ type ApiCommandHandler = (args: string[]) => Promise<void>;
{ name: 'create', handle: handleApiCreateCommand }, type ApiCommandHelpHandler = () => Promise<void>;
{ name: 'list', handle: handleApiListCommand }, type ApiCommandUnknownHandler = (command: string) => Promise<void>;
{ name: 'discover', handle: handleApiDiscoverCommand },
{ name: 'copy', handle: handleApiCopyCommand }, export interface ApiCommandDependencies {
{ name: 'export', handle: handleApiExportCommand }, help: ApiCommandHelpHandler;
{ name: 'import', handle: handleApiImportCommand }, unknown: ApiCommandUnknownHandler;
{ name: 'remove', aliases: ['delete', 'rm'], handle: handleApiRemoveCommand }, 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<ApiCommandDependencies> = {}
): (args: string[]) => Promise<void> {
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<void> { export async function handleApiCommand(args: string[]): Promise<void> {
await dispatchNamedCommand({ await createApiCommandHandler()(args);
args,
routes: API_COMMAND_ROUTES,
onHelp: showApiCommandHelp,
onUnknown: showUnknownApiCommand,
allowEmptyHelp: true,
});
} }
+4 -3
View File
@@ -294,9 +294,10 @@ export function applyClaudeExtendedContextPreference(
continue; continue;
} }
nextModels[key] = enabled && likelySupportsClaudeExtendedContext(value) nextModels[key] =
? applyExtendedContextSuffix(value) enabled && likelySupportsClaudeExtendedContext(value)
: stripExtendedContextSuffix(value); ? applyExtendedContextSuffix(value)
: stripExtendedContextSuffix(value);
} }
return nextModels; return nextModels;
+337 -247
View File
@@ -5,6 +5,8 @@ import { isUnifiedMode } from '../config/unified-config-loader';
import { getCcsDir, getCcsDirSource } from '../utils/config-manager'; import { getCcsDir, getCcsDirSource } from '../utils/config-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-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) // Get version from package.json (same as version-command.ts)
const VERSION = JSON.parse( const VERSION = JSON.parse(
fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8') fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8')
@@ -19,28 +21,33 @@ const VERSION = JSON.parse(
* *
* command Description * 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 // Section header with ═══ borders
console.log(sectionHeader(title)); writeLine(sectionHeader(title));
// Subtitles on separate lines (dim) // Subtitles on separate lines (dim)
for (const subtitle of subtitles) { for (const subtitle of subtitles) {
console.log(` ${dim(subtitle)}`); writeLine(` ${dim(subtitle)}`);
} }
// Empty line before items // Empty line before items
console.log(''); writeLine('');
// Calculate max command length for alignment // Calculate max command length for alignment
const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length)); const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length));
for (const [cmd, desc] of items) { for (const [cmd, desc] of items) {
const paddedCmd = cmd.padEnd(maxCmdLen + 2); const paddedCmd = cmd.padEnd(maxCmdLen + 2);
console.log(` ${color(paddedCmd, 'command')} ${desc}`); writeLine(` ${color(paddedCmd, 'command')} ${desc}`);
} }
// Extra spacing after section // Extra spacing after section
console.log(''); writeLine('');
} }
/** /**
@@ -49,20 +56,24 @@ function printMajorSection(title: string, subtitles: string[], items: [string, s
* Title (context): * Title (context):
* command Description * 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) // Sub-section header (colored, no borders)
console.log(subheader(`${title}:`)); writeLine(subheader(`${title}:`));
// Calculate max command length for alignment // Calculate max command length for alignment
const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length)); const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length));
for (const [cmd, desc] of items) { for (const [cmd, desc] of items) {
const paddedCmd = cmd.padEnd(maxCmdLen + 2); const paddedCmd = cmd.padEnd(maxCmdLen + 2);
console.log(` ${color(paddedCmd, 'command')} ${desc}`); writeLine(` ${color(paddedCmd, 'command')} ${desc}`);
} }
// Spacing after section // Spacing after section
console.log(''); writeLine('');
} }
/** /**
@@ -71,24 +82,28 @@ function printSubSection(title: string, items: [string, string][]): void {
* Title: * Title:
* Label: path * Label: path
*/ */
function printConfigSection(title: string, items: [string, string][]): void { function printConfigSection(
console.log(subheader(`${title}:`)); title: string,
items: [string, string][],
writeLine: HelpWriter = console.log
): void {
writeLine(subheader(`${title}:`));
// Calculate max label length for alignment // Calculate max label length for alignment
const maxLabelLen = Math.max(...items.map(([label]) => label.length)); const maxLabelLen = Math.max(...items.map(([label]) => label.length));
for (const [label, path] of items) { for (const [label, path] of items) {
const paddedLabel = label.padEnd(maxLabelLen); 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) * Display comprehensive help information for CCS (Claude Code Switch)
*/ */
export async function handleHelpCommand(): Promise<void> { export async function handleHelpCommand(writeLine: HelpWriter = console.log): Promise<void> {
// Initialize UI (if not already) // Initialize UI (if not already)
await initUI(); await initUI();
@@ -103,24 +118,24 @@ Claude Code Profile & Model Switcher
Run ${color('ccs config', 'command')} for web dashboard`.trim(); Run ${color('ccs config', 'command')} for web dashboard`.trim();
console.log( writeLine(
box(logo, { box(logo, {
padding: 1, padding: 1,
borderStyle: 'round', borderStyle: 'round',
titleAlignment: 'center', titleAlignment: 'center',
}) })
); );
console.log(''); writeLine('');
// Resolve display path for dynamic sections // Resolve display path for dynamic sections
const [dirSource] = getCcsDirSource(); const [dirSource] = getCcsDirSource();
const dirDisplay = dirSource === 'default' ? '~/.ccs' : getCcsDir(); const dirDisplay = dirSource === 'default' ? '~/.ccs' : getCcsDir();
// Usage section // Usage section
console.log(subheader('Usage:')); writeLine(subheader('Usage:'));
console.log(` ${color('ccs', 'command')} [profile] [claude-args...]`); writeLine(` ${color('ccs', 'command')} [profile] [claude-args...]`);
console.log(` ${color('ccs', 'command')} [flags]`); writeLine(` ${color('ccs', 'command')} [flags]`);
console.log(''); writeLine('');
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// MAJOR SECTION 1: API Key Profiles // MAJOR SECTION 1: API Key Profiles
@@ -152,7 +167,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs api import <file>', 'Import profile bundle'], ['ccs api import <file>', 'Import profile bundle'],
['ccs api remove', 'Remove an API profile'], ['ccs api remove', 'Remove an API profile'],
['ccs api list', 'List all API profiles'], ['ccs api list', 'List all API profiles'],
] ],
writeLine
); );
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
@@ -176,7 +192,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs auth default <name>', 'Set default profile'], ['ccs auth default <name>', 'Set default profile'],
['ccs auth reset-default', 'Restore original CCS default'], ['ccs auth reset-default', 'Restore original CCS default'],
['ccs cliproxy auth claude', 'Alternative: authenticate Claude account pool via CLIProxy'], ['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 --import', 'Import token from Kiro IDE'],
['ccs kiro --incognito', 'Use incognito browser (default: normal)'], ['ccs kiro --incognito', 'Use incognito browser (default: normal)'],
['ccs codex "explain code"', 'Use with prompt'], ['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 stop', 'Stop copilot-api daemon'],
['ccs copilot enable', 'Enable integration'], ['ccs copilot enable', 'Enable integration'],
['ccs copilot disable', 'Disable 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 stop', 'Stop proxy daemon'],
['ccs cursor enable', 'Enable cursor integration'], ['ccs cursor enable', 'Enable cursor integration'],
['ccs cursor disable', 'Disable cursor integration'], ['ccs cursor disable', 'Disable cursor integration'],
] ],
writeLine
); );
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
@@ -285,281 +305,351 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// Delegation // Delegation
printSubSection('Delegation (inside Claude Code CLI)', [ printSubSection(
['/ccs "task"', 'Delegate task (auto-selects profile)'], 'Delegation (inside Claude Code CLI)',
['/ccs --glm "task"', 'Force GLM-5 for simple tasks'], [
['/ccs --kimi "task"', 'Force Kimi OAuth for long context'], ['/ccs "task"', 'Delegate task (auto-selects profile)'],
['/ccs --km "task"', 'Force Kimi API key for long context'], ['/ccs --glm "task"', 'Force GLM-5 for simple tasks'],
['/ccs:continue "follow-up"', 'Continue last delegation session'], ['/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) // Delegation CLI Flags (Claude Code passthrough)
printSubSection('Delegation Flags (Claude Code passthrough)', [ printSubSection(
['--max-turns <n>', 'Limit agentic turns (prevents loops)'], 'Delegation Flags (Claude Code passthrough)',
['--fallback-model <model>', 'Auto-fallback on overload (sonnet)'], [
['--agents <json>', 'Inject dynamic subagents'], ['--max-turns <n>', 'Limit agentic turns (prevents loops)'],
['--betas <features>', 'Enable experimental features'], ['--fallback-model <model>', 'Auto-fallback on overload (sonnet)'],
['--allowedTools <list>', 'Restrict available tools'], ['--agents <json>', 'Inject dynamic subagents'],
['--disallowedTools <list>', 'Block specific tools'], ['--betas <features>', 'Enable experimental features'],
]); ['--allowedTools <list>', 'Restrict available tools'],
['--disallowedTools <list>', 'Block specific tools'],
],
writeLine
);
// Diagnostics // Diagnostics
printSubSection('Diagnostics', [ printSubSection(
['ccs setup', 'First-time setup wizard'], 'Diagnostics',
['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', ['ccs setup', 'First-time setup wizard'],
'Auto-add Telegram + Discord on supported native Claude runs', ['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=<token>', '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 <profile>', '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=<token>', 'Save TELEGRAM_BOT_TOKEN'], writeLine
['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 <profile>', '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)'],
]);
// Environment export // Environment export
printSubSection('Environment Export', [ printSubSection(
['ccs env <profile>', 'Export env vars for third-party tools'], 'Environment Export',
['ccs env <profile> --format openai', 'OpenAI-compatible vars (OpenCode/Cursor)'],
['ccs env <profile> --format anthropic', 'Anthropic vars (default)'],
['ccs env <profile> --format raw', 'All effective env vars'],
[ [
'ccs env <profile> --format claude-extension --ide vscode', ['ccs env <profile>', 'Export env vars for third-party tools'],
'VS Code/Cursor Claude extension settings JSON', ['ccs env <profile> --format openai', 'OpenAI-compatible vars (OpenCode/Cursor)'],
['ccs env <profile> --format anthropic', 'Anthropic vars (default)'],
['ccs env <profile> --format raw', 'All effective env vars'],
[
'ccs env <profile> --format claude-extension --ide vscode',
'VS Code/Cursor Claude extension settings JSON',
],
[
'ccs env <profile> --format claude-extension --ide windsurf',
'Windsurf Claude extension settings JSON',
],
['ccs env <profile> --shell fish', 'Fish shell syntax'],
], ],
[ writeLine
'ccs env <profile> --format claude-extension --ide windsurf', );
'Windsurf Claude extension settings JSON',
],
['ccs env <profile> --shell fish', 'Fish shell syntax'],
]);
// Flags // Flags
printSubSection('Flags', [ printSubSection(
['--config-dir <path>', 'Use custom CCS config directory'], 'Flags',
['--target <cli>', 'Target CLI: claude (default), droid'], [
['-h, --help', 'Show this help message'], ['--config-dir <path>', 'Use custom CCS config directory'],
['-v, --version', 'Show version and installation info'], ['--target <cli>', 'Target CLI: claude (default), droid'],
['-sc, --shell-completion', 'Install shell auto-completion'], ['-h, --help', 'Show this help message'],
]); ['-v, --version', 'Show version and installation info'],
['-sc, --shell-completion', 'Install shell auto-completion'],
],
writeLine
);
// Aliases // Aliases
printSubSection('Aliases', [ printSubSection(
['ccs-droid <profile> [args]', 'Explicit Droid runtime alias'], 'Aliases',
['ccsd <profile> [args]', 'Legacy shortcut for: ccs-droid <profile> [args]'], [
]); ['ccs-droid <profile> [args]', 'Explicit Droid runtime alias'],
['ccsd <profile> [args]', 'Legacy shortcut for: ccs-droid <profile> [args]'],
],
writeLine
);
// Multi-target examples // Multi-target examples
printSubSection('Multi-Target', [ printSubSection(
['ccs glm --target droid', 'Run GLM profile on Droid CLI'], 'Multi-Target',
['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"', ['ccs glm --target droid', 'Run GLM profile on Droid CLI'],
'Pass through Droid exec mode', ['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'], writeLine
[ );
'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)'],
]);
// Configuration // Configuration
printConfigSection('Configuration', [ printConfigSection(
['Config File:', isUnifiedMode() ? `${dirDisplay}/config.yaml` : `${dirDisplay}/config.json`], 'Configuration',
['Profiles:', `${dirDisplay}/profiles.json`], [
['Instances:', `${dirDisplay}/instances/`], ['Config File:', isUnifiedMode() ? `${dirDisplay}/config.yaml` : `${dirDisplay}/config.json`],
['Settings:', `${dirDisplay}/*.settings.json`], ['Profiles:', `${dirDisplay}/profiles.json`],
]); ['Instances:', `${dirDisplay}/instances/`],
['Settings:', `${dirDisplay}/*.settings.json`],
],
writeLine
);
// CLI Proxy management // CLI Proxy management
printSubSection('CLI Proxy Plus Management', [ printSubSection(
['ccs cliproxy', 'Show CLIProxy Plus status and version'], 'CLI Proxy Plus Management',
['ccs cliproxy --help', 'Full CLIProxy Plus management help'], [
['ccs cliproxy doctor', 'Quota diagnostics (Antigravity)'], ['ccs cliproxy', 'Show CLIProxy Plus status and version'],
['ccs cliproxy --install <ver>', 'Install specific version (e.g., 6.6.6)'], ['ccs cliproxy --help', 'Full CLIProxy Plus management help'],
['ccs cliproxy --latest', 'Update to latest version'], ['ccs cliproxy doctor', 'Quota diagnostics (Antigravity)'],
['', ''], // Spacer ['ccs cliproxy --install <ver>', 'Install specific version (e.g., 6.6.6)'],
['ccs cliproxy pause <p> <a>', 'Pause account from rotation'], ['ccs cliproxy --latest', 'Update to latest version'],
['ccs cliproxy resume <p> <a>', 'Resume paused account'], ['', ''], // Spacer
['ccs cliproxy status', 'Show CLIProxy process status'], ['ccs cliproxy pause <p> <a>', 'Pause account from rotation'],
['ccs cliproxy quota', 'Show quota/tier/pause status for all providers'], ['ccs cliproxy resume <p> <a>', 'Resume paused account'],
['ccs cliproxy quota --provider <name>', 'Show quota/tier/pause status for one provider'], ['ccs cliproxy status', 'Show CLIProxy process status'],
]); ['ccs cliproxy quota', 'Show quota/tier/pause status for all providers'],
['ccs cliproxy quota --provider <name>', 'Show quota/tier/pause status for one provider'],
],
writeLine
);
// CLI Proxy configuration flags (new) // CLI Proxy configuration flags (new)
printSubSection('CLI Proxy Configuration', [ printSubSection(
['--proxy-host <host>', 'Remote proxy hostname/IP'], 'CLI Proxy Configuration',
['--proxy-port <port>', `Proxy port (default: ${CLIPROXY_DEFAULT_PORT})`], [
['--proxy-protocol <proto>', 'Protocol: http or https (default: http)'], ['--proxy-host <host>', 'Remote proxy hostname/IP'],
['--proxy-auth-token <token>', 'Auth token for remote proxy'], ['--proxy-port <port>', `Proxy port (default: ${CLIPROXY_DEFAULT_PORT})`],
['--proxy-timeout <ms>', 'Connection timeout in ms (default: 2000)'], ['--proxy-protocol <proto>', 'Protocol: http or https (default: http)'],
['--local-proxy', 'Force local mode, ignore remote config'], ['--proxy-auth-token <token>', 'Auth token for remote proxy'],
['--remote-only', 'Fail if remote unreachable (no fallback)'], ['--proxy-timeout <ms>', 'Connection timeout in ms (default: 2000)'],
['--allow-self-signed', 'Allow self-signed certs (for dev proxies)'], ['--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 // W3: Thinking Budget explanation
printSubSection('Extended Thinking / Reasoning', [ printSubSection(
['--thinking off', 'Disable extended thinking'], 'Extended Thinking / Reasoning',
['--thinking auto', 'Let model decide dynamically'], [
['--thinking low', '1K tokens - Quick responses'], ['--thinking off', 'Disable extended thinking'],
['--thinking medium', '8K tokens - Standard analysis'], ['--thinking auto', 'Let model decide dynamically'],
['--thinking high', '24K tokens - Deep reasoning'], ['--thinking low', '1K tokens - Quick responses'],
['--thinking xhigh', '32K tokens - Maximum depth'], ['--thinking medium', '8K tokens - Standard analysis'],
['--thinking <number>', 'Custom token budget (512-100000)'], ['--thinking high', '24K tokens - Deep reasoning'],
['', ''], ['--thinking xhigh', '32K tokens - Maximum depth'],
['--effort <level>', 'Codex alias for reasoning effort (medium/high/xhigh)'], ['--thinking <number>', 'Custom token budget (512-100000)'],
['--effort xhigh', 'Pin Codex effort to xhigh for this run'], ['', ''],
['', ''], ['--effort <level>', 'Codex alias for reasoning effort (medium/high/xhigh)'],
['Droid exec:', 'Use native Droid flag: --reasoning-effort <level>'], ['--effort xhigh', 'Pin Codex effort to xhigh for this run'],
['', 'CCS auto-maps --thinking/--effort to --reasoning-effort in droid exec mode.'], ['', ''],
['', 'For interactive droid sessions, CCS applies reasoning via Droid BYOK model config.'], ['Droid exec:', 'Use native Droid flag: --reasoning-effort <level>'],
['', 'When multiple reasoning flags are provided, the first flag wins.'], ['', 'CCS auto-maps --thinking/--effort to --reasoning-effort in droid exec mode.'],
['', ''], ['', 'For interactive droid sessions, CCS applies reasoning via Droid BYOK model config.'],
['Note:', 'Extended thinking allocates compute for step-by-step reasoning'], ['', 'When multiple reasoning flags are provided, the first flag wins.'],
['', 'before responding.'], ['', ''],
['', 'Providers: agy/gemini use --thinking, codex uses --effort (or --thinking alias).'], ['Note:', 'Extended thinking allocates compute for step-by-step reasoning'],
['', 'Codex model suffixes also pin effort: -medium / -high / -xhigh.'], ['', '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) // Extended Context (1M)
printSubSection('Extended Context (--1m)', [ printSubSection(
['--1m', 'Request 1M token context when the selected model supports [1m]'], 'Extended Context (--1m)',
['--no-1m', 'Force standard context (Claude default stays plain)'], [
['', ''], ['--1m', 'Request 1M token context when the selected model supports [1m]'],
['Auto behavior:', 'Gemini models: CCS auto-adds [1m] when supported'], ['--no-1m', 'Force standard context (Claude default stays plain)'],
['', 'Claude models: plain by default, opt-in with --1m or saved [1m]'], ['', ''],
['', ''], ['Auto behavior:', 'Gemini models: CCS auto-adds [1m] when supported'],
['Note:', 'CCS only controls the saved [1m] suffix.'], ['', 'Claude models: plain by default, opt-in with --1m or saved [1m]'],
['', 'Provider pricing and entitlement stay upstream.'], ['', ''],
['', 'Some accounts/providers can still return 429 extra-usage errors for long-context requests.'], ['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 // Image Analysis
printSubSection('Image Analysis (CLIProxy vision)', [ printSubSection(
['ccs config image-analysis', 'Show current settings'], 'Image Analysis (CLIProxy vision)',
['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 <p> <m>', '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'],
[ [
'ccs config channels --set telegram,discord', ['ccs config image-analysis', 'Show current settings'],
'Auto-add selected channels on native Claude default/account sessions', ['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 <p> <m>', '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'], writeLine
['ccs config channels --unattended', 'Also add --dangerously-skip-permissions'], );
['ccs config channels --set-token telegram=<token>', 'Save TELEGRAM_BOT_TOKEN'],
['ccs config channels --set-token discord=<token>', 'Save DISCORD_BOT_TOKEN'], printSubSection(
['ccs config channels --clear-token [channel]', 'Remove one or all saved channel tokens'], 'Official Channels (official Claude plugins)',
['', ''], [
['', 'Fastest path: turn on the channel, save the token if needed, then run ccs.'], ['ccs config', 'Dashboard -> Settings -> Channels (fastest path)'],
['Note:', 'Runtime-only. Applies to native Claude default/account sessions only.'], ['ccs config channels', 'Show current status'],
['', 'Not supported for ccs glm, other API/OAuth profiles, or Droid targets.'], [
['', 'Telegram/Discord tokens live in ~/.claude/channels/<channel>/.env.'], 'ccs config channels --set telegram,discord',
['', 'Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work for that launch.'], 'Auto-add selected channels on native Claude default/account sessions',
['', 'iMessage is macOS-only and requires local OS permissions instead of a bot token.'], ],
]); ['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=<token>', 'Save TELEGRAM_BOT_TOKEN'],
['ccs config channels --set-token discord=<token>', '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/<channel>/.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 // CCS Environment Variables
printSubSection('Environment Variables', [ printSubSection(
['CCS_DIR', 'Override CCS config directory (default: ~/.ccs)'], 'Environment Variables',
['CCS_HOME', 'Override home directory (legacy, appends .ccs)'], [
['CCS_DEBUG', 'Enable debug logging'], ['CCS_DIR', 'Override CCS config directory (default: ~/.ccs)'],
['CCS_THINKING', 'Override thinking level (flag > env > config)'], ['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 // CLI Proxy env vars
printSubSection('CLI Proxy Environment Variables', [ printSubSection(
['CCS_PROXY_HOST', 'Remote proxy hostname'], 'CLI Proxy Environment Variables',
['CCS_PROXY_PORT', 'Proxy port'], [
['CCS_PROXY_PROTOCOL', 'Protocol (http/https)'], ['CCS_PROXY_HOST', 'Remote proxy hostname'],
['CCS_PROXY_AUTH_TOKEN', 'Auth token'], ['CCS_PROXY_PORT', 'Proxy port'],
['CCS_PROXY_TIMEOUT', 'Connection timeout in ms'], ['CCS_PROXY_PROTOCOL', 'Protocol (http/https)'],
['CCS_PROXY_FALLBACK_ENABLED', 'Enable local fallback (1/0)'], ['CCS_PROXY_AUTH_TOKEN', 'Auth token'],
['CCS_ALLOW_SELF_SIGNED', 'Allow self-signed certs (1/0)'], ['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 // CLI Proxy paths
console.log(subheader('CLI Proxy:')); writeLine(subheader('CLI Proxy:'));
console.log(` Binary: ${color(`${dirDisplay}/cliproxy/bin/cli-proxy-api-plus`, 'path')}`); writeLine(` Binary: ${color(`${dirDisplay}/cliproxy/bin/cli-proxy-api-plus`, 'path')}`);
console.log(` Config: ${color(`${dirDisplay}/cliproxy/config.yaml`, 'path')}`); writeLine(` Config: ${color(`${dirDisplay}/cliproxy/config.yaml`, 'path')}`);
console.log(` Auth: ${color(`${dirDisplay}/cliproxy/auth/`, 'path')}`); writeLine(` Auth: ${color(`${dirDisplay}/cliproxy/auth/`, 'path')}`);
console.log(` ${dim(`Port: ${CLIPROXY_DEFAULT_PORT} (default)`)}`); writeLine(` ${dim(`Port: ${CLIPROXY_DEFAULT_PORT} (default)`)}`);
console.log(''); writeLine('');
// Shared Data // Shared Data
console.log(subheader('Shared Data:')); writeLine(subheader('Shared Data:'));
console.log(` Commands: ${color(`${dirDisplay}/shared/commands/`, 'path')}`); writeLine(` Commands: ${color(`${dirDisplay}/shared/commands/`, 'path')}`);
console.log(` Skills: ${color(`${dirDisplay}/shared/skills/`, 'path')}`); writeLine(` Skills: ${color(`${dirDisplay}/shared/skills/`, 'path')}`);
console.log(` Agents: ${color(`${dirDisplay}/shared/agents/`, 'path')}`); writeLine(` Agents: ${color(`${dirDisplay}/shared/agents/`, 'path')}`);
console.log(` ${dim('Note: Symlinked across all profiles')}`); writeLine(` ${dim('Note: Symlinked across all profiles')}`);
console.log(''); writeLine('');
// Examples (aligned with consistent spacing) // Examples (aligned with consistent spacing)
console.log(subheader('Examples:')); writeLine(subheader('Examples:'));
console.log(` $ ${color('ccs', 'command')} ${dim('# Use default account')}`); writeLine(` $ ${color('ccs', 'command')} ${dim('# Use default account')}`);
console.log( writeLine(
` $ ${color('ccs gemini', 'command')} ${dim('# OAuth (browser opens first time)')}` ` $ ${color('ccs gemini', 'command')} ${dim('# OAuth (browser opens first time)')}`
); );
console.log(` $ ${color('ccs glm "implement API"', 'command')} ${dim('# API key model')}`); writeLine(` $ ${color('ccs glm "implement API"', 'command')} ${dim('# API key model')}`);
console.log(` $ ${color('ccs config', 'command')} ${dim('# Open web dashboard')}`); writeLine(` $ ${color('ccs config', 'command')} ${dim('# Open web dashboard')}`);
console.log(''); writeLine('');
// Update examples // Update examples
console.log(subheader('Update:')); writeLine(subheader('Update:'));
console.log( writeLine(
` $ ${color('ccs update', 'command')} ${dim('# Update to latest stable')}` ` $ ${color('ccs update', 'command')} ${dim('# Update to latest stable')}`
); );
console.log( writeLine(
` $ ${color('ccs update --force', 'command')} ${dim('# Force reinstall current')}` ` $ ${color('ccs update --force', 'command')} ${dim('# Force reinstall current')}`
); );
console.log(` $ ${color('ccs update --beta', 'command')} ${dim('# Install dev channel')}`); writeLine(` $ ${color('ccs update --beta', 'command')} ${dim('# Install dev channel')}`);
console.log(''); writeLine('');
// Docs link // Docs link
console.log(` ${dim('Docs: https://github.com/kaitranntt/ccs')}`); writeLine(` ${dim('Docs: https://github.com/kaitranntt/ccs')}`);
console.log(''); writeLine('');
// Uninstall // Uninstall
console.log(subheader('Uninstall:')); writeLine(subheader('Uninstall:'));
console.log(` ${color('npm uninstall -g @kaitranntt/ccs', 'command')}`); writeLine(` ${color('npm uninstall -g @kaitranntt/ccs', 'command')}`);
console.log(''); writeLine('');
// License // License
console.log(dim('License: MIT')); writeLine(dim('License: MIT'));
console.log(''); writeLine('');
} }
+23 -49
View File
@@ -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[] = []; let calls: string[] = [];
beforeEach(() => { beforeEach(() => {
calls = []; calls = [];
});
mock.module('../../../src/commands/api-command/help', () => ({ function buildHandleApiCommand() {
showApiCommandHelp: async () => { return createApiCommandHandler({
help: async () => {
calls.push('help'); calls.push('help');
}, },
showUnknownApiCommand: async (command: string) => { unknown: async (command: string) => {
calls.push(`unknown:${command}`); calls.push(`unknown:${command}`);
}, },
})); create: async (args: string[]) => {
mock.module('../../../src/commands/api-command/create-command', () => ({
handleApiCreateCommand: async (args: string[]) => {
calls.push(`create:${args.join(' ')}`); calls.push(`create:${args.join(' ')}`);
}, },
})); list: async (args: string[]) => {
mock.module('../../../src/commands/api-command/list-command', () => ({
handleApiListCommand: async (args: string[]) => {
calls.push(`list:${args.join(' ')}`); calls.push(`list:${args.join(' ')}`);
}, },
})); discover: async (args: string[]) => {
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[]) => {
calls.push(`discover:${args.join(' ')}`); calls.push(`discover:${args.join(' ')}`);
}, },
})); copy: async (args: string[]) => {
mock.module('../../../src/commands/api-command/copy-command', () => ({
handleApiCopyCommand: async (args: string[]) => {
calls.push(`copy:${args.join(' ')}`); calls.push(`copy:${args.join(' ')}`);
}, },
})); export: async (args: string[]) => {
mock.module('../../../src/commands/api-command/export-command', () => ({
handleApiExportCommand: async (args: string[]) => {
calls.push(`export:${args.join(' ')}`); calls.push(`export:${args.join(' ')}`);
}, },
})); import: async (args: string[]) => {
mock.module('../../../src/commands/api-command/import-command', () => ({
handleApiImportCommand: async (args: string[]) => {
calls.push(`import:${args.join(' ')}`); calls.push(`import:${args.join(' ')}`);
}, },
})); remove: async (args: string[]) => {
}); calls.push(`remove:${args.join(' ')}`);
},
afterEach(() => { });
mock.restore();
});
async function loadHandleApiCommand() {
const mod = await import(`../../../src/commands/api-command?test=${Date.now()}-${Math.random()}`);
return mod.handleApiCommand;
} }
describe('api-command router', () => { describe('api-command router', () => {
it('defaults to help when no subcommand is provided', async () => { it('defaults to help when no subcommand is provided', async () => {
const handleApiCommand = await loadHandleApiCommand(); const handleApiCommand = buildHandleApiCommand();
await handleApiCommand([]); await handleApiCommand([]);
@@ -76,7 +50,7 @@ describe('api-command router', () => {
}); });
it('routes remove aliases through the named command dispatcher', async () => { it('routes remove aliases through the named command dispatcher', async () => {
const handleApiCommand = await loadHandleApiCommand(); const handleApiCommand = buildHandleApiCommand();
await handleApiCommand(['rm', 'profile-a']); await handleApiCommand(['rm', 'profile-a']);
@@ -84,7 +58,7 @@ describe('api-command router', () => {
}); });
it('forwards list arguments to the handler for validation', async () => { it('forwards list arguments to the handler for validation', async () => {
const handleApiCommand = await loadHandleApiCommand(); const handleApiCommand = buildHandleApiCommand();
await handleApiCommand(['list', 'unexpected']); await handleApiCommand(['list', 'unexpected']);
@@ -92,7 +66,7 @@ describe('api-command router', () => {
}); });
it('routes hardened subcommands through their handlers', async () => { it('routes hardened subcommands through their handlers', async () => {
const handleApiCommand = await loadHandleApiCommand(); const handleApiCommand = buildHandleApiCommand();
await handleApiCommand(['discover', '--json']); await handleApiCommand(['discover', '--json']);
await handleApiCommand(['copy', 'source', 'dest']); await handleApiCommand(['copy', 'source', 'dest']);
@@ -108,7 +82,7 @@ describe('api-command router', () => {
}); });
it('delegates unknown commands to the shared unknown handler', async () => { it('delegates unknown commands to the shared unknown handler', async () => {
const handleApiCommand = await loadHandleApiCommand(); const handleApiCommand = buildHandleApiCommand();
await handleApiCommand(['bogus']); await handleApiCommand(['bogus']);
@@ -15,20 +15,6 @@ beforeEach(() => {
logLines.push(args.map(String).join(' ')); 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', () => ({ mock.module('../../../src/commands/config-auth/setup-command', () => ({
handleSetup: async () => { handleSetup: async () => {
calls.push('setup'); calls.push('setup');
@@ -55,9 +41,7 @@ afterEach(() => {
}); });
async function loadHandleConfigAuthCommand() { async function loadHandleConfigAuthCommand() {
const mod = await import( const mod = await import(`../../../src/commands/config-auth?test=${Date.now()}-${Math.random()}`);
`../../../src/commands/config-auth?test=${Date.now()}-${Math.random()}`
);
return mod.handleConfigAuthCommand; return mod.handleConfigAuthCommand;
} }
+19 -57
View File
@@ -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 { showApiCommandHelp } from '../../../src/commands/api-command/help';
import { handleHelpCommand } from '../../../src/commands/help-command'; import { handleHelpCommand } from '../../../src/commands/help-command';
@@ -8,19 +8,9 @@ function stripAnsi(input: string): string {
} }
describe('help command parity', () => { describe('help command parity', () => {
const originalLog = console.log;
afterEach(() => {
console.log = originalLog;
});
test('root help documents cliproxy provider filter under quota command', async () => { test('root help documents cliproxy provider filter under quota command', async () => {
const lines: string[] = []; const lines: string[] = [];
console.log = (...args: unknown[]) => { await handleHelpCommand((line) => lines.push(line));
lines.push(args.map((arg) => String(arg)).join(' '));
};
await handleHelpCommand();
const rendered = stripAnsi(lines.join('\n')); const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('ccs cliproxy status [provider]')).toBe(false); 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 () => { test('root help documents llama.cpp as a local API profile', async () => {
const lines: string[] = []; const lines: string[] = [];
console.log = (...args: unknown[]) => { await handleHelpCommand((line) => lines.push(line));
lines.push(args.map((arg) => String(arg)).join(' '));
};
await handleHelpCommand();
const rendered = stripAnsi(lines.join('\n')); const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('ccs llamacpp')).toBe(true); 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 () => { test('root help no longer markets glmt as a supported profile', async () => {
const lines: string[] = []; const lines: string[] = [];
console.log = (...args: unknown[]) => { await handleHelpCommand((line) => lines.push(line));
lines.push(args.map((arg) => String(arg)).join(' '));
};
await handleHelpCommand();
const rendered = stripAnsi(lines.join('\n')); const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('ccs glmt')).toBe(false); 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 () => { test('root help documents Claude IDE extension setup surfaces', async () => {
const lines: string[] = []; const lines: string[] = [];
console.log = (...args: unknown[]) => { await handleHelpCommand((line) => lines.push(line));
lines.push(args.map((arg) => String(arg)).join(' '));
};
await handleHelpCommand();
const rendered = stripAnsi(lines.join('\n')); const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('Claude IDE Extension setup page')).toBe(true); 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 () => { test('root help documents dashboard host binding example', async () => {
const lines: string[] = []; const lines: string[] = [];
console.log = (...args: unknown[]) => { await handleHelpCommand((line) => lines.push(line));
lines.push(args.map((arg) => String(arg)).join(' '));
};
await handleHelpCommand();
const rendered = stripAnsi(lines.join('\n')); const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('ccs config --host 0.0.0.0')).toBe(true); 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 () => { test('root help documents official channels native-only scope and process-env tokens', async () => {
const lines: string[] = []; const lines: string[] = [];
console.log = (...args: unknown[]) => { await handleHelpCommand((line) => lines.push(line));
lines.push(args.map((arg) => String(arg)).join(' '));
};
await handleHelpCommand();
const rendered = stripAnsi(lines.join('\n')); const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('Dashboard -> Settings -> Channels (fastest path)')).toBe(true); expect(rendered.includes('Dashboard -> Settings -> Channels (fastest path)')).toBe(true);
expect( 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); ).toBe(true);
expect(rendered.includes('Not supported for ccs glm')).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( expect(
true 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 () => { test('root help explains Claude [1m] as an explicit CCS suffix with upstream limits', async () => {
const lines: string[] = []; const lines: string[] = [];
console.log = (...args: unknown[]) => { await handleHelpCommand((line) => lines.push(line));
lines.push(args.map((arg) => String(arg)).join(' '));
};
await handleHelpCommand();
const rendered = stripAnsi(lines.join('\n')); const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('Claude models: plain by default, opt-in with --1m or saved [1m]')).toBe( expect(
true 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('CCS only controls the saved [1m] suffix.')).toBe(true);
expect(rendered.includes('return 429 extra-usage errors for long-context requests')).toBe( expect(rendered.includes('return 429 extra-usage errors for long-context requests')).toBe(true);
true
);
}); });
test('api help documents create-time Claude [1m] flags and entitlement warning', async () => { test('api help documents create-time Claude [1m] flags and entitlement warning', async () => {
const lines: string[] = []; const lines: string[] = [];
console.log = (...args: unknown[]) => { await showApiCommandHelp((line) => lines.push(line));
lines.push(args.map((arg) => String(arg)).join(' '));
};
await showApiCommandHelp();
const rendered = stripAnsi(lines.join('\n')); const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('--1m / --no-1m')).toBe(true); expect(rendered.includes('--1m / --no-1m')).toBe(true);