docguard-cli 0.5.1 → 0.6.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/README.md +37 -15
- package/cli/commands/generate.mjs +432 -82
- package/cli/commands/publish.mjs +246 -0
- package/cli/docguard.mjs +19 -3
- package/cli/scanners/doc-tools.mjs +351 -0
- package/cli/scanners/routes.mjs +461 -0
- package/cli/scanners/schemas.mjs +567 -0
- package/docs/ai-integration.md +7 -7
- package/docs/commands.md +40 -40
- package/docs/faq.md +1 -1
- package/docs/profiles.md +6 -6
- package/docs/quickstart.md +10 -10
- package/package.json +1 -1
- package/templates/ci/github-actions.yml +1 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocGuard Publish Command
|
|
3
|
+
* Scaffolds documentation publishing config for external doc platforms.
|
|
4
|
+
* Currently supports: Mintlify
|
|
5
|
+
*
|
|
6
|
+
* Usage: docguard publish --platform mintlify [--dir .]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { resolve, basename } from 'node:path';
|
|
11
|
+
import { c } from '../docguard.mjs';
|
|
12
|
+
|
|
13
|
+
const SUPPORTED_PLATFORMS = ['mintlify'];
|
|
14
|
+
|
|
15
|
+
export function runPublish(projectDir, config, flags) {
|
|
16
|
+
const platform = flags.platform || 'mintlify';
|
|
17
|
+
|
|
18
|
+
if (!SUPPORTED_PLATFORMS.includes(platform)) {
|
|
19
|
+
console.error(`${c.red}✗ Unsupported platform: ${platform}${c.reset}`);
|
|
20
|
+
console.log(` Supported: ${SUPPORTED_PLATFORMS.join(', ')}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(`${c.bold}📚 DocGuard Publish — ${platform}${c.reset}`);
|
|
25
|
+
console.log(`${c.dim} Scaffolding ${platform} docs from canonical documentation...${c.reset}\n`);
|
|
26
|
+
|
|
27
|
+
switch (platform) {
|
|
28
|
+
case 'mintlify':
|
|
29
|
+
scaffoldMintlify(projectDir, config, flags);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function scaffoldMintlify(dir, config, flags) {
|
|
35
|
+
const docsDir = resolve(dir, 'docs');
|
|
36
|
+
|
|
37
|
+
// Check for existing Mintlify setup
|
|
38
|
+
if (existsSync(resolve(dir, 'docs.json')) && !flags.force) {
|
|
39
|
+
console.log(` ${c.yellow}⚠️ docs.json already exists.${c.reset} Use --force to overwrite.`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create docs directory
|
|
44
|
+
if (!existsSync(docsDir)) {
|
|
45
|
+
mkdirSync(docsDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let created = 0;
|
|
49
|
+
|
|
50
|
+
// ── 1. Generate docs.json (Mintlify v2 config) ──
|
|
51
|
+
const docsJson = {
|
|
52
|
+
"$schema": "https://mintlify.com/docs.json",
|
|
53
|
+
name: config.projectName || basename(dir),
|
|
54
|
+
logo: {
|
|
55
|
+
dark: "/logo/dark.svg",
|
|
56
|
+
light: "/logo/light.svg",
|
|
57
|
+
},
|
|
58
|
+
favicon: "/favicon.svg",
|
|
59
|
+
colors: {
|
|
60
|
+
primary: "#0D9373",
|
|
61
|
+
light: "#07C983",
|
|
62
|
+
dark: "#0D9373",
|
|
63
|
+
},
|
|
64
|
+
topbarLinks: [
|
|
65
|
+
{
|
|
66
|
+
name: "GitHub",
|
|
67
|
+
url: `https://github.com/${config.repository || 'your-org/your-repo'}`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
topbarCtaButton: {
|
|
71
|
+
name: "Get Started",
|
|
72
|
+
url: "/quickstart",
|
|
73
|
+
},
|
|
74
|
+
tabs: [
|
|
75
|
+
{
|
|
76
|
+
name: "Architecture",
|
|
77
|
+
url: "architecture",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "API Reference",
|
|
81
|
+
url: "api-reference",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
navigation: buildMintlifyNavigation(dir),
|
|
85
|
+
footerSocials: {
|
|
86
|
+
github: `https://github.com/${config.repository || 'your-org/your-repo'}`,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
writeFileSync(resolve(dir, 'docs.json'), JSON.stringify(docsJson, null, 2), 'utf-8');
|
|
91
|
+
console.log(` ${c.green}✅ docs.json${c.reset} (Mintlify v2 config)`);
|
|
92
|
+
created++;
|
|
93
|
+
|
|
94
|
+
// ── 2. Generate introduction.mdx ──
|
|
95
|
+
const readmePath = resolve(dir, 'README.md');
|
|
96
|
+
let readmeContent = '';
|
|
97
|
+
if (existsSync(readmePath)) {
|
|
98
|
+
readmeContent = readFileSync(readmePath, 'utf-8');
|
|
99
|
+
// Extract first paragraph for description
|
|
100
|
+
const firstPara = readmeContent.split('\n\n').slice(1, 3).join('\n\n');
|
|
101
|
+
readmeContent = firstPara;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const introContent = `---
|
|
105
|
+
title: Introduction
|
|
106
|
+
description: "${config.projectName} documentation"
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
# ${config.projectName}
|
|
110
|
+
|
|
111
|
+
${readmeContent || `Welcome to the ${config.projectName} documentation.`}
|
|
112
|
+
|
|
113
|
+
## Quick Links
|
|
114
|
+
|
|
115
|
+
<CardGroup cols={2}>
|
|
116
|
+
<Card title="Quick Start" icon="rocket" href="/quickstart">
|
|
117
|
+
Get up and running in 5 minutes
|
|
118
|
+
</Card>
|
|
119
|
+
<Card title="Architecture" icon="building" href="/architecture">
|
|
120
|
+
Understand the system design
|
|
121
|
+
</Card>
|
|
122
|
+
<Card title="API Reference" icon="code" href="/api-reference">
|
|
123
|
+
Explore the API endpoints
|
|
124
|
+
</Card>
|
|
125
|
+
<Card title="Data Model" icon="database" href="/data-model">
|
|
126
|
+
Learn about the data structure
|
|
127
|
+
</Card>
|
|
128
|
+
</CardGroup>
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
writeFileSync(resolve(docsDir, 'introduction.mdx'), introContent, 'utf-8');
|
|
132
|
+
console.log(` ${c.green}✅ docs/introduction.mdx${c.reset}`);
|
|
133
|
+
created++;
|
|
134
|
+
|
|
135
|
+
// ── 3. Generate quickstart.mdx ──
|
|
136
|
+
const quickstartContent = `---
|
|
137
|
+
title: Quick Start
|
|
138
|
+
description: "Get started with ${config.projectName}"
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
# Quick Start
|
|
142
|
+
|
|
143
|
+
## Prerequisites
|
|
144
|
+
|
|
145
|
+
- Node.js 18+
|
|
146
|
+
- npm or pnpm
|
|
147
|
+
|
|
148
|
+
## Installation
|
|
149
|
+
|
|
150
|
+
\`\`\`bash
|
|
151
|
+
npm install
|
|
152
|
+
\`\`\`
|
|
153
|
+
|
|
154
|
+
## Setup
|
|
155
|
+
|
|
156
|
+
\`\`\`bash
|
|
157
|
+
cp .env.example .env.local
|
|
158
|
+
# Fill in environment variables
|
|
159
|
+
npm run dev
|
|
160
|
+
\`\`\`
|
|
161
|
+
|
|
162
|
+
## Verify
|
|
163
|
+
|
|
164
|
+
\`\`\`bash
|
|
165
|
+
npx docguard-cli guard # Check documentation compliance
|
|
166
|
+
npx docguard-cli score # View CDD maturity score
|
|
167
|
+
\`\`\`
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
writeFileSync(resolve(docsDir, 'quickstart.mdx'), quickstartContent, 'utf-8');
|
|
171
|
+
console.log(` ${c.green}✅ docs/quickstart.mdx${c.reset}`);
|
|
172
|
+
created++;
|
|
173
|
+
|
|
174
|
+
// ── 4. Map canonical docs to Mintlify pages ──
|
|
175
|
+
const canonicalDir = resolve(dir, 'docs-canonical');
|
|
176
|
+
const mappings = [
|
|
177
|
+
{ source: 'ARCHITECTURE.md', target: 'architecture.mdx', title: 'Architecture' },
|
|
178
|
+
{ source: 'API-REFERENCE.md', target: 'api-reference.mdx', title: 'API Reference' },
|
|
179
|
+
{ source: 'DATA-MODEL.md', target: 'data-model.mdx', title: 'Data Model' },
|
|
180
|
+
{ source: 'SECURITY.md', target: 'security.mdx', title: 'Security' },
|
|
181
|
+
{ source: 'ENVIRONMENT.md', target: 'environment.mdx', title: 'Environment' },
|
|
182
|
+
{ source: 'TEST-SPEC.md', target: 'test-spec.mdx', title: 'Test Specification' },
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
for (const mapping of mappings) {
|
|
186
|
+
const sourcePath = resolve(canonicalDir, mapping.source);
|
|
187
|
+
if (existsSync(sourcePath)) {
|
|
188
|
+
let content = readFileSync(sourcePath, 'utf-8');
|
|
189
|
+
|
|
190
|
+
// Add Mintlify frontmatter
|
|
191
|
+
const frontmatter = `---\ntitle: "${mapping.title}"\ndescription: "${config.projectName} ${mapping.title.toLowerCase()}"\n---\n\n`;
|
|
192
|
+
|
|
193
|
+
// Remove docguard metadata comments
|
|
194
|
+
content = content.replace(/<!--\s*docguard:\w+\s+[^>]+\s*-->\n?/g, '');
|
|
195
|
+
content = content.replace(/> \*\*Auto-generated by DocGuard\.\*\*[^\n]*\n?/g, '');
|
|
196
|
+
|
|
197
|
+
writeFileSync(resolve(docsDir, mapping.target), frontmatter + content, 'utf-8');
|
|
198
|
+
console.log(` ${c.green}✅ docs/${mapping.target}${c.reset} ← ${mapping.source}`);
|
|
199
|
+
created++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Summary ──
|
|
204
|
+
console.log(`\n${c.bold} ─────────────────────────────────────────${c.reset}`);
|
|
205
|
+
console.log(` ${c.green}Created: ${created} files${c.reset}`);
|
|
206
|
+
console.log(`\n ${c.bold}Next steps:${c.reset}`);
|
|
207
|
+
console.log(` ${c.cyan}1.${c.reset} Install Mintlify: ${c.dim}npm install -g mintlify${c.reset}`);
|
|
208
|
+
console.log(` ${c.cyan}2.${c.reset} Preview locally: ${c.dim}mintlify dev${c.reset}`);
|
|
209
|
+
console.log(` ${c.cyan}3.${c.reset} Push to GitHub → auto-deploys on Mintlify${c.reset}`);
|
|
210
|
+
console.log(` ${c.dim}\n Mintlify is free for open-source projects.${c.reset}`);
|
|
211
|
+
console.log(` ${c.dim} Docs: https://mintlify.com/docs${c.reset}\n`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildMintlifyNavigation(dir) {
|
|
215
|
+
const nav = [
|
|
216
|
+
{
|
|
217
|
+
group: "Getting Started",
|
|
218
|
+
pages: ["introduction", "quickstart"],
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
// Add architecture group if docs exist
|
|
223
|
+
const canonicalDir = resolve(dir, 'docs-canonical');
|
|
224
|
+
const architecturePages = [];
|
|
225
|
+
if (existsSync(resolve(canonicalDir, 'ARCHITECTURE.md'))) architecturePages.push("architecture");
|
|
226
|
+
if (existsSync(resolve(canonicalDir, 'DATA-MODEL.md'))) architecturePages.push("data-model");
|
|
227
|
+
if (existsSync(resolve(canonicalDir, 'SECURITY.md'))) architecturePages.push("security");
|
|
228
|
+
if (architecturePages.length > 0) {
|
|
229
|
+
nav.push({ group: "Architecture", pages: architecturePages });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Add operations group
|
|
233
|
+
const opsPages = [];
|
|
234
|
+
if (existsSync(resolve(canonicalDir, 'ENVIRONMENT.md'))) opsPages.push("environment");
|
|
235
|
+
if (existsSync(resolve(canonicalDir, 'TEST-SPEC.md'))) opsPages.push("test-spec");
|
|
236
|
+
if (opsPages.length > 0) {
|
|
237
|
+
nav.push({ group: "Operations", pages: opsPages });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Add API reference
|
|
241
|
+
if (existsSync(resolve(canonicalDir, 'API-REFERENCE.md'))) {
|
|
242
|
+
nav.push({ group: "API Reference", pages: ["api-reference"] });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return nav;
|
|
246
|
+
}
|
package/cli/docguard.mjs
CHANGED
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { readFileSync, existsSync } from 'node:fs';
|
|
18
|
-
import { resolve, basename } from 'node:path';
|
|
18
|
+
import { resolve, basename, dirname } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
// Read version from package.json (single source of truth)
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const PKG = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
24
|
+
const VERSION = PKG.version;
|
|
19
25
|
import { runAudit } from './commands/audit.mjs';
|
|
20
26
|
import { runInit } from './commands/init.mjs';
|
|
21
27
|
import { runGuard } from './commands/guard.mjs';
|
|
@@ -29,6 +35,7 @@ import { runCI } from './commands/ci.mjs';
|
|
|
29
35
|
import { runFix } from './commands/fix.mjs';
|
|
30
36
|
import { runWatch } from './commands/watch.mjs';
|
|
31
37
|
import { runDiagnose } from './commands/diagnose.mjs';
|
|
38
|
+
import { runPublish } from './commands/publish.mjs';
|
|
32
39
|
|
|
33
40
|
// ── Colors (ANSI escape codes, zero deps) ──────────────────────────────────
|
|
34
41
|
export const c = {
|
|
@@ -253,7 +260,7 @@ function deepMerge(target, source) {
|
|
|
253
260
|
function printBanner() {
|
|
254
261
|
console.log(`
|
|
255
262
|
${c.cyan}${c.bold} ╔═══════════════════════════════════════════╗
|
|
256
|
-
║ DocGuard
|
|
263
|
+
║ DocGuard v${VERSION.padEnd(27)}║
|
|
257
264
|
║ Canonical-Driven Development (CDD) ║
|
|
258
265
|
╚═══════════════════════════════════════════╝${c.reset}
|
|
259
266
|
`);
|
|
@@ -279,6 +286,7 @@ ${c.bold}Commands:${c.reset}
|
|
|
279
286
|
${c.green}ci${c.reset} Single command for CI/CD pipelines (guard + score)
|
|
280
287
|
${c.green}fix${c.reset} Find issues and generate AI fix instructions
|
|
281
288
|
${c.green}watch${c.reset} Watch for file changes and re-run guard automatically
|
|
289
|
+
${c.green}publish${c.reset} Scaffold external docs (Mintlify, Docusaurus)
|
|
282
290
|
|
|
283
291
|
${c.bold}Options:${c.reset}
|
|
284
292
|
--dir <path> Project directory (default: current directory)
|
|
@@ -295,6 +303,7 @@ ${c.bold}Options:${c.reset}
|
|
|
295
303
|
--auto Auto-fix what's possible (used with fix command)
|
|
296
304
|
--doc <name> Generate AI prompt for specific doc (architecture, security, etc.)
|
|
297
305
|
--profile <p> Compliance profile: starter, standard, enterprise (init command)
|
|
306
|
+
--platform <p> Doc platform: mintlify (publish command)
|
|
298
307
|
--tax Show estimated documentation maintenance cost (with score)
|
|
299
308
|
--help Show this help message
|
|
300
309
|
--version Show version
|
|
@@ -384,6 +393,9 @@ function main() {
|
|
|
384
393
|
flags.autoFix = true;
|
|
385
394
|
} else if (args[i] === '--skip-prompts') {
|
|
386
395
|
flags.skipPrompts = true;
|
|
396
|
+
} else if (args[i] === '--platform' && args[i + 1]) {
|
|
397
|
+
flags.platform = args[i + 1];
|
|
398
|
+
i++;
|
|
387
399
|
}
|
|
388
400
|
}
|
|
389
401
|
|
|
@@ -395,7 +407,7 @@ function main() {
|
|
|
395
407
|
}
|
|
396
408
|
|
|
397
409
|
if (command === '--version' || command === '-v') {
|
|
398
|
-
console.log(
|
|
410
|
+
console.log(`docguard v${VERSION}`);
|
|
399
411
|
process.exit(0);
|
|
400
412
|
}
|
|
401
413
|
|
|
@@ -448,6 +460,10 @@ function main() {
|
|
|
448
460
|
case 'watch':
|
|
449
461
|
runWatch(projectDir, config, flags);
|
|
450
462
|
break;
|
|
463
|
+
case 'publish':
|
|
464
|
+
case 'pub':
|
|
465
|
+
runPublish(projectDir, config, flags);
|
|
466
|
+
break;
|
|
451
467
|
default:
|
|
452
468
|
console.error(`${c.red}Unknown command: ${command}${c.reset}`);
|
|
453
469
|
console.log(`Run ${c.cyan}docguard --help${c.reset} for usage.`);
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doc Tool Detection Scanner
|
|
3
|
+
* Detects existing documentation tools in a project (OpenAPI, TypeDoc, JSDoc, Storybook, etc.)
|
|
4
|
+
* and extracts available data from their outputs.
|
|
5
|
+
*
|
|
6
|
+
* Philosophy: Detect and leverage, never replace.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { resolve, join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect all documentation tools present in the project.
|
|
14
|
+
* @param {string} dir - Project root directory
|
|
15
|
+
* @returns {object} Detected tools with their config and extracted data
|
|
16
|
+
*/
|
|
17
|
+
export function detectDocTools(dir) {
|
|
18
|
+
const tools = {
|
|
19
|
+
openapi: detectOpenAPI(dir),
|
|
20
|
+
typedoc: detectTypeDoc(dir),
|
|
21
|
+
jsdoc: detectJSDoc(dir),
|
|
22
|
+
storybook: detectStorybook(dir),
|
|
23
|
+
docusaurus: detectDocusaurus(dir),
|
|
24
|
+
mintlify: detectMintlify(dir),
|
|
25
|
+
redocly: detectRedocly(dir),
|
|
26
|
+
swagger: detectSwagger(dir),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Count detected tools
|
|
30
|
+
tools._detected = Object.entries(tools)
|
|
31
|
+
.filter(([k, v]) => k !== '_detected' && v.found)
|
|
32
|
+
.map(([k]) => k);
|
|
33
|
+
|
|
34
|
+
return tools;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── OpenAPI / Swagger Spec ─────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function detectOpenAPI(dir) {
|
|
40
|
+
const candidates = [
|
|
41
|
+
'openapi.yaml', 'openapi.yml', 'openapi.json',
|
|
42
|
+
'swagger.yaml', 'swagger.yml', 'swagger.json',
|
|
43
|
+
'api/openapi.yaml', 'api/openapi.yml', 'api/openapi.json',
|
|
44
|
+
'docs/openapi.yaml', 'docs/openapi.yml',
|
|
45
|
+
'spec/openapi.yaml', 'spec/openapi.yml',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const fullPath = resolve(dir, candidate);
|
|
50
|
+
if (existsSync(fullPath)) {
|
|
51
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
52
|
+
const spec = parseOpenAPISpec(content, candidate);
|
|
53
|
+
return {
|
|
54
|
+
found: true,
|
|
55
|
+
path: candidate,
|
|
56
|
+
version: spec.version,
|
|
57
|
+
endpoints: spec.endpoints,
|
|
58
|
+
schemas: spec.schemas,
|
|
59
|
+
info: spec.info,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { found: false };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseOpenAPISpec(content, filename) {
|
|
68
|
+
const result = { version: null, endpoints: [], schemas: [], info: {} };
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
let spec;
|
|
72
|
+
if (filename.endsWith('.json')) {
|
|
73
|
+
spec = JSON.parse(content);
|
|
74
|
+
} else {
|
|
75
|
+
// Simple YAML parsing for common patterns (no dependency)
|
|
76
|
+
spec = parseSimpleYAML(content);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Version
|
|
80
|
+
result.version = spec.openapi || spec.swagger || 'unknown';
|
|
81
|
+
|
|
82
|
+
// Info
|
|
83
|
+
result.info = {
|
|
84
|
+
title: spec.info?.title || '',
|
|
85
|
+
description: spec.info?.description || '',
|
|
86
|
+
version: spec.info?.version || '',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Endpoints
|
|
90
|
+
if (spec.paths) {
|
|
91
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
92
|
+
for (const [method, details] of Object.entries(methods)) {
|
|
93
|
+
if (['get', 'post', 'put', 'delete', 'patch', 'options', 'head'].includes(method)) {
|
|
94
|
+
result.endpoints.push({
|
|
95
|
+
method: method.toUpperCase(),
|
|
96
|
+
path,
|
|
97
|
+
summary: details?.summary || details?.description || '',
|
|
98
|
+
tags: details?.tags || [],
|
|
99
|
+
operationId: details?.operationId || '',
|
|
100
|
+
auth: !!(details?.security?.length),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Schemas
|
|
108
|
+
const schemas = spec.components?.schemas || spec.definitions || {};
|
|
109
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
110
|
+
const fields = [];
|
|
111
|
+
if (schema.properties) {
|
|
112
|
+
for (const [fieldName, fieldDef] of Object.entries(schema.properties)) {
|
|
113
|
+
fields.push({
|
|
114
|
+
name: fieldName,
|
|
115
|
+
type: fieldDef.type || fieldDef.$ref?.split('/').pop() || 'object',
|
|
116
|
+
required: (schema.required || []).includes(fieldName),
|
|
117
|
+
description: fieldDef.description || '',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
result.schemas.push({ name, fields, description: schema.description || '' });
|
|
122
|
+
}
|
|
123
|
+
} catch { /* spec parsing failed, return empty */ }
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Minimal YAML parser for OpenAPI specs.
|
|
130
|
+
* Handles the most common structures without external dependencies.
|
|
131
|
+
* NOT a full YAML parser — covers 80% of real-world OpenAPI files.
|
|
132
|
+
*/
|
|
133
|
+
function parseSimpleYAML(content) {
|
|
134
|
+
// Try JSON first (some .yaml files are actually JSON)
|
|
135
|
+
try { return JSON.parse(content); } catch { /* not JSON */ }
|
|
136
|
+
|
|
137
|
+
const result = {};
|
|
138
|
+
const lines = content.split('\n');
|
|
139
|
+
const stack = [{ obj: result, indent: -1 }];
|
|
140
|
+
|
|
141
|
+
for (const rawLine of lines) {
|
|
142
|
+
const line = rawLine.replace(/\r$/, '');
|
|
143
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
144
|
+
|
|
145
|
+
const indent = line.search(/\S/);
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
|
|
148
|
+
// Pop stack to find parent
|
|
149
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
150
|
+
stack.pop();
|
|
151
|
+
}
|
|
152
|
+
const parent = stack[stack.length - 1].obj;
|
|
153
|
+
|
|
154
|
+
// Array item
|
|
155
|
+
if (trimmed.startsWith('- ')) {
|
|
156
|
+
const value = trimmed.slice(2).trim();
|
|
157
|
+
if (Array.isArray(parent)) {
|
|
158
|
+
if (value.includes(':')) {
|
|
159
|
+
const obj = {};
|
|
160
|
+
const [k, ...rest] = value.split(':');
|
|
161
|
+
obj[k.trim()] = rest.join(':').trim().replace(/^['"]|['"]$/g, '');
|
|
162
|
+
parent.push(obj);
|
|
163
|
+
stack.push({ obj, indent });
|
|
164
|
+
} else {
|
|
165
|
+
parent.push(value.replace(/^['"]|['"]$/g, ''));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Key-value pair
|
|
172
|
+
const colonIdx = trimmed.indexOf(':');
|
|
173
|
+
if (colonIdx > 0) {
|
|
174
|
+
const key = trimmed.substring(0, colonIdx).trim();
|
|
175
|
+
const rawVal = trimmed.substring(colonIdx + 1).trim();
|
|
176
|
+
|
|
177
|
+
if (rawVal === '' || rawVal === '|' || rawVal === '>') {
|
|
178
|
+
// Nested object or block
|
|
179
|
+
const child = {};
|
|
180
|
+
if (typeof parent === 'object' && !Array.isArray(parent)) {
|
|
181
|
+
parent[key] = child;
|
|
182
|
+
}
|
|
183
|
+
stack.push({ obj: child, indent });
|
|
184
|
+
} else if (rawVal.startsWith('[')) {
|
|
185
|
+
// Inline array
|
|
186
|
+
try {
|
|
187
|
+
parent[key] = JSON.parse(rawVal);
|
|
188
|
+
} catch {
|
|
189
|
+
parent[key] = rawVal;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Simple value
|
|
193
|
+
let val = rawVal.replace(/^['"]|['"]$/g, '');
|
|
194
|
+
if (val === 'true') val = true;
|
|
195
|
+
else if (val === 'false') val = false;
|
|
196
|
+
else if (/^\d+$/.test(val)) val = parseInt(val);
|
|
197
|
+
if (typeof parent === 'object' && !Array.isArray(parent)) {
|
|
198
|
+
parent[key] = val;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── TypeDoc ────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
function detectTypeDoc(dir) {
|
|
210
|
+
const configs = ['typedoc.json', 'typedoc.config.js', 'typedoc.config.mjs'];
|
|
211
|
+
for (const config of configs) {
|
|
212
|
+
if (existsSync(resolve(dir, config))) {
|
|
213
|
+
return { found: true, config };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check package.json devDeps
|
|
218
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
219
|
+
if (existsSync(pkgPath)) {
|
|
220
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
221
|
+
if (pkg.devDependencies?.typedoc) {
|
|
222
|
+
return { found: true, config: 'package.json (devDependency)' };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { found: false };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── JSDoc ──────────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
function detectJSDoc(dir) {
|
|
232
|
+
const configs = ['jsdoc.json', '.jsdoc.json', 'jsdoc.conf.json'];
|
|
233
|
+
for (const config of configs) {
|
|
234
|
+
if (existsSync(resolve(dir, config))) {
|
|
235
|
+
return { found: true, config };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
240
|
+
if (existsSync(pkgPath)) {
|
|
241
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
242
|
+
if (pkg.devDependencies?.jsdoc) {
|
|
243
|
+
return { found: true, config: 'package.json (devDependency)' };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { found: false };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Storybook ──────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
function detectStorybook(dir) {
|
|
253
|
+
if (existsSync(resolve(dir, '.storybook'))) {
|
|
254
|
+
// Count stories
|
|
255
|
+
let storyCount = 0;
|
|
256
|
+
const storyDirs = ['src', 'components', 'stories'];
|
|
257
|
+
for (const sd of storyDirs) {
|
|
258
|
+
const fullDir = resolve(dir, sd);
|
|
259
|
+
if (existsSync(fullDir)) {
|
|
260
|
+
storyCount += countFiles(fullDir, /\.(stories|story)\.(js|jsx|ts|tsx|mdx)$/);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { found: true, config: '.storybook/', storyCount };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { found: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Docusaurus ─────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function detectDocusaurus(dir) {
|
|
272
|
+
const configs = ['docusaurus.config.js', 'docusaurus.config.ts', 'docusaurus.config.mjs'];
|
|
273
|
+
for (const config of configs) {
|
|
274
|
+
if (existsSync(resolve(dir, config))) {
|
|
275
|
+
return { found: true, config };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { found: false };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Mintlify ───────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
function detectMintlify(dir) {
|
|
284
|
+
// Check for docs.json (new) or mint.json (legacy)
|
|
285
|
+
for (const config of ['docs.json', 'mint.json']) {
|
|
286
|
+
const fullPath = resolve(dir, config);
|
|
287
|
+
if (existsSync(fullPath)) {
|
|
288
|
+
try {
|
|
289
|
+
const content = JSON.parse(readFileSync(fullPath, 'utf-8'));
|
|
290
|
+
return {
|
|
291
|
+
found: true,
|
|
292
|
+
config,
|
|
293
|
+
name: content.name || '',
|
|
294
|
+
version: config === 'docs.json' ? 'v2' : 'v1',
|
|
295
|
+
};
|
|
296
|
+
} catch {
|
|
297
|
+
return { found: true, config };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return { found: false };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Redocly ────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function detectRedocly(dir) {
|
|
307
|
+
const configs = ['redocly.yaml', 'redocly.yml', '.redocly.yaml', '.redocly.yml'];
|
|
308
|
+
for (const config of configs) {
|
|
309
|
+
if (existsSync(resolve(dir, config))) {
|
|
310
|
+
return { found: true, config };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { found: false };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── Swagger UI ─────────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
function detectSwagger(dir) {
|
|
319
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
320
|
+
if (existsSync(pkgPath)) {
|
|
321
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
322
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
323
|
+
if (allDeps['swagger-ui-express'] || allDeps['@fastify/swagger'] || allDeps['swagger-jsdoc']) {
|
|
324
|
+
return {
|
|
325
|
+
found: true,
|
|
326
|
+
middleware: allDeps['swagger-ui-express'] ? 'swagger-ui-express' :
|
|
327
|
+
allDeps['@fastify/swagger'] ? '@fastify/swagger' : 'swagger-jsdoc',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { found: false };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
function countFiles(dir, pattern) {
|
|
337
|
+
let count = 0;
|
|
338
|
+
try {
|
|
339
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
342
|
+
const fullPath = join(dir, entry.name);
|
|
343
|
+
if (entry.isDirectory()) {
|
|
344
|
+
count += countFiles(fullPath, pattern);
|
|
345
|
+
} else if (pattern.test(entry.name)) {
|
|
346
|
+
count++;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} catch { /* skip */ }
|
|
350
|
+
return count;
|
|
351
|
+
}
|