create-zudo-doc 1.1.0 → 1.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/api.d.ts +6 -0
- package/dist/api.js +5 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4 -0
- package/dist/index.js +26 -1
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.js +12 -6
- package/dist/utils.d.ts +27 -0
- package/dist/utils.js +72 -1
- package/dist/zfb-config-gen.js +3 -1
- package/package.json +1 -1
- package/templates/base/src/config/i18n.ts +1 -1
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +137 -22
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +115 -28
package/dist/api.d.ts
CHANGED
|
@@ -16,5 +16,11 @@ export interface CreateOptions {
|
|
|
16
16
|
packageManager: "pnpm" | "npm" | "yarn" | "bun";
|
|
17
17
|
/** Install dependencies after scaffolding (default: false) */
|
|
18
18
|
install?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Initialize a git repository + initial commit after scaffolding
|
|
21
|
+
* (default: false). The CLI defaults this on; the programmatic API defaults
|
|
22
|
+
* it off so automation / test callers never create unexpected repos.
|
|
23
|
+
*/
|
|
24
|
+
git?: boolean;
|
|
19
25
|
}
|
|
20
26
|
export declare function createZudoDoc(options: CreateOptions): Promise<string>;
|
package/dist/api.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { scaffold } from "./scaffold.js";
|
|
3
|
-
import { installDependencies, validateProjectName } from "./utils.js";
|
|
3
|
+
import { initGitRepo, installDependencies, validateProjectName } from "./utils.js";
|
|
4
4
|
export async function createZudoDoc(options) {
|
|
5
|
-
const { install = false, ...rest } = options;
|
|
5
|
+
const { install = false, git = false, ...rest } = options;
|
|
6
6
|
const nameError = validateProjectName(rest.projectName);
|
|
7
7
|
if (nameError)
|
|
8
8
|
throw new Error(`Invalid projectName: ${nameError}`);
|
|
@@ -12,5 +12,8 @@ export async function createZudoDoc(options) {
|
|
|
12
12
|
if (install) {
|
|
13
13
|
installDependencies(targetDir, choices.packageManager);
|
|
14
14
|
}
|
|
15
|
+
if (git) {
|
|
16
|
+
initGitRepo(targetDir);
|
|
17
|
+
}
|
|
15
18
|
return targetDir;
|
|
16
19
|
}
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -67,6 +67,8 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
67
67
|
}
|
|
68
68
|
if (wasPassed("install"))
|
|
69
69
|
args.install = raw["install"] !== false;
|
|
70
|
+
if (wasPassed("git"))
|
|
71
|
+
args.git = raw["git"] !== false;
|
|
70
72
|
if (raw.yes || raw.y)
|
|
71
73
|
args.yes = true;
|
|
72
74
|
if (raw.help || raw.h)
|
|
@@ -95,6 +97,8 @@ ${featureHelp}
|
|
|
95
97
|
--preset <path> Load settings from a JSON preset file (use "-" for stdin)
|
|
96
98
|
--pm <manager> pnpm | npm | yarn | bun
|
|
97
99
|
--[no-]install Install dependencies after scaffolding
|
|
100
|
+
--[no-]git Initialize a git repository + initial commit
|
|
101
|
+
(default: on; enables doc-history metadata)
|
|
98
102
|
-y, --yes Use defaults for unspecified options, skip prompts
|
|
99
103
|
-h, --help Show this help message
|
|
100
104
|
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { FEATURES } from "./constants.js";
|
|
|
6
6
|
import { loadPreset } from "./preset.js";
|
|
7
7
|
import { runPrompts } from "./prompts.js";
|
|
8
8
|
import { scaffold } from "./scaffold.js";
|
|
9
|
-
import { installDependencies } from "./utils.js";
|
|
9
|
+
import { installDependencies, initGitRepo } from "./utils.js";
|
|
10
10
|
async function main() {
|
|
11
11
|
const args = parseArgs();
|
|
12
12
|
// Handle --help
|
|
@@ -144,6 +144,31 @@ async function main() {
|
|
|
144
144
|
s2.stop("Installation failed. Run install manually.");
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
// Initialize a git repository (default on; --no-git to opt out). doc-history
|
|
148
|
+
// reads `git log` for each page's Created/Updated/Author block, so a project
|
|
149
|
+
// without git renders empty history. Runs after install so the lockfile is
|
|
150
|
+
// part of the initial commit; auto-skips if the target is already inside a
|
|
151
|
+
// repo or if git is unavailable.
|
|
152
|
+
const shouldInitGit = args.git ?? true;
|
|
153
|
+
if (shouldInitGit) {
|
|
154
|
+
const s3 = p.spinner();
|
|
155
|
+
s3.start("Initializing git repository...");
|
|
156
|
+
const result = initGitRepo(targetDir);
|
|
157
|
+
switch (result.status) {
|
|
158
|
+
case "ok":
|
|
159
|
+
s3.stop("Initialized git repository with an initial commit.");
|
|
160
|
+
break;
|
|
161
|
+
case "skipped-existing-repo":
|
|
162
|
+
s3.stop("Already inside a git repository — skipped git init.");
|
|
163
|
+
break;
|
|
164
|
+
case "skipped-no-git":
|
|
165
|
+
s3.stop("git not found — skipped git init.");
|
|
166
|
+
break;
|
|
167
|
+
case "failed":
|
|
168
|
+
s3.stop("Could not initialize git — continuing without it.");
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
147
172
|
p.outro(`${pc.green("Done!")} Your project is ready at ${pc.cyan(choices.projectName)}`);
|
|
148
173
|
console.log();
|
|
149
174
|
console.log(` ${pc.bold("Next steps:")}`);
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export { getSecondaryLang };
|
|
|
11
11
|
*
|
|
12
12
|
* Bumped in lockstep by scripts/release-create-zudo-doc.sh.
|
|
13
13
|
*/
|
|
14
|
-
export declare const ZUDO_DOC_PIN = "^1.
|
|
14
|
+
export declare const ZUDO_DOC_PIN = "^1.3.0";
|
|
15
15
|
export declare function scaffold(choices: UserChoices): Promise<void>;
|
package/dist/scaffold.js
CHANGED
|
@@ -18,7 +18,7 @@ export { getSecondaryLang };
|
|
|
18
18
|
*
|
|
19
19
|
* Bumped in lockstep by scripts/release-create-zudo-doc.sh.
|
|
20
20
|
*/
|
|
21
|
-
export const ZUDO_DOC_PIN = "^1.
|
|
21
|
+
export const ZUDO_DOC_PIN = "^1.3.0";
|
|
22
22
|
/**
|
|
23
23
|
* Files in `templates/base/**` that must never be copied into a generated
|
|
24
24
|
* project. Each entry is matched against the path relative to `templates/base/`
|
|
@@ -475,13 +475,19 @@ function generatePackageJson(choices) {
|
|
|
475
475
|
// (e.g. `@/*`) into its synthetic tsconfig (zfb#1238) — fixes silent island
|
|
476
476
|
// hydration failure under route injection. Unblocks packageOwnedRoutes.
|
|
477
477
|
// No consumer-facing / CLI breaking change.
|
|
478
|
-
// next.
|
|
478
|
+
// next.68: routine toolchain bump from next.67, adopted in
|
|
479
479
|
// lockstep with the root package.json pins. No consumer-facing / CLI change.
|
|
480
|
-
|
|
481
|
-
|
|
480
|
+
// next.69: routine toolchain bump from next.68, adopted in
|
|
481
|
+
// lockstep with the root package.json pins. No consumer-facing / CLI change.
|
|
482
|
+
// next.70 (current pin): routine toolchain bump from next.69, re-aligned
|
|
483
|
+
// with the root package.json pins (root was bumped in b5489acf; this
|
|
484
|
+
// scaffold pin lagged at next.69 and broke check-pin-parity). No
|
|
485
|
+
// consumer-facing / CLI change.
|
|
486
|
+
"@takazudo/zfb": "0.1.0-next.70",
|
|
487
|
+
"@takazudo/zfb-runtime": "0.1.0-next.70",
|
|
482
488
|
// zfb-adapter-cloudflare — required for any route with `prerender = false`.
|
|
483
489
|
// Pinned in lockstep with @takazudo/zfb.
|
|
484
|
-
"@takazudo/zfb-adapter-cloudflare": "0.1.0-next.
|
|
490
|
+
"@takazudo/zfb-adapter-cloudflare": "0.1.0-next.70",
|
|
485
491
|
// @takazudo/zudo-doc — published from this monorepo via
|
|
486
492
|
// .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
|
|
487
493
|
// lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
|
|
@@ -564,7 +570,7 @@ function generatePackageJson(choices) {
|
|
|
564
570
|
// @takazudo/zudo-doc/integrations/doc-history which in turn imports
|
|
565
571
|
// @takazudo/zudo-doc-history-server/git-history. Without this dep the
|
|
566
572
|
// plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
|
|
567
|
-
deps["@takazudo/zudo-doc-history-server"] = "^1.
|
|
573
|
+
deps["@takazudo/zudo-doc-history-server"] = "^1.3.0";
|
|
568
574
|
// tsx is no longer needed here: the relocated package plugin imports the
|
|
569
575
|
// runner directly (no `tsx -e` spawn) since the package ships compiled
|
|
570
576
|
// dist/ — package-first migration #2321 (#2337).
|
package/dist/utils.d.ts
CHANGED
|
@@ -7,6 +7,33 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function validateProjectName(name: string): string | null;
|
|
9
9
|
export declare function installDependencies(dir: string, pm: string): void;
|
|
10
|
+
export type GitInitResult = {
|
|
11
|
+
status: "ok";
|
|
12
|
+
} | {
|
|
13
|
+
status: "skipped-existing-repo";
|
|
14
|
+
} | {
|
|
15
|
+
status: "skipped-no-git";
|
|
16
|
+
} | {
|
|
17
|
+
status: "failed";
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Initialize a git repository in `dir` and create an initial commit.
|
|
22
|
+
*
|
|
23
|
+
* Why: zudo-doc's doc-history feature reads `git log` for each page's
|
|
24
|
+
* Created/Updated/Author block. A scaffolded project with no git repo renders
|
|
25
|
+
* empty history — and on older `@takazudo/zudo-doc-history-server` it crashed
|
|
26
|
+
* `pnpm dev` outright (the preBuild hook ran `git rev-parse` and threw).
|
|
27
|
+
* Initializing git here makes the feature work out of the box.
|
|
28
|
+
*
|
|
29
|
+
* Safe by construction:
|
|
30
|
+
* - `skipped-no-git` when git is not installed;
|
|
31
|
+
* - `skipped-existing-repo` when `dir` is already inside a git work tree (e.g.
|
|
32
|
+
* scaffolding into an existing monorepo) — never nest repositories;
|
|
33
|
+
* - the initial commit only falls back to a neutral identity when the user has
|
|
34
|
+
* none configured, so a normal user's commit keeps their own identity.
|
|
35
|
+
*/
|
|
36
|
+
export declare function initGitRepo(dir: string): GitInitResult;
|
|
10
37
|
export declare function capitalize(str: string): string;
|
|
11
38
|
/** Get a short uppercase label for a language code (e.g. "en" → "EN", "zh-cn" → "ZH-CN"). */
|
|
12
39
|
export declare function getLangLabel(langCode: string): string;
|
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
1
|
+
import { execSync, execFileSync } from "child_process";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
3
|
// Project-name grammar (locked by F4 — S4 #2013):
|
|
4
4
|
// /^[a-z0-9][a-z0-9._-]*$/, max 214 chars, unscoped, used as both directory
|
|
@@ -36,6 +36,77 @@ export function installDependencies(dir, pm) {
|
|
|
36
36
|
// Use pipe to avoid garbled output when used alongside spinner
|
|
37
37
|
execSync(cmd, { cwd: dir, stdio: "pipe" });
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Initialize a git repository in `dir` and create an initial commit.
|
|
41
|
+
*
|
|
42
|
+
* Why: zudo-doc's doc-history feature reads `git log` for each page's
|
|
43
|
+
* Created/Updated/Author block. A scaffolded project with no git repo renders
|
|
44
|
+
* empty history — and on older `@takazudo/zudo-doc-history-server` it crashed
|
|
45
|
+
* `pnpm dev` outright (the preBuild hook ran `git rev-parse` and threw).
|
|
46
|
+
* Initializing git here makes the feature work out of the box.
|
|
47
|
+
*
|
|
48
|
+
* Safe by construction:
|
|
49
|
+
* - `skipped-no-git` when git is not installed;
|
|
50
|
+
* - `skipped-existing-repo` when `dir` is already inside a git work tree (e.g.
|
|
51
|
+
* scaffolding into an existing monorepo) — never nest repositories;
|
|
52
|
+
* - the initial commit only falls back to a neutral identity when the user has
|
|
53
|
+
* none configured, so a normal user's commit keeps their own identity.
|
|
54
|
+
*/
|
|
55
|
+
export function initGitRepo(dir) {
|
|
56
|
+
// Is git available at all?
|
|
57
|
+
try {
|
|
58
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { status: "skipped-no-git" };
|
|
62
|
+
}
|
|
63
|
+
// Already inside a git work tree? Don't create a nested repository.
|
|
64
|
+
try {
|
|
65
|
+
const inside = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
66
|
+
cwd: dir,
|
|
67
|
+
encoding: "utf-8",
|
|
68
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
69
|
+
}).trim();
|
|
70
|
+
if (inside === "true")
|
|
71
|
+
return { status: "skipped-existing-repo" };
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// git exits non-zero when `dir` is not inside a repo — the expected case.
|
|
75
|
+
}
|
|
76
|
+
const COMMIT_MESSAGE = "Initial commit from create-zudo-doc";
|
|
77
|
+
try {
|
|
78
|
+
execFileSync("git", ["init", "-q"], { cwd: dir, stdio: "ignore" });
|
|
79
|
+
execFileSync("git", ["add", "-A"], { cwd: dir, stdio: "ignore" });
|
|
80
|
+
try {
|
|
81
|
+
execFileSync("git", ["commit", "-q", "-m", COMMIT_MESSAGE], {
|
|
82
|
+
cwd: dir,
|
|
83
|
+
stdio: "ignore",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// No git identity configured on this machine — retry with a neutral
|
|
88
|
+
// fallback so the initial commit still succeeds. Users with a configured
|
|
89
|
+
// identity never reach this branch (their identity is used above).
|
|
90
|
+
execFileSync("git", [
|
|
91
|
+
"-c",
|
|
92
|
+
"user.name=create-zudo-doc",
|
|
93
|
+
"-c",
|
|
94
|
+
"user.email=create-zudo-doc@users.noreply.github.com",
|
|
95
|
+
"commit",
|
|
96
|
+
"-q",
|
|
97
|
+
"-m",
|
|
98
|
+
COMMIT_MESSAGE,
|
|
99
|
+
], { cwd: dir, stdio: "ignore" });
|
|
100
|
+
}
|
|
101
|
+
return { status: "ok" };
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
status: "failed",
|
|
106
|
+
message: err instanceof Error ? err.message : String(err),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
39
110
|
export function capitalize(str) {
|
|
40
111
|
return str.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
41
112
|
}
|
package/dist/zfb-config-gen.js
CHANGED
|
@@ -28,6 +28,8 @@ export function generateZfbConfig(_choices) {
|
|
|
28
28
|
lines.push(`import { zudoDocPreset } from "@takazudo/zudo-doc/preset";`);
|
|
29
29
|
lines.push(`import { settings } from "./src/config/settings";`);
|
|
30
30
|
lines.push(`import { buildDocsSchema } from "./src/config/docs-schema";`);
|
|
31
|
+
lines.push(`import { translations } from "./src/config/i18n";`);
|
|
32
|
+
lines.push(`import { colorSchemes } from "./src/config/color-schemes";`);
|
|
31
33
|
lines.push(``);
|
|
32
34
|
// --- Directive vocabulary ---
|
|
33
35
|
// The seven canonical directives registered in pages/_mdx-components.ts.
|
|
@@ -54,7 +56,7 @@ export function generateZfbConfig(_choices) {
|
|
|
54
56
|
lines.push(` base: settings.base,`);
|
|
55
57
|
lines.push(``);
|
|
56
58
|
lines.push(` // ── Preset-owned fields (content collections, plugins, markdown, …) ────────`);
|
|
57
|
-
lines.push(` ...zudoDocPreset({ settings, buildDocsSchema, directiveVocabulary }),`);
|
|
59
|
+
lines.push(` ...zudoDocPreset({ settings, buildDocsSchema, directiveVocabulary, translations, colorSchemes }),`);
|
|
58
60
|
lines.push(`});`);
|
|
59
61
|
return lines.join("\n") + "\n";
|
|
60
62
|
}
|
package/package.json
CHANGED
|
@@ -55,7 +55,7 @@ export function detectLocaleFromPath(path: string): Locale {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** UI string translations */
|
|
58
|
-
const translations: Record<string, Record<string, string>> = {
|
|
58
|
+
export const translations: Record<string, Record<string, string>> = {
|
|
59
59
|
en: {
|
|
60
60
|
"nav.gettingStarted": "Getting Started",
|
|
61
61
|
"nav.learn": "Learn",
|
|
@@ -111,15 +111,20 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
it("generates skill as
|
|
114
|
+
it("generates skill page as <dir>/index.mdx", () => {
|
|
115
115
|
generateClaudeResourcesDocs({
|
|
116
116
|
claudeDir,
|
|
117
117
|
projectRoot: tmpDir,
|
|
118
118
|
docsDir,
|
|
119
119
|
});
|
|
120
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
|
+
|
|
121
126
|
const flatPath = path.join(docsDir, "claude-skills", "test-skill.mdx");
|
|
122
|
-
expect(fs.existsSync(flatPath)).toBe(
|
|
127
|
+
expect(fs.existsSync(flatPath)).toBe(false);
|
|
123
128
|
});
|
|
124
129
|
});
|
|
125
130
|
|
|
@@ -150,7 +155,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
150
155
|
});
|
|
151
156
|
|
|
152
157
|
const skillPage = fs.readFileSync(
|
|
153
|
-
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
158
|
+
path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
|
|
154
159
|
"utf8",
|
|
155
160
|
);
|
|
156
161
|
const parsed = matter(skillPage);
|
|
@@ -168,7 +173,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
168
173
|
});
|
|
169
174
|
|
|
170
175
|
const skillPage = fs.readFileSync(
|
|
171
|
-
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
176
|
+
path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
|
|
172
177
|
"utf8",
|
|
173
178
|
);
|
|
174
179
|
|
|
@@ -179,7 +184,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
179
184
|
expect(skillPage).toContain("SKILL.md");
|
|
180
185
|
});
|
|
181
186
|
|
|
182
|
-
it("skill page has links to sub-files that resolve
|
|
187
|
+
it("skill page has links to sub-files that resolve to sibling nested files", () => {
|
|
183
188
|
generateClaudeResourcesDocs({
|
|
184
189
|
claudeDir,
|
|
185
190
|
projectRoot: tmpDir,
|
|
@@ -187,12 +192,12 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
187
192
|
});
|
|
188
193
|
|
|
189
194
|
const skillPage = fs.readFileSync(
|
|
190
|
-
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
195
|
+
path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
|
|
191
196
|
"utf8",
|
|
192
197
|
);
|
|
193
198
|
|
|
194
|
-
// Links use ./<subpage
|
|
195
|
-
//
|
|
199
|
+
// Links use ./<subpage>; the skill page is <dir>/index.mdx so these
|
|
200
|
+
// resolve against the file path to the sibling <dir>/<subpage>.mdx (#2411).
|
|
196
201
|
expect(skillPage).toContain("./ref-guide");
|
|
197
202
|
expect(skillPage).toContain("./asset-template");
|
|
198
203
|
|
|
@@ -200,8 +205,9 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
200
205
|
expect(skillPage).not.toContain("./test-skill/ref-guide");
|
|
201
206
|
expect(skillPage).not.toContain("./test-skill/asset-template");
|
|
202
207
|
|
|
203
|
-
// Each linked sub-page must exist as a
|
|
204
|
-
//
|
|
208
|
+
// Each linked sub-page must exist as a sibling nested file inside <dir>/,
|
|
209
|
+
// i.e. claude-skills/test-skill/<subpage>.mdx — which is exactly where the
|
|
210
|
+
// ./<subpage> relative link resolves to.
|
|
205
211
|
const linkPattern = /\]\(\.\/([\w-]+)\)/g;
|
|
206
212
|
let match;
|
|
207
213
|
while ((match = linkPattern.exec(skillPage)) !== null) {
|
|
@@ -209,11 +215,12 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
209
215
|
const targetFile = path.join(
|
|
210
216
|
docsDir,
|
|
211
217
|
"claude-skills",
|
|
212
|
-
|
|
218
|
+
"test-skill",
|
|
219
|
+
`${subPage}.mdx`,
|
|
213
220
|
);
|
|
214
221
|
expect(
|
|
215
222
|
fs.existsSync(targetFile),
|
|
216
|
-
`Link target "test-skill
|
|
223
|
+
`Link target "test-skill/${subPage}.mdx" should exist`,
|
|
217
224
|
).toBe(true);
|
|
218
225
|
}
|
|
219
226
|
});
|
|
@@ -226,7 +233,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
226
233
|
});
|
|
227
234
|
|
|
228
235
|
const skillPage = fs.readFileSync(
|
|
229
|
-
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
236
|
+
path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
|
|
230
237
|
"utf8",
|
|
231
238
|
);
|
|
232
239
|
|
|
@@ -255,28 +262,28 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
255
262
|
// ---------------------------------------------------------------------------
|
|
256
263
|
|
|
257
264
|
describe("sub-file pages", () => {
|
|
258
|
-
it("generates unlisted reference page", () => {
|
|
265
|
+
it("generates unlisted reference page as a nested file", () => {
|
|
259
266
|
generateClaudeResourcesDocs({
|
|
260
267
|
claudeDir,
|
|
261
268
|
projectRoot: tmpDir,
|
|
262
269
|
docsDir,
|
|
263
270
|
});
|
|
264
271
|
|
|
265
|
-
const refPage = path.join(docsDir, "claude-skills", "test-skill
|
|
272
|
+
const refPage = path.join(docsDir, "claude-skills", "test-skill", "ref-guide.mdx");
|
|
266
273
|
expect(fs.existsSync(refPage)).toBe(true);
|
|
267
274
|
|
|
268
275
|
const parsed = matter(fs.readFileSync(refPage, "utf8"));
|
|
269
276
|
expect(parsed.data.unlisted).toBe(true);
|
|
270
277
|
});
|
|
271
278
|
|
|
272
|
-
it("generates unlisted asset page for .md files", () => {
|
|
279
|
+
it("generates unlisted asset page for .md files as a nested file", () => {
|
|
273
280
|
generateClaudeResourcesDocs({
|
|
274
281
|
claudeDir,
|
|
275
282
|
projectRoot: tmpDir,
|
|
276
283
|
docsDir,
|
|
277
284
|
});
|
|
278
285
|
|
|
279
|
-
const assetPage = path.join(docsDir, "claude-skills", "test-skill
|
|
286
|
+
const assetPage = path.join(docsDir, "claude-skills", "test-skill", "asset-template.mdx");
|
|
280
287
|
expect(fs.existsSync(assetPage)).toBe(true);
|
|
281
288
|
|
|
282
289
|
const parsed = matter(fs.readFileSync(assetPage, "utf8"));
|
|
@@ -290,23 +297,26 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
290
297
|
docsDir,
|
|
291
298
|
});
|
|
292
299
|
|
|
293
|
-
const scriptPage = path.join(docsDir, "claude-skills", "test-skill
|
|
300
|
+
const scriptPage = path.join(docsDir, "claude-skills", "test-skill", "script-run.mdx");
|
|
294
301
|
expect(fs.existsSync(scriptPage)).toBe(false);
|
|
295
302
|
});
|
|
296
303
|
|
|
297
|
-
it("sub-pages
|
|
304
|
+
it("sub-pages derive their route from the file path (no explicit slug)", () => {
|
|
298
305
|
generateClaudeResourcesDocs({
|
|
299
306
|
claudeDir,
|
|
300
307
|
projectRoot: tmpDir,
|
|
301
308
|
docsDir,
|
|
302
309
|
});
|
|
303
310
|
|
|
311
|
+
// The page lives at claude-skills/test-skill/ref-guide.mdx, so its route
|
|
312
|
+
// is claude-skills/test-skill/ref-guide — nested breadcrumbs come from the
|
|
313
|
+
// file path, not an explicit slug (which zfb's link resolver ignores, #2411).
|
|
304
314
|
const refPage = fs.readFileSync(
|
|
305
|
-
path.join(docsDir, "claude-skills", "test-skill
|
|
315
|
+
path.join(docsDir, "claude-skills", "test-skill", "ref-guide.mdx"),
|
|
306
316
|
"utf8",
|
|
307
317
|
);
|
|
308
318
|
const parsed = matter(refPage);
|
|
309
|
-
expect(parsed.data.slug).
|
|
319
|
+
expect(parsed.data.slug).toBeUndefined();
|
|
310
320
|
});
|
|
311
321
|
|
|
312
322
|
it("reference page content is correct", () => {
|
|
@@ -317,7 +327,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
317
327
|
});
|
|
318
328
|
|
|
319
329
|
const refPage = fs.readFileSync(
|
|
320
|
-
path.join(docsDir, "claude-skills", "test-skill
|
|
330
|
+
path.join(docsDir, "claude-skills", "test-skill", "ref-guide.mdx"),
|
|
321
331
|
"utf8",
|
|
322
332
|
);
|
|
323
333
|
const parsed = matter(refPage);
|
|
@@ -532,4 +542,109 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
532
542
|
).toThrow(/reserved name "index"/);
|
|
533
543
|
});
|
|
534
544
|
});
|
|
545
|
+
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
// projectRoot default scope regression test
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
describe("projectRoot default scope", () => {
|
|
551
|
+
it("walks claudeDir (not its parent) when projectRoot is omitted", () => {
|
|
552
|
+
// The fixture already writes tmpDir/CLAUDE.md (outside claudeDir).
|
|
553
|
+
// With the old bug (projectRoot = path.dirname(claudeDir) = tmpDir), the
|
|
554
|
+
// walk starts at tmpDir and picks up tmpDir/CLAUDE.md. With the fix
|
|
555
|
+
// (projectRoot = claudeDir), the walk starts inside claudeDir and does NOT
|
|
556
|
+
// reach tmpDir/CLAUDE.md (parent dirs are never walked upward).
|
|
557
|
+
//
|
|
558
|
+
// To distinguish, we write a CLAUDE.md inside claudeDir with unique content
|
|
559
|
+
// and verify the generated page body matches IT, not the outside one.
|
|
560
|
+
fs.writeFileSync(
|
|
561
|
+
path.join(claudeDir, "CLAUDE.md"),
|
|
562
|
+
"# Claude dir unique content xyz123",
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// Call with NO projectRoot.
|
|
566
|
+
generateClaudeResourcesDocs({ claudeDir, docsDir });
|
|
567
|
+
|
|
568
|
+
// The generated root.mdx body must contain the inside-claudeDir content.
|
|
569
|
+
const claudeMdDir = path.join(docsDir, "claude-md");
|
|
570
|
+
const rootMdx = fs.readFileSync(path.join(claudeMdDir, "root.mdx"), "utf8");
|
|
571
|
+
expect(rootMdx).toContain("xyz123");
|
|
572
|
+
|
|
573
|
+
// And must NOT contain the outside content (from tmpDir/CLAUDE.md).
|
|
574
|
+
expect(rootMdx).not.toContain("Project instructions");
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// CLAUDE.md repo-relative link downgrade (#2411, Class B)
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
describe("CLAUDE.md repo-relative link downgrade", () => {
|
|
583
|
+
const claudeMdBody = [
|
|
584
|
+
"# Project",
|
|
585
|
+
"",
|
|
586
|
+
"See [wrangler.toml](./wrangler.toml) and [schema](../../schema/photos.sql).",
|
|
587
|
+
"Also a [workflow](../../.github/workflows/deploy.yml) and a [bare](docs/guide.md) link.",
|
|
588
|
+
"External [site](https://example.com), [anchor](#section), [site-abs](/docs/getting-started), [mail](mailto:x@y.com).",
|
|
589
|
+
"Image: ",
|
|
590
|
+
"",
|
|
591
|
+
"Inline code stays: `[x](./y)`",
|
|
592
|
+
"",
|
|
593
|
+
"```",
|
|
594
|
+
"[code](./should-not-change.md)",
|
|
595
|
+
"```",
|
|
596
|
+
"",
|
|
597
|
+
"~~~",
|
|
598
|
+
"[tilde-code](./also-should-not-change.md)",
|
|
599
|
+
"~~~",
|
|
600
|
+
"",
|
|
601
|
+
].join("\n");
|
|
602
|
+
|
|
603
|
+
function genRoot() {
|
|
604
|
+
fs.writeFileSync(path.join(tmpDir, "CLAUDE.md"), claudeMdBody);
|
|
605
|
+
generateClaudeResourcesDocs({ claudeDir, projectRoot: tmpDir, docsDir });
|
|
606
|
+
return fs.readFileSync(
|
|
607
|
+
path.join(docsDir, "claude-md", "root.mdx"),
|
|
608
|
+
"utf8",
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
it("downgrades repo-relative file links to inline code", () => {
|
|
613
|
+
const rootMdx = genRoot();
|
|
614
|
+
expect(rootMdx).toContain("`wrangler.toml`");
|
|
615
|
+
expect(rootMdx).toContain("`schema`");
|
|
616
|
+
expect(rootMdx).toContain("`workflow`");
|
|
617
|
+
expect(rootMdx).toContain("`bare`");
|
|
618
|
+
// The dangling hrefs must be gone.
|
|
619
|
+
expect(rootMdx).not.toContain("](./wrangler.toml)");
|
|
620
|
+
expect(rootMdx).not.toContain("](../../schema/photos.sql)");
|
|
621
|
+
expect(rootMdx).not.toContain("](../../.github/workflows/deploy.yml)");
|
|
622
|
+
expect(rootMdx).not.toContain("](docs/guide.md)");
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("preserves resolvable links (external, anchor, site-absolute, scheme)", () => {
|
|
626
|
+
const rootMdx = genRoot();
|
|
627
|
+
expect(rootMdx).toContain("](https://example.com)");
|
|
628
|
+
expect(rootMdx).toContain("](#section)");
|
|
629
|
+
expect(rootMdx).toContain("](/docs/getting-started)");
|
|
630
|
+
expect(rootMdx).toContain("](mailto:x@y.com)");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("downgrades repo-relative image links without leaving a stray '!'", () => {
|
|
634
|
+
const rootMdx = genRoot();
|
|
635
|
+
expect(rootMdx).toContain("`diagram`");
|
|
636
|
+
expect(rootMdx).not.toContain("](./diagram.png)");
|
|
637
|
+
expect(rootMdx).not.toContain("!`diagram`");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("does not rewrite links inside inline code or fenced code blocks", () => {
|
|
641
|
+
const rootMdx = genRoot();
|
|
642
|
+
// Inline-code span is preserved verbatim.
|
|
643
|
+
expect(rootMdx).toContain("`[x](./y)`");
|
|
644
|
+
// Backtick-fenced code block content is preserved verbatim.
|
|
645
|
+
expect(rootMdx).toContain("[code](./should-not-change.md)");
|
|
646
|
+
// Tilde-fenced code block content is preserved verbatim too.
|
|
647
|
+
expect(rootMdx).toContain("[tilde-code](./also-should-not-change.md)");
|
|
648
|
+
});
|
|
649
|
+
});
|
|
535
650
|
});
|
package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts
CHANGED
|
@@ -96,18 +96,24 @@ generated: true
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* Writes an unlisted sub-page MDX file
|
|
100
|
-
*
|
|
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).
|
|
101
108
|
*/
|
|
102
109
|
function writeUnlistedSubPage(
|
|
103
110
|
outputPath: string,
|
|
104
111
|
title: string,
|
|
105
|
-
slug: string,
|
|
106
112
|
body: string,
|
|
107
113
|
) {
|
|
108
114
|
fs.writeFileSync(
|
|
109
115
|
outputPath,
|
|
110
|
-
`---\ntitle: "${escapeTitle(title)}"\
|
|
116
|
+
`---\ntitle: "${escapeTitle(title)}"\nunlisted: true\ngenerated: true\n---\n\n${body}\n`,
|
|
111
117
|
);
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -124,6 +130,82 @@ function assertNotIndexReserved(
|
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
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
|
+
|
|
127
209
|
// ---------------------------------------------------------------------------
|
|
128
210
|
// CLAUDE.md discovery
|
|
129
211
|
// ---------------------------------------------------------------------------
|
|
@@ -164,7 +246,7 @@ function findClaudeMdFiles(dir: string, excludeDirs: string[]): string[] {
|
|
|
164
246
|
function generateClaudemdDocs(
|
|
165
247
|
config: ClaudeResourcesConfig,
|
|
166
248
|
): ClaudeMdItem[] {
|
|
167
|
-
const projectRoot = config.projectRoot ??
|
|
249
|
+
const projectRoot = config.projectRoot ?? config.claudeDir;
|
|
168
250
|
const outputDir = path.join(config.docsDir, "claude-md");
|
|
169
251
|
|
|
170
252
|
cleanDir(outputDir);
|
|
@@ -229,7 +311,7 @@ generated: true
|
|
|
229
311
|
|
|
230
312
|
**Path:** \`${item.relPath}\`
|
|
231
313
|
|
|
232
|
-
${escapeForMdx(content.trim())}
|
|
314
|
+
${escapeForMdx(downgradeRepoRelativeLinks(content.trim()))}
|
|
233
315
|
`;
|
|
234
316
|
fs.writeFileSync(path.join(outputDir, `${item.slug}.mdx`), mdx);
|
|
235
317
|
});
|
|
@@ -309,6 +391,7 @@ function getSkillFileTree(
|
|
|
309
391
|
|
|
310
392
|
for (let i = 0; i < entries.length; i++) {
|
|
311
393
|
const entry = entries[i];
|
|
394
|
+
if (!entry) continue;
|
|
312
395
|
const isLast = i === entries.length - 1;
|
|
313
396
|
const prefix = isLast ? "└── " : "├── ";
|
|
314
397
|
|
|
@@ -318,6 +401,7 @@ function getSkillFileTree(
|
|
|
318
401
|
lines.push(`${prefix}${entry.name}/`);
|
|
319
402
|
for (let j = 0; j < entry.children.length; j++) {
|
|
320
403
|
const child = entry.children[j];
|
|
404
|
+
if (!child) continue;
|
|
321
405
|
const childIsLast = j === entry.children.length - 1;
|
|
322
406
|
const continuation = isLast ? " " : "│ ";
|
|
323
407
|
const childPrefix = childIsLast ? "└── " : "├── ";
|
|
@@ -333,9 +417,10 @@ function getScriptDescription(filePath: string): string {
|
|
|
333
417
|
try {
|
|
334
418
|
const topLines = fs.readFileSync(filePath, "utf8").split("\n", 2);
|
|
335
419
|
// Skip shebang, use second line if available
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
420
|
+
const firstLine = topLines[0] ?? "";
|
|
421
|
+
const commentLine = firstLine.startsWith("#!")
|
|
422
|
+
? topLines[1] ?? ""
|
|
423
|
+
: firstLine;
|
|
339
424
|
// Match # comments (shell/python) or // comments (JS/TS)
|
|
340
425
|
const match = commentLine.match(/^(?:#|\/\/)\s*(.+)/);
|
|
341
426
|
return match ? ` — ${match[1]}` : "";
|
|
@@ -358,7 +443,7 @@ function getSkillReferences(
|
|
|
358
443
|
const content = fs.readFileSync(path.join(refsDir, f), "utf8");
|
|
359
444
|
const name = f.replace(/\.md$/, "");
|
|
360
445
|
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
361
|
-
const title = h1Match
|
|
446
|
+
const title = h1Match?.[1] ?? name;
|
|
362
447
|
return { name, title, content };
|
|
363
448
|
})
|
|
364
449
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -418,9 +503,9 @@ function generateSkillsDocs(config: ClaudeResourcesConfig): SkillItem[] {
|
|
|
418
503
|
if (subDirs.length > 0) {
|
|
419
504
|
const tree = `\`\`\`\n${getSkillFileTree(dir, subDirs)}\n\`\`\``;
|
|
420
505
|
|
|
421
|
-
// Collect links to all .md sub-files that get pages
|
|
422
|
-
//
|
|
423
|
-
//
|
|
506
|
+
// Collect links to all .md sub-files that get pages. Links use
|
|
507
|
+
// ./<subpage>; because the skill page is written as `<dir>/index.mdx`,
|
|
508
|
+
// these resolve to the sibling `<dir>/<subpage>.mdx` files (#2411).
|
|
424
509
|
const links: string[] = [];
|
|
425
510
|
for (const ref of references) {
|
|
426
511
|
links.push(`- [references/${ref.name}.md](./ref-${ref.name})`);
|
|
@@ -465,18 +550,22 @@ generated: true
|
|
|
465
550
|
|
|
466
551
|
${body}`;
|
|
467
552
|
|
|
468
|
-
// Write skill page as
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
553
|
+
// Write the skill page as the directory index (`<dir>/index.mdx`) so its
|
|
554
|
+
// route is `claude-skills/<dir>` served at URL `.../claude-skills/<dir>/`.
|
|
555
|
+
// This makes the reference/script/asset pages genuine siblings inside
|
|
556
|
+
// `<dir>/`, which is what lets the `./ref-<name>` links above resolve.
|
|
557
|
+
const skillDirOut = path.join(outputDir, dir);
|
|
558
|
+
ensureDir(skillDirOut);
|
|
559
|
+
fs.writeFileSync(path.join(skillDirOut, "index.mdx"), mdx);
|
|
560
|
+
|
|
561
|
+
// Generate unlisted sub-pages as nested files inside `<dir>/`. Their routes
|
|
562
|
+
// (`claude-skills/<dir>/ref-<name>`, …) are derived from these file paths
|
|
563
|
+
// and therefore match the `./ref-<name>` / `./script-<name>` /
|
|
564
|
+
// `./asset-<name>` links emitted above (#2411).
|
|
475
565
|
for (const ref of references) {
|
|
476
566
|
writeUnlistedSubPage(
|
|
477
|
-
path.join(
|
|
567
|
+
path.join(skillDirOut, `ref-${ref.name}.mdx`),
|
|
478
568
|
ref.title,
|
|
479
|
-
`${skillSlugBase}/ref-${ref.name}`,
|
|
480
569
|
escapeForMdx(ref.content.trim()),
|
|
481
570
|
);
|
|
482
571
|
}
|
|
@@ -488,11 +577,10 @@ ${body}`;
|
|
|
488
577
|
"utf8",
|
|
489
578
|
);
|
|
490
579
|
const h1Match = raw.match(/^#\s+(.+)$/m);
|
|
491
|
-
const title = h1Match
|
|
580
|
+
const title = h1Match?.[1] ?? slug;
|
|
492
581
|
writeUnlistedSubPage(
|
|
493
|
-
path.join(
|
|
582
|
+
path.join(skillDirOut, `script-${slug}.mdx`),
|
|
494
583
|
title,
|
|
495
|
-
`${skillSlugBase}/script-${slug}`,
|
|
496
584
|
escapeForMdx(raw.trim()),
|
|
497
585
|
);
|
|
498
586
|
}
|
|
@@ -504,11 +592,10 @@ ${body}`;
|
|
|
504
592
|
"utf8",
|
|
505
593
|
);
|
|
506
594
|
const h1Match = raw.match(/^#\s+(.+)$/m);
|
|
507
|
-
const title = h1Match
|
|
595
|
+
const title = h1Match?.[1] ?? slug;
|
|
508
596
|
writeUnlistedSubPage(
|
|
509
|
-
path.join(
|
|
597
|
+
path.join(skillDirOut, `asset-${slug}.mdx`),
|
|
510
598
|
title,
|
|
511
|
-
`${skillSlugBase}/asset-${slug}`,
|
|
512
599
|
escapeForMdx(raw.trim()),
|
|
513
600
|
);
|
|
514
601
|
}
|