@jpkme/diff-review 0.1.16 → 0.1.18

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.
Files changed (154) hide show
  1. package/README.md +22 -14
  2. package/bin/diff-review.mjs +220 -29
  3. package/lib/acp.mjs +86 -10
  4. package/lib/agent-login.mjs +848 -36
  5. package/lib/agent-login.test.mjs +538 -0
  6. package/lib/agents.mjs +0 -39
  7. package/lib/agents.test.mjs +17 -24
  8. package/lib/extract-cache.test.mjs +162 -162
  9. package/lib/git.mjs +25 -9
  10. package/lib/model-label.mjs +20 -4
  11. package/lib/model-label.test.mjs +15 -2
  12. package/lib/prompt.mjs +12 -10
  13. package/lib/prompt.test.mjs +12 -0
  14. package/lib/review-meta.mjs +11 -3
  15. package/lib/review-meta.test.mjs +19 -2
  16. package/lib/review-prep.mjs +63 -63
  17. package/lib/validate.mjs +1 -1
  18. package/lib/wait-output.test.mjs +1 -1
  19. package/package.json +2 -2
  20. package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css +1 -0
  21. package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css.br +0 -0
  22. package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css.gz +0 -0
  23. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css +1 -0
  24. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.br +0 -0
  25. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.gz +0 -0
  26. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css +1 -0
  27. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.br +0 -0
  28. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.gz +0 -0
  29. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css +1 -0
  30. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.br +0 -0
  31. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.gz +0 -0
  32. package/web/build/client/_app/immutable/chunks/{cYiFfRa3.js → -cMMFbSG.js} +1 -1
  33. package/web/build/client/_app/immutable/chunks/-cMMFbSG.js.br +2 -0
  34. package/web/build/client/_app/immutable/chunks/-cMMFbSG.js.gz +0 -0
  35. package/web/build/client/_app/immutable/chunks/0vBZPdKl.js +215 -0
  36. package/web/build/client/_app/immutable/chunks/0vBZPdKl.js.br +0 -0
  37. package/web/build/client/_app/immutable/chunks/0vBZPdKl.js.gz +0 -0
  38. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js +1 -0
  39. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.br +0 -0
  40. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.gz +0 -0
  41. package/web/build/client/_app/immutable/chunks/{DHO7NTh1.js → BMoYpzpA.js} +2 -2
  42. package/web/build/client/_app/immutable/chunks/BMoYpzpA.js.br +0 -0
  43. package/web/build/client/_app/immutable/chunks/BMoYpzpA.js.gz +0 -0
  44. package/web/build/client/_app/immutable/chunks/{CXhIC34H.js → DA0YnpGR.js} +3 -3
  45. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.br +0 -0
  46. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.gz +0 -0
  47. package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js +2 -0
  48. package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js.br +0 -0
  49. package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js.gz +0 -0
  50. package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js +1 -0
  51. package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js.br +0 -0
  52. package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js.gz +0 -0
  53. package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js +1 -0
  54. package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js.br +0 -0
  55. package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js.gz +0 -0
  56. package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js +1 -0
  57. package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js.br +2 -0
  58. package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js.gz +0 -0
  59. package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js +12 -0
  60. package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js.br +0 -0
  61. package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js.gz +0 -0
  62. package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js +340 -0
  63. package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js.br +0 -0
  64. package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js.gz +0 -0
  65. package/web/build/client/_app/version.json +1 -1
  66. package/web/build/client/_app/version.json.br +0 -0
  67. package/web/build/client/_app/version.json.gz +0 -0
  68. package/web/build/handler.js +3 -3
  69. package/web/build/index.js +3 -3
  70. package/web/build/server/chunks/0-BUdytb9b.js +11 -0
  71. package/web/build/server/chunks/{0-D7NSxn1-.js.map → 0-BUdytb9b.js.map} +1 -1
  72. package/web/build/server/chunks/1-DRokSBkY.js +7 -0
  73. package/web/build/server/chunks/{1-DboYz8Fw.js.map → 1-DRokSBkY.js.map} +1 -1
  74. package/web/build/server/chunks/2-BxyzaJTR.js +7 -0
  75. package/web/build/server/chunks/2-BxyzaJTR.js.map +1 -0
  76. package/web/build/server/chunks/3-D8o0VWkj.js +7 -0
  77. package/web/build/server/chunks/{3-DgJ_LmTL.js.map → 3-D8o0VWkj.js.map} +1 -1
  78. package/web/build/server/chunks/{_server.ts-CP5cRLhW.js → _server.ts-kax5Szn0.js} +5 -4
  79. package/web/build/server/chunks/_server.ts-kax5Szn0.js.map +1 -0
  80. package/web/build/server/chunks/_server.ts-vS3A9t3f.js +123 -0
  81. package/web/build/server/chunks/_server.ts-vS3A9t3f.js.map +1 -0
  82. package/web/build/server/chunks/{agent-sessions-2XGojSJ_.js → agent-sessions-BArYLTW6.js} +1023 -135
  83. package/web/build/server/chunks/agent-sessions-BArYLTW6.js.map +1 -0
  84. package/web/build/server/chunks/{git-review-jZ_JJYfv.js → git-review-hZSqk_1P.js} +54 -21
  85. package/web/build/server/chunks/{git-review-jZ_JJYfv.js.map → git-review-hZSqk_1P.js.map} +1 -1
  86. package/web/build/server/chunks/{handler-B_8yUXji.js → handler-DbFqzg7l.js} +3 -3
  87. package/web/build/server/chunks/{handler-B_8yUXji.js.map → handler-DbFqzg7l.js.map} +1 -1
  88. package/web/build/server/chunks/{hooks.server-rF_eBCff.js → hooks.server-BEl7rVad.js} +4 -3
  89. package/web/build/server/chunks/{hooks.server-rF_eBCff.js.map → hooks.server-BEl7rVad.js.map} +1 -1
  90. package/web/build/server/chunks/{index.js-CGw8_Q7V.js → index.js-BB9DZBdR.js} +4 -4
  91. package/web/build/server/chunks/index.js-BB9DZBdR.js.map +1 -0
  92. package/web/build/server/chunks/manifest.js-Dk6YX1jw.js +65 -0
  93. package/web/build/server/chunks/manifest.js-Dk6YX1jw.js.map +1 -0
  94. package/web/build/server/chunks/{remote-1g4ayag-BMpjk8UD.js → remote-1g4ayag-Bz1vj8X1.js} +6 -6
  95. package/web/build/server/chunks/remote-1g4ayag-Bz1vj8X1.js.map +1 -0
  96. package/web/build/server/chunks/{remote-wh6t6j-dYLCg8ks.js → remote-wh6t6j-DSwM3S9h.js} +44 -10
  97. package/web/build/server/chunks/remote-wh6t6j-DSwM3S9h.js.map +1 -0
  98. package/web/build/server/chunks/{validate-Da6e71V3.js → validate-CXjTRGnR.js} +34 -17
  99. package/web/build/server/chunks/validate-CXjTRGnR.js.map +1 -0
  100. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css +0 -1
  101. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  102. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.gz +0 -0
  103. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +0 -1
  104. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
  105. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
  106. package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css +0 -1
  107. package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.br +0 -0
  108. package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.gz +0 -0
  109. package/web/build/client/_app/immutable/assets/storage.BxR6930R.css +0 -1
  110. package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.br +0 -0
  111. package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.gz +0 -0
  112. package/web/build/client/_app/immutable/chunks/BLMWhW3J.js +0 -1
  113. package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.br +0 -1
  114. package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.gz +0 -0
  115. package/web/build/client/_app/immutable/chunks/CXhIC34H.js.br +0 -0
  116. package/web/build/client/_app/immutable/chunks/CXhIC34H.js.gz +0 -0
  117. package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.br +0 -0
  118. package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.gz +0 -0
  119. package/web/build/client/_app/immutable/chunks/DpxY75GB.js +0 -215
  120. package/web/build/client/_app/immutable/chunks/DpxY75GB.js.br +0 -0
  121. package/web/build/client/_app/immutable/chunks/DpxY75GB.js.gz +0 -0
  122. package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.br +0 -1
  123. package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.gz +0 -0
  124. package/web/build/client/_app/immutable/entry/app.CCv57mvp.js +0 -2
  125. package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.br +0 -0
  126. package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.gz +0 -0
  127. package/web/build/client/_app/immutable/entry/start.B012TSbW.js +0 -1
  128. package/web/build/client/_app/immutable/entry/start.B012TSbW.js.br +0 -1
  129. package/web/build/client/_app/immutable/entry/start.B012TSbW.js.gz +0 -0
  130. package/web/build/client/_app/immutable/nodes/0.slrnBioC.js +0 -1
  131. package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.br +0 -0
  132. package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.gz +0 -0
  133. package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js +0 -1
  134. package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.br +0 -0
  135. package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.gz +0 -0
  136. package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js +0 -10
  137. package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.br +0 -0
  138. package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.gz +0 -0
  139. package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js +0 -173
  140. package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.br +0 -0
  141. package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.gz +0 -0
  142. package/web/build/server/chunks/0-D7NSxn1-.js +0 -11
  143. package/web/build/server/chunks/1-DboYz8Fw.js +0 -7
  144. package/web/build/server/chunks/2-B7LX817Q.js +0 -7
  145. package/web/build/server/chunks/2-B7LX817Q.js.map +0 -1
  146. package/web/build/server/chunks/3-DgJ_LmTL.js +0 -7
  147. package/web/build/server/chunks/_server.ts-CP5cRLhW.js.map +0 -1
  148. package/web/build/server/chunks/agent-sessions-2XGojSJ_.js.map +0 -1
  149. package/web/build/server/chunks/index.js-CGw8_Q7V.js.map +0 -1
  150. package/web/build/server/chunks/manifest.js-CImZFS6G.js +0 -58
  151. package/web/build/server/chunks/manifest.js-CImZFS6G.js.map +0 -1
  152. package/web/build/server/chunks/remote-1g4ayag-BMpjk8UD.js.map +0 -1
  153. package/web/build/server/chunks/remote-wh6t6j-dYLCg8ks.js.map +0 -1
  154. package/web/build/server/chunks/validate-Da6e71V3.js.map +0 -1
