entexto-cli 1.4.2 → 1.4.4

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.
@@ -85,13 +85,17 @@ module.exports = async function pull(options) {
85
85
 
86
86
  // Escribir archivos localmente
87
87
  let escritos = 0;
88
- for (const { ruta, content } of data.archivos) {
88
+ for (const { ruta, content, isBinary } of data.archivos) {
89
89
  const destFile = path.join(absDir, ruta);
90
90
  const dir = path.dirname(destFile);
91
91
 
92
92
  try {
93
93
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
94
- fs.writeFileSync(destFile, content, 'utf8');
94
+ if (isBinary) {
95
+ fs.writeFileSync(destFile, Buffer.from(content, 'base64'));
96
+ } else {
97
+ fs.writeFileSync(destFile, content, 'utf8');
98
+ }
95
99
  escritos++;
96
100
  } catch (err) {
97
101
  console.warn(chalk.yellow(` ⚠ No se pudo escribir: ${ruta} — ${err.message}`));
@@ -178,19 +178,24 @@ module.exports = async function sync(options) {
178
178
  try {
179
179
  const pullData = await pullProject(uuid);
180
180
  const archivos = pullData.archivos || [];
181
- const contentMap = new Map(archivos.map(a => [a.ruta, a.content]));
181
+ const contentMap = new Map(archivos.map(a => [a.ruta, a]));
182
182
  sp.stop();
183
183
  let downloaded = 0;
184
184
  for (const ruta of aDescargar) {
185
- const contenido = contentMap.get(ruta);
186
- if (contenido === undefined) {
185
+ const entrada = contentMap.get(ruta);
186
+ if (!entrada) {
187
187
  console.log(' ' + chalk.yellow('⚠') + ' No encontrado en pull: ' + chalk.gray(ruta));
188
188
  continue;
189
189
  }
190
+ const contenido = entrada.content;
190
191
  const destFile = path.join(absDir, ruta);
191
192
  const destDir = path.dirname(destFile);
192
193
  if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
193
- fs.writeFileSync(destFile, contenido, 'utf8');
194
+ if (entrada.isBinary) {
195
+ fs.writeFileSync(destFile, Buffer.from(contenido, 'base64'));
196
+ } else {
197
+ fs.writeFileSync(destFile, contenido, 'utf8');
198
+ }
194
199
  localUploadedAt.set(ruta, Date.now()); // evitar re-subida por chokidar
195
200
  console.log(' ' + chalk.cyan('↓') + ' ' + chalk.white(ruta) + chalk.gray(' ← remoto'));
196
201
  downloaded++;
@@ -231,21 +236,52 @@ module.exports = async function sync(options) {
231
236
  let sseStream = null;
232
237
  let sseReconnectTimer = null;
233
238
 
239
+ // Reintenta una operación FS hasta maxRetries veces con delay ms entre intentos.
240
+ // Necesario en Windows donde los archivos abiertos en VS Code quedan bloqueados (EPERM/EBUSY).
241
+ // retryFsOp: reintenta una operación FS hasta maxRetries veces si el archivo
242
+ // está bloqueado (EBUSY/EPERM/EACCES). Si se agotan los intentos, llama a
243
+ // onFail() para que el caller pueda limpiar cualquier guard de estado
244
+ // (ej. localDeletedAt) y no quede desincronizado.
245
+ function retryFsOp(opName, ruta, fn, maxRetries, delay, onFail) {
246
+ let attempt = 0;
247
+ function tryOnce() {
248
+ try {
249
+ fn();
250
+ // Éxito: no se llama onFail
251
+ } catch (err) {
252
+ const locked = err.code === 'EBUSY' || err.code === 'EPERM' || err.code === 'EACCES';
253
+ if (locked && attempt < maxRetries) {
254
+ attempt++;
255
+ if (attempt === 1) {
256
+ console.log(' ' + chalk.yellow('⏳') + ' ' + chalk.white(ruta) + chalk.gray(' bloqueado por otro proceso, reintentando...'));
257
+ }
258
+ setTimeout(tryOnce, delay);
259
+ } else if (locked) {
260
+ console.log(' ' + chalk.red('✖') + ' ' + chalk.white(ruta) + chalk.red(' bloqueado — cierra el archivo e intenta de nuevo'));
261
+ if (typeof onFail === 'function') onFail();
262
+ } else {
263
+ console.log(' ' + chalk.red('✖') + ' ' + opName + ' fallido (' + (err.code || err.message) + '): ' + ruta);
264
+ if (typeof onFail === 'function') onFail();
265
+ }
266
+ }
267
+ }
268
+ tryOnce();
269
+ }
270
+
234
271
  function manejarEventoRemoto(eventType, data) {
235
272
  if (eventType === 'file-delete') {
236
273
  const { ruta } = data || {};
237
274
  if (!ruta) return;
238
275
  const destFile = path.join(absDir, ruta);
239
- // ⚠️ El guard DEBE ponerse ANTES de unlinkSync porque chokidar
240
- // detecta el 'unlink' síncronamente y dispararía eliminarArchivoRemoto
241
- // antes de que el guard estuviera listo (race condition).
276
+ // ⚠️ Guard ANTES de la operación FS: chokidar detecta el 'unlink'
277
+ // síncronamente y dispararía eliminarArchivoRemoto antes de que el
278
+ // guard estuviera listo (race condition).
242
279
  localDeletedAt.set(ruta, Date.now());
243
- try {
244
- if (fs.existsSync(destFile)) {
245
- fs.unlinkSync(destFile);
246
- console.log(' ' + chalk.red('✖') + ' ' + chalk.white(ruta) + chalk.gray(' ← borrado remoto'));
247
- }
248
- } catch { console.log(' ' + chalk.red('✖') + ' No se pudo borrar: ' + ruta); }
280
+ if (!fs.existsSync(destFile)) return;
281
+ retryFsOp('borrar', ruta, () => {
282
+ fs.unlinkSync(destFile);
283
+ console.log(' ' + chalk.red('✖') + ' ' + chalk.white(ruta) + chalk.gray(' ← borrado remoto'));
284
+ }, 5, 500, () => localDeletedAt.delete(ruta));
249
285
  return;
250
286
  }
251
287
  if (eventType === 'file-rename') {
@@ -254,22 +290,31 @@ module.exports = async function sync(options) {
254
290
  const oldFile = path.join(absDir, ruta_antigua);
255
291
  const newFile = path.join(absDir, ruta_nueva);
256
292
  const newDir = path.dirname(newFile);
257
- // ⚠️ Guards ANTES de renameSync por el mismo motivo (race con chokidar).
293
+ // ⚠️ Guards ANTES de la operación FS por el mismo motivo.
258
294
  localDeletedAt.set(ruta_antigua, Date.now());
259
295
  localUploadedAt.set(ruta_nueva, Date.now());
260
- try {
261
- if (fs.existsSync(oldFile)) {
262
- if (!fs.existsSync(newDir)) fs.mkdirSync(newDir, { recursive: true });
263
- fs.renameSync(oldFile, newFile);
264
- console.log(' ' + chalk.yellow('↔') + ' ' + chalk.white(ruta_antigua) + chalk.gray(' → ') + chalk.white(ruta_nueva));
265
- }
266
- } catch { console.log(' ' + chalk.red('✖') + ' No se pudo renombrar: ' + ruta_antigua); }
296
+ if (!fs.existsSync(oldFile)) return;
297
+ retryFsOp('renombrar', ruta_antigua, () => {
298
+ if (!fs.existsSync(newDir)) fs.mkdirSync(newDir, { recursive: true });
299
+ fs.renameSync(oldFile, newFile);
300
+ console.log(' ' + chalk.yellow('↔') + ' ' + chalk.white(ruta_antigua) + chalk.gray(' → ') + chalk.white(ruta_nueva));
301
+ }, 5, 500, () => {
302
+ localDeletedAt.delete(ruta_antigua);
303
+ localUploadedAt.delete(ruta_nueva);
304
+ });
267
305
  return;
268
306
  }
269
307
  if (eventType !== 'file-update') return;
270
- const { ruta, contenido } = data || {};
308
+ const { ruta, contenido, isBinary, isText } = data || {};
271
309
  if (!ruta || contenido === undefined) return;
272
310
 
311
+ // Binary files via SSE: skip (handled by full pull on reconnect)
312
+ const binary = isBinary === true || isText === false;
313
+ if (binary) {
314
+ console.log(' ' + chalk.gray('⊘') + ' ' + chalk.gray(ruta) + chalk.gray(' (binario, ignorado por SSE)'));
315
+ return;
316
+ }
317
+
273
318
  // Evitar eco: si acabamos de subir este archivo (< 3s), ignorar
274
319
  const uploadedAt = localUploadedAt.get(ruta);
275
320
  if (uploadedAt && Date.now() - uploadedAt < 3000) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entexto-cli",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "CLI oficial de Entexto — Deploy y gestión de proyectos desde tu terminal",
5
5
  "main": "lib/index.js",
6
6
  "bin": {