@jpkme/diff-review 0.1.4 → 0.1.6

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 (73) hide show
  1. package/README.md +4 -4
  2. package/bin/diff-review.mjs +240 -90
  3. package/lib/autocomplete-es.mjs +143 -0
  4. package/lib/check-report.mjs +213 -0
  5. package/lib/check-report.test.mjs +46 -0
  6. package/lib/output-name.mjs +18 -30
  7. package/lib/output-name.test.mjs +10 -27
  8. package/lib/prompt.mjs +29 -10
  9. package/package.json +3 -2
  10. package/schema.json +12 -0
  11. package/web/build/client/_app/immutable/chunks/{CAnYe5db.js → Ber_0nz7.js} +1 -1
  12. package/web/build/client/_app/immutable/chunks/Ber_0nz7.js.br +2 -0
  13. package/web/build/client/_app/immutable/chunks/Ber_0nz7.js.gz +0 -0
  14. package/web/build/client/_app/immutable/chunks/{BaYDuTXu.js → DRga3k74.js} +28 -17
  15. package/web/build/client/_app/immutable/chunks/DRga3k74.js.br +0 -0
  16. package/web/build/client/_app/immutable/chunks/DRga3k74.js.gz +0 -0
  17. package/web/build/client/_app/immutable/chunks/{Gya4IU1I.js → zvcaSoGP.js} +2 -2
  18. package/web/build/client/_app/immutable/chunks/zvcaSoGP.js.br +0 -0
  19. package/web/build/client/_app/immutable/chunks/zvcaSoGP.js.gz +0 -0
  20. package/web/build/client/_app/immutable/entry/{app.e96uPVtl.js → app.BP_R_kTF.js} +2 -2
  21. package/web/build/client/_app/immutable/entry/app.BP_R_kTF.js.br +0 -0
  22. package/web/build/client/_app/immutable/entry/app.BP_R_kTF.js.gz +0 -0
  23. package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js +1 -0
  24. package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js.br +1 -0
  25. package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js.gz +0 -0
  26. package/web/build/client/_app/immutable/nodes/{1.V-g5-sfL.js → 1.CGJk77oo.js} +1 -1
  27. package/web/build/client/_app/immutable/nodes/1.CGJk77oo.js.br +3 -0
  28. package/web/build/client/_app/immutable/nodes/1.CGJk77oo.js.gz +0 -0
  29. package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js +66 -0
  30. package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js.br +0 -0
  31. package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js.gz +0 -0
  32. package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js +170 -0
  33. package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js.br +0 -0
  34. package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js.gz +0 -0
  35. package/web/build/client/_app/version.json +1 -1
  36. package/web/build/client/_app/version.json.br +0 -0
  37. package/web/build/client/_app/version.json.gz +0 -0
  38. package/web/build/handler.js +3 -3
  39. package/web/build/index.js +3 -3
  40. package/web/build/server/chunks/1-CRg9pswO.js +7 -0
  41. package/web/build/server/chunks/{1-DCKScL3A.js.map → 1-CRg9pswO.js.map} +1 -1
  42. package/web/build/server/chunks/{2-BpBQQ02B.js → 2-BmYzresl.js} +2 -2
  43. package/web/build/server/chunks/{2-BpBQQ02B.js.map → 2-BmYzresl.js.map} +1 -1
  44. package/web/build/server/chunks/{3-rGkcNCVz.js → 3-DaeIJXI7.js} +2 -2
  45. package/web/build/server/chunks/{3-rGkcNCVz.js.map → 3-DaeIJXI7.js.map} +1 -1
  46. package/web/build/server/chunks/{handler-BaT-QavL.js → handler-2eew4UOl.js} +3 -3
  47. package/web/build/server/chunks/{handler-BaT-QavL.js.map → handler-2eew4UOl.js.map} +1 -1
  48. package/web/build/server/chunks/{index.js-C-K0YDBJ.js → index.js-DbiNsix3.js} +2 -2
  49. package/web/build/server/chunks/{index.js-C-K0YDBJ.js.map → index.js-DbiNsix3.js.map} +1 -1
  50. package/web/build/server/chunks/{manifest.js-CDz-Fem4.js → manifest.js-BYNXhn6E.js} +6 -6
  51. package/web/build/server/chunks/{manifest.js-CDz-Fem4.js.map → manifest.js-BYNXhn6E.js.map} +1 -1
  52. package/web/build/server/chunks/{remote-1g4ayag-DXdFe1BG.js → remote-1g4ayag-DESnyPRn.js} +27 -40
  53. package/web/build/server/chunks/{remote-1g4ayag-DXdFe1BG.js.map → remote-1g4ayag-DESnyPRn.js.map} +1 -1
  54. package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.br +0 -0
  55. package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.gz +0 -0
  56. package/web/build/client/_app/immutable/chunks/CAnYe5db.js.br +0 -1
  57. package/web/build/client/_app/immutable/chunks/CAnYe5db.js.gz +0 -0
  58. package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.br +0 -0
  59. package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.gz +0 -0
  60. package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.br +0 -0
  61. package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.gz +0 -0
  62. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js +0 -1
  63. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.br +0 -0
  64. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.gz +0 -0
  65. package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.br +0 -0
  66. package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.gz +0 -0
  67. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js +0 -66
  68. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.br +0 -0
  69. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.gz +0 -0
  70. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js +0 -170
  71. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.br +0 -0
  72. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.gz +0 -0
  73. package/web/build/server/chunks/1-DCKScL3A.js +0 -7
