diff --git a/apps/cloud/executor.config.ts b/apps/cloud/executor.config.ts index c0a3fb0e9..b2cbb0ecf 100644 --- a/apps/cloud/executor.config.ts +++ b/apps/cloud/executor.config.ts @@ -2,24 +2,52 @@ import { defineExecutorConfig } from "@executor-js/sdk"; import { openApiPlugin } from "@executor-js/plugin-openapi"; import { mcpPlugin } from "@executor-js/plugin-mcp"; import { graphqlPlugin } from "@executor-js/plugin-graphql"; -import { workosVaultPlugin } from "@executor-js/plugin-workos-vault"; +import { + workosVaultPlugin, + type WorkOSVaultClient, +} from "@executor-js/plugin-workos-vault"; // --------------------------------------------------------------------------- -// Executor config for CLI schema generation. +// Single source of truth for the cloud app's plugin list. // -// The CLI reads `plugins` + `dialect` to produce a drizzle schema file. -// Plugin credentials are stubs — the CLI only reads `plugin.schema`, -// never calls the plugin at runtime. +// Consumed by: +// - the schema-gen CLI (reads `plugin.schema` only; calls `plugins({})`) +// - the host runtime (calls `plugins({ workosCredentials })` per request) +// - the test harness (calls `plugins({ workosVaultClient })` per test) +// +// `TDeps` is inferred directly from the factory parameter annotation — +// no global `declare module "@executor-js/sdk"` augmentation. Each +// caller (runtime / CLI / tests) passes whatever subset of the deps it +// has; all fields are optional so the CLI's `plugins({})` keeps working. +// +// Cloud only ships plugins safe to run in a multi-tenant setting — no +// stdio MCP, no keychain/file-secrets/1password/google-discovery. // --------------------------------------------------------------------------- +interface CloudPluginDeps { + /** WorkOS vault credentials. Provided per-request from `env.WORKOS_*` + * in production; the test harness leaves this undefined and uses + * `workosVaultClient` to inject an in-memory fake instead. */ + readonly workosCredentials?: { + readonly apiKey: string; + readonly clientId: string; + }; + /** Pluggable WorkOS Vault HTTP client — set by the test harness to + * bypass the real WorkOS API. Production leaves this undefined and + * falls back to the credential-driven default. */ + readonly workosVaultClient?: WorkOSVaultClient; +} + export default defineExecutorConfig({ dialect: "pg", - plugins: [ - openApiPlugin(), - mcpPlugin({ dangerouslyAllowStdioMCP: false }), - graphqlPlugin(), - workosVaultPlugin({ - credentials: { apiKey: "", clientId: "" }, - }), - ], + plugins: ({ workosCredentials, workosVaultClient }: CloudPluginDeps = {}) => + [ + openApiPlugin(), + mcpPlugin({ dangerouslyAllowStdioMCP: false }), + graphqlPlugin(), + workosVaultPlugin({ + credentials: workosCredentials ?? { apiKey: "", clientId: "" }, + ...(workosVaultClient ? { client: workosVaultClient } : {}), + }), + ] as const, }); diff --git a/apps/cloud/package.json b/apps/cloud/package.json index 15a1c2aca..74d33ffc8 100644 --- a/apps/cloud/package.json +++ b/apps/cloud/package.json @@ -13,7 +13,7 @@ "db:studio": "drizzle-kit studio", "db:studio:prod": "op run --env-file=.env.production -- drizzle-kit studio", "db:migrate:prod": "op run --env-file=.env.production -- drizzle-kit migrate", - "build": "vite build", + "build": "turbo run build --filter @executor-js/vite-plugin && vite build", "preview": "vite preview", "deploy": "op run --env-file=.env.production -- bun run build && op run --env-file=.env.production -- wrangler deploy", "cf-typegen": "wrangler types", @@ -40,6 +40,7 @@ "@executor-js/sdk": "workspace:*", "@executor-js/storage-core": "workspace:*", "@executor-js/storage-postgres": "workspace:*", + "@executor-js/vite-plugin": "workspace:*", "@microlabs/otel-cf-workers": "^1.0.0-rc.52", "@modelcontextprotocol/sdk": "^1.29.0", "@opentelemetry/api": "~1.9.0", diff --git a/apps/cloud/src/api.request-scope.node.test.ts b/apps/cloud/src/api.request-scope.node.test.ts index 959212550..8d2908360 100644 --- a/apps/cloud/src/api.request-scope.node.test.ts +++ b/apps/cloud/src/api.request-scope.node.test.ts @@ -152,8 +152,8 @@ describe("HttpRouter.toWebHandler request scoping", () => { handler(new Request("http://test.local/")), ]); - const aBody = await a.json(); - const bBody = await b.json(); + const aBody = (await a.json()) as { id: number }; + const bBody = (await b.json()) as { id: number }; // Two concurrent requests must see two distinct acquired counters. // Otherwise both fibers share one postgres socket -> Cloudflare // Workers I/O isolation crash in prod. diff --git a/apps/cloud/src/api/cloud-plugins.ts b/apps/cloud/src/api/cloud-plugins.ts new file mode 100644 index 000000000..c4f006e0e --- /dev/null +++ b/apps/cloud/src/api/cloud-plugins.ts @@ -0,0 +1,17 @@ +// Single shared instantiation of the cloud plugin list. +// +// `executor.config.ts`'s `plugins()` factory is safe to call at +// module-eval time without runtime credentials: the heavy per-request +// dependencies (WorkOS Vault credentials, vault HTTP client) are only +// consumed when the plugin's extension is actually constructed inside +// `createScopedExecutor`. Both the API composition (`protected-layers.ts`) +// and the per-request middleware (`protected.ts` + the test harness) +// derive their typed views — `composePluginApi(cloudPlugins)`, +// `composePluginHandlerLayer(cloudPlugins)`, +// `providePluginExtensions(cloudPlugins)`, `PluginExtensionServices` — from this one tuple, so adding/removing a plugin is +// still a single `executor.config.ts` edit. +import executorConfig from "../../executor.config"; + +export const cloudPlugins = executorConfig.plugins(); +export type CloudPlugins = typeof cloudPlugins; diff --git a/apps/cloud/src/api/protected-layers.ts b/apps/cloud/src/api/protected-layers.ts index 1b5685141..bcaeec70d 100644 --- a/apps/cloud/src/api/protected-layers.ts +++ b/apps/cloud/src/api/protected-layers.ts @@ -7,15 +7,14 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"; import { HttpRouter, HttpServer } from "effect/unstable/http"; import { Layer } from "effect"; +import { observabilityMiddleware } from "@executor-js/api"; import { - CoreExecutorApi, - observabilityMiddleware, -} from "@executor-js/api"; -import { CoreHandlers } from "@executor-js/api/server"; -import { OpenApiGroup, OpenApiHandlers } from "@executor-js/plugin-openapi/api"; -import { McpGroup, McpHandlers } from "@executor-js/plugin-mcp/api"; -import { GraphqlGroup, GraphqlHandlers } from "@executor-js/plugin-graphql/api"; + CoreHandlers, + composePluginApi, + composePluginHandlerLayer, +} from "@executor-js/api/server"; +import { cloudPlugins } from "./cloud-plugins"; import { UserStoreService } from "../auth/context"; import { WorkOSAuth } from "../auth/workos"; import { AutumnService } from "../services/autumn"; @@ -30,9 +29,13 @@ import { ErrorCaptureLive } from "../observability"; // it INSIDE the router middleware (wrong order), and added a second auth // pass on top of the existing one in `protected.ts`'s outer effect. The // router-middleware approach folds both into one place. -export const ProtectedCloudApi = CoreExecutorApi.add(OpenApiGroup) - .add(McpGroup) - .add(GraphqlGroup); +// +// `composePluginApi(cloudPlugins)` returns a precisely typed `HttpApi` +// — the group union is derived from `typeof cloudPlugins` via the +// plugin spec's `TGroup` generic. Test harness clients type via +// `HttpApiClient.ForApi` directly, with no +// per-plugin Group imports at the host. +export const ProtectedCloudApi = composePluginApi(cloudPlugins); const ObservabilityLive = observabilityMiddleware(ProtectedCloudApi); @@ -49,14 +52,14 @@ export const SharedServices = Layer.mergeAll( export const RouterConfig = Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 }); -// Every handler the ProtectedCloudApi routes to. The test harness builds -// its own api-live by merging this with its own per-request middleware -// fakes; prod uses `ProtectedCloudApiLive` below. +// Every handler the ProtectedCloudApi routes to. Plugin handler layers +// are late-binding — they require their plugin's `extensionService` +// Tag, which the per-request `ExecutionStackMiddleware` satisfies via +// `providePluginExtensions`. The test harness mirrors this; nothing +// else needs to know which plugins are wired. export const ProtectedCloudApiHandlers = Layer.mergeAll( CoreHandlers, - OpenApiHandlers, - McpHandlers, - GraphqlHandlers, + composePluginHandlerLayer(cloudPlugins), ); // `ErrorCaptureLive` is provided above the handler + middleware layers diff --git a/apps/cloud/src/api/protected.ts b/apps/cloud/src/api/protected.ts index 608eed7a7..bbdc096e7 100644 --- a/apps/cloud/src/api/protected.ts +++ b/apps/cloud/src/api/protected.ts @@ -12,11 +12,11 @@ import { Effect, Layer } from "effect"; import { ExecutionEngineService, ExecutorService, + providePluginExtensions, + type PluginExtensionServices, } from "@executor-js/api/server"; -import { OpenApiExtensionService } from "@executor-js/plugin-openapi/api"; -import { McpExtensionService } from "@executor-js/plugin-mcp/api"; -import { GraphqlExtensionService } from "@executor-js/plugin-graphql/api"; +import { cloudPlugins, type CloudPlugins } from "./cloud-plugins"; import { AuthContext } from "../auth/middleware"; import { authorizeOrganization } from "../auth/authorize-organization"; import { UserStoreService } from "../auth/context"; @@ -33,23 +33,10 @@ import { } from "./protected-layers"; import { requestScopedMiddleware } from "./request-scoped"; -// One `HttpRouter` middleware that: -// 1. authenticates the WorkOS sealed session, -// 2. verifies live org membership (closes the JWT-cache gap — see -// `auth/authorize-organization.ts`), -// 3. resolves the org name, -// 4. builds the per-request executor + engine, -// 5. provides `AuthContext` + the execution-stack services to the handler. -// -// Replaces both the old outer `Effect.gen` in this file (which did its own -// WorkOS lookup) and the per-route `OrgAuth` HttpApiMiddleware (which did -// a second one). -// -// Errors are NOT caught here: failures propagate as typed errors and are -// rendered to a JSON response by the framework's `Respondable` pipeline -// (see `HttpResponseError` in `./error-response.ts`). Letting `unhandled` -// pass through is what satisfies `HttpRouter.middleware`'s brand check -// without any type casts. +// Pre-compute the per-plugin `Effect.provideService(extensionService, +// executor[id])` chain. The plugin spec carries the Service tag so +// this file doesn't import each plugin's `*/api` directly. +const provideExecutorExtensions = providePluginExtensions(cloudPlugins); // One `HttpRouter` middleware that: // 1. authenticates the WorkOS sealed session, @@ -74,13 +61,15 @@ import { requestScopedMiddleware } from "./request-scoped"; // fresh per request so the postgres.js socket lives in the request // fiber's scope, not the worker's boot scope. const ExecutionStackMiddleware = HttpRouter.middleware<{ + // The plugin extension Services this middleware satisfies are derived + // from `typeof cloudPlugins` — no per-plugin `*ExtensionService` + // imports at the host. Runtime binding mirrors the type: + // `providePluginExtensions(cloudPlugins)(executor)` below. provides: | AuthContext | ExecutorService | ExecutionEngineService - | OpenApiExtensionService - | McpExtensionService - | GraphqlExtensionService; + | PluginExtensionServices; }>()( Effect.gen(function* () { const longLived = yield* Effect.context(); @@ -117,9 +106,7 @@ const ExecutionStackMiddleware = HttpRouter.middleware<{ Effect.provideService(AuthContext, auth), Effect.provideService(ExecutorService, executor), Effect.provideService(ExecutionEngineService, engine), - Effect.provideService(OpenApiExtensionService, executor.openapi), - Effect.provideService(McpExtensionService, executor.mcp), - Effect.provideService(GraphqlExtensionService, executor.graphql), + provideExecutorExtensions(executor), ); }).pipe(Effect.provideContext(longLived)); }), diff --git a/apps/cloud/src/mcp-session.e2e.node.test.ts b/apps/cloud/src/mcp-session.e2e.node.test.ts index a7dfdcb2c..0f8eac095 100644 --- a/apps/cloud/src/mcp-session.e2e.node.test.ts +++ b/apps/cloud/src/mcp-session.e2e.node.test.ts @@ -37,11 +37,7 @@ import { makePostgresAdapter, makePostgresBlobStore, } from "@executor-js/storage-postgres"; -import { openApiPlugin } from "@executor-js/plugin-openapi"; -import { mcpPlugin } from "@executor-js/plugin-mcp"; -import { graphqlPlugin } from "@executor-js/plugin-graphql"; -import { workosVaultPlugin } from "@executor-js/plugin-workos-vault"; - +import executorConfig from "../executor.config"; import { DbService } from "./services/db"; import { makeFakeVaultClient } from "./services/__test-harness__/api-harness"; @@ -105,12 +101,9 @@ const buildScopedExecutor = ( ) => Effect.gen(function* () { const { db } = yield* DbService; - const basePlugins = [ - openApiPlugin(), - mcpPlugin({ dangerouslyAllowStdioMCP: false }), - graphqlPlugin(), - workosVaultPlugin({ client: makeFakeVaultClient() }), - ] as const; + const basePlugins = executorConfig.plugins({ + workosVaultClient: makeFakeVaultClient(), + }); const plugins = options.withElicitingPlugin ? ([...basePlugins, elicitingTestPlugin()] as const) : basePlugins; diff --git a/apps/cloud/src/routes/__root.tsx b/apps/cloud/src/routes/__root.tsx index 8b81b5c2b..5374743dc 100644 --- a/apps/cloud/src/routes/__root.tsx +++ b/apps/cloud/src/routes/__root.tsx @@ -7,6 +7,8 @@ import { PostHogProvider } from "posthog-js/react"; import { ExecutorProvider } from "@executor-js/react/api/provider"; import { Skeleton } from "@executor-js/react/components/skeleton"; import { Toaster } from "@executor-js/react/components/sonner"; +import { ExecutorPluginsProvider } from "@executor-js/sdk/client"; +import { plugins as clientPlugins } from "virtual:executor/plugins-client"; import { AuthProvider, useAuth } from "../web/auth"; import { LoginPage } from "../web/pages/login"; import { OnboardingPage } from "../web/pages/onboarding"; @@ -161,8 +163,10 @@ function AuthGate() { return ( }> - - + + + + ); diff --git a/apps/cloud/src/routes/index.tsx b/apps/cloud/src/routes/index.tsx index ad2a93e7e..01273b87a 100644 --- a/apps/cloud/src/routes/index.tsx +++ b/apps/cloud/src/routes/index.tsx @@ -1,11 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; import { SourcesPage } from "@executor-js/react/pages/sources"; -import { openApiSourcePlugin } from "@executor-js/plugin-openapi/react"; -import { mcpSourcePlugin } from "@executor-js/plugin-mcp/react"; -import { graphqlSourcePlugin } from "@executor-js/plugin-graphql/react"; - -const sourcePlugins = [openApiSourcePlugin, mcpSourcePlugin, graphqlSourcePlugin]; export const Route = createFileRoute("/")({ - component: () => , + component: SourcesPage, }); diff --git a/apps/cloud/src/routes/secrets.tsx b/apps/cloud/src/routes/secrets.tsx index 40904354c..aff164652 100644 --- a/apps/cloud/src/routes/secrets.tsx +++ b/apps/cloud/src/routes/secrets.tsx @@ -4,7 +4,6 @@ import { SecretsPage } from "@executor-js/react/pages/secrets"; export const Route = createFileRoute("/secrets")({ component: () => ( { const { namespace } = Route.useParams(); - return ; + return ; }, }); diff --git a/apps/cloud/src/routes/sources.add.$pluginKey.tsx b/apps/cloud/src/routes/sources.add.$pluginKey.tsx index f6a849eaa..9d5043201 100644 --- a/apps/cloud/src/routes/sources.add.$pluginKey.tsx +++ b/apps/cloud/src/routes/sources.add.$pluginKey.tsx @@ -1,11 +1,6 @@ import { Schema } from "effect"; import { createFileRoute } from "@tanstack/react-router"; import { SourcesAddPage } from "@executor-js/react/pages/sources-add"; -import { openApiSourcePlugin } from "@executor-js/plugin-openapi/react"; -import { mcpSourcePlugin } from "@executor-js/plugin-mcp/react"; -import { graphqlSourcePlugin } from "@executor-js/plugin-graphql/react"; - -const sourcePlugins = [openApiSourcePlugin, mcpSourcePlugin, graphqlSourcePlugin]; const SearchParams = Schema.toStandardSchemaV1( Schema.Struct({ @@ -26,7 +21,6 @@ export const Route = createFileRoute("/sources/add/$pluginKey")({ url={url} preset={preset} namespace={namespace} - sourcePlugins={sourcePlugins} /> ); }, diff --git a/apps/cloud/src/services/__test-harness__/api-harness.ts b/apps/cloud/src/services/__test-harness__/api-harness.ts index 2810ed5da..3183f34ab 100644 --- a/apps/cloud/src/services/__test-harness__/api-harness.ts +++ b/apps/cloud/src/services/__test-harness__/api-harness.ts @@ -26,6 +26,8 @@ import { import { ExecutionEngineService, ExecutorService, + providePluginExtensions, + type PluginExtensionServices, } from "@executor-js/api/server"; import { createExecutionEngine } from "@executor-js/execution"; import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; @@ -39,20 +41,14 @@ import { makePostgresAdapter, makePostgresBlobStore, } from "@executor-js/storage-postgres"; -import { openApiPlugin } from "@executor-js/plugin-openapi"; -import { mcpPlugin } from "@executor-js/plugin-mcp"; -import { graphqlPlugin } from "@executor-js/plugin-graphql"; import { - workosVaultPlugin, WorkOSVaultClientError, type WorkOSVaultClient, type WorkOSVaultObject, type WorkOSVaultObjectMetadata, } from "@executor-js/plugin-workos-vault"; -import { OpenApiExtensionService } from "@executor-js/plugin-openapi/api"; -import { McpExtensionService } from "@executor-js/plugin-mcp/api"; -import { GraphqlExtensionService } from "@executor-js/plugin-graphql/api"; +import executorConfig from "../../../executor.config"; import { AuthContext } from "../../auth/middleware"; import { ProtectedCloudApi, @@ -174,6 +170,7 @@ export const makeFakeVaultClient = (): WorkOSVaultClient => { // --------------------------------------------------------------------------- const fakeVault = makeFakeVaultClient(); +const testPlugins = executorConfig.plugins({ workosVaultClient: fakeVault }); const createTestScopedExecutor = ( userId: string, @@ -182,12 +179,7 @@ const createTestScopedExecutor = ( ) => Effect.gen(function* () { const { db } = yield* DbService; - const plugins = [ - openApiPlugin(), - mcpPlugin({ dangerouslyAllowStdioMCP: false }), - graphqlPlugin(), - workosVaultPlugin({ client: fakeVault }), - ] as const; + const plugins = testPlugins; const schema = collectSchemas(plugins); const adapter = makePostgresAdapter({ db, schema }); const blobs = makePostgresBlobStore({ db }); @@ -225,15 +217,14 @@ const TestExecutionStackMiddleware = HttpRouter.middleware<{ | AuthContext | ExecutorService | ExecutionEngineService - | OpenApiExtensionService - | McpExtensionService - | GraphqlExtensionService; + | PluginExtensionServices; }>()( // Layer-time setup — captures `DbService` so the per-request function // only depends on `HttpRouter`-Provided context. See `api/protected.ts` // for the same pattern. Effect.gen(function* () { const context = yield* Effect.context(); + const provideExecutorExtensions = providePluginExtensions(testPlugins); return (httpEffect) => Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; @@ -265,9 +256,7 @@ const TestExecutionStackMiddleware = HttpRouter.middleware<{ ), Effect.provideService(ExecutorService, executor), Effect.provideService(ExecutionEngineService, engine), - Effect.provideService(OpenApiExtensionService, executor.openapi), - Effect.provideService(McpExtensionService, executor.mcp), - Effect.provideService(GraphqlExtensionService, executor.graphql), + provideExecutorExtensions(executor), ); }).pipe(Effect.provideContext(context)); }), diff --git a/apps/cloud/src/services/executor.ts b/apps/cloud/src/services/executor.ts index eaf6848a0..33d9d8f54 100644 --- a/apps/cloud/src/services/executor.ts +++ b/apps/cloud/src/services/executor.ts @@ -20,36 +20,27 @@ import { makePostgresAdapter, makePostgresBlobStore, } from "@executor-js/storage-postgres"; -import { openApiPlugin } from "@executor-js/plugin-openapi"; -import { mcpPlugin } from "@executor-js/plugin-mcp"; -import { graphqlPlugin } from "@executor-js/plugin-graphql"; -import { workosVaultPlugin } from "@executor-js/plugin-workos-vault"; import { env } from "cloudflare:workers"; +import executorConfig from "../../executor.config"; import { DbService } from "./db"; // --------------------------------------------------------------------------- -// Plugin list — one place, used for both the runtime and the CLI config -// (executor.config.ts). No stdio MCP in cloud; no keychain/file-secrets/ -// 1password/google-discovery. -// -// NOTE: the CLI config (executor.config.ts) imports these same plugins with -// stub credentials because it only reads `plugin.schema`. Here we pass -// real credentials from the env. +// Plugin list lives in `executor.config.ts` — that file is the single +// source of truth, also consumed by the schema-gen CLI and the test +// harness. Per-request runtime values (WorkOS credentials from the +// Worker env) are passed through the factory's `deps` parameter. // --------------------------------------------------------------------------- -const createOrgPlugins = () => - [ - openApiPlugin(), - mcpPlugin({ dangerouslyAllowStdioMCP: false }), - graphqlPlugin(), - workosVaultPlugin({ - credentials: { - apiKey: env.WORKOS_API_KEY, - clientId: env.WORKOS_CLIENT_ID, - }, - }), - ] as const; +export type CloudPlugins = ReturnType; + +const orgPlugins = (): CloudPlugins => + executorConfig.plugins({ + workosCredentials: { + apiKey: env.WORKOS_API_KEY, + clientId: env.WORKOS_CLIENT_ID, + }, + }); // --------------------------------------------------------------------------- // Create a fresh executor for a (user, org) pair (stateless, per-request). @@ -74,7 +65,7 @@ export const createScopedExecutor = ( Effect.gen(function* () { const { db } = yield* DbService; - const plugins = createOrgPlugins(); + const plugins = orgPlugins(); const schema = collectSchemas(plugins); const adapter = makePostgresAdapter({ db, schema }); const blobs = makePostgresBlobStore({ db }); diff --git a/apps/cloud/src/vite-env.d.ts b/apps/cloud/src/vite-env.d.ts index 11f02fe2a..4c4f44cb3 100644 --- a/apps/cloud/src/vite-env.d.ts +++ b/apps/cloud/src/vite-env.d.ts @@ -1 +1,6 @@ /// + +declare module "virtual:executor/plugins-client" { + import type { ClientPluginSpec } from "@executor-js/sdk/client"; + export const plugins: readonly ClientPluginSpec[]; +} diff --git a/apps/cloud/src/web/shell.tsx b/apps/cloud/src/web/shell.tsx index d42602499..a8f4825d9 100644 --- a/apps/cloud/src/web/shell.tsx +++ b/apps/cloud/src/web/shell.tsx @@ -29,9 +29,6 @@ import { } from "@executor-js/react/components/dropdown-menu"; import { SourceFavicon } from "@executor-js/react/components/source-favicon"; import { CommandPalette } from "@executor-js/react/components/command-palette"; -import { openApiSourcePlugin } from "@executor-js/plugin-openapi/react"; -import { mcpSourcePlugin } from "@executor-js/plugin-mcp/react"; -import { graphqlSourcePlugin } from "@executor-js/plugin-graphql/react"; import { authWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { AUTH_PATHS } from "../auth/api"; import { organizationsAtom, switchOrganization, useAuth } from "./auth"; @@ -40,8 +37,6 @@ import { useCreateOrganizationForm, } from "./components/create-organization-form"; -const sourcePlugins = [openApiSourcePlugin, mcpSourcePlugin, graphqlSourcePlugin]; - // ── NavItem ────────────────────────────────────────────────────────────── function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) { @@ -430,7 +425,7 @@ export function Shell() { return (
- + {/* Desktop sidebar */}