Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/ui/bin/ca-ui-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
searchIndex,
} from "../scripts/index-search.ts";
import type { StyleFile } from "../scripts/styles-markdown.ts";
import { API_DIR, API_PATHS } from "../src/cli/agent-guide.ts";
import { type Equivalent, findEquivalent } from "../src/cli/equivalents.ts";
import { type MigrationFile, migrationMarkdown } from "../src/cli/migrations.ts";
import { buildReport } from "../src/cli/report.ts";
Expand Down Expand Up @@ -412,7 +413,7 @@ server.registerTool(
? demos.find((d) => d.id === num.padStart(3, "0"))
: demos.find((d) => raw.endsWith(d.demo.slice(d.demo.indexOf("/docs/"))));
if (!hit) return fail(demos.length ? `Unknown demo id "${raw}"; use search_demos.` : noDemos);
out.push(readFileSync(join(DIST, `api/v1/en/e/${hit.id}.txt`), "utf8"));
out.push(readFileSync(join(DIST, API_DIR, API_PATHS.demoSource(hit.id)), "utf8"));
}
return text(out.join("\n\n---\n\n"));
}
Expand Down
8 changes: 4 additions & 4 deletions packages/ui/scripts/ai-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export {
SITE,
} from "../src/cli/agent-guide.ts";

import { ENTRY_PAGES, SITE } from "../src/cli/agent-guide.ts";
import { API_PATHS, apiUrl, ENTRY_PAGES, SITE } from "../src/cli/agent-guide.ts";

