Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/excaliburjs/create-excalibur. A repo bootstrapping tool to get you started making your first games!
Something went wrong. Try again.
8.4 kB · 192 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193import path from "node:path";import { plainSnippet } from "../../docs/algolia.ts";import { docsCacheRoot } from "../../docs/cache.ts";import { DocsError } from "../../docs/errors.ts";import { syncApiSymbols, syncDocs } from "../../docs/fetch-docs.ts";import { loadPage, pageSectionMarkdown } from "../../docs/page.ts";import { loadPluginPage, syncPlugins } from "../../docs/plugins.ts";import { runDocsSearch } from "../../docs/search.ts";import { detectExcaliburVersion, refForVersion } from "../../docs/version.ts";import { jsonResult, textResult } from "../result.ts";import type { JsonSchema, Tool, ToolContext } from "../types.ts";import type { HitKind } from "../../docs/types.ts";import type { DocsPage } from "../../docs/page.ts";
const PROJECT_DIR_PROP: Record<string, JsonSchema> = { projectDir: { type: "string", description: "Absolute path to the project root (used to auto-detect the installed Excalibur version). Defaults to the server's working directory.", },};
const REF_PROP: Record<string, JsonSchema> = { ref: { type: "string", description: 'Docs version ref, e.g. "v0.32.0" or "main". Defaults to the version matching the project\'s installed excalibur (or "main").', },};
export function resolveProjectDir(args: { projectDir?: string | null }, ctx: ToolContext): string { return path.resolve(ctx.defaultProjectDir, args.projectDir ?? ".");}
function resolveRef(args: { ref?: string | null; projectDir?: string | null }, ctx: ToolContext): string { if (args.ref) return args.ref; const projectDir = resolveProjectDir(args, ctx); return refForVersion(detectExcaliburVersion(projectDir).version);}
/** Rewrite CLI-facing hints ("run `ex docs offline`") into tool-facing ones. */function toolifyHint(error: unknown): unknown { if (error instanceof DocsError && typeof error.hint === "string" && error.hint.includes("ex docs offline")) { error.hint = "Call the docs_sync tool first to download the docs for offline use."; } if (error instanceof DocsError && typeof error.hint === "string" && error.hint.includes("--ref main")) { error.hint = error.hint.replace(/--ref main/g, 'ref: "main"'); } return error;}
interface DocsSearchArgs { query: string; limit?: number; kind?: HitKind; offline?: boolean; ref?: string; projectDir?: string;}
interface DocsGetPageArgs { slug?: string; path?: string; anchor?: string; full?: boolean; ref?: string; projectDir?: string;}
interface DocsSyncArgs { force?: boolean; apiSymbols?: boolean; plugins?: boolean; ref?: string; projectDir?: string;}
export const docsTools: [Tool<DocsSearchArgs>, Tool<DocsGetPageArgs>, Tool<DocsSyncArgs>] = [ { name: "docs_search", description: "Search the Excalibur.js documentation, API reference, and @excaliburjs/* plugin READMEs. Returns hits with title, url, snippet, and (for docs/plugin pages) a slug/path usable with docs_get_page. Searches live by default, falling back to the offline cache when the network is unavailable; plugin hits always come from the offline plugin index (see docs_sync).", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search terms, e.g. \"actor collision events\"." }, limit: { type: "integer", minimum: 1, maximum: 25, description: "Max results (default 10)." }, kind: { type: "string", enum: ["docs", "api", "plugin"], description: "Restrict to guide docs, API reference, or @excaliburjs plugin READMEs. Omit for all." }, offline: { type: "boolean", description: "Force the offline index (requires a prior docs_sync). Default false." }, ...REF_PROP, ...PROJECT_DIR_PROP, }, required: ["query"], }, async handler(args, ctx) { const ref = resolveRef(args, ctx); let result; try { result = await runDocsSearch({ query: args.query, ref, limit: args.limit ?? 10, kind: args.kind ?? null, offline: args.offline ?? false, }); } catch (error) { throw toolifyHint(error); } const hits = result.hits.map((h) => ({ ...h, snippet: plainSnippet(h.snippet) })); const payload: { source: string; ref: string; hits: unknown[]; fallback?: string } = { source: result.source, ref, hits }; if (result.fallback) payload.fallback = `Live search unavailable (${result.fallback.message}); used the offline index.`; return jsonResult(payload); }, }, { name: "docs_get_page", description: "Fetch an Excalibur docs page or plugin README (or one section of it) as markdown. Pass a slug or path from docs_search results; plugin pages use \"/plugins/<name>\" slugs and require a prior docs_sync. Docs pages fetch on demand — no prior docs_sync needed when online.", inputSchema: { type: "object", properties: { slug: { type: "string", description: 'Page slug from a docs_search hit, e.g. "/colliders-and-shapes" ("/" is the docs root) or "/plugins/tiled" for a plugin README.' }, path: { type: "string", description: "site/docs-relative mdx path from an offline docs_search hit. Provide either slug or path, not both." }, anchor: { type: "string", description: "Section anchor from a docs_search hit — returns just that section." }, full: { type: "boolean", description: "Return the whole page even when anchor is set. Default false." }, ...REF_PROP, ...PROJECT_DIR_PROP, }, }, async handler(args, ctx) { const hasSlug = typeof args.slug === "string"; const hasPath = typeof args.path === "string"; if (hasSlug === hasPath) { throw new DocsError("docs_get_page needs exactly one of slug or path.", { hint: "Use the slug (docs hits) or path (offline hits) from a docs_search result.", }); } const isPlugin = hasSlug && args.slug!.startsWith("/plugins/"); let page: DocsPage; try { page = isPlugin ? loadPluginPage(args.slug!) : await loadPage(resolveRef(args, ctx), { slug: args.slug ?? null, path: args.path ?? null }); } catch (error) { throw toolifyHint(error); } const { markdown, partial } = pageSectionMarkdown(page, args.anchor ?? null, { full: args.full ?? false }); const preamble = [ `Title: ${page.title}`, `URL: ${page.url}`, `Ref: ${isPlugin ? `plugin@${page.version ?? "latest"}` : page.ref}`, `Section: ${partial ? `${args.anchor} (partial)` : "full page"}`, ].join("\n"); return textResult(`${preamble}\n\n${markdown}`); }, }, { name: "docs_sync", description: "Download the Excalibur docs for a version (plus @excaliburjs plugin READMEs) into the local cache to enable offline docs_search and plugin search. Slow (fetches the whole docs tree); needed for offline use, version-pinned search, or plugin-README search.", inputSchema: { type: "object", properties: { force: { type: "boolean", description: "Re-download even if the cache is fresh. Default false." }, apiSymbols: { type: "boolean", description: "Also refresh the API symbol link map. Default true." }, plugins: { type: "boolean", description: "Also fetch @excaliburjs/plugin-* READMEs from npm and index them. Default true." }, ...REF_PROP, ...PROJECT_DIR_PROP, }, }, async handler(args, ctx) { const ref = resolveRef(args, ctx); const warnings: string[] = []; const message = (error: unknown) => (error instanceof Error ? error.message : String(error)); const { fetched, total, pages } = await syncDocs(ref, { force: args.force ?? false }); if (args.apiSymbols ?? true) { await syncApiSymbols().catch((error) => { warnings.push(`API symbol sync failed: ${message(error)}`); }); } let plugins: number | null = null; if (args.plugins ?? true) { try { const result = await syncPlugins(); plugins = result.plugins; warnings.push(...result.warnings); } catch (error) { warnings.push(`Plugin README sync failed: ${message(error)}`); } } return jsonResult({ ref, pages, fetched, total, plugins, cacheDir: docsCacheRoot(), warnings }); }, },];