g360-cli 1.6.0 ā 1.7.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 +71 -10
- package/package.json +1 -1
- package/py/pyproject.toml +34 -0
- package/py/src/g360_core/__init__.py +7 -0
- package/py/src/g360_core/flet/__init__.py +3 -0
- package/py/src/g360_core/flet/ingestion_panel.py +218 -0
- package/py/src/g360_core/ingestion.py +480 -0
- package/src/assets/ingestion/core/__init__.py +0 -0
- package/src/assets/ingestion/core/ingestion.py +480 -0
- package/src/assets/ingestion/ui/__init__.py +0 -0
- package/src/assets/ingestion/ui/ingestion_panel.py +221 -0
- package/src/assets/templates/python-flet/pyproject.toml +2 -0
- package/src/assets/templates/python-flet/src/core/ingestion.py +480 -0
- package/src/assets/templates/python-flet/src/main.py +22 -3
- package/src/assets/templates/python-flet/src/test_ingestion.py +121 -0
- package/src/assets/templates/python-flet/src/ui/ingestion_panel.py +221 -0
- package/src/commands/bring.js +87 -0
- package/src/commands/list.js +18 -1
- package/src/commands/signature.js +25 -8
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import flet as ft
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from g360_core.ingestion import estabilizar_excel_crudo
|
|
9
|
+
except ImportError:
|
|
10
|
+
from core.ingestion import estabilizar_excel_crudo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IngestionPanel(ft.Container):
|
|
14
|
+
def __init__(self, theme, on_data_loaded=None):
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.theme = theme
|
|
17
|
+
self.on_data_loaded = on_data_loaded
|
|
18
|
+
self.df: pd.DataFrame | None = None
|
|
19
|
+
self.metadata: dict | None = None
|
|
20
|
+
self._build()
|
|
21
|
+
|
|
22
|
+
def _build(self):
|
|
23
|
+
self.file_picker = ft.FilePicker(on_result=self._on_file_result)
|
|
24
|
+
self.status_text = ft.Text(
|
|
25
|
+
"Selecciona un archivo .xls o .xlsx del ERP",
|
|
26
|
+
size=14,
|
|
27
|
+
color=self.theme.muted,
|
|
28
|
+
)
|
|
29
|
+
self.stats_container = ft.Column(spacing=4, visible=False)
|
|
30
|
+
self.alertas_container = ft.Column(spacing=2, visible=False)
|
|
31
|
+
self.preview_table = ft.Column(scroll=ft.ScrollMode.AUTO, visible=False)
|
|
32
|
+
|
|
33
|
+
self.content = ft.Column(
|
|
34
|
+
controls=[
|
|
35
|
+
self.file_picker,
|
|
36
|
+
ft.Row(
|
|
37
|
+
controls=[
|
|
38
|
+
self.theme.accent_button(
|
|
39
|
+
text="Cargar Archivo Excel",
|
|
40
|
+
on_click=self._open_picker,
|
|
41
|
+
),
|
|
42
|
+
ft.Container(width=12),
|
|
43
|
+
self.status_text,
|
|
44
|
+
],
|
|
45
|
+
alignment=ft.MainAxisAlignment.START,
|
|
46
|
+
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
|
47
|
+
),
|
|
48
|
+
ft.Container(height=12),
|
|
49
|
+
self.stats_container,
|
|
50
|
+
ft.Container(height=8),
|
|
51
|
+
self.alertas_container,
|
|
52
|
+
ft.Container(height=8),
|
|
53
|
+
self.preview_table,
|
|
54
|
+
],
|
|
55
|
+
spacing=0,
|
|
56
|
+
)
|
|
57
|
+
self.bgcolor = self.theme.surface
|
|
58
|
+
self.border_radius = self.theme.rounded
|
|
59
|
+
self.padding = 20
|
|
60
|
+
self.expand = True
|
|
61
|
+
|
|
62
|
+
def _open_picker(self, e):
|
|
63
|
+
self.file_picker.pick_files(
|
|
64
|
+
file_type=ft.FilePickerFileType.CUSTOM,
|
|
65
|
+
allowed_extensions=["xls", "xlsx", "xlsm"],
|
|
66
|
+
dialog_title="Seleccionar reporte del ERP",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _on_file_result(self, e: ft.FilePickerResultEvent):
|
|
70
|
+
if not e.files:
|
|
71
|
+
self.status_text.value = "No se selecciono ningun archivo."
|
|
72
|
+
self.status_text.color = self.theme.warning
|
|
73
|
+
self.update()
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
archivo = e.files[0]
|
|
77
|
+
ruta = archivo.path
|
|
78
|
+
nombre = archivo.name
|
|
79
|
+
self.status_text.value = f"Procesando: {nombre}..."
|
|
80
|
+
self.status_text.color = self.theme.text
|
|
81
|
+
self.update()
|
|
82
|
+
|
|
83
|
+
self._show_loading(True)
|
|
84
|
+
|
|
85
|
+
def _procesar():
|
|
86
|
+
try:
|
|
87
|
+
df, metadata = estabilizar_excel_crudo(ruta)
|
|
88
|
+
self.df = df
|
|
89
|
+
self.metadata = metadata
|
|
90
|
+
self._mostrar_resultado(df, metadata, nombre)
|
|
91
|
+
if self.on_data_loaded:
|
|
92
|
+
self.on_data_loaded(df, metadata)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
self.status_text.value = f"Error: {exc}"
|
|
95
|
+
self.status_text.color = self.theme.error
|
|
96
|
+
self._show_loading(False)
|
|
97
|
+
self.update()
|
|
98
|
+
|
|
99
|
+
thread = threading.Thread(target=_procesar, daemon=True)
|
|
100
|
+
thread.start()
|
|
101
|
+
|
|
102
|
+
def _show_loading(self, visible: bool):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def _mostrar_resultado(self, df: pd.DataFrame, metadata: dict, nombre: str):
|
|
106
|
+
self.status_text.value = f"OK: {nombre} ({len(df):,} filas)"
|
|
107
|
+
self.status_text.color = self.theme.success
|
|
108
|
+
|
|
109
|
+
columnas = metadata.get("columnas_finales", metadata.get("columnas", []))
|
|
110
|
+
transformaciones = metadata.get("transformaciones", [])
|
|
111
|
+
alertas = metadata.get("alertas", [])
|
|
112
|
+
columnas_nuevas = metadata.get("columnas_nuevas", [])
|
|
113
|
+
|
|
114
|
+
self.stats_container.controls = [
|
|
115
|
+
ft.Text("Resumen de Ingesta", size=16, weight=ft.FontWeight.BOLD, color=self.theme.text),
|
|
116
|
+
ft.Text(f"Filas estabilizadas: {len(df):,}", size=13, color=self.theme.muted),
|
|
117
|
+
ft.Text(f"Columnas originales: {metadata.get('filas_originales', 0)}", size=13, color=self.theme.muted),
|
|
118
|
+
ft.Text(f"Columnas finales: {len(columnas)}", size=13, color=self.theme.muted),
|
|
119
|
+
ft.Text(f"Archivo: {metadata.get('archivo', 'N/A')}", size=13, color=self.theme.muted),
|
|
120
|
+
ft.Text(f"Moneda: {metadata.get('moneda', 'N/A')}", size=13, color=self.theme.muted),
|
|
121
|
+
ft.Divider(color=self.theme.bg, height=8),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
if columnas_nuevas:
|
|
125
|
+
nuevas_text = ft.Text(
|
|
126
|
+
"Columnas derivadas:",
|
|
127
|
+
size=13, weight=ft.FontWeight.BOLD, color=self.theme.accent,
|
|
128
|
+
)
|
|
129
|
+
chips = ft.Row(
|
|
130
|
+
controls=[
|
|
131
|
+
ft.Container(
|
|
132
|
+
content=ft.Text(col, size=10, color=self.theme.bg),
|
|
133
|
+
bgcolor=self.theme.accent,
|
|
134
|
+
border_radius=12,
|
|
135
|
+
padding=ft.padding.only(left=8, right=8, top=3, bottom=3),
|
|
136
|
+
)
|
|
137
|
+
for col in columnas_nuevas
|
|
138
|
+
],
|
|
139
|
+
spacing=6,
|
|
140
|
+
wrap=True,
|
|
141
|
+
)
|
|
142
|
+
self.stats_container.controls.extend([nuevas_text, chips])
|
|
143
|
+
|
|
144
|
+
if transformaciones:
|
|
145
|
+
self.stats_container.controls.append(
|
|
146
|
+
ft.Divider(color=self.theme.bg, height=8)
|
|
147
|
+
)
|
|
148
|
+
self.stats_container.controls.append(
|
|
149
|
+
ft.Text("Transformaciones:", size=13, weight=ft.FontWeight.BOLD, color=self.theme.text)
|
|
150
|
+
)
|
|
151
|
+
for t in transformaciones[-8:]:
|
|
152
|
+
self.stats_container.controls.append(
|
|
153
|
+
ft.Row(
|
|
154
|
+
controls=[
|
|
155
|
+
ft.Container(
|
|
156
|
+
content=ft.Text(">", size=11, color=self.theme.accent),
|
|
157
|
+
width=16,
|
|
158
|
+
),
|
|
159
|
+
ft.Text(t, size=11, color=self.theme.muted),
|
|
160
|
+
],
|
|
161
|
+
spacing=0,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self.stats_container.visible = True
|
|
166
|
+
|
|
167
|
+
if alertas:
|
|
168
|
+
self.alertas_container.controls = [
|
|
169
|
+
ft.Divider(color=self.theme.bg, height=4),
|
|
170
|
+
ft.Text("Alertas:", size=13, weight=ft.FontWeight.BOLD, color=self.theme.warning),
|
|
171
|
+
]
|
|
172
|
+
for a in alertas:
|
|
173
|
+
self.alertas_container.controls.append(
|
|
174
|
+
ft.Row(
|
|
175
|
+
controls=[
|
|
176
|
+
ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, size=14, color=self.theme.warning),
|
|
177
|
+
ft.Text(a, size=11, color=self.theme.warning),
|
|
178
|
+
],
|
|
179
|
+
spacing=4,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
self.alertas_container.visible = True
|
|
183
|
+
|
|
184
|
+
filas_preview = df.head(5)
|
|
185
|
+
columnas_preview = columnas[:8]
|
|
186
|
+
|
|
187
|
+
data_rows = []
|
|
188
|
+
for _, row in filas_preview.iterrows():
|
|
189
|
+
cells = []
|
|
190
|
+
for col in columnas_preview:
|
|
191
|
+
val = row.get(col, "")
|
|
192
|
+
v_str = str(val)[:30] if not pd.isna(val) else "-"
|
|
193
|
+
cells.append(
|
|
194
|
+
ft.DataCell(ft.Text(v_str, size=10, color=self.theme.text))
|
|
195
|
+
)
|
|
196
|
+
data_rows.append(ft.DataRow(cells=cells))
|
|
197
|
+
|
|
198
|
+
header_cells = [
|
|
199
|
+
ft.DataColumn(ft.Text(col[:16], size=10, color=self.theme.accent, weight=ft.FontWeight.BOLD))
|
|
200
|
+
for col in columnas_preview
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
tabla = ft.DataTable(
|
|
204
|
+
columns=header_cells,
|
|
205
|
+
rows=data_rows,
|
|
206
|
+
heading_text_color=self.theme.accent,
|
|
207
|
+
horizontal_margin=4,
|
|
208
|
+
column_spacing=16,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
self.preview_table.controls = [
|
|
212
|
+
ft.Text("Vista previa (5 primeras filas):", size=13, weight=ft.FontWeight.BOLD, color=self.theme.text),
|
|
213
|
+
ft.Container(
|
|
214
|
+
content=tabla,
|
|
215
|
+
bgcolor=self.theme.bg,
|
|
216
|
+
border_radius=8,
|
|
217
|
+
padding=8,
|
|
218
|
+
),
|
|
219
|
+
]
|
|
220
|
+
self.preview_table.visible = True
|
|
221
|
+
self.update()
|
package/src/commands/bring.js
CHANGED
|
@@ -5,6 +5,12 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import { progress } from '../lib/progress.js';
|
|
6
6
|
|
|
7
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const INGESTION_FILES = [
|
|
9
|
+
{ src: 'ingestion/core/ingestion.py', dest: 'src/core/ingestion.py' },
|
|
10
|
+
{ src: 'ingestion/core/__init__.py', dest: 'src/core/__init__.py' },
|
|
11
|
+
{ src: 'ingestion/ui/ingestion_panel.py', dest: 'src/ui/ingestion_panel.py' },
|
|
12
|
+
{ src: 'ingestion/ui/__init__.py', dest: 'src/ui/__init__.py' },
|
|
13
|
+
];
|
|
8
14
|
|
|
9
15
|
export async function bring(asset, options) {
|
|
10
16
|
const { path: targetPath = '.', dryRun = false, force = false } = options;
|
|
@@ -26,6 +32,10 @@ export async function bring(asset, options) {
|
|
|
26
32
|
return;
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
if (asset === 'ingestion') {
|
|
36
|
+
return installIngestion(targetDir, dryRun, force);
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
await copyAssets(assetPath, targetDir, dryRun, force);
|
|
30
40
|
|
|
31
41
|
if (asset.startsWith('brand')) {
|
|
@@ -104,6 +114,83 @@ async function applyBrand(targetDir, brandName, dryRun) {
|
|
|
104
114
|
console.log(chalk.gray(` Signature: ${brand.signature?.text || 'none'}`));
|
|
105
115
|
}
|
|
106
116
|
|
|
117
|
+
async function installIngestion(targetDir, dryRun, force) {
|
|
118
|
+
const assetsPath = path.join(__dirname, '../assets');
|
|
119
|
+
const ingestionDir = path.join(assetsPath, 'ingestion');
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(ingestionDir)) {
|
|
122
|
+
console.error(chalk.red('ā Ingestion asset not found in package.'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const hasFlet = (
|
|
127
|
+
fs.existsSync(path.join(targetDir, 'pyproject.toml')) &&
|
|
128
|
+
fs.existsSync(path.join(targetDir, 'src', 'main.py'))
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (!hasFlet) {
|
|
132
|
+
console.error(chalk.red('ā This command requires a G360 Flet project.'));
|
|
133
|
+
console.log(chalk.gray(' Run "g360 init <name> --template python-flet" first.'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const srcCore = path.join(targetDir, 'src', 'core');
|
|
138
|
+
const srcUi = path.join(targetDir, 'src', 'ui');
|
|
139
|
+
|
|
140
|
+
if (!fs.existsSync(srcCore)) {
|
|
141
|
+
if (!dryRun) fs.mkdirpSync(srcCore);
|
|
142
|
+
console.log(chalk.gray(' Created src/core/'));
|
|
143
|
+
}
|
|
144
|
+
if (!fs.existsSync(srcUi)) {
|
|
145
|
+
if (!dryRun) fs.mkdirpSync(srcUi);
|
|
146
|
+
console.log(chalk.gray(' Created src/ui/'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (dryRun) {
|
|
150
|
+
console.log(chalk.yellow('\nš DRY RUN - Would install:'));
|
|
151
|
+
for (const f of INGESTION_FILES) {
|
|
152
|
+
console.log(chalk.gray(` ${f.dest}`));
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(chalk.cyan('\nš¦ Option A ā Install via pip (recommended):'));
|
|
158
|
+
console.log(chalk.white(' pip install g360-core'));
|
|
159
|
+
|
|
160
|
+
const progressBar = progress('Installing local fallback files...');
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
let copied = 0;
|
|
164
|
+
for (const f of INGESTION_FILES) {
|
|
165
|
+
const srcFile = path.join(assetsPath, f.src);
|
|
166
|
+
const destFile = path.join(targetDir, f.dest);
|
|
167
|
+
|
|
168
|
+
if (fs.existsSync(destFile) && !force) {
|
|
169
|
+
console.log(chalk.yellow(`\nā ${f.dest} exists. Use --force to overwrite.`));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await fs.copy(srcFile, destFile, { overwrite: force });
|
|
174
|
+
copied++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
progressBar.stop();
|
|
178
|
+
|
|
179
|
+
if (copied > 0) {
|
|
180
|
+
console.log(chalk.green(`\nā
${copied} local fallback file(s) installed\n`));
|
|
181
|
+
console.log(chalk.gray(' These files are used when g360-core pip package is not available.'));
|
|
182
|
+
console.log(chalk.cyan('\n Import (auto-detects pip package ā local fallback):'));
|
|
183
|
+
console.log(chalk.white(' from ui.ingestion_panel import IngestionPanel'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log(chalk.cyan('\n Or add to your pyproject.toml:'));
|
|
187
|
+
console.log(chalk.white(' "g360-core>=0.1.0"'));
|
|
188
|
+
} catch (error) {
|
|
189
|
+
progressBar.stop();
|
|
190
|
+
console.error(chalk.red(`\nā Error: ${error.message}`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
107
194
|
async function copyAssets(src, dest, dryRun, force) {
|
|
108
195
|
if (dryRun) {
|
|
109
196
|
console.log(chalk.yellow('š DRY RUN - No files will be copied\n'));
|
package/src/commands/list.js
CHANGED
|
@@ -15,7 +15,8 @@ export async function list(type, options) {
|
|
|
15
15
|
templates: [],
|
|
16
16
|
components: [],
|
|
17
17
|
skills: [],
|
|
18
|
-
brands: []
|
|
18
|
+
brands: [],
|
|
19
|
+
ingestion: []
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
const templatesPath = path.join(assetsPath, 'templates');
|
|
@@ -47,6 +48,11 @@ export async function list(type, options) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
const ingestionPath = path.join(assetsPath, 'ingestion');
|
|
52
|
+
if (fs.existsSync(ingestionPath)) {
|
|
53
|
+
assets.ingestion = ['ingestion'];
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
if (fs.existsSync(snippetsPath)) {
|
|
51
57
|
const snippetsJsonPath = path.join(snippetsPath, 'snippets.json');
|
|
52
58
|
if (fs.existsSync(snippetsJsonPath)) {
|
|
@@ -115,6 +121,17 @@ export async function list(type, options) {
|
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
123
|
|
|
124
|
+
if (!type || type === 'all' || type === 'ingestion') {
|
|
125
|
+
console.log(chalk.bold.yellow('\nš„ Ingestion Modules:'));
|
|
126
|
+
const ingestionPath = path.join(assetsPath, 'ingestion');
|
|
127
|
+
if (fs.existsSync(ingestionPath)) {
|
|
128
|
+
console.log(chalk.gray(' - ingestion (ERP data normalizer for Flet)'));
|
|
129
|
+
console.log(chalk.gray(' Run: g360 bring ingestion'));
|
|
130
|
+
} else {
|
|
131
|
+
console.log(chalk.gray(' No ingestion module found'));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
118
135
|
if (!type || type === 'all' || type === 'brands') {
|
|
119
136
|
console.log(chalk.bold.yellow('\nšØ Brands:'));
|
|
120
137
|
if (assets.brands.length) {
|
|
@@ -231,12 +231,29 @@ async function installFlet(targetDir, { force, mode, version, position }) {
|
|
|
231
231
|
footerCode = `\n # Firma G360\n page.add(g360_footer(${versionAttr.trim() ? versionAttr : ''}))\n`;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
if (!mainContent.includes('g360_signature')) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
if (!mainContent.includes('from core.components.g360_signature')) {
|
|
235
|
+
// Intentar agregar import despues de otros imports de core
|
|
236
|
+
const importRegex = /(from\s+core\.\w+.*\n)/;
|
|
237
|
+
if (importRegex.test(mainContent)) {
|
|
238
|
+
mainContent = mainContent.replace(importRegex, `$1${importLine}\n`);
|
|
239
|
+
} else {
|
|
240
|
+
// Si no hay imports de core, agregar despues del ultimo import
|
|
241
|
+
const lastImport = mainContent.lastIndexOf('import ');
|
|
242
|
+
if (lastImport !== -1) {
|
|
243
|
+
const endOfLine = mainContent.indexOf('\n', lastImport);
|
|
244
|
+
if (endOfLine !== -1) {
|
|
245
|
+
mainContent = mainContent.slice(0, endOfLine + 1) + importLine + '\n' + mainContent.slice(endOfLine + 1);
|
|
246
|
+
} else {
|
|
247
|
+
mainContent = mainContent + '\n' + importLine + '\n';
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
// Si no hay imports, agregar al inicio
|
|
251
|
+
mainContent = importLine + '\n\n' + mainContent;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
239
255
|
|
|
256
|
+
if (!mainContent.includes('g360_footer()') && !mainContent.includes('G360Signature')) {
|
|
240
257
|
if (mainContent.includes('page.add')) {
|
|
241
258
|
const lastAdd = mainContent.lastIndexOf('page.add');
|
|
242
259
|
const endOfLine = mainContent.indexOf('\n', lastAdd);
|
|
@@ -244,9 +261,9 @@ async function installFlet(targetDir, { force, mode, version, position }) {
|
|
|
244
261
|
} else {
|
|
245
262
|
mainContent += footerCode;
|
|
246
263
|
}
|
|
247
|
-
|
|
248
|
-
fs.writeFileSync(mainPyPath, mainContent, 'utf8');
|
|
249
|
-
console.log(chalk.gray(' main.py actualizado'));
|
|
250
264
|
}
|
|
265
|
+
|
|
266
|
+
fs.writeFileSync(mainPyPath, mainContent, 'utf8');
|
|
267
|
+
console.log(chalk.gray(' main.py actualizado'));
|
|
251
268
|
}
|
|
252
269
|
}
|