santifer/career-ops · error · Error
plugins.local/${id} already exists — `node plugins.mjs remov
Error message
plugins.local/${id} already exists — `node plugins.mjs remove ${id}` first What it means
Thrown by installFromRepo() when the destination plugins.local/<id> already exists at install time. Re-installing over an existing plugin would silently change its code/version without going through the explicit remove → reinstall flow, so the operation fails fast and tells the user how to proceed.
Source
Thrown at plugin-install.mjs:82
// 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 9b17a8ac97)
Solutions
- Run `node plugins.mjs remove <id>` first, then reinstall.
- If the existing dir is a leftover from a failed install, remove it manually or via the remove command.
- Use a different plugin name/id if you want both copies.
- Confirm you are not reinstalling the same plugin unintentionally.
Example fix
// before: re-run install over existing cpSync(result.dir, dest); // throws: already exists // after: remove then install // `node plugins.mjs remove my-plugin` // `node plugins.mjs install <repo> --sha <sha>`
Defensive patterns
Strategy: validation
Validate before calling
import { existsSync } from 'fs';
import path from 'path';
function pluginInstalled(root, id) {
return existsSync(path.join(root, 'plugins.local', id));
}
if (pluginInstalled(root, id)) {
// prompt: remove first, or pick a different id
} Type guard
null
Try / catch
try {
installFromRepo(root, { url, sha });
} catch (e) {
if (/already exists/.test(e.message)) {
// run `node plugins.mjs remove <id>` then retry
} else throw e;
} Prevention
- Check `node plugins.mjs list` before installing to detect id collisions.
- Always remove before reinstalling to keep state explicit.
- Clean up partial installs left by interrupted runs.
When it happens
Trigger: Running install twice without removing; a plugin with the same id was scaffolded locally earlier; a previous failed install left a partial plugins.local/<id> directory.
Common situations: Iterating on plugin install during development; reinstalling to bump a SHA; a prior install was interrupted after cpSync created the dest dir.
Related errors
- plugins.local/${name} already exists
- payload.cv requires a non-empty dedupKey (used for dedup/ide
- LOCK_TIMEOUT
- ROW_NOT_FOUND
- Cannot read benchmarks at ${path}: ${err.message}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/fd9cc40883b02199.
Report an issue: GitHub.