@@ -0,0 +1,538 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ authFailureMessage,
5
+ authMethodsFromError,
6
+ canInheritLoginTty,
7
+ classifyAuthMethod,
8
+ claudeCliLoginArgs,
9
+ groupAuthMethods,
10
+ isAuthRequiredError,
11
+ loginCommand,
12
+ looksLikeAuthFailure,
13
+ pickLoginAuthMethod,
14
+ resolveLoginCommand,
15
+ resolveLoginLaunchMode,
16
+ normalizeCopilotLoginHost,
17
+ agentAccountStatus,
18
+ copilotTokenEnvOverrides,
19
+ listGhAuthAccounts,
20
+ logoutCommand,
21
+ logoutGhAuthAccounts,
22
+ withAgentLoginRetry,
23
+ withCopilotLoginHost,
24
+ } from "./agent-login.mjs";
25
+
26
+ describe("classifyAuthMethod", () => {
27
+ it("trata type ausente o agent como agent", () => {
28
+ assert.equal(classifyAuthMethod({ id: "cursor_login" }), "agent");
29
+ assert.equal(classifyAuthMethod({ id: "cursor_login", type: "agent" }), "agent");
30
+ });
31
+
32
+ it("trata type terminal como terminal", () => {
33
+ assert.equal(
34
+ classifyAuthMethod({ id: "claude-ai-login", type: "terminal", args: ["--login"] }),
35
+ "terminal"
36
+ );
37
+ });
38
+
39
+ it("prioriza _meta.terminal-auth sobre type agent o terminal", () => {
40
+ assert.equal(
41
+ classifyAuthMethod({
42
+ id: "copilot-login",
43
+ name: "Log in with Copilot CLI",
44
+ _meta: {
45
+ "terminal-auth": {
46
+ command: "/opt/homebrew/bin/copilot",
47
+ args: ["login"],
48
+ label: "Copilot Login",
49
+ },
50
+ },
51
+ }),
52
+ "terminal-meta"
53
+ );
54
+ assert.equal(
55
+ classifyAuthMethod({
56
+ id: "claude-ai-login",
57
+ type: "terminal",
58
+ args: ["--cli", "auth", "login", "--claudeai"],
59
+ _meta: {
60
+ "terminal-auth": {
61
+ command: "node",
62
+ args: ["/tmp/adapter.js", "--cli", "auth", "login", "--claudeai"],
63
+ label: "Claude Login",
64
+ },
65
+ },
66
+ }),
67
+ "terminal-meta"
68
+ );
69
+ });
70
+
71
+ it("marca unknown si falta id o type raro", () => {
72
+ assert.equal(classifyAuthMethod(null), "unknown");
73
+ assert.equal(classifyAuthMethod({}), "unknown");
74
+ assert.equal(classifyAuthMethod({ id: "x", type: "oauth" }), "unknown");
75
+ });
76
+ });
77
+
78
+ describe("groupAuthMethods", () => {
79
+ it("separa agent, terminal y terminal-meta", () => {
80
+ const grouped = groupAuthMethods([
81
+ { id: "cursor_login" },
82
+ { id: "claude-ai-login", type: "terminal", args: ["a"] },
83
+ {
84
+ id: "copilot-login",
85
+ _meta: { "terminal-auth": { command: "copilot", args: ["login"] } },
86
+ },
87
+ ]);
88
+ assert.deepEqual(
89
+ grouped.agent.map((m) => m.id),
90
+ ["cursor_login"]
91
+ );
92
+ assert.deepEqual(
93
+ grouped.terminal.map((m) => m.id),
94
+ ["claude-ai-login"]
95
+ );
96
+ assert.deepEqual(
97
+ grouped.terminalMeta.map((m) => m.id),
98
+ ["copilot-login"]
99
+ );
100
+ });
101
+ });
102
+
103
+ describe("loginCommand", () => {
104
+ it("usa el CLI real de cada agente (claude sin --cli del adapter)", () => {
105
+ assert.deepEqual(loginCommand("claude"), {
106
+ bin: "claude",
107
+ args: ["auth", "login"],
108
+ display: "claude auth login",
109
+ });
110
+ assert.deepEqual(loginCommand("copilot"), {
111
+ bin: "copilot",
112
+ args: ["login"],
113
+ display: "copilot login",
114
+ });
115
+ assert.deepEqual(loginCommand("cursor"), {
116
+ bin: "cursor-agent",
117
+ args: ["login"],
118
+ display: "cursor-agent login",
119
+ });
120
+ assert.equal(loginCommand("other"), null);
121
+ });
122
+
123
+ it("Copilot acepta host Enterprise opcional (elección del usuario)", () => {
124
+ assert.deepEqual(loginCommand("copilot", { host: "acme.ghe.com" }), {
125
+ bin: "copilot",
126
+ args: ["login", "--host", "acme.ghe.com"],
127
+ display: "copilot login --host acme.ghe.com",
128
+ });
129
+ assert.deepEqual(loginCommand("copilot", { host: "github.com" }).args, ["login"]);
130
+ });
131
+ });
132
+
133
+ describe("withCopilotLoginHost", () => {
134
+ it("normaliza y agrega --host; github.com queda sin flag", () => {
135
+ assert.equal(normalizeCopilotLoginHost("https://acme.ghe.com/foo"), "acme.ghe.com");
136
+ assert.equal(normalizeCopilotLoginHost("github.com"), "");
137
+ const base = { bin: "copilot", args: ["login"], display: "copilot login" };
138
+ assert.deepEqual(withCopilotLoginHost(base, "").args, ["login"]);
139
+ assert.deepEqual(withCopilotLoginHost(base, "acme.ghe.com").args, [
140
+ "login",
141
+ "--host",
142
+ "acme.ghe.com",
143
+ ]);
144
+ });
145
+ });
146
+
147
+ describe("copilot / gh logout helpers", () => {
148
+ it("detecta env overrides de token", () => {
149
+ assert.deepEqual(copilotTokenEnvOverrides({}), []);
150
+ assert.deepEqual(copilotTokenEnvOverrides({ GH_TOKEN: "x", FOO: "1" }), ["GH_TOKEN"]);
151
+ });
152
+
153
+ it("lista cuentas gh desde JSON", () => {
154
+ const accounts = listGhAuthAccounts({
155
+ run: () => ({
156
+ status: 0,
157
+ stdout: JSON.stringify({
158
+ hosts: {
159
+ "github.com": [
160
+ { login: "personal", active: true },
161
+ { login: "corp", active: false },
162
+ ],
163
+ },
164
+ }),
165
+ stderr: "",
166
+ error: undefined,
167
+ }),
168
+ });
169
+ assert.deepEqual(accounts, [
170
+ { host: "github.com", user: "personal", active: true },
171
+ { host: "github.com", user: "corp", active: false },
172
+ ]);
173
+ });
174
+
175
+ it("logoutGhAuthAccounts llama gh auth logout por cuenta", () => {
176
+ /** @type {string[][]} */
177
+ const calls = [];
178
+ const result = logoutGhAuthAccounts({
179
+ env: {},
180
+ run: (bin, args) => {
181
+ if (args?.[0] === "auth" && args?.[1] === "status") {
182
+ return {
183
+ status: 0,
184
+ stdout: JSON.stringify({
185
+ hosts: { "github.com": [{ login: "personal", active: true }] },
186
+ }),
187
+ stderr: "",
188
+ error: undefined,
189
+ };
190
+ }
191
+ calls.push([bin, ...(args || [])]);
192
+ return { status: 0, stdout: "", stderr: "", error: undefined };
193
+ },
194
+ });
195
+ assert.equal(result.loggedOut.length, 1);
196
+ assert.deepEqual(calls[0], [
197
+ "gh",
198
+ "auth",
199
+ "logout",
200
+ "--hostname",
201
+ "github.com",
202
+ "--user",
203
+ "personal",
204
+ ]);
205
+ });
206
+
207
+ it("logoutCommand solo para claude", () => {
208
+ assert.deepEqual(logoutCommand("claude")?.args, ["auth", "logout"]);
209
+ assert.equal(logoutCommand("copilot"), null);
210
+ });
211
+
212
+ it("agentAccountStatus lee cuenta activa de gh para Copilot", () => {
213
+ const status = agentAccountStatus("copilot", {
214
+ env: {},
215
+ run: () => ({
216
+ status: 0,
217
+ stdout: JSON.stringify({
218
+ hosts: { "github.com": [{ login: "corp-user", active: true }] },
219
+ }),
220
+ stderr: "",
221
+ error: undefined,
222
+ }),
223
+ });
224
+ assert.equal(status.loggedIn, true);
225
+ assert.equal(status.accountLabel, "corp-user");
226
+ assert.equal(status.source, "gh");
227
+ });
228
+ });
229
+
230
+ describe("claudeCliLoginArgs", () => {
231
+ it("saca --cli y conserva auth login + flags del método", () => {
232
+ assert.deepEqual(
233
+ claudeCliLoginArgs({
234
+ id: "claude-ai-login",
235
+ type: "terminal",
236
+ args: ["--cli", "auth", "login", "--claudeai"],
237
+ }),
238
+ ["auth", "login", "--claudeai"]
239
+ );
240
+ assert.deepEqual(
241
+ claudeCliLoginArgs({
242
+ id: "console-login",
243
+ type: "terminal",
244
+ args: ["--cli", "auth", "login", "--console"],
245
+ _meta: {
246
+ "terminal-auth": {
247
+ command: "node",
248
+ args: ["/x/adapter.js", "--cli", "auth", "login", "--console"],
249
+ },
250
+ },
251
+ }),
252
+ ["auth", "login", "--console"]
253
+ );
254
+ assert.deepEqual(claudeCliLoginArgs(null), ["auth", "login"]);
255
+ });
256
+ });
257
+
258
+ describe("looksLikeAuthFailure / authFailureMessage", () => {
259
+ it("detecta fallos de auth típicos", () => {
260
+ assert.equal(looksLikeAuthFailure("Authentication required"), true);
261
+ assert.equal(looksLikeAuthFailure("auth_required"), true);
262
+ assert.equal(looksLikeAuthFailure("not logged in"), true);
263
+ assert.equal(looksLikeAuthFailure("ENOENT"), false);
264
+ });
265
+
266
+ it("arma mensaje en español sin comando CLI", () => {
267
+ const msg = authFailureMessage("copilot", "GitHub Copilot CLI", "Authentication required");
268
+ assert.match(msg, /Necesitás iniciar sesión en GitHub Copilot CLI/);
269
+ assert.equal(/copilot login/.test(msg || ""), false);
270
+ assert.equal(authFailureMessage("copilot", "Copilot", "random crash"), null);
271
+ });
272
+ });
273
+
274
+ describe("resolveLoginCommand", () => {
275
+ const agent = {
276
+ def: {
277
+ id: "copilot",
278
+ acp: ({ path }) => ({ file: path, args: ["--acp"] }),
279
+ },
280
+ path: "copilot",
281
+ };
282
+
283
+ it("usa command/args de _meta.terminal-auth", () => {
284
+ const cmd = resolveLoginCommand(agent, {
285
+ id: "copilot-login",
286
+ _meta: {
287
+ "terminal-auth": {
288
+ command: "/usr/bin/copilot",
289
+ args: ["login"],
290
+ label: "Copilot Login",
291
+ },
292
+ },
293
+ });
294
+ assert.equal(cmd.bin, "/usr/bin/copilot");
295
+ assert.deepEqual(cmd.args, ["login"]);
296
+ assert.equal(cmd.display, "Copilot Login");
297
+ });
298
+
299
+ it("para Claude type terminal usa el binario claude, no npx/adapter", () => {
300
+ const claude = {
301
+ def: {
302
+ id: "claude",
303
+ acp: () => ({ file: "npx", args: ["--yes", "pkg"] }),
304
+ },
305
+ path: "/usr/bin/claude",
306
+ };
307
+ const cmd = resolveLoginCommand(claude, {
308
+ id: "claude-ai-login",
309
+ type: "terminal",
310
+ args: ["--cli", "auth", "login", "--claudeai"],
311
+ });
312
+ assert.equal(cmd.bin, "/usr/bin/claude");
313
+ assert.deepEqual(cmd.args, ["auth", "login", "--claudeai"]);
314
+ assert.equal(cmd.display, "claude auth login --claudeai");
315
+ });
316
+
317
+ it("para Claude con _meta.terminal-auth del adapter igual prefiere claude CLI", () => {
318
+ const claude = {
319
+ def: { id: "claude", acp: () => ({ file: "npx", args: ["--yes", "pkg"] }) },
320
+ path: "C:\\\\Users\\\\me\\\\claude.exe",
321
+ };
322
+ const cmd = resolveLoginCommand(claude, {
323
+ id: "claude-ai-login",
324
+ type: "terminal",
325
+ args: ["--cli", "auth", "login", "--claudeai"],
326
+ _meta: {
327
+ "terminal-auth": {
328
+ command: "C:\\\\Program Files\\\\nodejs\\\\node.exe",
329
+ args: [
330
+ "C:\\\\npm\\\\claude-agent-acp\\\\dist\\\\index.js",
331
+ "--cli",
332
+ "auth",
333
+ "login",
334
+ "--claudeai",
335
+ ],
336
+ label: "Claude Login",
337
+ },
338
+ },
339
+ });
340
+ assert.equal(cmd.bin, "C:\\\\Users\\\\me\\\\claude.exe");
341
+ assert.deepEqual(cmd.args, ["auth", "login", "--claudeai"]);
342
+ });
343
+
344
+ it("para type terminal sin meta (no Claude) usa launch ACP + args del método", () => {
345
+ const other = {
346
+ def: {
347
+ id: "other-agent",
348
+ acp: () => ({ file: "npx", args: ["--yes", "pkg"] }),
349
+ },
350
+ path: "other",
351
+ };
352
+ const cmd = resolveLoginCommand(other, {
353
+ id: "login",
354
+ type: "terminal",
355
+ args: ["--login"],
356
+ });
357
+ assert.equal(cmd.bin, "npx");
358
+ assert.deepEqual(cmd.args, ["--yes", "pkg", "--login"]);
359
+ });
360
+
361
+ it("cae al loginCommand del agente si no hay método", () => {
362
+ const cmd = resolveLoginCommand(agent, null);
363
+ assert.equal(cmd.display, "copilot login");
364
+ });
365
+
366
+ it("fallback Claude usa agent.path sin --cli", () => {
367
+ const claude = { def: { id: "claude" }, path: "/opt/claude" };
368
+ const cmd = resolveLoginCommand(claude, null);
369
+ assert.equal(cmd.bin, "/opt/claude");
370
+ assert.deepEqual(cmd.args, ["auth", "login"]);
371
+ });
372
+ });
373
+
374
+ describe("isAuthRequiredError / authMethodsFromError / pickLoginAuthMethod", () => {
375
+ it("detecta AcpAuthRequiredError por name", () => {
376
+ const err = Object.assign(new Error("Copilot requiere autenticación"), {
377
+ name: "AcpAuthRequiredError",
378
+ methods: [{ id: "copilot-login", _meta: { "terminal-auth": { command: "copilot", args: ["login"] } } }],
379
+ });
380
+ assert.equal(isAuthRequiredError(err), true);
381
+ assert.equal(authMethodsFromError(err).length, 1);
382
+ });
383
+
384
+ it("detecta texto de auth sin clase especial", () => {
385
+ assert.equal(isAuthRequiredError(new Error("not logged in")), true);
386
+ assert.equal(isAuthRequiredError(new Error("ENOENT")), false);
387
+ assert.equal(authMethodsFromError(new Error("x")).length, 0);
388
+ });
389
+
390
+ it("elige terminal-meta antes que terminal; null si solo agent", () => {
391
+ assert.equal(
392
+ pickLoginAuthMethod([
393
+ { id: "cursor_login" },
394
+ { id: "claude", type: "terminal" },
395
+ { id: "copilot-login", _meta: { "terminal-auth": { command: "copilot", args: ["login"] } } },
396
+ ])?.id,
397
+ "copilot-login"
398
+ );
399
+ assert.equal(pickLoginAuthMethod([{ id: "claude", type: "terminal" }])?.id, "claude");
400
+ assert.equal(pickLoginAuthMethod([{ id: "cursor_login" }]), null);
401
+ assert.equal(pickLoginAuthMethod([]), null);
402
+ });
403
+ });
404
+
405
+ describe("canInheritLoginTty", () => {
406
+ it("respeta external: true aunque haya TTY", () => {
407
+ assert.equal(canInheritLoginTty({ external: true }), false);
408
+ });
409
+ });
410
+
411
+ describe("resolveLoginLaunchMode", () => {
412
+ it("con wait y external: true fuerza external (no inherit)", () => {
413
+ assert.equal(resolveLoginLaunchMode({ wait: true, external: true }), "external");
414
+ });
415
+
416
+ it("sin wait ni external usa hidden (web / OAuth sin ventana)", () => {
417
+ assert.equal(resolveLoginLaunchMode({}), "hidden");
418
+ assert.equal(resolveLoginLaunchMode({ wait: false }), "hidden");
419
+ });
420
+
421
+ it("con wait y sin external: inherit si hay TTY, si no hidden", () => {
422
+ const mode = resolveLoginLaunchMode({ wait: true });
423
+ const hasTty = Boolean(process.stdin?.isTTY && process.stdout?.isTTY);
424
+ assert.equal(mode, hasTty ? "inherit" : "hidden");
425
+ });
426
+ });
427
+
428
+ describe("withAgentLoginRetry", () => {
429
+ const agent = {
430
+ def: { id: "copilot", label: "GitHub Copilot CLI" },
431
+ path: "copilot",
432
+ };
433
+
434
+ it("devuelve value si op ok", async () => {
435
+ const out = await withAgentLoginRetry(async () => 42, {
436
+ agent,
437
+ askToLogin: async () => {
438
+ throw new Error("no debería preguntar");
439
+ },
440
+ });
441
+ assert.deepEqual(out, { ok: true, value: 42 });
442
+ });
443
+
444
+ it("cancela sin lanzar login si el usuario dice no", async () => {
445
+ const err = Object.assign(new Error("Authentication required"), {
446
+ name: "AcpAuthRequiredError",
447
+ methods: [],
448
+ });
449
+ let ranTerminal = false;
450
+ const out = await withAgentLoginRetry(async () => {
451
+ throw new Error("no retry");
452
+ }, {
453
+ agent,
454
+ priorError: err,
455
+ askToLogin: async ({ displayCmd }) => {
456
+ assert.match(displayCmd, /copilot login/);
457
+ return false;
458
+ },
459
+ runTerminal: async () => {
460
+ ranTerminal = true;
461
+ return { display: "x", code: 0, mode: "hidden" };
462
+ },
463
+ });
464
+ assert.equal(out.ok, false);
465
+ assert.equal(out.cancelled, true);
466
+ assert.equal(ranTerminal, false);
467
+ });
468
+
469
+ it("abre login, espera exit 0 y reintenta op", async () => {
470
+ const err = Object.assign(new Error("Authentication required"), {
471
+ name: "AcpAuthRequiredError",
472
+ methods: [
473
+ {
474
+ id: "copilot-login",
475
+ _meta: { "terminal-auth": { command: "copilot", args: ["login"], label: "Copilot Login" } },
476
+ },
477
+ ],
478
+ });
479
+ let attempts = 0;
480
+ /** @type {string[]} */
481
+ const statuses = [];
482
+ const out = await withAgentLoginRetry(async () => {
483
+ attempts += 1;
484
+ return "models";
485
+ }, {
486
+ agent,
487
+ priorError: err,
488
+ askToLogin: async () => true,
489
+ onStatus: (msg) => statuses.push(msg),
490
+ runTerminal: async (cmd) => {
491
+ assert.equal(cmd.display, "Copilot Login");
492
+ return { display: cmd.display, code: 0, mode: "inherit" };
493
+ },
494
+ });
495
+ assert.deepEqual(out, { ok: true, value: "models" });
496
+ assert.equal(attempts, 1);
497
+ assert.ok(statuses.some((s) => /Esperando que completes el inicio de sesión/.test(s)));
498
+ assert.ok(statuses.some((s) => /Login ok/.test(s)));
499
+ });
500
+
501
+ it("no dice Login ok si el exit code no es 0", async () => {
502
+ const err = Object.assign(new Error("Authentication required"), {
503
+ name: "AcpAuthRequiredError",
504
+ methods: [],
505
+ });
506
+ /** @type {string[]} */
507
+ const statuses = [];
508
+ const out = await withAgentLoginRetry(async () => {
509
+ throw new Error("no debería reintentar");
510
+ }, {
511
+ agent,
512
+ priorError: err,
513
+ askToLogin: async () => true,
514
+ onStatus: (msg) => statuses.push(msg),
515
+ runTerminal: async () => ({ display: "copilot login", code: 1, mode: "inherit" }),
516
+ });
517
+ assert.equal(out.ok, false);
518
+ assert.equal(out.cancelled, false);
519
+ assert.match(
520
+ String(/** @type {{ error: Error }} */ (out).error.message),
521
+ /No se pudo completar el inicio de sesión/
522
+ );
523
+ assert.equal(statuses.some((s) => /Login ok/.test(s)), false);
524
+ });
525
+
526
+ it("no ofrece login si el error no es de auth", async () => {
527
+ const out = await withAgentLoginRetry(async () => {
528
+ throw new Error("boom");
529
+ }, {
530
+ agent,
531
+ askToLogin: async () => true,
532
+ runTerminal: async () => ({ display: "x", code: 0, mode: "hidden" }),
533
+ });
534
+ assert.equal(out.ok, false);
535
+ assert.equal(out.cancelled, false);
536
+ assert.match(String(/** @type {{ error: Error }} */ (out).error.message), /boom/);
537
+ });
538
+ });
package/lib/agents.mjs CHANGED
@@ -50,16 +50,6 @@ function readJson(file) {
50
50
  }
