shilo-cli 0.3.2 → 0.6.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.
@@ -30,6 +30,16 @@ function safeTarget(destDir, repoPath) {
30
30
  return { ok: true, target };
31
31
  }
32
32
 
33
+ /** Los bytes de un archivo local, o null si no existe o no se puede leer. */
34
+ function readLocalBytes(absPath) {
35
+ try {
36
+ if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) return null;
37
+ return fs.readFileSync(absPath);
38
+ } catch (_) {
39
+ return null;
40
+ }
41
+ }
42
+
33
43
  /**
34
44
  * shilo pull [carpeta] [--project <uuid>] [--base-url <url>] [--dry-run] [--yes] [--force]
35
45
  *
@@ -72,9 +82,13 @@ async function pull(args) {
72
82
  ui.warn(`no se pudo listar ${ui.c.dim(err.folderPath || '(raíz)')} — ${err.reason}`);
73
83
  }
74
84
 
75
- // 1) Separar texto (descargable) de lo que no se puede bajar sin corromper.
85
+ // 1) Descartar sólo lo que no se puede escribir con seguridad.
86
+ //
87
+ // Los binarios ya NO se saltean (Etapa 2 del PLAN_MOTOR_SHILO): el GET
88
+ // devuelve una URL firmada en vez de contenido, y se bajan como bytes. Lo
89
+ // que se saltaba antes no era una limitación del formato sino del camino:
90
+ // el contenido viajaba como texto y un PNG volvía mutilado.
76
91
  const candidates = [];
77
- const skippedBinary = [];
78
92
  const unsafe = [];
79
93
  for (const f of tree.files) {
80
94
  const safe = safeTarget(dir, f.repoPath);
@@ -82,15 +96,12 @@ async function pull(args) {
82
96
  unsafe.push({ repoPath: f.repoPath, reason: safe.reason });
83
97
  continue;
84
98
  }
85
- if (!sync.isTextMime(f.mime_type)) {
86
- skippedBinary.push({ repoPath: f.repoPath, reason: `mime no textual (${f.mime_type})` });
87
- continue;
88
- }
89
- candidates.push({ ...f, target: safe.target });
99
+ candidates.push({ ...f, target: safe.target, binary: !sync.isTextMime(f.mime_type) });
90
100
  }
91
101
 
92
102
  for (const u of unsafe) ui.warn(`saltado ${ui.c.dim(u.repoPath)} — ${u.reason}`);
93
- for (const s of skippedBinary) ui.warn(`saltado ${ui.c.dim(s.repoPath)} — ${s.reason}`);
103
+
104
+ const binaryCount = candidates.filter((f) => f.binary).length;
94
105
 
95
106
  // 2) Clasificar contra lo que hay en disco, sin bajar nada todavía.
96
107
  // `maybeSame` (mismo tamaño) se confirma recién al descargar el contenido.
@@ -116,7 +127,7 @@ async function pull(args) {
116
127
  ` ${ui.c.yellow('en conflicto:')} ${conflicting.length} ${ui.c.dim('(difieren en tamaño del local)')}\n` +
117
128
  ` ${ui.c.dim('posiblemente iguales:')} ${maybeSame.length}\n` +
118
129
  ` ${ui.c.dim('sólo locales:')} ${onlyLocal.length} ${ui.c.dim('(el pull no los toca)')}` +
119
- (skippedBinary.length ? `\n ${ui.c.dim('binarios saltados:')} ${skippedBinary.length}` : '') +
130
+ (binaryCount ? `\n ${ui.c.dim('binarios:')} ${binaryCount}` : '') +
120
131
  (unsafe.length ? `\n ${ui.c.red('inseguros:')} ${unsafe.length}` : '')
121
132
  );
122
133
  ui.info('');
@@ -186,6 +197,23 @@ async function pull(args) {
186
197
  const outcomes = await sync.runPool(toDownload, CONCURRENCY, async (f) => {
187
198
  const creds = await mgr.valid();
188
199
  const read = await client.readFile(projectId, f.repoPath, creds);
200
+
201
+ // Binario: el servidor no manda contenido, manda una URL firmada. Los bytes
202
+ // se escriben tal cual, sin encoding: pasarlos por 'utf8' es exactamente lo
203
+ // que corrompía los binarios antes de esta etapa.
204
+ if (read.file && read.file.is_binary && read.file.download_url) {
205
+ const bytes = await client.downloadBinary(read.file.download_url);
206
+
207
+ const currentBytes = readLocalBytes(f.target);
208
+ if (currentBytes !== null && currentBytes.equals(bytes)) {
209
+ return { repoPath: f.repoPath, skipped: true };
210
+ }
211
+
212
+ fs.mkdirSync(path.dirname(f.target), { recursive: true });
213
+ fs.writeFileSync(f.target, bytes);
214
+ return { repoPath: f.repoPath, skipped: false };
215
+ }
216
+
189
217
  const content = read.file && read.file.content != null ? read.file.content : '';
190
218
 
191
219
  const current = sync.readLocalText(f.target);