mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-03 15:11:42 +00:00
226 lines
6.3 KiB
TypeScript
226 lines
6.3 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { ConfigError } from '../errors/error-types';
|
|
import { resolveCanonicalPath, symlinkPointsTo } from '../management/shared-manager/fs-helpers';
|
|
import { createLogger } from '../services/logging';
|
|
|
|
const logger = createLogger('codex-auth:resources');
|
|
const FALLBACK_SAFE_SYMLINK_ERRORS = new Set(['EPERM', 'EACCES', 'ENOSYS']);
|
|
|
|
export function ensureSharedPluginCache(profileDir: string, sharedCodexHome: string): void {
|
|
const targetPath = path.join(sharedCodexHome, 'plugins', 'cache');
|
|
const pluginsPath = path.join(profileDir, 'plugins');
|
|
const linkPath = path.join(pluginsPath, 'cache');
|
|
|
|
ensureProfileLocalPluginsDirectory(pluginsPath);
|
|
fs.mkdirSync(targetPath, { recursive: true, mode: 0o700 });
|
|
|
|
if (resolveCanonicalPath(pluginsPath) === resolveCanonicalPath(path.dirname(targetPath))) {
|
|
throw new ConfigError(
|
|
'Refusing plugin cache repair: profile plugins directory resolves to the shared plugins directory.'
|
|
);
|
|
}
|
|
|
|
const existingStat = lstatIfExists(linkPath);
|
|
if (isExpectedCacheLink(linkPath, targetPath, existingStat)) {
|
|
return;
|
|
}
|
|
|
|
let backupPath = existingStat === null ? null : createPluginCacheBackupPath(pluginsPath);
|
|
if (backupPath !== null) {
|
|
try {
|
|
fs.renameSync(linkPath, backupPath);
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
throw err;
|
|
}
|
|
backupPath = null;
|
|
}
|
|
}
|
|
|
|
try {
|
|
fs.symlinkSync(targetPath, linkPath, 'dir');
|
|
} catch (err) {
|
|
if (isExpectedCacheLink(linkPath, targetPath)) {
|
|
removePluginCacheBackup(backupPath);
|
|
return;
|
|
}
|
|
|
|
const pathAfterFailure = lstatIfExists(linkPath);
|
|
if (backupPath !== null && pathAfterFailure === null) {
|
|
if (restorePluginCacheWithoutOverwrite(linkPath, backupPath)) {
|
|
backupPath = null;
|
|
}
|
|
}
|
|
|
|
const code = (err as NodeJS.ErrnoException | null)?.code;
|
|
if (!code || !FALLBACK_SAFE_SYMLINK_ERRORS.has(code)) {
|
|
throw err;
|
|
}
|
|
|
|
if (isExpectedCacheLink(linkPath, targetPath)) {
|
|
removePluginCacheBackup(backupPath);
|
|
return;
|
|
}
|
|
const fallbackPathStat = lstatIfExists(linkPath);
|
|
if (backupPath !== null && fallbackPathStat?.isDirectory()) {
|
|
mergeMissingResourceTree(backupPath, linkPath);
|
|
removePluginCacheBackup(backupPath);
|
|
}
|
|
|
|
ensureLocalPluginCacheDirectory(linkPath);
|
|
mergeMissingResourceTree(targetPath, linkPath);
|
|
process.stderr.write(
|
|
`[!] codex-auth: symlink unavailable; using profile-local plugin cache at ${linkPath}. ` +
|
|
`Copied missing shared entries; plugin cache updates won't propagate automatically.\n`
|
|
);
|
|
logger.warn(
|
|
'codex-auth.plugin-cache-copy-fallback',
|
|
'Copied shared plugin cache after symlink failure',
|
|
{
|
|
link: linkPath,
|
|
target: targetPath,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
removePluginCacheBackup(backupPath);
|
|
|
|
logger.stage(
|
|
'dispatch',
|
|
'codex.plugin-cache.symlink.created',
|
|
'Created shared plugin cache symlink',
|
|
{
|
|
link: linkPath,
|
|
target: targetPath,
|
|
}
|
|
);
|
|
}
|
|
|
|
function ensureProfileLocalPluginsDirectory(pluginsPath: string): void {
|
|
const pluginsStat = lstatIfExists(pluginsPath);
|
|
if (pluginsStat === null) {
|
|
fs.mkdirSync(pluginsPath, { recursive: true, mode: 0o700 });
|
|
return;
|
|
}
|
|
if (!pluginsStat.isDirectory()) {
|
|
throw new ConfigError(
|
|
'Refusing plugin cache repair: profile plugins path is not a local directory.'
|
|
);
|
|
}
|
|
}
|
|
|
|
function isExpectedCacheLink(
|
|
linkPath: string,
|
|
targetPath: string,
|
|
stat: fs.Stats | null = lstatIfExists(linkPath)
|
|
): boolean {
|
|
if (!stat?.isSymbolicLink()) {
|
|
return false;
|
|
}
|
|
return symlinkPointsTo(linkPath, targetPath);
|
|
}
|
|
|
|
function lstatIfExists(resourcePath: string): fs.Stats | null {
|
|
try {
|
|
return fs.lstatSync(resourcePath);
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
return null;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function createPluginCacheBackupPath(pluginsPath: string): string {
|
|
return path.join(
|
|
pluginsPath,
|
|
`.cache.ccs-backup-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
);
|
|
}
|
|
|
|
function removePluginCacheBackup(backupPath: string | null): void {
|
|
if (backupPath !== null) {
|
|
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function ensureLocalPluginCacheDirectory(linkPath: string): void {
|
|
let existingStat = lstatIfExists(linkPath);
|
|
if (existingStat?.isDirectory()) {
|
|
return;
|
|
}
|
|
if (existingStat !== null) {
|
|
throw new ConfigError(
|
|
'Refusing plugin cache fallback: profile cache path is not a local directory.'
|
|
);
|
|
}
|
|
|
|
try {
|
|
fs.mkdirSync(linkPath, { recursive: false, mode: 0o700 });
|
|
return;
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
existingStat = lstatIfExists(linkPath);
|
|
if (!existingStat?.isDirectory()) {
|
|
throw new ConfigError(
|
|
'Refusing plugin cache fallback: profile cache path is not a local directory.'
|
|
);
|
|
}
|
|
}
|
|
|
|
function restorePluginCacheWithoutOverwrite(linkPath: string, backupPath: string): boolean {
|
|
const backupStat = lstatIfExists(backupPath);
|
|
if (!backupStat?.isDirectory()) {
|
|
return false;
|
|
}
|
|
|
|
let linkStat = lstatIfExists(linkPath);
|
|
if (linkStat === null) {
|
|
try {
|
|
fs.mkdirSync(linkPath, { recursive: false, mode: 0o700 });
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
throw err;
|
|
}
|
|
}
|
|
linkStat = lstatIfExists(linkPath);
|
|
}
|
|
|
|
if (!linkStat?.isDirectory()) {
|
|
return false;
|
|
}
|
|
|
|
mergeMissingResourceTree(backupPath, linkPath);
|
|
removePluginCacheBackup(backupPath);
|
|
return true;
|
|
}
|
|
|
|
function mergeMissingResourceTree(sourceDir: string, targetDir: string): void {
|
|
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
const targetPath = path.join(targetDir, entry.name);
|
|
const targetStat = lstatIfExists(targetPath);
|
|
|
|
if (targetStat === null) {
|
|
fs.cpSync(sourcePath, targetPath, {
|
|
recursive: true,
|
|
force: false,
|
|
errorOnExist: false,
|
|
preserveTimestamps: true,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (entry.isDirectory() && targetStat.isDirectory()) {
|
|
mergeMissingResourceTree(sourcePath, targetPath);
|
|
}
|
|
}
|
|
}
|