51
51
  }
52
52
 
53
- /** Primer `model = "..."` de nivel raíz de un TOML (antes de cualquier `[sección]`). */
54
- export function topLevelTomlModel(text) {
55
- for (const line of String(text).split(/\r?\n/)) {
56
- if (/^\s*\[/.test(line)) break;
57
- const m = line.match(/^\s*model\s*=\s*["']([^"']+)["']/);
58
- if (m) return m[1];
59
- }
60
- return null;
61
- }
62
-
63
53
  /** Versiones tipo `2.1.286` de mayor a menor (las que no parsean, al final). */
64
54
  function byVersionDesc(a, b) {
65
55
  const pa = a.split(".").map(Number);
@@ -180,22 +170,6 @@ export const AGENTS = [
180
170
  };
181
171
  },
182
172
  },
183
- {
184
- id: "codex",
185
- label: "Codex",
186
- bins: ["codex"],
187
- models: [],
188
- configuredModel: ({ home }) => {
189
- try {
190
- const model = topLevelTomlModel(readFileSync(join(home, ".codex", "config.toml"), "utf8"));
191
- return model ? { model, source: "~/.codex/config.toml" } : null;
192
- } catch {
193
- return null;
194
- }
195
- },
196
- // `-` = prompt por stdin; workspace-write deja escribir el JSON en el repo.
197
- args: ({ model }) => ["exec", "--sandbox", "workspace-write", ...(model ? ["-m", model] : []), "-"],
198
- },
199
173
  {
200
174
  id: "cursor",
201
175
  label: "Cursor CLI",
@@ -246,19 +220,6 @@ export const AGENTS = [
246
220
  // Modo ACP nativo. Su opción de sesión `allow_all` queda apagada: cada permiso pasa por nuestra política.
247
221
  acp: ({ path }) => ({ file: path, args: ["--acp"] }),
248
222
  },
249
- {
250
- id: "gemini",
251
- label: "Gemini CLI",
252
- bins: ["gemini"],
253
- models: [],
254
- configuredModel: ({ home }) => {
255
- const model = readJson(join(home, ".gemini", "settings.json"))?.model;
256
- const name = typeof model === "string" ? model : model?.name;
257
- return typeof name === "string" && name ? { model: name, source: "~/.gemini/settings.json" } : null;
258
- },
259
- // Sin TTY lee el prompt de stdin; yolo porque no hay quién apruebe los comandos.
260
- args: ({ model }) => [...(model ? ["-m", model] : []), "--yolo"],
261
- },
262
223
  ];
