Files
ccs/src/codex-auth/commands/use-command.ts
T
Tam Nhu Tran bf92645b35 feat(codex-auth): add ccsx auth CLI subcommands (create/login/switch/use/show/remove)
Implements the user-facing surface for ccsx auth profile management.
After `ccsx auth create work` (auto-spawns codex login with CODEX_HOME
pinned per D11), users can `eval "$(ccsx auth use work)"` in any shell
to scope all subsequent codex invocations to that profile — letting
two terminals run two different Codex accounts concurrently.

- codex-auth-router: dispatches argv to subcommand handlers
- create: idempotent, --force re-links config.toml preserving auth.json
  (D9), then auto-spawns codex login with CODEX_HOME pinned (D11);
  filesystem ops happen before registry write to avoid registry orphans
  on EACCES/ENOSPC
- login: standalone re-auth for an existing profile
- switch: persistent default in YAML registry
- use: STDOUT-DISCIPLINED — emits only shell-evalable exports;
  bash/zsh/fish/PowerShell/cmd syntaxes via shell-detect; sets
  CCS_NO_PRE_DISPATCH=1 at module load to suppress recovery/migration
  banners that would otherwise contaminate eval (C2)
- show: list (active(missing) row at top per D14) + detail views
- remove: default-profile guard, active-shell warn, --yes / --force
- ASCII-only output, NO_COLOR honored, all errors to stderr via
  exitWithError
- pre-dispatch.ts: early-return when CCS_NO_PRE_DISPATCH=1, placed
  before autoMigrate which is itself a stdout writer

57 unit tests, all green. Help text cross-references the ccsxp/ccsx
distinction since the binaries differ by one character (H5).
2026-05-17 14:44:44 -04:00

103 lines
4.1 KiB
TypeScript

/**
* codex-auth use command.
*
* STDOUT DISCIPLINE (C2, R4): stdout contains ONLY shell-evalable export
* statements. ALL errors, hints, and info messages go to STDERR so that
* `eval "$(ccsx auth use <name>)"` is never contaminated.
*
* Belt-and-suspenders for C2: the primary protection is
* `src/bin/codex-runtime-router.ts`, which dispatches `auth` subcommands
* BEFORE pre-dispatch runs at all. This IIFE is a fallback for any future
* code path (e.g., direct import from a different bin entry) that bypasses
* the router and would otherwise let pre-dispatch banners hit stdout.
*/
(function guardPreDispatch() {
const argv = process.argv;
// argv[2] is the first user arg to ccsx (e.g. "auth"), argv[3] is the subcommand
for (let i = 2; i < argv.length - 1; i++) {
if (argv[i] === 'auth' && argv[i + 1] === 'use') {
process.env.CCS_NO_PRE_DISPATCH = '1';
break;
}
}
})();
// ── End guard — safe to import CCS modules now ───────────────────────────────
import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes';
import { resolveCodexProfileDir } from '../codex-profile-paths';
import { detectShell, formatExport } from '../shell-detect';
import { parseArgs, rejectUnsupportedOptions, getProfileNameError } from './types';
import type { Shell } from '../shell-detect';
import type { CodexCommandContext } from './types';
const VALID_SHELLS = new Set<string>(['bash', 'zsh', 'fish', 'pwsh', 'cmd']);
export async function handleUseCodex(ctx: CodexCommandContext, args: string[]): Promise<void> {
const parsed = parseArgs(args);
rejectUnsupportedOptions(parsed, 'ccsx auth use <name> [--shell <bash|zsh|fish|pwsh|cmd>]');
const { profileName, shell: shellOverride } = parsed;
// All errors → stderr, empty stdout
if (!profileName) {
process.stderr.write('[X] Profile name is required.\n');
process.stderr.write('Usage: ccsx auth use <name> [--shell <bash|zsh|fish|pwsh|cmd>]\n');
process.exit(ExitCode.PROFILE_ERROR);
return;
}
const nameError = getProfileNameError(profileName);
if (nameError) {
process.stderr.write(`[X] ${nameError}\n`);
process.exit(ExitCode.PROFILE_ERROR);
return;
}
if (shellOverride !== undefined && !VALID_SHELLS.has(shellOverride)) {
process.stderr.write(
`[X] Unsupported --shell value: "${shellOverride}". Valid: bash, zsh, fish, pwsh, cmd\n`
);
process.exit(ExitCode.GENERAL_ERROR);
return;
}
const { registry } = ctx;
if (!registry.hasProfile(profileName)) {
const available = registry.listProfiles();
const availableStr = available.length > 0 ? available.join(', ') : '<none>';
process.stderr.write(`[X] Profile not found: ${profileName}. Available: ${availableStr}\n`);
process.exit(ExitCode.PROFILE_ERROR);
return;
}
const profileDir = resolveCodexProfileDir(profileName);
const shell: Shell =
shellOverride !== undefined
? (shellOverride as Shell)
: detectShell(process.env, process.platform);
// ── STDOUT: only export statements ──────────────────────────────────────────
process.stdout.write(formatExport(shell, 'CODEX_HOME', profileDir) + '\n');
process.stdout.write(formatExport(shell, 'CCS_CODEX_PROFILE', profileName) + '\n');
// ── STDERR: human-readable hint ─────────────────────────────────────────────
process.stderr.write(`[i] Codex profile "${profileName}" active in this shell. Run: codex\n`);
if (shell === 'cmd') {
process.stderr.write('[i] Note: cmd.exe cannot eval output from a subprocess natively.\n');
process.stderr.write(
' Use PowerShell: ccsx auth use ' + profileName + ' | Invoke-Expression\n'
);
}
// Note: This profile applies only to native `codex`.
// `ccsxp` ignores CCS_CODEX_PROFILE and uses its own cliproxy pool.
}
// suppress unused import warning — exitWithError is available but we use process.exit
// for stdout purity in this command
void exitWithError;