diff --git a/.async-pipeline/tasks.lock.json b/.async-pipeline/tasks.lock.json index 3825842..afb2c7e 100644 --- a/.async-pipeline/tasks.lock.json +++ b/.async-pipeline/tasks.lock.json @@ -335,5 +335,5 @@ } ], "hash": "sha256:c0794418dd6a745cb8892a404c68926b88a3a79b9d522281edc0171aa88f3e2a", - "generatedAt": "2026-06-14T17:30:22.259Z" + "generatedAt": "2026-06-14T19:47:18.657Z" } diff --git a/.github/async-pipeline.lock.json b/.github/async-pipeline.lock.json index 85cd74b..230dfbd 100644 --- a/.github/async-pipeline.lock.json +++ b/.github/async-pipeline.lock.json @@ -4,7 +4,7 @@ "config": "pipeline.js", "workflow": ".github/workflows/async-pipeline.yml", "hash": "sha256:7c8a1dde68f81793d9c0cc4b1cbfd1ae3e6ff63f95429b50b103a4fad9b189f0", - "generatedAt": "2026-06-14T17:30:22.256Z", + "generatedAt": "2026-06-14T19:47:18.653Z", "triggers": { "pull_request": {}, "push": { diff --git a/pipeline.js b/pipeline.js index 51f6a3f..d2311b5 100644 --- a/pipeline.js +++ b/pipeline.js @@ -119,9 +119,9 @@ export default definePipeline({ ] }), "docs.site": task({ - description: "Build the README-backed GitHub Pages site.", - inputs: ["README.md", "scripts/build-pages.js"], - outputs: [".async/pages/index.html"], + description: "Build the standardized GitHub Pages documentation site.", + inputs: ["README.md", "templates/**/*.md", "scripts/build-pages.js"], + outputs: [".async/pages/**"], cache: true, // TODO(@async/pipeline): replace this fallback when pipeline provides a first-class README-to-Pages builder. run: sh`node scripts/build-pages.js` diff --git a/scripts/build-pages.js b/scripts/build-pages.js index 1404739..1a091e8 100644 --- a/scripts/build-pages.js +++ b/scripts/build-pages.js @@ -1,62 +1,202 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; +#!/usr/bin/env node +import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { basename, dirname, join, relative } from "node:path"; -const root = process.cwd(); -const readmePath = resolve(root, "README.md"); -const outputPath = resolve(root, ".async/pages/index.html"); +const site = { + "title": "@async/dispatch", + "repo": "dispatch", + "pipelineFile": "pipeline.js", + "stage": "Beta", + "description": "Installable local coordination CLI for goal-first async chat work, plan boards, runtime ledgers, workers, and receipts.", + "lead": "Keep broad async work explicit before it turns into repo edits, chats, or release handoffs.", + "quickstart": "pnpm add --global @async/dispatch\nasync-dispatch help", + "docsRoots": [ + "templates" + ] +}; +const outDir = ".async/pages"; +const asyncProjects = [ + ["@async/db", "https://async.github.io/db/", "Data workflow"], + ["@async/web", "https://async.github.io/web/", "Web runtime"], + ["@async/pipeline", "https://async.github.io/pipeline/", "Pipeline workflows"], + ["@async/dispatch", "https://async.github.io/dispatch/", "Goal-first coordination"], + ["@async/auto-git", "https://async.github.io/auto-git/", "Git handoffs"], + ["@async/api-contract", "https://async.github.io/api-contract/", "API ledgers"], + ["@async/claims", "https://async.github.io/claims/", "Doc claim checks"] +]; -const markdown = await readFile(readmePath, "utf8"); -const escaped = markdown - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">"); +await rm(outDir, { recursive: true, force: true }); +await mkdir(outDir, { recursive: true }); -const html = ` +const readme = await readFile("README.md", "utf8"); +const docs = await collectDocs(site.docsRoots); +for (const doc of docs) { + const markdown = await readFile(doc.path, "utf8"); + const htmlPath = join(outDir, doc.href); + await mkdir(dirname(htmlPath), { recursive: true }); + await writeFile(htmlPath, layout({ + title: doc.title, + body: renderMarkdown(markdown), + rootPrefix: "../".repeat(doc.href.split("/").length - 1) + })); +} + +await writeFile(join(outDir, "index.html"), layout({ + title: site.title, + body: home(readme, docs), + rootPrefix: "" +})); + +function home(readme, docs) { + const guideLinks = docs.length ? docs.map((doc) => + `${escapeHtml(doc.title)}${escapeHtml(doc.source)}` + ).join("\n") : "
No guide pages are published for this repo yet.
"; + const related = asyncProjects + .filter(([name]) => name !== site.title) + .map(([name, url, label]) => `${name}${label}`) + .join("\n"); + return ` +${escapeHtml(site.stage)} / Async
+${renderInline(site.description)}
+${renderInline(site.lead)}
+ +${escapeHtml(site.quickstart)}
+ ${escapeHtml(code.join("\n"))}`); code = null; } else { code = []; }
+ continue;
}
+ if (code) { code.push(line); continue; }
+ if (line.startsWith("|")) { closeList(); table.push(line); continue; }
+ closeTable();
+ if (/^###\s+/.test(line)) { closeList(); html.push(`${renderInline(line.replace(/^>\s?/, ""))}`); continue; } + if (!line.trim()) { closeList(); continue; } + closeList(); html.push(`
${renderInline(line.trim())}
`); + } + closeList(); closeTable(); + return html.join("\n"); +} + +function renderTable(lines) { + const rows = lines.map((line) => line.trim().slice(1, -1).split("|").map((cell) => cell.trim())); + const body = rows.filter((row) => !row.every((cell) => /^:?-{3,}:?$/.test(cell))).map((row, index) => { + const tag = index === 0 ? "th" : "td"; + return `${escapeHtml(segment.slice(1, -1))}`;
}
+ return renderTextLinks(segment);
+ }).join("");
+}
- code {
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
- font-size: 0.95em;
+function renderTextLinks(value) {
+ return String(value).split(/(\[[^\]]+\]\([^)]+\))/g).map((segment) => {
+ const match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(segment);
+ if (match) return `${escapeHtml(match[1])}`;
+ let html = escapeHtml(segment);
+ for (const [name, url] of asyncProjects) {
+ if (name === site.title) continue;
+ html = html.replace(new RegExp(escapeRegExp(name), "g"), `${name}`);
}
-
-
-
- ${escaped}
-