jorgex-stack 1.0.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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/PRD.md +297 -0
  3. package/README.md +58 -0
  4. package/dist/cli.js +2894 -0
  5. package/package.json +51 -0
  6. package/stack/agents/README.md +33 -0
  7. package/stack/agents/backend-analyst.md +61 -0
  8. package/stack/agents/code-reviewer.md +66 -0
  9. package/stack/agents/code-simplifier.md +81 -0
  10. package/stack/agents/comment-fixer.md +54 -0
  11. package/stack/agents/docs-maintainer.md +88 -0
  12. package/stack/agents/engram.md +124 -0
  13. package/stack/agents/frontend-analyst.md +51 -0
  14. package/stack/agents/implementer.md +55 -0
  15. package/stack/agents/orchestrator.md +192 -0
  16. package/stack/agents/security-auditor.md +66 -0
  17. package/stack/agents/silent-failure-hunter.md +167 -0
  18. package/stack/agents/test-analyzer.md +91 -0
  19. package/stack/agents/tester.md +71 -0
  20. package/stack/agents/translator.md +101 -0
  21. package/stack/agents/type-design-analyzer.md +128 -0
  22. package/stack/commands/xreview.md +80 -0
  23. package/stack/config/defaults.json +37 -0
  24. package/stack/hooks/hooks.json +18 -0
  25. package/stack/mcp/servers.json +19 -0
  26. package/stack/plugins/opencode/engram.ts +378 -0
  27. package/stack/plugins/opencode/hooks.ts +766 -0
  28. package/stack/plugins/opencode/package.json +10 -0
  29. package/stack/plugins/opencode/worktree.ts +405 -0
  30. package/stack/scripts/post-pr-review.cjs +156 -0
  31. package/stack/skills/agent-browser/SKILL.md +55 -0
  32. package/stack/skills/agent-delegation/SKILL.md +58 -0
  33. package/stack/skills/deploy-to-vercel/SKILL.md +296 -0
  34. package/stack/skills/deploy-to-vercel/resources/deploy-codex.sh +301 -0
  35. package/stack/skills/deploy-to-vercel/resources/deploy.sh +301 -0
  36. package/stack/skills/diagnose/SKILL.md +117 -0
  37. package/stack/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  38. package/stack/skills/find-skills/SKILL.md +133 -0
  39. package/stack/skills/graphify/.graphify_version +1 -0
  40. package/stack/skills/graphify/SKILL.md +1319 -0
  41. package/stack/skills/mcp-builder/LICENSE.txt +202 -0
  42. package/stack/skills/mcp-builder/SKILL.md +236 -0
  43. package/stack/skills/mcp-builder/reference/evaluation.md +602 -0
  44. package/stack/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  45. package/stack/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  46. package/stack/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  47. package/stack/skills/mcp-builder/scripts/connections.py +151 -0
  48. package/stack/skills/mcp-builder/scripts/evaluation.py +373 -0
  49. package/stack/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  50. package/stack/skills/mcp-builder/scripts/requirements.txt +2 -0
  51. package/stack/skills/obsidian-cli/SKILL.md +106 -0
  52. package/stack/skills/obsidian-markdown/SKILL.md +196 -0
  53. package/stack/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
  54. package/stack/skills/obsidian-markdown/references/EMBEDS.md +63 -0
  55. package/stack/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
  56. package/stack/skills/react-doctor/SKILL.md +19 -0
  57. package/stack/skills/skill-creator/LICENSE.txt +202 -0
  58. package/stack/skills/skill-creator/SKILL.md +485 -0
  59. package/stack/skills/skill-creator/agents/analyzer.md +274 -0
  60. package/stack/skills/skill-creator/agents/comparator.md +202 -0
  61. package/stack/skills/skill-creator/agents/grader.md +223 -0
  62. package/stack/skills/skill-creator/assets/eval_review.html +146 -0
  63. package/stack/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  64. package/stack/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  65. package/stack/skills/skill-creator/references/schemas.md +430 -0
  66. package/stack/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  67. package/stack/skills/skill-creator/scripts/generate_report.py +326 -0
  68. package/stack/skills/skill-creator/scripts/improve_description.py +248 -0
  69. package/stack/skills/skill-creator/scripts/package_skill.py +136 -0
  70. package/stack/skills/skill-creator/scripts/quick_validate.py +103 -0
  71. package/stack/skills/skill-creator/scripts/run_eval.py +310 -0
  72. package/stack/skills/skill-creator/scripts/run_loop.py +332 -0
  73. package/stack/skills/skill-creator/scripts/utils.py +47 -0
  74. package/stack/skills/supabase/SKILL.md +135 -0
  75. package/stack/skills/supabase/assets/feedback-issue-template.md +17 -0
  76. package/stack/skills/supabase/references/skill-feedback.md +17 -0
  77. package/stack/skills/supabase-postgres-best-practices/SKILL.md +64 -0
  78. package/stack/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
  79. package/stack/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
  80. package/stack/skills/supabase-postgres-best-practices/references/_template.md +34 -0
  81. package/stack/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
  82. package/stack/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
  83. package/stack/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
  84. package/stack/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
  85. package/stack/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
  86. package/stack/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
  87. package/stack/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
  88. package/stack/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
  89. package/stack/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
  90. package/stack/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
  91. package/stack/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
  92. package/stack/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
  93. package/stack/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
  94. package/stack/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
  95. package/stack/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
  96. package/stack/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
  97. package/stack/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
  98. package/stack/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
  99. package/stack/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
  100. package/stack/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
  101. package/stack/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
  102. package/stack/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
  103. package/stack/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
  104. package/stack/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
  105. package/stack/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
  106. package/stack/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
  107. package/stack/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
  108. package/stack/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
  109. package/stack/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
  110. package/stack/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
  111. package/stack/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
  112. package/stack/skills/tdd/SKILL.md +109 -0
  113. package/stack/skills/tdd/deep-modules.md +33 -0
  114. package/stack/skills/tdd/interface-design.md +31 -0
  115. package/stack/skills/tdd/mocking.md +59 -0
  116. package/stack/skills/tdd/refactoring.md +10 -0
  117. package/stack/skills/tdd/tests.md +61 -0
  118. package/stack/skills/to-issues/SKILL.md +83 -0
  119. package/stack/skills/to-prd/SKILL.md +72 -0
  120. package/stack/skills/work-lifecycle/SKILL.md +79 -0
  121. package/stack/skills/work-lifecycle/references/plan-template.md +185 -0
  122. package/stack/system-prompt/AGENTS.md +171 -0
  123. package/stack/system-prompt/engram-protocol.md +53 -0
  124. package/upstreams.json +96 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2894 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import * as p6 from "@clack/prompts";