export interface DocLink {
slug: string;
Expand Down Expand Up @@ -193,17 +193,17 @@ export function buildLlmsIndex(i: {
},
{
title: "Demo catalog",
href: `${SITE}/api/v1/catalog.json`,
href: apiUrl(API_PATHS.catalog),
description: "every numbered demo (#NNN): id, page, components, summary",
},
{
title: "Demo source",
href: `${SITE}/api/v1/en/e/001.txt`,
href: apiUrl(API_PATHS.demoSource("001")),
description: "one demo as text: swap 001 for any id from the catalog",
},
{
title: "Demo index (TSV)",
href: `${SITE}/api/v1/en/catalog.tsv`,
href: apiUrl(API_PATHS.catalogTsv),
description: "id, group, page, title, tags, summary: grep it",
},
{
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/scripts/atlas-markdown.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// The atlas as plain markdown for agents: one file per term (`/llms/atlas/<slug>.txt`), an index of all terms
// (`/llms/atlas.txt`) and the compact line the MCP `search_terms` returns. Pure functions over the dataset, so the
// site endpoints, the MCP build and the tests share one text.

import {
ATLAS_LIMITS,
ATLAS_REVIEWED,
Expand All @@ -24,8 +25,7 @@ import {
type Reference,
STRENGTH_LABEL,
} from "../../../site/src/data/evidence/references.ts";

export const SITE = "https://ui.codeatlas.com.br";
import { SITE } from "../src/cli/agent-guide.ts";

/** Path of a term's markdown twin. */
export const atlasTermPath = (t: Pick<Term, "term">) => `/llms/atlas/${termSlug(t)}.txt`;
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/scripts/build-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { buildIndexFiles } from "./index-build.ts";
import { buildManifest } from "./manifest/components.ts";
import { buildDemos, checkCatalog } from "./manifest/demos.ts";
import { buildGuides, buildRecipes } from "./manifest/guides.ts";
import { DEMOS_DIR, ROOT } from "./manifest/paths.ts";
import { API_DIR, DEMOS_DIR, ROOT } from "./manifest/paths.ts";
import { buildSurfaces, buildTokens } from "./manifest/tokens.ts";
import { buildParts, demosMarkdown, skillMarkdown } from "./manifest/writers.ts";
import { buildStyleFile } from "./styles-markdown.ts";
Expand Down Expand Up @@ -121,7 +121,7 @@ if (import.meta.main) {
mkdirSync(dirname(join(siteRoot, name)), { recursive: true });
writeFileSync(join(siteRoot, name), body);
}
for (const base of [join(dist, "api/v1"), resolve(ROOT, "../../site/public/api/v1")])
for (const base of [join(dist, API_DIR), resolve(ROOT, "../../site/public", API_DIR)])
for (const [name, body] of Object.entries(files)) {
mkdirSync(dirname(join(base, name)), { recursive: true });
writeFileSync(join(base, name), body);
Expand Down
9 changes: 4 additions & 5 deletions packages/ui/scripts/index-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*
* Pure: takes the demo records, returns `{ path: body }`. The caller decides where the files go.
*/
import { API_DIR, API_PATHS, apiUrl, SITE } from "../src/cli/agent-guide.ts";
import { type IndexFacets, PENDING_FACETS } from "./index-schema.ts";

/** The fields of a demo record the index reads (a subset of `ManifestDemo`). */
Expand Down Expand Up @@ -36,8 +37,6 @@ export interface IndexEntry extends Partial<IndexFacets> {
names: string[];
}

const SITE = "https://ui.codeatlas.com.br";

/** Facets that are filter chips, and how to read their values off an entry. A pending facet is absent from the entry, so it has no counts. */
const COUNTED: Record<string, (e: IndexEntry) => string[] | undefined> = {
kind: (e) => [e.kind as string],
Expand Down Expand Up @@ -124,7 +123,7 @@ export function entryMarkdown(d: IndexDemo): string {
"",
`Needs: ${d.needs}`,
`Live demo: ${d.demo}`,
`Reference: ${SITE}/api/v1/en/e/${d.id}.txt`,
`Reference: ${apiUrl(API_PATHS.demoSource(d.id))}`,
"",
"## Source",
"",
Expand All @@ -143,7 +142,7 @@ function indexJson(entries: readonly IndexEntry[], version: string): string {
urls: {
page: `${SITE}/gallery/{id}/`,
markdown: `${SITE}/gallery/{id}.md`,
reference: `${SITE}/api/v1/en/e/{id}.txt`,
reference: apiUrl(API_PATHS.demoSource("{id}")),
},
facets: facetCounts(entries),
},
Expand All @@ -160,7 +159,7 @@ export function buildIndexFiles(
): Record<string, string> {
const entries = indexEntries(demos);
const files: Record<string, string> = {
"api/v1/index.json": indexJson(entries, version),
[`${API_DIR}/${API_PATHS.index}`]: indexJson(entries, version),
};
for (const d of demos) files[`gallery/${d.id}.md`] = entryMarkdown(d);
return files;
Expand Down
10 changes: 5 additions & 5 deletions packages/ui/scripts/manifest/demos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join, relative } from "node:path";
import { checkIdRange, checkIndexFacets, ID_PATTERN, type IndexFacets } from "../index-schema.ts";
import type { Manifest } from "./components.ts";
import { mdxFiles } from "./guides.ts";
import { API, DEMOS_DIR, DOCS_DIR, PKG, SITE } from "./paths.ts";
import { API_PATHS, apiUrl, DEMOS_DIR, DOCS_DIR, PKG, SITE } from "./paths.ts";

/** One animated property of a motion demo: literal values, each of which must appear verbatim in the demo source. */
export interface ManifestMotion {
Expand Down Expand Up @@ -305,8 +305,8 @@ export function buildDemos(
return { slug, first: g[0].id, last: g[g.length - 1].id, count: g.length };
});
const files: Record<string, string> = {
"catalog.json": `${JSON.stringify({ version: manifest.version, count: demos.length, groups, elements: demos.map(({ source: _, ...d }) => d) }, null, "\t")}\n`,
"en/catalog.tsv": `${["id\tgroup\tpage\ttitle\tnames\ttags\tsummary", ...demos.map((d) => [d.id, d.group, d.page, d.title, (d.names ?? []).join(" | "), d.tags.join(","), d.summary].map((v) => v.replace(/[\t\n]/g, " ")).join("\t"))].join("\n")}\n`,
[API_PATHS.catalog]: `${JSON.stringify({ version: manifest.version, count: demos.length, groups, elements: demos.map(({ source: _, ...d }) => d) }, null, "\t")}\n`,
[API_PATHS.catalogTsv]: `${["id\tgroup\tpage\ttitle\tnames\ttags\tsummary", ...demos.map((d) => [d.id, d.group, d.page, d.title, (d.names ?? []).join(" | "), d.tags.join(","), d.summary].map((v) => v.replace(/[\t\n]/g, " ")).join("\t"))].join("\n")}\n`,
};
const motionLines = (d: ManifestDemo) =>
d.motion?.length
Expand Down Expand Up @@ -340,7 +340,7 @@ export function buildDemos(
`Components: ${d.components.join(", ") || "none"}`,
`Live demo: ${d.demo}`,
];
files[`en/e/${d.id}.txt`] = `${header.join("\n")}\n\n${d.source}`;
files[API_PATHS.demoSource(d.id)] = `${header.join("\n")}\n\n${d.source}`;
files[`en/p/${d.id}.txt`] = `${[
`Use ${PKG} demo #${d.id} "${d.title}" (${d.group}/${d.page}).`,
`What it shows: ${d.summary.replace(/\.$/, "")}. When to use: ${d.use.replace(/\.$/, "")}.`,
Expand All @@ -358,7 +358,7 @@ export function buildDemos(
" variants of the reference as they are; no layout CSS, --ca-* tokens only (SKILL.md rules);",
" replace sample data/copy with the project's; keep the first comment line.",
`Report: #${d.id} ${d.title} → file, what changed, ${d.demo}; what you could not check (light/dark, 375px, keyboard, reduced motion).`,
`Reference: ${API}/en/e/${d.id}.txt Components API: ca-ui-mcp get_component(${d.components[0] ?? "…"})`,
`Reference: ${apiUrl(API_PATHS.demoSource(d.id))} Components API: ca-ui-mcp get_component(${d.components[0] ?? "…"})`,
].join("\n")}\n`;
}
return { demos, files };
Expand Down
3 changes: 1 addition & 2 deletions packages/ui/scripts/manifest/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ export const LIB = join(ROOT, "src/lib");
export const PKG = "@codeatlasdev/ui";
export const DOCS_DIR = resolve(ROOT, "../../site/src/content/docs");
export const DEMOS_DIR = resolve(ROOT, "../../site/src/demos");
export const SITE = "https://ui.codeatlas.com.br";
export const API = `${SITE}/api/v1`;
export { API, API_DIR, API_PATHS, apiUrl, SITE } from "../../src/cli/agent-guide.ts";

/** Public entry points: import specifier → entry file under src/lib. */
export const ENTRIES: Record<string, string> = {
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/scripts/manifest/writers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/** Writers: the parts file, API.md (agent skill reference) and DEMOS.md (demo catalog table). */
import type { Manifest } from "./components.ts";
import type { ManifestDemo } from "./demos.ts";
import { API } from "./paths.ts";
import { API_PATHS, apiUrl } from "./paths.ts";
import type { ManifestDataAttribute, ManifestPart, ManifestStyling } from "./styling.ts";

export function demosMarkdown(demos: ManifestDemo[]): string {
return `${[
"<!-- generated by packages/ui/scripts/build-manifest.ts (bun run --cwd packages/ui manifest -- --skill). Do not edit. -->",
"# Demo catalog",
"",
`Every demo has a stable number. Fetch one with \`get_demo\` (MCP) or ${API}/en/e/NNN.txt.`,
`Every demo has a stable number. Fetch one with \`get_demo\` (MCP) or ${apiUrl(API_PATHS.demoSource("NNN"))}.`,
"",
"| id | title | group/page | components |",
"|---|---|---|---|",
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/scripts/styles-markdown.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// The design-style pages as plain markdown for agents: one file per style (`/llms/styles/<id>.txt`), a digest of all
// of them (`/llms/styles.txt`) and the file the MCP `get_style` tool serves offline (`dist/styles.json`). Pure functions
// over the same data the pages render, so the site endpoints, the MCP build and the tests share one text.

import { STRENGTH_LABEL } from "../../../site/src/data/evidence/references.ts";
import {
recipeKnobs,
Expand All @@ -11,8 +12,7 @@ import {
styleReferences,
TRAIT_AXES,
} from "../../../site/src/data/styles/styles.ts";

export const SITE = "https://ui.codeatlas.com.br";
import { SITE } from "../src/cli/agent-guide.ts";

/** Path of the digest of every style. */
export const STYLES_INDEX_PATH = "/llms/styles.txt";
Expand Down
18 changes: 18 additions & 0 deletions packages/ui/src/cli/agent-guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@
// src/cli so the CLI stays self-contained; scripts/ai-entry.ts re-exports it for the site and MCP.
export const SITE = "https://ui.codeatlas.com.br";

/** Where the machine-readable endpoints live under the site root (and under `dist/` and `site/public/`). */
export const API_DIR = "api/v1";

/** Base URL of the machine-readable endpoints. */
export const API = `${SITE}/${API_DIR}`;

/** Endpoint paths under `API_DIR`, relative to it (the manifest writes the same paths to `dist/` and `site/public/`). */
export const API_PATHS = {
catalog: "catalog.json",
catalogTsv: "en/catalog.tsv",
index: "index.json",
/** The source of one demo as plain text; `{id}` stays literal in a template. */
demoSource: (id: string) => `en/e/${id}.txt`,
} as const;

/** The absolute URL of an `API_PATHS` entry. */
export const apiUrl = (path: string) => `${API}/${path}`;

/** Docs pages an agent opens first, in this order. The slugs are checked against the real pages by a test. */
export const ENTRY_PAGES = {
recipes: "getting-started/recipes",
Expand Down
55 changes: 55 additions & 0 deletions packages/ui/tests/api-paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from "bun:test";
import { readdirSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";
import { API as MANIFEST_API, SITE as MANIFEST_SITE } from "../scripts/manifest/paths.ts";
import { API, API_PATHS, apiUrl, SITE } from "../src/cli/agent-guide.ts";

/**
* #652: ai-entry.ts, index-build.ts, the manifest writers, the MCP server and the site's endpoint list each
* spelled `${SITE}/api/v1/en/e/<id>.txt` and friends by hand, and `SITE` itself was declared nine times, so a
* rename of one endpoint left the others pointing at a 404. `src/cli/agent-guide.ts` owns the host, the base
* and every endpoint name; everything else builds from it.
*/
const ROOT = join(import.meta.dir, "../../..");

describe("API paths (#652)", () => {
it("builds the published endpoints", () => {
expect(SITE).toBe("https://ui.codeatlas.com.br");
expect(API).toBe(`${SITE}/api/v1`);
expect(apiUrl(API_PATHS.catalog)).toBe(`${SITE}/api/v1/catalog.json`);
expect(apiUrl(API_PATHS.catalogTsv)).toBe(`${SITE}/api/v1/en/catalog.tsv`);
expect(apiUrl(API_PATHS.index)).toBe(`${SITE}/api/v1/index.json`);
expect(apiUrl(API_PATHS.demoSource("001"))).toBe(`${SITE}/api/v1/en/e/001.txt`);
expect(apiUrl(API_PATHS.demoSource("{id}"))).toBe(`${SITE}/api/v1/en/e/{id}.txt`);
});

it("the manifest uses the same host and base", () => {
expect(MANIFEST_SITE).toBe(SITE);
expect(MANIFEST_API).toBe(API);
});

it("no script, bin or API page spells an /api/v1/ URL or declares SITE itself", () => {
const roots = ["packages/ui/scripts", "packages/ui/bin", "site/src/pages/api"];
const owners = new Set(["packages/ui/src/cli/agent-guide.ts"]);
const hits: string[] = [];
const walk = (dir: string) => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) {
if (e.name !== "node_modules") walk(p);
} else if (/\.(ts|astro)$/.test(e.name) && !owners.has(relative(ROOT, p))) {
readFileSync(p, "utf8")
.split("\n")
.forEach((l, i) => {
// `//` comments and JSDoc may name the path; code may not
if (/^\s*(\/\/|\*|\/\*)/.test(l)) return;
if (/\$\{(SITE|API)\}\/api\/v1|["'`]api\/v1\/|const SITE = "https:/.test(l))
hits.push(`${relative(ROOT, p)}:${i + 1}`);
});
}
}
};
for (const r of roots) walk(join(ROOT, r));
expect(hits).toEqual([]);
});
});
10 changes: 5 additions & 5 deletions site/src/pages/api/v1/endpoints.json.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const SITE = "https://ui.codeatlas.com.br";
import { API_PATHS, apiUrl, SITE } from "../../../../../packages/ui/src/cli/agent-guide";

/** The real machine-readable endpoints: a guessed URL under /api/v1/ is a 404, this lists what exists. (`index.json` itself is the generated gallery index, #570.) */
export const GET = () =>
Expand All @@ -10,10 +10,10 @@ export const GET = () =>
"llms.txt": `${SITE}/llms.txt`,
"llms-full.txt": `${SITE}/llms-full.txt`,
"prompt.txt": `${SITE}/prompt.txt`,
"gallery index (json)": `${SITE}/api/v1/index.json`,
"demo catalog (json)": `${SITE}/api/v1/catalog.json`,
"demo catalog (tsv)": `${SITE}/api/v1/en/catalog.tsv`,
"demo source (text)": `${SITE}/api/v1/en/e/{id}.txt`,
"gallery index (json)": apiUrl(API_PATHS.index),
"demo catalog (json)": apiUrl(API_PATHS.catalog),
"demo catalog (tsv)": apiUrl(API_PATHS.catalogTsv),
"demo source (text)": apiUrl(API_PATHS.demoSource("{id}")),
"docs search index": `${SITE}/search-index.json`,
},
note: "Component props are not served over HTTP: use the ca-ui-mcp server or node_modules/@codeatlasdev/ui/dist/components.json of the installed version.",
Expand Down