docs-ready 0.4.0 → 0.8.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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli/index.ts","../src/cli/commands/init.ts","../src/frameworks/detector.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { readFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport path from \"node:path\";\nimport { initCommand } from \"./commands/init.js\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nasync function getVersion(): Promise<string> {\n let dir = __dirname;\n for (let i = 0; i < 5; i++) {\n try {\n const pkg = JSON.parse(await readFile(path.join(dir, \"package.json\"), \"utf-8\"));\n return pkg.version;\n } catch {\n dir = path.dirname(dir);\n }\n }\n return \"0.0.0\";\n}\n\nasync function main(): Promise<void> {\n const version = await getVersion();\n\n const program = new Command();\n\n program\n .name(\"docs-ready\")\n .description(\"Make your docs AI-ready. Keep them that way.\")\n .version(version);\n\n program\n .command(\"init\")\n .description(\"Initialize docs-ready in your project\")\n .action(async () => {\n await initCommand();\n });\n\n program\n .command(\"generate\")\n .description(\"Generate AI-facing documentation files\")\n .option(\"--dry-run\", \"Show what would be generated without writing files\")\n .option(\"--only <type>\", \"Generate only: llms-txt, llms-full, or ai-context\")\n .action(async (opts) => {\n const { generateCommand } = await import(\"./commands/generate.js\");\n await generateCommand({ dryRun: opts.dryRun, only: opts.only });\n });\n\n program\n .command(\"guard\")\n .description(\"Check AI-facing docs for staleness\")\n .option(\"--output <format>\", \"Output format: console, json, or markdown\", \"console\")\n .action(async (opts) => {\n const { guardCommand } = await import(\"./commands/guard.js\");\n await guardCommand({ output: opts.output });\n });\n\n program\n .command(\"validate\")\n .description(\"Lint and validate AI-facing docs\")\n .action(() => {\n console.log(\"Validate command coming in v0.6.0\");\n });\n\n await program.parseAsync(process.argv);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\nimport { stdin, stdout } from \"node:process\";\nimport { detectFramework } from \"../../frameworks/detector.js\";\nimport { log, spinner } from \"../../utils/logger.js\";\n\ninterface InitOptions {\n cwd?: string;\n}\n\nexport async function initCommand(options: InitOptions = {}): Promise<void> {\n const cwd = options.cwd ?? process.cwd();\n const configPath = path.join(cwd, \".docs-ready.yaml\");\n\n // Check if config already exists\n try {\n await fs.access(configPath);\n log.warn(\".docs-ready.yaml already exists. Use --force to overwrite.\");\n return;\n } catch {\n // Config doesn't exist, proceed\n }\n\n const spin = spinner(\"Detecting project structure...\");\n spin.start();\n\n // Detect framework\n const framework = await detectFramework(cwd);\n spin.stop();\n\n log.info(`Detected framework: ${framework.name}`);\n log.info(`Docs directory: ${framework.docsDir}`);\n\n // Detect deployment platform\n const platform = await detectPlatform(cwd);\n if (platform !== \"none\") {\n log.info(`Detected platform: ${platform}`);\n }\n\n // Detect docusaurus-plugin-llms\n const hasLlmsPlugin = await detectLlmsPlugin(cwd);\n if (hasLlmsPlugin) {\n log.warn(\"Detected docusaurus-plugin-llms — disabling llms.txt/llms-full.txt generation.\");\n }\n\n // Prompt for project info\n const rl = createInterface({ input: stdin, output: stdout });\n const title = await rl.question(\"Project title: \");\n const description = await rl.question(\"Description: \");\n const url = await rl.question(\"Docs URL (e.g. https://docs.example.com): \");\n rl.close();\n\n // Render config from template (try file first, fall back to inline)\n const templateData: Record<string, string> = {\n title: title || \"My Project\",\n description: description || \"Project documentation\",\n url: url || \"https://docs.example.com\",\n docsDir: framework.docsDir,\n llmsTxt: String(!hasLlmsPlugin),\n llmsFullTxt: String(!hasLlmsPlugin),\n platform,\n };\n\n const template = await loadTemplate();\n const rendered = template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => templateData[key] ?? \"\");\n\n await fs.writeFile(configPath, rendered, \"utf-8\");\n log.success(\"Created .docs-ready.yaml\");\n log.dim(\"Next: run `docs-ready generate` to create AI-facing files.\");\n}\n\nasync function loadTemplate(): Promise<string> {\n // Try to load the template file (works when installed as npm package)\n const candidates = [\n path.join(path.dirname(new URL(import.meta.url).pathname), \"../../templates/config.yaml.tmpl\"),\n path.join(path.dirname(new URL(import.meta.url).pathname), \"../templates/config.yaml.tmpl\"),\n ];\n\n for (const candidate of candidates) {\n try {\n return await fs.readFile(candidate, \"utf-8\");\n } catch {\n // Try next\n }\n }\n\n // Inline fallback\n return `# docs-ready configuration\ntitle: \"{{title}}\"\ndescription: \"{{description}}\"\nurl: \"{{url}}\"\ndocs:\n dir: \"{{docsDir}}\"\n include:\n - \"**/*.md\"\n - \"**/*.mdx\"\n exclude:\n - \"**/node_modules/**\"\n - \"**/_*\"\ngenerate:\n llms_txt: {{llmsTxt}}\n llms_full_txt: {{llmsFullTxt}}\n ai_context: true\n output_dir: \"./build\"\ndeploy:\n platform: \"{{platform}}\"\n`;\n}\n\nasync function detectPlatform(\n cwd: string\n): Promise<\"vercel\" | \"netlify\" | \"cloudflare\" | \"none\"> {\n const checks: Array<{ file: string; platform: \"vercel\" | \"netlify\" | \"cloudflare\" }> = [\n { file: \"vercel.json\", platform: \"vercel\" },\n { file: \"netlify.toml\", platform: \"netlify\" },\n { file: \"_headers\", platform: \"cloudflare\" },\n { file: \"wrangler.toml\", platform: \"cloudflare\" },\n ];\n\n for (const { file, platform } of checks) {\n try {\n await fs.access(path.join(cwd, file));\n return platform;\n } catch {\n // Not found, continue\n }\n }\n\n return \"none\";\n}\n\nasync function detectLlmsPlugin(cwd: string): Promise<boolean> {\n try {\n const pkgPath = path.join(cwd, \"package.json\");\n const content = await fs.readFile(pkgPath, \"utf-8\");\n const pkg = JSON.parse(content);\n const allDeps = {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n };\n return \"docusaurus-plugin-llms\" in allDeps;\n } catch {\n return false;\n }\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport interface FrameworkResult {\n name: \"docusaurus\" | \"vitepress\" | \"mkdocs\" | \"starlight\" | \"generic\";\n configFile: string | null;\n docsDir: string;\n}\n\nconst DETECTORS: Array<{\n name: FrameworkResult[\"name\"];\n files: string[];\n docsDir: string;\n}> = [\n {\n name: \"docusaurus\",\n files: [\n \"docusaurus.config.ts\",\n \"docusaurus.config.js\",\n \"docusaurus.config.mts\",\n \"docusaurus.config.mjs\",\n ],\n docsDir: \"./docs\",\n },\n {\n name: \"vitepress\",\n files: [\n \".vitepress/config.ts\",\n \".vitepress/config.js\",\n \".vitepress/config.mts\",\n \".vitepress/config.mjs\",\n ],\n docsDir: \"./docs\",\n },\n {\n name: \"mkdocs\",\n files: [\"mkdocs.yml\", \"mkdocs.yaml\"],\n docsDir: \"./docs\",\n },\n {\n name: \"starlight\",\n files: [\"astro.config.mjs\", \"astro.config.ts\", \"astro.config.js\"],\n docsDir: \"./src/content/docs\",\n },\n];\n\nexport async function detectFramework(projectDir: string): Promise<FrameworkResult> {\n for (const detector of DETECTORS) {\n for (const file of detector.files) {\n const fullPath = path.join(projectDir, file);\n try {\n await fs.access(fullPath);\n\n if (detector.name === \"starlight\") {\n const content = await fs.readFile(fullPath, \"utf-8\");\n if (!content.includes(\"starlight\")) {\n continue;\n }\n }\n\n return {\n name: detector.name,\n configFile: fullPath,\n docsDir: detector.docsDir,\n };\n } catch {\n // File doesn't exist, try next\n }\n }\n }\n\n return {\n name: \"generic\",\n configFile: null,\n docsDir: \"./docs\",\n };\n}\n"],"mappings":"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","names":["path","fs","path","path","fs","__dirname","path"]}
1
+ {"version":3,"sources":["../src/cli/index.ts","../src/cli/commands/init.ts","../src/frameworks/detector.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { readFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport path from \"node:path\";\nimport { initCommand } from \"./commands/init.js\";\nimport { setLogLevel } from \"../utils/logger.js\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nasync function getVersion(): Promise<string> {\n let dir = __dirname;\n for (let i = 0; i < 5; i++) {\n try {\n const pkg = JSON.parse(await readFile(path.join(dir, \"package.json\"), \"utf-8\"));\n return pkg.version;\n } catch {\n dir = path.dirname(dir);\n }\n }\n return \"0.0.0\";\n}\n\nasync function main(): Promise<void> {\n const version = await getVersion();\n\n const program = new Command();\n\n program\n .name(\"docs-ready\")\n .description(\"Make your docs AI-ready. Keep them that way.\")\n .version(version)\n .option(\"--quiet\", \"Only show errors\")\n .option(\"--verbose\", \"Show debug logging\")\n .option(\"--no-color\", \"Disable colored output\");\n\n program.hook(\"preAction\", (thisCommand) => {\n const opts = thisCommand.opts();\n if (opts.quiet) {\n setLogLevel(\"quiet\");\n } else if (opts.verbose) {\n setLogLevel(\"verbose\");\n }\n });\n\n program\n .command(\"init\")\n .description(\"Initialize docs-ready in your project\")\n .action(async () => {\n await initCommand();\n });\n\n program\n .command(\"generate\")\n .description(\"Generate AI-facing documentation files\")\n .option(\"--dry-run\", \"Show what would be generated without writing files\")\n .option(\"--only <type>\", \"Generate only: llms-txt, llms-full, or ai-context\")\n .option(\"--watch\", \"Watch for changes and regenerate\")\n .action(async (opts) => {\n const { generateCommand } = await import(\"./commands/generate.js\");\n await generateCommand({ dryRun: opts.dryRun, only: opts.only, watch: opts.watch });\n });\n\n program\n .command(\"guard\")\n .description(\"Check AI-facing docs for staleness\")\n .option(\"--output <format>\", \"Output format: console, json, or markdown\", \"console\")\n .option(\"--init-workflow\", \"Generate GitHub Actions workflow file\")\n .action(async (opts) => {\n const { guardCommand } = await import(\"./commands/guard.js\");\n await guardCommand({ output: opts.output, initWorkflow: opts.initWorkflow });\n });\n\n program\n .command(\"validate\")\n .description(\"Lint and validate AI-facing docs\")\n .option(\"--no-links\", \"Skip link checking\")\n .action(async (opts) => {\n const { validateCommand } = await import(\"./commands/validate.js\");\n await validateCommand({ checkLinks: opts.links });\n });\n\n await program.parseAsync(process.argv);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\nimport { stdin, stdout } from \"node:process\";\nimport { detectFramework } from \"../../frameworks/detector.js\";\nimport { log, spinner } from \"../../utils/logger.js\";\n\ninterface InitOptions {\n cwd?: string;\n}\n\nexport async function initCommand(options: InitOptions = {}): Promise<void> {\n const cwd = options.cwd ?? process.cwd();\n const configPath = path.join(cwd, \".docs-ready.yaml\");\n\n // Check if config already exists\n try {\n await fs.access(configPath);\n log.warn(\".docs-ready.yaml already exists. Use --force to overwrite.\");\n return;\n } catch {\n // Config doesn't exist, proceed\n }\n\n const spin = spinner(\"Detecting project structure...\");\n spin.start();\n\n // Detect framework\n const framework = await detectFramework(cwd);\n spin.stop();\n\n log.info(`Detected framework: ${framework.name}`);\n log.info(`Docs directory: ${framework.docsDir}`);\n\n // Detect deployment platform\n const platform = await detectPlatform(cwd);\n if (platform !== \"none\") {\n log.info(`Detected platform: ${platform}`);\n }\n\n // Detect docusaurus-plugin-llms\n const hasLlmsPlugin = await detectLlmsPlugin(cwd);\n if (hasLlmsPlugin) {\n log.warn(\"Detected docusaurus-plugin-llms — disabling llms.txt/llms-full.txt generation.\");\n }\n\n // Prompt for project info\n const rl = createInterface({ input: stdin, output: stdout });\n const title = await rl.question(\"Project title: \");\n const description = await rl.question(\"Description: \");\n const url = await rl.question(\"Docs URL (e.g. https://docs.example.com): \");\n rl.close();\n\n // Render config from template (try file first, fall back to inline)\n const templateData: Record<string, string> = {\n title: title || \"My Project\",\n description: description || \"Project documentation\",\n url: url || \"https://docs.example.com\",\n docsDir: framework.docsDir,\n llmsTxt: String(!hasLlmsPlugin),\n llmsFullTxt: String(!hasLlmsPlugin),\n platform,\n };\n\n const template = await loadTemplate();\n const rendered = template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => templateData[key] ?? \"\");\n\n await fs.writeFile(configPath, rendered, \"utf-8\");\n log.success(\"Created .docs-ready.yaml\");\n log.dim(\"Next: run `docs-ready generate` to create AI-facing files.\");\n}\n\nasync function loadTemplate(): Promise<string> {\n // Try to load the template file (works when installed as npm package)\n const candidates = [\n path.join(path.dirname(new URL(import.meta.url).pathname), \"../../templates/config.yaml.tmpl\"),\n path.join(path.dirname(new URL(import.meta.url).pathname), \"../templates/config.yaml.tmpl\"),\n ];\n\n for (const candidate of candidates) {\n try {\n return await fs.readFile(candidate, \"utf-8\");\n } catch {\n // Try next\n }\n }\n\n // Inline fallback\n return `# docs-ready configuration\ntitle: \"{{title}}\"\ndescription: \"{{description}}\"\nurl: \"{{url}}\"\ndocs:\n dir: \"{{docsDir}}\"\n include:\n - \"**/*.md\"\n - \"**/*.mdx\"\n exclude:\n - \"**/node_modules/**\"\n - \"**/_*\"\ngenerate:\n llms_txt: {{llmsTxt}}\n llms_full_txt: {{llmsFullTxt}}\n ai_context: true\n output_dir: \"./build\"\ndeploy:\n platform: \"{{platform}}\"\n`;\n}\n\nasync function detectPlatform(\n cwd: string\n): Promise<\"vercel\" | \"netlify\" | \"cloudflare\" | \"none\"> {\n const checks: Array<{ file: string; platform: \"vercel\" | \"netlify\" | \"cloudflare\" }> = [\n { file: \"vercel.json\", platform: \"vercel\" },\n { file: \"netlify.toml\", platform: \"netlify\" },\n { file: \"_headers\", platform: \"cloudflare\" },\n { file: \"wrangler.toml\", platform: \"cloudflare\" },\n ];\n\n for (const { file, platform } of checks) {\n try {\n await fs.access(path.join(cwd, file));\n return platform;\n } catch {\n // Not found, continue\n }\n }\n\n return \"none\";\n}\n\nasync function detectLlmsPlugin(cwd: string): Promise<boolean> {\n try {\n const pkgPath = path.join(cwd, \"package.json\");\n const content = await fs.readFile(pkgPath, \"utf-8\");\n const pkg = JSON.parse(content);\n const allDeps = {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n };\n return \"docusaurus-plugin-llms\" in allDeps;\n } catch {\n return false;\n }\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport interface FrameworkResult {\n name: \"docusaurus\" | \"vitepress\" | \"mkdocs\" | \"starlight\" | \"generic\";\n configFile: string | null;\n docsDir: string;\n}\n\nconst DETECTORS: Array<{\n name: FrameworkResult[\"name\"];\n files: string[];\n docsDir: string;\n}> = [\n {\n name: \"docusaurus\",\n files: [\n \"docusaurus.config.ts\",\n \"docusaurus.config.js\",\n \"docusaurus.config.mts\",\n \"docusaurus.config.mjs\",\n ],\n docsDir: \"./docs\",\n },\n {\n name: \"vitepress\",\n files: [\n \".vitepress/config.ts\",\n \".vitepress/config.js\",\n \".vitepress/config.mts\",\n \".vitepress/config.mjs\",\n ],\n docsDir: \"./docs\",\n },\n {\n name: \"mkdocs\",\n files: [\"mkdocs.yml\", \"mkdocs.yaml\"],\n docsDir: \"./docs\",\n },\n {\n name: \"starlight\",\n files: [\"astro.config.mjs\", \"astro.config.ts\", \"astro.config.js\"],\n docsDir: \"./src/content/docs\",\n },\n];\n\nexport async function detectFramework(projectDir: string): Promise<FrameworkResult> {\n for (const detector of DETECTORS) {\n for (const file of detector.files) {\n const fullPath = path.join(projectDir, file);\n try {\n await fs.access(fullPath);\n\n if (detector.name === \"starlight\") {\n const content = await fs.readFile(fullPath, \"utf-8\");\n if (!content.includes(\"starlight\")) {\n continue;\n }\n }\n\n return {\n name: detector.name,\n configFile: fullPath,\n docsDir: detector.docsDir,\n };\n } catch {\n // File doesn't exist, try next\n }\n }\n }\n\n return {\n name: \"generic\",\n configFile: null,\n docsDir: \"./docs\",\n };\n}\n"],"mappings":"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","names":["path","fs","path","path","fs","__dirname","path"]}
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ estimateTokens,
4
+ scanDocs
5
+ } from "./chunk-4NYEWEU2.js";
6
+ import {
7
+ fetchWithTimeout
8
+ } from "./chunk-5XFC7QYB.js";
9
+ import {
10
+ loadConfig
11
+ } from "./chunk-QI2AROM3.js";
12
+ import {
13
+ log,
14
+ spinner
15
+ } from "./chunk-PXQN4E6K.js";
16
+
17
+ // src/cli/commands/validate.ts
18
+ import path2 from "path";
19
+
20
+ // src/validate/runner.ts
21
+ import fs from "fs/promises";
22
+ import path from "path";
23
+
24
+ // src/validate/rules/format.ts
25
+ function validateFormat(content) {
26
+ const results = [];
27
+ const lines = content.split("\n");
28
+ const hasH1 = lines.some((l) => /^# .+/.test(l));
29
+ results.push({
30
+ rule: "format",
31
+ severity: "error",
32
+ passed: hasH1,
33
+ message: hasH1 ? "H1 title present" : "Missing H1 title (required by llmstxt.org spec)"
34
+ });
35
+ const hasBlockquote = lines.some((l) => /^> .+/.test(l));
36
+ results.push({
37
+ rule: "format",
38
+ severity: "error",
39
+ passed: hasBlockquote,
40
+ message: hasBlockquote ? "Blockquote description present" : "Missing blockquote description"
41
+ });
42
+ const linkLines = lines.filter((l) => l.trim().startsWith("- ["));
43
+ const malformed = linkLines.filter((l) => !/^- \[.+\]\(.+\)/.test(l.trim()));
44
+ const linksOk = malformed.length === 0 && linkLines.length > 0;
45
+ results.push({
46
+ rule: "format",
47
+ severity: "error",
48
+ passed: linksOk,
49
+ message: linksOk ? `${linkLines.length} link entries correctly formatted` : malformed.length > 0 ? `${malformed.length} malformed link entries` : "No link entries found"
50
+ });
51
+ return results;
52
+ }
53
+
54
+ // src/validate/rules/tokens.ts
55
+ function validateTokens(content, maxTokens) {
56
+ const count = estimateTokens(content);
57
+ const passed = count <= maxTokens;
58
+ return {
59
+ rule: "tokens",
60
+ severity: "warning",
61
+ passed,
62
+ message: passed ? `Token count ~${count} is within limit (${maxTokens})` : `Token count ~${count} exceeds limit of ${maxTokens}`,
63
+ details: { estimatedTokens: count, maxTokens }
64
+ };
65
+ }
66
+
67
+ // src/validate/rules/coverage.ts
68
+ function validateCoverage(llmsTxtContent, totalPages, threshold) {
69
+ const linkRegex = /- \[.+?\]\((.+?)\)/g;
70
+ const links = /* @__PURE__ */ new Set();
71
+ let match;
72
+ while ((match = linkRegex.exec(llmsTxtContent)) !== null) {
73
+ links.add(match[1]);
74
+ }
75
+ const coverage = totalPages > 0 ? links.size / totalPages : 0;
76
+ const passed = coverage >= threshold;
77
+ const pct = Math.round(coverage * 100);
78
+ return {
79
+ rule: "coverage",
80
+ severity: "error",
81
+ passed,
82
+ message: passed ? `Coverage ${pct}% (${links.size}/${totalPages} pages) meets ${Math.round(threshold * 100)}% threshold` : `Coverage ${pct}% (${links.size}/${totalPages} pages) below ${Math.round(threshold * 100)}% threshold`,
83
+ details: { coverage, linkedPages: links.size, totalPages, threshold }
84
+ };
85
+ }
86
+
87
+ // src/validate/rules/links.ts
88
+ async function validateLinks(content) {
89
+ const results = [];
90
+ const linkRegex = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g;
91
+ const urls = /* @__PURE__ */ new Set();
92
+ let match;
93
+ while ((match = linkRegex.exec(content)) !== null) {
94
+ urls.add(match[2]);
95
+ }
96
+ for (const url of urls) {
97
+ try {
98
+ const response = await fetchWithTimeout(url, { timeout: 1e4 });
99
+ if (response.ok || response.status === 301 || response.status === 302) {
100
+ results.push({
101
+ rule: "links",
102
+ severity: "error",
103
+ passed: true,
104
+ message: `${url} \u2014 ${response.status} OK`
105
+ });
106
+ } else {
107
+ results.push({
108
+ rule: "links",
109
+ severity: "error",
110
+ passed: false,
111
+ message: `${url} \u2014 ${response.status}`,
112
+ details: { url, statusCode: response.status }
113
+ });
114
+ }
115
+ } catch (error) {
116
+ const err = error;
117
+ results.push({
118
+ rule: "links",
119
+ severity: "warning",
120
+ passed: false,
121
+ message: `${url} \u2014 ${err.name === "AbortError" ? "timeout" : err.message}`,
122
+ details: { url, error: err.message }
123
+ });
124
+ }
125
+ }
126
+ return results;
127
+ }
128
+
129
+ // src/validate/runner.ts
130
+ async function runValidation(config, pages, outputDir) {
131
+ const results = [];
132
+ const llmsTxtPath = path.join(outputDir, "llms.txt");
133
+ const llmsFullTxtPath = path.join(outputDir, "llms-full.txt");
134
+ const aiContextPath = path.join(outputDir, "ai-context.md");
135
+ let llmsTxt = null;
136
+ let llmsFullTxt = null;
137
+ let aiContext = null;
138
+ try {
139
+ llmsTxt = await fs.readFile(llmsTxtPath, "utf-8");
140
+ } catch {
141
+ }
142
+ try {
143
+ llmsFullTxt = await fs.readFile(llmsFullTxtPath, "utf-8");
144
+ } catch {
145
+ }
146
+ try {
147
+ aiContext = await fs.readFile(aiContextPath, "utf-8");
148
+ } catch {
149
+ }
150
+ if (llmsTxt) {
151
+ results.push(...validateFormat(llmsTxt));
152
+ } else {
153
+ results.push({
154
+ rule: "format",
155
+ severity: "warning",
156
+ passed: false,
157
+ message: "llms.txt not found \u2014 run `docs-ready generate` first"
158
+ });
159
+ }
160
+ if (llmsFullTxt) {
161
+ results.push(validateTokens(llmsFullTxt, config.validate.max_tokens));
162
+ }
163
+ if (llmsTxt && config.validate.check_coverage) {
164
+ results.push(validateCoverage(llmsTxt, pages.length, config.validate.coverage_threshold));
165
+ }
166
+ if (config.validate.check_links) {
167
+ const contentToCheck = [llmsTxt, aiContext].filter(Boolean).join("\n");
168
+ if (contentToCheck) {
169
+ results.push(...await validateLinks(contentToCheck));
170
+ }
171
+ }
172
+ return results;
173
+ }
174
+ function getValidationExitCode(results) {
175
+ return results.some((r) => r.severity === "error" && !r.passed) ? 1 : 0;
176
+ }
177
+
178
+ // src/cli/commands/validate.ts
179
+ async function validateCommand(options = {}) {
180
+ const cwd = process.cwd();
181
+ const config = await loadConfig(cwd);
182
+ if (options.checkLinks === false) {
183
+ config.validate.check_links = false;
184
+ }
185
+ const spin = spinner("Scanning documentation...");
186
+ spin.start();
187
+ const docsDir = path2.resolve(cwd, config.docs.dir);
188
+ const pages = await scanDocs(docsDir, {
189
+ include: config.docs.include,
190
+ exclude: config.docs.exclude
191
+ });
192
+ spin.stop();
193
+ const outputDir = path2.resolve(cwd, config.generate.output_dir);
194
+ const spin2 = spinner("Running validation...");
195
+ spin2.start();
196
+ const results = await runValidation(config, pages, outputDir);
197
+ spin2.stop();
198
+ for (const result of results) {
199
+ if (result.passed) {
200
+ log.success(`[${result.rule}] ${result.message}`);
201
+ } else if (result.severity === "warning") {
202
+ log.warn(`[${result.rule}] ${result.message}`);
203
+ } else {
204
+ log.error(`[${result.rule}] ${result.message}`);
205
+ }
206
+ }
207
+ const passed = results.filter((r) => r.passed).length;
208
+ const failed = results.filter((r) => !r.passed).length;
209
+ log.info(`
210
+ Validation: ${passed} passed, ${failed} issues`);
211
+ const exitCode = getValidationExitCode(results);
212
+ if (exitCode !== 0) {
213
+ process.exitCode = exitCode;
214
+ }
215
+ }
216
+ export {
217
+ validateCommand
218
+ };
219
+ //# sourceMappingURL=validate-GKDIBEZX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/commands/validate.ts","../src/validate/runner.ts","../src/validate/rules/format.ts","../src/validate/rules/tokens.ts","../src/validate/rules/coverage.ts","../src/validate/rules/links.ts"],"sourcesContent":["import path from \"node:path\";\nimport { loadConfig } from \"../../core/config.js\";\nimport { scanDocs } from \"../../core/scanner.js\";\nimport { runValidation, getValidationExitCode } from \"../../validate/runner.js\";\nimport { log, spinner } from \"../../utils/logger.js\";\n\ninterface ValidateOptions {\n checkLinks?: boolean;\n}\n\nexport async function validateCommand(options: ValidateOptions = {}): Promise<void> {\n const cwd = process.cwd();\n const config = await loadConfig(cwd);\n\n // Override check_links if --no-links was passed\n if (options.checkLinks === false) {\n config.validate.check_links = false;\n }\n\n const spin = spinner(\"Scanning documentation...\");\n spin.start();\n const docsDir = path.resolve(cwd, config.docs.dir);\n const pages = await scanDocs(docsDir, {\n include: config.docs.include,\n exclude: config.docs.exclude,\n });\n spin.stop();\n\n const outputDir = path.resolve(cwd, config.generate.output_dir);\n\n const spin2 = spinner(\"Running validation...\");\n spin2.start();\n const results = await runValidation(config, pages, outputDir);\n spin2.stop();\n\n // Print results\n for (const result of results) {\n if (result.passed) {\n log.success(`[${result.rule}] ${result.message}`);\n } else if (result.severity === \"warning\") {\n log.warn(`[${result.rule}] ${result.message}`);\n } else {\n log.error(`[${result.rule}] ${result.message}`);\n }\n }\n\n const passed = results.filter((r) => r.passed).length;\n const failed = results.filter((r) => !r.passed).length;\n log.info(`\\nValidation: ${passed} passed, ${failed} issues`);\n\n const exitCode = getValidationExitCode(results);\n if (exitCode !== 0) {\n process.exitCode = exitCode;\n }\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { DocsReadyConfig } from \"../core/config.js\";\nimport type { ScannedPage } from \"../core/scanner.js\";\nimport type { ValidationResult } from \"./types.js\";\nimport { validateFormat } from \"./rules/format.js\";\nimport { validateTokens } from \"./rules/tokens.js\";\nimport { validateCoverage } from \"./rules/coverage.js\";\nimport { validateLinks } from \"./rules/links.js\";\n\nexport async function runValidation(\n config: DocsReadyConfig,\n pages: ScannedPage[],\n outputDir: string\n): Promise<ValidationResult[]> {\n const results: ValidationResult[] = [];\n\n // Read generated files\n const llmsTxtPath = path.join(outputDir, \"llms.txt\");\n const llmsFullTxtPath = path.join(outputDir, \"llms-full.txt\");\n const aiContextPath = path.join(outputDir, \"ai-context.md\");\n\n let llmsTxt: string | null = null;\n let llmsFullTxt: string | null = null;\n let aiContext: string | null = null;\n\n try { llmsTxt = await fs.readFile(llmsTxtPath, \"utf-8\"); } catch {}\n try { llmsFullTxt = await fs.readFile(llmsFullTxtPath, \"utf-8\"); } catch {}\n try { aiContext = await fs.readFile(aiContextPath, \"utf-8\"); } catch {}\n\n // Format check on llms.txt\n if (llmsTxt) {\n results.push(...validateFormat(llmsTxt));\n } else {\n results.push({\n rule: \"format\",\n severity: \"warning\",\n passed: false,\n message: \"llms.txt not found — run `docs-ready generate` first\",\n });\n }\n\n // Token check on llms-full.txt\n if (llmsFullTxt) {\n results.push(validateTokens(llmsFullTxt, config.validate.max_tokens));\n }\n\n // Coverage check\n if (llmsTxt && config.validate.check_coverage) {\n results.push(validateCoverage(llmsTxt, pages.length, config.validate.coverage_threshold));\n }\n\n // Link check\n if (config.validate.check_links) {\n const contentToCheck = [llmsTxt, aiContext].filter(Boolean).join(\"\\n\");\n if (contentToCheck) {\n results.push(...await validateLinks(contentToCheck));\n }\n }\n\n return results;\n}\n\nexport function getValidationExitCode(results: ValidationResult[]): number {\n return results.some((r) => r.severity === \"error\" && !r.passed) ? 1 : 0;\n}\n","import type { ValidationResult } from \"../types.js\";\n\nexport function validateFormat(content: string): ValidationResult[] {\n const results: ValidationResult[] = [];\n const lines = content.split(\"\\n\");\n\n // Check H1 title\n const hasH1 = lines.some((l) => /^# .+/.test(l));\n results.push({\n rule: \"format\",\n severity: \"error\",\n passed: hasH1,\n message: hasH1 ? \"H1 title present\" : \"Missing H1 title (required by llmstxt.org spec)\",\n });\n\n // Check blockquote description\n const hasBlockquote = lines.some((l) => /^> .+/.test(l));\n results.push({\n rule: \"format\",\n severity: \"error\",\n passed: hasBlockquote,\n message: hasBlockquote ? \"Blockquote description present\" : \"Missing blockquote description\",\n });\n\n // Check link format: - [Title](url) or - [Title](url): description\n const linkLines = lines.filter((l) => l.trim().startsWith(\"- [\"));\n const malformed = linkLines.filter((l) => !/^- \\[.+\\]\\(.+\\)/.test(l.trim()));\n const linksOk = malformed.length === 0 && linkLines.length > 0;\n results.push({\n rule: \"format\",\n severity: \"error\",\n passed: linksOk,\n message: linksOk\n ? `${linkLines.length} link entries correctly formatted`\n : malformed.length > 0\n ? `${malformed.length} malformed link entries`\n : \"No link entries found\",\n });\n\n return results;\n}\n","import { estimateTokens } from \"../../utils/tokens.js\";\nimport type { ValidationResult } from \"../types.js\";\n\nexport function validateTokens(content: string, maxTokens: number): ValidationResult {\n const count = estimateTokens(content);\n const passed = count <= maxTokens;\n return {\n rule: \"tokens\",\n severity: \"warning\",\n passed,\n message: passed\n ? `Token count ~${count} is within limit (${maxTokens})`\n : `Token count ~${count} exceeds limit of ${maxTokens}`,\n details: { estimatedTokens: count, maxTokens },\n };\n}\n","import type { ValidationResult } from \"../types.js\";\n\nexport function validateCoverage(\n llmsTxtContent: string,\n totalPages: number,\n threshold: number\n): ValidationResult {\n // Count unique links in llms.txt\n const linkRegex = /- \\[.+?\\]\\((.+?)\\)/g;\n const links = new Set<string>();\n let match;\n while ((match = linkRegex.exec(llmsTxtContent)) !== null) {\n links.add(match[1]);\n }\n\n const coverage = totalPages > 0 ? links.size / totalPages : 0;\n const passed = coverage >= threshold;\n const pct = Math.round(coverage * 100);\n\n return {\n rule: \"coverage\",\n severity: \"error\",\n passed,\n message: passed\n ? `Coverage ${pct}% (${links.size}/${totalPages} pages) meets ${Math.round(threshold * 100)}% threshold`\n : `Coverage ${pct}% (${links.size}/${totalPages} pages) below ${Math.round(threshold * 100)}% threshold`,\n details: { coverage, linkedPages: links.size, totalPages, threshold },\n };\n}\n","import { fetchWithTimeout } from \"../../utils/http.js\";\nimport type { ValidationResult } from \"../types.js\";\n\nexport async function validateLinks(content: string): Promise<ValidationResult[]> {\n const results: ValidationResult[] = [];\n const linkRegex = /\\[([^\\]]+)\\]\\((https?:\\/\\/[^)]+)\\)/g;\n const urls = new Set<string>();\n let match;\n\n while ((match = linkRegex.exec(content)) !== null) {\n urls.add(match[2]);\n }\n\n for (const url of urls) {\n try {\n const response = await fetchWithTimeout(url, { timeout: 10000 });\n if (response.ok || response.status === 301 || response.status === 302) {\n results.push({\n rule: \"links\",\n severity: \"error\",\n passed: true,\n message: `${url} — ${response.status} OK`,\n });\n } else {\n results.push({\n rule: \"links\",\n severity: \"error\",\n passed: false,\n message: `${url} — ${response.status}`,\n details: { url, statusCode: response.status },\n });\n }\n } catch (error) {\n const err = error as Error;\n results.push({\n rule: \"links\",\n severity: \"warning\",\n passed: false,\n message: `${url} — ${err.name === \"AbortError\" ? \"timeout\" : err.message}`,\n details: { url, error: err.message },\n });\n }\n }\n\n return results;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAAA,OAAOA,WAAU;;;ACAjB,OAAO,QAAQ;AACf,OAAO,UAAU;;;ACCV,SAAS,eAAe,SAAqC;AAClE,QAAM,UAA8B,CAAC;AACrC,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAGhC,QAAM,QAAQ,MAAM,KAAK,CAAC,MAAM,QAAQ,KAAK,CAAC,CAAC;AAC/C,UAAQ,KAAK;AAAA,IACX,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,SAAS,QAAQ,qBAAqB;AAAA,EACxC,CAAC;AAGD,QAAM,gBAAgB,MAAM,KAAK,CAAC,MAAM,QAAQ,KAAK,CAAC,CAAC;AACvD,UAAQ,KAAK;AAAA,IACX,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,SAAS,gBAAgB,mCAAmC;AAAA,EAC9D,CAAC;AAGD,QAAM,YAAY,MAAM,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,WAAW,KAAK,CAAC;AAChE,QAAM,YAAY,UAAU,OAAO,CAAC,MAAM,CAAC,kBAAkB,KAAK,EAAE,KAAK,CAAC,CAAC;AAC3E,QAAM,UAAU,UAAU,WAAW,KAAK,UAAU,SAAS;AAC7D,UAAQ,KAAK;AAAA,IACX,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,SAAS,UACL,GAAG,UAAU,MAAM,sCACnB,UAAU,SAAS,IACjB,GAAG,UAAU,MAAM,4BACnB;AAAA,EACR,CAAC;AAED,SAAO;AACT;;;ACrCO,SAAS,eAAe,SAAiB,WAAqC;AACnF,QAAM,QAAQ,eAAe,OAAO;AACpC,QAAM,SAAS,SAAS;AACxB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV;AAAA,IACA,SAAS,SACL,gBAAgB,KAAK,qBAAqB,SAAS,MACnD,gBAAgB,KAAK,qBAAqB,SAAS;AAAA,IACvD,SAAS,EAAE,iBAAiB,OAAO,UAAU;AAAA,EAC/C;AACF;;;ACbO,SAAS,iBACd,gBACA,YACA,WACkB;AAElB,QAAM,YAAY;AAClB,QAAM,QAAQ,oBAAI,IAAY;AAC9B,MAAI;AACJ,UAAQ,QAAQ,UAAU,KAAK,cAAc,OAAO,MAAM;AACxD,UAAM,IAAI,MAAM,CAAC,CAAC;AAAA,EACpB;AAEA,QAAM,WAAW,aAAa,IAAI,MAAM,OAAO,aAAa;AAC5D,QAAM,SAAS,YAAY;AAC3B,QAAM,MAAM,KAAK,MAAM,WAAW,GAAG;AAErC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV;AAAA,IACA,SAAS,SACL,YAAY,GAAG,MAAM,MAAM,IAAI,IAAI,UAAU,iBAAiB,KAAK,MAAM,YAAY,GAAG,CAAC,gBACzF,YAAY,GAAG,MAAM,MAAM,IAAI,IAAI,UAAU,iBAAiB,KAAK,MAAM,YAAY,GAAG,CAAC;AAAA,IAC7F,SAAS,EAAE,UAAU,aAAa,MAAM,MAAM,YAAY,UAAU;AAAA,EACtE;AACF;;;ACzBA,eAAsB,cAAc,SAA8C;AAChF,QAAM,UAA8B,CAAC;AACrC,QAAM,YAAY;AAClB,QAAM,OAAO,oBAAI,IAAY;AAC7B,MAAI;AAEJ,UAAQ,QAAQ,UAAU,KAAK,OAAO,OAAO,MAAM;AACjD,SAAK,IAAI,MAAM,CAAC,CAAC;AAAA,EACnB;AAEA,aAAW,OAAO,MAAM;AACtB,QAAI;AACF,YAAM,WAAW,MAAM,iBAAiB,KAAK,EAAE,SAAS,IAAM,CAAC;AAC/D,UAAI,SAAS,MAAM,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACrE,gBAAQ,KAAK;AAAA,UACX,MAAM;AAAA,UACN,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,SAAS,GAAG,GAAG,WAAM,SAAS,MAAM;AAAA,QACtC,CAAC;AAAA,MACH,OAAO;AACL,gBAAQ,KAAK;AAAA,UACX,MAAM;AAAA,UACN,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,SAAS,GAAG,GAAG,WAAM,SAAS,MAAM;AAAA,UACpC,SAAS,EAAE,KAAK,YAAY,SAAS,OAAO;AAAA,QAC9C,CAAC;AAAA,MACH;AAAA,IACF,SAAS,OAAO;AACd,YAAM,MAAM;AACZ,cAAQ,KAAK;AAAA,QACX,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,SAAS,GAAG,GAAG,WAAM,IAAI,SAAS,eAAe,YAAY,IAAI,OAAO;AAAA,QACxE,SAAS,EAAE,KAAK,OAAO,IAAI,QAAQ;AAAA,MACrC,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;;;AJnCA,eAAsB,cACpB,QACA,OACA,WAC6B;AAC7B,QAAM,UAA8B,CAAC;AAGrC,QAAM,cAAc,KAAK,KAAK,WAAW,UAAU;AACnD,QAAM,kBAAkB,KAAK,KAAK,WAAW,eAAe;AAC5D,QAAM,gBAAgB,KAAK,KAAK,WAAW,eAAe;AAE1D,MAAI,UAAyB;AAC7B,MAAI,cAA6B;AACjC,MAAI,YAA2B;AAE/B,MAAI;AAAE,cAAU,MAAM,GAAG,SAAS,aAAa,OAAO;AAAA,EAAG,QAAQ;AAAA,EAAC;AAClE,MAAI;AAAE,kBAAc,MAAM,GAAG,SAAS,iBAAiB,OAAO;AAAA,EAAG,QAAQ;AAAA,EAAC;AAC1E,MAAI;AAAE,gBAAY,MAAM,GAAG,SAAS,eAAe,OAAO;AAAA,EAAG,QAAQ;AAAA,EAAC;AAGtE,MAAI,SAAS;AACX,YAAQ,KAAK,GAAG,eAAe,OAAO,CAAC;AAAA,EACzC,OAAO;AACL,YAAQ,KAAK;AAAA,MACX,MAAM;AAAA,MACN,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAGA,MAAI,aAAa;AACf,YAAQ,KAAK,eAAe,aAAa,OAAO,SAAS,UAAU,CAAC;AAAA,EACtE;AAGA,MAAI,WAAW,OAAO,SAAS,gBAAgB;AAC7C,YAAQ,KAAK,iBAAiB,SAAS,MAAM,QAAQ,OAAO,SAAS,kBAAkB,CAAC;AAAA,EAC1F;AAGA,MAAI,OAAO,SAAS,aAAa;AAC/B,UAAM,iBAAiB,CAAC,SAAS,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AACrE,QAAI,gBAAgB;AAClB,cAAQ,KAAK,GAAG,MAAM,cAAc,cAAc,CAAC;AAAA,IACrD;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,sBAAsB,SAAqC;AACzE,SAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,WAAW,CAAC,EAAE,MAAM,IAAI,IAAI;AACxE;;;ADvDA,eAAsB,gBAAgB,UAA2B,CAAC,GAAkB;AAClF,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,SAAS,MAAM,WAAW,GAAG;AAGnC,MAAI,QAAQ,eAAe,OAAO;AAChC,WAAO,SAAS,cAAc;AAAA,EAChC;AAEA,QAAM,OAAO,QAAQ,2BAA2B;AAChD,OAAK,MAAM;AACX,QAAM,UAAUC,MAAK,QAAQ,KAAK,OAAO,KAAK,GAAG;AACjD,QAAM,QAAQ,MAAM,SAAS,SAAS;AAAA,IACpC,SAAS,OAAO,KAAK;AAAA,IACrB,SAAS,OAAO,KAAK;AAAA,EACvB,CAAC;AACD,OAAK,KAAK;AAEV,QAAM,YAAYA,MAAK,QAAQ,KAAK,OAAO,SAAS,UAAU;AAE9D,QAAM,QAAQ,QAAQ,uBAAuB;AAC7C,QAAM,MAAM;AACZ,QAAM,UAAU,MAAM,cAAc,QAAQ,OAAO,SAAS;AAC5D,QAAM,KAAK;AAGX,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,QAAQ;AACjB,UAAI,QAAQ,IAAI,OAAO,IAAI,KAAK,OAAO,OAAO,EAAE;AAAA,IAClD,WAAW,OAAO,aAAa,WAAW;AACxC,UAAI,KAAK,IAAI,OAAO,IAAI,KAAK,OAAO,OAAO,EAAE;AAAA,IAC/C,OAAO;AACL,UAAI,MAAM,IAAI,OAAO,IAAI,KAAK,OAAO,OAAO,EAAE;AAAA,IAChD;AAAA,EACF;AAEA,QAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE;AAC/C,QAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE;AAChD,MAAI,KAAK;AAAA,cAAiB,MAAM,YAAY,MAAM,SAAS;AAE3D,QAAM,WAAW,sBAAsB,OAAO;AAC9C,MAAI,aAAa,GAAG;AAClB,YAAQ,WAAW;AAAA,EACrB;AACF;","names":["path","path"]}
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/watcher.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ function watchDocs(options) {
7
+ const { dir, debounceMs = 500, onChange } = options;
8
+ let timeout = null;
9
+ let running = false;
10
+ const resolvedDir = path.resolve(dir);
11
+ const watcher = fs.watch(resolvedDir, { recursive: true }, (_event, filename) => {
12
+ if (!filename) return;
13
+ if (!filename.endsWith(".md") && !filename.endsWith(".mdx")) return;
14
+ if (timeout) clearTimeout(timeout);
15
+ timeout = setTimeout(async () => {
16
+ if (running) return;
17
+ running = true;
18
+ try {
19
+ await onChange();
20
+ } finally {
21
+ running = false;
22
+ }
23
+ }, debounceMs);
24
+ });
25
+ return {
26
+ close: () => {
27
+ if (timeout) clearTimeout(timeout);
28
+ watcher.close();
29
+ }
30
+ };
31
+ }
32
+ export {
33
+ watchDocs
34
+ };
35
+ //# sourceMappingURL=watcher-TAYSGKGV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/watcher.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport interface WatcherOptions {\n dir: string;\n patterns: string[];\n debounceMs?: number;\n onChange: () => void | Promise<void>;\n}\n\nexport function watchDocs(options: WatcherOptions): { close: () => void } {\n const { dir, debounceMs = 500, onChange } = options;\n let timeout: ReturnType<typeof setTimeout> | null = null;\n let running = false;\n\n const resolvedDir = path.resolve(dir);\n\n const watcher = fs.watch(resolvedDir, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n // Only trigger for markdown files\n if (!filename.endsWith(\".md\") && !filename.endsWith(\".mdx\")) return;\n\n // Debounce\n if (timeout) clearTimeout(timeout);\n timeout = setTimeout(async () => {\n if (running) return;\n running = true;\n try {\n await onChange();\n } finally {\n running = false;\n }\n }, debounceMs);\n });\n\n return {\n close: () => {\n if (timeout) clearTimeout(timeout);\n watcher.close();\n },\n };\n}\n"],"mappings":";;;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AASV,SAAS,UAAU,SAAgD;AACxE,QAAM,EAAE,KAAK,aAAa,KAAK,SAAS,IAAI;AAC5C,MAAI,UAAgD;AACpD,MAAI,UAAU;AAEd,QAAM,cAAc,KAAK,QAAQ,GAAG;AAEpC,QAAM,UAAU,GAAG,MAAM,aAAa,EAAE,WAAW,KAAK,GAAG,CAAC,QAAQ,aAAa;AAC/E,QAAI,CAAC,SAAU;AAEf,QAAI,CAAC,SAAS,SAAS,KAAK,KAAK,CAAC,SAAS,SAAS,MAAM,EAAG;AAG7D,QAAI,QAAS,cAAa,OAAO;AACjC,cAAU,WAAW,YAAY;AAC/B,UAAI,QAAS;AACb,gBAAU;AACV,UAAI;AACF,cAAM,SAAS;AAAA,MACjB,UAAE;AACA,kBAAU;AAAA,MACZ;AAAA,IACF,GAAG,UAAU;AAAA,EACf,CAAC;AAED,SAAO;AAAA,IACL,OAAO,MAAM;AACX,UAAI,QAAS,cAAa,OAAO;AACjC,cAAQ,MAAM;AAAA,IAChB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/guard/workflow.ts
4
+ function generateWorkflowYaml(config) {
5
+ const schedule = config.guard.workflow.schedule;
6
+ const labels = config.guard.workflow.labels.map((l) => `"${l}"`).join(", ");
7
+ return `# Auto-generated by docs-ready. Safe to re-run.
8
+ name: docs-ready guard
9
+
10
+ on:
11
+ schedule:
12
+ - cron: "${schedule}"
13
+ workflow_dispatch:
14
+
15
+ permissions:
16
+ issues: write
17
+
18
+ jobs:
19
+ guard:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - uses: actions/setup-node@v4
25
+ with:
26
+ node-version: "20"
27
+
28
+ - name: Install docs-ready
29
+ run: npm install -g docs-ready
30
+
31
+ - name: Run guard checks
32
+ id: guard
33
+ run: |
34
+ docs-ready guard --output markdown > guard-report.md 2>&1 || echo "GUARD_FAILED=true" >> $GITHUB_ENV
35
+
36
+ - name: Close previous docs-ready issues
37
+ if: env.GUARD_FAILED == 'true'
38
+ uses: actions/github-script@v7
39
+ with:
40
+ script: |
41
+ const issues = await github.rest.issues.listForRepo({
42
+ owner: context.repo.owner,
43
+ repo: context.repo.repo,
44
+ labels: ${labels ? `[${labels}]` : '"docs-ready"'},
45
+ state: "open",
46
+ });
47
+ for (const issue of issues.data) {
48
+ await github.rest.issues.update({
49
+ owner: context.repo.owner,
50
+ repo: context.repo.repo,
51
+ issue_number: issue.number,
52
+ state: "closed",
53
+ });
54
+ }
55
+
56
+ - name: Create issue on failure
57
+ if: env.GUARD_FAILED == 'true'
58
+ uses: actions/github-script@v7
59
+ with:
60
+ script: |
61
+ const fs = require("fs");
62
+ const body = fs.readFileSync("guard-report.md", "utf-8");
63
+ await github.rest.issues.create({
64
+ owner: context.repo.owner,
65
+ repo: context.repo.repo,
66
+ title: "docs-ready: Guard checks failed",
67
+ body: body,
68
+ labels: [${labels}],
69
+ });
70
+ `;
71
+ }
72
+ export {
73
+ generateWorkflowYaml
74
+ };
75
+ //# sourceMappingURL=workflow-PTPU42JU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/guard/workflow.ts"],"sourcesContent":["import type { DocsReadyConfig } from \"../core/config.js\";\n\nexport function generateWorkflowYaml(config: DocsReadyConfig): string {\n const schedule = config.guard.workflow.schedule;\n const labels = config.guard.workflow.labels.map((l) => `\"${l}\"`).join(\", \");\n\n return `# Auto-generated by docs-ready. Safe to re-run.\nname: docs-ready guard\n\non:\n schedule:\n - cron: \"${schedule}\"\n workflow_dispatch:\n\npermissions:\n issues: write\n\njobs:\n guard:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Install docs-ready\n run: npm install -g docs-ready\n\n - name: Run guard checks\n id: guard\n run: |\n docs-ready guard --output markdown > guard-report.md 2>&1 || echo \"GUARD_FAILED=true\" >> $GITHUB_ENV\n\n - name: Close previous docs-ready issues\n if: env.GUARD_FAILED == 'true'\n uses: actions/github-script@v7\n with:\n script: |\n const issues = await github.rest.issues.listForRepo({\n owner: context.repo.owner,\n repo: context.repo.repo,\n labels: ${labels ? `[${labels}]` : '\"docs-ready\"'},\n state: \"open\",\n });\n for (const issue of issues.data) {\n await github.rest.issues.update({\n owner: context.repo.owner,\n repo: context.repo.repo,\n issue_number: issue.number,\n state: \"closed\",\n });\n }\n\n - name: Create issue on failure\n if: env.GUARD_FAILED == 'true'\n uses: actions/github-script@v7\n with:\n script: |\n const fs = require(\"fs\");\n const body = fs.readFileSync(\"guard-report.md\", \"utf-8\");\n await github.rest.issues.create({\n owner: context.repo.owner,\n repo: context.repo.repo,\n title: \"docs-ready: Guard checks failed\",\n body: body,\n labels: [${labels}],\n });\n`;\n}\n"],"mappings":";;;AAEO,SAAS,qBAAqB,QAAiC;AACpE,QAAM,WAAW,OAAO,MAAM,SAAS;AACvC,QAAM,SAAS,OAAO,MAAM,SAAS,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI;AAE1E,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,eAKM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAgCC,SAAS,IAAI,MAAM,MAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAwBtC,MAAM;AAAA;AAAA;AAG/B;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docs-ready",
3
- "version": "0.4.0",
3
+ "version": "0.8.0",
4
4
  "description": "Make your docs AI-ready. Keep them that way.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,21 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/utils/logger.ts
4
- import chalk from "chalk";
5
- import ora from "ora";
6
- var log = {
7
- info: (msg) => console.log(chalk.blue("\u2139"), msg),
8
- success: (msg) => console.log(chalk.green("\u2714"), msg),
9
- warn: (msg) => console.log(chalk.yellow("\u26A0"), msg),
10
- error: (msg) => console.error(chalk.red("\u2716"), msg),
11
- dim: (msg) => console.log(chalk.dim(msg))
12
- };
13
- function spinner(text) {
14
- return ora({ text, color: "cyan" });
15
- }
16
-
17
- export {
18
- log,
19
- spinner
20
- };
21
- //# sourceMappingURL=chunk-7YN54Y4Y.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/logger.ts"],"sourcesContent":["import chalk from \"chalk\";\nimport ora, { type Ora } from \"ora\";\n\nexport const log = {\n info: (msg: string) => console.log(chalk.blue(\"ℹ\"), msg),\n success: (msg: string) => console.log(chalk.green(\"✔\"), msg),\n warn: (msg: string) => console.log(chalk.yellow(\"⚠\"), msg),\n error: (msg: string) => console.error(chalk.red(\"✖\"), msg),\n dim: (msg: string) => console.log(chalk.dim(msg)),\n};\n\nexport function spinner(text: string): Ora {\n return ora({ text, color: \"cyan\" });\n}\n"],"mappings":";;;AAAA,OAAO,WAAW;AAClB,OAAO,SAAuB;AAEvB,IAAM,MAAM;AAAA,EACjB,MAAM,CAAC,QAAgB,QAAQ,IAAI,MAAM,KAAK,QAAG,GAAG,GAAG;AAAA,EACvD,SAAS,CAAC,QAAgB,QAAQ,IAAI,MAAM,MAAM,QAAG,GAAG,GAAG;AAAA,EAC3D,MAAM,CAAC,QAAgB,QAAQ,IAAI,MAAM,OAAO,QAAG,GAAG,GAAG;AAAA,EACzD,OAAO,CAAC,QAAgB,QAAQ,MAAM,MAAM,IAAI,QAAG,GAAG,GAAG;AAAA,EACzD,KAAK,CAAC,QAAgB,QAAQ,IAAI,MAAM,IAAI,GAAG,CAAC;AAClD;AAEO,SAAS,QAAQ,MAAmB;AACzC,SAAO,IAAI,EAAE,MAAM,OAAO,OAAO,CAAC;AACpC;","names":[]}