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.
- package/lib/commands/pull.js +6 -2
- package/lib/commands/sync.js +67 -22
- package/package.json +1 -1
package/lib/commands/pull.js
CHANGED
|
@@ -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
|
-
|
|
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}`));
|
package/lib/commands/sync.js
CHANGED
|
@@ -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
|
|
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
|
|
186
|
-
if (
|
|
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
|
-
|
|
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
|
-
// ⚠️
|
|
240
|
-
//
|
|
241
|
-
//
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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;
|