{"record":{"id":"fd9cc40883b02199","repo":"santifer/career-ops","slug":"plugins-local-id-already-exists-node-plugins","errorCode":null,"errorMessage":"plugins.local/${id} already exists — `node plugins.mjs remove ${id}` first","messagePattern":"plugins\\.local/(.+?) already exists — `node plugins\\.mjs remove (.+?)` first","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugin-install.mjs","lineNumber":84,"sourceCode":"  // validateManifest wants the dir to BE the plugin dir + the basename to equal id.\n  const tmpNamed = path.join(path.dirname(dir), expectId);\n  if (dir !== tmpNamed) { try { renameSync(dir, tmpNamed); dir = tmpNamed; } catch { /* validate in place using expectId */ } }\n  const manifest = validateManifest(parsed, dir, expectId);\n  if (!manifest) return { ok: false, problems: ['manifest failed validation (see ⚠️ above)'], manifest: null, dir };\n  const audit = auditPlugin(dir);\n  if (!audit.ok) return { ok: false, problems: audit.findings.map(f => `${f.file}: ${f.issue}`), manifest, dir };\n  return { ok: true, problems: [], manifest, dir };\n}\n\n/**\n * Install a community plugin from a github repo at a pinned SHA into\n * plugins.local/<id>. Returns { id, manifest, integrity, dir } WITHOUT enabling\n * it (the caller runs the consent gate). Throws on any validation failure.\n */\nexport function installFromRepo(root, { url, sha }) {\n  const { url: safeUrl, id } = parseRepoArg(url);\n  const dest = path.join(root, 'plugins.local', id);\n  if (existsSync(dest)) throw new Error(`plugins.local/${id} already exists — \\`node plugins.mjs remove ${id}\\` first`);\n  let cloned = safeClone(safeUrl, sha);\n  let result;\n  try { result = validateInstall(cloned, id); }\n  catch (e) { rmSync(cloned, { recursive: true, force: true }); throw e; }\n  if (!result.ok) {\n    rmSync(result.dir || cloned, { recursive: true, force: true });\n    throw new Error(`plugin rejected:\\n  - ${result.problems.join('\\n  - ')}`);\n  }\n  mkdirSync(path.join(root, 'plugins.local'), { recursive: true });\n  cpSync(result.dir, dest, { recursive: true });\n  rmSync(result.dir, { recursive: true, force: true });\n  const tree = hashPluginTree(dest);\n  return { id, manifest: { ...result.manifest, dir: dest }, integrity: tree.integrity, files: tree.files, repo: safeUrl, sha };\n}\n\n/**\n * Clone + statically validate a registry entry WITHOUT installing it (used by\n * the registry-validate CI). Executes NO plugin code — manifest is parsed, the","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugin-install.mjs#L66-L102","documentation":"installFromRepo installs into plugins.local/<id> and refuses to clobber an existing installation. When the destination directory already exists it throws this error, directing the user to the canonical removal path (`node plugins.mjs remove <id>`) instead of overwriting files in place.","triggerScenarios":"installFromRepo(root, {url, sha}) called while plugins.local/<id> already exists — the plugin is already installed (possibly at a different pinned sha), a previous failed install left the directory behind, or the id collides with an unrelated leftover directory.","commonSituations":"Re-running an install command twice; upgrading by re-installing instead of using an update path; a previous install crashed after cloning but before consent/enabling; manually created plugins.local/<id> directory for local testing.","solutions":["Run `node plugins.mjs remove <id>` first, then re-install with the new sha","If the directory is a leftover from a failed install and remove is unavailable, delete plugins.local/<id> manually after checking it holds nothing valuable","Check whether the plugin is already installed at the version you want — no reinstall needed","If testing locally, use a different plugin name so the id does not collide"],"exampleFix":"// before\nnode plugins.mjs install acme/career-ops-plugin-demo --sha <sha>  # second run, throws\n// after\nnode plugins.mjs remove demo\nnode plugins.mjs install acme/career-ops-plugin-demo --sha <sha>","handlingStrategy":"try-catch","validationCode":"const id = parseRepoArg(url).id; if (existsSync(path.join(root, 'plugins.local', id))) console.warn(`${id} already installed — remove it first`);","typeGuard":null,"tryCatchPattern":"try { installFromRepo(root, { url, sha }); } catch (e) { if (e.message.includes('already exists')) { execFileSync('node', ['plugins.mjs', 'remove', id], { stdio: 'inherit' }); installFromRepo(root, { url, sha }); } else throw e; }","preventionTips":["Check plugins.local/ for the id before installing","Use the remove command for upgrades instead of re-installing blindly","Clean up leftovers from failed installs before retrying","Script the remove-then-install sequence for automated upgrades"],"tags":["plugins","file-already-exists","install"],"backgroundTag":"file-already-exists","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}