{"record":{"id":"29e1ae09d700a079","repo":"santifer/career-ops","slug":"repo-must-be-named-career-ops-plugin-name-got","errorCode":null,"errorMessage":"repo must be named \"career-ops-plugin-<name>\" (got \"${repoName}\")","messagePattern":"repo must be named \"career-ops-plugin-<name>\" \\(got \"(.+?)\"\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugin-install.mjs","lineNumber":36,"sourceCode":"import { hashPluginTree } from './plugins/_lock.mjs';\nimport { auditPlugin } from './plugin-audit.mjs';\nimport { validateFlags } from './lib/cli-flags.mjs';\nimport { isMainModule } from './lib/is-main-module.mjs';\n\nconst GITHUB_URL_RE = /^https:\\/\\/github\\.com\\/[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+?(?:\\.git)?$/;\nconst NAME_RE = /^career-ops-plugin-([a-z0-9][a-z0-9-]*)$/;\nconst SHA_RE = /^[0-9a-f]{40}$/;\nconst MIN_FILES = ['manifest.json', 'index.mjs', 'README.md', 'LICENSE'];\n\n/** Normalize `owner/repo` | full URL into a validated github URL + the plugin id. */\nexport function parseRepoArg(arg) {\n  let url = arg;\n  if (/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$/.test(arg)) url = `https://github.com/${arg}`;\n  url = url.replace(/\\.git$/, '');\n  if (!GITHUB_URL_RE.test(url)) throw new Error(`refusing non-GitHub/unsafe repo URL: ${arg} (expected https://github.com/<owner>/<repo>)`);\n  const repoName = url.split('/').pop() || '';\n  const m = NAME_RE.exec(repoName);\n  if (!m) throw new Error(`repo must be named \"career-ops-plugin-<name>\" (got \"${repoName}\")`);\n  return { url, id: m[1] };\n}\n\n/** Clone the EXACT pinned SHA into a fresh temp dir. Returns the temp dir path. */\nexport function safeClone(url, sha) {\n  if (!SHA_RE.test(sha || '')) throw new Error(`a 40-hex commit --sha is required (got ${JSON.stringify(sha)})`);\n  const dir = mkdtempSync(path.join(tmpdir(), 'co-plugin-'));\n  const git = (...args) => execFileSync('git', ['-c', 'protocol.ext.allow=never', '-c', 'protocol.file.allow=never', ...args], { stdio: ['ignore', 'ignore', 'pipe'], timeout: 120_000 });\n  try {\n    git('-C', dir, 'init', '-q');\n    git('-C', dir, 'remote', 'add', 'origin', '--', url);\n    git('-C', dir, 'fetch', '--depth', '1', '--no-tags', '-q', 'origin', sha);\n    git('-C', dir, 'checkout', '-q', 'FETCH_HEAD');\n    rmSync(path.join(dir, '.git'), { recursive: true, force: true }); // drop VCS metadata (and any hooks)\n    return dir;\n  } catch (err) {\n    rmSync(dir, { recursive: true, force: true });\n    throw new Error(`clone of ${url}@${sha.slice(0, 10)} failed — ${err.stderr ? String(err.stderr).slice(0, 200) : err.message}`);","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugin-install.mjs#L18-L54","documentation":"After URL validation, parseRepoArg derives the plugin id from the repo name, which must match the naming convention career-ops-plugin-<name> (NAME_RE). This convention guarantees the extracted id is safe and namespaced; repos outside the convention are refused.","triggerScenarios":"parseRepoArg called with a valid GitHub URL whose last path segment is not of the form career-ops-plugin-<name> — e.g. github.com/acme/career-ops (missing plugin-), github.com/acme/ops-plugin-demo (wrong prefix), or a trailing segment picked up from a longer URL.","commonSituations":"Forking/renaming the plugin repo to a custom name; pointing at a fork named differently; passing a URL with a trailing path segment (/tree/main) that changes the parsed repo name; typo in the prefix.","solutions":["Rename the repo to career-ops-plugin-<name> or use a fork that follows the convention","Verify the URL ends exactly at the repo name (no /tree/... suffix)","Check the prefix spelling: it must start with career-ops-plugin-","If the plugin is third-party and can't be renamed, ask the maintainer to adopt the naming convention"],"exampleFix":"// before\nparseRepoArg('https://github.com/acme/ops-plugin-demo');\n// after\nparseRepoArg('https://github.com/acme/career-ops-plugin-demo'); // id = 'demo'","handlingStrategy":"validation","validationCode":"const validName = (repo) => /^career-ops-plugin-[A-Za-z0-9_-]+$/.test(repo); if (!validName(url.split('/').pop().replace(/\\.git$/, ''))) throw new Error('repo must be named career-ops-plugin-<name>');","typeGuard":"const isPluginRepoName = (n) => typeof n === 'string' && n.startsWith('career-ops-plugin-');","tryCatchPattern":"try { parseRepoArg(arg); } catch (e) { if (e.message.includes('career-ops-plugin-<name>')) { console.error('Rename the repo to career-ops-plugin-<name> or pick a convention-following fork'); } else throw e; }","preventionTips":["Verify the repo name matches the convention before installing","Ensure the URL ends at the repo name (no path suffix)","When forking, keep the career-ops-plugin- prefix","Check the extracted id by reading the error's got \"...\" segment"],"tags":["validation","naming","plugins"],"backgroundTag":"invalid-identifier-format","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}