History

Remove ARCHITECTURE.md and update README.md to reflect documentation changes; enhance executor.jsonc with new source configurations for Axiom and Linear.

Rhys Sullivan committed 7 months ago

Commit d673a5a1 parent db9b84d34 files · +495 −1625Browse files

Unified · Split

34 files
  1. .executor/executor.jsonc +6 −0
  2. ARCHITECTURE.md +0 −411
  3. README.md +0 −2
  4. apps/docs/developer/adapters-and-formats.mdx +0 −98
  5. apps/docs/developer/cli.mdx +0 −62
  6. apps/docs/developer/codemode.mdx +0 −65
  7. apps/docs/developer/core-model.mdx +0 −134
  8. apps/docs/developer/credentials-and-auth.mdx +0 −116
  9. apps/docs/developer/extending-executor.mdx +0 −90
  10. apps/docs/developer/import-fidelity.mdx +0 −132
  11. apps/docs/developer/mcp.mdx +0 −50
  12. apps/docs/developer/overview.mdx +0 −106
  13. apps/docs/developer/persistence-and-migrations.mdx +0 −86
  14. apps/docs/developer/tool-catalog-and-execution.mdx +0 −91
  15. apps/docs/docs.json +2 −37
  16. apps/docs/introduction.mdx +2 −12
  17. apps/web/src/components/shell.tsx +1 −1
  18. apps/web/src/components/source-not-found-state.tsx +1 −1
  19. apps/web/src/main.tsx +70 −9
  20. apps/web/src/source-plugins/index.ts +25 −6
  21. apps/web/src/source-plugins/pages.tsx +106 −47
  22. apps/web/src/views/home.tsx +1 −1
  23. packages/clients/react/package.json +1 −0
  24. packages/clients/react/src/plugins/index.ts +1 −0
  25. packages/clients/react/src/source-plugins/index.ts +26 −1
  26. packages/clients/react/src/source-plugins/paths.ts +35 −2
  27. packages/clients/react/src/source-plugins/plugin-route-context.tsx +55 −0
  28. packages/clients/react/src/source-plugins/registry.ts +97 −45
  29. packages/clients/react/src/source-plugins/route-context.tsx +3 −0
  30. packages/clients/react/src/source-plugins/types.ts +41 −17
  31. packages/platform/sdk/src/plugins.ts +19 −0
  32. plugins/google-discovery/react/index.tsx +1 −1
  33. plugins/graphql/react/index.tsx +1 −1
  34. plugins/mcp/react/index.tsx +1 −1
