jackwener/OpenCLI · error
Expected monorepo plugin link at ${linkPath} to be a symlink
Error message
Expected monorepo plugin link at ${linkPath} to be a symlink What it means
beginReplaceSymlink atomically replaces the symlink in the plugins dir that points at a monorepo sub-plugin. It throws a plain Error if the path exists but is NOT a symlink (a real file or directory sits where the link should be), because the transactional swap would destroy real data.
Source
Thrown at src/plugin.ts:382
try { fsOps.rmSync(backupDest, { recursive: true, force: true }); } catch {}
}
},
rollback() {
if (settled) return;
settled = true;
try { fsOps.rmSync(dest, { recursive: true, force: true }); } catch {}
if (backupDest) {
try { fsOps.renameSync(backupDest, dest); } catch {}
}
try { fsOps.rmSync(tempDest, { recursive: true, force: true }); } catch {}
},
};
}
function beginReplaceSymlink(target: string, linkPath: string): TransactionHandle {
const linkExists = pathExistsSync(linkPath);
if (linkExists && !isSymlinkSync(linkPath)) {
throw new Error(`Expected monorepo plugin link at ${linkPath} to be a symlink`);
}
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
const tempLink = createSiblingTempPath(linkPath, 'tmp');
const backupLink = linkExists ? createSiblingTempPath(linkPath, 'bak') : null;
const linkType = isWindows ? 'junction' : 'dir';
let settled = false;
try {
fs.symlinkSync(target, tempLink, linkType);
if (backupLink) {
fs.renameSync(linkPath, backupLink);
}
fs.renameSync(tempLink, linkPath);
} catch (err) {
removePathSync(tempLink);
if (backupLink && !pathExistsSync(linkPath)) {View on GitHub (pinned to 49907e53dc)
Solutions
- Uninstall the conflicting plugin: `opencli plugin uninstall <name>` (this removes the directory).
- If it is a leftover directory, remove it manually: `rm -rf ~/.opencli/plugins/<name>`.
- Rename the monorepo sub-plugin in its manifest to avoid the collision.
- Re-run install/update afterward.
Example fix
// before opencli plugin install github:user/monorepo/shared-name // fails: real dir exists // after opencli plugin uninstall shared-name opencli plugin install github:user/monorepo/shared-name
Defensive patterns
Strategy: validation
Validate before calling
import * as fs from 'node:fs';
const linkPath = `${pluginsDir}/${name}`;
if (fs.existsSync(linkPath) && !fs.lstatSync(linkPath).isSymbolicLink()) {
fs.rmSync(linkPath, { recursive: true }); // or uninstall the plugin first
} Type guard
function isSymlink(p) {
try { return fs.lstatSync(p).isSymbolicLink(); } catch { return false; }
} Try / catch
try {
installPlugin('github:user/monorepo');
} catch (err) {
if (err.message.startsWith('Expected monorepo plugin link')) {
uninstallPlugin(conflictingName); // then retry install
} else throw err;
} Prevention
- Run `opencli plugin uninstall <name>` before reinstalling the same name from a monorepo.
- Never manually copy directories into ~/.opencli/plugins — install via opencli.
- Avoid naming monorepo sub-plugins the same as existing standalone plugins.
When it happens
Trigger: publishMonorepoPlugins (via installMonorepo or updatePlugin of monorepo sub-plugins) when ~/.opencli/plugins/<name> already exists as a regular directory or file — typically a previously-installed standalone plugin with the same name, or a leftover from a crashed install.
Common situations: Name collision between a standalone plugin and a monorepo sub-plugin; user copied a real directory into the plugins folder manually; a previous failed install left a materialized directory where a symlink belongs.
Related errors
- output path is not a safe directory: ${ancestor}
- output path must not be a symbolic link: ${resolved}
- Failed to link host opencli into plugin: ${getErrorMessage(e
- Verify command returned no metric for baseline
- File not found: ${path}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/3a6b0e330374d258.
Report an issue: GitHub.