PluginManager.install (bun install + bun update), PluginManager.uninstall,
PluginManager.#fixMissingPlugin, the legacy installer.ts install/uninstall
helpers, and generate-legacy-pi-bundled-registry.ts's formatInPlace all
called Bun.spawn with stdout/stderr piped and awaited proc.exited before
touching either stream. Once a child's output exceeded the ~64 KiB OS
pipe buffer, the child would block on write(2) while the parent blocked
on exit — a classic pipe-buffer deadlock. Even where Bun's current runtime
happens to buffer eagerly, the pattern silently leaked unbounded bytes.
Each site now starts new Response(proc.stdout).text() and stderr readers
immediately after Bun.spawn and awaits them alongside proc.exited via
Promise.all. Existing error semantics are preserved: install throws with
stderr, uninstall keeps its generic error, and formatInPlace still includes
Biome's stderr in the failure message.
Adds a regression test (plugin-install-git.test.ts) that models the
OS-pipe deadlock by holding proc.exited until both mock streams are
drained — install must read them before awaiting exit, else the test hits
its 2s Promise.race timeout.
Fixes#4230
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)
bun install <spec> respects the existing bun.lock pin when the spec is
unchanged and never re-resolves the remote ref, so re-running
`omp plugin install github:owner/repo` on an already-installed plugin
reported success while silently keeping the user on the original
resolved commit (1ms no-op, no network).
PluginManager.install now follows a git re-install with
`bun update <name>` to force re-resolution of the ref against the
upstream. First-time installs (no prior dep entry) skip the update —
the initial bun install already fetches HEAD. bun update failures
trigger the same rollback path as validation failures.
Fixes#3063
Extends `omp plugin install` to accept git sources alongside npm specs and
marketplace refs. Bun's installer already understands git URLs; the blocker
was `PluginManager.install`'s strict npm-name validator and the assumption
that the actual package name could be derived from the spec.
- `git-url.ts`: `parseGitUrl` now recognizes npm-style namespaced shorthand
(`github:user/repo`, `gitlab:`, `bitbucket:`, `codeberg:`, `sourcehut:` /
`srht:`), with optional `#ref` and `.git` suffix. Exposes `isGitSpec` as
`parseGitUrl(s) !== null`. Existing protocol-URL and `git:` shorthand paths
are untouched.
- `manager.ts`: `install()` branches on `isGitSpec`. Git specs go through a
separate `validateGitSpec` (shell-metachar rejection only — `/`, `:`, `@`,
`#`, `+` are legal) and the real package name is discovered by snapshotting
`plugins/package.json` deps before `bun install` and diffing afterwards.
Falls back to value-match on force-reinstall where the key already exists.
- Help text in `plugin-cli` documents the new sources and adds a github:
example.
Smoke tested end-to-end on Windows with both forms against the test repo:
PluginManager.install('github:oldschoola/omp-insights')
PluginManager.install('https://github.com/oldschoola/omp-insights')
both resolve `@oldschoola/omp-insights@1.2.3` and write a correct lock entry.
Shell-injection probe (`github:foo/bar; rm -rf /`) is rejected.