Migrate 203 test files (356 call sites) from fs.rm/fs.rmSync to
removeWithRetries/removeSyncWithRetries to reduce EBUSY test failures
on Windows. removeWithRetries is now exported from @oh-my-pi/pi-utils.
The migration uses a regex-based approach that:
- Replaces fs.rm(path, { recursive, force }) → removeWithRetries(path)
- Replaces fs.rmSync(path, { recursive, force }) → removeSyncWithRetries(path)
- Replaces fs.rm(path) → removeWithRetries(path) (no options)
- Skips fs.rm/fs.rmSync inside template literals (bun --eval scripts)
- Adds imports to existing @oh-my-pi/pi-utils import or creates new one
- Removes unused fs imports where fs.rm was the only fs usage (4 files)
plugin-extensions-discovery.test.ts redirected XDG_DATA_HOME to a temp dir to isolate the plugins directory, but XDG is gated to Linux/macOS in dirs.ts, so on Windows getPluginsDir() still resolved to the real ~/.omp/plugins. The suite then wrote fixtures into -- and fs.rmSync'd the node_modules of -- the developer's real plugins directory, wiping installed plugins (green on Linux CI, destructive on Windows local).
Isolate the whole config root cross-platform: mock os.homedir() so configRoot = <tempHome>/.omp, AND clear the XDG_* vars in beforeEach (restored in afterEach). Clearing XDG is required because on Linux/macOS the resolver prefers $XDG_DATA_HOME/omp over the home config root when that dir exists, so an XDG-migrated environment would otherwise still resolve -- and these tests would rm -r -- the real plugins dir. Test-only; no production change.
Verified on Windows: real ~/.omp/plugins/package.json sha256 is unchanged across a full run; with XDG_DATA_HOME set to a sentinel containing an omp/ dir, no plugins dir is created there. 9/9 tests pass.
Installed pi packages declare their entry as `pi.extensions: ["./extensions"]` with the real module at `extensions/<name>/index.ts`, but the plugin manifest resolver only matched a file or a directory containing a direct index.{ts,js,mjs,cjs}. A directory whose entry sits one level below resolved to null, so `omp plugin install` rejected it ("declared extension entry not found on disk") and runtime load silently skipped it.
For the extensions key, resolveManifestEntryFiles now resolves a directory like OMP's configured-directory (-e) scanner: the directory's own package.json omp/pi extensions (authoritative -- a declared-but-missing entry is reported, not replaced by a decoy index), then a direct index.{ts,js,mjs,cjs}, then a one-level scan where each child directory is itself resolved manifest-first plus direct *.{ts,js,mjs,cjs} files. The directory expansion is gated to the extensions key; tools/hooks/commands keep direct-index resolution so a directory entry like `tools: "."` is unaffected.
Adds CHANGELOG Unreleased entry and regression tests for subdir-index, nested-manifest-over-index, missing-declared-entry, and key-aware (tools vs extensions) resolution. Verified: omp plugin install @zosmaai/pi-llm-wiki registers all 14 wiki_* tools; pi-mcp-adapter loads.
Kept explicit null package imports as exclusions so exact entries and active conditions do not fall through to wildcard or fallback targets.\n\nFixes #1889
Returned null from resolveSourceModuleFile when a package imports alias points at a JSON, WASM, or other non-code asset so the on-load rewrite hook no longer claims it and forces it through the JS loader. Bun resolves these targets natively. Added a regression where #schema maps to a .json file imported with a JSON type assertion.\n\nFixes #1889
Selected conditional package import targets by package.json object order for supported Bun runtime conditions, including node, instead of probing a fixed precedence list. Added a regression where node precedes import and must be selected for a plugin-local #src/* import.\n\nFixes #1889
Extended the legacy Pi, TypeBox, package-import alias, and extension graph regexes to recognise the bare \"import \\"specifier\\";\" shape so side-effect-only loads such as \"import \\"#src/register\\";\" walk into the source graph and get their legacy @(scope)/pi-* imports rewritten. Added a regression that loads a plugin with a side-effect alias import whose target contains a legacy scope import.\n\nFixes #1889
Resolved plugin-local package import aliases while loading extension source graphs so legacy Pi plugins with TypeScript source imports like #src/* register correctly under OMP. Added a regression covering legacy scope rewrites through a package import module.\n\nFixes #1889
Plugin manifests that declare a directory entry (e.g. pi-goal's
`"pi": { "extensions": [".pi/extensions/pi-goal"] }`) were
pushed through to the legacy module loader unchanged. `Bun.file()`
on a directory throws "Directories cannot be read like files",
so every such plugin failed to load.
`resolvePluginPaths` now routes each joined manifest entry through
`resolveManifestEntryFile`, which:
- returns the path as-is when it is a file,
- returns the directory's `index.{ts,js,mjs,cjs}` when it is a
directory containing one of those files,
- otherwise returns null (skip).
This mirrors the documented native auto-discovery behavior in
`resolveExtensionEntries` and the candidate list in
`extensibility/custom-commands/loader.ts`.
Closes#1001.
- Updated plugin extension discovery test to use XDG_DATA_HOME instead of HOME for environment isolation.
- Replaced manual path construction with getPluginsDir() utility for consistency.
- Added XDG directory structure setup and path cache rebuild to ensure correct plugin resolution.
- Added `toExtensionId()` method to all capability types for granular extension identification and disabling.
- Added `disabledExtensions` and `includeDisabled` options to LoadOptions for filtering disabled capabilities by extension ID.
- Added plugin manifest support for `extensions` entry points with automatic discovery from installed plugins.
- Fixed skill loading to properly respect disabled skill names from custom directories.
- Improved cross-platform path handling by using `path.basename()` instead of string splitting in context file and state manager.
- Refactored capability index loading to validate source metadata and filter disabled extensions before processing.