Build a basic Next.js control-plane frontend using Effect Atom HttpApi atoms, simplify the control-plane atom client to generic query/mutation primitives, and wire root v2 dev with Turbo while fixing Convex runtime imports for dev bundling.
Unified · Split
| @@ -1,3 +1,6 @@ | |||
| 1 | .reference/ | 1 | .reference/ |
| 2 | .vercel | 2 | .vercel |
| 3 | .env*.local | 3 | .env*.local |
| 4 | .next/ | ||
| 5 | .turbo/ | ||
| 6 | .executor-v2/ | ||
| @@ -1,14 +1,17 @@ | |||
| 1 | import { | 1 | import { |
| 2 | makeLocalInProcessRuntimeAdapter, | ||
| 3 | makeToolProviderRegistry, | ||
| 4 | ToolProviderRegistryService, | 2 | ToolProviderRegistryService, |
| 5 | type RuntimeExecuteError, | 3 | makeToolProviderRegistry, |
| 6 | } from "@executor-v2/engine"; | 4 | } from "@executor-v2/engine/tool-providers"; |
| 7 | import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk"; | 5 | import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk"; |
| 8 | import * as Context from "effect/Context"; | 6 | import * as Context from "effect/Context"; |
| 9 | import * as Effect from "effect/Effect"; | 7 | import * as Effect from "effect/Effect"; |
| 10 | import * as Layer from "effect/Layer"; | 8 | import * as Layer from "effect/Layer"; |
| 11 | 9 | ||
| 10 | import { | ||
| 11 | makeLocalInProcessRuntimeAdapter, | ||
| 12 | type RuntimeExecuteError, | ||
| 13 | } from "./runtime_adapter"; | ||
| 14 | |||
| 12 | export type ConvexRunExecutorService = { | 15 | export type ConvexRunExecutorService = { |
| 13 | executeRun: ( | 16 | executeRun: ( |
| 14 | input: ExecuteRunInput, | 17 | input: ExecuteRunInput, |
| @@ -23,9 +26,7 @@ const runtimeAdapter = makeLocalInProcessRuntimeAdapter(); · 8 unmodified lines | |||
| 23 | 26 | ||
| 24 | const formatRuntimeExecuteError = (error: RuntimeExecuteError): string => { | 27 | const formatRuntimeExecuteError = (error: RuntimeExecuteError): string => { |
| 25 | switch (error._tag) { | 28 | switch (error._tag) { |
| 26 | case "RuntimeAdapterError": | ||
| 27 | case "LocalCodeRunnerError": | 29 | case "LocalCodeRunnerError": |
| 28 | case "DenoSubprocessRunnerError": | ||
| 29 | case "ToolProviderError": | 30 | case "ToolProviderError": |
| 30 | return error.details ? `${error.message}: ${error.details}` : error.message; | 31 | return error.details ? `${error.message}: ${error.details}` : error.message; |
| 31 | case "ToolProviderRegistryError": | 32 | case "ToolProviderRegistryError": |
| @@ -1,7 +1,12 @@ | |||
| 1 | import { executeJavaScriptWithTools } from "@executor-v2/engine/local-runner"; | 1 | import { |
| 2 | import type { | 2 | LocalCodeRunnerError, |
| 3 | CanonicalToolDescriptor, | 3 | executeJavaScriptWithTools, |
| 4 | } from "@executor-v2/engine/local-runner"; | ||
| 5 | import { | ||
| 6 | ToolProviderError, | ||
| 7 | ToolProviderRegistryError, | ||
| 4 | ToolProviderRegistryService, | 8 | ToolProviderRegistryService, |
| 9 | type CanonicalToolDescriptor, | ||
| 5 | } from "@executor-v2/engine/tool-providers"; | 10 | } from "@executor-v2/engine/tool-providers"; |
| 6 | import type { Source } from "@executor-v2/schema"; | 11 | import type { Source } from "@executor-v2/schema"; |
| 7 | import * as Effect from "effect/Effect"; | 12 | import * as Effect from "effect/Effect"; |
| @@ -17,12 +22,17 @@ export type RuntimeExecuteInput = { · 9 unmodified lines | |||
| 17 | timeoutMs?: number; | 22 | timeoutMs?: number; |
| 18 | }; | 23 | }; |
| 19 | 24 | ||
| 25 | export type RuntimeExecuteError = | ||
| 26 | | LocalCodeRunnerError | ||
| 27 | | ToolProviderRegistryError | ||
| 28 | | ToolProviderError; | ||
| 29 | |||
| 20 | export type RuntimeAdapter = { | 30 | export type RuntimeAdapter = { |
| 21 | kind: "local-inproc"; | 31 | kind: "local-inproc"; |
| 22 | isAvailable: () => Effect.Effect<boolean>; | 32 | isAvailable: () => Effect.Effect<boolean>; |
| 23 | execute: ( | 33 | execute: ( |
| 24 | input: RuntimeExecuteInput, | 34 | input: RuntimeExecuteInput, |
| 25 | ) => Effect.Effect<unknown, unknown, ToolProviderRegistryService>; | 35 | ) => Effect.Effect<unknown, RuntimeExecuteError, ToolProviderRegistryService>; |
| 26 | }; | 36 | }; |
| 27 | 37 | ||
| 28 | export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({ | 38 | export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({ |
| @@ -1,3 +1,15 @@ | |||
| 1 | # app-web | 1 | # app-web |
| 2 | 2 | ||
| 3 | Web UI app scaffold for Executor v2. | 3 | Basic Next.js frontend for Executor v2 control plane. |
| 4 | |||
| 5 | - Uses `@executor-v2/control-plane` Effect HttpApi client | ||
| 6 | - Uses Effect Atom (`@effect-atom/atom`, `@effect-atom/atom-react`) for query state | ||
| 7 | - Proxies backend calls through `/api/control-plane/*` via `next.config.ts` rewrites | ||
| 8 | |||
| 9 | Run: | ||
| 10 | |||
| 11 | - `bun run --cwd apps/web dev` | ||
| 12 | - Open `http://127.0.0.1:3000` | ||
| 13 | |||
| 14 | By default, proxy target is `http://127.0.0.1:8787`. | ||
| 15 | Override with `CONTROL_PLANE_UPSTREAM_URL`. | ||
| @@ -0,0 +1,114 @@ | |||
| 1 | # Effect Atom guide for `apps/web` | ||
| 2 | |||
| 3 | This file defines how we write Effect Atom code in this app. | ||
| 4 | |||
| 5 | ## Goal | ||
| 6 | |||
| 7 | Keep React components dumb. Put async/result orchestration in atoms, and render from typed state objects. | ||
| 8 | |||
| 9 | ## Hard rules | ||
| 10 | |||
| 11 | - Do not create query atoms in render with `useMemo`. | ||
| 12 | - Do not hand-roll `isLoading` / `hasError` booleans from `_tag` checks in components. | ||
| 13 | - Do not manually mirror server state with React local state. | ||
| 14 | - Prefer `Atom.family` + derived state atoms over component-level branching. | ||
| 15 | |||
| 16 | ## Baseline stack | ||
| 17 | |||
| 18 | - `@effect-atom/atom` | ||
| 19 | - `@effect-atom/atom-react` | ||
| 20 | - `@executor-v2/control-plane` client (Effect HttpApi) | ||
| 21 | |||
| 22 | ## Recommended structure | ||
| 23 | |||
| 24 | - `lib/control-plane/client.ts` | ||
| 25 | - Export one shared `createControlPlaneAtomClient(...)` instance. | ||
| 26 | - `lib/control-plane/atoms.ts` | ||
| 27 | - Export query/mutation atoms and derived state atoms. | ||
| 28 | - `app/.../page.tsx` | ||
| 29 | - Use `useAtomValue`, `useAtomSet`, and `useAtomRefresh`. | ||
| 30 | - Render from derived state atom output. | ||
| 31 | |||
| 32 | ## Preferred patterns | ||
| 33 | |||
| 34 | ### 1) Parametrized query atom with `Atom.family` | ||
| 35 | |||
| 36 | Create once at module scope: | ||
| 37 | |||
| 38 | ```ts | ||
| 39 | const sourcesResultByWorkspace = Atom.family((workspaceId: WorkspaceId) => | ||
| 40 | controlPlaneClient.query("sources", "list", { | ||
| 41 | path: { workspaceId }, | ||
| 42 | }), | ||
| 43 | ); | ||
| 44 | ``` | ||
| 45 | |||
| 46 | Then in component: | ||
| 47 | |||
| 48 | ```ts | ||
| 49 | const result = useAtomValue(sourcesResultByWorkspace(workspaceId)); | ||
| 50 | ``` | ||
| 51 | |||
| 52 | No `useMemo` needed. | ||
| 53 | |||
| 54 | ### 2) Derived state atom for UI projection | ||
| 55 | |||
| 56 | Map `Result` once in atom-land: | ||
| 57 | |||
| 58 | ```ts | ||
| 59 | const sourcesByWorkspace = Atom.family((workspaceId: WorkspaceId) => | ||
| 60 | Atom.make((get) => { | ||
| 61 | const result = get(sourcesResultByWorkspace(workspaceId)); | ||
| 62 | return Result.match(result, { | ||
| 63 | onInitial: () => ({ state: "loading" as const, items: [] as const, message: null }), | ||
| 64 | onFailure: (failure) => ({ | ||
| 65 | state: "error" as const, | ||
| 66 | items: Option.getOrElse(Result.value(result), () => []), | ||
| 67 | message: Cause.pretty(failure.cause), | ||
| 68 | }), | ||
| 69 | onSuccess: (success) => ({ | ||
| 70 | state: success.waiting ? ("refreshing" as const) : ("ready" as const), | ||
| 71 | items: success.value, | ||
| 72 | message: null, | ||
| 73 | }), | ||
| 74 | }); | ||
| 75 | }), | ||
| 76 | ); | ||
| 77 | ``` | ||
| 78 | |||
| 79 | Components switch on `sources.state`, not raw result internals. | ||
| 80 | |||
| 81 | ### 3) Mutations as atoms | ||
| 82 | |||
| 83 | Define mutation atoms once: | ||
| 84 | |||
| 85 | ```ts | ||
| 86 | const upsertSource = controlPlaneClient.mutation("sources", "upsert"); | ||
| 87 | const removeSource = controlPlaneClient.mutation("sources", "remove"); | ||
| 88 | ``` | ||
| 89 | |||
| 90 | In components, execute with `useAtomSet(..., { mode: "promise" })`, then refresh the related query atom. | ||
| 91 | |||
| 92 | ## React hooks usage | ||
| 93 | |||
| 94 | - `useAtomValue(atom)` for reads. | ||
| 95 | - `useAtomSet(writableAtom, { mode: "promise" })` for mutation execution. | ||
| 96 | - `useAtomRefresh(atom)` for explicit refresh. | ||
| 97 | - `useAtomSuspense(...)` is preferred when route is Suspense-driven. | ||
| 98 | |||
| 99 | ## Error formatting | ||
| 100 | |||
| 101 | - Prefer `Cause.pretty(...)` for UI/debug text when surfacing failures. | ||
| 102 | - Keep raw `Cause` in atom-derived state when useful; do not stringify ad-hoc in JSX. | ||
| 103 | |||
| 104 | ## Internal references | ||
| 105 | |||
| 106 | - `@effect-atom/atom` d.ts: `dist/dts/Atom.d.ts`, `dist/dts/Result.d.ts`, `dist/dts/AtomHttpApi.d.ts` | ||
| 107 | - `@effect-atom/atom-react` d.ts: `dist/dts/Hooks.d.ts`, `dist/dts/RegistryContext.d.ts` | ||
| 108 | |||
| 109 | ## Anti-pattern checklist (reject in review) | ||
| 110 | |||
| 111 | - `useMemo(() => client.query(...), [deps])` | ||
| 112 | - `const isLoading = result._tag === "Initial" || ...` | ||
| 113 | - component-level `if/else` trees over raw `Result` in many places | ||
| 114 | - duplicating remote state into `useState` unless it is true form draft state | ||
| @@ -0,0 +1,189 @@ | |||
| 1 | :root { | ||
| 2 | --bg: #f2f0ea; | ||
| 3 | --panel: #fffdf8; | ||
| 4 | --ink: #1b1a16; | ||
| 5 | --muted: #6b675d; | ||
| 6 | --accent: #0a7f64; | ||
| 7 | --accent-ink: #f4fffc; | ||
| 8 | --border: rgba(27, 26, 22, 0.14); | ||
| 9 | --danger: #a22a2a; | ||
| 10 | } | ||
| 11 | |||
| 12 | * { | ||
| 13 | box-sizing: border-box; | ||
| 14 | } | ||
| 15 | |||
| 16 | html, | ||
| 17 | body { | ||
| 18 | margin: 0; | ||
| 19 | padding: 0; | ||
| 20 | min-height: 100%; | ||
| 21 | } | ||
| 22 | |||
| 23 | body { | ||
| 24 | font-family: var(--font-body), "IBM Plex Sans", sans-serif; | ||
| 25 | background: | ||
| 26 | radial-gradient(circle at 15% 20%, rgba(10, 127, 100, 0.22), transparent 45%), | ||
| 27 | radial-gradient(circle at 80% 10%, rgba(203, 166, 70, 0.17), transparent 36%), | ||
| 28 | linear-gradient(170deg, #ece9de 0%, #f6f4ee 52%, #e8e3d7 100%); | ||
| 29 | color: var(--ink); | ||
| 30 | } | ||
| 31 | |||
| 32 | a { | ||
| 33 | color: inherit; | ||
| 34 | } | ||
| 35 | |||
| 36 | main { | ||
| 37 | width: min(980px, calc(100% - 2.5rem)); | ||
| 38 | margin: 2.5rem auto 3.5rem; | ||
| 39 | } | ||
| 40 | |||
| 41 | .shell { | ||
| 42 | border: 1px solid var(--border); | ||
| 43 | border-radius: 18px; | ||
| 44 | background: var(--panel); | ||
| 45 | box-shadow: 0 20px 60px rgba(18, 16, 10, 0.08); | ||
| 46 | overflow: hidden; | ||
| 47 | } | ||
| 48 | |||
| 49 | .hero { | ||
| 50 | padding: 2rem 2rem 1rem; | ||
| 51 | border-bottom: 1px dashed var(--border); | ||
| 52 | } | ||
| 53 | |||
| 54 | .hero h1 { | ||
| 55 | margin: 0; | ||
| 56 | font-family: var(--font-display), "Fraunces", serif; | ||
| 57 | font-size: clamp(1.75rem, 3.1vw, 2.4rem); | ||
| 58 | line-height: 1.1; | ||
| 59 | } | ||
| 60 | |||
| 61 | .hero p { | ||
| 62 | margin: 0.75rem 0 0; | ||
| 63 | color: var(--muted); | ||
| 64 | max-width: 60ch; | ||
| 65 | } | ||
| 66 | |||
| 67 | .grid { | ||
| 68 | display: grid; | ||
| 69 | grid-template-columns: 1fr 1fr; | ||
| 70 | } | ||
| 71 | |||
| 72 | .card { | ||
| 73 | padding: 1.5rem 2rem 2rem; | ||
| 74 | } | ||
| 75 | |||
| 76 | .card + .card { | ||
| 77 | border-left: 1px solid var(--border); | ||
| 78 | } | ||
| 79 | |||
| 80 | h2 { | ||
| 81 | margin: 0 0 1rem; | ||
| 82 | font-family: var(--font-display), "Fraunces", serif; | ||
| 83 | font-size: 1.3rem; | ||
| 84 | } | ||
| 85 | |||
| 86 | .field { | ||
| 87 | margin-bottom: 0.95rem; | ||
| 88 | } | ||
| 89 | |||
| 90 | label { | ||
| 91 | display: block; | ||
| 92 | margin-bottom: 0.4rem; | ||
| 93 | color: var(--muted); | ||
| 94 | font-size: 0.9rem; | ||
| 95 | } | ||
| 96 | |||
| 97 | input, | ||
| 98 | select, | ||
| 99 | button { | ||
| 100 | width: 100%; | ||
| 101 | border-radius: 10px; | ||
| 102 | border: 1px solid var(--border); | ||
| 103 | font: inherit; | ||
| 104 | } | ||
| 105 | |||
| 106 | input, | ||
| 107 | select { | ||
| 108 | padding: 0.62rem 0.7rem; | ||
| 109 | background: #fff; | ||
| 110 | } | ||
| 111 | |||
| 112 | button { | ||
| 113 | padding: 0.7rem 0.8rem; | ||
| 114 | font-weight: 600; | ||
| 115 | cursor: pointer; | ||
| 116 | } | ||
| 117 | |||
| 118 | button.primary { | ||
| 119 | background: var(--accent); | ||
| 120 | color: var(--accent-ink); | ||
| 121 | border-color: transparent; | ||
| 122 | } | ||
| 123 | |||
| 124 | button.secondary { | ||
| 125 | background: transparent; | ||
| 126 | } | ||
| 127 | |||
| 128 | button.danger { | ||
| 129 | background: transparent; | ||
| 130 | color: var(--danger); | ||
| 131 | border-color: rgba(162, 42, 42, 0.35); | ||
| 132 | width: auto; | ||
| 133 | padding: 0.45rem 0.7rem; | ||
| 134 | } | ||
| 135 | |||
| 136 | .status { | ||
| 137 | margin-top: 0.7rem; | ||
| 138 | padding: 0.65rem 0.75rem; | ||
| 139 | border: 1px solid var(--border); | ||
| 140 | border-radius: 10px; | ||
| 141 | color: var(--muted); | ||
| 142 | font-size: 0.9rem; | ||
| 143 | } | ||
| 144 | |||
| 145 | .error { | ||
| 146 | color: var(--danger); | ||
| 147 | } | ||
| 148 | |||
| 149 | .list { | ||
| 150 | display: grid; | ||
| 151 | gap: 0.75rem; | ||
| 152 | } | ||
| 153 | |||
| 154 | .list-item { | ||
| 155 | border: 1px solid var(--border); | ||
| 156 | border-radius: 12px; | ||
| 157 | padding: 0.8rem; | ||
| 158 | background: rgba(255, 255, 255, 0.65); | ||
| 159 | } | ||
| 160 | |||
| 161 | .list-item header { | ||
| 162 | display: flex; | ||
| 163 | align-items: flex-start; | ||
| 164 | justify-content: space-between; | ||
| 165 | gap: 0.75rem; | ||
| 166 | } | ||
| 167 | |||
| 168 | .list-item strong { | ||
| 169 | display: block; | ||
| 170 | font-size: 1rem; | ||
| 171 | } | ||
| 172 | |||
| 173 | .meta { | ||
| 174 | margin-top: 0.2rem; | ||
| 175 | color: var(--muted); | ||
| 176 | font-size: 0.82rem; | ||
| 177 | word-break: break-all; | ||
| 178 | } | ||
| 179 | |||
| 180 | @media (max-width: 860px) { | ||
| 181 | .grid { | ||
| 182 | grid-template-columns: 1fr; | ||
| 183 | } | ||
| 184 | |||
| 185 | .card + .card { | ||
| 186 | border-left: 0; | ||
| 187 | border-top: 1px solid var(--border); | ||
| 188 | } | ||
| 189 | } | ||
| @@ -0,0 +1,37 @@ | |||
| 1 | import type { Metadata } from "next"; | ||
| 2 | import { Fraunces, IBM_Plex_Sans } from "next/font/google"; | ||
| 3 | import type { ReactNode } from "react"; | ||
| 4 | |||
| 5 | import { Providers } from "./providers"; | ||
| 6 | import "./globals.css"; | ||
| 7 | |||
| 8 | const bodyFont = IBM_Plex_Sans({ | ||
| 9 | subsets: ["latin"], | ||
| 10 | variable: "--font-body", | ||
| 11 | weight: ["400", "500", "600"], | ||
| 12 | }); | ||
| 13 | |||
| 14 | const displayFont = Fraunces({ | ||
| 15 | subsets: ["latin"], | ||
| 16 | variable: "--font-display", | ||
| 17 | weight: ["500", "700"], | ||
| 18 | }); | ||
| 19 | |||
| 20 | export const metadata: Metadata = { | ||
| 21 | title: "Executor v2 Control Plane", | ||
| 22 | description: "Basic Next.js frontend for Executor v2 control plane", | ||
| 23 | }; | ||
| 24 | |||
| 25 | type RootLayoutProps = { | ||
| 26 | children: ReactNode; | ||
| 27 | }; | ||
| 28 | |||
| 29 | const RootLayout = ({ children }: RootLayoutProps) => ( | ||
| 30 | <html lang="en" className={`${bodyFont.variable} ${displayFont.variable}`}> | ||
| 31 | <body> | ||
| 32 | <Providers>{children}</Providers> | ||
| 33 | </body> | ||
| 34 | </html> | ||
| 35 | ); | ||
| 36 | |||
| 37 | export default RootLayout; | ||
| @@ -0,0 +1,210 @@ | |||
| 1 | "use client"; | ||
| 2 | |||
| 3 | import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect-atom/atom-react"; | ||
| 4 | import type { UpsertSourcePayload } from "@executor-v2/control-plane"; | ||
| 5 | import type { SourceId, SourceKind, WorkspaceId } from "@executor-v2/schema"; | ||
| 6 | import { useEffect, useState, useTransition, type FormEvent } from "react"; | ||
| 7 | |||
| 8 | import { | ||
| 9 | removeSource, | ||
| 10 | sourcesByWorkspace, | ||
| 11 | sourcesLoading, | ||
| 12 | sourcesResultByWorkspace, | ||
| 13 | sourcesResultDisabled, | ||
| 14 | upsertSource, | ||
| 15 | } from "../lib/control-plane/atoms"; | ||
| 16 | |||
| 17 | const kindOptions: ReadonlyArray<SourceKind> = [ | ||
| 18 | "openapi", | ||
| 19 | "mcp", | ||
| 20 | "graphql", | ||
| 21 | "internal", | ||
| 22 | ]; | ||
| 23 | |||
| 24 | const Page = () => { | ||
| 25 | const [workspaceIdInput, setWorkspaceIdInput] = useState("ws_demo"); | ||
| 26 | const [name, setName] = useState("Weather API"); | ||
| 27 | const [kind, setKind] = useState<SourceKind>("openapi"); | ||
| 28 | const [endpoint, setEndpoint] = useState("https://example.com/openapi.json"); | ||
| 29 | const [statusText, setStatusText] = useState<string | null>(null); | ||
| 30 | const [isPending, startTransition] = useTransition(); | ||
| 31 | const [isClient, setIsClient] = useState(false); | ||
| 32 | |||
| 33 | useEffect(() => { | ||
| 34 | setIsClient(true); | ||
| 35 | }, []); | ||
| 36 | |||
| 37 | const workspaceId = workspaceIdInput as WorkspaceId; | ||
| 38 | |||
| 39 | const sources = useAtomValue( | ||
| 40 | isClient ? sourcesByWorkspace(workspaceId) : sourcesLoading, | ||
| 41 | ); | ||
| 42 | const refreshSources = useAtomRefresh( | ||
| 43 | isClient ? sourcesResultByWorkspace(workspaceId) : sourcesResultDisabled, | ||
| 44 | ); | ||
| 45 | |||
| 46 | const runUpsertSource = useAtomSet(upsertSource, { mode: "promise" }); | ||
| 47 | const runRemoveSource = useAtomSet(removeSource, { mode: "promise" }); | ||
| 48 | |||
| 49 | const handleAddSource = (event: FormEvent<HTMLFormElement>) => { | ||
| 50 | event.preventDefault(); | ||
| 51 | |||
| 52 | const payload: UpsertSourcePayload = { | ||
| 53 | name, | ||
| 54 | kind, | ||
| 55 | endpoint, | ||
| 56 | enabled: true, | ||
| 57 | configJson: "{}", | ||
| 58 | status: "draft", | ||
| 59 | sourceHash: null, | ||
| 60 | lastError: null, | ||
| 61 | }; | ||
| 62 | |||
| 63 | startTransition(() => { | ||
| 64 | void runUpsertSource({ | ||
| 65 | path: { workspaceId }, | ||
| 66 | payload, | ||
| 67 | }) | ||
| 68 | .then(() => { | ||
| 69 | setStatusText("Source saved."); | ||
| 70 | refreshSources(); | ||
| 71 | }) | ||
| 72 | .catch(() => { | ||
| 73 | setStatusText("Source save failed."); | ||
| 74 | }); | ||
| 75 | }); | ||
| 76 | }; | ||
| 77 | |||
| 78 | const handleRemoveSource = (sourceId: SourceId) => { | ||
| 79 | startTransition(() => { | ||
| 80 | void runRemoveSource({ | ||
| 81 | path: { workspaceId, sourceId }, | ||
| 82 | }) | ||
| 83 | .then(() => { | ||
| 84 | setStatusText("Source removed."); | ||
| 85 | refreshSources(); | ||
| 86 | }) | ||
| 87 | .catch(() => { | ||
| 88 | setStatusText("Source removal failed."); | ||
| 89 | }); | ||
| 90 | }); | ||
| 91 | }; | ||
| 92 | |||
| 93 | return ( | ||
| 94 | <main> | ||
| 95 | <section className="shell"> | ||
| 96 | <header className="hero"> | ||
| 97 | <h1>Executor v2 Control Plane</h1> | ||
| 98 | <p> | ||
| 99 | Basic Next.js frontend wired to the shared Effect HttpApi client via | ||
| 100 | Effect Atom. | ||
| 101 | </p> | ||
| 102 | </header> | ||
| 103 | |||
| 104 | <div className="grid"> | ||
| 105 | <section className="card"> | ||
| 106 | <h2>Workspace + Source</h2> | ||
| 107 | <form onSubmit={handleAddSource}> | ||
| 108 | <div className="field"> | ||
| 109 | <label htmlFor="workspace-id">Workspace ID</label> | ||
| 110 | <input | ||
| 111 | id="workspace-id" | ||
| 112 | value={workspaceIdInput} | ||
| 113 | onChange={(event) => setWorkspaceIdInput(event.target.value)} | ||
| 114 | required | ||
| 115 | /> | ||
| 116 | </div> | ||
| 117 | |||
| 118 | <div className="field"> | ||
| 119 | <label htmlFor="source-name">Name</label> | ||
| 120 | <input | ||
| 121 | id="source-name" | ||
| 122 | value={name} | ||
| 123 | onChange={(event) => setName(event.target.value)} | ||
| 124 | required | ||
| 125 | /> | ||
| 126 | </div> | ||
| 127 | |||
| 128 | <div className="field"> | ||
| 129 | <label htmlFor="source-kind">Kind</label> | ||
| 130 | <select | ||
| 131 | id="source-kind" | ||
| 132 | value={kind} | ||
| 133 | onChange={(event) => setKind(event.target.value as SourceKind)} | ||
| 134 | > | ||
| 135 | {kindOptions.map((option) => ( | ||
| 136 | <option key={option} value={option}> | ||
| 137 | {option} | ||
| 138 | </option> | ||
| 139 | ))} | ||
| 140 | </select> | ||
| 141 | </div> | ||
| 142 | |||
| 143 | <div className="field"> | ||
| 144 | <label htmlFor="source-endpoint">Endpoint</label> | ||
| 145 | <input | ||
| 146 | id="source-endpoint" | ||
| 147 | value={endpoint} | ||
| 148 | onChange={(event) => setEndpoint(event.target.value)} | ||
| 149 | required | ||
| 150 | /> | ||
| 151 | </div> | ||
| 152 | |||
| 153 | <button className="primary" type="submit" disabled={isPending}> | ||
| 154 | {isPending ? "Saving..." : "Save Source"} | ||
| 155 | </button> | ||
| 156 | </form> | ||
| 157 | |||
| 158 | {statusText ? <p className="status">{statusText}</p> : null} | ||
| 159 | </section> | ||
| 160 | |||
| 161 | <section className="card"> | ||
| 162 | <h2>Sources</h2> | ||
| 163 | |||
| 164 | {sources.state === "loading" ? ( | ||
| 165 | <p className="status">Loading sources...</p> | ||
| 166 | ) : null} | ||
| 167 | |||
| 168 | {sources.state === "refreshing" ? ( | ||
| 169 | <p className="status">Refreshing sources...</p> | ||
| 170 | ) : null} | ||
| 171 | |||
| 172 | {sources.state === "error" ? ( | ||
| 173 | <p className="status error">{sources.message}</p> | ||
| 174 | ) : null} | ||
| 175 | |||
| 176 | {sources.state !== "loading" && sources.items.length === 0 ? ( | ||
| 177 | <p className="status">No sources yet in this workspace.</p> | ||
| 178 | ) : null} | ||
| 179 | |||
| 180 | <div className="list"> | ||
| 181 | {sources.items.map((source) => ( | ||
| 182 | <article className="list-item" key={source.id}> | ||
| 183 | <header> | ||
| 184 | <div> | ||
| 185 | <strong>{source.name}</strong> | ||
| 186 | <div className="meta"> | ||
| 187 | {source.kind} · {source.status} | ||
| 188 | </div> | ||
| 189 | <div className="meta">{source.endpoint}</div> | ||
| 190 | </div> | ||
| 191 | <button | ||
| 192 | className="danger" | ||
| 193 | type="button" | ||
| 194 | onClick={() => handleRemoveSource(source.id)} | ||
| 195 | disabled={isPending} | ||
| 196 | > | ||
| 197 | Remove | ||
| 198 | </button> | ||
| 199 | </header> | ||
| 200 | </article> | ||
| 201 | ))} | ||
| 202 | </div> | ||
| 203 | </section> | ||
| 204 | </div> | ||
| 205 | </section> | ||
| 206 | </main> | ||
| 207 | ); | ||
| 208 | }; | ||
| 209 | |||
| 210 | export default Page; | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | "use client"; | ||
| 2 | |||
| 3 | import { RegistryProvider } from "@effect-atom/atom-react"; | ||
| 4 | import type { ReactNode } from "react"; | ||
| 5 | |||
| 6 | type ProvidersProps = { | ||
| 7 | children: ReactNode; | ||
| 8 | }; | ||
| 9 | |||
| 10 | export const Providers = ({ children }: ProvidersProps) => ( | ||
| 11 | <RegistryProvider>{children}</RegistryProvider> | ||
| 12 | ); | ||
| @@ -0,0 +1,73 @@ | |||
| 1 | import { Atom, Result } from "@effect-atom/atom"; | ||
| 2 | import type { Source, WorkspaceId } from "@executor-v2/schema"; | ||
| 3 | import * as Cause from "effect/Cause"; | ||
| 4 | import * as Option from "effect/Option"; | ||
| 5 | |||
| 6 | import { controlPlaneClient } from "./client"; | ||
| 7 | |||
| 8 | type SourcesResult = Result.Result<ReadonlyArray<Source>, unknown>; | ||
| 9 | |||
| 10 | const emptySources: ReadonlyArray<Source> = []; | ||
| 11 | |||
| 12 | export const sourcesResultDisabled = Atom.make( | ||
| 13 | Result.initial<ReadonlyArray<Source>, unknown>(), | ||
| 14 | ); | ||
| 15 | |||
| 16 | export const sourcesResultByWorkspace = Atom.family( | ||
| 17 | (workspaceId: WorkspaceId): Atom.Atom<SourcesResult> => | ||
| 18 | controlPlaneClient.query("sources", "list", { | ||
| 19 | path: { | ||
| 20 | workspaceId, | ||
| 21 | }, | ||
| 22 | }) as Atom.Atom<SourcesResult>, | ||
| 23 | ); | ||
| 24 | |||
| 25 | export const upsertSource = controlPlaneClient.mutation("sources", "upsert"); | ||
| 26 | export const removeSource = controlPlaneClient.mutation("sources", "remove"); | ||
| 27 | |||
| 28 | export type SourcesState = | ||
| 29 | | { | ||
| 30 | state: "loading"; | ||
| 31 | items: ReadonlyArray<Source>; | ||
| 32 | message: null; | ||
| 33 | } | ||
| 34 | | { | ||
| 35 | state: "error"; | ||
| 36 | items: ReadonlyArray<Source>; | ||
| 37 | message: string; | ||
| 38 | } | ||
| 39 | | { | ||
| 40 | state: "ready" | "refreshing"; | ||
| 41 | items: ReadonlyArray<Source>; | ||
| 42 | message: null; | ||
| 43 | }; | ||
| 44 | |||
| 45 | export const sourcesLoading = Atom.make<SourcesState>({ | ||
| 46 | state: "loading", | ||
| 47 | items: emptySources, | ||
| 48 | message: null, | ||
| 49 | }); | ||
| 50 | |||
| 51 | export const sourcesByWorkspace = Atom.family((workspaceId: WorkspaceId) => | ||
| 52 | Atom.make((get): SourcesState => { | ||
| 53 | const result = get(sourcesResultByWorkspace(workspaceId)); | ||
| 54 | |||
| 55 | return Result.match(result, { | ||
| 56 | onInitial: () => ({ | ||
| 57 | state: "loading", | ||
| 58 | items: emptySources, | ||
| 59 | message: null, | ||
| 60 | }), | ||
| 61 | onFailure: (failure) => ({ | ||
| 62 | state: "error", | ||
| 63 | items: Option.getOrElse(Result.value(result), () => emptySources), | ||
| 64 | message: Cause.pretty(failure.cause), | ||
| 65 | }), | ||
| 66 | onSuccess: (success) => ({ | ||
| 67 | state: success.waiting ? "refreshing" : "ready", | ||
| 68 | items: success.value, | ||
| 69 | message: null, | ||
| 70 | }), | ||
| 71 | }); | ||
| 72 | }), | ||
| 73 | ); | ||
| @@ -0,0 +1,5 @@ | |||
| 1 | import { createControlPlaneAtomClient } from "@executor-v2/control-plane"; | ||
| 2 | |||
| 3 | export const controlPlaneClient = createControlPlaneAtomClient({ | ||
| 4 | baseUrl: process.env.NEXT_PUBLIC_CONTROL_PLANE_BASE_URL ?? "/api/control-plane", | ||
| 5 | }); | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | /// <reference types="next" /> | ||
| 2 | /// <reference types="next/image-types/global" /> | ||
| 3 | /// <reference path="./.next/types/routes.d.ts" /> | ||
| 4 | |||
| 5 | // NOTE: This file should not be edited | ||
| 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | import { dirname, resolve } from "node:path"; | ||
| 2 | import { fileURLToPath } from "node:url"; | ||
| 3 | import type { NextConfig } from "next"; | ||
| 4 | |||
| 5 | const appDir = dirname(fileURLToPath(import.meta.url)); | ||
| 6 | |||
| 7 | const controlPlaneUpstream = | ||
| 8 | process.env.CONTROL_PLANE_UPSTREAM_URL ?? "http://127.0.0.1:8787"; | ||
| 9 | |||
| 10 | const nextConfig: NextConfig = { | ||
| 11 | outputFileTracingRoot: resolve(appDir, "../.."), | ||
| 12 | async rewrites() { | ||
| 13 | return [ | ||
| 14 | { | ||
| 15 | source: "/api/control-plane/:path*", | ||
| 16 | destination: `${controlPlaneUpstream}/:path*`, | ||
| 17 | }, | ||
| 18 | ]; | ||
| 19 | }, | ||
| 20 | }; | ||
| 21 | |||
| 22 | export default nextConfig; | ||
| @@ -3,6 +3,23 @@ · 2 unmodified lines | |||
| 3 | "private": true, | 3 | "private": true, |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "scripts": { | 5 | "scripts": { |
| 6 | "dev": "next dev --port 3000", | ||
| 7 | "build": "next build", | ||
| 8 | "start": "next start --port 3000", | ||
| 6 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" |
| 10 | }, | ||
| 11 | "dependencies": { | ||
| 12 | "@effect-atom/atom": "^0.4.11", | ||
| 13 | "@effect-atom/atom-react": "^0.4.6", | ||
| 14 | "@executor-v2/control-plane": "workspace:*", | ||
| 15 | "@executor-v2/schema": "workspace:*", | ||
| 16 | "effect": "catalog:", | ||
| 17 | "next": "^15.4.6", | ||
| 18 | "react": "^19.1.1", | ||
| 19 | "react-dom": "^19.1.1" | ||
| 20 | }, | ||
| 21 | "devDependencies": { | ||
| 22 | "@types/react": "^19.1.10", | ||
| 23 | "@types/react-dom": "^19.1.7" | ||
| 7 | } | 24 | } |
| 8 | } | 25 | } |
| @@ -1 +0,0 @@ | |||
| 1 | export {}; | ||
| @@ -1,4 +1,34 @@ | |||
| 1 | { | 1 | { |
| 2 | "extends": "../../tsconfig.json", | 2 | "extends": "../../tsconfig.json", |
| 3 | "include": ["src"] | 3 | "compilerOptions": { |
| 4 | "lib": [ | ||
| 5 | "dom", | ||
| 6 | "dom.iterable", | ||
| 7 | "esnext" | ||
| 8 | ], | ||
| 9 | "jsx": "preserve", | ||
| 10 | "incremental": true, | ||
| 11 | "plugins": [ | ||
| 12 | { | ||
| 13 | "name": "next" | ||
| 14 | }, | ||
| 15 | { | ||
| 16 | "name": "@effect/language-service" | ||
| 17 | } | ||
| 18 | ], | ||
| 19 | "types": [ | ||
| 20 | "node" | ||
| 21 | ], | ||
| 22 | "allowJs": true, | ||
| 23 | "esModuleInterop": true | ||
| 24 | }, | ||
| 25 | "include": [ | ||
| 26 | "next-env.d.ts", | ||
| 27 | "**/*.ts", | ||
| 28 | "**/*.tsx", | ||
| 29 | ".next/types/**/*.ts" | ||
| 30 | ], | ||
| 31 | "exclude": [ | ||
| 32 | "node_modules" | ||
| 33 | ] | ||
| 4 | } | 34 | } |
| @@ -11,6 +11,7 @@ · 10 unmodified lines | |||
| 11 | "@effect/language-service": "^0.76.0", | 11 | "@effect/language-service": "^0.76.0", |
| 12 | "@types/bun": "^1.2.22", | 12 | "@types/bun": "^1.2.22", |
| 13 | "@types/node": "catalog:", | 13 | "@types/node": "catalog:", |
| 14 | "turbo": "^2.8.11", | ||
| 14 | "typescript": "catalog:", | 15 | "typescript": "catalog:", |
| 15 | }, | 16 | }, |
| 16 | }, | 17 | }, |
| @@ -54,6 +55,20 @@ · 37 unmodified lines | |||
| 54 | }, | 55 | }, |
| 55 | "apps/web": { | 56 | "apps/web": { |
| 56 | "name": "@executor-v2/app-web", | 57 | "name": "@executor-v2/app-web", |
| 58 | "dependencies": { | ||
| 59 | "@effect-atom/atom": "^0.4.11", | ||
| 60 | "@effect-atom/atom-react": "^0.4.6", | ||
| 61 | "@executor-v2/control-plane": "workspace:*", | ||
| 62 | "@executor-v2/schema": "workspace:*", | ||
| 63 | "effect": "catalog:", | ||
| 64 | "next": "^15.4.6", | ||
| 65 | "react": "^19.1.1", | ||
| 66 | "react-dom": "^19.1.1", | ||
| 67 | }, | ||
| 68 | "devDependencies": { | ||
| 69 | "@types/react": "^19.1.10", | ||
| 70 | "@types/react-dom": "^19.1.7", | ||
| 71 | }, | ||
| 57 | }, | 72 | }, |
| 58 | "packages/ai-sdk-adapter": { | 73 | "packages/ai-sdk-adapter": { |
| 59 | "name": "@executor-v2/ai-sdk-adapter", | 74 | "name": "@executor-v2/ai-sdk-adapter", |
| @@ -408,23 +423,23 @@ · 348 unmodified lines | |||
| 408 | 423 | ||
| 409 | "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], | 424 | "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], |
| 410 | 425 | ||
| 411 | "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], | 426 | "@next/env": ["@next/env@15.5.12", "", {}, "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg=="], |
| 412 | 427 | ||
| 413 | "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="], | 428 | "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg=="], |
| 414 | 429 | ||
| 415 | "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="], | 430 | "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA=="], |
| 416 | 431 | ||
| 417 | "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="], | 432 | "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw=="], |
| 418 | 433 | ||
| 419 | "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="], | 434 | "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw=="], |
| 420 | 435 | ||
| 421 | "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="], | 436 | "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw=="], |
| 422 | 437 | ||
| 423 | "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="], | 438 | "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w=="], |
| 424 | 439 | ||
| 425 | "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="], | 440 | "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg=="], |
| 426 | 441 | ||
| 427 | "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="], | 442 | "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.12", "", { "os": "win32", "cpu": "x64" }, "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw=="], |
| 428 | 443 | ||
| 429 | "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], | 444 | "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], |
| 430 | 445 | ||
| @@ -612,6 +627,10 @@ · 181 unmodified lines | |||
| 612 | 627 | ||
| 613 | "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], | 628 | "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], |
| 614 | 629 | ||
| 630 | "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], | ||
| 631 | |||
| 632 | "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], | ||
| 633 | |||
| 615 | "@vercel/backends": ["@vercel/backends@0.0.37", "", { "dependencies": { "@vercel/build-utils": "13.5.0", "@vercel/nft": "1.3.0", "execa": "3.2.0", "fs-extra": "11.1.0", "oxc-transform": "0.111.0", "path-to-regexp": "8.3.0", "resolve.exports": "2.0.3", "rolldown": "1.0.0-rc.1", "srvx": "0.8.9", "tsx": "4.21.0", "zod": "3.22.4" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" } }, "sha512-RSUWaDIlcfv6Q2B34Lce77RhB5lsG0bTz6rOrPAB1ZzkUNpDVlmobGxIZTgSD3MHmJGJCP8MBjM2zqG9jY3XYQ=="], | 634 | "@vercel/backends": ["@vercel/backends@0.0.37", "", { "dependencies": { "@vercel/build-utils": "13.5.0", "@vercel/nft": "1.3.0", "execa": "3.2.0", "fs-extra": "11.1.0", "oxc-transform": "0.111.0", "path-to-regexp": "8.3.0", "resolve.exports": "2.0.3", "rolldown": "1.0.0-rc.1", "srvx": "0.8.9", "tsx": "4.21.0", "zod": "3.22.4" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" } }, "sha512-RSUWaDIlcfv6Q2B34Lce77RhB5lsG0bTz6rOrPAB1ZzkUNpDVlmobGxIZTgSD3MHmJGJCP8MBjM2zqG9jY3XYQ=="], |
| 616 | 635 | ||
| 617 | "@vercel/blob": ["@vercel/blob@2.3.0", "", { "dependencies": { "async-retry": "^1.3.3", "is-buffer": "^2.0.5", "is-node-process": "^1.2.0", "throttleit": "^2.1.0", "undici": "^6.23.0" } }, "sha512-oYWiJbWRQ7gz9Mj0X/NHFJ3OcLMOBzq/2b3j6zeNrQmtFo6dHwU8FAwNpxVIYddVMd+g8eqEi7iRueYx8FtM0Q=="], | 636 | "@vercel/blob": ["@vercel/blob@2.3.0", "", { "dependencies": { "async-retry": "^1.3.3", "is-buffer": "^2.0.5", "is-node-process": "^1.2.0", "throttleit": "^2.1.0", "undici": "^6.23.0" } }, "sha512-oYWiJbWRQ7gz9Mj0X/NHFJ3OcLMOBzq/2b3j6zeNrQmtFo6dHwU8FAwNpxVIYddVMd+g8eqEi7iRueYx8FtM0Q=="], |
| @@ -788,6 +807,8 @@ · 170 unmodified lines | |||
| 788 | 807 | ||
| 789 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], | 808 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], |
| 790 | 809 | ||
| 810 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], | ||
| 811 | |||
| 791 | "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], | 812 | "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], |
| 792 | 813 | ||
| 793 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], | 814 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], |
| @@ -1018,7 +1039,7 @@ · 224 unmodified lines | |||
| 1018 | 1039 | ||
| 1019 | "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], | 1040 | "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], |
| 1020 | 1041 | ||
| 1021 | "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], | 1042 | "next": ["next@15.5.12", "", { "dependencies": { "@next/env": "15.5.12", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.12", "@next/swc-darwin-x64": "15.5.12", "@next/swc-linux-arm64-gnu": "15.5.12", "@next/swc-linux-arm64-musl": "15.5.12", "@next/swc-linux-x64-gnu": "15.5.12", "@next/swc-linux-x64-musl": "15.5.12", "@next/swc-win32-arm64-msvc": "15.5.12", "@next/swc-win32-x64-msvc": "15.5.12", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA=="], |
| 1022 | 1043 | ||
| 1023 | "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], | 1044 | "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], |
| 1024 | 1045 | ||
| @@ -1232,6 +1253,20 @@ · 207 unmodified lines | |||
| 1232 | 1253 | ||
| 1233 | "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], | 1254 | "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], |
| 1234 | 1255 | ||
| 1256 | "turbo": ["turbo@2.8.11", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.11", "turbo-darwin-arm64": "2.8.11", "turbo-linux-64": "2.8.11", "turbo-linux-arm64": "2.8.11", "turbo-windows-64": "2.8.11", "turbo-windows-arm64": "2.8.11" }, "bin": { "turbo": "bin/turbo" } }, "sha512-H+rwSHHPLoyPOSoHdmI1zY0zy0GGj1Dmr7SeJW+nZiWLz2nex8EJ+fkdVabxXFMNEux+aywI4Sae8EqhmnOv4A=="], | ||
| 1257 | |||
| 1258 | "turbo-darwin-64": ["turbo-darwin-64@2.8.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-XKaCWaz4OCt77oYYvGCIRpvYD4c/aNaKjRkUpv+e8rN3RZb+5Xsyew4yRO+gaHdMIUhQznXNXfHlhs+/p7lIhA=="], | ||
| 1259 | |||
| 1260 | "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VvynLHGUNvQ9k7GZjRPSsRcK4VkioTfFb7O7liAk4nHKjEcMdls7GqxzjVWgJiKz3hWmQGaP9hRa9UUnhVWCxA=="], | ||
| 1261 | |||
| 1262 | "turbo-linux-64": ["turbo-linux-64@2.8.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cbSn37dcm+EmkQ7DD0euy7xV7o2el4GAOr1XujvkAyKjjNvQ+6QIUeDgQcwAx3D17zPpDvfDMJY2dLQadWnkmQ=="], | ||
| 1263 | |||
| 1264 | "turbo-linux-arm64": ["turbo-linux-arm64@2.8.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-+trymp2s2aBrhS04l6qFxcExzZ8ffndevuUB9c5RCeqsVpZeiWuGQlWNm5XjOmzoMayxRARZ5ma7yiWbGMiLqQ=="], | ||
| 1265 | |||
| 1266 | "turbo-windows-64": ["turbo-windows-64@2.8.11", "", { "os": "win32", "cpu": "x64" }, "sha512-3kJjFSM4yw1n9Uzmi+XkAUgCae19l/bH6RJ442xo7mnZm0tpOjo33F+FYHoSVpIWVMd0HG0LDccyafPSdylQbA=="], | ||
| 1267 | |||
| 1268 | "turbo-windows-arm64": ["turbo-windows-arm64@2.8.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-JOM4uF2vuLsJUvibdR6X9QqdZr6BhC6Nhlrw4LKFPsXZZI/9HHLoqAiYRpE4MuzIwldCH/jVySnWXrI1SKto0g=="], | ||
| 1269 | |||
| 1235 | "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], | 1270 | "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], |
| 1236 | 1271 | ||
| 1237 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], | 1272 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], |
| @@ -1292,6 +1327,8 @@ · 54 unmodified lines | |||
| 1292 | 1327 | ||
| 1293 | "@effect/platform-node-shared/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], | 1328 | "@effect/platform-node-shared/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], |
| 1294 | 1329 | ||
| 1330 | "@executor-v2/confect/next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], | ||
| 1331 | |||
| 1295 | "@mapbox/node-pre-gyp/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], | 1332 | "@mapbox/node-pre-gyp/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], |
| 1296 | 1333 | ||
| 1297 | "@mapbox/node-pre-gyp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], | 1334 | "@mapbox/node-pre-gyp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], |
| @@ -1388,6 +1425,24 @@ · 90 unmodified lines | |||
| 1388 | 1425 | ||
| 1389 | "vite-node/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], | 1426 | "vite-node/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], |
| 1390 | 1427 | ||
| 1428 | "@executor-v2/confect/next/@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], | ||
| 1429 | |||
| 1430 | "@executor-v2/confect/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="], | ||
| 1431 | |||
| 1432 | "@executor-v2/confect/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="], | ||
| 1433 | |||
| 1434 | "@executor-v2/confect/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="], | ||
| 1435 | |||
| 1436 | "@executor-v2/confect/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="], | ||
| 1437 | |||
| 1438 | "@executor-v2/confect/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="], | ||
| 1439 | |||
| 1440 | "@executor-v2/confect/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="], | ||
| 1441 | |||
| 1442 | "@executor-v2/confect/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="], | ||
| 1443 | |||
| 1444 | "@executor-v2/confect/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="], | ||
| 1445 | |||
| 1391 | "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], | 1446 | "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], |
| 1392 | 1447 | ||
| 1393 | "@vercel/express/@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], | 1448 | "@vercel/express/@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], |
| @@ -18,6 +18,7 @@ · 17 unmodified lines | |||
| 18 | }, | 18 | }, |
| 19 | "scripts": { | 19 | "scripts": { |
| 20 | "prepare": "effect-language-service patch", | 20 | "prepare": "effect-language-service patch", |
| 21 | "dev": "turbo run dev --parallel --filter=@executor-v2/app-pm --filter=@executor-v2/app-convex --filter=@executor-v2/app-web", | ||
| 21 | "typecheck": "tsc --noEmit", | 22 | "typecheck": "tsc --noEmit", |
| 22 | "knip": "bunx knip --config knip.json" | 23 | "knip": "bunx knip --config knip.json" |
| 23 | }, | 24 | }, |
| @@ -25,6 +26,7 @@ · 1 unmodified line | |||
| 25 | "@effect/language-service": "^0.76.0", | 26 | "@effect/language-service": "^0.76.0", |
| 26 | "@types/bun": "^1.2.22", | 27 | "@types/bun": "^1.2.22", |
| 27 | "@types/node": "catalog:", | 28 | "@types/node": "catalog:", |
| 29 | "turbo": "^2.8.11", | ||
| 28 | "typescript": "catalog:" | 30 | "typescript": "catalog:" |
| 29 | }, | 31 | }, |
| 30 | "dependencies": { | 32 | "dependencies": { |
| @@ -1,24 +1,16 @@ | |||
| 1 | import { Atom, Result } from "@effect-atom/atom"; | 1 | import { AtomHttpApi } from "@effect-atom/atom"; |
| 2 | import { | 2 | import { |
| 3 | FetchHttpClient, | 3 | FetchHttpClient, |
| 4 | HttpApiClient, | 4 | HttpApiClient, |
| 5 | HttpApiError, | 5 | HttpApiError, |
| 6 | HttpClientError, | 6 | HttpClientError, |
| 7 | } from "@effect/platform"; | 7 | } from "@effect/platform"; |
| 8 | import type { | ||
| 9 | Source, | ||
| 10 | SourceId, | ||
| 11 | WorkspaceId, | ||
| 12 | } from "@executor-v2/schema"; | ||
| 13 | import * as Effect from "effect/Effect"; | ||
| 14 | import * as ParseResult from "effect/ParseResult"; | 8 | import * as ParseResult from "effect/ParseResult"; |
| 15 | 9 | ||
| 16 | import { | 10 | import { |
| 17 | ControlPlaneApi, | 11 | ControlPlaneApi, |
| 18 | type ControlPlaneBadRequestError, | 12 | type ControlPlaneBadRequestError, |
| 19 | type ControlPlaneStorageError, | 13 | type ControlPlaneStorageError, |
| 20 | type RemoveSourceResult, | ||
| 21 | type UpsertSourcePayload, | ||
| 22 | } from "./api"; | 14 | } from "./api"; |
| 23 | 15 | ||
| 24 | export type ControlPlaneClientOptions = { | 16 | export type ControlPlaneClientOptions = { |
| @@ -32,128 +24,16 @@ export type ControlPlaneClientError = · 7 unmodified lines | |||
| 32 | | HttpClientError.HttpClientError | 24 | | HttpClientError.HttpClientError |
| 33 | | ParseResult.ParseError; | 25 | | ParseResult.ParseError; |
| 34 | 26 | ||
| 35 | export type ListSourcesRequest = { | ||
| 36 | workspaceId: WorkspaceId; | ||
| 37 | }; | ||
| 38 | |||
| 39 | export type UpsertSourceRequest = { | ||
| 40 | workspaceId: WorkspaceId; | ||
| 41 | payload: UpsertSourcePayload; | ||
| 42 | }; | ||
| 43 | |||
| 44 | export type RemoveSourceRequest = { | ||
| 45 | workspaceId: WorkspaceId; | ||
| 46 | sourceId: SourceId; | ||
| 47 | }; | ||
| 48 | |||
| 49 | export type ControlPlaneReadOptions = { | ||
| 50 | enabled?: boolean; | ||
| 51 | }; | ||
| 52 | |||
| 53 | export const makeControlPlaneClient = (options: ControlPlaneClientOptions) => | 27 | export const makeControlPlaneClient = (options: ControlPlaneClientOptions) => |
| 54 | HttpApiClient.make(ControlPlaneApi, { | 28 | HttpApiClient.make(ControlPlaneApi, { |
| 55 | baseUrl: options.baseUrl, | 29 | baseUrl: options.baseUrl, |
| 56 | }); | 30 | }); |
| 57 | 31 | ||
| 58 | const isReadEnabled = (options?: ControlPlaneReadOptions): boolean => | 32 | export const createControlPlaneAtomClient = (options: ControlPlaneClientOptions) => |
| 59 | options?.enabled ?? true; | 33 | AtomHttpApi.Tag<unknown>()("@executor-v2/control-plane/AtomClient", { |
| 60 | 34 | api: ControlPlaneApi, | |
| 61 | const makeDisabledResultAtom = <Success, Error>() => | 35 | httpClient: FetchHttpClient.layer, |
| 62 | Atom.make(Result.initial<Success, Error>()); | 36 | baseUrl: options.baseUrl, |
| 63 | 37 | }); | |
| 64 | export const createControlPlaneAtomClient = ( | ||
| 65 | options: ControlPlaneClientOptions, | ||
| 66 | ) => { | ||
| 67 | const baseLayer = FetchHttpClient.layer; | ||
| 68 | const runtime = Atom.runtime(baseLayer); | ||
| 69 | |||
| 70 | const listSourcesEffect = ( | ||
| 71 | input: ListSourcesRequest, | ||
| 72 | ): Effect.Effect<ReadonlyArray<Source>, ControlPlaneClientError, never> => | ||
| 73 | Effect.gen(function* () { | ||
| 74 | const client = yield* makeControlPlaneClient(options); | ||
| 75 | return yield* client.sources.list({ | ||
| 76 | path: { | ||
| 77 | workspaceId: input.workspaceId, | ||
| 78 | }, | ||
| 79 | }); | ||
| 80 | }).pipe(Effect.provide(baseLayer)); | ||
| 81 | |||
| 82 | const upsertSourceEffect = ( | ||
| 83 | input: UpsertSourceRequest, | ||
| 84 | ): Effect.Effect<Source, ControlPlaneClientError, never> => | ||
| 85 | Effect.gen(function* () { | ||
| 86 | const client = yield* makeControlPlaneClient(options); | ||
| 87 | return yield* client.sources.upsert({ | ||
| 88 | path: { | ||
| 89 | workspaceId: input.workspaceId, | ||
| 90 | }, | ||
| 91 | payload: input.payload, | ||
| 92 | }); | ||
| 93 | }).pipe(Effect.provide(baseLayer)); | ||
| 94 | |||
| 95 | const removeSourceEffect = ( | ||
| 96 | input: RemoveSourceRequest, | ||
| 97 | ): Effect.Effect<RemoveSourceResult, ControlPlaneClientError, never> => | ||
| 98 | Effect.gen(function* () { | ||
| 99 | const client = yield* makeControlPlaneClient(options); | ||
| 100 | return yield* client.sources.remove({ | ||
| 101 | path: { | ||
| 102 | workspaceId: input.workspaceId, | ||
| 103 | sourceId: input.sourceId, | ||
| 104 | }, | ||
| 105 | }); | ||
| 106 | }).pipe(Effect.provide(baseLayer)); | ||
| 107 | |||
| 108 | const listSourcesFamily = Atom.family((workspaceId: WorkspaceId) => | ||
| 109 | runtime.atom( | ||
| 110 | listSourcesEffect({ | ||
| 111 | workspaceId, | ||
| 112 | }), | ||
| 113 | ), | ||
| 114 | ); | ||
| 115 | |||
| 116 | const listDisabledAtom = makeDisabledResultAtom< | ||
| 117 | ReadonlyArray<Source>, | ||
| 118 | ControlPlaneClientError | ||
| 119 | >(); | ||
| 120 | |||
| 121 | const upsertSourceMutate = runtime.fn<UpsertSourceRequest>()( | ||
| 122 | Effect.fnUntraced(function* (input) { | ||
| 123 | return yield* upsertSourceEffect(input); | ||
| 124 | }), | ||
| 125 | ); | ||
| 126 | |||
| 127 | const removeSourceMutate = runtime.fn<RemoveSourceRequest>()( | ||
| 128 | Effect.fnUntraced(function* (input) { | ||
| 129 | return yield* removeSourceEffect(input); | ||
| 130 | }), | ||
| 131 | ); | ||
| 132 | 38 | ||
| 133 | return { | 39 | export type ControlPlaneAtomClient = ReturnType<typeof createControlPlaneAtomClient>; |
| 134 | runtime, | ||
| 135 | sources: { | ||
| 136 | list: { | ||
| 137 | query: (input: ListSourcesRequest, readOptions?: ControlPlaneReadOptions) => | ||
| 138 | isReadEnabled(readOptions) | ||
| 139 | ? listSourcesFamily(input.workspaceId) | ||
| 140 | : listDisabledAtom, | ||
| 141 | queryEffect: listSourcesEffect, | ||
| 142 | queryPromise: (input: ListSourcesRequest) => | ||
| 143 | Effect.runPromise(listSourcesEffect(input)), | ||
| 144 | }, | ||
| 145 | upsert: { | ||
| 146 | mutate: upsertSourceMutate, | ||
| 147 | mutateEffect: upsertSourceEffect, | ||
| 148 | mutatePromise: (input: UpsertSourceRequest) => | ||
| 149 | Effect.runPromise(upsertSourceEffect(input)), | ||
| 150 | }, | ||
| 151 | remove: { | ||
| 152 | mutate: removeSourceMutate, | ||
| 153 | mutateEffect: removeSourceEffect, | ||
| 154 | mutatePromise: (input: RemoveSourceRequest) => | ||
| 155 | Effect.runPromise(removeSourceEffect(input)), | ||
| 156 | }, | ||
| 157 | }, | ||
| 158 | }; | ||
| 159 | }; | ||
| @@ -35,10 +35,7 @@ export { · 34 unmodified lines | |||
| 35 | export { | 35 | export { |
| 36 | createControlPlaneAtomClient, | 36 | createControlPlaneAtomClient, |
| 37 | makeControlPlaneClient, | 37 | makeControlPlaneClient, |
| 38 | type ControlPlaneAtomClient, | ||
| 38 | type ControlPlaneClientError, | 39 | type ControlPlaneClientError, |
| 39 | type ControlPlaneClientOptions, | 40 | type ControlPlaneClientOptions, |
| 40 | type ControlPlaneReadOptions, | ||
| 41 | type ListSourcesRequest, | ||
| 42 | type RemoveSourceRequest, | ||
| 43 | type UpsertSourceRequest, | ||
| 44 | } from "./client"; | 41 | } from "./client"; |
| @@ -4,6 +4,7 @@ · 3 unmodified lines | |||
| 4 | "module": "ESNext", | 4 | "module": "ESNext", |
| 5 | "moduleResolution": "Bundler", | 5 | "moduleResolution": "Bundler", |
| 6 | "moduleDetection": "force", | 6 | "moduleDetection": "force", |
| 7 | "jsx": "preserve", | ||
| 7 | "strict": true, | 8 | "strict": true, |
| 8 | "noEmit": true, | 9 | "noEmit": true, |
| 9 | "verbatimModuleSyntax": true, | 10 | "verbatimModuleSyntax": true, |
| @@ -0,0 +1,19 @@ | |||
| 1 | { | ||
| 2 | "$schema": "https://turbo.build/schema.json", | ||
| 3 | "tasks": { | ||
| 4 | "dev": { | ||
| 5 | "cache": false, | ||
| 6 | "persistent": true | ||
| 7 | }, | ||
| 8 | "typecheck": { | ||
| 9 | "outputs": [] | ||
| 10 | }, | ||
| 11 | "build": { | ||
| 12 | "dependsOn": ["^build"] | ||
| 13 | }, | ||
| 14 | "test": { | ||
| 15 | "dependsOn": ["^test"], | ||
| 16 | "outputs": [] | ||
| 17 | } | ||
| 18 | } | ||
| 19 | } | ||