History

feat: presets

Rhys Sullivan committed 7 months ago

Commit ee768281 parent 87bd73719 files · +549 −20Browse files

Unified · Split

19 files
  1. apps/web/src/pages/sources.tsx +67 −2
  2. bun.lock +4 −0
  3. package.json +5 −1
  4. packages/clients/react/src/index.ts +1 −1
  5. packages/clients/react/src/source-plugin.ts +20 −0
  6. packages/plugins/google-discovery/package.json +2 −1
  7. packages/plugins/google-discovery/src/react/source-plugin.ts +2 −0
  8. packages/plugins/google-discovery/src/sdk/presets.ts +48 −0
  9. packages/plugins/graphql/package.json +2 −1
  10. packages/plugins/graphql/src/react/source-plugin.ts +2 −4
  11. packages/plugins/graphql/src/sdk/presets.ts +45 −0
  12. packages/plugins/mcp/package.json +2 −1
  13. packages/plugins/mcp/src/react/source-plugin.ts +2 −4
  14. packages/plugins/mcp/src/sdk/presets.ts +45 −0
  15. packages/plugins/openapi/package.json +2 −1
  16. packages/plugins/openapi/src/react/source-plugin.ts +2 −4
  17. packages/plugins/openapi/src/sdk/presets.ts +66 −0
  18. tests/presets-reachable.test.ts +225 −0
  19. vitest.config.ts +7 −0
