{"record":{"id":"d026f5fa2861674c","repo":"santifer/career-ops","slug":"plugin-id-inactive-reason-run-node-doct","errorCode":null,"errorMessage":"plugin \"${id}\" inactive: ${reason}. Run `node doctor.mjs` for setup.","messagePattern":"plugin \"(.+?)\" inactive: (.+?)\\. Run `node doctor\\.mjs` for setup\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/_engine.mjs","lineNumber":673,"sourceCode":" *     bundled zero-key provider).\n *  4. detect-EXEMPT: a merged provider's detect() is forced to null, so it fires\n *     ONLY on an explicit `provider: <id>` portals.yml entry — never via\n *     auto-detection (no surprise paid/keyed network during a plain scan).\n *  5. A known-but-inactive provider plugin registers a STUB whose fetch throws\n *     an actionable message (disabled / missing key) — so `provider: apify` with\n *     the plugin off yields a helpful error, not a confusing \"unknown provider\".\n *\n * @param {Map<string, any>} providersMap   The Map returned by scan.mjs loadProviders.\n * @param {{ root: string }} opts\n */\n// A detect-exempt provider whose fetch throws an actionable message — used when\n// a known provider plugin is inactive (disabled / missing key / failed import)\n// so an explicit `provider: <id>` portals.yml entry stays self-explaining.\nfunction inactiveProviderStub(id, reason) {\n  return {\n    id,\n    detect: () => null,\n    fetch: async () => { throw new Error(`plugin \"${id}\" inactive: ${reason}. Run \\`node doctor.mjs\\` for setup.`); },\n  };\n}\n\nexport async function mergeProviderPlugins(providersMap, { root }) {\n  if (!existsSync(pluginsConfigPath(root))) return; // (1) opted out → inert (no work, no env read)\n\n  // Everything past the opt-out gate is wrapped so an UNANTICIPATED throw\n  // (a callee regression) degrades to a ⚠️ and leaves the core providers Map\n  // untouched — fail-open is enforced structurally here, not just emergently.\n  try {\n    const cfg = await loadPluginConfig(root);\n    const providerManifests = discoverPlugins(pluginRoots(root), resolveSuccessorIds(root)).filter(m => m.hooks.includes('provider'));\n    if (providerManifests.length === 0) return;\n\n    // Only the plugins the user actually switched on in plugins.yml matter.\n    const configuredOn = providerManifests.filter(m => cfg?.plugins?.[m.id]?.enabled === true);\n    if (configuredOn.length === 0) return;\n","sourceCodeStart":655,"sourceCodeEnd":691,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/plugins/_engine.mjs#L655-L691","documentation":"Thrown by `inactiveProviderStub`, a synthetic provider object injected by the plugin engine (`mergeProviderPlugins` in plugins/_engine.mjs) when a known provider plugin is registered in portals.yml with an explicit `provider: <id>` entry but the plugin itself is inactive (disabled, missing its API key, or failed to import). Instead of producing a confusing 'unknown provider' error, the stub yields an actionable message directing you to `node doctor.mjs`. The `reason` string names the specific cause (e.g. 'missing key', 'disabled', 'failed import').","triggerScenarios":"A portals.yml entry sets `provider: apify` (or gmail, etc.) but the corresponding plugin is not active. The stub's `fetch()` is invoked by scan.mjs when it tries to pull jobs from that provider. The `detect()` always returns null (so auto-detection never fires), and only an explicit `provider:` reference triggers the stub's fetch path.","commonSituations":"Apify plugin referenced but `APIFY_TOKEN` not added to .env; gmail plugin referenced but not enabled in config/plugins.yml; a plugin's manifest declared `keyed: true` and the key file is missing; a plugin failed dynamic import due to a syntax error and the engine marked it inactive. Also happens after a config edit that adds a provider line before running `node doctor.mjs` to verify setup.","solutions":["Run `node doctor.mjs` — it reports which plugins are inactive and the exact reason (missing key, disabled, import failure).","If the reason is a missing key, add it to .env (e.g. APIFY_TOKEN=...) and enable the plugin in config/plugins.yml.","If the reason is 'disabled', set the plugin's `enabled: true` in config/plugins.yml.","If the reason is 'failed import', check the plugin's .mjs file for syntax/runtime errors, or update the plugin.","If you no longer want that provider, remove the `provider: <id>` line from portals.yml so scan.mjs stops referencing it."],"exampleFix":"// portals.yml — before\n- name: indeed\n  provider: apify\n  actor: misceres/indeed-scraper\n// doctor.mjs reports: plugin \"apify\" inactive: missing key APIFY_TOKEN\n// .env — after\nAPIFY_TOKEN=apify_api_xxxxxxxxxxxxx","handlingStrategy":"validation","validationCode":"import { execSync } from 'node:child_process';\n// Before referencing a provider, confirm its plugin is active.\nfunction assertPluginActive(id) {\n  const out = execSync('node doctor.mjs --json', { encoding: 'utf-8' });\n  const { plugins } = JSON.parse(out);\n  const p = plugins?.[id];\n  if (!p || p.status !== 'active') {\n    throw new Error(`Refusing to scan: provider '${id}' is ${p?.reason || 'not configured'}. Run: node doctor.mjs`);\n  }\n}\nassertPluginActive('apify');","typeGuard":null,"tryCatchPattern":"// When invoking a known-keyed provider, catch and surface the setup hint.\ntry {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (/plugin \".+\" inactive/.test(err.message)) {\n    console.error(`Setup required: ${err.message}`);\n    process.exitCode = 2;\n  } else throw err;\n}","preventionTips":["Run `node doctor.mjs` after every portals.yml or config/plugins.yml change.","Gate scans behind a doctor check in CI before invoking providers."],"tags":["plugin","configuration","setup","doctor"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}