@zebradil/docs-kit 0.0.0-stage → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ import { getCollection } from 'astro:content';
2
+
3
+ const sections = ['Getting started', 'Guides', 'Concepts', 'Reference'];
4
+
5
+ /** Published docs pages in sidebar order, grouped under the sidebar's section labels. */
6
+ export async function docsSections() {
7
+ const pages = (await getCollection('docs', ({ data }) => !data.draft)).sort(
8
+ (a, b) => (a.data.sidebar?.order ?? Infinity) - (b.data.sidebar?.order ?? Infinity) || a.id.localeCompare(b.id),
9
+ );
10
+ const section = ({ id }) => (id.includes('/') ? id.split('/')[0] : 'getting started');
11
+ return sections
12
+ .map((label) => ({ label, pages: pages.filter((page) => section(page) === label.toLowerCase()) }))
13
+ .filter(({ pages }) => pages.length);
14
+ }
15
+
16
+ /** The site's root URL, base path included, with a trailing slash. */
17
+ export const siteRoot = ({ site }) => new URL(import.meta.env.BASE_URL.replace(/\/?$/, '/'), site);
18
+
19
+ // Inline `](target)` and reference-style `[label]: target` link destinations.
20
+ // shortcut: also matches look-alikes inside code blocks; parse with remark if a page trips on it.
21
+ const linkTarget = /(\]\(|^ {0,3}\[[^\]]+\]:[ \t]*)([^)\s]+)/gm;
22
+
23
+ /**
24
+ * A page as Markdown: its title and description from frontmatter, then the source body. Relative links are written
25
+ * against the page's HTML URL (/guides/x/), so they are made absolute to stay valid at /guides/x.md and in llms-full.txt.
26
+ */
27
+ export function pageMarkdown({ id, data: { title, description }, body = '' }, root) {
28
+ const page = new URL(`${id}/`, root);
29
+ const absolute = body.trim().replace(linkTarget, (_, prefix, target) => prefix + new URL(target, page));
30
+ return [`# ${title}`, description && `> ${description}`, absolute].filter(Boolean).join('\n\n') + '\n';
31
+ }
32
+
33
+ /** Serves each docs page's Markdown at its URL with `.md` appended, for agents that read docs. */
34
+ export async function getStaticPaths() {
35
+ return (await docsSections()).flatMap(({ pages }) => pages.map((entry) => ({ params: { slug: entry.id }, props: { entry } })));
36
+ }
37
+
38
+ export function GET(context) {
39
+ return new Response(pageMarkdown(context.props.entry, siteRoot(context)), {
40
+ headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
41
+ });
42
+ }
@@ -0,0 +1,6 @@
1
+ /** Escapes text for HTML strings passed to `set:html` or Starlight's hero slots. */
2
+ export const escape = (text) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
3
+
4
+ // ponytail: only `inline code` is rendered; switch to a Markdown renderer if bodies need links or emphasis.
5
+ /** Escapes text and renders its `inline code` spans. */
6
+ export const inline = (text) => escape(text).replace(/`([^`]+)`/g, '<code>$1</code>');
@@ -0,0 +1,12 @@
1
+ /* Starlight's stock palette and fonts with a teal accent. */
2
+ :root {
3
+ --sl-color-accent-low: hsl(174, 60%, 16%);
4
+ --sl-color-accent: hsl(174, 70%, 40%);
5
+ --sl-color-accent-high: hsl(174, 70%, 82%);
6
+ }
7
+
8
+ :root[data-theme='light'] {
9
+ --sl-color-accent-low: hsl(174, 60%, 88%);
10
+ --sl-color-accent: hsl(174, 80%, 30%);
11
+ --sl-color-accent-high: hsl(174, 80%, 18%);
12
+ }
@@ -0,0 +1,30 @@
1
+ /* Grayscale, monospace everywhere. */
2
+ :root {
3
+ --sl-font: var(--sl-font-system-mono);
4
+ --sl-color-white: hsl(0, 0%, 100%);
5
+ --sl-color-gray-1: hsl(0, 0%, 92%);
6
+ --sl-color-gray-2: hsl(0, 0%, 76%);
7
+ --sl-color-gray-3: hsl(0, 0%, 56%);
8
+ --sl-color-gray-4: hsl(0, 0%, 36%);
9
+ --sl-color-gray-5: hsl(0, 0%, 22%);
10
+ --sl-color-gray-6: hsl(0, 0%, 12%);
11
+ --sl-color-black: hsl(0, 0%, 4%);
12
+ --sl-color-accent-low: hsl(0, 0%, 20%);
13
+ --sl-color-accent: hsl(0, 0%, 90%);
14
+ --sl-color-accent-high: hsl(0, 0%, 100%);
15
+ }
16
+
17
+ :root[data-theme='light'] {
18
+ --sl-color-white: hsl(0, 0%, 0%);
19
+ --sl-color-gray-1: hsl(0, 0%, 10%);
20
+ --sl-color-gray-2: hsl(0, 0%, 22%);
21
+ --sl-color-gray-3: hsl(0, 0%, 36%);
22
+ --sl-color-gray-4: hsl(0, 0%, 56%);
23
+ --sl-color-gray-5: hsl(0, 0%, 78%);
24
+ --sl-color-gray-6: hsl(0, 0%, 93%);
25
+ --sl-color-gray-7: hsl(0, 0%, 97%);
26
+ --sl-color-black: hsl(0, 0%, 100%);
27
+ --sl-color-accent-low: hsl(0, 0%, 88%);
28
+ --sl-color-accent: hsl(0, 0%, 15%);
29
+ --sl-color-accent-high: hsl(0, 0%, 0%);
30
+ }
@@ -0,0 +1,42 @@
1
+ /* Warm oat and roast neutrals with a honey accent; Fraunces headings over Instrument Sans. */
2
+ @import '@fontsource-variable/fraunces';
3
+ @import '@fontsource-variable/instrument-sans';
4
+ @import '@fontsource-variable/jetbrains-mono';
5
+
6
+ :root {
7
+ --sl-font: 'Instrument Sans Variable', system-ui, sans-serif;
8
+ --sl-font-mono: 'JetBrains Mono Variable', ui-monospace, monospace;
9
+ --kit-font-display: 'Fraunces Variable', Georgia, serif;
10
+
11
+ --sl-color-white: #f2eadb;
12
+ --sl-color-gray-1: #e5dac7;
13
+ --sl-color-gray-2: #c9b8a2;
14
+ --sl-color-gray-3: #a8957f;
15
+ --sl-color-gray-4: #6b5a4c;
16
+ --sl-color-gray-5: #3f3128;
17
+ --sl-color-gray-6: #211a15;
18
+ --sl-color-black: #17120e;
19
+ --sl-color-accent-low: #3a2a18;
20
+ --sl-color-accent: #e39a3b;
21
+ --sl-color-accent-high: #f6ce96;
22
+ }
23
+
24
+ :root[data-theme='light'] {
25
+ --sl-color-white: #1e1712;
26
+ --sl-color-gray-1: #2b2119;
27
+ --sl-color-gray-2: #4a3a2e;
28
+ --sl-color-gray-3: #6b5a4c;
29
+ --sl-color-gray-4: #9a8876;
30
+ --sl-color-gray-5: #cdb99a;
31
+ --sl-color-gray-6: #e5dac6;
32
+ --sl-color-gray-7: #efe7d8;
33
+ --sl-color-black: #f7f2e8;
34
+ --sl-color-accent-low: #f4ddbb;
35
+ --sl-color-accent: #8a4b1f;
36
+ --sl-color-accent-high: #4a2a16;
37
+ }
38
+
39
+ :is(h1, h2, h3, .site-title, .card .title) {
40
+ font-family: var(--kit-font-display);
41
+ letter-spacing: -0.015em;
42
+ }
@@ -0,0 +1,49 @@
1
+ /* Cool graphite with a signal-green accent and amber for "down"; Schibsted Grotesk over IBM Plex Mono. */
2
+ @import '@fontsource-variable/schibsted-grotesk';
3
+ @import '@fontsource/ibm-plex-mono/400.css';
4
+ @import '@fontsource/ibm-plex-mono/500.css';
5
+
6
+ :root {
7
+ --sl-font: 'Schibsted Grotesk Variable', system-ui, sans-serif;
8
+ --sl-font-mono: 'IBM Plex Mono', ui-monospace, monospace;
9
+ --kit-font-display: 'Schibsted Grotesk Variable', system-ui, sans-serif;
10
+
11
+ --sl-color-white: #eef3f1;
12
+ --sl-color-gray-1: #d5dedf;
13
+ --sl-color-gray-2: #aebbbf;
14
+ --sl-color-gray-3: #7d8d94;
15
+ --sl-color-gray-4: #4a5a62;
16
+ --sl-color-gray-5: #263137;
17
+ --sl-color-gray-6: #151c20;
18
+ --sl-color-black: #0d1215;
19
+ --sl-color-accent-low: #12301f;
20
+ --sl-color-accent: #5fd39a;
21
+ --sl-color-accent-high: #c4f2d6;
22
+ --sl-color-orange-low: #3a2a12;
23
+ --sl-color-orange: #c98a3a;
24
+ --sl-color-orange-high: #e0a85a;
25
+ }
26
+
27
+ :root[data-theme='light'] {
28
+ --sl-color-white: #10181a;
29
+ --sl-color-gray-1: #222b2c;
30
+ --sl-color-gray-2: #3a4645;
31
+ --sl-color-gray-3: #5d6b69;
32
+ --sl-color-gray-4: #8a9894;
33
+ --sl-color-gray-5: #b9c4c0;
34
+ --sl-color-gray-6: #e4e9e5;
35
+ --sl-color-gray-7: #eef1ee;
36
+ --sl-color-black: #f4f6f3;
37
+ --sl-color-accent-low: #cfeedd;
38
+ --sl-color-accent: #17714a;
39
+ --sl-color-accent-high: #0b4029;
40
+ --sl-color-orange-low: #f5e2c4;
41
+ --sl-color-orange: #b07a2a;
42
+ --sl-color-orange-high: #6e4a0e;
43
+ }
44
+
45
+ :is(h1, h2, h3, .site-title, .card .title) {
46
+ font-family: var(--kit-font-display);
47
+ font-weight: 800;
48
+ letter-spacing: -0.03em;
49
+ }
@@ -0,0 +1,45 @@
1
+ /* Plum-tinted neutrals with a coral accent; Outfit SemiBold headings over the system sans. */
2
+ @import '@fontsource/outfit/600.css';
3
+
4
+ :root {
5
+ --kit-font-display: 'Outfit', system-ui, sans-serif;
6
+
7
+ --sl-color-white: #f7f4fc;
8
+ --sl-color-gray-1: #e9e4f2;
9
+ --sl-color-gray-2: #c9c1d8;
10
+ --sl-color-gray-3: #a197b5;
11
+ --sl-color-gray-4: #6a5f80;
12
+ --sl-color-gray-5: #382c4d;
13
+ --sl-color-gray-6: #1c1030;
14
+ --sl-color-black: #120a1f;
15
+ --sl-color-accent-low: #4a1a22;
16
+ --sl-color-accent: #ff5a4e;
17
+ --sl-color-accent-high: #ffc9c4;
18
+ }
19
+
20
+ :root[data-theme='light'] {
21
+ --sl-color-white: #1c1030;
22
+ --sl-color-gray-1: #2a1f3d;
23
+ --sl-color-gray-2: #3d3250;
24
+ --sl-color-gray-3: #5c5270;
25
+ --sl-color-gray-4: #8c839c;
26
+ --sl-color-gray-5: #cbc4d6;
27
+ --sl-color-gray-6: #ece8f2;
28
+ --sl-color-gray-7: #f6f4f9;
29
+ --sl-color-black: #ffffff;
30
+ --sl-color-accent-low: #ffe1dd;
31
+ --sl-color-accent: #c22f25; /* 4.5:1 or more as text on every light background, white and tints alike */
32
+ --sl-color-accent-high: #7a1a14;
33
+ }
34
+
35
+ :is(h1, h2, h3, .site-title, .card .title) {
36
+ font-family: var(--kit-font-display);
37
+ font-weight: 600;
38
+ letter-spacing: -0.02em;
39
+ }
40
+
41
+ /* The wordmark: white on dark, plum on light, as in the logo lockups. */
42
+ .site-title {
43
+ color: var(--sl-color-white);
44
+ letter-spacing: -0.035em;
45
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ // Walks `<bin> [sub...] --help` recursively and writes one Starlight Markdown page per command.
3
+ // Usage: node refgen/help2md.mjs --bin <path-or-name> [--out <dir>]
4
+ import { execFileSync } from 'node:child_process';
5
+ import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { basename, join } from 'node:path';
7
+ import { parseArgs } from 'node:util';
8
+
9
+ // Fixed width and no colors so output is identical on every machine and CI run. Only PATH is inherited:
10
+ // clap's `env` feature prints each variable's current value in --help (`[env: KASHA_TOKEN=secret]`),
11
+ // which would put local secrets into committed pages and make them differ from CI's.
12
+ const ENV = { PATH: process.env.PATH, COLUMNS: '100', NO_COLOR: '1', CLICOLOR: '0', TERM: 'dumb' };
13
+ const MAX_DEPTH = 8;
14
+
15
+ const help = (bin, path) =>
16
+ execFileSync(bin, [...path, '--help'], { env: ENV, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
17
+ .split('\n').map((l) => l.trimEnd()).join('\n').trim();
18
+
19
+ // Subcommand names from clap ("Commands:", clap 3 "SUBCOMMANDS:") and cobra ("Available Commands:",
20
+ // "Additional Commands:", command-group titles) sections. Entries sit at a 2-space indent; deeper
21
+ // lines are wrapped descriptions.
22
+ function subcommands(text) {
23
+ const names = [];
24
+ let inSection = false;
25
+ for (const line of text.split('\n')) {
26
+ if (/^(\w[\w ]*)?commands:$/i.test(line)) inSection = true;
27
+ else if (!line.trim() || /^\S/.test(line)) inSection = false;
28
+ else if (inSection) {
29
+ const m = /^ {2}([\w][\w.-]*)(?:\s+(.*))?$/.exec(line);
30
+ if (m && !skip(m[1], m[2] ?? '')) names.push(m[1]);
31
+ }
32
+ }
33
+ return names;
34
+ }
35
+
36
+ // `help` only repeats other pages. Cobra's default `completion` is identical boilerplate in every
37
+ // cobra CLI, so it is recognised by its stock description; a project's own `completion` stays.
38
+ const skip = (name, desc) =>
39
+ name === 'help' || (name === 'completion' && desc.startsWith('Generate the autocompletion script'));
40
+
41
+ function page(title, text) {
42
+ const description = text.split('\n')[0];
43
+ const fence = '`'.repeat(Math.max(3, ...(text.match(/`+/g) ?? []).map((s) => s.length + 1)));
44
+ return [
45
+ '---',
46
+ `title: ${JSON.stringify(title)}`,
47
+ ...(description && !/^usage:/i.test(description) ? [`description: ${JSON.stringify(description)}`] : []),
48
+ '---',
49
+ '',
50
+ '<!-- Generated by docs-kit refgen/help2md from --help output. Do not edit. -->',
51
+ '',
52
+ `${fence}text`,
53
+ text,
54
+ fence,
55
+ '',
56
+ ].join('\n');
57
+ }
58
+
59
+ function generate(bin, out) {
60
+ const root = basename(bin).toLowerCase();
61
+ mkdirSync(out, { recursive: true });
62
+ // Drop pages of commands that no longer exist so the "reference up to date" check sees removals.
63
+ // ponytail: prefix match also hits a sibling binary's pages (`kasha` vs `kasha-server`) in the same
64
+ // --out; give each binary its own --out if that ever happens.
65
+ for (const f of readdirSync(out)) {
66
+ if (f === `${root}.md` || (f.startsWith(`${root}-`) && f.endsWith('.md'))) rmSync(join(out, f));
67
+ }
68
+ const walk = (path) => {
69
+ if (path.length > MAX_DEPTH) throw new Error(`command tree deeper than ${MAX_DEPTH}: ${path.join(' ')}`);
70
+ const text = help(bin, path);
71
+ const file = [root, ...path].join('-').toLowerCase() + '.md';
72
+ writeFileSync(join(out, file), page([root, ...path].join(' '), text));
73
+ for (const sub of subcommands(text)) walk([...path, sub]);
74
+ };
75
+ walk([]);
76
+ }
77
+
78
+ const { values } = parseArgs({
79
+ options: { bin: { type: 'string' }, out: { type: 'string', default: 'docs/src/content/docs/reference/cli' } },
80
+ });
81
+ if (!values.bin) {
82
+ console.error('usage: help2md.mjs --bin <path-or-name> [--out <dir>]');
83
+ process.exit(2);
84
+ }
85
+ generate(values.bin, values.out);