jorgex-stack 1.9.61 → 1.9.63

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,989 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- HOME,
4
- dataDir
5
- } from "./chunk-NQZVJFV2.js";
6
-
7
- // src/lib/official-engram-setup.ts
8
- import fs3 from "fs";
9
- import path3 from "path";
10
- import { execFileSync } from "child_process";
11
-
12
- // src/lib/backup.ts
13
- import fs2 from "fs";
14
- import path2 from "path";
15
- import crypto from "crypto";
16
-
17
- // src/lib/fsx.ts
18
- import fs from "fs";
19
- import path from "path";
20
- function readTextIfExists(file) {
21
- try {
22
- return fs.readFileSync(file, "utf8");
23
- } catch {
24
- return null;
25
- }
26
- }
27
- function ensureDir(dir) {
28
- fs.mkdirSync(dir, { recursive: true });
29
- }
30
- var tmpCounter = 0;
31
- function tmpPath(target) {
32
- return path.join(path.dirname(target), `.jorgex-${process.pid}-${tmpCounter++}.tmp`);
33
- }
34
- function renameInto(tmp, target) {
35
- try {
36
- fs.renameSync(tmp, target);
37
- } catch (err) {
38
- fs.rmSync(tmp, { force: true });
39
- throw err;
40
- }
41
- }
42
- function writeText(file, content, mode) {
43
- ensureDir(path.dirname(file));
44
- const tmp = tmpPath(file);
45
- fs.writeFileSync(tmp, content, { encoding: "utf8", mode });
46
- renameInto(tmp, file);
47
- }
48
- function copyFile(source, target) {
49
- ensureDir(path.dirname(target));
50
- const tmp = tmpPath(target);
51
- fs.copyFileSync(source, tmp);
52
- renameInto(tmp, target);
53
- }
54
- function isContainedIn(child, root) {
55
- const rel = path.relative(path.resolve(root), path.resolve(child));
56
- return rel !== "" && rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
57
- }
58
- function pruneEmptyDirs(file, root) {
59
- let dir = path.dirname(file);
60
- while (dir.startsWith(root) && dir !== root) {
61
- try {
62
- if (fs.readdirSync(dir).length > 0) return;
63
- fs.rmdirSync(dir);
64
- } catch {
65
- return;
66
- }
67
- dir = path.dirname(dir);
68
- }
69
- }
70
- function sameFileContent(a, b) {
71
- try {
72
- const bufA = fs.readFileSync(a);
73
- const bufB = fs.readFileSync(b);
74
- return bufA.equals(bufB);
75
- } catch {
76
- return false;
77
- }
78
- }
79
- function listFilesRecursive(dir) {
80
- const out = [];
81
- if (!fs.existsSync(dir)) return out;
82
- const walk = (d) => {
83
- for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
84
- const p = path.join(d, entry.name);
85
- if (entry.isDirectory()) walk(p);
86
- else out.push(p);
87
- }
88
- };
89
- walk(dir);
90
- return out;
91
- }
92
-
93
- // src/lib/backup.ts
94
- var KEEP_BACKUPS = 10;
95
- function backupsRoot() {
96
- return path2.join(dataDir(), "backups");
97
- }
98
- function compositeChecksum(files) {
99
- const hash = crypto.createHash("sha256");
100
- for (const file of [...files].sort()) {
101
- const content = crypto.createHash("sha256").update(fs2.readFileSync(file)).digest("hex");
102
- hash.update(`${file}:${content}
103
- `);
104
- }
105
- return hash.digest("hex");
106
- }
107
- function createBackup(files, label, root = backupsRoot()) {
108
- const existing = [...new Set(files)].filter((f) => fs2.existsSync(f));
109
- if (existing.length === 0) return null;
110
- const checksum = compositeChecksum(existing);
111
- const latest = listBackups(root)[0];
112
- if (latest?.checksum === checksum) return latest;
113
- const base = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${label}`;
114
- let id = base;
115
- let dir = path2.join(root, id);
116
- for (let n = 1; fs2.existsSync(dir); n++) {
117
- id = `${base}-${n}`;
118
- dir = path2.join(root, id);
119
- }
120
- ensureDir(path2.join(dir, "files"));
121
- const entries = existing.map((original, i) => {
122
- const stored = path2.join(dir, "files", `${String(i).padStart(4, "0")}-${path2.basename(original)}`);
123
- fs2.copyFileSync(original, stored);
124
- return { original, stored };
125
- });
126
- const info = { id, label, createdAt: (/* @__PURE__ */ new Date()).toISOString(), files: entries, checksum };
127
- writeText(path2.join(dir, "manifest.json"), JSON.stringify(info, null, 2) + "\n");
128
- pruneBackups(root);
129
- return info;
130
- }
131
- function listBackups(root = backupsRoot()) {
132
- if (!fs2.existsSync(root)) return [];
133
- const infos = [];
134
- for (const entry of fs2.readdirSync(root, { withFileTypes: true })) {
135
- if (!entry.isDirectory()) continue;
136
- const manifest = readTextIfExists(path2.join(root, entry.name, "manifest.json"));
137
- if (manifest === null) continue;
138
- try {
139
- infos.push(JSON.parse(manifest));
140
- } catch {
141
- infos.push({ id: entry.name, label: "(manifest corrupto)", createdAt: "", files: [] });
142
- }
143
- }
144
- return infos.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
145
- }
146
- function restoreBackup(id, root = backupsRoot(), boundary = HOME) {
147
- const info = listBackups(root).find((b) => b.id === id);
148
- if (!info) throw new Error(`Backup no encontrado: ${id}`);
149
- const boundaryResolved = path2.resolve(boundary);
150
- const hasSymlinkAncestor = (file) => {
151
- let dir = path2.dirname(path2.resolve(file));
152
- while (dir !== boundaryResolved && isContainedIn(dir, boundaryResolved)) {
153
- try {
154
- if (fs2.lstatSync(dir).isSymbolicLink()) return true;
155
- } catch (error) {
156
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
157
- if (code !== "ENOENT") return true;
158
- }
159
- const parent = path2.dirname(dir);
160
- if (parent === dir) break;
161
- dir = parent;
162
- }
163
- return false;
164
- };
165
- let restored = 0;
166
- for (const { original, stored } of info.files) {
167
- if (!fs2.existsSync(stored)) continue;
168
- if (!isContainedIn(original, boundary)) continue;
169
- try {
170
- if (fs2.lstatSync(original).isSymbolicLink()) continue;
171
- } catch (error) {
172
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
173
- if (code !== "ENOENT") continue;
174
- }
175
- if (hasSymlinkAncestor(original)) continue;
176
- ensureDir(path2.dirname(original));
177
- fs2.copyFileSync(stored, original);
178
- restored++;
179
- }
180
- return restored;
181
- }
182
- function pruneBackups(root) {
183
- const all = listBackups(root);
184
- for (const old of all.slice(KEEP_BACKUPS)) {
185
- fs2.rmSync(path2.join(root, old.id), { recursive: true, force: true });
186
- }
187
- }
188
-
189
- // src/lib/official-engram-setup.ts
190
- var OFFICIAL_SETUP_RUNTIMES = [
191
- "claude-code",
192
- "codex",
193
- "opencode",
194
- "pi"
195
- ];
196
- function isOfficialSetupRuntime(runtime) {
197
- return OFFICIAL_SETUP_RUNTIMES.includes(runtime);
198
- }
199
- function resolveOfficialSetupArgv(runtime) {
200
- switch (runtime) {
201
- case "claude-code":
202
- return ["setup", "claude-code"];
203
- case "codex":
204
- return ["setup", "codex"];
205
- case "opencode":
206
- return ["setup", "opencode"];
207
- case "pi":
208
- return ["setup", "pi"];
209
- default:
210
- throw new Error(`Runtime setup oficial desconocido: ${runtime}.`);
211
- }
212
- }
213
- function shouldRunOfficialSetup(opts) {
214
- return opts.command === "install" && opts.dryRun === false && opts.targetDir === void 0;
215
- }
216
- function snapshotDirEntries(dir) {
217
- const out = /* @__PURE__ */ new Set();
218
- try {
219
- const walk = (current) => {
220
- for (const entry of fs3.readdirSync(current, { withFileTypes: true })) {
221
- const full = path3.resolve(path3.join(current, entry.name));
222
- out.add(full);
223
- if (entry.isDirectory()) walk(full);
224
- }
225
- };
226
- walk(path3.resolve(dir));
227
- return out;
228
- } catch {
229
- return null;
230
- }
231
- }
232
- function errnoCode(error) {
233
- return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
234
- }
235
- function resolveTrustedNpmRoots(runtime, preciseTargets) {
236
- if (runtime !== "pi") return [];
237
- const roots = /* @__PURE__ */ new Set();
238
- for (const t of preciseTargets) {
239
- const resolved = path3.resolve(t);
240
- if (path3.basename(resolved) === "npm") roots.add(resolved);
241
- }
242
- return [...roots];
243
- }
244
- function findTrustedRootFor(linkPath, trustedRoots) {
245
- const resolved = path3.resolve(linkPath);
246
- for (const root of trustedRoots) {
247
- if (isContainedIn(resolved, root)) return root;
248
- }
249
- return null;
250
- }
251
- function validateTrustedNpmSymlink(linkPath, trustedRoots) {
252
- const resolvedLink = path3.resolve(linkPath);
253
- const root = findTrustedRootFor(resolvedLink, trustedRoots);
254
- if (root === null) return `${resolvedLink}: symlink fuera de npm trusted (rechazado)`;
255
- let raw;
256
- try {
257
- raw = fs3.readlinkSync(resolvedLink);
258
- } catch (error) {
259
- return `${resolvedLink}: symlink ilegible (${errnoCode(error)}, no se puede descartar alias)`;
260
- }
261
- if (path3.isAbsolute(raw)) {
262
- return `${resolvedLink}: symlink absoluto con escape fuera de npm trusted (rechazado)`;
263
- }
264
- const logical = path3.resolve(path3.dirname(resolvedLink), raw);
265
- if (logical !== root && !isContainedIn(logical, root)) {
266
- return `${resolvedLink}: symlink con escape fuera de npm trusted (${raw}, rechazado)`;
267
- }
268
- let canonical;
269
- try {
270
- canonical = fs3.realpathSync(resolvedLink);
271
- } catch (error) {
272
- const code = errnoCode(error);
273
- if (code === "ENOENT") {
274
- return `${resolvedLink}: symlink roto/broken hacia ${raw} (rechazado, no resuelve)`;
275
- }
276
- if (code === "ELOOP" || code === "EAGAIN" || code === "ENAMETOOLONG") {
277
- return `${resolvedLink}: symlink con ciclo/cycle/loop hacia ${raw} (rechazado)`;
278
- }
279
- return `${resolvedLink}: symlink ilegible (${code}, no se puede descartar alias)`;
280
- }
281
- if (canonical !== root && !isContainedIn(canonical, root)) {
282
- return `${resolvedLink}: symlink con escape fuera de npm trusted tras resolver (rechazado)`;
283
- }
284
- return null;
285
- }
286
- function findSymlinkInTree(root, trustedRoots = []) {
287
- const resolvedRoot = path3.resolve(root);
288
- try {
289
- if (fs3.lstatSync(resolvedRoot).isSymbolicLink()) {
290
- return `${resolvedRoot}: el target es un symlink (no se respalda ni se ejecuta setup)`;
291
- }
292
- } catch (error) {
293
- if (errnoCode(error) !== "ENOENT") {
294
- return `${resolvedRoot}: ilegible (${errnoCode(error)}, no se puede descartar alias)`;
295
- }
296
- return null;
297
- }
298
- const stack = [resolvedRoot];
299
- while (stack.length > 0) {
300
- const current = stack.pop();
301
- let entries;
302
- try {
303
- entries = fs3.readdirSync(current, { withFileTypes: true });
304
- } catch (error) {
305
- if (errnoCode(error) === "ENOENT") continue;
306
- return `${current}: ilegible (${errnoCode(error)}, no se puede descartar alias)`;
307
- }
308
- for (const entry of entries) {
309
- const full = path3.join(current, entry.name);
310
- let st;
311
- try {
312
- st = fs3.lstatSync(full);
313
- } catch (error) {
314
- if (errnoCode(error) === "ENOENT") continue;
315
- return `${full}: ilegible (${errnoCode(error)}, no se puede descartar alias)`;
316
- }
317
- if (st.isSymbolicLink()) {
318
- if (trustedRoots.length > 0) {
319
- const allowed = validateTrustedNpmSymlink(full, trustedRoots);
320
- if (allowed === null) continue;
321
- return allowed;
322
- }
323
- return `${full}: es un symlink (no se sigue ni se copia)`;
324
- }
325
- if (st.isDirectory()) stack.push(full);
326
- }
327
- }
328
- return null;
329
- }
330
- function snapshotTrustedSymlinks(trustedRoots) {
331
- const out = /* @__PURE__ */ new Map();
332
- try {
333
- for (const root of trustedRoots) {
334
- const resolvedRoot = path3.resolve(root);
335
- try {
336
- if (fs3.lstatSync(resolvedRoot).isSymbolicLink()) return null;
337
- } catch (error) {
338
- if (errnoCode(error) === "ENOENT") continue;
339
- return null;
340
- }
341
- let isDir = false;
342
- try {
343
- isDir = fs3.statSync(resolvedRoot).isDirectory();
344
- } catch {
345
- continue;
346
- }
347
- if (!isDir) continue;
348
- const stack = [resolvedRoot];
349
- while (stack.length > 0) {
350
- const current = stack.pop();
351
- let names;
352
- try {
353
- names = fs3.readdirSync(current);
354
- } catch (error) {
355
- if (errnoCode(error) === "ENOENT") continue;
356
- return null;
357
- }
358
- for (const name of names) {
359
- const full = path3.join(current, name);
360
- let st;
361
- try {
362
- st = fs3.lstatSync(full);
363
- } catch (error) {
364
- if (errnoCode(error) === "ENOENT") continue;
365
- return null;
366
- }
367
- if (st.isSymbolicLink()) {
368
- try {
369
- out.set(path3.resolve(full), fs3.readlinkSync(full));
370
- } catch {
371
- return null;
372
- }
373
- continue;
374
- }
375
- if (st.isDirectory()) stack.push(full);
376
- }
377
- }
378
- }
379
- return out;
380
- } catch {
381
- return null;
382
- }
383
- }
384
- function restoreTrustedSymlinks(snapshot, trustedRoots) {
385
- if (snapshot === null) {
386
- return { ok: false, error: "symlink snapshot ilegible (no se puede restaurar a ciegas)" };
387
- }
388
- for (const [linkPath, rawTarget] of snapshot) {
389
- const resolvedLink = path3.resolve(linkPath);
390
- const root = findTrustedRootFor(resolvedLink, trustedRoots);
391
- if (root === null) {
392
- return { ok: false, error: `symlink ${resolvedLink} fuera de npm trusted (restore omitido)` };
393
- }
394
- if (path3.isAbsolute(rawTarget)) {
395
- return { ok: false, error: `symlink ${resolvedLink} con target absoluto (restore omitido)` };
396
- }
397
- const logical = path3.resolve(path3.dirname(resolvedLink), rawTarget);
398
- const stop = path3.resolve(root);
399
- if (logical !== stop && !isContainedIn(logical, stop)) {
400
- return { ok: false, error: `symlink ${resolvedLink} con escape fuera de npm trusted (restore omitido)` };
401
- }
402
- let dir = path3.dirname(resolvedLink);
403
- let unsafe = null;
404
- while (true) {
405
- if (dir === stop) {
406
- try {
407
- if (fs3.lstatSync(dir).isSymbolicLink()) unsafe = dir;
408
- } catch (error) {
409
- unsafe = errnoCode(error) === "ENOENT" ? `${dir}: ra\xEDz ausente` : `${dir}: ilegible`;
410
- }
411
- break;
412
- }
413
- if (!isContainedIn(dir, stop)) break;
414
- try {
415
- if (fs3.lstatSync(dir).isSymbolicLink()) {
416
- unsafe = dir;
417
- break;
418
- }
419
- } catch (error) {
420
- if (errnoCode(error) !== "ENOENT") {
421
- unsafe = `${dir}: ilegible`;
422
- break;
423
- }
424
- }
425
- const parent = path3.dirname(dir);
426
- if (parent === dir) break;
427
- dir = parent;
428
- }
429
- if (unsafe !== null) {
430
- return { ok: false, error: `symlink ${resolvedLink}: ancestro inseguro ${unsafe} (restore omitido)` };
431
- }
432
- try {
433
- fs3.mkdirSync(path3.dirname(resolvedLink), { recursive: true });
434
- try {
435
- if (fs3.lstatSync(path3.dirname(resolvedLink)).isSymbolicLink()) {
436
- return { ok: false, error: `symlink ${resolvedLink}: padre es symlink tras mkdir (restore omitido)` };
437
- }
438
- } catch (error) {
439
- return { ok: false, error: `symlink ${resolvedLink}: padre ilegible tras mkdir (${errnoCode(error)})` };
440
- }
441
- try {
442
- const re = fs3.lstatSync(resolvedLink);
443
- if (re.isSymbolicLink()) {
444
- try {
445
- if (fs3.readlinkSync(resolvedLink) === rawTarget) continue;
446
- } catch {
447
- }
448
- fs3.rmSync(resolvedLink, { recursive: true, force: true });
449
- } else {
450
- fs3.rmSync(resolvedLink, { recursive: true, force: true });
451
- }
452
- } catch (error) {
453
- if (errnoCode(error) !== "ENOENT") {
454
- return { ok: false, error: `symlink ${resolvedLink} ilegible antes de recrear (${errnoCode(error)})` };
455
- }
456
- }
457
- try {
458
- fs3.symlinkSync(rawTarget, resolvedLink);
459
- } catch (error) {
460
- return { ok: false, error: `symlink ${resolvedLink}: no se pudo restaurar target (${errnoCode(error)})` };
461
- }
462
- } catch (error) {
463
- return { ok: false, error: `symlink ${resolvedLink}: restore fall\xF3 (${errnoCode(error)})` };
464
- }
465
- }
466
- return { ok: true };
467
- }
468
- function findSetupSymlinkViolation(targets, homeDir, trustedRoots = []) {
469
- const homeResolved = path3.resolve(homeDir);
470
- for (const target of targets) {
471
- const resolved = path3.resolve(target);
472
- try {
473
- if (fs3.lstatSync(resolved).isSymbolicLink()) {
474
- return `${resolved}: el target es un symlink (no se respalda ni se ejecuta setup)`;
475
- }
476
- } catch (error) {
477
- if (errnoCode(error) !== "ENOENT") {
478
- return `${resolved}: ilegible (${errnoCode(error)}, no se puede descartar alias)`;
479
- }
480
- }
481
- let dir = path3.dirname(resolved);
482
- while (dir !== homeResolved && isContainedIn(dir, homeResolved)) {
483
- try {
484
- if (fs3.lstatSync(dir).isSymbolicLink()) {
485
- return `${dir}: ancestro existente es un symlink (no se atraviesa)`;
486
- }
487
- } catch (error) {
488
- if (errnoCode(error) !== "ENOENT") {
489
- return `${dir}: ilegible (${errnoCode(error)}, no se puede descartar alias)`;
490
- }
491
- }
492
- const parent = path3.dirname(dir);
493
- if (parent === dir) break;
494
- dir = parent;
495
- }
496
- try {
497
- if (fs3.statSync(resolved).isDirectory()) {
498
- const bad = findSymlinkInTree(resolved, trustedRoots);
499
- if (bad !== null) return bad;
500
- }
501
- } catch {
502
- }
503
- }
504
- return null;
505
- }
506
- async function runOfficialSetup(runtime, deps) {
507
- if (typeof deps.engramBin !== "string" || !path3.isAbsolute(deps.engramBin)) {
508
- throw new Error(`runOfficialSetup: engramBin absoluto requerido (runtime ${runtime}).`);
509
- }
510
- const argv = resolveOfficialSetupArgv(runtime);
511
- if (!Array.isArray(deps.targets) || deps.targets.length === 0) {
512
- throw new Error(`runOfficialSetup: targets expl\xEDcitos no vac\xEDos requeridos (runtime ${runtime}).`);
513
- }
514
- const preciseTargets = [...new Set(deps.targets.map((t) => path3.resolve(t)))];
515
- const homeResolved = path3.resolve(deps.homeDir);
516
- const outside = preciseTargets.filter(
517
- (t) => path3.resolve(t) !== homeResolved && !isContainedIn(t, homeResolved)
518
- );
519
- if (outside.length > 0) {
520
- const detail2 = `runOfficialSetup: target fuera de la frontera de restore (${homeResolved}): ${outside[0]}. Bloqueado antes del setup.`;
521
- return {
522
- ok: false,
523
- ownershipTransferred: false,
524
- stderr: detail2,
525
- reason: detail2,
526
- recovery: "none",
527
- backupId: null
528
- };
529
- }
530
- const trustedNpmRoots = resolveTrustedNpmRoots(runtime, preciseTargets);
531
- const preViolation = findSetupSymlinkViolation(preciseTargets, homeResolved, trustedNpmRoots);
532
- if (preViolation !== null) {
533
- const detail2 = `runOfficialSetup: symlink rechazado antes del setup (${preViolation}). Bloqueado antes del backup.`;
534
- return {
535
- ok: false,
536
- ownershipTransferred: false,
537
- stderr: detail2,
538
- reason: detail2,
539
- recovery: "none",
540
- backupId: null
541
- };
542
- }
543
- let backupId = null;
544
- try {
545
- const backup = await deps.backup();
546
- backupId = backup?.id !== void 0 ? String(backup.id) : null;
547
- } catch (error) {
548
- const detail2 = error instanceof Error ? error.message : String(error);
549
- return {
550
- ok: false,
551
- ownershipTransferred: false,
552
- stderr: detail2,
553
- reason: detail2,
554
- recovery: "none",
555
- backupId: null,
556
- backupError: detail2
557
- };
558
- }
559
- const existedBefore = new Set(preciseTargets.filter((t) => {
560
- try {
561
- return fs3.existsSync(t);
562
- } catch {
563
- return false;
564
- }
565
- }));
566
- const dirSnapshots = /* @__PURE__ */ new Map();
567
- for (const t of preciseTargets) {
568
- if (!existedBefore.has(t)) continue;
569
- try {
570
- if (fs3.statSync(t).isDirectory()) dirSnapshots.set(t, snapshotDirEntries(t));
571
- } catch {
572
- dirSnapshots.set(t, null);
573
- }
574
- }
575
- const trustedSymlinkSnapshot = snapshotTrustedSymlinks(trustedNpmRoots);
576
- let spawnResult;
577
- try {
578
- spawnResult = await deps.spawn(deps.engramBin, argv, { shell: false });
579
- } catch (error) {
580
- const detail2 = error instanceof Error ? error.message : String(error);
581
- spawnResult = { ok: false, stdout: "", stderr: detail2 };
582
- }
583
- let verifyResult;
584
- try {
585
- verifyResult = await deps.verify();
586
- } catch (error) {
587
- const detail2 = error instanceof Error ? error.message : String(error);
588
- verifyResult = { ok: false, reason: detail2 };
589
- }
590
- const postViolation = findSetupSymlinkViolation(preciseTargets, homeResolved, trustedNpmRoots);
591
- const tainted = /* @__PURE__ */ new Set();
592
- if (postViolation !== null) {
593
- for (const t of preciseTargets) {
594
- if (findSetupSymlinkViolation([t], homeResolved, trustedNpmRoots) !== null) tainted.add(t);
595
- }
596
- }
597
- const ok = spawnResult.ok === true && verifyResult.ok === true && postViolation === null;
598
- if (ok) {
599
- return {
600
- ok: true,
601
- ownershipTransferred: verifyResult.ownershipTransferred ?? true,
602
- stdout: spawnResult.stdout,
603
- stderr: spawnResult.stderr,
604
- ...verifyResult.layers === void 0 ? {} : { layers: verifyResult.layers },
605
- recovery: "none",
606
- backupId
607
- };
608
- }
609
- let restoreError;
610
- let restoreFailed = false;
611
- let symlinkRestoreFailed = false;
612
- let symlinkRestoreError;
613
- if (postViolation !== null) {
614
- restoreFailed = true;
615
- restoreError = `symlink post-setup (${postViolation}); restore omitido para no escribir a trav\xE9s del enlace`;
616
- } else {
617
- try {
618
- await deps.restore?.();
619
- } catch (error) {
620
- restoreFailed = true;
621
- restoreError = error instanceof Error ? error.message : String(error);
622
- }
623
- if (trustedNpmRoots.length > 0) {
624
- const symRes = restoreTrustedSymlinks(trustedSymlinkSnapshot, trustedNpmRoots);
625
- if (!symRes.ok) {
626
- symlinkRestoreFailed = true;
627
- symlinkRestoreError = symRes.error;
628
- }
629
- } else if (trustedSymlinkSnapshot !== null && trustedSymlinkSnapshot.size > 0) {
630
- symlinkRestoreFailed = true;
631
- symlinkRestoreError = "symlink snapshot inesperado fuera de Pi (restore omitido)";
632
- }
633
- }
634
- let cleanupFailed = tainted.size > 0;
635
- for (const t of preciseTargets) {
636
- if (tainted.has(t)) continue;
637
- try {
638
- if (!existedBefore.has(t)) {
639
- try {
640
- fs3.lstatSync(t);
641
- fs3.rmSync(t, { recursive: true, force: true });
642
- } catch (error) {
643
- if (errnoCode(error) !== "ENOENT") cleanupFailed = true;
644
- }
645
- continue;
646
- }
647
- if (!dirSnapshots.has(t)) continue;
648
- const before = dirSnapshots.get(t);
649
- if (before === null || before === void 0) {
650
- cleanupFailed = true;
651
- continue;
652
- }
653
- const after = snapshotDirEntries(t);
654
- if (after === null) {
655
- cleanupFailed = true;
656
- continue;
657
- }
658
- const created = [...after].filter((p) => !before.has(p));
659
- created.sort((a, b) => {
660
- const depthA = a.split(path3.sep).length;
661
- const depthB = b.split(path3.sep).length;
662
- if (depthA !== depthB) return depthB - depthA;
663
- return b.length - a.length;
664
- });
665
- for (const p of created) {
666
- try {
667
- try {
668
- fs3.lstatSync(p);
669
- } catch (error) {
670
- if (errnoCode(error) === "ENOENT") continue;
671
- cleanupFailed = true;
672
- continue;
673
- }
674
- fs3.rmSync(p, { recursive: true, force: true });
675
- } catch {
676
- cleanupFailed = true;
677
- }
678
- }
679
- } catch {
680
- cleanupFailed = true;
681
- }
682
- }
683
- const incomplete = restoreFailed || symlinkRestoreFailed || cleanupFailed;
684
- const combinedRestoreError = [restoreError, symlinkRestoreError].filter((p) => p !== void 0).join("; ") || void 0;
685
- restoreError = combinedRestoreError;
686
- const detail = [spawnResult.stderr, verifyResult.stderr, verifyResult.reason, (verifyResult.layers ?? []).join(", ")].map((part) => (part ?? "").trim()).filter((part) => part !== "").join("; ") || "setup oficial fall\xF3";
687
- const recoveryDetail = incomplete ? `${detail}; recuperaci\xF3n incompleta${restoreError !== void 0 ? `: ${restoreError}` : ""}` : detail;
688
- return {
689
- ok: false,
690
- ownershipTransferred: false,
691
- stdout: spawnResult.stdout,
692
- stderr: recoveryDetail,
693
- reason: recoveryDetail,
694
- ...verifyResult.layers === void 0 ? {} : { layers: verifyResult.layers },
695
- ...incomplete ? { incompleteRecovery: true } : {},
696
- recovery: incomplete ? "incomplete" : "complete",
697
- backupId,
698
- ...restoreError === void 0 ? {} : { restoreError }
699
- };
700
- }
701
- function isExplicitClaudeConfigDirEnv(explicit) {
702
- if (explicit !== void 0) return explicit;
703
- return process.env.CLAUDE_CONFIG_DIR !== void 0;
704
- }
705
- var officialSetupVerifiers = {};
706
- function registerOfficialSetupVerifier(runtime, fn) {
707
- officialSetupVerifiers[runtime] = fn;
708
- }
709
- function collectOfficialSetupBackupTargets(runtime, configDir, homeDir, isExplicitClaudeConfigDir) {
710
- switch (runtime) {
711
- case "claude-code": {
712
- const explicit = isExplicitClaudeConfigDirEnv(isExplicitClaudeConfigDir);
713
- const pathIsDefault = homeDir !== void 0 ? path3.resolve(configDir) === path3.resolve(path3.join(homeDir, ".claude")) : path3.basename(path3.resolve(configDir)) === ".claude";
714
- const isDefault = !explicit && pathIsDefault;
715
- const nested = path3.join(configDir, ".claude.json");
716
- const sibling = path3.join(path3.dirname(configDir), ".claude.json");
717
- const targets = [
718
- path3.join(configDir, "settings.json"),
719
- path3.join(configDir, "plugins", "installed_plugins.json"),
720
- path3.join(configDir, "plugins", "known_marketplaces.json"),
721
- path3.join(configDir, "plugins", "marketplaces", "engram"),
722
- path3.join(configDir, "plugins", "cache", "engram"),
723
- path3.join(configDir, "mcp", "engram.json"),
724
- // El MCP exacto puede estar en el archivo hermano (modo predeterminado)
725
- // o en la ruta anidada (modo explícito); se respaldan ambas ubicaciones.
726
- nested,
727
- // En el modo predeterminado se añade el archivo hermano efectivo
728
- // (`HOME/.claude.json`).
729
- ...isDefault && path3.resolve(sibling) !== path3.resolve(nested) ? [sibling] : []
730
- ];
731
- if (homeDir !== void 0 && !isDefault) {
732
- const fallback = path3.join(path3.resolve(homeDir), ".claude.json");
733
- if (!targets.map((t) => path3.resolve(t)).includes(path3.resolve(fallback))) {
734
- targets.push(fallback);
735
- }
736
- }
737
- return [...new Set(targets)];
738
- }
739
- case "codex":
740
- return [
741
- path3.join(configDir, "config.toml"),
742
- path3.join(configDir, "engram-instructions.md"),
743
- path3.join(configDir, "engram-compact-prompt.md"),
744
- path3.join(configDir, "AGENTS.md"),
745
- path3.join(configDir, "hooks.json")
746
- ];
747
- case "opencode":
748
- return [
749
- path3.join(configDir, "opencode.json"),
750
- path3.join(configDir, "opencode.jsonc"),
751
- path3.join(configDir, "tui.json"),
752
- path3.join(configDir, "tui.jsonc"),
753
- path3.join(configDir, "plugins", "engram.ts")
754
- ];
755
- case "pi":
756
- return [
757
- path3.join(configDir, "settings.json"),
758
- path3.join(configDir, "mcp.json"),
759
- path3.join(configDir, "npm")
760
- ];
761
- }
762
- }
763
- function validateOfficialSetupDestination(runtime, configDir, homeDir) {
764
- if (runtime === "codex") {
765
- const expected = path3.resolve(path3.join(homeDir, ".codex"));
766
- if (path3.resolve(configDir) !== expected) {
767
- 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.`;
768
- }
769
- return null;
770
- }
771
- if (runtime === "opencode" && path3.basename(path3.resolve(configDir)) !== "opencode") {
772
- return `runOfficialSetupIfNeeded: configDir OpenCode incompatible (${configDir}); el provider solo alinea <parent>/opencode v\xEDa XDG_CONFIG_HOME.`;
773
- }
774
- if (runtime === "pi") {
775
- const resolved = path3.resolve(configDir);
776
- const homeResolved = path3.resolve(homeDir);
777
- if (resolved !== homeResolved && !isContainedIn(resolved, homeResolved)) {
778
- return `runOfficialSetupIfNeeded: Pi PI_CODING_AGENT_DIR (${configDir}) fuera de HOME (${homeDir}): queda fuera de la frontera de restore acotada a HOME; usa un PI_CODING_AGENT_DIR dentro de HOME o ajusta HOME antes de reintentar.`;
779
- }
780
- return null;
781
- }
782
- return null;
783
- }
784
- function resolveOfficialSetupEnv(runtime, configDir, homeDir, isExplicitClaudeConfigDir) {
785
- switch (runtime) {
786
- case "claude-code": {
787
- const home = homeDir ?? HOME;
788
- const explicit = isExplicitClaudeConfigDirEnv(isExplicitClaudeConfigDir);
789
- if (explicit) return { CLAUDE_CONFIG_DIR: configDir };
790
- if (path3.resolve(configDir) === path3.resolve(path3.join(home, ".claude"))) {
791
- return { CLAUDE_CONFIG_DIR: void 0 };
792
- }
793
- return { CLAUDE_CONFIG_DIR: configDir };
794
- }
795
- case "codex":
796
- return { CODEX_HOME: void 0 };
797
- case "opencode":
798
- return { OPENCODE_CONFIG_DIR: configDir, XDG_CONFIG_HOME: path3.dirname(configDir) };
799
- case "pi":
800
- return { PI_CODING_AGENT_DIR: configDir };
801
- }
802
- }
803
- async function spawnOfficialSetupBin(bin, argv, env) {
804
- try {
805
- let childEnv;
806
- if (env !== void 0) {
807
- childEnv = { ...process.env };
808
- for (const [key, value] of Object.entries(env)) {
809
- if (value === void 0) delete childEnv[key];
810
- else childEnv[key] = value;
811
- }
812
- }
813
- const stdout = execFileSync(bin, argv, {
814
- encoding: "utf8",
815
- stdio: ["ignore", "pipe", "pipe"],
816
- timeout: 12e4,
817
- shell: false,
818
- ...childEnv === void 0 ? {} : { env: childEnv }
819
- });
820
- return { ok: true, stdout: stdout ?? "", stderr: "" };
821
- } catch (error) {
822
- const stdout = error?.stdout;
823
- const stderr = error?.stderr;
824
- const toText = (value) => {
825
- if (typeof value === "string") return value;
826
- if (value instanceof Buffer) return value.toString("utf8");
827
- return "";
828
- };
829
- const errText = error instanceof Error ? error.message : String(error);
830
- const captured = toText(stderr).trim();
831
- return { ok: false, stdout: toText(stdout), stderr: captured !== "" ? captured : errText };
832
- }
833
- }
834
- function isClaudeEngramVersionSupported(version) {
835
- const trimmed = version.trim();
836
- if (trimmed === "") return false;
837
- if (trimmed.includes("-")) return false;
838
- const match = /(\d+)\.(\d+)\.(\d+)/.exec(trimmed);
839
- if (!match) return false;
840
- const major = Number(match[1]);
841
- const minor = Number(match[2]);
842
- if (!Number.isFinite(major) || !Number.isFinite(minor)) return false;
843
- if (major > 2) return true;
844
- if (major < 2) return false;
845
- return minor >= 0;
846
- }
847
- async function runOfficialSetupIfNeeded(runtime, opts) {
848
- if (!shouldRunOfficialSetup({ command: opts.command, dryRun: opts.dryRun, targetDir: opts.targetDir })) {
849
- return { ran: false };
850
- }
851
- if (!isOfficialSetupRuntime(runtime)) {
852
- const detail = `runOfficialSetupIfNeeded: runtime desconocido en install real: ${runtime}.`;
853
- return { ran: true, ok: false, ownershipTransferred: false, stderr: detail, reason: detail, recovery: "none" };
854
- }
855
- if (typeof opts.engramBin !== "string" || !path3.isAbsolute(opts.engramBin)) {
856
- const detail = `runOfficialSetupIfNeeded: engramBin absoluto requerido en install real (runtime ${runtime}).`;
857
- return { ran: true, ok: false, ownershipTransferred: false, stderr: detail, reason: detail, recovery: "none" };
858
- }
859
- const verify = officialSetupVerifiers[runtime];
860
- if (!verify) {
861
- const detail = `runOfficialSetupIfNeeded: sin verificador registrado en install real (runtime ${runtime}).`;
862
- return { ran: true, ok: false, ownershipTransferred: false, stderr: detail, reason: detail, recovery: "none" };
863
- }
864
- if (runtime === "claude-code") {
865
- const raw = opts.engramVersion;
866
- const provided = typeof raw === "string" ? raw.trim() : "";
867
- if (provided === "" || !isClaudeEngramVersionSupported(provided)) {
868
- const shown = provided === "" ? "unknown/unreadable" : provided;
869
- 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.`;
870
- return {
871
- ran: true,
872
- ok: false,
873
- ownershipTransferred: false,
874
- stderr: detail,
875
- reason: detail,
876
- recovery: "none",
877
- backupId: null
878
- };
879
- }
880
- }
881
- const engramBin = opts.engramBin;
882
- const configDir = opts.configDir;
883
- const destinationError = validateOfficialSetupDestination(runtime, configDir, opts.homeDir);
884
- if (destinationError !== null) {
885
- return { ran: true, ok: false, ownershipTransferred: false, stderr: destinationError, reason: destinationError, recovery: "none", backupId: null };
886
- }
887
- const explicitClaude = runtime === "claude-code" ? isExplicitClaudeConfigDirEnv(opts.isExplicitClaudeConfigDir) : void 0;
888
- const targets = collectOfficialSetupBackupTargets(runtime, configDir, opts.homeDir, explicitClaude);
889
- const setupEnv = resolveOfficialSetupEnv(runtime, configDir, opts.homeDir, explicitClaude);
890
- let backupId = null;
891
- let expectedBackupFiles = 0;
892
- const expandBackupTargets = (files) => {
893
- const out = [];
894
- for (const file of files) {
895
- try {
896
- const stat = fs3.lstatSync(file);
897
- if (stat.isSymbolicLink()) continue;
898
- if (stat.isDirectory()) {
899
- const walk = (dir) => {
900
- for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
901
- const full = path3.join(dir, entry.name);
902
- let entryStat;
903
- try {
904
- entryStat = fs3.lstatSync(full);
905
- } catch {
906
- continue;
907
- }
908
- if (entryStat.isSymbolicLink()) continue;
909
- if (entryStat.isDirectory()) walk(full);
910
- else out.push(full);
911
- }
912
- };
913
- walk(file);
914
- continue;
915
- }
916
- } catch {
917
- }
918
- out.push(file);
919
- }
920
- return out;
921
- };
922
- const result = await runOfficialSetup(runtime, {
923
- homeDir: opts.homeDir,
924
- engramBin,
925
- backup: async () => {
926
- const backup = createBackup(expandBackupTargets(targets), `official-engram-setup-${runtime}`);
927
- backupId = backup?.id ?? null;
928
- expectedBackupFiles = backup?.files.length ?? 0;
929
- return { id: backupId ?? "no-backup" };
930
- },
931
- spawn: async (bin, argv) => spawnOfficialSetupBin(bin, argv, setupEnv),
932
- verify: async () => verify({
933
- configDir,
934
- engramBin,
935
- homeDir: opts.homeDir,
936
- ...explicitClaude === void 0 ? {} : { isExplicitClaudeConfigDir: explicitClaude }
937
- }),
938
- restore: async () => {
939
- if (backupId === null) return { restored: false };
940
- const restored = restoreBackup(backupId);
941
- if (restored !== expectedBackupFiles) {
942
- throw new Error(
943
- `restore incompleto: ${restored}/${expectedBackupFiles} archivos (backup ${backupId}).`
944
- );
945
- }
946
- return { restored: true };
947
- },
948
- targets
949
- });
950
- return {
951
- ran: true,
952
- ok: result.ok,
953
- ownershipTransferred: result.ownershipTransferred,
954
- ...result.stderr === void 0 ? {} : { stderr: result.stderr },
955
- ...result.reason === void 0 ? {} : { reason: result.reason },
956
- ...result.incompleteRecovery === void 0 ? {} : { incompleteRecovery: result.incompleteRecovery },
957
- ...result.recovery === void 0 ? {} : { recovery: result.recovery },
958
- ...result.backupId === void 0 ? {} : { backupId: result.backupId },
959
- ...result.restoreError === void 0 ? {} : { restoreError: result.restoreError }
960
- };
961
- }
962
-
963
- export {
964
- readTextIfExists,
965
- ensureDir,
966
- writeText,
967
- copyFile,
968
- isContainedIn,
969
- pruneEmptyDirs,
970
- sameFileContent,
971
- listFilesRecursive,
972
- createBackup,
973
- listBackups,
974
- restoreBackup,
975
- OFFICIAL_SETUP_RUNTIMES,
976
- isOfficialSetupRuntime,
977
- resolveOfficialSetupArgv,
978
- shouldRunOfficialSetup,
979
- runOfficialSetup,
980
- isExplicitClaudeConfigDirEnv,
981
- officialSetupVerifiers,
982
- registerOfficialSetupVerifier,
983
- collectOfficialSetupBackupTargets,
984
- validateOfficialSetupDestination,
985
- resolveOfficialSetupEnv,
986
- spawnOfficialSetupBin,
987
- isClaudeEngramVersionSupported,
988
- runOfficialSetupIfNeeded
989
- };