g360-cli 1.12.0 → 1.13.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,815 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const BRAND_PATH = path.join(__dirname, '..', '..', 'assets', 'brand', 'brand.json');
8
+
9
+ const LEVELS = ['readme', 'architecture', 'business-rules', 'api', 'dependencies', 'classes', 'code-graph', 'all'];
10
+
11
+ export async function docs(level, options) {
12
+ const { project = '.', dryRun = false } = options;
13
+ const targetDir = path.join(process.cwd(), project);
14
+
15
+ if (!LEVELS.includes(level)) {
16
+ console.error(chalk.red(`❌ Nivel invalido: "${level}"`));
17
+ console.log(chalk.gray('Niveles disponibles:'));
18
+ console.log(chalk.gray(' readme - README.md con diagrama y logo'));
19
+ console.log(chalk.gray(' architecture - ARCHITECTURE.md con diagramas'));
20
+ console.log(chalk.gray(' business-rules - BUSINESS_RULES.md (solo Python con commercial_engine)'));
21
+ console.log(chalk.gray(' api - API.md (solo proyectos con endpoints)'));
22
+ console.log(chalk.gray(' dependencies - docs/generated/dependencies.mmd'));
23
+ console.log(chalk.gray(' classes - docs/generated/classes.mmd'));
24
+ console.log(chalk.gray(' code-graph - docs/generated/code_graph.mmd (proyectos grandes)'));
25
+ console.log(chalk.gray(' all - Todos los niveles aplicables'));
26
+ console.log(chalk.gray('\nEjemplo:'));
27
+ console.log(chalk.gray(' g360 docs'));
28
+ console.log(chalk.gray(' g360 docs --level all'));
29
+ console.log(chalk.gray(' g360 docs --level readme --project ./mi-proyecto'));
30
+ return;
31
+ }
32
+
33
+ if (!fs.existsSync(targetDir)) {
34
+ console.error(chalk.red(`❌ Directorio no encontrado: ${targetDir}`));
35
+ return;
36
+ }
37
+
38
+ const projectInfo = detectProject(targetDir);
39
+ const brand = loadBrand();
40
+ const manifest = loadManifest(targetDir);
41
+ const skill = loadSkill(targetDir);
42
+
43
+ if (dryRun) {
44
+ console.log(chalk.yellow('\n📋 DRY RUN — Archivos que se generarian:\n'));
45
+ } else {
46
+ console.log(chalk.bold.cyan('\n📝 G360 Documentation Generator\n'));
47
+ console.log(chalk.gray(`Path: ${targetDir}\n`));
48
+ }
49
+
50
+ const levelsToRun = level === 'all'
51
+ ? getApplicableLevels(projectInfo)
52
+ : [level];
53
+
54
+ for (const lvl of levelsToRun) {
55
+ const result = await generateLevel(lvl, targetDir, projectInfo, brand, manifest, skill, dryRun);
56
+ if (result) {
57
+ console.log(chalk.green(` ✅ ${result}`));
58
+ }
59
+ }
60
+
61
+ if (!dryRun) {
62
+ console.log(chalk.gray('\n💡 Usa --dry-run para previsualizar sin escribir archivos.\n'));
63
+ }
64
+ }
65
+
66
+ function detectProject(dir) {
67
+ const pyproject = path.join(dir, 'pyproject.toml');
68
+ const mainPy = path.join(dir, 'src', 'main.py');
69
+ const indexHtml = path.join(dir, 'index.html');
70
+ const pkgJson = path.join(dir, 'package.json');
71
+ const skillJson = path.join(dir, 'skill.json');
72
+
73
+ let type = 'unknown';
74
+ let framework = null;
75
+
76
+ if (fs.existsSync(pyproject)) {
77
+ const content = fs.readFileSync(pyproject, 'utf8');
78
+ if (content.includes('flet')) {
79
+ type = 'python-flet';
80
+ framework = 'flet';
81
+ } else if (content.includes('customtkinter')) {
82
+ type = 'python-customtkinter';
83
+ framework = 'customtkinter';
84
+ } else if (fs.existsSync(mainPy)) {
85
+ const mainContent = fs.readFileSync(mainPy, 'utf8');
86
+ if (mainContent.includes('flet')) {
87
+ type = 'python-flet';
88
+ framework = 'flet';
89
+ } else {
90
+ type = 'python-cli';
91
+ framework = 'cli';
92
+ }
93
+ } else {
94
+ type = 'python-cli';
95
+ framework = 'cli';
96
+ }
97
+ }
98
+
99
+ if (type === 'unknown') {
100
+ if (fs.existsSync(indexHtml)) {
101
+ type = 'web';
102
+ if (fs.existsSync(pkgJson)) {
103
+ try {
104
+ const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'));
105
+ const deps = Object.keys(pkg.dependencies || {});
106
+ if (deps.some(d => d.includes('react'))) framework = 'react';
107
+ else if (deps.some(d => d.includes('lit'))) framework = 'lit';
108
+ else if (deps.some(d => d.includes('solid'))) framework = 'solid';
109
+ else if (deps.some(d => d.includes('svelte'))) framework = 'svelte';
110
+ } catch { /* keep framework as detected */ }
111
+ }
112
+ } else if (fs.existsSync(pkgJson)) {
113
+ type = 'web';
114
+ try {
115
+ const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'));
116
+ const deps = Object.keys(pkg.dependencies || {});
117
+ if (deps.some(d => d.includes('react'))) framework = 'react';
118
+ else if (deps.some(d => d.includes('lit'))) framework = 'lit';
119
+ else if (deps.some(d => d.includes('solid'))) framework = 'solid';
120
+ else if (deps.some(d => d.includes('svelte'))) framework = 'svelte';
121
+ } catch { /* keep framework as detected */ }
122
+ }
123
+ }
124
+
125
+ return { type, framework, dir };
126
+ }
127
+
128
+ function loadBrand() {
129
+ if (fs.existsSync(BRAND_PATH)) {
130
+ try {
131
+ return JSON.parse(fs.readFileSync(BRAND_PATH, 'utf8'));
132
+ } catch { /* fallback */ }
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function loadManifest(dir) {
138
+ const manifestPath = path.join(dir, 'g360-manifest.json');
139
+ if (fs.existsSync(manifestPath)) {
140
+ try {
141
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
142
+ } catch { /* fallback */ }
143
+ }
144
+ return null;
145
+ }
146
+
147
+ function loadSkill(dir) {
148
+ const candidates = [
149
+ path.join(dir, 'skill.json'),
150
+ path.join(dir, 'src', 'core', 'skill.json'),
151
+ ];
152
+ for (const p of candidates) {
153
+ if (fs.existsSync(p)) {
154
+ try {
155
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
156
+ } catch { /* fallback */ }
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+
162
+ function getApplicableLevels(projectInfo) {
163
+ const levels = ['readme', 'architecture'];
164
+ if (projectInfo.type === 'python-flet' || projectInfo.type === 'python-cli') {
165
+ levels.push('business-rules');
166
+ }
167
+ levels.push('dependencies', 'classes');
168
+ return levels;
169
+ }
170
+
171
+ async function generateLevel(lvl, dir, projectInfo, brand, manifest, skill, dryRun) {
172
+ switch (lvl) {
173
+ case 'readme': return await generateReadme(dir, projectInfo, brand, manifest, skill, dryRun);
174
+ case 'architecture': return await generateArchitecture(dir, projectInfo, brand, manifest, skill, dryRun);
175
+ case 'business-rules': return await generateBusinessRules(dir, projectInfo, dryRun);
176
+ case 'dependencies': return await generateDependencies(dir, projectInfo, dryRun);
177
+ case 'classes': return await generateClasses(dir, projectInfo, dryRun);
178
+ case 'code-graph': return await generateCodeGraph(dir, projectInfo, dryRun);
179
+ default: return null;
180
+ }
181
+ }
182
+
183
+ async function generateReadme(dir, projectInfo, brand, manifest, skill, dryRun) {
184
+ const name = manifest?.name || skill?.name || path.basename(dir);
185
+ const description = manifest?.description || skill?.description || `Proyecto ${name}`;
186
+ const version = manifest?.version || '1.0.0';
187
+ const template = manifest?.template || projectInfo.type;
188
+ const signature = skill?.signature || { mode: 'powered', text: 'powered by G360' };
189
+ const brandName = brand?.brands?.[skill?.brand || 'g360'];
190
+ const logoPath = brandName?.default_logo || 'logotypes/logo-g360-dark.svg';
191
+ const logoDir = path.dirname(logoPath);
192
+ const logoFile = path.basename(logoPath);
193
+
194
+ const diagram = buildReadmeDiagram(dir, projectInfo);
195
+
196
+ const content = `# ${name}
197
+
198
+ <picture>
199
+ <source media="(prefers-color-scheme: dark)" srcset="${logoDir}/${logoFile.replace('-dark', '-light')}">
200
+ <img alt="${name}" height="64" src="${logoDir}/${logoFile}">
201
+ </picture>
202
+
203
+ > ${description}
204
+
205
+ [![Version](https://img.shields.io/badge/version-${version}-blue)](https://github.com)
206
+
207
+ ## ¿Cómo está organizado el proyecto?
208
+
209
+ \`\`\`mermaid
210
+ ${diagram}
211
+ \`\`\`
212
+
213
+ ## Quick Start
214
+
215
+ \`\`\`bash
216
+ # 1. Entrar al proyecto
217
+ cd ${name}
218
+
219
+ # 2. Ver estructura
220
+ g360 present
221
+
222
+ # 3. Auditar compliance
223
+ g360 audit
224
+
225
+ # 4. Traer assets de marca
226
+ g360 bring brand
227
+ \`\`\`
228
+
229
+ ## Identidad de Marca
230
+
231
+ | Elemento | Valor |
232
+ |---|---|
233
+ | Marca | ${brandName?.name || 'G360'} |
234
+ | Color primario | ${brandName?.primary_color || '#00d084'} |
235
+ | Signature mode | ${signature.mode} |
236
+ | Signature text | "${signature.text}" |
237
+ | Logo | ${logoPath} |
238
+
239
+ ## Footer
240
+
241
+ \`\`\`html
242
+ <g360-signature mode="${signature.mode}"></g360-signature>
243
+ \`\`\`
244
+
245
+ ---
246
+
247
+ **Marca**: ${brandName?.name || 'G360'} · **Isotipo**: 3 puntos + chevron \`>\`
248
+ **Signature**: ${signature.text} · **Powered by**: [g360-signature](https://github.com/carloscus/g360-signature)
249
+
250
+ *Generado por \`g360 docs\` · Fuente: \`brand.json\` + \`skill.json\` + \`g360-manifest.json\`*
251
+ `;
252
+
253
+ const outputPath = path.join(dir, 'README.md');
254
+ if (dryRun) {
255
+ console.log(chalk.gray(` [dry-run] README.md`));
256
+ return null;
257
+ }
258
+ await fs.writeFile(outputPath, content, 'utf8');
259
+ return 'README.md';
260
+ }
261
+
262
+ async function generateArchitecture(dir, projectInfo, brand, manifest, skill, dryRun) {
263
+ const name = manifest?.name || skill?.name || path.basename(dir);
264
+ const diagram = buildArchitectureDiagram(dir, projectInfo);
265
+
266
+ const content = `# ARCHITECTURE.md — ${name}
267
+
268
+ > Arquitectura general del proyecto. Generado automáticamente por \`g360 docs --level architecture\`.
269
+
270
+ ## Arquitectura General
271
+
272
+ \`\`\`mermaid
273
+ ${diagram}
274
+ \`\`\`
275
+
276
+ ## Componentes
277
+
278
+ | Componente | Responsabilidad |
279
+ |---|---|
280
+ | Entry point | Punto de inicio de la aplicación |
281
+ | Core | Lógica de negocio y configuración |
282
+ | UI | Presentación y interacción con el usuario |
283
+ | Assets | Recursos estáticos (imágenes, iconos, marca) |
284
+ | Config | Archivos de configuración (skill.json, manifest) |
285
+
286
+ ## Flujo de Datos
287
+
288
+ \`\`\`mermaid
289
+ flowchart LR
290
+ INPUT["Entrada del usuario"] --> PROCESS["Procesamiento"]
291
+ PROCESS --> VALIDATE["Validación"]
292
+ VALIDATE --> OUTPUT["Resultado"]
293
+ OUTPUT --> PERSIST["Persistencia"]
294
+ \`\`\`
295
+
296
+ ---
297
+
298
+ *Generado por \`g360 docs --level architecture\`*
299
+ `;
300
+
301
+ const outputPath = path.join(dir, 'ARCHITECTURE.md');
302
+ if (dryRun) {
303
+ console.log(chalk.gray(` [dry-run] ARCHITECTURE.md`));
304
+ return null;
305
+ }
306
+ await fs.writeFile(outputPath, content, 'utf8');
307
+ return 'ARCHITECTURE.md';
308
+ }
309
+
310
+ async function generateBusinessRules(dir, projectInfo, dryRun) {
311
+ const commercialEngine = path.join(dir, 'py', 'src', 'g360_core', 'commercial_engine.py');
312
+ if (!fs.existsSync(commercialEngine)) {
313
+ console.log(chalk.yellow(' ⚠ No se encontró commercial_engine.py. Se omite BUSINESS_RULES.md.\n'));
314
+ return null;
315
+ }
316
+
317
+ const content = `# BUSINESS_RULES.md — Motor de clasificación comercial
318
+
319
+ > Única fuente de verdad para reglas de negocio. Todas las reglas viven en
320
+ > \`commercial_engine.py\`, no en \`processor.py\` ni \`pipeline.py\`.
321
+
322
+ **Módulo**: \`g360_core.commercial_engine\`
323
+
324
+ ## Flujo de reglas
325
+
326
+ \`\`\`mermaid
327
+ flowchart TD
328
+ IN["Entrada<br/>DataFrame ERP (.xls/.xlsx/.csv)"]
329
+ P1["parse_referencia<br/>REF_TIPO / REF_SERIE / REF_NUMERO"]
330
+ P2["classify_base<br/>CATEGORIA_OP"]
331
+ P3["resolve_document_relationships<br/>SUBTIPO_AJUSTE"]
332
+ P4["calculate_prices<br/>PRECIO_BASE / RECARGO / EFECTIVO"]
333
+ OUT["Salida<br/>DataFrame enriquecido"]
334
+ CSV["Persistencia<br/>maestro_ventas_crm.csv"]
335
+
336
+ IN --> P1 --> P2 --> P3 --> P4 --> OUT --> CSV
337
+ \`\`\`
338
+
339
+ ## Paso 1 — Parseo de REFERENCIA
340
+
341
+ Extrae \`REF_TIPO\`, \`REF_SERIE\` y \`REF_NUMERO\` del campo \`REFERENCIA\`.
342
+ Formato esperado: \`F01/204-56287\`. Regex: \`^([A-Z0-9]+)/(\\d+)-(\\d+)$\`.
343
+ Sin match → \`"S/R"\` en los 3 campos.
344
+
345
+ ## Paso 2 — Clasificación primaria
346
+
347
+ Solo mira la fila actual. No cruza con otros documentos.
348
+
349
+ | TPO_DOC | CANTIDAD | → CATEGORIA_OP |
350
+ |---|---|---|
351
+ | F01, BDI, F03, B01, B03, F07, F08, B07, B08 | cualquiera | **VENTA** |
352
+ | NC\* (prefijos en \`NC_PREFIXES\`) | ≠ 0 | **DEVOLUCION** |
353
+ | NC\* | = 0 | **AJUSTE** |
354
+ | ND\* (prefijos en \`ND_PREFIXES\`) | cualquiera | **AJUSTE** |
355
+
356
+ \`SUBTIPO_AJUSTE\` se inicializa vacío aquí; se asigna en el paso 3.
357
+
358
+ ## Paso 3 — Resolución de relaciones
359
+
360
+ Cruza \`REFERENCIA\` contra el índice de facturas de \`build_invoice_index\`.
361
+ Clave: \`(TPO_DOC, SERIE_DOC, NRO_DOC, ID_ARTICULO)\`. Solo se indexan registros VENTA.
362
+
363
+ | Condición | CANTIDAD_FAE | → SUBTIPO_AJUSTE |
364
+ |---|---|---|
365
+ | Clave con SKU coincide | = 0 | **CARGO_FIJO** |
366
+ | Clave con SKU coincide | ≈ CANTIDAD factura | **PRECIO_LINEA** |
367
+ | Clave con SKU coincide | < CANTIDAD factura | **PRECIO_PARCIAL** |
368
+ | Clave con SKU coincide | > CANTIDAD factura | **SIN_BASE** |
369
+ | Clave no coincide | = 1 | **CARGO_FIJO** |
370
+ | Clave no coincide | ≠ 1 | **SIN_BASE** |
371
+
372
+ ## Paso 4 — Cálculo de precios
373
+
374
+ | Columna | Fórmula | Cuándo |
375
+ |---|---|---|
376
+ | \`PRECIO_BASE\` | \`\|SOLES\| / \|CANTIDAD\|\` | CANTIDAD ≠ 0 |
377
+ | \`RECARGO_UNITARIO\` | \`SOLES / \|CANTIDAD_FAE\|\` | AJUSTE con SUBTIPO_LINEA/PARCIAL y FAE ≠ 0 |
378
+ | \`PRECIO_EFECTIVO\` | \`PRECIO_BASE + RECARGO\` | PRECIO_BASE no es NaN |
379
+
380
+ ---
381
+
382
+ *Generado por \`g360 docs --level business-rules\`*
383
+ `;
384
+
385
+ const outputPath = path.join(dir, 'BUSINESS_RULES.md');
386
+ if (dryRun) {
387
+ console.log(chalk.gray(` [dry-run] BUSINESS_RULES.md`));
388
+ return null;
389
+ }
390
+ await fs.writeFile(outputPath, content, 'utf8');
391
+ return 'BUSINESS_RULES.md';
392
+ }
393
+
394
+ async function generateDependencies(dir, projectInfo, dryRun) {
395
+ const scan = scanProject(dir);
396
+ const deps = [];
397
+
398
+ for (const file of scan.pyFiles) {
399
+ if (file.endsWith('__init__.py')) continue;
400
+ const filePath = path.join(dir, file);
401
+ const content = fs.readFileSync(filePath, 'utf8');
402
+ const imports = extractImports(content);
403
+ for (const imp of imports) {
404
+ if (imp.startsWith('.')) {
405
+ const target = imp.replace(/^\.\.?\//, '').replace(/\.py$/, '');
406
+ deps.push({ from: file.replace(/\.py$/, ''), to: target });
407
+ }
408
+ }
409
+ }
410
+
411
+ for (const file of scan.jsFiles) {
412
+ const filePath = path.join(dir, file);
413
+ const content = fs.readFileSync(filePath, 'utf8');
414
+ const imports = extractJsImports(content);
415
+ for (const imp of imports) {
416
+ deps.push({ from: file.replace(/\.(js|jsx|ts|tsx)$/, ''), to: imp });
417
+ }
418
+ }
419
+
420
+ let diagram;
421
+ if (deps.length > 0) {
422
+ const nodes = new Set();
423
+ deps.forEach(d => { nodes.add(d.from); nodes.add(d.to); });
424
+ const nodeList = [...nodes].map(n => ` ${n}`).join('\n');
425
+ const edges = deps.map(d => ` ${d.from} --> ${d.to}`).join('\n');
426
+ diagram = `flowchart TD\n${nodeList}\n\n${edges}`;
427
+ } else {
428
+ diagram = `flowchart TD\n main --> core\n core --> utils\n main --> config`;
429
+ }
430
+
431
+ const outputDir = path.join(dir, 'docs', 'generated');
432
+ const outputPath = path.join(outputDir, 'dependencies.mmd');
433
+
434
+ if (dryRun) {
435
+ console.log(chalk.gray(` [dry-run] docs/generated/dependencies.mmd`));
436
+ return null;
437
+ }
438
+ await fs.ensureDir(outputDir);
439
+ await fs.writeFile(outputPath, diagram, 'utf8');
440
+ return 'docs/generated/dependencies.mmd';
441
+ }
442
+
443
+ async function generateClasses(dir, projectInfo, dryRun) {
444
+ const scan = scanProject(dir);
445
+ const classes = [];
446
+
447
+ for (const file of scan.pyFiles) {
448
+ if (file.endsWith('__init__.py')) continue;
449
+ const filePath = path.join(dir, file);
450
+ const content = fs.readFileSync(filePath, 'utf8');
451
+ const classMatches = content.match(/^class\s+(\w+)/gm);
452
+ if (classMatches) {
453
+ for (const m of classMatches) {
454
+ const name = m.replace(/^class\s+/, '').replace(/\(.*/, '');
455
+ classes.push({ file: file.replace(/\.py$/, ''), name });
456
+ }
457
+ }
458
+ }
459
+
460
+ for (const file of scan.jsFiles) {
461
+ const filePath = path.join(dir, file);
462
+ const content = fs.readFileSync(filePath, 'utf8');
463
+ const classMatches = content.match(/^export\s+class\s+(\w+)/gm);
464
+ if (classMatches) {
465
+ for (const m of classMatches) {
466
+ const name = m.replace(/^export\s+class\s+/, '');
467
+ classes.push({ file: file.replace(/\.(js|jsx|ts|tsx)$/, ''), name });
468
+ }
469
+ }
470
+ }
471
+
472
+ let diagram;
473
+ if (classes.length > 0) {
474
+ const nodes = classes.map(c => ` ${c.name}`).join('\n');
475
+ const edges = classes.map(c => ` ${c.file} --> ${c.name}`).join('\n');
476
+ diagram = `flowchart TD\n${nodes}\n\n${edges}`;
477
+ } else {
478
+ diagram = `flowchart TD\n main --> core\n core --> utils`;
479
+ }
480
+
481
+ const outputDir = path.join(dir, 'docs', 'generated');
482
+ const outputPath = path.join(outputDir, 'classes.mmd');
483
+
484
+ if (dryRun) {
485
+ console.log(chalk.gray(` [dry-run] docs/generated/classes.mmd`));
486
+ return null;
487
+ }
488
+ await fs.ensureDir(outputDir);
489
+ await fs.writeFile(outputPath, diagram, 'utf8');
490
+ return 'docs/generated/classes.mmd';
491
+ }
492
+
493
+ async function generateCodeGraph(dir, projectInfo, dryRun) {
494
+ const scan = scanProject(dir);
495
+ const totalFiles = scan.files.length;
496
+ if (totalFiles < 5) {
497
+ console.log(chalk.gray(' ⚠ Proyecto demasiado pequeño para code_graph. Se omite.\n'));
498
+ return null;
499
+ }
500
+
501
+ const pyFiles = scan.pyFiles.filter(f => !f.endsWith('__init__.py'));
502
+ const jsFiles = scan.jsFiles;
503
+
504
+ let diagram;
505
+ if (pyFiles.length > 0 || jsFiles.length > 0) {
506
+ diagram = 'flowchart TD\n';
507
+ diagram += ' subgraph Entrada["Entrada"]\n';
508
+ if (scan.hasMainPy) diagram += ' Main["main.py"]\n';
509
+ if (scan.hasIndexHtml) diagram += ' Index["index.html"]\n';
510
+ diagram += ' end\n';
511
+ diagram += ' subgraph Core["Core"]\n';
512
+ for (const f of pyFiles.slice(0, 10)) {
513
+ diagram += ` ${f.replace(/\.py$/, '')}\n`;
514
+ }
515
+ for (const f of jsFiles.slice(0, 10)) {
516
+ diagram += ` ${f.replace(/\.(js|jsx|ts|tsx)$/, '')}\n`;
517
+ }
518
+ diagram += ' end\n';
519
+ diagram += ' subgraph Salida["Salida"]\n';
520
+ diagram += ' Output["Resultado"]\n';
521
+ diagram += ' end\n';
522
+ diagram += ' Entrada --> Core\n';
523
+ diagram += ' Core --> Salida';
524
+ } else {
525
+ diagram = `flowchart TD\n App["Aplicacion"]\n Config["Config"]\n App --> Config`;
526
+ }
527
+
528
+ const outputDir = path.join(dir, 'docs', 'generated');
529
+ const outputPath = path.join(outputDir, 'code_graph.mmd');
530
+
531
+ if (dryRun) {
532
+ console.log(chalk.gray(` [dry-run] docs/generated/code_graph.mmd`));
533
+ return null;
534
+ }
535
+ await fs.ensureDir(outputDir);
536
+ await fs.writeFile(outputPath, diagram, 'utf8');
537
+ return 'docs/generated/code_graph.mmd';
538
+ }
539
+
540
+ function scanProject(dir) {
541
+ const results = {
542
+ files: [],
543
+ dirs: [],
544
+ pyFiles: [],
545
+ jsFiles: [],
546
+ htmlFiles: [],
547
+ configFiles: [],
548
+ uiDirs: [],
549
+ coreDirs: [],
550
+ hasCommercialEngine: false,
551
+ hasMainPy: false,
552
+ hasIndexHtml: false,
553
+ hasPackageJson: false,
554
+ hasPyproject: false,
555
+ };
556
+
557
+ const extensions = {
558
+ py: '.py',
559
+ js: '.js',
560
+ jsx: '.jsx',
561
+ ts: '.ts',
562
+ tsx: '.tsx',
563
+ html: '.html',
564
+ svelte: '.svelte',
565
+ vue: '.vue',
566
+ json: '.json',
567
+ toml: '.toml',
568
+ svg: '.svg',
569
+ ico: '.ico',
570
+ png: '.png',
571
+ jpg: '.jpg',
572
+ jpeg: '.jpeg',
573
+ gif: '.gif',
574
+ css: '.css',
575
+ scss: '.scss',
576
+ bat: '.bat',
577
+ sh: '.sh',
578
+ };
579
+
580
+ function walk(d, depth) {
581
+ if (depth > 5) return;
582
+ const items = fs.readdirSync(d, { withFileTypes: true });
583
+ for (const item of items) {
584
+ if (item.name.startsWith('.')) continue;
585
+ if (item.name === 'node_modules') continue;
586
+ if (item.name === '__pycache__') continue;
587
+ if (item.name === '.pytest_cache') continue;
588
+ if (item.name === 'g360') continue;
589
+
590
+ const fullPath = path.join(d, item.name);
591
+ const relPath = path.relative(dir, fullPath);
592
+
593
+ if (item.isDirectory()) {
594
+ results.dirs.push(relPath);
595
+ if (relPath.includes('ui') || relPath.includes('views') || relPath.includes('pages') || relPath.includes('components')) {
596
+ results.uiDirs.push(relPath);
597
+ }
598
+ if (relPath.includes('core') || relPath.includes('business') || relPath.includes('engine')) {
599
+ results.coreDirs.push(relPath);
600
+ }
601
+ walk(fullPath, depth + 1);
602
+ } else if (item.isFile()) {
603
+ const ext = path.extname(item.name).toLowerCase();
604
+ results.files.push({ path: relPath, ext, name: item.name });
605
+
606
+ if (ext === '.py') results.pyFiles.push(relPath);
607
+ if (ext === '.js' || ext === '.jsx' || ext === '.ts' || ext === '.tsx') results.jsFiles.push(relPath);
608
+ if (ext === '.html') results.htmlFiles.push(relPath);
609
+ if (ext === '.json' || ext === '.toml') results.configFiles.push(relPath);
610
+ if (item.name === 'main.py') results.hasMainPy = true;
611
+ if (item.name === 'index.html') results.hasIndexHtml = true;
612
+ if (item.name === 'package.json') results.hasPackageJson = true;
613
+ if (item.name === 'pyproject.toml') results.hasPyproject = true;
614
+ if (item.name === 'commercial_engine.py') results.hasCommercialEngine = true;
615
+ }
616
+ }
617
+ }
618
+
619
+ walk(dir, 0);
620
+ return results;
621
+ }
622
+
623
+ function buildReadmeDiagram(dir, projectInfo) {
624
+ const scan = scanProject(dir);
625
+ const type = projectInfo.type;
626
+
627
+ if (type === 'web') {
628
+ const components = scan.jsFiles.filter(f => f.includes('component') || f.includes('Component'));
629
+ const pages = scan.jsFiles.filter(f => f.includes('page') || f.includes('Page') || f.includes('route') || f.includes('Route'));
630
+ const assets = scan.files.filter(f => ['.svg', '.png', '.jpg', '.ico'].includes(f.ext));
631
+
632
+ let diagram = 'flowchart TD\n';
633
+ diagram += ' Frontend["Frontend<br/>' + (projectInfo.framework || 'web') + '"]\n';
634
+ diagram += ' Assets["Assets<br/>' + assets.length + ' files"]\n';
635
+ diagram += ' Config["Config<br/>skill.json"]\n';
636
+
637
+ if (components.length > 0) {
638
+ diagram += ' Frontend --> Components["Components<br/>' + components.length + '"]\n';
639
+ }
640
+ if (pages.length > 0) {
641
+ diagram += ' Frontend --> Pages["Pages<br/>' + pages.length + '"]\n';
642
+ }
643
+ diagram += ' Frontend --> Assets\n';
644
+ diagram += ' Frontend --> Config';
645
+ return diagram;
646
+ }
647
+
648
+ if (type === 'python-flet') {
649
+ const coreFiles = scan.pyFiles.filter(f => f.includes('core') && !f.includes('__init__'));
650
+ const uiFiles = scan.pyFiles.filter(f => f.includes('ui') && !f.includes('__init__'));
651
+ const exportFiles = scan.pyFiles.filter(f => f.includes('export') && !f.includes('__init__'));
652
+
653
+ let diagram = 'flowchart TD\n';
654
+ diagram += ' UI["UI<br/>Flet widgets<br/>' + uiFiles.length + ' files"]\n';
655
+ diagram += ' Core["Core<br/>business logic<br/>' + coreFiles.length + ' files"]\n';
656
+ diagram += ' Theme["Theme<br/>G360Theme"]\n';
657
+
658
+ if (exportFiles.length > 0) {
659
+ diagram += ' Core --> Export["Export<br/>' + exportFiles.length + '"]\n';
660
+ }
661
+ diagram += ' UI --> Core\n';
662
+ diagram += ' UI --> Theme\n';
663
+ if (scan.hasCommercialEngine) {
664
+ diagram += ' Core --> Engine["commercial_engine"]\n';
665
+ }
666
+ return diagram;
667
+ }
668
+
669
+ if (type === 'python-cli') {
670
+ const coreFiles = scan.pyFiles.filter(f => !f.includes('__init__'));
671
+ let diagram = 'flowchart TD\n';
672
+ diagram += ' CLI["CLI<br/>argparse<br/>main.py"]\n';
673
+ diagram += ' Core["Core<br/>' + coreFiles.length + ' modules"]\n';
674
+ diagram += ' Config["Config<br/>skill.json"]\n';
675
+ diagram += ' CLI --> Core\n';
676
+ diagram += ' CLI --> Config';
677
+ return diagram;
678
+ }
679
+
680
+ if (type === 'python-customtkinter') {
681
+ const coreFiles = scan.pyFiles.filter(f => !f.includes('__init__'));
682
+ let diagram = 'flowchart TD\n';
683
+ diagram += ' UI["UI<br/>CustomTkinter<br/>main.py"]\n';
684
+ diagram += ' Core["Core<br/>' + coreFiles.length + ' modules"]\n';
685
+ diagram += ' Theme["Theme<br/>G360Theme"]\n';
686
+ diagram += ' UI --> Core\n';
687
+ diagram += ' UI --> Theme';
688
+ return diagram;
689
+ }
690
+
691
+ let diagram = 'flowchart TD\n';
692
+ diagram += ' App["Aplicacion"]\n';
693
+ diagram += ' Config["Configuracion"]\n';
694
+ diagram += ' Assets["Assets"]\n';
695
+ diagram += ' App --> Config\n';
696
+ diagram += ' App --> Assets';
697
+ return diagram;
698
+ }
699
+
700
+ function buildArchitectureDiagram(dir, projectInfo) {
701
+ const scan = scanProject(dir);
702
+ const type = projectInfo.type;
703
+
704
+ if (type === 'web') {
705
+ const components = scan.jsFiles.filter(f => f.includes('component') || f.includes('Component'));
706
+ const pages = scan.jsFiles.filter(f => f.includes('page') || f.includes('Page') || f.includes('route') || f.includes('Route'));
707
+ const styles = scan.files.filter(f => f.ext === '.css' || f.ext === '.scss');
708
+
709
+ let diagram = 'flowchart TD\n';
710
+ diagram += ' subgraph Frontend["Frontend"]\n';
711
+ diagram += ' Index["index.html"]\n';
712
+ if (pages.length > 0) diagram += ' Pages["Pages (' + pages.length + ')"]\n';
713
+ if (components.length > 0) diagram += ' Components["Components (' + components.length + ')"]\n';
714
+ if (styles.length > 0) diagram += ' Styles["Styles (' + styles.length + ')"]\n';
715
+ diagram += ' end\n';
716
+ diagram += ' subgraph Assets["Assets"]\n';
717
+ diagram += ' Brand["Brand / logo"]\n';
718
+ diagram += ' Signature["Signature"]\n';
719
+ diagram += ' Favicon["Favicon"]\n';
720
+ diagram += ' end\n';
721
+ diagram += ' subgraph Config["Config"]\n';
722
+ diagram += ' Skill["skill.json"]\n';
723
+ diagram += ' Manifest["manifest"]\n';
724
+ diagram += ' end\n';
725
+ diagram += ' Frontend --> Assets\n';
726
+ diagram += ' Frontend --> Config';
727
+ return diagram;
728
+ }
729
+
730
+ if (type === 'python-flet') {
731
+ const coreFiles = scan.pyFiles.filter(f => f.includes('core') && !f.includes('__init__'));
732
+ const uiFiles = scan.pyFiles.filter(f => f.includes('ui') && !f.includes('__init__'));
733
+ const exportFiles = scan.pyFiles.filter(f => f.includes('export') && !f.includes('__init__'));
734
+
735
+ let diagram = 'flowchart TD\n';
736
+ diagram += ' subgraph UI["UI Layer"]\n';
737
+ diagram += ' Main["main.py"]\n';
738
+ if (uiFiles.length > 0) diagram += ' Widgets["Widgets (' + uiFiles.length + ')"]\n';
739
+ diagram += ' end\n';
740
+ diagram += ' subgraph Core["Core Layer"]\n';
741
+ if (coreFiles.length > 0) diagram += ' Logic["Logic (' + coreFiles.length + ')"]\n';
742
+ diagram += ' Theme["G360Theme"]\n';
743
+ diagram += ' Signature["g360_signature"]\n';
744
+ diagram += ' end\n';
745
+ diagram += ' subgraph Data["Data"]\n';
746
+ if (scan.hasPyproject) diagram += ' ERP["ERP files (.xls/.xlsx/.csv)"]\n';
747
+ if (exportFiles.length > 0) diagram += ' Export["Excel export (' + exportFiles.length + ')"]\n';
748
+ diagram += ' end\n';
749
+ diagram += ' UI --> Core\n';
750
+ diagram += ' Core --> Data';
751
+ return diagram;
752
+ }
753
+
754
+ if (type === 'python-cli') {
755
+ let diagram = 'flowchart TD\n';
756
+ diagram += ' subgraph CLI["CLI"]\n';
757
+ diagram += ' Entry["main.py"]\n';
758
+ diagram += ' Commands["Commands"]\n';
759
+ diagram += ' end\n';
760
+ diagram += ' subgraph Core["Core"]\n';
761
+ diagram += ' Logic["Business logic"]\n';
762
+ diagram += ' Config["Config"]\n';
763
+ diagram += ' end\n';
764
+ diagram += ' CLI --> Core';
765
+ return diagram;
766
+ }
767
+
768
+ let diagram = 'flowchart TD\n';
769
+ diagram += ' subgraph App["Aplicacion"]\n';
770
+ diagram += ' UI["UI"]\n';
771
+ diagram += ' Core["Core"]\n';
772
+ diagram += ' Data["Data"]\n';
773
+ diagram += ' end\n';
774
+ diagram += ' UI --> Core\n';
775
+ diagram += ' Core --> Data';
776
+ return diagram;
777
+ }
778
+
779
+ function extractImports(content) {
780
+ const imports = [];
781
+ const fromRegex = /^\s*from\s+(\.[\w.]+)\s+import/gm;
782
+ const match = content.match(fromRegex);
783
+ if (match) {
784
+ for (const m of match) {
785
+ const parts = m.match(/from\s+(\.[\w.]+)/);
786
+ if (parts) imports.push(parts[1]);
787
+ }
788
+ }
789
+ return imports;
790
+ }
791
+
792
+ function extractJsImports(content) {
793
+ const imports = [];
794
+ const fromRegex = /^\s*import\s+.*\s+from\s+['"](\.[^'"]+)['"]/gm;
795
+ const requireRegex = /require\(['"](\.[^'"]+)['"]\)/g;
796
+
797
+ const fromMatches = content.match(fromRegex);
798
+ if (fromMatches) {
799
+ for (const m of fromMatches) {
800
+ const parts = m.match(/from\s+['"](\.[^'"]+)['"]/);
801
+ if (parts) imports.push(parts[1]);
802
+ }
803
+ }
804
+
805
+ const requireMatches = content.match(requireRegex);
806
+ if (requireMatches) {
807
+ for (const m of requireMatches) {
808
+ const parts = m.match(/require\(['"](\.[^'"]+)['"]\)/);
809
+ if (parts) imports.push(parts[1]);
810
+ }
811
+ }
812
+
813
+ return imports;
814
+ }
815
+