@takazudo/zudo-doc 5.7.0 → 5.9.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/CHANGELOG.md +35 -0
- package/README.md +17 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +1 -0
- package/dist/eject/index.js +9 -0
- package/dist/i18n-defaults/index.js +3 -0
- package/dist/plugins/codex-resources.d.ts +4 -0
- package/dist/plugins/codex-resources.js +31 -0
- package/dist/plugins/internal/claude-resources/generate.js +20 -291
- package/dist/plugins/internal/codex-resources/agents-md.d.ts +6 -0
- package/dist/plugins/internal/codex-resources/agents-md.js +70 -0
- package/dist/plugins/internal/codex-resources/agents.d.ts +7 -0
- package/dist/plugins/internal/codex-resources/agents.js +104 -0
- package/dist/plugins/internal/codex-resources/config.d.ts +6 -0
- package/dist/plugins/internal/codex-resources/config.js +111 -0
- package/dist/plugins/internal/codex-resources/generate.d.ts +20 -0
- package/dist/plugins/internal/codex-resources/generate.js +34 -0
- package/dist/plugins/internal/codex-resources/hooks.d.ts +6 -0
- package/dist/plugins/internal/codex-resources/hooks.js +174 -0
- package/dist/plugins/internal/codex-resources/index.d.ts +24 -0
- package/dist/plugins/internal/codex-resources/index.js +21 -0
- package/dist/plugins/internal/codex-resources/overview.d.ts +10 -0
- package/dist/plugins/internal/codex-resources/overview.js +33 -0
- package/dist/plugins/internal/codex-resources/rules.d.ts +6 -0
- package/dist/plugins/internal/codex-resources/rules.js +255 -0
- package/dist/plugins/internal/codex-resources/skills.d.ts +2 -0
- package/dist/plugins/internal/codex-resources/skills.js +84 -0
- package/dist/plugins/internal/codex-resources/utils.d.ts +14 -0
- package/dist/plugins/internal/codex-resources/utils.js +80 -0
- package/dist/plugins/internal/resource-docs-shared/fs.d.ts +3 -0
- package/dist/plugins/internal/resource-docs-shared/fs.js +19 -0
- package/dist/plugins/internal/resource-docs-shared/index.d.ts +7 -0
- package/dist/plugins/internal/resource-docs-shared/index.js +44 -0
- package/dist/plugins/internal/resource-docs-shared/links.d.ts +22 -0
- package/dist/plugins/internal/resource-docs-shared/links.js +44 -0
- package/dist/plugins/internal/resource-docs-shared/markdown-structure.d.ts +9 -0
- package/dist/plugins/internal/resource-docs-shared/markdown-structure.js +22 -0
- package/dist/plugins/internal/resource-docs-shared/mdx.d.ts +23 -0
- package/dist/plugins/internal/resource-docs-shared/mdx.js +63 -0
- package/dist/plugins/internal/resource-docs-shared/skills.d.ts +37 -0
- package/dist/plugins/internal/resource-docs-shared/skills.js +318 -0
- package/dist/plugins/internal/resource-docs-shared/walk.d.ts +20 -0
- package/dist/plugins/internal/resource-docs-shared/walk.js +41 -0
- package/dist/preset.d.ts +8 -0
- package/dist/preset.js +11 -0
- package/dist/safelist.css +1 -1
- package/dist/settings.d.ts +5 -0
- package/dist/transitions/nested-island-props-refresh.js +9 -3
- package/eject/header/gen-nav-overflow-script.mjs +771 -0
- package/eject/header/header.tsx +2 -0
- package/eject/header/nav-overflow-script.ts +11 -8
- package/package.json +9 -4
- /package/dist/plugins/internal/{claude-resources → resource-docs-shared}/escape-for-mdx.d.ts +0 -0
- /package/dist/plugins/internal/{claude-resources → resource-docs-shared}/escape-for-mdx.js +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
formatFrontmatterString,
|
|
5
|
+
cleanDir,
|
|
6
|
+
ensureDir,
|
|
7
|
+
renderCodeFence,
|
|
8
|
+
writeCategoryIndex
|
|
9
|
+
} from "../resource-docs-shared/index.js";
|
|
10
|
+
import {
|
|
11
|
+
assertUniqueSlug,
|
|
12
|
+
renderTableRow,
|
|
13
|
+
warn,
|
|
14
|
+
writeGeneratedPage
|
|
15
|
+
} from "./utils.js";
|
|
16
|
+
function freshState() {
|
|
17
|
+
return { quote: null, triple: false, escaped: false, comment: false };
|
|
18
|
+
}
|
|
19
|
+
function advanceState(text, index, state) {
|
|
20
|
+
const char = text[index] ?? "";
|
|
21
|
+
if (state.comment) {
|
|
22
|
+
if (char === "\n") state.comment = false;
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
if (state.quote !== null) {
|
|
26
|
+
if (state.escaped) {
|
|
27
|
+
state.escaped = false;
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
if (char === "\\") {
|
|
31
|
+
state.escaped = true;
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
if (state.triple && text.slice(index, index + 3) === state.quote.repeat(3)) {
|
|
35
|
+
state.quote = null;
|
|
36
|
+
state.triple = false;
|
|
37
|
+
return 3;
|
|
38
|
+
}
|
|
39
|
+
if (!state.triple && char === state.quote) state.quote = null;
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
if (char === "#") {
|
|
43
|
+
state.comment = true;
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (char === "'" || char === '"') {
|
|
47
|
+
state.quote = char;
|
|
48
|
+
state.triple = text.slice(index, index + 3) === char.repeat(3);
|
|
49
|
+
return state.triple ? 3 : 1;
|
|
50
|
+
}
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
function findPrefixRuleBodies(source) {
|
|
54
|
+
const bodies = [];
|
|
55
|
+
const state = freshState();
|
|
56
|
+
let index = 0;
|
|
57
|
+
while (index < source.length) {
|
|
58
|
+
if (state.quote === null && !state.comment && source.startsWith("prefix_rule", index)) {
|
|
59
|
+
const before = source[index - 1];
|
|
60
|
+
const after = source[index + "prefix_rule".length];
|
|
61
|
+
if ((!before || !/[\w]/.test(before)) && (!after || !/[\w]/.test(after))) {
|
|
62
|
+
let open = index + "prefix_rule".length;
|
|
63
|
+
while (/\s/.test(source[open] ?? "")) open++;
|
|
64
|
+
if (source[open] === "(") {
|
|
65
|
+
const callState = freshState();
|
|
66
|
+
let depth = 1;
|
|
67
|
+
let cursor = open + 1;
|
|
68
|
+
const bodyStart = cursor;
|
|
69
|
+
while (cursor < source.length) {
|
|
70
|
+
if (callState.quote === null && !callState.comment) {
|
|
71
|
+
if (source[cursor] === "(") depth++;
|
|
72
|
+
if (source[cursor] === ")") {
|
|
73
|
+
depth--;
|
|
74
|
+
if (depth === 0) break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
cursor += advanceState(source, cursor, callState);
|
|
78
|
+
}
|
|
79
|
+
bodies.push(source.slice(bodyStart, cursor));
|
|
80
|
+
index = cursor < source.length ? cursor + 1 : source.length;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
index += advanceState(source, index, state);
|
|
86
|
+
}
|
|
87
|
+
return bodies;
|
|
88
|
+
}
|
|
89
|
+
function splitTopLevel(text, delimiter) {
|
|
90
|
+
const parts = [];
|
|
91
|
+
const state = freshState();
|
|
92
|
+
let square = 0;
|
|
93
|
+
let round = 0;
|
|
94
|
+
let curly = 0;
|
|
95
|
+
let start = 0;
|
|
96
|
+
let index = 0;
|
|
97
|
+
while (index < text.length) {
|
|
98
|
+
if (state.quote === null && !state.comment) {
|
|
99
|
+
const char = text[index];
|
|
100
|
+
if (char === "[") square++;
|
|
101
|
+
else if (char === "]") square--;
|
|
102
|
+
else if (char === "(") round++;
|
|
103
|
+
else if (char === ")") round--;
|
|
104
|
+
else if (char === "{") curly++;
|
|
105
|
+
else if (char === "}") curly--;
|
|
106
|
+
else if (char === delimiter && square === 0 && round === 0 && curly === 0) {
|
|
107
|
+
parts.push(text.slice(start, index));
|
|
108
|
+
start = index + 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
index += advanceState(text, index, state);
|
|
112
|
+
}
|
|
113
|
+
parts.push(text.slice(start));
|
|
114
|
+
return parts;
|
|
115
|
+
}
|
|
116
|
+
function parseStringLiteral(value) {
|
|
117
|
+
const text = value.trim();
|
|
118
|
+
const quote = text[0];
|
|
119
|
+
if (quote !== '"' && quote !== "'" || text.at(-1) !== quote) return void 0;
|
|
120
|
+
if (text.startsWith(quote.repeat(3)) && text.endsWith(quote.repeat(3))) {
|
|
121
|
+
return text.slice(3, -3);
|
|
122
|
+
}
|
|
123
|
+
const inner = text.slice(1, -1);
|
|
124
|
+
let result = "";
|
|
125
|
+
for (let i = 0; i < inner.length; i++) {
|
|
126
|
+
const char = inner[i];
|
|
127
|
+
if (char !== "\\") {
|
|
128
|
+
result += char;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const next = inner[++i];
|
|
132
|
+
if (next === void 0) return void 0;
|
|
133
|
+
const escapes = {
|
|
134
|
+
n: "\n",
|
|
135
|
+
r: "\r",
|
|
136
|
+
t: " ",
|
|
137
|
+
"\\": "\\",
|
|
138
|
+
"'": "'",
|
|
139
|
+
'"': '"'
|
|
140
|
+
};
|
|
141
|
+
result += escapes[next] ?? next;
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
function parsePatternValue(text) {
|
|
146
|
+
const trimmed = text.trim();
|
|
147
|
+
const stringValue = parseStringLiteral(trimmed);
|
|
148
|
+
if (stringValue !== void 0) return stringValue;
|
|
149
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
|
|
150
|
+
const inner = trimmed.slice(1, -1);
|
|
151
|
+
if (inner.trim() === "") return [];
|
|
152
|
+
const values = [];
|
|
153
|
+
for (const part of splitTopLevel(inner, ",")) {
|
|
154
|
+
if (part.trim() === "") continue;
|
|
155
|
+
const parsed = parsePatternValue(part);
|
|
156
|
+
if (parsed === void 0) return void 0;
|
|
157
|
+
values.push(parsed);
|
|
158
|
+
}
|
|
159
|
+
return values;
|
|
160
|
+
}
|
|
161
|
+
function formatPattern(value) {
|
|
162
|
+
if (!Array.isArray(value)) return value;
|
|
163
|
+
const parts = [];
|
|
164
|
+
for (const element of value) {
|
|
165
|
+
if (Array.isArray(element)) {
|
|
166
|
+
if (element.some(Array.isArray)) return void 0;
|
|
167
|
+
parts.push(element.join("|"));
|
|
168
|
+
} else {
|
|
169
|
+
parts.push(element);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return parts.join(" ");
|
|
173
|
+
}
|
|
174
|
+
function parseRule(body) {
|
|
175
|
+
const args = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const part of splitTopLevel(body, ",")) {
|
|
177
|
+
const assignment = splitTopLevel(part, "=");
|
|
178
|
+
if (assignment.length !== 2) continue;
|
|
179
|
+
const key = assignment[0]?.trim();
|
|
180
|
+
const value = assignment[1];
|
|
181
|
+
if (key && value !== void 0) args.set(key, value.trim());
|
|
182
|
+
}
|
|
183
|
+
const rawPattern = args.get("pattern");
|
|
184
|
+
const parsedPattern = rawPattern === void 0 ? void 0 : parsePatternValue(rawPattern);
|
|
185
|
+
const decision = args.has("decision") ? parseStringLiteral(args.get("decision") ?? "") : "allow";
|
|
186
|
+
return {
|
|
187
|
+
pattern: parsedPattern === void 0 ? void 0 : formatPattern(parsedPattern),
|
|
188
|
+
decision,
|
|
189
|
+
justification: args.has("justification") ? parseStringLiteral(args.get("justification") ?? "") : void 0
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function generateRulesCategory(config) {
|
|
193
|
+
const rulesDir = path.join(config.codexDir, "rules");
|
|
194
|
+
const outputDir = path.join(config.docsDir, "codex-rules");
|
|
195
|
+
cleanDir(outputDir);
|
|
196
|
+
if (!fs.existsSync(rulesDir)) return [];
|
|
197
|
+
let files;
|
|
198
|
+
try {
|
|
199
|
+
files = fs.readdirSync(rulesDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".rules")).map((entry) => entry.name).sort();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
warn(rulesDir, `unable to read rules directory: ${String(error)}`);
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
if (files.length === 0) return [];
|
|
205
|
+
ensureDir(outputDir);
|
|
206
|
+
const items = [];
|
|
207
|
+
const emitted = /* @__PURE__ */ new Map();
|
|
208
|
+
for (const filename of files) {
|
|
209
|
+
const filePath = path.join(rulesDir, filename);
|
|
210
|
+
const slug = filename.replace(/\.rules$/, "");
|
|
211
|
+
assertUniqueSlug("rule", slug, filePath, emitted);
|
|
212
|
+
let source;
|
|
213
|
+
try {
|
|
214
|
+
source = fs.readFileSync(filePath, "utf8");
|
|
215
|
+
} catch (error) {
|
|
216
|
+
warn(filePath, `unable to read rules file, skipping it: ${String(error)}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const rows = findPrefixRuleBodies(source).map(parseRule);
|
|
220
|
+
const body = [
|
|
221
|
+
"## Rules",
|
|
222
|
+
"",
|
|
223
|
+
"| Pattern | Decision | Justification |",
|
|
224
|
+
"| --- | --- | --- |",
|
|
225
|
+
...rows.length > 0 ? rows.map(
|
|
226
|
+
(row) => renderTableRow([row.pattern, row.decision, row.justification])
|
|
227
|
+
) : [renderTableRow(Array.from({ length: 3 }))],
|
|
228
|
+
"",
|
|
229
|
+
"## Source",
|
|
230
|
+
"",
|
|
231
|
+
renderCodeFence(source, "python")
|
|
232
|
+
].join("\n");
|
|
233
|
+
writeGeneratedPage({
|
|
234
|
+
outputPath: path.join(outputDir, `${slug}.mdx`),
|
|
235
|
+
title: filename,
|
|
236
|
+
description: `Command approval rules from ${filename}`,
|
|
237
|
+
sidebarLabel: filename,
|
|
238
|
+
body
|
|
239
|
+
});
|
|
240
|
+
items.push({ filename, slug });
|
|
241
|
+
}
|
|
242
|
+
if (items.length > 0) {
|
|
243
|
+
writeCategoryIndex(
|
|
244
|
+
outputDir,
|
|
245
|
+
"Rules",
|
|
246
|
+
909,
|
|
247
|
+
"Command approval rules",
|
|
248
|
+
formatFrontmatterString
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return items;
|
|
252
|
+
}
|
|
253
|
+
export {
|
|
254
|
+
generateRulesCategory
|
|
255
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import {
|
|
5
|
+
escapeForMdx,
|
|
6
|
+
formatFrontmatterString,
|
|
7
|
+
generateSkillsCategory
|
|
8
|
+
} from "../resource-docs-shared/index.js";
|
|
9
|
+
import { isRecord, warn } from "./utils.js";
|
|
10
|
+
function optionalMetadataString(record, field, filePath) {
|
|
11
|
+
const value = record[field];
|
|
12
|
+
if (value === void 0) return void 0;
|
|
13
|
+
if (typeof value === "string") return value;
|
|
14
|
+
warn(filePath, `${field} must be a string; omitting it`);
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
function renderOpenAiMetadata(skillAbsDir) {
|
|
18
|
+
const filePath = path.join(skillAbsDir, "agents", "openai.yaml");
|
|
19
|
+
if (!fs.existsSync(filePath)) return "";
|
|
20
|
+
let data;
|
|
21
|
+
try {
|
|
22
|
+
const yaml = fs.readFileSync(filePath, "utf8");
|
|
23
|
+
data = matter(`---
|
|
24
|
+
${yaml}
|
|
25
|
+
---
|
|
26
|
+
`).data;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
warn(filePath, `unable to parse YAML, omitting metadata: ${String(error)}`);
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
if (!isRecord(data)) {
|
|
32
|
+
warn(filePath, "YAML root must be an object; omitting metadata");
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
let interfaceData;
|
|
36
|
+
if (data.interface !== void 0) {
|
|
37
|
+
if (isRecord(data.interface)) interfaceData = data.interface;
|
|
38
|
+
else warn(filePath, "interface must be an object; omitting interface metadata");
|
|
39
|
+
}
|
|
40
|
+
let policy;
|
|
41
|
+
if (data.policy !== void 0) {
|
|
42
|
+
if (isRecord(data.policy)) policy = data.policy;
|
|
43
|
+
else warn(filePath, "policy must be an object; omitting policy metadata");
|
|
44
|
+
}
|
|
45
|
+
const displayName = interfaceData ? optionalMetadataString(interfaceData, "display_name", filePath) : void 0;
|
|
46
|
+
const shortDescription = interfaceData ? optionalMetadataString(interfaceData, "short_description", filePath) : void 0;
|
|
47
|
+
let explicitOnly = false;
|
|
48
|
+
if (policy?.allow_implicit_invocation !== void 0) {
|
|
49
|
+
if (typeof policy.allow_implicit_invocation === "boolean") {
|
|
50
|
+
explicitOnly = policy.allow_implicit_invocation === false;
|
|
51
|
+
} else {
|
|
52
|
+
warn(
|
|
53
|
+
filePath,
|
|
54
|
+
"policy.allow_implicit_invocation must be a boolean; omitting invocation metadata"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
displayName ? `**Display name:** ${escapeForMdx(displayName)}` : "",
|
|
60
|
+
shortDescription ? `**Short description:** ${escapeForMdx(shortDescription)}` : "",
|
|
61
|
+
explicitOnly ? `**Invocation:** explicit only (\`$${path.basename(skillAbsDir)}\`)` : ""
|
|
62
|
+
].filter(Boolean).join("\n");
|
|
63
|
+
}
|
|
64
|
+
function generateSkillsCategoryDocs(config) {
|
|
65
|
+
const projectRoot = config.projectRoot ?? config.codexDir;
|
|
66
|
+
const scanRoot = config.scanRoot ?? projectRoot;
|
|
67
|
+
return generateSkillsCategory({
|
|
68
|
+
skillsDirs: [
|
|
69
|
+
path.join(config.codexDir, "skills"),
|
|
70
|
+
path.join(projectRoot, ".agents", "skills"),
|
|
71
|
+
path.join(scanRoot, ".agents", "skills")
|
|
72
|
+
],
|
|
73
|
+
outputDir: path.join(config.docsDir, "codex-skills"),
|
|
74
|
+
label: "Skills",
|
|
75
|
+
position: 910,
|
|
76
|
+
description: "Skill packages",
|
|
77
|
+
sourceLabel: ".codex/skills",
|
|
78
|
+
renderExtraHeader: renderOpenAiMetadata,
|
|
79
|
+
renderFrontmatterString: formatFrontmatterString
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
generateSkillsCategoryDocs
|
|
84
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function warn(filePath: string, message: string): void;
|
|
2
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
export declare function filenameSlug(filename: string): string;
|
|
4
|
+
export declare function renderTableRow(values: unknown[]): string;
|
|
5
|
+
export declare function assertUniqueSlug(resource: string, slug: string, filePath: string, emitted: Map<string, string>): void;
|
|
6
|
+
export declare function writeGeneratedPage({ outputPath, title, description, sidebarLabel, sidebarPosition, body, }: {
|
|
7
|
+
outputPath: string;
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
sidebarLabel?: string;
|
|
11
|
+
sidebarPosition?: number;
|
|
12
|
+
body: string;
|
|
13
|
+
}): void;
|
|
14
|
+
export declare function languageForFilename(filename: string): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
assertNotIndexReserved,
|
|
5
|
+
escapeMarkdownTableCell,
|
|
6
|
+
formatFrontmatterString
|
|
7
|
+
} from "../resource-docs-shared/index.js";
|
|
8
|
+
function warn(filePath, message) {
|
|
9
|
+
console.warn(`${filePath}: ${message}`);
|
|
10
|
+
}
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function filenameSlug(filename) {
|
|
15
|
+
return filename.replace(/\./g, "-");
|
|
16
|
+
}
|
|
17
|
+
function renderTableRow(values) {
|
|
18
|
+
return `| ${values.map(escapeMarkdownTableCell).join(" | ")} |`;
|
|
19
|
+
}
|
|
20
|
+
function assertUniqueSlug(resource, slug, filePath, emitted) {
|
|
21
|
+
assertNotIndexReserved(
|
|
22
|
+
slug,
|
|
23
|
+
`codex-resources: "${filePath}" maps to the reserved slug "index", which is used for the category metadata file. Rename the file or directory to resolve the conflict.`
|
|
24
|
+
);
|
|
25
|
+
const previous = emitted.get(slug);
|
|
26
|
+
if (previous !== void 0) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`codex-resources: ${resource} slug collision \u2014 "${slug}" is produced by both "${previous}" and "${filePath}". Rename one of the files to resolve the conflict.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
emitted.set(slug, filePath);
|
|
32
|
+
}
|
|
33
|
+
function writeGeneratedPage({
|
|
34
|
+
outputPath,
|
|
35
|
+
title,
|
|
36
|
+
description,
|
|
37
|
+
sidebarLabel,
|
|
38
|
+
sidebarPosition,
|
|
39
|
+
body
|
|
40
|
+
}) {
|
|
41
|
+
const position = sidebarPosition === void 0 ? "" : `sidebar_position: ${sidebarPosition}
|
|
42
|
+
`;
|
|
43
|
+
const label = sidebarLabel === void 0 ? "" : `sidebar_label: ${formatFrontmatterString(sidebarLabel)}
|
|
44
|
+
`;
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
outputPath,
|
|
47
|
+
`---
|
|
48
|
+
title: ${formatFrontmatterString(title)}
|
|
49
|
+
description: ${formatFrontmatterString(description)}
|
|
50
|
+
${position}${label}generated: true
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
${body.trim()}
|
|
54
|
+
`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
function languageForFilename(filename) {
|
|
58
|
+
switch (path.extname(filename).toLowerCase()) {
|
|
59
|
+
case ".sh":
|
|
60
|
+
return "bash";
|
|
61
|
+
case ".py":
|
|
62
|
+
return "python";
|
|
63
|
+
case ".js":
|
|
64
|
+
case ".mjs":
|
|
65
|
+
return "javascript";
|
|
66
|
+
case ".ts":
|
|
67
|
+
return "typescript";
|
|
68
|
+
default:
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
assertUniqueSlug,
|
|
74
|
+
filenameSlug,
|
|
75
|
+
isRecord,
|
|
76
|
+
languageForFilename,
|
|
77
|
+
renderTableRow,
|
|
78
|
+
warn,
|
|
79
|
+
writeGeneratedPage
|
|
80
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
function ensureDir(dir) {
|
|
3
|
+
if (!fs.existsSync(dir)) {
|
|
4
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
function cleanDir(dir) {
|
|
8
|
+
if (!fs.existsSync(dir)) return;
|
|
9
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
10
|
+
}
|
|
11
|
+
function listFiles(dir) {
|
|
12
|
+
if (!fs.existsSync(dir)) return [];
|
|
13
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile()).map((d) => d.name).sort();
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
cleanDir,
|
|
17
|
+
ensureDir,
|
|
18
|
+
listFiles
|
|
19
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { escapeForMdx } from "./escape-for-mdx.js";
|
|
2
|
+
export { cleanDir, ensureDir, listFiles } from "./fs.js";
|
|
3
|
+
export { assertNotIndexReserved, escapeTitle, formatFrontmatterString, parseFrontmatter, type FrontmatterStringRenderer, writeCategoryIndex, writeUnlistedSubPage, } from "./mdx.js";
|
|
4
|
+
export { isRepoRelativeLink, downgradeRepoRelativeLinks } from "./links.js";
|
|
5
|
+
export { escapeMarkdownTableCell, renderCodeFence, } from "./markdown-structure.js";
|
|
6
|
+
export { EXCLUDED_DIR_NAMES, findNamedFiles } from "./walk.js";
|
|
7
|
+
export { generateSkillsCategory, getScriptDescription, getSkillFileTree, getSkillReferences, type GenerateSkillsCategoryOptions, type RenderExtraHeader, type SkillItem, type SkillReference, } from "./skills.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { escapeForMdx } from "./escape-for-mdx.js";
|
|
2
|
+
import { cleanDir, ensureDir, listFiles } from "./fs.js";
|
|
3
|
+
import {
|
|
4
|
+
assertNotIndexReserved,
|
|
5
|
+
escapeTitle,
|
|
6
|
+
formatFrontmatterString,
|
|
7
|
+
parseFrontmatter,
|
|
8
|
+
writeCategoryIndex,
|
|
9
|
+
writeUnlistedSubPage
|
|
10
|
+
} from "./mdx.js";
|
|
11
|
+
import { isRepoRelativeLink, downgradeRepoRelativeLinks } from "./links.js";
|
|
12
|
+
import {
|
|
13
|
+
escapeMarkdownTableCell,
|
|
14
|
+
renderCodeFence
|
|
15
|
+
} from "./markdown-structure.js";
|
|
16
|
+
import { EXCLUDED_DIR_NAMES, findNamedFiles } from "./walk.js";
|
|
17
|
+
import {
|
|
18
|
+
generateSkillsCategory,
|
|
19
|
+
getScriptDescription,
|
|
20
|
+
getSkillFileTree,
|
|
21
|
+
getSkillReferences
|
|
22
|
+
} from "./skills.js";
|
|
23
|
+
export {
|
|
24
|
+
EXCLUDED_DIR_NAMES,
|
|
25
|
+
assertNotIndexReserved,
|
|
26
|
+
cleanDir,
|
|
27
|
+
downgradeRepoRelativeLinks,
|
|
28
|
+
ensureDir,
|
|
29
|
+
escapeForMdx,
|
|
30
|
+
escapeMarkdownTableCell,
|
|
31
|
+
escapeTitle,
|
|
32
|
+
findNamedFiles,
|
|
33
|
+
formatFrontmatterString,
|
|
34
|
+
generateSkillsCategory,
|
|
35
|
+
getScriptDescription,
|
|
36
|
+
getSkillFileTree,
|
|
37
|
+
getSkillReferences,
|
|
38
|
+
isRepoRelativeLink,
|
|
39
|
+
listFiles,
|
|
40
|
+
parseFrontmatter,
|
|
41
|
+
renderCodeFence,
|
|
42
|
+
writeCategoryIndex,
|
|
43
|
+
writeUnlistedSubPage
|
|
44
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a markdown link target is a repo-relative file reference
|
|
3
|
+
* (`./wrangler.toml`, `../../schema/photos.sql`, `foo/bar.md`) rather than
|
|
4
|
+
* something the doc site can resolve: an absolute URL (`https://…`), a
|
|
5
|
+
* protocol-relative URL (`//…`), a site-absolute path (`/docs/…`), a pure
|
|
6
|
+
* anchor (`#…`), or a scheme (`mailto:`, `tel:`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function isRepoRelativeLink(url: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Downgrade repo-relative markdown links in a mirrored `CLAUDE.md` body to
|
|
11
|
+
* inline code so they don't dangle in the flattened mirror tree (#2411).
|
|
12
|
+
*
|
|
13
|
+
* A `CLAUDE.md`'s relative links point at real repo files (correct for an
|
|
14
|
+
* in-repo reader), but the mirror flattens each file into a single
|
|
15
|
+
* `claude-md/<name>.mdx` page with no counterpart for those targets — left as
|
|
16
|
+
* links they surface as `broken link:` warnings on every affected page. Inline
|
|
17
|
+
* code keeps the reference legible (`` `wrangler.toml` ``) without a href.
|
|
18
|
+
*
|
|
19
|
+
* Code spans are preserved verbatim: a `[x](./y)` inside a fenced block or an
|
|
20
|
+
* inline-code span is literal text, not a link, and must not be rewritten.
|
|
21
|
+
*/
|
|
22
|
+
export declare function downgradeRepoRelativeLinks(content: string): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function isRepoRelativeLink(url) {
|
|
2
|
+
const trimmed = url.trim();
|
|
3
|
+
if (trimmed === "") return false;
|
|
4
|
+
if (trimmed.startsWith("#")) return false;
|
|
5
|
+
if (trimmed.startsWith("/")) return false;
|
|
6
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return false;
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
function downgradeRepoRelativeLinks(content) {
|
|
10
|
+
const blockPlaceholder = "\0CRLINK_BLOCK_";
|
|
11
|
+
const inlinePlaceholder = "\0CRLINK_INLINE_";
|
|
12
|
+
const codeBlocks = [];
|
|
13
|
+
const withBlocks = content.replace(/(`{3,}|~{3,})[^\n]*\n[\s\S]*?\1/g, (match) => {
|
|
14
|
+
codeBlocks.push(match);
|
|
15
|
+
return `${blockPlaceholder}${codeBlocks.length - 1}\0`;
|
|
16
|
+
});
|
|
17
|
+
const transformed = withBlocks.split(new RegExp(`(${blockPlaceholder}\\d+\0)`, "g")).map((part) => {
|
|
18
|
+
if (new RegExp(`^${blockPlaceholder}\\d+\0$`).test(part)) return part;
|
|
19
|
+
const inlineCodes = [];
|
|
20
|
+
const withInline = part.replace(
|
|
21
|
+
/(`{1,3})(?!`)([\s\S]*?[^`])\1(?!`)/g,
|
|
22
|
+
(match) => {
|
|
23
|
+
inlineCodes.push(match);
|
|
24
|
+
return `${inlinePlaceholder}${inlineCodes.length - 1}\0`;
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
const rewritten = withInline.replace(
|
|
28
|
+
/!?\[([^\]]*)\]\(([^)]+)\)/g,
|
|
29
|
+
(match, text, url) => isRepoRelativeLink(url) ? `\`${text}\`` : match
|
|
30
|
+
);
|
|
31
|
+
return rewritten.replace(
|
|
32
|
+
new RegExp(`${inlinePlaceholder}(\\d+)\0`, "g"),
|
|
33
|
+
(_, idx) => inlineCodes[Number(idx)] ?? ""
|
|
34
|
+
);
|
|
35
|
+
}).join("");
|
|
36
|
+
return transformed.replace(
|
|
37
|
+
new RegExp(`${blockPlaceholder}(\\d+)\0`, "g"),
|
|
38
|
+
(_, idx) => codeBlocks[Number(idx)] ?? ""
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
downgradeRepoRelativeLinks,
|
|
43
|
+
isRepoRelativeLink
|
|
44
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Render source inside a Markdown fence that cannot be closed by the source. */
|
|
2
|
+
export declare function renderCodeFence(source: string, language?: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Render an arbitrary value as a structurally safe Markdown table cell.
|
|
5
|
+
*
|
|
6
|
+
* Code-span delimiters are chosen from the value itself. Pipes are escaped
|
|
7
|
+
* for the surrounding table and physical newlines are collapsed to spaces.
|
|
8
|
+
*/
|
|
9
|
+
export declare function escapeMarkdownTableCell(value: unknown): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function renderCodeFence(source, language = "") {
|
|
2
|
+
const runs = source.match(/`+/g) ?? [];
|
|
3
|
+
const longestRun = runs.reduce((max, run) => Math.max(max, run.length), 0);
|
|
4
|
+
const fence = "`".repeat(Math.max(3, longestRun + 1));
|
|
5
|
+
const trailingNewline = source.endsWith("\n") ? "" : "\n";
|
|
6
|
+
return `${fence}${language}
|
|
7
|
+
${source}${trailingNewline}${fence}`;
|
|
8
|
+
}
|
|
9
|
+
function escapeMarkdownTableCell(value) {
|
|
10
|
+
if (value === void 0 || value === null || value === "") return "\u2014";
|
|
11
|
+
const content = String(value).replace(/\r\n?|\n/g, " ").replace(/\|/g, "\\|");
|
|
12
|
+
if (content === "") return "\u2014";
|
|
13
|
+
const runs = content.match(/`+/g) ?? [];
|
|
14
|
+
const longestRun = runs.reduce((max, run) => Math.max(max, run.length), 0);
|
|
15
|
+
const delimiter = "`".repeat(Math.max(1, longestRun + 1));
|
|
16
|
+
const needsPadding = /^[` ]|[` ]$/.test(content);
|
|
17
|
+
return needsPadding ? `${delimiter} ${content} ${delimiter}` : `${delimiter}${content}${delimiter}`;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
escapeMarkdownTableCell,
|
|
21
|
+
renderCodeFence
|
|
22
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import matter from "gray-matter";
|
|
2
|
+
export declare function parseFrontmatter(content: string): matter.GrayMatterFile<string> | null;
|
|
3
|
+
export declare function escapeTitle(s: string): string;
|
|
4
|
+
export type FrontmatterStringRenderer = (value: string) => string;
|
|
5
|
+
export declare function formatFrontmatterString(value: string): string;
|
|
6
|
+
export declare function writeCategoryIndex(outputDir: string, label: string, position: number, description: string, renderString?: FrontmatterStringRenderer): void;
|
|
7
|
+
/**
|
|
8
|
+
* Writes an unlisted sub-page MDX file. Used for skill references, scripts,
|
|
9
|
+
* and assets.
|
|
10
|
+
*
|
|
11
|
+
* The route is derived from the file's path within the content collection —
|
|
12
|
+
* deliberately NOT from an explicit `slug:`. zfb's `resolveMarkdownLinks`
|
|
13
|
+
* resolves relative links against the source file path, so the on-disk
|
|
14
|
+
* location of these pages must match the URL the skill page links to. Writing
|
|
15
|
+
* them at `<dir>/ref-<name>.mdx` (siblings of the skill's `index.mdx`) is what
|
|
16
|
+
* makes the `./ref-<name>` links resolve (#2411).
|
|
17
|
+
*/
|
|
18
|
+
export declare function writeUnlistedSubPage(outputPath: string, title: string, body: string, renderString?: FrontmatterStringRenderer): void;
|
|
19
|
+
/**
|
|
20
|
+
* Guards that the given name/slug is not the reserved "index" value.
|
|
21
|
+
* Throws with a contextual message if it is.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertNotIndexReserved(nameOrSlug: string, errorMessage: string): void;
|