santifer/career-ops · error · Error
plugins.local/ already exists — `node plugins.mjs remove `…
Error message
plugins.local/${id} already exists — `node plugins.mjs remove ${id}` first What it means
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.
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
Example fix
// before node plugins.mjs install acme/career-ops-plugin-demo --sha <sha> # second run, throws // after node plugins.mjs remove demo node plugins.mjs install acme/career-ops-plugin-demo --sha <sha>
Defensive patterns
Strategy: try-catch
Validate before calling
const id = parseRepoArg(url).id; if (existsSync(path.join(root, 'plugins.local', id))) console.warn(`${id} already installed — remove it first`); Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- career-ops at is not a git checkout of its own, so git…
- could not read the release pointer
- does not contain a YAML mapping — refusing to overwrite it…
- is not valid YAML ( ) — refusing to overwrite it. Fix the…
- plugin egress must use HTTPS
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/fd9cc40883b02199.
Report an issue: GitHub.
Appendix: source
Thrown at plugin-install.mjs:84
// validateManifest wants the dir to BE the plugin dir + the basename to equal id.
const tmpNamed = path.join(path.dirname(dir), expectId);
if (dir !== tmpNamed) { try { renameSync(dir, tmpNamed); dir = tmpNamed; } catch { /* validate in place using expectId */ } }
const manifest = validateManifest(parsed, dir, expectId);
if (!manifest) return { ok: false, problems: ['manifest failed validation (see ⚠️ above)'], manifest: null, dir };
const audit = auditPlugin(dir);
if (!audit.ok) return { ok: false, problems: audit.findings.map(f => `${f.file}: ${f.issue}`), manifest, dir };
return { ok: true, problems: [], manifest, dir };
}
/**
* Install a community plugin from a github repo at a pinned SHA into
* plugins.local/<id>. Returns { id, manifest, integrity, dir } WITHOUT enabling
* it (the caller runs the consent gate). Throws on any validation failure.
*/
export function installFromRepo(root, { url, sha }) {
const { url: safeUrl, id } = parseRepoArg(url);
const dest = path.join(root, 'plugins.local', id);
if (existsSync(dest)) throw new Error(`plugins.local/${id} already exists — \`node plugins.mjs remove ${id}\` first`);
let cloned = safeClone(safeUrl, sha);
let result;
try { result = validateInstall(cloned, id); }
catch (e) { rmSync(cloned, { recursive: true, force: true }); throw e; }
if (!result.ok) {
rmSync(result.dir || cloned, { recursive: true, force: true });
throw new Error(`plugin rejected:\n - ${result.problems.join('\n - ')}`);
}
mkdirSync(path.join(root, 'plugins.local'), { recursive: true });
cpSync(result.dir, dest, { recursive: true });
rmSync(result.dir, { recursive: true, force: true });
const tree = hashPluginTree(dest);
return { id, manifest: { ...result.manifest, dir: dest }, integrity: tree.integrity, files: tree.files, repo: safeUrl, sha };
}
/**
* Clone + statically validate a registry entry WITHOUT installing it (used by
* the registry-validate CI). Executes NO plugin code — manifest is parsed, theView on GitHub (pinned to aac998c7ed)