{"record":{"id":"4fe2788019b5cd5f","repo":"jackwener/OpenCLI","slug":"invalid-plugin-structure-validation-errors-jo","errorCode":null,"errorMessage":"Invalid plugin structure:\n- ${validation.errors.join('\n- ')}","messagePattern":"Invalid plugin structure:\n- (.+?)","errorType":"validation","errorClass":"PluginError","httpStatus":null,"severity":"error","filePath":"src/plugin.ts","lineNumber":624,"sourceCode":" * For monorepos that do NOT use workspaces, sub-plugins may declare their own\n * production dependencies in their package.json.  We install those per sub-plugin\n * so that runtime imports (e.g. `undici`) can be resolved from the sub-plugin\n * directory.  When the root already satisfies all deps this is a fast no-op.\n */\nfunction postInstallMonorepoLifecycle(repoDir: string, pluginDirs: string[]): void {\n  installDependencies(repoDir);\n  for (const pluginDir of pluginDirs) {\n    if (pluginDir !== repoDir && hasOwnDependencies(pluginDir)) {\n      installDependencies(pluginDir);\n    }\n    finalizePluginRuntime(pluginDir);\n  }\n}\n\nfunction ensureStandalonePluginReady(pluginDir: string): void {\n  const validation = validatePluginStructure(pluginDir);\n  if (!validation.valid) {\n    throw new PluginError(`Invalid plugin structure:\\n- ${validation.errors.join('\\n- ')}`);\n  }\n\n  postInstallLifecycle(pluginDir);\n}\n\ntype LockEntryInput = Omit<LockEntry, 'installedAt'> & Partial<Pick<LockEntry, 'installedAt'>>;\n\nfunction upsertLockEntry(\n  lock: Record<string, LockEntry>,\n  name: string,\n  entry: LockEntryInput,\n): void {\n  lock[name] = {\n    ...entry,\n    installedAt: entry.installedAt ?? new Date().toISOString(),\n  };\n}\n","sourceCodeStart":606,"sourceCodeEnd":642,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/src/plugin.ts#L606-L642","documentation":"ensureStandalonePluginReady validates the cloned/updated plugin directory via validatePluginStructure and throws a PluginError listing all structural problems. The check requires at least one .ts or .js command file, and for TS plugins a package.json with \"type\": \"module\". opencli throws this so broken plugins never reach installation.","triggerScenarios":"installSinglePlugin (via installPlugin of a non-monorepo repo) or updatePlugin on a standalone plugin whose repo: contains no command files, has .ts files but no package.json, package.json lacks \"type\": \"module\", or has malformed package.json.","commonSituations":"Pointing installPlugin at a repo that isn't actually an opencli plugin (wrong repo, docs-only repo); plugin author forgot package.json or shipped CommonJS config; repo restructured and command files moved into subdirectories (only root files are scanned).","solutions":["Read the bullet list in the error and fix each item in the plugin repo.","Add at least one .ts or .js command file at the plugin root.","For TS plugins, add a package.json containing \"type\": \"module\" and the @jackwener/opencli peer dependency.","Ensure you're installing the intended plugin repo, not an unrelated repository.","Fix malformed JSON in package.json if the error reports it."],"exampleFix":"// before (package.json)\n{ \"name\": \"my-plugin\" }\n// after\n{ \"name\": \"my-plugin\", \"type\": \"module\", \"peerDependencies\": { \"@jackwener/opencli\": \"*\" } }","handlingStrategy":"validation","validationCode":"import * as fs from 'node:fs';\nimport * as path from 'node:path';\nfunction looksLikePlugin(dir) {\n  if (!fs.existsSync(dir)) return false;\n  const files = fs.readdirSync(dir);\n  const hasTs = files.some(f => f.endsWith('.ts'));\n  const hasJs = files.some(f => f.endsWith('.js'));\n  if (!hasTs && !hasJs) return false;\n  if (hasTs) {\n    const pkgPath = path.join(dir, 'package.json');\n    if (!fs.existsSync(pkgPath)) return false;\n    try { return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).type === 'module'; }\n    catch { return false; }\n  }\n  return true;\n}","typeGuard":null,"tryCatchPattern":"try {\n  installPlugin(source);\n} catch (err) {\n  if (err instanceof PluginError && err.message.startsWith('Invalid plugin structure')) {\n    // surface err.message bullets to the plugin author\n  } else throw err;\n}","preventionTips":["Ship at least one root-level .ts or .js command file in every plugin repo.","Include a package.json with \"type\": \"module\" for TypeScript plugins.","Validate the plugin locally with opencli before publishing the repo.","Confirm you're installing a plugin repo, not an unrelated repository."],"tags":["validation","plugin-structure","package-json"],"backgroundTag":"plugin-structure-invalid","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}