santifer/career-ops · error · Error
plugins.local/${name} already exists
Error message
plugins.local/${name} already exists What it means
Thrown by scaffoldNew() when the destination plugins.local/<name> already exists. Mirrors the install path's guard: scaffolding over an existing plugin would clobber user edits silently, so it refuses and asks for an explicit remove first.
Source
Thrown at plugin-install.mjs:123
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;
}
View on GitHub (pinned to 9b17a8ac97)
Solutions
- Run `node plugins.mjs remove <name>` to delete the existing dir, then scaffold again.
- Choose a different name if you want to keep the existing one.
- Manually rm -rf plugins.local/<name> if it is a known leftover.
- Confirm the name is not already in use via `node plugins.mjs list`.
Example fix
// before: scaffold over existing cpSync(tpl, dest); // throws: already exists // after: remove then scaffold // `node plugins.mjs remove my-plugin` // `node plugins.mjs scaffold my-plugin`
Defensive patterns
Strategy: validation
Validate before calling
import { existsSync } from 'fs';
import path from 'path';
function scaffoldDestFree(root, name) {
return !existsSync(path.join(root, 'plugins.local', name));
}
if (!scaffoldDestFree(root, name)) {
// remove existing plugin or choose a new name
} Type guard
null
Try / catch
try {
scaffoldNew(root, name);
} catch (e) {
if (/already exists/.test(e.message)) {
// `node plugins.mjs remove <name>` then retry
} else throw e;
} Prevention
- Check `node plugins.mjs list` to avoid name collisions before scaffolding.
- Remove an existing plugin before re-scaffolding the same name.
- Pick unique slugs for each new plugin.
When it happens
Trigger: Scaffolding a name that was already scaffolded/installed; a prior scaffold left a directory; name collision with an installed community plugin of the same id.
Common situations: Re-running scaffold during iteration; scaffolding a name already taken by an installed plugin; a previous interrupted scaffold left a partial dir.
Related errors
- plugins.local/${id} already exists — `node plugins.mjs remov
- plugin name must match [a-z0-9-] (got "${name}")
- plugins/_template/ not found
- payload.cv requires a non-empty dedupKey (used for dedup/ide
- LOCK_TIMEOUT
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/59ee6673b3b26872.
Report an issue: GitHub.