pulse-js-framework 1.7.11 → 1.7.13

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/docs.js ADDED
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Pulse CLI - Docs Command
3
+ * Generate API documentation from JSDoc comments
4
+ */
5
+
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
7
+ import { join, relative, dirname, basename, extname } from 'path';
8
+ import { log } from './logger.js';
9
+ import { parseArgs, formatBytes } from './utils/file-utils.js';
10
+ import { createTimer, formatDuration } from './utils/cli-ui.js';
11
+
12
+ /**
13
+ * JSDoc tag types
14
+ */
15
+ const JSDOC_TAGS = [
16
+ 'param', 'parameter', 'arg', 'argument',
17
+ 'returns', 'return',
18
+ 'type', 'typedef',
19
+ 'property', 'prop',
20
+ 'example',
21
+ 'throws', 'exception',
22
+ 'deprecated',
23
+ 'see', 'link',
24
+ 'since', 'version',
25
+ 'author',
26
+ 'module', 'namespace',
27
+ 'exports', 'export',
28
+ 'class', 'constructor',
29
+ 'function', 'func', 'method',
30
+ 'private', 'protected', 'public',
31
+ 'static',
32
+ 'async',
33
+ 'callback',
34
+ 'template',
35
+ 'default', 'defaultvalue'
36
+ ];
37
+
38
+ /**
39
+ * Parse JSDoc comment block
40
+ * @param {string} comment - Raw JSDoc comment
41
+ * @returns {Object} Parsed JSDoc data
42
+ */
43
+ function parseJSDocComment(comment) {
44
+ // Remove comment markers
45
+ const lines = comment
46
+ .replace(/^\/\*\*\s*/, '')
47
+ .replace(/\s*\*\/$/, '')
48
+ .split('\n')
49
+ .map(line => line.replace(/^\s*\*\s?/, ''));
50
+
51
+ const result = {
52
+ description: '',
53
+ tags: []
54
+ };
55
+
56
+ let currentTag = null;
57
+ let descriptionLines = [];
58
+
59
+ for (const line of lines) {
60
+ const tagMatch = line.match(/^@(\w+)\s*(.*)?$/);
61
+
62
+ if (tagMatch) {
63
+ // Save previous tag
64
+ if (currentTag) {
65
+ result.tags.push(currentTag);
66
+ }
67
+
68
+ // Start new tag
69
+ const [, tagName, tagContent] = tagMatch;
70
+ currentTag = {
71
+ tag: tagName,
72
+ content: tagContent || ''
73
+ };
74
+
75
+ // Parse specific tag formats
76
+ if (['param', 'parameter', 'arg', 'argument'].includes(tagName)) {
77
+ const paramMatch = tagContent?.match(/^\{([^}]+)\}\s*(\[?[\w.]+\]?)\s*-?\s*(.*)$/);
78
+ if (paramMatch) {
79
+ const [, type, name, desc] = paramMatch;
80
+ const optional = name.startsWith('[') && name.endsWith(']');
81
+ currentTag.type = type;
82
+ currentTag.name = name.replace(/^\[|\]$/g, '').split('=')[0];
83
+ currentTag.description = desc;
84
+ currentTag.optional = optional;
85
+ if (name.includes('=')) {
86
+ currentTag.default = name.split('=')[1].replace(/\]$/, '');
87
+ }
88
+ }
89
+ } else if (['returns', 'return'].includes(tagName)) {
90
+ const returnMatch = tagContent?.match(/^\{([^}]+)\}\s*(.*)$/);
91
+ if (returnMatch) {
92
+ const [, type, desc] = returnMatch;
93
+ currentTag.type = type;
94
+ currentTag.description = desc;
95
+ }
96
+ } else if (['type', 'typedef'].includes(tagName)) {
97
+ const typeMatch = tagContent?.match(/^\{([^}]+)\}\s*(.*)$/);
98
+ if (typeMatch) {
99
+ currentTag.type = typeMatch[1];
100
+ currentTag.name = typeMatch[2];
101
+ }
102
+ } else if (['property', 'prop'].includes(tagName)) {
103
+ const propMatch = tagContent?.match(/^\{([^}]+)\}\s*([\w.]+)\s*-?\s*(.*)$/);
104
+ if (propMatch) {
105
+ const [, type, name, desc] = propMatch;
106
+ currentTag.type = type;
107
+ currentTag.name = name;
108
+ currentTag.description = desc;
109
+ }
110
+ }
111
+ } else if (currentTag) {
112
+ // Continue current tag
113
+ currentTag.content += '\n' + line;
114
+ if (currentTag.description !== undefined) {
115
+ currentTag.description += '\n' + line;
116
+ }
117
+ } else {
118
+ // Description line
119
+ descriptionLines.push(line);
120
+ }
121
+ }
122
+
123
+ // Save last tag
124
+ if (currentTag) {
125
+ result.tags.push(currentTag);
126
+ }
127
+
128
+ // Clean up description
129
+ result.description = descriptionLines.join('\n').trim();
130
+
131
+ return result;
132
+ }
133
+
134
+ /**
135
+ * Extract JSDoc comments from source code
136
+ * @param {string} source - Source code
137
+ * @returns {Array} Array of JSDoc entries
138
+ */
139
+ function extractJSDocEntries(source) {
140
+ const entries = [];
141
+ const jsdocRegex = /\/\*\*[\s\S]*?\*\/\s*(?:export\s+)?(?:(async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=|class\s+(\w+)|(\w+)\s*[:(])/g;
142
+
143
+ let match;
144
+ while ((match = jsdocRegex.exec(source)) !== null) {
145
+ const fullMatch = match[0];
146
+ const commentEnd = fullMatch.indexOf('*/') + 2;
147
+ const comment = fullMatch.slice(0, commentEnd);
148
+ const afterComment = fullMatch.slice(commentEnd).trim();
149
+
150
+ // Determine the name and type of the documented item
151
+ let name = match[2] || match[3] || match[4] || match[5];
152
+ let kind = 'unknown';
153
+
154
+ if (match[2]) {
155
+ kind = match[1] ? 'async function' : 'function';
156
+ } else if (match[4]) {
157
+ kind = 'class';
158
+ } else if (match[3]) {
159
+ // Check what's assigned
160
+ if (afterComment.includes('function') || afterComment.includes('=>')) {
161
+ kind = 'function';
162
+ } else if (afterComment.includes('class')) {
163
+ kind = 'class';
164
+ } else {
165
+ kind = 'const';
166
+ }
167
+ }
168
+
169
+ const parsed = parseJSDocComment(comment);
170
+
171
+ entries.push({
172
+ name,
173
+ kind,
174
+ ...parsed,
175
+ line: source.slice(0, match.index).split('\n').length
176
+ });
177
+ }
178
+
179
+ return entries;
180
+ }
181
+
182
+ /**
183
+ * Process a JavaScript/TypeScript file
184
+ * @param {string} filePath - Path to file
185
+ * @returns {Object} Processed file data
186
+ */
187
+ function processFile(filePath) {
188
+ const source = readFileSync(filePath, 'utf-8');
189
+ const entries = extractJSDocEntries(source);
190
+
191
+ // Find module-level JSDoc
192
+ const moduleDocMatch = source.match(/^\/\*\*[\s\S]*?\*\/\s*\n(?!\s*(export|function|class|const|let|var))/);
193
+ let moduleDoc = null;
194
+
195
+ if (moduleDocMatch) {
196
+ moduleDoc = parseJSDocComment(moduleDocMatch[0]);
197
+ }
198
+
199
+ return {
200
+ path: filePath,
201
+ name: basename(filePath, extname(filePath)),
202
+ moduleDoc,
203
+ entries
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Find JavaScript files in directory
209
+ * @param {string} dir - Directory to search
210
+ * @param {Object} options - Search options
211
+ * @returns {string[]} Array of file paths
212
+ */
213
+ function findJsFiles(dir, options = {}) {
214
+ const { extensions = ['.js', '.mjs', '.ts'], exclude = ['node_modules', '.git', 'dist', 'coverage'] } = options;
215
+ const files = [];
216
+
217
+ function walk(currentDir) {
218
+ try {
219
+ const entries = readdirSync(currentDir);
220
+ for (const entry of entries) {
221
+ if (exclude.includes(entry) || entry.startsWith('.')) continue;
222
+
223
+ const fullPath = join(currentDir, entry);
224
+ try {
225
+ const stat = statSync(fullPath);
226
+ if (stat.isDirectory()) {
227
+ walk(fullPath);
228
+ } else if (extensions.some(ext => entry.endsWith(ext))) {
229
+ files.push(fullPath);
230
+ }
231
+ } catch (e) {
232
+ // Skip inaccessible files
233
+ }
234
+ }
235
+ } catch (e) {
236
+ // Skip inaccessible directories
237
+ }
238
+ }
239
+
240
+ walk(dir);
241
+ return files;
242
+ }
243
+
244
+ /**
245
+ * Generate Markdown documentation
246
+ * @param {Object} fileData - Processed file data
247
+ * @param {Object} options - Generation options
248
+ * @returns {string} Markdown content
249
+ */
250
+ function generateMarkdown(fileData, options = {}) {
251
+ const { includeSource = false } = options;
252
+ let md = '';
253
+
254
+ // Module header
255
+ md += `# ${fileData.name}\n\n`;
256
+
257
+ if (fileData.moduleDoc) {
258
+ md += `${fileData.moduleDoc.description}\n\n`;
259
+ }
260
+
261
+ md += `**File:** \`${relative(process.cwd(), fileData.path)}\`\n\n`;
262
+
263
+ // Table of contents
264
+ if (fileData.entries.length > 0) {
265
+ md += `## Table of Contents\n\n`;
266
+ for (const entry of fileData.entries) {
267
+ const anchor = entry.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
268
+ md += `- [${entry.name}](#${anchor})\n`;
269
+ }
270
+ md += '\n---\n\n';
271
+ }
272
+
273
+ // Document each entry
274
+ for (const entry of fileData.entries) {
275
+ md += `## ${entry.name}\n\n`;
276
+
277
+ // Kind badge
278
+ const kindBadge = `\`${entry.kind}\``;
279
+ md += `${kindBadge}\n\n`;
280
+
281
+ // Description
282
+ if (entry.description) {
283
+ md += `${entry.description}\n\n`;
284
+ }
285
+
286
+ // Parameters
287
+ const params = entry.tags.filter(t => ['param', 'parameter', 'arg', 'argument'].includes(t.tag));
288
+ if (params.length > 0) {
289
+ md += `### Parameters\n\n`;
290
+ md += `| Name | Type | Description |\n`;
291
+ md += `|------|------|-------------|\n`;
292
+ for (const param of params) {
293
+ const optional = param.optional ? ' (optional)' : '';
294
+ const defaultVal = param.default ? ` = \`${param.default}\`` : '';
295
+ md += `| \`${param.name}\`${optional}${defaultVal} | \`${param.type || 'any'}\` | ${(param.description || '').replace(/\n/g, ' ')} |\n`;
296
+ }
297
+ md += '\n';
298
+ }
299
+
300
+ // Returns
301
+ const returns = entry.tags.find(t => ['returns', 'return'].includes(t.tag));
302
+ if (returns) {
303
+ md += `### Returns\n\n`;
304
+ md += `\`${returns.type || 'void'}\``;
305
+ if (returns.description) {
306
+ md += ` - ${returns.description.replace(/\n/g, ' ')}`;
307
+ }
308
+ md += '\n\n';
309
+ }
310
+
311
+ // Throws
312
+ const throws = entry.tags.filter(t => ['throws', 'exception'].includes(t.tag));
313
+ if (throws.length > 0) {
314
+ md += `### Throws\n\n`;
315
+ for (const t of throws) {
316
+ md += `- ${t.content}\n`;
317
+ }
318
+ md += '\n';
319
+ }
320
+
321
+ // Examples
322
+ const examples = entry.tags.filter(t => t.tag === 'example');
323
+ if (examples.length > 0) {
324
+ md += `### Example${examples.length > 1 ? 's' : ''}\n\n`;
325
+ for (const example of examples) {
326
+ md += `\`\`\`javascript\n${example.content.trim()}\n\`\`\`\n\n`;
327
+ }
328
+ }
329
+
330
+ // Deprecated
331
+ const deprecated = entry.tags.find(t => t.tag === 'deprecated');
332
+ if (deprecated) {
333
+ md += `> **Deprecated:** ${deprecated.content || 'This is deprecated.'}\n\n`;
334
+ }
335
+
336
+ // See also
337
+ const seeAlso = entry.tags.filter(t => ['see', 'link'].includes(t.tag));
338
+ if (seeAlso.length > 0) {
339
+ md += `### See Also\n\n`;
340
+ for (const see of seeAlso) {
341
+ md += `- ${see.content}\n`;
342
+ }
343
+ md += '\n';
344
+ }
345
+
346
+ md += '---\n\n';
347
+ }
348
+
349
+ return md;
350
+ }
351
+
352
+ /**
353
+ * Generate JSON documentation
354
+ * @param {Object[]} filesData - Array of processed file data
355
+ * @returns {Object} JSON documentation
356
+ */
357
+ function generateJson(filesData) {
358
+ return {
359
+ generated: new Date().toISOString(),
360
+ version: '1.0.0',
361
+ modules: filesData.map(f => ({
362
+ name: f.name,
363
+ path: relative(process.cwd(), f.path),
364
+ description: f.moduleDoc?.description || null,
365
+ exports: f.entries.map(e => ({
366
+ name: e.name,
367
+ kind: e.kind,
368
+ description: e.description,
369
+ line: e.line,
370
+ params: e.tags
371
+ .filter(t => ['param', 'parameter'].includes(t.tag))
372
+ .map(p => ({
373
+ name: p.name,
374
+ type: p.type,
375
+ description: p.description,
376
+ optional: p.optional || false,
377
+ default: p.default || null
378
+ })),
379
+ returns: (() => {
380
+ const ret = e.tags.find(t => ['returns', 'return'].includes(t.tag));
381
+ return ret ? { type: ret.type, description: ret.description } : null;
382
+ })(),
383
+ examples: e.tags
384
+ .filter(t => t.tag === 'example')
385
+ .map(t => t.content.trim()),
386
+ deprecated: e.tags.find(t => t.tag === 'deprecated')?.content || null,
387
+ since: e.tags.find(t => t.tag === 'since')?.content || null
388
+ }))
389
+ }))
390
+ };
391
+ }
392
+
393
+ /**
394
+ * Generate HTML documentation
395
+ * @param {Object[]} filesData - Array of processed file data
396
+ * @param {Object} options - Generation options
397
+ * @returns {string} HTML content
398
+ */
399
+ function generateHtml(filesData, options = {}) {
400
+ const { title = 'API Documentation' } = options;
401
+
402
+ let html = `<!DOCTYPE html>
403
+ <html lang="en">
404
+ <head>
405
+ <meta charset="UTF-8">
406
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
407
+ <title>${title}</title>
408
+ <style>
409
+ :root {
410
+ --bg: #ffffff;
411
+ --text: #1a1a1a;
412
+ --code-bg: #f5f5f5;
413
+ --border: #e0e0e0;
414
+ --accent: #646cff;
415
+ --link: #646cff;
416
+ }
417
+ @media (prefers-color-scheme: dark) {
418
+ :root {
419
+ --bg: #1a1a1a;
420
+ --text: #ffffff;
421
+ --code-bg: #2d2d2d;
422
+ --border: #404040;
423
+ }
424
+ }
425
+ * { box-sizing: border-box; margin: 0; padding: 0; }
426
+ body {
427
+ font-family: system-ui, -apple-system, sans-serif;
428
+ background: var(--bg);
429
+ color: var(--text);
430
+ line-height: 1.6;
431
+ }
432
+ .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
433
+ h1 { font-size: 2.5rem; margin-bottom: 1rem; color: var(--accent); }
434
+ h2 { font-size: 1.8rem; margin: 2rem 0 1rem; border-bottom: 2px solid var(--border); padding-bottom: 0.5rem; }
435
+ h3 { font-size: 1.3rem; margin: 1.5rem 0 0.5rem; }
436
+ h4 { font-size: 1.1rem; margin: 1rem 0 0.5rem; color: var(--accent); }
437
+ p { margin: 0.5rem 0; }
438
+ code { background: var(--code-bg); padding: 0.2em 0.4em; border-radius: 4px; font-size: 0.9em; }
439
+ pre { background: var(--code-bg); padding: 1rem; border-radius: 8px; overflow-x: auto; margin: 1rem 0; }
440
+ pre code { padding: 0; background: none; }
441
+ table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
442
+ th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid var(--border); }
443
+ th { background: var(--code-bg); font-weight: 600; }
444
+ .badge { display: inline-block; padding: 0.2em 0.6em; border-radius: 4px; background: var(--accent); color: white; font-size: 0.8em; margin-left: 0.5rem; }
445
+ .deprecated { background: #dc3545; }
446
+ .nav { position: fixed; left: 0; top: 0; width: 250px; height: 100vh; background: var(--code-bg); padding: 1rem; overflow-y: auto; border-right: 1px solid var(--border); }
447
+ .nav h3 { margin-bottom: 0.5rem; }
448
+ .nav ul { list-style: none; }
449
+ .nav li { margin: 0.25rem 0; }
450
+ .nav a { color: var(--link); text-decoration: none; }
451
+ .nav a:hover { text-decoration: underline; }
452
+ .main { margin-left: 270px; }
453
+ @media (max-width: 768px) { .nav { display: none; } .main { margin-left: 0; } }
454
+ .entry { margin-bottom: 3rem; }
455
+ </style>
456
+ </head>
457
+ <body>
458
+ <nav class="nav">
459
+ <h3>Modules</h3>
460
+ <ul>
461
+ `;
462
+
463
+ // Navigation
464
+ for (const file of filesData) {
465
+ html += ` <li><strong>${file.name}</strong>\n <ul>\n`;
466
+ for (const entry of file.entries) {
467
+ const anchor = `${file.name}-${entry.name}`.toLowerCase().replace(/[^a-z0-9]+/g, '-');
468
+ html += ` <li><a href="#${anchor}">${entry.name}</a></li>\n`;
469
+ }
470
+ html += ` </ul>\n </li>\n`;
471
+ }
472
+
473
+ html += ` </ul>
474
+ </nav>
475
+ <main class="main">
476
+ <div class="container">
477
+ <h1>${title}</h1>
478
+ <p>Generated: ${new Date().toLocaleString()}</p>
479
+ `;
480
+
481
+ // Content
482
+ for (const file of filesData) {
483
+ html += ` <section>
484
+ <h2 id="${file.name.toLowerCase()}">${file.name}</h2>
485
+ <p><code>${relative(process.cwd(), file.path)}</code></p>
486
+ `;
487
+
488
+ if (file.moduleDoc?.description) {
489
+ html += ` <p>${escapeHtml(file.moduleDoc.description)}</p>\n`;
490
+ }
491
+
492
+ for (const entry of file.entries) {
493
+ const anchor = `${file.name}-${entry.name}`.toLowerCase().replace(/[^a-z0-9]+/g, '-');
494
+ const deprecated = entry.tags.find(t => t.tag === 'deprecated');
495
+
496
+ html += ` <div class="entry" id="${anchor}">
497
+ <h3>${entry.name}<span class="badge">${entry.kind}</span>${deprecated ? '<span class="badge deprecated">deprecated</span>' : ''}</h3>
498
+ `;
499
+
500
+ if (entry.description) {
501
+ html += ` <p>${escapeHtml(entry.description)}</p>\n`;
502
+ }
503
+
504
+ // Parameters
505
+ const params = entry.tags.filter(t => ['param', 'parameter'].includes(t.tag));
506
+ if (params.length > 0) {
507
+ html += ` <h4>Parameters</h4>
508
+ <table>
509
+ <tr><th>Name</th><th>Type</th><th>Description</th></tr>
510
+ `;
511
+ for (const p of params) {
512
+ const opt = p.optional ? ' (optional)' : '';
513
+ const def = p.default ? ` = <code>${escapeHtml(p.default)}</code>` : '';
514
+ html += ` <tr><td><code>${p.name}</code>${opt}${def}</td><td><code>${escapeHtml(p.type || 'any')}</code></td><td>${escapeHtml(p.description || '')}</td></tr>\n`;
515
+ }
516
+ html += ` </table>\n`;
517
+ }
518
+
519
+ // Returns
520
+ const returns = entry.tags.find(t => ['returns', 'return'].includes(t.tag));
521
+ if (returns) {
522
+ html += ` <h4>Returns</h4>
523
+ <p><code>${escapeHtml(returns.type || 'void')}</code>${returns.description ? ` - ${escapeHtml(returns.description)}` : ''}</p>\n`;
524
+ }
525
+
526
+ // Examples
527
+ const examples = entry.tags.filter(t => t.tag === 'example');
528
+ if (examples.length > 0) {
529
+ html += ` <h4>Example${examples.length > 1 ? 's' : ''}</h4>\n`;
530
+ for (const ex of examples) {
531
+ html += ` <pre><code>${escapeHtml(ex.content.trim())}</code></pre>\n`;
532
+ }
533
+ }
534
+
535
+ html += ` </div>\n`;
536
+ }
537
+
538
+ html += ` </section>\n`;
539
+ }
540
+
541
+ html += ` </div>
542
+ </main>
543
+ </body>
544
+ </html>`;
545
+
546
+ return html;
547
+ }
548
+
549
+ /**
550
+ * Escape HTML special characters
551
+ */
552
+ function escapeHtml(str) {
553
+ if (!str) return '';
554
+ return str
555
+ .replace(/&/g, '&amp;')
556
+ .replace(/</g, '&lt;')
557
+ .replace(/>/g, '&gt;')
558
+ .replace(/"/g, '&quot;');
559
+ }
560
+
561
+ /**
562
+ * Main docs command handler
563
+ */
564
+ export async function runDocs(args) {
565
+ const { options, patterns } = parseArgs(args);
566
+
567
+ const generate = options.generate || options.g || false;
568
+ const format = options.format || options.f || 'markdown';
569
+ const output = options.output || options.o || 'docs/api';
570
+ const title = options.title || 'API Documentation';
571
+ const verbose = options.verbose || options.v || false;
572
+
573
+ // If not generating, show help
574
+ if (!generate) {
575
+ showDocsHelp();
576
+ return;
577
+ }
578
+
579
+ const timer = createTimer();
580
+
581
+ // Find source files
582
+ const inputPaths = patterns.length > 0 ? patterns : ['src', 'runtime'];
583
+ const allFiles = [];
584
+
585
+ for (const inputPath of inputPaths) {
586
+ const fullPath = join(process.cwd(), inputPath);
587
+ if (!existsSync(fullPath)) {
588
+ log.warn(`Path not found: ${inputPath}`);
589
+ continue;
590
+ }
591
+
592
+ const stat = statSync(fullPath);
593
+ if (stat.isDirectory()) {
594
+ allFiles.push(...findJsFiles(fullPath));
595
+ } else if (stat.isFile()) {
596
+ allFiles.push(fullPath);
597
+ }
598
+ }
599
+
600
+ if (allFiles.length === 0) {
601
+ log.error('No JavaScript files found to document.');
602
+ return;
603
+ }
604
+
605
+ log.info(`Processing ${allFiles.length} file(s)...\n`);
606
+
607
+ // Process files
608
+ const filesData = [];
609
+ let totalEntries = 0;
610
+
611
+ for (const file of allFiles) {
612
+ const data = processFile(file);
613
+ if (data.entries.length > 0) {
614
+ filesData.push(data);
615
+ totalEntries += data.entries.length;
616
+
617
+ if (verbose) {
618
+ log.info(` ${relative(process.cwd(), file)}: ${data.entries.length} entries`);
619
+ }
620
+ }
621
+ }
622
+
623
+ if (filesData.length === 0) {
624
+ log.warn('No documented exports found.');
625
+ return;
626
+ }
627
+
628
+ // Create output directory
629
+ const outputPath = join(process.cwd(), output);
630
+ if (!existsSync(outputPath)) {
631
+ mkdirSync(outputPath, { recursive: true });
632
+ }
633
+
634
+ // Generate documentation
635
+ if (format === 'json') {
636
+ const json = generateJson(filesData);
637
+ const jsonPath = join(outputPath, 'api.json');
638
+ writeFileSync(jsonPath, JSON.stringify(json, null, 2));
639
+ log.success(`Generated: ${relative(process.cwd(), jsonPath)}`);
640
+ } else if (format === 'html') {
641
+ const html = generateHtml(filesData, { title });
642
+ const htmlPath = join(outputPath, 'index.html');
643
+ writeFileSync(htmlPath, html);
644
+ log.success(`Generated: ${relative(process.cwd(), htmlPath)}`);
645
+ } else {
646
+ // Default: Markdown (one file per module)
647
+ for (const fileData of filesData) {
648
+ const md = generateMarkdown(fileData, options);
649
+ const mdPath = join(outputPath, `${fileData.name}.md`);
650
+ writeFileSync(mdPath, md);
651
+ log.success(`Generated: ${relative(process.cwd(), mdPath)}`);
652
+ }
653
+
654
+ // Generate index
655
+ let indexMd = `# API Documentation\n\n`;
656
+ indexMd += `Generated: ${new Date().toLocaleString()}\n\n`;
657
+ indexMd += `## Modules\n\n`;
658
+
659
+ for (const fileData of filesData) {
660
+ indexMd += `- [${fileData.name}](./${fileData.name}.md)`;
661
+ if (fileData.moduleDoc?.description) {
662
+ const shortDesc = fileData.moduleDoc.description.split('\n')[0].slice(0, 80);
663
+ indexMd += ` - ${shortDesc}`;
664
+ }
665
+ indexMd += '\n';
666
+ }
667
+
668
+ writeFileSync(join(outputPath, 'README.md'), indexMd);
669
+ }
670
+
671
+ const elapsed = formatDuration(timer.elapsed());
672
+ log.info(`\nDocumented ${totalEntries} exports from ${filesData.length} module(s) in ${elapsed}`);
673
+ }
674
+
675
+ /**
676
+ * Show docs help
677
+ */
678
+ function showDocsHelp() {
679
+ log.info(`
680
+ Pulse Docs - Generate API documentation from JSDoc
681
+
682
+ Usage: pulse docs --generate [paths...] [options]
683
+
684
+ Options:
685
+ --generate, -g Generate documentation (required)
686
+ --format, -f Output format: markdown, json, html (default: markdown)
687
+ --output, -o Output directory (default: docs/api)
688
+ --title Documentation title (for HTML)
689
+ --verbose, -v Show detailed output
690
+
691
+ Examples:
692
+ pulse docs --generate # Document src/ and runtime/
693
+ pulse docs --generate src/ # Document specific directory
694
+ pulse docs --generate --format html # Generate HTML docs
695
+ pulse docs --generate --format json # Generate JSON docs
696
+ pulse docs -g -f html -o docs/ # HTML docs to docs/
697
+
698
+ Output:
699
+ markdown -> One .md file per module + README.md index
700
+ html -> Single index.html with navigation
701
+ json -> Single api.json with structured data
702
+
703
+ JSDoc Tags Supported:
704
+ @param {type} name - description
705
+ @returns {type} description
706
+ @example
707
+ @throws {Error}
708
+ @deprecated
709
+ @see reference
710
+ @since version
711
+ `);
712
+ }