263
224
 
264
225
  /** @param {string} file @param {NodeJS.Platform} platform */
@@ -9,11 +9,10 @@ import {
9
9
  detectAgents,
10
10
  findOnPath,
11
11
  modelChoices,
12
- topLevelTomlModel,
13
12
  } from "./agents.mjs";
13
+ import { supportsAcp } from "./acp.mjs";
14
14
 
15
15
  const claude = AGENTS.find((a) => a.id === "claude");
16
- const codex = AGENTS.find((a) => a.id === "codex");
17
16
  const cursor = AGENTS.find((a) => a.id === "cursor");
18
17
  const copilot = AGENTS.find((a) => a.id === "copilot");
19
18
 
@@ -27,20 +26,32 @@ function touch(file, content = "") {
27
26
  return file;
28
27
  }
29
28
 
29
+ describe("AGENTS", () => {
30
+ it("solo hay agentes ACP (Claude, Cursor, Copilot)", () => {
31
+ assert.deepEqual(
32
+ AGENTS.map((a) => a.id),
33
+ ["claude", "cursor", "copilot"]
34
+ );
35
+ for (const def of AGENTS) {
36
+ assert.equal(supportsAcp({ def, path: def.bins[0] }), true, `${def.id} debe tener acp()`);
37
+ }
38
+ });
39
+ });
40
+
30
41
  describe("findOnPath", () => {
31
42
  it("en Windows prueba las extensiones de PATHEXT (shims .cmd de npm)", () => {
32
43
  const root = sandbox();
33
44
  const bin = join(root, "npm");
34
- touch(join(bin, "codex.cmd"));
35
- const found = findOnPath("codex", {
45
+ touch(join(bin, "foocli.cmd"));
46
+ const found = findOnPath("foocli", {
36
47
  env: { PATH: `${join(root, "nada")};${bin}`, PATHEXT: ".EXE;.CMD" },
37
48
  platform: "win32",
38
49
  });
39
- assert.equal(found, join(bin, "codex.cmd"));
50
+ assert.equal(found, join(bin, "foocli.cmd"));
40
51
  });
41
52
 
42
53
  it("devuelve null si no está", () => {
43
- assert.equal(findOnPath("codex", { env: { PATH: sandbox() }, platform: "linux" }), null);
54
+ assert.equal(findOnPath("foocli", { env: { PATH: sandbox() }, platform: "linux" }), null);
44
55
  });
45
56
  });
46
57
 
@@ -105,20 +116,6 @@ describe("modelChoices", () => {
105
116
  });
106
117
  assert.equal(options[0].label, "Por defecto (opus)");
107
118
  });
