docguard-cli 0.5.2 → 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.
@@ -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 v0.5.0
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('docguard v0.5.0');
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
+ }