create-zudo-doc 3.1.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 (127) 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/preset.d.ts +1 -0
  41. package/dist/preset.js +5 -0
  42. package/dist/prompts.d.ts +2 -0
  43. package/dist/prompts.js +1 -0
  44. package/dist/scaffold.d.ts +13 -6
  45. package/dist/scaffold.js +78 -59
  46. package/dist/utils.d.ts +10 -0
  47. package/dist/utils.js +14 -0
  48. package/dist/zfb-config-gen.d.ts +32 -20
  49. package/dist/zfb-config-gen.js +400 -53
  50. package/package.json +2 -2
  51. package/templates/base/pages/docs/[[...slug]].tsx +59 -0
  52. package/templates/base/pages/index.tsx +6 -41
  53. package/templates/base/scripts/setup-doc-skill.sh +261 -0
  54. package/templates/base/src/styles/global.css +15 -340
  55. package/templates/base/tsconfig.json +3 -26
  56. package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +88 -0
  57. package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +3 -1
  58. package/dist/settings-gen.d.ts +0 -2
  59. package/dist/settings-gen.js +0 -319
  60. package/templates/base/.htmlvalidate.json +0 -5
  61. package/templates/base/.zfb/doc-history-meta.json +0 -1
  62. package/templates/base/pages/_data.ts +0 -160
  63. package/templates/base/pages/lib/_body-end-islands.tsx +0 -165
  64. package/templates/base/pages/lib/_chrome.ts +0 -167
  65. package/templates/base/pages/lib/_details.tsx +0 -29
  66. package/templates/base/pages/lib/_doc-route-entries.ts +0 -10
  67. package/templates/base/pages/lib/_extract-headings.ts +0 -37
  68. package/templates/base/pages/lib/_frontmatter-preview-data.ts +0 -27
  69. package/templates/base/pages/lib/_nav-source-cache.ts +0 -100
  70. package/templates/base/pages/lib/_nav-source-docs.ts +0 -17
  71. package/templates/base/pages/lib/_preset-generator.tsx +0 -81
  72. package/templates/base/pages/lib/_route-context.ts +0 -32
  73. package/templates/base/pages/lib/_search-widget.tsx +0 -17
  74. package/templates/base/pages/lib/doc-page-props.ts +0 -30
  75. package/templates/base/pages/lib/locale-merge.ts +0 -59
  76. package/templates/base/scripts/run-b4push.sh +0 -102
  77. package/templates/base/src/components/ai-chat-modal.tsx +0 -18
  78. package/templates/base/src/components/content/code-group.tsx +0 -3
  79. package/templates/base/src/components/content/content-admonition.tsx +0 -4
  80. package/templates/base/src/components/desktop-sidebar-toggle.tsx +0 -15
  81. package/templates/base/src/components/doc-history.tsx +0 -21
  82. package/templates/base/src/components/image-enlarge.tsx +0 -24
  83. package/templates/base/src/components/preset-generator.tsx +0 -14
  84. package/templates/base/src/components/sidebar-toggle.tsx +0 -6
  85. package/templates/base/src/components/sidebar-tree.tsx +0 -6
  86. package/templates/base/src/config/color-scheme-utils.ts +0 -69
  87. package/templates/base/src/config/color-schemes.ts +0 -165
  88. package/templates/base/src/config/docs-schema.ts +0 -95
  89. package/templates/base/src/config/frontmatter-preview-defaults.ts +0 -27
  90. package/templates/base/src/config/frontmatter-preview-renderers.tsx +0 -46
  91. package/templates/base/src/config/i18n.ts +0 -239
  92. package/templates/base/src/config/settings-types.ts +0 -45
  93. package/templates/base/src/config/sidebars.ts +0 -66
  94. package/templates/base/src/config/tag-vocabulary-types.ts +0 -4
  95. package/templates/base/src/config/tag-vocabulary.ts +0 -20
  96. package/templates/base/src/config/z-index-tokens.ts +0 -128
  97. package/templates/base/src/types/docs-entry.ts +0 -28
  98. package/templates/base/src/types/heading.ts +0 -5
  99. package/templates/base/src/types/locale.ts +0 -10
  100. package/templates/base/src/utils/base.ts +0 -164
  101. package/templates/base/src/utils/docs.ts +0 -446
  102. package/templates/base/src/utils/git-info.ts +0 -70
  103. package/templates/base/src/utils/github.ts +0 -22
  104. package/templates/base/src/utils/nav-scope.ts +0 -34
  105. package/templates/base/src/utils/sidebar.ts +0 -36
  106. package/templates/base/src/utils/slug.ts +0 -10
  107. package/templates/base/src/utils/smart-break.tsx +0 -12
  108. package/templates/base/src/utils/tags.ts +0 -83
  109. package/templates/base/zfb-shim.d.ts +0 -178
  110. package/templates/features/bodyFootUtil/files/src/utils/github.ts +0 -22
  111. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +0 -42
  112. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +0 -752
  113. package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +0 -97
  114. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +0 -735
  115. package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +0 -15
  116. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +0 -435
  117. package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +0 -174
  118. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +0 -30
  119. package/templates/features/docHistory/files/src/components/doc-history.tsx +0 -10
  120. package/templates/features/docHistory/files/src/types/doc-history.ts +0 -7
  121. package/templates/features/dynamicPageTransition/files/src/components/client-router-bootstrap.tsx +0 -72
  122. package/templates/features/i18n/files/pages/[locale]/index.tsx +0 -72
  123. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +0 -11
  124. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +0 -6
  125. package/templates/features/tauri/files/src/components/find-bar.tsx +0 -122
  126. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +0 -59
  127. package/templates/features/tauri/files/src/utils/find-in-page.ts +0 -175
