{"record":{"id":"3a6b0e330374d258","repo":"jackwener/OpenCLI","slug":"expected-monorepo-plugin-link-at-linkpath-to-be","errorCode":null,"errorMessage":"Expected monorepo plugin link at ${linkPath} to be a symlink","messagePattern":"Expected monorepo plugin link at (.+?) to be a symlink","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/plugin.ts","lineNumber":382,"sourceCode":"        try { fsOps.rmSync(backupDest, { recursive: true, force: true }); } catch {}\n      }\n    },\n    rollback() {\n      if (settled) return;\n      settled = true;\n      try { fsOps.rmSync(dest, { recursive: true, force: true }); } catch {}\n      if (backupDest) {\n        try { fsOps.renameSync(backupDest, dest); } catch {}\n      }\n      try { fsOps.rmSync(tempDest, { recursive: true, force: true }); } catch {}\n    },\n  };\n}\n\nfunction beginReplaceSymlink(target: string, linkPath: string): TransactionHandle {\n  const linkExists = pathExistsSync(linkPath);\n  if (linkExists && !isSymlinkSync(linkPath)) {\n    throw new Error(`Expected monorepo plugin link at ${linkPath} to be a symlink`);\n  }\n\n  fs.mkdirSync(path.dirname(linkPath), { recursive: true });\n\n  const tempLink = createSiblingTempPath(linkPath, 'tmp');\n  const backupLink = linkExists ? createSiblingTempPath(linkPath, 'bak') : null;\n  const linkType = isWindows ? 'junction' : 'dir';\n  let settled = false;\n\n  try {\n    fs.symlinkSync(target, tempLink, linkType);\n    if (backupLink) {\n      fs.renameSync(linkPath, backupLink);\n    }\n    fs.renameSync(tempLink, linkPath);\n  } catch (err) {\n    removePathSync(tempLink);\n    if (backupLink && !pathExistsSync(linkPath)) {","sourceCodeStart":364,"sourceCodeEnd":400,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/src/plugin.ts#L364-L400","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nopencli plugin install github:user/monorepo/shared-name // fails: real dir exists\n// after\nopencli plugin uninstall shared-name\nopencli plugin install github:user/monorepo/shared-name","handlingStrategy":"validation","validationCode":"import * as fs from 'node:fs';\nconst linkPath = `${pluginsDir}/${name}`;\nif (fs.existsSync(linkPath) && !fs.lstatSync(linkPath).isSymbolicLink()) {\n  fs.rmSync(linkPath, { recursive: true }); // or uninstall the plugin first\n}","typeGuard":"function isSymlink(p) {\n  try { return fs.lstatSync(p).isSymbolicLink(); } catch { return false; }\n}","tryCatchPattern":"try {\n  installPlugin('github:user/monorepo');\n} catch (err) {\n  if (err.message.startsWith('Expected monorepo plugin link')) {\n    uninstallPlugin(conflictingName); // then retry install\n  } else throw err;\n}","preventionTips":["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."],"tags":["filesystem","symlink","name-collision","monorepo"],"backgroundTag":"symlink-path-conflict","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}