@farming-labs/docs 0.1.119 → 0.1.120
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/{agent-D8DpCgt_.mjs → agent-ByXnegrS.mjs} +3 -3
- package/dist/{agents-XyolXdXp.mjs → agents-CpTNRbsh.mjs} +2 -2
- package/dist/cli/index.mjs +33 -15
- package/dist/{dev-FC6Fh7nT.mjs → dev-C03tUSTz.mjs} +2 -2
- package/dist/{doctor-CU4knIej.mjs → doctor-DMs3Q0wj.mjs} +4 -4
- package/dist/{downgrade-Bt4yrVyy.mjs → downgrade-Bv7E5LV2.mjs} +2 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1 -1
- package/dist/{init-BgzyLAay.mjs → init-_HAuo5Dv.mjs} +2 -2
- package/dist/{mcp-BMgH1Q33.mjs → mcp-DojNlB8t.mjs} +1 -1
- package/dist/mcp.d.mts +1 -1
- package/dist/{package-version-DQgrHnSb.mjs → package-version-L4GZowaF.mjs} +1 -1
- package/dist/{reading-time-DNLXwuqA.mjs → reading-time-Io7iRZ7S.mjs} +2 -1
- package/dist/review-BHFhvl2F.mjs +475 -0
- package/dist/review-_5fnI667.mjs +553 -0
- package/dist/{robots-Byj0knC3.mjs → robots-BxZaiGH3.mjs} +2 -2
- package/dist/{search-BQ1cY913.mjs → search-DKpKe0rf.mjs} +1 -1
- package/dist/{search-Dqu1Q27e.d.mts → search-Dd0kOr6B.d.mts} +1 -1
- package/dist/server.d.mts +65 -3
- package/dist/server.mjs +2 -1
- package/dist/{sitemap-mqWvYODL.mjs → sitemap-CXwYOIIb.mjs} +2 -2
- package/dist/{types-Dts3a32G.d.mts → types-DtBNjsk2.d.mts} +99 -1
- package/dist/{upgrade-B1EMfRQJ.mjs → upgrade-DrOWQIKI.mjs} +2 -2
- package/package.json +1 -1
- /package/dist/{config-Cio3byUJ.mjs → config-BHRL4R2v.mjs} +0 -0
- /package/dist/{templates-CkL3bEE5.mjs → templates-CakZBXK8.mjs} +0 -0
- /package/dist/{utils-TPe8H1P-.mjs → utils-x5EtYWjC.mjs} +0 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { a as ensureDocsReviewWorkflow, o as readDocsReviewConfigFromSource, s as resolveDocsReviewConfig } from "./review-_5fnI667.mjs";
|
|
2
|
+
import { d as readTopLevelStringProperty, f as resolveDocsConfigPath, i as loadDocsConfigModule, p as resolveDocsContentDir } from "./config-BHRL4R2v.mjs";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
//#region src/cli/review.ts
|
|
10
|
+
const DOCS_FILE_PATTERN = /\.(?:md|mdx)$/;
|
|
11
|
+
const IGNORED_DIRS = new Set([
|
|
12
|
+
".git",
|
|
13
|
+
".next",
|
|
14
|
+
".nuxt",
|
|
15
|
+
".output",
|
|
16
|
+
".svelte-kit",
|
|
17
|
+
"coverage",
|
|
18
|
+
"dist",
|
|
19
|
+
"node_modules",
|
|
20
|
+
"out"
|
|
21
|
+
]);
|
|
22
|
+
function parseReviewArgs(argv) {
|
|
23
|
+
const parsed = {};
|
|
24
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
25
|
+
const arg = argv[index];
|
|
26
|
+
if (arg === "setup") {
|
|
27
|
+
parsed.setup = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === "--help" || arg === "-h") {
|
|
31
|
+
parsed.help = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg === "--ci") {
|
|
35
|
+
parsed.ci = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === "--json") {
|
|
39
|
+
parsed.json = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg === "--config" || arg === "--base" || arg === "--head" || arg === "--mode") {
|
|
43
|
+
const value = argv[index + 1];
|
|
44
|
+
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${arg}.`);
|
|
45
|
+
if (arg === "--config") parsed.configPath = value;
|
|
46
|
+
if (arg === "--base") parsed.base = value;
|
|
47
|
+
if (arg === "--head") parsed.head = value;
|
|
48
|
+
if (arg === "--mode") parsed.mode = parseReviewCiMode(value);
|
|
49
|
+
index += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (arg === "--score-threshold") {
|
|
53
|
+
const value = argv[index + 1];
|
|
54
|
+
if (!value || value.startsWith("--")) throw new Error("Missing value for --score-threshold.");
|
|
55
|
+
parsed.scoreThreshold = Number.parseInt(value, 10);
|
|
56
|
+
index += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg.startsWith("--config=")) {
|
|
60
|
+
parsed.configPath = readInlineFlag(arg, "--config=");
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg.startsWith("--base=")) {
|
|
64
|
+
parsed.base = readInlineFlag(arg, "--base=");
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg.startsWith("--head=")) {
|
|
68
|
+
parsed.head = readInlineFlag(arg, "--head=");
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (arg.startsWith("--mode=")) {
|
|
72
|
+
parsed.mode = parseReviewCiMode(readInlineFlag(arg, "--mode="));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg.startsWith("--score-threshold=")) {
|
|
76
|
+
parsed.scoreThreshold = Number.parseInt(readInlineFlag(arg, "--score-threshold="), 10);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Unknown review flag: ${arg}.`);
|
|
80
|
+
}
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
|
83
|
+
async function runReview(options = {}) {
|
|
84
|
+
const rootDir = process.cwd();
|
|
85
|
+
const configPath = resolveDocsConfigPath(rootDir, options.configPath);
|
|
86
|
+
const configContent = readFileSync(configPath, "utf-8");
|
|
87
|
+
const config = (await loadDocsConfigModule(rootDir, options.configPath))?.config;
|
|
88
|
+
const review = withReviewOptionOverrides(resolveDocsReviewConfig(config?.review ?? readDocsReviewConfigFromSource(configContent)), options.mode, options.scoreThreshold);
|
|
89
|
+
if (options.setup) {
|
|
90
|
+
const result = ensureDocsReviewWorkflow({
|
|
91
|
+
rootDir,
|
|
92
|
+
config,
|
|
93
|
+
configPath: path.relative(rootDir, configPath),
|
|
94
|
+
configContent,
|
|
95
|
+
log: (message) => console.log(pc.green(message))
|
|
96
|
+
});
|
|
97
|
+
if (result.status === "exists") console.log(pc.dim(`${result.relativePath} already exists.`));
|
|
98
|
+
if (result.status === "disabled") console.log(pc.yellow("Docs Review CI is disabled in docs.config."));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!review.enabled) {
|
|
102
|
+
const report = {
|
|
103
|
+
score: 100,
|
|
104
|
+
threshold: review.score.threshold,
|
|
105
|
+
mode: options.ci ? review.ci.mode : "local",
|
|
106
|
+
reviewedFiles: [],
|
|
107
|
+
changedFiles: [],
|
|
108
|
+
findings: []
|
|
109
|
+
};
|
|
110
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
111
|
+
else console.log(pc.yellow("Docs Review is disabled in docs.config."));
|
|
112
|
+
return report;
|
|
113
|
+
}
|
|
114
|
+
const entry = config?.entry ?? readTopLevelStringProperty(configContent, "entry") ?? "docs";
|
|
115
|
+
const contentDir = config?.contentDir ?? resolveDocsContentDir(rootDir, configContent, entry);
|
|
116
|
+
const pages = scanDocsPages(rootDir, contentDir, entry);
|
|
117
|
+
const changedFiles = getChangedFiles(rootDir, options);
|
|
118
|
+
const relevantFiles = selectReviewFiles({
|
|
119
|
+
changedFiles,
|
|
120
|
+
pages,
|
|
121
|
+
configPath: path.relative(rootDir, configPath),
|
|
122
|
+
rootDir,
|
|
123
|
+
contentDir
|
|
124
|
+
});
|
|
125
|
+
const findings = collectReviewFindings({
|
|
126
|
+
rootDir,
|
|
127
|
+
entry,
|
|
128
|
+
pages,
|
|
129
|
+
files: relevantFiles,
|
|
130
|
+
review
|
|
131
|
+
});
|
|
132
|
+
const score = calculateReviewScore(findings, review);
|
|
133
|
+
const mode = options.ci ? review.ci.mode : "local";
|
|
134
|
+
const report = {
|
|
135
|
+
score,
|
|
136
|
+
threshold: review.score.threshold,
|
|
137
|
+
mode,
|
|
138
|
+
reviewedFiles: relevantFiles,
|
|
139
|
+
changedFiles,
|
|
140
|
+
findings
|
|
141
|
+
};
|
|
142
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
143
|
+
else printReviewReport(report);
|
|
144
|
+
if (options.ci && review.ci.annotations) emitGitHubAnnotations(report.findings);
|
|
145
|
+
if (options.ci && review.ci.mode === "block" && (score < review.score.threshold || findings.some((finding) => finding.severity === "error"))) process.exitCode = 1;
|
|
146
|
+
return report;
|
|
147
|
+
}
|
|
148
|
+
function printReviewHelp() {
|
|
149
|
+
console.log(`
|
|
150
|
+
${pc.bold("docs review")} — Review changed docs content for CI and agent-readiness.
|
|
151
|
+
|
|
152
|
+
${pc.dim("Usage:")}
|
|
153
|
+
pnpm exec docs ${pc.cyan("review")} ${pc.dim("[--ci]")}
|
|
154
|
+
pnpm exec docs ${pc.cyan("review setup")}
|
|
155
|
+
|
|
156
|
+
${pc.dim("Options:")}
|
|
157
|
+
${pc.cyan("--ci")} Use docs.config review.ci behavior and GitHub annotations
|
|
158
|
+
${pc.cyan("--json")} Print JSON report
|
|
159
|
+
${pc.cyan("--config <path>")} Use a custom docs config path
|
|
160
|
+
${pc.cyan("--base <ref> --head <ref>")} Review files changed between two git refs
|
|
161
|
+
${pc.cyan("--mode <off|warn|block>")} Override review.ci.mode
|
|
162
|
+
${pc.cyan("--score-threshold <0-100>")} Override review.score.threshold
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
function withReviewOptionOverrides(review, mode, scoreThreshold) {
|
|
166
|
+
return {
|
|
167
|
+
...review,
|
|
168
|
+
ci: mode ? {
|
|
169
|
+
...review.ci,
|
|
170
|
+
mode,
|
|
171
|
+
enabled: mode !== "off"
|
|
172
|
+
} : review.ci,
|
|
173
|
+
score: typeof scoreThreshold === "number" && Number.isFinite(scoreThreshold) ? {
|
|
174
|
+
...review.score,
|
|
175
|
+
threshold: Math.max(0, Math.min(100, Math.round(scoreThreshold)))
|
|
176
|
+
} : review.score
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function collectReviewFindings(options) {
|
|
180
|
+
const findings = [];
|
|
181
|
+
const knownRoutes = /* @__PURE__ */ new Set();
|
|
182
|
+
const slugSources = /* @__PURE__ */ new Map();
|
|
183
|
+
for (const page of options.pages) {
|
|
184
|
+
knownRoutes.add(page.route);
|
|
185
|
+
knownRoutes.add(page.markdownRoute);
|
|
186
|
+
if (slugSources.has(page.route)) pushFinding(findings, options.review, {
|
|
187
|
+
rule: "duplicateSlugs",
|
|
188
|
+
severity: "error",
|
|
189
|
+
file: page.relativePath,
|
|
190
|
+
message: `Duplicate docs route ${page.route}; already used by ${slugSources.get(page.route)}.`
|
|
191
|
+
});
|
|
192
|
+
else slugSources.set(page.route, page.relativePath);
|
|
193
|
+
}
|
|
194
|
+
for (const file of options.files) {
|
|
195
|
+
if (!DOCS_FILE_PATTERN.test(file)) continue;
|
|
196
|
+
const absolutePath = path.join(options.rootDir, file);
|
|
197
|
+
if (!existsSync(absolutePath)) continue;
|
|
198
|
+
let source = "";
|
|
199
|
+
let parsed;
|
|
200
|
+
try {
|
|
201
|
+
source = readFileSync(absolutePath, "utf-8");
|
|
202
|
+
parsed = matter(source);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
pushFinding(findings, options.review, {
|
|
205
|
+
rule: "invalidMdx",
|
|
206
|
+
severity: "error",
|
|
207
|
+
file,
|
|
208
|
+
message: `Could not read or parse this docs file: ${error instanceof Error ? error.message : String(error)}`
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (!file.endsWith("agent.md")) {
|
|
213
|
+
checkFrontmatter(findings, options.review, file, parsed.data);
|
|
214
|
+
checkAgentContext(findings, options.review, {
|
|
215
|
+
file,
|
|
216
|
+
source,
|
|
217
|
+
rootDir: options.rootDir
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
checkBrokenLinks(findings, options.review, {
|
|
221
|
+
file,
|
|
222
|
+
source,
|
|
223
|
+
entry: options.entry,
|
|
224
|
+
knownRoutes
|
|
225
|
+
});
|
|
226
|
+
checkCodeFences(findings, options.review, {
|
|
227
|
+
file,
|
|
228
|
+
source
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return findings;
|
|
232
|
+
}
|
|
233
|
+
function checkFrontmatter(findings, review, file, data) {
|
|
234
|
+
if (!data.title || typeof data.title !== "string") pushFinding(findings, review, {
|
|
235
|
+
rule: "frontmatter",
|
|
236
|
+
severity: "error",
|
|
237
|
+
file,
|
|
238
|
+
line: 1,
|
|
239
|
+
message: "Missing frontmatter title."
|
|
240
|
+
});
|
|
241
|
+
if (!data.description || typeof data.description !== "string") pushFinding(findings, review, {
|
|
242
|
+
rule: "frontmatter",
|
|
243
|
+
severity: "error",
|
|
244
|
+
file,
|
|
245
|
+
line: 1,
|
|
246
|
+
message: "Missing frontmatter description."
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
function checkBrokenLinks(findings, review, options) {
|
|
250
|
+
const linkPattern = /\[[^\]]+\]\(([^)]+)\)|href=["']([^"']+)["']/g;
|
|
251
|
+
let match;
|
|
252
|
+
while (match = linkPattern.exec(options.source)) {
|
|
253
|
+
if (match[1] && options.source[match.index - 1] === "!") continue;
|
|
254
|
+
const href = match[1] ?? match[2];
|
|
255
|
+
if (!href || !href.startsWith("/")) continue;
|
|
256
|
+
const normalized = normalizeInternalHref(href);
|
|
257
|
+
if (!normalized || !normalized.startsWith(`/${options.entry}`)) continue;
|
|
258
|
+
if (options.knownRoutes.has(normalized)) continue;
|
|
259
|
+
pushFinding(findings, review, {
|
|
260
|
+
rule: "brokenLinks",
|
|
261
|
+
severity: "error",
|
|
262
|
+
file: options.file,
|
|
263
|
+
line: lineForIndex(options.source, match.index),
|
|
264
|
+
message: `Internal docs link does not resolve: ${href}`
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function checkCodeFences(findings, review, options) {
|
|
269
|
+
const fencePattern = /^```([^\n`]*)\n([\s\S]*?)^```/gm;
|
|
270
|
+
let match;
|
|
271
|
+
while (match = fencePattern.exec(options.source)) {
|
|
272
|
+
const info = match[1]?.trim() ?? "";
|
|
273
|
+
const code = match[2] ?? "";
|
|
274
|
+
const language = info.split(/\s+/)[0] ?? "";
|
|
275
|
+
if (!language || !isImplementationLanguage(language)) continue;
|
|
276
|
+
const hasTitle = /\btitle=/.test(info);
|
|
277
|
+
const hasPackageManager = /\bpackageManager=/.test(info);
|
|
278
|
+
const isRunnable = /\brunnable\b/.test(info);
|
|
279
|
+
const isConfigExample = /\bdefineDocs\s*\(|\bwithDocs\s*\(|\bdocs\.config\b/.test(code);
|
|
280
|
+
if (!hasTitle && (isRunnable || isConfigExample)) pushFinding(findings, review, {
|
|
281
|
+
rule: "codeFenceMetadata",
|
|
282
|
+
severity: "warn",
|
|
283
|
+
file: options.file,
|
|
284
|
+
line: lineForIndex(options.source, match.index),
|
|
285
|
+
message: "Code block is missing title metadata, e.g. title=\"docs.config.ts\"."
|
|
286
|
+
});
|
|
287
|
+
if (isRunnable && !hasPackageManager) pushFinding(findings, review, {
|
|
288
|
+
rule: "runnableMetadata",
|
|
289
|
+
severity: "warn",
|
|
290
|
+
file: options.file,
|
|
291
|
+
line: lineForIndex(options.source, match.index),
|
|
292
|
+
message: "Runnable code block is missing packageManager metadata."
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function checkAgentContext(findings, review, options) {
|
|
297
|
+
if (options.source.includes("<Agent>") || options.source.includes("</Agent>")) return;
|
|
298
|
+
if (existsSync(path.join(path.dirname(path.join(options.rootDir, options.file)), "agent.md"))) return;
|
|
299
|
+
if (!/\b(install|configure|setup|implement|defineDocs|docs\.config|MCP|agent)\b/i.test(options.source)) return;
|
|
300
|
+
pushFinding(findings, review, {
|
|
301
|
+
rule: "agentContext",
|
|
302
|
+
severity: "suggestion",
|
|
303
|
+
file: options.file,
|
|
304
|
+
line: 1,
|
|
305
|
+
message: "Implementation-heavy docs page could use an <Agent> block or sibling agent.md."
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function pushFinding(findings, review, finding) {
|
|
309
|
+
const configured = review.rules[finding.rule];
|
|
310
|
+
if (configured === "off") return;
|
|
311
|
+
findings.push({
|
|
312
|
+
...finding,
|
|
313
|
+
severity: configured === "error" || configured === "warn" ? configured : "suggestion"
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function calculateReviewScore(findings, review) {
|
|
317
|
+
const penalty = findings.reduce((total, finding) => {
|
|
318
|
+
if (finding.severity === "error") return total + review.score.weights.error;
|
|
319
|
+
if (finding.severity === "warn") return total + review.score.weights.warn;
|
|
320
|
+
return total + review.score.weights.suggestion;
|
|
321
|
+
}, 0);
|
|
322
|
+
return Math.max(0, 100 - penalty);
|
|
323
|
+
}
|
|
324
|
+
function printReviewReport(report) {
|
|
325
|
+
const counts = countFindings(report.findings);
|
|
326
|
+
const modeLabel = report.mode === "local" ? "local" : report.mode;
|
|
327
|
+
console.log(pc.bold("Docs Review"));
|
|
328
|
+
console.log("");
|
|
329
|
+
console.log(`Score: ${scoreColor(report.score, report.threshold)} / 100`);
|
|
330
|
+
console.log(`Threshold: ${report.threshold}`);
|
|
331
|
+
console.log(`Mode: ${modeLabel}`);
|
|
332
|
+
console.log(`Changed files: ${report.changedFiles.length}`);
|
|
333
|
+
console.log(`Reviewed docs files: ${report.reviewedFiles.length}`);
|
|
334
|
+
console.log(`Findings: ${counts.error} error${counts.error === 1 ? "" : "s"}, ${counts.warn} warning${counts.warn === 1 ? "" : "s"}, ${counts.suggestion} suggestion${counts.suggestion === 1 ? "" : "s"}`);
|
|
335
|
+
if (report.reviewedFiles.length === 0) {
|
|
336
|
+
console.log("");
|
|
337
|
+
console.log(pc.green("No docs changes detected. Skipping review."));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (report.findings.length === 0) {
|
|
341
|
+
console.log("");
|
|
342
|
+
console.log(pc.green("No docs review findings."));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
console.log("");
|
|
346
|
+
for (const finding of report.findings) {
|
|
347
|
+
const label = finding.severity === "error" ? pc.red("ERROR") : finding.severity === "warn" ? pc.yellow("WARN") : pc.cyan("SUGGEST");
|
|
348
|
+
const location = `${finding.file}${finding.line ? `:${finding.line}` : ""}`;
|
|
349
|
+
console.log(`${label} ${pc.dim(location)} ${finding.message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function emitGitHubAnnotations(findings) {
|
|
353
|
+
if (process.env.GITHUB_ACTIONS !== "true") return;
|
|
354
|
+
for (const finding of findings) {
|
|
355
|
+
const command = finding.severity === "error" ? "error" : finding.severity === "warn" ? "warning" : "notice";
|
|
356
|
+
const location = [`file=${escapeGitHubAnnotationValue(finding.file)}`, finding.line ? `line=${finding.line}` : void 0].filter(Boolean).join(",");
|
|
357
|
+
console.log(`::${command} ${location}::${escapeGitHubAnnotationValue(finding.message)}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function scoreColor(score, threshold) {
|
|
361
|
+
if (score < threshold) return pc.red(String(score));
|
|
362
|
+
if (score < Math.min(100, threshold + 10)) return pc.yellow(String(score));
|
|
363
|
+
return pc.green(String(score));
|
|
364
|
+
}
|
|
365
|
+
function countFindings(findings) {
|
|
366
|
+
return findings.reduce((counts, finding) => {
|
|
367
|
+
counts[finding.severity] += 1;
|
|
368
|
+
return counts;
|
|
369
|
+
}, {
|
|
370
|
+
error: 0,
|
|
371
|
+
warn: 0,
|
|
372
|
+
suggestion: 0
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function selectReviewFiles(options) {
|
|
376
|
+
const pageFiles = new Set(options.pages.map((page) => page.relativePath));
|
|
377
|
+
const normalizedConfigPath = toPosixPath(options.configPath);
|
|
378
|
+
if (options.changedFiles.includes(normalizedConfigPath)) return Array.from(pageFiles).sort();
|
|
379
|
+
return options.changedFiles.map(toPosixPath).filter((file) => pageFiles.has(file)).sort();
|
|
380
|
+
}
|
|
381
|
+
function getChangedFiles(rootDir, options) {
|
|
382
|
+
const ranges = [
|
|
383
|
+
options.base && options.head ? `${options.base}...${options.head}` : void 0,
|
|
384
|
+
process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}...HEAD` : void 0,
|
|
385
|
+
"HEAD~1...HEAD",
|
|
386
|
+
void 0
|
|
387
|
+
].filter((range, index, allRanges) => range !== void 0 || index === allRanges.length - 1);
|
|
388
|
+
for (const range of ranges) try {
|
|
389
|
+
const args = [
|
|
390
|
+
"diff",
|
|
391
|
+
"--relative",
|
|
392
|
+
"--name-only",
|
|
393
|
+
"--diff-filter=ACMRTUXB"
|
|
394
|
+
];
|
|
395
|
+
if (range) args.push(range);
|
|
396
|
+
const files = execFileSync("git", args, {
|
|
397
|
+
cwd: rootDir,
|
|
398
|
+
encoding: "utf-8"
|
|
399
|
+
}).split(/\r?\n/).map((file) => file.trim()).filter(Boolean).map(toPosixPath);
|
|
400
|
+
if (files.length > 0 || range === void 0) return files;
|
|
401
|
+
} catch {}
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
function scanDocsPages(rootDir, contentDir, entry) {
|
|
405
|
+
const contentRoot = path.isAbsolute(contentDir) ? contentDir : path.join(rootDir, contentDir);
|
|
406
|
+
if (!existsSync(contentRoot)) return [];
|
|
407
|
+
return listFiles(contentRoot).filter((file) => DOCS_FILE_PATTERN.test(file)).map((absolutePath) => {
|
|
408
|
+
const relativeToContent = toPosixPath(path.relative(contentRoot, absolutePath));
|
|
409
|
+
const relativePath = toPosixPath(path.relative(rootDir, absolutePath));
|
|
410
|
+
const slug = docsSlugFromFile(relativeToContent);
|
|
411
|
+
const route = normalizeRoute(`/${entry}${slug ? `/${slug}` : ""}`);
|
|
412
|
+
return {
|
|
413
|
+
relativePath,
|
|
414
|
+
absolutePath,
|
|
415
|
+
route,
|
|
416
|
+
markdownRoute: `${route}.md`
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
function listFiles(dir) {
|
|
421
|
+
const files = [];
|
|
422
|
+
for (const name of readdirSync(dir)) {
|
|
423
|
+
if (IGNORED_DIRS.has(name)) continue;
|
|
424
|
+
const fullPath = path.join(dir, name);
|
|
425
|
+
let stat;
|
|
426
|
+
try {
|
|
427
|
+
stat = lstatSync(fullPath);
|
|
428
|
+
} catch {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (stat.isDirectory()) files.push(...listFiles(fullPath));
|
|
432
|
+
else if (stat.isFile()) files.push(fullPath);
|
|
433
|
+
}
|
|
434
|
+
return files;
|
|
435
|
+
}
|
|
436
|
+
function docsSlugFromFile(relativePath) {
|
|
437
|
+
const withoutExt = relativePath.replace(/\.(?:md|mdx)$/, "");
|
|
438
|
+
if (withoutExt === "page" || withoutExt === "index") return "";
|
|
439
|
+
if (withoutExt.endsWith("/page") || withoutExt.endsWith("/index")) return withoutExt.replace(/\/(?:page|index)$/, "");
|
|
440
|
+
return withoutExt;
|
|
441
|
+
}
|
|
442
|
+
function normalizeInternalHref(href) {
|
|
443
|
+
const [withoutHash] = href.split("#");
|
|
444
|
+
const [withoutQuery] = withoutHash.split("?");
|
|
445
|
+
if (!withoutQuery || withoutQuery === "/") return "/";
|
|
446
|
+
return normalizeRoute(withoutQuery.replace(/\.md$/, ""));
|
|
447
|
+
}
|
|
448
|
+
function normalizeRoute(route) {
|
|
449
|
+
const normalized = `/${route}`.replace(/\/+/g, "/");
|
|
450
|
+
return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
|
|
451
|
+
}
|
|
452
|
+
function isImplementationLanguage(language) {
|
|
453
|
+
return /^(?:bash|sh|shell|zsh|ts|tsx|js|jsx|json|mdx?)$/.test(language);
|
|
454
|
+
}
|
|
455
|
+
function lineForIndex(source, index) {
|
|
456
|
+
return source.slice(0, index).split(/\r?\n/).length;
|
|
457
|
+
}
|
|
458
|
+
function escapeGitHubAnnotationValue(value) {
|
|
459
|
+
return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
460
|
+
}
|
|
461
|
+
function parseReviewCiMode(value) {
|
|
462
|
+
if (value === "off" || value === "warn" || value === "block") return value;
|
|
463
|
+
throw new Error(`Invalid review mode: ${value}. Expected off, warn, or block.`);
|
|
464
|
+
}
|
|
465
|
+
function readInlineFlag(arg, prefix) {
|
|
466
|
+
const value = arg.slice(prefix.length);
|
|
467
|
+
if (!value) throw new Error(`Missing value for ${prefix.replace(/=$/, "")}.`);
|
|
468
|
+
return value;
|
|
469
|
+
}
|
|
470
|
+
function toPosixPath(value) {
|
|
471
|
+
return value.replaceAll("\\", "/");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
//#endregion
|
|
475
|
+
export { parseReviewArgs, printReviewHelp, runReview };
|