shilo-cli 0.3.0 → 0.3.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/commands/push.js +109 -68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shilo-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "CLI para sincronizar carpetas locales con repositorios de Shilo: push, pull, status, log y diff.",
5
5
  "keywords": [
6
6
  "shilo",
@@ -138,40 +138,45 @@ async function push(args) {
138
138
 
139
139
  // 5) Clasificar contra el remote (added / modified / unchanged) para planear.
140
140
  // Esto ya toca la red (getFileMeta / readFile) pero no escribe nada.
141
- let creds = cfg.credentials;
141
+ // Se hace con un pool de concurrencia (mismo patrón que walkRemote en
142
+ // pull/status) en vez de un archivo a la vez: es lo que hacía que un push
143
+ // de muchos archivos tardara minutos por puro round-trip secuencial.
144
+ // `mgr` serializa el refresh del JWT para que llamadas paralelas no
145
+ // quemen el mismo refresh_token.
146
+ const mgr = sync.makeCredsManager(client, cfg);
142
147
  const plan = { added: [], modified: [], unchanged: [], failed: [] };
143
148
 
144
- for (const u of uploads) {
145
- try {
146
- const meta = await client.getFileMeta(projectId, u.relPath, creds);
147
- creds = meta.credentials;
149
+ const planResults = await sync.runPool(uploads, sync.CONCURRENCY, async (u) => {
150
+ const creds = await mgr.valid();
151
+ const meta = await client.getFileMeta(projectId, u.relPath, creds);
148
152
 
149
- if (!meta.exists) {
150
- plan.added.push(u);
151
- continue;
152
- }
153
- if (meta.file && meta.file.type === 'folder') {
154
- plan.failed.push({ ...u, reason: 'ya existe una carpeta con ese nombre' });
155
- continue;
156
- }
153
+ if (!meta.exists) return { kind: 'added', u };
154
+ if (meta.file && meta.file.type === 'folder') {
155
+ return { kind: 'failed', u, reason: 'ya existe una carpeta con ese nombre' };
156
+ }
157
157
 
158
- // Existe como archivo: ver si cambió comparando tamaño y luego contenido.
159
- const localSize = Buffer.byteLength(u.content, 'utf8');
160
- if (meta.file && typeof meta.file.size === 'number' && meta.file.size === localSize) {
161
- const remoteRead = await client.readFile(projectId, u.relPath, creds);
162
- creds = remoteRead.credentials;
163
- if (remoteRead.file && remoteRead.file.content === u.content) {
164
- plan.unchanged.push(u);
165
- continue;
166
- }
158
+ // Existe como archivo: ver si cambió comparando tamaño y luego contenido.
159
+ const localSize = Buffer.byteLength(u.content, 'utf8');
160
+ if (meta.file && typeof meta.file.size === 'number' && meta.file.size === localSize) {
161
+ const readCreds = await mgr.valid();
162
+ const remoteRead = await client.readFile(projectId, u.relPath, readCreds);
163
+ if (remoteRead.file && remoteRead.file.content === u.content) {
164
+ return { kind: 'unchanged', u };
167
165
  }
168
- plan.modified.push({ ...u, remote: meta.file });
169
- } catch (e) {
170
- plan.failed.push({ ...u, reason: e.message });
171
166
  }
167
+ return { kind: 'modified', u: { ...u, remote: meta.file } };
168
+ });
169
+
170
+ for (const r of planResults) {
171
+ if (r && r.__error) {
172
+ plan.failed.push({ ...r.item, reason: r.__error });
173
+ continue;
174
+ }
175
+ if (r.kind === 'failed') plan.failed.push({ ...r.u, reason: r.reason });
176
+ else plan[r.kind].push(r.u);
172
177
  }
173
178
 
174
- config.persistCredsIfChanged(cfg, creds);
179
+ config.persistCredsIfChanged(cfg, mgr.current);
175
180
 
176
181
  // 5b) Atribución: para los archivos que vamos a pisar, quién los tocó último.
177
182
  // Es lo que distingue "esto lo subí yo antes" de "esto lo va a perder un
@@ -179,7 +184,6 @@ async function push(args) {
179
184
  let attribution = { index: new Map() };
180
185
  const myId = cfg.credentials && cfg.credentials.user ? cfg.credentials.user.id : null;
181
186
  if (plan.modified.length > 0) {
182
- const mgr = sync.makeCredsManager(client, cfg);
183
187
  attribution = await sync.buildAuthorIndex(client, projectId, mgr, { commits: 50 });
184
188
  config.persistCredsIfChanged(cfg, mgr.current);
185
189
  }
@@ -266,57 +270,94 @@ async function push(args) {
266
270
  const ensuredCache = new Set();
267
271
  const failedWrites = [];
268
272
 
273
+ // 8a) Asegurar carpetas primero, EN SERIE. `ensuredCache` evita re-listar una
274
+ // carpeta ya confirmada, pero además evita la carrera real: el backend no
275
+ // siempre responde 409 de forma confiable si dos requests intentan crear
276
+ // la misma carpeta al mismo tiempo, así que esta fase no se paraleliza.
277
+ // Como la mayoría de los archivos comparten carpetas, el cache hace que
278
+ // esto sea barato (proporcional a carpetas únicas, no a archivos).
279
+ const ensureFailed = new Set();
269
280
  for (const u of plan.added) {
270
281
  try {
271
- creds = await client.ensurePath(projectId, u.relPath, creds, ensuredCache);
272
- const created = await client.createFile(
273
- projectId, u.relPath, u.content, u.mimeType, creds
274
- );
275
- creds = created.credentials;
276
-
277
- if (created.created && created.file) {
278
- changes.push({
279
- file_id: created.file.id,
280
- action: 'added',
281
- new_storage_path: created.file.storage_path,
282
- });
283
- ui.success(`${ui.c.green('+')} ${u.relPath}`);
284
- } else {
285
- // 409: apareció entremedio; caemos a update.
286
- const upd = await client.writeFile(projectId, u.relPath, u.content, u.mimeType, creds);
287
- creds = upd.credentials;
288
- changes.push({
289
- file_id: upd.file.id,
290
- action: 'modified',
291
- previous_storage_path: (created.file && created.file.storage_path) || upd.file.storage_path,
292
- new_storage_path: upd.file.storage_path,
293
- });
294
- ui.success(`${ui.c.yellow('~')} ${u.relPath}`);
295
- }
296
- config.persistCredsIfChanged(cfg, creds);
282
+ const creds = await mgr.valid();
283
+ await client.ensurePath(projectId, u.relPath, creds, ensuredCache);
297
284
  } catch (e) {
285
+ ensureFailed.add(u.relPath);
298
286
  failedWrites.push({ relPath: u.relPath, reason: e.message });
299
287
  ui.error(`${u.relPath} — ${e.message}`);
300
288
  }
301
289
  }
290
+ config.persistCredsIfChanged(cfg, mgr.current);
291
+
292
+ // 8b) Subidas propiamente dichas, en paralelo: acá es donde vive el grueso
293
+ // del tiempo (un request por archivo), y a diferencia de ensurePath no
294
+ // hay riesgo de carrera porque cada archivo tiene una ruta propia.
295
+ const addedReady = plan.added.filter((u) => !ensureFailed.has(u.relPath));
296
+ const addedResults = await sync.runPool(addedReady, sync.CONCURRENCY, async (u) => {
297
+ const creds = await mgr.valid();
298
+ const created = await client.createFile(projectId, u.relPath, u.content, u.mimeType, creds);
299
+
300
+ if (created.created && created.file) {
301
+ return {
302
+ u,
303
+ symbol: '+',
304
+ change: {
305
+ file_id: created.file.id,
306
+ action: 'added',
307
+ new_storage_path: created.file.storage_path,
308
+ },
309
+ };
310
+ }
311
+ // 409: apareció entremedio; caemos a update.
312
+ const writeCreds = await mgr.valid();
313
+ const upd = await client.writeFile(projectId, u.relPath, u.content, u.mimeType, writeCreds);
314
+ return {
315
+ u,
316
+ symbol: '~',
317
+ change: {
318
+ file_id: upd.file.id,
319
+ action: 'modified',
320
+ previous_storage_path: (created.file && created.file.storage_path) || upd.file.storage_path,
321
+ new_storage_path: upd.file.storage_path,
322
+ },
323
+ };
324
+ });
302
325
 
303
- for (const u of plan.modified) {
304
- try {
305
- const upd = await client.writeFile(projectId, u.relPath, u.content, u.mimeType, creds);
306
- creds = upd.credentials;
307
- changes.push({
326
+ for (const r of addedResults) {
327
+ if (r && r.__error) {
328
+ failedWrites.push({ relPath: r.item.relPath, reason: r.__error });
329
+ ui.error(`${r.item.relPath} — ${r.__error}`);
330
+ continue;
331
+ }
332
+ changes.push(r.change);
333
+ ui.success(`${ui.c[r.symbol === '+' ? 'green' : 'yellow'](r.symbol)} ${r.u.relPath}`);
334
+ }
335
+ config.persistCredsIfChanged(cfg, mgr.current);
336
+
337
+ const modifiedResults = await sync.runPool(plan.modified, sync.CONCURRENCY, async (u) => {
338
+ const creds = await mgr.valid();
339
+ const upd = await client.writeFile(projectId, u.relPath, u.content, u.mimeType, creds);
340
+ return {
341
+ u,
342
+ change: {
308
343
  file_id: upd.file.id,
309
344
  action: 'modified',
310
345
  previous_storage_path: u.remote ? u.remote.storage_path : undefined,
311
346
  new_storage_path: upd.file.storage_path,
312
- });
313
- ui.success(`${ui.c.yellow('~')} ${u.relPath}`);
314
- config.persistCredsIfChanged(cfg, creds);
315
- } catch (e) {
316
- failedWrites.push({ relPath: u.relPath, reason: e.message });
317
- ui.error(`${u.relPath} — ${e.message}`);
347
+ },
348
+ };
349
+ });
350
+
351
+ for (const r of modifiedResults) {
352
+ if (r && r.__error) {
353
+ failedWrites.push({ relPath: r.item.relPath, reason: r.__error });
354
+ ui.error(`${r.item.relPath} — ${r.__error}`);
355
+ continue;
318
356
  }
357
+ changes.push(r.change);
358
+ ui.success(`${ui.c.yellow('~')} ${r.u.relPath}`);
319
359
  }
360
+ config.persistCredsIfChanged(cfg, mgr.current);
320
361
 
321
362
  // 9) Commit(s) en lotes de <=100.
322
363
  let committed = 0;
@@ -328,10 +369,10 @@ async function push(args) {
328
369
  const partNo = Math.floor(i / COMMIT_BATCH) + 1;
329
370
  const msg = buildMessage(message, batch.length, total, partNo, nBatches, card);
330
371
  try {
331
- const result = await client.createCommit(projectId, msg, batch, creds);
332
- creds = result.credentials;
372
+ const creds = await mgr.valid();
373
+ await client.createCommit(projectId, msg, batch, creds);
333
374
  committed += batch.length;
334
- config.persistCredsIfChanged(cfg, creds);
375
+ config.persistCredsIfChanged(cfg, mgr.current);
335
376
  } catch (e) {
336
377
  ui.error(`Fallo al crear el commit (lote ${partNo}/${nBatches}): ${e.message}`);
337
378
  failedWrites.push({ relPath: `<commit lote ${partNo}>`, reason: e.message });
@@ -339,7 +380,7 @@ async function push(args) {
339
380
  }
340
381
  }
341
382
 
342
- config.persistCredsIfChanged(cfg, creds);
383
+ config.persistCredsIfChanged(cfg, mgr.current);
343
384
 
344
385
  // 10) Anotar la sincronización: `status` la usa después para saber qué del
345
386
  // remoto es posterior a lo último que subimos.