@atlashub/smartstack-cli 3.36.0 → 3.37.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,811 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate Documentation with Mock UI for SmartStack Modules
4
+ *
5
+ * This script generates complete documentation pages with embedded mock UI components
6
+ * that visually represent the module's interface without requiring real screenshots.
7
+ *
8
+ * Usage:
9
+ * node generate-doc-with-mock-ui.ts \
10
+ * --module users \
11
+ * --context platform \
12
+ * --application administration \
13
+ * --app-path "D:/01 - projets/SmartStack.app/02-Develop"
14
+ *
15
+ * Output: Complete TSX file with mock UI (e.g., UsersDocPage.tsx)
16
+ */
17
+
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ import { glob } from 'glob';
21
+ import { execSync } from 'child_process';
22
+
23
+ interface ModuleInfo {
24
+ module: string; // e.g., "users"
25
+ context: string; // e.g., "platform"
26
+ application: string; // e.g., "administration"
27
+ appPath: string;
28
+ }
29
+
30
+ interface EntityProperty {
31
+ name: string; // e.g., "Email", "FirstName"
32
+ type: string; // e.g., "string", "bool", "DateTime?"
33
+ isRequired: boolean;
34
+ isNullable: boolean;
35
+ }
36
+
37
+ interface MockDataField {
38
+ propertyName: string;
39
+ mockValue: string; // Generated mock value
40
+ }
41
+
42
+ interface PageStructure {
43
+ hasTable: boolean;
44
+ hasKPIs: boolean;
45
+ hasForm: boolean;
46
+ hasFilters: boolean;
47
+ columns: string[]; // Table column names
48
+ }
49
+
50
+ /**
51
+ * Parse command line arguments
52
+ */
53
+ function parseArgs(): ModuleInfo {
54
+ const args = process.argv.slice(2);
55
+
56
+ const getArg = (flag: string): string | null => {
57
+ const index = args.indexOf(flag);
58
+ return index !== -1 && index + 1 < args.length ? args[index + 1] : null;
59
+ };
60
+
61
+ const module = getArg('--module');
62
+ const context = getArg('--context');
63
+ const application = getArg('--application');
64
+ const appPath = getArg('--app-path');
65
+
66
+ if (!module || !context || !application || !appPath) {
67
+ console.error('Usage: node generate-doc-with-mock-ui.ts --module <name> --context <ctx> --application <app> --app-path <path>');
68
+ process.exit(1);
69
+ }
70
+
71
+ return { module, context, application, appPath };
72
+ }
73
+
74
+ /**
75
+ * Find Domain entity file
76
+ * Tries both singular and plural forms (e.g., users → User.cs)
77
+ */
78
+ async function findEntityFile(appPath: string, module: string): Promise<string | null> {
79
+ const domainPath = path.join(appPath, 'src/SmartStack.Domain');
80
+
81
+ // Try singular form first (most common: users → User.cs)
82
+ const singularForm = capitalize(singularize(module));
83
+ const singularPattern = `**/${singularForm}.cs`;
84
+
85
+ try {
86
+ let files = await glob(singularPattern, { cwd: domainPath, absolute: true });
87
+ if (files.length > 0) {
88
+ return files[0];
89
+ }
90
+
91
+ // Fallback to plural form
92
+ const pluralForm = capitalize(module);
93
+ const pluralPattern = `**/${pluralForm}.cs`;
94
+ files = await glob(pluralPattern, { cwd: domainPath, absolute: true });
95
+ return files.length > 0 ? files[0] : null;
96
+ } catch (error) {
97
+ console.error('Error finding entity:', error);
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Find React page file
104
+ */
105
+ async function findPageFile(appPath: string, module: string, context: string, application: string): Promise<string | null> {
106
+ const pagesPath = path.join(appPath, `web/smartstack-web/src/pages/${context}/${application}/${module}`);
107
+ const pattern = `${capitalize(module)}Page.tsx`;
108
+
109
+ const fullPath = path.join(pagesPath, pattern);
110
+ return fs.existsSync(fullPath) ? fullPath : null;
111
+ }
112
+
113
+ /**
114
+ * Extract entity properties from Domain entity file
115
+ */
116
+ function extractEntityProperties(entityPath: string): EntityProperty[] {
117
+ const content = fs.readFileSync(entityPath, 'utf-8');
118
+ const lines = content.split('\n');
119
+ const properties: EntityProperty[] = [];
120
+
121
+ for (const line of lines) {
122
+ // Match property declarations: public string Email { get; private set; }
123
+ const propMatch = line.match(/public\s+(\w+\??)\s+(\w+)\s*\{\s*get;/);
124
+ if (propMatch) {
125
+ const type = propMatch[1];
126
+ const name = propMatch[2];
127
+
128
+ // Skip base entity properties and navigation properties
129
+ if (['Id', 'CreatedAt', 'UpdatedAt', 'CreatedBy', 'UpdatedBy'].includes(name)) {
130
+ continue;
131
+ }
132
+
133
+ // Skip collections (List, IReadOnlyCollection, etc.)
134
+ if (line.includes('List<') || line.includes('IReadOnlyCollection<') || line.includes('ICollection<')) {
135
+ continue;
136
+ }
137
+
138
+ properties.push({
139
+ name,
140
+ type,
141
+ isRequired: !type.endsWith('?'),
142
+ isNullable: type.endsWith('?'),
143
+ });
144
+ }
145
+ }
146
+
147
+ return properties;
148
+ }
149
+
150
+ /**
151
+ * Analyze React page structure
152
+ */
153
+ function analyzePageStructure(pagePath: string): PageStructure {
154
+ const content = fs.readFileSync(pagePath, 'utf-8');
155
+
156
+ const structure: PageStructure = {
157
+ hasTable: content.includes('<table') || content.includes('Table'),
158
+ hasKPIs: content.includes('KPI') || content.includes('stat') || content.includes('metric'),
159
+ hasForm: content.includes('<form') || content.includes('Form') || content.includes('input'),
160
+ hasFilters: content.includes('filter') || content.includes('Filter') || content.includes('search'),
161
+ columns: [],
162
+ };
163
+
164
+ // Try to extract table column names
165
+ const thRegex = /<th[^>]*>([^<]+)<\/th>/g;
166
+ let match;
167
+ while ((match = thRegex.exec(content)) !== null) {
168
+ structure.columns.push(match[1].trim());
169
+ }
170
+
171
+ return structure;
172
+ }
173
+
174
+ /**
175
+ * Generate mock value for a property
176
+ */
177
+ function generateMockValue(property: EntityProperty, index: number): string {
178
+ const type = property.type.replace('?', '');
179
+
180
+ switch (type.toLowerCase()) {
181
+ case 'string':
182
+ if (property.name.toLowerCase().includes('email')) {
183
+ const names = ['john.doe', 'jane.smith', 'bob.johnson', 'alice.williams', 'charlie.brown'];
184
+ return `"${names[index % names.length]}@example.com"`;
185
+ }
186
+ if (property.name.toLowerCase().includes('name')) {
187
+ if (property.name.toLowerCase().includes('first')) {
188
+ const names = ['John', 'Jane', 'Bob', 'Alice', 'Charlie'];
189
+ return `"${names[index % names.length]}"`;
190
+ }
191
+ if (property.name.toLowerCase().includes('last')) {
192
+ const names = ['Doe', 'Smith', 'Johnson', 'Williams', 'Brown'];
193
+ return `"${names[index % names.length]}"`;
194
+ }
195
+ return `"${property.name} ${index + 1}"`;
196
+ }
197
+ if (property.name.toLowerCase().includes('title')) {
198
+ return `"Sample ${property.name} ${index + 1}"`;
199
+ }
200
+ if (property.name.toLowerCase().includes('description')) {
201
+ return `"This is a sample description for item ${index + 1}"`;
202
+ }
203
+ return `"Sample ${property.name}"`;
204
+
205
+ case 'bool':
206
+ case 'boolean':
207
+ return index % 2 === 0 ? 'true' : 'false';
208
+
209
+ case 'int':
210
+ case 'int32':
211
+ case 'long':
212
+ return String((index + 1) * 10);
213
+
214
+ case 'decimal':
215
+ case 'double':
216
+ case 'float':
217
+ return String((index + 1) * 10.5);
218
+
219
+ case 'datetime':
220
+ const dates = ['2026-01-15', '2026-01-20', '2026-02-01', '2026-02-10', '2026-02-15'];
221
+ return `"${dates[index % dates.length]}"`;
222
+
223
+ case 'guid':
224
+ // Generate a fixed mock GUID based on index to keep consistent data
225
+ const mockGuids = [
226
+ '"550e8400-e29b-41d4-a716-446655440000"',
227
+ '"6ba7b810-9dad-11d1-80b4-00c04fd430c8"',
228
+ '"6ba7b811-9dad-11d1-80b4-00c04fd430c9"',
229
+ '"6ba7b812-9dad-11d1-80b4-00c04fd430ca"',
230
+ '"6ba7b813-9dad-11d1-80b4-00c04fd430cb"'
231
+ ];
232
+ return mockGuids[index % mockGuids.length];
233
+
234
+ default:
235
+ return property.isNullable ? 'null' : '""';
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Generate mock data array
241
+ */
242
+ function generateMockData(properties: EntityProperty[], module: string, count: number = 5): string {
243
+ const mockItems: string[] = [];
244
+
245
+ for (let i = 0; i < count; i++) {
246
+ const fields = properties
247
+ .filter(p => !p.name.includes('Hash') && !p.name.includes('Token')) // Skip sensitive fields
248
+ .map(p => `${p.name.charAt(0).toLowerCase() + p.name.slice(1)}: ${generateMockValue(p, i)}`)
249
+ .join(', ');
250
+
251
+ mockItems.push(` { ${fields} }`);
252
+ }
253
+
254
+ const singularName = module.endsWith('s') ? module.slice(0, -1) : module;
255
+ return `const mock${capitalize(module)} = [\n${mockItems.join(',\n')}\n];`;
256
+ }
257
+
258
+ /**
259
+ * Generate KPI stats based on entity properties
260
+ */
261
+ function generateKPIStats(properties: EntityProperty[], module: string, count: number = 5): string {
262
+ const stats: string[] = [];
263
+
264
+ // Always add total count
265
+ stats.push(` { label: 'Total', value: '${count * 10}', color: 'var(--text-primary)' }`);
266
+
267
+ // Check for IsActive/Status property
268
+ const activeProperty = properties.find(p =>
269
+ p.name.toLowerCase() === 'isactive' && (p.type.toLowerCase() === 'bool' || p.type.toLowerCase() === 'boolean')
270
+ );
271
+
272
+ if (activeProperty) {
273
+ const activeCount = Math.floor(count * 10 * 0.6); // 60% active
274
+ const inactiveCount = count * 10 - activeCount;
275
+ stats.push(` { label: 'Active', value: '${activeCount}', color: 'var(--success-text)', border: 'var(--success-border)' }`);
276
+ stats.push(` { label: 'Inactive', value: '${inactiveCount}', color: 'var(--error-text)', border: 'var(--error-border)' }`);
277
+ }
278
+
279
+ // Check for Status property (enum/string)
280
+ const statusProperty = properties.find(p =>
281
+ p.name.toLowerCase() === 'status' && p.type.toLowerCase() === 'string'
282
+ );
283
+
284
+ if (statusProperty) {
285
+ stats.push(` { label: 'Pending', value: '${Math.floor(count * 2)}', color: 'var(--warning-text)', border: 'var(--warning-border)' }`);
286
+ stats.push(` { label: 'In Progress', value: '${Math.floor(count * 3)}', color: 'var(--info-text)', border: 'var(--info-border)' }`);
287
+ stats.push(` { label: 'Completed', value: '${Math.floor(count * 5)}', color: 'var(--success-text)', border: 'var(--success-border)' }`);
288
+ }
289
+
290
+ // Check for DateTime property (e.g., CreatedAt, LastLoginAt)
291
+ const dateProperty = properties.find(p =>
292
+ p.type.toLowerCase() === 'datetime' &&
293
+ (p.name.toLowerCase().includes('created') || p.name.toLowerCase().includes('login'))
294
+ );
295
+
296
+ if (dateProperty) {
297
+ stats.push(` { label: 'Last 7 days', value: '${Math.floor(count * 1.5)}', color: 'var(--info-text)', border: 'var(--info-border)' }`);
298
+ }
299
+
300
+ return `const ${module}Stats = [\n${stats.join(',\n')}\n];`;
301
+ }
302
+
303
+ /**
304
+ * Generate form fields based on entity properties
305
+ */
306
+ function generateFormSection(properties: EntityProperty[], module: string): string {
307
+ const ModuleName = capitalize(singularize(module));
308
+
309
+ // Filter properties suitable for form input
310
+ const formProps = properties.filter(p => {
311
+ const name = p.name.toLowerCase();
312
+ // Exclude read-only, computed, and sensitive fields
313
+ return !name.includes('hash') &&
314
+ !name.includes('token') &&
315
+ !name.includes('normalized') &&
316
+ !name.endsWith('id') &&
317
+ !name.includes('createdat') &&
318
+ !name.includes('updatedat') &&
319
+ !name.includes('createdby') &&
320
+ !name.includes('updatedby') &&
321
+ !name.includes('data') &&
322
+ !name.includes('proxy') &&
323
+ p.name !== 'Id';
324
+ }).slice(0, 6); // Limit to 6 most relevant fields
325
+
326
+ const formFields = formProps.map(p => {
327
+ const fieldName = p.name.charAt(0).toLowerCase() + p.name.slice(1);
328
+ const label = p.name.replace(/([A-Z])/g, ' $1').trim();
329
+ const isRequired = p.isRequired;
330
+
331
+ if (p.type.toLowerCase() === 'bool' || p.type.toLowerCase() === 'boolean') {
332
+ return ` <div className="flex items-center gap-2">
333
+ <input type="checkbox" id="${fieldName}" className="w-4 h-4" />
334
+ <label htmlFor="${fieldName}" className="text-sm">${label}</label>
335
+ </div>`;
336
+ }
337
+
338
+ if (p.type.toLowerCase() === 'datetime') {
339
+ return ` <div>
340
+ <label className="block text-sm font-medium mb-1">${label}${isRequired ? ' *' : ''}</label>
341
+ <input type="date" className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg" />
342
+ </div>`;
343
+ }
344
+
345
+ if (p.name.toLowerCase().includes('email')) {
346
+ return ` <div>
347
+ <label className="block text-sm font-medium mb-1">${label}${isRequired ? ' *' : ''}</label>
348
+ <input type="email" placeholder="user@example.com" className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg" />
349
+ </div>`;
350
+ }
351
+
352
+ if (p.name.toLowerCase().includes('description') || p.name.toLowerCase().includes('notes')) {
353
+ return ` <div className="md:col-span-2">
354
+ <label className="block text-sm font-medium mb-1">${label}</label>
355
+ <textarea rows={3} className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg" />
356
+ </div>`;
357
+ }
358
+
359
+ // Default text input
360
+ return ` <div>
361
+ <label className="block text-sm font-medium mb-1">${label}${isRequired ? ' *' : ''}</label>
362
+ <input type="text" className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg" />
363
+ </div>`;
364
+ }).join('\n');
365
+
366
+ return ` {/* Section 4: Create Form Example */}
367
+ <section id="create-form" className="card p-6">
368
+ <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
369
+ <span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">4</span>
370
+ Create ${ModuleName} Form
371
+ </h2>
372
+ <p className="text-[var(--text-secondary)] mb-4">
373
+ Form interface for creating new ${module}.
374
+ </p>
375
+
376
+ <div className="border border-[var(--border-color)] rounded-lg p-6 bg-[var(--bg-secondary)]">
377
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
378
+ ${formFields}
379
+ </div>
380
+
381
+ <div className="flex items-center gap-3 mt-6 pt-6 border-t border-[var(--border-color)]">
382
+ <button className="px-4 py-2 bg-[var(--color-primary-600)] text-white rounded-lg flex items-center gap-2">
383
+ <Plus className="w-4 h-4" />
384
+ Create ${ModuleName}
385
+ </button>
386
+ <button className="px-4 py-2 border border-[var(--border-color)] rounded-lg">
387
+ Cancel
388
+ </button>
389
+ </div>
390
+ </div>
391
+ </section>`;
392
+ }
393
+
394
+ /**
395
+ * Generate KPI section UI
396
+ */
397
+ function generateKPISection(module: string): string {
398
+ return ` {/* Stats */}
399
+ <div className="p-4 border-b border-[var(--border-color)] bg-[var(--bg-secondary)]">
400
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2">
401
+ {${module}Stats.map((stat) => (
402
+ <div
403
+ key={stat.label}
404
+ className="p-3 bg-[var(--bg-primary)] rounded-lg text-center"
405
+ style={{ borderLeft: stat.border ? \`3px solid \${stat.border}\` : undefined }}
406
+ >
407
+ <div className="text-2xl font-bold" style={{ color: stat.color }}>{stat.value}</div>
408
+ <div className="text-xs text-[var(--text-secondary)] mt-1">{stat.label}</div>
409
+ </div>
410
+ ))}
411
+ </div>
412
+ </div>`;
413
+ }
414
+
415
+ /**
416
+ * Generate table mock UI section
417
+ */
418
+ function generateTableMockUI(properties: EntityProperty[], module: string): string {
419
+ // Smart property selection for table display
420
+ const filteredProps = properties.filter(p => {
421
+ const name = p.name.toLowerCase();
422
+ // Exclude sensitive, internal, and FK properties
423
+ return !name.includes('hash') &&
424
+ !name.includes('token') &&
425
+ !name.includes('password') &&
426
+ !name.endsWith('id') && // Skip foreign keys
427
+ !name.includes('normalized') &&
428
+ !name.includes('data') && // Skip JSON/blob fields
429
+ !name.includes('proxy') &&
430
+ !name.includes('im') &&
431
+ p.name !== 'Sponsors' &&
432
+ p.name !== 'ProxyAddresses' &&
433
+ p.name !== 'ImAddresses';
434
+ });
435
+
436
+ // Prioritize display properties: name/title > email > status > date
437
+ const priorityProps: EntityProperty[] = [];
438
+
439
+ // Add name/title properties first
440
+ priorityProps.push(...filteredProps.filter(p =>
441
+ p.name.toLowerCase().includes('name') ||
442
+ p.name.toLowerCase().includes('title')
443
+ ).slice(0, 2));
444
+
445
+ // Add email if available
446
+ const emailProp = filteredProps.find(p => p.name.toLowerCase().includes('email'));
447
+ if (emailProp && !priorityProps.includes(emailProp)) {
448
+ priorityProps.push(emailProp);
449
+ }
450
+
451
+ // Add status/active properties
452
+ const statusProp = filteredProps.find(p =>
453
+ p.name.toLowerCase().includes('active') ||
454
+ p.name.toLowerCase().includes('status')
455
+ );
456
+ if (statusProp && !priorityProps.includes(statusProp)) {
457
+ priorityProps.push(statusProp);
458
+ }
459
+
460
+ // Fill remaining slots with other properties (max 6 total)
461
+ const remainingProps = filteredProps.filter(p => !priorityProps.includes(p));
462
+ priorityProps.push(...remainingProps.slice(0, 6 - priorityProps.length));
463
+
464
+ const displayProps = priorityProps.slice(0, 6);
465
+
466
+ const headers = displayProps.map(p => `<th className="text-left p-3 font-medium">${p.name}</th>`).join('\n ');
467
+
468
+ const cells = displayProps.map(p => {
469
+ const propName = p.name.charAt(0).toLowerCase() + p.name.slice(1);
470
+
471
+ if (p.type.toLowerCase() === 'bool' || p.type.toLowerCase() === 'boolean') {
472
+ return `<td className="p-3">
473
+ <span className={\`px-2 py-1 text-xs rounded \${item.${propName} ? 'bg-green-500/10 text-green-600' : 'bg-gray-500/10 text-gray-600'}\`}>
474
+ {item.${propName} ? 'Yes' : 'No'}
475
+ </span>
476
+ </td>`;
477
+ }
478
+
479
+ if (p.type.toLowerCase() === 'datetime') {
480
+ return `<td className="p-3 text-[var(--text-secondary)]">{new Date(item.${propName}).toLocaleDateString()}</td>`;
481
+ }
482
+
483
+ return `<td className="p-3">{item.${propName}}</td>`;
484
+ }).join('\n ');
485
+
486
+ return ` {/* Table */}
487
+ <div className="overflow-x-auto">
488
+ <table className="w-full text-sm">
489
+ <thead className="bg-[var(--bg-secondary)]">
490
+ <tr>
491
+ ${headers}
492
+ <th className="text-right p-3 font-medium">Actions</th>
493
+ </tr>
494
+ </thead>
495
+ <tbody>
496
+ {mock${capitalize(module)}.map((item, idx) => (
497
+ <tr key={idx} className="border-b border-[var(--border-color)] hover:bg-[var(--bg-hover)]">
498
+ ${cells}
499
+ <td className="p-3 text-right">
500
+ <div className="flex items-center justify-end gap-1">
501
+ <button className="p-1.5 hover:bg-[var(--bg-secondary)] rounded">
502
+ <Eye className="w-4 h-4" />
503
+ </button>
504
+ <button className="p-1.5 hover:bg-[var(--bg-secondary)] rounded text-[var(--error-text)]">
505
+ <Trash2 className="w-4 h-4" />
506
+ </button>
507
+ </div>
508
+ </td>
509
+ </tr>
510
+ ))}
511
+ </tbody>
512
+ </table>
513
+ </div>`;
514
+ }
515
+
516
+ /**
517
+ * Generate complete TSX documentation file
518
+ */
519
+ function generateDocumentationTSX(
520
+ moduleInfo: ModuleInfo,
521
+ properties: EntityProperty[],
522
+ apiEndpoints: any[],
523
+ businessRules: any[],
524
+ pageStructure: PageStructure
525
+ ): string {
526
+ const { module, context, application } = moduleInfo;
527
+ const ModuleName = capitalize(module);
528
+
529
+ // Generate mock data
530
+ const mockDataCode = generateMockData(properties, module);
531
+
532
+ // Generate KPI stats
533
+ const kpiStatsCode = generateKPIStats(properties, module);
534
+ const kpiSectionUI = generateKPISection(module);
535
+
536
+ // Generate table UI
537
+ const tableMockUI = generateTableMockUI(properties, module);
538
+
539
+ // Generate form section
540
+ const formSectionUI = generateFormSection(properties, module);
541
+
542
+ // Generate API endpoints mock data
543
+ const apiEndpointsCode = `const apiEndpoints = ${JSON.stringify(apiEndpoints, null, 2)};`;
544
+
545
+ return `import { Link } from 'react-router-dom';
546
+ import { useTranslation } from 'react-i18next';
547
+ import {
548
+ ArrowRight,
549
+ Shield,
550
+ Eye,
551
+ Trash2,
552
+ Plus,
553
+ Info,
554
+ MessageSquare,
555
+ HelpCircle,
556
+ } from 'lucide-react';
557
+
558
+ // Mock data for ${ModuleName}
559
+ ${mockDataCode}
560
+
561
+ // KPI Stats
562
+ ${kpiStatsCode}
563
+
564
+ // API endpoints
565
+ ${apiEndpointsCode}
566
+
567
+ export function ${ModuleName}DocPage() {
568
+ const { t } = useTranslation('docs');
569
+
570
+ return (
571
+ <div className="space-y-8">
572
+ {/* Breadcrumb */}
573
+ <div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
574
+ <Link to="/system/docs" className="hover:text-[var(--color-primary-600)]">Documentation</Link>
575
+ <span>/</span>
576
+ <Link to="/system/docs/user" className="hover:text-[var(--color-primary-600)]">User</Link>
577
+ <span>/</span>
578
+ <Link to="/system/docs/user/${context}" className="hover:text-[var(--color-primary-600)]">${capitalize(context)}</Link>
579
+ <span>/</span>
580
+ <span>${ModuleName}</span>
581
+ </div>
582
+
583
+ {/* Header */}
584
+ <div>
585
+ <h1 className="text-3xl font-bold mb-4">
586
+ ${ModuleName} Management
587
+ </h1>
588
+ <p className="text-lg text-[var(--text-secondary)]">
589
+ Complete management interface for ${module}
590
+ </p>
591
+ </div>
592
+
593
+ {/* Section 1: Introduction */}
594
+ <section id="introduction" className="card p-6">
595
+ <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
596
+ <span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">1</span>
597
+ Introduction
598
+ </h2>
599
+ <p className="text-[var(--text-secondary)]">
600
+ The ${ModuleName} module provides comprehensive management capabilities including
601
+ viewing, creating, updating, and deleting ${module}.
602
+ </p>
603
+ </section>
604
+
605
+ {/* Section 2: Access */}
606
+ <section id="access" className="card p-6">
607
+ <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
608
+ <span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">2</span>
609
+ Access
610
+ </h2>
611
+
612
+ <div className="mb-4">
613
+ <div className="text-sm font-medium mb-2">Navigation</div>
614
+ <div className="flex items-center gap-2 text-sm bg-[var(--bg-secondary)] p-3 rounded-lg">
615
+ <span className="px-2 py-1 bg-[var(--bg-primary)] rounded">${capitalize(context)}</span>
616
+ <ArrowRight className="w-4 h-4 text-[var(--text-tertiary)]" />
617
+ <span className="px-2 py-1 bg-[var(--bg-primary)] rounded">${capitalize(application)}</span>
618
+ <ArrowRight className="w-4 h-4 text-[var(--text-tertiary)]" />
619
+ <span className="px-2 py-1 bg-[var(--color-primary-600)] text-white rounded">${ModuleName}</span>
620
+ </div>
621
+ </div>
622
+
623
+ <div className="mb-4">
624
+ <div className="text-sm font-medium mb-2">URL</div>
625
+ <code className="block p-3 bg-[var(--bg-secondary)] rounded-lg text-sm font-mono">
626
+ /${context}/${application}/${module}
627
+ </code>
628
+ </div>
629
+ </section>
630
+
631
+ {/* Section 3: Overview - MOCK UI */}
632
+ <section id="overview" className="card p-6">
633
+ <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
634
+ <span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">3</span>
635
+ Interface Overview
636
+ </h2>
637
+ <p className="text-[var(--text-secondary)] mb-4">
638
+ The main ${module} interface displays all records in a table format with search and filter capabilities.
639
+ </p>
640
+
641
+ {/* Mock UI - Complete Interface */}
642
+ <div className="border border-[var(--border-color)] rounded-lg overflow-hidden">
643
+ {/* Header */}
644
+ <div className="p-4 border-b border-[var(--border-color)] flex items-center justify-between">
645
+ <div>
646
+ <div className="font-bold">${ModuleName} Management</div>
647
+ <div className="text-sm text-[var(--text-secondary)]">Manage all ${module}</div>
648
+ </div>
649
+ <button className="px-4 py-2 bg-[var(--color-primary-600)] text-white rounded-lg flex items-center gap-2 text-sm">
650
+ <Plus className="w-4 h-4" />
651
+ New ${ModuleName}
652
+ </button>
653
+ </div>
654
+
655
+ ${kpiSectionUI}
656
+
657
+ {/* Table */}
658
+ ${tableMockUI}
659
+ </div>
660
+ </section>
661
+
662
+ ${formSectionUI}
663
+
664
+ {/* Section 5: API Reference */}
665
+ <section id="api" className="card p-6">
666
+ <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
667
+ <span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">5</span>
668
+ API Reference
669
+ </h2>
670
+
671
+ <div className="overflow-x-auto">
672
+ <table className="w-full text-sm">
673
+ <thead>
674
+ <tr className="bg-[var(--bg-secondary)]">
675
+ <th className="text-left py-2 px-3 rounded-tl-lg">Method</th>
676
+ <th className="text-left py-2 px-3">Endpoint</th>
677
+ <th className="text-left py-2 px-3 rounded-tr-lg">Handler</th>
678
+ </tr>
679
+ </thead>
680
+ <tbody>
681
+ {apiEndpoints.map((endpoint, index) => (
682
+ <tr key={index} className="border-b border-[var(--border-color)]">
683
+ <td className="py-2 px-3">
684
+ <span className={\`px-2 py-0.5 rounded text-xs font-medium \${
685
+ endpoint.method === 'GET' ? 'bg-green-500/10 text-green-600' :
686
+ endpoint.method === 'POST' ? 'bg-yellow-500/10 text-yellow-600' :
687
+ endpoint.method === 'PUT' ? 'bg-blue-500/10 text-blue-600' :
688
+ 'bg-red-500/10 text-red-600'
689
+ }\`}>
690
+ {endpoint.method}
691
+ </span>
692
+ </td>
693
+ <td className="py-2 px-3 font-mono text-xs">{endpoint.path}</td>
694
+ <td className="py-2 px-3 text-[var(--text-secondary)]">{endpoint.handler}</td>
695
+ </tr>
696
+ ))}
697
+ </tbody>
698
+ </table>
699
+ </div>
700
+ </section>
701
+ </div>
702
+ );
703
+ }
704
+ `;
705
+ }
706
+
707
+ /**
708
+ * Capitalize first letter
709
+ */
710
+ function capitalize(str: string): string {
711
+ return str.charAt(0).toUpperCase() + str.slice(1);
712
+ }
713
+
714
+ /**
715
+ * Simple singularize function for common English plurals
716
+ */
717
+ function singularize(str: string): string {
718
+ if (str.endsWith('ies')) {
719
+ return str.slice(0, -3) + 'y';
720
+ }
721
+ if (str.endsWith('sses') || str.endsWith('shes') || str.endsWith('ches') || str.endsWith('xes')) {
722
+ return str.slice(0, -2);
723
+ }
724
+ if (str.endsWith('s') && !str.endsWith('ss')) {
725
+ return str.slice(0, -1);
726
+ }
727
+ return str;
728
+ }
729
+
730
+ /**
731
+ * Main execution
732
+ */
733
+ async function main() {
734
+ const moduleInfo = parseArgs();
735
+ const { module, context, application, appPath } = moduleInfo;
736
+
737
+ console.error(`\n🚀 Generating documentation for: ${context}/${application}/${module}\n`);
738
+
739
+ // 1. Find entity file
740
+ console.error('📁 Finding entity file...');
741
+ const entityPath = await findEntityFile(appPath, module);
742
+ if (!entityPath) {
743
+ console.error(`❌ Entity not found for module: ${module}`);
744
+ process.exit(1);
745
+ }
746
+ console.error(`✅ Found: ${entityPath}`);
747
+
748
+ // 2. Extract entity properties
749
+ console.error('\n📊 Extracting entity properties...');
750
+ const properties = extractEntityProperties(entityPath);
751
+ console.error(`✅ Extracted ${properties.length} properties`);
752
+
753
+ // 3. Find page file
754
+ console.error('\n📄 Finding React page...');
755
+ const pagePath = await findPageFile(appPath, module, context, application);
756
+ if (pagePath) {
757
+ console.error(`✅ Found: ${pagePath}`);
758
+ } else {
759
+ console.error(`⚠️ Page not found (will use defaults)`);
760
+ }
761
+
762
+ // 4. Analyze page structure
763
+ const pageStructure = pagePath ? analyzePageStructure(pagePath) : {
764
+ hasTable: true,
765
+ hasKPIs: false,
766
+ hasForm: false,
767
+ hasFilters: false,
768
+ columns: [],
769
+ };
770
+
771
+ // 5. Extract API endpoints using existing script
772
+ console.error('\n🔌 Extracting API endpoints...');
773
+ let apiEndpoints: any[] = [];
774
+ try {
775
+ const endpointsOutput = execSync(
776
+ `npx tsx "${__dirname}/extract-api-endpoints.ts" --module ${module} --app-path "${appPath}"`,
777
+ { encoding: 'utf-8' }
778
+ );
779
+ apiEndpoints = JSON.parse(endpointsOutput);
780
+ console.error(`✅ Extracted ${apiEndpoints.length} endpoints`);
781
+ } catch (error) {
782
+ console.error(`⚠️ Could not extract endpoints`);
783
+ }
784
+
785
+ // 6. Extract business rules using existing script
786
+ console.error('\n📋 Extracting business rules...');
787
+ let businessRules: any[] = [];
788
+ try {
789
+ const rulesOutput = execSync(
790
+ `npx tsx "${__dirname}/extract-business-rules.ts" --module ${module} --app-path "${appPath}"`,
791
+ { encoding: 'utf-8' }
792
+ );
793
+ businessRules = JSON.parse(rulesOutput);
794
+ console.error(`✅ Extracted ${businessRules.length} business rules`);
795
+ } catch (error) {
796
+ console.error(`⚠️ Could not extract business rules`);
797
+ }
798
+
799
+ // 7. Generate documentation TSX
800
+ console.error('\n✨ Generating documentation TSX...');
801
+ const tsx = generateDocumentationTSX(moduleInfo, properties, apiEndpoints, businessRules, pageStructure);
802
+
803
+ // 8. Output
804
+ console.log(tsx);
805
+ console.error('\n✅ Documentation generated successfully!');
806
+ }
807
+
808
+ main().catch((error) => {
809
+ console.error('❌ Fatal error:', error);
810
+ process.exit(1);
811
+ });