@jpkme/diff-review 0.1.3 → 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 +22 -6
- package/bin/diff-review.mjs +328 -18
- package/lib/autocomplete-es.mjs +143 -0
- package/lib/clipboard.mjs +29 -0
- package/lib/config.mjs +233 -0
- package/lib/config.test.mjs +73 -0
- package/lib/extract.mjs +111 -0
- package/lib/git.mjs +202 -0
- package/lib/git.test.mjs +29 -0
- package/lib/json-payload.mjs +31 -0
- package/lib/json-payload.test.mjs +22 -0
- package/lib/output-name.mjs +44 -0
- package/lib/output-name.test.mjs +37 -0
- package/lib/pick-folder.mjs +4 -0
- package/lib/prompt.mjs +160 -0
- package/lib/remote-repo.mjs +182 -0
- package/lib/review.mjs +228 -0
- package/lib/review.test.mjs +160 -0
- package/lib/validate.mjs +221 -0
- package/lib/validate.test.mjs +85 -0
- package/lib/wait-output.mjs +104 -0
- package/package.json +4 -2
- package/schema.json +12 -0
- package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css +1 -0
- 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 +1 -0
- 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/{C5moC2DW.js → CdHZDAAo.js} +42 -32
- 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 +2 -0
- 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.Dei8qxo7.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.ws8Khan5.js +1 -0
- 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-qrn24yN5.js.map +1 -0
- package/web/build/server/chunks/2-C7_Loqyn.js +7 -0
- package/web/build/server/chunks/2-C7_Loqyn.js.map +1 -0
- package/web/build/server/chunks/{3-Zc5_qwgV.js → 3-DZ5PBhOc.js} +2 -2
- package/web/build/server/chunks/{3-Zc5_qwgV.js.map → 3-DZ5PBhOc.js.map} +1 -1
- package/web/build/server/chunks/{handler-BmuqxCCf.js → handler-DD4vP3bU.js} +3 -3
- package/web/build/server/chunks/{handler-BmuqxCCf.js.map → handler-DD4vP3bU.js.map} +1 -1
- package/web/build/server/chunks/{index.js-41EcOndB.js → index.js-IhFPHwmp.js} +150 -150
- package/web/build/server/chunks/index.js-IhFPHwmp.js.map +1 -0
- package/web/build/server/chunks/{manifest.js-KnmnjP9i.js → manifest.js-Dk3RuAA5.js} +6 -6
- package/web/build/server/chunks/{manifest.js-KnmnjP9i.js.map → manifest.js-Dk3RuAA5.js.map} +1 -1
- package/web/build/server/chunks/{remote-1g4ayag-BgUNMUS8.js → remote-1g4ayag-BRL-uaKE.js} +632 -146
- package/web/build/server/chunks/remote-1g4ayag-BRL-uaKE.js.map +1 -0
- package/web/build/client/_app/immutable/assets/2.Du8E-7-D.css +0 -1
- package/web/build/client/_app/immutable/assets/2.Du8E-7-D.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.Du8E-7-D.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/C5moC2DW.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/C5moC2DW.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/WxPrACSs.js +0 -2
- package/web/build/client/_app/immutable/chunks/WxPrACSs.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/WxPrACSs.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.Dei8qxo7.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.Dei8qxo7.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.Fzxrn2l4.js +0 -1
- package/web/build/client/_app/immutable/entry/start.Fzxrn2l4.js.br +0 -1
- package/web/build/client/_app/immutable/entry/start.Fzxrn2l4.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.Cn_iktkl.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.Cn_iktkl.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.Cn_iktkl.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.vvYFHTy-.js +0 -66
- package/web/build/client/_app/immutable/nodes/2.vvYFHTy-.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.vvYFHTy-.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.CeJ7dEsh.js +0 -170
- package/web/build/client/_app/immutable/nodes/3.CeJ7dEsh.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.CeJ7dEsh.js.gz +0 -0
- package/web/build/server/chunks/1-Bjgeu1Iz.js +0 -7
- package/web/build/server/chunks/1-Bjgeu1Iz.js.map +0 -1
- package/web/build/server/chunks/2-BvMZU9fP.js +0 -7
- package/web/build/server/chunks/2-BvMZU9fP.js.map +0 -1
- package/web/build/server/chunks/index.js-41EcOndB.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-BgUNMUS8.js.map +0 -1
package/README.md
CHANGED
|
@@ -22,7 +22,23 @@ O sin instalar:
|
|
|
22
22
|
npx @jpkme/diff-review
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
### Modos
|
|
26
|
+
|
|
27
|
+
Desde un **repo git**, el default es terminal-first:
|
|
28
|
+
|
|
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
|
+
3. Valida y abre el viewer con el reporte ya importado.
|
|
32
|
+
|
|
33
|
+
Si pasás `--branch` y `--base` juntos, saltea los selects. Si quedan iguales (diff vacío), abre la UI.
|
|
34
|
+
|
|
35
|
+
Para abrir la UI primero (sin esperar el JSON):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
diff-review --ui
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Opciones: `--port`, `--host`, `--base`, `--branch`, `--no-open`.
|
|
26
42
|
|
|
27
43
|
Si lo lanzás desde un repositorio git, la UI precarga esa ruta y el branch actual.
|
|
28
44
|
|
|
@@ -35,12 +51,12 @@ npm run build # build de producción → web/build
|
|
|
35
51
|
npm start # mismo que el bin global
|
|
36
52
|
```
|
|
37
53
|
|
|
38
|
-
## Flujo
|
|
54
|
+
## Flujo (UI)
|
|
39
55
|
|
|
40
|
-
1. Pegá la ruta (o URL) del repo y elegí branch / base — o lanzá `diff-review` desde el repo
|
|
56
|
+
1. Pegá la ruta (o URL) del repo y elegí branch / base — o lanzá `diff-review` / `diff-review --ui` desde el repo.
|
|
41
57
|
2. Copiá el prompt (o el comando de skill `/diff-review`).
|
|
42
|
-
3. Corrélo en tu agente; que escriba `diff-
|
|
43
|
-
4. Arrastrá ese JSON a la dropzone. La UI hidrata los diffs con git.
|
|
58
|
+
3. Corrélo en tu agente; que escriba `diff-rev_{branchTip}_{baseTip}.json` (también acepta nombres legacy).
|
|
59
|
+
4. Arrastrá ese JSON a la dropzone (o dejá que el CLI lo importe solo). La UI hidrata los diffs con git.
|
|
44
60
|
|
|
45
61
|
"Ver ejemplo" carga un reporte de muestra sin tocar un repo.
|
|
46
62
|
|
|
@@ -84,7 +100,7 @@ y minificados.
|
|
|
84
100
|
.
|
|
85
101
|
├── bin/ # CLI (diff-review)
|
|
86
102
|
├── schema.json # contrato del JSON que escribe el agente
|
|
87
|
-
├── lib/ # git / extract (dev + tests; va
|
|
103
|
+
├── lib/ # git / extract / prompt (dev + tests; va en el paquete)
|
|
88
104
|
└── web/ # UI SvelteKit → web/build en el paquete npm
|
|
89
105
|
```
|
|
90
106
|
|
package/bin/diff-review.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* CLI
|
|
3
|
+
* CLI híbrido C:
|
|
4
|
+
* diff-review → terminal: elegir branch/base → prompt + wait JSON → viewer
|
|
5
|
+
* diff-review --ui → web-first (UI inmediata)
|
|
4
6
|
*
|
|
5
|
-
* diff-review
|
|
6
7
|
* diff-review --port 5191
|
|
8
|
+
* diff-review --branch x --base y → saltea selects de Clack
|
|
7
9
|
* diff-review --no-open
|
|
8
10
|
*/
|
|
9
11
|
|
|
@@ -12,6 +14,13 @@ import { existsSync } from 'node:fs';
|
|
|
12
14
|
import { createServer } from 'node:net';
|
|
13
15
|
import { dirname, join } from 'node:path';
|
|
14
16
|
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import * as p from '@clack/prompts';
|
|
18
|
+
import { autocompleteEs } from '../lib/autocomplete-es.mjs';
|
|
19
|
+
import { copyTextToClipboard } from '../lib/clipboard.mjs';
|
|
20
|
+
import { branchTips, getRemoteUrl, listBranches, tryResolveGitRoot } from '../lib/git.mjs';
|
|
21
|
+
import { makeOutputFilename } from '../lib/output-name.mjs';
|
|
22
|
+
import { buildPrompt } from '../lib/prompt.mjs';
|
|
23
|
+
import { waitForReviewOutput } from '../lib/wait-output.mjs';
|
|
15
24
|
|
|
16
25
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
26
|
const packageRoot = join(here, '..');
|
|
@@ -37,14 +46,22 @@ function resolveNodeBin() {
|
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
function printHelp() {
|
|
40
|
-
console.log(`diff-review —
|
|
49
|
+
console.log(`diff-review — review de branch con tu agente + UI local.
|
|
41
50
|
|
|
42
51
|
Usage:
|
|
43
52
|
diff-review [options]
|
|
44
53
|
|
|
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.
|
|
57
|
+
Con --ui: abre la UI de inmediato (web-first).
|
|
58
|
+
|
|
45
59
|
Options:
|
|
46
60
|
-p, --port <n> Puerto (default ${DEFAULT_PORT})
|
|
47
61
|
--host <h> Host (default ${DEFAULT_HOST})
|
|
62
|
+
--ui Abrir la UI primero (sin esperar JSON)
|
|
63
|
+
--base <b> Base del merge-base (salta el select de base)
|
|
64
|
+
--branch <b> Branch a revisar (salta el select de branch)
|
|
48
65
|
--no-open No abrir el navegador
|
|
49
66
|
-h, --help Esta ayuda
|
|
50
67
|
|
|
@@ -53,12 +70,23 @@ Requiere git en PATH. Los reportes viven en localStorage del navegador.
|
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
function parseArgs(argv) {
|
|
56
|
-
const opts = {
|
|
73
|
+
const opts = {
|
|
74
|
+
port: DEFAULT_PORT,
|
|
75
|
+
host: DEFAULT_HOST,
|
|
76
|
+
open: true,
|
|
77
|
+
help: false,
|
|
78
|
+
ui: false,
|
|
79
|
+
base: '',
|
|
80
|
+
branch: ''
|
|
81
|
+
};
|
|
57
82
|
for (let i = 0; i < argv.length; i++) {
|
|
58
83
|
const a = argv[i];
|
|
59
84
|
if (a === '-h' || a === '--help') opts.help = true;
|
|
60
85
|
else if (a === '--no-open') opts.open = false;
|
|
86
|
+
else if (a === '--ui') opts.ui = true;
|
|
61
87
|
else if (a === '--host') opts.host = argv[++i] || opts.host;
|
|
88
|
+
else if (a === '--base') opts.base = argv[++i] || opts.base;
|
|
89
|
+
else if (a === '--branch') opts.branch = argv[++i] || opts.branch;
|
|
62
90
|
else if (a === '-p' || a === '--port') {
|
|
63
91
|
const n = Number(argv[++i]);
|
|
64
92
|
if (!Number.isFinite(n) || n <= 0) {
|
|
@@ -105,13 +133,172 @@ function canListen(host, port) {
|
|
|
105
133
|
});
|
|
106
134
|
}
|
|
107
135
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
/**
|
|
137
|
+
* @returns {{
|
|
138
|
+
* repo: string,
|
|
139
|
+
* listed: { branches: string[], current: string, defaultBase: string },
|
|
140
|
+
* remoteUrl?: string
|
|
141
|
+
* } | null}
|
|
142
|
+
*/
|
|
143
|
+
function loadRepoLaunch() {
|
|
144
|
+
const cwd = process.cwd();
|
|
145
|
+
const root = tryResolveGitRoot(cwd);
|
|
146
|
+
if (!root) return null;
|
|
147
|
+
const listed = listBranches(root);
|
|
148
|
+
const remoteUrl = getRemoteUrl(root) || undefined;
|
|
149
|
+
return { repo: root, listed, remoteUrl };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function defaultBranch(listed) {
|
|
153
|
+
return listed.current && listed.current !== 'HEAD' ? listed.current : listed.branches[0] || '';
|
|
154
|
+
}
|
|
155
|
+
|
|
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.');
|
|
112
173
|
process.exit(0);
|
|
113
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);
|
|
205
|
+
}
|
|
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
|
+
);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
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
|
+
}
|
|
114
297
|
|
|
298
|
+
/**
|
|
299
|
+
* @param {{ host: string, port: number, open: boolean, launchCwd: string, importFile?: string, openPath?: string }} cfg
|
|
300
|
+
*/
|
|
301
|
+
async function startServer(cfg) {
|
|
115
302
|
if (!existsSync(entry)) {
|
|
116
303
|
console.error(`No encontré el build en ${entry}
|
|
117
304
|
Corré desde el repo: npm run build
|
|
@@ -119,23 +306,23 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
|
|
|
119
306
|
process.exit(1);
|
|
120
307
|
}
|
|
121
308
|
|
|
122
|
-
if (!(await canListen(
|
|
123
|
-
console.error(`No se pudo usar ${
|
|
309
|
+
if (!(await canListen(cfg.host, cfg.port))) {
|
|
310
|
+
console.error(`No se pudo usar ${cfg.host}:${cfg.port} (¿ocupado?). Probá --port otro.`);
|
|
124
311
|
process.exit(1);
|
|
125
312
|
}
|
|
126
313
|
|
|
127
|
-
const origin = `http://${
|
|
314
|
+
const origin = `http://${cfg.host}:${cfg.port}`;
|
|
315
|
+
const openUrl = cfg.openPath ? `${origin}${cfg.openPath}` : origin;
|
|
128
316
|
const nodeBin = resolveNodeBin();
|
|
129
|
-
/** CWD del usuario al invocar el CLI (el server corre desde web/build). */
|
|
130
|
-
const launchCwd = process.cwd();
|
|
131
317
|
const child = spawn(nodeBin, [entry], {
|
|
132
318
|
cwd: buildDir,
|
|
133
319
|
env: {
|
|
134
320
|
...process.env,
|
|
135
|
-
HOST:
|
|
136
|
-
PORT: String(
|
|
321
|
+
HOST: cfg.host,
|
|
322
|
+
PORT: String(cfg.port),
|
|
137
323
|
ORIGIN: origin,
|
|
138
|
-
DIFF_REVIEW_LAUNCH_CWD: launchCwd
|
|
324
|
+
DIFF_REVIEW_LAUNCH_CWD: cfg.launchCwd,
|
|
325
|
+
...(cfg.importFile ? { DIFF_REVIEW_IMPORT_FILE: cfg.importFile } : {})
|
|
139
326
|
},
|
|
140
327
|
stdio: ['ignore', 'pipe', 'inherit'],
|
|
141
328
|
windowsHide: false
|
|
@@ -152,7 +339,7 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
|
|
|
152
339
|
if (!ready) {
|
|
153
340
|
ready = true;
|
|
154
341
|
console.log(`diff-review listo en ${terminalLink(origin)} — Ctrl+C para salir.`);
|
|
155
|
-
if (
|
|
342
|
+
if (cfg.open) openBrowser(openUrl);
|
|
156
343
|
}
|
|
157
344
|
continue;
|
|
158
345
|
}
|
|
@@ -175,4 +362,127 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
|
|
|
175
362
|
process.on('SIGTERM', stop);
|
|
176
363
|
}
|
|
177
364
|
|
|
178
|
-
|
|
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
|
+
|
|
405
|
+
async function main() {
|
|
406
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
407
|
+
if (opts.help) {
|
|
408
|
+
printHelp();
|
|
409
|
+
process.exit(0);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const launchCwd = process.cwd();
|
|
413
|
+
const launch = loadRepoLaunch();
|
|
414
|
+
|
|
415
|
+
if (opts.ui) {
|
|
416
|
+
await runUiMode(opts, launch, launchCwd);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
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.'
|
|
423
|
+
);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
|
|
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
|
+
);
|
|
434
|
+
await startServer({
|
|
435
|
+
host: opts.host,
|
|
436
|
+
port: opts.port,
|
|
437
|
+
open: opts.open,
|
|
438
|
+
launchCwd,
|
|
439
|
+
openPath: '/'
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
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');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const prompt = buildPrompt(
|
|
454
|
+
{
|
|
455
|
+
source: 'local',
|
|
456
|
+
repo: launch.repo,
|
|
457
|
+
branch,
|
|
458
|
+
base,
|
|
459
|
+
remoteUrl: launch.remoteUrl
|
|
460
|
+
},
|
|
461
|
+
{ outputFilename }
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const copied = copyPromptOrWarn(prompt);
|
|
465
|
+
explainWaitingForAgent(outputFilename, { copied, useClack: prompted });
|
|
466
|
+
|
|
467
|
+
const result = await waitForReviewOutput(launch.repo, outputFilename, {
|
|
468
|
+
onInvalid: (errors) => {
|
|
469
|
+
console.log(`JSON encontrado pero inválido (${errors.length} error(es)). Esperando corrección…`);
|
|
470
|
+
for (const e of errors.slice(0, 5)) console.log(` · ${e}`);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
console.log('JSON OK. Abriendo la UI web…');
|
|
475
|
+
await startServer({
|
|
476
|
+
host: opts.host,
|
|
477
|
+
port: opts.port,
|
|
478
|
+
open: opts.open,
|
|
479
|
+
launchCwd: launch.repo,
|
|
480
|
+
importFile: result.path,
|
|
481
|
+
openPath: '/?import=1'
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
main().catch((err) => {
|
|
486
|
+
console.error(err?.message || err);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/** Copia texto al portapapeles del SO. Devuelve true si parece haber funcionado. */
|
|
4
|
+
export function copyTextToClipboard(text) {
|
|
5
|
+
const value = String(text ?? "");
|
|
6
|
+
try {
|
|
7
|
+
if (process.platform === "win32") {
|
|
8
|
+
const r = spawnSync("clip", [], {
|
|
9
|
+
input: value,
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
windowsHide: true,
|
|
12
|
+
});
|
|
13
|
+
return r.status === 0;
|
|
14
|
+
}
|
|
15
|
+
if (process.platform === "darwin") {
|
|
16
|
+
const r = spawnSync("pbcopy", [], { input: value, encoding: "utf8" });
|
|
17
|
+
return r.status === 0;
|
|
18
|
+
}
|
|
19
|
+
let r = spawnSync("wl-copy", [], { input: value, encoding: "utf8" });
|
|
20
|
+
if (r.status === 0) return true;
|
|
21
|
+
r = spawnSync("xclip", ["-selection", "clipboard"], {
|
|
22
|
+
input: value,
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
});
|
|
25
|
+
return r.status === 0;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|