santifer/career-ops · error · Error

plugins/_template/ not found

Error message

plugins/_template/ not found

What it means

scaffoldNew copies plugins/_template/ as the starting point for a new local plugin. If that template directory does not exist in the repo root provided, it throws before creating anything. The template ships with the system; its absence means an incomplete checkout or a deleted/renamed directory.

Solutions

  1. Re-sync/re-clone the repo so plugins/_template/ exists (git checkout / reinstall the system files)
  2. Verify you're passing the correct repo root to scaffoldNew, not a subdirectory
  3. Recreate a minimal plugins/_template/ directory with a valid plugin manifest if intentionally customized

Example fix

// before: running from a shallow copy without the template
await scaffoldNew(process.cwd(), 'my-plugin');
// Error: plugins/_template/ not found
// after: restore the template from the repo
// git checkout -- plugins/_template/   (or re-clone)
await scaffoldNew(repoRoot, 'my-plugin');
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync } from 'node:fs';
if (!existsSync(path.join(root, 'plugins', '_template'))) throw new Error(`template missing at ${root}/plugins/_template — reinstall system files`);

Type guard

null

Try / catch

try { await scaffoldNew(root, name); } catch (e) { if (e.message === 'plugins/_template/ not found') { await restoreTemplate(root); return scaffoldNew(root, name); } throw e; }

Prevention

When it happens

Trigger: Calling scaffoldNew(root, name) where path.join(root, 'plugins', '_template') fails existsSync — repo missing the plugins/_template directory.

Common situations: Running from a wrong working directory/root; a partial clone or sparse checkout that dropped plugins/; user deleted the template; an old system version before the template was added.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/27d6b4b47b3cb2bd. Report an issue: GitHub.

Appendix: source

Thrown at plugin-install.mjs:123

 */
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;
}

// ── 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

View on GitHub (pinned to aac998c7ed)