mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
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');
|
|
|
|
export interface EnsureSharedConfigSymlinkOptions {
|
|
overwriteRegularFile?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Ensure <profileDir>/config.toml points at the shared ~/.codex/config.toml.
|
|
* Self-healing: recreates stale or missing symlinks. If symlink creation is
|
|
* unavailable, copies the shared config so the profile still has settings.
|
|
*
|
|
* @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,
|
|
options: EnsureSharedConfigSymlinkOptions = {}
|
|
): 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 if (
|
|
options.overwriteRegularFile ||
|
|
isRegularConfigCopyUnmodified(linkPath, targetPath)
|
|
) {
|
|
// Explicit repair or unchanged fallback copy — replace with a shared symlink when possible.
|
|
process.stderr.write(
|
|
`[!] codex-auth: overwriting regular file at ${linkPath} with symlink to shared config.toml\n`
|
|
);
|
|
fs.unlinkSync(linkPath);
|
|
} else {
|
|
process.stderr.write(
|
|
`[!] codex-auth: preserving existing regular config.toml at ${linkPath}; ` +
|
|
`use ccsx auth create <name> --force to refresh it.\n`
|
|
);
|
|
logger.warn('codex-auth.symlink-regular-file-preserved', 'Preserved regular config.toml', {
|
|
link: linkPath,
|
|
target: targetPath,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
fs.symlinkSync(targetPath, linkPath);
|
|
logger.stage('dispatch', 'codex.symlink.created', 'Created shared config symlink', {
|
|
link: linkPath,
|
|
target: targetPath,
|
|
});
|
|
} catch (err) {
|
|
handleSymlinkFailure(targetPath, linkPath, err);
|
|
}
|
|
}
|
|
|
|
function isRegularConfigCopyUnmodified(linkPath: string, targetPath: string): boolean {
|
|
try {
|
|
return fs.readFileSync(linkPath, 'utf8') === fs.readFileSync(targetPath, 'utf8');
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function handleSymlinkFailure(targetPath: string, linkPath: string, err: unknown): void {
|
|
const code = (err as NodeJS.ErrnoException | null)?.code;
|
|
if (code !== 'EPERM' && code !== 'EACCES' && code !== 'ENOSYS') {
|
|
throw err;
|
|
}
|
|
|
|
const tmpPath = path.join(
|
|
path.dirname(linkPath),
|
|
`.config.toml.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
);
|
|
|
|
try {
|
|
fs.copyFileSync(targetPath, tmpPath, fs.constants.COPYFILE_EXCL);
|
|
fs.chmodSync(tmpPath, 0o600);
|
|
fs.renameSync(tmpPath, linkPath);
|
|
} catch (copyErr) {
|
|
try {
|
|
fs.rmSync(tmpPath, { force: true });
|
|
} catch {
|
|
// best effort cleanup
|
|
}
|
|
throw copyErr;
|
|
}
|
|
|
|
process.stderr.write(
|
|
`[!] codex-auth: symlink unavailable; copied shared config.toml to ${linkPath}. ` +
|
|
`Config edits won't propagate automatically.\n`
|
|
);
|
|
logger.warn('codex-auth.symlink-copy-fallback', 'Copied shared config after symlink failure', {
|
|
link: linkPath,
|
|
target: targetPath,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
});
|
|
}
|