@jpkme/diff-review 0.1.4 → 0.1.5

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 (71) hide show
  1. package/README.md +4 -4
  2. package/bin/diff-review.mjs +231 -89
  3. package/lib/autocomplete-es.mjs +143 -0
  4. package/lib/output-name.mjs +18 -30
  5. package/lib/output-name.test.mjs +10 -27
  6. package/lib/prompt.mjs +13 -7
  7. package/package.json +3 -2
  8. package/schema.json +12 -0
  9. package/web/build/client/_app/immutable/chunks/{CAnYe5db.js → CO0mUwMJ.js} +1 -1
  10. package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.br +2 -0
  11. package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.gz +0 -0
  12. package/web/build/client/_app/immutable/chunks/{BaYDuTXu.js → CdHZDAAo.js} +26 -23
  13. package/web/build/client/_app/immutable/chunks/CdHZDAAo.js.br +0 -0
  14. package/web/build/client/_app/immutable/chunks/CdHZDAAo.js.gz +0 -0
  15. package/web/build/client/_app/immutable/chunks/{Gya4IU1I.js → DzqTpz7_.js} +2 -2
  16. package/web/build/client/_app/immutable/chunks/DzqTpz7_.js.br +0 -0
  17. package/web/build/client/_app/immutable/chunks/DzqTpz7_.js.gz +0 -0
  18. package/web/build/client/_app/immutable/entry/{app.e96uPVtl.js → app.BRzz8J51.js} +2 -2
  19. package/web/build/client/_app/immutable/entry/app.BRzz8J51.js.br +0 -0
  20. package/web/build/client/_app/immutable/entry/app.BRzz8J51.js.gz +0 -0
  21. package/web/build/client/_app/immutable/entry/start.BSioRkqK.js +1 -0
  22. package/web/build/client/_app/immutable/entry/start.BSioRkqK.js.br +0 -0
  23. package/web/build/client/_app/immutable/entry/start.BSioRkqK.js.gz +0 -0
  24. package/web/build/client/_app/immutable/nodes/{1.V-g5-sfL.js → 1.ws8Khan5.js} +1 -1
  25. package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.br +1 -0
  26. package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.gz +0 -0
  27. package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js +66 -0
  28. package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js.br +0 -0
  29. package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js.gz +0 -0
  30. package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js +170 -0
  31. package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js.br +0 -0
  32. package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js.gz +0 -0
  33. package/web/build/client/_app/version.json +1 -1
  34. package/web/build/client/_app/version.json.br +0 -0
  35. package/web/build/client/_app/version.json.gz +0 -0
  36. package/web/build/handler.js +3 -3
  37. package/web/build/index.js +3 -3
  38. package/web/build/server/chunks/1-qrn24yN5.js +7 -0
  39. package/web/build/server/chunks/{1-DCKScL3A.js.map → 1-qrn24yN5.js.map} +1 -1
  40. package/web/build/server/chunks/{2-BpBQQ02B.js → 2-C7_Loqyn.js} +2 -2
  41. package/web/build/server/chunks/{2-BpBQQ02B.js.map → 2-C7_Loqyn.js.map} +1 -1
  42. package/web/build/server/chunks/{3-rGkcNCVz.js → 3-DZ5PBhOc.js} +2 -2
  43. package/web/build/server/chunks/{3-rGkcNCVz.js.map → 3-DZ5PBhOc.js.map} +1 -1
  44. package/web/build/server/chunks/{handler-BaT-QavL.js → handler-DD4vP3bU.js} +3 -3
  45. package/web/build/server/chunks/{handler-BaT-QavL.js.map → handler-DD4vP3bU.js.map} +1 -1
  46. package/web/build/server/chunks/{index.js-C-K0YDBJ.js → index.js-IhFPHwmp.js} +2 -2
  47. package/web/build/server/chunks/{index.js-C-K0YDBJ.js.map → index.js-IhFPHwmp.js.map} +1 -1
  48. package/web/build/server/chunks/{manifest.js-CDz-Fem4.js → manifest.js-Dk3RuAA5.js} +6 -6
  49. package/web/build/server/chunks/{manifest.js-CDz-Fem4.js.map → manifest.js-Dk3RuAA5.js.map} +1 -1
  50. package/web/build/server/chunks/{remote-1g4ayag-DXdFe1BG.js → remote-1g4ayag-BRL-uaKE.js} +27 -40
  51. package/web/build/server/chunks/{remote-1g4ayag-DXdFe1BG.js.map → remote-1g4ayag-BRL-uaKE.js.map} +1 -1
  52. package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.br +0 -0
  53. package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.gz +0 -0
  54. package/web/build/client/_app/immutable/chunks/CAnYe5db.js.br +0 -1
  55. package/web/build/client/_app/immutable/chunks/CAnYe5db.js.gz +0 -0
  56. package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.br +0 -0
  57. package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.gz +0 -0
  58. package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.br +0 -0
  59. package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.gz +0 -0
  60. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js +0 -1
  61. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.br +0 -0
  62. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.gz +0 -0
  63. package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.br +0 -0
  64. package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.gz +0 -0
  65. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js +0 -66
  66. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.br +0 -0
  67. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.gz +0 -0
  68. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js +0 -170
  69. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.br +0 -0
  70. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.gz +0 -0
  71. 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` (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,9 +14,11 @@ 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';
21
24
 
@@ -48,16 +51,17 @@ function printHelp() {
48
51
  Usage:
49
52
  diff-review [options]
50
53
 
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.
54
+ Por defecto (desde un repo git): elegís branch/base en la terminal (Clack),
55
+ copia el prompt, espera el JSON y abre el viewer.
56
+ Si pasás --branch y --base juntos, saltea los selects.
53
57
  Con --ui: abre la UI de inmediato (web-first).
54
58
 
55
59
  Options:
56
60
  -p, --port <n> Puerto (default ${DEFAULT_PORT})
57
61
  --host <h> Host (default ${DEFAULT_HOST})
58
62
  --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)
63
+ --base <b> Base del merge-base (salta el select de base)
64
+ --branch <b> Branch a revisar (salta el select de branch)
61
65
  --no-open No abrir el navegador
62
66
  -h, --help Esta ayuda
63
67
 
@@ -130,37 +134,167 @@ function canListen(host, port) {
130
134
  }
131
135
 
132
136
  /**
133
- * @returns {{ repo: string, branch: string, base: string, remoteUrl?: string } | null}
137
+ * @returns {{
138
+ * repo: string,
139
+ * listed: { branches: string[], current: string, defaultBase: string },
140
+ * remoteUrl?: string
141
+ * } | null}
134
142
  */
135
- function resolveLaunchRepo(opts) {
143
+ function loadRepoLaunch() {
136
144
  const cwd = process.cwd();
137
145
  const root = tryResolveGitRoot(cwd);
138
146
  if (!root) return null;
139
147
  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
148
  const remoteUrl = getRemoteUrl(root) || undefined;
147
- return { repo: root, branch, base, remoteUrl };
149
+ return { repo: root, listed, remoteUrl };
148
150
  }
149
151
 
150
- function logRepoContext(ctx) {
151
- console.log(`Repo: ${ctx.repo}`);
152
- console.log(`Branch: ${ctx.branch} (base: ${ctx.base})`);
152
+ function defaultBranch(listed) {
153
+ return listed.current && listed.current !== 'HEAD' ? listed.current : listed.branches[0] || '';
153
154
  }
154
155
 
155
- function copyPromptOrWarn(prompt) {
156
- if (copyTextToClipboard(prompt)) {
157
- console.log('Prompt copiado al portapapeles.');
158
- return true;
156
+ function defaultBase(listed, branch) {
157
+ const preferred = listed.branches.includes('develop') ? 'develop' : listed.defaultBase;
158
+ if (preferred && preferred !== branch) return preferred;
159
+ return listed.branches.find((b) => b !== branch) || preferred || '';
160
+ }
161
+
162
+ function branchOptions(listed) {
163
+ return listed.branches.map((b) => ({
164
+ value: b,
165
+ label: b,
166
+ hint: b === listed.current ? 'actual' : b === listed.defaultBase ? 'base típica' : undefined
167
+ }));
168
+ }
169
+
170
+ function ensureValue(value, label) {
171
+ if (p.isCancel(value)) {
172
+ p.cancel('Cancelado.');
173
+ process.exit(0);
174
+ }
175
+ if (value == null || value === '') {
176
+ p.cancel(`Falta ${label}.`);
177
+ process.exit(1);
178
+ }
179
+ return /** @type {string} */ (value);
180
+ }
181
+
182
+ /**
183
+ * Selects interactivos salvo que --branch y --base vengan juntos.
184
+ * @param {{ listed: { branches: string[], current: string, defaultBase: string }, repo: string }} launch
185
+ * @param {{ branch: string, base: string }} opts
186
+ */
187
+ async function resolveBranchBase(launch, opts) {
188
+ const flagBranch = String(opts.branch || '').trim();
189
+ const flagBase = String(opts.base || '').trim();
190
+ const bothFlags = Boolean(flagBranch && flagBase);
191
+
192
+ if (bothFlags) {
193
+ return { branch: flagBranch, base: flagBase, prompted: false };
194
+ }
195
+
196
+ if (!p.isTTY) {
197
+ const branch = flagBranch || defaultBranch(launch.listed);
198
+ const base = flagBase || defaultBase(launch.listed, branch);
199
+ return { branch, base, prompted: false };
200
+ }
201
+
202
+ if (!launch.listed.branches.length) {
203
+ p.cancel('No hay branches en este repo.');
204
+ process.exit(1);
159
205
  }
160
- console.log('No pude copiar al portapapeles; pegá el prompt a mano desde la UI o reintentá.');
206
+
207
+ const options = branchOptions(launch.listed);
208
+ p.intro('diff-review');
209
+ p.log.info(launch.repo);
210
+
211
+ let branch = flagBranch;
212
+ if (!branch) {
213
+ branch = ensureValue(
214
+ await autocompleteEs({
215
+ message: 'Branch a revisar',
216
+ options,
217
+ initialValue: defaultBranch(launch.listed) || undefined,
218
+ maxItems: 12
219
+ }),
220
+ 'branch'
221
+ );
222
+ } else {
223
+ p.log.step(`Branch: ${branch}`);
224
+ }
225
+
226
+ let base = flagBase;
227
+ if (!base) {
228
+ base = ensureValue(
229
+ await autocompleteEs({
230
+ message: 'Base (merge-base)',
231
+ options,
232
+ initialValue: defaultBase(launch.listed, branch) || undefined,
233
+ maxItems: 12
234
+ }),
235
+ 'base'
236
+ );
237
+ } else {
238
+ p.log.step(`Base: ${base}`);
239
+ }
240
+
241
+ while (branch === base) {
242
+ p.log.warn(`Branch y base son iguales (${branch}): el diff estaría vacío.`);
243
+ base = ensureValue(
244
+ await autocompleteEs({
245
+ message: 'Elegí otra base',
246
+ options,
247
+ initialValue: defaultBase(launch.listed, branch) || undefined,
248
+ maxItems: 12
249
+ }),
250
+ 'base'
251
+ );
252
+ }
253
+
254
+ return { branch, base, prompted: true };
255
+ }
256
+
257
+ function buildOutputFilename(repo, branch, base) {
258
+ try {
259
+ const tips = branchTips(repo, branch, base);
260
+ return makeOutputFilename({
261
+ branchSha: tips.branchSha,
262
+ baseSha: tips.baseSha
263
+ });
264
+ } catch (err) {
265
+ console.warn(
266
+ 'No pude resolver tips para el nombre del JSON:',
267
+ err instanceof Error ? err.message : err
268
+ );
269
+ return makeOutputFilename({});
270
+ }
271
+ }
272
+
273
+ function copyPromptOrWarn(prompt) {
274
+ if (copyTextToClipboard(prompt)) return true;
275
+ p.log.warn(
276
+ 'No pude copiar al portapapeles. Copiá el prompt a mano desde la terminal o reintentá.'
277
+ );
161
278
  return false;
162
279
  }
163
280
 
281
+ function explainWaitingForAgent(outputFilename, { copied, useClack }) {
282
+ const copyMsg = copied
283
+ ? 'Se copió un prompt al portapapeles: pegalo en el chat de tu agente de IA para generar el contenido de la review.'
284
+ : 'Generá el JSON con tu agente de IA usando el prompt de esta sesión (no se pudo copiar solo al portapapeles).';
285
+ const waitMsg = `Aguardando el resultado del agente (${outputFilename} en la raíz del repo). La UI web se abre cuando ese archivo esté listo.`;
286
+
287
+ if (useClack) {
288
+ p.log.success(copyMsg);
289
+ p.log.info(waitMsg);
290
+ p.outro('Ctrl+C cancela la espera.');
291
+ return;
292
+ }
293
+ console.log(copyMsg);
294
+ console.log(waitMsg);
295
+ console.log('(Ctrl+C cancela la espera)');
296
+ }
297
+
164
298
  /**
165
299
  * @param {{ host: string, port: number, open: boolean, launchCwd: string, importFile?: string, openPath?: string }} cfg
166
300
  */
@@ -228,6 +362,46 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
228
362
  process.on('SIGTERM', stop);
229
363
  }
230
364
 
365
+ async function runUiMode(opts, launch, launchCwd) {
366
+ const listed = launch?.listed;
367
+ const branch = String(opts.branch || '').trim() || (listed ? defaultBranch(listed) : '');
368
+ const base =
369
+ String(opts.base || '').trim() || (listed ? defaultBase(listed, branch) : 'develop');
370
+ const outputFilename = launch
371
+ ? buildOutputFilename(launch.repo, branch, base)
372
+ : makeOutputFilename({});
373
+
374
+ if (launch) {
375
+ console.log(`Repo: ${launch.repo}`);
376
+ console.log(`Branch: ${branch} (base: ${base})`);
377
+ const prompt = buildPrompt(
378
+ {
379
+ source: 'local',
380
+ repo: launch.repo,
381
+ branch,
382
+ base,
383
+ remoteUrl: launch.remoteUrl
384
+ },
385
+ { outputFilename }
386
+ );
387
+ if (copyTextToClipboard(prompt)) {
388
+ console.log('Prompt copiado al portapapeles.');
389
+ } else {
390
+ console.log('No pude copiar al portapapeles; pegá el prompt a mano desde la UI o reintentá.');
391
+ }
392
+ } else {
393
+ console.log('Sin repo git en el CWD — la UI arranca vacía.');
394
+ }
395
+
396
+ await startServer({
397
+ host: opts.host,
398
+ port: opts.port,
399
+ open: opts.open,
400
+ launchCwd,
401
+ openPath: launch ? `/?notify=prompt&out=${encodeURIComponent(outputFilename)}` : '/'
402
+ });
403
+ }
404
+
231
405
  async function main() {
232
406
  const opts = parseArgs(process.argv.slice(2));
233
407
  if (opts.help) {
@@ -236,105 +410,73 @@ async function main() {
236
410
  }
237
411
 
238
412
  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
- }
413
+ const launch = loadRepoLaunch();
414
+
415
+ if (opts.ui) {
416
+ await runUiMode(opts, launch, launchCwd);
417
+ return;
257
418
  }
258
419
 
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.`
420
+ if (!launch) {
421
+ console.error(
422
+ 'No hay un repositorio git en el directorio actual.\nUsá --ui para abrir la interfaz, o corré diff-review desde un repo.'
265
423
  );
266
- openUi = true;
267
- skipPromptCopy = true;
424
+ process.exit(1);
268
425
  }
