jorgex-stack 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2442,1922 +2442,2390 @@ async function runInstall(opts) {
2442
2442
  }
2443
2443
 
2444
2444
  // src/uninstall.ts
2445
+ import fs17 from "fs";
2446
+ import path25 from "path";
2447
+ import * as p2 from "@clack/prompts";
2448
+
2449
+ // src/lib/pi-projection-lifecycle.ts
2445
2450
  import fs16 from "fs";
2451
+ import path24 from "path";
2452
+
2453
+ // src/adapters/pi.ts
2446
2454
  import path22 from "path";
2447
- import * as p2 from "@clack/prompts";
2448
- function resolvePlaywrightUninstallPlan(input) {
2455
+ var piAdapter = {
2456
+ id: "pi",
2457
+ paths(configDir) {
2458
+ const piConfigDir = process.env.PI_CODING_AGENT_DIR ?? path22.join(HOME, ".pi", "agent");
2459
+ const agentsHome = samePath(configDir, piConfigDir) ? HOME : path22.join(path22.dirname(configDir), "home");
2460
+ return {
2461
+ systemPromptFile: path22.join(configDir, "AGENTS.md"),
2462
+ agentsDir: path22.join(configDir, "agents"),
2463
+ skillsDir: path22.join(agentsHome, ".agents", "skills"),
2464
+ commandsDir: path22.join(configDir, "prompts"),
2465
+ pluginsDir: null,
2466
+ scriptsDir: path22.join(configDir, "scripts"),
2467
+ outputStylesDir: null,
2468
+ profilesDir: null
2469
+ };
2470
+ },
2471
+ renderCommand(file, content) {
2472
+ return { file, content: content.replace(/\{\{input\}\}/g, "$ARGUMENTS") };
2473
+ },
2474
+ injectEngramProtocol() {
2475
+ return true;
2476
+ }
2477
+ };
2478
+
2479
+ // src/lib/pi-package-lifecycle.ts
2480
+ import path23 from "path";
2481
+ var REQUIRED_CAPABILITIES = /* @__PURE__ */ new Set([
2482
+ "foundation-contract-v1",
2483
+ "runner-json-v1",
2484
+ "managed-primary-model-v1"
2485
+ ]);
2486
+ var ALLOWED_EXTERNAL_WRITES = /* @__PURE__ */ new Set([
2487
+ "settings.json",
2488
+ "models.json",
2489
+ "jorgex-pi/sol-lifecycle.v1.json"
2490
+ ]);
2491
+ function sameRecord(left, right) {
2492
+ return JSON.stringify(left) === JSON.stringify(right);
2493
+ }
2494
+ function managedExternalWritesAreSafe(writes) {
2495
+ if (writes.length !== ALLOWED_EXTERNAL_WRITES.size) return false;
2496
+ const seen = /* @__PURE__ */ new Set();
2497
+ for (const write of writes) {
2498
+ if (write === null || typeof write !== "object" || Array.isArray(write)) return false;
2499
+ if (Object.keys(write).sort().join(",") !== "owner,relativePath,root,semantics") return false;
2500
+ if (write.owner !== "jorgex-pi" || write.root !== "PI_CODING_AGENT_DIR") return false;
2501
+ if (typeof write.relativePath !== "string" || !ALLOWED_EXTERNAL_WRITES.has(write.relativePath)) return false;
2502
+ if (/^(?:[A-Za-z]:|[\\/])/.test(write.relativePath) || write.relativePath.split(/[\\/]/).includes("..")) return false;
2503
+ if (typeof write.semantics !== "string" || write.semantics.trim() === "" || seen.has(write.relativePath)) return false;
2504
+ seen.add(write.relativePath);
2505
+ }
2506
+ return seen.size === ALLOWED_EXTERNAL_WRITES.size;
2507
+ }
2508
+ function ownership(receipt) {
2509
+ return { receipt, adapters: false, manifest: false, modelMap: false };
2510
+ }
2511
+ function blocked(input, reason) {
2449
2512
  return {
2450
- actions: input.removePackage ? ["remove"] : [],
2451
- preserveBrowserData: true
2513
+ kind: "blocked",
2514
+ reason,
2515
+ receiptPath: input.scope.receiptPath,
2516
+ ownership: ownership(input.receiptJson !== null)
2452
2517
  };
2453
2518
  }
2454
- async function runUninstall(opts) {
2455
- p2.intro(`jorgex-stack ${opts.dryRun ? "uninstall (dry-run)" : "uninstall"}`);
2456
- const useBrowserPreferences = opts.targetDir === void 0;
2457
- const preferenceErrors = useBrowserPreferences ? [...browserPreferenceErrors(), primaryModelOwnershipError()].filter((error) => error !== null) : [];
2458
- if (preferenceErrors.length > 0) {
2459
- for (const error of preferenceErrors) p2.log.error(error);
2460
- p2.outro("Uninstall cancelado: corrige el estado de configuraci\xF3n indicado arriba antes de reintentar.");
2461
- return 1;
2519
+ function packageSource(entry) {
2520
+ if (typeof entry === "string") return entry;
2521
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
2522
+ const source = Reflect.get(entry, "source");
2523
+ return typeof source === "string" ? source : null;
2524
+ }
2525
+ function isJorgeXPiSource(source) {
2526
+ return source.includes("jorgex-pi");
2527
+ }
2528
+ function parsePackageSources(settingsJson) {
2529
+ try {
2530
+ const parsed = JSON.parse(settingsJson);
2531
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2532
+ const packages = Reflect.get(parsed, "packages");
2533
+ if (!Array.isArray(packages)) return null;
2534
+ const sources = packages.map((entry) => ({ entry, source: packageSource(entry) }));
2535
+ return sources.every((value) => value.source !== null) ? sources : null;
2536
+ } catch {
2537
+ return null;
2462
2538
  }
2463
- const stackDir = stackRoot();
2464
- const mcp = loadCanonicalMcp(stackDir);
2465
- const hooks = loadCanonicalHooks(stackDir);
2466
- let exitCode = 0;
2467
- let removeEngram = opts.removeEngram;
2468
- if (!removeEngram && !opts.yes && !opts.dryRun && process.stdout.isTTY) {
2469
- const answer = await p2.confirm({
2470
- message: "\xBFQuitar tambi\xE9n el registro de Engram (MCP/plugin) de los runtimes? Tus memorias (~/.engram) y el binario NO se tocan en ning\xFAn caso.",
2471
- initialValue: false
2539
+ }
2540
+ function isExactManagedPackage(entry, source) {
2541
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false;
2542
+ const keys = Object.keys(entry);
2543
+ const skills = Reflect.get(entry, "skills");
2544
+ const prompts = Reflect.get(entry, "prompts");
2545
+ return keys.length === 3 && keys.includes("source") && keys.includes("skills") && keys.includes("prompts") && Reflect.get(entry, "source") === source && Array.isArray(skills) && skills.length === 0 && Array.isArray(prompts) && prompts.length === 0;
2546
+ }
2547
+ function filterProjectedPiPackage(settingsJson, source) {
2548
+ try {
2549
+ const parsed = JSON.parse(settingsJson);
2550
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2551
+ const packages = Reflect.get(parsed, "packages");
2552
+ if (!Array.isArray(packages)) return null;
2553
+ const matchingSources = packages.filter((entry) => {
2554
+ const entrySource = packageSource(entry);
2555
+ return entrySource !== null && isJorgeXPiSource(entrySource);
2472
2556
  });
2473
- if (p2.isCancel(answer)) {
2474
- p2.cancel("Cancelado \u2014 no se ha tocado nada.");
2475
- return 1;
2476
- }
2477
- removeEngram = answer === true;
2478
- }
2479
- const mcpForUnmerge = removeEngram ? mcp : { servers: Object.fromEntries(Object.entries(mcp.servers).filter(([name]) => name !== "engram")) };
2480
- if (!removeEngram) {
2481
- p2.log.info("Engram se conserva: memorias, binario y registro intactos (usa --remove-engram para desregistrarlo).");
2482
- }
2483
- const retained = /* @__PURE__ */ new Set();
2484
- if (useBrowserPreferences) {
2485
- for (const keep of Object.values(ADAPTERS)) {
2486
- if (opts.runtimes.includes(keep.id)) continue;
2487
- const detection = keep.detect();
2488
- if (!detection.installed) continue;
2489
- const keepCtx = makeContext(keep, detection.configDir);
2490
- if (!keepCtx) continue;
2491
- for (const action of buildPlan(keep, keepCtx)) retained.add(path22.resolve(action.target));
2492
- }
2557
+ if (matchingSources.length !== 1) return null;
2558
+ const managedEntry = matchingSources[0];
2559
+ if (isExactManagedPackage(managedEntry, source)) return JSON.stringify(parsed);
2560
+ if (managedEntry !== source) return null;
2561
+ Reflect.set(parsed, "packages", packages.map((entry) => entry === source ? {
2562
+ source,
2563
+ skills: [],
2564
+ prompts: []
2565
+ } : entry));
2566
+ return JSON.stringify(parsed);
2567
+ } catch {
2568
+ return null;
2493
2569
  }
2494
- for (const id of opts.runtimes) {
2495
- const adapter = ADAPTERS[id];
2496
- if (!adapter) {
2497
- p2.log.warn(`${id}: sin adapter \u2014 omitido.`);
2498
- continue;
2499
- }
2500
- const detection = adapter.detect();
2501
- const configDir = opts.targetDir ?? detection.configDir;
2502
- if (!detection.installed && opts.targetDir === void 0) {
2503
- p2.log.warn(`${adapter.name} no detectado \u2014 omitido.`);
2504
- continue;
2505
- }
2506
- const ctx = makeContext(adapter, configDir, void 0, useBrowserPreferences);
2507
- if (!ctx) continue;
2508
- ctx.preserveEngram = !removeEngram;
2509
- const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
2510
- const mergedTargets = new Set(unmerge.map((a) => path22.resolve(a.target)));
2511
- const usingRealConfig = opts.targetDir === void 0;
2512
- const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
2513
- const pruneRoot = usingRealConfig ? HOME : path22.dirname(configDir);
2514
- const planTargets = [
2515
- .../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path22.resolve(a.target)), ...prevOwned.map((t) => path22.resolve(t))])
2516
- ].filter((t) => !mergedTargets.has(t) && fs16.existsSync(t));
2517
- const deleteTargets = planTargets.filter(
2518
- (t) => !retained.has(t) && !(ctx.preserveEngram && path22.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
2519
- );
2520
- const sharedKept = planTargets.length - deleteTargets.length;
2521
- p2.log.step(`${adapter.name} \u2192 ${configDir}`);
2522
- p2.log.info(`${deleteTargets.length} archivos a borrar, ${unmerge.length} archivos compartidos a limpiar`);
2523
- if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
2524
- if (opts.dryRun) continue;
2525
- let backup;
2526
- try {
2527
- backup = createBackup(
2528
- [...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs16.existsSync(t))],
2529
- `uninstall-${id}`
2530
- );
2531
- } catch (error) {
2532
- p2.log.error(`${adapter.name}: no se pudo respaldar una configuraci\xF3n ilegible en ${configDir} \u2014 ${error instanceof Error ? error.message : String(error)}.`);
2533
- exitCode = 1;
2534
- continue;
2570
+ }
2571
+ function expectedReceipt(candidate, state, scope, engramBin) {
2572
+ return {
2573
+ schemaVersion: 1,
2574
+ state,
2575
+ candidate: {
2576
+ package: candidate.package,
2577
+ tarball: candidate.tarball,
2578
+ provenance: candidate.provenance
2579
+ },
2580
+ scope,
2581
+ engram: { binary: engramBin }
2582
+ };
2583
+ }
2584
+ function parseReceiptShape(receiptJson) {
2585
+ try {
2586
+ const parsed = JSON.parse(receiptJson);
2587
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2588
+ const schemaVersion = Reflect.get(parsed, "schemaVersion");
2589
+ if (schemaVersion !== 1) return null;
2590
+ const state = Reflect.get(parsed, "state");
2591
+ const candidate = Reflect.get(parsed, "candidate");
2592
+ if (state !== "installing" && state !== "installed" || candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
2593
+ return null;
2535
2594
  }
2536
- if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
2537
- for (const target of deleteTargets) {
2538
- fs16.rmSync(target, { force: true });
2539
- pruneEmptyDirs(target, pruneRoot);
2595
+ const packageValue = Reflect.get(candidate, "package");
2596
+ const tarball = Reflect.get(candidate, "tarball");
2597
+ const provenance = Reflect.get(candidate, "provenance");
2598
+ const scope = Reflect.get(parsed, "scope");
2599
+ const engram = Reflect.get(parsed, "engram");
2600
+ if (packageValue === null || typeof packageValue !== "object" || tarball === null || typeof tarball !== "object" || provenance === null || typeof provenance !== "object" || scope === null || typeof scope !== "object" || Array.isArray(scope)) {
2601
+ return null;
2540
2602
  }
2541
- for (const action of unmerge) {
2542
- if (action.kind !== "write") continue;
2543
- if (action.content.trim() === "") {
2544
- fs16.rmSync(action.target, { force: true });
2545
- } else {
2546
- writeText(action.target, action.content);
2547
- }
2548
- if (usingRealConfig) {
2549
- for (const change of action.mcpOwnership ?? []) {
2550
- saveDevtoolsMcpOwnership(devtoolsMcpPreferenceFile(), id, change.server, change.owned);
2551
- }
2552
- for (const change of action.primaryModelOwnership ?? []) {
2553
- savePrimaryModelOwnership(primaryModelOwnershipFile(), id, configDir, change.field, change.owned);
2554
- }
2555
- }
2603
+ const source = Reflect.get(packageValue, "source");
2604
+ const name = Reflect.get(packageValue, "name");
2605
+ const version = Reflect.get(packageValue, "version");
2606
+ const scopeKind = Reflect.get(scope, "kind");
2607
+ const codingAgentDir = Reflect.get(scope, "codingAgentDir");
2608
+ if (name !== "jorgex-pi" || typeof version !== "string" || typeof source !== "string" || source !== `npm:jorgex-pi@${version}` || scopeKind !== "real" && scopeKind !== "target-dir" || typeof codingAgentDir !== "string") {
2609
+ return null;
2556
2610
  }
2557
- if (usingRealConfig) {
2558
- for (const field of ctx.ownedPrimaryModelFields ?? []) {
2559
- savePrimaryModelOwnership(primaryModelOwnershipFile(), id, configDir, field, false);
2560
- }
2611
+ if (engram === void 0) return "upgrade-required";
2612
+ if (engram === null || typeof engram !== "object" || Array.isArray(engram) || typeof Reflect.get(engram, "binary") !== "string" || !path23.isAbsolute(Reflect.get(engram, "binary"))) {
2613
+ return null;
2561
2614
  }
2562
- if (usingRealConfig) removeRuntimeManifest(id);
2563
- p2.log.success(`${adapter.name}: stack retirado (lo tuyo queda intacto).`);
2615
+ return parsed;
2616
+ } catch {
2617
+ return null;
2564
2618
  }
2565
- const playwrightPlan = resolvePlaywrightUninstallPlan({
2566
- removePackage: opts.targetDir === void 0 && opts.removePlaywright
2567
- });
2568
- if (opts.removePlaywright && opts.targetDir !== void 0) {
2569
- p2.log.info("Playwright CLI: --target-dir conserva el paquete global y los datos del navegador.");
2570
- } else if (playwrightPlan.actions.length > 0) {
2571
- if (opts.dryRun) {
2572
- p2.log.info("Playwright CLI: se retirar\xEDa solo el paquete global; los datos y navegadores se conservan.");
2573
- } else {
2574
- const removal = executePlaywrightToolAction2("remove");
2575
- if (removal.ok) {
2576
- try {
2577
- savePlaywrightCliPreference(playwrightCliPreferenceFile(), false);
2578
- p2.log.success("Playwright CLI: paquete global retirado; los datos y navegadores se conservan.");
2579
- } catch (error) {
2580
- p2.log.error(`Playwright CLI: paquete global retirado, pero no se pudo guardar la preferencia (${error instanceof Error ? error.message : String(error)}). Corrige la preferencia antes de reintentar.`);
2581
- exitCode = 1;
2582
- }
2583
- } else {
2584
- const pnpmRemedy = resolvePnpmFailureRemedy(removal.reason);
2585
- const recovery = pnpmRemedy === null ? " Revisa el error de pnpm anterior y ejecuta 'jorgex-stack uninstall --remove-playwright' para reintentar." : ` ${pnpmRemedy} Despu\xE9s, ejecuta 'jorgex-stack uninstall --remove-playwright' para reintentar.`;
2586
- p2.log.error(`Playwright CLI: no se pudo retirar el paquete global; los datos y la preferencia se conservan.${recovery}`);
2587
- exitCode = 1;
2588
- }
2589
- }
2590
- } else {
2591
- p2.log.info("Playwright CLI: paquete global y datos del navegador conservados (usa --remove-playwright para retirar solo el paquete).");
2592
- }
2593
- p2.outro(opts.dryRun ? "Dry-run: no se ha tocado nada." : exitCode === 0 ? "Hecho. Usa 'restore' si quieres volver atr\xE1s." : "Uninstall completado con errores (revisa arriba).");
2594
- return exitCode;
2595
- }
2596
-
2597
- // src/doctor.ts
2598
- import path23 from "path";
2599
- import fs17 from "fs";
2600
- import * as p3 from "@clack/prompts";
2601
- function engramVersion(bin) {
2602
- const out = runDetectedBin(bin, ["--version"], 5e3);
2603
- if (out === null) return null;
2604
- return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
2605
2619
  }
2606
- function resolvePlaywrightDoctorState(input) {
2607
- if (input.enabled !== true) return { status: "disabled" };
2608
- if (input.cli.status === "absent") return { status: "missing", missing: "package" };
2609
- if (input.cli.status === "broken") return { status: "broken" };
2610
- if (input.cli.status === "outdated") return { status: "outdated" };
2611
- if (input.browserCache?.status === "unreadable") {
2612
- return { status: "unreadable", path: input.browserCache.path, errorCode: input.browserCache.errorCode };
2613
- }
2614
- if (!input.browserReady) return { status: "missing", missing: "browser" };
2615
- return { status: "healthy" };
2620
+ function parseReceipt(receiptJson, candidate, scope, engramBin) {
2621
+ const parsed = parseReceiptShape(receiptJson);
2622
+ if (parsed === null || parsed === "upgrade-required") return parsed;
2623
+ const expected = expectedReceipt(candidate, parsed.state, scope, engramBin);
2624
+ return sameRecord(parsed, expected) ? expected : null;
2616
2625
  }
2617
- function context7KeyConfigured(id, configDir) {
2618
- const file = id === "codex" ? path23.join(configDir, "config.toml") : id === "claude-code" ? path23.join(path23.dirname(configDir), `${path23.basename(configDir)}.json`) : path23.join(configDir, "opencode.json");
2619
- const content = readTextIfExists(file);
2620
- if (content === null) return null;
2621
- const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
2622
- if (!match) return null;
2623
- return match[1] !== "";
2626
+ function candidateIsValid(candidate, observed) {
2627
+ return candidate.package.name === "jorgex-pi" && candidate.package.source === `npm:${candidate.package.name}@${candidate.package.version}` && candidate.contract.schemaVersion === 1 && candidate.contract.runner.schemaVersion === 1 && candidate.contract.runner.bin === "jorgex-pi" && candidate.contract.runner.maxStdoutBytes === 65536 && managedExternalWritesAreSafe(candidate.contract.managedExternalWrites) && [...REQUIRED_CAPABILITIES].every((capability) => candidate.contract.capabilities.includes(capability)) && sameRecord(candidate.tarball, observed);
2624
2628
  }
2625
- async function runDoctor() {
2626
- p3.intro("jorgex-stack doctor");
2627
- let problems = 0;
2628
- const engramBin = detectEngram();
2629
- if (engramBin === null) {
2630
- p3.log.warn("Engram: NO detectado. El protocolo de memoria no funcionar\xE1 \u2014 inst\xE1lalo: github.com/Gentleman-Programming/engram");
2631
- problems++;
2632
- } else {
2633
- const version = engramVersion(engramBin);
2634
- if (version === null) {
2635
- p3.log.error(`Engram: binario en ${engramBin} pero no responde a --version.`);
2636
- problems++;
2637
- } else {
2638
- p3.log.success(`Engram: ${version} (${engramBin})`);
2639
- }
2629
+ function planPiPackageLifecycle(input) {
2630
+ if (!candidateIsValid(input.candidate, input.observedTarball)) {
2631
+ return blocked(input, "tarball-integrity");
2640
2632
  }
2641
- const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path23.join(HOME, ".engram");
2642
- const engramDb = path23.join(engramDataDir, "engram.db");
2643
- if (fs17.existsSync(engramDb)) {
2644
- const sizeMb = (fs17.statSync(engramDb).size / 1024 / 1024).toFixed(1);
2645
- p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
2633
+ if (!input.candidate.pi.testedVersions.includes(input.pi.version)) {
2634
+ return blocked(input, "unsupported-pi-version");
2646
2635
  }
2647
- if (!fs17.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
2648
- const preferenceErrors = browserPreferenceErrors();
2649
- const primaryOwnershipError = primaryModelOwnershipError();
2650
- if (primaryOwnershipError !== null) {
2651
- p3.log.error(primaryOwnershipError);
2652
- problems++;
2636
+ if (input.engramBin === null) return blocked(input, "engram-missing");
2637
+ const sources = parsePackageSources(input.pi.settingsJson);
2638
+ if (sources === null) return blocked(input, "settings-corrupt");
2639
+ const matchingSources = sources.filter(({ source }) => isJorgeXPiSource(source));
2640
+ const exactSources = matchingSources.filter(({ source }) => source === input.candidate.package.source);
2641
+ if (exactSources.length > 1) return blocked(input, "duplicate-package");
2642
+ if (matchingSources.some(({ source }) => source !== input.candidate.package.source)) {
2643
+ return blocked(input, "source-divergent");
2653
2644
  }
2654
- if (preferenceErrors.length > 0) {
2655
- for (const error of preferenceErrors) p3.log.error(error);
2656
- problems += preferenceErrors.length;
2657
- } else {
2658
- const browserCache = isPlaywrightBrowserReady();
2659
- const playwright = resolvePlaywrightDoctorState({
2660
- enabled: loadPlaywrightCliPreference(),
2661
- cli: detectPlaywrightCli(),
2662
- browserReady: browserCache.status === "ready",
2663
- browserCache
2664
- });
2665
- if (playwright.status === "disabled") {
2666
- p3.log.info("Playwright CLI: deshabilitado (opcional). Usa 'install --playwright' para instalarlo de forma expl\xEDcita.");
2667
- } else if (playwright.status === "healthy") {
2668
- p3.log.success("Playwright CLI: paquete y navegador listos.");
2669
- } else if (playwright.status === "missing") {
2670
- const target = playwright.missing === "package" ? "el paquete global" : "el navegador de Playwright";
2671
- p3.log.warn(`Playwright CLI: habilitado, pero falta ${target} \u2192 ejecuta 'jorgex-stack install --playwright'.`);
2672
- problems++;
2673
- } else if (playwright.status === "broken") {
2674
- p3.log.error("Playwright CLI: el binario detectado no responde correctamente \u2192 ejecuta 'jorgex-stack install --playwright'.");
2675
- problems++;
2676
- } else if (playwright.status === "unreadable") {
2677
- p3.log.error(`Playwright CLI: no se puede leer la cach\xE9 de navegadores en ${playwright.path} (${playwright.errorCode}) \u2192 revisa permisos o ejecuta 'jorgex-stack install --playwright'.`);
2678
- problems++;
2679
- } else {
2680
- p3.log.warn("Playwright CLI: versi\xF3n distinta del pin aprobado \u2192 ejecuta 'jorgex-stack update' o 'install --playwright'.");
2681
- problems++;
2645
+ let receipt = null;
2646
+ if (input.receiptJson !== null) {
2647
+ const parsedReceipt = parseReceipt(input.receiptJson, input.candidate, {
2648
+ kind: input.scope.kind,
2649
+ codingAgentDir: path23.resolve(input.scope.codingAgentDir)
2650
+ }, input.engramBin);
2651
+ if (parsedReceipt === "upgrade-required") return blocked(input, "receipt-upgrade-required");
2652
+ if (parsedReceipt === null) return blocked(input, "receipt-corrupt");
2653
+ receipt = parsedReceipt;
2654
+ if (receipt.state === "installing") return blocked(input, "partial-state");
2655
+ const exactSource = exactSources[0];
2656
+ if (exactSources.length !== 1 || exactSource === void 0 || !isExactManagedPackage(exactSource.entry, input.candidate.package.source)) {
2657
+ return blocked(input, "source-divergent");
2682
2658
  }
2683
2659
  }
2684
- const manifest = readManifest();
2685
- const modePreference = loadInstallModePreference();
2686
- const current = collectAllCurrentTargets(modePreference);
2687
- if (!current.complete || current.warnings.length > 0) {
2688
- p3.log.warn("Limpieza de hu\xE9rfanos deshabilitada: no se pudo construir el plan completo de todos los runtimes.");
2689
- for (const warning of current.warnings) p3.log.warn(warning);
2690
- problems++;
2691
- }
2692
- for (const adapter of Object.values(ADAPTERS)) {
2693
- const detection = adapter.detect();
2694
- if (!detection.installed) {
2695
- p3.log.warn(`${adapter.name}: no instalado en esta m\xE1quina.`);
2696
- continue;
2697
- }
2698
- const ctx = makeContext(adapter, detection.configDir, modePreference);
2699
- if (!ctx) continue;
2700
- let pending;
2701
- try {
2702
- pending = diffPlan(buildPlan(adapter, ctx)).filter((d) => d.status !== "unchanged").length;
2703
- } catch (err) {
2704
- p3.log.error(
2705
- `${adapter.name}: config ilegible en ${detection.configDir} \u2014 ${err instanceof Error ? err.message : err}`
2706
- );
2707
- problems++;
2708
- continue;
2709
- }
2710
- if (pending > 0) {
2711
- p3.log.warn(`${adapter.name}: ${pending} archivos gestionados desactualizados o ausentes \u2192 ejecuta 'sync'.`);
2712
- problems++;
2713
- } else {
2714
- p3.log.success(`${adapter.name}: config del stack al d\xEDa (${detection.configDir}).`);
2715
- }
2716
- const prev = manifest.runtimes[adapter.id];
2717
- const orphans = prev && current.complete ? findOrphans(prev.owned, current.targets) : [];
2718
- if (orphans.length > 0) {
2719
- p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
2720
- problems++;
2721
- }
2722
- if (adapter.id === "codex" && fs17.existsSync(path23.join(detection.configDir, "hooks.json"))) {
2723
- p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
2724
- }
2725
- if (adapter.id === "codex" && fs17.existsSync(path23.join(detection.configDir, "AGENTS.override.md"))) {
2726
- p3.log.warn(
2727
- "Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
2728
- );
2729
- problems++;
2730
- }
2731
- const key = context7KeyConfigured(adapter.id, detection.configDir);
2732
- if (key === false) p3.log.info(`${adapter.name}: context7 sin key (opcional \u2014 con\xE9ctala cuando quieras).`);
2660
+ if (exactSources.length === 1 && receipt === null) {
2661
+ return {
2662
+ kind: "manual-existing",
2663
+ receiptPath: input.scope.receiptPath,
2664
+ ownership: ownership(false)
2665
+ };
2733
2666
  }
2734
- p3.outro(problems === 0 ? "Todo sano." : `${problems} avisos \u2014 revisa arriba.`);
2735
- return problems > 0 ? 1 : 0;
2736
- }
2737
-
2738
- // src/update.ts
2739
- import fs20 from "fs";
2740
- import path26 from "path";
2741
- import os4 from "os";
2742
- import { execFileSync as execFileSync5 } from "child_process";
2743
- import * as p4 from "@clack/prompts";
2744
-
2745
- // src/lib/github.ts
2746
- import fs18 from "fs";
2747
- import path24 from "path";
2748
- import { execFileSync as execFileSync3 } from "child_process";
2749
- import os3 from "os";
2750
- import { Readable } from "stream";
2751
- import { pipeline } from "stream/promises";
2752
- var cachedToken;
2753
- var ghTokenFailed = false;
2754
- function githubToken() {
2755
- if (cachedToken !== void 0) return cachedToken;
2756
- const env = process.env["GH_TOKEN"]?.trim() || process.env["GITHUB_TOKEN"]?.trim();
2757
- if (env) return cachedToken = env;
2758
- const gh = lookPath("gh");
2759
- if (gh) {
2760
- const fromGh = runDetectedBin(gh, ["auth", "token"], 5e3)?.trim();
2761
- if (fromGh) return cachedToken = fromGh;
2762
- ghTokenFailed = true;
2667
+ if (exactSources.length === 1 && receipt !== null) {
2668
+ return {
2669
+ kind: "ready",
2670
+ receiptPath: input.scope.receiptPath,
2671
+ ownership: ownership(true)
2672
+ };
2763
2673
  }
2764
- return cachedToken = null;
2765
- }
2766
- function ghPresentButTokenFailed() {
2767
- return ghTokenFailed;
2768
- }
2769
- function authHeaders() {
2770
- const token = githubToken();
2771
- return token ? { Authorization: `Bearer ${token}` } : {};
2772
- }
2773
- var rateLimitHit = false;
2774
- function githubRateLimited() {
2775
- return rateLimitHit;
2776
- }
2777
- function githubHeaders() {
2778
2674
  return {
2779
- Accept: "application/vnd.github+json",
2780
- "User-Agent": "jorgex-stack",
2781
- ...authHeaders()
2675
+ kind: "install",
2676
+ receiptPath: input.scope.receiptPath,
2677
+ invocation: {
2678
+ executable: input.pi.executable,
2679
+ args: ["install", input.candidate.package.source, "--no-approve"],
2680
+ environment: input.scope.environment
2681
+ },
2682
+ receipt: expectedReceipt(input.candidate, "installing", {
2683
+ kind: input.scope.kind,
2684
+ codingAgentDir: path23.resolve(input.scope.codingAgentDir)
2685
+ }, input.engramBin),
2686
+ ownership: ownership(true)
2782
2687
  };
2783
2688
  }
2784
- async function latestGithubRelease(repo) {
2689
+ function parseRunnerRecord(stdout, stderr, command, candidate, packageRunner) {
2690
+ if (stderr !== "" || !stdout.endsWith("\n") || Buffer.byteLength(stdout) > candidate.contract.runner.maxStdoutBytes) {
2691
+ return null;
2692
+ }
2693
+ const body = stdout.slice(0, -1);
2694
+ if (body === "" || body.includes("\n") || body.includes("\r")) return null;
2785
2695
  try {
2786
- const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
2787
- headers: githubHeaders(),
2788
- signal: AbortSignal.timeout(1e4)
2789
- });
2790
- if (!res.ok) {
2791
- if (res.status === 403 || res.status === 429) rateLimitHit = true;
2696
+ const parsed = JSON.parse(body);
2697
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2698
+ const record = parsed;
2699
+ if (record.schemaVersion !== candidate.contract.runner.schemaVersion || record.command !== command || record.ok !== true || record.package === null || typeof record.package !== "object" || record.package.name !== candidate.package.name || record.package.version !== candidate.package.version || typeof record.package.root !== "string" || !path23.isAbsolute(record.package.root) || path23.resolve(packageRunner) !== path23.resolve(record.package.root, "bin", "jorgex-pi.mjs")) {
2792
2700
  return null;
2793
2701
  }
2794
- const data = await res.json();
2795
- return data.tag_name?.replace(/^v/, "") ?? null;
2702
+ return record;
2796
2703
  } catch {
2797
2704
  return null;
2798
2705
  }
2799
2706
  }
2800
- async function latestGithubCommit(repo) {
2801
- try {
2802
- const res = await fetch(`https://api.github.com/repos/${repo}/commits?per_page=1`, {
2803
- headers: githubHeaders(),
2804
- signal: AbortSignal.timeout(1e4)
2805
- });
2806
- if (!res.ok) {
2807
- if (res.status === 403 || res.status === 429) rateLimitHit = true;
2808
- return null;
2809
- }
2810
- const data = await res.json();
2811
- return data[0]?.sha ?? null;
2812
- } catch {
2813
- return null;
2814
- }
2815
- }
2816
- function validateExtractedTree(destDir) {
2817
- const resolved = path24.resolve(destDir);
2818
- const walk = (dir) => {
2819
- let entries;
2820
- try {
2821
- entries = fs18.readdirSync(dir, { withFileTypes: true });
2822
- } catch {
2823
- return false;
2824
- }
2825
- for (const entry of entries) {
2826
- const full = path24.join(dir, entry.name);
2827
- let stat;
2828
- try {
2829
- stat = fs18.lstatSync(full);
2830
- } catch {
2831
- return false;
2832
- }
2833
- if (stat.isSymbolicLink()) return false;
2834
- if (!isContainedIn(full, resolved)) return false;
2835
- if (entry.isDirectory()) {
2836
- if (!walk(full)) return false;
2837
- }
2838
- }
2839
- return true;
2840
- };
2841
- return walk(resolved);
2707
+ function runPackageCommand(input, deps, command) {
2708
+ const result = deps.run({
2709
+ executable: input.packageRunner,
2710
+ args: [command, "--json"],
2711
+ environment: input.environment
2712
+ });
2713
+ if (result.exitCode !== 0) return { kind: "blocked", reason: "runner-unhealthy" };
2714
+ return parseRunnerRecord(result.stdout, result.stderr, command, input.candidate, input.packageRunner) ?? { kind: "blocked", reason: "runner-output" };
2842
2715
  }
2843
- function resolveTarBin() {
2844
- if (process.platform !== "win32") return "tar";
2845
- const winTar = path24.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
2846
- return fs18.existsSync(winTar) ? winTar : "tar";
2716
+ function isBlockedResult(value) {
2717
+ return "kind" in value;
2847
2718
  }
2848
- async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
2849
- const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
2850
- const tmp = path24.join(os3.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
2851
- const fail = (reason) => {
2852
- try {
2853
- fs18.rmSync(destDir, { recursive: true, force: true });
2854
- } catch {
2855
- }
2856
- return { ok: false, reason };
2857
- };
2858
- try {
2859
- const res = await fetch(url, {
2860
- headers: { "User-Agent": "jorgex-stack", ...authHeaders() },
2861
- signal: AbortSignal.timeout(12e4)
2862
- });
2863
- if (!res.ok) {
2864
- if (res.status === 403 || res.status === 429) rateLimitHit = true;
2865
- return fail(
2866
- `HTTP ${res.status}${res.status === 403 || res.status === 429 ? " \u2014 rate limit; define GH_TOKEN o inicia sesi\xF3n en gh" : ""}`
2867
- );
2868
- }
2869
- if (!res.body) return fail("respuesta HTTP sin cuerpo");
2870
- await pipeline(
2871
- Readable.fromWeb(res.body),
2872
- fs18.createWriteStream(tmp)
2873
- );
2874
- fs18.rmSync(destDir, { recursive: true, force: true });
2875
- fs18.mkdirSync(destDir, { recursive: true });
2876
- try {
2877
- execFileSync3(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
2878
- } catch (err) {
2879
- const e = err;
2880
- const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
2881
- return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
2882
- }
2883
- const resolvedDest = path24.resolve(destDir);
2884
- const validateRoot = validateSubdir ? path24.resolve(resolvedDest, validateSubdir) : resolvedDest;
2885
- if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
2886
- return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
2719
+ function executePiPackageLifecycle(input, deps) {
2720
+ if (input.plan.kind === "manual-existing") return { kind: "manual-existing" };
2721
+ if (input.operation === "install") {
2722
+ if (input.plan.kind !== "install" || input.plan.receipt === void 0 || input.plan.invocation === void 0) {
2723
+ return { kind: "blocked", reason: "runner-unhealthy" };
2887
2724
  }
2888
- if (fs18.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
2889
- return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
2725
+ deps.writeReceipt(input.plan.receipt);
2726
+ const installed = deps.run(input.plan.invocation);
2727
+ if (installed.exitCode !== 0 || installed.stderr !== "") {
2728
+ return { kind: "blocked", reason: "pi-install-failed" };
2890
2729
  }
2891
- const validated = fs18.existsSync(validateRoot);
2892
- return { ok: true, validated };
2893
- } catch (err) {
2894
- const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
2895
- return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
2896
- } finally {
2897
- try {
2898
- fs18.rmSync(tmp, { force: true });
2899
- } catch {
2730
+ const doctor = runPackageCommand(input, deps, "doctor");
2731
+ if (isBlockedResult(doctor)) return doctor;
2732
+ const doctorResult = doctor.result;
2733
+ if (doctorResult === null || typeof doctorResult !== "object" || Reflect.get(doctorResult, "healthy") !== true) {
2734
+ return { kind: "blocked", reason: "runner-unhealthy" };
2900
2735
  }
2736
+ const receipt = { ...input.plan.receipt, state: "installed" };
2737
+ deps.writeReceipt(receipt);
2738
+ return { kind: "installed", receipt };
2901
2739
  }
2902
- }
2903
-
2904
- // src/lib/skill-update.ts
2905
- import fs19 from "fs";
2906
- import path25 from "path";
2907
- import { execFileSync as execFileSync4 } from "child_process";
2908
- var PROTECTED_SKILLS = /* @__PURE__ */ new Set([
2909
- "agent-delegation",
2910
- "lean-code",
2911
- "orchestrator",
2912
- "work-lifecycle",
2913
- "xreview"
2914
- ]);
2915
- function sameTextContentNormalized(a, b) {
2916
- const ba = fs19.readFileSync(a);
2917
- const bb = fs19.readFileSync(b);
2918
- if (ba.equals(bb)) return true;
2919
- const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
2920
- const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
2921
- return sa === sb;
2922
- }
2923
- function diffSkillDirs(upstreamDir, localDir) {
2924
- const upstreamFiles = new Set(
2925
- listFilesRecursive(upstreamDir).map((f) => path25.relative(upstreamDir, f))
2926
- );
2927
- const localFiles = new Set(
2928
- listFilesRecursive(localDir).map((f) => path25.relative(localDir, f))
2929
- );
2930
- const added = [];
2931
- const modified = [];
2932
- const deleted = [];
2933
- for (const rel of upstreamFiles) {
2934
- if (!localFiles.has(rel)) {
2935
- added.push(rel);
2936
- } else if (!sameTextContentNormalized(path25.join(upstreamDir, rel), path25.join(localDir, rel))) {
2937
- modified.push(rel);
2740
+ if (input.plan.kind !== "ready") return { kind: "blocked", reason: "runner-unhealthy" };
2741
+ const command = runPackageCommand(input, deps, input.operation);
2742
+ if (isBlockedResult(command)) return command;
2743
+ if (input.operation === "sync") {
2744
+ const result = command.result;
2745
+ if (result === null || typeof result !== "object" || Reflect.get(result, "changed") !== true && Reflect.get(result, "changed") !== false || !Array.isArray(Reflect.get(result, "actions"))) {
2746
+ return { kind: "blocked", reason: "runner-unhealthy" };
2938
2747
  }
2748
+ return { kind: "synced", actions: Reflect.get(result, "actions") };
2939
2749
  }
2940
- for (const rel of localFiles) {
2941
- if (!upstreamFiles.has(rel)) {
2942
- deleted.push(rel);
2943
- }
2750
+ const models = command.result;
2751
+ if (models === null || typeof models !== "object" || Reflect.get(models, "mode") !== "inherit-session" || !sameRecord(Reflect.get(models, "tiers"), ["strong", "standard", "cheap"])) {
2752
+ return { kind: "blocked", reason: "runner-unhealthy" };
2944
2753
  }
2754
+ return { kind: "models", models: { mode: "inherit-session", tiers: ["strong", "standard", "cheap"] } };
2755
+ }
2756
+ function receiptUpgradeRequired() {
2945
2757
  return {
2946
- added: added.sort(),
2947
- modified: modified.sort(),
2948
- deleted: deleted.sort()
2758
+ kind: "blocked",
2759
+ reason: "receipt-upgrade-required",
2760
+ remedy: "El receipt no enlaza Engram; usa la versi\xF3n anterior de Stack para desinstalarlo y luego reinstala."
2949
2761
  };
2950
2762
  }
2951
- var DIFF_MAX_LINES = 400;
2952
- function renderSkillDiff(upstreamDir, localDir) {
2953
- try {
2954
- execFileSync4("git", ["diff", "--no-index", "--stat", "--", localDir, upstreamDir], {
2955
- stdio: "pipe",
2956
- encoding: "utf8"
2957
- });
2958
- return "";
2959
- } catch (statErr) {
2960
- const se = statErr;
2961
- if (typeof se.stdout !== "string" || se.status !== 1) {
2962
- const d = diffSkillDirs(upstreamDir, localDir);
2963
- const total = d.added.length + d.modified.length + d.deleted.length;
2964
- if (total === 0) return "";
2965
- return `[git no disponible \u2014 resumen de cambios]
2966
- a\xF1adidos: ${d.added.length}, modificados: ${d.modified.length}, eliminados: ${d.deleted.length}`;
2967
- }
2968
- const stat = se.stdout;
2969
- let fullDiff = "";
2970
- try {
2971
- execFileSync4("git", ["diff", "--no-index", "--", localDir, upstreamDir], {
2972
- stdio: "pipe",
2973
- encoding: "utf8"
2974
- });
2975
- } catch (diffErr) {
2976
- const de = diffErr;
2977
- if (typeof de.stdout === "string" && de.status === 1) {
2978
- const lines = de.stdout.split("\n");
2979
- if (lines.length > DIFF_MAX_LINES) {
2980
- fullDiff = lines.slice(0, DIFF_MAX_LINES).join("\n") + `
2981
- (truncado \u2014 ${lines.length - DIFF_MAX_LINES} l\xEDneas omitidas)`;
2982
- } else {
2983
- fullDiff = de.stdout;
2984
- }
2985
- }
2986
- }
2987
- return fullDiff ? `${stat}
2988
- ${fullDiff}` : stat;
2989
- }
2990
- }
2991
- function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
2992
- const upstreamsFilePath = opts?.upstreamsFilePath;
2993
- const localSkillsRoot = opts?.localSkillsRoot;
2994
- const backupsRoot2 = opts?.backupsRoot;
2995
- if (PROTECTED_SKILLS.has(name)) {
2996
- throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
2763
+ function validateOwnedOperationState(input) {
2764
+ const sources = parsePackageSources(input.detected.settingsJson);
2765
+ if (sources === null) return { kind: "blocked", reason: "settings-corrupt" };
2766
+ const matchingSources = sources.filter(({ source: source2 }) => isJorgeXPiSource(source2));
2767
+ if (matchingSources.length > 1) return { kind: "blocked", reason: "duplicate-package" };
2768
+ if (input.receiptJson === null) {
2769
+ const matchingSource2 = matchingSources[0];
2770
+ return matchingSource2 !== void 0 && matchingSource2.source === input.registry.candidate.package.source && isExactManagedPackage(matchingSource2.entry, matchingSource2.source) ? { kind: "blocked", reason: "manual-existing" } : { kind: "blocked", reason: "source-divergent" };
2997
2771
  }
2998
- const upstreamsFile = upstreamsFilePath ?? path25.join(path25.dirname(stackRoot()), "upstreams.json");
2999
- const raw = fs19.readFileSync(upstreamsFile, "utf8");
3000
- const data = JSON.parse(raw);
3001
- const skillEntry = data?.skills?.[name];
3002
- if (!skillEntry) {
3003
- throw new Error(`Skill "${name}" no encontrada en upstreams.json.`);
2772
+ const parsedReceipt = parseReceiptShape(input.receiptJson);
2773
+ if (parsedReceipt === "upgrade-required") return receiptUpgradeRequired();
2774
+ if (parsedReceipt === null) return { kind: "blocked", reason: "receipt-corrupt" };
2775
+ const receipt = parsedReceipt;
2776
+ if (receipt.state !== "installed") return { kind: "blocked", reason: "partial-state" };
2777
+ const accepted = input.registry.acceptedCandidates ?? [input.registry.candidate];
2778
+ if (!accepted.some((candidate) => sameRecord(receipt.candidate, {
2779
+ package: candidate.package,
2780
+ tarball: candidate.tarball,
2781
+ provenance: candidate.provenance
2782
+ }))) {
2783
+ return { kind: "blocked", reason: "receipt-untrusted" };
3004
2784
  }
3005
- if (skillEntry.kind === "release") {
3006
- throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
2785
+ if (receipt.scope.kind !== (input.paths.targetDir ? "target-dir" : "real") || path23.resolve(receipt.scope.codingAgentDir) !== path23.resolve(input.paths.codingAgentDir)) {
2786
+ return { kind: "blocked", reason: "source-divergent" };
3007
2787
  }
3008
- const skillsRoot = localSkillsRoot ?? path25.join(stackRoot(), "skills");
3009
- const localSkillDir = path25.join(skillsRoot, name);
3010
- const localFiles = listFilesRecursive(localSkillDir);
3011
- if (localFiles.length > 0) {
3012
- createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
2788
+ if (input.engramBin !== null && path23.resolve(receipt.engram.binary) !== path23.resolve(input.engramBin)) {
2789
+ return { kind: "blocked", reason: "receipt-corrupt" };
3013
2790
  }
3014
- const stagingDir = `${localSkillDir}.staging-${process.pid}`;
3015
- try {
3016
- const upstreamFiles = listFilesRecursive(upstreamSkillDir);
3017
- for (const src of upstreamFiles) {
3018
- const st = fs19.lstatSync(src);
3019
- if (st.isSymbolicLink()) {
3020
- throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
3021
- }
3022
- const rel = path25.relative(upstreamSkillDir, src);
3023
- const dest = path25.join(stagingDir, rel);
3024
- ensureDir(path25.dirname(dest));
3025
- copyFile(src, dest);
3026
- }
3027
- const oldDir = `${localSkillDir}.old-${process.pid}`;
3028
- if (fs19.existsSync(localSkillDir)) {
3029
- fs19.renameSync(localSkillDir, oldDir);
3030
- }
3031
- fs19.renameSync(stagingDir, localSkillDir);
3032
- fs19.rmSync(oldDir, { recursive: true, force: true });
3033
- } catch (err) {
3034
- try {
3035
- fs19.rmSync(stagingDir, { recursive: true, force: true });
3036
- } catch {
3037
- }
3038
- throw err;
2791
+ const source = receipt.candidate.package.source;
2792
+ const matchingSource = matchingSources[0];
2793
+ if (matchingSources.length !== 1 || matchingSource === void 0 || matchingSource.source !== source || !isExactManagedPackage(matchingSource.entry, source)) {
2794
+ return { kind: "blocked", reason: "source-divergent" };
3039
2795
  }
3040
- skillEntry.commit = newCommit;
3041
- writeText(upstreamsFile, JSON.stringify(data, null, 2) + "\n");
2796
+ return { receipt, source };
3042
2797
  }
3043
-
3044
- // src/update.ts
3045
- function rateLimitHint(prefix) {
3046
- return ghPresentButTokenFailed() ? `${prefix} Tienes gh instalado pero \`gh auth token\` no devolvi\xF3 credencial (\xBFsesi\xF3n caducada?) \u2014 prueba \`gh auth login\` o define GH_TOKEN.` : `${prefix} Define GH_TOKEN o inicia sesi\xF3n en gh CLI.`;
2798
+ function operationWasBlocked(value) {
2799
+ return "kind" in value;
3047
2800
  }
3048
- function loadUpstreams() {
3049
- const file = path26.join(path26.dirname(stackRoot()), "upstreams.json");
3050
- return JSON.parse(fs20.readFileSync(file, "utf8"));
3051
- }
3052
- function skillsToScan(maintainer, upstreams) {
3053
- return maintainer ? Object.keys(upstreams.skills) : [];
3054
- }
3055
- async function querySkillHeads(skillNames, upstreams) {
3056
- const requests = /* @__PURE__ */ new Map();
3057
- for (const name of skillNames) {
3058
- const info = upstreams.skills[name];
3059
- const repo = info.source.replace(/^github:/, "");
3060
- if (!requests.has(repo)) requests.set(repo, latestGithubCommit(repo));
3061
- }
3062
- const heads = /* @__PURE__ */ new Map();
3063
- await Promise.all(
3064
- [...requests.entries()].map(async ([repo, request]) => {
3065
- heads.set(repo, await request);
3066
- })
3067
- );
3068
- return skillNames.map((name) => {
3069
- const info = upstreams.skills[name];
3070
- const repo = info.source.replace(/^github:/, "");
3071
- return { name, repo, info, head: heads.get(repo) ?? null };
2801
+ function runManagedRunner(input, deps, command) {
2802
+ const result = deps.run({
2803
+ executable: input.detected.packageRunner,
2804
+ args: [command, "--json"],
2805
+ environment: input.paths.environment
3072
2806
  });
2807
+ if (result.exitCode !== 0) return { kind: "blocked", reason: "runner-unhealthy" };
2808
+ const parsed = parseRunnerRecord(
2809
+ result.stdout,
2810
+ result.stderr,
2811
+ command,
2812
+ input.registry.candidate,
2813
+ input.detected.packageRunner
2814
+ );
2815
+ return parsed ?? { kind: "blocked", reason: "runner-output" };
3073
2816
  }
3074
- async function latestNpmVersion(pkg) {
3075
- try {
3076
- const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
3077
- signal: AbortSignal.timeout(1e4)
3078
- });
3079
- if (!res.ok) return null;
3080
- const data = await res.json();
3081
- return data.version ?? null;
3082
- } catch {
3083
- return null;
3084
- }
2817
+ function managedRunnerWasBlocked(value) {
2818
+ return "kind" in value;
3085
2819
  }
3086
- function resolvePlaywrightUpdateCheck(input) {
3087
- if (input.enabled !== true) return null;
3088
- if (input.cli.status === "current") {
2820
+ function runPiPackageManagedOperation(input, deps) {
2821
+ if (input.engramBin === null && input.operation !== "uninstall") {
3089
2822
  return {
3090
- level: "success",
3091
- message: `Playwright CLI: ${PLAYWRIGHT_CLI.version} \u2014 al d\xEDa con el pin aprobado.`
2823
+ kind: "blocked",
2824
+ reason: "engram-missing",
2825
+ remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
3092
2826
  };
3093
2827
  }
3094
- const local = input.cli.detectedVersion ?? (input.cli.status === "absent" ? "no instalado" : "no verificable");
3095
- return {
3096
- level: "warn",
3097
- message: `Playwright CLI: ${local} local, pin aprobado ${PLAYWRIGHT_CLI.version} \u2192 ejecuta 'jorgex-stack update' o 'jorgex-stack install --playwright'.`
3098
- };
3099
- }
3100
- async function runUpdateCheck(localVersion, includeBrowserState = true) {
3101
- p4.intro("jorgex-stack update --check");
3102
- if (includeBrowserState) {
3103
- const preferenceErrors = browserPreferenceErrors();
3104
- if (preferenceErrors.length > 0) {
3105
- for (const error of preferenceErrors) p4.log.error(error);
3106
- p4.outro("Check cancelado: corrige las preferencias de navegador antes de reintentar.");
3107
- return 1;
2828
+ if (input.operation === "uninstall" && input.receiptJson === null) {
2829
+ const sources = parsePackageSources(input.detected.settingsJson);
2830
+ if (sources === null) return { kind: "blocked", reason: "settings-corrupt" };
2831
+ if (!sources.some(({ source }) => isJorgeXPiSource(source))) {
2832
+ return deps.isPackageAbsent() ? { kind: "uninstalled" } : { kind: "blocked", reason: "absence-unverified" };
3108
2833
  }
3109
2834
  }
3110
- const upstreams = loadUpstreams();
3111
- const npmLatest = await latestNpmVersion("jorgex-stack");
3112
- if (npmLatest === null)
3113
- p4.log.info(`jorgex-stack: v${localVersion} local (a\xFAn no publicado en npm o sin red).`);
3114
- else if (npmLatest === localVersion)
3115
- p4.log.success(`jorgex-stack: v${localVersion} \u2014 al d\xEDa.`);
3116
- else
3117
- p4.log.warn(
3118
- `jorgex-stack: v${localVersion} local, v${npmLatest} en npm \u2192 pnpm dlx jorgex-stack@latest`
3119
- );
3120
- const engramRepo = upstreams.tools["engram"]?.source.replace(/^github:/, "");
3121
- if (engramRepo) {
3122
- const bin = detectEngram();
3123
- const local = bin ? engramVersion(bin) : null;
3124
- const latest = await latestGithubRelease(engramRepo);
3125
- if (local === null) p4.log.warn("engram: no detectado en esta m\xE1quina.");
3126
- else if (latest === null)
3127
- p4.log.info(`engram: ${local} local (no se pudo consultar el upstream).`);
3128
- else if (latest === local) p4.log.success(`engram: ${local} \u2014 al d\xEDa.`);
3129
- else
3130
- p4.log.warn(
3131
- `engram: ${local} local, ${latest} disponible. Tu instalaci\xF3n NO se toca (D7) \u2014 actualiza t\xFA: github.com/${engramRepo}/releases`
3132
- );
2835
+ const owned = validateOwnedOperationState(input);
2836
+ if (operationWasBlocked(owned)) return owned;
2837
+ if (input.operation === "doctor") {
2838
+ if (!sameRecord(owned.receipt.candidate, {
2839
+ package: input.registry.candidate.package,
2840
+ tarball: input.registry.candidate.tarball,
2841
+ provenance: input.registry.candidate.provenance
2842
+ })) {
2843
+ return { kind: "blocked", reason: "source-divergent" };
2844
+ }
2845
+ const doctor = runManagedRunner(input, deps, "doctor");
2846
+ if (managedRunnerWasBlocked(doctor)) return doctor;
2847
+ const result = doctor.result;
2848
+ return result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true ? { kind: "healthy" } : { kind: "blocked", reason: "runner-unhealthy" };
3133
2849
  }
3134
- if (includeBrowserState && loadPlaywrightCliPreference() === true) {
3135
- const playwright = resolvePlaywrightUpdateCheck({
3136
- enabled: true,
3137
- cli: detectPlaywrightCli()
2850
+ if (input.operation === "uninstall") {
2851
+ if (!sameRecord(owned.receipt.candidate, {
2852
+ package: input.registry.candidate.package,
2853
+ tarball: input.registry.candidate.tarball,
2854
+ provenance: input.registry.candidate.provenance
2855
+ })) {
2856
+ return { kind: "blocked", reason: "source-divergent" };
2857
+ }
2858
+ const cleanup = runManagedRunner(input, deps, "cleanup");
2859
+ if (managedRunnerWasBlocked(cleanup)) return cleanup;
2860
+ deps.backupSettings();
2861
+ const removed = deps.run({
2862
+ executable: input.detected.executable,
2863
+ args: ["remove", owned.source, "--no-approve"],
2864
+ environment: input.paths.environment
3138
2865
  });
3139
- if (playwright) p4.log[playwright.level](playwright.message);
2866
+ if (removed.exitCode !== 0 || removed.stderr !== "") return { kind: "blocked", reason: "remove-failed" };
2867
+ if (!deps.isPackageAbsent()) return { kind: "blocked", reason: "absence-unverified" };
2868
+ deps.deleteReceipt();
2869
+ return { kind: "uninstalled" };
3140
2870
  }
3141
- const checkSkillNames = skillsToScan(isGitClone(), upstreams);
3142
- if (checkSkillNames.length === 0) {
3143
- p4.log.info(
3144
- "Skills de terceros: pineadas con la versi\xF3n del stack (su revisi\xF3n upstream se hace desde el clon del repo)."
3145
- );
3146
- } else {
3147
- const skillQueries = await querySkillHeads(checkSkillNames, upstreams);
3148
- let moved = 0;
3149
- for (const { name, repo, info, head } of skillQueries) {
3150
- const label = info.modified ? `${name} (modificada localmente)` : name;
3151
- const pinned = info.commit;
3152
- if (!pinned)
3153
- p4.log.warn(`${label}: sin pin en upstreams.json \u2014 a\xF1ade el commit revisado.`);
3154
- else if (head === null)
3155
- p4.log.info(`${label}: pin ${pinned.slice(0, 7)} (no se pudo consultar el upstream).`);
3156
- else if (head === pinned)
3157
- p4.log.success(`${label}: al d\xEDa con el pin ${pinned.slice(0, 7)}.`);
3158
- else {
3159
- moved++;
3160
- p4.log.warn(
3161
- `${label}: el upstream se movi\xF3 (pin ${pinned.slice(0, 7)} \u2192 ${head.slice(0, 7)}).
3162
- Revisa el diff y, si lo aceptas, actualiza la copia vendorizada y el pin: github.com/${repo}/compare/${pinned.slice(0, 7)}...${head.slice(0, 7)}`
3163
- );
3164
- }
3165
- }
3166
- if (moved === 0 && skillQueries.every(({ head }) => head !== null)) {
3167
- p4.log.info("Skills de terceros: ning\xFAn upstream se ha movido respecto a su pin.");
3168
- }
2871
+ const nextSource = input.registry.candidate.package.source;
2872
+ if (nextSource === owned.source) return { kind: "healthy" };
2873
+ return {
2874
+ kind: "blocked",
2875
+ reason: "verified-update-required",
2876
+ remedy: "A cross-version Pi update requires verified replacement and rollback tgz artifacts."
2877
+ };
2878
+ }
2879
+
2880
+ // src/lib/pi-projection-lifecycle.ts
2881
+ var preparedPiProjectionUninstalls = /* @__PURE__ */ new WeakMap();
2882
+ function projectionScope(scope) {
2883
+ return {
2884
+ ...scope,
2885
+ settingsFile: path24.join(scope.codingAgentDir, "settings.json")
2886
+ };
2887
+ }
2888
+ function isInside(root, file) {
2889
+ const relative = path24.relative(path24.resolve(root), path24.resolve(file));
2890
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path24.sep}`) && !path24.isAbsolute(relative);
2891
+ }
2892
+ function isManagedPath(scope, file) {
2893
+ return isInside(scope.home, file) || isInside(scope.codingAgentDir, file);
2894
+ }
2895
+ function isAllowedPath(scope, file) {
2896
+ return isManagedPath(scope, file) || path24.resolve(file) === path24.resolve(scope.receiptFile);
2897
+ }
2898
+ function projectedPiAdapter(scope) {
2899
+ const paths = piAdapter.paths(scope.codingAgentDir);
2900
+ return {
2901
+ ...piAdapter,
2902
+ paths: () => ({
2903
+ ...paths,
2904
+ skillsDir: path24.join(scope.home, ".agents", "skills")
2905
+ })
2906
+ };
2907
+ }
2908
+ function projectionPlan(input, scope) {
2909
+ const ctx = {
2910
+ stackDir: input.stackDir,
2911
+ configDir: scope.codingAgentDir,
2912
+ engramBin: input.engramBin,
2913
+ models: DEFAULT_MODEL_MAP.codex,
2914
+ warnings: [],
2915
+ playwrightCliEnabled: input.playwrightCliEnabled
2916
+ };
2917
+ const adapter = projectedPiAdapter(scope);
2918
+ return [
2919
+ ...planSystemPrompt(adapter, ctx),
2920
+ ...planSkills(adapter, ctx),
2921
+ ...planCommands(adapter, ctx)
2922
+ ];
2923
+ }
2924
+ function canonicalActionContent(action) {
2925
+ return action.kind === "write" ? action.content : fs16.readFileSync(action.source, "utf8");
2926
+ }
2927
+ function hasActionDrift(action, deps) {
2928
+ return deps.readText(action.target) !== canonicalActionContent(action);
2929
+ }
2930
+ function applyActions(actions, deps) {
2931
+ for (const action of actions) {
2932
+ if (action.kind === "write") deps.writeText(action.target, action.content);
2933
+ else deps.copyFile(action.source, action.target);
3169
2934
  }
3170
- if (githubRateLimited()) {
3171
- p4.log.warn(rateLimitHint("GitHub limit\xF3 algunas consultas (rate limit sin token)."));
2935
+ }
2936
+ function assertPlanContained(plan, scope) {
2937
+ for (const action of plan) {
2938
+ if (!isAllowedPath(scope, action.target)) {
2939
+ throw new Error(`La proyecci\xF3n de Pi intent\xF3 escribir fuera del scope permitido: ${action.target}`);
2940
+ }
3172
2941
  }
3173
- p4.outro("Check completado.");
3174
- return 0;
3175
2942
  }
3176
- function isEngramRunning() {
2943
+ function receiptFor(plan, scope) {
2944
+ const systemPrompt = path24.join(scope.codingAgentDir, "AGENTS.md");
2945
+ const owned = [...new Set(
2946
+ plan.map((action) => path24.resolve(action.target)).filter((target) => target !== systemPrompt)
2947
+ )];
2948
+ return {
2949
+ schemaVersion: 1,
2950
+ scope: {
2951
+ kind: scope.kind,
2952
+ home: scope.home,
2953
+ codingAgentDir: scope.codingAgentDir,
2954
+ receiptFile: scope.receiptFile
2955
+ },
2956
+ owned
2957
+ };
2958
+ }
2959
+ function receiptContent(receipt) {
2960
+ return `${JSON.stringify(receipt, null, 2)}
2961
+ `;
2962
+ }
2963
+ function hasExactKeys(record, expected) {
2964
+ const keys = Object.keys(record);
2965
+ return keys.length === expected.length && expected.every((key) => keys.includes(key));
2966
+ }
2967
+ function parseReceipt2(raw, expected) {
2968
+ if (raw === null) return null;
3177
2969
  try {
3178
- if (process.platform === "win32") {
3179
- const ps = lookPath("powershell") ?? lookPath("pwsh");
3180
- if (ps) {
3181
- const out = runDetectedBin(ps, ["-NoProfile", "-Command", "Get-Process -Name engram -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count"], 5e3);
3182
- return out !== null ? parseInt(out.trim(), 10) > 0 : null;
3183
- }
2970
+ const receipt = JSON.parse(raw);
2971
+ if (receipt === null || typeof receipt !== "object" || Array.isArray(receipt)) return null;
2972
+ const version = Reflect.get(receipt, "schemaVersion");
2973
+ const receivedScope = Reflect.get(receipt, "scope");
2974
+ const owned = Reflect.get(receipt, "owned");
2975
+ if (version !== expected.schemaVersion || !hasExactKeys(receipt, ["schemaVersion", "scope", "owned"]) || receivedScope === null || typeof receivedScope !== "object" || Array.isArray(receivedScope) || !hasExactKeys(receivedScope, ["kind", "home", "codingAgentDir", "receiptFile"])) {
2976
+ return null;
2977
+ }
2978
+ if (Reflect.get(receivedScope, "kind") !== expected.scope.kind || Reflect.get(receivedScope, "home") !== expected.scope.home || Reflect.get(receivedScope, "codingAgentDir") !== expected.scope.codingAgentDir || Reflect.get(receivedScope, "receiptFile") !== expected.scope.receiptFile || !Array.isArray(owned) || owned.length !== expected.owned.length) {
2979
+ return null;
2980
+ }
2981
+ if (!owned.every((file, index) => typeof file === "string" && file === expected.owned[index])) {
3184
2982
  return null;
3185
- } else {
3186
- const pgrep = lookPath("pgrep");
3187
- if (!pgrep) return null;
3188
- try {
3189
- execFileSync5(pgrep, ["-x", "engram"], { stdio: "ignore" });
3190
- return true;
3191
- } catch {
3192
- return false;
3193
- }
3194
2983
  }
2984
+ return expected;
3195
2985
  } catch {
3196
2986
  return null;
3197
2987
  }
3198
2988
  }
3199
- function isGitClone(projectRoot = path26.dirname(stackRoot())) {
3200
- return fs20.existsSync(path26.join(projectRoot, ".git"));
2989
+ function expectedProjectionReceipt(input, scope) {
2990
+ const plan = projectionPlan(input, scope);
2991
+ assertPlanContained(plan, scope);
2992
+ return receiptFor(plan, scope);
3201
2993
  }
3202
- var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
3203
- function resolvePnpm() {
3204
- const bin = lookPath("pnpm") ?? lookPath("pnpm.cmd") ?? lookPath("pnpm.ps1");
3205
- if (!bin) throw new Error("pnpm no encontrado en PATH.");
3206
- return bin;
2994
+ function realPiProjectionScope() {
2995
+ return {
2996
+ kind: "real",
2997
+ home: HOME,
2998
+ codingAgentDir: process.env.PI_CODING_AGENT_DIR ?? path24.join(HOME, ".pi", "agent"),
2999
+ receiptFile: path24.join(dataDir(), "pi-projection-receipt.json")
3000
+ };
3207
3001
  }
3208
- function cleanupTmp(dir) {
3002
+ function readTextOnlyIfMissing(file) {
3209
3003
  try {
3210
- fs20.rmSync(dir, { recursive: true, force: true });
3211
- } catch {
3004
+ return fs16.readFileSync(file, "utf8");
3005
+ } catch (error) {
3006
+ if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "ENOENT") return null;
3007
+ throw error;
3212
3008
  }
3213
3009
  }
3214
- function updateStackGitClone() {
3215
- const projectRoot = path26.dirname(stackRoot());
3216
- const git = lookPath("git");
3217
- if (!git) throw new Error("git no encontrado en PATH.");
3218
- const pnpm = resolvePnpm();
3219
- p4.log.info("Ejecutando git pull\u2026");
3220
- execFileSync5(git, ["pull"], { cwd: projectRoot, stdio: "inherit" });
3221
- p4.log.info("Ejecutando pnpm install\u2026");
3222
- execFileSync5(pnpm, ["install"], { cwd: projectRoot, stdio: "inherit" });
3223
- p4.log.info("Ejecutando pnpm build\u2026");
3224
- execFileSync5(pnpm, ["build"], { cwd: projectRoot, stdio: "inherit" });
3010
+ function readRealPiProjectionOwned() {
3011
+ const scope = projectionScope(realPiProjectionScope());
3012
+ const expected = expectedProjectionReceipt({
3013
+ operation: "doctor",
3014
+ scope,
3015
+ packageSource: "",
3016
+ stackDir: stackRoot(),
3017
+ engramBin: "",
3018
+ playwrightCliEnabled: false
3019
+ }, scope);
3020
+ let receiptContent2;
3021
+ try {
3022
+ receiptContent2 = readTextOnlyIfMissing(scope.receiptFile);
3023
+ } catch (error) {
3024
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
3025
+ return { kind: "unreadable", file: scope.receiptFile, code };
3026
+ }
3027
+ if (receiptContent2 === null) {
3028
+ return { kind: "absent" };
3029
+ }
3030
+ const receipt = parseReceipt2(receiptContent2, expected);
3031
+ return receipt === null ? { kind: "corrupt", file: scope.receiptFile } : { kind: "valid", owned: receipt.owned };
3225
3032
  }
3226
- function updateStackGlobal() {
3227
- const pnpm = resolvePnpm();
3228
- p4.log.info("Ejecutando pnpm add -g jorgex-stack@latest\u2026");
3229
- execFileSync5(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
3033
+ function manifestOwned(manifest) {
3034
+ return new Set([
3035
+ ...manifest.runtimes.codex?.owned ?? [],
3036
+ ...manifest.runtimes.opencode?.owned ?? []
3037
+ ].map((file) => path24.resolve(file)));
3230
3038
  }
3231
- async function downloadSkillToTemp(repo, sha, skillPath) {
3232
- const root = fs20.mkdtempSync(path26.join(os4.tmpdir(), "jorgex-skill-"));
3039
+ function withoutManagedPromptSections(content) {
3040
+ return ["system-prompt", "engram-protocol", "browser"].reduce((current, section) => removeMarkdownSection(current, section), content);
3041
+ }
3042
+ function uniquePaths(paths) {
3043
+ return [...new Set(paths.map((file) => path24.resolve(file)))];
3044
+ }
3045
+ function blocked2(reason, paths, remedy) {
3046
+ return { kind: "blocked", reason, paths: uniquePaths(paths), remedy };
3047
+ }
3048
+ function backupExisting(paths, deps) {
3049
+ const candidates = uniquePaths(paths);
3050
+ let existing;
3233
3051
  try {
3234
- const result = await downloadRepoTarball(repo, sha, root, skillPath);
3235
- if (!result.ok) {
3236
- cleanupTmp(root);
3237
- return { error: result.reason };
3238
- }
3239
- if (skillPath) {
3240
- const sub = path26.resolve(path26.join(root, skillPath));
3241
- if (!isContainedIn(sub, root)) {
3242
- cleanupTmp(root);
3243
- return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
3244
- }
3245
- if (fs20.existsSync(sub)) return { dir: sub, root };
3246
- const lastSeg = skillPath.split("/").pop();
3247
- const sub2 = path26.resolve(path26.join(root, lastSeg));
3248
- if (isContainedIn(sub2, root) && fs20.existsSync(sub2)) {
3249
- if (!validateExtractedTree(sub2)) {
3250
- cleanupTmp(root);
3251
- return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
3252
- }
3253
- return { dir: sub2, root };
3254
- }
3255
- cleanupTmp(root);
3256
- return { error: `la ruta "${skillPath}" no existe en el tarball del upstream` };
3257
- }
3258
- return { dir: root, root };
3259
- } catch (err) {
3260
- cleanupTmp(root);
3261
- return { error: err instanceof Error ? err.message : "error desconocido" };
3052
+ existing = candidates.filter((file) => deps.readText(file) !== null);
3053
+ } catch {
3054
+ return { kind: "backup-failed", paths: candidates };
3055
+ }
3056
+ if (existing.length === 0) return { kind: "backed-up" };
3057
+ try {
3058
+ deps.backup(existing);
3059
+ return { kind: "backed-up" };
3060
+ } catch {
3061
+ return { kind: "backup-failed", paths: existing };
3262
3062
  }
3263
3063
  }
3264
- function pruneEngramDbBackups() {
3064
+ function backupFailure(paths) {
3065
+ return blocked2(
3066
+ "projection-backup-failed",
3067
+ paths,
3068
+ "Revisa permisos y espacio disponible para crear las copias de seguridad indicadas antes de reintentar."
3069
+ );
3070
+ }
3071
+ function cleanupFailure(paths) {
3072
+ return blocked2(
3073
+ "projection-cleanup-failed",
3074
+ paths,
3075
+ "Revisa permisos, cierra procesos que usen estas rutas y vuelve a ejecutar la desinstalaci\xF3n."
3076
+ );
3077
+ }
3078
+ function receiptInvalid(receiptFile) {
3079
+ return blocked2(
3080
+ "projection-receipt-invalid",
3081
+ [receiptFile],
3082
+ "Restaura un receipt de proyecci\xF3n \xEDntegro o elimina manualmente solo los archivos gestionados tras verificar su propiedad."
3083
+ );
3084
+ }
3085
+ function receiptUnreadable(receiptFile) {
3086
+ return blocked2(
3087
+ "projection-receipt-unreadable",
3088
+ [receiptFile],
3089
+ "Revisa los permisos o el estado de E/S del receipt de proyecci\xF3n y vuelve a intentarlo."
3090
+ );
3091
+ }
3092
+ function writeFailure(file) {
3093
+ return blocked2(
3094
+ "projection-write-failed",
3095
+ [file],
3096
+ "Revisa los permisos de escritura de esta ruta y vuelve a ejecutar la desinstalaci\xF3n."
3097
+ );
3098
+ }
3099
+ function readProjectionReceipt(receiptFile, deps) {
3265
3100
  try {
3266
- const dir = dataDir();
3267
- if (!fs20.existsSync(dir)) return;
3268
- const backups = fs20.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs20.statSync(path26.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
3269
- for (const old of backups.slice(3)) {
3270
- try {
3271
- fs20.rmSync(path26.join(dir, old.name));
3272
- } catch {
3273
- }
3101
+ const content = deps.readText(receiptFile);
3102
+ return content === null ? { kind: "absent" } : { kind: "present", content };
3103
+ } catch (error) {
3104
+ if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "ENOENT") {
3105
+ return { kind: "absent" };
3274
3106
  }
3275
- } catch {
3107
+ return { kind: "unreadable" };
3276
3108
  }
3277
3109
  }
3278
- function rotateLockedBinary(binPath, sweepRoot = HOME) {
3279
- if (!fs20.existsSync(binPath)) return null;
3280
- const dir = path26.dirname(binPath);
3281
- const base = path26.basename(binPath);
3282
- const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
3283
- const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
3284
- const resolvedDir = path26.resolve(dir);
3285
- if (resolvedDir === path26.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
3110
+ function preparePiProjectionUninstall(input, deps) {
3111
+ if (input.operation !== "uninstall") {
3112
+ return cleanupFailure([]);
3113
+ }
3114
+ const scope = projectionScope(input.scope);
3115
+ const expectedReceipt2 = expectedProjectionReceipt(input, scope);
3116
+ const prompt = path24.join(scope.codingAgentDir, "AGENTS.md");
3117
+ let existingPrompt;
3118
+ try {
3119
+ existingPrompt = deps.readText(prompt);
3120
+ } catch {
3121
+ return cleanupFailure([prompt]);
3122
+ }
3123
+ const promptContent = existingPrompt === null ? null : withoutManagedPromptSections(existingPrompt);
3124
+ const promptUpdate = promptContent === null || promptContent === existingPrompt ? null : { file: path24.resolve(prompt), content: promptContent };
3125
+ const receiptRead = readProjectionReceipt(scope.receiptFile, deps);
3126
+ if (receiptRead.kind === "unreadable") return receiptUnreadable(scope.receiptFile);
3127
+ let ownedToRemove = [];
3128
+ let receiptFile = null;
3129
+ let backupTargets = promptUpdate === null ? [] : [promptUpdate.file];
3130
+ if (receiptRead.kind === "present") {
3131
+ const receipt = parseReceipt2(receiptRead.content, expectedReceipt2);
3132
+ if (receipt === null) return receiptInvalid(scope.receiptFile);
3133
+ let retained;
3286
3134
  try {
3287
- for (const entry of fs20.readdirSync(dir)) {
3288
- if (oldPattern.test(entry)) {
3289
- try {
3290
- fs20.rmSync(path26.join(dir, entry), { force: true });
3291
- } catch {
3292
- }
3293
- }
3294
- }
3135
+ retained = manifestOwned(deps.readManifest());
3295
3136
  } catch {
3296
- }
3297
- }
3298
- const rotated = path26.join(dir, `${base}.old-${Date.now()}`);
3299
- fs20.renameSync(binPath, rotated);
3300
- return rotated;
3137
+ return cleanupFailure([scope.receiptFile]);
3138
+ }
3139
+ ownedToRemove = receipt.owned.filter((owned) => !retained.has(owned));
3140
+ receiptFile = path24.resolve(scope.receiptFile);
3141
+ backupTargets = [...backupTargets, ...receipt.owned, receiptFile];
3142
+ }
3143
+ const backup = backupExisting(backupTargets, deps);
3144
+ if (backup.kind === "backup-failed") return backupFailure(backup.paths);
3145
+ const token = {};
3146
+ preparedPiProjectionUninstalls.set(token, {
3147
+ prompt: promptUpdate,
3148
+ ownedToRemove,
3149
+ receiptFile
3150
+ });
3151
+ return { kind: "prepared", plan: token };
3301
3152
  }
3302
- async function updateEngram(engramRepo, latestVersion) {
3303
- if (isEngramRunning()) {
3304
- p4.log.info(
3305
- "Engram est\xE1 en ejecuci\xF3n: los procesos vivos seguir\xE1n usando la versi\xF3n antigua hasta que reinicies los clientes (Claude Code/OpenCode/Codex)."
3306
- );
3153
+ function completePiProjectionUninstall(plan, deps) {
3154
+ if (typeof plan !== "object" || plan === null) {
3155
+ return cleanupFailure([]);
3307
3156
  }
3308
- const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path26.join(HOME, ".engram");
3309
- const engramDb = path26.join(engramDataDir, "engram.db");
3310
- if (fs20.existsSync(engramDb)) {
3311
- const doBackup = await p4.confirm({
3312
- message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
3313
- initialValue: true
3314
- });
3315
- if (!p4.isCancel(doBackup) && doBackup) {
3316
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3317
- const dest = path26.join(dataDir(), `engram-db-backup-${ts}.db`);
3318
- try {
3319
- if (!fs20.existsSync(dataDir())) fs20.mkdirSync(dataDir(), { recursive: true });
3320
- fs20.copyFileSync(engramDb, dest);
3321
- p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
3322
- pruneEngramDbBackups();
3323
- } catch (err) {
3324
- p4.log.warn(`No se pudo copiar la DB: ${err instanceof Error ? err.message : err}. Continuando sin backup.`);
3325
- }
3326
- }
3157
+ const prepared = preparedPiProjectionUninstalls.get(plan);
3158
+ if (prepared === void 0) {
3159
+ return cleanupFailure([]);
3327
3160
  }
3328
- let anyChannelTried = false;
3329
- const brew = lookPath("brew");
3330
- if (brew) {
3331
- let brewManages = false;
3161
+ if (prepared.prompt !== null) {
3332
3162
  try {
3333
- execFileSync5(brew, ["list", "engram"], { stdio: "pipe" });
3334
- brewManages = true;
3163
+ deps.writeText(prepared.prompt.file, prepared.prompt.content);
3335
3164
  } catch {
3336
- }
3337
- if (brewManages) {
3338
- anyChannelTried = true;
3339
- p4.log.info("Actualizando engram con brew\u2026");
3340
- try {
3341
- execFileSync5(brew, ["upgrade", "engram"], { stdio: "inherit" });
3342
- return true;
3343
- } catch (err) {
3344
- p4.log.error(`brew upgrade engram fall\xF3: ${err instanceof Error ? err.message : err}`);
3345
- return false;
3346
- }
3165
+ return writeFailure(prepared.prompt.file);
3347
3166
  }
3348
3167
  }
3349
- const go = lookPath("go");
3350
- if (go) {
3351
- anyChannelTried = true;
3352
- let rotated = null;
3353
- const bin = detectEngram();
3354
- if (process.platform === "win32" && bin) {
3355
- try {
3356
- rotated = rotateLockedBinary(bin);
3357
- } catch (err) {
3358
- p4.log.warn(
3359
- `No se pudo rotar el binario en uso: ${err instanceof Error ? err.message : err}. go install puede fallar por bloqueo.`
3360
- );
3361
- }
3362
- }
3363
- p4.log.info(`Actualizando engram con go install (${latestVersion})\u2026`);
3168
+ for (const owned of prepared.ownedToRemove) {
3364
3169
  try {
3365
- execFileSync5(
3366
- go,
3367
- ["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
3368
- { stdio: "inherit" }
3369
- );
3370
- const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs20.existsSync(bin ?? "") });
3371
- if (rollbackOk.action === "restore") {
3372
- try {
3373
- fs20.renameSync(rotated, bin);
3374
- p4.log.warn(rollbackOk.messages.onRestore);
3375
- } catch {
3376
- p4.log.warn(rollbackOk.messages.onRenameFail);
3377
- }
3378
- }
3379
- return true;
3380
- } catch (err) {
3381
- const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs20.existsSync(bin ?? "") });
3382
- if (rollbackFail.action === "restore") {
3383
- try {
3384
- fs20.renameSync(rotated, bin);
3385
- p4.log.info(rollbackFail.messages.onRestore);
3386
- } catch {
3387
- p4.log.error(rollbackFail.messages.onRenameFail);
3388
- }
3389
- } else if (rollbackFail.action === "leave_old") {
3390
- p4.log.warn(rollbackFail.messages.onLeaveOld);
3391
- }
3392
- p4.log.error(`go install fall\xF3: ${err instanceof Error ? err.message : err}`);
3393
- return false;
3170
+ deps.removeFile(owned);
3171
+ } catch {
3172
+ return cleanupFailure([owned]);
3394
3173
  }
3395
3174
  }
3396
- if (anyChannelTried) {
3397
- p4.log.error(
3398
- `Los canales de actualizaci\xF3n disponibles fallaron \u2014 revisa el error arriba; descarga manual: github.com/${engramRepo}/releases/tag/v${latestVersion}`
3399
- );
3400
- } else {
3401
- p4.log.warn(
3402
- `No se encontr\xF3 brew ni go en PATH.
3403
- Descarga manual: github.com/${engramRepo}/releases/tag/v${latestVersion}`
3404
- );
3175
+ if (prepared.receiptFile !== null) {
3176
+ try {
3177
+ deps.removeFile(prepared.receiptFile);
3178
+ } catch {
3179
+ return cleanupFailure([prepared.receiptFile]);
3180
+ }
3405
3181
  }
3406
- return false;
3182
+ return { kind: "uninstalled" };
3407
3183
  }
3408
- function resolveEngramRollback(input) {
3409
- const { installOk, rotated, bin, binExists } = input;
3410
- if (!rotated || !bin) return { action: "none" };
3411
- if (installOk && !binExists) {
3412
- return {
3413
- action: "restore",
3414
- messages: {
3415
- onRestore: `go install instal\xF3 en otra ruta (GOBIN distinto); el binario activo en ${bin} se ha restaurado. Comprueba qu\xE9 engram resuelve tu PATH.`,
3416
- onRenameFail: `go install instal\xF3 en otra ruta y no se pudo restaurar ${bin}; tu binario anterior est\xE1 en ${rotated}.`
3417
- }
3418
- };
3419
- }
3420
- if (!installOk && !binExists) {
3421
- return {
3422
- action: "restore",
3423
- messages: {
3424
- onRestore: "Rotaci\xF3n revertida \u2014 el binario anterior sigue en su sitio.",
3425
- onRenameFail: `No se pudo revertir la rotaci\xF3n: tu binario anterior est\xE1 en ${rotated} \u2014 ren\xF3mbralo a ${bin} a mano.`
3426
- }
3427
- };
3428
- }
3429
- if (!installOk && binExists) {
3430
- return {
3431
- action: "leave_old",
3432
- messages: {
3433
- onLeaveOld: `El install fall\xF3 pero dej\xF3 un binario en ${bin}; tu copia anterior queda en ${rotated}.`
3434
- }
3435
- };
3184
+ function runPiProjectionLifecycle(input, deps) {
3185
+ const scope = projectionScope(input.scope);
3186
+ if (input.operation === "uninstall") {
3187
+ const prepared = preparePiProjectionUninstall(input, deps);
3188
+ return prepared.kind === "blocked" ? prepared : completePiProjectionUninstall(prepared.plan, deps);
3189
+ }
3190
+ const plan = projectionPlan(input, scope);
3191
+ assertPlanContained(plan, scope);
3192
+ const receipt = receiptFor(plan, scope);
3193
+ const expectedReceipt2 = receiptContent(receipt);
3194
+ const drifted = plan.filter((action) => hasActionDrift(action, deps));
3195
+ if (input.operation === "doctor") {
3196
+ const paths = drifted.map((action) => path24.resolve(action.target));
3197
+ const currentSettings2 = deps.readText(scope.settingsFile);
3198
+ if (currentSettings2 === null || filterProjectedPiPackage(currentSettings2, input.packageSource) !== currentSettings2) {
3199
+ paths.push(scope.settingsFile);
3200
+ }
3201
+ if (deps.readText(scope.receiptFile) !== expectedReceipt2) paths.push(scope.receiptFile);
3202
+ const unique = uniquePaths(paths);
3203
+ return unique.length === 0 ? { kind: "healthy" } : { kind: "drift", paths: unique };
3204
+ }
3205
+ const currentSettings = deps.readText(scope.settingsFile);
3206
+ const filteredSettings = currentSettings === null ? null : filterProjectedPiPackage(currentSettings, input.packageSource);
3207
+ if (currentSettings !== null && filteredSettings === null) {
3208
+ return { kind: "blocked", reason: "source-divergent" };
3436
3209
  }
3437
- return { action: "none" };
3210
+ const packageWillChange = filteredSettings !== null && filteredSettings !== currentSettings;
3211
+ const receiptChanged = deps.readText(scope.receiptFile) !== expectedReceipt2;
3212
+ if (drifted.length > 0 || packageWillChange || receiptChanged) {
3213
+ const backup = backupExisting([
3214
+ ...drifted.map((action) => action.target),
3215
+ ...packageWillChange ? [scope.settingsFile] : [],
3216
+ ...receiptChanged ? [scope.receiptFile] : []
3217
+ ], deps);
3218
+ if (backup.kind === "backup-failed") return backupFailure(backup.paths);
3219
+ }
3220
+ applyActions(drifted, deps);
3221
+ if (packageWillChange && filteredSettings !== null) deps.writeText(scope.settingsFile, filteredSettings);
3222
+ if (receiptChanged) deps.writeText(scope.receiptFile, expectedReceipt2);
3223
+ if (input.operation === "install") return { kind: "installed", receipt };
3224
+ return { kind: "synced", changed: drifted.length > 0 || packageWillChange || receiptChanged };
3225
+ }
3226
+ function systemProjectionLifecycle(input) {
3227
+ const targetRoot = input.targetDir === void 0 ? null : path24.resolve(input.targetDir);
3228
+ const scope = targetRoot === null ? realPiProjectionScope() : {
3229
+ kind: "target-dir",
3230
+ home: path24.join(targetRoot, "home"),
3231
+ codingAgentDir: path24.join(targetRoot, "pi-agent"),
3232
+ receiptFile: path24.join(targetRoot, "state", "pi-projection-receipt.json")
3233
+ };
3234
+ return {
3235
+ input: {
3236
+ operation: input.operation,
3237
+ scope,
3238
+ packageSource: input.packageSource,
3239
+ stackDir: stackRoot(),
3240
+ engramBin: input.engramBin ?? "",
3241
+ playwrightCliEnabled: input.playwrightCliEnabled
3242
+ },
3243
+ deps: {
3244
+ readText: readTextOnlyIfMissing,
3245
+ backup: (paths) => createBackup(
3246
+ paths,
3247
+ `pi-projection-${input.operation}`,
3248
+ targetRoot === null ? void 0 : path24.join(targetRoot, "backups")
3249
+ ),
3250
+ writeText,
3251
+ copyFile,
3252
+ removeFile: (file) => fs16.rmSync(file, { force: true }),
3253
+ readManifest: targetRoot === null ? readManifest : () => ({ runtimes: {} })
3254
+ }
3255
+ };
3438
3256
  }
3439
- function buildEligibleSkillUpdates(skills) {
3440
- const result = [];
3441
- for (const { name, repo, info, head } of skills) {
3442
- if (info.kind === "release") continue;
3443
- const pinned = info.commit;
3444
- if (!pinned) continue;
3445
- if (head === null) continue;
3446
- if (head === pinned) continue;
3447
- result.push({
3448
- name,
3449
- repo,
3450
- head,
3451
- pinned,
3452
- skillPath: info.path,
3453
- modified: info.modified ?? false
3454
- });
3455
- }
3456
- return result;
3257
+ function preparePiProjectionUninstallSystem(input) {
3258
+ const lifecycle = systemProjectionLifecycle(input);
3259
+ return preparePiProjectionUninstall(lifecycle.input, lifecycle.deps);
3457
3260
  }
3458
- function hasIncompleteSkillScan(skills) {
3459
- return skills.some(({ info, head }) => info.kind !== "release" && (!info.commit || head === null));
3261
+ function completePiProjectionUninstallSystem(plan, input) {
3262
+ const lifecycle = systemProjectionLifecycle(input);
3263
+ return completePiProjectionUninstall(plan, lifecycle.deps);
3460
3264
  }
3461
- function resolveUpdateSyncRequired(updated) {
3462
- return updated.some((item) => item === "stack" || item === "skill" || item.startsWith("skill:"));
3265
+ function runPiProjectionLifecycleSystem(input) {
3266
+ const lifecycle = systemProjectionLifecycle(input);
3267
+ return runPiProjectionLifecycle(lifecycle.input, lifecycle.deps);
3463
3268
  }
3464
- async function runInteractiveUpdate(localVersion, yes, dryRun = false, includeBrowserState = true) {
3465
- if (dryRun || yes || !process.stdout.isTTY) {
3466
- return { exitCode: await runUpdateCheck(localVersion, includeBrowserState), appliedUpdates: false, syncRequired: false };
3269
+
3270
+ // src/uninstall.ts
3271
+ function resolvePlaywrightUninstallPlan(input) {
3272
+ return {
3273
+ actions: input.removePackage ? ["remove"] : [],
3274
+ preserveBrowserData: true
3275
+ };
3276
+ }
3277
+ async function runUninstall(opts) {
3278
+ p2.intro(`jorgex-stack ${opts.dryRun ? "uninstall (dry-run)" : "uninstall"}`);
3279
+ const useBrowserPreferences = opts.targetDir === void 0;
3280
+ const preferenceErrors = useBrowserPreferences ? [...browserPreferenceErrors(), primaryModelOwnershipError()].filter((error) => error !== null) : [];
3281
+ if (preferenceErrors.length > 0) {
3282
+ for (const error of preferenceErrors) p2.log.error(error);
3283
+ p2.outro("Uninstall cancelado: corrige el estado de configuraci\xF3n indicado arriba antes de reintentar.");
3284
+ return 1;
3467
3285
  }
3468
- p4.intro("jorgex-stack update");
3469
- if (includeBrowserState) {
3470
- const preferenceErrors = browserPreferenceErrors();
3471
- if (preferenceErrors.length > 0) {
3472
- for (const error of preferenceErrors) p4.log.error(error);
3473
- p4.outro("Update cancelado: corrige las preferencias de navegador antes de reintentar.");
3474
- return { exitCode: 1, appliedUpdates: false, syncRequired: false };
3475
- }
3286
+ const removesSharedSkills = opts.runtimes.some((runtime) => runtime === "codex" || runtime === "opencode");
3287
+ const piProjection = useBrowserPreferences && removesSharedSkills ? readRealPiProjectionOwned() : { kind: "absent" };
3288
+ if (piProjection.kind === "corrupt") {
3289
+ p2.log.error(`Pi: receipt de proyecci\xF3n inv\xE1lido en ${piProjection.file}. Restaura o repara ese receipt antes de reintentar.`);
3290
+ p2.outro("Uninstall cancelado: restaura o repara el receipt de proyecci\xF3n de Pi antes de reintentar.");
3291
+ return 1;
3476
3292
  }
3477
- const upstreams = loadUpstreams();
3293
+ if (piProjection.kind === "unreadable") {
3294
+ p2.log.error(`Pi: receipt de proyecci\xF3n ilegible en ${piProjection.file} (${piProjection.code}). Revisa los permisos o el estado de E/S antes de reintentar.`);
3295
+ p2.outro("Uninstall cancelado: revisa los permisos o el estado de E/S del receipt de proyecci\xF3n de Pi antes de reintentar.");
3296
+ return 1;
3297
+ }
3298
+ const stackDir = stackRoot();
3299
+ const mcp = loadCanonicalMcp(stackDir);
3300
+ const hooks = loadCanonicalHooks(stackDir);
3478
3301
  let exitCode = 0;
3479
- let appliedUpdates = false;
3480
- const updated = [];
3481
- const maintainer = isGitClone();
3482
- const spin = p4.spinner();
3483
- spin.start("Consultando versiones upstream\u2026");
3484
- const skillNames = skillsToScan(maintainer, upstreams);
3485
- const [npmLatest, engramLatestRaw, skillHeads] = await Promise.all([
3486
- latestNpmVersion("jorgex-stack"),
3487
- (async () => {
3488
- const repo = upstreams.tools["engram"]?.source.replace(/^github:/, "");
3489
- if (!repo) return null;
3490
- return { repo, version: await latestGithubRelease(repo) };
3491
- })(),
3492
- querySkillHeads(skillNames, upstreams)
3493
- ]);
3494
- spin.stop("Consulta completada.");
3495
- if (githubRateLimited()) {
3496
- p4.log.warn(rateLimitHint("GitHub limit\xF3 algunas consultas \u2014 los upstreams 'sin conexi\xF3n' pueden ser eso."));
3497
- }
3498
- const updateItems = [];
3499
- const engramData = engramLatestRaw;
3500
- let stackNeedsUpdate = false;
3501
- if (npmLatest === null) {
3502
- p4.log.info(`jorgex-stack: v${localVersion} local (a\xFAn no publicado en npm o sin red).`);
3503
- } else if (npmLatest === localVersion) {
3504
- p4.log.success(`jorgex-stack: v${localVersion} \u2014 al d\xEDa.`);
3505
- } else {
3506
- stackNeedsUpdate = true;
3507
- const mode = maintainer ? STACK_METHOD_CLONE : `pnpm add -g jorgex-stack@${npmLatest}`;
3508
- updateItems.push({
3509
- value: "stack",
3510
- label: `jorgex-stack: v${localVersion} \u2192 v${npmLatest}`,
3511
- hint: mode
3302
+ let removeEngram = opts.removeEngram;
3303
+ if (!removeEngram && !opts.yes && !opts.dryRun && process.stdout.isTTY) {
3304
+ const answer = await p2.confirm({
3305
+ message: "\xBFQuitar tambi\xE9n el registro de Engram (MCP/plugin) de los runtimes? Tus memorias (~/.engram) y el binario NO se tocan en ning\xFAn caso.",
3306
+ initialValue: false
3512
3307
  });
3513
- }
3514
- let engramNeedsUpdate = false;
3515
- let engramLocalVersion = null;
3516
- if (engramData) {
3517
- const engramBinLocal = detectEngram();
3518
- engramLocalVersion = engramBinLocal ? engramVersion(engramBinLocal) : null;
3519
- if (engramLocalVersion === null) {
3520
- p4.log.warn("engram: no detectado en esta m\xE1quina.");
3521
- } else if (engramData.version === null) {
3522
- p4.log.info(`engram: ${engramLocalVersion} local (no se pudo consultar el upstream).`);
3523
- } else if (engramData.version === engramLocalVersion) {
3524
- p4.log.success(`engram: ${engramLocalVersion} \u2014 al d\xEDa.`);
3525
- } else {
3526
- engramNeedsUpdate = true;
3527
- updateItems.push({
3528
- value: "engram",
3529
- label: `engram: ${engramLocalVersion} \u2192 ${engramData.version}`,
3530
- hint: "canal nativo (brew \u2192 go install \u2192 URL)"
3531
- });
3308
+ if (p2.isCancel(answer)) {
3309
+ p2.cancel("Cancelado \u2014 no se ha tocado nada.");
3310
+ return 1;
3532
3311
  }
3312
+ removeEngram = answer === true;
3533
3313
  }
3534
- let playwrightNeedsUpdate = false;
3535
- if (includeBrowserState && loadPlaywrightCliPreference() === true) {
3536
- const playwright = detectPlaywrightCli();
3537
- if (playwright.status === "current") {
3538
- p4.log.success("Playwright CLI: al d\xEDa.");
3539
- } else {
3540
- playwrightNeedsUpdate = true;
3541
- const current = playwright.detectedVersion ?? playwright.status;
3542
- updateItems.push({
3543
- value: "playwright-cli",
3544
- label: `Playwright CLI: ${current} \u2192 pin aprobado`,
3545
- hint: "pnpm add --global @playwright/cli@0.1.18"
3546
- });
3314
+ const mcpForUnmerge = removeEngram ? mcp : { servers: Object.fromEntries(Object.entries(mcp.servers).filter(([name]) => name !== "engram")) };
3315
+ if (!removeEngram) {
3316
+ p2.log.info("Engram se conserva: memorias, binario y registro intactos (usa --remove-engram para desregistrarlo).");
3317
+ }
3318
+ const retained = /* @__PURE__ */ new Set();
3319
+ if (useBrowserPreferences) {
3320
+ if (piProjection.kind === "valid") {
3321
+ for (const target of piProjection.owned) retained.add(target);
3322
+ }
3323
+ for (const keep of Object.values(ADAPTERS)) {
3324
+ if (opts.runtimes.includes(keep.id)) continue;
3325
+ const detection = keep.detect();
3326
+ if (!detection.installed) continue;
3327
+ const keepCtx = makeContext(keep, detection.configDir);
3328
+ if (!keepCtx) continue;
3329
+ for (const action of buildPlan(keep, keepCtx)) retained.add(path25.resolve(action.target));
3547
3330
  }
3548
3331
  }
3549
- const typedSkillHeads = skillHeads;
3550
- for (const { name, repo, info, head } of typedSkillHeads) {
3551
- if (info.kind === "release") {
3552
- if (head !== null && info.commit && head !== info.commit) {
3553
- p4.log.warn(`${name} (release): upstream se movi\xF3. Revisa: github.com/${repo}/releases`);
3554
- }
3332
+ for (const id of opts.runtimes) {
3333
+ const adapter = ADAPTERS[id];
3334
+ if (!adapter) {
3335
+ p2.log.warn(`${id}: sin adapter \u2014 omitido.`);
3555
3336
  continue;
3556
3337
  }
3557
- const label = info.modified ? `${name} (modificada localmente)` : name;
3558
- const pinned = info.commit;
3559
- if (!pinned) {
3560
- p4.log.warn(`${label}: sin pin \u2014 no se puede actualizar.`);
3561
- } else if (head === null) {
3562
- p4.log.info(`${label}: sin conexi\xF3n al upstream.`);
3563
- } else if (head === pinned) {
3564
- p4.log.success(`${label}: al d\xEDa (pin ${pinned.slice(0, 7)}).`);
3338
+ const detection = adapter.detect();
3339
+ const configDir = opts.targetDir ?? detection.configDir;
3340
+ if (!detection.installed && opts.targetDir === void 0) {
3341
+ p2.log.warn(`${adapter.name} no detectado \u2014 omitido.`);
3342
+ continue;
3565
3343
  }
3566
- }
3567
- if (!maintainer) {
3568
- p4.log.info(
3569
- "Skills de terceros: pineadas con esta versi\xF3n del stack \u2014 su revisi\xF3n upstream se hace desde el clon del repo."
3344
+ const ctx = makeContext(adapter, configDir, void 0, useBrowserPreferences);
3345
+ if (!ctx) continue;
3346
+ ctx.preserveEngram = !removeEngram;
3347
+ const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
3348
+ const mergedTargets = new Set(unmerge.map((a) => path25.resolve(a.target)));
3349
+ const usingRealConfig = opts.targetDir === void 0;
3350
+ const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
3351
+ const pruneRoot = usingRealConfig ? HOME : path25.dirname(configDir);
3352
+ const planTargets = [
3353
+ .../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path25.resolve(a.target)), ...prevOwned.map((t) => path25.resolve(t))])
3354
+ ].filter((t) => !mergedTargets.has(t) && fs17.existsSync(t));
3355
+ const deleteTargets = planTargets.filter(
3356
+ (t) => !retained.has(t) && !(ctx.preserveEngram && path25.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
3570
3357
  );
3571
- }
3572
- const skillUpdates = buildEligibleSkillUpdates(typedSkillHeads);
3573
- for (const skillInfo of skillUpdates) {
3574
- const modifiedWarning = skillInfo.modified ? " \u26A0 modificada localmente" : "";
3575
- updateItems.push({
3576
- value: `skill:${skillInfo.name}`,
3577
- label: `skill ${skillInfo.name}: ${skillInfo.pinned.slice(0, 7)} \u2192 ${skillInfo.head.slice(0, 7)}${modifiedWarning}`,
3578
- hint: `github.com/${skillInfo.repo}/compare/${skillInfo.pinned.slice(0, 7)}...${skillInfo.head.slice(0, 7)}`
3579
- });
3580
- }
3581
- if (updateItems.length === 0) {
3582
- if (maintainer && hasIncompleteSkillScan(typedSkillHeads)) {
3583
- p4.outro("Escaneo incompleto: no se pudo comprobar el estado de todas las skills.");
3584
- return { exitCode: 1, appliedUpdates: false, syncRequired: false };
3358
+ const sharedKept = planTargets.length - deleteTargets.length;
3359
+ p2.log.step(`${adapter.name} \u2192 ${configDir}`);
3360
+ p2.log.info(`${deleteTargets.length} archivos a borrar, ${unmerge.length} archivos compartidos a limpiar`);
3361
+ if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
3362
+ if (opts.dryRun) continue;
3363
+ let backup;
3364
+ try {
3365
+ backup = createBackup(
3366
+ [...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs17.existsSync(t))],
3367
+ `uninstall-${id}`
3368
+ );
3369
+ } catch (error) {
3370
+ p2.log.error(`${adapter.name}: no se pudo respaldar una configuraci\xF3n ilegible en ${configDir} \u2014 ${error instanceof Error ? error.message : String(error)}.`);
3371
+ exitCode = 1;
3372
+ continue;
3585
3373
  }
3586
- p4.outro("Todo al d\xEDa. No hay actualizaciones disponibles.");
3587
- return { exitCode: 0, appliedUpdates: false, syncRequired: false };
3374
+ if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
3375
+ for (const target of deleteTargets) {
3376
+ fs17.rmSync(target, { force: true });
3377
+ pruneEmptyDirs(target, pruneRoot);
3378
+ }
3379
+ for (const action of unmerge) {
3380
+ if (action.kind !== "write") continue;
3381
+ if (action.content.trim() === "") {
3382
+ fs17.rmSync(action.target, { force: true });
3383
+ } else {
3384
+ writeText(action.target, action.content);
3385
+ }
3386
+ if (usingRealConfig) {
3387
+ for (const change of action.mcpOwnership ?? []) {
3388
+ saveDevtoolsMcpOwnership(devtoolsMcpPreferenceFile(), id, change.server, change.owned);
3389
+ }
3390
+ for (const change of action.primaryModelOwnership ?? []) {
3391
+ savePrimaryModelOwnership(primaryModelOwnershipFile(), id, configDir, change.field, change.owned);
3392
+ }
3393
+ }
3394
+ }
3395
+ if (usingRealConfig) {
3396
+ for (const field of ctx.ownedPrimaryModelFields ?? []) {
3397
+ savePrimaryModelOwnership(primaryModelOwnershipFile(), id, configDir, field, false);
3398
+ }
3399
+ }
3400
+ if (usingRealConfig) removeRuntimeManifest(id);
3401
+ p2.log.success(`${adapter.name}: stack retirado (lo tuyo queda intacto).`);
3588
3402
  }
3589
- const selected = await p4.multiselect({
3590
- message: "Selecciona qu\xE9 actualizar (espacio para marcar, intro para confirmar):",
3591
- options: updateItems,
3592
- initialValues: updateItems.map((i) => i.value),
3593
- required: false
3403
+ const playwrightPlan = resolvePlaywrightUninstallPlan({
3404
+ removePackage: opts.targetDir === void 0 && opts.removePlaywright
3594
3405
  });
3595
- if (p4.isCancel(selected)) {
3596
- p4.outro("Update cancelado.");
3597
- return { exitCode: 0, appliedUpdates: false, syncRequired: false };
3598
- }
3599
- if (selected.length === 0) {
3600
- p4.outro("Nada seleccionado.");
3601
- return { exitCode: 0, appliedUpdates: false, syncRequired: false };
3602
- }
3603
- const sel = selected;
3604
- if (stackNeedsUpdate && sel.includes("stack")) {
3605
- const isClone = isGitClone();
3606
- const method = isClone ? STACK_METHOD_CLONE : "pnpm add -g jorgex-stack@latest";
3607
- const confirm5 = await p4.confirm({
3608
- message: `Actualizar el stack con: ${method}`,
3609
- initialValue: true
3610
- });
3611
- if (p4.isCancel(confirm5) || !confirm5) {
3612
- p4.log.info("Stack: actualizaci\xF3n omitida.");
3406
+ if (opts.removePlaywright && opts.targetDir !== void 0) {
3407
+ p2.log.info("Playwright CLI: --target-dir conserva el paquete global y los datos del navegador.");
3408
+ } else if (playwrightPlan.actions.length > 0) {
3409
+ if (opts.dryRun) {
3410
+ p2.log.info("Playwright CLI: se retirar\xEDa solo el paquete global; los datos y navegadores se conservan.");
3613
3411
  } else {
3614
- try {
3615
- if (isClone) {
3616
- updateStackGitClone();
3617
- } else {
3618
- updateStackGlobal();
3412
+ const removal = executePlaywrightToolAction2("remove");
3413
+ if (removal.ok) {
3414
+ try {
3415
+ savePlaywrightCliPreference(playwrightCliPreferenceFile(), false);
3416
+ p2.log.success("Playwright CLI: paquete global retirado; los datos y navegadores se conservan.");
3417
+ } catch (error) {
3418
+ p2.log.error(`Playwright CLI: paquete global retirado, pero no se pudo guardar la preferencia (${error instanceof Error ? error.message : String(error)}). Corrige la preferencia antes de reintentar.`);
3419
+ exitCode = 1;
3619
3420
  }
3620
- p4.log.success("Stack actualizado correctamente.");
3621
- appliedUpdates = true;
3622
- updated.push("stack");
3623
- } catch (err) {
3624
- p4.log.error(`Stack: error al actualizar \u2014 ${err instanceof Error ? err.message : err}`);
3421
+ } else {
3422
+ const pnpmRemedy = resolvePnpmFailureRemedy(removal.reason);
3423
+ const recovery = pnpmRemedy === null ? " Revisa el error de pnpm anterior y ejecuta 'jorgex-stack uninstall --remove-playwright' para reintentar." : ` ${pnpmRemedy} Despu\xE9s, ejecuta 'jorgex-stack uninstall --remove-playwright' para reintentar.`;
3424
+ p2.log.error(`Playwright CLI: no se pudo retirar el paquete global; los datos y la preferencia se conservan.${recovery}`);
3625
3425
  exitCode = 1;
3626
3426
  }
3627
3427
  }
3428
+ } else {
3429
+ p2.log.info("Playwright CLI: paquete global y datos del navegador conservados (usa --remove-playwright para retirar solo el paquete).");
3628
3430
  }
3629
- for (const skillInfo of skillUpdates) {
3630
- if (!sel.includes(`skill:${skillInfo.name}`)) continue;
3631
- if (skillInfo.modified) {
3632
- p4.log.warn(
3633
- `skill ${skillInfo.name}: TIENE CAMBIOS LOCALES. Actualizar sobreescribir\xE1 esas modificaciones (backup autom\xE1tico).`
3634
- );
3635
- const forceConfirm = await p4.confirm({
3636
- message: `\xBFConfirmas sobreescribir los cambios locales de ${skillInfo.name}?`,
3637
- initialValue: false
3638
- });
3639
- if (p4.isCancel(forceConfirm) || !forceConfirm) {
3640
- p4.log.info(`skill ${skillInfo.name}: omitida.`);
3641
- continue;
3642
- }
3643
- }
3644
- const spin2 = p4.spinner();
3645
- spin2.start(`Descargando upstream de ${skillInfo.name}\u2026`);
3646
- const tmpResult = await downloadSkillToTemp(skillInfo.repo, skillInfo.head, skillInfo.skillPath);
3647
- spin2.stop("error" in tmpResult ? "Error de descarga." : "Descargado.");
3648
- if ("error" in tmpResult) {
3649
- p4.log.error(`skill ${skillInfo.name}: descarga fallida \u2014 ${tmpResult.error}. Omitiendo.`);
3650
- exitCode = 1;
3651
- continue;
3652
- }
3653
- const { dir: tmpDir, root: tmpRoot } = tmpResult;
3654
- const localSkillDir = path26.join(stackRoot(), "skills", skillInfo.name);
3655
- const diff = renderSkillDiff(tmpDir, localSkillDir);
3656
- if (diff) {
3657
- p4.log.info(`Diff de ${skillInfo.name}:
3658
- ${diff}`);
3431
+ p2.outro(opts.dryRun ? "Dry-run: no se ha tocado nada." : exitCode === 0 ? "Hecho. Usa 'restore' si quieres volver atr\xE1s." : "Uninstall completado con errores (revisa arriba).");
3432
+ return exitCode;
3433
+ }
3434
+
3435
+ // src/doctor.ts
3436
+ import path26 from "path";
3437
+ import fs18 from "fs";
3438
+ import * as p3 from "@clack/prompts";
3439
+ function engramVersion(bin) {
3440
+ const out = runDetectedBin(bin, ["--version"], 5e3);
3441
+ if (out === null) return null;
3442
+ return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
3443
+ }
3444
+ function resolvePlaywrightDoctorState(input) {
3445
+ if (input.enabled !== true) return { status: "disabled" };
3446
+ if (input.cli.status === "absent") return { status: "missing", missing: "package" };
3447
+ if (input.cli.status === "broken") return { status: "broken" };
3448
+ if (input.cli.status === "outdated") return { status: "outdated" };
3449
+ if (input.browserCache?.status === "unreadable") {
3450
+ return { status: "unreadable", path: input.browserCache.path, errorCode: input.browserCache.errorCode };
3451
+ }
3452
+ if (!input.browserReady) return { status: "missing", missing: "browser" };
3453
+ return { status: "healthy" };
3454
+ }
3455
+ function context7KeyConfigured(id, configDir) {
3456
+ const file = id === "codex" ? path26.join(configDir, "config.toml") : id === "claude-code" ? path26.join(path26.dirname(configDir), `${path26.basename(configDir)}.json`) : path26.join(configDir, "opencode.json");
3457
+ const content = readTextIfExists(file);
3458
+ if (content === null) return null;
3459
+ const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
3460
+ if (!match) return null;
3461
+ return match[1] !== "";
3462
+ }
3463
+ async function runDoctor() {
3464
+ p3.intro("jorgex-stack doctor");
3465
+ let problems = 0;
3466
+ const engramBin = detectEngram();
3467
+ if (engramBin === null) {
3468
+ p3.log.warn("Engram: NO detectado. El protocolo de memoria no funcionar\xE1 \u2014 inst\xE1lalo: github.com/Gentleman-Programming/engram");
3469
+ problems++;
3470
+ } else {
3471
+ const version = engramVersion(engramBin);
3472
+ if (version === null) {
3473
+ p3.log.error(`Engram: binario en ${engramBin} pero no responde a --version.`);
3474
+ problems++;
3659
3475
  } else {
3660
- p4.log.info(`${skillInfo.name}: sin cambios en contenido (el repo se movi\xF3 pero la skill no cambi\xF3).`);
3661
- cleanupTmp(tmpRoot);
3662
- continue;
3476
+ p3.log.success(`Engram: ${version} (${engramBin})`);
3663
3477
  }
3664
- const applySkill = await p4.confirm({
3665
- message: `\xBFAplicar la actualizaci\xF3n de ${skillInfo.name}?`,
3666
- // Default No — política deliberate-pin (el diff es de contenido)
3667
- initialValue: false
3478
+ }
3479
+ const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path26.join(HOME, ".engram");
3480
+ const engramDb = path26.join(engramDataDir, "engram.db");
3481
+ if (fs18.existsSync(engramDb)) {
3482
+ const sizeMb = (fs18.statSync(engramDb).size / 1024 / 1024).toFixed(1);
3483
+ p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
3484
+ }
3485
+ if (!fs18.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
3486
+ const preferenceErrors = browserPreferenceErrors();
3487
+ const primaryOwnershipError = primaryModelOwnershipError();
3488
+ if (primaryOwnershipError !== null) {
3489
+ p3.log.error(primaryOwnershipError);
3490
+ problems++;
3491
+ }
3492
+ if (preferenceErrors.length > 0) {
3493
+ for (const error of preferenceErrors) p3.log.error(error);
3494
+ problems += preferenceErrors.length;
3495
+ } else {
3496
+ const browserCache = isPlaywrightBrowserReady();
3497
+ const playwright = resolvePlaywrightDoctorState({
3498
+ enabled: loadPlaywrightCliPreference(),
3499
+ cli: detectPlaywrightCli(),
3500
+ browserReady: browserCache.status === "ready",
3501
+ browserCache
3668
3502
  });
3669
- if (p4.isCancel(applySkill) || !applySkill) {
3670
- p4.log.info(`skill ${skillInfo.name}: omitida.`);
3671
- cleanupTmp(tmpRoot);
3503
+ if (playwright.status === "disabled") {
3504
+ p3.log.info("Playwright CLI: deshabilitado (opcional). Usa 'install --playwright' para instalarlo de forma expl\xEDcita.");
3505
+ } else if (playwright.status === "healthy") {
3506
+ p3.log.success("Playwright CLI: paquete y navegador listos.");
3507
+ } else if (playwright.status === "missing") {
3508
+ const target = playwright.missing === "package" ? "el paquete global" : "el navegador de Playwright";
3509
+ p3.log.warn(`Playwright CLI: habilitado, pero falta ${target} \u2192 ejecuta 'jorgex-stack install --playwright'.`);
3510
+ problems++;
3511
+ } else if (playwright.status === "broken") {
3512
+ p3.log.error("Playwright CLI: el binario detectado no responde correctamente \u2192 ejecuta 'jorgex-stack install --playwright'.");
3513
+ problems++;
3514
+ } else if (playwright.status === "unreadable") {
3515
+ p3.log.error(`Playwright CLI: no se puede leer la cach\xE9 de navegadores en ${playwright.path} (${playwright.errorCode}) \u2192 revisa permisos o ejecuta 'jorgex-stack install --playwright'.`);
3516
+ problems++;
3517
+ } else {
3518
+ p3.log.warn("Playwright CLI: versi\xF3n distinta del pin aprobado \u2192 ejecuta 'jorgex-stack update' o 'install --playwright'.");
3519
+ problems++;
3520
+ }
3521
+ }
3522
+ const manifest = readManifest();
3523
+ const modePreference = loadInstallModePreference();
3524
+ const current = collectAllCurrentTargets(modePreference);
3525
+ if (!current.complete || current.warnings.length > 0) {
3526
+ p3.log.warn("Limpieza de hu\xE9rfanos deshabilitada: no se pudo construir el plan completo de todos los runtimes.");
3527
+ for (const warning of current.warnings) p3.log.warn(warning);
3528
+ problems++;
3529
+ }
3530
+ for (const adapter of Object.values(ADAPTERS)) {
3531
+ const detection = adapter.detect();
3532
+ if (!detection.installed) {
3533
+ p3.log.warn(`${adapter.name}: no instalado en esta m\xE1quina.`);
3672
3534
  continue;
3673
3535
  }
3536
+ const ctx = makeContext(adapter, detection.configDir, modePreference);
3537
+ if (!ctx) continue;
3538
+ let pending;
3674
3539
  try {
3675
- replaceSkill(skillInfo.name, tmpDir, skillInfo.head, {});
3676
- p4.log.success(`skill ${skillInfo.name}: actualizada y re-pineada a ${skillInfo.head.slice(0, 7)}.`);
3677
- appliedUpdates = true;
3678
- updated.push("skill");
3540
+ pending = diffPlan(buildPlan(adapter, ctx)).filter((d) => d.status !== "unchanged").length;
3679
3541
  } catch (err) {
3680
- p4.log.error(`skill ${skillInfo.name}: error \u2014 ${err instanceof Error ? err.message : err}`);
3681
- exitCode = 1;
3682
- } finally {
3683
- cleanupTmp(tmpRoot);
3542
+ p3.log.error(
3543
+ `${adapter.name}: config ilegible en ${detection.configDir} \u2014 ${err instanceof Error ? err.message : err}`
3544
+ );
3545
+ problems++;
3546
+ continue;
3684
3547
  }
3685
- }
3686
- if (playwrightNeedsUpdate && sel.includes("playwright-cli")) {
3687
- const confirmPlaywright = await p4.confirm({
3688
- message: "Actualizar Playwright CLI global al pin aprobado con pnpm add --global?",
3689
- initialValue: true
3690
- });
3691
- if (p4.isCancel(confirmPlaywright) || !confirmPlaywright) {
3692
- p4.log.info("Playwright CLI: actualizaci\xF3n omitida.");
3548
+ if (pending > 0) {
3549
+ p3.log.warn(`${adapter.name}: ${pending} archivos gestionados desactualizados o ausentes \u2192 ejecuta 'sync'.`);
3550
+ problems++;
3693
3551
  } else {
3694
- const packageResult = executePlaywrightToolAction2("update");
3695
- const browserResult = packageResult.ok ? executePlaywrightToolAction2("install-browser") : null;
3696
- if (packageResult.ok && browserResult?.ok) {
3697
- p4.log.success("Playwright CLI actualizado al pin aprobado.");
3698
- appliedUpdates = true;
3699
- updated.push("playwright-cli");
3700
- } else {
3701
- const failedResult = packageResult.ok ? browserResult : packageResult;
3702
- const pnpmRemedy = failedResult && !failedResult.ok ? resolvePnpmFailureRemedy(failedResult.reason) : null;
3703
- const recovery = pnpmRemedy === null ? "Ejecuta 'jorgex-stack install --playwright' para reintentar el paquete y el navegador." : `${pnpmRemedy} Despu\xE9s, ejecuta 'jorgex-stack update' para reintentar.`;
3704
- const failedStep = packageResult.ok ? "descargar el navegador" : "actualizar el paquete global";
3705
- p4.log.error(`Playwright CLI: no se pudo ${failedStep}. ${recovery}`);
3706
- exitCode = 1;
3707
- }
3552
+ p3.log.success(`${adapter.name}: config del stack al d\xEDa (${detection.configDir}).`);
3708
3553
  }
3709
- }
3710
- if (engramNeedsUpdate && sel.includes("engram") && engramData?.version) {
3711
- const confirmEngram = await p4.confirm({
3712
- message: `Actualizar engram a v${engramData.version} (canal nativo: brew \u2192 go install \u2192 URL)`,
3713
- initialValue: true
3714
- });
3715
- if (p4.isCancel(confirmEngram) || !confirmEngram) {
3716
- p4.log.info("Engram: actualizaci\xF3n omitida.");
3717
- } else {
3718
- const ok = await updateEngram(engramData.repo, engramData.version);
3719
- if (!ok) {
3720
- exitCode = 1;
3721
- } else {
3722
- const binPost = detectEngram();
3723
- const versionPost = binPost ? engramVersion(binPost) : null;
3724
- if (versionPost) {
3725
- p4.log.success(`Engram: ahora en v${versionPost}.`);
3726
- } else {
3727
- p4.log.warn("Engram actualizado, pero no se pudo verificar la versi\xF3n \u2014 comprueba con engram --version");
3728
- }
3729
- p4.log.info("Reinicia los clientes (Claude Code/OpenCode/Codex) para que sus MCP usen la versi\xF3n nueva.");
3730
- appliedUpdates = true;
3731
- updated.push("engram");
3732
- }
3554
+ const prev = manifest.runtimes[adapter.id];
3555
+ const orphans = prev && current.complete ? findOrphans(prev.owned, current.targets) : [];
3556
+ if (orphans.length > 0) {
3557
+ p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
3558
+ problems++;
3559
+ }
3560
+ if (adapter.id === "codex" && fs18.existsSync(path26.join(detection.configDir, "hooks.json"))) {
3561
+ p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
3733
3562
  }
3563
+ if (adapter.id === "codex" && fs18.existsSync(path26.join(detection.configDir, "AGENTS.override.md"))) {
3564
+ p3.log.warn(
3565
+ "Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
3566
+ );
3567
+ problems++;
3568
+ }
3569
+ const key = context7KeyConfigured(adapter.id, detection.configDir);
3570
+ if (key === false) p3.log.info(`${adapter.name}: context7 sin key (opcional \u2014 con\xE9ctala cuando quieras).`);
3734
3571
  }
3735
- p4.outro(exitCode === 0 ? "Update completado." : "Update completado con errores (revisa arriba).");
3736
- return { exitCode, appliedUpdates, syncRequired: resolveUpdateSyncRequired(updated) };
3572
+ p3.outro(problems === 0 ? "Todo sano." : `${problems} avisos \u2014 revisa arriba.`);
3573
+ return problems > 0 ? 1 : 0;
3737
3574
  }
3738
3575
 
3739
- // src/models-picker.ts
3576
+ // src/update.ts
3577
+ import fs21 from "fs";
3578
+ import path29 from "path";
3579
+ import os4 from "os";
3580
+ import { execFileSync as execFileSync5 } from "child_process";
3581
+ import * as p4 from "@clack/prompts";
3582
+
3583
+ // src/lib/github.ts
3584
+ import fs19 from "fs";
3740
3585
  import path27 from "path";
3741
- import * as p5 from "@clack/prompts";
3742
- var TIERS = ["strong", "standard", "cheap"];
3743
- var EFFORTS = ["low", "medium", "high", "xhigh"];
3744
- var CLAUDE_ALIASES = ["fable", "opus", "sonnet", "haiku", "inherit"];
3745
- var CODEX_MODELS = [
3746
- "default",
3747
- "gpt-5.6-sol",
3748
- "gpt-5.6-terra",
3749
- "gpt-5.6-luna",
3750
- "gpt-5.5",
3751
- "gpt-5.4",
3752
- "gpt-5.4-mini"
3753
- ];
3754
- var CODEX_CUSTOM = "__custom__";
3755
- var CANCEL = /* @__PURE__ */ Symbol("cancel");
3756
- function opencodeLiveModels(binPath) {
3757
- const out = runDetectedBin(binPath, ["models"], 2e4);
3758
- if (out === null) return null;
3759
- const models = out.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== "" && l.includes("/"));
3760
- return models.length > 0 ? models : null;
3761
- }
3762
- function agentsByTier() {
3763
- const grouped = { strong: [], standard: [], cheap: [] };
3764
- for (const agent of loadCanonicalAgents(path27.join(stackRoot(), "agents"))) {
3765
- if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
3586
+ import { execFileSync as execFileSync3 } from "child_process";
3587
+ import os3 from "os";
3588
+ import { Readable } from "stream";
3589
+ import { pipeline } from "stream/promises";
3590
+ var cachedToken;
3591
+ var ghTokenFailed = false;
3592
+ function githubToken() {
3593
+ if (cachedToken !== void 0) return cachedToken;
3594
+ const env = process.env["GH_TOKEN"]?.trim() || process.env["GITHUB_TOKEN"]?.trim();
3595
+ if (env) return cachedToken = env;
3596
+ const gh = lookPath("gh");
3597
+ if (gh) {
3598
+ const fromGh = runDetectedBin(gh, ["auth", "token"], 5e3)?.trim();
3599
+ if (fromGh) return cachedToken = fromGh;
3600
+ ghTokenFailed = true;
3766
3601
  }
3767
- return grouped;
3602
+ return cachedToken = null;
3768
3603
  }
3769
- function isCompleteRuntimeModelMap(models) {
3770
- return TIERS.every((tier) => models[tier]?.model);
3604
+ function ghPresentButTokenFailed() {
3605
+ return ghTokenFailed;
3771
3606
  }
3772
- async function askModel(det, subject, current) {
3773
- if (det.id === "codex") {
3774
- if (!current) throw new Error("Codex requiere un model-map base.");
3775
- const effort = await p5.select({
3776
- message: `${det.name} \xB7 ${subject} \u2014 reasoning effort`,
3777
- options: EFFORTS.map((v) => ({ value: v, label: v })),
3778
- initialValue: current.variant ?? "medium"
3779
- });
3780
- if (p5.isCancel(effort)) return CANCEL;
3781
- const modelOptions = [
3782
- ...CODEX_MODELS.includes(current.model) ? [] : [{ value: current.model, label: `${current.model} (actual)` }],
3783
- ...CODEX_MODELS.map((m) => ({
3784
- value: m,
3785
- label: m === "default" ? "default \u2014 el del CLI, se actualiza solo (recomendado)" : m
3786
- })),
3787
- { value: CODEX_CUSTOM, label: "otro\u2026 (escribir un ID a mano)" }
3788
- ];
3789
- let model = await p5.select({
3790
- message: `${det.name} \xB7 ${subject} \u2014 modelo`,
3791
- options: modelOptions,
3792
- initialValue: current.model
3607
+ function authHeaders() {
3608
+ const token = githubToken();
3609
+ return token ? { Authorization: `Bearer ${token}` } : {};
3610
+ }
3611
+ var rateLimitHit = false;
3612
+ function githubRateLimited() {
3613
+ return rateLimitHit;
3614
+ }
3615
+ function githubHeaders() {
3616
+ return {
3617
+ Accept: "application/vnd.github+json",
3618
+ "User-Agent": "jorgex-stack",
3619
+ ...authHeaders()
3620
+ };
3621
+ }
3622
+ async function latestGithubRelease(repo) {
3623
+ try {
3624
+ const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
3625
+ headers: githubHeaders(),
3626
+ signal: AbortSignal.timeout(1e4)
3793
3627
  });
3794
- if (p5.isCancel(model)) return CANCEL;
3795
- if (model === CODEX_CUSTOM) {
3796
- const typed = await p5.text({
3797
- message: `${det.name} \xB7 ${subject} \u2014 ID exacto del modelo (min\xFAsculas; compru\xE9balo con /model dentro de codex)`,
3798
- initialValue: current.model,
3799
- validate: (v) => !v || v.trim() === "" ? 'Vac\xEDo no \u2014 usa "default" o un ID.' : void 0
3800
- });
3801
- if (p5.isCancel(typed)) return CANCEL;
3802
- model = typed.trim().toLowerCase();
3628
+ if (!res.ok) {
3629
+ if (res.status === 403 || res.status === 429) rateLimitHit = true;
3630
+ return null;
3803
3631
  }
3804
- return { model, variant: effort };
3805
- }
3806
- const optionsList = det.options;
3807
- const options = optionsList.map((m) => ({ value: m, label: m }));
3808
- if (current && !optionsList.includes(current.model)) {
3809
- options.unshift({ value: current.model, label: `${current.model} (actual)` });
3632
+ const data = await res.json();
3633
+ return data.tag_name?.replace(/^v/, "") ?? null;
3634
+ } catch {
3635
+ return null;
3810
3636
  }
3811
- const choice = await p5.select({
3812
- message: `${det.name} \xB7 ${subject} \u2014 modelo`,
3813
- options,
3814
- initialValue: current?.model,
3815
- maxItems: 12
3816
- });
3817
- if (p5.isCancel(choice)) return CANCEL;
3818
- if (det.id === "claude-code") return { model: choice };
3819
- const keptCurrent = current && choice === current.model && current.variant ? current.variant : null;
3820
- const variant = await p5.select({
3821
- message: `${det.name} \xB7 ${subject} \u2014 reasoning effort (variant; solo si el modelo lo soporta)`,
3822
- options: [
3823
- { value: "", label: "(sin variant \u2014 el default del modelo)" },
3824
- ...EFFORTS.map((v) => ({ value: v, label: v })),
3825
- ...keptCurrent && !EFFORTS.includes(keptCurrent) ? [{ value: keptCurrent, label: `${keptCurrent} (actual)` }] : []
3826
- ],
3827
- initialValue: keptCurrent ?? ""
3828
- });
3829
- if (p5.isCancel(variant)) return CANCEL;
3830
- return { model: choice, ...variant ? { variant } : {} };
3831
3637
  }
3832
- async function runModelsPicker(opts) {
3833
- const file = ensureModelMapFile();
3834
- const map = loadModelMap();
3835
- if (opts.yes || !process.stdout.isTTY) {
3836
- if (opts.runtimes.includes("opencode") && !map.opencode) {
3837
- console.error("OpenCode requiere selecci\xF3n interactiva desde los proveedores conectados; ejecuta 'models --agents opencode' sin --yes.");
3838
- return 1;
3839
- }
3840
- console.log(`Model-map en ${file}. Ed\xEDtalo o ejecuta 'models' sin --yes para el picker.`);
3841
- return 0;
3842
- }
3843
- p5.intro("jorgex-stack models \u2014 modelos por tier o por subagente");
3844
- const grouped = agentsByTier();
3845
- const tierLine = (tier) => grouped[tier].join(", ");
3846
- const agentCount = TIERS.reduce((n, tier) => n + grouped[tier].length, 0);
3847
- const detections = [];
3848
- if (opts.runtimes.includes("opencode")) {
3849
- const opencode = detectOpenCode();
3850
- detections.push({
3851
- id: "opencode",
3852
- name: "OpenCode",
3853
- installed: opencode.installed,
3854
- options: opencode.binPath ? opencodeLiveModels(opencode.binPath) : null
3638
+ async function latestGithubCommit(repo) {
3639
+ try {
3640
+ const res = await fetch(`https://api.github.com/repos/${repo}/commits?per_page=1`, {
3641
+ headers: githubHeaders(),
3642
+ signal: AbortSignal.timeout(1e4)
3855
3643
  });
3856
- }
3857
- if (opts.runtimes.includes("claude-code")) {
3858
- detections.push({ id: "claude-code", name: "Claude Code", installed: detectClaudeCode().installed, options: CLAUDE_ALIASES });
3859
- }
3860
- if (opts.runtimes.includes("codex")) {
3861
- detections.push({ id: "codex", name: "Codex CLI", installed: detectCodex().installed, options: null });
3862
- }
3863
- for (const det of detections) {
3864
- if (!det.installed) {
3865
- p5.log.info(`${det.name}: no instalado \u2014 se mantienen los defaults.`);
3866
- continue;
3644
+ if (!res.ok) {
3645
+ if (res.status === 403 || res.status === 429) rateLimitHit = true;
3646
+ return null;
3867
3647
  }
3868
- if (det.id === "opencode" && det.options === null) {
3869
- p5.log.warn("OpenCode: no se pudo listar `opencode models` \u2014 se mantiene la selecci\xF3n actual.");
3870
- continue;
3648
+ const data = await res.json();
3649
+ return data[0]?.sha ?? null;
3650
+ } catch {
3651
+ return null;
3652
+ }
3653
+ }
3654
+ function validateExtractedTree(destDir) {
3655
+ const resolved = path27.resolve(destDir);
3656
+ const walk = (dir) => {
3657
+ let entries;
3658
+ try {
3659
+ entries = fs19.readdirSync(dir, { withFileTypes: true });
3660
+ } catch {
3661
+ return false;
3871
3662
  }
3872
- const existingRuntimeMap = map[det.id];
3873
- const runtimeMap = {
3874
- ...existingRuntimeMap
3875
- };
3876
- p5.log.message(
3877
- `${det.name} \u2014 tiers y sus subagentes:
3878
- strong \u2192 ${tierLine("strong")}
3879
- standard \u2192 ${tierLine("standard")}
3880
- cheap \u2192 ${tierLine("cheap")}`
3881
- );
3882
- const mode = await p5.select({
3883
- message: `${det.name} \u2014 \xBFc\xF3mo asignar los modelos?`,
3884
- options: [
3885
- { value: "tier", label: "Por tier \u2014 3 grupos (r\xE1pido)" },
3886
- { value: "agent", label: `Por subagente \u2014 uno a uno (${agentCount} subagentes, control total)` }
3887
- ],
3888
- initialValue: "tier"
3889
- });
3890
- if (p5.isCancel(mode)) return cancelled();
3891
- if (mode === "tier") {
3892
- for (const tier of TIERS) {
3893
- const asked = await askModel(det, `tier ${tier} (${tierLine(tier)})`, existingRuntimeMap?.[tier]);
3894
- if (asked === CANCEL) return cancelled();
3895
- runtimeMap[tier] = asked;
3896
- }
3897
- const overrideCount = Object.keys(runtimeMap.overrides ?? {}).length;
3898
- if (overrideCount > 0) {
3899
- p5.log.info(
3900
- `${det.name}: se conservan ${overrideCount} ajustes por subagente previos \u2014 elige "Por subagente" para revisarlos.`
3901
- );
3663
+ for (const entry of entries) {
3664
+ const full = path27.join(dir, entry.name);
3665
+ let stat;
3666
+ try {
3667
+ stat = fs19.lstatSync(full);
3668
+ } catch {
3669
+ return false;
3902
3670
  }
3903
- } else {
3904
- const overrides = { ...runtimeMap.overrides ?? {} };
3905
- for (const tier of TIERS) {
3906
- let base = runtimeMap[tier];
3907
- for (const name of grouped[tier]) {
3908
- const current = existingRuntimeMap ? resolveAgentModel(existingRuntimeMap, name, tier) : void 0;
3909
- const asked = await askModel(det, `${name} (tier ${tier})`, current);
3910
- if (asked === CANCEL) return cancelled();
3911
- if (!base) {
3912
- base = asked;
3913
- runtimeMap[tier] = base;
3914
- }
3915
- const sameAsTier = asked.model === base.model && (asked.variant ?? "") === (base.variant ?? "");
3916
- if (sameAsTier) {
3917
- delete overrides[name];
3918
- } else {
3919
- overrides[name] = {
3920
- model: asked.model,
3921
- ...asked.variant ? { variant: asked.variant } : base.variant ? { variant: "" } : {}
3922
- };
3923
- }
3924
- }
3671
+ if (stat.isSymbolicLink()) return false;
3672
+ if (!isContainedIn(full, resolved)) return false;
3673
+ if (entry.isDirectory()) {
3674
+ if (!walk(full)) return false;
3925
3675
  }
3926
- if (Object.keys(overrides).length > 0) runtimeMap.overrides = overrides;
3927
- else delete runtimeMap.overrides;
3928
3676
  }
3929
- if (!isCompleteRuntimeModelMap(runtimeMap)) {
3930
- throw new Error(`${det.name}: selecci\xF3n de modelos incompleta.`);
3677
+ return true;
3678
+ };
3679
+ return walk(resolved);
3680
+ }
3681
+ function resolveTarBin() {
3682
+ if (process.platform !== "win32") return "tar";
3683
+ const winTar = path27.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
3684
+ return fs19.existsSync(winTar) ? winTar : "tar";
3685
+ }
3686
+ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
3687
+ const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
3688
+ const tmp = path27.join(os3.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
3689
+ const fail = (reason) => {
3690
+ try {
3691
+ fs19.rmSync(destDir, { recursive: true, force: true });
3692
+ } catch {
3693
+ }
3694
+ return { ok: false, reason };
3695
+ };
3696
+ try {
3697
+ const res = await fetch(url, {
3698
+ headers: { "User-Agent": "jorgex-stack", ...authHeaders() },
3699
+ signal: AbortSignal.timeout(12e4)
3700
+ });
3701
+ if (!res.ok) {
3702
+ if (res.status === 403 || res.status === 429) rateLimitHit = true;
3703
+ return fail(
3704
+ `HTTP ${res.status}${res.status === 403 || res.status === 429 ? " \u2014 rate limit; define GH_TOKEN o inicia sesi\xF3n en gh" : ""}`
3705
+ );
3706
+ }
3707
+ if (!res.body) return fail("respuesta HTTP sin cuerpo");
3708
+ await pipeline(
3709
+ Readable.fromWeb(res.body),
3710
+ fs19.createWriteStream(tmp)
3711
+ );
3712
+ fs19.rmSync(destDir, { recursive: true, force: true });
3713
+ fs19.mkdirSync(destDir, { recursive: true });
3714
+ try {
3715
+ execFileSync3(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
3716
+ } catch (err) {
3717
+ const e = err;
3718
+ const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
3719
+ return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
3720
+ }
3721
+ const resolvedDest = path27.resolve(destDir);
3722
+ const validateRoot = validateSubdir ? path27.resolve(resolvedDest, validateSubdir) : resolvedDest;
3723
+ if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
3724
+ return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
3725
+ }
3726
+ if (fs19.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
3727
+ return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
3728
+ }
3729
+ const validated = fs19.existsSync(validateRoot);
3730
+ return { ok: true, validated };
3731
+ } catch (err) {
3732
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
3733
+ return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
3734
+ } finally {
3735
+ try {
3736
+ fs19.rmSync(tmp, { force: true });
3737
+ } catch {
3931
3738
  }
3932
- map[det.id] = runtimeMap;
3933
3739
  }
3934
- writeText(file, JSON.stringify(map, null, 2) + "\n");
3935
- p5.log.success(`Guardado en ${file}`);
3936
- return 0;
3937
- }
3938
- function cancelled() {
3939
- p5.cancel("Cancelado \u2014 no se ha guardado nada.");
3940
- return 1;
3941
3740
  }
3942
3741
 
3943
- // src/lib/release.ts
3944
- import fs21 from "fs";
3742
+ // src/lib/skill-update.ts
3743
+ import fs20 from "fs";
3945
3744
  import path28 from "path";
3946
- import { execFileSync as execFileSync6 } from "child_process";
3947
- import { fileURLToPath as fileURLToPath2 } from "url";
3948
- function findPackageJson() {
3949
- let dir = path28.dirname(fileURLToPath2(import.meta.url));
3950
- for (let i = 0; i < 6; i++) {
3951
- const candidate = path28.join(dir, "package.json");
3952
- if (fs21.existsSync(candidate)) return candidate;
3953
- dir = path28.dirname(dir);
3954
- }
3955
- throw new Error("No se encontr\xF3 package.json cerca del CLI.");
3956
- }
3957
- function readPackageVersion() {
3958
- return readPackageMetadata().version;
3745
+ import { execFileSync as execFileSync4 } from "child_process";
3746
+ var PROTECTED_SKILLS = /* @__PURE__ */ new Set([
3747
+ "agent-delegation",
3748
+ "lean-code",
3749
+ "orchestrator",
3750
+ "work-lifecycle",
3751
+ "xreview"
3752
+ ]);
3753
+ function sameTextContentNormalized(a, b) {
3754
+ const ba = fs20.readFileSync(a);
3755
+ const bb = fs20.readFileSync(b);
3756
+ if (ba.equals(bb)) return true;
3757
+ const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
3758
+ const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
3759
+ return sa === sb;
3959
3760
  }
3960
- function readPackageMetadata() {
3961
- const packageJson = findPackageJson();
3962
- const raw = fs21.readFileSync(packageJson, "utf8");
3963
- const parsed = JSON.parse(raw);
3964
- const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
3965
- const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
3966
- if (name === "" || version === "") {
3967
- throw new Error("package.json no expone nombre o versi\xF3n v\xE1lidos.");
3761
+ function diffSkillDirs(upstreamDir, localDir) {
3762
+ const upstreamFiles = new Set(
3763
+ listFilesRecursive(upstreamDir).map((f) => path28.relative(upstreamDir, f))
3764
+ );
3765
+ const localFiles = new Set(
3766
+ listFilesRecursive(localDir).map((f) => path28.relative(localDir, f))
3767
+ );
3768
+ const added = [];
3769
+ const modified = [];
3770
+ const deleted = [];
3771
+ for (const rel of upstreamFiles) {
3772
+ if (!localFiles.has(rel)) {
3773
+ added.push(rel);
3774
+ } else if (!sameTextContentNormalized(path28.join(upstreamDir, rel), path28.join(localDir, rel))) {
3775
+ modified.push(rel);
3776
+ }
3968
3777
  }
3969
- return { name, version };
3778
+ for (const rel of localFiles) {
3779
+ if (!upstreamFiles.has(rel)) {
3780
+ deleted.push(rel);
3781
+ }
3782
+ }
3783
+ return {
3784
+ added: added.sort(),
3785
+ modified: modified.sort(),
3786
+ deleted: deleted.sort()
3787
+ };
3970
3788
  }
3971
-
3972
- // src/lib/pi-runtime.ts
3973
- import os5 from "os";
3974
- import path30 from "path";
3975
- import fs22 from "fs";
3976
- import { spawnSync } from "child_process";
3977
- import { createHash } from "crypto";
3978
-
3979
- // src/lib/pi-package-lifecycle.ts
3980
- import path29 from "path";
3981
- var REQUIRED_CAPABILITIES = /* @__PURE__ */ new Set([
3982
- "foundation-contract-v1",
3983
- "runner-json-v1",
3984
- "managed-primary-model-v1"
3985
- ]);
3986
- var ALLOWED_EXTERNAL_WRITES = /* @__PURE__ */ new Set([
3987
- "settings.json",
3988
- "models.json",
3989
- "jorgex-pi/sol-lifecycle.v1.json"
3990
- ]);
3991
- function sameRecord(left, right) {
3992
- return JSON.stringify(left) === JSON.stringify(right);
3789
+ var DIFF_MAX_LINES = 400;
3790
+ function renderSkillDiff(upstreamDir, localDir) {
3791
+ try {
3792
+ execFileSync4("git", ["diff", "--no-index", "--stat", "--", localDir, upstreamDir], {
3793
+ stdio: "pipe",
3794
+ encoding: "utf8"
3795
+ });
3796
+ return "";
3797
+ } catch (statErr) {
3798
+ const se = statErr;
3799
+ if (typeof se.stdout !== "string" || se.status !== 1) {
3800
+ const d = diffSkillDirs(upstreamDir, localDir);
3801
+ const total = d.added.length + d.modified.length + d.deleted.length;
3802
+ if (total === 0) return "";
3803
+ return `[git no disponible \u2014 resumen de cambios]
3804
+ a\xF1adidos: ${d.added.length}, modificados: ${d.modified.length}, eliminados: ${d.deleted.length}`;
3805
+ }
3806
+ const stat = se.stdout;
3807
+ let fullDiff = "";
3808
+ try {
3809
+ execFileSync4("git", ["diff", "--no-index", "--", localDir, upstreamDir], {
3810
+ stdio: "pipe",
3811
+ encoding: "utf8"
3812
+ });
3813
+ } catch (diffErr) {
3814
+ const de = diffErr;
3815
+ if (typeof de.stdout === "string" && de.status === 1) {
3816
+ const lines = de.stdout.split("\n");
3817
+ if (lines.length > DIFF_MAX_LINES) {
3818
+ fullDiff = lines.slice(0, DIFF_MAX_LINES).join("\n") + `
3819
+ (truncado \u2014 ${lines.length - DIFF_MAX_LINES} l\xEDneas omitidas)`;
3820
+ } else {
3821
+ fullDiff = de.stdout;
3822
+ }
3823
+ }
3824
+ }
3825
+ return fullDiff ? `${stat}
3826
+ ${fullDiff}` : stat;
3827
+ }
3993
3828
  }
3994
- function managedExternalWritesAreSafe(writes) {
3995
- if (writes.length !== ALLOWED_EXTERNAL_WRITES.size) return false;
3996
- const seen = /* @__PURE__ */ new Set();
3997
- for (const write of writes) {
3998
- if (write === null || typeof write !== "object" || Array.isArray(write)) return false;
3999
- if (Object.keys(write).sort().join(",") !== "owner,relativePath,root,semantics") return false;
4000
- if (write.owner !== "jorgex-pi" || write.root !== "PI_CODING_AGENT_DIR") return false;
4001
- if (typeof write.relativePath !== "string" || !ALLOWED_EXTERNAL_WRITES.has(write.relativePath)) return false;
4002
- if (/^(?:[A-Za-z]:|[\\/])/.test(write.relativePath) || write.relativePath.split(/[\\/]/).includes("..")) return false;
4003
- if (typeof write.semantics !== "string" || write.semantics.trim() === "" || seen.has(write.relativePath)) return false;
4004
- seen.add(write.relativePath);
3829
+ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
3830
+ const upstreamsFilePath = opts?.upstreamsFilePath;
3831
+ const localSkillsRoot = opts?.localSkillsRoot;
3832
+ const backupsRoot2 = opts?.backupsRoot;
3833
+ if (PROTECTED_SKILLS.has(name)) {
3834
+ throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
4005
3835
  }
4006
- return seen.size === ALLOWED_EXTERNAL_WRITES.size;
3836
+ const upstreamsFile = upstreamsFilePath ?? path28.join(path28.dirname(stackRoot()), "upstreams.json");
3837
+ const raw = fs20.readFileSync(upstreamsFile, "utf8");
3838
+ const data = JSON.parse(raw);
3839
+ const skillEntry = data?.skills?.[name];
3840
+ if (!skillEntry) {
3841
+ throw new Error(`Skill "${name}" no encontrada en upstreams.json.`);
3842
+ }
3843
+ if (skillEntry.kind === "release") {
3844
+ throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
3845
+ }
3846
+ const skillsRoot = localSkillsRoot ?? path28.join(stackRoot(), "skills");
3847
+ const localSkillDir = path28.join(skillsRoot, name);
3848
+ const localFiles = listFilesRecursive(localSkillDir);
3849
+ if (localFiles.length > 0) {
3850
+ createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
3851
+ }
3852
+ const stagingDir = `${localSkillDir}.staging-${process.pid}`;
3853
+ try {
3854
+ const upstreamFiles = listFilesRecursive(upstreamSkillDir);
3855
+ for (const src of upstreamFiles) {
3856
+ const st = fs20.lstatSync(src);
3857
+ if (st.isSymbolicLink()) {
3858
+ throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
3859
+ }
3860
+ const rel = path28.relative(upstreamSkillDir, src);
3861
+ const dest = path28.join(stagingDir, rel);
3862
+ ensureDir(path28.dirname(dest));
3863
+ copyFile(src, dest);
3864
+ }
3865
+ const oldDir = `${localSkillDir}.old-${process.pid}`;
3866
+ if (fs20.existsSync(localSkillDir)) {
3867
+ fs20.renameSync(localSkillDir, oldDir);
3868
+ }
3869
+ fs20.renameSync(stagingDir, localSkillDir);
3870
+ fs20.rmSync(oldDir, { recursive: true, force: true });
3871
+ } catch (err) {
3872
+ try {
3873
+ fs20.rmSync(stagingDir, { recursive: true, force: true });
3874
+ } catch {
3875
+ }
3876
+ throw err;
3877
+ }
3878
+ skillEntry.commit = newCommit;
3879
+ writeText(upstreamsFile, JSON.stringify(data, null, 2) + "\n");
4007
3880
  }
4008
- function ownership(receipt) {
4009
- return { receipt, adapters: false, manifest: false, modelMap: false };
3881
+
3882
+ // src/update.ts
3883
+ function rateLimitHint(prefix) {
3884
+ return ghPresentButTokenFailed() ? `${prefix} Tienes gh instalado pero \`gh auth token\` no devolvi\xF3 credencial (\xBFsesi\xF3n caducada?) \u2014 prueba \`gh auth login\` o define GH_TOKEN.` : `${prefix} Define GH_TOKEN o inicia sesi\xF3n en gh CLI.`;
4010
3885
  }
4011
- function blocked(input, reason) {
4012
- return {
4013
- kind: "blocked",
4014
- reason,
4015
- receiptPath: input.scope.receiptPath,
4016
- ownership: ownership(input.receiptJson !== null)
4017
- };
3886
+ function loadUpstreams() {
3887
+ const file = path29.join(path29.dirname(stackRoot()), "upstreams.json");
3888
+ return JSON.parse(fs21.readFileSync(file, "utf8"));
4018
3889
  }
4019
- function packageSource(entry) {
4020
- if (typeof entry === "string") return entry;
4021
- if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
4022
- const source = Reflect.get(entry, "source");
4023
- return typeof source === "string" ? source : null;
3890
+ function skillsToScan(maintainer, upstreams) {
3891
+ return maintainer ? Object.keys(upstreams.skills) : [];
4024
3892
  }
4025
- function isJorgeXPiSource(source) {
4026
- return source.includes("jorgex-pi");
3893
+ async function querySkillHeads(skillNames, upstreams) {
3894
+ const requests = /* @__PURE__ */ new Map();
3895
+ for (const name of skillNames) {
3896
+ const info = upstreams.skills[name];
3897
+ const repo = info.source.replace(/^github:/, "");
3898
+ if (!requests.has(repo)) requests.set(repo, latestGithubCommit(repo));
3899
+ }
3900
+ const heads = /* @__PURE__ */ new Map();
3901
+ await Promise.all(
3902
+ [...requests.entries()].map(async ([repo, request]) => {
3903
+ heads.set(repo, await request);
3904
+ })
3905
+ );
3906
+ return skillNames.map((name) => {
3907
+ const info = upstreams.skills[name];
3908
+ const repo = info.source.replace(/^github:/, "");
3909
+ return { name, repo, info, head: heads.get(repo) ?? null };
3910
+ });
4027
3911
  }
4028
- function parsePackageSources(settingsJson) {
3912
+ async function latestNpmVersion(pkg) {
4029
3913
  try {
4030
- const parsed = JSON.parse(settingsJson);
4031
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
4032
- const packages = Reflect.get(parsed, "packages");
4033
- if (!Array.isArray(packages)) return null;
4034
- const sources = packages.map((entry) => ({ entry, source: packageSource(entry) }));
4035
- return sources.every((value) => value.source !== null) ? sources : null;
3914
+ const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
3915
+ signal: AbortSignal.timeout(1e4)
3916
+ });
3917
+ if (!res.ok) return null;
3918
+ const data = await res.json();
3919
+ return data.version ?? null;
4036
3920
  } catch {
4037
3921
  return null;
4038
3922
  }
4039
3923
  }
4040
- function isExactManagedPackage(entry, source) {
4041
- if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false;
4042
- const keys = Object.keys(entry);
4043
- const skills = Reflect.get(entry, "skills");
4044
- return keys.length === 2 && keys.includes("source") && keys.includes("skills") && Reflect.get(entry, "source") === source && Array.isArray(skills) && skills.length === 0;
4045
- }
4046
- function expectedReceipt(candidate, state, scope, engramBin) {
3924
+ function resolvePlaywrightUpdateCheck(input) {
3925
+ if (input.enabled !== true) return null;
3926
+ if (input.cli.status === "current") {
3927
+ return {
3928
+ level: "success",
3929
+ message: `Playwright CLI: ${PLAYWRIGHT_CLI.version} \u2014 al d\xEDa con el pin aprobado.`
3930
+ };
3931
+ }
3932
+ const local = input.cli.detectedVersion ?? (input.cli.status === "absent" ? "no instalado" : "no verificable");
4047
3933
  return {
4048
- schemaVersion: 1,
4049
- state,
4050
- candidate: {
4051
- package: candidate.package,
4052
- tarball: candidate.tarball,
4053
- provenance: candidate.provenance
4054
- },
4055
- scope,
4056
- engram: { binary: engramBin }
3934
+ level: "warn",
3935
+ message: `Playwright CLI: ${local} local, pin aprobado ${PLAYWRIGHT_CLI.version} \u2192 ejecuta 'jorgex-stack update' o 'jorgex-stack install --playwright'.`
4057
3936
  };
4058
3937
  }
4059
- function parseReceiptShape(receiptJson) {
4060
- try {
4061
- const parsed = JSON.parse(receiptJson);
4062
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
4063
- const schemaVersion = Reflect.get(parsed, "schemaVersion");
4064
- if (schemaVersion !== 1) return null;
4065
- const state = Reflect.get(parsed, "state");
4066
- const candidate = Reflect.get(parsed, "candidate");
4067
- if (state !== "installing" && state !== "installed" || candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
4068
- return null;
3938
+ async function runUpdateCheck(localVersion, includeBrowserState = true) {
3939
+ p4.intro("jorgex-stack update --check");
3940
+ if (includeBrowserState) {
3941
+ const preferenceErrors = browserPreferenceErrors();
3942
+ if (preferenceErrors.length > 0) {
3943
+ for (const error of preferenceErrors) p4.log.error(error);
3944
+ p4.outro("Check cancelado: corrige las preferencias de navegador antes de reintentar.");
3945
+ return 1;
4069
3946
  }
4070
- const packageValue = Reflect.get(candidate, "package");
4071
- const tarball = Reflect.get(candidate, "tarball");
4072
- const provenance = Reflect.get(candidate, "provenance");
4073
- const scope = Reflect.get(parsed, "scope");
4074
- const engram = Reflect.get(parsed, "engram");
4075
- if (packageValue === null || typeof packageValue !== "object" || tarball === null || typeof tarball !== "object" || provenance === null || typeof provenance !== "object" || scope === null || typeof scope !== "object" || Array.isArray(scope)) {
4076
- return null;
3947
+ }
3948
+ const upstreams = loadUpstreams();
3949
+ const npmLatest = await latestNpmVersion("jorgex-stack");
3950
+ if (npmLatest === null)
3951
+ p4.log.info(`jorgex-stack: v${localVersion} local (a\xFAn no publicado en npm o sin red).`);
3952
+ else if (npmLatest === localVersion)
3953
+ p4.log.success(`jorgex-stack: v${localVersion} \u2014 al d\xEDa.`);
3954
+ else
3955
+ p4.log.warn(
3956
+ `jorgex-stack: v${localVersion} local, v${npmLatest} en npm \u2192 pnpm dlx jorgex-stack@latest`
3957
+ );
3958
+ const engramRepo = upstreams.tools["engram"]?.source.replace(/^github:/, "");
3959
+ if (engramRepo) {
3960
+ const bin = detectEngram();
3961
+ const local = bin ? engramVersion(bin) : null;
3962
+ const latest = await latestGithubRelease(engramRepo);
3963
+ if (local === null) p4.log.warn("engram: no detectado en esta m\xE1quina.");
3964
+ else if (latest === null)
3965
+ p4.log.info(`engram: ${local} local (no se pudo consultar el upstream).`);
3966
+ else if (latest === local) p4.log.success(`engram: ${local} \u2014 al d\xEDa.`);
3967
+ else
3968
+ p4.log.warn(
3969
+ `engram: ${local} local, ${latest} disponible. Tu instalaci\xF3n NO se toca (D7) \u2014 actualiza t\xFA: github.com/${engramRepo}/releases`
3970
+ );
3971
+ }
3972
+ if (includeBrowserState && loadPlaywrightCliPreference() === true) {
3973
+ const playwright = resolvePlaywrightUpdateCheck({
3974
+ enabled: true,
3975
+ cli: detectPlaywrightCli()
3976
+ });
3977
+ if (playwright) p4.log[playwright.level](playwright.message);
3978
+ }
3979
+ const checkSkillNames = skillsToScan(isGitClone(), upstreams);
3980
+ if (checkSkillNames.length === 0) {
3981
+ p4.log.info(
3982
+ "Skills de terceros: pineadas con la versi\xF3n del stack (su revisi\xF3n upstream se hace desde el clon del repo)."
3983
+ );
3984
+ } else {
3985
+ const skillQueries = await querySkillHeads(checkSkillNames, upstreams);
3986
+ let moved = 0;
3987
+ for (const { name, repo, info, head } of skillQueries) {
3988
+ const label = info.modified ? `${name} (modificada localmente)` : name;
3989
+ const pinned = info.commit;
3990
+ if (!pinned)
3991
+ p4.log.warn(`${label}: sin pin en upstreams.json \u2014 a\xF1ade el commit revisado.`);
3992
+ else if (head === null)
3993
+ p4.log.info(`${label}: pin ${pinned.slice(0, 7)} (no se pudo consultar el upstream).`);
3994
+ else if (head === pinned)
3995
+ p4.log.success(`${label}: al d\xEDa con el pin ${pinned.slice(0, 7)}.`);
3996
+ else {
3997
+ moved++;
3998
+ p4.log.warn(
3999
+ `${label}: el upstream se movi\xF3 (pin ${pinned.slice(0, 7)} \u2192 ${head.slice(0, 7)}).
4000
+ Revisa el diff y, si lo aceptas, actualiza la copia vendorizada y el pin: github.com/${repo}/compare/${pinned.slice(0, 7)}...${head.slice(0, 7)}`
4001
+ );
4002
+ }
4077
4003
  }
4078
- const source = Reflect.get(packageValue, "source");
4079
- const name = Reflect.get(packageValue, "name");
4080
- const version = Reflect.get(packageValue, "version");
4081
- const scopeKind = Reflect.get(scope, "kind");
4082
- const codingAgentDir = Reflect.get(scope, "codingAgentDir");
4083
- if (name !== "jorgex-pi" || typeof version !== "string" || typeof source !== "string" || source !== `npm:jorgex-pi@${version}` || scopeKind !== "real" && scopeKind !== "target-dir" || typeof codingAgentDir !== "string") {
4084
- return null;
4004
+ if (moved === 0 && skillQueries.every(({ head }) => head !== null)) {
4005
+ p4.log.info("Skills de terceros: ning\xFAn upstream se ha movido respecto a su pin.");
4085
4006
  }
4086
- if (engram === void 0) return "upgrade-required";
4087
- if (engram === null || typeof engram !== "object" || Array.isArray(engram) || typeof Reflect.get(engram, "binary") !== "string" || !path29.isAbsolute(Reflect.get(engram, "binary"))) {
4007
+ }
4008
+ if (githubRateLimited()) {
4009
+ p4.log.warn(rateLimitHint("GitHub limit\xF3 algunas consultas (rate limit sin token)."));
4010
+ }
4011
+ p4.outro("Check completado.");
4012
+ return 0;
4013
+ }
4014
+ function isEngramRunning() {
4015
+ try {
4016
+ if (process.platform === "win32") {
4017
+ const ps = lookPath("powershell") ?? lookPath("pwsh");
4018
+ if (ps) {
4019
+ const out = runDetectedBin(ps, ["-NoProfile", "-Command", "Get-Process -Name engram -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count"], 5e3);
4020
+ return out !== null ? parseInt(out.trim(), 10) > 0 : null;
4021
+ }
4088
4022
  return null;
4023
+ } else {
4024
+ const pgrep = lookPath("pgrep");
4025
+ if (!pgrep) return null;
4026
+ try {
4027
+ execFileSync5(pgrep, ["-x", "engram"], { stdio: "ignore" });
4028
+ return true;
4029
+ } catch {
4030
+ return false;
4031
+ }
4089
4032
  }
4090
- return parsed;
4091
4033
  } catch {
4092
4034
  return null;
4093
4035
  }
4094
4036
  }
4095
- function parseReceipt(receiptJson, candidate, scope, engramBin) {
4096
- const parsed = parseReceiptShape(receiptJson);
4097
- if (parsed === null || parsed === "upgrade-required") return parsed;
4098
- const expected = expectedReceipt(candidate, parsed.state, scope, engramBin);
4099
- return sameRecord(parsed, expected) ? expected : null;
4037
+ function isGitClone(projectRoot = path29.dirname(stackRoot())) {
4038
+ return fs21.existsSync(path29.join(projectRoot, ".git"));
4100
4039
  }
4101
- function candidateIsValid(candidate, observed) {
4102
- return candidate.package.name === "jorgex-pi" && candidate.package.source === `npm:${candidate.package.name}@${candidate.package.version}` && candidate.contract.schemaVersion === 1 && candidate.contract.runner.schemaVersion === 1 && candidate.contract.runner.bin === "jorgex-pi" && candidate.contract.runner.maxStdoutBytes === 65536 && managedExternalWritesAreSafe(candidate.contract.managedExternalWrites) && [...REQUIRED_CAPABILITIES].every((capability) => candidate.contract.capabilities.includes(capability)) && sameRecord(candidate.tarball, observed);
4040
+ var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
4041
+ function resolvePnpm() {
4042
+ const bin = lookPath("pnpm") ?? lookPath("pnpm.cmd") ?? lookPath("pnpm.ps1");
4043
+ if (!bin) throw new Error("pnpm no encontrado en PATH.");
4044
+ return bin;
4103
4045
  }
4104
- function planPiPackageLifecycle(input) {
4105
- if (!candidateIsValid(input.candidate, input.observedTarball)) {
4106
- return blocked(input, "tarball-integrity");
4107
- }
4108
- if (!input.candidate.pi.testedVersions.includes(input.pi.version)) {
4109
- return blocked(input, "unsupported-pi-version");
4110
- }
4111
- if (input.engramBin === null) return blocked(input, "engram-missing");
4112
- const sources = parsePackageSources(input.pi.settingsJson);
4113
- if (sources === null) return blocked(input, "settings-corrupt");
4114
- const matchingSources = sources.filter(({ source }) => isJorgeXPiSource(source));
4115
- const exactSources = matchingSources.filter(({ source }) => source === input.candidate.package.source);
4116
- if (exactSources.length > 1) return blocked(input, "duplicate-package");
4117
- if (matchingSources.some(({ source }) => source !== input.candidate.package.source)) {
4118
- return blocked(input, "source-divergent");
4046
+ function cleanupTmp(dir) {
4047
+ try {
4048
+ fs21.rmSync(dir, { recursive: true, force: true });
4049
+ } catch {
4119
4050
  }
4120
- let receipt = null;
4121
- if (input.receiptJson !== null) {
4122
- const parsedReceipt = parseReceipt(input.receiptJson, input.candidate, {
4123
- kind: input.scope.kind,
4124
- codingAgentDir: path29.resolve(input.scope.codingAgentDir)
4125
- }, input.engramBin);
4126
- if (parsedReceipt === "upgrade-required") return blocked(input, "receipt-upgrade-required");
4127
- if (parsedReceipt === null) return blocked(input, "receipt-corrupt");
4128
- receipt = parsedReceipt;
4129
- if (receipt.state === "installing") return blocked(input, "partial-state");
4130
- const exactSource = exactSources[0];
4131
- if (exactSources.length !== 1 || exactSource === void 0 || !isExactManagedPackage(exactSource.entry, input.candidate.package.source)) {
4132
- return blocked(input, "source-divergent");
4051
+ }
4052
+ function updateStackGitClone() {
4053
+ const projectRoot = path29.dirname(stackRoot());
4054
+ const git = lookPath("git");
4055
+ if (!git) throw new Error("git no encontrado en PATH.");
4056
+ const pnpm = resolvePnpm();
4057
+ p4.log.info("Ejecutando git pull\u2026");
4058
+ execFileSync5(git, ["pull"], { cwd: projectRoot, stdio: "inherit" });
4059
+ p4.log.info("Ejecutando pnpm install\u2026");
4060
+ execFileSync5(pnpm, ["install"], { cwd: projectRoot, stdio: "inherit" });
4061
+ p4.log.info("Ejecutando pnpm build\u2026");
4062
+ execFileSync5(pnpm, ["build"], { cwd: projectRoot, stdio: "inherit" });
4063
+ }
4064
+ function updateStackGlobal() {
4065
+ const pnpm = resolvePnpm();
4066
+ p4.log.info("Ejecutando pnpm add -g jorgex-stack@latest\u2026");
4067
+ execFileSync5(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
4068
+ }
4069
+ async function downloadSkillToTemp(repo, sha, skillPath) {
4070
+ const root = fs21.mkdtempSync(path29.join(os4.tmpdir(), "jorgex-skill-"));
4071
+ try {
4072
+ const result = await downloadRepoTarball(repo, sha, root, skillPath);
4073
+ if (!result.ok) {
4074
+ cleanupTmp(root);
4075
+ return { error: result.reason };
4133
4076
  }
4077
+ if (skillPath) {
4078
+ const sub = path29.resolve(path29.join(root, skillPath));
4079
+ if (!isContainedIn(sub, root)) {
4080
+ cleanupTmp(root);
4081
+ return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
4082
+ }
4083
+ if (fs21.existsSync(sub)) return { dir: sub, root };
4084
+ const lastSeg = skillPath.split("/").pop();
4085
+ const sub2 = path29.resolve(path29.join(root, lastSeg));
4086
+ if (isContainedIn(sub2, root) && fs21.existsSync(sub2)) {
4087
+ if (!validateExtractedTree(sub2)) {
4088
+ cleanupTmp(root);
4089
+ return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
4090
+ }
4091
+ return { dir: sub2, root };
4092
+ }
4093
+ cleanupTmp(root);
4094
+ return { error: `la ruta "${skillPath}" no existe en el tarball del upstream` };
4095
+ }
4096
+ return { dir: root, root };
4097
+ } catch (err) {
4098
+ cleanupTmp(root);
4099
+ return { error: err instanceof Error ? err.message : "error desconocido" };
4134
4100
  }
4135
- if (exactSources.length === 1 && receipt === null) {
4136
- return {
4137
- kind: "manual-existing",
4138
- receiptPath: input.scope.receiptPath,
4139
- ownership: ownership(false)
4140
- };
4141
- }
4142
- if (exactSources.length === 1 && receipt !== null) {
4143
- return {
4144
- kind: "ready",
4145
- receiptPath: input.scope.receiptPath,
4146
- ownership: ownership(true)
4147
- };
4148
- }
4149
- return {
4150
- kind: "install",
4151
- receiptPath: input.scope.receiptPath,
4152
- invocation: {
4153
- executable: input.pi.executable,
4154
- args: ["install", input.candidate.package.source, "--no-approve"],
4155
- environment: input.scope.environment
4156
- },
4157
- receipt: expectedReceipt(input.candidate, "installing", {
4158
- kind: input.scope.kind,
4159
- codingAgentDir: path29.resolve(input.scope.codingAgentDir)
4160
- }, input.engramBin),
4161
- ownership: ownership(true)
4162
- };
4163
4101
  }
4164
- function parseRunnerRecord(stdout, stderr, command, candidate, packageRunner) {
4165
- if (stderr !== "" || !stdout.endsWith("\n") || Buffer.byteLength(stdout) > candidate.contract.runner.maxStdoutBytes) {
4166
- return null;
4167
- }
4168
- const body = stdout.slice(0, -1);
4169
- if (body === "" || body.includes("\n") || body.includes("\r")) return null;
4102
+ function pruneEngramDbBackups() {
4170
4103
  try {
4171
- const parsed = JSON.parse(body);
4172
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
4173
- const record = parsed;
4174
- if (record.schemaVersion !== candidate.contract.runner.schemaVersion || record.command !== command || record.ok !== true || record.package === null || typeof record.package !== "object" || record.package.name !== candidate.package.name || record.package.version !== candidate.package.version || typeof record.package.root !== "string" || !path29.isAbsolute(record.package.root) || path29.resolve(packageRunner) !== path29.resolve(record.package.root, "bin", "jorgex-pi.mjs")) {
4175
- return null;
4104
+ const dir = dataDir();
4105
+ if (!fs21.existsSync(dir)) return;
4106
+ const backups = fs21.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs21.statSync(path29.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
4107
+ for (const old of backups.slice(3)) {
4108
+ try {
4109
+ fs21.rmSync(path29.join(dir, old.name));
4110
+ } catch {
4111
+ }
4176
4112
  }
4177
- return record;
4178
4113
  } catch {
4179
- return null;
4180
4114
  }
4181
4115
  }
4182
- function runPackageCommand(input, deps, command) {
4183
- const result = deps.run({
4184
- executable: input.packageRunner,
4185
- args: [command, "--json"],
4186
- environment: input.environment
4187
- });
4188
- if (result.exitCode !== 0) return { kind: "blocked", reason: "runner-unhealthy" };
4189
- return parseRunnerRecord(result.stdout, result.stderr, command, input.candidate, input.packageRunner) ?? { kind: "blocked", reason: "runner-output" };
4190
- }
4191
- function isBlockedResult(value) {
4192
- return "kind" in value;
4193
- }
4194
- function executePiPackageLifecycle(input, deps) {
4195
- if (input.plan.kind === "manual-existing") return { kind: "manual-existing" };
4196
- if (input.operation === "install") {
4197
- if (input.plan.kind !== "install" || input.plan.receipt === void 0 || input.plan.invocation === void 0) {
4198
- return { kind: "blocked", reason: "runner-unhealthy" };
4199
- }
4200
- deps.writeReceipt(input.plan.receipt);
4201
- const installed = deps.run(input.plan.invocation);
4202
- if (installed.exitCode !== 0 || installed.stderr !== "") {
4203
- return { kind: "blocked", reason: "pi-install-failed" };
4116
+ function rotateLockedBinary(binPath, sweepRoot = HOME) {
4117
+ if (!fs21.existsSync(binPath)) return null;
4118
+ const dir = path29.dirname(binPath);
4119
+ const base = path29.basename(binPath);
4120
+ const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
4121
+ const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
4122
+ const resolvedDir = path29.resolve(dir);
4123
+ if (resolvedDir === path29.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
4124
+ try {
4125
+ for (const entry of fs21.readdirSync(dir)) {
4126
+ if (oldPattern.test(entry)) {
4127
+ try {
4128
+ fs21.rmSync(path29.join(dir, entry), { force: true });
4129
+ } catch {
4130
+ }
4131
+ }
4132
+ }
4133
+ } catch {
4134
+ }
4135
+ }
4136
+ const rotated = path29.join(dir, `${base}.old-${Date.now()}`);
4137
+ fs21.renameSync(binPath, rotated);
4138
+ return rotated;
4139
+ }
4140
+ async function updateEngram(engramRepo, latestVersion) {
4141
+ if (isEngramRunning()) {
4142
+ p4.log.info(
4143
+ "Engram est\xE1 en ejecuci\xF3n: los procesos vivos seguir\xE1n usando la versi\xF3n antigua hasta que reinicies los clientes (Claude Code/OpenCode/Codex)."
4144
+ );
4145
+ }
4146
+ const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path29.join(HOME, ".engram");
4147
+ const engramDb = path29.join(engramDataDir, "engram.db");
4148
+ if (fs21.existsSync(engramDb)) {
4149
+ const doBackup = await p4.confirm({
4150
+ message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
4151
+ initialValue: true
4152
+ });
4153
+ if (!p4.isCancel(doBackup) && doBackup) {
4154
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4155
+ const dest = path29.join(dataDir(), `engram-db-backup-${ts}.db`);
4156
+ try {
4157
+ if (!fs21.existsSync(dataDir())) fs21.mkdirSync(dataDir(), { recursive: true });
4158
+ fs21.copyFileSync(engramDb, dest);
4159
+ p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
4160
+ pruneEngramDbBackups();
4161
+ } catch (err) {
4162
+ p4.log.warn(`No se pudo copiar la DB: ${err instanceof Error ? err.message : err}. Continuando sin backup.`);
4163
+ }
4164
+ }
4165
+ }
4166
+ let anyChannelTried = false;
4167
+ const brew = lookPath("brew");
4168
+ if (brew) {
4169
+ let brewManages = false;
4170
+ try {
4171
+ execFileSync5(brew, ["list", "engram"], { stdio: "pipe" });
4172
+ brewManages = true;
4173
+ } catch {
4174
+ }
4175
+ if (brewManages) {
4176
+ anyChannelTried = true;
4177
+ p4.log.info("Actualizando engram con brew\u2026");
4178
+ try {
4179
+ execFileSync5(brew, ["upgrade", "engram"], { stdio: "inherit" });
4180
+ return true;
4181
+ } catch (err) {
4182
+ p4.log.error(`brew upgrade engram fall\xF3: ${err instanceof Error ? err.message : err}`);
4183
+ return false;
4184
+ }
4185
+ }
4186
+ }
4187
+ const go = lookPath("go");
4188
+ if (go) {
4189
+ anyChannelTried = true;
4190
+ let rotated = null;
4191
+ const bin = detectEngram();
4192
+ if (process.platform === "win32" && bin) {
4193
+ try {
4194
+ rotated = rotateLockedBinary(bin);
4195
+ } catch (err) {
4196
+ p4.log.warn(
4197
+ `No se pudo rotar el binario en uso: ${err instanceof Error ? err.message : err}. go install puede fallar por bloqueo.`
4198
+ );
4199
+ }
4200
+ }
4201
+ p4.log.info(`Actualizando engram con go install (${latestVersion})\u2026`);
4202
+ try {
4203
+ execFileSync5(
4204
+ go,
4205
+ ["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
4206
+ { stdio: "inherit" }
4207
+ );
4208
+ const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs21.existsSync(bin ?? "") });
4209
+ if (rollbackOk.action === "restore") {
4210
+ try {
4211
+ fs21.renameSync(rotated, bin);
4212
+ p4.log.warn(rollbackOk.messages.onRestore);
4213
+ } catch {
4214
+ p4.log.warn(rollbackOk.messages.onRenameFail);
4215
+ }
4216
+ }
4217
+ return true;
4218
+ } catch (err) {
4219
+ const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs21.existsSync(bin ?? "") });
4220
+ if (rollbackFail.action === "restore") {
4221
+ try {
4222
+ fs21.renameSync(rotated, bin);
4223
+ p4.log.info(rollbackFail.messages.onRestore);
4224
+ } catch {
4225
+ p4.log.error(rollbackFail.messages.onRenameFail);
4226
+ }
4227
+ } else if (rollbackFail.action === "leave_old") {
4228
+ p4.log.warn(rollbackFail.messages.onLeaveOld);
4229
+ }
4230
+ p4.log.error(`go install fall\xF3: ${err instanceof Error ? err.message : err}`);
4231
+ return false;
4232
+ }
4233
+ }
4234
+ if (anyChannelTried) {
4235
+ p4.log.error(
4236
+ `Los canales de actualizaci\xF3n disponibles fallaron \u2014 revisa el error arriba; descarga manual: github.com/${engramRepo}/releases/tag/v${latestVersion}`
4237
+ );
4238
+ } else {
4239
+ p4.log.warn(
4240
+ `No se encontr\xF3 brew ni go en PATH.
4241
+ Descarga manual: github.com/${engramRepo}/releases/tag/v${latestVersion}`
4242
+ );
4243
+ }
4244
+ return false;
4245
+ }
4246
+ function resolveEngramRollback(input) {
4247
+ const { installOk, rotated, bin, binExists } = input;
4248
+ if (!rotated || !bin) return { action: "none" };
4249
+ if (installOk && !binExists) {
4250
+ return {
4251
+ action: "restore",
4252
+ messages: {
4253
+ onRestore: `go install instal\xF3 en otra ruta (GOBIN distinto); el binario activo en ${bin} se ha restaurado. Comprueba qu\xE9 engram resuelve tu PATH.`,
4254
+ onRenameFail: `go install instal\xF3 en otra ruta y no se pudo restaurar ${bin}; tu binario anterior est\xE1 en ${rotated}.`
4255
+ }
4256
+ };
4257
+ }
4258
+ if (!installOk && !binExists) {
4259
+ return {
4260
+ action: "restore",
4261
+ messages: {
4262
+ onRestore: "Rotaci\xF3n revertida \u2014 el binario anterior sigue en su sitio.",
4263
+ onRenameFail: `No se pudo revertir la rotaci\xF3n: tu binario anterior est\xE1 en ${rotated} \u2014 ren\xF3mbralo a ${bin} a mano.`
4264
+ }
4265
+ };
4266
+ }
4267
+ if (!installOk && binExists) {
4268
+ return {
4269
+ action: "leave_old",
4270
+ messages: {
4271
+ onLeaveOld: `El install fall\xF3 pero dej\xF3 un binario en ${bin}; tu copia anterior queda en ${rotated}.`
4272
+ }
4273
+ };
4274
+ }
4275
+ return { action: "none" };
4276
+ }
4277
+ function buildEligibleSkillUpdates(skills) {
4278
+ const result = [];
4279
+ for (const { name, repo, info, head } of skills) {
4280
+ if (info.kind === "release") continue;
4281
+ const pinned = info.commit;
4282
+ if (!pinned) continue;
4283
+ if (head === null) continue;
4284
+ if (head === pinned) continue;
4285
+ result.push({
4286
+ name,
4287
+ repo,
4288
+ head,
4289
+ pinned,
4290
+ skillPath: info.path,
4291
+ modified: info.modified ?? false
4292
+ });
4293
+ }
4294
+ return result;
4295
+ }
4296
+ function hasIncompleteSkillScan(skills) {
4297
+ return skills.some(({ info, head }) => info.kind !== "release" && (!info.commit || head === null));
4298
+ }
4299
+ function resolveUpdateSyncRequired(updated) {
4300
+ return updated.some((item) => item === "stack" || item === "skill" || item.startsWith("skill:"));
4301
+ }
4302
+ async function runInteractiveUpdate(localVersion, yes, dryRun = false, includeBrowserState = true) {
4303
+ if (dryRun || yes || !process.stdout.isTTY) {
4304
+ return { exitCode: await runUpdateCheck(localVersion, includeBrowserState), appliedUpdates: false, syncRequired: false };
4305
+ }
4306
+ p4.intro("jorgex-stack update");
4307
+ if (includeBrowserState) {
4308
+ const preferenceErrors = browserPreferenceErrors();
4309
+ if (preferenceErrors.length > 0) {
4310
+ for (const error of preferenceErrors) p4.log.error(error);
4311
+ p4.outro("Update cancelado: corrige las preferencias de navegador antes de reintentar.");
4312
+ return { exitCode: 1, appliedUpdates: false, syncRequired: false };
4313
+ }
4314
+ }
4315
+ const upstreams = loadUpstreams();
4316
+ let exitCode = 0;
4317
+ let appliedUpdates = false;
4318
+ const updated = [];
4319
+ const maintainer = isGitClone();
4320
+ const spin = p4.spinner();
4321
+ spin.start("Consultando versiones upstream\u2026");
4322
+ const skillNames = skillsToScan(maintainer, upstreams);
4323
+ const [npmLatest, engramLatestRaw, skillHeads] = await Promise.all([
4324
+ latestNpmVersion("jorgex-stack"),
4325
+ (async () => {
4326
+ const repo = upstreams.tools["engram"]?.source.replace(/^github:/, "");
4327
+ if (!repo) return null;
4328
+ return { repo, version: await latestGithubRelease(repo) };
4329
+ })(),
4330
+ querySkillHeads(skillNames, upstreams)
4331
+ ]);
4332
+ spin.stop("Consulta completada.");
4333
+ if (githubRateLimited()) {
4334
+ p4.log.warn(rateLimitHint("GitHub limit\xF3 algunas consultas \u2014 los upstreams 'sin conexi\xF3n' pueden ser eso."));
4335
+ }
4336
+ const updateItems = [];
4337
+ const engramData = engramLatestRaw;
4338
+ let stackNeedsUpdate = false;
4339
+ if (npmLatest === null) {
4340
+ p4.log.info(`jorgex-stack: v${localVersion} local (a\xFAn no publicado en npm o sin red).`);
4341
+ } else if (npmLatest === localVersion) {
4342
+ p4.log.success(`jorgex-stack: v${localVersion} \u2014 al d\xEDa.`);
4343
+ } else {
4344
+ stackNeedsUpdate = true;
4345
+ const mode = maintainer ? STACK_METHOD_CLONE : `pnpm add -g jorgex-stack@${npmLatest}`;
4346
+ updateItems.push({
4347
+ value: "stack",
4348
+ label: `jorgex-stack: v${localVersion} \u2192 v${npmLatest}`,
4349
+ hint: mode
4350
+ });
4351
+ }
4352
+ let engramNeedsUpdate = false;
4353
+ let engramLocalVersion = null;
4354
+ if (engramData) {
4355
+ const engramBinLocal = detectEngram();
4356
+ engramLocalVersion = engramBinLocal ? engramVersion(engramBinLocal) : null;
4357
+ if (engramLocalVersion === null) {
4358
+ p4.log.warn("engram: no detectado en esta m\xE1quina.");
4359
+ } else if (engramData.version === null) {
4360
+ p4.log.info(`engram: ${engramLocalVersion} local (no se pudo consultar el upstream).`);
4361
+ } else if (engramData.version === engramLocalVersion) {
4362
+ p4.log.success(`engram: ${engramLocalVersion} \u2014 al d\xEDa.`);
4363
+ } else {
4364
+ engramNeedsUpdate = true;
4365
+ updateItems.push({
4366
+ value: "engram",
4367
+ label: `engram: ${engramLocalVersion} \u2192 ${engramData.version}`,
4368
+ hint: "canal nativo (brew \u2192 go install \u2192 URL)"
4369
+ });
4370
+ }
4371
+ }
4372
+ let playwrightNeedsUpdate = false;
4373
+ if (includeBrowserState && loadPlaywrightCliPreference() === true) {
4374
+ const playwright = detectPlaywrightCli();
4375
+ if (playwright.status === "current") {
4376
+ p4.log.success("Playwright CLI: al d\xEDa.");
4377
+ } else {
4378
+ playwrightNeedsUpdate = true;
4379
+ const current = playwright.detectedVersion ?? playwright.status;
4380
+ updateItems.push({
4381
+ value: "playwright-cli",
4382
+ label: `Playwright CLI: ${current} \u2192 pin aprobado`,
4383
+ hint: "pnpm add --global @playwright/cli@0.1.18"
4384
+ });
4385
+ }
4386
+ }
4387
+ const typedSkillHeads = skillHeads;
4388
+ for (const { name, repo, info, head } of typedSkillHeads) {
4389
+ if (info.kind === "release") {
4390
+ if (head !== null && info.commit && head !== info.commit) {
4391
+ p4.log.warn(`${name} (release): upstream se movi\xF3. Revisa: github.com/${repo}/releases`);
4392
+ }
4393
+ continue;
4394
+ }
4395
+ const label = info.modified ? `${name} (modificada localmente)` : name;
4396
+ const pinned = info.commit;
4397
+ if (!pinned) {
4398
+ p4.log.warn(`${label}: sin pin \u2014 no se puede actualizar.`);
4399
+ } else if (head === null) {
4400
+ p4.log.info(`${label}: sin conexi\xF3n al upstream.`);
4401
+ } else if (head === pinned) {
4402
+ p4.log.success(`${label}: al d\xEDa (pin ${pinned.slice(0, 7)}).`);
4403
+ }
4404
+ }
4405
+ if (!maintainer) {
4406
+ p4.log.info(
4407
+ "Skills de terceros: pineadas con esta versi\xF3n del stack \u2014 su revisi\xF3n upstream se hace desde el clon del repo."
4408
+ );
4409
+ }
4410
+ const skillUpdates = buildEligibleSkillUpdates(typedSkillHeads);
4411
+ for (const skillInfo of skillUpdates) {
4412
+ const modifiedWarning = skillInfo.modified ? " \u26A0 modificada localmente" : "";
4413
+ updateItems.push({
4414
+ value: `skill:${skillInfo.name}`,
4415
+ label: `skill ${skillInfo.name}: ${skillInfo.pinned.slice(0, 7)} \u2192 ${skillInfo.head.slice(0, 7)}${modifiedWarning}`,
4416
+ hint: `github.com/${skillInfo.repo}/compare/${skillInfo.pinned.slice(0, 7)}...${skillInfo.head.slice(0, 7)}`
4417
+ });
4418
+ }
4419
+ if (updateItems.length === 0) {
4420
+ if (maintainer && hasIncompleteSkillScan(typedSkillHeads)) {
4421
+ p4.outro("Escaneo incompleto: no se pudo comprobar el estado de todas las skills.");
4422
+ return { exitCode: 1, appliedUpdates: false, syncRequired: false };
4423
+ }
4424
+ p4.outro("Todo al d\xEDa. No hay actualizaciones disponibles.");
4425
+ return { exitCode: 0, appliedUpdates: false, syncRequired: false };
4426
+ }
4427
+ const selected = await p4.multiselect({
4428
+ message: "Selecciona qu\xE9 actualizar (espacio para marcar, intro para confirmar):",
4429
+ options: updateItems,
4430
+ initialValues: updateItems.map((i) => i.value),
4431
+ required: false
4432
+ });
4433
+ if (p4.isCancel(selected)) {
4434
+ p4.outro("Update cancelado.");
4435
+ return { exitCode: 0, appliedUpdates: false, syncRequired: false };
4436
+ }
4437
+ if (selected.length === 0) {
4438
+ p4.outro("Nada seleccionado.");
4439
+ return { exitCode: 0, appliedUpdates: false, syncRequired: false };
4440
+ }
4441
+ const sel = selected;
4442
+ if (stackNeedsUpdate && sel.includes("stack")) {
4443
+ const isClone = isGitClone();
4444
+ const method = isClone ? STACK_METHOD_CLONE : "pnpm add -g jorgex-stack@latest";
4445
+ const confirm5 = await p4.confirm({
4446
+ message: `Actualizar el stack con: ${method}`,
4447
+ initialValue: true
4448
+ });
4449
+ if (p4.isCancel(confirm5) || !confirm5) {
4450
+ p4.log.info("Stack: actualizaci\xF3n omitida.");
4451
+ } else {
4452
+ try {
4453
+ if (isClone) {
4454
+ updateStackGitClone();
4455
+ } else {
4456
+ updateStackGlobal();
4457
+ }
4458
+ p4.log.success("Stack actualizado correctamente.");
4459
+ appliedUpdates = true;
4460
+ updated.push("stack");
4461
+ } catch (err) {
4462
+ p4.log.error(`Stack: error al actualizar \u2014 ${err instanceof Error ? err.message : err}`);
4463
+ exitCode = 1;
4464
+ }
4204
4465
  }
4205
- const doctor = runPackageCommand(input, deps, "doctor");
4206
- if (isBlockedResult(doctor)) return doctor;
4207
- const doctorResult = doctor.result;
4208
- if (doctorResult === null || typeof doctorResult !== "object" || Reflect.get(doctorResult, "healthy") !== true) {
4209
- return { kind: "blocked", reason: "runner-unhealthy" };
4466
+ }
4467
+ for (const skillInfo of skillUpdates) {
4468
+ if (!sel.includes(`skill:${skillInfo.name}`)) continue;
4469
+ if (skillInfo.modified) {
4470
+ p4.log.warn(
4471
+ `skill ${skillInfo.name}: TIENE CAMBIOS LOCALES. Actualizar sobreescribir\xE1 esas modificaciones (backup autom\xE1tico).`
4472
+ );
4473
+ const forceConfirm = await p4.confirm({
4474
+ message: `\xBFConfirmas sobreescribir los cambios locales de ${skillInfo.name}?`,
4475
+ initialValue: false
4476
+ });
4477
+ if (p4.isCancel(forceConfirm) || !forceConfirm) {
4478
+ p4.log.info(`skill ${skillInfo.name}: omitida.`);
4479
+ continue;
4480
+ }
4481
+ }
4482
+ const spin2 = p4.spinner();
4483
+ spin2.start(`Descargando upstream de ${skillInfo.name}\u2026`);
4484
+ const tmpResult = await downloadSkillToTemp(skillInfo.repo, skillInfo.head, skillInfo.skillPath);
4485
+ spin2.stop("error" in tmpResult ? "Error de descarga." : "Descargado.");
4486
+ if ("error" in tmpResult) {
4487
+ p4.log.error(`skill ${skillInfo.name}: descarga fallida \u2014 ${tmpResult.error}. Omitiendo.`);
4488
+ exitCode = 1;
4489
+ continue;
4490
+ }
4491
+ const { dir: tmpDir, root: tmpRoot } = tmpResult;
4492
+ const localSkillDir = path29.join(stackRoot(), "skills", skillInfo.name);
4493
+ const diff = renderSkillDiff(tmpDir, localSkillDir);
4494
+ if (diff) {
4495
+ p4.log.info(`Diff de ${skillInfo.name}:
4496
+ ${diff}`);
4497
+ } else {
4498
+ p4.log.info(`${skillInfo.name}: sin cambios en contenido (el repo se movi\xF3 pero la skill no cambi\xF3).`);
4499
+ cleanupTmp(tmpRoot);
4500
+ continue;
4501
+ }
4502
+ const applySkill = await p4.confirm({
4503
+ message: `\xBFAplicar la actualizaci\xF3n de ${skillInfo.name}?`,
4504
+ // Default No — política deliberate-pin (el diff es de contenido)
4505
+ initialValue: false
4506
+ });
4507
+ if (p4.isCancel(applySkill) || !applySkill) {
4508
+ p4.log.info(`skill ${skillInfo.name}: omitida.`);
4509
+ cleanupTmp(tmpRoot);
4510
+ continue;
4511
+ }
4512
+ try {
4513
+ replaceSkill(skillInfo.name, tmpDir, skillInfo.head, {});
4514
+ p4.log.success(`skill ${skillInfo.name}: actualizada y re-pineada a ${skillInfo.head.slice(0, 7)}.`);
4515
+ appliedUpdates = true;
4516
+ updated.push("skill");
4517
+ } catch (err) {
4518
+ p4.log.error(`skill ${skillInfo.name}: error \u2014 ${err instanceof Error ? err.message : err}`);
4519
+ exitCode = 1;
4520
+ } finally {
4521
+ cleanupTmp(tmpRoot);
4210
4522
  }
4211
- const receipt = { ...input.plan.receipt, state: "installed" };
4212
- deps.writeReceipt(receipt);
4213
- return { kind: "installed", receipt };
4214
4523
  }
4215
- if (input.plan.kind !== "ready") return { kind: "blocked", reason: "runner-unhealthy" };
4216
- const command = runPackageCommand(input, deps, input.operation);
4217
- if (isBlockedResult(command)) return command;
4218
- if (input.operation === "sync") {
4219
- const result = command.result;
4220
- if (result === null || typeof result !== "object" || Reflect.get(result, "changed") !== false) {
4221
- return { kind: "blocked", reason: "runner-unhealthy" };
4524
+ if (playwrightNeedsUpdate && sel.includes("playwright-cli")) {
4525
+ const confirmPlaywright = await p4.confirm({
4526
+ message: "Actualizar Playwright CLI global al pin aprobado con pnpm add --global?",
4527
+ initialValue: true
4528
+ });
4529
+ if (p4.isCancel(confirmPlaywright) || !confirmPlaywright) {
4530
+ p4.log.info("Playwright CLI: actualizaci\xF3n omitida.");
4531
+ } else {
4532
+ const packageResult = executePlaywrightToolAction2("update");
4533
+ const browserResult = packageResult.ok ? executePlaywrightToolAction2("install-browser") : null;
4534
+ if (packageResult.ok && browserResult?.ok) {
4535
+ p4.log.success("Playwright CLI actualizado al pin aprobado.");
4536
+ appliedUpdates = true;
4537
+ updated.push("playwright-cli");
4538
+ } else {
4539
+ const failedResult = packageResult.ok ? browserResult : packageResult;
4540
+ const pnpmRemedy = failedResult && !failedResult.ok ? resolvePnpmFailureRemedy(failedResult.reason) : null;
4541
+ const recovery = pnpmRemedy === null ? "Ejecuta 'jorgex-stack install --playwright' para reintentar el paquete y el navegador." : `${pnpmRemedy} Despu\xE9s, ejecuta 'jorgex-stack update' para reintentar.`;
4542
+ const failedStep = packageResult.ok ? "descargar el navegador" : "actualizar el paquete global";
4543
+ p4.log.error(`Playwright CLI: no se pudo ${failedStep}. ${recovery}`);
4544
+ exitCode = 1;
4545
+ }
4222
4546
  }
4223
- return { kind: "synced", actions: [] };
4224
4547
  }
4225
- const models = command.result;
4226
- if (models === null || typeof models !== "object" || Reflect.get(models, "mode") !== "inherit-session" || !sameRecord(Reflect.get(models, "tiers"), ["strong", "standard", "cheap"])) {
4227
- return { kind: "blocked", reason: "runner-unhealthy" };
4548
+ if (engramNeedsUpdate && sel.includes("engram") && engramData?.version) {
4549
+ const confirmEngram = await p4.confirm({
4550
+ message: `Actualizar engram a v${engramData.version} (canal nativo: brew \u2192 go install \u2192 URL)`,
4551
+ initialValue: true
4552
+ });
4553
+ if (p4.isCancel(confirmEngram) || !confirmEngram) {
4554
+ p4.log.info("Engram: actualizaci\xF3n omitida.");
4555
+ } else {
4556
+ const ok = await updateEngram(engramData.repo, engramData.version);
4557
+ if (!ok) {
4558
+ exitCode = 1;
4559
+ } else {
4560
+ const binPost = detectEngram();
4561
+ const versionPost = binPost ? engramVersion(binPost) : null;
4562
+ if (versionPost) {
4563
+ p4.log.success(`Engram: ahora en v${versionPost}.`);
4564
+ } else {
4565
+ p4.log.warn("Engram actualizado, pero no se pudo verificar la versi\xF3n \u2014 comprueba con engram --version");
4566
+ }
4567
+ p4.log.info("Reinicia los clientes (Claude Code/OpenCode/Codex) para que sus MCP usen la versi\xF3n nueva.");
4568
+ appliedUpdates = true;
4569
+ updated.push("engram");
4570
+ }
4571
+ }
4228
4572
  }
4229
- return { kind: "models", models: { mode: "inherit-session", tiers: ["strong", "standard", "cheap"] } };
4573
+ p4.outro(exitCode === 0 ? "Update completado." : "Update completado con errores (revisa arriba).");
4574
+ return { exitCode, appliedUpdates, syncRequired: resolveUpdateSyncRequired(updated) };
4230
4575
  }
4231
- function receiptUpgradeRequired() {
4232
- return {
4233
- kind: "blocked",
4234
- reason: "receipt-upgrade-required",
4235
- remedy: "El receipt no enlaza Engram; usa la versi\xF3n anterior de Stack para desinstalarlo y luego reinstala."
4236
- };
4576
+
4577
+ // src/models-picker.ts
4578
+ import path30 from "path";
4579
+ import * as p5 from "@clack/prompts";
4580
+ var TIERS = ["strong", "standard", "cheap"];
4581
+ var EFFORTS = ["low", "medium", "high", "xhigh"];
4582
+ var CLAUDE_ALIASES = ["fable", "opus", "sonnet", "haiku", "inherit"];
4583
+ var CODEX_MODELS = [
4584
+ "default",
4585
+ "gpt-5.6-sol",
4586
+ "gpt-5.6-terra",
4587
+ "gpt-5.6-luna",
4588
+ "gpt-5.5",
4589
+ "gpt-5.4",
4590
+ "gpt-5.4-mini"
4591
+ ];
4592
+ var CODEX_CUSTOM = "__custom__";
4593
+ var CANCEL = /* @__PURE__ */ Symbol("cancel");
4594
+ function opencodeLiveModels(binPath) {
4595
+ const out = runDetectedBin(binPath, ["models"], 2e4);
4596
+ if (out === null) return null;
4597
+ const models = out.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== "" && l.includes("/"));
4598
+ return models.length > 0 ? models : null;
4599
+ }
4600
+ function agentsByTier() {
4601
+ const grouped = { strong: [], standard: [], cheap: [] };
4602
+ for (const agent of loadCanonicalAgents(path30.join(stackRoot(), "agents"))) {
4603
+ if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
4604
+ }
4605
+ return grouped;
4606
+ }
4607
+ function isCompleteRuntimeModelMap(models) {
4608
+ return TIERS.every((tier) => models[tier]?.model);
4609
+ }
4610
+ async function askModel(det, subject, current) {
4611
+ if (det.id === "codex") {
4612
+ if (!current) throw new Error("Codex requiere un model-map base.");
4613
+ const effort = await p5.select({
4614
+ message: `${det.name} \xB7 ${subject} \u2014 reasoning effort`,
4615
+ options: EFFORTS.map((v) => ({ value: v, label: v })),
4616
+ initialValue: current.variant ?? "medium"
4617
+ });
4618
+ if (p5.isCancel(effort)) return CANCEL;
4619
+ const modelOptions = [
4620
+ ...CODEX_MODELS.includes(current.model) ? [] : [{ value: current.model, label: `${current.model} (actual)` }],
4621
+ ...CODEX_MODELS.map((m) => ({
4622
+ value: m,
4623
+ label: m === "default" ? "default \u2014 el del CLI, se actualiza solo (recomendado)" : m
4624
+ })),
4625
+ { value: CODEX_CUSTOM, label: "otro\u2026 (escribir un ID a mano)" }
4626
+ ];
4627
+ let model = await p5.select({
4628
+ message: `${det.name} \xB7 ${subject} \u2014 modelo`,
4629
+ options: modelOptions,
4630
+ initialValue: current.model
4631
+ });
4632
+ if (p5.isCancel(model)) return CANCEL;
4633
+ if (model === CODEX_CUSTOM) {
4634
+ const typed = await p5.text({
4635
+ message: `${det.name} \xB7 ${subject} \u2014 ID exacto del modelo (min\xFAsculas; compru\xE9balo con /model dentro de codex)`,
4636
+ initialValue: current.model,
4637
+ validate: (v) => !v || v.trim() === "" ? 'Vac\xEDo no \u2014 usa "default" o un ID.' : void 0
4638
+ });
4639
+ if (p5.isCancel(typed)) return CANCEL;
4640
+ model = typed.trim().toLowerCase();
4641
+ }
4642
+ return { model, variant: effort };
4643
+ }
4644
+ const optionsList = det.options;
4645
+ const options = optionsList.map((m) => ({ value: m, label: m }));
4646
+ if (current && !optionsList.includes(current.model)) {
4647
+ options.unshift({ value: current.model, label: `${current.model} (actual)` });
4648
+ }
4649
+ const choice = await p5.select({
4650
+ message: `${det.name} \xB7 ${subject} \u2014 modelo`,
4651
+ options,
4652
+ initialValue: current?.model,
4653
+ maxItems: 12
4654
+ });
4655
+ if (p5.isCancel(choice)) return CANCEL;
4656
+ if (det.id === "claude-code") return { model: choice };
4657
+ const keptCurrent = current && choice === current.model && current.variant ? current.variant : null;
4658
+ const variant = await p5.select({
4659
+ message: `${det.name} \xB7 ${subject} \u2014 reasoning effort (variant; solo si el modelo lo soporta)`,
4660
+ options: [
4661
+ { value: "", label: "(sin variant \u2014 el default del modelo)" },
4662
+ ...EFFORTS.map((v) => ({ value: v, label: v })),
4663
+ ...keptCurrent && !EFFORTS.includes(keptCurrent) ? [{ value: keptCurrent, label: `${keptCurrent} (actual)` }] : []
4664
+ ],
4665
+ initialValue: keptCurrent ?? ""
4666
+ });
4667
+ if (p5.isCancel(variant)) return CANCEL;
4668
+ return { model: choice, ...variant ? { variant } : {} };
4237
4669
  }
4238
- function validateOwnedOperationState(input) {
4239
- const sources = parsePackageSources(input.detected.settingsJson);
4240
- if (sources === null) return { kind: "blocked", reason: "settings-corrupt" };
4241
- const matchingSources = sources.filter(({ source: source2 }) => isJorgeXPiSource(source2));
4242
- if (matchingSources.length > 1) return { kind: "blocked", reason: "duplicate-package" };
4243
- if (input.receiptJson === null) {
4244
- return { kind: "blocked", reason: matchingSources.length === 1 ? "manual-existing" : "source-divergent" };
4670
+ async function runModelsPicker(opts) {
4671
+ const file = ensureModelMapFile();
4672
+ const map = loadModelMap();
4673
+ if (opts.yes || !process.stdout.isTTY) {
4674
+ if (opts.runtimes.includes("opencode") && !map.opencode) {
4675
+ console.error("OpenCode requiere selecci\xF3n interactiva desde los proveedores conectados; ejecuta 'models --agents opencode' sin --yes.");
4676
+ return 1;
4677
+ }
4678
+ console.log(`Model-map en ${file}. Ed\xEDtalo o ejecuta 'models' sin --yes para el picker.`);
4679
+ return 0;
4245
4680
  }
4246
- const parsedReceipt = parseReceiptShape(input.receiptJson);
4247
- if (parsedReceipt === "upgrade-required") return receiptUpgradeRequired();
4248
- if (parsedReceipt === null) return { kind: "blocked", reason: "receipt-corrupt" };
4249
- const receipt = parsedReceipt;
4250
- if (receipt.state !== "installed") return { kind: "blocked", reason: "partial-state" };
4251
- const accepted = input.registry.acceptedCandidates ?? [input.registry.candidate];
4252
- if (!accepted.some((candidate) => sameRecord(receipt.candidate, {
4253
- package: candidate.package,
4254
- tarball: candidate.tarball,
4255
- provenance: candidate.provenance
4256
- }))) {
4257
- return { kind: "blocked", reason: "receipt-untrusted" };
4681
+ p5.intro("jorgex-stack models \u2014 modelos por tier o por subagente");
4682
+ const grouped = agentsByTier();
4683
+ const tierLine = (tier) => grouped[tier].join(", ");
4684
+ const agentCount = TIERS.reduce((n, tier) => n + grouped[tier].length, 0);
4685
+ const detections = [];
4686
+ if (opts.runtimes.includes("opencode")) {
4687
+ const opencode = detectOpenCode();
4688
+ detections.push({
4689
+ id: "opencode",
4690
+ name: "OpenCode",
4691
+ installed: opencode.installed,
4692
+ options: opencode.binPath ? opencodeLiveModels(opencode.binPath) : null
4693
+ });
4258
4694
  }
4259
- if (receipt.scope.kind !== (input.paths.targetDir ? "target-dir" : "real") || path29.resolve(receipt.scope.codingAgentDir) !== path29.resolve(input.paths.codingAgentDir)) {
4260
- return { kind: "blocked", reason: "source-divergent" };
4695
+ if (opts.runtimes.includes("claude-code")) {
4696
+ detections.push({ id: "claude-code", name: "Claude Code", installed: detectClaudeCode().installed, options: CLAUDE_ALIASES });
4261
4697
  }
4262
- if (input.engramBin !== null && path29.resolve(receipt.engram.binary) !== path29.resolve(input.engramBin)) {
4263
- return { kind: "blocked", reason: "receipt-corrupt" };
4698
+ if (opts.runtimes.includes("codex")) {
4699
+ detections.push({ id: "codex", name: "Codex CLI", installed: detectCodex().installed, options: null });
4264
4700
  }
4265
- const source = receipt.candidate.package.source;
4266
- const matchingSource = matchingSources[0];
4267
- if (matchingSources.length !== 1 || matchingSource === void 0 || matchingSource.source !== source || !isExactManagedPackage(matchingSource.entry, source)) {
4268
- return { kind: "blocked", reason: "source-divergent" };
4701
+ for (const det of detections) {
4702
+ if (!det.installed) {
4703
+ p5.log.info(`${det.name}: no instalado \u2014 se mantienen los defaults.`);
4704
+ continue;
4705
+ }
4706
+ if (det.id === "opencode" && det.options === null) {
4707
+ p5.log.warn("OpenCode: no se pudo listar `opencode models` \u2014 se mantiene la selecci\xF3n actual.");
4708
+ continue;
4709
+ }
4710
+ const existingRuntimeMap = map[det.id];
4711
+ const runtimeMap = {
4712
+ ...existingRuntimeMap
4713
+ };
4714
+ p5.log.message(
4715
+ `${det.name} \u2014 tiers y sus subagentes:
4716
+ strong \u2192 ${tierLine("strong")}
4717
+ standard \u2192 ${tierLine("standard")}
4718
+ cheap \u2192 ${tierLine("cheap")}`
4719
+ );
4720
+ const mode = await p5.select({
4721
+ message: `${det.name} \u2014 \xBFc\xF3mo asignar los modelos?`,
4722
+ options: [
4723
+ { value: "tier", label: "Por tier \u2014 3 grupos (r\xE1pido)" },
4724
+ { value: "agent", label: `Por subagente \u2014 uno a uno (${agentCount} subagentes, control total)` }
4725
+ ],
4726
+ initialValue: "tier"
4727
+ });
4728
+ if (p5.isCancel(mode)) return cancelled();
4729
+ if (mode === "tier") {
4730
+ for (const tier of TIERS) {
4731
+ const asked = await askModel(det, `tier ${tier} (${tierLine(tier)})`, existingRuntimeMap?.[tier]);
4732
+ if (asked === CANCEL) return cancelled();
4733
+ runtimeMap[tier] = asked;
4734
+ }
4735
+ const overrideCount = Object.keys(runtimeMap.overrides ?? {}).length;
4736
+ if (overrideCount > 0) {
4737
+ p5.log.info(
4738
+ `${det.name}: se conservan ${overrideCount} ajustes por subagente previos \u2014 elige "Por subagente" para revisarlos.`
4739
+ );
4740
+ }
4741
+ } else {
4742
+ const overrides = { ...runtimeMap.overrides ?? {} };
4743
+ for (const tier of TIERS) {
4744
+ let base = runtimeMap[tier];
4745
+ for (const name of grouped[tier]) {
4746
+ const current = existingRuntimeMap ? resolveAgentModel(existingRuntimeMap, name, tier) : void 0;
4747
+ const asked = await askModel(det, `${name} (tier ${tier})`, current);
4748
+ if (asked === CANCEL) return cancelled();
4749
+ if (!base) {
4750
+ base = asked;
4751
+ runtimeMap[tier] = base;
4752
+ }
4753
+ const sameAsTier = asked.model === base.model && (asked.variant ?? "") === (base.variant ?? "");
4754
+ if (sameAsTier) {
4755
+ delete overrides[name];
4756
+ } else {
4757
+ overrides[name] = {
4758
+ model: asked.model,
4759
+ ...asked.variant ? { variant: asked.variant } : base.variant ? { variant: "" } : {}
4760
+ };
4761
+ }
4762
+ }
4763
+ }
4764
+ if (Object.keys(overrides).length > 0) runtimeMap.overrides = overrides;
4765
+ else delete runtimeMap.overrides;
4766
+ }
4767
+ if (!isCompleteRuntimeModelMap(runtimeMap)) {
4768
+ throw new Error(`${det.name}: selecci\xF3n de modelos incompleta.`);
4769
+ }
4770
+ map[det.id] = runtimeMap;
4269
4771
  }
4270
- return { receipt, source };
4772
+ writeText(file, JSON.stringify(map, null, 2) + "\n");
4773
+ p5.log.success(`Guardado en ${file}`);
4774
+ return 0;
4271
4775
  }
4272
- function operationWasBlocked(value) {
4273
- return "kind" in value;
4776
+ function cancelled() {
4777
+ p5.cancel("Cancelado \u2014 no se ha guardado nada.");
4778
+ return 1;
4274
4779
  }
4275
- function runManagedRunner(input, deps, command) {
4276
- const result = deps.run({
4277
- executable: input.detected.packageRunner,
4278
- args: [command, "--json"],
4279
- environment: input.paths.environment
4280
- });
4281
- if (result.exitCode !== 0) return { kind: "blocked", reason: "runner-unhealthy" };
4282
- const parsed = parseRunnerRecord(
4283
- result.stdout,
4284
- result.stderr,
4285
- command,
4286
- input.registry.candidate,
4287
- input.detected.packageRunner
4288
- );
4289
- return parsed ?? { kind: "blocked", reason: "runner-output" };
4780
+
4781
+ // src/lib/release.ts
4782
+ import fs22 from "fs";
4783
+ import path31 from "path";
4784
+ import { execFileSync as execFileSync6 } from "child_process";
4785
+ import { fileURLToPath as fileURLToPath2 } from "url";
4786
+ function findPackageJson() {
4787
+ let dir = path31.dirname(fileURLToPath2(import.meta.url));
4788
+ for (let i = 0; i < 6; i++) {
4789
+ const candidate = path31.join(dir, "package.json");
4790
+ if (fs22.existsSync(candidate)) return candidate;
4791
+ dir = path31.dirname(dir);
4792
+ }
4793
+ throw new Error("No se encontr\xF3 package.json cerca del CLI.");
4290
4794
  }
4291
- function managedRunnerWasBlocked(value) {
4292
- return "kind" in value;
4795
+ function readPackageVersion() {
4796
+ return readPackageMetadata().version;
4293
4797
  }
4294
- function runPiPackageManagedOperation(input, deps) {
4295
- if (input.engramBin === null && input.operation !== "uninstall") {
4296
- return {
4297
- kind: "blocked",
4298
- reason: "engram-missing",
4299
- remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
4300
- };
4301
- }
4302
- const owned = validateOwnedOperationState(input);
4303
- if (operationWasBlocked(owned)) return owned;
4304
- if (input.operation === "doctor") {
4305
- if (!sameRecord(owned.receipt.candidate, {
4306
- package: input.registry.candidate.package,
4307
- tarball: input.registry.candidate.tarball,
4308
- provenance: input.registry.candidate.provenance
4309
- })) {
4310
- return { kind: "blocked", reason: "source-divergent" };
4311
- }
4312
- const doctor = runManagedRunner(input, deps, "doctor");
4313
- if (managedRunnerWasBlocked(doctor)) return doctor;
4314
- const result = doctor.result;
4315
- return result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true ? { kind: "healthy" } : { kind: "blocked", reason: "runner-unhealthy" };
4316
- }
4317
- if (input.operation === "uninstall") {
4318
- if (!sameRecord(owned.receipt.candidate, {
4319
- package: input.registry.candidate.package,
4320
- tarball: input.registry.candidate.tarball,
4321
- provenance: input.registry.candidate.provenance
4322
- })) {
4323
- return { kind: "blocked", reason: "source-divergent" };
4324
- }
4325
- const cleanup = runManagedRunner(input, deps, "cleanup");
4326
- if (managedRunnerWasBlocked(cleanup)) return cleanup;
4327
- deps.backupSettings();
4328
- const removed = deps.run({
4329
- executable: input.detected.executable,
4330
- args: ["remove", owned.source, "--no-approve"],
4331
- environment: input.paths.environment
4332
- });
4333
- if (removed.exitCode !== 0 || removed.stderr !== "") return { kind: "blocked", reason: "remove-failed" };
4334
- if (!deps.isPackageAbsent()) return { kind: "blocked", reason: "absence-unverified" };
4335
- deps.deleteReceipt();
4336
- return { kind: "uninstalled" };
4798
+ function readPackageMetadata() {
4799
+ const packageJson = findPackageJson();
4800
+ const raw = fs22.readFileSync(packageJson, "utf8");
4801
+ const parsed = JSON.parse(raw);
4802
+ const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
4803
+ const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
4804
+ if (name === "" || version === "") {
4805
+ throw new Error("package.json no expone nombre o versi\xF3n v\xE1lidos.");
4337
4806
  }
4338
- const nextSource = input.registry.candidate.package.source;
4339
- if (nextSource === owned.source) return { kind: "healthy" };
4340
- return {
4341
- kind: "blocked",
4342
- reason: "verified-update-required",
4343
- remedy: "A cross-version Pi update requires verified replacement and rollback tgz artifacts."
4344
- };
4807
+ return { name, version };
4345
4808
  }
4346
4809
 
4347
4810
  // src/lib/pi-runtime.ts
4811
+ import os5 from "os";
4812
+ import path32 from "path";
4813
+ import fs23 from "fs";
4814
+ import { spawnSync } from "child_process";
4815
+ import { createHash } from "crypto";
4348
4816
  var PI_RUNTIME_CANDIDATE = {
4349
4817
  package: {
4350
4818
  name: "jorgex-pi",
4351
- version: "0.3.0",
4352
- source: "npm:jorgex-pi@0.3.0"
4819
+ version: "0.4.0",
4820
+ source: "npm:jorgex-pi@0.4.0"
4353
4821
  },
4354
4822
  provenance: {
4355
- commit: "cc8c66f1254e3f7a7c7f4679a30e7a0c7627498c"
4823
+ commit: "40abf9a0f7139e54da25344699c907cbdd859e27"
4356
4824
  },
4357
4825
  tarball: {
4358
- bytes: 89104529,
4359
- sha256: "13919b9aaed407e4e08c774cd24a496d3befbd91de6aafd37725fd7263963a3b",
4360
- sha512: "85c9adf038e8a0e826009fc8cffe23006688c184a43602d81e29807516073e604b0e451bd8f6883f1d352fb858d232acd593d72af67689b8ef5f7467f17fc096"
4826
+ bytes: 89116913,
4827
+ sha256: "6a984cf270dc204e1ccec15bcccd52b9a9eac168be68e404ca588bd54d924cd1",
4828
+ sha512: "8ec9424cb5bb313b9a29b30d39f84ab32af3004039f077573d3344c2d9564556679a370919912e1fb969780d989d56e5ccea0d06bd5056c00b67b248a68de661"
4361
4829
  },
4362
4830
  pi: {
4363
4831
  testedVersions: ["0.84.2"]
@@ -4366,7 +4834,7 @@ var PI_RUNTIME_CANDIDATE = {
4366
4834
  schemaVersion: 1,
4367
4835
  capabilities: [
4368
4836
  "foundation-contract-v1",
4369
- "stack-snapshot-v1",
4837
+ "stack-snapshot-v2",
4370
4838
  "runtime-agents-v1",
4371
4839
  "permission-gated-tools-v1",
4372
4840
  "structured-questions-v1",
@@ -4482,7 +4950,7 @@ function normalizeInstalledSource(settingsJson, alias, canonical) {
4482
4950
  if (!Array.isArray(packages)) return null;
4483
4951
  const hasCanonical = packages.some((entry) => entry === canonical || entry !== null && typeof entry === "object" && !Array.isArray(entry) && Reflect.get(entry, "source") === canonical);
4484
4952
  if (packages.filter((entry) => entry === alias).length !== 1 || hasCanonical) return null;
4485
- Reflect.set(parsed, "packages", packages.map((entry) => entry === alias ? { source: canonical, skills: [] } : entry));
4953
+ Reflect.set(parsed, "packages", packages.map((entry) => entry === alias ? canonical : entry));
4486
4954
  return JSON.stringify(parsed);
4487
4955
  } catch {
4488
4956
  return null;
@@ -4495,14 +4963,14 @@ function healthyDoctor(stdout, stderr, packageRunner, candidate) {
4495
4963
  if (record === null || typeof record !== "object" || Array.isArray(record)) return false;
4496
4964
  const packageValue = Reflect.get(record, "package");
4497
4965
  const result = Reflect.get(record, "result");
4498
- return Reflect.get(record, "schemaVersion") === 1 && Reflect.get(record, "command") === "doctor" && Reflect.get(record, "ok") === true && packageValue !== null && typeof packageValue === "object" && Reflect.get(packageValue, "name") === "jorgex-pi" && Reflect.get(packageValue, "version") === (candidate.package?.version ?? /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source)?.[1]) && path30.resolve(packageRunner) === path30.resolve(String(Reflect.get(packageValue, "root")), "bin", "jorgex-pi.mjs") && result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true;
4966
+ return Reflect.get(record, "schemaVersion") === 1 && Reflect.get(record, "command") === "doctor" && Reflect.get(record, "ok") === true && packageValue !== null && typeof packageValue === "object" && Reflect.get(packageValue, "name") === "jorgex-pi" && Reflect.get(packageValue, "version") === (candidate.package?.version ?? /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source)?.[1]) && path32.resolve(packageRunner) === path32.resolve(String(Reflect.get(packageValue, "root")), "bin", "jorgex-pi.mjs") && result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true;
4499
4967
  } catch {
4500
4968
  return false;
4501
4969
  }
4502
4970
  }
4503
4971
  function installPiFromVerifiedTarball(input, deps) {
4504
4972
  const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.piExecutable) : targetPaths(input.targetDir, input.engramBin, input.piExecutable);
4505
- const destination = input.targetDir === void 0 ? path30.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path30.join(path30.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
4973
+ const destination = input.targetDir === void 0 ? path32.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path32.join(path32.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
4506
4974
  const artifact = deps.download(destination);
4507
4975
  if (artifact.bytes !== input.candidate.bytes || artifact.sha256 !== input.candidate.sha256 || artifact.sha512 !== input.candidate.sha512) {
4508
4976
  return { kind: "blocked", reason: "tarball-integrity" };
@@ -4510,7 +4978,7 @@ function installPiFromVerifiedTarball(input, deps) {
4510
4978
  deps.backupSettings();
4511
4979
  const scope = {
4512
4980
  kind: input.targetDir === void 0 ? "real" : "target-dir",
4513
- codingAgentDir: path30.resolve(paths.codingAgentDir)
4981
+ codingAgentDir: path32.resolve(paths.codingAgentDir)
4514
4982
  };
4515
4983
  const installing = flatCandidateReceipt(input.candidate, scope, "installing", input.engramBin);
4516
4984
  deps.writeReceiptAtomic(`${JSON.stringify(installing)}
@@ -4539,30 +5007,30 @@ function installPiFromVerifiedTarball(input, deps) {
4539
5007
  return { kind: "installed", receipt };
4540
5008
  }
4541
5009
  function runtimePath(piExecutable) {
4542
- const entries = process.platform === "win32" ? [piExecutable === void 0 ? null : path30.dirname(piExecutable), path30.dirname(process.execPath), process.env.SystemRoot ? path30.join(process.env.SystemRoot, "System32") : null] : [piExecutable === void 0 ? null : path30.dirname(piExecutable), path30.dirname(process.execPath), "/usr/local/bin", "/usr/bin", "/bin"];
4543
- return [...new Set(entries.filter((entry) => entry !== null))].join(path30.delimiter);
5010
+ const entries = process.platform === "win32" ? [piExecutable === void 0 ? null : path32.dirname(piExecutable), path32.dirname(process.execPath), process.env.SystemRoot ? path32.join(process.env.SystemRoot, "System32") : null] : [piExecutable === void 0 ? null : path32.dirname(piExecutable), path32.dirname(process.execPath), "/usr/local/bin", "/usr/bin", "/bin"];
5011
+ return [...new Set(entries.filter((entry) => entry !== null))].join(path32.delimiter);
4544
5012
  }
4545
5013
  function targetPaths(targetDir, engramBin, piExecutable) {
4546
- const root = path30.resolve(targetDir);
4547
- const codingAgentDir = path30.join(root, "pi-agent");
4548
- const home = path30.join(root, "home");
4549
- const temporary = path30.join(root, "tmp");
5014
+ const root = path32.resolve(targetDir);
5015
+ const codingAgentDir = path32.join(root, "pi-agent");
5016
+ const home = path32.join(root, "home");
5017
+ const temporary = path32.join(root, "tmp");
4550
5018
  return {
4551
5019
  codingAgentDir,
4552
- receiptPath: path30.join(root, "state", "pi-receipt.json"),
4553
- packageRunner: path30.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
5020
+ receiptPath: path32.join(root, "state", "pi-receipt.json"),
5021
+ packageRunner: path32.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
4554
5022
  environment: {
4555
5023
  HOME: home,
4556
5024
  USERPROFILE: home,
4557
- APPDATA: path30.join(root, "appdata"),
4558
- LOCALAPPDATA: path30.join(root, "localappdata"),
4559
- XDG_CONFIG_HOME: path30.join(root, "xdg-config"),
4560
- XDG_DATA_HOME: path30.join(root, "xdg-data"),
4561
- XDG_CACHE_HOME: path30.join(root, "xdg-cache"),
5025
+ APPDATA: path32.join(root, "appdata"),
5026
+ LOCALAPPDATA: path32.join(root, "localappdata"),
5027
+ XDG_CONFIG_HOME: path32.join(root, "xdg-config"),
5028
+ XDG_DATA_HOME: path32.join(root, "xdg-data"),
5029
+ XDG_CACHE_HOME: path32.join(root, "xdg-cache"),
4562
5030
  TEMP: temporary,
4563
5031
  TMP: temporary,
4564
5032
  TMPDIR: temporary,
4565
- npm_config_cache: path30.join(root, "npm-cache"),
5033
+ npm_config_cache: path32.join(root, "npm-cache"),
4566
5034
  NPM_CONFIG_IGNORE_SCRIPTS: "true",
4567
5035
  NPM_CONFIG_UPDATE_NOTIFIER: "false",
4568
5036
  PI_CODING_AGENT_DIR: codingAgentDir,
@@ -4573,15 +5041,15 @@ function targetPaths(targetDir, engramBin, piExecutable) {
4573
5041
  }
4574
5042
  function userPaths(engramBin, piExecutable) {
4575
5043
  const home = os5.homedir();
4576
- const codingAgentDir = process.env.PI_CODING_AGENT_DIR ?? path30.join(home, ".pi", "agent");
5044
+ const codingAgentDir = process.env.PI_CODING_AGENT_DIR ?? path32.join(home, ".pi", "agent");
4577
5045
  return {
4578
5046
  codingAgentDir,
4579
- receiptPath: path30.join(dataDir(), "pi-receipt.json"),
4580
- packageRunner: path30.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
5047
+ receiptPath: path32.join(dataDir(), "pi-receipt.json"),
5048
+ packageRunner: path32.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
4581
5049
  environment: {
4582
5050
  HOME: home,
4583
- XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path30.join(home, ".config"),
4584
- XDG_CACHE_HOME: process.env.XDG_CACHE_HOME ?? path30.join(home, ".cache"),
5051
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path32.join(home, ".config"),
5052
+ XDG_CACHE_HOME: process.env.XDG_CACHE_HOME ?? path32.join(home, ".cache"),
4585
5053
  TMPDIR: os5.tmpdir(),
4586
5054
  NPM_CONFIG_IGNORE_SCRIPTS: "true",
4587
5055
  NPM_CONFIG_UPDATE_NOTIFIER: "false",
@@ -4609,7 +5077,7 @@ function runPiRuntime(input, deps) {
4609
5077
  return { kind: "blocked", reason: "tarball-integrity" };
4610
5078
  }
4611
5079
  const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.detected.executable) : targetPaths(input.targetDir, input.engramBin, input.detected.executable);
4612
- const settingsJson = deps.readSettings(path30.join(paths.codingAgentDir, "settings.json"));
5080
+ const settingsJson = deps.readSettings(path32.join(paths.codingAgentDir, "settings.json"));
4613
5081
  const receiptJson = deps.readReceipt(paths.receiptPath);
4614
5082
  const lifecycleInput = {
4615
5083
  candidate: PI_RUNTIME_CANDIDATE,
@@ -4664,19 +5132,19 @@ function runPiRuntime(input, deps) {
4664
5132
  return result;
4665
5133
  }
4666
5134
  function readJsonFile(file) {
4667
- return JSON.parse(fs22.readFileSync(file, "utf8"));
5135
+ return JSON.parse(fs23.readFileSync(file, "utf8"));
4668
5136
  }
4669
5137
  function packageVersionFromExecutable(executable) {
4670
5138
  let current;
4671
5139
  try {
4672
- current = path30.dirname(fs22.realpathSync(executable));
5140
+ current = path32.dirname(fs23.realpathSync(executable));
4673
5141
  } catch {
4674
5142
  return null;
4675
5143
  }
4676
5144
  for (let depth = 0; depth < 8; depth++) {
4677
5145
  const manifests = [
4678
- path30.join(current, "package.json"),
4679
- path30.join(current, "node_modules", "@earendil-works", "pi-coding-agent", "package.json")
5146
+ path32.join(current, "package.json"),
5147
+ path32.join(current, "node_modules", "@earendil-works", "pi-coding-agent", "package.json")
4680
5148
  ];
4681
5149
  for (const manifest of manifests) {
4682
5150
  try {
@@ -4687,7 +5155,7 @@ function packageVersionFromExecutable(executable) {
4687
5155
  } catch {
4688
5156
  }
4689
5157
  }
4690
- const parent = path30.dirname(current);
5158
+ const parent = path32.dirname(current);
4691
5159
  if (parent === current) break;
4692
5160
  current = parent;
4693
5161
  }
@@ -4702,35 +5170,35 @@ function detectPiRuntime() {
4702
5170
  installed: executable !== null,
4703
5171
  executable,
4704
5172
  version: executable === null ? null : packageVersionFromExecutable(executable),
4705
- codingAgentDir: process.env.PI_CODING_AGENT_DIR ?? path30.join(home, ".pi", "agent")
5173
+ codingAgentDir: process.env.PI_CODING_AGENT_DIR ?? path32.join(home, ".pi", "agent")
4706
5174
  };
4707
5175
  }
4708
5176
  function hasManagedPiRuntime(targetDir) {
4709
- const receipt = targetDir === void 0 ? path30.join(dataDir(), "pi-receipt.json") : path30.join(path30.resolve(targetDir), "state", "pi-receipt.json");
4710
- return fs22.statSync(receipt, { throwIfNoEntry: false })?.isFile() === true;
5177
+ const receipt = targetDir === void 0 ? path32.join(dataDir(), "pi-receipt.json") : path32.join(path32.resolve(targetDir), "state", "pi-receipt.json");
5178
+ return fs23.statSync(receipt, { throwIfNoEntry: false })?.isFile() === true;
4711
5179
  }
4712
5180
  function resolvePiEngramBin(targetDir) {
4713
5181
  if (targetDir === void 0) return detectEngram();
4714
- const candidate = path30.join(path30.resolve(targetDir), "bin", process.platform === "win32" ? "engram.exe" : "engram");
4715
- return fs22.statSync(candidate, { throwIfNoEntry: false })?.isFile() ? candidate : null;
5182
+ const candidate = path32.join(path32.resolve(targetDir), "bin", process.platform === "win32" ? "engram.exe" : "engram");
5183
+ return fs23.statSync(candidate, { throwIfNoEntry: false })?.isFile() ? candidate : null;
4716
5184
  }
4717
5185
  function readOptional(file, fallback) {
4718
5186
  try {
4719
- return fs22.readFileSync(file, "utf8");
5187
+ return fs23.readFileSync(file, "utf8");
4720
5188
  } catch (error) {
4721
5189
  if (error.code === "ENOENT") return fallback;
4722
5190
  throw error;
4723
5191
  }
4724
5192
  }
4725
5193
  function hashPiTarball(file) {
4726
- const descriptor = fs22.openSync(file, "r");
5194
+ const descriptor = fs23.openSync(file, "r");
4727
5195
  const sha256 = createHash("sha256");
4728
5196
  const sha512 = createHash("sha512");
4729
5197
  const buffer = Buffer.allocUnsafe(1024 * 1024);
4730
5198
  let bytes = 0;
4731
5199
  try {
4732
5200
  while (true) {
4733
- const read = fs22.readSync(descriptor, buffer, 0, buffer.length, null);
5201
+ const read = fs23.readSync(descriptor, buffer, 0, buffer.length, null);
4734
5202
  if (read === 0) break;
4735
5203
  bytes += read;
4736
5204
  const chunk = buffer.subarray(0, read);
@@ -4738,27 +5206,27 @@ function hashPiTarball(file) {
4738
5206
  sha512.update(chunk);
4739
5207
  }
4740
5208
  } finally {
4741
- fs22.closeSync(descriptor);
5209
+ fs23.closeSync(descriptor);
4742
5210
  }
4743
5211
  return { path: file, bytes, sha256: sha256.digest("hex"), sha512: sha512.digest("hex") };
4744
5212
  }
4745
5213
  async function acquirePiTarball(destination) {
4746
- const existing = fs22.statSync(destination, { throwIfNoEntry: false });
5214
+ const existing = fs23.statSync(destination, { throwIfNoEntry: false });
4747
5215
  if (existing?.isFile()) {
4748
5216
  const observed = hashPiTarball(destination);
4749
5217
  if (observed.bytes === PI_RUNTIME_CANDIDATE.tarball.bytes && observed.sha256 === PI_RUNTIME_CANDIDATE.tarball.sha256 && observed.sha512 === PI_RUNTIME_CANDIDATE.tarball.sha512) {
4750
5218
  return observed;
4751
5219
  }
4752
- fs22.rmSync(destination, { force: true });
5220
+ fs23.rmSync(destination, { force: true });
4753
5221
  }
4754
- fs22.mkdirSync(path30.dirname(destination), { recursive: true });
5222
+ fs23.mkdirSync(path32.dirname(destination), { recursive: true });
4755
5223
  const partial = `${destination}.partial-${process.pid}`;
4756
5224
  const response = await fetch(`https://registry.npmjs.org/jorgex-pi/-/jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`, {
4757
5225
  redirect: "error",
4758
5226
  headers: { accept: "application/octet-stream" }
4759
5227
  });
4760
5228
  if (!response.ok || response.body === null) throw new Error(`No se pudo descargar jorgex-pi@${PI_RUNTIME_CANDIDATE.package.version} (${response.status}).`);
4761
- const descriptor = fs22.openSync(partial, "wx", 384);
5229
+ const descriptor = fs23.openSync(partial, "wx", 384);
4762
5230
  let bytes = 0;
4763
5231
  try {
4764
5232
  for await (const chunk of response.body) {
@@ -4766,15 +5234,15 @@ async function acquirePiTarball(destination) {
4766
5234
  bytes += buffer.length;
4767
5235
  if (bytes > PI_RUNTIME_CANDIDATE.tarball.bytes) throw new Error("El tarball de jorgex-pi excede el tama\xF1o fijado.");
4768
5236
  let offset = 0;
4769
- while (offset < buffer.length) offset += fs22.writeSync(descriptor, buffer, offset);
5237
+ while (offset < buffer.length) offset += fs23.writeSync(descriptor, buffer, offset);
4770
5238
  }
4771
5239
  } catch (error) {
4772
- fs22.closeSync(descriptor);
4773
- fs22.rmSync(partial, { force: true });
5240
+ fs23.closeSync(descriptor);
5241
+ fs23.rmSync(partial, { force: true });
4774
5242
  throw error;
4775
5243
  }
4776
- fs22.closeSync(descriptor);
4777
- fs22.renameSync(partial, destination);
5244
+ fs23.closeSync(descriptor);
5245
+ fs23.renameSync(partial, destination);
4778
5246
  return hashPiTarball(destination);
4779
5247
  }
4780
5248
  function runProcess(invocation) {
@@ -4824,7 +5292,7 @@ async function runPiRuntimeSystem(input) {
4824
5292
  remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
4825
5293
  };
4826
5294
  }
4827
- const destination = input.targetDir === void 0 ? path30.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path30.join(path30.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
5295
+ const destination = input.targetDir === void 0 ? path32.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path32.join(path32.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
4828
5296
  let artifact;
4829
5297
  try {
4830
5298
  artifact = await acquirePiTarball(destination);
@@ -4844,18 +5312,18 @@ async function runPiRuntimeSystem(input) {
4844
5312
  }, {
4845
5313
  download: () => artifact,
4846
5314
  backupSettings: () => createBackup(
4847
- [path30.join(paths.codingAgentDir, "settings.json")],
5315
+ [path32.join(paths.codingAgentDir, "settings.json")],
4848
5316
  "pi-package-install",
4849
- input.targetDir === void 0 ? void 0 : path30.join(path30.resolve(input.targetDir), "backups")
5317
+ input.targetDir === void 0 ? void 0 : path32.join(path32.resolve(input.targetDir), "backups")
4850
5318
  ),
4851
5319
  run: runProcess,
4852
- readSettings: () => readOptional(path30.join(paths.codingAgentDir, "settings.json"), '{"packages":[]}'),
4853
- rewriteSettings: (content) => writeText(path30.join(paths.codingAgentDir, "settings.json"), `${content}
5320
+ readSettings: () => readOptional(path32.join(paths.codingAgentDir, "settings.json"), '{"packages":[]}'),
5321
+ rewriteSettings: (content) => writeText(path32.join(paths.codingAgentDir, "settings.json"), `${content}
4854
5322
  `),
4855
5323
  writeReceiptAtomic: (content) => writeText(paths.receiptPath, content)
4856
5324
  });
4857
5325
  }
4858
- const packageRoot = path30.dirname(path30.dirname(paths.packageRunner));
5326
+ const packageRoot = path32.dirname(path32.dirname(paths.packageRunner));
4859
5327
  const writeReceipt = (receipt) => {
4860
5328
  writeText(paths.receiptPath, `${JSON.stringify(receipt, null, 2)}
4861
5329
  `);
@@ -4873,19 +5341,108 @@ async function runPiRuntimeSystem(input) {
4873
5341
  value,
4874
5342
  {
4875
5343
  backupSettings: () => createBackup(
4876
- [path30.join(paths.codingAgentDir, "settings.json")],
5344
+ [path32.join(paths.codingAgentDir, "settings.json")],
4877
5345
  "pi-package-uninstall",
4878
- input.targetDir === void 0 ? void 0 : path30.join(path30.resolve(input.targetDir), "backups")
5346
+ input.targetDir === void 0 ? void 0 : path32.join(path32.resolve(input.targetDir), "backups")
4879
5347
  ),
4880
5348
  run: runProcess,
4881
- isPackageAbsent: () => !fs22.existsSync(packageRoot),
4882
- deleteReceipt: () => fs22.rmSync(paths.receiptPath, { force: true })
5349
+ isPackageAbsent: () => !fs23.existsSync(packageRoot),
5350
+ deleteReceipt: () => fs23.rmSync(paths.receiptPath, { force: true })
4883
5351
  }
4884
5352
  )
4885
5353
  };
4886
5354
  return runPiRuntime(input, deps);
4887
5355
  }
4888
5356
 
5357
+ // src/lib/pi-managed-runtime.ts
5358
+ function projectionOperation(operation) {
5359
+ return operation === "update" ? "sync" : operation;
5360
+ }
5361
+ function manualExistingResult(packageResult) {
5362
+ return packageResult.remedy === void 0 ? { kind: "blocked", reason: "manual-existing" } : { kind: "blocked", reason: "manual-existing", remedy: packageResult.remedy };
5363
+ }
5364
+ async function completeProjection(operation, packageResult, deps) {
5365
+ const projectionResult = await deps.runProjection(operation);
5366
+ if (projectionResult.kind === "drift") {
5367
+ return {
5368
+ kind: "blocked",
5369
+ reason: "projection-drift",
5370
+ paths: projectionResult.paths,
5371
+ remedy: projectionResult.remedy
5372
+ };
5373
+ }
5374
+ return projectionResult.kind === "blocked" ? projectionResult : packageResult;
5375
+ }
5376
+ async function runManagedPiOperation(operation, deps) {
5377
+ if (operation === "uninstall") {
5378
+ const preparation = await deps.prepareProjectionUninstall();
5379
+ if (preparation.kind === "blocked") return preparation;
5380
+ const packageResult2 = await deps.runPackage(operation);
5381
+ if (packageResult2.kind === "manual-existing") return manualExistingResult(packageResult2);
5382
+ if (packageResult2.kind === "blocked") return packageResult2;
5383
+ return deps.completeProjectionUninstall(preparation.token);
5384
+ }
5385
+ const packageResult = await deps.runPackage(operation);
5386
+ if (packageResult.kind === "manual-existing") return manualExistingResult(packageResult);
5387
+ if (operation === "models") return packageResult;
5388
+ const nextProjectionOperation = projectionOperation(operation);
5389
+ if (packageResult.kind !== "blocked") {
5390
+ return completeProjection(nextProjectionOperation, packageResult, deps);
5391
+ }
5392
+ if (packageResult.reason !== "source-divergent" || operation !== "sync" && operation !== "update") {
5393
+ return packageResult;
5394
+ }
5395
+ const recoveryProjection = await deps.runProjection("sync");
5396
+ if (recoveryProjection.kind === "blocked") return recoveryProjection;
5397
+ if (recoveryProjection.kind !== "synced" || !recoveryProjection.changed) return packageResult;
5398
+ const retryResult = await deps.runPackage(operation);
5399
+ if (retryResult.kind === "manual-existing") return manualExistingResult(retryResult);
5400
+ if (retryResult.kind === "blocked") return retryResult;
5401
+ return completeProjection("sync", retryResult, deps);
5402
+ }
5403
+ function managedPackageResult(result) {
5404
+ if (result.kind === "manual-existing") {
5405
+ return {
5406
+ kind: "manual-existing",
5407
+ remedy: result.remedy ?? "Pi ya est\xE1 configurado manualmente; conserva esa configuraci\xF3n o elim\xEDnala antes de ejecutar sync --agents pi."
5408
+ };
5409
+ }
5410
+ if (result.kind === "models") return { kind: "models", models: result.models };
5411
+ return result;
5412
+ }
5413
+ async function runManagedPiSystem(input) {
5414
+ const playwrightCliEnabled = input.targetDir === void 0 && loadPlaywrightCliPreference() === true;
5415
+ const projectionInput = {
5416
+ targetDir: input.targetDir,
5417
+ packageSource: PI_RUNTIME_CANDIDATE.package.source,
5418
+ engramBin: input.engramBin,
5419
+ playwrightCliEnabled
5420
+ };
5421
+ return runManagedPiOperation(input.operation, {
5422
+ async runPackage(operation) {
5423
+ return managedPackageResult(await runPiRuntimeSystem({ ...input, operation }));
5424
+ },
5425
+ runProjection(operation) {
5426
+ const result = runPiProjectionLifecycleSystem({
5427
+ operation,
5428
+ ...projectionInput
5429
+ });
5430
+ return Promise.resolve(result.kind === "drift" ? {
5431
+ kind: "drift",
5432
+ paths: result.paths,
5433
+ remedy: "Ejecuta sync --agents pi para reparar la proyecci\xF3n de Pi."
5434
+ } : result);
5435
+ },
5436
+ prepareProjectionUninstall() {
5437
+ const result = preparePiProjectionUninstallSystem({ operation: "uninstall", ...projectionInput });
5438
+ return Promise.resolve(result.kind === "prepared" ? { kind: "prepared", token: result.plan } : result);
5439
+ },
5440
+ completeProjectionUninstall(token) {
5441
+ return Promise.resolve(completePiProjectionUninstallSystem(token, { operation: "uninstall", ...projectionInput }));
5442
+ }
5443
+ });
5444
+ }
5445
+
4889
5446
  // src/cli.ts
4890
5447
  var VERSION = readPackageVersion();
4891
5448
  var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall"];
@@ -5105,6 +5662,13 @@ async function resolveRuntimes(flags, includeAvailablePi = false) {
5105
5662
  return choice;
5106
5663
  }
5107
5664
  async function runSelectedPi(operation, targetDir, yes = false) {
5665
+ if (targetDir === void 0 && operation !== "models") {
5666
+ const preferenceErrors = browserPreferenceErrors();
5667
+ if (preferenceErrors.length > 0) {
5668
+ for (const error of preferenceErrors) console.error(error);
5669
+ return 1;
5670
+ }
5671
+ }
5108
5672
  const detected = detectPiRuntime();
5109
5673
  if (!detected.installed || detected.executable === null) {
5110
5674
  console.error("Pi no detectado. Instala el runtime Pi antes de gestionar jorgex-pi.");
@@ -5135,14 +5699,15 @@ async function runSelectedPi(operation, targetDir, yes = false) {
5135
5699
  }
5136
5700
  engramBin = requirement.bin;
5137
5701
  }
5138
- const result = await runPiRuntimeSystem({
5702
+ const result = await runManagedPiSystem({
5139
5703
  operation,
5140
5704
  targetDir,
5141
5705
  detected: { executable: detected.executable, version: detected.version },
5142
5706
  engramBin
5143
5707
  });
5144
5708
  if (result.kind === "blocked") {
5145
- console.error(`Pi: ${result.reason ?? "operaci\xF3n bloqueada"}${result.remedy ? `. ${result.remedy}` : ""}`);
5709
+ const paths = "paths" in result ? `: ${result.paths.join(", ")}` : "";
5710
+ console.error(`Pi: ${result.reason ?? "operaci\xF3n bloqueada"}${paths}${result.remedy ? `. ${result.remedy}` : ""}`);
5146
5711
  return 1;
5147
5712
  }
5148
5713
  if (result.kind === "models" && result.models !== void 0) console.log(JSON.stringify(result.models));
@@ -5244,7 +5809,26 @@ Flags disponibles: jorgex-stack --help`
5244
5809
  devtoolsMcpSelection
5245
5810
  });
5246
5811
  }
5247
- if (runtimes.includes("pi")) {
5812
+ let piCanRun = true;
5813
+ if (command === "install" && fileRuntimes.length === 0 && runtimes.includes("pi")) {
5814
+ const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags);
5815
+ if (playwrightToolConsent === null) return;
5816
+ if (resolvePlaywrightToolPlan(playwrightToolConsent).actions.length > 0) {
5817
+ if (flags.dryRun) {
5818
+ p6.log.info("Playwright CLI: instalaci\xF3n global y navegador previstos (dry-run; no se ejecutan).");
5819
+ } else {
5820
+ exitCode = await runInstall({
5821
+ runtimes: [],
5822
+ targetDir: flags.targetDir,
5823
+ dryRun: false,
5824
+ yes: flags.yes,
5825
+ playwrightToolConsent
5826
+ });
5827
+ piCanRun = exitCode === 0;
5828
+ }
5829
+ }
5830
+ }
5831
+ if (runtimes.includes("pi") && piCanRun) {
5248
5832
  if (flags.dryRun) p6.log.info(`Pi: ${command} previsto; dry-run no ejecuta subprocess ni escribe receipt.`);
5249
5833
  else exitCode = Math.max(exitCode, await runSelectedPi(command, flags.targetDir, flags.yes));
5250
5834
  }