create-zudo-doc 3.2.0 → 3.3.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.
Files changed (122) hide show
  1. package/dist/claude-md-gen.d.ts +8 -0
  2. package/dist/claude-md-gen.js +43 -26
  3. package/dist/compose.d.ts +14 -20
  4. package/dist/compose.js +15 -25
  5. package/dist/features/body-foot-util.d.ts +8 -4
  6. package/dist/features/body-foot-util.js +8 -4
  7. package/dist/features/claude-resources.d.ts +12 -0
  8. package/dist/features/claude-resources.js +13 -6
  9. package/dist/features/design-token-panel.d.ts +0 -12
  10. package/dist/features/design-token-panel.js +32 -93
  11. package/dist/features/doc-history.d.ts +18 -3
  12. package/dist/features/doc-history.js +45 -60
  13. package/dist/features/doc-tags.d.ts +9 -13
  14. package/dist/features/doc-tags.js +10 -26
  15. package/dist/features/dynamic-page-transition.d.ts +19 -30
  16. package/dist/features/dynamic-page-transition.js +21 -209
  17. package/dist/features/footer-taglist.d.ts +1 -1
  18. package/dist/features/footer-taglist.js +1 -1
  19. package/dist/features/footer.d.ts +3 -2
  20. package/dist/features/footer.js +3 -2
  21. package/dist/features/i18n.d.ts +13 -8
  22. package/dist/features/i18n.js +14 -9
  23. package/dist/features/image-enlarge.d.ts +7 -26
  24. package/dist/features/image-enlarge.js +7 -26
  25. package/dist/features/llms-txt.d.ts +3 -5
  26. package/dist/features/llms-txt.js +3 -5
  27. package/dist/features/search.d.ts +7 -3
  28. package/dist/features/search.js +7 -3
  29. package/dist/features/sidebar-resizer.d.ts +4 -8
  30. package/dist/features/sidebar-resizer.js +4 -8
  31. package/dist/features/sidebar-toggle.d.ts +7 -7
  32. package/dist/features/sidebar-toggle.js +7 -7
  33. package/dist/features/tag-governance.d.ts +14 -7
  34. package/dist/features/tag-governance.js +52 -8
  35. package/dist/features/tauri.d.ts +13 -10
  36. package/dist/features/tauri.js +16 -52
  37. package/dist/features/versioning.d.ts +12 -24
  38. package/dist/features/versioning.js +13 -39
  39. package/dist/index.js +2 -3
  40. package/dist/scaffold.d.ts +13 -6
  41. package/dist/scaffold.js +64 -45
  42. package/dist/utils.d.ts +10 -0
  43. package/dist/utils.js +14 -0
  44. package/dist/zfb-config-gen.d.ts +32 -20
  45. package/dist/zfb-config-gen.js +400 -53
  46. package/package.json +2 -2
  47. package/templates/base/pages/docs/[[...slug]].tsx +59 -0
  48. package/templates/base/pages/index.tsx +6 -41
  49. package/templates/base/src/styles/global.css +15 -340
  50. package/templates/base/tsconfig.json +3 -26
  51. package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +88 -0
  52. package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +3 -1
  53. package/dist/settings-gen.d.ts +0 -2
  54. package/dist/settings-gen.js +0 -320
  55. package/templates/base/.htmlvalidate.json +0 -5
  56. package/templates/base/.zfb/doc-history-meta.json +0 -1
  57. package/templates/base/pages/_data.ts +0 -160
  58. package/templates/base/pages/lib/_body-end-islands.tsx +0 -165
  59. package/templates/base/pages/lib/_chrome.ts +0 -167
  60. package/templates/base/pages/lib/_details.tsx +0 -29
  61. package/templates/base/pages/lib/_doc-route-entries.ts +0 -10
  62. package/templates/base/pages/lib/_extract-headings.ts +0 -37
  63. package/templates/base/pages/lib/_frontmatter-preview-data.ts +0 -27
  64. package/templates/base/pages/lib/_nav-source-cache.ts +0 -100
  65. package/templates/base/pages/lib/_nav-source-docs.ts +0 -17
  66. package/templates/base/pages/lib/_preset-generator.tsx +0 -81
  67. package/templates/base/pages/lib/_route-context.ts +0 -32
  68. package/templates/base/pages/lib/_search-widget.tsx +0 -17
  69. package/templates/base/pages/lib/doc-page-props.ts +0 -30
  70. package/templates/base/pages/lib/locale-merge.ts +0 -59
  71. package/templates/base/scripts/run-b4push.sh +0 -102
  72. package/templates/base/src/components/ai-chat-modal.tsx +0 -18
  73. package/templates/base/src/components/content/code-group.tsx +0 -3
  74. package/templates/base/src/components/content/content-admonition.tsx +0 -4
  75. package/templates/base/src/components/desktop-sidebar-toggle.tsx +0 -15
  76. package/templates/base/src/components/doc-history.tsx +0 -21
  77. package/templates/base/src/components/image-enlarge.tsx +0 -24
  78. package/templates/base/src/components/preset-generator.tsx +0 -14
  79. package/templates/base/src/components/sidebar-toggle.tsx +0 -6
  80. package/templates/base/src/components/sidebar-tree.tsx +0 -6
  81. package/templates/base/src/config/color-scheme-utils.ts +0 -69
  82. package/templates/base/src/config/color-schemes.ts +0 -165
  83. package/templates/base/src/config/docs-schema.ts +0 -95
  84. package/templates/base/src/config/frontmatter-preview-defaults.ts +0 -27
  85. package/templates/base/src/config/frontmatter-preview-renderers.tsx +0 -46
  86. package/templates/base/src/config/i18n.ts +0 -239
  87. package/templates/base/src/config/settings-types.ts +0 -45
  88. package/templates/base/src/config/sidebars.ts +0 -66
  89. package/templates/base/src/config/tag-vocabulary-types.ts +0 -4
  90. package/templates/base/src/config/tag-vocabulary.ts +0 -20
  91. package/templates/base/src/config/z-index-tokens.ts +0 -128
  92. package/templates/base/src/types/docs-entry.ts +0 -28
  93. package/templates/base/src/types/heading.ts +0 -5
  94. package/templates/base/src/types/locale.ts +0 -10
  95. package/templates/base/src/utils/base.ts +0 -164
  96. package/templates/base/src/utils/docs.ts +0 -446
  97. package/templates/base/src/utils/git-info.ts +0 -70
  98. package/templates/base/src/utils/github.ts +0 -22
  99. package/templates/base/src/utils/nav-scope.ts +0 -34
  100. package/templates/base/src/utils/sidebar.ts +0 -36
  101. package/templates/base/src/utils/slug.ts +0 -10
  102. package/templates/base/src/utils/smart-break.tsx +0 -12
  103. package/templates/base/src/utils/tags.ts +0 -83
  104. package/templates/base/zfb-shim.d.ts +0 -183
  105. package/templates/features/bodyFootUtil/files/src/utils/github.ts +0 -22
  106. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +0 -42
  107. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +0 -752
  108. package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +0 -97
  109. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +0 -735
  110. package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +0 -15
  111. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +0 -435
  112. package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +0 -174
  113. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +0 -30
  114. package/templates/features/docHistory/files/src/components/doc-history.tsx +0 -10
  115. package/templates/features/docHistory/files/src/types/doc-history.ts +0 -7
  116. package/templates/features/dynamicPageTransition/files/src/components/client-router-bootstrap.tsx +0 -72
  117. package/templates/features/i18n/files/pages/[locale]/index.tsx +0 -72
  118. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +0 -11
  119. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +0 -6
  120. package/templates/features/tauri/files/src/components/find-bar.tsx +0 -122
  121. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +0 -59
  122. package/templates/features/tauri/files/src/utils/find-in-page.ts +0 -175
