g360-cli 1.10.0 → 1.11.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/README.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  > CLI tool for bootstrapping G360 projects with standardized structure, assets, and identity
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/g360-cli)](https://www.npmjs.com/package/g360-cli)
5
+ [![npm version](https://img.shields.io/npm/v/g360-cli?color=00d084&label=version)](https://www.npmjs.com/package/g360-cli)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![npm downloads](https://img.shields.io/npm/dm/g360-cli?color=94a3b8)](https://www.npmjs.com/package/g360-cli)
7
8
 
8
9
  ## Tabla de Contenidos
9
10
 
@@ -67,6 +68,12 @@ CLI tool para el ecosistema G360 que permite inicializar proyectos con estructur
67
68
 
68
69
  ---
69
70
 
71
+ ## Versión
72
+
73
+ **Current: v1.11.0** — [Ver en npm](https://www.npmjs.com/package/g360-cli)
74
+
75
+ ---
76
+
70
77
  ## Instalación
71
78
 
72
79
  ### Requisitos
@@ -84,9 +91,19 @@ npm install -g g360-cli
84
91
 
85
92
  ```bash
86
93
  g360 --version
94
+ # → 1.11.0
95
+
87
96
  g360 health
88
97
  ```
89
98
 
99
+ ### Publicar nueva versión
100
+
101
+ ```bash
102
+ npm version patch # o minor / major
103
+ git push --tags
104
+ npm publish
105
+ ```
106
+
90
107
  ---
91
108
 
92
109
  ## Inicio Rápido
@@ -629,6 +646,10 @@ mi-app/
629
646
  - **Precio efectivo**: PRECIO_BASE (físico) y RECARGO_UNITARIO (financiero) separados
630
647
  - Cruce de NC/NDB contra facturas referenciadas para determinar ajustes de precio por línea
631
648
  - Purga de filas total/general/acumulado
649
+ - **Columnas derivadas para UI**: `cliente_label`, `vendedor_label`, `articulo_label`, `linea_label`, etc. (ID - NOMBRE)
650
+ - **Cliente completo**: `cliente_full_label` = ID_CLIENTE - DOC_CLIENTE_CLEAN - NOM_CLIENTE
651
+ - **Código de factura**: `doc_completo` = TPO_DOC + SERIE_DOC + NRO_DOC (ej: "F204-56287")
652
+ - **Precio unitario**: `precio_base` = SOLES / CANTIDAD
632
653
 
633
654
  ### python-flet-migrate
634
655
 
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "g360-cli",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, identity, and ERP data processing",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
7
7
  "bin": {
8
8
  "g360": "src/cli.js"
9
9
  },
10
+ "files": [
11
+ "src/",
12
+ "!src/assets/templates/**/node_modules",
13
+ "!src/assets/templates/**/package-lock.json",
14
+ "py/",
15
+ "README.md",
16
+ "LICENSE",
17
+ "package.json"
18
+ ],
10
19
  "scripts": {
11
20
  "prepublishOnly": "node -e \"console.log('Use: npm install -g g360-cli')\"",
12
21
  "test": "vitest",
@@ -15,7 +24,7 @@
15
24
  "test:coverage": "vitest --coverage",
16
25
  "build": "npm run build:portable",
17
26
  "build:portable": "pkg . --targets node18-win-x64 --output dist/g360.exe",
18
- "postinstall": "echo '✅ g360-cli instalado. Asegúrate de tener Python 3.11+ y pip instalados.'"
27
+ "postinstall": "node -e \"try{require('child_process').execSync('python --version',{encoding:'utf8',stdio:'pipe'})}catch{console.log('⚠️ Python no detectado. Instala Python 3.11+ para funcionalidad completa.')}\""
19
28
  },
20
29
  "pkg": {
21
30
  "assets": [
@@ -25,6 +25,17 @@ _KEYWORDS_CANTIDAD = [
25
25
  ]
26
26
  _KEYWORDS_FECHA = ["fecha", "fec_", "venc"]
27
27
 
28
+ _ENTITY_LABELS = {
29
+ ("id_cliente", "nom_cliente"): "cliente_label",
30
+ ("id_vendedor", "nom_vendedor"): "vendedor_label",
31
+ ("id_articulo", "nom_articulo"): "articulo_label",
32
+ ("id_linea", "nom_linea"): "linea_label",
33
+ ("id_grupo", "nom_grupo"): "grupo_label",
34
+ ("id_tipo", "nom_tipo"): "tipo_label",
35
+ ("id_familia", "nom_familia"): "familia_label",
36
+ ("cod_sucursal", "nom_sucursal"): "sucursal_label",
37
+ }
38
+
28
39
  _MAPA_TPO_DOC = {
29
40
  "NCR": "NOTA DE CREDITO",
30
41
  "NDB": "NOTA DE DEBITO",
@@ -247,6 +258,26 @@ def _clasificar_transaccion(row: dict) -> str:
247
258
  return "indefinido"
248
259
 
249
260
 
261
+ def _build_entity_labels(df: pd.DataFrame) -> pd.DataFrame:
262
+ for (id_col, name_col), label_col in _ENTITY_LABELS.items():
263
+ id_exists = id_col in df.columns
264
+ name_exists = name_col in df.columns
265
+
266
+ if id_exists and name_exists:
267
+ id_vals = df[id_col].fillna("").astype(str).str.strip()
268
+ name_vals = df[name_col].fillna("").astype(str).str.strip()
269
+ df[label_col] = np.where(
270
+ (id_vals != "") & (name_vals != ""),
271
+ id_vals + " - " + name_vals,
272
+ np.where(id_vals != "", id_vals, name_vals)
273
+ )
274
+ elif id_exists:
275
+ df[label_col] = df[id_col].astype(str).str.strip()
276
+ elif name_exists:
277
+ df[label_col] = df[name_col].astype(str).str.strip()
278
+ return df
279
+
280
+
250
281
  def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dict]:
251
282
  ruta = Path(ruta_archivo)
252
283
  if not ruta.exists():
@@ -359,6 +390,22 @@ def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dic
359
390
  f"{col}: normalizado ({ruc_count} RUC, {dni_count} DNI, "
360
391
  f"{(~df[tipo_col].isin(['RUC','DNI'])).sum()} otros)"
361
392
  )
393
+
394
+ # ── 9b. CLIENTE_FULL_LABEL = ID + DOC_CLIENTE_CLEAN + NOMBRE ──
395
+ if "id_cliente" in df.columns and "doc_cliente_clean" in df.columns:
396
+ id_vals = df["id_cliente"].fillna("").astype(str).str.strip()
397
+ doc_vals = df["doc_cliente_clean"].fillna("").astype(str).str.strip()
398
+ name_vals = df["nom_cliente"].fillna("").astype(str).str.strip() if "nom_cliente" in df.columns else None
399
+
400
+ if name_vals is not None:
401
+ df["cliente_full_label"] = np.where(
402
+ (id_vals != "") & (doc_vals != "") & (name_vals != ""),
403
+ id_vals + " - " + doc_vals + " - " + name_vals,
404
+ np.where((id_vals != "") & (doc_vals != ""), id_vals + " - " + doc_vals, id_vals)
405
+ )
406
+ else:
407
+ df["cliente_full_label"] = id_vals + " - " + doc_vals
408
+ transformaciones.append("cliente_full_label: ID + DOC_CLIENTE_CLEAN + NOM_CLIENTE")
362
409
 
363
410
  # ── 10. Columnas TEXTO ──
364
411
  for col in df.columns:
@@ -25,6 +25,17 @@ _KEYWORDS_CANTIDAD = [
25
25
  ]
26
26
  _KEYWORDS_FECHA = ["fecha", "fec_", "venc"]
27
27
 
28
+ _ENTITY_LABELS = {
29
+ ("id_cliente", "nom_cliente"): "cliente_label",
30
+ ("id_vendedor", "nom_vendedor"): "vendedor_label",
31
+ ("id_articulo", "nom_articulo"): "articulo_label",
32
+ ("id_linea", "nom_linea"): "linea_label",
33
+ ("id_grupo", "nom_grupo"): "grupo_label",
34
+ ("id_tipo", "nom_tipo"): "tipo_label",
35
+ ("id_familia", "nom_familia"): "familia_label",
36
+ ("cod_sucursal", "nom_sucursal"): "sucursal_label",
37
+ }
38
+
28
39
  _MAPA_TPO_DOC = {
29
40
  "NCR": "NOTA DE CREDITO",
30
41
  "NDB": "NOTA DE DEBITO",
@@ -247,6 +258,26 @@ def _clasificar_transaccion(row: dict) -> str:
247
258
  return "indefinido"
248
259
 
249
260
 
261
+ def _build_entity_labels(df: pd.DataFrame) -> pd.DataFrame:
262
+ for (id_col, name_col), label_col in _ENTITY_LABELS.items():
263
+ id_exists = id_col in df.columns
264
+ name_exists = name_col in df.columns
265
+
266
+ if id_exists and name_exists:
267
+ id_vals = df[id_col].fillna("").astype(str).str.strip()
268
+ name_vals = df[name_col].fillna("").astype(str).str.strip()
269
+ df[label_col] = np.where(
270
+ (id_vals != "") & (name_vals != ""),
271
+ id_vals + " - " + name_vals,
272
+ np.where(id_vals != "", id_vals, name_vals)
273
+ )
274
+ elif id_exists:
275
+ df[label_col] = df[id_col].astype(str).str.strip()
276
+ elif name_exists:
277
+ df[label_col] = df[name_col].astype(str).str.strip()
278
+ return df
279
+
280
+
250
281
  def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dict]:
251
282
  ruta = Path(ruta_archivo)
252
283
  if not ruta.exists():
@@ -359,6 +390,22 @@ def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dic
359
390
  f"{col}: normalizado ({ruc_count} RUC, {dni_count} DNI, "
360
391
  f"{(~df[tipo_col].isin(['RUC','DNI'])).sum()} otros)"
361
392
  )
393
+
394
+ # ── 9b. CLIENTE_FULL_LABEL = ID + DOC_CLIENTE_CLEAN + NOMBRE ──
395
+ if "id_cliente" in df.columns and "doc_cliente_clean" in df.columns:
396
+ id_vals = df["id_cliente"].fillna("").astype(str).str.strip()
397
+ doc_vals = df["doc_cliente_clean"].fillna("").astype(str).str.strip()
398
+ name_vals = df["nom_cliente"].fillna("").astype(str).str.strip() if "nom_cliente" in df.columns else None
399
+
400
+ if name_vals is not None:
401
+ df["cliente_full_label"] = np.where(
402
+ (id_vals != "") & (doc_vals != "") & (name_vals != ""),
403
+ id_vals + " - " + doc_vals + " - " + name_vals,
404
+ np.where((id_vals != "") & (doc_vals != ""), id_vals + " - " + doc_vals, id_vals)
405
+ )
406
+ else:
407
+ df["cliente_full_label"] = id_vals + " - " + doc_vals
408
+ transformaciones.append("cliente_full_label: ID + DOC_CLIENTE_CLEAN + NOM_CLIENTE")
362
409
 
363
410
  # ── 10. Columnas TEXTO ──
364
411
  for col in df.columns:
@@ -386,6 +433,18 @@ def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dic
386
433
  df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0.0)
387
434
  transformaciones.append(f"{col}: forzado a float64, NaN → 0.0")
388
435
 
436
+ # ── 12b. PRECIO_BASE (precio unitario por fila) ──
437
+ if "soles" in df.columns and cols_cantidad:
438
+ cant_col = cols_cantidad[0]
439
+ cant = df[cant_col].fillna(0)
440
+ soles = df["soles"].fillna(0)
441
+ df["precio_base"] = np.where(
442
+ cant != 0,
443
+ np.round(soles / cant.abs(), 4),
444
+ np.nan
445
+ )
446
+ transformaciones.append(f"precio_base: SOLES / {cant_col}")
447
+
389
448
  # ── 13. cantidad + cantidad_fae → cantidad_total, tipo_transaccion ──
390
449
  if "cantidad" in df.columns:
391
450
  if "cantidad_fae" in df.columns:
@@ -397,6 +456,13 @@ def estabilizar_excel_crudo(ruta_archivo: str | Path) -> tuple[pd.DataFrame, dic
397
456
  t_counts = df["tipo_transaccion"].value_counts().to_dict()
398
457
  transformaciones.append(f"tipo_transaccion: {t_counts}")
399
458
 
459
+ # ── 13b. Construir columnas _LABEL (ID - Nombre) para UI ──
460
+ cols_before_labels = list(df.columns)
461
+ df = _build_entity_labels(df)
462
+ label_cols_added = [c for c in df.columns if c.endswith("_label") and c not in cols_before_labels]
463
+ if label_cols_added:
464
+ transformaciones.append(f"labels: {', '.join(label_cols_added)} generados")
465
+
400
466
  # ── 14. Columnas FECHA ──
401
467
  for col in df.columns:
402
468
  if _coincide_keywords(col, _KEYWORDS_FECHA) and col != "fec_":
@@ -46,9 +46,12 @@ _SAMPLE_DF = pd.DataFrame({
46
46
  "ANHO": [2026.0, 2026.0, np.nan],
47
47
  "ID_ARTICULO": ["11030", "78456", np.nan],
48
48
  "NOM_CLIENTE": [" CLIENTE A ", "CLIENTE B", np.nan],
49
+ "ID_CLIENTE": ["001", "002", np.nan],
50
+ "DOC_CLIENTE": ["20491653745", "12345678", np.nan],
49
51
  "SOLES": [1172.10, "S/.2,500.50", np.nan],
50
52
  "FECHA_ORIG": ["24/06/2026", "20/06/2026", np.nan],
51
53
  "ID_LINEA": ["0111", "0178", np.nan],
54
+ "NOM_LINEA": ["LINEA A", "LINEA B", np.nan],
52
55
  })
53
56
 
54
57
 
@@ -104,6 +107,17 @@ class TestEstabilizarExcelCrudo(unittest.TestCase):
104
107
  with self.assertRaises(FileNotFoundError):
105
108
  estabilizar_excel_crudo("no_existe_archivo_falso_999.xls")
106
109
 
110
+ def test_cliente_full_label_creado(self):
111
+ df, _ = _patched_ingestion(_SAMPLE_DF)
112
+ self.assertIn("cliente_full_label", df.columns)
113
+ self.assertEqual(df["cliente_full_label"].iloc[0], "001 - 20491653745 - CLIENTE A")
114
+
115
+ def test_labels_generados(self):
116
+ df, meta = _patched_ingestion(_SAMPLE_DF)
117
+ self.assertIn("cliente_label", df.columns)
118
+ self.assertIn("linea_label", df.columns)
119
+ self.assertIn("cliente_full_label", df.columns)
120
+
107
121
 
108
122
  class TestColumnasDuplicadas(unittest.TestCase):
109
123
  def test_duplicados_se_sufijan(self):
package/src/cli.js CHANGED
@@ -21,6 +21,42 @@ import { validate } from './commands/validate.js';
21
21
  import { ingest } from './commands/ingest.js';
22
22
  import { addon } from './commands/addon.js';
23
23
 
24
+ // Comando config (no requiere archivo separado)
25
+ function configAction(options) {
26
+ const { get, set, list } = options;
27
+ if (list) {
28
+ console.log(chalk.bold.cyan('\n⚙️ G360 Config\n'));
29
+ console.log(chalk.white(' g360-signature:'));
30
+ console.log(chalk.gray(' mode: powered, own'));
31
+ console.log(chalk.gray(' positions: bottom-right, bottom-left, bottom-center, footer-right, footer-left'));
32
+ console.log(chalk.white(' templates:'));
33
+ console.log(chalk.gray(' web-pwa, lit-web, solid-web, svelte-web,'));
34
+ console.log(chalk.gray(' python-cli, python-flet, python-flet-migrate, python-customtkinter, vba-excel'));
35
+ console.log(chalk.white(' skills:'));
36
+ console.log(chalk.gray(' corporativo, corporativo-movil, corporativo-g360, corporativo-g360-movil,'));
37
+ console.log(chalk.gray(' moderno, moderno-movil, minimalista, custom,'));
38
+ console.log(chalk.gray(' flet-desktop, flet-desktop-corporativo\n'));
39
+ return;
40
+ }
41
+ if (get) {
42
+ console.log(chalk.bold.cyan('\n⚙️ G360 Config\n'));
43
+ console.log(chalk.white(` ${get}: `) + chalk.gray('(config value placeholder)'));
44
+ console.log(chalk.gray(' Use: g360 config --list to see all options\n'));
45
+ return;
46
+ }
47
+ if (set) {
48
+ const [key, value] = set.split('=');
49
+ if (key && value) {
50
+ console.log(chalk.bold.cyan('\n⚙️ G360 Config\n'));
51
+ console.log(chalk.green(` ✅ Config set: ${chalk.white(key)} = ${chalk.white(value)}\n`));
52
+ } else {
53
+ console.log(chalk.red('❌ Invalid format. Use: g360 config --set key=value\n'));
54
+ }
55
+ return;
56
+ }
57
+ console.log(chalk.yellow('\n⚠️ Use --list, --get <key>, or --set <key>=<value>\n'));
58
+ }
59
+
24
60
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
61
  const pkg = fs.readJsonSync(path.join(__dirname, '../package.json'));
26
62
 
@@ -143,6 +179,14 @@ program
143
179
  .option('-o, --output <archivo>', 'Ruta de salida', 'maestro_ventas_crm.csv')
144
180
  .action(ingest);
145
181
 
182
+ program
183
+ .command('config')
184
+ .description('View or modify G360 configuration')
185
+ .option('--list', 'List all configuration options')
186
+ .option('--get <key>', 'Get a configuration value')
187
+ .option('--set <key=value>', 'Set a configuration value')
188
+ .action(configAction);
189
+
146
190
  program
147
191
  .command('addon')
148
192
  .argument('<command>', 'Command: install, list, remove')
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @file addon.test.js
3
+ * @description Tests para el comando addon
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
7
+
8
+ describe('addon command', () => {
9
+ describe('validation', () => {
10
+ it('should export addon function', async () => {
11
+ const { addon } = await import('../commands/addon.js');
12
+ expect(typeof addon).toBe('function');
13
+ });
14
+
15
+ it('should accept command and options parameters', async () => {
16
+ const { addon } = await import('../commands/addon.js');
17
+ expect(addon.length).toBe(2);
18
+ });
19
+ });
20
+
21
+ describe('commands', () => {
22
+ it('should handle install command', async () => {
23
+ const { addon } = await import('../commands/addon.js');
24
+ expect(addon).toHaveProperty('name', undefined);
25
+ });
26
+
27
+ it('should handle list command', async () => {
28
+ const { addon } = await import('../commands/addon.js');
29
+ expect(typeof addon).toBe('function');
30
+ });
31
+
32
+ it('should handle remove command', async () => {
33
+ const { addon } = await import('../commands/addon.js');
34
+ expect(addon).toBeDefined();
35
+ });
36
+ });
37
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @file audit.test.js
3
+ * @description Tests para el comando audit
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
7
+
8
+ describe('audit command', () => {
9
+ describe('validation', () => {
10
+ it('should have audit function exported', async () => {
11
+ const { audit } = await import('../commands/audit.js');
12
+ expect(typeof audit).toBe('function');
13
+ });
14
+
15
+ it('should accept projectPath and options parameters', async () => {
16
+ const { audit } = await import('../commands/audit.js');
17
+ expect(audit.length).toBe(2);
18
+ });
19
+ });
20
+
21
+ describe('options', () => {
22
+ it('should support fix option', async () => {
23
+ const { audit } = await import('../commands/audit.js');
24
+ expect(audit).toBeDefined();
25
+ });
26
+
27
+ it('should support verbose option', async () => {
28
+ const { audit } = await import('../commands/audit.js');
29
+ expect(audit).toBeDefined();
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @file bring.test.js
3
+ * @description Tests para el comando bring
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
7
+
8
+ describe('bring command', () => {
9
+ describe('validation', () => {
10
+ it('should have bring function exported', async () => {
11
+ const { bring } = await import('../commands/bring.js');
12
+ expect(typeof bring).toBe('function');
13
+ });
14
+
15
+ it('should accept asset and options parameters', async () => {
16
+ const { bring } = await import('../commands/bring.js');
17
+ expect(bring.length).toBe(2);
18
+ });
19
+ });
20
+
21
+ describe('options', () => {
22
+ it('should support path option', async () => {
23
+ const { bring } = await import('../commands/bring.js');
24
+ expect(bring).toBeDefined();
25
+ });
26
+
27
+ it('should support dryRun option', async () => {
28
+ const { bring } = await import('../commands/bring.js');
29
+ expect(bring).toBeDefined();
30
+ });
31
+
32
+ it('should support force option', async () => {
33
+ const { bring } = await import('../commands/bring.js');
34
+ expect(bring).toBeDefined();
35
+ });
36
+ });
37
+ });
@@ -40,10 +40,21 @@ export async function init(name, options) {
40
40
 
41
41
  const targetDir = path.join(process.cwd(), dir, name);
42
42
 
43
+ // Leer version del CLI desde package.json
44
+ const cliPkgPath = path.join(__dirname, '..', '..', 'package.json');
45
+ let cliVersion = '1.0.0';
46
+ try {
47
+ const cliPkg = fs.readJsonSync(cliPkgPath);
48
+ cliVersion = cliPkg.version || '1.0.0';
49
+ } catch {
50
+ // fallback si no se puede leer
51
+ }
52
+
43
53
  console.log(chalk.bold.cyan('\n🚀 G360 Project Initialization\n'));
44
54
  console.log(`Project: ${chalk.yellow(name)}`);
45
55
  console.log(`Template: ${chalk.blue(template)}`);
46
56
  console.log(`Skill: ${chalk.magenta(skill)}`);
57
+ console.log(`CLI Version: ${chalk.gray(cliVersion)}`);
47
58
  console.log(`Target: ${chalk.gray(targetDir)}\n`);
48
59
 
49
60
  let wantPortable = false;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @file init.test.js
3
+ * @description Tests para el comando init
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+
8
+ describe('init command', () => {
9
+ it('should export init function', async () => {
10
+ const mod = await import('../commands/init.js');
11
+ expect(mod).toHaveProperty('init');
12
+ expect(typeof mod.init).toBe('function');
13
+ });
14
+
15
+ it('should accept name and options parameters', async () => {
16
+ const { init } = await import('../commands/init.js');
17
+ expect(init.length).toBe(2);
18
+ });
19
+ });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @file list.test.js
3
+ * @description Tests para el comando list
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
7
+ import fs from 'fs-extra';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { list } from '../commands/list.js';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ // Mock de fs-extra
15
+ vi.mock('fs-extra', () => ({
16
+ default: {
17
+ existsSync: vi.fn(),
18
+ readdirSync: vi.fn(),
19
+ readJsonSync: vi.fn()
20
+ }
21
+ }));
22
+
23
+ describe('list command', () => {
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ });
27
+
28
+ describe('list templates', () => {
29
+ it('should list available templates', async () => {
30
+ const mockTemplates = ['web-pwa', 'svelte-web', 'solid-web'];
31
+ fs.existsSync.mockReturnValue(true);
32
+ fs.readdirSync.mockReturnValue(mockTemplates);
33
+
34
+ await list('templates', { json: false });
35
+
36
+ expect(fs.readdirSync).toHaveBeenCalled();
37
+ });
38
+
39
+ it('should return empty array when templates directory does not exist', async () => {
40
+ fs.existsSync.mockReturnValue(false);
41
+
42
+ await list('templates', { json: false });
43
+
44
+ expect(fs.readdirSync).not.toHaveBeenCalled();
45
+ });
46
+ });
47
+
48
+ describe('list skills', () => {
49
+ it('should list skills from g360-skills.json', async () => {
50
+ const mockSkills = {
51
+ skills: [
52
+ { name: 'corporativo', description: 'Proyectos corporativos', device: 'pc' },
53
+ { name: 'moderno', description: 'Herramientas modernas', device: 'movil' }
54
+ ]
55
+ };
56
+
57
+ fs.existsSync.mockReturnValue(true);
58
+ fs.readJsonSync.mockReturnValue(mockSkills);
59
+
60
+ await list('skills', { json: false });
61
+
62
+ expect(fs.readJsonSync).toHaveBeenCalled();
63
+ });
64
+
65
+ it('should handle missing g360-skills.json gracefully', async () => {
66
+ fs.existsSync.mockReturnValue(false);
67
+
68
+ await list('skills', { json: false });
69
+
70
+ expect(fs.readJsonSync).not.toHaveBeenCalled();
71
+ });
72
+ });
73
+
74
+ describe('list snippets', () => {
75
+ it('should list snippets from snippets.json', async () => {
76
+ const mockSnippets = {
77
+ snippets: [
78
+ { name: 'cli-argparse-basic', description: 'Basic argparse CLI', language: 'python' },
79
+ { name: 'g360-button', description: 'G360 styled button', language: 'html' }
80
+ ]
81
+ };
82
+
83
+ fs.existsSync.mockReturnValue(true);
84
+ fs.readJsonSync.mockReturnValue(mockSnippets);
85
+
86
+ await list('snippets', { json: false });
87
+
88
+ expect(fs.readJsonSync).toHaveBeenCalled();
89
+ });
90
+
91
+ it('should handle missing snippets.json gracefully', async () => {
92
+ fs.existsSync.mockReturnValue(false);
93
+
94
+ await list('snippets', { json: false });
95
+
96
+ expect(fs.readJsonSync).not.toHaveBeenCalled();
97
+ });
98
+ });
99
+
100
+ describe('list all', () => {
101
+ it('should list all asset types', async () => {
102
+ const mockTemplates = ['web-pwa', 'svelte-web'];
103
+ const mockSkills = {
104
+ skills: [{ name: 'corporativo', description: 'Proyectos corporativos', device: 'pc' }]
105
+ };
106
+ const mockSnippets = {
107
+ snippets: [{ name: 'cli-argparse-basic', description: 'Basic argparse', language: 'python' }]
108
+ };
109
+
110
+ fs.existsSync.mockReturnValue(true);
111
+ fs.readdirSync.mockReturnValue(mockTemplates);
112
+ fs.readJsonSync.mockReturnValue(mockSkills).mockReturnValueOnce(mockSnippets);
113
+
114
+ await list('all', { json: false });
115
+
116
+ expect(fs.readdirSync).toHaveBeenCalled();
117
+ expect(fs.readJsonSync).toHaveBeenCalled();
118
+ });
119
+ });
120
+
121
+ describe('JSON output', () => {
122
+ it('should output JSON when requested', async () => {
123
+ const mockTemplates = ['web-pwa'];
124
+ fs.existsSync.mockReturnValue(true);
125
+ fs.readdirSync.mockReturnValue(mockTemplates);
126
+
127
+ await list('templates', { json: true });
128
+
129
+ expect(fs.readdirSync).toHaveBeenCalled();
130
+ });
131
+ });
132
+ });