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
- Pass the extracted plugin directory (the folder containing opencli.json / plugin manifest), not the archive or a file inside it.
- If the plugin is an archive, extract it first (unzip/untar) and use the resulting directory.
- 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
- Always pass the extracted plugin folder, never a .zip/.tar.gz or a file inside it.
- Add a pre-install fs.statSync(path).isDirectory() check in install scripts.
- Resolve symlinks with fs.realpathSync before installing.
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
- Directory "${targetDir}" already exists and is not empty.
- Video file not found: ${filePath}
- Video file is empty: ${filePath}
- 封面文件不存在: ${path.resolve(coverPath)}
- Story media file not found: ${resolved}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/32ac24b46d78a110.
Report an issue: GitHub.