diff --git a/NOTICE.md b/NOTICE.md index c6f7b90f..61d20ea5 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -38,7 +38,7 @@ Summary of structural changes: - The native `plugins/pstack/skills/` tree is the only user-facing workflow surface. Claude Code and Codex invoke those skills directly. - Seven skills imported from `cursor-team-kit`: `deslop`, `thermo-nuclear-code-quality-review`, `make-pr-easy-to-review`, `fix-ci`, `fix-merge-conflicts`, `get-pr-comments`, `what-did-i-get-done`. All copied verbatim — no rewiring needed. - `plugins/pstack/skills/babysit/` is independently authored as the Claude Code analog of Cursor's `/babysit` built-in. It has no upstream pstack equivalent; its workflow is informed by Cursor's public `/babysit` behavior. No code or prose was copied from any source. -- `plugins/pstack/skills/poteto-mode/scripts/` is vendored from upstream (`watch-pr`, `orch`, `bootstrap.ts`, `worktree-audit.sh`, `package.json`, `bun.lock`) with these port edits: `worktree-audit.sh` reads `~/.claude/projects/` instead of Cursor's transcript directory and warns when `jq` or `rg` is missing (their absence silently blanks the columns the prune decision reads), the private workspace package is named `@open-pstack/poteto-mode-tools`, `bootstrap.ts` rejects Node before it reads Bun-only APIs, and `package.json` includes the port-authored tests in `bun run test`. `check-plan.mjs` is the Cursor 0.14.3 checker adapted for the shared Claude Code and Codex skeleton. `bootstrap.test.ts` and `check-plan.test.ts` are authored for this port. `check-plan.test.ts` also carries the fence cases from upstream's `check-plan.test.ts` at 0.15.20, run against the extracted plan skeleton. +- `plugins/pstack/skills/poteto-mode/scripts/` is vendored from upstream (`watch-pr`, `orch`, `bootstrap.ts`, `worktree-audit.sh`, `package.json`, `bun.lock`) with these port edits: `worktree-audit.sh` reads `~/.claude/projects/` instead of Cursor's transcript directory and warns when `jq` or `rg` is missing (their absence silently blanks the columns the prune decision reads), the private workspace package is named `@open-pstack/poteto-mode-tools`, `bootstrap.ts` rejects Node before it reads Bun-only APIs and installs frozen dependencies in a user cache keyed by the manifest and lockfile outside the plugin, then registers Commander with Bun before the CLI imports it (#293), and `package.json` includes the port-authored tests in `bun run test`. `check-plan.mjs` is the Cursor 0.14.3 checker adapted for the shared Claude Code and Codex skeleton. `bootstrap.test.ts` and `check-plan.test.ts` are authored for this port. `check-plan.test.ts` also carries the fence cases from upstream's `check-plan.test.ts` at 0.15.20, run against the extracted plan skeleton. - `plugins/pstack/agents/comment-sicko.md` is upstream's `Comment Sicko` agent, renamed to `comment-sicko` so the name works as a Claude Code `subagent_type`. The body is verbatim. - `plugins/pstack/agents/poteto-agent.md` keeps the upstream prompt and adds Claude Code `skills` frontmatter so the isolated subagent preloads `pstack:poteto-mode`. - Claude-native lanes are port-authored agent definitions. Fable, Opus and Sonnet select their rolling family alias, and the Opus 5.5 and Haiku 5.5 pins select their exact model, each with the requested effort, for every selectable Claude-native pair in the model catalog. `registry/render.ts` generates them from the runner registry. diff --git a/UPSTREAM.md b/UPSTREAM.md index 452a93e1..e4fd3a20 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -47,6 +47,7 @@ The table above is the current Cursor sync point. Open Pstack 1.17.0 imported th ## Local port-patch ledger +- **#293 — dependency cache outside the plugin**: upstream's `bootstrap.ts` installs `watch-pr` dependencies into the plugin, where Bun's TypeScript bin symlink breaks Codex hook fingerprinting. The port keeps `ensureDependenciesInstalled(): void` and the early Bun guard, but hashes `package.json` and `bun.lock` into a harness-independent user cache under `XDG_CACHE_HOME`, Windows local app data, or `~/.cache`. Frozen installs run in unique staging directories and publish by atomic rename; concurrent starters reuse the completed winner and remove only their own staging data. An incomplete published cache stays untouched and reports its path for removal. The bootstrap loads Commander from that cache and registers it with Bun before the CLI imports it, without restarting the command or changing its environment. Preserve this divergence during syncs. Existing plugin installations polluted by the old bootstrap need a clean reinstall; this bootstrap does not repair them. - **#235 — Claude transcript folder encoding**: the port wrote Claude Code's per-project transcript path into `recall`, `automate-me` and the `session-pickup` playbook. Claude Code encodes each `/` and each `.` in the workspace path as `-`, so a `.claude/worktrees/` session's folder holds `--claude-worktrees-`. Keep both characters in those three lines during syncs. - **#140 — Reflect transcript resolution**: Cursor's Reflect step 1 reads the `agent-transcripts/` directory its system prompt names; the port had substituted Claude Code's per-project directory, which left Codex without a lookup. The port replaces that step's body with one line that points to `plugins/pstack/skills/poteto-mode/references/codex-tools.md#active-transcripts`, and leaves the rest of `reflect/SKILL.md` unchanged. That mapping section holds the harness specifics: the Claude per-project scope, layouts, candidate and scan limits, opening-message verification, the Codex resolver call, and the 2,000-word digest fallback. The port-authored Bun resolver `plugins/pstack/skills/poteto-mode/scripts/resolve-codex-transcript.ts`, started through the `sh` launcher `resolve-codex-transcript` next to it so project Bun config and `.env` files cannot affect it, looks up the parent's known chat ID in Codex's read-only `threads` index, checks rollout scope and ownership, and prints opening-message metadata without transcript text. Preserve this divergence during upstream syncs: keep step 1 as the one-line pointer and keep the mapping section and resolver. - **#120 — harness config homes** (tracked for #105): Cursor's setup destination is `~/.cursor/rules/`. The shared port resolves nonempty `CLAUDE_CONFIG_DIR` / `CODEX_HOME`, otherwise `$HOME/.claude` / `$HOME/.codex`, once at `plugins/pstack/skills/poteto-mode/references/codex-tools.md#harness-config-homes`. `setup-pstack/SKILL.md` reuses that home for reads, sheet/integration writes, snapshots, restoration, and readback; Claude preserves the literal legacy import at the default home and renders exactly `@./pstack-models.md` for redirected homes, keeping spaces and `#` in the config-directory name out of the import line; reruns replace the one import whose target basename is `pstack-models.md`, append if absent, and stop if duplicated. Preserve this intentional divergence during upstream syncs. The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default destinations outside the legacy import, requires the literal default-home rendering and zero/one/many import rules, and verifies unset/empty/space-containing resolution without daily writes. See `CHANGES.md` for the port correction and `tests/setup-config-home-repro.sh` for redirected live-evidence preparation. The Cursor sync point is unchanged. diff --git a/plugins/pstack/skills/poteto-mode/scripts/bootstrap.test.ts b/plugins/pstack/skills/poteto-mode/scripts/bootstrap.test.ts index 2c6816ac..0c6296a3 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/bootstrap.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/bootstrap.test.ts @@ -1,10 +1,26 @@ -import { describe, expect, it } from "bun:test"; -import { join } from "node:path"; +import { afterEach, describe, expect, it } from "bun:test"; +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + realpathSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, delimiter, join } from "node:path"; const nodeEntryPoints = [ ["bootstrap", "bootstrap.ts"], ["watch-pr", "watch-pr/watch-pr"], ] as const; +const bunPackageCache = Bun.spawnSync([process.execPath, "pm", "cache"], { + cwd: import.meta.dir, +}) + .stdout.toString().trim(); describe("Bun runtime guard", () => { for (const [name, path] of nodeEntryPoints) { @@ -19,3 +35,311 @@ describe("Bun runtime guard", () => { }); } }); + +describe("dependency bootstrap", () => { + const directories: string[] = []; + afterEach(() => { + for (const directory of directories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } + }); + + function fixture() { + const directory = mkdtempSync(join(tmpdir(), "pstack-bootstrap-")); + directories.push(directory); + const pluginDirectory = join(directory, "plugins", "pstack"); + const scriptsDirectory = join(pluginDirectory, "skills", "poteto-mode", "scripts"); + const configPath = join(directory, "bunfig.toml"); + writeFileSync(configPath, ""); + cpSync(import.meta.dir, scriptsDirectory, { + recursive: true, + filter: (source) => basename(source) !== "node_modules", + }); + const environment: Record = { + ...process.env, + HOME: join(directory, "home"), + USERPROFILE: join(directory, "home"), + LOCALAPPDATA: join(directory, "local"), + XDG_CONFIG_HOME: join(directory, "config"), + XDG_CACHE_HOME: join(directory, "cache"), + BUN_INSTALL: join(directory, "bun"), + BUN_INSTALL_CACHE_DIR: bunPackageCache, + }; + delete environment.NODE_PATH; + delete environment.NODE_OPTIONS; + delete environment.BUN_OPTIONS; + + function command(args = ["--help"], entry = join("watch-pr", "watch-pr")) { + return [ + process.execPath, + "--no-env-file", + `--config=${configPath}`, + "--no-install", + join(scriptsDirectory, entry), + ...args, + ]; + } + + return { + directory, + pluginDirectory, + scriptsDirectory, + configPath, + environment, + command, + cacheRoot: join(directory, "cache", "open-pstack", "poteto-mode-tools"), + run(args = ["--help"], entry = join("watch-pr", "watch-pr"), input = "") { + const result = Bun.spawnSync(command(args, entry), { + cwd: directory, + env: environment, + stdin: Buffer.from(input), + }); + return { + exitCode: result.exitCode, + stdout: result.stdout.toString(), + stderr: result.stderr.toString(), + }; + }, + }; + } + + const helpResult = { + exitCode: 0, + stdout: expect.stringContaining("Usage: watch-pr [options]"), + stderr: "", + }; + + it("runs copied watch-pr without installing dependencies inside the plugin", () => { + const test = fixture(); + const initialEntries = readdirSync(test.pluginDirectory, { recursive: true }).sort(); + + for (let attempt = 0; attempt < 2; attempt++) { + expect(test.run()).toEqual(helpResult); + } + + expect(existsSync(join(test.scriptsDirectory, "node_modules"))).toBe(false); + expect(readdirSync(test.pluginDirectory, { recursive: true }).sort()).toEqual(initialEntries); + }, 60_000); + + it("publishes one complete cache for concurrent cold starts", async () => { + const test = fixture(); + const children = Array.from({ length: 4 }, () => Bun.spawn(test.command(), { + cwd: test.directory, + env: test.environment, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + })); + const results = await Promise.all(children.map(async (child) => { + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + return { exitCode, stdout, stderr }; + })); + + for (const result of results) expect(result).toEqual(helpResult); + expect(readdirSync(test.cacheRoot)).toHaveLength(1); + expect(existsSync(join(test.scriptsDirectory, "node_modules"))).toBe(false); + expect(test.run()).toEqual(helpResult); + }, 60_000); + + it("loads the cached dependency instead of an incompatible ancestor package", () => { + const test = fixture(); + const ancestor = join(test.directory, "node_modules", "commander"); + mkdirSync(ancestor, { recursive: true }); + writeFileSync(join(ancestor, "package.json"), '{"name":"commander","version":"0.0.0","main":"index.js"}'); + writeFileSync(join(ancestor, "index.js"), 'throw new Error("wrong commander");\n'); + + expect(test.run()).toEqual(helpResult); + expect(existsSync(join(test.scriptsDirectory, "node_modules"))).toBe(false); + }, 60_000); + + it("accepts a cache path containing the platform path-list delimiter", () => { + const test = fixture(); + test.environment.XDG_CACHE_HOME = join(test.directory, `cache${delimiter}path`); + + expect(test.run()).toEqual(helpResult); + expect(existsSync(join(test.scriptsDirectory, "node_modules"))).toBe(false); + }, 60_000); + + it("uses fresh caches when either the manifest or frozen lockfile changes", () => { + const test = fixture(); + expect(test.run()).toEqual(helpResult); + const originalCache = join(test.cacheRoot, readdirSync(test.cacheRoot)[0]!); + const manifestPath = join(test.scriptsDirectory, "package.json"); + const lockfilePath = join(test.scriptsDirectory, "bun.lock"); + const originalManifest = readFileSync(manifestPath, "utf8"); + const originalLockfile = readFileSync(lockfilePath, "utf8"); + + writeFileSync(manifestPath, `${originalManifest}\n`); + expect(test.run()).toEqual(helpResult); + expect(readdirSync(test.cacheRoot)).toHaveLength(2); + + writeFileSync(lockfilePath, `${originalLockfile}\n`); + expect(test.run()).toEqual(helpResult); + expect(readdirSync(test.cacheRoot)).toHaveLength(3); + expect(readFileSync(join(originalCache, "package.json"), "utf8")).toBe(originalManifest); + expect(readFileSync(join(originalCache, "bun.lock"), "utf8")).toBe(originalLockfile); + }, 60_000); + + it("preserves arguments, runtime flags, cwd, streams, status, and existing NODE_PATH", () => { + const test = fixture(); + const externalPaths = [join(test.directory, "first modules"), join(test.directory, "second modules")]; + const externalPackage = join(externalPaths[1]!, "bootstrap-external-probe"); + mkdirSync(externalPackage, { recursive: true }); + writeFileSync(join(externalPackage, "package.json"), JSON.stringify({ + type: "module", + exports: "./index.js", + })); + writeFileSync(join(externalPackage, "index.js"), 'export default "external dependency";\n'); + test.environment.NODE_PATH = externalPaths.join(delimiter); + writeFileSync(join(test.scriptsDirectory, "probe.ts"), ` +import { ensureDependenciesInstalled } from "./bootstrap.ts"; +ensureDependenciesInstalled(); +const { Command } = await import("commander"); +console.log(JSON.stringify({ + name: new Command("probe").name(), + args: process.argv.slice(2), + execArgv: process.execArgv, + cwd: process.cwd(), + input: await Bun.stdin.text(), + nodePath: process.env.NODE_PATH, + external: (await import("bootstrap-external-probe")).default, +})); +console.error("probe stderr"); +process.exit(23); +`); + + const result = test.run(["--value", "two words", "-"], "probe.ts", "stdin payload\n"); + expect(result.exitCode).toBe(23); + expect(result.stderr).toBe("probe stderr\n"); + const observed = JSON.parse(result.stdout); + expect(observed).toMatchObject({ + name: "probe", + args: ["--value", "two words", "-"], + execArgv: ["--no-env-file", `--config=${test.configPath}`, "--no-install"], + cwd: realpathSync(test.directory), + input: "stdin payload\n", + external: "external dependency", + }); + expect(observed.nodePath).toBe(externalPaths.join(delimiter)); + expect(existsSync(join(test.scriptsDirectory, "node_modules"))).toBe(false); + }, 60_000); + + it("reports an incomplete published cache without replacing or deleting it", () => { + const test = fixture(); + expect(test.run()).toEqual(helpResult); + const cacheDirectory = join(test.cacheRoot, readdirSync(test.cacheRoot)[0]!); + rmSync(join(cacheDirectory, "node_modules", "commander"), { recursive: true }); + const entries = readdirSync(test.cacheRoot, { recursive: true }).sort(); + + const result = test.run(); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain(`Incomplete poteto-mode dependency cache at ${cacheDirectory}`); + expect(result.stderr).toContain("Remove that cache directory and re-run."); + expect(readdirSync(test.cacheRoot, { recursive: true }).sort()).toEqual(entries); + expect(existsSync(join(test.scriptsDirectory, "node_modules"))).toBe(false); + }, 60_000); + + it("retains frozen-install diagnostics and removes only failed staging data", () => { + const test = fixture(); + expect(test.run()).toEqual(helpResult); + const publishedCaches = readdirSync(test.cacheRoot); + writeFileSync(join(test.scriptsDirectory, "bun.lock"), "invalid lockfile\n"); + + const result = test.run(); + expect(result.exitCode).toBe(1); + expect(result.stdout).toContain("bun install"); + expect(result.stderr).toContain("lockfile"); + expect(result.stderr).toContain("bun install --frozen-lockfile exited with status 1"); + expect(readdirSync(test.cacheRoot)).toEqual(publishedCaches); + expect(existsSync(join(test.scriptsDirectory, "node_modules"))).toBe(false); + }, 60_000); + + it("keeps the working directory selected by a relative Bun --cwd flag", () => { + const test = fixture(); + const launchDirectory = join(test.directory, "work", "start"); + mkdirSync(launchDirectory, { recursive: true }); + writeFileSync(join(test.scriptsDirectory, "cwd-probe.ts"), ` +import { ensureDependenciesInstalled } from "./bootstrap.ts"; +ensureDependenciesInstalled(); +console.log(process.cwd()); +`); + const [bun, ...args] = test.command([], "cwd-probe.ts"); + + for (const flags of [["--cwd=.."], ["--cwd", ".."]]) { + const result = Bun.spawnSync([bun!, ...flags, ...args], { + cwd: launchDirectory, + env: test.environment, + }); + expect(result.exitCode).toBe(0); + expect(result.stderr.toString()).toBe(""); + expect(result.stdout.toString().trim()).toBe( + realpathSync(join(test.directory, "work")) + ); + } + }, 60_000); + + it("stops the running command when its process is terminated", async () => { + if (process.platform === "win32") return; + const test = fixture(); + expect(test.run()).toEqual(helpResult); + const readyPath = join(test.directory, "ready"); + test.environment.PSTACK_BOOTSTRAP_TEST_READY = readyPath; + writeFileSync(join(test.scriptsDirectory, "signal-probe.ts"), ` +import { writeFileSync } from "node:fs"; +import { ensureDependenciesInstalled } from "./bootstrap.ts"; +ensureDependenciesInstalled(); +writeFileSync(process.env.PSTACK_BOOTSTRAP_TEST_READY!, String(process.pid)); +setInterval(() => {}, 1000); +`); + const child = Bun.spawn(test.command([], "signal-probe.ts"), { + cwd: test.directory, + env: test.environment, + stdout: "ignore", + stderr: "pipe", + }); + let commandPid: number | undefined; + try { + for (let attempt = 0; attempt < 400 && !existsSync(readyPath); attempt++) { + if (child.exitCode !== null) break; + await Bun.sleep(25); + } + expect(existsSync(readyPath)).toBe(true); + commandPid = Number(readFileSync(readyPath, "utf8")); + child.kill("SIGTERM"); + await child.exited; + let running = true; + try { + process.kill(commandPid, 0); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; + running = false; + } + expect(running).toBe(false); + } finally { + if (child.exitCode === null) child.kill("SIGKILL"); + if (commandPid && commandPid !== child.pid) { + try { + process.kill(commandPid, "SIGKILL"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; + } + } + } + }, 60_000); + + it("uses a user cache when XDG_CACHE_HOME is absent", () => { + const test = fixture(); + delete test.environment.XDG_CACHE_HOME; + + expect(test.run()).toEqual(helpResult); + const cacheHome = process.platform === "win32" + ? test.environment.LOCALAPPDATA! + : join(test.environment.HOME!, ".cache"); + expect(readdirSync(join(cacheHome, "open-pstack", "poteto-mode-tools"))).toHaveLength(1); + expect(existsSync(join(test.scriptsDirectory, "node_modules"))).toBe(false); + }, 60_000); +}); diff --git a/plugins/pstack/skills/poteto-mode/scripts/bootstrap.ts b/plugins/pstack/skills/poteto-mode/scripts/bootstrap.ts index 64e86d42..fc97918d 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/bootstrap.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/bootstrap.ts @@ -1,6 +1,17 @@ import { createHash } from "node:crypto"; -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { createRequire } from "node:module"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + realpathSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join, resolve, sep } from "node:path"; if (typeof Bun === "undefined") { console.error( @@ -10,60 +21,93 @@ if (typeof Bun === "undefined") { } const scriptsDirectory = import.meta.dir; -const nodeModulesDirectory = join(scriptsDirectory, "node_modules"); -const commanderPackagePath = join( - nodeModulesDirectory, - "commander", - "package.json" -); -const installKeyPath = join( - nodeModulesDirectory, - ".poteto-mode-tools-install-key" -); -function currentInstallKey(): string { - return createHash("sha256") - .update(readFileSync(join(scriptsDirectory, "package.json"))) - .update("\0") - .update(readFileSync(join(scriptsDirectory, "bun.lock"))) - .digest("hex"); +function dependencyCacheRoot(): string { + const cacheHome = + process.env.XDG_CACHE_HOME || + (process.platform === "win32" + ? process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local") + : join(homedir(), ".cache")); + return resolve(cacheHome, "open-pstack", "poteto-mode-tools"); } -export function ensureDependenciesInstalled(): void { - const installKey = currentInstallKey(); - if ( - existsSync(commanderPackagePath) && - existsSync(installKeyPath) && - readFileSync(installKeyPath, "utf8").trim() === installKey - ) { - return; +function hasCommander(directory: string): boolean { + try { + const entry = Bun.resolveSync("commander", directory); + const commanderDirectory = realpathSync( + join(directory, "node_modules", "commander") + ); + return realpathSync(entry).startsWith(commanderDirectory + sep); + } catch { + return false; } +} - const result = Bun.spawnSync( - [process.execPath, "install", "--frozen-lockfile"], - { cwd: scriptsDirectory } - ); - if (result.exitCode !== 0) { - process.stdout.write(result.stdout); - process.stderr.write(result.stderr); - throw new Error( - `bun install --frozen-lockfile exited with status ${result.exitCode}` +function installDependencies( + cacheDirectory: string, + packageJson: Buffer, + lockfile: Buffer +): void { + const cacheRoot = dirname(cacheDirectory); + mkdirSync(cacheRoot, { recursive: true }); + const stagingDirectory = mkdtempSync(join(cacheRoot, ".staging-")); + try { + writeFileSync(join(stagingDirectory, "package.json"), packageJson); + writeFileSync(join(stagingDirectory, "bun.lock"), lockfile); + const result = Bun.spawnSync( + [process.execPath, "install", "--frozen-lockfile"], + { cwd: stagingDirectory } ); + if (result.exitCode !== 0) { + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + throw new Error( + `bun install --frozen-lockfile exited with status ${result.exitCode}` + ); + } + if (!hasCommander(stagingDirectory)) { + throw new Error( + "bun install --frozen-lockfile completed without installing commander" + ); + } + + try { + renameSync(stagingDirectory, cacheDirectory); + } catch (error) { + if (!existsSync(cacheDirectory)) throw error; + } + } finally { + rmSync(stagingDirectory, { recursive: true, force: true }); } - if (!existsSync(commanderPackagePath)) { +} + +export function ensureDependenciesInstalled(): void { + const packageJson = readFileSync(join(scriptsDirectory, "package.json")); + const lockfile = readFileSync(join(scriptsDirectory, "bun.lock")); + const installKey = createHash("sha256") + .update(packageJson) + .update("\0") + .update(lockfile) + .digest("hex"); + const cacheDirectory = join(dependencyCacheRoot(), installKey); + if (!existsSync(cacheDirectory)) { + installDependencies(cacheDirectory, packageJson, lockfile); + } + if (!hasCommander(cacheDirectory)) { throw new Error( - "bun install --frozen-lockfile completed without installing commander" + `Incomplete poteto-mode dependency cache at ${cacheDirectory}. Remove that cache directory and re-run.` ); } - writeFileSync(installKeyPath, `${installKey}\n`); - - const restarted = Bun.spawnSync([process.execPath, ...process.argv.slice(1)], { - cwd: process.cwd(), - env: process.env, - stdin: "inherit", - stdout: "inherit", - stderr: "inherit", + const load = createRequire(join(cacheDirectory, "package.json")); + const commander = load("commander"); + Bun.plugin({ + name: "pstack-commander", + setup(builder) { + builder.module("commander", () => ({ + loader: "object", + exports: commander, + })); + }, }); - process.exit(restarted.exitCode ?? 1); }