History

feat: add v2 web control-plane app and turbo dev orchestration

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.

Rhys Sullivan committed 8 months ago

Commit ef56bd51 parent fc53fa822 files · +849 −155Browse files

Unified · Split

22 files
  1. v2/.gitignore +3 −0
  2. v2/apps/convex/convex/run_executor.ts +7 −6
  3. v2/apps/convex/convex/runtime_adapter.ts +14 −4
  4. v2/apps/web/README.md +13 −1
  5. v2/apps/web/agents.md +114 −0
  6. v2/apps/web/app/globals.css +189 −0
  7. v2/apps/web/app/layout.tsx +37 −0
  8. v2/apps/web/app/page.tsx +210 −0
  9. v2/apps/web/app/providers.tsx +12 −0
  10. v2/apps/web/lib/control-plane/atoms.ts +73 −0
  11. v2/apps/web/lib/control-plane/client.ts +5 −0
  12. v2/apps/web/next-env.d.ts +6 −0
  13. v2/apps/web/next.config.ts +22 −0
  14. v2/apps/web/package.json +17 −0
  15. v2/apps/web/src/main.ts +0 −1
  16. v2/apps/web/tsconfig.json +31 −1
  17. v2/bun.lock +65 −10
  18. v2/package.json +2 −0
  19. v2/packages/control-plane/src/client.ts +8 −128
  20. v2/packages/control-plane/src/index.ts +1 −4
  21. v2/tsconfig.json +1 −0
  22. v2/turbo.json +19 −0