package/README.md CHANGED
@@ -26,11 +26,11 @@ npx @jpkme/diff-review
26
26
 
27
27
  Desde un **repo git**, el default es terminal-first:
28
28
 
29
- 1. Loguea repo / branch y copia el prompt al portapapeles.
30
- 2. Espera el JSON (`diff-report_{branchTip}-{baseTip}_YYYYMMDD-HHmmss.json`) en la raíz del repo.
29
+ 1. Elegís **branch** y **base** con prompts en la terminal (`@clack/prompts`; tipeá para filtrar).
30
+ 2. Copia el prompt al portapapeles y espera el JSON (`diff-rev_{branchTip}_{baseTip}.json`).
31
31
  3. Valida y abre el viewer con el reporte ya importado.
32
32
 
33
- Si el branch actual y la base son iguales (diff vacío), abre la UI directamente para que elijas otra base o branch.
33
+ Si pasás `--branch` y `--base` juntos, saltea los selects. Si quedan iguales (diff vacío), abre la UI.
34
34
 
35
35
  Para abrir la UI primero (sin esperar el JSON):
36
36
 
@@ -55,7 +55,7 @@ npm start # mismo que el bin global
55
55
 
56
56
  1. Pegá la ruta (o URL) del repo y elegí branch / base — o lanzá `diff-review` / `diff-review --ui` desde el repo.
57
57
  2. Copiá el prompt (o el comando de skill `/diff-review`).
58
- 3. Corrélo en tu agente; que escriba `diff-report_{tips}_{stamp}.json` (también acepta el legacy `diff-review-output….json`).
58
+ 3. Corrélo en tu agente; que escriba `diff-rev_{branchTip}_{baseTip}.json` y lo valide con `npx --yes @jpkme/diff-review validate <archivo>` (también acepta nombres legacy).
59
59
  4. Arrastrá ese JSON a la dropzone (o dejá que el CLI lo importe solo). La UI hidrata los diffs con git.
60
60
 
61
61
  "Ver ejemplo" carga un reporte de muestra sin tocar un repo.
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * CLI híbrido C:
4
- * diff-review → terminal: prompt + clipboard + espera JSON → abre viewer
4
+ * diff-review → terminal: elegir branch/base → prompt + wait JSON → viewer
5
5
  * diff-review --ui → web-first (UI inmediata)
6
6
  *
7
7
  * diff-review --port 5191
8
+ * diff-review --branch x --base y → saltea selects de Clack
8
9
  * diff-review --no-open
