jorgex-stack 1.9.59 → 1.9.60

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.
@@ -1,1400 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- HOME,
4
- dataDir
5
- } from "./chunk-NQZVJFV2.js";
6
-
7
- // src/lib/fsx.ts
8
- import fs from "fs";
9
- import path from "path";
10
- function readTextIfExists(file) {
11
- try {
12
- return fs.readFileSync(file, "utf8");
13
- } catch {
14
- return null;
15
- }
16
- }
17
- function ensureDir(dir) {
18
- fs.mkdirSync(dir, { recursive: true });
19
- }
20
- var tmpCounter = 0;
21
- function tmpPath(target) {
22
- return path.join(path.dirname(target), `.jorgex-${process.pid}-${tmpCounter++}.tmp`);
23
- }
24
- function renameInto(tmp, target) {
25
- try {
26
- fs.renameSync(tmp, target);
27
- } catch (err) {
28
- fs.rmSync(tmp, { force: true });
29
- throw err;
30
- }
31
- }
32
- function writeText(file, content, mode) {
33
- ensureDir(path.dirname(file));
34
- const tmp = tmpPath(file);
35
- fs.writeFileSync(tmp, content, { encoding: "utf8", mode });
36
- renameInto(tmp, file);
37
- }
38
- function copyFile(source, target) {
39
- ensureDir(path.dirname(target));
40
- const tmp = tmpPath(target);
41
- fs.copyFileSync(source, tmp);
42
- renameInto(tmp, target);
43
- }
44
- function isContainedIn(child, root) {
45
- const rel = path.relative(path.resolve(root), path.resolve(child));
46
- return rel !== "" && rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
47
- }
48
- function pruneEmptyDirs(file, root) {
49
- let dir = path.dirname(file);
50
- while (dir.startsWith(root) && dir !== root) {
51
- try {
52
- if (fs.readdirSync(dir).length > 0) return;
53
- fs.rmdirSync(dir);
54
- } catch {
55
- return;
56
- }
57
- dir = path.dirname(dir);
58
- }
59
- }
60
- function sameFileContent(a, b) {
61
- try {
62
- const bufA = fs.readFileSync(a);
63
- const bufB = fs.readFileSync(b);
64
- return bufA.equals(bufB);
65
- } catch {
66
- return false;
67
- }
68
- }
69
- function listFilesRecursive(dir) {
70
- const out = [];
71
- if (!fs.existsSync(dir)) return out;
72
- const walk = (d) => {
73
- for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
74
- const p = path.join(d, entry.name);
75
- if (entry.isDirectory()) walk(p);
76
- else out.push(p);
77
- }
78
- };
79
- walk(dir);
80
- return out;
81
- }
82
-
83
- // src/lib/backup.ts
84
- import fs2 from "fs";
85
- import path2 from "path";
86
- import crypto from "crypto";
87
- var KEEP_BACKUPS = 10;
88
- function backupsRoot() {
89
- return path2.join(dataDir(), "backups");
90
- }
91
- function compositeChecksum(files) {
92
- const hash = crypto.createHash("sha256");
93
- for (const file of [...files].sort()) {
94
- const content = crypto.createHash("sha256").update(fs2.readFileSync(file)).digest("hex");
95
- hash.update(`${file}:${content}
96
- `);
97
- }
98
- return hash.digest("hex");
99
- }
100
- function createBackup(files, label, root = backupsRoot()) {
101
- const existing = [...new Set(files)].filter((f) => fs2.existsSync(f));
102
- if (existing.length === 0) return null;
103
- const checksum = compositeChecksum(existing);
104
- const latest = listBackups(root)[0];
105
- if (latest?.checksum === checksum) return latest;
106
- const base = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${label}`;
107
- let id = base;
108
- let dir = path2.join(root, id);
109
- for (let n = 1; fs2.existsSync(dir); n++) {
110
- id = `${base}-${n}`;
111
- dir = path2.join(root, id);
112
- }
113
- ensureDir(path2.join(dir, "files"));
114
- const entries = existing.map((original, i) => {
115
- const stored = path2.join(dir, "files", `${String(i).padStart(4, "0")}-${path2.basename(original)}`);
116
- fs2.copyFileSync(original, stored);
117
- return { original, stored };
118
- });
119
- const info = { id, label, createdAt: (/* @__PURE__ */ new Date()).toISOString(), files: entries, checksum };
120
- writeText(path2.join(dir, "manifest.json"), JSON.stringify(info, null, 2) + "\n");
121
- pruneBackups(root);
122
- return info;
123
- }
124
- function listBackups(root = backupsRoot()) {
125
- if (!fs2.existsSync(root)) return [];
126
- const infos = [];
127
- for (const entry of fs2.readdirSync(root, { withFileTypes: true })) {
128
- if (!entry.isDirectory()) continue;
129
- const manifest = readTextIfExists(path2.join(root, entry.name, "manifest.json"));
130
- if (manifest === null) continue;
131
- try {
132
- infos.push(JSON.parse(manifest));
133
- } catch {
134
- infos.push({ id: entry.name, label: "(manifest corrupto)", createdAt: "", files: [] });
135
- }
136
- }
137
- return infos.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
138
- }
139
- function restoreBackup(id, root = backupsRoot(), boundary = HOME) {
140
- const info = listBackups(root).find((b) => b.id === id);
141
- if (!info) throw new Error(`Backup no encontrado: ${id}`);
142
- const boundaryResolved = path2.resolve(boundary);
143
- const hasSymlinkAncestor = (file) => {
144
- let dir = path2.dirname(path2.resolve(file));
145
- while (dir !== boundaryResolved && isContainedIn(dir, boundaryResolved)) {
146
- try {
147
- if (fs2.lstatSync(dir).isSymbolicLink()) return true;
148
- } catch (error) {
149
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
150
- if (code !== "ENOENT") return true;
151
- }
152
- const parent = path2.dirname(dir);
153
- if (parent === dir) break;
154
- dir = parent;
155
- }
156
- return false;
157
- };
158
- let restored = 0;
159
- for (const { original, stored } of info.files) {
160
- if (!fs2.existsSync(stored)) continue;
161
- if (!isContainedIn(original, boundary)) continue;
162
- try {
163
- if (fs2.lstatSync(original).isSymbolicLink()) continue;
164
- } catch (error) {
165
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
166
- if (code !== "ENOENT") continue;
167
- }
168
- if (hasSymlinkAncestor(original)) continue;
169
- ensureDir(path2.dirname(original));
170
- fs2.copyFileSync(stored, original);
171
- restored++;
172
- }
173
- return restored;
174
- }
175
- function pruneBackups(root) {
176
- const all = listBackups(root);
177
- for (const old of all.slice(KEEP_BACKUPS)) {
178
- fs2.rmSync(path2.join(root, old.id), { recursive: true, force: true });
179
- }
180
- }
181
-
182
- // src/lib/filemerge.ts
183
- function markers(name) {
184
- return { open: `<!-- jorgex:${name} -->`, close: `<!-- /jorgex:${name} -->` };
185
- }
186
- function hasHealthyManagedMarkdownMarkers(existing, name) {
187
- const { open, close } = markers(name);
188
- const count = (marker) => existing.split(marker).length - 1;
189
- const onOwnLine = (marker) => existing.split("\n").some((line) => line.trim() === marker);
190
- return count(open) === 1 && count(close) === 1 && existing.indexOf(close) > existing.indexOf(open) && onOwnLine(open) && onOwnLine(close);
191
- }
192
- function repairOrphanMarkers(existing, name) {
193
- const { open, close } = markers(name);
194
- const count = (marker) => existing.split(marker).length - 1;
195
- const opens = count(open);
196
- const closes = count(close);
197
- if (opens === 0 && closes === 0) return existing;
198
- if (hasHealthyManagedMarkdownMarkers(existing, name)) return existing;
199
- return existing.split("\n").map((line) => line.trim() === open || line.trim() === close ? null : line.split(open).join("").split(close).join("")).filter((line) => line !== null).join("\n");
200
- }
201
- function upsertMarkdownSection(existing, name, content) {
202
- const { open, close } = markers(name);
203
- const block = `${open}
204
- ${content.trim()}
205
- ${close}`;
206
- if (existing === null || existing.trim() === "") return block + "\n";
207
- existing = repairOrphanMarkers(existing, name);
208
- const start = existing.indexOf(open);
209
- const end = existing.indexOf(close);
210
- if (start !== -1 && end !== -1 && end > start) {
211
- return existing.slice(0, start) + block + existing.slice(end + close.length);
212
- }
213
- const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
214
- return existing + sep + block + "\n";
215
- }
216
- function removeMarkdownSection(existing, name) {
217
- existing = repairOrphanMarkers(existing, name);
218
- const { open, close } = markers(name);
219
- const start = existing.indexOf(open);
220
- const end = existing.indexOf(close);
221
- if (start === -1 || end === -1 || end < start) return existing;
222
- const before = existing.slice(0, start).replace(/\n+$/, "\n");
223
- const after = existing.slice(end + close.length).replace(/^\n+/, "\n");
224
- const joined = before + after;
225
- return joined.trim() === "" ? "" : joined.replace(/^\n+/, "");
226
- }
227
- function stripLeadingHtmlComments(md) {
228
- let out = md.trimStart();
229
- while (out.startsWith("<!--")) {
230
- const end = out.indexOf("-->");
231
- if (end === -1) break;
232
- out = out.slice(end + 3).trimStart();
233
- }
234
- return out;
235
- }
236
- function upsertJson(existing, mutate) {
237
- let root = {};
238
- if (existing !== null && existing.trim() !== "") {
239
- root = JSON.parse(existing);
240
- }
241
- mutate(root);
242
- return JSON.stringify(root, null, 2) + "\n";
243
- }
244
- function tomlRootEnd(lines) {
245
- const mask = multilineStringMask(lines);
246
- const index = lines.findIndex((line, lineIndex) => !mask[lineIndex] && headerName(line) !== null);
247
- return index === -1 ? lines.length : index;
248
- }
249
- function rootKeyLineIndex(lines, key) {
250
- const end = tomlRootEnd(lines);
251
- const mask = multilineStringMask(lines);
252
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
253
- const pattern = new RegExp(`^\\s*(?:${escaped}|"${escaped}"|'${escaped}')\\s*=`);
254
- return lines.slice(0, end).findIndex((line, index) => !mask[index] && pattern.test(line));
255
- }
256
- function hasTomlRootKey(existing, key) {
257
- if (existing === null || existing === "") return false;
258
- return rootKeyLineIndex(existing.replace(/\r\n/g, "\n").split("\n"), key) !== -1;
259
- }
260
- function upsertTomlRootKeyIfMissing(existing, key, value) {
261
- if (hasTomlRootKey(existing, key)) return existing;
262
- const eol = existing?.includes("\r\n") ? "\r\n" : "\n";
263
- const normalized = (existing ?? "").replace(/\r\n/g, "\n");
264
- const lines = normalized === "" ? [] : normalized.split("\n");
265
- const index = tomlRootEnd(lines);
266
- lines.splice(index, 0, `${key} = ${value}`);
267
- return lines.join(eol);
268
- }
269
- function removeTomlRootKeyIfExact(existing, key, value) {
270
- const eol = existing.includes("\r\n") ? "\r\n" : "\n";
271
- const normalized = existing.replace(/\r\n/g, "\n");
272
- const lines = normalized.split("\n");
273
- const index = rootKeyLineIndex(lines, key);
274
- if (index === -1) return existing;
275
- const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
276
- const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
277
- const exact = new RegExp(`^\\s*(?:${escapedKey}|"${escapedKey}"|'${escapedKey}')\\s*=\\s*${escapedValue}\\s*(?:#.*)?$`);
278
- if (!exact.test(lines[index])) return existing;
279
- lines.splice(index, 1);
280
- return lines.join(eol);
281
- }
282
- function headerName(line) {
283
- const match = /^\s*\[\s*([^\]]+?)\s*\]\s*(#.*)?$/.exec(line);
284
- if (!match) return null;
285
- return match[1].split(".").map((part) => part.trim().replace(/^["']|["']$/g, "")).join(".");
286
- }
287
- function quoteRunLength(line, start, quote) {
288
- let end = start + 1;
289
- while (end < line.length && line[end] === quote) end++;
290
- return end - start;
291
- }
292
- function skipShortString(line, start, quote) {
293
- let index = start + 1;
294
- while (index < line.length) {
295
- if (line[index] === quote) return index + 1;
296
- if (quote === '"' && line[index] === "\\") {
297
- index += 2;
298
- continue;
299
- }
300
- index++;
301
- }
302
- return line.length;
303
- }
304
- function scanTomlLine(line, initial) {
305
- let inside = initial;
306
- let index = 0;
307
- while (index < line.length) {
308
- if (inside !== null) {
309
- const quote = inside === '"""' ? '"' : "'";
310
- if (line[index] === quote) {
311
- const run2 = quoteRunLength(line, index, quote);
312
- if (run2 >= 3) {
313
- inside = null;
314
- }
315
- index += run2;
316
- continue;
317
- }
318
- if (inside === '"""' && line[index] === "\\") {
319
- index += 2;
320
- continue;
321
- }
322
- index++;
323
- continue;
324
- }
325
- const char = line[index];
326
- if (char === "#") break;
327
- if (char !== '"' && char !== "'") {
328
- index++;
329
- continue;
330
- }
331
- const run = quoteRunLength(line, index, char);
332
- if (run >= 3) {
333
- inside = char === '"' ? '"""' : "'''";
334
- index += 3;
335
- } else {
336
- index = skipShortString(line, index, char);
337
- }
338
- }
339
- return inside;
340
- }
341
- function multilineStringMask(lines) {
342
- const mask = [];
343
- let inside = null;
344
- for (const line of lines) {
345
- mask.push(inside !== null);
346
- inside = scanTomlLine(line, inside);
347
- }
348
- return mask;
349
- }
350
- function findTomlSection(lines, section) {
351
- const mask = multilineStringMask(lines);
352
- const start = lines.findIndex((line, i) => !mask[i] && headerName(line) === section);
353
- if (start === -1) return null;
354
- let end = start + 1;
355
- while (end < lines.length && (mask[end] || headerName(lines[end]) === null)) end++;
356
- return { start, end };
357
- }
358
- function upsertTomlSection(existing, section, body) {
359
- const header = `[${section}]`;
360
- const target = headerName(header) ?? section;
361
- const block = `${header}
362
- ${body.trim()}
363
- `;
364
- if (existing === null || existing.trim() === "") return block;
365
- const rawLines = existing.split("\n");
366
- const lines = rawLines.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
367
- const lineStarts = [];
368
- let offset = 0;
369
- for (let index = 0; index < rawLines.length; index++) {
370
- lineStarts.push(offset);
371
- offset += rawLines[index].length;
372
- if (index < rawLines.length - 1) offset++;
373
- }
374
- const found = findTomlSection(lines, target);
375
- if (found === null) {
376
- const sep = existing.endsWith("\n") ? "\n" : "\n\n";
377
- return existing + sep + block;
378
- }
379
- let sectionEnd = found.end;
380
- while (sectionEnd > found.start + 1 && lines[sectionEnd - 1].trim() === "") sectionEnd--;
381
- const startOffset = lineStarts[found.start];
382
- const endOffset = lineStarts[sectionEnd] ?? existing.length;
383
- return existing.slice(0, startOffset) + block.trimEnd() + "\n" + existing.slice(endOffset);
384
- }
385
- function removeTomlSection(existing, section) {
386
- const eol = existing.includes("\r\n") ? "\r\n" : "\n";
387
- const lines = existing.split(/\r?\n/);
388
- const found = findTomlSection(lines, section);
389
- if (found === null) return existing;
390
- let realStart = found.start;
391
- while (realStart > 0 && lines[realStart - 1].trim() === "") realStart--;
392
- const result = [...lines.slice(0, realStart), ...lines.slice(found.end)].join(eol);
393
- if (result.trim() === "") return "";
394
- return result.endsWith(eol) ? result : result + eol;
395
- }
396
- function readTomlSection(existing, section) {
397
- if (existing === null) return null;
398
- const lines = existing.split(/\r?\n/);
399
- const found = findTomlSection(lines, section);
400
- if (found === null) return null;
401
- return lines.slice(found.start + 1, found.end).join("\n");
402
- }
403
- function hasTomlChildSection(existing, section) {
404
- if (existing === null) return false;
405
- const lines = existing.split(/\r?\n/);
406
- const mask = multilineStringMask(lines);
407
- return lines.some((line, index) => !mask[index] && headerName(line)?.startsWith(`${section}.`) === true);
408
- }
409
-
410
- // src/lib/system-prompt-sections.ts
411
- import fs3 from "fs";
412
- import path3 from "path";
413
- var SYSTEM_PROMPT_SECTIONS = [
414
- "system-prompt",
415
- "engram-protocol",
416
- "context7",
417
- "playwright",
418
- "chrome-devtools",
419
- "browser",
420
- "writing-style"
421
- ];
422
- function assertSystemPromptMarkers(content, target) {
423
- if (content === null) return;
424
- const ranges = [];
425
- for (const section of SYSTEM_PROMPT_SECTIONS) {
426
- const markers2 = [...content.matchAll(new RegExp(`<!--\\s*\\/?jorgex:${section}(?=[\\s>]|-->|$)`, "g"))];
427
- if (markers2.length === 0) continue;
428
- if (markers2.length !== 2 || !hasHealthyManagedMarkdownMarkers(content, section)) {
429
- throw new Error(`Marcadores jorgex:${section} ambiguos en ${target}. Repara el bloque antes de reintentar; su contenido se conserva.`);
430
- }
431
- ranges.push({
432
- section,
433
- start: content.indexOf(`<!-- jorgex:${section} -->`),
434
- end: content.indexOf(`<!-- /jorgex:${section} -->`) + `<!-- /jorgex:${section} -->`.length
435
- });
436
- }
437
- ranges.sort((a, b) => a.start - b.start);
438
- for (let index = 1; index < ranges.length; index++) {
439
- if (ranges[index].start < ranges[index - 1].end) {
440
- throw new Error(`Marcadores jorgex:${ranges[index].section} anidados en ${target}. Separa los bloques antes de reintentar; su contenido se conserva.`);
441
- }
442
- }
443
- }
444
- function readSystemPromptFile(target, rootDir) {
445
- if (rootDir !== void 0) {
446
- const probe = fs3.existsSync(target) ? target : path3.dirname(target);
447
- if (fs3.existsSync(probe) && fs3.existsSync(rootDir)) {
448
- const resolved = fs3.realpathSync(probe);
449
- const root = fs3.realpathSync(rootDir);
450
- if (resolved !== root && !isContainedIn(resolved, root)) {
451
- throw new Error(`El prompt ${target} sale del destino aislado.`);
452
- }
453
- }
454
- }
455
- let bytes;
456
- try {
457
- bytes = fs3.readFileSync(target);
458
- } catch (error) {
459
- if (error.code === "ENOENT") return null;
460
- throw new Error(`No se puede leer el prompt ${target}. Revisa los permisos y el tipo de archivo.`, { cause: error });
461
- }
462
- try {
463
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
464
- } catch (error) {
465
- throw new Error(`El prompt ${target} debe usar UTF-8 v\xE1lido.`, { cause: error });
466
- }
467
- }
468
- function assertSystemPromptFile(target, rootDir) {
469
- assertSystemPromptMarkers(readSystemPromptFile(target, rootDir), target);
470
- }
471
- function removeSystemPromptSections(content) {
472
- assertSystemPromptMarkers(content, "system prompt");
473
- return SYSTEM_PROMPT_SECTIONS.reduce((current, section) => removeMarkdownSection(current, section), content);
474
- }
475
-
476
- // src/lib/canonical.ts
477
- import path4 from "path";
478
- import fs4 from "fs";
479
- function parseCanonicalAgent(source, file) {
480
- const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(source);
481
- if (!match) throw new Error(`Agente sin frontmatter: ${file}`);
482
- const fields = {};
483
- for (const line of match[1].split(/\r?\n/)) {
484
- const sep = line.indexOf(":");
485
- if (sep === -1) continue;
486
- fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
487
- }
488
- const required = ["name", "description", "mode", "tier"];
489
- for (const key of required) {
490
- if (!fields[key]) throw new Error(`Agente ${file}: falta '${key}' en el frontmatter`);
491
- }
492
- return {
493
- name: fields.name,
494
- description: fields.description,
495
- mode: fields.mode === "primary" ? "primary" : "subagent",
496
- tier: fields.tier,
497
- readonly: fields.readonly === "true",
498
- bash: fields.bash ?? "full",
499
- spawn: fields.spawn !== "false",
500
- body: source.slice(match[0].length)
501
- };
502
- }
503
- function loadCanonicalAgents(agentsDir) {
504
- return fs4.readdirSync(agentsDir).filter((f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md").map((f) => {
505
- const file = path4.join(agentsDir, f);
506
- const source = fs4.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
507
- return parseCanonicalAgent(source, file);
508
- });
509
- }
510
- var DEVTOOLS_MCP_SERVER = "chrome-devtools";
511
- function isCanonicalMcpServerEnabled(name, server, enabledServers) {
512
- return !server.optional || server.defaultEnabled === true || enabledServers?.has(name) === true;
513
- }
514
- function loadCanonicalMcp(stackDir) {
515
- return JSON.parse(fs4.readFileSync(path4.join(stackDir, "mcp", "servers.json"), "utf8"));
516
- }
517
- function loadCanonicalHooks(stackDir) {
518
- return JSON.parse(fs4.readFileSync(path4.join(stackDir, "hooks", "hooks.json"), "utf8"));
519
- }
520
- function loadCanonicalDefaults(stackDir) {
521
- const file = path4.join(stackDir, "config", "defaults.json");
522
- if (!fs4.existsSync(file)) return {};
523
- const parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
524
- delete parsed["$comment"];
525
- return parsed;
526
- }
527
-
528
- // src/lib/model-map.ts
529
- import path5 from "path";
530
- import { existsSync } from "fs";
531
- function resolveAgentModel(models, agentName, tier) {
532
- const base = models[tier];
533
- const override = models.overrides?.[agentName];
534
- if (!override) return base;
535
- return {
536
- model: override.model || base.model,
537
- variant: "variant" in override ? override.variant || void 0 : base.variant
538
- };
539
- }
540
- var DEFAULT_MODEL_MAP = {
541
- "claude-code": {
542
- strong: { model: "fable" },
543
- standard: { model: "sonnet" },
544
- cheap: { model: "haiku" }
545
- },
546
- // El primary (orchestrator) no usa estos tiers: tanto el profile de CLI como
547
- // la skill de la app heredan el modelo elegido por el usuario. Estos defaults
548
- // son solo para subagentes; variant → model_reasoning_effort.
549
- codex: {
550
- strong: { model: "gpt-6-astra", variant: "low" },
551
- standard: { model: "gpt-5.6-luna", variant: "max" },
552
- cheap: { model: "gpt-5.6-luna", variant: "medium" }
553
- }
554
- };
555
- function modelMapFile() {
556
- return path5.join(dataDir(), "model-map.json");
557
- }
558
- function loadModelMap() {
559
- const file = modelMapFile();
560
- const raw = readTextIfExists(file);
561
- if (raw === null) return DEFAULT_MODEL_MAP;
562
- let fromDisk;
563
- try {
564
- fromDisk = JSON.parse(raw);
565
- } catch {
566
- throw new Error(`El mapa de modelos ${file} contiene JSON inv\xE1lido. Corrige el JSON o restaura una copia antes de continuar.`);
567
- }
568
- const merged = { ...fromDisk };
569
- for (const id of Object.keys(DEFAULT_MODEL_MAP)) {
570
- merged[id] = { ...DEFAULT_MODEL_MAP[id], ...fromDisk[id] ?? {} };
571
- if (fromDisk[id] && !Object.hasOwn(fromDisk[id], "overrides")) delete merged[id].overrides;
572
- }
573
- return merged;
574
- }
575
- function ensureModelMapFile() {
576
- const file = modelMapFile();
577
- if (!existsSync(file)) {
578
- writeText(file, JSON.stringify(DEFAULT_MODEL_MAP, null, 2) + "\n");
579
- }
580
- return file;
581
- }
582
-
583
- // src/lib/detect.ts
584
- import path6 from "path";
585
- import { existsSync as existsSync2, statSync } from "fs";
586
- import { execFileSync } from "child_process";
587
- function lookPath(cmd, env = process.env) {
588
- const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ".ps1", ""] : [""];
589
- for (const dir of (env.PATH ?? "").split(path6.delimiter)) {
590
- if (!dir) continue;
591
- for (const ext of exts) {
592
- const candidate = path6.join(dir, cmd + ext);
593
- if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
594
- }
595
- }
596
- return null;
597
- }
598
- function planDetectedBinCommand(bin, args) {
599
- if (process.platform !== "win32" || !/\.(cmd|bat)$/i.test(bin)) return { command: bin, args };
600
- if ([bin, ...args].some((part) => /[&|<>()^%!"\r\n]/.test(part))) return null;
601
- const quote = (part) => part === "" || /\s/.test(part) ? `"${part}"` : part;
602
- return {
603
- command: process.env.ComSpec ?? "cmd.exe",
604
- args: ["/d", "/s", "/c", [quote(bin), ...args.map(quote)].join(" ")]
605
- };
606
- }
607
- function runDetectedBin(bin, args, timeoutMs, envOverrides) {
608
- try {
609
- const command = planDetectedBinCommand(bin, args);
610
- if (command === null) return null;
611
- return execFileSync(command.command, command.args, {
612
- encoding: "utf8",
613
- timeout: timeoutMs,
614
- stdio: ["ignore", "pipe", "pipe"],
615
- ...envOverrides === void 0 ? {} : { env: { ...process.env, ...envOverrides } }
616
- });
617
- } catch {
618
- return null;
619
- }
620
- }
621
- function detectOpenCode() {
622
- const configDir = process.env.OPENCODE_CONFIG_DIR ?? path6.join(HOME, ".config", "opencode");
623
- const binPath = lookPath("opencode");
624
- return {
625
- id: "opencode",
626
- name: "OpenCode",
627
- installed: binPath !== null || existsSync2(configDir),
628
- binPath,
629
- configDir
630
- };
631
- }
632
- function detectClaudeCode() {
633
- const configDir = process.env.CLAUDE_CONFIG_DIR ?? path6.join(HOME, ".claude");
634
- const binPath = lookPath("claude");
635
- return {
636
- id: "claude-code",
637
- name: "Claude Code",
638
- installed: binPath !== null || existsSync2(configDir),
639
- binPath,
640
- configDir
641
- };
642
- }
643
- function detectCodex() {
644
- const configDir = process.env.CODEX_HOME ?? path6.join(HOME, ".codex");
645
- const binPath = lookPath("codex");
646
- return {
647
- id: "codex",
648
- name: "Codex CLI",
649
- installed: binPath !== null || existsSync2(configDir),
650
- binPath,
651
- configDir
652
- };
653
- }
654
- function detectEngram() {
655
- const fromEnv = process.env.ENGRAM_BIN;
656
- if (fromEnv && existsSync2(fromEnv)) return fromEnv;
657
- const onPath = lookPath("engram");
658
- if (onPath) return onPath;
659
- const candidates = [
660
- path6.join(HOME, "go", "bin", "engram.exe"),
661
- path6.join(HOME, "go", "bin", "engram"),
662
- path6.join(HOME, ".local", "bin", "engram.exe"),
663
- path6.join(HOME, ".local", "bin", "engram")
664
- ];
665
- for (const c of candidates) if (existsSync2(c)) return c;
666
- return null;
667
- }
668
- function engramVersion(bin) {
669
- const out = runDetectedBin(bin, ["--version"], 5e3);
670
- if (out === null) return null;
671
- return /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
672
- }
673
-
674
- // src/lib/hooks-format.ts
675
- import path7 from "path";
676
- function hookScriptNames(canonical) {
677
- return Object.values(canonical.hooks).flat().flatMap((entry) => entry.hooks).map((h) => /\{\{SCRIPTS_DIR\}\}[/\\]([\w./-]+)/.exec(h.command)?.[1]).filter((s) => s !== void 0).map((s) => path7.basename(s));
678
- }
679
- function plainHookCommands(canonical) {
680
- return new Set(
681
- Object.values(canonical.hooks).flat().flatMap((entry) => entry.hooks).map((h) => h.command).filter((c) => !c.includes("{{SCRIPTS_DIR}}"))
682
- );
683
- }
684
- function removeNativeHooks(existing, canonical) {
685
- if (existing === null || existing.trim() === "") return existing;
686
- const scriptNames = hookScriptNames(canonical);
687
- const plainCommands = plainHookCommands(canonical);
688
- return upsertJson(existing, (root) => {
689
- const hooks = root["hooks"];
690
- if (!hooks) return;
691
- for (const event of Object.keys(hooks)) {
692
- const list = hooks[event];
693
- if (!Array.isArray(list)) continue;
694
- hooks[event] = list.filter((entry) => {
695
- const e = entry;
696
- if (!Array.isArray(e?.hooks)) return true;
697
- const ours = e.hooks.some((hh) => {
698
- const cmd = String(hh?.command ?? "");
699
- return scriptNames.some((s) => cmd.includes(s)) || plainCommands.has(cmd);
700
- });
701
- return !ours;
702
- });
703
- if (hooks[event].length === 0) delete hooks[event];
704
- }
705
- if (Object.keys(hooks).length === 0) delete root["hooks"];
706
- });
707
- }
708
- function upsertNativeHooks(existing, canonical, scriptsDir, mapMatcher = (m) => m) {
709
- const scriptsDirForCommand = scriptsDir.replace(/\\/g, "/");
710
- return upsertJson(existing, (root) => {
711
- const hooks = root["hooks"] ??= {};
712
- for (const [event, entries] of Object.entries(canonical.hooks)) {
713
- const list = hooks[event] ??= [];
714
- for (const entry of entries) {
715
- const matcher = entry.matcher !== void 0 ? mapMatcher(entry.matcher) : void 0;
716
- const rendered = {
717
- ...matcher !== void 0 ? { matcher } : {},
718
- hooks: entry.hooks.map((h) => ({
719
- type: h.type,
720
- command: h.command.replace(/\{\{SCRIPTS_DIR\}\}/g, scriptsDirForCommand),
721
- ...h.timeout !== void 0 ? { timeout: h.timeout } : {}
722
- }))
723
- };
724
- const scriptNames = entry.hooks.map((h) => /\{\{SCRIPTS_DIR\}\}[/\\]([\w./-]+)/.exec(h.command)?.[1]).filter((s) => s !== void 0).map((s) => path7.basename(s));
725
- const plainCommands = entry.hooks.map((h) => h.command).filter((c) => !c.includes("{{SCRIPTS_DIR}}"));
726
- const index = list.findIndex((existingEntry) => {
727
- const e = existingEntry;
728
- if (!Array.isArray(e?.hooks)) return false;
729
- return e.hooks.some((hh) => {
730
- const cmd = String(hh?.command ?? "");
731
- return scriptNames.some((s) => cmd.includes(s)) || plainCommands.includes(cmd);
732
- });
733
- });
734
- if (index >= 0) list[index] = rendered;
735
- else list.push(rendered);
736
- }
737
- }
738
- });
739
- }
740
-
741
- // src/lib/quality-capabilities.ts
742
- var QUALITY_CAPABILITY_NAMESPACE = "jorgex.quality.capabilities";
743
- var QUALITY_CAPABILITY_VERSION = 1;
744
- var QUALITY_CAPABILITY_IDS = [
745
- "policy-guidance",
746
- "tool-approval",
747
- "external-verification"
748
- ];
749
- var UNAVAILABLE_REASON = "No reviewed local declaration is available";
750
- var EXTERNAL_VERIFICATION_REASON = "External verification is available only through the external verifier";
751
- function hasText(value) {
752
- return typeof value === "string" && value.trim() !== "";
753
- }
754
- function isRuntime(value) {
755
- return value === "claude-code" || value === "codex" || value === "opencode" || value === "pi";
756
- }
757
- function hasValidEvidence(value) {
758
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
759
- const evidence = value;
760
- return hasText(evidence.source) && hasText(evidence.version);
761
- }
762
- function unavailableCapability(id, reason = UNAVAILABLE_REASON) {
763
- return { id, state: "unavailable", reason };
764
- }
765
- function declarationFor(id, declarations, runtime) {
766
- if (runtime === "unknown") {
767
- return unavailableCapability(id, "Runtime is unknown; local capability cannot be established");
768
- }
769
- if (id === "external-verification") {
770
- return unavailableCapability(id, EXTERNAL_VERIFICATION_REASON);
771
- }
772
- const matches = declarations.filter((declaration2) => declaration2?.id === id);
773
- if (matches.length === 0) return unavailableCapability(id);
774
- if (matches.length !== 1) return unavailableCapability(id, "Duplicate capability declarations are ambiguous");
775
- const declaration = matches[0];
776
- const state = declaration.state;
777
- const reason = hasText(declaration.reason) ? declaration.reason.trim() : void 0;
778
- if (!reason) return unavailableCapability(id, "Capability declaration has no reason");
779
- if (state !== "prompt-only" && state !== "manual" && state !== "unavailable") {
780
- return unavailableCapability(id, "Capability state is not a supported local state");
781
- }
782
- if (state === "unavailable") return { id, state, reason };
783
- if (!hasValidEvidence(declaration.evidence)) {
784
- return unavailableCapability(id, "Capability declaration lacks reviewed source/version evidence");
785
- }
786
- const evidence = declaration.evidence;
787
- return {
788
- id,
789
- state,
790
- reason,
791
- evidence: {
792
- source: evidence.source.trim(),
793
- version: evidence.version.trim()
794
- }
795
- };
796
- }
797
- function hasManagedMarkdownSection(content, name) {
798
- return content !== null && hasHealthyManagedMarkdownMarkers(content, name);
799
- }
800
- function createLocalCapabilityReport(runtime, declarations) {
801
- const normalizedRuntime = isRuntime(runtime) ? runtime : "unknown";
802
- return {
803
- namespace: QUALITY_CAPABILITY_NAMESPACE,
804
- version: QUALITY_CAPABILITY_VERSION,
805
- runtime: normalizedRuntime,
806
- capabilities: QUALITY_CAPABILITY_IDS.map((id) => declarationFor(id, declarations, normalizedRuntime))
807
- };
808
- }
809
- function localQualityStatus(profile, status) {
810
- if ((profile === "high" || profile === "release") && status === "pass") return "incomplete";
811
- return status;
812
- }
813
-
814
- // src/lib/official-engram-setup.ts
815
- import fs5 from "fs";
816
- import path8 from "path";
817
- import { execFileSync as execFileSync2 } from "child_process";
818
- var OFFICIAL_SETUP_RUNTIMES = [
819
- "claude-code",
820
- "codex",
821
- "opencode"
822
- ];
823
- function isOfficialSetupRuntime(runtime) {
824
- return OFFICIAL_SETUP_RUNTIMES.includes(runtime);
825
- }
826
- function resolveOfficialSetupArgv(runtime) {
827
- switch (runtime) {
828
- case "claude-code":
829
- return ["setup", "claude-code"];
830
- case "codex":
831
- return ["setup", "codex"];
832
- case "opencode":
833
- return ["setup", "opencode"];
834
- default:
835
- throw new Error(`Runtime setup oficial desconocido: ${runtime}.`);
836
- }
837
- }
838
- function shouldRunOfficialSetup(opts) {
839
- return opts.command === "install" && opts.dryRun === false && opts.targetDir === void 0;
840
- }
841
- function snapshotDirEntries(dir) {
842
- const out = /* @__PURE__ */ new Set();
843
- try {
844
- const walk = (current) => {
845
- for (const entry of fs5.readdirSync(current, { withFileTypes: true })) {
846
- const full = path8.resolve(path8.join(current, entry.name));
847
- out.add(full);
848
- if (entry.isDirectory()) walk(full);
849
- }
850
- };
851
- walk(path8.resolve(dir));
852
- return out;
853
- } catch {
854
- return null;
855
- }
856
- }
857
- function errnoCode(error) {
858
- return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
859
- }
860
- function findSymlinkInTree(root) {
861
- const stack = [path8.resolve(root)];
862
- while (stack.length > 0) {
863
- const current = stack.pop();
864
- let entries;
865
- try {
866
- entries = fs5.readdirSync(current, { withFileTypes: true });
867
- } catch (error) {
868
- if (errnoCode(error) === "ENOENT") continue;
869
- return `${current}: ilegible (${errnoCode(error)}, no se puede descartar alias)`;
870
- }
871
- for (const entry of entries) {
872
- const full = path8.join(current, entry.name);
873
- let st;
874
- try {
875
- st = fs5.lstatSync(full);
876
- } catch (error) {
877
- if (errnoCode(error) === "ENOENT") continue;
878
- return `${full}: ilegible (${errnoCode(error)}, no se puede descartar alias)`;
879
- }
880
- if (st.isSymbolicLink()) return `${full}: es un symlink (no se sigue ni se copia)`;
881
- if (st.isDirectory()) stack.push(full);
882
- }
883
- }
884
- return null;
885
- }
886
- function findSetupSymlinkViolation(targets, homeDir) {
887
- const homeResolved = path8.resolve(homeDir);
888
- for (const target of targets) {
889
- const resolved = path8.resolve(target);
890
- try {
891
- if (fs5.lstatSync(resolved).isSymbolicLink()) {
892
- return `${resolved}: el target es un symlink (no se respalda ni se ejecuta setup)`;
893
- }
894
- } catch (error) {
895
- if (errnoCode(error) !== "ENOENT") {
896
- return `${resolved}: ilegible (${errnoCode(error)}, no se puede descartar alias)`;
897
- }
898
- }
899
- let dir = path8.dirname(resolved);
900
- while (dir !== homeResolved && isContainedIn(dir, homeResolved)) {
901
- try {
902
- if (fs5.lstatSync(dir).isSymbolicLink()) {
903
- return `${dir}: ancestro existente es un symlink (no se atraviesa)`;
904
- }
905
- } catch (error) {
906
- if (errnoCode(error) !== "ENOENT") {
907
- return `${dir}: ilegible (${errnoCode(error)}, no se puede descartar alias)`;
908
- }
909
- }
910
- const parent = path8.dirname(dir);
911
- if (parent === dir) break;
912
- dir = parent;
913
- }
914
- try {
915
- if (fs5.statSync(resolved).isDirectory()) {
916
- const bad = findSymlinkInTree(resolved);
917
- if (bad !== null) return bad;
918
- }
919
- } catch {
920
- }
921
- }
922
- return null;
923
- }
924
- async function runOfficialSetup(runtime, deps) {
925
- if (typeof deps.engramBin !== "string" || !path8.isAbsolute(deps.engramBin)) {
926
- throw new Error(`runOfficialSetup: engramBin absoluto requerido (runtime ${runtime}).`);
927
- }
928
- const argv = resolveOfficialSetupArgv(runtime);
929
- if (!Array.isArray(deps.targets) || deps.targets.length === 0) {
930
- throw new Error(`runOfficialSetup: targets expl\xEDcitos no vac\xEDos requeridos (runtime ${runtime}).`);
931
- }
932
- const preciseTargets = [...new Set(deps.targets.map((t) => path8.resolve(t)))];
933
- const homeResolved = path8.resolve(deps.homeDir);
934
- const outside = preciseTargets.filter(
935
- (t) => path8.resolve(t) !== homeResolved && !isContainedIn(t, homeResolved)
936
- );
937
- if (outside.length > 0) {
938
- const detail2 = `runOfficialSetup: target fuera de la frontera de restore (${homeResolved}): ${outside[0]}. Bloqueado antes del setup.`;
939
- return {
940
- ok: false,
941
- ownershipTransferred: false,
942
- stderr: detail2,
943
- reason: detail2,
944
- recovery: "none",
945
- backupId: null
946
- };
947
- }
948
- const preViolation = findSetupSymlinkViolation(preciseTargets, homeResolved);
949
- if (preViolation !== null) {
950
- const detail2 = `runOfficialSetup: symlink rechazado antes del setup (${preViolation}). Bloqueado antes del backup.`;
951
- return {
952
- ok: false,
953
- ownershipTransferred: false,
954
- stderr: detail2,
955
- reason: detail2,
956
- recovery: "none",
957
- backupId: null
958
- };
959
- }
960
- let backupId = null;
961
- try {
962
- const backup = await deps.backup();
963
- backupId = backup?.id !== void 0 ? String(backup.id) : null;
964
- } catch (error) {
965
- const detail2 = error instanceof Error ? error.message : String(error);
966
- return {
967
- ok: false,
968
- ownershipTransferred: false,
969
- stderr: detail2,
970
- reason: detail2,
971
- recovery: "none",
972
- backupId: null,
973
- backupError: detail2
974
- };
975
- }
976
- const existedBefore = new Set(preciseTargets.filter((t) => {
977
- try {
978
- return fs5.existsSync(t);
979
- } catch {
980
- return false;
981
- }
982
- }));
983
- const dirSnapshots = /* @__PURE__ */ new Map();
984
- for (const t of preciseTargets) {
985
- if (!existedBefore.has(t)) continue;
986
- try {
987
- if (fs5.statSync(t).isDirectory()) dirSnapshots.set(t, snapshotDirEntries(t));
988
- } catch {
989
- dirSnapshots.set(t, null);
990
- }
991
- }
992
- let spawnResult;
993
- try {
994
- spawnResult = await deps.spawn(deps.engramBin, argv, { shell: false });
995
- } catch (error) {
996
- const detail2 = error instanceof Error ? error.message : String(error);
997
- spawnResult = { ok: false, stdout: "", stderr: detail2 };
998
- }
999
- let verifyResult;
1000
- try {
1001
- verifyResult = await deps.verify();
1002
- } catch (error) {
1003
- const detail2 = error instanceof Error ? error.message : String(error);
1004
- verifyResult = { ok: false, reason: detail2 };
1005
- }
1006
- const postViolation = findSetupSymlinkViolation(preciseTargets, homeResolved);
1007
- const tainted = /* @__PURE__ */ new Set();
1008
- if (postViolation !== null) {
1009
- for (const t of preciseTargets) {
1010
- if (findSetupSymlinkViolation([t], homeResolved) !== null) tainted.add(t);
1011
- }
1012
- }
1013
- const ok = spawnResult.ok === true && verifyResult.ok === true && postViolation === null;
1014
- if (ok) {
1015
- return {
1016
- ok: true,
1017
- ownershipTransferred: verifyResult.ownershipTransferred ?? true,
1018
- stdout: spawnResult.stdout,
1019
- stderr: spawnResult.stderr,
1020
- ...verifyResult.layers === void 0 ? {} : { layers: verifyResult.layers },
1021
- recovery: "none",
1022
- backupId
1023
- };
1024
- }
1025
- let restoreError;
1026
- let restoreFailed = false;
1027
- if (postViolation !== null) {
1028
- restoreFailed = true;
1029
- restoreError = `symlink post-setup (${postViolation}); restore omitido para no escribir a trav\xE9s del enlace`;
1030
- } else {
1031
- try {
1032
- await deps.restore?.();
1033
- } catch (error) {
1034
- restoreFailed = true;
1035
- restoreError = error instanceof Error ? error.message : String(error);
1036
- }
1037
- }
1038
- let cleanupFailed = tainted.size > 0;
1039
- for (const t of preciseTargets) {
1040
- if (tainted.has(t)) continue;
1041
- try {
1042
- if (!existedBefore.has(t)) {
1043
- if (fs5.existsSync(t)) fs5.rmSync(t, { recursive: true, force: true });
1044
- continue;
1045
- }
1046
- if (!dirSnapshots.has(t)) continue;
1047
- const before = dirSnapshots.get(t);
1048
- if (before === null || before === void 0) {
1049
- cleanupFailed = true;
1050
- continue;
1051
- }
1052
- const after = snapshotDirEntries(t);
1053
- if (after === null) {
1054
- cleanupFailed = true;
1055
- continue;
1056
- }
1057
- const created = [...after].filter((p) => !before.has(p));
1058
- created.sort((a, b) => {
1059
- const depthA = a.split(path8.sep).length;
1060
- const depthB = b.split(path8.sep).length;
1061
- if (depthA !== depthB) return depthB - depthA;
1062
- return b.length - a.length;
1063
- });
1064
- for (const p of created) {
1065
- try {
1066
- if (fs5.existsSync(p)) fs5.rmSync(p, { recursive: true, force: true });
1067
- } catch {
1068
- cleanupFailed = true;
1069
- }
1070
- }
1071
- } catch {
1072
- cleanupFailed = true;
1073
- }
1074
- }
1075
- const incomplete = restoreFailed || cleanupFailed;
1076
- const detail = [spawnResult.stderr, verifyResult.stderr, verifyResult.reason, (verifyResult.layers ?? []).join(", ")].map((part) => (part ?? "").trim()).filter((part) => part !== "").join("; ") || "setup oficial fall\xF3";
1077
- const recoveryDetail = incomplete ? `${detail}; recuperaci\xF3n incompleta${restoreError !== void 0 ? `: ${restoreError}` : ""}` : detail;
1078
- return {
1079
- ok: false,
1080
- ownershipTransferred: false,
1081
- stdout: spawnResult.stdout,
1082
- stderr: recoveryDetail,
1083
- reason: recoveryDetail,
1084
- ...verifyResult.layers === void 0 ? {} : { layers: verifyResult.layers },
1085
- ...incomplete ? { incompleteRecovery: true } : {},
1086
- recovery: incomplete ? "incomplete" : "complete",
1087
- backupId,
1088
- ...restoreError === void 0 ? {} : { restoreError }
1089
- };
1090
- }
1091
- function isExplicitClaudeConfigDirEnv(explicit) {
1092
- if (explicit !== void 0) return explicit;
1093
- return process.env.CLAUDE_CONFIG_DIR !== void 0;
1094
- }
1095
- var officialSetupVerifiers = {};
1096
- function registerOfficialSetupVerifier(runtime, fn) {
1097
- officialSetupVerifiers[runtime] = fn;
1098
- }
1099
- function collectOfficialSetupBackupTargets(runtime, configDir, homeDir, isExplicitClaudeConfigDir) {
1100
- switch (runtime) {
1101
- case "claude-code": {
1102
- const explicit = isExplicitClaudeConfigDirEnv(isExplicitClaudeConfigDir);
1103
- const pathIsDefault = homeDir !== void 0 ? path8.resolve(configDir) === path8.resolve(path8.join(homeDir, ".claude")) : path8.basename(path8.resolve(configDir)) === ".claude";
1104
- const isDefault = !explicit && pathIsDefault;
1105
- const nested = path8.join(configDir, ".claude.json");
1106
- const sibling = path8.join(path8.dirname(configDir), ".claude.json");
1107
- const targets = [
1108
- path8.join(configDir, "settings.json"),
1109
- path8.join(configDir, "plugins", "installed_plugins.json"),
1110
- path8.join(configDir, "plugins", "known_marketplaces.json"),
1111
- path8.join(configDir, "plugins", "marketplaces", "engram"),
1112
- path8.join(configDir, "plugins", "cache", "engram"),
1113
- path8.join(configDir, "mcp", "engram.json"),
1114
- // El MCP exacto puede estar en el archivo hermano (modo predeterminado)
1115
- // o en la ruta anidada (modo explícito); se respaldan ambas ubicaciones.
1116
- nested,
1117
- // En el modo predeterminado se añade el archivo hermano efectivo
1118
- // (`HOME/.claude.json`).
1119
- ...isDefault && path8.resolve(sibling) !== path8.resolve(nested) ? [sibling] : []
1120
- ];
1121
- if (homeDir !== void 0 && !isDefault) {
1122
- const fallback = path8.join(path8.resolve(homeDir), ".claude.json");
1123
- if (!targets.map((t) => path8.resolve(t)).includes(path8.resolve(fallback))) {
1124
- targets.push(fallback);
1125
- }
1126
- }
1127
- return [...new Set(targets)];
1128
- }
1129
- case "codex":
1130
- return [
1131
- path8.join(configDir, "config.toml"),
1132
- path8.join(configDir, "engram-instructions.md"),
1133
- path8.join(configDir, "engram-compact-prompt.md"),
1134
- path8.join(configDir, "AGENTS.md"),
1135
- path8.join(configDir, "hooks.json")
1136
- ];
1137
- case "opencode":
1138
- return [
1139
- path8.join(configDir, "opencode.json"),
1140
- path8.join(configDir, "opencode.jsonc"),
1141
- path8.join(configDir, "tui.json"),
1142
- path8.join(configDir, "tui.jsonc"),
1143
- path8.join(configDir, "plugins", "engram.ts")
1144
- ];
1145
- }
1146
- }
1147
- function validateOfficialSetupDestination(runtime, configDir, homeDir) {
1148
- if (runtime === "codex") {
1149
- const expected = path8.resolve(path8.join(homeDir, ".codex"));
1150
- if (path8.resolve(configDir) !== expected) {
1151
- return `runOfficialSetupIfNeeded: Codex custom CODEX_HOME (${configDir}) is not supported: the provider ignores CODEX_HOME and 'engram setup codex' always writes ${expected}; use the default <home>/.codex.`;
1152
- }
1153
- return null;
1154
- }
1155
- if (runtime === "opencode" && path8.basename(path8.resolve(configDir)) !== "opencode") {
1156
- return `runOfficialSetupIfNeeded: configDir OpenCode incompatible (${configDir}); el provider solo alinea <parent>/opencode v\xEDa XDG_CONFIG_HOME.`;
1157
- }
1158
- return null;
1159
- }
1160
- function resolveOfficialSetupEnv(runtime, configDir, homeDir, isExplicitClaudeConfigDir) {
1161
- switch (runtime) {
1162
- case "claude-code": {
1163
- const home = homeDir ?? HOME;
1164
- const explicit = isExplicitClaudeConfigDirEnv(isExplicitClaudeConfigDir);
1165
- if (explicit) return { CLAUDE_CONFIG_DIR: configDir };
1166
- if (path8.resolve(configDir) === path8.resolve(path8.join(home, ".claude"))) {
1167
- return { CLAUDE_CONFIG_DIR: void 0 };
1168
- }
1169
- return { CLAUDE_CONFIG_DIR: configDir };
1170
- }
1171
- case "codex":
1172
- return { CODEX_HOME: void 0 };
1173
- case "opencode":
1174
- return { OPENCODE_CONFIG_DIR: configDir, XDG_CONFIG_HOME: path8.dirname(configDir) };
1175
- }
1176
- }
1177
- async function spawnOfficialSetupBin(bin, argv, env) {
1178
- try {
1179
- let childEnv;
1180
- if (env !== void 0) {
1181
- childEnv = { ...process.env };
1182
- for (const [key, value] of Object.entries(env)) {
1183
- if (value === void 0) delete childEnv[key];
1184
- else childEnv[key] = value;
1185
- }
1186
- }
1187
- const stdout = execFileSync2(bin, argv, {
1188
- encoding: "utf8",
1189
- stdio: ["ignore", "pipe", "pipe"],
1190
- timeout: 12e4,
1191
- shell: false,
1192
- ...childEnv === void 0 ? {} : { env: childEnv }
1193
- });
1194
- return { ok: true, stdout: stdout ?? "", stderr: "" };
1195
- } catch (error) {
1196
- const stdout = error?.stdout;
1197
- const stderr = error?.stderr;
1198
- const toText = (value) => {
1199
- if (typeof value === "string") return value;
1200
- if (value instanceof Buffer) return value.toString("utf8");
1201
- return "";
1202
- };
1203
- const errText = error instanceof Error ? error.message : String(error);
1204
- const captured = toText(stderr).trim();
1205
- return { ok: false, stdout: toText(stdout), stderr: captured !== "" ? captured : errText };
1206
- }
1207
- }
1208
- function isClaudeEngramVersionSupported(version) {
1209
- const trimmed = version.trim();
1210
- if (trimmed === "") return false;
1211
- if (trimmed.includes("-")) return false;
1212
- const match = /(\d+)\.(\d+)\.(\d+)/.exec(trimmed);
1213
- if (!match) return false;
1214
- const major = Number(match[1]);
1215
- const minor = Number(match[2]);
1216
- if (!Number.isFinite(major) || !Number.isFinite(minor)) return false;
1217
- if (major > 2) return true;
1218
- if (major < 2) return false;
1219
- return minor >= 0;
1220
- }
1221
- async function runOfficialSetupIfNeeded(runtime, opts) {
1222
- if (!shouldRunOfficialSetup({ command: opts.command, dryRun: opts.dryRun, targetDir: opts.targetDir })) {
1223
- return { ran: false };
1224
- }
1225
- if (runtime === "pi") {
1226
- return { ran: false };
1227
- }
1228
- if (!isOfficialSetupRuntime(runtime)) {
1229
- const detail = `runOfficialSetupIfNeeded: runtime desconocido en install real: ${runtime}.`;
1230
- return { ran: true, ok: false, ownershipTransferred: false, stderr: detail, reason: detail, recovery: "none" };
1231
- }
1232
- if (typeof opts.engramBin !== "string" || !path8.isAbsolute(opts.engramBin)) {
1233
- const detail = `runOfficialSetupIfNeeded: engramBin absoluto requerido en install real (runtime ${runtime}).`;
1234
- return { ran: true, ok: false, ownershipTransferred: false, stderr: detail, reason: detail, recovery: "none" };
1235
- }
1236
- const verify = officialSetupVerifiers[runtime];
1237
- if (!verify) {
1238
- const detail = `runOfficialSetupIfNeeded: sin verificador registrado en install real (runtime ${runtime}).`;
1239
- return { ran: true, ok: false, ownershipTransferred: false, stderr: detail, reason: detail, recovery: "none" };
1240
- }
1241
- if (runtime === "claude-code") {
1242
- const raw = opts.engramVersion;
1243
- const provided = typeof raw === "string" ? raw.trim() : "";
1244
- if (provided === "" || !isClaudeEngramVersionSupported(provided)) {
1245
- const shown = provided === "" ? "unknown/unreadable" : provided;
1246
- const detail = `runOfficialSetupIfNeeded: Engram ${shown} no registra el MCP de Claude Code; check engram --version and update to Engram 2.0.0+ and rerun install; existing binary was not replaced.`;
1247
- return {
1248
- ran: true,
1249
- ok: false,
1250
- ownershipTransferred: false,
1251
- stderr: detail,
1252
- reason: detail,
1253
- recovery: "none",
1254
- backupId: null
1255
- };
1256
- }
1257
- }
1258
- const engramBin = opts.engramBin;
1259
- const configDir = opts.configDir;
1260
- const destinationError = validateOfficialSetupDestination(runtime, configDir, opts.homeDir);
1261
- if (destinationError !== null) {
1262
- return { ran: true, ok: false, ownershipTransferred: false, stderr: destinationError, reason: destinationError, recovery: "none", backupId: null };
1263
- }
1264
- const explicitClaude = runtime === "claude-code" ? isExplicitClaudeConfigDirEnv(opts.isExplicitClaudeConfigDir) : void 0;
1265
- const targets = collectOfficialSetupBackupTargets(runtime, configDir, opts.homeDir, explicitClaude);
1266
- const setupEnv = resolveOfficialSetupEnv(runtime, configDir, opts.homeDir, explicitClaude);
1267
- let backupId = null;
1268
- let expectedBackupFiles = 0;
1269
- const expandBackupTargets = (files) => {
1270
- const out = [];
1271
- for (const file of files) {
1272
- try {
1273
- const stat = fs5.lstatSync(file);
1274
- if (stat.isSymbolicLink()) continue;
1275
- if (stat.isDirectory()) {
1276
- const walk = (dir) => {
1277
- for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
1278
- const full = path8.join(dir, entry.name);
1279
- let entryStat;
1280
- try {
1281
- entryStat = fs5.lstatSync(full);
1282
- } catch {
1283
- continue;
1284
- }
1285
- if (entryStat.isSymbolicLink()) continue;
1286
- if (entryStat.isDirectory()) walk(full);
1287
- else out.push(full);
1288
- }
1289
- };
1290
- walk(file);
1291
- continue;
1292
- }
1293
- } catch {
1294
- }
1295
- out.push(file);
1296
- }
1297
- return out;
1298
- };
1299
- const result = await runOfficialSetup(runtime, {
1300
- homeDir: opts.homeDir,
1301
- engramBin,
1302
- backup: async () => {
1303
- const backup = createBackup(expandBackupTargets(targets), `official-engram-setup-${runtime}`);
1304
- backupId = backup?.id ?? null;
1305
- expectedBackupFiles = backup?.files.length ?? 0;
1306
- return { id: backupId ?? "no-backup" };
1307
- },
1308
- spawn: async (bin, argv) => spawnOfficialSetupBin(bin, argv, setupEnv),
1309
- verify: async () => verify({
1310
- configDir,
1311
- engramBin,
1312
- homeDir: opts.homeDir,
1313
- ...explicitClaude === void 0 ? {} : { isExplicitClaudeConfigDir: explicitClaude }
1314
- }),
1315
- restore: async () => {
1316
- if (backupId === null) return { restored: false };
1317
- const restored = restoreBackup(backupId);
1318
- if (restored !== expectedBackupFiles) {
1319
- throw new Error(
1320
- `restore incompleto: ${restored}/${expectedBackupFiles} archivos (backup ${backupId}).`
1321
- );
1322
- }
1323
- return { restored: true };
1324
- },
1325
- targets
1326
- });
1327
- return {
1328
- ran: true,
1329
- ok: result.ok,
1330
- ownershipTransferred: result.ownershipTransferred,
1331
- ...result.stderr === void 0 ? {} : { stderr: result.stderr },
1332
- ...result.reason === void 0 ? {} : { reason: result.reason },
1333
- ...result.incompleteRecovery === void 0 ? {} : { incompleteRecovery: result.incompleteRecovery },
1334
- ...result.recovery === void 0 ? {} : { recovery: result.recovery },
1335
- ...result.backupId === void 0 ? {} : { backupId: result.backupId },
1336
- ...result.restoreError === void 0 ? {} : { restoreError: result.restoreError }
1337
- };
1338
- }
1339
-
1340
- export {
1341
- readTextIfExists,
1342
- ensureDir,
1343
- writeText,
1344
- copyFile,
1345
- isContainedIn,
1346
- pruneEmptyDirs,
1347
- sameFileContent,
1348
- listFilesRecursive,
1349
- createBackup,
1350
- listBackups,
1351
- restoreBackup,
1352
- hasHealthyManagedMarkdownMarkers,
1353
- upsertMarkdownSection,
1354
- removeMarkdownSection,
1355
- stripLeadingHtmlComments,
1356
- upsertJson,
1357
- hasTomlRootKey,
1358
- upsertTomlRootKeyIfMissing,
1359
- removeTomlRootKeyIfExact,
1360
- headerName,
1361
- multilineStringMask,
1362
- upsertTomlSection,
1363
- removeTomlSection,
1364
- readTomlSection,
1365
- hasTomlChildSection,
1366
- SYSTEM_PROMPT_SECTIONS,
1367
- assertSystemPromptMarkers,
1368
- readSystemPromptFile,
1369
- assertSystemPromptFile,
1370
- removeSystemPromptSections,
1371
- loadCanonicalAgents,
1372
- DEVTOOLS_MCP_SERVER,
1373
- isCanonicalMcpServerEnabled,
1374
- loadCanonicalMcp,
1375
- loadCanonicalHooks,
1376
- loadCanonicalDefaults,
1377
- resolveAgentModel,
1378
- DEFAULT_MODEL_MAP,
1379
- modelMapFile,
1380
- loadModelMap,
1381
- ensureModelMapFile,
1382
- lookPath,
1383
- planDetectedBinCommand,
1384
- runDetectedBin,
1385
- detectOpenCode,
1386
- detectClaudeCode,
1387
- detectCodex,
1388
- detectEngram,
1389
- engramVersion,
1390
- hookScriptNames,
1391
- removeNativeHooks,
1392
- upsertNativeHooks,
1393
- hasManagedMarkdownSection,
1394
- createLocalCapabilityReport,
1395
- localQualityStatus,
1396
- shouldRunOfficialSetup,
1397
- registerOfficialSetupVerifier,
1398
- validateOfficialSetupDestination,
1399
- runOfficialSetupIfNeeded
1400
- };