jackwener/OpenCLI · error · PluginError

Local plugin path is not a directory: ${localPath}

Error message

Local plugin path is not a directory: ${localPath}

What it means

opencli throws this PluginError during local plugin installation when the user-supplied localPath exists on disk but is a file (or symlink to a file) rather than a directory. Local plugins must be a directory containing a plugin manifest and code. The check runs after an existsSync guard, so this specifically means 'exists but not a directory'.

Source

Thrown at src/plugin.ts:777

    }
  });

  return pluginName;
}

/**
 * Install a local plugin by creating a symlink.
 * Used for plugin development: the source directory is symlinked into
 * the plugins dir so changes are reflected immediately.
 */
function installLocalPlugin(localPath: string, name: string): string {
  if (!fs.existsSync(localPath)) {
    throw new PluginError(`Local plugin path does not exist: ${localPath}`);
  }

  const stat = fs.statSync(localPath);
  if (!stat.isDirectory()) {
    throw new PluginError(`Local plugin path is not a directory: ${localPath}`);
  }

  const manifest = readPluginManifest(localPath);

  if (manifest?.opencli && !checkCompatibility(manifest.opencli)) {
    throw new PluginError(
      `Plugin requires opencli ${manifest.opencli}, but current version is incompatible.`,
      'Upgrade opencli to a compatible version.',
    );
  }

  const pluginName = manifest?.name ?? name;
  const targetDir = path.join(PLUGINS_DIR, pluginName);

  if (fs.existsSync(targetDir)) {
    throw new PluginError(`Plugin "${pluginName}" is already installed at ${targetDir}`, 'Use "opencli plugin uninstall" first, or pick a different name.');
  }

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Pass the extracted plugin directory (the folder containing opencli.json / plugin manifest), not the archive or a file inside it.
  2. If the plugin is an archive, extract it first (unzip/untar) and use the resulting directory.
  3. Verify with `ls -la <path>` that the path is a directory; fix the path if it points to a file or broken symlink.

Example fix

// before
opencli plugin install ./my-plugin.zip
// after
unzip my-plugin.zip -d my-plugin
opencli plugin install ./my-plugin
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'fs';
if (!fs.existsSync(localPath) || !fs.statSync(localPath).isDirectory()) {
  throw new Error(`Expected a plugin directory, got: ${localPath}`);
}

Type guard

function isPluginDir(p: string): boolean {
  try { return fs.statSync(p).isDirectory(); } catch { return false; }
}

Try / catch

try {
  await opencli.plugin.install(localPath);
} catch (e) {
  if (e instanceof PluginError && e.message.includes('is not a directory')) {
    console.error(`Extract the archive or pass the plugin folder: ${localPath}`);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the local plugin install function (opencli plugin install with a local path) with a path pointing to a regular file, e.g. a zipped/tarball plugin archive or the manifest file itself instead of its containing folder.

Common situations: Passing plugin.zip or plugin.tar.gz instead of the extracted folder; passing opencli.json (the manifest) instead of the plugin root; typos resolving to a file; pointing at a symlink whose target is a file.

Related errors


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