feat(cli): refresh help and shell completion UX

- add a shared command catalog for root help and completion routing

- replace shell-local completion logic with a hidden __complete backend

- add topic help and parity tests for router, help, and completion coverage
This commit is contained in:
Tam Nhu Tran committed 2026-04-03 00:53:10 -04:00
1 parent 3eb55d0e33
commit 24ba2abe10
15 files changed
+1227 -2118

No files matched your search

+10 -4
View File
@@ -318,10 +318,11 @@ async function main(): Promise<void> {
registerTarget(new CodexAdapter());
const args = process.argv.slice(2);
const isCompletionCommand = args[0] === '__complete';
// Initialize UI colors early to ensure consistent colored output
// Must happen before any status messages (ok, info, fail, etc.)
if (process.stdout.isTTY && !process.env['CI']) {
if (!isCompletionCommand && process.stdout.isTTY && !process.env['CI']) {
const { initUI } = await import('./utils/ui');
await initUI();
}
@@ -361,7 +362,7 @@ async function main(): Promise<void> {
// Security warning: cloud sync paths expose OAuth tokens
const cloudService = detectCloudSyncPath(configDirValue);
if (cloudService) {
if (!isCompletionCommand && cloudService) {
console.error(warn(`CCS directory is under ${cloudService}.`));
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
@@ -372,7 +373,7 @@ async function main(): Promise<void> {
} else if (process.env.CCS_DIR) {
// Also warn for CCS_DIR env var pointing to cloud sync
const cloudService = detectCloudSyncPath(process.env.CCS_DIR);
if (cloudService) {
if (!isCompletionCommand && cloudService) {
console.error(warn(`CCS directory is under ${cloudService}.`));
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
@@ -380,13 +381,18 @@ async function main(): Promise<void> {
} else if (process.env.CCS_HOME) {
// Also warn for CCS_HOME env var pointing to cloud sync
const cloudService = detectCloudSyncPath(process.env.CCS_HOME);
if (cloudService) {
if (!isCompletionCommand && cloudService) {
console.error(warn(`CCS directory is under ${cloudService}.`));
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
}
}
if (isCompletionCommand) {
await tryHandleRootCommand(args);
return;
}
if (shouldPassthroughNativeCodexFlagCommand(args)) {
execNativeCodexFlagCommand(args);
return;
+330
View File
@@ -0,0 +1,330 @@
import { COPILOT_SUBCOMMANDS } from '../copilot/constants';
import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities';
import { CURSOR_SUBCOMMANDS } from './cursor-command';
export type HelpTopicName = 'profiles' | 'providers' | 'completion' | 'targets';
export interface HelpTopicEntry {
name: HelpTopicName;
summary: string;
}
export interface RootCommandEntry {
name: string;
summary: string;
group: 'start' | 'manage' | 'runtime' | 'operations';
visibility: 'public' | 'hidden';
aliases?: readonly string[];
}
export interface ShortcutEntry {
name: string;
summary: string;
}
export const ROOT_HELP_TOPICS: readonly HelpTopicEntry[] = [
{ name: 'profiles', summary: 'Account profiles, API profiles, and CLIProxy variants' },
{ name: 'providers', summary: 'Built-in OAuth providers and runtime shortcuts' },
{ name: 'completion', summary: 'Shell completion install, refresh, and testing' },
{ name: 'targets', summary: 'Claude, Droid, and Codex target routing' },
] as const;
export const ROOT_COMMAND_CATALOG: readonly RootCommandEntry[] = [
{
name: 'help',
summary: 'Show root help or help for a topic/command',
group: 'start',
aliases: ['--help', '-h'],
visibility: 'public',
},
{
name: 'version',
summary: 'Show version and install details',
group: 'start',
aliases: ['--version', '-v'],
visibility: 'public',
},
{
name: 'setup',
summary: 'Run the first-time setup wizard',
group: 'start',
aliases: ['--setup'],
visibility: 'public',
},
{
name: 'config',
summary: 'Open the dashboard and config subcommands',
group: 'start',
visibility: 'public',
},
{
name: 'doctor',
summary: 'Run health checks and diagnostics',
group: 'start',
aliases: ['--doctor'],
visibility: 'public',
},
{
name: 'auth',
summary: 'Manage concurrent Claude accounts',
group: 'manage',
visibility: 'public',
},
{
name: 'api',
summary: 'Create, discover, copy, export, and import API profiles',
group: 'manage',
visibility: 'public',
},
{
name: 'cliproxy',
summary: 'Manage CLIProxy variants, quota, and local lifecycle',
group: 'manage',
visibility: 'public',
},
{
name: 'env',
summary: 'Export profile env for third-party tools',
group: 'manage',
visibility: 'public',
},
{
name: 'persist',
summary: 'Write profile setup to ~/.claude/settings.json',
group: 'manage',
visibility: 'public',
},
{ name: 'tokens', summary: 'Manage CLIProxy auth tokens', group: 'manage', visibility: 'public' },
{
name: 'migrate',
summary: 'Move legacy JSON config to unified YAML config',
group: 'manage',
aliases: ['--migrate'],
visibility: 'public',
},
{
name: 'cursor',
summary: 'Run or manage the Cursor bridge',
group: 'runtime',
visibility: 'public',
},
{
name: 'copilot',
summary: 'Run or manage the GitHub Copilot bridge',
group: 'runtime',
visibility: 'public',
},
{
name: 'docker',
summary: 'Deploy or operate the bundled Docker stack',
group: 'operations',
visibility: 'public',
},
{
name: 'sync',
summary: 'Sync delegation commands and skills',
group: 'operations',
aliases: ['--sync'],
visibility: 'public',
},
{
name: 'update',
summary: 'Update CCS to the latest version',
group: 'operations',
aliases: ['--update'],
visibility: 'public',
},
{
name: 'cleanup',
summary: 'Remove old CLIProxy logs',
group: 'operations',
aliases: ['--cleanup'],
visibility: 'public',
},
{
name: '--shell-completion',
summary: 'Install shell completion',
group: 'operations',
aliases: ['-sc'],
visibility: 'hidden',
},
{
name: '--install',
summary: 'Post-install bootstrap hook',
group: 'operations',
visibility: 'hidden',
},
{
name: '--uninstall',
summary: 'Post-uninstall cleanup hook',
group: 'operations',
visibility: 'hidden',
},
{
name: '__complete',
summary: 'Hidden shell completion backend',
group: 'operations',
visibility: 'hidden',
},
] as const;
export const BUILTIN_PROVIDER_SHORTCUTS: readonly ShortcutEntry[] = CLIPROXY_PROVIDER_IDS.map(
(name) => ({
name,
summary:
{
gemini: 'Google Gemini via CLIProxy OAuth',
codex: 'OpenAI Codex via CLIProxy OAuth',
agy: 'Antigravity via CLIProxy OAuth',
qwen: 'Qwen Code via CLIProxy OAuth',
iflow: 'iFlow via CLIProxy OAuth',
kiro: 'Kiro via CLIProxy OAuth',
ghcp: 'GitHub Copilot via CLIProxy OAuth',
claude: 'Claude via CLIProxy OAuth',
kimi: 'Kimi via CLIProxy OAuth',
}[name] || 'CLIProxy OAuth provider',
})
);
export const ROOT_PROFILE_EXAMPLES: readonly ShortcutEntry[] = [
{ name: 'ccs auth create work', summary: 'Create a concurrent Claude account profile' },
{ name: 'ccs api create --preset glm', summary: 'Create a GLM-backed API profile' },
{
name: 'ccs api create --preset anthropic --1m',
summary: 'Create a Claude API profile with explicit [1m]',
},
{
name: 'ccs env <profile> --format claude-extension --ide vscode',
summary: 'Export IDE extension settings',
},
] as const;
export const ROOT_COMPATIBLE_ALIAS_EXAMPLES: readonly ShortcutEntry[] = [
{ name: '--target claude|droid|codex', summary: 'Route a profile to the target runtime' },
{ name: 'ccs-droid / ccsd', summary: 'Explicit Droid runtime aliases' },
{ name: 'ccs-codex / ccsx / ccsxp', summary: 'Explicit Codex runtime aliases' },
] as const;
export const ROOT_COMMAND_FLAGS = [
'--help',
'-h',
'--version',
'-v',
'--shell-completion',
'-sc',
] as const;
export const AUTH_SUBCOMMANDS = [
'create',
'list',
'show',
'remove',
'default',
'reset-default',
] as const;
export const API_SUBCOMMANDS = [
'create',
'list',
'discover',
'copy',
'export',
'import',
'remove',
] as const;
export const CLIPROXY_SUBCOMMANDS = [
'create',
'edit',
'list',
'remove',
'catalog',
'sync',
'quota',
'start',
'restart',
'status',
'stop',
'doctor',
'default',
'pause',
'resume',
] as const;
export const CONFIG_SUBCOMMANDS = ['auth', 'channels', 'image-analysis', 'thinking'] as const;
export const DOCKER_SUBCOMMANDS = ['up', 'down', 'status', 'update', 'logs', 'config'] as const;
export const TOKENS_FLAGS = [
'--show',
'--api-key',
'--secret',
'--regenerate-secret',
'--variant',
'--reset',
'--help',
'-h',
] as const;
export const MIGRATE_FLAGS = ['--dry-run', '--rollback', '--list-backups', '--help'] as const;
export const CLEANUP_FLAGS = [
'--errors',
'--days=',
'--dry-run',
'--force',
'--help',
'-h',
] as const;
export const PROVIDER_FLAGS = [
'--auth',
'--add',
'--paste-callback',
'--accounts',
'--use',
'--config',
'--thinking',
'--effort',
'--1m',
'--no-1m',
'--logout',
'--headless',
'--port-forward',
'--help',
'-h',
] as const;
export const COMMAND_FLAG_SUGGESTIONS: Readonly<Record<string, readonly string[]>> = {
'--shell-completion': ['--bash', '--zsh', '--fish', '--powershell', '--force', '-f'],
auth: ['--help', '-h'],
api: ['--help', '-h'],
cleanup: CLEANUP_FLAGS,
config: ['--help', '-h', '--port', '-p', '--host', '-H', '--dev'],
cursor: ['--help', '-h'],
doctor: ['--fix', '-f', '--help', '-h'],
docker: ['--help', '-h', '--host'],
env: ['--format', '--shell', '--ide', '--help', '-h'],
migrate: MIGRATE_FLAGS,
tokens: TOKENS_FLAGS,
update: ['--force', '--beta', '--dev', '--help', '-h'],
};
export const CURSOR_COMPLETION_SUBCOMMANDS = [...CURSOR_SUBCOMMANDS] as const;
export const COPILOT_COMPLETION_SUBCOMMANDS = [...COPILOT_SUBCOMMANDS, 'help'] as const;
export function getPublicRootCommands(): readonly RootCommandEntry[] {
return ROOT_COMMAND_CATALOG.filter((entry) => entry.visibility === 'public');
}
export function getAllRootCommandTokens(): string[] {
return uniqueStrings(
ROOT_COMMAND_CATALOG.flatMap((entry) => [entry.name, ...(entry.aliases || [])]).concat(
'copilot'
)
);
}
export function getPublicRootCommandTokens(): string[] {
return uniqueStrings(
ROOT_COMMAND_CATALOG.filter((entry) => entry.visibility === 'public').flatMap((entry) => [
entry.name,
...(entry.aliases || []),
])
);
}
export function uniqueStrings(values: readonly string[]): string[] {
return [...new Set(values.filter(Boolean))].sort((left, right) => left.localeCompare(right));
}
+260
View File
@@ -0,0 +1,260 @@
import ProfileDetector from '../auth/profile-detector';
import {
API_SUBCOMMANDS,
AUTH_SUBCOMMANDS,
BUILTIN_PROVIDER_SHORTCUTS,
CLEANUP_FLAGS,
COMMAND_FLAG_SUGGESTIONS,
CONFIG_SUBCOMMANDS,
COPILOT_COMPLETION_SUBCOMMANDS,
DOCKER_SUBCOMMANDS,
ROOT_COMMAND_FLAGS,
ROOT_HELP_TOPICS,
TOKENS_FLAGS,
PROVIDER_FLAGS,
uniqueStrings,
getPublicRootCommandTokens,
CLIPROXY_SUBCOMMANDS,
MIGRATE_FLAGS,
CURSOR_COMPLETION_SUBCOMMANDS,
} from './command-catalog';
export interface CompletionSuggestion {
value: string;
description?: string;
}
type CompletionShell = 'bash' | 'zsh' | 'fish' | 'powershell';
interface CompletionRequest {
current: string;
shell: CompletionShell;
tokensBeforeCurrent: string[];
}
function suggestion(value: string, description?: string): CompletionSuggestion {
return { value, description };
}
function filterSuggestions(
suggestions: readonly CompletionSuggestion[],
current: string
): CompletionSuggestion[] {
const needle = current.trim().toLowerCase();
const deduped = [...new Map(suggestions.map((entry) => [entry.value, entry])).values()];
if (!needle) return deduped.sort((left, right) => left.value.localeCompare(right.value));
return deduped
.filter((entry) => entry.value.toLowerCase().startsWith(needle))
.sort((left, right) => left.value.localeCompare(right.value));
}
function getDynamicProfileSuggestions(): CompletionSuggestion[] {
const detector = new ProfileDetector();
const profiles = detector.getAllProfiles();
return uniqueStrings([
...profiles.settings,
...profiles.accounts,
...profiles.cliproxyVariants,
]).map((value) => suggestion(value));
}
function getTopLevelSuggestions(): CompletionSuggestion[] {
return [
...getPublicRootCommandTokens().map((value) => suggestion(value)),
...ROOT_COMMAND_FLAGS.map((value) => suggestion(value)),
...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => suggestion(entry.name, entry.summary)),
...getDynamicProfileSuggestions(),
];
}
function getProfileDetector(): ProfileDetector {
return new ProfileDetector();
}
function getProfileNames(type: 'settings' | 'accounts' | 'cliproxyVariants'): string[] {
return getProfileDetector().getAllProfiles()[type];
}
function completeSubcommands(
values: readonly string[],
flags: readonly string[] = ['--help', '-h']
): CompletionSuggestion[] {
return uniqueStrings([...values, ...flags]).map((value) => suggestion(value));
}
function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSuggestion[] {
const [command, subcommand] = tokensBeforeCurrent;
const lastToken = tokensBeforeCurrent[tokensBeforeCurrent.length - 1];
switch (command) {
case undefined:
return getTopLevelSuggestions();
case 'help':
return completeSubcommands(ROOT_HELP_TOPICS.map((topic) => topic.name));
case 'auth':
if (!subcommand) return completeSubcommands(AUTH_SUBCOMMANDS);
if (subcommand === 'show')
return completeSubcommands(getProfileNames('accounts'), ['--json']);
if (subcommand === 'remove')
return completeSubcommands(getProfileNames('accounts'), ['--yes', '-y']);
if (subcommand === 'default') return completeSubcommands(getProfileNames('accounts'));
if (subcommand === 'create') {
return completeSubcommands(
[],
['--force', '--share-context', '--context-group', '--deeper-continuity', '--bare']
);
}
if (subcommand === 'list') return completeSubcommands([], ['--verbose', '--json']);
return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.auth);
case 'api':
if (!subcommand) return completeSubcommands(API_SUBCOMMANDS);
if (subcommand === 'create') {
return completeSubcommands(
[],
[
'--preset',
'--cliproxy-provider',
'--base-url',
'--api-key',
'--model',
'--1m',
'--no-1m',
'--target',
'--force',
'--yes',
'-y',
]
);
}
if (subcommand === 'discover')
return completeSubcommands([], ['--register', '--json', '--force']);
if (subcommand === 'copy')
return completeSubcommands(getProfileNames('settings'), [
'--target',
'--force',
'--yes',
'-y',
]);
if (subcommand === 'export')
return completeSubcommands(getProfileNames('settings'), ['--out', '--include-secrets']);
if (subcommand === 'import')
return completeSubcommands([], ['--name', '--yes', '-y', '--force']);
if (subcommand === 'remove')
return completeSubcommands(getProfileNames('settings'), ['--yes', '-y']);
return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.api);
case 'cliproxy':
if (!subcommand)
return completeSubcommands(CLIPROXY_SUBCOMMANDS, [
'--install',
'--latest',
'--update',
'--backend',
'--verbose',
'-v',
'--help',
'-h',
]);
if (['remove', 'edit'].includes(subcommand)) {
return completeSubcommands(getProfileNames('cliproxyVariants'), ['--yes', '-y']);
}
if (subcommand === 'create' && lastToken === '--provider') {
return BUILTIN_PROVIDER_SHORTCUTS.map((entry) => suggestion(entry.name, entry.summary));
}
return completeSubcommands(
[],
['--provider', '--model', '--target', '--backend', '--force', '--yes', '-y', '--help', '-h']
);
case 'config':
if (!subcommand || subcommand.startsWith('-')) {
return completeSubcommands(CONFIG_SUBCOMMANDS, COMMAND_FLAG_SUGGESTIONS.config);
}
return [];
case 'docker':
if (!subcommand || subcommand.startsWith('-')) {
return completeSubcommands(DOCKER_SUBCOMMANDS, COMMAND_FLAG_SUGGESTIONS.docker);
}
if (subcommand === 'up')
return completeSubcommands([], ['--port', '--proxy-port', '--host', '--help', '-h']);
if (subcommand === 'logs')
return completeSubcommands([], ['--follow', '--service', '--host', '--help', '-h']);
return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.docker);
case 'cursor':
return completeSubcommands(CURSOR_COMPLETION_SUBCOMMANDS);
case 'copilot':
return completeSubcommands(COPILOT_COMPLETION_SUBCOMMANDS);
case 'env':
if (lastToken === '--format')
return completeSubcommands(['openai', 'anthropic', 'raw', 'claude-extension']);
if (lastToken === '--shell')
return completeSubcommands(['auto', 'bash', 'zsh', 'fish', 'powershell']);
if (lastToken === '--ide') return completeSubcommands(['vscode', 'cursor', 'windsurf']);
return completeSubcommands(
uniqueStrings([
...getProfileNames('settings'),
...getProfileNames('accounts'),
...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => entry.name),
]),
COMMAND_FLAG_SUGGESTIONS.env
);
case 'tokens':
if (lastToken === '--variant')
return completeSubcommands(getProfileNames('cliproxyVariants'));
return completeSubcommands([], TOKENS_FLAGS);
case 'migrate':
return completeSubcommands([], MIGRATE_FLAGS);
case 'cleanup':
return completeSubcommands([], CLEANUP_FLAGS);
case '--shell-completion':
case '-sc':
return completeSubcommands(
[],
['--bash', '--zsh', '--fish', '--powershell', '--force', '-f']
);
default:
if (BUILTIN_PROVIDER_SHORTCUTS.some((entry) => entry.name === command)) {
if (command === 'agy')
return completeSubcommands([], [...PROVIDER_FLAGS, '--accept-agr-risk']);
if (command === 'kiro') {
return completeSubcommands(
[],
[...PROVIDER_FLAGS, '--kiro-auth-method', '--import', '--incognito']
);
}
return completeSubcommands([], PROVIDER_FLAGS);
}
return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS[command] || []);
}
}
export function getCompletionSuggestions(request: CompletionRequest): CompletionSuggestion[] {
return filterSuggestions(getSuggestionsForCommand(request.tokensBeforeCurrent), request.current);
}
function parseCompletionArgs(args: string[]): CompletionRequest {
const shellIndex = args.indexOf('--shell');
const currentIndex = args.indexOf('--current');
const separatorIndex = args.indexOf('--');
const shell = (shellIndex !== -1 ? args[shellIndex + 1] : 'bash') as CompletionShell;
const current = currentIndex !== -1 ? args[currentIndex + 1] || '' : '';
const tokensBeforeCurrent = separatorIndex === -1 ? [] : args.slice(separatorIndex + 1);
return { shell, current, tokensBeforeCurrent };
}
function formatForShell(
shell: CompletionShell,
suggestions: readonly CompletionSuggestion[]
): string[] {
if (shell === 'fish' || shell === 'powershell') {
return suggestions.map((entry) =>
entry.description ? `${entry.value}\t${entry.description}` : entry.value
);
}
return suggestions.map((entry) => entry.value);
}
export async function handleCompletionCommand(args: string[]): Promise<void> {
const request = parseCompletionArgs(args);
const suggestions = getCompletionSuggestions(request);
process.stdout.write(formatForShell(request.shell, suggestions).join('\n'));
}
+196 -662
View File
@@ -1,695 +1,229 @@
import * as fs from 'fs';
import * as path from 'path';
import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui';
import { isUnifiedMode } from '../config/unified-config-loader';
import { getCcsDirDisplay } from '../utils/config-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime';
import packageJson from '../../package.json';
import { color, dim, header, initUI, subheader } from '../utils/ui';
import {
BUILTIN_PROVIDER_SHORTCUTS,
ROOT_COMMAND_CATALOG,
ROOT_COMPATIBLE_ALIAS_EXAMPLES,
ROOT_HELP_TOPICS,
ROOT_PROFILE_EXAMPLES,
getPublicRootCommands,
type HelpTopicName,
type RootCommandEntry,
} from './command-catalog';
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')
).version;
function getTopicSummary(name: HelpTopicName): string {
return ROOT_HELP_TOPICS.find((topic) => topic.name === name)?.summary || '';
}
/**
* Print a major section with ═══ borders (only for 3 main sections)
* Format:
* ═══ TITLE ═══
* Subtitle line 1
* Subtitle line 2
*
* command Description
*/
function printMajorSection(
function writeCommandTable(
title: string,
subtitles: string[],
items: [string, string][],
writeLine: HelpWriter = console.log
entries: readonly { name: string; summary: string }[],
writeLine: HelpWriter
): void {
// Section header with ═══ borders
writeLine(sectionHeader(title));
// Subtitles on separate lines (dim)
for (const subtitle of subtitles) {
writeLine(` ${dim(subtitle)}`);
writeLine(subheader(title));
const maxWidth = Math.max(...entries.map((entry) => entry.name.length));
for (const entry of entries) {
writeLine(` ${color(entry.name.padEnd(maxWidth + 2), 'command')} ${entry.summary}`);
}
// Empty line before items
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);
writeLine(` ${color(paddedCmd, 'command')} ${desc}`);
}
// Extra spacing after section
writeLine('');
}
/**
* Print a sub-section with colored title
* Format:
* Title (context):
* command Description
*/
function printSubSection(
title: string,
items: [string, string][],
writeLine: HelpWriter = console.log
): void {
// Sub-section header (colored, no borders)
writeLine(subheader(`${title}:`));
function writeGroupedCommands(group: RootCommandEntry['group'], writeLine: HelpWriter): void {
const entries = getPublicRootCommands()
.filter((entry) => entry.group === group)
.map((entry) => ({ name: entry.name, summary: entry.summary }));
// Calculate max command length for alignment
const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length));
const titles: Record<RootCommandEntry['group'], string> = {
start: 'Start Here',
manage: 'Profile Management',
operations: 'Operations',
runtime: 'Compatible Runtimes',
};
for (const [cmd, desc] of items) {
const paddedCmd = cmd.padEnd(maxCmdLen + 2);
writeLine(` ${color(paddedCmd, 'command')} ${desc}`);
}
writeCommandTable(titles[group], entries, writeLine);
}
// Spacing after section
async function showProfilesHelp(writeLine: HelpWriter): Promise<void> {
await initUI();
writeLine(header('CCS Profiles Help'));
writeLine('');
writeCommandTable(
'Profile Types',
[
{ name: 'ccs auth create <name>', summary: 'Concurrent Claude account profile' },
{ name: 'ccs api create', summary: 'API-backed settings profile' },
{ name: 'ccs cliproxy create <name>', summary: 'Named CLIProxy variant profile' },
{ name: 'ccs env <profile>', summary: 'Export an existing profile for other tools' },
],
writeLine
);
writeCommandTable('Examples', ROOT_PROFILE_EXAMPLES, writeLine);
writeLine(` ${dim('Deep help: ccs auth --help | ccs api --help | ccs cliproxy --help')}`);
writeLine('');
}
/**
* Print a config/paths section
* Format:
* Title:
* Label: path
*/
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);
writeLine(` ${paddedLabel} ${color(path, 'path')}`);
}
async function showProvidersHelp(writeLine: HelpWriter): Promise<void> {
await initUI();
writeLine(header('CCS Providers Help'));
writeLine('');
writeCommandTable('Built-in OAuth Providers', BUILTIN_PROVIDER_SHORTCUTS, writeLine);
writeCommandTable(
'Common Setup Paths',
[
{
name: 'ccs <provider> --auth',
summary: 'Authenticate a provider account without launching',
},
{ name: 'ccs api create --preset <id>', summary: 'Create an API-backed provider profile' },
{ name: 'ccs config', summary: 'Use the dashboard for provider and model setup' },
],
writeLine
);
writeLine(` ${dim('Deep help: ccs cliproxy --help | ccs api --help')}`);
writeLine('');
}
/**
* Display comprehensive help information for CCS (Claude Code Switch)
*/
async function showTargetsHelp(writeLine: HelpWriter): Promise<void> {
await initUI();
writeLine(header('CCS Targets Help'));
writeLine('');
writeCommandTable('Target Routing', ROOT_COMPATIBLE_ALIAS_EXAMPLES, writeLine);
writeCommandTable(
'Examples',
[
{ name: 'ccs glm --target droid', summary: 'Run a profile on Droid instead of Claude' },
{
name: 'ccs --target codex',
summary: 'Open a native Codex session with your current setup',
},
{ name: 'ccs codex-api --target codex', summary: 'Run a routed API bridge on native Codex' },
],
writeLine
);
}
export async function handleHelpCommand(writeLine: HelpWriter = console.log): Promise<void> {
// Initialize UI (if not already)
await initUI();
// Hero box with ASCII art logo and config hint
// Each letter: C=╔═╗/║ /╚═╝, C=╔═╗/║ /╚═╝, S=╔═╗/╚═╗/╚═╝
const logo = `
╔═╗ ╔═╗ ╔═╗
║ ║ ╚═╗ v${VERSION}
╚═╝ ╚═╝ ╚═╝
Claude Code Profile & Model Switcher
Run ${color('ccs config', 'command')} for web dashboard`.trim();
writeLine(
box(logo, {
padding: 1,
borderStyle: 'round',
titleAlignment: 'center',
})
);
writeLine(header(`CCS CLI v${packageJson.version}`));
writeLine('');
writeLine(' Claude profile switching, provider routing, and compatible runtime bridges.');
writeLine('');
// Resolve display path for dynamic sections
const dirDisplay = getCcsDirDisplay();
// Usage section
writeLine(subheader('Usage:'));
writeLine(` ${color('ccs', 'command')} [profile] [claude-args...]`);
writeLine(` ${color('ccs', 'command')} [flags]`);
writeLine(subheader('Usage'));
writeLine(` ${color('ccs <profile> [claude-args...]', 'command')}`);
writeLine(` ${color('ccs <command> [options]', 'command')}`);
writeLine(` ${color('ccs help <topic>', 'command')}`);
writeLine('');
// ═══════════════════════════════════════════════════════════════════════════
// MAJOR SECTION 1: API Key Profiles
// ═══════════════════════════════════════════════════════════════════════════
printMajorSection(
'API Key Profiles',
[`Configure in ${dirDisplay}/*.settings.json`],
writeGroupedCommands('start', writeLine);
writeGroupedCommands('manage', writeLine);
writeGroupedCommands('runtime', writeLine);
writeGroupedCommands('operations', writeLine);
writeCommandTable(
'OAuth Provider Shortcuts',
BUILTIN_PROVIDER_SHORTCUTS.map((entry) => ({
name: `ccs ${entry.name}`,
summary: entry.summary,
})),
writeLine
);
writeCommandTable('Examples', ROOT_PROFILE_EXAMPLES, writeLine);
writeCommandTable('Targets and Aliases', ROOT_COMPATIBLE_ALIAS_EXAMPLES, writeLine);
writeCommandTable(
'More Help',
[
['ccs', 'Use default Claude account'],
['ccs glm', 'GLM 5 (API key required)'],
['ccs km', 'Kimi for Coding (API key)'],
[
'ccs api create --preset alibaba-coding-plan',
'Alibaba Coding Plan (Anthropic-compatible API key)',
],
['ccs ollama', 'Local Ollama (http://localhost:11434)'],
['ccs llamacpp', 'Local llama.cpp (http://127.0.0.1:8080)'],
['ccs ollama-cloud', 'Ollama Cloud (API key required)'],
['', ''], // Spacer
['ccs api create --preset anthropic', 'Anthropic direct API key (sk-ant-...)'],
[
'ccs api create --cliproxy-provider gemini',
'Create routed API profile from CLIProxy Gemini',
],
['ccs api create', 'Create custom API profile'],
['ccs api discover --register', 'Discover/register orphan settings files'],
['ccs api copy <src> <dest>', 'Duplicate API profile'],
['ccs api export <name>', 'Export profile bundle'],
['ccs api import <file>', 'Import profile bundle'],
['ccs api remove', 'Remove an API profile'],
['ccs api list', 'List all API profiles'],
{ name: 'ccs help profiles', summary: getTopicSummary('profiles') },
{ name: 'ccs help providers', summary: getTopicSummary('providers') },
{ name: 'ccs help completion', summary: getTopicSummary('completion') },
{ name: 'ccs help targets', summary: getTopicSummary('targets') },
{ name: 'ccs api --help', summary: 'Deep help for API profile lifecycle commands' },
{ name: 'ccs cliproxy --help', summary: 'Deep help for variants, quota, and lifecycle' },
{ name: 'ccs docker --help', summary: 'Deep help for Docker deployment commands' },
{ name: 'ccs cursor --help', summary: 'Deep help for Cursor runtime/admin commands' },
{ name: 'ccs copilot --help', summary: 'Deep help for GitHub Copilot commands' },
],
writeLine
);
// ═══════════════════════════════════════════════════════════════════════════
// MAJOR SECTION 2: Account Management
// ═══════════════════════════════════════════════════════════════════════════
printMajorSection(
'Account Management',
['Run multiple Claude accounts concurrently'],
[
['ccs auth --help', 'Show account management commands'],
[
'ccs auth create <name>',
'Create account profile (supports --bare, shared groups, --deeper-continuity)',
],
['ccs config', 'Dashboard: Accounts table can edit context mode/group/continuity depth'],
[
'~/.ccs/config.yaml',
'Optional: continuity.inherit_from_account maps API/CLIProxy/copilot/default profiles to an account context',
],
['ccs auth list', 'List all account profiles'],
['ccs auth default <name>', 'Set default profile'],
['ccs auth reset-default', 'Restore original CCS default'],
['ccs cliproxy auth claude', 'Alternative: authenticate Claude account pool via CLIProxy'],
],
writeLine
);
// ═══════════════════════════════════════════════════════════════════════════
// MAJOR SECTION 3: CLI Proxy (OAuth Providers)
// ═══════════════════════════════════════════════════════════════════════════
printMajorSection(
'CLI Proxy Plus (OAuth Providers)',
[
'Zero-config OAuth authentication via CLIProxy Plus',
'First run: Browser opens for authentication, then model selection',
'Settings: ~/.ccs/{provider}.settings.json (created after auth)',
'Safety: do not reuse one Google account across "ccs gemini" and "ccs agy" (issue #509)',
'Antigravity requires multi-step responsibility confirmation (issue #509)',
'If you want to keep Google AI access, do not continue this shared-account setup',
'CCS is as-is and does not take responsibility for account bans/access loss',
],
[
['ccs gemini', 'Google Gemini (gemini-2.5-pro or 3-pro)'],
['ccs codex', 'OpenAI Codex (supports -medium/-high/-xhigh model suffixes)'],
['ccs agy', 'Antigravity (Claude/Gemini models)'],
['ccs qwen', 'Qwen Code OAuth (CLIProxy)'],
['ccs kimi', 'Kimi (Moonshot AI K2/K2.5 models)'],
['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'],
['ccs ghcp', 'GitHub Copilot (OAuth via CLIProxy Plus)'],
['', ''], // Spacer
['ccs <provider> --auth', 'Authenticate only'],
['ccs <provider> --auth --add', 'Add another account'],
[
'ccs <provider> --paste-callback',
'Show auth URL and prompt for callback paste (cross-browser)',
],
['ccs <provider> --accounts', 'List all accounts'],
['ccs <provider> --use <nickname-or-id>', 'Switch to account'],
['ccs <provider> --config', 'Change model (agy, gemini)'],
[
'ccs agy --accept-agr-risk',
'Bypass interactive Antigravity confirmation (you accept full responsibility)',
],
[
'ccs <provider> --thinking <value>',
'Set thinking budget (low/medium/high/xhigh/auto/off or number)',
],
['ccs codex --effort <level>', 'Set codex reasoning effort (minimal/low/medium/high/xhigh)'],
['ccs <provider> --1m', 'Request explicit 1M context when the selected model supports [1m]'],
['ccs <provider> --no-1m', 'Force standard context / clear [1m]'],
['ccs <provider> --logout', 'Clear authentication'],
['ccs <provider> --headless', 'Headless auth (for SSH)'],
['ccs <provider> --port-forward', 'Force port-forwarding mode (skip prompt)'],
['ccs kiro --auth --kiro-auth-method aws', 'Kiro via AWS Builder ID (device code)'],
['ccs kiro --auth --kiro-auth-method aws-authcode', 'Kiro via AWS auth code flow'],
['ccs kiro --auth --kiro-auth-method google', 'Kiro via Google OAuth'],
['ccs kiro --auth --kiro-auth-method github', 'Kiro via GitHub OAuth (Dashboard flow)'],
['ccs kiro --import', 'Import token from Kiro IDE'],
['ccs kiro --incognito', 'Use incognito browser (default: normal)'],
['ccs codex "explain code"', 'Use with prompt'],
],
writeLine
);
// ═══════════════════════════════════════════════════════════════════════════
// MAJOR SECTION 4: GitHub Copilot Integration (copilot-api)
// ═══════════════════════════════════════════════════════════════════════════
printMajorSection(
'GitHub Copilot Integration (copilot-api)',
[
'Use your GitHub Copilot subscription with Claude Code via copilot-api',
'Requires: npm install -g copilot-api',
'Note: For OAuth-based access, use ccs ghcp instead',
],
[
['ccs copilot', 'Use Copilot via copilot-api daemon'],
['ccs copilot auth', 'Authenticate with GitHub'],
['ccs copilot status', 'Show integration status'],
['ccs copilot models', 'List available models'],
['ccs copilot usage', 'Show Copilot quota usage'],
['ccs copilot start', 'Start copilot-api daemon'],
['ccs copilot stop', 'Stop copilot-api daemon'],
['ccs copilot enable', 'Enable integration'],
['ccs copilot disable', 'Disable integration'],
],
writeLine
);
// ═══════════════════════════════════════════════════════════════════════════
// MAJOR SECTION 5: Cursor IDE Integration
// ═══════════════════════════════════════════════════════════════════════════
printMajorSection(
'Cursor IDE Integration',
[
'Use Cursor IDE with Claude Code via cursor proxy daemon',
'Auto-detects token from Cursor installation',
],
[
['ccs cursor', 'Run Claude via Cursor local proxy'],
['ccs cursor auth', 'Import Cursor token'],
['ccs cursor auth --manual --token <t> --machine-id <id>', 'Manual token import'],
['ccs cursor status', 'Show connection status'],
['ccs cursor models', 'List available models'],
['ccs cursor start', 'Start proxy daemon'],
['ccs cursor stop', 'Stop proxy daemon'],
['ccs cursor enable', 'Enable cursor integration'],
['ccs cursor disable', 'Disable cursor integration'],
],
writeLine
);
// ═══════════════════════════════════════════════════════════════════════════
// SUB-SECTIONS (simpler styling)
// ═══════════════════════════════════════════════════════════════════════════
// 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'],
],
writeLine
);
// Delegation CLI Flags (Claude Code passthrough)
printSubSection(
'Delegation Flags (Claude Code passthrough)',
[
['--max-turns <n>', 'Limit agentic turns (prevents loops)'],
['--fallback-model <model>', 'Auto-fallback on overload (sonnet)'],
['--agents <json>', 'Inject dynamic subagents'],
['--betas <features>', 'Enable experimental features'],
['--allowedTools <list>', 'Restrict available tools'],
['--disallowedTools <list>', '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'],
[
'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)'],
],
writeLine
);
// Environment export
printSubSection(
'Environment Export',
[
['ccs env <profile>', 'Export env vars for third-party tools'],
['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
);
// Flags
printSubSection(
'Flags',
[
['--config-dir <path>', 'Use custom CCS config directory'],
['--target <cli>', 'Target CLI: claude (default), droid, codex (runtime-only)'],
['-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 <profile> [args]', 'Explicit Droid runtime alias'],
['ccsd <profile> [args]', 'Legacy shortcut for: ccs-droid <profile> [args]'],
['ccs-codex <profile> [args]', 'Explicit Codex runtime alias'],
['ccsx <profile> [args]', 'Short alias for: ccs-codex <profile> [args]'],
['ccsxp [args]', 'Shortcut for: ccs codex --target codex [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 --target codex', 'Open a native Codex session with your existing ~/.codex setup'],
['ccs-codex', 'Same as above (explicit Codex alias)'],
['ccsx', 'Short alias for ccs-codex'],
['ccsxp "your prompt"', 'Run built-in CLIProxy Codex on native Codex CLI'],
['ccs codex --target codex', 'Explicit form of ccsxp'],
[
'ccs api create codex-api --cliproxy-provider codex',
'Create a routed API bridge that can also run on Codex',
],
['ccs codex-api --target codex', 'Run a Codex bridge profile on native Codex CLI'],
['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)'],
],
writeLine
);
printSubSection(
'Codex + CLIProxy',
[
['ccsxp "your prompt"', 'Use the built-in CCS Codex provider shortcut on native Codex'],
['ccs config', 'Open Compatible -> Codex CLI for native Codex setup and diagnostics'],
[
'Default provider',
'Set to cliproxy if plain codex or a personal cxp alias should use CLIProxy',
],
['Model provider auth', 'Save cliproxy with env_key = "CLIPROXY_API_KEY"'],
],
writeLine
);
// Configuration
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 <ver>', 'Install specific version (e.g., 6.6.6)'],
['ccs cliproxy --latest', 'Update to latest version'],
['', ''], // Spacer
['ccs cliproxy pause <p> <a>', 'Pause account from rotation'],
['ccs cliproxy resume <p> <a>', '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 <name>', 'Show quota/tier/pause status for one provider'],
],
writeLine
);
printSubSection(
'Docker Deployment',
[
['ccs docker --help', 'Manage the integrated CCS + CLIProxy Docker stack'],
['ccs docker up', 'Build and start the stack locally'],
['ccs docker up --host <ssh>', 'Stage assets to ~/.ccs/docker and deploy remotely'],
['ccs docker status', 'Show docker compose and supervisor status'],
['ccs docker logs --follow', 'Tail combined CCS + CLIProxy logs'],
['ccs docker update', 'Update CCS and CLIProxy inside the running container'],
['ccs docker config', 'Show bundled asset paths and deployment defaults'],
],
writeLine
);
// CLI Proxy configuration flags (new)
printSubSection(
'CLI Proxy Configuration',
[
['--proxy-host <host>', 'Remote proxy hostname/IP'],
['--proxy-port <port>', `Proxy port (default: ${CLIPROXY_DEFAULT_PORT})`],
['--proxy-protocol <proto>', 'Protocol: http or https (default: http)'],
['--proxy-auth-token <token>', 'Auth token for remote proxy'],
['--proxy-timeout <ms>', '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 <number>', 'Custom token budget (512-100000)'],
['', ''],
['--effort <level>', 'Codex alias for reasoning effort (minimal/low/medium/high/xhigh)'],
['--effort xhigh', 'Pin Codex effort to xhigh for this run'],
['', ''],
['Droid exec:', 'Use native Droid flag: --reasoning-effort <level>'],
['', '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.',
],
],
writeLine
);
// Image Analysis
printSubSection(
'Image Analysis (first-class local tool)',
[
['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 <p> <m>', 'Set provider model'],
['', ''],
['Note:', 'When ready, third-party launches expose the local ImageAnalysis MCP tool'],
['', 'and route requests directly to the resolved CCS provider path.'],
['', 'If runtime/auth/proxy is unavailable, CCS falls back to native Read.'],
],
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=<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:', getOfficialChannelsSupportMessage()],
['', '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
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)'],
],
writeLine
);
// CLI Proxy paths
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
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)
writeLine(subheader('Examples:'));
writeLine(` $ ${color('ccs', 'command')} ${dim('# Use default account')}`);
writeLine(
` $ ${color('ccs gemini', 'command')} ${dim('# OAuth (browser opens first time)')}`
);
writeLine(` $ ${color('ccs glm "implement API"', 'command')} ${dim('# API key model')}`);
writeLine(` $ ${color('ccs config', 'command')} ${dim('# Open web dashboard')}`);
writeLine('');
// Update examples
writeLine(subheader('Update:'));
writeLine(
` $ ${color('ccs update', 'command')} ${dim('# Update to latest stable')}`
);
writeLine(
` $ ${color('ccs update --force', 'command')} ${dim('# Force reinstall current')}`
);
writeLine(` $ ${color('ccs update --beta', 'command')} ${dim('# Install dev channel')}`);
writeLine('');
// Docs link
writeLine(` ${dim('Docs: https://github.com/kaitranntt/ccs')}`);
writeLine('');
// Uninstall
writeLine(subheader('Uninstall:'));
writeLine(` ${color('npm uninstall -g @kaitranntt/ccs', 'command')}`);
writeLine('');
// License
writeLine(dim('License: MIT'));
writeLine(` ${dim('Flags: -h/--help, -v/--version, -sc/--shell-completion, --target <cli>')}`);
writeLine('');
}
function listHelpTargets(): string {
return ROOT_HELP_TOPICS.map((topic) => topic.name).join(', ');
}
export async function handleHelpRoute(
args: string[],
writeLine: HelpWriter = console.log
): Promise<void> {
const topic = args[0] as HelpTopicName | undefined;
if (!topic) {
await handleHelpCommand(writeLine);
return;
}
if (topic === 'profiles') {
await showProfilesHelp(writeLine);
return;
}
if (topic === 'providers') {
await showProvidersHelp(writeLine);
return;
}
if (topic === 'targets') {
await showTargetsHelp(writeLine);
return;
}
if (topic === 'completion') {
const { showShellCompletionHelp } = await import('./shell-completion-command');
showShellCompletionHelp(writeLine);
return;
}
const commandHandlers: Partial<Record<string, () => Promise<void>>> = {
api: async () => (await import('./api-command/help')).showApiCommandHelp(writeLine),
auth: async () => {
const authModule = await import('../auth/auth-commands');
const AuthCommands = authModule.default;
await new AuthCommands().showHelp();
},
cleanup: async () => (await import('./cleanup-command')).handleCleanupCommand(['--help']),
cliproxy: async () => (await import('./cliproxy/help-subcommand')).showHelp(),
copilot: async () =>
process.exit(await (await import('./copilot-command')).handleCopilotCommand(['--help'])),
cursor: async () =>
process.exit(await (await import('./cursor-command')).handleCursorCommand(['--help'])),
docker: async () => (await import('./docker/help-subcommand')).showHelp(),
migrate: async () => (await import('./migrate-command')).printMigrateHelp(),
setup: async () => (await import('./setup-command')).handleSetupCommand(['--help']),
tokens: async () =>
process.exit(await (await import('./tokens-command')).handleTokensCommand(['--help'])),
};
const handler = commandHandlers[topic];
if (handler) {
await handler();
return;
}
await initUI();
writeLine(color(`Unknown help topic or command: ${topic}`, 'error'));
writeLine('');
writeLine(` ${dim(`Available help topics: ${listHelpTargets()}`)}`);
writeLine('');
process.exitCode = 1;
}
export function getRootHelpVisibleCommands(): string[] {
return getPublicRootCommands().map((entry) => entry.name);
}
export function getRootHelpCatalogEntries(): readonly RootCommandEntry[] {
return ROOT_COMMAND_CATALOG;
}
+11 -4
View File
@@ -16,7 +16,7 @@ async function printUpdateCommandHelp(): Promise<void> {
console.log('');
}
const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
export const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
{
name: 'migrate',
aliases: ['--migrate'],
@@ -55,9 +55,9 @@ const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
{
name: 'help',
aliases: ['--help', '-h'],
handle: async () => {
const { handleHelpCommand } = await import('./help-command');
await handleHelpCommand();
handle: async (args) => {
const { handleHelpRoute } = await import('./help-command');
await handleHelpRoute(args);
},
},
{
@@ -82,6 +82,13 @@ const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
await handleShellCompletionCommand(args);
},
},
{
name: '__complete',
handle: async (args) => {
const { handleCompletionCommand } = await import('./completion-backend');
await handleCompletionCommand(args);
},
},
{
name: 'doctor',
aliases: ['--doctor'],
+26 -11
View File
@@ -28,6 +28,24 @@ interface ShellCompletionInstallerLike {
install(shell: ShellTarget, options: { force: boolean }): ShellCompletionInstallResult;
}
export function showShellCompletionHelp(writeLine: (line: string) => void = console.log): void {
writeLine(header('Shell Completion'));
writeLine('');
writeLine(color('Usage:', 'info'));
writeLine(' ccs --shell-completion # Auto-detect shell and install');
writeLine(' ccs --shell-completion --bash # Install for bash');
writeLine(' ccs --shell-completion --zsh # Install for zsh');
writeLine(' ccs --shell-completion --fish # Install for fish');
writeLine(' ccs --shell-completion --powershell # Install for PowerShell');
writeLine(' ccs --shell-completion --force # Reinstall/refresh the active shell setup');
writeLine('');
writeLine(color('Test:', 'info'));
writeLine(' ccs <TAB>');
writeLine(' ccs help <TAB>');
writeLine(' ccs auth <TAB>');
writeLine('');
}
export function parseShellCompletionArgs(args: string[]): ShellCompletionParsedArgs {
let targetShell: ShellTarget = null;
const force = args.includes('--force') || args.includes('-f');
@@ -77,12 +95,15 @@ export function createShellCompletionCommandContract(
*/
export async function handleShellCompletionCommand(args: string[]): Promise<void> {
await initUI();
const { ShellCompletionInstaller } = await import('../utils/shell-completion');
console.log(header('Shell Completion Installer'));
console.log('');
if (args.includes('--help') || args.includes('-h')) {
showShellCompletionHelp();
return;
}
try {
const { ShellCompletionInstaller } = await import('../utils/shell-completion');
console.log(header('Shell Completion Installer'));
console.log('');
const installer = new ShellCompletionInstaller();
const contract = createShellCompletionCommandContract(installer);
await runCommandWithContract(args, contract);
@@ -90,13 +111,7 @@ export async function handleShellCompletionCommand(args: string[]): Promise<void
const err = error as Error;
console.error(fail(`Error: ${err.message}`));
console.error('');
console.error(color('Usage:', 'warning'));
console.error(' ccs --shell-completion # Auto-detect shell');
console.error(' ccs --shell-completion --bash # Install for bash');
console.error(' ccs --shell-completion --zsh # Install for zsh');
console.error(' ccs --shell-completion --fish # Install for fish');
console.error(' ccs --shell-completion --powershell # Install for PowerShell');
console.error('');
showShellCompletionHelp(console.error);
process.exit(1);
}
}