@@ -1,752 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import os from "node:os";
5
- import matter from "gray-matter";
6
- import { generateClaudeResourcesDocs } from "../generate";
7
-
8
- let tmpDir: string;
9
- let claudeDir: string;
10
- let docsDir: string;
11
-
12
- function createFixture() {
13
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-res-test-"));
14
-
15
- // .claude/ directory
16
- claudeDir = path.join(tmpDir, ".claude");
17
- docsDir = path.join(tmpDir, "docs");
18
- fs.mkdirSync(docsDir, { recursive: true });
19
-
20
- // Commands
21
- const commandsDir = path.join(claudeDir, "commands");
22
- fs.mkdirSync(commandsDir, { recursive: true });
23
- fs.writeFileSync(
24
- path.join(commandsDir, "test-cmd.md"),
25
- '---\ndescription: "A test command"\n---\n\nThis is a test command body.',
26
- );
27
-
28
- // Skills
29
- const skillDir = path.join(claudeDir, "skills", "test-skill");
30
- fs.mkdirSync(path.join(skillDir, "references"), { recursive: true });
31
- fs.mkdirSync(path.join(skillDir, "scripts"), { recursive: true });
32
- fs.mkdirSync(path.join(skillDir, "assets"), { recursive: true });
33
-
34
- fs.writeFileSync(
35
- path.join(skillDir, "SKILL.md"),
36
- '---\nname: test-skill\ndescription: "A test skill"\n---\n\nSkill instructions here.\n\nSee [references/guide.md](references/guide.md) for details.',
37
- );
38
- fs.writeFileSync(
39
- path.join(skillDir, "references", "guide.md"),
40
- "# Guide\n\nSome guide content",
41
- );
42
- fs.writeFileSync(
43
- path.join(skillDir, "scripts", "run.sh"),
44
- "#!/bin/bash\n# Run the test",
45
- );
46
- fs.writeFileSync(
47
- path.join(skillDir, "assets", "template.md"),
48
- "# Template\n\nA template",
49
- );
50
-
51
- // Agents
52
- const agentsDir = path.join(claudeDir, "agents");
53
- fs.mkdirSync(agentsDir, { recursive: true });
54
- fs.writeFileSync(
55
- path.join(agentsDir, "test-agent.md"),
56
- '---\nname: test-agent\ndescription: "A test agent"\nmodel: sonnet\n---\n\nAgent instructions here.',
57
- );
58
-
59
- // Root CLAUDE.md
60
- fs.writeFileSync(
61
- path.join(tmpDir, "CLAUDE.md"),
62
- "# Project\n\nProject instructions",
63
- );
64
- }
65
-
66
- describe("generateClaudeResourcesDocs", () => {
67
- beforeEach(() => {
68
- createFixture();
69
- });
70
-
71
- afterEach(() => {
72
- fs.rmSync(tmpDir, { recursive: true, force: true });
73
- });
74
-
75
- // ---------------------------------------------------------------------------
76
- // File structure tests
77
- // ---------------------------------------------------------------------------
78
-
79
- describe("file structure", () => {
80
- it("generates correct directory structure", () => {
81
- generateClaudeResourcesDocs({
82
- claudeDir,
83
- projectRoot: tmpDir,
84
- docsDir,
85
- });
86
-
87
- expect(fs.existsSync(path.join(docsDir, "claude"))).toBe(true);
88
- expect(fs.existsSync(path.join(docsDir, "claude-md"))).toBe(true);
89
- expect(fs.existsSync(path.join(docsDir, "claude-commands"))).toBe(true);
90
- expect(fs.existsSync(path.join(docsDir, "claude-skills"))).toBe(true);
91
- expect(fs.existsSync(path.join(docsDir, "claude-agents"))).toBe(true);
92
- });
93
-
94
- it("generates index.mdx with category_no_page for sub-categories", () => {
95
- generateClaudeResourcesDocs({
96
- claudeDir,
97
- projectRoot: tmpDir,
98
- docsDir,
99
- });
100
-
101
- const dirs = ["claude-md", "claude-commands", "claude-skills", "claude-agents"];
102
- for (const dir of dirs) {
103
- const indexPath = path.join(docsDir, dir, "index.mdx");
104
- expect(fs.existsSync(indexPath)).toBe(true);
105
-
106
- const parsed = matter(fs.readFileSync(indexPath, "utf8"));
107
- expect(parsed.data).toHaveProperty("title");
108
- expect(parsed.data).toHaveProperty("sidebar_position");
109
- expect(parsed.data).toHaveProperty("description");
110
- expect(parsed.data.category_no_page).toBe(true);
111
- }
112
- });
113
-
114
- it("generates skill page as <dir>/index.mdx", () => {
115
- generateClaudeResourcesDocs({
116
- claudeDir,
117
- projectRoot: tmpDir,
118
- docsDir,
119
- });
120
-
121
- // The skill page is the directory index so `./ref-<name>` links resolve
122
- // against the source file path (#2411) — NOT a flat `<dir>.mdx`.
123
- const indexPath = path.join(docsDir, "claude-skills", "test-skill", "index.mdx");
124
- expect(fs.existsSync(indexPath)).toBe(true);
125
-
126
- const flatPath = path.join(docsDir, "claude-skills", "test-skill.mdx");
127
- expect(fs.existsSync(flatPath)).toBe(false);
128
- });
129
- });
130
-
131
- // ---------------------------------------------------------------------------
132
- // Content tests
133
- // ---------------------------------------------------------------------------
134
-
135
- describe("content", () => {
136
- it("generates overview page with CategoryNav", () => {
137
- generateClaudeResourcesDocs({
138
- claudeDir,
139
- projectRoot: tmpDir,
140
- docsDir,
141
- });
142
-
143
- const overview = fs.readFileSync(
144
- path.join(docsDir, "claude", "index.mdx"),
145
- "utf8",
146
- );
147
- expect(overview).toContain('<CategoryNav categories={');
148
- });
149
-
150
- it("overview body has no prose line duplicating the frontmatter description", () => {
151
- // Regression test for #2568: the body used to repeat the frontmatter
152
- // `description` verbatim as a standalone prose line right below `---`.
153
- generateClaudeResourcesDocs({
154
- claudeDir,
155
- projectRoot: tmpDir,
156
- docsDir,
157
- });
158
-
159
- const overview = fs.readFileSync(
160
- path.join(docsDir, "claude", "index.mdx"),
161
- "utf8",
162
- );
163
- const parsed = matter(overview);
164
- const description = parsed.data.description as string;
165
- const bodyLines = parsed.content.split("\n");
166
-
167
- expect(bodyLines).not.toContain(description);
168
- });
169
-
170
- it("skill page has correct frontmatter", () => {
171
- generateClaudeResourcesDocs({
172
- claudeDir,
173
- projectRoot: tmpDir,
174
- docsDir,
175
- });
176
-
177
- const skillPage = fs.readFileSync(
178
- path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
179
- "utf8",
180
- );
181
- const parsed = matter(skillPage);
182
-
183
- expect(parsed.data.title).toBe("test-skill");
184
- expect(parsed.data.description).toBe("A test skill");
185
- expect(parsed.data.sidebar_label).toBe("test-skill");
186
- });
187
-
188
- it("skill page has file tree", () => {
189
- generateClaudeResourcesDocs({
190
- claudeDir,
191
- projectRoot: tmpDir,
192
- docsDir,
193
- });
194
-
195
- const skillPage = fs.readFileSync(
196
- path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
197
- "utf8",
198
- );
199
-
200
- // Should contain tree-drawing characters
201
- expect(skillPage).toContain("├── ");
202
- expect(skillPage).toContain("└── ");
203
- expect(skillPage).toContain("test-skill/");
204
- expect(skillPage).toContain("SKILL.md");
205
- });
206
-
207
- it("skill page has links to sub-files that resolve to sibling nested files", () => {
208
- generateClaudeResourcesDocs({
209
- claudeDir,
210
- projectRoot: tmpDir,
211
- docsDir,
212
- });
213
-
214
- const skillPage = fs.readFileSync(
215
- path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
216
- "utf8",
217
- );
218
-
219
- // Links use ./<subpage>; the skill page is <dir>/index.mdx so these
220
- // resolve against the file path to the sibling <dir>/<subpage>.mdx (#2411).
221
- expect(skillPage).toContain("./ref-guide");
222
- expect(skillPage).toContain("./asset-template");
223
-
224
- // Must NOT contain the double-dir pattern ./<dir>/<subpage>
225
- expect(skillPage).not.toContain("./test-skill/ref-guide");
226
- expect(skillPage).not.toContain("./test-skill/asset-template");
227
-
228
- // Each linked sub-page must exist as a sibling nested file inside <dir>/,
229
- // i.e. claude-skills/test-skill/<subpage>.mdx — which is exactly where the
230
- // ./<subpage> relative link resolves to.
231
- const linkPattern = /\]\(\.\/([\w-]+)\)/g;
232
- let match;
233
- while ((match = linkPattern.exec(skillPage)) !== null) {
234
- const subPage = match[1];
235
- const targetFile = path.join(
236
- docsDir,
237
- "claude-skills",
238
- "test-skill",
239
- `${subPage}.mdx`,
240
- );
241
- expect(
242
- fs.existsSync(targetFile),
243
- `Link target "test-skill/${subPage}.mdx" should exist`,
244
- ).toBe(true);
245
- }
246
- });
247
-
248
- it("skill body references/scripts/assets links are rewritten to doc site format", () => {
249
- generateClaudeResourcesDocs({
250
- claudeDir,
251
- projectRoot: tmpDir,
252
- docsDir,
253
- });
254
-
255
- const skillPage = fs.readFileSync(
256
- path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
257
- "utf8",
258
- );
259
-
260
- // Body links like (references/guide.md) should be rewritten to (./ref-guide)
261
- expect(skillPage).toContain("](./ref-guide)");
262
- expect(skillPage).not.toContain("](references/guide.md)");
263
- });
264
-
265
- it("agent page has model badge", () => {
266
- generateClaudeResourcesDocs({
267
- claudeDir,
268
- projectRoot: tmpDir,
269
- docsDir,
270
- });
271
-
272
- const agentPage = fs.readFileSync(
273
- path.join(docsDir, "claude-agents", "test-agent.mdx"),
274
- "utf8",
275
- );
276
- expect(agentPage).toContain("**Model:** `sonnet`");
277
- });
278
- });
279
-
280
- // ---------------------------------------------------------------------------
281
- // Sub-file page tests
282
- // ---------------------------------------------------------------------------
283
-
284
- describe("sub-file pages", () => {
285
- it("generates unlisted reference page as a nested file", () => {
286
- generateClaudeResourcesDocs({
287
- claudeDir,
288
- projectRoot: tmpDir,
289
- docsDir,
290
- });
291
-
292
- const refPage = path.join(docsDir, "claude-skills", "test-skill", "ref-guide.mdx");
293
- expect(fs.existsSync(refPage)).toBe(true);
294
-
295
- const parsed = matter(fs.readFileSync(refPage, "utf8"));
296
- expect(parsed.data.unlisted).toBe(true);
297
- });
298
-
299
- it("generates unlisted asset page for .md files as a nested file", () => {
300
- generateClaudeResourcesDocs({
301
- claudeDir,
302
- projectRoot: tmpDir,
303
- docsDir,
304
- });
305
-
306
- const assetPage = path.join(docsDir, "claude-skills", "test-skill", "asset-template.mdx");
307
- expect(fs.existsSync(assetPage)).toBe(true);
308
-
309
- const parsed = matter(fs.readFileSync(assetPage, "utf8"));
310
- expect(parsed.data.unlisted).toBe(true);
311
- });
312
-
313
- it("does NOT generate page for non-.md scripts", () => {
314
- generateClaudeResourcesDocs({
315
- claudeDir,
316
- projectRoot: tmpDir,
317
- docsDir,
318
- });
319
-
320
- const scriptPage = path.join(docsDir, "claude-skills", "test-skill", "script-run.mdx");
321
- expect(fs.existsSync(scriptPage)).toBe(false);
322
- });
323
-
324
- it("sub-pages derive their route from the file path (no explicit slug)", () => {
325
- generateClaudeResourcesDocs({
326
- claudeDir,
327
- projectRoot: tmpDir,
328
- docsDir,
329
- });
330
-
331
- // The page lives at claude-skills/test-skill/ref-guide.mdx, so its route
332
- // is claude-skills/test-skill/ref-guide — nested breadcrumbs come from the
333
- // file path, not an explicit slug (which zfb's link resolver ignores, #2411).
334
- const refPage = fs.readFileSync(
335
- path.join(docsDir, "claude-skills", "test-skill", "ref-guide.mdx"),
336
- "utf8",
337
- );
338
- const parsed = matter(refPage);
339
- expect(parsed.data.slug).toBeUndefined();
340
- });
341
-
342
- it("reference page content is correct", () => {
343
- generateClaudeResourcesDocs({
344
- claudeDir,
345
- projectRoot: tmpDir,
346
- docsDir,
347
- });
348
-
349
- const refPage = fs.readFileSync(
350
- path.join(docsDir, "claude-skills", "test-skill", "ref-guide.mdx"),
351
- "utf8",
352
- );
353
- const parsed = matter(refPage);
354
-
355
- expect(parsed.data.title).toBe("Guide");
356
- expect(parsed.content).toContain("Some guide content");
357
- });
358
- });
359
-
360
- // ---------------------------------------------------------------------------
361
- // Category metadata tests
362
- // ---------------------------------------------------------------------------
363
-
364
- describe("category metadata", () => {
365
- it("index.mdx sidebar_position values are ordered correctly", () => {
366
- generateClaudeResourcesDocs({
367
- claudeDir,
368
- projectRoot: tmpDir,
369
- docsDir,
370
- });
371
-
372
- const readPos = (dir: string) => {
373
- const parsed = matter(
374
- fs.readFileSync(path.join(docsDir, dir, "index.mdx"), "utf8"),
375
- );
376
- return parsed.data.sidebar_position;
377
- };
378
-
379
- expect(readPos("claude-md")).toBe(900);
380
- expect(readPos("claude-commands")).toBe(901);
381
- expect(readPos("claude-skills")).toBe(902);
382
- expect(readPos("claude-agents")).toBe(903);
383
- });
384
-
385
- it("index.mdx has correct label as title for each sub-category", () => {
386
- generateClaudeResourcesDocs({
387
- claudeDir,
388
- projectRoot: tmpDir,
389
- docsDir,
390
- });
391
-
392
- const readTitle = (dir: string) => {
393
- const parsed = matter(
394
- fs.readFileSync(path.join(docsDir, dir, "index.mdx"), "utf8"),
395
- );
396
- return parsed.data.title;
397
- };
398
-
399
- expect(readTitle("claude-md")).toBe("CLAUDE.md");
400
- expect(readTitle("claude-commands")).toBe("Commands");
401
- expect(readTitle("claude-skills")).toBe("Skills");
402
- expect(readTitle("claude-agents")).toBe("Agents");
403
- });
404
- });
405
-
406
- // ---------------------------------------------------------------------------
407
- // Return value test
408
- // ---------------------------------------------------------------------------
409
-
410
- describe("return value", () => {
411
- it("returns correct counts", () => {
412
- const result = generateClaudeResourcesDocs({
413
- claudeDir,
414
- projectRoot: tmpDir,
415
- docsDir,
416
- });
417
-
418
- expect(result).toEqual({
419
- claudemd: 1,
420
- commands: 1,
421
- skills: 1,
422
- agents: 1,
423
- });
424
- });
425
- });
426
-
427
- // ---------------------------------------------------------------------------
428
- // Slug collision detection tests
429
- // ---------------------------------------------------------------------------
430
-
431
- describe("slug collision detection", () => {
432
- it("throws when two CLAUDE.md paths produce the same slug", () => {
433
- // foo/bar/CLAUDE.md → slug "foo--bar"
434
- // foo--bar/CLAUDE.md → slug "foo--bar" (collision)
435
- fs.mkdirSync(path.join(tmpDir, "foo", "bar"), { recursive: true });
436
- fs.writeFileSync(
437
- path.join(tmpDir, "foo", "bar", "CLAUDE.md"),
438
- "# foo/bar instructions",
439
- );
440
- fs.mkdirSync(path.join(tmpDir, "foo--bar"), { recursive: true });
441
- fs.writeFileSync(
442
- path.join(tmpDir, "foo--bar", "CLAUDE.md"),
443
- "# foo--bar instructions",
444
- );
445
-
446
- expect(() =>
447
- generateClaudeResourcesDocs({
448
- claudeDir,
449
- projectRoot: tmpDir,
450
- docsDir,
451
- }),
452
- ).toThrow(/slug collision/);
453
- });
454
-
455
- it("names both colliding source paths in the error", () => {
456
- fs.mkdirSync(path.join(tmpDir, "foo", "bar"), { recursive: true });
457
- fs.writeFileSync(
458
- path.join(tmpDir, "foo", "bar", "CLAUDE.md"),
459
- "# foo/bar instructions",
460
- );
461
- fs.mkdirSync(path.join(tmpDir, "foo--bar"), { recursive: true });
462
- fs.writeFileSync(
463
- path.join(tmpDir, "foo--bar", "CLAUDE.md"),
464
- "# foo--bar instructions",
465
- );
466
-
467
- let caughtMessage = "";
468
- try {
469
- generateClaudeResourcesDocs({
470
- claudeDir,
471
- projectRoot: tmpDir,
472
- docsDir,
473
- });
474
- } catch (e) {
475
- caughtMessage = (e as Error).message;
476
- }
477
-
478
- expect(caughtMessage).toContain("foo--bar");
479
- // Both source paths must appear in the message
480
- expect(caughtMessage).toMatch(/foo.bar.CLAUDE\.md/);
481
- expect(caughtMessage).toMatch(/foo--bar.CLAUDE\.md/);
482
- });
483
-
484
- it("does not throw for a clean tree (no collisions)", () => {
485
- // The default fixture has only root/CLAUDE.md — no collision
486
- expect(() =>
487
- generateClaudeResourcesDocs({
488
- claudeDir,
489
- projectRoot: tmpDir,
490
- docsDir,
491
- }),
492
- ).not.toThrow();
493
- });
494
- });
495
-
496
- // ---------------------------------------------------------------------------
497
- // Reserved "index" slug guard tests
498
- // ---------------------------------------------------------------------------
499
-
500
- describe("reserved index slug guard", () => {
501
- it("throws when a CLAUDE.md directory maps to the reserved index slug", () => {
502
- // index/CLAUDE.md → slug "index" — reserved for category index.mdx
503
- fs.mkdirSync(path.join(tmpDir, "index"), { recursive: true });
504
- fs.writeFileSync(
505
- path.join(tmpDir, "index", "CLAUDE.md"),
506
- "# index dir instructions",
507
- );
508
-
509
- expect(() =>
510
- generateClaudeResourcesDocs({
511
- claudeDir,
512
- projectRoot: tmpDir,
513
- docsDir,
514
- }),
515
- ).toThrow(/reserved slug "index"/);
516
- });
517
-
518
- it("throws when a command file is named index.md", () => {
519
- fs.writeFileSync(
520
- path.join(claudeDir, "commands", "index.md"),
521
- '---\ndescription: "Index command"\n---\n\nIndex body.',
522
- );
523
-
524
- expect(() =>
525
- generateClaudeResourcesDocs({
526
- claudeDir,
527
- projectRoot: tmpDir,
528
- docsDir,
529
- }),
530
- ).toThrow(/reserved name "index"/);
531
- });
532
-
533
- it("throws when a skill directory is named index", () => {
534
- const indexSkillDir = path.join(claudeDir, "skills", "index");
535
- fs.mkdirSync(indexSkillDir, { recursive: true });
536
- fs.writeFileSync(
537
- path.join(indexSkillDir, "SKILL.md"),
538
- '---\nname: index\ndescription: "Index skill"\n---\n\nIndex skill body.',
539
- );
540
-
541
- expect(() =>
542
- generateClaudeResourcesDocs({
543
- claudeDir,
544
- projectRoot: tmpDir,
545
- docsDir,
546
- }),
547
- ).toThrow(/reserved name "index"/);
548
- });
549
-
550
- it("throws when an agent file is named index.md", () => {
551
- fs.writeFileSync(
552
- path.join(claudeDir, "agents", "index.md"),
553
- '---\nname: index-agent\ndescription: "Index agent"\nmodel: sonnet\n---\n\nIndex agent body.',
554
- );
555
-
556
- expect(() =>
557
- generateClaudeResourcesDocs({
558
- claudeDir,
559
- projectRoot: tmpDir,
560
- docsDir,
561
- }),
562
- ).toThrow(/reserved name "index"/);
563
- });
564
- });
565
-
566
- // ---------------------------------------------------------------------------
567
- // projectRoot default scope regression test
568
- // ---------------------------------------------------------------------------
569
-
570
- describe("projectRoot default scope", () => {
571
- it("walks claudeDir (not its parent) when projectRoot is omitted", () => {
572
- // The fixture already writes tmpDir/CLAUDE.md (outside claudeDir).
573
- // With the old bug (projectRoot = path.dirname(claudeDir) = tmpDir), the
574
- // walk starts at tmpDir and picks up tmpDir/CLAUDE.md. With the fix
575
- // (projectRoot = claudeDir), the walk starts inside claudeDir and does NOT
576
- // reach tmpDir/CLAUDE.md (parent dirs are never walked upward).
577
- //
578
- // To distinguish, we write a CLAUDE.md inside claudeDir with unique content
579
- // and verify the generated page body matches IT, not the outside one.
580
- fs.writeFileSync(
581
- path.join(claudeDir, "CLAUDE.md"),
582
- "# Claude dir unique content xyz123",
583
- );
584
-
585
- // Call with NO projectRoot.
586
- generateClaudeResourcesDocs({ claudeDir, docsDir });
587
-
588
- // The generated root.mdx body must contain the inside-claudeDir content.
589
- const claudeMdDir = path.join(docsDir, "claude-md");
590
- const rootMdx = fs.readFileSync(path.join(claudeMdDir, "root.mdx"), "utf8");
591
- expect(rootMdx).toContain("xyz123");
592
-
593
- // And must NOT contain the outside content (from tmpDir/CLAUDE.md).
594
- expect(rootMdx).not.toContain("Project instructions");
595
- });
596
- });
597
-
598
- // ---------------------------------------------------------------------------
599
- // excludeDirs boundary matching (#2561)
600
- // ---------------------------------------------------------------------------
601
-
602
- describe("excludeDirs boundary matching", () => {
603
- it("discovers CLAUDE.md in a prefix-colliding sibling of an excluded dir (dist-extra vs dist)", () => {
604
- fs.mkdirSync(path.join(tmpDir, "dist-extra"), { recursive: true });
605
- fs.writeFileSync(
606
- path.join(tmpDir, "dist-extra", "CLAUDE.md"),
607
- "# dist-extra instructions",
608
- );
609
-
610
- const result = generateClaudeResourcesDocs({
611
- claudeDir,
612
- projectRoot: tmpDir,
613
- docsDir,
614
- });
615
-
616
- // root/CLAUDE.md (from the fixture) + dist-extra/CLAUDE.md
617
- expect(result.claudemd).toBe(2);
618
- const decoyPage = path.join(docsDir, "claude-md", "dist-extra.mdx");
619
- expect(fs.existsSync(decoyPage)).toBe(true);
620
- expect(fs.readFileSync(decoyPage, "utf8")).toContain("dist-extra instructions");
621
- });
622
-
623
- it("still excludes CLAUDE.md directly inside the excluded dist directory", () => {
624
- fs.mkdirSync(path.join(tmpDir, "dist"), { recursive: true });
625
- fs.writeFileSync(
626
- path.join(tmpDir, "dist", "CLAUDE.md"),
627
- "# dist instructions — should be excluded",
628
- );
629
-
630
- const result = generateClaudeResourcesDocs({
631
- claudeDir,
632
- projectRoot: tmpDir,
633
- docsDir,
634
- });
635
-
636
- // Only root/CLAUDE.md (from the fixture) — dist/CLAUDE.md is excluded.
637
- expect(result.claudemd).toBe(1);
638
- expect(fs.existsSync(path.join(docsDir, "claude-md", "dist.mdx"))).toBe(false);
639
- });
640
-
641
- it("still excludes nested CLAUDE.md files under the excluded dist directory (exact-match boundary holds)", () => {
642
- fs.mkdirSync(path.join(tmpDir, "dist", "nested"), { recursive: true });
643
- fs.writeFileSync(
644
- path.join(tmpDir, "dist", "nested", "CLAUDE.md"),
645
- "# nested dist instructions — should be excluded",
646
- );
647
-
648
- const result = generateClaudeResourcesDocs({
649
- claudeDir,
650
- projectRoot: tmpDir,
651
- docsDir,
652
- });
653
-
654
- // Only root/CLAUDE.md (from the fixture) — the whole dist/ subtree,
655
- // including nested dirs, is excluded once the top-level dist match hits.
656
- expect(result.claudemd).toBe(1);
657
- expect(fs.existsSync(path.join(docsDir, "claude-md", "dist--nested.mdx"))).toBe(false);
658
- });
659
-
660
- it("still excludes docsDir contents when docsDir carries a trailing separator", () => {
661
- // path.join preserves a trailing separator on a single argument, so the
662
- // docsDir exclude entry keeps it — the boundary compare must not break.
663
- fs.writeFileSync(
664
- path.join(docsDir, "CLAUDE.md"),
665
- "# decoy inside docsDir — should be excluded",
666
- );
667
-
668
- const result = generateClaudeResourcesDocs({
669
- claudeDir,
670
- projectRoot: tmpDir,
671
- docsDir: docsDir + path.sep,
672
- });
673
-
674
- // Only root/CLAUDE.md (from the fixture) — docs/CLAUDE.md is excluded.
675
- expect(result.claudemd).toBe(1);
676
- expect(fs.existsSync(path.join(docsDir, "claude-md", "docs.mdx"))).toBe(false);
677
- });
678
- });
679
-
680
- // ---------------------------------------------------------------------------
681
- // CLAUDE.md repo-relative link downgrade (#2411, Class B)
682
- // ---------------------------------------------------------------------------
683
-
684
- describe("CLAUDE.md repo-relative link downgrade", () => {
685
- const claudeMdBody = [
686
- "# Project",
687
- "",
688
- "See [wrangler.toml](./wrangler.toml) and [schema](../../schema/photos.sql).",
689
- "Also a [workflow](../../.github/workflows/deploy.yml) and a [bare](docs/guide.md) link.",
690
- "External [site](https://example.com), [anchor](#section), [site-abs](/docs/getting-started), [mail](mailto:x@y.com).",
691
- "Image: ![diagram](./diagram.png)",
692
- "",
693
- "Inline code stays: `[x](./y)`",
694
- "",
695
- "```",
696
- "[code](./should-not-change.md)",
697
- "```",
698
- "",
699
- "~~~",
700
- "[tilde-code](./also-should-not-change.md)",
701
- "~~~",
702
- "",
703
- ].join("\n");
704
-
705
- function genRoot() {
706
- fs.writeFileSync(path.join(tmpDir, "CLAUDE.md"), claudeMdBody);
707
- generateClaudeResourcesDocs({ claudeDir, projectRoot: tmpDir, docsDir });
708
- return fs.readFileSync(
709
- path.join(docsDir, "claude-md", "root.mdx"),
710
- "utf8",
711
- );
712
- }
713
-
714
- it("downgrades repo-relative file links to inline code", () => {
715
- const rootMdx = genRoot();
716
- expect(rootMdx).toContain("`wrangler.toml`");
717
- expect(rootMdx).toContain("`schema`");
718
- expect(rootMdx).toContain("`workflow`");
719
- expect(rootMdx).toContain("`bare`");
720
- // The dangling hrefs must be gone.
721
- expect(rootMdx).not.toContain("](./wrangler.toml)");
722
- expect(rootMdx).not.toContain("](../../schema/photos.sql)");
723
- expect(rootMdx).not.toContain("](../../.github/workflows/deploy.yml)");
724
- expect(rootMdx).not.toContain("](docs/guide.md)");
725
- });
726
-
727
- it("preserves resolvable links (external, anchor, site-absolute, scheme)", () => {
728
- const rootMdx = genRoot();
729
- expect(rootMdx).toContain("](https://example.com)");
730
- expect(rootMdx).toContain("](#section)");
731
- expect(rootMdx).toContain("](/docs/getting-started)");
732
- expect(rootMdx).toContain("](mailto:x@y.com)");
733
- });
734
-
735
- it("downgrades repo-relative image links without leaving a stray '!'", () => {
736
- const rootMdx = genRoot();
737
- expect(rootMdx).toContain("`diagram`");
738
- expect(rootMdx).not.toContain("](./diagram.png)");
739
- expect(rootMdx).not.toContain("!`diagram`");
740
- });
741
-
742
- it("does not rewrite links inside inline code or fenced code blocks", () => {
743
- const rootMdx = genRoot();
744
- // Inline-code span is preserved verbatim.
745
- expect(rootMdx).toContain("`[x](./y)`");
746
- // Backtick-fenced code block content is preserved verbatim.
747
- expect(rootMdx).toContain("[code](./should-not-change.md)");
748
- // Tilde-fenced code block content is preserved verbatim too.
749
- expect(rootMdx).toContain("[tilde-code](./also-should-not-change.md)");
750
- });
751
- });
752
- });