@jpkme/diff-review 0.1.5 → 0.1.7
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 +33 -2
- package/bin/diff-review.mjs +230 -18
- package/lib/agents.mjs +304 -0
- package/lib/agents.test.mjs +195 -0
- package/lib/check-report.mjs +213 -0
- package/lib/check-report.test.mjs +46 -0
- package/lib/fingerprint.mjs +24 -0
- package/lib/git.mjs +2 -19
- package/lib/output-name.mjs +1 -1
- package/lib/prompt.mjs +22 -9
- package/lib/wait-output.mjs +43 -17
- package/lib/wait-output.test.mjs +67 -0
- package/package.json +1 -1
- package/web/build/client/_app/immutable/assets/2.COsJT9ly.css +1 -0
- package/web/build/client/_app/immutable/assets/2.COsJT9ly.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.COsJT9ly.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/C4--ziCr.js +2 -0
- package/web/build/client/_app/immutable/chunks/C4--ziCr.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/C4--ziCr.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{CdHZDAAo.js → D8sAxZVo.js} +38 -30
- package/web/build/client/_app/immutable/chunks/D8sAxZVo.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/D8sAxZVo.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/{app.BRzz8J51.js → app.Dl9OTE-n.js} +2 -2
- package/web/build/client/_app/immutable/entry/app.Dl9OTE-n.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.Dl9OTE-n.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CDz8Qs8Q.js +1 -0
- package/web/build/client/_app/immutable/entry/start.CDz8Qs8Q.js.br +1 -0
- package/web/build/client/_app/immutable/entry/start.CDz8Qs8Q.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.B1c4AH1e.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.B1c4AH1e.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.B1c4AH1e.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Dx-oOpEf.js +66 -0
- package/web/build/client/_app/immutable/nodes/2.Dx-oOpEf.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.Dx-oOpEf.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/{3.ByTmpJQZ.js → 3.C-USexyX.js} +2 -2
- package/web/build/client/_app/immutable/nodes/3.C-USexyX.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.C-USexyX.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-0X6Jp9nH.js +7 -0
- package/web/build/server/chunks/1-0X6Jp9nH.js.map +1 -0
- package/web/build/server/chunks/2-HlcVALg6.js +7 -0
- package/web/build/server/chunks/2-HlcVALg6.js.map +1 -0
- package/web/build/server/chunks/{3-DZ5PBhOc.js → 3-BN1eS7L2.js} +2 -2
- package/web/build/server/chunks/{3-DZ5PBhOc.js.map → 3-BN1eS7L2.js.map} +1 -1
- package/web/build/server/chunks/{handler-DD4vP3bU.js → handler-cq3QoEzW.js} +3 -3
- package/web/build/server/chunks/{handler-DD4vP3bU.js.map → handler-cq3QoEzW.js.map} +1 -1
- package/web/build/server/chunks/{index.js-IhFPHwmp.js → index.js-D4qkHe3r.js} +2 -2
- package/web/build/server/chunks/{index.js-IhFPHwmp.js.map → index.js-D4qkHe3r.js.map} +1 -1
- package/web/build/server/chunks/{manifest.js-Dk3RuAA5.js → manifest.js-DQHMKyJh.js} +6 -6
- package/web/build/server/chunks/{manifest.js-Dk3RuAA5.js.map → manifest.js-DQHMKyJh.js.map} +1 -1
- package/web/build/server/chunks/{remote-1g4ayag-BRL-uaKE.js → remote-1g4ayag-B8KZPTkn.js} +154 -149
- package/web/build/server/chunks/remote-1g4ayag-B8KZPTkn.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/chunks/CO0mUwMJ.js +0 -1
- package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.br +0 -2
- package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CdHZDAAo.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CdHZDAAo.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/DzqTpz7_.js +0 -2
- package/web/build/client/_app/immutable/chunks/DzqTpz7_.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DzqTpz7_.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.BRzz8J51.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.BRzz8J51.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.BSioRkqK.js +0 -1
- package/web/build/client/_app/immutable/entry/start.BSioRkqK.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.BSioRkqK.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.br +0 -1
- package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js +0 -66
- package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js.gz +0 -0
- package/web/build/server/chunks/1-qrn24yN5.js +0 -7
- package/web/build/server/chunks/1-qrn24yN5.js.map +0 -1
- package/web/build/server/chunks/2-C7_Loqyn.js +0 -7
- package/web/build/server/chunks/2-C7_Loqyn.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-BRL-uaKE.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
|
|
|
@@ -55,7 +86,7 @@ npm start # mismo que el bin global
|
|
|
55
86
|
|
|
56
87
|
1. Pegá la ruta (o URL) del repo y elegí branch / base — o lanzá `diff-review` / `diff-review --ui` desde el repo.
|
|
57
88
|
2. Copiá el prompt (o el comando de skill `/diff-review`).
|
|
58
|
-
3. Corrélo en tu agente; que escriba `diff-rev_{branchTip}_{baseTip}.json` (también acepta nombres legacy).
|
|
89
|
+
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
90
|
4. Arrastrá ese JSON a la dropzone (o dejá que el CLI lo importe solo). La UI hidrata los diffs con git.
|
|
60
91
|
|
|
61
92
|
"Ver ejemplo" carga un reporte de muestra sin tocar un repo.
|
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, 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 { runValidateCli } from '../lib/check-report.mjs';
|
|
24
27
|
|
|
25
28
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
29
|
const packageRoot = join(here, '..');
|
|
@@ -50,18 +53,24 @@ function printHelp() {
|
|
|
50
53
|
|
|
51
54
|
Usage:
|
|
52
55
|
diff-review [options]
|
|
56
|
+
diff-review validate <file.json>
|
|
53
57
|
|
|
54
58
|
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.
|
|
59
|
+
copia el prompt, espera el JSON y abre el viewer. Si hay agentes instalados,
|
|
60
|
+
un solo select lista el portapapeles y cada agente detectado.
|
|
61
|
+
\`validate\` revisa el JSON y, si falla, dice cómo seguir (exit 0 = OK).
|
|
56
62
|
Si pasás --branch y --base juntos, saltea los selects.
|
|
63
|
+
Con --agent: saltea esa lista; elegís un agente instalado y el modelo,
|
|
64
|
+
y diff-review lo lanza con el prompt.
|
|
57
65
|
Con --ui: abre la UI de inmediato (web-first).
|
|
58
66
|
|
|
59
67
|
Options:
|
|
60
|
-
-p, --port <n> Puerto (default ${DEFAULT_PORT})
|
|
68
|
+
-p, --port <n> Puerto (default ${DEFAULT_PORT}; si está ocupado, el siguiente libre)
|
|
61
69
|
--host <h> Host (default ${DEFAULT_HOST})
|
|
62
70
|
--ui Abrir la UI primero (sin esperar JSON)
|
|
63
71
|
--base <b> Base del merge-base (salta el select de base)
|
|
64
72
|
--branch <b> Branch a revisar (salta el select de branch)
|
|
73
|
+
--agent Lanzar un agente instalado (${AGENTS.map((a) => a.bins[0]).join(', ')}) con el prompt
|
|
65
74
|
--no-open No abrir el navegador
|
|
66
75
|
-h, --help Esta ayuda
|
|
67
76
|
|
|
@@ -76,6 +85,7 @@ function parseArgs(argv) {
|
|
|
76
85
|
open: true,
|
|
77
86
|
help: false,
|
|
78
87
|
ui: false,
|
|
88
|
+
agent: false,
|
|
79
89
|
base: '',
|
|
80
90
|
branch: ''
|
|
81
91
|
};
|
|
@@ -84,6 +94,7 @@ function parseArgs(argv) {
|
|
|
84
94
|
if (a === '-h' || a === '--help') opts.help = true;
|
|
85
95
|
else if (a === '--no-open') opts.open = false;
|
|
86
96
|
else if (a === '--ui') opts.ui = true;
|
|
97
|
+
else if (a === '--agent') opts.agent = true;
|
|
87
98
|
else if (a === '--host') opts.host = argv[++i] || opts.host;
|
|
88
99
|
else if (a === '--base') opts.base = argv[++i] || opts.base;
|
|
89
100
|
else if (a === '--branch') opts.branch = argv[++i] || opts.branch;
|
|
@@ -133,6 +144,18 @@ function canListen(host, port) {
|
|
|
133
144
|
});
|
|
134
145
|
}
|
|
135
146
|
|
|
147
|
+
const PORT_SCAN = 50;
|
|
148
|
+
|
|
149
|
+
/** Primer puerto libre desde `start`, o `null` si ninguno de los siguientes cabe. */
|
|
150
|
+
async function findFreePort(host, start) {
|
|
151
|
+
const first = Math.max(1, Math.floor(start));
|
|
152
|
+
const last = Math.min(65535, first + PORT_SCAN - 1);
|
|
153
|
+
for (let port = first; port <= last; port++) {
|
|
154
|
+
if (await canListen(host, port)) return port;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
/**
|
|
137
160
|
* @returns {{
|
|
138
161
|
* repo: string,
|
|
@@ -167,6 +190,11 @@ function branchOptions(listed) {
|
|
|
167
190
|
}));
|
|
168
191
|
}
|
|
169
192
|
|
|
193
|
+
/** Selects solo con terminal interactiva. (`p.isTTY` de Clack es una función, no un booleano.) */
|
|
194
|
+
function isInteractive() {
|
|
195
|
+
return p.isTTY(process.stdin) && p.isTTY(process.stdout);
|
|
196
|
+
}
|
|
197
|
+
|
|
170
198
|
function ensureValue(value, label) {
|
|
171
199
|
if (p.isCancel(value)) {
|
|
172
200
|
p.cancel('Cancelado.');
|
|
@@ -193,7 +221,7 @@ async function resolveBranchBase(launch, opts) {
|
|
|
193
221
|
return { branch: flagBranch, base: flagBase, prompted: false };
|
|
194
222
|
}
|
|
195
223
|
|
|
196
|
-
if (!
|
|
224
|
+
if (!isInteractive()) {
|
|
197
225
|
const branch = flagBranch || defaultBranch(launch.listed);
|
|
198
226
|
const base = flagBase || defaultBase(launch.listed, branch);
|
|
199
227
|
return { branch, base, prompted: false };
|
|
@@ -278,6 +306,142 @@ function copyPromptOrWarn(prompt) {
|
|
|
278
306
|
return false;
|
|
279
307
|
}
|
|
280
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Modelo del agente: select con TTY; sin TTY, el modelo por defecto (string vacío).
|
|
311
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
312
|
+
*/
|
|
313
|
+
async function pickModel(agent) {
|
|
314
|
+
if (!isInteractive()) return '';
|
|
315
|
+
|
|
316
|
+
const { options, initialValue } = modelChoices(agent.def);
|
|
317
|
+
const choice = await p.select({ message: 'Modelo', options, initialValue });
|
|
318
|
+
if (p.isCancel(choice)) {
|
|
319
|
+
p.cancel('Cancelado.');
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
if (choice !== OTHER_MODEL) return /** @type {string} */ (choice);
|
|
323
|
+
|
|
324
|
+
const typed = ensureValue(
|
|
325
|
+
await p.text({
|
|
326
|
+
message: 'Id del modelo',
|
|
327
|
+
validate: (v) => (String(v ?? '').trim() ? undefined : 'Escribí el id del modelo.')
|
|
328
|
+
}),
|
|
329
|
+
'modelo'
|
|
330
|
+
);
|
|
331
|
+
return typed.trim();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Agente y modelo: selects con TTY; sin TTY, el primer agente detectado con su modelo por defecto.
|
|
336
|
+
* Si `preset` ya viene elegido, solo pregunta el modelo.
|
|
337
|
+
* @param {import('../lib/agents.mjs').DetectedAgent[]} detected
|
|
338
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} [preset]
|
|
339
|
+
*/
|
|
340
|
+
async function pickAgentAndModel(detected, preset) {
|
|
341
|
+
if (!isInteractive()) return { agent: preset ?? detected[0], model: '' };
|
|
342
|
+
|
|
343
|
+
let agent = preset;
|
|
344
|
+
if (!agent) {
|
|
345
|
+
const id = ensureValue(
|
|
346
|
+
await p.select({
|
|
347
|
+
message: 'Agente',
|
|
348
|
+
options: detected.map((a) => ({ value: a.def.id, label: a.def.label, hint: a.path })),
|
|
349
|
+
initialValue: detected[0].def.id
|
|
350
|
+
}),
|
|
351
|
+
'agente'
|
|
352
|
+
);
|
|
353
|
+
agent = detected.find((a) => a.def.id === id) ?? detected[0];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { agent, model: await pickModel(agent) };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Portapapeles primero y después cada agente detectado.
|
|
361
|
+
* @param {import('../lib/agents.mjs').DetectedAgent[]} detected
|
|
362
|
+
* @returns {Promise<import('../lib/agents.mjs').DetectedAgent | null>} `null` = portapapeles
|
|
363
|
+
*/
|
|
364
|
+
async function pickDelivery(detected) {
|
|
365
|
+
const choice = ensureValue(
|
|
366
|
+
await p.select({
|
|
367
|
+
message: '¿Cómo generás el review?',
|
|
368
|
+
options: [
|
|
369
|
+
{ value: 'clipboard', label: 'Copiar el prompt al portapapeles', hint: 'lo pegás en tu agente' },
|
|
370
|
+
...detected.map((a) => ({ value: a.def.id, label: a.def.label, hint: a.path }))
|
|
371
|
+
],
|
|
372
|
+
initialValue: 'clipboard'
|
|
373
|
+
}),
|
|
374
|
+
'modo'
|
|
375
|
+
);
|
|
376
|
+
if (choice === 'clipboard') return null;
|
|
377
|
+
return detected.find((a) => a.def.id === choice) ?? detected[0];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Ya hay un review válido para estos commits: pregunta si abrirlo o generarlo de nuevo.
|
|
382
|
+
* @param {{ path: string }} existing
|
|
383
|
+
* @returns {Promise<{ path: string } | null>} el existente, o `null` para generar de nuevo
|
|
384
|
+
*/
|
|
385
|
+
async function reuseExistingReview(existing, outputFilename) {
|
|
386
|
+
const choice = ensureValue(
|
|
387
|
+
await p.select({
|
|
388
|
+
message: `Ya hay un review válido para estos commits (${outputFilename})`,
|
|
389
|
+
options: [
|
|
390
|
+
{ value: 'open', label: 'Abrir el existente' },
|
|
391
|
+
{ value: 'regenerate', label: 'Generar de nuevo', hint: 'el agente lo reemplaza' }
|
|
392
|
+
],
|
|
393
|
+
initialValue: 'open'
|
|
394
|
+
}),
|
|
395
|
+
'review'
|
|
396
|
+
);
|
|
397
|
+
if (choice !== 'open') return null;
|
|
398
|
+
p.outro('Abro el review existente.');
|
|
399
|
+
return existing;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Lanza el agente y espera el JSON. Devuelve el reporte válido; si el agente termina sin dejarlo, sale con error.
|
|
404
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
405
|
+
* @param {{ repo: string, prompt: string, model: string, outputFilename: string, useClack: boolean }} run
|
|
406
|
+
*/
|
|
407
|
+
async function runAgentAndWait(agent, { repo, prompt, model, outputFilename, useClack }) {
|
|
408
|
+
const what = `${agent.def.label} (${model || 'modelo por defecto'})`;
|
|
409
|
+
const msg = `Lanzando ${what}. Espera ${outputFilename} en la raíz del repo; Ctrl+C cancela.`;
|
|
410
|
+
if (useClack) p.outro(msg);
|
|
411
|
+
else console.log(msg);
|
|
412
|
+
|
|
413
|
+
// Solo cuenta lo que escriba este agente: un JSON anterior de los mismos commits no es el resultado.
|
|
414
|
+
const since = Date.now();
|
|
415
|
+
const { exited } = runAgent(agent, { cwd: repo, prompt, model, outputFilename });
|
|
416
|
+
const waiting = waitForReviewOutput(repo, outputFilename, {
|
|
417
|
+
since,
|
|
418
|
+
onInvalid: (errors) => {
|
|
419
|
+
console.log(`JSON encontrado pero inválido (${errors.length} error(es)). El agente puede corregirlo…`);
|
|
420
|
+
for (const e of errors.slice(0, 5)) console.log(` · ${e}`);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
let code;
|
|
425
|
+
try {
|
|
426
|
+
code = await exited;
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.error(`No se pudo lanzar ${agent.path}: ${err instanceof Error ? err.message : err}`);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
// El watcher puede ir un tick atrás de la última escritura del agente.
|
|
432
|
+
const result = await Promise.race([waiting, new Promise((r) => setTimeout(() => r(null), 2000))]);
|
|
433
|
+
if (!result) {
|
|
434
|
+
console.error(
|
|
435
|
+
code === 0
|
|
436
|
+
? `${agent.def.label} terminó sin dejar un ${outputFilename} válido.`
|
|
437
|
+
: `${agent.def.label} terminó con código ${code} sin dejar un ${outputFilename} válido.`
|
|
438
|
+
);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
if (code !== 0) console.warn(`${agent.def.label} terminó con código ${code}, pero el JSON es válido.`);
|
|
442
|
+
return /** @type {{ path: string }} */ (result);
|
|
443
|
+
}
|
|
444
|
+
|
|
281
445
|
function explainWaitingForAgent(outputFilename, { copied, useClack }) {
|
|
282
446
|
const copyMsg = copied
|
|
283
447
|
? 'Se copió un prompt al portapapeles: pegalo en el chat de tu agente de IA para generar el contenido de la review.'
|
|
@@ -306,12 +470,17 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
|
|
|
306
470
|
process.exit(1);
|
|
307
471
|
}
|
|
308
472
|
|
|
309
|
-
|
|
310
|
-
|
|
473
|
+
const port = await findFreePort(cfg.host, cfg.port);
|
|
474
|
+
if (port == null) {
|
|
475
|
+
const last = Math.min(65535, cfg.port + PORT_SCAN - 1);
|
|
476
|
+
console.error(`No encontré un puerto libre entre ${cfg.port} y ${last} en ${cfg.host}.`);
|
|
311
477
|
process.exit(1);
|
|
312
478
|
}
|
|
479
|
+
if (port !== cfg.port) {
|
|
480
|
+
console.log(`Puerto ${cfg.port} ocupado; la UI usa ${port}.`);
|
|
481
|
+
}
|
|
313
482
|
|
|
314
|
-
const origin = `http://${cfg.host}:${
|
|
483
|
+
const origin = `http://${cfg.host}:${port}`;
|
|
315
484
|
const openUrl = cfg.openPath ? `${origin}${cfg.openPath}` : origin;
|
|
316
485
|
const nodeBin = resolveNodeBin();
|
|
317
486
|
const child = spawn(nodeBin, [entry], {
|
|
@@ -319,7 +488,7 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
|
|
|
319
488
|
env: {
|
|
320
489
|
...process.env,
|
|
321
490
|
HOST: cfg.host,
|
|
322
|
-
PORT: String(
|
|
491
|
+
PORT: String(port),
|
|
323
492
|
ORIGIN: origin,
|
|
324
493
|
DIFF_REVIEW_LAUNCH_CWD: cfg.launchCwd,
|
|
325
494
|
...(cfg.importFile ? { DIFF_REVIEW_IMPORT_FILE: cfg.importFile } : {})
|
|
@@ -398,12 +567,19 @@ async function runUiMode(opts, launch, launchCwd) {
|
|
|
398
567
|
port: opts.port,
|
|
399
568
|
open: opts.open,
|
|
400
569
|
launchCwd,
|
|
401
|
-
openPath: launch
|
|
570
|
+
openPath: launch
|
|
571
|
+
? `/?watch=1¬ify=prompt&out=${encodeURIComponent(outputFilename)}`
|
|
572
|
+
: '/'
|
|
402
573
|
});
|
|
403
574
|
}
|
|
404
575
|
|
|
405
576
|
async function main() {
|
|
406
|
-
const
|
|
577
|
+
const rawArgv = process.argv.slice(2);
|
|
578
|
+
if (rawArgv[0] === 'validate') {
|
|
579
|
+
process.exit(runValidateCli(rawArgv.slice(1)));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const opts = parseArgs(rawArgv);
|
|
407
583
|
if (opts.help) {
|
|
408
584
|
printHelp();
|
|
409
585
|
process.exit(0);
|
|
@@ -461,15 +637,51 @@ async function main() {
|
|
|
461
637
|
{ outputFilename }
|
|
462
638
|
);
|
|
463
639
|
|
|
464
|
-
|
|
465
|
-
|
|
640
|
+
// Sin --agent también se detectan (con terminal interactiva) para ofrecer lanzar uno en vez del portapapeles.
|
|
641
|
+
const detected = opts.agent || isInteractive() ? detectAgents() : [];
|
|
642
|
+
if (opts.agent && !detected.length) {
|
|
643
|
+
const names = AGENTS.map((a) => a.bins[0]).join(', ');
|
|
644
|
+
const warn = `No encontré agentes instalados (${names}). Sigo copiando el prompt.`;
|
|
645
|
+
if (prompted) p.log.warn(warn);
|
|
646
|
+
else console.warn(warn);
|
|
647
|
+
}
|
|
466
648
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
649
|
+
// Hay selects (agente/modelo, o abrir vs. regenerar un review existente) solo con terminal interactiva.
|
|
650
|
+
const existing = isInteractive() ? existingReviewOutput(launch.repo, outputFilename) : null;
|
|
651
|
+
const useClack = prompted || (isInteractive() && (detected.length > 0 || existing != null));
|
|
652
|
+
// Con flags de branch/base no hubo intro, pero estos selects abren sesión de Clack igual.
|
|
653
|
+
if (useClack && !prompted) p.intro('diff-review');
|
|
654
|
+
|
|
655
|
+
let result = existing ? await reuseExistingReview(existing, outputFilename) : null;
|
|
656
|
+
/** @type {import('../lib/agents.mjs').DetectedAgent | null} */
|
|
657
|
+
let chosen = null;
|
|
658
|
+
if (!result && detected.length > 0 && !opts.agent && isInteractive()) {
|
|
659
|
+
chosen = await pickDelivery(detected);
|
|
660
|
+
}
|
|
661
|
+
const useAgent = !result && detected.length > 0 && (opts.agent || chosen != null);
|
|
662
|
+
if (useAgent) {
|
|
663
|
+
const { agent, model } = await pickAgentAndModel(detected, chosen ?? undefined);
|
|
664
|
+
result = await runAgentAndWait(agent, {
|
|
665
|
+
repo: launch.repo,
|
|
666
|
+
prompt,
|
|
667
|
+
model,
|
|
668
|
+
outputFilename,
|
|
669
|
+
useClack
|
|
670
|
+
});
|
|
671
|
+
} else if (!result) {
|
|
672
|
+
// Si eligió generar de nuevo, el JSON existente no cuenta hasta que el agente lo reescriba.
|
|
673
|
+
const since = existing ? Date.now() : undefined;
|
|
674
|
+
const copied = copyPromptOrWarn(prompt);
|
|
675
|
+
explainWaitingForAgent(outputFilename, { copied, useClack });
|
|
676
|
+
|
|
677
|
+
result = await waitForReviewOutput(launch.repo, outputFilename, {
|
|
678
|
+
since,
|
|
679
|
+
onInvalid: (errors) => {
|
|
680
|
+
console.log(`JSON encontrado pero inválido (${errors.length} error(es)). Esperando corrección…`);
|
|
681
|
+
for (const e of errors.slice(0, 5)) console.log(` · ${e}`);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
473
685
|
|
|
474
686
|
console.log('JSON OK. Abriendo la UI web…');
|
|
475
687
|
await startServer({
|