shilo-cli 0.7.0 → 0.8.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/README.md +319 -255
- package/bin/shilo.js +197 -165
- package/package.json +1 -1
- package/src/ShiloClient.js +1027 -837
- package/src/args.js +32 -8
- package/src/commands/diff.js +18 -2
- package/src/commands/export.js +226 -0
- package/src/commands/log.js +4 -0
- package/src/commands/pull.js +86 -2
- package/src/commands/push.js +233 -8
- package/src/commands/review.js +101 -0
- package/src/commands/status.js +36 -0
- package/src/exportgit.js +417 -0
- package/src/gitobjects.js +532 -0
- package/src/sync.js +4 -1
- package/src/walk.js +30 -0
package/src/commands/push.js
CHANGED
|
@@ -36,6 +36,11 @@ const PROPOSAL_MAX_BYTES = 4 * 1024 * 1024; // el server corta en 5 MB; margen
|
|
|
36
36
|
const HEAD_BYTES = 8192;
|
|
37
37
|
|
|
38
38
|
// Cuántos ejemplos se muestran por cada motivo de salteo.
|
|
39
|
+
// Techo por campo del bloque `agent` y del prompt (Etapa 4, puntos 1 y 2). El
|
|
40
|
+
// del prompt es el mismo que aplica el servidor: avisar acá ahorra el viaje.
|
|
41
|
+
const AGENT_FIELD_MAX = 200;
|
|
42
|
+
const PROMPT_MAX_BYTES = 64 * 1024;
|
|
43
|
+
|
|
39
44
|
const SKIP_EXAMPLES = 5;
|
|
40
45
|
// A partir de cuántos salteos en una misma carpeta de primer nivel sugerimos
|
|
41
46
|
// ignorarla entera.
|
|
@@ -149,6 +154,61 @@ function preflight(dir) {
|
|
|
149
154
|
return { uploads, skipped: skips, ignored, gitDirs, findings };
|
|
150
155
|
}
|
|
151
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Aplica los selectores de `--only` sobre los archivos que dejó el preflight.
|
|
159
|
+
*
|
|
160
|
+
* Cada selector se normaliza con `sync.toRepoPath`, que ya blinda contra salir
|
|
161
|
+
* de la carpeta, y matchea por igualdad exacta o como prefijo de carpeta
|
|
162
|
+
* (`Documentacion/` alcanza todo lo de adentro).
|
|
163
|
+
*
|
|
164
|
+
* Un selector que no matchea nada es ERROR, no un push silencioso de todo:
|
|
165
|
+
* confundir "no subí nada de lo que pedí" con "subí el repo entero" es
|
|
166
|
+
* exactamente el fallo que arruinaría la confianza en el flag.
|
|
167
|
+
*
|
|
168
|
+
* Devuelve `{ uploads, leftOut }` o `{ error }`.
|
|
169
|
+
*/
|
|
170
|
+
function selectUploads(dir, allUploads, only) {
|
|
171
|
+
const selectors = (Array.isArray(only) ? only : only ? [only] : [])
|
|
172
|
+
.map((s) => String(s).trim())
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
|
|
175
|
+
if (selectors.length === 0) return { uploads: allUploads, leftOut: [] };
|
|
176
|
+
|
|
177
|
+
const normalized = [];
|
|
178
|
+
for (const raw of selectors) {
|
|
179
|
+
const repoPath = sync.toRepoPath(dir, raw);
|
|
180
|
+
if (!repoPath) return { error: `--only "${raw}" queda fuera de la carpeta del push.` };
|
|
181
|
+
normalized.push({ raw, repoPath });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const matched = new Set();
|
|
185
|
+
const uploads = [];
|
|
186
|
+
const leftOut = [];
|
|
187
|
+
|
|
188
|
+
for (const u of allUploads) {
|
|
189
|
+
const hit = normalized.find(
|
|
190
|
+
(s) => u.relPath === s.repoPath || u.relPath.startsWith(`${s.repoPath}/`)
|
|
191
|
+
);
|
|
192
|
+
if (hit) {
|
|
193
|
+
matched.add(hit.repoPath);
|
|
194
|
+
uploads.push(u);
|
|
195
|
+
} else {
|
|
196
|
+
leftOut.push(u);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const misses = normalized.filter((s) => !matched.has(s.repoPath));
|
|
201
|
+
if (misses.length > 0) {
|
|
202
|
+
return {
|
|
203
|
+
error:
|
|
204
|
+
`--only no encontró nada para: ${misses.map((s) => `"${s.raw}"`).join(', ')}. ` +
|
|
205
|
+
`Puede estar ignorado, salteado, o escrito distinto; nada se subió.`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { uploads, leftOut };
|
|
210
|
+
}
|
|
211
|
+
|
|
152
212
|
/**
|
|
153
213
|
* Reporte agrupado de salteos. Un repo con un `venv/` adentro generaba miles de
|
|
154
214
|
* líneas "saltado …" que tapaban todo lo demás; acá se resume por motivo, con
|
|
@@ -292,8 +352,26 @@ async function push(args) {
|
|
|
292
352
|
return 1;
|
|
293
353
|
}
|
|
294
354
|
|
|
355
|
+
// Procedencia (Etapa 4, punto 1). Sin --agent no se manda bloque alguno y el
|
|
356
|
+
// guardado queda como escrito por una persona, que es la verdad.
|
|
357
|
+
const agent = buildAgent(args);
|
|
358
|
+
if (agent && agent.error) {
|
|
359
|
+
ui.error(agent.error);
|
|
360
|
+
return 1;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const promptRead = readPrompt(args, agent);
|
|
364
|
+
if (promptRead.error) {
|
|
365
|
+
ui.error(promptRead.error);
|
|
366
|
+
return 1;
|
|
367
|
+
}
|
|
368
|
+
for (const w of promptRead.warnings) ui.warn(w);
|
|
369
|
+
const promptText = promptRead.prompt;
|
|
370
|
+
|
|
295
371
|
// 3) PREFLIGHT: todo offline. Recién después de esto se toca la red.
|
|
296
|
-
|
|
372
|
+
// `allUploads` es la carpeta entera; `--only` recorta DESPUÉS (ver 3b),
|
|
373
|
+
// para que los ignores y el escáner de secretos corran igual siempre.
|
|
374
|
+
const { uploads: allUploads, skipped, ignored, gitDirs, findings } = preflight(dir);
|
|
297
375
|
|
|
298
376
|
if (gitDirs.length > 0) {
|
|
299
377
|
ui.warn(
|
|
@@ -320,12 +398,23 @@ async function push(args) {
|
|
|
320
398
|
);
|
|
321
399
|
}
|
|
322
400
|
|
|
323
|
-
if (
|
|
401
|
+
if (allUploads.length === 0) {
|
|
324
402
|
ui.info('No hay archivos para subir.');
|
|
325
403
|
ui.info(ui.c.dim(`(${ignored} ignorados, ${skipped.length} salteados)`));
|
|
326
404
|
return 0;
|
|
327
405
|
}
|
|
328
406
|
|
|
407
|
+
// 3b) `--only`: recorta QUÉ se sube, no qué se mira. El push nunca borra
|
|
408
|
+
// archivos remotos (su plan es added/modified/unchanged/failed), así que
|
|
409
|
+
// subir un subconjunto es seguro por construcción: no hay semántica nueva.
|
|
410
|
+
const selection = selectUploads(dir, allUploads, args.only);
|
|
411
|
+
if (selection.error) {
|
|
412
|
+
ui.error(selection.error);
|
|
413
|
+
return 1;
|
|
414
|
+
}
|
|
415
|
+
const uploads = selection.uploads;
|
|
416
|
+
const leftOut = selection.leftOut;
|
|
417
|
+
|
|
329
418
|
// 4) Cliente + control de ritmo. `onThrottle` baja la concurrencia sola si el
|
|
330
419
|
// servidor empieza a responder 429, en vez de insistir hasta que corte.
|
|
331
420
|
const limit = sync.resolveConcurrency(args);
|
|
@@ -533,8 +622,9 @@ async function push(args) {
|
|
|
533
622
|
for (const f of plan.failed) ui.error(`${f.relPath} — ${f.reason}`);
|
|
534
623
|
return 1;
|
|
535
624
|
}
|
|
536
|
-
if (useIndex && !dryRun) saveIndex(dir, index, plan,
|
|
625
|
+
if (useIndex && !dryRun) saveIndex(dir, index, plan, allUploads);
|
|
537
626
|
ui.success('Todo al día. Nada para subir.');
|
|
627
|
+
reportLeftOut(leftOut, remoteByPath);
|
|
538
628
|
return 0;
|
|
539
629
|
}
|
|
540
630
|
|
|
@@ -543,6 +633,7 @@ async function push(args) {
|
|
|
543
633
|
for (const u of plan.modified) ui.info(` ${ui.c.yellow('~')} ${u.relPath}`);
|
|
544
634
|
for (const f of plan.failed) ui.info(` ${ui.c.red('!')} ${f.relPath} — ${f.reason}`);
|
|
545
635
|
ui.info('');
|
|
636
|
+
reportLeftOut(leftOut, remoteByPath);
|
|
546
637
|
ui.info(ui.c.dim('(--dry-run: no se subió nada)'));
|
|
547
638
|
return plan.failed.length ? 1 : 0;
|
|
548
639
|
}
|
|
@@ -714,7 +805,12 @@ async function push(args) {
|
|
|
714
805
|
const title = buildMessage(message, changes.length, toSend.length, bi + 1, batches.length, card);
|
|
715
806
|
let result;
|
|
716
807
|
try {
|
|
717
|
-
result = await client.propose(
|
|
808
|
+
result = await client.propose(
|
|
809
|
+
projectId,
|
|
810
|
+
{ base, title, summary: '', changes, agent, prompt: promptText, card },
|
|
811
|
+
await mgr.valid()
|
|
812
|
+
);
|
|
813
|
+
for (const w of result.warnings || []) ui.warn(w);
|
|
718
814
|
config.persistCredsIfChanged(cfg, mgr.current);
|
|
719
815
|
} catch (e) {
|
|
720
816
|
// Una base que el servidor no conoce (proyecto recreado, historial
|
|
@@ -1131,7 +1227,7 @@ async function push(args) {
|
|
|
1131
1227
|
);
|
|
1132
1228
|
try {
|
|
1133
1229
|
const creds = await mgr.valid();
|
|
1134
|
-
await client.createCommit(projectId, msg, batchChanges, creds);
|
|
1230
|
+
await client.createCommit(projectId, msg, batchChanges, creds, { card });
|
|
1135
1231
|
committed += batchChanges.length;
|
|
1136
1232
|
commitCount += 1;
|
|
1137
1233
|
config.persistCredsIfChanged(cfg, mgr.current);
|
|
@@ -1175,7 +1271,7 @@ async function push(args) {
|
|
|
1175
1271
|
for (const u of uploaded) {
|
|
1176
1272
|
pushindex.record(index, u, pushedAt);
|
|
1177
1273
|
}
|
|
1178
|
-
saveIndex(dir, index, plan,
|
|
1274
|
+
saveIndex(dir, index, plan, allUploads, pushedAt);
|
|
1179
1275
|
}
|
|
1180
1276
|
|
|
1181
1277
|
// 11) Anotar la sincronización y la base nueva. Sólo si TODO quedó en la
|
|
@@ -1189,6 +1285,7 @@ async function push(args) {
|
|
|
1189
1285
|
|
|
1190
1286
|
// 12) Resumen final.
|
|
1191
1287
|
ui.info('');
|
|
1288
|
+
reportLeftOut(leftOut, remoteByPath);
|
|
1192
1289
|
for (const o of open) reportOpen(o);
|
|
1193
1290
|
if (stale > 0) {
|
|
1194
1291
|
ui.warn(
|
|
@@ -1291,26 +1388,154 @@ async function loadContent(u) {
|
|
|
1291
1388
|
return u.content;
|
|
1292
1389
|
}
|
|
1293
1390
|
|
|
1391
|
+
/**
|
|
1392
|
+
* Lo que `--only` dejó afuera. "Una pieza = una tarjeta" sólo funciona si la
|
|
1393
|
+
* CLI te recuerda lo que quedó atrás.
|
|
1394
|
+
*
|
|
1395
|
+
* "Cambiado" se decide contra el ÁRBOL REMOTO —falta allá, o está con otro
|
|
1396
|
+
* tamaño—, que es el mismo criterio de `shilo status` y no cuesta un request
|
|
1397
|
+
* extra: el árbol ya se recorrió para planificar. El índice local no sirve acá:
|
|
1398
|
+
* `pull` no lo escribe, así que en una carpeta que se bajó de nuevo queda viejo
|
|
1399
|
+
* y daría por cambiado medio repo.
|
|
1400
|
+
*
|
|
1401
|
+
* Misma concesión que `status`: dos archivos del mismo tamaño se dan por
|
|
1402
|
+
* iguales aunque difieran byte a byte.
|
|
1403
|
+
*/
|
|
1404
|
+
function reportLeftOut(leftOut, remoteByPath) {
|
|
1405
|
+
if (!leftOut || leftOut.length === 0) return;
|
|
1406
|
+
|
|
1407
|
+
const changed = leftOut.filter((u) => {
|
|
1408
|
+
const r = remoteByPath.get(u.relPath);
|
|
1409
|
+
if (!r) return true; // nunca subido
|
|
1410
|
+
return typeof r.size === 'number' && r.size !== u.size;
|
|
1411
|
+
});
|
|
1412
|
+
if (changed.length === 0) {
|
|
1413
|
+
ui.info(
|
|
1414
|
+
ui.c.dim(
|
|
1415
|
+
`${leftOut.length} archivo(s) quedaron fuera de este push (--only); ninguno tiene cambios sin subir.`
|
|
1416
|
+
)
|
|
1417
|
+
);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
ui.warn(`${changed.length} archivo(s) con cambios locales quedaron fuera de este push (--only):`);
|
|
1422
|
+
for (const u of changed.slice(0, SKIP_EXAMPLES)) ui.info(` ${ui.c.dim(u.relPath)}`);
|
|
1423
|
+
if (changed.length > SKIP_EXAMPLES) {
|
|
1424
|
+
ui.info(ui.c.dim(` … y ${changed.length - SKIP_EXAMPLES} más`));
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1294
1428
|
/**
|
|
1295
1429
|
* Registra en el índice los archivos que ya estaban idénticos (para no volver a
|
|
1296
1430
|
* compararlos), saca los que ya no existen y lo escribe.
|
|
1431
|
+
*
|
|
1432
|
+
* `presentUploads` es lo que EXISTE en la carpeta, no lo que se subió en esta
|
|
1433
|
+
* corrida: con `--only` son cosas distintas. Podar con la selección borraría del
|
|
1434
|
+
* índice las entradas del resto del repo, y el push completo siguiente tendría
|
|
1435
|
+
* que recompararlo todo — lento, no incorrecto, pero evitable. Las entradas
|
|
1436
|
+
* nuevas se fusionan sobre el índice ya leído de disco, no lo reemplazan.
|
|
1297
1437
|
*/
|
|
1298
|
-
function saveIndex(dir, index, plan,
|
|
1438
|
+
function saveIndex(dir, index, plan, presentUploads, pushedAt = Date.now()) {
|
|
1299
1439
|
for (const u of plan.unchanged) {
|
|
1300
1440
|
if (u.content != null) {
|
|
1301
1441
|
pushindex.record(index, { ...u, hash: pushindex.hashContent(u.content) }, pushedAt);
|
|
1302
1442
|
}
|
|
1303
1443
|
}
|
|
1304
|
-
pushindex.prune(index, new Set(
|
|
1444
|
+
pushindex.prune(index, new Set(presentUploads.map((u) => u.relPath)));
|
|
1305
1445
|
pushindex.write(dir, index);
|
|
1306
1446
|
}
|
|
1307
1447
|
|
|
1448
|
+
/**
|
|
1449
|
+
* El bloque `agent` que se declara al proponer, o null si nadie lo declaró.
|
|
1450
|
+
*
|
|
1451
|
+
* `--model` y `--session` sin `--agent` son un error y no un descuido que se
|
|
1452
|
+
* pueda completar solo: decir qué modelo escribió algo sin decir qué
|
|
1453
|
+
* herramienta lo corrió deja una procedencia que no se puede rastrear hasta
|
|
1454
|
+
* ningún lado.
|
|
1455
|
+
*/
|
|
1456
|
+
function buildAgent(args) {
|
|
1457
|
+
const name = typeof args.agent === 'string' && args.agent.trim() ? args.agent.trim() : null;
|
|
1458
|
+
const model = typeof args.model === 'string' && args.model.trim() ? args.model.trim() : null;
|
|
1459
|
+
const session = typeof args.session === 'string' && args.session.trim() ? args.session.trim() : null;
|
|
1460
|
+
|
|
1461
|
+
if (!name) {
|
|
1462
|
+
if (model || session) {
|
|
1463
|
+
return { error: 'Para declarar procedencia hace falta --agent <nombre>; --model y --session solos no alcanzan.' };
|
|
1464
|
+
}
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
return {
|
|
1469
|
+
name: name.slice(0, AGENT_FIELD_MAX),
|
|
1470
|
+
...(model ? { model: model.slice(0, AGENT_FIELD_MAX) } : {}),
|
|
1471
|
+
...(session ? { session_id: session.slice(0, AGENT_FIELD_MAX) } : {}),
|
|
1472
|
+
tool: 'push',
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* El prompt que se sube, si quien llama lo pidió explícitamente.
|
|
1478
|
+
*
|
|
1479
|
+
* No se infiere del entorno ni se lee de ningún archivo por convención: un
|
|
1480
|
+
* prompt puede llevar adentro cualquier cosa que alguien haya pegado en su
|
|
1481
|
+
* terminal, y subir eso sin que nadie lo haya decidido es lo mismo que
|
|
1482
|
+
* filtrarlo.
|
|
1483
|
+
*
|
|
1484
|
+
* Un prompt más largo que el techo se descarta con aviso, no se trunca: medio
|
|
1485
|
+
* prompt es peor que ninguno porque parece completo.
|
|
1486
|
+
*/
|
|
1487
|
+
function readPrompt(args, agent) {
|
|
1488
|
+
const file = typeof args['prompt-file'] === 'string' ? args['prompt-file'].trim() : '';
|
|
1489
|
+
const inline = typeof args.prompt === 'string' ? args.prompt : '';
|
|
1490
|
+
|
|
1491
|
+
if (!file && !inline) return { prompt: null, warnings: [] };
|
|
1492
|
+
if (file && inline) {
|
|
1493
|
+
return { error: 'Elegí uno: --prompt o --prompt-file, no los dos.' };
|
|
1494
|
+
}
|
|
1495
|
+
if (!agent || agent.error) {
|
|
1496
|
+
return { error: 'El prompt se guarda dentro de la procedencia del agente: agregá --agent <nombre>.' };
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
let text = inline;
|
|
1500
|
+
if (file) {
|
|
1501
|
+
const resolved = path.resolve(file);
|
|
1502
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
|
|
1503
|
+
return { error: `No encontramos el archivo del prompt: ${resolved}` };
|
|
1504
|
+
}
|
|
1505
|
+
try {
|
|
1506
|
+
text = fs.readFileSync(resolved, 'utf8');
|
|
1507
|
+
} catch (e) {
|
|
1508
|
+
return { error: `No se pudo leer el prompt: ${e.message}` };
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (!text.trim()) return { prompt: null, warnings: [] };
|
|
1513
|
+
|
|
1514
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
1515
|
+
if (bytes > PROMPT_MAX_BYTES) {
|
|
1516
|
+
return {
|
|
1517
|
+
prompt: null,
|
|
1518
|
+
warnings: [
|
|
1519
|
+
`El prompt pesa ${Math.round(bytes / 1024)} KB y el techo es ` +
|
|
1520
|
+
`${PROMPT_MAX_BYTES / 1024} KB: se guarda el cambio sin él.`,
|
|
1521
|
+
],
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return { prompt: text, warnings: [] };
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1308
1528
|
/**
|
|
1309
1529
|
* Arma el mensaje del commit. Si vino `--card`, se agrega como trailer
|
|
1310
1530
|
* `[card: <id>]` al final: queda visible en el historial y es fácil de grepear
|
|
1311
1531
|
* para cruzar un cambio con la tarjeta que lo pidió (Metrónomo u otro tablero).
|
|
1312
1532
|
* El backend corta en 500 caracteres, así que recortamos el mensaje del usuario
|
|
1313
1533
|
* antes que el trailer.
|
|
1534
|
+
*
|
|
1535
|
+
* Desde la Etapa 4 la tarjeta viaja ADEMÁS aparte, en `provenance.card`, que es
|
|
1536
|
+
* lo que se puede consultar (`GET /api/commits/<id>?card=…`). El trailer no se
|
|
1537
|
+
* saca ni se va a sacar: es el formato de todo lo ya guardado, y la procedencia
|
|
1538
|
+
* entra en el oid del commit, así que lo viejo no se puede completar.
|
|
1314
1539
|
*/
|
|
1315
1540
|
function buildMessage(userMsg, batchCount, total, partNo, nBatches, card) {
|
|
1316
1541
|
let base = userMsg || `shilo push: ${total} archivo(s)`;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const config = require('../config');
|
|
4
|
+
const ui = require('../ui');
|
|
5
|
+
const sync = require('../sync');
|
|
6
|
+
const { resolveContext } = require('../context');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* shilo review [versión] [--aprobado | --reparos | --rechazado] [-m <nota>] [--json]
|
|
10
|
+
*
|
|
11
|
+
* Revisión humana (Etapa 4). Sin veredicto, lista quién revisó esa versión y
|
|
12
|
+
* qué dijo. Con uno, deja constancia. La versión es `actual` si no se dice
|
|
13
|
+
* otra cosa, o lo que imprime `shilo log` (alcanza un prefijo de 7 caracteres).
|
|
14
|
+
*
|
|
15
|
+
* El positional es una versión, no una carpeta: el contexto se resuelve siempre
|
|
16
|
+
* sobre el directorio actual, igual que en `log`.
|
|
17
|
+
*/
|
|
18
|
+
const FLAGS = { aprobado: 'aprobado', reparos: 'con_reparos', rechazado: 'rechazado' };
|
|
19
|
+
|
|
20
|
+
const ETIQUETAS = {
|
|
21
|
+
aprobado: 'aprobado',
|
|
22
|
+
con_reparos: 'con reparos',
|
|
23
|
+
rechazado: 'rechazado',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async function review(args) {
|
|
27
|
+
const version = typeof args._[0] === 'string' && args._[0].trim() ? args._[0].trim() : 'actual';
|
|
28
|
+
const elegidos = Object.keys(FLAGS).filter((f) => args[f]);
|
|
29
|
+
if (elegidos.length > 1) {
|
|
30
|
+
ui.error('Elegí un solo veredicto: --aprobado, --reparos o --rechazado.');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const verdict = elegidos.length ? FLAGS[elegidos[0]] : null;
|
|
34
|
+
const note = typeof args.message === 'string' ? args.message.trim() : '';
|
|
35
|
+
|
|
36
|
+
if (!verdict && note) {
|
|
37
|
+
ui.error('La nota va con un veredicto: --aprobado, --reparos o --rechazado.');
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
// La 017 lo exige igual; avisarlo acá ahorra un viaje y dice por qué.
|
|
41
|
+
if (verdict && verdict !== 'aprobado' && !note) {
|
|
42
|
+
ui.error('Un reparo o un rechazo necesita una nota que diga qué falta: -m "<nota>".');
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ctx = resolveContext({ ...args, _: [] });
|
|
47
|
+
if (!ctx.ok) return ctx.code;
|
|
48
|
+
const { projectId, cfg, client, mgr } = ctx;
|
|
49
|
+
|
|
50
|
+
let result;
|
|
51
|
+
try {
|
|
52
|
+
const creds = await mgr.valid();
|
|
53
|
+
result = verdict
|
|
54
|
+
? await client.review(projectId, { version, verdict, note }, creds)
|
|
55
|
+
: await client.listReviews(projectId, version, creds);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
ui.error(e.message);
|
|
58
|
+
return 1;
|
|
59
|
+
} finally {
|
|
60
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (args.json) {
|
|
64
|
+
console.log(JSON.stringify({ projectId, version: result.version, reviews: result.reviews }, null, 2));
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const corto = result.version ? String(result.version).slice(0, 8) : version;
|
|
69
|
+
if (verdict) ui.success(`Revisión registrada: ${ETIQUETAS[verdict]} (${corto}).`);
|
|
70
|
+
|
|
71
|
+
ui.info(`Revisiones de ${ui.c.yellow(corto)}`);
|
|
72
|
+
ui.info('');
|
|
73
|
+
if (result.reviews.length === 0) {
|
|
74
|
+
ui.info('Nadie la revisó todavía.');
|
|
75
|
+
ui.info(ui.c.dim('Para revisarla: shilo review ' + (version === 'actual' ? '' : version + ' ') + '--aprobado'));
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
for (const r of result.reviews) {
|
|
79
|
+
ui.info(`${pintar(r.verdict)} ${ui.c.bold(sync.describeAuthor(r.reviewer))} ${ui.c.dim(sync.formatDate(r.reviewed_at))}`);
|
|
80
|
+
if (r.note) ui.info(` ${r.note}`);
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pintar(verdict) {
|
|
86
|
+
const texto = ETIQUETAS[verdict] || verdict;
|
|
87
|
+
if (verdict === 'aprobado') return ui.c.green(texto);
|
|
88
|
+
if (verdict === 'rechazado') return ui.c.red(texto);
|
|
89
|
+
return ui.c.yellow(texto);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Una marca corta para `shilo log`: la revisión más reciente manda. */
|
|
93
|
+
function marcaDeRevision(reviews) {
|
|
94
|
+
if (!Array.isArray(reviews) || reviews.length === 0) return '';
|
|
95
|
+
const ultima = reviews[0];
|
|
96
|
+
const extra = reviews.length > 1 ? ` +${reviews.length - 1}` : '';
|
|
97
|
+
return `${pintar(ultima.verdict)} por ${sync.describeAuthor(ultima.reviewer)}${extra}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = review;
|
|
101
|
+
module.exports.marcaDeRevision = marcaDeRevision;
|
package/src/commands/status.js
CHANGED
|
@@ -57,10 +57,24 @@ async function status(args) {
|
|
|
57
57
|
|
|
58
58
|
// 3) "Detrás del remoto" = lo que otro subió y yo no tengo igual: archivos
|
|
59
59
|
// que faltan localmente, o que difieren y cambiaron después de mi lastSync.
|
|
60
|
+
//
|
|
61
|
+
// Lo que está ignorado localmente NO cuenta como "atrás": `walkLocal` no
|
|
62
|
+
// lo ve, así que quedaría en `onlyRemote` para siempre y el exit 2 —que
|
|
63
|
+
// significa "pull antes de trabajar"— se volvería ruido permanente. Se
|
|
64
|
+
// informa aparte: que alguien haya subido tu `settings.local.json` importa,
|
|
65
|
+
// pero no es trabajo que te falte bajar.
|
|
66
|
+
const isIgnoredPath = sync.makePathMatcher(dir);
|
|
67
|
+
|
|
60
68
|
const behind = [];
|
|
69
|
+
const ignoredRemote = [];
|
|
61
70
|
for (const item of cmp.onlyRemote) {
|
|
71
|
+
if (isIgnoredPath(item.repoPath)) {
|
|
72
|
+
ignoredRemote.push({ ...item, kind: 'ignorado local' });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
62
75
|
behind.push({ ...item, kind: 'falta local' });
|
|
63
76
|
}
|
|
77
|
+
ignoredRemote.sort((a, b) => a.repoPath.localeCompare(b.repoPath));
|
|
64
78
|
for (const item of cmp.differing) {
|
|
65
79
|
if (sync.changedSinceSync(item.remote, lastSync) !== false) {
|
|
66
80
|
behind.push({ ...item, kind: 'difiere' });
|
|
@@ -97,6 +111,9 @@ async function status(args) {
|
|
|
97
111
|
localFiles: local.files.length,
|
|
98
112
|
behind: behind.map((i) => ({ path: i.repoPath, kind: i.kind, lastCommit: decorate(i) })),
|
|
99
113
|
ahead: ahead.map((i) => ({ path: i.repoPath, kind: i.kind })),
|
|
114
|
+
// Campo aparte a propósito: si fueran parte de `behind`, un script
|
|
115
|
+
// volvería a leer "estás desactualizado" por algo que nunca vas a bajar.
|
|
116
|
+
ignoredRemote: ignoredRemote.map((i) => ({ path: i.repoPath })),
|
|
100
117
|
unchanged: cmp.same.length,
|
|
101
118
|
listErrors: tree.errors,
|
|
102
119
|
},
|
|
@@ -125,8 +142,25 @@ async function status(args) {
|
|
|
125
142
|
}
|
|
126
143
|
ui.info('');
|
|
127
144
|
|
|
145
|
+
/** Los ignorados que existen en el remoto: una línea, o la lista con --show-ignored. */
|
|
146
|
+
const reportIgnored = () => {
|
|
147
|
+
if (ignoredRemote.length === 0) return;
|
|
148
|
+
ui.info(
|
|
149
|
+
ui.c.dim(
|
|
150
|
+
`${ignoredRemote.length} archivo(s) existen en el remoto pero están ignorados localmente` +
|
|
151
|
+
(args['show-ignored'] ? ':' : ' (--show-ignored para verlos).')
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
if (args['show-ignored']) {
|
|
155
|
+
for (const item of ignoredRemote) ui.info(` ${ui.c.dim('·')} ${ui.c.dim(item.repoPath)}`);
|
|
156
|
+
}
|
|
157
|
+
ui.info('');
|
|
158
|
+
};
|
|
159
|
+
|
|
128
160
|
if (behind.length === 0 && ahead.length === 0) {
|
|
129
161
|
ui.success(`Al día: ${cmp.same.length} archivo(s) coinciden con el remoto.`);
|
|
162
|
+
ui.info('');
|
|
163
|
+
reportIgnored();
|
|
130
164
|
return 0;
|
|
131
165
|
}
|
|
132
166
|
|
|
@@ -156,6 +190,8 @@ async function status(args) {
|
|
|
156
190
|
ui.info('');
|
|
157
191
|
}
|
|
158
192
|
|
|
193
|
+
reportIgnored();
|
|
194
|
+
|
|
159
195
|
ui.info(
|
|
160
196
|
ui.c.dim(
|
|
161
197
|
` ${cmp.same.length} sin cambios (por tamaño) ` +
|