.executor/executor.jsonc+6−0
@@ -3,6 +3,12 @@ · 2 unmodified lines
3 "vercel-api": {3 "vercel-api": {
4 "name": "Vercel API",4 "name": "Vercel API",
5 "kind": "openapi"5 "kind": "openapi"
6 },
7 "axiom": {
8 "kind": "mcp"
9 },
10 "linear": {
11 "kind": "graphql"
6 }12 }
7 }13 }
8}14}
ARCHITECTURE.md+0−411
@@ -1,411 +0,0 @@
1# Architecture
2
3This document explains the active v3 architecture at a high level.
4
5If `README.md` answers "what is this product and how do I use it?", this file answers "what are the major moving parts and how do they fit together?"
6
7## One-line view
8
9`executor` is a local daemonized control plane that turns connected sources into a workspace tool catalog and then runs TypeScript against that catalog, pausing for user interaction whenever a tool or auth flow needs it.
10
11## System shape
12
13```text
14 +-----------------------+
15 | CLI / executor |
16 | apps/executor |
17 +-----------+-----------+
18 |
19 | HTTP
20 v
21+-------------+ +---------+---------+ +----------------+
22| Browser UI |-->| local server |<--| MCP clients |
23| apps/web | | packages/platform | | via /mcp |
24| | | /server | | |
25+-------------+ +---------+---------+ +----------------+
26 |
27 | provides runtime layer
28 v
29 +---------+---------+
30 | control plane |
31 | packages/platform/ |
32 | control-plane |
33 +----+-----+----+----+
34 | | |
35 persistence --+ | +-- source auth / discovery / inspection
36 |
37 +-- execution environment resolver
38 |
39 +-- live execution manager
40 v
41 +---------+---------+
42 | QuickJS sandboxed |
43 | executor runtime |
44 | packages/kernel/ |
45 | runtime-quickjs |
46 | default executor |
47 +--------------------+
48```
49
50## Design goals reflected in the code
51
52The current architecture optimizes for a few specific ideas:
53
54- local-first operation with one daemon instead of many disconnected tools
55- one shared runtime for CLI, browser UI, and MCP access
56- schema-rich tool usage instead of raw HTTP from prompts
57- reusable source connections that become workspace-scoped tools
58- human-in-the-loop execution that can pause and resume cleanly
59- adapters for multiple source kinds without hardwiring product logic to one protocol
60
61## Major components
62
63### `apps/executor`: installed CLI and daemon manager
64
65This is the main user entrypoint.
66
67Responsibilities:
68
69- exposes commands such as `up`, `down`, `status`, `doctor`, `call`, and `resume`
70- ensures the local daemon is running before execution
71- submits TypeScript executions to the local control plane
72- handles paused interaction flows, including opening browser URLs when needed
73- includes a few dev-only seed helpers for demo sources
74
75Conceptually, the CLI is not the business logic. It is a thin user-facing shell over the local runtime.
76
77### `packages/platform/server`: one local process for API, MCP, and UI
78
79This package hosts the actual local server.
80
81Responsibilities:
82
83- creates the control-plane runtime
84- mounts the control-plane HTTP API at `/v1`
85- mounts the `executor` MCP endpoint at `/mcp`
86- serves the web UI assets for normal browser routes
87- writes PID metadata for daemon lifecycle management
88
89This is an important architectural choice: the API and UI are served by the same local process, so the product behaves like one install rather than a pile of separate services.
90
91### `packages/platform/sdk`: product core
92
93This is the center of the system.
94
95It contains the runtime layer, persistence integration, and the business logic for:
96
97- local installation bootstrap
98- accounts, organizations, and workspaces
99- source discovery and connection
100- source auth and credential flows
101- source inspection and tool indexing
102- secret and credential handling
103- execution creation, resumption, and state tracking
104- policy-aware tool invocation
105
106If you want to understand the behavior of the product, this is the most important package.
107
108### `packages/kernel/runtime-quickjs`: default code execution runtime
109
110This package provides the TypeScript execution environment used by the local product.
111
112At a high level it receives:
113
114- an executor implementation
115- a tool catalog
116- a tool invoker
117
118and runs user-authored code against that environment.
119
120The default runtime executes code inside a QuickJS WebAssembly sandbox so tool calls stay proxied through the control plane.
121
122The workspace can override that in `.executor/executor.jsonc` with `runtime: "quickjs" | "ses" | "deno"`.
123
124### Runtime packages
125
126The source-plugin reset intentionally removed the previous built-in source-adapter packages.
127
128The remaining core packages are:
129
130- `packages/kernel/core`: shared tool abstractions, discovery, schemas, and system tools
131- `packages/hosts/mcp`: exposes the local runtime itself as an MCP server
132- `packages/clients/react`: React hooks and client state wrappers for the local UI
133
134Future source plugins can be added back on top of this runtime boundary.
135
136## Runtime model
137
138### Local installation bootstrap
139
140On first startup, the control plane provisions a local installation automatically.
141
142That bootstrap creates:
143
144- one local account
145- one personal organization
146- one default workspace
147- one local installation record that points at them
148
149This means the product can work out of the box as a local single-user system without requiring an external identity or tenant setup step.
150
151### Single daemon, shared state
152
153All main entrypoints talk to the same local daemon:
154
155- CLI commands call the local HTTP API
156- the web app calls the same local HTTP API
157- MCP hosts talk to the `/mcp` handler exposed by that same process
158
159Because of that, these surfaces share:
160
161- the same workspace
162- the same connected sources
163- the same secrets and credentials
164- the same execution history and interaction state
165
166## Persistence model
167
168The persistence layer is local-file-backed.
169
170Default behavior:
171
172- workspace config and state are stored in local files
173
174Optional behavior:
175
176- future cloud backends can plug in behind Effect service boundaries
177
178At a high level, the local control plane stores these domains:
179
180- installation identity: local installation identity for the workspace
181- source state: sources, auth sessions, source credential bindings
182- tool model: tool artifacts and related metadata extracted from sources
183- secret state: credentials and secret materials
184- execution state: executions and execution interactions
185- governance state: policies
186
187This is why `executor` can reconnect sources once, inspect them later, and run multiple executions over time without rebuilding everything from scratch on every prompt.
188
189## Source lifecycle
190
191The source lifecycle is one of the defining architectural paths in the system.
192
193### 1. Discovery
194
195The Add Source flow starts with URL discovery.
196
197The discovery service probes a URL and tries, in order, to determine whether it looks like:
198
199- an OpenAPI source
200- a GraphQL source
201- an MCP source
202- or an unknown endpoint
203
204Discovery also produces metadata such as:
205
206- inferred source kind
207- confidence level
208- probable auth style
209- optional spec URL
210- warnings
211- potential namespace and transport hints
212
213### 2. Connection and auth
214
215Once a source is identified, the auth service drives the connection flow.
216
217Depending on the source, that may involve:
218
219- connecting immediately with no auth
220- asking for a bearer token or other credential material
221- creating an OAuth session and handing the user into a browser flow
222- persisting auth bindings back to the source
223
224The important architecture point is that source connection is not just a CRUD write. It is a runtime workflow that can branch into interactive auth.
225
226### 3. Tool indexing
227
228After a source is connected, `executor` materializes a workspace-visible tool model.
229
230The exact extraction path depends on the source kind:
231
232- OpenAPI sources are transformed from the OpenAPI document into tool manifests and typed operations
233- GraphQL sources are introspected into callable query and mutation tools
234- MCP sources are represented through persisted tool artifacts and runtime invocation metadata
235
236This indexed representation is what powers search, inspection, and execution without re-parsing everything in the UI.
237
238### 4. Inspection
239
240The source inspection service reconstructs a rich inspection bundle for the UI.
241
242That bundle includes:
243
244- source metadata
245- namespace and pipeline kind
246- tool summaries
247- tool detail payloads
248- raw document text when available
249- manifest and definitions JSON when available
250
251The UI uses this to show both a tree view of tools and a search-oriented discovery view.
252
253## Execution architecture
254
255Execution is the other defining path in the system.
256
257### 1. Create execution
258
259An execution begins when the CLI, API, or MCP bridge submits TypeScript code.
260
261The control plane creates an execution record with status such as:
262
263- `pending`
264- `running`
265- `waiting_for_interaction`
266- `completed`
267- `failed`
268
269### 2. Resolve the workspace execution environment
270
271Before code runs, the runtime builds the execution environment for the current workspace.
272
273That environment combines:
274
275- built-in system tools from `codemode-core`
276- `executor` internal tools such as `executor.sources.add`
277- persisted tools generated from connected OpenAPI, GraphQL, and MCP sources
278
279The resolver returns three things:
280
281- the SES code executor
282- the workspace tool catalog
283- the tool invoker that actually dispatches calls
284
285This resolver is the composition point where the whole product becomes one callable tool surface.
286
287### 3. Run TypeScript
288
289The code then runs inside the local SES sandbox runtime.
290
291The intended calling pattern is:
292
2931. discover a tool by intent
2942. inspect its schema if needed
2953. call the selected `tools.*` path
296
297The runtime is deliberately opinionated here: the product is built around tool calls, not ad hoc `fetch` requests from user code.
298
299### 4. Invoke tools through the control plane
300
301When code calls a tool, the control plane decides how to handle it.
302
303Possible paths include:
304
305- built-in system tool invocation
306- internal `executor` tool invocation
307- OpenAPI tool invocation with resolved auth headers
308- GraphQL tool invocation with resolved auth headers
309- MCP tool invocation through the MCP connector
310
311This is also the point where policy checks and source auth resolution happen.
312
313### 5. Persist results and surface interactions
314
315The execution service updates persistent execution state as the run progresses.
316
317If a tool call or auth flow needs user interaction, the live execution manager:
318
319- creates an execution interaction record
320- marks the execution as `waiting_for_interaction`
321- stores enough payload to resume later
322- waits for a structured elicitation response
323
324Once the response arrives, the manager moves the execution back to `running` and the code continues.
325
326## Human interaction model
327
328A major architectural feature of `executor` is that interactions are first-class runtime state.
329
330This is handled by the live execution manager and the execution interaction tables.
331
332That gives the product a clean pause/resume loop instead of forcing every host to improvise its own half-finished approval flow.
333
334Interaction types include:
335
336- URL-based flows, such as opening a secure credential page or OAuth URL
337- form-like elicitation where a host can provide a structured response
338
339This same idea is used both for direct CLI execution and for the MCP-facing `execute` and `resume` tools.
340
341## API and MCP surfaces
342
343### HTTP API
344
345The control-plane HTTP API is mounted under `/v1`.
346
347Major groups include:
348
349- local installation and secret management
350- OAuth
351- organizations and memberships
352- workspaces
353- sources
354- policies
355- executions
356
357The web UI is a client of this API.
358
359### MCP bridge
360
361`packages/hosts/mcp` exposes the local runtime as an MCP server.
362
363The bridge registers two main tools:
364
365- `execute`: run TypeScript against the local runtime
366- `resume`: continue a paused execution
367
368When the MCP host supports managed elicitation, the bridge can drive the interaction loop directly through MCP instead of requiring the caller to implement custom resume handling.
369
370## Frontend architecture
371
372The web app lives in `apps/web` and is a React app built with Vite.
373
374At a high level:
375
376- routes are managed with TanStack Router
377- data access is wrapped by `@executor/react`
378- the UI is focused on sources, source inspection, and secrets
379- production assets are served by the local server
380- development runs Vite separately while still embedding the same backend behavior
381
382The frontend is intentionally thin. It is mostly a presentation layer over the control-plane API.
383
384## Why the architecture is shaped this way
385
386The system is trying to solve a specific product problem:
387
388- give agents a better way to use tools than pasting giant manifests into prompts
389- keep auth, credentials, and user interactions in a durable runtime
390- let many protocols look like one workspace tool surface
391- make CLI, UI, and MCP hosts all operate on the same local state
392
393That is why the architecture keeps converging on the same central idea:
394
395`executor` is not just a CLI and not just an MCP server. It is a local runtime that owns source connection, tool indexing, execution, and interaction state.
396
397## Current boundaries
398
399A few practical boundaries are worth calling out:
400
401- the active implementation is local-first and single-daemon
402- the current web app is React/Vite, not the older architectures mentioned in planning notes
403- `legacy/` and `legacy2/` are historical context, not the active runtime
404- policy infrastructure exists in the core, but the central product loop today is source connection plus execution
405
406## Read next
407
408- `README.md` for the product view and usage guidance
409- `apps/executor/src/cli/main.ts` for the CLI surface
410- `packages/platform/server/src/index.ts` for how the local server is assembled
411- `packages/platform/sdk/src/runtime/` for the core runtime flows
README.md+0−2
@@ -391,5 +391,3 @@ This repository is explicitly on its third major architecture iteration. · 390 unmodified lines
391- `apps/` and `packages/` are the active implementation391- `apps/` and `packages/` are the active implementation
392- `legacy/` is the original codebase392- `legacy/` is the original codebase
393- `legacy2/` is the second generation393- `legacy2/` is the second generation
394
395If you want the system view instead of the product view, read [`ARCHITECTURE.md`](./ARCHITECTURE.md).
apps/docs/developer/adapters-and-formats.mdx+0−98
@@ -1,98 +0,0 @@
1---
2title: "Adapters And Formats"
3description: "How executor supports multiple integration types without hardwiring the core to one protocol."
4---
5
6Adapters are the main extensibility boundary in executor.
7
8They let executor support multiple integration styles without turning the control plane into a long chain of provider-specific branches.
9
10## What an adapter does
11
12An adapter owns the format-specific or protocol-specific behavior for a source.
13
14That includes:
15
16- connect payload shape
17- binding config serialization and decoding
18- source validation
19- source config extraction
20- materialization
21- manifest parsing
22- operation description
23- invocation
24
25The control plane owns the generic lifecycle around that.
26
27## Why adapters exist
28
29Executor needs to support integrations that do not all look the same at the source boundary:
30
31- OpenAPI specs
32- GraphQL APIs
33- MCP servers
34- future Google Discovery imports
35- future Postman collections
36- future snippet bundles
37
38If the core storage model were shaped around one of those directly, every new integration type would cause another redesign.
39
40Adapters prevent that.
41
42## Adapter vs format
43
44It is important not to confuse the adapter with the import format.
45
46Examples:
47
48- OpenAPI and GraphQL are different adapters even though both import HTTP-shaped API descriptions
49- MCP is its own adapter because it has distinct session, discovery, and resume semantics
50
51This lets executor share a thin catalog model without pretending every source has the same runtime behavior.
52
53## What gets stored
54
55Adapters do **not** get to invent arbitrary top-level storage models.
56
57They compile into the same canonical layers:
58
59- recipe
60- revision
61- raw documents
62- schema bundles
63- operations
64
65That means the control plane can stay generic while adapters remain flexible.
66
67## Why binding config is adapter-owned
68
69Adapter-owned binding config is one of the most important anti-churn decisions in the codebase.
70
71Instead of adding more and more provider columns to the source model, the adapter carries its own binding payload and version.
72
73That makes it much cheaper to evolve formats without reopening the core schema for every field addition.
74
75## Why provider-specific detail still exists
76
77Executor does still store some provider-shaped detail in operation metadata and bundle/document kinds.
78
79That is intentional, but the direction is:
80
81- keep the generic IR thin
82- keep provider-specific detail in adapter code or typed provider metadata
83
84The goal is not to erase differences between formats. The goal is to stop those differences from dominating the core architecture.
85
86For the current fidelity tradeoffs and known lossy areas across adapters, see [Import Fidelity](/developer/import-fidelity).
87
88## What a good new adapter should do
89
90A good new adapter should:
91
92- compile into the canonical recipe/revision/operation model
93- use adapter-owned binding config
94- avoid requiring new generic tables or provider columns unless truly necessary
95- expose stable tool paths and search text
96- make auth needs explicit through the existing auth slot model
97
98If an integration needs a new primitive outside the source/tool model entirely, that is a sign it may not belong as just another adapter.
apps/docs/developer/cli.mdx+0−62
@@ -1,62 +0,0 @@
1---
2title: "CLI"
3description: "How the executor CLI fits into the system and why it stays thin."
4---
5
6The CLI lives in `apps/executor`.
7
8Its job is to be the installed user-facing shell around the local runtime, not the place where product logic is implemented.
9
10The main interactive entrypoints are foreground session commands such as `executor web` and `executor mcp`, plus `executor mcp --stdio` for local process-spawned MCP integrations.
11
12## Responsibilities
13
14The CLI is responsible for:
15
16- starting foreground web and MCP sessions
17- optionally starting and stopping the detached local daemon for compatibility
18- checking daemon health and installation state
19- submitting executions
20- resuming paused interactions
21- packaging and distribution concerns for the installed binary
22
23## What it should not own
24
25The CLI should not own:
26
27- source persistence rules
28- auth workflows
29- tool discovery
30- execution state machines
31- tool invocation behavior
32
33Those belong in the control plane.
34
35## Why this boundary matters
36
37This split is intentional.
38
39It means the browser UI, CLI, and local MCP entrypoint all talk to the same product core instead of each carrying their own business logic. That keeps behavior consistent across entrypoints and avoids reimplementing source, credential, and execution rules in multiple places.
40
41## How it talks to the rest of the system
42
43At a high level, the CLI:
44
451. starts a foreground local session or ensures the detached daemon is available
462. calls the local HTTP API exposed by `packages/platform/server`
473. hands off real work to `packages/platform/sdk` and `packages/platform/api`
48
49That makes the CLI a stable shell over a shared local runtime rather than a second product core.
50
51## Why this is flexible
52
53Because the CLI is thin, new product capabilities usually do not require redesigning the CLI architecture.
54
55Most new work lands in:
56
57- control-plane runtime logic
58- source adapters
59- codemode packages
60- server/API surfaces
61
62The CLI mostly needs new commands or command wiring on top of those primitives.
apps/docs/developer/codemode.mdx+0−65
@@ -1,65 +0,0 @@
1---
2title: "Code Mode"
3description: "The shared tool abstractions and adapter packages that connect sources to execution."
4---
5
6The codemode packages are the bridge between imported integrations and executable tools.
7
8They provide the shared tool abstractions that both the control plane and runtime can depend on.
9
10## Core role
11
12The codemode layer is where executor turns "some external thing" into a discoverable and invokable tool surface.
13
14That is why it exists as a separate layer instead of being buried directly inside each product surface.
15
16## Main packages
17
18### `packages/kernel/core`
19
20This package holds the common tool model:
21
22- tool definitions
23- tool catalogs
24- tool invokers
25- discovery helpers
26- schema-aware tool contracts
27- shared system-tool concepts
28
29This is the main abstraction boundary that other layers build on.
30
31### Source plugins
32
33The old built-in source packages were removed during the source-plugin reset.
34
35The current expectation is that source-specific transformation logic will return as explicit plugins layered on top of the shared codemode contracts instead of being baked into the product shell.
36
37## Why this layer exists
38
39Without the codemode layer, the control plane would have to understand every source format and execution detail directly.
40
41Instead, executor keeps the core model broad and leaves source-specific transformation logic to future plugin modules.
42
43That is what makes the system extensible:
44
45- new adapters can target the same internal tool model
46- the runtime can execute against one logical tool surface
47- the same abstractions can be reused outside the control plane, including SDK-style consumers
48
49## How it fits with the control plane
50
51The control plane owns:
52
53- bindings
54- credentials
55- revisions and materialization
56- tool indexing
57- policy and invocation lifecycle
58
59The codemode layer owns:
60
61- how external formats become tools
62- how those tools are described
63- how format-specific invocation helpers are implemented
64
65That boundary is what keeps the architecture adapter-based instead of turning the control plane into one large protocol-specific switch statement.
apps/docs/developer/core-model.mdx+0−134
@@ -1,134 +0,0 @@
1---
2title: "Core Model"
3description: "The main data and runtime concepts in executor today."
4---
5
6The most important thing to understand about executor is that it does **not** model the system as one flat `source` row.
7
8The current model has a few separate concepts that each exist for a different reason.
9
10## The primary concepts
11
12### Source binding
13
14A source is the workspace-local installation record.
15
16It answers:
17
18- what is this integration called in this workspace?
19- is it enabled?
20- what namespace should its tools use?
21- what install-local config does it have?
22
23This is why a source carries local concerns like name, namespace, enabled state, binding config, and status.
24
25### Recipe
26
27A recipe is the shareable integration identity.
28
29It answers:
30
31- what integration definition is this?
32- what adapter produced it?
33- what broad catalog kind does it belong to?
34
35A recipe is meant to converge across workspaces when the underlying integration definition is the same.
36
37### Revision / materialization
38
39A recipe revision is the compiled result of an import.
40
41It answers:
42
43- what source config produced this compiled shape?
44- what manifest, documents, schema bundles, and operations belong to it?
45
46This is the durable execution artifact layer.
47
48### Operation IR
49
50Operations are the canonical callable tool index.
51
52They answer:
53
54- what tools exist?
55- how should they be searched and described?
56- how does invocation find the right persisted operation quickly?
57
58The operation layer exists because raw upstream documents are not a good direct query surface for search, policy, and invocation lookup.
59
60### Secret material
61
62Secret material is private auth state.
63
64It answers:
65
66- which private values exist for this workspace actor?
67- which source bindings refer to them?
68- which plugin-owned auth configs use those secret refs?
69
70Secret material is actor-scoped by default because two users in the same workspace may connect different identities to the same source.
71
72## Why the split matters
73
74This split fixes a few old problems:
75
76- shareable definition is no longer confused with workspace-local installation
77- auth is no longer implicitly one-token-per-source
78- materialized tool content is no longer stored directly under one workspace source
79- provider-specific import details do not need to become universal top-level columns
80
81## Binding config
82
83Source-specific connection config now lives in adapter-owned binding payloads:
84
85- `bindingVersion`
86- `binding`
87
88That means the core model does not need built-in fields forever for every provider-specific connection knob.
89
90Examples:
91
92- OpenAPI binding can carry `specUrl` and `defaultHeaders`
93- MCP binding can carry `transport`, `queryParams`, and `headers`
94- internal bindings can be empty
95
96The adapter owns serialization, decoding, defaults, and validation for that payload.
97
98## Catalog kinds vs adapters
99
100Executor separates broad catalog kinds from adapter keys.
101
102Catalog kinds:
103
104- `imported`
105- `internal`
106
107Adapters are more specific:
108
109- `openapi`
110- `graphql`
111- `mcp`
112- `google_discovery`
113- future `postman_collection`
114- future `snippet_bundle`
115
116The important distinction is that execution now belongs to adapters. The shared catalog stays broad and stable, while connect/auth/invoke behavior is described through adapter strategies instead of one top-level runtime family enum.
117
118The same is true for auth behavior:
119
120- core owns secret storage and private state plumbing
121- plugins own auth schemas, request shaping, OAuth flows, and refresh logic
122
123## Why this is the right shape
124
125The current model is designed so that new support usually means:
126
127- add or evolve an adapter
128- compile into the same recipe/revision/operation layers
129
130and not:
131
132- redesign how source persistence fundamentally works
133
134That is the main goal of the current architecture.
apps/docs/developer/credentials-and-auth.mdx+0−116
@@ -1,116 +0,0 @@
1---
2title: "Credentials And Auth"
3description: "How executor models auth, interactive flows, and actor-scoped credential ownership."
4---
5
6Auth in executor is not treated as a side detail on the source row.
7
8The important boundary is:
9
10- core owns secret material storage, actor scoping, and private state plumbing
11- plugins own auth schemas and auth behavior for each source kind
12
13## Auth ownership
14
15Secret material is scoped to:
16
17- workspace
18- source binding
19- actor
20- slot
21
22This is important because different users in the same workspace may connect different identities to the same source.
23
24## Auth slots
25
26Plugins may distinguish where auth is used.
27
28Important current slots:
29
30- `runtime`
31- `import`
32
33Why this matters:
34
35- some integrations use the same auth for both
36- some need auth only to fetch or generate the materialization
37- some may eventually need setup/bootstrap-specific auth
38
39This is why plugin-owned source auth is more than a single token field.
40
41## Static credentials
42
43The common case today is still static auth backed by secret references.
44
45Examples:
46
47- bearer token
48- OAuth access token
49- OAuth refresh token
50
51The control plane stores secret refs, not raw secret values inline on the source or plugin-owned auth configs.
52
53## Interactive auth sessions
54
55Interactive auth, such as OAuth flows, is modeled separately from stored secret material.
56
57That separation exists because:
58
59- a pending auth flow has state and lifecycle of its own
60- it may be tied to an interaction or execution
61- it is not the same thing as a resolved credential
62
63This is why executor keeps auth sessions separate from stored secret material.
64
65## Shareable config vs private state
66
67Project config under `.executor/executor.jsonc` is intended to stay portable and committable.
68
69Private auth state does not belong in the workspace tree:
70
71- actor-scoped secret material
72- OAuth sessions
73- refresh leases
74- secret material
75
76That state lives in the local control-plane store under the user's home-scoped executor state instead of the project `.executor/state` directory.
77
78## Why actor scope matters
79
80The old one-token-per-source assumption does not hold once:
81
82- multiple users share a workspace
83- each user connects their own Google account, GitHub account, or other external identity
84
85Actor-scoped ownership is the durable fix for that.
86
87## The next auth primitive
88
89One important extension area is dynamic credentials.
90
91Static secret refs are not always enough for integrations that:
92
93- derive short-lived auth by calling login or refresh endpoints
94- need app-specific runtime headers
95- require token renewal logic
96
97The likely next auth-layer primitive is an auth provider or auth material resolver:
98
99- configured with secret refs and stable inputs
100- produces runtime auth material on demand
101- caches it with expiry metadata
102
103That is broader than sources and should be reusable by future plugins, tool providers, and code bundles.
104
105## Why auth is separated from recipes
106
107Recipes are shareable integration definitions.
108Secret material is private installation-specific state.
109
110That separation is important both for correctness and for shareability:
111
112- a recipe can be portable
113- a source binding can be local
114- secret material stays private
115
116This is the main reason auth is not modeled as part of the shareable recipe layer.
apps/docs/developer/extending-executor.mdx+0−90
@@ -1,90 +0,0 @@
1---
2title: "Extending Executor"
3description: "How to add new source support without breaking the current architecture."
4---
5
6The best way to extend executor is to respect the current boundaries instead of stretching the core model.
7
8## First question: is it really a source?
9
10Not every new integration should be modeled as a source.
11
12Use the source model when the thing is:
13
14- a persistent external system
15- with install-local configuration
16- with auth state
17- that materializes into callable tools
18
19If the new thing is really:
20
21- a local tool library
22- a package bundle
23- a browser automation runtime
24
25then it may need a different top-level primitive instead of being shoved into the source model.
26
27## If it is a source, add an adapter
28
29The happy path for a new integration is:
30
311. define the adapter key, catalog kind, and strategies
322. define the connect and binding config shape
333. materialize into documents, schema bundles, and operations
344. implement invocation
355. add tests against the canonical model
36
37The adapter should absorb provider-specific logic so the core does not have to.
38
39That includes auth behavior:
40
41- plugin-owned auth config
42- plugin-owned OAuth flows and refresh logic
43- core-owned secret material services
44
45## What a new adapter should not do
46
47Avoid these patterns:
48
49- adding new provider-specific columns to generic tables because one adapter wants them
50- storing expanded presentation output as canonical truth
51- bypassing the operation layer and invoking directly from raw documents
52- treating auth as provider-specific ad hoc fields on the source row
53
54Those are the patterns that lead back to churn.
55
56## The standard for a good extension
57
58A good new adapter should make the core system feel boring.
59
60That means:
61
62- persistence still looks like recipe/revision/operations
63- auth still uses the same secret material and private-state plumbing
64- auth behavior itself stays plugin-owned
65- discovery and inspection still work through the same catalog ideas
66- execution still delegates through the same tool and adapter boundaries
67
68If a new adapter needs deep new behavior in all of those places, that is usually a sign it may be a new primitive, not just a new adapter.
69
70## What feels like the next natural extensions
71
72The current architecture is well-suited to future additions like:
73
74- Google Discovery imports
75- Postman collection imports
76- snippet bundles
77- richer auth material providers
78
79Those should mostly be adapter and secret-material-layer work, not another persistence redesign.
80
81## Why this is the best setup
82
83The current setup is not trying to be abstract for its own sake.
84It is trying to keep the codebase in a state where:
85
86- core concepts stay stable
87- new support lands as extensions
88- source support does not require philosophy rewrites every few weeks
89
90That is the real reason the system is shaped around adapters, recipes, revisions, operations, and actor-scoped private state.
apps/docs/developer/import-fidelity.mdx+0−132
@@ -1,132 +0,0 @@
1---
2title: "Import Fidelity"
3description: "What executor preserves, normalizes, and currently loses when importing external formats."
4---
5
6Executor should normalize syntax, not meaning.
7
8That means the import pipeline can simplify surface differences between OpenAPI, GraphQL, Google Discovery, and MCP, but it should not silently erase semantics that change execution, trust, or user understanding.
9
10The practical shape is:
11
12- keep the raw source document or manifest
13- keep adapter-native metadata that does not fit the common model cleanly
14- promote execution-relevant semantics into first-class IR
15
16## The fidelity buckets
17
18This audit uses four buckets:
19
20- `Raw source`: the original imported document or manifest
21- `Adapter/native`: typed provider data or native blobs attached during import
22- `IR`: first-class catalog symbols and capability metadata
23- `UI today`: what current inspection surfaces directly
24
25Current inspection is still narrow. It mostly shows projected schemas plus `tool.executable.native[0]`, not all preserved native blobs or capability-native metadata.
26
27## OpenAPI
28
29| Source detail | Raw source | Adapter/native | IR | UI today | Notes |
30| --- | --- | --- | --- | --- | --- |
31| Operation auth requirements and security schemes | Yes | No | No | No | Operation `security` is not promoted; imported capabilities currently use `auth: none`. |
32| Exact response variants, status codes, and headers | Yes | Partial | No | Partial | Import chooses one preferred response, then projects one synthetic `2XX` response set. |
33| Parameter serialization (`style`, `explode`, `allowReserved`, content-based params) | Yes | Mostly no | No | No | The IR can model these, but the OpenAPI importer currently only takes parameter `schema`. |
34| Multiple request and response media types | Yes | Partial | Partial | Partial | Import picks a preferred content entry for typing; response docs keep content types only for the chosen response. |
35| `servers` defaults | Yes | No | No | No | The IR can represent scope server defaults, but OpenAPI import does not populate them. |
36| Rich schema semantics beyond the importer subset | Yes | Partial | Partial | Partial | Unsupported JSON Schema shapes fall back to `unknown` plus native preservation. |
37
38## GraphQL
39
40| Source detail | Raw source | Adapter/native | IR | UI today | Notes |
41| --- | --- | --- | --- | --- | --- |
42| Subscription root fields as tools | Yes | Partial | No | No | The manifest records `subscriptionTypeName`, but tool generation only materializes query and mutation fields. |
43| Full field result shapes for generated field tools | Yes | Partial | Partial | Partial | Field-tool outputs are heuristic projections, not full result types. |
44| Union and deep nested selections | Yes | Partial | Partial | Partial | Unions degrade to `__typename`; nested selection stops after depth 2 in generated field tools. |
45| Arbitrary caller-selected field projections | Yes | Partial | No | No | Generated field tools are compiled with fixed selections; the raw `request` tool is the escape hatch. |
46| Deprecation reasons | Yes | Partial | Partial | No | GraphQL schemas emit `deprecated: true` plus `x-deprecationReason`, but the generic schema importer only keeps `deprecated` as first-class shape metadata. |
47
48GraphQL is a special case: the main loss is not the raw source being discarded, but the generated field-tool projection being intentionally narrower than the schema. That tradeoff is reasonable for discoverability, but only if the system continues to preserve and expose the raw request path.
49
50## Google Discovery
51
52| Source detail | Raw source | Adapter/native | IR | UI today | Notes |
53| --- | --- | --- | --- | --- | --- |
54| OAuth scope descriptions | Yes | Partial | No | No | Document-level scope descriptions are extracted, but the IR security scheme rewrites scopes to `{ scope: scope }`. |
55| Rich media upload metadata | Yes | Partial | Partial | Partial | Upload support is collapsed to booleans such as `supportsMediaUpload` and `supportsMediaDownload`. |
56| Service-level metadata (`title`, `description`, `batchPath`, `documentationLink`) | Yes | Partial | No | No | The extracted manifest has these fields, but per-operation provider data does not carry them into the catalog. |
57| Parameter descriptions, repeated-ness, enums, defaults | Yes | Yes | Mostly yes | Mostly yes | This area is comparatively good. |
58| Request and response schemas | Yes | Yes | Yes | Yes | Core schema structure is preserved reasonably well. |
59
60Google Discovery is less lossy than OpenAPI on request and response typing, but it still loses useful service metadata and the human meaning attached to OAuth scopes.
61
62## MCP
63
64| Source detail | Raw source | Adapter/native | IR | UI today | Notes |
65| --- | --- | --- | --- | --- | --- |
66| Non-tool MCP resources such as prompts or resources | No | No | No | No | The adapter only discovers `listTools()`. Anything outside the tool list never enters import. |
67| Extra tool metadata beyond `name`, `description`, `inputSchema` or `parameters`, and `outputSchema` | Partial | Partial | No | No | The manifest schema keeps only a narrow subset of listed tool fields. |
68| Tool-level safety or effect hints, if present | Partial | No | No | No | Imported MCP operations are currently hardcoded to `effect: action`. |
69| Tool input and output schemas | Yes | Yes | Yes | Yes | This is the strongest part of the current MCP import. |
70| Server-level metadata outside the tool list | No | No | No | No | The stored MCP "document" is a synthesized tool manifest, not a richer upstream server description. |
71
72MCP is the most structurally lossy adapter today because the discovery step itself only imports tools and already narrows their metadata aggressively.
73
74## Cross-cutting UI caveat
75
76Even when data is preserved natively, the current inspection UI often does not show it directly.
77
78Today inspection primarily exposes:
79
80- projected call and result schemas
81- capability summary text
82- the first executable native blob
83
84That means "preserved in native data" and "visible to a user" are still different states.
85
86## What should be first-class in IR
87
88These semantics are worth promoting into the common model because they affect execution or trust:
89
90- auth and security requirements described by the source format
91- response and status variants
92- HTTP parameter serialization
93- upload, download, and streaming traits
94- GraphQL selection and subscription semantics
95- source-provided safety or idempotency hints
96
97By contrast, many provider-specific labels, service metadata fields, and raw source extras are fine to keep in adapter-native metadata as long as they remain inspectable.
98
99## Recommended priority order
100
101### P0
102
103- OpenAPI auth and security requirements
104- OpenAPI response variants and status distinctions
105- Google Discovery scope descriptions
106- MCP discovery breadth: decide whether executor intends to import only tools or broader MCP surface area
107
108### P1
109
110- OpenAPI parameter serialization
111- GraphQL subscription modeling
112- GraphQL field-tool projection metadata so the fixed-selection behavior is explicit
113- Google Discovery service metadata surfacing
114- MCP tool metadata preservation beyond the current narrow manifest
115
116### P2
117
118- Better UI fallback to native/provider metadata
119- Richer diagnostics for intentionally lossy normalization
120- A fixture-backed audit matrix test per adapter
121
122## The IR boundary
123
124The right boundary is:
125
126- `raw source` remains the source of truth
127- `adapter-native metadata` keeps source-specific details close to their format
128- `IR` stays unified, but it must be lossless for downstream semantics
129
130If executor keeps that line clear, a standardized representation is a strength.
131
132If executor lets semantically important details live only in hidden native blobs, the standardized representation becomes misleading.
apps/docs/developer/mcp.mdx+0−50
@@ -1,50 +0,0 @@
1---
2title: "MCP"
3description: "How executor uses MCP both as an adapter and as an external interface."
4---
5
6MCP shows up in executor in two different ways.
7
8## 1. Executor can connect to MCP servers
9
10This is the source side of MCP.
11
12When a user connects an MCP endpoint, executor treats it as an adapter with MCP-specific connection, discovery, invocation, and OAuth behavior. The control plane persists the binding and secret material state, while MCP-specific auth and execution behavior lives behind the adapter and codemode layers.
13
14This is why MCP is modeled as its own adapter instead of being flattened into a generic HTTP execution model.
15
16## 2. Executor can expose itself as an MCP server
17
18This is the outward-facing side of MCP.
19
20`packages/hosts/mcp` and the `/mcp` route in `packages/platform/server` let external MCP clients talk to the local executor runtime.
21
22That means external MCP hosts can:
23
24- discover executor tools
25- trigger executions
26- resume paused interactions
27- work against the same local workspace state as the CLI and browser UI
28
29## Why the distinction matters
30
31These two roles are related, but they are not the same thing:
32
33- inbound MCP means executor is acting as a client to a remote MCP server
34- outbound MCP means executor is acting as a server for external MCP clients
35
36Keeping that distinction clear avoids collapsing very different responsibilities into one vague "MCP integration" concept.
37
38## Where the logic lives
39
40Broadly:
41
42- control plane owns workspace state, bindings, secret material, and execution lifecycle
43- `packages/hosts/mcp` owns the local MCP server surface
44- `packages/platform/server` mounts the `/mcp` handler
45
46## Why MCP stays flexible in this model
47
48Because the host surface stays separate from source plugins, executor can support protocol-specific features such as sessions, elicitation, and resume without hard-wiring an MCP source adapter into the product shell.
49
50That is the main reason MCP is a first-class category in the architecture.
apps/docs/developer/overview.mdx+0−106
@@ -1,106 +0,0 @@
1---
2title: "Control Plane Overview"
3description: "How the control plane is structured today and how the major pieces fit together."
4---
5
6Executor is built around one central idea:
7
8- turn connected systems into a durable, discoverable tool catalog
9- run TypeScript against that catalog inside a controlled runtime
10- keep connection state, secret material, execution, and inspection in one local control plane
11
12## One-line architecture
13
14Executor is a local control plane that:
15
161. stores connected integrations as sources
172. materializes those sources into callable tools
183. exposes those tools to the runtime, the UI, and MCP clients
194. resolves secret material and policy at invocation time
20
21## Major packages
22
23### `apps/executor`
24
25The CLI and daemon manager.
26
27Responsibilities:
28
29- start and stop the local daemon
30- submit executions
31- resume paused interactions
32- seed and inspect sources in development flows
33
34### `packages/platform/server`
35
36The local server shell around the control plane.
37
38Responsibilities:
39
40- mount the HTTP API at `/v1`
41- mount the local MCP server at `/mcp`
42- serve the web UI
43
44### `packages/platform/sdk`
45
46The product core.
47
48Responsibilities:
49
50- source connection and lifecycle
51- secret material and private auth state
52- persistence
53- tool discovery and inspection
54- execution management
55- policy-aware invocation
56
57### Runtime and codemode packages
58
59These packages provide the execution and tool abstractions:
60
61- `packages/kernel/runtime-ses`
62- `packages/kernel/core`
63- `packages/clients/react`
64
65## The mental model
66
67The current architecture is easiest to understand if you think of it as an adapter-based compiler pipeline:
68
69- adapters ingest external formats and protocols
70- the control plane stores a canonical internal representation
71- the runtime executes against that representation
72
73That is why executor has strong internal concepts like:
74
75- source bindings
76- recipes and revisions
77- materialized operations
78- schema bundles
79- actor-scoped secret material
80
81Those are not UI concepts. They are the durable internal model that lets executor support multiple integration styles without rewriting the core every time.
82
83## What is intentionally centralized
84
85Executor intentionally centralizes a few things that many products split across multiple services:
86
87- local identity and workspace bootstrap
88- source connection state
89- secret material references
90- tool inspection data
91- execution history
92- interaction/resume state
93
94This is why the UI, CLI, and local MCP entrypoint can all behave like one system instead of separate tools bolted together.
95
96## Why the architecture is shaped this way
97
98The current implementation optimizes for:
99
100- local-first operation
101- strong schema-aware tool usage
102- reusable connected integrations
103- minimal provider-specific logic in the control plane core
104- extensibility through adapters instead of one-off execution paths
105
106The rest of the developer docs explain how those goals show up in the current model.
apps/docs/developer/persistence-and-migrations.mdx+0−86
@@ -1,86 +0,0 @@
1---
2title: "Persistence And Migrations"
3description: "How executor stores source state today and how migration logic is intended to work."
4---
5
6Executor persistence is designed around durable runtime state, not just CRUD storage.
7
8That means the database stores:
9
10- installation and workspace identity
11- source bindings
12- recipes and revisions
13- raw documents
14- schema bundles
15- operations
16- credentials and auth sessions
17- executions and interactions
18
19## Why the storage is layered
20
21The storage model is layered because these concerns have different reuse and ownership semantics.
22
23Examples:
24
25- raw documents are provenance
26- revisions are compiled materializations
27- operations are the hot-path query surface
28- source bindings are workspace-local
29- credentials are private and actor-scoped
30
31Trying to collapse those into one table or one document shape causes churn quickly.
32
33## What is normalized vs serialized
34
35Executor intentionally uses both relational columns and serialized JSON.
36
37Normalized:
38
39- ownership
40- ids and references
41- search and lookup fields
42- status and visibility
43- auth/session ownership
44
45Serialized:
46
47- adapter-owned binding config
48- source config blobs
49- provider metadata
50- some session payloads
51
52This is deliberate.
53The rule is to normalize what the core system queries and enforces, and serialize what belongs to adapter-specific contracts.
54
55## Migrations philosophy
56
57The intended migration model is:
58
59- SQL migrations move structural schema into the new shape
60- one-shot code migrations repair or rebuild data that SQL cannot derive sanely
61- steady-state runtime paths should not carry compatibility branches forever
62
63That last point matters.
64Compatibility logic belongs in upgrade code, not in normal reads and writes.
65
66## What is still acceptable in migration code
67
68One-shot rebuilds are still acceptable when:
69
70- the old data shape is structurally valid but not yet in the best compiled form
71- the rebuild can be derived from already-stored documents
72- the logic is ledgered and run once
73
74What is not acceptable:
75
76- provider-specific legacy fallbacks in steady-state runtime logic
77- carrying old columns indefinitely because one path still reads them
78
79## Why this supports extensibility
80
81This persistence shape is meant to let new integrations land in one of two ways:
82
83- add a new adapter that compiles into the same source model
84- add a genuinely new top-level primitive if the thing is not really a source
85
86That is how executor avoids turning every new integration into another schema redesign.
apps/docs/developer/tool-catalog-and-execution.mdx+0−91
@@ -1,91 +0,0 @@
1---
2title: "Tool Catalog And Execution"
3description: "How source materialization becomes callable tools and how invocation works."
4---
5
6Executor turns connected integrations into a workspace tool catalog.
7
8That catalog is the thing the runtime actually executes against.
9
10The execution sandbox is selected from `.executor/executor.jsonc`:
11
12```jsonc
13{
14 "runtime": "quickjs"
15}
16```
17
18Supported values are `quickjs` (default), `ses`, and `deno`.
19
20## How tools are produced
21
22At a high level:
23
241. an adapter materializes a source
252. the materialization writes documents, schema bundles, and operations
263. the control plane loads those persisted operations into a workspace tool index
274. the runtime invokes one tool at a time through the control plane
28
29This means executor does not rebuild every integration from scratch on every execution.
30
31## Why operations exist as a separate layer
32
33The operation layer exists to support:
34
35- search
36- inspection
37- policy
38- fast tool lookup
39- tool description without loading huge raw documents every time
40
41Raw upstream documents are still kept, but they are not the best hot-path query surface.
42
43## Schema transport
44
45Executor uses compact operation-root schemas plus shared schema bundles.
46
47This is better than storing expanded per-tool schema blobs because it avoids:
48
49- massive duplicate schema payloads
50- slow formatting and rendering
51- treating presentation output as canonical storage
52
53The result is:
54
55- smaller payloads
56- more reusable schema graphs
57- cleaner tool detail responses
58
59## Tool catalog shape
60
61The tool catalog is built from:
62
63- persisted source tools
64- built-in executor tools
65
66That lets executor present one unified discovery and invocation surface even though the underlying tools come from different places.
67
68## Invocation flow
69
70At a high level, invocation works like this:
71
721. resolve the persisted operation by tool path
732. run policy and approval checks
743. resolve auth material for the correct slot
754. load the manifest and schema bundle needed for the call
765. delegate actual invocation to the adapter
77
78This is the key point:
79
80- the control plane orchestrates
81- the adapter performs the protocol-specific call
82
83## Why this is flexible
84
85This architecture is flexible because it keeps a stable middle:
86
87- the runtime only needs tools and invokers
88- the control plane only needs the canonical operation and auth model
89- adapters can vary widely at the source boundary without changing the runtime contract
90
91That is what makes it possible to add more source types without constantly rewriting execution logic.
apps/docs/docs.json+2−37
@@ -11,50 +11,15 @@ · 10 unmodified lines
11 "navigation": {11 "navigation": {
12 "tabs": [12 "tabs": [
13 {13 {
14 "tab": "Product",14 "tab": "Docs",
15 "groups": [15 "groups": [
16 {16 {
17 "group": "Introduction",17 "group": "Status",
18 "pages": [18 "pages": [
19 "introduction"19 "introduction"
20 ]20 ]
21 }21 }
22 ]22 ]
23 },
24 {
25 "tab": "Developer",
26 "groups": [
27 {
28 "group": "Control Plane",
29 "pages": [
30 "developer/overview",
31 "developer/core-model",
32 "developer/adapters-and-formats",
33 "developer/credentials-and-auth",
34 "developer/tool-catalog-and-execution",
35 "developer/persistence-and-migrations",
36 "developer/extending-executor"
37 ]
38 },
39 {
40 "group": "CLI",
41 "pages": [
42 "developer/cli"
43 ]
44 },
45 {
46 "group": "MCP",
47 "pages": [
48 "developer/mcp"
49 ]
50 },
51 {
52 "group": "Code Mode",
53 "pages": [
54 "developer/codemode"
55 ]
56 }
57 ]
58 }23 }
59 ]24 ]
60 },25 },
apps/docs/introduction.mdx+2−12
@@ -5,16 +5,6 @@ description: "A local-first execution environment for AI agents." · 4 unmodified lines
5 5
6`executor` is a local-first execution environment for AI agents. It gives an agent a TypeScript runtime, a discoverable tool catalog, and a single local place to connect external systems such as MCP servers, OpenAPI APIs, and GraphQL APIs.6`executor` is a local-first execution environment for AI agents. It gives an agent a TypeScript runtime, a discoverable tool catalog, and a single local place to connect external systems such as MCP servers, OpenAPI APIs, and GraphQL APIs.
7 7
8This docs site is split into two documentation surfaces:8The docs set has been intentionally removed for now while the plugin architecture is still shifting.
9 9
10- product-facing docs for how to install and use executor10Use the repository source directly if you need current implementation details.
11- developer-facing docs for how executor is built and how to extend it
12
13The product-facing section will come later. The current docs start with the developer-facing architecture and implementation model.
14
15If you are trying to understand the current system shape, start with:
16
17- [Control Plane](/developer/overview)
18- [CLI](/developer/cli)
19- [MCP](/developer/mcp)
20- [Code Mode](/developer/codemode)
apps/web/src/components/shell.tsx+1−1
@@ -1,7 +1,7 @@
1import { Link, Outlet, useLocation, useMatchRoute } from "@tanstack/react-router";1import { Link, Outlet, useLocation, useMatchRoute } from "@tanstack/react-router";
2import { useCallback, useEffect, useState } from "react";2import { useCallback, useEffect, useState } from "react";
3import { useSources, type Source } from "@executor/react";3import { useSources, type Source } from "@executor/react";
4import { sourcePluginsIndexPath } from "@executor/react/source-plugins";4import { sourcePluginsIndexPath } from "@executor/react/plugins";
5import { cn } from "../lib/utils";5import { cn } from "../lib/utils";
6import { IconPlus, IconCopy, IconCheck } from "./icons";6import { IconPlus, IconCopy, IconCheck } from "./icons";
7import { LoadableBlock } from "./loadable";7import { LoadableBlock } from "./loadable";
apps/web/src/components/source-not-found-state.tsx+1−1
@@ -1,5 +1,5 @@
1import { Link } from "@tanstack/react-router";1import { Link } from "@tanstack/react-router";
2import { sourcePluginsIndexPath } from "@executor/react/source-plugins";2import { sourcePluginsIndexPath } from "@executor/react/plugins";
3 3
4import { IconEmpty } from "./icons";4import { IconEmpty } from "./icons";
5import { cn } from "../lib/utils";5import { cn } from "../lib/utils";
apps/web/src/main.tsx+70−9
@@ -8,21 +8,25 @@ import { · 7 unmodified lines
8} from "@tanstack/react-router";8} from "@tanstack/react-router";
9import { ExecutorReactProvider } from "@executor/react";9import { ExecutorReactProvider } from "@executor/react";
10import {10import {
11 createExecutorPluginPaths,
11 createSourcePluginPaths,12 createSourcePluginPaths,
12 normalizeSourcePluginPath,13 normalizeSourcePluginPath,
13 sourcePluginsIndexPath,14 sourcePluginsIndexPath,
15 type ExecutorPluginNavigation,
16 type FrontendPluginRouteSearch,
14 type FrontendSourceTypeDefinition,17 type FrontendSourceTypeDefinition,
15 type SourcePluginNavigation,18 type SourcePluginNavigation,
16 type SourcePluginRouteSearch,19} from "@executor/react/plugins";
17} from "@executor/react/source-plugins";
18 20
19import "./globals.css";21import "./globals.css";
20 22
21import { AppShell } from "./components/shell";23import { AppShell } from "./components/shell";
22import {24import {
25 registeredFrontendPluginRoutes,
23 registeredSourceFrontendTypes,26 registeredSourceFrontendTypes,
24} from "./source-plugins";27} from "./source-plugins";
25import {28import {
29 ExecutorPluginRoutePage,
26 SourcePluginAddPage,30 SourcePluginAddPage,
27 SourcePluginDetailChildPage,31 SourcePluginDetailChildPage,
28 SourcePluginDetailPage,32 SourcePluginDetailPage,
@@ -54,15 +58,37 @@ const secretsRoute = createRoute({ · 25 unmodified lines
54 component: SecretsPage,58 component: SecretsPage,
55});59});
56 60
57const createPluginNavigation = (61const createExecutorPluginNavigation = (
62 pluginKey: string,
63 input: {
64 navigateTo: (
65 to: string,
66 search?: FrontendPluginRouteSearch,
67 ) => void | Promise<void>;
68 updateSearch?: (
69 search: FrontendPluginRouteSearch,
70 ) => void | Promise<void>;
71 },
72): ExecutorPluginNavigation => {
73 const paths = createExecutorPluginPaths(pluginKey);
74
75 return {
76 paths,
77 home: () => input.navigateTo("/"),
78 route: (path = "", search) => input.navigateTo(paths.route(path), search),
79 updateSearch: (search) => input.updateSearch?.(search),
80 };
81};
82
83const createSourcePluginNavigation = (
58 definition: FrontendSourceTypeDefinition,84 definition: FrontendSourceTypeDefinition,
59 input: {85 input: {
60 navigateTo: (86 navigateTo: (
61 to: string,87 to: string,
62 search?: SourcePluginRouteSearch,88 search?: FrontendPluginRouteSearch,
63 ) => void | Promise<void>;89 ) => void | Promise<void>;
64 updateSearch?: (90 updateSearch?: (
65 search: SourcePluginRouteSearch,91 search: FrontendPluginRouteSearch,
66 ) => void | Promise<void>;92 ) => void | Promise<void>;
67 },93 },
68): SourcePluginNavigation => {94): SourcePluginNavigation => {
@@ -80,12 +106,46 @@ const createPluginNavigation = ( · 11 unmodified lines
80 };106 };
81};107};
82 108
109const frontendPluginRoutes = registeredFrontendPluginRoutes.map(({ plugin, route }) => {
110 const paths = createExecutorPluginPaths(plugin.key);
111
112 const PluginRouteComponent = () => {
113 const params = pluginRoute.useParams() as Record<string, string | undefined>;
114 const search = pluginRoute.useSearch();
115 const navigate = useNavigate();
116 const navigateFromRoute = useNavigate({ from: pluginRoute.fullPath });
117 const navigation = createExecutorPluginNavigation(plugin.key, {
118 navigateTo: (to, nextSearch) =>
119 nextSearch === undefined ? navigate({ to }) : navigate({ to, search: nextSearch }),
120 updateSearch: (nextSearch) => navigateFromRoute({ search: nextSearch }),
121 });
122
123 return (
124 <ExecutorPluginRoutePage
125 pluginKey={plugin.key}
126 routeKey={route.key}
127 params={params}
128 search={search}
129 navigation={navigation}
130 />
131 );
132 };
133
134 const pluginRoute = createRoute({
135 getParentRoute: () => rootRoute,
136 path: paths.routePattern(route.path ?? ""),
137 component: PluginRouteComponent,
138 });
139
140 return pluginRoute;
141});
142
83const sourcePluginRoutes = registeredSourceFrontendTypes.flatMap((definition) => {143const sourcePluginRoutes = registeredSourceFrontendTypes.flatMap((definition) => {
84 const paths = createSourcePluginPaths(definition.key);144 const paths = createSourcePluginPaths(definition.key);
85 145
86 const AddRouteComponent = () => {146 const AddRouteComponent = () => {
87 const navigate = useNavigate();147 const navigate = useNavigate();
88 const navigation = createPluginNavigation(definition, {148 const navigation = createSourcePluginNavigation(definition, {
89 navigateTo: (to, search) =>149 navigateTo: (to, search) =>
90 search === undefined ? navigate({ to }) : navigate({ to, search }),150 search === undefined ? navigate({ to }) : navigate({ to, search }),
91 });151 });
@@ -109,7 +169,7 @@ const sourcePluginRoutes = registeredSourceFrontendTypes.flatMap((definition) => · 17 unmodified lines
109 sourceId: string;169 sourceId: string;
110 };170 };
111 const navigate = useNavigate();171 const navigate = useNavigate();
112 const navigation = createPluginNavigation(definition, {172 const navigation = createSourcePluginNavigation(definition, {
113 navigateTo: (to, search) =>173 navigateTo: (to, search) =>
114 search === undefined ? navigate({ to }) : navigate({ to, search }),174 search === undefined ? navigate({ to }) : navigate({ to, search }),
115 });175 });
@@ -137,7 +197,7 @@ const sourcePluginRoutes = registeredSourceFrontendTypes.flatMap((definition) => · 21 unmodified lines
137 const search = detailRoute.useSearch();197 const search = detailRoute.useSearch();
138 const navigate = useNavigate();198 const navigate = useNavigate();
139 const navigateFromRoute = useNavigate({ from: detailRoute.fullPath });199 const navigateFromRoute = useNavigate({ from: detailRoute.fullPath });
140 const navigation = createPluginNavigation(definition, {200 const navigation = createSourcePluginNavigation(definition, {
141 navigateTo: (to, nextSearch) =>201 navigateTo: (to, nextSearch) =>
142 nextSearch === undefined ? navigate({ to }) : navigate({ to, search: nextSearch }),202 nextSearch === undefined ? navigate({ to }) : navigate({ to, search: nextSearch }),
143 updateSearch: (nextSearch) => navigateFromRoute({ search: nextSearch }),203 updateSearch: (nextSearch) => navigateFromRoute({ search: nextSearch }),
@@ -168,7 +228,7 @@ const sourcePluginRoutes = registeredSourceFrontendTypes.flatMap((definition) => · 24 unmodified lines
168 const search = childRoute.useSearch();228 const search = childRoute.useSearch();
169 const navigate = useNavigate();229 const navigate = useNavigate();
170 const navigateFromRoute = useNavigate({ from: childRoute.fullPath });230 const navigateFromRoute = useNavigate({ from: childRoute.fullPath });
171 const navigation = createPluginNavigation(definition, {231 const navigation = createSourcePluginNavigation(definition, {
172 navigateTo: (to, nextSearch) =>232 navigateTo: (to, nextSearch) =>
173 nextSearch === undefined ? navigate({ to }) : navigate({ to, search: nextSearch }),233 nextSearch === undefined ? navigate({ to }) : navigate({ to, search: nextSearch }),
174 updateSearch: (nextSearch) => navigateFromRoute({ search: nextSearch }),234 updateSearch: (nextSearch) => navigateFromRoute({ search: nextSearch }),
@@ -208,6 +268,7 @@ const sourcePluginRoutes = registeredSourceFrontendTypes.flatMap((definition) => · 33 unmodified lines
208const routeTree = rootRoute.addChildren([268const routeTree = rootRoute.addChildren([
209 homeRoute,269 homeRoute,
210 sourcePluginsIndexRoute,270 sourcePluginsIndexRoute,
271 ...frontendPluginRoutes,
211 ...sourcePluginRoutes,272 ...sourcePluginRoutes,
212 secretsRoute,273 secretsRoute,
213]);274]);
apps/web/src/source-plugins/index.ts+25−6
@@ -14,7 +14,7 @@ import { · 13 unmodified lines
14 createSourcePluginPaths,14 createSourcePluginPaths,
15 registerExecutorFrontendPlugins,15 registerExecutorFrontendPlugins,
16 type ExecutorFrontendPlugin,16 type ExecutorFrontendPlugin,
17} from "@executor/react/source-plugins";17} from "@executor/react/plugins";
18 18
19const frontendPlugins = [19const frontendPlugins = [
20 McpReactPlugin,20 McpReactPlugin,
@@ -23,18 +23,37 @@ const frontendPlugins = [ · 2 unmodified lines
23 OpenApiReactPlugin,23 OpenApiReactPlugin,
24] as const satisfies readonly ExecutorFrontendPlugin[];24] as const satisfies readonly ExecutorFrontendPlugin[];
25 25
26const frontendSourceRegistry = registerExecutorFrontendPlugins(frontendPlugins);26const frontendPluginRegistry = registerExecutorFrontendPlugins(frontendPlugins);
27 27
28export const registeredSourceFrontendTypes = frontendSourceRegistry.sourceTypes;28export const registeredFrontendPlugins = frontendPluginRegistry.plugins;
29export const registeredFrontendPluginRoutes = frontendPluginRegistry.routes;
30export const registeredSourceFrontendTypeEntries =
31 frontendPluginRegistry.sourceTypes;
32export const registeredSourceFrontendTypes =
33 registeredSourceFrontendTypeEntries.map((entry) => entry.definition);
34
35export const getFrontendPlugin = (key: string) =>
36 frontendPluginRegistry.getPlugin(key);
37
38export const getFrontendPluginRoute = (
39 pluginKey: string,
40 routeKey: string,
41) => frontendPluginRegistry.getRoute(pluginKey, routeKey);
42
43export const getSourceFrontendTypeEntry = (kind: string) =>
44 frontendPluginRegistry.getSourceType(kind);
45
46export const getSourceFrontendTypeEntryByKey = (key: string) =>
47 frontendPluginRegistry.getSourceTypeByKey(key);
29 48
30export const getSourceFrontendType = (kind: string) =>49export const getSourceFrontendType = (kind: string) =>
31 frontendSourceRegistry.getSourceType(kind);50 getSourceFrontendTypeEntry(kind)?.definition ?? null;
32 51
33export const getSourceFrontendTypeByKey = (key: string) =>52export const getSourceFrontendTypeByKey = (key: string) =>
34 frontendSourceRegistry.getSourceTypeByKey(key);53 getSourceFrontendTypeEntryByKey(key)?.definition ?? null;
35 54
36export const getDefaultSourceFrontendType = () =>55export const getDefaultSourceFrontendType = () =>
37 frontendSourceRegistry.getDefaultSourceType();56 frontendPluginRegistry.getDefaultSourceType()?.definition ?? null;
38 57
39export const getSourceFrontendPaths = (kind: string) => {58export const getSourceFrontendPaths = (kind: string) => {
40 const definition = getSourceFrontendType(kind);59 const definition = getSourceFrontendType(kind);
apps/web/src/source-plugins/pages.tsx+106−47
@@ -1,22 +1,43 @@
1import { Link } from "@tanstack/react-router";1import { Link } from "@tanstack/react-router";
2import { useSource } from "@executor/react";2import { useSource } from "@executor/react";
3import {3import {
4 ExecutorPluginRouteProvider,
4 LoadableBlock,5 LoadableBlock,
5 SourcePluginRouteProvider,6 SourcePluginRouteProvider,
6 createSourcePluginPaths,7 createSourcePluginPaths,
8 type ExecutorPluginNavigation,
9 type FrontendPluginRouteDefinition,
10 type FrontendPluginRouteParams,
11 type FrontendPluginRouteSearch,
7 type FrontendSourceDetailRouteDefinition,12 type FrontendSourceDetailRouteDefinition,
8 type SourcePluginNavigation,13 type SourcePluginNavigation,
9 type SourcePluginRouteParams,14 type SourcePluginRouteParams,
10 type SourcePluginRouteSearch,15 type SourcePluginRouteSearch,
11} from "@executor/react/source-plugins";16} from "@executor/react/plugins";
12 17
13import { SourcePluginsResetState } from "../components/source-plugins-reset-state";18import { SourcePluginsResetState } from "../components/source-plugins-reset-state";
14import {19import {
15 getSourceFrontendType,20 getFrontendPlugin,
16 getSourceFrontendTypeByKey,21 getFrontendPluginRoute,
22 getSourceFrontendTypeEntry,
23 getSourceFrontendTypeEntryByKey,
17 registeredSourceFrontendTypes,24 registeredSourceFrontendTypes,
18} from "./index";25} from "./index";
19 26
27const FrontendPluginUnavailableState = () => (
28 <SourcePluginsResetState
29 title="Frontend plugins are unavailable"
30 message="No frontend plugins are registered in this build."
31 />
32);
33
34const SourcePluginUnavailableState = () => (
35 <SourcePluginsResetState
36 title="Source plugins are unavailable"
37 message="No source plugins are registered in this build."
38 />
39);
40
20const SourcePluginPicker = (props: {41const SourcePluginPicker = (props: {
21 activeKey: string | null;42 activeKey: string | null;
22}) => {43}) => {
@@ -43,13 +64,6 @@ const SourcePluginPicker = (props: { · 20 unmodified lines
43 );64 );
44};65};
45 66
46const SourcePluginUnavailableState = () => (
47 <SourcePluginsResetState
48 title="Source plugins are unavailable"
49 message="No source plugins are registered in this build."
50 />
51);
52
53const SourcePluginRouteMismatchState = (props: {67const SourcePluginRouteMismatchState = (props: {
54 requestedDisplayName: string;68 requestedDisplayName: string;
55 actualDisplayName: string;69 actualDisplayName: string;
@@ -60,6 +74,47 @@ const SourcePluginRouteMismatchState = (props: { · 4 unmodified lines
60 />74 />
61);75);
62 76
77export function ExecutorPluginRoutePage(input: {
78 pluginKey: string;
79 routeKey: FrontendPluginRouteDefinition["key"];
80 params: FrontendPluginRouteParams;
81 search: FrontendPluginRouteSearch;
82 navigation: ExecutorPluginNavigation;
83}) {
84 const plugin = getFrontendPlugin(input.pluginKey);
85 if (plugin === null) {
86 return <FrontendPluginUnavailableState />;
87 }
88
89 const routeEntry = getFrontendPluginRoute(input.pluginKey, input.routeKey);
90 if (routeEntry === null) {
91 return (
92 <SourcePluginsResetState
93 title="Plugin route is unavailable"
94 message={`No plugin route is registered for "${input.routeKey}" in "${input.pluginKey}".`}
95 />
96 );
97 }
98
99 const PluginPage = routeEntry.route.component;
100
101 return (
102 <ExecutorPluginRouteProvider
103 value={{
104 plugin,
105 route: routeEntry.route,
106 params: input.params,
107 search: input.search,
108 navigation: input.navigation,
109 }}
110 >
111 <div className="h-full min-h-0">
112 <PluginPage />
113 </div>
114 </ExecutorPluginRouteProvider>
115 );
116}
117
63export function SourcePluginsIndexPage() {118export function SourcePluginsIndexPage() {
64 if (registeredSourceFrontendTypes.length === 0) {119 if (registeredSourceFrontendTypes.length === 0) {
65 return <SourcePluginUnavailableState />;120 return <SourcePluginUnavailableState />;
@@ -114,17 +169,18 @@ export function SourcePluginAddPage(props: { · 48 unmodified lines
114 definitionKey: string;169 definitionKey: string;
115 navigation: SourcePluginNavigation;170 navigation: SourcePluginNavigation;
116}) {171}) {
117 const definition = getSourceFrontendTypeByKey(props.definitionKey);172 const entry = getSourceFrontendTypeEntryByKey(props.definitionKey);
118 if (definition === null) {173 if (entry === null) {
119 return <SourcePluginUnavailableState />;174 return <SourcePluginUnavailableState />;
120 }175 }
121 176
122 const AddPage = definition.renderAddPage;177 const AddPage = entry.definition.renderAddPage;
123 178
124 return (179 return (
125 <SourcePluginRouteProvider180 <SourcePluginRouteProvider
126 value={{181 value={{
127 definition,182 plugin: entry.plugin,
183 definition: entry.definition,
128 params: {},184 params: {},
129 search: {},185 search: {},
130 navigation: props.navigation,186 navigation: props.navigation,
@@ -136,16 +192,16 @@ export function SourcePluginAddPage(props: { · 5 unmodified lines
136 Source Plugin192 Source Plugin
137 </div>193 </div>
138 <h1 className="mt-5 font-display text-3xl tracking-tight text-foreground lg:text-4xl">194 <h1 className="mt-5 font-display text-3xl tracking-tight text-foreground lg:text-4xl">
139 {definition.displayName}195 {entry.definition.displayName}
140 </h1>196 </h1>
141 {definition.description && (197 {entry.definition.description && (
142 <p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground">198 <p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground">
143 {definition.description}199 {entry.definition.description}
144 </p>200 </p>
145 )}201 )}
146 </div>202 </div>
147 203
148 <SourcePluginPicker activeKey={definition.key} />204 <SourcePluginPicker activeKey={entry.definition.key} />
149 <AddPage />205 <AddPage />
150 </div>206 </div>
151 </SourcePluginRouteProvider>207 </SourcePluginRouteProvider>
@@ -159,18 +215,18 @@ export function SourcePluginEditPage(input: { · 7 unmodified lines
159 search?: SourcePluginRouteSearch;215 search?: SourcePluginRouteSearch;
160 navigation: SourcePluginNavigation;216 navigation: SourcePluginNavigation;
161}) {217}) {
162 const requestedDefinition = getSourceFrontendTypeByKey(input.definitionKey);218 const requestedEntry = getSourceFrontendTypeEntryByKey(input.definitionKey);
163 const source = useSource(input.sourceId);219 const source = useSource(input.sourceId);
164 220
165 if (requestedDefinition === null) {221 if (requestedEntry === null) {
166 return <SourcePluginUnavailableState />;222 return <SourcePluginUnavailableState />;
167 }223 }
168 224
169 return (225 return (
170 <LoadableBlock loadable={source} loading="Loading source...">226 <LoadableBlock loadable={source} loading="Loading source...">
171 {(loadedSource) => {227 {(loadedSource) => {
172 const definition = getSourceFrontendType(loadedSource.kind);228 const entry = getSourceFrontendTypeEntry(loadedSource.kind);
173 if (definition === null) {229 if (entry === null) {
174 return (230 return (
175 <SourcePluginsResetState231 <SourcePluginsResetState
176 title="Source plugin is unavailable"232 title="Source plugin is unavailable"
@@ -179,16 +235,16 @@ export function SourcePluginEditPage(input: { · 2 unmodified lines
179 );235 );
180 }236 }
181 237
182 if (definition.key !== requestedDefinition.key) {238 if (entry.definition.key !== requestedEntry.definition.key) {
183 return (239 return (
184 <SourcePluginRouteMismatchState240 <SourcePluginRouteMismatchState
185 requestedDisplayName={requestedDefinition.displayName}241 requestedDisplayName={requestedEntry.definition.displayName}
186 actualDisplayName={definition.displayName}242 actualDisplayName={entry.definition.displayName}
187 />243 />
188 );244 );
189 }245 }
190 246
191 if (definition.renderEditPage === undefined) {247 if (entry.definition.renderEditPage === undefined) {
192 return (248 return (
193 <SourcePluginsResetState249 <SourcePluginsResetState
194 title="Source editing is disabled"250 title="Source editing is disabled"
@@ -197,12 +253,13 @@ export function SourcePluginEditPage(input: { · 2 unmodified lines
197 );253 );
198 }254 }
199 255
200 const EditPage = definition.renderEditPage;256 const EditPage = entry.definition.renderEditPage;
201 257
202 return (258 return (
203 <SourcePluginRouteProvider259 <SourcePluginRouteProvider
204 value={{260 value={{
205 definition,261 plugin: entry.plugin,
262 definition: entry.definition,
206 params: input.params ?? {263 params: input.params ?? {
207 sourceId: input.sourceId,264 sourceId: input.sourceId,
208 },265 },
@@ -227,18 +284,18 @@ export function SourcePluginDetailPage(input: { · 18 unmodified lines
227 search: SourcePluginRouteSearch;284 search: SourcePluginRouteSearch;
228 navigation: SourcePluginNavigation;285 navigation: SourcePluginNavigation;
229}) {286}) {
230 const requestedDefinition = getSourceFrontendTypeByKey(input.definitionKey);287 const requestedEntry = getSourceFrontendTypeEntryByKey(input.definitionKey);
231 const source = useSource(input.sourceId);288 const source = useSource(input.sourceId);
232 289
233 if (requestedDefinition === null) {290 if (requestedEntry === null) {
234 return <SourcePluginUnavailableState />;291 return <SourcePluginUnavailableState />;
235 }292 }
236 293
237 return (294 return (
238 <LoadableBlock loadable={source} loading="Loading source...">295 <LoadableBlock loadable={source} loading="Loading source...">
239 {(loadedSource) => {296 {(loadedSource) => {
240 const definition = getSourceFrontendType(loadedSource.kind);297 const entry = getSourceFrontendTypeEntry(loadedSource.kind);
241 if (definition === null) {298 if (entry === null) {
242 return (299 return (
243 <SourcePluginsResetState300 <SourcePluginsResetState
244 title="Source plugin is unavailable"301 title="Source plugin is unavailable"
@@ -247,16 +304,16 @@ export function SourcePluginDetailPage(input: { · 2 unmodified lines
247 );304 );
248 }305 }
249 306
250 if (definition.key !== requestedDefinition.key) {307 if (entry.definition.key !== requestedEntry.definition.key) {
251 return (308 return (
252 <SourcePluginRouteMismatchState309 <SourcePluginRouteMismatchState
253 requestedDisplayName={requestedDefinition.displayName}310 requestedDisplayName={requestedEntry.definition.displayName}
254 actualDisplayName={definition.displayName}311 actualDisplayName={entry.definition.displayName}
255 />312 />
256 );313 );
257 }314 }
258 315
259 if (definition.renderDetailPage === undefined) {316 if (entry.definition.renderDetailPage === undefined) {
260 return (317 return (
261 <SourcePluginsResetState318 <SourcePluginsResetState
262 title="Source detail is disabled"319 title="Source detail is disabled"
@@ -265,12 +322,13 @@ export function SourcePluginDetailPage(input: { · 2 unmodified lines
265 );322 );
266 }323 }
267 324
268 const DetailPage = definition.renderDetailPage;325 const DetailPage = entry.definition.renderDetailPage;
269 326
270 return (327 return (
271 <SourcePluginRouteProvider328 <SourcePluginRouteProvider
272 value={{329 value={{
273 definition,330 plugin: entry.plugin,
331 definition: entry.definition,
274 params: input.params ?? {332 params: input.params ?? {
275 sourceId: input.sourceId,333 sourceId: input.sourceId,
276 },334 },
@@ -296,18 +354,18 @@ export function SourcePluginDetailChildPage(input: { · 19 unmodified lines
296 search: SourcePluginRouteSearch;354 search: SourcePluginRouteSearch;
297 navigation: SourcePluginNavigation;355 navigation: SourcePluginNavigation;
298}) {356}) {
299 const requestedDefinition = getSourceFrontendTypeByKey(input.definitionKey);357 const requestedEntry = getSourceFrontendTypeEntryByKey(input.definitionKey);
300 const source = useSource(input.sourceId);358 const source = useSource(input.sourceId);
301 359
302 if (requestedDefinition === null) {360 if (requestedEntry === null) {
303 return <SourcePluginUnavailableState />;361 return <SourcePluginUnavailableState />;
304 }362 }
305 363
306 return (364 return (
307 <LoadableBlock loadable={source} loading="Loading source...">365 <LoadableBlock loadable={source} loading="Loading source...">
308 {(loadedSource) => {366 {(loadedSource) => {
309 const definition = getSourceFrontendType(loadedSource.kind);367 const entry = getSourceFrontendTypeEntry(loadedSource.kind);
310 if (definition === null) {368 if (entry === null) {
311 return (369 return (
312 <SourcePluginsResetState370 <SourcePluginsResetState
313 title="Source plugin is unavailable"371 title="Source plugin is unavailable"
@@ -316,16 +374,16 @@ export function SourcePluginDetailChildPage(input: { · 2 unmodified lines
316 );374 );
317 }375 }
318 376
319 if (definition.key !== requestedDefinition.key) {377 if (entry.definition.key !== requestedEntry.definition.key) {
320 return (378 return (
321 <SourcePluginRouteMismatchState379 <SourcePluginRouteMismatchState
322 requestedDisplayName={requestedDefinition.displayName}380 requestedDisplayName={requestedEntry.definition.displayName}
323 actualDisplayName={definition.displayName}381 actualDisplayName={entry.definition.displayName}
324 />382 />
325 );383 );
326 }384 }
327 385
328 const detailRoute = definition.detailRoutes?.find((route) =>386 const detailRoute = entry.definition.detailRoutes?.find((route) =>
329 route.key === input.routeKey387 route.key === input.routeKey
330 );388 );
331 389
@@ -343,7 +401,8 @@ export function SourcePluginDetailChildPage(input: { · 11 unmodified lines
343 return (401 return (
344 <SourcePluginRouteProvider402 <SourcePluginRouteProvider
345 value={{403 value={{
346 definition,404 plugin: entry.plugin,
405 definition: entry.definition,
347 params: input.params,406 params: input.params,
348 search: input.search,407 search: input.search,
349 navigation: input.navigation,408 navigation: input.navigation,
apps/web/src/views/home.tsx+1−1
@@ -1,6 +1,6 @@
1import { Link } from "@tanstack/react-router";1import { Link } from "@tanstack/react-router";
2import { useSources } from "@executor/react";2import { useSources } from "@executor/react";
3import { sourcePluginsIndexPath } from "@executor/react/source-plugins";3import { sourcePluginsIndexPath } from "@executor/react/plugins";
4import { LoadableBlock } from "../components/loadable";4import { LoadableBlock } from "../components/loadable";
5import { Badge } from "../components/ui/badge";5import { Badge } from "../components/ui/badge";
6import { Button } from "../components/ui/button";6import { Button } from "../components/ui/button";
packages/clients/react/package.json+1−0
@@ -4,6 +4,7 @@ · 3 unmodified lines
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 ".": "./src/index.ts",6 ".": "./src/index.ts",
7 "./plugins": "./src/plugins/index.ts",
7 "./source-plugins": "./src/source-plugins/index.ts"8 "./source-plugins": "./src/source-plugins/index.ts"
8 },9 },
9 "scripts": {10 "scripts": {
packages/clients/react/src/plugins/index.ts+1−0
@@ -0,0 +1 @@
1export * from "../source-plugins/index";
packages/clients/react/src/source-plugins/index.ts+26−1
@@ -1,6 +1,10 @@
1export type {1export type {
2 ExecutorFrontendPlugin,2 ExecutorFrontendPlugin,
3 ExecutorFrontendPluginApi,3 ExecutorPluginNavigation,
4 ExecutorPluginRouteContextValue,
5 FrontendPluginRouteDefinition,
6 FrontendPluginRouteParams,
7 FrontendPluginRouteSearch,
4 FrontendSourceDetailRouteDefinition,8 FrontendSourceDetailRouteDefinition,
5 FrontendSourceTypeDefinition,9 FrontendSourceTypeDefinition,
6 SourcePluginNavigation,10 SourcePluginNavigation,
@@ -10,7 +14,12 @@ export type { · 3 unmodified lines
10} from "./types";14} from "./types";
11 15
12export {16export {
17 createExecutorPluginPaths,
13 createSourcePluginPaths,18 createSourcePluginPaths,
19 executorPluginBasePath,
20 executorPluginRoutePath,
21 executorPluginRoutePattern,
22 normalizeExecutorPluginPath,
14 normalizeSourcePluginPath,23 normalizeSourcePluginPath,
15 sourcePluginAddPath,24 sourcePluginAddPath,
16 sourcePluginChildPath,25 sourcePluginChildPath,
@@ -20,17 +29,33 @@ export { · 3 unmodified lines
20 sourcePluginEditPath,29 sourcePluginEditPath,
21 sourcePluginEditPattern,30 sourcePluginEditPattern,
22 sourcePluginsIndexPath,31 sourcePluginsIndexPath,
32 type ExecutorPluginPaths,
23 type SourcePluginPaths,33 type SourcePluginPaths,
24} from "./paths";34} from "./paths";
25 35
26export {36export {
27 defineExecutorFrontendPlugin,37 defineExecutorFrontendPlugin,
38 defineFrontendPluginRoute,
28 defineFrontendSourceType,39 defineFrontendSourceType,
29 registerExecutorFrontendPlugins,40 registerExecutorFrontendPlugins,
41 type RegisteredFrontendPluginRoute,
42 type RegisteredFrontendSourceType,
30} from "./registry";43} from "./registry";
31 44
45export {
46 ExecutorPluginRouteProvider,
47 useExecutorPlugin,
48 useExecutorPluginNavigation,
49 useExecutorPluginPaths,
50 useExecutorPluginRoute,
51 useExecutorPluginRouteDefinition,
52 useExecutorPluginRouteParams,
53 useExecutorPluginSearch,
54} from "./plugin-route-context";
55
32export {56export {
33 SourcePluginRouteProvider,57 SourcePluginRouteProvider,
58 useSourcePlugin,
34 useSourcePluginDefinition,59 useSourcePluginDefinition,
35 useSourcePluginNavigation,60 useSourcePluginNavigation,
36 useSourcePluginPaths,61 useSourcePluginPaths,
packages/clients/react/src/source-plugins/paths.ts+35−2
@@ -4,6 +4,12 @@ const trimSlashes = (value: string): string => · 3 unmodified lines
4const joinPath = (...parts: ReadonlyArray<string>): string =>4const joinPath = (...parts: ReadonlyArray<string>): string =>
5 `/${parts.map(trimSlashes).filter(Boolean).join("/")}`;5 `/${parts.map(trimSlashes).filter(Boolean).join("/")}`;
6 6
7export type ExecutorPluginPaths = {
8 base: string;
9 routePattern: (path?: string) => string;
10 route: (path?: string) => string;
11};
12
7export type SourcePluginPaths = {13export type SourcePluginPaths = {
8 add: string;14 add: string;
9 detailPattern: string;15 detailPattern: string;
@@ -16,11 +22,32 @@ export type SourcePluginPaths = { · 6 unmodified lines
16 22
17export const sourcePluginsIndexPath = "/sources/add";23export const sourcePluginsIndexPath = "/sources/add";
18 24
19export const normalizeSourcePluginPath = (value: string): string =>25export const normalizeExecutorPluginPath = (value: string): string =>
20 trimSlashes(value);26 trimSlashes(value);
21 27
28export const normalizeSourcePluginPath = normalizeExecutorPluginPath;
29
30export const executorPluginBasePath = (key: string): string =>
31 joinPath("plugins", key);
32
33export const executorPluginRoutePattern = (
34 key: string,
35 path = "",
36): string => {
37 const relativePath = normalizeExecutorPluginPath(path);
38 return relativePath.length === 0
39 ? executorPluginBasePath(key)
40 : joinPath("plugins", key, relativePath);
41};
42
43export const executorPluginRoutePath = (
44 key: string,
45 path = "",
46): string =>
47 executorPluginRoutePattern(key, path);
48
22export const sourcePluginAddPath = (key: string): string =>49export const sourcePluginAddPath = (key: string): string =>
23 joinPath("plugins", key, "add");50 executorPluginRoutePath(key, "add");
24 51
25export const sourcePluginDetailPattern = (key: string): string =>52export const sourcePluginDetailPattern = (key: string): string =>
26 joinPath("plugins", key, "sources", "$sourceId");53 joinPath("plugins", key, "sources", "$sourceId");
@@ -61,6 +88,12 @@ export const sourcePluginChildPath = ( · 34 unmodified lines
61 : joinPath("plugins", key, "sources", sourceId, relativePath);88 : joinPath("plugins", key, "sources", sourceId, relativePath);
62};89};
63 90
91export const createExecutorPluginPaths = (key: string): ExecutorPluginPaths => ({
92 base: executorPluginBasePath(key),
93 routePattern: (path = "") => executorPluginRoutePattern(key, path),
94 route: (path = "") => executorPluginRoutePath(key, path),
95});
96
64export const createSourcePluginPaths = (key: string): SourcePluginPaths => ({97export const createSourcePluginPaths = (key: string): SourcePluginPaths => ({
65 add: sourcePluginAddPath(key),98 add: sourcePluginAddPath(key),
66 detailPattern: sourcePluginDetailPattern(key),99 detailPattern: sourcePluginDetailPattern(key),
packages/clients/react/src/source-plugins/plugin-route-context.tsx+55−0
@@ -0,0 +1,55 @@
1import * as React from "react";
2
3import type {
4 ExecutorPluginRouteContextValue,
5 FrontendPluginRouteParams,
6 FrontendPluginRouteSearch,
7} from "./types";
8
9const ExecutorPluginRouteContext =
10 React.createContext<ExecutorPluginRouteContextValue | null>(null);
11
12export function ExecutorPluginRouteProvider(props: {
13 value: ExecutorPluginRouteContextValue;
14 children: React.ReactNode;
15}) {
16 return (
17 <ExecutorPluginRouteContext.Provider value={props.value}>
18 {props.children}
19 </ExecutorPluginRouteContext.Provider>
20 );
21}
22
23const useExecutorPluginRouteContext = (): ExecutorPluginRouteContextValue => {
24 const value = React.useContext(ExecutorPluginRouteContext);
25 if (value === null) {
26 throw new Error("Executor plugin route context is unavailable.");
27 }
28
29 return value;
30};
31
32export const useExecutorPluginRoute = (): ExecutorPluginRouteContextValue =>
33 useExecutorPluginRouteContext();
34
35export const useExecutorPlugin = () =>
36 useExecutorPluginRouteContext().plugin;
37
38export const useExecutorPluginRouteDefinition = () =>
39 useExecutorPluginRouteContext().route;
40
41export const useExecutorPluginNavigation = () =>
42 useExecutorPluginRouteContext().navigation;
43
44export const useExecutorPluginSearch = <
45 TSearch extends FrontendPluginRouteSearch = FrontendPluginRouteSearch,
46>(): TSearch =>
47 useExecutorPluginRouteContext().search as TSearch;
48
49export const useExecutorPluginRouteParams = <
50 TParams extends FrontendPluginRouteParams = FrontendPluginRouteParams,
51>(): TParams =>
52 useExecutorPluginRouteContext().params as TParams;
53
54export const useExecutorPluginPaths = () =>
55 useExecutorPluginRouteContext().navigation.paths;
packages/clients/react/src/source-plugins/registry.ts+97−45
@@ -1,83 +1,135 @@
1import type {1import type {
2 ExecutorFrontendPlugin,2 ExecutorFrontendPlugin,
3 FrontendPluginRouteDefinition,
3 FrontendSourceTypeDefinition,4 FrontendSourceTypeDefinition,
4} from "./types";5} from "./types";
5import {6import {
7 normalizeExecutorPluginPath,
6 normalizeSourcePluginPath,8 normalizeSourcePluginPath,
7} from "./paths";9} from "./paths";
8 10
11export type RegisteredFrontendPluginRoute = {
12 plugin: ExecutorFrontendPlugin;
13 route: FrontendPluginRouteDefinition;
14};
15
16export type RegisteredFrontendSourceType = {
17 plugin: ExecutorFrontendPlugin;
18 definition: FrontendSourceTypeDefinition;
19};
20
9export const defineFrontendSourceType = <21export const defineFrontendSourceType = <
10 TDefinition extends FrontendSourceTypeDefinition,22 TDefinition extends FrontendSourceTypeDefinition,
11>(23>(
12 definition: TDefinition,24 definition: TDefinition,
13): TDefinition => definition;25): TDefinition => definition;
14 26
15export const defineExecutorFrontendPlugin = (input: {27export const defineFrontendPluginRoute = <
16 key: string;28 TDefinition extends FrontendPluginRouteDefinition,
17 sourceTypes: readonly FrontendSourceTypeDefinition[];29>(
18}): ExecutorFrontendPlugin => ({30 definition: TDefinition,
19 key: input.key,31): TDefinition => definition;
20 register(api) {32
21 for (const sourceType of input.sourceTypes) {33export const defineExecutorFrontendPlugin = <
22 api.sources.registerType(sourceType);34 TPlugin extends ExecutorFrontendPlugin,
23 }35>(
24 },36 input: TPlugin,
25});37): TPlugin => input;
26 38
27export const registerExecutorFrontendPlugins = (39export const registerExecutorFrontendPlugins = (
28 plugins: readonly ExecutorFrontendPlugin[],40 plugins: readonly ExecutorFrontendPlugin[],
29) => {41) => {
30 const sourceTypesByKind = new Map<string, FrontendSourceTypeDefinition>();42 const pluginsByKey = new Map<string, ExecutorFrontendPlugin>();
31 const sourceTypesByKey = new Map<string, FrontendSourceTypeDefinition>();43 const routesByPluginAndKey = new Map<string, RegisteredFrontendPluginRoute>();
44 const sourceTypesByKind = new Map<string, RegisteredFrontendSourceType>();
45 const sourceTypesByKey = new Map<string, RegisteredFrontendSourceType>();
32 46
33 for (const plugin of plugins) {47 for (const plugin of plugins) {
34 plugin.register({48 if (pluginsByKey.has(plugin.key)) {
35 sources: {49 throw new Error(`Duplicate frontend plugin registration: ${plugin.key}`);
36 registerType(definition) {50 }
37 if (sourceTypesByKind.has(definition.kind)) {51
52 pluginsByKey.set(plugin.key, plugin);
53
54 const pluginRouteKeys = new Set<string>();
55 const pluginRoutePaths = new Set<string>();
56
57 for (const route of plugin.routes ?? []) {
58 if (pluginRouteKeys.has(route.key)) {
59 throw new Error(
60 `Duplicate frontend plugin route key for ${plugin.key}: ${route.key}`,
61 );
62 }
63
64 pluginRouteKeys.add(route.key);
65
66 const normalizedPath = normalizeExecutorPluginPath(route.path ?? "");
67 if (pluginRoutePaths.has(normalizedPath)) {
68 throw new Error(
69 `Duplicate frontend plugin route path for ${plugin.key}: ${normalizedPath || "<root>"}`,
70 );
71 }
72
73 pluginRoutePaths.add(normalizedPath);
74 routesByPluginAndKey.set(`${plugin.key}:${route.key}`, {
75 plugin,
76 route,
77 });
78 }
79
80 for (const definition of plugin.sourceTypes ?? []) {
81 if (sourceTypesByKind.has(definition.kind)) {
82 throw new Error(
83 `Duplicate frontend source kind registration: ${definition.kind}`,
84 );
85 }
86
87 if (sourceTypesByKey.has(definition.key)) {
88 throw new Error(
89 `Duplicate frontend source key registration: ${definition.key}`,
90 );
91 }
92
93 if (definition.detailRoutes) {
94 const routeKeys = new Set<string>();
95
96 for (const detailRoute of definition.detailRoutes) {
97 if (routeKeys.has(detailRoute.key)) {
38 throw new Error(98 throw new Error(
39 `Duplicate frontend source kind registration: ${definition.kind}`,99 `Duplicate frontend source detail route key for ${definition.key}: ${detailRoute.key}`,
40 );100 );
41 }101 }
42 102
43 if (sourceTypesByKey.has(definition.key)) {103 routeKeys.add(detailRoute.key);
104
105 if (normalizeSourcePluginPath(detailRoute.path).length === 0) {
44 throw new Error(106 throw new Error(
45 `Duplicate frontend source key registration: ${definition.key}`,107 `Frontend source detail route path must be non-empty for ${definition.key}: ${detailRoute.key}`,
46 );108 );
47 }109 }
110 }
111 }
48 112
49 if (definition.detailRoutes) {113 const registeredSourceType: RegisteredFrontendSourceType = {
50 const routeKeys = new Set<string>();114 plugin,
51 115 definition,
52 for (const detailRoute of definition.detailRoutes) {116 };
53 if (routeKeys.has(detailRoute.key)) {
54 throw new Error(
55 `Duplicate frontend source detail route key for ${definition.key}: ${detailRoute.key}`,
56 );
57 }
58
59 routeKeys.add(detailRoute.key);
60 117
61 if (normalizeSourcePluginPath(detailRoute.path).length === 0) {118 sourceTypesByKind.set(definition.kind, registeredSourceType);
62 throw new Error(119 sourceTypesByKey.set(definition.key, registeredSourceType);
63 `Frontend source detail route path must be non-empty for ${definition.key}: ${detailRoute.key}`,120 }
64 );
65 }
66 }
67 }
68
69 sourceTypesByKind.set(definition.kind, definition);
70 sourceTypesByKey.set(definition.key, definition);
71 },
72 },
73 });
74 }121 }
75 122
123 const routes = [...routesByPluginAndKey.values()];
76 const sourceTypes = [...sourceTypesByKey.values()];124 const sourceTypes = [...sourceTypesByKey.values()];
77 125
78 return {126 return {
79 plugins,127 plugins: [...pluginsByKey.values()],
128 routes,
80 sourceTypes,129 sourceTypes,
130 getPlugin: (key: string) => pluginsByKey.get(key) ?? null,
131 getRoute: (pluginKey: string, routeKey: string) =>
132 routesByPluginAndKey.get(`${pluginKey}:${routeKey}`) ?? null,
81 getSourceType: (kind: string) => sourceTypesByKind.get(kind) ?? null,133 getSourceType: (kind: string) => sourceTypesByKind.get(kind) ?? null,
82 getSourceTypeByKey: (key: string) => sourceTypesByKey.get(key) ?? null,134 getSourceTypeByKey: (key: string) => sourceTypesByKey.get(key) ?? null,
83 getDefaultSourceType: () => sourceTypes[0] ?? null,135 getDefaultSourceType: () => sourceTypes[0] ?? null,
packages/clients/react/src/source-plugins/route-context.tsx+3−0
@@ -34,6 +34,9 @@ export const useSourcePluginRoute = (): SourcePluginRouteContextValue => · 33 unmodified lines
34export const useSourcePluginDefinition = () =>34export const useSourcePluginDefinition = () =>
35 useSourcePluginRouteContext().definition;35 useSourcePluginRouteContext().definition;
36 36
37export const useSourcePlugin = () =>
38 useSourcePluginRouteContext().plugin;
39
37export const useSourcePluginNavigation = () =>40export const useSourcePluginNavigation = () =>
38 useSourcePluginRouteContext().navigation;41 useSourcePluginRouteContext().navigation;
39 42
packages/clients/react/src/source-plugins/types.ts+41−17
@@ -1,10 +1,38 @@
1import type { ComponentType } from "react";1import type { ComponentType } from "react";
2import type { Source } from "@executor/platform-sdk/schema";2import type { Source } from "@executor/platform-sdk/schema";
3 3
4import type { SourcePluginPaths } from "./paths";4import type {
5 ExecutorPluginPaths,
6 SourcePluginPaths,
7} from "./paths";
5 8
6export type SourcePluginRouteSearch = Record<string, unknown>;9export type FrontendPluginRouteSearch = Record<string, unknown>;
7export type SourcePluginRouteParams = Readonly<Record<string, string | undefined>>;10export type FrontendPluginRouteParams = Readonly<Record<string, string | undefined>>;
11
12export type ExecutorPluginNavigation = {
13 paths: ExecutorPluginPaths;
14 home: () => void | Promise<void>;
15 route: (path?: string, search?: FrontendPluginRouteSearch) => void | Promise<void>;
16 updateSearch: (search: FrontendPluginRouteSearch) => void | Promise<void>;
17};
18
19export type FrontendPluginRouteDefinition = {
20 key: string;
21 path?: string;
22 component: ComponentType;
23 navigationLabel?: string;
24};
25
26export type ExecutorPluginRouteContextValue = {
27 plugin: ExecutorFrontendPlugin;
28 route: FrontendPluginRouteDefinition;
29 params: FrontendPluginRouteParams;
30 search: FrontendPluginRouteSearch;
31 navigation: ExecutorPluginNavigation;
32};
33
34export type SourcePluginRouteSearch = FrontendPluginRouteSearch;
35export type SourcePluginRouteParams = FrontendPluginRouteParams;
8 36
9export type SourcePluginNavigation = {37export type SourcePluginNavigation = {
10 paths: SourcePluginPaths;38 paths: SourcePluginPaths;
@@ -20,13 +48,6 @@ export type SourcePluginNavigation = { · 9 unmodified lines
20 updateSearch: (search: SourcePluginRouteSearch) => void | Promise<void>;48 updateSearch: (search: SourcePluginRouteSearch) => void | Promise<void>;
21};49};
22 50
23export type SourcePluginRouteContextValue = {
24 definition: FrontendSourceTypeDefinition;
25 params: SourcePluginRouteParams;
26 search: SourcePluginRouteSearch;
27 navigation: SourcePluginNavigation;
28};
29
30export type FrontendSourceDetailRouteDefinition = {51export type FrontendSourceDetailRouteDefinition = {
31 key: string;52 key: string;
32 path: string;53 path: string;
@@ -44,15 +65,18 @@ export type FrontendSourceTypeDefinition = { · 11 unmodified lines
44 detailRoutes?: readonly FrontendSourceDetailRouteDefinition[];65 detailRoutes?: readonly FrontendSourceDetailRouteDefinition[];
45};66};
46 67
47export type ExecutorFrontendPluginApi = {68export type SourcePluginRouteContextValue = {
48 sources: {69 plugin: ExecutorFrontendPlugin;
49 registerType: (70 definition: FrontendSourceTypeDefinition;
50 definition: FrontendSourceTypeDefinition,71 params: SourcePluginRouteParams;
51 ) => void;72 search: SourcePluginRouteSearch;
52 };73 navigation: SourcePluginNavigation;
53};74};
54 75
55export type ExecutorFrontendPlugin = {76export type ExecutorFrontendPlugin = {
56 key: string;77 key: string;
57 register: (api: ExecutorFrontendPluginApi) => void;78 displayName?: string;
79 description?: string;
80 routes?: readonly FrontendPluginRouteDefinition[];
81 sourceTypes?: readonly FrontendSourceTypeDefinition[];
58};82};
packages/platform/sdk/src/plugins.ts+19−0
@@ -82,14 +82,33 @@ export type SourcePluginRuntime = { · 81 unmodified lines
82export const registerExecutorSdkPlugins = (82export const registerExecutorSdkPlugins = (
83 plugins: readonly ExecutorSdkPlugin[],83 plugins: readonly ExecutorSdkPlugin[],
84) => {84) => {
85 const pluginKeys = new Set<string>();
85 const sourcePlugins = new Map<string, SourcePluginRuntime>();86 const sourcePlugins = new Map<string, SourcePluginRuntime>();
86 const sourceConnectors = new Map<string, ExecutorSourceConnector<any>>();87 const sourceConnectors = new Map<string, ExecutorSourceConnector<any>>();
87 88
88 for (const plugin of plugins) {89 for (const plugin of plugins) {
90 if (pluginKeys.has(plugin.key)) {
91 throw new Error(`Duplicate executor SDK plugin registration: ${plugin.key}`);
92 }
93
94 pluginKeys.add(plugin.key);
95
89 for (const source of plugin.sources ?? []) {96 for (const source of plugin.sources ?? []) {
97 if (sourcePlugins.has(source.kind)) {
98 throw new Error(
99 `Duplicate source plugin registration: ${source.kind}`,
100 );
101 }
102
90 sourcePlugins.set(source.kind, source);103 sourcePlugins.set(source.kind, source);
91 }104 }
92 for (const connector of plugin.sourceConnectors ?? []) {105 for (const connector of plugin.sourceConnectors ?? []) {
106 if (sourceConnectors.has(connector.kind)) {
107 throw new Error(
108 `Duplicate source connector registration: ${connector.kind}`,
109 );
110 }
111
93 sourceConnectors.set(connector.kind, connector);112 sourceConnectors.set(connector.kind, connector);
94 }113 }
95 }114 }
plugins/google-discovery/react/index.tsx+1−1
@@ -18,7 +18,7 @@ import { · 17 unmodified lines
18 type SourceToolExplorerSearch,18 type SourceToolExplorerSearch,
19 useSourcePluginNavigation,19 useSourcePluginNavigation,
20 useSourcePluginSearch,20 useSourcePluginSearch,
21} from "@executor/react/source-plugins";21} from "@executor/react/plugins";
22 22
23import {23import {
24 googleDiscoveryHttpApiExtension,24 googleDiscoveryHttpApiExtension,
plugins/graphql/react/index.tsx+1−1
@@ -42,7 +42,7 @@ import { · 41 unmodified lines
42 type SourceToolExplorerSearch,42 type SourceToolExplorerSearch,
43 useSourcePluginNavigation,43 useSourcePluginNavigation,
44 useSourcePluginSearch,44 useSourcePluginSearch,
45} from "@executor/react/source-plugins";45} from "@executor/react/plugins";
46 46
47import {47import {
48 graphqlHttpApiExtension,48 graphqlHttpApiExtension,
plugins/mcp/react/index.tsx+1−1
@@ -17,7 +17,7 @@ import { · 16 unmodified lines
17 type SourceToolExplorerSearch,17 type SourceToolExplorerSearch,
18 useSourcePluginNavigation,18 useSourcePluginNavigation,
19 useSourcePluginSearch,19 useSourcePluginSearch,
20} from "@executor/react/source-plugins";20} from "@executor/react/plugins";
21 21
22import {22import {
23 mcpHttpApiExtension,23 mcpHttpApiExtension,