@jpkme/diff-review 0.1.10 → 0.1.12
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 +38 -15
- package/bin/diff-review.mjs +151 -34
- package/lib/acp-policy.mjs +136 -0
- package/lib/acp-policy.test.mjs +83 -0
- package/lib/acp-shell.mjs +379 -0
- package/lib/acp-shell.test.mjs +127 -0
- package/lib/acp.mjs +368 -0
- package/lib/acp.test.mjs +58 -0
- package/lib/agent-login.mjs +105 -0
- package/lib/agents.mjs +87 -14
- package/lib/agents.test.mjs +4 -6
- package/lib/check-report.mjs +29 -11
- package/lib/extract-cache.test.mjs +162 -0
- package/lib/extract.mjs +197 -1
- package/lib/model-label.mjs +263 -0
- package/lib/model-label.test.mjs +77 -0
- package/lib/models-cache.mjs +75 -0
- package/lib/models-cache.test.mjs +50 -0
- package/lib/output-name.mjs +8 -0
- package/lib/prompt.mjs +168 -22
- package/lib/review-dir.mjs +108 -0
- package/lib/review-meta.mjs +5 -3
- package/lib/review-meta.test.mjs +11 -0
- package/lib/review-prep.mjs +63 -0
- package/lib/validate.mjs +2 -2
- package/lib/wait-output.mjs +46 -23
- package/lib/wait-output.test.mjs +24 -1
- package/package.json +3 -1
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +1 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css +1 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{BBWAo_yW.js → B5ByWcPE.js} +2 -2
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js +1 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js +3 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +210 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +1 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js +2 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js +1 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +10 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js +171 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.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/{0-SseDy__O.js → 0-BYok4D0n.js} +2 -2
- package/web/build/server/chunks/{0-SseDy__O.js.map → 0-BYok4D0n.js.map} +1 -1
- package/web/build/server/chunks/1-C7zcB0q0.js +7 -0
- package/web/build/server/chunks/1-C7zcB0q0.js.map +1 -0
- package/web/build/server/chunks/2-v6ejI_0-.js +7 -0
- package/web/build/server/chunks/2-v6ejI_0-.js.map +1 -0
- package/web/build/server/chunks/3-DAAmG6VX.js +7 -0
- package/web/build/server/chunks/3-DAAmG6VX.js.map +1 -0
- package/web/build/server/chunks/_server.ts-qYVrMYPB.js +62 -0
- package/web/build/server/chunks/_server.ts-qYVrMYPB.js.map +1 -0
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js +2688 -0
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +1 -0
- package/web/build/server/chunks/{remote-1g4ayag-B67QMR_2.js → git-review-CpO_G8z8.js} +70 -704
- package/web/build/server/chunks/git-review-CpO_G8z8.js.map +1 -0
- package/web/build/server/chunks/{handler-B5P65tLk.js → handler-Di1wJQuU.js} +3 -3
- package/web/build/server/chunks/{handler-B5P65tLk.js.map → handler-Di1wJQuU.js.map} +1 -1
- package/web/build/server/chunks/hooks.server-BLOHobEK.js +42 -0
- package/web/build/server/chunks/hooks.server-BLOHobEK.js.map +1 -0
- package/web/build/server/chunks/{index.js-CL95e0Gl.js → index.js-yLYmrAWy.js} +3 -5
- package/web/build/server/chunks/index.js-yLYmrAWy.js.map +1 -0
- package/web/build/server/chunks/manifest.js-CXj8IIo3.js +58 -0
- package/web/build/server/chunks/manifest.js-CXj8IIo3.js.map +1 -0
- package/web/build/server/chunks/remote-1g4ayag-BiJeU0Pk.js +144 -0
- package/web/build/server/chunks/remote-1g4ayag-BiJeU0Pk.js.map +1 -0
- package/web/build/server/chunks/remote-wh6t6j-CHanXurm.js +162 -0
- package/web/build/server/chunks/remote-wh6t6j-CHanXurm.js.map +1 -0
- package/web/build/server/chunks/validate-BsTKyhsq.js +1073 -0
- package/web/build/server/chunks/validate-BsTKyhsq.js.map +1 -0
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css +0 -1
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css +0 -1
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css +0 -1
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.br +0 -0
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/BBWAo_yW.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/BBWAo_yW.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js +0 -3
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.js +0 -176
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js +0 -1
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js +0 -2
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js +0 -1
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js +0 -1
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.br +0 -1
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js +0 -66
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js +0 -170
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.gz +0 -0
- package/web/build/server/chunks/1-2OO2bb7H.js +0 -7
- package/web/build/server/chunks/1-2OO2bb7H.js.map +0 -1
- package/web/build/server/chunks/2-BJvuqKN6.js +0 -7
- package/web/build/server/chunks/2-BJvuqKN6.js.map +0 -1
- package/web/build/server/chunks/3-ZIl1rHef.js +0 -7
- package/web/build/server/chunks/3-ZIl1rHef.js.map +0 -1
- package/web/build/server/chunks/hooks.server-DERFlOe1.js +0 -14
- package/web/build/server/chunks/hooks.server-DERFlOe1.js.map +0 -1
- package/web/build/server/chunks/index.js-CL95e0Gl.js.map +0 -1
- package/web/build/server/chunks/manifest.js-uD_a0079.js +0 -50
- package/web/build/server/chunks/manifest.js-uD_a0079.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-B67QMR_2.js.map +0 -1
package/lib/agents.mjs
CHANGED
|
@@ -6,9 +6,21 @@
|
|
|
6
6
|
* no se toca el portapapeles ni se choca con el límite de largo de la línea de comandos de los shims `.cmd`.
|
|
7
7
|
*/
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
accessSync,
|
|
11
|
+
constants,
|
|
12
|
+
copyFileSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
statSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
22
|
import { delimiter, join } from "node:path";
|
|
23
|
+
import { outputRelPath } from "./output-name.mjs";
|
|
12
24
|
|
|
13
25
|
/**
|
|
14
26
|
* @typedef {{ value: string, label: string, hint?: string }} ModelOption
|
|
@@ -21,7 +33,10 @@ import { delimiter, join } from "node:path";
|
|
|
21
33
|
* models: ModelOption[],
|
|
22
34
|
* configuredModel?: (ctx: DetectContext) => { model: string, source: string } | null,
|
|
23
35
|
* args: (spec: RunSpec) => string[],
|
|
36
|
+
* acp?: (ctx: { path: string }) => AcpLaunch,
|
|
24
37
|
* }} AgentDef
|
|
38
|
+
* @typedef {{ file: string, args: string[], env?: Record<string, string>, sessionMeta?: Record<string, unknown> }} AcpLaunch
|
|
39
|
+
* `env` se suma al del proceso; `sessionMeta` va como `_meta` en `session/new` / `session/load`.
|
|
25
40
|
* @typedef {{ env: NodeJS.ProcessEnv, platform: NodeJS.Platform, home: string }} DetectContext
|
|
26
41
|
* @typedef {{ def: AgentDef, path: string }} DetectedAgent
|
|
27
42
|
*/
|
|
@@ -78,6 +93,32 @@ function versionedBinaries(root, exe) {
|
|
|
78
93
|
return out;
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Carpeta de configuración para Cursor por ACP: la del usuario sin su allowlist. Cursor aprueba solo lo que matchee
|
|
98
|
+
* `permissions.allow` mirando el nombre del comando (`Shell(echo)` deja pasar `echo x > archivo`); vaciándola,
|
|
99
|
+
* cada comando pasa por la política de diff-review. El login no vive ahí, así que se conserva. El modelo elegido
|
|
100
|
+
* por ACP se guarda en esta carpeta y no pisa el default del usuario.
|
|
101
|
+
*/
|
|
102
|
+
export function cursorAcpConfigDir(ctx) {
|
|
103
|
+
const c = context(ctx);
|
|
104
|
+
const userDir = c.env.CURSOR_CONFIG_DIR?.trim() || (c.env.XDG_CONFIG_HOME?.trim() ? join(c.env.XDG_CONFIG_HOME, "cursor") : join(c.home, ".cursor"));
|
|
105
|
+
const dir = join(tmpdir(), "diff-review", "cursor-config");
|
|
106
|
+
mkdirSync(dir, { recursive: true });
|
|
107
|
+
const config = readJson(join(userDir, "cli-config.json")) ?? {};
|
|
108
|
+
writeFileSync(
|
|
109
|
+
join(dir, "cli-config.json"),
|
|
110
|
+
JSON.stringify({ ...config, permissions: { allow: [], deny: config.permissions?.deny ?? [] }, approvalMode: "allowlist" }, null, 2),
|
|
111
|
+
);
|
|
112
|
+
// El modelo que elige ACP queda en acp-config.json: arrancar siempre desde el default del usuario.
|
|
113
|
+
const userAcp = join(userDir, "acp-config.json");
|
|
114
|
+
if (existsSync(userAcp)) copyFileSync(userAcp, join(dir, "acp-config.json"));
|
|
115
|
+
else rmSync(join(dir, "acp-config.json"), { force: true });
|
|
116
|
+
return dir;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Adaptador ACP de Claude Code (antes `@zed-industries/claude-agent-acp`). */
|
|
120
|
+
export const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp@^0.91.0";
|
|
121
|
+
|
|
81
122
|
/** @type {AgentDef[]} */
|
|
82
123
|
export const AGENTS = [
|
|
83
124
|
{
|
|
@@ -118,8 +159,26 @@ export const AGENTS = [
|
|
|
118
159
|
"Glob",
|
|
119
160
|
"Bash(git:*)",
|
|
120
161
|
"Bash(npx --yes @jpkme/diff-review validate:*)",
|
|
121
|
-
`Edit(./${outputFilename})`,
|
|
162
|
+
`Edit(./${outputRelPath(outputFilename)})`,
|
|
122
163
|
],
|
|
164
|
+
// Claude Code no habla ACP solo: el adaptador oficial lo envuelve (usa el login de Claude Code del usuario).
|
|
165
|
+
acp: () => {
|
|
166
|
+
const local = findOnPath("claude-agent-acp", context());
|
|
167
|
+
return {
|
|
168
|
+
...(local ? { file: local, args: [] } : { file: "npx", args: ["--yes", CLAUDE_ACP_PACKAGE] }),
|
|
169
|
+
sessionMeta: {
|
|
170
|
+
claudeCode: {
|
|
171
|
+
options: {
|
|
172
|
+
// Solo las tools que una review necesita (nada de web, subagentes ni MCP).
|
|
173
|
+
tools: ["Read", "Grep", "Glob", "Bash", "Edit", "Write"],
|
|
174
|
+
// Sin la configuración del usuario ni la del repo: su allowlist aprobaría comandos sin pasar por la
|
|
175
|
+
// política de diff-review (y un repo podría traer la suya en .claude/settings.json).
|
|
176
|
+
settingSources: [],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
},
|
|
123
182
|
},
|
|
124
183
|
{
|
|
125
184
|
id: "codex",
|
|
@@ -157,6 +216,8 @@ export const AGENTS = [
|
|
|
157
216
|
// Con -p lee el prompt de stdin. Sin --force solo corre lo que esté en la allowlist del usuario
|
|
158
217
|
// (`~/.cursor/cli-config.json`) y bloquearía el validate; no hay flag para pasar una allowlist.
|
|
159
218
|
args: ({ model }) => ["-p", "--output-format", "text", "--trust", "--force", ...(model ? ["--model", model] : [])],
|
|
219
|
+
// Modo ACP nativo. Con su configuración aislada (ver cursorAcpConfigDir), cada comando pide permiso.
|
|
220
|
+
acp: ({ path }) => ({ file: path, args: ["acp"], env: { CURSOR_CONFIG_DIR: cursorAcpConfigDir() } }),
|
|
160
221
|
},
|
|
161
222
|
{
|
|
162
223
|
id: "copilot",
|
|
@@ -180,8 +241,10 @@ export const AGENTS = [
|
|
|
180
241
|
"--allow-tool",
|
|
181
242
|
"shell(npx:*)",
|
|
182
243
|
"--allow-tool",
|
|
183
|
-
`write(${outputFilename})`,
|
|
244
|
+
`write(${outputRelPath(outputFilename)})`,
|
|
184
245
|
],
|
|
246
|
+
// Modo ACP nativo. Su opción de sesión `allow_all` queda apagada: cada permiso pasa por nuestra política.
|
|
247
|
+
acp: ({ path }) => ({ file: path, args: ["--acp"] }),
|
|
185
248
|
},
|
|
186
249
|
{
|
|
187
250
|
id: "gemini",
|
|
@@ -253,12 +316,10 @@ export function detectAgents(ctx) {
|
|
|
253
316
|
|
|
254
317
|
/** Valor del select para "no pasar --model" (que el agente use el suyo). */
|
|
255
318
|
export const AGENT_DEFAULT_MODEL = "";
|
|
256
|
-
/** Valor del select para escribir un id de modelo a mano. */
|
|
257
|
-
export const OTHER_MODEL = "__other__";
|
|
258
319
|
|
|
259
320
|
/**
|
|
260
|
-
* Opciones del select de modelo: primero el default del agente (marcado como inicial), después los conocidos
|
|
261
|
-
*
|
|
321
|
+
* Opciones del select de modelo: primero el default del agente (marcado como inicial), después los conocidos.
|
|
322
|
+
* Si el default configurado coincide con uno conocido, no se repite.
|
|
262
323
|
* @param {AgentDef} def
|
|
263
324
|
* @param {Partial<DetectContext>} [ctx]
|
|
264
325
|
* @returns {{ options: ModelOption[], initialValue: string }}
|
|
@@ -270,7 +331,7 @@ export function modelChoices(def, ctx) {
|
|
|
270
331
|
: { value: AGENT_DEFAULT_MODEL, label: "Por defecto del agente", hint: "no se pasa --model" };
|
|
271
332
|
const rest = def.models.filter((m) => m.value !== configured?.model);
|
|
272
333
|
return {
|
|
273
|
-
options: [first, ...rest
|
|
334
|
+
options: [first, ...rest],
|
|
274
335
|
initialValue: AGENT_DEFAULT_MODEL,
|
|
275
336
|
};
|
|
276
337
|
}
|
|
@@ -295,6 +356,22 @@ function quoteForCmd(arg) {
|
|
|
295
356
|
return /^[\w./:@=-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '""')}"`;
|
|
296
357
|
}
|
|
297
358
|
|
|
359
|
+
/**
|
|
360
|
+
* `spawn` que también lanza los shims `.cmd`/`.bat` de npm en Windows (y `npx` a secas, que es uno de ellos).
|
|
361
|
+
* @param {string} file
|
|
362
|
+
* @param {string[]} args
|
|
363
|
+
* @param {import('node:child_process').SpawnOptions} options
|
|
364
|
+
*/
|
|
365
|
+
export function spawnCommand(file, args, options) {
|
|
366
|
+
const resolved =
|
|
367
|
+
process.platform === "win32" && !/[\\/]/.test(file) ? (findOnPath(file, context()) ?? file) : file;
|
|
368
|
+
// Node no lanza .cmd/.bat sin shell (CVE-2024-27980); con shell hay que citar a mano.
|
|
369
|
+
const viaShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(resolved);
|
|
370
|
+
return viaShell
|
|
371
|
+
? spawn([resolved, ...args].map(quoteForCmd).join(" "), { ...options, shell: true })
|
|
372
|
+
: spawn(resolved, args, options);
|
|
373
|
+
}
|
|
374
|
+
|
|
298
375
|
/**
|
|
299
376
|
* Lanza el agente en el repo con el prompt por stdin; stdout/stderr van a la terminal.
|
|
300
377
|
* @param {DetectedAgent} agent
|
|
@@ -303,11 +380,7 @@ function quoteForCmd(arg) {
|
|
|
303
380
|
*/
|
|
304
381
|
export function runAgent(agent, { cwd, prompt, model, outputFilename }) {
|
|
305
382
|
const args = agent.def.args({ model, outputFilename });
|
|
306
|
-
|
|
307
|
-
const viaShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(agent.path);
|
|
308
|
-
const child = viaShell
|
|
309
|
-
? spawn([agent.path, ...args].map(quoteForCmd).join(" "), { cwd, shell: true, stdio: ["pipe", "inherit", "inherit"] })
|
|
310
|
-
: spawn(agent.path, args, { cwd, stdio: ["pipe", "inherit", "inherit"] });
|
|
383
|
+
const child = spawnCommand(agent.path, args, { cwd, stdio: ["pipe", "inherit", "inherit"] });
|
|
311
384
|
// Si el agente sale antes de leer stdin (p. ej. un modelo inválido), el EPIPE no debe tirar abajo la CLI.
|
|
312
385
|
child.stdin?.on("error", () => {});
|
|
313
386
|
child.stdin?.end(prompt);
|
package/lib/agents.test.mjs
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
detectAgents,
|
|
10
10
|
findOnPath,
|
|
11
11
|
modelChoices,
|
|
12
|
-
OTHER_MODEL,
|
|
13
12
|
topLevelTomlModel,
|
|
14
13
|
} from "./agents.mjs";
|
|
15
14
|
|
|
@@ -78,16 +77,15 @@ describe("detectAgents", () => {
|
|
|
78
77
|
});
|
|
79
78
|
|
|
80
79
|
describe("modelChoices", () => {
|
|
81
|
-
it("sin modelo configurado: 'Por defecto del agente' primero y como inicial
|
|
80
|
+
it("sin modelo configurado: 'Por defecto del agente' primero y como inicial", () => {
|
|
82
81
|
const { options, initialValue } = modelChoices(claude, { env: {}, platform: "linux", home: sandbox() });
|
|
83
82
|
assert.equal(initialValue, AGENT_DEFAULT_MODEL);
|
|
84
83
|
assert.equal(options[0].value, AGENT_DEFAULT_MODEL);
|
|
85
84
|
assert.equal(options[0].label, "Por defecto del agente");
|
|
86
85
|
assert.deepEqual(
|
|
87
|
-
options.slice(1
|
|
86
|
+
options.slice(1).map((o) => o.value),
|
|
88
87
|
["opus", "sonnet", "haiku"]
|
|
89
88
|
);
|
|
90
|
-
assert.equal(options.at(-1).value, OTHER_MODEL);
|
|
91
89
|
});
|
|
92
90
|
|
|
93
91
|
it("muestra el modelo configurado en ~/.claude/settings.json y no lo repite en la lista", () => {
|
|
@@ -127,7 +125,7 @@ describe("args", () => {
|
|
|
127
125
|
it("claude: -p, --model solo si se eligió uno, y escritura limitada al JSON del review", () => {
|
|
128
126
|
const args = claude.args({ model: "opus", outputFilename: "diff-rev_a_b.json" });
|
|
129
127
|
assert.deepEqual(args.slice(0, 3), ["-p", "--model", "opus"]);
|
|
130
|
-
assert.ok(args.includes("Edit(
|
|
128
|
+
assert.ok(args.includes("Edit(./.diff-review/diff-rev_a_b.json)"));
|
|
131
129
|
assert.ok(!claude.args({ outputFilename: "x.json" }).includes("--model"));
|
|
132
130
|
});
|
|
133
131
|
|
|
@@ -189,7 +187,7 @@ describe("copilot", () => {
|
|
|
189
187
|
const args = copilot.args({ model: "auto", outputFilename: "diff-rev_a_b.json" });
|
|
190
188
|
assert.ok(!args.includes("-p"));
|
|
191
189
|
assert.deepEqual(args.slice(0, 3), ["-s", "--model", "auto"]);
|
|
192
|
-
assert.ok(args.includes("write(diff-rev_a_b.json)"));
|
|
190
|
+
assert.ok(args.includes("write(.diff-review/diff-rev_a_b.json)"));
|
|
193
191
|
assert.ok(!args.includes("--allow-all-tools"));
|
|
194
192
|
});
|
|
195
193
|
});
|
package/lib/check-report.mjs
CHANGED
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
* Valida un JSON de review y arma un texto accionable para el agente.
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { globMatch } from "./config.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { loadBranchDiff } from "./extract.mjs";
|
|
8
8
|
import { tryResolveGitRoot } from "./git.mjs";
|
|
9
9
|
import { decodePayload } from "./json-payload.mjs";
|
|
10
|
+
import { OUTPUT_DIR } from "./output-name.mjs";
|
|
10
11
|
import { REVIEW_SCHEMA, reviewIssues, validateAgainst } from "./validate.mjs";
|
|
11
12
|
|
|
13
|
+
/** Raíz del repo a partir del JSON (`.diff-review/foo.json` → parent, o la carpeta del archivo legacy). */
|
|
14
|
+
function repoRootFromReviewFile(absJson) {
|
|
15
|
+
const dir = dirname(absJson);
|
|
16
|
+
return basename(dir) === OUTPUT_DIR ? dirname(dir) : dir;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
const MISSING_LIMIT = 40;
|
|
13
20
|
|
|
14
21
|
/**
|
|
@@ -70,7 +77,8 @@ export function checkPayload(payload, paths = []) {
|
|
|
70
77
|
|
|
71
78
|
/**
|
|
72
79
|
* @param {string} filePath
|
|
73
|
-
* @param {{ repo?: string, branch?: string, base?: string, cwd?: string }} [opts]
|
|
80
|
+
* @param {{ repo?: string, branch?: string, base?: string, cwd?: string, paths?: string[] }} [opts]
|
|
81
|
+
* `paths`: lista ya conocida (p. ej. de la sesión); si viene, no se lee el `.diff` ni se llama a git.
|
|
74
82
|
*/
|
|
75
83
|
export function checkReportFile(filePath, opts = {}) {
|
|
76
84
|
const cwd = opts.cwd || process.cwd();
|
|
@@ -101,28 +109,38 @@ export function checkReportFile(filePath, opts = {}) {
|
|
|
101
109
|
const branch = String(opts.branch || meta.branch || "").trim();
|
|
102
110
|
const base = String(opts.base || meta.base || "develop").trim();
|
|
103
111
|
const repoHint = String(opts.repo || meta.repo || "").trim();
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
const fromHint =
|
|
113
|
+
repoHint && !/^(https?:\/\/|git@|ssh:\/\/)/i.test(repoHint)
|
|
114
|
+
? tryResolveGitRoot(repoHint) || (existsSync(repoHint) ? resolve(repoHint) : null)
|
|
115
|
+
: null;
|
|
116
|
+
const repo = fromHint || tryResolveGitRoot(cwd) || repoRootFromReviewFile(abs);
|
|
107
117
|
|
|
108
118
|
/** @type {string[]} */
|
|
109
|
-
let paths = [];
|
|
119
|
+
let paths = Array.isArray(opts.paths) ? opts.paths.map(String).filter(Boolean) : [];
|
|
110
120
|
/** @type {string[]} */
|
|
111
121
|
const errors = [];
|
|
112
122
|
|
|
113
|
-
if (
|
|
123
|
+
if (paths.length) {
|
|
124
|
+
/* cobertura ya resuelta por el caller */
|
|
125
|
+
} else if (!branch) {
|
|
114
126
|
errors.push("Falta meta.branch (o --branch) para contrastar el diff.");
|
|
115
127
|
} else if (!repo) {
|
|
116
128
|
errors.push(
|
|
117
|
-
"No encontré un repo
|
|
129
|
+
"No encontré un repo local (meta.repo, el directorio actual, o la carpeta del JSON). Sin eso no puedo verificar cobertura."
|
|
118
130
|
);
|
|
119
131
|
} else {
|
|
120
132
|
try {
|
|
121
|
-
|
|
133
|
+
// Preferí el companion `.diff` (mismo stem que el JSON); solo cae a `git diff` si falta.
|
|
134
|
+
const extracted = loadBranchDiff({
|
|
135
|
+
repo,
|
|
136
|
+
branch,
|
|
137
|
+
base,
|
|
138
|
+
filename: basename(abs),
|
|
139
|
+
});
|
|
122
140
|
paths = (extracted.files || []).map((f) => f.path);
|
|
123
141
|
} catch (err) {
|
|
124
142
|
errors.push(
|
|
125
|
-
`No pude listar el diff
|
|
143
|
+
`No pude listar el diff (${err instanceof Error ? err.message : String(err)}). Revisá meta.repo, meta.branch y meta.base, o el companion .diff.`
|
|
126
144
|
);
|
|
127
145
|
}
|
|
128
146
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, test } from "node:test";
|
|
6
|
+
import { extractBranchDiff, parseGitDiffFiles } from "./extract.mjs";
|
|
7
|
+
import { OUTPUT_DIR } from "./output-name.mjs";
|
|
8
|
+
import { companionPaths, ensureReviewDir, writeDiffText } from "./review-dir.mjs";
|
|
9
|
+
import { checkReportFile } from "./check-report.mjs";
|
|
10
|
+
|
|
11
|
+
const sample = [
|
|
12
|
+
"diff --git a/src/a.ts b/src/a.ts",
|
|
13
|
+
"index 111..222 100644",
|
|
14
|
+
"--- a/src/a.ts",
|
|
15
|
+
"+++ b/src/a.ts",
|
|
16
|
+
"@@ -1,2 +1,3 @@",
|
|
17
|
+
" keep",
|
|
18
|
+
"-old",
|
|
19
|
+
"+new",
|
|
20
|
+
"+extra",
|
|
21
|
+
"diff --git a/package-lock.json b/package-lock.json",
|
|
22
|
+
"index 1..2 100644",
|
|
23
|
+
"--- a/package-lock.json",
|
|
24
|
+
"+++ b/package-lock.json",
|
|
25
|
+
"@@ -1 +1 @@",
|
|
26
|
+
"-{}",
|
|
27
|
+
"+{ }",
|
|
28
|
+
"diff --git a/old.ts b/new.ts",
|
|
29
|
+
"similarity index 90%",
|
|
30
|
+
"rename from old.ts",
|
|
31
|
+
"rename to new.ts",
|
|
32
|
+
"index 1..2 100644",
|
|
33
|
+
"--- a/old.ts",
|
|
34
|
+
"+++ b/new.ts",
|
|
35
|
+
"@@ -1 +1 @@",
|
|
36
|
+
"-x",
|
|
37
|
+
"+y",
|
|
38
|
+
].join("\n");
|
|
39
|
+
|
|
40
|
+
const dirs = [];
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
for (const d of dirs.splice(0)) {
|
|
43
|
+
try {
|
|
44
|
+
rmSync(d, { recursive: true, force: true });
|
|
45
|
+
} catch {
|
|
46
|
+
/* ignore */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("parseGitDiffFiles splits paths and change types", () => {
|
|
52
|
+
const files = parseGitDiffFiles(sample);
|
|
53
|
+
assert.equal(files.length, 3);
|
|
54
|
+
assert.equal(files[0].path, "src/a.ts");
|
|
55
|
+
assert.equal(files[0].changeType, "modified");
|
|
56
|
+
assert.match(files[0].hunkDiff, /^@@/);
|
|
57
|
+
assert.equal(files[1].path, "package-lock.json");
|
|
58
|
+
assert.equal(files[2].changeType, "renamed");
|
|
59
|
+
assert.equal(files[2].path, "new.ts");
|
|
60
|
+
assert.equal(files[2].oldPath, "old.ts");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("extractBranchDiff with cachedDiff applies DEFAULT_IGNORE", () => {
|
|
64
|
+
const repo = mkdtempSync(join(tmpdir(), "diff-review-cache-"));
|
|
65
|
+
dirs.push(repo);
|
|
66
|
+
mkdirSync(join(repo, "src"), { recursive: true });
|
|
67
|
+
// repo path only used for config/guidelines; no git needed when cachedDiff is set
|
|
68
|
+
const skeleton = extractBranchDiff({
|
|
69
|
+
repo,
|
|
70
|
+
branch: "feature",
|
|
71
|
+
base: "develop",
|
|
72
|
+
cachedDiff: sample,
|
|
73
|
+
});
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
skeleton.files.map((f) => f.path).sort(),
|
|
76
|
+
["new.ts", "src/a.ts"]
|
|
77
|
+
);
|
|
78
|
+
assert.ok(skeleton.context.ignored.includes("package-lock.json"));
|
|
79
|
+
assert.match(skeleton.files.find((f) => f.path === "src/a.ts").diff, /^@@/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("checkReportFile reuses companion .diff paths without inventing coverage", () => {
|
|
83
|
+
const repo = mkdtempSync(join(tmpdir(), "diff-review-check-"));
|
|
84
|
+
dirs.push(repo);
|
|
85
|
+
ensureReviewDir(repo);
|
|
86
|
+
const filename = "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json";
|
|
87
|
+
writeDiffText(repo, filename, sample);
|
|
88
|
+
const payload = {
|
|
89
|
+
meta: {
|
|
90
|
+
agent: "Cursor",
|
|
91
|
+
model: "composer",
|
|
92
|
+
repo,
|
|
93
|
+
branch: "feature",
|
|
94
|
+
base: "develop",
|
|
95
|
+
},
|
|
96
|
+
intent: "Cambia src/a.ts y renombra old.ts para dejar el módulo listo en la rama.",
|
|
97
|
+
groups: [{ id: "g1", kind: "feat", title: "Ajuste", intent: "base" }],
|
|
98
|
+
blocks: [
|
|
99
|
+
{
|
|
100
|
+
id: "b1",
|
|
101
|
+
group: "g1",
|
|
102
|
+
file: "src/a.ts",
|
|
103
|
+
lines: "L1-3",
|
|
104
|
+
op: "mod",
|
|
105
|
+
what: "cambia la lógica",
|
|
106
|
+
why: "hace falta",
|
|
107
|
+
source: "code",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "b2",
|
|
111
|
+
group: "g1",
|
|
112
|
+
file: "new.ts",
|
|
113
|
+
lines: "L1",
|
|
114
|
+
op: "rename",
|
|
115
|
+
what: "renombra el módulo",
|
|
116
|
+
why: "nombre más claro",
|
|
117
|
+
source: "code",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
findings: [],
|
|
121
|
+
skipped: [{ file: "package-lock.json", reason: "lockfile" }],
|
|
122
|
+
};
|
|
123
|
+
const jsonPath = companionPaths(repo, filename).json;
|
|
124
|
+
writeFileSync(jsonPath, JSON.stringify(payload));
|
|
125
|
+
// Sin git: loadBranchDiff lee el .diff y aplica ignore (lockfile fuera de paths).
|
|
126
|
+
const result = checkReportFile(jsonPath, { repo, branch: "feature", base: "develop" });
|
|
127
|
+
assert.equal(result.ok, true, result.errors.join("; "));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("buildPrompt with precomputed context skips merge-base rediscovery", async () => {
|
|
131
|
+
const { buildPrompt } = await import("./prompt.mjs");
|
|
132
|
+
const text = buildPrompt(
|
|
133
|
+
{ source: "local", repo: "/tmp/r", branch: "feat", base: "develop" },
|
|
134
|
+
{
|
|
135
|
+
outputFilename: "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json",
|
|
136
|
+
diffRelPath: ".diff-review/diff-rev_aaaaaaaaaa_bbbbbbbbbb.diff",
|
|
137
|
+
mergeBase: "abc123",
|
|
138
|
+
branchSha: "def456",
|
|
139
|
+
baseSha: "789abc",
|
|
140
|
+
files: [{ path: "src/a.ts", changeType: "modified" }],
|
|
141
|
+
ignored: ["package-lock.json"],
|
|
142
|
+
ignoreGlobs: ["**/package-lock.json"],
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
assert.match(text, /Precomputed git context/);
|
|
146
|
+
assert.match(text, /diff-rev_aaaaaaaaaa_bbbbbbbbbb\.diff/);
|
|
147
|
+
assert.match(text, /Merge-base SHA: `abc123`/);
|
|
148
|
+
assert.match(text, /modified: src\/a\.ts/);
|
|
149
|
+
assert.doesNotMatch(text, /Resolve the merge-base/);
|
|
150
|
+
assert.doesNotMatch(text, /Run exactly: `git diff/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("buildPrompt acp omits validate CLI", async () => {
|
|
154
|
+
const { buildPrompt, validateCliCommand } = await import("./prompt.mjs");
|
|
155
|
+
const filename = "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json";
|
|
156
|
+
const text = buildPrompt(
|
|
157
|
+
{ source: "local", repo: "/tmp/r", branch: "feat", base: "develop" },
|
|
158
|
+
{ outputFilename: filename, acp: true, diffRelPath: `.${OUTPUT_DIR}/${filename.replace(/\.json$/, ".diff")}` }
|
|
159
|
+
);
|
|
160
|
+
assert.doesNotMatch(text, new RegExp(validateCliCommand(filename).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
161
|
+
assert.match(text, /host validates/i);
|
|
162
|
+
});
|
package/lib/extract.mjs
CHANGED
|
@@ -2,14 +2,195 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { capDiff, collectGuidelines, isIgnored, isTestPath, loadReviewConfig } from "./config.mjs";
|
|
4
4
|
import { commitLog, git } from "./git.mjs";
|
|
5
|
+
import { readDiffCache } from "./review-dir.mjs";
|
|
5
6
|
|
|
6
7
|
export function flattenFiles(report) {
|
|
7
8
|
return Array.isArray(report?.files) ? report.files : [];
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Parte un `git diff` multi-archivo (texto de `writeDiffCache`) en entradas por path.
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @returns {Array<{ path: string, oldPath?: string, changeType: string, hunkDiff: string }>}
|
|
15
|
+
*/
|
|
16
|
+
export function parseGitDiffFiles(text) {
|
|
17
|
+
const raw = String(text || "").replace(/\r\n/g, "\n");
|
|
18
|
+
if (!raw.trim()) return [];
|
|
19
|
+
|
|
20
|
+
const parts = raw.split(/^diff --git /m);
|
|
21
|
+
/** @type {Array<{ path: string, oldPath?: string, changeType: string, hunkDiff: string }>} */
|
|
22
|
+
const out = [];
|
|
23
|
+
|
|
24
|
+
for (const part of parts) {
|
|
25
|
+
const chunk = part.trimStart();
|
|
26
|
+
if (!chunk) continue;
|
|
27
|
+
|
|
28
|
+
const firstNl = chunk.indexOf("\n");
|
|
29
|
+
const firstLine = (firstNl === -1 ? chunk : chunk.slice(0, firstNl)).trim();
|
|
30
|
+
// `a/path b/path` (paths sin espacios raros; el formato de git suele ser este)
|
|
31
|
+
const paths = firstLine.match(/^a\/(.+) b\/(.+)$/);
|
|
32
|
+
if (!paths) continue;
|
|
33
|
+
|
|
34
|
+
let oldPath = paths[1];
|
|
35
|
+
let path = paths[2];
|
|
36
|
+
const body = firstNl === -1 ? "" : chunk.slice(firstNl + 1);
|
|
37
|
+
|
|
38
|
+
let changeType = "modified";
|
|
39
|
+
if (/^new file mode /m.test(body) || /^--- \/dev\/null/m.test(body)) changeType = "added";
|
|
40
|
+
else if (/^deleted file mode /m.test(body) || /^\+\+\+ \/dev\/null/m.test(body)) changeType = "deleted";
|
|
41
|
+
else if (/^rename from /m.test(body) || /^rename to /m.test(body)) {
|
|
42
|
+
changeType = "renamed";
|
|
43
|
+
const from = body.match(/^rename from (.+)$/m);
|
|
44
|
+
const to = body.match(/^rename to (.+)$/m);
|
|
45
|
+
if (from) oldPath = from[1].trim();
|
|
46
|
+
if (to) path = to[1].trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let hunkDiff = "";
|
|
50
|
+
const idx = `\n${body}`.indexOf("\n@@");
|
|
51
|
+
if (idx !== -1) hunkDiff = `\n${body}`.slice(idx + 1);
|
|
52
|
+
else if (/^Binary files /m.test(body)) hunkDiff = "";
|
|
53
|
+
|
|
54
|
+
out.push({
|
|
55
|
+
path,
|
|
56
|
+
oldPath: oldPath !== path ? oldPath : undefined,
|
|
57
|
+
changeType,
|
|
58
|
+
hunkDiff,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stats baratos desde el texto del diff (sin volver a llamar a git).
|
|
67
|
+
* @param {string} text
|
|
68
|
+
*/
|
|
69
|
+
function statsFromDiffText(text) {
|
|
70
|
+
let insertions = 0;
|
|
71
|
+
let deletions = 0;
|
|
72
|
+
for (const line of String(text || "").split("\n")) {
|
|
73
|
+
if (!line.length) continue;
|
|
74
|
+
if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff ") || line.startsWith("index ")) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (line.startsWith("+")) insertions += 1;
|
|
78
|
+
else if (line.startsWith("-")) deletions += 1;
|
|
79
|
+
}
|
|
80
|
+
return { insertions, deletions };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Tips / merge-base si git responde; si no, strings vacíos (el contenido del diff ya viene del cache).
|
|
85
|
+
*/
|
|
86
|
+
function tryTips(repo, branch, base) {
|
|
87
|
+
try {
|
|
88
|
+
return {
|
|
89
|
+
mergeBase: git(repo, ["merge-base", base, branch]).trim(),
|
|
90
|
+
branchSha: git(repo, ["rev-parse", branch]).trim(),
|
|
91
|
+
baseSha: git(repo, ["rev-parse", base]).trim(),
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
return { mergeBase: "", branchSha: "", baseSha: "" };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Arma el mismo shape que `extractBranchDiff` a partir de un texto de `git diff` ya cacheado.
|
|
100
|
+
* No vuelve a correr `git diff`.
|
|
101
|
+
*/
|
|
102
|
+
function skeletonFromCachedDiff(repo, branch, base, cachedDiff) {
|
|
103
|
+
const config = loadReviewConfig(repo);
|
|
104
|
+
const tips = tryTips(repo, branch, base);
|
|
105
|
+
const parsed = parseGitDiffFiles(cachedDiff);
|
|
106
|
+
|
|
107
|
+
const ignored = [];
|
|
108
|
+
const included = [];
|
|
109
|
+
for (const f of parsed) {
|
|
110
|
+
if (isIgnored(f.path, config.ignore)) ignored.push(f.path);
|
|
111
|
+
else included.push(f);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const files = included.map((f) => {
|
|
115
|
+
const capped = capDiff(f.hunkDiff);
|
|
116
|
+
return {
|
|
117
|
+
path: f.path,
|
|
118
|
+
oldPath: f.oldPath || undefined,
|
|
119
|
+
changeType: f.changeType,
|
|
120
|
+
diff: capped.diff,
|
|
121
|
+
diffChars: capped.diffChars,
|
|
122
|
+
truncated: capped.truncated,
|
|
123
|
+
modelChars: capped.diff.length,
|
|
124
|
+
group: "",
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const { insertions, deletions } = statsFromDiffText(cachedDiff);
|
|
129
|
+
let commits = [];
|
|
130
|
+
if (tips.mergeBase) {
|
|
131
|
+
try {
|
|
132
|
+
commits = commitLog(repo, tips.mergeBase, branch);
|
|
133
|
+
} catch {
|
|
134
|
+
commits = [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
version: 2,
|
|
140
|
+
repo,
|
|
141
|
+
branch,
|
|
142
|
+
baseBranch: base,
|
|
143
|
+
mergeBase: tips.mergeBase,
|
|
144
|
+
branchSha: tips.branchSha,
|
|
145
|
+
baseSha: tips.baseSha,
|
|
146
|
+
generatedAt: new Date().toISOString(),
|
|
147
|
+
intent: "",
|
|
148
|
+
stats: {
|
|
149
|
+
filesChanged: files.length,
|
|
150
|
+
filesIgnored: ignored.length,
|
|
151
|
+
filesTruncated: files.filter((f) => f.truncated).length,
|
|
152
|
+
insertions,
|
|
153
|
+
deletions,
|
|
154
|
+
},
|
|
155
|
+
context: {
|
|
156
|
+
commits,
|
|
157
|
+
rules: config.rules,
|
|
158
|
+
rulesFile: config.rulesFile,
|
|
159
|
+
ignored: ignored.slice(0, 40),
|
|
160
|
+
testsTouched: files.filter((f) => isTestPath(f.path)).map((f) => f.path),
|
|
161
|
+
testsMissing: files.filter((f) => !isTestPath(f.path)).length,
|
|
162
|
+
guidelines: collectGuidelines(repo, files.map((f) => f.path), {
|
|
163
|
+
rulesFile: config.rulesFile,
|
|
164
|
+
skipNames: config.rules.trim() ? [config.rulesFile] : [],
|
|
165
|
+
maxDepth: 1,
|
|
166
|
+
maxChars: 8_000,
|
|
167
|
+
maxTotalChars: 12_000,
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
files,
|
|
171
|
+
groups: [],
|
|
172
|
+
blocks: [],
|
|
173
|
+
findings: [],
|
|
174
|
+
skipped: [],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {{
|
|
180
|
+
* repo: string,
|
|
181
|
+
* branch: string,
|
|
182
|
+
* base?: string,
|
|
183
|
+
* cachedDiff?: string | null,
|
|
184
|
+
* }} opts
|
|
185
|
+
* `cachedDiff`: texto de `git diff base...branch` ya materializado; si viene, no se vuelve a calcular el diff.
|
|
186
|
+
*/
|
|
187
|
+
export function extractBranchDiff({ repo, branch, base = "develop", cachedDiff = null } = {}) {
|
|
11
188
|
if (!branch) throw new Error("Falta el branch a revisar.");
|
|
12
189
|
|
|
190
|
+
if (typeof cachedDiff === "string" && cachedDiff.length) {
|
|
191
|
+
return skeletonFromCachedDiff(repo, branch, base, cachedDiff);
|
|
192
|
+
}
|
|
193
|
+
|
|
13
194
|
const config = loadReviewConfig(repo);
|
|
14
195
|
const mergeBase = git(repo, ["merge-base", base, branch]).trim();
|
|
15
196
|
const branchSha = git(repo, ["rev-parse", branch]).trim();
|
|
@@ -103,6 +284,21 @@ export function extractBranchDiff({ repo, branch, base = "develop" }) {
|
|
|
103
284
|
};
|
|
104
285
|
}
|
|
105
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Preferí el companion `.diff` en `.diff-review/` si existe; si no, `git diff` vía `extractBranchDiff`.
|
|
289
|
+
* @param {{ repo: string, branch: string, base?: string, filename?: string }} opts
|
|
290
|
+
*/
|
|
291
|
+
export function loadBranchDiff({ repo, branch, base = "develop", filename } = {}) {
|
|
292
|
+
const name = String(filename || "").trim();
|
|
293
|
+
if (name) {
|
|
294
|
+
const cached = readDiffCache(repo, name);
|
|
295
|
+
if (cached != null && cached.length) {
|
|
296
|
+
return extractBranchDiff({ repo, branch, base, cachedDiff: cached });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return extractBranchDiff({ repo, branch, base });
|
|
300
|
+
}
|
|
301
|
+
|
|
106
302
|
export function writeSkeleton(outPath, skeleton) {
|
|
107
303
|
const resolved = resolve(outPath);
|
|
108
304
|
mkdirSync(dirname(resolved), { recursive: true });
|