Files
Miroslav Drbal 4e199f93f7 feat: add marketplace plugin system
Add Claude Code-compatible marketplace plugin infrastructure:

- Registry: types, ID helpers, atomic read/write for marketplaces.json
  and installed_plugins.json (Claude Code format with version: 2)
- Fetcher: classifySource (6 ordered rules), parseMarketplaceCatalog,
  fetchMarketplace (local sources; git/http stubs for Phase 2)
- Resolver + Cache: resolvePluginSource with pathIsWithin containment,
  cachePlugin, removeCachedPlugin, cleanOrphanedCache
- MarketplaceManager: orchestrates add/remove/update marketplaces,
  install/uninstall/enable plugins, clearPluginRootsCache on mutation
- CLI: omp plugin marketplace add|remove|update|list,
  omp plugin discover, classifyInstallTarget for name@marketplace
- Discovery: listClaudePluginRoots reads OMP registry alongside
  Claude's, OMP authoritative for duplicate plugin IDs
- Args: --plugin-dir repeatable flag (parsing only, runtime wiring TBD)
- Slash command: /reload-plugins clears fs + roots cache
- Test fixtures and 130 tests across 8 test files
2026-03-30 13:44:53 +02:00

60 lines
2.0 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { substitutePluginRoot } from "@oh-my-pi/pi-coding-agent/discovery/substitute-plugin-root";
// Use concatenation to avoid noTemplateCurlyInString lint rule on literal placeholder names
const CLAUDE_VAR = "$" + "{CLAUDE_PLUGIN_ROOT}";
const OMP_VAR = "$" + "{OMP_PLUGIN_ROOT}";
describe("substitutePluginRoot", () => {
const ROOT = "/plugins/my-plugin";
it("replaces CLAUDE_PLUGIN_ROOT in strings", () => {
expect(substitutePluginRoot(`${CLAUDE_VAR}/bin/server`, ROOT)).toBe("/plugins/my-plugin/bin/server");
});
it("replaces OMP_PLUGIN_ROOT in strings", () => {
expect(substitutePluginRoot(`${OMP_VAR}/bin/server`, ROOT)).toBe("/plugins/my-plugin/bin/server");
});
it("replaces both variables in same string", () => {
expect(substitutePluginRoot(`${CLAUDE_VAR}:${OMP_VAR}`, ROOT)).toBe("/plugins/my-plugin:/plugins/my-plugin");
});
it("handles arrays recursively", () => {
expect(substitutePluginRoot(["--config", `${CLAUDE_VAR}/config.json`], ROOT)).toEqual([
"--config",
"/plugins/my-plugin/config.json",
]);
});
it("handles objects recursively", () => {
expect(substitutePluginRoot({ PATH: `${CLAUDE_VAR}/bin` }, ROOT)).toEqual({
PATH: "/plugins/my-plugin/bin",
});
});
it("handles nested structures", () => {
const input = {
command: `${CLAUDE_VAR}/server`,
args: ["--port", "3000"],
env: { HOME: OMP_VAR },
};
expect(substitutePluginRoot(input, ROOT)).toEqual({
command: "/plugins/my-plugin/server",
args: ["--port", "3000"],
env: { HOME: "/plugins/my-plugin" },
});
});
it("passes through non-string primitives", () => {
expect(substitutePluginRoot(42, ROOT)).toBe(42);
expect(substitutePluginRoot(true, ROOT)).toBe(true);
expect(substitutePluginRoot(null, ROOT)).toBeNull();
expect(substitutePluginRoot(undefined, ROOT)).toBeUndefined();
});
it("returns string unchanged when no variables present", () => {
expect(substitutePluginRoot("no-vars-here", ROOT)).toBe("no-vars-here");
});
});