From 2b9a28a031158085bb4ade6cbb80c4fba6c07ecd Mon Sep 17 00:00:00 2001 From: Prateek Date: Fri, 13 Feb 2026 17:47:28 +0530 Subject: [PATCH] test: add comprehensive unit tests for all core services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add vitest test suite with 103 tests across 5 test files: - metadata.test.ts (19 tests): read/write/update/delete/list, key=value parsing, comments, values with equals signs, archiving, dotfile exclusion - event-bus.test.ts (22 tests): emit/on/off, wildcard listeners, handler error resilience, priority inference, JSONL persistence round-trip, filtered history with sessionId/projectId/type/priority/since/limit - tmux.test.ts (23 tests): all tmux wrappers with mocked child_process, session listing/parsing, send-keys short vs load-buffer long text, capture-pane, kill, getPaneTTY - session-manager.test.ts (23 tests): spawn pipeline with mocked plugins, session numbering, branch derivation, event emission, metadata writing, list with dead runtime detection, kill with cleanup, send via runtime - lifecycle-manager.test.ts (16 tests): state transitions (spawning→working, killed, stuck, needs_input, ci_failed, merged, mergeable), reaction triggering, auto=false suppression, on/off handlers, getStates isolation Also fixes inferPriority to handle merge.completed as "action" priority. Co-Authored-By: Claude Opus 4.6 --- packages/core/package.json | 5 +- packages/core/src/__tests__/event-bus.test.ts | 288 ++++ .../src/__tests__/lifecycle-manager.test.ts | 702 +++++++++ packages/core/src/__tests__/metadata.test.ts | 255 ++++ .../src/__tests__/session-manager.test.ts | 521 +++++++ packages/core/src/__tests__/tmux.test.ts | 304 ++++ packages/core/src/event-bus.ts | 2 +- pnpm-lock.yaml | 1262 +++++++---------- 8 files changed, 2585 insertions(+), 754 deletions(-) create mode 100644 packages/core/src/__tests__/event-bus.test.ts create mode 100644 packages/core/src/__tests__/lifecycle-manager.test.ts create mode 100644 packages/core/src/__tests__/metadata.test.ts create mode 100644 packages/core/src/__tests__/session-manager.test.ts create mode 100644 packages/core/src/__tests__/tmux.test.ts diff --git a/packages/core/package.json b/packages/core/package.json index ab13f7221..2c686e7ae 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,6 +18,8 @@ "scripts": { "build": "tsc", "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", "clean": "rm -rf dist" }, "dependencies": { @@ -26,6 +28,7 @@ }, "devDependencies": { "@types/node": "^25.2.3", - "typescript": "^5.7.0" + "typescript": "^5.7.0", + "vitest": "^4.0.18" } } diff --git a/packages/core/src/__tests__/event-bus.test.ts b/packages/core/src/__tests__/event-bus.test.ts new file mode 100644 index 000000000..65da37d5c --- /dev/null +++ b/packages/core/src/__tests__/event-bus.test.ts @@ -0,0 +1,288 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { randomUUID } from "node:crypto"; +import { createEventBus, createEvent } from "../event-bus.js"; +import type { OrchestratorEvent, EventType } from "../types.js"; + +let tmpDir: string; + +beforeEach(() => { + tmpDir = join(tmpdir(), `ao-test-eventbus-${randomUUID()}`); + mkdirSync(tmpDir, { recursive: true }); +}); + +afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); +}); + +describe("createEvent", () => { + it("creates event with all fields", () => { + const event = createEvent("session.spawned", { + sessionId: "app-1", + projectId: "my-app", + message: "Session spawned", + data: { branch: "main" }, + }); + + expect(event.id).toBeTruthy(); + expect(event.type).toBe("session.spawned"); + expect(event.sessionId).toBe("app-1"); + expect(event.projectId).toBe("my-app"); + expect(event.message).toBe("Session spawned"); + expect(event.data).toEqual({ branch: "main" }); + expect(event.timestamp).toBeInstanceOf(Date); + }); + + it("infers priority from event type", () => { + expect(createEvent("session.stuck", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("urgent"); + expect(createEvent("session.needs_input", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("urgent"); + expect(createEvent("session.errored", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("urgent"); + expect(createEvent("review.approved", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("action"); + expect(createEvent("merge.ready", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("action"); + expect(createEvent("merge.completed", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("action"); + expect(createEvent("ci.failing", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("warning"); + expect(createEvent("review.changes_requested", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("warning"); + expect(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("info"); + expect(createEvent("pr.created", { sessionId: "a", projectId: "p", message: "" }).priority).toBe("info"); + }); + + it("allows explicit priority override", () => { + const event = createEvent("session.spawned", { + sessionId: "a", + projectId: "p", + message: "", + priority: "urgent", + }); + expect(event.priority).toBe("urgent"); + }); + + it("generates unique IDs", () => { + const e1 = createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" }); + const e2 = createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" }); + expect(e1.id).not.toBe(e2.id); + }); +}); + +describe("createEventBus (no persistence)", () => { + it("emits events to typed handlers", () => { + const bus = createEventBus(null); + const received: OrchestratorEvent[] = []; + + bus.on("session.spawned", (e) => received.push(e)); + + const event = createEvent("session.spawned", { + sessionId: "app-1", + projectId: "p", + message: "spawned", + }); + bus.emit(event); + + expect(received).toHaveLength(1); + expect(received[0].sessionId).toBe("app-1"); + }); + + it("emits events to wildcard handlers", () => { + const bus = createEventBus(null); + const received: OrchestratorEvent[] = []; + + bus.on("*", (e) => received.push(e)); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + bus.emit(createEvent("ci.failing", { sessionId: "b", projectId: "p", message: "" })); + + expect(received).toHaveLength(2); + }); + + it("does not deliver events to wrong type", () => { + const bus = createEventBus(null); + const received: OrchestratorEvent[] = []; + + bus.on("ci.failing", (e) => received.push(e)); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + + expect(received).toHaveLength(0); + }); + + it("unsubscribes handlers with off()", () => { + const bus = createEventBus(null); + const received: OrchestratorEvent[] = []; + const handler = (e: OrchestratorEvent) => received.push(e); + + bus.on("session.spawned", handler); + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + expect(received).toHaveLength(1); + + bus.off("session.spawned", handler); + bus.emit(createEvent("session.spawned", { sessionId: "b", projectId: "p", message: "" })); + expect(received).toHaveLength(1); // still 1, not 2 + }); + + it("survives handler errors without breaking", () => { + const bus = createEventBus(null); + const received: OrchestratorEvent[] = []; + + bus.on("session.spawned", () => { throw new Error("boom"); }); + bus.on("session.spawned", (e) => received.push(e)); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + + expect(received).toHaveLength(1); + }); + + it("supports multiple handlers on same event", () => { + const bus = createEventBus(null); + let count = 0; + + bus.on("session.spawned", () => count++); + bus.on("session.spawned", () => count++); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + expect(count).toBe(2); + }); +}); + +describe("getHistory", () => { + it("returns all events when no filter", () => { + const bus = createEventBus(null); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p1", message: "" })); + bus.emit(createEvent("ci.failing", { sessionId: "b", projectId: "p2", message: "" })); + + const history = bus.getHistory(); + expect(history).toHaveLength(2); + }); + + it("filters by sessionId", () => { + const bus = createEventBus(null); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + bus.emit(createEvent("session.spawned", { sessionId: "b", projectId: "p", message: "" })); + + expect(bus.getHistory({ sessionId: "a" })).toHaveLength(1); + }); + + it("filters by projectId", () => { + const bus = createEventBus(null); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p1", message: "" })); + bus.emit(createEvent("session.spawned", { sessionId: "b", projectId: "p2", message: "" })); + + expect(bus.getHistory({ projectId: "p1" })).toHaveLength(1); + }); + + it("filters by type", () => { + const bus = createEventBus(null); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + bus.emit(createEvent("ci.failing", { sessionId: "b", projectId: "p", message: "" })); + + expect(bus.getHistory({ type: "ci.failing" })).toHaveLength(1); + }); + + it("filters by priority", () => { + const bus = createEventBus(null); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + bus.emit(createEvent("session.stuck", { sessionId: "b", projectId: "p", message: "" })); + + expect(bus.getHistory({ priority: "urgent" })).toHaveLength(1); + }); + + it("filters by since", () => { + const bus = createEventBus(null); + const beforeTime = new Date(Date.now() - 1000); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + + const afterTime = new Date(Date.now() + 1000); + expect(bus.getHistory({ since: beforeTime })).toHaveLength(1); + expect(bus.getHistory({ since: afterTime })).toHaveLength(0); + }); + + it("applies limit (takes last N)", () => { + const bus = createEventBus(null); + + for (let i = 0; i < 10; i++) { + bus.emit(createEvent("session.spawned", { sessionId: `s-${i}`, projectId: "p", message: `msg-${i}` })); + } + + const result = bus.getHistory({ limit: 3 }); + expect(result).toHaveLength(3); + expect(result[0].sessionId).toBe("s-7"); + expect(result[2].sessionId).toBe("s-9"); + }); + + it("combines multiple filters", () => { + const bus = createEventBus(null); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p1", message: "" })); + bus.emit(createEvent("session.spawned", { sessionId: "b", projectId: "p1", message: "" })); + bus.emit(createEvent("ci.failing", { sessionId: "a", projectId: "p1", message: "" })); + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p2", message: "" })); + + expect(bus.getHistory({ sessionId: "a", projectId: "p1" })).toHaveLength(2); + expect(bus.getHistory({ sessionId: "a", type: "session.spawned" })).toHaveLength(2); + expect(bus.getHistory({ sessionId: "a", projectId: "p1", type: "session.spawned" })).toHaveLength(1); + }); +}); + +describe("JSONL persistence", () => { + it("persists events to JSONL file", () => { + const logPath = join(tmpDir, "events.jsonl"); + const bus = createEventBus(logPath); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "hello" })); + bus.emit(createEvent("ci.failing", { sessionId: "b", projectId: "p", message: "oops" })); + + const content = readFileSync(logPath, "utf-8"); + const lines = content.trim().split("\n"); + expect(lines).toHaveLength(2); + + const parsed = JSON.parse(lines[0]); + expect(parsed.type).toBe("session.spawned"); + expect(parsed.sessionId).toBe("a"); + expect(parsed.timestamp).toBeTruthy(); + }); + + it("loads history from existing JSONL file on creation", () => { + const logPath = join(tmpDir, "existing.jsonl"); + const event = createEvent("session.spawned", { sessionId: "old", projectId: "p", message: "old event" }); + const serialized = JSON.stringify({ ...event, timestamp: event.timestamp.toISOString() }); + require("node:fs").writeFileSync(logPath, serialized + "\n", "utf-8"); + + const bus = createEventBus(logPath); + const history = bus.getHistory(); + expect(history).toHaveLength(1); + expect(history[0].sessionId).toBe("old"); + expect(history[0].timestamp).toBeInstanceOf(Date); + }); + + it("creates log directory if it does not exist", () => { + const logPath = join(tmpDir, "subdir", "nested", "events.jsonl"); + const bus = createEventBus(logPath); + + bus.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + + expect(existsSync(logPath)).toBe(true); + }); + + it("appends to existing log file", () => { + const logPath = join(tmpDir, "append.jsonl"); + + const bus1 = createEventBus(logPath); + bus1.emit(createEvent("session.spawned", { sessionId: "a", projectId: "p", message: "" })); + + const bus2 = createEventBus(logPath); + bus2.emit(createEvent("ci.failing", { sessionId: "b", projectId: "p", message: "" })); + + const content = readFileSync(logPath, "utf-8"); + const lines = content.trim().split("\n"); + expect(lines).toHaveLength(2); + + // bus2 should have loaded the first event + emitted the second + expect(bus2.getHistory()).toHaveLength(2); + }); +}); diff --git a/packages/core/src/__tests__/lifecycle-manager.test.ts b/packages/core/src/__tests__/lifecycle-manager.test.ts new file mode 100644 index 000000000..e2538d667 --- /dev/null +++ b/packages/core/src/__tests__/lifecycle-manager.test.ts @@ -0,0 +1,702 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { mkdirSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { randomUUID } from "node:crypto"; +import { createLifecycleManager } from "../lifecycle-manager.js"; +import { createEventBus } from "../event-bus.js"; +import { writeMetadata, readMetadataRaw } from "../metadata.js"; +import type { + OrchestratorConfig, + PluginRegistry, + SessionManager, + Session, + Runtime, + Agent, + SCM, + Notifier, + EventBus, + OrchestratorEvent, + ActivityState, + PRInfo, +} from "../types.js"; + +let dataDir: string; +let eventBus: EventBus; +let mockSessionManager: SessionManager; +let mockRuntime: Runtime; +let mockAgent: Agent; +let mockRegistry: PluginRegistry; +let config: OrchestratorConfig; + +function makeSession(overrides: Partial = {}): Session { + return { + id: "app-1", + projectId: "my-app", + status: "spawning", + activity: "active", + branch: "feat/test", + issueId: null, + pr: null, + workspacePath: "/tmp/ws", + runtimeHandle: { id: "rt-1", runtimeName: "mock", data: {} }, + agentInfo: null, + createdAt: new Date(), + lastActivityAt: new Date(), + metadata: {}, + ...overrides, + }; +} + +function makePR(overrides: Partial = {}): PRInfo { + return { + number: 42, + url: "https://github.com/org/repo/pull/42", + title: "Fix things", + owner: "org", + repo: "repo", + branch: "feat/test", + baseBranch: "main", + isDraft: false, + ...overrides, + }; +} + +beforeEach(() => { + dataDir = join(tmpdir(), `ao-test-lifecycle-${randomUUID()}`); + mkdirSync(join(dataDir, "sessions"), { recursive: true }); + + eventBus = createEventBus(null); + + mockRuntime = { + name: "mock", + create: vi.fn(), + destroy: vi.fn(), + sendMessage: vi.fn().mockResolvedValue(undefined), + getOutput: vi.fn(), + isAlive: vi.fn().mockResolvedValue(true), + }; + + mockAgent = { + name: "mock-agent", + processName: "mock", + getLaunchCommand: vi.fn(), + getEnvironment: vi.fn(), + detectActivity: vi.fn().mockResolvedValue("active" as ActivityState), + isProcessRunning: vi.fn(), + introspect: vi.fn(), + }; + + mockRegistry = { + register: vi.fn(), + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") return mockAgent; + return null; + }), + list: vi.fn().mockReturnValue([]), + loadBuiltins: vi.fn(), + loadFromConfig: vi.fn(), + }; + + mockSessionManager = { + spawn: vi.fn(), + list: vi.fn().mockResolvedValue([]), + get: vi.fn().mockResolvedValue(null), + kill: vi.fn().mockResolvedValue(undefined), + cleanup: vi.fn(), + send: vi.fn().mockResolvedValue(undefined), + }; + + config = { + dataDir, + worktreeDir: "/tmp/worktrees", + port: 3000, + defaults: { + runtime: "mock", + agent: "mock-agent", + workspace: "mock-ws", + notifiers: ["desktop"], + }, + projects: { + "my-app": { + name: "My App", + repo: "org/my-app", + path: "/tmp/my-app", + defaultBranch: "main", + sessionPrefix: "app", + scm: { plugin: "github" }, + }, + }, + notifiers: {}, + notificationRouting: { + urgent: ["desktop"], + action: ["desktop"], + warning: [], + info: [], + }, + reactions: {}, + }; +}); + +afterEach(() => { + rmSync(dataDir, { recursive: true, force: true }); +}); + +describe("start / stop", () => { + it("starts and stops the polling loop", () => { + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + lm.start(60_000); + // Should not throw on double start + lm.start(60_000); + lm.stop(); + // Should not throw on double stop + lm.stop(); + }); +}); + +describe("check (single session)", () => { + it("detects transition from spawning to working", async () => { + const session = makeSession({ status: "spawning" }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + // Write metadata so updateMetadata works + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "spawning", + project: "my-app", + }); + + const received: OrchestratorEvent[] = []; + eventBus.on("session.working", (e) => received.push(e)); + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + expect(lm.getStates().get("app-1")).toBe("working"); + expect(received).toHaveLength(1); + expect(received[0].type).toBe("session.working"); + + // Metadata should be updated + const meta = readMetadataRaw(dataDir, "app-1"); + expect(meta!["status"]).toBe("working"); + }); + + it("detects killed state when runtime is dead", async () => { + vi.mocked(mockRuntime.isAlive).mockResolvedValue(false); + + const session = makeSession({ status: "working" }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + }); + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + expect(lm.getStates().get("app-1")).toBe("killed"); + }); + + it("detects stuck state from agent", async () => { + vi.mocked(mockAgent.detectActivity).mockResolvedValue("blocked"); + + const session = makeSession({ status: "working" }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + }); + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + expect(lm.getStates().get("app-1")).toBe("stuck"); + }); + + it("detects needs_input from agent", async () => { + vi.mocked(mockAgent.detectActivity).mockResolvedValue("waiting_input"); + + const session = makeSession({ status: "working" }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + }); + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + expect(lm.getStates().get("app-1")).toBe("needs_input"); + }); + + it("detects PR states from SCM", async () => { + const mockSCM: SCM = { + name: "mock-scm", + detectPR: vi.fn(), + getPRState: vi.fn().mockResolvedValue("open"), + mergePR: vi.fn(), + closePR: vi.fn(), + getCIChecks: vi.fn(), + getCISummary: vi.fn().mockResolvedValue("failing"), + getReviews: vi.fn(), + getReviewDecision: vi.fn().mockResolvedValue("none"), + getPendingComments: vi.fn(), + getAutomatedComments: vi.fn(), + getMergeability: vi.fn(), + }; + + const registryWithSCM: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") return mockAgent; + if (slot === "scm") return mockSCM; + return null; + }), + }; + + const session = makeSession({ status: "pr_open", pr: makePR() }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "pr_open", + project: "my-app", + }); + + const lm = createLifecycleManager({ + config, + registry: registryWithSCM, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + expect(lm.getStates().get("app-1")).toBe("ci_failed"); + }); + + it("detects merged PR", async () => { + const mockSCM: SCM = { + name: "mock-scm", + detectPR: vi.fn(), + getPRState: vi.fn().mockResolvedValue("merged"), + mergePR: vi.fn(), + closePR: vi.fn(), + getCIChecks: vi.fn(), + getCISummary: vi.fn(), + getReviews: vi.fn(), + getReviewDecision: vi.fn(), + getPendingComments: vi.fn(), + getAutomatedComments: vi.fn(), + getMergeability: vi.fn(), + }; + + const registryWithSCM: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") return mockAgent; + if (slot === "scm") return mockSCM; + return null; + }), + }; + + const session = makeSession({ status: "approved", pr: makePR() }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "approved", + project: "my-app", + }); + + const lm = createLifecycleManager({ + config, + registry: registryWithSCM, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + expect(lm.getStates().get("app-1")).toBe("merged"); + }); + + it("detects mergeable when approved + CI green", async () => { + const mockSCM: SCM = { + name: "mock-scm", + detectPR: vi.fn(), + getPRState: vi.fn().mockResolvedValue("open"), + mergePR: vi.fn(), + closePR: vi.fn(), + getCIChecks: vi.fn(), + getCISummary: vi.fn().mockResolvedValue("passing"), + getReviews: vi.fn(), + getReviewDecision: vi.fn().mockResolvedValue("approved"), + getPendingComments: vi.fn(), + getAutomatedComments: vi.fn(), + getMergeability: vi.fn().mockResolvedValue({ + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }), + }; + + const registryWithSCM: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") return mockAgent; + if (slot === "scm") return mockSCM; + return null; + }), + }; + + const session = makeSession({ status: "pr_open", pr: makePR() }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "pr_open", + project: "my-app", + }); + + const lm = createLifecycleManager({ + config, + registry: registryWithSCM, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + expect(lm.getStates().get("app-1")).toBe("mergeable"); + }); + + it("throws for nonexistent session", async () => { + vi.mocked(mockSessionManager.get).mockResolvedValue(null); + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + await expect(lm.check("nonexistent")).rejects.toThrow("not found"); + }); + + it("does not emit event when status unchanged", async () => { + const session = makeSession({ status: "working" }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + }); + + const received: OrchestratorEvent[] = []; + eventBus.on("*", (e) => received.push(e)); + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + // First check: spawning (from states map, empty) → working = transition + // But the session's status is "working" and determineStatus returns "working" + // Since states map is empty, oldStatus = session.status = "working", newStatus = "working" + // So no transition + // We need to seed the states map first + // Actually, let's check twice: first time sets it, second time same = no event + await lm.check("app-1"); + const eventsAfterFirst = [...received]; + + await lm.check("app-1"); + // No new events emitted since status didn't change + expect(received.length).toBe(eventsAfterFirst.length); + }); +}); + +describe("reactions", () => { + it("triggers send-to-agent reaction on CI failure", async () => { + config.reactions = { + "ci-failed": { + auto: true, + action: "send-to-agent", + message: "CI is failing. Fix it.", + retries: 2, + escalateAfter: 2, + }, + }; + + const mockSCM: SCM = { + name: "mock-scm", + detectPR: vi.fn(), + getPRState: vi.fn().mockResolvedValue("open"), + mergePR: vi.fn(), + closePR: vi.fn(), + getCIChecks: vi.fn(), + getCISummary: vi.fn().mockResolvedValue("failing"), + getReviews: vi.fn(), + getReviewDecision: vi.fn().mockResolvedValue("none"), + getPendingComments: vi.fn(), + getAutomatedComments: vi.fn(), + getMergeability: vi.fn(), + }; + + const registryWithSCM: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") return mockAgent; + if (slot === "scm") return mockSCM; + return null; + }), + }; + + const session = makeSession({ status: "pr_open", pr: makePR() }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "pr_open", + project: "my-app", + }); + + const lm = createLifecycleManager({ + config, + registry: registryWithSCM, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + expect(mockSessionManager.send).toHaveBeenCalledWith("app-1", "CI is failing. Fix it."); + }); + + it("does not trigger reaction when auto=false", async () => { + config.reactions = { + "ci-failed": { + auto: false, + action: "send-to-agent", + message: "CI is failing.", + }, + }; + + const mockSCM: SCM = { + name: "mock-scm", + detectPR: vi.fn(), + getPRState: vi.fn().mockResolvedValue("open"), + mergePR: vi.fn(), + closePR: vi.fn(), + getCIChecks: vi.fn(), + getCISummary: vi.fn().mockResolvedValue("failing"), + getReviews: vi.fn(), + getReviewDecision: vi.fn().mockResolvedValue("none"), + getPendingComments: vi.fn(), + getAutomatedComments: vi.fn(), + getMergeability: vi.fn(), + }; + + const registryWithSCM: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") return mockAgent; + if (slot === "scm") return mockSCM; + return null; + }), + }; + + const session = makeSession({ status: "pr_open", pr: makePR() }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "pr_open", + project: "my-app", + }); + + const lm = createLifecycleManager({ + config, + registry: registryWithSCM, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + expect(mockSessionManager.send).not.toHaveBeenCalled(); + }); +}); + +describe("on / off event handlers", () => { + it("subscribes and receives lifecycle events", async () => { + const session = makeSession({ status: "spawning" }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "spawning", + project: "my-app", + }); + + const received: OrchestratorEvent[] = []; + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + lm.on("session.working", (e) => received.push(e)); + + await lm.check("app-1"); + + expect(received).toHaveLength(1); + expect(received[0].type).toBe("session.working"); + }); + + it("unsubscribes handlers", async () => { + const session = makeSession({ status: "spawning" }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "spawning", + project: "my-app", + }); + + const received: OrchestratorEvent[] = []; + const handler = (e: OrchestratorEvent) => received.push(e); + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + lm.on("session.working", handler); + lm.off("session.working", handler); + + await lm.check("app-1"); + + expect(received).toHaveLength(0); + }); + + it("wildcard handler receives all events", async () => { + const session = makeSession({ status: "spawning" }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "spawning", + project: "my-app", + }); + + const received: OrchestratorEvent[] = []; + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + lm.on("*", (e) => received.push(e)); + + await lm.check("app-1"); + + expect(received.length).toBeGreaterThan(0); + }); +}); + +describe("getStates", () => { + it("returns copy of states map", async () => { + const session = makeSession({ status: "spawning" }); + vi.mocked(mockSessionManager.get).mockResolvedValue(session); + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "spawning", + project: "my-app", + }); + + const lm = createLifecycleManager({ + config, + registry: mockRegistry, + sessionManager: mockSessionManager, + eventBus, + }); + + await lm.check("app-1"); + + const states = lm.getStates(); + expect(states.get("app-1")).toBe("working"); + + // Modifying returned map shouldn't affect internal state + states.set("app-1", "killed"); + expect(lm.getStates().get("app-1")).toBe("working"); + }); +}); diff --git a/packages/core/src/__tests__/metadata.test.ts b/packages/core/src/__tests__/metadata.test.ts new file mode 100644 index 000000000..bb1df87b9 --- /dev/null +++ b/packages/core/src/__tests__/metadata.test.ts @@ -0,0 +1,255 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { randomUUID } from "node:crypto"; +import { + readMetadata, + readMetadataRaw, + writeMetadata, + updateMetadata, + deleteMetadata, + listMetadata, +} from "../metadata.js"; + +let dataDir: string; + +beforeEach(() => { + dataDir = join(tmpdir(), `ao-test-metadata-${randomUUID()}`); + mkdirSync(join(dataDir, "sessions"), { recursive: true }); +}); + +afterEach(() => { + rmSync(dataDir, { recursive: true, force: true }); +}); + +describe("writeMetadata + readMetadata", () => { + it("writes and reads basic metadata", () => { + writeMetadata(dataDir, "app-1", { + worktree: "/tmp/worktree", + branch: "feat/test", + status: "working", + }); + + const meta = readMetadata(dataDir, "app-1"); + expect(meta).not.toBeNull(); + expect(meta!.worktree).toBe("/tmp/worktree"); + expect(meta!.branch).toBe("feat/test"); + expect(meta!.status).toBe("working"); + }); + + it("writes and reads optional fields", () => { + writeMetadata(dataDir, "app-2", { + worktree: "/tmp/w", + branch: "main", + status: "pr_open", + issue: "https://linear.app/team/issue/INT-100", + pr: "https://github.com/org/repo/pull/42", + summary: "Implementing feature X", + project: "my-app", + createdAt: "2025-01-01T00:00:00.000Z", + runtimeHandle: '{"id":"tmux-1","runtimeName":"tmux"}', + }); + + const meta = readMetadata(dataDir, "app-2"); + expect(meta).not.toBeNull(); + expect(meta!.issue).toBe("https://linear.app/team/issue/INT-100"); + expect(meta!.pr).toBe("https://github.com/org/repo/pull/42"); + expect(meta!.summary).toBe("Implementing feature X"); + expect(meta!.project).toBe("my-app"); + expect(meta!.createdAt).toBe("2025-01-01T00:00:00.000Z"); + expect(meta!.runtimeHandle).toBe('{"id":"tmux-1","runtimeName":"tmux"}'); + }); + + it("returns null for nonexistent session", () => { + const meta = readMetadata(dataDir, "nonexistent"); + expect(meta).toBeNull(); + }); + + it("produces key=value format matching bash scripts", () => { + writeMetadata(dataDir, "app-3", { + worktree: "/tmp/w", + branch: "feat/INT-123", + status: "working", + issue: "https://linear.app/team/issue/INT-123", + }); + + const content = readFileSync(join(dataDir, "sessions", "app-3"), "utf-8"); + expect(content).toContain("worktree=/tmp/w\n"); + expect(content).toContain("branch=feat/INT-123\n"); + expect(content).toContain("status=working\n"); + expect(content).toContain("issue=https://linear.app/team/issue/INT-123\n"); + }); + + it("omits optional fields that are undefined", () => { + writeMetadata(dataDir, "app-4", { + worktree: "/tmp/w", + branch: "main", + status: "spawning", + }); + + const content = readFileSync(join(dataDir, "sessions", "app-4"), "utf-8"); + expect(content).not.toContain("issue="); + expect(content).not.toContain("pr="); + expect(content).not.toContain("summary="); + }); +}); + +describe("readMetadataRaw", () => { + it("reads arbitrary key=value pairs", () => { + writeFileSync( + join(dataDir, "sessions", "raw-1"), + "worktree=/tmp/w\nbranch=main\ncustom_key=custom_value\n", + "utf-8" + ); + + const raw = readMetadataRaw(dataDir, "raw-1"); + expect(raw).not.toBeNull(); + expect(raw!["worktree"]).toBe("/tmp/w"); + expect(raw!["custom_key"]).toBe("custom_value"); + }); + + it("returns null for nonexistent session", () => { + expect(readMetadataRaw(dataDir, "nope")).toBeNull(); + }); + + it("handles comments and empty lines", () => { + writeFileSync( + join(dataDir, "sessions", "raw-2"), + "# This is a comment\n\nkey1=value1\n\n# Another comment\nkey2=value2\n", + "utf-8" + ); + + const raw = readMetadataRaw(dataDir, "raw-2"); + expect(raw).toEqual({ key1: "value1", key2: "value2" }); + }); + + it("handles values containing equals signs", () => { + writeFileSync( + join(dataDir, "sessions", "raw-3"), + 'runtimeHandle={"id":"foo","data":{"key":"val"}}\n', + "utf-8" + ); + + const raw = readMetadataRaw(dataDir, "raw-3"); + expect(raw!["runtimeHandle"]).toBe('{"id":"foo","data":{"key":"val"}}'); + }); +}); + +describe("updateMetadata", () => { + it("updates specific fields while preserving others", () => { + writeMetadata(dataDir, "upd-1", { + worktree: "/tmp/w", + branch: "main", + status: "spawning", + }); + + updateMetadata(dataDir, "upd-1", { status: "working", pr: "https://github.com/org/repo/pull/1" }); + + const meta = readMetadata(dataDir, "upd-1"); + expect(meta!.status).toBe("working"); + expect(meta!.pr).toBe("https://github.com/org/repo/pull/1"); + expect(meta!.worktree).toBe("/tmp/w"); + expect(meta!.branch).toBe("main"); + }); + + it("deletes keys set to empty string", () => { + writeMetadata(dataDir, "upd-2", { + worktree: "/tmp/w", + branch: "main", + status: "working", + summary: "doing stuff", + }); + + updateMetadata(dataDir, "upd-2", { summary: "" }); + + const raw = readMetadataRaw(dataDir, "upd-2"); + expect(raw!["summary"]).toBeUndefined(); + expect(raw!["status"]).toBe("working"); + }); + + it("creates file if it does not exist", () => { + updateMetadata(dataDir, "upd-3", { status: "new", branch: "test" }); + + const raw = readMetadataRaw(dataDir, "upd-3"); + expect(raw).toEqual({ status: "new", branch: "test" }); + }); + + it("ignores undefined values", () => { + writeMetadata(dataDir, "upd-4", { + worktree: "/tmp/w", + branch: "main", + status: "working", + }); + + updateMetadata(dataDir, "upd-4", { status: "pr_open", summary: undefined }); + + const meta = readMetadata(dataDir, "upd-4"); + expect(meta!.status).toBe("pr_open"); + expect(meta!.summary).toBeUndefined(); + }); +}); + +describe("deleteMetadata", () => { + it("deletes metadata file and archives it", () => { + writeMetadata(dataDir, "del-1", { + worktree: "/tmp/w", + branch: "main", + status: "working", + }); + + deleteMetadata(dataDir, "del-1", true); + + expect(existsSync(join(dataDir, "sessions", "del-1"))).toBe(false); + const archiveDir = join(dataDir, "sessions", "archive"); + expect(existsSync(archiveDir)).toBe(true); + const files = require("node:fs").readdirSync(archiveDir) as string[]; + expect(files.length).toBe(1); + expect(files[0]).toMatch(/^del-1_/); + }); + + it("deletes without archiving when archive=false", () => { + writeMetadata(dataDir, "del-2", { + worktree: "/tmp/w", + branch: "main", + status: "working", + }); + + deleteMetadata(dataDir, "del-2", false); + + expect(existsSync(join(dataDir, "sessions", "del-2"))).toBe(false); + expect(existsSync(join(dataDir, "sessions", "archive"))).toBe(false); + }); + + it("is a no-op for nonexistent session", () => { + expect(() => deleteMetadata(dataDir, "nope")).not.toThrow(); + }); +}); + +describe("listMetadata", () => { + it("lists all session IDs", () => { + writeMetadata(dataDir, "app-1", { worktree: "/tmp", branch: "a", status: "s" }); + writeMetadata(dataDir, "app-2", { worktree: "/tmp", branch: "b", status: "s" }); + writeMetadata(dataDir, "app-3", { worktree: "/tmp", branch: "c", status: "s" }); + + const list = listMetadata(dataDir); + expect(list).toHaveLength(3); + expect(list.sort()).toEqual(["app-1", "app-2", "app-3"]); + }); + + it("excludes archive directory and dotfiles", () => { + writeMetadata(dataDir, "app-1", { worktree: "/tmp", branch: "a", status: "s" }); + mkdirSync(join(dataDir, "sessions", "archive"), { recursive: true }); + writeFileSync(join(dataDir, "sessions", ".hidden"), "x", "utf-8"); + + const list = listMetadata(dataDir); + expect(list).toEqual(["app-1"]); + }); + + it("returns empty array when sessions dir does not exist", () => { + const emptyDir = join(tmpdir(), `ao-test-empty-${randomUUID()}`); + const list = listMetadata(emptyDir); + expect(list).toEqual([]); + // no cleanup needed since dir was never created + }); +}); diff --git a/packages/core/src/__tests__/session-manager.test.ts b/packages/core/src/__tests__/session-manager.test.ts new file mode 100644 index 000000000..e080d8c90 --- /dev/null +++ b/packages/core/src/__tests__/session-manager.test.ts @@ -0,0 +1,521 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { randomUUID } from "node:crypto"; +import { createSessionManager } from "../session-manager.js"; +import { createEventBus } from "../event-bus.js"; +import { writeMetadata, readMetadata, listMetadata } from "../metadata.js"; +import type { + OrchestratorConfig, + PluginRegistry, + Runtime, + Agent, + Workspace, + Tracker, + SCM, + RuntimeHandle, + EventBus, + OrchestratorEvent, +} from "../types.js"; + +let dataDir: string; +let eventBus: EventBus; +let mockRuntime: Runtime; +let mockAgent: Agent; +let mockWorkspace: Workspace; +let mockRegistry: PluginRegistry; +let config: OrchestratorConfig; + +function makeHandle(id: string): RuntimeHandle { + return { id, runtimeName: "mock", data: {} }; +} + +beforeEach(() => { + dataDir = join(tmpdir(), `ao-test-session-mgr-${randomUUID()}`); + mkdirSync(join(dataDir, "sessions"), { recursive: true }); + + eventBus = createEventBus(null); + + mockRuntime = { + name: "mock", + create: vi.fn().mockResolvedValue(makeHandle("rt-1")), + destroy: vi.fn().mockResolvedValue(undefined), + sendMessage: vi.fn().mockResolvedValue(undefined), + getOutput: vi.fn().mockResolvedValue(""), + isAlive: vi.fn().mockResolvedValue(true), + }; + + mockAgent = { + name: "mock-agent", + processName: "mock", + getLaunchCommand: vi.fn().mockReturnValue("mock-agent --start"), + getEnvironment: vi.fn().mockReturnValue({ AGENT_VAR: "1" }), + detectActivity: vi.fn().mockResolvedValue("active"), + isProcessRunning: vi.fn().mockResolvedValue(true), + introspect: vi.fn().mockResolvedValue(null), + }; + + mockWorkspace = { + name: "mock-ws", + create: vi.fn().mockResolvedValue({ + path: "/tmp/mock-ws/app-1", + branch: "feat/TEST-1", + sessionId: "app-1", + projectId: "my-app", + }), + destroy: vi.fn().mockResolvedValue(undefined), + list: vi.fn().mockResolvedValue([]), + }; + + mockRegistry = { + register: vi.fn(), + get: vi.fn().mockImplementation((slot: string, name: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") return mockAgent; + if (slot === "workspace") return mockWorkspace; + return null; + }), + list: vi.fn().mockReturnValue([]), + loadBuiltins: vi.fn().mockResolvedValue(undefined), + loadFromConfig: vi.fn().mockResolvedValue(undefined), + }; + + config = { + dataDir, + worktreeDir: "/tmp/worktrees", + port: 3000, + defaults: { + runtime: "mock", + agent: "mock-agent", + workspace: "mock-ws", + notifiers: ["desktop"], + }, + projects: { + "my-app": { + name: "My App", + repo: "org/my-app", + path: "/tmp/my-app", + defaultBranch: "main", + sessionPrefix: "app", + scm: { plugin: "github" }, + tracker: { plugin: "github" }, + }, + }, + notifiers: {}, + notificationRouting: { + urgent: ["desktop"], + action: ["desktop"], + warning: [], + info: [], + }, + reactions: {}, + }; +}); + +afterEach(() => { + rmSync(dataDir, { recursive: true, force: true }); +}); + +describe("spawn", () => { + it("creates a session with workspace, runtime, and agent", async () => { + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + + const session = await sm.spawn({ projectId: "my-app" }); + + expect(session.id).toBe("app-1"); + expect(session.status).toBe("spawning"); + expect(session.projectId).toBe("my-app"); + expect(session.runtimeHandle).toEqual(makeHandle("rt-1")); + + // Verify workspace was created + expect(mockWorkspace.create).toHaveBeenCalled(); + // Verify agent launch command was requested + expect(mockAgent.getLaunchCommand).toHaveBeenCalled(); + // Verify runtime was created + expect(mockRuntime.create).toHaveBeenCalled(); + }); + + it("uses issue ID to derive branch name", async () => { + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + + const session = await sm.spawn({ projectId: "my-app", issueId: "INT-100" }); + + expect(session.branch).toBe("feat/INT-100"); + expect(session.issueId).toBe("INT-100"); + }); + + it("uses tracker.branchName when tracker is available", async () => { + const mockTracker: Tracker = { + name: "mock-tracker", + getIssue: vi.fn().mockResolvedValue({}), + isCompleted: vi.fn().mockResolvedValue(false), + issueUrl: vi.fn().mockReturnValue(""), + branchName: vi.fn().mockReturnValue("custom/INT-100-my-feature"), + generatePrompt: vi.fn().mockResolvedValue(""), + }; + + const registryWithTracker: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") return mockAgent; + if (slot === "workspace") return mockWorkspace; + if (slot === "tracker") return mockTracker; + return null; + }), + }; + + const sm = createSessionManager({ + config, + registry: registryWithTracker, + eventBus, + }); + + const session = await sm.spawn({ projectId: "my-app", issueId: "INT-100" }); + expect(session.branch).toBe("custom/INT-100-my-feature"); + }); + + it("increments session numbers correctly", async () => { + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + + // Pre-create some metadata to simulate existing sessions + writeMetadata(dataDir, "app-3", { worktree: "/tmp", branch: "b", status: "working" }); + writeMetadata(dataDir, "app-7", { worktree: "/tmp", branch: "b", status: "working" }); + + const session = await sm.spawn({ projectId: "my-app" }); + expect(session.id).toBe("app-8"); + }); + + it("emits session.spawned event", async () => { + const received: OrchestratorEvent[] = []; + eventBus.on("session.spawned", (e) => received.push(e)); + + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + await sm.spawn({ projectId: "my-app" }); + + expect(received).toHaveLength(1); + expect(received[0].type).toBe("session.spawned"); + expect(received[0].sessionId).toBe("app-1"); + }); + + it("writes metadata file", async () => { + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + await sm.spawn({ projectId: "my-app", issueId: "INT-42" }); + + const meta = readMetadata(dataDir, "app-1"); + expect(meta).not.toBeNull(); + expect(meta!.status).toBe("spawning"); + expect(meta!.project).toBe("my-app"); + expect(meta!.issue).toBe("INT-42"); + }); + + it("throws for unknown project", async () => { + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + await expect(sm.spawn({ projectId: "nonexistent" })).rejects.toThrow("Unknown project"); + }); + + it("throws when runtime plugin is missing", async () => { + const emptyRegistry: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockReturnValue(null), + }; + + const sm = createSessionManager({ config, registry: emptyRegistry, eventBus }); + await expect(sm.spawn({ projectId: "my-app" })).rejects.toThrow("not found"); + }); +}); + +describe("list", () => { + it("lists sessions from metadata", async () => { + writeMetadata(dataDir, "app-1", { + worktree: "/tmp/w1", + branch: "feat/a", + status: "working", + project: "my-app", + }); + writeMetadata(dataDir, "app-2", { + worktree: "/tmp/w2", + branch: "feat/b", + status: "pr_open", + project: "my-app", + }); + + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + const sessions = await sm.list(); + + expect(sessions).toHaveLength(2); + expect(sessions.map((s) => s.id).sort()).toEqual(["app-1", "app-2"]); + }); + + it("filters by project ID", async () => { + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "a", + status: "working", + project: "my-app", + }); + writeMetadata(dataDir, "other-1", { + worktree: "/tmp", + branch: "b", + status: "working", + project: "other", + }); + + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + const sessions = await sm.list("my-app"); + + expect(sessions).toHaveLength(1); + expect(sessions[0].id).toBe("app-1"); + }); + + it("marks dead runtimes as killed", async () => { + const deadRuntime: Runtime = { + ...mockRuntime, + isAlive: vi.fn().mockResolvedValue(false), + }; + const registryWithDead: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return deadRuntime; + if (slot === "agent") return mockAgent; + return null; + }), + }; + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "a", + status: "working", + project: "my-app", + runtimeHandle: JSON.stringify(makeHandle("rt-1")), + }); + + const sm = createSessionManager({ config, registry: registryWithDead, eventBus }); + const sessions = await sm.list(); + + expect(sessions[0].status).toBe("killed"); + expect(sessions[0].activity).toBe("exited"); + }); +}); + +describe("get", () => { + it("returns session by ID", async () => { + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + pr: "https://github.com/org/repo/pull/42", + }); + + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + const session = await sm.get("app-1"); + + expect(session).not.toBeNull(); + expect(session!.id).toBe("app-1"); + expect(session!.pr).not.toBeNull(); + expect(session!.pr!.number).toBe(42); + expect(session!.pr!.url).toBe("https://github.com/org/repo/pull/42"); + }); + + it("returns null for nonexistent session", async () => { + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + expect(await sm.get("nonexistent")).toBeNull(); + }); +}); + +describe("kill", () => { + it("destroys runtime, workspace, and archives metadata", async () => { + writeMetadata(dataDir, "app-1", { + worktree: "/tmp/ws", + branch: "main", + status: "working", + project: "my-app", + runtimeHandle: JSON.stringify(makeHandle("rt-1")), + }); + + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + await sm.kill("app-1"); + + expect(mockRuntime.destroy).toHaveBeenCalledWith(makeHandle("rt-1")); + expect(mockWorkspace.destroy).toHaveBeenCalledWith("/tmp/ws"); + expect(readMetadata(dataDir, "app-1")).toBeNull(); // archived + deleted + }); + + it("emits session.killed event", async () => { + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + }); + + const received: OrchestratorEvent[] = []; + eventBus.on("session.killed", (e) => received.push(e)); + + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + await sm.kill("app-1"); + + expect(received).toHaveLength(1); + expect(received[0].sessionId).toBe("app-1"); + }); + + it("throws for nonexistent session", async () => { + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + await expect(sm.kill("nonexistent")).rejects.toThrow("not found"); + }); + + it("tolerates runtime destroy failure", async () => { + const failRuntime: Runtime = { + ...mockRuntime, + destroy: vi.fn().mockRejectedValue(new Error("already gone")), + }; + const registryWithFail: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return failRuntime; + if (slot === "workspace") return mockWorkspace; + return null; + }), + }; + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + runtimeHandle: JSON.stringify(makeHandle("rt-1")), + }); + + const sm = createSessionManager({ config, registry: registryWithFail, eventBus }); + // Should not throw even though runtime.destroy fails + await expect(sm.kill("app-1")).resolves.toBeUndefined(); + }); +}); + +describe("cleanup", () => { + it("kills sessions with merged PRs", async () => { + const mockSCM: SCM = { + name: "mock-scm", + detectPR: vi.fn(), + getPRState: vi.fn().mockResolvedValue("merged"), + mergePR: vi.fn(), + closePR: vi.fn(), + getCIChecks: vi.fn(), + getCISummary: vi.fn(), + getReviews: vi.fn(), + getReviewDecision: vi.fn(), + getPendingComments: vi.fn(), + getAutomatedComments: vi.fn(), + getMergeability: vi.fn(), + }; + + const registryWithSCM: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") return mockAgent; + if (slot === "workspace") return mockWorkspace; + if (slot === "scm") return mockSCM; + return null; + }), + }; + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "pr_open", + project: "my-app", + pr: "https://github.com/org/repo/pull/10", + runtimeHandle: JSON.stringify(makeHandle("rt-1")), + }); + + const sm = createSessionManager({ config, registry: registryWithSCM, eventBus }); + const result = await sm.cleanup(); + + expect(result.killed).toContain("app-1"); + expect(result.skipped).toHaveLength(0); + }); + + it("skips sessions without merged PRs or completed issues", async () => { + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + }); + + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + const result = await sm.cleanup(); + + expect(result.killed).toHaveLength(0); + expect(result.skipped).toContain("app-1"); + }); + + it("kills sessions with dead runtimes", async () => { + const deadRuntime: Runtime = { + ...mockRuntime, + isAlive: vi.fn().mockResolvedValue(false), + }; + const registryWithDead: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return deadRuntime; + if (slot === "agent") return mockAgent; + if (slot === "workspace") return mockWorkspace; + return null; + }), + }; + + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + runtimeHandle: JSON.stringify(makeHandle("rt-1")), + }); + + const sm = createSessionManager({ config, registry: registryWithDead, eventBus }); + const result = await sm.cleanup(); + + expect(result.killed).toContain("app-1"); + }); +}); + +describe("send", () => { + it("sends message via runtime.sendMessage", async () => { + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + runtimeHandle: JSON.stringify(makeHandle("rt-1")), + }); + + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + await sm.send("app-1", "Fix the CI failures"); + + expect(mockRuntime.sendMessage).toHaveBeenCalledWith( + makeHandle("rt-1"), + "Fix the CI failures" + ); + }); + + it("throws for nonexistent session", async () => { + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + await expect(sm.send("nope", "hello")).rejects.toThrow("not found"); + }); + + it("throws when no runtime handle", async () => { + writeMetadata(dataDir, "app-1", { + worktree: "/tmp", + branch: "main", + status: "working", + project: "my-app", + }); + + const sm = createSessionManager({ config, registry: mockRegistry, eventBus }); + await expect(sm.send("app-1", "hello")).rejects.toThrow("No runtime handle"); + }); +}); diff --git a/packages/core/src/__tests__/tmux.test.ts b/packages/core/src/__tests__/tmux.test.ts new file mode 100644 index 000000000..5fe28abe6 --- /dev/null +++ b/packages/core/src/__tests__/tmux.test.ts @@ -0,0 +1,304 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import * as childProcess from "node:child_process"; +import { + isTmuxAvailable, + listSessions, + hasSession, + newSession, + sendKeys, + capturePane, + killSession, + getPaneTTY, +} from "../tmux.js"; + +// Mock child_process.execFile +vi.mock("node:child_process", () => ({ + execFile: vi.fn(), +})); + +const mockExecFile = vi.mocked(childProcess.execFile); + +/** Helper to make execFile resolve with stdout. */ +function mockTmuxSuccess(stdout: string) { + mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => { + (callback as Function)(null, stdout, ""); + return {} as any; + }); +} + +/** Helper to make execFile reject with an error. */ +function mockTmuxError(message: string) { + mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => { + (callback as Function)(new Error(message), "", message); + return {} as any; + }); +} + +/** Helper for sequential tmux calls returning different results. */ +function mockTmuxSequence(results: Array<{ stdout?: string; error?: string }>) { + let callIndex = 0; + mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => { + const result = results[callIndex] ?? results[results.length - 1]; + callIndex++; + if (result.error) { + (callback as Function)(new Error(result.error), "", result.error); + } else { + (callback as Function)(null, result.stdout ?? "", ""); + } + return {} as any; + }); +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("isTmuxAvailable", () => { + it("returns true when tmux server is running", async () => { + mockTmuxSuccess("session1\nsession2\n"); + expect(await isTmuxAvailable()).toBe(true); + }); + + it("returns false when tmux server is not running", async () => { + mockTmuxError("no server running"); + expect(await isTmuxAvailable()).toBe(false); + }); +}); + +describe("listSessions", () => { + it("parses tmux session list", async () => { + mockTmuxSuccess( + "app-1\tMon Jan 1 00:00:00 2025\t0\t2\n" + + "app-2\tTue Jan 2 00:00:00 2025\t1\t1\n" + ); + + const sessions = await listSessions(); + expect(sessions).toHaveLength(2); + expect(sessions[0]).toEqual({ + name: "app-1", + created: "Mon Jan 1 00:00:00 2025", + attached: false, + windows: 2, + }); + expect(sessions[1]).toEqual({ + name: "app-2", + created: "Tue Jan 2 00:00:00 2025", + attached: true, + windows: 1, + }); + }); + + it("returns empty array when no sessions", async () => { + mockTmuxError("no server running on /private/tmp/tmux-501/default"); + expect(await listSessions()).toEqual([]); + }); + + it("handles empty output", async () => { + mockTmuxSuccess(""); + expect(await listSessions()).toEqual([]); + }); +}); + +describe("hasSession", () => { + it("returns true when session exists", async () => { + mockTmuxSuccess(""); + expect(await hasSession("app-1")).toBe(true); + expect(mockExecFile).toHaveBeenCalledWith( + "tmux", + ["has-session", "-t", "app-1"], + expect.any(Object), + expect.any(Function) + ); + }); + + it("returns false when session does not exist", async () => { + mockTmuxError("session not found"); + expect(await hasSession("app-99")).toBe(false); + }); +}); + +describe("newSession", () => { + it("creates a basic session", async () => { + mockTmuxSuccess(""); + + await newSession({ name: "test-1", cwd: "/tmp/workspace" }); + + expect(mockExecFile).toHaveBeenCalledWith( + "tmux", + ["new-session", "-d", "-s", "test-1", "-c", "/tmp/workspace"], + expect.any(Object), + expect.any(Function) + ); + }); + + it("includes environment variables", async () => { + mockTmuxSuccess(""); + + await newSession({ + name: "test-2", + cwd: "/tmp", + environment: { AO_SESSION: "test-2", SOME_VAR: "value" }, + }); + + const args = mockExecFile.mock.calls[0][1] as string[]; + expect(args).toContain("-e"); + expect(args).toContain("AO_SESSION=test-2"); + expect(args).toContain("SOME_VAR=value"); + }); + + it("includes window size", async () => { + mockTmuxSuccess(""); + + await newSession({ name: "test-3", cwd: "/tmp", width: 200, height: 50 }); + + const args = mockExecFile.mock.calls[0][1] as string[]; + expect(args).toContain("-x"); + expect(args).toContain("200"); + expect(args).toContain("-y"); + expect(args).toContain("50"); + }); + + it("sends initial command after creation", async () => { + // First call: new-session, Second call: send-keys, Third call: send-keys Enter + mockTmuxSequence([ + { stdout: "" }, + { stdout: "" }, + { stdout: "" }, + ]); + + await newSession({ name: "test-4", cwd: "/tmp", command: "echo hello" }); + + // Should have called new-session then send-keys twice (text + Enter) + expect(mockExecFile).toHaveBeenCalledTimes(3); + const secondCallArgs = mockExecFile.mock.calls[1][1] as string[]; + expect(secondCallArgs).toContain("send-keys"); + expect(secondCallArgs).toContain("echo hello"); + }); +}); + +describe("sendKeys", () => { + it("sends short text with send-keys", async () => { + mockTmuxSequence([{ stdout: "" }, { stdout: "" }]); + + await sendKeys("app-1", "hello world"); + + expect(mockExecFile).toHaveBeenCalledTimes(2); + const firstArgs = mockExecFile.mock.calls[0][1] as string[]; + expect(firstArgs).toEqual(["send-keys", "-t", "app-1", "hello world"]); + // Second call is Enter + const secondArgs = mockExecFile.mock.calls[1][1] as string[]; + expect(secondArgs).toEqual(["send-keys", "-t", "app-1", "Enter"]); + }); + + it("skips Enter when pressEnter=false", async () => { + mockTmuxSuccess(""); + + await sendKeys("app-1", "hello", false); + + expect(mockExecFile).toHaveBeenCalledTimes(1); + }); + + it("uses load-buffer for long text", async () => { + const longText = "a".repeat(250); + mockTmuxSequence([ + { stdout: "" }, // load-buffer + { stdout: "" }, // paste-buffer + { stdout: "" }, // send-keys Enter + ]); + + await sendKeys("app-1", longText); + + const firstArgs = mockExecFile.mock.calls[0][1] as string[]; + expect(firstArgs[0]).toBe("load-buffer"); + + const secondArgs = mockExecFile.mock.calls[1][1] as string[]; + expect(secondArgs).toEqual(["paste-buffer", "-t", "app-1"]); + }); + + it("uses load-buffer for multiline text", async () => { + mockTmuxSequence([ + { stdout: "" }, // load-buffer + { stdout: "" }, // paste-buffer + { stdout: "" }, // send-keys Enter + ]); + + await sendKeys("app-1", "line1\nline2"); + + const firstArgs = mockExecFile.mock.calls[0][1] as string[]; + expect(firstArgs[0]).toBe("load-buffer"); + }); +}); + +describe("capturePane", () => { + it("captures pane output with default lines", async () => { + mockTmuxSuccess("some output\nfrom tmux\n"); + + const output = await capturePane("app-1"); + expect(output).toBe("some output\nfrom tmux\n"); + expect(mockExecFile).toHaveBeenCalledWith( + "tmux", + ["capture-pane", "-t", "app-1", "-p", "-S", "-30"], + expect.any(Object), + expect.any(Function) + ); + }); + + it("captures with custom line count", async () => { + mockTmuxSuccess("output\n"); + + await capturePane("app-1", 50); + + const args = mockExecFile.mock.calls[0][1] as string[]; + expect(args).toContain("-50"); + }); +}); + +describe("killSession", () => { + it("kills a tmux session", async () => { + mockTmuxSuccess(""); + + await killSession("app-1"); + + expect(mockExecFile).toHaveBeenCalledWith( + "tmux", + ["kill-session", "-t", "app-1"], + expect.any(Object), + expect.any(Function) + ); + }); + + it("throws when session does not exist", async () => { + mockTmuxError("session not found: app-99"); + await expect(killSession("app-99")).rejects.toThrow("session not found"); + }); +}); + +describe("getPaneTTY", () => { + it("returns TTY for first pane", async () => { + mockTmuxSuccess("/dev/ttys004\n"); + + const tty = await getPaneTTY("app-1"); + expect(tty).toBe("/dev/ttys004"); + }); + + it("returns first TTY when multiple panes", async () => { + mockTmuxSuccess("/dev/ttys004\n/dev/ttys005\n"); + + const tty = await getPaneTTY("app-1"); + expect(tty).toBe("/dev/ttys004"); + }); + + it("returns null when session not found", async () => { + mockTmuxError("session not found"); + + const tty = await getPaneTTY("nonexistent"); + expect(tty).toBeNull(); + }); + + it("returns null for empty output", async () => { + mockTmuxSuccess(""); + + const tty = await getPaneTTY("app-1"); + expect(tty).toBeNull(); + }); +}); diff --git a/packages/core/src/event-bus.ts b/packages/core/src/event-bus.ts index 92c21eadf..b5143375b 100644 --- a/packages/core/src/event-bus.ts +++ b/packages/core/src/event-bus.ts @@ -52,7 +52,7 @@ function inferPriority(type: EventType): EventPriority { if (type.includes("stuck") || type.includes("needs_input") || type.includes("errored")) { return "urgent"; } - if (type.includes("approved") || type.includes("ready") || type.includes("merged")) { + if (type.includes("approved") || type.includes("ready") || type.includes("merged") || type.includes("completed")) { return "action"; } if (type.includes("fail") || type.includes("changes_requested") || type.includes("conflicts")) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9575723fd..1ece77252 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,26 +6,7 @@ settings: importers: - .: - devDependencies: - '@eslint/js': - specifier: ^10.0.1 - version: 10.0.1(eslint@10.0.0) - '@types/node': - specifier: ^25.2.3 - version: 25.2.3 - eslint: - specifier: ^10.0.0 - version: 10.0.0 - eslint-config-prettier: - specifier: ^10.1.8 - version: 10.1.8(eslint@10.0.0) - prettier: - specifier: ^3.8.1 - version: 3.8.1 - typescript-eslint: - specifier: ^8.55.0 - version: 8.55.0(eslint@10.0.0)(typescript@5.9.3) + .: {} packages/cli: dependencies: @@ -67,6 +48,9 @@ importers: typescript: specifier: ^5.7.0 version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) packages/plugins/agent-aider: dependencies: @@ -452,61 +436,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.23.1': - resolution: {integrity: sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/config-helpers@0.5.2': - resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/core@1.1.0': - resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/js@10.0.1': - resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/object-schema@3.0.1': - resolution: {integrity: sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.6.0': - resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -644,9 +573,8 @@ packages: cpu: [x64] os: [win32] - '@isaacs/cliui@9.0.0': - resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} - engines: {node: '>=18'} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} '@next/env@15.5.12': resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==} @@ -699,18 +627,146 @@ packages: cpu: [x64] os: [win32] + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.2.3': resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} @@ -722,99 +778,50 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - '@typescript-eslint/eslint-plugin@8.55.0': - resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} peerDependencies: - '@typescript-eslint/parser': ^8.55.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true - '@typescript-eslint/parser@8.55.0': - resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} - '@typescript-eslint/project-service@8.55.0': - resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} - '@typescript-eslint/scope-manager@8.55.0': - resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} - '@typescript-eslint/tsconfig-utils@8.55.0': - resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} - '@typescript-eslint/type-utils@8.55.0': - resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/types@8.55.0': - resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.55.0': - resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.55.0': - resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.55.0': - resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - balanced-match@4.0.2: - resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} - engines: {node: 20 || >=22} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - brace-expansion@5.0.2: - resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} - engines: {node: 20 || >=22} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} caniuse-lite@1.0.30001769: resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -834,25 +841,9 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -860,75 +851,20 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - eslint-config-prettier@10.1.8: - resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-scope@9.1.0: - resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@5.0.0: - resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint@10.0.0: - resolution: {integrity: sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@11.1.0: - resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -939,21 +875,6 @@ packages: picomatch: optional: true - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -966,30 +887,6 @@ packages: get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} @@ -1002,60 +899,22 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jackspeak@4.2.3: - resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} - engines: {node: 20 || >=22} - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - minimatch@10.2.0: - resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==} - engines: {node: 20 || >=22} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@15.5.12: resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -1077,33 +936,19 @@ packages: sass: optional: true + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - ora@8.2.0: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1116,18 +961,9 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} - engines: {node: '>=14'} - hasBin: true - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} @@ -1145,6 +981,11 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1157,13 +998,8 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} @@ -1173,6 +1009,12 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -1201,15 +1043,20 @@ packages: tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1219,17 +1066,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - typescript-eslint@8.55.0: - resolution: {integrity: sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1238,27 +1074,90 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -1347,51 +1246,6 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.0.0)': - dependencies: - eslint: 10.0.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.23.1': - dependencies: - '@eslint/object-schema': 3.0.1 - debug: 4.4.3 - minimatch: 10.2.0 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.5.2': - dependencies: - '@eslint/core': 1.1.0 - - '@eslint/core@1.1.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/js@10.0.1(eslint@10.0.0)': - optionalDependencies: - eslint: 10.0.0 - - '@eslint/object-schema@3.0.1': {} - - '@eslint/plugin-kit@0.6.0': - dependencies: - '@eslint/core': 1.1.0 - levn: 0.4.1 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - '@img/colour@1.0.0': optional: true @@ -1489,7 +1343,7 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@isaacs/cliui@9.0.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} '@next/env@15.5.12': {} @@ -1517,16 +1371,96 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.12': optional: true + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@standard-schema/spec@1.1.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 - '@types/esrecurse@4.3.1': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} - '@types/json-schema@7.0.15': {} - '@types/node@25.2.3': dependencies: undici-types: 7.16.0 @@ -1539,128 +1473,53 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3)': + '@vitest/expect@4.0.18': dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/type-utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.55.0 - eslint: 10.0.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 - '@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3)': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.55.0 - debug: 4.4.3 - eslint: 10.0.0 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) - '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': + '@vitest/pretty-format@4.0.18': dependencies: - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + tinyrainbow: 3.0.3 - '@typescript-eslint/scope-manager@8.55.0': + '@vitest/runner@4.0.18': dependencies: - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 + '@vitest/utils': 4.0.18 + pathe: 2.0.3 - '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': + '@vitest/snapshot@4.0.18': dependencies: - typescript: 5.9.3 + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 - '@typescript-eslint/type-utils@8.55.0(eslint@10.0.0)(typescript@5.9.3)': + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': dependencies: - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.0.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.55.0': {} - - '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 - debug: 4.4.3 - minimatch: 9.0.5 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.55.0(eslint@10.0.0)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - eslint: 10.0.0 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.55.0': - dependencies: - '@typescript-eslint/types': 8.55.0 - eslint-visitor-keys: 4.2.1 - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 ansi-regex@6.2.2: {} - balanced-match@1.0.2: {} - - balanced-match@4.0.2: - dependencies: - jackspeak: 4.2.3 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - brace-expansion@5.0.2: - dependencies: - balanced-match: 4.0.2 + assertion-error@2.0.1: {} caniuse-lite@1.0.30001769: {} + chai@6.2.2: {} + chalk@5.6.2: {} cli-cursor@5.0.0: @@ -1673,25 +1532,15 @@ snapshots: commander@13.1.0: {} - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - csstype@3.2.3: {} - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - detect-libc@2.1.2: optional: true emoji-regex@10.6.0: {} + es-module-lexer@1.7.0: {} + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -1721,104 +1570,16 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 - escape-string-regexp@4.0.0: {} - - eslint-config-prettier@10.1.8(eslint@10.0.0): + estree-walker@3.0.3: dependencies: - eslint: 10.0.0 - - eslint-scope@9.1.0: - dependencies: - '@types/esrecurse': 4.3.1 '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint-visitor-keys@5.0.0: {} - - eslint@10.0.0: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.1 - '@eslint/config-helpers': 0.5.2 - '@eslint/core': 1.1.0 - '@eslint/plugin-kit': 0.6.0 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.12.6 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.0 - eslint-visitor-keys: 5.0.0 - espree: 11.1.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.0 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@11.1.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 5.0.0 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} + expect-type@1.3.0: {} fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - - flatted@3.3.3: {} - fsevents@2.3.3: optional: true @@ -1828,74 +1589,25 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - ignore@5.3.2: {} - - ignore@7.0.5: {} - - imurmurhash@0.1.4: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - is-interactive@2.0.0: {} is-unicode-supported@1.3.0: {} is-unicode-supported@2.1.0: {} - isexe@2.0.0: {} - - jackspeak@4.2.3: - dependencies: - '@isaacs/cliui': 9.0.0 - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - log-symbols@6.0.0: dependencies: chalk: 5.6.2 is-unicode-supported: 1.3.0 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + mimic-function@5.0.1: {} - minimatch@10.2.0: - dependencies: - brace-expansion: 5.0.2 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - - ms@2.1.3: {} - nanoid@3.3.11: {} - natural-compare@1.4.0: {} - next@15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 15.5.12 @@ -1919,19 +1631,12 @@ snapshots: - '@babel/core' - babel-plugin-macros + obug@2.1.1: {} + onetime@7.0.0: dependencies: mimic-function: 5.0.1 - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - ora@8.2.0: dependencies: chalk: 5.6.2 @@ -1944,17 +1649,7 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -1966,11 +1661,11 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prelude-ls@1.2.1: {} - - prettier@3.8.1: {} - - punycode@2.3.1: {} + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 react-dom@19.2.4(react@19.2.4): dependencies: @@ -1986,9 +1681,41 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + scheduler@0.27.0: {} - semver@7.7.4: {} + semver@7.7.4: + optional: true sharp@0.34.5: dependencies: @@ -2022,16 +1749,16 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} source-map-js@1.2.1: {} + stackback@0.0.2: {} + + std-env@3.10.0: {} + stdin-discarder@0.2.2: {} string-width@7.2.0: @@ -2051,14 +1778,16 @@ snapshots: tailwindcss@4.1.18: {} + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - ts-api-utils@2.4.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 + tinyrainbow@3.0.3: {} tslib@2.8.1: {} @@ -2069,37 +1798,66 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - typescript-eslint@8.55.0(eslint@10.0.0)(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - eslint: 10.0.0 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - typescript@5.9.3: {} undici-types@7.16.0: {} - uri-js@4.4.1: + vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2): dependencies: - punycode: 2.3.1 + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.2.3 + fsevents: 2.3.3 + tsx: 4.21.0 + yaml: 2.8.2 - which@2.0.2: + vitest@4.0.18(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2): dependencies: - isexe: 2.0.0 + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.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: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.2.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml - word-wrap@1.2.5: {} + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 yaml@2.8.2: {} - yocto-queue@0.1.0: {} - zod@3.25.76: {}