shilo-cli 0.7.0 → 0.9.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.
@@ -40,6 +40,38 @@ function readLocalBytes(absPath) {
40
40
  }
41
41
  }
42
42
 
43
+ /**
44
+ * Carpeta de respaldo de esta corrida: `.shilo/respaldo/<ISO>/`.
45
+ *
46
+ * Va adentro de `.shilo/` —que está en DEFAULT_IGNORES— y no como un
47
+ * `<archivo>.local` al lado: un hermano se vuelve un archivo más del árbol y el
48
+ * push siguiente lo sube, o sea que cambiaríamos "perder trabajo" por "publicar
49
+ * tu versión en conflicto". Al ir por timestamp, dos pulls seguidos no se pisan.
50
+ *
51
+ * Los `:` del ISO no son válidos en nombres de archivo de Windows.
52
+ */
53
+ function backupDirFor(dir, stamp) {
54
+ return path.join(dir, '.shilo', 'respaldo', stamp.replace(/:/g, '-'));
55
+ }
56
+
57
+ /**
58
+ * Copia la versión local de `repoPath` al respaldo antes de pisarla. Devuelve
59
+ * true si quedó copiada. Es best-effort: si falla, el pull sigue —avisando—
60
+ * porque abortar a mitad del pool dejaría la carpeta peor que como estaba.
61
+ */
62
+ function backupLocal(baseDir, absPath, repoPath) {
63
+ try {
64
+ if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) return false;
65
+ const target = path.join(baseDir, ...repoPath.split('/'));
66
+ fs.mkdirSync(path.dirname(target), { recursive: true });
67
+ fs.copyFileSync(absPath, target);
68
+ return true;
69
+ } catch (e) {
70
+ ui.warn(`no se pudo respaldar ${ui.c.dim(repoPath)} — ${e.message}`);
71
+ return false;
72
+ }
73
+ }
74
+
43
75
  /**
44
76
  * shilo pull [carpeta] [--project <uuid>] [--base-url <url>] [--dry-run] [--yes] [--force]
45
77
  *
@@ -101,14 +133,27 @@ async function pull(args) {
101
133
  // devuelve una URL firmada en vez de contenido, y se bajan como bytes. Lo
102
134
  // que se saltaba antes no era una limitación del formato sino del camino:
103
135
  // el contenido viajaba como texto y un PNG volvía mutilado.
136
+ //
137
+ // Lo ignorado localmente tampoco se baja: si `.env.*` está en el ignore,
138
+ // que alguien haya subido un `.env.ejemplo` desde otra máquina no es razón
139
+ // para que aparezca acá. Además arregla el conteo: antes entraban en
140
+ // "nuevos" (walkLocal no los ve) y después se descartaban al descargar.
141
+ const includeIgnored = !!args['include-ignored'];
142
+ const isIgnoredPath = sync.makePathMatcher(dir);
143
+
104
144
  const candidates = [];
105
145
  const unsafe = [];
146
+ const ignoredRemote = [];
106
147
  for (const f of tree.files) {
107
148
  const safe = safeTarget(dir, f.repoPath);
108
149
  if (!safe.ok) {
109
150
  unsafe.push({ repoPath: f.repoPath, reason: safe.reason });
110
151
  continue;
111
152
  }
153
+ if (!includeIgnored && isIgnoredPath(f.repoPath)) {
154
+ ignoredRemote.push(f.repoPath);
155
+ continue;
156
+ }
112
157
  candidates.push({ ...f, target: safe.target, binary: !sync.isTextMime(f.mime_type) });
113
158
  }
114
159
 
@@ -141,6 +186,9 @@ async function pull(args) {
141
186
  ` ${ui.c.dim('posiblemente iguales:')} ${maybeSame.length}\n` +
142
187
  ` ${ui.c.dim('sólo locales:')} ${onlyLocal.length} ${ui.c.dim('(el pull no los toca)')}` +
143
188
  (binaryCount ? `\n ${ui.c.dim('binarios:')} ${binaryCount}` : '') +
189
+ (ignoredRemote.length
190
+ ? `\n ${ui.c.dim('ignorados:')} ${ignoredRemote.length} ${ui.c.dim('(no se bajan)')}`
191
+ : '') +
144
192
  (unsafe.length ? `\n ${ui.c.red('inseguros:')} ${unsafe.length}` : '')
145
193
  );
146
194
  ui.info('');
@@ -157,6 +205,9 @@ async function pull(args) {
157
205
  );
158
206
  }
159
207
  for (const f of onlyLocal) ui.info(` ${ui.c.dim('·')} ${ui.c.dim(`${f.repoPath} (sólo local, se conserva)`)}`);
208
+ for (const p of ignoredRemote) {
209
+ ui.info(` ${ui.c.dim('·')} ${ui.c.dim(`${p} (ignorado localmente, no se baja)`)}`);
210
+ }
160
211
  ui.info('');
161
212
  if (conflicting.length > 0) {
162
213
  ui.warn(`${conflicting.length} archivo(s) locales se sobrescribirían: hace falta --force.`);
@@ -204,9 +255,17 @@ async function pull(args) {
204
255
 
205
256
  // 6) Descargar y escribir, con pool. Recién acá se sabe si un archivo del
206
257
  // mismo tamaño era realmente idéntico: si lo era, no se reescribe.
258
+ // Antes de pisar un archivo que difería del remoto (sólo pasa con --force)
259
+ // se guarda la versión local bajo `.shilo/respaldo/<ISO>/`: recuperarse de
260
+ // una divergencia dejaba de depender de haber copiado a mano antes.
261
+ const conflictPaths = new Set(conflicting.map((f) => f.repoPath));
262
+ const backupStamp = new Date().toISOString();
263
+ const backupDir = backupDirFor(dir, backupStamp);
264
+
207
265
  const written = [];
208
266
  const unchanged = [];
209
267
  const failed = [];
268
+ const backedUp = [];
210
269
  const outcomes = await sync.runPool(toDownload, CONCURRENCY, async (f) => {
211
270
  const creds = await mgr.valid();
212
271
  const read = await client.readFile(projectId, f.repoPath, creds);
@@ -222,9 +281,13 @@ async function pull(args) {
222
281
  return { repoPath: f.repoPath, skipped: true };
223
282
  }
224
283
 
284
+ const saved = conflictPaths.has(f.repoPath)
285
+ ? backupLocal(backupDir, f.target, f.repoPath)
286
+ : false;
287
+
225
288
  fs.mkdirSync(path.dirname(f.target), { recursive: true });
226
289
  fs.writeFileSync(f.target, bytes);
227
- return { repoPath: f.repoPath, skipped: false };
290
+ return { repoPath: f.repoPath, skipped: false, backedUp: saved };
228
291
  }
229
292
 
230
293
  const content = read.file && read.file.content != null ? read.file.content : '';
@@ -234,9 +297,13 @@ async function pull(args) {
234
297
  return { repoPath: f.repoPath, skipped: true };
235
298
  }
236
299
 
300
+ const saved = conflictPaths.has(f.repoPath)
301
+ ? backupLocal(backupDir, f.target, f.repoPath)
302
+ : false;
303
+
237
304
  fs.mkdirSync(path.dirname(f.target), { recursive: true });
238
305
  fs.writeFileSync(f.target, content, 'utf8');
239
- return { repoPath: f.repoPath, skipped: false };
306
+ return { repoPath: f.repoPath, skipped: false, backedUp: saved };
240
307
  });
241
308
 
242
309
  for (const res of outcomes) {
@@ -247,6 +314,7 @@ async function pull(args) {
247
314
  unchanged.push(res.repoPath);
248
315
  } else {
249
316
  written.push(res.repoPath);
317
+ if (res.backedUp) backedUp.push(res.repoPath);
250
318
  ui.success(`${ui.c.green('↓')} ${res.repoPath}`);
251
319
  }
252
320
  }
@@ -271,6 +339,22 @@ async function pull(args) {
271
339
 
272
340
  // 8) Resumen.
273
341
  ui.info('');
342
+ if (backedUp.length > 0) {
343
+ ui.info(
344
+ `${backedUp.length} versión(es) local(es) sobrescrita(s) quedaron respaldadas en ` +
345
+ ui.c.cyan(path.relative(dir, backupDir).split(path.sep).join('/'))
346
+ );
347
+ ui.info(ui.c.dim('(está dentro de .shilo/, así que no se sube ni aparece en el status.)'));
348
+ ui.info('');
349
+ }
350
+ if (ignoredRemote.length > 0) {
351
+ ui.info(
352
+ ui.c.dim(
353
+ `${ignoredRemote.length} archivo(s) del remoto están ignorados localmente y no se bajaron ` +
354
+ `(--include-ignored para bajarlos igual).`
355
+ )
356
+ );
357
+ }
274
358
  const tail = unchanged.length ? ` (${unchanged.length} ya estaban al día)` : '';
275
359
  if (failed.length === 0) {
276
360
  ui.success(`Pull completo: ${written.length} archivo(s) actualizados en "${dir}"${tail}.`);