jackwener/OpenCLI · error · PluginError

Invalid plugin structure: - ${validation.errors.join(' - ')}

Error message

Invalid plugin structure:
- ${validation.errors.join('
- ')}

What it means

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.

Source

Thrown at src/plugin.ts:624

 * For monorepos that do NOT use workspaces, sub-plugins may declare their own
 * production dependencies in their package.json.  We install those per sub-plugin
 * so that runtime imports (e.g. `undici`) can be resolved from the sub-plugin
 * directory.  When the root already satisfies all deps this is a fast no-op.
 */
function postInstallMonorepoLifecycle(repoDir: string, pluginDirs: string[]): void {
  installDependencies(repoDir);
  for (const pluginDir of pluginDirs) {
    if (pluginDir !== repoDir && hasOwnDependencies(pluginDir)) {
      installDependencies(pluginDir);
    }
    finalizePluginRuntime(pluginDir);
  }
}

function ensureStandalonePluginReady(pluginDir: string): void {
  const validation = validatePluginStructure(pluginDir);
  if (!validation.valid) {
    throw new PluginError(`Invalid plugin structure:\n- ${validation.errors.join('\n- ')}`);
  }

  postInstallLifecycle(pluginDir);
}

type LockEntryInput = Omit<LockEntry, 'installedAt'> & Partial<Pick<LockEntry, 'installedAt'>>;

function upsertLockEntry(
  lock: Record<string, LockEntry>,
  name: string,
  entry: LockEntryInput,
): void {
  lock[name] = {
    ...entry,
    installedAt: entry.installedAt ?? new Date().toISOString(),
  };
}

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read the bullet list in the error and fix each item in the plugin repo.
  2. Add at least one .ts or .js command file at the plugin root.
  3. For TS plugins, add a package.json containing "type": "module" and the @jackwener/opencli peer dependency.
  4. Ensure you're installing the intended plugin repo, not an unrelated repository.
  5. Fix malformed JSON in package.json if the error reports it.

Example fix

// before (package.json)
{ "name": "my-plugin" }
// after
{ "name": "my-plugin", "type": "module", "peerDependencies": { "@jackwener/opencli": "*" } }
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from 'node:fs';
import * as path from 'node:path';
function looksLikePlugin(dir) {
  if (!fs.existsSync(dir)) return false;
  const files = fs.readdirSync(dir);
  const hasTs = files.some(f => f.endsWith('.ts'));
  const hasJs = files.some(f => f.endsWith('.js'));
  if (!hasTs && !hasJs) return false;
  if (hasTs) {
    const pkgPath = path.join(dir, 'package.json');
    if (!fs.existsSync(pkgPath)) return false;
    try { return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).type === 'module'; }
    catch { return false; }
  }
  return true;
}

Try / catch

try {
  installPlugin(source);
} catch (err) {
  if (err instanceof PluginError && err.message.startsWith('Invalid plugin structure')) {
    // surface err.message bullets to the plugin author
  } else throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/4fe2788019b5cd5f. Report an issue: GitHub.