@dforce2055/dai 0.13.0 → 0.13.1
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/CHANGELOG.md +42 -0
- package/VERSION +1 -1
- package/cli/dai.mjs +52 -3
- package/cli/lib/git-ssh.mjs +107 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,47 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.13.1] — 2026-08-24
|
|
7
|
+
|
|
8
|
+
**Un dev en Windows no podía pushear contra el GitLab de su empresa. `ssh -T` le autenticaba
|
|
9
|
+
perfecto, `git push` moría con `Permission denied (publickey…)`. Dos días buscando el problema
|
|
10
|
+
en la clave, en el token y en los permisos del server: no estaba en ninguno de los tres, y dai
|
|
11
|
+
tapaba la única línea que lo decía.**
|
|
12
|
+
|
|
13
|
+
Su clave tenía passphrase. En Windows conviven dos `ssh.exe` — el de OpenSSH for Windows, que
|
|
14
|
+
habla con el servicio `ssh-agent`, y el que trae Git for Windows, que no lo ve. `ssh -T` usaba
|
|
15
|
+
el primero (la firma la hacía el agente, passphrase nunca), `git push` usaba el segundo y pedía
|
|
16
|
+
la passphrase por stderr. Ese prompt caía en un pipe de dai: el dev veía un cuelgue sin
|
|
17
|
+
explicación, ssh se rendía, caía a autenticación por password y lo único legible al final era
|
|
18
|
+
un error que acusa a la clave. Todo lo que hacía falta para resolverlo estaba en pantalla, y no
|
|
19
|
+
llegaba.
|
|
20
|
+
|
|
21
|
+
### Arreglado
|
|
22
|
+
- **`dai pr` se tragaba lo que git y ssh preguntan.** El push corría con `stderr` en `pipe`
|
|
23
|
+
para no ensuciar la salida, pero git y ssh **preguntan por stderr**: la passphrase de una
|
|
24
|
+
clave, la confirmación de un host nuevo, el aviso del credential manager. Con el prompt
|
|
25
|
+
invisible el comando no se cuelga por un bug, se cuelga esperando una respuesta que nadie
|
|
26
|
+
sabe que tiene que dar. Ahora `stderr` va heredado y el push pregunta a la vista. El error de
|
|
27
|
+
git se lee en vivo, cuando todavía sirve, en vez de aparecer resumido después del fracaso.
|
|
28
|
+
- **La pista al fallar el push asumía que el remoto era HTTPS.** Decía siempre *"si es la
|
|
29
|
+
primera vez contra este remoto, autenticá pusheando a mano una vez"*. Eso arregla HTTPS,
|
|
30
|
+
donde el credential manager pide la credencial la primera vez. Contra un remoto **SSH** el
|
|
31
|
+
push a mano falla exactamente igual — así que la pista mandaba a repetir un comando condenado
|
|
32
|
+
y a seguir buscando en el lugar equivocado. Ahora el consejo depende del transporte: en SSH
|
|
33
|
+
aclara que el token de `gh`/`glab` no interviene en el push, que lo que hay que mirar es la
|
|
34
|
+
clave, y deriva a `dai doctor`.
|
|
35
|
+
|
|
36
|
+
### Agregado
|
|
37
|
+
- **`dai doctor` — sección `forge`.** Reporta el remoto `origin` con su forge detectado y, en
|
|
38
|
+
Windows con remoto SSH, **qué `ssh.exe` va a usar git**: si es el suyo (el de Git for
|
|
39
|
+
Windows, que no llega al `ssh-agent`), lo advierte, explica el modo de falla y da el fix
|
|
40
|
+
(`git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"`). Respeta la
|
|
41
|
+
precedencia real de git —`GIT_SSH_COMMAND` > `GIT_SSH` > `core.sshCommand`— y avisa cuando
|
|
42
|
+
una variable de entorno está pisando un `core.sshCommand` que ya estaba bien: ese caso es
|
|
43
|
+
particularmente cruel, porque el dev arregla el config, no funciona, y mirando el
|
|
44
|
+
`.gitconfig` no hay nada que ver. Fuera de Windows, o con un remoto HTTPS, no opina.
|
|
45
|
+
Núcleo puro y testeado en `cli/lib/git-ssh.mjs` (14 tests); en `dai.mjs` queda solo el I/O.
|
|
46
|
+
|
|
6
47
|
## [0.13.0] — 2026-08-21
|
|
7
48
|
|
|
8
49
|
**Un dev de backend en Windows siguió el tutorial al pie de la letra y el agente se puso a
|
|
@@ -696,6 +737,7 @@ ClickUp y Jira Cloud.
|
|
|
696
737
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
697
738
|
`files` de npm sin tests ni secretos.
|
|
698
739
|
|
|
740
|
+
[0.13.1]: https://github.com/dforce2055/dai/releases/tag/v0.13.1
|
|
699
741
|
[0.13.0]: https://github.com/dforce2055/dai/releases/tag/v0.13.0
|
|
700
742
|
[0.12.0]: https://github.com/dforce2055/dai/releases/tag/v0.12.0
|
|
701
743
|
[0.11.0]: https://github.com/dforce2055/dai/releases/tag/v0.11.0
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.13.
|
|
1
|
+
0.13.1
|
package/cli/dai.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { parsePrRef, getPR, postComment, postReview } from "./lib/forge-api.mjs"
|
|
|
28
28
|
import { trackerUrl } from "./lib/tracker-url.mjs";
|
|
29
29
|
import { parseFindings, diffPositions, validateFindings, filterFindings, renderFindingBody, renderReviewSummary } from "./lib/review-findings.mjs";
|
|
30
30
|
import { composePrBody, prTitle, forgeTool } from "./lib/pr.mjs";
|
|
31
|
+
import { diagnoseGitSsh, pushFailureHint, WINDOWS_OPENSSH } from "./lib/git-ssh.mjs";
|
|
31
32
|
import { dirsEqual } from "./lib/fsutil.mjs";
|
|
32
33
|
import { parseFlags, parseAssistants, isAssistantToken, asList } from "./lib/args.mjs";
|
|
33
34
|
import { versionDrift, planUpgrade, compareVersions } from "./lib/semver.mjs";
|
|
@@ -990,11 +991,21 @@ async function cmdPr(opts) {
|
|
|
990
991
|
// stdin heredado + GIT_TERMINAL_PROMPT=1: la PRIMERA vez contra un remoto HTTPS
|
|
991
992
|
// corporativo, git/credential-manager necesita poder pedir la credencial. Con stdin
|
|
992
993
|
// ignorado (el default de git()) el login no completaba y el push fallaba en seco.
|
|
993
|
-
|
|
994
|
+
//
|
|
995
|
+
// stderr heredado, y esto no es cosmético: git y ssh PREGUNTAN por stderr. Con
|
|
996
|
+
// stderr en 'pipe' el prompt de una clave con passphrase ("Enter passphrase for
|
|
997
|
+
// key …") caía en el pipe, invisible: el dev veía un cuelgue sin explicación, ssh
|
|
998
|
+
// se rendía y terminaba cayendo a autenticación por password, y lo único que se
|
|
999
|
+
// llegaba a leer era un "Permission denied (publickey…)" que acusa a la clave
|
|
1000
|
+
// cuando la clave estaba bien. Que pregunte a la vista.
|
|
1001
|
+
git(["push", "-u", "origin", branch], { stdio: ["inherit", "pipe", "inherit"], env: { ...process.env, GIT_TERMINAL_PROMPT: "1" } });
|
|
994
1002
|
} catch (e) {
|
|
995
|
-
|
|
1003
|
+
// git ya imprimió su error en vivo (stderr heredado); acá solo va la pista, y esa
|
|
1004
|
+
// pista depende del transporte: "pushea a mano una vez" arregla HTTPS y no arregla
|
|
1005
|
+
// nada en SSH, donde el push a mano falla igual (lib/git-ssh.mjs).
|
|
1006
|
+
const err = String(e.stderr || "").trim();
|
|
996
1007
|
if (err) process.stderr.write(" " + err.split("\n").join("\n ") + "\n");
|
|
997
|
-
process.stdout.write(`
|
|
1008
|
+
for (const l of pushFailureHint(remote, branch)) process.stdout.write(` ${l}\n`);
|
|
998
1009
|
fail(`no pude pushear la branch '${branch}'.`, 1);
|
|
999
1010
|
}
|
|
1000
1011
|
|
|
@@ -1728,6 +1739,44 @@ function cmdDoctor() {
|
|
|
1728
1739
|
: warn("DAI_CLICKUP_LIST_ID vacío — solo hace falta para `dai publish` (crear tareas)");
|
|
1729
1740
|
}
|
|
1730
1741
|
|
|
1742
|
+
// ── forge: el CÓMO sale del repo por acá ─────────────────────────────────────
|
|
1743
|
+
// Si el push no sale, no hay PR; sin PR no hay link QUÉ↔CÓMO. El chequeo del cliente
|
|
1744
|
+
// ssh existe porque su modo de fallar miente: ver lib/git-ssh.mjs.
|
|
1745
|
+
const remote = gitRemote();
|
|
1746
|
+
info("forge:");
|
|
1747
|
+
if (!remote) warn("no hay remoto 'origin' — `dai pr` no tiene dónde publicar la branch");
|
|
1748
|
+
else {
|
|
1749
|
+
const p = parseRemote(remote);
|
|
1750
|
+
ok(`remoto origin: ${remote}${p ? ` (${detectForge(p.host)})` : ""}`);
|
|
1751
|
+
let sshConfig = null;
|
|
1752
|
+
try { sshConfig = git(["config", "--get", "core.sshCommand"]) || null; } catch { /* sin valor: git sale 1 */ }
|
|
1753
|
+
const d = diagnoseGitSsh({
|
|
1754
|
+
platform: process.platform,
|
|
1755
|
+
remote,
|
|
1756
|
+
config: sshConfig,
|
|
1757
|
+
env: { GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND, GIT_SSH: process.env.GIT_SSH },
|
|
1758
|
+
hasWindowsOpenSsh: existsSync(WINDOWS_OPENSSH),
|
|
1759
|
+
});
|
|
1760
|
+
if (d.status === "ok") ok(`git usa el OpenSSH de Windows (ve al ssh-agent)`);
|
|
1761
|
+
if (d.status === "ok-por-env") {
|
|
1762
|
+
ok(`git usa el OpenSSH de Windows (ve al ssh-agent)`);
|
|
1763
|
+
warn(`pero viene de ${d.source}, no del config: se pierde al cerrar la terminal.`);
|
|
1764
|
+
process.stdout.write(` Para que quede: git config --global core.sshCommand "${WINDOWS_OPENSSH}"\n`);
|
|
1765
|
+
}
|
|
1766
|
+
if (d.status === "bundled" || d.status === "otro-ssh") {
|
|
1767
|
+
warn(d.status === "bundled"
|
|
1768
|
+
? "git usa su propio ssh (Git for Windows), que NO ve al ssh-agent de Windows."
|
|
1769
|
+
: `git usa ${d.bin} (por ${d.source}), que probablemente no vea al ssh-agent de Windows.`);
|
|
1770
|
+
process.stdout.write(` Si tu clave tiene passphrase, cada push te la va a pedir — y si el prompt no llega,\n`);
|
|
1771
|
+
process.stdout.write(` ssh cae a password y falla con "Permission denied (publickey…)", que acusa a la clave.\n`);
|
|
1772
|
+
if (d.fix) process.stdout.write(` → git config --global core.sshCommand "${d.fix}"\n`);
|
|
1773
|
+
if (d.shadowed) process.stdout.write(` Ojo: ${d.source} está seteada y pisa tu core.sshCommand. Límpiala primero.\n`);
|
|
1774
|
+
}
|
|
1775
|
+
if (d.status === "bundled-sin-openssh") {
|
|
1776
|
+
warn("git usa su propio ssh y no encontré el OpenSSH de Windows: si el push pide passphrase, instálalo (Configuración → Características opcionales → Cliente OpenSSH).");
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1731
1780
|
// ── version-drift del scaffold vs el CLI (ADR-0010) ──────────────────────────
|
|
1732
1781
|
if (existsSync(join(process.cwd(), ".dai", "VERSION"))) { info("versión del scaffold:"); reportDrift(); }
|
|
1733
1782
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// dai · qué cliente SSH usa git, y si ese cliente llega al agente (Windows).
|
|
2
|
+
//
|
|
3
|
+
// En Windows conviven dos ssh.exe: el de OpenSSH for Windows (C:\Windows\System32\
|
|
4
|
+
// OpenSSH), que habla con el servicio `ssh-agent`, y el que trae Git for Windows
|
|
5
|
+
// (MSYS), que NO lo ve. Con una clave con passphrase el síntoma es desconcertante:
|
|
6
|
+
// `ssh -T` autentica (la firma la hace el agente) y `git push` pide la passphrase o,
|
|
7
|
+
// si nadie la contesta, cae a autenticación por password y muere con
|
|
8
|
+
// "Permission denied (publickey…)" — un error que apunta al lugar equivocado y se
|
|
9
|
+
// come una mañana buscando el problema en la clave, en el token o en el server.
|
|
10
|
+
//
|
|
11
|
+
// Núcleo puro: el I/O (git config, existsSync) vive en dai.mjs.
|
|
12
|
+
|
|
13
|
+
export const WINDOWS_OPENSSH = "C:/Windows/System32/OpenSSH/ssh.exe";
|
|
14
|
+
|
|
15
|
+
// ¿Por dónde habla el remoto? Decide qué consejo tiene sentido cuando el push falla.
|
|
16
|
+
export function remoteTransport(remote) {
|
|
17
|
+
const r = String(remote ?? "").trim();
|
|
18
|
+
if (!r) return null;
|
|
19
|
+
if (/^ssh:\/\//i.test(r)) return "ssh";
|
|
20
|
+
if (/^https?:\/\//i.test(r)) return "https";
|
|
21
|
+
if (/^[\w.-]+@[^:]+:/.test(r)) return "ssh"; // scp-like: git@host:org/repo.git
|
|
22
|
+
return null; // git://, file://, ruta local…
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// El binario de un core.sshCommand / GIT_SSH_COMMAND, que puede traer argumentos y
|
|
26
|
+
// comillas: `"C:/Program Files/Git/usr/bin/ssh.exe" -v` → la ruta sola.
|
|
27
|
+
export function sshBinaryOf(cmd) {
|
|
28
|
+
const s = String(cmd ?? "").trim();
|
|
29
|
+
if (!s) return null;
|
|
30
|
+
if (s[0] === '"' || s[0] === "'") {
|
|
31
|
+
const end = s.indexOf(s[0], 1);
|
|
32
|
+
return end === -1 ? s.slice(1) : s.slice(1, end);
|
|
33
|
+
}
|
|
34
|
+
const sp = s.search(/\s/);
|
|
35
|
+
return sp === -1 ? s : s.slice(0, sp);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ¿Esa ruta es el OpenSSH de Windows? Normalizado: mayúsculas y `\` vs `/` varían
|
|
39
|
+
// según quién haya escrito el config (git acepta las dos formas).
|
|
40
|
+
export function isWindowsOpenSsh(bin) {
|
|
41
|
+
const p = String(bin ?? "").replace(/\\/g, "/").toLowerCase();
|
|
42
|
+
return /(^|\/)system32\/openssh\/ssh(\.exe)?$/.test(p);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Diagnóstico del cliente ssh que va a usar git. Entradas ya resueltas por el
|
|
46
|
+
// llamador; acá no se toca el disco ni se lanza un proceso.
|
|
47
|
+
// platform — process.platform
|
|
48
|
+
// remote — url de origin (o null)
|
|
49
|
+
// config — `git config --get core.sshCommand` (o null si no está)
|
|
50
|
+
// env — { GIT_SSH_COMMAND, GIT_SSH }
|
|
51
|
+
// hasWindowsOpenSsh — existsSync(WINDOWS_OPENSSH)
|
|
52
|
+
//
|
|
53
|
+
// status:
|
|
54
|
+
// n/a → no aplica (no es Windows, o el remoto no habla SSH)
|
|
55
|
+
// ok → apunta al OpenSSH de Windows por core.sshCommand
|
|
56
|
+
// ok-por-env → apunta bien, pero desde una env var: se pierde al cerrar la terminal
|
|
57
|
+
// otro-ssh → apunta a otro ssh (el de MSYS, plink…)
|
|
58
|
+
// bundled → nadie lo configuró: git usa el suyo, que no ve el agente
|
|
59
|
+
// bundled-sin-openssh → igual, pero no hay OpenSSH de Windows que recomendar
|
|
60
|
+
export function diagnoseGitSsh({ platform, remote, config, env = {}, hasWindowsOpenSsh = false } = {}) {
|
|
61
|
+
if (platform !== "win32") return { status: "n/a", reason: "no-windows" };
|
|
62
|
+
const transport = remoteTransport(remote);
|
|
63
|
+
if (transport !== "ssh") return { status: "n/a", reason: transport ? "remoto-https" : "sin-remoto" };
|
|
64
|
+
|
|
65
|
+
// Precedencia real de git: GIT_SSH_COMMAND > GIT_SSH > core.sshCommand.
|
|
66
|
+
const [source, raw] =
|
|
67
|
+
env.GIT_SSH_COMMAND ? ["GIT_SSH_COMMAND", env.GIT_SSH_COMMAND]
|
|
68
|
+
: env.GIT_SSH ? ["GIT_SSH", env.GIT_SSH]
|
|
69
|
+
: config ? ["core.sshCommand", config]
|
|
70
|
+
: ["default", null];
|
|
71
|
+
|
|
72
|
+
// Una env var pisando un core.sshCommand que ya estaba bien: el dev "arregló" el
|
|
73
|
+
// config, no funciona, y no hay forma de verlo mirando el .gitconfig.
|
|
74
|
+
const shadowed = source !== "default" && source !== "core.sshCommand" && Boolean(config);
|
|
75
|
+
|
|
76
|
+
if (source === "default") {
|
|
77
|
+
return hasWindowsOpenSsh
|
|
78
|
+
? { status: "bundled", source, bin: null, fix: WINDOWS_OPENSSH, shadowed }
|
|
79
|
+
: { status: "bundled-sin-openssh", source, bin: null, fix: null, shadowed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bin = sshBinaryOf(raw);
|
|
83
|
+
if (isWindowsOpenSsh(bin)) {
|
|
84
|
+
return { status: source === "core.sshCommand" ? "ok" : "ok-por-env", source, bin, fix: null, shadowed };
|
|
85
|
+
}
|
|
86
|
+
return { status: "otro-ssh", source, bin, fix: hasWindowsOpenSsh ? WINDOWS_OPENSSH : null, shadowed };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Qué decirle al dev cuando `git push` falla. El consejo de "pusheá a mano una vez"
|
|
90
|
+
// solo aplica a HTTPS, donde el credential manager pide la credencial la primera vez;
|
|
91
|
+
// contra un remoto SSH el push a mano falla EXACTAMENTE igual, así que mandarlo por ahí
|
|
92
|
+
// es hacerle perder el tiempo mientras el problema real (la clave, el agente) sigue ahí.
|
|
93
|
+
export function pushFailureHint(remote, branch) {
|
|
94
|
+
if (remoteTransport(remote) === "ssh") {
|
|
95
|
+
return [
|
|
96
|
+
`El remoto habla SSH: en el push no interviene el token de gh/glab, solo tu clave.`,
|
|
97
|
+
`Prueba a mano — así ves lo que ssh pregunta (p. ej. la passphrase de tu clave):`,
|
|
98
|
+
` git push -u origin ${branch}`,
|
|
99
|
+
`Si te la pide en cada push, tu clave está en un agente que git no ve: dai doctor`,
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
return [
|
|
103
|
+
`Si es la primera vez contra este remoto, autentica pusheando a mano una vez:`,
|
|
104
|
+
` git push -u origin ${branch}`,
|
|
105
|
+
`y vuelve a ejecutar: dai pr`,
|
|
106
|
+
];
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforce2055/dai",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Metodología de desarrollo asistido por IA — CLI de acciones deterministas (trazabilidad QUÉ↔CÓMO).",
|
|
5
5
|
"repository": { "type": "git", "url": "git+https://github.com/dforce2055/dai.git" },
|
|
6
6
|
"homepage": "https://dforce2055.github.io/dai/",
|