jorgex-stack 1.0.19 → 1.0.20
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/package.json
CHANGED
package/stack/hooks/hooks.json
CHANGED
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
"timeout": 30
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"matcher": "Bash|PowerShell|EnterWorktree|ExitWorktree",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"{{SCRIPTS_DIR}}/repair-worktree-config.cjs\"",
|
|
22
|
+
"timeout": 10
|
|
23
|
+
}
|
|
24
|
+
]
|
|
15
25
|
}
|
|
16
26
|
]
|
|
17
27
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Repara la config de git que el harness de worktrees de los agentes puede dejar
|
|
3
|
+
// corrupta cuando hay varios worktrees activos en la misma sesion:
|
|
4
|
+
// - core.bare=true en el repo principal -> rompe rev-parse --show-toplevel / marca (bare)
|
|
5
|
+
// - core.worktree apuntando a OTRO worktree -> show-toplevel/status/add operan sobre
|
|
6
|
+
// el working-tree equivocado (branch/HEAD/git-dir siguen correctos: sintoma enganoso)
|
|
7
|
+
//
|
|
8
|
+
// GATE DE ALCANCE: el bug SOLO lo causa el harness de worktrees, asi que este guardian
|
|
9
|
+
// solo actua en repos con worktrees ENLAZADOS. Un repo normal (o uno con layout legitimo
|
|
10
|
+
// --separate-git-dir sin worktrees) NUNCA se toca, aunque el hook se publique y corra en
|
|
11
|
+
// cada tool call de cualquier repo.
|
|
12
|
+
//
|
|
13
|
+
// El directorio del proyecto se toma del payload de stdin (data.cwd/directory/
|
|
14
|
+
// activeWorktreePath), igual que post-pr-review.cjs: en OpenCode el hook se ejecuta con
|
|
15
|
+
// cwd = dir de config, no el del proyecto, asi que fiarse de process.cwd() no repararia
|
|
16
|
+
// el repo correcto. Fallback a process.cwd() (Claude Code / Codex ya corren ahi).
|
|
17
|
+
//
|
|
18
|
+
// Cross-agent (Claude Code / Codex / OpenCode) y cross-platform: node puro, sin deps.
|
|
19
|
+
// Idempotente, INDEPENDIENTE del cwd de escritura (opera con --file sobre rutas absolutas),
|
|
20
|
+
// fail-open y exit 0 SIEMPRE (un guardian nunca debe bloquear una tool). Como el guardian
|
|
21
|
+
// hermano (block-destructive-git.cjs), un fail-open deja rastro en stderr para ser diagnosticable.
|
|
22
|
+
|
|
23
|
+
const { execFileSync } = require("node:child_process");
|
|
24
|
+
const fs = require("node:fs");
|
|
25
|
+
const path = require("node:path");
|
|
26
|
+
|
|
27
|
+
const TAG = "[repair-worktree-config]";
|
|
28
|
+
|
|
29
|
+
/** Ejecuta git en `cwd` y devuelve stdout recortado, o null si falla (fail-open). */
|
|
30
|
+
function git(args, cwd) {
|
|
31
|
+
try {
|
|
32
|
+
return execFileSync("git", args, {
|
|
33
|
+
cwd: cwd || undefined,
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
36
|
+
}).trim();
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Diagnostico a stderr (no bloquea): un no-op/fallo silencioso es indetectable. */
|
|
43
|
+
function warn(message) {
|
|
44
|
+
try {
|
|
45
|
+
process.stderr.write(`${TAG} ${message}\n`);
|
|
46
|
+
} catch {
|
|
47
|
+
/* stderr cerrado: no hay nada que hacer */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** .git comun (compartido por todos los worktrees). Fiable aun con core.worktree
|
|
52
|
+
* envenenado: este afecta al work-tree, no al git-dir. */
|
|
53
|
+
function gitCommonDir(cwd) {
|
|
54
|
+
const abs = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
|
|
55
|
+
if (abs) return abs;
|
|
56
|
+
const rel = git(["rev-parse", "--git-common-dir"], cwd);
|
|
57
|
+
return rel ? path.resolve(cwd, rel) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Quita core.worktree de `file` si esta puesto. Devuelve la etiqueta reparada o null.
|
|
61
|
+
* Verifica el resultado: un unset fallido se avisa por stderr y NO se reporta reparado.
|
|
62
|
+
* Las ops --file son independientes del cwd (rutas absolutas). */
|
|
63
|
+
function unsetWorktree(file, label) {
|
|
64
|
+
if (!git(["config", "--file", file, "--get", "core.worktree"])) return null;
|
|
65
|
+
if (git(["config", "--file", file, "--unset-all", "core.worktree"]) === null) {
|
|
66
|
+
warn(`fallo al quitar core.worktree(${label}) - sigue puesto en ${file}`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return `core.worktree(${label})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function repair(cwd) {
|
|
73
|
+
const common = gitCommonDir(cwd);
|
|
74
|
+
if (!common) {
|
|
75
|
+
// Distinguir "git no disponible" de "no es un repo": un guardian muerto por
|
|
76
|
+
// falta de git seria un no-op PERMANENTE e invisible en cada tool call.
|
|
77
|
+
if (git(["--version"], cwd) === null) {
|
|
78
|
+
warn("git no esta disponible en el PATH - el guardian no puede reparar");
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!fs.existsSync(common)) return;
|
|
83
|
+
|
|
84
|
+
// Gate de alcance: solo repos con worktrees enlazados (donde el bug es posible).
|
|
85
|
+
let entries = [];
|
|
86
|
+
try {
|
|
87
|
+
entries = fs
|
|
88
|
+
.readdirSync(path.join(common, "worktrees"), { withFileTypes: true })
|
|
89
|
+
.filter((entry) => entry.isDirectory());
|
|
90
|
+
} catch {
|
|
91
|
+
/* repo sin worktrees enlazados */
|
|
92
|
+
}
|
|
93
|
+
if (entries.length === 0) return;
|
|
94
|
+
|
|
95
|
+
const sharedCfg = path.join(common, "config");
|
|
96
|
+
const repaired = [];
|
|
97
|
+
|
|
98
|
+
// 1) core.bare -> false si quedo en true. Solo en layout normal (.git): nunca
|
|
99
|
+
// tocamos un repo genuinamente bare (cuyo git-dir no se llama ".git").
|
|
100
|
+
if (
|
|
101
|
+
path.basename(common) === ".git" &&
|
|
102
|
+
git(["config", "--file", sharedCfg, "--get", "core.bare"]) === "true"
|
|
103
|
+
) {
|
|
104
|
+
if (git(["config", "--file", sharedCfg, "core.bare", "false"]) === null) {
|
|
105
|
+
warn(`fallo al forzar core.bare=false en ${sharedCfg}`);
|
|
106
|
+
} else {
|
|
107
|
+
repaired.push("core.bare(shared)");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2) core.worktree en el config compartido (caso sin extensions.worktreeConfig)
|
|
112
|
+
const shared = unsetWorktree(sharedCfg, "shared");
|
|
113
|
+
if (shared) repaired.push(shared);
|
|
114
|
+
|
|
115
|
+
// 3) core.worktree en cada config.worktree por-worktree (caso worktreeConfig activo)
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const cw = path.join(common, "worktrees", entry.name, "config.worktree");
|
|
118
|
+
if (!fs.existsSync(cw)) continue;
|
|
119
|
+
const perWorktree = unsetWorktree(cw, entry.name);
|
|
120
|
+
if (perWorktree) repaired.push(perWorktree);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (repaired.length > 0) {
|
|
124
|
+
process.stdout.write(`${TAG} reparado: ${repaired.join(", ")}\n`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Directorio del proyecto desde el payload del hook; fallback a process.cwd(). */
|
|
129
|
+
function resolveCwd(data) {
|
|
130
|
+
const candidate =
|
|
131
|
+
data && (data.activeWorktreePath || data.worktreePath || data.cwd || data.directory);
|
|
132
|
+
return typeof candidate === "string" && candidate ? candidate : process.cwd();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function run(data) {
|
|
136
|
+
try {
|
|
137
|
+
repair(resolveCwd(data));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// fail-open: nunca bloquear una tool, pero deja rastro del fallo del propio guardian.
|
|
140
|
+
warn(`error inesperado: ${error && error.message ? error.message : String(error)}`);
|
|
141
|
+
}
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Los 3 runtimes entregan el payload del hook por stdin (JSON). Sin stdin (ejecucion
|
|
146
|
+
// manual/TTY) no bloqueamos: reparamos contra process.cwd().
|
|
147
|
+
if (process.stdin.isTTY) {
|
|
148
|
+
run({});
|
|
149
|
+
} else {
|
|
150
|
+
let raw = "";
|
|
151
|
+
process.stdin.setEncoding("utf8");
|
|
152
|
+
process.stdin.on("data", (chunk) => (raw += chunk));
|
|
153
|
+
process.stdin.on("end", () => {
|
|
154
|
+
let data = {};
|
|
155
|
+
try {
|
|
156
|
+
data = JSON.parse(raw || "{}");
|
|
157
|
+
} catch {
|
|
158
|
+
data = {};
|
|
159
|
+
}
|
|
160
|
+
run(data);
|
|
161
|
+
});
|
|
162
|
+
process.stdin.on("error", () => run({}));
|
|
163
|
+
}
|