agent-ecosystem/src/features/runtime-provider-management/main/infrastructure/openCodeWindowsNodeModulesJunction.ts
ComradeSwarog cc3c9f7dc7 fix(opencode): address code review feedback — extract paths from error message, fix test imports
- 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
2026-05-28 13:08:55 +03:00

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;
}
}