Files
ccs/src/commands/cliproxy/pool-subcommand.ts
T
Tam Nhu Tran 1c90388b5d fix(cliproxy): harden account pools per pre-merge usefulness review
- pool --enable/--disable: refuse remote targets with manual config
  guidance; resolve the lifecycle port instead of assuming 8317
- enablePoolRouting: roll back the pool flag when config regeneration
  fails; already-enabled path re-runs regeneration (repairable state)
- pool opt-in prompt: gate on hasUnifiedConfig (legacy installs skip);
  never auto-accept consent under --yes/CCS_YES
- order show (file mode): render selector pick order incl. residual
  on-disk priorities and surface drift instead of alphabetical order
- order --reset: clear residual priority fields via management-API
  PATCH when proxy runs, atomic direct write when stopped
- quota pool section: classify in-proxy 429 cooldowns as cooling with
  reset times (graceful degradation when proxy or endpoint is absent);
  honest paused label plus resume hint
- routing state: remote targets report pool as not manageable instead
  of echoing the local flag; strategy/affinity apply warns when pool
  routing overrides the change (CLI, API message, and dashboard)
- claude model-neutral: --config explicit pins survive the stale-pin
  migration; remote env path filters historical default pins read from
  claude.settings.json without mutating the file
- cross-lane guard: also checks account-profile CLAUDE_CONFIG_DIR lanes
  for email overlap, not only the ambient ~/.claude login
- onboarding hint: opt-in copy naming ccs cliproxy pool --enable
- dashboard card: pool-ON shows drain-order pointer, pool-OFF shows the
  enable command, local-only note for remote proxies (i18n x5)
2026-06-11 18:50:12 -04:00

122 lines
4.3 KiB
TypeScript

/**
* CLIProxy Pool Routing Subcommand
*
* Handles:
* ccs cliproxy pool --enable Enable pool routing (fill-first + affinity + cooling ON)
* ccs cliproxy pool --disable Disable pool routing and restore non-pool config
* ccs cliproxy pool Show current pool routing state
*/
import { initUI, header, ok, warn, info } from '../../utils/ui';
import {
enablePoolRouting,
disablePoolRouting,
POOL_MAX_RETRY_CREDENTIALS,
} from '../../cliproxy/routing/routing-strategy';
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
import { resolveLifecyclePort } from '../../cliproxy/config/port-manager';
import { getProxyTarget } from '../../cliproxy/proxy/proxy-target-resolver';
import { getConfigPathForPort, getAuthDir } from '../../cliproxy/config/path-resolver';
import { hasAnyFlag } from '../arg-extractor';
/**
* Print the manual-config guidance for remote/Docker targets, then refuse.
* enablePoolRouting/disablePoolRouting only write local config files, so a
* remote proxy is never touched — refuse rather than lie about hot-reload.
* Mirrors the remote-refusal copy in order-subcommand and the manual snippet
* in pool-opt-in-prompt's printRemoteHint().
*/
function printRemotePoolRefusal(enable: boolean): void {
console.log(
warn(
`Remote proxy target detected. Pool routing management for remote proxies is not\n` +
` supported in v1. Run this command on the host running CLIProxy instead.`
)
);
console.log('');
if (enable) {
console.log(' To enable pool routing manually, add to your CLIProxy config.yaml:');
console.log(' disable-cooling: false');
console.log(` max-retry-credentials: ${POOL_MAX_RETRY_CREDENTIALS}`);
console.log(' routing:');
console.log(' strategy: fill-first');
console.log(' session-affinity: true');
console.log(' session-affinity-ttl: "1h"');
console.log('');
}
}
export async function handlePoolSubcommand(args: string[]): Promise<void> {
await initUI();
console.log('');
console.log(header('CLIProxy Pool Routing'));
console.log('');
const wantsEnable = hasAnyFlag(args, ['--enable']);
const wantsDisable = hasAnyFlag(args, ['--disable']);
// Resolve the real proxy target before any config write. Remote targets are
// refused (enable/disable only mutate local files; the remote proxy is never
// touched, so the "hot-reload" success message would be a lie). Local targets
// use the configured lifecycle port so the regenerated config-<port>.yaml is
// the same file the running proxy reads.
if (wantsEnable || wantsDisable) {
const target = getProxyTarget();
if (target.isRemote) {
printRemotePoolRefusal(wantsEnable);
process.exitCode = 1;
return;
}
}
const port = resolveLifecyclePort();
const configPath = getConfigPathForPort(port);
const authDir = getAuthDir();
if (wantsEnable) {
const result = enablePoolRouting(port, { configPath, authDir });
if (result.failed) {
console.log(warn(result.message));
process.exitCode = 1;
} else if (result.changed) {
console.log(ok(result.message));
} else {
console.log(info(result.message));
}
console.log('');
return;
}
if (wantsDisable) {
const result = disablePoolRouting(port, { configPath, authDir });
if (result.failed) {
console.log(warn(result.message));
process.exitCode = 1;
} else if (result.changed) {
console.log(ok(result.message));
} else {
console.log(info(result.message));
}
console.log('');
return;
}
// Default: show status
const config = loadOrCreateUnifiedConfig();
const enabled = config.cliproxy?.pool_routing?.enabled === true;
const dismissed = config.cliproxy?.pool_routing?.prompt_dismissed === true;
const maxRetry = config.cliproxy?.pool_routing?.max_retry_credentials;
console.log(` Status: ${enabled ? ok('enabled') : warn('disabled')}`);
if (enabled && maxRetry !== undefined) {
console.log(` Max retry: ${maxRetry}`);
}
if (!enabled && dismissed) {
console.log(` Dismissed: ${info('yes (prompt will not re-show)')}`);
}
console.log('');
console.log(` Enable: ccs cliproxy pool --enable`);
console.log(` Disable: ccs cliproxy pool --disable`);
console.log('');
}