@jpkme/diff-review 0.1.12 → 0.1.13
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 +1 -1
- package/lib/acp-policy.mjs +99 -12
- package/lib/acp-policy.test.mjs +56 -3
- package/lib/acp-shell.mjs +38 -8
- package/lib/acp-shell.test.mjs +32 -1
- package/lib/agent-login.mjs +105 -105
- package/lib/check-report.mjs +1 -0
- package/lib/check-report.test.mjs +1 -0
- package/lib/extract-cache.test.mjs +162 -162
- package/lib/model-label.mjs +263 -263
- package/lib/model-label.test.mjs +77 -77
- package/lib/models-cache.mjs +75 -75
- package/lib/models-cache.test.mjs +50 -50
- package/lib/review-prep.mjs +63 -63
- package/package.json +1 -1
- package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css → 0.BKRG1f36.css} +1 -1
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
- package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css.gz → 0.BKRG1f36.css.gz} +0 -0
- package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css +1 -0
- package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css +1 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/BXUWMim4.js +213 -0
- package/web/build/client/_app/immutable/chunks/BXUWMim4.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/BXUWMim4.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{CPyWH418.js → Bu1mm6Oz.js} +1 -1
- package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{B5ByWcPE.js → DPfXzQnm.js} +2 -2
- package/web/build/client/_app/immutable/chunks/DPfXzQnm.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DPfXzQnm.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/{app.BiBohpyO.js → app.CTgiuiU1.js} +2 -2
- package/web/build/client/_app/immutable/entry/app.CTgiuiU1.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.CTgiuiU1.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CFSl0kht.js +1 -0
- package/web/build/client/_app/immutable/entry/start.CFSl0kht.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.CFSl0kht.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/{1.BzPXaJJj.js → 1.0PQZpSbJ.js} +1 -1
- package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js +10 -0
- package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.AciugOtL.js +171 -0
- package/web/build/client/_app/immutable/nodes/3.AciugOtL.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.AciugOtL.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-BYok4D0n.js → 0-DUStDOsr.js} +3 -3
- package/web/build/server/chunks/{0-BYok4D0n.js.map → 0-DUStDOsr.js.map} +1 -1
- package/web/build/server/chunks/1-C2qlInYk.js +7 -0
- package/web/build/server/chunks/{1-C7zcB0q0.js.map → 1-C2qlInYk.js.map} +1 -1
- package/web/build/server/chunks/2-DyE177MK.js +7 -0
- package/web/build/server/chunks/2-DyE177MK.js.map +1 -0
- package/web/build/server/chunks/3-SO3atuWO.js +7 -0
- package/web/build/server/chunks/{3-DAAmG6VX.js.map → 3-SO3atuWO.js.map} +1 -1
- package/web/build/server/chunks/{_server.ts-qYVrMYPB.js → _server.ts-CRNhzL2a.js} +3 -3
- package/web/build/server/chunks/{_server.ts-qYVrMYPB.js.map → _server.ts-CRNhzL2a.js.map} +1 -1
- package/web/build/server/chunks/{agent-sessions-DSqGavSn.js → agent-sessions-kcUaLhK5.js} +171 -40
- package/web/build/server/chunks/agent-sessions-kcUaLhK5.js.map +1 -0
- package/web/build/server/chunks/{git-review-CpO_G8z8.js → git-review-CXKcjYgZ.js} +2 -2
- package/web/build/server/chunks/{git-review-CpO_G8z8.js.map → git-review-CXKcjYgZ.js.map} +1 -1
- package/web/build/server/chunks/{handler-Di1wJQuU.js → handler-_E0NucKN.js} +3 -3
- package/web/build/server/chunks/{handler-Di1wJQuU.js.map → handler-_E0NucKN.js.map} +1 -1
- package/web/build/server/chunks/{hooks.server-BLOHobEK.js → hooks.server-D7yPODoc.js} +2 -2
- package/web/build/server/chunks/{hooks.server-BLOHobEK.js.map → hooks.server-D7yPODoc.js.map} +1 -1
- package/web/build/server/chunks/{index.js-yLYmrAWy.js → index.js-lMtDM8ZQ.js} +3 -3
- package/web/build/server/chunks/{index.js-yLYmrAWy.js.map → index.js-lMtDM8ZQ.js.map} +1 -1
- package/web/build/server/chunks/{manifest.js-CXj8IIo3.js → manifest.js-BIm6jot5.js} +9 -9
- package/web/build/server/chunks/{manifest.js-CXj8IIo3.js.map → manifest.js-BIm6jot5.js.map} +1 -1
- package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js → remote-1g4ayag-DmqxwUIz.js} +3 -3
- package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js.map → remote-1g4ayag-DmqxwUIz.js.map} +1 -1
- package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js → remote-wh6t6j-DZk0W9Pp.js} +4 -4
- package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js.map → remote-wh6t6j-DZk0W9Pp.js.map} +1 -1
- package/web/build/client/_app/immutable/assets/0.CLQiq3Ed.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +0 -1
- 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 +0 -1
- 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 +0 -1
- 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/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.br +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +0 -210
- 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/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 +0 -1
- 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/1.BzPXaJJj.js.br +0 -2
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +0 -10
- 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 +0 -171
- 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/server/chunks/1-C7zcB0q0.js +0 -7
- package/web/build/server/chunks/2-v6ejI_0-.js +0 -7
- package/web/build/server/chunks/2-v6ejI_0-.js.map +0 -1
- package/web/build/server/chunks/3-DAAmG6VX.js +0 -7
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +0 -1
- /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js → 0.Dj97JW46.js} +0 -0
- /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.br → 0.Dj97JW46.js.br} +0 -0
- /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.gz → 0.Dj97JW46.js.gz} +0 -0
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ va por el adaptador `@agentclientprotocol/claude-agent-acp` (se baja con `npx` l
|
|
|
71
71
|
|
|
72
72
|
- El select de modelo muestra los que ofrece cada agente (se consultan por ACP la primera vez).
|
|
73
73
|
- La web muestra lo que va haciendo (comandos, archivos que lee, plan). La terminal solo avisa fallos duros o JSON válido.
|
|
74
|
-
- Al terminar cada turno, el server valida el JSON; si falta algo se lo pide al agente
|
|
74
|
+
- Al terminar cada turno, el server valida el JSON; si falta algo se lo pide al agente hasta dos veces y, cuando valida,
|
|
75
75
|
la web abre el reporte.
|
|
76
76
|
- Cada pedido de permiso pasa por una política fija: leer dentro del repo, escribir únicamente el JSON del review
|
|
77
77
|
y comandos de solo lectura: `git` (diff, log, show, blame…), `cat`, `head`, `tail`, `grep`, `rg`, `find`, `ls`,
|
package/lib/acp-policy.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Qué puede hacer el agente cuando corre por ACP. Cada `session/request_permission` pasa por acá: no hay nadie
|
|
3
3
|
* mirando la terminal para aprobar, así que la política es una allowlist (leer, comandos de solo lectura y escribir
|
|
4
|
-
*
|
|
4
|
+
* el JSON de salida en `.diff-review/`) y todo lo demás se rechaza.
|
|
5
5
|
*
|
|
6
6
|
* Ojo: el agente solo pide permiso para lo que *su* configuración no aprueba sola (p. ej. Claude Code aplica la
|
|
7
7
|
* allowlist de `~/.claude/settings.json` antes de preguntar). Esto acota lo que pide, no lo que el usuario ya
|
|
8
8
|
* habilitó en su agente.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { realpathSync } from "node:fs";
|
|
11
|
+
import { basename, dirname, isAbsolute, normalize, resolve } from "node:path";
|
|
11
12
|
import { checkShell } from "./acp-shell.mjs";
|
|
12
|
-
import {
|
|
13
|
+
import { outputRelPath } from "./output-name.mjs";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @typedef {"review" | "ask" | "edit"} PolicyMode
|
|
@@ -32,10 +33,34 @@ function samePath(a, b, platform = process.platform) {
|
|
|
32
33
|
return platform === "win32" ? na.toLowerCase() === nb.toLowerCase() : na === nb;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Resuelve symlinks cuando el path (o un ancestro) existe. Sin eso, un link dentro del repo
|
|
38
|
+
* puede apuntar fuera y pasar `isInside` con solo `resolve`.
|
|
39
|
+
* @param {string} path
|
|
40
|
+
*/
|
|
41
|
+
function canonicalPath(path) {
|
|
42
|
+
try {
|
|
43
|
+
return realpathSync(path);
|
|
44
|
+
} catch {
|
|
45
|
+
const parts = [];
|
|
46
|
+
let cur = normalize(path);
|
|
47
|
+
for (;;) {
|
|
48
|
+
const parent = dirname(cur);
|
|
49
|
+
if (!parent || parent === cur) return normalize(path);
|
|
50
|
+
parts.unshift(basename(cur));
|
|
51
|
+
try {
|
|
52
|
+
return resolve(realpathSync(parent), ...parts);
|
|
53
|
+
} catch {
|
|
54
|
+
cur = parent;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
/** @param {string} child @param {string} root */
|
|
36
61
|
function isInside(child, root, platform = process.platform) {
|
|
37
|
-
const c = normalize(child);
|
|
38
|
-
const r = normalize(root).replace(/[\\/]+$/, "");
|
|
62
|
+
const c = normalize(canonicalPath(child));
|
|
63
|
+
const r = normalize(canonicalPath(root)).replace(/[\\/]+$/, "");
|
|
39
64
|
const [cc, rr] = platform === "win32" ? [c.toLowerCase(), r.toLowerCase()] : [c, r];
|
|
40
65
|
return cc === rr || cc.startsWith(`${rr}\\`) || cc.startsWith(`${rr}/`);
|
|
41
66
|
}
|
|
@@ -68,6 +93,66 @@ function commandOf(rawInput) {
|
|
|
68
93
|
return typeof cmd === "string" ? cmd : "";
|
|
69
94
|
}
|
|
70
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Comando de shell que la política evalúa (rawInput, backticks o título entero).
|
|
98
|
+
* Cursor a veces envuelve el script con `@…@` en el título.
|
|
99
|
+
* @param {ToolCallLike} toolCall
|
|
100
|
+
*/
|
|
101
|
+
export function toolCallCommand(toolCall) {
|
|
102
|
+
const fromRaw = commandOf(toolCall.rawInput).trim();
|
|
103
|
+
if (fromRaw) return fromRaw;
|
|
104
|
+
const title = String(toolCall.title ?? "").trim();
|
|
105
|
+
const tick = /^`([\s\S]+)`$/.exec(title);
|
|
106
|
+
if (tick) return tick[1].trim();
|
|
107
|
+
const at = /^@\s*([\s\S]*?)\s*@$/.exec(title);
|
|
108
|
+
if (at) return at[1].trim();
|
|
109
|
+
return title;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Etiqueta corta del agente si el título no es el comando mismo.
|
|
114
|
+
* @param {ToolCallLike} toolCall
|
|
115
|
+
* @param {string} [command]
|
|
116
|
+
*/
|
|
117
|
+
export function toolCallLabel(toolCall, command = toolCallCommand(toolCall)) {
|
|
118
|
+
const title = String(toolCall.title ?? "").trim();
|
|
119
|
+
if (!title) return "";
|
|
120
|
+
if (/^`[\s\S]+`$/.test(title) || /^@\s*[\s\S]*?\s*@$/.test(title)) return "";
|
|
121
|
+
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
122
|
+
if (command && norm(title) === norm(command)) return "";
|
|
123
|
+
if (command && command.length >= 24 && title.includes(command.slice(0, 24))) return "";
|
|
124
|
+
return title;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Motivo de denegación legible en la UI de permisos.
|
|
129
|
+
* @param {string} reason
|
|
130
|
+
*/
|
|
131
|
+
export function humanizePolicyReason(reason) {
|
|
132
|
+
const r = String(reason || "").trim();
|
|
133
|
+
if (!r) return "La política bloqueó este pedido.";
|
|
134
|
+
let m;
|
|
135
|
+
if ((m = /^(.+?) no está permitido$/.exec(r))) {
|
|
136
|
+
return `La política bloqueó «${m[1]}» (no está en la allowlist de comandos).`;
|
|
137
|
+
}
|
|
138
|
+
if ((m = /^variables de entorno en el comando:\s*(.*)$/s.exec(r))) {
|
|
139
|
+
const token = String(m[1] || "?").trim().slice(0, 40);
|
|
140
|
+
return `La política interpretó parte del comando como variable de entorno (${token}) y lo bloqueó.`;
|
|
141
|
+
}
|
|
142
|
+
if ((m = /^path fuera del repo:\s*(.*)$/s.exec(r))) {
|
|
143
|
+
return `El path queda fuera del repo: ${m[1]}`;
|
|
144
|
+
}
|
|
145
|
+
if ((m = /^lectura fuera del repo:\s*(.*)$/s.exec(r))) {
|
|
146
|
+
return `Lectura fuera del repo: ${m[1]}`;
|
|
147
|
+
}
|
|
148
|
+
if ((m = /^opción global de git no permitida:\s*(.*)$/s.exec(r))) {
|
|
149
|
+
return `Opción de git no permitida: ${m[1]}`;
|
|
150
|
+
}
|
|
151
|
+
if (r.startsWith("git ")) return `Comando git no permitido por la política: ${r}`;
|
|
152
|
+
if (r.startsWith("solo se puede escribir") || r.includes("sin path") || r.startsWith("modo pregunta")) return r;
|
|
153
|
+
return `La política bloqueó el pedido: ${r}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
71
156
|
/**
|
|
72
157
|
* ¿El comando de shell solo lee? Ver `checkShell` en `acp-shell.mjs`.
|
|
73
158
|
* @param {string} command
|
|
@@ -87,15 +172,17 @@ export function decidePermission(toolCall, { repo, outputFilename, mode }) {
|
|
|
87
172
|
const paths = toolCallPaths(toolCall, repo);
|
|
88
173
|
|
|
89
174
|
if (kind === "read" || kind === "search") {
|
|
175
|
+
if (!paths.length) {
|
|
176
|
+
return { allow: false, reason: `${kind === "search" ? "búsqueda" : "lectura"} sin path` };
|
|
177
|
+
}
|
|
90
178
|
const outside = paths.find((p) => !isInside(p, repo));
|
|
91
179
|
return outside ? { allow: false, reason: `lectura fuera del repo: ${outside}` } : { allow: true, reason: "lectura" };
|
|
92
180
|
}
|
|
93
181
|
if (kind === "think") return { allow: true, reason: "pensar" };
|
|
94
182
|
|
|
95
183
|
if (kind === "execute") {
|
|
96
|
-
// Cursor no manda `rawInput`: el comando viene
|
|
97
|
-
const
|
|
98
|
-
const cmd = commandOf(toolCall.rawInput) || (/^`([\s\S]+)`$/.exec(title)?.[1] ?? title);
|
|
184
|
+
// Cursor no manda `rawInput`: el comando viene en el título (backticks / @…@ / texto).
|
|
185
|
+
const cmd = toolCallCommand(toolCall);
|
|
99
186
|
const check = checkShell(cmd, { repo });
|
|
100
187
|
return check.ok ? { allow: true, reason: "comando de solo lectura" } : { allow: false, reason: check.reason };
|
|
101
188
|
}
|
|
@@ -103,14 +190,14 @@ export function decidePermission(toolCall, { repo, outputFilename, mode }) {
|
|
|
103
190
|
if (kind === "edit" || kind === "delete") {
|
|
104
191
|
if (mode === "ask") return { allow: false, reason: "modo pregunta: no se edita nada" };
|
|
105
192
|
if (!paths.length) return { allow: false, reason: `${kind === "delete" ? "borrado" : "edición"} sin path` };
|
|
106
|
-
const
|
|
107
|
-
const outside = paths.find((p) => !
|
|
193
|
+
const target = resolve(repo, outputRelPath(outputFilename));
|
|
194
|
+
const outside = paths.find((p) => !samePath(canonicalPath(p), canonicalPath(target)));
|
|
108
195
|
return outside
|
|
109
196
|
? {
|
|
110
197
|
allow: false,
|
|
111
|
-
reason: `solo se puede escribir
|
|
198
|
+
reason: `solo se puede escribir ${outputRelPath(outputFilename)} (no ${outside})`
|
|
112
199
|
}
|
|
113
|
-
: { allow: true, reason: `escritura
|
|
200
|
+
: { allow: true, reason: `escritura de ${outputRelPath(outputFilename)}` };
|
|
114
201
|
}
|
|
115
202
|
|
|
116
203
|
return { allow: false, reason: `${kind} no permitido: ${toolCall.title ?? "?"}` };
|
package/lib/acp-policy.test.mjs
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
3
5
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
decidePermission,
|
|
8
|
+
humanizePolicyReason,
|
|
9
|
+
permissionOutcome,
|
|
10
|
+
toolCallCommand,
|
|
11
|
+
toolCallLabel,
|
|
12
|
+
} from "./acp-policy.mjs";
|
|
5
13
|
|
|
6
14
|
const repo = process.platform === "win32" ? "C:\\repos\\app" : "/repos/app";
|
|
7
15
|
const out = "diff-rev_75eca0c739_52eb729c8f.json";
|
|
@@ -19,12 +27,43 @@ test("decidePermission: lecturas solo dentro del repo", () => {
|
|
|
19
27
|
const home = process.platform === "win32" ? "C:\\Users\\me\\.ssh\\id_rsa" : "/home/me/.ssh/id_rsa";
|
|
20
28
|
assert.equal(decidePermission(read(home), ctx()).allow, false);
|
|
21
29
|
assert.equal(decidePermission(read(join(repo, "..", "otro", "x")), ctx()).allow, false);
|
|
30
|
+
assert.equal(decidePermission({ kind: "read", title: "Read", rawInput: {} }, ctx()).allow, false);
|
|
31
|
+
assert.equal(decidePermission({ kind: "search", title: "Grep", rawInput: { pattern: "x" } }, ctx()).allow, false);
|
|
32
|
+
assert.equal(decidePermission({ kind: "search", title: "Grep", rawInput: { pattern: "x", path: "lib" } }, ctx()).allow, true);
|
|
22
33
|
});
|
|
23
34
|
|
|
24
|
-
test("decidePermission:
|
|
35
|
+
test("decidePermission: symlink dentro del repo que apunta afuera no pasa", () => {
|
|
36
|
+
const root = mkdtempSync(join(tmpdir(), "dr-policy-"));
|
|
37
|
+
const outside = mkdtempSync(join(tmpdir(), "dr-policy-out-"));
|
|
38
|
+
writeFileSync(join(outside, "secret.txt"), "secret");
|
|
39
|
+
mkdirSync(join(root, "lib"), { recursive: true });
|
|
40
|
+
writeFileSync(join(root, "lib", "ok.mjs"), "ok");
|
|
41
|
+
const link = join(root, "escape");
|
|
42
|
+
try {
|
|
43
|
+
// junction en Windows no pide admin; 'dir' en posix.
|
|
44
|
+
symlinkSync(outside, link, process.platform === "win32" ? "junction" : "dir");
|
|
45
|
+
} catch (err) {
|
|
46
|
+
rmSync(root, { recursive: true, force: true });
|
|
47
|
+
rmSync(outside, { recursive: true, force: true });
|
|
48
|
+
// Entornos sin permiso de symlink: no fallar el suite.
|
|
49
|
+
if (err && (err.code === "EPERM" || err.code === "EACCES")) return;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
const local = (mode = "review") => ({ repo: root, outputFilename: out, mode });
|
|
53
|
+
try {
|
|
54
|
+
assert.equal(decidePermission(read(join(root, "lib", "ok.mjs")), local()).allow, true);
|
|
55
|
+
assert.equal(decidePermission(read(join(link, "secret.txt")), local()).allow, false);
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(root, { recursive: true, force: true });
|
|
58
|
+
rmSync(outside, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("decidePermission: escritura solo del JSON objetivo, nunca en modo pregunta ni fuera", () => {
|
|
25
63
|
assert.equal(decidePermission(edit(outPath), ctx("review")).allow, true);
|
|
26
64
|
assert.equal(decidePermission(edit(`.diff-review/${out}`), ctx("edit")).allow, true);
|
|
27
|
-
assert.equal(decidePermission(edit(join(repo, ".diff-review", "scratch.tmp")), ctx("review")).allow,
|
|
65
|
+
assert.equal(decidePermission(edit(join(repo, ".diff-review", "scratch.tmp")), ctx("review")).allow, false);
|
|
66
|
+
assert.equal(decidePermission(edit(join(repo, ".diff-review", out.replace(/\.json$/i, ".diff"))), ctx("review")).allow, false);
|
|
28
67
|
assert.equal(decidePermission(edit(outPath), ctx("ask")).allow, false);
|
|
29
68
|
// Fuera de .diff-review/: no.
|
|
30
69
|
assert.equal(decidePermission(edit(join(repo, out)), ctx("review")).allow, false);
|
|
@@ -61,6 +100,20 @@ test("decidePermission: comandos según isAllowedShell y el resto rechazado", ()
|
|
|
61
100
|
assert.equal(decidePermission({ kind: "delete", title: "rm", rawInput: {} }, ctx("edit")).allow, false);
|
|
62
101
|
});
|
|
63
102
|
|
|
103
|
+
test("toolCallCommand / toolCallLabel y humanizePolicyReason para la UI de permisos", () => {
|
|
104
|
+
assert.equal(toolCallCommand({ kind: "execute", rawInput: { command: "git status" } }), "git status");
|
|
105
|
+
assert.equal(toolCallCommand({ kind: "execute", title: "`ls -la`" }), "ls -la");
|
|
106
|
+
assert.equal(
|
|
107
|
+
toolCallCommand({ kind: "execute", title: "@\nnode -e 'x'\n@" }),
|
|
108
|
+
"node -e 'x'"
|
|
109
|
+
);
|
|
110
|
+
assert.equal(toolCallLabel({ title: "Probe symlink" }, "node -e 'x'"), "Probe symlink");
|
|
111
|
+
assert.equal(toolCallLabel({ title: "`ls`" }, "ls"), "");
|
|
112
|
+
assert.equal(toolCallLabel({ title: "@node@" }, "node"), "");
|
|
113
|
+
assert.match(humanizePolicyReason("node no está permitido"), /allowlist/);
|
|
114
|
+
assert.match(humanizePolicyReason("variables de entorno en el comando: @"), /variable de entorno/);
|
|
115
|
+
});
|
|
116
|
+
|
|
64
117
|
test("permissionOutcome elige once / always según el scope", () => {
|
|
65
118
|
const options = [
|
|
66
119
|
{ optionId: "a1", kind: "allow_always", name: "Siempre" },
|
package/lib/acp-shell.mjs
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* El comando se tokeniza respetando comillas (un `grep "a\|b"` no es un pipe). Se aceptan `|`, `&&`, `||`, `;` y
|
|
7
7
|
* descartar salida a /dev/null; cualquier otra redirección, `$(…)`, backticks o `&` lo rechazan.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { realpathSync } from "node:fs";
|
|
10
|
+
import { basename, dirname, isAbsolute, normalize, resolve } from "node:path";
|
|
10
11
|
|
|
11
12
|
/** @typedef {{ ok: true } | { ok: false, reason: string }} ShellCheck */
|
|
12
13
|
|
|
@@ -99,18 +100,48 @@ const TEXT_ARGS = new Set(["echo", "printf", "tr", "write-output", "which", "typ
|
|
|
99
100
|
/** Destinos de redirección que solo descartan salida. */
|
|
100
101
|
const DISCARD = new Set(["/dev/null", "nul", "$null"]);
|
|
101
102
|
|
|
102
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Paths de shell Unix → forma Windows cuando la política corre en win32:
|
|
105
|
+
* - Git Bash: `/c/Users/...` → `C:/Users/...`
|
|
106
|
+
* - WSL: `/mnt/c/Users/...` → `C:/Users/...`
|
|
107
|
+
*/
|
|
103
108
|
function toNativePath(p, platform) {
|
|
104
109
|
if (platform === "win32") {
|
|
110
|
+
const wsl = /^\/mnt\/([a-zA-Z])(\/|$)(.*)$/.exec(p);
|
|
111
|
+
if (wsl) return `${wsl[1].toUpperCase()}:/${wsl[3]}`;
|
|
105
112
|
const msys = /^\/([a-zA-Z])(\/|$)(.*)$/.exec(p);
|
|
106
113
|
if (msys) return `${msys[1].toUpperCase()}:/${msys[3]}`;
|
|
107
114
|
}
|
|
108
115
|
return p;
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Resuelve symlinks cuando el path (o un ancestro) existe. Sin eso, un link
|
|
120
|
+
* dentro del repo puede apuntar fuera y pasar `isInside` con solo `normalize`.
|
|
121
|
+
* @param {string} path
|
|
122
|
+
*/
|
|
123
|
+
function canonicalPath(path) {
|
|
124
|
+
try {
|
|
125
|
+
return realpathSync(path);
|
|
126
|
+
} catch {
|
|
127
|
+
const parts = [];
|
|
128
|
+
let cur = normalize(path);
|
|
129
|
+
for (;;) {
|
|
130
|
+
const parent = dirname(cur);
|
|
131
|
+
if (!parent || parent === cur) return normalize(path);
|
|
132
|
+
parts.unshift(basename(cur));
|
|
133
|
+
try {
|
|
134
|
+
return resolve(realpathSync(parent), ...parts);
|
|
135
|
+
} catch {
|
|
136
|
+
cur = parent;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
function isInside(child, root, platform) {
|
|
112
|
-
const c = normalize(child).replace(/[\\/]+$/, "");
|
|
113
|
-
const r = normalize(root).replace(/[\\/]+$/, "");
|
|
143
|
+
const c = normalize(canonicalPath(child)).replace(/[\\/]+$/, "");
|
|
144
|
+
const r = normalize(canonicalPath(root)).replace(/[\\/]+$/, "");
|
|
114
145
|
const [cc, rr] = platform === "win32" ? [c.toLowerCase(), r.toLowerCase()] : [c, r];
|
|
115
146
|
return cc === rr || cc.startsWith(`${rr}\\`) || cc.startsWith(`${rr}/`);
|
|
116
147
|
}
|
|
@@ -318,13 +349,12 @@ function isValidate(argv) {
|
|
|
318
349
|
*/
|
|
319
350
|
function checkReviewDirMkdir(argv, ctx) {
|
|
320
351
|
const name = argv[0].toLowerCase();
|
|
321
|
-
const reviewRoot =
|
|
352
|
+
const reviewRoot = resolve(ctx.repo, ".diff-review");
|
|
322
353
|
/** @param {string} arg */
|
|
323
354
|
const underReviewDir = (arg) => {
|
|
324
355
|
const native = toNativePath(String(arg || "").replace(/^~(?=[\\/]|$)/, "/__home__"), ctx.platform);
|
|
325
|
-
const abs =
|
|
326
|
-
|
|
327
|
-
return a === r || a.startsWith(`${r}\\`) || a.startsWith(`${r}/`);
|
|
356
|
+
const abs = isAbsolute(native) ? native : resolve(ctx.repo, native);
|
|
357
|
+
return isInside(abs, reviewRoot, ctx.platform);
|
|
328
358
|
};
|
|
329
359
|
|
|
330
360
|
if (name === "mkdir" || name === "md") {
|
package/lib/acp-shell.test.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
3
5
|
import { join } from "node:path";
|
|
4
6
|
import { checkShell, tokenizeShell } from "./acp-shell.mjs";
|
|
5
7
|
|
|
@@ -121,7 +123,36 @@ test("checkShell permite mkdir bajo .diff-review en el repo", () => {
|
|
|
121
123
|
assert.equal(ok("New-Item -ItemType Directory lib"), false);
|
|
122
124
|
});
|
|
123
125
|
|
|
124
|
-
test("checkShell entiende
|
|
126
|
+
test("checkShell entiende paths de Git Bash y WSL en Windows", { skip: !win }, () => {
|
|
125
127
|
assert.equal(ok("cat /c/repos/app/lib/a.mjs"), true);
|
|
126
128
|
assert.equal(ok("cat /c/Users/me/.ssh/id_rsa"), false);
|
|
129
|
+
assert.equal(ok("cd /mnt/c/repos/app && ls -la"), true);
|
|
130
|
+
assert.equal(ok("cat /mnt/c/repos/app/lib/a.mjs"), true);
|
|
131
|
+
assert.equal(ok("cd /mnt/c/Users/me && ls"), false);
|
|
132
|
+
assert.equal(ok("cat /mnt/c/Users/me/.ssh/id_rsa"), false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("checkShell rechaza symlink dentro del repo que apunta afuera", () => {
|
|
136
|
+
const root = mkdtempSync(join(tmpdir(), "dr-shell-"));
|
|
137
|
+
const outside = mkdtempSync(join(tmpdir(), "dr-shell-out-"));
|
|
138
|
+
writeFileSync(join(outside, "secret.txt"), "secret");
|
|
139
|
+
mkdirSync(join(root, "lib"), { recursive: true });
|
|
140
|
+
writeFileSync(join(root, "lib", "ok.mjs"), "ok");
|
|
141
|
+
const link = join(root, "escape");
|
|
142
|
+
try {
|
|
143
|
+
symlinkSync(outside, link, process.platform === "win32" ? "junction" : "dir");
|
|
144
|
+
} catch (err) {
|
|
145
|
+
rmSync(root, { recursive: true, force: true });
|
|
146
|
+
rmSync(outside, { recursive: true, force: true });
|
|
147
|
+
if (err && (err.code === "EPERM" || err.code === "EACCES")) return;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
assert.equal(checkShell(`cat "${join(root, "lib", "ok.mjs")}"`, { repo: root }).ok, true);
|
|
152
|
+
assert.equal(checkShell(`cat "${join(link, "secret.txt")}"`, { repo: root }).ok, false);
|
|
153
|
+
assert.equal(checkShell(`cd "${link}" && ls`, { repo: root }).ok, false);
|
|
154
|
+
} finally {
|
|
155
|
+
rmSync(root, { recursive: true, force: true });
|
|
156
|
+
rmSync(outside, { recursive: true, force: true });
|
|
157
|
+
}
|
|
127
158
|
});
|
package/lib/agent-login.mjs
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Login de agentes ACP: mensajes claros + abrir una terminal interactiva cuando se pueda.
|
|
3
|
-
*/
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @typedef {{ bin: string, args: string[], display: string }} LoginCommand
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Comando de login interactivo por agente (null = no hay CLI de login, p. ej. Cursor app).
|
|
12
|
-
* @param {string} agentId
|
|
13
|
-
* @returns {LoginCommand | null}
|
|
14
|
-
*/
|
|
15
|
-
export function loginCommand(agentId) {
|
|
16
|
-
switch (agentId) {
|
|
17
|
-
case "claude":
|
|
18
|
-
return { bin: "claude", args: ["/login"], display: "claude /login" };
|
|
19
|
-
case "copilot":
|
|
20
|
-
return { bin: "gh", args: ["auth", "login"], display: "gh auth login" };
|
|
21
|
-
case "codex":
|
|
22
|
-
return { bin: "codex", args: ["login"], display: "codex login" };
|
|
23
|
-
case "gemini":
|
|
24
|
-
return { bin: "gemini", args: [], display: "gemini" }; // el CLI guía el auth al arrancar
|
|
25
|
-
default:
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** @param {string} text */
|
|
31
|
-
export function looksLikeAuthFailure(text) {
|
|
32
|
-
const s = String(text || "").toLowerCase();
|
|
33
|
-
return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run/.test(
|
|
34
|
-
s
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Mensaje en español si el agente murió / falló por falta de login.
|
|
40
|
-
* @param {string} agentId
|
|
41
|
-
* @param {string} agentLabel
|
|
42
|
-
* @param {string} detail
|
|
43
|
-
* @returns {string | null}
|
|
44
|
-
*/
|
|
45
|
-
export function authFailureMessage(agentId, agentLabel, detail) {
|
|
46
|
-
if (!looksLikeAuthFailure(detail)) return null;
|
|
47
|
-
const cmd = loginCommand(agentId);
|
|
48
|
-
if (agentId === "cursor") {
|
|
49
|
-
return `${agentLabel} no está autenticado. Iniciá sesión en la app de Cursor y reintentá.`;
|
|
50
|
-
}
|
|
51
|
-
if (cmd) {
|
|
52
|
-
return `${agentLabel} no está autenticado. Corré \`${cmd.display}\` en una terminal y reintentá.`;
|
|
53
|
-
}
|
|
54
|
-
return `${agentLabel} no está autenticado. Revisá el login del agente y reintentá.`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Abre una terminal nueva con el login del agente (Windows / macOS / Linux).
|
|
59
|
-
* @param {string} agentId
|
|
60
|
-
* @returns {{ display: string }}
|
|
61
|
-
*/
|
|
62
|
-
export function openLoginTerminal(agentId) {
|
|
63
|
-
const cmd = loginCommand(agentId);
|
|
64
|
-
if (!cmd) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
agentId === "cursor"
|
|
67
|
-
? "Cursor se autentica desde la app de escritorio, no desde una terminal."
|
|
68
|
-
: "Este agente no tiene un comando de login conocido."
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const platform = process.platform;
|
|
73
|
-
if (platform === "win32") {
|
|
74
|
-
// `start "title" cmd /k …` — el primer quoted es el título de la ventana.
|
|
75
|
-
spawn("cmd.exe", ["/c", "start", `${cmd.bin} login`, "cmd.exe", "/k", cmd.bin, ...cmd.args], {
|
|
76
|
-
detached: true,
|
|
77
|
-
stdio: "ignore",
|
|
78
|
-
windowsHide: false,
|
|
79
|
-
}).unref();
|
|
80
|
-
} else if (platform === "darwin") {
|
|
81
|
-
const script = `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`;
|
|
82
|
-
spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
83
|
-
} else {
|
|
84
|
-
const line = `${cmd.bin} ${cmd.args.join(" ")}`.trim();
|
|
85
|
-
const tried = [
|
|
86
|
-
["x-terminal-emulator", ["-e", "bash", "-lc", `${line}; exec bash`]],
|
|
87
|
-
["gnome-terminal", ["--", "bash", "-lc", `${line}; exec bash`]],
|
|
88
|
-
["konsole", ["-e", "bash", "-lc", `${line}; exec bash`]],
|
|
89
|
-
["xterm", ["-e", "bash", "-lc", `${line}; exec bash`]],
|
|
90
|
-
];
|
|
91
|
-
let ok = false;
|
|
92
|
-
for (const [bin, args] of tried) {
|
|
93
|
-
try {
|
|
94
|
-
spawn(bin, args, { detached: true, stdio: "ignore" }).unref();
|
|
95
|
-
ok = true;
|
|
96
|
-
break;
|
|
97
|
-
} catch {
|
|
98
|
-
/* siguiente */
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (!ok) throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { display: cmd.display };
|
|
105
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Login de agentes ACP: mensajes claros + abrir una terminal interactiva cuando se pueda.
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{ bin: string, args: string[], display: string }} LoginCommand
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Comando de login interactivo por agente (null = no hay CLI de login, p. ej. Cursor app).
|
|
12
|
+
* @param {string} agentId
|
|
13
|
+
* @returns {LoginCommand | null}
|
|
14
|
+
*/
|
|
15
|
+
export function loginCommand(agentId) {
|
|
16
|
+
switch (agentId) {
|
|
17
|
+
case "claude":
|
|
18
|
+
return { bin: "claude", args: ["/login"], display: "claude /login" };
|
|
19
|
+
case "copilot":
|
|
20
|
+
return { bin: "gh", args: ["auth", "login"], display: "gh auth login" };
|
|
21
|
+
case "codex":
|
|
22
|
+
return { bin: "codex", args: ["login"], display: "codex login" };
|
|
23
|
+
case "gemini":
|
|
24
|
+
return { bin: "gemini", args: [], display: "gemini" }; // el CLI guía el auth al arrancar
|
|
25
|
+
default:
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @param {string} text */
|
|
31
|
+
export function looksLikeAuthFailure(text) {
|
|
32
|
+
const s = String(text || "").toLowerCase();
|
|
33
|
+
return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run/.test(
|
|
34
|
+
s
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Mensaje en español si el agente murió / falló por falta de login.
|
|
40
|
+
* @param {string} agentId
|
|
41
|
+
* @param {string} agentLabel
|
|
42
|
+
* @param {string} detail
|
|
43
|
+
* @returns {string | null}
|
|
44
|
+
*/
|
|
45
|
+
export function authFailureMessage(agentId, agentLabel, detail) {
|
|
46
|
+
if (!looksLikeAuthFailure(detail)) return null;
|
|
47
|
+
const cmd = loginCommand(agentId);
|
|
48
|
+
if (agentId === "cursor") {
|
|
49
|
+
return `${agentLabel} no está autenticado. Iniciá sesión en la app de Cursor y reintentá.`;
|
|
50
|
+
}
|
|
51
|
+
if (cmd) {
|
|
52
|
+
return `${agentLabel} no está autenticado. Corré \`${cmd.display}\` en una terminal y reintentá.`;
|
|
53
|
+
}
|
|
54
|
+
return `${agentLabel} no está autenticado. Revisá el login del agente y reintentá.`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Abre una terminal nueva con el login del agente (Windows / macOS / Linux).
|
|
59
|
+
* @param {string} agentId
|
|
60
|
+
* @returns {{ display: string }}
|
|
61
|
+
*/
|
|
62
|
+
export function openLoginTerminal(agentId) {
|
|
63
|
+
const cmd = loginCommand(agentId);
|
|
64
|
+
if (!cmd) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
agentId === "cursor"
|
|
67
|
+
? "Cursor se autentica desde la app de escritorio, no desde una terminal."
|
|
68
|
+
: "Este agente no tiene un comando de login conocido."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const platform = process.platform;
|
|
73
|
+
if (platform === "win32") {
|
|
74
|
+
// `start "title" cmd /k …` — el primer quoted es el título de la ventana.
|
|
75
|
+
spawn("cmd.exe", ["/c", "start", `${cmd.bin} login`, "cmd.exe", "/k", cmd.bin, ...cmd.args], {
|
|
76
|
+
detached: true,
|
|
77
|
+
stdio: "ignore",
|
|
78
|
+
windowsHide: false,
|
|
79
|
+
}).unref();
|
|
80
|
+
} else if (platform === "darwin") {
|
|
81
|
+
const script = `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`;
|
|
82
|
+
spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
83
|
+
} else {
|
|
84
|
+
const line = `${cmd.bin} ${cmd.args.join(" ")}`.trim();
|
|
85
|
+
const tried = [
|
|
86
|
+
["x-terminal-emulator", ["-e", "bash", "-lc", `${line}; exec bash`]],
|
|
87
|
+
["gnome-terminal", ["--", "bash", "-lc", `${line}; exec bash`]],
|
|
88
|
+
["konsole", ["-e", "bash", "-lc", `${line}; exec bash`]],
|
|
89
|
+
["xterm", ["-e", "bash", "-lc", `${line}; exec bash`]],
|
|
90
|
+
];
|
|
91
|
+
let ok = false;
|
|
92
|
+
for (const [bin, args] of tried) {
|
|
93
|
+
try {
|
|
94
|
+
spawn(bin, args, { detached: true, stdio: "ignore" }).unref();
|
|
95
|
+
ok = true;
|
|
96
|
+
break;
|
|
97
|
+
} catch {
|
|
98
|
+
/* siguiente */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!ok) throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { display: cmd.display };
|
|
105
|
+
}
|
package/lib/check-report.mjs
CHANGED
|
@@ -177,6 +177,7 @@ export function formatCheckReport(file, command, result) {
|
|
|
177
177
|
"How to continue:",
|
|
178
178
|
"- Fix every problem above in place. Keep existing meta/intent/groups/blocks/findings; only change what is wrong or missing.",
|
|
179
179
|
'- Every diff path must appear in "blocks" or "skipped" (a skipped glob counts).',
|
|
180
|
+
"- If a problem lists missing paths: ADD a block or a skipped entry for each exact path. Do not only bump meta.generatedAt, notes, or claim the file is already complete.",
|
|
180
181
|
"- meta.agent and meta.model are required (product name and the exact model id you are running).",
|
|
181
182
|
"- intent ≥40 chars; enums exactly as in the schema.",
|
|
182
183
|
"- Human-readable strings in Spanish.",
|