{"record":{"id":"32ac24b46d78a110","repo":"jackwener/OpenCLI","slug":"local-plugin-path-is-not-a-directory-localpath","errorCode":null,"errorMessage":"Local plugin path is not a directory: ${localPath}","messagePattern":"Local plugin path is not a directory: (.+?)","errorType":"validation","errorClass":"PluginError","httpStatus":null,"severity":"error","filePath":"src/plugin.ts","lineNumber":777,"sourceCode":"    }\n  });\n\n  return pluginName;\n}\n\n/**\n * Install a local plugin by creating a symlink.\n * Used for plugin development: the source directory is symlinked into\n * the plugins dir so changes are reflected immediately.\n */\nfunction installLocalPlugin(localPath: string, name: string): string {\n  if (!fs.existsSync(localPath)) {\n    throw new PluginError(`Local plugin path does not exist: ${localPath}`);\n  }\n\n  const stat = fs.statSync(localPath);\n  if (!stat.isDirectory()) {\n    throw new PluginError(`Local plugin path is not a directory: ${localPath}`);\n  }\n\n  const manifest = readPluginManifest(localPath);\n\n  if (manifest?.opencli && !checkCompatibility(manifest.opencli)) {\n    throw new PluginError(\n      `Plugin requires opencli ${manifest.opencli}, but current version is incompatible.`,\n      'Upgrade opencli to a compatible version.',\n    );\n  }\n\n  const pluginName = manifest?.name ?? name;\n  const targetDir = path.join(PLUGINS_DIR, pluginName);\n\n  if (fs.existsSync(targetDir)) {\n    throw new PluginError(`Plugin \"${pluginName}\" is already installed at ${targetDir}`, 'Use \"opencli plugin uninstall\" first, or pick a different name.');\n  }\n","sourceCodeStart":759,"sourceCodeEnd":795,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/src/plugin.ts#L759-L795","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nopencli plugin install ./my-plugin.zip\n// after\nunzip my-plugin.zip -d my-plugin\nopencli plugin install ./my-plugin","handlingStrategy":"validation","validationCode":"import fs from 'fs';\nif (!fs.existsSync(localPath) || !fs.statSync(localPath).isDirectory()) {\n  throw new Error(`Expected a plugin directory, got: ${localPath}`);\n}","typeGuard":"function isPluginDir(p: string): boolean {\n  try { return fs.statSync(p).isDirectory(); } catch { return false; }\n}","tryCatchPattern":"try {\n  await opencli.plugin.install(localPath);\n} catch (e) {\n  if (e instanceof PluginError && e.message.includes('is not a directory')) {\n    console.error(`Extract the archive or pass the plugin folder: ${localPath}`);\n  } else throw e;\n}","preventionTips":["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."],"tags":["plugin","filesystem","path","validation"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}