Public: anyone can read this; the crew can change it.

/docs reads the forge's documentation on the forge itself: an index and each doc rendered, with no hop to the code host #11081

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/11067-docs-reads-the-forge-s-documentation-on → main 5 commits fork 2 threads · 2 unresolved

  • Merged. torque/11067-docs-reads-the-forge-s-documentation-on was merged into main as d59f243.

Review

Sign in to review this change.
8 files changed +235 −59
src/tui/forge.ts +8
@@ -81,6 +81,7 @@ import { noteRead, telemetryReader } from "../actions/telemetry.ts";
8181 import { queuesFor, systemFor } from "../system/telemetry.ts";
8282 import { apiScreen } from "./screens/api.ts";
8383 import { changelogScreen } from "./screens/changelog.ts";
84 import { docsScreen } from "./screens/docs.ts";
8485 import { keeperScreen } from "./screens/keeper.ts";
8586 import { releasesScreen } from "./screens/releases.ts";
8687 import { mayRead } from "../release/gate.ts";
@@ -514,6 +515,13 @@ export function forgeCommands(session: Session): (ctx: Ctx) => Command[] {
514515 hint: "the forge's endpoints; run a GET",
515516 run: () => ctx.push(apiScreen(session, ctx)),
516517 });
518 // The forge's documentation, as /docs lists it: for anyone.
519 commands.push({
520 id: "docs",
521 title: "Docs",
522 hint: "the forge's documentation; Enter reads one",
523 run: () => ctx.push(docsScreen(ctx)),
524 });
517525 // The last releases with their pulls, as /changelog shows them: for anyone.
518526 commands.push({
519527 id: "changelog",
src/tui/screens/docs.ts added +55
@@ -0,0 +1,55 @@
1 /**
2 * The forge's documentation, the terminal's /docs: the same listing the page
3 * makes (`docEntries`), for anyone, a guest included. Enter reads a document,
4 * rendered as markdown, scrolled with j and k.
5 */
6
7 import { docEntries, docText, type DocEntry } from "../../view/docs.ts";
8 import type { Ctx, Screen } from "../app/app.ts";
9 import { binding } from "../app/keys.ts";
10 import { renderMarkdown } from "../ui/markdown.ts";
11 import { Scroll, ListState, drawLines, drawList, type ListRow } from "./common.ts";
12
13 export function docRows(entries: readonly DocEntry[]): ListRow[] {
14 return entries.map((e) => ({ title: e.title }));
15 }
16
17 function docScreen(ctx: Ctx, entry: DocEntry): Screen {
18 const scroll = new Scroll();
19 let cache: { w: number; lines: ReturnType<typeof renderMarkdown> } | undefined;
20 const text = docText(entry);
21 return {
22 title: entry.title,
23 keys: [binding(["j", "k"], "scroll"), binding(["esc", "q"], "back")],
24 render(area, s) {
25 const w = Math.max(20, area.w - 2);
26 if (cache === undefined || cache.w !== w) cache = { w, lines: renderMarkdown(text, w, ctx.theme.c) };
27 drawLines(s, area, ctx.theme.c, scroll, cache.lines);
28 },
29 key(e) {
30 if (e.key === "esc" || e.key === "q") ctx.pop();
31 else scroll.key(e, cache?.lines.length ?? 0);
32 return true;
33 },
34 };
35 }
36
37 export function docsScreen(ctx: Ctx): Screen {
38 const state = new ListState();
39 const entries = docEntries();
40 return {
41 title: "Docs",
42 keys: [binding(["j", "k"], "move"), binding(["enter"], "read"), binding(["esc", "q"], "close")],
43 render(area, s) {
44 drawList(s, area, ctx.theme.c, state, docRows(entries));
45 },
46 key(e) {
47 if (e.key === "esc" || e.key === "q") ctx.pop();
48 else if (e.key === "enter") {
49 const entry = entries[state.index];
50 if (entry !== undefined) ctx.push(docScreen(ctx, entry));
51 } else state.key(e, entries.length);
52 return true;
53 },
54 };
55 }
src/view/docs.ts added +47
@@ -0,0 +1,47 @@
1 /**
2 * The forge's own documentation as plain data: the listing of `docs/*.md` in
3 * the release's checkout and the text of one file. The page (/docs) and the
4 * terminal screen read here, so both show the same list. A name is served
5 * only when it is in the listing, so a request's text is looked up, never
6 * joined into a path. Docs change only with the release: the listing is kept
7 * for the life of the process.
8 */
9
10 import { readdirSync, readFileSync } from "node:fs";
11 import { join } from "node:path";
12
13 const DOCS = join(import.meta.dir, "..", "..", "docs");
14 export const DOC_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,80}\.md$/;
15
16 export interface DocEntry {
17 readonly name: string;
18 readonly title: string;
19 }
20
21 let listing: readonly DocEntry[] | undefined;
22
23 /** The first `#` line of a document, or its file name. */
24 export function titleOf(name: string, text: string): string {
25 const m = /^# +(.+?)\s*#*\s*$/m.exec(text);
26 return m?.[1]?.replace(/[`*_]/g, "") ?? name.slice(0, -3);
27 }
28
29 export function docEntries(): readonly DocEntry[] {
30 if (listing !== undefined) return listing;
31 const out: DocEntry[] = [];
32 for (const d of readdirSync(DOCS, { withFileTypes: true })) {
33 if (!d.isFile() || !DOC_NAME.test(d.name)) continue;
34 out.push({ name: d.name, title: titleOf(d.name, readFileSync(join(DOCS, d.name), "utf8")) });
35 }
36 out.sort((a, b) => a.name.localeCompare(b.name));
37 return (listing = out);
38 }
39
40 /** The entry for a name, or undefined when the name is not in the listing. */
41 export function docEntry(name: string): DocEntry | undefined {
42 return DOC_NAME.test(name) ? docEntries().find((e) => e.name === name) : undefined;
43 }
44
45 export function docText(entry: DocEntry): string {
46 return readFileSync(join(DOCS, entry.name), "utf8");
47 }
test/palette-roster.test.ts +4
@@ -120,6 +120,7 @@ test("a site-admin person is offered the whole roster", () => {
120120 ["scrapes", "Scrapes", "what was read by repository, and groups in a running capture"],
121121 ["traffic", "Traffic", "who is using the forge's answering time, and the top readers"],
122122 ["api", "API", "the forge's endpoints; run a GET"],
123 ["docs", "Docs", "the forge's documentation; Enter reads one"],
123124 ["changelog", "Changelog", "the last 30 releases and their pulls"],
124125 ["releases", "Releases", "the live release and the kept ones"],
125126 ["keeper", "Release keeper", "what holds the next cut, and each release it cut with its probes"],
@@ -225,6 +226,7 @@ test("a site-admin agent is offered the roster without the person-only entries",
225226 ["resilience", "Resilience", "where snapshots go and the mirror; each protection green, red or untested"],
226227 ["origin-apps", "Connection apps", "your OAuth apps for connecting accounts; a adds, d removes"],
227228 ["api", "API", "the forge's endpoints; run a GET"],
229 ["docs", "Docs", "the forge's documentation; Enter reads one"],
228230 ["changelog", "Changelog", "the last 30 releases and their pulls"],
229231 ["releases", "Releases", "the live release and the kept ones"],
230232 ["keeper", "Release keeper", "what holds the next cut, and each release it cut with its probes"],
@@ -314,6 +316,7 @@ test("a member person is offered the signed-in roster without the admin entries"
314316 ["signins", "Signed in", "your browsers and tokens; d signs a browser out"],
315317 ["origin-apps", "Connection apps", "your OAuth apps for connecting accounts; a adds, d removes"],
316318 ["api", "API", "the forge's endpoints; run a GET"],
319 ["docs", "Docs", "the forge's documentation; Enter reads one"],
317320 ["changelog", "Changelog", "the last 30 releases and their pulls"],
318321 ["stalled", "Stalled steps", "pulls whose check is red or was never asked for; enter re-runs"],
319322 ["file-holds", "Held files", "hold new work on a file, release it, or list the holds"],
@@ -370,6 +373,7 @@ test("signed out, the palette offers sign-in and the public entries", () => {
370373 ["settings", "Repository settings", "owner/name"],
371374 ["signin", "Sign in", undefined],
372375 ["api", "API", "the forge's endpoints; run a GET"],
376 ["docs", "Docs", "the forge's documentation; Enter reads one"],
373377 ["changelog", "Changelog", "the last 30 releases and their pulls"],
374378 ["bugs", "Bugs", "open bugs and the bug clock"],
375379 ["audit", "Who can do what", "each app's doors from its contract.json: yes, own or no"],
test/server-timing-sweep.test.ts −1
@@ -47,7 +47,6 @@ const REDIRECTS = new Set([
4747 "/alice/forge/tells",
4848 "/alice/forge/tells/new",
4949 "/big",
50 "/docs",
5150 "/login",
5251 "/openapi.json",
5352 "/settings/origins/callback",

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs