mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 14:21:20 +00:00
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).
141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
let tempDir: string;
|
|
let ccsHome: string;
|
|
const ORIG_CCS_HOME = process.env.CCS_HOME;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-router-test-'));
|
|
ccsHome = path.join(tempDir, 'ccs');
|
|
fs.mkdirSync(path.join(ccsHome, '.ccs'), { recursive: true });
|
|
process.env.CCS_HOME = ccsHome;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (ORIG_CCS_HOME === undefined) delete process.env.CCS_HOME;
|
|
else process.env.CCS_HOME = ORIG_CCS_HOME;
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
async function loadRouter() {
|
|
// Re-import fresh each test via dynamic import cache busting with timestamp
|
|
const { runCodexAuth } = await import('../../../src/codex-auth/codex-auth-router');
|
|
return runCodexAuth;
|
|
}
|
|
|
|
describe('runCodexAuth — help and no-arg', () => {
|
|
it('no args → prints help and returns 0', async () => {
|
|
const runCodexAuth = await loadRouter();
|
|
const out: string[] = [];
|
|
const origWrite = process.stdout.write.bind(process.stdout);
|
|
process.stdout.write = (chunk: string | Uint8Array) => {
|
|
out.push(String(chunk));
|
|
return true;
|
|
};
|
|
try {
|
|
const code = await runCodexAuth([]);
|
|
expect(code).toBe(0);
|
|
expect(out.join('')).toContain('ccsx auth');
|
|
} finally {
|
|
process.stdout.write = origWrite;
|
|
}
|
|
});
|
|
|
|
it('--help → returns 0 and prints help', async () => {
|
|
const runCodexAuth = await loadRouter();
|
|
const out: string[] = [];
|
|
const origWrite = process.stdout.write.bind(process.stdout);
|
|
process.stdout.write = (chunk: string | Uint8Array) => {
|
|
out.push(String(chunk));
|
|
return true;
|
|
};
|
|
try {
|
|
const code = await runCodexAuth(['--help']);
|
|
expect(code).toBe(0);
|
|
expect(out.join('')).toContain('Commands');
|
|
} finally {
|
|
process.stdout.write = origWrite;
|
|
}
|
|
});
|
|
|
|
it('-h → returns 0', async () => {
|
|
const runCodexAuth = await loadRouter();
|
|
const out: string[] = [];
|
|
const origWrite = process.stdout.write.bind(process.stdout);
|
|
process.stdout.write = (chunk: string | Uint8Array) => {
|
|
out.push(String(chunk));
|
|
return true;
|
|
};
|
|
try {
|
|
const code = await runCodexAuth(['-h']);
|
|
expect(code).toBe(0);
|
|
} finally {
|
|
process.stdout.write = origWrite;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('runCodexAuth — unknown subcommand', () => {
|
|
it('unknown subcommand → returns 1 and writes to stderr', async () => {
|
|
const runCodexAuth = await loadRouter();
|
|
const errOut: string[] = [];
|
|
const origWrite = process.stderr.write.bind(process.stderr);
|
|
process.stderr.write = (chunk: string | Uint8Array) => {
|
|
errOut.push(String(chunk));
|
|
return true;
|
|
};
|
|
try {
|
|
const code = await runCodexAuth(['bogus']);
|
|
expect(code).toBe(1);
|
|
expect(errOut.join('')).toContain('Unknown command');
|
|
expect(errOut.join('')).toContain('bogus');
|
|
} finally {
|
|
process.stderr.write = origWrite;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('runCodexAuth — version', () => {
|
|
it('--version → returns 0 and prints version', async () => {
|
|
const runCodexAuth = await loadRouter();
|
|
const out: string[] = [];
|
|
const origWrite = process.stdout.write.bind(process.stdout);
|
|
process.stdout.write = (chunk: string | Uint8Array) => {
|
|
out.push(String(chunk));
|
|
return true;
|
|
};
|
|
try {
|
|
const code = await runCodexAuth(['--version']);
|
|
expect(code).toBe(0);
|
|
expect(out.join('')).toMatch(/\d+\.\d+/);
|
|
} finally {
|
|
process.stdout.write = origWrite;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('runCodexAuth — dispatches show without crashing', () => {
|
|
it('show with no profiles → exit 0', async () => {
|
|
const runCodexAuth = await loadRouter();
|
|
const out: string[] = [];
|
|
const origLog = console.log;
|
|
const origWrite = process.stdout.write.bind(process.stdout);
|
|
console.log = (...a: unknown[]) => out.push(a.map(String).join(' '));
|
|
process.stdout.write = (chunk: string | Uint8Array) => {
|
|
out.push(String(chunk));
|
|
return true;
|
|
};
|
|
try {
|
|
const code = await runCodexAuth(['show']);
|
|
expect(code).toBe(0);
|
|
expect(out.join('')).toContain('No Codex profiles');
|
|
} finally {
|
|
console.log = origLog;
|
|
process.stdout.write = origWrite;
|
|
}
|
|
});
|
|
});
|