v2/.gitignore+3−0
@@ -1,3 +1,6 @@
11 .reference/
22 .vercel
33 .env*.local
4+.next/
5+.turbo/
6+.executor-v2/
v2/apps/convex/convex/run_executor.ts+7−6
@@ -1,14 +1,17 @@
11 import {
2− makeLocalInProcessRuntimeAdapter,
3− makeToolProviderRegistry,
42 ToolProviderRegistryService,
5− type RuntimeExecuteError,
6−} from "@executor-v2/engine";
3+ makeToolProviderRegistry,
4+} from "@executor-v2/engine/tool-providers";
75 import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk";
86 import * as Context from "effect/Context";
97 import * as Effect from "effect/Effect";
108 import * as Layer from "effect/Layer";
119
10+import {
11+ makeLocalInProcessRuntimeAdapter,
12+ type RuntimeExecuteError,
13+} from "./runtime_adapter";
14+
1215 export type ConvexRunExecutorService = {
1316 executeRun: (
1417 input: ExecuteRunInput,
@@ -23,9 +26,7 @@ const runtimeAdapter = makeLocalInProcessRuntimeAdapter(); · 8 unmodified lines
2326
2427 const formatRuntimeExecuteError = (error: RuntimeExecuteError): string => {
2528 switch (error._tag) {
26− case "RuntimeAdapterError":
2729 case "LocalCodeRunnerError":
28− case "DenoSubprocessRunnerError":
2930 case "ToolProviderError":
3031 return error.details ? `${error.message}: ${error.details}` : error.message;
3132 case "ToolProviderRegistryError":
v2/apps/convex/convex/runtime_adapter.ts+14−4
@@ -1,7 +1,12 @@
1−import { executeJavaScriptWithTools } from "@executor-v2/engine/local-runner";
2−import type {
3− CanonicalToolDescriptor,
1+import {
2+ LocalCodeRunnerError,
3+ executeJavaScriptWithTools,
4+} from "@executor-v2/engine/local-runner";
5+import {
6+ ToolProviderError,
7+ ToolProviderRegistryError,
48 ToolProviderRegistryService,
9+ type CanonicalToolDescriptor,
510 } from "@executor-v2/engine/tool-providers";
611 import type { Source } from "@executor-v2/schema";
712 import * as Effect from "effect/Effect";
@@ -17,12 +22,17 @@ export type RuntimeExecuteInput = { · 9 unmodified lines
1722 timeoutMs?: number;
1823 };
1924
25+export type RuntimeExecuteError =
26+ | LocalCodeRunnerError
27+ | ToolProviderRegistryError
28+ | ToolProviderError;
29+
2030 export type RuntimeAdapter = {
2131 kind: "local-inproc";
2232 isAvailable: () => Effect.Effect<boolean>;
2333 execute: (
2434 input: RuntimeExecuteInput,
25− ) => Effect.Effect<unknown, unknown, ToolProviderRegistryService>;
35+ ) => Effect.Effect<unknown, RuntimeExecuteError, ToolProviderRegistryService>;
2636 };
2737
2838 export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({
v2/apps/web/README.md+13−1
@@ -1,3 +1,15 @@
11 # app-web
22
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`.
v2/apps/web/agents.md+114−0
@@ -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
v2/apps/web/app/globals.css+189−0
@@ -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+}
v2/apps/web/app/layout.tsx+37−0
@@ -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;
v2/apps/web/app/page.tsx+210−0
@@ -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;
v2/apps/web/app/providers.tsx+12−0
@@ -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+);
v2/apps/web/lib/control-plane/atoms.ts+73−0
@@ -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+);
v2/apps/web/lib/control-plane/client.ts+5−0
@@ -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+});
v2/apps/web/next-env.d.ts+6−0
@@ -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.
v2/apps/web/next.config.ts+22−0
@@ -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;
v2/apps/web/package.json+17−0
@@ -3,6 +3,23 @@ · 2 unmodified lines
33 "private": true,
44 "type": "module",
55 "scripts": {
6+ "dev": "next dev --port 3000",
7+ "build": "next build",
8+ "start": "next start --port 3000",
69 "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"
724 }
825 }
v2/apps/web/src/main.ts+0−1
@@ -1 +0,0 @@
1−export {};
v2/apps/web/tsconfig.json+31−1
@@ -1,4 +1,34 @@
11 {
22 "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+ ]
434 }
v2/bun.lock+65−10
@@ -11,6 +11,7 @@ · 10 unmodified lines
1111 "@effect/language-service": "^0.76.0",
1212 "@types/bun": "^1.2.22",
1313 "@types/node": "catalog:",
14+ "turbo": "^2.8.11",
1415 "typescript": "catalog:",
1516 },
1617 },
@@ -54,6 +55,20 @@ · 37 unmodified lines
5455 },
5556 "apps/web": {
5657 "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+ },
5772 },
5873 "packages/ai-sdk-adapter": {
5974 "name": "@executor-v2/ai-sdk-adapter",
@@ -408,23 +423,23 @@ · 348 unmodified lines
408423
409424 "@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=="],
410425
411− "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
426+ "@next/env": ["@next/env@15.5.12", "", {}, "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg=="],
412427
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=="],
414429
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=="],
416431
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=="],
418433
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=="],
420435
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=="],
422437
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=="],
424439
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=="],
426441
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=="],
428443
429444 "@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=="],
430445
@@ -612,6 +627,10 @@ · 181 unmodified lines
612627
613628 "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
614629
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+
615634 "@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=="],
616635
617636 "@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
788807
789808 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
790809
810+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
811+
791812 "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
792813
793814 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -1018,7 +1039,7 @@ · 224 unmodified lines
10181039
10191040 "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
10201041
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=="],
10221043
10231044 "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
10241045
@@ -1232,6 +1253,20 @@ · 207 unmodified lines
12321253
12331254 "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=="],
12341255
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+
12351270 "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=="],
12361271
12371272 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -1292,6 +1327,8 @@ · 54 unmodified lines
12921327
12931328 "@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=="],
12941329
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+
12951332 "@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=="],
12961333
12971334 "@mapbox/node-pre-gyp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -1388,6 +1425,24 @@ · 90 unmodified lines
13881425
13891426 "vite-node/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
13901427
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+
13911446 "@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=="],
13921447
13931448 "@vercel/express/@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
v2/package.json+2−0
@@ -18,6 +18,7 @@ · 17 unmodified lines
1818 },
1919 "scripts": {
2020 "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",
2122 "typecheck": "tsc --noEmit",
2223 "knip": "bunx knip --config knip.json"
2324 },
@@ -25,6 +26,7 @@ · 1 unmodified line
2526 "@effect/language-service": "^0.76.0",
2627 "@types/bun": "^1.2.22",
2728 "@types/node": "catalog:",
29+ "turbo": "^2.8.11",
2830 "typescript": "catalog:"
2931 },
3032 "dependencies": {
v2/packages/control-plane/src/client.ts+8−128
@@ -1,24 +1,16 @@
1−import { Atom, Result } from "@effect-atom/atom";
1+import { AtomHttpApi } from "@effect-atom/atom";
22 import {
33 FetchHttpClient,
44 HttpApiClient,
55 HttpApiError,
66 HttpClientError,
77 } from "@effect/platform";
8−import type {
9− Source,
10− SourceId,
11− WorkspaceId,
12−} from "@executor-v2/schema";
13−import * as Effect from "effect/Effect";
148 import * as ParseResult from "effect/ParseResult";
159
1610 import {
1711 ControlPlaneApi,
1812 type ControlPlaneBadRequestError,
1913 type ControlPlaneStorageError,
20− type RemoveSourceResult,
21− type UpsertSourcePayload,
2214 } from "./api";
2315
2416 export type ControlPlaneClientOptions = {
@@ -32,128 +24,16 @@ export type ControlPlaneClientError = · 7 unmodified lines
3224 | HttpClientError.HttpClientError
3325 | ParseResult.ParseError;
3426
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−
5327 export const makeControlPlaneClient = (options: ControlPlaneClientOptions) =>
5428 HttpApiClient.make(ControlPlaneApi, {
5529 baseUrl: options.baseUrl,
5630 });
5731
58−const isReadEnabled = (options?: ControlPlaneReadOptions): boolean =>
59− options?.enabled ?? true;
60−
61−const makeDisabledResultAtom = <Success, Error>() =>
62− Atom.make(Result.initial<Success, Error>());
63−
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− );
32+export const createControlPlaneAtomClient = (options: ControlPlaneClientOptions) =>
33+ AtomHttpApi.Tag<unknown>()("@executor-v2/control-plane/AtomClient", {
34+ api: ControlPlaneApi,
35+ httpClient: FetchHttpClient.layer,
36+ baseUrl: options.baseUrl,
37+ });
13238
133− return {
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−};
39+export type ControlPlaneAtomClient = ReturnType<typeof createControlPlaneAtomClient>;
v2/packages/control-plane/src/index.ts+1−4
@@ -35,10 +35,7 @@ export { · 34 unmodified lines
3535 export {
3636 createControlPlaneAtomClient,
3737 makeControlPlaneClient,
38+ type ControlPlaneAtomClient,
3839 type ControlPlaneClientError,
3940 type ControlPlaneClientOptions,
40− type ControlPlaneReadOptions,
41− type ListSourcesRequest,
42− type RemoveSourceRequest,
43− type UpsertSourceRequest,
4441 } from "./client";
v2/tsconfig.json+1−0
@@ -4,6 +4,7 @@ · 3 unmodified lines
44 "module": "ESNext",
55 "moduleResolution": "Bundler",
66 "moduleDetection": "force",
7+ "jsx": "preserve",
78 "strict": true,
89 "noEmit": true,
910 "verbatimModuleSyntax": true,
v2/turbo.json+19−0
@@ -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+}