diff --git a/docs/superpowers/plans/2026-06-03-pluginos-quality-helpers.md b/docs/superpowers/plans/2026-06-03-pluginos-quality-helpers.md new file mode 100644 index 0000000..8882770 --- /dev/null +++ b/docs/superpowers/plans/2026-06-03-pluginos-quality-helpers.md @@ -0,0 +1,2094 @@ +# PluginOS Quality Helpers (PR-B) 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:** Ship five `PluginOS.*` sandbox helpers injected into every `execute_figma` script, a 7-rule pre-flight linter, skill recipes, and an op-count drift fix. + +**Architecture:** A sandbox prelude (JS source blob) is prepended to user scripts by the MCP server before they reach the bridge. The bridge plugin remains unaware of `PluginOS.*`. A pre-flight linter runs against the user's script and returns warn/error/hint records in the response payload (warn-first policy — no blocking in v1). The `execute_figma` response shape is extended additively. + +**Tech Stack:** TypeScript, Vitest, Node `vm` module for sandboxed helper tests, regex-based linter (no AST parser), existing happy-dom for any bridge-side integration. + +**Spec:** [docs/superpowers/specs/2026-06-03-pluginos-quality-helpers-design.md](../specs/2026-06-03-pluginos-quality-helpers-design.md) + +--- + +## File Map + +**Create:** +- `packages/mcp-server/src/prelude/source.ts` — prelude JS source blob (template literal) +- `packages/mcp-server/src/prelude/index.ts` — exports `PRELUDE_VERSION`, `wrapScript` +- `packages/mcp-server/src/prelude/__tests__/wrap.test.ts` +- `packages/mcp-server/src/prelude/__tests__/helpers.test.ts` — vm-based behavioral tests for each helper +- `packages/mcp-server/src/lint/types.ts` +- `packages/mcp-server/src/lint/index.ts` — registry + `runLint(code)` entrypoint +- `packages/mcp-server/src/lint/rules/no-notify.ts` +- `packages/mcp-server/src/lint/rules/no-sync-style-setters.ts` +- `packages/mcp-server/src/lint/rules/no-itemspacing-auto.ts` +- `packages/mcp-server/src/lint/rules/invalid-variable-name.ts` +- `packages/mcp-server/src/lint/rules/no-hyphenated-plugindata-key.ts` +- `packages/mcp-server/src/lint/rules/no-text-encoders.ts` +- `packages/mcp-server/src/lint/rules/prefer-helpers.ts` +- `packages/mcp-server/src/lint/__tests__/no-notify.test.ts` +- `packages/mcp-server/src/lint/__tests__/no-sync-style-setters.test.ts` +- `packages/mcp-server/src/lint/__tests__/no-itemspacing-auto.test.ts` +- `packages/mcp-server/src/lint/__tests__/invalid-variable-name.test.ts` +- `packages/mcp-server/src/lint/__tests__/no-hyphenated-plugindata-key.test.ts` +- `packages/mcp-server/src/lint/__tests__/no-text-encoders.test.ts` +- `packages/mcp-server/src/lint/__tests__/prefer-helpers.test.ts` +- `packages/mcp-server/src/__tests__/execute-with-lint.test.ts` — integration +- `packages/claude-plugin/scripts/sync-recipes.ts` +- `packages/claude-plugin/__tests__/sync-recipes.test.ts` + +**Modify:** +- `packages/mcp-server/src/server.ts` — `execute_figma` handler: lint → wrap → send → augment response. Also `list_operations`: include `total` from registry response. +- `packages/mcp-server/package.json` — add `sync-recipes` script wiring if needed at workspace level (probably not — script lives in claude-plugin) +- `packages/claude-plugin/package.json` — add `"sync-recipes": "tsx scripts/sync-recipes.ts"` +- `packages/claude-plugin/skills/pluginos-figma/SKILL.md` — append `## Recipes for bulk-seed scripts` section (generated by sync-recipes) +- `README.md` — replace any specific operation count with "see `list_operations`" +- `INSTALL.md` — replace "list of 39 operations" with "list of operations" +- `packages/bridge-plugin/src/operations/index.ts` — ensure `__list_operations` returns `total` in its payload (verify behavior; may already work) + +--- + +## Conventions + +- All commits use the `/commit` skill via `Skill(commit)`. Never write commit messages manually. +- After every passing test, run the workspace-scoped test command and read the **full** output before claiming pass. +- Build order if needed: `npm run build:shared` before mcp-server work, though most tasks won't need shared rebuilds. +- Push only after the full PR is ready. All work lands on branch `feat/pr-b-quality-helpers` (create at task 0). + +--- + +## Task 0: Set up the feature branch + +**Files:** +- None — git only + +- [ ] **Step 1: Confirm starting point** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && git status && git branch --show-current` +Expected: clean tree, on `main`, at commit `fc59ff7`. + +- [ ] **Step 2: Create and switch to feature branch** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && git checkout -b feat/pr-b-quality-helpers` +Expected: `Switched to a new branch 'feat/pr-b-quality-helpers'`. + +--- + +## Task 1: Add lint types module + +**Files:** +- Create: `packages/mcp-server/src/lint/types.ts` + +- [ ] **Step 1: Write types module** + +```typescript +// packages/mcp-server/src/lint/types.ts +export type LintSeverity = "error" | "warn" | "hint"; + +export interface LintResult { + ruleId: string; + severity: LintSeverity; + line: number; + message: string; + fix?: string; +} + +export interface LintRule { + id: string; + severity: LintSeverity; + check(code: string): LintResult[]; +} +``` + +- [ ] **Step 2: Typecheck** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm run typecheck` +Expected: no errors. + +- [ ] **Step 3: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add lint types module`. + +--- + +## Task 2: Lint registry + `runLint` entrypoint (TDD) + +**Files:** +- Create: `packages/mcp-server/src/lint/index.ts` +- Create: `packages/mcp-server/src/lint/__tests__/registry.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/lint/__tests__/registry.test.ts +import { describe, it, expect } from "vitest"; +import { runLint, registerRule } from "../index.js"; +import type { LintRule } from "../types.js"; + +describe("lint registry", () => { + it("returns empty array when no rules registered match", () => { + expect(runLint("const x = 1;")).toEqual([]); + }); + + it("aggregates results from all rules", () => { + const dummy: LintRule = { + id: "dummy", + severity: "error", + check: () => [{ ruleId: "dummy", severity: "error", line: 1, message: "x" }], + }; + registerRule(dummy); + const results = runLint("anything"); + expect(results.some((r) => r.ruleId === "dummy")).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/registry` +Expected: FAIL with "Cannot find module '../index.js'". + +- [ ] **Step 3: Implement minimal registry** + +```typescript +// packages/mcp-server/src/lint/index.ts +import type { LintResult, LintRule } from "./types.js"; + +const rules: LintRule[] = []; + +export function registerRule(rule: LintRule): void { + rules.push(rule); +} + +export function runLint(code: string): LintResult[] { + const out: LintResult[] = []; + for (const rule of rules) { + for (const result of rule.check(code)) { + out.push(result); + } + } + return out; +} + +export type { LintResult, LintRule, LintSeverity } from "./types.js"; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/registry` +Expected: 2 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add lint registry and runLint entrypoint`. + +--- + +## Task 3: `no-notify` lint rule (TDD) + +**Files:** +- Create: `packages/mcp-server/src/lint/rules/no-notify.ts` +- Create: `packages/mcp-server/src/lint/__tests__/no-notify.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/lint/__tests__/no-notify.test.ts +import { describe, it, expect } from "vitest"; +import { noNotifyRule } from "../rules/no-notify.js"; + +describe("no-notify rule", () => { + it("flags figma.notify calls", () => { + const results = noNotifyRule.check(`figma.notify("hi");`); + expect(results).toHaveLength(1); + expect(results[0].ruleId).toBe("no-notify"); + expect(results[0].severity).toBe("error"); + expect(results[0].line).toBe(1); + }); + + it("flags figma.notify on later line with correct line number", () => { + const code = `const x = 1;\nconst y = 2;\nfigma.notify("hi");`; + const results = noNotifyRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].line).toBe(3); + }); + + it("does not flag .notify on other objects", () => { + expect(noNotifyRule.check(`emitter.notify("hi");`)).toEqual([]); + }); + + it("does not flag in code without notify", () => { + expect(noNotifyRule.check(`const x = 1;`)).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-notify` +Expected: FAIL (module not found). + +- [ ] **Step 3: Implement the rule** + +```typescript +// packages/mcp-server/src/lint/rules/no-notify.ts +import type { LintRule, LintResult } from "../types.js"; + +const PATTERN = /\bfigma\.notify\s*\(/g; + +export const noNotifyRule: LintRule = { + id: "no-notify", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (PATTERN.test(lines[i])) { + out.push({ + ruleId: "no-notify", + severity: "error", + line: i + 1, + message: "figma.notify() is forbidden in the plugin sandbox. Remove the call.", + }); + PATTERN.lastIndex = 0; + } + } + return out; + }, +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-notify` +Expected: 4 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add no-notify lint rule`. + +--- + +## Task 4: `no-sync-style-setters` lint rule (TDD) + +**Files:** +- Create: `packages/mcp-server/src/lint/rules/no-sync-style-setters.ts` +- Create: `packages/mcp-server/src/lint/__tests__/no-sync-style-setters.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/lint/__tests__/no-sync-style-setters.test.ts +import { describe, it, expect } from "vitest"; +import { noSyncStyleSettersRule } from "../rules/no-sync-style-setters.js"; + +describe("no-sync-style-setters rule", () => { + it.each([ + ["node.fillStyleId = id;", "fillStyleId"], + ["foo.textStyleId = '123';", "textStyleId"], + ["x.strokeStyleId='abc'", "strokeStyleId"], + ["a.effectStyleId = 'eee';", "effectStyleId"], + ["b.gridStyleId='ggg';", "gridStyleId"], + ])("flags %s", (code, field) => { + const results = noSyncStyleSettersRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("warn"); + expect(results[0].message).toContain(field); + }); + + it("does not flag async setters", () => { + expect(noSyncStyleSettersRule.check(`await node.setFillStyleIdAsync(id);`)).toEqual([]); + }); + + it("does not flag reads", () => { + expect(noSyncStyleSettersRule.check(`const id = node.fillStyleId;`)).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-sync-style-setters` +Expected: FAIL. + +- [ ] **Step 3: Implement the rule** + +```typescript +// packages/mcp-server/src/lint/rules/no-sync-style-setters.ts +import type { LintRule, LintResult } from "../types.js"; + +const FIELDS = ["fillStyleId", "textStyleId", "strokeStyleId", "effectStyleId", "gridStyleId"]; +const PATTERN = new RegExp(`\\.(${FIELDS.join("|")})\\s*=\\s*[^=]`); + +export const noSyncStyleSettersRule: LintRule = { + id: "no-sync-style-setters", + severity: "warn", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + const m = lines[i].match(PATTERN); + if (m) { + const field = m[1]; + out.push({ + ruleId: "no-sync-style-setters", + severity: "warn", + line: i + 1, + message: `Sync setter '.${field} = ...' is deprecated. Use .set${field.charAt(0).toUpperCase() + field.slice(1)}Async(...).`, + fix: `await node.set${field.charAt(0).toUpperCase() + field.slice(1)}Async(...)`, + }); + } + } + return out; + }, +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-sync-style-setters` +Expected: 7 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add no-sync-style-setters lint rule`. + +--- + +## Task 5: `no-itemspacing-auto` lint rule (TDD) + +**Files:** +- Create: `packages/mcp-server/src/lint/rules/no-itemspacing-auto.ts` +- Create: `packages/mcp-server/src/lint/__tests__/no-itemspacing-auto.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/lint/__tests__/no-itemspacing-auto.test.ts +import { describe, it, expect } from "vitest"; +import { noItemSpacingAutoRule } from "../rules/no-itemspacing-auto.js"; + +describe("no-itemspacing-auto rule", () => { + it.each([ + `frame.itemSpacing = "AUTO";`, + `frame.itemSpacing = 'AUTO';`, + `{ itemSpacing: "AUTO" }`, + `{itemSpacing:'AUTO'}`, + ])("flags %s", (code) => { + const results = noItemSpacingAutoRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("error"); + }); + + it("does not flag numeric itemSpacing", () => { + expect(noItemSpacingAutoRule.check(`frame.itemSpacing = 16;`)).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-itemspacing-auto` +Expected: FAIL. + +- [ ] **Step 3: Implement the rule** + +```typescript +// packages/mcp-server/src/lint/rules/no-itemspacing-auto.ts +import type { LintRule, LintResult } from "../types.js"; + +const PATTERN = /itemSpacing\s*[:=]\s*["']AUTO["']/; + +export const noItemSpacingAutoRule: LintRule = { + id: "no-itemspacing-auto", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (PATTERN.test(lines[i])) { + out.push({ + ruleId: "no-itemspacing-auto", + severity: "error", + line: i + 1, + message: 'itemSpacing = "AUTO" is rejected at runtime. Use PluginOS.layoutSpaceBetween(frame, { growChild }) instead.', + }); + } + } + return out; + }, +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-itemspacing-auto` +Expected: 5 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add no-itemspacing-auto lint rule`. + +--- + +## Task 6: `invalid-variable-name` lint rule (TDD) + +**Files:** +- Create: `packages/mcp-server/src/lint/rules/invalid-variable-name.ts` +- Create: `packages/mcp-server/src/lint/__tests__/invalid-variable-name.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/lint/__tests__/invalid-variable-name.test.ts +import { describe, it, expect } from "vitest"; +import { invalidVariableNameRule } from "../rules/invalid-variable-name.js"; + +describe("invalid-variable-name rule", () => { + it("flags dot in variable name", () => { + const code = `figma.variables.createVariable("Spacing/1.5", coll, "FLOAT");`; + const results = invalidVariableNameRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("error"); + expect(results[0].message).toContain("."); + expect(results[0].fix).toContain("1_5"); + }); + + it("flags hyphen in variable name", () => { + const code = `figma.variables.createVariable("body-medium", coll, "STRING");`; + const results = invalidVariableNameRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].fix).toContain("body_medium"); + }); + + it("flags space in variable name", () => { + const code = `figma.variables.createVariable("body text", coll, "STRING");`; + expect(invalidVariableNameRule.check(code)).toHaveLength(1); + }); + + it("does not flag valid names", () => { + expect(invalidVariableNameRule.check(`figma.variables.createVariable("Spacing_1_5", coll, "FLOAT");`)).toEqual([]); + expect(invalidVariableNameRule.check(`figma.variables.createVariable("h1", coll, "STRING");`)).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/invalid-variable-name` +Expected: FAIL. + +- [ ] **Step 3: Implement the rule** + +```typescript +// packages/mcp-server/src/lint/rules/invalid-variable-name.ts +import type { LintRule, LintResult } from "../types.js"; + +const CALL = /createVariable\s*\(\s*["']([^"']+)["']/g; +const VALID = /^[A-Za-z0-9_/]+$/; + +export const invalidVariableNameRule: LintRule = { + id: "invalid-variable-name", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + CALL.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = CALL.exec(line)) !== null) { + const name = m[1]; + if (!VALID.test(name)) { + const bad = [...name].find((ch) => !/[A-Za-z0-9_/]/.test(ch)) ?? "?"; + const sanitized = name.replace(/[^A-Za-z0-9_/]/g, "_"); + out.push({ + ruleId: "invalid-variable-name", + severity: "error", + line: i + 1, + message: `Variable name "${name}" contains invalid character "${bad}". Use [A-Za-z0-9_] (slashes allowed for nesting).`, + fix: sanitized, + }); + } + } + } + return out; + }, +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/invalid-variable-name` +Expected: 4 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add invalid-variable-name lint rule`. + +--- + +## Task 7: `no-hyphenated-plugindata-key` lint rule (TDD) + +**Files:** +- Create: `packages/mcp-server/src/lint/rules/no-hyphenated-plugindata-key.ts` +- Create: `packages/mcp-server/src/lint/__tests__/no-hyphenated-plugindata-key.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/lint/__tests__/no-hyphenated-plugindata-key.test.ts +import { describe, it, expect } from "vitest"; +import { noHyphenatedPluginDataKeyRule } from "../rules/no-hyphenated-plugindata-key.js"; + +describe("no-hyphenated-plugindata-key rule", () => { + it("flags hyphen in setPluginData key", () => { + const code = `figma.root.setPluginData("my-key", "value");`; + const results = noHyphenatedPluginDataKeyRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("error"); + expect(results[0].fix).toContain("my_key"); + }); + + it("flags hyphen in setSharedPluginData key", () => { + const code = `node.setSharedPluginData("ns", "shared-key", "v");`; + expect(noHyphenatedPluginDataKeyRule.check(code)).toHaveLength(1); + }); + + it("does not flag valid keys", () => { + expect(noHyphenatedPluginDataKeyRule.check(`figma.root.setPluginData("my_key", "v");`)).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-hyphenated-plugindata-key` +Expected: FAIL. + +- [ ] **Step 3: Implement the rule** + +```typescript +// packages/mcp-server/src/lint/rules/no-hyphenated-plugindata-key.ts +import type { LintRule, LintResult } from "../types.js"; + +// setPluginData(key, value) — flag hyphen in key (first arg) +// setSharedPluginData(ns, key, value) — flag hyphen in key (second arg) +const SET_PLUGIN_DATA = /setPluginData\s*\(\s*["']([^"']+)["']/g; +const SET_SHARED = /setSharedPluginData\s*\(\s*["'][^"']*["']\s*,\s*["']([^"']+)["']/g; + +export const noHyphenatedPluginDataKeyRule: LintRule = { + id: "no-hyphenated-plugindata-key", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + for (const pattern of [SET_PLUGIN_DATA, SET_SHARED]) { + pattern.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = pattern.exec(line)) !== null) { + const key = m[1]; + if (key.includes("-")) { + out.push({ + ruleId: "no-hyphenated-plugindata-key", + severity: "error", + line: i + 1, + message: `Plugin data key "${key}" contains a hyphen. Use underscore: "${key.replace(/-/g, "_")}".`, + fix: key.replace(/-/g, "_"), + }); + } + } + } + } + return out; + }, +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-hyphenated-plugindata-key` +Expected: 3 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add no-hyphenated-plugindata-key lint rule`. + +--- + +## Task 8: `no-text-encoders` lint rule (TDD) + +**Files:** +- Create: `packages/mcp-server/src/lint/rules/no-text-encoders.ts` +- Create: `packages/mcp-server/src/lint/__tests__/no-text-encoders.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/lint/__tests__/no-text-encoders.test.ts +import { describe, it, expect } from "vitest"; +import { noTextEncodersRule } from "../rules/no-text-encoders.js"; + +describe("no-text-encoders rule", () => { + it.each([ + [`new TextEncoder().encode("x")`, "TextEncoder"], + [`const d = new TextDecoder();`, "TextDecoder"], + [`await crypto.subtle.digest("SHA-256", buf);`, "crypto.subtle"], + ])("flags %s", (code, mention) => { + const results = noTextEncodersRule.check(code); + expect(results).toHaveLength(1); + expect(results[0].severity).toBe("error"); + expect(results[0].message).toContain(mention); + }); + + it("does not flag arbitrary code", () => { + expect(noTextEncodersRule.check(`const x = "encoder";`)).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-text-encoders` +Expected: FAIL. + +- [ ] **Step 3: Implement the rule** + +```typescript +// packages/mcp-server/src/lint/rules/no-text-encoders.ts +import type { LintRule, LintResult } from "../types.js"; + +const PATTERNS: Array<[RegExp, string]> = [ + [/\bTextEncoder\b/, "TextEncoder"], + [/\bTextDecoder\b/, "TextDecoder"], + [/\bcrypto\.subtle\b/, "crypto.subtle"], +]; + +export const noTextEncodersRule: LintRule = { + id: "no-text-encoders", + severity: "error", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + for (const [pattern, name] of PATTERNS) { + if (pattern.test(lines[i])) { + out.push({ + ruleId: "no-text-encoders", + severity: "error", + line: i + 1, + message: `${name} is unavailable in the Figma plugin sandbox. Compute via plain JS string/array operations.`, + }); + } + } + } + return out; + }, +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/no-text-encoders` +Expected: 4 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add no-text-encoders lint rule`. + +--- + +## Task 9: `prefer-helpers` lint rule (TDD) + +**Files:** +- Create: `packages/mcp-server/src/lint/rules/prefer-helpers.ts` +- Create: `packages/mcp-server/src/lint/__tests__/prefer-helpers.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/lint/__tests__/prefer-helpers.test.ts +import { describe, it, expect } from "vitest"; +import { preferHelpersRule } from "../rules/prefer-helpers.js"; + +describe("prefer-helpers rule", () => { + it("hints createStyledText when createText + loadFontAsync co-occur", () => { + const code = ` +await figma.loadFontAsync({ family: "Inter", style: "Regular" }); +const t = figma.createText(); +t.characters = "hi"; +`; + const results = preferHelpersRule.check(code); + expect(results.some((r) => r.message.includes("createStyledText"))).toBe(true); + expect(results.every((r) => r.severity === "hint")).toBe(true); + }); + + it("hints bindSpacing when 3+ padding bindings are set", () => { + const code = ` +node.setBoundVariable("paddingTop", v); +node.setBoundVariable("paddingBottom", v); +node.setBoundVariable("paddingLeft", v); +`; + const results = preferHelpersRule.check(code); + expect(results.some((r) => r.message.includes("bindSpacing"))).toBe(true); + }); + + it("does not hint for unrelated code", () => { + expect(preferHelpersRule.check(`const x = 1;`)).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/prefer-helpers` +Expected: FAIL. + +- [ ] **Step 3: Implement the rule** + +```typescript +// packages/mcp-server/src/lint/rules/prefer-helpers.ts +import type { LintRule, LintResult } from "../types.js"; + +const PADDING_FIELDS = ["paddingTop", "paddingBottom", "paddingLeft", "paddingRight", "itemSpacing"]; + +export const preferHelpersRule: LintRule = { + id: "prefer-helpers", + severity: "hint", + check(code: string): LintResult[] { + const out: LintResult[] = []; + const lines = code.split("\n"); + const hasCreateText = /\bfigma\.createText\s*\(/.test(code); + const hasLoadFont = /\bfigma\.loadFontAsync\s*\(/.test(code); + if (hasCreateText && hasLoadFont) { + const idx = lines.findIndex((l) => /\bfigma\.createText\s*\(/.test(l)); + out.push({ + ruleId: "prefer-helpers", + severity: "hint", + line: idx + 1, + message: "Consider PluginOS.createStyledText({ characters, textStyleId, family, weight, size, fillStyleId, name }) — handles font load + create + style binding in one call.", + }); + } + let paddingCount = 0; + let firstPaddingLine = -1; + for (let i = 0; i < lines.length; i++) { + for (const field of PADDING_FIELDS) { + const pattern = new RegExp(`setBoundVariable\\s*\\(\\s*["']${field}["']`); + if (pattern.test(lines[i])) { + paddingCount++; + if (firstPaddingLine === -1) firstPaddingLine = i + 1; + } + } + } + if (paddingCount >= 3) { + out.push({ + ruleId: "prefer-helpers", + severity: "hint", + line: firstPaddingLine, + message: "Consider PluginOS.bindSpacing(node, { padding, itemSpacing }) — binds all padding/itemSpacing fields in one call.", + }); + } + return out; + }, +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/prefer-helpers` +Expected: 3 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add prefer-helpers lint rule`. + +--- + +## Task 10: Wire all rules into the registry + +**Files:** +- Modify: `packages/mcp-server/src/lint/index.ts` + +- [ ] **Step 1: Write the failing test** + +Add to `packages/mcp-server/src/lint/__tests__/registry.test.ts`: + +```typescript +import { describe, it, expect } from "vitest"; +import { runLint } from "../index.js"; + +describe("default ruleset wired", () => { + it("flags figma.notify via runLint", () => { + const results = runLint(`figma.notify("hi")`); + expect(results.some((r) => r.ruleId === "no-notify")).toBe(true); + }); + + it("flags itemSpacing AUTO via runLint", () => { + const results = runLint(`frame.itemSpacing = "AUTO"`); + expect(results.some((r) => r.ruleId === "no-itemspacing-auto")).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/__tests__/registry` +Expected: FAIL — rules not registered by default. + +- [ ] **Step 3: Register all rules at module load** + +Edit `packages/mcp-server/src/lint/index.ts`: + +```typescript +import type { LintResult, LintRule } from "./types.js"; +import { noNotifyRule } from "./rules/no-notify.js"; +import { noSyncStyleSettersRule } from "./rules/no-sync-style-setters.js"; +import { noItemSpacingAutoRule } from "./rules/no-itemspacing-auto.js"; +import { invalidVariableNameRule } from "./rules/invalid-variable-name.js"; +import { noHyphenatedPluginDataKeyRule } from "./rules/no-hyphenated-plugindata-key.js"; +import { noTextEncodersRule } from "./rules/no-text-encoders.js"; +import { preferHelpersRule } from "./rules/prefer-helpers.js"; + +const rules: LintRule[] = []; + +export function registerRule(rule: LintRule): void { + rules.push(rule); +} + +export function runLint(code: string): LintResult[] { + const out: LintResult[] = []; + for (const rule of rules) { + for (const result of rule.check(code)) { + out.push(result); + } + } + return out; +} + +// Register default ruleset +registerRule(noNotifyRule); +registerRule(noSyncStyleSettersRule); +registerRule(noItemSpacingAutoRule); +registerRule(invalidVariableNameRule); +registerRule(noHyphenatedPluginDataKeyRule); +registerRule(noTextEncodersRule); +registerRule(preferHelpersRule); + +export type { LintResult, LintRule, LintSeverity } from "./types.js"; +``` + +- [ ] **Step 4: Run all lint tests** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- lint/` +Expected: all green. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): register default lint ruleset`. + +--- + +## Task 11: Prelude source blob — module skeleton + +**Files:** +- Create: `packages/mcp-server/src/prelude/source.ts` +- Create: `packages/mcp-server/src/prelude/index.ts` + +This task only creates the skeleton and `wrapScript` function. Each helper's implementation lands in subsequent tasks. + +- [ ] **Step 1: Write the failing test for wrapScript** + +```typescript +// packages/mcp-server/src/prelude/__tests__/wrap.test.ts +import { describe, it, expect } from "vitest"; +import { wrapScript, PRELUDE_VERSION } from "../index.js"; + +describe("wrapScript", () => { + it("returns wrapped with userJs after prelude", () => { + const userJs = `return figma.currentPage.name;`; + const { wrapped } = wrapScript(userJs); + expect(wrapped.endsWith(userJs)).toBe(true); + expect(wrapped).toContain("PluginOS"); + }); + + it("reports prelude line count consistent with prelude size", () => { + const { wrapped, preludeLineCount } = wrapScript(`x`); + const lines = wrapped.split("\n"); + // user's "x" is on lines[preludeLineCount] (0-based) + expect(lines[preludeLineCount]).toBe("x"); + }); + + it("exports a non-empty PRELUDE_VERSION", () => { + expect(PRELUDE_VERSION).toMatch(/^\d+\.\d+\.\d+/); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/wrap` +Expected: FAIL (module not found). + +- [ ] **Step 3: Create the source blob with placeholder helpers** + +```typescript +// packages/mcp-server/src/prelude/source.ts +// The JS source injected into every execute_figma script. Runs in Figma's plugin sandbox. +// Helpers are filled in by subsequent tasks. This file is a single string template +// because the sandbox has no module system — everything must be inline. +export const PRELUDE_SOURCE = `// --- PluginOS prelude --- +;(function(){ + var P = {}; + P.version = '__PRELUDE_VERSION__'; + // Helpers added in subsequent tasks. + globalThis.PluginOS = P; +})(); +// --- end prelude --- +`; +``` + +- [ ] **Step 4: Create the index module** + +```typescript +// packages/mcp-server/src/prelude/index.ts +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { PRELUDE_SOURCE } from "./source.js"; + +function readPackageVersion(): string { + const dir = dirname(fileURLToPath(import.meta.url)); + // dist runtime: dir = .../packages/mcp-server/dist/prelude + // src runtime (tests): dir = .../packages/mcp-server/src/prelude + const candidates = [ + join(dir, "..", "..", "package.json"), + join(dir, "..", "..", "..", "package.json"), + ]; + for (const p of candidates) { + try { + const pkg = JSON.parse(readFileSync(p, "utf8")); + if (pkg.name === "pluginos" && typeof pkg.version === "string") return pkg.version; + } catch { + // try next + } + } + return "0.0.0"; +} + +export const PRELUDE_VERSION: string = readPackageVersion(); + +const RESOLVED_PRELUDE = PRELUDE_SOURCE.replace("__PRELUDE_VERSION__", PRELUDE_VERSION); +const PRELUDE_LINES = RESOLVED_PRELUDE.split("\n").length; + +export function wrapScript(userJs: string): { wrapped: string; preludeLineCount: number } { + return { + wrapped: RESOLVED_PRELUDE + userJs, + preludeLineCount: PRELUDE_LINES - 1, // last line of prelude is newline-terminated + }; +} +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/wrap` +Expected: 3 passed. + +- [ ] **Step 6: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add prelude module skeleton and wrapScript`. + +--- + +## Task 12: Helper — `createStyledText` (TDD with vm sandbox) + +**Files:** +- Modify: `packages/mcp-server/src/prelude/source.ts` +- Create: `packages/mcp-server/src/prelude/__tests__/helpers.test.ts` + +This and Tasks 13-16 use Node's `vm` module to execute the prelude with a mock `figma` global, so we can verify behavior without a real Figma sandbox. + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/mcp-server/src/prelude/__tests__/helpers.test.ts +import { describe, it, expect } from "vitest"; +import vm from "node:vm"; +import { wrapScript } from "../index.js"; + +function makeContext(figma: object) { + const ctx: Record = { figma, console }; + vm.createContext(ctx); + return ctx; +} + +function runWith(figma: object, userJs: string): Record { + const ctx = makeContext(figma); + const { wrapped } = wrapScript(`${userJs}\nglobalThis.__out = out;`); + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + return ctx; +} + +describe("createStyledText", () => { + it("calls loadFontAsync, createText, setTextStyleIdAsync, and sets characters", async () => { + const calls: string[] = []; + const textNode: Record = { + setTextStyleIdAsync: async (id: string) => { calls.push("setTextStyle:" + id); }, + setFillStyleIdAsync: async (id: string) => { calls.push("setFill:" + id); }, + }; + const figma = { + loadFontAsync: async (font: { family: string; style: string }) => { calls.push(`loadFont:${font.family}/${font.style}`); }, + createText: () => { calls.push("createText"); return textNode; }, + }; + const userJs = ` +const out = await PluginOS.createStyledText({ + characters: "Hello", + family: "Inter", + weight: "Bold", + size: 16, + textStyleId: "tsid", + fillStyleId: "fsid", + name: "Title", +}); +`; + const ctx = runWith(figma, userJs); + await new Promise((r) => setTimeout(r, 10)); + expect(calls).toContain("loadFont:Inter/Bold"); + expect(calls).toContain("createText"); + expect(calls).toContain("setTextStyle:tsid"); + expect(calls).toContain("setFill:fsid"); + expect(textNode.characters).toBe("Hello"); + expect(textNode.name).toBe("Title"); + expect(ctx.__out).toBe(textNode); + }); + + it("throws when neither textStyleId nor family+size is provided", async () => { + const figma = { loadFontAsync: async () => {}, createText: () => ({}) }; + const ctx = makeContext(figma); + const { wrapped } = wrapScript(` +try { + await PluginOS.createStyledText({ characters: "x" }); + globalThis.__err = null; +} catch (e) { + globalThis.__err = String(e.message); +} +`); + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(String(ctx.__err)).toContain("[PluginOS.createStyledText]"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: FAIL — `PluginOS.createStyledText is not a function`. + +- [ ] **Step 3: Add createStyledText to the prelude source** + +Replace the placeholder body in `packages/mcp-server/src/prelude/source.ts`: + +```typescript +export const PRELUDE_SOURCE = `// --- PluginOS prelude --- +;(function(){ + var P = {}; + P.version = '__PRELUDE_VERSION__'; + + P.createStyledText = async function(opts) { + if (!opts.textStyleId && (!opts.family || opts.size == null)) { + throw new Error('[PluginOS.createStyledText] requires textStyleId or (family + size)'); + } + var weight = opts.weight || 'Regular'; + if (opts.family) { + await figma.loadFontAsync({ family: opts.family, style: weight }); + } + var node = figma.createText(); + if (opts.textStyleId) { + await node.setTextStyleIdAsync(opts.textStyleId); + } else { + node.fontName = { family: opts.family, style: weight }; + node.fontSize = opts.size; + } + node.characters = opts.characters; + if (opts.fillStyleId) { + await node.setFillStyleIdAsync(opts.fillStyleId); + } + if (opts.name) node.name = opts.name; + return node; + }; + + globalThis.PluginOS = P; +})(); +// --- end prelude --- +`; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: 2 passed. + +- [ ] **Step 5: Verify wrapScript line count still consistent** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/wrap` +Expected: 3 passed. + +- [ ] **Step 6: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add PluginOS.createStyledText helper`. + +--- + +## Task 13: Helper — `bindSpacing` (TDD) + +**Files:** +- Modify: `packages/mcp-server/src/prelude/source.ts` +- Modify: `packages/mcp-server/src/prelude/__tests__/helpers.test.ts` + +- [ ] **Step 1: Write the failing test** + +Append to `helpers.test.ts`: + +```typescript +describe("bindSpacing", () => { + it("binds all four padding fields when given `padding`", async () => { + const bound: Array<[string, string]> = []; + const node = { + layoutMode: "VERTICAL", + setBoundVariable: (field: string, v: { id: string }) => bound.push([field, v.id]), + }; + const figma = {}; + const ctx = makeContext(figma); + const { wrapped } = wrapScript(` +const node = globalThis.__node; +await PluginOS.bindSpacing(node, { padding: { id: "v1" } }); +`); + (ctx as Record).__node = node; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + const fields = bound.map(([f]) => f).sort(); + expect(fields).toEqual(["paddingBottom", "paddingLeft", "paddingRight", "paddingTop"]); + expect(bound.every(([, id]) => id === "v1")).toBe(true); + }); + + it("specificity: paddingTop overrides padding", async () => { + const bound: Array<[string, string]> = []; + const node = { + layoutMode: "VERTICAL", + setBoundVariable: (field: string, v: { id: string }) => bound.push([field, v.id]), + }; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +await PluginOS.bindSpacing(globalThis.__node, { padding: { id: "all" }, paddingTop: { id: "top" } }); +`); + (ctx as Record).__node = node; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + const top = bound.find(([f]) => f === "paddingTop"); + expect(top).toEqual(["paddingTop", "top"]); + }); + + it("no-ops on non-autolayout node", async () => { + const bound: Array<[string, string]> = []; + const node = { + layoutMode: "NONE", + setBoundVariable: (field: string, v: { id: string }) => bound.push([field, v.id]), + }; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +await PluginOS.bindSpacing(globalThis.__node, { padding: { id: "v1" } }); +`); + (ctx as Record).__node = node; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(bound).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: 3 new failures. + +- [ ] **Step 3: Add bindSpacing to the prelude source** + +Insert into `PRELUDE_SOURCE` (inside the IIFE, after `createStyledText`): + +```javascript + P.bindSpacing = async function(node, vars) { + if (!node || !('layoutMode' in node) || node.layoutMode === 'NONE') return; + function pick(specific, axis, all) { + if (specific) return specific; + if (axis) return axis; + if (all) return all; + return null; + } + var pairs = [ + ['paddingTop', pick(vars.paddingTop, vars.paddingY, vars.padding)], + ['paddingBottom', pick(vars.paddingBottom, vars.paddingY, vars.padding)], + ['paddingLeft', pick(vars.paddingLeft, vars.paddingX, vars.padding)], + ['paddingRight', pick(vars.paddingRight, vars.paddingX, vars.padding)], + ['itemSpacing', vars.itemSpacing || null], + ]; + for (var i = 0; i < pairs.length; i++) { + var field = pairs[i][0]; + var v = pairs[i][1]; + if (v) node.setBoundVariable(field, v); + } + }; +``` + +- [ ] **Step 4: Run tests to verify all pass** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: 5 passed (2 + 3). + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add PluginOS.bindSpacing helper`. + +--- + +## Task 14: Helper — `combineAsVariantsTiled` (TDD) + +**Files:** +- Modify: `packages/mcp-server/src/prelude/source.ts` +- Modify: `packages/mcp-server/src/prelude/__tests__/helpers.test.ts` + +- [ ] **Step 1: Write the failing test** + +Append to `helpers.test.ts`: + +```typescript +describe("combineAsVariantsTiled", () => { + it("calls combineAsVariants and sets layout fields", async () => { + const set: Record = { resize: function(w: number, h: number) { this.width = w; this.height = h; } }; + set.width = 0; + set.height = 0; + const calls: string[] = []; + const figma = { + combineAsVariants: (cells: unknown[], parent: unknown) => { calls.push("combine:" + cells.length); return set; }, + }; + const cells = [{ width: 100, height: 50 }, { width: 100, height: 50 }, { width: 100, height: 50 }, { width: 100, height: 50 }]; + const ctx = makeContext(figma); + const { wrapped } = wrapScript(` +const set = PluginOS.combineAsVariantsTiled(globalThis.__cells, {}, { cols: 2, gutter: 10 }); +globalThis.__out = set; +`); + (ctx as Record).__cells = cells; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(calls).toContain("combine:4"); + expect(set.layoutMode).toBe("HORIZONTAL"); + expect(set.layoutWrap).toBe("WRAP"); + expect(set.itemSpacing).toBe(10); + expect(set.primaryAxisSizingMode).toBe("FIXED"); + expect(set.counterAxisSizingMode).toBe("AUTO"); + expect(set.width).toBeGreaterThan(0); + expect(ctx.__out).toBe(set); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: new failure. + +- [ ] **Step 3: Add combineAsVariantsTiled to the prelude source** + +Insert into `PRELUDE_SOURCE` (after `bindSpacing`): + +```javascript + P.combineAsVariantsTiled = function(cells, parent, opts) { + opts = opts || {}; + var cols = opts.cols || Math.ceil(Math.sqrt(cells.length)); + var gutter = opts.gutter == null ? 16 : opts.gutter; + var layoutMode = opts.layoutMode || 'HORIZONTAL'; + var wrap = opts.wrap !== false; + var set = figma.combineAsVariants(cells, parent); + set.layoutMode = layoutMode; + if (wrap) set.layoutWrap = 'WRAP'; + set.itemSpacing = gutter; + set.counterAxisSpacing = gutter; + set.primaryAxisSizingMode = 'FIXED'; + set.counterAxisSizingMode = 'AUTO'; + var width = opts.width; + if (width == null) { + var cellW = cells[0] && cells[0].width ? cells[0].width : 0; + width = cols * cellW + (cols - 1) * gutter + 32; + } + set.resize(width, set.height || 100); + return set; + }; +``` + +- [ ] **Step 4: Run tests to verify pass** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: 6 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add PluginOS.combineAsVariantsTiled helper`. + +--- + +## Task 15: Helper — `tileTopLevel` (TDD) + +**Files:** +- Modify: `packages/mcp-server/src/prelude/source.ts` +- Modify: `packages/mcp-server/src/prelude/__tests__/helpers.test.ts` + +- [ ] **Step 1: Write the failing test** + +Append to `helpers.test.ts`: + +```typescript +describe("tileTopLevel", () => { + it("places nodes in a grid with the configured cols and gutter", async () => { + const appended: unknown[] = []; + const page = { appendChild: (n: unknown) => appended.push(n) }; + const nodes = [ + { width: 100, height: 50, x: 0, y: 0 }, + { width: 100, height: 50, x: 0, y: 0 }, + { width: 100, height: 50, x: 0, y: 0 }, + { width: 100, height: 50, x: 0, y: 0 }, + ]; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +const place = PluginOS.tileTopLevel(globalThis.__page, { cols: 2, gutter: 10 }); +globalThis.__nodes.forEach(place); +`); + (ctx as Record).__page = page; + (ctx as Record).__nodes = nodes; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(nodes[0].x).toBe(0); + expect(nodes[0].y).toBe(0); + expect(nodes[1].x).toBe(110); + expect(nodes[1].y).toBe(0); + expect(nodes[2].x).toBe(0); + expect(nodes[2].y).toBe(60); + expect(nodes[3].x).toBe(110); + expect(nodes[3].y).toBe(60); + expect(appended).toEqual(nodes); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: new failure. + +- [ ] **Step 3: Add tileTopLevel to the prelude source** + +Insert into `PRELUDE_SOURCE` (after `combineAsVariantsTiled`): + +```javascript + P.tileTopLevel = function(page, opts) { + opts = opts || {}; + var cols = opts.cols || 4; + var gutter = opts.gutter == null ? 64 : opts.gutter; + var origin = opts.origin || { x: 0, y: 0 }; + var i = 0; + var rowH = 0; + var cursorX = origin.x; + var cursorY = origin.y; + return function place(node) { + var col = i % cols; + if (col === 0 && i > 0) { + cursorY += rowH + gutter; + cursorX = origin.x; + rowH = 0; + } + node.x = cursorX; + node.y = cursorY; + page.appendChild(node); + cursorX += node.width + gutter; + if (node.height > rowH) rowH = node.height; + i++; + }; + }; +``` + +- [ ] **Step 4: Run tests to verify pass** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: 7 passed. + +- [ ] **Step 5: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add PluginOS.tileTopLevel helper`. + +--- + +## Task 16: Helper — `layoutSpaceBetween` (TDD) + +**Files:** +- Modify: `packages/mcp-server/src/prelude/source.ts` +- Modify: `packages/mcp-server/src/prelude/__tests__/helpers.test.ts` + +- [ ] **Step 1: Write the failing test** + +Append to `helpers.test.ts`: + +```typescript +describe("layoutSpaceBetween", () => { + it("for 2 children, picks the last child as grow target (non-TEXT → layoutGrow=1)", async () => { + const left = { type: "FRAME", layoutGrow: 0 }; + const right = { type: "FRAME", layoutGrow: 0 }; + const frame: Record = { primaryAxisAlignItems: "INIT" }; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +PluginOS.layoutSpaceBetween(globalThis.__frame, { children: [globalThis.__left, globalThis.__right] }); +`); + (ctx as Record).__frame = frame; + (ctx as Record).__left = left; + (ctx as Record).__right = right; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(frame.primaryAxisAlignItems).toBe("MIN"); + expect(right.layoutGrow).toBe(1); + expect(left.layoutGrow).toBe(0); + }); + + it("for TEXT grow target, uses layoutSizingHorizontal=FILL", async () => { + const left = { type: "TEXT", layoutSizingHorizontal: "HUG" }; + const right = { type: "FRAME", layoutGrow: 0 }; + const frame: Record = {}; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +PluginOS.layoutSpaceBetween(globalThis.__frame, { growChild: globalThis.__left }); +`); + (ctx as Record).__frame = frame; + (ctx as Record).__left = left; + (ctx as Record).__right = right; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(left.layoutSizingHorizontal).toBe("FILL"); + }); + + it("for 3 children, picks the middle child", async () => { + const a = { type: "FRAME", layoutGrow: 0 }; + const b = { type: "FRAME", layoutGrow: 0 }; + const c = { type: "FRAME", layoutGrow: 0 }; + const frame: Record = {}; + const ctx = makeContext({}); + const { wrapped } = wrapScript(` +PluginOS.layoutSpaceBetween(globalThis.__frame, { children: [globalThis.__a, globalThis.__b, globalThis.__c] }); +`); + (ctx as Record).__frame = frame; + (ctx as Record).__a = a; + (ctx as Record).__b = b; + (ctx as Record).__c = c; + vm.runInContext(`(async()=>{${wrapped}})()`, ctx); + await new Promise((r) => setTimeout(r, 10)); + expect(b.layoutGrow).toBe(1); + expect(a.layoutGrow).toBe(0); + expect(c.layoutGrow).toBe(0); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: 3 new failures. + +- [ ] **Step 3: Add layoutSpaceBetween to the prelude source** + +Insert into `PRELUDE_SOURCE` (after `tileTopLevel`): + +```javascript + P.layoutSpaceBetween = function(frame, opts) { + frame.primaryAxisAlignItems = 'MIN'; + var growChild = opts.growChild; + if (!growChild && opts.children) { + var kids = opts.children; + if (kids.length >= 3) growChild = kids[Math.floor(kids.length / 2)]; + else if (kids.length === 2) growChild = kids[kids.length - 1]; + } + if (!growChild) throw new Error('[PluginOS.layoutSpaceBetween] no growChild resolvable'); + if (growChild.type === 'TEXT') { + growChild.layoutSizingHorizontal = 'FILL'; + } else { + growChild.layoutGrow = 1; + } + }; +``` + +- [ ] **Step 4: Run tests to verify pass** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/__tests__/helpers` +Expected: 10 passed. + +- [ ] **Step 5: Verify wrapScript line count test still passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- prelude/` +Expected: all green. + +- [ ] **Step 6: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): add PluginOS.layoutSpaceBetween helper`. + +--- + +## Task 17: Wire lint + prelude into `execute_figma` handler (TDD) + +**Files:** +- Modify: `packages/mcp-server/src/server.ts` +- Create: `packages/mcp-server/src/__tests__/execute-with-lint.test.ts` + +- [ ] **Step 1: Write the failing integration test** + +```typescript +// packages/mcp-server/src/__tests__/execute-with-lint.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 createMockBridge(): IPluginBridge { + return { + sendAndWait: vi.fn().mockResolvedValue({ + id: "test", + type: "result", + success: true, + result: { foo: "bar" }, + }), + getStatus: vi.fn().mockReturnValue({ + connected: true, + fileKey: "mock-file", + fileName: "Mock File", + currentPage: "Page 1", + port: 9500, + connectedFiles: 1, + }), + listFiles: vi.fn().mockReturnValue([]), + isConnected: vi.fn().mockReturnValue(true), + }; +} + +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("execute_figma with lint + prelude", () => { + it("returns lint warnings alongside the result for a script using figma.notify", async () => { + const bridge = createMockBridge(); + const client = await setupClient(bridge); + const res = (await client.callTool({ + name: "execute_figma", + arguments: { code: `figma.notify("hi"); return 1;` }, + })) as ToolResult; + expect(res.isError).toBeFalsy(); + const payload = JSON.parse(res.content[0].text); + expect(payload.result).toEqual({ foo: "bar" }); + expect(Array.isArray(payload.lint)).toBe(true); + expect(payload.lint.some((r: { ruleId: string }) => r.ruleId === "no-notify")).toBe(true); + expect(typeof payload.preludeVersion).toBe("string"); + expect(typeof payload.durationMs).toBe("number"); + }); + + it("sends the wrapped (prelude + user) script to the bridge", async () => { + const bridge = createMockBridge(); + const client = await setupClient(bridge); + await client.callTool({ + name: "execute_figma", + arguments: { code: `return PluginOS.version;` }, + }); + const sendMock = bridge.sendAndWait as ReturnType; + const [msg] = sendMock.mock.calls[0]; + expect(msg.type).toBe("execute"); + expect(msg.code).toContain("PluginOS"); + expect(msg.code).toContain("return PluginOS.version;"); + }); + + it("returns empty lint array for clean scripts", async () => { + const bridge = createMockBridge(); + const client = await setupClient(bridge); + const res = (await client.callTool({ + name: "execute_figma", + arguments: { code: `return figma.currentPage.name;` }, + })) as ToolResult; + const payload = JSON.parse(res.content[0].text); + expect(payload.lint).toEqual([]); + }); +}); +``` + +- [ ] **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__/execute-with-lint` +Expected: FAIL — handler doesn't wrap, doesn't lint. + +- [ ] **Step 3: Modify the `execute_figma` handler in `server.ts`** + +In `packages/mcp-server/src/server.ts`, add to the imports block at the top: + +```typescript +import { wrapScript, PRELUDE_VERSION } from "./prelude/index.js"; +import { runLint } from "./lint/index.js"; +``` + +Then replace the `execute_figma` tool body (lines 131-167) — keep the tool registration, replace the handler: + +```typescript + async ({ code, timeout, file_key }) => { + const safeTimeout = Math.min(timeout, 30000); + const lint = runLint(code); + const { wrapped } = wrapScript(code); + const msg = createExecuteMessage(wrapped, safeTimeout); + const startedAt = Date.now(); + + try { + const result = await bridge.sendAndWait(msg, safeTimeout + 2000, file_key); + const durationMs = Date.now() - startedAt; + if (result.success) { + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + result: result.result, + lint, + preludeVersion: PRELUDE_VERSION, + durationMs, + }, + null, + 2 + ), + }, + ], + }; + } + return { + content: [ + { + type: "text" as const, + text: `Execution failed: ${result.error}`, + }, + ], + isError: true, + }; + } catch (err) { + return { + content: [ + { + type: "text" as const, + text: `Error: ${(err as Error).message}`, + }, + ], + isError: true, + }; + } + } +``` + +- [ ] **Step 4: Run integration test to verify pass** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server -- __tests__/execute-with-lint` +Expected: 3 passed. + +- [ ] **Step 5: Run all mcp-server tests to confirm no regressions** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/mcp-server` +Expected: all green (existing 6 test files + new ones). + +- [ ] **Step 6: Commit** + +Use `/commit` skill. Suggested message: `feat(mcp-server): wire prelude and lint into execute_figma`. + +--- + +## Task 18: Skill recipes generation script + +**Files:** +- Create: `packages/claude-plugin/scripts/sync-recipes.ts` +- Create: `packages/claude-plugin/__tests__/sync-recipes.test.ts` +- Modify: `packages/claude-plugin/package.json` + +The recipes are written by hand in this PR (5 helpers), but the sync script enforces they stay aligned with the prelude — if a helper is removed from the prelude, the recipe block is missing it on regen and CI fails. + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/claude-plugin/__tests__/sync-recipes.test.ts +import { describe, it, expect } from "vitest"; +import { generateRecipesSection } from "../scripts/sync-recipes.js"; + +describe("sync-recipes generator", () => { + it("includes all five helpers", () => { + const section = generateRecipesSection(); + expect(section).toContain("PluginOS.createStyledText"); + expect(section).toContain("PluginOS.bindSpacing"); + expect(section).toContain("PluginOS.combineAsVariantsTiled"); + expect(section).toContain("PluginOS.tileTopLevel"); + expect(section).toContain("PluginOS.layoutSpaceBetween"); + }); + + it("starts with the section header", () => { + expect(generateRecipesSection()).toMatch(/^## Recipes for bulk-seed scripts/m); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/claude-plugin -- sync-recipes` +Expected: FAIL — module not found. + +- [ ] **Step 3: Write the generator** + +```typescript +// packages/claude-plugin/scripts/sync-recipes.ts +import { writeFileSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +interface Recipe { + title: string; + dont: string; + doLine: string; + why: string; +} + +const RECIPES: Recipe[] = [ + { + title: "Styled text nodes", + dont: "createText + set fontName + set fontSize manually — leaves text unbound to styles.", + doLine: "PluginOS.createStyledText({ characters, textStyleId, fillStyleId, name })", + why: "load-font + create + bind-style + set-fill in one async call.", + }, + { + title: "Spacing variable bindings", + dont: "setBoundVariable per padding field — 5 lines per frame.", + doLine: "PluginOS.bindSpacing(node, { padding: spacingVar, itemSpacing: spacingVar })", + why: "binds all 4 padding fields + itemSpacing in one call. paddingX/Y/specific keys win over `padding`.", + }, + { + title: "Variant sets", + dont: "combineAsVariants then manually set layoutMode, wrap, sizing — variants stack at (0,0).", + doLine: "PluginOS.combineAsVariantsTiled(cells, parent, { cols, gutter })", + why: "applies HORIZONTAL + WRAP + FIXED width so variants render tiled instead of stacked.", + }, + { + title: "Top-level placement", + dont: "figma.createComponent() in a loop — every node lands at (0,0) and overlaps.", + doLine: "const place = PluginOS.tileTopLevel(figma.currentPage, { cols, gutter }); place(node);", + why: "maintains a placement cursor across a single execute_figma call.", + }, + { + title: "SPACE_BETWEEN auto-layout", + dont: 'frame.itemSpacing = "AUTO" — runtime-rejected. primaryAxisAlignItems = "SPACE_BETWEEN" collapses on inspect.', + doLine: "PluginOS.layoutSpaceBetween(frame, { growChild })", + why: "sets primaryAxisAlignItems = MIN and gives the grow target layoutGrow=1 (or layoutSizingHorizontal=FILL for TEXT).", + }, +]; + +export function generateRecipesSection(): string { + const intro = `## Recipes for bulk-seed scripts + +These helpers are available inside every \`execute_figma\` script. They prevent the most common bulk-seed bugs. +`; + const body = RECIPES.map((r) => `### ${r.title} +Don't: ${r.dont} +Do: ${r.doLine} +Why: ${r.why} +`).join("\n"); + return intro + "\n" + body; +} + +const MARKER_START = ""; +const MARKER_END = ""; + +export function applyRecipes(skillContent: string, recipes: string): string { + const block = `${MARKER_START}\n${recipes}\n${MARKER_END}`; + if (skillContent.includes(MARKER_START) && skillContent.includes(MARKER_END)) { + return skillContent.replace( + new RegExp(`${MARKER_START}[\\s\\S]*?${MARKER_END}`), + block + ); + } + return `${skillContent.trimEnd()}\n\n${block}\n`; +} + +function main(): void { + const path = resolve(__dirname, "..", "skills", "pluginos-figma", "SKILL.md"); + const before = readFileSync(path, "utf8"); + const after = applyRecipes(before, generateRecipesSection()); + if (before !== after) { + writeFileSync(path, after); + console.warn("sync-recipes: updated SKILL.md"); + } else { + console.warn("sync-recipes: SKILL.md already in sync"); + } +} + +// Only run as a script if invoked directly. +if (process.argv[1] && process.argv[1].endsWith("sync-recipes.ts")) { + main(); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test -w packages/claude-plugin -- sync-recipes` +Expected: 2 passed. + +- [ ] **Step 5: Add npm script to `packages/claude-plugin/package.json`** + +In the `scripts` object, add: + +```json +"sync-recipes": "tsx scripts/sync-recipes.ts" +``` + +- [ ] **Step 6: Commit** + +Use `/commit` skill. Suggested message: `feat(claude-plugin): add sync-recipes generator and tests`. + +--- + +## Task 19: Apply recipes to `SKILL.md` + +**Files:** +- Modify: `packages/claude-plugin/skills/pluginos-figma/SKILL.md` + +- [ ] **Step 1: Run the sync script** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm run sync-recipes -w packages/claude-plugin` +Expected: `sync-recipes: updated SKILL.md`. + +- [ ] **Step 2: Verify the section was appended** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && grep -A1 "Recipes for bulk-seed" packages/claude-plugin/skills/pluginos-figma/SKILL.md | head -5` +Expected: shows the header line. + +- [ ] **Step 3: Verify SKILL.md is under the 1150-token budget** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && wc -w packages/claude-plugin/skills/pluginos-figma/SKILL.md` + +Then run whichever existing budget check the CI uses. Locate it first: + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && grep -rn "1150\|skill.*budget\|token.*budget" .github/ packages/claude-plugin/ 2>/dev/null | head -10` + +Expected: there's an existing check (e.g., in `package.json` scripts or a CI workflow). Run it; expected: PASS, count under 1150. + +- [ ] **Step 4: Commit** + +Use `/commit` skill. Suggested message: `docs(claude-plugin): append PluginOS recipes section to skill`. + +--- + +## Task 20: Op count truth — `list_operations` includes total + +**Files:** +- Modify: `packages/bridge-plugin/src/operations/index.ts` (verify or add `total` to `__list_operations` response) +- Modify: `packages/mcp-server/src/server.ts` (only if the bridge response shape changes; the server passes results through, so likely no change needed) + +The bridge's internal `__list_operations` operation is the one called by the MCP `list_operations` tool. Check whether it already includes a count. + +- [ ] **Step 1: Find the `__list_operations` handler** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && grep -rn "__list_operations\|listOperations" packages/bridge-plugin/src/operations/ | head -20` +Expected: locates the handler. + +- [ ] **Step 2: Read it and verify response shape** + +Read the file the grep found. If the response is `{ operations: [...] }`, add `total: operations.length`. If it already includes a count, no change needed — move to Step 5. + +- [ ] **Step 3: Write a test for the new field** + +If the handler is in `packages/bridge-plugin/src/operations/registry.ts`, add to `packages/bridge-plugin/src/operations/__tests__/registry.test.ts` (create if needed): + +```typescript +import { describe, it, expect } from "vitest"; +import { listOperations } from "../registry.js"; + +describe("listOperations", () => { + it("returns a total field matching the array length", () => { + const result = listOperations(); + if (Array.isArray(result)) { + // Older shape — wrap in next step + expect(result.length).toBeGreaterThan(0); + } else { + expect(result.total).toBe(result.operations.length); + } + }); +}); +``` + +(Adjust based on what you see in Step 2; the test should assert the **new** shape after Step 4.) + +- [ ] **Step 4: Modify the handler to include `total`** + +Wherever the handler returns the ops list, wrap it as `{ operations, total: operations.length }`. Make sure all existing call sites tolerate the new shape — this may require updating the MCP `list_operations` tool's response stringification in `server.ts` to surface `total` in the JSON. + +- [ ] **Step 5: Run all tests** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test` +Expected: all green. + +- [ ] **Step 6: Commit** + +Use `/commit` skill. Suggested message: `feat(bridge-plugin): include total operation count in list_operations response`. + +--- + +## Task 21: Remove hardcoded op counts from docs + +**Files:** +- Modify: `INSTALL.md` +- Modify: `README.md` (if it mentions a specific count) + +- [ ] **Step 1: Find all hardcoded counts** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && grep -n "26 operations\|28 operations\|39 operations" README.md INSTALL.md packages/` +Expected: lists every occurrence. + +- [ ] **Step 2: Replace each with phrasing that doesn't bake in a number** + +In `INSTALL.md`, change the verification line: + +```diff +- 2. In your agent, ask: "list available pluginos operations". You should get a list of 39 operations. ++ 2. In your agent, ask: "list available pluginos operations". You should get a list of operations and their categories. +``` + +Apply the same replacement to any `README.md` occurrences. + +- [ ] **Step 3: Verify no remaining hardcoded counts** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && grep -rn "[0-9]\+ operations" README.md INSTALL.md packages/claude-plugin/skills/` +Expected: no matches. + +- [ ] **Step 4: Commit** + +Use `/commit` skill. Suggested message: `docs: stop hardcoding the operation count`. + +--- + +## Task 22: Full check + manual 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: Confirm test counts** + +Verify the new test files are picked up: +- `packages/mcp-server/src/lint/__tests__/registry.test.ts` +- `packages/mcp-server/src/lint/__tests__/no-notify.test.ts` +- `packages/mcp-server/src/lint/__tests__/no-sync-style-setters.test.ts` +- `packages/mcp-server/src/lint/__tests__/no-itemspacing-auto.test.ts` +- `packages/mcp-server/src/lint/__tests__/invalid-variable-name.test.ts` +- `packages/mcp-server/src/lint/__tests__/no-hyphenated-plugindata-key.test.ts` +- `packages/mcp-server/src/lint/__tests__/no-text-encoders.test.ts` +- `packages/mcp-server/src/lint/__tests__/prefer-helpers.test.ts` +- `packages/mcp-server/src/prelude/__tests__/wrap.test.ts` +- `packages/mcp-server/src/prelude/__tests__/helpers.test.ts` +- `packages/mcp-server/src/__tests__/execute-with-lint.test.ts` +- `packages/claude-plugin/__tests__/sync-recipes.test.ts` + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm test 2>&1 | grep -E "Test Files|Tests"` +Expected: all suites pass, total test count increased by ~30-40. + +- [ ] **Step 3: Confirm no untracked files** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && git status` +Expected: clean working tree. + +- [ ] **Step 4: Build the bridge plugin and confirm it loads** + +Run: `cd "/Users/dimi/Documents/TheVault/00 Joint Projects/PluginOS" && npm run build` +Expected: all packages build, no errors. + +- [ ] **Step 5: Document the end-to-end smoke test in a checklist for the PR description** + +The smoke test happens on the user's machine — write the checklist that goes into the PR body: + +```markdown +## End-to-end smoke test (manual) + +Run these against a real Figma file before merging: + +1. `npm install -g pluginos@` (or run `npx pluginos` from the local build). +2. Open the bridge plugin in Figma. Confirm "Connected". +3. From Claude, call `execute_figma` with: `return PluginOS.version;`. Expect the response payload to include `preludeVersion` matching the mcp-server package version, `lint: []`, and `result: ""`. +4. Call `execute_figma` with `figma.notify("hi"); return 1;`. Expect `lint` to contain a `no-notify` error, response still succeeds (warn-first policy). +5. Rerun the TYPO3 Bootstrap seed runbook using the helpers. Expected outcome: + - Zero unbound text nodes + - Zero unbound padding values + - Zero variant overlaps + - Zero SPACE_BETWEEN collapses +6. Confirm `list_operations` response includes `total: N` matching the registry. +``` + +Don't commit this checklist — it goes into the PR description. + +--- + +## Task 23: Open the 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-b-quality-helpers` +Expected: pre-push hooks pass, branch pushed. + +- [ ] **Step 2: Open the PR** + +Run: `gh pr create --base main --head feat/pr-b-quality-helpers --title "feat: PR-B quality helpers — prelude + lint + recipes"` with a body that includes: +- One-paragraph summary +- Bullet list of what's shipped (the 5 helpers, the 7 lint rules, the recipes, the op count fix) +- Reference to the spec PR (`#26`) and the design doc path +- The manual smoke test checklist from Task 22 Step 5 +- 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 (prelude): Tasks 11-16 ✓ + - §B (lint): Tasks 1-10 ✓ + - §C (response shape): Task 17 ✓ + - §D (skill recipes): Tasks 18-19 ✓ + - §E (op count): Tasks 20-21 ✓ + - Backwards compat (additive, warn-first): enforced in Task 17 (existing call sites untouched) and Task 10 (warn severities, no blocking) ✓ + - Testing strategy (unit per rule, integration, drift): Tasks 3-9, 17, 18 ✓ + - Non-goals (deferred): explicitly not in any task ✓ + +2. **Placeholder scan:** No TBDs, TODOs, or "implement later". Task 20 has a conditional flow but spells out the conditional explicitly. Task 19's budget check refers to "whichever existing budget check the CI uses" — this is a known unknown, with an exact discovery command provided. + +3. **Type consistency:** `LintResult` shape consistent across Tasks 1, 3-10, 17. `wrapScript` return type consistent across Tasks 11, 17. `PluginOS.*` method names consistent: `createStyledText`, `bindSpacing`, `combineAsVariantsTiled`, `tileTopLevel`, `layoutSpaceBetween` (matches spec exactly). + +4. **One known unknown:** Task 20 depends on the current shape of `__list_operations` response, which the plan asks the executor to discover first rather than guessing. This is intentional — the cost of getting it wrong is small (one extra commit), and prescribing a change without seeing the file would be brittle. diff --git a/docs/superpowers/specs/2026-06-03-pluginos-quality-helpers-design.md b/docs/superpowers/specs/2026-06-03-pluginos-quality-helpers-design.md new file mode 100644 index 0000000..01f0935 --- /dev/null +++ b/docs/superpowers/specs/2026-06-03-pluginos-quality-helpers-design.md @@ -0,0 +1,250 @@ +# PluginOS Quality Helpers (PR-B) — Design + +**Date:** 2026-06-03 +**Status:** Approved for implementation planning +**Author:** Brainstorm session with Claude +**Scope:** Single PR. Additive only — zero behavior change to existing operations or `execute_figma` calls that don't reference new helpers. + +## Context + +PluginOS shipped 39 operations through v0.4.3, but a real-world bulk-seed run (TYPO3 Bootstrap, 2026-06-03, ~450 nodes) surfaced quality gaps in `execute_figma`-driven scripts: + +- 306 text nodes left unbound to text styles (font properties set manually instead of via `setTextStyleIdAsync`) +- 113 padding/gap values left unbound to spacing variables (`setBoundVariable` boilerplate skipped) +- 49 top-level components stacked at (0,0) +- 58 frames with broken SPACE_BETWEEN layouts (collapse on selection inspect) +- Multiple rounds of debugging `figma.notify`, `itemSpacing = "AUTO"`, and invalid variable names — all sandbox-known errors with poor surfacing + +These are not Figma API bugs — they are patterns PluginOS can codify on behalf of the agent. + +The full feedback document lives at `/Users/dimi/Documents/TheVault/03 Vice Versa/TYPO3 Bootstrap/2026-06-03-pluginos-feedback.md` (15 numbered items + revised tl;dr). + +## Goals + +1. **Codify five high-frequency patterns** as `PluginOS.*` helpers usable inside `execute_figma` scripts. +2. **Catch a class of sandbox no-gos** before sending JS to Figma via a pre-flight linter. +3. **Surface the helpers in the skill** so the agent reaches for them by default. +4. **Stop drift on the "how many operations" number** by sourcing it from the registry. + +## Non-goals (deferred) + +- Connection reliability, port/process discovery, dark mode, UI state machine → **PR-A** +- Install and distribution polish → **PR-C** +- Runtime error translation (chasing Figma's error strings is a never-ending job) +- Standalone op (`run_operation`) versions of the helpers — the bugs happen inside loops in `execute_figma` scripts, which is where the helpers must live +- `wait_for_reconnect` MCP tool — moved to PR-A +- Bootstrap-5 token preset (feedback item #15) — niche, defer + +## Architecture + +``` +┌─────────────────┐ ┌──────────────────────┐ +│ agent (Claude) │ execute_figma │ mcp-server │ +│ ├─────────────────►│ │ +│ script body │ │ 1. lint(script) │ +└─────────────────┘ │ 2. wrap(script) │ + │ = prelude+script │ + │ 3. send to bridge │ + └──────────┬───────────┘ + │ WebSocket + ▼ + ┌──────────────────────┐ + │ bridge-plugin │ + │ execute wrapped JS │ + │ in figma sandbox │ + └──────────────────────┘ +``` + +**Source of truth for the prelude:** `mcp-server`. The bridge plugin remains unaware of `PluginOS.*` — it just executes whatever JS arrives. This keeps the helper surface versioned with the npm-published server. + +## Component-by-component design + +### A. Sandbox prelude (`packages/mcp-server/src/prelude/`) + +New module. Exports: + +```ts +export const PLUGINOS_PRELUDE: string; // the JS source blob +export const PRELUDE_VERSION: string; // pinned to package.json version +export function wrapScript(userJs: string): { + wrapped: string; // prelude + "\n" + userJs + preludeLineCount: number; // so the linter can offset line numbers +}; +``` + +The prelude defines `globalThis.PluginOS = { createStyledText, bindSpacing, combineAsVariantsTiled, tileTopLevel, layoutSpaceBetween }`. All five functions are pure JS — no imports — written directly into the source blob. + +Helper signatures (full TS types are documented in the prelude source via JSDoc): + +```ts +PluginOS.createStyledText(opts: { + characters: string; + textStyleId?: string; + family?: string; + weight?: string; + size?: number; + fillStyleId?: string; + name?: string; +}): Promise + +PluginOS.bindSpacing(node: FrameNode, vars: { + padding?: VariableAlias; + paddingX?: VariableAlias; + paddingY?: VariableAlias; + paddingTop?: VariableAlias; + paddingBottom?: VariableAlias; + paddingLeft?: VariableAlias; + paddingRight?: VariableAlias; + itemSpacing?: VariableAlias; +}): Promise + +PluginOS.combineAsVariantsTiled(cells: ComponentNode[], parent: BaseNode & ChildrenMixin, opts?: { + cols?: number; // default: Math.ceil(Math.sqrt(cells.length)) + gutter?: number; // default: 16 + width?: number; // default: computed + layoutMode?: "HORIZONTAL" | "VERTICAL"; // default: "HORIZONTAL" + wrap?: boolean; // default: true +}): ComponentSetNode + +PluginOS.tileTopLevel(page: PageNode, opts?: { + cols?: number; // default 4 + gutter?: number; // default 64 + origin?: { x: number; y: number }; +}): (node: SceneNode) => void + +PluginOS.layoutSpaceBetween(frame: FrameNode, opts: { + growChild?: SceneNode; + children?: SceneNode[]; // auto-picks middle (3+) or last (2) +}): void +``` + +**Behavior details:** + +- `createStyledText`: `loadFontAsync` → `createText` → if `textStyleId` then `setTextStyleIdAsync`, else apply `fontName`+`fontSize` → if `fillStyleId` then `setFillStyleIdAsync` → set `name`. Throws if neither `textStyleId` nor (`family`+`size`) is provided. +- `bindSpacing`: per provided key, calls `node.setBoundVariable(field, variable)`. Specificity wins — explicit `paddingTop` overrides `padding`. Warns (returns in lint output) if the node isn't an auto-layout frame, no-ops. +- `combineAsVariantsTiled`: `figma.combineAsVariants(cells, parent)` → set `layoutMode`, `layoutWrap = wrap ? "WRAP" : "NO_WRAP"`, `primaryAxisSizingMode = "FIXED"`, `counterAxisSizingMode = "AUTO"`, `itemSpacing = gutter`, `width`. Returns the set. +- `tileTopLevel`: returns a *placer closure* with the cursor state. Cursor lifetime is the single `execute_figma` call — not persisted to plugin data. +- `layoutSpaceBetween`: set `frame.primaryAxisAlignItems = "MIN"`, then on `growChild` (or auto-picked child): if `TEXT`, `layoutSizingHorizontal = "FILL"`; else `layoutGrow = 1`. + +**Error convention:** all helpers throw `Error` with prefix `[PluginOS.]` so call sites are obvious in response payloads. + +### B. Pre-flight linter (`packages/mcp-server/src/lint/`) + +New module. Regex+heuristic — no AST parser. Rules registered via an index. + +**Rule set v1:** + +| ID | Severity | Trigger | +|---|---|---| +| `no-notify` | error | `figma.notify(...)` calls (sandbox-forbidden) | +| `no-sync-style-setters` | warn | `.{fill,text,stroke,effect,grid}StyleId = ...` (deprecated) | +| `no-itemspacing-auto` | error | `itemSpacing = "AUTO"` string literal (runtime-rejected) | +| `invalid-variable-name` | error | `createVariable("...", ...)` first arg containing non-`[A-Za-z0-9_]` | +| `no-hyphenated-plugindata-key` | error | `setPluginData(...)` first arg containing `-` | +| `no-text-encoders` | error | `TextEncoder` / `TextDecoder` / `crypto.subtle` references | +| `prefer-helpers` | hint | `createText` + `loadFontAsync` proximity (suggest `createStyledText`); 3+ padding bindings (suggest `bindSpacing`) | + +**Result shape:** + +```ts +type LintResult = { + ruleId: string; + severity: "error" | "warn" | "hint"; + line: number; // 1-based, in user's submitted script (prelude excluded) + message: string; + fix?: string; +}; +``` + +**Policy v1: warn first, do not block.** Lint results are returned alongside the execution result in the response payload. Tightening to "block on error" is a follow-up after we've observed real false-positive rates. No `skipLint` escape hatch in v1. + +**Line numbers:** lint runs against the user's original script. The `preludeLineCount` from `wrapScript` is used to offset any positions reported back from the sandbox; lint itself sees no prelude. + +### C. `execute_figma` response shape (extended) + +```ts +{ + result: unknown; // unchanged + lint: LintResult[]; // new; may be empty + preludeVersion: string; // new; pinned to mcp-server package version + durationMs: number; // unchanged +} +``` + +`preludeVersion` lets the agent detect when its mental model of `PluginOS.*` is stale. + +### D. Skill recipes (`packages/claude-plugin/skills/pluginos-figma/SKILL.md`) + +Append a new section: `## Recipes for bulk-seed scripts`. Five short recipes, one per helper, format: + +``` +### Styled text nodes +Don't: createText + set fontName + set fontSize manually — leaves text unbound to styles. +Do: PluginOS.createStyledText({ characters, textStyleId, fillStyleId, name }) +Why: load-font + create + bind-style + set-fill in one async call. +``` + +Plus a 3-line preamble: *"These helpers are available inside every `execute_figma` script. They prevent the most common bulk-seed bugs."* + +**Budget:** stay under 1000 tokens total (current 72-line skill is well under the 1150 CI cap; recipes add ~150-200 tokens). + +**Sync:** recipes are generated from the prelude's helper metadata via `npm run sync-recipes -w packages/claude-plugin`. CI check identical to existing ops-reference drift check. + +### E. Op count truth (`list_operations`) + +- `list_operations` tool response includes `total: ` from the registry at startup. +- `README.md` and `INSTALL.md` reference no specific number — they say "see `list_operations` for the current set." +- `INSTALL.md`'s verification step changes from "You should get a list of 39 operations" to "You should get a list of operations." + +## Backwards compatibility + +- All existing 39 operations: untouched. +- `execute_figma` scripts that don't reference `PluginOS.*`: behavior identical (prelude is injected but never executed beyond its top-level definitions, which are pure assignments). +- Response shape: existing fields preserved; new fields are additive — existing agents and tests that read `result` and `durationMs` continue to work. +- Linter policy "warn first": no script that runs today will be blocked tomorrow. + +## Testing strategy + +**Unit (Vitest):** + +- `mcp-server/src/lint/__tests__/.test.ts` — one file per rule, positive and negative cases each. +- `mcp-server/src/prelude/__tests__/wrap.test.ts` — wrapping correctness; line-number offset preserved. + +**Integration:** + +- `bridge-plugin/src/__tests__/prelude-integration.test.ts` — execute a fixture script per helper against happy-dom mock of the Figma API; assert side-effects and return values. +- `mcp-server/src/__tests__/execute-with-lint.test.ts` — full `execute_figma` flow: script with lint trigger → response contains lint output and execution result. + +**Drift / CI:** + +- `claude-plugin/scripts/sync-recipes.test.ts` — recipes in `SKILL.md` match the prelude's helper metadata. +- Existing skill token budget check (1150) continues to enforce ceiling. + +**End-to-end (manual):** + +- Rerun the TYPO3 Bootstrap seed runbook using the new helpers. +- Acceptance: zero unbound text nodes, zero unbound paddings, zero variant overlaps, zero SPACE_BETWEEN collapses. +- Capture before/after metrics in the PR description. + +## Open questions deferred to implementation + +1. Should the prelude expose `PluginOS.version` at runtime for in-script introspection? (Probably yes — small.) +2. Should `prefer-helpers` hints include the exact suggested code as `fix`? (Yes if we can generate it cleanly; otherwise just a textual nudge.) +3. Vitest mock for `figma.combineAsVariants` — does the happy-dom fixture already cover this, or do we need a stub? (Investigate during implementation.) + +## Sequencing within the PR + +1. Prelude module + `wrapScript` + tests (foundation) +2. Each helper + its integration test (parallel, one commit each) +3. Linter module + rule set + tests +4. `execute_figma` handler wiring (lint → wrap → send → augmented response) +5. Skill recipes + sync script + CI hook +6. Op count: registry total + tool response + docs cleanup +7. End-to-end rerun of TYPO3 Bootstrap seed; capture metrics + +## References + +- Feedback document: `/Users/dimi/Documents/TheVault/03 Vice Versa/TYPO3 Bootstrap/2026-06-03-pluginos-feedback.md` +- Existing operations registry: `packages/bridge-plugin/src/operations/registry.ts` +- Existing skill: `packages/claude-plugin/skills/pluginos-figma/SKILL.md` +- Existing `execute_figma` handler: `packages/bridge-plugin/src/handlers/execute.ts` (called by mcp-server's tool) 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" } }