9
10
  */
10
11
 
@@ -13,11 +14,14 @@ import { existsSync } from 'node:fs';
13
14
  import { createServer } from 'node:net';
14
15
  import { dirname, join } from 'node:path';
15
16
  import { fileURLToPath } from 'node:url';
17
+ import * as p from '@clack/prompts';
18
+ import { autocompleteEs } from '../lib/autocomplete-es.mjs';
16
19
  import { copyTextToClipboard } from '../lib/clipboard.mjs';
17
20
  import { branchTips, getRemoteUrl, listBranches, tryResolveGitRoot } from '../lib/git.mjs';
18
- import { formatOutputStamp, makeOutputFilename } from '../lib/output-name.mjs';
21
+ import { makeOutputFilename } from '../lib/output-name.mjs';
19
22
  import { buildPrompt } from '../lib/prompt.mjs';
20
23
  import { waitForReviewOutput } from '../lib/wait-output.mjs';
24
+ import { runValidateCli } from '../lib/check-report.mjs';
21
25
 
22
26
  const here = dirname(fileURLToPath(import.meta.url));
23
27
  const packageRoot = join(here, '..');
@@ -47,17 +51,20 @@ function printHelp() {
47
51
 
48
52
  Usage:
49
53
  diff-review [options]
54
+ diff-review validate <file.json>
50
55
 
51
- Por defecto (desde un repo git): copia el prompt, espera el JSON y abre el viewer.
52
- Si branch y base coinciden, abre la UI para que elijas otra combinación.
56
+ Por defecto (desde un repo git): elegís branch/base en la terminal (Clack),
57
+ copia el prompt, espera el JSON y abre el viewer.
58
+ \`validate\` revisa el JSON y, si falla, dice cómo seguir (exit 0 = OK).
59
+ Si pasás --branch y --base juntos, saltea los selects.
53
60
  Con --ui: abre la UI de inmediato (web-first).
54
61
 
55
62
  Options:
56
63
  -p, --port <n> Puerto (default ${DEFAULT_PORT})
57
64
  --host <h> Host (default ${DEFAULT_HOST})
58
65
  --ui Abrir la UI primero (sin esperar JSON)
59
- --base <b> Base del merge-base (default: develop / detección)
60
- --branch <b> Branch a revisar (default: branch actual)
66
+ --base <b> Base del merge-base (salta el select de base)
67
+ --branch <b> Branch a revisar (salta el select de branch)
61
68
  --no-open No abrir el navegador
62
69
  -h, --help Esta ayuda
63
70
 
@@ -130,37 +137,167 @@ function canListen(host, port) {
130
137
  }
131
138
 
132
139
  /**
133
- * @returns {{ repo: string, branch: string, base: string, remoteUrl?: string } | null}
140
+ * @returns {{
141
+ * repo: string,
142
+ * listed: { branches: string[], current: string, defaultBase: string },
143
+ * remoteUrl?: string
144
+ * } | null}
134
145
  */
135
- function resolveLaunchRepo(opts) {
146
+ function loadRepoLaunch() {
136
147
  const cwd = process.cwd();
137
148
  const root = tryResolveGitRoot(cwd);
138
149
  if (!root) return null;
139
150
  const listed = listBranches(root);
140
- const branch =
141
- String(opts.branch || '').trim() ||
142
- (listed.current && listed.current !== 'HEAD' ? listed.current : listed.branches[0] || '');
143
- const base =
144
- String(opts.base || '').trim() ||
145
- (listed.branches.includes('develop') ? 'develop' : listed.defaultBase);
146
151
  const remoteUrl = getRemoteUrl(root) || undefined;
147
- return { repo: root, branch, base, remoteUrl };
152
+ return { repo: root, listed, remoteUrl };
148
153
  }
149
154
 
150
- function logRepoContext(ctx) {
151
- console.log(`Repo: ${ctx.repo}`);
152
- console.log(`Branch: ${ctx.branch} (base: ${ctx.base})`);
155
+ function defaultBranch(listed) {
156
+ return listed.current && listed.current !== 'HEAD' ? listed.current : listed.branches[0] || '';
153
157
  }
154
158
 
155
- function copyPromptOrWarn(prompt) {
156
- if (copyTextToClipboard(prompt)) {
157
- console.log('Prompt copiado al portapapeles.');
158
- return true;
159
+ function defaultBase(listed, branch) {
160
+ const preferred = listed.branches.includes('develop') ? 'develop' : listed.defaultBase;
161
+ if (preferred && preferred !== branch) return preferred;
162
+ return listed.branches.find((b) => b !== branch) || preferred || '';
163
+ }
164
+
165
+ function branchOptions(listed) {
166
+ return listed.branches.map((b) => ({
167
+ value: b,
168
+ label: b,
169
+ hint: b === listed.current ? 'actual' : b === listed.defaultBase ? 'base típica' : undefined
170
+ }));
171
+ }
172
+
173
+ function ensureValue(value, label) {
174
+ if (p.isCancel(value)) {
175
+ p.cancel('Cancelado.');
176
+ process.exit(0);
177
+ }
178
+ if (value == null || value === '') {
179
+ p.cancel(`Falta ${label}.`);
180
+ process.exit(1);
159
181
  }
160
- console.log('No pude copiar al portapapeles; pegá el prompt a mano desde la UI o reintentá.');
182
+ return /** @type {string} */ (value);
183
+ }
184
+
185
+ /**
186
+ * Selects interactivos salvo que --branch y --base vengan juntos.
187
+ * @param {{ listed: { branches: string[], current: string, defaultBase: string }, repo: string }} launch
188
+ * @param {{ branch: string, base: string }} opts
189
+ */
190
+ async function resolveBranchBase(launch, opts) {
191
+ const flagBranch = String(opts.branch || '').trim();
192
+ const flagBase = String(opts.base || '').trim();
193
+ const bothFlags = Boolean(flagBranch && flagBase);
194
+
195
+ if (bothFlags) {
196
+ return { branch: flagBranch, base: flagBase, prompted: false };
197
+ }
198
+
199
+ if (!p.isTTY) {
200
+ const branch = flagBranch || defaultBranch(launch.listed);
201
+ const base = flagBase || defaultBase(launch.listed, branch);
202
+ return { branch, base, prompted: false };
203
+ }
204
+
205
+ if (!launch.listed.branches.length) {
206
+ p.cancel('No hay branches en este repo.');
207
+ process.exit(1);
208
+ }
209
+
210
+ const options = branchOptions(launch.listed);
211
+ p.intro('diff-review');
212
+ p.log.info(launch.repo);
213
+
214
+ let branch = flagBranch;
215
+ if (!branch) {
216
+ branch = ensureValue(
217
+ await autocompleteEs({
218
+ message: 'Branch a revisar',
219
+ options,
220
+ initialValue: defaultBranch(launch.listed) || undefined,
221
+ maxItems: 12
222
+ }),
223
+ 'branch'
224
+ );
225
+ } else {
226
+ p.log.step(`Branch: ${branch}`);
227
+ }
228
+
229
+ let base = flagBase;
230
+ if (!base) {
231
+ base = ensureValue(
232
+ await autocompleteEs({
233
+ message: 'Base (merge-base)',
234
+ options,
235
+ initialValue: defaultBase(launch.listed, branch) || undefined,
236
+ maxItems: 12
237
+ }),
238
+ 'base'
239
+ );
240
+ } else {
241
+ p.log.step(`Base: ${base}`);
242
+ }
243
+
244
+ while (branch === base) {
245
+ p.log.warn(`Branch y base son iguales (${branch}): el diff estaría vacío.`);
246
+ base = ensureValue(
247
+ await autocompleteEs({
248
+ message: 'Elegí otra base',
249
+ options,
250
+ initialValue: defaultBase(launch.listed, branch) || undefined,
251
+ maxItems: 12
252
+ }),
253
+ 'base'
254
+ );
255
+ }
256
+
257
+ return { branch, base, prompted: true };
258
+ }
259
+
260
+ function buildOutputFilename(repo, branch, base) {
261
+ try {
262
+ const tips = branchTips(repo, branch, base);
263
+ return makeOutputFilename({
264
+ branchSha: tips.branchSha,
265
+ baseSha: tips.baseSha
266
+ });
267
+ } catch (err) {
268
+ console.warn(
269
+ 'No pude resolver tips para el nombre del JSON:',
270
+ err instanceof Error ? err.message : err
271
+ );
272
+ return makeOutputFilename({});
273
+ }
274
+ }
275
+
276
+ function copyPromptOrWarn(prompt) {
277
+ if (copyTextToClipboard(prompt)) return true;
278
+ p.log.warn(
279
+ 'No pude copiar al portapapeles. Copiá el prompt a mano desde la terminal o reintentá.'
280
+ );
161
281
  return false;
162
282
  }
163
283
 
284
+ function explainWaitingForAgent(outputFilename, { copied, useClack }) {
285
+ const copyMsg = copied
286
+ ? 'Se copió un prompt al portapapeles: pegalo en el chat de tu agente de IA para generar el contenido de la review.'
287
+ : 'Generá el JSON con tu agente de IA usando el prompt de esta sesión (no se pudo copiar solo al portapapeles).';
288
+ const waitMsg = `Aguardando el resultado del agente (${outputFilename} en la raíz del repo). La UI web se abre cuando ese archivo esté listo.`;
289
+
290
+ if (useClack) {
291
+ p.log.success(copyMsg);
292
+ p.log.info(waitMsg);
293
+ p.outro('Ctrl+C cancela la espera.');
294
+ return;
295
+ }
296
+ console.log(copyMsg);
297
+ console.log(waitMsg);
298
+ console.log('(Ctrl+C cancela la espera)');
299
+ }
300
+
164
301
  /**
165
302
  * @param {{ host: string, port: number, open: boolean, launchCwd: string, importFile?: string, openPath?: string }} cfg
166
303
  */
@@ -228,113 +365,126 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
228
365
  process.on('SIGTERM', stop);
229
366
  }
230
367
 
368
+ async function runUiMode(opts, launch, launchCwd) {
369
+ const listed = launch?.listed;
370
+ const branch = String(opts.branch || '').trim() || (listed ? defaultBranch(listed) : '');
371
+ const base =
372
+ String(opts.base || '').trim() || (listed ? defaultBase(listed, branch) : 'develop');
373
+ const outputFilename = launch
374
+ ? buildOutputFilename(launch.repo, branch, base)
375
+ : makeOutputFilename({});
376
+
377
+ if (launch) {
378
+ console.log(`Repo: ${launch.repo}`);
379
+ console.log(`Branch: ${branch} (base: ${base})`);
380
+ const prompt = buildPrompt(
381
+ {
382
+ source: 'local',
383
+ repo: launch.repo,
384
+ branch,
385
+ base,
386
+ remoteUrl: launch.remoteUrl
387
+ },
388
+ { outputFilename }
389
+ );
390
+ if (copyTextToClipboard(prompt)) {
391
+ console.log('Prompt copiado al portapapeles.');
392
+ } else {
393
+ console.log('No pude copiar al portapapeles; pegá el prompt a mano desde la UI o reintentá.');
394
+ }
395
+ } else {
396
+ console.log('Sin repo git en el CWD — la UI arranca vacía.');
397
+ }
398
+
399
+ await startServer({
400
+ host: opts.host,
401
+ port: opts.port,
402
+ open: opts.open,
403
+ launchCwd,
404
+ openPath: launch ? `/?notify=prompt&out=${encodeURIComponent(outputFilename)}` : '/'
405
+ });
406
+ }
407
+
231
408
  async function main() {
232
- const opts = parseArgs(process.argv.slice(2));
409
+ const rawArgv = process.argv.slice(2);
410
+ if (rawArgv[0] === 'validate') {
411
+ process.exit(runValidateCli(rawArgv.slice(1)));
412
+ }
413
+
414
+ const opts = parseArgs(rawArgv);
233
415
  if (opts.help) {
234
416
  printHelp();
235
417
  process.exit(0);
236
418
  }
237
419
 
238
420
  const launchCwd = process.cwd();
239
- const ctx = resolveLaunchRepo(opts);
240
- const outputStamp = formatOutputStamp();
241
- /** @type {string} */
242
- let outputFilename = makeOutputFilename({ stamp: outputStamp });
243
- if (ctx) {
244
- try {
245
- const tips = branchTips(ctx.repo, ctx.branch, ctx.base);
246
- outputFilename = makeOutputFilename({
247
- branchSha: tips.branchSha,
248
- baseSha: tips.baseSha,
249
- stamp: outputStamp
250
- });
251
- } catch (err) {
252
- console.warn(
253
- 'No pude resolver tips para el nombre del JSON:',
254
- err instanceof Error ? err.message : err
255
- );
256
- }
421
+ const launch = loadRepoLaunch();
422
+
423
+ if (opts.ui) {
424
+ await runUiMode(opts, launch, launchCwd);
425
+ return;
257
426
  }
258
427
 
259
- /** Si branch===base el three-dot está vacío: forzar UI para elegir. */
260
- let openUi = opts.ui;
261
- let skipPromptCopy = false;
262
- if (!openUi && ctx && ctx.branch && ctx.branch === ctx.base) {
263
- console.log(
264
- `Branch y base son iguales (${ctx.branch}): el diff estaría vacío.\nAbro la UI para que elijas otra base o branch.`
428
+ if (!launch) {
429
+ console.error(
430
+ 'No hay un repositorio git en el directorio actual.\nUsá --ui para abrir la interfaz, o corré diff-review desde un repo.'
265
431
  );
266
- openUi = true;
267
- skipPromptCopy = true;
432
+ process.exit(1);
268
433
  }
269
434
 
270
- if (openUi) {
271
- if (ctx) {
272
- logRepoContext(ctx);
273
- if (!skipPromptCopy) {
274
- const prompt = buildPrompt(
275
- {
276
- source: 'local',
277
- repo: ctx.repo,
278
- branch: ctx.branch,
279
- base: ctx.base,
280
- remoteUrl: ctx.remoteUrl
281
- },
282
- { outputFilename }
283
- );
284
- copyPromptOrWarn(prompt);
285
- }
286
- } else {
287
- console.log('Sin repo git en el CWD — la UI arranca vacía.');
288
- }
435
+ const { branch, base, prompted } = await resolveBranchBase(launch, opts);
436
+
437
+ if (branch === base) {
438
+ // Ambos flags iguales / sin TTY: no se puede corregir en selects.
439
+ console.log(
440
+ `Branch y base son iguales (${branch}): el diff estaría vacío.\nAbro la UI para que elijas otra combinación.`
441
+ );
289
442
  await startServer({
290
443
  host: opts.host,
291
444
  port: opts.port,
292
445
  open: opts.open,
293
446
  launchCwd,
294
- openPath:
295
- ctx && !skipPromptCopy
296
- ? `/?notify=prompt&out=${encodeURIComponent(outputFilename)}`
297
- : '/'
447
+ openPath: '/'
298
448
  });
299
449
  return;
300
450
  }
301
451
 
302
- // Flujo terminal por defecto
303
- if (!ctx) {
304
- console.error(
305
- 'No hay un repositorio git en el directorio actual.\nUsá --ui para abrir la interfaz, o corré diff-review desde un repo.'
306
- );
307
- process.exit(1);
452
+ const outputFilename = buildOutputFilename(launch.repo, branch, base);
453
+
454
+ if (!prompted) {
455
+ console.log(`Repo: ${launch.repo}`);
456
+ console.log(`Branch: ${branch} (base: ${base})`);
457
+ } else {
458
+ p.note(`${branch} ← ${base}\n${outputFilename}`, 'Review');
308
459
  }
309
460
 
310
- logRepoContext(ctx);
311
- console.log(`Salida: ${outputFilename}`);
312
461
  const prompt = buildPrompt(
313
462
  {
314
463
  source: 'local',
315
- repo: ctx.repo,
316
- branch: ctx.branch,
317
- base: ctx.base,
318
- remoteUrl: ctx.remoteUrl
464
+ repo: launch.repo,
465
+ branch,
466
+ base,
467
+ remoteUrl: launch.remoteUrl
319
468
  },
320
469
  { outputFilename }
321
470
  );
322
- copyPromptOrWarn(prompt);
323
- console.log(`Esperando ${outputFilename} en la raíz del repo… (Ctrl+C para cancelar)`);
324
471
 
325
- const result = await waitForReviewOutput(ctx.repo, outputFilename, {
472
+ const copied = copyPromptOrWarn(prompt);
473
+ explainWaitingForAgent(outputFilename, { copied, useClack: prompted });
474
+
475
+ const result = await waitForReviewOutput(launch.repo, outputFilename, {
326
476
  onInvalid: (errors) => {
327
477
  console.log(`JSON encontrado pero inválido (${errors.length} error(es)). Esperando corrección…`);
328
478
  for (const e of errors.slice(0, 5)) console.log(` · ${e}`);
329
479
  }
330
480
  });
331
481
 
332
- console.log('JSON OK. Abriendo viewer…');
482
+ console.log('JSON OK. Abriendo la UI web…');
333
483
  await startServer({
334
484
  host: opts.host,
335
485
  port: opts.port,
336
486
  open: opts.open,
337
- launchCwd: ctx.repo,
487
+ launchCwd: launch.repo,
338
488
  importFile: result.path,
339
489
  openPath: '/?import=1'
340
490
  });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Autocomplete estilo Clack con etiquetas en español
3
+ * ("Buscar:" en lugar de "Search:").
4
+ */
5
+ import { AutocompletePrompt, settings } from '@clack/core';
6
+ import {
7
+ S_BAR,
8
+ S_BAR_END,
9
+ S_RADIO_ACTIVE,
10
+ S_RADIO_INACTIVE,
11
+ limitOptions,
12
+ symbol
13
+ } from '@clack/prompts';
14
+ import { styleText } from 'node:util';
15
+
16
+ function optionLabel(opt) {
17
+ return opt.label ?? String(opt.value ?? '');
18
+ }
19
+
20
+ function defaultFilter(search, option) {
21
+ const q = String(search || '').toLowerCase();
22
+ if (!q) return true;
23
+ const label = optionLabel(option).toLowerCase();
24
+ const value = String(option.value ?? '').toLowerCase();
25
+ const hint = String(option.hint ?? '').toLowerCase();
26
+ return label.includes(q) || value.includes(q) || hint.includes(q);
27
+ }
28
+
29
+ /**
30
+ * @template Value
31
+ * @param {{
32
+ * message: string,
33
+ * options: Array<{ value: Value, label?: string, hint?: string, disabled?: boolean }>,
34
+ * initialValue?: Value,
35
+ * maxItems?: number,
36
+ * placeholder?: string,
37
+ * }} opts
38
+ * @returns {Promise<Value | symbol>}
39
+ */
40
+ export function autocompleteEs(opts) {
41
+ return new AutocompletePrompt({
42
+ options: opts.options,
43
+ initialValue: opts.initialValue !== undefined ? [opts.initialValue] : undefined,
44
+ filter: defaultFilter,
45
+ render() {
46
+ const withGuide = settings.withGuide;
47
+ const title = withGuide
48
+ ? [`${styleText('gray', S_BAR)}`, `${symbol(this.state)} ${opts.message}`]
49
+ : [`${symbol(this.state)} ${opts.message}`];
50
+ const input = this.userInput;
51
+ const all = this.options;
52
+ const placeholder = opts.placeholder;
53
+ const showPlaceholder = input === '' && placeholder !== undefined;
54
+
55
+ const styleOption = (opt, kind) => {
56
+ const label = optionLabel(opt);
57
+ const hint =
58
+ opt.hint && opt.value === this.focusedValue ? styleText('dim', ` (${opt.hint})`) : '';
59
+ switch (kind) {
60
+ case 'active':
61
+ return `${styleText('green', S_RADIO_ACTIVE)} ${label}${hint}`;
62
+ case 'inactive':
63
+ return `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', label)}`;
64
+ case 'disabled':
65
+ return `${styleText('gray', S_RADIO_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`;
66
+ default:
67
+ return label;
68
+ }
69
+ };
70
+
71
+ switch (this.state) {
72
+ case 'submit': {
73
+ const selected = this.selectedValues?.[0];
74
+ const label =
75
+ selected !== undefined
76
+ ? ` ${styleText('dim', optionLabel(all.find((o) => o.value === selected) || { value: selected }))}`
77
+ : '';
78
+ const bar = withGuide ? styleText('gray', S_BAR) : '';
79
+ return `${title.join('\n')}\n${bar}${label}`;
80
+ }
81
+ case 'cancel': {
82
+ const typed = input ? ` ${styleText(['strikethrough', 'dim'], input)}` : '';
83
+ const bar = withGuide ? styleText('gray', S_BAR) : '';
84
+ return `${title.join('\n')}\n${bar}${typed}`;
85
+ }
86
+ default: {
87
+ const color = this.state === 'error' ? 'yellow' : 'cyan';
88
+ const prefix = withGuide ? `${styleText(color, S_BAR)} ` : '';
89
+ const end = withGuide ? styleText(color, S_BAR_END) : '';
90
+
91
+ let field = '';
92
+ if (this.isNavigating || showPlaceholder) {
93
+ const shown = showPlaceholder ? placeholder : input;
94
+ field = shown !== '' ? ` ${styleText('dim', shown)}` : '';
95
+ } else {
96
+ field = ` ${this.userInputWithCursor}`;
97
+ }
98
+
99
+ const matchHint =
100
+ this.filteredOptions.length !== all.length
101
+ ? styleText(
102
+ 'dim',
103
+ ` (${this.filteredOptions.length} coincidencia${this.filteredOptions.length === 1 ? '' : 's'})`
104
+ )
105
+ : '';
106
+
107
+ const lines = [...title];
108
+ if (withGuide) lines.push(`${prefix.trimEnd()}`);
109
+ lines.push(`${prefix}${styleText('dim', 'Buscar:')}${field}${matchHint}`);
110
+
111
+ if (this.filteredOptions.length === 0 && input) {
112
+ lines.push(`${prefix}${styleText('yellow', 'Sin coincidencias')}`);
113
+ }
114
+ if (this.state === 'error') {
115
+ lines.push(`${prefix}${styleText('yellow', this.error)}`);
116
+ }
117
+
118
+ const hints = [
119
+ `${styleText('dim', '↑/↓')} elegir`,
120
+ `${styleText('dim', 'Enter')} confirmar`,
121
+ `${styleText('dim', 'Escribí')} para filtrar`
122
+ ];
123
+ const footer = [`${prefix}${hints.join(' · ')}`, end];
124
+
125
+ const options =
126
+ this.filteredOptions.length === 0
127
+ ? []
128
+ : limitOptions({
129
+ cursor: this.cursor,
130
+ options: this.filteredOptions,
131
+ columnPadding: withGuide ? 3 : 0,
132
+ rowPadding: lines.length + footer.length,
133
+ style: (opt, active) =>
134
+ styleOption(opt, opt.disabled ? 'disabled' : active ? 'active' : 'inactive'),
135
+ maxItems: opts.maxItems
136
+ });
137
+
138
+ return [...lines, ...options.map((row) => `${prefix}${row}`), ...footer].join('\n');
139
+ }
140
+ }
141
+ }
142
+ }).prompt();
143
+ }