@jpkme/diff-review 0.1.15 → 0.1.17
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 -14
- package/bin/diff-review.mjs +192 -21
- package/lib/acp-policy.mjs +10 -2
- package/lib/acp-policy.test.mjs +27 -0
- package/lib/acp.mjs +86 -10
- package/lib/agent-login.mjs +848 -36
- package/lib/agent-login.test.mjs +538 -0
- package/lib/agents.mjs +0 -39
- package/lib/agents.test.mjs +17 -24
- package/lib/extract-cache.test.mjs +162 -162
- package/lib/git.mjs +25 -9
- package/lib/model-label.mjs +20 -4
- package/lib/model-label.test.mjs +15 -2
- package/lib/output-name.mjs +7 -0
- package/lib/prompt.mjs +14 -11
- package/lib/prompt.test.mjs +49 -0
- package/lib/review-dir.mjs +14 -1
- package/lib/review-meta.mjs +11 -3
- package/lib/review-meta.test.mjs +19 -2
- package/lib/review-prep.mjs +63 -63
- package/lib/review.mjs +36 -4
- package/lib/review.test.mjs +35 -0
- package/lib/validate.mjs +11 -1
- package/lib/validate.test.mjs +47 -0
- package/lib/wait-output.test.mjs +1 -1
- package/package.json +2 -2
- package/schema.json +16 -1
- package/web/build/client/_app/immutable/assets/0.B04dzd8P.css +1 -0
- package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.br +0 -0
- package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css +1 -0
- package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.BVJgADrM.css +1 -0
- package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/B-2kfLeA.js +1 -0
- package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/B4SiKXUO.js +2 -0
- package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{D8WOD_eS.js → DA0YnpGR.js} +3 -3
- package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{BpVtXA-T.js → Dv85Hz5p.js} +1 -1
- package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.br +2 -0
- package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/b-BaDYIc.js +215 -0
- package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js +2 -0
- package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.hH87UZsD.js +1 -0
- package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js +12 -0
- package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js +340 -0
- package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.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-8NUeB9cB.js +11 -0
- package/web/build/server/chunks/{0-DUStDOsr.js.map → 0-8NUeB9cB.js.map} +1 -1
- package/web/build/server/chunks/1-BApN-ILJ.js +7 -0
- package/web/build/server/chunks/{1-DUj63QTT.js.map → 1-BApN-ILJ.js.map} +1 -1
- package/web/build/server/chunks/2-Bq7iMeQx.js +7 -0
- package/web/build/server/chunks/2-Bq7iMeQx.js.map +1 -0
- package/web/build/server/chunks/3-W0cqofyE.js +7 -0
- package/web/build/server/chunks/{3-C7UD6rei.js.map → 3-W0cqofyE.js.map} +1 -1
- package/web/build/server/chunks/_server.ts-Bu1fdGSB.js +123 -0
- package/web/build/server/chunks/_server.ts-Bu1fdGSB.js.map +1 -0
- package/web/build/server/chunks/{_server.ts-4RcoUd0l.js → _server.ts-BuUNechu.js} +5 -4
- package/web/build/server/chunks/_server.ts-BuUNechu.js.map +1 -0
- package/web/build/server/chunks/{agent-sessions-C_hlZJKN.js → agent-sessions-BArYLTW6.js} +1103 -146
- package/web/build/server/chunks/agent-sessions-BArYLTW6.js.map +1 -0
- package/web/build/server/chunks/{git-review-Bh1w0gak.js → git-review-BJ_JrFNP.js} +87 -26
- package/web/build/server/chunks/git-review-BJ_JrFNP.js.map +1 -0
- package/web/build/server/chunks/{handler-e2lEvjVT.js → handler-Df_bJiCq.js} +3 -3
- package/web/build/server/chunks/{handler-e2lEvjVT.js.map → handler-Df_bJiCq.js.map} +1 -1
- package/web/build/server/chunks/{hooks.server-Um1Sknmz.js → hooks.server-BEl7rVad.js} +4 -3
- package/web/build/server/chunks/{hooks.server-Um1Sknmz.js.map → hooks.server-BEl7rVad.js.map} +1 -1
- package/web/build/server/chunks/{index.js-C3-YX8SA.js → index.js-Bb0CZLZ_.js} +884 -881
- package/web/build/server/chunks/index.js-Bb0CZLZ_.js.map +1 -0
- package/web/build/server/chunks/manifest.js-j442duM7.js +65 -0
- package/web/build/server/chunks/manifest.js-j442duM7.js.map +1 -0
- package/web/build/server/chunks/{remote-1g4ayag-DMiPy1mT.js → remote-1g4ayag-BxKnZQPn.js} +6 -6
- package/web/build/server/chunks/remote-1g4ayag-BxKnZQPn.js.map +1 -0
- package/web/build/server/chunks/{remote-wh6t6j-RlIVOUe-.js → remote-wh6t6j-B-AHrGaN.js} +58 -12
- package/web/build/server/chunks/remote-wh6t6j-B-AHrGaN.js.map +1 -0
- package/web/build/server/chunks/{validate-CAQ31C7k.js → validate-CXjTRGnR.js} +67 -21
- package/web/build/server/chunks/validate-CXjTRGnR.js.map +1 -0
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css +0 -1
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +0 -1
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css +0 -1
- package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css +0 -1
- package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/BpVtXA-T.js.br +0 -2
- package/web/build/client/_app/immutable/chunks/BpVtXA-T.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CUhrLa7d.js +0 -210
- package/web/build/client/_app/immutable/chunks/CUhrLa7d.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CUhrLa7d.js.gz +0 -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/QvO6K-HK.js +0 -2
- package/web/build/client/_app/immutable/chunks/QvO6K-HK.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/QvO6K-HK.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +0 -1
- 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.BjJsHui4.js +0 -2
- package/web/build/client/_app/immutable/entry/app.BjJsHui4.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.BjJsHui4.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.D4unRHC4.js +0 -1
- package/web/build/client/_app/immutable/entry/start.D4unRHC4.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.D4unRHC4.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js +0 -1
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js.br +0 -1
- package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js +0 -10
- package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js +0 -171
- package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js.gz +0 -0
- package/web/build/server/chunks/0-DUStDOsr.js +0 -11
- package/web/build/server/chunks/1-DUj63QTT.js +0 -7
- package/web/build/server/chunks/2-B4PSKe5P.js +0 -7
- package/web/build/server/chunks/2-B4PSKe5P.js.map +0 -1
- package/web/build/server/chunks/3-C7UD6rei.js +0 -7
- package/web/build/server/chunks/_server.ts-4RcoUd0l.js.map +0 -1
- package/web/build/server/chunks/agent-sessions-C_hlZJKN.js.map +0 -1
- package/web/build/server/chunks/git-review-Bh1w0gak.js.map +0 -1
- package/web/build/server/chunks/index.js-C3-YX8SA.js.map +0 -1
- package/web/build/server/chunks/manifest.js-CmbRPv0j.js +0 -58
- package/web/build/server/chunks/manifest.js-CmbRPv0j.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-DMiPy1mT.js.map +0 -1
- package/web/build/server/chunks/remote-wh6t6j-RlIVOUe-.js.map +0 -1
- package/web/build/server/chunks/validate-CAQ31C7k.js.map +0 -1
package/lib/agent-login.mjs
CHANGED
|
@@ -1,42 +1,155 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Login de agentes ACP: mensajes claros
|
|
2
|
+
* Login de agentes ACP: clasificación de authMethods, mensajes claros y lanzamiento del CLI de auth.
|
|
3
|
+
*
|
|
4
|
+
* Preferencia de launch:
|
|
5
|
+
* 1. Misma TTY (stdio inherit) si stdin/stdout son TTY — CLI interactiva, exit code real.
|
|
6
|
+
* 2. Proceso hijo oculto (sin ventana de terminal) si no hay TTY — p. ej. web; OAuth abre el browser.
|
|
7
|
+
* 3. Terminal externa solo con `external: true` (raro: login que exige TTY interactiva).
|
|
8
|
+
*
|
|
9
|
+
* En Windows, `cmd /c start /wait` **no** propaga el exit code del proceso iniciado (siempre 0
|
|
10
|
+
* hacia Node). El fallback externo usa un archivo sentinel, igual que macOS/Linux.
|
|
3
11
|
*/
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
12
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { spawnCommand } from "./agents.mjs";
|
|
5
18
|
|
|
6
19
|
/**
|
|
7
|
-
* @typedef {{ bin: string, args: string[], display: string }} LoginCommand
|
|
20
|
+
* @typedef {{ bin: string, args: string[], display: string, env?: Record<string, string> }} LoginCommand
|
|
21
|
+
* @typedef {'agent' | 'terminal' | 'terminal-meta' | 'unknown'} AuthMethodKind
|
|
22
|
+
* @typedef {'inherit' | 'hidden' | 'external'} LoginLaunchMode
|
|
8
23
|
*/
|
|
9
24
|
|
|
10
25
|
/**
|
|
11
|
-
*
|
|
26
|
+
* Hostname de Copilot (Enterprise). Vacío / `github.com` = cuenta en github.com.
|
|
27
|
+
* @param {string | null | undefined} host
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeCopilotLoginHost(host) {
|
|
31
|
+
const h = String(host || "")
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/^https?:\/\//i, "")
|
|
34
|
+
.replace(/\/.*$/, "");
|
|
35
|
+
if (!h || /^github\.com$/i.test(h)) return "";
|
|
36
|
+
return h;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Agrega `--host` a un comando `copilot login` cuando el usuario eligió Enterprise.
|
|
41
|
+
* No fuerza nada por env: el host lo elige quien llama (CLI/UI).
|
|
42
|
+
* @param {LoginCommand} cmd
|
|
43
|
+
* @param {string | null | undefined} host
|
|
44
|
+
* @returns {LoginCommand}
|
|
45
|
+
*/
|
|
46
|
+
export function withCopilotLoginHost(cmd, host) {
|
|
47
|
+
if (!cmd || cmd.bin == null) return cmd;
|
|
48
|
+
const h = normalizeCopilotLoginHost(host);
|
|
49
|
+
if (!h) return cmd;
|
|
50
|
+
const args = Array.isArray(cmd.args) ? [...cmd.args] : [];
|
|
51
|
+
const hostIdx = args.findIndex((a) => a === "--host");
|
|
52
|
+
if (hostIdx >= 0) {
|
|
53
|
+
args[hostIdx + 1] = h;
|
|
54
|
+
} else {
|
|
55
|
+
args.push("--host", h);
|
|
56
|
+
}
|
|
57
|
+
const withoutHost = String(cmd.display || "")
|
|
58
|
+
.replace(/\s+--host\s+\S+/g, "")
|
|
59
|
+
.trim();
|
|
60
|
+
return {
|
|
61
|
+
...cmd,
|
|
62
|
+
args,
|
|
63
|
+
display: `${withoutHost} --host ${h}`.trim(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Comando de login interactivo por agente (fallback si no hay authMethod usable).
|
|
12
69
|
* @param {string} agentId
|
|
70
|
+
* @param {{ host?: string | null }} [opts] solo Copilot: host Enterprise opcional
|
|
13
71
|
* @returns {LoginCommand | null}
|
|
14
72
|
*/
|
|
15
|
-
export function loginCommand(agentId) {
|
|
73
|
+
export function loginCommand(agentId, opts = {}) {
|
|
16
74
|
switch (agentId) {
|
|
17
75
|
case "claude":
|
|
18
|
-
|
|
76
|
+
// `--cli` es del adaptador ACP, no del binario `claude` (falla: unknown option '--cli').
|
|
77
|
+
return { bin: "claude", args: ["auth", "login"], display: "claude auth login" };
|
|
19
78
|
case "copilot":
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
79
|
+
return withCopilotLoginHost(
|
|
80
|
+
{ bin: "copilot", args: ["login"], display: "copilot login" },
|
|
81
|
+
opts.host
|
|
82
|
+
);
|
|
83
|
+
case "cursor":
|
|
84
|
+
return { bin: "cursor-agent", args: ["login"], display: "cursor-agent login" };
|
|
25
85
|
default:
|
|
26
86
|
return null;
|
|
27
87
|
}
|
|
28
88
|
}
|
|
29
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Args de `claude auth login …` a partir de un authMethod ACP.
|
|
92
|
+
* El adaptador anuncia `["--cli", "auth", "login", "--claudeai"]` (o lo mismo tras el path del
|
|
93
|
+
* adapter en `_meta.terminal-auth.args`); `--cli` solo entiende el wrapper ACP, no `claude`.
|
|
94
|
+
* @param {any} [method]
|
|
95
|
+
* @returns {string[]}
|
|
96
|
+
*/
|
|
97
|
+
export function claudeCliLoginArgs(method) {
|
|
98
|
+
const meta = method?._meta?.["terminal-auth"];
|
|
99
|
+
const raw = Array.isArray(meta?.args)
|
|
100
|
+
? meta.args.map(String)
|
|
101
|
+
: Array.isArray(method?.args)
|
|
102
|
+
? method.args.map(String)
|
|
103
|
+
: [];
|
|
104
|
+
const cliIdx = raw.indexOf("--cli");
|
|
105
|
+
const after = cliIdx >= 0 ? raw.slice(cliIdx + 1) : raw;
|
|
106
|
+
if (after[0] === "auth") return after;
|
|
107
|
+
const flags = after.filter((a) => a.startsWith("--"));
|
|
108
|
+
return ["auth", "login", ...flags];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clasifica un authMethod de `initialize` según ACP v1 + extensión Copilot `_meta.terminal-auth`.
|
|
113
|
+
* @param {any} method
|
|
114
|
+
* @returns {AuthMethodKind}
|
|
115
|
+
*/
|
|
116
|
+
export function classifyAuthMethod(method) {
|
|
117
|
+
if (!method || typeof method.id !== "string" || !method.id) return "unknown";
|
|
118
|
+
// Copilot / Claude-con-meta: `_meta.terminal-auth` gana aunque también traigan type.
|
|
119
|
+
if (method._meta != null && method._meta["terminal-auth"] != null) return "terminal-meta";
|
|
120
|
+
if (method.type === "terminal") return "terminal";
|
|
121
|
+
if (!method.type || method.type === "agent") return "agent";
|
|
122
|
+
return "unknown";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {any[]} authMethods
|
|
127
|
+
* @returns {{ agent: any[], terminal: any[], terminalMeta: any[], unknown: any[] }}
|
|
128
|
+
*/
|
|
129
|
+
export function groupAuthMethods(authMethods) {
|
|
130
|
+
/** @type {{ agent: any[], terminal: any[], terminalMeta: any[], unknown: any[] }} */
|
|
131
|
+
const groups = { agent: [], terminal: [], terminalMeta: [], unknown: [] };
|
|
132
|
+
for (const method of authMethods || []) {
|
|
133
|
+
const kind = classifyAuthMethod(method);
|
|
134
|
+
if (kind === "agent") groups.agent.push(method);
|
|
135
|
+
else if (kind === "terminal") groups.terminal.push(method);
|
|
136
|
+
else if (kind === "terminal-meta") groups.terminalMeta.push(method);
|
|
137
|
+
else groups.unknown.push(method);
|
|
138
|
+
}
|
|
139
|
+
return groups;
|
|
140
|
+
}
|
|
141
|
+
|
|
30
142
|
/** @param {string} text */
|
|
31
143
|
export function looksLikeAuthFailure(text) {
|
|
32
144
|
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(
|
|
145
|
+
return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run|auth_required/.test(
|
|
34
146
|
s
|
|
35
147
|
);
|
|
36
148
|
}
|
|
37
149
|
|
|
38
150
|
/**
|
|
39
151
|
* Mensaje en español si el agente murió / falló por falta de login.
|
|
152
|
+
* Sin comandos CLI: el CTA abre el login por el usuario.
|
|
40
153
|
* @param {string} agentId
|
|
41
154
|
* @param {string} agentLabel
|
|
42
155
|
* @param {string} detail
|
|
@@ -44,43 +157,428 @@ export function looksLikeAuthFailure(text) {
|
|
|
44
157
|
*/
|
|
45
158
|
export function authFailureMessage(agentId, agentLabel, detail) {
|
|
46
159
|
if (!looksLikeAuthFailure(detail)) return null;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
160
|
+
const label = String(agentLabel || "").trim() || "el agente";
|
|
161
|
+
return `Necesitás iniciar sesión en ${label}.`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Fallo del proceso de login (exit ≠ 0 o cancelado a medias). */
|
|
165
|
+
export function loginIncompleteMessage() {
|
|
166
|
+
return "No se pudo completar el inicio de sesión. Probá de nuevo.";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Estado mientras corre el login (TTY, proceso oculto o terminal externa). */
|
|
170
|
+
export function loginWaitingMessage() {
|
|
171
|
+
return "Esperando que completes el inicio de sesión…";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* ¿El error pide login interactivo? (AcpAuthRequiredError o texto típico de auth).
|
|
176
|
+
* @param {unknown} err
|
|
177
|
+
*/
|
|
178
|
+
export function isAuthRequiredError(err) {
|
|
179
|
+
if (!err) return false;
|
|
180
|
+
if (typeof err === "object" && /** @type {{ name?: string }} */ (err).name === "AcpAuthRequiredError") {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
184
|
+
return looksLikeAuthFailure(msg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* authMethods de un AcpAuthRequiredError (si los trae).
|
|
189
|
+
* @param {unknown} err
|
|
190
|
+
* @returns {any[]}
|
|
191
|
+
*/
|
|
192
|
+
export function authMethodsFromError(err) {
|
|
193
|
+
if (!err || typeof err !== "object") return [];
|
|
194
|
+
const methods = /** @type {{ methods?: unknown }} */ (err).methods;
|
|
195
|
+
return Array.isArray(methods) ? methods : [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Método interactivo a abrir: `_meta.terminal-auth` (Copilot) o `type: terminal` (Claude).
|
|
200
|
+
* Si solo hay métodos `agent` (Cursor), devolvemos null → `resolveLoginCommand` usa el fallback CLI.
|
|
201
|
+
* @param {any[]} [methods]
|
|
202
|
+
* @returns {any | null}
|
|
203
|
+
*/
|
|
204
|
+
export function pickLoginAuthMethod(methods) {
|
|
205
|
+
const grouped = groupAuthMethods(methods || []);
|
|
206
|
+
return grouped.terminalMeta[0] ?? grouped.terminal[0] ?? null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Tras un fallo de auth: pregunta, abre el login (espera exit 0) y reintenta `op` una vez.
|
|
211
|
+
* Con `priorError`, saltea el primer intento de `op` (ya falló el caller).
|
|
212
|
+
* No duplica clasificación: reusa `pickLoginAuthMethod` / `resolveLoginCommand` / `runLoginTerminal`.
|
|
213
|
+
*
|
|
214
|
+
* @template T
|
|
215
|
+
* @param {() => Promise<T>} op
|
|
216
|
+
* @param {{
|
|
217
|
+
* agent: { def: { id: string, label: string, acp?: Function }, path: string },
|
|
218
|
+
* cwd?: string,
|
|
219
|
+
* priorError?: unknown,
|
|
220
|
+
* askToLogin: (info: { message: string, agentLabel: string, displayCmd: string }) => Promise<boolean>,
|
|
221
|
+
* pickCopilotHost?: () => Promise<string | null | undefined | false>,
|
|
222
|
+
* onStatus?: (msg: string) => void,
|
|
223
|
+
* runTerminal?: typeof runLoginTerminal,
|
|
224
|
+
* }} opts
|
|
225
|
+
* @returns {Promise<
|
|
226
|
+
* | { ok: true, value: T }
|
|
227
|
+
* | { ok: false, cancelled: true, error: unknown }
|
|
228
|
+
* | { ok: false, cancelled: false, error: unknown }
|
|
229
|
+
* >}
|
|
230
|
+
*/
|
|
231
|
+
export async function withAgentLoginRetry(op, opts) {
|
|
232
|
+
/** @type {unknown} */
|
|
233
|
+
let err = opts.priorError;
|
|
234
|
+
if (err === undefined) {
|
|
235
|
+
try {
|
|
236
|
+
return { ok: true, value: await op() };
|
|
237
|
+
} catch (e) {
|
|
238
|
+
err = e;
|
|
239
|
+
}
|
|
50
240
|
}
|
|
51
|
-
|
|
52
|
-
|
|
241
|
+
|
|
242
|
+
if (!isAuthRequiredError(err)) {
|
|
243
|
+
return { ok: false, cancelled: false, error: err };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const agentLabel = opts.agent?.def?.label || "El agente";
|
|
247
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
248
|
+
const fromAcp =
|
|
249
|
+
typeof err === "object" &&
|
|
250
|
+
err &&
|
|
251
|
+
/** @type {{ name?: string }} */ (err).name === "AcpAuthRequiredError";
|
|
252
|
+
const friendly =
|
|
253
|
+
authFailureMessage(opts.agent?.def?.id || "", agentLabel, raw) ||
|
|
254
|
+
(fromAcp ? `Necesitás iniciar sesión en ${agentLabel}.` : null) ||
|
|
255
|
+
raw;
|
|
256
|
+
const message =
|
|
257
|
+
String(friendly).split(/\r?\n/)[0].trim() || `Necesitás iniciar sesión en ${agentLabel}.`;
|
|
258
|
+
|
|
259
|
+
const method = pickLoginAuthMethod(authMethodsFromError(err));
|
|
260
|
+
let cmd;
|
|
261
|
+
try {
|
|
262
|
+
cmd = resolveLoginCommand(opts.agent, method);
|
|
263
|
+
} catch (resolveErr) {
|
|
264
|
+
return { ok: false, cancelled: false, error: resolveErr };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let want = false;
|
|
268
|
+
try {
|
|
269
|
+
want = Boolean(await opts.askToLogin({ message, agentLabel, displayCmd: cmd.display }));
|
|
270
|
+
} catch {
|
|
271
|
+
want = false;
|
|
272
|
+
}
|
|
273
|
+
if (!want) {
|
|
274
|
+
return { ok: false, cancelled: true, error: err };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (opts.agent?.def?.id === "copilot" && typeof opts.pickCopilotHost === "function") {
|
|
278
|
+
let hostPick;
|
|
279
|
+
try {
|
|
280
|
+
hostPick = await opts.pickCopilotHost();
|
|
281
|
+
} catch {
|
|
282
|
+
return { ok: false, cancelled: true, error: err };
|
|
283
|
+
}
|
|
284
|
+
if (hostPick === false) {
|
|
285
|
+
return { ok: false, cancelled: true, error: err };
|
|
286
|
+
}
|
|
287
|
+
cmd = withCopilotLoginHost(cmd, hostPick);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
opts.onStatus?.(loginWaitingMessage());
|
|
291
|
+
const run = opts.runTerminal || runLoginTerminal;
|
|
292
|
+
let result;
|
|
293
|
+
try {
|
|
294
|
+
result = await run(cmd, { wait: true, cwd: opts.cwd });
|
|
295
|
+
} catch (loginErr) {
|
|
296
|
+
return { ok: false, cancelled: false, error: loginErr };
|
|
297
|
+
}
|
|
298
|
+
if (result.code !== 0) {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
cancelled: false,
|
|
302
|
+
error: new Error(loginIncompleteMessage()),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
opts.onStatus?.("Login ok. Reintentando…");
|
|
307
|
+
try {
|
|
308
|
+
return { ok: true, value: await op() };
|
|
309
|
+
} catch (retryErr) {
|
|
310
|
+
return { ok: false, cancelled: false, error: retryErr };
|
|
53
311
|
}
|
|
54
|
-
return `${agentLabel} no está autenticado. Revisá el login del agente y reintentá.`;
|
|
55
312
|
}
|
|
56
313
|
|
|
57
314
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
315
|
+
* ¿Podemos correr el login en la misma terminal (stdio inherit)?
|
|
316
|
+
* La web / procesos sin TTY usan proceso oculto (o `external: true` → ventana).
|
|
317
|
+
* @param {{ external?: boolean }} [opts]
|
|
318
|
+
*/
|
|
319
|
+
export function canInheritLoginTty(opts = {}) {
|
|
320
|
+
if (opts.external) return false;
|
|
321
|
+
return Boolean(process.stdin?.isTTY && process.stdout?.isTTY);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Modo de launch: inherit (CLI con TTY) → hidden (web / OAuth en browser) → external (forzado).
|
|
326
|
+
* @param {{ wait?: boolean, external?: boolean }} [opts]
|
|
327
|
+
* @returns {LoginLaunchMode}
|
|
328
|
+
*/
|
|
329
|
+
export function resolveLoginLaunchMode(opts = {}) {
|
|
330
|
+
if (Boolean(opts.wait) && canInheritLoginTty(opts)) return "inherit";
|
|
331
|
+
if (opts.external) return "external";
|
|
332
|
+
return "hidden";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Arma el comando de login a partir de un authMethod (o fallback por agentId).
|
|
337
|
+
* @param {{ def: { id: string, acp?: Function }, path: string }} agent
|
|
338
|
+
* @param {any} [method]
|
|
339
|
+
* @returns {LoginCommand}
|
|
340
|
+
*/
|
|
341
|
+
export function resolveLoginCommand(agent, method) {
|
|
342
|
+
const kind = method ? classifyAuthMethod(method) : "unknown";
|
|
343
|
+
const meta = method?._meta?.["terminal-auth"];
|
|
344
|
+
|
|
345
|
+
// Claude: preferir el CLI real (`claude auth login …`). Lanzar npx/adapter + `--cli` falla o es
|
|
346
|
+
// frágil en Windows; el meta `node + adapter --cli …` funciona pero es un rodeos innecesario.
|
|
347
|
+
if (
|
|
348
|
+
agent?.def?.id === "claude" &&
|
|
349
|
+
method &&
|
|
350
|
+
(kind === "terminal" || kind === "terminal-meta")
|
|
351
|
+
) {
|
|
352
|
+
const args = claudeCliLoginArgs(method);
|
|
353
|
+
const bin = typeof agent.path === "string" && agent.path.trim() ? agent.path : "claude";
|
|
354
|
+
return {
|
|
355
|
+
bin,
|
|
356
|
+
args,
|
|
357
|
+
display: `claude ${args.join(" ")}`.trim(),
|
|
358
|
+
...(method?.env && typeof method.env === "object" ? { env: method.env } : {}),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if ((kind === "terminal-meta" || kind === "terminal") && meta && typeof meta === "object") {
|
|
363
|
+
const command = typeof meta.command === "string" ? meta.command.trim() : "";
|
|
364
|
+
if (command) {
|
|
365
|
+
const args = Array.isArray(meta.args) ? meta.args.map(String) : [];
|
|
366
|
+
const label = typeof meta.label === "string" && meta.label.trim() ? meta.label.trim() : "";
|
|
367
|
+
return {
|
|
368
|
+
bin: command,
|
|
369
|
+
args,
|
|
370
|
+
display: label || `${command}${args.length ? ` ${args.join(" ")}` : ""}`.trim(),
|
|
371
|
+
...(method?.env && typeof method.env === "object" ? { env: method.env } : {}),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (kind === "terminal" && method && typeof agent?.def?.acp === "function") {
|
|
377
|
+
const launch = agent.def.acp({ path: agent.path });
|
|
378
|
+
const extra = Array.isArray(method.args) ? method.args.map(String) : [];
|
|
379
|
+
const env = {
|
|
380
|
+
...(launch.env && typeof launch.env === "object" ? launch.env : {}),
|
|
381
|
+
...(method.env && typeof method.env === "object" ? method.env : {}),
|
|
382
|
+
};
|
|
383
|
+
const args = [...(launch.args || []), ...extra];
|
|
384
|
+
return {
|
|
385
|
+
bin: launch.file,
|
|
386
|
+
args,
|
|
387
|
+
display: `${launch.file}${args.length ? ` ${args.join(" ")}` : ""}`.trim(),
|
|
388
|
+
...(Object.keys(env).length ? { env } : {}),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const fallback = loginCommand(agent?.def?.id);
|
|
393
|
+
if (fallback) {
|
|
394
|
+
if (agent?.def?.id === "claude" && typeof agent.path === "string" && agent.path.trim()) {
|
|
395
|
+
return { ...fallback, bin: agent.path };
|
|
396
|
+
}
|
|
397
|
+
return fallback;
|
|
398
|
+
}
|
|
399
|
+
throw new Error("Este agente no tiene un inicio de sesión conocido.");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** @param {import('node:child_process').ChildProcess} child */
|
|
403
|
+
function waitExit(child) {
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
child.once("error", reject);
|
|
406
|
+
child.once("exit", (code, signal) => resolve(signal ? 1 : (code ?? 1)));
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Lanza el login en background (proceso oculto; no espera). Compat con UI/CTA simples (web sin sesión).
|
|
412
|
+
* @param {string | LoginCommand} agentIdOrCmd
|
|
60
413
|
* @returns {{ display: string }}
|
|
61
414
|
*/
|
|
62
|
-
export function openLoginTerminal(
|
|
63
|
-
const cmd = loginCommand(
|
|
415
|
+
export function openLoginTerminal(agentIdOrCmd) {
|
|
416
|
+
const cmd = typeof agentIdOrCmd === "string" ? loginCommand(agentIdOrCmd) : agentIdOrCmd;
|
|
64
417
|
if (!cmd) {
|
|
418
|
+
const agentId = typeof agentIdOrCmd === "string" ? agentIdOrCmd : "";
|
|
65
419
|
throw new Error(
|
|
66
420
|
agentId === "cursor"
|
|
67
|
-
? "
|
|
68
|
-
: "Este agente no tiene un
|
|
421
|
+
? "Necesitás iniciar sesión en Cursor."
|
|
422
|
+
: "Este agente no tiene un inicio de sesión conocido."
|
|
69
423
|
);
|
|
70
424
|
}
|
|
425
|
+
void launchLoginProcess(cmd, { wait: false, mode: "hidden" });
|
|
426
|
+
return { display: cmd.display };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Lanza el login. Con TTY y `wait: true`, corre en esta terminal (inherit); si no, proceso oculto
|
|
431
|
+
* (OAuth en browser). Solo con `external: true` abre una ventana de terminal del OS.
|
|
432
|
+
* @param {LoginCommand} cmd
|
|
433
|
+
* @param {{ wait?: boolean, cwd?: string, external?: boolean }} [opts]
|
|
434
|
+
* @returns {Promise<{ display: string, code: number | null, mode: LoginLaunchMode }>}
|
|
435
|
+
*/
|
|
436
|
+
export async function runLoginTerminal(cmd, opts = {}) {
|
|
437
|
+
const wait = Boolean(opts.wait);
|
|
438
|
+
const mode = resolveLoginLaunchMode(opts);
|
|
439
|
+
const code = await launchLoginProcess(cmd, {
|
|
440
|
+
wait,
|
|
441
|
+
cwd: opts.cwd,
|
|
442
|
+
mode,
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
display: cmd.display,
|
|
446
|
+
code: wait ? code : null,
|
|
447
|
+
mode,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** @param {string} arg */
|
|
452
|
+
function quoteBatArg(arg) {
|
|
453
|
+
const s = String(arg);
|
|
454
|
+
if (/^[\w./:\\@=-]+$/.test(s)) return s;
|
|
455
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Espera a que el sentinel tenga un exit code (macOS/Linux/Windows externo).
|
|
460
|
+
* @param {string} sentinel
|
|
461
|
+
* @param {string} display
|
|
462
|
+
* @returns {Promise<number>}
|
|
463
|
+
*/
|
|
464
|
+
async function waitForExitSentinel(sentinel, display) {
|
|
465
|
+
const deadline = Date.now() + 30 * 60 * 1000;
|
|
466
|
+
while (Date.now() < deadline) {
|
|
467
|
+
try {
|
|
468
|
+
const raw = readFileSync(sentinel, "utf8").trim();
|
|
469
|
+
if (raw !== "") {
|
|
470
|
+
const code = Number.parseInt(raw, 10);
|
|
471
|
+
try {
|
|
472
|
+
unlinkSync(sentinel);
|
|
473
|
+
} catch {
|
|
474
|
+
/* ignore */
|
|
475
|
+
}
|
|
476
|
+
return Number.isFinite(code) ? code : 1;
|
|
477
|
+
}
|
|
478
|
+
} catch {
|
|
479
|
+
/* aún no */
|
|
480
|
+
}
|
|
481
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
unlinkSync(sentinel);
|
|
485
|
+
} catch {
|
|
486
|
+
/* ignore */
|
|
487
|
+
}
|
|
488
|
+
throw new Error(`Timeout esperando el login (${display}).`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Proceso hijo sin ventana de terminal (web / OAuth). Exit code real vía waitExit.
|
|
493
|
+
* @param {LoginCommand} cmd
|
|
494
|
+
* @param {{ wait: boolean, cwd?: string, env: NodeJS.ProcessEnv }} opts
|
|
495
|
+
* @returns {Promise<number>}
|
|
496
|
+
*/
|
|
497
|
+
async function launchHiddenLogin(cmd, { wait, cwd, env }) {
|
|
498
|
+
const child = spawnCommand(cmd.bin, cmd.args, {
|
|
499
|
+
cwd,
|
|
500
|
+
env,
|
|
501
|
+
stdio: "ignore",
|
|
502
|
+
windowsHide: true,
|
|
503
|
+
...(wait ? {} : { detached: true }),
|
|
504
|
+
});
|
|
505
|
+
if (!wait) {
|
|
506
|
+
child.unref();
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
return waitExit(child);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* @param {LoginCommand} cmd
|
|
514
|
+
* @param {{ wait: boolean, cwd?: string, mode: LoginLaunchMode }} opts
|
|
515
|
+
* @returns {Promise<number>}
|
|
516
|
+
*/
|
|
517
|
+
async function launchLoginProcess(cmd, { wait, cwd, mode }) {
|
|
518
|
+
const env = { ...process.env, ...(cmd.env || {}) };
|
|
519
|
+
|
|
520
|
+
if (mode === "inherit") {
|
|
521
|
+
const child = spawnCommand(cmd.bin, cmd.args, { cwd, env, stdio: "inherit" });
|
|
522
|
+
return waitExit(child);
|
|
523
|
+
}
|
|
71
524
|
|
|
525
|
+
if (mode === "hidden") {
|
|
526
|
+
return launchHiddenLogin(cmd, { wait, cwd, env });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// mode === "external": ventana visible del OS (solo si el caller pasó external: true).
|
|
72
530
|
const platform = process.platform;
|
|
531
|
+
|
|
73
532
|
if (platform === "win32") {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
533
|
+
if (!wait) {
|
|
534
|
+
const child = spawn(
|
|
535
|
+
"cmd.exe",
|
|
536
|
+
["/c", "start", "diff-review login", "cmd.exe", "/k", cmd.bin, ...cmd.args],
|
|
537
|
+
{ cwd, env, detached: true, stdio: "ignore", windowsHide: false }
|
|
538
|
+
);
|
|
539
|
+
child.unref();
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// `start /wait` siempre reporta 0 a Node: el exit real va al sentinel vía .bat.
|
|
544
|
+
const id = randomBytes(8).toString("hex");
|
|
545
|
+
const sentinel = join(tmpdir(), `diff-review-login-${id}.exit`);
|
|
546
|
+
const bat = join(tmpdir(), `diff-review-login-${id}.bat`);
|
|
547
|
+
const line = [quoteBatArg(cmd.bin), ...cmd.args.map(quoteBatArg)].join(" ");
|
|
548
|
+
// No usar `echo %ERRORLEVEL%>file`: con código 0, cmd interpreta `echo 0>` y el sentinel queda vacío.
|
|
549
|
+
const sentPath = sentinel.replace(/"/g, '""');
|
|
550
|
+
writeFileSync(
|
|
551
|
+
bat,
|
|
552
|
+
[`@echo off`, `call ${line}`, `> "${sentPath}" echo %ERRORLEVEL%`, ``].join("\r\n")
|
|
553
|
+
);
|
|
554
|
+
try {
|
|
555
|
+
writeFileSync(sentinel, "");
|
|
556
|
+
} catch {
|
|
557
|
+
/* ok */
|
|
558
|
+
}
|
|
559
|
+
const child = spawn(
|
|
560
|
+
"cmd.exe",
|
|
561
|
+
["/c", "start", "/wait", "diff-review login", "cmd.exe", "/c", bat],
|
|
562
|
+
{ cwd, env, stdio: "ignore", windowsHide: false }
|
|
563
|
+
);
|
|
564
|
+
try {
|
|
565
|
+
await waitExit(child);
|
|
566
|
+
return await waitForExitSentinel(sentinel, cmd.display);
|
|
567
|
+
} finally {
|
|
568
|
+
try {
|
|
569
|
+
unlinkSync(bat);
|
|
570
|
+
} catch {
|
|
571
|
+
/* ignore */
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (!wait) {
|
|
577
|
+
if (platform === "darwin") {
|
|
578
|
+
const script = `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`;
|
|
579
|
+
spawn("osascript", ["-e", script], { detached: true, stdio: "ignore", env }).unref();
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
84
582
|
const line = `${cmd.bin} ${cmd.args.join(" ")}`.trim();
|
|
85
583
|
const tried = [
|
|
86
584
|
["x-terminal-emulator", ["-e", "bash", "-lc", `${line}; exec bash`]],
|
|
@@ -88,10 +586,42 @@ export function openLoginTerminal(agentId) {
|
|
|
88
586
|
["konsole", ["-e", "bash", "-lc", `${line}; exec bash`]],
|
|
89
587
|
["xterm", ["-e", "bash", "-lc", `${line}; exec bash`]],
|
|
90
588
|
];
|
|
589
|
+
for (const [bin, args] of tried) {
|
|
590
|
+
try {
|
|
591
|
+
spawn(bin, args, { detached: true, stdio: "ignore", env, cwd }).unref();
|
|
592
|
+
return 0;
|
|
593
|
+
} catch {
|
|
594
|
+
/* siguiente */
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// wait=true en macOS/Linux: terminal externa + sentinel.
|
|
601
|
+
const sentinel = join(tmpdir(), `diff-review-login-${randomBytes(8).toString("hex")}.exit`);
|
|
602
|
+
try {
|
|
603
|
+
writeFileSync(sentinel, "");
|
|
604
|
+
} catch {
|
|
605
|
+
/* ok */
|
|
606
|
+
}
|
|
607
|
+
const quoted = [cmd.bin, ...cmd.args].map((a) => JSON.stringify(a)).join(" ");
|
|
608
|
+
const line = `${quoted}; echo $? > ${JSON.stringify(sentinel)}`;
|
|
609
|
+
|
|
610
|
+
if (platform === "darwin") {
|
|
611
|
+
const script = `tell application "Terminal" to do script ${JSON.stringify(line)}`;
|
|
612
|
+
spawn("osascript", ["-e", script], { stdio: "ignore", env, cwd });
|
|
613
|
+
} else {
|
|
614
|
+
const tried = [
|
|
615
|
+
["gnome-terminal", ["--wait", "--", "bash", "-lc", line]],
|
|
616
|
+
["x-terminal-emulator", ["-e", "bash", "-lc", line]],
|
|
617
|
+
["konsole", ["-e", "bash", "-lc", line]],
|
|
618
|
+
["xterm", ["-e", "bash", "-lc", line]],
|
|
619
|
+
];
|
|
91
620
|
let ok = false;
|
|
92
621
|
for (const [bin, args] of tried) {
|
|
93
622
|
try {
|
|
94
|
-
spawn(bin, args, {
|
|
623
|
+
const child = spawn(bin, args, { stdio: "ignore", env, cwd });
|
|
624
|
+
await waitExit(child);
|
|
95
625
|
ok = true;
|
|
96
626
|
break;
|
|
97
627
|
} catch {
|
|
@@ -101,5 +631,287 @@ export function openLoginTerminal(agentId) {
|
|
|
101
631
|
if (!ok) throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
|
|
102
632
|
}
|
|
103
633
|
|
|
104
|
-
return
|
|
634
|
+
return waitForExitSentinel(sentinel, cmd.display);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Cuenta activa del agente (para el header de la web).
|
|
639
|
+
* @param {string} agentId
|
|
640
|
+
* @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync }} [opts]
|
|
641
|
+
* @returns {{
|
|
642
|
+
* agentId: string,
|
|
643
|
+
* loggedIn: boolean,
|
|
644
|
+
* accountLabel: string | null,
|
|
645
|
+
* detail: string | null,
|
|
646
|
+
* source: 'gh' | 'claude' | 'env' | 'unknown' | null,
|
|
647
|
+
* warnings: string[],
|
|
648
|
+
* }}
|
|
649
|
+
*/
|
|
650
|
+
export function agentAccountStatus(agentId, opts = {}) {
|
|
651
|
+
const id = String(agentId || "").trim();
|
|
652
|
+
const env = opts.env || process.env;
|
|
653
|
+
const run = opts.run || spawnSync;
|
|
654
|
+
/** @type {string[]} */
|
|
655
|
+
const warnings = [];
|
|
656
|
+
|
|
657
|
+
if (id === "copilot") {
|
|
658
|
+
const envHits = copilotTokenEnvOverrides(env);
|
|
659
|
+
if (envHits.length) {
|
|
660
|
+
warnings.push(`${envHits.join(", ")} en el entorno (pisa otras cuentas).`);
|
|
661
|
+
return {
|
|
662
|
+
agentId: id,
|
|
663
|
+
loggedIn: true,
|
|
664
|
+
accountLabel: "Token de entorno",
|
|
665
|
+
detail: envHits.join(", "),
|
|
666
|
+
source: "env",
|
|
667
|
+
warnings,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const accounts = listGhAuthAccounts({ env, run });
|
|
671
|
+
const active = accounts.find((a) => a.active) || accounts[0] || null;
|
|
672
|
+
if (active) {
|
|
673
|
+
return {
|
|
674
|
+
agentId: id,
|
|
675
|
+
loggedIn: true,
|
|
676
|
+
accountLabel: active.user,
|
|
677
|
+
detail: active.host,
|
|
678
|
+
source: "gh",
|
|
679
|
+
warnings,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
agentId: id,
|
|
684
|
+
loggedIn: false,
|
|
685
|
+
accountLabel: null,
|
|
686
|
+
detail: null,
|
|
687
|
+
source: null,
|
|
688
|
+
warnings,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (id === "claude") {
|
|
693
|
+
const r = run("claude", ["auth", "status", "--json"], {
|
|
694
|
+
encoding: "utf8",
|
|
695
|
+
env,
|
|
696
|
+
windowsHide: true,
|
|
697
|
+
});
|
|
698
|
+
if (r.error) {
|
|
699
|
+
return {
|
|
700
|
+
agentId: id,
|
|
701
|
+
loggedIn: false,
|
|
702
|
+
accountLabel: null,
|
|
703
|
+
detail: null,
|
|
704
|
+
source: null,
|
|
705
|
+
warnings: [r.error.message],
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
const parsed = JSON.parse(String(r.stdout || "{}"));
|
|
710
|
+
const loggedIn = Boolean(parsed?.loggedIn);
|
|
711
|
+
const email = String(parsed?.email || "").trim() || null;
|
|
712
|
+
const org = String(parsed?.orgName || "").trim() || null;
|
|
713
|
+
return {
|
|
714
|
+
agentId: id,
|
|
715
|
+
loggedIn,
|
|
716
|
+
accountLabel: loggedIn ? email || "Claude" : null,
|
|
717
|
+
detail: loggedIn ? org || String(parsed?.authMethod || "") || null : null,
|
|
718
|
+
source: loggedIn ? "claude" : null,
|
|
719
|
+
warnings,
|
|
720
|
+
};
|
|
721
|
+
} catch {
|
|
722
|
+
return {
|
|
723
|
+
agentId: id,
|
|
724
|
+
loggedIn: false,
|
|
725
|
+
accountLabel: null,
|
|
726
|
+
detail: null,
|
|
727
|
+
source: null,
|
|
728
|
+
warnings: ["No pude leer el estado de Claude."],
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (id === "cursor") {
|
|
734
|
+
return {
|
|
735
|
+
agentId: id,
|
|
736
|
+
loggedIn: true,
|
|
737
|
+
accountLabel: "Cursor",
|
|
738
|
+
detail: "Sesión del CLI / app",
|
|
739
|
+
source: "unknown",
|
|
740
|
+
warnings,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
agentId: id,
|
|
746
|
+
loggedIn: false,
|
|
747
|
+
accountLabel: null,
|
|
748
|
+
detail: null,
|
|
749
|
+
source: null,
|
|
750
|
+
warnings,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Env vars que Copilot usa antes que OAuth / gh (impiden un cambio de cuenta real).
|
|
756
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
757
|
+
* @returns {string[]}
|
|
758
|
+
*/
|
|
759
|
+
export function copilotTokenEnvOverrides(env = process.env) {
|
|
760
|
+
/** @type {string[]} */
|
|
761
|
+
const hit = [];
|
|
762
|
+
for (const key of ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]) {
|
|
763
|
+
if (String(env[key] || "").trim()) hit.push(key);
|
|
764
|
+
}
|
|
765
|
+
return hit;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Cuentas conocidas por `gh auth status --json hosts`.
|
|
770
|
+
* @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync }} [opts]
|
|
771
|
+
* @returns {{ host: string, user: string, active: boolean }[]}
|
|
772
|
+
*/
|
|
773
|
+
export function listGhAuthAccounts(opts = {}) {
|
|
774
|
+
const env = opts.env || process.env;
|
|
775
|
+
const run = opts.run || spawnSync;
|
|
776
|
+
const r = run("gh", ["auth", "status", "--json", "hosts"], {
|
|
777
|
+
encoding: "utf8",
|
|
778
|
+
env,
|
|
779
|
+
windowsHide: true,
|
|
780
|
+
});
|
|
781
|
+
if (r.error) return [];
|
|
782
|
+
try {
|
|
783
|
+
const parsed = JSON.parse(String(r.stdout || "{}"));
|
|
784
|
+
const hosts = parsed?.hosts && typeof parsed.hosts === "object" ? parsed.hosts : {};
|
|
785
|
+
/** @type {{ host: string, user: string, active: boolean }[]} */
|
|
786
|
+
const out = [];
|
|
787
|
+
for (const [host, accounts] of Object.entries(hosts)) {
|
|
788
|
+
const list = Array.isArray(accounts) ? accounts : [];
|
|
789
|
+
for (const acc of list) {
|
|
790
|
+
const user = String(acc?.login || acc?.user || "").trim();
|
|
791
|
+
if (!user) continue;
|
|
792
|
+
out.push({ host: String(host), user, active: Boolean(acc?.active) });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return out;
|
|
796
|
+
} catch {
|
|
797
|
+
return [];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Cierra sesiones de `gh` (Copilot las reusa si no hay OAuth propio).
|
|
803
|
+
* @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync, host?: string | null }} [opts]
|
|
804
|
+
* @returns {{ ok: boolean, loggedOut: { host: string, user: string }[], warnings: string[] }}
|
|
805
|
+
*/
|
|
806
|
+
export function logoutGhAuthAccounts(opts = {}) {
|
|
807
|
+
const env = opts.env || process.env;
|
|
808
|
+
const run = opts.run || spawnSync;
|
|
809
|
+
const onlyHost = normalizeCopilotLoginHost(opts.host) || null;
|
|
810
|
+
const accounts = listGhAuthAccounts({ env, run }).filter((a) =>
|
|
811
|
+
onlyHost ? a.host === onlyHost || a.host === `https://${onlyHost}` : true
|
|
812
|
+
);
|
|
813
|
+
/** @type {{ host: string, user: string }[]} */
|
|
814
|
+
const loggedOut = [];
|
|
815
|
+
/** @type {string[]} */
|
|
816
|
+
const warnings = [];
|
|
817
|
+
|
|
818
|
+
for (const acc of accounts) {
|
|
819
|
+
const r = run(
|
|
820
|
+
"gh",
|
|
821
|
+
["auth", "logout", "--hostname", acc.host, "--user", acc.user],
|
|
822
|
+
{ encoding: "utf8", env, windowsHide: true }
|
|
823
|
+
);
|
|
824
|
+
if (r.error) {
|
|
825
|
+
warnings.push(`No pude cerrar ${acc.user}@${acc.host}: ${r.error.message}`);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (r.status !== 0) {
|
|
829
|
+
const err = String(r.stderr || r.stdout || "").trim() || `exit ${r.status}`;
|
|
830
|
+
warnings.push(`No pude cerrar ${acc.user}@${acc.host}: ${err}`);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
loggedOut.push({ host: acc.host, user: acc.user });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const envHits = copilotTokenEnvOverrides(env);
|
|
837
|
+
if (envHits.length) {
|
|
838
|
+
warnings.push(
|
|
839
|
+
`Hay ${envHits.join(", ")} en el entorno: Copilot seguirá usando ese token hasta que lo saques.`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return { ok: warnings.length === 0 || loggedOut.length > 0, loggedOut, warnings };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Comando de logout CLI por agente (sin depender de gh).
|
|
848
|
+
* @param {string} agentId
|
|
849
|
+
* @returns {LoginCommand | null}
|
|
850
|
+
*/
|
|
851
|
+
export function logoutCommand(agentId) {
|
|
852
|
+
switch (agentId) {
|
|
853
|
+
case "claude":
|
|
854
|
+
return { bin: "claude", args: ["auth", "logout"], display: "claude auth logout" };
|
|
855
|
+
default:
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Cierra la sesión del agente para poder elegir otra cuenta.
|
|
862
|
+
* Copilot: también desloguea cuentas de `gh` (si solo hacés logout OAuth, gh sigue ganando).
|
|
863
|
+
*
|
|
864
|
+
* @param {string} agentId
|
|
865
|
+
* @param {{
|
|
866
|
+
* host?: string | null,
|
|
867
|
+
* env?: NodeJS.ProcessEnv,
|
|
868
|
+
* run?: typeof spawnSync,
|
|
869
|
+
* runTerminal?: typeof runLoginTerminal,
|
|
870
|
+
* cwd?: string,
|
|
871
|
+
* }} [opts]
|
|
872
|
+
* @returns {Promise<{ ok: boolean, steps: string[], warnings: string[] }>}
|
|
873
|
+
*/
|
|
874
|
+
export async function runAgentLogout(agentId, opts = {}) {
|
|
875
|
+
const id = String(agentId || "").trim();
|
|
876
|
+
/** @type {string[]} */
|
|
877
|
+
const steps = [];
|
|
878
|
+
/** @type {string[]} */
|
|
879
|
+
const warnings = [];
|
|
880
|
+
const env = opts.env || process.env;
|
|
881
|
+
const run = opts.run || spawnSync;
|
|
882
|
+
const runTerm = opts.runTerminal || runLoginTerminal;
|
|
883
|
+
|
|
884
|
+
if (id === "copilot") {
|
|
885
|
+
const gh = logoutGhAuthAccounts({ env, run, host: opts.host });
|
|
886
|
+
for (const a of gh.loggedOut) {
|
|
887
|
+
steps.push(`Cerré ${a.user} en ${a.host} (GitHub CLI)`);
|
|
888
|
+
}
|
|
889
|
+
warnings.push(...gh.warnings);
|
|
890
|
+
if (!gh.loggedOut.length && !gh.warnings.length) {
|
|
891
|
+
steps.push("No había cuentas de GitHub CLI activas.");
|
|
892
|
+
}
|
|
893
|
+
return { ok: true, steps, warnings };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const cmd = logoutCommand(id);
|
|
897
|
+
if (!cmd) {
|
|
898
|
+
return {
|
|
899
|
+
ok: false,
|
|
900
|
+
steps,
|
|
901
|
+
warnings: ["Este agente no tiene cierre de sesión desde la app."],
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
const result = await runTerm(cmd, { wait: true, cwd: opts.cwd });
|
|
907
|
+
if (result.code !== 0) {
|
|
908
|
+
warnings.push(`El logout terminó con código ${result.code ?? "?"}.`);
|
|
909
|
+
return { ok: false, steps, warnings };
|
|
910
|
+
}
|
|
911
|
+
steps.push(`Cerré la sesión (${cmd.display}).`);
|
|
912
|
+
return { ok: true, steps, warnings };
|
|
913
|
+
} catch (err) {
|
|
914
|
+
warnings.push(err instanceof Error ? err.message : String(err));
|
|
915
|
+
return { ok: false, steps, warnings };
|
|
916
|
+
}
|
|
105
917
|
}
|