entexto-cli 1.2.0 → 1.4.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/lib/commands/sync.js +148 -16
- package/lib/utils/api.js +6 -1
- package/package.json +1 -1
package/lib/commands/sync.js
CHANGED
|
@@ -8,7 +8,7 @@ const ora = require('ora');
|
|
|
8
8
|
const FormData = require('form-data');
|
|
9
9
|
const chokidar = require('chokidar');
|
|
10
10
|
const { getToken } = require('../utils/config');
|
|
11
|
-
const { getProjects, deployFiles, getManifest, streamEvents } = require('../utils/api');
|
|
11
|
+
const { getProjects, deployFiles, getManifest, pullProject, streamEvents, deleteFileRemote } = require('../utils/api');
|
|
12
12
|
const { hashFile, hashContent } = require('../utils/delta');
|
|
13
13
|
|
|
14
14
|
const IGNORE_NAMES = new Set([
|
|
@@ -43,6 +43,7 @@ function recolectarArchivos(dir) {
|
|
|
43
43
|
|
|
44
44
|
// Rastrea cuándo subimos cada archivo (para evitar eco del SSE)
|
|
45
45
|
const localUploadedAt = new Map();
|
|
46
|
+
const localDeletedAt = new Map();
|
|
46
47
|
|
|
47
48
|
async function subirArchivoSolo(uuid, absDir, filePath) {
|
|
48
49
|
if (!fs.existsSync(filePath)) return;
|
|
@@ -60,6 +61,19 @@ async function subirArchivoSolo(uuid, absDir, filePath) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
async function eliminarArchivoRemoto(uuid, absDir, filePath) {
|
|
65
|
+
const ruta = path.relative(absDir, filePath).replace(/\\/g, '/');
|
|
66
|
+
if (debeIgnorar(path.basename(filePath))) return;
|
|
67
|
+
try {
|
|
68
|
+
await deleteFileRemote(uuid, ruta);
|
|
69
|
+
localDeletedAt.set(ruta, Date.now());
|
|
70
|
+
console.log(' ' + chalk.red('✖') + ' ' + chalk.white(ruta) + chalk.gray(' borrado → remoto'));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const msg = err.response?.data?.error || err.message;
|
|
73
|
+
console.log(' ' + chalk.red('✖') + ' ' + chalk.white(ruta) + chalk.red(' error borrando: ' + msg));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
module.exports = async function sync(options) {
|
|
64
78
|
if (!getToken()) {
|
|
65
79
|
console.error(chalk.red('✖ No has iniciado sesión. Ejecuta: entexto login'));
|
|
@@ -103,9 +117,10 @@ module.exports = async function sync(options) {
|
|
|
103
117
|
process.exit(1);
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
// ── 1. Delta inicial
|
|
107
|
-
console.log(chalk.bold('\n🔄
|
|
108
|
-
|
|
120
|
+
// ── 1. Delta inicial bidireccional ──────────────────────────────────────
|
|
121
|
+
console.log(chalk.bold('\n🔄 Sincronización inicial...\n'));
|
|
122
|
+
|
|
123
|
+
// 1a. Manifest remoto { ruta: hash }
|
|
109
124
|
let manifest = {};
|
|
110
125
|
try {
|
|
111
126
|
manifest = await getManifest(uuid);
|
|
@@ -113,19 +128,82 @@ module.exports = async function sync(options) {
|
|
|
113
128
|
console.log(chalk.gray(' (manifest no disponible, comparando todos)'));
|
|
114
129
|
}
|
|
115
130
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
// 1b. Hashes de los archivos locales
|
|
132
|
+
const todosArchivos = recolectarArchivos(absDir);
|
|
133
|
+
const localHashes = new Map();
|
|
134
|
+
for (const fp of todosArchivos) {
|
|
135
|
+
const ruta = path.relative(absDir, fp).replace(/\\/g, '/');
|
|
136
|
+
localHashes.set(ruta, hashFile(fp));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 1c. Clasificar qué hacer con cada archivo
|
|
140
|
+
const aDescargar = []; // ruta[] — remoto → local
|
|
141
|
+
const aSubir = []; // { filePath, ruta }[] — local → remoto
|
|
142
|
+
|
|
143
|
+
// Recorrer archivos remotos
|
|
144
|
+
for (const [ruta, remHash] of Object.entries(manifest)) {
|
|
145
|
+
const localHash = localHashes.get(ruta);
|
|
146
|
+
if (!localHash) {
|
|
147
|
+
// No existe localmente → descargar
|
|
148
|
+
aDescargar.push(ruta);
|
|
149
|
+
} else if (localHash === remHash) {
|
|
150
|
+
// Mismo contenido → sin cambios
|
|
121
151
|
console.log(' ' + chalk.gray('──') + ' ' + chalk.gray(ruta));
|
|
122
152
|
} else {
|
|
123
|
-
|
|
153
|
+
// Hash diferente → si el archivo local fue modificado hace < 10s, local gana
|
|
154
|
+
// de lo contrario remoto gana (remoto es fuente de verdad en sync inicial)
|
|
155
|
+
try {
|
|
156
|
+
const stat = fs.statSync(path.join(absDir, ruta));
|
|
157
|
+
if (Date.now() - stat.mtimeMs < 10000) {
|
|
158
|
+
aSubir.push({ filePath: path.join(absDir, ruta), ruta });
|
|
159
|
+
} else {
|
|
160
|
+
aDescargar.push(ruta);
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
aDescargar.push(ruta);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Archivos solo en local (no están en remoto) → subir
|
|
169
|
+
for (const [ruta] of localHashes) {
|
|
170
|
+
if (!(ruta in manifest)) {
|
|
171
|
+
aSubir.push({ filePath: path.join(absDir, ruta), ruta });
|
|
124
172
|
}
|
|
125
173
|
}
|
|
126
174
|
|
|
175
|
+
// 1d. Descargar archivos remotos faltantes o actualizados
|
|
176
|
+
if (aDescargar.length > 0) {
|
|
177
|
+
const sp = ora(`Descargando ${aDescargar.length} archivo(s) del servidor...`).start();
|
|
178
|
+
try {
|
|
179
|
+
const pullData = await pullProject(uuid);
|
|
180
|
+
const archivos = pullData.archivos || [];
|
|
181
|
+
const contentMap = new Map(archivos.map(a => [a.ruta, a.content]));
|
|
182
|
+
sp.stop();
|
|
183
|
+
let downloaded = 0;
|
|
184
|
+
for (const ruta of aDescargar) {
|
|
185
|
+
const contenido = contentMap.get(ruta);
|
|
186
|
+
if (contenido === undefined) {
|
|
187
|
+
console.log(' ' + chalk.yellow('⚠') + ' No encontrado en pull: ' + chalk.gray(ruta));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const destFile = path.join(absDir, ruta);
|
|
191
|
+
const destDir = path.dirname(destFile);
|
|
192
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
193
|
+
fs.writeFileSync(destFile, contenido, 'utf8');
|
|
194
|
+
localUploadedAt.set(ruta, Date.now()); // evitar re-subida por chokidar
|
|
195
|
+
console.log(' ' + chalk.cyan('↓') + ' ' + chalk.white(ruta) + chalk.gray(' ← remoto'));
|
|
196
|
+
downloaded++;
|
|
197
|
+
}
|
|
198
|
+
console.log(chalk.green(` ✔ ${downloaded} archivo(s) descargados\n`));
|
|
199
|
+
} catch (err) {
|
|
200
|
+
sp.fail('Error descargando: ' + (err.response?.data?.error || err.message));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 1e. Subir archivos locales nuevos o modificados recientemente
|
|
127
205
|
if (aSubir.length > 0) {
|
|
128
|
-
const sp = ora(`Subiendo ${aSubir.length} archivo(s)
|
|
206
|
+
const sp = ora(`Subiendo ${aSubir.length} archivo(s) al servidor...`).start();
|
|
129
207
|
try {
|
|
130
208
|
const form = new FormData();
|
|
131
209
|
for (const { filePath, ruta } of aSubir) {
|
|
@@ -133,13 +211,18 @@ module.exports = async function sync(options) {
|
|
|
133
211
|
localUploadedAt.set(ruta, Date.now());
|
|
134
212
|
}
|
|
135
213
|
await deployFiles(uuid, form, false);
|
|
136
|
-
sp.
|
|
137
|
-
aSubir.forEach(({ ruta }) =>
|
|
214
|
+
sp.stop();
|
|
215
|
+
aSubir.forEach(({ ruta }) =>
|
|
216
|
+
console.log(' ' + chalk.green('↑') + ' ' + chalk.white(ruta) + chalk.gray(' → remoto'))
|
|
217
|
+
);
|
|
218
|
+
console.log(chalk.green(` ✔ ${aSubir.length} archivo(s) subidos\n`));
|
|
138
219
|
} catch (err) {
|
|
139
|
-
sp.fail('Error
|
|
220
|
+
sp.fail('Error subiendo: ' + (err.response?.data?.error || err.message));
|
|
140
221
|
}
|
|
141
|
-
}
|
|
142
|
-
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (aDescargar.length === 0 && aSubir.length === 0) {
|
|
225
|
+
console.log(chalk.green(' ✔ Todo sincronizado, sin cambios.\n'));
|
|
143
226
|
}
|
|
144
227
|
|
|
145
228
|
// ── 2. Canal SSE (remoto → local) ───────────────────────────────────────
|
|
@@ -149,6 +232,36 @@ module.exports = async function sync(options) {
|
|
|
149
232
|
let sseReconnectTimer = null;
|
|
150
233
|
|
|
151
234
|
function manejarEventoRemoto(eventType, data) {
|
|
235
|
+
if (eventType === 'file-delete') {
|
|
236
|
+
const { ruta } = data || {};
|
|
237
|
+
if (!ruta) return;
|
|
238
|
+
const destFile = path.join(absDir, ruta);
|
|
239
|
+
try {
|
|
240
|
+
if (fs.existsSync(destFile)) {
|
|
241
|
+
fs.unlinkSync(destFile);
|
|
242
|
+
localDeletedAt.set(ruta, Date.now());
|
|
243
|
+
console.log(' ' + chalk.red('✖') + ' ' + chalk.white(ruta) + chalk.gray(' ← borrado remoto'));
|
|
244
|
+
}
|
|
245
|
+
} catch { console.log(' ' + chalk.red('✖') + ' No se pudo borrar: ' + ruta); }
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (eventType === 'file-rename') {
|
|
249
|
+
const { ruta_antigua, ruta_nueva } = data || {};
|
|
250
|
+
if (!ruta_antigua || !ruta_nueva) return;
|
|
251
|
+
const oldFile = path.join(absDir, ruta_antigua);
|
|
252
|
+
const newFile = path.join(absDir, ruta_nueva);
|
|
253
|
+
const newDir = path.dirname(newFile);
|
|
254
|
+
try {
|
|
255
|
+
if (fs.existsSync(oldFile)) {
|
|
256
|
+
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir, { recursive: true });
|
|
257
|
+
fs.renameSync(oldFile, newFile);
|
|
258
|
+
localDeletedAt.set(ruta_antigua, Date.now());
|
|
259
|
+
localUploadedAt.set(ruta_nueva, Date.now());
|
|
260
|
+
console.log(' ' + chalk.yellow('↔') + ' ' + chalk.white(ruta_antigua) + chalk.gray(' → ') + chalk.white(ruta_nueva));
|
|
261
|
+
}
|
|
262
|
+
} catch { console.log(' ' + chalk.red('✖') + ' No se pudo renombrar: ' + ruta_antigua); }
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
152
265
|
if (eventType !== 'file-update') return;
|
|
153
266
|
const { ruta, contenido } = data || {};
|
|
154
267
|
if (!ruta || contenido === undefined) return;
|
|
@@ -179,6 +292,7 @@ module.exports = async function sync(options) {
|
|
|
179
292
|
try {
|
|
180
293
|
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
181
294
|
fs.writeFileSync(destFile, contenido, 'utf8');
|
|
295
|
+
localUploadedAt.set(ruta, Date.now()); // evitar re-subida por chokidar
|
|
182
296
|
console.log(' ' + chalk.cyan('↓') + ' ' + chalk.white(ruta) + chalk.gray(' ← remoto'));
|
|
183
297
|
} catch {
|
|
184
298
|
console.log(' ' + chalk.red('✖') + ' No se pudo escribir: ' + ruta);
|
|
@@ -234,8 +348,26 @@ module.exports = async function sync(options) {
|
|
|
234
348
|
watcher.on('change', fp => { changed.add(fp); disparar(); });
|
|
235
349
|
watcher.on('error', err => console.error(chalk.red(' [watch] Error: ' + err.message)));
|
|
236
350
|
|
|
351
|
+
const deletedFiles = new Set();
|
|
352
|
+
let deleteDebounce = null;
|
|
353
|
+
|
|
354
|
+
watcher.on('unlink', fp => {
|
|
355
|
+
const ruta = path.relative(absDir, fp).replace(/\\/g, '/');
|
|
356
|
+
if (debeIgnorar(path.basename(fp))) return;
|
|
357
|
+
const deletedAt = localDeletedAt.get(ruta);
|
|
358
|
+
if (deletedAt && Date.now() - deletedAt < 3000) return;
|
|
359
|
+
deletedFiles.add(fp);
|
|
360
|
+
clearTimeout(deleteDebounce);
|
|
361
|
+
deleteDebounce = setTimeout(async () => {
|
|
362
|
+
const files = [...deletedFiles];
|
|
363
|
+
deletedFiles.clear();
|
|
364
|
+
for (const f of files) await eliminarArchivoRemoto(uuid, absDir, f);
|
|
365
|
+
}, 300);
|
|
366
|
+
});
|
|
367
|
+
|
|
237
368
|
process.on('SIGINT', () => {
|
|
238
369
|
clearTimeout(sseReconnectTimer);
|
|
370
|
+
clearTimeout(deleteDebounce);
|
|
239
371
|
if (sseStream) try { sseStream.destroy(); } catch {}
|
|
240
372
|
watcher.close();
|
|
241
373
|
console.log(chalk.gray('\n Sync detenido.\n'));
|
package/lib/utils/api.js
CHANGED
|
@@ -96,4 +96,9 @@ async function streamEvents(uuid, onEvent) {
|
|
|
96
96
|
return stream;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
async function deleteFileRemote(uuid, ruta) {
|
|
100
|
+
const res = await client().post(`/api/cli/delete/${uuid}`, { ruta });
|
|
101
|
+
return res.data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { login, getProjects, deployFiles, deployWithPublish, publishProject, pullProject, getManifest, streamEvents, deleteFileRemote };
|