@@ -1,735 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import matter from "gray-matter";
4
- import { escapeForMdx } from "./escape-for-mdx";
5
-
6
- export interface ClaudeResourcesConfig {
7
- claudeDir: string;
8
- projectRoot?: string;
9
- docsDir: string;
10
- }
11
-
12
- interface ClaudeMdItem {
13
- displayPath: string;
14
- slug: string;
15
- relPath: string;
16
- }
17
-
18
- interface CommandItem {
19
- name: string;
20
- description: string;
21
- }
22
-
23
- interface SkillReference {
24
- name: string;
25
- title: string;
26
- content: string;
27
- }
28
-
29
- interface SkillItem {
30
- name: string;
31
- dir: string;
32
- description: string;
33
- references: SkillReference[];
34
- }
35
-
36
- interface AgentItem {
37
- name: string;
38
- file: string;
39
- description: string;
40
- model: string;
41
- }
42
-
43
- // ---------------------------------------------------------------------------
44
- // Utility
45
- // ---------------------------------------------------------------------------
46
-
47
- function ensureDir(dir: string) {
48
- if (!fs.existsSync(dir)) {
49
- fs.mkdirSync(dir, { recursive: true });
50
- }
51
- }
52
-
53
- function cleanDir(dir: string) {
54
- if (!fs.existsSync(dir)) return;
55
- fs.rmSync(dir, { recursive: true, force: true });
56
- }
57
-
58
- function parseFrontmatter(content: string) {
59
- try {
60
- return matter(content);
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
- function escapeTitle(s: string): string {
67
- // Backslashes must be escaped first — the value is embedded in
68
- // double-quoted YAML frontmatter where `\d` or `C:\path` is invalid.
69
- return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
70
- }
71
-
72
- function listFiles(dir: string): string[] {
73
- if (!fs.existsSync(dir)) return [];
74
- return fs
75
- .readdirSync(dir, { withFileTypes: true })
76
- .filter((d) => d.isFile())
77
- .map((d) => d.name)
78
- .sort();
79
- }
80
-
81
- function writeCategoryIndex(
82
- outputDir: string,
83
- label: string,
84
- position: number,
85
- description: string,
86
- ) {
87
- const mdx = `---
88
- title: "${escapeTitle(label)}"
89
- description: "${escapeTitle(description)}"
90
- sidebar_position: ${position}
91
- category_no_page: true
92
- generated: true
93
- ---
94
- `;
95
- fs.writeFileSync(path.join(outputDir, "index.mdx"), mdx);
96
- }
97
-
98
- /**
99
- * Writes an unlisted sub-page MDX file. Used for skill references, scripts,
100
- * and assets.
101
- *
102
- * The route is derived from the file's path within the content collection —
103
- * deliberately NOT from an explicit `slug:`. zfb's `resolveMarkdownLinks`
104
- * resolves relative links against the *source file path*, so the on-disk
105
- * location of these pages must match the URL the skill page links to. Writing
106
- * them at `<dir>/ref-<name>.mdx` (siblings of the skill's `index.mdx`) is what
107
- * makes the `./ref-<name>` links resolve (#2411).
108
- */
109
- function writeUnlistedSubPage(
110
- outputPath: string,
111
- title: string,
112
- body: string,
113
- ) {
114
- fs.writeFileSync(
115
- outputPath,
116
- `---\ntitle: "${escapeTitle(title)}"\nunlisted: true\ngenerated: true\n---\n\n${body}\n`,
117
- );
118
- }
119
-
120
- /**
121
- * Guards that the given name/slug is not the reserved "index" value.
122
- * Throws with a contextual message if it is.
123
- */
124
- function assertNotIndexReserved(
125
- nameOrSlug: string,
126
- errorMessage: string,
127
- ) {
128
- if (nameOrSlug === "index") {
129
- throw new Error(errorMessage);
130
- }
131
- }
132
-
133
- /**
134
- * Whether a markdown link target is a repo-relative file reference
135
- * (`./wrangler.toml`, `../../schema/photos.sql`, `foo/bar.md`) rather than
136
- * something the doc site can resolve: an absolute URL (`https://…`), a
137
- * protocol-relative URL (`//…`), a site-absolute path (`/docs/…`), a pure
138
- * anchor (`#…`), or a scheme (`mailto:`, `tel:`).
139
- */
140
- function isRepoRelativeLink(url: string): boolean {
141
- const trimmed = url.trim();
142
- if (trimmed === "") return false;
143
- if (trimmed.startsWith("#")) return false; // anchor
144
- if (trimmed.startsWith("/")) return false; // site-absolute or protocol-relative (//host)
145
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return false; // has a scheme (http:, mailto:, …)
146
- return true;
147
- }
148
-
149
- /**
150
- * Downgrade repo-relative markdown links in a mirrored `CLAUDE.md` body to
151
- * inline code so they don't dangle in the flattened mirror tree (#2411).
152
- *
153
- * A `CLAUDE.md`'s relative links point at real repo files (correct for an
154
- * in-repo reader), but the mirror flattens each file into a single
155
- * `claude-md/<name>.mdx` page with no counterpart for those targets — left as
156
- * links they surface as `broken link:` warnings on every affected page. Inline
157
- * code keeps the reference legible (`` `wrangler.toml` ``) without a href.
158
- *
159
- * Code spans are preserved verbatim: a `[x](./y)` inside a fenced block or an
160
- * inline-code span is literal text, not a link, and must not be rewritten.
161
- */
162
- function downgradeRepoRelativeLinks(content: string): string {
163
- const blockPlaceholder = "\x00CRLINK_BLOCK_";
164
- const inlinePlaceholder = "\x00CRLINK_INLINE_";
165
-
166
- // Extract fenced code blocks so their contents are untouched. Both backtick
167
- // (```) and tilde (~~~) fences are recognised; the `\1` backreference makes
168
- // the closing fence match the same delimiter the block opened with.
169
- const codeBlocks: string[] = [];
170
- const withBlocks = content.replace(/(`{3,}|~{3,})[^\n]*\n[\s\S]*?\1/g, (match) => {
171
- codeBlocks.push(match);
172
- return `${blockPlaceholder}${codeBlocks.length - 1}\x00`;
173
- });
174
-
175
- const transformed = withBlocks
176
- .split(new RegExp(`(${blockPlaceholder}\\d+\x00)`, "g"))
177
- .map((part) => {
178
- if (new RegExp(`^${blockPlaceholder}\\d+\x00$`).test(part)) return part;
179
-
180
- // Preserve inline-code spans, then rewrite links in the remaining text.
181
- const inlineCodes: string[] = [];
182
- const withInline = part.replace(
183
- /(`{1,3})(?!`)([\s\S]*?[^`])\1(?!`)/g,
184
- (match) => {
185
- inlineCodes.push(match);
186
- return `${inlinePlaceholder}${inlineCodes.length - 1}\x00`;
187
- },
188
- );
189
-
190
- const rewritten = withInline.replace(
191
- /!?\[([^\]]*)\]\(([^)]+)\)/g,
192
- (match, text: string, url: string) =>
193
- isRepoRelativeLink(url) ? `\`${text}\`` : match,
194
- );
195
-
196
- return rewritten.replace(
197
- new RegExp(`${inlinePlaceholder}(\\d+)\x00`, "g"),
198
- (_, idx: string) => inlineCodes[Number(idx)] ?? "",
199
- );
200
- })
201
- .join("");
202
-
203
- return transformed.replace(
204
- new RegExp(`${blockPlaceholder}(\\d+)\x00`, "g"),
205
- (_, idx: string) => codeBlocks[Number(idx)] ?? "",
206
- );
207
- }
208
-
209
- // ---------------------------------------------------------------------------
210
- // CLAUDE.md discovery
211
- // ---------------------------------------------------------------------------
212
-
213
- function findClaudeMdFiles(dir: string, excludeDirs: string[]): string[] {
214
- const results: string[] = [];
215
- if (!fs.existsSync(dir)) return results;
216
-
217
- // Strip trailing separators (path.join preserves one on e.g. "docs/") so the
218
- // boundary compare below stays exact for such entries too.
219
- const excludes = excludeDirs.map((d) =>
220
- d.endsWith(path.sep) ? d.slice(0, -path.sep.length) : d,
221
- );
222
-
223
- for (const item of fs.readdirSync(dir)) {
224
- if (item === "node_modules") continue;
225
- if (item.startsWith(".")) continue;
226
- const itemPath = path.join(dir, item);
227
- // Path-segment-boundary-aware: a raw startsWith(d) would also match a
228
- // sibling like "dist-extra" against an excluded "dist" (#2561).
229
- if (excludes.some((d) => itemPath === d || itemPath.startsWith(d + path.sep))) continue;
230
-
231
- // lstat (not stat) so symlinks aren't followed — a symlinked dir can point
232
- // back into the project (e.g. e2e fixtures linking to packages/) or out to
233
- // a slow mount (e.g. /mnt/c on WSL) and either turns the walk into a
234
- // multi-minute hang.
235
- let stat: fs.Stats;
236
- try {
237
- stat = fs.lstatSync(itemPath);
238
- } catch {
239
- continue;
240
- }
241
- if (stat.isDirectory()) {
242
- results.push(...findClaudeMdFiles(itemPath, excludes));
243
- } else if (stat.isFile() && item === "CLAUDE.md") {
244
- results.push(itemPath);
245
- }
246
- }
247
- return results;
248
- }
249
-
250
- // ---------------------------------------------------------------------------
251
- // CLAUDE.md generation
252
- // ---------------------------------------------------------------------------
253
-
254
- function generateClaudemdDocs(
255
- config: ClaudeResourcesConfig,
256
- ): ClaudeMdItem[] {
257
- const projectRoot = config.projectRoot ?? config.claudeDir;
258
- const outputDir = path.join(config.docsDir, "claude-md");
259
-
260
- cleanDir(outputDir);
261
-
262
- const excludeDirs = [
263
- path.join(projectRoot, ".git"),
264
- path.join(projectRoot, "node_modules"),
265
- path.join(projectRoot, "worktrees"),
266
- path.join(projectRoot, "dist"),
267
- path.join(projectRoot, "out"),
268
- path.join(projectRoot, "public"),
269
- path.join(projectRoot, "__inbox"),
270
- path.join(projectRoot, "test-results"),
271
- path.join(projectRoot, "e2e", "fixtures"),
272
- path.join(config.docsDir),
273
- ];
274
-
275
- const files = findClaudeMdFiles(projectRoot, excludeDirs);
276
- if (files.length === 0) return [];
277
-
278
- ensureDir(outputDir);
279
- const items: ClaudeMdItem[] = [];
280
-
281
- for (const filePath of files) {
282
- const relPath = path.relative(projectRoot, filePath);
283
- const displayPath = `/${relPath}`;
284
- const dirPart = path.dirname(relPath);
285
- const slug = dirPart === "." ? "root" : dirPart.replace(/\//g, "--");
286
- items.push({ displayPath, slug, relPath });
287
- }
288
-
289
- // Sort BEFORE writing: sidebar_position is baked into each generated .mdx,
290
- // so the root-first/alphabetical order must be applied first — sorting after
291
- // the write loop would leave positions in filesystem-walk order.
292
- items.sort((a, b) => {
293
- if (a.slug === "root") return -1;
294
- if (b.slug === "root") return 1;
295
- return a.displayPath.localeCompare(b.displayPath);
296
- });
297
-
298
- const emittedSlugs = new Map<string, string>();
299
- items.forEach((item, index) => {
300
- assertNotIndexReserved(
301
- item.slug,
302
- `claude-resources: "${item.relPath}" maps to the reserved slug "index", which is used for the category metadata file. Rename the directory to resolve the conflict.`,
303
- );
304
- const previous = emittedSlugs.get(item.slug);
305
- if (previous !== undefined) {
306
- throw new Error(
307
- `claude-resources: slug collision — "${item.slug}" is produced by both "${previous}" and "${item.relPath}". Rename one of the directories to resolve the conflict.`,
308
- );
309
- }
310
- emittedSlugs.set(item.slug, item.relPath);
311
- const content = fs.readFileSync(path.join(projectRoot, item.relPath), "utf8");
312
- const mdx = `---
313
- title: "${escapeTitle(item.displayPath)}"
314
- description: "CLAUDE.md at ${escapeTitle(item.displayPath)}"
315
- sidebar_position: ${index + 1}
316
- sidebar_label: "${escapeTitle(item.relPath)}"
317
- generated: true
318
- ---
319
-
320
- **Path:** \`${item.relPath}\`
321
-
322
- ${escapeForMdx(downgradeRepoRelativeLinks(content.trim()))}
323
- `;
324
- fs.writeFileSync(path.join(outputDir, `${item.slug}.mdx`), mdx);
325
- });
326
-
327
- writeCategoryIndex(outputDir, "CLAUDE.md", 900, "Project-specific instructions");
328
- return items;
329
- }
330
-
331
- // ---------------------------------------------------------------------------
332
- // Commands generation
333
- // ---------------------------------------------------------------------------
334
-
335
- function generateCommandsDocs(config: ClaudeResourcesConfig): CommandItem[] {
336
- const commandsDir = path.join(config.claudeDir, "commands");
337
- const outputDir = path.join(config.docsDir, "claude-commands");
338
-
339
- cleanDir(outputDir);
340
-
341
- if (!fs.existsSync(commandsDir)) return [];
342
-
343
- const files = fs.readdirSync(commandsDir).filter((f) => f.endsWith(".md"));
344
- if (files.length === 0) return [];
345
-
346
- ensureDir(outputDir);
347
- const items: CommandItem[] = [];
348
-
349
- for (const file of files) {
350
- const content = fs.readFileSync(path.join(commandsDir, file), "utf8");
351
- const parsed = parseFrontmatter(content);
352
- if (!parsed) continue;
353
-
354
- const name = file.replace(/\.md$/, "");
355
- assertNotIndexReserved(
356
- name,
357
- `claude-resources: ".claude/commands/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the command file to resolve the conflict.`,
358
- );
359
- const description = (parsed.data.description as string) || "";
360
-
361
- items.push({ name, description });
362
-
363
- const mdx = `---
364
- title: "${escapeTitle(name)}"
365
- description: "${escapeTitle(description)}"
366
- sidebar_label: "${escapeTitle(name)}"
367
- generated: true
368
- ---
369
-
370
- ${escapeForMdx(parsed.content.trim())}
371
- `;
372
- fs.writeFileSync(path.join(outputDir, `${name}.mdx`), mdx);
373
- }
374
-
375
- items.sort((a, b) => a.name.localeCompare(b.name));
376
-
377
- writeCategoryIndex(outputDir, "Commands", 901, "Custom slash commands");
378
- return items;
379
- }
380
-
381
- // ---------------------------------------------------------------------------
382
- // Skills generation
383
- // ---------------------------------------------------------------------------
384
-
385
- type TreeEntry =
386
- | { isDir: false; name: string }
387
- | { isDir: true; name: string; children: string[] };
388
-
389
- function getSkillFileTree(
390
- skillDir: string,
391
- subDirs: { name: string; files: string[] }[],
392
- ): string {
393
- const lines: string[] = [`${skillDir}/`];
394
- const entries: TreeEntry[] = [{ isDir: false, name: "SKILL.md" }];
395
-
396
- for (const sub of subDirs) {
397
- entries.push({ isDir: true, name: sub.name, children: sub.files });
398
- }
399
-
400
- for (let i = 0; i < entries.length; i++) {
401
- const entry = entries[i];
402
- if (!entry) continue;
403
- const isLast = i === entries.length - 1;
404
- const prefix = isLast ? "└── " : "├── ";
405
-
406
- if (!entry.isDir) {
407
- lines.push(`${prefix}${entry.name}`);
408
- } else {
409
- lines.push(`${prefix}${entry.name}/`);
410
- for (let j = 0; j < entry.children.length; j++) {
411
- const child = entry.children[j];
412
- if (!child) continue;
413
- const childIsLast = j === entry.children.length - 1;
414
- const continuation = isLast ? " " : "│ ";
415
- const childPrefix = childIsLast ? "└── " : "├── ";
416
- lines.push(`${continuation}${childPrefix}${child}`);
417
- }
418
- }
419
- }
420
-
421
- return lines.join("\n");
422
- }
423
-
424
- function getScriptDescription(filePath: string): string {
425
- try {
426
- const topLines = fs.readFileSync(filePath, "utf8").split("\n", 2);
427
- // Skip shebang, use second line if available
428
- const firstLine = topLines[0] ?? "";
429
- const commentLine = firstLine.startsWith("#!")
430
- ? topLines[1] ?? ""
431
- : firstLine;
432
- // Match # comments (shell/python) or // comments (JS/TS)
433
- const match = commentLine.match(/^(?:#|\/\/)\s*(.+)/);
434
- return match ? ` — ${match[1]}` : "";
435
- } catch {
436
- return "";
437
- }
438
- }
439
-
440
- function getSkillReferences(
441
- skillsDir: string,
442
- skillDir: string,
443
- ): SkillReference[] {
444
- const refsDir = path.join(skillsDir, skillDir, "references");
445
- if (!fs.existsSync(refsDir)) return [];
446
-
447
- return fs
448
- .readdirSync(refsDir)
449
- .filter((f) => f.endsWith(".md"))
450
- .map((f) => {
451
- const content = fs.readFileSync(path.join(refsDir, f), "utf8");
452
- const name = f.replace(/\.md$/, "");
453
- const h1Match = content.match(/^#\s+(.+)$/m);
454
- const title = h1Match?.[1] ?? name;
455
- return { name, title, content };
456
- })
457
- .sort((a, b) => a.name.localeCompare(b.name));
458
- }
459
-
460
- function generateSkillsDocs(config: ClaudeResourcesConfig): SkillItem[] {
461
- const skillsDir = path.join(config.claudeDir, "skills");
462
- const outputDir = path.join(config.docsDir, "claude-skills");
463
-
464
- cleanDir(outputDir);
465
-
466
- if (!fs.existsSync(skillsDir)) return [];
467
-
468
- const dirs = fs.readdirSync(skillsDir).filter((d) => {
469
- const skillPath = path.join(skillsDir, d);
470
- return (
471
- fs.statSync(skillPath).isDirectory() &&
472
- fs.existsSync(path.join(skillPath, "SKILL.md"))
473
- );
474
- });
475
-
476
- if (dirs.length === 0) return [];
477
-
478
- ensureDir(outputDir);
479
- const items: SkillItem[] = [];
480
-
481
- for (const dir of dirs) {
482
- assertNotIndexReserved(
483
- dir,
484
- `claude-resources: skill directory ".claude/skills/index/" uses the reserved name "index", which is used for the category metadata file. Rename the skill directory to resolve the conflict.`,
485
- );
486
- const content = fs.readFileSync(
487
- path.join(skillsDir, dir, "SKILL.md"),
488
- "utf8",
489
- );
490
- const parsed = parseFrontmatter(content);
491
- if (!parsed) continue;
492
-
493
- const name = (parsed.data.name as string) || dir;
494
- const description = (parsed.data.description as string) || "";
495
- const references = getSkillReferences(skillsDir, dir);
496
-
497
- items.push({ name, dir, description, references });
498
-
499
- const scriptFiles = listFiles(path.join(skillsDir, dir, "scripts"));
500
- const assetFiles = listFiles(path.join(skillsDir, dir, "assets"));
501
- const refFiles = references.map((r) => `${r.name}.md`);
502
-
503
- // Collect non-empty subdirectories for tree display
504
- const subDirs: { name: string; files: string[] }[] = [];
505
- if (scriptFiles.length > 0) subDirs.push({ name: "scripts", files: scriptFiles });
506
- if (refFiles.length > 0) subDirs.push({ name: "references", files: refFiles });
507
- if (assetFiles.length > 0) subDirs.push({ name: "assets", files: assetFiles });
508
-
509
- // File tree + links to renderable .md sub-files
510
- let fileStructureSection = "";
511
- if (subDirs.length > 0) {
512
- const tree = `\`\`\`\n${getSkillFileTree(dir, subDirs)}\n\`\`\``;
513
-
514
- // Collect links to all .md sub-files that get pages. Links use
515
- // ./<subpage>; because the skill page is written as `<dir>/index.mdx`,
516
- // these resolve to the sibling `<dir>/<subpage>.mdx` files (#2411).
517
- const links: string[] = [];
518
- for (const ref of references) {
519
- links.push(`- [references/${ref.name}.md](./ref-${ref.name})`);
520
- }
521
- for (const f of scriptFiles.filter((s) => s.endsWith(".md"))) {
522
- const slug = f.replace(/\.md$/, "");
523
- links.push(`- [scripts/${f}](./script-${slug})`);
524
- }
525
- for (const f of assetFiles.filter((a) => a.endsWith(".md"))) {
526
- const slug = f.replace(/\.md$/, "");
527
- links.push(`- [assets/${f}](./asset-${slug})`);
528
- }
529
-
530
- const linkList = links.length > 0 ? `\n\n${links.join("\n")}` : "";
531
- fileStructureSection = `## File Structure\n\n${tree}${linkList}`;
532
- }
533
-
534
- const shortDesc = description.length > 200
535
- ? description.substring(0, 200) + "..."
536
- : description;
537
-
538
- // Rewrite references/scripts/assets links in skill body to match doc site URLs
539
- let skillBody = parsed.content.trim();
540
- skillBody = skillBody
541
- .replace(/\]\(references\/([^)]+)\.md\)/g, "](./ref-$1)")
542
- .replace(/\]\(scripts\/([^)]+)\.md\)/g, "](./script-$1)")
543
- .replace(/\]\(assets\/([^)]+)\.md\)/g, "](./asset-$1)");
544
-
545
- const body = [
546
- fileStructureSection,
547
- escapeForMdx(skillBody),
548
- ]
549
- .filter(Boolean)
550
- .join("\n\n");
551
-
552
- const mdx = `---
553
- title: "${escapeTitle(name)}"
554
- description: "${escapeTitle(shortDesc)}"
555
- sidebar_label: "${escapeTitle(name)}"
556
- generated: true
557
- ---
558
-
559
- ${body}`;
560
-
561
- // Write the skill page as the directory index (`<dir>/index.mdx`) so its
562
- // route is `claude-skills/<dir>` served at URL `.../claude-skills/<dir>/`.
563
- // This makes the reference/script/asset pages genuine siblings inside
564
- // `<dir>/`, which is what lets the `./ref-<name>` links above resolve.
565
- const skillDirOut = path.join(outputDir, dir);
566
- ensureDir(skillDirOut);
567
- fs.writeFileSync(path.join(skillDirOut, "index.mdx"), mdx);
568
-
569
- // Generate unlisted sub-pages as nested files inside `<dir>/`. Their routes
570
- // (`claude-skills/<dir>/ref-<name>`, …) are derived from these file paths
571
- // and therefore match the `./ref-<name>` / `./script-<name>` /
572
- // `./asset-<name>` links emitted above (#2411).
573
- for (const ref of references) {
574
- writeUnlistedSubPage(
575
- path.join(skillDirOut, `ref-${ref.name}.mdx`),
576
- ref.title,
577
- escapeForMdx(ref.content.trim()),
578
- );
579
- }
580
-
581
- for (const f of scriptFiles.filter((s) => s.endsWith(".md"))) {
582
- const slug = f.replace(/\.md$/, "");
583
- const raw = fs.readFileSync(
584
- path.join(skillsDir, dir, "scripts", f),
585
- "utf8",
586
- );
587
- const h1Match = raw.match(/^#\s+(.+)$/m);
588
- const title = h1Match?.[1] ?? slug;
589
- writeUnlistedSubPage(
590
- path.join(skillDirOut, `script-${slug}.mdx`),
591
- title,
592
- escapeForMdx(raw.trim()),
593
- );
594
- }
595
-
596
- for (const f of assetFiles.filter((a) => a.endsWith(".md"))) {
597
- const slug = f.replace(/\.md$/, "");
598
- const raw = fs.readFileSync(
599
- path.join(skillsDir, dir, "assets", f),
600
- "utf8",
601
- );
602
- const h1Match = raw.match(/^#\s+(.+)$/m);
603
- const title = h1Match?.[1] ?? slug;
604
- writeUnlistedSubPage(
605
- path.join(skillDirOut, `asset-${slug}.mdx`),
606
- title,
607
- escapeForMdx(raw.trim()),
608
- );
609
- }
610
- }
611
-
612
- items.sort((a, b) => a.name.localeCompare(b.name));
613
-
614
- writeCategoryIndex(outputDir, "Skills", 902, "Skill packages");
615
- return items;
616
- }
617
-
618
- // ---------------------------------------------------------------------------
619
- // Agents generation
620
- // ---------------------------------------------------------------------------
621
-
622
- function generateAgentsDocs(config: ClaudeResourcesConfig): AgentItem[] {
623
- const agentsDir = path.join(config.claudeDir, "agents");
624
- const outputDir = path.join(config.docsDir, "claude-agents");
625
-
626
- cleanDir(outputDir);
627
-
628
- if (!fs.existsSync(agentsDir)) return [];
629
-
630
- const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md"));
631
- if (files.length === 0) return [];
632
-
633
- ensureDir(outputDir);
634
- const items: AgentItem[] = [];
635
-
636
- for (const file of files) {
637
- const content = fs.readFileSync(path.join(agentsDir, file), "utf8");
638
- const parsed = parseFrontmatter(content);
639
- if (!parsed) continue;
640
-
641
- const name = (parsed.data.name as string) || file.replace(/\.md$/, "");
642
- const description = (parsed.data.description as string) || "";
643
- const model = (parsed.data.model as string) || "";
644
- const fileSlug = file.replace(/\.md$/, "");
645
- assertNotIndexReserved(
646
- fileSlug,
647
- `claude-resources: ".claude/agents/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the agent file to resolve the conflict.`,
648
- );
649
-
650
- items.push({ name, file: fileSlug, description, model });
651
-
652
- const modelBadge = model ? `**Model:** \`${model}\`\n` : "";
653
-
654
- const mdx = `---
655
- title: "${escapeTitle(name)}"
656
- description: "${escapeTitle(description)}"
657
- sidebar_label: "${escapeTitle(name)}"
658
- generated: true
659
- ---
660
-
661
- ${modelBadge}
662
- ${escapeForMdx(parsed.content.trim())}
663
- `;
664
- fs.writeFileSync(path.join(outputDir, `${fileSlug}.mdx`), mdx);
665
- }
666
-
667
- items.sort((a, b) => a.name.localeCompare(b.name));
668
-
669
- writeCategoryIndex(outputDir, "Agents", 903, "Custom subagents");
670
- return items;
671
- }
672
-
673
- // ---------------------------------------------------------------------------
674
- // Main
675
- // ---------------------------------------------------------------------------
676
-
677
- function generateOverviewIndex(
678
- config: ClaudeResourcesConfig,
679
- {
680
- hasCommands,
681
- hasSkills,
682
- hasAgents,
683
- hasClaudemd,
684
- }: { hasCommands: boolean; hasSkills: boolean; hasAgents: boolean; hasClaudemd: boolean },
685
- ) {
686
- const outputDir = path.join(config.docsDir, "claude");
687
- cleanDir(outputDir);
688
- ensureDir(outputDir);
689
-
690
- // Build the explicit slug list from whichever sub-categories were generated.
691
- // CategoryNav with `categories` renders cards for each slug by resolving
692
- // the node in the nav tree (including noPage auto-index categories) and
693
- // falling back to docsUrl(slug, locale) for the href when noPage=true.
694
- const categorySlugs: string[] = [];
695
- if (hasClaudemd) categorySlugs.push("claude-md");
696
- if (hasSkills) categorySlugs.push("claude-skills");
697
- if (hasAgents) categorySlugs.push("claude-agents");
698
- if (hasCommands) categorySlugs.push("claude-commands");
699
-
700
- const categoriesAttr = JSON.stringify(categorySlugs);
701
-
702
- const index = `---
703
- title: "Claude"
704
- description: "Claude Code configuration reference."
705
- sidebar_position: 899
706
- generated: true
707
- ---
708
-
709
- ## Resources
710
-
711
- <CategoryNav categories={${categoriesAttr}} />
712
- `;
713
- fs.writeFileSync(path.join(outputDir, "index.mdx"), index);
714
- }
715
-
716
- export function generateClaudeResourcesDocs(config: ClaudeResourcesConfig) {
717
- const claudemds = generateClaudemdDocs(config);
718
- const commands = generateCommandsDocs(config);
719
- const skills = generateSkillsDocs(config);
720
- const agents = generateAgentsDocs(config);
721
-
722
- generateOverviewIndex(config, {
723
- hasClaudemd: claudemds.length > 0,
724
- hasCommands: commands.length > 0,
725
- hasSkills: skills.length > 0,
726
- hasAgents: agents.length > 0,
727
- });
728
-
729
- return {
730
- claudemd: claudemds.length,
731
- commands: commands.length,
732
- skills: skills.length,
733
- agents: agents.length,
734
- };
735
- }