mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-09 02:17:03 +00:00
fix: resolve ccsx auth profiles before CCS profiles
This commit is contained in:
@@ -28,6 +28,9 @@ codex
|
|||||||
# Terminal B:
|
# Terminal B:
|
||||||
eval "$(ccsx auth use personal)"
|
eval "$(ccsx auth use personal)"
|
||||||
codex
|
codex
|
||||||
|
|
||||||
|
# Or launch a named profile directly through ccsx
|
||||||
|
ccsx work
|
||||||
```
|
```
|
||||||
|
|
||||||
## Two-terminal example
|
## Two-terminal example
|
||||||
@@ -49,6 +52,7 @@ codex # runs with CODEX_HOME=~/.ccs/codex-instances/pers
|
|||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `ccsx auth create <name>` | Create profile dir + auto-login |
|
| `ccsx auth create <name>` | Create profile dir + auto-login |
|
||||||
|
| `ccsx <name>` | Launch a named Codex auth profile |
|
||||||
| `ccsx auth login <name>` | (Re-)authenticate an existing profile |
|
| `ccsx auth login <name>` | (Re-)authenticate an existing profile |
|
||||||
| `ccsx auth switch <name>` | Set the persistent default profile for future `ccsx` launches |
|
| `ccsx auth switch <name>` | Set the persistent default profile for future `ccsx` launches |
|
||||||
| `ccsx auth use <name>` | Emit shell exports for this shell only (use with `eval`) |
|
| `ccsx auth use <name>` | Emit shell exports for this shell only (use with `eval`) |
|
||||||
@@ -60,6 +64,7 @@ codex # runs with CODEX_HOME=~/.ccs/codex-instances/pers
|
|||||||
|
|
||||||
| Method | Scope | How |
|
| Method | Scope | How |
|
||||||
|--------|-------|-----|
|
|--------|-------|-----|
|
||||||
|
| `ccsx <name>` | One launch | Resolves `<name>` from the Codex profile registry |
|
||||||
| `ccsx auth switch <name>` | Future `ccsx` launches | Writes to `~/.ccs/codex-profiles.yaml` |
|
| `ccsx auth switch <name>` | Future `ccsx` launches | Writes to `~/.ccs/codex-profiles.yaml` |
|
||||||
| `eval "$(ccsx auth use <name>)"` | Current shell only | Sets `CODEX_HOME` + `CCS_CODEX_PROFILE` in your shell |
|
| `eval "$(ccsx auth use <name>)"` | Current shell only | Sets `CODEX_HOME` + `CCS_CODEX_PROFILE` in your shell |
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,31 @@ function errorMessage(err: unknown): string {
|
|||||||
return String(err);
|
return String(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeSelectPositionalCodexProfile(argv: string[]): void {
|
||||||
|
const candidate = (argv[2] ?? '').trim();
|
||||||
|
if (!candidate || candidate.startsWith('-') || candidate === 'auth') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getNativeCodexPassthroughArgs } = require('../dispatcher/cli-argument-parser') as {
|
||||||
|
getNativeCodexPassthroughArgs: (args: string[]) => string[] | null;
|
||||||
|
};
|
||||||
|
if (getNativeCodexPassthroughArgs(argv.slice(2)) !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { CodexProfileRegistry } = require('../codex-auth/codex-profile-registry') as {
|
||||||
|
CodexProfileRegistry: new () => { hasProfile: (name: string) => boolean };
|
||||||
|
};
|
||||||
|
const registry = new CodexProfileRegistry();
|
||||||
|
if (!registry.hasProfile(candidate)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.CCS_CODEX_PROFILE = candidate;
|
||||||
|
argv[2] = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry-point for the ccsx / codex-runtime binary.
|
* Main entry-point for the ccsx / codex-runtime binary.
|
||||||
*
|
*
|
||||||
@@ -54,11 +79,13 @@ export async function main(argv: string[]): Promise<number> {
|
|||||||
|
|
||||||
// ── non-auth branch: profile resolution ─────────────────────────────────
|
// ── non-auth branch: profile resolution ─────────────────────────────────
|
||||||
|
|
||||||
// F1: respect explicit CODEX_HOME unless CCS_CODEX_PROFILE asks for a managed profile.
|
try {
|
||||||
const explicit = (process.env.CODEX_HOME ?? '').trim();
|
maybeSelectPositionalCodexProfile(argv);
|
||||||
const profileOverride = (process.env.CCS_CODEX_PROFILE ?? '').trim();
|
|
||||||
if (!explicit || profileOverride) {
|
// F1: respect explicit CODEX_HOME unless CCS_CODEX_PROFILE asks for a managed profile.
|
||||||
try {
|
const explicit = (process.env.CODEX_HOME ?? '').trim();
|
||||||
|
const profileOverride = (process.env.CCS_CODEX_PROFILE ?? '').trim();
|
||||||
|
if (!explicit || profileOverride) {
|
||||||
const { resolveActiveProfile } = require('../codex-auth/resolve-active-profile') as {
|
const { resolveActiveProfile } = require('../codex-auth/resolve-active-profile') as {
|
||||||
resolveActiveProfile: (
|
resolveActiveProfile: (
|
||||||
env: NodeJS.ProcessEnv
|
env: NodeJS.ProcessEnv
|
||||||
@@ -85,15 +112,15 @@ export async function main(argv: string[]): Promise<number> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (resolverErr) {
|
}
|
||||||
const msg = errorMessage(resolverErr);
|
} catch (resolverErr) {
|
||||||
if (isCodexAuthProfileResolutionError(resolverErr)) {
|
const msg = errorMessage(resolverErr);
|
||||||
process.stderr.write(`[X] codex-auth: ${msg}\n`);
|
if (isCodexAuthProfileResolutionError(resolverErr)) {
|
||||||
return 1;
|
process.stderr.write(`[X] codex-auth: ${msg}\n`);
|
||||||
}
|
|
||||||
process.stderr.write(`[X] codex-auth: profile resolution failed (${msg})\n`);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
process.stderr.write(`[X] codex-auth: profile resolution failed (${msg})\n`);
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── delegate to CCS ─────────────────────────────────────────────────────
|
// ── delegate to CCS ─────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -146,6 +146,28 @@ describe('codex-runtime router — non-auth profile resolution', () => {
|
|||||||
expect(process.env.CODEX_HOME).toBe(profileDir);
|
expect(process.env.CODEX_HOME).toBe(profileDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves a first positional ccsx arg from the Codex auth registry before CCS profiles', async () => {
|
||||||
|
const profileDir = makeProfileDir('ck');
|
||||||
|
writeRegistry({
|
||||||
|
version: '1.0',
|
||||||
|
default: null,
|
||||||
|
profiles: { ck: { type: 'codex', created: '2026-01-01T00:00:00.000Z', last_used: null } },
|
||||||
|
});
|
||||||
|
|
||||||
|
require.cache[ccsPath] = { exports: {} } as NodeJS.Module;
|
||||||
|
flushRouterCache();
|
||||||
|
require.cache[ccsPath] = { exports: {} } as NodeJS.Module;
|
||||||
|
|
||||||
|
const argv = ['node', 'codex-runtime', 'ck', 'fix failing tests'];
|
||||||
|
const { main } = require(routerPath) as { main: (argv: string[]) => Promise<number> };
|
||||||
|
const code = await main(argv);
|
||||||
|
|
||||||
|
expect(code).toBe(-1);
|
||||||
|
expect(process.env.CCS_CODEX_PROFILE).toBe('ck');
|
||||||
|
expect(process.env.CODEX_HOME).toBe(profileDir);
|
||||||
|
expect(argv).toEqual(['node', 'codex-runtime', 'default', 'fix failing tests']);
|
||||||
|
});
|
||||||
|
|
||||||
it('leaves CODEX_HOME unset when no registry exists and no env profile set', async () => {
|
it('leaves CODEX_HOME unset when no registry exists and no env profile set', async () => {
|
||||||
// No registry file, no CCS_CODEX_PROFILE
|
// No registry file, no CCS_CODEX_PROFILE
|
||||||
require.cache[ccsPath] = { exports: {} } as NodeJS.Module;
|
require.cache[ccsPath] = { exports: {} } as NodeJS.Module;
|
||||||
|
|||||||
@@ -1095,6 +1095,62 @@ supports_websockets = false
|
|||||||
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([]);
|
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('launches a ccsx auth profile even when a Claude account has the same name', () => {
|
||||||
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
const codexProfileDir = path.join(ccsDir, 'codex-instances', 'ck');
|
||||||
|
fs.mkdirSync(codexProfileDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'config.yaml'),
|
||||||
|
[
|
||||||
|
'version: 2',
|
||||||
|
'accounts:',
|
||||||
|
' ck:',
|
||||||
|
' created: "2026-01-01"',
|
||||||
|
' last_used: "2026-01-01"',
|
||||||
|
].join('\n')
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'codex-profiles.yaml'),
|
||||||
|
[
|
||||||
|
'version: "1.0"',
|
||||||
|
'default: null',
|
||||||
|
'profiles:',
|
||||||
|
' ck:',
|
||||||
|
' type: codex',
|
||||||
|
' created: "2026-01-01T00:00:00.000Z"',
|
||||||
|
' last_used: null',
|
||||||
|
].join('\n')
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = runCodexAlias(['ck', 'fix failing tests'], {
|
||||||
|
...process.env,
|
||||||
|
CI: '1',
|
||||||
|
NO_COLOR: '1',
|
||||||
|
HOME: tmpHome,
|
||||||
|
CCS_HOME: tmpHome,
|
||||||
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
|
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
||||||
|
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
expect(result.stderr).not.toContain('Codex CLI does not support Claude account-based profiles.');
|
||||||
|
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['fix failing tests']]);
|
||||||
|
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
|
||||||
|
{
|
||||||
|
CODEX_HOME: codexProfileDir,
|
||||||
|
CODEX_CI: undefined,
|
||||||
|
CODEX_MANAGED_BY_BUN: undefined,
|
||||||
|
CODEX_THREAD_ID: undefined,
|
||||||
|
ANTHROPIC_BASE_URL: undefined,
|
||||||
|
CCS_BROWSER_USER_DATA_DIR: undefined,
|
||||||
|
CCS_BROWSER_PROFILE_DIR: undefined,
|
||||||
|
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects conflicting native provider config overrides for ccsxp', () => {
|
it('rejects conflicting native provider config overrides for ccsxp', () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user