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 +22 -1
- package/package.json +11 -2
- package/src/assets/ingestion/core/__pycache__/ingestion.cpython-312.pyc +0 -0
- package/src/assets/ingestion/core/ingestion.py +47 -0
- package/src/assets/templates/python-flet/src/core/ingestion.py +66 -0
- package/src/assets/templates/python-flet/src/test_ingestion.py +14 -0
- package/src/cli.js +44 -0
- package/src/commands/addon.test.js +37 -0
- package/src/commands/audit.test.js +32 -0
- package/src/commands/bring.test.js +37 -0
- package/src/commands/init.js +11 -0
- package/src/commands/init.test.js +19 -0
- package/src/commands/list.test.js +132 -0
- package/src/commands/set-skill.js +21 -17
- package/src/commands/set-skill.test.js +134 -0
- package/src/commands/signature.js +46 -26
- package/src/commands/update.js +14 -2
- package/src/lib/asset-validator.test.js +237 -0
- package/src/lib/manifest.test.js +173 -0
- package/src/lib/validator.test.js +115 -0
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
|
-
[](https://www.npmjs.com/package/g360-cli)
|
|
5
|
+
[](https://www.npmjs.com/package/g360-cli)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](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.
|
|
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": "
|
|
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": [
|
|
Binary file
|
|
@@ -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
|
+
});
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
+
});
|