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/README.md +7 -5
- package/dist/cli.js +2335 -1751
- package/package.json +1 -1
- package/stack/contracts/quality-receipt.v1.schema.json +164 -0
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
|
-
|
|
2448
|
-
|
|
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
|
-
|
|
2451
|
-
|
|
2513
|
+
kind: "blocked",
|
|
2514
|
+
reason,
|
|
2515
|
+
receiptPath: input.scope.receiptPath,
|
|
2516
|
+
ownership: ownership(input.receiptJson !== null)
|
|
2452
2517
|
};
|
|
2453
2518
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
const
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
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
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
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 (
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
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
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
const
|
|
2513
|
-
|
|
2514
|
-
const
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
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
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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 (
|
|
2558
|
-
|
|
2559
|
-
|
|
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
|
-
|
|
2563
|
-
|
|
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
|
|
2607
|
-
|
|
2608
|
-
if (
|
|
2609
|
-
|
|
2610
|
-
|
|
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
|
|
2618
|
-
|
|
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
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
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
|
-
|
|
2642
|
-
|
|
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 (
|
|
2648
|
-
const
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
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
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
if (
|
|
2666
|
-
|
|
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
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
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
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
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
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
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
|
-
|
|
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
|
|
2787
|
-
|
|
2788
|
-
|
|
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
|
-
|
|
2795
|
-
return data.tag_name?.replace(/^v/, "") ?? null;
|
|
2702
|
+
return record;
|
|
2796
2703
|
} catch {
|
|
2797
2704
|
return null;
|
|
2798
2705
|
}
|
|
2799
2706
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
|
2844
|
-
|
|
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
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
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
|
-
|
|
2889
|
-
|
|
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
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
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
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
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
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
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
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
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
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
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
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
const
|
|
3002
|
-
if (
|
|
3003
|
-
|
|
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 (
|
|
3006
|
-
|
|
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
|
-
|
|
3009
|
-
|
|
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
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
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
|
-
|
|
3041
|
-
writeText(upstreamsFile, JSON.stringify(data, null, 2) + "\n");
|
|
2796
|
+
return { receipt, source };
|
|
3042
2797
|
}
|
|
3043
|
-
|
|
3044
|
-
|
|
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
|
|
3049
|
-
const
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
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
|
-
|
|
3075
|
-
|
|
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
|
|
3087
|
-
if (input.
|
|
3088
|
-
if (input.cli.status === "current") {
|
|
2820
|
+
function runPiPackageManagedOperation(input, deps) {
|
|
2821
|
+
if (input.engramBin === null && input.operation !== "uninstall") {
|
|
3089
2822
|
return {
|
|
3090
|
-
|
|
3091
|
-
|
|
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
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
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
|
|
3111
|
-
|
|
3112
|
-
if (
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
const
|
|
3123
|
-
|
|
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 (
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
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 (
|
|
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
|
|
3142
|
-
if (
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
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
|
-
|
|
3171
|
-
|
|
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
|
|
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
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
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
|
|
3200
|
-
|
|
2989
|
+
function expectedProjectionReceipt(input, scope) {
|
|
2990
|
+
const plan = projectionPlan(input, scope);
|
|
2991
|
+
assertPlanContained(plan, scope);
|
|
2992
|
+
return receiptFor(plan, scope);
|
|
3201
2993
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
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
|
|
3002
|
+
function readTextOnlyIfMissing(file) {
|
|
3209
3003
|
try {
|
|
3210
|
-
|
|
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
|
|
3215
|
-
const
|
|
3216
|
-
const
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
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
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
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
|
-
|
|
3232
|
-
|
|
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
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
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
|
|
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
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
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
|
-
|
|
3107
|
+
return { kind: "unreadable" };
|
|
3276
3108
|
}
|
|
3277
3109
|
}
|
|
3278
|
-
function
|
|
3279
|
-
if (
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
const
|
|
3283
|
-
const
|
|
3284
|
-
const
|
|
3285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
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
|
-
|
|
3303
|
-
if (
|
|
3304
|
-
|
|
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
|
|
3309
|
-
|
|
3310
|
-
|
|
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
|
-
|
|
3329
|
-
const brew = lookPath("brew");
|
|
3330
|
-
if (brew) {
|
|
3331
|
-
let brewManages = false;
|
|
3161
|
+
if (prepared.prompt !== null) {
|
|
3332
3162
|
try {
|
|
3333
|
-
|
|
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
|
|
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
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
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 (
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
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
|
|
3182
|
+
return { kind: "uninstalled" };
|
|
3407
3183
|
}
|
|
3408
|
-
function
|
|
3409
|
-
const
|
|
3410
|
-
if (
|
|
3411
|
-
|
|
3412
|
-
return
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
};
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
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
|
-
|
|
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
|
|
3440
|
-
const
|
|
3441
|
-
|
|
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
|
|
3459
|
-
|
|
3261
|
+
function completePiProjectionUninstallSystem(plan, input) {
|
|
3262
|
+
const lifecycle = systemProjectionLifecycle(input);
|
|
3263
|
+
return completePiProjectionUninstall(plan, lifecycle.deps);
|
|
3460
3264
|
}
|
|
3461
|
-
function
|
|
3462
|
-
|
|
3265
|
+
function runPiProjectionLifecycleSystem(input) {
|
|
3266
|
+
const lifecycle = systemProjectionLifecycle(input);
|
|
3267
|
+
return runPiProjectionLifecycle(lifecycle.input, lifecycle.deps);
|
|
3463
3268
|
}
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
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
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
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
|
-
|
|
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
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
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
|
-
|
|
3515
|
-
|
|
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
|
-
|
|
3535
|
-
if (
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
const
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
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
|
|
3550
|
-
|
|
3551
|
-
if (
|
|
3552
|
-
|
|
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
|
|
3558
|
-
const
|
|
3559
|
-
if (!
|
|
3560
|
-
|
|
3561
|
-
|
|
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
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
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
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
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
|
-
|
|
3587
|
-
|
|
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
|
|
3590
|
-
|
|
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 (
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
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
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
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
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
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
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
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
|
-
|
|
3661
|
-
cleanupTmp(tmpRoot);
|
|
3662
|
-
continue;
|
|
3476
|
+
p3.log.success(`Engram: ${version} (${engramBin})`);
|
|
3663
3477
|
}
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
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 (
|
|
3670
|
-
|
|
3671
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
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
|
-
|
|
3687
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
}
|
|
3715
|
-
if (
|
|
3716
|
-
|
|
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
|
-
|
|
3736
|
-
return
|
|
3572
|
+
p3.outro(problems === 0 ? "Todo sano." : `${problems} avisos \u2014 revisa arriba.`);
|
|
3573
|
+
return problems > 0 ? 1 : 0;
|
|
3737
3574
|
}
|
|
3738
3575
|
|
|
3739
|
-
// src/
|
|
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
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
var
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
"
|
|
3750
|
-
|
|
3751
|
-
"
|
|
3752
|
-
|
|
3753
|
-
];
|
|
3754
|
-
|
|
3755
|
-
|
|
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
|
|
3602
|
+
return cachedToken = null;
|
|
3768
3603
|
}
|
|
3769
|
-
function
|
|
3770
|
-
return
|
|
3604
|
+
function ghPresentButTokenFailed() {
|
|
3605
|
+
return ghTokenFailed;
|
|
3771
3606
|
}
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
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 (
|
|
3795
|
-
|
|
3796
|
-
|
|
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
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
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
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
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
|
-
|
|
3858
|
-
|
|
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
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
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
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
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
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
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
|
-
|
|
3930
|
-
|
|
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/
|
|
3944
|
-
import
|
|
3742
|
+
// src/lib/skill-update.ts
|
|
3743
|
+
import fs20 from "fs";
|
|
3945
3744
|
import path28 from "path";
|
|
3946
|
-
import { execFileSync as
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
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
|
|
3961
|
-
const
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
const
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
]
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
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
|
|
3995
|
-
|
|
3996
|
-
const
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4009
|
-
|
|
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
|
|
4012
|
-
|
|
4013
|
-
|
|
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
|
|
4020
|
-
|
|
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
|
|
4026
|
-
|
|
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
|
|
3912
|
+
async function latestNpmVersion(pkg) {
|
|
4029
3913
|
try {
|
|
4030
|
-
const
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
if (!
|
|
4034
|
-
const
|
|
4035
|
-
return
|
|
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
|
|
4041
|
-
if (
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
}
|
|
4046
|
-
|
|
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
|
-
|
|
4049
|
-
|
|
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
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
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
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
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
|
-
|
|
4079
|
-
|
|
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
|
-
|
|
4087
|
-
|
|
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
|
|
4096
|
-
|
|
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
|
-
|
|
4102
|
-
|
|
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
|
|
4105
|
-
|
|
4106
|
-
|
|
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
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
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
|
|
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
|
|
4172
|
-
if (
|
|
4173
|
-
const
|
|
4174
|
-
|
|
4175
|
-
|
|
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
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
}
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
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
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
if (
|
|
4209
|
-
|
|
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 (
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
if (
|
|
4221
|
-
|
|
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
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
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
|
-
|
|
4573
|
+
p4.outro(exitCode === 0 ? "Update completado." : "Update completado con errores (revisa arriba).");
|
|
4574
|
+
return { exitCode, appliedUpdates, syncRequired: resolveUpdateSyncRequired(updated) };
|
|
4230
4575
|
}
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
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
|
|
4239
|
-
const
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
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
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
const
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
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 (
|
|
4260
|
-
|
|
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 (
|
|
4263
|
-
|
|
4698
|
+
if (opts.runtimes.includes("codex")) {
|
|
4699
|
+
detections.push({ id: "codex", name: "Codex CLI", installed: detectCodex().installed, options: null });
|
|
4264
4700
|
}
|
|
4265
|
-
const
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
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
|
-
|
|
4772
|
+
writeText(file, JSON.stringify(map, null, 2) + "\n");
|
|
4773
|
+
p5.log.success(`Guardado en ${file}`);
|
|
4774
|
+
return 0;
|
|
4271
4775
|
}
|
|
4272
|
-
function
|
|
4273
|
-
|
|
4776
|
+
function cancelled() {
|
|
4777
|
+
p5.cancel("Cancelado \u2014 no se ha guardado nada.");
|
|
4778
|
+
return 1;
|
|
4274
4779
|
}
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
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
|
|
4292
|
-
return
|
|
4795
|
+
function readPackageVersion() {
|
|
4796
|
+
return readPackageMetadata().version;
|
|
4293
4797
|
}
|
|
4294
|
-
function
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
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
|
-
|
|
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.
|
|
4352
|
-
source: "npm:jorgex-pi@0.
|
|
4819
|
+
version: "0.4.0",
|
|
4820
|
+
source: "npm:jorgex-pi@0.4.0"
|
|
4353
4821
|
},
|
|
4354
4822
|
provenance: {
|
|
4355
|
-
commit: "
|
|
4823
|
+
commit: "40abf9a0f7139e54da25344699c907cbdd859e27"
|
|
4356
4824
|
},
|
|
4357
4825
|
tarball: {
|
|
4358
|
-
bytes:
|
|
4359
|
-
sha256: "
|
|
4360
|
-
sha512: "
|
|
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-
|
|
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 ?
|
|
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]) &&
|
|
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 ?
|
|
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:
|
|
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 :
|
|
4543
|
-
return [...new Set(entries.filter((entry) => entry !== null))].join(
|
|
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 =
|
|
4547
|
-
const codingAgentDir =
|
|
4548
|
-
const home =
|
|
4549
|
-
const temporary =
|
|
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:
|
|
4553
|
-
packageRunner:
|
|
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:
|
|
4558
|
-
LOCALAPPDATA:
|
|
4559
|
-
XDG_CONFIG_HOME:
|
|
4560
|
-
XDG_DATA_HOME:
|
|
4561
|
-
XDG_CACHE_HOME:
|
|
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:
|
|
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 ??
|
|
5044
|
+
const codingAgentDir = process.env.PI_CODING_AGENT_DIR ?? path32.join(home, ".pi", "agent");
|
|
4577
5045
|
return {
|
|
4578
5046
|
codingAgentDir,
|
|
4579
|
-
receiptPath:
|
|
4580
|
-
packageRunner:
|
|
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 ??
|
|
4584
|
-
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME ??
|
|
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(
|
|
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(
|
|
5135
|
+
return JSON.parse(fs23.readFileSync(file, "utf8"));
|
|
4668
5136
|
}
|
|
4669
5137
|
function packageVersionFromExecutable(executable) {
|
|
4670
5138
|
let current;
|
|
4671
5139
|
try {
|
|
4672
|
-
current =
|
|
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
|
-
|
|
4679
|
-
|
|
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 =
|
|
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 ??
|
|
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 ?
|
|
4710
|
-
return
|
|
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 =
|
|
4715
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
5220
|
+
fs23.rmSync(destination, { force: true });
|
|
4753
5221
|
}
|
|
4754
|
-
|
|
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 =
|
|
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 +=
|
|
5237
|
+
while (offset < buffer.length) offset += fs23.writeSync(descriptor, buffer, offset);
|
|
4770
5238
|
}
|
|
4771
5239
|
} catch (error) {
|
|
4772
|
-
|
|
4773
|
-
|
|
5240
|
+
fs23.closeSync(descriptor);
|
|
5241
|
+
fs23.rmSync(partial, { force: true });
|
|
4774
5242
|
throw error;
|
|
4775
5243
|
}
|
|
4776
|
-
|
|
4777
|
-
|
|
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 ?
|
|
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
|
-
[
|
|
5315
|
+
[path32.join(paths.codingAgentDir, "settings.json")],
|
|
4848
5316
|
"pi-package-install",
|
|
4849
|
-
input.targetDir === void 0 ? void 0 :
|
|
5317
|
+
input.targetDir === void 0 ? void 0 : path32.join(path32.resolve(input.targetDir), "backups")
|
|
4850
5318
|
),
|
|
4851
5319
|
run: runProcess,
|
|
4852
|
-
readSettings: () => readOptional(
|
|
4853
|
-
rewriteSettings: (content) => writeText(
|
|
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 =
|
|
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
|
-
[
|
|
5344
|
+
[path32.join(paths.codingAgentDir, "settings.json")],
|
|
4877
5345
|
"pi-package-uninstall",
|
|
4878
|
-
input.targetDir === void 0 ? void 0 :
|
|
5346
|
+
input.targetDir === void 0 ? void 0 : path32.join(path32.resolve(input.targetDir), "backups")
|
|
4879
5347
|
),
|
|
4880
5348
|
run: runProcess,
|
|
4881
|
-
isPackageAbsent: () => !
|
|
4882
|
-
deleteReceipt: () =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|