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