jackwener/OpenCLI · error

Plugin "${name}" is not installed.

Error message

Plugin "${name}" is not installed.

What it means

uninstallPlugin() throws this when the plugin directory does not exist under PLUGINS_DIR. The library treats a missing plugin directory as proof the plugin is not installed, so uninstalling it would be a no-op on nonexistent files. It is a guard against uninstalling something that was never installed or was already removed.

Source

Thrown at src/plugin.ts:1055

  if (!commitHash) return;

  upsertLockEntry(lock, name, {
    source: { kind: 'git', url: cloneUrl },
    commitHash,
    installedAt: existing?.installedAt ?? new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  });
}

/**
 * Uninstall a plugin by name.
 * For monorepo sub-plugins: removes symlink and cleans up the monorepo
 * directory when no more sub-plugins reference it.
 */
export function uninstallPlugin(name: string): void {
  const targetDir = path.join(PLUGINS_DIR, name);
  if (!fs.existsSync(targetDir)) {
    throw new Error(`Plugin "${name}" is not installed.`);
  }

  const lock = readLockFile();
  const lockEntry = lock[name];

  // Check if this is a symlink (monorepo sub-plugin)
  const isSymlink = isSymlinkSync(targetDir);

  if (isSymlink) {
    // Remove symlink only (not the actual directory)
    fs.unlinkSync(targetDir);
  } else {
    fs.rmSync(targetDir, { recursive: true, force: true });
  }

  // Clean up monorepo directory if no more sub-plugins reference it
  if (lockEntry?.source.kind === 'monorepo') {
    delete lock[name];

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Check the plugin is installed first: verify the directory exists under PLUGINS_DIR (e.g. fs.existsSync(path.join(PLUGINS_DIR, name))) before calling uninstallPlugin.
  2. List installed plugins to get the exact name/spelling and retry with the correct name.
  3. If the directory was hand-deleted but lock entries remain, clean up the stale entry from the lock file directly instead of calling uninstallPlugin.
  4. Wrap the call in try/catch and treat this error as 'already uninstalled' in idempotent scripts.

Example fix

// before
uninstallPlugin("my-plguin");
// after
const dir = path.join(PLUGINS_DIR, "my-plugin");
if (fs.existsSync(dir)) {
  uninstallPlugin("my-plugin");
}
Defensive patterns

Strategy: validation

Validate before calling

import fs from "fs";
import path from "path";
function canUninstall(name: string): boolean {
  return fs.existsSync(path.join(PLUGINS_DIR, name));
}
if (canUninstall("my-plugin")) uninstallPlugin("my-plugin");

Try / catch

try {
  uninstallPlugin(name);
} catch (e) {
  if (e instanceof Error && e.message === `Plugin "${name}" is not installed.`) {
    // treat as already-uninstalled; continue idempotently
  } else throw e;
}

Prevention

When it happens

Trigger: Calling uninstallPlugin(name) when no directory exists at path.join(PLUGINS_DIR, name) — i.e. fs.existsSync(targetDir) is false. This happens for a never-installed plugin name, a typo in the name, or a plugin whose directory was deleted manually while a lock-file entry may still exist.

Common situations: Scripting bulk uninstalls where some plugins were already removed; typos in plugin names in CI scripts; cleaning up a checkout by deleting ~/.opencli plugin dirs by hand and then running uninstall; stale shell history re-running an old uninstall command.

Related errors


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