269
426
 
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
- }
427
+ const { branch, base, prompted } = await resolveBranchBase(launch, opts);
428
+
429
+ if (branch === base) {
430
+ // Ambos flags iguales / sin TTY: no se puede corregir en selects.
431
+ console.log(
432
+ `Branch y base son iguales (${branch}): el diff estaría vacío.\nAbro la UI para que elijas otra combinación.`
433
+ );
289
434
  await startServer({
290
435
  host: opts.host,
291
436
  port: opts.port,
292
437
  open: opts.open,
293
438
  launchCwd,
294
- openPath:
295
- ctx && !skipPromptCopy
296
- ? `/?notify=prompt&out=${encodeURIComponent(outputFilename)}`
297
- : '/'
439
+ openPath: '/'
298
440
  });
299
441
  return;
300
442
  }
301
443
 
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);
444
+ const outputFilename = buildOutputFilename(launch.repo, branch, base);
445
+
446
+ if (!prompted) {
447
+ console.log(`Repo: ${launch.repo}`);
448
+ console.log(`Branch: ${branch} (base: ${base})`);
449
+ } else {
450
+ p.note(`${branch} ← ${base}\n${outputFilename}`, 'Review');
308
451
  }
309
452
 
310
- logRepoContext(ctx);
311
- console.log(`Salida: ${outputFilename}`);
312
453
  const prompt = buildPrompt(
313
454
  {
314
455
  source: 'local',
315
- repo: ctx.repo,
316
- branch: ctx.branch,
317
- base: ctx.base,
318
- remoteUrl: ctx.remoteUrl
456
+ repo: launch.repo,
457
+ branch,
458
+ base,
459
+ remoteUrl: launch.remoteUrl
319
460
  },
320
461
  { outputFilename }
321
462
  );
