diff --git a/apps/web/app/gen2/[workspaceId]/superset/page.tsx b/apps/web/app/gen2/[workspaceId]/superset/page.tsx index 30ca88e58..06125f743 100644 --- a/apps/web/app/gen2/[workspaceId]/superset/page.tsx +++ b/apps/web/app/gen2/[workspaceId]/superset/page.tsx @@ -1,10 +1,11 @@ import { notFound } from "next/navigation"; import type { Metadata } from "next"; -import { SupersetFilePane } from "@/components/gen2/superset-file-pane"; +import { SupersetWorkspaceShell } from "@/components/gen2/superset-workspace-shell"; import { requireUser } from "@/lib/auth/session"; import { Gen2AccessError } from "@/lib/gen2/errors"; import { isGen2SupersetFilePaneEnabled } from "@/lib/gen2/superset-file-feature"; +import { isGen2SupersetRuntimeEnabled } from "@/lib/gen2/superset-runtime-feature"; import { getGen2WorkspaceDetail } from "@/lib/gen2/workspaces"; export const metadata: Metadata = { title: "Superset files" }; @@ -28,6 +29,10 @@ export default async function Gen2SupersetFilesPage({ } return ( - + ); } diff --git a/apps/web/app/gen2/workspace.css b/apps/web/app/gen2/workspace.css index b9595677d..6446d30f4 100644 --- a/apps/web/app/gen2/workspace.css +++ b/apps/web/app/gen2/workspace.css @@ -1090,6 +1090,212 @@ /* --------------------------------------------------- Superset file slice */ +.gen2-superset-workspace-shell { + --ss-background: #151110; + --ss-foreground: #eae8e6; + --ss-card: #201e1c; + --ss-secondary: #2a2827; + --ss-muted: #2a2827; + --ss-muted-foreground: #a8a5a3; + --ss-border: #2a2827; + min-height: 100dvh; + background: var(--ss-background); + color: var(--ss-foreground); + font-family: var(--font-geist-sans), Arial, sans-serif; + color-scheme: dark; +} + +.gen2-superset-workspace-header { + display: flex; + align-items: center; + gap: 8px; + min-height: 48px; + padding: 6px 12px; + border-bottom: 1px solid var(--ss-border); + background: color-mix(in srgb, var(--ss-muted) 45%, var(--ss-background)); +} + +.gen2-superset-workspace-header select, +.gen2-superset-worktree-create, +.gen2-superset-workspace-tabs button, +.gen2-superset-worktree-form button { + min-height: 32px; + border: 1px solid var(--ss-border); + border-radius: 6px; + background: var(--ss-secondary); + color: var(--ss-foreground); + font: inherit; + font-size: 0.8125rem; +} + +.gen2-superset-workspace-header select { + min-width: 10rem; + padding: 0 8px; +} + +.gen2-superset-worktree-create, +.gen2-superset-workspace-tabs button, +.gen2-superset-worktree-form button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 10px; + cursor: pointer; +} + +.gen2-superset-workspace-tabs { + display: flex; + gap: 4px; + margin-left: auto; +} + +.gen2-superset-workspace-tabs button[aria-selected="true"] { + border-color: color-mix(in srgb, var(--ss-highlight) 65%, var(--ss-border)); + background: var(--ss-card); +} + +.gen2-superset-workspace-header select:focus-visible, +.gen2-superset-worktree-create:focus-visible, +.gen2-superset-workspace-tabs button:focus-visible, +.gen2-superset-worktree-form input:focus-visible, +.gen2-superset-worktree-form button:focus-visible { + outline: 2px solid var(--ss-highlight); + outline-offset: 2px; +} + +.gen2-superset-worktree-form { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)) auto; + gap: 8px; + padding: 10px 12px; + border-bottom: 1px solid var(--ss-border); + background: var(--ss-card); +} + +.gen2-superset-worktree-form label { + display: grid; + gap: 4px; + color: var(--ss-muted-foreground); + font-size: 0.75rem; +} + +.gen2-superset-worktree-form label span { + font-size: 0.6875rem; +} + +.gen2-superset-worktree-form input { + min-width: 0; + min-height: 32px; + padding: 0 8px; + border: 1px solid var(--ss-border); + border-radius: 6px; + background: var(--ss-background); + color: var(--ss-foreground); + font: inherit; +} + +.gen2-superset-runtime-notice { + margin: 0; + padding: 8px 12px; + border-bottom: 1px solid var(--ss-border); + background: color-mix(in srgb, var(--ss-highlight) 11%, var(--ss-background)); + color: var(--ss-foreground); + font-size: 0.8125rem; +} + +.gen2-superset-files-panel .gen2-superset-file-pane { + min-height: calc(100dvh - 48px); +} + +.gen2-superset-tool-panel { + min-height: calc(100dvh - 48px); + padding: 16px; + background: var(--ss-background); +} + +.gen2-superset-tool-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + max-width: 72rem; + margin: 0 auto; + padding-bottom: 12px; + border-bottom: 1px solid var(--ss-border); +} + +.gen2-superset-tool-header h2, +.gen2-superset-tool-header p { + margin: 0; +} + +.gen2-superset-tool-header h2 { + font-size: 1rem; +} + +.gen2-superset-tool-header p, +.gen2-superset-tool-empty { + color: var(--ss-muted-foreground); + font-size: 0.8125rem; +} + +.gen2-superset-tool-error { + max-width: 72rem; + margin: 12px auto; + color: #ffb4a0; +} + +.gen2-superset-change-list, +.gen2-superset-diff { + box-sizing: border-box; + width: min(72rem, 100%); + margin: 16px auto; + border: 1px solid var(--ss-border); + border-radius: 8px; + background: var(--ss-card); +} + +.gen2-superset-change-list { + padding: 0; + overflow: hidden; + list-style: none; +} + +.gen2-superset-change-list li { + display: flex; + gap: 10px; + padding: 8px 10px; + border-bottom: 1px solid var(--ss-border); + font: + 0.8125rem var(--font-geist-mono), + monospace; +} + +.gen2-superset-change-list li:last-child { + border-bottom: 0; +} + +.gen2-superset-change-list code { + min-width: 1.5rem; + color: var(--ss-highlight); +} + +.gen2-superset-diff { + min-height: 16rem; + padding: 12px; + overflow: auto; + color: var(--ss-foreground); + font: + 0.75rem/1.55 var(--font-geist-mono), + monospace; + white-space: pre-wrap; +} + +.gen2-superset-tool-empty { + width: min(72rem, 100%); + margin: 16px auto; +} + .gen2-superset-file-pane { --ss-background: #151110; --ss-foreground: #eae8e6; @@ -1809,6 +2015,27 @@ } @media (max-width: 600px) { + .gen2-superset-workspace-header { + flex-wrap: wrap; + min-height: 52px; + } + + .gen2-superset-workspace-tabs { + order: 3; + width: 100%; + margin-left: 0; + } + + .gen2-superset-workspace-tabs button { + flex: 1; + justify-content: center; + min-height: 40px; + } + + .gen2-superset-worktree-form { + grid-template-columns: 1fr; + } + .gen2-superset-files-actions .gen2-superset-icon-button:disabled { display: none; } diff --git a/apps/web/components/gen2/superset-file-client.test.ts b/apps/web/components/gen2/superset-file-client.test.ts index a93a2d116..274b91153 100644 --- a/apps/web/components/gen2/superset-file-client.test.ts +++ b/apps/web/components/gen2/superset-file-client.test.ts @@ -25,10 +25,10 @@ describe("Superset file client", () => { ); vi.stubGlobal("fetch", fetchMock); - const loaded = await readSupersetFile(workspaceId, file.path); + const loaded = await readSupersetFile(workspaceId, "main", file.path); const readCall = fetchMock.mock.calls.at(0); expect(readCall?.[0]).toContain("path=src%2Fa+file.ts"); - const saved = await saveSupersetFile(workspaceId, loaded, "edited"); + const saved = await saveSupersetFile(workspaceId, "main", loaded, "edited"); expect(saved.revision).toBe("rev-2"); const saveCall = fetchMock.mock.calls.at(1); expect(saveCall?.[1]).toMatchObject({ method: "PUT" }); @@ -53,7 +53,9 @@ describe("Superset file client", () => { ), ); - await expect(saveSupersetFile(workspaceId, file, "edited")).rejects.toEqual( + await expect( + saveSupersetFile(workspaceId, "main", file, "edited"), + ).rejects.toEqual( expect.objectContaining({ status: 409, currentRevision: "rev-2", diff --git a/apps/web/components/gen2/superset-file-client.ts b/apps/web/components/gen2/superset-file-client.ts index d6224ee8e..a6c6beb7f 100644 --- a/apps/web/components/gen2/superset-file-client.ts +++ b/apps/web/components/gen2/superset-file-client.ts @@ -6,12 +6,15 @@ import { gen2SupersetMoveEntryResponseSchema, gen2SupersetReadFileResponseSchema, gen2SupersetSaveFileResponseSchema, + gen2SupersetWorktreeCreateResponseSchema, + gen2SupersetWorktreeListResponseSchema, type Gen2SupersetFile, type Gen2SupersetEntry, type Gen2SupersetExternalFileChange, + type Gen2SupersetWorktree, } from "@codev/contracts"; -export const SUPERSET_WORKTREE_ID = "main"; +export const DEFAULT_SUPERSET_WORKTREE_ID = "main"; export class SupersetFileApiError extends Error { constructor( @@ -53,10 +56,11 @@ async function request( export async function listSupersetFiles( workspaceId: string, + worktreeId: string, signal?: AbortSignal, ): Promise { const response = await request<{ files: Gen2SupersetEntry[] }>( - `${fileApiBase(workspaceId)}/files?worktreeId=${SUPERSET_WORKTREE_ID}`, + `${fileApiBase(workspaceId)}/files?worktreeId=${encodeURIComponent(worktreeId)}`, (payload) => gen2SupersetListFilesResponseSchema.parse(payload), { signal: signal ?? null }, ); @@ -65,6 +69,7 @@ export async function listSupersetFiles( export async function createSupersetEntry( workspaceId: string, + worktreeId: string, input: { parentPath: string; name: string; kind: "file" | "directory" }, ): Promise { const response = await request<{ entry: Gen2SupersetEntry }>( @@ -73,7 +78,7 @@ export async function createSupersetEntry( { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ worktreeId: SUPERSET_WORKTREE_ID, ...input }), + body: JSON.stringify({ worktreeId, ...input }), }, ); return response.entry; @@ -81,6 +86,7 @@ export async function createSupersetEntry( export async function moveSupersetEntry( workspaceId: string, + worktreeId: string, input: { path: string; parentPath: string; name: string }, ): Promise { const response = await request<{ entry: Gen2SupersetEntry }>( @@ -89,7 +95,7 @@ export async function moveSupersetEntry( { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ worktreeId: SUPERSET_WORKTREE_ID, ...input }), + body: JSON.stringify({ worktreeId, ...input }), }, ); return response.entry; @@ -97,6 +103,7 @@ export async function moveSupersetEntry( export async function deleteSupersetEntry( workspaceId: string, + worktreeId: string, path: string, ): Promise { const response = await request<{ path: string }>( @@ -105,7 +112,7 @@ export async function deleteSupersetEntry( { method: "DELETE", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ worktreeId: SUPERSET_WORKTREE_ID, path }), + body: JSON.stringify({ worktreeId, path }), }, ); return response.path; @@ -113,11 +120,12 @@ export async function deleteSupersetEntry( export async function readSupersetFile( workspaceId: string, + worktreeId: string, path: string, signal?: AbortSignal, ): Promise { const query = new URLSearchParams({ - worktreeId: SUPERSET_WORKTREE_ID, + worktreeId, path, }); const response = await request<{ file: Gen2SupersetFile }>( @@ -130,6 +138,7 @@ export async function readSupersetFile( export async function saveSupersetFile( workspaceId: string, + worktreeId: string, file: Gen2SupersetFile, contents: string, ): Promise { @@ -140,7 +149,7 @@ export async function saveSupersetFile( method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - worktreeId: SUPERSET_WORKTREE_ID, + worktreeId, path: file.path, contents, expectedRevision: file.revision, @@ -152,12 +161,41 @@ export async function saveSupersetFile( export async function listSupersetFileChanges( workspaceId: string, + worktreeId: string, signal?: AbortSignal, ): Promise { const response = await request<{ changes: Gen2SupersetExternalFileChange[] }>( - `${fileApiBase(workspaceId)}/file/changes?worktreeId=${SUPERSET_WORKTREE_ID}`, + `${fileApiBase(workspaceId)}/file/changes?worktreeId=${encodeURIComponent(worktreeId)}`, (payload) => gen2SupersetExternalFileChangesResponseSchema.parse(payload), { signal: signal ?? null }, ); return response.changes; } + +export async function listSupersetWorktrees( + workspaceId: string, + signal?: AbortSignal, +): Promise { + const response = await request<{ worktrees: Gen2SupersetWorktree[] }>( + `${fileApiBase(workspaceId)}/worktrees`, + (payload) => gen2SupersetWorktreeListResponseSchema.parse(payload), + { signal: signal ?? null }, + ); + return response.worktrees; +} + +export async function createSupersetWorktree( + workspaceId: string, + input: { worktreeId: string; branch: string; baseRef?: string }, +): Promise { + const response = await request<{ worktree: Gen2SupersetWorktree }>( + `${fileApiBase(workspaceId)}/worktrees`, + (payload) => gen2SupersetWorktreeCreateResponseSchema.parse(payload), + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }, + ); + return response.worktree; +} diff --git a/apps/web/components/gen2/superset-file-pane.test.tsx b/apps/web/components/gen2/superset-file-pane.test.tsx index 8f9983672..8033ab019 100644 --- a/apps/web/components/gen2/superset-file-pane.test.tsx +++ b/apps/web/components/gen2/superset-file-pane.test.tsx @@ -69,11 +69,17 @@ const secondFile = { describe("SupersetFilePane", () => { beforeEach(() => { mocks.list.mockResolvedValue([firstFile, secondFile]); - mocks.read.mockImplementation(async (_id: string, path: string) => - path === firstFile.path ? firstFile : secondFile, + mocks.read.mockImplementation( + async (_id: string, _worktree: string, path: string) => + path === firstFile.path ? firstFile : secondFile, ); mocks.save.mockImplementation( - async (_id: string, file: typeof firstFile, contents: string) => ({ + async ( + _id: string, + _worktree: string, + file: typeof firstFile, + contents: string, + ) => ({ ...file, contents, revision: "rev-2", @@ -82,6 +88,7 @@ describe("SupersetFilePane", () => { mocks.create.mockImplementation( async ( _id: string, + _worktree: string, input: { parentPath: string; name: string; @@ -107,10 +114,12 @@ describe("SupersetFilePane", () => { ); expect(mocks.list).toHaveBeenCalledWith( workspaceId, + "main", expect.any(AbortSignal), ); expect(mocks.read).toHaveBeenCalledWith( workspaceId, + "main", firstFile.path, expect.any(AbortSignal), ); @@ -126,6 +135,7 @@ describe("SupersetFilePane", () => { await waitFor(() => expect(mocks.save).toHaveBeenCalledWith( workspaceId, + "main", expect.objectContaining({ path: firstFile.path, revision: "rev-1" }), "export const live = true;", ), @@ -134,6 +144,29 @@ describe("SupersetFilePane", () => { expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); }); + it("scopes file requests and the shared document to the selected worktree", async () => { + render( + , + ); + + await screen.findByLabelText("Code"); + expect(mocks.list).toHaveBeenCalledWith( + workspaceId, + "feature-auth", + expect.any(AbortSignal), + ); + expect(mocks.read).toHaveBeenCalledWith( + workspaceId, + "feature-auth", + firstFile.path, + expect.any(AbortSignal), + ); + }); + it("preserves the draft and offers recovery when a save conflicts", async () => { mocks.save.mockRejectedValue( new SupersetFileApiError("This file changed.", 409, "rev-2"), @@ -180,12 +213,13 @@ describe("SupersetFilePane", () => { contents: "", revision: "rev-new", }; - mocks.read.mockImplementation(async (_id: string, path: string) => - path === newFile.path - ? newFile - : path === firstFile.path - ? firstFile - : secondFile, + mocks.read.mockImplementation( + async (_id: string, _worktree: string, path: string) => + path === newFile.path + ? newFile + : path === firstFile.path + ? firstFile + : secondFile, ); render(); await screen.findByLabelText("Code"); @@ -197,7 +231,7 @@ describe("SupersetFilePane", () => { fireEvent.click(screen.getByRole("button", { name: "Create" })); await waitFor(() => - expect(mocks.create).toHaveBeenCalledWith(workspaceId, { + expect(mocks.create).toHaveBeenCalledWith(workspaceId, "main", { parentPath: "", name: newFile.path, kind: "file", @@ -206,6 +240,7 @@ describe("SupersetFilePane", () => { await waitFor(() => expect(mocks.read).toHaveBeenLastCalledWith( workspaceId, + "main", newFile.path, undefined, ), @@ -228,7 +263,7 @@ describe("SupersetFilePane", () => { fireEvent.click(screen.getByRole("button", { name: "Create" })); await waitFor(() => - expect(mocks.create).toHaveBeenCalledWith(workspaceId, { + expect(mocks.create).toHaveBeenCalledWith(workspaceId, "main", { parentPath: "", name: "notes", kind: "directory", diff --git a/apps/web/components/gen2/superset-file-pane.tsx b/apps/web/components/gen2/superset-file-pane.tsx index 7b904c7ba..875a1afd3 100644 --- a/apps/web/components/gen2/superset-file-pane.tsx +++ b/apps/web/components/gen2/superset-file-pane.tsx @@ -36,6 +36,7 @@ import { deleteSupersetEntry, moveSupersetEntry, SupersetFileApiError, + DEFAULT_SUPERSET_WORKTREE_ID, } from "./superset-file-client"; import { useGen2SharedFileDocument } from "./use-gen2-shared-file-document"; @@ -105,9 +106,13 @@ function errorMessage(error: unknown, fallback: string) { export function SupersetFilePane({ workspaceId, canEdit, + worktreeId = DEFAULT_SUPERSET_WORKTREE_ID, + onDirtyChange, }: { workspaceId: string; canEdit: boolean; + worktreeId?: string; + onDirtyChange?: (dirty: boolean) => void; }) { const [files, setFiles] = useState([]); const [openFile, setOpenFile] = useState(null); @@ -148,6 +153,7 @@ export function SupersetFilePane({ const dirty = openFile !== null && contents !== openFile.contents; const sharedDocument = useGen2SharedFileDocument({ workspaceId, + worktreeId, path: openFile?.path ?? null, canEdit, onContentsChange: (next) => { @@ -168,6 +174,11 @@ export function SupersetFilePane({ return () => window.removeEventListener("beforeunload", warn); }, [dirty]); + useEffect(() => { + onDirtyChange?.(dirty); + return () => onDirtyChange?.(false); + }, [dirty, onDirtyChange]); + useEffect(() => { if (!deleteEntry) return; const closeOnEscape = (event: KeyboardEvent) => { @@ -182,7 +193,12 @@ export function SupersetFilePane({ const requestId = ++openRequestId.current; setOpeningPath(path); try { - const file = await readSupersetFile(workspaceId, path, signal); + const file = await readSupersetFile( + workspaceId, + worktreeId, + path, + signal, + ); if (signal?.aborted || requestId !== openRequestId.current) return; openFileRef.current = file; contentsRef.current = file.contents; @@ -211,7 +227,7 @@ export function SupersetFilePane({ } } }, - [workspaceId], + [workspaceId, worktreeId], ); const refreshFiles = useCallback( @@ -219,7 +235,11 @@ export function SupersetFilePane({ const requestId = ++listRequestId.current; if (!quiet) setLoadingFiles(true); try { - const nextFiles = await listSupersetFiles(workspaceId, signal); + const nextFiles = await listSupersetFiles( + workspaceId, + worktreeId, + signal, + ); if (signal?.aborted || requestId !== listRequestId.current) return; setFiles(nextFiles); if (selectFirst && !openFileRef.current && nextFiles.length > 0) { @@ -258,7 +278,7 @@ export function SupersetFilePane({ } } }, - [workspaceId, openPath], + [workspaceId, worktreeId, openPath], ); useEffect(() => { @@ -285,7 +305,12 @@ export function SupersetFilePane({ setSaving(true); setNotice(null); try { - const saved = await saveSupersetFile(workspaceId, file, draft); + const saved = await saveSupersetFile( + workspaceId, + worktreeId, + file, + draft, + ); const editedDuringSave = contentsRef.current !== draft; openFileRef.current = saved; setOpenFile(saved); @@ -337,7 +362,7 @@ export function SupersetFilePane({ setCreating(true); setNotice(null); try { - const entry = await createSupersetEntry(workspaceId, { + const entry = await createSupersetEntry(workspaceId, worktreeId, { parentPath: createParentPath, name, kind, @@ -401,7 +426,7 @@ export function SupersetFilePane({ setRenaming(true); setNotice(null); try { - const moved = await moveSupersetEntry(workspaceId, { + const moved = await moveSupersetEntry(workspaceId, worktreeId, { path: entry.path, parentPath: parentPath(entry.path), name, @@ -453,7 +478,7 @@ export function SupersetFilePane({ setDeleting(true); setNotice(null); try { - await deleteSupersetEntry(workspaceId, entry.path); + await deleteSupersetEntry(workspaceId, worktreeId, entry.path); const current = openFileRef.current; if (current && isPathOrDescendant(current.path, entry.path)) { openFileRef.current = null; diff --git a/apps/web/components/gen2/superset-workspace-shell.test.tsx b/apps/web/components/gen2/superset-workspace-shell.test.tsx new file mode 100644 index 000000000..976a02977 --- /dev/null +++ b/apps/web/components/gen2/superset-workspace-shell.test.tsx @@ -0,0 +1,116 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + listWorktrees: vi.fn(), + createWorktree: vi.fn(), +})); + +vi.mock("./superset-file-client", async (importOriginal) => ({ + ...(await importOriginal()), + listSupersetWorktrees: mocks.listWorktrees, + createSupersetWorktree: mocks.createWorktree, +})); + +vi.mock("./superset-file-pane", () => ({ + SupersetFilePane: ({ worktreeId }: { worktreeId?: string }) => ( +
+ ), +})); + +vi.mock("./terminal-pane", () => ({ + Gen2TerminalPane: ({ worktreeId }: { worktreeId?: string }) => ( +
+ ), +})); + +import { SupersetWorkspaceShell } from "./superset-workspace-shell"; + +const workspaceId = "e010bd2c-a3c1-438f-acef-166287a3b1cb"; + +describe("SupersetWorkspaceShell", () => { + beforeEach(() => { + mocks.listWorktrees.mockResolvedValue([ + { worktreeId: "main", branch: "main" }, + { worktreeId: "feature-auth", branch: "feature/auth" }, + ]); + mocks.createWorktree.mockResolvedValue({ + worktreeId: "fix-login", + branch: "fix/login", + }); + vi.stubGlobal( + "fetch", + vi + .fn() + .mockResolvedValue( + Response.json({ output: "## feature/auth\n M src/login.ts\n" }), + ), + ); + }); + + it("switches every Superset panel to the selected host worktree", async () => { + render( + , + ); + await waitFor(() => + expect( + screen.getByRole("option", { name: "feature/auth" }), + ).toBeInTheDocument(), + ); + + fireEvent.change(screen.getByLabelText("Selected branch worktree"), { + target: { value: "feature-auth" }, + }); + expect(screen.getByTestId("files")).toHaveAttribute( + "data-worktree-id", + "feature-auth", + ); + + fireEvent.click(screen.getByRole("tab", { name: "Changes" })); + expect(await screen.findByText("src/login.ts")).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Changes" })).toHaveAttribute( + "aria-selected", + "true", + ); + + fireEvent.click(screen.getByRole("tab", { name: "Terminal" })); + expect(screen.getByTestId("terminal")).toHaveAttribute( + "data-worktree-id", + "feature-auth", + ); + }); + + it("lets editors create and select a Superset-owned worktree", async () => { + render( + , + ); + await screen.findByRole("button", { name: "New branch" }); + fireEvent.click(screen.getByRole("button", { name: "New branch" })); + fireEvent.change(screen.getByLabelText("Worktree ID"), { + target: { value: "fix-login" }, + }); + fireEvent.change(screen.getByLabelText("Branch"), { + target: { value: "fix/login" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Create worktree" })); + + await waitFor(() => + expect(mocks.createWorktree).toHaveBeenCalledWith(workspaceId, { + worktreeId: "fix-login", + branch: "fix/login", + }), + ); + expect(screen.getByTestId("files")).toHaveAttribute( + "data-worktree-id", + "fix-login", + ); + }); +}); diff --git a/apps/web/components/gen2/superset-workspace-shell.tsx b/apps/web/components/gen2/superset-workspace-shell.tsx new file mode 100644 index 000000000..6719e50b1 --- /dev/null +++ b/apps/web/components/gen2/superset-workspace-shell.tsx @@ -0,0 +1,382 @@ +"use client"; + +import { type FormEvent, useCallback, useEffect, useState } from "react"; +import { + Files, + GitBranch, + Plus, + RefreshCw, + SquareTerminal, +} from "lucide-react"; +import { parseGitStatus } from "@/lib/runtime/ide"; + +import { Gen2TerminalPane } from "./terminal-pane"; +import { + createSupersetWorktree, + DEFAULT_SUPERSET_WORKTREE_ID, + listSupersetWorktrees, + SupersetFileApiError, +} from "./superset-file-client"; +import { SupersetFilePane } from "./superset-file-pane"; + +type Tab = "files" | "changes" | "terminal"; + +type Worktree = { worktreeId: string; branch: string }; + +function errorMessage(error: unknown, fallback: string) { + return error instanceof SupersetFileApiError ? error.message : fallback; +} + +function SupersetChangesPane({ + workspaceId, + worktreeId, + visible, +}: { + workspaceId: string; + worktreeId: string; + visible: boolean; +}) { + const [status, setStatus] = useState(""); + const [diff, setDiff] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const refresh = useCallback(async () => { + setLoading(true); + try { + const query = new URLSearchParams({ worktreeId }); + const base = `/api/gen2/workspaces/${encodeURIComponent(workspaceId)}/git`; + const [statusResponse, diffResponse] = await Promise.all([ + fetch(`${base}?${query}&operation=status`), + fetch(`${base}?${query}&operation=diff`), + ]); + const statusPayload = (await statusResponse.json().catch(() => ({}))) as { + output?: string; + error?: string; + }; + if (!statusResponse.ok) { + setError(statusPayload.error ?? "Couldn’t read the Git status."); + return; + } + const diffPayload = (await diffResponse.json().catch(() => ({}))) as { + output?: string; + }; + setStatus(statusPayload.output ?? ""); + setDiff(diffResponse.ok ? (diffPayload.output ?? "") : ""); + setError(""); + } catch { + setError("Couldn’t reach CoDev. Try refreshing Changes."); + } finally { + setLoading(false); + } + }, [workspaceId, worktreeId]); + + useEffect(() => { + // The asynchronous continuation owns state updates after the initial + // render; the rule cannot infer that boundary through refresh(). + // eslint-disable-next-line react-hooks/set-state-in-effect + if (visible) void refresh(); + }, [visible, refresh]); + + const files = [...parseGitStatus(status)].map(([path, code]) => ({ + path, + code, + })); + + return ( + + ); +} + +/** + * Browser adaptation of Superset's workspace shell. The host owns terminal, + * Git, and worktree operations; CoDev supplies member authorization and the + * worktree-scoped Yjs editor beneath it. + */ +export function SupersetWorkspaceShell({ + workspaceId, + canEdit, + runtimeEnabled, +}: { + workspaceId: string; + canEdit: boolean; + runtimeEnabled: boolean; +}) { + const [tab, setTab] = useState("files"); + const [worktrees, setWorktrees] = useState([ + { worktreeId: DEFAULT_SUPERSET_WORKTREE_ID, branch: "main" }, + ]); + const [worktreeId, setWorktreeId] = useState(DEFAULT_SUPERSET_WORKTREE_ID); + const [dirty, setDirty] = useState(false); + const [notice, setNotice] = useState(""); + const [creating, setCreating] = useState(false); + const [showCreate, setShowCreate] = useState(false); + const [newWorktreeId, setNewWorktreeId] = useState(""); + const [newBranch, setNewBranch] = useState(""); + const [baseRef, setBaseRef] = useState(""); + + const refreshWorktrees = useCallback(async () => { + if (!runtimeEnabled) return; + try { + const next = await listSupersetWorktrees(workspaceId); + if (next.length) setWorktrees(next); + setNotice(""); + } catch (error) { + setNotice(errorMessage(error, "Couldn’t load worktrees.")); + } + }, [runtimeEnabled, workspaceId]); + + useEffect(() => { + // The request settles after mount and updates only its own worktree list. + // eslint-disable-next-line react-hooks/set-state-in-effect + void refreshWorktrees(); + }, [refreshWorktrees]); + + function selectWorktree(next: string) { + if (next === worktreeId) return true; + if ( + dirty && + !window.confirm("Discard unsaved changes and switch branches?") + ) + return false; + setWorktreeId(next); + setDirty(false); + setNotice(""); + return true; + } + + async function createWorktree(event: FormEvent) { + event.preventDefault(); + if (!newWorktreeId.trim() || !newBranch.trim() || creating) return; + setCreating(true); + try { + const created = await createSupersetWorktree(workspaceId, { + worktreeId: newWorktreeId.trim(), + branch: newBranch.trim(), + ...(baseRef.trim() ? { baseRef: baseRef.trim() } : {}), + }); + setWorktrees((current) => [...current, created]); + setShowCreate(false); + setNewWorktreeId(""); + setNewBranch(""); + setBaseRef(""); + const selected = selectWorktree(created.worktreeId); + setNotice( + selected + ? `Created and selected ${created.branch}.` + : `Created ${created.branch}. Your current unsaved changes were preserved.`, + ); + } catch (error) { + setNotice(errorMessage(error, "Couldn’t create this worktree.")); + } finally { + setCreating(false); + } + } + + if (!runtimeEnabled) { + return ( + <> +

+ Superset terminal, Changes, and branch worktrees are disabled for this + environment. +

+ + + ); + } + + return ( +
+
+ + {canEdit ? ( + + ) : null} +
+ + + +
+
+ {showCreate ? ( +
void createWorktree(event)} + > + + + + +
+ ) : null} + {notice ? ( +

+ {notice} +

+ ) : null} + + + +
+ ); +} diff --git a/apps/web/components/gen2/terminal-pane.tsx b/apps/web/components/gen2/terminal-pane.tsx index 1a5ec740c..830494e13 100644 --- a/apps/web/components/gen2/terminal-pane.tsx +++ b/apps/web/components/gen2/terminal-pane.tsx @@ -14,16 +14,18 @@ import type { FitAddon } from "@xterm/addon-fit"; */ export function Gen2TerminalPane({ workspaceId, + worktreeId = "main", visible, canStart, onExit, onResumeWorkspace, }: { workspaceId: string; + worktreeId?: string; visible: boolean; canStart: boolean; onExit: () => void; - onResumeWorkspace: () => Promise; + onResumeWorkspace?: () => Promise; }) { const hostRef = useRef(null); const termRef = useRef(null); @@ -44,13 +46,13 @@ export function Gen2TerminalPane({ }, []); const post = useCallback( - (body: unknown) => + (body: Record) => fetch(`/api/gen2/workspaces/${workspaceId}/terminal`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), + body: JSON.stringify({ ...body, worktreeId }), }), - [workspaceId], + [workspaceId, worktreeId], ); const start = useCallback(async () => { @@ -221,13 +223,13 @@ export function Gen2TerminalPane({ const sessionId = sessionRef.current; if (sessionId) { void fetch( - `/api/gen2/workspaces/${workspaceId}/terminal?sessionId=${sessionId}`, + `/api/gen2/workspaces/${workspaceId}/terminal?sessionId=${sessionId}&worktreeId=${encodeURIComponent(worktreeId)}`, { method: "DELETE", keepalive: true }, ); } termRef.current?.dispose(); }, - [workspaceId], + [workspaceId, worktreeId], ); return ( @@ -237,13 +239,15 @@ export function Gen2TerminalPane({

{error} Resume the workspace to reconnect.

- + {onResumeWorkspace ? ( + + ) : null}
) : status === "idle" || status === "ended" ? (
diff --git a/apps/web/components/gen2/turn-activity.tsx b/apps/web/components/gen2/turn-activity.tsx index 53d03dc2b..740b385ad 100644 --- a/apps/web/components/gen2/turn-activity.tsx +++ b/apps/web/components/gen2/turn-activity.tsx @@ -39,44 +39,30 @@ export function Gen2TurnActivity({ ); const itemRunning = visible.some((item) => item.status === "running"); const active = live || itemRunning; - const [open, setOpen] = useState(active); + const [openOverride, setOpenOverride] = useState<{ + active: boolean; + value: boolean; + } | null>(null); + const open = openOverride?.active === active ? openOverride.value : active; const [elapsed, setElapsed] = useState(0); const startedAt = useRef(null); useEffect(() => { if (visible.length === 0) { startedAt.current = null; - setElapsed(0); return; } startedAt.current ??= Date.now(); - if (!active) { + if (!active) return; + const id = window.setInterval(() => { + if (startedAt.current === null) return; setElapsed( Math.max(0, Math.round((Date.now() - startedAt.current) / 1000)), ); - // Done turns collapse to the Cursor-style summary line. - setOpen(false); - return; - } - setOpen(true); - const tick = () => { - if (startedAt.current == null) return; - setElapsed( - Math.max(0, Math.round((Date.now() - startedAt.current) / 1000)), - ); - }; - tick(); - const id = window.setInterval(tick, 1000); + }, 1000); return () => window.clearInterval(id); }, [visible.length, active]); - useEffect(() => { - if (visible.length === 0) { - setOpen(false); - startedAt.current = null; - } - }, [visible.length]); - if (visible.length === 0) return null; const summary = active @@ -91,7 +77,7 @@ export function Gen2TurnActivity({ type="button" className="gen2-steps-summary" aria-expanded={open} - onClick={() => setOpen((value) => !value)} + onClick={() => setOpenOverride({ active, value: !open })} > {summary} void; @@ -88,6 +89,7 @@ export function useGen2SharedFileDocument(input: { const nextText = doc.getText("content"); const nextAwareness = new Awareness(doc); const path = input.path; + const worktreeId = input.worktreeId; syncedRef.current = false; setText(nextText); setAwareness(nextAwareness); @@ -132,7 +134,7 @@ export function useGen2SharedFileDocument(input: { setState(syncedRef.current ? "syncing" : "connecting"); const socket = new WebSocket(socketUrl(input.workspaceId)); socketRef.current = socket; - socket.onopen = () => send({ type: "join" }); + socket.onopen = () => send({ type: "join", worktreeId }); socket.onmessage = (event) => { if (typeof event.data !== "string") return; let payload: unknown; @@ -152,22 +154,43 @@ export function useGen2SharedFileDocument(input: { syncedRef.current = true; setState("connected"); setNotice(null); - } else if (message.type === "update" && message.path === path) { + } else if ( + message.type === "update" && + message.worktreeId === worktreeId && + message.path === path + ) { Y.applyUpdate(doc, decodeBase64(message.update), REMOTE_ORIGIN); - } else if (message.type === "awareness" && message.path === path) { + } else if ( + message.type === "awareness" && + message.worktreeId === worktreeId && + message.path === path + ) { applyAwarenessUpdate( nextAwareness, decodeBase64(message.update), REMOTE_ORIGIN, ); } else if (message.type === "presence") { - setMembers(message.members.filter((member) => member.path === path)); - } else if (message.type === "reconciled" && message.path === path) { + setMembers( + message.members.filter( + (member) => + member.worktreeId === worktreeId && member.path === path, + ), + ); + } else if ( + message.type === "reconciled" && + message.worktreeId === worktreeId && + message.path === path + ) { if (message.update) { Y.applyUpdate(doc, decodeBase64(message.update), REMOTE_ORIGIN); } setNotice("An agent updated this file from the workspace."); - } else if (message.type === "conflict" && message.path === path) { + } else if ( + message.type === "conflict" && + message.worktreeId === worktreeId && + message.path === path + ) { setState("conflict"); setNotice(message.message); } else if ( @@ -200,7 +223,7 @@ export function useGen2SharedFileDocument(input: { doc.destroy(); }; /* eslint-enable react-hooks/set-state-in-effect */ - }, [input.workspaceId, input.path]); + }, [input.workspaceId, input.worktreeId, input.path]); const updateCursor = useCallback( (cursor: { anchor: number; head: number } | null) => { diff --git a/apps/web/lib/gen2/collaboration-documents.ts b/apps/web/lib/gen2/collaboration-documents.ts index 2dbad4c43..b546dfe29 100644 --- a/apps/web/lib/gen2/collaboration-documents.ts +++ b/apps/web/lib/gen2/collaboration-documents.ts @@ -15,6 +15,7 @@ import { readGen2SupersetFile } from "./superset"; export interface Gen2DocumentSnapshot { workspaceId: string; + worktreeId: string; path: string; revision: string; update: string; @@ -27,11 +28,13 @@ export interface Gen2DocumentSnapshot { export async function loadGen2Document( workspaceId: string, + worktreeId: string, path: string, ): Promise { const [snapshot] = await getDatabase() .select({ workspaceId: schema.gen2YjsDocuments.workspaceId, + worktreeId: schema.gen2YjsDocuments.worktreeId, path: schema.gen2YjsDocuments.path, revision: schema.gen2YjsDocuments.revision, update: schema.gen2YjsDocuments.update, @@ -46,6 +49,7 @@ export async function loadGen2Document( .where( and( eq(schema.gen2YjsDocuments.workspaceId, workspaceId), + eq(schema.gen2YjsDocuments.worktreeId, worktreeId), eq(schema.gen2YjsDocuments.path, path), ), ) @@ -71,6 +75,7 @@ export async function saveGen2Document( .onConflictDoUpdate({ target: [ schema.gen2YjsDocuments.workspaceId, + schema.gen2YjsDocuments.worktreeId, schema.gen2YjsDocuments.path, ], set: { @@ -91,13 +96,20 @@ export async function saveGen2Document( export async function initializeGen2Document( workspaceId: string, userId: string, + worktreeId: string, path: string, ) { - const file = await readGen2SupersetFile(workspaceId, userId, "main", path); + const file = await readGen2SupersetFile( + workspaceId, + userId, + worktreeId, + path, + ); const doc = new Y.Doc(); doc.getText("content").insert(0, file.contents); const snapshot: Gen2DocumentSnapshot = { workspaceId, + worktreeId, path, revision: file.revision, ...encodedDocument(doc), @@ -113,11 +125,16 @@ export async function initializeGen2Document( /** Records a completed revision-checked filesystem save without replacing a newer Yjs edit. */ export async function recordGen2DocumentSave(input: { workspaceId: string; + worktreeId: string; path: string; contents: string; revision: string; }) { - const snapshot = await loadGen2Document(input.workspaceId, input.path); + const snapshot = await loadGen2Document( + input.workspaceId, + input.worktreeId, + input.path, + ); if (!snapshot) return; await saveGen2Document({ ...snapshot, @@ -141,7 +158,7 @@ export async function reconcileGen2Document( const file = await readGen2SupersetFile( workspaceId, userId, - "main", + snapshot.worktreeId, snapshot.path, ); const previousRevision = snapshot.filesystemRevision ?? snapshot.revision; diff --git a/apps/web/lib/gen2/collaboration-events.test.ts b/apps/web/lib/gen2/collaboration-events.test.ts new file mode 100644 index 000000000..4920ecbe4 --- /dev/null +++ b/apps/web/lib/gen2/collaboration-events.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + load: vi.fn(), + reconcile: vi.fn(), + publish: vi.fn(), +})); + +vi.mock("./collaboration-documents", () => ({ + loadGen2Document: mocks.load, + reconcileGen2Document: mocks.reconcile, +})); + +vi.mock("../workspaces/collaboration-rooms", () => ({ + publish: mocks.publish, +})); + +import { reconcileGen2CollaborationPaths } from "./collaboration-events"; + +const workspaceId = "e010bd2c-a3c1-438f-acef-166287a3b1cb"; +const userId = "c010bd2c-a3c1-438f-acef-166287a3b1cb"; + +describe("reconcileGen2CollaborationPaths", () => { + it("keeps agent reconciliation within the reported Superset worktree", async () => { + const snapshot = { + workspaceId, + worktreeId: "feature-auth", + path: "src/login.ts", + }; + mocks.load.mockResolvedValue(snapshot); + mocks.reconcile.mockResolvedValue({ + snapshot, + event: { + type: "reconciled", + path: snapshot.path, + revision: "rev-2", + update: "AQID", + }, + }); + + await reconcileGen2CollaborationPaths({ + workspaceId, + userId, + worktreeId: "feature-auth", + paths: [snapshot.path], + }); + + expect(mocks.load).toHaveBeenCalledWith( + workspaceId, + "feature-auth", + snapshot.path, + ); + expect(mocks.publish).toHaveBeenCalledWith(`gen2:${workspaceId}`, { + type: "reconciled", + worktreeId: "feature-auth", + path: snapshot.path, + revision: "rev-2", + source: "filesystem", + update: "AQID", + }); + }); +}); diff --git a/apps/web/lib/gen2/collaboration-events.ts b/apps/web/lib/gen2/collaboration-events.ts index 0e691f87f..659ba545c 100644 --- a/apps/web/lib/gen2/collaboration-events.ts +++ b/apps/web/lib/gen2/collaboration-events.ts @@ -6,7 +6,7 @@ import { reconcileGen2Document, } from "./collaboration-documents"; -/** Redis room namespace; unlike Gen 1 it never identifies a worktree. */ +/** One workspace room carries events for all worktrees; fan-out filters them. */ export function gen2CollaborationRoom(workspaceId: string) { return `gen2:${workspaceId}`; } @@ -19,10 +19,16 @@ export function gen2CollaborationRoom(workspaceId: string) { export async function reconcileGen2CollaborationPaths(input: { workspaceId: string; userId: string; + worktreeId?: string; paths: string[]; }) { + const worktreeId = input.worktreeId ?? "main"; for (const path of [...new Set(input.paths)]) { - const snapshot = await loadGen2Document(input.workspaceId, path); + const snapshot = await loadGen2Document( + input.workspaceId, + worktreeId, + path, + ); if (!snapshot) continue; const result = await reconcileGen2Document( input.workspaceId, @@ -33,10 +39,7 @@ export async function reconcileGen2CollaborationPaths(input: { if (result.event.type === "reconciled") { await publish(gen2CollaborationRoom(input.workspaceId), { type: "reconciled", - // The shared collaboration wire format predates Gen 2. This carries - // the workspace ID solely for per-document event filtering; it is not - // looked up as a Gen 1 worktree. - worktreeId: input.workspaceId, + worktreeId, path: result.event.path, revision: result.event.revision, source: "filesystem", @@ -45,7 +48,7 @@ export async function reconcileGen2CollaborationPaths(input: { } else { await publish(gen2CollaborationRoom(input.workspaceId), { type: "conflict", - worktreeId: input.workspaceId, + worktreeId, path: result.event.path, snapshotRevision: result.event.snapshotRevision, filesystemRevision: result.event.filesystemRevision, diff --git a/apps/web/lib/gen2/collaboration-socket.ts b/apps/web/lib/gen2/collaboration-socket.ts index 18e458c1f..02938f20e 100644 --- a/apps/web/lib/gen2/collaboration-socket.ts +++ b/apps/web/lib/gen2/collaboration-socket.ts @@ -21,6 +21,7 @@ import { encodeBase64, encodedDocument, } from "../collaboration/yjs-document"; +import { logEvent } from "../platform/observability"; import { send, sendError, @@ -57,7 +58,6 @@ import { gen2CollaborationRoom } from "./collaboration-events"; export const gen2CollaborationSocketMaxPayload = MAX_SOCKET_PAYLOAD_BYTES; function roomConnection( - workspaceId: string, socket: WebSocket, user: CollaborationUser, canEdit: boolean, @@ -67,10 +67,9 @@ function roomConnection( socket, user, joined: false, - // Compatibility envelope only: this is never read from the Gen 1 - // worktrees table. It lets the existing room fan-out filter a Gen 2 - // workspace's document updates without duplicating transport machinery. - worktreeId: workspaceId, + // Superset's host worktree ID is selected at join time. The common room + // transport uses it solely to filter per-document fan-out. + worktreeId: null, subscriptions: new Set(), activePath: null, cursor: null, @@ -87,15 +86,24 @@ async function subscribe( path: string, stateVector?: string, ) { + if (!connection.worktreeId) { + sendError(connection, "not_joined", "Join a worktree first.", false, path); + return; + } const roomKey = gen2CollaborationRoom(workspaceId); const result = await withDocumentLock( roomKey, - workspaceId, + connection.worktreeId, path, async () => { const snapshot = - (await loadGen2Document(workspaceId, path)) ?? - (await initializeGen2Document(workspaceId, connection.user.id, path)); + (await loadGen2Document(workspaceId, connection.worktreeId!, path)) ?? + (await initializeGen2Document( + workspaceId, + connection.user.id, + connection.worktreeId!, + path, + )); return reconcileGen2Document(workspaceId, connection.user.id, snapshot); }, ); @@ -104,7 +112,7 @@ async function subscribe( if (result.event?.type === "reconciled") { await publish(roomKey, { type: "reconciled", - worktreeId: workspaceId, + worktreeId: connection.worktreeId, path, revision: result.event.revision, source: "filesystem", @@ -113,7 +121,7 @@ async function subscribe( } else if (result.event?.type === "conflict") { await publish(roomKey, { type: "conflict", - worktreeId: workspaceId, + worktreeId: connection.worktreeId, path, snapshotRevision: result.event.snapshotRevision, filesystemRevision: result.event.filesystemRevision, @@ -145,13 +153,21 @@ async function applyUpdate( path: string, update: string, ) { + if (!connection.worktreeId) { + sendError(connection, "not_joined", "Join a worktree first.", false, path); + return; + } const roomKey = gen2CollaborationRoom(workspaceId); const outcome = await withDocumentLock( roomKey, - workspaceId, + connection.worktreeId, path, async () => { - const loaded = await loadGen2Document(workspaceId, path); + const loaded = await loadGen2Document( + workspaceId, + connection.worktreeId!, + path, + ); if (!loaded) throw new Error("The collaborative document was not found."); const reconciled = await reconcileGen2Document( workspaceId, @@ -177,7 +193,7 @@ async function applyUpdate( if (outcome.conflict) { await publish(roomKey, { type: "conflict", - worktreeId: workspaceId, + worktreeId: connection.worktreeId, path, snapshotRevision: outcome.conflict.snapshotRevision, filesystemRevision: outcome.conflict.filesystemRevision, @@ -189,7 +205,7 @@ async function applyUpdate( if (outcome.reconciled) { await publish(roomKey, { type: "reconciled", - worktreeId: workspaceId, + worktreeId: connection.worktreeId, path, revision: outcome.reconciled.revision, source: "filesystem", @@ -207,11 +223,12 @@ async function applyUpdate( "payload", JSON.stringify({ type: "update", - worktreeId: workspaceId, + worktreeId: connection.worktreeId, path, update, revision: - (await loadGen2Document(workspaceId, path))?.revision ?? "pending", + (await loadGen2Document(workspaceId, connection.worktreeId!, path)) + ?.revision ?? "pending", actorId: connection.user.id, streamId: "pending", }), @@ -220,11 +237,12 @@ async function applyUpdate( roomKey, { type: "update", - worktreeId: workspaceId, + worktreeId: connection.worktreeId, path, update, revision: - (await loadGen2Document(workspaceId, path))?.revision ?? "pending", + (await loadGen2Document(workspaceId, connection.worktreeId!, path)) + ?.revision ?? "pending", actorId: connection.user.id, streamId: streamId ?? "0-0", }, @@ -274,7 +292,7 @@ async function publishAwareness( path: string, update: string, ) { - if (!connection.subscriptions.has(path)) { + if (!connection.worktreeId || !connection.subscriptions.has(path)) { sendError( connection, "not_subscribed", @@ -298,7 +316,7 @@ async function publishAwareness( "payload", JSON.stringify({ type: "awareness", - worktreeId: workspaceId, + worktreeId: connection.worktreeId, path, update: sanitized.update, actorId: connection.user.id, @@ -310,7 +328,7 @@ async function publishAwareness( roomKey, { type: "awareness", - worktreeId: workspaceId, + worktreeId: connection.worktreeId, path, update: sanitized.update, actorId: connection.user.id, @@ -359,6 +377,7 @@ async function handleMessage( } try { if (message.type === "join") { + connection.worktreeId = message.worktreeId ?? "main"; connection.joined = true; connection.resumeFrom = message.resumeFrom ?? null; const roomKey = gen2CollaborationRoom(workspaceId); @@ -423,7 +442,14 @@ async function handleMessage( connection.alive = true; await refreshPresence(gen2CollaborationRoom(workspaceId), connection); } - } catch { + } catch (error) { + logEvent("error", "gen2.collaboration.socket_operation_failed", { + workspaceId, + worktreeId: connection.worktreeId, + operation: message.type, + path: "path" in message ? message.path : null, + detail: error instanceof Error ? error.message : "unknown", + }); sendError( connection, "internal_error", @@ -442,7 +468,7 @@ export async function handleGen2CollaborationSocket( ) { const roomKey = gen2CollaborationRoom(workspaceId); const room = await startRoom(roomKey); - const connection = roomConnection(workspaceId, socket, user, options.canEdit); + const connection = roomConnection(socket, user, options.canEdit); room.connections.add(connection); const heartbeat = setInterval(() => { if (!connection.alive) return socket.terminate(); diff --git a/apps/web/lib/gen2/superset.ts b/apps/web/lib/gen2/superset.ts index a1d1fbaf6..7d7613fe4 100644 --- a/apps/web/lib/gen2/superset.ts +++ b/apps/web/lib/gen2/superset.ts @@ -214,6 +214,7 @@ export async function saveGen2SupersetFile( // successful revision-checked filesystem save as failed. await recordGen2DocumentSave({ workspaceId, + worktreeId: input.worktreeId, path: file.path, contents: file.contents, revision: file.revision, diff --git a/apps/web/lib/workspaces/collaboration-presence.ts b/apps/web/lib/workspaces/collaboration-presence.ts index d763ad2d6..21cb28efb 100644 --- a/apps/web/lib/workspaces/collaboration-presence.ts +++ b/apps/web/lib/workspaces/collaboration-presence.ts @@ -161,6 +161,7 @@ export async function refreshPresence( user: connection.user, path: connection.activePath, cursor: connection.cursor, + worktreeId: connection.worktreeId, lastSeenAt: new Date().toISOString(), }; const now = Date.now(); diff --git a/apps/web/lib/workspaces/collaboration-socket.ts b/apps/web/lib/workspaces/collaboration-socket.ts index b8aa001a8..feb78c263 100644 --- a/apps/web/lib/workspaces/collaboration-socket.ts +++ b/apps/web/lib/workspaces/collaboration-socket.ts @@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto"; import { collaborationClientMessageSchema, + identifierSchema, presenceCursorSchema, type CollaborationUser, } from "@codev/contracts"; @@ -405,6 +406,13 @@ async function handleMessage( try { if (message.type === "join") { + if ( + message.worktreeId && + !identifierSchema.safeParse(message.worktreeId).success + ) { + sendError(connection, "not_found", "Active worktree not found.", false); + return; + } const worktreeId = await resolveWorktree(workspaceId, message.worktreeId); if (!worktreeId) { sendError(connection, "not_found", "Active worktree not found.", false); diff --git a/apps/web/package.json b/apps/web/package.json index 694ea1b54..3a1c62f76 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -48,7 +48,7 @@ "@upstash/redis": "^1.38.0", "@vercel/analytics": "^2.0.1", "@vercel/functions": "3.7.6", - "@vercel/oidc": "^3.8.7", + "@vercel/oidc": "^3.8.9", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "ai": "7.0.44", diff --git a/docs/README.md b/docs/README.md index 87e61d14c..b0260652c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,6 +28,7 @@ kept for the record only. | [WORKSPACE_CONSOLIDATION.md](./WORKSPACE_CONSOLIDATION.md) | Current | Sandbox/IDE consolidation; what shipped and what is still gated | | [SUPERSET_WORKSPACE_OWNERSHIP.md](./SUPERSET_WORKSPACE_OWNERSHIP.md) | Design | Phase 1 ownership contract for a first-party Superset-powered workspace | | [SUPERSET_ADOPTION_MANIFEST.md](./SUPERSET_ADOPTION_MANIFEST.md) | Design | Pinned Superset source, build boundary, and Phase 1 runtime/UI map | +| [SUPERSET_AGENT_SESSION_PLAN.md](./SUPERSET_AGENT_SESSION_PLAN.md) | Design | Superset agent sessions, CoDev provider ownership, and rollout gates | | [workspace-startup-performance-plan.md](./workspace-startup-performance-plan.md) | Active | Faster workspace startup, host pooling, persistence, and headless IDE | | [branch-workspaces-plan.md](./branch-workspaces-plan.md) | Current | Branches-first collaboration plan, phases 1 to 5 | | [branch-workspaces-phase-5-verification.md](./branch-workspaces-phase-5-verification.md) | Current | Verification record for the branch-workspaces rollout | diff --git a/docs/SUPERSET_AGENT_SESSION_PLAN.md b/docs/SUPERSET_AGENT_SESSION_PLAN.md new file mode 100644 index 000000000..059922a8f --- /dev/null +++ b/docs/SUPERSET_AGENT_SESSION_PLAN.md @@ -0,0 +1,229 @@ +# Superset agent-session integration plan + +**Status:** Design; begin only after the Superset terminal, Git, and worktree +bridge passes a real Gen 2 workspace smoke test. The `/superset` page—not the +legacy Gen 2 workbench—is that run's acceptance surface. +**Date:** 2026-09-27 + +## Objective + +Replace the Gen 2 fresh-turn runtime (`codex exec` through `codev-guestd`) with +Superset terminal-agent sessions. CoDev continues to own identity, workspace +membership, provider connections, quotas, durable conversation history, and +recovery decisions. Superset owns the process and terminal lifecycle in the +selected worktree. + +The initial provider is Codex. The launch contract must be provider-neutral so +Claude and later providers can use it without adding provider-specific fields to +the general session or worktree model. + +## Required boundary + +```text +Browser + -> CoDev agent route: membership, role, connection, quota, audit + -> CoDev database: durable run and worktree mapping + -> orchestrator -> codev-guestd + -> one private profile for this launch + -> Superset host terminal-agent session in one worktree + -> process output and terminal lifecycle events + <- profile refresh / final state + <- CoDev persists transcript, metering, and refreshed connection state +``` + +The browser sees CoDev session identifiers and redacted progress only. It never +sees host bridge credentials, a provider auth cache, profile paths, or a +Superset administrative endpoint. + +## Existing pieces to retain + +- `apps/web/lib/gen2/agent.ts` already checks membership and instance state, + resolves a Codex connection, claims a hosted-subscription execution, writes + chat/turn records, and writes back a refreshed auth cache. +- `apps/web/lib/agents/cli-agent-session.ts` already has the durable CoDev + `agent_sessions` and worktree registration pattern for CLI agents. +- `vendor/superset/packages/host-service/src/terminal-agents/` owns Superset's + terminal-agent launch, monitoring, interruption, and host-side recovery. +- The flag-gated Superset runtime bridge already provides worktree selection + and a private CoDev-to-host request path. + +Do not keep a second agent process implementation in `codev-guestd`. Once the +new path is enabled, guestd validates fixed requests, manages the private +profile boundary, and forwards lifecycle operations to Superset. + +## Phase 0: prove the prerequisite runtime + +Before adding agent code, run the current terminal/Git/worktree smoke test in a +real Gen 2 VM with `CODEV_SUPERSET_RUNTIME_ENABLED=true`. + +- Create an isolated worktree, open a shell in it, and verify status and diff. +- Confirm the shell uid is `codev-shell` and cannot read a running Codex + credential profile. +- Confirm a viewer cannot create a worktree or bypass the route. +- Restart the host service and confirm terminal/worktree state has the intended + recovery outcome. + +Record the result in the adoption manifest. A failed prerequisite is a runtime +or image issue to fix before agent migration. + +## Phase 1: define durable run identity + +Use the existing CoDev `agent_sessions` row as the user-visible session. Add a +small Gen 2 Superset-run mapping rather than making Superset's SQLite database +the source of truth. + +The mapping needs: + +- CoDev workspace ID, CoDev agent-session ID, selected CoDev worktree ID, and + requesting member ID. +- Superset host workspace ID, terminal ID, and terminal-agent session ID. +- Provider name, CoDev connection/credential ID, and a credential revision or + fingerprint. Store no credential material in PostgreSQL mapping columns. +- Lifecycle state: `creating`, `running`, `stopping`, `finished`, `failed`, or + `recovery_required`; timestamps; exit reason; and a bounded recovery count. +- The hosted-subscription claim/lease state so every terminal exit, cancel, + failed start, and recovery path can release it exactly once. + +Create a migration, Zod contract, repository functions, and audit events in +the normal `apps/web/lib/agents` and `packages/contracts` patterns. Use the +CoDev agent-session UUID for idempotency; retries must return the same active +run instead of starting another provider process. + +## Phase 2: create a per-launch credential profile + +CoDev resolves the requesting member's eligible provider connection and claims +its subscription lease before it asks the runtime to start an agent. + +`codev-guestd` materializes one private profile for that run on the guest. It +contains only the provider files and environment required by that agent. The +profile must: + +- be outside `/workspace` and outside Superset's shared home; +- have a random run-scoped directory, restrictive ownership and permissions, + and no path exposed to the browser; +- be readable only by the agent process identity and the trusted guest/host + launch path, never by ordinary terminal shells or other agent sessions; +- be removed after final credential capture, cancellation, failed launch, VM + cleanup, or expired recovery window. + +The guest should pass Superset a profile handle and approved launch environment, +not a raw credential payload in a general host request. Superset launches the +provider process with only that profile. Disable Superset default accounts and +all host-wide provider environment for these sessions. + +Start with the existing Codex official auth-cache format. Define an internal +provider launch recipe interface containing the provider kind, executable, +arguments, sanitized environment names, profile handle, and refresh collector. +The recipe contains no secret in CoDev's durable run mapping or browser API. + +## Phase 3: add fixed private Superset agent operations + +Extend `vendor/superset/packages/host-service/src/codev/` with a narrow, +bridge-secret-protected agent endpoint set: + +- `POST /codev/agents` starts an agent in a validated worktree from a private + profile handle and returns host terminal/agent IDs. +- `POST /codev/agents/:id/input` sends approved interactive input when a + provider flow needs it. +- `POST /codev/agents/:id/poll` returns bounded output, lifecycle state, and + a refresh-ready signal. It never returns a profile file or secret. +- `DELETE /codev/agents/:id` stops the process and reports a terminal state. +- `GET /codev/agents/:id/recovery` reports whether the host can adopt an + existing process after a service restart. + +Use Superset terminal-agent APIs and terminal lifecycle functions internally. +Validate that every host session belongs to the requested CoDev worktree. +Do not expose generic command execution, arbitrary environment variables, or +arbitrary host URLs. + +Add corresponding fixed routes in guestd, the orchestrator, and a server-only +CoDev runtime adapter. Extend the existing runtime feature flag with a separate +`CODEV_SUPERSET_AGENT_SESSIONS_ENABLED` flag so terminal/Git adoption can stay +independent during rollout. + +## Phase 4: replace the Gen 2 agent route behind the flag + +Keep the current `startGen2AgentTurn`, `pollGen2AgentTurn`, and +`cancelGen2AgentTurn` browser contract initially. Behind the new flag, change +their implementation flow to: + +1. Check current membership, workspace state, chat access, agent-launch role, + connection eligibility, and capacity before any runtime request. +2. Select or create an isolated agent worktree. Concurrent independent agents + receive distinct worktrees and CoDev path claims. +3. Resolve the provider connection, claim its lease, create the durable mapping + in `creating`, and materialize the private profile. +4. Start the Superset agent. Atomically store host IDs and move the mapping to + `running`; append the user message and create the CoDev turn record. +5. Poll Superset through CoDev, reduce output into the existing durable turn + records, and publish only CoDev-safe chunks to the browser. +6. On final exit, collect a refreshed provider cache through the trusted path, + persist it with the existing encrypted connection store, release the lease, + remove the profile, and mark the run terminal. + +If startup fails after a lease claim, cleanup is mandatory: stop any host +process, remove the profile, release the claim, and record a failed run. Make +each cleanup step safe to repeat. + +## Phase 5: refresh, cancellation, and recovery + +Provider refreshes are captured only after the agent process has ended or at a +defined safe checkpoint. The guest reads the designated profile artifact, +returns it through the authenticated internal path, and CoDev validates and +encrypts it with the existing connection credential store. Never stream this +material to the browser, logs, chat transcript, or audit events. + +Cancellation and role/connection revocation must stop the host process, remove +the profile, release the lease, and transition the durable run once. A member +who loses access can no longer poll, send input, or cancel another member's +session through a bypassed UI. + +On host restart or VM restore, CoDev reconciles each nonterminal mapping with +Superset's recovery endpoint: + +- Adopt a verified live host session with matching workspace, worktree, and + host IDs. +- Mark a missing or ambiguous session `recovery_required`; do not silently + relaunch it with a potentially stale credential profile. +- Let the member explicitly resume as a new run after CoDev resolves a fresh + connection and creates a new private profile. + +## Phase 6: browser integration + +Adapt the Superset agent/session panel only after the host and CoDev paths are +working. The browser facade calls CoDev APIs and renders CoDev session IDs, +worktree/branch, redacted state, output, cancel, and recovery-required state. +It must not import Superset desktop authentication, local settings, or cloud +API clients. + +Show worktree ownership and path claims clearly: agents on separate worktrees +can run concurrently; a shared human worktree requires an explicit exclusive +claim before an agent can write there. + +## Verification gates + +- Unit: role and connection denial precede all runtime calls; idempotent starts; + lease release on every failure and terminal state; profile metadata never + contains secrets. +- Host/guest: fixed routes reject malformed IDs, mismatched worktrees, raw + profile contents, arbitrary environment, and arbitrary commands. +- Integration: two editors launch two Codex agents in separate worktrees; both + outputs, branch status, and file reconciliation appear in the shared page. +- Isolation: an ordinary shell, a second agent, a viewer, and the browser each + fail to read the first agent's profile or refreshed cache. +- Lifecycle: cancellation, provider revocation, host-service restart, and VM + restoration each leave exactly one durable CoDev state and no held lease or + leftover profile. +- Provider refresh: a controlled refresh writes back to the requesting + member's connection only, then the profile is removed. + +## Rollout and removal + +Enable the agent-session flag only for an internal workspace after every gate +passes. Instrument starts, profile cleanup, lease duration, host adoption, +refresh success, and recovery-required outcomes using metadata only. Keep the +current `codex exec` route as a rollback path during this internal rollout. + +Remove that fallback only after the Superset path has sustained real shared +workspace use, recovery has been exercised, and the ownership docs are updated +from Design to Current. diff --git a/docs/SUPERSET_WORKSPACE_OWNERSHIP.md b/docs/SUPERSET_WORKSPACE_OWNERSHIP.md index 108576595..da1cef572 100644 --- a/docs/SUPERSET_WORKSPACE_OWNERSHIP.md +++ b/docs/SUPERSET_WORKSPACE_OWNERSHIP.md @@ -130,14 +130,22 @@ host-wide managed provider environment for multi-member agent sessions. ### Current bridge boundary `CODEV_SUPERSET_RUNTIME_ENABLED` is deliberately off by default. When enabled -for an internal workspace, the existing authenticated terminal and Git routes -call the private Superset runtime bridge for the selected worktree; the -`/superset/worktrees` route lists and creates host-owned checkouts. Members can -read Git and use terminals, while only editors and owners can create a -worktree. The host service validates its bridge credential and terminal -worktree ownership; it starts CoDev shells via `codev-shell` without a -host-wide Superset provider account. Do not enable the flag in production until -the guest image has passed an end-to-end terminal, Git, and worktree smoke test. +for an internal workspace, the authenticated terminal and Git routes call the +private Superset runtime bridge for the selected worktree; the +`/superset/worktrees` route lists and creates host-owned checkouts. The +`/superset` browser page is the acceptance surface: its branch selector drives +the Superset file pane, Changes diff, and terminal together. Members can read +Git and use terminals, while only editors and owners can create a worktree. + +The Gen 2 Yjs snapshot identity, Redis document lock, collaboration event, and +presence entry are all scoped by **workspace ID + Superset worktree ID + path**. +This mirrors the Gen 1 `yjs_snapshots` invariant without reusing Gen 1's +worktree table or sandbox filesystem. A relative path from one branch must +never reconcile with, broadcast to, or overwrite the same path from another. +The host service validates its bridge credential and terminal worktree +ownership; it starts CoDev shells via `codev-shell` without a host-wide +Superset provider account. Do not enable the flag in production until the guest +image has passed an end-to-end terminal, Git, and worktree smoke test. ## Phase 1 proof and exit criteria diff --git a/packages/contracts/src/collaboration.ts b/packages/contracts/src/collaboration.ts index b3ac7b762..2ec50e645 100644 --- a/packages/contracts/src/collaboration.ts +++ b/packages/contracts/src/collaboration.ts @@ -29,10 +29,22 @@ export const collaborationUserSchema = z.object({ avatarUrl: z.url().nullable(), }); +// Gen 1 worktrees are database UUIDs; Gen 2 worktrees are safe host-service +// identifiers such as "main". The common socket transport carries either; +// each workspace adapter still validates and resolves its own identifier. +const collaborationWorktreeIdSchema = z.union([ + identifierSchema, + z + .string() + .min(1) + .max(64) + .regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/), +]); + const joinMessageSchema = z .object({ type: z.literal("join"), - worktreeId: identifierSchema.optional(), + worktreeId: collaborationWorktreeIdSchema.optional(), resumeFrom: z.string().min(1).max(128).optional(), }) .strict(); @@ -93,7 +105,7 @@ const syncMessageSchema = z.object({ const serverUpdateMessageSchema = z.object({ type: z.literal("update"), - worktreeId: identifierSchema, + worktreeId: collaborationWorktreeIdSchema, path: collaborationPathSchema, update: yjsUpdateBase64Schema, revision: z.string().min(1), @@ -102,7 +114,7 @@ const serverUpdateMessageSchema = z.object({ }); const serverAwarenessMessageSchema = awarenessMessageSchema.extend({ - worktreeId: identifierSchema, + worktreeId: collaborationWorktreeIdSchema, actorId: identifierSchema, connectionId: z.string().min(1), streamId: z.string().min(1), @@ -120,6 +132,7 @@ export const collaborationPresenceEntrySchema = z.object({ .strict() .nullable() .default(null), + worktreeId: collaborationWorktreeIdSchema.nullable().default(null), lastSeenAt: timestampSchema, }); @@ -130,7 +143,7 @@ const presenceMessageSchema = z.object({ const reconciledMessageSchema = z.object({ type: z.literal("reconciled"), - worktreeId: identifierSchema, + worktreeId: collaborationWorktreeIdSchema, path: collaborationPathSchema, revision: z.string().min(1), source: z.enum(["collaboration", "filesystem"]), @@ -139,7 +152,7 @@ const reconciledMessageSchema = z.object({ const conflictMessageSchema = z.object({ type: z.literal("conflict"), - worktreeId: identifierSchema, + worktreeId: collaborationWorktreeIdSchema, path: collaborationPathSchema, snapshotRevision: z.string().min(1), filesystemRevision: z.string().min(1), diff --git a/packages/contracts/src/contracts.test.ts b/packages/contracts/src/contracts.test.ts index c90db88b2..c54cbe2ce 100644 --- a/packages/contracts/src/contracts.test.ts +++ b/packages/contracts/src/contracts.test.ts @@ -327,6 +327,13 @@ describe("workspace contracts", () => { describe("collaboration contracts", () => { it("accepts document subscriptions and bounded Yjs updates", () => { + expect( + collaborationClientMessageSchema.parse({ + type: "join", + worktreeId: "feature-auth", + }), + ).toMatchObject({ type: "join", worktreeId: "feature-auth" }); + expect( collaborationClientMessageSchema.parse({ type: "subscribe", diff --git a/packages/db/drizzle/0056_gen2_yjs_document_worktrees.sql b/packages/db/drizzle/0056_gen2_yjs_document_worktrees.sql new file mode 100644 index 000000000..04ef12451 --- /dev/null +++ b/packages/db/drizzle/0056_gen2_yjs_document_worktrees.sql @@ -0,0 +1,5 @@ +ALTER TABLE "gen2_yjs_documents" ADD COLUMN "worktree_id" text DEFAULT 'main' NOT NULL; +--> statement-breakpoint +DROP INDEX "gen2_yjs_documents_workspace_path_idx"; +--> statement-breakpoint +CREATE UNIQUE INDEX "gen2_yjs_documents_workspace_worktree_path_idx" ON "gen2_yjs_documents" USING btree ("workspace_id","worktree_id","path"); diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 87b184a19..e4d3c7996 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -393,6 +393,13 @@ "when": 1790470000000, "tag": "0055_gen2_yjs_documents", "breakpoints": true + }, + { + "idx": 56, + "version": "7", + "when": 1790556400000, + "tag": "0056_gen2_yjs_document_worktrees", + "breakpoints": true } ] } diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 0a83f7c11..3ed9ac191 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -2085,6 +2085,10 @@ export const gen2YjsDocuments = pgTable( workspaceId: uuid("workspace_id") .references(() => gen2Workspaces.id, { onDelete: "cascade" }) .notNull(), + // Superset owns the checkout names inside a Gen 2 VM. Keep that host + // identity on every snapshot so identical paths on separate branches can + // never share a Yjs document. + worktreeId: text("worktree_id").notNull().default("main"), path: text("path").notNull(), revision: text("revision").notNull(), update: text("update_base64").notNull(), @@ -2100,8 +2104,9 @@ export const gen2YjsDocuments = pgTable( ...timestamps, }, (table) => [ - uniqueIndex("gen2_yjs_documents_workspace_path_idx").on( + uniqueIndex("gen2_yjs_documents_workspace_worktree_path_idx").on( table.workspaceId, + table.worktreeId, table.path, ), ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7072efc96..8db391eb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,7 +102,7 @@ importers: version: 4.21.0 vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)) apps/mobile: dependencies: @@ -339,8 +339,8 @@ importers: specifier: 3.7.6 version: 3.7.6(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.21.1) '@vercel/oidc': - specifier: ^3.8.7 - version: 3.8.7 + specifier: ^3.8.9 + version: 3.8.9 '@xterm/addon-fit': specifier: ^0.11.0 version: 0.11.0 @@ -455,10 +455,10 @@ importers: version: 8.18.1 '@vitejs/plugin-react': specifier: 6.0.4 - version: 6.0.4(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 6.0.4(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)) '@workflow/vitest': specifier: 4.0.14 - version: 4.0.14(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)))(ws@8.21.1) + version: 4.0.14(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1))(vitest@4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)))(ws@8.21.1) eslint-config-next: specifier: 16.2.12 version: 16.2.12(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.2(jiti@2.7.0))(typescript@5.9.3) @@ -470,7 +470,7 @@ importers: version: 4.3.3 vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)) optionalDependencies: node-pty: specifier: 1.1.0 @@ -484,7 +484,7 @@ importers: devDependencies: vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)) packages/contracts: dependencies: @@ -497,7 +497,7 @@ importers: devDependencies: vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)) packages/db: dependencies: @@ -522,7 +522,7 @@ importers: version: 0.31.10 vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)) packages/shared-types: dependencies: @@ -532,7 +532,7 @@ importers: devDependencies: vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)) packages: @@ -866,6 +866,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-proposal-decorators@7.29.7': resolution: {integrity: sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==} engines: {node: '>=6.9.0'} @@ -1115,6 +1120,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.29.9': + resolution: {integrity: sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-unicode-regex@7.29.7': resolution: {integrity: sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==} engines: {node: '>=6.9.0'} @@ -2753,6 +2764,13 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@nestjs/common@11.1.28': resolution: {integrity: sha512-bRImsxibie+AM7xjdwcrm/gr5YeacI65kSBNzTufa1Ib5iwziaY/lqMtRh9THq6pbV4e1HP9aI2ZxGUumnmaoQ==} peerDependencies: @@ -3677,6 +3695,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -4058,8 +4079,8 @@ packages: '@vercel/cli-config@0.2.1': resolution: {integrity: sha512-RhfyXmRLHdbnry8RJqHDc+5rGxMZ0bu+fpysZjtv3bE+BubpuwxTancHOKiH5zKQREsdwFVr3mOI2kOvxlOyxA==} - '@vercel/cli-config@0.2.6': - resolution: {integrity: sha512-2AsKCf6gE/Eniq09ARm1RK9rQMV5pcAHU7BFcR9MISO+4/RsjjkaYbQN6G85/xi9pYZ5CJNXvYn4TI6p1jKBcg==} + '@vercel/cli-config@0.3.0': + resolution: {integrity: sha512-kQLt5Pm8wKd7/imzDY9/9u0tf8OuRkAuXI+s7j057FSUrXapAmumJSKwDqnR4ZZqdWoUcNLlFAgVIAdiwcGTcw==} '@vercel/cli-exec@1.0.0': resolution: {integrity: sha512-kQF8LGie/Hbdq9/psJxLE7owRTcqMQMhgybU04gCeR7cbQAr5t8OrjefDNColJv1QSSucFt4pLwRiARVmlOnug==} @@ -4089,8 +4110,8 @@ packages: resolution: {integrity: sha512-ufdalm2MWOYksyj8KVpWjoOFPJO6zoYpuyvIggIQ2bB0CFCjTCiTkGXHqAKwG77GVRjOaN3/8S5ITlZpXWmqOw==} engines: {node: '>= 20'} - '@vercel/oidc@3.8.7': - resolution: {integrity: sha512-fRu59npOu+1vsV570tiLKskfuRyOJ1MqceAkXbTIfWPnM+c9ve1tSK81oj4umKKirymlGUss3V60Lm5I38rKDw==} + '@vercel/oidc@3.8.9': + resolution: {integrity: sha512-TcbdH3YtGS2KU98T99dN4sbf+E4vbJZsJTRN97wKQ7QWQ0BR7U02QX3KN4+5GHxBtKTj1c5CnvTTitc2ZRad0A==} engines: {node: '>= 20'} '@vercel/queue@0.3.1': @@ -4286,12 +4307,12 @@ packages: resolution: {integrity: sha512-Y/b0YJoCDda6DCFj8ikks06GrEWDsz/3vdgGLeectV9p+YJc76YugRjtqFdd2KTf2rnEPjalL2hcXP+x2KcSLQ==} engines: {node: '>=20'} - '@xhmikosr/bin-wrapper@14.5.0': - resolution: {integrity: sha512-qzNWA0+YUbN3lVz79b2WK+X3QbR8+hmd6nMTAko1F/1jAeK7VbpK7TcfkYU4dL8iaLNNGYL94WsTewwH17jMTQ==} + '@xhmikosr/bin-wrapper@14.5.1': + resolution: {integrity: sha512-UZUuTYWxeAbTIiRKKEAmV3csoE36B3CGFZrYYn87+bSEBTyJ32p5gx5Gmj5HOgyOtioFUypbOZ1V5M/l/VoePw==} engines: {node: '>=20'} - '@xhmikosr/decompress-tar@9.0.1': - resolution: {integrity: sha512-4AkVR1SoqTxYY22IRRYKDeLirPIDGqMqYsqgjKYuwhgRcBb+yDP4t5Xph33UCzL/nahK/aADmlMEjTNstbX7kw==} + '@xhmikosr/decompress-tar@9.0.2': + resolution: {integrity: sha512-8nPZ6lZ3ExhsSxi/X/PMB3K+Vtsuxk43HowxYpxw4AsCHTYqFBXwC8B3Y+M/meaUOGOVm+2tFNUAfWGRjBt+Ww==} engines: {node: '>=20'} '@xhmikosr/decompress-tarbz2@9.0.2': @@ -4306,12 +4327,12 @@ packages: resolution: {integrity: sha512-2MS94QnmXQwjkKN8WyFiu1sU7J3rcWJcMze4kRYsX7tN+CXpUGECgkh4YSOhujpkWPuVlFudIziJHO/TxOqkQQ==} engines: {node: '>=20'} - '@xhmikosr/decompress@11.1.3': - resolution: {integrity: sha512-NiyhJq6z7ERsYghcnXZUI6ooDXgZtoB+G9eUsYhfSM4VLp2rKx9UxhKI1NEf1PqosrNPxG3bnSsr2UBVbNurlg==} + '@xhmikosr/decompress@11.1.4': + resolution: {integrity: sha512-ZbYL7SAfY37/TMpopqBR3mQiuQ76kI/RNpN4q82YHSw/UxktZNy8P4wgiKPSrTImMAWRaUG8UK1pgEa56YdLaw==} engines: {node: '>=20'} - '@xhmikosr/downloader@16.3.0': - resolution: {integrity: sha512-DF6qHkozD2SiE6yOUOUk8JWpLEPwT4vKZRsDeOZX6ekKDUO/LyeauxOO+H8j3AuRT23YWIx+7Thv7f5SoPS3lQ==} + '@xhmikosr/downloader@16.3.1': + resolution: {integrity: sha512-M67dvznaFbsvoqhGT4FsHWysaXXQ8386OViGZm0WOyQS3apW9p16WgvHp9nWj2vfKQAR2ZdqIBPNCHSM5rKSCg==} engines: {node: '>=20'} '@xhmikosr/os-filter-obj@4.1.0': @@ -4367,6 +4388,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -4522,8 +4548,8 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} - b4a@1.8.1: - resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + b4a@1.9.0: + resolution: {integrity: sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==} peerDependencies: react-native-b4a: '*' peerDependenciesMeta: @@ -4588,8 +4614,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - bare-events@2.9.1: - resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} peerDependencies: bare-abort-controller: '*' peerDependenciesMeta: @@ -4653,6 +4679,9 @@ packages: brace-expansion@2.1.3: resolution: {integrity: sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==} + brace-expansion@2.1.7: + resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} + brace-expansion@5.0.8: resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} engines: {node: 20 || >=22} @@ -5878,12 +5907,12 @@ packages: filelist@1.0.6: resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} - filename-reserved-regex@4.0.0: - resolution: {integrity: sha512-9ZT504KxEQDamsOogZImAWGEN24R1uFAxU3ZS4AZqn2ooidmN68Olh7n4/RcA4lLatZztjA0ZSuxeLHVoCc8JA==} + filename-reserved-regex@4.0.1: + resolution: {integrity: sha512-qUet2faQFKvtvVUsEf7wCrTURwxBOIZpspsLHGifw9QCWk55ITE2FrG8XhfQqG/uxMA1xEGFVQbL+Yfm0O94+Q==} engines: {node: '>=20'} - filenamify@7.0.2: - resolution: {integrity: sha512-fz10TUqSZ1lG7ftW1KnRotJzMD8YRb6kaAQKpZJBLvqXXfFgIEpuazy1w2lK3zhMiBSdH/uF9LFlv5smJ2Jl1w==} + filenamify@7.0.3: + resolution: {integrity: sha512-Pf0dwHrWs1GcIK15ps304fmxp2AOcfNR6vOIMvCPEy6ZztKYxzRQHlOuPVWo2HRjbCLHaRTY+kD8n3RbQ0A/fw==} engines: {node: '>=20'} fill-range@7.1.1: @@ -6501,8 +6530,8 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.2.4: - resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} + jose@6.2.12: + resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -6968,60 +6997,118 @@ packages: resolution: {integrity: sha512-2WbHILKMiJUzfdjmGOQOqU1bWi9//gqiclc/tkk/AIsrrVw3efhZ1uhkOwMTxUEPOzqoo091H0olLmVZH5FHGQ==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-babel-transformer@0.84.6: + resolution: {integrity: sha512-B1ozl6KxFHbQjXZ2U8WiTPWXylqiD3V4EXyV5r0fqETOrqIrJlBvbVTD82UxIN+B3Vpe4l8G1tb15ocd2RyOQA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-cache-key@0.84.5: resolution: {integrity: sha512-3dPB2TnvGjjf0/9O7AXVQURKXuQNauTZE7WpTGTlR017Gh/B5y0m/2wcqxfveUguHSpu89KhVxCAlr2k/H7uhQ==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-cache-key@0.84.6: + resolution: {integrity: sha512-6tyXt1BZ/3U183XV48oif7Nm65TE+pTo0Vs31u4FxMgqiYQR/SiSr3RoXn3pmy4pd280ZmLmct0x3jZpu0pV4A==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-cache@0.84.5: resolution: {integrity: sha512-WHS0n2OxQqtwEjSeQFPePNrMvEFhmQcUQM9cRJMHByWoi/GMWFBEWOf7hVkAM/0KRutAXNbDlSu/cZB6CyxgQQ==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-cache@0.84.6: + resolution: {integrity: sha512-KBJVpb02oKNO+jlWQ1Aax7cd11YP0MClDaMgx6C4MhyZ9g95J/dC1Bo6sCzXaU9L9h/w5cQfT5zDkGW5sOj99A==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-config@0.84.5: resolution: {integrity: sha512-zie+uN6oohscowi2S7ByU+wUw6CrT4ZxW9uAbONOObSxx86RGmnIAmjXHLkfmcdYoY7jzOPEbqcI6oeVmqyBQA==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-config@0.84.6: + resolution: {integrity: sha512-cD2mLEofcuV26kxSV4hfV1Pbuh6igdUhYQriO8ZVeJ7HyZfq4/wqHV0gSR7fEO8NUp9odr60/Kzm5YakmghWwQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-core@0.84.5: resolution: {integrity: sha512-xwm605hCi5Y6eJTTb8ZWo6pkUcoBEIyiQOfkZh5GwtDwUrP9SNhTQZhzJHrBCwwxlf3Ptl/pxWJgQ1rsNYMnrA==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-core@0.84.6: + resolution: {integrity: sha512-ZqYskM8+f3PFMosBDJd8DLqRvT1ltRGhWL/ZfTpBvf4SoadO30VVrdhAoHFVGz3G1CJpWa3fgZH7nnnX/ybllA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-file-map@0.84.5: resolution: {integrity: sha512-mlm/JL8toSbSc2akpKIGmzvrVRSCgZ5vkbycI34oMLoOnLGuLyC8WTyVJ6P0hZG/usDaGwZSl/s9BCRriqjGJA==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-file-map@0.84.6: + resolution: {integrity: sha512-ov9VywWBsHPt54Zc7/DxWvqjTwxuZvhfeyHg8ZAMGKtf2PcH1wS3EnoBH7In53Rspk/eXVWAO5+avxunPn081w==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-minify-terser@0.84.5: resolution: {integrity: sha512-BJoFwCEDsYnagPqarayInv2+diCDNDdLlaof/p6s9w4gh+gc9HXYM+pDvsKGKKUumpZswNF3Z/ftTMqKl/5IBg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-minify-terser@0.84.6: + resolution: {integrity: sha512-SqhB/Kxrw0XfyW0k8mEscqX/CQopm6Fp3EpdSIPzvKfGgQp9bTMahg9PAdVsXUmIWwErbmFw1VqB+VFBOKksUw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-resolver@0.84.5: resolution: {integrity: sha512-VSSnepg1k6LyCwtb6eirWdAWlpKwBG8Rdtsr1mU38rMelFyWgh3/QuMSiZIZAIjwg/fsa8GhW5/FO54CAUPCEA==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-resolver@0.84.6: + resolution: {integrity: sha512-PplpGc/OCLxgKeLNsLQO/VHsYDIvkcGGr3zNT7UanHCGRqhxKv245o+naV8cRI4/pcLqyc+9j+J0AJbT3Kp5Sg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-runtime@0.84.5: resolution: {integrity: sha512-U1m2+d1Pr+JO2/iVXBB2OfXXityz7tqwIorxfrT15IEgaHvpJBq/OHiqnOWPKJbUl3JcxjcdviZZOKk85oK4Qg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-runtime@0.84.6: + resolution: {integrity: sha512-47EYO/DOai0PA3GoAyDpyXOgHNMwbl/Z9dQlQineDLHkpD42xOwMrIx1crT0+n6F+aGoEyxZmeFwfMzaIqVqKQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-source-map@0.84.5: resolution: {integrity: sha512-2BtV5L9uPc49F13Gn5wiP6bX/EncqzqTIk2VL/0F/96Vo0YEOjluT/qktQjFODfqGFsucwnh5mPEAl/2jVEfeg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-source-map@0.84.6: + resolution: {integrity: sha512-4p/EplRbgNxhdqA6kAj1AmIq9qqYXYSAjgxuZ73rYA+p037xDDrxK8CyDKNfbn1k6aMuVt8LV0fpt7NHUTKiZw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-symbolicate@0.84.5: resolution: {integrity: sha512-rQ40zYDAkaWBN9yvjUuAD0ZpzBMZSoKyGYXnb5JrfbKjun7fTvfoLHL3KXFYenBTYZkQtlp4cKSCv/1utxFyOw==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} hasBin: true + metro-symbolicate@0.84.6: + resolution: {integrity: sha512-wJFyyF5ysbVyoYVWGL6GwVMiNnGllwDU2gF096376fl0fc8IFUlADSyNlswj31K9og2sOilXe/FdyWFPczPUYg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + metro-transform-plugins@0.84.5: resolution: {integrity: sha512-+InaSVGaOyt0DyRo4Y/zIdPI6CZwnbNho5LAL23tgmuGwv7fyfkF7kKfPjZcfxXBcoYdTLLFnCfCH/dHSiCqNg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-transform-plugins@0.84.6: + resolution: {integrity: sha512-pROPMFaj25Y9+3LlLqpBRz/7B/2YPnfEQz/z3juYxkh3FGQ+3c/Qh4E+khPSTdwTw0M6LitLYHMqaHMbQmuBJw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-transform-worker@0.84.5: resolution: {integrity: sha512-ui1Z8x4s5RL36gMmKLaMMO7O9NNDHNdthEZSCDQHAau3JcAsTaFOK6I+2q4I/kW5u8hSEjJk9L45TXSVJw6g1A==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-transform-worker@0.84.6: + resolution: {integrity: sha512-xLTVrOENaHR9DiuhnHS+zcle0rKsCy2CpXSIx7xD+zhJs+qVRNUyV7Z1zIt/0P/d0cSGVx846R+dIjHEudhcww==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro@0.84.5: resolution: {integrity: sha512-r1liLkyFZMVSEMNjU1CJU5pRzs3NdkxHqXS60O25c0rCIqAR+cGk7rPydw/g0WAIKVXojIBIF45yYBPagJGcgw==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} hasBin: true + metro@0.84.6: + resolution: {integrity: sha512-ty/tx/imE5Ph2gvPU788Ckim6RAC2tyZxLz2hWRgC9U7/fGKV7qY/slADy0qHPEw9cpUgc4BfmV3oUsYwm7oeg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -7353,6 +7440,10 @@ packages: resolution: {integrity: sha512-aH9RkoZc7w/90HBamFxTw8ZLFr05wXS+iOnvmrgo53Ep8Pyrm5FieQSaPIVROkfFVQISeD/zo92fes26TOwe+A==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + ob1@0.84.6: + resolution: {integrity: sha512-+s+6zjd0X68hfAcn92NsEPXnSYOF3O7exw/Fj4UfRTB+UL+Tdjdkzu5/4WquXlzqgvNm8FWuuW6/0Yd8S2rYxw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -7610,8 +7701,8 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - piscina@4.9.3: - resolution: {integrity: sha512-3e3ka9QCE8RJ5I9uszdAADZnkcYi21cqmF3gxox3u884N72qpFHCsIVhHt8cEQ9t3Auq/NqoiCEuhxlxxQuDWA==} + piscina@4.9.4: + resolution: {integrity: sha512-RyBDr2VheQ8ZfH3N8SzQZHztqVyOtadTwLnUYof6gdj5161/eu2wNhCbGl5GHnNKDpnkicJYpKtL2J+y/gk6XA==} pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -7689,8 +7780,8 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-ms@9.3.0: - resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + pretty-ms@9.3.1: + resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==} engines: {node: '>=18'} proc-log@4.2.0: @@ -8319,8 +8410,8 @@ packages: resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} engines: {node: '>= 0.10.0'} - streamx@2.28.0: - resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} @@ -8500,6 +8591,11 @@ packages: engines: {node: '>=10'} hasBin: true + terser@5.51.2: + resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} + engines: {node: '>=10'} + hasBin: true + text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} @@ -8672,8 +8768,8 @@ packages: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} - uint8array-extras@1.5.0: - resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + uint8array-extras@1.6.0: + resolution: {integrity: sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==} engines: {node: '>=18'} ulid@3.0.2: @@ -9122,6 +9218,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -9260,7 +9361,7 @@ snapshots: '@auth/core@0.41.3': dependencies: '@panva/hkdf': 1.2.1 - jose: 6.2.4 + jose: 6.2.12 oauth4webapi: 3.8.6 preact: 10.24.3 preact-render-to-string: 6.5.11(preact@10.24.3) @@ -9766,6 +9867,10 @@ snapshots: dependencies: '@babel/types': 7.29.8 + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -10050,6 +10155,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-typescript@7.29.9(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -11508,11 +11624,11 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 + '@tybys/wasm-util': 0.10.4 optional: true '@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)': @@ -12100,7 +12216,7 @@ snapshots: '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-regenerator': 7.29.8(@babel/core@7.29.7) '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.9(@babel/core@7.29.7) '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) '@react-native/babel-plugin-codegen': 0.86.2(@babel/core@7.29.7) babel-plugin-syntax-hermes-parser: 0.36.0 @@ -12181,8 +12297,8 @@ snapshots: dependencies: '@react-native/js-polyfills': 0.86.2 '@react-native/metro-babel-transformer': 0.86.2(@babel/core@7.29.7) - metro-config: 0.84.5 - metro-runtime: 0.84.5 + metro-config: 0.84.6 + metro-runtime: 0.84.6 transitivePeerDependencies: - '@babel/core' - bufferutil @@ -12242,7 +12358,7 @@ snapshots: dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true '@rolldown/binding-win32-arm64-msvc@1.1.5': @@ -12320,10 +12436,10 @@ snapshots: dependencies: '@swc/core': 1.15.3 '@swc/counter': 0.1.3 - '@xhmikosr/bin-wrapper': 14.5.0 + '@xhmikosr/bin-wrapper': 14.5.1 commander: 8.3.0 minimatch: 9.0.9 - piscina: 4.9.3 + piscina: 4.9.4 semver: 7.8.5 slash: 3.0.0 source-map: 0.7.6 @@ -12514,6 +12630,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aria-query@5.0.4': {} '@types/chai@5.2.3': @@ -12973,7 +13094,7 @@ snapshots: dependencies: async-listen: 3.0.0 open: 8.4.0 - xdg-app-paths: 5.5.1 + xdg-app-paths: 5.1.0 zod: 4.1.11 '@vercel/cli-config@0.2.1': @@ -12981,7 +13102,7 @@ snapshots: xdg-app-paths: 5.5.1 zod: 4.1.11 - '@vercel/cli-config@0.2.6': + '@vercel/cli-config@0.3.0': dependencies: xdg-app-paths: 5.5.1 zod: 4.1.11 @@ -13009,23 +13130,23 @@ snapshots: '@vercel/cli-exec': 1.0.0 jose: 5.10.0 - '@vercel/oidc@3.8.7': + '@vercel/oidc@3.8.9': dependencies: - '@vercel/cli-config': 0.2.6 + '@vercel/cli-config': 0.3.0 '@vercel/cli-exec': 1.0.1 jose: 5.10.0 '@vercel/queue@0.3.1': dependencies: - '@vercel/oidc': 3.8.1 + '@vercel/oidc': 3.8.9 minimatch: 10.2.6 mixpart: 0.0.6 picocolors: 1.1.1 - '@vitejs/plugin-react@6.0.4(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.4(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1) optionalDependencies: babel-plugin-react-compiler: 1.0.0 @@ -13038,13 +13159,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1) '@vitest/pretty-format@4.1.10': dependencies: @@ -13347,16 +13468,16 @@ snapshots: - supports-color - ws - '@workflow/vitest@4.0.14(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)))(ws@8.21.1)': + '@workflow/vitest@4.0.14(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1))(vitest@4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)))(ws@8.21.1)': dependencies: '@workflow/builders': 4.1.3(ws@8.21.1) '@workflow/core': 4.6.2(ws@8.21.1) '@workflow/rollup': 4.0.13(ws@8.21.1) '@workflow/world': 4.2.1 '@workflow/world-local': 4.2.2 - vitest: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)) optionalDependencies: - vite: 8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1) transitivePeerDependencies: - '@opentelemetry/api' - '@swc/helpers' @@ -13432,10 +13553,10 @@ snapshots: execa: 9.6.1 isexe: 4.0.0 - '@xhmikosr/bin-wrapper@14.5.0': + '@xhmikosr/bin-wrapper@14.5.1': dependencies: '@xhmikosr/bin-check': 8.2.2 - '@xhmikosr/downloader': 16.3.0 + '@xhmikosr/downloader': 16.3.1 '@xhmikosr/os-filter-obj': 4.1.0 binary-version-check: 6.1.0 transitivePeerDependencies: @@ -13443,7 +13564,7 @@ snapshots: - react-native-b4a - supports-color - '@xhmikosr/decompress-tar@9.0.1': + '@xhmikosr/decompress-tar@9.0.2': dependencies: file-type: 21.3.4 is-stream: 4.0.1 @@ -13455,7 +13576,7 @@ snapshots: '@xhmikosr/decompress-tarbz2@9.0.2': dependencies: - '@xhmikosr/decompress-tar': 9.0.1 + '@xhmikosr/decompress-tar': 9.0.2 file-type: 21.3.4 is-stream: 4.0.1 seek-bzip: 2.0.0 @@ -13467,7 +13588,7 @@ snapshots: '@xhmikosr/decompress-targz@9.0.1': dependencies: - '@xhmikosr/decompress-tar': 9.0.1 + '@xhmikosr/decompress-tar': 9.0.2 file-type: 21.3.4 is-stream: 4.0.1 transitivePeerDependencies: @@ -13483,9 +13604,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@xhmikosr/decompress@11.1.3': + '@xhmikosr/decompress@11.1.4': dependencies: - '@xhmikosr/decompress-tar': 9.0.1 + '@xhmikosr/decompress-tar': 9.0.2 '@xhmikosr/decompress-tarbz2': 9.0.2 '@xhmikosr/decompress-targz': 9.0.1 '@xhmikosr/decompress-unzip': 8.2.1 @@ -13496,14 +13617,14 @@ snapshots: - react-native-b4a - supports-color - '@xhmikosr/downloader@16.3.0': + '@xhmikosr/downloader@16.3.1': dependencies: '@xhmikosr/archive-type': 8.1.0 - '@xhmikosr/decompress': 11.1.3 + '@xhmikosr/decompress': 11.1.4 content-disposition: 2.0.1 ext-name: 5.0.0 file-type: 21.3.4 - filenamify: 7.0.2 + filenamify: 7.0.3 got: 14.6.6 transitivePeerDependencies: - bare-abort-controller @@ -13552,6 +13673,8 @@ snapshots: acorn@8.17.0: {} + acorn@8.18.0: {} + agent-base@7.1.4: {} agent-cli-detector@0.1.6: {} @@ -13720,7 +13843,7 @@ snapshots: axobject-query@4.1.0: {} - b4a@1.8.1: {} + b4a@1.9.0: {} babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7): dependencies: @@ -13826,7 +13949,7 @@ snapshots: balanced-match@4.0.4: {} - bare-events@2.9.1: {} + bare-events@2.9.2: {} base64-js@1.5.1: {} @@ -13901,6 +14024,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.1.7: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.4 @@ -14902,8 +15029,8 @@ snapshots: '@babel/parser': 7.29.8 eslint: 9.39.2(jiti@2.7.0) hermes-parser: 0.25.1 - zod: 4.4.3 - zod-validation-error: 4.0.2(zod@4.4.3) + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color @@ -15011,7 +15138,7 @@ snapshots: events-universal@1.0.1: dependencies: - bare-events: 2.9.1 + bare-events: 2.9.2 transitivePeerDependencies: - bare-abort-controller @@ -15051,7 +15178,7 @@ snapshots: is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 6.0.0 - pretty-ms: 9.3.0 + pretty-ms: 9.3.1 signal-exit: 4.1.0 strip-final-newline: 4.0.0 yoctocolors: 2.2.0 @@ -15435,7 +15562,7 @@ snapshots: '@tokenizer/inflate': 0.4.1 strtok3: 10.3.5 token-types: 6.1.2 - uint8array-extras: 1.5.0 + uint8array-extras: 1.6.0 transitivePeerDependencies: - supports-color @@ -15443,11 +15570,11 @@ snapshots: dependencies: minimatch: 5.1.9 - filename-reserved-regex@4.0.0: {} + filename-reserved-regex@4.0.1: {} - filenamify@7.0.2: + filenamify@7.0.3: dependencies: - filename-reserved-regex: 4.0.0 + filename-reserved-regex: 4.0.1 fill-range@7.1.1: dependencies: @@ -16066,7 +16193,7 @@ snapshots: jose@5.10.0: {} - jose@6.2.4: {} + jose@6.2.12: {} js-tokens@4.0.0: {} @@ -16581,10 +16708,24 @@ snapshots: transitivePeerDependencies: - supports-color + metro-babel-transformer@0.84.6: + dependencies: + '@babel/core': 7.29.7 + flow-enums-runtime: 0.0.6 + hermes-parser: 0.35.0 + metro-cache-key: 0.84.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + metro-cache-key@0.84.5: dependencies: flow-enums-runtime: 0.0.6 + metro-cache-key@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + metro-cache@0.84.5: dependencies: exponential-backoff: 3.1.3 @@ -16594,6 +16735,15 @@ snapshots: transitivePeerDependencies: - supports-color + metro-cache@0.84.6: + dependencies: + exponential-backoff: 3.1.3 + flow-enums-runtime: 0.0.6 + https-proxy-agent: 7.0.6 + metro-core: 0.84.6 + transitivePeerDependencies: + - supports-color + metro-config@0.84.5: dependencies: connect: 3.7.0 @@ -16609,12 +16759,33 @@ snapshots: - supports-color - utf-8-validate + metro-config@0.84.6: + dependencies: + connect: 3.7.0 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.84.6 + metro-cache: 0.84.6 + metro-core: 0.84.6 + metro-runtime: 0.84.6 + yaml: 2.9.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + metro-core@0.84.5: dependencies: flow-enums-runtime: 0.0.6 lodash.throttle: 4.1.1 metro-resolver: 0.84.5 + metro-core@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.84.6 + metro-file-map@0.84.5: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -16629,20 +16800,48 @@ snapshots: transitivePeerDependencies: - supports-color + metro-file-map@0.84.6: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + nullthrows: 1.1.1 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + metro-minify-terser@0.84.5: dependencies: flow-enums-runtime: 0.0.6 terser: 5.50.0 + metro-minify-terser@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.51.2 + metro-resolver@0.84.5: dependencies: flow-enums-runtime: 0.0.6 + metro-resolver@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + metro-runtime@0.84.5: dependencies: '@babel/runtime': 7.29.7 flow-enums-runtime: 0.0.6 + metro-runtime@0.84.6: + dependencies: + '@babel/runtime': 7.29.7 + flow-enums-runtime: 0.0.6 + metro-source-map@0.84.5: dependencies: '@babel/traverse': 7.29.8 @@ -16657,6 +16856,20 @@ snapshots: transitivePeerDependencies: - supports-color + metro-source-map@0.84.6: + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.84.6 + nullthrows: 1.1.1 + ob1: 0.84.6 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + metro-symbolicate@0.84.5: dependencies: flow-enums-runtime: 0.0.6 @@ -16668,6 +16881,17 @@ snapshots: transitivePeerDependencies: - supports-color + metro-symbolicate@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.84.6 + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + metro-transform-plugins@0.84.5: dependencies: '@babel/core': 7.29.7 @@ -16679,6 +16903,17 @@ snapshots: transitivePeerDependencies: - supports-color + metro-transform-plugins@0.84.6: + dependencies: + '@babel/core': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + metro-transform-worker@0.84.5: dependencies: '@babel/core': 7.29.7 @@ -16699,6 +16934,26 @@ snapshots: - supports-color - utf-8-validate + metro-transform-worker@0.84.6: + dependencies: + '@babel/core': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + flow-enums-runtime: 0.0.6 + metro: 0.84.6 + metro-babel-transformer: 0.84.6 + metro-cache: 0.84.6 + metro-cache-key: 0.84.6 + metro-minify-terser: 0.84.6 + metro-source-map: 0.84.6 + metro-transform-plugins: 0.84.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + metro@0.84.5: dependencies: '@babel/code-frame': 7.29.7 @@ -16744,6 +16999,51 @@ snapshots: - supports-color - utf-8-validate + metro@0.84.6: + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + accepts: 2.0.0 + ci-info: 2.0.0 + connect: 3.7.0 + debug: 4.4.3(supports-color@8.1.1) + error-stack-parser: 2.1.4 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + hermes-parser: 0.35.0 + invariant: 2.2.4 + jest-worker: 29.7.0 + jsc-safe-url: 0.2.4 + lodash.throttle: 4.1.1 + metro-babel-transformer: 0.84.6 + metro-cache: 0.84.6 + metro-cache-key: 0.84.6 + metro-config: 0.84.6 + metro-core: 0.84.6 + metro-file-map: 0.84.6 + metro-resolver: 0.84.6 + metro-runtime: 0.84.6 + metro-source-map: 0.84.6 + metro-symbolicate: 0.84.6 + metro-transform-plugins: 0.84.6 + metro-transform-worker: 0.84.6 + mime-types: 3.0.2 + nullthrows: 1.1.1 + serialize-error: 2.1.0 + source-map: 0.5.7 + throat: 5.0.0 + ws: 7.5.13 + yargs: 17.7.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -16980,7 +17280,7 @@ snapshots: minimatch@9.0.9: dependencies: - brace-expansion: 2.1.3 + brace-expansion: 2.1.7 minimist@1.2.8: {} @@ -17130,6 +17430,10 @@ snapshots: dependencies: flow-enums-runtime: 0.0.6 + ob1@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -17386,7 +17690,7 @@ snapshots: picomatch@4.0.7: {} - piscina@4.9.3: + piscina@4.9.4: optionalDependencies: '@napi-rs/nice': 1.1.1 @@ -17460,7 +17764,7 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - pretty-ms@9.3.0: + pretty-ms@9.3.1: dependencies: parse-ms: 4.0.0 @@ -18225,7 +18529,7 @@ snapshots: stream-buffers@2.2.0: {} - streamx@2.28.0: + streamx@2.28.1: dependencies: events-universal: 1.0.1 fast-fifo: 1.3.2 @@ -18406,9 +18710,9 @@ snapshots: tar-stream@3.1.7: dependencies: - b4a: 1.8.1 + b4a: 1.9.0 fast-fifo: 1.3.2 - streamx: 2.28.0 + streamx: 2.28.1 transitivePeerDependencies: - bare-abort-controller - react-native-b4a @@ -18430,9 +18734,16 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + terser@5.51.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.18.0 + commander: 2.20.3 + source-map-support: 0.5.21 + text-decoder@1.2.7: dependencies: - b4a: 1.8.1 + b4a: 1.9.0 transitivePeerDependencies: - react-native-b4a @@ -18606,7 +18917,7 @@ snapshots: dependencies: '@lukeed/csprng': 1.1.0 - uint8array-extras@1.5.0: {} + uint8array-extras@1.6.0: {} ulid@3.0.2: {} @@ -18793,7 +19104,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0): + vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 @@ -18805,14 +19116,14 @@ snapshots: esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 - terser: 5.50.0 + terser: 5.51.2 tsx: 4.21.0 - yaml: 2.9.0 + yaml: 2.9.1 - vitest@4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)): + vitest@4.1.10(@types/node@24.10.13)(jsdom@30.0.0)(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -18829,7 +19140,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.51.2)(tsx@4.21.0)(yaml@2.9.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.13 @@ -19050,6 +19361,8 @@ snapshots: yaml@2.9.0: {} + yaml@2.9.1: {} + yargs-parser@21.1.1: {} yargs@17.7.3: @@ -19076,9 +19389,9 @@ snapshots: yoctocolors@2.2.0: {} - zod-validation-error@4.0.2(zod@4.4.3): + zod-validation-error@4.0.2(zod@4.3.6): dependencies: - zod: 4.4.3 + zod: 4.3.6 zod@3.25.76: {}