create-zudo-doc 0.1.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 (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/bin/create-zudo-doc.js +2 -0
  4. package/dist/api.d.ts +20 -0
  5. package/dist/api.js +13 -0
  6. package/dist/claude-md-gen.d.ts +2 -0
  7. package/dist/claude-md-gen.js +113 -0
  8. package/dist/cli.d.ts +39 -0
  9. package/dist/cli.js +157 -0
  10. package/dist/compose.d.ts +95 -0
  11. package/dist/compose.js +206 -0
  12. package/dist/constants.d.ts +20 -0
  13. package/dist/constants.js +224 -0
  14. package/dist/features/body-foot-util.d.ts +10 -0
  15. package/dist/features/body-foot-util.js +12 -0
  16. package/dist/features/claude-resources.d.ts +2 -0
  17. package/dist/features/claude-resources.js +6 -0
  18. package/dist/features/design-token-panel.d.ts +14 -0
  19. package/dist/features/design-token-panel.js +27 -0
  20. package/dist/features/doc-history.d.ts +9 -0
  21. package/dist/features/doc-history.js +11 -0
  22. package/dist/features/doc-tags.d.ts +19 -0
  23. package/dist/features/doc-tags.js +33 -0
  24. package/dist/features/footer-taglist.d.ts +14 -0
  25. package/dist/features/footer-taglist.js +17 -0
  26. package/dist/features/footer.d.ts +8 -0
  27. package/dist/features/footer.js +10 -0
  28. package/dist/features/i18n.d.ts +22 -0
  29. package/dist/features/i18n.js +41 -0
  30. package/dist/features/image-enlarge.d.ts +11 -0
  31. package/dist/features/image-enlarge.js +13 -0
  32. package/dist/features/index.d.ts +15 -0
  33. package/dist/features/index.js +53 -0
  34. package/dist/features/llms-txt.d.ts +11 -0
  35. package/dist/features/llms-txt.js +13 -0
  36. package/dist/features/search.d.ts +9 -0
  37. package/dist/features/search.js +11 -0
  38. package/dist/features/sidebar-resizer.d.ts +14 -0
  39. package/dist/features/sidebar-resizer.js +16 -0
  40. package/dist/features/sidebar-toggle.d.ts +13 -0
  41. package/dist/features/sidebar-toggle.js +15 -0
  42. package/dist/features/tag-governance.d.ts +14 -0
  43. package/dist/features/tag-governance.js +16 -0
  44. package/dist/features/tauri-dev.d.ts +2 -0
  45. package/dist/features/tauri-dev.js +25 -0
  46. package/dist/features/tauri.d.ts +11 -0
  47. package/dist/features/tauri.js +52 -0
  48. package/dist/features/versioning.d.ts +27 -0
  49. package/dist/features/versioning.js +43 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +150 -0
  52. package/dist/preset.d.ts +37 -0
  53. package/dist/preset.js +156 -0
  54. package/dist/prompts.d.ts +32 -0
  55. package/dist/prompts.js +248 -0
  56. package/dist/scaffold.d.ts +4 -0
  57. package/dist/scaffold.js +344 -0
  58. package/dist/settings-gen.d.ts +2 -0
  59. package/dist/settings-gen.js +237 -0
  60. package/dist/utils.d.ts +8 -0
  61. package/dist/utils.js +34 -0
  62. package/dist/zfb-config-gen.d.ts +19 -0
  63. package/dist/zfb-config-gen.js +222 -0
  64. package/package.json +65 -0
  65. package/templates/base/.htmlvalidate.json +5 -0
  66. package/templates/base/.zfb/doc-history-meta.json +1 -0
  67. package/templates/base/pages/404.tsx +55 -0
  68. package/templates/base/pages/_data.ts +179 -0
  69. package/templates/base/pages/_mdx-components.ts +249 -0
  70. package/templates/base/pages/docs/[...slug].tsx +448 -0
  71. package/templates/base/pages/index.tsx +158 -0
  72. package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
  73. package/templates/base/pages/lib/_category-nav.tsx +148 -0
  74. package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
  75. package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
  76. package/templates/base/pages/lib/_details.tsx +30 -0
  77. package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
  78. package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
  79. package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
  80. package/templates/base/pages/lib/_extract-headings.ts +81 -0
  81. package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
  82. package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
  83. package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
  84. package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
  85. package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
  86. package/templates/base/pages/lib/_math-block.tsx +63 -0
  87. package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
  88. package/templates/base/pages/lib/_preset-generator.tsx +81 -0
  89. package/templates/base/pages/lib/_search-widget-script.ts +388 -0
  90. package/templates/base/pages/lib/_search-widget.tsx +196 -0
  91. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
  92. package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
  93. package/templates/base/pages/lib/locale-merge.ts +58 -0
  94. package/templates/base/pages/lib/route-enumerators.ts +302 -0
  95. package/templates/base/pages/sitemap.xml.tsx +51 -0
  96. package/templates/base/plugins/connect-adapter.mjs +144 -0
  97. package/templates/base/plugins/copy-public-plugin.mjs +50 -0
  98. package/templates/base/plugins/search-index-plugin.mjs +54 -0
  99. package/templates/base/scripts/run-b4push.sh +102 -0
  100. package/templates/base/src/components/ai-chat-modal.tsx +15 -0
  101. package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
  102. package/templates/base/src/components/content/component-map.ts +25 -0
  103. package/templates/base/src/components/content/content-blockquote.tsx +16 -0
  104. package/templates/base/src/components/content/content-code.tsx +117 -0
  105. package/templates/base/src/components/content/content-link.tsx +83 -0
  106. package/templates/base/src/components/content/content-ol.tsx +19 -0
  107. package/templates/base/src/components/content/content-paragraph.tsx +10 -0
  108. package/templates/base/src/components/content/content-strong.tsx +16 -0
  109. package/templates/base/src/components/content/content-table.tsx +18 -0
  110. package/templates/base/src/components/content/content-ul.tsx +18 -0
  111. package/templates/base/src/components/content/heading-h2.tsx +26 -0
  112. package/templates/base/src/components/content/heading-h3.tsx +26 -0
  113. package/templates/base/src/components/content/heading-h4.tsx +26 -0
  114. package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
  115. package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
  116. package/templates/base/src/components/doc-history.tsx +18 -0
  117. package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
  118. package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
  119. package/templates/base/src/components/html-preview/preflight.ts +112 -0
  120. package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
  121. package/templates/base/src/components/image-enlarge.tsx +19 -0
  122. package/templates/base/src/components/mobile-toc.tsx +94 -0
  123. package/templates/base/src/components/preset-generator.tsx +14 -0
  124. package/templates/base/src/components/sidebar-toggle.tsx +98 -0
  125. package/templates/base/src/components/sidebar-tree.tsx +543 -0
  126. package/templates/base/src/components/site-tree-nav.tsx +233 -0
  127. package/templates/base/src/components/theme-toggle.tsx +93 -0
  128. package/templates/base/src/components/toc.tsx +63 -0
  129. package/templates/base/src/components/tree-nav-shared.tsx +71 -0
  130. package/templates/base/src/config/color-scheme-utils.ts +182 -0
  131. package/templates/base/src/config/color-schemes.ts +128 -0
  132. package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
  133. package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
  134. package/templates/base/src/config/i18n.ts +225 -0
  135. package/templates/base/src/config/settings-types.ts +162 -0
  136. package/templates/base/src/config/sidebars.ts +66 -0
  137. package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
  138. package/templates/base/src/config/tag-vocabulary.ts +20 -0
  139. package/templates/base/src/hooks/use-active-heading.ts +133 -0
  140. package/templates/base/src/plugins/docs-source-map.ts +103 -0
  141. package/templates/base/src/plugins/hast-utils.ts +10 -0
  142. package/templates/base/src/plugins/rehype-code-title.ts +50 -0
  143. package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
  144. package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
  145. package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
  146. package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
  147. package/templates/base/src/plugins/remark-admonitions.ts +99 -0
  148. package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
  149. package/templates/base/src/plugins/url-utils.ts +4 -0
  150. package/templates/base/src/styles/global.css +1066 -0
  151. package/templates/base/src/types/docs-entry.ts +39 -0
  152. package/templates/base/src/types/heading.ts +5 -0
  153. package/templates/base/src/types/locale.ts +10 -0
  154. package/templates/base/src/utils/base.ts +139 -0
  155. package/templates/base/src/utils/content-files.ts +106 -0
  156. package/templates/base/src/utils/dedent.ts +24 -0
  157. package/templates/base/src/utils/docs.ts +335 -0
  158. package/templates/base/src/utils/git-info.ts +70 -0
  159. package/templates/base/src/utils/github.ts +19 -0
  160. package/templates/base/src/utils/header-right-items.ts +38 -0
  161. package/templates/base/src/utils/nav-scope.ts +63 -0
  162. package/templates/base/src/utils/sidebar.ts +104 -0
  163. package/templates/base/src/utils/slug.ts +10 -0
  164. package/templates/base/src/utils/smart-break.tsx +126 -0
  165. package/templates/base/src/utils/tags.ts +126 -0
  166. package/templates/base/tsconfig.json +36 -0
  167. package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
  168. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
  169. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
  170. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
  171. package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
  172. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
  173. package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
  174. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
  175. package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
  176. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
  177. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
  178. package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
  179. package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
  180. package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
  181. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
  182. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
  183. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
  184. package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
  185. package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
  186. package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
  187. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
  188. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
  189. package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
  190. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
  191. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
  192. package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
  193. package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
  194. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
  195. package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
  196. package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
  197. package/templates/features/tauri/files/src-tauri/build.rs +3 -0
  198. package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
  199. package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
  200. package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
  201. package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
  202. package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
  203. package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
  204. package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
  205. package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
  206. package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
  207. package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
  208. package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
  209. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
  210. package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
  211. package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
  212. package/templates/features/versioning/files/pages/v/[version]/ja/docs/[...slug].tsx +490 -0
@@ -0,0 +1,137 @@
1
+ // zfb plugin module: claude-resources.
2
+ //
3
+ // Wires `runClaudeResourcesPreStep` (from
4
+ // `@takazudo/zudo-doc/integrations/claude-resources`) into zfb's
5
+ // `preBuild` lifecycle hook. Replaces the npm `prebuild`-script glue
6
+ // in `scripts/zfb-prebuild.mjs` for this step (the script remains in
7
+ // place during the merge window — T6 retires it).
8
+ //
9
+ // Why this shim exists (and is not the v2 integration module directly):
10
+ //
11
+ // 1. zfb's plugin host runs `node` without any TypeScript loader, so
12
+ // it cannot import `.ts` source files. The v2 integration package
13
+ // (`@takazudo/zudo-doc/integrations/claude-resources`) currently
14
+ // ships only TypeScript source through its `exports` map and has
15
+ // no build step.
16
+ //
17
+ // 2. The `runClaudeResourcesPreStep` runner pulls in `gray-matter`,
18
+ // which performs a CJS `require("fs")` that esbuild's
19
+ // configuration-loader bundle (ESM-only) cannot satisfy. Loading
20
+ // it inline at the top of `zfb.config.ts` therefore breaks config
21
+ // parsing entirely.
22
+ //
23
+ // Both problems are solved by isolating the runner behind a child
24
+ // process: this shim spawns `tsx` (the project's existing TS-aware
25
+ // Node runner, pinned via `tsx` in `package.json`) on a tiny inline
26
+ // script that imports the runner. tsx handles `.ts` resolution and
27
+ // Node's CJS↔ESM interop for gray-matter; the parent plugin host stays
28
+ // in plain Node and only sees a child-process exit code.
29
+ //
30
+ // Inline functions are not supported by zfb's plugin runtime — see
31
+ // `@takazudo/zfb/plugins` source comment. This file is the plugin-host
32
+ // equivalent once the npm script is retired (T6).
33
+
34
+ import { spawn } from "node:child_process";
35
+ import { fileURLToPath } from "node:url";
36
+ import { dirname, resolve } from "node:path";
37
+
38
+ const PLUGIN_NAME = "@takazudo/zudo-doc-claude-resources";
39
+
40
+ // `tsx` is a workspace dependency of the host project and resolves to
41
+ // `<projectRoot>/node_modules/.bin/tsx` from this file. We resolve the
42
+ // binary explicitly so the shim does not depend on the parent shell's
43
+ // `PATH` (the plugin host is spawned by zfb without sourcing the
44
+ // user's profile — Node's `PATH` is whatever zfb itself was launched
45
+ // with).
46
+ const HERE = dirname(fileURLToPath(import.meta.url));
47
+ const TSX_BIN = resolve(HERE, "..", "node_modules", ".bin", "tsx");
48
+
49
+ /**
50
+ * Run the v2 claude-resources runner under tsx.
51
+ *
52
+ * Inlines a minimal ESM script via `tsx -e` so we don't have to ship a
53
+ * second `.ts` file just to host the call site. The runner returns a
54
+ * `{ claudemd, commands, skills, agents }` summary which we re-emit on
55
+ * the child's stdout — the parent (this shim) parses it and forwards
56
+ * the summary to the plugin host's logger.
57
+ */
58
+ function runRunnerUnderTsx({ claudeDir, projectRoot, docsDir }) {
59
+ // The runner accepts `projectRoot` and `docsDir` as relative paths
60
+ // (resolved against `process.cwd()` in the runner). To insulate the
61
+ // child from whatever cwd zfb spawned us with, we set cwd explicitly
62
+ // and pass absolute paths through.
63
+ // tsx's `-e` flag emits CJS by default (it picks the format from the
64
+ // entry's extension, and an inline `-e` script has none), so we wrap
65
+ // the body in an `async`-IIFE rather than relying on top-level await.
66
+ const childScript = `
67
+ (async () => {
68
+ const { runClaudeResourcesPreStep } = await import("@takazudo/zudo-doc/integrations/claude-resources");
69
+ const result = await runClaudeResourcesPreStep({
70
+ claudeDir: ${JSON.stringify(claudeDir)},
71
+ projectRoot: ${JSON.stringify(projectRoot)},
72
+ docsDir: ${JSON.stringify(docsDir)},
73
+ });
74
+ process.stdout.write(JSON.stringify(result));
75
+ })().catch((err) => {
76
+ process.stderr.write(err && err.stack ? err.stack : String(err));
77
+ process.exit(1);
78
+ });
79
+ `;
80
+
81
+ return new Promise((resolveCall, reject) => {
82
+ const child = spawn(TSX_BIN, ["-e", childScript], {
83
+ cwd: projectRoot,
84
+ stdio: ["ignore", "pipe", "pipe"],
85
+ });
86
+ let stdout = "";
87
+ let stderr = "";
88
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
89
+ child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
90
+ child.on("error", reject);
91
+ child.on("close", (code) => {
92
+ if (code !== 0) {
93
+ reject(
94
+ new Error(
95
+ `[${PLUGIN_NAME}] runner exited with code ${code}\n${stderr}`,
96
+ ),
97
+ );
98
+ return;
99
+ }
100
+ try {
101
+ resolveCall(JSON.parse(stdout));
102
+ } catch (err) {
103
+ reject(
104
+ new Error(
105
+ `[${PLUGIN_NAME}] failed to parse runner stdout: ${err.message}\nstdout: ${stdout}\nstderr: ${stderr}`,
106
+ ),
107
+ );
108
+ }
109
+ });
110
+ });
111
+ }
112
+
113
+ export default {
114
+ name: PLUGIN_NAME,
115
+ async preBuild(ctx) {
116
+ const claudeDir = ctx.options.claudeDir;
117
+ if (typeof claudeDir !== "string" || claudeDir.length === 0) {
118
+ throw new Error(
119
+ `[${PLUGIN_NAME}] preBuild: options.claudeDir must be a non-empty string (got ${JSON.stringify(claudeDir)})`,
120
+ );
121
+ }
122
+ const projectRootOpt = ctx.options.projectRoot;
123
+ const docsDirOpt = ctx.options.docsDir;
124
+ const result = await runRunnerUnderTsx({
125
+ claudeDir,
126
+ projectRoot:
127
+ typeof projectRootOpt === "string" ? projectRootOpt : ctx.projectRoot,
128
+ docsDir: typeof docsDirOpt === "string" ? docsDirOpt : "src/content/docs",
129
+ });
130
+ // Surface a one-line summary so build logs make the migration
131
+ // observable (mirrors the `[zfb-prebuild]` lines from the legacy
132
+ // npm-script glue).
133
+ ctx.logger.info(
134
+ `claude-resources: ${result.claudemd} CLAUDE.md, ${result.commands} commands, ${result.skills} skills, ${result.agents} agents`,
135
+ );
136
+ },
137
+ };
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { escapeForMdx } from "../escape-for-mdx";
3
+
4
+ describe("escapeForMdx", () => {
5
+ // ---------------------------------------------------------------------------
6
+ // Path-like angle bracket sequences (not self-closing JSX)
7
+ // ---------------------------------------------------------------------------
8
+
9
+ it("escapes path-like <name>/suffix sequences", () => {
10
+ expect(escapeForMdx("<foo>/bar")).toBe("&lt;foo&gt;/bar");
11
+ });
12
+
13
+ it("escapes path-like angle brackets in prose", () => {
14
+ expect(escapeForMdx("binary at <repo>/target/release/zfb")).toBe(
15
+ "binary at &lt;repo&gt;/target/release/zfb",
16
+ );
17
+ });
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Allowlisted HTML tags must pass through unchanged
21
+ // ---------------------------------------------------------------------------
22
+
23
+ it("preserves allowlisted HTML tags followed by a path separator", () => {
24
+ expect(escapeForMdx("<div>/path")).toBe("<div>/path");
25
+ });
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Self-closing non-allowlisted tags must still be escaped
29
+ // ---------------------------------------------------------------------------
30
+
31
+ it("escapes self-closing non-allowlisted component tags", () => {
32
+ expect(escapeForMdx("<Component />")).toMatch(/&lt;Component\s*\/&gt;/);
33
+ });
34
+ });
@@ -0,0 +1,376 @@
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 _category_.json with noPage 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 catPath = path.join(docsDir, dir, "_category_.json");
104
+ expect(fs.existsSync(catPath)).toBe(true);
105
+
106
+ const cat = JSON.parse(fs.readFileSync(catPath, "utf8"));
107
+ expect(cat).toHaveProperty("label");
108
+ expect(cat).toHaveProperty("position");
109
+ expect(cat).toHaveProperty("description");
110
+ expect(cat.noPage).toBe(true);
111
+ }
112
+ });
113
+
114
+ it("generates skill as flat .mdx file", () => {
115
+ generateClaudeResourcesDocs({
116
+ claudeDir,
117
+ projectRoot: tmpDir,
118
+ docsDir,
119
+ });
120
+
121
+ const flatPath = path.join(docsDir, "claude-skills", "test-skill.mdx");
122
+ expect(fs.existsSync(flatPath)).toBe(true);
123
+ });
124
+ });
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Content tests
128
+ // ---------------------------------------------------------------------------
129
+
130
+ describe("content", () => {
131
+ it("generates overview page with CategoryNav", () => {
132
+ generateClaudeResourcesDocs({
133
+ claudeDir,
134
+ projectRoot: tmpDir,
135
+ docsDir,
136
+ });
137
+
138
+ const overview = fs.readFileSync(
139
+ path.join(docsDir, "claude", "index.mdx"),
140
+ "utf8",
141
+ );
142
+ expect(overview).toContain('<CategoryNav categories={');
143
+ });
144
+
145
+ it("skill page has correct frontmatter", () => {
146
+ generateClaudeResourcesDocs({
147
+ claudeDir,
148
+ projectRoot: tmpDir,
149
+ docsDir,
150
+ });
151
+
152
+ const skillPage = fs.readFileSync(
153
+ path.join(docsDir, "claude-skills", "test-skill.mdx"),
154
+ "utf8",
155
+ );
156
+ const parsed = matter(skillPage);
157
+
158
+ expect(parsed.data.title).toBe("test-skill");
159
+ expect(parsed.data.description).toBe("A test skill");
160
+ expect(parsed.data.sidebar_label).toBe("test-skill");
161
+ });
162
+
163
+ it("skill page has file tree", () => {
164
+ generateClaudeResourcesDocs({
165
+ claudeDir,
166
+ projectRoot: tmpDir,
167
+ docsDir,
168
+ });
169
+
170
+ const skillPage = fs.readFileSync(
171
+ path.join(docsDir, "claude-skills", "test-skill.mdx"),
172
+ "utf8",
173
+ );
174
+
175
+ // Should contain tree-drawing characters
176
+ expect(skillPage).toContain("├── ");
177
+ expect(skillPage).toContain("└── ");
178
+ expect(skillPage).toContain("test-skill/");
179
+ expect(skillPage).toContain("SKILL.md");
180
+ });
181
+
182
+ it("skill page has links to sub-files that resolve correctly from the page URL", () => {
183
+ generateClaudeResourcesDocs({
184
+ claudeDir,
185
+ projectRoot: tmpDir,
186
+ docsDir,
187
+ });
188
+
189
+ const skillPage = fs.readFileSync(
190
+ path.join(docsDir, "claude-skills", "test-skill.mdx"),
191
+ "utf8",
192
+ );
193
+
194
+ // Links use ./<subpage> format (relative to the skill page URL which
195
+ // already includes the skill dir, e.g. /docs/claude-skills/test-skill/)
196
+ expect(skillPage).toContain("./ref-guide");
197
+ expect(skillPage).toContain("./asset-template");
198
+
199
+ // Must NOT contain the double-dir pattern ./<dir>/<subpage>
200
+ expect(skillPage).not.toContain("./test-skill/ref-guide");
201
+ expect(skillPage).not.toContain("./test-skill/asset-template");
202
+
203
+ // Each linked sub-page must exist as a generated flat .mdx file
204
+ // The file is flat (test-skill--ref-guide.mdx) but slug is nested
205
+ const linkPattern = /\]\(\.\/([\w-]+)\)/g;
206
+ let match;
207
+ while ((match = linkPattern.exec(skillPage)) !== null) {
208
+ const subPage = match[1];
209
+ const targetFile = path.join(
210
+ docsDir,
211
+ "claude-skills",
212
+ `test-skill--${subPage}.mdx`,
213
+ );
214
+ expect(
215
+ fs.existsSync(targetFile),
216
+ `Link target "test-skill--${subPage}.mdx" should exist`,
217
+ ).toBe(true);
218
+ }
219
+ });
220
+
221
+ it("skill body references/scripts/assets links are rewritten to doc site format", () => {
222
+ generateClaudeResourcesDocs({
223
+ claudeDir,
224
+ projectRoot: tmpDir,
225
+ docsDir,
226
+ });
227
+
228
+ const skillPage = fs.readFileSync(
229
+ path.join(docsDir, "claude-skills", "test-skill.mdx"),
230
+ "utf8",
231
+ );
232
+
233
+ // Body links like (references/guide.md) should be rewritten to (./ref-guide)
234
+ expect(skillPage).toContain("](./ref-guide)");
235
+ expect(skillPage).not.toContain("](references/guide.md)");
236
+ });
237
+
238
+ it("agent page has model badge", () => {
239
+ generateClaudeResourcesDocs({
240
+ claudeDir,
241
+ projectRoot: tmpDir,
242
+ docsDir,
243
+ });
244
+
245
+ const agentPage = fs.readFileSync(
246
+ path.join(docsDir, "claude-agents", "test-agent.mdx"),
247
+ "utf8",
248
+ );
249
+ expect(agentPage).toContain("**Model:** `sonnet`");
250
+ });
251
+ });
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Sub-file page tests
255
+ // ---------------------------------------------------------------------------
256
+
257
+ describe("sub-file pages", () => {
258
+ it("generates unlisted reference page", () => {
259
+ generateClaudeResourcesDocs({
260
+ claudeDir,
261
+ projectRoot: tmpDir,
262
+ docsDir,
263
+ });
264
+
265
+ const refPage = path.join(docsDir, "claude-skills", "test-skill--ref-guide.mdx");
266
+ expect(fs.existsSync(refPage)).toBe(true);
267
+
268
+ const parsed = matter(fs.readFileSync(refPage, "utf8"));
269
+ expect(parsed.data.unlisted).toBe(true);
270
+ });
271
+
272
+ it("generates unlisted asset page for .md files", () => {
273
+ generateClaudeResourcesDocs({
274
+ claudeDir,
275
+ projectRoot: tmpDir,
276
+ docsDir,
277
+ });
278
+
279
+ const assetPage = path.join(docsDir, "claude-skills", "test-skill--asset-template.mdx");
280
+ expect(fs.existsSync(assetPage)).toBe(true);
281
+
282
+ const parsed = matter(fs.readFileSync(assetPage, "utf8"));
283
+ expect(parsed.data.unlisted).toBe(true);
284
+ });
285
+
286
+ it("does NOT generate page for non-.md scripts", () => {
287
+ generateClaudeResourcesDocs({
288
+ claudeDir,
289
+ projectRoot: tmpDir,
290
+ docsDir,
291
+ });
292
+
293
+ const scriptPage = path.join(docsDir, "claude-skills", "test-skill--script-run.mdx");
294
+ expect(fs.existsSync(scriptPage)).toBe(false);
295
+ });
296
+
297
+ it("sub-pages have custom slug for nested breadcrumbs", () => {
298
+ generateClaudeResourcesDocs({
299
+ claudeDir,
300
+ projectRoot: tmpDir,
301
+ docsDir,
302
+ });
303
+
304
+ const refPage = fs.readFileSync(
305
+ path.join(docsDir, "claude-skills", "test-skill--ref-guide.mdx"),
306
+ "utf8",
307
+ );
308
+ const parsed = matter(refPage);
309
+ expect(parsed.data.slug).toBe("claude-skills/test-skill/ref-guide");
310
+ });
311
+
312
+ it("reference page content is correct", () => {
313
+ generateClaudeResourcesDocs({
314
+ claudeDir,
315
+ projectRoot: tmpDir,
316
+ docsDir,
317
+ });
318
+
319
+ const refPage = fs.readFileSync(
320
+ path.join(docsDir, "claude-skills", "test-skill--ref-guide.mdx"),
321
+ "utf8",
322
+ );
323
+ const parsed = matter(refPage);
324
+
325
+ expect(parsed.data.title).toBe("Guide");
326
+ expect(parsed.content).toContain("Some guide content");
327
+ });
328
+ });
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Category metadata tests
332
+ // ---------------------------------------------------------------------------
333
+
334
+ describe("category metadata", () => {
335
+ it("_category_.json positions are ordered correctly", () => {
336
+ generateClaudeResourcesDocs({
337
+ claudeDir,
338
+ projectRoot: tmpDir,
339
+ docsDir,
340
+ });
341
+
342
+ const readPos = (dir: string) => {
343
+ const cat = JSON.parse(
344
+ fs.readFileSync(path.join(docsDir, dir, "_category_.json"), "utf8"),
345
+ );
346
+ return cat.position;
347
+ };
348
+
349
+ expect(readPos("claude-md")).toBe(900);
350
+ expect(readPos("claude-commands")).toBe(901);
351
+ expect(readPos("claude-skills")).toBe(902);
352
+ expect(readPos("claude-agents")).toBe(903);
353
+ });
354
+ });
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Return value test
358
+ // ---------------------------------------------------------------------------
359
+
360
+ describe("return value", () => {
361
+ it("returns correct counts", () => {
362
+ const result = generateClaudeResourcesDocs({
363
+ claudeDir,
364
+ projectRoot: tmpDir,
365
+ docsDir,
366
+ });
367
+
368
+ expect(result).toEqual({
369
+ claudemd: 1,
370
+ commands: 1,
371
+ skills: 1,
372
+ agents: 1,
373
+ });
374
+ });
375
+ });
376
+ });
@@ -0,0 +1,93 @@
1
+ const htmlTags = new Set([
2
+ "div", "span", "p", "a", "img", "br", "hr", "ul", "ol", "li",
3
+ "h1", "h2", "h3", "h4", "h5", "h6", "code", "pre", "blockquote",
4
+ "table", "tr", "td", "th", "thead", "tbody", "tfoot", "colgroup", "col",
5
+ "strong", "em", "b", "i", "u", "s", "del", "ins", "sub", "sup",
6
+ "details", "summary", "figure", "figcaption", "mark", "small",
7
+ "cite", "q", "abbr", "dfn", "time", "var", "samp", "kbd",
8
+ "section", "article", "aside", "header", "footer", "nav", "main",
9
+ "form", "input", "button", "select", "option", "textarea", "label",
10
+ "fieldset", "legend", "dl", "dt", "dd", "caption",
11
+ ]);
12
+
13
+ /**
14
+ * Escape angle brackets and curly braces in content for MDX compatibility.
15
+ * Preserves content inside code blocks (``` ... ```) and inline code (` ... `).
16
+ * Handles 3+ backtick fenced blocks correctly.
17
+ */
18
+ export function escapeForMdx(content: string): string {
19
+ // Extract code blocks (supports 3+ backtick fences via backreference)
20
+ const codeBlocks: string[] = [];
21
+ const placeholder = "\x00CODEBLOCK_";
22
+ const codeBlockRegex = /(`{3,})[^\n]*\n[\s\S]*?\1/g;
23
+ const withPlaceholders = content.replace(codeBlockRegex, (match) => {
24
+ const idx = codeBlocks.length;
25
+ codeBlocks.push(match);
26
+ return `${placeholder}${idx}\x00`;
27
+ });
28
+ const parts = withPlaceholders.split(
29
+ new RegExp(`(${placeholder}\\d+\x00)`, "g"),
30
+ );
31
+
32
+ return parts
33
+ .map((part) => {
34
+ const placeholderMatch = part.match(
35
+ new RegExp(`^${placeholder}(\\d+)\x00$`),
36
+ );
37
+ if (placeholderMatch) return codeBlocks[Number(placeholderMatch[1])];
38
+
39
+ // For non-code-block text, split on inline code to preserve it.
40
+ // Supports multi-backtick inline code (`` `code` ``, ``` ``code`` ```).
41
+ const inlineCodeRegex = /(`{1,3})(?!`)([\s\S]*?[^`])\1(?!`)/g;
42
+ const inlineCodes: string[] = [];
43
+ const inlinePlaceholder = "\x00INLINE_";
44
+ const withInlinePlaceholders = part.replace(
45
+ inlineCodeRegex,
46
+ (match) => {
47
+ const idx = inlineCodes.length;
48
+ inlineCodes.push(match);
49
+ return `${inlinePlaceholder}${idx}\x00`;
50
+ },
51
+ );
52
+
53
+ let escaped = withInlinePlaceholders
54
+ // Escape opening tags: <Name>, <Name attr="val">
55
+ .replace(
56
+ /<([A-Za-z][A-Za-z0-9_-]*)(\s[^>]*)?>/g,
57
+ (match, name: string) => {
58
+ if (htmlTags.has(name.toLowerCase())) return match;
59
+ return match.replace(/</g, "&lt;").replace(/>/g, "&gt;");
60
+ },
61
+ )
62
+ // Escape closing tags: </Name>
63
+ .replace(
64
+ /<\/([A-Za-z][A-Za-z0-9_-]*)>/g,
65
+ (match, name: string) => {
66
+ if (htmlTags.has(name.toLowerCase())) return match;
67
+ return `&lt;/${name}&gt;`;
68
+ },
69
+ )
70
+ // Escape self-closing tags: <Name />
71
+ .replace(
72
+ /<([A-Za-z][A-Za-z0-9_-]*)(\s[^>]*)?\s*\/>/g,
73
+ (match, name: string) => {
74
+ if (htmlTags.has(name.toLowerCase())) return match;
75
+ return match.replace(/</g, "&lt;").replace(/>/g, "&gt;");
76
+ },
77
+ )
78
+ .replace(/<(-+|=+)/g, "&lt;$1")
79
+ .replace(/<(\d)/g, "&lt;$1")
80
+ // Escape curly braces (MDX interprets them as JSX expressions)
81
+ .replace(/\{/g, "&#123;")
82
+ .replace(/\}/g, "&#125;");
83
+
84
+ // Restore inline code placeholders
85
+ escaped = escaped.replace(
86
+ new RegExp(`${inlinePlaceholder}(\\d+)\x00`, "g"),
87
+ (_, idx: string) => inlineCodes[Number(idx)],
88
+ );
89
+
90
+ return escaped;
91
+ })
92
+ .join("");
93
+ }