108
-
109
- it("codex lee el model de nivel raíz de ~/.codex/config.toml", () => {
110
- const home = sandbox();
111
- touch(join(home, ".codex", "config.toml"), 'model = "gpt-x"\n[profiles.otro]\nmodel = "nope"\n');
112
- const { options } = modelChoices(codex, { env: {}, platform: "linux", home });
113
- assert.equal(options[0].label, "Por defecto (gpt-x)");
114
- });
115
- });
116
-
117
- describe("topLevelTomlModel", () => {
118
- it("ignora los model dentro de secciones", () => {
119
- assert.equal(topLevelTomlModel('[profiles.a]\nmodel = "x"\n'), null);
120
- assert.equal(topLevelTomlModel('approval = "never"\nmodel = \'y\'\n'), "y");
121
- });
122
119
  });
123
120
 
124
121
  describe("args", () => {
@@ -128,10 +125,6 @@ describe("args", () => {
128
125
  assert.ok(args.includes("Edit(./.diff-review/diff-rev_a_b.json)"));
129
126
  assert.ok(!claude.args({ outputFilename: "x.json" }).includes("--model"));
130
127
  });
131
-
132
- it("codex: lee el prompt de stdin", () => {
133
- assert.equal(codex.args({ outputFilename: "x.json" }).at(-1), "-");
134
- });
135
128
  });
136
129
 
137
130
  describe("cursor", () => {