- Extract symlink source/target paths directly from the error message instead of reconstructing them from process.env (Codex P2 review) - Add extractSymlinkSourcePath and extractSymlinkTargetPath functions - Update ensureOpenCodeProfileNodeModulesJunction to accept optional errorMessage parameter and use extracted paths from it - Fix unused imports in test (remove 'os', replace 'beforeEach' with 'afterEach' per CodeRabbit review) - Widen fs.statSync mock signatures to use Parameters<typeof fs.statSync> per CodeRabbit review - Add tests for new extraction functions - Pass errorMessage to ensureOpenCodeProfileNodeModulesJunction calls in CLI client tests
128 lines
No EOL
3.2 KiB
TypeScript
128 lines
No EOL
3.2 KiB
TypeScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const OPENCODE_SHARED_CACHE_NODE_MODULES_RELATIVE = path.join(
|
|
'Cache',
|
|
'opencode',
|
|
'shared-cache',
|
|
'config-node_modules'
|
|
);
|
|
const OPENCODE_PROFILES_BASE_RELATIVE = path.join(
|
|
'Data',
|
|
'opencode',
|
|
'profiles'
|
|
);
|
|
|
|
function getLocalAppDataPath(): string {
|
|
return process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
}
|
|
|
|
function getBaseDir(): string {
|
|
return path.join(getLocalAppDataPath(), 'claude-multimodel-nodejs');
|
|
}
|
|
|
|
export function getSharedCacheNodeModulesPath(): string {
|
|
return path.join(getBaseDir(), OPENCODE_SHARED_CACHE_NODE_MODULES_RELATIVE);
|
|
}
|
|
|
|
export function getProfileNodeModulesPath(profileId: string): string {
|
|
return path.join(
|
|
getBaseDir(),
|
|
OPENCODE_PROFILES_BASE_RELATIVE,
|
|
profileId,
|
|
'config',
|
|
'opencode',
|
|
'node_modules'
|
|
);
|
|
}
|
|
|
|
export function isOpenCodeNodeModulesSymlinkError(message: string): boolean {
|
|
const normalized = message.toLowerCase();
|
|
return (
|
|
(normalized.includes('eperm') || normalized.includes('eacces')) &&
|
|
normalized.includes('symlink') &&
|
|
normalized.includes('opencode') &&
|
|
normalized.includes('node_modules')
|
|
);
|
|
}
|
|
|
|
export function extractProfileIdFromSymlinkError(message: string): string | null {
|
|
const profilePathPattern =
|
|
/profiles[\\/]([0-9a-f]+)[\\/]config[\\/]opencode[\\/]node_modules/i;
|
|
const match = profilePathPattern.exec(message);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
const SYMLINK_SOURCE_PATTERN = /symlink\s+'([^']+)'/i;
|
|
const SYMLINK_TARGET_PATTERN = /->\s+'([^']+)'/i;
|
|
|
|
export function extractSymlinkSourcePath(message: string): string | null {
|
|
const match = SYMLINK_SOURCE_PATTERN.exec(message);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
export function extractSymlinkTargetPath(message: string): string | null {
|
|
const match = SYMLINK_TARGET_PATTERN.exec(message);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
export function ensureOpenCodeProfileNodeModulesJunction(
|
|
profileId: string,
|
|
errorMessage?: string
|
|
): boolean {
|
|
if (process.platform !== 'win32') {
|
|
return false;
|
|
}
|
|
|
|
let source: string;
|
|
let target: string;
|
|
|
|
if (errorMessage) {
|
|
const extractedSource = extractSymlinkSourcePath(errorMessage);
|
|
const extractedTarget = extractSymlinkTargetPath(errorMessage);
|
|
|
|
if (extractedTarget) {
|
|
target = extractedTarget;
|
|
source = extractedSource ?? getSharedCacheNodeModulesPath();
|
|
} else {
|
|
target = getProfileNodeModulesPath(profileId);
|
|
source = getSharedCacheNodeModulesPath();
|
|
}
|
|
} else {
|
|
target = getProfileNodeModulesPath(profileId);
|
|
source = getSharedCacheNodeModulesPath();
|
|
}
|
|
|
|
try {
|
|
const existingStat = fs.statSync(target, { throwIfNoEntry: false });
|
|
if (existingStat !== undefined) {
|
|
return true;
|
|
}
|
|
} catch {
|
|
// Target does not exist, proceed to create junction.
|
|
}
|
|
|
|
try {
|
|
const sourceStat = fs.statSync(source, { throwIfNoEntry: false });
|
|
if (sourceStat === undefined) {
|
|
return false;
|
|
}
|
|
} catch {
|
|
return false;
|
|
}
|
|
|
|
const parentDir = path.dirname(target);
|
|
try {
|
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
} catch {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
fs.symlinkSync(source, target, 'junction');
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
} |