5
+
6
+ // src/install.ts
7
+ import fs11 from "fs";
8
+ import path17 from "path";
9
+ import * as p from "@clack/prompts";
10
+
11
+ // src/adapters/opencode.ts
12
+ import path7 from "path";
13
+ import fs3 from "fs";
14
+ import { pathToFileURL } from "url";
15
+
16
+ // src/lib/canonical.ts
17
+ import path from "path";
18
+ import fs from "fs";
19
+ function parseCanonicalAgent(source, file) {
20
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(source);
21
+ if (!match) throw new Error(`Agente sin frontmatter: ${file}`);
22
+ const fields = {};
23
+ for (const line of match[1].split(/\r?\n/)) {
24
+ const sep = line.indexOf(":");
25
+ if (sep === -1) continue;
26
+ fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
27
+ }
28
+ const required = ["name", "description", "mode", "tier"];
29
+ for (const key of required) {
30
+ if (!fields[key]) throw new Error(`Agente ${file}: falta '${key}' en el frontmatter`);
31
+ }
32
+ return {
33
+ name: fields.name,
34
+ description: fields.description,
35
+ mode: fields.mode === "primary" ? "primary" : "subagent",
36
+ tier: fields.tier,
37
+ readonly: fields.readonly === "true",
38
+ bash: fields.bash ?? "full",
39
+ spawn: fields.spawn !== "false",
40
+ body: source.slice(match[0].length)
41
+ };
42
+ }
43
+ function loadCanonicalAgents(agentsDir) {
44
+ return fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md").map((f) => {
45
+ const file = path.join(agentsDir, f);
46
+ const source = fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
47
+ return parseCanonicalAgent(source, file);
48
+ });
49
+ }
50
+ function loadCanonicalMcp(stackDir) {
51
+ return JSON.parse(fs.readFileSync(path.join(stackDir, "mcp", "servers.json"), "utf8"));
52
+ }
53
+ function loadCanonicalHooks(stackDir) {
54
+ return JSON.parse(fs.readFileSync(path.join(stackDir, "hooks", "hooks.json"), "utf8"));
55
+ }
56
+ function loadCanonicalDefaults(stackDir) {
57
+ const file = path.join(stackDir, "config", "defaults.json");
58
+ if (!fs.existsSync(file)) return {};
59
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
60
+ delete parsed["$comment"];
61
+ return parsed;
62
+ }
63
+
64
+ // src/lib/model-map.ts
65
+ import path4 from "path";
66
+ import { existsSync as existsSync2 } from "fs";
67
+
68
+ // src/lib/paths.ts
69
+ import os from "os";
70
+ import path2 from "path";
71
+ import { existsSync } from "fs";
72
+ import { fileURLToPath } from "url";
73
+ var HOME = os.homedir();
74
+ function stackRoot() {
75
+ let dir = path2.dirname(fileURLToPath(import.meta.url));
76
+ for (let i = 0; i < 6; i++) {
77
+ const candidate = path2.join(dir, "stack");
78
+ if (existsSync(path2.join(candidate, "system-prompt", "AGENTS.md"))) return candidate;
79
+ dir = path2.dirname(dir);
80
+ }
81
+ throw new Error("No se encontr\xF3 stack/ relativa al CLI \u2014 instalaci\xF3n rota.");
82
+ }
83
+ function dataDir() {
84
+ return path2.join(HOME, ".jorgex-stack");
85
+ }
86
+ function samePath(a, b) {
87
+ const resolvedA = path2.resolve(a);
88
+ const resolvedB = path2.resolve(b);
89
+ if (process.platform === "win32") return resolvedA.toLowerCase() === resolvedB.toLowerCase();
90
+ return resolvedA === resolvedB;
91
+ }
92
+
93
+ // src/lib/fsx.ts
94
+ import fs2 from "fs";
95
+ import path3 from "path";
96
+ function readTextIfExists(file) {
97
+ try {
98
+ return fs2.readFileSync(file, "utf8");
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function ensureDir(dir) {
104
+ fs2.mkdirSync(dir, { recursive: true });
105
+ }
106
+ var tmpCounter = 0;
107
+ function tmpPath(target) {
108
+ return path3.join(path3.dirname(target), `.jorgex-${process.pid}-${tmpCounter++}.tmp`);
109
+ }
110
+ function renameInto(tmp, target) {
111
+ try {
112
+ fs2.renameSync(tmp, target);
113
+ } catch (err) {
114
+ fs2.rmSync(tmp, { force: true });
115
+ throw err;
116
+ }
117
+ }
118
+ function writeText(file, content) {
119
+ ensureDir(path3.dirname(file));
120
+ const tmp = tmpPath(file);
121
+ fs2.writeFileSync(tmp, content, "utf8");
122
+ renameInto(tmp, file);
123
+ }
124
+ function copyFile(source, target) {
125
+ ensureDir(path3.dirname(target));
126
+ const tmp = tmpPath(target);
127
+ fs2.copyFileSync(source, tmp);
128
+ renameInto(tmp, target);
129
+ }
130
+ function isContainedIn(child, root) {
131
+ const rel = path3.relative(path3.resolve(root), path3.resolve(child));
132
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${path3.sep}`) && !path3.isAbsolute(rel);
133
+ }
134
+ function pruneEmptyDirs(file, root) {
135
+ let dir = path3.dirname(file);
136
+ while (dir.startsWith(root) && dir !== root) {
137
+ try {
138
+ if (fs2.readdirSync(dir).length > 0) return;
139
+ fs2.rmdirSync(dir);
140
+ } catch {
141
+ return;
142
+ }
143
+ dir = path3.dirname(dir);
144
+ }
145
+ }
146
+ function sameFileContent(a, b) {
147
+ try {
148
+ const bufA = fs2.readFileSync(a);
149
+ const bufB = fs2.readFileSync(b);
150
+ return bufA.equals(bufB);
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+ function listFilesRecursive(dir) {
156
+ const out = [];
157
+ if (!fs2.existsSync(dir)) return out;
158
+ const walk = (d) => {
159
+ for (const entry of fs2.readdirSync(d, { withFileTypes: true })) {
160
+ const p7 = path3.join(d, entry.name);
161
+ if (entry.isDirectory()) walk(p7);
162
+ else out.push(p7);
163
+ }
164
+ };
165
+ walk(dir);
166
+ return out;
167
+ }
168
+
169
+ // src/lib/model-map.ts
170
+ function resolveAgentModel(models, agentName, tier) {
171
+ const base = models[tier];
172
+ const override = models.overrides?.[agentName];
173
+ if (!override) return base;
174
+ return {
175
+ model: override.model || base.model,
176
+ variant: "variant" in override ? override.variant || void 0 : base.variant
177
+ };
178
+ }
179
+ var DEFAULT_MODEL_MAP = {
180
+ opencode: {
181
+ strong: { model: "openai/gpt-5.4", variant: "high" },
182
+ standard: { model: "openai/gpt-5.4-mini", variant: "high" },
183
+ cheap: { model: "minimax/MiniMax-M3" }
184
+ },
185
+ "claude-code": {
186
+ strong: { model: "fable" },
187
+ standard: { model: "sonnet" },
188
+ cheap: { model: "haiku" }
189
+ },
190
+ // Codex: "default" = no fijar modelo (usa el del CLI, que OpenAI actualiza
191
+ // solo — D6); variant → model_reasoning_effort.
192
+ codex: {
193
+ strong: { model: "default", variant: "high" },
194
+ standard: { model: "default", variant: "medium" },
195
+ cheap: { model: "default", variant: "low" }
196
+ }
197
+ };
198
+ function modelMapFile() {
199
+ return path4.join(dataDir(), "model-map.json");
200
+ }
201
+ function loadModelMap() {
202
+ const raw = readTextIfExists(modelMapFile());
203
+ if (raw === null) return DEFAULT_MODEL_MAP;
204
+ let fromDisk;
205
+ try {
206
+ fromDisk = JSON.parse(raw);
207
+ } catch {
208
+ return DEFAULT_MODEL_MAP;
209
+ }
210
+ const merged = {};
211
+ for (const id of Object.keys(DEFAULT_MODEL_MAP)) {
212
+ merged[id] = { ...DEFAULT_MODEL_MAP[id], ...fromDisk[id] ?? {} };
213
+ }
214
+ return merged;
215
+ }
216
+ function ensureModelMapFile() {
217
+ const file = modelMapFile();
218
+ if (!existsSync2(file)) {
219
+ writeText(file, JSON.stringify(DEFAULT_MODEL_MAP, null, 2) + "\n");
220
+ }
221
+ return file;
222
+ }
223
+
224
+ // src/lib/detect.ts
225
+ import path5 from "path";
226
+ import { existsSync as existsSync3, statSync } from "fs";
227
+ import { execFileSync, execSync } from "child_process";
228
+ function lookPath(cmd) {
229
+ const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ".ps1", ""] : [""];
230
+ for (const dir of (process.env.PATH ?? "").split(path5.delimiter)) {
231
+ if (!dir) continue;
232
+ for (const ext of exts) {
233
+ const candidate = path5.join(dir, cmd + ext);
234
+ if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
235
+ }
236
+ }
237
+ return null;
238
+ }
239
+ function runDetectedBin(bin, args, timeoutMs) {
240
+ try {
241
+ if (process.platform === "win32" && /\.(cmd|bat)$/i.test(bin)) {
242
+ if (/[&|<>^%!"]/.test(bin)) return null;
243
+ return execSync(`"${bin}" ${args.join(" ")}`, {
244
+ encoding: "utf8",
245
+ timeout: timeoutMs,
246
+ stdio: ["ignore", "pipe", "pipe"]
247
+ });
248
+ }
249
+ return execFileSync(bin, args, { encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"] });
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ function detectOpenCode() {
255
+ const configDir = process.env.OPENCODE_CONFIG_DIR ?? path5.join(HOME, ".config", "opencode");
256
+ const binPath = lookPath("opencode");
257
+ return {
258
+ id: "opencode",
259
+ name: "OpenCode",
260
+ installed: binPath !== null || existsSync3(configDir),
261
+ binPath,
262
+ configDir
263
+ };
264
+ }
265
+ function detectClaudeCode() {
266
+ const configDir = path5.join(HOME, ".claude");
267
+ const binPath = lookPath("claude");
268
+ return {
269
+ id: "claude-code",
270
+ name: "Claude Code",
271
+ installed: binPath !== null || existsSync3(configDir),
272
+ binPath,
273
+ configDir
274
+ };
275
+ }
276
+ function detectCodex() {
277
+ const configDir = process.env.CODEX_HOME ?? path5.join(HOME, ".codex");
278
+ const binPath = lookPath("codex");
279
+ return {
280
+ id: "codex",
281
+ name: "Codex CLI",
282
+ installed: binPath !== null || existsSync3(configDir),
283
+ binPath,
284
+ configDir
285
+ };
286
+ }
287
+ function detectEngram() {
288
+ const fromEnv = process.env.ENGRAM_BIN;
289
+ if (fromEnv && existsSync3(fromEnv)) return fromEnv;
290
+ const onPath = lookPath("engram");
291
+ if (onPath) return onPath;
292
+ const candidates = [
293
+ path5.join(HOME, "go", "bin", "engram.exe"),
294
+ path5.join(HOME, "go", "bin", "engram"),
295
+ path5.join(HOME, ".local", "bin", "engram.exe"),
296
+ path5.join(HOME, ".local", "bin", "engram")
297
+ ];
298
+ for (const c of candidates) if (existsSync3(c)) return c;
299
+ return null;
300
+ }
301
+
302
+ // src/lib/filemerge.ts
303
+ function markers(name) {
304
+ return { open: `<!-- jorgex:${name} -->`, close: `<!-- /jorgex:${name} -->` };
305
+ }
306
+ function repairOrphanMarkers(existing, name) {
307
+ const { open, close } = markers(name);
308
+ const count = (marker) => existing.split(marker).length - 1;
309
+ const opens = count(open);
310
+ const closes = count(close);
311
+ if (opens === 0 && closes === 0) return existing;
312
+ const onOwnLine = (marker) => existing.split("\n").some((line) => line.trim() === marker);
313
+ const healthy = opens === 1 && closes === 1 && existing.indexOf(close) > existing.indexOf(open) && onOwnLine(open) && onOwnLine(close);
314
+ if (healthy) return existing;
315
+ return existing.split("\n").map((line) => line.trim() === open || line.trim() === close ? null : line.split(open).join("").split(close).join("")).filter((line) => line !== null).join("\n");
316
+ }
317
+ function upsertMarkdownSection(existing, name, content) {
318
+ const { open, close } = markers(name);
319
+ const block = `${open}
320
+ ${content.trim()}
321
+ ${close}`;
322
+ if (existing === null || existing.trim() === "") return block + "\n";
323
+ existing = repairOrphanMarkers(existing, name);
324
+ const start = existing.indexOf(open);
325
+ const end = existing.indexOf(close);
326
+ if (start !== -1 && end !== -1 && end > start) {
327
+ return existing.slice(0, start) + block + existing.slice(end + close.length);
328
+ }
329
+ const sep = existing.endsWith("\n") ? "\n" : "\n\n";
330
+ return existing + sep + block + "\n";
331
+ }
332
+ function removeMarkdownSection(existing, name) {
333
+ existing = repairOrphanMarkers(existing, name);
334
+ const { open, close } = markers(name);
335
+ const start = existing.indexOf(open);
336
+ const end = existing.indexOf(close);
337
+ if (start === -1 || end === -1 || end < start) return existing;
338
+ const before = existing.slice(0, start).replace(/\n+$/, "\n");
339
+ const after = existing.slice(end + close.length).replace(/^\n+/, "\n");
340
+ const joined = before + after;
341
+ return joined.trim() === "" ? "" : joined.replace(/^\n+/, "");
342
+ }
343
+ function stripLeadingHtmlComments(md) {
344
+ let out = md.trimStart();
345
+ while (out.startsWith("<!--")) {
346
+ const end = out.indexOf("-->");
347
+ if (end === -1) break;
348
+ out = out.slice(end + 3).trimStart();
349
+ }
350
+ return out;
351
+ }
352
+ function upsertJson(existing, mutate) {
353
+ let root = {};
354
+ if (existing !== null && existing.trim() !== "") {
355
+ root = JSON.parse(existing);
356
+ }
357
+ mutate(root);
358
+ return JSON.stringify(root, null, 2) + "\n";
359
+ }
360
+ function headerName(line) {
361
+ const match = /^\s*\[\s*([^\]]+?)\s*\]\s*(#.*)?$/.exec(line);
362
+ if (!match) return null;
363
+ return match[1].split(".").map((part) => part.trim().replace(/^["']|["']$/g, "")).join(".");
364
+ }
365
+ function multilineStringMask(lines) {
366
+ const mask = [];
367
+ let inside = null;
368
+ for (const line of lines) {
369
+ if (inside !== null) {
370
+ mask.push(true);
371
+ if (line.includes(inside)) inside = null;
372
+ continue;
373
+ }
374
+ mask.push(false);
375
+ for (const delim of ["'''", '"""']) {
376
+ if ((line.split(delim).length - 1) % 2 === 1) {
377
+ inside = delim;
378
+ break;
379
+ }
380
+ }
381
+ }
382
+ return mask;
383
+ }
384
+ function findTomlSection(lines, section) {
385
+ const mask = multilineStringMask(lines);
386
+ const start = lines.findIndex((line, i) => !mask[i] && headerName(line) === section);
387
+ if (start === -1) return null;
388
+ let end = start + 1;
389
+ while (end < lines.length && (mask[end] || headerName(lines[end]) === null)) end++;
390
+ return { start, end };
391
+ }
392
+ function upsertTomlSection(existing, section, body) {
393
+ const header = `[${section}]`;
394
+ const block = `${header}
395
+ ${body.trim()}
396
+ `;
397
+ if (existing === null || existing.trim() === "") return block;
398
+ const lines = existing.split(/\r?\n/);
399
+ const found = findTomlSection(lines, section);
400
+ if (found === null) {
401
+ const sep = existing.endsWith("\n") ? "\n" : "\n\n";
402
+ return existing + sep + block;
403
+ }
404
+ let sectionEnd = found.end;
405
+ while (sectionEnd > found.start + 1 && lines[sectionEnd - 1].trim() === "") sectionEnd--;
406
+ const result = [...lines.slice(0, found.start), block.trimEnd(), ...lines.slice(sectionEnd)].join("\n");
407
+ return result.endsWith("\n") ? result : result + "\n";
408
+ }
409
+ function removeTomlSection(existing, section) {
410
+ const lines = existing.split(/\r?\n/);
411
+ const found = findTomlSection(lines, section);
412
+ if (found === null) return existing;
413
+ let realStart = found.start;
414
+ while (realStart > 0 && lines[realStart - 1].trim() === "") realStart--;
415
+ const result = [...lines.slice(0, realStart), ...lines.slice(found.end)].join("\n");
416
+ if (result.trim() === "") return "";
417
+ return result.endsWith("\n") ? result : result + "\n";
418
+ }
419
+ function readTomlSection(existing, section) {
420
+ if (existing === null) return null;
421
+ const lines = existing.split(/\r?\n/);
422
+ const found = findTomlSection(lines, section);
423
+ if (found === null) return null;
424
+ return lines.slice(found.start + 1, found.end).join("\n");
425
+ }
426
+
427
+ // src/lib/hooks-format.ts
428
+ import path6 from "path";
429
+ function hookScriptNames(canonical) {
430
+ return Object.values(canonical.hooks).flat().flatMap((entry) => entry.hooks).map((h) => /\{\{SCRIPTS_DIR\}\}[/\\]([\w./-]+)/.exec(h.command)?.[1]).filter((s) => s !== void 0).map((s) => path6.basename(s));
431
+ }
432
+ function plainHookCommands(canonical) {
433
+ return new Set(
434
+ Object.values(canonical.hooks).flat().flatMap((entry) => entry.hooks).map((h) => h.command).filter((c) => !c.includes("{{SCRIPTS_DIR}}"))
435
+ );
436
+ }
437
+ function removeNativeHooks(existing, canonical) {
438
+ if (existing === null || existing.trim() === "") return existing;
439
+ const scriptNames = hookScriptNames(canonical);
440
+ const plainCommands = plainHookCommands(canonical);
441
+ return upsertJson(existing, (root) => {
442
+ const hooks = root["hooks"];
443
+ if (!hooks) return;
444
+ for (const event of Object.keys(hooks)) {
445
+ const list = hooks[event];
446
+ if (!Array.isArray(list)) continue;
447
+ hooks[event] = list.filter((entry) => {
448
+ const e = entry;
449
+ if (!Array.isArray(e?.hooks)) return true;
450
+ const ours = e.hooks.some((hh) => {
451
+ const cmd = String(hh?.command ?? "");
452
+ return scriptNames.some((s) => cmd.includes(s)) || plainCommands.has(cmd);
453
+ });
454
+ return !ours;
455
+ });
456
+ if (hooks[event].length === 0) delete hooks[event];
457
+ }
458
+ if (Object.keys(hooks).length === 0) delete root["hooks"];
459
+ });
460
+ }
461
+ function upsertNativeHooks(existing, canonical, scriptsDir, mapMatcher = (m) => m) {
462
+ const scriptsDirForCommand = scriptsDir.replace(/\\/g, "/");
463
+ return upsertJson(existing, (root) => {
464
+ const hooks = root["hooks"] ??= {};
465
+ for (const [event, entries] of Object.entries(canonical.hooks)) {
466
+ const list = hooks[event] ??= [];
467
+ for (const entry of entries) {
468
+ const matcher = entry.matcher !== void 0 ? mapMatcher(entry.matcher) : void 0;
469
+ const rendered = {
470
+ ...matcher !== void 0 ? { matcher } : {},
471
+ hooks: entry.hooks.map((h) => ({
472
+ type: h.type,
473
+ command: h.command.replace(/\{\{SCRIPTS_DIR\}\}/g, scriptsDirForCommand),
474
+ ...h.timeout !== void 0 ? { timeout: h.timeout } : {}
475
+ }))
476
+ };
477
+ const scriptNames = entry.hooks.map((h) => /\{\{SCRIPTS_DIR\}\}[/\\]([\w./-]+)/.exec(h.command)?.[1]).filter((s) => s !== void 0).map((s) => path6.basename(s));
478
+ const plainCommands = entry.hooks.map((h) => h.command).filter((c) => !c.includes("{{SCRIPTS_DIR}}"));
479
+ const index = list.findIndex((existingEntry) => {
480
+ const e = existingEntry;
481
+ if (!Array.isArray(e?.hooks)) return false;
482
+ return e.hooks.some((hh) => {
483
+ const cmd = String(hh?.command ?? "");
484
+ return scriptNames.some((s) => cmd.includes(s)) || plainCommands.includes(cmd);
485
+ });
486
+ });
487
+ if (index >= 0) list[index] = rendered;
488
+ else list.push(rendered);
489
+ }
490
+ }
491
+ });
492
+ }
493
+
494
+ // src/adapters/opencode.ts
495
+ function yamlString(value) {
496
+ return JSON.stringify(value);
497
+ }
498
+ var opencodeAdapter = {
499
+ id: "opencode",
500
+ name: "OpenCode",
501
+ detect: detectOpenCode,
502
+ injectEngramProtocol() {
503
+ return false;
504
+ },
505
+ paths(configDir) {
506
+ const isRealConfigDir = samePath(
507
+ configDir,
508
+ process.env.OPENCODE_CONFIG_DIR ?? path7.join(HOME, ".config", "opencode")
509
+ );
510
+ const agentsHome = isRealConfigDir ? HOME : path7.dirname(configDir);
511
+ return {
512
+ systemPromptFile: path7.join(configDir, "AGENTS.md"),
513
+ agentsDir: path7.join(configDir, "agents"),
514
+ skillsDir: path7.join(agentsHome, ".agents", "skills"),
515
+ commandsDir: path7.join(configDir, "commands"),
516
+ pluginsDir: path7.join(configDir, "plugins"),
517
+ scriptsDir: path7.join(configDir, "scripts"),
518
+ outputStylesDir: null,
519
+ profilesDir: null
520
+ };
521
+ },
522
+ renderAgent(agent, models) {
523
+ const lines = [`description: ${yamlString(agent.description)}`, `mode: ${agent.mode}`];
524
+ if (agent.mode === "subagent") {
525
+ const tierModel = resolveAgentModel(models, agent.name, agent.tier);
526
+ lines.push(`model: ${tierModel.model}`);
527
+ if (tierModel.variant) lines.push(`variant: ${tierModel.variant}`);
528
+ lines.push("permission:");
529
+ lines.push(` edit: ${agent.readonly ? "deny" : "allow"}`);
530
+ if (agent.bash === "none") lines.push(" bash: deny");
531
+ else if (agent.bash === "git-read") lines.push(' bash:\n "git diff*": allow\n "git log*": allow');
532
+ else lines.push(" bash: allow");
533
+ if (!agent.spawn) lines.push(" task: deny");
534
+ lines.push("tools:");
535
+ lines.push(` write: ${agent.readonly ? "false" : "true"}`);
536
+ }
537
+ return [
538
+ {
539
+ file: `${agent.name}.md`,
540
+ content: `---
541
+ ${lines.join("\n")}
542
+ ---
543
+ ${agent.body}`,
544
+ kind: "agent"
545
+ }
546
+ ];
547
+ },
548
+ renderCommand(file, content) {
549
+ return { file, content: content.replace(/\{\{input\}\}/g, "$ARGUMENTS") };
550
+ },
551
+ planHooks(canonical, ctx) {
552
+ const actions = [];
553
+ const { scriptsDir } = this.paths(ctx.configDir);
554
+ const bashEntries = {};
555
+ for (const [event, entries] of Object.entries(canonical.hooks)) {
556
+ if (event !== "PostToolUse") {
557
+ ctx.warnings.push(`opencode: evento de hook '${event}' a\xFAn no soportado por el puente \u2014 omitido.`);
558
+ continue;
559
+ }
560
+ for (const entry of entries) {
561
+ if (!(entry.matcher ?? "").split("|").some((p7) => p7.trim().toLowerCase() === "bash")) {
562
+ ctx.warnings.push(`opencode: matcher de hook '${entry.matcher}' no soportado \u2014 omitido.`);
563
+ continue;
564
+ }
565
+ const includes = entry["x-command-includes"] ?? "*";
566
+ for (const hook of entry.hooks) {
567
+ const match = /\{\{SCRIPTS_DIR\}\}[/\\]([\w./\\-]+)/.exec(hook.command);
568
+ if (!match) {
569
+ ctx.warnings.push(`opencode: hook sin {{SCRIPTS_DIR}} no traducible: ${hook.command}`);
570
+ continue;
571
+ }
572
+ const script = `scripts/${path7.basename(match[1])}`;
573
+ (bashEntries[includes] ??= []).push(script);
574
+ }
575
+ }
576
+ }
577
+ const hooksFile = path7.join(ctx.configDir, "hooks.json");
578
+ const content = upsertJson(readTextIfExists(hooksFile), (root) => {
579
+ const after = root["tool.execute.after"] ??= {};
580
+ const bash = after["bash"] ??= {};
581
+ for (const [includes, scripts] of Object.entries(bashEntries)) {
582
+ const list = bash[includes] ??= [];
583
+ for (const s of scripts) if (!list.includes(s)) list.push(s);
584
+ }
585
+ });
586
+ actions.push({ kind: "write", target: hooksFile, content });
587
+ const scriptsSource = path7.join(ctx.stackDir, "scripts");
588
+ if (fs3.existsSync(scriptsSource)) {
589
+ for (const f of fs3.readdirSync(scriptsSource)) {
590
+ actions.push({ kind: "copy", source: path7.join(scriptsSource, f), target: path7.join(scriptsDir, f) });
591
+ }
592
+ }
593
+ return actions;
594
+ },
595
+ planMainConfig(canonical, ctx) {
596
+ const file = path7.join(ctx.configDir, "opencode.json");
597
+ const { pluginsDir } = this.paths(ctx.configDir);
598
+ const content = upsertJson(readTextIfExists(file), (root) => {
599
+ root["$schema"] ??= "https://opencode.ai/config.json";
600
+ const defaults = loadCanonicalDefaults(ctx.stackDir)["opencode"];
601
+ if (defaults?.["permission"] !== void 0 && !("permission" in root)) {
602
+ root["permission"] = defaults["permission"];
603
+ }
604
+ const mcp = root["mcp"] ??= {};
605
+ for (const [name, server] of Object.entries(canonical.servers)) {
606
+ if (server.transport === "stdio") {
607
+ if (server.command === "{{ENGRAM_BIN}}" && ctx.engramBin === null) {
608
+ ctx.warnings.push(
609
+ "Engram no detectado: el MCP 'engram' no se registra. Inst\xE1lalo (github.com/Gentleman-Programming/engram) y re-ejecuta sync."
610
+ );
611
+ continue;
612
+ }
613
+ const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
614
+ mcp[name] = { type: "local", command: [command, ...server.args ?? []] };
615
+ } else {
616
+ const previous = mcp[name];
617
+ const headers = {};
618
+ for (const [key, raw] of Object.entries(server.headers ?? {})) {
619
+ const envRef = /^\$\{(\w+)\}$/.exec(raw);
620
+ headers[key] = previous?.headers?.[key] || (envRef ? `{env:${envRef[1]}}` : raw);
621
+ }
622
+ mcp[name] = {
623
+ type: "remote",
624
+ url: server.url,
625
+ ...Object.keys(headers).length > 0 ? { headers } : {}
626
+ };
627
+ }
628
+ }
629
+ if (pluginsDir !== null) {
630
+ const plugin = root["plugin"];
631
+ if (Array.isArray(plugin)) {
632
+ const pluginsDirPrefix = pathToFileURL(pluginsDir).href + "/";
633
+ const kept = plugin.filter((url) => !url.startsWith(pluginsDirPrefix));
634
+ if (kept.length === 0) delete root["plugin"];
635
+ else root["plugin"] = kept;
636
+ if (kept.some((u) => /engram/i.test(u))) {
637
+ ctx.warnings.push(
638
+ "OpenCode: hay un plugin de Engram registrado como paquete \u2014 revisa que no conviva con el engram.ts del stack (duplicar\xEDa la integraci\xF3n)."
639
+ );
640
+ }
641
+ }
642
+ }
643
+ });
644
+ return [{ kind: "write", target: file, content }];
645
+ },
646
+ planUnmerge(mcp, hooks, ctx) {
647
+ const actions = [];
648
+ const { systemPromptFile, pluginsDir } = this.paths(ctx.configDir);
649
+ const prompt = readTextIfExists(systemPromptFile);
650
+ if (prompt !== null) {
651
+ let content = removeMarkdownSection(prompt, "system-prompt");
652
+ content = removeMarkdownSection(content, "engram-protocol");
653
+ actions.push({ kind: "write", target: systemPromptFile, content });
654
+ }
655
+ const configFile = path7.join(ctx.configDir, "opencode.json");
656
+ const config = readTextIfExists(configFile);
657
+ if (config !== null) {
658
+ const content = upsertJson(config, (root) => {
659
+ const mcpBlock = root["mcp"];
660
+ if (mcpBlock) {
661
+ for (const name of Object.keys(mcp.servers)) delete mcpBlock[name];
662
+ if (Object.keys(mcpBlock).length === 0) delete root["mcp"];
663
+ }
664
+ const plugin = root["plugin"];
665
+ if (Array.isArray(plugin) && pluginsDir !== null) {
666
+ const pluginsDirPrefix = pathToFileURL(pluginsDir).href + "/";
667
+ const kept = plugin.filter((url) => !url.startsWith(pluginsDirPrefix));
668
+ if (kept.length === 0) delete root["plugin"];
669
+ else root["plugin"] = kept;
670
+ }
671
+ });
672
+ actions.push({ kind: "write", target: configFile, content });
673
+ }
674
+ const hooksFile = path7.join(ctx.configDir, "hooks.json");
675
+ const hooksJson = readTextIfExists(hooksFile);
676
+ if (hooksJson !== null) {
677
+ const ourScripts = hookScriptNames(hooks);
678
+ const content = upsertJson(hooksJson, (root) => {
679
+ const after = root["tool.execute.after"];
680
+ if (!after) return;
681
+ for (const tool of Object.keys(after)) {
682
+ const byCommand = after[tool];
683
+ for (const includes of Object.keys(byCommand)) {
684
+ byCommand[includes] = byCommand[includes].filter(
685
+ (script) => !ourScripts.some((s) => script.includes(s))
686
+ );
687
+ if (byCommand[includes].length === 0) delete byCommand[includes];
688
+ }
689
+ if (Object.keys(byCommand).length === 0) delete after[tool];
690
+ }
691
+ if (Object.keys(after).length === 0) delete root["tool.execute.after"];
692
+ });
693
+ actions.push({ kind: "write", target: hooksFile, content: content.trim() === "{}" ? "" : content });
694
+ }
695
+ return actions;
696
+ }
697
+ };
698
+
699
+ // src/adapters/claude-code.ts
700
+ import path8 from "path";
701
+ import fs4 from "fs";
702
+ function yamlString2(value) {
703
+ return JSON.stringify(value);
704
+ }
705
+ var memoryTools = (names) => names.flatMap((n) => [`mcp__engram__${n}`, `mcp__plugin_engram_engram__${n}`]);
706
+ var MEMORY_TOOLS = memoryTools(["mem_save", "mem_search", "mem_context"]);
707
+ var ENGRAM_AGENT_TOOLS = [
708
+ "Read",
709
+ "Skill",
710
+ ...memoryTools(["mem_context", "mem_search", "mem_get_observation", "mem_timeline", "mem_current_project"])
711
+ ];
712
+ function toolsFor(agent) {
713
+ if (agent.name === "engram") return ENGRAM_AGENT_TOOLS.join(", ");
714
+ if (!agent.readonly) return null;
715
+ const tools = ["Read", "Grep", "Glob", "Skill"];
716
+ if (agent.bash !== "none") tools.push("Bash");
717
+ tools.push(...MEMORY_TOOLS);
718
+ return tools.join(", ");
719
+ }
720
+ function hasEngramPlugin(configDir) {
721
+ if (fs4.existsSync(path8.join(configDir, "plugins", "marketplaces", "engram"))) return true;
722
+ const registry = readTextIfExists(path8.join(configDir, "plugins", "installed_plugins.json"));
723
+ if (registry === null) return false;
724
+ try {
725
+ const parsed = JSON.parse(registry);
726
+ const keys = [...Object.keys(parsed), ...Object.keys(parsed["plugins"] ?? {})];
727
+ return keys.some((k) => k === "engram" || k.startsWith("engram@"));
728
+ } catch {
729
+ return false;
730
+ }
731
+ }
732
+ var claudeCodeAdapter = {
733
+ id: "claude-code",
734
+ name: "Claude Code",
735
+ detect: detectClaudeCode,
736
+ injectEngramProtocol(ctx) {
737
+ return !hasEngramPlugin(ctx.configDir);
738
+ },
739
+ paths(configDir) {
740
+ return {
741
+ systemPromptFile: path8.join(configDir, "CLAUDE.md"),
742
+ agentsDir: path8.join(configDir, "agents"),
743
+ skillsDir: path8.join(configDir, "skills"),
744
+ commandsDir: path8.join(configDir, "commands"),
745
+ pluginsDir: null,
746
+ scriptsDir: path8.join(configDir, "scripts"),
747
+ outputStylesDir: path8.join(configDir, "output-styles"),
748
+ profilesDir: null
749
+ };
750
+ },
751
+ renderAgent(agent, models) {
752
+ if (agent.mode === "primary") {
753
+ const title = agent.name.charAt(0).toUpperCase() + agent.name.slice(1);
754
+ const style = `---
755
+ name: ${title}
756
+ description: ${yamlString2(agent.description)}
757
+ keep-coding-instructions: true
758
+ ---
759
+ ${agent.body}`;
760
+ const skillDescription = `${agent.description} Invoke to switch into ${agent.name} mode and apply its flow to the current task.`;
761
+ const skill = `---
762
+ name: ${agent.name}
763
+ description: ${yamlString2(skillDescription)}
764
+ ---
765
+ ${agent.body}`;
766
+ return [
767
+ { file: `${agent.name}.md`, content: style, kind: "output-style" },
768
+ { file: `${agent.name}/SKILL.md`, content: skill, kind: "skill" }
769
+ ];
770
+ }
771
+ const lines = [`name: ${agent.name}`, `description: ${yamlString2(agent.description)}`];
772
+ const tools = toolsFor(agent);
773
+ if (tools !== null) lines.push(`tools: ${tools}`);
774
+ lines.push(`model: ${resolveAgentModel(models, agent.name, agent.tier).model}`);
775
+ return [
776
+ {
777
+ file: `${agent.name}.md`,
778
+ content: `---
779
+ ${lines.join("\n")}
780
+ ---
781
+ ${agent.body}`,
782
+ kind: "agent"
783
+ }
784
+ ];
785
+ },
786
+ renderCommand(file, content) {
787
+ return { file, content: content.replace(/\{\{input\}\}/g, "$ARGUMENTS") };
788
+ },
789
+ planHooks(canonical, ctx) {
790
+ const actions = [];
791
+ const { scriptsDir } = this.paths(ctx.configDir);
792
+ const settingsFile = path8.join(ctx.configDir, "settings.json");
793
+ let content = upsertNativeHooks(readTextIfExists(settingsFile), canonical, scriptsDir);
794
+ const defaults = loadCanonicalDefaults(ctx.stackDir)["claude-code"];
795
+ if (defaults?.["permissions"] !== void 0) {
796
+ content = upsertJson(content, (root) => {
797
+ if (!("permissions" in root)) root["permissions"] = defaults["permissions"];
798
+ });
799
+ }
800
+ actions.push({ kind: "write", target: settingsFile, content });
801
+ const scriptsSource = path8.join(ctx.stackDir, "scripts");
802
+ if (fs4.existsSync(scriptsSource)) {
803
+ for (const f of fs4.readdirSync(scriptsSource)) {
804
+ actions.push({ kind: "copy", source: path8.join(scriptsSource, f), target: path8.join(scriptsDir, f) });
805
+ }
806
+ }
807
+ return actions;
808
+ },
809
+ planMainConfig(canonical, ctx) {
810
+ const file = path8.join(path8.dirname(ctx.configDir), `${path8.basename(ctx.configDir)}.json`);
811
+ const content = upsertJson(readTextIfExists(file), (root) => {
812
+ const servers = root["mcpServers"] ??= {};
813
+ for (const [name, server] of Object.entries(canonical.servers)) {
814
+ if (server.transport === "stdio") {
815
+ if (server.command === "{{ENGRAM_BIN}}" && hasEngramPlugin(ctx.configDir)) {
816
+ if (name in servers) delete servers[name];
817
+ ctx.warnings.push(
818
+ "Claude Code: Engram ya est\xE1 integrado v\xEDa plugin \u2014 no se registra el MCP para no duplicar las tools de memoria."
819
+ );
820
+ continue;
821
+ }
822
+ if (server.command === "{{ENGRAM_BIN}}" && ctx.engramBin === null) {
823
+ ctx.warnings.push(
824
+ "Engram no detectado: el MCP 'engram' no se registra. Inst\xE1lalo (github.com/Gentleman-Programming/engram) y re-ejecuta sync."
825
+ );
826
+ continue;
827
+ }
828
+ const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
829
+ servers[name] = { command, args: server.args ?? [] };
830
+ } else {
831
+ const previous = servers[name];
832
+ const headers = {};
833
+ for (const [key, raw] of Object.entries(server.headers ?? {})) {
834
+ const envRef = /^\$\{(\w+)\}$/.exec(raw);
835
+ headers[key] = previous?.headers?.[key] || (envRef ? "" : raw);
836
+ }
837
+ servers[name] = {
838
+ type: "http",
839
+ url: server.url,
840
+ ...Object.keys(headers).length > 0 ? { headers } : {}
841
+ };
842
+ }
843
+ }
844
+ });
845
+ return [{ kind: "write", target: file, content }];
846
+ },
847
+ planUnmerge(mcp, hooks, ctx) {
848
+ const actions = [];
849
+ const { systemPromptFile } = this.paths(ctx.configDir);
850
+ const prompt = readTextIfExists(systemPromptFile);
851
+ if (prompt !== null) {
852
+ let content = removeMarkdownSection(prompt, "system-prompt");
853
+ content = removeMarkdownSection(content, "engram-protocol");
854
+ actions.push({ kind: "write", target: systemPromptFile, content });
855
+ }
856
+ const settingsFile = path8.join(ctx.configDir, "settings.json");
857
+ const settings = readTextIfExists(settingsFile);
858
+ if (settings !== null) {
859
+ const content = removeNativeHooks(settings, hooks);
860
+ if (content !== null) {
861
+ actions.push({ kind: "write", target: settingsFile, content: content.trim() === "{}" ? "" : content });
862
+ }
863
+ }
864
+ const mainFile = path8.join(path8.dirname(ctx.configDir), `${path8.basename(ctx.configDir)}.json`);
865
+ const main2 = readTextIfExists(mainFile);
866
+ if (main2 !== null) {
867
+ const content = upsertJson(main2, (root) => {
868
+ const servers = root["mcpServers"];
869
+ if (!servers) return;
870
+ for (const name of Object.keys(mcp.servers)) delete servers[name];
871
+ if (Object.keys(servers).length === 0) delete root["mcpServers"];
872
+ });
873
+ actions.push({ kind: "write", target: mainFile, content });
874
+ }
875
+ return actions;
876
+ }
877
+ };
878
+
879
+ // src/adapters/codex.ts
880
+ import path9 from "path";
881
+ import fs5 from "fs";
882
+ function tomlString(value) {
883
+ return JSON.stringify(value);
884
+ }
885
+ function tomlMultiline(value) {
886
+ if (value.includes("'''")) return JSON.stringify(value);
887
+ return `'''
888
+ ${value.replace(/\r\n/g, "\n").trim()}
889
+ '''`;
890
+ }
891
+ function hasActiveEngramPlugin(configDir) {
892
+ const config = readTextIfExists(path9.join(configDir, "config.toml"));
893
+ if (config === null) return false;
894
+ const match = /\[plugins\."engram@[^"]*"\]([^[]*)/.exec(config);
895
+ return match !== null && !/enabled\s*=\s*false/.test(match[1]);
896
+ }
897
+ function hasEngramProtocol(configDir) {
898
+ if (hasActiveEngramPlugin(configDir)) return true;
899
+ if (fs5.existsSync(path9.join(configDir, "engram-instructions.md"))) return true;
900
+ const config = readTextIfExists(path9.join(configDir, "config.toml"));
901
+ return config !== null && /engram-instructions\.md/.test(config);
902
+ }
903
+ var codexAdapter = {
904
+ id: "codex",
905
+ name: "Codex CLI",
906
+ detect: detectCodex,
907
+ injectEngramProtocol(ctx) {
908
+ return !hasEngramProtocol(ctx.configDir);
909
+ },
910
+ paths(configDir) {
911
+ const isRealConfigDir = samePath(configDir, process.env.CODEX_HOME ?? path9.join(HOME, ".codex"));
912
+ const agentsHome = isRealConfigDir ? HOME : path9.dirname(configDir);
913
+ const skillsDir = path9.join(agentsHome, ".agents", "skills");
914
+ return {
915
+ systemPromptFile: path9.join(configDir, "AGENTS.md"),
916
+ agentsDir: path9.join(configDir, "agents"),
917
+ skillsDir,
918
+ // Los custom prompts de Codex están deprecados: los commands se
919
+ // instalan como skills (renderCommand produce <nombre>/SKILL.md).
920
+ commandsDir: skillsDir,
921
+ pluginsDir: null,
922
+ scriptsDir: path9.join(configDir, "scripts"),
923
+ outputStylesDir: null,
924
+ profilesDir: configDir
925
+ };
926
+ },
927
+ renderAgent(agent, models) {
928
+ const tierModel = resolveAgentModel(models, agent.name, agent.tier);
929
+ if (agent.mode === "primary") {
930
+ const profileLines = [
931
+ "# Managed by jorgex-stack \u2014 modo orquestador del agente principal.",
932
+ `# Uso: codex --profile ${agent.name}`,
933
+ `developer_instructions = ${tomlMultiline(agent.body)}`
934
+ ];
935
+ const skillDescription = `${agent.description} Invoke to switch into ${agent.name} mode and apply its flow to the current task.`;
936
+ const skill = `---
937
+ name: ${agent.name}
938
+ description: ${JSON.stringify(skillDescription)}
939
+ ---
940
+ ${agent.body}`;
941
+ return [
942
+ { file: `${agent.name}.config.toml`, content: profileLines.join("\n") + "\n", kind: "profile" },
943
+ { file: `${agent.name}/SKILL.md`, content: skill, kind: "skill" }
944
+ ];
945
+ }
946
+ const lines = [
947
+ `name = ${tomlString(agent.name)}`,
948
+ `description = ${tomlString(agent.description)}`
949
+ ];
950
+ if (tierModel.model !== "default") lines.push(`model = ${tomlString(tierModel.model)}`);
951
+ if (tierModel.variant) lines.push(`model_reasoning_effort = ${tomlString(tierModel.variant)}`);
952
+ lines.push(`sandbox_mode = ${tomlString(agent.readonly ? "read-only" : "workspace-write")}`);
953
+ lines.push(`developer_instructions = ${tomlMultiline(agent.body)}`);
954
+ return [{ file: `${agent.name}.toml`, content: lines.join("\n") + "\n", kind: "agent" }];
955
+ },
956
+ renderCommand(file, content) {
957
+ const name = file.replace(/\.md$/, "");
958
+ let description = name;
959
+ let body = content;
960
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(content);
961
+ if (fm) {
962
+ const match = /(?:^|\n)description:\s*(.+)/.exec(fm[1]);
963
+ if (match) description = match[1].trim();
964
+ body = content.slice(fm[0].length);
965
+ }
966
+ body = body.replace(/\{\{input\}\}/g, "the user's request in this conversation");
967
+ return {
968
+ file: `${name}/SKILL.md`,
969
+ content: `---
970
+ name: ${name}
971
+ description: ${JSON.stringify(description)}
972
+ ---
973
+ ${body}`
974
+ };
975
+ },
976
+ planHooks(canonical, ctx) {
977
+ const actions = [];
978
+ const { scriptsDir } = this.paths(ctx.configDir);
979
+ const hooksFile = path9.join(ctx.configDir, "hooks.json");
980
+ const content = upsertNativeHooks(
981
+ readTextIfExists(hooksFile),
982
+ canonical,
983
+ scriptsDir,
984
+ (matcher) => matcher.split("|").some((p7) => p7.trim() === "Bash") ? "shell" : matcher
985
+ );
986
+ actions.push({ kind: "write", target: hooksFile, content });
987
+ ctx.warnings.push(
988
+ "Codex: los hooks no-managed requieren aprobaci\xF3n manual \u2014 ejecuta /hooks dentro de codex para activarlos."
989
+ );
990
+ const scriptsSource = path9.join(ctx.stackDir, "scripts");
991
+ if (fs5.existsSync(scriptsSource)) {
992
+ for (const f of fs5.readdirSync(scriptsSource)) {
993
+ actions.push({ kind: "copy", source: path9.join(scriptsSource, f), target: path9.join(scriptsDir, f) });
994
+ }
995
+ }
996
+ return actions;
997
+ },
998
+ planMainConfig(canonical, ctx) {
999
+ const file = path9.join(ctx.configDir, "config.toml");
1000
+ let content = readTextIfExists(file);
1001
+ const defaults = loadCanonicalDefaults(ctx.stackDir)["codex"] ?? {};
1002
+ for (const [key, value] of Object.entries(defaults)) {
1003
+ if (content !== null && new RegExp(`^\\s*${key}\\s*=`, "m").test(content)) continue;
1004
+ const line = `${key} = ${tomlString(String(value))}
1005
+ `;
1006
+ content = content === null ? line : line + content;
1007
+ }
1008
+ for (const [name, server] of Object.entries(canonical.servers)) {
1009
+ const section = `mcp_servers.${name}`;
1010
+ if (server.transport === "stdio") {
1011
+ if (server.command === "{{ENGRAM_BIN}}" && hasActiveEngramPlugin(ctx.configDir)) {
1012
+ if (content !== null) content = removeTomlSection(content, section);
1013
+ ctx.warnings.push(
1014
+ "Codex: Engram ya est\xE1 integrado v\xEDa plugin de marketplace \u2014 no se registra el MCP para no duplicar las tools de memoria."
1015
+ );
1016
+ continue;
1017
+ }
1018
+ if (server.command === "{{ENGRAM_BIN}}" && ctx.engramBin === null) {
1019
+ ctx.warnings.push(
1020
+ "Engram no detectado: el MCP 'engram' no se registra. Inst\xE1lalo (github.com/Gentleman-Programming/engram) y re-ejecuta sync."
1021
+ );
1022
+ continue;
1023
+ }
1024
+ const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
1025
+ const args = (server.args ?? []).map(tomlString).join(", ");
1026
+ content = upsertTomlSection(content, section, `command = ${tomlString(command)}
1027
+ args = [${args}]`);
1028
+ } else {
1029
+ const previousSection = readTomlSection(content, section);
1030
+ const prevUsesEnvHeaders = previousSection?.includes("env_http_headers") ?? false;
1031
+ const literalPairs = [];
1032
+ const refPairs = [];
1033
+ for (const [key, raw] of Object.entries(server.headers ?? {})) {
1034
+ const envRef = /^\$\{(\w+)\}$/.exec(raw);
1035
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1036
+ const previousValue = !prevUsesEnvHeaders && previousSection !== null ? new RegExp(`(?:^|[{,]\\s*)"?${escaped}"?\\s*=\\s*"([^"]*)"`, "m").exec(previousSection)?.[1] : void 0;
1037
+ if (previousValue) literalPairs.push(`${tomlString(key)} = ${tomlString(previousValue)}`);
1038
+ else refPairs.push(`${tomlString(key)} = ${tomlString(envRef ? envRef[1] : raw)}`);
1039
+ }
1040
+ const body = [`url = ${tomlString(server.url)}`];
1041
+ if (literalPairs.length > 0) body.push(`http_headers = { ${literalPairs.join(", ")} }`);
1042
+ if (refPairs.length > 0) body.push(`env_http_headers = { ${refPairs.join(", ")} }`);
1043
+ content = upsertTomlSection(content, section, body.join("\n"));
1044
+ }
1045
+ }
1046
+ if (content === null) return [];
1047
+ if (!content.endsWith("\n")) content += "\n";
1048
+ return [{ kind: "write", target: file, content }];
1049
+ },
1050
+ planUnmerge(mcp, hooks, ctx) {
1051
+ const actions = [];
1052
+ const { systemPromptFile } = this.paths(ctx.configDir);
1053
+ const prompt = readTextIfExists(systemPromptFile);
1054
+ if (prompt !== null) {
1055
+ let content = removeMarkdownSection(prompt, "system-prompt");
1056
+ content = removeMarkdownSection(content, "engram-protocol");
1057
+ actions.push({ kind: "write", target: systemPromptFile, content });
1058
+ }
1059
+ const configFile = path9.join(ctx.configDir, "config.toml");
1060
+ const config = readTextIfExists(configFile);
1061
+ if (config !== null) {
1062
+ let content = config;
1063
+ for (const name of Object.keys(mcp.servers)) content = removeTomlSection(content, `mcp_servers.${name}`);
1064
+ actions.push({ kind: "write", target: configFile, content });
1065
+ }
1066
+ const hooksFile = path9.join(ctx.configDir, "hooks.json");
1067
+ const hooksJson = readTextIfExists(hooksFile);
1068
+ if (hooksJson !== null) {
1069
+ const content = removeNativeHooks(hooksJson, hooks);
1070
+ if (content !== null) {
1071
+ actions.push({ kind: "write", target: hooksFile, content: content.trim() === "{}" ? "" : content });
1072
+ }
1073
+ }
1074
+ return actions;
1075
+ }
1076
+ };
1077
+
1078
+ // src/lib/backup.ts
1079
+ import fs6 from "fs";
1080
+ import path10 from "path";
1081
+ import crypto from "crypto";
1082
+ var KEEP_BACKUPS = 10;
1083
+ function backupsRoot() {
1084
+ return path10.join(dataDir(), "backups");
1085
+ }
1086
+ function compositeChecksum(files) {
1087
+ const hash = crypto.createHash("sha256");
1088
+ for (const file of [...files].sort()) {
1089
+ const content = crypto.createHash("sha256").update(fs6.readFileSync(file)).digest("hex");
1090
+ hash.update(`${file}:${content}
1091
+ `);
1092
+ }
1093
+ return hash.digest("hex");
1094
+ }
1095
+ function createBackup(files, label, root = backupsRoot()) {
1096
+ const existing = [...new Set(files)].filter((f) => fs6.existsSync(f));
1097
+ if (existing.length === 0) return null;
1098
+ const checksum = compositeChecksum(existing);
1099
+ const latest = listBackups(root)[0];
1100
+ if (latest?.checksum === checksum) return latest;
1101
+ const id = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${label}`;
1102
+ const dir = path10.join(root, id);
1103
+ ensureDir(path10.join(dir, "files"));
1104
+ const entries = existing.map((original, i) => {
1105
+ const stored = path10.join(dir, "files", `${String(i).padStart(4, "0")}-${path10.basename(original)}`);
1106
+ fs6.copyFileSync(original, stored);
1107
+ return { original, stored };
1108
+ });
1109
+ const info = { id, label, createdAt: (/* @__PURE__ */ new Date()).toISOString(), files: entries, checksum };
1110
+ writeText(path10.join(dir, "manifest.json"), JSON.stringify(info, null, 2) + "\n");
1111
+ pruneBackups(root);
1112
+ return info;
1113
+ }
1114
+ function listBackups(root = backupsRoot()) {
1115
+ if (!fs6.existsSync(root)) return [];
1116
+ const infos = [];
1117
+ for (const entry of fs6.readdirSync(root, { withFileTypes: true })) {
1118
+ if (!entry.isDirectory()) continue;
1119
+ const manifest = readTextIfExists(path10.join(root, entry.name, "manifest.json"));
1120
+ if (manifest === null) continue;
1121
+ try {
1122
+ infos.push(JSON.parse(manifest));
1123
+ } catch {
1124
+ infos.push({ id: entry.name, label: "(manifest corrupto)", createdAt: "", files: [] });
1125
+ }
1126
+ }
1127
+ return infos.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
1128
+ }
1129
+ function restoreBackup(id, root = backupsRoot(), boundary = HOME) {
1130
+ const info = listBackups(root).find((b) => b.id === id);
1131
+ if (!info) throw new Error(`Backup no encontrado: ${id}`);
1132
+ let restored = 0;
1133
+ for (const { original, stored } of info.files) {
1134
+ if (!fs6.existsSync(stored)) continue;
1135
+ if (!isContainedIn(original, boundary)) continue;
1136
+ ensureDir(path10.dirname(original));
1137
+ fs6.copyFileSync(stored, original);
1138
+ restored++;
1139
+ }
1140
+ return restored;
1141
+ }
1142
+ function pruneBackups(root) {
1143
+ const all = listBackups(root);
1144
+ for (const old of all.slice(KEEP_BACKUPS)) {
1145
+ fs6.rmSync(path10.join(root, old.id), { recursive: true, force: true });
1146
+ }
1147
+ }
1148
+
1149
+ // src/lib/manifest.ts
1150
+ import fs7 from "fs";
1151
+ import path11 from "path";
1152
+ function manifestFile() {
1153
+ return path11.join(dataDir(), "manifest.json");
1154
+ }
1155
+ function readManifest(file = manifestFile()) {
1156
+ const raw = readTextIfExists(file);
1157
+ if (raw === null) return { runtimes: {} };
1158
+ try {
1159
+ const parsed = JSON.parse(raw);
1160
+ return { runtimes: parsed.runtimes ?? {} };
1161
+ } catch {
1162
+ return { runtimes: {} };
1163
+ }
1164
+ }
1165
+ function writeRuntimeManifest(id, entry, file = manifestFile()) {
1166
+ const manifest = readManifest(file);
1167
+ manifest.runtimes[id] = entry;
1168
+ writeText(file, JSON.stringify(manifest, null, 2) + "\n");
1169
+ }
1170
+ function removeRuntimeManifest(id, file = manifestFile()) {
1171
+ const manifest = readManifest(file);
1172
+ if (!(id in manifest.runtimes)) return;
1173
+ delete manifest.runtimes[id];
1174
+ writeText(file, JSON.stringify(manifest, null, 2) + "\n");
1175
+ }
1176
+ function findOrphans(prevOwned, currentTargets, root = HOME) {
1177
+ return prevOwned.map((f) => path11.resolve(f)).filter(
1178
+ (f) => !currentTargets.has(f) && path11.basename(f) !== "engram.ts" && isContainedIn(f, root) && fs7.existsSync(f)
1179
+ );
1180
+ }
1181
+
1182
+ // src/components/system-prompt.ts
1183
+ import path12 from "path";
1184
+ import fs8 from "fs";
1185
+ var normalize = (s) => s.replace(/\r\n/g, "\n");
1186
+ function planSystemPrompt(adapter, ctx) {
1187
+ const target = adapter.paths(ctx.configDir).systemPromptFile;
1188
+ const agentsMd = normalize(fs8.readFileSync(path12.join(ctx.stackDir, "system-prompt", "AGENTS.md"), "utf8"));
1189
+ const protocol = stripLeadingHtmlComments(
1190
+ normalize(fs8.readFileSync(path12.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8"))
1191
+ );
1192
+ let content = readTextIfExists(target);
1193
+ content = upsertMarkdownSection(content, "system-prompt", agentsMd);
1194
+ if (adapter.injectEngramProtocol(ctx)) {
1195
+ content = upsertMarkdownSection(content, "engram-protocol", protocol);
1196
+ } else {
1197
+ content = removeMarkdownSection(content, "engram-protocol");
1198
+ }
1199
+ return [{ kind: "write", target, content }];
1200
+ }
1201
+
1202
+ // src/components/agents.ts
1203
+ import path13 from "path";
1204
+ function planAgents(adapter, ctx) {
1205
+ const { agentsDir, commandsDir, outputStylesDir, skillsDir, profilesDir } = adapter.paths(ctx.configDir);
1206
+ const dirFor = {
1207
+ agent: agentsDir,
1208
+ command: commandsDir,
1209
+ "output-style": outputStylesDir,
1210
+ skill: skillsDir,
1211
+ profile: profilesDir
1212
+ };
1213
+ return loadCanonicalAgents(path13.join(ctx.stackDir, "agents")).flatMap(
1214
+ (agent) => adapter.renderAgent(agent, ctx.models).flatMap((rendered) => {
1215
+ const dir = dirFor[rendered.kind];
1216
+ if (dir === null) {
1217
+ ctx.warnings.push(`${adapter.name}: sin destino para '${rendered.kind}' (${rendered.file}) \u2014 omitido.`);
1218
+ return [];
1219
+ }
1220
+ return [{ kind: "write", target: path13.join(dir, rendered.file), content: rendered.content }];
1221
+ })
1222
+ );
1223
+ }
1224
+
1225
+ // src/components/skills.ts
1226
+ import path14 from "path";
1227
+ function planSkills(adapter, ctx) {
1228
+ const { skillsDir } = adapter.paths(ctx.configDir);
1229
+ const source = path14.join(ctx.stackDir, "skills");
1230
+ return listFilesRecursive(source).map((file) => ({
1231
+ kind: "copy",
1232
+ source: file,
1233
+ target: path14.join(skillsDir, path14.relative(source, file))
1234
+ }));
1235
+ }
1236
+
1237
+ // src/components/commands.ts
1238
+ import path15 from "path";
1239
+ import fs9 from "fs";
1240
+ function planCommands(adapter, ctx) {
1241
+ const { commandsDir } = adapter.paths(ctx.configDir);
1242
+ const source = path15.join(ctx.stackDir, "commands");
1243
+ if (!fs9.existsSync(source)) return [];
1244
+ return fs9.readdirSync(source).filter((f) => f.endsWith(".md")).map((f) => {
1245
+ const raw = fs9.readFileSync(path15.join(source, f), "utf8").replace(/\r\n/g, "\n");
1246
+ const rendered = adapter.renderCommand(f, raw);
1247
+ return { kind: "write", target: path15.join(commandsDir, rendered.file), content: rendered.content };
1248
+ });
1249
+ }
1250
+
1251
+ // src/components/hooks.ts
1252
+ function planHooks(adapter, ctx) {
1253
+ return adapter.planHooks(loadCanonicalHooks(ctx.stackDir), ctx);
1254
+ }
1255
+
1256
+ // src/components/mcp.ts
1257
+ function planMcp(adapter, ctx) {
1258
+ return adapter.planMainConfig(loadCanonicalMcp(ctx.stackDir), ctx);
1259
+ }
1260
+
1261
+ // src/components/plugins.ts
1262
+ import path16 from "path";
1263
+ import fs10 from "fs";
1264
+ function planPlugins(adapter, ctx) {
1265
+ const { pluginsDir } = adapter.paths(ctx.configDir);
1266
+ if (pluginsDir === null) return [];
1267
+ const source = path16.join(ctx.stackDir, "plugins", adapter.id);
1268
+ if (!fs10.existsSync(source)) return [];
1269
+ return fs10.readdirSync(source).filter((f) => f.endsWith(".ts")).map((f) => {
1270
+ const sourceFile = path16.join(source, f);
1271
+ const target = path16.join(pluginsDir, f);
1272
+ const raw = fs10.readFileSync(sourceFile, "utf8");
1273
+ let content = raw;
1274
+ if (content.includes('"{{ENGRAM_BIN}}"')) {
1275
+ content = content.replace(/"\{\{ENGRAM_BIN\}\}"/g, JSON.stringify(ctx.engramBin ?? "engram"));
1276
+ }
1277
+ if (content.includes('"{{ENGRAM_PROTOCOL}}"')) {
1278
+ const protocol = stripLeadingHtmlComments(
1279
+ fs10.readFileSync(path16.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8").replace(/\r\n/g, "\n")
1280
+ );
1281
+ content = content.replace(/"\{\{ENGRAM_PROTOCOL\}\}"/g, JSON.stringify(protocol));
1282
+ }
1283
+ if (content === raw) return { kind: "copy", source: sourceFile, target };
1284
+ return { kind: "write", target, content };
1285
+ });
1286
+ }
1287
+
1288
+ // src/install.ts
1289
+ var ADAPTERS = {
1290
+ opencode: opencodeAdapter,
1291
+ "claude-code": claudeCodeAdapter,
1292
+ codex: codexAdapter
1293
+ };
1294
+ function makeContext(adapter, configDir) {
1295
+ const models = loadModelMap()[adapter.id];
1296
+ if (!models) return null;
1297
+ return {
1298
+ stackDir: stackRoot(),
1299
+ configDir,
1300
+ engramBin: detectEngram(),
1301
+ models,
1302
+ warnings: []
1303
+ };
1304
+ }
1305
+ function buildPlan(adapter, ctx) {
1306
+ return [
1307
+ ...planSystemPrompt(adapter, ctx),
1308
+ ...planAgents(adapter, ctx),
1309
+ ...planSkills(adapter, ctx),
1310
+ ...planCommands(adapter, ctx),
1311
+ ...planHooks(adapter, ctx),
1312
+ ...planMcp(adapter, ctx),
1313
+ ...planPlugins(adapter, ctx)
1314
+ ];
1315
+ }
1316
+ function diffPlan(plan) {
1317
+ return plan.map((action) => {
1318
+ if (action.kind === "write") {
1319
+ const current = readTextIfExists(action.target);
1320
+ if (current === null) return { action, status: "create" };
1321
+ return { action, status: current === action.content ? "unchanged" : "update" };
1322
+ }
1323
+ if (!fs11.existsSync(action.target)) return { action, status: "create" };
1324
+ return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
1325
+ });
1326
+ }
1327
+ function applyChanges(changes) {
1328
+ for (const { action } of changes) {
1329
+ if (action.kind === "write") writeText(action.target, action.content);
1330
+ else copyFile(action.source, action.target);
1331
+ }
1332
+ }
1333
+ function collectAllCurrentTargets() {
1334
+ const targets = /* @__PURE__ */ new Set();
1335
+ let complete = true;
1336
+ for (const adapter of Object.values(ADAPTERS)) {
1337
+ const detection = adapter.detect();
1338
+ if (!detection.installed) continue;
1339
+ const ctx = makeContext(adapter, detection.configDir);
1340
+ if (!ctx) continue;
1341
+ try {
1342
+ for (const action of buildPlan(adapter, ctx)) targets.add(path17.resolve(action.target));
1343
+ } catch {
1344
+ complete = false;
1345
+ }
1346
+ }
1347
+ return { targets, complete };
1348
+ }
1349
+ async function runInstall(opts) {
1350
+ p.intro(`jorgex-stack ${opts.dryRun ? "install (dry-run)" : "install"}`);
1351
+ const stackDir = stackRoot();
1352
+ const engramBin = detectEngram();
1353
+ const modelMap = loadModelMap();
1354
+ ensureModelMapFile();
1355
+ p.log.info(engramBin ? `Engram detectado: ${engramBin} (se respeta, D7)` : "Engram NO detectado.");
1356
+ const useManifest = opts.targetDir === void 0;
1357
+ const current = useManifest ? collectAllCurrentTargets() : { targets: /* @__PURE__ */ new Set(), complete: false };
1358
+ const canOrphan = useManifest && current.complete;
1359
+ const canonicalMcp = loadCanonicalMcp(stackDir);
1360
+ const canonicalHooks = loadCanonicalHooks(stackDir);
1361
+ let exitCode = 0;
1362
+ for (const id of opts.runtimes) {
1363
+ const adapter = ADAPTERS[id];
1364
+ if (!adapter) {
1365
+ p.log.warn(`${id}: adapter pendiente (F3/F4) \u2014 omitido.`);
1366
+ continue;
1367
+ }
1368
+ const detection = adapter.detect();
1369
+ const configDir = opts.targetDir ?? detection.configDir;
1370
+ if (!detection.installed && opts.targetDir === void 0) {
1371
+ p.log.warn(`${adapter.name} no detectado en esta m\xE1quina \u2014 omitido.`);
1372
+ continue;
1373
+ }
1374
+ const models = modelMap[id];
1375
+ if (!models) {
1376
+ p.log.warn(`${adapter.name}: sin model-map para este runtime \u2014 omitido.`);
1377
+ continue;
1378
+ }
1379
+ const ctx = {
1380
+ stackDir,
1381
+ configDir,
1382
+ engramBin,
1383
+ models,
1384
+ warnings: []
1385
+ };
1386
+ let plan = buildPlan(adapter, ctx);
1387
+ let diff = diffPlan(plan);
1388
+ let creates = diff.filter((d) => d.status === "create");
1389
+ let updates = diff.filter((d) => d.status === "update");
1390
+ let changes = [...creates, ...updates];
1391
+ const prevManifest = canOrphan ? readManifest().runtimes[id] : void 0;
1392
+ const orphans = prevManifest ? findOrphans(prevManifest.owned, current.targets) : [];
1393
+ p.log.step(`${adapter.name} \u2192 ${configDir}`);
1394
+ p.log.info(
1395
+ `${diff.length} archivos gestionados: ${creates.length} nuevos, ${updates.length} modificados, ${diff.length - changes.length} sin cambios`
1396
+ );
1397
+ if (orphans.length > 0) p.log.info(`${orphans.length} hu\xE9rfanos de versiones previas a eliminar`);
1398
+ for (const w of ctx.warnings) p.log.warn(w);
1399
+ if (opts.dryRun) {
1400
+ for (const c of changes.slice(0, 40)) p.log.message(` ${c.status === "create" ? "+" : "~"} ${c.action.target}`);
1401
+ if (changes.length > 40) p.log.message(` \u2026 y ${changes.length - 40} m\xE1s`);
1402
+ for (const o of orphans) p.log.message(` - ${o}`);
1403
+ continue;
1404
+ }
1405
+ const writeManifest = () => {
1406
+ if (!useManifest) return;
1407
+ const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) => path17.resolve(a.target)));
1408
+ const owned = plan.map((a) => path17.resolve(a.target)).filter((t) => !unmergeTargets.has(t));
1409
+ writeRuntimeManifest(id, { configDir, owned, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1410
+ };
1411
+ if (changes.length === 0 && orphans.length === 0) {
1412
+ writeManifest();
1413
+ p.log.success(`${adapter.name}: ya al d\xEDa (idempotente).`);
1414
+ continue;
1415
+ }
1416
+ if (!opts.yes && process.stdout.isTTY) {
1417
+ const orphanNote = orphans.length > 0 ? ` (+ ${orphans.length} hu\xE9rfanos a eliminar)` : "";
1418
+ const ok = await p.confirm({ message: `\xBFAplicar ${changes.length} cambios en ${adapter.name}?${orphanNote}` });
1419
+ if (p.isCancel(ok) || !ok) {
1420
+ p.log.warn(`${adapter.name}: omitido por el usuario.`);
1421
+ continue;
1422
+ }
1423
+ plan = buildPlan(adapter, { ...ctx, warnings: [] });
1424
+ diff = diffPlan(plan);
1425
+ creates = diff.filter((d) => d.status === "create");
1426
+ updates = diff.filter((d) => d.status === "update");
1427
+ changes = [...creates, ...updates];
1428
+ }
1429
+ const backup = createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`);
1430
+ if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
1431
+ applyChanges(changes);
1432
+ const pruneRoot = useManifest ? HOME : path17.dirname(configDir);
1433
+ for (const orphan of orphans) {
1434
+ fs11.rmSync(orphan, { force: true });
1435
+ pruneEmptyDirs(orphan, pruneRoot);
1436
+ }
1437
+ const verifyCtx = { ...ctx, warnings: [] };
1438
+ const dirty = diffPlan(buildPlan(adapter, verifyCtx)).filter((d) => d.status !== "unchanged");
1439
+ if (dirty.length > 0) {
1440
+ p.log.error(`${adapter.name}: verificaci\xF3n de idempotencia FALL\xD3 (${dirty.length} acciones inestables).`);
1441
+ for (const d of dirty.slice(0, 10)) p.log.message(` ! ${d.action.target}`);
1442
+ exitCode = 1;
1443
+ } else {
1444
+ writeManifest();
1445
+ p.log.success(`${adapter.name}: ${changes.length} archivos aplicados y verificados (idempotente).`);
1446
+ }
1447
+ }
1448
+ p.outro(opts.dryRun ? "Dry-run: no se ha escrito nada." : "Hecho.");
1449
+ return exitCode;
1450
+ }
1451
+
1452
+ // src/uninstall.ts
1453
+ import fs12 from "fs";
1454
+ import path18 from "path";
1455
+ import * as p2 from "@clack/prompts";
1456
+ async function runUninstall(opts) {
1457
+ p2.intro(`jorgex-stack ${opts.dryRun ? "uninstall (dry-run)" : "uninstall"}`);
1458
+ const stackDir = stackRoot();
1459
+ const mcp = loadCanonicalMcp(stackDir);
1460
+ const hooks = loadCanonicalHooks(stackDir);
1461
+ let removeEngram = opts.removeEngram;
1462
+ if (!removeEngram && !opts.yes && !opts.dryRun && process.stdout.isTTY) {
1463
+ const answer = await p2.confirm({
1464
+ 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.",
1465
+ initialValue: false
1466
+ });
1467
+ if (p2.isCancel(answer)) {
1468
+ p2.cancel("Cancelado \u2014 no se ha tocado nada.");
1469
+ return 1;
1470
+ }
1471
+ removeEngram = answer === true;
1472
+ }
1473
+ const mcpForUnmerge = removeEngram ? mcp : { servers: Object.fromEntries(Object.entries(mcp.servers).filter(([name]) => name !== "engram")) };
1474
+ if (!removeEngram) {
1475
+ p2.log.info("Engram se conserva: memorias, binario y registro intactos (usa --remove-engram para desregistrarlo).");
1476
+ }
1477
+ const retained = /* @__PURE__ */ new Set();
1478
+ for (const keep of Object.values(ADAPTERS)) {
1479
+ if (opts.runtimes.includes(keep.id)) continue;
1480
+ const detection = keep.detect();
1481
+ if (!detection.installed) continue;
1482
+ const keepCtx = makeContext(keep, detection.configDir);
1483
+ if (!keepCtx) continue;
1484
+ for (const action of buildPlan(keep, keepCtx)) retained.add(path18.resolve(action.target));
1485
+ }
1486
+ for (const id of opts.runtimes) {
1487
+ const adapter = ADAPTERS[id];
1488
+ if (!adapter) {
1489
+ p2.log.warn(`${id}: sin adapter \u2014 omitido.`);
1490
+ continue;
1491
+ }
1492
+ const detection = adapter.detect();
1493
+ const configDir = opts.targetDir ?? detection.configDir;
1494
+ if (!detection.installed && opts.targetDir === void 0) {
1495
+ p2.log.warn(`${adapter.name} no detectado \u2014 omitido.`);
1496
+ continue;
1497
+ }
1498
+ const ctx = makeContext(adapter, configDir);
1499
+ if (!ctx) continue;
1500
+ ctx.preserveEngram = !removeEngram;
1501
+ const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
1502
+ const mergedTargets = new Set(unmerge.map((a) => path18.resolve(a.target)));
1503
+ const usingRealConfig = opts.targetDir === void 0;
1504
+ const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
1505
+ const pruneRoot = usingRealConfig ? HOME : path18.dirname(configDir);
1506
+ const planTargets = [
1507
+ .../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path18.resolve(a.target)), ...prevOwned.map((t) => path18.resolve(t))])
1508
+ ].filter((t) => !mergedTargets.has(t) && fs12.existsSync(t));
1509
+ const deleteTargets = planTargets.filter(
1510
+ (t) => !retained.has(t) && !(ctx.preserveEngram && path18.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
1511
+ );
1512
+ const sharedKept = planTargets.length - deleteTargets.length;
1513
+ p2.log.step(`${adapter.name} \u2192 ${configDir}`);
1514
+ p2.log.info(`${deleteTargets.length} archivos a borrar, ${unmerge.length} archivos compartidos a limpiar`);
1515
+ if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
1516
+ if (opts.dryRun) continue;
1517
+ const backup = createBackup(
1518
+ [...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs12.existsSync(t))],
1519
+ `uninstall-${id}`
1520
+ );
1521
+ if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
1522
+ for (const target of deleteTargets) {
1523
+ fs12.rmSync(target, { force: true });
1524
+ pruneEmptyDirs(target, pruneRoot);
1525
+ }
1526
+ for (const action of unmerge) {
1527
+ if (action.kind !== "write") continue;
1528
+ if (action.content.trim() === "") {
1529
+ fs12.rmSync(action.target, { force: true });
1530
+ } else {
1531
+ writeText(action.target, action.content);
1532
+ }
1533
+ }
1534
+ if (usingRealConfig) removeRuntimeManifest(id);
1535
+ p2.log.success(`${adapter.name}: stack retirado (lo tuyo queda intacto).`);
1536
+ }
1537
+ p2.outro(opts.dryRun ? "Dry-run: no se ha tocado nada." : "Hecho. Usa 'restore' si quieres volver atr\xE1s.");
1538
+ return 0;
1539
+ }
1540
+
1541
+ // src/doctor.ts
1542
+ import path19 from "path";
1543
+ import fs13 from "fs";
1544
+ import * as p3 from "@clack/prompts";
1545
+ function engramVersion(bin) {
1546
+ const out = runDetectedBin(bin, ["--version"], 5e3);
1547
+ if (out === null) return null;
1548
+ return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
1549
+ }
1550
+ function context7KeyConfigured(id, configDir) {
1551
+ const file = id === "codex" ? path19.join(configDir, "config.toml") : id === "claude-code" ? path19.join(path19.dirname(configDir), `${path19.basename(configDir)}.json`) : path19.join(configDir, "opencode.json");
1552
+ const content = readTextIfExists(file);
1553
+ if (content === null) return null;
1554
+ const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
1555
+ if (!match) return null;
1556
+ return match[1] !== "";
1557
+ }
1558
+ async function runDoctor() {
1559
+ p3.intro("jorgex-stack doctor");
1560
+ let problems = 0;
1561
+ const engramBin = detectEngram();
1562
+ if (engramBin === null) {
1563
+ p3.log.warn("Engram: NO detectado. El protocolo de memoria no funcionar\xE1 \u2014 inst\xE1lalo: github.com/Gentleman-Programming/engram");
1564
+ problems++;
1565
+ } else {
1566
+ const version = engramVersion(engramBin);
1567
+ if (version === null) {
1568
+ p3.log.error(`Engram: binario en ${engramBin} pero no responde a --version.`);
1569
+ problems++;
1570
+ } else {
1571
+ p3.log.success(`Engram: ${version} (${engramBin})`);
1572
+ }
1573
+ }
1574
+ const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path19.join(HOME, ".engram");
1575
+ const engramDb = path19.join(engramDataDir, "engram.db");
1576
+ if (fs13.existsSync(engramDb)) {
1577
+ const sizeMb = (fs13.statSync(engramDb).size / 1024 / 1024).toFixed(1);
1578
+ p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
1579
+ }
1580
+ if (!fs13.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
1581
+ const manifest = readManifest();
1582
+ const current = collectAllCurrentTargets();
1583
+ for (const adapter of Object.values(ADAPTERS)) {
1584
+ const detection = adapter.detect();
1585
+ if (!detection.installed) {
1586
+ p3.log.warn(`${adapter.name}: no instalado en esta m\xE1quina.`);
1587
+ continue;
1588
+ }
1589
+ const ctx = makeContext(adapter, detection.configDir);
1590
+ if (!ctx) continue;
1591
+ let pending;
1592
+ try {
1593
+ pending = diffPlan(buildPlan(adapter, ctx)).filter((d) => d.status !== "unchanged").length;
1594
+ } catch (err) {
1595
+ p3.log.error(
1596
+ `${adapter.name}: config ilegible en ${detection.configDir} \u2014 ${err instanceof Error ? err.message : err}`
1597
+ );
1598
+ problems++;
1599
+ continue;
1600
+ }
1601
+ if (pending > 0) {
1602
+ p3.log.warn(`${adapter.name}: ${pending} archivos gestionados desactualizados o ausentes \u2192 ejecuta 'sync'.`);
1603
+ problems++;
1604
+ } else {
1605
+ p3.log.success(`${adapter.name}: config del stack al d\xEDa (${detection.configDir}).`);
1606
+ }
1607
+ const prev = manifest.runtimes[adapter.id];
1608
+ const orphans = prev && current.complete ? findOrphans(prev.owned, current.targets) : [];
1609
+ if (orphans.length > 0) {
1610
+ p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
1611
+ problems++;
1612
+ }
1613
+ if (adapter.id === "codex" && fs13.existsSync(path19.join(detection.configDir, "hooks.json"))) {
1614
+ p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
1615
+ }
1616
+ if (adapter.id === "codex" && fs13.existsSync(path19.join(detection.configDir, "AGENTS.override.md"))) {
1617
+ p3.log.warn(
1618
+ "Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
1619
+ );
1620
+ problems++;
1621
+ }
1622
+ const key = context7KeyConfigured(adapter.id, detection.configDir);
1623
+ if (key === false) p3.log.info(`${adapter.name}: context7 sin key (opcional \u2014 con\xE9ctala cuando quieras).`);
1624
+ }
1625
+ p3.outro(problems === 0 ? "Todo sano." : `${problems} avisos \u2014 revisa arriba.`);
1626
+ return problems > 0 ? 1 : 0;
1627
+ }
1628
+
1629
+ // src/update.ts
1630
+ import fs16 from "fs";
1631
+ import path22 from "path";
1632
+ import os3 from "os";
1633
+ import { execFileSync as execFileSync4 } from "child_process";
1634
+ import * as p4 from "@clack/prompts";
1635
+
1636
+ // src/lib/github.ts
1637
+ import fs14 from "fs";
1638
+ import path20 from "path";
1639
+ import { execFileSync as execFileSync2 } from "child_process";
1640
+ import os2 from "os";
1641
+ import { Readable } from "stream";
1642
+ import { pipeline } from "stream/promises";
1643
+ var cachedToken;
1644
+ var ghTokenFailed = false;
1645
+ function githubToken() {
1646
+ if (cachedToken !== void 0) return cachedToken;
1647
+ const env = process.env["GH_TOKEN"]?.trim() || process.env["GITHUB_TOKEN"]?.trim();
1648
+ if (env) return cachedToken = env;
1649
+ const gh = lookPath("gh");
1650
+ if (gh) {
1651
+ const fromGh = runDetectedBin(gh, ["auth", "token"], 5e3)?.trim();
1652
+ if (fromGh) return cachedToken = fromGh;
1653
+ ghTokenFailed = true;
1654
+ }
1655
+ return cachedToken = null;
1656
+ }
1657
+ function ghPresentButTokenFailed() {
1658
+ return ghTokenFailed;
1659
+ }
1660
+ function authHeaders() {
1661
+ const token = githubToken();
1662
+ return token ? { Authorization: `Bearer ${token}` } : {};
1663
+ }
1664
+ var rateLimitHit = false;
1665
+ function githubRateLimited() {
1666
+ return rateLimitHit;
1667
+ }
1668
+ function githubHeaders() {
1669
+ return {
1670
+ Accept: "application/vnd.github+json",
1671
+ "User-Agent": "jorgex-stack",
1672
+ ...authHeaders()
1673
+ };
1674
+ }
1675
+ async function latestGithubRelease(repo) {
1676
+ try {
1677
+ const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
1678
+ headers: githubHeaders(),
1679
+ signal: AbortSignal.timeout(1e4)
1680
+ });
1681
+ if (!res.ok) {
1682
+ if (res.status === 403 || res.status === 429) rateLimitHit = true;
1683
+ return null;
1684
+ }
1685
+ const data = await res.json();
1686
+ return data.tag_name?.replace(/^v/, "") ?? null;
1687
+ } catch {
1688
+ return null;
1689
+ }
1690
+ }
1691
+ async function latestGithubCommit(repo) {
1692
+ try {
1693
+ const res = await fetch(`https://api.github.com/repos/${repo}/commits?per_page=1`, {
1694
+ headers: githubHeaders(),
1695
+ signal: AbortSignal.timeout(1e4)
1696
+ });
1697
+ if (!res.ok) {
1698
+ if (res.status === 403 || res.status === 429) rateLimitHit = true;
1699
+ return null;
1700
+ }
1701
+ const data = await res.json();
1702
+ return data[0]?.sha ?? null;
1703
+ } catch {
1704
+ return null;
1705
+ }
1706
+ }
1707
+ function validateExtractedTree(destDir) {
1708
+ const resolved = path20.resolve(destDir);
1709
+ const walk = (dir) => {
1710
+ let entries;
1711
+ try {
1712
+ entries = fs14.readdirSync(dir, { withFileTypes: true });
1713
+ } catch {
1714
+ return false;
1715
+ }
1716
+ for (const entry of entries) {
1717
+ const full = path20.join(dir, entry.name);
1718
+ let stat;
1719
+ try {
1720
+ stat = fs14.lstatSync(full);
1721
+ } catch {
1722
+ return false;
1723
+ }
1724
+ if (stat.isSymbolicLink()) return false;
1725
+ if (!isContainedIn(full, resolved)) return false;
1726
+ if (entry.isDirectory()) {
1727
+ if (!walk(full)) return false;
1728
+ }
1729
+ }
1730
+ return true;
1731
+ };
1732
+ return walk(resolved);
1733
+ }
1734
+ function resolveTarBin() {
1735
+ if (process.platform !== "win32") return "tar";
1736
+ const winTar = path20.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
1737
+ return fs14.existsSync(winTar) ? winTar : "tar";
1738
+ }
1739
+ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
1740
+ const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
1741
+ const tmp = path20.join(os2.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
1742
+ const fail = (reason) => {
1743
+ try {
1744
+ fs14.rmSync(destDir, { recursive: true, force: true });
1745
+ } catch {
1746
+ }
1747
+ return { ok: false, reason };
1748
+ };
1749
+ try {
1750
+ const res = await fetch(url, {
1751
+ headers: { "User-Agent": "jorgex-stack", ...authHeaders() },
1752
+ signal: AbortSignal.timeout(12e4)
1753
+ });
1754
+ if (!res.ok) {
1755
+ if (res.status === 403 || res.status === 429) rateLimitHit = true;
1756
+ return fail(
1757
+ `HTTP ${res.status}${res.status === 403 || res.status === 429 ? " \u2014 rate limit; define GH_TOKEN o inicia sesi\xF3n en gh" : ""}`
1758
+ );
1759
+ }
1760
+ if (!res.body) return fail("respuesta HTTP sin cuerpo");
1761
+ await pipeline(
1762
+ Readable.fromWeb(res.body),
1763
+ fs14.createWriteStream(tmp)
1764
+ );
1765
+ fs14.rmSync(destDir, { recursive: true, force: true });
1766
+ fs14.mkdirSync(destDir, { recursive: true });
1767
+ try {
1768
+ execFileSync2(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
1769
+ } catch (err) {
1770
+ const e = err;
1771
+ const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
1772
+ return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
1773
+ }
1774
+ const resolvedDest = path20.resolve(destDir);
1775
+ const validateRoot = validateSubdir ? path20.resolve(resolvedDest, validateSubdir) : resolvedDest;
1776
+ if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
1777
+ return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
1778
+ }
1779
+ if (fs14.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
1780
+ return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
1781
+ }
1782
+ const validated = fs14.existsSync(validateRoot);
1783
+ return { ok: true, validated };
1784
+ } catch (err) {
1785
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
1786
+ return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
1787
+ } finally {
1788
+ try {
1789
+ fs14.rmSync(tmp, { force: true });
1790
+ } catch {
1791
+ }
1792
+ }
1793
+ }
1794
+
1795
+ // src/lib/skill-update.ts
1796
+ import fs15 from "fs";
1797
+ import path21 from "path";
1798
+ import { execFileSync as execFileSync3 } from "child_process";
1799
+ var PROTECTED_SKILLS = /* @__PURE__ */ new Set(["agent-delegation", "work-lifecycle"]);
1800
+ function sameTextContentNormalized(a, b) {
1801
+ const ba = fs15.readFileSync(a);
1802
+ const bb = fs15.readFileSync(b);
1803
+ if (ba.equals(bb)) return true;
1804
+ const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
1805
+ const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
1806
+ return sa === sb;
1807
+ }
1808
+ function diffSkillDirs(upstreamDir, localDir) {
1809
+ const upstreamFiles = new Set(
1810
+ listFilesRecursive(upstreamDir).map((f) => path21.relative(upstreamDir, f))
1811
+ );
1812
+ const localFiles = new Set(
1813
+ listFilesRecursive(localDir).map((f) => path21.relative(localDir, f))
1814
+ );
1815
+ const added = [];
1816
+ const modified = [];
1817
+ const deleted = [];
1818
+ for (const rel of upstreamFiles) {
1819
+ if (!localFiles.has(rel)) {
1820
+ added.push(rel);
1821
+ } else if (!sameTextContentNormalized(path21.join(upstreamDir, rel), path21.join(localDir, rel))) {
1822
+ modified.push(rel);
1823
+ }
1824
+ }
1825
+ for (const rel of localFiles) {
1826
+ if (!upstreamFiles.has(rel)) {
1827
+ deleted.push(rel);
1828
+ }
1829
+ }
1830
+ return {
1831
+ added: added.sort(),
1832
+ modified: modified.sort(),
1833
+ deleted: deleted.sort()
1834
+ };
1835
+ }
1836
+ var DIFF_MAX_LINES = 400;
1837
+ function renderSkillDiff(upstreamDir, localDir) {
1838
+ try {
1839
+ execFileSync3("git", ["diff", "--no-index", "--stat", "--", localDir, upstreamDir], {
1840
+ stdio: "pipe",
1841
+ encoding: "utf8"
1842
+ });
1843
+ return "";
1844
+ } catch (statErr) {
1845
+ const se = statErr;
1846
+ if (typeof se.stdout !== "string" || se.status !== 1) {
1847
+ const d = diffSkillDirs(upstreamDir, localDir);
1848
+ const total = d.added.length + d.modified.length + d.deleted.length;
1849
+ if (total === 0) return "";
1850
+ return `[git no disponible \u2014 resumen de cambios]
1851
+ a\xF1adidos: ${d.added.length}, modificados: ${d.modified.length}, eliminados: ${d.deleted.length}`;
1852
+ }
1853
+ const stat = se.stdout;
1854
+ let fullDiff = "";
1855
+ try {
1856
+ execFileSync3("git", ["diff", "--no-index", "--", localDir, upstreamDir], {
1857
+ stdio: "pipe",
1858
+ encoding: "utf8"
1859
+ });
1860
+ } catch (diffErr) {
1861
+ const de = diffErr;
1862
+ if (typeof de.stdout === "string" && de.status === 1) {
1863
+ const lines = de.stdout.split("\n");
1864
+ if (lines.length > DIFF_MAX_LINES) {
1865
+ fullDiff = lines.slice(0, DIFF_MAX_LINES).join("\n") + `
1866
+ (truncado \u2014 ${lines.length - DIFF_MAX_LINES} l\xEDneas omitidas)`;
1867
+ } else {
1868
+ fullDiff = de.stdout;
1869
+ }
1870
+ }
1871
+ }
1872
+ return fullDiff ? `${stat}
1873
+ ${fullDiff}` : stat;
1874
+ }
1875
+ }
1876
+ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
1877
+ const upstreamsFilePath = opts?.upstreamsFilePath;
1878
+ const localSkillsRoot = opts?.localSkillsRoot;
1879
+ const backupsRoot2 = opts?.backupsRoot;
1880
+ if (PROTECTED_SKILLS.has(name)) {
1881
+ throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
1882
+ }
1883
+ const upstreamsFile = upstreamsFilePath ?? path21.join(path21.dirname(stackRoot()), "upstreams.json");
1884
+ const raw = fs15.readFileSync(upstreamsFile, "utf8");
1885
+ const data = JSON.parse(raw);
1886
+ const skillEntry = data?.skills?.[name];
1887
+ if (!skillEntry) {
1888
+ throw new Error(`Skill "${name}" no encontrada en upstreams.json.`);
1889
+ }
1890
+ if (skillEntry.kind === "release") {
1891
+ throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
1892
+ }
1893
+ const skillsRoot = localSkillsRoot ?? path21.join(stackRoot(), "skills");
1894
+ const localSkillDir = path21.join(skillsRoot, name);
1895
+ const localFiles = listFilesRecursive(localSkillDir);
1896
+ if (localFiles.length > 0) {
1897
+ createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
1898
+ }
1899
+ const stagingDir = `${localSkillDir}.staging-${process.pid}`;
1900
+ try {
1901
+ const upstreamFiles = listFilesRecursive(upstreamSkillDir);
1902
+ for (const src of upstreamFiles) {
1903
+ const st = fs15.lstatSync(src);
1904
+ if (st.isSymbolicLink()) {
1905
+ throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
1906
+ }
1907
+ const rel = path21.relative(upstreamSkillDir, src);
1908
+ const dest = path21.join(stagingDir, rel);
1909
+ ensureDir(path21.dirname(dest));
1910
+ copyFile(src, dest);
1911
+ }
1912
+ const oldDir = `${localSkillDir}.old-${process.pid}`;
1913
+ if (fs15.existsSync(localSkillDir)) {
1914
+ fs15.renameSync(localSkillDir, oldDir);
1915
+ }
1916
+ fs15.renameSync(stagingDir, localSkillDir);
1917
+ fs15.rmSync(oldDir, { recursive: true, force: true });
1918
+ } catch (err) {
1919
+ try {
1920
+ fs15.rmSync(stagingDir, { recursive: true, force: true });
1921
+ } catch {
1922
+ }
1923
+ throw err;
1924
+ }
1925
+ skillEntry.commit = newCommit;
1926
+ writeText(upstreamsFile, JSON.stringify(data, null, 2) + "\n");
1927
+ }
1928
+
1929
+ // src/update.ts
1930
+ function rateLimitHint(prefix) {
1931
+ 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.`;
1932
+ }
1933
+ function loadUpstreams() {
1934
+ const file = path22.join(path22.dirname(stackRoot()), "upstreams.json");
1935
+ return JSON.parse(fs16.readFileSync(file, "utf8"));
1936
+ }
1937
+ async function latestNpmVersion(pkg) {
1938
+ try {
1939
+ const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
1940
+ signal: AbortSignal.timeout(1e4)
1941
+ });
1942
+ if (!res.ok) return null;
1943
+ const data = await res.json();
1944
+ return data.version ?? null;
1945
+ } catch {
1946
+ return null;
1947
+ }
1948
+ }
1949
+ async function runUpdateCheck(localVersion) {
1950
+ p4.intro("jorgex-stack update --check");
1951
+ const upstreams = loadUpstreams();
1952
+ const npmLatest = await latestNpmVersion("jorgex-stack");
1953
+ if (npmLatest === null)
1954
+ p4.log.info(`jorgex-stack: v${localVersion} local (a\xFAn no publicado en npm o sin red).`);
1955
+ else if (npmLatest === localVersion)
1956
+ p4.log.success(`jorgex-stack: v${localVersion} \u2014 al d\xEDa.`);
1957
+ else
1958
+ p4.log.warn(
1959
+ `jorgex-stack: v${localVersion} local, v${npmLatest} en npm \u2192 pnpm dlx jorgex-stack@latest`
1960
+ );
1961
+ const engramRepo = upstreams.tools["engram"]?.source.replace(/^github:/, "");
1962
+ if (engramRepo) {
1963
+ const bin = detectEngram();
1964
+ const local = bin ? engramVersion(bin) : null;
1965
+ const latest = await latestGithubRelease(engramRepo);
1966
+ if (local === null) p4.log.warn("engram: no detectado en esta m\xE1quina.");
1967
+ else if (latest === null)
1968
+ p4.log.info(`engram: ${local} local (no se pudo consultar el upstream).`);
1969
+ else if (latest === local) p4.log.success(`engram: ${local} \u2014 al d\xEDa.`);
1970
+ else
1971
+ p4.log.warn(
1972
+ `engram: ${local} local, ${latest} disponible. Tu instalaci\xF3n NO se toca (D7) \u2014 actualiza t\xFA: github.com/${engramRepo}/releases`
1973
+ );
1974
+ }
1975
+ const byRepo = /* @__PURE__ */ new Map();
1976
+ for (const [name, info] of Object.entries(upstreams.skills)) {
1977
+ const repo = info.source.replace(/^github:/, "");
1978
+ const entry = byRepo.get(repo) ?? { skills: [], pinned: info.commit };
1979
+ entry.skills.push(info.modified ? `${name} (modificada localmente)` : name);
1980
+ byRepo.set(repo, entry);
1981
+ }
1982
+ const heads = await Promise.all(
1983
+ [...byRepo.keys()].map(async (repo) => [repo, await latestGithubCommit(repo)])
1984
+ );
1985
+ let moved = 0;
1986
+ for (const [repo, head] of heads) {
1987
+ const { skills, pinned } = byRepo.get(repo);
1988
+ if (!pinned)
1989
+ p4.log.warn(
1990
+ `${repo}: sin pin en upstreams.json \u2014 a\xF1ade el commit revisado. Skills: ${skills.join(", ")}`
1991
+ );
1992
+ else if (head === null)
1993
+ p4.log.info(`${repo}: pin ${pinned.slice(0, 7)} (no se pudo consultar el upstream).`);
1994
+ else if (head === pinned)
1995
+ p4.log.success(`${repo}: al d\xEDa con el pin ${pinned.slice(0, 7)} (${skills.join(", ")}).`);
1996
+ else {
1997
+ moved++;
1998
+ p4.log.warn(
1999
+ `${repo}: el upstream se movi\xF3 (pin ${pinned.slice(0, 7)} \u2192 ${head.slice(0, 7)}). Skills: ${skills.join(", ")}.
2000
+ 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)}`
2001
+ );
2002
+ }
2003
+ }
2004
+ if (moved === 0 && heads.every(([, head]) => head !== null)) {
2005
+ p4.log.info("Skills de terceros: ning\xFAn upstream se ha movido respecto a su pin.");
2006
+ }
2007
+ if (githubRateLimited()) {
2008
+ p4.log.warn(rateLimitHint("GitHub limit\xF3 algunas consultas (rate limit sin token)."));
2009
+ }
2010
+ p4.outro("Check completado.");
2011
+ return 0;
2012
+ }
2013
+ function isEngramRunning() {
2014
+ try {
2015
+ if (process.platform === "win32") {
2016
+ const ps = lookPath("powershell") ?? lookPath("pwsh");
2017
+ if (ps) {
2018
+ const out = runDetectedBin(ps, ["-NoProfile", "-Command", "Get-Process -Name engram -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count"], 5e3);
2019
+ return out !== null ? parseInt(out.trim(), 10) > 0 : null;
2020
+ }
2021
+ return null;
2022
+ } else {
2023
+ const pgrep = lookPath("pgrep");
2024
+ if (!pgrep) return null;
2025
+ try {
2026
+ execFileSync4(pgrep, ["-x", "engram"], { stdio: "ignore" });
2027
+ return true;
2028
+ } catch {
2029
+ return false;
2030
+ }
2031
+ }
2032
+ } catch {
2033
+ return null;
2034
+ }
2035
+ }
2036
+ function isGitClone() {
2037
+ const projectRoot = path22.dirname(stackRoot());
2038
+ return fs16.existsSync(path22.join(projectRoot, ".git"));
2039
+ }
2040
+ var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
2041
+ function resolvePnpm() {
2042
+ const bin = lookPath("pnpm") ?? lookPath("pnpm.cmd") ?? lookPath("pnpm.ps1");
2043
+ if (!bin) throw new Error("pnpm no encontrado en PATH.");
2044
+ return bin;
2045
+ }
2046
+ function cleanupTmp(dir) {
2047
+ try {
2048
+ fs16.rmSync(dir, { recursive: true, force: true });
2049
+ } catch {
2050
+ }
2051
+ }
2052
+ function updateStackGitClone() {
2053
+ const projectRoot = path22.dirname(stackRoot());
2054
+ const git = lookPath("git");
2055
+ if (!git) throw new Error("git no encontrado en PATH.");
2056
+ const pnpm = resolvePnpm();
2057
+ p4.log.info("Ejecutando git pull\u2026");
2058
+ execFileSync4(git, ["pull"], { cwd: projectRoot, stdio: "inherit" });
2059
+ p4.log.info("Ejecutando pnpm install\u2026");
2060
+ execFileSync4(pnpm, ["install"], { cwd: projectRoot, stdio: "inherit" });
2061
+ p4.log.info("Ejecutando pnpm build\u2026");
2062
+ execFileSync4(pnpm, ["build"], { cwd: projectRoot, stdio: "inherit" });
2063
+ }
2064
+ function updateStackGlobal() {
2065
+ const pnpm = resolvePnpm();
2066
+ p4.log.info("Ejecutando pnpm add -g jorgex-stack@latest\u2026");
2067
+ execFileSync4(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
2068
+ }
2069
+ async function downloadSkillToTemp(repo, sha, skillPath) {
2070
+ const root = fs16.mkdtempSync(path22.join(os3.tmpdir(), "jorgex-skill-"));
2071
+ try {
2072
+ const result = await downloadRepoTarball(repo, sha, root, skillPath);
2073
+ if (!result.ok) {
2074
+ cleanupTmp(root);
2075
+ return { error: result.reason };
2076
+ }
2077
+ if (skillPath) {
2078
+ const sub = path22.resolve(path22.join(root, skillPath));
2079
+ if (!isContainedIn(sub, root)) {
2080
+ cleanupTmp(root);
2081
+ return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
2082
+ }
2083
+ if (fs16.existsSync(sub)) return { dir: sub, root };
2084
+ const lastSeg = skillPath.split("/").pop();
2085
+ const sub2 = path22.resolve(path22.join(root, lastSeg));
2086
+ if (isContainedIn(sub2, root) && fs16.existsSync(sub2)) {
2087
+ if (!validateExtractedTree(sub2)) {
2088
+ cleanupTmp(root);
2089
+ return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
2090
+ }
2091
+ return { dir: sub2, root };
2092
+ }
2093
+ cleanupTmp(root);
2094
+ return { error: `la ruta "${skillPath}" no existe en el tarball del upstream` };
2095
+ }
2096
+ return { dir: root, root };
2097
+ } catch (err) {
2098
+ cleanupTmp(root);
2099
+ return { error: err instanceof Error ? err.message : "error desconocido" };
2100
+ }
2101
+ }
2102
+ function pruneEngramDbBackups() {
2103
+ try {
2104
+ const dir = dataDir();
2105
+ if (!fs16.existsSync(dir)) return;
2106
+ const backups = fs16.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs16.statSync(path22.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2107
+ for (const old of backups.slice(3)) {
2108
+ try {
2109
+ fs16.rmSync(path22.join(dir, old.name));
2110
+ } catch {
2111
+ }
2112
+ }
2113
+ } catch {
2114
+ }
2115
+ }
2116
+ function rotateLockedBinary(binPath, sweepRoot = HOME) {
2117
+ if (!fs16.existsSync(binPath)) return null;
2118
+ const dir = path22.dirname(binPath);
2119
+ const base = path22.basename(binPath);
2120
+ const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
2121
+ const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
2122
+ const resolvedDir = path22.resolve(dir);
2123
+ if (resolvedDir === path22.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
2124
+ try {
2125
+ for (const entry of fs16.readdirSync(dir)) {
2126
+ if (oldPattern.test(entry)) {
2127
+ try {
2128
+ fs16.rmSync(path22.join(dir, entry), { force: true });
2129
+ } catch {
2130
+ }
2131
+ }
2132
+ }
2133
+ } catch {
2134
+ }
2135
+ }
2136
+ const rotated = path22.join(dir, `${base}.old-${Date.now()}`);
2137
+ fs16.renameSync(binPath, rotated);
2138
+ return rotated;
2139
+ }
2140
+ async function updateEngram(engramRepo, latestVersion) {
2141
+ if (isEngramRunning()) {
2142
+ p4.log.info(
2143
+ "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)."
2144
+ );
2145
+ }
2146
+ const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path22.join(HOME, ".engram");
2147
+ const engramDb = path22.join(engramDataDir, "engram.db");
2148
+ if (fs16.existsSync(engramDb)) {
2149
+ const doBackup = await p4.confirm({
2150
+ message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
2151
+ initialValue: true
2152
+ });
2153
+ if (!p4.isCancel(doBackup) && doBackup) {
2154
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2155
+ const dest = path22.join(dataDir(), `engram-db-backup-${ts}.db`);
2156
+ try {
2157
+ if (!fs16.existsSync(dataDir())) fs16.mkdirSync(dataDir(), { recursive: true });
2158
+ fs16.copyFileSync(engramDb, dest);
2159
+ p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
2160
+ pruneEngramDbBackups();
2161
+ } catch (err) {
2162
+ p4.log.warn(`No se pudo copiar la DB: ${err instanceof Error ? err.message : err}. Continuando sin backup.`);
2163
+ }
2164
+ }
2165
+ }
2166
+ let anyChannelTried = false;
2167
+ const brew = lookPath("brew");
2168
+ if (brew) {
2169
+ let brewManages = false;
2170
+ try {
2171
+ execFileSync4(brew, ["list", "engram"], { stdio: "pipe" });
2172
+ brewManages = true;
2173
+ } catch {
2174
+ }
2175
+ if (brewManages) {
2176
+ anyChannelTried = true;
2177
+ p4.log.info("Actualizando engram con brew\u2026");
2178
+ try {
2179
+ execFileSync4(brew, ["upgrade", "engram"], { stdio: "inherit" });
2180
+ return true;
2181
+ } catch (err) {
2182
+ p4.log.error(`brew upgrade engram fall\xF3: ${err instanceof Error ? err.message : err}`);
2183
+ return false;
2184
+ }
2185
+ }
2186
+ }
2187
+ const go = lookPath("go");
2188
+ if (go) {
2189
+ anyChannelTried = true;
2190
+ let rotated = null;
2191
+ const bin = detectEngram();
2192
+ if (process.platform === "win32" && bin) {
2193
+ try {
2194
+ rotated = rotateLockedBinary(bin);
2195
+ } catch (err) {
2196
+ p4.log.warn(
2197
+ `No se pudo rotar el binario en uso: ${err instanceof Error ? err.message : err}. go install puede fallar por bloqueo.`
2198
+ );
2199
+ }
2200
+ }
2201
+ p4.log.info(`Actualizando engram con go install (${latestVersion})\u2026`);
2202
+ try {
2203
+ execFileSync4(
2204
+ go,
2205
+ ["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
2206
+ { stdio: "inherit" }
2207
+ );
2208
+ const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs16.existsSync(bin ?? "") });
2209
+ if (rollbackOk.action === "restore") {
2210
+ try {
2211
+ fs16.renameSync(rotated, bin);
2212
+ p4.log.warn(rollbackOk.messages.onRestore);
2213
+ } catch {
2214
+ p4.log.warn(rollbackOk.messages.onRenameFail);
2215
+ }
2216
+ }
2217
+ return true;
2218
+ } catch (err) {
2219
+ const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs16.existsSync(bin ?? "") });
2220
+ if (rollbackFail.action === "restore") {
2221
+ try {
2222
+ fs16.renameSync(rotated, bin);
2223
+ p4.log.info(rollbackFail.messages.onRestore);
2224
+ } catch {
2225
+ p4.log.error(rollbackFail.messages.onRenameFail);
2226
+ }
2227
+ } else if (rollbackFail.action === "leave_old") {
2228
+ p4.log.warn(rollbackFail.messages.onLeaveOld);
2229
+ }
2230
+ p4.log.error(`go install fall\xF3: ${err instanceof Error ? err.message : err}`);
2231
+ return false;
2232
+ }
2233
+ }
2234
+ if (anyChannelTried) {
2235
+ p4.log.error(
2236
+ `Los canales de actualizaci\xF3n disponibles fallaron \u2014 revisa el error arriba; descarga manual: github.com/${engramRepo}/releases/tag/v${latestVersion}`
2237
+ );
2238
+ } else {
2239
+ p4.log.warn(
2240
+ `No se encontr\xF3 brew ni go en PATH.
2241
+ Descarga manual: github.com/${engramRepo}/releases/tag/v${latestVersion}`
2242
+ );
2243
+ }
2244
+ return false;
2245
+ }
2246
+ function resolveEngramRollback(input) {
2247
+ const { installOk, rotated, bin, binExists } = input;
2248
+ if (!rotated || !bin) return { action: "none" };
2249
+ if (installOk && !binExists) {
2250
+ return {
2251
+ action: "restore",
2252
+ messages: {
2253
+ 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.`,
2254
+ onRenameFail: `go install instal\xF3 en otra ruta y no se pudo restaurar ${bin}; tu binario anterior est\xE1 en ${rotated}.`
2255
+ }
2256
+ };
2257
+ }
2258
+ if (!installOk && !binExists) {
2259
+ return {
2260
+ action: "restore",
2261
+ messages: {
2262
+ onRestore: "Rotaci\xF3n revertida \u2014 el binario anterior sigue en su sitio.",
2263
+ onRenameFail: `No se pudo revertir la rotaci\xF3n: tu binario anterior est\xE1 en ${rotated} \u2014 ren\xF3mbralo a ${bin} a mano.`
2264
+ }
2265
+ };
2266
+ }
2267
+ if (!installOk && binExists) {
2268
+ return {
2269
+ action: "leave_old",
2270
+ messages: {
2271
+ onLeaveOld: `El install fall\xF3 pero dej\xF3 un binario en ${bin}; tu copia anterior queda en ${rotated}.`
2272
+ }
2273
+ };
2274
+ }
2275
+ return { action: "none" };
2276
+ }
2277
+ function buildEligibleSkillUpdates(skills) {
2278
+ const byRepo = /* @__PURE__ */ new Map();
2279
+ for (const { name, repo, info, head } of skills) {
2280
+ if (info.kind === "release") continue;
2281
+ const existing = byRepo.get(repo);
2282
+ if (!existing) {
2283
+ byRepo.set(repo, { names: [name], pinned: info.commit ?? "", headVal: head });
2284
+ } else {
2285
+ existing.names.push(name);
2286
+ }
2287
+ }
2288
+ const result = [];
2289
+ for (const [repo, { names, pinned, headVal }] of byRepo) {
2290
+ if (!pinned) continue;
2291
+ if (headVal === null) continue;
2292
+ if (headVal === pinned) continue;
2293
+ for (const name of names) {
2294
+ const skill = skills.find((s) => s.name === name);
2295
+ result.push({
2296
+ name,
2297
+ repo,
2298
+ head: headVal,
2299
+ pinned,
2300
+ skillPath: skill.info.path,
2301
+ modified: skill.info.modified ?? false
2302
+ });
2303
+ }
2304
+ }
2305
+ return result;
2306
+ }
2307
+ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
2308
+ if (dryRun || yes || !process.stdout.isTTY) {
2309
+ return { exitCode: await runUpdateCheck(localVersion), appliedUpdates: false };
2310
+ }
2311
+ p4.intro("jorgex-stack update");
2312
+ const upstreams = loadUpstreams();
2313
+ let exitCode = 0;
2314
+ let appliedUpdates = false;
2315
+ const spin = p4.spinner();
2316
+ spin.start("Consultando versiones upstream\u2026");
2317
+ const [npmLatest, engramLatestRaw, ...skillHeads] = await Promise.all([
2318
+ latestNpmVersion("jorgex-stack"),
2319
+ (async () => {
2320
+ const repo = upstreams.tools["engram"]?.source.replace(/^github:/, "");
2321
+ if (!repo) return null;
2322
+ return { repo, version: await latestGithubRelease(repo) };
2323
+ })(),
2324
+ ...Object.keys(upstreams.skills).map(async (name) => {
2325
+ const info = upstreams.skills[name];
2326
+ const repo = info.source.replace(/^github:/, "");
2327
+ const head = await latestGithubCommit(repo);
2328
+ return { name, repo, info, head };
2329
+ })
2330
+ ]);
2331
+ spin.stop("Consulta completada.");
2332
+ if (githubRateLimited()) {
2333
+ p4.log.warn(rateLimitHint("GitHub limit\xF3 algunas consultas \u2014 los upstreams 'sin conexi\xF3n' pueden ser eso."));
2334
+ }
2335
+ const updateItems = [];
2336
+ const engramData = engramLatestRaw;
2337
+ let stackNeedsUpdate = false;
2338
+ if (npmLatest === null) {
2339
+ p4.log.info(`jorgex-stack: v${localVersion} local (a\xFAn no publicado en npm o sin red).`);
2340
+ } else if (npmLatest === localVersion) {
2341
+ p4.log.success(`jorgex-stack: v${localVersion} \u2014 al d\xEDa.`);
2342
+ } else {
2343
+ stackNeedsUpdate = true;
2344
+ const mode = isGitClone() ? STACK_METHOD_CLONE : `pnpm add -g jorgex-stack@${npmLatest}`;
2345
+ updateItems.push({
2346
+ value: "stack",
2347
+ label: `jorgex-stack: v${localVersion} \u2192 v${npmLatest}`,
2348
+ hint: mode
2349
+ });
2350
+ }
2351
+ let engramNeedsUpdate = false;
2352
+ let engramLocalVersion = null;
2353
+ if (engramData) {
2354
+ const engramBinLocal = detectEngram();
2355
+ engramLocalVersion = engramBinLocal ? engramVersion(engramBinLocal) : null;
2356
+ if (engramLocalVersion === null) {
2357
+ p4.log.warn("engram: no detectado en esta m\xE1quina.");
2358
+ } else if (engramData.version === null) {
2359
+ p4.log.info(`engram: ${engramLocalVersion} local (no se pudo consultar el upstream).`);
2360
+ } else if (engramData.version === engramLocalVersion) {
2361
+ p4.log.success(`engram: ${engramLocalVersion} \u2014 al d\xEDa.`);
2362
+ } else {
2363
+ engramNeedsUpdate = true;
2364
+ updateItems.push({
2365
+ value: "engram",
2366
+ label: `engram: ${engramLocalVersion} \u2192 ${engramData.version}`,
2367
+ hint: "canal nativo (brew \u2192 go install \u2192 URL)"
2368
+ });
2369
+ }
2370
+ }
2371
+ const typedSkillHeads = skillHeads;
2372
+ const loggedRepos = /* @__PURE__ */ new Map();
2373
+ for (const { name, repo, info, head } of typedSkillHeads) {
2374
+ if (info.kind === "release") {
2375
+ if (head !== null && info.commit && head !== info.commit) {
2376
+ p4.log.warn(`${name} (release): upstream se movi\xF3. Revisa: github.com/${repo}/releases`);
2377
+ }
2378
+ continue;
2379
+ }
2380
+ const existing = loggedRepos.get(repo);
2381
+ if (!existing) {
2382
+ loggedRepos.set(repo, { names: [name], pinned: info.commit ?? "", headVal: head, anyRelease: false });
2383
+ } else {
2384
+ existing.names.push(name);
2385
+ }
2386
+ }
2387
+ for (const [repo, { names, pinned, headVal }] of loggedRepos) {
2388
+ if (!pinned) {
2389
+ p4.log.warn(`${repo}: sin pin \u2014 no se puede actualizar. Skills: ${names.join(", ")}`);
2390
+ } else if (headVal === null) {
2391
+ p4.log.info(`${repo}: sin conexi\xF3n al upstream. Skills: ${names.join(", ")}`);
2392
+ } else if (headVal === pinned) {
2393
+ p4.log.success(`${repo}: al d\xEDa (pin ${pinned.slice(0, 7)}). Skills: ${names.join(", ")}`);
2394
+ }
2395
+ }
2396
+ const skillUpdates = buildEligibleSkillUpdates(typedSkillHeads);
2397
+ for (const skillInfo of skillUpdates) {
2398
+ const modifiedWarning = skillInfo.modified ? " \u26A0 modificada localmente" : "";
2399
+ updateItems.push({
2400
+ value: `skill:${skillInfo.name}`,
2401
+ label: `skill ${skillInfo.name}: ${skillInfo.pinned.slice(0, 7)} \u2192 ${skillInfo.head.slice(0, 7)}${modifiedWarning}`,
2402
+ hint: `github.com/${skillInfo.repo}/compare/${skillInfo.pinned.slice(0, 7)}...${skillInfo.head.slice(0, 7)}`
2403
+ });
2404
+ }
2405
+ if (updateItems.length === 0) {
2406
+ p4.outro("Todo al d\xEDa. No hay actualizaciones disponibles.");
2407
+ return { exitCode: 0, appliedUpdates: false };
2408
+ }
2409
+ const selected = await p4.multiselect({
2410
+ message: "Selecciona qu\xE9 actualizar (espacio para marcar, intro para confirmar):",
2411
+ options: updateItems,
2412
+ initialValues: updateItems.map((i) => i.value),
2413
+ required: false
2414
+ });
2415
+ if (p4.isCancel(selected)) {
2416
+ p4.outro("Update cancelado.");
2417
+ return { exitCode: 0, appliedUpdates: false };
2418
+ }
2419
+ if (selected.length === 0) {
2420
+ p4.outro("Nada seleccionado.");
2421
+ return { exitCode: 0, appliedUpdates: false };
2422
+ }
2423
+ const sel = selected;
2424
+ if (stackNeedsUpdate && sel.includes("stack")) {
2425
+ const isClone = isGitClone();
2426
+ const method = isClone ? STACK_METHOD_CLONE : "pnpm add -g jorgex-stack@latest";
2427
+ const confirm5 = await p4.confirm({
2428
+ message: `Actualizar el stack con: ${method}`,
2429
+ initialValue: true
2430
+ });
2431
+ if (p4.isCancel(confirm5) || !confirm5) {
2432
+ p4.log.info("Stack: actualizaci\xF3n omitida.");
2433
+ } else {
2434
+ try {
2435
+ if (isClone) {
2436
+ updateStackGitClone();
2437
+ } else {
2438
+ updateStackGlobal();
2439
+ }
2440
+ p4.log.success("Stack actualizado correctamente.");
2441
+ appliedUpdates = true;
2442
+ } catch (err) {
2443
+ p4.log.error(`Stack: error al actualizar \u2014 ${err instanceof Error ? err.message : err}`);
2444
+ exitCode = 1;
2445
+ }
2446
+ }
2447
+ }
2448
+ for (const skillInfo of skillUpdates) {
2449
+ if (!sel.includes(`skill:${skillInfo.name}`)) continue;
2450
+ if (skillInfo.modified) {
2451
+ p4.log.warn(
2452
+ `skill ${skillInfo.name}: TIENE CAMBIOS LOCALES. Actualizar sobreescribir\xE1 esas modificaciones (backup autom\xE1tico).`
2453
+ );
2454
+ const forceConfirm = await p4.confirm({
2455
+ message: `\xBFConfirmas sobreescribir los cambios locales de ${skillInfo.name}?`,
2456
+ initialValue: false
2457
+ });
2458
+ if (p4.isCancel(forceConfirm) || !forceConfirm) {
2459
+ p4.log.info(`skill ${skillInfo.name}: omitida.`);
2460
+ continue;
2461
+ }
2462
+ }
2463
+ const spin2 = p4.spinner();
2464
+ spin2.start(`Descargando upstream de ${skillInfo.name}\u2026`);
2465
+ const tmpResult = await downloadSkillToTemp(skillInfo.repo, skillInfo.head, skillInfo.skillPath);
2466
+ spin2.stop("error" in tmpResult ? "Error de descarga." : "Descargado.");
2467
+ if ("error" in tmpResult) {
2468
+ p4.log.error(`skill ${skillInfo.name}: descarga fallida \u2014 ${tmpResult.error}. Omitiendo.`);
2469
+ exitCode = 1;
2470
+ continue;
2471
+ }
2472
+ const { dir: tmpDir, root: tmpRoot } = tmpResult;
2473
+ const localSkillDir = path22.join(stackRoot(), "skills", skillInfo.name);
2474
+ const diff = renderSkillDiff(tmpDir, localSkillDir);
2475
+ if (diff) {
2476
+ p4.log.info(`Diff de ${skillInfo.name}:
2477
+ ${diff}`);
2478
+ } else {
2479
+ p4.log.info(`${skillInfo.name}: sin cambios en contenido (el repo se movi\xF3 pero la skill no cambi\xF3).`);
2480
+ cleanupTmp(tmpRoot);
2481
+ continue;
2482
+ }
2483
+ const applySkill = await p4.confirm({
2484
+ message: `\xBFAplicar la actualizaci\xF3n de ${skillInfo.name}?`,
2485
+ // Default No — política deliberate-pin (el diff es de contenido)
2486
+ initialValue: false
2487
+ });
2488
+ if (p4.isCancel(applySkill) || !applySkill) {
2489
+ p4.log.info(`skill ${skillInfo.name}: omitida.`);
2490
+ cleanupTmp(tmpRoot);
2491
+ continue;
2492
+ }
2493
+ try {
2494
+ replaceSkill(skillInfo.name, tmpDir, skillInfo.head, {});
2495
+ p4.log.success(`skill ${skillInfo.name}: actualizada y re-pineada a ${skillInfo.head.slice(0, 7)}.`);
2496
+ appliedUpdates = true;
2497
+ } catch (err) {
2498
+ p4.log.error(`skill ${skillInfo.name}: error \u2014 ${err instanceof Error ? err.message : err}`);
2499
+ exitCode = 1;
2500
+ } finally {
2501
+ cleanupTmp(tmpRoot);
2502
+ }
2503
+ }
2504
+ if (engramNeedsUpdate && sel.includes("engram") && engramData?.version) {
2505
+ const confirmEngram = await p4.confirm({
2506
+ message: `Actualizar engram a v${engramData.version} (canal nativo: brew \u2192 go install \u2192 URL)`,
2507
+ initialValue: true
2508
+ });
2509
+ if (p4.isCancel(confirmEngram) || !confirmEngram) {
2510
+ p4.log.info("Engram: actualizaci\xF3n omitida.");
2511
+ } else {
2512
+ const ok = await updateEngram(engramData.repo, engramData.version);
2513
+ if (!ok) {
2514
+ exitCode = 1;
2515
+ } else {
2516
+ const binPost = detectEngram();
2517
+ const versionPost = binPost ? engramVersion(binPost) : null;
2518
+ if (versionPost) {
2519
+ p4.log.success(`Engram: ahora en v${versionPost}.`);
2520
+ } else {
2521
+ p4.log.warn("Engram actualizado, pero no se pudo verificar la versi\xF3n \u2014 comprueba con engram --version");
2522
+ }
2523
+ p4.log.info("Reinicia los clientes (Claude Code/OpenCode/Codex) para que sus MCP usen la versi\xF3n nueva.");
2524
+ }
2525
+ }
2526
+ }
2527
+ p4.outro(exitCode === 0 ? "Update completado." : "Update completado con errores (revisa arriba).");
2528
+ return { exitCode, appliedUpdates };
2529
+ }
2530
+
2531
+ // src/models-picker.ts
2532
+ import path23 from "path";
2533
+ import * as p5 from "@clack/prompts";
2534
+ var TIERS = ["strong", "standard", "cheap"];
2535
+ var EFFORTS = ["low", "medium", "high", "xhigh"];
2536
+ var CLAUDE_ALIASES = ["fable", "opus", "sonnet", "haiku", "inherit"];
2537
+ var CODEX_MODELS = ["default", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
2538
+ var CODEX_CUSTOM = "__custom__";
2539
+ var CANCEL = /* @__PURE__ */ Symbol("cancel");
2540
+ function opencodeLiveModels(binPath) {
2541
+ const out = runDetectedBin(binPath, ["models"], 2e4);
2542
+ if (out === null) return null;
2543
+ const models = out.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== "" && l.includes("/"));
2544
+ return models.length > 0 ? models : null;
2545
+ }
2546
+ function agentsByTier() {
2547
+ const grouped = { strong: [], standard: [], cheap: [] };
2548
+ for (const agent of loadCanonicalAgents(path23.join(stackRoot(), "agents"))) {
2549
+ if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
2550
+ }
2551
+ return grouped;
2552
+ }
2553
+ async function askModel(det, subject, current) {
2554
+ if (det.id === "codex") {
2555
+ const effort = await p5.select({
2556
+ message: `${det.name} \xB7 ${subject} \u2014 reasoning effort`,
2557
+ options: EFFORTS.map((v) => ({ value: v, label: v })),
2558
+ initialValue: current.variant ?? "medium"
2559
+ });
2560
+ if (p5.isCancel(effort)) return CANCEL;
2561
+ const modelOptions = [
2562
+ ...CODEX_MODELS.includes(current.model) ? [] : [{ value: current.model, label: `${current.model} (actual)` }],
2563
+ ...CODEX_MODELS.map((m) => ({
2564
+ value: m,
2565
+ label: m === "default" ? "default \u2014 el del CLI, se actualiza solo (recomendado)" : m
2566
+ })),
2567
+ { value: CODEX_CUSTOM, label: "otro\u2026 (escribir un ID a mano)" }
2568
+ ];
2569
+ let model = await p5.select({
2570
+ message: `${det.name} \xB7 ${subject} \u2014 modelo`,
2571
+ options: modelOptions,
2572
+ initialValue: current.model
2573
+ });
2574
+ if (p5.isCancel(model)) return CANCEL;
2575
+ if (model === CODEX_CUSTOM) {
2576
+ const typed = await p5.text({
2577
+ message: `${det.name} \xB7 ${subject} \u2014 ID exacto del modelo (min\xFAsculas; compru\xE9balo con /model dentro de codex)`,
2578
+ initialValue: current.model,
2579
+ validate: (v) => !v || v.trim() === "" ? 'Vac\xEDo no \u2014 usa "default" o un ID.' : void 0
2580
+ });
2581
+ if (p5.isCancel(typed)) return CANCEL;
2582
+ model = typed.trim().toLowerCase();
2583
+ }
2584
+ return { model, variant: effort };
2585
+ }
2586
+ const optionsList = det.options;
2587
+ const options = optionsList.map((m) => ({ value: m, label: m }));
2588
+ if (!optionsList.includes(current.model)) options.unshift({ value: current.model, label: `${current.model} (actual)` });
2589
+ const choice = await p5.select({
2590
+ message: `${det.name} \xB7 ${subject} \u2014 modelo`,
2591
+ options,
2592
+ initialValue: current.model,
2593
+ maxItems: 12
2594
+ });
2595
+ if (p5.isCancel(choice)) return CANCEL;
2596
+ if (det.id === "claude-code") return { model: choice };
2597
+ const keptCurrent = choice === current.model && current.variant ? current.variant : null;
2598
+ const variant = await p5.select({
2599
+ message: `${det.name} \xB7 ${subject} \u2014 reasoning effort (variant; solo si el modelo lo soporta)`,
2600
+ options: [
2601
+ { value: "", label: "(sin variant \u2014 el default del modelo)" },
2602
+ ...EFFORTS.map((v) => ({ value: v, label: v })),
2603
+ ...keptCurrent && !EFFORTS.includes(keptCurrent) ? [{ value: keptCurrent, label: `${keptCurrent} (actual)` }] : []
2604
+ ],
2605
+ initialValue: keptCurrent ?? ""
2606
+ });
2607
+ if (p5.isCancel(variant)) return CANCEL;
2608
+ return { model: choice, ...variant ? { variant } : {} };
2609
+ }
2610
+ async function runModelsPicker(opts) {
2611
+ const file = ensureModelMapFile();
2612
+ if (opts.yes || !process.stdout.isTTY) {
2613
+ console.log(`Model-map en ${file} (defaults). Ed\xEDtalo o ejecuta 'models' sin --yes para el picker.`);
2614
+ return 0;
2615
+ }
2616
+ p5.intro("jorgex-stack models \u2014 modelos por tier o por subagente");
2617
+ const map = loadModelMap();
2618
+ const grouped = agentsByTier();
2619
+ const tierLine = (tier) => grouped[tier].join(", ");
2620
+ const agentCount = TIERS.reduce((n, tier) => n + grouped[tier].length, 0);
2621
+ const detections = [];
2622
+ if (opts.runtimes.includes("opencode")) {
2623
+ const opencode = detectOpenCode();
2624
+ detections.push({
2625
+ id: "opencode",
2626
+ name: "OpenCode",
2627
+ installed: opencode.installed,
2628
+ options: opencode.binPath ? opencodeLiveModels(opencode.binPath) : null
2629
+ });
2630
+ }
2631
+ if (opts.runtimes.includes("claude-code")) {
2632
+ detections.push({ id: "claude-code", name: "Claude Code", installed: detectClaudeCode().installed, options: CLAUDE_ALIASES });
2633
+ }
2634
+ if (opts.runtimes.includes("codex")) {
2635
+ detections.push({ id: "codex", name: "Codex CLI", installed: detectCodex().installed, options: null });
2636
+ }
2637
+ for (const det of detections) {
2638
+ if (!det.installed) {
2639
+ p5.log.info(`${det.name}: no instalado \u2014 se mantienen los defaults.`);
2640
+ continue;
2641
+ }
2642
+ if (det.id === "opencode" && det.options === null) {
2643
+ p5.log.warn("OpenCode: no se pudo listar `opencode models` \u2014 se mantiene la selecci\xF3n actual.");
2644
+ continue;
2645
+ }
2646
+ const runtimeMap = { ...map[det.id] };
2647
+ p5.log.message(
2648
+ `${det.name} \u2014 tiers y sus subagentes:
2649
+ strong \u2192 ${tierLine("strong")}
2650
+ standard \u2192 ${tierLine("standard")}
2651
+ cheap \u2192 ${tierLine("cheap")}`
2652
+ );
2653
+ const mode = await p5.select({
2654
+ message: `${det.name} \u2014 \xBFc\xF3mo asignar los modelos?`,
2655
+ options: [
2656
+ { value: "tier", label: "Por tier \u2014 3 grupos (r\xE1pido)" },
2657
+ { value: "agent", label: `Por subagente \u2014 uno a uno (${agentCount} subagentes, control total)` }
2658
+ ],
2659
+ initialValue: "tier"
2660
+ });
2661
+ if (p5.isCancel(mode)) return cancelled();
2662
+ if (mode === "tier") {
2663
+ for (const tier of TIERS) {
2664
+ const asked = await askModel(det, `tier ${tier} (${tierLine(tier)})`, runtimeMap[tier]);
2665
+ if (asked === CANCEL) return cancelled();
2666
+ runtimeMap[tier] = asked;
2667
+ }
2668
+ const overrideCount = Object.keys(runtimeMap.overrides ?? {}).length;
2669
+ if (overrideCount > 0) {
2670
+ p5.log.info(
2671
+ `${det.name}: se conservan ${overrideCount} ajustes por subagente previos \u2014 elige "Por subagente" para revisarlos.`
2672
+ );
2673
+ }
2674
+ } else {
2675
+ const overrides = { ...runtimeMap.overrides ?? {} };
2676
+ for (const tier of TIERS) {
2677
+ const base = runtimeMap[tier];
2678
+ for (const name of grouped[tier]) {
2679
+ const asked = await askModel(det, `${name} (tier ${tier})`, resolveAgentModel(runtimeMap, name, tier));
2680
+ if (asked === CANCEL) return cancelled();
2681
+ const sameAsTier = asked.model === base.model && (asked.variant ?? "") === (base.variant ?? "");
2682
+ if (sameAsTier) {
2683
+ delete overrides[name];
2684
+ } else {
2685
+ overrides[name] = {
2686
+ model: asked.model,
2687
+ ...asked.variant ? { variant: asked.variant } : base.variant ? { variant: "" } : {}
2688
+ };
2689
+ }
2690
+ }
2691
+ }
2692
+ if (Object.keys(overrides).length > 0) runtimeMap.overrides = overrides;
2693
+ else delete runtimeMap.overrides;
2694
+ }
2695
+ map[det.id] = runtimeMap;
2696
+ }
2697
+ writeText(file, JSON.stringify(map, null, 2) + "\n");
2698
+ p5.log.success(`Guardado en ${file}`);
2699
+ return 0;
2700
+ }
2701
+ function cancelled() {
2702
+ p5.cancel("Cancelado \u2014 no se ha guardado nada.");
2703
+ return 1;
2704
+ }
2705
+
2706
+ // src/cli.ts
2707
+ var VERSION = "0.6.0";
2708
+ var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall"];
2709
+ function parseFlags(args) {
2710
+ const flags = {
2711
+ agents: [],
2712
+ dryRun: false,
2713
+ yes: false,
2714
+ list: false,
2715
+ check: false,
2716
+ removeEngram: false,
2717
+ positional: []
2718
+ };
2719
+ for (let i = 0; i < args.length; i++) {
2720
+ const arg = args[i];
2721
+ if (arg === "--agents" || arg === "-a") flags.agents = (args[++i] ?? "").split(",").filter(Boolean);
2722
+ else if (arg.startsWith("--agents=")) flags.agents = arg.slice(9).split(",").filter(Boolean);
2723
+ else if (arg === "--target-dir") flags.targetDir = args[++i];
2724
+ else if (arg.startsWith("--target-dir=")) flags.targetDir = arg.slice(13);
2725
+ else if (arg === "--dry-run") flags.dryRun = true;
2726
+ else if (arg === "--yes" || arg === "-y") flags.yes = true;
2727
+ else if (arg === "--list") flags.list = true;
2728
+ else if (arg === "--check") flags.check = true;
2729
+ else if (arg === "--remove-engram") flags.removeEngram = true;
2730
+ else flags.positional.push(arg);
2731
+ }
2732
+ return flags;
2733
+ }
2734
+ async function resolveRuntimes(flags) {
2735
+ if (flags.agents.length > 0) return flags.agents;
2736
+ const detected = Object.values(ADAPTERS).filter((a) => a.detect().installed);
2737
+ if (detected.length === 0) return [];
2738
+ if (flags.yes || !process.stdout.isTTY || flags.targetDir !== void 0) return detected.map((a) => a.id);
2739
+ const choice = await p6.multiselect({
2740
+ message: "\xBFEn qu\xE9 runtimes? (detectados en esta m\xE1quina)",
2741
+ options: detected.map((a) => ({ value: a.id, label: a.name })),
2742
+ initialValues: detected.map((a) => a.id)
2743
+ });
2744
+ if (p6.isCancel(choice)) return null;
2745
+ return choice;
2746
+ }
2747
+ function printHelp() {
2748
+ console.log(`jorgex-stack v${VERSION}
2749
+
2750
+ Uso: pnpm dlx jorgex-stack [comando] [opciones]
2751
+
2752
+ Comandos:
2753
+ install Instala el stack en los runtimes elegidos (default, interactivo)
2754
+ sync Re-aplica la config (idempotente; alias de install)
2755
+ models Picker de modelos por tier (OpenCode: lista en vivo de 'opencode models')
2756
+ update --check: compara stack/Engram/skills con sus upstreams
2757
+ doctor Estado: Engram, drift de config, hooks de Codex, key de context7
2758
+ restore --list para ver backups \xB7 'restore <id>' para restaurar
2759
+ uninstall Retira SOLO lo gestionado por el stack (con backup).
2760
+ Engram se CONSERVA por defecto (memorias, binario y registro);
2761
+ desregistrarlo exige --remove-engram o el s\xED expl\xEDcito
2762
+
2763
+ Opciones:
2764
+ --agents, -a opencode,claude-code,codex Runtimes destino (default: detectados)
2765
+ --target-dir <dir> Dir alternativo (pruebas de paridad; requiere 1 runtime)
2766
+ --dry-run Muestra el plan sin escribir nada
2767
+ --yes, -y No interactivo
2768
+ --remove-engram (uninstall) desregistra Engram de los runtimes;
2769
+ memorias y binario quedan intactos igualmente
2770
+
2771
+ Ver PRD.md para el dise\xF1o completo.`);
2772
+ }
2773
+ async function main() {
2774
+ const [first, ...rest] = process.argv.slice(2);
2775
+ if (first === "--help" || first === "-h") return printHelp();
2776
+ if (first === "--version" || first === "-v") return console.log(VERSION);
2777
+ const isCommand = COMMANDS.includes(first ?? "install");
2778
+ if (first !== void 0 && !isCommand && !first.startsWith("-")) {
2779
+ console.error(`Comando desconocido: ${first}`);
2780
+ printHelp();
2781
+ process.exitCode = 1;
2782
+ return;
2783
+ }
2784
+ const command = isCommand ? first ?? "install" : "install";
2785
+ const flags = parseFlags(isCommand ? rest : process.argv.slice(2));
2786
+ if (flags.targetDir !== void 0 && flags.agents.length !== 1) {
2787
+ console.error("--target-dir requiere exactamente un runtime en --agents.");
2788
+ process.exitCode = 1;
2789
+ return;
2790
+ }
2791
+ switch (command) {
2792
+ case "install":
2793
+ case "sync": {
2794
+ const runtimes = await resolveRuntimes(flags);
2795
+ if (runtimes === null) return;
2796
+ if (runtimes.length === 0) {
2797
+ console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex).");
2798
+ process.exitCode = 1;
2799
+ return;
2800
+ }
2801
+ process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: flags.yes });
2802
+ return;
2803
+ }
2804
+ case "uninstall": {
2805
+ const runtimes = await resolveRuntimes(flags);
2806
+ if (runtimes === null) return;
2807
+ if (runtimes.length === 0) {
2808
+ console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex).");
2809
+ process.exitCode = 1;
2810
+ return;
2811
+ }
2812
+ process.exitCode = await runUninstall({
2813
+ runtimes,
2814
+ targetDir: flags.targetDir,
2815
+ dryRun: flags.dryRun,
2816
+ yes: flags.yes,
2817
+ removeEngram: flags.removeEngram
2818
+ });
2819
+ return;
2820
+ }
2821
+ case "doctor": {
2822
+ process.exitCode = await runDoctor();
2823
+ return;
2824
+ }
2825
+ case "update": {
2826
+ if (flags.check) {
2827
+ process.exitCode = await runUpdateCheck(VERSION);
2828
+ return;
2829
+ }
2830
+ if (flags.dryRun) {
2831
+ process.exitCode = await runUpdateCheck(VERSION);
2832
+ return;
2833
+ }
2834
+ const runtimes = await resolveRuntimes(flags);
2835
+ if (runtimes === null) return;
2836
+ if (runtimes.length > 0) {
2837
+ const code = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: true });
2838
+ if (code !== 0) {
2839
+ process.exitCode = code;
2840
+ return;
2841
+ }
2842
+ }
2843
+ const result = await runInteractiveUpdate(VERSION, flags.yes, flags.dryRun);
2844
+ process.exitCode = result.exitCode;
2845
+ if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && !flags.yes && process.stdout.isTTY) {
2846
+ const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
2847
+ if (!p6.isCancel(apply) && apply) {
2848
+ process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: false, yes: false });
2849
+ } else {
2850
+ console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
2851
+ }
2852
+ } else if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && (flags.yes || !process.stdout.isTTY)) {
2853
+ console.log("Skills/stack actualizados. Ejecuta jorgex-stack sync para aplicarlos a los runtimes.");
2854
+ }
2855
+ return;
2856
+ }
2857
+ case "models": {
2858
+ const runtimes = await resolveRuntimes(flags);
2859
+ if (runtimes === null) return;
2860
+ if (runtimes.length === 0) {
2861
+ console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex).");
2862
+ process.exitCode = 1;
2863
+ return;
2864
+ }
2865
+ const code = await runModelsPicker({ yes: flags.yes, runtimes });
2866
+ process.exitCode = code;
2867
+ if (code === 0 && !flags.yes && process.stdout.isTTY) {
2868
+ const apply = await p6.confirm({ message: "\xBFAplicar ahora los modelos a los agentes instalados? (sync)" });
2869
+ if (!p6.isCancel(apply) && apply) {
2870
+ process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: false });
2871
+ } else {
2872
+ console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
2873
+ }
2874
+ }
2875
+ return;
2876
+ }
2877
+ case "restore": {
2878
+ if (flags.list || flags.positional.length === 0) {
2879
+ const backups = listBackups();
2880
+ if (backups.length === 0) return console.log("No hay backups.");
2881
+ for (const b of backups) console.log(`${b.id} (${b.files.length} archivos, ${b.createdAt})`);
2882
+ if (!flags.list) console.log("\nUsa: jorgex-stack restore <id>");
2883
+ return;
2884
+ }
2885
+ const restored = restoreBackup(flags.positional[0]);
2886
+ console.log(`Restaurados ${restored} archivos.`);
2887
+ return;
2888
+ }
2889
+ }
2890
+ }
2891
+ main().catch((err) => {
2892
+ console.error(err instanceof Error ? err.message : String(err));
2893
+ process.exitCode = 1;
2894
+ });