@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.
Files changed (27) hide show
  1. package/dist/{agent-D8DpCgt_.mjs → agent-ByXnegrS.mjs} +3 -3
  2. package/dist/{agents-XyolXdXp.mjs → agents-CpTNRbsh.mjs} +2 -2
  3. package/dist/cli/index.mjs +33 -15
  4. package/dist/{dev-FC6Fh7nT.mjs → dev-C03tUSTz.mjs} +2 -2
  5. package/dist/{doctor-CU4knIej.mjs → doctor-DMs3Q0wj.mjs} +4 -4
  6. package/dist/{downgrade-Bt4yrVyy.mjs → downgrade-Bv7E5LV2.mjs} +2 -2
  7. package/dist/index.d.mts +3 -3
  8. package/dist/index.mjs +1 -1
  9. package/dist/{init-BgzyLAay.mjs → init-_HAuo5Dv.mjs} +2 -2
  10. package/dist/{mcp-BMgH1Q33.mjs → mcp-DojNlB8t.mjs} +1 -1
  11. package/dist/mcp.d.mts +1 -1
  12. package/dist/{package-version-DQgrHnSb.mjs → package-version-L4GZowaF.mjs} +1 -1
  13. package/dist/{reading-time-DNLXwuqA.mjs → reading-time-Io7iRZ7S.mjs} +2 -1
  14. package/dist/review-BHFhvl2F.mjs +475 -0
  15. package/dist/review-_5fnI667.mjs +553 -0
  16. package/dist/{robots-Byj0knC3.mjs → robots-BxZaiGH3.mjs} +2 -2
  17. package/dist/{search-BQ1cY913.mjs → search-DKpKe0rf.mjs} +1 -1
  18. package/dist/{search-Dqu1Q27e.d.mts → search-Dd0kOr6B.d.mts} +1 -1
  19. package/dist/server.d.mts +65 -3
  20. package/dist/server.mjs +2 -1
  21. package/dist/{sitemap-mqWvYODL.mjs → sitemap-CXwYOIIb.mjs} +2 -2
  22. package/dist/{types-Dts3a32G.d.mts → types-DtBNjsk2.d.mts} +99 -1
  23. package/dist/{upgrade-B1EMfRQJ.mjs → upgrade-DrOWQIKI.mjs} +2 -2
  24. package/package.json +1 -1
  25. /package/dist/{config-Cio3byUJ.mjs → config-BHRL4R2v.mjs} +0 -0
  26. /package/dist/{templates-CkL3bEE5.mjs → templates-CakZBXK8.mjs} +0 -0
  27. /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 };