mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-05 22:24:04 +00:00
refactor(core): centralize claude paths and command parsing
This commit is contained in:
+71
-113
@@ -433,56 +433,6 @@ async function main(): Promise<void> {
|
|||||||
console.warn('[!] Recovery failed:', (err as Error).message);
|
console.warn('[!] Recovery failed:', (err as Error).message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: version command (check BEFORE profile detection)
|
|
||||||
if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
|
|
||||||
await handleVersionCommand();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: help command
|
|
||||||
if (firstArg === '--help' || firstArg === '-h' || firstArg === 'help') {
|
|
||||||
await handleHelpCommand();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: install command
|
|
||||||
if (firstArg === '--install') {
|
|
||||||
handleInstallCommand();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: uninstall command
|
|
||||||
if (firstArg === '--uninstall') {
|
|
||||||
handleUninstallCommand();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: shell completion installer
|
|
||||||
if (firstArg === '--shell-completion' || firstArg === '-sc') {
|
|
||||||
await handleShellCompletionCommand(args.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: doctor command
|
|
||||||
if (firstArg === 'doctor' || firstArg === '--doctor') {
|
|
||||||
const restArgs = args.slice(args.indexOf(firstArg) + 1);
|
|
||||||
await handleDoctorCommand(restArgs);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: sync command
|
|
||||||
if (firstArg === 'sync' || firstArg === '--sync') {
|
|
||||||
await handleSyncCommand();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: cleanup command
|
|
||||||
if (firstArg === 'cleanup' || firstArg === '--cleanup') {
|
|
||||||
const { handleCleanupCommand } = await import('./commands/cleanup-command');
|
|
||||||
await handleCleanupCommand(args.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: migrate command
|
// Special case: migrate command
|
||||||
if (firstArg === 'migrate' || firstArg === '--migrate') {
|
if (firstArg === 'migrate' || firstArg === '--migrate') {
|
||||||
const { handleMigrateCommand, printMigrateHelp } = await import('./commands/migrate-command');
|
const { handleMigrateCommand, printMigrateHelp } = await import('./commands/migrate-command');
|
||||||
@@ -525,72 +475,80 @@ async function main(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: auth command
|
const commandAliases: Record<string, string> = {
|
||||||
if (firstArg === 'auth') {
|
'--version': 'version',
|
||||||
const AuthCommandsModule = await import('./auth/auth-commands');
|
'-v': 'version',
|
||||||
const AuthCommands = AuthCommandsModule.default;
|
'--help': 'help',
|
||||||
const authCommands = new AuthCommands();
|
'-h': 'help',
|
||||||
await authCommands.route(args.slice(1));
|
'--doctor': 'doctor',
|
||||||
|
'--sync': 'sync',
|
||||||
|
'--cleanup': 'cleanup',
|
||||||
|
'--setup': 'setup',
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizedFirstArg = commandAliases[firstArg] || firstArg;
|
||||||
|
|
||||||
|
const earlyCommandHandlers: Record<string, () => Promise<void>> = {
|
||||||
|
version: async () => handleVersionCommand(),
|
||||||
|
help: async () => handleHelpCommand(),
|
||||||
|
'--install': async () => handleInstallCommand(),
|
||||||
|
'--uninstall': async () => handleUninstallCommand(),
|
||||||
|
'--shell-completion': async () => handleShellCompletionCommand(args.slice(1)),
|
||||||
|
'-sc': async () => handleShellCompletionCommand(args.slice(1)),
|
||||||
|
doctor: async () => handleDoctorCommand(args.slice(1)),
|
||||||
|
sync: async () => handleSyncCommand(),
|
||||||
|
cleanup: async () => {
|
||||||
|
const { handleCleanupCommand } = await import('./commands/cleanup-command');
|
||||||
|
await handleCleanupCommand(args.slice(1));
|
||||||
|
},
|
||||||
|
auth: async () => {
|
||||||
|
const AuthCommandsModule = await import('./auth/auth-commands');
|
||||||
|
const AuthCommands = AuthCommandsModule.default;
|
||||||
|
const authCommands = new AuthCommands();
|
||||||
|
await authCommands.route(args.slice(1));
|
||||||
|
},
|
||||||
|
api: async () => {
|
||||||
|
const { handleApiCommand } = await import('./commands/api-command');
|
||||||
|
await handleApiCommand(args.slice(1));
|
||||||
|
},
|
||||||
|
cliproxy: async () => {
|
||||||
|
const { handleCliproxyCommand } = await import('./commands/cliproxy-command');
|
||||||
|
await handleCliproxyCommand(args.slice(1));
|
||||||
|
},
|
||||||
|
config: async () => {
|
||||||
|
const { handleConfigCommand } = await import('./commands/config-command');
|
||||||
|
await handleConfigCommand(args.slice(1));
|
||||||
|
},
|
||||||
|
tokens: async () => {
|
||||||
|
const { handleTokensCommand } = await import('./commands/tokens-command');
|
||||||
|
const exitCode = await handleTokensCommand(args.slice(1));
|
||||||
|
process.exit(exitCode);
|
||||||
|
},
|
||||||
|
persist: async () => {
|
||||||
|
const { handlePersistCommand } = await import('./commands/persist-command');
|
||||||
|
await handlePersistCommand(args.slice(1));
|
||||||
|
},
|
||||||
|
env: async () => {
|
||||||
|
const { handleEnvCommand } = await import('./commands/env-command');
|
||||||
|
await handleEnvCommand(args.slice(1));
|
||||||
|
},
|
||||||
|
setup: async () => {
|
||||||
|
const { handleSetupCommand } = await import('./commands/setup-command');
|
||||||
|
await handleSetupCommand(args.slice(1));
|
||||||
|
},
|
||||||
|
cursor: async () => {
|
||||||
|
const { handleCursorCommand } = await import('./commands/cursor-command');
|
||||||
|
const exitCode = await handleCursorCommand(args.slice(1));
|
||||||
|
process.exit(exitCode);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const earlyCommandHandler = earlyCommandHandlers[normalizedFirstArg];
|
||||||
|
if (earlyCommandHandler) {
|
||||||
|
await earlyCommandHandler();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: api command (manages API profiles)
|
|
||||||
if (firstArg === 'api') {
|
|
||||||
const { handleApiCommand } = await import('./commands/api-command');
|
|
||||||
await handleApiCommand(args.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: cliproxy command (manages CLIProxyAPI binary)
|
|
||||||
if (firstArg === 'cliproxy') {
|
|
||||||
const { handleCliproxyCommand } = await import('./commands/cliproxy-command');
|
|
||||||
await handleCliproxyCommand(args.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: config command (web dashboard)
|
|
||||||
if (firstArg === 'config') {
|
|
||||||
const { handleConfigCommand } = await import('./commands/config-command');
|
|
||||||
await handleConfigCommand(args.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: tokens command (auth token management)
|
|
||||||
if (firstArg === 'tokens') {
|
|
||||||
const { handleTokensCommand } = await import('./commands/tokens-command');
|
|
||||||
const exitCode = await handleTokensCommand(args.slice(1));
|
|
||||||
process.exit(exitCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: persist command (write profile env to ~/.claude/settings.json)
|
|
||||||
if (firstArg === 'persist') {
|
|
||||||
const { handlePersistCommand } = await import('./commands/persist-command');
|
|
||||||
await handlePersistCommand(args.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: env command (export env vars for third-party tools)
|
|
||||||
if (firstArg === 'env') {
|
|
||||||
const { handleEnvCommand } = await import('./commands/env-command');
|
|
||||||
await handleEnvCommand(args.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: setup command (first-time wizard)
|
|
||||||
if (firstArg === 'setup' || firstArg === '--setup') {
|
|
||||||
const { handleSetupCommand } = await import('./commands/setup-command');
|
|
||||||
await handleSetupCommand(args.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: cursor command (Cursor IDE integration)
|
|
||||||
// All `ccs cursor *` routes to cursor command handler — cursor has no profile-switching mode
|
|
||||||
if (firstArg === 'cursor') {
|
|
||||||
const { handleCursorCommand } = await import('./commands/cursor-command');
|
|
||||||
const exitCode = await handleCursorCommand(args.slice(1));
|
|
||||||
process.exit(exitCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: copilot command (GitHub Copilot integration)
|
// Special case: copilot command (GitHub Copilot integration)
|
||||||
// Only route to command handler for known subcommands, otherwise treat as profile
|
// Only route to command handler for known subcommands, otherwise treat as profile
|
||||||
const COPILOT_SUBCOMMANDS = [
|
const COPILOT_SUBCOMMANDS = [
|
||||||
|
|||||||
@@ -10,26 +10,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { CLIProxyProvider } from '../types';
|
import { CLIProxyProvider } from '../types';
|
||||||
import { supportsExtendedContext, isNativeGeminiModel } from '../model-catalog';
|
import { supportsExtendedContext } from '../model-catalog';
|
||||||
import { warn } from '../../utils/ui';
|
import { warn } from '../../utils/ui';
|
||||||
|
import {
|
||||||
|
applyExtendedContextSuffix as applyExtendedContextSuffixShared,
|
||||||
|
isNativeGeminiModel,
|
||||||
|
stripExtendedContextSuffix,
|
||||||
|
} from '../../shared/extended-context-utils';
|
||||||
|
|
||||||
/** Extended context suffix recognized by Claude Code */
|
// Backward-compatible export retained for tests/importers that reference this module.
|
||||||
const EXTENDED_CONTEXT_SUFFIX = '[1m]';
|
export function applyExtendedContextSuffix(modelId: string): string {
|
||||||
|
return applyExtendedContextSuffixShared(modelId);
|
||||||
/**
|
|
||||||
* Apply extended context suffix to model name.
|
|
||||||
* Appends [1m] suffix if not already present.
|
|
||||||
*
|
|
||||||
* @param model - Model name (may include thinking suffix like "model(high)")
|
|
||||||
* @returns Model name with [1m] suffix, e.g., "gemini-2.5-pro[1m]" or "gemini-2.5-pro(high)[1m]"
|
|
||||||
*/
|
|
||||||
export function applyExtendedContextSuffix(model: string): string {
|
|
||||||
if (!model) return model;
|
|
||||||
// Case-insensitive check to avoid double suffix (handles [1M], [1m], etc.)
|
|
||||||
if (model.toLowerCase().endsWith(EXTENDED_CONTEXT_SUFFIX.toLowerCase())) {
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
return `${model}${EXTENDED_CONTEXT_SUFFIX}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,7 +100,7 @@ export function applyExtendedContextConfig(
|
|||||||
|
|
||||||
// Apply suffix to main model
|
// Apply suffix to main model
|
||||||
if (envVars.ANTHROPIC_MODEL) {
|
if (envVars.ANTHROPIC_MODEL) {
|
||||||
envVars.ANTHROPIC_MODEL = applyExtendedContextSuffix(envVars.ANTHROPIC_MODEL);
|
envVars.ANTHROPIC_MODEL = applyExtendedContextSuffixShared(envVars.ANTHROPIC_MODEL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply to tier models if they support extended context
|
// Apply to tier models if they support extended context
|
||||||
@@ -119,7 +110,7 @@ export function applyExtendedContextConfig(
|
|||||||
if (model) {
|
if (model) {
|
||||||
const tierCleanId = stripModelSuffixes(model);
|
const tierCleanId = stripModelSuffixes(model);
|
||||||
if (shouldApplyExtendedContext(provider, tierCleanId, extendedContextOverride)) {
|
if (shouldApplyExtendedContext(provider, tierCleanId, extendedContextOverride)) {
|
||||||
envVars[tierVar] = applyExtendedContextSuffix(model);
|
envVars[tierVar] = applyExtendedContextSuffixShared(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,8 +124,5 @@ export function applyExtendedContextConfig(
|
|||||||
* "gemini-2.5-pro" -> "gemini-2.5-pro"
|
* "gemini-2.5-pro" -> "gemini-2.5-pro"
|
||||||
*/
|
*/
|
||||||
function stripModelSuffixes(modelId: string): string {
|
function stripModelSuffixes(modelId: string): string {
|
||||||
return modelId
|
return stripExtendedContextSuffix(modelId.trim()).replace(/\([^)]+\)$/, '');
|
||||||
.trim()
|
|
||||||
.replace(/\[1m\]$/i, '') // Remove [1m] suffix
|
|
||||||
.replace(/\([^)]+\)$/, ''); // Remove thinking suffix like (high) or (8192)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-19
@@ -43,6 +43,7 @@ import {
|
|||||||
type ProviderPreset,
|
type ProviderPreset,
|
||||||
} from '../api/services';
|
} from '../api/services';
|
||||||
import { syncToLocalConfig } from '../cliproxy/sync/local-config-sync';
|
import { syncToLocalConfig } from '../cliproxy/sync/local-config-sync';
|
||||||
|
import { extractOption, hasAnyFlag } from './arg-extractor';
|
||||||
|
|
||||||
interface ApiCommandArgs {
|
interface ApiCommandArgs {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -72,28 +73,30 @@ function renderPresetHelpLine(preset: ProviderPreset, idWidth: number): string {
|
|||||||
|
|
||||||
/** Parse command line arguments for api commands */
|
/** Parse command line arguments for api commands */
|
||||||
function parseArgs(args: string[]): ApiCommandArgs {
|
function parseArgs(args: string[]): ApiCommandArgs {
|
||||||
const result: ApiCommandArgs = {};
|
const result: ApiCommandArgs = {
|
||||||
|
force: hasAnyFlag(args, ['--force']),
|
||||||
|
yes: hasAnyFlag(args, ['--yes', '-y']),
|
||||||
|
};
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
let remaining = [...args];
|
||||||
const arg = args[i];
|
|
||||||
|
|
||||||
if (arg === '--base-url' && args[i + 1]) {
|
const baseUrl = extractOption(remaining, ['--base-url']);
|
||||||
result.baseUrl = args[++i];
|
if (baseUrl.value) result.baseUrl = baseUrl.value;
|
||||||
} else if (arg === '--api-key' && args[i + 1]) {
|
remaining = baseUrl.remainingArgs;
|
||||||
result.apiKey = args[++i];
|
|
||||||
} else if (arg === '--model' && args[i + 1]) {
|
|
||||||
result.model = args[++i];
|
|
||||||
} else if (arg === '--preset' && args[i + 1]) {
|
|
||||||
result.preset = args[++i];
|
|
||||||
} else if (arg === '--force') {
|
|
||||||
result.force = true;
|
|
||||||
} else if (arg === '--yes' || arg === '-y') {
|
|
||||||
result.yes = true;
|
|
||||||
} else if (!arg.startsWith('-') && !result.name) {
|
|
||||||
result.name = arg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const apiKey = extractOption(remaining, ['--api-key']);
|
||||||
|
if (apiKey.value) result.apiKey = apiKey.value;
|
||||||
|
remaining = apiKey.remainingArgs;
|
||||||
|
|
||||||
|
const model = extractOption(remaining, ['--model']);
|
||||||
|
if (model.value) result.model = model.value;
|
||||||
|
remaining = model.remainingArgs;
|
||||||
|
|
||||||
|
const preset = extractOption(remaining, ['--preset']);
|
||||||
|
if (preset.value) result.preset = preset.value;
|
||||||
|
remaining = preset.remainingArgs;
|
||||||
|
|
||||||
|
result.name = remaining.find((arg) => !arg.startsWith('-'));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Small helpers for consistent CLI option extraction.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ExtractedOption {
|
||||||
|
found: boolean;
|
||||||
|
value?: string;
|
||||||
|
missingValue: boolean;
|
||||||
|
remainingArgs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function findInlineOption(arg: string, flag: string): string | undefined {
|
||||||
|
const prefix = `${flag}=`;
|
||||||
|
return arg.startsWith(prefix) ? arg.slice(prefix.length) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a single-value option and remove it from args.
|
||||||
|
* Supports `--flag value` and `--flag=value` forms.
|
||||||
|
*/
|
||||||
|
export function extractOption(args: string[], flags: readonly string[]): ExtractedOption {
|
||||||
|
const remaining = [...args];
|
||||||
|
|
||||||
|
for (let i = 0; i < remaining.length; i++) {
|
||||||
|
const token = remaining[i];
|
||||||
|
|
||||||
|
for (const flag of flags) {
|
||||||
|
if (token === flag) {
|
||||||
|
const next = remaining[i + 1];
|
||||||
|
if (!next || next.startsWith('-')) {
|
||||||
|
remaining.splice(i, 1);
|
||||||
|
return { found: true, missingValue: true, remainingArgs: remaining };
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining.splice(i, 2);
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
value: next,
|
||||||
|
missingValue: false,
|
||||||
|
remainingArgs: remaining,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const inlineValue = findInlineOption(token, flag);
|
||||||
|
if (inlineValue !== undefined) {
|
||||||
|
remaining.splice(i, 1);
|
||||||
|
if (!inlineValue.trim()) {
|
||||||
|
return { found: true, missingValue: true, remainingArgs: remaining };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
value: inlineValue,
|
||||||
|
missingValue: false,
|
||||||
|
remainingArgs: remaining,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { found: false, missingValue: false, remainingArgs: remaining };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if any of the provided boolean flags are present. */
|
||||||
|
export function hasAnyFlag(args: string[], flags: readonly string[]): boolean {
|
||||||
|
return args.some((arg) => flags.includes(arg));
|
||||||
|
}
|
||||||
+49
-129
@@ -9,6 +9,7 @@ import { CLIProxyBackend } from '../../cliproxy/types';
|
|||||||
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
|
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
|
||||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
import { handleSync } from '../cliproxy-sync-handler';
|
import { handleSync } from '../cliproxy-sync-handler';
|
||||||
|
import { extractOption, hasAnyFlag } from '../arg-extractor';
|
||||||
|
|
||||||
// Import subcommand handlers
|
// Import subcommand handlers
|
||||||
import { handleList } from './auth-subcommand';
|
import { handleList } from './auth-subcommand';
|
||||||
@@ -42,30 +43,23 @@ function parseBackendArg(args: string[]): {
|
|||||||
backend: CLIProxyBackend | undefined;
|
backend: CLIProxyBackend | undefined;
|
||||||
remainingArgs: string[];
|
remainingArgs: string[];
|
||||||
} {
|
} {
|
||||||
const backendIdx = args.indexOf('--backend');
|
const extracted = extractOption(args, ['--backend']);
|
||||||
if (backendIdx === -1) {
|
if (!extracted.found) {
|
||||||
// Also check for --backend=value format
|
|
||||||
const backendEqualsIdx = args.findIndex((a) => a.startsWith('--backend='));
|
|
||||||
if (backendEqualsIdx !== -1) {
|
|
||||||
const value = args[backendEqualsIdx].split('=')[1] as CLIProxyBackend;
|
|
||||||
if (value !== 'original' && value !== 'plus') {
|
|
||||||
console.warn(`Invalid backend '${value}'. Valid options: original, plus`);
|
|
||||||
return { backend: undefined, remainingArgs: args };
|
|
||||||
}
|
|
||||||
const remainingArgs = [...args];
|
|
||||||
remainingArgs.splice(backendEqualsIdx, 1);
|
|
||||||
return { backend: value, remainingArgs };
|
|
||||||
}
|
|
||||||
return { backend: undefined, remainingArgs: args };
|
return { backend: undefined, remainingArgs: args };
|
||||||
}
|
}
|
||||||
const value = args[backendIdx + 1];
|
|
||||||
|
if (extracted.missingValue || !extracted.value) {
|
||||||
|
console.warn(`Invalid backend ''. Valid options: original, plus`);
|
||||||
|
return { backend: undefined, remainingArgs: extracted.remainingArgs };
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = extracted.value as CLIProxyBackend;
|
||||||
if (value !== 'original' && value !== 'plus') {
|
if (value !== 'original' && value !== 'plus') {
|
||||||
console.warn(`Invalid backend '${value}'. Valid options: original, plus`);
|
console.warn(`Invalid backend '${value}'. Valid options: original, plus`);
|
||||||
return { backend: undefined, remainingArgs: args };
|
return { backend: undefined, remainingArgs: extracted.remainingArgs };
|
||||||
}
|
}
|
||||||
const remainingArgs = [...args];
|
|
||||||
remainingArgs.splice(backendIdx, 2);
|
return { backend: value, remainingArgs: extracted.remainingArgs };
|
||||||
return { backend: value, remainingArgs };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,54 +80,19 @@ function parseProviderArg(args: string[]): {
|
|||||||
provider: 'agy' | 'codex' | 'gemini' | 'ghcp' | 'all';
|
provider: 'agy' | 'codex' | 'gemini' | 'ghcp' | 'all';
|
||||||
remainingArgs: string[];
|
remainingArgs: string[];
|
||||||
} {
|
} {
|
||||||
const providerIdx = args.indexOf('--provider');
|
const extracted = extractOption(args, ['--provider']);
|
||||||
if (providerIdx === -1) {
|
if (!extracted.found) {
|
||||||
// Also check for --provider=value format
|
|
||||||
const providerEqualsIdx = args.findIndex((a) => a.startsWith('--provider='));
|
|
||||||
if (providerEqualsIdx !== -1) {
|
|
||||||
const value = args[providerEqualsIdx].split('=')[1]?.toLowerCase() || '';
|
|
||||||
const remainingArgs = [...args];
|
|
||||||
remainingArgs.splice(providerEqualsIdx, 1);
|
|
||||||
// Handle empty value
|
|
||||||
if (!value) {
|
|
||||||
console.error(
|
|
||||||
'Warning: --provider requires a value. Valid options: agy, codex, gemini, gemini-cli, ghcp, github-copilot, all'
|
|
||||||
);
|
|
||||||
return { provider: 'all', remainingArgs };
|
|
||||||
}
|
|
||||||
// Normalize gemini-cli to gemini
|
|
||||||
const normalized =
|
|
||||||
value === 'gemini-cli' ? 'gemini' : value === 'github-copilot' ? 'ghcp' : value;
|
|
||||||
if (
|
|
||||||
normalized !== 'agy' &&
|
|
||||||
normalized !== 'codex' &&
|
|
||||||
normalized !== 'gemini' &&
|
|
||||||
normalized !== 'ghcp' &&
|
|
||||||
normalized !== 'all'
|
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
`Invalid provider '${value}'. Valid options: agy, codex, gemini, gemini-cli, ghcp, github-copilot, all`
|
|
||||||
);
|
|
||||||
return { provider: 'all', remainingArgs };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
provider: normalized as 'agy' | 'codex' | 'gemini' | 'ghcp' | 'all',
|
|
||||||
remainingArgs,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { provider: 'all', remainingArgs: args };
|
return { provider: 'all', remainingArgs: args };
|
||||||
}
|
}
|
||||||
const rawValue = args[providerIdx + 1];
|
|
||||||
// Warn if no value or value looks like another flag
|
if (extracted.missingValue || !extracted.value) {
|
||||||
if (!rawValue || rawValue.startsWith('-')) {
|
|
||||||
console.error(
|
console.error(
|
||||||
'Warning: --provider requires a value. Valid options: agy, codex, gemini, gemini-cli, ghcp, github-copilot, all'
|
'Warning: --provider requires a value. Valid options: agy, codex, gemini, gemini-cli, ghcp, github-copilot, all'
|
||||||
);
|
);
|
||||||
|
return { provider: 'all', remainingArgs: extracted.remainingArgs };
|
||||||
}
|
}
|
||||||
const value = rawValue?.toLowerCase() || 'all';
|
|
||||||
const remainingArgs = [...args];
|
const value = extracted.value.toLowerCase();
|
||||||
remainingArgs.splice(providerIdx, 2);
|
|
||||||
// Normalize gemini-cli to gemini
|
|
||||||
const normalized =
|
const normalized =
|
||||||
value === 'gemini-cli' ? 'gemini' : value === 'github-copilot' ? 'ghcp' : value;
|
value === 'gemini-cli' ? 'gemini' : value === 'github-copilot' ? 'ghcp' : value;
|
||||||
if (
|
if (
|
||||||
@@ -146,11 +105,11 @@ function parseProviderArg(args: string[]): {
|
|||||||
console.error(
|
console.error(
|
||||||
`Invalid provider '${value}'. Valid options: agy, codex, gemini, gemini-cli, ghcp, github-copilot, all`
|
`Invalid provider '${value}'. Valid options: agy, codex, gemini, gemini-cli, ghcp, github-copilot, all`
|
||||||
);
|
);
|
||||||
return { provider: 'all', remainingArgs };
|
return { provider: 'all', remainingArgs: extracted.remainingArgs };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
provider: normalized as 'agy' | 'codex' | 'gemini' | 'ghcp' | 'all',
|
provider: normalized as 'agy' | 'codex' | 'gemini' | 'ghcp' | 'all',
|
||||||
remainingArgs,
|
remainingArgs: extracted.remainingArgs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,35 +121,14 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
|||||||
const { backend: cliBackend, remainingArgs } = parseBackendArg(args);
|
const { backend: cliBackend, remainingArgs } = parseBackendArg(args);
|
||||||
const effectiveBackend = getEffectiveBackend(cliBackend);
|
const effectiveBackend = getEffectiveBackend(cliBackend);
|
||||||
|
|
||||||
const verbose = remainingArgs.includes('--verbose') || remainingArgs.includes('-v');
|
const verbose = hasAnyFlag(remainingArgs, ['--verbose', '-v']);
|
||||||
const command = remainingArgs[0];
|
const command = remainingArgs[0];
|
||||||
|
|
||||||
if (remainingArgs.includes('--help') || remainingArgs.includes('-h')) {
|
if (hasAnyFlag(remainingArgs, ['--help', '-h'])) {
|
||||||
await showHelp();
|
await showHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profile commands
|
|
||||||
if (command === 'create') {
|
|
||||||
await handleCreate(remainingArgs.slice(1), effectiveBackend);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'edit') {
|
|
||||||
await handleEdit(remainingArgs.slice(1), effectiveBackend);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'list' || command === 'ls') {
|
|
||||||
await handleList();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'remove' || command === 'delete' || command === 'rm') {
|
|
||||||
await handleRemove(remainingArgs.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catalog commands
|
// Catalog commands
|
||||||
if (command === 'catalog') {
|
if (command === 'catalog') {
|
||||||
const subcommand = remainingArgs[1];
|
const subcommand = remainingArgs[1];
|
||||||
@@ -212,55 +150,37 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy lifecycle commands
|
|
||||||
if (command === 'start') {
|
|
||||||
await handleStart(verbose);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'stop') {
|
|
||||||
await handleStop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'restart') {
|
|
||||||
await handleRestart(verbose);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'status') {
|
|
||||||
await handleProxyStatus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diagnostics
|
|
||||||
if (command === 'doctor' || command === 'diag') {
|
|
||||||
await handleDoctor(verbose);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quota management commands
|
|
||||||
if (command === 'default') {
|
|
||||||
await handleSetDefault(remainingArgs.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'pause') {
|
|
||||||
await handlePauseAccount(remainingArgs.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'resume') {
|
|
||||||
await handleResumeAccount(remainingArgs.slice(1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'quota') {
|
if (command === 'quota') {
|
||||||
const { provider: providerFilter } = parseProviderArg(remainingArgs.slice(1));
|
const { provider: providerFilter } = parseProviderArg(remainingArgs.slice(1));
|
||||||
await handleQuotaStatus(verbose, providerFilter);
|
await handleQuotaStatus(verbose, providerFilter);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commandHandlers: Record<string, () => Promise<void>> = {
|
||||||
|
create: async () => handleCreate(remainingArgs.slice(1), effectiveBackend),
|
||||||
|
edit: async () => handleEdit(remainingArgs.slice(1), effectiveBackend),
|
||||||
|
list: async () => handleList(),
|
||||||
|
ls: async () => handleList(),
|
||||||
|
remove: async () => handleRemove(remainingArgs.slice(1)),
|
||||||
|
delete: async () => handleRemove(remainingArgs.slice(1)),
|
||||||
|
rm: async () => handleRemove(remainingArgs.slice(1)),
|
||||||
|
start: async () => handleStart(verbose),
|
||||||
|
stop: async () => handleStop(),
|
||||||
|
restart: async () => handleRestart(verbose),
|
||||||
|
status: async () => handleProxyStatus(),
|
||||||
|
doctor: async () => handleDoctor(verbose),
|
||||||
|
diag: async () => handleDoctor(verbose),
|
||||||
|
default: async () => handleSetDefault(remainingArgs.slice(1)),
|
||||||
|
pause: async () => handlePauseAccount(remainingArgs.slice(1)),
|
||||||
|
resume: async () => handleResumeAccount(remainingArgs.slice(1)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandHandler = command ? commandHandlers[command] : undefined;
|
||||||
|
if (commandHandler) {
|
||||||
|
await commandHandler();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Binary installation commands
|
// Binary installation commands
|
||||||
const installIdx = remainingArgs.indexOf('--install');
|
const installIdx = remainingArgs.indexOf('--install');
|
||||||
if (installIdx !== -1) {
|
if (installIdx !== -1) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { setupGracefulShutdown } from '../web-server/shutdown';
|
|||||||
import { ensureCliproxyService } from '../cliproxy/service-manager';
|
import { ensureCliproxyService } from '../cliproxy/service-manager';
|
||||||
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config-generator';
|
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config-generator';
|
||||||
import { initUI, header, ok, info, warn, fail } from '../utils/ui';
|
import { initUI, header, ok, info, warn, fail } from '../utils/ui';
|
||||||
|
import { extractOption, hasAnyFlag } from './arg-extractor';
|
||||||
|
|
||||||
interface ConfigOptions {
|
interface ConfigOptions {
|
||||||
port?: number;
|
port?: number;
|
||||||
@@ -25,25 +26,28 @@ interface ConfigOptions {
|
|||||||
function parseArgs(args: string[]): ConfigOptions {
|
function parseArgs(args: string[]): ConfigOptions {
|
||||||
const result: ConfigOptions = {};
|
const result: ConfigOptions = {};
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
if (hasAnyFlag(args, ['--help', '-h'])) {
|
||||||
const arg = args[i];
|
showHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
if ((arg === '--port' || arg === '-p') && args[i + 1]) {
|
const portOption = extractOption(args, ['--port', '-p']);
|
||||||
const port = parseInt(args[++i], 10);
|
if (portOption.found) {
|
||||||
if (!isNaN(port) && port > 0 && port < 65536) {
|
if (portOption.missingValue || !portOption.value) {
|
||||||
result.port = port;
|
console.error(fail('Invalid port number'));
|
||||||
} else {
|
process.exit(1);
|
||||||
console.error(fail('Invalid port number'));
|
}
|
||||||
process.exit(1);
|
|
||||||
}
|
const port = parseInt(portOption.value, 10);
|
||||||
} else if (arg === '--dev') {
|
if (!isNaN(port) && port > 0 && port < 65536) {
|
||||||
result.dev = true;
|
result.port = port;
|
||||||
} else if (arg === '--help' || arg === '-h') {
|
} else {
|
||||||
showHelp();
|
console.error(fail('Invalid port number'));
|
||||||
process.exit(0);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.dev = hasAnyFlag(args, ['--dev']);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
loadOrCreateUnifiedConfig,
|
loadOrCreateUnifiedConfig,
|
||||||
} from '../config/unified-config-loader';
|
} from '../config/unified-config-loader';
|
||||||
import { DEFAULT_IMAGE_ANALYSIS_CONFIG } from '../config/unified-config-types';
|
import { DEFAULT_IMAGE_ANALYSIS_CONFIG } from '../config/unified-config-types';
|
||||||
|
import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities';
|
||||||
|
import { extractOption, hasAnyFlag } from './arg-extractor';
|
||||||
|
|
||||||
interface ImageAnalysisCommandOptions {
|
interface ImageAnalysisCommandOptions {
|
||||||
enable?: boolean;
|
enable?: boolean;
|
||||||
@@ -22,29 +24,28 @@ interface ImageAnalysisCommandOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(args: string[]): ImageAnalysisCommandOptions {
|
function parseArgs(args: string[]): ImageAnalysisCommandOptions {
|
||||||
const options: ImageAnalysisCommandOptions = {};
|
const options: ImageAnalysisCommandOptions = {
|
||||||
|
enable: hasAnyFlag(args, ['--enable']),
|
||||||
|
disable: hasAnyFlag(args, ['--disable']),
|
||||||
|
help: hasAnyFlag(args, ['--help', '-h']),
|
||||||
|
};
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
const timeoutOption = extractOption(args, ['--timeout']);
|
||||||
const arg = args[i];
|
if (timeoutOption.found) {
|
||||||
|
const timeout = parseInt(timeoutOption.value || '', 10);
|
||||||
|
if (isNaN(timeout) || timeout < 10 || timeout > 600) {
|
||||||
|
console.error(fail('Timeout must be between 10 and 600 seconds'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
options.timeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
if (arg === '--enable') {
|
const setModelIdx = args.indexOf('--set-model');
|
||||||
options.enable = true;
|
if (setModelIdx !== -1) {
|
||||||
} else if (arg === '--disable') {
|
const provider = args[setModelIdx + 1];
|
||||||
options.disable = true;
|
const model = args[setModelIdx + 2];
|
||||||
} else if (arg === '--timeout' && args[i + 1]) {
|
if (provider && model && !provider.startsWith('-') && !model.startsWith('-')) {
|
||||||
const timeout = parseInt(args[++i], 10);
|
options.setModel = { provider, model };
|
||||||
if (isNaN(timeout) || timeout < 10 || timeout > 600) {
|
|
||||||
console.error(fail('Timeout must be between 10 and 600 seconds'));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
options.timeout = timeout;
|
|
||||||
} else if (arg === '--set-model' && args[i + 1] && args[i + 2]) {
|
|
||||||
options.setModel = {
|
|
||||||
provider: args[++i],
|
|
||||||
model: args[++i],
|
|
||||||
};
|
|
||||||
} else if (arg === '--help' || arg === '-h') {
|
|
||||||
options.help = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,8 +187,10 @@ export async function handleConfigImageAnalysisCommand(args: string[]): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.setModel) {
|
if (options.setModel) {
|
||||||
const validProviders = ['agy', 'gemini', 'codex', 'kiro', 'ghcp', 'claude', 'qwen', 'iflow'];
|
const validProviders = [...CLIPROXY_PROVIDER_IDS];
|
||||||
if (!validProviders.includes(options.setModel.provider)) {
|
if (
|
||||||
|
!validProviders.includes(options.setModel.provider as (typeof CLIPROXY_PROVIDER_IDS)[number])
|
||||||
|
) {
|
||||||
console.error(fail(`Invalid provider: ${options.setModel.provider}`));
|
console.error(fail(`Invalid provider: ${options.setModel.provider}`));
|
||||||
console.error(info(`Valid providers: ${validProviders.join(', ')}`));
|
console.error(info(`Valid providers: ${validProviders.join(', ')}`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
|
||||||
import { initUI, header, subheader, color, dim, ok, fail, warn, info } from '../utils/ui';
|
import { initUI, header, subheader, color, dim, ok, fail, warn, info } from '../utils/ui';
|
||||||
import { InteractivePrompt } from '../utils/prompt';
|
import { InteractivePrompt } from '../utils/prompt';
|
||||||
import ProfileDetector, {
|
import ProfileDetector, {
|
||||||
@@ -21,6 +20,8 @@ import ProfileDetector, {
|
|||||||
import { getEffectiveEnvVars, CLIPROXY_DEFAULT_PORT } from '../cliproxy/config-generator';
|
import { getEffectiveEnvVars, CLIPROXY_DEFAULT_PORT } from '../cliproxy/config-generator';
|
||||||
import { generateCopilotEnv } from '../copilot/copilot-executor';
|
import { generateCopilotEnv } from '../copilot/copilot-executor';
|
||||||
import { expandPath } from '../utils/helpers';
|
import { expandPath } from '../utils/helpers';
|
||||||
|
import { getClaudeConfigDir, getClaudeSettingsPath } from '../utils/claude-config-path';
|
||||||
|
import { extractOption, hasAnyFlag } from './arg-extractor';
|
||||||
|
|
||||||
interface PersistCommandArgs {
|
interface PersistCommandArgs {
|
||||||
profile?: string;
|
profile?: string;
|
||||||
@@ -37,34 +38,37 @@ interface ResolvedEnv {
|
|||||||
|
|
||||||
/** Parse command line arguments */
|
/** Parse command line arguments */
|
||||||
function parseArgs(args: string[]): PersistCommandArgs {
|
function parseArgs(args: string[]): PersistCommandArgs {
|
||||||
const result: PersistCommandArgs = {};
|
const result: PersistCommandArgs = {
|
||||||
for (let i = 0; i < args.length; i++) {
|
yes: hasAnyFlag(args, ['--yes', '-y']),
|
||||||
const arg = args[i];
|
listBackups: hasAnyFlag(args, ['--list-backups']),
|
||||||
if (arg === '--yes' || arg === '-y') {
|
};
|
||||||
result.yes = true;
|
|
||||||
} else if (arg === '--help' || arg === '-h') {
|
const restoreOption = extractOption(args, ['--restore']);
|
||||||
// Will be handled in main function
|
if (restoreOption.found) {
|
||||||
} else if (arg === '--list-backups') {
|
result.restore = restoreOption.missingValue ? true : restoreOption.value || true;
|
||||||
result.listBackups = true;
|
}
|
||||||
} else if (arg === '--restore') {
|
|
||||||
// Check if next arg is a timestamp (not a flag)
|
for (const arg of restoreOption.remainingArgs) {
|
||||||
const nextArg = args[i + 1];
|
if (!arg.startsWith('-')) {
|
||||||
if (nextArg && !nextArg.startsWith('-')) {
|
|
||||||
result.restore = nextArg;
|
|
||||||
i++; // Skip next arg
|
|
||||||
} else {
|
|
||||||
result.restore = true; // Use latest
|
|
||||||
}
|
|
||||||
} else if (!arg.startsWith('-') && !result.profile) {
|
|
||||||
result.profile = arg;
|
result.profile = arg;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get Claude settings.json path */
|
function formatDisplayPath(filePath: string): string {
|
||||||
function getClaudeSettingsPath(): string {
|
const claudeDir = getClaudeConfigDir();
|
||||||
return path.join(os.homedir(), '.claude', 'settings.json');
|
if (filePath === claudeDir) {
|
||||||
|
return '~/.claude';
|
||||||
|
}
|
||||||
|
|
||||||
|
const claudePrefix = `${claudeDir}${path.sep}`;
|
||||||
|
if (filePath.startsWith(claudePrefix)) {
|
||||||
|
return filePath.replace(claudePrefix, '~/.claude/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read existing Claude settings.json with validation */
|
/** Read existing Claude settings.json with validation */
|
||||||
@@ -517,7 +521,7 @@ export async function handlePersistCommand(args: string[]): Promise<void> {
|
|||||||
if (createBackupFlag) {
|
if (createBackupFlag) {
|
||||||
try {
|
try {
|
||||||
createdBackupPath = createBackup();
|
createdBackupPath = createBackup();
|
||||||
console.log(ok(`Backup created: ${createdBackupPath.replace(os.homedir(), '~')}`));
|
console.log(ok(`Backup created: ${formatDisplayPath(createdBackupPath)}`));
|
||||||
console.log('');
|
console.log('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(fail(`Failed to create backup: ${(error as Error).message}`));
|
console.log(fail(`Failed to create backup: ${(error as Error).message}`));
|
||||||
@@ -560,7 +564,7 @@ export async function handlePersistCommand(args: string[]): Promise<void> {
|
|||||||
if (createdBackupPath) {
|
if (createdBackupPath) {
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(info(`A backup was created before this error:`));
|
console.log(info(`A backup was created before this error:`));
|
||||||
console.log(` ${createdBackupPath.replace(os.homedir(), '~')}`);
|
console.log(` ${formatDisplayPath(createdBackupPath)}`);
|
||||||
console.log(dim(' To restore: ccs persist --restore'));
|
console.log(dim(' To restore: ccs persist --restore'));
|
||||||
}
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
|
||||||
import { ok, fail, warn, info } from '../../utils/ui';
|
import { ok, fail, warn, info } from '../../utils/ui';
|
||||||
import { HealthCheck, IHealthChecker, createSpinner } from './types';
|
import { HealthCheck, IHealthChecker, createSpinner } from './types';
|
||||||
import { getCcsDir } from '../../utils/config-manager';
|
import { getCcsDir } from '../../utils/config-manager';
|
||||||
|
import { getClaudeConfigDir } from '../../utils/claude-config-path';
|
||||||
|
|
||||||
const ora = createSpinner();
|
const ora = createSpinner();
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ export class ClaudeSettingsChecker implements IHealthChecker {
|
|||||||
private readonly claudeDir: string;
|
private readonly claudeDir: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.claudeDir = path.join(os.homedir(), '.claude');
|
this.claudeDir = getClaudeConfigDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
run(results: HealthCheck): void {
|
run(results: HealthCheck): void {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Shared extended context helpers used by CLI + UI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Extended context suffix recognized by Claude Code. */
|
||||||
|
export const EXTENDED_CONTEXT_SUFFIX = '[1m]';
|
||||||
|
|
||||||
|
/** Check if model is a native Gemini model (auto-enabled behavior). */
|
||||||
|
export function isNativeGeminiModel(modelId: string): boolean {
|
||||||
|
return modelId.toLowerCase().startsWith('gemini-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if model already has [1m] suffix. */
|
||||||
|
export function hasExtendedContextSuffix(model: string): boolean {
|
||||||
|
return model.toLowerCase().endsWith(EXTENDED_CONTEXT_SUFFIX.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply [1m] suffix to model if not already present. */
|
||||||
|
export function applyExtendedContextSuffix(model: string): string {
|
||||||
|
if (!model) return model;
|
||||||
|
if (hasExtendedContextSuffix(model)) return model;
|
||||||
|
return `${model}${EXTENDED_CONTEXT_SUFFIX}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strip [1m] suffix from model string. */
|
||||||
|
export function stripExtendedContextSuffix(model: string): string {
|
||||||
|
if (!model) return model;
|
||||||
|
return hasExtendedContextSuffix(model) ? model.slice(0, -EXTENDED_CONTEXT_SUFFIX.length) : model;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve Claude config directory with test/dev overrides.
|
||||||
|
* Precedence:
|
||||||
|
* 1. CLAUDE_CONFIG_DIR (explicit override)
|
||||||
|
* 2. CCS_HOME compatibility path (<dirname(CCS_HOME)>/.claude)
|
||||||
|
* 3. ~/.claude (default)
|
||||||
|
*/
|
||||||
|
export function getClaudeConfigDir(): string {
|
||||||
|
if (process.env.CLAUDE_CONFIG_DIR) {
|
||||||
|
return path.resolve(process.env.CLAUDE_CONFIG_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CCS_HOME) {
|
||||||
|
return path.join(path.dirname(path.resolve(process.env.CCS_HOME)), '.claude');
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(os.homedir(), '.claude');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve Claude settings.json path. */
|
||||||
|
export function getClaudeSettingsPath(): string {
|
||||||
|
return path.join(getClaudeConfigDir(), 'settings.json');
|
||||||
|
}
|
||||||
@@ -8,30 +8,15 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
|
||||||
import { info, warn } from '../ui';
|
import { info, warn } from '../ui';
|
||||||
import { getWebSearchConfig } from '../../config/unified-config-loader';
|
import { getWebSearchConfig } from '../../config/unified-config-loader';
|
||||||
import { getCcsHooksDir } from '../config-manager';
|
import { getCcsHooksDir } from '../config-manager';
|
||||||
|
import { getClaudeSettingsPath } from '../claude-config-path';
|
||||||
import { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils';
|
import { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils';
|
||||||
|
|
||||||
// Hook file name
|
// Hook file name
|
||||||
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
|
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Claude settings path (respects CCS_HOME for test isolation)
|
|
||||||
* In tests, returns path under CCS_HOME; in production, uses real ~/.claude/
|
|
||||||
*/
|
|
||||||
function getClaudeSettingsPath(): string {
|
|
||||||
const ccsHome = process.env.CCS_HOME;
|
|
||||||
if (ccsHome) {
|
|
||||||
// Test mode: use CCS_HOME parent for .claude directory
|
|
||||||
// This prevents tests from modifying user's real settings
|
|
||||||
return path.join(path.dirname(ccsHome), '.claude', 'settings.json');
|
|
||||||
}
|
|
||||||
// Production: use real home directory
|
|
||||||
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer time added to max provider timeout for hook timeout (seconds)
|
// Buffer time added to max provider timeout for hook timeout (seconds)
|
||||||
const HOOK_TIMEOUT_BUFFER = 30;
|
const HOOK_TIMEOUT_BUFFER = 30;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import * as os from 'os';
|
import { getClaudeConfigDir } from '../utils/claude-config-path';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TYPE DEFINITIONS
|
// TYPE DEFINITIONS
|
||||||
@@ -176,8 +176,7 @@ export async function parseProjectDirectory(projectDir: string): Promise<RawUsag
|
|||||||
* Get default Claude projects directory
|
* Get default Claude projects directory
|
||||||
*/
|
*/
|
||||||
export function getDefaultProjectsDir(): string {
|
export function getDefaultProjectsDir(): string {
|
||||||
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
return path.join(getClaudeConfigDir(), 'projects');
|
||||||
return path.join(configDir, 'projects');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Router, Request, Response } from 'express';
|
|||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import { getClaudeSettingsPath } from '../../utils/claude-config-path';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -66,11 +66,6 @@ class RestoreMutex {
|
|||||||
|
|
||||||
const restoreMutex = new RestoreMutex();
|
const restoreMutex = new RestoreMutex();
|
||||||
|
|
||||||
/** Get Claude settings.json path */
|
|
||||||
function getClaudeSettingsPath(): string {
|
|
||||||
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if path is a symlink (security check) */
|
/** Check if path is a symlink (security check) */
|
||||||
function isSymlink(filePath: string): boolean {
|
function isSymlink(filePath: string): boolean {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCcsDir, getConfigPath, loadConfigSafe, loadSettings } from '../../utils/config-manager';
|
import { getCcsDir, getConfigPath, loadConfigSafe, loadSettings } from '../../utils/config-manager';
|
||||||
import { expandPath } from '../../utils/helpers';
|
import { expandPath } from '../../utils/helpers';
|
||||||
|
import { getClaudeSettingsPath } from '../../utils/claude-config-path';
|
||||||
import type { Config, Settings } from '../../types/config';
|
import type { Config, Settings } from '../../types/config';
|
||||||
|
|
||||||
/** Model mapping for API profiles */
|
/** Model mapping for API profiles */
|
||||||
@@ -168,7 +169,7 @@ export function validateFilePath(filePath: string): {
|
|||||||
const expandedPath = expandPath(filePath);
|
const expandedPath = expandPath(filePath);
|
||||||
const normalizedPath = path.normalize(expandedPath);
|
const normalizedPath = path.normalize(expandedPath);
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const claudeSettingsPath = expandPath('~/.claude/settings.json');
|
const claudeSettingsPath = path.normalize(getClaudeSettingsPath());
|
||||||
|
|
||||||
// Check if path is within ~/.ccs/
|
// Check if path is within ~/.ccs/
|
||||||
if (normalizedPath.startsWith(ccsDir)) {
|
if (normalizedPath.startsWith(ccsDir)) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
|
||||||
import { getCcsDir } from '../utils/config-manager';
|
import { getCcsDir } from '../utils/config-manager';
|
||||||
|
import { getClaudeConfigDir } from '../utils/claude-config-path';
|
||||||
|
|
||||||
export const sharedRoutes = Router();
|
export const sharedRoutes = Router();
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ function checkSymlinkStatus(): { valid: boolean; message: string } {
|
|||||||
if (stats.isSymbolicLink()) {
|
if (stats.isSymbolicLink()) {
|
||||||
const target = fs.readlinkSync(linkPath);
|
const target = fs.readlinkSync(linkPath);
|
||||||
// Check if it points to ~/.claude/{linkType}
|
// Check if it points to ~/.claude/{linkType}
|
||||||
const expectedTarget = path.join(os.homedir(), '.claude', linkType);
|
const expectedTarget = path.join(getClaudeConfigDir(), linkType);
|
||||||
if (path.resolve(path.dirname(linkPath), target) === path.resolve(expectedTarget)) {
|
if (path.resolve(path.dirname(linkPath), target) === path.resolve(expectedTarget)) {
|
||||||
validLinks++;
|
validLinks++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Extended Context Utilities
|
* UI facade for shared extended-context helpers.
|
||||||
* Shared utilities for extended context (1M token window) feature.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** The suffix that enables 1M token context in Claude Code */
|
export {
|
||||||
export const EXTENDED_CONTEXT_SUFFIX = '[1m]';
|
EXTENDED_CONTEXT_SUFFIX,
|
||||||
|
isNativeGeminiModel,
|
||||||
/**
|
hasExtendedContextSuffix,
|
||||||
* Check if model is a native Gemini model (auto-enabled behavior).
|
applyExtendedContextSuffix,
|
||||||
* Native Gemini models have the gemini-* prefix.
|
stripExtendedContextSuffix,
|
||||||
*
|
} from '../../../src/shared/extended-context-utils';
|
||||||
* NOTE: This function is intentionally duplicated from src/cliproxy/model-catalog.ts
|
|
||||||
* to avoid bundling backend code in the UI. Keep both in sync.
|
|
||||||
*/
|
|
||||||
export function isNativeGeminiModel(modelId: string): boolean {
|
|
||||||
const lower = modelId.toLowerCase();
|
|
||||||
return lower.startsWith('gemini-');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if model string already has [1m] suffix
|
|
||||||
*/
|
|
||||||
export function hasExtendedContextSuffix(model: string): boolean {
|
|
||||||
return model.toLowerCase().endsWith(EXTENDED_CONTEXT_SUFFIX.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply [1m] suffix to model string if not already present
|
|
||||||
*/
|
|
||||||
export function applyExtendedContextSuffix(model: string): string {
|
|
||||||
if (!model) return model;
|
|
||||||
if (hasExtendedContextSuffix(model)) return model;
|
|
||||||
return `${model}${EXTENDED_CONTEXT_SUFFIX}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip [1m] suffix from model string
|
|
||||||
*/
|
|
||||||
export function stripExtendedContextSuffix(model: string): string {
|
|
||||||
if (!model) return model;
|
|
||||||
if (hasExtendedContextSuffix(model)) {
|
|
||||||
return model.slice(0, -EXTENDED_CONTEXT_SUFFIX.length);
|
|
||||||
}
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* Provider Configuration
|
* Provider Configuration
|
||||||
* Shared constants for CLIProxy providers - SINGLE SOURCE OF TRUTH for UI
|
* Backend provider capabilities are the source of truth.
|
||||||
*
|
* UI keeps only presentation-specific overrides (assets/colors/instructions).
|
||||||
* When adding a new provider, update CLIPROXY_PROVIDERS array and related mappings.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
import {
|
||||||
* Canonical list of CLIProxy provider IDs
|
CLIPROXY_PROVIDER_IDS,
|
||||||
* This is the UI's single source of truth for valid providers.
|
PROVIDER_CAPABILITIES,
|
||||||
* Must stay in sync with backend's CLIPROXY_PROFILES in src/auth/profile-detector.ts
|
getProvidersByOAuthFlow,
|
||||||
*/
|
} from '../../../src/cliproxy/provider-capabilities';
|
||||||
export const CLIPROXY_PROVIDERS = [
|
|
||||||
'gemini',
|
/** Canonical list of CLIProxy provider IDs (shared with backend). */
|
||||||
'codex',
|
export const CLIPROXY_PROVIDERS = CLIPROXY_PROVIDER_IDS;
|
||||||
'agy',
|
|
||||||
'qwen',
|
|
||||||
'iflow',
|
|
||||||
'kiro',
|
|
||||||
'ghcp',
|
|
||||||
'claude',
|
|
||||||
'kimi',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/** Union type for CLIProxy provider IDs */
|
/** Union type for CLIProxy provider IDs */
|
||||||
export type CLIProxyProvider = (typeof CLIPROXY_PROVIDERS)[number];
|
export type CLIProxyProvider = (typeof CLIPROXY_PROVIDERS)[number];
|
||||||
@@ -39,44 +30,17 @@ interface ProviderMetadata {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PROVIDER_METADATA: Record<CLIProxyProvider, ProviderMetadata> = {
|
export const PROVIDER_METADATA: Record<CLIProxyProvider, ProviderMetadata> = Object.freeze(
|
||||||
agy: {
|
Object.fromEntries(
|
||||||
displayName: 'Antigravity',
|
CLIPROXY_PROVIDERS.map((provider) => [
|
||||||
description: 'Antigravity AI models',
|
provider,
|
||||||
},
|
{
|
||||||
claude: {
|
displayName: PROVIDER_CAPABILITIES[provider].displayName,
|
||||||
displayName: 'Claude (Anthropic)',
|
description: PROVIDER_CAPABILITIES[provider].description,
|
||||||
description: 'Claude Opus/Sonnet models',
|
},
|
||||||
},
|
])
|
||||||
gemini: {
|
) as Record<CLIProxyProvider, ProviderMetadata>
|
||||||
displayName: 'Google Gemini',
|
);
|
||||||
description: 'Gemini Pro/Flash models',
|
|
||||||
},
|
|
||||||
codex: {
|
|
||||||
displayName: 'OpenAI Codex',
|
|
||||||
description: 'GPT-4 and codex models',
|
|
||||||
},
|
|
||||||
qwen: {
|
|
||||||
displayName: 'Alibaba Qwen',
|
|
||||||
description: 'Qwen Code models',
|
|
||||||
},
|
|
||||||
iflow: {
|
|
||||||
displayName: 'iFlow',
|
|
||||||
description: 'iFlow AI models',
|
|
||||||
},
|
|
||||||
kiro: {
|
|
||||||
displayName: 'Kiro (AWS)',
|
|
||||||
description: 'AWS CodeWhisperer models',
|
|
||||||
},
|
|
||||||
ghcp: {
|
|
||||||
displayName: 'GitHub Copilot (OAuth)',
|
|
||||||
description: 'GitHub Copilot via OAuth',
|
|
||||||
},
|
|
||||||
kimi: {
|
|
||||||
displayName: 'Kimi (Moonshot)',
|
|
||||||
description: 'Moonshot AI K2/K2.5 models',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Map provider names to asset filenames (only providers with actual logos)
|
// Map provider names to asset filenames (only providers with actual logos)
|
||||||
export const PROVIDER_ASSETS: Record<CLIProxyProvider, string> = {
|
export const PROVIDER_ASSETS: Record<CLIProxyProvider, string> = {
|
||||||
@@ -149,13 +113,12 @@ export const PROVIDER_COLORS: Record<string, string> = {
|
|||||||
vertex: '#4285F4',
|
vertex: '#4285F4',
|
||||||
iflow: '#f94144',
|
iflow: '#f94144',
|
||||||
qwen: '#6236FF',
|
qwen: '#6236FF',
|
||||||
kiro: '#4d908e', // Dark Cyan (AWS-inspired)
|
kiro: '#4d908e',
|
||||||
ghcp: '#43aa8b', // Seaweed (GitHub-inspired)
|
ghcp: '#43aa8b',
|
||||||
claude: '#D97757', // Anthropic brand color (matches SVG)
|
claude: '#D97757',
|
||||||
kimi: '#FF6B35', // Moonshot AI brand orange
|
kimi: '#FF6B35',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Provider display names
|
|
||||||
const PROVIDER_NAMES: Record<string, string> = {
|
const PROVIDER_NAMES: Record<string, string> = {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
CLIPROXY_PROVIDERS.map((provider) => [provider, PROVIDER_METADATA[provider].displayName])
|
CLIPROXY_PROVIDERS.map((provider) => [provider, PROVIDER_METADATA[provider].displayName])
|
||||||
@@ -163,7 +126,6 @@ const PROVIDER_NAMES: Record<string, string> = {
|
|||||||
vertex: 'Vertex AI',
|
vertex: 'Vertex AI',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map provider to display name
|
|
||||||
export function getProviderDisplayName(provider: unknown): string {
|
export function getProviderDisplayName(provider: unknown): string {
|
||||||
const normalized = normalizeProviderInput(provider);
|
const normalized = normalizeProviderInput(provider);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -181,9 +143,10 @@ export function getProviderDescription(provider: unknown): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Providers that use Device Code OAuth flow instead of Authorization Code flow.
|
* Providers that use Device Code OAuth flow instead of Authorization Code flow.
|
||||||
* Device Code flow requires displaying a user code for manual entry at provider's website.
|
|
||||||
*/
|
*/
|
||||||
export const DEVICE_CODE_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro', 'qwen', 'kimi'];
|
export const DEVICE_CODE_PROVIDERS: CLIProxyProvider[] = [
|
||||||
|
...getProvidersByOAuthFlow('device_code'),
|
||||||
|
];
|
||||||
|
|
||||||
const DEVICE_CODE_PROVIDER_DISPLAY_NAMES: Readonly<Partial<Record<CLIProxyProvider, string>>> =
|
const DEVICE_CODE_PROVIDER_DISPLAY_NAMES: Readonly<Partial<Record<CLIProxyProvider, string>>> =
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
|
|||||||
Reference in New Issue
Block a user