fix(plugins): enumerate linked plugins for sub-discovery

PluginManager.link symlinks the package into <plugins>/node_modules
and records it in omp-plugins.lock.json, but never writes to
<plugins>/package.json#dependencies. getEnabledPlugins iterated only
the dependency map, so the documented `omp install ./local-extension`
workflow (delegated to plugin link) succeeded but its sibling skills/,
hooks/, tools/, etc. stayed invisible after install.

Iterate the union of package.json#dependencies and
omp-plugins.lock.json#plugins so symlinked-only packages surface
alongside npm/marketplace installs. Lockfile entries whose
node_modules tree has since been deleted (stale link) are skipped
silently. Linked-only setups with no <plugins>/package.json at all
now work too.

Per-PR review feedback: https://github.com/can1357/oh-my-pi/pull/1498
This commit is contained in:
roboomp
2026-05-29 06:34:10 +00:00
parent bdce9cf068
commit 4d108c1951
2 changed files with 59 additions and 16 deletions
@@ -215,3 +215,31 @@ test("disabled installed plugins do not contribute sub-discovery", async () => {
const skills = await loadFromPlugin<{ name: string; path: string }>(skillCapability.id, ctx());
expect(skills.find(s => s.path.includes("my-disabled-ext"))).toBeUndefined();
});
test("linked plugins (only in lockfile, not in package.json#dependencies) are surfaced", async () => {
// `omp plugin link ./local-ext` creates a symlink under
// `<plugins>/node_modules/<pkg>` plus a lockfile entry, but it never
// touches `<plugins>/package.json#dependencies`. The discovery path must
// still find the package — otherwise the documented `omp install
// ./local-extension` workflow leaves the sibling skills/hooks/tools
// invisible (see PR #1498 review).
const pluginsDir = path.join(home, ".omp", "plugins");
const nodeModules = path.join(pluginsDir, "node_modules");
fs.mkdirSync(nodeModules, { recursive: true });
const linkTarget = path.join(nodeModules, "my-linked-ext");
fs.symlinkSync(ext, linkTarget);
// Intentionally NO `<plugins>/package.json` — matches a fresh `plugin link`
// against a setup that has never run `plugin install`.
writeFile(
path.join(pluginsDir, "omp-plugins.lock.json"),
JSON.stringify({
plugins: { "my-linked-ext": { version: "1.0.0", enabled: true, enabledFeatures: null } },
settings: {},
}),
);
const skills = await loadFromPlugin<{ name: string; path: string }>(skillCapability.id, ctx());
const tools = await loadFromPlugin<{ name: string; path: string }>(toolCapability.id, ctx());
expect(skills.find(s => s.name === "my-skill" && s.path.includes("my-linked-ext"))).toBeDefined();
expect(tools.find(t => t.name === "wcount" && t.path.includes("my-linked-ext"))).toBeDefined();
});