@abstractdata/starlight-theme 0.3.1 → 0.3.3
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.
- package/README.md +7 -2
- package/bin/install-skills.js +251 -0
- package/package.json +12 -6
- package/scripts/build-python-docs.mjs +385 -0
- package/scripts/build-ts-docs.mjs +349 -0
- package/scripts/python-autodoc.json +10 -0
- package/scripts/ts-autodoc.json +10 -0
- package/skills/claude/CLAUDE.md +46 -0
- package/skills/claude/abstract-data-docs-author/SKILL.md +305 -0
- package/skills/claude/abstract-data-setup/SKILL.md +555 -0
- package/skills/cursor/abstract-data-docs-author.mdc +311 -0
- package/skills/cursor/abstract-data-setup.mdc +561 -0
- package/skills/cursor/welcome.mdc +29 -0
- package/skills/github/copilot-instructions.md +893 -0
- package/src/components/SocialIcons.astro +17 -2
- package/src/components/VersionPicker.astro +238 -0
- package/src/index.ts +17 -1
- package/src/styles/hud.css +222 -210
- package/src/styles/theme.css +444 -432
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* build-python-docs.mjs
|
|
4
|
+
*
|
|
5
|
+
* Orchestrates Python autodoc generation for this Starlight site.
|
|
6
|
+
*
|
|
7
|
+
* Reads `scripts/python-autodoc.json` for configuration:
|
|
8
|
+
* - searchPath: relative path to your Python source root (parent of the package directory)
|
|
9
|
+
* - modules: list of fully-qualified module names to document
|
|
10
|
+
* - outputDir: where the generated .md files land (relative to project root)
|
|
11
|
+
* - repoUrl: (optional) base URL for "View source on GitHub" links
|
|
12
|
+
* - repoBranch: (optional) default 'main'
|
|
13
|
+
* - versions: (optional) array of { tag, label, default } for versioned API docs
|
|
14
|
+
* — when present, the script does one build per tag via git worktrees,
|
|
15
|
+
* emitting into <outputDir>/<safeTag>/. The default version's pages
|
|
16
|
+
* ALSO emit at <outputDir>/<page>.md (the un-versioned URL) so existing
|
|
17
|
+
* links keep working.
|
|
18
|
+
*
|
|
19
|
+
* For each module, in each version:
|
|
20
|
+
* 1. Invokes `pydoc-markdown -I <searchPath> -m <module>` to capture markdown.
|
|
21
|
+
* 2. Lifts the first H1 into Starlight `title:` frontmatter, synthesizes a
|
|
22
|
+
* `description:` from the first paragraph, injects `version: <tag>` if versioned.
|
|
23
|
+
* 3. Post-processes thin pages (auto-generates Submodules section on package landings,
|
|
24
|
+
* injects a `:::note` banner on truly-empty pages).
|
|
25
|
+
*
|
|
26
|
+
* Run via:
|
|
27
|
+
* bun run docs:python
|
|
28
|
+
*
|
|
29
|
+
* Requires Python ≥ 3.9 and pydoc-markdown:
|
|
30
|
+
* pipx install pydoc-markdown
|
|
31
|
+
*/
|
|
32
|
+
import { execSync } from 'node:child_process';
|
|
33
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, mkdtempSync } from 'node:fs';
|
|
34
|
+
import { join, resolve, dirname, relative } from 'node:path';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
import { tmpdir } from 'node:os';
|
|
37
|
+
|
|
38
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
40
|
+
const CONFIG_PATH = resolve(__dirname, 'python-autodoc.json');
|
|
41
|
+
|
|
42
|
+
const c = {
|
|
43
|
+
reset: '\x1b[0m', dim: '\x1b[2m', cyan: '\x1b[36m', gold: '\x1b[33m',
|
|
44
|
+
red: '\x1b[31m', green: '\x1b[32m',
|
|
45
|
+
};
|
|
46
|
+
const log = (...a) => console.log(...a);
|
|
47
|
+
const die = (msg) => { console.error(`${c.red}error${c.reset} ${msg}`); process.exit(1); };
|
|
48
|
+
|
|
49
|
+
// ─── Load config ──────────────────────────────────────────────────────
|
|
50
|
+
if (!existsSync(CONFIG_PATH)) die(`Missing config: ${CONFIG_PATH}`);
|
|
51
|
+
const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
52
|
+
if (!cfg.searchPath) die('python-autodoc.json: `searchPath` is required.');
|
|
53
|
+
if (!Array.isArray(cfg.modules) || cfg.modules.length === 0) {
|
|
54
|
+
die('python-autodoc.json: `modules` must be a non-empty array.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ORIGINAL_SEARCH_PATH = resolve(PROJECT_ROOT, cfg.searchPath);
|
|
58
|
+
const outputDir = resolve(PROJECT_ROOT, cfg.outputDir ?? 'src/content/docs/api');
|
|
59
|
+
|
|
60
|
+
if (!existsSync(ORIGINAL_SEARCH_PATH)) {
|
|
61
|
+
die(`searchPath does not exist: ${ORIGINAL_SEARCH_PATH}\n Resolved from cfg.searchPath = "${cfg.searchPath}"`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Verify pydoc-markdown is available ───────────────────────────────
|
|
65
|
+
log(`${c.dim}→ checking pydoc-markdown${c.reset}`);
|
|
66
|
+
try {
|
|
67
|
+
execSync('pydoc-markdown --version', { stdio: 'ignore' });
|
|
68
|
+
} catch {
|
|
69
|
+
die(`pydoc-markdown not found on PATH. Install it:
|
|
70
|
+
pipx install pydoc-markdown
|
|
71
|
+
# or
|
|
72
|
+
pip install --user pydoc-markdown
|
|
73
|
+
Then re-run this script.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Versioning setup ────────────────────────────────────────────────
|
|
77
|
+
//
|
|
78
|
+
// If `versions` is configured, each entry triggers an independent build
|
|
79
|
+
// from a `git worktree` checkout of the source repo at that tag. We need
|
|
80
|
+
// to know:
|
|
81
|
+
// - the source repo root (where `.git` lives) so we can `git -C` it
|
|
82
|
+
// - the relative path from source-repo-root to the original searchPath,
|
|
83
|
+
// so we can map it to the equivalent path inside each worktree
|
|
84
|
+
//
|
|
85
|
+
// We walk up from ORIGINAL_SEARCH_PATH looking for `.git`; bail out if we
|
|
86
|
+
// hit the filesystem root without finding it.
|
|
87
|
+
function findGitRoot(start) {
|
|
88
|
+
let dir = start;
|
|
89
|
+
while (true) {
|
|
90
|
+
if (existsSync(join(dir, '.git'))) return dir;
|
|
91
|
+
const parent = dirname(dir);
|
|
92
|
+
if (parent === dir) return null;
|
|
93
|
+
dir = parent;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const versions = Array.isArray(cfg.versions) ? cfg.versions : null;
|
|
98
|
+
let SOURCE_REPO_ROOT = null;
|
|
99
|
+
let SEARCH_PATH_REL = null;
|
|
100
|
+
if (versions) {
|
|
101
|
+
if (versions.length === 0) die('`versions` is an empty array — set it to null/omit, or list at least one version.');
|
|
102
|
+
if (!versions.some((v) => v.tag)) die('`versions[].tag` is required on every entry.');
|
|
103
|
+
if (versions.filter((v) => v.default).length > 1) die('Only one `versions[].default: true` allowed.');
|
|
104
|
+
if (!versions.some((v) => v.default)) {
|
|
105
|
+
log(`${c.gold}warn${c.reset} no version marked default; treating the first one (${versions[0].tag}) as default`);
|
|
106
|
+
versions[0].default = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
SOURCE_REPO_ROOT = findGitRoot(ORIGINAL_SEARCH_PATH);
|
|
110
|
+
if (!SOURCE_REPO_ROOT) {
|
|
111
|
+
die(`versions[] is configured but no .git directory was found above ${ORIGINAL_SEARCH_PATH}.\n Versioned builds require the source to be a git checkout.`);
|
|
112
|
+
}
|
|
113
|
+
SEARCH_PATH_REL = relative(SOURCE_REPO_ROOT, ORIGINAL_SEARCH_PATH);
|
|
114
|
+
log(`${c.dim}→ source repo: ${SOURCE_REPO_ROOT}${c.reset}`);
|
|
115
|
+
log(`${c.dim}→ relative searchPath: ${SEARCH_PATH_REL || '(repo root)'}${c.reset}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Make a tag filesystem-safe for use as a directory name. We have to be
|
|
119
|
+
// strict here: Astro's slug normalizer strips dots from URL segments
|
|
120
|
+
// (`0.1.0` → `010`), so if our directory names contain dots the URL the
|
|
121
|
+
// VersionPicker constructs won't match the rendered URL. Convert dots
|
|
122
|
+
// to dashes (and any other non-alphanumeric to dashes) so the directory
|
|
123
|
+
// name AND the URL slug Astro generates from it stay byte-identical.
|
|
124
|
+
// v0.3.0 → 0-3-0
|
|
125
|
+
// v1.0.0-rc.1 → 1-0-0-rc-1
|
|
126
|
+
function safeTag(tag) {
|
|
127
|
+
return tag.replace(/^v/, '').replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Per-build pipeline (one invocation per version, or one total) ─────
|
|
131
|
+
function buildOnce({ searchPath, version }) {
|
|
132
|
+
// version may be null (single-version mode) or { tag, label, default }
|
|
133
|
+
const versionDir = version ? join(outputDir, safeTag(version.tag)) : outputDir;
|
|
134
|
+
mkdirSync(versionDir, { recursive: true });
|
|
135
|
+
|
|
136
|
+
const tagPrefix = version
|
|
137
|
+
? `${c.cyan}[${version.label ?? version.tag}]${c.reset} `
|
|
138
|
+
: '';
|
|
139
|
+
log(`${c.dim}→ ${tagPrefix}generating ${cfg.modules.length} module page${cfg.modules.length === 1 ? '' : 's'}${c.reset}`);
|
|
140
|
+
|
|
141
|
+
// Two-pass build: first collect every page in memory so the thin-page
|
|
142
|
+
// post-processor can cross-reference siblings (for "Submodules" sections
|
|
143
|
+
// on package landing pages), then write everything to disk.
|
|
144
|
+
const pages = [];
|
|
145
|
+
|
|
146
|
+
for (const mod of cfg.modules) {
|
|
147
|
+
const safeName = mod.replace(/\./g, '_');
|
|
148
|
+
const outPath = join(versionDir, `${safeName}.md`);
|
|
149
|
+
|
|
150
|
+
let markdown;
|
|
151
|
+
try {
|
|
152
|
+
markdown = execSync(
|
|
153
|
+
`pydoc-markdown -I "${searchPath}" -m ${mod}`,
|
|
154
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] },
|
|
155
|
+
);
|
|
156
|
+
} catch {
|
|
157
|
+
log(`${c.red} ✗ ${tagPrefix}${mod}${c.reset}`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Frontmatter post-process ────────────────────────────────
|
|
162
|
+
const h1 = markdown.match(/^# (.+?)$/m);
|
|
163
|
+
const title = (h1?.[1] ?? mod).trim().replace(/\\_/g, '_');
|
|
164
|
+
let body = h1 ? markdown.replace(h1[0] + '\n', '') : markdown;
|
|
165
|
+
body = body.replace(/<a id="[^"]*"><\/a>\n?/g, '');
|
|
166
|
+
body = body.replace(
|
|
167
|
+
/:(?:mod|class|func|obj|attr|meth|exc|any|data|const)(?::)?\s*`([^`]+)`/g,
|
|
168
|
+
'`$1`',
|
|
169
|
+
);
|
|
170
|
+
const desc = body.split('\n').find((l) => {
|
|
171
|
+
const t = l.trim();
|
|
172
|
+
if (!t) return false;
|
|
173
|
+
if (/^#{1,6} /.test(t)) return false;
|
|
174
|
+
if (t.startsWith('```')) return false;
|
|
175
|
+
if (t.startsWith('|')) return false;
|
|
176
|
+
if (/^[-*+] /.test(t)) return false;
|
|
177
|
+
if (/^<[^>]+>/.test(t)) return false;
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
180
|
+
const description = (desc ?? `API reference for \`${mod}\`.`)
|
|
181
|
+
.trim().replace(/`/g, '').replace(/"/g, "'").slice(0, 160);
|
|
182
|
+
|
|
183
|
+
const fmLines = ['---', `title: ${title}`, `description: "${description}"`];
|
|
184
|
+
if (version) {
|
|
185
|
+
// Emit version metadata. `versionDefault: true` lets the bundled
|
|
186
|
+
// <VersionPicker> auto-discover which version to pre-select without
|
|
187
|
+
// duplicating the canonical list outside the autodoc JSON config.
|
|
188
|
+
fmLines.push(`version: "${version.tag}"`);
|
|
189
|
+
if (version.label) fmLines.push(`versionLabel: "${version.label}"`);
|
|
190
|
+
if (version.default) fmLines.push(`versionDefault: true`);
|
|
191
|
+
}
|
|
192
|
+
fmLines.push('---', '');
|
|
193
|
+
const frontmatter = fmLines.join('\n');
|
|
194
|
+
|
|
195
|
+
pages.push({ mod, safeName, outPath, title, description, body, frontmatter });
|
|
196
|
+
log(`${c.green} ✓${c.reset} ${tagPrefix}${mod} ${c.dim}→ ${relative(PROJECT_ROOT, outPath)}${c.reset}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (pages.length === 0) {
|
|
200
|
+
log(`${c.red} no pages generated for ${version ? version.tag : 'single build'} — skipping.${c.reset}`);
|
|
201
|
+
return { pages: [] };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Thin-page post-processor ──────────────────────────────────────
|
|
205
|
+
const childrenOf = (mod) => pages.filter((p) => p.mod !== mod && p.mod.startsWith(mod + '.'));
|
|
206
|
+
let enriched = 0;
|
|
207
|
+
let bannered = 0;
|
|
208
|
+
|
|
209
|
+
for (const page of pages) {
|
|
210
|
+
const proseLines = page.body.split('\n').filter((l) => {
|
|
211
|
+
const t = l.trim();
|
|
212
|
+
if (!t) return false;
|
|
213
|
+
if (/^#{1,6} /.test(t)) return false;
|
|
214
|
+
if (t.startsWith('```')) return false;
|
|
215
|
+
if (t.startsWith('|') || /^[-=]{3,}/.test(t)) return false;
|
|
216
|
+
if (/^[-*+] /.test(t)) return false;
|
|
217
|
+
if (/^<[^>]+>/.test(t)) return false;
|
|
218
|
+
return true;
|
|
219
|
+
}).length;
|
|
220
|
+
const bodyChars = page.body.replace(/\s+/g, '').length;
|
|
221
|
+
const isThin = bodyChars < 150 && proseLines < 1;
|
|
222
|
+
|
|
223
|
+
const kids = childrenOf(page.mod);
|
|
224
|
+
const isPackageLanding = kids.length > 0;
|
|
225
|
+
|
|
226
|
+
let newBody = page.body;
|
|
227
|
+
let touched = false;
|
|
228
|
+
|
|
229
|
+
// Stale-version banner: non-default versions get a "Latest is X →"
|
|
230
|
+
// pointer at the top of every page. Lives above the thin/package
|
|
231
|
+
// banners since version drift is the higher-priority signal.
|
|
232
|
+
if (version && !version.default) {
|
|
233
|
+
const defaultVersion = (cfg.versions ?? []).find((v) => v.default);
|
|
234
|
+
const latestLabel = defaultVersion ? (defaultVersion.label ?? defaultVersion.tag) : 'latest';
|
|
235
|
+
const latestPath = defaultVersion
|
|
236
|
+
? `/${cfg.outputDir.replace(/^src\/content\/docs\/?/, '').replace(/\/$/, '')}/${safeTag(defaultVersion.tag)}/${page.safeName}/`
|
|
237
|
+
: null;
|
|
238
|
+
const link = latestPath ? `[${latestLabel} →](${latestPath})` : latestLabel;
|
|
239
|
+
const stale = [
|
|
240
|
+
'',
|
|
241
|
+
`:::caution[Older version]`,
|
|
242
|
+
`You're viewing **${version.label ?? version.tag}**. Latest is ${link}.`,
|
|
243
|
+
':::',
|
|
244
|
+
'',
|
|
245
|
+
].join('\n');
|
|
246
|
+
newBody = stale + newBody;
|
|
247
|
+
touched = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (isThin && !isPackageLanding) {
|
|
251
|
+
const noteBlock = [
|
|
252
|
+
'',
|
|
253
|
+
':::note[This page is sparse]',
|
|
254
|
+
`The auto-generated reference for \`${page.mod}\` is short. Expanding the source docstring at the top of \`${page.mod.replace(/\./g, '/')}.py\` (a sentence about purpose, when to use it, and a tiny example) would populate this page with real context.`,
|
|
255
|
+
':::',
|
|
256
|
+
'',
|
|
257
|
+
].join('\n');
|
|
258
|
+
newBody = noteBlock + newBody;
|
|
259
|
+
bannered += 1;
|
|
260
|
+
touched = true;
|
|
261
|
+
log(`${c.gold} ⚠${c.reset} ${tagPrefix}thin-page banner on ${page.mod}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (isPackageLanding) {
|
|
265
|
+
const lines = ['', '## Submodules', ''];
|
|
266
|
+
for (const kid of kids) {
|
|
267
|
+
const summary = kid.description && !kid.description.startsWith('API reference for')
|
|
268
|
+
? ` — ${kid.description}`
|
|
269
|
+
: '';
|
|
270
|
+
lines.push(`- [\`${kid.mod}\`](./${kid.safeName}.md)${summary}`);
|
|
271
|
+
}
|
|
272
|
+
lines.push('');
|
|
273
|
+
const submodulesSection = lines.join('\n');
|
|
274
|
+
|
|
275
|
+
if (isThin) {
|
|
276
|
+
// Replace stub body with brief intro + submodules. If we already
|
|
277
|
+
// prepended a stale-version banner, preserve it at the very top.
|
|
278
|
+
const stalePrefix = newBody.startsWith('\n:::caution[Older version]')
|
|
279
|
+
? newBody.slice(0, newBody.indexOf(':::\n', 1) + 4) + '\n'
|
|
280
|
+
: '';
|
|
281
|
+
newBody = stalePrefix +
|
|
282
|
+
`\nTop-level package — see submodules below for the documented API surface.\n${submodulesSection}`;
|
|
283
|
+
} else {
|
|
284
|
+
newBody = newBody.replace(/\s+$/, '') + '\n' + submodulesSection;
|
|
285
|
+
}
|
|
286
|
+
enriched += 1;
|
|
287
|
+
touched = true;
|
|
288
|
+
log(`${c.green} ✓${c.reset} added Submodules section to ${page.mod} (${kids.length} child${kids.length === 1 ? '' : 'ren'})`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (touched && cfg.repoUrl) {
|
|
292
|
+
const branch = version ? version.tag : (cfg.repoBranch ?? 'main');
|
|
293
|
+
const repo = cfg.repoUrl.replace(/\/$/, '');
|
|
294
|
+
const sourcePath = page.mod.replace(/\./g, '/');
|
|
295
|
+
const target = isPackageLanding
|
|
296
|
+
? `${sourcePath}/__init__.py`
|
|
297
|
+
: `${sourcePath}.py`;
|
|
298
|
+
newBody = newBody.replace(/\s+$/, '') +
|
|
299
|
+
`\n\n## See also\n\n- [View source on GitHub](${repo}/blob/${branch}/${target})\n`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
writeFileSync(page.outPath, page.frontmatter + newBody);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
log('');
|
|
306
|
+
log(`${c.green}✓${c.reset} ${tagPrefix}Generated ${c.gold}${pages.length}${c.reset} page${pages.length === 1 ? '' : 's'} in ${c.cyan}${relative(PROJECT_ROOT, versionDir)}${c.reset}/`);
|
|
307
|
+
if (enriched || bannered) {
|
|
308
|
+
log(`${c.dim} ${enriched} package landing${enriched === 1 ? '' : 's'} enriched, ${bannered} thin page${bannered === 1 ? '' : 's'} flagged${c.reset}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { pages };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── Run: single-build or per-version with worktrees ──────────────────
|
|
315
|
+
const createdWorktrees = [];
|
|
316
|
+
|
|
317
|
+
function cleanup() {
|
|
318
|
+
for (const wt of createdWorktrees) {
|
|
319
|
+
try {
|
|
320
|
+
execSync(`git -C "${SOURCE_REPO_ROOT}" worktree remove --force "${wt}"`,
|
|
321
|
+
{ stdio: 'ignore' });
|
|
322
|
+
} catch {
|
|
323
|
+
// best-effort; if remove failed, rm -rf the directory
|
|
324
|
+
try { rmSync(wt, { recursive: true, force: true }); } catch {}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
process.on('exit', cleanup);
|
|
330
|
+
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
if (!versions) {
|
|
334
|
+
// Single-version build (existing behavior)
|
|
335
|
+
buildOnce({ searchPath: ORIGINAL_SEARCH_PATH, version: null });
|
|
336
|
+
} else {
|
|
337
|
+
// Per-version builds via git worktrees
|
|
338
|
+
for (const v of versions) {
|
|
339
|
+
const wt = mkdtempSync(join(tmpdir(), `autodoc-${safeTag(v.tag)}-`));
|
|
340
|
+
createdWorktrees.push(wt);
|
|
341
|
+
log(`${c.dim}→ git worktree add ${wt} ${v.tag}${c.reset}`);
|
|
342
|
+
try {
|
|
343
|
+
execSync(`git -C "${SOURCE_REPO_ROOT}" worktree add --detach "${wt}" "${v.tag}"`,
|
|
344
|
+
{ stdio: 'inherit' });
|
|
345
|
+
} catch {
|
|
346
|
+
die(`Failed to create git worktree for ${v.tag}.\n Verify the tag exists in ${SOURCE_REPO_ROOT}: git tag --list "${v.tag}"`);
|
|
347
|
+
}
|
|
348
|
+
const wtSearchPath = SEARCH_PATH_REL ? join(wt, SEARCH_PATH_REL) : wt;
|
|
349
|
+
if (!existsSync(wtSearchPath)) {
|
|
350
|
+
log(`${c.gold}warn${c.reset} ${v.tag}: searchPath ${wtSearchPath} doesn't exist (the directory layout may have changed). Skipping.`);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
buildOnce({ searchPath: wtSearchPath, version: v });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Also emit the default version at the un-versioned path so existing
|
|
357
|
+
// links to /api/foo/ keep resolving without a redirect step.
|
|
358
|
+
const defaultV = versions.find((v) => v.default);
|
|
359
|
+
if (defaultV) {
|
|
360
|
+
log(`${c.dim}→ aliasing ${defaultV.tag} as the default (un-versioned) build${c.reset}`);
|
|
361
|
+
const wt = mkdtempSync(join(tmpdir(), `autodoc-default-`));
|
|
362
|
+
createdWorktrees.push(wt);
|
|
363
|
+
try {
|
|
364
|
+
execSync(`git -C "${SOURCE_REPO_ROOT}" worktree add --detach "${wt}" "${defaultV.tag}"`,
|
|
365
|
+
{ stdio: 'ignore' });
|
|
366
|
+
const wtSearchPath = SEARCH_PATH_REL ? join(wt, SEARCH_PATH_REL) : wt;
|
|
367
|
+
// Stash original outputDir, point at root for un-versioned emit
|
|
368
|
+
buildOnce({ searchPath: wtSearchPath, version: null });
|
|
369
|
+
} catch (err) {
|
|
370
|
+
log(`${c.gold}warn${c.reset} default-alias build failed: ${err.message}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} finally {
|
|
375
|
+
cleanup();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
log('');
|
|
379
|
+
log(`${c.dim}Sidebar wiring (astro.config.mjs):${c.reset}`);
|
|
380
|
+
log(`${c.dim} { label: 'API Reference', autogenerate: { directory: '${cfg.outputDir.replace(/^src\/content\/docs\/?/, '')}' } }${c.reset}`);
|
|
381
|
+
if (versions) {
|
|
382
|
+
log(`${c.dim} → with ${versions.length} version${versions.length === 1 ? '' : 's'}, the sidebar will auto-group by version subdirectory.${c.reset}`);
|
|
383
|
+
log(`${c.dim} → import VersionPicker: import { VersionPicker } from '@abstractdata/starlight-theme/components';${c.reset}`);
|
|
384
|
+
}
|
|
385
|
+
log('');
|