santifer/career-ops · error · Error
plugin name must match [a-z0-9-] (got "${name}")
Error message
plugin name must match [a-z0-9-] (got "${name}") What it means
Thrown by scaffoldNew() in plugin-install.mjs when the requested plugin name does not match ^[a-z0-9][a-z0-9-]*$ (lowercase alphanumeric and hyphens, starting alphanumerically). Names become directory names and id tokens, so they must be filesystem- and slug-safe.
Source
Thrown at plugin-install.mjs:119
* audit is static. Returns problems (empty = clean).
* @returns {string[]}
*/
export function auditRegistryEntry(url, sha, expectId) {
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
- Use only lowercase letters, digits, and hyphens; start with a letter or digit.
- Pick a concise slug like 'greenhouse-export' or 'notion-sync'.
- Avoid underscores, spaces, and uppercase.
- Re-run `node plugins.mjs scaffold <slug>`.
Example fix
// before scaffoldNew(root, 'My_Cool Plugin'); // after scaffoldNew(root, 'my-cool-plugin');
Defensive patterns
Strategy: validation
Validate before calling
const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
function isValidPluginName(name) {
return NAME_RE.test(name || '');
} Type guard
/** Narrows a string to a valid plugin slug (lowercase alnum + hyphens). */
function isValidPluginName(name) {
return typeof name === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(name);
} Prevention
- Always use lowercase slugs (letters, digits, hyphens) for plugin names.
- Validate the name at the CLI boundary before calling scaffoldNew.
- Avoid underscores, spaces, and uppercase in ids.
When it happens
Trigger: Passing a name with underscores, spaces, uppercase letters, a leading hyphen, or special characters; passing an empty string; using CamelCase or a name starting with a digit-only segment like '-x'.
Common situations: User typed My Plugin, my_plugin, or -plugin; copied a name with a leading hyphen; used a reserved-looking or mixed-case id.
Related errors
- repo must be named "career-ops-plugin-<name>" (got "${repoNa
- plugin rejected: - ${result.problems.join('\n - ')}
- plugins/_template/ not found
- plugins.local/${name} already exists
- payload must include at least one of: cv, articleDigest
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/914b347149ddf4ef.
Report an issue: GitHub.