diff --git a/docs/superpowers/plans/2026-06-04-pluginos-connection-foundation.md b/docs/superpowers/plans/2026-06-04-pluginos-connection-foundation.md new file mode 100644 index 0000000..4c8d604 --- /dev/null +++ b/docs/superpowers/plans/2026-06-04-pluginos-connection-foundation.md @@ -0,0 +1,1978 @@ +# PluginOS Connection Foundation (PR-A1) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Eliminate orphan `pluginos` server processes via cross-platform singleton enforcement + state-file discovery, and add a `wait_for_reconnect` MCP tool for graceful mid-script reconnects. + +**Architecture:** A new `singleton/` module in `mcp-server` acquires a lockfile at startup, reaps the prior server (SIGTERM → 1s grace → SIGKILL), and writes `~/.pluginos/state.json` for bridge discovery. The bridge plugin probes each port via HTTP `/state.json` and ranks candidates by `parentAlive` + `startedAt`. The `wait_for_reconnect` tool polls `IPluginBridge.isConnected()` until success or timeout. + +**Tech Stack:** Node.js `fs` for atomic file writes, `process.kill(pid, signal)` for liveness checks and signaling, `fetch` (native in Node 18+ / Figma sandbox) for bridge-side HTTP probes, Vitest for unit + integration tests, `child_process.fork()` for the two-process integration test. + +**Spec:** [docs/superpowers/specs/2026-06-04-pluginos-connection-foundation-design.md](../specs/2026-06-04-pluginos-connection-foundation-design.md) + +--- + +## File Map + +**Create (server side):** +- `packages/mcp-server/src/singleton/lockfile.ts` — acquireLock / releaseLock primitives +- `packages/mcp-server/src/singleton/pid-file.ts` — atomic PID file r/w +- `packages/mcp-server/src/singleton/takeover.ts` — SIGTERM → poll → SIGKILL sequence +- `packages/mcp-server/src/singleton/state-file.ts` — state.json write/read + parent-alive heartbeat +- `packages/mcp-server/src/singleton/index.ts` — orchestrator +- `packages/mcp-server/src/singleton/types.ts` — shared types (`StateFile`, `SingletonInfo`) +- `packages/mcp-server/src/singleton/__tests__/lockfile.test.ts` +- `packages/mcp-server/src/singleton/__tests__/pid-file.test.ts` +- `packages/mcp-server/src/singleton/__tests__/takeover.test.ts` +- `packages/mcp-server/src/singleton/__tests__/state-file.test.ts` +- `packages/mcp-server/src/singleton/__tests__/integration.test.ts` — two-process test via `child_process.fork` +- `packages/mcp-server/src/__tests__/http-state-endpoint.test.ts` +- `packages/mcp-server/src/__tests__/wait-for-reconnect.test.ts` + +**Create (bridge side):** +- `packages/bridge-plugin/src/discovery.ts` — `fetchStateJson` + `StateFile` type + ranking +- `packages/bridge-plugin/src/__tests__/discovery.test.ts` + +**Modify:** +- `packages/mcp-server/src/index.ts` — call `acquireSingletonLock()` before `wsServer.start()`, register shutdown handlers, start parent-liveness interval +- `packages/mcp-server/src/http-server.ts` — add `GET /state.json` route +- `packages/mcp-server/src/server.ts` — register `wait_for_reconnect` tool +- `packages/bridge-plugin/src/ui-entry.ts` — modify `connect()` to use ranked discovery before scan +- `packages/claude-plugin/skills/pluginos-figma/SKILL.md` — append `wait_for_reconnect` troubleshooting note via `sync-recipes` (or direct edit if it's outside the autogen block) + +--- + +## Conventions + +- All commits use `Skill(commit-commands:commit)` — never write commit messages manually +- After every passing test, run the workspace-scoped test command and read the FULL output before claiming pass +- Tests use Vitest; existing patterns in `packages/mcp-server/src/__tests__/` are the reference +- Tests for filesystem code use temp dirs via `os.tmpdir()` + `crypto.randomUUID()` per test to avoid cross-test interference +- Tests for parent-liveness inject the PID-check function rather than calling `process.kill` directly, so the test doesn't depend on real PIDs +- Build order if needed: `npm run build:shared` before mcp-server work (rare for this PR — no shared changes) +- Push only after the full PR is ready; all work lands on branch `feat/pr-a1-connection-foundation` (created in Task 0) + +--- + +## Task 0: Set up the feature branch + +**Files:** None — git only + +- [ ] **Step 1: Confirm clean starting state** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && git status && git branch --show-current` +Expected: clean tree, on `main`. + +- [ ] **Step 2: Create and switch to feature branch** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && git checkout -b feat/pr-a1-connection-foundation` +Expected: `Switched to a new branch 'feat/pr-a1-connection-foundation'`. + +--- + +## Task 1: Singleton types module + +**Files:** +- Create: `packages/mcp-server/src/singleton/types.ts` + +- [ ] **Step 1: Write types module** + +```typescript +// packages/mcp-server/src/singleton/types.ts + +export interface StateFile { + version: 1; + pid: number; + port: number; + serverVersion: string; + startedAt: number; + parentPid: number; + parentAlive: boolean; + socketPath: string | null; +} + +export interface SingletonInfo { + takeoverFromPid?: number; + stateDir: string; + pidFilePath: string; + stateFilePath: string; + lockFilePath: string; +} + +export interface LockAcquisition { + acquired: boolean; + oldPid: number | null; +} +``` + +- [ ] **Step 2: Typecheck** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm run typecheck` +Expected: no errors. + +- [ ] **Step 3: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(mcp-server): add singleton types module`. + +--- + +## Task 2: Lockfile primitive (TDD) + +**Files:** +- Create: `packages/mcp-server/src/singleton/lockfile.ts` +- Create: `packages/mcp-server/src/singleton/__tests__/lockfile.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/singleton/__tests__/lockfile.test.ts +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { acquireLock, releaseLock } from "../lockfile.js"; + +describe("lockfile primitive", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "pluginos-lock-test-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("acquires a lock on a fresh path", async () => { + const lockPath = join(dir, "server.pid.lock"); + const result = await acquireLock(lockPath); + expect(result.acquired).toBe(true); + expect(result.oldPid).toBeNull(); + }); + + it("fails to acquire when held by a live PID", async () => { + const lockPath = join(dir, "server.pid.lock"); + await acquireLock(lockPath); + const result = await acquireLock(lockPath, { maxRetries: 1, retryDelayMs: 10 }); + expect(result.acquired).toBe(false); + expect(result.oldPid).toBe(process.pid); + }); + + it("releases the lock", async () => { + const lockPath = join(dir, "server.pid.lock"); + await acquireLock(lockPath); + await releaseLock(lockPath); + const result = await acquireLock(lockPath); + expect(result.acquired).toBe(true); + }); + + it("treats a lockfile with a dead PID as stale and takes over", async () => { + const lockPath = join(dir, "server.pid.lock"); + // Manually write a fake dead PID — use a very high PID unlikely to exist + const { writeFileSync } = await import("node:fs"); + writeFileSync(lockPath, "999999999"); + const result = await acquireLock(lockPath, { maxRetries: 1, retryDelayMs: 10 }); + expect(result.acquired).toBe(true); + expect(result.oldPid).toBe(999999999); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/lockfile` +Expected: FAIL (module not found). + +- [ ] **Step 3: Implement the lockfile primitive** + +```typescript +// packages/mcp-server/src/singleton/lockfile.ts +import { open, readFile, unlink } from "node:fs/promises"; +import type { LockAcquisition } from "./types.js"; + +export interface AcquireOptions { + maxRetries?: number; + retryDelayMs?: number; +} + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (err) { + return (err as NodeJS.ErrnoException).code === "EPERM"; + } +} + +async function readPidFromLockfile(path: string): Promise { + try { + const content = (await readFile(path, "utf8")).trim(); + const pid = Number.parseInt(content, 10); + return Number.isFinite(pid) ? pid : null; + } catch { + return null; + } +} + +export async function acquireLock( + path: string, + opts: AcquireOptions = {} +): Promise { + const maxRetries = opts.maxRetries ?? 5; + const retryDelayMs = opts.retryDelayMs ?? 200; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const fh = await open(path, "wx"); + await fh.write(String(process.pid)); + await fh.close(); + return { acquired: true, oldPid: null }; + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err; + + const oldPid = await readPidFromLockfile(path); + if (oldPid !== null && !isProcessAlive(oldPid)) { + // Stale lock — remove and retry + try { + await unlink(path); + } catch { + // race with another process — proceed + } + continue; + } + + if (attempt === maxRetries) { + return { acquired: false, oldPid }; + } + await new Promise((r) => setTimeout(r, retryDelayMs)); + } + } + + return { acquired: false, oldPid: null }; +} + +export async function releaseLock(path: string): Promise { + try { + await unlink(path); + } catch { + // best-effort + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/lockfile` +Expected: 4 passed. + +- [ ] **Step 5: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(mcp-server): add lockfile primitive with stale-PID detection`. + +--- + +## Task 3: PID file r/w (TDD) + +**Files:** +- Create: `packages/mcp-server/src/singleton/pid-file.ts` +- Create: `packages/mcp-server/src/singleton/__tests__/pid-file.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/singleton/__tests__/pid-file.test.ts +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { writePidFile, readPidFile, removePidFile } from "../pid-file.js"; + +describe("pid-file r/w", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "pluginos-pid-test-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("writes a pid atomically (tmp + rename)", async () => { + const path = join(dir, "server.pid"); + await writePidFile(path, 12345); + const read = await readPidFile(path); + expect(read).toBe(12345); + }); + + it("returns null for a missing file", async () => { + const path = join(dir, "missing.pid"); + expect(await readPidFile(path)).toBeNull(); + }); + + it("returns null for a corrupt file", async () => { + const path = join(dir, "corrupt.pid"); + await writeFile(path, "not-a-number"); + expect(await readPidFile(path)).toBeNull(); + }); + + it("removes the pid file", async () => { + const path = join(dir, "server.pid"); + await writePidFile(path, 42); + await removePidFile(path); + expect(await readPidFile(path)).toBeNull(); + }); + + it("remove is a no-op when file is missing", async () => { + const path = join(dir, "missing.pid"); + await expect(removePidFile(path)).resolves.toBeUndefined(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/pid-file` +Expected: FAIL. + +- [ ] **Step 3: Implement the pid-file module** + +```typescript +// packages/mcp-server/src/singleton/pid-file.ts +import { writeFile, readFile, rename, unlink } from "node:fs/promises"; + +export async function writePidFile(path: string, pid: number): Promise { + const tmp = `${path}.tmp`; + await writeFile(tmp, String(pid)); + await rename(tmp, path); +} + +export async function readPidFile(path: string): Promise { + try { + const content = (await readFile(path, "utf8")).trim(); + const pid = Number.parseInt(content, 10); + return Number.isFinite(pid) ? pid : null; + } catch { + return null; + } +} + +export async function removePidFile(path: string): Promise { + try { + await unlink(path); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/pid-file` +Expected: 5 passed. + +- [ ] **Step 5: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(mcp-server): add pid-file r/w with atomic write`. + +--- + +## Task 4: Takeover sequence (TDD) + +**Files:** +- Create: `packages/mcp-server/src/singleton/takeover.ts` +- Create: `packages/mcp-server/src/singleton/__tests__/takeover.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/singleton/__tests__/takeover.test.ts +import { describe, it, expect, vi } from "vitest"; +import { reapProcess } from "../takeover.js"; + +describe("reapProcess", () => { + it("sends SIGTERM and returns true when process exits within grace", async () => { + const calls: Array<[number, NodeJS.Signals | 0]> = []; + let alive = true; + const kill = vi.fn((pid: number, sig: NodeJS.Signals | 0) => { + calls.push([pid, sig]); + if (sig === "SIGTERM") { + // Simulate the process dying 50ms after SIGTERM + setTimeout(() => { + alive = false; + }, 50); + } + if (sig === 0 && !alive) { + const e = new Error("ESRCH") as NodeJS.ErrnoException; + e.code = "ESRCH"; + throw e; + } + return true; + }); + const result = await reapProcess(12345, { kill, graceMs: 500, pollMs: 25 }); + expect(result.reaped).toBe(true); + expect(result.usedSignal).toBe("SIGTERM"); + expect(calls.some(([, s]) => s === "SIGTERM")).toBe(true); + expect(calls.some(([, s]) => s === "SIGKILL")).toBe(false); + }); + + it("escalates to SIGKILL when SIGTERM doesn't take", async () => { + const calls: Array<[number, NodeJS.Signals | 0]> = []; + const kill = vi.fn((pid: number, sig: NodeJS.Signals | 0) => { + calls.push([pid, sig]); + // Process never dies on SIGTERM. SIGKILL kills it. + return true; + }); + const result = await reapProcess(12345, { kill, graceMs: 100, pollMs: 25 }); + expect(result.reaped).toBe(true); + expect(result.usedSignal).toBe("SIGKILL"); + expect(calls.some(([, s]) => s === "SIGTERM")).toBe(true); + expect(calls.some(([, s]) => s === "SIGKILL")).toBe(true); + }); + + it("returns reaped=false if the process never dies even after SIGKILL", async () => { + const kill = vi.fn(() => true); + const result = await reapProcess(12345, { kill, graceMs: 50, pollMs: 25, postKillWaitMs: 50 }); + expect(result.reaped).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/takeover` +Expected: FAIL. + +- [ ] **Step 3: Implement the takeover sequence** + +```typescript +// packages/mcp-server/src/singleton/takeover.ts + +export interface ReapOptions { + kill?: (pid: number, signal: NodeJS.Signals | 0) => boolean; + graceMs?: number; + pollMs?: number; + postKillWaitMs?: number; +} + +export interface ReapResult { + reaped: boolean; + usedSignal: NodeJS.Signals | null; +} + +function defaultKill(pid: number, signal: NodeJS.Signals | 0): boolean { + return process.kill(pid, signal as NodeJS.Signals); +} + +function isAlive(pid: number, kill: (pid: number, signal: 0) => boolean): boolean { + try { + kill(pid, 0); + return true; + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code === "EPERM") return true; + if (code === "ESRCH") return false; + return false; + } +} + +async function pollUntilDead( + pid: number, + kill: (pid: number, signal: 0) => boolean, + timeoutMs: number, + pollMs: number +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (!isAlive(pid, kill)) return true; + await new Promise((r) => setTimeout(r, pollMs)); + } + return !isAlive(pid, kill); +} + +export async function reapProcess(pid: number, opts: ReapOptions = {}): Promise { + const kill = opts.kill ?? defaultKill; + const graceMs = opts.graceMs ?? 1000; + const pollMs = opts.pollMs ?? 100; + const postKillWaitMs = opts.postKillWaitMs ?? 200; + + try { + kill(pid, "SIGTERM"); + } catch { + // process may already be dead — that's fine + } + + const diedFromSigterm = await pollUntilDead( + pid, + (p, s) => kill(p, s as NodeJS.Signals | 0), + graceMs, + pollMs + ); + if (diedFromSigterm) { + return { reaped: true, usedSignal: "SIGTERM" }; + } + + try { + kill(pid, "SIGKILL"); + } catch { + // proceed + } + const diedFromSigkill = await pollUntilDead( + pid, + (p, s) => kill(p, s as NodeJS.Signals | 0), + postKillWaitMs, + pollMs + ); + return { reaped: diedFromSigkill, usedSignal: diedFromSigkill ? "SIGKILL" : null }; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/takeover` +Expected: 3 passed. + +- [ ] **Step 5: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(mcp-server): add process takeover (SIGTERM → grace → SIGKILL)`. + +--- + +## Task 5: state.json writer (TDD) + +**Files:** +- Create: `packages/mcp-server/src/singleton/state-file.ts` +- Create: `packages/mcp-server/src/singleton/__tests__/state-file.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/singleton/__tests__/state-file.test.ts +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + buildStateFile, + writeStateFile, + readStateFile, + removeStateFile, +} from "../state-file.js"; +import type { StateFile } from "../types.js"; + +describe("state-file", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "pluginos-state-test-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("builds a state object with required fields", () => { + const state = buildStateFile({ + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + parentPid: 99, + parentAlive: true, + }); + expect(state.version).toBe(1); + expect(state.pid).toBe(1234); + expect(state.port).toBe(9500); + expect(state.serverVersion).toBe("0.4.3"); + expect(state.parentPid).toBe(99); + expect(state.parentAlive).toBe(true); + expect(state.socketPath).toBeNull(); + expect(typeof state.startedAt).toBe("number"); + }); + + it("writes atomically (tmp + rename) and reads back", async () => { + const path = join(dir, "state.json"); + const state: StateFile = buildStateFile({ + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + parentPid: 99, + parentAlive: true, + }); + await writeStateFile(path, state); + const read = await readStateFile(path); + expect(read).toEqual(state); + }); + + it("reads return null for missing files", async () => { + expect(await readStateFile(join(dir, "missing.json"))).toBeNull(); + }); + + it("reads return null for malformed files", async () => { + const path = join(dir, "malformed.json"); + await writeFile(path, "not-json"); + expect(await readStateFile(path)).toBeNull(); + }); + + it("reads return null for state with wrong version", async () => { + const path = join(dir, "future.json"); + await writeFile(path, JSON.stringify({ version: 999, pid: 1, port: 9500 })); + expect(await readStateFile(path)).toBeNull(); + }); + + it("removes the file", async () => { + const path = join(dir, "state.json"); + const state = buildStateFile({ + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + parentPid: 99, + parentAlive: true, + }); + await writeStateFile(path, state); + await removeStateFile(path); + expect(await readStateFile(path)).toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/state-file` +Expected: FAIL. + +- [ ] **Step 3: Implement state-file** + +```typescript +// packages/mcp-server/src/singleton/state-file.ts +import { writeFile, readFile, rename, unlink } from "node:fs/promises"; +import type { StateFile } from "./types.js"; + +export interface BuildStateInput { + pid: number; + port: number; + serverVersion: string; + parentPid: number; + parentAlive: boolean; +} + +export function buildStateFile(input: BuildStateInput): StateFile { + return { + version: 1, + pid: input.pid, + port: input.port, + serverVersion: input.serverVersion, + startedAt: Date.now(), + parentPid: input.parentPid, + parentAlive: input.parentAlive, + socketPath: null, + }; +} + +export async function writeStateFile(path: string, state: StateFile): Promise { + const tmp = `${path}.tmp`; + await writeFile(tmp, JSON.stringify(state, null, 2)); + await rename(tmp, path); +} + +export async function readStateFile(path: string): Promise { + try { + const raw = await readFile(path, "utf8"); + const parsed = JSON.parse(raw) as unknown; + if ( + typeof parsed === "object" && + parsed !== null && + (parsed as { version?: unknown }).version === 1 + ) { + return parsed as StateFile; + } + return null; + } catch { + return null; + } +} + +export async function removeStateFile(path: string): Promise { + try { + await unlink(path); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/state-file` +Expected: 6 passed. + +- [ ] **Step 5: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(mcp-server): add state.json read/write helpers`. + +--- + +## Task 6: Singleton orchestrator (TDD) + +**Files:** +- Create: `packages/mcp-server/src/singleton/index.ts` +- Modify: `packages/mcp-server/src/singleton/__tests__/lockfile.test.ts` (no — separate test file) +- Create: `packages/mcp-server/src/singleton/__tests__/orchestrator.test.ts` + +The orchestrator ties the four primitives together. Tests use mocked primitives via dependency injection. + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/singleton/__tests__/orchestrator.test.ts +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { acquireSingletonLock } from "../index.js"; + +describe("acquireSingletonLock", () => { + let stateDir: string; + + beforeEach(async () => { + stateDir = await mkdtemp(join(tmpdir(), "pluginos-orch-test-")); + }); + + afterEach(async () => { + await rm(stateDir, { recursive: true, force: true }); + }); + + it("acquires on a fresh dir with no prior server", async () => { + const info = await acquireSingletonLock({ stateDir }); + expect(info.takeoverFromPid).toBeUndefined(); + expect(info.stateDir).toBe(stateDir); + expect(info.pidFilePath).toBe(join(stateDir, "server.pid")); + expect(info.stateFilePath).toBe(join(stateDir, "state.json")); + expect(info.lockFilePath).toBe(join(stateDir, "server.pid.lock")); + }); + + it("reaps a stale PID and reports takeoverFromPid", async () => { + // Pre-write a server.pid for a dead PID (very high, unlikely to exist) + await writeFile(join(stateDir, "server.pid"), "999999998"); + const info = await acquireSingletonLock({ stateDir }); + expect(info.takeoverFromPid).toBe(999999998); + }); + + it("creates the state dir if missing", async () => { + const missingDir = join(stateDir, "nested", "pluginos"); + const info = await acquireSingletonLock({ stateDir: missingDir }); + expect(info.stateDir).toBe(missingDir); + }); + + it("returns a degraded info object when the state dir is not writable", async () => { + // Hard to test cross-platform; use an obviously unwritable path + const badDir = "/dev/null/not-a-dir"; + const info = await acquireSingletonLock({ stateDir: badDir }); + // We expect it to NOT throw — the orchestrator should swallow and continue + expect(info.stateDir).toBe(badDir); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/orchestrator` +Expected: FAIL. + +- [ ] **Step 3: Implement the orchestrator** + +```typescript +// packages/mcp-server/src/singleton/index.ts +import { mkdir, chmod } from "node:fs/promises"; +import { join } from "node:path"; +import { homedir } from "node:os"; +import { acquireLock, releaseLock } from "./lockfile.js"; +import { readPidFile, writePidFile, removePidFile } from "./pid-file.js"; +import { reapProcess } from "./takeover.js"; +import { writeStateFile, removeStateFile, buildStateFile } from "./state-file.js"; +import type { SingletonInfo, StateFile } from "./types.js"; + +export { buildStateFile, writeStateFile, readStateFile, removeStateFile } from "./state-file.js"; +export { reapProcess } from "./takeover.js"; +export type { StateFile, SingletonInfo } from "./types.js"; + +export interface AcquireOptions { + stateDir?: string; +} + +function defaultStateDir(): string { + return process.env.PLUGINOS_STATE_DIR ?? join(homedir(), ".pluginos"); +} + +export async function acquireSingletonLock(opts: AcquireOptions = {}): Promise { + const stateDir = opts.stateDir ?? defaultStateDir(); + const pidFilePath = join(stateDir, "server.pid"); + const stateFilePath = join(stateDir, "state.json"); + const lockFilePath = join(stateDir, "server.pid.lock"); + + try { + await mkdir(stateDir, { recursive: true }); + await chmod(stateDir, 0o700).catch(() => { + // chmod can fail on Windows or special FS — ignore + }); + } catch (err) { + console.error( + `[singleton] Failed to create state dir ${stateDir}: ${(err as Error).message}. Continuing in degraded mode.` + ); + return { stateDir, pidFilePath, stateFilePath, lockFilePath }; + } + + const lock = await acquireLock(lockFilePath); + if (!lock.acquired) { + console.error( + `[singleton] Could not acquire lock at ${lockFilePath} after retries — proceeding without singleton enforcement.` + ); + return { stateDir, pidFilePath, stateFilePath, lockFilePath }; + } + + let takeoverFromPid: number | undefined; + const oldPid = await readPidFile(pidFilePath); + if (oldPid !== null && isProcessAlive(oldPid)) { + const result = await reapProcess(oldPid); + if (result.reaped) { + takeoverFromPid = oldPid; + console.error( + `[singleton] Reaped PID ${oldPid} (signal: ${result.usedSignal}). Took over.` + ); + } else { + console.error( + `[singleton] Could not reap PID ${oldPid} — proceeding anyway. Port collision may occur.` + ); + } + } else if (oldPid !== null) { + takeoverFromPid = oldPid; + console.error(`[singleton] Found stale PID file (${oldPid} not alive). Took over.`); + } + + await releaseLock(lockFilePath); + return { takeoverFromPid, stateDir, pidFilePath, stateFilePath, lockFilePath }; +} + +export async function writeSingletonState(info: SingletonInfo, state: StateFile): Promise { + try { + await writePidFile(info.pidFilePath, state.pid); + await writeStateFile(info.stateFilePath, state); + } catch (err) { + console.error(`[singleton] Failed to write state files: ${(err as Error).message}`); + } +} + +export async function clearSingletonState(info: SingletonInfo): Promise { + await Promise.allSettled([removeStateFile(info.stateFilePath), removePidFile(info.pidFilePath)]); +} + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (err) { + return (err as NodeJS.ErrnoException).code === "EPERM"; + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/orchestrator` +Expected: 4 passed. + +- [ ] **Step 5: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(mcp-server): add singleton orchestrator (acquire + writeState + clearState)`. + +--- + +## Task 7: Two-process integration test + +**Files:** +- Create: `packages/mcp-server/src/singleton/__tests__/integration.test.ts` +- Create: `packages/mcp-server/src/singleton/__tests__/fixtures/mock-server.mjs` + +This is the test that validates the whole singleton lifecycle end-to-end. We spawn two real Node processes that each call `acquireSingletonLock`, and assert the second reaps the first. + +- [ ] **Step 1: Write the mock-server fixture** + +```javascript +// packages/mcp-server/src/singleton/__tests__/fixtures/mock-server.mjs +import { acquireSingletonLock, writeSingletonState, buildStateFile, clearSingletonState } from "../../index.js"; + +const stateDir = process.env.PLUGINOS_STATE_DIR; + +async function main() { + const info = await acquireSingletonLock({ stateDir }); + const state = buildStateFile({ + pid: process.pid, + port: 9500, + serverVersion: "test", + parentPid: process.ppid, + parentAlive: true, + }); + await writeSingletonState(info, state); + + // Notify parent we're ready + if (process.send) process.send({ ready: true, takeoverFromPid: info.takeoverFromPid }); + + // Handle takeover by exiting cleanly on SIGTERM + process.on("SIGTERM", async () => { + await clearSingletonState(info); + process.exit(0); + }); + + // Stay alive indefinitely + await new Promise(() => {}); +} + +main().catch((err) => { + console.error("mock-server fatal:", err); + process.exit(1); +}); +``` + +- [ ] **Step 2: Write the integration test** + +```typescript +// packages/mcp-server/src/singleton/__tests__/integration.test.ts +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { fork, ChildProcess } from "node:child_process"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const fixturePath = join(__dirname, "fixtures", "mock-server.mjs"); + +interface ReadyMessage { + ready: boolean; + takeoverFromPid?: number; +} + +function spawnMockServer(stateDir: string): Promise<{ + proc: ChildProcess; + ready: ReadyMessage; +}> { + return new Promise((resolve, reject) => { + const proc = fork(fixturePath, { + env: { ...process.env, PLUGINOS_STATE_DIR: stateDir }, + stdio: ["ignore", "pipe", "pipe", "ipc"], + }); + proc.once("error", reject); + proc.once("message", (msg) => resolve({ proc, ready: msg as ReadyMessage })); + }); +} + +function waitForExit(proc: ChildProcess, timeoutMs: number): Promise { + return new Promise((resolve) => { + let resolved = false; + proc.once("exit", (code) => { + if (!resolved) { + resolved = true; + resolve(code); + } + }); + setTimeout(() => { + if (!resolved) { + resolved = true; + resolve(null); + } + }, timeoutMs); + }); +} + +describe("singleton integration: two-process takeover", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "pluginos-integ-test-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("second invocation reaps the first and reports takeoverFromPid", async () => { + const first = await spawnMockServer(dir); + expect(first.ready.ready).toBe(true); + expect(first.ready.takeoverFromPid).toBeUndefined(); + + const firstPid = first.proc.pid!; + + const second = await spawnMockServer(dir); + expect(second.ready.ready).toBe(true); + expect(second.ready.takeoverFromPid).toBe(firstPid); + + // First should have exited + const firstExitCode = await waitForExit(first.proc, 3000); + expect(firstExitCode).not.toBeNull(); + + // The pid file should now contain second's PID + const pidContent = (await readFile(join(dir, "server.pid"), "utf8")).trim(); + expect(Number.parseInt(pidContent, 10)).toBe(second.proc.pid); + + second.proc.kill("SIGTERM"); + await waitForExit(second.proc, 3000); + }, 15000); + + it("a fresh start with no prior state has no takeoverFromPid", async () => { + const one = await spawnMockServer(dir); + expect(one.ready.takeoverFromPid).toBeUndefined(); + one.proc.kill("SIGTERM"); + await waitForExit(one.proc, 3000); + }, 10000); +}); +``` + +- [ ] **Step 3: Build singleton (ensure JS exists for fork to import)** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm run build -w packages/mcp-server` + +If the fixture uses `.js` imports but only `.ts` exists, we need to either build first OR mark the fixture as `.ts` and run via tsx. Since this is a Vitest test, the test runner can transpile the fixture if we point it at a `.ts` file. + +Adjust: change `fixtures/mock-server.mjs` → `fixtures/mock-server.ts` AND change `fork(fixturePath)` to `fork(fixturePath, { execArgv: ["--import", "tsx"] })`. (Note: requires `tsx` in devDependencies, which is already present.) + +Apply this change before running the test if the .mjs version doesn't find the module. + +- [ ] **Step 4: Run the integration test** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/integration` +Expected: 2 passed (the test is slow; allow up to 15s timeout per case). + +- [ ] **Step 5: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `test(mcp-server): add two-process singleton integration test`. + +--- + +## Task 8: Wire singleton into mcp-server main() + +**Files:** +- Modify: `packages/mcp-server/src/index.ts` + +- [ ] **Step 1: Read existing main() to understand current shape** + +Read `packages/mcp-server/src/index.ts` from line 40 onwards. + +- [ ] **Step 2: Modify main() to acquire singleton lock + register shutdown + write state file** + +Replace the `main()` body and add a parent-liveness heartbeat. Below is the full new main() (preserve everything ABOVE the existing `async function main()` declaration; only this function changes): + +```typescript +import { + acquireSingletonLock, + writeSingletonState, + clearSingletonState, + buildStateFile, + writeStateFile, +} from "./singleton/index.js"; + +let singletonInfo: Awaited> | null = null; +let currentParentAlive = true; +let parentLivenessInterval: NodeJS.Timeout | null = null; +let selfTerminateTimeout: NodeJS.Timeout | null = null; + +const PARENT_LIVENESS_INTERVAL_MS = 10_000; +const ORPHAN_GRACE_MS = 30_000; + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (err) { + return (err as NodeJS.ErrnoException).code === "EPERM"; + } +} + +function registerShutdownHandlers(): void { + const cleanup = async () => { + if (singletonInfo) { + await clearSingletonState(singletonInfo); + } + if (parentLivenessInterval) { + clearInterval(parentLivenessInterval); + parentLivenessInterval = null; + } + if (selfTerminateTimeout) { + clearTimeout(selfTerminateTimeout); + selfTerminateTimeout = null; + } + }; + process.on("SIGTERM", async () => { + await cleanup(); + process.exit(0); + }); + process.on("SIGINT", async () => { + await cleanup(); + process.exit(0); + }); + process.on("exit", () => { + // Synchronous best-effort: try to unlink files. Promises won't run in 'exit'. + if (singletonInfo) { + try { + require("node:fs").unlinkSync(singletonInfo.stateFilePath); + } catch { + // ignored + } + try { + require("node:fs").unlinkSync(singletonInfo.pidFilePath); + } catch { + // ignored + } + } + }); +} + +async function startParentLivenessHeartbeat(state: ReturnType): Promise { + parentLivenessInterval = setInterval(async () => { + if (!singletonInfo) return; + const alive = isProcessAlive(process.ppid); + if (alive !== currentParentAlive) { + currentParentAlive = alive; + const updated = { ...state, parentAlive: alive }; + await writeStateFile(singletonInfo.stateFilePath, updated); + } + if (!alive && selfTerminateTimeout === null) { + console.error( + `[singleton] Parent PID ${state.parentPid} is dead. Self-terminating in ${ORPHAN_GRACE_MS / 1000}s.` + ); + selfTerminateTimeout = setTimeout(() => { + console.error("[singleton] Grace period elapsed. Exiting."); + process.exit(0); + }, ORPHAN_GRACE_MS); + } + }, PARENT_LIVENESS_INTERVAL_MS); +} + +async function main() { + singletonInfo = await acquireSingletonLock(); + if (singletonInfo.takeoverFromPid !== undefined) { + console.error(`PluginOS server: took over from PID ${singletonInfo.takeoverFromPid}`); + } + registerShutdownHandlers(); + + // Re-read on every request so rebuilds land without restarting the server. + const httpServer = createHttpServer(() => loadUiContent()); + + const wsServer = new WebSocketPluginBridge({ httpServer }); + const port = await wsServer.start(); + console.error(`PluginOS WebSocket + HTTP server on port ${port}`); + + // Read package version for state.json + const pkgPath = join(__dirname, "..", "package.json"); + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version: string }; + + const state = buildStateFile({ + pid: process.pid, + port, + serverVersion: pkg.version, + parentPid: process.ppid, + parentAlive: true, + }); + await writeSingletonState(singletonInfo, state); + await startParentLivenessHeartbeat(state); + + const mcpServer = createPluginOSServer(wsServer); + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); + console.error("PluginOS MCP server running on stdio"); +} + +main().catch((err) => { + console.error("Fatal:", err); + process.exit(1); +}); +``` + +- [ ] **Step 3: Run typecheck and existing mcp-server tests** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm run typecheck && npm test -w packages/mcp-server` +Expected: no typecheck errors; all existing tests pass. + +If typecheck fails on the `require()` calls in the `exit` handler, replace them with `import { unlinkSync } from "node:fs"` at the top and use `unlinkSync(...)` directly. + +- [ ] **Step 4: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(mcp-server): wire singleton lock + state-file into startup`. + +--- + +## Task 9: HTTP /state.json endpoint (TDD) + +**Files:** +- Create: `packages/mcp-server/src/__tests__/http-state-endpoint.test.ts` +- Modify: `packages/mcp-server/src/http-server.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/__tests__/http-state-endpoint.test.ts +import { describe, it, expect } from "vitest"; +import { createHttpServer } from "../http-server.js"; +import type { StateFile } from "../singleton/types.js"; + +describe("HTTP /state.json endpoint", () => { + it("returns the current state object when set", async () => { + const state: StateFile = { + version: 1, + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + startedAt: 1700000000000, + parentPid: 99, + parentAlive: true, + socketPath: null, + }; + const server = createHttpServer( + () => "", + () => state + ); + await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); + const port = (server.address() as { port: number }).port; + try { + const res = await fetch(`http://127.0.0.1:${port}/state.json`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toEqual(state); + } finally { + server.close(); + } + }); + + it("returns 503 when no state is set", async () => { + const server = createHttpServer( + () => "", + () => null + ); + await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); + const port = (server.address() as { port: number }).port; + try { + const res = await fetch(`http://127.0.0.1:${port}/state.json`); + expect(res.status).toBe(503); + } finally { + server.close(); + } + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- __tests__/http-state-endpoint` +Expected: FAIL (createHttpServer doesn't accept a state getter). + +- [ ] **Step 3: Read current http-server.ts and modify the signature** + +Read `packages/mcp-server/src/http-server.ts` fully. Then change `createHttpServer(getUiContent: () => string)` to accept an optional state getter: + +```typescript +import { createServer, IncomingMessage, ServerResponse, Server } from "http"; +import type { StateFile } from "./singleton/types.js"; + +export function createHttpServer( + getUiContent: () => string, + getStateFile?: () => StateFile | null +): Server { + return createServer((req: IncomingMessage, res: ServerResponse) => { + // Existing routes — keep as-is. + // (Insert your existing routing here.) + + if (req.url === "/state.json" && req.method === "GET") { + if (!getStateFile) { + res.writeHead(503, { "Content-Type": "text/plain" }); + res.end("No state available"); + return; + } + const state = getStateFile(); + if (state === null) { + res.writeHead(503, { "Content-Type": "text/plain" }); + res.end("No state available"); + return; + } + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(state)); + return; + } + + // ... rest of existing routing + }); +} +``` + +Preserve all existing routing logic that was there before. Only add the `/state.json` handler. + +- [ ] **Step 4: Wire the state getter in main()** + +In `packages/mcp-server/src/index.ts`, modify the `createHttpServer` call to pass a state getter. The state is captured at startup, so: + +```typescript +let currentState: StateFile | null = null; + +// ... inside main(), after building `state`: +currentState = state; + +// And inside the parent-liveness interval, update currentState when state changes: +const updated = { ...state, parentAlive: alive }; +currentState = updated; +await writeStateFile(singletonInfo.stateFilePath, updated); +``` + +The `createHttpServer` call becomes: + +```typescript +const httpServer = createHttpServer( + () => loadUiContent(), + () => currentState +); +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- __tests__/http-state-endpoint` +Expected: 2 passed. + +- [ ] **Step 6: Run full mcp-server suite** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server` +Expected: all green. + +- [ ] **Step 7: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(mcp-server): add HTTP /state.json endpoint`. + +--- + +## Task 10: Bridge discovery module (TDD) + +**Files:** +- Create: `packages/bridge-plugin/src/discovery.ts` +- Create: `packages/bridge-plugin/src/__tests__/discovery.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/bridge-plugin/src/__tests__/discovery.test.ts +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { fetchStateJson, rankCandidates, type StateFile, SUPPORTED_VERSION } from "../discovery.js"; + +describe("fetchStateJson", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns parsed state on 200 with a supported version", async () => { + const state: StateFile = { + version: 1, + pid: 1234, + port: 9500, + serverVersion: "0.4.3", + startedAt: 100, + parentPid: 99, + parentAlive: true, + socketPath: null, + }; + (fetch as unknown as ReturnType).mockResolvedValueOnce({ + ok: true, + json: async () => state, + }); + const result = await fetchStateJson(9500); + expect(result).toEqual(state); + }); + + it("returns null on a non-200 response", async () => { + (fetch as unknown as ReturnType).mockResolvedValueOnce({ ok: false }); + expect(await fetchStateJson(9500)).toBeNull(); + }); + + it("returns null when fetch throws", async () => { + (fetch as unknown as ReturnType).mockRejectedValueOnce(new Error("boom")); + expect(await fetchStateJson(9500)).toBeNull(); + }); + + it("returns null for a future version", async () => { + (fetch as unknown as ReturnType).mockResolvedValueOnce({ + ok: true, + json: async () => ({ version: SUPPORTED_VERSION + 1, pid: 1, port: 9500 }), + }); + expect(await fetchStateJson(9500)).toBeNull(); + }); +}); + +describe("rankCandidates", () => { + function makeState(overrides: Partial): StateFile { + return { + version: 1, + pid: 1, + port: 9500, + serverVersion: "0.4.3", + startedAt: 0, + parentPid: 99, + parentAlive: true, + socketPath: null, + ...overrides, + }; + } + + it("filters out candidates with parentAlive=false", () => { + const ranked = rankCandidates([ + { port: 9500, state: makeState({ parentAlive: false, startedAt: 100 }) }, + { port: 9501, state: makeState({ parentAlive: true, startedAt: 50 }) }, + ]); + expect(ranked).toHaveLength(1); + expect(ranked[0].port).toBe(9501); + }); + + it("sorts by startedAt descending (newest first)", () => { + const ranked = rankCandidates([ + { port: 9500, state: makeState({ startedAt: 100 }) }, + { port: 9501, state: makeState({ startedAt: 200 }) }, + { port: 9502, state: makeState({ startedAt: 150 }) }, + ]); + expect(ranked.map((c) => c.port)).toEqual([9501, 9502, 9500]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/bridge-plugin -- discovery` +Expected: FAIL. + +- [ ] **Step 3: Implement discovery** + +```typescript +// packages/bridge-plugin/src/discovery.ts + +export interface StateFile { + version: 1; + pid: number; + port: number; + serverVersion: string; + startedAt: number; + parentPid: number; + parentAlive: boolean; + socketPath: string | null; +} + +export const SUPPORTED_VERSION = 1; +export const FETCH_TIMEOUT_MS = 300; + +export interface DiscoveryCandidate { + port: number; + state: StateFile; +} + +export async function fetchStateJson(port: number): Promise { + try { + const controller = new AbortController(); + const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + const res = await fetch(`http://127.0.0.1:${port}/state.json`, { + signal: controller.signal, + }); + clearTimeout(t); + if (!res.ok) return null; + const body = (await res.json()) as unknown; + if ( + typeof body === "object" && + body !== null && + typeof (body as { version?: unknown }).version === "number" && + (body as { version: number }).version <= SUPPORTED_VERSION + ) { + return body as StateFile; + } + return null; + } catch { + return null; + } +} + +export function rankCandidates(candidates: DiscoveryCandidate[]): DiscoveryCandidate[] { + return candidates + .filter((c) => c.state.parentAlive !== false) + .sort((a, b) => b.state.startedAt - a.state.startedAt); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/bridge-plugin -- discovery` +Expected: 6 passed. + +- [ ] **Step 5: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(bridge-plugin): add discovery module (fetchStateJson + rankCandidates)`. + +--- + +## Task 11: Wire discovery into the bridge connect() flow + +**Files:** +- Modify: `packages/bridge-plugin/src/ui-entry.ts` +- Create: `packages/bridge-plugin/src/__tests__/connect-with-discovery.test.ts` + +The existing connect logic in `ui-entry.ts` scans ports directly. We modify it to (1) probe each port for state.json first, (2) rank candidates, (3) connect to the best. + +- [ ] **Step 1: Write the failing test (happy-dom integration test)** + +```typescript +// packages/bridge-plugin/src/__tests__/connect-with-discovery.test.ts +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + discoverCandidatePorts, + type StateFile, +} from "../discovery.js"; + +describe("discoverCandidatePorts (probe-and-rank end-to-end)", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns ranked candidates, excluding orphans", async () => { + const orphan: StateFile = { + version: 1, + pid: 1, + port: 9500, + serverVersion: "0.4.3", + startedAt: 100, + parentPid: 99, + parentAlive: false, + socketPath: null, + }; + const live: StateFile = { + version: 1, + pid: 2, + port: 9501, + serverVersion: "0.4.3", + startedAt: 200, + parentPid: 100, + parentAlive: true, + socketPath: null, + }; + const fetchMock = fetch as unknown as ReturnType; + fetchMock.mockImplementation(async (url: string) => { + if (url.includes(":9500")) return { ok: true, json: async () => orphan }; + if (url.includes(":9501")) return { ok: true, json: async () => live }; + throw new Error("ECONNREFUSED"); + }); + const ranked = await discoverCandidatePorts([9500, 9501, 9502]); + expect(ranked).toHaveLength(1); + expect(ranked[0].port).toBe(9501); + }); + + it("returns empty when no servers respond", async () => { + const fetchMock = fetch as unknown as ReturnType; + fetchMock.mockRejectedValue(new Error("ECONNREFUSED")); + const ranked = await discoverCandidatePorts([9500, 9501]); + expect(ranked).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/bridge-plugin -- connect-with-discovery` +Expected: FAIL — `discoverCandidatePorts` not exported. + +- [ ] **Step 3: Add `discoverCandidatePorts` to discovery.ts** + +Append to `packages/bridge-plugin/src/discovery.ts`: + +```typescript +export async function discoverCandidatePorts(ports: number[]): Promise { + const probed = await Promise.all( + ports.map(async (port) => { + const state = await fetchStateJson(port); + return state ? { port, state } : null; + }) + ); + const candidates = probed.filter((c): c is DiscoveryCandidate => c !== null); + return rankCandidates(candidates); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/bridge-plugin -- connect-with-discovery` +Expected: 2 passed. + +- [ ] **Step 5: Modify ui-entry.ts to use discovery before scan** + +Read the current `connect()` implementation in `packages/bridge-plugin/src/ui-entry.ts` (around lines 165-185 or wherever the port loop lives). Replace the port-iteration logic with: + +```typescript +import { discoverCandidatePorts } from "./discovery.js"; + +const PORT_MIN = 9500; +const PORT_MAX = 9510; +const PORTS: number[] = []; +for (let p = PORT_MIN; p <= PORT_MAX; p++) PORTS.push(p); + +async function connect(): Promise { + setStatus("connecting"); + + // Phase 1: discovery probe + const ordered = [lastPort, ...PORTS.filter((p) => p !== lastPort)].filter( + (p): p is number => typeof p === "number" + ); + const ranked = await discoverCandidatePorts(ordered); + + // Phase 2: connect to ranked candidates first + for (const { port } of ranked) { + if (await tryWsConnect(port)) { + lastPort = port; + return; + } + } + + // Phase 3: fallback — any port that opens a socket + for (const port of ordered) { + if (await tryWsConnect(port)) { + lastPort = port; + return; + } + } + + setStatus("disconnected"); + scheduleReconnect(); +} + +async function tryWsConnect(port: number): Promise { + // Existing WebSocket connect logic, but returning boolean success + // ... +} +``` + +Adapt the surrounding existing code (state tracking, reconnect scheduler) — don't rewrite anything outside connect itself. + +- [ ] **Step 6: Run all bridge-plugin tests** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/bridge-plugin` +Expected: all green (78 existing + new discovery tests). + +- [ ] **Step 7: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(bridge-plugin): use ranked discovery before port scan in connect()`. + +--- + +## Task 12: wait_for_reconnect MCP tool (TDD) + +**Files:** +- Create: `packages/mcp-server/src/__tests__/wait-for-reconnect.test.ts` +- Modify: `packages/mcp-server/src/server.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/__tests__/wait-for-reconnect.test.ts +import { describe, it, expect, vi } from "vitest"; +import type { IPluginBridge } from "@pluginos/shared"; +import { createPluginOSServer } from "../server.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; + +type ToolResult = { content: Array<{ type: string; text: string }>; isError?: boolean }; + +function makeBridge(isConnected: () => boolean): IPluginBridge { + return { + sendAndWait: vi.fn(), + getStatus: vi.fn().mockReturnValue({ + connected: isConnected(), + fileKey: "mock-file", + fileName: "Mock File", + currentPage: "Page 1", + port: 9500, + connectedFiles: 1, + }), + listFiles: vi.fn().mockReturnValue([]), + isConnected: vi.fn(isConnected), + } as unknown as IPluginBridge; +} + +async function setupClient(bridge: IPluginBridge) { + const server = createPluginOSServer(bridge); + const [c, s] = InMemoryTransport.createLinkedPair(); + await server.connect(s); + const client = new Client({ name: "t", version: "1" }); + await client.connect(c); + return client; +} + +describe("wait_for_reconnect tool", () => { + it("returns connected immediately when bridge is already connected", async () => { + const bridge = makeBridge(() => true); + const client = await setupClient(bridge); + const res = (await client.callTool({ + name: "wait_for_reconnect", + arguments: { timeoutSec: 5 }, + })) as ToolResult; + expect(res.isError).toBeFalsy(); + const payload = JSON.parse(res.content[0].text); + expect(payload.connected).toBe(true); + expect(payload.waitedMs).toBeLessThan(700); // first poll + tiny overhead + }); + + it("returns timeout response when bridge never connects", async () => { + const bridge = makeBridge(() => false); + const client = await setupClient(bridge); + const res = (await client.callTool({ + name: "wait_for_reconnect", + arguments: { timeoutSec: 2 }, + })) as ToolResult; + expect(res.isError).toBe(true); + const payload = JSON.parse(res.content[0].text); + expect(payload.connected).toBe(false); + expect(payload.waitedMs).toBeGreaterThanOrEqual(2000); + }, 5000); + + it("returns connected when bridge connects mid-wait", async () => { + let connected = false; + const bridge = makeBridge(() => connected); + const client = await setupClient(bridge); + setTimeout(() => { + connected = true; + }, 500); + const res = (await client.callTool({ + name: "wait_for_reconnect", + arguments: { timeoutSec: 5 }, + })) as ToolResult; + expect(res.isError).toBeFalsy(); + const payload = JSON.parse(res.content[0].text); + expect(payload.connected).toBe(true); + expect(payload.waitedMs).toBeGreaterThanOrEqual(500); + expect(payload.waitedMs).toBeLessThan(1500); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- __tests__/wait-for-reconnect` +Expected: FAIL — tool doesn't exist. + +- [ ] **Step 3: Register the tool in server.ts** + +Add to `packages/mcp-server/src/server.ts` (after existing tool registrations): + +```typescript +server.tool( + "wait_for_reconnect", + "Wait for the PluginOS Bridge plugin to reconnect after a disconnect. " + + "Returns when the bridge reports connected, or when timeoutSec elapses. " + + "Use this when a prior tool call returned 'No plugin connected' to gracefully " + + "wait for the user to relaunch the plugin instead of immediately failing back to chat.", + { + timeoutSec: z + .number() + .int() + .min(1) + .max(300) + .default(60) + .describe("Maximum seconds to wait. Default 60, max 300."), + }, + async ({ timeoutSec }) => { + const startedAt = Date.now(); + const deadline = startedAt + timeoutSec * 1000; + + while (Date.now() < deadline) { + if (bridge.isConnected()) { + const status = bridge.getStatus(); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + connected: true, + waitedMs: Date.now() - startedAt, + fileName: status.fileName, + fileKey: status.fileKey, + }, + null, + 2 + ), + }, + ], + }; + } + await new Promise((r) => setTimeout(r, 500)); + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + connected: false, + waitedMs: Date.now() - startedAt, + timeoutSec, + }, + null, + 2 + ), + }, + ], + isError: true, + }; + } +); +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- __tests__/wait-for-reconnect` +Expected: 3 passed. + +- [ ] **Step 5: Run the full mcp-server suite** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server` +Expected: all green. + +- [ ] **Step 6: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `feat(mcp-server): add wait_for_reconnect MCP tool`. + +--- + +## Task 13: Skill note for wait_for_reconnect + +**Files:** +- Modify: `packages/claude-plugin/skills/pluginos-figma/SKILL.md` + +The recipes block is autogen. The connection troubleshooting section is hand-written. We add a brief mention there. + +- [ ] **Step 1: Locate the Connection troubleshooting section** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && grep -n "Connection troubleshooting" packages/claude-plugin/skills/pluginos-figma/SKILL.md` + +- [ ] **Step 2: Append a one-sentence note to that section** + +Find the existing list step `3. Wait for confirmation before retrying.` (or equivalent). Add a new step after it: + +```markdown +4. If the user relaunches the plugin, call `pluginos.wait_for_reconnect({ timeoutSec: 60 })` to gracefully block until reconnect — then retry the failed op. This avoids bouncing back to chat for every short disconnect. +``` + +If the section's wording doesn't match exactly, adapt to the existing tone. Keep under 50 tokens. + +- [ ] **Step 3: Verify the skill budget** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && node scripts/check-skill-budget.cjs` +Expected: PASS, count under 1150. + +- [ ] **Step 4: Commit** + +Use `Skill(commit-commands:commit)`. Suggested message: `docs(claude-plugin): document wait_for_reconnect in skill troubleshooting`. + +--- + +## Task 14: Full check + smoke test prep + +**Files:** None (verification only) + +- [ ] **Step 1: Run the full pipeline** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm run check` +Expected: lint, format, typecheck, build, test all pass. + +- [ ] **Step 2: Run the integration test specifically (slow)** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- singleton/__tests__/integration` +Expected: 2 passed. + +- [ ] **Step 3: Confirm no untracked files** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && git status` +Expected: clean working tree. + +- [ ] **Step 4: Write the manual smoke test checklist for the PR body** + +The smoke test happens on the user's machine. Write the checklist that goes into the PR description (don't commit this — it goes into the PR body): + +```markdown +## Manual smoke test + +Before merging, against a real Figma file: + +1. **Orphan reaping (cross-session):** + - Start one Claude Code session. Verify `pluginos.get_status` works. + - Without exiting that session, open a second Claude Code session. + - In the second session, run `pluginos.get_status`. Expected: returns live status. First session's pluginos was reaped silently. + - Verify `~/.pluginos/server.pid` contains the second session's `pluginos` PID. + +2. **Stale state file cleanup:** + - Force-kill `pluginos` with `kill -9 $(pgrep -f pluginos)`. + - Verify `~/.pluginos/state.json` still exists (orphaned). + - Start a new Claude session. Verify `pluginos.get_status` works. + - Verify `~/.pluginos/state.json` now reflects the new server's PID. + +3. **wait_for_reconnect end-to-end:** + - Start a Claude session with the bridge plugin open. Run `pluginos.execute_figma { code: "return 'hi'" }`. Confirm response. + - Close the bridge plugin (Figma side). + - Ask Claude to run another execute_figma. Expected: `No plugin connected` error. + - Ask Claude to call `pluginos.wait_for_reconnect({ timeoutSec: 60 })`. + - Reopen the bridge plugin within 60s. + - Confirm `wait_for_reconnect` returns `connected: true` within ~5s of reopening. + - Retry the failed `execute_figma`. Confirm success. + +4. **Parent-alive self-termination:** + - Note `pluginos` PID via `pgrep -f pluginos`. + - Kill the Claude session that spawned it (or `kill -9` its parent shell). + - Wait 45 seconds (30s grace + 10s heartbeat overhead). + - Verify `pluginos` PID is gone via `pgrep -f pluginos`. +``` + +--- + +## Task 15: Open the implementation PR + +**Files:** None — git/gh only + +- [ ] **Step 1: Push the branch** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && git push -u origin feat/pr-a1-connection-foundation` +Expected: pre-push hooks pass, branch pushed. + +- [ ] **Step 2: Open the PR** + +Run `gh pr create --base main --head feat/pr-a1-connection-foundation --title "feat: PR-A1 connection foundation — singleton + discovery + wait_for_reconnect"` with a body that includes: +- One-paragraph summary +- Bulleted list of what's shipped (singleton, state.json, /state.json endpoint, ranked discovery in bridge, wait_for_reconnect) +- Reference to the design doc path +- The manual smoke test checklist from Task 14 Step 4 +- Test plan: "All unit + integration tests pass via `npm run check`. Manual smoke test pending against a real Figma file." + +- [ ] **Step 3: Report the PR URL to the user** + +The terminal phase is complete. + +--- + +## Self-Review Notes + +Performed: + +1. **Spec coverage:** + - §A (singleton) → Tasks 2, 3, 4, 5, 6, 7, 8 ✓ + - §B (code organization) → File map at top ✓ + - §C (state.json) → Tasks 5, 8 (heartbeat in main()) ✓ + - §D (bridge discovery) → Tasks 9 (server endpoint), 10 (discovery module), 11 (wiring) ✓ + - §E (wait_for_reconnect) → Tasks 12, 13 ✓ + - Backwards compatibility — explicitly preserved in Task 11 (Phase 4 fallback) ✓ + - Testing strategy — Tasks 2-13 each include tests; integration in Task 7 ✓ + - Non-goals — explicitly not in any task ✓ + +2. **Placeholder scan:** No TBDs. Two adapt-to-existing-code areas (Task 8's existing http-server.ts routing, Task 11's existing connect() body): the engineer reads the current file first, then applies the modifications shown. These are unavoidable because the existing code has implementation details we don't want to copy-paste into the plan verbatim. + +3. **Type consistency:** `StateFile` shape consistent across `types.ts` (Task 1), `state-file.ts` (Task 5), `discovery.ts` (Task 10), and the test fixtures. `SingletonInfo` shape consistent across `types.ts` and orchestrator (Task 6). `acquireSingletonLock` return type used in Task 8 matches its definition in Task 6. + +4. **Known unknown:** Task 7 may need to switch the fixture from `.mjs` to `.ts` if Node ESM resolution doesn't find the built `singleton/index.js`. Explicit instruction provided in the task to make that switch if needed, with the tsx command pattern. diff --git a/docs/superpowers/specs/2026-06-04-pluginos-connection-foundation-design.md b/docs/superpowers/specs/2026-06-04-pluginos-connection-foundation-design.md new file mode 100644 index 0000000..4a3af30 --- /dev/null +++ b/docs/superpowers/specs/2026-06-04-pluginos-connection-foundation-design.md @@ -0,0 +1,409 @@ +# PluginOS Connection Foundation (PR-A1) — Design + +**Date:** 2026-06-04 +**Status:** Approved for implementation planning +**Author:** Brainstorm session with Claude +**Scope:** Single PR. Server-side singleton enforcement + bridge-side discovery + a new `wait_for_reconnect` MCP tool. Bridge UI cleanup (dark mode, state machine, activity panel) deferred to PR-A2. + +## Context + +The PluginOS bulk-seed run on 2026-06-03 surfaced a class of bugs around port collisions and stale server processes: + +- A previous Claude Desktop conversation session spawned `npx pluginos`, then ended without killing it. The orphan held port 9500. +- A new Claude Desktop conversation spawned its own `pluginos` server, which fell back to port 9501. +- The bridge plugin remained connected to the orphan PID 5762 (still bound to 9500), so the active session's MCP calls reached a dead server. +- The user discovered this only by running `lsof -iTCP:9500` and tracing the parent process chain. + +This happened twice on the same day. The pain is **acute** (silent failure mode, no error to diagnose) and **repeated** (orphan persistence across sessions is the default outcome). + +The full feedback document is at `/Users/dimi/Documents/TheVault/03 Vice Versa/TYPO3 Bootstrap/2026-06-03-pluginos-feedback.md` (items #9, #10, #11 in particular). + +## Goals + +1. **Make orphan servers impossible to leave behind.** Any new `npx pluginos` invocation gracefully reaps the prior server before binding. +2. **Make the bridge plugin connect to the right server even if multiple are listening.** A discovery file + `parentAlive` heuristic let the bridge pick the live one. +3. **Make mid-script disconnects recoverable without a chat-bounce.** A `wait_for_reconnect` MCP tool lets the agent block until the bridge reconnects. + +## Non-goals (deferred to PR-A2 or later) + +- Bridge plugin dark mode fix → **PR-A2** +- UI state machine cleanup (connect/disconnect view consistency) → **PR-A2** +- Activity panel in connected view → **PR-A2** +- Install and distribution polish → **PR-C** +- Cross-machine discovery — single user, single host only +- Unix domain socket transport (`state.json` reserves `socketPath` field for future use, but it stays `null` in v1) +- Daemonization — the server still lives and dies with the invoking process +- MCP-level proxying (second `npx pluginos` does not become a client of the first; it reaps and takes over) +- Windows-specific CI testing — `fs.open('wx')` and `process.kill(pid, 0)` both work on Windows, but our matrix only covers Linux today + +## Architecture + +``` + ~/.pluginos/ + ├── server.pid (PID of current server) + ├── server.pid.lock (mutex during startup) + └── state.json (discovery + parentAlive) + ▲ + │ writes +┌───────────────┐ ┌──────────────────┴─────────┐ +│ agent │ ── execute / list_ops ──► │ mcp-server │ +│ (Claude) │ │ ├── singleton/ │ +│ │ ── wait_for_reconnect ──► │ │ ├── lockfile.ts │ +└───────────────┘ │ │ ├── pid-file.ts │ + │ │ ├── takeover.ts │ + │ │ └── state-file.ts │ + │ └── http-server.ts │ + │ ├── (existing routes) │ + │ └── GET /state.json │ + └──────────┬──────────────────┘ + │ WebSocket + ▼ + ┌─────────────────────────────┐ + │ bridge-plugin │ + │ └── ui-entry.ts │ + │ ├── fetchStateJson() │ ◄── HTTP probe + │ └── connect() (ranked) │ + └─────────────────────────────┘ +``` + +## Component-by-component design + +### A. Singleton mechanism + +**State directory:** `~/.pluginos/` created with mode `0700` if missing. All files inside owned by current user. + +**Lock primitive:** `fs.open(lockPath, 'wx')` — creates the lockfile atomically or fails with `EEXIST`. Works identically on Linux, macOS, and Windows. Simpler than `flock(2)` and cross-platform native. + +**Startup sequence:** + +``` +1. mkdir -p ~/.pluginos (mode 0700) +2. acquireLock(server.pid.lock): + loop (max 5 retries, 200ms backoff): + try fs.open('wx') → got lock, break + catch EEXIST → check who holds it (read PID), retry if stale +3. read server.pid (if exists) +4. if oldPid exists and alive (process.kill(oldPid, 0) succeeds): + a. send SIGTERM(oldPid) + b. poll process.kill(oldPid, 0) every 100ms for 1s + c. if still alive: send SIGKILL(oldPid) + d. wait 200ms for kernel to release the port +5. start WebSocket server (existing 9500-9510 scan logic — unchanged) +6. write server.pid atomically: write to server.pid.tmp, rename to server.pid +7. write state.json atomically (Section C) +8. release lock: unlink server.pid.lock +``` + +**Shutdown sequence** (handlers on `SIGTERM`, `SIGINT`, and `process.on('exit')`): + +``` +1. unlink state.json (best-effort, swallow ENOENT) +2. unlink server.pid (best-effort) +3. close WebSocket +``` + +If the process is SIGKILL'd or crashes hard, files are left orphaned — the next startup detects this via the PID-liveness check at step 4 and takes over without ceremony. + +**Edge cases handled inline:** + +| Case | Behavior | +|---|---| +| `~/.pluginos/` not writable | Log warning to stderr, skip the entire singleton dance, run as before (degraded) | +| PID file corrupted / unreadable | Treat as no lock — take over by unlinking and rewriting | +| Lock held >1s by another startup | After 5 retries, log warning and proceed anyway (race window we accept rather than deadlock) | +| Old server's port is reused by a non-pluginos process | The 9500-9510 scan moves to the next port — old behavior preserved | +| Multiple users on same machine | Each has own `~/.pluginos/` — no conflict | + +### B. Code organization for the singleton module + +``` +packages/mcp-server/src/singleton/ + ├── lockfile.ts — acquireLock / releaseLock with retry + stale-detection + ├── takeover.ts — SIGTERM → poll → SIGKILL sequence + ├── pid-file.ts — read/write server.pid atomically + ├── state-file.ts — write/clean state.json atomically + parent-liveness heartbeat + └── index.ts — orchestrator: acquireSingletonLock(): Promise<{ takeoverFromPid?: number }> +``` + +Integration in `packages/mcp-server/src/index.ts` `main()`: call `acquireSingletonLock()` before `wsServer.start()`, register shutdown handlers immediately after acquisition. + +### C. Discovery file (`state.json`) + +**Path:** `~/.pluginos/state.json` + +**Shape (v1):** + +```json +{ + "version": 1, + "pid": 12345, + "port": 9500, + "serverVersion": "0.4.3", + "startedAt": 1735036820123, + "parentPid": 1234, + "parentAlive": true, + "socketPath": null +} +``` + +- `version` — schema version; bridge gracefully ignores files with newer-than-expected versions +- `pid` — server PID, for liveness checks +- `port` — bound WebSocket port (could be 9500-9510) +- `serverVersion` — semver of the running `pluginos`; bridge uses this for the existing mismatch UI without waiting for a WebSocket `SERVER_HELLO` +- `startedAt` — epoch millis; tiebreaker between multiple live candidates +- `parentPid` — PID of the process that spawned `pluginos`; useful for diagnostics +- `parentAlive` — `true` if the server's parent is still alive; toggled by the server itself on a 10s interval (see below) +- `socketPath` — reserved for future Unix-socket transport; null in v1 + +**Write protocol** (server side): + +``` +1. After WebSocket bind succeeds, build the state object +2. Write to ~/.pluginos/state.json.tmp +3. fs.rename(tmp, state.json) — atomic on POSIX, near-atomic on Windows +4. On graceful shutdown: fs.unlink(state.json) — best effort +``` + +The write happens *after* the WebSocket is listening so a bridge that reads the file is guaranteed to find a live server (modulo race conditions during the millisecond between bind and write, which the bridge handles via retry). + +**Parent-liveness heartbeat** (server side, every 10 seconds): + +```typescript +setInterval(() => { + const alive = isProcessAlive(process.ppid); + if (alive !== currentParentAlive) { + writeStateFile({ ...state, parentAlive: alive }); + currentParentAlive = alive; + } + if (!alive) { + // Self-terminate after a 30s grace period if our parent is dead. + // This is the orphan-prevention mechanism that cures the user's bug at the source. + setTimeout(() => process.exit(0), 30_000); + } +}, 10_000); +``` + +The 30s grace lets the agent finish an in-flight call before the orphan reaps itself. Combined with the bridge's `parentAlive` filter (Section D), orphans become double-blind: even if one persists past the heartbeat, the bridge ignores it. + +### D. Bridge plugin integration + +The bridge plugin sandbox cannot read files from disk. Instead, it discovers servers by HTTP-probing each port in the existing 9500-9510 range, reading `state.json` from the server's HTTP endpoint, and ranking candidates. + +**New flow** (replaces the existing `connect()` in `packages/bridge-plugin/src/ui-entry.ts` ~lines 165-185): + +``` +async connect(): + order = [lastPort, ...PORT_MIN..PORT_MAX].filter(uniq) + candidates = [] + + // Phase 1: HTTP probe each port for state.json + for port in order: + state = await fetchStateJson(port) // 300ms timeout per port + if state: + candidates.push({ port, state }) + + // Phase 2: rank candidates + ranked = candidates + .filter(c => c.state.parentAlive !== false) // exclude orphans + .sort((a, b) => b.state.startedAt - a.state.startedAt) // newest first + + // Phase 3: connect to best + for { port } in ranked: + try wsConnect(port) + if SERVER_HELLO and version compatible: + lastPort = port + return + + // Phase 4: fallback — try anything + for port in order: + try wsConnect(port) + if success: return + + // Phase 5: give up + setStatus("disconnected") + scheduleReconnect() +``` + +The Phase 4 fallback preserves backward compatibility: if `state.json` is missing, malformed, or all servers report `parentAlive: false`, the bridge still tries direct connection. + +**`fetchStateJson(port)` helper:** + +```typescript +async function fetchStateJson(port: number): Promise { + try { + const controller = new AbortController(); + const t = setTimeout(() => controller.abort(), 300); + const res = await fetch(`http://127.0.0.1:${port}/state.json`, { + signal: controller.signal, + }); + clearTimeout(t); + if (!res.ok) return null; + const body = await res.json(); + if (typeof body?.version !== "number" || body.version > SUPPORTED_VERSION) { + return null; + } + return body as StateFile; + } catch { + return null; + } +} +``` + +Worst-case Phase 1 budget: ~3 seconds (10 ports × 300ms). On reconnects, `lastPort` is first in the order, so the typical path is one ~5ms localhost HTTP roundtrip. + +**HTTP `/state.json` endpoint** — added to `packages/mcp-server/src/http-server.ts`: + +```typescript +if (req.url === "/state.json" && req.method === "GET") { + const stateBody = getStateFileContent(); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(stateBody)); + return; +} +``` + +**Reconnection behavior** (mid-session disconnects): + +The existing exponential backoff `[1s, 3s, 5s, 10s]` with a 30s giveup stays. We only change what `scheduleReconnect()` calls into — the new `connect()` with discovery. So if the user opens a fresh agent session mid-Figma-session (which kills the old server via takeover from Section A), the bridge's first reconnect tries `lastPort` (now dead), gets a connection failure, falls back to scan, finds the new server's port via `state.json`, and reconnects. User sees ~3 seconds of "Reconnecting…" then green. + +### E. `wait_for_reconnect` MCP tool + +**Tool signature** (in `packages/mcp-server/src/server.ts`): + +```typescript +server.tool( + "wait_for_reconnect", + "Wait for the PluginOS Bridge plugin to reconnect after a disconnect. " + + "Returns when the bridge reports connected, or when timeoutSec elapses. " + + "Use this when a prior tool call returned 'No plugin connected' to gracefully " + + "wait for the user to relaunch the plugin instead of immediately failing back to chat.", + { + timeoutSec: z.number().int().min(1).max(300).default(60) + .describe("Maximum seconds to wait. Default 60, max 300."), + }, + async ({ timeoutSec }) => { + const startedAt = Date.now(); + const deadline = startedAt + timeoutSec * 1000; + + while (Date.now() < deadline) { + if (bridge.isConnected()) { + const status = bridge.getStatus(); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + connected: true, + waitedMs: Date.now() - startedAt, + fileName: status.fileName, + fileKey: status.fileKey, + }, + null, + 2 + ), + }, + ], + }; + } + await new Promise((r) => setTimeout(r, 500)); + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + connected: false, + waitedMs: Date.now() - startedAt, + timeoutSec, + }, + null, + 2 + ), + }, + ], + isError: true, + }; + } +); +``` + +**Design choices baked in:** + +- **Polling, not event subscription.** `IPluginBridge.isConnected()` already exists. Polling at 500ms is cheap; the worst-case extra latency (500ms after the bridge connects) is invisible next to the wait time itself. +- **Default 60s, max 300s.** Long enough for a real user to notice the wait, open Figma, relaunch the plugin. Cap prevents pathological cases. +- **`isError: true` on timeout.** Treats timeout as a failure mode so the agent's error-handling path triggers. Connect = success. +- **Response body always includes `waitedMs`** so the agent can decide whether to retry the original op immediately or with backoff. +- **No `file_key` arg.** The tool answers "is any bridge connected" — the subsequent op handles file routing. + +**Skill update** — add to `packages/claude-plugin/skills/pluginos-figma/SKILL.md` Connection Troubleshooting section: + +> If a `pluginos.*` call returns "No plugin connected" mid-task, ask the user once to relaunch the plugin, then call `pluginos.wait_for_reconnect({ timeoutSec: 60 })` to block gracefully. When it returns `connected: true`, retry the failed op. + +## Backwards compatibility + +- A bridge plugin from an older release (no discovery file awareness) keeps working via the unchanged 9500-9510 port scan; it ignores `state.json` entirely. +- A server from an older release (no PID file, no `state.json`) gets reaped without ceremony by a newer-version starter — the older server doesn't know to write a PID file, so the newer starter treats its port as "competitor, kill it." Acceptable because we control both halves of the protocol. +- The 9500-9510 port range stays as the scan fallback. New connection logic *augments* discovery on top; it does not remove the scan. +- `IPluginBridge.isConnected()` already exists, so `wait_for_reconnect` adds zero coupling to bridge code. + +## Testing strategy + +**Unit (most coverage):** + +| File | Coverage | +|---|---| +| `singleton/__tests__/lockfile.test.ts` | acquire / release / EEXIST handling / stale-PID detection | +| `singleton/__tests__/takeover.test.ts` | SIGTERM → poll → SIGKILL sequence with mocked `process.kill` | +| `singleton/__tests__/pid-file.test.ts` | atomic write (tmp + rename), corrupt-file recovery, missing-file handling | +| `singleton/__tests__/state-file.test.ts` | write + read roundtrip, schema version check, parent-alive flag mutation | +| `__tests__/wait-for-reconnect.test.ts` | already-connected, mid-wait connection, timeout | +| `__tests__/http-state-endpoint.test.ts` | `GET /state.json` returns current state | +| `bridge-plugin/__tests__/connect-with-discovery.test.ts` | fetch ranking + fallback to scan | + +**Integration:** + +`singleton/__tests__/integration.test.ts` — spawn two real `pluginos` processes via `child_process.fork()` against a temp `~/.pluginos/` (`PLUGINOS_STATE_DIR` env override for test isolation). Assert: +- First binds, writes PID + `state.json` +- Second starts, reaps first, takes over its port +- Second's stderr contains `Reaped PID X` log line +- `~/.pluginos/state.json` after both shows second's PID + +**Manual smoke test** (documented in PR description): + +- Open two Claude Code sessions concurrently. Second's `pluginos.get_status` should return live status; first's reconnects silently to the new server. +- Kill `pluginos` with `kill -9 $(pgrep -f pluginos)`. Start new session. Verify it takes over cleanly (no manual cleanup of state files). +- Open Figma plugin, force-close it mid-`execute_figma`, observe agent calling `wait_for_reconnect`, relaunch plugin, observe successful resumption. + +## Sequencing within the PR (commit-by-commit grain) + +Ten loose phases, each landing as 2–6 commits depending on TDD cycles: + +1. **State dir + lockfile primitive** (`lockfile.ts` + tests) +2. **PID file read/write** (`pid-file.ts` + tests) +3. **Takeover sequence** (`takeover.ts` + tests) +4. **`state.json` shape + writer** (`state-file.ts` + tests) +5. **Singleton orchestrator** (`singleton/index.ts` + integration test that spawns two processes) +6. **Server integration** (wire `acquireSingletonLock()` into `main()`, register shutdown handlers, add 10s parent-liveness interval) +7. **HTTP `/state.json` endpoint** (`http-server.ts` + test) +8. **Bridge discovery** (`fetchStateJson` + ranked connect in `ui-entry.ts` + happy-dom test) +9. **`wait_for_reconnect` MCP tool** (`server.ts` + test + skill note) +10. **Manual smoke + PR-A1 polish** (CHANGELOG entry, version bump, manual smoke checklist) + +After step 5 the singleton works end-to-end (you can manually verify by spawning two processes). After step 8 the bridge-side discovery works. After step 9 the whole user-facing UX is complete. Each phase is independently testable. + +## Open questions deferred to implementation + +1. **Log format for takeover events** — `[singleton] Reaped PID 12345 (orphaned by parent 4321, started 2026-06-04T12:34:56Z)` or shorter? Decide during implementation when we see what `console.error` looks like in the existing codebase. +2. **State file schema migration policy** — v1 only ships now. The bridge already gracefully ignores unknown versions, so when v2 ships the bridge falls back to scan. We add explicit migration paths only when we have a second version to migrate from. +3. **`PLUGINOS_DISABLE_SINGLETON=1` env override** — useful for testing or weird deployment setups. Add it iff a test or smoke run actually needs it. + +## References + +- Feedback document: `/Users/dimi/Documents/TheVault/03 Vice Versa/TYPO3 Bootstrap/2026-06-03-pluginos-feedback.md` (items #6, #9, #10, #11) +- Existing port scan: `packages/mcp-server/src/WebSocketPluginBridge.ts:52-66` +- Existing bridge connect: `packages/bridge-plugin/src/ui-entry.ts:165-185` +- Existing HTTP server: `packages/mcp-server/src/http-server.ts` +- Existing skill (where the troubleshooting note will go): `packages/claude-plugin/skills/pluginos-figma/SKILL.md` diff --git a/package-lock.json b/package-lock.json index aaaf674..26474d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,31 +10,19 @@ ], "devDependencies": { "@eslint/js": "^10.0.1", + "@vitest/coverage-v8": "^4.1.8", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", "prettier": "^3.8.2", - "typescript-eslint": "^8.58.2" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "typescript-eslint": "^8.58.2", + "vitest": "^4.1.8" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -42,9 +30,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -52,13 +40,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -68,25 +56,28 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", @@ -753,76 +744,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -913,17 +834,6 @@ } } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@pluginos/bridge-plugin": { "resolved": "packages/bridge-plugin", "link": true @@ -1286,10 +1196,10 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, @@ -1303,6 +1213,24 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -1611,31 +1539,29 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", - "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", - "magicast": "^0.3.5", - "std-env": "^3.8.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.9", - "vitest": "2.1.9" + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1644,38 +1570,40 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1687,84 +1615,68 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2041,19 +1953,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/adm-zip": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", @@ -2153,6 +2052,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2371,18 +2282,11 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -2404,16 +2308,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2544,6 +2438,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2640,16 +2541,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2666,16 +2557,6 @@ "node": ">= 0.8" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -2770,13 +2651,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2790,13 +2664,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2862,9 +2729,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -3193,30 +3060,6 @@ "node": ">=18.0.0" } }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3441,23 +3284,6 @@ "dev": true, "license": "ISC" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3500,16 +3326,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3547,19 +3363,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-tsconfig": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", @@ -3573,28 +3376,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3615,39 +3396,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3868,16 +3616,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -4010,16 +3748,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4062,19 +3790,6 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4116,21 +3831,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -4145,22 +3845,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -4212,9 +3896,9 @@ } }, "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -4322,31 +4006,14 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^5.0.0" }, "engines": { "node": ">=10" @@ -4362,13 +4029,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -4379,13 +4039,6 @@ "tslib": "^2.0.3" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4397,15 +4050,15 @@ } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, "node_modules/make-dir": { @@ -4500,19 +4153,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -4529,16 +4169,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -4630,35 +4260,6 @@ "dev": true, "license": "MIT" }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -4693,6 +4294,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4714,22 +4329,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4790,13 +4389,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -4854,23 +4446,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-to-regexp": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", @@ -4888,16 +4463,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5131,34 +4696,6 @@ "renderkid": "^3.0.0" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5221,13 +4758,6 @@ "node": ">= 0.10" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -5632,19 +5162,6 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5693,82 +5210,12 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5782,46 +5229,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -5955,21 +5362,6 @@ "dev": true, "license": "MIT" }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6055,30 +5447,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -6271,16 +5643,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -6489,36 +5851,6 @@ } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -6551,58 +5883,79 @@ } }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -6613,15 +5966,34 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/watchpack": { "version": "2.5.1", @@ -6766,13 +6138,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -6856,107 +6221,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7026,7 +6290,7 @@ "html-webpack-plugin": "^5.6.0", "ts-loader": "^9.5.0", "typescript": "^5.5.0", - "vitest": "^2.1.0", + "vitest": "^4.1.8", "webpack": "^5.90.0", "webpack-cli": "^5.1.0" } @@ -7051,199 +6315,7 @@ "devDependencies": { "tsx": "^4.0.0", "typescript": "^5.0.0", - "vitest": "^1.0.0" - } - }, - "packages/claude-plugin/node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^2.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "packages/claude-plugin/node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "packages/claude-plugin/node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "packages/claude-plugin/node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "packages/claude-plugin/node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "packages/claude-plugin/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/claude-plugin/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "packages/claude-plugin/node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "packages/claude-plugin/node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "packages/claude-plugin/node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" + "vitest": "^4.1.8" } }, "packages/claude-plugin/node_modules/typescript": { @@ -7260,108 +6332,6 @@ "node": ">=14.17" } }, - "packages/claude-plugin/node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/claude-plugin/node_modules/vitest": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.1", - "@vitest/ui": "1.6.1", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "packages/claude-plugin/node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/mcp-server": { "name": "pluginos", "version": "0.4.3", @@ -7378,12 +6348,12 @@ "@pluginos/shared": "*", "@types/adm-zip": "^0.5.8", "@types/ws": "^8.5.0", - "@vitest/coverage-v8": "^2.1.9", + "@vitest/coverage-v8": "^4.1.8", "adm-zip": "^0.5.17", "tsup": "^8.5.1", "tsx": "^4.19.0", "typescript": "^5.5.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" }, "engines": { "node": ">=18" @@ -7407,9 +6377,9 @@ "name": "@pluginos/shared", "version": "0.4.3", "devDependencies": { - "@vitest/coverage-v8": "^2.1.9", + "@vitest/coverage-v8": "^4.1.8", "typescript": "^5.5.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" } }, "packages/shared/node_modules/typescript": { diff --git a/package.json b/package.json index c25e5e3..5eea0b2 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,18 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@vitest/coverage-v8": "^4.1.8", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", "prettier": "^3.8.2", - "typescript-eslint": "^8.58.2" + "typescript-eslint": "^8.58.2", + "vitest": "^4.1.8" }, "overrides": { "vite": "^6.4.2", - "esbuild": "^0.25.0" + "esbuild": "^0.25.0", + "vitest": "^4.1.8", + "@vitest/coverage-v8": "^4.1.8" } } diff --git a/packages/bridge-plugin/package.json b/packages/bridge-plugin/package.json index 464d676..5f29f89 100644 --- a/packages/bridge-plugin/package.json +++ b/packages/bridge-plugin/package.json @@ -16,7 +16,7 @@ "html-webpack-plugin": "^5.6.0", "ts-loader": "^9.5.0", "typescript": "^5.5.0", - "vitest": "^2.1.0", + "vitest": "^4.1.8", "webpack": "^5.90.0", "webpack-cli": "^5.1.0" } diff --git a/packages/claude-plugin/package.json b/packages/claude-plugin/package.json index eba7463..649e9f6 100644 --- a/packages/claude-plugin/package.json +++ b/packages/claude-plugin/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "tsx": "^4.0.0", - "vitest": "^1.0.0", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "vitest": "^4.1.8" } } diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 698f9d7..ef4241f 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -53,11 +53,11 @@ "@pluginos/shared": "*", "@types/adm-zip": "^0.5.8", "@types/ws": "^8.5.0", - "@vitest/coverage-v8": "^2.1.9", + "@vitest/coverage-v8": "^4.1.8", "adm-zip": "^0.5.17", "tsup": "^8.5.1", "tsx": "^4.19.0", "typescript": "^5.5.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" } } diff --git a/packages/shared/package.json b/packages/shared/package.json index cc84225..24117b4 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -11,8 +11,8 @@ "test:coverage": "vitest run --coverage" }, "devDependencies": { - "@vitest/coverage-v8": "^2.1.9", + "@vitest/coverage-v8": "^4.1.8", "typescript": "^5.5.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" } }