feat(discovery): scan installed plugin packages for sub-discovery
Marketplace and `omp plugin link` installs write to
`<plugins>/node_modules/` rather than to `extensions:` in settings,
so the original PR still missed their sibling skills/, hooks/,
tools/, commands/, rules/, prompts/, .mcp.json sub-trees. Wire
listOmpExtensionRoots to enumerate getEnabledPlugins(cwd, { home })
in addition to CLI-injected and settings-driven roots.
Adds an optional { home } parameter to getEnabledPlugins so the
discovery loader can pass through LoadContext.home for tempdir-rooted
tests. The getPluginsNodeModules/getPluginsPackageJson/
getPluginsLockfile helpers gain the same optional home overload so
they mirror getPluginsDir.
Per-PR review feedback: https://github.com/can1357/oh-my-pi/pull/1498
This commit is contained in:
@@ -17,9 +17,10 @@
|
||||
*/
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { isEnoent, tryParseJson } from "@oh-my-pi/pi-utils";
|
||||
import { isEnoent, logger, tryParseJson } from "@oh-my-pi/pi-utils";
|
||||
import { readDirEntries, readFile } from "../capability/fs";
|
||||
import type { LoadContext } from "../capability/types";
|
||||
import { getEnabledPlugins } from "../extensibility/plugins/loader";
|
||||
import { expandTilde } from "../tools/path-utils";
|
||||
|
||||
/** A resolved extension package directory wired into the discovery surfaces. */
|
||||
@@ -122,24 +123,31 @@ async function isDirectory(p: string): Promise<boolean> {
|
||||
* 1. CLI roots injected via {@link injectOmpExtensionCliRoots}
|
||||
* 2. Project `<cwd>/.omp/settings.json#extensions`
|
||||
* 3. User `~/.omp/agent/settings.json#extensions`
|
||||
* 4. Enabled plugins installed under `<plugins>/node_modules/` (e.g. via
|
||||
* `omp install <pkg>` / `omp plugin install` / `omp plugin link`)
|
||||
*
|
||||
* Only entries that resolve to a directory on disk are returned; file
|
||||
* entrypoints contribute zero sub-discovery surface and are filtered out.
|
||||
* Installed-plugin enumeration failures (missing lockfile, unreadable
|
||||
* `package.json`, etc.) are logged at `debug` and degrade gracefully — the
|
||||
* other sources still surface.
|
||||
*/
|
||||
export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtensionRoot[]> {
|
||||
const { project, user } = scopeDirs(ctx);
|
||||
const [projectExtensions, userExtensions] = await Promise.all([
|
||||
const [projectExtensions, userExtensions, installedPlugins] = await Promise.all([
|
||||
readSettingsExtensions(path.join(project, "settings.json")),
|
||||
readSettingsExtensions(path.join(user, "settings.json")),
|
||||
listInstalledPluginRoots(ctx),
|
||||
]);
|
||||
|
||||
const candidates: InjectedRoot[] = [
|
||||
...injectedCliRoots,
|
||||
...projectExtensions.map((raw): InjectedRoot => ({ path: resolveAgainst(raw, ctx), level: "project" })),
|
||||
...userExtensions.map((raw): InjectedRoot => ({ path: resolveAgainst(raw, ctx), level: "user" })),
|
||||
...installedPlugins,
|
||||
];
|
||||
|
||||
// First-seen-wins dedup preserves CLI > project > user precedence.
|
||||
// First-seen-wins dedup preserves CLI > project-settings > user-settings > installed precedence.
|
||||
const seen = new Set<string>();
|
||||
const unique: InjectedRoot[] = [];
|
||||
for (const candidate of candidates) {
|
||||
@@ -157,3 +165,26 @@ export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtens
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate every enabled installed plugin's package directory so its
|
||||
* conventional `skills/`, `hooks/`, `tools/`, `commands/`, `rules/`,
|
||||
* `prompts/`, and `.mcp.json` are wired into discovery — mirrors how
|
||||
* `getAllPluginExtensionPaths` already feeds the extension factory loader.
|
||||
*
|
||||
* Marketplace and `omp plugin link` installs write to the plugin manager's
|
||||
* `node_modules` (or symlink into it) rather than to `extensions:` in
|
||||
* settings; without this branch the sub-discovery provider would still miss
|
||||
* everything those install paths produce.
|
||||
*/
|
||||
async function listInstalledPluginRoots(ctx: LoadContext): Promise<InjectedRoot[]> {
|
||||
try {
|
||||
const plugins = await getEnabledPlugins(ctx.cwd, { home: ctx.home });
|
||||
// Installed plugins are always user-scope; project disablement is already
|
||||
// honored by `getEnabledPlugins` via `loadProjectOverrides`.
|
||||
return plugins.map(({ path: p }) => ({ path: p, level: "user" }));
|
||||
} catch (err) {
|
||||
logger.debug("listInstalledPluginRoots: enumeration failed", { error: String(err) });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,14 @@ installLegacyPiSpecifierShim();
|
||||
|
||||
/**
|
||||
* Load plugin runtime config from lock file.
|
||||
*
|
||||
* `home` controls which `<plugins>/omp-plugins.lock.json` is read — pass it
|
||||
* through whenever the caller is loading plugins for a tempdir-rooted
|
||||
* scenario (tests, discovery sub-surfaces that need to mirror an alternate
|
||||
* `LoadContext.home`).
|
||||
*/
|
||||
async function loadRuntimeConfig(): Promise<PluginRuntimeConfig> {
|
||||
const lockPath = getPluginsLockfile();
|
||||
async function loadRuntimeConfig(home?: string): Promise<PluginRuntimeConfig> {
|
||||
const lockPath = getPluginsLockfile(home);
|
||||
try {
|
||||
return await Bun.file(lockPath).json();
|
||||
} catch (err) {
|
||||
@@ -46,10 +51,15 @@ async function loadProjectOverrides(cwd: string): Promise<ProjectPluginOverrides
|
||||
}
|
||||
/**
|
||||
* Get list of enabled plugins with their resolved configurations.
|
||||
* Respects both global runtime config and project overrides.
|
||||
*
|
||||
* Respects both global runtime config and project overrides. The optional
|
||||
* `home` parameter pins the plugins root for callers that need to enumerate
|
||||
* plugins relative to a non-default home (tests with a tempdir, discovery
|
||||
* loaders threaded with `LoadContext.home`).
|
||||
*/
|
||||
export async function getEnabledPlugins(cwd: string): Promise<InstalledPlugin[]> {
|
||||
const pkgJsonPath = getPluginsPackageJson();
|
||||
export async function getEnabledPlugins(cwd: string, opts: { home?: string } = {}): Promise<InstalledPlugin[]> {
|
||||
const { home } = opts;
|
||||
const pkgJsonPath = getPluginsPackageJson(home);
|
||||
let pkg: { dependencies?: Record<string, string> };
|
||||
try {
|
||||
pkg = await Bun.file(pkgJsonPath).json();
|
||||
@@ -58,13 +68,13 @@ export async function getEnabledPlugins(cwd: string): Promise<InstalledPlugin[]>
|
||||
throw err;
|
||||
}
|
||||
|
||||
const nodeModulesPath = getPluginsNodeModules();
|
||||
const nodeModulesPath = getPluginsNodeModules(home);
|
||||
if (!fs.existsSync(nodeModulesPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const deps = pkg.dependencies || {};
|
||||
const runtimeConfig = await loadRuntimeConfig();
|
||||
const runtimeConfig = await loadRuntimeConfig(home);
|
||||
const projectOverrides = await loadProjectOverrides(cwd);
|
||||
const plugins: InstalledPlugin[] = [];
|
||||
for (const [name] of Object.entries(deps)) {
|
||||
|
||||
Reference in New Issue
Block a user