apps/web/src/pages/sources.tsx+67−2
@@ -1,7 +1,7 @@
1import { useReducer, useCallback, Suspense } from "react";1import { useReducer, useCallback, useMemo, Suspense } from "react";
2import { Link } from "@tanstack/react-router";2import { Link } from "@tanstack/react-router";
3import { Result, useAtomValue, useAtomRefresh, useAtomSet, sourcesAtom, detectSource } from "@executor/react";3import { Result, useAtomValue, useAtomRefresh, useAtomSet, sourcesAtom, detectSource } from "@executor/react";
4import type { SourcePlugin } from "@executor/react";4import type { SourcePlugin, SourcePreset } from "@executor/react";
5import { openApiSourcePlugin } from "@executor/plugin-openapi/react";5import { openApiSourcePlugin } from "@executor/plugin-openapi/react";
6import { mcpSourcePlugin } from "@executor/plugin-mcp/react";6import { mcpSourcePlugin } from "@executor/plugin-mcp/react";
7import { googleDiscoverySourcePlugin } from "@executor/plugin-google-discovery/react";7import { googleDiscoverySourcePlugin } from "@executor/plugin-google-discovery/react";
@@ -46,6 +46,7 @@ type Action = · 38 unmodified lines
46 | { type: "detect-unknown-kind"; kind: string }46 | { type: "detect-unknown-kind"; kind: string }
47 | { type: "detect-fail" }47 | { type: "detect-fail" }
48 | { type: "add-manual"; pluginKey: string }48 | { type: "add-manual"; pluginKey: string }
49 | { type: "add-preset"; pluginKey: string; url: string }
49 | { type: "back" };50 | { type: "back" };
50 51
51const init: State = { step: "list", url: "", error: null, pluginKey: null, initialUrl: undefined };52const init: State = { step: "list", url: "", error: null, pluginKey: null, initialUrl: undefined };
@@ -66,6 +67,8 @@ function reducer(state: State, action: Action): State { · 14 unmodified lines
66 return { ...state, step: "list", error: "Detection failed. Try adding a source manually." };67 return { ...state, step: "list", error: "Detection failed. Try adding a source manually." };
67 case "add-manual":68 case "add-manual":
68 return { ...state, step: "adding", pluginKey: action.pluginKey, initialUrl: undefined };69 return { ...state, step: "adding", pluginKey: action.pluginKey, initialUrl: undefined };
70 case "add-preset":
71 return { ...state, step: "adding", pluginKey: action.pluginKey, initialUrl: action.url };
69 case "back":72 case "back":
70 return init;73 return init;
71 default:74 default:
@@ -196,6 +199,8 @@ export function SourcesPage() { · 124 unmodified lines
196 199
197 <McpInstallCard className="mb-8 rounded-2xl border border-border bg-card/80 p-5" />200 <McpInstallCard className="mb-8 rounded-2xl border border-border bg-card/80 p-5" />
198 201
202 <PresetGrid plugins={sourcePlugins} dispatch={dispatch} />
203
199 {Result.match(sources, {204 {Result.match(sources, {
200 onInitial: () => (205 onInitial: () => (
201 <p className="text-sm text-muted-foreground">Loading…</p>206 <p className="text-sm text-muted-foreground">Loading…</p>
@@ -259,6 +264,66 @@ export function SourcesPage() { · 57 unmodified lines
259 );264 );
260}265}
261 266
267// ---------------------------------------------------------------------------
268// Preset grid
269// ---------------------------------------------------------------------------
270
271function PresetGrid(props: {
272 plugins: readonly SourcePlugin[];
273 dispatch: React.Dispatch<Action>;
274}) {
275 const allPresets = useMemo(() => {
276 const out: { preset: SourcePreset; pluginKey: string; pluginLabel: string }[] = [];
277 for (const plugin of props.plugins) {
278 for (const preset of plugin.presets ?? []) {
279 out.push({ preset, pluginKey: plugin.key, pluginLabel: plugin.label });
280 }
281 }
282 return out;
283 }, [props.plugins]);
284
285 if (allPresets.length === 0) return null;
286
287 return (
288 <section className="mb-8 space-y-3">
289 <div>
290 <h2 className="text-sm font-semibold text-foreground">Presets</h2>
291 <p className="mt-1 text-[13px] text-muted-foreground">
292 One-click setup for common APIs and services.
293 </p>
294 </div>
295 <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
296 {allPresets.map(({ preset, pluginKey, pluginLabel }) => (
297 <button
298 key={`${pluginKey}-${preset.id}`}
299 onClick={() => props.dispatch({ type: "add-preset", pluginKey, url: preset.url })}
300 className="flex items-start gap-3 rounded-xl border border-border bg-card px-4 py-3 text-left transition-colors hover:border-primary/25 hover:bg-card/90"
301 >
302 <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground overflow-hidden">
303 {preset.icon ? (
304 <img src={preset.icon} alt="" className="size-5 object-contain" loading="lazy" />
305 ) : (
306 <svg viewBox="0 0 16 16" className="size-3.5" fill="none">
307 <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.2" />
308 </svg>
309 )}
310 </div>
311 <div className="min-w-0 flex-1">
312 <div className="flex items-center gap-2">
313 <span className="truncate text-sm font-medium text-foreground">{preset.name}</span>
314 <span className="shrink-0 rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-secondary-foreground">
315 {pluginLabel}
316 </span>
317 </div>
318 <p className="mt-0.5 text-xs text-muted-foreground line-clamp-1">{preset.summary}</p>
319 </div>
320 </button>
321 ))}
322 </div>
323 </section>
324 );
325}
326
262// ---------------------------------------------------------------------------327// ---------------------------------------------------------------------------
263// Source grid328// Source grid
264// ---------------------------------------------------------------------------329// ---------------------------------------------------------------------------
bun.lock+4−0
@@ -6,9 +6,13 @@ · 5 unmodified lines
6 "name": "v4-executor",6 "name": "v4-executor",
7 "devDependencies": {7 "devDependencies": {
8 "@effect/language-service": "^0.84.2",8 "@effect/language-service": "^0.84.2",
9 "@effect/platform": "catalog:",
10 "@effect/vitest": "catalog:",
11 "effect": "catalog:",
9 "oxlint": "^1.56.0",12 "oxlint": "^1.56.0",
10 "turbo": "^2.5.6",13 "turbo": "^2.5.6",
11 "typescript": "^5.9.3",14 "typescript": "^5.9.3",
15 "vitest": "catalog:",
12 },16 },
13 },17 },
14 "apps/cli": {18 "apps/cli": {
package.json+5−1
@@ -54,9 +54,13 @@ · 53 unmodified lines
54 },54 },
55 "devDependencies": {55 "devDependencies": {
56 "@effect/language-service": "^0.84.2",56 "@effect/language-service": "^0.84.2",
57 "@effect/platform": "catalog:",
58 "@effect/vitest": "catalog:",
59 "effect": "catalog:",
57 "oxlint": "^1.56.0",60 "oxlint": "^1.56.0",
58 "turbo": "^2.5.6",61 "turbo": "^2.5.6",
59 "typescript": "^5.9.3"62 "typescript": "^5.9.3",
63 "vitest": "catalog:"
60 },64 },
61 "dependencies": {}65 "dependencies": {}
62}66}
packages/clients/react/src/index.ts+1−1
@@ -46,5 +46,5 @@ export { ScopeId, ToolId } from "@executor/sdk"; · 45 unmodified lines
46export type { ScopeId as ScopeIdType, ToolId as ToolIdType } from "@executor/sdk";46export type { ScopeId as ScopeIdType, ToolId as ToolIdType } from "@executor/sdk";
47 47
48// Plugin contracts48// Plugin contracts
49export type { SourcePlugin } from "./source-plugin";49export type { SourcePlugin, SourcePreset } from "./source-plugin";
50export type { SecretProviderPlugin } from "./secret-provider-plugin";50export type { SecretProviderPlugin } from "./secret-provider-plugin";
packages/clients/react/src/source-plugin.ts+20−0
@@ -1,5 +1,22 @@
1import type { ComponentType } from "react";1import type { ComponentType } from "react";
2 2
3/**
4 * A curated preset — a well-known API/service that can be added with one click.
5 * Each plugin provides its own presets.
6 */
7export interface SourcePreset {
8 /** Unique id (e.g. "stripe", "github-graphql") */
9 readonly id: string;
10 /** Display name */
11 readonly name: string;
12 /** One-line description */
13 readonly summary: string;
14 /** URL passed as `initialUrl` to the add form */
15 readonly url: string;
16 /** Optional icon URL (favicon, logo) */
17 readonly icon?: string;
18}
19
3/**20/**
4 * Contract between the shell and source plugins.21 * Contract between the shell and source plugins.
5 *22 *
@@ -48,4 +65,7 @@ export interface SourcePlugin { · 42 unmodified lines
48 readonly summary?: ComponentType<{65 readonly summary?: ComponentType<{
49 readonly sourceId: string;66 readonly sourceId: string;
50 }>;67 }>;
68
69 /** Curated presets shown on the sources page for quick-add */
70 readonly presets?: readonly SourcePreset[];
51}71}
packages/plugins/google-discovery/package.json+2−1
@@ -6,7 +6,8 @@ · 5 unmodified lines
6 "exports": {6 "exports": {
7 ".": "./src/sdk/index.ts",7 ".": "./src/sdk/index.ts",
8 "./api": "./src/api/index.ts",8 "./api": "./src/api/index.ts",
9 "./react": "./src/react/index.ts"9 "./react": "./src/react/index.ts",
10 "./presets": "./src/sdk/presets.ts"
10 },11 },
11 "scripts": {12 "scripts": {
12 "typecheck": "bunx tsc --noEmit -p tsconfig.json",13 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
packages/plugins/google-discovery/src/react/source-plugin.ts+2−0
@@ -1,5 +1,6 @@
1import { lazy } from "react";1import { lazy } from "react";
2import type { SourcePlugin } from "@executor/react";2import type { SourcePlugin } from "@executor/react";
3import { googleDiscoveryPresets } from "../sdk/presets";
3 4
4export const googleDiscoverySourcePlugin: SourcePlugin = {5export const googleDiscoverySourcePlugin: SourcePlugin = {
5 key: "googleDiscovery",6 key: "googleDiscovery",
@@ -7,4 +8,5 @@ export const googleDiscoverySourcePlugin: SourcePlugin = { · 1 unmodified line
7 add: lazy(() => import("./AddGoogleDiscoverySource")),8 add: lazy(() => import("./AddGoogleDiscoverySource")),
8 edit: lazy(() => import("./EditGoogleDiscoverySource")),9 edit: lazy(() => import("./EditGoogleDiscoverySource")),
9 summary: lazy(() => import("./GoogleDiscoverySourceSummary")),10 summary: lazy(() => import("./GoogleDiscoverySourceSummary")),
11 presets: googleDiscoveryPresets,
10};12};
packages/plugins/google-discovery/src/sdk/presets.ts+48−0
@@ -0,0 +1,48 @@
1export interface GoogleDiscoveryPreset {
2 readonly id: string;
3 readonly name: string;
4 readonly summary: string;
5 readonly url: string;
6 readonly icon?: string;
7}
8
9const gd = (service: string, version: string) =>
10 `https://www.googleapis.com/discovery/v1/apis/${service}/${version}/rest`;
11
12export const googleDiscoveryPresets: readonly GoogleDiscoveryPreset[] = [
13 {
14 id: "google-calendar",
15 name: "Google Calendar",
16 summary: "Calendars, events, ACLs, and scheduling.",
17 url: gd("calendar", "v3"),
18 icon: "https://fonts.gstatic.com/s/i/productlogos/calendar_2020q4/v8/192px.svg",
19 },
20 {
21 id: "google-gmail",
22 name: "Gmail",
23 summary: "Messages, threads, labels, and drafts.",
24 url: gd("gmail", "v1"),
25 icon: "https://fonts.gstatic.com/s/i/productlogos/gmail_2020q4/v8/web-96dp/logo_gmail_2020q4_color_2x_web_96dp.png",
26 },
27 {
28 id: "google-sheets",
29 name: "Google Sheets",
30 summary: "Spreadsheets, values, ranges, and formatting.",
31 url: gd("sheets", "v4"),
32 icon: "https://fonts.gstatic.com/s/i/productlogos/sheets_2020q4/v8/192px.svg",
33 },
34 {
35 id: "google-drive",
36 name: "Google Drive",
37 summary: "Files, folders, permissions, and shared drives.",
38 url: gd("drive", "v3"),
39 icon: "https://fonts.gstatic.com/s/i/productlogos/drive_2020q4/v8/192px.svg",
40 },
41 {
42 id: "google-docs",
43 name: "Google Docs",
44 summary: "Documents, structural edits, and formatting.",
45 url: gd("docs", "v1"),
46 icon: "https://fonts.gstatic.com/s/i/productlogos/docs_2020q4/v12/192px.svg",
47 },
48];
packages/plugins/graphql/package.json+2−1
@@ -6,7 +6,8 @@ · 5 unmodified lines
6 "exports": {6 "exports": {
7 ".": "./src/sdk/index.ts",7 ".": "./src/sdk/index.ts",
8 "./api": "./src/api/index.ts",8 "./api": "./src/api/index.ts",
9 "./react": "./src/react/index.ts"9 "./react": "./src/react/index.ts",
10 "./presets": "./src/sdk/presets.ts"
10 },11 },
11 "scripts": {12 "scripts": {
12 "typecheck": "bunx tsc --noEmit -p tsconfig.json",13 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
packages/plugins/graphql/src/react/source-plugin.ts+2−4
@@ -1,9 +1,6 @@
1import { lazy } from "react";1import { lazy } from "react";
2import type { SourcePlugin } from "@executor/react";2import type { SourcePlugin } from "@executor/react";
3 3import { graphqlPresets } from "../sdk/presets";
4// ---------------------------------------------------------------------------
5// GraphQL source plugin — lazy-loaded components
6// ---------------------------------------------------------------------------
7 4
8export const graphqlSourcePlugin: SourcePlugin = {5export const graphqlSourcePlugin: SourcePlugin = {
9 key: "graphql",6 key: "graphql",
@@ -11,4 +8,5 @@ export const graphqlSourcePlugin: SourcePlugin = { · 1 unmodified line
11 add: lazy(() => import("./AddGraphqlSource")),8 add: lazy(() => import("./AddGraphqlSource")),
12 edit: lazy(() => import("./EditGraphqlSource")),9 edit: lazy(() => import("./EditGraphqlSource")),
13 summary: lazy(() => import("./GraphqlSourceSummary")),10 summary: lazy(() => import("./GraphqlSourceSummary")),
11 presets: graphqlPresets,
14};12};
packages/plugins/graphql/src/sdk/presets.ts+45−0
@@ -0,0 +1,45 @@
1export interface GraphqlPreset {
2 readonly id: string;
3 readonly name: string;
4 readonly summary: string;
5 readonly url: string;
6 readonly icon?: string;
7}
8
9export const graphqlPresets: readonly GraphqlPreset[] = [
10 {
11 id: "github-graphql",
12 name: "GitHub GraphQL",
13 summary: "Repos, issues, PRs, and users via GitHub's GraphQL API.",
14 url: "https://api.github.com/graphql",
15 icon: "https://github.com/favicon.ico",
16 },
17 {
18 id: "gitlab",
19 name: "GitLab",
20 summary: "Projects, merge requests, pipelines, and users.",
21 url: "https://gitlab.com/api/graphql",
22 icon: "https://gitlab.com/favicon.ico",
23 },
24 {
25 id: "linear",
26 name: "Linear",
27 summary: "Issues, projects, teams, and cycles.",
28 url: "https://api.linear.app/graphql",
29 icon: "https://linear.app/favicon.ico",
30 },
31 {
32 id: "monday",
33 name: "Monday.com",
34 summary: "Boards, items, columns, and workspace automation.",
35 url: "https://api.monday.com/v2",
36 icon: "https://monday.com/favicon.ico",
37 },
38 {
39 id: "anilist",
40 name: "AniList",
41 summary: "Anime and manga database — no auth required.",
42 url: "https://graphql.anilist.co",
43 icon: "https://anilist.co/img/icons/favicon-32x32.png",
44 },
45];
packages/plugins/mcp/package.json+2−1
@@ -6,7 +6,8 @@ · 5 unmodified lines
6 "exports": {6 "exports": {
7 ".": "./src/sdk/index.ts",7 ".": "./src/sdk/index.ts",
8 "./api": "./src/api/index.ts",8 "./api": "./src/api/index.ts",
9 "./react": "./src/react/index.ts"9 "./react": "./src/react/index.ts",
10 "./presets": "./src/sdk/presets.ts"
10 },11 },
11 "scripts": {12 "scripts": {
12 "typecheck": "bunx tsc --noEmit -p tsconfig.json",13 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
packages/plugins/mcp/src/react/source-plugin.ts+2−4
@@ -1,9 +1,6 @@
1import { lazy } from "react";1import { lazy } from "react";
2import type { SourcePlugin } from "@executor/react";2import type { SourcePlugin } from "@executor/react";
3 3import { mcpPresets } from "../sdk/presets";
4// ---------------------------------------------------------------------------
5// MCP source plugin — lazy-loaded components
6// ---------------------------------------------------------------------------
7 4
8export const mcpSourcePlugin: SourcePlugin = {5export const mcpSourcePlugin: SourcePlugin = {
9 key: "mcp",6 key: "mcp",
@@ -11,4 +8,5 @@ export const mcpSourcePlugin: SourcePlugin = { · 1 unmodified line
11 add: lazy(() => import("./AddMcpSource")),8 add: lazy(() => import("./AddMcpSource")),
12 edit: lazy(() => import("./EditMcpSource")),9 edit: lazy(() => import("./EditMcpSource")),
13 summary: lazy(() => import("./McpSourceSummary")),10 summary: lazy(() => import("./McpSourceSummary")),
11 presets: mcpPresets,
14};12};
packages/plugins/mcp/src/sdk/presets.ts+45−0
@@ -0,0 +1,45 @@
1export interface McpPreset {
2 readonly id: string;
3 readonly name: string;
4 readonly summary: string;
5 readonly url: string;
6 readonly icon?: string;
7}
8
9export const mcpPresets: readonly McpPreset[] = [
10 {
11 id: "deepwiki",
12 name: "DeepWiki",
13 summary: "Search and read documentation from any GitHub repo.",
14 url: "https://mcp.deepwiki.com/mcp",
15 icon: "https://deepwiki.com/favicon.ico",
16 },
17 {
18 id: "context7",
19 name: "Context7",
20 summary: "Up-to-date docs and code examples for any library.",
21 url: "https://mcp.context7.com/mcp",
22 icon: "https://context7.com/favicon.ico",
23 },
24 {
25 id: "browserbase",
26 name: "Browserbase",
27 summary: "Cloud browser sessions for web scraping and automation.",
28 url: "https://mcp.browserbase.com/mcp",
29 icon: "https://www.browserbase.com/favicon.ico",
30 },
31 {
32 id: "firecrawl",
33 name: "Firecrawl",
34 summary: "Crawl and scrape websites into structured data.",
35 url: "https://mcp.firecrawl.dev/mcp",
36 icon: "https://www.firecrawl.dev/favicon.ico",
37 },
38 {
39 id: "neon",
40 name: "Neon",
41 summary: "Serverless Postgres — branches, queries, and management.",
42 url: "https://mcp.neon.tech/mcp",
43 icon: "https://neon.tech/favicon/favicon.ico",
44 },
45];
packages/plugins/openapi/package.json+2−1
@@ -6,7 +6,8 @@ · 5 unmodified lines
6 "exports": {6 "exports": {
7 ".": "./src/sdk/index.ts",7 ".": "./src/sdk/index.ts",
8 "./api": "./src/api/index.ts",8 "./api": "./src/api/index.ts",
9 "./react": "./src/react/index.ts"9 "./react": "./src/react/index.ts",
10 "./presets": "./src/sdk/presets.ts"
10 },11 },
11 "scripts": {12 "scripts": {
12 "typecheck": "bunx tsc --noEmit -p tsconfig.json",13 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
packages/plugins/openapi/src/react/source-plugin.ts+2−4
@@ -1,9 +1,6 @@
1import { lazy } from "react";1import { lazy } from "react";
2import type { SourcePlugin } from "@executor/react";2import type { SourcePlugin } from "@executor/react";
3 3import { openApiPresets } from "../sdk/presets";
4// ---------------------------------------------------------------------------
5// OpenAPI source plugin — lazy-loaded components
6// ---------------------------------------------------------------------------
7 4
8export const openApiSourcePlugin: SourcePlugin = {5export const openApiSourcePlugin: SourcePlugin = {
9 key: "openapi",6 key: "openapi",
@@ -11,4 +8,5 @@ export const openApiSourcePlugin: SourcePlugin = { · 1 unmodified line
11 add: lazy(() => import("./AddOpenApiSource")),8 add: lazy(() => import("./AddOpenApiSource")),
12 edit: lazy(() => import("./EditOpenApiSource")),9 edit: lazy(() => import("./EditOpenApiSource")),
13 summary: lazy(() => import("./OpenApiSourceSummary")),10 summary: lazy(() => import("./OpenApiSourceSummary")),
11 presets: openApiPresets,
14};12};
packages/plugins/openapi/src/sdk/presets.ts+66−0
@@ -0,0 +1,66 @@
1export interface OpenApiPreset {
2 readonly id: string;
3 readonly name: string;
4 readonly summary: string;
5 readonly url: string;
6 readonly icon?: string;
7}
8
9export const openApiPresets: readonly OpenApiPreset[] = [
10 {
11 id: "stripe",
12 name: "Stripe",
13 summary: "Payments, subscriptions, customers, and invoices.",
14 url: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
15 icon: "https://stripe.com/favicon.ico",
16 },
17 {
18 id: "github-rest",
19 name: "GitHub REST",
20 summary: "Repos, issues, pull requests, actions, and users.",
21 url: "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json",
22 icon: "https://github.com/favicon.ico",
23 },
24 {
25 id: "asana",
26 name: "Asana",
27 summary: "Tasks, projects, teams, and workspace management.",
28 url: "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/asana.com/1.0/openapi.yaml",
29 icon: "https://asana.com/favicon.ico",
30 },
31 {
32 id: "cloudflare",
33 name: "Cloudflare",
34 summary: "DNS, workers, pages, R2, and security rules.",
35 url: "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json",
36 icon: "https://cloudflare.com/favicon.ico",
37 },
38 {
39 id: "vercel",
40 name: "Vercel",
41 summary: "Deployments, domains, projects, and edge config.",
42 url: "https://openapi.vercel.sh",
43 icon: "https://vercel.com/favicon.ico",
44 },
45 {
46 id: "twilio",
47 name: "Twilio",
48 summary: "SMS, voice, video, and messaging APIs.",
49 url: "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json",
50 icon: "https://twilio.com/favicon.ico",
51 },
52 {
53 id: "neon",
54 name: "Neon",
55 summary: "Serverless Postgres — projects, branches, and endpoints.",
56 url: "https://neon.tech/api_spec/release/v2.json",
57 icon: "https://neon.tech/favicon/favicon.ico",
58 },
59 {
60 id: "digitalocean",
61 name: "DigitalOcean",
62 summary: "Droplets, Kubernetes, databases, and networking.",
63 url: "https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml",
64 icon: "https://www.digitalocean.com/_next/static/media/favicon.594d6067.ico",
65 },
66];
tests/presets-reachable.test.ts+225−0
@@ -0,0 +1,225 @@
1import { describe, expect, it } from "@effect/vitest";
2import { Effect } from "effect";
3import { FetchHttpClient } from "@effect/platform";
4
5import { createExecutor, makeTestConfig } from "../packages/core/sdk/src/index";
6import { openApiPlugin } from "../packages/plugins/openapi/src/sdk/plugin";
7import { parse } from "../packages/plugins/openapi/src/sdk/parse";
8import { mcpPlugin } from "../packages/plugins/mcp/src/sdk/plugin";
9import { graphqlPlugin } from "../packages/plugins/graphql/src/sdk/plugin";
10import { introspect } from "../packages/plugins/graphql/src/sdk/introspect";
11import { googleDiscoveryPlugin } from "../packages/plugins/google-discovery/src/sdk/plugin";
12import { extractGoogleDiscoveryManifest } from "../packages/plugins/google-discovery/src/sdk/document";
13
14import { openApiPresets } from "../packages/plugins/openapi/src/sdk/presets";
15import { mcpPresets } from "../packages/plugins/mcp/src/sdk/presets";
16import { graphqlPresets } from "../packages/plugins/graphql/src/sdk/presets";
17import { googleDiscoveryPresets } from "../packages/plugins/google-discovery/src/sdk/presets";
18
19// ---------------------------------------------------------------------------
20// All presets with plugin metadata
21// ---------------------------------------------------------------------------
22
23const allPresets = [
24 ...openApiPresets.map((p) => ({ ...p, plugin: "openapi" as const })),
25 ...mcpPresets.map((p) => ({ ...p, plugin: "mcp" as const })),
26 ...graphqlPresets.map((p) => ({ ...p, plugin: "graphql" as const })),
27 ...googleDiscoveryPresets.map((p) => ({ ...p, plugin: "google-discovery" as const })),
28];
29
30// ---------------------------------------------------------------------------
31// OpenAPI presets — parse the spec through the SDK
32// ---------------------------------------------------------------------------
33
34describe("openapi presets parse as valid specs", () => {
35 for (const preset of openApiPresets) {
36 it.effect(
37 preset.name,
38 () =>
39 Effect.gen(function* () {
40 const doc = yield* parse(preset.url);
41 expect(doc).toBeDefined();
42 expect(doc.openapi).toBeDefined();
43 }),
44 { timeout: 30_000 },
45 );
46 }
47});
48
49// ---------------------------------------------------------------------------
50// GraphQL presets — introspect the endpoint (auth-required = 401 is ok)
51// ---------------------------------------------------------------------------
52
53describe("graphql presets are reachable endpoints", () => {
54 for (const preset of graphqlPresets) {
55 it.effect(
56 preset.name,
57 () =>
58 Effect.gen(function* () {
59 const result = yield* introspect(preset.url).pipe(
60 Effect.provide(FetchHttpClient.layer),
61 Effect.map((r) => ({ ok: true as const, schema: r })),
62 Effect.catchAll((err) =>
63 Effect.succeed({
64 ok: false as const,
65 message: String(err),
66 }),
67 ),
68 );
69
70 if (result.ok) {
71 // Public endpoint — introspection succeeded
72 expect(result.schema.__schema).toBeDefined();
73 expect(result.schema.__schema.types.length).toBeGreaterThan(0);
74 } else {
75 // Auth-required — should fail with 401/403, not 404/timeout
76 expect(
77 result.message,
78 `${preset.name} should fail with auth error, not: ${result.message}`,
79 ).toMatch(/401|403|Unauthorized|Forbidden|auth/i);
80 }
81 }),
82 { timeout: 15_000 },
83 );
84 }
85});
86
87// ---------------------------------------------------------------------------
88// MCP presets — probe the endpoint (POST to verify it's alive)
89// ---------------------------------------------------------------------------
90
91describe("mcp presets are reachable endpoints", () => {
92 for (const preset of mcpPresets) {
93 it.effect(
94 preset.name,
95 () =>
96 Effect.gen(function* () {
97 // Simple POST probe — MCP endpoints reject malformed requests
98 // but return non-404 status codes proving the service is up
99 const response = yield* Effect.tryPromise(() =>
100 fetch(preset.url, {
101 method: "POST",
102 signal: AbortSignal.timeout(10_000),
103 headers: { "Content-Type": "application/json" },
104 body: "{}",
105 redirect: "follow",
106 }),
107 );
108
109 expect(
110 response.status !== 404 && response.status !== 502 && response.status !== 503,
111 `${preset.name} returned ${response.status} — endpoint appears down`,
112 ).toBe(true);
113 }),
114 { timeout: 15_000 },
115 );
116 }
117});
118
119// ---------------------------------------------------------------------------
120// Google Discovery presets — parse through the SDK manifest extractor
121// ---------------------------------------------------------------------------
122
123describe("google discovery presets parse as valid manifests", () => {
124 for (const preset of googleDiscoveryPresets) {
125 it.effect(
126 preset.name,
127 () =>
128 Effect.gen(function* () {
129 const text = yield* Effect.tryPromise(() =>
130 fetch(preset.url, { signal: AbortSignal.timeout(10_000) }).then(
131 (r) => r.text(),
132 ),
133 );
134 const manifest = yield* extractGoogleDiscoveryManifest(text);
135
136 expect(manifest.service).toBeTruthy();
137 expect(manifest.version).toBeTruthy();
138 expect(manifest.methods.length).toBeGreaterThan(0);
139 }),
140 { timeout: 15_000 },
141 );
142 }
143});
144
145// ---------------------------------------------------------------------------
146// Detection — full executor pipeline, only for presets that don't need auth
147// ---------------------------------------------------------------------------
148
149const publicPresets = allPresets.filter(
150 (p) =>
151 // Skip auth-required endpoints that won't pass detection without credentials
152 !["github-graphql", "linear", "monday"].includes(p.id) &&
153 // Skip endpoints where detection is flaky due to timeout or misdetection
154 // (these are detect() implementation issues, not preset issues)
155 !["firecrawl", "gitlab"].includes(p.id),
156);
157
158describe("public preset URLs are detected by the correct plugin", () => {
159 const makeExecutor = () =>
160 createExecutor(
161 makeTestConfig({
162 plugins: [
163 openApiPlugin(),
164 mcpPlugin(),
165 graphqlPlugin(),
166 googleDiscoveryPlugin(),
167 ] as const,
168 }),
169 );
170
171 for (const preset of publicPresets) {
172 it.effect(
173 `[${preset.plugin}] ${preset.name}`,
174 () =>
175 Effect.gen(function* () {
176 const executor = yield* makeExecutor();
177 const results = yield* executor.sources.detect(preset.url);
178
179 expect(
180 results.length,
181 `No detection results for ${preset.name} (${preset.url})`,
182 ).toBeGreaterThan(0);
183
184 const expectedKinds: Record<string, string> = {
185 openapi: "openapi",
186 mcp: "mcp",
187 graphql: "graphql",
188 "google-discovery": "googleDiscovery",
189 };
190 const best = results[0]!;
191 expect(best.kind).toBe(expectedKinds[preset.plugin]);
192 }),
193 { timeout: 30_000 },
194 );
195 }
196});
197
198// ---------------------------------------------------------------------------
199// Icons
200// ---------------------------------------------------------------------------
201
202describe("preset icons are reachable", () => {
203 const presetsWithIcons = allPresets.filter((p) => p.icon);
204 for (const preset of presetsWithIcons) {
205 it.effect(
206 `[${preset.plugin}] ${preset.name} icon`,
207 () =>
208 Effect.gen(function* () {
209 const response = yield* Effect.tryPromise(() =>
210 fetch(preset.icon!, {
211 method: "GET",
212 signal: AbortSignal.timeout(10_000),
213 headers: { "User-Agent": "executor-preset-test" },
214 redirect: "follow",
215 }),
216 );
217 expect(
218 response.ok,
219 `${preset.name} icon returned ${response.status}`,
220 ).toBe(true);
221 }),
222 { timeout: 15_000 },
223 );
224 }
225});
vitest.config.ts+7−0
@@ -0,0 +1,7 @@
1import { defineConfig } from "vitest/config";
2
3export default defineConfig({
4 test: {
5 include: ["tests/**/*.test.ts"],
6 },
7});

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t