@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.
- package/README.md +4 -4
- package/bin/diff-review.mjs +231 -89
- package/lib/autocomplete-es.mjs +143 -0
- package/lib/output-name.mjs +18 -30
- package/lib/output-name.test.mjs +10 -27
- package/lib/prompt.mjs +13 -7
- package/package.json +3 -2
- package/schema.json +12 -0
- package/web/build/client/_app/immutable/chunks/{CAnYe5db.js → CO0mUwMJ.js} +1 -1
- package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.br +2 -0
- package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{BaYDuTXu.js → CdHZDAAo.js} +26 -23
- 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/{Gya4IU1I.js → DzqTpz7_.js} +2 -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.e96uPVtl.js → app.BRzz8J51.js} +2 -2
- 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 +1 -0
- 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.V-g5-sfL.js → 1.ws8Khan5.js} +1 -1
- package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.br +1 -0
- package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js +66 -0
- 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 +170 -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/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-qrn24yN5.js +7 -0
- package/web/build/server/chunks/{1-DCKScL3A.js.map → 1-qrn24yN5.js.map} +1 -1
- package/web/build/server/chunks/{2-BpBQQ02B.js → 2-C7_Loqyn.js} +2 -2
- package/web/build/server/chunks/{2-BpBQQ02B.js.map → 2-C7_Loqyn.js.map} +1 -1
- package/web/build/server/chunks/{3-rGkcNCVz.js → 3-DZ5PBhOc.js} +2 -2
- package/web/build/server/chunks/{3-rGkcNCVz.js.map → 3-DZ5PBhOc.js.map} +1 -1
- package/web/build/server/chunks/{handler-BaT-QavL.js → handler-DD4vP3bU.js} +3 -3
- package/web/build/server/chunks/{handler-BaT-QavL.js.map → handler-DD4vP3bU.js.map} +1 -1
- package/web/build/server/chunks/{index.js-C-K0YDBJ.js → index.js-IhFPHwmp.js} +2 -2
- package/web/build/server/chunks/{index.js-C-K0YDBJ.js.map → index.js-IhFPHwmp.js.map} +1 -1
- package/web/build/server/chunks/{manifest.js-CDz-Fem4.js → manifest.js-Dk3RuAA5.js} +6 -6
- package/web/build/server/chunks/{manifest.js-CDz-Fem4.js.map → manifest.js-Dk3RuAA5.js.map} +1 -1
- package/web/build/server/chunks/{remote-1g4ayag-DXdFe1BG.js → remote-1g4ayag-BRL-uaKE.js} +27 -40
- package/web/build/server/chunks/{remote-1g4ayag-DXdFe1BG.js.map → remote-1g4ayag-BRL-uaKE.js.map} +1 -1
- package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CAnYe5db.js.br +0 -1
- package/web/build/client/_app/immutable/chunks/CAnYe5db.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js +0 -1
- package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js +0 -66
- package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js +0 -170
- package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.gz +0 -0
- 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.
|
|
30
|
-
2.
|
|
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
|
|
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-
|
|
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.
|
package/bin/diff-review.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* CLI híbrido C:
|
|
4
|
-
* diff-review → terminal:
|
|
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 {
|
|
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):
|
|
52
|
-
|
|
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 (
|
|
60
|
-
--branch <b> Branch a revisar (
|
|
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 {{
|
|
137
|
+
* @returns {{
|
|
138
|
+
* repo: string,
|
|
139
|
+
* listed: { branches: string[], current: string, defaultBase: string },
|
|
140
|
+
* remoteUrl?: string
|
|
141
|
+
* } | null}
|
|
134
142
|
*/
|
|
135
|
-
function
|
|
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,
|
|
149
|
+
return { repo: root, listed, remoteUrl };
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
function
|
|
151
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
267
|
-
skipPromptCopy = true;
|
|
424
|
+
process.exit(1);
|
|
268
425
|
}
|
|
269
426
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
);
|
|
307
|
-
|
|
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:
|
|
316
|
-
branch
|
|
317
|
-
base
|
|
318
|
-
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
|
|
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
|
|
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:
|
|
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
|
+
}
|
package/lib/output-name.mjs
CHANGED
|
@@ -1,54 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Nombre del JSON que escribe el agente.
|
|
3
|
-
* Preferido: tips cortos
|
|
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
|
|
8
|
-
export const OUTPUT_PREFIX = "diff-
|
|
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-
|
|
19
|
-
* - diff-
|
|
20
|
-
* - diff-
|
|
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-
|
|
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 {
|
|
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-
|
|
44
|
-
* sin tips: diff-review-output
|
|
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 ? {
|
|
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)
|
|
51
|
-
|
|
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) {
|