diff --git a/.github/workflows/deploy-ccs-worker.yml b/.github/workflows/deploy-ccs-worker.yml index 6391e91f..d08900c4 100644 --- a/.github/workflows/deploy-ccs-worker.yml +++ b/.github/workflows/deploy-ccs-worker.yml @@ -8,9 +8,13 @@ on: - 'scripts/wrangler.toml' - 'installers/**' - # Run on all PRs to satisfy branch protection (no path filter) + # Only run on PRs that touch installer-related files pull_request: branches: [main] + paths: + - 'scripts/worker.js' + - 'scripts/wrangler.toml' + - 'installers/**' # Allow manual trigger workflow_dispatch: diff --git a/VERSION b/VERSION index 1bc788d3..0305f287 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.6.0 +5.6.0-dev.3 diff --git a/config/base-iflow.settings.json b/config/base-iflow.settings.json new file mode 100644 index 00000000..2fc56b47 --- /dev/null +++ b/config/base-iflow.settings.json @@ -0,0 +1,10 @@ +{ + "env": { + "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/iflow", + "ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed", + "ANTHROPIC_MODEL": "deepseek-v3.2", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "kimi-k2-thinking", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v3.2", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "minimax-m2" + } +} diff --git a/config/base-qwen.settings.json b/config/base-qwen.settings.json index 03b3e36c..03c510ca 100644 --- a/config/base-qwen.settings.json +++ b/config/base-qwen.settings.json @@ -2,9 +2,9 @@ "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/qwen", "ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed", - "ANTHROPIC_MODEL": "qwen3-coder", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3-coder", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3-coder", - "ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3-coder" + "ANTHROPIC_MODEL": "qwen3-coder-plus", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3-coder-plus", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3-coder-plus", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3-vl-plus" } } diff --git a/package.json b/package.json index ad99505d..b432e6a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "5.6.0", + "version": "5.6.0-dev.3", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", diff --git a/src/ccs.ts b/src/ccs.ts index eb6f2d11..ff210f8f 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -228,7 +228,8 @@ async function main(): Promise { // Special case: doctor command if (firstArg === 'doctor' || firstArg === '--doctor') { - await handleDoctorCommand(); + const shouldFix = args.includes('--fix') || args.includes('-f'); + await handleDoctorCommand(shouldFix); return; } diff --git a/src/cliproxy/auth-handler.ts b/src/cliproxy/auth-handler.ts index 4a192c33..311ad428 100644 --- a/src/cliproxy/auth-handler.ts +++ b/src/cliproxy/auth-handler.ts @@ -12,12 +12,12 @@ * Each provider has its own directory to avoid conflicts. */ +import { execSync, spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { spawn, execSync } from 'child_process'; import { ProgressIndicator } from '../utils/progress-indicator'; -import { getProviderAuthDir, generateConfig } from './config-generator'; import { ensureCLIProxyBinary } from './binary-manager'; +import { generateConfig, getProviderAuthDir } from './config-generator'; import { CLIProxyProvider } from './types'; /** @@ -169,6 +169,13 @@ const OAUTH_CONFIGS: Record = { scopes: ['openid', 'profile', 'email', 'model.completion'], authFlag: '--qwen-login', }, + iflow: { + provider: 'iflow', + displayName: 'iFlow', + authUrl: 'https://iflow.cn/oauth', + scopes: ['phone', 'profile', 'email'], + authFlag: '--iflow-login', + }, }; /** @@ -199,6 +206,7 @@ const PROVIDER_AUTH_PREFIXES: Record = { codex: ['codex-', 'openai-'], agy: ['antigravity-', 'agy-'], qwen: ['qwen-'], + iflow: ['iflow-'], }; /** @@ -210,6 +218,7 @@ const PROVIDER_TYPE_VALUES: Record = { codex: ['codex'], agy: ['antigravity'], qwen: ['qwen'], + iflow: ['iflow'], }; /** @@ -327,7 +336,7 @@ export function getAuthStatus(provider: CLIProxyProvider): AuthStatus { * Get auth status for all providers */ export function getAllAuthStatus(): AuthStatus[] { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen']; + const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow']; return providers.map(getAuthStatus); } diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index 9c6cde6c..c267bc29 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -12,7 +12,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { getCcsDir } from '../utils/config-manager'; import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types'; -import { getModelMappingFromConfig } from './base-config-loader'; +import { getModelMappingFromConfig, getEnvVarsFromConfig } from './base-config-loader'; /** Settings file structure for user overrides */ interface ProviderSettings { @@ -31,6 +31,7 @@ const PROVIDER_DISPLAY_NAMES: Record = { codex: 'Codex', agy: 'Antigravity', qwen: 'Qwen Code', + iflow: 'iFlow', }; /** @@ -197,7 +198,11 @@ export function getClaudeEnvVars( ): NodeJS.ProcessEnv { const models = getModelMapping(provider); - return { + // Base env vars from config file (includes ANTHROPIC_MAX_TOKENS, etc.) + const baseEnvVars = getEnvVarsFromConfig(provider); + + // Core env vars that we always set dynamically + const coreEnvVars = { // Provider-specific endpoint - routes to correct provider via URL path ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}/api/provider/${provider}`, ANTHROPIC_AUTH_TOKEN: CCS_INTERNAL_API_KEY, @@ -206,6 +211,23 @@ export function getClaudeEnvVars( ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnetModel || models.claudeModel, ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haikuModel || models.claudeModel, }; + + // Filter out core env vars from base config to avoid conflicts + const { + ANTHROPIC_BASE_URL: _baseUrl, + ANTHROPIC_AUTH_TOKEN: _authToken, + ANTHROPIC_MODEL: _model, + ANTHROPIC_DEFAULT_OPUS_MODEL: _opusModel, + ANTHROPIC_DEFAULT_SONNET_MODEL: _sonnetModel, + ANTHROPIC_DEFAULT_HAIKU_MODEL: _haikuModel, + ...additionalEnvVars + } = baseEnvVars; + + // Merge core env vars with additional env vars from base config + return { + ...coreEnvVars, + ...additionalEnvVars, // Includes ANTHROPIC_MAX_TOKENS, etc. + }; } /** diff --git a/src/cliproxy/types.ts b/src/cliproxy/types.ts index a9ae85a7..925842d0 100644 --- a/src/cliproxy/types.ts +++ b/src/cliproxy/types.ts @@ -47,7 +47,7 @@ export interface BinaryManagerConfig { /** Enable verbose logging */ verbose: boolean; /** Force specific version (skip auto-upgrade to latest) */ - forceVersion?: boolean; + forceVersion: boolean; } /** @@ -113,8 +113,9 @@ export interface DownloadResult { * - codex: OpenAI Codex via OAuth * - agy: Antigravity via OAuth (short name for easy usage) * - qwen: Qwen Code via OAuth (qwen3-coder) + * - iflow: iFlow via OAuth */ -export type CLIProxyProvider = 'gemini' | 'codex' | 'agy' | 'qwen'; +export type CLIProxyProvider = 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow'; /** * CLIProxy config.yaml structure (minimal) diff --git a/src/commands/doctor-command.ts b/src/commands/doctor-command.ts index 5d5ff175..6f4c7c65 100644 --- a/src/commands/doctor-command.ts +++ b/src/commands/doctor-command.ts @@ -6,14 +6,20 @@ /** * Handle doctor command + * @param shouldFix - If true, attempt to fix detected issues */ -export async function handleDoctorCommand(): Promise { +export async function handleDoctorCommand(shouldFix = false): Promise { const DoctorModule = await import('../management/doctor'); const Doctor = DoctorModule.default; const doctor = new Doctor(); await doctor.runAllChecks(); + // Attempt to fix issues if --fix flag provided + if (shouldFix) { + await doctor.fixIssues(); + } + // Exit with error code if unhealthy process.exit(doctor.isHealthy() ? 0 : 1); } diff --git a/src/commands/sync-command.ts b/src/commands/sync-command.ts index 33244096..bb2c60c0 100644 --- a/src/commands/sync-command.ts +++ b/src/commands/sync-command.ts @@ -31,6 +31,15 @@ export async function handleSyncCommand(): Promise { const manager = new ClaudeSymlinkManager(); manager.install(false); + console.log(''); + + // Repair shared symlinks (~/.ccs/shared/ → ~/.claude/) + // This fixes symlinks broken by Claude CLI's atomic writes (e.g., toggle thinking) + const SharedManager = (await import('../management/shared-manager')).default; + const sharedManager = new SharedManager(); + sharedManager.ensureSharedDirectories(); + console.log(colored('[OK]', 'green') + ' Shared symlinks verified'); + console.log(''); console.log(colored('[OK] Sync complete!', 'green')); console.log(''); diff --git a/src/management/doctor.ts b/src/management/doctor.ts index af25862a..3dfaee1b 100644 --- a/src/management/doctor.ts +++ b/src/management/doctor.ts @@ -955,6 +955,49 @@ class Doctor { ); } + /** + * Fix detected issues + * Currently fixes: shared symlinks broken by Claude CLI's atomic writes + */ + async fixIssues(): Promise { + console.log(''); + console.log(header('ATTEMPTING FIXES')); + console.log(''); + + let fixed = 0; + + // Fix shared symlinks (settings.json broken by Claude CLI toggle thinking, etc.) + const sharedSettings = path.join(this.homedir, '.ccs', 'shared', 'settings.json'); + if (fs.existsSync(sharedSettings)) { + try { + const stats = fs.lstatSync(sharedSettings); + if (!stats.isSymbolicLink()) { + const spinner = ora('Fixing shared settings.json symlink').start(); + try { + // Import and use SharedManager to fix symlinks + const SharedManagerModule = await import('./shared-manager'); + const SharedManager = SharedManagerModule.default; + const sharedManager = new SharedManager(); + sharedManager.ensureSharedDirectories(); + spinner.succeed(ok('Fixed') + ' Shared settings.json symlink restored'); + fixed++; + } catch (err) { + spinner.fail(fail('Failed') + ` Could not fix: ${(err as Error).message}`); + } + } + } catch (_err) { + // Ignore stat errors + } + } + + if (fixed > 0) { + console.log(''); + console.log(ok(`[OK] Fixed ${fixed} issue(s)`)); + } else { + console.log(info('[i] No fixable issues detected')); + } + } + /** * Check if the health check results are healthy */