jackwener/OpenCLI · warning
Failed to link host opencli into plugin: ${getErrorMessage(e
Error message
Failed to link host opencli into plugin: ${getErrorMessage(err)} What it means
To let plugins resolve the host CLI, the host root is symlinked into the plugin directory (junction on Windows, dir symlink elsewhere). If fs.symlinkSync throws — existing target, permissions, unsupported filesystem — this warning is logged and linking is abandoned without failing the overall operation.
Source
Thrown at src/plugin.ts:1425
const hostRoot = resolveHostOpencliRoot();
const targetLink = path.join(pluginDir, 'node_modules', '@jackwener', 'opencli');
// Remove existing (npm-installed copy or stale symlink)
if (fs.existsSync(targetLink)) {
fs.rmSync(targetLink, { recursive: true, force: true });
}
// Ensure parent directory exists
fs.mkdirSync(path.dirname(targetLink), { recursive: true });
// Use 'junction' on Windows (doesn't require admin privileges),
// 'dir' symlink on other platforms.
const linkType = isWindows ? 'junction' : 'dir';
fs.symlinkSync(hostRoot, targetLink, linkType);
log.debug(`Linked host opencli into plugin: ${targetLink} → ${hostRoot}`);
} catch (err) {
log.warn(`Failed to link host opencli into plugin: ${getErrorMessage(err)}`);
}
}
/**
* Resolve the path to the esbuild CLI executable with fallback strategies.
*/
export function resolveEsbuildBin(): string | null {
const hostRoot = resolveHostOpencliRoot();
// Strategy 1 (Windows): prefer the .cmd wrapper which is executable via shell
if (isWindows) {
const cmdPath = path.join(hostRoot, 'node_modules', '.bin', 'esbuild.cmd');
if (fs.existsSync(cmdPath)) {
return cmdPath;
}
}
// Strategy 2: resolve esbuild binary via import.meta.resolveView on GitHub (pinned to 49907e53dc)
Solutions
- Delete the existing link first if EEXIST: fs.rmSync(targetLink, {force:true, recursive:true}) then retry.
- On Windows, keep the 'junction' type (as the code does) and/or enable Developer Mode for symlink support.
- Move the plugin directory onto a local filesystem that supports symlinks (not FAT32/network shares).
- Run with write permission on the plugin directory (chown/chmod or elevated shell).
Example fix
// before
fs.symlinkSync(hostRoot, targetLink, linkType);
// after
if (fs.existsSync(targetLink)) fs.rmSync(targetLink, { force: true, recursive: true });
fs.symlinkSync(hostRoot, targetLink, linkType); Defensive patterns
Strategy: try-catch
Validate before calling
if (fs.existsSync(targetLink)) fs.rmSync(targetLink, { force: true, recursive: true });
fs.accessSync(path.dirname(targetLink), fs.constants.W_OK); // writable parent? Type guard
function canSymlink(parentDir: string): boolean {
try { fs.accessSync(parentDir, fs.constants.W_OK); return true; } catch { return false; }
} Try / catch
try {
fs.symlinkSync(hostRoot, targetLink, isWindows ? 'junction' : 'dir');
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === 'EEXIST') fs.rmSync(targetLink, { force: true }) || fs.symlinkSync(hostRoot, targetLink, 'junction');
else console.warn(`link skipped (${code}); plugin may not resolve host CLI`);
} Prevention
- Remove stale links before reinstalling.
- Keep plugin directories on local symlink-capable filesystems.
- On Windows, use junctions and/or enable Developer Mode.
- Ensure the process has write access to the plugin directory.
When it happens
Trigger: fs.symlinkSync(hostRoot, targetLink, linkType) throws: targetLink already exists (EEXIST), the plugin directory is read-only (EACCES), the filesystem doesn't support symlinks (e.g. FAT/exFAT, some network mounts), or on Windows without the junction type when privileges are missing (EPERM).
Common situations: Re-running install without cleaning up a previous link; plugins living on a synced/network drive without symlink support; Windows developer-mode disabled blocking symlink creation; CI containers running as non-root with read-only plugin dirs.
Related errors
- output path is not a safe directory: ${ancestor}
- output path must not be a symbolic link: ${resolved}
- Directory "${targetDir}" already exists and is not empty.
- Expected monorepo plugin link at ${linkPath} to be a symlink
- Local plugin path is not a directory: ${localPath}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/8af4b21db1bb9d09.
Report an issue: GitHub.