docguard-cli 0.5.2 → 0.7.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/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,8 @@ 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';
39
+ import { runTrace } from './commands/trace.mjs';
32
40
 
33
41
  // ── Colors (ANSI escape codes, zero deps) ──────────────────────────────────
34
42
  export const c = {
@@ -253,7 +261,7 @@ function deepMerge(target, source) {
253
261
  function printBanner() {
254
262
  console.log(`
255
263
  ${c.cyan}${c.bold} ╔═══════════════════════════════════════════╗
256
- ║ DocGuard v0.5.0
264
+ ║ DocGuard v${VERSION.padEnd(27)}
257
265
  ║ Canonical-Driven Development (CDD) ║
258
266
  ╚═══════════════════════════════════════════╝${c.reset}
259
267
  `);
@@ -279,6 +287,8 @@ ${c.bold}Commands:${c.reset}
279
287
  ${c.green}ci${c.reset} Single command for CI/CD pipelines (guard + score)
280
288
  ${c.green}fix${c.reset} Find issues and generate AI fix instructions
281
289
  ${c.green}watch${c.reset} Watch for file changes and re-run guard automatically
290
+ ${c.green}publish${c.reset} Scaffold external docs (Mintlify, Docusaurus)
291
+ ${c.green}trace${c.reset} Generate requirements traceability matrix
282
292
 
283
293
  ${c.bold}Options:${c.reset}
284
294
  --dir <path> Project directory (default: current directory)
@@ -295,6 +305,7 @@ ${c.bold}Options:${c.reset}
295
305
  --auto Auto-fix what's possible (used with fix command)
296
306
  --doc <name> Generate AI prompt for specific doc (architecture, security, etc.)
297
307
  --profile <p> Compliance profile: starter, standard, enterprise (init command)
308
+ --platform <p> Doc platform: mintlify (publish command)
298
309
  --tax Show estimated documentation maintenance cost (with score)
299
310
  --help Show this help message
300
311
  --version Show version
@@ -384,6 +395,15 @@ function main() {
384
395
  flags.autoFix = true;
385
396
  } else if (args[i] === '--skip-prompts') {
386
397
  flags.skipPrompts = true;
398
+ } else if (args[i] === '--platform' && args[i + 1]) {
399
+ flags.platform = args[i + 1];
400
+ i++;
401
+ } else if (args[i] === '--no-fix') {
402
+ flags.noFix = true;
403
+ } else if (args[i] === '--signals') {
404
+ flags.signals = true;
405
+ } else if (args[i] === '--debate') {
406
+ flags.debate = true;
387
407
  }
388
408
  }
389
409
 
@@ -395,7 +415,7 @@ function main() {
395
415
  }
396
416
 
397
417
  if (command === '--version' || command === '-v') {
398
- console.log('docguard v0.5.0');
418
+ console.log(`docguard v${VERSION}`);
399
419
  process.exit(0);
400
420
  }
401
421
 
@@ -448,6 +468,14 @@ function main() {
448
468
  case 'watch':
449
469
  runWatch(projectDir, config, flags);
450
470
  break;
471
+ case 'publish':
472
+ case 'pub':
473
+ runPublish(projectDir, config, flags);
474
+ break;
475
+ case 'trace':
476
+ case 'traceability':
477
+ runTrace(projectDir, config, flags);
478
+ break;
451
479
  default:
452
480
  console.error(`${c.red}Unknown command: ${command}${c.reset}`);
453
481
  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
+ }