santifer/career-ops · error · Error
plugins.local/ already exists
Error message
plugins.local/${name} already exists What it means
scaffoldNew refuses to overwrite an existing plugin: if plugins.local/<name> already exists, it throws instead of clobbering your work. This is a deliberate collision guard for the destination directory.
Solutions
- Pick a different, unique plugin name
- Delete or rename the existing plugins.local/<name> directory if you truly want to start over
- Inspect the existing plugin first — you may want to keep your modifications
Example fix
// before
await scaffoldNew(root, 'my-plugin');
// Error: plugins.local/my-plugin already exists
// after
rmSync(path.join(root, 'plugins.local', 'my-plugin'), { recursive: true, force: true }); // only if reset is intended
await scaffoldNew(root, 'my-plugin'); Defensive patterns
Strategy: validation
Validate before calling
import { existsSync } from 'node:fs';
const dest = path.join(root, 'plugins.local', name);
if (existsSync(dest)) throw new Error(`choose another name or remove ${dest} first`); Type guard
null
Try / catch
try { await scaffoldNew(root, name); } catch (e) { if (e.message.includes('already exists')) { /* prompt user: reuse, rename, or delete */ } throw e; } Prevention
- Check plugins.local/ contents before scaffolding
- Use idempotent scripts that skip scaffolding when the target exists
- Clean up failed scaffold runs promptly
When it happens
Trigger: Calling scaffoldNew(root, name) when root/plugins.local/name exists — from a previous scaffold, a manually created folder, or a previously installed plugin with the same id.
Common situations: Re-running a scaffold script that already succeeded; scaffolding a plugin you previously installed from a repo; leftover directory from a failed/cancelled run; wanting to reset a plugin to template state.
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
- plugins/_template/ not found
- apify: JD cache write failed for
- cannot read
- Could not release report reservation
- ⚠️ Could not release report reservation
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/59ee6673b3b26872.
Report an issue: GitHub.
Appendix: source
Thrown at plugin-install.mjs:125
let parsed;
try { parsed = parseRepoArg(url); } catch (e) { return [e.message]; }
if (expectId && parsed.id !== expectId) return [`repo "${url}" → id "${parsed.id}" but registry id is "${expectId}"`];
let dir;
try { dir = safeClone(parsed.url, sha); } catch (e) { return [e.message]; }
let result;
try { result = validateInstall(dir, parsed.id); }
catch (e) { rmSync(dir, { recursive: true, force: true }); return [e.message]; }
try { rmSync(result.dir || dir, { recursive: true, force: true }); } catch { /* best-effort */ }
return result.ok ? [] : result.problems;
}
/** Scaffold a new local plugin from plugins/_template/. */
export function scaffoldNew(root, name) {
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) throw new Error(`plugin name must match [a-z0-9-] (got "${name}")`);
const tpl = path.join(root, 'plugins', '_template');
if (!existsSync(tpl)) throw new Error('plugins/_template/ not found');
const dest = path.join(root, 'plugins.local', name);
if (existsSync(dest)) throw new Error(`plugins.local/${name} already exists`);
mkdirSync(path.join(root, 'plugins.local'), { recursive: true });
cpSync(tpl, dest, { recursive: true });
// Substitute {{NAME}} placeholders in shipped text files.
const sub = (p) => { if (existsSync(p)) writeFileSync(p, readFileSync(p, 'utf8').replaceAll('{{NAME}}', name), 'utf8'); };
for (const f of readdirSync(dest, { withFileTypes: true })) {
if (f.isFile()) sub(path.join(dest, f.name));
}
if (existsSync(path.join(dest, 'test'))) for (const f of readdirSync(path.join(dest, 'test'))) sub(path.join(dest, 'test', f));
return dest;
}
// ── CLI flags + help ────────────────────────────────────────────────
//
// This file is a library first: plugins.mjs owns the user-facing `new`/`add`
// commands and calls the exports above. But docs/SCRIPTS.md lists `node
// plugin-install.mjs` as a runnable command, so it is reachable directly — and
// every direct invocation printed nothing and exited 0, `--help` and a typo'd
// flag included. That is the silent-success shape lib/cli-flags.mjs exists toView on GitHub (pinned to aac998c7ed)