jorgex-stack 1.2.1 → 1.2.2

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.
Files changed (3) hide show
  1. package/README.md +20 -2
  2. package/dist/cli.js +981 -51
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # JorgeX Stack
2
2
 
3
- Portable multi-agent harness: one configuration source — 15 agents, 17 skills, hooks, persistent memory ([Engram](https://github.com/Gentleman-Programming/engram)), MCPs, and system prompt — installable with one command in **Claude Code**, **Codex CLI**, and **OpenCode**.
3
+ Portable multi-agent harness: one configuration source — 15 agents, 17 skills, hooks, persistent memory ([Engram](https://github.com/Gentleman-Programming/engram)), MCPs, and system prompt — installable with one command in **Claude Code**, **Codex CLI**, **OpenCode**, and **Pi**.
4
4
 
5
5
  > Inspired by [gentle-ai](https://github.com/Gentleman-Programming/gentle-ai), rebuilt for the JorgeX stack.
6
6
 
@@ -75,7 +75,7 @@ Flags:
75
75
  pnpm dlx jorgex-stack install --mode programmatic --subagent-concurrency serial --yes
76
76
  ```
77
77
 
78
- This installs into all detected runtimes. To be explicit, add `--agents opencode,claude-code,codex` or a comma-separated subset. Always pass `--mode programmatic`; without `--mode`, `--yes` and non-TTY installs default to `human`.
78
+ This installs into all detected runtimes. To be explicit, add `--agents opencode,claude-code,codex,pi` or a comma-separated subset. Always pass `--mode programmatic`; without `--mode`, `--yes` and non-TTY installs default to `human`.
79
79
 
80
80
  OpenCode also requires an existing selection in `~/.jorgex-stack/model-map.json`; run `pnpm dlx jorgex-stack models --agents opencode` interactively once before a headless install.
81
81
 
@@ -95,6 +95,24 @@ Programmatic mode does **not** provide:
95
95
  - Any special stdout streaming guarantee — the runtime's normal output rules apply.
96
96
  - Telemetry, JSONL streams, or runtime token-budget enforcement.
97
97
 
98
+ ### Pi runtime
99
+
100
+ Pi is package-managed rather than file-managed. Stack supports the exact tested pair **Pi 0.84.2 + `jorgex-pi@0.1.0`** and keeps Pi out of the adapter/component manifest and model map.
101
+
102
+ ```bash
103
+ pnpm dlx jorgex-stack install --agents pi
104
+ pnpm dlx jorgex-stack doctor --agents pi
105
+ pnpm dlx jorgex-stack models --agents pi
106
+ pnpm dlx jorgex-stack sync --agents pi
107
+ pnpm dlx jorgex-stack uninstall --agents pi
108
+ ```
109
+
110
+ Stack downloads the frozen registry tarball, verifies its exact size plus SHA-256/SHA-512, backs up Pi's `settings.json`, and only then asks Pi to install that local file. Pi's own package-manager invocation is the narrow runtime exception to the repository's pnpm-only rule; the Stack lifecycle never launches npm directly. A scope-bound receipt under `~/.jorgex-stack/pi-receipt.json` records ownership only after the package runner reports a healthy install. Manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state fails closed and is never adopted or removed silently.
111
+
112
+ Engram remains mandatory and user-owned. An existing binary is preserved. Interactive install may offer the existing native `brew`/`go`/release channel with a default-No confirmation; `--yes` and non-TTY installs fail with a remedy when Engram is absent. No Pi lifecycle operation updates or deletes the Engram database or memories. Under `--target-dir`, Stack accepts only `<target>/bin/engram`, isolates Pi/Home/XDG/AppData/temp/npm-cache paths inside the target, and never consults the host Engram or Pi configuration.
113
+
114
+ `update --agents pi` only runs the Pi package lifecycle; it does not enter the global Stack updater. `update --check --agents pi` is a read-only Pi doctor. Uninstall runs package cleanup, backs up Pi's settings before removal, removes only the exact receipt-owned package after verifying absence, and preserves all companion/user state. Full behavior, failure states and troubleshooting are in [docs/references/pi-runtime.md](docs/references/pi-runtime.md).
115
+
98
116
  ### Browser automation
99
117
 
100
118
  Browser automation is opt-in and explicit. The legacy `agent-browser` integration has been removed; rely on the two surfaces below.
package/dist/cli.js CHANGED
@@ -3713,6 +3713,856 @@ function readPackageMetadata() {
3713
3713
  return { name, version };
3714
3714
  }
3715
3715
 
3716
+ // src/lib/pi-runtime.ts
3717
+ import os5 from "os";
3718
+ import path30 from "path";
3719
+ import fs22 from "fs";
3720
+ import { spawnSync } from "child_process";
3721
+ import { createHash } from "crypto";
3722
+
3723
+ // src/lib/pi-package-lifecycle.ts
3724
+ import path29 from "path";
3725
+ var REQUIRED_CAPABILITIES = /* @__PURE__ */ new Set([
3726
+ "foundation-contract-v1",
3727
+ "runner-json-v1"
3728
+ ]);
3729
+ function sameRecord(left, right) {
3730
+ return JSON.stringify(left) === JSON.stringify(right);
3731
+ }
3732
+ function ownership(receipt) {
3733
+ return { receipt, adapters: false, manifest: false, modelMap: false };
3734
+ }
3735
+ function blocked(input, reason) {
3736
+ return {
3737
+ kind: "blocked",
3738
+ reason,
3739
+ receiptPath: input.scope.receiptPath,
3740
+ ownership: ownership(input.receiptJson !== null)
3741
+ };
3742
+ }
3743
+ function packageSource(entry) {
3744
+ if (typeof entry === "string") return entry;
3745
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
3746
+ const source = Reflect.get(entry, "source");
3747
+ return typeof source === "string" ? source : null;
3748
+ }
3749
+ function isJorgeXPiSource(source) {
3750
+ return source.includes("jorgex-pi");
3751
+ }
3752
+ function parsePackageSources(settingsJson) {
3753
+ try {
3754
+ const parsed = JSON.parse(settingsJson);
3755
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3756
+ const packages = Reflect.get(parsed, "packages");
3757
+ if (!Array.isArray(packages)) return null;
3758
+ const sources = packages.map(packageSource);
3759
+ return sources.every((source) => source !== null) ? sources : null;
3760
+ } catch {
3761
+ return null;
3762
+ }
3763
+ }
3764
+ function expectedReceipt(candidate, state, scope) {
3765
+ return {
3766
+ schemaVersion: 1,
3767
+ state,
3768
+ candidate: {
3769
+ package: candidate.package,
3770
+ tarball: candidate.tarball,
3771
+ provenance: candidate.provenance
3772
+ },
3773
+ scope
3774
+ };
3775
+ }
3776
+ function parseReceipt(receiptJson, candidate, scope) {
3777
+ try {
3778
+ const parsed = JSON.parse(receiptJson);
3779
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3780
+ const state = Reflect.get(parsed, "state");
3781
+ if (state !== "installing" && state !== "installed") return null;
3782
+ const expected = expectedReceipt(candidate, state, scope);
3783
+ return sameRecord(parsed, expected) ? expected : null;
3784
+ } catch {
3785
+ return null;
3786
+ }
3787
+ }
3788
+ function candidateIsValid(candidate, observed) {
3789
+ 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 && candidate.contract.managedExternalWrites.length === 0 && [...REQUIRED_CAPABILITIES].every((capability) => candidate.contract.capabilities.includes(capability)) && sameRecord(candidate.tarball, observed);
3790
+ }
3791
+ function planPiPackageLifecycle(input) {
3792
+ if (!candidateIsValid(input.candidate, input.observedTarball)) {
3793
+ return blocked(input, "tarball-integrity");
3794
+ }
3795
+ if (!input.candidate.pi.testedVersions.includes(input.pi.version)) {
3796
+ return blocked(input, "unsupported-pi-version");
3797
+ }
3798
+ if (input.engramBin === null) return blocked(input, "engram-missing");
3799
+ const sources = parsePackageSources(input.pi.settingsJson);
3800
+ if (sources === null) return blocked(input, "settings-corrupt");
3801
+ const matchingSources = sources.filter(isJorgeXPiSource);
3802
+ const exactSources = matchingSources.filter((source) => source === input.candidate.package.source);
3803
+ if (exactSources.length > 1) return blocked(input, "duplicate-package");
3804
+ if (matchingSources.some((source) => source !== input.candidate.package.source)) {
3805
+ return blocked(input, "source-divergent");
3806
+ }
3807
+ let receipt = null;
3808
+ if (input.receiptJson !== null) {
3809
+ receipt = parseReceipt(input.receiptJson, input.candidate, {
3810
+ kind: input.scope.kind,
3811
+ codingAgentDir: path29.resolve(input.scope.codingAgentDir)
3812
+ });
3813
+ if (receipt === null) return blocked(input, "receipt-corrupt");
3814
+ if (receipt.state === "installing") return blocked(input, "partial-state");
3815
+ if (exactSources.length !== 1) return blocked(input, "partial-state");
3816
+ }
3817
+ if (exactSources.length === 1 && receipt === null) {
3818
+ return {
3819
+ kind: "manual-existing",
3820
+ receiptPath: input.scope.receiptPath,
3821
+ ownership: ownership(false)
3822
+ };
3823
+ }
3824
+ if (exactSources.length === 1 && receipt !== null) {
3825
+ return {
3826
+ kind: "ready",
3827
+ receiptPath: input.scope.receiptPath,
3828
+ ownership: ownership(true)
3829
+ };
3830
+ }
3831
+ return {
3832
+ kind: "install",
3833
+ receiptPath: input.scope.receiptPath,
3834
+ invocation: {
3835
+ executable: input.pi.executable,
3836
+ args: ["install", input.candidate.package.source, "--no-approve"],
3837
+ environment: input.scope.environment
3838
+ },
3839
+ receipt: expectedReceipt(input.candidate, "installing", {
3840
+ kind: input.scope.kind,
3841
+ codingAgentDir: path29.resolve(input.scope.codingAgentDir)
3842
+ }),
3843
+ ownership: ownership(true)
3844
+ };
3845
+ }
3846
+ function parseRunnerRecord(stdout, stderr, command, candidate, packageRunner) {
3847
+ if (stderr !== "" || !stdout.endsWith("\n") || Buffer.byteLength(stdout) > candidate.contract.runner.maxStdoutBytes) {
3848
+ return null;
3849
+ }
3850
+ const body = stdout.slice(0, -1);
3851
+ if (body === "" || body.includes("\n") || body.includes("\r")) return null;
3852
+ try {
3853
+ const parsed = JSON.parse(body);
3854
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3855
+ const record = parsed;
3856
+ if (record.schemaVersion !== candidate.contract.runner.schemaVersion || record.command !== command || record.ok !== true || record.package === null || typeof record.package !== "object" || record.package.name !== candidate.package.name || record.package.version !== candidate.package.version || typeof record.package.root !== "string" || !path29.isAbsolute(record.package.root) || path29.resolve(packageRunner) !== path29.resolve(record.package.root, "bin", "jorgex-pi.mjs")) {
3857
+ return null;
3858
+ }
3859
+ return record;
3860
+ } catch {
3861
+ return null;
3862
+ }
3863
+ }
3864
+ function runPackageCommand(input, deps, command) {
3865
+ const result = deps.run({
3866
+ executable: input.packageRunner,
3867
+ args: [command, "--json"],
3868
+ environment: input.environment
3869
+ });
3870
+ if (result.exitCode !== 0) return { kind: "blocked", reason: "runner-unhealthy" };
3871
+ return parseRunnerRecord(result.stdout, result.stderr, command, input.candidate, input.packageRunner) ?? { kind: "blocked", reason: "runner-output" };
3872
+ }
3873
+ function isBlockedResult(value) {
3874
+ return "kind" in value;
3875
+ }
3876
+ function executePiPackageLifecycle(input, deps) {
3877
+ if (input.plan.kind === "manual-existing") return { kind: "manual-existing" };
3878
+ if (input.operation === "install") {
3879
+ if (input.plan.kind !== "install" || input.plan.receipt === void 0 || input.plan.invocation === void 0) {
3880
+ return { kind: "blocked", reason: "runner-unhealthy" };
3881
+ }
3882
+ deps.writeReceipt(input.plan.receipt);
3883
+ const installed = deps.run(input.plan.invocation);
3884
+ if (installed.exitCode !== 0 || installed.stderr !== "") {
3885
+ return { kind: "blocked", reason: "pi-install-failed" };
3886
+ }
3887
+ const doctor = runPackageCommand(input, deps, "doctor");
3888
+ if (isBlockedResult(doctor)) return doctor;
3889
+ const doctorResult = doctor.result;
3890
+ if (doctorResult === null || typeof doctorResult !== "object" || Reflect.get(doctorResult, "healthy") !== true) {
3891
+ return { kind: "blocked", reason: "runner-unhealthy" };
3892
+ }
3893
+ const receipt = { ...input.plan.receipt, state: "installed" };
3894
+ deps.writeReceipt(receipt);
3895
+ return { kind: "installed", receipt };
3896
+ }
3897
+ if (input.plan.kind !== "ready") return { kind: "blocked", reason: "runner-unhealthy" };
3898
+ const command = runPackageCommand(input, deps, input.operation);
3899
+ if (isBlockedResult(command)) return command;
3900
+ if (input.operation === "sync") {
3901
+ const result = command.result;
3902
+ if (result === null || typeof result !== "object" || Reflect.get(result, "changed") !== false) {
3903
+ return { kind: "blocked", reason: "runner-unhealthy" };
3904
+ }
3905
+ return { kind: "synced", actions: [] };
3906
+ }
3907
+ const models = command.result;
3908
+ if (models === null || typeof models !== "object" || Reflect.get(models, "mode") !== "inherit-session" || !sameRecord(Reflect.get(models, "tiers"), ["strong", "standard", "cheap"])) {
3909
+ return { kind: "blocked", reason: "runner-unhealthy" };
3910
+ }
3911
+ return { kind: "models", models: { mode: "inherit-session", tiers: ["strong", "standard", "cheap"] } };
3912
+ }
3913
+ function readReceiptCandidate(receiptJson) {
3914
+ try {
3915
+ const parsed = JSON.parse(receiptJson);
3916
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3917
+ const schemaVersion = Reflect.get(parsed, "schemaVersion");
3918
+ const state = Reflect.get(parsed, "state");
3919
+ const candidate = Reflect.get(parsed, "candidate");
3920
+ if (schemaVersion !== 1 || state !== "installing" && state !== "installed" || candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
3921
+ return null;
3922
+ }
3923
+ const packageValue = Reflect.get(candidate, "package");
3924
+ const tarball = Reflect.get(candidate, "tarball");
3925
+ const provenance = Reflect.get(candidate, "provenance");
3926
+ const scope = Reflect.get(parsed, "scope");
3927
+ if (packageValue === null || typeof packageValue !== "object" || tarball === null || typeof tarball !== "object" || provenance === null || typeof provenance !== "object" || scope === null || typeof scope !== "object" || Array.isArray(scope)) {
3928
+ return null;
3929
+ }
3930
+ const source = Reflect.get(packageValue, "source");
3931
+ const name = Reflect.get(packageValue, "name");
3932
+ const version = Reflect.get(packageValue, "version");
3933
+ if (name !== "jorgex-pi" || typeof version !== "string" || typeof source !== "string" || source !== `npm:jorgex-pi@${version}`) {
3934
+ return null;
3935
+ }
3936
+ const scopeKind = Reflect.get(scope, "kind");
3937
+ const codingAgentDir = Reflect.get(scope, "codingAgentDir");
3938
+ if (scopeKind !== "real" && scopeKind !== "target-dir" || typeof codingAgentDir !== "string") return null;
3939
+ return parsed;
3940
+ } catch {
3941
+ return null;
3942
+ }
3943
+ }
3944
+ function validateOwnedOperationState(input) {
3945
+ const sources = parsePackageSources(input.detected.settingsJson);
3946
+ if (sources === null) return { kind: "blocked", reason: "settings-corrupt" };
3947
+ const matchingSources = sources.filter(isJorgeXPiSource);
3948
+ if (matchingSources.length > 1) return { kind: "blocked", reason: "duplicate-package" };
3949
+ if (input.receiptJson === null) {
3950
+ return { kind: "blocked", reason: matchingSources.length === 1 ? "manual-existing" : "source-divergent" };
3951
+ }
3952
+ const receipt = readReceiptCandidate(input.receiptJson);
3953
+ if (receipt === null) return { kind: "blocked", reason: "receipt-corrupt" };
3954
+ if (receipt.state !== "installed") return { kind: "blocked", reason: "partial-state" };
3955
+ const accepted = input.registry.acceptedCandidates ?? [input.registry.candidate];
3956
+ if (!accepted.some((candidate) => sameRecord(receipt.candidate, {
3957
+ package: candidate.package,
3958
+ tarball: candidate.tarball,
3959
+ provenance: candidate.provenance
3960
+ }))) {
3961
+ return { kind: "blocked", reason: "receipt-untrusted" };
3962
+ }
3963
+ if (receipt.scope.kind !== (input.paths.targetDir ? "target-dir" : "real") || path29.resolve(receipt.scope.codingAgentDir) !== path29.resolve(input.paths.codingAgentDir)) {
3964
+ return { kind: "blocked", reason: "source-divergent" };
3965
+ }
3966
+ const source = receipt.candidate.package.source;
3967
+ if (matchingSources.length !== 1 || matchingSources[0] !== source) {
3968
+ return { kind: "blocked", reason: "source-divergent" };
3969
+ }
3970
+ return { receipt, source };
3971
+ }
3972
+ function operationWasBlocked(value) {
3973
+ return "kind" in value;
3974
+ }
3975
+ function runManagedRunner(input, deps, command) {
3976
+ const result = deps.run({
3977
+ executable: input.detected.packageRunner,
3978
+ args: [command, "--json"],
3979
+ environment: input.paths.environment
3980
+ });
3981
+ if (result.exitCode !== 0) return { kind: "blocked", reason: "runner-unhealthy" };
3982
+ const parsed = parseRunnerRecord(
3983
+ result.stdout,
3984
+ result.stderr,
3985
+ command,
3986
+ input.registry.candidate,
3987
+ input.detected.packageRunner
3988
+ );
3989
+ return parsed ?? { kind: "blocked", reason: "runner-output" };
3990
+ }
3991
+ function managedRunnerWasBlocked(value) {
3992
+ return "kind" in value;
3993
+ }
3994
+ function runPiPackageManagedOperation(input, deps) {
3995
+ if (input.engramBin === null && input.operation !== "uninstall") {
3996
+ return {
3997
+ kind: "blocked",
3998
+ reason: "engram-missing",
3999
+ remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
4000
+ };
4001
+ }
4002
+ const owned = validateOwnedOperationState(input);
4003
+ if (operationWasBlocked(owned)) return owned;
4004
+ if (input.operation === "doctor") {
4005
+ if (!sameRecord(owned.receipt.candidate, {
4006
+ package: input.registry.candidate.package,
4007
+ tarball: input.registry.candidate.tarball,
4008
+ provenance: input.registry.candidate.provenance
4009
+ })) {
4010
+ return { kind: "blocked", reason: "source-divergent" };
4011
+ }
4012
+ const doctor = runManagedRunner(input, deps, "doctor");
4013
+ if (managedRunnerWasBlocked(doctor)) return doctor;
4014
+ const result = doctor.result;
4015
+ return result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true ? { kind: "healthy" } : { kind: "blocked", reason: "runner-unhealthy" };
4016
+ }
4017
+ if (input.operation === "uninstall") {
4018
+ if (!sameRecord(owned.receipt.candidate, {
4019
+ package: input.registry.candidate.package,
4020
+ tarball: input.registry.candidate.tarball,
4021
+ provenance: input.registry.candidate.provenance
4022
+ })) {
4023
+ return { kind: "blocked", reason: "source-divergent" };
4024
+ }
4025
+ const cleanup = runManagedRunner(input, deps, "cleanup");
4026
+ if (managedRunnerWasBlocked(cleanup)) return cleanup;
4027
+ deps.backupSettings();
4028
+ const removed = deps.run({
4029
+ executable: input.detected.executable,
4030
+ args: ["remove", owned.source, "--no-approve"],
4031
+ environment: input.paths.environment
4032
+ });
4033
+ if (removed.exitCode !== 0 || removed.stderr !== "") return { kind: "blocked", reason: "remove-failed" };
4034
+ if (!deps.isPackageAbsent()) return { kind: "blocked", reason: "absence-unverified" };
4035
+ deps.deleteReceipt();
4036
+ return { kind: "uninstalled" };
4037
+ }
4038
+ const nextSource = input.registry.candidate.package.source;
4039
+ if (nextSource === owned.source) return { kind: "healthy" };
4040
+ return {
4041
+ kind: "blocked",
4042
+ reason: "verified-update-required",
4043
+ remedy: "A cross-version Pi update requires verified replacement and rollback tgz artifacts."
4044
+ };
4045
+ }
4046
+
4047
+ // src/lib/pi-runtime.ts
4048
+ var PI_RUNTIME_CANDIDATE = {
4049
+ package: {
4050
+ name: "jorgex-pi",
4051
+ version: "0.1.0",
4052
+ source: "npm:jorgex-pi@0.1.0"
4053
+ },
4054
+ provenance: {
4055
+ commit: "791db79e33efd6661899995b5491e4dff5caa363"
4056
+ },
4057
+ tarball: {
4058
+ bytes: 89066153,
4059
+ sha256: "6243bf8e3a8dbe7be9103d7ca9b03e196c41ac9eef6578f47ea6d03655366feb",
4060
+ sha512: "07590abec9e9594b001e28d75eb810259c4088f9f2f6d1d5b9fe456bb2d15a7259ff31ee225d5f1f59b27a1c337a1f1f8e3e57089656bf7bbad966e653110ddd"
4061
+ },
4062
+ pi: {
4063
+ testedVersions: ["0.84.2"]
4064
+ },
4065
+ contract: {
4066
+ schemaVersion: 1,
4067
+ capabilities: [
4068
+ "foundation-contract-v1",
4069
+ "stack-snapshot-v1",
4070
+ "runtime-agents-v1",
4071
+ "permission-gated-tools-v1",
4072
+ "structured-questions-v1",
4073
+ "web-access-v1",
4074
+ "goal-continuation-v1",
4075
+ "mcp-adapter-v1",
4076
+ "engram-runtime-tools-v1",
4077
+ "runner-json-v1"
4078
+ ],
4079
+ runner: {
4080
+ bin: "jorgex-pi",
4081
+ commands: ["status", "doctor", "models", "sync", "cleanup"],
4082
+ schemaVersion: 1,
4083
+ maxStdoutBytes: 65536
4084
+ },
4085
+ managedExternalWrites: []
4086
+ }
4087
+ };
4088
+ var PI_RUNTIME_REGISTRY = {
4089
+ pi: {
4090
+ id: "pi",
4091
+ kind: "package-managed",
4092
+ source: PI_RUNTIME_CANDIDATE.package.source,
4093
+ tarball: PI_RUNTIME_CANDIDATE.tarball,
4094
+ pi: PI_RUNTIME_CANDIDATE.pi,
4095
+ candidate: PI_RUNTIME_CANDIDATE,
4096
+ acceptedCandidates: [PI_RUNTIME_CANDIDATE]
4097
+ }
4098
+ };
4099
+ async function resolvePiEngramRequirement(input, deps) {
4100
+ if (input.targetDir !== void 0) {
4101
+ const targetBin = deps.detectTarget(input.targetDir);
4102
+ return targetBin === null ? {
4103
+ kind: "blocked",
4104
+ reason: "engram-missing-target",
4105
+ remedy: "A\xF1ade el binario Engram dentro del target-dir antes de reintentar."
4106
+ } : { kind: "existing", bin: targetBin, scope: "target-dir" };
4107
+ }
4108
+ const existing = deps.detectHost();
4109
+ if (existing !== null) return { kind: "existing", bin: existing, scope: "host" };
4110
+ if (input.yes || !input.interactive) {
4111
+ return {
4112
+ kind: "blocked",
4113
+ reason: "engram-required",
4114
+ remedy: "Instala Engram de forma interactiva o configura ENGRAM_BIN antes de reintentar."
4115
+ };
4116
+ }
4117
+ const accepted = await deps.confirm({
4118
+ message: "Engram es obligatorio para JorgeX Pi. \xBFInstalar ahora el binario mediante el canal nativo?",
4119
+ initialValue: false
4120
+ });
4121
+ if (!accepted) return { kind: "offer", accepted: false };
4122
+ const installed = await deps.installNative({ version: "1.20.0", channels: ["brew", "go", "url"] });
4123
+ if (!installed) {
4124
+ return {
4125
+ kind: "blocked",
4126
+ reason: "engram-install-failed",
4127
+ remedy: "Instala Engram manualmente o configura ENGRAM_BIN antes de reintentar."
4128
+ };
4129
+ }
4130
+ const detected = deps.detectHost();
4131
+ return detected === null ? {
4132
+ kind: "blocked",
4133
+ reason: "engram-install-unverified",
4134
+ remedy: "La instalaci\xF3n termin\xF3, pero Engram no qued\xF3 detectable; configura ENGRAM_BIN."
4135
+ } : { kind: "existing", bin: detected, scope: "host" };
4136
+ }
4137
+ function flatCandidateReceipt(candidate, scope, state) {
4138
+ const match = /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source);
4139
+ const packageValue = candidate.package ?? {
4140
+ name: "jorgex-pi",
4141
+ version: match?.[1] ?? "0.1.0",
4142
+ source: candidate.source
4143
+ };
4144
+ return {
4145
+ schemaVersion: 1,
4146
+ state,
4147
+ candidate: {
4148
+ package: packageValue,
4149
+ tarball: { bytes: candidate.bytes, sha256: candidate.sha256, sha512: candidate.sha512 },
4150
+ provenance: candidate.provenance ?? { commit: PI_RUNTIME_CANDIDATE.provenance.commit }
4151
+ },
4152
+ scope
4153
+ };
4154
+ }
4155
+ function normalizeInstalledSource(settingsJson, alias, canonical) {
4156
+ try {
4157
+ const parsed = JSON.parse(settingsJson);
4158
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
4159
+ const packages = Reflect.get(parsed, "packages");
4160
+ if (!Array.isArray(packages)) return null;
4161
+ if (packages.filter((entry) => entry === alias).length !== 1 || packages.includes(canonical)) return null;
4162
+ Reflect.set(parsed, "packages", packages.map((entry) => entry === alias ? canonical : entry));
4163
+ return JSON.stringify(parsed);
4164
+ } catch {
4165
+ return null;
4166
+ }
4167
+ }
4168
+ function healthyDoctor(stdout, stderr, packageRunner, candidate) {
4169
+ if (stderr !== "" || !stdout.endsWith("\n") || stdout.slice(0, -1).includes("\n")) return false;
4170
+ try {
4171
+ const record = JSON.parse(stdout.slice(0, -1));
4172
+ if (record === null || typeof record !== "object" || Array.isArray(record)) return false;
4173
+ const packageValue = Reflect.get(record, "package");
4174
+ const result = Reflect.get(record, "result");
4175
+ return Reflect.get(record, "schemaVersion") === 1 && Reflect.get(record, "command") === "doctor" && Reflect.get(record, "ok") === true && packageValue !== null && typeof packageValue === "object" && Reflect.get(packageValue, "name") === "jorgex-pi" && Reflect.get(packageValue, "version") === (candidate.package?.version ?? /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source)?.[1]) && path30.resolve(packageRunner) === path30.resolve(String(Reflect.get(packageValue, "root")), "bin", "jorgex-pi.mjs") && result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true;
4176
+ } catch {
4177
+ return false;
4178
+ }
4179
+ }
4180
+ function installPiFromVerifiedTarball(input, deps) {
4181
+ const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.piExecutable) : targetPaths(input.targetDir, input.engramBin, input.piExecutable);
4182
+ const destination = input.targetDir === void 0 ? path30.join(dataDir(), "packages", "jorgex-pi-0.1.0.tgz") : path30.join(path30.resolve(input.targetDir), "downloads", "jorgex-pi-0.1.0.tgz");
4183
+ const artifact = deps.download(destination);
4184
+ if (artifact.bytes !== input.candidate.bytes || artifact.sha256 !== input.candidate.sha256 || artifact.sha512 !== input.candidate.sha512) {
4185
+ return { kind: "blocked", reason: "tarball-integrity" };
4186
+ }
4187
+ deps.backupSettings();
4188
+ const scope = {
4189
+ kind: input.targetDir === void 0 ? "real" : "target-dir",
4190
+ codingAgentDir: path30.resolve(paths.codingAgentDir)
4191
+ };
4192
+ const installing = flatCandidateReceipt(input.candidate, scope, "installing");
4193
+ deps.writeReceiptAtomic(`${JSON.stringify(installing)}
4194
+ `);
4195
+ const alias = `npm:jorgex-pi@file:${artifact.path}`;
4196
+ const installed = deps.run({
4197
+ executable: input.piExecutable,
4198
+ args: ["install", alias, "--no-approve"],
4199
+ environment: paths.environment
4200
+ });
4201
+ if (installed.exitCode !== 0 || installed.stderr !== "") return { kind: "blocked", reason: "pi-install-failed" };
4202
+ const normalized = normalizeInstalledSource(deps.readSettings(), alias, input.candidate.source);
4203
+ if (normalized === null) return { kind: "blocked", reason: "settings-corrupt" };
4204
+ deps.rewriteSettings(normalized);
4205
+ const doctor = deps.run({
4206
+ executable: process.execPath,
4207
+ args: [paths.packageRunner, "doctor", "--json"],
4208
+ environment: paths.environment
4209
+ });
4210
+ if (doctor.exitCode !== 0 || !healthyDoctor(doctor.stdout, doctor.stderr, paths.packageRunner, input.candidate)) {
4211
+ return { kind: "blocked", reason: "runner-unhealthy" };
4212
+ }
4213
+ const receipt = flatCandidateReceipt(input.candidate, scope, "installed");
4214
+ deps.writeReceiptAtomic(`${JSON.stringify(receipt)}
4215
+ `);
4216
+ return { kind: "installed", receipt };
4217
+ }
4218
+ function runtimePath(piExecutable) {
4219
+ const entries = process.platform === "win32" ? [piExecutable === void 0 ? null : path30.dirname(piExecutable), path30.dirname(process.execPath), process.env.SystemRoot ? path30.join(process.env.SystemRoot, "System32") : null] : [piExecutable === void 0 ? null : path30.dirname(piExecutable), path30.dirname(process.execPath), "/usr/local/bin", "/usr/bin", "/bin"];
4220
+ return [...new Set(entries.filter((entry) => entry !== null))].join(path30.delimiter);
4221
+ }
4222
+ function targetPaths(targetDir, engramBin, piExecutable) {
4223
+ const root = path30.resolve(targetDir);
4224
+ const codingAgentDir = path30.join(root, "pi-agent");
4225
+ const home = path30.join(root, "home");
4226
+ const temporary = path30.join(root, "tmp");
4227
+ return {
4228
+ codingAgentDir,
4229
+ receiptPath: path30.join(root, "state", "pi-receipt.json"),
4230
+ packageRunner: path30.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
4231
+ environment: {
4232
+ HOME: home,
4233
+ USERPROFILE: home,
4234
+ APPDATA: path30.join(root, "appdata"),
4235
+ LOCALAPPDATA: path30.join(root, "localappdata"),
4236
+ XDG_CONFIG_HOME: path30.join(root, "xdg-config"),
4237
+ XDG_DATA_HOME: path30.join(root, "xdg-data"),
4238
+ XDG_CACHE_HOME: path30.join(root, "xdg-cache"),
4239
+ TEMP: temporary,
4240
+ TMP: temporary,
4241
+ TMPDIR: temporary,
4242
+ npm_config_cache: path30.join(root, "npm-cache"),
4243
+ NPM_CONFIG_IGNORE_SCRIPTS: "true",
4244
+ NPM_CONFIG_UPDATE_NOTIFIER: "false",
4245
+ PI_CODING_AGENT_DIR: codingAgentDir,
4246
+ ...engramBin === null ? {} : { ENGRAM_BIN: engramBin },
4247
+ PATH: runtimePath(piExecutable)
4248
+ }
4249
+ };
4250
+ }
4251
+ function userPaths(engramBin, piExecutable) {
4252
+ const home = os5.homedir();
4253
+ const codingAgentDir = process.env.PI_CODING_AGENT_DIR ?? path30.join(home, ".pi", "agent");
4254
+ return {
4255
+ codingAgentDir,
4256
+ receiptPath: path30.join(dataDir(), "pi-receipt.json"),
4257
+ packageRunner: path30.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
4258
+ environment: {
4259
+ HOME: home,
4260
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path30.join(home, ".config"),
4261
+ XDG_CACHE_HOME: process.env.XDG_CACHE_HOME ?? path30.join(home, ".cache"),
4262
+ TMPDIR: os5.tmpdir(),
4263
+ NPM_CONFIG_IGNORE_SCRIPTS: "true",
4264
+ NPM_CONFIG_UPDATE_NOTIFIER: "false",
4265
+ PI_CODING_AGENT_DIR: codingAgentDir,
4266
+ ...engramBin === null ? {} : { ENGRAM_BIN: engramBin },
4267
+ PATH: runtimePath(piExecutable)
4268
+ }
4269
+ };
4270
+ }
4271
+ function persistReturnedReceipt(result, paths, deps) {
4272
+ if (result.receipt !== void 0) {
4273
+ deps.writeReceiptAtomic(paths.receiptPath, `${JSON.stringify(result.receipt)}
4274
+ `);
4275
+ }
4276
+ }
4277
+ function runPiRuntime(input, deps) {
4278
+ if (input.engramBin === null && input.operation !== "uninstall") {
4279
+ return {
4280
+ kind: "blocked",
4281
+ reason: "engram-missing",
4282
+ remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
4283
+ };
4284
+ }
4285
+ if (input.operation === "install" && input.verifiedArtifact === void 0) {
4286
+ return { kind: "blocked", reason: "tarball-integrity" };
4287
+ }
4288
+ const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.detected.executable) : targetPaths(input.targetDir, input.engramBin, input.detected.executable);
4289
+ const settingsJson = deps.readSettings(path30.join(paths.codingAgentDir, "settings.json"));
4290
+ const receiptJson = deps.readReceipt(paths.receiptPath);
4291
+ const lifecycleInput = {
4292
+ candidate: PI_RUNTIME_CANDIDATE,
4293
+ observedTarball: input.verifiedArtifact ?? PI_RUNTIME_CANDIDATE.tarball,
4294
+ pi: {
4295
+ executable: input.detected.executable,
4296
+ version: input.detected.version,
4297
+ packageRunner: paths.packageRunner,
4298
+ settingsJson
4299
+ },
4300
+ engramBin: input.engramBin,
4301
+ receiptJson,
4302
+ scope: {
4303
+ kind: input.targetDir === void 0 ? "real" : "target-dir",
4304
+ codingAgentDir: paths.codingAgentDir,
4305
+ receiptPath: paths.receiptPath,
4306
+ environment: paths.environment
4307
+ }
4308
+ };
4309
+ if (input.operation === "install" || input.operation === "sync" || input.operation === "models") {
4310
+ const plan = deps.prepare(lifecycleInput);
4311
+ const result2 = deps.execute({
4312
+ operation: input.operation,
4313
+ plan,
4314
+ candidate: PI_RUNTIME_CANDIDATE,
4315
+ packageRunner: paths.packageRunner,
4316
+ environment: paths.environment
4317
+ });
4318
+ persistReturnedReceipt(result2, paths, deps);
4319
+ return result2;
4320
+ }
4321
+ const result = deps.operate({
4322
+ operation: input.operation,
4323
+ interactive: false,
4324
+ registry: PI_RUNTIME_REGISTRY.pi,
4325
+ detected: {
4326
+ executable: input.detected.executable,
4327
+ packageRunner: paths.packageRunner,
4328
+ settingsJson
4329
+ },
4330
+ engramBin: input.engramBin,
4331
+ receiptJson,
4332
+ paths: {
4333
+ targetDir: input.targetDir !== void 0,
4334
+ codingAgentDir: paths.codingAgentDir,
4335
+ receiptPath: paths.receiptPath,
4336
+ environment: paths.environment
4337
+ },
4338
+ removeArgs: ["remove", PI_RUNTIME_CANDIDATE.package.source, "--no-approve"]
4339
+ });
4340
+ persistReturnedReceipt(result, paths, deps);
4341
+ return result;
4342
+ }
4343
+ function readJsonFile(file) {
4344
+ return JSON.parse(fs22.readFileSync(file, "utf8"));
4345
+ }
4346
+ function packageVersionFromExecutable(executable) {
4347
+ let current;
4348
+ try {
4349
+ current = path30.dirname(fs22.realpathSync(executable));
4350
+ } catch {
4351
+ return null;
4352
+ }
4353
+ for (let depth = 0; depth < 8; depth++) {
4354
+ const manifests = [
4355
+ path30.join(current, "package.json"),
4356
+ path30.join(current, "node_modules", "@earendil-works", "pi-coding-agent", "package.json")
4357
+ ];
4358
+ for (const manifest of manifests) {
4359
+ try {
4360
+ const parsed = readJsonFile(manifest);
4361
+ if (parsed !== null && typeof parsed === "object" && Reflect.get(parsed, "name") === "@earendil-works/pi-coding-agent" && typeof Reflect.get(parsed, "version") === "string") {
4362
+ return Reflect.get(parsed, "version");
4363
+ }
4364
+ } catch {
4365
+ }
4366
+ }
4367
+ const parent = path30.dirname(current);
4368
+ if (parent === current) break;
4369
+ current = parent;
4370
+ }
4371
+ return null;
4372
+ }
4373
+ function detectPiRuntime() {
4374
+ const executable = lookPath("pi");
4375
+ const home = os5.homedir();
4376
+ return {
4377
+ id: "pi",
4378
+ name: "Pi",
4379
+ installed: executable !== null,
4380
+ executable,
4381
+ version: executable === null ? null : packageVersionFromExecutable(executable),
4382
+ codingAgentDir: process.env.PI_CODING_AGENT_DIR ?? path30.join(home, ".pi", "agent")
4383
+ };
4384
+ }
4385
+ function hasManagedPiRuntime(targetDir) {
4386
+ const receipt = targetDir === void 0 ? path30.join(dataDir(), "pi-receipt.json") : path30.join(path30.resolve(targetDir), "state", "pi-receipt.json");
4387
+ return fs22.statSync(receipt, { throwIfNoEntry: false })?.isFile() === true;
4388
+ }
4389
+ function resolvePiEngramBin(targetDir) {
4390
+ if (targetDir === void 0) return detectEngram();
4391
+ const candidate = path30.join(path30.resolve(targetDir), "bin", process.platform === "win32" ? "engram.exe" : "engram");
4392
+ return fs22.statSync(candidate, { throwIfNoEntry: false })?.isFile() ? candidate : null;
4393
+ }
4394
+ function readOptional(file, fallback) {
4395
+ try {
4396
+ return fs22.readFileSync(file, "utf8");
4397
+ } catch (error) {
4398
+ if (error.code === "ENOENT") return fallback;
4399
+ throw error;
4400
+ }
4401
+ }
4402
+ function hashPiTarball(file) {
4403
+ const descriptor = fs22.openSync(file, "r");
4404
+ const sha256 = createHash("sha256");
4405
+ const sha512 = createHash("sha512");
4406
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
4407
+ let bytes = 0;
4408
+ try {
4409
+ while (true) {
4410
+ const read = fs22.readSync(descriptor, buffer, 0, buffer.length, null);
4411
+ if (read === 0) break;
4412
+ bytes += read;
4413
+ const chunk = buffer.subarray(0, read);
4414
+ sha256.update(chunk);
4415
+ sha512.update(chunk);
4416
+ }
4417
+ } finally {
4418
+ fs22.closeSync(descriptor);
4419
+ }
4420
+ return { path: file, bytes, sha256: sha256.digest("hex"), sha512: sha512.digest("hex") };
4421
+ }
4422
+ async function acquirePiTarball(destination) {
4423
+ const existing = fs22.statSync(destination, { throwIfNoEntry: false });
4424
+ if (existing?.isFile()) {
4425
+ const observed = hashPiTarball(destination);
4426
+ if (observed.bytes === PI_RUNTIME_CANDIDATE.tarball.bytes && observed.sha256 === PI_RUNTIME_CANDIDATE.tarball.sha256 && observed.sha512 === PI_RUNTIME_CANDIDATE.tarball.sha512) {
4427
+ return observed;
4428
+ }
4429
+ fs22.rmSync(destination, { force: true });
4430
+ }
4431
+ fs22.mkdirSync(path30.dirname(destination), { recursive: true });
4432
+ const partial = `${destination}.partial-${process.pid}`;
4433
+ const response = await fetch("https://registry.npmjs.org/jorgex-pi/-/jorgex-pi-0.1.0.tgz", {
4434
+ redirect: "error",
4435
+ headers: { accept: "application/octet-stream" }
4436
+ });
4437
+ if (!response.ok || response.body === null) throw new Error(`No se pudo descargar jorgex-pi@0.1.0 (${response.status}).`);
4438
+ const descriptor = fs22.openSync(partial, "wx", 384);
4439
+ let bytes = 0;
4440
+ try {
4441
+ for await (const chunk of response.body) {
4442
+ const buffer = Buffer.from(chunk);
4443
+ bytes += buffer.length;
4444
+ if (bytes > PI_RUNTIME_CANDIDATE.tarball.bytes) throw new Error("El tarball de jorgex-pi excede el tama\xF1o fijado.");
4445
+ let offset = 0;
4446
+ while (offset < buffer.length) offset += fs22.writeSync(descriptor, buffer, offset);
4447
+ }
4448
+ } catch (error) {
4449
+ fs22.closeSync(descriptor);
4450
+ fs22.rmSync(partial, { force: true });
4451
+ throw error;
4452
+ }
4453
+ fs22.closeSync(descriptor);
4454
+ fs22.renameSync(partial, destination);
4455
+ return hashPiTarball(destination);
4456
+ }
4457
+ function runProcess(invocation) {
4458
+ const planned = /\.mjs$/i.test(invocation.executable) ? { command: process.execPath, args: [invocation.executable, ...invocation.args] } : planDetectedBinCommand(invocation.executable, invocation.args);
4459
+ if (planned === null) return { exitCode: 1, stdout: "", stderr: "unsafe executable" };
4460
+ const result = spawnSync(planned.command, planned.args, {
4461
+ encoding: "utf8",
4462
+ env: invocation.environment,
4463
+ shell: false,
4464
+ timeout: 12e4,
4465
+ maxBuffer: PI_RUNTIME_CANDIDATE.contract.runner.maxStdoutBytes + 1,
4466
+ stdio: ["ignore", "pipe", "pipe"]
4467
+ });
4468
+ return {
4469
+ exitCode: result.status ?? 1,
4470
+ stdout: result.stdout ?? "",
4471
+ stderr: result.stderr ?? (result.error?.message ?? "")
4472
+ };
4473
+ }
4474
+ async function runPiRuntimeSystem(input) {
4475
+ if (input.engramBin === null && input.operation !== "uninstall") return runPiRuntime(input, {
4476
+ readSettings: () => {
4477
+ throw new Error("unreachable");
4478
+ },
4479
+ readReceipt: () => {
4480
+ throw new Error("unreachable");
4481
+ },
4482
+ writeReceiptAtomic: () => {
4483
+ throw new Error("unreachable");
4484
+ },
4485
+ prepare: () => {
4486
+ throw new Error("unreachable");
4487
+ },
4488
+ execute: () => {
4489
+ throw new Error("unreachable");
4490
+ },
4491
+ operate: () => {
4492
+ throw new Error("unreachable");
4493
+ }
4494
+ });
4495
+ const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.detected.executable) : targetPaths(input.targetDir, input.engramBin, input.detected.executable);
4496
+ if (input.operation === "install") {
4497
+ if (input.engramBin === null) {
4498
+ return {
4499
+ kind: "blocked",
4500
+ reason: "engram-required",
4501
+ remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
4502
+ };
4503
+ }
4504
+ const destination = input.targetDir === void 0 ? path30.join(dataDir(), "packages", "jorgex-pi-0.1.0.tgz") : path30.join(path30.resolve(input.targetDir), "downloads", "jorgex-pi-0.1.0.tgz");
4505
+ let artifact;
4506
+ try {
4507
+ artifact = await acquirePiTarball(destination);
4508
+ } catch (error) {
4509
+ return { kind: "blocked", reason: "tarball-download", remedy: error instanceof Error ? error.message : String(error) };
4510
+ }
4511
+ return installPiFromVerifiedTarball({
4512
+ targetDir: input.targetDir,
4513
+ piExecutable: input.detected.executable,
4514
+ engramBin: input.engramBin,
4515
+ candidate: {
4516
+ source: PI_RUNTIME_CANDIDATE.package.source,
4517
+ ...PI_RUNTIME_CANDIDATE.tarball,
4518
+ package: PI_RUNTIME_CANDIDATE.package,
4519
+ provenance: PI_RUNTIME_CANDIDATE.provenance
4520
+ }
4521
+ }, {
4522
+ download: () => artifact,
4523
+ backupSettings: () => createBackup(
4524
+ [path30.join(paths.codingAgentDir, "settings.json")],
4525
+ "pi-package-install",
4526
+ input.targetDir === void 0 ? void 0 : path30.join(path30.resolve(input.targetDir), "backups")
4527
+ ),
4528
+ run: runProcess,
4529
+ readSettings: () => readOptional(path30.join(paths.codingAgentDir, "settings.json"), '{"packages":[]}'),
4530
+ rewriteSettings: (content) => writeText(path30.join(paths.codingAgentDir, "settings.json"), `${content}
4531
+ `),
4532
+ writeReceiptAtomic: (content) => writeText(paths.receiptPath, content)
4533
+ });
4534
+ }
4535
+ const packageRoot = path30.dirname(path30.dirname(paths.packageRunner));
4536
+ const writeReceipt = (receipt) => {
4537
+ writeText(paths.receiptPath, `${JSON.stringify(receipt, null, 2)}
4538
+ `);
4539
+ };
4540
+ const deps = {
4541
+ readSettings: (file) => readOptional(file, '{"packages":[]}'),
4542
+ readReceipt: (file) => readOptional(file, null),
4543
+ writeReceiptAtomic: (file, content) => writeText(file, content),
4544
+ prepare: (value) => planPiPackageLifecycle(value),
4545
+ execute: (value) => executePiPackageLifecycle(
4546
+ value,
4547
+ { writeReceipt, run: runProcess }
4548
+ ),
4549
+ operate: (value) => runPiPackageManagedOperation(
4550
+ value,
4551
+ {
4552
+ backupSettings: () => createBackup(
4553
+ [path30.join(paths.codingAgentDir, "settings.json")],
4554
+ "pi-package-uninstall",
4555
+ input.targetDir === void 0 ? void 0 : path30.join(path30.resolve(input.targetDir), "backups")
4556
+ ),
4557
+ run: runProcess,
4558
+ isPackageAbsent: () => !fs22.existsSync(packageRoot),
4559
+ deleteReceipt: () => fs22.rmSync(paths.receiptPath, { force: true })
4560
+ }
4561
+ )
4562
+ };
4563
+ return runPiRuntime(input, deps);
4564
+ }
4565
+
3716
4566
  // src/cli.ts
3717
4567
  var VERSION = readPackageVersion();
3718
4568
  var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall"];
@@ -3911,19 +4761,71 @@ function parseCliArgs(argv) {
3911
4761
  if (flags.unknownFlags.length > 0) return { action: "unknown-flags", command, flags };
3912
4762
  return { action: "run", command, flags };
3913
4763
  }
3914
- async function resolveRuntimes(flags) {
4764
+ function isFileManagedRuntime(runtime) {
4765
+ return runtime !== "pi";
4766
+ }
4767
+ async function resolveRuntimes(flags, includeAvailablePi = false) {
3915
4768
  if (flags.agents.length > 0) return flags.agents;
3916
- const detected = Object.values(ADAPTERS).filter((a) => a.detect().installed);
4769
+ const detected = Object.values(ADAPTERS).filter((adapter) => adapter.detect().installed).map((adapter) => ({ id: adapter.id, name: adapter.name }));
4770
+ const pi = detectPiRuntime();
4771
+ if (pi.installed && (includeAvailablePi || hasManagedPiRuntime(flags.targetDir))) {
4772
+ detected.push({ id: "pi", name: "Pi" });
4773
+ }
3917
4774
  if (detected.length === 0) return [];
3918
- if (flags.yes || !process.stdout.isTTY || flags.targetDir !== void 0) return detected.map((a) => a.id);
4775
+ if (flags.yes || !process.stdout.isTTY || flags.targetDir !== void 0) return detected.map((runtime) => runtime.id);
3919
4776
  const choice = await p6.multiselect({
3920
4777
  message: "\xBFEn qu\xE9 runtimes? (detectados en esta m\xE1quina)",
3921
- options: detected.map((a) => ({ value: a.id, label: a.name })),
3922
- initialValues: detected.map((a) => a.id)
4778
+ options: detected.map((runtime) => ({ value: runtime.id, label: runtime.name })),
4779
+ initialValues: detected.map((runtime) => runtime.id)
3923
4780
  });
3924
4781
  if (p6.isCancel(choice)) return null;
3925
4782
  return choice;
3926
4783
  }
4784
+ async function runSelectedPi(operation, targetDir, yes = false) {
4785
+ const detected = detectPiRuntime();
4786
+ if (!detected.installed || detected.executable === null) {
4787
+ console.error("Pi no detectado. Instala el runtime Pi antes de gestionar jorgex-pi.");
4788
+ return 1;
4789
+ }
4790
+ if (detected.version === null) {
4791
+ console.error("No se pudo verificar la versi\xF3n instalada de Pi sin ejecutarlo; revisa la instalaci\xF3n de Pi.");
4792
+ return 1;
4793
+ }
4794
+ let engramBin = resolvePiEngramBin(targetDir);
4795
+ if (operation === "install" && engramBin === null) {
4796
+ const requirement = await resolvePiEngramRequirement({
4797
+ targetDir,
4798
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
4799
+ yes
4800
+ }, {
4801
+ detectHost: () => resolvePiEngramBin(),
4802
+ detectTarget: (root) => resolvePiEngramBin(root),
4803
+ confirm: async ({ message, initialValue }) => {
4804
+ const answer = await p6.confirm({ message, initialValue });
4805
+ return !p6.isCancel(answer) && answer;
4806
+ },
4807
+ installNative: async ({ version }) => updateEngram("Gentleman-Programming/engram", version)
4808
+ });
4809
+ if (requirement.kind !== "existing") {
4810
+ console.error(requirement.kind === "offer" ? "Pi: instalaci\xF3n cancelada; Engram sigue siendo obligatorio." : `Pi: ${requirement.reason}. ${requirement.remedy}`);
4811
+ return 1;
4812
+ }
4813
+ engramBin = requirement.bin;
4814
+ }
4815
+ const result = await runPiRuntimeSystem({
4816
+ operation,
4817
+ targetDir,
4818
+ detected: { executable: detected.executable, version: detected.version },
4819
+ engramBin
4820
+ });
4821
+ if (result.kind === "blocked") {
4822
+ console.error(`Pi: ${result.reason ?? "operaci\xF3n bloqueada"}${result.remedy ? `. ${result.remedy}` : ""}`);
4823
+ return 1;
4824
+ }
4825
+ if (result.kind === "models" && result.models !== void 0) console.log(JSON.stringify(result.models));
4826
+ else p6.log.success(`Pi: ${result.kind}.`);
4827
+ return 0;
4828
+ }
3927
4829
  function printHelp() {
3928
4830
  console.log(`jorgex-stack v${VERSION}
3929
4831
 
@@ -3941,7 +4843,7 @@ Comandos:
3941
4843
  desregistrarlo exige --remove-engram o el s\xED expl\xEDcito
3942
4844
 
3943
4845
  Opciones:
3944
- --agents, -a opencode,claude-code,codex Runtimes destino (default: detectados)
4846
+ --agents, -a opencode,claude-code,codex,pi Runtimes destino (default: detectados)
3945
4847
  --mode human|programmatic Modo de instalaci\xF3n (default: preferencia guardada o human)
3946
4848
  --subagent-concurrency serial|parallel Concurrencia de subagentes en modo programmatic
3947
4849
  --target-dir <dir> Dir alternativo (pruebas de paridad; requiere 1 runtime)
@@ -3988,34 +4890,42 @@ Flags disponibles: jorgex-stack --help`
3988
4890
  switch (command) {
3989
4891
  case "install":
3990
4892
  case "sync": {
3991
- const mode = await resolveInstallMode(flags);
3992
- if (mode === null) return;
3993
- const runtimes = await resolveRuntimes(flags);
4893
+ const runtimes = await resolveRuntimes(flags, command === "install");
3994
4894
  if (runtimes === null) return;
3995
4895
  if (runtimes.length === 0) {
3996
- console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex).");
4896
+ console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex, pi).");
3997
4897
  process.exitCode = 1;
3998
4898
  return;
3999
4899
  }
4000
- const devtoolsMcpSelection = await resolveDevtoolsMcpSelection(command, flags, runtimes);
4001
- if (devtoolsMcpSelection === null) return;
4002
- const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags);
4003
- if (playwrightToolConsent === null) return;
4004
- const hasOpenCodeModels = await ensureOpenCodeModelsForInstall(command, flags, runtimes);
4005
- if (!hasOpenCodeModels) {
4006
- process.exitCode = 1;
4007
- return;
4900
+ const fileRuntimes = runtimes.filter(isFileManagedRuntime);
4901
+ let exitCode = 0;
4902
+ if (fileRuntimes.length > 0) {
4903
+ const mode = await resolveInstallMode(flags);
4904
+ if (mode === null) return;
4905
+ const devtoolsMcpSelection = await resolveDevtoolsMcpSelection(command, flags, fileRuntimes);
4906
+ if (devtoolsMcpSelection === null) return;
4907
+ const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags);
4908
+ if (playwrightToolConsent === null) return;
4909
+ const hasOpenCodeModels = await ensureOpenCodeModelsForInstall(command, flags, fileRuntimes);
4910
+ if (!hasOpenCodeModels) {
4911
+ process.exitCode = 1;
4912
+ return;
4913
+ }
4914
+ exitCode = await runInstall({
4915
+ runtimes: fileRuntimes,
4916
+ targetDir: flags.targetDir,
4917
+ dryRun: flags.dryRun,
4918
+ yes: flags.yes,
4919
+ mode,
4920
+ playwrightToolConsent,
4921
+ devtoolsMcpSelection
4922
+ });
4008
4923
  }
4009
- const installExitCode = await runInstall({
4010
- runtimes,
4011
- targetDir: flags.targetDir,
4012
- dryRun: flags.dryRun,
4013
- yes: flags.yes,
4014
- mode,
4015
- playwrightToolConsent,
4016
- devtoolsMcpSelection
4017
- });
4018
- process.exitCode = installExitCode;
4924
+ if (runtimes.includes("pi")) {
4925
+ if (flags.dryRun) p6.log.info(`Pi: ${command} previsto; dry-run no ejecuta subprocess ni escribe receipt.`);
4926
+ else exitCode = Math.max(exitCode, await runSelectedPi(command, flags.targetDir, flags.yes));
4927
+ }
4928
+ process.exitCode = exitCode;
4019
4929
  return;
4020
4930
  }
4021
4931
  case "uninstall": {
@@ -4026,41 +4936,56 @@ Flags disponibles: jorgex-stack --help`
4026
4936
  process.exitCode = 1;
4027
4937
  return;
4028
4938
  }
4029
- process.exitCode = await runUninstall({
4030
- runtimes,
4939
+ const fileRuntimes = runtimes.filter(isFileManagedRuntime);
4940
+ let exitCode = fileRuntimes.length > 0 || flags.removePlaywright ? await runUninstall({
4941
+ runtimes: fileRuntimes,
4031
4942
  targetDir: flags.targetDir,
4032
4943
  dryRun: flags.dryRun,
4033
4944
  yes: flags.yes,
4034
4945
  removeEngram: flags.removeEngram,
4035
4946
  removePlaywright: flags.removePlaywright
4036
- });
4947
+ }) : 0;
4948
+ if (runtimes.includes("pi")) {
4949
+ if (flags.dryRun) p6.log.info("Pi: uninstall previsto; dry-run conserva paquete y receipt.");
4950
+ else exitCode = Math.max(exitCode, await runSelectedPi("uninstall", flags.targetDir));
4951
+ }
4952
+ process.exitCode = exitCode;
4037
4953
  return;
4038
4954
  }
4039
4955
  case "doctor": {
4040
- process.exitCode = await runDoctor();
4956
+ const fileDoctorSelected = flags.agents.length === 0 || flags.agents.some(isFileManagedRuntime);
4957
+ let exitCode = fileDoctorSelected ? await runDoctor() : 0;
4958
+ const piSelected = flags.agents.includes("pi") || flags.agents.length === 0 && detectPiRuntime().installed && hasManagedPiRuntime(flags.targetDir);
4959
+ if (piSelected) exitCode = Math.max(exitCode, await runSelectedPi("doctor", flags.targetDir));
4960
+ process.exitCode = exitCode;
4041
4961
  return;
4042
4962
  }
4043
4963
  case "update": {
4044
- if (flags.check) {
4045
- process.exitCode = await runUpdateCheck(VERSION, flags.targetDir === void 0);
4046
- return;
4047
- }
4048
- if (flags.dryRun) {
4049
- process.exitCode = await runUpdateCheck(VERSION, flags.targetDir === void 0);
4964
+ if (flags.check || flags.dryRun) {
4965
+ const piExplicit = flags.agents.includes("pi");
4966
+ const fileRuntimeExplicit = flags.agents.some(isFileManagedRuntime);
4967
+ let exitCode = flags.agents.length === 0 || fileRuntimeExplicit ? await runUpdateCheck(VERSION, flags.targetDir === void 0) : 0;
4968
+ if (piExplicit) exitCode = Math.max(exitCode, await runSelectedPi("doctor", flags.targetDir));
4969
+ process.exitCode = exitCode;
4050
4970
  return;
4051
4971
  }
4052
4972
  const runtimes = await resolveRuntimes(flags);
4053
4973
  if (runtimes === null) return;
4974
+ const fileRuntimes = runtimes.filter(isFileManagedRuntime);
4975
+ if (fileRuntimes.length === 0 && runtimes.includes("pi")) {
4976
+ process.exitCode = await runSelectedPi("update", flags.targetDir);
4977
+ return;
4978
+ }
4054
4979
  const preferenceFile = installModePreferenceFile();
4055
4980
  const explicitMode = flags.mode !== void 0 || flags.subagentConcurrency !== void 0;
4056
4981
  const hasSavedMode = hasInstallModePreference(preferenceFile);
4057
4982
  const canResolveMode = flags.targetDir !== void 0 || explicitMode || hasSavedMode;
4058
- const mode = runtimes.length > 0 && canResolveMode ? await resolveInstallMode(flags, false) : DEFAULT_INSTALL_MODE_PREFERENCE;
4983
+ const mode = fileRuntimes.length > 0 && canResolveMode ? await resolveInstallMode(flags, false) : DEFAULT_INSTALL_MODE_PREFERENCE;
4059
4984
  if (mode === null) return;
4060
- const canSync = runtimes.length === 0 || canResolveMode;
4061
- if (runtimes.length > 0 && canSync) {
4985
+ const canSync = fileRuntimes.length === 0 || canResolveMode;
4986
+ if (fileRuntimes.length > 0 && canSync) {
4062
4987
  const code = await runInstall({
4063
- runtimes,
4988
+ runtimes: fileRuntimes,
4064
4989
  targetDir: flags.targetDir,
4065
4990
  dryRun: flags.dryRun,
4066
4991
  yes: true,
@@ -4070,7 +4995,7 @@ Flags disponibles: jorgex-stack --help`
4070
4995
  process.exitCode = code;
4071
4996
  return;
4072
4997
  }
4073
- } else if (runtimes.length > 0) {
4998
+ } else if (fileRuntimes.length > 0) {
4074
4999
  console.error("No hay modo guardado; se omite el sync previo y se contin\xFAa con update. Usa --mode expl\xEDcito si quieres sincronizar.");
4075
5000
  }
4076
5001
  const result = await runInteractiveUpdate(
@@ -4080,13 +5005,16 @@ Flags disponibles: jorgex-stack --help`
4080
5005
  flags.targetDir === void 0
4081
5006
  );
4082
5007
  process.exitCode = result.exitCode;
4083
- if (result.syncRequired && runtimes.length > 0 && (result.exitCode !== 0 || !canSync)) {
5008
+ if (result.exitCode === 0 && runtimes.includes("pi")) {
5009
+ process.exitCode = Math.max(process.exitCode, await runSelectedPi("update", flags.targetDir));
5010
+ }
5011
+ if (result.syncRequired && fileRuntimes.length > 0 && (result.exitCode !== 0 || !canSync)) {
4084
5012
  p6.log.warn("Skills/stack actualizados, pero el sync con los runtimes sigue pendiente. Ejecuta jorgex-stack sync --mode human|programmatic.");
4085
- } else if (result.exitCode === 0 && result.syncRequired && runtimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
5013
+ } else if (result.exitCode === 0 && result.syncRequired && fileRuntimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
4086
5014
  const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
4087
5015
  if (!p6.isCancel(apply) && apply) {
4088
5016
  process.exitCode = await runInstall({
4089
- runtimes,
5017
+ runtimes: fileRuntimes,
4090
5018
  targetDir: flags.targetDir,
4091
5019
  dryRun: false,
4092
5020
  yes: false,
@@ -4095,7 +5023,7 @@ Flags disponibles: jorgex-stack --help`
4095
5023
  } else {
4096
5024
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
4097
5025
  }
4098
- } else if (result.exitCode === 0 && result.syncRequired && runtimes.length > 0 && canSync && (flags.yes || !process.stdout.isTTY)) {
5026
+ } else if (result.exitCode === 0 && result.syncRequired && fileRuntimes.length > 0 && canSync && (flags.yes || !process.stdout.isTTY)) {
4099
5027
  console.log("Skills/stack actualizados. Ejecuta jorgex-stack sync para aplicarlos a los runtimes.");
4100
5028
  }
4101
5029
  return;
@@ -4104,13 +5032,15 @@ Flags disponibles: jorgex-stack --help`
4104
5032
  const runtimes = await resolveRuntimes(flags);
4105
5033
  if (runtimes === null) return;
4106
5034
  if (runtimes.length === 0) {
4107
- console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex).");
5035
+ console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex, pi).");
4108
5036
  process.exitCode = 1;
4109
5037
  return;
4110
5038
  }
4111
- const code = await runModelsPicker({ yes: flags.yes, runtimes });
5039
+ const fileRuntimes = runtimes.filter(isFileManagedRuntime);
5040
+ let code = fileRuntimes.length > 0 ? await runModelsPicker({ yes: flags.yes, runtimes: fileRuntimes }) : 0;
5041
+ if (runtimes.includes("pi")) code = Math.max(code, await runSelectedPi("models", flags.targetDir));
4112
5042
  process.exitCode = code;
4113
- if (code === 0 && !flags.yes && process.stdout.isTTY) {
5043
+ if (code === 0 && fileRuntimes.length > 0 && !flags.yes && process.stdout.isTTY) {
4114
5044
  const apply = await p6.confirm({ message: "\xBFAplicar ahora los modelos a los agentes instalados? (sync)" });
4115
5045
  if (!p6.isCancel(apply) && apply) {
4116
5046
  const preferenceFile = installModePreferenceFile();
@@ -4123,7 +5053,7 @@ Flags disponibles: jorgex-stack --help`
4123
5053
  }
4124
5054
  const mode = await resolveInstallMode(flags, false);
4125
5055
  if (mode === null) return;
4126
- process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: false, mode });
5056
+ process.exitCode = await runInstall({ runtimes: fileRuntimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: false, mode });
4127
5057
  } else {
4128
5058
  console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
4129
5059
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI y OpenCode",
5
5
  "type": "module",
6
6
  "license": "MIT",