t3-code-android-nightly/.repos/alchemy-effect/website/scripts/generate-llms-txt.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

795 lines
26 KiB
TypeScript

/**
* Generates `public/llms.txt` and `public/llms-full.txt` — navigation
* indexes of the docs pages, grouped by section, with title + description
* pulled from each page's frontmatter.
*
* `llms.txt` covers the guides and concept docs and links to
* `llms-full.txt`, which additionally lists every generated service-level
* API reference page (the Providers section) and is much larger.
*
* Run with: `bun scripts/generate-llms-txt.ts`
*
* Section ordering, headings, and prose intros are configured here.
* Page metadata (title, description) comes from the source frontmatter,
* so editing a page's frontmatter is enough to update llms.txt.
*/
import { readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const here = path.dirname(fileURLToPath(import.meta.url));
const docsDir = path.resolve(here, "../src/content/docs");
const outFile = path.resolve(here, "../public/llms.txt");
const outFullFile = path.resolve(here, "../public/llms-full.txt");
const siteUrl = "https://alchemy.run";
interface Page {
/** URL path, e.g. "/infrastructure-as-effects/binding" */
href: string;
/** Path relative to docs dir without extension, e.g. "infrastructure-as-effects/binding" */
slug: string;
title: string;
description: string;
draft: boolean;
/** `sidebar.order` from frontmatter; `Infinity` when unset. */
order: number;
}
interface Section {
/** H2 heading */
heading: string;
/** Optional prose paragraph after the heading. */
intro?: string;
/**
* Pages to include. Either a list of explicit slugs (relative to docs dir,
* no extension) in the desired order, or a directory to enumerate
* alphabetically.
*/
pages: { slugs: string[] } | { directory: string; exclude?: string[] };
}
const SECTIONS: Section[] = [
{
heading: "Start here",
pages: {
slugs: ["what-is-alchemy", "getting-started", "migrating-from-v1"],
},
},
{
heading: "CLI",
intro:
"Every command runs through your package manager: bun alchemy <command>. Command pages by role, plus the Adopting Resources and Inspecting State guides.",
pages: {
slugs: [
"cli/index",
"cli/deploy",
"cli/plan",
"cli/destroy",
"cli/drift",
"cli/nuke",
"cli/adopting-resources",
"cli/dev",
"cli/logs",
"cli/profile",
"cli/state",
"cli/inspecting-state",
"cli/aws",
"cli/cloudflare",
],
},
},
{
heading: "Infrastructure as Code",
intro:
"The noun graph and its semantics: Stacks of Resources and Actions, data flow via Inputs & Outputs and References, deploy behavior via the Resource lifecycle and Providers.",
pages: {
slugs: [
"infrastructure-as-code/stack",
"infrastructure-as-code/resource",
"infrastructure-as-code/action",
"infrastructure-as-code/outputs",
"infrastructure-as-code/references",
"infrastructure-as-code/resource-lifecycle",
"infrastructure-as-code/provider",
"infrastructure-as-code/custom-provider",
],
},
},
{
heading: "Infrastructure as Effects",
intro:
"How app code and infrastructure compose: Runtime, Bindings, the Construction and Runtime Phases, and Layers.",
pages: {
slugs: [
"infrastructure-as-effects/runtime",
"infrastructure-as-effects/binding",
"infrastructure-as-effects/layers",
"infrastructure-as-effects/event-sources",
"infrastructure-as-effects/sinks",
"infrastructure-as-effects/phases",
"infrastructure-as-effects/circular-bindings",
"infrastructure-as-effects/custom-runtime",
],
},
},
{
heading: "APIs",
intro:
"Typed calls between Functions and Servers: schemaless RPC for internal communication, Effect RPC and Effect HTTP for trust boundaries.",
pages: {
slugs: [
"apis/index",
"apis/schemaless",
"apis/effect-rpc",
"apis/effect-http",
],
},
},
{
heading: "Environments — the same app in many places",
intro:
"Stages, credential profiles, auth providers, secrets and config, local development, and CI.",
pages: {
slugs: [
"environments/stages",
"environments/profiles",
"environments/auth-providers",
"environments/custom-auth-provider",
"environments/secrets",
"environments/local-development",
"environments/ci",
],
},
},
{
heading: "State Store",
pages: { slugs: ["state-store/index", "state-store/custom-state-store"] },
},
{
heading: "Project structure",
intro:
"Scaling the codebase: file conventions for one stack, monorepos for many — single Stack or one per package.",
pages: {
slugs: ["project-structure/file-layout", "project-structure/monorepo"],
},
},
{
heading: "Testing & observability",
intro:
"Tests run against real clouds: the model, the end-to-end walkthrough, provider-lifecycle testing, and the harness reference.",
pages: {
slugs: [
"testing/index",
"testing/testing-a-stack",
"testing/testing-providers",
"testing/test-harness",
"testing/observability",
],
},
},
{
heading: "Cloudflare — start here",
intro:
"The Cloudflare hub: overview (resources + recipes) and setup (install, account, OAuth vs API token, profiles).",
pages: { slugs: ["cloudflare/index", "cloudflare/setup"] },
},
{
heading: "Cloudflare — tutorial",
intro:
"A linear five-part walkthrough from zero to a tested, locally-developed, CI-deployed Cloudflare project. Each part builds on the previous one.",
pages: {
slugs: [
"cloudflare/tutorial/part-1",
"cloudflare/tutorial/part-2",
"cloudflare/tutorial/part-3",
"cloudflare/tutorial/part-4",
"cloudflare/tutorial/part-5",
],
},
},
{
heading: "Cloudflare — Compute",
pages: { directory: "cloudflare/compute" },
},
{
heading: "Cloudflare — Frontend",
pages: { directory: "cloudflare/frontend" },
},
{
heading: "Cloudflare — APIs",
pages: { directory: "cloudflare/apis" },
},
{
heading: "Cloudflare — Data",
pages: { directory: "cloudflare/data" },
},
{
heading: "Cloudflare — Messaging & events",
pages: { directory: "cloudflare/messaging" },
},
{
heading: "Cloudflare — Email",
pages: { directory: "cloudflare/email" },
},
{
heading: "Cloudflare — AI",
pages: { directory: "cloudflare/ai" },
},
{
heading: "Cloudflare — Security & secrets",
pages: { directory: "cloudflare/security" },
},
{
heading: "Cloudflare — Observability",
pages: { directory: "cloudflare/observability" },
},
{
heading: "Cloudflare — Networking",
pages: { directory: "cloudflare/networking" },
},
{
heading: "Prisma — start here",
pages: { slugs: ["prisma/index", "prisma/setup"] },
},
{
heading: "Prisma — tutorial",
intro:
"A four-part walkthrough: a Project, an Effect-native Compute API, Postgres, and a Vite frontend.",
pages: { directory: "prisma/tutorial" },
},
{
heading: "Prisma — Compute",
pages: { directory: "prisma/compute" },
},
{
heading: "Prisma — Data",
pages: { directory: "prisma/data" },
},
{
heading: "Prisma — Frontend",
pages: { directory: "prisma/frontend" },
},
{
heading: "Prisma — Guides",
pages: { directory: "prisma/guides" },
},
{
heading: "AWS — start here",
intro:
"The AWS hub: overview (runtimes + resources + recipes), setup (credentials, profiles, region), and the Lambda vs ECS vs EKS vs EC2 decision page.",
pages: {
slugs: ["aws/index", "aws/setup", "aws/compute/choosing-a-runtime"],
},
},
{
heading: "AWS — tutorial",
intro:
"A linear five-part walkthrough from zero to a tested, CI-deployed AWS project. Each part builds on the previous one.",
pages: {
slugs: [
"aws/tutorial/part-1",
"aws/tutorial/part-2",
"aws/tutorial/part-3",
"aws/tutorial/part-4",
"aws/tutorial/part-5",
],
},
},
{
heading: "AWS — Compute",
pages: { directory: "aws/compute" },
},
{
heading: "AWS — Frontend",
pages: { directory: "aws/frontend" },
},
{
heading: "AWS — APIs",
pages: { directory: "aws/apis" },
},
{
heading: "AWS — Data",
pages: { directory: "aws/data" },
},
{
heading: "AWS — Messaging & events",
pages: { directory: "aws/messaging" },
},
{
heading: "AWS — Email",
pages: { directory: "aws/email" },
},
{
heading: "AWS — Security & secrets",
pages: { directory: "aws/security" },
},
{
heading: "AWS — Observability",
pages: { directory: "aws/observability" },
},
{
heading: "AWS — Networking",
pages: { directory: "aws/networking" },
},
{
heading: "GCP",
intro:
"Google Cloud as Stack resources — Cloud Run services and jobs, Firestore, Pub/Sub, BigQuery, Secret Manager, Memorystore. Bindings grant IAM onto a per-host service account.",
pages: {
slugs: [
"gcp/index",
"gcp/setup",
"gcp/guides/cloud-run-api",
"gcp/guides/event-pipeline",
"gcp/guides/memorystore",
"gcp/guides/bindings",
],
},
},
{
heading: "Fly — start here",
intro:
"The Fly hub: overview (Apps, Machines, Services, Sprites) and setup (org, API token, profile).",
pages: {
slugs: ["fly/index", "fly/setup"],
},
},
{
heading: "Fly — SQL",
pages: { slugs: ["fly/data/postgres", "fly/data/drizzle-postgres"] },
},
{
heading: "Fly — Frontend",
pages: { directory: "fly/frontend" },
},
{
heading: "Hetzner — start here",
intro:
"The Hetzner hub: overview (Servers and Services) and setup (project, API token, profile).",
pages: {
slugs: ["hetzner/index", "hetzner/setup"],
},
},
{
heading: "Hetzner — SQL",
pages: { slugs: ["hetzner/data/drizzle-postgres"] },
},
{
heading: "Hetzner — Frontend",
pages: { directory: "hetzner/frontend" },
},
{
heading: "Railway — start here",
intro:
"The Railway hub: overview (Projects, Services, databases) and setup (workspace, API token, profile).",
pages: {
slugs: ["railway/index", "railway/setup"],
},
},
{
heading: "Railway — SQL",
pages: {
slugs: [
"railway/data/postgres",
"railway/data/mysql",
"railway/data/drizzle-postgres",
"railway/data/drizzle-mysql",
],
},
},
{
heading: "Prisma — SQL",
pages: {
slugs: [
"prisma/data/postgres",
"prisma/data/branches",
"prisma/data/connections",
"prisma/data/drizzle-postgres",
],
},
},
{
heading: "Railway — Frontend",
pages: { directory: "railway/frontend" },
},
{
heading: "PlanetScale",
intro:
"Serverless MySQL & Postgres as Stack resources. Composes with Cloudflare Hyperdrive and Drizzle — those guides are listed under Cloudflare.",
pages: {
slugs: [
"planetscale/index",
"planetscale/setup",
"planetscale/data/postgres",
"planetscale/data/mysql",
"planetscale/data/migrations",
"planetscale/data/credentials",
"planetscale/data/backups",
"planetscale/guides/preview-branches",
"planetscale/guides/drizzle",
],
},
},
{
heading: "Neon",
intro:
"Neon backend resources: Postgres branches, Functions, private object storage, managed Auth, Data API, AI Gateway and Website adapters. The upload tutorial combines native and Effect handlers with authenticated browser uploads.",
pages: {
slugs: [
"neon/index",
"neon/setup",
"neon/governance",
"neon/guides/ai-gateway",
"neon/guides/production-auth",
"neon/guides/private-networking",
"neon/guides/custom-domains",
"neon/guides/state-recovery",
"neon/tutorial/index",
"neon/tutorial/backend",
"neon/tutorial/functions",
"neon/tutorial/frontend",
"neon/tutorial/previews",
"neon/data/branching",
"neon/data/connections",
"neon/data/migrations",
"neon/guides/preview-branches",
"neon/guides/drizzle",
],
},
},
{
heading: "Axiom",
intro:
"Observability as Stack resources — datasets, monitors, notifiers. The exporter-Layer pattern is documented in Concepts → Observability.",
pages: {
slugs: [
"axiom/index",
"axiom/setup",
"axiom/data/ingest",
"axiom/guides/alerting",
"axiom/guides/dashboards",
"axiom/guides/annotations",
],
},
},
{
heading: "GitHub",
intro:
"Repos, secrets, variables, and repository event sources as Stack resources; the CI/CD guides live under Guides and each cloud's tutorial part 5.",
pages: {
slugs: [
"github/index",
"github/setup",
"github/repository",
"github/actions-config",
"github/events",
],
},
},
{
heading: "Git",
intro:
"A git server you build in one file, use like any remote, and reshape one block at a time: setup, a four-part tutorial (build it, repositories and tokens, your own rules, your app on the same domain), the REST and GitHub-compatible planes, the building blocks with their implementations, and recipes with the scaling model and measurements.",
pages: {
slugs: [
"git/index",
"git/getting-started",
"git/tutorial/part-1",
"git/tutorial/part-2",
"git/tutorial/part-3",
"git/tutorial/part-4",
"git/clone-and-push",
"git/repositories",
"git/pull-requests",
"git/github-api",
"git/blocks/index",
"git/blocks/server",
"git/blocks/repositories",
"git/blocks/registry",
"git/blocks/blob-store",
"git/blocks/hasher",
"git/blocks/auth",
"git/recipes/index",
"git/recipes/scaling",
"git/recipes/cloudflare",
"git/recipes/cloudflare-aws",
"git/recipes/your-own-store",
],
},
},
{
heading: "Stripe",
intro:
"Products, prices, coupons, customers, payment links, and webhooks as Stack resources. HTTP bindings run inside a Worker or Lambda.",
pages: {
slugs: [
"stripe/index",
"stripe/setup",
"stripe/guides/subscriptions",
"stripe/guides/connect",
"stripe/guides/webhooks",
],
},
},
{
heading: "Docker",
intro:
"Local and CI Docker as Stack resources — images, containers, networks, and volumes driven through the active Docker CLI context; cloud container runtimes (Cloudflare Containers, ECS) consume the pushed image refs from their own hubs.",
pages: {
slugs: [
"docker/index",
"docker/setup",
"docker/local-services",
"docker/build-and-push",
],
},
},
{
heading: "Kubernetes",
intro:
"Cluster-agnostic Kubernetes workloads — Deployments, Jobs and CronJobs (as images or Effect programs), raw manifests, and Helm charts — on a local cluster or any cluster reachable through a kubeconfig, token, client certificate, or exec plugin.",
pages: {
slugs: [
"kubernetes/index",
"kubernetes/setup",
"kubernetes/tutorial/part-1",
"kubernetes/tutorial/part-2",
"kubernetes/tutorial/part-3",
"kubernetes/tutorial/part-4",
"kubernetes/tutorial/part-5",
"kubernetes/clusters/connecting",
"kubernetes/clusters/registries",
"kubernetes/clusters/local",
"kubernetes/clusters/eks",
"kubernetes/clusters/cluster-adapters",
"kubernetes/workloads/deployments",
"kubernetes/workloads/jobs",
"kubernetes/workloads/images",
"kubernetes/workloads/bindings",
"kubernetes/workloads/object-lifecycle",
"kubernetes/objects/manifests",
"kubernetes/objects/helm-charts",
],
},
},
{
heading: "SQL",
intro:
"Database comparison, Effect SQL, Drizzle, Prisma ORM v8 (Postgres-only), committed migrations, and connection lifecycle. Deployment walkthroughs live with their runtime providers.",
pages: {
slugs: [
"sql/index",
"sql/databases",
"sql/effect-sql/postgres",
"sql/effect-sql/mysql",
"sql/effect-sql/d1",
"sql/effect-sql/migrations",
"sql/effect-sql/lifecycle",
"sql/drizzle/postgres",
"sql/drizzle/mysql",
"sql/drizzle/d1",
"sql/drizzle/migrations",
"sql/prisma/postgres",
"sql/prisma/contracts",
"sql/prisma/migrations",
],
},
},
{
heading: "Command",
intro:
"Cloud-agnostic local process primitives — memoized builds, one-off commands, and dev servers; the static-site guides in each cloud hub consume them under the hood.",
pages: {
slugs: ["command/index", "command/memoization", "command/dev-servers"],
},
},
];
const PROVIDERS_INTRO_BASE = `Service-level API reference, generated from JSDoc on the source \`.ts\` files via \`bun scripts/generate-api-reference.ts\`. Each page groups resources and capabilities by service, with a heading per resource and examples extracted from source JSDoc.`;
const PROVIDERS_INTRO = `${PROVIDERS_INTRO_BASE} Grouped by cloud below.`;
/**
* Enumerates every generated provider page under `providers/{Cloud}/...`,
* grouped by cloud. These pages are produced by `build:reference` (which runs
* before this script in the build), so they exist on disk at generation time
* even though they are gitignored.
*/
async function renderProvidersSection(): Promise<string> {
const providersDir = path.join(docsDir, "providers");
let clouds: string[];
try {
const entries = await readdir(providersDir, { withFileTypes: true });
clouds = entries
.filter((e) => e.isDirectory())
.map((e) => e.name)
.sort();
} catch (err: any) {
if (err?.code === "ENOENT") return `## Providers\n\n${PROVIDERS_INTRO}`;
throw err;
}
const blocks: string[] = [`## Providers`, PROVIDERS_INTRO];
for (const cloud of clouds) {
const slugs = await listSlugs(`providers/${cloud}`);
const pages = (await Promise.all(slugs.map(loadPage)))
.filter((p) => !p.draft)
// Starlight serves provider routes lowercased (e.g. the CamelCase source
// `providers/AWS/S3/Bucket.md` is reachable at `/providers/aws/s3/bucket`).
.map((p) => ({ ...p, href: p.href.toLowerCase() }))
.sort((a, b) => a.title.localeCompare(b.title));
if (pages.length === 0) continue;
blocks.push(`### ${cloud}`);
blocks.push(pages.map(renderPage).join("\n"));
}
return blocks.join("\n\n");
}
const HEADER = `# Alchemy
> Alchemy Effect is an Infrastructure-as-Effects (IaE) framework that combines cloud infrastructure and application logic into a single, type-safe program powered by [Effect](https://effect.website). Resources are declared as Effects; bindings wire IAM, env vars, and typed SDKs in one call; deploys and runtime share the same code.
This file is a navigation index for the documentation site at ${siteUrl}. Guide and concept pages are listed below with their URL and a one-line summary, so an agent can pick the right page in one hop. The service-level API reference is indexed separately in [llms-full.txt](${siteUrl}/llms-full.txt).`;
const FULL_HEADER = `# Alchemy
> Alchemy Effect is an Infrastructure-as-Effects (IaE) framework that combines cloud infrastructure and application logic into a single, type-safe program powered by [Effect](https://effect.website). Resources are declared as Effects; bindings wire IAM, env vars, and typed SDKs in one call; deploys and runtime share the same code.
This file is a navigation index for the documentation site at ${siteUrl}. Every page under \`/src/content/docs/\` is listed below with its URL and a one-line summary, so an agent can pick the right page in one hop. This is the full index including every service-level API reference page; the smaller [llms.txt](${siteUrl}/llms.txt) covers just the guides and concept docs.`;
function parseFrontmatter(source: string): Record<string, string> {
if (!source.startsWith("---")) return {};
const end = source.indexOf("\n---", 3);
if (end === -1) return {};
const block = source.slice(3, end);
const out: Record<string, string> = {};
for (const rawLine of block.split("\n")) {
const line = rawLine.trimEnd();
const m = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
if (!m) continue;
let value = m[2].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
out[m[1]] = value;
}
return out;
}
/**
* Extracts the nested `sidebar.order` value from a frontmatter block.
* Starlight uses this to order autogenerated sidebar groups; we mirror it
* so llms.txt lists pages in the same order the sidebar shows them.
* Returns `Infinity` when unset, so unordered pages sort after ordered ones.
*/
function parseSidebarOrder(source: string): number {
if (!source.startsWith("---")) return Number.POSITIVE_INFINITY;
const end = source.indexOf("\n---", 3);
if (end === -1) return Number.POSITIVE_INFINITY;
const block = source.slice(3, end);
const lines = block.split("\n");
for (let i = 0; i < lines.length; i++) {
if (!/^sidebar:\s*$/.test(lines[i])) continue;
for (let j = i + 1; j < lines.length; j++) {
if (/^\S/.test(lines[j])) break; // dedented out of the sidebar block
const m = lines[j].match(/^\s+order:\s*(-?[\d.]+)\s*$/);
if (m) return Number.parseFloat(m[1]);
}
break;
}
return Number.POSITIVE_INFINITY;
}
async function loadPage(slug: string): Promise<Page> {
const candidates = [`${slug}.mdx`, `${slug}.md`];
for (const rel of candidates) {
const full = path.join(docsDir, rel);
try {
// Dev-generated reference pages carry copy-editor markers.
const source = (await readFile(full, "utf8")).replace(
/<!--copy:[^>]*?-->|^<div data-copy=[^\n]*>\n|^<\/div><!--\/copy-->\n/gm,
"",
);
const fm = parseFrontmatter(source);
const title = fm.title;
const description = fm.description ?? fm.excerpt ?? "";
if (!title) {
throw new Error(`Missing title in frontmatter: ${rel}`);
}
return {
// `foo/index.mdx` is served at `/foo`.
href: `/${slug}`.replace(/\/index$/, "") || "/",
slug,
title,
description,
draft: fm.draft === "true" || (fm.draft as unknown as boolean) === true,
order: parseSidebarOrder(source),
};
} catch (err: any) {
if (err?.code !== "ENOENT") throw err;
}
}
throw new Error(`Page not found: ${slug} (looked for .mdx and .md)`);
}
async function listSlugs(
directory: string,
exclude: string[] = [],
): Promise<string[]> {
const dir = path.join(docsDir, directory);
const entries = await readdir(dir, { withFileTypes: true });
const slugs: string[] = [];
for (const entry of entries) {
const rel = `${directory}/${entry.name}`;
if (entry.isDirectory()) {
slugs.push(...(await listSlugs(rel, exclude)));
continue;
}
if (!entry.isFile()) continue;
const ext = path.extname(entry.name);
if (ext !== ".md" && ext !== ".mdx") continue;
const slug = `${directory}/${entry.name.slice(0, -ext.length)}`;
if (exclude.includes(slug)) continue;
slugs.push(slug);
}
slugs.sort();
return slugs;
}
function byOrderThenTitle(a: Page, b: Page): number {
if (a.order !== b.order) return a.order - b.order;
return a.title.localeCompare(b.title);
}
function renderPage(page: Page): string {
const url = `${siteUrl}${page.href}`;
const desc = page.description ? ` — ${page.description}` : "";
return `- [${page.title}](${url})${desc}`;
}
async function main() {
const sections: string[] = [];
for (const section of SECTIONS) {
const isDirectory = !("slugs" in section.pages);
const slugs =
"slugs" in section.pages
? section.pages.slugs
: await listSlugs(section.pages.directory, section.pages.exclude);
const pages = (await Promise.all(slugs.map(loadPage))).filter(
(p) => !p.draft,
);
// Directory sections mirror the sidebar's `sidebar.order` ordering; slug
// sections keep the curated order they were declared in.
if (isDirectory) pages.sort(byOrderThenTitle);
sections.push(`## ${section.heading}`);
if (section.intro) sections.push(section.intro);
sections.push(pages.map(renderPage).join("\n"));
}
const fullBody =
[FULL_HEADER, ...sections, await renderProvidersSection()].join("\n\n") +
"\n";
await writeFile(outFullFile, fullBody, "utf8");
console.log(`Wrote ${outFullFile}`);
const fullSizeKb = Math.round(Buffer.byteLength(fullBody, "utf8") / 1024);
const referenceSection = [
`## API reference`,
`${PROVIDERS_INTRO_BASE}\n\nThe service-level pages are indexed in [llms-full.txt](${siteUrl}/llms-full.txt), which repeats this entire file plus one link per service. Warning: it is large (~${fullSizeKb} KB) — only fetch it when you need to locate a specific resource's reference page.`,
];
const body = [HEADER, ...sections, ...referenceSection].join("\n\n") + "\n";
await writeFile(outFile, body, "utf8");
console.log(`Wrote ${outFile}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});