@jpkme/diff-review 0.1.6 → 0.1.10
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 +32 -1
- package/bin/diff-review.mjs +227 -18
- package/lib/agents.mjs +319 -0
- package/lib/agents.test.mjs +195 -0
- package/lib/check-report.mjs +2 -1
- package/lib/check-report.test.mjs +1 -0
- package/lib/fingerprint.mjs +24 -0
- package/lib/git.mjs +2 -19
- package/lib/output-name.mjs +1 -1
- package/lib/prompt.mjs +19 -18
- package/lib/review-meta.mjs +33 -0
- package/lib/review-meta.test.mjs +30 -0
- package/lib/review.test.mjs +1 -0
- package/lib/validate.mjs +28 -11
- package/lib/validate.test.mjs +25 -0
- package/lib/wait-output.mjs +43 -17
- package/lib/wait-output.test.mjs +68 -0
- package/package.json +1 -1
- package/schema.json +9 -10
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css +1 -0
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/{3.qhPDz1xQ.css → 3.PHTPLlZ7.css} +1 -1
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css.br +0 -0
- package/web/build/client/_app/immutable/assets/{3.qhPDz1xQ.css.gz → 3.PHTPLlZ7.css.gz} +0 -0
- package/web/build/client/_app/immutable/chunks/BBWAo_yW.js +2 -0
- package/web/build/client/_app/immutable/chunks/BBWAo_yW.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/BBWAo_yW.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{DRga3k74.js → QdZF3nkf.js} +35 -33
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/{app.BP_R_kTF.js → app.Dce6uJU3.js} +2 -2
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js +1 -0
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js +66 -0
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/{3.Bn5l-P1e.js → 3.CeUs4lBX.js} +2 -2
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.gz +0 -0
- package/web/build/client/_app/version.json +1 -1
- package/web/build/client/_app/version.json.br +0 -0
- package/web/build/client/_app/version.json.gz +0 -0
- package/web/build/handler.js +3 -3
- package/web/build/index.js +3 -3
- package/web/build/server/chunks/1-2OO2bb7H.js +7 -0
- package/web/build/server/chunks/1-2OO2bb7H.js.map +1 -0
- package/web/build/server/chunks/2-BJvuqKN6.js +7 -0
- package/web/build/server/chunks/2-BJvuqKN6.js.map +1 -0
- package/web/build/server/chunks/3-ZIl1rHef.js +7 -0
- package/web/build/server/chunks/{3-DaeIJXI7.js.map → 3-ZIl1rHef.js.map} +1 -1
- package/web/build/server/chunks/{handler-2eew4UOl.js → handler-B5P65tLk.js} +3 -3
- package/web/build/server/chunks/{handler-2eew4UOl.js.map → handler-B5P65tLk.js.map} +1 -1
- package/web/build/server/chunks/{index.js-DbiNsix3.js → index.js-CL95e0Gl.js} +2 -2
- package/web/build/server/chunks/index.js-CL95e0Gl.js.map +1 -0
- package/web/build/server/chunks/{manifest.js-BYNXhn6E.js → manifest.js-uD_a0079.js} +6 -6
- package/web/build/server/chunks/{manifest.js-BYNXhn6E.js.map → manifest.js-uD_a0079.js.map} +1 -1
- package/web/build/server/chunks/{remote-1g4ayag-DESnyPRn.js → remote-1g4ayag-B67QMR_2.js} +154 -149
- package/web/build/server/chunks/remote-1g4ayag-B67QMR_2.js.map +1 -0
- package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css +0 -1
- package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.qhPDz1xQ.css.br +0 -0
- package/web/build/client/_app/immutable/chunks/Ber_0nz7.js +0 -1
- package/web/build/client/_app/immutable/chunks/Ber_0nz7.js.br +0 -2
- package/web/build/client/_app/immutable/chunks/Ber_0nz7.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/DRga3k74.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DRga3k74.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/zvcaSoGP.js +0 -2
- package/web/build/client/_app/immutable/chunks/zvcaSoGP.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/zvcaSoGP.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.BP_R_kTF.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.BP_R_kTF.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js +0 -1
- package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js.br +0 -1
- package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.CGJk77oo.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.CGJk77oo.js.br +0 -3
- package/web/build/client/_app/immutable/nodes/1.CGJk77oo.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js +0 -66
- package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js.gz +0 -0
- package/web/build/server/chunks/1-CRg9pswO.js +0 -7
- package/web/build/server/chunks/1-CRg9pswO.js.map +0 -1
- package/web/build/server/chunks/2-BmYzresl.js +0 -7
- package/web/build/server/chunks/2-BmYzresl.js.map +0 -1
- package/web/build/server/chunks/3-DaeIJXI7.js +0 -7
- package/web/build/server/chunks/index.js-DbiNsix3.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-DESnyPRn.js.map +0 -1
package/README.md
CHANGED
|
@@ -32,13 +32,44 @@ Desde un **repo git**, el default es terminal-first:
|
|
|
32
32
|
|
|
33
33
|
Si pasás `--branch` y `--base` juntos, saltea los selects. Si quedan iguales (diff vacío), abre la UI.
|
|
34
34
|
|
|
35
|
+
Si ya hay un JSON válido para esos mismos commits, pregunta si abrir el existente o generarlo de nuevo; en ese
|
|
36
|
+
caso el archivo viejo no cuenta hasta que el agente lo reescriba. Sin terminal interactiva no pregunta: con
|
|
37
|
+
`--agent` lo genera de nuevo y sin `--agent` abre el existente.
|
|
38
|
+
|
|
39
|
+
### Lanzar el agente (`--agent`)
|
|
40
|
+
|
|
41
|
+
Si hay agentes instalados, después de branch y base un solo select lista copiar el prompt al portapapeles (primera
|
|
42
|
+
opción) y cada agente detectado. Con `--agent` saltea esa lista y va directo a elegir agente y modelo:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
diff-review --agent
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Al enviarlo a un agente, en vez de copiar el prompt pregunta el modelo (con `--agent`, primero el agente):
|
|
49
|
+
|
|
50
|
+
1. **Agente**: los instalados que encuentra (Claude Code, Codex, Cursor CLI, GitHub Copilot CLI, Gemini CLI). Si ya lo elegiste en la lista anterior, este paso no se repite. Se buscan por nombre en el `PATH` y en algunas rutas fijas: en Windows, el Claude Code que trae la app de
|
|
51
|
+
escritorio (`%APPDATA%\Claude\claude-code\<versión>\…\claude.exe`) y `%LOCALAPPDATA%\cursor-agent\cursor-agent.cmd`;
|
|
52
|
+
en Linux/macOS, `~/.local/bin/claude`, `~/.claude/local/claude` y `~/.local/bin/cursor-agent`.
|
|
53
|
+
2. **Modelo**: arranca en el default del agente (el configurado en `~/.claude/settings.json` /
|
|
54
|
+
`ANTHROPIC_MODEL`, `~/.codex/config.toml`, `~/.cursor/cli-config.json`, `~/.copilot/settings.json` o
|
|
55
|
+
`~/.gemini/settings.json`; si no hay, no se pasa `--model`). También lista los conocidos (para Claude, los
|
|
56
|
+
alias `opus` / `sonnet` / `haiku`; para Copilot, `auto`) y "Otro…" para escribir un id.
|
|
57
|
+
|
|
58
|
+
El prompt viaja por stdin y la salida del agente se ve en la terminal. Claude Code y Copilot corren con permisos
|
|
59
|
+
acotados: leer, `git`, el `validate` y escribir solo el JSON del review (Copilot no puede acotar por argumentos,
|
|
60
|
+
así que permite `npx` en general). Cursor corre con `--force` (no tiene
|
|
61
|
+
flag para pasar una allowlist y sin él bloquearía el `validate`) y Gemini con `--yolo`. Si no hay agentes
|
|
62
|
+
instalados, sigue con el portapapeles. Sin terminal interactiva usa el primer agente con su modelo por defecto.
|
|
63
|
+
|
|
64
|
+
El agente tiene que estar logueado (p. ej. `claude /login` una vez).
|
|
65
|
+
|
|
35
66
|
Para abrir la UI primero (sin esperar el JSON):
|
|
36
67
|
|
|
37
68
|
```bash
|
|
38
69
|
diff-review --ui
|
|
39
70
|
```
|
|
40
71
|
|
|
41
|
-
Opciones: `--port
|
|
72
|
+
Opciones: `--port` (si está ocupado, usa el siguiente libre), `--host`, `--base`, `--branch`, `--agent`, `--no-open`.
|
|
42
73
|
|
|
43
74
|
Si lo lanzás desde un repositorio git, la UI precarga esa ruta y el branch actual.
|
|
44
75
|
|
package/bin/diff-review.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* diff-review --port 5191
|
|
8
8
|
* diff-review --branch x --base y → saltea selects de Clack
|
|
9
|
+
* diff-review --agent → elegir agente + modelo y lanzarlo con el prompt (sin portapapeles)
|
|
9
10
|
* diff-review --no-open
|
|
10
11
|
*/
|
|
11
12
|
|
|
@@ -15,12 +16,14 @@ import { createServer } from 'node:net';
|
|
|
15
16
|
import { dirname, join } from 'node:path';
|
|
16
17
|
import { fileURLToPath } from 'node:url';
|
|
17
18
|
import * as p from '@clack/prompts';
|
|
19
|
+
import { AGENTS, detectAgents, modelChoices, OTHER_MODEL, resolvedRunModel, runAgent } from '../lib/agents.mjs';
|
|
18
20
|
import { autocompleteEs } from '../lib/autocomplete-es.mjs';
|
|
19
21
|
import { copyTextToClipboard } from '../lib/clipboard.mjs';
|
|
20
22
|
import { branchTips, getRemoteUrl, listBranches, tryResolveGitRoot } from '../lib/git.mjs';
|
|
21
23
|
import { makeOutputFilename } from '../lib/output-name.mjs';
|
|
22
24
|
import { buildPrompt } from '../lib/prompt.mjs';
|
|
23
|
-
import { waitForReviewOutput } from '../lib/wait-output.mjs';
|
|
25
|
+
import { existingReviewOutput, waitForReviewOutput } from '../lib/wait-output.mjs';
|
|
26
|
+
import { stampReviewProvenance } from '../lib/review-meta.mjs';
|
|
24
27
|
import { runValidateCli } from '../lib/check-report.mjs';
|
|
25
28
|
|
|
26
29
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -54,17 +57,21 @@ Usage:
|
|
|
54
57
|
diff-review validate <file.json>
|
|
55
58
|
|
|
56
59
|
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.
|
|
60
|
+
copia el prompt, espera el JSON y abre el viewer. Si hay agentes instalados,
|
|
61
|
+
un solo select lista el portapapeles y cada agente detectado.
|
|
58
62
|
\`validate\` revisa el JSON y, si falla, dice cómo seguir (exit 0 = OK).
|
|
59
63
|
Si pasás --branch y --base juntos, saltea los selects.
|
|
64
|
+
Con --agent: saltea esa lista; elegís un agente instalado y el modelo,
|
|
65
|
+
y diff-review lo lanza con el prompt.
|
|
60
66
|
Con --ui: abre la UI de inmediato (web-first).
|
|
61
67
|
|
|
62
68
|
Options:
|
|
63
|
-
-p, --port <n> Puerto (default ${DEFAULT_PORT})
|
|
69
|
+
-p, --port <n> Puerto (default ${DEFAULT_PORT}; si está ocupado, el siguiente libre)
|
|
64
70
|
--host <h> Host (default ${DEFAULT_HOST})
|
|
65
71
|
--ui Abrir la UI primero (sin esperar JSON)
|
|
66
72
|
--base <b> Base del merge-base (salta el select de base)
|
|
67
73
|
--branch <b> Branch a revisar (salta el select de branch)
|
|
74
|
+
--agent Lanzar un agente instalado (${AGENTS.map((a) => a.bins[0]).join(', ')}) con el prompt
|
|
68
75
|
--no-open No abrir el navegador
|
|
69
76
|
-h, --help Esta ayuda
|
|
70
77
|
|
|
@@ -79,6 +86,7 @@ function parseArgs(argv) {
|
|
|
79
86
|
open: true,
|
|
80
87
|
help: false,
|
|
81
88
|
ui: false,
|
|
89
|
+
agent: false,
|
|
82
90
|
base: '',
|
|
83
91
|
branch: ''
|
|
84
92
|
};
|
|
@@ -87,6 +95,7 @@ function parseArgs(argv) {
|
|
|
87
95
|
if (a === '-h' || a === '--help') opts.help = true;
|
|
88
96
|
else if (a === '--no-open') opts.open = false;
|
|
89
97
|
else if (a === '--ui') opts.ui = true;
|
|
98
|
+
else if (a === '--agent') opts.agent = true;
|
|
90
99
|
else if (a === '--host') opts.host = argv[++i] || opts.host;
|
|
91
100
|
else if (a === '--base') opts.base = argv[++i] || opts.base;
|
|
92
101
|
else if (a === '--branch') opts.branch = argv[++i] || opts.branch;
|
|
@@ -136,6 +145,18 @@ function canListen(host, port) {
|
|
|
136
145
|
});
|
|
137
146
|
}
|
|
138
147
|
|
|
148
|
+
const PORT_SCAN = 50;
|
|
149
|
+
|
|
150
|
+
/** Primer puerto libre desde `start`, o `null` si ninguno de los siguientes cabe. */
|
|
151
|
+
async function findFreePort(host, start) {
|
|
152
|
+
const first = Math.max(1, Math.floor(start));
|
|
153
|
+
const last = Math.min(65535, first + PORT_SCAN - 1);
|
|
154
|
+
for (let port = first; port <= last; port++) {
|
|
155
|
+
if (await canListen(host, port)) return port;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
/**
|
|
140
161
|
* @returns {{
|
|
141
162
|
* repo: string,
|
|
@@ -170,6 +191,11 @@ function branchOptions(listed) {
|
|
|
170
191
|
}));
|
|
171
192
|
}
|
|
172
193
|
|
|
194
|
+
/** Selects solo con terminal interactiva. (`p.isTTY` de Clack es una función, no un booleano.) */
|
|
195
|
+
function isInteractive() {
|
|
196
|
+
return p.isTTY(process.stdin) && p.isTTY(process.stdout);
|
|
197
|
+
}
|
|
198
|
+
|
|
173
199
|
function ensureValue(value, label) {
|
|
174
200
|
if (p.isCancel(value)) {
|
|
175
201
|
p.cancel('Cancelado.');
|
|
@@ -196,7 +222,7 @@ async function resolveBranchBase(launch, opts) {
|
|
|
196
222
|
return { branch: flagBranch, base: flagBase, prompted: false };
|
|
197
223
|
}
|
|
198
224
|
|
|
199
|
-
if (!
|
|
225
|
+
if (!isInteractive()) {
|
|
200
226
|
const branch = flagBranch || defaultBranch(launch.listed);
|
|
201
227
|
const base = flagBase || defaultBase(launch.listed, branch);
|
|
202
228
|
return { branch, base, prompted: false };
|
|
@@ -281,6 +307,146 @@ function copyPromptOrWarn(prompt) {
|
|
|
281
307
|
return false;
|
|
282
308
|
}
|
|
283
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Modelo del agente: select con TTY; sin TTY, el modelo por defecto (string vacío).
|
|
312
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
313
|
+
*/
|
|
314
|
+
async function pickModel(agent) {
|
|
315
|
+
if (!isInteractive()) return '';
|
|
316
|
+
|
|
317
|
+
const { options, initialValue } = modelChoices(agent.def);
|
|
318
|
+
const choice = await p.select({ message: 'Modelo', options, initialValue });
|
|
319
|
+
if (p.isCancel(choice)) {
|
|
320
|
+
p.cancel('Cancelado.');
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
if (choice !== OTHER_MODEL) return /** @type {string} */ (choice);
|
|
324
|
+
|
|
325
|
+
const typed = ensureValue(
|
|
326
|
+
await p.text({
|
|
327
|
+
message: 'Id del modelo',
|
|
328
|
+
validate: (v) => (String(v ?? '').trim() ? undefined : 'Escribí el id del modelo.')
|
|
329
|
+
}),
|
|
330
|
+
'modelo'
|
|
331
|
+
);
|
|
332
|
+
return typed.trim();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Agente y modelo: selects con TTY; sin TTY, el primer agente detectado con su modelo por defecto.
|
|
337
|
+
* Si `preset` ya viene elegido, solo pregunta el modelo.
|
|
338
|
+
* @param {import('../lib/agents.mjs').DetectedAgent[]} detected
|
|
339
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} [preset]
|
|
340
|
+
*/
|
|
341
|
+
async function pickAgentAndModel(detected, preset) {
|
|
342
|
+
if (!isInteractive()) return { agent: preset ?? detected[0], model: '' };
|
|
343
|
+
|
|
344
|
+
let agent = preset;
|
|
345
|
+
if (!agent) {
|
|
346
|
+
const id = ensureValue(
|
|
347
|
+
await p.select({
|
|
348
|
+
message: 'Agente',
|
|
349
|
+
options: detected.map((a) => ({ value: a.def.id, label: a.def.label, hint: a.path })),
|
|
350
|
+
initialValue: detected[0].def.id
|
|
351
|
+
}),
|
|
352
|
+
'agente'
|
|
353
|
+
);
|
|
354
|
+
agent = detected.find((a) => a.def.id === id) ?? detected[0];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { agent, model: await pickModel(agent) };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Portapapeles primero y después cada agente detectado.
|
|
362
|
+
* @param {import('../lib/agents.mjs').DetectedAgent[]} detected
|
|
363
|
+
* @returns {Promise<import('../lib/agents.mjs').DetectedAgent | null>} `null` = portapapeles
|
|
364
|
+
*/
|
|
365
|
+
async function pickDelivery(detected) {
|
|
366
|
+
const choice = ensureValue(
|
|
367
|
+
await p.select({
|
|
368
|
+
message: '¿Cómo generás el review?',
|
|
369
|
+
options: [
|
|
370
|
+
{ value: 'clipboard', label: 'Copiar el prompt al portapapeles', hint: 'lo pegás en tu agente' },
|
|
371
|
+
...detected.map((a) => ({ value: a.def.id, label: a.def.label, hint: a.path }))
|
|
372
|
+
],
|
|
373
|
+
initialValue: 'clipboard'
|
|
374
|
+
}),
|
|
375
|
+
'modo'
|
|
376
|
+
);
|
|
377
|
+
if (choice === 'clipboard') return null;
|
|
378
|
+
return detected.find((a) => a.def.id === choice) ?? detected[0];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Ya hay un review válido para estos commits: pregunta si abrirlo o generarlo de nuevo.
|
|
383
|
+
* @param {{ path: string }} existing
|
|
384
|
+
* @returns {Promise<{ path: string } | null>} el existente, o `null` para generar de nuevo
|
|
385
|
+
*/
|
|
386
|
+
async function reuseExistingReview(existing, outputFilename) {
|
|
387
|
+
const choice = ensureValue(
|
|
388
|
+
await p.select({
|
|
389
|
+
message: `Ya hay un review válido para estos commits (${outputFilename})`,
|
|
390
|
+
options: [
|
|
391
|
+
{ value: 'open', label: 'Abrir el existente' },
|
|
392
|
+
{ value: 'regenerate', label: 'Generar de nuevo', hint: 'el agente lo reemplaza' }
|
|
393
|
+
],
|
|
394
|
+
initialValue: 'open'
|
|
395
|
+
}),
|
|
396
|
+
'review'
|
|
397
|
+
);
|
|
398
|
+
if (choice !== 'open') return null;
|
|
399
|
+
p.outro('Abro el review existente.');
|
|
400
|
+
return existing;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Lanza el agente y espera el JSON. Devuelve el reporte válido; si el agente termina sin dejarlo, sale con error.
|
|
405
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
406
|
+
* @param {{ repo: string, prompt: string, model: string, outputFilename: string, useClack: boolean }} run
|
|
407
|
+
*/
|
|
408
|
+
async function runAgentAndWait(agent, { repo, prompt, model, outputFilename, useClack }) {
|
|
409
|
+
const what = `${agent.def.label} (${model || 'modelo por defecto'})`;
|
|
410
|
+
const msg = `Lanzando ${what}. Espera ${outputFilename} en la raíz del repo; Ctrl+C cancela.`;
|
|
411
|
+
if (useClack) p.outro(msg);
|
|
412
|
+
else console.log(msg);
|
|
413
|
+
|
|
414
|
+
// Solo cuenta lo que escriba este agente: un JSON anterior de los mismos commits no es el resultado.
|
|
415
|
+
const since = Date.now();
|
|
416
|
+
const { exited } = runAgent(agent, { cwd: repo, prompt, model, outputFilename });
|
|
417
|
+
const waiting = waitForReviewOutput(repo, outputFilename, {
|
|
418
|
+
since,
|
|
419
|
+
onInvalid: (errors) => {
|
|
420
|
+
console.log(`JSON encontrado pero inválido (${errors.length} error(es)). El agente puede corregirlo…`);
|
|
421
|
+
for (const e of errors.slice(0, 5)) console.log(` · ${e}`);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
let code;
|
|
426
|
+
try {
|
|
427
|
+
code = await exited;
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.error(`No se pudo lanzar ${agent.path}: ${err instanceof Error ? err.message : err}`);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
// El watcher puede ir un tick atrás de la última escritura del agente.
|
|
433
|
+
const result = await Promise.race([waiting, new Promise((r) => setTimeout(() => r(null), 2000))]);
|
|
434
|
+
if (!result) {
|
|
435
|
+
console.error(
|
|
436
|
+
code === 0
|
|
437
|
+
? `${agent.def.label} terminó sin dejar un ${outputFilename} válido.`
|
|
438
|
+
: `${agent.def.label} terminó con código ${code} sin dejar un ${outputFilename} válido.`
|
|
439
|
+
);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
if (code !== 0) console.warn(`${agent.def.label} terminó con código ${code}, pero el JSON es válido.`);
|
|
443
|
+
stampReviewProvenance(result.path, {
|
|
444
|
+
agent: agent.def.label,
|
|
445
|
+
model: resolvedRunModel(agent.def, model)
|
|
446
|
+
});
|
|
447
|
+
return /** @type {{ path: string }} */ (result);
|
|
448
|
+
}
|
|
449
|
+
|
|
284
450
|
function explainWaitingForAgent(outputFilename, { copied, useClack }) {
|
|
285
451
|
const copyMsg = copied
|
|
286
452
|
? 'Se copió un prompt al portapapeles: pegalo en el chat de tu agente de IA para generar el contenido de la review.'
|
|
@@ -309,12 +475,17 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
|
|
|
309
475
|
process.exit(1);
|
|
310
476
|
}
|
|
311
477
|
|
|
312
|
-
|
|
313
|
-
|
|
478
|
+
const port = await findFreePort(cfg.host, cfg.port);
|
|
479
|
+
if (port == null) {
|
|
480
|
+
const last = Math.min(65535, cfg.port + PORT_SCAN - 1);
|
|
481
|
+
console.error(`No encontré un puerto libre entre ${cfg.port} y ${last} en ${cfg.host}.`);
|
|
314
482
|
process.exit(1);
|
|
315
483
|
}
|
|
484
|
+
if (port !== cfg.port) {
|
|
485
|
+
console.log(`Puerto ${cfg.port} ocupado; la UI usa ${port}.`);
|
|
486
|
+
}
|
|
316
487
|
|
|
317
|
-
const origin = `http://${cfg.host}:${
|
|
488
|
+
const origin = `http://${cfg.host}:${port}`;
|
|
318
489
|
const openUrl = cfg.openPath ? `${origin}${cfg.openPath}` : origin;
|
|
319
490
|
const nodeBin = resolveNodeBin();
|
|
320
491
|
const child = spawn(nodeBin, [entry], {
|
|
@@ -322,7 +493,7 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
|
|
|
322
493
|
env: {
|
|
323
494
|
...process.env,
|
|
324
495
|
HOST: cfg.host,
|
|
325
|
-
PORT: String(
|
|
496
|
+
PORT: String(port),
|
|
326
497
|
ORIGIN: origin,
|
|
327
498
|
DIFF_REVIEW_LAUNCH_CWD: cfg.launchCwd,
|
|
328
499
|
...(cfg.importFile ? { DIFF_REVIEW_IMPORT_FILE: cfg.importFile } : {})
|
|
@@ -401,7 +572,9 @@ async function runUiMode(opts, launch, launchCwd) {
|
|
|
401
572
|
port: opts.port,
|
|
402
573
|
open: opts.open,
|
|
403
574
|
launchCwd,
|
|
404
|
-
openPath: launch
|
|
575
|
+
openPath: launch
|
|
576
|
+
? `/?watch=1¬ify=prompt&out=${encodeURIComponent(outputFilename)}`
|
|
577
|
+
: '/'
|
|
405
578
|
});
|
|
406
579
|
}
|
|
407
580
|
|
|
@@ -455,7 +628,7 @@ async function main() {
|
|
|
455
628
|
console.log(`Repo: ${launch.repo}`);
|
|
456
629
|
console.log(`Branch: ${branch} (base: ${base})`);
|
|
457
630
|
} else {
|
|
458
|
-
p.note(`${branch}
|
|
631
|
+
p.note(`${branch} → ${base}\n${outputFilename}`, 'Review');
|
|
459
632
|
}
|
|
460
633
|
|
|
461
634
|
const prompt = buildPrompt(
|
|
@@ -469,15 +642,51 @@ async function main() {
|
|
|
469
642
|
{ outputFilename }
|
|
470
643
|
);
|
|
471
644
|
|
|
472
|
-
|
|
473
|
-
|
|
645
|
+
// Sin --agent también se detectan (con terminal interactiva) para ofrecer lanzar uno en vez del portapapeles.
|
|
646
|
+
const detected = opts.agent || isInteractive() ? detectAgents() : [];
|
|
647
|
+
if (opts.agent && !detected.length) {
|
|
648
|
+
const names = AGENTS.map((a) => a.bins[0]).join(', ');
|
|
649
|
+
const warn = `No encontré agentes instalados (${names}). Sigo copiando el prompt.`;
|
|
650
|
+
if (prompted) p.log.warn(warn);
|
|
651
|
+
else console.warn(warn);
|
|
652
|
+
}
|
|
474
653
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
654
|
+
// Hay selects (agente/modelo, o abrir vs. regenerar un review existente) solo con terminal interactiva.
|
|
655
|
+
const existing = isInteractive() ? existingReviewOutput(launch.repo, outputFilename) : null;
|
|
656
|
+
const useClack = prompted || (isInteractive() && (detected.length > 0 || existing != null));
|
|
657
|
+
// Con flags de branch/base no hubo intro, pero estos selects abren sesión de Clack igual.
|
|
658
|
+
if (useClack && !prompted) p.intro('diff-review');
|
|
659
|
+
|
|
660
|
+
let result = existing ? await reuseExistingReview(existing, outputFilename) : null;
|
|
661
|
+
/** @type {import('../lib/agents.mjs').DetectedAgent | null} */
|
|
662
|
+
let chosen = null;
|
|
663
|
+
if (!result && detected.length > 0 && !opts.agent && isInteractive()) {
|
|
664
|
+
chosen = await pickDelivery(detected);
|
|
665
|
+
}
|
|
666
|
+
const useAgent = !result && detected.length > 0 && (opts.agent || chosen != null);
|
|
667
|
+
if (useAgent) {
|
|
668
|
+
const { agent, model } = await pickAgentAndModel(detected, chosen ?? undefined);
|
|
669
|
+
result = await runAgentAndWait(agent, {
|
|
670
|
+
repo: launch.repo,
|
|
671
|
+
prompt,
|
|
672
|
+
model,
|
|
673
|
+
outputFilename,
|
|
674
|
+
useClack
|
|
675
|
+
});
|
|
676
|
+
} else if (!result) {
|
|
677
|
+
// Si eligió generar de nuevo, el JSON existente no cuenta hasta que el agente lo reescriba.
|
|
678
|
+
const since = existing ? Date.now() : undefined;
|
|
679
|
+
const copied = copyPromptOrWarn(prompt);
|
|
680
|
+
explainWaitingForAgent(outputFilename, { copied, useClack });
|
|
681
|
+
|
|
682
|
+
result = await waitForReviewOutput(launch.repo, outputFilename, {
|
|
683
|
+
since,
|
|
684
|
+
onInvalid: (errors) => {
|
|
685
|
+
console.log(`JSON encontrado pero inválido (${errors.length} error(es)). Esperando corrección…`);
|
|
686
|
+
for (const e of errors.slice(0, 5)) console.log(` · ${e}`);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
481
690
|
|
|
482
691
|
console.log('JSON OK. Abriendo la UI web…');
|
|
483
692
|
await startServer({
|