322
- copyPromptOrWarn(prompt);
323
- console.log(`Esperando ${outputFilename} en la raíz del repo… (Ctrl+C para cancelar)`);
324
463
 
325
- const result = await waitForReviewOutput(ctx.repo, outputFilename, {
464
+ const copied = copyPromptOrWarn(prompt);
465
+ explainWaitingForAgent(outputFilename, { copied, useClack: prompted });
466
+
467
+ const result = await waitForReviewOutput(launch.repo, outputFilename, {
326
468
  onInvalid: (errors) => {
327
469
  console.log(`JSON encontrado pero inválido (${errors.length} error(es)). Esperando corrección…`);
328
470
  for (const e of errors.slice(0, 5)) console.log(` · ${e}`);
329
471
  }
330
472
  });
331
473
 
332
- console.log('JSON OK. Abriendo viewer…');
474
+ console.log('JSON OK. Abriendo la UI web…');
333
475
  await startServer({
334
476
  host: opts.host,
335
477
  port: opts.port,
336
478
  open: opts.open,
337
- launchCwd: ctx.repo,
479
+ launchCwd: launch.repo,
338
480
  importFile: result.path,
339
481
  openPath: '/?import=1'
340
482
  });
@@ -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
+ }
@@ -1,54 +1,42 @@
1
1
  /**
2
2
  * Nombre del JSON que escribe el agente.
3
- * Preferido: tips cortos + stamp. Legacy sin tips sigue siendo válido al importar.
3
+ * Preferido: tips cortos branch/base. Legacy (con stamp o sin tips) sigue válido al importar.
4
4
  */
5
5
  import { fingerprint } from "./git.mjs";
6
6
 
7
- /** Prefijo del formato actual (con tips). */
8
- export const OUTPUT_PREFIX = "diff-report";
7
+ /** Prefijo del formato actual. */
8
+ export const OUTPUT_PREFIX = "diff-rev";
9
9
 
10
- /** Prefijo legacy (sin tips). */
10
+ /** Prefijo legacy (sin tips / con stamp). */
11
11
  export const OUTPUT_PREFIX_LEGACY = "diff-review-output";
12
+ export const OUTPUT_PREFIX_REPORT = "diff-report";
12
13
 
13
14
  /** Nombre legacy fijo. */
14
15
  export const OUTPUT_FILENAME_LEGACY = `${OUTPUT_PREFIX_LEGACY}.json`;
15
16
 
16
17
  /**
17
18
  * Válidos:
18
- * - diff-review-output.json
19
- * - diff-review-output-YYYYMMDD-HHmmss.json
20
- * - diff-report_{branchTip}-{baseTip}_YYYYMMDD-HHmmss.json
19
+ * - diff-rev_{branchTip}_{baseTip}.json
20
+ * - diff-report_{branchTip}-{baseTip}_YYYYMMDD-HHmmss.json (legacy)
21
+ * - diff-review-output.json / diff-review-output-YYYYMMDD-HHmmss.json (legacy)
21
22
  */
22
23
  export const OUTPUT_FILENAME_RE =
23
- /^(?:diff-review-output(?:-\d{8}-\d{6})?|diff-report_[0-9a-f]{7,12}-[0-9a-f]{7,12}_\d{8}-\d{6})\.json$/i;
24
+ /^(?:diff-rev_[0-9a-f]{7,12}_[0-9a-f]{7,12}|diff-report_[0-9a-f]{7,12}-[0-9a-f]{7,12}_\d{8}-\d{6}|diff-review-output(?:-\d{8}-\d{6})?)\.json$/i;
24
25
 
25
26
  /**
26
- * @param {Date} [when]
27
- * @returns {string} ej. 20260915-143045
28
- */
29
- export function formatOutputStamp(when = new Date()) {
30
- const pad = (n) => String(n).padStart(2, "0");
31
- const y = when.getFullYear();
32
- const mo = pad(when.getMonth() + 1);
33
- const d = pad(when.getDate());
34
- const h = pad(when.getHours());
35
- const mi = pad(when.getMinutes());
36
- const s = pad(when.getSeconds());
37
- return `${y}${mo}${d}-${h}${mi}${s}`;
38
- }
39
-
40
- /**
41
- * @param {{ branchSha?: string, baseSha?: string, stamp?: string, when?: Date } | Date} [input]
27
+ * @param {{ branchSha?: string, baseSha?: string }} [input]
42
28
  * @returns {string}
43
- * con tips: diff-report_aaaaaaaaaa-bbbbbbbbbb_20260915-143045.json
44
- * sin tips: diff-review-output-20260915-143045.json
29
+ * con tips: diff-rev_aaaaaaaaaa_bbbbbbbbbb.json
30
+ * sin tips: diff-review-output.json
45
31
  */
46
32
  export function makeOutputFilename(input = {}) {
47
- const opts = input instanceof Date ? { when: input } : input || {};
48
- const stamp = opts.stamp || formatOutputStamp(opts.when ?? new Date());
33
+ const opts = input instanceof Date ? {} : input || {};
49
34
  const fp = fingerprint(opts.branchSha, opts.baseSha);
50
- if (fp) return `${OUTPUT_PREFIX}_${fp}_${stamp}.json`;
51
- return `${OUTPUT_PREFIX_LEGACY}-${stamp}.json`;
35
+ if (fp) {
36
+ const [branchTip, baseTip] = fp.split("-");
37
+ if (branchTip && baseTip) return `${OUTPUT_PREFIX}_${branchTip}_${baseTip}.json`;
38
+ }
39
+ return OUTPUT_FILENAME_LEGACY;
52
40
  }
53
41
 
54
42
  export function isOutputFilename(name) {