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.
- package/dist/claude-md-gen.d.ts +8 -0
- package/dist/claude-md-gen.js +43 -26
- package/dist/compose.d.ts +14 -20
- package/dist/compose.js +15 -25
- package/dist/features/body-foot-util.d.ts +8 -4
- package/dist/features/body-foot-util.js +8 -4
- package/dist/features/claude-resources.d.ts +12 -0
- package/dist/features/claude-resources.js +13 -6
- package/dist/features/design-token-panel.d.ts +0 -12
- package/dist/features/design-token-panel.js +32 -93
- package/dist/features/doc-history.d.ts +18 -3
- package/dist/features/doc-history.js +45 -60
- package/dist/features/doc-tags.d.ts +9 -13
- package/dist/features/doc-tags.js +10 -26
- package/dist/features/dynamic-page-transition.d.ts +19 -30
- package/dist/features/dynamic-page-transition.js +21 -209
- package/dist/features/footer-taglist.d.ts +1 -1
- package/dist/features/footer-taglist.js +1 -1
- package/dist/features/footer.d.ts +3 -2
- package/dist/features/footer.js +3 -2
- package/dist/features/i18n.d.ts +13 -8
- package/dist/features/i18n.js +14 -9
- package/dist/features/image-enlarge.d.ts +7 -26
- package/dist/features/image-enlarge.js +7 -26
- package/dist/features/llms-txt.d.ts +3 -5
- package/dist/features/llms-txt.js +3 -5
- package/dist/features/search.d.ts +7 -3
- package/dist/features/search.js +7 -3
- package/dist/features/sidebar-resizer.d.ts +4 -8
- package/dist/features/sidebar-resizer.js +4 -8
- package/dist/features/sidebar-toggle.d.ts +7 -7
- package/dist/features/sidebar-toggle.js +7 -7
- package/dist/features/tag-governance.d.ts +14 -7
- package/dist/features/tag-governance.js +52 -8
- package/dist/features/tauri.d.ts +13 -10
- package/dist/features/tauri.js +16 -52
- package/dist/features/versioning.d.ts +12 -24
- package/dist/features/versioning.js +13 -39
- package/dist/index.js +2 -3
- package/dist/preset.d.ts +1 -0
- package/dist/preset.js +5 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +1 -0
- package/dist/scaffold.d.ts +13 -6
- package/dist/scaffold.js +78 -59
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +14 -0
- package/dist/zfb-config-gen.d.ts +32 -20
- package/dist/zfb-config-gen.js +400 -53
- package/package.json +2 -2
- package/templates/base/pages/docs/[[...slug]].tsx +59 -0
- package/templates/base/pages/index.tsx +6 -41
- package/templates/base/scripts/setup-doc-skill.sh +261 -0
- package/templates/base/src/styles/global.css +15 -340
- package/templates/base/tsconfig.json +3 -26
- package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +88 -0
- package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +3 -1
- package/dist/settings-gen.d.ts +0 -2
- package/dist/settings-gen.js +0 -319
- package/templates/base/.htmlvalidate.json +0 -5
- package/templates/base/.zfb/doc-history-meta.json +0 -1
- package/templates/base/pages/_data.ts +0 -160
- package/templates/base/pages/lib/_body-end-islands.tsx +0 -165
- package/templates/base/pages/lib/_chrome.ts +0 -167
- package/templates/base/pages/lib/_details.tsx +0 -29
- package/templates/base/pages/lib/_doc-route-entries.ts +0 -10
- package/templates/base/pages/lib/_extract-headings.ts +0 -37
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +0 -27
- package/templates/base/pages/lib/_nav-source-cache.ts +0 -100
- package/templates/base/pages/lib/_nav-source-docs.ts +0 -17
- package/templates/base/pages/lib/_preset-generator.tsx +0 -81
- package/templates/base/pages/lib/_route-context.ts +0 -32
- package/templates/base/pages/lib/_search-widget.tsx +0 -17
- package/templates/base/pages/lib/doc-page-props.ts +0 -30
- package/templates/base/pages/lib/locale-merge.ts +0 -59
- package/templates/base/scripts/run-b4push.sh +0 -102
- package/templates/base/src/components/ai-chat-modal.tsx +0 -18
- package/templates/base/src/components/content/code-group.tsx +0 -3
- package/templates/base/src/components/content/content-admonition.tsx +0 -4
- package/templates/base/src/components/desktop-sidebar-toggle.tsx +0 -15
- package/templates/base/src/components/doc-history.tsx +0 -21
- package/templates/base/src/components/image-enlarge.tsx +0 -24
- package/templates/base/src/components/preset-generator.tsx +0 -14
- package/templates/base/src/components/sidebar-toggle.tsx +0 -6
- package/templates/base/src/components/sidebar-tree.tsx +0 -6
- package/templates/base/src/config/color-scheme-utils.ts +0 -69
- package/templates/base/src/config/color-schemes.ts +0 -165
- package/templates/base/src/config/docs-schema.ts +0 -95
- package/templates/base/src/config/frontmatter-preview-defaults.ts +0 -27
- package/templates/base/src/config/frontmatter-preview-renderers.tsx +0 -46
- package/templates/base/src/config/i18n.ts +0 -239
- package/templates/base/src/config/settings-types.ts +0 -45
- package/templates/base/src/config/sidebars.ts +0 -66
- package/templates/base/src/config/tag-vocabulary-types.ts +0 -4
- package/templates/base/src/config/tag-vocabulary.ts +0 -20
- package/templates/base/src/config/z-index-tokens.ts +0 -128
- package/templates/base/src/types/docs-entry.ts +0 -28
- package/templates/base/src/types/heading.ts +0 -5
- package/templates/base/src/types/locale.ts +0 -10
- package/templates/base/src/utils/base.ts +0 -164
- package/templates/base/src/utils/docs.ts +0 -446
- package/templates/base/src/utils/git-info.ts +0 -70
- package/templates/base/src/utils/github.ts +0 -22
- package/templates/base/src/utils/nav-scope.ts +0 -34
- package/templates/base/src/utils/sidebar.ts +0 -36
- package/templates/base/src/utils/slug.ts +0 -10
- package/templates/base/src/utils/smart-break.tsx +0 -12
- package/templates/base/src/utils/tags.ts +0 -83
- package/templates/base/zfb-shim.d.ts +0 -178
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +0 -22
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +0 -42
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +0 -752
- package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +0 -97
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +0 -735
- package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +0 -15
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +0 -435
- package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +0 -174
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +0 -30
- package/templates/features/docHistory/files/src/components/doc-history.tsx +0 -10
- package/templates/features/docHistory/files/src/types/doc-history.ts +0 -7
- package/templates/features/dynamicPageTransition/files/src/components/client-router-bootstrap.tsx +0 -72
- package/templates/features/i18n/files/pages/[locale]/index.tsx +0 -72
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +0 -11
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +0 -6
- package/templates/features/tauri/files/src/components/find-bar.tsx +0 -122
- package/templates/features/tauri/files/src/components/find-in-page-init.tsx +0 -59
- package/templates/features/tauri/files/src/utils/find-in-page.ts +0 -175
package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts
DELETED
|
@@ -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
|
-
}
|