mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-16 00:14:37 +00:00
feat(codex-auth): add profile registry + storage foundation
Adds the storage substrate for ccsx auth profile isolation. Each Codex profile gets its own CODEX_HOME dir under ~/.ccs/codex-instances/<name>/ with isolated auth.json and history.jsonl; config.toml is shared via symlink to ~/.codex/config.toml so two terminals can run two Codex accounts simultaneously without duplicating user config. - CodexProfileRegistry (YAML, atomic write tmp.<pid>.<rand> + rename, orphan cleanup, full CRUD + default pointer) - decode-id-token: pure base64 JWT decoder for OpenAI id_token, reads nested https://api.openai.com/auth claims (chatgpt_plan_type, chatgpt_account_id) and dual-path email - ensureSharedConfigSymlink: self-healing, idempotent, overwrites stale entries with stderr warning - 45 unit tests, all green Foundation only — no CLI, no runtime injection, no dashboard. Subsequent commits wire those in.
This commit is contained in:
1 parent
d2f7d3f407
commit
358b703d98
12 files changed
+1006
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { createLogger } from '../services/logging';
|
||||
import { getSharedCodexConfigPath } from './codex-profile-paths';
|
||||
|
||||
const logger = createLogger('codex-auth:symlink');
|
||||
|
||||
/**
|
||||
* Ensure <profileDir>/config.toml is a symlink pointing to the shared
|
||||
* ~/.codex/config.toml. Self-healing: recreates stale or missing symlinks.
|
||||
*
|
||||
* @param profileDir - The per-profile directory (will be created if missing).
|
||||
* @param sharedConfigPath - Override for the shared target path (used in tests
|
||||
* to avoid touching real ~/.codex/config.toml). Defaults to getSharedCodexConfigPath().
|
||||
*/
|
||||
export function ensureSharedConfigSymlink(profileDir: string, sharedConfigPath?: string): void {
|
||||
const targetPath = sharedConfigPath ?? getSharedCodexConfigPath();
|
||||
const linkPath = path.join(profileDir, 'config.toml');
|
||||
|
||||
// Ensure profile directory exists
|
||||
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
|
||||
|
||||
// Ensure shared config target parent directory exists
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
|
||||
|
||||
// Create empty shared config if it doesn't exist — Codex will populate on first run
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
fs.writeFileSync(targetPath, '', { mode: 0o600 });
|
||||
logger.stage('dispatch', 'codex.shared-config.created', 'Created empty shared config.toml', {
|
||||
path: targetPath,
|
||||
});
|
||||
}
|
||||
|
||||
// Inspect whatever currently exists at the link path
|
||||
let existingStat: fs.Stats | null = null;
|
||||
try {
|
||||
existingStat = fs.lstatSync(linkPath);
|
||||
} catch {
|
||||
// ENOENT — nothing there yet, proceed to create
|
||||
}
|
||||
|
||||
if (existingStat !== null) {
|
||||
if (existingStat.isSymbolicLink()) {
|
||||
const currentTarget = fs.readlinkSync(linkPath);
|
||||
if (currentTarget === targetPath) {
|
||||
// Already correct — idempotent return
|
||||
logger.stage('dispatch', 'codex.symlink.ok', 'Shared config symlink already correct', {
|
||||
link: linkPath,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Stale symlink — remove and re-create
|
||||
fs.unlinkSync(linkPath);
|
||||
logger.stage('dispatch', 'codex.symlink.repaired', 'Replaced stale symlink', {
|
||||
link: linkPath,
|
||||
was: currentTarget,
|
||||
now: targetPath,
|
||||
});
|
||||
} else {
|
||||
// Regular file or other non-symlink entry — overwrite with warning
|
||||
process.stderr.write(
|
||||
`[!] codex-auth: overwriting regular file at ${linkPath} with symlink to shared config.toml\n`
|
||||
);
|
||||
fs.unlinkSync(linkPath);
|
||||
}
|
||||
}
|
||||
|
||||
fs.symlinkSync(targetPath, linkPath);
|
||||
logger.stage('dispatch', 'codex.symlink.created', 'Created shared config symlink', {
|
||||
link: linkPath,
|
||||
target: targetPath,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user