jorgex-stack 1.9.57 → 1.9.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/dist/chunk-GOLJ5PA3.js +711 -0
- package/dist/chunk-KB5IQEKL.js +1400 -0
- package/dist/chunk-M6QX2F3A.js +782 -0
- package/dist/{chunk-IO2FFZOH.js → chunk-N5IC2JBJ.js} +0 -29
- package/dist/chunk-NNPFC6QV.js +563 -0
- package/dist/chunk-NQZVJFV2.js +33 -0
- package/dist/claude-code-WY4JJAXP.js +11 -0
- package/dist/cli.js +729 -2658
- package/dist/codex-WSWHXYJ3.js +11 -0
- package/dist/opencode-NUDGK6J7.js +31 -0
- package/dist/quality-verifier.js +4 -2
- package/package.json +1 -1
- package/stack/plugins/opencode/package.json +1 -1
- package/upstreams.json +22 -15
- package/stack/plugins/opencode/engram.ts +0 -444
package/dist/cli.js
CHANGED
|
@@ -1,443 +1,115 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
HOME,
|
|
4
3
|
QUALITY_PROFILES,
|
|
5
4
|
canonicalJson,
|
|
6
5
|
createQualityReceipt,
|
|
7
|
-
dataDir,
|
|
8
6
|
evaluateQualityPolicy,
|
|
9
|
-
samePath,
|
|
10
7
|
serializeQualityReceipt,
|
|
11
8
|
sha256,
|
|
12
|
-
stackRoot,
|
|
13
9
|
validateQualityReceipt
|
|
14
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-N5IC2JBJ.js";
|
|
11
|
+
import {
|
|
12
|
+
inspectOpencodePluginFile,
|
|
13
|
+
opencodeAdapter,
|
|
14
|
+
shouldRetireLegacyEngram
|
|
15
|
+
} from "./chunk-M6QX2F3A.js";
|
|
16
|
+
import {
|
|
17
|
+
claudeCodeAdapter
|
|
18
|
+
} from "./chunk-NNPFC6QV.js";
|
|
19
|
+
import {
|
|
20
|
+
codexAdapter
|
|
21
|
+
} from "./chunk-GOLJ5PA3.js";
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_MODEL_MAP,
|
|
24
|
+
DEVTOOLS_MCP_SERVER,
|
|
25
|
+
SYSTEM_PROMPT_SECTIONS,
|
|
26
|
+
assertSystemPromptFile,
|
|
27
|
+
assertSystemPromptMarkers,
|
|
28
|
+
copyFile,
|
|
29
|
+
createBackup,
|
|
30
|
+
detectClaudeCode,
|
|
31
|
+
detectCodex,
|
|
32
|
+
detectEngram,
|
|
33
|
+
detectOpenCode,
|
|
34
|
+
engramVersion,
|
|
35
|
+
ensureDir,
|
|
36
|
+
ensureModelMapFile,
|
|
37
|
+
hasHealthyManagedMarkdownMarkers,
|
|
38
|
+
isContainedIn,
|
|
39
|
+
listBackups,
|
|
40
|
+
listFilesRecursive,
|
|
41
|
+
loadCanonicalAgents,
|
|
42
|
+
loadCanonicalHooks,
|
|
43
|
+
loadCanonicalMcp,
|
|
44
|
+
loadModelMap,
|
|
45
|
+
localQualityStatus,
|
|
46
|
+
lookPath,
|
|
47
|
+
modelMapFile,
|
|
48
|
+
planDetectedBinCommand,
|
|
49
|
+
pruneEmptyDirs,
|
|
50
|
+
readSystemPromptFile,
|
|
51
|
+
readTextIfExists,
|
|
52
|
+
removeMarkdownSection,
|
|
53
|
+
removeSystemPromptSections,
|
|
54
|
+
resolveAgentModel,
|
|
55
|
+
restoreBackup,
|
|
56
|
+
runDetectedBin,
|
|
57
|
+
runOfficialSetupIfNeeded,
|
|
58
|
+
sameFileContent,
|
|
59
|
+
shouldRunOfficialSetup,
|
|
60
|
+
stripLeadingHtmlComments,
|
|
61
|
+
upsertMarkdownSection,
|
|
62
|
+
validateOfficialSetupDestination,
|
|
63
|
+
writeText
|
|
64
|
+
} from "./chunk-KB5IQEKL.js";
|
|
65
|
+
import {
|
|
66
|
+
HOME,
|
|
67
|
+
dataDir,
|
|
68
|
+
samePath,
|
|
69
|
+
stackRoot
|
|
70
|
+
} from "./chunk-NQZVJFV2.js";
|
|
15
71
|
|
|
16
72
|
// src/cli.ts
|
|
17
73
|
import * as p6 from "@clack/prompts";
|
|
18
|
-
import
|
|
19
|
-
import { pathToFileURL
|
|
74
|
+
import fs20 from "fs";
|
|
75
|
+
import { pathToFileURL } from "url";
|
|
20
76
|
|
|
21
77
|
// src/install.ts
|
|
22
|
-
import
|
|
23
|
-
import
|
|
78
|
+
import fs10 from "fs";
|
|
79
|
+
import path12 from "path";
|
|
24
80
|
import * as p from "@clack/prompts";
|
|
25
81
|
|
|
26
82
|
// src/lib/writing-style.ts
|
|
27
|
-
import fs3 from "fs";
|
|
28
|
-
import path3 from "path";
|
|
29
|
-
|
|
30
|
-
// src/lib/backup.ts
|
|
31
|
-
import fs2 from "fs";
|
|
32
|
-
import path2 from "path";
|
|
33
|
-
import crypto from "crypto";
|
|
34
|
-
|
|
35
|
-
// src/lib/fsx.ts
|
|
36
83
|
import fs from "fs";
|
|
37
84
|
import path from "path";
|
|
38
|
-
function readTextIfExists(file) {
|
|
39
|
-
try {
|
|
40
|
-
return fs.readFileSync(file, "utf8");
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function ensureDir(dir) {
|
|
46
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
-
}
|
|
48
|
-
var tmpCounter = 0;
|
|
49
|
-
function tmpPath(target) {
|
|
50
|
-
return path.join(path.dirname(target), `.jorgex-${process.pid}-${tmpCounter++}.tmp`);
|
|
51
|
-
}
|
|
52
|
-
function renameInto(tmp, target) {
|
|
53
|
-
try {
|
|
54
|
-
fs.renameSync(tmp, target);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
fs.rmSync(tmp, { force: true });
|
|
57
|
-
throw err;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function writeText(file, content, mode) {
|
|
61
|
-
ensureDir(path.dirname(file));
|
|
62
|
-
const tmp = tmpPath(file);
|
|
63
|
-
fs.writeFileSync(tmp, content, { encoding: "utf8", mode });
|
|
64
|
-
renameInto(tmp, file);
|
|
65
|
-
}
|
|
66
|
-
function copyFile(source, target) {
|
|
67
|
-
ensureDir(path.dirname(target));
|
|
68
|
-
const tmp = tmpPath(target);
|
|
69
|
-
fs.copyFileSync(source, tmp);
|
|
70
|
-
renameInto(tmp, target);
|
|
71
|
-
}
|
|
72
|
-
function isContainedIn(child, root) {
|
|
73
|
-
const rel = path.relative(path.resolve(root), path.resolve(child));
|
|
74
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
|
|
75
|
-
}
|
|
76
|
-
function pruneEmptyDirs(file, root) {
|
|
77
|
-
let dir = path.dirname(file);
|
|
78
|
-
while (dir.startsWith(root) && dir !== root) {
|
|
79
|
-
try {
|
|
80
|
-
if (fs.readdirSync(dir).length > 0) return;
|
|
81
|
-
fs.rmdirSync(dir);
|
|
82
|
-
} catch {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
dir = path.dirname(dir);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function sameFileContent(a, b) {
|
|
89
|
-
try {
|
|
90
|
-
const bufA = fs.readFileSync(a);
|
|
91
|
-
const bufB = fs.readFileSync(b);
|
|
92
|
-
return bufA.equals(bufB);
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
function listFilesRecursive(dir) {
|
|
98
|
-
const out = [];
|
|
99
|
-
if (!fs.existsSync(dir)) return out;
|
|
100
|
-
const walk = (d) => {
|
|
101
|
-
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
102
|
-
const p7 = path.join(d, entry.name);
|
|
103
|
-
if (entry.isDirectory()) walk(p7);
|
|
104
|
-
else out.push(p7);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
walk(dir);
|
|
108
|
-
return out;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// src/lib/backup.ts
|
|
112
|
-
var KEEP_BACKUPS = 10;
|
|
113
|
-
function backupsRoot() {
|
|
114
|
-
return path2.join(dataDir(), "backups");
|
|
115
|
-
}
|
|
116
|
-
function compositeChecksum(files) {
|
|
117
|
-
const hash = crypto.createHash("sha256");
|
|
118
|
-
for (const file of [...files].sort()) {
|
|
119
|
-
const content = crypto.createHash("sha256").update(fs2.readFileSync(file)).digest("hex");
|
|
120
|
-
hash.update(`${file}:${content}
|
|
121
|
-
`);
|
|
122
|
-
}
|
|
123
|
-
return hash.digest("hex");
|
|
124
|
-
}
|
|
125
|
-
function createBackup(files, label, root = backupsRoot()) {
|
|
126
|
-
const existing = [...new Set(files)].filter((f) => fs2.existsSync(f));
|
|
127
|
-
if (existing.length === 0) return null;
|
|
128
|
-
const checksum = compositeChecksum(existing);
|
|
129
|
-
const latest = listBackups(root)[0];
|
|
130
|
-
if (latest?.checksum === checksum) return latest;
|
|
131
|
-
const base = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${label}`;
|
|
132
|
-
let id = base;
|
|
133
|
-
let dir = path2.join(root, id);
|
|
134
|
-
for (let n = 1; fs2.existsSync(dir); n++) {
|
|
135
|
-
id = `${base}-${n}`;
|
|
136
|
-
dir = path2.join(root, id);
|
|
137
|
-
}
|
|
138
|
-
ensureDir(path2.join(dir, "files"));
|
|
139
|
-
const entries = existing.map((original, i) => {
|
|
140
|
-
const stored = path2.join(dir, "files", `${String(i).padStart(4, "0")}-${path2.basename(original)}`);
|
|
141
|
-
fs2.copyFileSync(original, stored);
|
|
142
|
-
return { original, stored };
|
|
143
|
-
});
|
|
144
|
-
const info = { id, label, createdAt: (/* @__PURE__ */ new Date()).toISOString(), files: entries, checksum };
|
|
145
|
-
writeText(path2.join(dir, "manifest.json"), JSON.stringify(info, null, 2) + "\n");
|
|
146
|
-
pruneBackups(root);
|
|
147
|
-
return info;
|
|
148
|
-
}
|
|
149
|
-
function listBackups(root = backupsRoot()) {
|
|
150
|
-
if (!fs2.existsSync(root)) return [];
|
|
151
|
-
const infos = [];
|
|
152
|
-
for (const entry of fs2.readdirSync(root, { withFileTypes: true })) {
|
|
153
|
-
if (!entry.isDirectory()) continue;
|
|
154
|
-
const manifest = readTextIfExists(path2.join(root, entry.name, "manifest.json"));
|
|
155
|
-
if (manifest === null) continue;
|
|
156
|
-
try {
|
|
157
|
-
infos.push(JSON.parse(manifest));
|
|
158
|
-
} catch {
|
|
159
|
-
infos.push({ id: entry.name, label: "(manifest corrupto)", createdAt: "", files: [] });
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return infos.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
163
|
-
}
|
|
164
|
-
function restoreBackup(id, root = backupsRoot(), boundary = HOME) {
|
|
165
|
-
const info = listBackups(root).find((b) => b.id === id);
|
|
166
|
-
if (!info) throw new Error(`Backup no encontrado: ${id}`);
|
|
167
|
-
let restored = 0;
|
|
168
|
-
for (const { original, stored } of info.files) {
|
|
169
|
-
if (!fs2.existsSync(stored)) continue;
|
|
170
|
-
if (!isContainedIn(original, boundary)) continue;
|
|
171
|
-
ensureDir(path2.dirname(original));
|
|
172
|
-
fs2.copyFileSync(stored, original);
|
|
173
|
-
restored++;
|
|
174
|
-
}
|
|
175
|
-
return restored;
|
|
176
|
-
}
|
|
177
|
-
function pruneBackups(root) {
|
|
178
|
-
const all = listBackups(root);
|
|
179
|
-
for (const old of all.slice(KEEP_BACKUPS)) {
|
|
180
|
-
fs2.rmSync(path2.join(root, old.id), { recursive: true, force: true });
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// src/lib/filemerge.ts
|
|
185
|
-
function markers(name) {
|
|
186
|
-
return { open: `<!-- jorgex:${name} -->`, close: `<!-- /jorgex:${name} -->` };
|
|
187
|
-
}
|
|
188
|
-
function hasHealthyManagedMarkdownMarkers(existing, name) {
|
|
189
|
-
const { open, close } = markers(name);
|
|
190
|
-
const count = (marker) => existing.split(marker).length - 1;
|
|
191
|
-
const onOwnLine = (marker) => existing.split("\n").some((line) => line.trim() === marker);
|
|
192
|
-
return count(open) === 1 && count(close) === 1 && existing.indexOf(close) > existing.indexOf(open) && onOwnLine(open) && onOwnLine(close);
|
|
193
|
-
}
|
|
194
|
-
function repairOrphanMarkers(existing, name) {
|
|
195
|
-
const { open, close } = markers(name);
|
|
196
|
-
const count = (marker) => existing.split(marker).length - 1;
|
|
197
|
-
const opens = count(open);
|
|
198
|
-
const closes = count(close);
|
|
199
|
-
if (opens === 0 && closes === 0) return existing;
|
|
200
|
-
if (hasHealthyManagedMarkdownMarkers(existing, name)) return existing;
|
|
201
|
-
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");
|
|
202
|
-
}
|
|
203
|
-
function upsertMarkdownSection(existing, name, content) {
|
|
204
|
-
const { open, close } = markers(name);
|
|
205
|
-
const block = `${open}
|
|
206
|
-
${content.trim()}
|
|
207
|
-
${close}`;
|
|
208
|
-
if (existing === null || existing.trim() === "") return block + "\n";
|
|
209
|
-
existing = repairOrphanMarkers(existing, name);
|
|
210
|
-
const start = existing.indexOf(open);
|
|
211
|
-
const end = existing.indexOf(close);
|
|
212
|
-
if (start !== -1 && end !== -1 && end > start) {
|
|
213
|
-
return existing.slice(0, start) + block + existing.slice(end + close.length);
|
|
214
|
-
}
|
|
215
|
-
const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
216
|
-
return existing + sep + block + "\n";
|
|
217
|
-
}
|
|
218
|
-
function removeMarkdownSection(existing, name) {
|
|
219
|
-
existing = repairOrphanMarkers(existing, name);
|
|
220
|
-
const { open, close } = markers(name);
|
|
221
|
-
const start = existing.indexOf(open);
|
|
222
|
-
const end = existing.indexOf(close);
|
|
223
|
-
if (start === -1 || end === -1 || end < start) return existing;
|
|
224
|
-
const before = existing.slice(0, start).replace(/\n+$/, "\n");
|
|
225
|
-
const after = existing.slice(end + close.length).replace(/^\n+/, "\n");
|
|
226
|
-
const joined = before + after;
|
|
227
|
-
return joined.trim() === "" ? "" : joined.replace(/^\n+/, "");
|
|
228
|
-
}
|
|
229
|
-
function stripLeadingHtmlComments(md) {
|
|
230
|
-
let out = md.trimStart();
|
|
231
|
-
while (out.startsWith("<!--")) {
|
|
232
|
-
const end = out.indexOf("-->");
|
|
233
|
-
if (end === -1) break;
|
|
234
|
-
out = out.slice(end + 3).trimStart();
|
|
235
|
-
}
|
|
236
|
-
return out;
|
|
237
|
-
}
|
|
238
|
-
function upsertJson(existing, mutate) {
|
|
239
|
-
let root = {};
|
|
240
|
-
if (existing !== null && existing.trim() !== "") {
|
|
241
|
-
root = JSON.parse(existing);
|
|
242
|
-
}
|
|
243
|
-
mutate(root);
|
|
244
|
-
return JSON.stringify(root, null, 2) + "\n";
|
|
245
|
-
}
|
|
246
|
-
function tomlRootEnd(lines) {
|
|
247
|
-
const mask = multilineStringMask(lines);
|
|
248
|
-
const index = lines.findIndex((line, lineIndex) => !mask[lineIndex] && headerName(line) !== null);
|
|
249
|
-
return index === -1 ? lines.length : index;
|
|
250
|
-
}
|
|
251
|
-
function rootKeyLineIndex(lines, key) {
|
|
252
|
-
const end = tomlRootEnd(lines);
|
|
253
|
-
const mask = multilineStringMask(lines);
|
|
254
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
255
|
-
const pattern = new RegExp(`^\\s*(?:${escaped}|"${escaped}"|'${escaped}')\\s*=`);
|
|
256
|
-
return lines.slice(0, end).findIndex((line, index) => !mask[index] && pattern.test(line));
|
|
257
|
-
}
|
|
258
|
-
function hasTomlRootKey(existing, key) {
|
|
259
|
-
if (existing === null || existing === "") return false;
|
|
260
|
-
return rootKeyLineIndex(existing.replace(/\r\n/g, "\n").split("\n"), key) !== -1;
|
|
261
|
-
}
|
|
262
|
-
function upsertTomlRootKeyIfMissing(existing, key, value) {
|
|
263
|
-
if (hasTomlRootKey(existing, key)) return existing;
|
|
264
|
-
const eol = existing?.includes("\r\n") ? "\r\n" : "\n";
|
|
265
|
-
const normalized = (existing ?? "").replace(/\r\n/g, "\n");
|
|
266
|
-
const lines = normalized === "" ? [] : normalized.split("\n");
|
|
267
|
-
const index = tomlRootEnd(lines);
|
|
268
|
-
lines.splice(index, 0, `${key} = ${value}`);
|
|
269
|
-
return lines.join(eol);
|
|
270
|
-
}
|
|
271
|
-
function removeTomlRootKeyIfExact(existing, key, value) {
|
|
272
|
-
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
273
|
-
const normalized = existing.replace(/\r\n/g, "\n");
|
|
274
|
-
const lines = normalized.split("\n");
|
|
275
|
-
const index = rootKeyLineIndex(lines, key);
|
|
276
|
-
if (index === -1) return existing;
|
|
277
|
-
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
278
|
-
const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
279
|
-
const exact = new RegExp(`^\\s*(?:${escapedKey}|"${escapedKey}"|'${escapedKey}')\\s*=\\s*${escapedValue}\\s*(?:#.*)?$`);
|
|
280
|
-
if (!exact.test(lines[index])) return existing;
|
|
281
|
-
lines.splice(index, 1);
|
|
282
|
-
return lines.join(eol);
|
|
283
|
-
}
|
|
284
|
-
function headerName(line) {
|
|
285
|
-
const match = /^\s*\[\s*([^\]]+?)\s*\]\s*(#.*)?$/.exec(line);
|
|
286
|
-
if (!match) return null;
|
|
287
|
-
return match[1].split(".").map((part) => part.trim().replace(/^["']|["']$/g, "")).join(".");
|
|
288
|
-
}
|
|
289
|
-
function quoteRunLength(line, start, quote) {
|
|
290
|
-
let end = start + 1;
|
|
291
|
-
while (end < line.length && line[end] === quote) end++;
|
|
292
|
-
return end - start;
|
|
293
|
-
}
|
|
294
|
-
function skipShortString(line, start, quote) {
|
|
295
|
-
let index = start + 1;
|
|
296
|
-
while (index < line.length) {
|
|
297
|
-
if (line[index] === quote) return index + 1;
|
|
298
|
-
if (quote === '"' && line[index] === "\\") {
|
|
299
|
-
index += 2;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
index++;
|
|
303
|
-
}
|
|
304
|
-
return line.length;
|
|
305
|
-
}
|
|
306
|
-
function scanTomlLine(line, initial) {
|
|
307
|
-
let inside = initial;
|
|
308
|
-
let index = 0;
|
|
309
|
-
while (index < line.length) {
|
|
310
|
-
if (inside !== null) {
|
|
311
|
-
const quote = inside === '"""' ? '"' : "'";
|
|
312
|
-
if (line[index] === quote) {
|
|
313
|
-
const run2 = quoteRunLength(line, index, quote);
|
|
314
|
-
if (run2 >= 3) {
|
|
315
|
-
inside = null;
|
|
316
|
-
}
|
|
317
|
-
index += run2;
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (inside === '"""' && line[index] === "\\") {
|
|
321
|
-
index += 2;
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
index++;
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
const char = line[index];
|
|
328
|
-
if (char === "#") break;
|
|
329
|
-
if (char !== '"' && char !== "'") {
|
|
330
|
-
index++;
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
const run = quoteRunLength(line, index, char);
|
|
334
|
-
if (run >= 3) {
|
|
335
|
-
inside = char === '"' ? '"""' : "'''";
|
|
336
|
-
index += 3;
|
|
337
|
-
} else {
|
|
338
|
-
index = skipShortString(line, index, char);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return inside;
|
|
342
|
-
}
|
|
343
|
-
function multilineStringMask(lines) {
|
|
344
|
-
const mask = [];
|
|
345
|
-
let inside = null;
|
|
346
|
-
for (const line of lines) {
|
|
347
|
-
mask.push(inside !== null);
|
|
348
|
-
inside = scanTomlLine(line, inside);
|
|
349
|
-
}
|
|
350
|
-
return mask;
|
|
351
|
-
}
|
|
352
|
-
function findTomlSection(lines, section) {
|
|
353
|
-
const mask = multilineStringMask(lines);
|
|
354
|
-
const start = lines.findIndex((line, i) => !mask[i] && headerName(line) === section);
|
|
355
|
-
if (start === -1) return null;
|
|
356
|
-
let end = start + 1;
|
|
357
|
-
while (end < lines.length && (mask[end] || headerName(lines[end]) === null)) end++;
|
|
358
|
-
return { start, end };
|
|
359
|
-
}
|
|
360
|
-
function upsertTomlSection(existing, section, body) {
|
|
361
|
-
const header = `[${section}]`;
|
|
362
|
-
const target = headerName(header) ?? section;
|
|
363
|
-
const block = `${header}
|
|
364
|
-
${body.trim()}
|
|
365
|
-
`;
|
|
366
|
-
if (existing === null || existing.trim() === "") return block;
|
|
367
|
-
const rawLines = existing.split("\n");
|
|
368
|
-
const lines = rawLines.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
369
|
-
const lineStarts = [];
|
|
370
|
-
let offset = 0;
|
|
371
|
-
for (let index = 0; index < rawLines.length; index++) {
|
|
372
|
-
lineStarts.push(offset);
|
|
373
|
-
offset += rawLines[index].length;
|
|
374
|
-
if (index < rawLines.length - 1) offset++;
|
|
375
|
-
}
|
|
376
|
-
const found = findTomlSection(lines, target);
|
|
377
|
-
if (found === null) {
|
|
378
|
-
const sep = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
379
|
-
return existing + sep + block;
|
|
380
|
-
}
|
|
381
|
-
let sectionEnd = found.end;
|
|
382
|
-
while (sectionEnd > found.start + 1 && lines[sectionEnd - 1].trim() === "") sectionEnd--;
|
|
383
|
-
const startOffset = lineStarts[found.start];
|
|
384
|
-
const endOffset = lineStarts[sectionEnd] ?? existing.length;
|
|
385
|
-
return existing.slice(0, startOffset) + block.trimEnd() + "\n" + existing.slice(endOffset);
|
|
386
|
-
}
|
|
387
|
-
function removeTomlSection(existing, section) {
|
|
388
|
-
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
389
|
-
const lines = existing.split(/\r?\n/);
|
|
390
|
-
const found = findTomlSection(lines, section);
|
|
391
|
-
if (found === null) return existing;
|
|
392
|
-
let realStart = found.start;
|
|
393
|
-
while (realStart > 0 && lines[realStart - 1].trim() === "") realStart--;
|
|
394
|
-
const result = [...lines.slice(0, realStart), ...lines.slice(found.end)].join(eol);
|
|
395
|
-
if (result.trim() === "") return "";
|
|
396
|
-
return result.endsWith(eol) ? result : result + eol;
|
|
397
|
-
}
|
|
398
|
-
function readTomlSection(existing, section) {
|
|
399
|
-
if (existing === null) return null;
|
|
400
|
-
const lines = existing.split(/\r?\n/);
|
|
401
|
-
const found = findTomlSection(lines, section);
|
|
402
|
-
if (found === null) return null;
|
|
403
|
-
return lines.slice(found.start + 1, found.end).join("\n");
|
|
404
|
-
}
|
|
405
|
-
function hasTomlChildSection(existing, section) {
|
|
406
|
-
if (existing === null) return false;
|
|
407
|
-
const lines = existing.split(/\r?\n/);
|
|
408
|
-
const mask = multilineStringMask(lines);
|
|
409
|
-
return lines.some((line, index) => !mask[index] && headerName(line)?.startsWith(`${section}.`) === true);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// src/lib/writing-style.ts
|
|
413
85
|
var DEFAULT_SECTION = "writing-style-default";
|
|
414
86
|
var DEFAULT_OPEN = `<!-- jorgex:${DEFAULT_SECTION} -->`;
|
|
415
87
|
var DEFAULT_CLOSE = `<!-- /jorgex:${DEFAULT_SECTION} -->`;
|
|
416
88
|
function assertContained(file, rootDir) {
|
|
417
|
-
const relative =
|
|
418
|
-
if (relative === ".." || relative.startsWith(`..${
|
|
89
|
+
const relative = path.relative(fs.realpathSync(rootDir), fs.realpathSync(file));
|
|
90
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
419
91
|
throw new Error(`La ruta de estilo ${file} sale del destino aislado.`);
|
|
420
92
|
}
|
|
421
93
|
}
|
|
422
94
|
function validateBackupRoot(rootDir) {
|
|
423
95
|
if (rootDir === void 0) return;
|
|
424
|
-
const backupRoot =
|
|
96
|
+
const backupRoot = path.join(rootDir, "backups");
|
|
425
97
|
try {
|
|
426
|
-
|
|
98
|
+
fs.lstatSync(backupRoot);
|
|
427
99
|
} catch (error) {
|
|
428
100
|
if (error.code === "ENOENT") return;
|
|
429
101
|
throw error;
|
|
430
102
|
}
|
|
431
103
|
assertContained(backupRoot, rootDir);
|
|
432
|
-
if (!
|
|
104
|
+
if (!fs.statSync(backupRoot).isDirectory()) throw new Error(`El destino de backups ${backupRoot} no es un directorio.`);
|
|
433
105
|
}
|
|
434
106
|
function resolveWritingStyleFile(options = {}) {
|
|
435
|
-
return
|
|
107
|
+
return path.join(options.targetDir ?? options.stateDir ?? dataDir(), "writing-style.md");
|
|
436
108
|
}
|
|
437
109
|
function readStyleText(sourcePath, options = {}) {
|
|
438
110
|
let stat;
|
|
439
111
|
try {
|
|
440
|
-
stat =
|
|
112
|
+
stat = fs.lstatSync(sourcePath);
|
|
441
113
|
} catch (error) {
|
|
442
114
|
if (error.code === "ENOENT") return null;
|
|
443
115
|
throw new Error(`No se puede leer el estilo ${sourcePath}. Revisa los permisos del archivo.`, { cause: error });
|
|
@@ -445,12 +117,12 @@ function readStyleText(sourcePath, options = {}) {
|
|
|
445
117
|
if (options.rootDir !== void 0) {
|
|
446
118
|
assertContained(sourcePath, options.rootDir);
|
|
447
119
|
}
|
|
448
|
-
if (!(stat.isSymbolicLink() ?
|
|
120
|
+
if (!(stat.isSymbolicLink() ? fs.statSync(sourcePath) : stat).isFile()) {
|
|
449
121
|
throw new Error(`La fuente de estilo ${sourcePath} debe ser un archivo.`);
|
|
450
122
|
}
|
|
451
123
|
let bytes;
|
|
452
124
|
try {
|
|
453
|
-
bytes =
|
|
125
|
+
bytes = fs.readFileSync(sourcePath);
|
|
454
126
|
} catch (error) {
|
|
455
127
|
throw new Error(`No se puede leer el estilo ${sourcePath}. Revisa los permisos del archivo.`, { cause: error });
|
|
456
128
|
}
|
|
@@ -468,1884 +140,46 @@ function readWritingStyle(sourcePath, options = {}) {
|
|
|
468
140
|
return { sourcePath, content: content || null };
|
|
469
141
|
}
|
|
470
142
|
function prepareWritingStyle(sourcePath, options = {}) {
|
|
471
|
-
const canonicalPath =
|
|
143
|
+
const canonicalPath = path.join(options.stackDir ?? stackRoot(), "system-prompt", "writing-style.md");
|
|
472
144
|
const canonical = readWritingStyle(canonicalPath).content;
|
|
473
145
|
if (canonical === null) throw new Error(`Falta el estilo can\xF3nico o est\xE1 vac\xEDo en ${canonicalPath}; repara la instalaci\xF3n de Stack.`);
|
|
474
146
|
const originalContent = readStyleText(sourcePath, options);
|
|
475
147
|
if (originalContent?.includes("jorgex:")) {
|
|
476
148
|
if (!hasHealthyManagedMarkdownMarkers(originalContent, DEFAULT_SECTION) || originalContent.replace(DEFAULT_OPEN, "").replace(DEFAULT_CLOSE, "").includes("jorgex:")) {
|
|
477
|
-
throw new Error(`Marcadores de estilo ambiguos o desconocidos en ${sourcePath}; revisa el bloque ${DEFAULT_SECTION}.`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
const installedContent = upsertMarkdownSection(originalContent, DEFAULT_SECTION, canonical);
|
|
481
|
-
if (originalContent !== null && originalContent !== installedContent) validateBackupRoot(options.rootDir);
|
|
482
|
-
const content = installedContent.replace(DEFAULT_OPEN, "").replace(DEFAULT_CLOSE, "").replace(/\r\n?/g, "\n").trim();
|
|
483
|
-
return {
|
|
484
|
-
sourcePath,
|
|
485
|
-
content,
|
|
486
|
-
canonicalPath,
|
|
487
|
-
originalContent,
|
|
488
|
-
installedContent,
|
|
489
|
-
rootDir: options.rootDir,
|
|
490
|
-
backupRoot: options.rootDir === void 0 ? void 0 : path3.join(options.rootDir, "backups")
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
function applyWritingStyle(plan, dryRun = false) {
|
|
494
|
-
if (dryRun) return;
|
|
495
|
-
const current = readStyleText(plan.sourcePath, { rootDir: plan.rootDir });
|
|
496
|
-
if (current === plan.installedContent) return;
|
|
497
|
-
if (current !== plan.originalContent) {
|
|
498
|
-
throw new Error(`El estilo ${plan.sourcePath} cambi\xF3 durante la preparaci\xF3n; vuelve a ejecutar el comando.`);
|
|
499
|
-
}
|
|
500
|
-
if (current !== null) {
|
|
501
|
-
validateBackupRoot(plan.rootDir);
|
|
502
|
-
createBackup([plan.sourcePath], "writing-style", plan.backupRoot);
|
|
503
|
-
}
|
|
504
|
-
const mode = current === null ? 384 : fs3.statSync(plan.sourcePath).mode & 511;
|
|
505
|
-
writeText(plan.sourcePath, plan.installedContent, mode);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// src/lib/system-prompt-sections.ts
|
|
509
|
-
import fs4 from "fs";
|
|
510
|
-
import path4 from "path";
|
|
511
|
-
var SYSTEM_PROMPT_SECTIONS = [
|
|
512
|
-
"system-prompt",
|
|
513
|
-
"engram-protocol",
|
|
514
|
-
"context7",
|
|
515
|
-
"playwright",
|
|
516
|
-
"chrome-devtools",
|
|
517
|
-
"browser",
|
|
518
|
-
"writing-style"
|
|
519
|
-
];
|
|
520
|
-
function assertSystemPromptMarkers(content, target) {
|
|
521
|
-
if (content === null) return;
|
|
522
|
-
const ranges = [];
|
|
523
|
-
for (const section of SYSTEM_PROMPT_SECTIONS) {
|
|
524
|
-
const markers2 = [...content.matchAll(new RegExp(`<!--\\s*\\/?jorgex:${section}(?=[\\s>]|-->|$)`, "g"))];
|
|
525
|
-
if (markers2.length === 0) continue;
|
|
526
|
-
if (markers2.length !== 2 || !hasHealthyManagedMarkdownMarkers(content, section)) {
|
|
527
|
-
throw new Error(`Marcadores jorgex:${section} ambiguos en ${target}. Repara el bloque antes de reintentar; su contenido se conserva.`);
|
|
528
|
-
}
|
|
529
|
-
ranges.push({
|
|
530
|
-
section,
|
|
531
|
-
start: content.indexOf(`<!-- jorgex:${section} -->`),
|
|
532
|
-
end: content.indexOf(`<!-- /jorgex:${section} -->`) + `<!-- /jorgex:${section} -->`.length
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
ranges.sort((a, b) => a.start - b.start);
|
|
536
|
-
for (let index = 1; index < ranges.length; index++) {
|
|
537
|
-
if (ranges[index].start < ranges[index - 1].end) {
|
|
538
|
-
throw new Error(`Marcadores jorgex:${ranges[index].section} anidados en ${target}. Separa los bloques antes de reintentar; su contenido se conserva.`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
function readSystemPromptFile(target, rootDir) {
|
|
543
|
-
if (rootDir !== void 0) {
|
|
544
|
-
const probe = fs4.existsSync(target) ? target : path4.dirname(target);
|
|
545
|
-
if (fs4.existsSync(probe) && fs4.existsSync(rootDir)) {
|
|
546
|
-
const resolved = fs4.realpathSync(probe);
|
|
547
|
-
const root = fs4.realpathSync(rootDir);
|
|
548
|
-
if (resolved !== root && !isContainedIn(resolved, root)) {
|
|
549
|
-
throw new Error(`El prompt ${target} sale del destino aislado.`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
let bytes;
|
|
554
|
-
try {
|
|
555
|
-
bytes = fs4.readFileSync(target);
|
|
556
|
-
} catch (error) {
|
|
557
|
-
if (error.code === "ENOENT") return null;
|
|
558
|
-
throw new Error(`No se puede leer el prompt ${target}. Revisa los permisos y el tipo de archivo.`, { cause: error });
|
|
559
|
-
}
|
|
560
|
-
try {
|
|
561
|
-
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
562
|
-
} catch (error) {
|
|
563
|
-
throw new Error(`El prompt ${target} debe usar UTF-8 v\xE1lido.`, { cause: error });
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
function assertSystemPromptFile(target, rootDir) {
|
|
567
|
-
assertSystemPromptMarkers(readSystemPromptFile(target, rootDir), target);
|
|
568
|
-
}
|
|
569
|
-
function removeSystemPromptSections(content) {
|
|
570
|
-
assertSystemPromptMarkers(content, "system prompt");
|
|
571
|
-
return SYSTEM_PROMPT_SECTIONS.reduce((current, section) => removeMarkdownSection(current, section), content);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// src/adapters/opencode.ts
|
|
575
|
-
import path9 from "path";
|
|
576
|
-
import fs6 from "fs";
|
|
577
|
-
import { isDeepStrictEqual } from "util";
|
|
578
|
-
import { pathToFileURL } from "url";
|
|
579
|
-
|
|
580
|
-
// src/lib/canonical.ts
|
|
581
|
-
import path5 from "path";
|
|
582
|
-
import fs5 from "fs";
|
|
583
|
-
function parseCanonicalAgent(source, file) {
|
|
584
|
-
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(source);
|
|
585
|
-
if (!match) throw new Error(`Agente sin frontmatter: ${file}`);
|
|
586
|
-
const fields = {};
|
|
587
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
588
|
-
const sep = line.indexOf(":");
|
|
589
|
-
if (sep === -1) continue;
|
|
590
|
-
fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
|
|
591
|
-
}
|
|
592
|
-
const required = ["name", "description", "mode", "tier"];
|
|
593
|
-
for (const key of required) {
|
|
594
|
-
if (!fields[key]) throw new Error(`Agente ${file}: falta '${key}' en el frontmatter`);
|
|
595
|
-
}
|
|
596
|
-
return {
|
|
597
|
-
name: fields.name,
|
|
598
|
-
description: fields.description,
|
|
599
|
-
mode: fields.mode === "primary" ? "primary" : "subagent",
|
|
600
|
-
tier: fields.tier,
|
|
601
|
-
readonly: fields.readonly === "true",
|
|
602
|
-
bash: fields.bash ?? "full",
|
|
603
|
-
spawn: fields.spawn !== "false",
|
|
604
|
-
body: source.slice(match[0].length)
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
function loadCanonicalAgents(agentsDir) {
|
|
608
|
-
return fs5.readdirSync(agentsDir).filter((f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md").map((f) => {
|
|
609
|
-
const file = path5.join(agentsDir, f);
|
|
610
|
-
const source = fs5.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
|
|
611
|
-
return parseCanonicalAgent(source, file);
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
var DEVTOOLS_MCP_SERVER = "chrome-devtools";
|
|
615
|
-
function isCanonicalMcpServerEnabled(name, server, enabledServers) {
|
|
616
|
-
return !server.optional || server.defaultEnabled === true || enabledServers?.has(name) === true;
|
|
617
|
-
}
|
|
618
|
-
function loadCanonicalMcp(stackDir) {
|
|
619
|
-
return JSON.parse(fs5.readFileSync(path5.join(stackDir, "mcp", "servers.json"), "utf8"));
|
|
620
|
-
}
|
|
621
|
-
function loadCanonicalHooks(stackDir) {
|
|
622
|
-
return JSON.parse(fs5.readFileSync(path5.join(stackDir, "hooks", "hooks.json"), "utf8"));
|
|
623
|
-
}
|
|
624
|
-
function loadCanonicalDefaults(stackDir) {
|
|
625
|
-
const file = path5.join(stackDir, "config", "defaults.json");
|
|
626
|
-
if (!fs5.existsSync(file)) return {};
|
|
627
|
-
const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
628
|
-
delete parsed["$comment"];
|
|
629
|
-
return parsed;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// src/lib/model-map.ts
|
|
633
|
-
import path6 from "path";
|
|
634
|
-
import { existsSync } from "fs";
|
|
635
|
-
function resolveAgentModel(models, agentName, tier) {
|
|
636
|
-
const base = models[tier];
|
|
637
|
-
const override = models.overrides?.[agentName];
|
|
638
|
-
if (!override) return base;
|
|
639
|
-
return {
|
|
640
|
-
model: override.model || base.model,
|
|
641
|
-
variant: "variant" in override ? override.variant || void 0 : base.variant
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
var DEFAULT_MODEL_MAP = {
|
|
645
|
-
"claude-code": {
|
|
646
|
-
strong: { model: "fable" },
|
|
647
|
-
standard: { model: "sonnet" },
|
|
648
|
-
cheap: { model: "haiku" }
|
|
649
|
-
},
|
|
650
|
-
// El primary (orchestrator) no usa estos tiers: tanto el profile de CLI como
|
|
651
|
-
// la skill de la app heredan el modelo elegido por el usuario. Estos defaults
|
|
652
|
-
// son solo para subagentes; variant → model_reasoning_effort.
|
|
653
|
-
codex: {
|
|
654
|
-
strong: { model: "gpt-6-astra", variant: "low" },
|
|
655
|
-
standard: { model: "gpt-5.6-luna", variant: "max" },
|
|
656
|
-
cheap: { model: "gpt-5.6-luna", variant: "medium" }
|
|
657
|
-
}
|
|
658
|
-
};
|
|
659
|
-
function modelMapFile() {
|
|
660
|
-
return path6.join(dataDir(), "model-map.json");
|
|
661
|
-
}
|
|
662
|
-
function loadModelMap() {
|
|
663
|
-
const file = modelMapFile();
|
|
664
|
-
const raw = readTextIfExists(file);
|
|
665
|
-
if (raw === null) return DEFAULT_MODEL_MAP;
|
|
666
|
-
let fromDisk;
|
|
667
|
-
try {
|
|
668
|
-
fromDisk = JSON.parse(raw);
|
|
669
|
-
} catch {
|
|
670
|
-
throw new Error(`El mapa de modelos ${file} contiene JSON inv\xE1lido. Corrige el JSON o restaura una copia antes de continuar.`);
|
|
671
|
-
}
|
|
672
|
-
const merged = { ...fromDisk };
|
|
673
|
-
for (const id of Object.keys(DEFAULT_MODEL_MAP)) {
|
|
674
|
-
merged[id] = { ...DEFAULT_MODEL_MAP[id], ...fromDisk[id] ?? {} };
|
|
675
|
-
if (fromDisk[id] && !Object.hasOwn(fromDisk[id], "overrides")) delete merged[id].overrides;
|
|
676
|
-
}
|
|
677
|
-
return merged;
|
|
678
|
-
}
|
|
679
|
-
function ensureModelMapFile() {
|
|
680
|
-
const file = modelMapFile();
|
|
681
|
-
if (!existsSync(file)) {
|
|
682
|
-
writeText(file, JSON.stringify(DEFAULT_MODEL_MAP, null, 2) + "\n");
|
|
683
|
-
}
|
|
684
|
-
return file;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// src/lib/detect.ts
|
|
688
|
-
import path7 from "path";
|
|
689
|
-
import { existsSync as existsSync2, statSync } from "fs";
|
|
690
|
-
import { execFileSync } from "child_process";
|
|
691
|
-
function lookPath(cmd, env = process.env) {
|
|
692
|
-
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ".ps1", ""] : [""];
|
|
693
|
-
for (const dir of (env.PATH ?? "").split(path7.delimiter)) {
|
|
694
|
-
if (!dir) continue;
|
|
695
|
-
for (const ext of exts) {
|
|
696
|
-
const candidate = path7.join(dir, cmd + ext);
|
|
697
|
-
if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
return null;
|
|
701
|
-
}
|
|
702
|
-
function planDetectedBinCommand(bin, args) {
|
|
703
|
-
if (process.platform !== "win32" || !/\.(cmd|bat)$/i.test(bin)) return { command: bin, args };
|
|
704
|
-
if ([bin, ...args].some((part) => /[&|<>()^%!"\r\n]/.test(part))) return null;
|
|
705
|
-
const quote = (part) => part === "" || /\s/.test(part) ? `"${part}"` : part;
|
|
706
|
-
return {
|
|
707
|
-
command: process.env.ComSpec ?? "cmd.exe",
|
|
708
|
-
args: ["/d", "/s", "/c", [quote(bin), ...args.map(quote)].join(" ")]
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
function runDetectedBin(bin, args, timeoutMs, envOverrides) {
|
|
712
|
-
try {
|
|
713
|
-
const command = planDetectedBinCommand(bin, args);
|
|
714
|
-
if (command === null) return null;
|
|
715
|
-
return execFileSync(command.command, command.args, {
|
|
716
|
-
encoding: "utf8",
|
|
717
|
-
timeout: timeoutMs,
|
|
718
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
719
|
-
...envOverrides === void 0 ? {} : { env: { ...process.env, ...envOverrides } }
|
|
720
|
-
});
|
|
721
|
-
} catch {
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
function detectOpenCode() {
|
|
726
|
-
const configDir = process.env.OPENCODE_CONFIG_DIR ?? path7.join(HOME, ".config", "opencode");
|
|
727
|
-
const binPath = lookPath("opencode");
|
|
728
|
-
return {
|
|
729
|
-
id: "opencode",
|
|
730
|
-
name: "OpenCode",
|
|
731
|
-
installed: binPath !== null || existsSync2(configDir),
|
|
732
|
-
binPath,
|
|
733
|
-
configDir
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
function detectClaudeCode() {
|
|
737
|
-
const configDir = path7.join(HOME, ".claude");
|
|
738
|
-
const binPath = lookPath("claude");
|
|
739
|
-
return {
|
|
740
|
-
id: "claude-code",
|
|
741
|
-
name: "Claude Code",
|
|
742
|
-
installed: binPath !== null || existsSync2(configDir),
|
|
743
|
-
binPath,
|
|
744
|
-
configDir
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
function detectCodex() {
|
|
748
|
-
const configDir = process.env.CODEX_HOME ?? path7.join(HOME, ".codex");
|
|
749
|
-
const binPath = lookPath("codex");
|
|
750
|
-
return {
|
|
751
|
-
id: "codex",
|
|
752
|
-
name: "Codex CLI",
|
|
753
|
-
installed: binPath !== null || existsSync2(configDir),
|
|
754
|
-
binPath,
|
|
755
|
-
configDir
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
function detectEngram() {
|
|
759
|
-
const fromEnv = process.env.ENGRAM_BIN;
|
|
760
|
-
if (fromEnv && existsSync2(fromEnv)) return fromEnv;
|
|
761
|
-
const onPath = lookPath("engram");
|
|
762
|
-
if (onPath) return onPath;
|
|
763
|
-
const candidates = [
|
|
764
|
-
path7.join(HOME, "go", "bin", "engram.exe"),
|
|
765
|
-
path7.join(HOME, "go", "bin", "engram"),
|
|
766
|
-
path7.join(HOME, ".local", "bin", "engram.exe"),
|
|
767
|
-
path7.join(HOME, ".local", "bin", "engram")
|
|
768
|
-
];
|
|
769
|
-
for (const c of candidates) if (existsSync2(c)) return c;
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// src/lib/hooks-format.ts
|
|
774
|
-
import path8 from "path";
|
|
775
|
-
function hookScriptNames(canonical) {
|
|
776
|
-
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) => path8.basename(s));
|
|
777
|
-
}
|
|
778
|
-
function plainHookCommands(canonical) {
|
|
779
|
-
return new Set(
|
|
780
|
-
Object.values(canonical.hooks).flat().flatMap((entry) => entry.hooks).map((h) => h.command).filter((c) => !c.includes("{{SCRIPTS_DIR}}"))
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
function removeNativeHooks(existing, canonical) {
|
|
784
|
-
if (existing === null || existing.trim() === "") return existing;
|
|
785
|
-
const scriptNames = hookScriptNames(canonical);
|
|
786
|
-
const plainCommands = plainHookCommands(canonical);
|
|
787
|
-
return upsertJson(existing, (root) => {
|
|
788
|
-
const hooks = root["hooks"];
|
|
789
|
-
if (!hooks) return;
|
|
790
|
-
for (const event of Object.keys(hooks)) {
|
|
791
|
-
const list = hooks[event];
|
|
792
|
-
if (!Array.isArray(list)) continue;
|
|
793
|
-
hooks[event] = list.filter((entry) => {
|
|
794
|
-
const e = entry;
|
|
795
|
-
if (!Array.isArray(e?.hooks)) return true;
|
|
796
|
-
const ours = e.hooks.some((hh) => {
|
|
797
|
-
const cmd = String(hh?.command ?? "");
|
|
798
|
-
return scriptNames.some((s) => cmd.includes(s)) || plainCommands.has(cmd);
|
|
799
|
-
});
|
|
800
|
-
return !ours;
|
|
801
|
-
});
|
|
802
|
-
if (hooks[event].length === 0) delete hooks[event];
|
|
803
|
-
}
|
|
804
|
-
if (Object.keys(hooks).length === 0) delete root["hooks"];
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
function upsertNativeHooks(existing, canonical, scriptsDir, mapMatcher = (m) => m) {
|
|
808
|
-
const scriptsDirForCommand = scriptsDir.replace(/\\/g, "/");
|
|
809
|
-
return upsertJson(existing, (root) => {
|
|
810
|
-
const hooks = root["hooks"] ??= {};
|
|
811
|
-
for (const [event, entries] of Object.entries(canonical.hooks)) {
|
|
812
|
-
const list = hooks[event] ??= [];
|
|
813
|
-
for (const entry of entries) {
|
|
814
|
-
const matcher = entry.matcher !== void 0 ? mapMatcher(entry.matcher) : void 0;
|
|
815
|
-
const rendered = {
|
|
816
|
-
...matcher !== void 0 ? { matcher } : {},
|
|
817
|
-
hooks: entry.hooks.map((h) => ({
|
|
818
|
-
type: h.type,
|
|
819
|
-
command: h.command.replace(/\{\{SCRIPTS_DIR\}\}/g, scriptsDirForCommand),
|
|
820
|
-
...h.timeout !== void 0 ? { timeout: h.timeout } : {}
|
|
821
|
-
}))
|
|
822
|
-
};
|
|
823
|
-
const scriptNames = entry.hooks.map((h) => /\{\{SCRIPTS_DIR\}\}[/\\]([\w./-]+)/.exec(h.command)?.[1]).filter((s) => s !== void 0).map((s) => path8.basename(s));
|
|
824
|
-
const plainCommands = entry.hooks.map((h) => h.command).filter((c) => !c.includes("{{SCRIPTS_DIR}}"));
|
|
825
|
-
const index = list.findIndex((existingEntry) => {
|
|
826
|
-
const e = existingEntry;
|
|
827
|
-
if (!Array.isArray(e?.hooks)) return false;
|
|
828
|
-
return e.hooks.some((hh) => {
|
|
829
|
-
const cmd = String(hh?.command ?? "");
|
|
830
|
-
return scriptNames.some((s) => cmd.includes(s)) || plainCommands.includes(cmd);
|
|
831
|
-
});
|
|
832
|
-
});
|
|
833
|
-
if (index >= 0) list[index] = rendered;
|
|
834
|
-
else list.push(rendered);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// src/lib/quality-capabilities.ts
|
|
841
|
-
var QUALITY_CAPABILITY_NAMESPACE = "jorgex.quality.capabilities";
|
|
842
|
-
var QUALITY_CAPABILITY_VERSION = 1;
|
|
843
|
-
var QUALITY_CAPABILITY_IDS = [
|
|
844
|
-
"policy-guidance",
|
|
845
|
-
"tool-approval",
|
|
846
|
-
"external-verification"
|
|
847
|
-
];
|
|
848
|
-
var UNAVAILABLE_REASON = "No reviewed local declaration is available";
|
|
849
|
-
var EXTERNAL_VERIFICATION_REASON = "External verification is available only through the external verifier";
|
|
850
|
-
function hasText(value) {
|
|
851
|
-
return typeof value === "string" && value.trim() !== "";
|
|
852
|
-
}
|
|
853
|
-
function isRuntime(value) {
|
|
854
|
-
return value === "claude-code" || value === "codex" || value === "opencode" || value === "pi";
|
|
855
|
-
}
|
|
856
|
-
function hasValidEvidence(value) {
|
|
857
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
858
|
-
const evidence = value;
|
|
859
|
-
return hasText(evidence.source) && hasText(evidence.version);
|
|
860
|
-
}
|
|
861
|
-
function unavailableCapability(id, reason = UNAVAILABLE_REASON) {
|
|
862
|
-
return { id, state: "unavailable", reason };
|
|
863
|
-
}
|
|
864
|
-
function declarationFor(id, declarations, runtime) {
|
|
865
|
-
if (runtime === "unknown") {
|
|
866
|
-
return unavailableCapability(id, "Runtime is unknown; local capability cannot be established");
|
|
867
|
-
}
|
|
868
|
-
if (id === "external-verification") {
|
|
869
|
-
return unavailableCapability(id, EXTERNAL_VERIFICATION_REASON);
|
|
870
|
-
}
|
|
871
|
-
const matches = declarations.filter((declaration2) => declaration2?.id === id);
|
|
872
|
-
if (matches.length === 0) return unavailableCapability(id);
|
|
873
|
-
if (matches.length !== 1) return unavailableCapability(id, "Duplicate capability declarations are ambiguous");
|
|
874
|
-
const declaration = matches[0];
|
|
875
|
-
const state = declaration.state;
|
|
876
|
-
const reason = hasText(declaration.reason) ? declaration.reason.trim() : void 0;
|
|
877
|
-
if (!reason) return unavailableCapability(id, "Capability declaration has no reason");
|
|
878
|
-
if (state !== "prompt-only" && state !== "manual" && state !== "unavailable") {
|
|
879
|
-
return unavailableCapability(id, "Capability state is not a supported local state");
|
|
880
|
-
}
|
|
881
|
-
if (state === "unavailable") return { id, state, reason };
|
|
882
|
-
if (!hasValidEvidence(declaration.evidence)) {
|
|
883
|
-
return unavailableCapability(id, "Capability declaration lacks reviewed source/version evidence");
|
|
884
|
-
}
|
|
885
|
-
const evidence = declaration.evidence;
|
|
886
|
-
return {
|
|
887
|
-
id,
|
|
888
|
-
state,
|
|
889
|
-
reason,
|
|
890
|
-
evidence: {
|
|
891
|
-
source: evidence.source.trim(),
|
|
892
|
-
version: evidence.version.trim()
|
|
893
|
-
}
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
function hasManagedMarkdownSection(content, name) {
|
|
897
|
-
return content !== null && hasHealthyManagedMarkdownMarkers(content, name);
|
|
898
|
-
}
|
|
899
|
-
function createLocalCapabilityReport(runtime, declarations) {
|
|
900
|
-
const normalizedRuntime = isRuntime(runtime) ? runtime : "unknown";
|
|
901
|
-
return {
|
|
902
|
-
namespace: QUALITY_CAPABILITY_NAMESPACE,
|
|
903
|
-
version: QUALITY_CAPABILITY_VERSION,
|
|
904
|
-
runtime: normalizedRuntime,
|
|
905
|
-
capabilities: QUALITY_CAPABILITY_IDS.map((id) => declarationFor(id, declarations, normalizedRuntime))
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
function localQualityStatus(profile, status) {
|
|
909
|
-
if ((profile === "high" || profile === "release") && status === "pass") return "incomplete";
|
|
910
|
-
return status;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// src/adapters/opencode.ts
|
|
914
|
-
var gitReadPrefix = "git --no-pager -c core.fsmonitor=false -c log.showSignature=false";
|
|
915
|
-
var gitReadCommands = [
|
|
916
|
-
"diff",
|
|
917
|
-
"diff --stat",
|
|
918
|
-
"diff --name-only",
|
|
919
|
-
"diff --cached",
|
|
920
|
-
"log",
|
|
921
|
-
"log --oneline -10"
|
|
922
|
-
].map((action) => `${gitReadPrefix} ${action} --no-ext-diff --no-textconv --end-of-options`);
|
|
923
|
-
function yamlString(value) {
|
|
924
|
-
return JSON.stringify(value);
|
|
925
|
-
}
|
|
926
|
-
function readMcpConfig(file) {
|
|
927
|
-
let content;
|
|
928
|
-
try {
|
|
929
|
-
content = fs6.readFileSync(file, "utf8");
|
|
930
|
-
} catch (error) {
|
|
931
|
-
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
932
|
-
if (code === "ENOENT") return null;
|
|
933
|
-
throw new Error(`OpenCode: no se pudo leer la configuraci\xF3n MCP en ${file} (${code}).`);
|
|
934
|
-
}
|
|
935
|
-
if (content.trim() !== "") {
|
|
936
|
-
try {
|
|
937
|
-
if (objectValue(JSON.parse(content)) === null) throw new Error();
|
|
938
|
-
} catch {
|
|
939
|
-
throw new Error(`OpenCode: la configuraci\xF3n MCP en ${file} debe contener un objeto JSON v\xE1lido.`);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
return content;
|
|
943
|
-
}
|
|
944
|
-
function isManagedOptionalStdioServer(server, value) {
|
|
945
|
-
if (!server.optional || server.transport !== "stdio" || value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
946
|
-
return false;
|
|
947
|
-
}
|
|
948
|
-
const current = value;
|
|
949
|
-
const expectedCommand = [server.command, ...server.args ?? []];
|
|
950
|
-
return Object.keys(current).length === 2 && current.type === "local" && Array.isArray(current.command) && current.command.length === expectedCommand.length && current.command.every((arg, index) => arg === expectedCommand[index]);
|
|
951
|
-
}
|
|
952
|
-
var PRIMARY_MODEL = "openai/gpt-5.6-sol";
|
|
953
|
-
var PRIMARY_MODEL_ID = "gpt-5.6-sol";
|
|
954
|
-
var PRIMARY_LIMITS = { context: 872e3, input: 744e3, output: 128e3 };
|
|
955
|
-
var PRIMARY_MODEL_FIELD = "model";
|
|
956
|
-
var PRIMARY_PROVIDER_FIELD = "provider";
|
|
957
|
-
var PRIMARY_OPENAI_FIELD = "provider.openai";
|
|
958
|
-
var PRIMARY_MODELS_FIELD = "provider.openai.models";
|
|
959
|
-
var PRIMARY_SOL_FIELD = `provider.openai.models.${PRIMARY_MODEL_ID}`;
|
|
960
|
-
var PRIMARY_LIMIT_PREFIX = `provider.openai.models.${PRIMARY_MODEL_ID}.limit`;
|
|
961
|
-
function objectValue(value) {
|
|
962
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
963
|
-
}
|
|
964
|
-
function isCompatibleContext7Server(server, value) {
|
|
965
|
-
const current = objectValue(value);
|
|
966
|
-
if (server.transport !== "http" || typeof server.url !== "string" || current === null) return false;
|
|
967
|
-
return current.type === "remote" && current.url === server.url && (current.enabled === void 0 || current.enabled === true);
|
|
968
|
-
}
|
|
969
|
-
function canonicalContext7Server(server) {
|
|
970
|
-
const headers = {};
|
|
971
|
-
for (const [key, raw] of Object.entries(server.headers ?? {})) {
|
|
972
|
-
const envRef = /^\$\{(\w+)\}$/.exec(raw);
|
|
973
|
-
headers[key] = envRef ? `{env:${envRef[1]}}` : raw;
|
|
974
|
-
}
|
|
975
|
-
return {
|
|
976
|
-
type: "remote",
|
|
977
|
-
url: server.url,
|
|
978
|
-
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
function isCanonicalContext7Server(server, value) {
|
|
982
|
-
return isCompatibleContext7Server(server, value) && isDeepStrictEqual(value, canonicalContext7Server(server));
|
|
983
|
-
}
|
|
984
|
-
function assertCompatibleContext7(server, value) {
|
|
985
|
-
if (value !== void 0 && !isCompatibleContext7Server(server, value)) {
|
|
986
|
-
throw new Error("OpenCode: MCP 'context7' entra en conflicto con una definici\xF3n existente (endpoint, tipo o estado nativo incompatible). Conserva la configuraci\xF3n y corrige el conflicto antes de reintentar.");
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
function ensureObject(parent, key, fieldPath) {
|
|
990
|
-
if (parent[key] === void 0) parent[key] = {};
|
|
991
|
-
const value = objectValue(parent[key]);
|
|
992
|
-
if (value === null) throw new Error(`OpenCode: '${fieldPath}' debe ser un objeto; corr\xEDgelo antes de reintentar sync.`);
|
|
993
|
-
return value;
|
|
994
|
-
}
|
|
995
|
-
function ensureOwnedPrimaryObject(parent, key, field, owned, changes) {
|
|
996
|
-
const created = parent[key] === void 0;
|
|
997
|
-
const value = ensureObject(parent, key, field);
|
|
998
|
-
if (created && owned?.has(field) !== true) changes.push({ field, owned: true });
|
|
999
|
-
return value;
|
|
1000
|
-
}
|
|
1001
|
-
function pruneEmpty(parent, key) {
|
|
1002
|
-
const value = objectValue(parent[key]);
|
|
1003
|
-
if (value !== null && Object.keys(value).length === 0) delete parent[key];
|
|
1004
|
-
}
|
|
1005
|
-
function hasOpenCodeManualApproval(configDir) {
|
|
1006
|
-
const content = readTextIfExists(path9.join(configDir, "opencode.json"));
|
|
1007
|
-
if (content === null) return false;
|
|
1008
|
-
try {
|
|
1009
|
-
const root = JSON.parse(content);
|
|
1010
|
-
const permission = objectValue(objectValue(root)?.permission);
|
|
1011
|
-
const expected = loadCanonicalDefaults(stackRoot())["opencode"]?.["permission"];
|
|
1012
|
-
return permission !== null && expected !== void 0 && isDeepStrictEqual(permission, expected);
|
|
1013
|
-
} catch {
|
|
1014
|
-
return false;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
var opencodeAdapter = {
|
|
1018
|
-
id: "opencode",
|
|
1019
|
-
name: "OpenCode",
|
|
1020
|
-
detect: detectOpenCode,
|
|
1021
|
-
reportCapabilities(configDir) {
|
|
1022
|
-
const prompt = readTextIfExists(path9.join(configDir, "AGENTS.md"));
|
|
1023
|
-
return createLocalCapabilityReport("opencode", [
|
|
1024
|
-
...hasManagedMarkdownSection(prompt, "system-prompt") ? [{
|
|
1025
|
-
id: "policy-guidance",
|
|
1026
|
-
state: "prompt-only",
|
|
1027
|
-
reason: "The managed policy prompt is advisory and cannot enforce the policy",
|
|
1028
|
-
evidence: { source: "jorgex-stack-system-prompt", version: "1" }
|
|
1029
|
-
}] : [],
|
|
1030
|
-
...hasOpenCodeManualApproval(configDir) ? [{
|
|
1031
|
-
id: "tool-approval",
|
|
1032
|
-
state: "manual",
|
|
1033
|
-
reason: "Canonical approval declarations require a human decision; runtime activation is not certified",
|
|
1034
|
-
evidence: { source: "jorgex-stack-opencode-approval-policy", version: "1" }
|
|
1035
|
-
}] : []
|
|
1036
|
-
]);
|
|
1037
|
-
},
|
|
1038
|
-
injectEngramProtocol() {
|
|
1039
|
-
return false;
|
|
1040
|
-
},
|
|
1041
|
-
paths(configDir) {
|
|
1042
|
-
const isRealConfigDir = samePath(
|
|
1043
|
-
configDir,
|
|
1044
|
-
process.env.OPENCODE_CONFIG_DIR ?? path9.join(HOME, ".config", "opencode")
|
|
1045
|
-
);
|
|
1046
|
-
const agentsHome = isRealConfigDir ? HOME : path9.dirname(configDir);
|
|
1047
|
-
return {
|
|
1048
|
-
systemPromptFile: path9.join(configDir, "AGENTS.md"),
|
|
1049
|
-
agentsDir: path9.join(configDir, "agents"),
|
|
1050
|
-
skillsDir: path9.join(agentsHome, ".agents", "skills"),
|
|
1051
|
-
commandsDir: path9.join(configDir, "commands"),
|
|
1052
|
-
pluginsDir: path9.join(configDir, "plugins"),
|
|
1053
|
-
scriptsDir: path9.join(configDir, "scripts"),
|
|
1054
|
-
outputStylesDir: null,
|
|
1055
|
-
profilesDir: null
|
|
1056
|
-
};
|
|
1057
|
-
},
|
|
1058
|
-
renderAgent(agent, models) {
|
|
1059
|
-
const lines = [`description: ${yamlString(agent.description)}`, `mode: ${agent.mode}`];
|
|
1060
|
-
if (agent.mode === "subagent") {
|
|
1061
|
-
const tierModel = resolveAgentModel(models, agent.name, agent.tier);
|
|
1062
|
-
lines.push(`model: ${tierModel.model}`);
|
|
1063
|
-
if (tierModel.variant) lines.push(`variant: ${tierModel.variant}`);
|
|
1064
|
-
if (agent.readonly || agent.bash !== "full" || !agent.spawn) lines.push("permission:");
|
|
1065
|
-
if (agent.readonly) lines.push(" edit: deny");
|
|
1066
|
-
if (agent.bash === "none") lines.push(" bash: deny");
|
|
1067
|
-
else if (agent.bash === "git-read") {
|
|
1068
|
-
lines.push(' bash:\n "*": deny');
|
|
1069
|
-
for (const command of gitReadCommands) {
|
|
1070
|
-
lines.push(` ${yamlString(command)}: allow`, ` ${yamlString(`${command} *`)}: allow`);
|
|
1071
|
-
}
|
|
1072
|
-
const permission = objectValue(loadCanonicalDefaults(stackRoot())["opencode"]?.permission);
|
|
1073
|
-
const bash = objectValue(permission?.bash);
|
|
1074
|
-
if (bash === null) throw new Error("OpenCode: canonical Bash policy is required for git-read agents.");
|
|
1075
|
-
for (const [pattern, decision] of Object.entries(bash)) {
|
|
1076
|
-
if (decision === "deny") lines.push(` ${yamlString(pattern)}: deny`);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
if (!agent.spawn) lines.push(" task: deny");
|
|
1080
|
-
}
|
|
1081
|
-
return [
|
|
1082
|
-
{
|
|
1083
|
-
file: `${agent.name}.md`,
|
|
1084
|
-
content: `---
|
|
1085
|
-
${lines.join("\n")}
|
|
1086
|
-
---
|
|
1087
|
-
${agent.body}${agent.bash === "git-read" ? `
|
|
1088
|
-
|
|
1089
|
-
Use only these read-only Git command prefixes; put refs and paths after --end-of-options:
|
|
1090
|
-
${gitReadCommands.map((command) => `- \`${command}\``).join("\n")}
|
|
1091
|
-
` : ""}`,
|
|
1092
|
-
kind: "agent"
|
|
1093
|
-
}
|
|
1094
|
-
];
|
|
1095
|
-
},
|
|
1096
|
-
renderCommand(file, content) {
|
|
1097
|
-
return { file, content: content.replace(/\{\{input\}\}/g, "$ARGUMENTS") };
|
|
1098
|
-
},
|
|
1099
|
-
planHooks(canonical, ctx) {
|
|
1100
|
-
const actions = [];
|
|
1101
|
-
const { scriptsDir } = this.paths(ctx.configDir);
|
|
1102
|
-
const bashEntries = {};
|
|
1103
|
-
for (const [event, entries] of Object.entries(canonical.hooks)) {
|
|
1104
|
-
if (event !== "PostToolUse") {
|
|
1105
|
-
ctx.warnings.push(`opencode: evento de hook '${event}' a\xFAn no soportado por el puente \u2014 omitido.`);
|
|
1106
|
-
continue;
|
|
1107
|
-
}
|
|
1108
|
-
for (const entry of entries) {
|
|
1109
|
-
if (!(entry.matcher ?? "").split("|").some((p7) => p7.trim().toLowerCase() === "bash")) {
|
|
1110
|
-
ctx.warnings.push(`opencode: matcher de hook '${entry.matcher}' no soportado \u2014 omitido.`);
|
|
1111
|
-
continue;
|
|
1112
|
-
}
|
|
1113
|
-
const includes = entry["x-command-includes"] ?? "*";
|
|
1114
|
-
for (const hook of entry.hooks) {
|
|
1115
|
-
const match = /\{\{SCRIPTS_DIR\}\}[/\\]([\w./\\-]+)/.exec(hook.command);
|
|
1116
|
-
if (!match) {
|
|
1117
|
-
ctx.warnings.push(`opencode: hook sin {{SCRIPTS_DIR}} no traducible: ${hook.command}`);
|
|
1118
|
-
continue;
|
|
1119
|
-
}
|
|
1120
|
-
const script = `scripts/${path9.basename(match[1])}`;
|
|
1121
|
-
(bashEntries[includes] ??= []).push(script);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
const hooksFile = path9.join(ctx.configDir, "hooks.json");
|
|
1126
|
-
const content = upsertJson(readTextIfExists(hooksFile), (root) => {
|
|
1127
|
-
const afterValue = root["tool.execute.after"] ??= {};
|
|
1128
|
-
if (afterValue === null || typeof afterValue !== "object" || Array.isArray(afterValue)) {
|
|
1129
|
-
ctx.warnings.push("opencode: tool.execute.after no es un objeto; hooks gestionados omitidos.");
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
const after = afterValue;
|
|
1133
|
-
const bashValue = after["bash"];
|
|
1134
|
-
if (bashValue !== void 0 && !Array.isArray(bashValue) && (bashValue === null || typeof bashValue !== "object")) {
|
|
1135
|
-
ctx.warnings.push("opencode: tool.execute.after.bash no es un array ni un mapa; hooks gestionados omitidos.");
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
const bash = Array.isArray(bashValue) ? { "*": bashValue } : bashValue ?? {};
|
|
1139
|
-
after["bash"] = bash;
|
|
1140
|
-
const managedScripts = new Set(Object.values(bashEntries).flat());
|
|
1141
|
-
for (const [includes, scripts] of Object.entries(bash)) {
|
|
1142
|
-
if (!Array.isArray(scripts)) continue;
|
|
1143
|
-
const preserved = scripts.filter(
|
|
1144
|
-
(script) => typeof script !== "string" || !managedScripts.has(script)
|
|
1145
|
-
);
|
|
1146
|
-
if (preserved.length > 0) bash[includes] = preserved;
|
|
1147
|
-
else delete bash[includes];
|
|
1148
|
-
}
|
|
1149
|
-
for (const [includes, scripts] of Object.entries(bashEntries)) {
|
|
1150
|
-
const current = bash[includes];
|
|
1151
|
-
if (current !== void 0 && !Array.isArray(current)) {
|
|
1152
|
-
ctx.warnings.push(`opencode: trigger bash '${includes}' no es un array; hook gestionado omitido.`);
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
const list = bash[includes] ??= [];
|
|
1156
|
-
for (const s of scripts) if (!list.includes(s)) list.push(s);
|
|
1157
|
-
}
|
|
1158
|
-
});
|
|
1159
|
-
actions.push({ kind: "write", target: hooksFile, content });
|
|
1160
|
-
const scriptsSource = path9.join(ctx.stackDir, "scripts");
|
|
1161
|
-
if (fs6.existsSync(scriptsSource)) {
|
|
1162
|
-
for (const f of fs6.readdirSync(scriptsSource)) {
|
|
1163
|
-
actions.push({ kind: "copy", source: path9.join(scriptsSource, f), target: path9.join(scriptsDir, f) });
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
return actions;
|
|
1167
|
-
},
|
|
1168
|
-
planMainConfig(canonical, ctx) {
|
|
1169
|
-
const file = path9.join(ctx.configDir, "opencode.json");
|
|
1170
|
-
const { pluginsDir } = this.paths(ctx.configDir);
|
|
1171
|
-
const original = readMcpConfig(file);
|
|
1172
|
-
const contentSource = original === null || original.trim() === "" ? null : original;
|
|
1173
|
-
const isFreshConfig = contentSource === null;
|
|
1174
|
-
const mcpOwnership = [];
|
|
1175
|
-
const primaryModelOwnership = [];
|
|
1176
|
-
const content = upsertJson(contentSource, (root) => {
|
|
1177
|
-
const rawMcp = root["mcp"];
|
|
1178
|
-
if (rawMcp !== void 0 && objectValue(rawMcp) === null) {
|
|
1179
|
-
throw new Error("OpenCode: la clave 'mcp' debe ser un objeto; corr\xEDgela antes de reintentar sync.");
|
|
1180
|
-
}
|
|
1181
|
-
const existingMcp = rawMcp;
|
|
1182
|
-
const context7 = canonical.servers.context7;
|
|
1183
|
-
if (context7 !== void 0) assertCompatibleContext7(context7, existingMcp?.["context7"]);
|
|
1184
|
-
root["$schema"] ??= "https://opencode.ai/config.json";
|
|
1185
|
-
if (root[PRIMARY_MODEL_FIELD] === void 0) {
|
|
1186
|
-
root[PRIMARY_MODEL_FIELD] = PRIMARY_MODEL;
|
|
1187
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_MODEL_FIELD) !== true) {
|
|
1188
|
-
primaryModelOwnership.push({ field: PRIMARY_MODEL_FIELD, owned: true });
|
|
1189
|
-
}
|
|
1190
|
-
} else if (typeof root[PRIMARY_MODEL_FIELD] !== "string" || root[PRIMARY_MODEL_FIELD].trim() === "") {
|
|
1191
|
-
throw new Error("OpenCode: 'model' debe ser un identificador provider/model no vac\xEDo; corr\xEDgelo antes de reintentar sync.");
|
|
1192
|
-
}
|
|
1193
|
-
const provider = ensureOwnedPrimaryObject(root, "provider", PRIMARY_PROVIDER_FIELD, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
1194
|
-
const openai = ensureOwnedPrimaryObject(provider, "openai", PRIMARY_OPENAI_FIELD, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
1195
|
-
const models = ensureOwnedPrimaryObject(openai, "models", PRIMARY_MODELS_FIELD, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
1196
|
-
const sol = ensureOwnedPrimaryObject(models, PRIMARY_MODEL_ID, PRIMARY_SOL_FIELD, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
1197
|
-
const limit = ensureOwnedPrimaryObject(sol, "limit", PRIMARY_LIMIT_PREFIX, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
1198
|
-
for (const [key, value] of Object.entries(PRIMARY_LIMITS)) {
|
|
1199
|
-
if (limit[key] !== void 0) continue;
|
|
1200
|
-
limit[key] = value;
|
|
1201
|
-
const field = `${PRIMARY_LIMIT_PREFIX}.${key}`;
|
|
1202
|
-
if (ctx.ownedPrimaryModelFields?.has(field) !== true) {
|
|
1203
|
-
primaryModelOwnership.push({ field, owned: true });
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
const defaults = loadCanonicalDefaults(ctx.stackDir)["opencode"];
|
|
1207
|
-
const canonicalPermission = defaults?.["permission"];
|
|
1208
|
-
if (isFreshConfig) {
|
|
1209
|
-
if (canonicalPermission !== void 0) {
|
|
1210
|
-
root["permission"] = canonicalPermission;
|
|
1211
|
-
ctx.warnings.push(
|
|
1212
|
-
"OpenCode: fresh config allows ordinary reads, edits, web access and Bash; sensitive operations ask, while protected paths and obvious destruction are denied. Native matching is not a universal filesystem sandbox."
|
|
1213
|
-
);
|
|
1214
|
-
}
|
|
1215
|
-
} else if (canonicalPermission !== void 0 && !isDeepStrictEqual(root["permission"], canonicalPermission)) {
|
|
1216
|
-
if (ctx.upgradePermissions === true) {
|
|
1217
|
-
root["permission"] = canonicalPermission;
|
|
1218
|
-
} else {
|
|
1219
|
-
ctx.warnings.push(
|
|
1220
|
-
"OpenCode: permission block differs from the stack default and was left untouched; re-run with --upgrade-permissions to replace it (a backup is created first), or edit it by hand. Overwriting discards your own permission changes, including any extra hardenings."
|
|
1221
|
-
);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
const mcp = root["mcp"] ??= {};
|
|
1225
|
-
for (const [name, server] of Object.entries(canonical.servers)) {
|
|
1226
|
-
const existing = mcp[name];
|
|
1227
|
-
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
1228
|
-
if (name === "context7" && existing !== void 0) {
|
|
1229
|
-
if (owned && !isCanonicalContext7Server(server, existing)) {
|
|
1230
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
1231
|
-
}
|
|
1232
|
-
continue;
|
|
1233
|
-
}
|
|
1234
|
-
if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
|
|
1235
|
-
if (owned) {
|
|
1236
|
-
if (isManagedOptionalStdioServer(server, existing)) delete mcp[name];
|
|
1237
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
1238
|
-
}
|
|
1239
|
-
continue;
|
|
1240
|
-
}
|
|
1241
|
-
if (server.optional && existing !== void 0) {
|
|
1242
|
-
if (!owned || !isManagedOptionalStdioServer(server, existing)) {
|
|
1243
|
-
if (owned) mcpOwnership.push({ server: name, owned: false });
|
|
1244
|
-
ctx.warnings.push(`OpenCode: MCP opcional '${name}' ya pertenece a la configuraci\xF3n del usuario; se conserva.`);
|
|
1245
|
-
continue;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
if (server.transport === "stdio") {
|
|
1249
|
-
if (server.command === "{{ENGRAM_BIN}}" && ctx.engramBin === null) {
|
|
1250
|
-
ctx.warnings.push(
|
|
1251
|
-
"Engram no detectado: el MCP 'engram' no se registra. Inst\xE1lalo (github.com/Gentleman-Programming/engram) y re-ejecuta sync."
|
|
1252
|
-
);
|
|
1253
|
-
continue;
|
|
1254
|
-
}
|
|
1255
|
-
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
1256
|
-
mcp[name] = { type: "local", command: [command, ...server.args ?? []] };
|
|
1257
|
-
if (server.optional && existing === void 0 && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
1258
|
-
} else {
|
|
1259
|
-
const previous = mcp[name];
|
|
1260
|
-
const headers = {};
|
|
1261
|
-
for (const [key, raw] of Object.entries(server.headers ?? {})) {
|
|
1262
|
-
const envRef = /^\$\{(\w+)\}$/.exec(raw);
|
|
1263
|
-
headers[key] = previous?.headers?.[key] || (envRef ? `{env:${envRef[1]}}` : raw);
|
|
1264
|
-
}
|
|
1265
|
-
mcp[name] = {
|
|
1266
|
-
type: "remote",
|
|
1267
|
-
url: server.url,
|
|
1268
|
-
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
1269
|
-
};
|
|
1270
|
-
if (name === "context7" && existing === void 0 && !owned) {
|
|
1271
|
-
mcpOwnership.push({ server: name, owned: true });
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
if (pluginsDir !== null) {
|
|
1276
|
-
const plugin = root["plugin"];
|
|
1277
|
-
if (Array.isArray(plugin)) {
|
|
1278
|
-
const pluginsDirPrefix = pathToFileURL(pluginsDir).href + "/";
|
|
1279
|
-
const kept = plugin.filter((url) => !url.startsWith(pluginsDirPrefix));
|
|
1280
|
-
if (kept.length === 0) delete root["plugin"];
|
|
1281
|
-
else root["plugin"] = kept;
|
|
1282
|
-
if (kept.some((u) => /engram/i.test(u))) {
|
|
1283
|
-
ctx.warnings.push(
|
|
1284
|
-
"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)."
|
|
1285
|
-
);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
});
|
|
1290
|
-
return [{
|
|
1291
|
-
kind: "write",
|
|
1292
|
-
target: file,
|
|
1293
|
-
content,
|
|
1294
|
-
...mcpOwnership.length > 0 ? { mcpOwnership } : {},
|
|
1295
|
-
...primaryModelOwnership.length > 0 ? { primaryModelOwnership } : {}
|
|
1296
|
-
}];
|
|
1297
|
-
},
|
|
1298
|
-
planUnmerge(mcp, hooks, ctx) {
|
|
1299
|
-
const actions = [];
|
|
1300
|
-
const { systemPromptFile, pluginsDir } = this.paths(ctx.configDir);
|
|
1301
|
-
const prompt = readTextIfExists(systemPromptFile);
|
|
1302
|
-
if (prompt !== null) {
|
|
1303
|
-
const content = removeSystemPromptSections(prompt);
|
|
1304
|
-
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
1305
|
-
}
|
|
1306
|
-
const configFile = path9.join(ctx.configDir, "opencode.json");
|
|
1307
|
-
const config = readMcpConfig(configFile);
|
|
1308
|
-
if (config !== null) {
|
|
1309
|
-
const mcpOwnership = [];
|
|
1310
|
-
const primaryModelOwnership = [];
|
|
1311
|
-
const content = upsertJson(config, (root) => {
|
|
1312
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_MODEL_FIELD) === true) {
|
|
1313
|
-
if (root[PRIMARY_MODEL_FIELD] === PRIMARY_MODEL) delete root[PRIMARY_MODEL_FIELD];
|
|
1314
|
-
primaryModelOwnership.push({ field: PRIMARY_MODEL_FIELD, owned: false });
|
|
1315
|
-
}
|
|
1316
|
-
const provider = objectValue(root["provider"]);
|
|
1317
|
-
const openai = provider === null ? null : objectValue(provider["openai"]);
|
|
1318
|
-
const models = openai === null ? null : objectValue(openai["models"]);
|
|
1319
|
-
const sol = models === null ? null : objectValue(models[PRIMARY_MODEL_ID]);
|
|
1320
|
-
const limit = sol === null ? null : objectValue(sol["limit"]);
|
|
1321
|
-
if (limit !== null) {
|
|
1322
|
-
for (const [key, value] of Object.entries(PRIMARY_LIMITS)) {
|
|
1323
|
-
const field = `${PRIMARY_LIMIT_PREFIX}.${key}`;
|
|
1324
|
-
if (ctx.ownedPrimaryModelFields?.has(field) !== true) continue;
|
|
1325
|
-
if (limit[key] === value) delete limit[key];
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
if (sol !== null && ctx.ownedPrimaryModelFields?.has(PRIMARY_LIMIT_PREFIX) === true) pruneEmpty(sol, "limit");
|
|
1329
|
-
if (models !== null && ctx.ownedPrimaryModelFields?.has(PRIMARY_SOL_FIELD) === true) pruneEmpty(models, PRIMARY_MODEL_ID);
|
|
1330
|
-
if (openai !== null && ctx.ownedPrimaryModelFields?.has(PRIMARY_MODELS_FIELD) === true) pruneEmpty(openai, "models");
|
|
1331
|
-
if (provider !== null && ctx.ownedPrimaryModelFields?.has(PRIMARY_OPENAI_FIELD) === true) pruneEmpty(provider, "openai");
|
|
1332
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_PROVIDER_FIELD) === true) pruneEmpty(root, "provider");
|
|
1333
|
-
const managedFields = [
|
|
1334
|
-
PRIMARY_PROVIDER_FIELD,
|
|
1335
|
-
PRIMARY_OPENAI_FIELD,
|
|
1336
|
-
PRIMARY_MODELS_FIELD,
|
|
1337
|
-
PRIMARY_SOL_FIELD,
|
|
1338
|
-
PRIMARY_LIMIT_PREFIX,
|
|
1339
|
-
...Object.keys(PRIMARY_LIMITS).map((key) => `${PRIMARY_LIMIT_PREFIX}.${key}`)
|
|
1340
|
-
];
|
|
1341
|
-
for (const field of managedFields) {
|
|
1342
|
-
if (ctx.ownedPrimaryModelFields?.has(field) === true) primaryModelOwnership.push({ field, owned: false });
|
|
1343
|
-
}
|
|
1344
|
-
const rawMcpBlock = root["mcp"];
|
|
1345
|
-
if (rawMcpBlock !== void 0 && objectValue(rawMcpBlock) === null) {
|
|
1346
|
-
throw new Error("OpenCode: la clave 'mcp' debe ser un objeto; corr\xEDgela antes de reintentar uninstall.");
|
|
1347
|
-
}
|
|
1348
|
-
const mcpBlock = rawMcpBlock;
|
|
1349
|
-
if (mcpBlock !== void 0) {
|
|
1350
|
-
for (const [name, server] of Object.entries(mcp.servers)) {
|
|
1351
|
-
if (name === "context7") {
|
|
1352
|
-
const canonical = isCanonicalContext7Server(server, mcpBlock[name]);
|
|
1353
|
-
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
1354
|
-
if (owned) {
|
|
1355
|
-
if (canonical) delete mcpBlock[name];
|
|
1356
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
1357
|
-
}
|
|
1358
|
-
continue;
|
|
1359
|
-
}
|
|
1360
|
-
if (!server.optional) {
|
|
1361
|
-
delete mcpBlock[name];
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
if (ctx.ownedMcpServers?.has(name) === true) {
|
|
1365
|
-
if (isManagedOptionalStdioServer(server, mcpBlock[name])) delete mcpBlock[name];
|
|
1366
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
if (Object.keys(mcpBlock).length === 0) delete root["mcp"];
|
|
1370
|
-
}
|
|
1371
|
-
const plugin = root["plugin"];
|
|
1372
|
-
if (Array.isArray(plugin) && pluginsDir !== null) {
|
|
1373
|
-
const pluginsDirPrefix = pathToFileURL(pluginsDir).href + "/";
|
|
1374
|
-
const kept = plugin.filter((url) => !url.startsWith(pluginsDirPrefix));
|
|
1375
|
-
if (kept.length === 0) delete root["plugin"];
|
|
1376
|
-
else root["plugin"] = kept;
|
|
1377
|
-
}
|
|
1378
|
-
});
|
|
1379
|
-
actions.push({
|
|
1380
|
-
kind: "write",
|
|
1381
|
-
target: configFile,
|
|
1382
|
-
content,
|
|
1383
|
-
...mcpOwnership.length > 0 ? { mcpOwnership } : {},
|
|
1384
|
-
...primaryModelOwnership.length > 0 ? { primaryModelOwnership } : {}
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
const hooksFile = path9.join(ctx.configDir, "hooks.json");
|
|
1388
|
-
const hooksJson = readTextIfExists(hooksFile);
|
|
1389
|
-
if (hooksJson !== null) {
|
|
1390
|
-
const ourScripts = hookScriptNames(hooks);
|
|
1391
|
-
const content = upsertJson(hooksJson, (root) => {
|
|
1392
|
-
const after = root["tool.execute.after"];
|
|
1393
|
-
if (!after) return;
|
|
1394
|
-
for (const tool of Object.keys(after)) {
|
|
1395
|
-
const byCommand = after[tool];
|
|
1396
|
-
for (const includes of Object.keys(byCommand)) {
|
|
1397
|
-
byCommand[includes] = byCommand[includes].filter(
|
|
1398
|
-
(script) => !ourScripts.some((s) => script.includes(s))
|
|
1399
|
-
);
|
|
1400
|
-
if (byCommand[includes].length === 0) delete byCommand[includes];
|
|
1401
|
-
}
|
|
1402
|
-
if (Object.keys(byCommand).length === 0) delete after[tool];
|
|
1403
|
-
}
|
|
1404
|
-
if (Object.keys(after).length === 0) delete root["tool.execute.after"];
|
|
1405
|
-
});
|
|
1406
|
-
actions.push({ kind: "write", target: hooksFile, content: content.trim() === "{}" ? "" : content });
|
|
1407
|
-
}
|
|
1408
|
-
return actions;
|
|
1409
|
-
}
|
|
1410
|
-
};
|
|
1411
|
-
|
|
1412
|
-
// src/adapters/claude-code.ts
|
|
1413
|
-
import path10 from "path";
|
|
1414
|
-
import fs7 from "fs";
|
|
1415
|
-
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
1416
|
-
function yamlString2(value) {
|
|
1417
|
-
return JSON.stringify(value);
|
|
1418
|
-
}
|
|
1419
|
-
var memoryTools = (names) => names.flatMap((n) => [`mcp__engram__${n}`, `mcp__plugin_engram_engram__${n}`]);
|
|
1420
|
-
var MEMORY_TOOLS = memoryTools(["mem_save", "mem_search", "mem_context"]);
|
|
1421
|
-
var ENGRAM_AGENT_TOOLS = [
|
|
1422
|
-
"Read",
|
|
1423
|
-
"Skill",
|
|
1424
|
-
...memoryTools(["mem_context", "mem_search", "mem_get_observation", "mem_timeline", "mem_current_project"])
|
|
1425
|
-
];
|
|
1426
|
-
function isRecord(value) {
|
|
1427
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1428
|
-
}
|
|
1429
|
-
function readMcpConfig2(file) {
|
|
1430
|
-
let content;
|
|
1431
|
-
try {
|
|
1432
|
-
content = fs7.readFileSync(file, "utf8");
|
|
1433
|
-
} catch (error) {
|
|
1434
|
-
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
1435
|
-
if (code === "ENOENT") return null;
|
|
1436
|
-
throw new Error(`Claude Code: no se pudo leer la configuraci\xF3n MCP en ${file} (${code}).`);
|
|
1437
|
-
}
|
|
1438
|
-
if (content.trim() !== "") {
|
|
1439
|
-
try {
|
|
1440
|
-
if (!isRecord(JSON.parse(content))) throw new Error();
|
|
1441
|
-
} catch {
|
|
1442
|
-
throw new Error(`Claude Code: la configuraci\xF3n MCP en ${file} debe contener un objeto JSON v\xE1lido.`);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
return content;
|
|
1446
|
-
}
|
|
1447
|
-
function isCompatibleContext7Server2(server, value) {
|
|
1448
|
-
if (server.transport !== "http" || typeof server.url !== "string" || !isRecord(value)) return false;
|
|
1449
|
-
return value.type === "http" && value.url === server.url;
|
|
1450
|
-
}
|
|
1451
|
-
function canonicalContext7Server2(server) {
|
|
1452
|
-
const headers = {};
|
|
1453
|
-
for (const [key, raw] of Object.entries(server.headers ?? {})) {
|
|
1454
|
-
headers[key] = /^\$\{(\w+)\}$/.test(raw) ? "" : raw;
|
|
1455
|
-
}
|
|
1456
|
-
return {
|
|
1457
|
-
type: "http",
|
|
1458
|
-
url: server.url,
|
|
1459
|
-
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
1460
|
-
};
|
|
1461
|
-
}
|
|
1462
|
-
function isCanonicalContext7Server2(server, value) {
|
|
1463
|
-
return isCompatibleContext7Server2(server, value) && isDeepStrictEqual2(value, canonicalContext7Server2(server));
|
|
1464
|
-
}
|
|
1465
|
-
function assertCompatibleContext72(server, value) {
|
|
1466
|
-
if (value !== void 0 && !isCompatibleContext7Server2(server, value)) {
|
|
1467
|
-
throw new Error("Claude Code: MCP 'context7' entra en conflicto con una definici\xF3n existente (endpoint o tipo incompatible). Conserva la configuraci\xF3n y corrige el conflicto antes de reintentar.");
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
function hasClaudeManualApproval(configDir) {
|
|
1471
|
-
const content = readTextIfExists(path10.join(configDir, "settings.json"));
|
|
1472
|
-
if (content === null) return false;
|
|
1473
|
-
try {
|
|
1474
|
-
const root = JSON.parse(content);
|
|
1475
|
-
const permissions = isRecord(root) ? root.permissions : void 0;
|
|
1476
|
-
const expected = loadCanonicalDefaults(stackRoot())["claude-code"]?.["permissions"];
|
|
1477
|
-
return isRecord(permissions) && expected !== void 0 && isDeepStrictEqual2(permissions, expected);
|
|
1478
|
-
} catch {
|
|
1479
|
-
return false;
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
function toolsFor(agent) {
|
|
1483
|
-
if (agent.name === "engram") return ENGRAM_AGENT_TOOLS.join(", ");
|
|
1484
|
-
if (!agent.readonly) return null;
|
|
1485
|
-
const tools = ["Read", "Grep", "Glob", "Skill"];
|
|
1486
|
-
if (agent.bash !== "none") tools.push("Bash");
|
|
1487
|
-
tools.push(...MEMORY_TOOLS);
|
|
1488
|
-
return tools.join(", ");
|
|
1489
|
-
}
|
|
1490
|
-
function hasEngramPlugin(configDir) {
|
|
1491
|
-
if (fs7.existsSync(path10.join(configDir, "plugins", "marketplaces", "engram"))) return true;
|
|
1492
|
-
const registry = readTextIfExists(path10.join(configDir, "plugins", "installed_plugins.json"));
|
|
1493
|
-
if (registry === null) return false;
|
|
1494
|
-
try {
|
|
1495
|
-
const parsed = JSON.parse(registry);
|
|
1496
|
-
const keys = [...Object.keys(parsed), ...Object.keys(parsed["plugins"] ?? {})];
|
|
1497
|
-
return keys.some((k) => k === "engram" || k.startsWith("engram@"));
|
|
1498
|
-
} catch {
|
|
1499
|
-
return false;
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
function isManagedOptionalStdioServer2(server, value) {
|
|
1503
|
-
if (!server.optional || server.transport !== "stdio" || value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1504
|
-
return false;
|
|
1505
|
-
}
|
|
1506
|
-
const current = value;
|
|
1507
|
-
const expectedArgs = server.args ?? [];
|
|
1508
|
-
return Object.keys(current).length === 3 && current.type === "stdio" && current.command === server.command && Array.isArray(current.args) && current.args.length === expectedArgs.length && current.args.every((arg, index) => arg === expectedArgs[index]);
|
|
1509
|
-
}
|
|
1510
|
-
var claudeCodeAdapter = {
|
|
1511
|
-
id: "claude-code",
|
|
1512
|
-
name: "Claude Code",
|
|
1513
|
-
detect: detectClaudeCode,
|
|
1514
|
-
reportCapabilities(configDir) {
|
|
1515
|
-
const prompt = readTextIfExists(path10.join(configDir, "CLAUDE.md"));
|
|
1516
|
-
return createLocalCapabilityReport("claude-code", [
|
|
1517
|
-
...hasManagedMarkdownSection(prompt, "system-prompt") ? [{
|
|
1518
|
-
id: "policy-guidance",
|
|
1519
|
-
state: "prompt-only",
|
|
1520
|
-
reason: "The managed policy prompt is advisory and cannot enforce the policy",
|
|
1521
|
-
evidence: { source: "jorgex-stack-system-prompt", version: "1" }
|
|
1522
|
-
}] : [],
|
|
1523
|
-
...hasClaudeManualApproval(configDir) ? [{
|
|
1524
|
-
id: "tool-approval",
|
|
1525
|
-
state: "manual",
|
|
1526
|
-
reason: "Canonical approval declarations require a human decision; runtime activation is not certified",
|
|
1527
|
-
evidence: { source: "jorgex-stack-claude-approval-policy", version: "1" }
|
|
1528
|
-
}] : []
|
|
1529
|
-
]);
|
|
1530
|
-
},
|
|
1531
|
-
injectEngramProtocol(ctx) {
|
|
1532
|
-
return !hasEngramPlugin(ctx.configDir);
|
|
1533
|
-
},
|
|
1534
|
-
paths(configDir) {
|
|
1535
|
-
return {
|
|
1536
|
-
systemPromptFile: path10.join(configDir, "CLAUDE.md"),
|
|
1537
|
-
agentsDir: path10.join(configDir, "agents"),
|
|
1538
|
-
skillsDir: path10.join(configDir, "skills"),
|
|
1539
|
-
commandsDir: path10.join(configDir, "commands"),
|
|
1540
|
-
pluginsDir: null,
|
|
1541
|
-
scriptsDir: path10.join(configDir, "scripts"),
|
|
1542
|
-
outputStylesDir: path10.join(configDir, "output-styles"),
|
|
1543
|
-
profilesDir: null
|
|
1544
|
-
};
|
|
1545
|
-
},
|
|
1546
|
-
renderAgent(agent, models) {
|
|
1547
|
-
if (agent.mode === "primary") {
|
|
1548
|
-
const title = agent.name.charAt(0).toUpperCase() + agent.name.slice(1);
|
|
1549
|
-
const style = `---
|
|
1550
|
-
name: ${title}
|
|
1551
|
-
description: ${yamlString2(agent.description)}
|
|
1552
|
-
keep-coding-instructions: true
|
|
1553
|
-
---
|
|
1554
|
-
${agent.body}`;
|
|
1555
|
-
return [{ file: `${agent.name}.md`, content: style, kind: "output-style" }];
|
|
1556
|
-
}
|
|
1557
|
-
const lines = [`name: ${agent.name}`, `description: ${yamlString2(agent.description)}`];
|
|
1558
|
-
const tools = toolsFor(agent);
|
|
1559
|
-
if (tools !== null) lines.push(`tools: ${tools}`);
|
|
1560
|
-
lines.push(`model: ${resolveAgentModel(models, agent.name, agent.tier).model}`);
|
|
1561
|
-
return [
|
|
1562
|
-
{
|
|
1563
|
-
file: `${agent.name}.md`,
|
|
1564
|
-
content: `---
|
|
1565
|
-
${lines.join("\n")}
|
|
1566
|
-
---
|
|
1567
|
-
${agent.body}`,
|
|
1568
|
-
kind: "agent"
|
|
1569
|
-
}
|
|
1570
|
-
];
|
|
1571
|
-
},
|
|
1572
|
-
renderCommand(file, content) {
|
|
1573
|
-
return { file, content: content.replace(/\{\{input\}\}/g, "$ARGUMENTS") };
|
|
1574
|
-
},
|
|
1575
|
-
planHooks(canonical, ctx) {
|
|
1576
|
-
const actions = [];
|
|
1577
|
-
const { scriptsDir } = this.paths(ctx.configDir);
|
|
1578
|
-
const original = readTextIfExists(path10.join(ctx.configDir, "settings.json"));
|
|
1579
|
-
const contentSource = original === null || original.trim() === "" ? null : original;
|
|
1580
|
-
const settingsFile = path10.join(ctx.configDir, "settings.json");
|
|
1581
|
-
let content = upsertNativeHooks(contentSource, canonical, scriptsDir);
|
|
1582
|
-
const defaults = loadCanonicalDefaults(ctx.stackDir)["claude-code"];
|
|
1583
|
-
const canonicalPermissions = defaults?.["permissions"];
|
|
1584
|
-
if (contentSource === null) {
|
|
1585
|
-
if (canonicalPermissions !== void 0) {
|
|
1586
|
-
content = upsertJson(content, (root) => {
|
|
1587
|
-
if (root["permissions"] === void 0) {
|
|
1588
|
-
root["permissions"] = canonicalPermissions;
|
|
1589
|
-
ctx.warnings.push(
|
|
1590
|
-
"Claude Code: fresh config enables read-anywhere via Read/Grep/Glob allow rules; shell, writes and web egress remain approval-gated, but broad local reads can expose secrets not covered by deny rules."
|
|
1591
|
-
);
|
|
1592
|
-
}
|
|
1593
|
-
});
|
|
1594
|
-
}
|
|
1595
|
-
} else if (canonicalPermissions !== void 0) {
|
|
1596
|
-
content = upsertJson(content, (root) => {
|
|
1597
|
-
if (isDeepStrictEqual2(root["permissions"], canonicalPermissions)) return;
|
|
1598
|
-
if (ctx.upgradePermissions === true) {
|
|
1599
|
-
root["permissions"] = canonicalPermissions;
|
|
1600
|
-
} else {
|
|
1601
|
-
ctx.warnings.push(
|
|
1602
|
-
"Claude Code: permissions block differs from the stack default and was left untouched; re-run with --upgrade-permissions to replace it (a backup is created first), or edit it by hand. Overwriting discards your own permission changes, including any extra hardenings."
|
|
1603
|
-
);
|
|
1604
|
-
}
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
actions.push({ kind: "write", target: settingsFile, content });
|
|
1608
|
-
const scriptsSource = path10.join(ctx.stackDir, "scripts");
|
|
1609
|
-
if (fs7.existsSync(scriptsSource)) {
|
|
1610
|
-
for (const f of fs7.readdirSync(scriptsSource)) {
|
|
1611
|
-
actions.push({ kind: "copy", source: path10.join(scriptsSource, f), target: path10.join(scriptsDir, f) });
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
return actions;
|
|
1615
|
-
},
|
|
1616
|
-
planMainConfig(canonical, ctx) {
|
|
1617
|
-
const file = path10.join(path10.dirname(ctx.configDir), `${path10.basename(ctx.configDir)}.json`);
|
|
1618
|
-
const mcpOwnership = [];
|
|
1619
|
-
const content = upsertJson(readMcpConfig2(file), (root) => {
|
|
1620
|
-
const rawServers = root["mcpServers"];
|
|
1621
|
-
if (rawServers !== void 0 && !isRecord(rawServers)) {
|
|
1622
|
-
throw new Error("Claude Code: la clave 'mcpServers' debe ser un objeto; corr\xEDgela antes de reintentar sync.");
|
|
1623
|
-
}
|
|
1624
|
-
const existingServers = rawServers;
|
|
1625
|
-
const context7 = canonical.servers.context7;
|
|
1626
|
-
if (context7 !== void 0) assertCompatibleContext72(context7, existingServers?.["context7"]);
|
|
1627
|
-
const servers = root["mcpServers"] ??= {};
|
|
1628
|
-
for (const [name, server] of Object.entries(canonical.servers)) {
|
|
1629
|
-
const existing = servers[name];
|
|
1630
|
-
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
1631
|
-
if (name === "context7" && existing !== void 0) {
|
|
1632
|
-
if (owned && !isCanonicalContext7Server2(server, existing)) {
|
|
1633
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
1634
|
-
}
|
|
1635
|
-
continue;
|
|
1636
|
-
}
|
|
1637
|
-
if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
|
|
1638
|
-
if (owned) {
|
|
1639
|
-
if (isManagedOptionalStdioServer2(server, existing)) delete servers[name];
|
|
1640
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
1641
|
-
}
|
|
1642
|
-
continue;
|
|
1643
|
-
}
|
|
1644
|
-
if (server.optional && existing !== void 0) {
|
|
1645
|
-
if (!owned || !isManagedOptionalStdioServer2(server, existing)) {
|
|
1646
|
-
if (owned) mcpOwnership.push({ server: name, owned: false });
|
|
1647
|
-
ctx.warnings.push(`Claude Code: MCP opcional '${name}' ya pertenece a la configuraci\xF3n del usuario; se conserva.`);
|
|
1648
|
-
continue;
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
if (server.transport === "stdio") {
|
|
1652
|
-
if (server.command === "{{ENGRAM_BIN}}" && hasEngramPlugin(ctx.configDir)) {
|
|
1653
|
-
if (name in servers) delete servers[name];
|
|
1654
|
-
ctx.warnings.push(
|
|
1655
|
-
"Claude Code: Engram ya est\xE1 integrado v\xEDa plugin \u2014 no se registra el MCP para no duplicar las tools de memoria."
|
|
1656
|
-
);
|
|
1657
|
-
continue;
|
|
1658
|
-
}
|
|
1659
|
-
if (server.command === "{{ENGRAM_BIN}}" && ctx.engramBin === null) {
|
|
1660
|
-
ctx.warnings.push(
|
|
1661
|
-
"Engram no detectado: el MCP 'engram' no se registra. Inst\xE1lalo (github.com/Gentleman-Programming/engram) y re-ejecuta sync."
|
|
1662
|
-
);
|
|
1663
|
-
continue;
|
|
1664
|
-
}
|
|
1665
|
-
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
1666
|
-
servers[name] = { type: "stdio", command, args: server.args ?? [] };
|
|
1667
|
-
if (server.optional && existing === void 0 && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
1668
|
-
} else {
|
|
1669
|
-
const previous = servers[name];
|
|
1670
|
-
const headers = {};
|
|
1671
|
-
for (const [key, raw] of Object.entries(server.headers ?? {})) {
|
|
1672
|
-
const envRef = /^\$\{(\w+)\}$/.exec(raw);
|
|
1673
|
-
headers[key] = previous?.headers?.[key] || (envRef ? "" : raw);
|
|
1674
|
-
}
|
|
1675
|
-
servers[name] = {
|
|
1676
|
-
type: "http",
|
|
1677
|
-
url: server.url,
|
|
1678
|
-
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
1679
|
-
};
|
|
1680
|
-
if (name === "context7" && existing === void 0 && !owned) {
|
|
1681
|
-
mcpOwnership.push({ server: name, owned: true });
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
});
|
|
1686
|
-
return [{ kind: "write", target: file, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} }];
|
|
1687
|
-
},
|
|
1688
|
-
planUnmerge(mcp, hooks, ctx) {
|
|
1689
|
-
const actions = [];
|
|
1690
|
-
const { systemPromptFile } = this.paths(ctx.configDir);
|
|
1691
|
-
const prompt = readTextIfExists(systemPromptFile);
|
|
1692
|
-
if (prompt !== null) {
|
|
1693
|
-
const content = removeSystemPromptSections(prompt);
|
|
1694
|
-
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
1695
|
-
}
|
|
1696
|
-
const settingsFile = path10.join(ctx.configDir, "settings.json");
|
|
1697
|
-
const settings = readTextIfExists(settingsFile);
|
|
1698
|
-
if (settings !== null) {
|
|
1699
|
-
const content = removeNativeHooks(settings, hooks);
|
|
1700
|
-
if (content !== null) {
|
|
1701
|
-
actions.push({ kind: "write", target: settingsFile, content: content.trim() === "{}" ? "" : content });
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
const mainFile = path10.join(path10.dirname(ctx.configDir), `${path10.basename(ctx.configDir)}.json`);
|
|
1705
|
-
const main2 = readMcpConfig2(mainFile);
|
|
1706
|
-
if (main2 !== null) {
|
|
1707
|
-
const mcpOwnership = [];
|
|
1708
|
-
const content = upsertJson(main2, (root) => {
|
|
1709
|
-
const rawServers = root["mcpServers"];
|
|
1710
|
-
if (rawServers === void 0) return;
|
|
1711
|
-
if (!isRecord(rawServers)) {
|
|
1712
|
-
throw new Error("Claude Code: la clave 'mcpServers' debe ser un objeto; corr\xEDgela antes de reintentar uninstall.");
|
|
1713
|
-
}
|
|
1714
|
-
const servers = rawServers;
|
|
1715
|
-
for (const [name, server] of Object.entries(mcp.servers)) {
|
|
1716
|
-
if (name === "context7") {
|
|
1717
|
-
const canonical = isCanonicalContext7Server2(server, servers[name]);
|
|
1718
|
-
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
1719
|
-
if (owned) {
|
|
1720
|
-
if (canonical) delete servers[name];
|
|
1721
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
1722
|
-
}
|
|
1723
|
-
continue;
|
|
1724
|
-
}
|
|
1725
|
-
if (!server.optional) {
|
|
1726
|
-
delete servers[name];
|
|
1727
|
-
continue;
|
|
1728
|
-
}
|
|
1729
|
-
if (ctx.ownedMcpServers?.has(name) === true) {
|
|
1730
|
-
if (isManagedOptionalStdioServer2(server, servers[name])) delete servers[name];
|
|
1731
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
if (Object.keys(servers).length === 0) delete root["mcpServers"];
|
|
1735
|
-
});
|
|
1736
|
-
actions.push({ kind: "write", target: mainFile, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} });
|
|
1737
|
-
}
|
|
1738
|
-
return actions;
|
|
1739
|
-
}
|
|
1740
|
-
};
|
|
1741
|
-
|
|
1742
|
-
// src/adapters/codex.ts
|
|
1743
|
-
import path11 from "path";
|
|
1744
|
-
import fs8 from "fs";
|
|
1745
|
-
function tomlString(value) {
|
|
1746
|
-
return JSON.stringify(value);
|
|
1747
|
-
}
|
|
1748
|
-
function readMcpConfig3(file) {
|
|
1749
|
-
try {
|
|
1750
|
-
return fs8.readFileSync(file, "utf8");
|
|
1751
|
-
} catch (error) {
|
|
1752
|
-
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
1753
|
-
if (code === "ENOENT") return null;
|
|
1754
|
-
throw new Error(`Codex: no se pudo leer la configuraci\xF3n MCP en ${file} (${code}).`);
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
var PRIMARY_MODEL2 = '"gpt-5.6-sol"';
|
|
1758
|
-
var PRIMARY_CONTEXT_WINDOW = "872000";
|
|
1759
|
-
var PRIMARY_MODEL_FIELD2 = "model";
|
|
1760
|
-
var PRIMARY_CONTEXT_FIELD = "model_context_window";
|
|
1761
|
-
function tomlMultiline(value) {
|
|
1762
|
-
if (value.includes("'''")) return JSON.stringify(value);
|
|
1763
|
-
return `'''
|
|
1764
|
-
${value.replace(/\r\n/g, "\n").trim()}
|
|
1765
|
-
'''`;
|
|
1766
|
-
}
|
|
1767
|
-
function stdioMcpSection(server) {
|
|
1768
|
-
const args = (server.args ?? []).map(tomlString).join(", ");
|
|
1769
|
-
return `command = ${tomlString(server.command)}
|
|
1770
|
-
args = [${args}]`;
|
|
1771
|
-
}
|
|
1772
|
-
function isManagedOptionalStdioServer3(server, section) {
|
|
1773
|
-
return server.optional === true && server.transport === "stdio" && section?.trim() === stdioMcpSection(server);
|
|
1774
|
-
}
|
|
1775
|
-
function hasActiveEngramPlugin(configDir) {
|
|
1776
|
-
const config = readTextIfExists(path11.join(configDir, "config.toml"));
|
|
1777
|
-
if (config === null) return false;
|
|
1778
|
-
const match = /\[plugins\."engram@[^"]*"\]([^[]*)/.exec(config);
|
|
1779
|
-
return match !== null && !/enabled\s*=\s*false/.test(match[1]);
|
|
1780
|
-
}
|
|
1781
|
-
function hasEngramProtocol(configDir) {
|
|
1782
|
-
if (hasActiveEngramPlugin(configDir)) return true;
|
|
1783
|
-
if (fs8.existsSync(path11.join(configDir, "engram-instructions.md"))) return true;
|
|
1784
|
-
const config = readTextIfExists(path11.join(configDir, "config.toml"));
|
|
1785
|
-
return config !== null && /engram-instructions\.md/.test(config);
|
|
1786
|
-
}
|
|
1787
|
-
var CODEX_JSON_STRING = String.raw`"(?:\\.|[^"\\\r\n])*"`;
|
|
1788
|
-
var CODEX_JSON_VALUE = String.raw`(?:${CODEX_JSON_STRING}|-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?|true|false)`;
|
|
1789
|
-
var CODEX_JSON_STRING_ARRAY = String.raw`\[(?:\s*${CODEX_JSON_STRING}(?:\s*,\s*${CODEX_JSON_STRING})*\s*)?\]`;
|
|
1790
|
-
var CODEX_KEY = String.raw`(?:[A-Za-z0-9_-]+|${CODEX_JSON_STRING})`;
|
|
1791
|
-
var CODEX_ASSIGNMENT = new RegExp(String.raw`^\s*(${CODEX_KEY})\s*=\s*(${CODEX_JSON_STRING_ARRAY}|${CODEX_JSON_VALUE})\s*(?:#.*)?$`);
|
|
1792
|
-
var CODEX_HEADER = new RegExp(String.raw`^\[\s*(${CODEX_KEY}(?:\s*\.\s*${CODEX_KEY})*)\s*\]\s*(?:#.*)?$`);
|
|
1793
|
-
function tomlAssignment(section, key) {
|
|
1794
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1795
|
-
const keyPattern = new RegExp(`^\\s*(?:${escaped}|"${escaped}"|'${escaped}')\\s*=`);
|
|
1796
|
-
const lines = section.split(/\r?\n/);
|
|
1797
|
-
const mask = multilineStringMask(lines);
|
|
1798
|
-
for (const [index, line] of lines.entries()) {
|
|
1799
|
-
if (mask[index] || !keyPattern.test(line)) continue;
|
|
1800
|
-
const match = CODEX_ASSIGNMENT.exec(line);
|
|
1801
|
-
if (match) return { present: true, raw: match[2] };
|
|
1802
|
-
const equals = line.indexOf("=");
|
|
1803
|
-
const raw = equals === -1 ? void 0 : line.slice(equals + 1).trim().replace(/\s+#.*$/, "");
|
|
1804
|
-
return { present: true, ...raw?.startsWith("'") && raw.endsWith("'") ? { raw } : {} };
|
|
1805
|
-
}
|
|
1806
|
-
return { present: false };
|
|
1807
|
-
}
|
|
1808
|
-
function parseTomlString(raw) {
|
|
1809
|
-
if (raw === void 0) return void 0;
|
|
1810
|
-
const value = raw.trim();
|
|
1811
|
-
if (value.startsWith('"')) {
|
|
1812
|
-
try {
|
|
1813
|
-
const parsed = JSON.parse(value);
|
|
1814
|
-
return typeof parsed === "string" ? parsed : void 0;
|
|
1815
|
-
} catch {
|
|
1816
|
-
return void 0;
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
1820
|
-
return value.slice(1, -1);
|
|
1821
|
-
}
|
|
1822
|
-
return void 0;
|
|
1823
|
-
}
|
|
1824
|
-
function context7HttpSection(server) {
|
|
1825
|
-
const body = [`url = ${tomlString(server.url)}`];
|
|
1826
|
-
const envHeaders = [];
|
|
1827
|
-
const literalHeaders = [];
|
|
1828
|
-
for (const [key, raw] of Object.entries(server.headers ?? {})) {
|
|
1829
|
-
const envRef = /^\$\{(\w+)\}$/.exec(raw);
|
|
1830
|
-
if (envRef) envHeaders.push(`${tomlString(key)} = ${tomlString(envRef[1])}`);
|
|
1831
|
-
else literalHeaders.push(`${tomlString(key)} = ${tomlString(raw)}`);
|
|
1832
|
-
}
|
|
1833
|
-
if (literalHeaders.length > 0) body.push(`http_headers = { ${literalHeaders.join(", ")} }`);
|
|
1834
|
-
if (envHeaders.length > 0) body.push(`env_http_headers = { ${envHeaders.join(", ")} }`);
|
|
1835
|
-
return body.join("\n");
|
|
1836
|
-
}
|
|
1837
|
-
function isCompatibleContext7Server3(server, section) {
|
|
1838
|
-
if (server.transport !== "http" || typeof server.url !== "string" || section === null) return false;
|
|
1839
|
-
const url = tomlAssignment(section, "url");
|
|
1840
|
-
const command = tomlAssignment(section, "command");
|
|
1841
|
-
const type = tomlAssignment(section, "type");
|
|
1842
|
-
const enabled = tomlAssignment(section, "enabled");
|
|
1843
|
-
const declaredType = type.present ? parseTomlString(type.raw) : "http";
|
|
1844
|
-
return url.present && parseTomlString(url.raw) === server.url && !command.present && declaredType === "http" && (!enabled.present || enabled.raw?.trim() === "true");
|
|
1845
|
-
}
|
|
1846
|
-
function isCanonicalContext7Server3(server, section, config) {
|
|
1847
|
-
return isCompatibleContext7Server3(server, section) && section.trim() === context7HttpSection(server) && !hasTomlChildSection(config, "mcp_servers.context7");
|
|
1848
|
-
}
|
|
1849
|
-
function assertCompatibleContext73(server, section, config) {
|
|
1850
|
-
if (section === null && hasTomlChildSection(config, "mcp_servers.context7") || section !== null && !isCompatibleContext7Server3(server, section)) {
|
|
1851
|
-
throw new Error("Codex: MCP 'context7' entra en conflicto con una definici\xF3n existente (endpoint, tipo o estado nativo incompatible). Conserva la configuraci\xF3n y corrige el conflicto antes de reintentar.");
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
var CODEX_PERMISSION_HEADERS = {
|
|
1855
|
-
base: "permissions.jorgex-read-anywhere",
|
|
1856
|
-
filesystem: "permissions.jorgex-read-anywhere.filesystem",
|
|
1857
|
-
workspaceRoots: 'permissions.jorgex-read-anywhere.filesystem.":workspace_roots"'
|
|
1858
|
-
};
|
|
1859
|
-
var CODEX_PERMISSION_PROFILE = {
|
|
1860
|
-
base: [["extends", ":workspace"]],
|
|
1861
|
-
filesystem: [
|
|
1862
|
-
[":root", "read"],
|
|
1863
|
-
["*.env", "deny"],
|
|
1864
|
-
["*.env.*", "deny"],
|
|
1865
|
-
["~/.ssh/**", "deny"],
|
|
1866
|
-
["~/.aws/credentials", "deny"],
|
|
1867
|
-
["~/.npmrc", "deny"],
|
|
1868
|
-
["~/.git-credentials", "deny"],
|
|
1869
|
-
["**/id_rsa", "deny"],
|
|
1870
|
-
["**/id_ed25519", "deny"],
|
|
1871
|
-
["**/*.pem", "deny"],
|
|
1872
|
-
["**/*.key", "deny"]
|
|
1873
|
-
],
|
|
1874
|
-
workspaceRoots: [
|
|
1875
|
-
[".", "write"],
|
|
1876
|
-
["*.env", "deny"],
|
|
1877
|
-
["*.env.*", "deny"],
|
|
1878
|
-
[".ssh/**", "deny"],
|
|
1879
|
-
[".aws/credentials", "deny"],
|
|
1880
|
-
[".npmrc", "deny"],
|
|
1881
|
-
[".git-credentials", "deny"],
|
|
1882
|
-
["**/id_rsa", "deny"],
|
|
1883
|
-
["**/id_ed25519", "deny"],
|
|
1884
|
-
["**/*.pem", "deny"],
|
|
1885
|
-
["**/*.key", "deny"]
|
|
1886
|
-
]
|
|
1887
|
-
};
|
|
1888
|
-
var CODEX_PERMISSION_SECTIONS = [
|
|
1889
|
-
{ header: CODEX_PERMISSION_HEADERS.base, entries: CODEX_PERMISSION_PROFILE.base, quoteKeys: false },
|
|
1890
|
-
{ header: CODEX_PERMISSION_HEADERS.filesystem, entries: CODEX_PERMISSION_PROFILE.filesystem, quoteKeys: true },
|
|
1891
|
-
{ header: CODEX_PERMISSION_HEADERS.workspaceRoots, entries: CODEX_PERMISSION_PROFILE.workspaceRoots, quoteKeys: true }
|
|
1892
|
-
];
|
|
1893
|
-
function renderCodexPermissionEntries(entries, quoteKeys) {
|
|
1894
|
-
return entries.map(([key, value]) => `${quoteKeys ? JSON.stringify(key) : key} = ${JSON.stringify(value)}`).join("\n");
|
|
1895
|
-
}
|
|
1896
|
-
var CODEX_STALE_PERMISSIONS_WARNING = "Codex: permission profile differs from the stack default and was left untouched; re-run with --upgrade-permissions to replace it (a backup is created first), or edit it by hand. Overwriting discards your own permission changes, including any extra hardenings.";
|
|
1897
|
-
function codexNormalizedHeader(header) {
|
|
1898
|
-
return headerName(`[${header}]`) ?? header;
|
|
1899
|
-
}
|
|
1900
|
-
function readCodexRootValue(config, key) {
|
|
1901
|
-
const lines = config.replace(/\r\n/g, "\n").split("\n");
|
|
1902
|
-
const mask = multilineStringMask(lines);
|
|
1903
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1904
|
-
const keyPattern = new RegExp(`^\\s*(?:${escaped}|"${escaped}"|'${escaped}')\\s*=`);
|
|
1905
|
-
for (const [index, line] of lines.entries()) {
|
|
1906
|
-
if (mask[index]) continue;
|
|
1907
|
-
if (line.trim().startsWith("[")) break;
|
|
1908
|
-
if (!keyPattern.test(line)) continue;
|
|
1909
|
-
const match = CODEX_ASSIGNMENT.exec(line.trim());
|
|
1910
|
-
return match === null ? void 0 : parseTomlString(match[2]);
|
|
1911
|
-
}
|
|
1912
|
-
return void 0;
|
|
1913
|
-
}
|
|
1914
|
-
function setCodexRootKey(config, key, value) {
|
|
1915
|
-
const eol = config.includes("\r\n") ? "\r\n" : "\n";
|
|
1916
|
-
const lines = config.replace(/\r\n/g, "\n").split("\n");
|
|
1917
|
-
const mask = multilineStringMask(lines);
|
|
1918
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1919
|
-
const keyPattern = new RegExp(`^\\s*(?:${escaped}|"${escaped}"|'${escaped}')\\s*=`);
|
|
1920
|
-
const assignment = new RegExp(
|
|
1921
|
-
`^(\\s*(?:${escaped}|"${escaped}"|'${escaped}')\\s*=\\s*)(${CODEX_JSON_STRING_ARRAY}|${CODEX_JSON_VALUE})(\\s*(?:#.*)?)$`
|
|
1922
|
-
);
|
|
1923
|
-
for (const [index, line] of lines.entries()) {
|
|
1924
|
-
if (mask[index]) continue;
|
|
1925
|
-
if (line.trim().startsWith("[")) break;
|
|
1926
|
-
if (!keyPattern.test(line)) continue;
|
|
1927
|
-
const match = assignment.exec(line);
|
|
1928
|
-
lines[index] = match === null ? `${key} = ${value}` : `${match[1]}${value}${match[3]}`;
|
|
1929
|
-
return lines.join(eol);
|
|
1930
|
-
}
|
|
1931
|
-
return upsertTomlRootKeyIfMissing(config, key, value);
|
|
1932
|
-
}
|
|
1933
|
-
function isCodexPermissionBlockCurrent(config, defaults) {
|
|
1934
|
-
const expectedApproval = defaults["approval_policy"];
|
|
1935
|
-
const expectedDefault = defaults["default_permissions"];
|
|
1936
|
-
if (typeof expectedApproval !== "string" || typeof expectedDefault !== "string") return true;
|
|
1937
|
-
if (readCodexRootValue(config, "approval_policy") !== expectedApproval) return false;
|
|
1938
|
-
if (readCodexRootValue(config, "default_permissions") !== expectedDefault) return false;
|
|
1939
|
-
return CODEX_PERMISSION_SECTIONS.every(
|
|
1940
|
-
({ header, entries, quoteKeys }) => (readTomlSection(config, codexNormalizedHeader(header)) ?? "").trim() === renderCodexPermissionEntries(entries, quoteKeys).trim()
|
|
1941
|
-
);
|
|
1942
|
-
}
|
|
1943
|
-
function reseedCodexPermissionBlock(config, defaults) {
|
|
1944
|
-
let out = setCodexRootKey(config, "approval_policy", tomlString(String(defaults["approval_policy"])));
|
|
1945
|
-
out = setCodexRootKey(out, "default_permissions", tomlString(String(defaults["default_permissions"])));
|
|
1946
|
-
for (const { header, entries, quoteKeys } of CODEX_PERMISSION_SECTIONS) {
|
|
1947
|
-
out = upsertTomlSection(out, header, renderCodexPermissionEntries(entries, quoteKeys));
|
|
1948
|
-
}
|
|
1949
|
-
return out;
|
|
1950
|
-
}
|
|
1951
|
-
function parseCodexKey(raw) {
|
|
1952
|
-
if (!raw.startsWith('"')) return raw;
|
|
1953
|
-
try {
|
|
1954
|
-
const value = JSON.parse(raw);
|
|
1955
|
-
return typeof value === "string" ? value : null;
|
|
1956
|
-
} catch {
|
|
1957
|
-
return null;
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
function parseCodexValue(raw) {
|
|
1961
|
-
try {
|
|
1962
|
-
const value = JSON.parse(raw);
|
|
1963
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string") || !Array.isArray(value) && (value === null || typeof value !== "object") ? value : void 0;
|
|
1964
|
-
} catch {
|
|
1965
|
-
return void 0;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
function parseCodexHeader(line) {
|
|
1969
|
-
const match = CODEX_HEADER.exec(line.trim());
|
|
1970
|
-
if (match === null) return null;
|
|
1971
|
-
for (const quoted of match[1].match(/"(?:\\.|[^"\\\r\n])*"/g) ?? []) {
|
|
1972
|
-
if (parseCodexKey(quoted) === null) return null;
|
|
1973
|
-
}
|
|
1974
|
-
return match[1].replace(/\s*\.\s*/g, ".");
|
|
1975
|
-
}
|
|
1976
|
-
function scanCodexToml(config) {
|
|
1977
|
-
if (config.includes("'''") || config.includes('"""')) return null;
|
|
1978
|
-
const root = /* @__PURE__ */ new Map();
|
|
1979
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1980
|
-
const headers = /* @__PURE__ */ new Set();
|
|
1981
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1982
|
-
let section = null;
|
|
1983
|
-
for (const line of config.replace(/\r\n/g, "\n").split("\n")) {
|
|
1984
|
-
const trimmed = line.trim();
|
|
1985
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1986
|
-
if (trimmed.startsWith("[")) {
|
|
1987
|
-
const header = parseCodexHeader(trimmed);
|
|
1988
|
-
if (header === null || headers.has(header)) return null;
|
|
1989
|
-
headers.add(header);
|
|
1990
|
-
if (header.startsWith(`${CODEX_PERMISSION_HEADERS.base}.`) && !CODEX_PERMISSION_SECTIONS.some((entry) => entry.header === header)) {
|
|
1991
|
-
return null;
|
|
1992
|
-
}
|
|
1993
|
-
section = header;
|
|
1994
|
-
if (CODEX_PERMISSION_SECTIONS.some((entry) => entry.header === header)) sections.set(header, /* @__PURE__ */ new Map());
|
|
1995
|
-
continue;
|
|
1996
|
-
}
|
|
1997
|
-
const relevant = section === null || CODEX_PERMISSION_SECTIONS.some((entry) => entry.header === section);
|
|
1998
|
-
const match = CODEX_ASSIGNMENT.exec(trimmed);
|
|
1999
|
-
if (!relevant && match === null) continue;
|
|
2000
|
-
if (match === null) return null;
|
|
2001
|
-
const key = parseCodexKey(match[1]);
|
|
2002
|
-
const value = parseCodexValue(match[2]);
|
|
2003
|
-
if (key === null || value === void 0) return null;
|
|
2004
|
-
if (section === null && (key === "profile" || key === "sandbox_mode")) return null;
|
|
2005
|
-
const scope = section ?? "<root>";
|
|
2006
|
-
const declaration = `${scope}\0${key}`;
|
|
2007
|
-
if (keys.has(declaration)) return null;
|
|
2008
|
-
keys.add(declaration);
|
|
2009
|
-
if (section === null) root.set(key, value);
|
|
2010
|
-
else if (sections.has(section)) sections.get(section).set(key, value);
|
|
2011
|
-
}
|
|
2012
|
-
return { root, sections };
|
|
2013
|
-
}
|
|
2014
|
-
function hasCanonicalCodexPermissionProfile(scan) {
|
|
2015
|
-
for (const { header, entries } of CODEX_PERMISSION_SECTIONS) {
|
|
2016
|
-
const actual = scan.sections.get(header);
|
|
2017
|
-
if (actual === void 0 || actual.size !== entries.length) return false;
|
|
2018
|
-
for (const [key, expected] of entries) {
|
|
2019
|
-
if (actual.get(key) !== expected) return false;
|
|
149
|
+
throw new Error(`Marcadores de estilo ambiguos o desconocidos en ${sourcePath}; revisa el bloque ${DEFAULT_SECTION}.`);
|
|
2020
150
|
}
|
|
2021
151
|
}
|
|
2022
|
-
|
|
152
|
+
const installedContent = upsertMarkdownSection(originalContent, DEFAULT_SECTION, canonical);
|
|
153
|
+
if (originalContent !== null && originalContent !== installedContent) validateBackupRoot(options.rootDir);
|
|
154
|
+
const content = installedContent.replace(DEFAULT_OPEN, "").replace(DEFAULT_CLOSE, "").replace(/\r\n?/g, "\n").trim();
|
|
155
|
+
return {
|
|
156
|
+
sourcePath,
|
|
157
|
+
content,
|
|
158
|
+
canonicalPath,
|
|
159
|
+
originalContent,
|
|
160
|
+
installedContent,
|
|
161
|
+
rootDir: options.rootDir,
|
|
162
|
+
backupRoot: options.rootDir === void 0 ? void 0 : path.join(options.rootDir, "backups")
|
|
163
|
+
};
|
|
2023
164
|
}
|
|
2024
|
-
function
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
if (
|
|
2029
|
-
|
|
2030
|
-
const defaults = loadCanonicalDefaults(stackRoot())["codex"];
|
|
2031
|
-
const expectedApproval = defaults?.["approval_policy"];
|
|
2032
|
-
const expectedPermissions = defaults?.["default_permissions"];
|
|
2033
|
-
return typeof expectedApproval === "string" && typeof expectedPermissions === "string" && scan.root.get("approval_policy") === expectedApproval && scan.root.get("default_permissions") === expectedPermissions && hasCanonicalCodexPermissionProfile(scan);
|
|
2034
|
-
} catch {
|
|
2035
|
-
return false;
|
|
165
|
+
function applyWritingStyle(plan, dryRun = false) {
|
|
166
|
+
if (dryRun) return;
|
|
167
|
+
const current = readStyleText(plan.sourcePath, { rootDir: plan.rootDir });
|
|
168
|
+
if (current === plan.installedContent) return;
|
|
169
|
+
if (current !== plan.originalContent) {
|
|
170
|
+
throw new Error(`El estilo ${plan.sourcePath} cambi\xF3 durante la preparaci\xF3n; vuelve a ejecutar el comando.`);
|
|
2036
171
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
name: "Codex CLI",
|
|
2041
|
-
detect: detectCodex,
|
|
2042
|
-
reportCapabilities(configDir) {
|
|
2043
|
-
const prompt = readTextIfExists(path11.join(configDir, "AGENTS.md"));
|
|
2044
|
-
return createLocalCapabilityReport("codex", [
|
|
2045
|
-
...hasManagedMarkdownSection(prompt, "system-prompt") ? [{
|
|
2046
|
-
id: "policy-guidance",
|
|
2047
|
-
state: "prompt-only",
|
|
2048
|
-
reason: "The managed policy prompt is advisory and cannot enforce the policy",
|
|
2049
|
-
evidence: { source: "jorgex-stack-system-prompt", version: "1" }
|
|
2050
|
-
}] : [],
|
|
2051
|
-
...hasCodexManualApproval(configDir) ? [{
|
|
2052
|
-
id: "tool-approval",
|
|
2053
|
-
state: "manual",
|
|
2054
|
-
reason: "Canonical approval declarations require a human decision; runtime activation is not certified",
|
|
2055
|
-
evidence: { source: "jorgex-stack-codex-approval-policy", version: "1" }
|
|
2056
|
-
}] : [],
|
|
2057
|
-
{
|
|
2058
|
-
id: "external-verification",
|
|
2059
|
-
state: "unavailable",
|
|
2060
|
-
reason: "External verification is available only through the external verifier"
|
|
2061
|
-
}
|
|
2062
|
-
]);
|
|
2063
|
-
},
|
|
2064
|
-
injectEngramProtocol(ctx) {
|
|
2065
|
-
return !hasEngramProtocol(ctx.configDir);
|
|
2066
|
-
},
|
|
2067
|
-
paths(configDir) {
|
|
2068
|
-
const isRealConfigDir = samePath(configDir, process.env.CODEX_HOME ?? path11.join(HOME, ".codex"));
|
|
2069
|
-
const agentsHome = isRealConfigDir ? HOME : path11.dirname(configDir);
|
|
2070
|
-
const skillsDir = path11.join(agentsHome, ".agents", "skills");
|
|
2071
|
-
return {
|
|
2072
|
-
systemPromptFile: path11.join(configDir, "AGENTS.md"),
|
|
2073
|
-
agentsDir: path11.join(configDir, "agents"),
|
|
2074
|
-
skillsDir,
|
|
2075
|
-
// Los custom prompts de Codex están deprecados: los commands se
|
|
2076
|
-
// instalan como skills (renderCommand produce <nombre>/SKILL.md).
|
|
2077
|
-
commandsDir: skillsDir,
|
|
2078
|
-
pluginsDir: null,
|
|
2079
|
-
scriptsDir: path11.join(configDir, "scripts"),
|
|
2080
|
-
outputStylesDir: null,
|
|
2081
|
-
profilesDir: configDir
|
|
2082
|
-
};
|
|
2083
|
-
},
|
|
2084
|
-
renderAgent(agent, models) {
|
|
2085
|
-
const tierModel = resolveAgentModel(models, agent.name, agent.tier);
|
|
2086
|
-
if (agent.mode === "primary") {
|
|
2087
|
-
const profileLines = [
|
|
2088
|
-
"# Managed by jorgex-stack \u2014 modo orquestador del agente principal.",
|
|
2089
|
-
`# Uso: codex --profile ${agent.name}`,
|
|
2090
|
-
`developer_instructions = ${tomlMultiline(agent.body)}`
|
|
2091
|
-
];
|
|
2092
|
-
return [{ file: `${agent.name}.config.toml`, content: profileLines.join("\n") + "\n", kind: "profile" }];
|
|
2093
|
-
}
|
|
2094
|
-
const lines = [
|
|
2095
|
-
`name = ${tomlString(agent.name)}`,
|
|
2096
|
-
`description = ${tomlString(agent.description)}`
|
|
2097
|
-
];
|
|
2098
|
-
if (tierModel.model !== "default") lines.push(`model = ${tomlString(tierModel.model)}`);
|
|
2099
|
-
if (tierModel.variant) lines.push(`model_reasoning_effort = ${tomlString(tierModel.variant)}`);
|
|
2100
|
-
lines.push(`sandbox_mode = ${tomlString(agent.readonly ? "read-only" : "workspace-write")}`);
|
|
2101
|
-
lines.push(`developer_instructions = ${tomlMultiline(agent.body)}`);
|
|
2102
|
-
return [{ file: `${agent.name}.toml`, content: lines.join("\n") + "\n", kind: "agent" }];
|
|
2103
|
-
},
|
|
2104
|
-
renderCommand(file, content) {
|
|
2105
|
-
const name = file.replace(/\.md$/, "");
|
|
2106
|
-
let description = name;
|
|
2107
|
-
let body = content;
|
|
2108
|
-
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(content);
|
|
2109
|
-
if (fm) {
|
|
2110
|
-
const match = /(?:^|\n)description:\s*(.+)/.exec(fm[1]);
|
|
2111
|
-
if (match) description = match[1].trim();
|
|
2112
|
-
body = content.slice(fm[0].length);
|
|
2113
|
-
}
|
|
2114
|
-
body = body.replace(/\{\{input\}\}/g, "the user's request in this conversation");
|
|
2115
|
-
return {
|
|
2116
|
-
file: `${name}/SKILL.md`,
|
|
2117
|
-
content: `---
|
|
2118
|
-
name: ${name}
|
|
2119
|
-
description: ${JSON.stringify(description)}
|
|
2120
|
-
---
|
|
2121
|
-
${body}`
|
|
2122
|
-
};
|
|
2123
|
-
},
|
|
2124
|
-
planHooks(canonical, ctx) {
|
|
2125
|
-
const actions = [];
|
|
2126
|
-
const { scriptsDir } = this.paths(ctx.configDir);
|
|
2127
|
-
const hooksFile = path11.join(ctx.configDir, "hooks.json");
|
|
2128
|
-
const content = upsertNativeHooks(
|
|
2129
|
-
readTextIfExists(hooksFile),
|
|
2130
|
-
canonical,
|
|
2131
|
-
scriptsDir,
|
|
2132
|
-
(matcher) => matcher.split("|").some((p7) => p7.trim() === "Bash") ? "shell" : matcher
|
|
2133
|
-
);
|
|
2134
|
-
actions.push({ kind: "write", target: hooksFile, content });
|
|
2135
|
-
ctx.warnings.push(
|
|
2136
|
-
"Codex: los hooks no-managed requieren aprobaci\xF3n manual \u2014 ejecuta /hooks dentro de codex para activarlos."
|
|
2137
|
-
);
|
|
2138
|
-
const scriptsSource = path11.join(ctx.stackDir, "scripts");
|
|
2139
|
-
if (fs8.existsSync(scriptsSource)) {
|
|
2140
|
-
for (const f of fs8.readdirSync(scriptsSource)) {
|
|
2141
|
-
actions.push({ kind: "copy", source: path11.join(scriptsSource, f), target: path11.join(scriptsDir, f) });
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
return actions;
|
|
2145
|
-
},
|
|
2146
|
-
planMainConfig(canonical, ctx) {
|
|
2147
|
-
const file = path11.join(ctx.configDir, "config.toml");
|
|
2148
|
-
const original = readMcpConfig3(file);
|
|
2149
|
-
const contentSource = original === null || original.trim() === "" ? null : original;
|
|
2150
|
-
let content = contentSource;
|
|
2151
|
-
const mcpOwnership = [];
|
|
2152
|
-
const primaryModelOwnership = [];
|
|
2153
|
-
const context7 = canonical.servers.context7;
|
|
2154
|
-
if (context7 !== void 0) {
|
|
2155
|
-
assertCompatibleContext73(context7, readTomlSection(contentSource, "mcp_servers.context7"), contentSource);
|
|
2156
|
-
}
|
|
2157
|
-
if (contentSource === null) {
|
|
2158
|
-
const defaults = loadCanonicalDefaults(ctx.stackDir)["codex"] ?? {};
|
|
2159
|
-
for (const [key, value] of Object.entries(defaults)) {
|
|
2160
|
-
const line = `${key} = ${tomlString(String(value))}
|
|
2161
|
-
`;
|
|
2162
|
-
content = content === null ? line : line + content;
|
|
2163
|
-
}
|
|
2164
|
-
ctx.warnings.push(
|
|
2165
|
-
"Codex: fresh config enables read-anywhere via the jorgex-read-anywhere permission profile; broad local reads can expose secrets not covered by deny rules."
|
|
2166
|
-
);
|
|
2167
|
-
content = upsertTomlSection(
|
|
2168
|
-
content,
|
|
2169
|
-
CODEX_PERMISSION_HEADERS.base,
|
|
2170
|
-
renderCodexPermissionEntries(CODEX_PERMISSION_PROFILE.base, false)
|
|
2171
|
-
);
|
|
2172
|
-
content = upsertTomlSection(
|
|
2173
|
-
content,
|
|
2174
|
-
CODEX_PERMISSION_HEADERS.filesystem,
|
|
2175
|
-
renderCodexPermissionEntries(CODEX_PERMISSION_PROFILE.filesystem, true)
|
|
2176
|
-
);
|
|
2177
|
-
content += [
|
|
2178
|
-
`
|
|
2179
|
-
[${CODEX_PERMISSION_HEADERS.workspaceRoots}]`,
|
|
2180
|
-
renderCodexPermissionEntries(CODEX_PERMISSION_PROFILE.workspaceRoots, true),
|
|
2181
|
-
""
|
|
2182
|
-
].join("\n");
|
|
2183
|
-
}
|
|
2184
|
-
if (contentSource !== null) {
|
|
2185
|
-
const codexDefaults = loadCanonicalDefaults(ctx.stackDir)["codex"];
|
|
2186
|
-
if (codexDefaults !== void 0 && !isCodexPermissionBlockCurrent(contentSource, codexDefaults)) {
|
|
2187
|
-
if (ctx.upgradePermissions === true) {
|
|
2188
|
-
content = reseedCodexPermissionBlock(content, codexDefaults);
|
|
2189
|
-
} else {
|
|
2190
|
-
ctx.warnings.push(CODEX_STALE_PERMISSIONS_WARNING);
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
if (!hasTomlRootKey(content, PRIMARY_MODEL_FIELD2)) {
|
|
2195
|
-
content = upsertTomlRootKeyIfMissing(content, PRIMARY_MODEL_FIELD2, PRIMARY_MODEL2);
|
|
2196
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_MODEL_FIELD2) !== true) {
|
|
2197
|
-
primaryModelOwnership.push({ field: PRIMARY_MODEL_FIELD2, owned: true });
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
if (!hasTomlRootKey(content, PRIMARY_CONTEXT_FIELD)) {
|
|
2201
|
-
content = upsertTomlRootKeyIfMissing(content, PRIMARY_CONTEXT_FIELD, PRIMARY_CONTEXT_WINDOW);
|
|
2202
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_CONTEXT_FIELD) !== true) {
|
|
2203
|
-
primaryModelOwnership.push({ field: PRIMARY_CONTEXT_FIELD, owned: true });
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
for (const [name, server] of Object.entries(canonical.servers)) {
|
|
2207
|
-
const section = `mcp_servers.${name}`;
|
|
2208
|
-
const existing = readTomlSection(content, section);
|
|
2209
|
-
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
2210
|
-
if (name === "context7" && existing !== null) {
|
|
2211
|
-
if (owned && !isCanonicalContext7Server3(server, existing, content)) {
|
|
2212
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
2213
|
-
}
|
|
2214
|
-
continue;
|
|
2215
|
-
}
|
|
2216
|
-
if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
|
|
2217
|
-
if (owned) {
|
|
2218
|
-
if (isManagedOptionalStdioServer3(server, existing)) content = removeTomlSection(content, section);
|
|
2219
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
2220
|
-
}
|
|
2221
|
-
continue;
|
|
2222
|
-
}
|
|
2223
|
-
if (server.optional && existing !== null) {
|
|
2224
|
-
if (!owned || !isManagedOptionalStdioServer3(server, existing)) {
|
|
2225
|
-
if (owned) mcpOwnership.push({ server: name, owned: false });
|
|
2226
|
-
ctx.warnings.push(`Codex: MCP opcional '${name}' ya pertenece a la configuraci\xF3n del usuario; se conserva.`);
|
|
2227
|
-
continue;
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
if (server.transport === "stdio") {
|
|
2231
|
-
if (server.command === "{{ENGRAM_BIN}}" && hasActiveEngramPlugin(ctx.configDir)) {
|
|
2232
|
-
if (content !== null) content = removeTomlSection(content, section);
|
|
2233
|
-
ctx.warnings.push(
|
|
2234
|
-
"Codex: Engram ya est\xE1 integrado v\xEDa plugin de marketplace \u2014 no se registra el MCP para no duplicar las tools de memoria."
|
|
2235
|
-
);
|
|
2236
|
-
continue;
|
|
2237
|
-
}
|
|
2238
|
-
if (server.command === "{{ENGRAM_BIN}}" && ctx.engramBin === null) {
|
|
2239
|
-
ctx.warnings.push(
|
|
2240
|
-
"Engram no detectado: el MCP 'engram' no se registra. Inst\xE1lalo (github.com/Gentleman-Programming/engram) y re-ejecuta sync."
|
|
2241
|
-
);
|
|
2242
|
-
continue;
|
|
2243
|
-
}
|
|
2244
|
-
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
2245
|
-
content = upsertTomlSection(content, section, `command = ${tomlString(command)}
|
|
2246
|
-
args = [${(server.args ?? []).map(tomlString).join(", ")}]`);
|
|
2247
|
-
if (server.optional && existing === null && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
2248
|
-
} else {
|
|
2249
|
-
const previousSection = readTomlSection(content, section);
|
|
2250
|
-
const prevUsesEnvHeaders = previousSection?.includes("env_http_headers") ?? false;
|
|
2251
|
-
const literalPairs = [];
|
|
2252
|
-
const refPairs = [];
|
|
2253
|
-
for (const [key, raw] of Object.entries(server.headers ?? {})) {
|
|
2254
|
-
const envRef = /^\$\{(\w+)\}$/.exec(raw);
|
|
2255
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2256
|
-
const previousValue = !prevUsesEnvHeaders && previousSection !== null ? new RegExp(`(?:^|[{,]\\s*)"?${escaped}"?\\s*=\\s*"([^"]*)"`, "m").exec(previousSection)?.[1] : void 0;
|
|
2257
|
-
if (previousValue) literalPairs.push(`${tomlString(key)} = ${tomlString(previousValue)}`);
|
|
2258
|
-
else refPairs.push(`${tomlString(key)} = ${tomlString(envRef ? envRef[1] : raw)}`);
|
|
2259
|
-
}
|
|
2260
|
-
const body = [`url = ${tomlString(server.url)}`];
|
|
2261
|
-
if (literalPairs.length > 0) body.push(`http_headers = { ${literalPairs.join(", ")} }`);
|
|
2262
|
-
if (refPairs.length > 0) body.push(`env_http_headers = { ${refPairs.join(", ")} }`);
|
|
2263
|
-
content = upsertTomlSection(content, section, body.join("\n"));
|
|
2264
|
-
if (name === "context7" && existing === null && !owned) {
|
|
2265
|
-
mcpOwnership.push({ server: name, owned: true });
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
if (content === null) return [];
|
|
2270
|
-
if (!content.endsWith("\n")) content += "\n";
|
|
2271
|
-
return [{
|
|
2272
|
-
kind: "write",
|
|
2273
|
-
target: file,
|
|
2274
|
-
content,
|
|
2275
|
-
...mcpOwnership.length > 0 ? { mcpOwnership } : {},
|
|
2276
|
-
...primaryModelOwnership.length > 0 ? { primaryModelOwnership } : {}
|
|
2277
|
-
}];
|
|
2278
|
-
},
|
|
2279
|
-
planUnmerge(mcp, hooks, ctx) {
|
|
2280
|
-
const actions = [];
|
|
2281
|
-
const { systemPromptFile } = this.paths(ctx.configDir);
|
|
2282
|
-
const prompt = readTextIfExists(systemPromptFile);
|
|
2283
|
-
if (prompt !== null) {
|
|
2284
|
-
const content = removeSystemPromptSections(prompt);
|
|
2285
|
-
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
2286
|
-
}
|
|
2287
|
-
const configFile = path11.join(ctx.configDir, "config.toml");
|
|
2288
|
-
const config = readMcpConfig3(configFile);
|
|
2289
|
-
if (config !== null) {
|
|
2290
|
-
let content = config;
|
|
2291
|
-
const primaryModelOwnership = [];
|
|
2292
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_MODEL_FIELD2) === true) {
|
|
2293
|
-
content = removeTomlRootKeyIfExact(content, PRIMARY_MODEL_FIELD2, PRIMARY_MODEL2);
|
|
2294
|
-
primaryModelOwnership.push({ field: PRIMARY_MODEL_FIELD2, owned: false });
|
|
2295
|
-
}
|
|
2296
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_CONTEXT_FIELD) === true) {
|
|
2297
|
-
content = removeTomlRootKeyIfExact(content, PRIMARY_CONTEXT_FIELD, PRIMARY_CONTEXT_WINDOW);
|
|
2298
|
-
primaryModelOwnership.push({ field: PRIMARY_CONTEXT_FIELD, owned: false });
|
|
2299
|
-
}
|
|
2300
|
-
const mcpOwnership = [];
|
|
2301
|
-
for (const [name, server] of Object.entries(mcp.servers)) {
|
|
2302
|
-
const section = `mcp_servers.${name}`;
|
|
2303
|
-
if (name === "context7") {
|
|
2304
|
-
const current = readTomlSection(content, section);
|
|
2305
|
-
const canonical = isCanonicalContext7Server3(server, current, content);
|
|
2306
|
-
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
2307
|
-
if (owned) {
|
|
2308
|
-
if (canonical) {
|
|
2309
|
-
content = removeTomlSection(content, section);
|
|
2310
|
-
}
|
|
2311
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
2312
|
-
}
|
|
2313
|
-
continue;
|
|
2314
|
-
}
|
|
2315
|
-
if (!server.optional) {
|
|
2316
|
-
content = removeTomlSection(content, section);
|
|
2317
|
-
continue;
|
|
2318
|
-
}
|
|
2319
|
-
if (ctx.ownedMcpServers?.has(name) === true) {
|
|
2320
|
-
if (isManagedOptionalStdioServer3(server, readTomlSection(content, section))) {
|
|
2321
|
-
content = removeTomlSection(content, section);
|
|
2322
|
-
}
|
|
2323
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
actions.push({
|
|
2327
|
-
kind: "write",
|
|
2328
|
-
target: configFile,
|
|
2329
|
-
content,
|
|
2330
|
-
...mcpOwnership.length > 0 ? { mcpOwnership } : {},
|
|
2331
|
-
...primaryModelOwnership.length > 0 ? { primaryModelOwnership } : {}
|
|
2332
|
-
});
|
|
2333
|
-
}
|
|
2334
|
-
const hooksFile = path11.join(ctx.configDir, "hooks.json");
|
|
2335
|
-
const hooksJson = readTextIfExists(hooksFile);
|
|
2336
|
-
if (hooksJson !== null) {
|
|
2337
|
-
const content = removeNativeHooks(hooksJson, hooks);
|
|
2338
|
-
if (content !== null) {
|
|
2339
|
-
actions.push({ kind: "write", target: hooksFile, content: content.trim() === "{}" ? "" : content });
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
return actions;
|
|
172
|
+
if (current !== null) {
|
|
173
|
+
validateBackupRoot(plan.rootDir);
|
|
174
|
+
createBackup([plan.sourcePath], "writing-style", plan.backupRoot);
|
|
2343
175
|
}
|
|
2344
|
-
|
|
176
|
+
const mode = current === null ? 384 : fs.statSync(plan.sourcePath).mode & 511;
|
|
177
|
+
writeText(plan.sourcePath, plan.installedContent, mode);
|
|
178
|
+
}
|
|
2345
179
|
|
|
2346
180
|
// src/lib/install-mode.ts
|
|
2347
|
-
import
|
|
2348
|
-
import
|
|
181
|
+
import fs2 from "fs";
|
|
182
|
+
import path2 from "path";
|
|
2349
183
|
var INSTALL_MODES = /* @__PURE__ */ new Set(["human", "programmatic"]);
|
|
2350
184
|
var SUBAGENT_CONCURRENCIES = /* @__PURE__ */ new Set(["serial", "parallel"]);
|
|
2351
185
|
var DEFAULT_INSTALL_MODE_PREFERENCE = {
|
|
@@ -2353,10 +187,10 @@ var DEFAULT_INSTALL_MODE_PREFERENCE = {
|
|
|
2353
187
|
subagentConcurrency: "serial"
|
|
2354
188
|
};
|
|
2355
189
|
function installModePreferenceFile() {
|
|
2356
|
-
return
|
|
190
|
+
return path2.join(dataDir(), "install-mode.json");
|
|
2357
191
|
}
|
|
2358
192
|
function hasInstallModePreference(file = installModePreferenceFile()) {
|
|
2359
|
-
return
|
|
193
|
+
return fs2.existsSync(file);
|
|
2360
194
|
}
|
|
2361
195
|
function isInstallMode(value) {
|
|
2362
196
|
return INSTALL_MODES.has(value);
|
|
@@ -2387,9 +221,9 @@ function normalizeInstallModePreference(value) {
|
|
|
2387
221
|
throw new Error("Preferencia de instalaci\xF3n inv\xE1lida o corrupta.");
|
|
2388
222
|
}
|
|
2389
223
|
function loadInstallModePreference(file = installModePreferenceFile()) {
|
|
2390
|
-
if (!
|
|
224
|
+
if (!fs2.existsSync(file)) return DEFAULT_INSTALL_MODE_PREFERENCE;
|
|
2391
225
|
try {
|
|
2392
|
-
const raw =
|
|
226
|
+
const raw = fs2.readFileSync(file, "utf8");
|
|
2393
227
|
return normalizeInstallModePreference(JSON.parse(raw));
|
|
2394
228
|
} catch (error) {
|
|
2395
229
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2427,10 +261,10 @@ function parseInstallModePreferenceFlags(mode, subagentConcurrency) {
|
|
|
2427
261
|
}
|
|
2428
262
|
|
|
2429
263
|
// src/lib/manifest.ts
|
|
2430
|
-
import
|
|
2431
|
-
import
|
|
264
|
+
import fs3 from "fs";
|
|
265
|
+
import path3 from "path";
|
|
2432
266
|
function manifestFile() {
|
|
2433
|
-
return
|
|
267
|
+
return path3.join(dataDir(), "manifest.json");
|
|
2434
268
|
}
|
|
2435
269
|
function readManifest(file = manifestFile()) {
|
|
2436
270
|
const raw = readTextIfExists(file);
|
|
@@ -2454,24 +288,24 @@ function removeRuntimeManifest(id, file = manifestFile()) {
|
|
|
2454
288
|
writeText(file, JSON.stringify(manifest, null, 2) + "\n");
|
|
2455
289
|
}
|
|
2456
290
|
function findOrphans(prevOwned, currentTargets, root = HOME) {
|
|
2457
|
-
return prevOwned.map((f) =>
|
|
2458
|
-
(f) => !currentTargets.has(f) &&
|
|
291
|
+
return prevOwned.map((f) => path3.resolve(f)).filter(
|
|
292
|
+
(f) => !currentTargets.has(f) && path3.basename(f) !== "engram.ts" && isContainedIn(f, root) && fs3.existsSync(f)
|
|
2459
293
|
);
|
|
2460
294
|
}
|
|
2461
295
|
|
|
2462
296
|
// src/components/system-prompt.ts
|
|
2463
|
-
import
|
|
2464
|
-
import
|
|
297
|
+
import path5 from "path";
|
|
298
|
+
import fs5 from "fs";
|
|
2465
299
|
|
|
2466
300
|
// src/lib/mode-composition.ts
|
|
2467
|
-
import
|
|
2468
|
-
import
|
|
301
|
+
import fs4 from "fs";
|
|
302
|
+
import path4 from "path";
|
|
2469
303
|
var PROGRAMMATIC_ROOT = ["modes", "programmatic"];
|
|
2470
304
|
var PROGRAMMATIC_MARKER = "<!-- jorgex:programmatic-mode -->";
|
|
2471
305
|
var LEGACY_RESULT_CONTRACT_SECTION = /\n?##\s+Result contract[\s\S]*$/;
|
|
2472
306
|
var normalize = (value) => value.replace(/\r\n/g, "\n");
|
|
2473
307
|
function loadProgrammaticAddendum(stackDir, fileName) {
|
|
2474
|
-
return normalize(
|
|
308
|
+
return normalize(fs4.readFileSync(path4.join(stackDir, ...PROGRAMMATIC_ROOT, fileName), "utf8")).trim();
|
|
2475
309
|
}
|
|
2476
310
|
function appendAddendum(base, addendum) {
|
|
2477
311
|
const normalizedBase = normalize(base);
|
|
@@ -2517,7 +351,7 @@ function planSystemPrompt(adapter, ctx) {
|
|
|
2517
351
|
const existing = readSystemPromptFile(target);
|
|
2518
352
|
assertSystemPromptMarkers(existing, target);
|
|
2519
353
|
const readModule = (file) => stripLeadingHtmlComments(
|
|
2520
|
-
normalize2(
|
|
354
|
+
normalize2(fs5.readFileSync(path5.join(ctx.stackDir, "system-prompt", file), "utf8"))
|
|
2521
355
|
);
|
|
2522
356
|
const modules = {
|
|
2523
357
|
"system-prompt": composeProgrammaticSystemPrompt(ctx.stackDir, readModule("AGENTS.md"), ctx.mode),
|
|
@@ -2537,7 +371,7 @@ function planSystemPrompt(adapter, ctx) {
|
|
|
2537
371
|
}
|
|
2538
372
|
|
|
2539
373
|
// src/components/agents.ts
|
|
2540
|
-
import
|
|
374
|
+
import path6 from "path";
|
|
2541
375
|
function planAgents(adapter, ctx) {
|
|
2542
376
|
const { agentsDir, commandsDir, outputStylesDir, profilesDir, scriptsDir } = adapter.paths(ctx.configDir);
|
|
2543
377
|
const dirFor = {
|
|
@@ -2547,7 +381,7 @@ function planAgents(adapter, ctx) {
|
|
|
2547
381
|
profile: profilesDir
|
|
2548
382
|
};
|
|
2549
383
|
const scriptsBase = scriptsDir.replace(/\\/g, "/");
|
|
2550
|
-
return loadCanonicalAgents(
|
|
384
|
+
return loadCanonicalAgents(path6.join(ctx.stackDir, "agents")).flatMap((agent) => {
|
|
2551
385
|
const composedAgent = {
|
|
2552
386
|
...agent,
|
|
2553
387
|
body: composeProgrammaticAgentBody(ctx.stackDir, agent, ctx.mode, ctx.subagentConcurrency)
|
|
@@ -2559,42 +393,42 @@ function planAgents(adapter, ctx) {
|
|
|
2559
393
|
return [];
|
|
2560
394
|
}
|
|
2561
395
|
const content = rendered.content.replace(/\{\{SCRIPTS_DIR\}\}/g, scriptsBase);
|
|
2562
|
-
return [{ kind: "write", target:
|
|
396
|
+
return [{ kind: "write", target: path6.join(dir, rendered.file), content }];
|
|
2563
397
|
});
|
|
2564
398
|
});
|
|
2565
399
|
}
|
|
2566
400
|
|
|
2567
401
|
// src/components/skills.ts
|
|
2568
|
-
import
|
|
402
|
+
import path7 from "path";
|
|
2569
403
|
function planSkills(adapter, ctx) {
|
|
2570
404
|
const { skillsDir } = adapter.paths(ctx.configDir);
|
|
2571
|
-
const source =
|
|
405
|
+
const source = path7.join(ctx.stackDir, "skills");
|
|
2572
406
|
return listFilesRecursive(source).map((file) => {
|
|
2573
|
-
const relative =
|
|
2574
|
-
return { kind: "copy", source: file, target:
|
|
407
|
+
const relative = path7.relative(source, file);
|
|
408
|
+
return { kind: "copy", source: file, target: path7.join(skillsDir, relative) };
|
|
2575
409
|
});
|
|
2576
410
|
}
|
|
2577
411
|
|
|
2578
412
|
// src/components/commands.ts
|
|
2579
|
-
import
|
|
2580
|
-
import
|
|
413
|
+
import path8 from "path";
|
|
414
|
+
import fs6 from "fs";
|
|
2581
415
|
function planCommands(adapter, ctx) {
|
|
2582
416
|
const { commandsDir } = adapter.paths(ctx.configDir);
|
|
2583
|
-
const source =
|
|
2584
|
-
if (!
|
|
417
|
+
const source = path8.join(ctx.stackDir, "commands");
|
|
418
|
+
if (!fs6.existsSync(source)) return [];
|
|
2585
419
|
const commandFiles = [
|
|
2586
420
|
...listMarkdownFiles(source),
|
|
2587
|
-
...listMarkdownFiles(
|
|
421
|
+
...listMarkdownFiles(path8.join(source, adapter.id))
|
|
2588
422
|
];
|
|
2589
423
|
return commandFiles.map(({ file, fullPath }) => {
|
|
2590
|
-
const raw =
|
|
424
|
+
const raw = fs6.readFileSync(fullPath, "utf8").replace(/\r\n/g, "\n");
|
|
2591
425
|
const rendered = adapter.renderCommand(file, raw);
|
|
2592
|
-
return { kind: "write", target:
|
|
426
|
+
return { kind: "write", target: path8.join(commandsDir, rendered.file), content: rendered.content };
|
|
2593
427
|
});
|
|
2594
428
|
}
|
|
2595
429
|
function listMarkdownFiles(dir) {
|
|
2596
|
-
if (!
|
|
2597
|
-
return
|
|
430
|
+
if (!fs6.existsSync(dir)) return [];
|
|
431
|
+
return fs6.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => ({ file: entry.name, fullPath: path8.join(dir, entry.name) }));
|
|
2598
432
|
}
|
|
2599
433
|
|
|
2600
434
|
// src/components/hooks.ts
|
|
@@ -2608,23 +442,24 @@ function planMcp(adapter, ctx) {
|
|
|
2608
442
|
}
|
|
2609
443
|
|
|
2610
444
|
// src/components/plugins.ts
|
|
2611
|
-
import
|
|
2612
|
-
import
|
|
445
|
+
import path9 from "path";
|
|
446
|
+
import fs7 from "fs";
|
|
2613
447
|
function planPlugins(adapter, ctx) {
|
|
2614
448
|
const { pluginsDir } = adapter.paths(ctx.configDir);
|
|
2615
449
|
if (pluginsDir === null) return [];
|
|
2616
|
-
const source =
|
|
2617
|
-
if (!
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
const
|
|
450
|
+
const source = path9.join(ctx.stackDir, "plugins", adapter.id);
|
|
451
|
+
if (!fs7.existsSync(source)) return [];
|
|
452
|
+
const excluded = new Set(adapter.excludedPluginBasenames ?? []);
|
|
453
|
+
return listFilesRecursive(source).filter((f) => f.endsWith(".ts")).filter((f) => !excluded.has(path9.basename(f))).map((sourceFile) => {
|
|
454
|
+
const target = path9.join(pluginsDir, path9.relative(source, sourceFile));
|
|
455
|
+
const raw = fs7.readFileSync(sourceFile, "utf8");
|
|
2621
456
|
let content = raw;
|
|
2622
457
|
if (content.includes('"{{ENGRAM_BIN}}"')) {
|
|
2623
458
|
content = content.replace(/"\{\{ENGRAM_BIN\}\}"/g, JSON.stringify(ctx.engramBin ?? "engram"));
|
|
2624
459
|
}
|
|
2625
460
|
if (content.includes('"{{ENGRAM_PROTOCOL}}"')) {
|
|
2626
461
|
const protocol = stripLeadingHtmlComments(
|
|
2627
|
-
|
|
462
|
+
fs7.readFileSync(path9.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8").replace(/\r\n/g, "\n")
|
|
2628
463
|
);
|
|
2629
464
|
content = content.replace(/"\{\{ENGRAM_PROTOCOL\}\}"/g, JSON.stringify(protocol));
|
|
2630
465
|
}
|
|
@@ -2635,10 +470,10 @@ function planPlugins(adapter, ctx) {
|
|
|
2635
470
|
}
|
|
2636
471
|
|
|
2637
472
|
// src/lib/external-tools.ts
|
|
2638
|
-
import
|
|
473
|
+
import fs8 from "fs";
|
|
2639
474
|
import os from "os";
|
|
2640
|
-
import
|
|
2641
|
-
import { execFileSync
|
|
475
|
+
import path10 from "path";
|
|
476
|
+
import { execFileSync } from "child_process";
|
|
2642
477
|
var PLAYWRIGHT_CLI = {
|
|
2643
478
|
packageName: "@playwright/cli",
|
|
2644
479
|
bin: "playwright-cli",
|
|
@@ -2683,9 +518,9 @@ function detectPlaywrightCli(env) {
|
|
|
2683
518
|
if (binPath === null) {
|
|
2684
519
|
const pnpmHome = resolvePnpmHome(actualEnv);
|
|
2685
520
|
const name = process.platform === "win32" ? `${PLAYWRIGHT_CLI.bin}.cmd` : PLAYWRIGHT_CLI.bin;
|
|
2686
|
-
const known = [
|
|
521
|
+
const known = [path10.join(pnpmHome, name), path10.join(pnpmHome, "bin", name)].find((file) => {
|
|
2687
522
|
try {
|
|
2688
|
-
return
|
|
523
|
+
return fs8.statSync(file).isFile();
|
|
2689
524
|
} catch {
|
|
2690
525
|
return false;
|
|
2691
526
|
}
|
|
@@ -2704,7 +539,7 @@ function resolvePnpmBin(env) {
|
|
|
2704
539
|
return pnpmCmd !== null && !pnpmCmd.toLowerCase().endsWith(".ps1") ? pnpmCmd : null;
|
|
2705
540
|
}
|
|
2706
541
|
function resolvePnpmHome(env = process.env, platform = process.platform, homeDir = os.homedir()) {
|
|
2707
|
-
const paths = platform === "win32" ?
|
|
542
|
+
const paths = platform === "win32" ? path10.win32 : path10.posix;
|
|
2708
543
|
if (env.PNPM_HOME) return env.PNPM_HOME;
|
|
2709
544
|
if (env.XDG_DATA_HOME) return paths.join(env.XDG_DATA_HOME, "pnpm");
|
|
2710
545
|
if (platform === "darwin") return paths.join(homeDir, "Library", "pnpm");
|
|
@@ -2713,7 +548,7 @@ function resolvePnpmHome(env = process.env, platform = process.platform, homeDir
|
|
|
2713
548
|
}
|
|
2714
549
|
function setupPnpmGlobal(pnpmBin, env = process.env, platform = process.platform, homeDir = os.homedir()) {
|
|
2715
550
|
const pnpmHome = resolvePnpmHome(env, platform, homeDir);
|
|
2716
|
-
const paths = platform === "win32" ?
|
|
551
|
+
const paths = platform === "win32" ? path10.win32 : path10.posix;
|
|
2717
552
|
if (!paths.isAbsolute(pnpmHome)) return { ok: false, reason: "PNPM_HOME debe ser una ruta absoluta; corrige la configuraci\xF3n antes de reintentar." };
|
|
2718
553
|
const pathEntries = [pnpmHome, paths.join(pnpmHome, "bin")];
|
|
2719
554
|
if (env.PATH !== void 0) pathEntries.push(env.PATH);
|
|
@@ -2725,7 +560,7 @@ function setupPnpmGlobal(pnpmBin, env = process.env, platform = process.platform
|
|
|
2725
560
|
const invocation = planDetectedBinCommand(pnpmBin, ["setup"]);
|
|
2726
561
|
if (invocation === null) return { ok: false, reason: "No se pudo preparar la invocaci\xF3n de pnpm setup." };
|
|
2727
562
|
try {
|
|
2728
|
-
|
|
563
|
+
execFileSync(invocation.command, invocation.args, { stdio: "inherit", env: childEnv });
|
|
2729
564
|
return { ok: true, env: childEnv };
|
|
2730
565
|
} catch (error) {
|
|
2731
566
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -2733,10 +568,10 @@ function setupPnpmGlobal(pnpmBin, env = process.env, platform = process.platform
|
|
|
2733
568
|
}
|
|
2734
569
|
function isPlaywrightBrowserReady(env = process.env, platform = process.platform, homeDir = os.homedir()) {
|
|
2735
570
|
const configuredPath = env.PLAYWRIGHT_BROWSERS_PATH;
|
|
2736
|
-
const cacheDir = configuredPath ?? (platform === "win32" ?
|
|
571
|
+
const cacheDir = configuredPath ?? (platform === "win32" ? path10.join(env.LOCALAPPDATA ?? path10.join(homeDir, "AppData", "Local"), "ms-playwright") : platform === "darwin" ? path10.join(homeDir, "Library", "Caches", "ms-playwright") : path10.join(env.XDG_CACHE_HOME ?? path10.join(homeDir, ".cache"), "ms-playwright"));
|
|
2737
572
|
if (configuredPath === "0") return { status: "missing", path: cacheDir, errorCode: "DISABLED" };
|
|
2738
573
|
try {
|
|
2739
|
-
const ready =
|
|
574
|
+
const ready = fs8.readdirSync(cacheDir, { withFileTypes: true }).some(
|
|
2740
575
|
(entry) => entry.isDirectory() && /^chromium(?:_headless_shell)?-/.test(entry.name)
|
|
2741
576
|
);
|
|
2742
577
|
return ready ? { status: "ready", path: cacheDir } : { status: "missing", path: cacheDir };
|
|
@@ -2761,16 +596,16 @@ function verifyPlaywrightBrowser(pnpmBin, env = process.env, cwd) {
|
|
|
2761
596
|
const rootCommand = planDetectedBinCommand(pnpmBin, ["root", "--global"]);
|
|
2762
597
|
if (rootCommand === null) return false;
|
|
2763
598
|
try {
|
|
2764
|
-
const root =
|
|
599
|
+
const root = execFileSync(rootCommand.command, rootCommand.args, {
|
|
2765
600
|
encoding: "utf8",
|
|
2766
601
|
timeout: 5e3,
|
|
2767
602
|
env,
|
|
2768
603
|
cwd,
|
|
2769
604
|
stdio: ["ignore", "pipe", "inherit"]
|
|
2770
605
|
}).trim();
|
|
2771
|
-
if (!
|
|
2772
|
-
const packageFile =
|
|
2773
|
-
const installed = JSON.parse(
|
|
606
|
+
if (!path10.isAbsolute(root)) return false;
|
|
607
|
+
const packageFile = fs8.realpathSync(path10.join(root, "@playwright", "cli", "package.json"));
|
|
608
|
+
const installed = JSON.parse(fs8.readFileSync(packageFile, "utf8"));
|
|
2774
609
|
if (installed.name !== PLAYWRIGHT_CLI.packageName || installed.version !== PLAYWRIGHT_CLI.version) return false;
|
|
2775
610
|
const probe = `
|
|
2776
611
|
const { createRequire } = require('node:module');
|
|
@@ -2783,7 +618,7 @@ function verifyPlaywrightBrowser(pnpmBin, env = process.env, cwd) {
|
|
|
2783
618
|
} finally { await browser.close(); }
|
|
2784
619
|
})().catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
2785
620
|
`;
|
|
2786
|
-
|
|
621
|
+
execFileSync(process.execPath, ["-e", probe, packageFile], {
|
|
2787
622
|
timeout: 25e3,
|
|
2788
623
|
stdio: "inherit",
|
|
2789
624
|
cwd,
|
|
@@ -2800,7 +635,7 @@ function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin(), env) {
|
|
|
2800
635
|
const preflight = planDetectedBinCommand(pnpmBin, ["bin", "--global"]);
|
|
2801
636
|
if (preflight === null) return { ok: false, reason: "pnpm-command" };
|
|
2802
637
|
try {
|
|
2803
|
-
const globalBin =
|
|
638
|
+
const globalBin = execFileSync(preflight.command, preflight.args, {
|
|
2804
639
|
encoding: "utf8",
|
|
2805
640
|
stdio: ["ignore", "pipe", "inherit"],
|
|
2806
641
|
...env === void 0 ? {} : { env }
|
|
@@ -2813,10 +648,10 @@ function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin(), env) {
|
|
|
2813
648
|
const command = planPlaywrightCliCommand(action, pnpmBin);
|
|
2814
649
|
const invocation = planDetectedBinCommand(command.command, command.args);
|
|
2815
650
|
if (invocation === null) return { ok: false, reason: "pnpm-command" };
|
|
2816
|
-
const cwd = action === "install-browser" ?
|
|
651
|
+
const cwd = action === "install-browser" ? fs8.mkdtempSync(path10.join(os.tmpdir(), "jorgex-playwright-")) : void 0;
|
|
2817
652
|
const childEnv = action === "install-browser" ? { ...env ?? process.env, NO_UPDATE_NOTIFIER: "1" } : env ?? process.env;
|
|
2818
653
|
try {
|
|
2819
|
-
|
|
654
|
+
execFileSync(invocation.command, invocation.args, {
|
|
2820
655
|
stdio: "inherit",
|
|
2821
656
|
env: childEnv,
|
|
2822
657
|
...cwd === void 0 ? {} : { cwd }
|
|
@@ -2828,7 +663,7 @@ function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin(), env) {
|
|
|
2828
663
|
} catch {
|
|
2829
664
|
return { ok: false, reason: "action-failed" };
|
|
2830
665
|
} finally {
|
|
2831
|
-
if (cwd !== void 0)
|
|
666
|
+
if (cwd !== void 0) fs8.rmSync(cwd, { recursive: true, force: true });
|
|
2832
667
|
}
|
|
2833
668
|
}
|
|
2834
669
|
|
|
@@ -2857,29 +692,29 @@ function inspectPlaywrightCapability(options = {}) {
|
|
|
2857
692
|
}
|
|
2858
693
|
|
|
2859
694
|
// src/lib/tool-preferences.ts
|
|
2860
|
-
import
|
|
2861
|
-
import
|
|
695
|
+
import fs9 from "fs";
|
|
696
|
+
import path11 from "path";
|
|
2862
697
|
var PLAYWRIGHT_CLI_PREFERENCE_VERSION = 1;
|
|
2863
698
|
var DEVTOOLS_MCP_PREFERENCE_VERSION = 1;
|
|
2864
699
|
var PRIMARY_MODEL_OWNERSHIP_VERSION = 1;
|
|
2865
700
|
var PLAYWRIGHT_RUNTIMES = ["opencode", "claude-code", "codex", "pi"];
|
|
2866
701
|
function readPreference(file) {
|
|
2867
702
|
try {
|
|
2868
|
-
return { raw:
|
|
703
|
+
return { raw: fs9.readFileSync(file, "utf8"), errorCode: null };
|
|
2869
704
|
} catch (error) {
|
|
2870
705
|
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
2871
706
|
return code === "ENOENT" ? { raw: null, errorCode: null } : { raw: null, errorCode: code };
|
|
2872
707
|
}
|
|
2873
708
|
}
|
|
2874
709
|
function playwrightCliPreferenceFile(stateDir = dataDir()) {
|
|
2875
|
-
return
|
|
710
|
+
return path11.join(stateDir, "playwright-cli.json");
|
|
2876
711
|
}
|
|
2877
712
|
function parsePlaywrightCliPreference(raw) {
|
|
2878
713
|
try {
|
|
2879
714
|
const value = JSON.parse(raw);
|
|
2880
|
-
if (!
|
|
715
|
+
if (!isRecord(value)) return void 0;
|
|
2881
716
|
if (value.version === 1 && typeof value.enabled === "boolean") return { version: 1, enabled: value.enabled };
|
|
2882
|
-
if (value.version !== 2 || !
|
|
717
|
+
if (value.version !== 2 || !isRecord(value.enabled)) return void 0;
|
|
2883
718
|
if (Object.entries(value.enabled).some(([runtime, enabled]) => !PLAYWRIGHT_RUNTIMES.includes(runtime) || typeof enabled !== "boolean")) return void 0;
|
|
2884
719
|
return { version: 2, enabled: value.enabled };
|
|
2885
720
|
} catch {
|
|
@@ -2918,12 +753,12 @@ function savePlaywrightCliPreference(file, enabled, selection) {
|
|
|
2918
753
|
writeText(file, content);
|
|
2919
754
|
}
|
|
2920
755
|
function devtoolsMcpPreferenceFile(stateDir = dataDir()) {
|
|
2921
|
-
return
|
|
756
|
+
return path11.join(stateDir, "devtools-mcp.json");
|
|
2922
757
|
}
|
|
2923
758
|
function primaryModelOwnershipFile(stateDir = dataDir()) {
|
|
2924
|
-
return
|
|
759
|
+
return path11.join(stateDir, "primary-model.json");
|
|
2925
760
|
}
|
|
2926
|
-
function
|
|
761
|
+
function isRecord(value) {
|
|
2927
762
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2928
763
|
}
|
|
2929
764
|
function isRuntimeId(value) {
|
|
@@ -2932,7 +767,7 @@ function isRuntimeId(value) {
|
|
|
2932
767
|
function parseDevtoolsMcpState(raw) {
|
|
2933
768
|
try {
|
|
2934
769
|
const value = JSON.parse(raw);
|
|
2935
|
-
if (!
|
|
770
|
+
if (!isRecord(value) || value.version !== DEVTOOLS_MCP_PREFERENCE_VERSION || !isRecord(value.enabled) || !isRecord(value.owned)) return null;
|
|
2936
771
|
const enabled = {};
|
|
2937
772
|
for (const [runtime, selected] of Object.entries(value.enabled)) {
|
|
2938
773
|
if (!isRuntimeId(runtime) && runtime !== "pi" || typeof selected !== "boolean") return null;
|
|
@@ -2940,7 +775,7 @@ function parseDevtoolsMcpState(raw) {
|
|
|
2940
775
|
}
|
|
2941
776
|
const owned = {};
|
|
2942
777
|
for (const [runtime, servers] of Object.entries(value.owned)) {
|
|
2943
|
-
if (!isRuntimeId(runtime) || !
|
|
778
|
+
if (!isRuntimeId(runtime) || !isRecord(servers)) return null;
|
|
2944
779
|
const managed = {};
|
|
2945
780
|
for (const [server, marked] of Object.entries(servers)) {
|
|
2946
781
|
if (marked !== true) return null;
|
|
@@ -2996,13 +831,13 @@ function saveDevtoolsMcpOwnership(file, runtime, server, owned) {
|
|
|
2996
831
|
function parsePrimaryModelOwnership(raw) {
|
|
2997
832
|
try {
|
|
2998
833
|
const value = JSON.parse(raw);
|
|
2999
|
-
if (!
|
|
834
|
+
if (!isRecord(value) || value.version !== PRIMARY_MODEL_OWNERSHIP_VERSION || !isRecord(value.owned)) return null;
|
|
3000
835
|
const owned = {};
|
|
3001
836
|
for (const [runtime, configs] of Object.entries(value.owned)) {
|
|
3002
|
-
if (!isRuntimeId(runtime) || !
|
|
837
|
+
if (!isRuntimeId(runtime) || !isRecord(configs)) return null;
|
|
3003
838
|
const markedConfigs = {};
|
|
3004
839
|
for (const [configDir, fields] of Object.entries(configs)) {
|
|
3005
|
-
if (configDir === "" || !
|
|
840
|
+
if (configDir === "" || !isRecord(fields)) return null;
|
|
3006
841
|
const markedFields = {};
|
|
3007
842
|
for (const [field, state] of Object.entries(fields)) {
|
|
3008
843
|
if (field === "" || state !== true) return null;
|
|
@@ -3031,7 +866,7 @@ function primaryModelOwnershipError(file = primaryModelOwnershipFile()) {
|
|
|
3031
866
|
return `Primary model: ownership inv\xE1lido en ${file}. Corrige o borra ese archivo antes de reintentar.`;
|
|
3032
867
|
}
|
|
3033
868
|
function primaryModelConfigKey(configDir) {
|
|
3034
|
-
const resolved =
|
|
869
|
+
const resolved = path11.resolve(configDir);
|
|
3035
870
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
3036
871
|
}
|
|
3037
872
|
function loadPrimaryModelOwnership(file, runtime, configDir) {
|
|
@@ -3176,7 +1011,7 @@ function diffPlan(plan) {
|
|
|
3176
1011
|
if (current === null) return { action, status: "create" };
|
|
3177
1012
|
return { action, status: current === action.content ? "unchanged" : "update" };
|
|
3178
1013
|
}
|
|
3179
|
-
if (!
|
|
1014
|
+
if (!fs10.existsSync(action.target)) return { action, status: "create" };
|
|
3180
1015
|
return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
|
|
3181
1016
|
});
|
|
3182
1017
|
}
|
|
@@ -3202,7 +1037,7 @@ function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE, playwr
|
|
|
3202
1037
|
continue;
|
|
3203
1038
|
}
|
|
3204
1039
|
try {
|
|
3205
|
-
for (const action of buildPlan(adapter, ctx)) targets.add(
|
|
1040
|
+
for (const action of buildPlan(adapter, ctx)) targets.add(path12.resolve(action.target));
|
|
3206
1041
|
} catch (error) {
|
|
3207
1042
|
complete = false;
|
|
3208
1043
|
warnings.push(
|
|
@@ -3212,6 +1047,40 @@ function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE, playwr
|
|
|
3212
1047
|
}
|
|
3213
1048
|
return { targets, complete, warnings };
|
|
3214
1049
|
}
|
|
1050
|
+
function legacyOpencodePluginPath(configDir) {
|
|
1051
|
+
return path12.resolve(path12.join(configDir, "plugins", "engram.ts"));
|
|
1052
|
+
}
|
|
1053
|
+
async function runOfficialSetupForInstall(args) {
|
|
1054
|
+
let detectedVersion = null;
|
|
1055
|
+
if (args.runtime === "claude-code" && typeof args.engramBin === "string" && args.engramBin !== "") {
|
|
1056
|
+
try {
|
|
1057
|
+
detectedVersion = engramVersion(args.engramBin);
|
|
1058
|
+
} catch {
|
|
1059
|
+
detectedVersion = null;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return runOfficialSetupIfNeeded(args.runtime, {
|
|
1063
|
+
command: args.command,
|
|
1064
|
+
dryRun: args.dryRun,
|
|
1065
|
+
targetDir: args.targetDir,
|
|
1066
|
+
engramBin: args.engramBin,
|
|
1067
|
+
configDir: args.configDir,
|
|
1068
|
+
homeDir: HOME,
|
|
1069
|
+
engramVersion: detectedVersion,
|
|
1070
|
+
...args.runtime === "claude-code" ? { isExplicitClaudeConfigDir: process.env.CLAUDE_CONFIG_DIR !== void 0 } : {}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
function formatOfficialFailure(adapterName, official) {
|
|
1074
|
+
const detail = official.stderr ?? official.reason ?? "sin detalle";
|
|
1075
|
+
if (official.ok) return `${adapterName}: setup oficial Engram ok.`;
|
|
1076
|
+
if (official.incompleteRecovery === true) {
|
|
1077
|
+
return `${adapterName}: setup oficial Engram fall\xF3 (${detail}). Recuperaci\xF3n incompleta \u2014 revisa el backup ${official.backupId ?? "previo"}.`;
|
|
1078
|
+
}
|
|
1079
|
+
if (official.recovery === "complete") {
|
|
1080
|
+
return `${adapterName}: setup oficial Engram fall\xF3 (${detail}). Se restaur\xF3 el backup previo.`;
|
|
1081
|
+
}
|
|
1082
|
+
return `${adapterName}: setup oficial Engram fall\xF3 (${detail}).`;
|
|
1083
|
+
}
|
|
3215
1084
|
async function runInstall(opts) {
|
|
3216
1085
|
const showSummary = opts.showSummary !== false;
|
|
3217
1086
|
if (showSummary) p.intro(`jorgex-stack ${opts.dryRun ? "install (dry-run)" : "install"}`);
|
|
@@ -3311,6 +1180,15 @@ async function runInstall(opts) {
|
|
|
3311
1180
|
reportStatus(adapter.name, "failed");
|
|
3312
1181
|
continue;
|
|
3313
1182
|
}
|
|
1183
|
+
if (shouldRunOfficialSetup({ command: opts.command, dryRun: opts.dryRun, targetDir: opts.targetDir })) {
|
|
1184
|
+
const destinationError = validateOfficialSetupDestination(id, configDir, HOME);
|
|
1185
|
+
if (destinationError !== null) {
|
|
1186
|
+
p.log.error(`${adapter.name}: ${destinationError}`);
|
|
1187
|
+
exitCode = 1;
|
|
1188
|
+
reportStatus(adapter.name, "failed");
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
3314
1192
|
const ctx = {
|
|
3315
1193
|
writingStyle,
|
|
3316
1194
|
stackDir,
|
|
@@ -3355,19 +1233,75 @@ async function runInstall(opts) {
|
|
|
3355
1233
|
reportStatus(adapter.name, "preview");
|
|
3356
1234
|
continue;
|
|
3357
1235
|
}
|
|
3358
|
-
const writeManifest = () => {
|
|
1236
|
+
const writeManifest = async (official, opts2) => {
|
|
3359
1237
|
if (!useManifest) return;
|
|
3360
|
-
const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) =>
|
|
1238
|
+
const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) => path12.resolve(a.target)));
|
|
3361
1239
|
const keepTarget = (target) => !unmergeTargets.has(target);
|
|
3362
|
-
const liveOwned = plan.map((a) =>
|
|
3363
|
-
const previousOwned = (prevManifest?.owned ?? []).map((target) =>
|
|
3364
|
-
const
|
|
3365
|
-
|
|
1240
|
+
const liveOwned = plan.map((a) => path12.resolve(a.target)).filter(keepTarget);
|
|
1241
|
+
const previousOwned = (prevManifest?.owned ?? []).map((target) => path12.resolve(target)).filter(keepTarget);
|
|
1242
|
+
const officialFailed = official?.ran === true && !official.ok;
|
|
1243
|
+
let owned;
|
|
1244
|
+
let pendingOrphans;
|
|
1245
|
+
if (!canOrphan) {
|
|
1246
|
+
owned = [.../* @__PURE__ */ new Set([...previousOwned, ...liveOwned])];
|
|
1247
|
+
pendingOrphans = [];
|
|
1248
|
+
} else if (officialFailed) {
|
|
1249
|
+
owned = [.../* @__PURE__ */ new Set([...previousOwned, ...liveOwned])];
|
|
1250
|
+
pendingOrphans = [...orphans];
|
|
1251
|
+
} else {
|
|
1252
|
+
const keepPending = (opts2?.keepPendingOrphans ?? []).map((t) => path12.resolve(t));
|
|
1253
|
+
owned = [...liveOwned];
|
|
1254
|
+
if (id === "opencode") {
|
|
1255
|
+
const legacy = legacyOpencodePluginPath(configDir);
|
|
1256
|
+
if (previousOwned.includes(legacy)) {
|
|
1257
|
+
let state = "unknown";
|
|
1258
|
+
try {
|
|
1259
|
+
fs10.statSync(legacy);
|
|
1260
|
+
state = "present";
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
1263
|
+
state = code === "ENOENT" ? "absent" : "unknown";
|
|
1264
|
+
}
|
|
1265
|
+
if (state !== "absent") {
|
|
1266
|
+
let retire = false;
|
|
1267
|
+
if (state === "present" && official?.ran === true && official.ok && official.ownershipTransferred) {
|
|
1268
|
+
try {
|
|
1269
|
+
retire = (await shouldRetireLegacyEngram({ configDir })).retire;
|
|
1270
|
+
} catch {
|
|
1271
|
+
retire = false;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (!retire && !owned.includes(legacy)) owned.push(legacy);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
for (const pending of keepPending) {
|
|
1279
|
+
if (!owned.includes(pending)) owned.push(pending);
|
|
1280
|
+
}
|
|
1281
|
+
owned = [...new Set(owned)];
|
|
1282
|
+
pendingOrphans = [...keepPending];
|
|
1283
|
+
}
|
|
1284
|
+
writeRuntimeManifest(id, { configDir, owned, pendingOrphans, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3366
1285
|
};
|
|
3367
1286
|
if (changes.length === 0 && orphans.length === 0) {
|
|
3368
|
-
writeManifest();
|
|
3369
1287
|
if (useManifest) persistConfigurationOwnershipChanges(id, configDir, plan);
|
|
3370
1288
|
persistDevtoolsSelection();
|
|
1289
|
+
const official = await runOfficialSetupForInstall({
|
|
1290
|
+
runtime: id,
|
|
1291
|
+
configDir,
|
|
1292
|
+
engramBin,
|
|
1293
|
+
command: opts.command,
|
|
1294
|
+
dryRun: opts.dryRun,
|
|
1295
|
+
targetDir: opts.targetDir
|
|
1296
|
+
});
|
|
1297
|
+
if (official.ran && !official.ok) {
|
|
1298
|
+
await writeManifest(official);
|
|
1299
|
+
p.log.error(formatOfficialFailure(adapter.name, official));
|
|
1300
|
+
exitCode = 1;
|
|
1301
|
+
reportStatus(adapter.name, "failed");
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
await writeManifest(official.ran ? official : void 0);
|
|
3371
1305
|
p.log.success(`${adapter.name}: ya al d\xEDa (idempotente).`);
|
|
3372
1306
|
successfulRuns++;
|
|
3373
1307
|
successfulContexts.push({ adapter, ctx });
|
|
@@ -3391,11 +1325,6 @@ async function runInstall(opts) {
|
|
|
3391
1325
|
const backup = useManifest ? createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`) : null;
|
|
3392
1326
|
if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
3393
1327
|
applyChanges(changes, useManifest ? (action) => persistConfigurationOwnershipChanges(id, configDir, [action]) : void 0);
|
|
3394
|
-
const pruneRoot = useManifest ? HOME : path22.dirname(configDir);
|
|
3395
|
-
for (const orphan of orphans) {
|
|
3396
|
-
fs17.rmSync(orphan, { force: true });
|
|
3397
|
-
pruneEmptyDirs(orphan, pruneRoot);
|
|
3398
|
-
}
|
|
3399
1328
|
const verifyCtx = { ...ctx, warnings: [] };
|
|
3400
1329
|
const dirty = diffPlan(buildPlan(adapter, verifyCtx)).filter((d) => d.status !== "unchanged");
|
|
3401
1330
|
if (dirty.length > 0) {
|
|
@@ -3404,13 +1333,51 @@ async function runInstall(opts) {
|
|
|
3404
1333
|
exitCode = 1;
|
|
3405
1334
|
reportStatus(adapter.name, "failed");
|
|
3406
1335
|
} else {
|
|
3407
|
-
writeManifest();
|
|
3408
1336
|
if (useManifest) persistConfigurationOwnershipChanges(id, configDir, plan);
|
|
3409
1337
|
persistDevtoolsSelection();
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
1338
|
+
const official = await runOfficialSetupForInstall({
|
|
1339
|
+
runtime: id,
|
|
1340
|
+
configDir,
|
|
1341
|
+
engramBin,
|
|
1342
|
+
command: opts.command,
|
|
1343
|
+
dryRun: opts.dryRun,
|
|
1344
|
+
targetDir: opts.targetDir
|
|
1345
|
+
});
|
|
1346
|
+
if (official.ran && !official.ok) {
|
|
1347
|
+
await writeManifest(official);
|
|
1348
|
+
p.log.error(formatOfficialFailure(adapter.name, official));
|
|
1349
|
+
exitCode = 1;
|
|
1350
|
+
reportStatus(adapter.name, "failed");
|
|
1351
|
+
} else {
|
|
1352
|
+
const pruneRoot = useManifest ? HOME : path12.dirname(configDir);
|
|
1353
|
+
let orphanFailed = null;
|
|
1354
|
+
let failedIndex = null;
|
|
1355
|
+
for (let index = 0; index < orphans.length; index++) {
|
|
1356
|
+
const orphan = orphans[index];
|
|
1357
|
+
if (path12.basename(orphan) === "engram.ts") continue;
|
|
1358
|
+
try {
|
|
1359
|
+
fs10.rmSync(orphan, { force: true });
|
|
1360
|
+
pruneEmptyDirs(orphan, pruneRoot);
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
orphanFailed = `${orphan} (${error instanceof Error ? error.message : String(error)})`;
|
|
1363
|
+
failedIndex = index;
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (orphanFailed !== null) {
|
|
1368
|
+
const keepPending = failedIndex === null ? [...orphans] : orphans.slice(failedIndex);
|
|
1369
|
+
await writeManifest(official.ran ? official : void 0, { keepPendingOrphans: keepPending });
|
|
1370
|
+
p.log.error(`${adapter.name}: no se pudo eliminar hu\xE9rfano ${orphanFailed} \u2014 se conserva y no se reporta \xE9xito.`);
|
|
1371
|
+
exitCode = 1;
|
|
1372
|
+
reportStatus(adapter.name, "failed");
|
|
1373
|
+
} else {
|
|
1374
|
+
await writeManifest(official.ran ? official : void 0);
|
|
1375
|
+
p.log.success(`${adapter.name}: ${changes.length} archivos aplicados y verificados (idempotente).`);
|
|
1376
|
+
successfulRuns++;
|
|
1377
|
+
successfulContexts.push({ adapter, ctx });
|
|
1378
|
+
reportStatus(adapter.name, "ok");
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
3414
1381
|
}
|
|
3415
1382
|
}
|
|
3416
1383
|
if (toolPlan?.actions.length) {
|
|
@@ -3523,33 +1490,33 @@ async function runInstall(opts) {
|
|
|
3523
1490
|
}
|
|
3524
1491
|
|
|
3525
1492
|
// src/uninstall.ts
|
|
3526
|
-
import
|
|
3527
|
-
import
|
|
1493
|
+
import fs12 from "fs";
|
|
1494
|
+
import path16 from "path";
|
|
3528
1495
|
import * as p2 from "@clack/prompts";
|
|
3529
1496
|
|
|
3530
1497
|
// src/lib/pi-projection-lifecycle.ts
|
|
3531
|
-
import
|
|
3532
|
-
import
|
|
1498
|
+
import fs11 from "fs";
|
|
1499
|
+
import path15 from "path";
|
|
3533
1500
|
import { createHash } from "crypto";
|
|
3534
1501
|
|
|
3535
1502
|
// src/adapters/pi.ts
|
|
3536
|
-
import
|
|
1503
|
+
import path13 from "path";
|
|
3537
1504
|
function piSystemPromptFile(targetDir) {
|
|
3538
|
-
const configDir = targetDir === void 0 ? process.env.PI_CODING_AGENT_DIR ??
|
|
3539
|
-
return
|
|
1505
|
+
const configDir = targetDir === void 0 ? process.env.PI_CODING_AGENT_DIR ?? path13.join(HOME, ".pi", "agent") : path13.join(path13.resolve(targetDir), "pi-agent");
|
|
1506
|
+
return path13.join(configDir, "AGENTS.md");
|
|
3540
1507
|
}
|
|
3541
1508
|
var piAdapter = {
|
|
3542
1509
|
id: "pi",
|
|
3543
1510
|
paths(configDir) {
|
|
3544
|
-
const piConfigDir =
|
|
3545
|
-
const agentsHome = samePath(configDir, piConfigDir) ? HOME :
|
|
1511
|
+
const piConfigDir = path13.dirname(piSystemPromptFile());
|
|
1512
|
+
const agentsHome = samePath(configDir, piConfigDir) ? HOME : path13.join(path13.dirname(configDir), "home");
|
|
3546
1513
|
return {
|
|
3547
|
-
systemPromptFile:
|
|
3548
|
-
agentsDir:
|
|
3549
|
-
skillsDir:
|
|
3550
|
-
commandsDir:
|
|
1514
|
+
systemPromptFile: path13.join(configDir, "AGENTS.md"),
|
|
1515
|
+
agentsDir: path13.join(configDir, "agents"),
|
|
1516
|
+
skillsDir: path13.join(agentsHome, ".agents", "skills"),
|
|
1517
|
+
commandsDir: path13.join(configDir, "prompts"),
|
|
3551
1518
|
pluginsDir: null,
|
|
3552
|
-
scriptsDir:
|
|
1519
|
+
scriptsDir: path13.join(configDir, "scripts"),
|
|
3553
1520
|
outputStylesDir: null,
|
|
3554
1521
|
profilesDir: null
|
|
3555
1522
|
};
|
|
@@ -3569,7 +1536,7 @@ var piAdapter = {
|
|
|
3569
1536
|
};
|
|
3570
1537
|
|
|
3571
1538
|
// src/lib/pi-package-lifecycle.ts
|
|
3572
|
-
import
|
|
1539
|
+
import path14 from "path";
|
|
3573
1540
|
var REQUIRED_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
3574
1541
|
"foundation-contract-v1",
|
|
3575
1542
|
"runner-json-v1",
|
|
@@ -3721,7 +1688,7 @@ function parseReceiptShape(receiptJson) {
|
|
|
3721
1688
|
return null;
|
|
3722
1689
|
}
|
|
3723
1690
|
if (engram === void 0) return "upgrade-required";
|
|
3724
|
-
if (engram === null || typeof engram !== "object" || Array.isArray(engram) || typeof Reflect.get(engram, "binary") !== "string" || !
|
|
1691
|
+
if (engram === null || typeof engram !== "object" || Array.isArray(engram) || typeof Reflect.get(engram, "binary") !== "string" || !path14.isAbsolute(Reflect.get(engram, "binary"))) {
|
|
3725
1692
|
return null;
|
|
3726
1693
|
}
|
|
3727
1694
|
return parsed;
|
|
@@ -3758,7 +1725,7 @@ function planPiPackageLifecycle(input) {
|
|
|
3758
1725
|
if (input.receiptJson !== null) {
|
|
3759
1726
|
const parsedReceipt = parseReceipt(input.receiptJson, input.candidate, {
|
|
3760
1727
|
kind: input.scope.kind,
|
|
3761
|
-
codingAgentDir:
|
|
1728
|
+
codingAgentDir: path14.resolve(input.scope.codingAgentDir)
|
|
3762
1729
|
}, input.engramBin);
|
|
3763
1730
|
if (parsedReceipt === "upgrade-required") return blocked(input, "receipt-upgrade-required");
|
|
3764
1731
|
if (parsedReceipt === null) return blocked(input, "receipt-corrupt");
|
|
@@ -3793,7 +1760,7 @@ function planPiPackageLifecycle(input) {
|
|
|
3793
1760
|
},
|
|
3794
1761
|
receipt: expectedReceipt(input.candidate, "installing", {
|
|
3795
1762
|
kind: input.scope.kind,
|
|
3796
|
-
codingAgentDir:
|
|
1763
|
+
codingAgentDir: path14.resolve(input.scope.codingAgentDir)
|
|
3797
1764
|
}, input.engramBin),
|
|
3798
1765
|
ownership: ownership(true)
|
|
3799
1766
|
};
|
|
@@ -3808,7 +1775,7 @@ function parseRunnerRecord(stdout, stderr, command, candidate, packageRunner) {
|
|
|
3808
1775
|
const parsed = JSON.parse(body);
|
|
3809
1776
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3810
1777
|
const record = parsed;
|
|
3811
|
-
if (record.schemaVersion !== candidate.contract.runner.schemaVersion || record.command !== command || record.ok !== true || record.package === null || typeof record.package !== "object" || record.package.name !== candidate.package.name || record.package.version !== candidate.package.version || typeof record.package.root !== "string" || !
|
|
1778
|
+
if (record.schemaVersion !== candidate.contract.runner.schemaVersion || record.command !== command || record.ok !== true || record.package === null || typeof record.package !== "object" || record.package.name !== candidate.package.name || record.package.version !== candidate.package.version || typeof record.package.root !== "string" || !path14.isAbsolute(record.package.root) || path14.resolve(packageRunner) !== path14.resolve(record.package.root, "bin", "jorgex-pi.mjs")) {
|
|
3812
1779
|
return null;
|
|
3813
1780
|
}
|
|
3814
1781
|
return record;
|
|
@@ -3956,10 +1923,10 @@ function validateOwnedOperationState(input) {
|
|
|
3956
1923
|
}))) {
|
|
3957
1924
|
return { kind: "blocked", reason: "receipt-untrusted" };
|
|
3958
1925
|
}
|
|
3959
|
-
if (receipt.scope.kind !== (input.paths.targetDir ? "target-dir" : "real") ||
|
|
1926
|
+
if (receipt.scope.kind !== (input.paths.targetDir ? "target-dir" : "real") || path14.resolve(receipt.scope.codingAgentDir) !== path14.resolve(input.paths.codingAgentDir)) {
|
|
3960
1927
|
return { kind: "blocked", reason: "source-divergent" };
|
|
3961
1928
|
}
|
|
3962
|
-
if (input.engramBin !== null &&
|
|
1929
|
+
if (input.engramBin !== null && path14.resolve(receipt.engram.binary) !== path14.resolve(input.engramBin)) {
|
|
3963
1930
|
return { kind: "blocked", reason: "receipt-corrupt" };
|
|
3964
1931
|
}
|
|
3965
1932
|
const source = receipt.candidate.package.source;
|
|
@@ -4066,7 +2033,7 @@ var RETIRED_PLAYWRIGHT_FILES = [
|
|
|
4066
2033
|
"references/video-recording.md"
|
|
4067
2034
|
];
|
|
4068
2035
|
function retiredPlaywrightFiles(scope) {
|
|
4069
|
-
return RETIRED_PLAYWRIGHT_FILES.map((file) =>
|
|
2036
|
+
return RETIRED_PLAYWRIGHT_FILES.map((file) => path15.resolve(scope.home, ".agents", "skills", "playwright-cli", file));
|
|
4070
2037
|
}
|
|
4071
2038
|
function handoffEnabled(input, kind) {
|
|
4072
2039
|
return (kind === "devtools" ? input.devtoolsMcpEnabled : input.playwrightHandoffEnabled) === true;
|
|
@@ -4075,18 +2042,18 @@ var preparedPiProjectionUninstalls = /* @__PURE__ */ new WeakMap();
|
|
|
4075
2042
|
function projectionScope(scope) {
|
|
4076
2043
|
return {
|
|
4077
2044
|
...scope,
|
|
4078
|
-
settingsFile:
|
|
2045
|
+
settingsFile: path15.join(scope.codingAgentDir, "settings.json")
|
|
4079
2046
|
};
|
|
4080
2047
|
}
|
|
4081
2048
|
function isInside(root, file) {
|
|
4082
|
-
const relative =
|
|
4083
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
2049
|
+
const relative = path15.relative(path15.resolve(root), path15.resolve(file));
|
|
2050
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path15.sep}`) && !path15.isAbsolute(relative);
|
|
4084
2051
|
}
|
|
4085
2052
|
function isManagedPath(scope, file) {
|
|
4086
2053
|
return isInside(scope.home, file) || isInside(scope.codingAgentDir, file);
|
|
4087
2054
|
}
|
|
4088
2055
|
function isAllowedPath(scope, file) {
|
|
4089
|
-
return isManagedPath(scope, file) ||
|
|
2056
|
+
return isManagedPath(scope, file) || path15.resolve(file) === path15.resolve(scope.receiptFile);
|
|
4090
2057
|
}
|
|
4091
2058
|
function projectedPiAdapter(scope) {
|
|
4092
2059
|
const paths = piAdapter.paths(scope.codingAgentDir);
|
|
@@ -4094,7 +2061,7 @@ function projectedPiAdapter(scope) {
|
|
|
4094
2061
|
...piAdapter,
|
|
4095
2062
|
paths: () => ({
|
|
4096
2063
|
...paths,
|
|
4097
|
-
skillsDir:
|
|
2064
|
+
skillsDir: path15.join(scope.home, ".agents", "skills")
|
|
4098
2065
|
})
|
|
4099
2066
|
};
|
|
4100
2067
|
}
|
|
@@ -4138,20 +2105,20 @@ function projectionPlan(input, scope, includePrompt = true) {
|
|
|
4138
2105
|
return actions;
|
|
4139
2106
|
}
|
|
4140
2107
|
function handoffPath(scope, kind) {
|
|
4141
|
-
return
|
|
2108
|
+
return path15.resolve(scope.codingAgentDir, "jorgex-pi", `${kind}.v1.json`);
|
|
4142
2109
|
}
|
|
4143
2110
|
function contentHash(content) {
|
|
4144
2111
|
return createHash("sha256").update(content).digest("hex");
|
|
4145
2112
|
}
|
|
4146
2113
|
function canonicalActionContent(action) {
|
|
4147
|
-
return action.kind === "write" ? action.content :
|
|
2114
|
+
return action.kind === "write" ? action.content : fs11.readFileSync(action.source, "utf8");
|
|
4148
2115
|
}
|
|
4149
2116
|
function hasActionDrift(action, deps) {
|
|
4150
2117
|
return deps.readText(action.target) !== canonicalActionContent(action);
|
|
4151
2118
|
}
|
|
4152
2119
|
function applyActions(actions, deps, handoffs = []) {
|
|
4153
2120
|
for (const action of actions) {
|
|
4154
|
-
const handoff = handoffs.find((entry) =>
|
|
2121
|
+
const handoff = handoffs.find((entry) => path15.resolve(action.target) === entry.file);
|
|
4155
2122
|
if (handoff) {
|
|
4156
2123
|
try {
|
|
4157
2124
|
if (deps.readText(handoff.file) !== handoff.previous) return handoffConflict(handoff.kind, handoff.file);
|
|
@@ -4171,9 +2138,9 @@ function assertPlanContained(plan, scope) {
|
|
|
4171
2138
|
}
|
|
4172
2139
|
}
|
|
4173
2140
|
function receiptFor(plan, scope) {
|
|
4174
|
-
const systemPrompt =
|
|
2141
|
+
const systemPrompt = path15.join(scope.codingAgentDir, "AGENTS.md");
|
|
4175
2142
|
const owned = [...new Set(
|
|
4176
|
-
plan.map((action) =>
|
|
2143
|
+
plan.map((action) => path15.resolve(action.target)).filter((target) => target !== systemPrompt)
|
|
4177
2144
|
)];
|
|
4178
2145
|
const receipt = {
|
|
4179
2146
|
schemaVersion: 1,
|
|
@@ -4186,7 +2153,7 @@ function receiptFor(plan, scope) {
|
|
|
4186
2153
|
owned
|
|
4187
2154
|
};
|
|
4188
2155
|
for (const kind of HANDOFF_KINDS) {
|
|
4189
|
-
const action = plan.find((entry) =>
|
|
2156
|
+
const action = plan.find((entry) => path15.resolve(entry.target) === handoffPath(scope, kind));
|
|
4190
2157
|
if (action) receipt[kind] = { sha256: contentHash(canonicalActionContent(action)) };
|
|
4191
2158
|
}
|
|
4192
2159
|
return receipt;
|
|
@@ -4227,7 +2194,7 @@ function parseReceipt2(raw, expected) {
|
|
|
4227
2194
|
if (matches(expectedOwned)) return { schemaVersion: 1, scope: expected.scope, owned: expectedOwned, ...digests };
|
|
4228
2195
|
const retired = retiredPlaywrightFiles(expected.scope);
|
|
4229
2196
|
if (retired.some((file) => expectedOwned.includes(file))) return null;
|
|
4230
|
-
const skillsRoot = `${
|
|
2197
|
+
const skillsRoot = `${path15.resolve(expected.scope.home, ".agents", "skills")}${path15.sep}`;
|
|
4231
2198
|
const insertion = expectedOwned.findIndex((file) => !file.startsWith(skillsRoot) || file > retired[0]);
|
|
4232
2199
|
const legacyOwned = [...expectedOwned];
|
|
4233
2200
|
legacyOwned.splice(insertion === -1 ? legacyOwned.length : insertion, 0, ...retired);
|
|
@@ -4246,13 +2213,13 @@ function realPiProjectionScope() {
|
|
|
4246
2213
|
return {
|
|
4247
2214
|
kind: "real",
|
|
4248
2215
|
home: HOME,
|
|
4249
|
-
codingAgentDir:
|
|
4250
|
-
receiptFile:
|
|
2216
|
+
codingAgentDir: path15.dirname(piSystemPromptFile()),
|
|
2217
|
+
receiptFile: path15.join(dataDir(), "pi-projection-receipt.json")
|
|
4251
2218
|
};
|
|
4252
2219
|
}
|
|
4253
2220
|
function readTextOnlyIfMissing(file) {
|
|
4254
2221
|
try {
|
|
4255
|
-
return
|
|
2222
|
+
return fs11.readFileSync(file, "utf8");
|
|
4256
2223
|
} catch (error) {
|
|
4257
2224
|
if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "ENOENT") return null;
|
|
4258
2225
|
throw error;
|
|
@@ -4285,13 +2252,13 @@ function manifestOwned(manifest) {
|
|
|
4285
2252
|
return new Set([
|
|
4286
2253
|
...manifest.runtimes.codex?.owned ?? [],
|
|
4287
2254
|
...manifest.runtimes.opencode?.owned ?? []
|
|
4288
|
-
].map((file) =>
|
|
2255
|
+
].map((file) => path15.resolve(file)));
|
|
4289
2256
|
}
|
|
4290
2257
|
function withoutManagedPromptSections(content) {
|
|
4291
2258
|
return removeSystemPromptSections(content);
|
|
4292
2259
|
}
|
|
4293
2260
|
function uniquePaths(paths) {
|
|
4294
|
-
return [...new Set(paths.map((file) =>
|
|
2261
|
+
return [...new Set(paths.map((file) => path15.resolve(file)))];
|
|
4295
2262
|
}
|
|
4296
2263
|
function blocked2(reason, paths, remedy) {
|
|
4297
2264
|
return { kind: "blocked", reason, paths: uniquePaths(paths), remedy };
|
|
@@ -4398,7 +2365,7 @@ function preparePiProjectionUninstall(input, deps) {
|
|
|
4398
2365
|
}
|
|
4399
2366
|
const scope = projectionScope(input.scope);
|
|
4400
2367
|
const expectedReceipt2 = expectedProjectionReceipt(input, scope);
|
|
4401
|
-
const prompt =
|
|
2368
|
+
const prompt = path15.join(scope.codingAgentDir, "AGENTS.md");
|
|
4402
2369
|
let existingPrompt;
|
|
4403
2370
|
try {
|
|
4404
2371
|
existingPrompt = deps.readText(prompt);
|
|
@@ -4411,7 +2378,7 @@ function preparePiProjectionUninstall(input, deps) {
|
|
|
4411
2378
|
return blocked2("projection-prompt-markers", [prompt], error instanceof Error ? error.message : String(error));
|
|
4412
2379
|
}
|
|
4413
2380
|
const promptContent = existingPrompt === null ? null : withoutManagedPromptSections(existingPrompt);
|
|
4414
|
-
const promptUpdate = promptContent === null || promptContent === existingPrompt ? null : { file:
|
|
2381
|
+
const promptUpdate = promptContent === null || promptContent === existingPrompt ? null : { file: path15.resolve(prompt), content: promptContent };
|
|
4415
2382
|
const receiptRead = readProjectionReceipt(scope.receiptFile, deps);
|
|
4416
2383
|
if (receiptRead.kind === "unreadable") return receiptUnreadable(scope.receiptFile);
|
|
4417
2384
|
let ownedToRemove = [];
|
|
@@ -4429,7 +2396,7 @@ function preparePiProjectionUninstall(input, deps) {
|
|
|
4429
2396
|
return cleanupFailure([scope.receiptFile]);
|
|
4430
2397
|
}
|
|
4431
2398
|
ownedToRemove = receipt.owned.filter((owned) => !retained.has(owned));
|
|
4432
|
-
receiptFile =
|
|
2399
|
+
receiptFile = path15.resolve(scope.receiptFile);
|
|
4433
2400
|
backupTargets = [...backupTargets, ...receipt.owned, receiptFile];
|
|
4434
2401
|
}
|
|
4435
2402
|
const handoffs = [];
|
|
@@ -4507,16 +2474,16 @@ function runPiProjectionLifecycle(input, deps) {
|
|
|
4507
2474
|
const prepared = preparePiProjectionUninstall(input, deps);
|
|
4508
2475
|
return prepared.kind === "blocked" ? prepared : completePiProjectionUninstall(prepared.plan, deps);
|
|
4509
2476
|
}
|
|
4510
|
-
const prompt =
|
|
2477
|
+
const prompt = path15.join(scope.codingAgentDir, "AGENTS.md");
|
|
4511
2478
|
try {
|
|
4512
2479
|
assertSystemPromptMarkers(deps.readText(prompt), prompt);
|
|
4513
2480
|
} catch (error) {
|
|
4514
2481
|
return blocked2("projection-prompt-markers", [prompt], error instanceof Error ? error.message : String(error));
|
|
4515
2482
|
}
|
|
4516
|
-
if (input.devtoolsMcpEnabled && (!input.pnpmBin || !
|
|
2483
|
+
if (input.devtoolsMcpEnabled && (!input.pnpmBin || !path15.isAbsolute(input.pnpmBin))) {
|
|
4517
2484
|
return blocked2("projection-devtools-command", [handoffPath(scope, "devtools")], "DevTools requiere un ejecutable pnpm absoluto disponible en PATH.");
|
|
4518
2485
|
}
|
|
4519
|
-
if (input.playwrightHandoffEnabled && (!input.playwrightCliCommand || !
|
|
2486
|
+
if (input.playwrightHandoffEnabled && (!input.playwrightCliCommand || !path15.isAbsolute(input.playwrightCliCommand) || /[\u0000-\u001f\u007f]/.test(input.playwrightCliCommand))) {
|
|
4520
2487
|
return blocked2("projection-playwright-command", [handoffPath(scope, "playwright")], "Playwright requiere un ejecutable absoluto verificado. Abre una terminal nueva tras pnpm setup y reintenta install --playwright.");
|
|
4521
2488
|
}
|
|
4522
2489
|
const plan = projectionPlan(input, scope);
|
|
@@ -4529,12 +2496,12 @@ function runPiProjectionLifecycle(input, deps) {
|
|
|
4529
2496
|
const expectedReceipt2 = receiptContent(receipt);
|
|
4530
2497
|
const drifted = plan.filter((action) => hasActionDrift(action, deps));
|
|
4531
2498
|
for (const { kind, file } of checked.handoffs) {
|
|
4532
|
-
if (handoffEnabled(input, kind) && checked.previous?.[kind]?.sha256 !== receipt[kind]?.sha256 && !drifted.some((action) =>
|
|
2499
|
+
if (handoffEnabled(input, kind) && checked.previous?.[kind]?.sha256 !== receipt[kind]?.sha256 && !drifted.some((action) => path15.resolve(action.target) === file)) {
|
|
4533
2500
|
return input.operation === "doctor" ? { kind: "drift", paths: [file] } : handoffConflict(kind, file);
|
|
4534
2501
|
}
|
|
4535
2502
|
}
|
|
4536
2503
|
if (input.operation === "doctor") {
|
|
4537
|
-
const paths = drifted.map((action) =>
|
|
2504
|
+
const paths = drifted.map((action) => path15.resolve(action.target));
|
|
4538
2505
|
paths.push(...removedHandoffs.map(({ file }) => file), ...retiredSkills);
|
|
4539
2506
|
const currentSettings2 = deps.readText(scope.settingsFile);
|
|
4540
2507
|
if (currentSettings2 === null || filterProjectedPiPackage(currentSettings2, input.packageSource) !== currentSettings2) {
|
|
@@ -4593,12 +2560,12 @@ function runPiProjectionLifecycle(input, deps) {
|
|
|
4593
2560
|
return { kind: "synced", changed: drifted.length > 0 || packageWillChange || receiptChanged || removedHandoffs.length > 0 };
|
|
4594
2561
|
}
|
|
4595
2562
|
function systemProjectionLifecycle(input) {
|
|
4596
|
-
const targetRoot = input.targetDir === void 0 ? null :
|
|
2563
|
+
const targetRoot = input.targetDir === void 0 ? null : path15.resolve(input.targetDir);
|
|
4597
2564
|
const scope = targetRoot === null ? realPiProjectionScope() : {
|
|
4598
2565
|
kind: "target-dir",
|
|
4599
|
-
home:
|
|
4600
|
-
codingAgentDir:
|
|
4601
|
-
receiptFile:
|
|
2566
|
+
home: path15.join(targetRoot, "home"),
|
|
2567
|
+
codingAgentDir: path15.dirname(piSystemPromptFile(targetRoot)),
|
|
2568
|
+
receiptFile: path15.join(targetRoot, "state", "pi-projection-receipt.json")
|
|
4602
2569
|
};
|
|
4603
2570
|
return {
|
|
4604
2571
|
input: {
|
|
@@ -4619,14 +2586,14 @@ function systemProjectionLifecycle(input) {
|
|
|
4619
2586
|
backup: (paths) => createBackup(
|
|
4620
2587
|
paths,
|
|
4621
2588
|
`pi-projection-${input.operation}`,
|
|
4622
|
-
targetRoot === null ? void 0 :
|
|
2589
|
+
targetRoot === null ? void 0 : path15.join(targetRoot, "backups")
|
|
4623
2590
|
),
|
|
4624
2591
|
writeText,
|
|
4625
2592
|
copyFile,
|
|
4626
2593
|
removeFile: (file) => {
|
|
4627
|
-
|
|
4628
|
-
if (retiredPlaywrightFiles(scope).includes(
|
|
4629
|
-
pruneEmptyDirs(file,
|
|
2594
|
+
fs11.rmSync(file, { force: true });
|
|
2595
|
+
if (retiredPlaywrightFiles(scope).includes(path15.resolve(file))) {
|
|
2596
|
+
pruneEmptyDirs(file, path15.join(scope.home, ".agents", "skills"));
|
|
4630
2597
|
}
|
|
4631
2598
|
},
|
|
4632
2599
|
readManifest: targetRoot === null ? readManifest : () => ({ runtimes: {} })
|
|
@@ -4729,7 +2696,7 @@ async function runUninstall(opts) {
|
|
|
4729
2696
|
if (!detection.installed) continue;
|
|
4730
2697
|
const keepCtx = makeContext(keep, detection.configDir);
|
|
4731
2698
|
if (!keepCtx) continue;
|
|
4732
|
-
for (const action of buildContentPlan(keep, keepCtx)) retained.add(
|
|
2699
|
+
for (const action of buildContentPlan(keep, keepCtx)) retained.add(path16.resolve(action.target));
|
|
4733
2700
|
}
|
|
4734
2701
|
}
|
|
4735
2702
|
for (const id of opts.runtimes) {
|
|
@@ -4755,16 +2722,20 @@ async function runUninstall(opts) {
|
|
|
4755
2722
|
exitCode = 1;
|
|
4756
2723
|
continue;
|
|
4757
2724
|
}
|
|
4758
|
-
const mergedTargets = new Set(unmerge.map((a) =>
|
|
2725
|
+
const mergedTargets = new Set(unmerge.map((a) => path16.resolve(a.target)));
|
|
4759
2726
|
const usingRealConfig = opts.targetDir === void 0;
|
|
4760
2727
|
const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
|
|
4761
|
-
const pruneRoot = usingRealConfig ? HOME :
|
|
2728
|
+
const pruneRoot = usingRealConfig ? HOME : path16.dirname(configDir);
|
|
4762
2729
|
const planTargets = [
|
|
4763
|
-
.../* @__PURE__ */ new Set([...buildContentPlan(adapter, ctx).map((a) =>
|
|
4764
|
-
].filter((t) => !mergedTargets.has(t) &&
|
|
4765
|
-
const deleteTargets = planTargets.filter(
|
|
4766
|
-
(
|
|
4767
|
-
|
|
2730
|
+
.../* @__PURE__ */ new Set([...buildContentPlan(adapter, ctx).map((a) => path16.resolve(a.target)), ...prevOwned.map((t) => path16.resolve(t))])
|
|
2731
|
+
].filter((t) => !mergedTargets.has(t) && fs12.existsSync(t));
|
|
2732
|
+
const deleteTargets = planTargets.filter((t) => {
|
|
2733
|
+
if (retained.has(t) || !isContainedIn(t, pruneRoot)) return false;
|
|
2734
|
+
if (path16.basename(t) !== "engram.ts") return true;
|
|
2735
|
+
const state = inspectOpencodePluginFile(t);
|
|
2736
|
+
if (state === "official" || state === "unknown") return false;
|
|
2737
|
+
return !ctx.preserveEngram;
|
|
2738
|
+
});
|
|
4768
2739
|
const sharedKept = planTargets.length - deleteTargets.length;
|
|
4769
2740
|
p2.log.step(`${adapter.name} \u2192 ${configDir}`);
|
|
4770
2741
|
p2.log.info(`${deleteTargets.length} archivos a borrar, ${unmerge.length} archivos compartidos a limpiar`);
|
|
@@ -4773,7 +2744,7 @@ async function runUninstall(opts) {
|
|
|
4773
2744
|
let backup;
|
|
4774
2745
|
try {
|
|
4775
2746
|
backup = createBackup(
|
|
4776
|
-
[...deleteTargets, ...unmerge.map((a) => a.target).filter((t) =>
|
|
2747
|
+
[...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs12.existsSync(t))],
|
|
4777
2748
|
`uninstall-${id}`
|
|
4778
2749
|
);
|
|
4779
2750
|
} catch (error) {
|
|
@@ -4783,13 +2754,13 @@ async function runUninstall(opts) {
|
|
|
4783
2754
|
}
|
|
4784
2755
|
if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
4785
2756
|
for (const target of deleteTargets) {
|
|
4786
|
-
|
|
2757
|
+
fs12.rmSync(target, { force: true });
|
|
4787
2758
|
pruneEmptyDirs(target, pruneRoot);
|
|
4788
2759
|
}
|
|
4789
2760
|
for (const action of unmerge) {
|
|
4790
2761
|
if (action.kind !== "write") continue;
|
|
4791
2762
|
if (action.content.trim() === "") {
|
|
4792
|
-
|
|
2763
|
+
fs12.rmSync(action.target, { force: true });
|
|
4793
2764
|
} else {
|
|
4794
2765
|
writeText(action.target, action.content);
|
|
4795
2766
|
}
|
|
@@ -4843,12 +2814,12 @@ async function runUninstall(opts) {
|
|
|
4843
2814
|
}
|
|
4844
2815
|
|
|
4845
2816
|
// src/doctor.ts
|
|
4846
|
-
import
|
|
4847
|
-
import
|
|
2817
|
+
import path17 from "path";
|
|
2818
|
+
import fs13 from "fs";
|
|
4848
2819
|
import * as p3 from "@clack/prompts";
|
|
4849
2820
|
function readDoctorTextIfExists(file) {
|
|
4850
2821
|
try {
|
|
4851
|
-
return
|
|
2822
|
+
return fs13.readFileSync(file, "utf8");
|
|
4852
2823
|
} catch (error) {
|
|
4853
2824
|
if (error.code === "ENOENT") return null;
|
|
4854
2825
|
throw error;
|
|
@@ -4857,12 +2828,12 @@ function readDoctorTextIfExists(file) {
|
|
|
4857
2828
|
var STALE_PERMISSIONS_MARKER = "differs from the stack default and was left untouched";
|
|
4858
2829
|
var UPGRADE_PERMISSIONS_REMEDY = "jorgex-stack sync --upgrade-permissions --dry-run";
|
|
4859
2830
|
function piAgentDir(targetDir) {
|
|
4860
|
-
return targetDir === void 0 ? process.env.PI_CODING_AGENT_DIR ??
|
|
2831
|
+
return targetDir === void 0 ? process.env.PI_CODING_AGENT_DIR ?? path17.join(HOME, ".pi", "agent") : path17.join(targetDir, "pi-agent");
|
|
4861
2832
|
}
|
|
4862
2833
|
function reportPiPermissions(targetDir) {
|
|
4863
2834
|
const agentDir = piAgentDir(targetDir);
|
|
4864
|
-
const configFile =
|
|
4865
|
-
const receiptFile =
|
|
2835
|
+
const configFile = path17.join(agentDir, "extensions", "pi-permission-system", "config.json");
|
|
2836
|
+
const receiptFile = path17.join(agentDir, "jorgex-pi", "permissions-lifecycle.v1.json");
|
|
4866
2837
|
let config;
|
|
4867
2838
|
let receipt;
|
|
4868
2839
|
try {
|
|
@@ -4885,11 +2856,6 @@ function reportPiPermissions(targetDir) {
|
|
|
4885
2856
|
);
|
|
4886
2857
|
return 1;
|
|
4887
2858
|
}
|
|
4888
|
-
function engramVersion(bin) {
|
|
4889
|
-
const out = runDetectedBin(bin, ["--version"], 5e3);
|
|
4890
|
-
if (out === null) return null;
|
|
4891
|
-
return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
|
|
4892
|
-
}
|
|
4893
2859
|
function resolvePlaywrightDoctorState(input) {
|
|
4894
2860
|
if (input.enabled !== true) return { status: "disabled" };
|
|
4895
2861
|
if (input.cli.status === "not-in-path") return { status: "not-in-path" };
|
|
@@ -4904,7 +2870,7 @@ function resolvePlaywrightDoctorState(input) {
|
|
|
4904
2870
|
return { status: "healthy" };
|
|
4905
2871
|
}
|
|
4906
2872
|
function context7KeyConfigured(id, configDir) {
|
|
4907
|
-
const file = id === "codex" ?
|
|
2873
|
+
const file = id === "codex" ? path17.join(configDir, "config.toml") : id === "claude-code" ? path17.join(path17.dirname(configDir), `${path17.basename(configDir)}.json`) : path17.join(configDir, "opencode.json");
|
|
4908
2874
|
const content = readTextIfExists(file);
|
|
4909
2875
|
if (content === null) return null;
|
|
4910
2876
|
const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
|
|
@@ -4924,7 +2890,7 @@ function reportWritingStyle(options, style, mode) {
|
|
|
4924
2890
|
for (const id of runtimes) {
|
|
4925
2891
|
const adapter = id === "pi" ? piAdapter : ADAPTERS[id];
|
|
4926
2892
|
if (adapter === void 0) continue;
|
|
4927
|
-
const configDir = id === "pi" ? options.targetDir === void 0 ? process.env.PI_CODING_AGENT_DIR ??
|
|
2893
|
+
const configDir = id === "pi" ? options.targetDir === void 0 ? process.env.PI_CODING_AGENT_DIR ?? path17.join(HOME, ".pi", "agent") : path17.join(options.targetDir, "pi-agent") : options.targetDir ?? ADAPTERS[id].detect().configDir;
|
|
4928
2894
|
const file = adapter.paths(configDir).systemPromptFile;
|
|
4929
2895
|
try {
|
|
4930
2896
|
const content = readDoctorTextIfExists(file) ?? "";
|
|
@@ -4939,7 +2905,7 @@ function reportWritingStyle(options, style, mode) {
|
|
|
4939
2905
|
problems++;
|
|
4940
2906
|
}
|
|
4941
2907
|
if (id === "codex") {
|
|
4942
|
-
const override =
|
|
2908
|
+
const override = path17.join(configDir, "AGENTS.override.md");
|
|
4943
2909
|
if ((readDoctorTextIfExists(override) ?? "").trim() !== "") {
|
|
4944
2910
|
p3.log.warn(`Codex: ${override} no vac\xEDo puede ocultar el AGENTS.md gestionado; rev\xEDsalo sin modificarlo autom\xE1ticamente.`);
|
|
4945
2911
|
problems++;
|
|
@@ -4952,6 +2918,34 @@ function reportWritingStyle(options, style, mode) {
|
|
|
4952
2918
|
}
|
|
4953
2919
|
return problems;
|
|
4954
2920
|
}
|
|
2921
|
+
async function verifyOfficialForRuntime(runtime, configDir, engramBin, homeDir, isExplicitClaudeConfigDir) {
|
|
2922
|
+
if (runtime === "claude-code") {
|
|
2923
|
+
const { verifyOfficialSetup: verifyOfficialSetup2 } = await import("./claude-code-WY4JJAXP.js");
|
|
2924
|
+
const explicit = isExplicitClaudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR !== void 0;
|
|
2925
|
+
const report2 = await verifyOfficialSetup2({ configDir, engramBin, homeDir, isExplicitClaudeConfigDir: explicit });
|
|
2926
|
+
return {
|
|
2927
|
+
ok: report2.ok,
|
|
2928
|
+
layers: report2.layers,
|
|
2929
|
+
...report2.reason === void 0 ? {} : { reason: report2.reason }
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
if (runtime === "codex") {
|
|
2933
|
+
const { verifyOfficialSetup: verifyOfficialSetup2 } = await import("./codex-WSWHXYJ3.js");
|
|
2934
|
+
const report2 = await verifyOfficialSetup2({ configDir, engramBin });
|
|
2935
|
+
return {
|
|
2936
|
+
ok: report2.ok,
|
|
2937
|
+
layers: report2.layers,
|
|
2938
|
+
...report2.reason === void 0 ? {} : { reason: report2.reason }
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
const { verifyOfficialSetup } = await import("./opencode-NUDGK6J7.js");
|
|
2942
|
+
const report = await verifyOfficialSetup({ configDir, engramBin });
|
|
2943
|
+
return {
|
|
2944
|
+
ok: report.ok,
|
|
2945
|
+
layers: report.layers,
|
|
2946
|
+
...report.reason === void 0 ? {} : { reason: report.reason }
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
4955
2949
|
async function runDoctor(options = {}) {
|
|
4956
2950
|
p3.intro("jorgex-stack doctor");
|
|
4957
2951
|
let writingStyle;
|
|
@@ -4973,6 +2967,7 @@ async function runDoctor(options = {}) {
|
|
|
4973
2967
|
return problems > 0 ? 1 : 0;
|
|
4974
2968
|
}
|
|
4975
2969
|
const engramBin = detectEngram();
|
|
2970
|
+
let engramBinAvailable = false;
|
|
4976
2971
|
if (engramBin === null) {
|
|
4977
2972
|
p3.log.warn("Engram: NO detectado. El protocolo de memoria no funcionar\xE1 \u2014 inst\xE1lalo: github.com/Gentleman-Programming/engram");
|
|
4978
2973
|
problems++;
|
|
@@ -4983,15 +2978,17 @@ async function runDoctor(options = {}) {
|
|
|
4983
2978
|
problems++;
|
|
4984
2979
|
} else {
|
|
4985
2980
|
p3.log.success(`Engram: ${version} (${engramBin})`);
|
|
2981
|
+
engramBinAvailable = true;
|
|
4986
2982
|
}
|
|
4987
2983
|
}
|
|
4988
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
4989
|
-
const engramDb =
|
|
4990
|
-
if (
|
|
4991
|
-
const sizeMb = (
|
|
2984
|
+
const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path17.join(HOME, ".engram");
|
|
2985
|
+
const engramDb = path17.join(engramDataDir, "engram.db");
|
|
2986
|
+
if (fs13.existsSync(engramDb)) {
|
|
2987
|
+
const sizeMb = (fs13.statSync(engramDb).size / 1024 / 1024).toFixed(1);
|
|
4992
2988
|
p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
|
|
4993
2989
|
}
|
|
4994
|
-
|
|
2990
|
+
const officialResults = [];
|
|
2991
|
+
if (!fs13.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
|
|
4995
2992
|
const preferenceErrors = browserPreferenceErrors();
|
|
4996
2993
|
const primaryOwnershipError = primaryModelOwnershipError();
|
|
4997
2994
|
if (primaryOwnershipError !== null) {
|
|
@@ -5090,27 +3087,64 @@ async function runDoctor(options = {}) {
|
|
|
5090
3087
|
p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
|
|
5091
3088
|
problems++;
|
|
5092
3089
|
}
|
|
5093
|
-
if (adapter.id === "codex" &&
|
|
3090
|
+
if (adapter.id === "codex" && fs13.existsSync(path17.join(detection.configDir, "hooks.json"))) {
|
|
5094
3091
|
p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
|
|
5095
3092
|
}
|
|
5096
3093
|
const key = context7KeyConfigured(adapter.id, detection.configDir);
|
|
5097
3094
|
if (key === false) p3.log.info(`${adapter.name}: context7 sin key (opcional \u2014 con\xE9ctala cuando quieras).`);
|
|
3095
|
+
try {
|
|
3096
|
+
const explicitClaude = adapter.id === "claude-code" ? process.env.CLAUDE_CONFIG_DIR !== void 0 : void 0;
|
|
3097
|
+
const setup = await verifyOfficialForRuntime(
|
|
3098
|
+
adapter.id,
|
|
3099
|
+
detection.configDir,
|
|
3100
|
+
engramBin ?? "",
|
|
3101
|
+
HOME,
|
|
3102
|
+
explicitClaude
|
|
3103
|
+
);
|
|
3104
|
+
const exposed = setup.layers.includes("mcp") && engramBinAvailable;
|
|
3105
|
+
officialResults.push({ runtime: adapter.id, ok: setup.ok, layers: setup.layers, exposed });
|
|
3106
|
+
if (!setup.ok) {
|
|
3107
|
+
const reason = typeof setup.reason === "string" ? setup.reason.trim() : "";
|
|
3108
|
+
if (reason !== "") {
|
|
3109
|
+
p3.log.warn(`${adapter.name}: ${reason}`);
|
|
3110
|
+
} else {
|
|
3111
|
+
p3.log.warn(
|
|
3112
|
+
`${adapter.name}: setup oficial Engram incompleto (${setup.layers.join(", ")}) \u2192 ejecuta install.`
|
|
3113
|
+
);
|
|
3114
|
+
}
|
|
3115
|
+
problems++;
|
|
3116
|
+
}
|
|
3117
|
+
} catch (error) {
|
|
3118
|
+
p3.log.warn(
|
|
3119
|
+
`${adapter.name}: setup oficial Engram no verificable (${error instanceof Error ? error.message : String(error)}).`
|
|
3120
|
+
);
|
|
3121
|
+
problems++;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
if (officialResults.length === 0) {
|
|
3125
|
+
p3.log.info("Engram official setup: no hay runtimes instalados seleccionados para verificar.");
|
|
3126
|
+
p3.log.info("Engram official exposure: no verificado (sin runtimes instalados seleccionados).");
|
|
3127
|
+
} else {
|
|
3128
|
+
const setupSummary = officialResults.map((r) => `${r.runtime}=${r.ok ? "ok" : "missing"} (${r.layers.join(",")})`).join("; ");
|
|
3129
|
+
const exposureSummary = officialResults.map((r) => `${r.runtime}=${r.exposed ? "exposed" : "not-exposed"}`).join("; ");
|
|
3130
|
+
p3.log.info(`Engram official setup: ${setupSummary}.`);
|
|
3131
|
+
p3.log.info(`Engram official exposure: ${exposureSummary} (MCP registrado y binario disponible; sin certificar carga en runtime).`);
|
|
5098
3132
|
}
|
|
5099
3133
|
p3.outro(problems === 0 ? "Doctor: todo sano." : `Doctor: ${problems} aviso(s) \u2014 revisa arriba.`);
|
|
5100
3134
|
return problems > 0 ? 1 : 0;
|
|
5101
3135
|
}
|
|
5102
3136
|
|
|
5103
3137
|
// src/update.ts
|
|
5104
|
-
import
|
|
5105
|
-
import
|
|
3138
|
+
import fs16 from "fs";
|
|
3139
|
+
import path20 from "path";
|
|
5106
3140
|
import os3 from "os";
|
|
5107
|
-
import { execFileSync as
|
|
3141
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
5108
3142
|
import * as p4 from "@clack/prompts";
|
|
5109
3143
|
|
|
5110
3144
|
// src/lib/github.ts
|
|
5111
|
-
import
|
|
5112
|
-
import
|
|
5113
|
-
import { execFileSync as
|
|
3145
|
+
import fs14 from "fs";
|
|
3146
|
+
import path18 from "path";
|
|
3147
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
5114
3148
|
import os2 from "os";
|
|
5115
3149
|
import { Readable } from "stream";
|
|
5116
3150
|
import { pipeline } from "stream/promises";
|
|
@@ -5179,19 +3213,19 @@ async function latestGithubCommit(repo) {
|
|
|
5179
3213
|
}
|
|
5180
3214
|
}
|
|
5181
3215
|
function validateExtractedTree(destDir) {
|
|
5182
|
-
const resolved =
|
|
3216
|
+
const resolved = path18.resolve(destDir);
|
|
5183
3217
|
const walk = (dir) => {
|
|
5184
3218
|
let entries;
|
|
5185
3219
|
try {
|
|
5186
|
-
entries =
|
|
3220
|
+
entries = fs14.readdirSync(dir, { withFileTypes: true });
|
|
5187
3221
|
} catch {
|
|
5188
3222
|
return false;
|
|
5189
3223
|
}
|
|
5190
3224
|
for (const entry of entries) {
|
|
5191
|
-
const full =
|
|
3225
|
+
const full = path18.join(dir, entry.name);
|
|
5192
3226
|
let stat;
|
|
5193
3227
|
try {
|
|
5194
|
-
stat =
|
|
3228
|
+
stat = fs14.lstatSync(full);
|
|
5195
3229
|
} catch {
|
|
5196
3230
|
return false;
|
|
5197
3231
|
}
|
|
@@ -5207,15 +3241,15 @@ function validateExtractedTree(destDir) {
|
|
|
5207
3241
|
}
|
|
5208
3242
|
function resolveTarBin() {
|
|
5209
3243
|
if (process.platform !== "win32") return "tar";
|
|
5210
|
-
const winTar =
|
|
5211
|
-
return
|
|
3244
|
+
const winTar = path18.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
|
|
3245
|
+
return fs14.existsSync(winTar) ? winTar : "tar";
|
|
5212
3246
|
}
|
|
5213
3247
|
async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
5214
3248
|
const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
|
|
5215
|
-
const tmp =
|
|
3249
|
+
const tmp = path18.join(os2.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
|
|
5216
3250
|
const fail = (reason) => {
|
|
5217
3251
|
try {
|
|
5218
|
-
|
|
3252
|
+
fs14.rmSync(destDir, { recursive: true, force: true });
|
|
5219
3253
|
} catch {
|
|
5220
3254
|
}
|
|
5221
3255
|
return { ok: false, reason };
|
|
@@ -5234,42 +3268,42 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
|
5234
3268
|
if (!res.body) return fail("respuesta HTTP sin cuerpo");
|
|
5235
3269
|
await pipeline(
|
|
5236
3270
|
Readable.fromWeb(res.body),
|
|
5237
|
-
|
|
3271
|
+
fs14.createWriteStream(tmp)
|
|
5238
3272
|
);
|
|
5239
|
-
|
|
5240
|
-
|
|
3273
|
+
fs14.rmSync(destDir, { recursive: true, force: true });
|
|
3274
|
+
fs14.mkdirSync(destDir, { recursive: true });
|
|
5241
3275
|
try {
|
|
5242
|
-
|
|
3276
|
+
execFileSync2(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
|
|
5243
3277
|
} catch (err) {
|
|
5244
3278
|
const e = err;
|
|
5245
3279
|
const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
|
|
5246
3280
|
return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
|
|
5247
3281
|
}
|
|
5248
|
-
const resolvedDest =
|
|
5249
|
-
const validateRoot = validateSubdir ?
|
|
3282
|
+
const resolvedDest = path18.resolve(destDir);
|
|
3283
|
+
const validateRoot = validateSubdir ? path18.resolve(resolvedDest, validateSubdir) : resolvedDest;
|
|
5250
3284
|
if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
|
|
5251
3285
|
return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
|
|
5252
3286
|
}
|
|
5253
|
-
if (
|
|
3287
|
+
if (fs14.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
|
|
5254
3288
|
return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
|
|
5255
3289
|
}
|
|
5256
|
-
const validated =
|
|
3290
|
+
const validated = fs14.existsSync(validateRoot);
|
|
5257
3291
|
return { ok: true, validated };
|
|
5258
3292
|
} catch (err) {
|
|
5259
3293
|
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
5260
3294
|
return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
|
|
5261
3295
|
} finally {
|
|
5262
3296
|
try {
|
|
5263
|
-
|
|
3297
|
+
fs14.rmSync(tmp, { force: true });
|
|
5264
3298
|
} catch {
|
|
5265
3299
|
}
|
|
5266
3300
|
}
|
|
5267
3301
|
}
|
|
5268
3302
|
|
|
5269
3303
|
// src/lib/skill-update.ts
|
|
5270
|
-
import
|
|
5271
|
-
import
|
|
5272
|
-
import { execFileSync as
|
|
3304
|
+
import fs15 from "fs";
|
|
3305
|
+
import path19 from "path";
|
|
3306
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
5273
3307
|
var PROTECTED_SKILLS = /* @__PURE__ */ new Set([
|
|
5274
3308
|
"agent-delegation",
|
|
5275
3309
|
"lean-code",
|
|
@@ -5279,8 +3313,8 @@ var PROTECTED_SKILLS = /* @__PURE__ */ new Set([
|
|
|
5279
3313
|
"xreview"
|
|
5280
3314
|
]);
|
|
5281
3315
|
function sameTextContentNormalized(a, b) {
|
|
5282
|
-
const ba =
|
|
5283
|
-
const bb =
|
|
3316
|
+
const ba = fs15.readFileSync(a);
|
|
3317
|
+
const bb = fs15.readFileSync(b);
|
|
5284
3318
|
if (ba.equals(bb)) return true;
|
|
5285
3319
|
const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
|
|
5286
3320
|
const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
|
|
@@ -5288,10 +3322,10 @@ function sameTextContentNormalized(a, b) {
|
|
|
5288
3322
|
}
|
|
5289
3323
|
function diffSkillDirs(upstreamDir, localDir) {
|
|
5290
3324
|
const upstreamFiles = new Set(
|
|
5291
|
-
listFilesRecursive(upstreamDir).map((f) =>
|
|
3325
|
+
listFilesRecursive(upstreamDir).map((f) => path19.relative(upstreamDir, f))
|
|
5292
3326
|
);
|
|
5293
3327
|
const localFiles = new Set(
|
|
5294
|
-
listFilesRecursive(localDir).map((f) =>
|
|
3328
|
+
listFilesRecursive(localDir).map((f) => path19.relative(localDir, f))
|
|
5295
3329
|
);
|
|
5296
3330
|
const added = [];
|
|
5297
3331
|
const modified = [];
|
|
@@ -5299,7 +3333,7 @@ function diffSkillDirs(upstreamDir, localDir) {
|
|
|
5299
3333
|
for (const rel of upstreamFiles) {
|
|
5300
3334
|
if (!localFiles.has(rel)) {
|
|
5301
3335
|
added.push(rel);
|
|
5302
|
-
} else if (!sameTextContentNormalized(
|
|
3336
|
+
} else if (!sameTextContentNormalized(path19.join(upstreamDir, rel), path19.join(localDir, rel))) {
|
|
5303
3337
|
modified.push(rel);
|
|
5304
3338
|
}
|
|
5305
3339
|
}
|
|
@@ -5317,7 +3351,7 @@ function diffSkillDirs(upstreamDir, localDir) {
|
|
|
5317
3351
|
var DIFF_MAX_LINES = 400;
|
|
5318
3352
|
function renderSkillDiff(upstreamDir, localDir) {
|
|
5319
3353
|
try {
|
|
5320
|
-
|
|
3354
|
+
execFileSync3("git", ["diff", "--no-index", "--stat", "--", localDir, upstreamDir], {
|
|
5321
3355
|
stdio: "pipe",
|
|
5322
3356
|
encoding: "utf8"
|
|
5323
3357
|
});
|
|
@@ -5334,7 +3368,7 @@ function renderSkillDiff(upstreamDir, localDir) {
|
|
|
5334
3368
|
const stat = se.stdout;
|
|
5335
3369
|
let fullDiff = "";
|
|
5336
3370
|
try {
|
|
5337
|
-
|
|
3371
|
+
execFileSync3("git", ["diff", "--no-index", "--", localDir, upstreamDir], {
|
|
5338
3372
|
stdio: "pipe",
|
|
5339
3373
|
encoding: "utf8"
|
|
5340
3374
|
});
|
|
@@ -5357,12 +3391,12 @@ ${fullDiff}` : stat;
|
|
|
5357
3391
|
function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
5358
3392
|
const upstreamsFilePath = opts?.upstreamsFilePath;
|
|
5359
3393
|
const localSkillsRoot = opts?.localSkillsRoot;
|
|
5360
|
-
const
|
|
3394
|
+
const backupsRoot = opts?.backupsRoot;
|
|
5361
3395
|
if (PROTECTED_SKILLS.has(name)) {
|
|
5362
3396
|
throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
|
|
5363
3397
|
}
|
|
5364
|
-
const upstreamsFile = upstreamsFilePath ??
|
|
5365
|
-
const raw =
|
|
3398
|
+
const upstreamsFile = upstreamsFilePath ?? path19.join(path19.dirname(stackRoot()), "upstreams.json");
|
|
3399
|
+
const raw = fs15.readFileSync(upstreamsFile, "utf8");
|
|
5366
3400
|
const data = JSON.parse(raw);
|
|
5367
3401
|
const skillEntry = data?.skills?.[name];
|
|
5368
3402
|
if (!skillEntry) {
|
|
@@ -5371,34 +3405,34 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
5371
3405
|
if (skillEntry.kind === "release") {
|
|
5372
3406
|
throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
|
|
5373
3407
|
}
|
|
5374
|
-
const skillsRoot = localSkillsRoot ??
|
|
5375
|
-
const localSkillDir =
|
|
3408
|
+
const skillsRoot = localSkillsRoot ?? path19.join(stackRoot(), "skills");
|
|
3409
|
+
const localSkillDir = path19.join(skillsRoot, name);
|
|
5376
3410
|
const localFiles = listFilesRecursive(localSkillDir);
|
|
5377
3411
|
if (localFiles.length > 0) {
|
|
5378
|
-
createBackup(localFiles, `skill-update-${name}`,
|
|
3412
|
+
createBackup(localFiles, `skill-update-${name}`, backupsRoot);
|
|
5379
3413
|
}
|
|
5380
3414
|
const stagingDir = `${localSkillDir}.staging-${process.pid}`;
|
|
5381
3415
|
try {
|
|
5382
3416
|
const upstreamFiles = listFilesRecursive(upstreamSkillDir);
|
|
5383
3417
|
for (const src of upstreamFiles) {
|
|
5384
|
-
const st =
|
|
3418
|
+
const st = fs15.lstatSync(src);
|
|
5385
3419
|
if (st.isSymbolicLink()) {
|
|
5386
3420
|
throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
|
|
5387
3421
|
}
|
|
5388
|
-
const rel =
|
|
5389
|
-
const dest =
|
|
5390
|
-
ensureDir(
|
|
3422
|
+
const rel = path19.relative(upstreamSkillDir, src);
|
|
3423
|
+
const dest = path19.join(stagingDir, rel);
|
|
3424
|
+
ensureDir(path19.dirname(dest));
|
|
5391
3425
|
copyFile(src, dest);
|
|
5392
3426
|
}
|
|
5393
3427
|
const oldDir = `${localSkillDir}.old-${process.pid}`;
|
|
5394
|
-
if (
|
|
5395
|
-
|
|
3428
|
+
if (fs15.existsSync(localSkillDir)) {
|
|
3429
|
+
fs15.renameSync(localSkillDir, oldDir);
|
|
5396
3430
|
}
|
|
5397
|
-
|
|
5398
|
-
|
|
3431
|
+
fs15.renameSync(stagingDir, localSkillDir);
|
|
3432
|
+
fs15.rmSync(oldDir, { recursive: true, force: true });
|
|
5399
3433
|
} catch (err) {
|
|
5400
3434
|
try {
|
|
5401
|
-
|
|
3435
|
+
fs15.rmSync(stagingDir, { recursive: true, force: true });
|
|
5402
3436
|
} catch {
|
|
5403
3437
|
}
|
|
5404
3438
|
throw err;
|
|
@@ -5412,8 +3446,8 @@ function rateLimitHint(prefix) {
|
|
|
5412
3446
|
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.`;
|
|
5413
3447
|
}
|
|
5414
3448
|
function loadUpstreams() {
|
|
5415
|
-
const file =
|
|
5416
|
-
return JSON.parse(
|
|
3449
|
+
const file = path20.join(path20.dirname(stackRoot()), "upstreams.json");
|
|
3450
|
+
return JSON.parse(fs16.readFileSync(file, "utf8"));
|
|
5417
3451
|
}
|
|
5418
3452
|
function skillsToScan(maintainer, upstreams) {
|
|
5419
3453
|
return maintainer ? Object.keys(upstreams.skills) : [];
|
|
@@ -5442,11 +3476,39 @@ function complementsToScan(upstreams) {
|
|
|
5442
3476
|
}
|
|
5443
3477
|
function resolveComplementUpdateCheck(name, info, upstream) {
|
|
5444
3478
|
const pinned = info.version ?? null;
|
|
5445
|
-
|
|
3479
|
+
const strategy = info.strategy;
|
|
3480
|
+
if (strategy === "provider-managed") {
|
|
3481
|
+
if (!pinned) {
|
|
3482
|
+
if (upstream === null)
|
|
3483
|
+
return {
|
|
3484
|
+
level: "info",
|
|
3485
|
+
message: `${name}: provider-managed sin versi\xF3n fijada (rolling aceptado; no se pudo consultar el upstream).`
|
|
3486
|
+
};
|
|
3487
|
+
return {
|
|
3488
|
+
level: "info",
|
|
3489
|
+
message: `${name}: provider-managed ${upstream} (rolling aceptado; el provider gestiona la versi\xF3n).`
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
if (upstream === null)
|
|
3493
|
+
return { level: "info", message: `${name}: provider-managed ${pinned} (no se pudo consultar el upstream).` };
|
|
3494
|
+
if (upstream === pinned)
|
|
3495
|
+
return { level: "success", message: `${name}: al d\xEDa con ${pinned} (provider-managed).` };
|
|
3496
|
+
return {
|
|
3497
|
+
level: "info",
|
|
3498
|
+
message: `${name}: provider-managed (referencia ${pinned}, upstream observado ${upstream}; el provider gestiona la versi\xF3n, sin re-pin requerido).`
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
if (!pinned) {
|
|
3502
|
+
if (strategy === "exact")
|
|
3503
|
+
return {
|
|
3504
|
+
level: "error",
|
|
3505
|
+
message: `${name}: estrategia exact sin pin en upstreams.json \u2014 fija la versi\xF3n revisada antes de instalar.`
|
|
3506
|
+
};
|
|
5446
3507
|
return {
|
|
5447
3508
|
level: "warn",
|
|
5448
3509
|
message: `${name}: sin pin en upstreams.json \u2014 fija la versi\xF3n revisada antes de instalar.`
|
|
5449
3510
|
};
|
|
3511
|
+
}
|
|
5450
3512
|
if (upstream === null)
|
|
5451
3513
|
return { level: "info", message: `${name}: pin ${pinned} (no se pudo consultar el upstream).` };
|
|
5452
3514
|
if (upstream === pinned)
|
|
@@ -5580,7 +3642,7 @@ async function runUpdateCheck(localVersion, includeBrowserState = true) {
|
|
|
5580
3642
|
for (const { name, upstream } of complementQueries) {
|
|
5581
3643
|
const report = resolveComplementUpdateCheck(name, upstreams.complements[name], upstream);
|
|
5582
3644
|
p4.log[report.level](report.message);
|
|
5583
|
-
if (report.level === "warn") movedComplement = true;
|
|
3645
|
+
if (report.level === "warn" || report.level === "error") movedComplement = true;
|
|
5584
3646
|
if (report.level === "info") unknownComplement = true;
|
|
5585
3647
|
}
|
|
5586
3648
|
if (!movedComplement && !unknownComplement) {
|
|
@@ -5606,7 +3668,7 @@ function isEngramRunning() {
|
|
|
5606
3668
|
const pgrep = lookPath("pgrep");
|
|
5607
3669
|
if (!pgrep) return null;
|
|
5608
3670
|
try {
|
|
5609
|
-
|
|
3671
|
+
execFileSync4(pgrep, ["-x", "engram"], { stdio: "ignore" });
|
|
5610
3672
|
return true;
|
|
5611
3673
|
} catch {
|
|
5612
3674
|
return false;
|
|
@@ -5616,8 +3678,8 @@ function isEngramRunning() {
|
|
|
5616
3678
|
return null;
|
|
5617
3679
|
}
|
|
5618
3680
|
}
|
|
5619
|
-
function isGitClone(projectRoot =
|
|
5620
|
-
return
|
|
3681
|
+
function isGitClone(projectRoot = path20.dirname(stackRoot())) {
|
|
3682
|
+
return fs16.existsSync(path20.join(projectRoot, ".git"));
|
|
5621
3683
|
}
|
|
5622
3684
|
var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
|
|
5623
3685
|
function resolvePnpm() {
|
|
@@ -5627,29 +3689,29 @@ function resolvePnpm() {
|
|
|
5627
3689
|
}
|
|
5628
3690
|
function cleanupTmp(dir) {
|
|
5629
3691
|
try {
|
|
5630
|
-
|
|
3692
|
+
fs16.rmSync(dir, { recursive: true, force: true });
|
|
5631
3693
|
} catch {
|
|
5632
3694
|
}
|
|
5633
3695
|
}
|
|
5634
3696
|
function updateStackGitClone() {
|
|
5635
|
-
const projectRoot =
|
|
3697
|
+
const projectRoot = path20.dirname(stackRoot());
|
|
5636
3698
|
const git = lookPath("git");
|
|
5637
3699
|
if (!git) throw new Error("git no encontrado en PATH.");
|
|
5638
3700
|
const pnpm = resolvePnpm();
|
|
5639
3701
|
p4.log.info("Ejecutando git pull\u2026");
|
|
5640
|
-
|
|
3702
|
+
execFileSync4(git, ["pull"], { cwd: projectRoot, stdio: "inherit" });
|
|
5641
3703
|
p4.log.info("Ejecutando pnpm install\u2026");
|
|
5642
|
-
|
|
3704
|
+
execFileSync4(pnpm, ["install"], { cwd: projectRoot, stdio: "inherit" });
|
|
5643
3705
|
p4.log.info("Ejecutando pnpm build\u2026");
|
|
5644
|
-
|
|
3706
|
+
execFileSync4(pnpm, ["build"], { cwd: projectRoot, stdio: "inherit" });
|
|
5645
3707
|
}
|
|
5646
3708
|
function updateStackGlobal() {
|
|
5647
3709
|
const pnpm = resolvePnpm();
|
|
5648
3710
|
p4.log.info("Ejecutando pnpm add -g jorgex-stack@latest\u2026");
|
|
5649
|
-
|
|
3711
|
+
execFileSync4(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
|
|
5650
3712
|
}
|
|
5651
3713
|
async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
5652
|
-
const root =
|
|
3714
|
+
const root = fs16.mkdtempSync(path20.join(os3.tmpdir(), "jorgex-skill-"));
|
|
5653
3715
|
try {
|
|
5654
3716
|
const result = await downloadRepoTarball(repo, sha, root, skillPath);
|
|
5655
3717
|
if (!result.ok) {
|
|
@@ -5657,15 +3719,15 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
5657
3719
|
return { error: result.reason };
|
|
5658
3720
|
}
|
|
5659
3721
|
if (skillPath) {
|
|
5660
|
-
const sub =
|
|
3722
|
+
const sub = path20.resolve(path20.join(root, skillPath));
|
|
5661
3723
|
if (!isContainedIn(sub, root)) {
|
|
5662
3724
|
cleanupTmp(root);
|
|
5663
3725
|
return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
|
|
5664
3726
|
}
|
|
5665
|
-
if (
|
|
3727
|
+
if (fs16.existsSync(sub)) return { dir: sub, root };
|
|
5666
3728
|
const lastSeg = skillPath.split("/").pop();
|
|
5667
|
-
const sub2 =
|
|
5668
|
-
if (isContainedIn(sub2, root) &&
|
|
3729
|
+
const sub2 = path20.resolve(path20.join(root, lastSeg));
|
|
3730
|
+
if (isContainedIn(sub2, root) && fs16.existsSync(sub2)) {
|
|
5669
3731
|
if (!validateExtractedTree(sub2)) {
|
|
5670
3732
|
cleanupTmp(root);
|
|
5671
3733
|
return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
|
|
@@ -5684,11 +3746,11 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
5684
3746
|
function pruneEngramDbBackups() {
|
|
5685
3747
|
try {
|
|
5686
3748
|
const dir = dataDir();
|
|
5687
|
-
if (!
|
|
5688
|
-
const backups =
|
|
3749
|
+
if (!fs16.existsSync(dir)) return;
|
|
3750
|
+
const backups = fs16.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs16.statSync(path20.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
5689
3751
|
for (const old of backups.slice(3)) {
|
|
5690
3752
|
try {
|
|
5691
|
-
|
|
3753
|
+
fs16.rmSync(path20.join(dir, old.name));
|
|
5692
3754
|
} catch {
|
|
5693
3755
|
}
|
|
5694
3756
|
}
|
|
@@ -5696,18 +3758,18 @@ function pruneEngramDbBackups() {
|
|
|
5696
3758
|
}
|
|
5697
3759
|
}
|
|
5698
3760
|
function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
5699
|
-
if (!
|
|
5700
|
-
const dir =
|
|
5701
|
-
const base =
|
|
3761
|
+
if (!fs16.existsSync(binPath)) return null;
|
|
3762
|
+
const dir = path20.dirname(binPath);
|
|
3763
|
+
const base = path20.basename(binPath);
|
|
5702
3764
|
const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
|
|
5703
3765
|
const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
|
|
5704
|
-
const resolvedDir =
|
|
5705
|
-
if (resolvedDir ===
|
|
3766
|
+
const resolvedDir = path20.resolve(dir);
|
|
3767
|
+
if (resolvedDir === path20.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
|
|
5706
3768
|
try {
|
|
5707
|
-
for (const entry of
|
|
3769
|
+
for (const entry of fs16.readdirSync(dir)) {
|
|
5708
3770
|
if (oldPattern.test(entry)) {
|
|
5709
3771
|
try {
|
|
5710
|
-
|
|
3772
|
+
fs16.rmSync(path20.join(dir, entry), { force: true });
|
|
5711
3773
|
} catch {
|
|
5712
3774
|
}
|
|
5713
3775
|
}
|
|
@@ -5715,8 +3777,8 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
|
5715
3777
|
} catch {
|
|
5716
3778
|
}
|
|
5717
3779
|
}
|
|
5718
|
-
const rotated =
|
|
5719
|
-
|
|
3780
|
+
const rotated = path20.join(dir, `${base}.old-${Date.now()}`);
|
|
3781
|
+
fs16.renameSync(binPath, rotated);
|
|
5720
3782
|
return rotated;
|
|
5721
3783
|
}
|
|
5722
3784
|
async function updateEngram(engramRepo, latestVersion) {
|
|
@@ -5725,19 +3787,19 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
5725
3787
|
"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)."
|
|
5726
3788
|
);
|
|
5727
3789
|
}
|
|
5728
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
5729
|
-
const engramDb =
|
|
5730
|
-
if (
|
|
3790
|
+
const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path20.join(HOME, ".engram");
|
|
3791
|
+
const engramDb = path20.join(engramDataDir, "engram.db");
|
|
3792
|
+
if (fs16.existsSync(engramDb)) {
|
|
5731
3793
|
const doBackup = await p4.confirm({
|
|
5732
3794
|
message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
|
|
5733
3795
|
initialValue: true
|
|
5734
3796
|
});
|
|
5735
3797
|
if (!p4.isCancel(doBackup) && doBackup) {
|
|
5736
3798
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5737
|
-
const dest =
|
|
3799
|
+
const dest = path20.join(dataDir(), `engram-db-backup-${ts}.db`);
|
|
5738
3800
|
try {
|
|
5739
|
-
if (!
|
|
5740
|
-
|
|
3801
|
+
if (!fs16.existsSync(dataDir())) fs16.mkdirSync(dataDir(), { recursive: true });
|
|
3802
|
+
fs16.copyFileSync(engramDb, dest);
|
|
5741
3803
|
p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
|
|
5742
3804
|
pruneEngramDbBackups();
|
|
5743
3805
|
} catch (err) {
|
|
@@ -5750,7 +3812,7 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
5750
3812
|
if (brew) {
|
|
5751
3813
|
let brewManages = false;
|
|
5752
3814
|
try {
|
|
5753
|
-
|
|
3815
|
+
execFileSync4(brew, ["list", "engram"], { stdio: "pipe" });
|
|
5754
3816
|
brewManages = true;
|
|
5755
3817
|
} catch {
|
|
5756
3818
|
}
|
|
@@ -5758,7 +3820,7 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
5758
3820
|
anyChannelTried = true;
|
|
5759
3821
|
p4.log.info("Actualizando engram con brew\u2026");
|
|
5760
3822
|
try {
|
|
5761
|
-
|
|
3823
|
+
execFileSync4(brew, ["upgrade", "engram"], { stdio: "inherit" });
|
|
5762
3824
|
return true;
|
|
5763
3825
|
} catch (err) {
|
|
5764
3826
|
p4.log.error(`brew upgrade engram fall\xF3: ${err instanceof Error ? err.message : err}`);
|
|
@@ -5782,15 +3844,15 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
5782
3844
|
}
|
|
5783
3845
|
p4.log.info(`Actualizando engram con go install (${latestVersion})\u2026`);
|
|
5784
3846
|
try {
|
|
5785
|
-
|
|
3847
|
+
execFileSync4(
|
|
5786
3848
|
go,
|
|
5787
3849
|
["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
|
|
5788
3850
|
{ stdio: "inherit" }
|
|
5789
3851
|
);
|
|
5790
|
-
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists:
|
|
3852
|
+
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs16.existsSync(bin ?? "") });
|
|
5791
3853
|
if (rollbackOk.action === "restore") {
|
|
5792
3854
|
try {
|
|
5793
|
-
|
|
3855
|
+
fs16.renameSync(rotated, bin);
|
|
5794
3856
|
p4.log.warn(rollbackOk.messages.onRestore);
|
|
5795
3857
|
} catch {
|
|
5796
3858
|
p4.log.warn(rollbackOk.messages.onRenameFail);
|
|
@@ -5798,10 +3860,10 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
5798
3860
|
}
|
|
5799
3861
|
return true;
|
|
5800
3862
|
} catch (err) {
|
|
5801
|
-
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists:
|
|
3863
|
+
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs16.existsSync(bin ?? "") });
|
|
5802
3864
|
if (rollbackFail.action === "restore") {
|
|
5803
3865
|
try {
|
|
5804
|
-
|
|
3866
|
+
fs16.renameSync(rotated, bin);
|
|
5805
3867
|
p4.log.info(rollbackFail.messages.onRestore);
|
|
5806
3868
|
} catch {
|
|
5807
3869
|
p4.log.error(rollbackFail.messages.onRenameFail);
|
|
@@ -6072,7 +4134,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false, includeBr
|
|
|
6072
4134
|
continue;
|
|
6073
4135
|
}
|
|
6074
4136
|
const { dir: tmpDir, root: tmpRoot } = tmpResult;
|
|
6075
|
-
const localSkillDir =
|
|
4137
|
+
const localSkillDir = path20.join(stackRoot(), "skills", skillInfo.name);
|
|
6076
4138
|
const diff = renderSkillDiff(tmpDir, localSkillDir);
|
|
6077
4139
|
if (diff) {
|
|
6078
4140
|
p4.log.info(`Diff de ${skillInfo.name}:
|
|
@@ -6164,7 +4226,7 @@ ${diff}`);
|
|
|
6164
4226
|
}
|
|
6165
4227
|
|
|
6166
4228
|
// src/models-picker.ts
|
|
6167
|
-
import
|
|
4229
|
+
import path21 from "path";
|
|
6168
4230
|
import * as p5 from "@clack/prompts";
|
|
6169
4231
|
var TIERS = ["strong", "standard", "cheap"];
|
|
6170
4232
|
var EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
@@ -6190,7 +4252,7 @@ function opencodeLiveModels(binPath) {
|
|
|
6190
4252
|
}
|
|
6191
4253
|
function agentsByTier() {
|
|
6192
4254
|
const grouped = { strong: [], standard: [], cheap: [] };
|
|
6193
|
-
for (const agent of loadCanonicalAgents(
|
|
4255
|
+
for (const agent of loadCanonicalAgents(path21.join(stackRoot(), "agents"))) {
|
|
6194
4256
|
if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
|
|
6195
4257
|
}
|
|
6196
4258
|
return grouped;
|
|
@@ -6375,16 +4437,16 @@ function cancelled() {
|
|
|
6375
4437
|
}
|
|
6376
4438
|
|
|
6377
4439
|
// src/lib/release.ts
|
|
6378
|
-
import
|
|
6379
|
-
import
|
|
6380
|
-
import { execFileSync as
|
|
4440
|
+
import fs17 from "fs";
|
|
4441
|
+
import path22 from "path";
|
|
4442
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
6381
4443
|
import { fileURLToPath } from "url";
|
|
6382
4444
|
function findPackageJson() {
|
|
6383
|
-
let dir =
|
|
4445
|
+
let dir = path22.dirname(fileURLToPath(import.meta.url));
|
|
6384
4446
|
for (let i = 0; i < 6; i++) {
|
|
6385
|
-
const candidate =
|
|
6386
|
-
if (
|
|
6387
|
-
dir =
|
|
4447
|
+
const candidate = path22.join(dir, "package.json");
|
|
4448
|
+
if (fs17.existsSync(candidate)) return candidate;
|
|
4449
|
+
dir = path22.dirname(dir);
|
|
6388
4450
|
}
|
|
6389
4451
|
throw new Error("No se encontr\xF3 package.json cerca del CLI.");
|
|
6390
4452
|
}
|
|
@@ -6393,7 +4455,7 @@ function readPackageVersion() {
|
|
|
6393
4455
|
}
|
|
6394
4456
|
function readPackageMetadata() {
|
|
6395
4457
|
const packageJson = findPackageJson();
|
|
6396
|
-
const raw =
|
|
4458
|
+
const raw = fs17.readFileSync(packageJson, "utf8");
|
|
6397
4459
|
const parsed = JSON.parse(raw);
|
|
6398
4460
|
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
6399
4461
|
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
@@ -6405,8 +4467,8 @@ function readPackageMetadata() {
|
|
|
6405
4467
|
|
|
6406
4468
|
// src/lib/pi-runtime.ts
|
|
6407
4469
|
import os4 from "os";
|
|
6408
|
-
import
|
|
6409
|
-
import
|
|
4470
|
+
import path23 from "path";
|
|
4471
|
+
import fs18 from "fs";
|
|
6410
4472
|
import { spawnSync } from "child_process";
|
|
6411
4473
|
import { createHash as createHash2 } from "crypto";
|
|
6412
4474
|
|
|
@@ -6545,7 +4607,7 @@ async function resolvePiEngramRequirement(input, deps) {
|
|
|
6545
4607
|
initialValue: false
|
|
6546
4608
|
});
|
|
6547
4609
|
if (!accepted) return { kind: "offer", accepted: false };
|
|
6548
|
-
const installed = await deps.installNative({ version: "
|
|
4610
|
+
const installed = await deps.installNative({ version: "2.0.0", channels: ["brew", "go", "url"] });
|
|
6549
4611
|
if (!installed) {
|
|
6550
4612
|
return {
|
|
6551
4613
|
kind: "blocked",
|
|
@@ -6603,7 +4665,7 @@ function healthyDoctor(stdout, stderr, packageRunner, candidate) {
|
|
|
6603
4665
|
if (record === null || typeof record !== "object" || Array.isArray(record)) return false;
|
|
6604
4666
|
const packageValue = Reflect.get(record, "package");
|
|
6605
4667
|
const result = Reflect.get(record, "result");
|
|
6606
|
-
return Reflect.get(record, "schemaVersion") === 1 && Reflect.get(record, "command") === "doctor" && Reflect.get(record, "ok") === true && packageValue !== null && typeof packageValue === "object" && Reflect.get(packageValue, "name") === "jorgex-pi" && Reflect.get(packageValue, "version") === (candidate.package?.version ?? /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source)?.[1]) &&
|
|
4668
|
+
return Reflect.get(record, "schemaVersion") === 1 && Reflect.get(record, "command") === "doctor" && Reflect.get(record, "ok") === true && packageValue !== null && typeof packageValue === "object" && Reflect.get(packageValue, "name") === "jorgex-pi" && Reflect.get(packageValue, "version") === (candidate.package?.version ?? /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source)?.[1]) && path23.resolve(packageRunner) === path23.resolve(String(Reflect.get(packageValue, "root")), "bin", "jorgex-pi.mjs") && result !== null && typeof result === "object" && Reflect.get(result, "healthy") === true;
|
|
6607
4669
|
} catch {
|
|
6608
4670
|
return false;
|
|
6609
4671
|
}
|
|
@@ -6624,7 +4686,7 @@ function provisionalPendingDoctor(exitCode, stdout, stderr, packageRunner, candi
|
|
|
6624
4686
|
if (JSON.stringify(Object.keys(packageValue).sort()) !== JSON.stringify(["name", "root", "version"])) return false;
|
|
6625
4687
|
if (Reflect.get(packageValue, "name") !== "jorgex-pi") return false;
|
|
6626
4688
|
if (Reflect.get(packageValue, "version") !== (candidate.package?.version ?? /^npm:jorgex-pi@([^\s]+)$/.exec(candidate.source)?.[1])) return false;
|
|
6627
|
-
if (
|
|
4689
|
+
if (path23.resolve(packageRunner) !== path23.resolve(String(Reflect.get(packageValue, "root")), "bin", "jorgex-pi.mjs")) return false;
|
|
6628
4690
|
const result = Reflect.get(record, "result");
|
|
6629
4691
|
if (result === null || typeof result !== "object" || Array.isArray(result)) return false;
|
|
6630
4692
|
if (JSON.stringify(Object.keys(result).sort()) !== JSON.stringify(["checks", "healthy"])) return false;
|
|
@@ -6653,7 +4715,7 @@ function provisionalPendingDoctor(exitCode, stdout, stderr, packageRunner, candi
|
|
|
6653
4715
|
}
|
|
6654
4716
|
function installPiFromVerifiedTarball(input, deps) {
|
|
6655
4717
|
const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.piExecutable) : targetPaths(input.targetDir, input.engramBin, input.piExecutable);
|
|
6656
|
-
const destination = input.targetDir === void 0 ?
|
|
4718
|
+
const destination = input.targetDir === void 0 ? path23.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path23.join(path23.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
|
|
6657
4719
|
const artifact = deps.download(destination);
|
|
6658
4720
|
if (artifact.bytes !== input.candidate.bytes || artifact.sha256 !== input.candidate.sha256 || artifact.sha512 !== input.candidate.sha512) {
|
|
6659
4721
|
return { kind: "blocked", reason: "tarball-integrity" };
|
|
@@ -6661,7 +4723,7 @@ function installPiFromVerifiedTarball(input, deps) {
|
|
|
6661
4723
|
deps.backupSettings();
|
|
6662
4724
|
const scope = {
|
|
6663
4725
|
kind: input.targetDir === void 0 ? "real" : "target-dir",
|
|
6664
|
-
codingAgentDir:
|
|
4726
|
+
codingAgentDir: path23.resolve(paths.codingAgentDir)
|
|
6665
4727
|
};
|
|
6666
4728
|
const installing = flatCandidateReceipt(input.candidate, scope, "installing", input.engramBin);
|
|
6667
4729
|
deps.writeReceiptAtomic(`${JSON.stringify(installing)}
|
|
@@ -6692,30 +4754,30 @@ function installPiFromVerifiedTarball(input, deps) {
|
|
|
6692
4754
|
return { kind: "installed", receipt };
|
|
6693
4755
|
}
|
|
6694
4756
|
function runtimePath(piExecutable) {
|
|
6695
|
-
const entries = process.platform === "win32" ? [piExecutable === void 0 ? null :
|
|
6696
|
-
return [...new Set(entries.filter((entry) => entry !== null))].join(
|
|
4757
|
+
const entries = process.platform === "win32" ? [piExecutable === void 0 ? null : path23.dirname(piExecutable), path23.dirname(process.execPath), process.env.SystemRoot ? path23.join(process.env.SystemRoot, "System32") : null] : [piExecutable === void 0 ? null : path23.dirname(piExecutable), path23.dirname(process.execPath), "/usr/local/bin", "/usr/bin", "/bin"];
|
|
4758
|
+
return [...new Set(entries.filter((entry) => entry !== null))].join(path23.delimiter);
|
|
6697
4759
|
}
|
|
6698
4760
|
function targetPaths(targetDir, engramBin, piExecutable) {
|
|
6699
|
-
const root =
|
|
6700
|
-
const codingAgentDir =
|
|
6701
|
-
const home =
|
|
6702
|
-
const temporary =
|
|
4761
|
+
const root = path23.resolve(targetDir);
|
|
4762
|
+
const codingAgentDir = path23.join(root, "pi-agent");
|
|
4763
|
+
const home = path23.join(root, "home");
|
|
4764
|
+
const temporary = path23.join(root, "tmp");
|
|
6703
4765
|
return {
|
|
6704
4766
|
codingAgentDir,
|
|
6705
|
-
receiptPath:
|
|
6706
|
-
packageRunner:
|
|
4767
|
+
receiptPath: path23.join(root, "state", "pi-receipt.json"),
|
|
4768
|
+
packageRunner: path23.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
|
|
6707
4769
|
environment: {
|
|
6708
4770
|
HOME: home,
|
|
6709
4771
|
USERPROFILE: home,
|
|
6710
|
-
APPDATA:
|
|
6711
|
-
LOCALAPPDATA:
|
|
6712
|
-
XDG_CONFIG_HOME:
|
|
6713
|
-
XDG_DATA_HOME:
|
|
6714
|
-
XDG_CACHE_HOME:
|
|
4772
|
+
APPDATA: path23.join(root, "appdata"),
|
|
4773
|
+
LOCALAPPDATA: path23.join(root, "localappdata"),
|
|
4774
|
+
XDG_CONFIG_HOME: path23.join(root, "xdg-config"),
|
|
4775
|
+
XDG_DATA_HOME: path23.join(root, "xdg-data"),
|
|
4776
|
+
XDG_CACHE_HOME: path23.join(root, "xdg-cache"),
|
|
6715
4777
|
TEMP: temporary,
|
|
6716
4778
|
TMP: temporary,
|
|
6717
4779
|
TMPDIR: temporary,
|
|
6718
|
-
npm_config_cache:
|
|
4780
|
+
npm_config_cache: path23.join(root, "npm-cache"),
|
|
6719
4781
|
NPM_CONFIG_IGNORE_SCRIPTS: "true",
|
|
6720
4782
|
NPM_CONFIG_UPDATE_NOTIFIER: "false",
|
|
6721
4783
|
PI_CODING_AGENT_DIR: codingAgentDir,
|
|
@@ -6726,15 +4788,15 @@ function targetPaths(targetDir, engramBin, piExecutable) {
|
|
|
6726
4788
|
}
|
|
6727
4789
|
function userPaths(engramBin, piExecutable) {
|
|
6728
4790
|
const home = os4.homedir();
|
|
6729
|
-
const codingAgentDir = process.env.PI_CODING_AGENT_DIR ??
|
|
4791
|
+
const codingAgentDir = process.env.PI_CODING_AGENT_DIR ?? path23.join(home, ".pi", "agent");
|
|
6730
4792
|
return {
|
|
6731
4793
|
codingAgentDir,
|
|
6732
|
-
receiptPath:
|
|
6733
|
-
packageRunner:
|
|
4794
|
+
receiptPath: path23.join(dataDir(), "pi-receipt.json"),
|
|
4795
|
+
packageRunner: path23.join(codingAgentDir, "npm", "node_modules", "jorgex-pi", "bin", "jorgex-pi.mjs"),
|
|
6734
4796
|
environment: {
|
|
6735
4797
|
HOME: home,
|
|
6736
|
-
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ??
|
|
6737
|
-
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME ??
|
|
4798
|
+
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path23.join(home, ".config"),
|
|
4799
|
+
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME ?? path23.join(home, ".cache"),
|
|
6738
4800
|
TMPDIR: os4.tmpdir(),
|
|
6739
4801
|
NPM_CONFIG_IGNORE_SCRIPTS: "true",
|
|
6740
4802
|
NPM_CONFIG_UPDATE_NOTIFIER: "false",
|
|
@@ -6762,7 +4824,7 @@ function runPiRuntime(input, deps) {
|
|
|
6762
4824
|
return { kind: "blocked", reason: "tarball-integrity" };
|
|
6763
4825
|
}
|
|
6764
4826
|
const paths = input.targetDir === void 0 ? userPaths(input.engramBin, input.detected.executable) : targetPaths(input.targetDir, input.engramBin, input.detected.executable);
|
|
6765
|
-
const settingsJson = deps.readSettings(
|
|
4827
|
+
const settingsJson = deps.readSettings(path23.join(paths.codingAgentDir, "settings.json"));
|
|
6766
4828
|
const receiptJson = deps.readReceipt(paths.receiptPath);
|
|
6767
4829
|
const lifecycleInput = {
|
|
6768
4830
|
candidate: PI_RUNTIME_CANDIDATE,
|
|
@@ -6821,19 +4883,19 @@ function runPiRuntime(input, deps) {
|
|
|
6821
4883
|
return result;
|
|
6822
4884
|
}
|
|
6823
4885
|
function readJsonFile(file) {
|
|
6824
|
-
return JSON.parse(
|
|
4886
|
+
return JSON.parse(fs18.readFileSync(file, "utf8"));
|
|
6825
4887
|
}
|
|
6826
4888
|
function packageVersionFromExecutable(executable) {
|
|
6827
4889
|
let current;
|
|
6828
4890
|
try {
|
|
6829
|
-
current =
|
|
4891
|
+
current = path23.dirname(fs18.realpathSync(executable));
|
|
6830
4892
|
} catch {
|
|
6831
4893
|
return null;
|
|
6832
4894
|
}
|
|
6833
4895
|
for (let depth = 0; depth < 8; depth++) {
|
|
6834
4896
|
const manifests = [
|
|
6835
|
-
|
|
6836
|
-
|
|
4897
|
+
path23.join(current, "package.json"),
|
|
4898
|
+
path23.join(current, "node_modules", "@earendil-works", "pi-coding-agent", "package.json")
|
|
6837
4899
|
];
|
|
6838
4900
|
for (const manifest of manifests) {
|
|
6839
4901
|
try {
|
|
@@ -6844,7 +4906,7 @@ function packageVersionFromExecutable(executable) {
|
|
|
6844
4906
|
} catch {
|
|
6845
4907
|
}
|
|
6846
4908
|
}
|
|
6847
|
-
const parent =
|
|
4909
|
+
const parent = path23.dirname(current);
|
|
6848
4910
|
if (parent === current) break;
|
|
6849
4911
|
current = parent;
|
|
6850
4912
|
}
|
|
@@ -6859,35 +4921,35 @@ function detectPiRuntime() {
|
|
|
6859
4921
|
installed: executable !== null,
|
|
6860
4922
|
executable,
|
|
6861
4923
|
version: executable === null ? null : packageVersionFromExecutable(executable),
|
|
6862
|
-
codingAgentDir: process.env.PI_CODING_AGENT_DIR ??
|
|
4924
|
+
codingAgentDir: process.env.PI_CODING_AGENT_DIR ?? path23.join(home, ".pi", "agent")
|
|
6863
4925
|
};
|
|
6864
4926
|
}
|
|
6865
4927
|
function hasManagedPiRuntime(targetDir) {
|
|
6866
|
-
const receipt = targetDir === void 0 ?
|
|
6867
|
-
return
|
|
4928
|
+
const receipt = targetDir === void 0 ? path23.join(dataDir(), "pi-receipt.json") : path23.join(path23.resolve(targetDir), "state", "pi-receipt.json");
|
|
4929
|
+
return fs18.statSync(receipt, { throwIfNoEntry: false })?.isFile() === true;
|
|
6868
4930
|
}
|
|
6869
4931
|
function resolvePiEngramBin(targetDir) {
|
|
6870
4932
|
if (targetDir === void 0) return detectEngram();
|
|
6871
|
-
const candidate =
|
|
6872
|
-
return
|
|
4933
|
+
const candidate = path23.join(path23.resolve(targetDir), "bin", process.platform === "win32" ? "engram.exe" : "engram");
|
|
4934
|
+
return fs18.statSync(candidate, { throwIfNoEntry: false })?.isFile() ? candidate : null;
|
|
6873
4935
|
}
|
|
6874
4936
|
function readOptional(file, fallback) {
|
|
6875
4937
|
try {
|
|
6876
|
-
return
|
|
4938
|
+
return fs18.readFileSync(file, "utf8");
|
|
6877
4939
|
} catch (error) {
|
|
6878
4940
|
if (error.code === "ENOENT") return fallback;
|
|
6879
4941
|
throw error;
|
|
6880
4942
|
}
|
|
6881
4943
|
}
|
|
6882
4944
|
function hashPiTarball(file) {
|
|
6883
|
-
const descriptor =
|
|
4945
|
+
const descriptor = fs18.openSync(file, "r");
|
|
6884
4946
|
const sha2562 = createHash2("sha256");
|
|
6885
4947
|
const sha512 = createHash2("sha512");
|
|
6886
4948
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
6887
4949
|
let bytes = 0;
|
|
6888
4950
|
try {
|
|
6889
4951
|
while (true) {
|
|
6890
|
-
const read =
|
|
4952
|
+
const read = fs18.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
6891
4953
|
if (read === 0) break;
|
|
6892
4954
|
bytes += read;
|
|
6893
4955
|
const chunk = buffer.subarray(0, read);
|
|
@@ -6895,27 +4957,27 @@ function hashPiTarball(file) {
|
|
|
6895
4957
|
sha512.update(chunk);
|
|
6896
4958
|
}
|
|
6897
4959
|
} finally {
|
|
6898
|
-
|
|
4960
|
+
fs18.closeSync(descriptor);
|
|
6899
4961
|
}
|
|
6900
4962
|
return { path: file, bytes, sha256: sha2562.digest("hex"), sha512: sha512.digest("hex") };
|
|
6901
4963
|
}
|
|
6902
4964
|
async function acquirePiTarball(destination) {
|
|
6903
|
-
const existing =
|
|
4965
|
+
const existing = fs18.statSync(destination, { throwIfNoEntry: false });
|
|
6904
4966
|
if (existing?.isFile()) {
|
|
6905
4967
|
const observed = hashPiTarball(destination);
|
|
6906
4968
|
if (observed.bytes === PI_RUNTIME_CANDIDATE.tarball.bytes && observed.sha256 === PI_RUNTIME_CANDIDATE.tarball.sha256 && observed.sha512 === PI_RUNTIME_CANDIDATE.tarball.sha512) {
|
|
6907
4969
|
return observed;
|
|
6908
4970
|
}
|
|
6909
|
-
|
|
4971
|
+
fs18.rmSync(destination, { force: true });
|
|
6910
4972
|
}
|
|
6911
|
-
|
|
4973
|
+
fs18.mkdirSync(path23.dirname(destination), { recursive: true });
|
|
6912
4974
|
const partial = `${destination}.partial-${process.pid}`;
|
|
6913
4975
|
const response = await fetch(`https://registry.npmjs.org/jorgex-pi/-/jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`, {
|
|
6914
4976
|
redirect: "error",
|
|
6915
4977
|
headers: { accept: "application/octet-stream" }
|
|
6916
4978
|
});
|
|
6917
4979
|
if (!response.ok || response.body === null) throw new Error(`No se pudo descargar jorgex-pi@${PI_RUNTIME_CANDIDATE.package.version} (${response.status}).`);
|
|
6918
|
-
const descriptor =
|
|
4980
|
+
const descriptor = fs18.openSync(partial, "wx", 384);
|
|
6919
4981
|
let bytes = 0;
|
|
6920
4982
|
try {
|
|
6921
4983
|
for await (const chunk of response.body) {
|
|
@@ -6923,15 +4985,15 @@ async function acquirePiTarball(destination) {
|
|
|
6923
4985
|
bytes += buffer.length;
|
|
6924
4986
|
if (bytes > PI_RUNTIME_CANDIDATE.tarball.bytes) throw new Error("El tarball de jorgex-pi excede el tama\xF1o fijado.");
|
|
6925
4987
|
let offset = 0;
|
|
6926
|
-
while (offset < buffer.length) offset +=
|
|
4988
|
+
while (offset < buffer.length) offset += fs18.writeSync(descriptor, buffer, offset);
|
|
6927
4989
|
}
|
|
6928
4990
|
} catch (error) {
|
|
6929
|
-
|
|
6930
|
-
|
|
4991
|
+
fs18.closeSync(descriptor);
|
|
4992
|
+
fs18.rmSync(partial, { force: true });
|
|
6931
4993
|
throw error;
|
|
6932
4994
|
}
|
|
6933
|
-
|
|
6934
|
-
|
|
4995
|
+
fs18.closeSync(descriptor);
|
|
4996
|
+
fs18.renameSync(partial, destination);
|
|
6935
4997
|
return hashPiTarball(destination);
|
|
6936
4998
|
}
|
|
6937
4999
|
function runProcess(invocation) {
|
|
@@ -6981,7 +5043,7 @@ async function runPiRuntimeSystem(input) {
|
|
|
6981
5043
|
remedy: "Instala Engram o configura un ENGRAM_BIN absoluto antes de reintentar."
|
|
6982
5044
|
};
|
|
6983
5045
|
}
|
|
6984
|
-
const destination = input.targetDir === void 0 ?
|
|
5046
|
+
const destination = input.targetDir === void 0 ? path23.join(dataDir(), "packages", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`) : path23.join(path23.resolve(input.targetDir), "downloads", `jorgex-pi-${PI_RUNTIME_CANDIDATE.package.version}.tgz`);
|
|
6985
5047
|
let artifact;
|
|
6986
5048
|
try {
|
|
6987
5049
|
artifact = await acquirePiTarball(destination);
|
|
@@ -7002,18 +5064,18 @@ async function runPiRuntimeSystem(input) {
|
|
|
7002
5064
|
}, {
|
|
7003
5065
|
download: () => artifact,
|
|
7004
5066
|
backupSettings: () => createBackup(
|
|
7005
|
-
[
|
|
5067
|
+
[path23.join(paths.codingAgentDir, "settings.json")],
|
|
7006
5068
|
"pi-package-install",
|
|
7007
|
-
input.targetDir === void 0 ? void 0 :
|
|
5069
|
+
input.targetDir === void 0 ? void 0 : path23.join(path23.resolve(input.targetDir), "backups")
|
|
7008
5070
|
),
|
|
7009
5071
|
run: runProcess,
|
|
7010
|
-
readSettings: () => readOptional(
|
|
7011
|
-
rewriteSettings: (content) => writeText(
|
|
5072
|
+
readSettings: () => readOptional(path23.join(paths.codingAgentDir, "settings.json"), '{"packages":[]}'),
|
|
5073
|
+
rewriteSettings: (content) => writeText(path23.join(paths.codingAgentDir, "settings.json"), `${content}
|
|
7012
5074
|
`),
|
|
7013
5075
|
writeReceiptAtomic: (content) => writeText(paths.receiptPath, content)
|
|
7014
5076
|
});
|
|
7015
5077
|
}
|
|
7016
|
-
const packageRoot =
|
|
5078
|
+
const packageRoot = path23.dirname(path23.dirname(paths.packageRunner));
|
|
7017
5079
|
const writeReceipt = (receipt) => {
|
|
7018
5080
|
writeText(paths.receiptPath, `${JSON.stringify(receipt, null, 2)}
|
|
7019
5081
|
`);
|
|
@@ -7031,13 +5093,13 @@ async function runPiRuntimeSystem(input) {
|
|
|
7031
5093
|
value,
|
|
7032
5094
|
{
|
|
7033
5095
|
backupSettings: () => createBackup(
|
|
7034
|
-
[
|
|
5096
|
+
[path23.join(paths.codingAgentDir, "settings.json")],
|
|
7035
5097
|
"pi-package-uninstall",
|
|
7036
|
-
input.targetDir === void 0 ? void 0 :
|
|
5098
|
+
input.targetDir === void 0 ? void 0 : path23.join(path23.resolve(input.targetDir), "backups")
|
|
7037
5099
|
),
|
|
7038
5100
|
run: runProcess,
|
|
7039
|
-
isPackageAbsent: () => !
|
|
7040
|
-
deleteReceipt: () =>
|
|
5101
|
+
isPackageAbsent: () => !fs18.existsSync(packageRoot),
|
|
5102
|
+
deleteReceipt: () => fs18.rmSync(paths.receiptPath, { force: true })
|
|
7041
5103
|
}
|
|
7042
5104
|
)
|
|
7043
5105
|
};
|
|
@@ -7212,7 +5274,7 @@ async function runManagedPiSystem(input) {
|
|
|
7212
5274
|
}
|
|
7213
5275
|
|
|
7214
5276
|
// src/lib/quality-runner.ts
|
|
7215
|
-
import
|
|
5277
|
+
import path24 from "path";
|
|
7216
5278
|
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
7217
5279
|
function normalizeLimit(value, name) {
|
|
7218
5280
|
if (value === void 0) return void 0;
|
|
@@ -7232,10 +5294,10 @@ function normalizeTimeout(value) {
|
|
|
7232
5294
|
return timeoutMs;
|
|
7233
5295
|
}
|
|
7234
5296
|
var COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
7235
|
-
function
|
|
5297
|
+
function isRecord2(value) {
|
|
7236
5298
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7237
5299
|
}
|
|
7238
|
-
function
|
|
5300
|
+
function hasText(value) {
|
|
7239
5301
|
return typeof value === "string" && value.trim() !== "";
|
|
7240
5302
|
}
|
|
7241
5303
|
function isDenseStringArray(value) {
|
|
@@ -7252,14 +5314,14 @@ function isQualityProfile(value) {
|
|
|
7252
5314
|
return typeof value === "string" && QUALITY_PROFILES.includes(value);
|
|
7253
5315
|
}
|
|
7254
5316
|
function assertValidEnvironment(value, label) {
|
|
7255
|
-
if (!
|
|
5317
|
+
if (!isRecord2(value)) throw new Error(`Invalid ${label}`);
|
|
7256
5318
|
for (const [key, entry] of Object.entries(value)) {
|
|
7257
5319
|
if (typeof entry !== "string") throw new Error(`Invalid ${label}.${key}`);
|
|
7258
5320
|
}
|
|
7259
5321
|
}
|
|
7260
5322
|
function assertQualityPlanInput(value) {
|
|
7261
|
-
if (!
|
|
7262
|
-
if (!
|
|
5323
|
+
if (!isRecord2(value)) throw new Error("Invalid quality plan");
|
|
5324
|
+
if (!isRecord2(value.identity)) throw new Error("Invalid quality plan identity");
|
|
7263
5325
|
if (typeof value.identity.baseSha !== "string" || !COMMIT_SHA_PATTERN.test(value.identity.baseSha)) {
|
|
7264
5326
|
throw new Error("Invalid quality plan identity.baseSha");
|
|
7265
5327
|
}
|
|
@@ -7271,7 +5333,7 @@ function assertQualityPlanInput(value) {
|
|
|
7271
5333
|
const controlIds = /* @__PURE__ */ new Set();
|
|
7272
5334
|
for (let index = 0; index < value.controls.length; index += 1) {
|
|
7273
5335
|
const control = value.controls[index];
|
|
7274
|
-
if (!
|
|
5336
|
+
if (!isRecord2(control) || !hasText(control.id) || control.requirement !== "required" && control.requirement !== "optional") {
|
|
7275
5337
|
throw new Error(`Invalid quality plan controls[${index}]`);
|
|
7276
5338
|
}
|
|
7277
5339
|
if (controlIds.has(control.id)) {
|
|
@@ -7287,9 +5349,9 @@ function assertQualityPlanInput(value) {
|
|
|
7287
5349
|
const commandControlIds = /* @__PURE__ */ new Set();
|
|
7288
5350
|
for (let index = 0; index < value.commands.length; index += 1) {
|
|
7289
5351
|
const command = value.commands[index];
|
|
7290
|
-
if (!
|
|
5352
|
+
if (!isRecord2(command)) throw new Error(`Invalid quality plan commands[${index}]`);
|
|
7291
5353
|
const timeoutMs = command.timeoutMs;
|
|
7292
|
-
if (!
|
|
5354
|
+
if (!hasText(command.controlId) || !hasText(command.commandId) || !hasText(command.executable) || !isDenseStringArray(command.argv) || !isNonNegativeSafeInteger(timeoutMs)) {
|
|
7293
5355
|
throw new Error(`Invalid quality plan commands[${index}]`);
|
|
7294
5356
|
}
|
|
7295
5357
|
if (timeoutMs > MAX_NODE_TIMEOUT_MS) {
|
|
@@ -7344,7 +5406,7 @@ function killProcessTree(child) {
|
|
|
7344
5406
|
if (pid === void 0) return;
|
|
7345
5407
|
if (process.platform === "win32") {
|
|
7346
5408
|
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR ?? "C:\\Windows";
|
|
7347
|
-
const taskkill =
|
|
5409
|
+
const taskkill = path24.join(systemRoot, "System32", "taskkill.exe");
|
|
7348
5410
|
const result = spawnSync2(taskkill, ["/pid", String(pid), "/t", "/f"], {
|
|
7349
5411
|
shell: false,
|
|
7350
5412
|
stdio: "ignore",
|
|
@@ -7532,7 +5594,7 @@ function missingRequiredResults(controls, commands) {
|
|
|
7532
5594
|
const commandControlIds = new Set(commands.map((command) => command.controlId));
|
|
7533
5595
|
const missing = /* @__PURE__ */ new Set();
|
|
7534
5596
|
for (const control of controls) {
|
|
7535
|
-
if (control.requirement !== "required" || !
|
|
5597
|
+
if (control.requirement !== "required" || !hasText(control.id) || commandControlIds.has(control.id)) continue;
|
|
7536
5598
|
missing.add(control.id);
|
|
7537
5599
|
}
|
|
7538
5600
|
return [...missing].map((controlId) => ({
|
|
@@ -7590,62 +5652,62 @@ async function runQualityPlan(input) {
|
|
|
7590
5652
|
}
|
|
7591
5653
|
|
|
7592
5654
|
// src/lib/engram-install.ts
|
|
7593
|
-
import
|
|
5655
|
+
import fs19 from "fs";
|
|
7594
5656
|
import os5 from "os";
|
|
7595
|
-
import
|
|
7596
|
-
import { execFileSync as
|
|
5657
|
+
import path25 from "path";
|
|
5658
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
7597
5659
|
import { createHash as createHash3 } from "crypto";
|
|
7598
5660
|
|
|
7599
5661
|
// src/lib/engram-release.json
|
|
7600
5662
|
var engram_release_default = {
|
|
7601
5663
|
assets: {
|
|
7602
5664
|
darwin_arm64: {
|
|
7603
|
-
name: "
|
|
7604
|
-
sha256: "
|
|
7605
|
-
size:
|
|
5665
|
+
name: "engram_2.0.0_darwin_arm64.tar.gz",
|
|
5666
|
+
sha256: "793c695153e0e76144dc53b006a7039771bd24115385856578bc4d6506d46d7f",
|
|
5667
|
+
size: 7210321
|
|
7606
5668
|
},
|
|
7607
5669
|
darwin_x64: {
|
|
7608
|
-
name: "
|
|
7609
|
-
sha256: "
|
|
7610
|
-
size:
|
|
5670
|
+
name: "engram_2.0.0_darwin_amd64.tar.gz",
|
|
5671
|
+
sha256: "f1dcc343ac241c5001617113279b49dec698005550b538250ccf21655fc728ac",
|
|
5672
|
+
size: 7678428
|
|
7611
5673
|
},
|
|
7612
5674
|
linux_arm64: {
|
|
7613
|
-
name: "
|
|
7614
|
-
sha256: "
|
|
7615
|
-
size:
|
|
5675
|
+
name: "engram_2.0.0_linux_arm64.tar.gz",
|
|
5676
|
+
sha256: "a942e73ab424faaa6e2785d1563e0d9d7f20739944dae0c50071223301d44333",
|
|
5677
|
+
size: 6926960
|
|
7616
5678
|
},
|
|
7617
5679
|
linux_x64: {
|
|
7618
|
-
name: "
|
|
7619
|
-
sha256: "
|
|
7620
|
-
size:
|
|
5680
|
+
name: "engram_2.0.0_linux_amd64.tar.gz",
|
|
5681
|
+
sha256: "23be1c2ce9739c455097ff864736213717b925b3e8821a988dfc619685a5abd5",
|
|
5682
|
+
size: 7544949
|
|
7621
5683
|
},
|
|
7622
5684
|
windows_arm64: {
|
|
7623
|
-
name: "
|
|
7624
|
-
sha256: "
|
|
7625
|
-
size:
|
|
5685
|
+
name: "engram_2.0.0_windows_arm64.zip",
|
|
5686
|
+
sha256: "e4d9074ebf8a839573f62d7bd2719421fc428ab25a9affe82ca4b2d5555183d1",
|
|
5687
|
+
size: 6980876
|
|
7626
5688
|
},
|
|
7627
5689
|
windows_x64: {
|
|
7628
|
-
name: "
|
|
7629
|
-
sha256: "
|
|
7630
|
-
size:
|
|
5690
|
+
name: "engram_2.0.0_windows_amd64.zip",
|
|
5691
|
+
sha256: "fba31e7221702f213954c2081aa83b38833e0e9b824a981f976f8cee2c74548c",
|
|
5692
|
+
size: 7738896
|
|
7631
5693
|
}
|
|
7632
5694
|
},
|
|
7633
|
-
version: "
|
|
5695
|
+
version: "2.0.0"
|
|
7634
5696
|
};
|
|
7635
5697
|
|
|
7636
5698
|
// src/lib/engram-install.ts
|
|
7637
5699
|
async function installMissingEngram(options = {}) {
|
|
7638
5700
|
const platform = options.platform ?? process.platform;
|
|
7639
5701
|
const homeDir = options.homeDir ?? os5.homedir();
|
|
7640
|
-
if (!
|
|
5702
|
+
if (!path25.isAbsolute(homeDir)) return { ok: false, reason: "El directorio personal debe ser absoluto." };
|
|
7641
5703
|
const binaryName = platform === "win32" ? "engram.exe" : "engram";
|
|
7642
|
-
const bin =
|
|
5704
|
+
const bin = path25.join(homeDir, ".local", "bin", binaryName);
|
|
7643
5705
|
let staging;
|
|
7644
5706
|
try {
|
|
7645
|
-
const existing =
|
|
5707
|
+
const existing = fs19.lstatSync(bin, { throwIfNoEntry: false });
|
|
7646
5708
|
if (existing) {
|
|
7647
|
-
if (!
|
|
7648
|
-
|
|
5709
|
+
if (!fs19.statSync(bin).isFile()) throw new Error(`El destino Engram no es un archivo: ${bin}`);
|
|
5710
|
+
fs19.accessSync(bin, platform === "win32" ? fs19.constants.F_OK : fs19.constants.X_OK);
|
|
7649
5711
|
return { ok: true, bin };
|
|
7650
5712
|
}
|
|
7651
5713
|
const key = `${platform === "win32" ? "windows" : platform}_${options.arch ?? process.arch}`;
|
|
@@ -7674,24 +5736,24 @@ async function installMissingEngram(options = {}) {
|
|
|
7674
5736
|
if (size !== asset.size || createHash3("sha256").update(archive).digest("hex") !== asset.sha256) {
|
|
7675
5737
|
throw new Error("Engram: tama\xF1o o hash SHA-256 no coincide con el artefacto aprobado.");
|
|
7676
5738
|
}
|
|
7677
|
-
|
|
7678
|
-
staging =
|
|
7679
|
-
const archivePath =
|
|
7680
|
-
|
|
7681
|
-
const binary =
|
|
5739
|
+
fs19.mkdirSync(path25.dirname(bin), { recursive: true });
|
|
5740
|
+
staging = fs19.mkdtempSync(path25.join(path25.dirname(bin), ".engram-install-"));
|
|
5741
|
+
const archivePath = path25.join(staging, asset.name);
|
|
5742
|
+
fs19.writeFileSync(archivePath, archive, { flag: "wx", mode: 384 });
|
|
5743
|
+
const binary = execFileSync6(resolveTarBin(), ["-xOf", archivePath, binaryName], {
|
|
7682
5744
|
maxBuffer: 64 * 1024 * 1024,
|
|
7683
5745
|
timeout: 3e4,
|
|
7684
5746
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7685
5747
|
});
|
|
7686
5748
|
if (binary.length === 0) throw new Error("El artefacto Engram no contiene un binario v\xE1lido.");
|
|
7687
|
-
const stagedBin =
|
|
7688
|
-
|
|
7689
|
-
|
|
5749
|
+
const stagedBin = path25.join(staging, binaryName);
|
|
5750
|
+
fs19.writeFileSync(stagedBin, binary, { flag: "wx", mode: 493 });
|
|
5751
|
+
fs19.linkSync(stagedBin, bin);
|
|
7690
5752
|
return { ok: true, bin };
|
|
7691
5753
|
} catch (error) {
|
|
7692
5754
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
7693
5755
|
} finally {
|
|
7694
|
-
if (staging)
|
|
5756
|
+
if (staging) fs19.rmSync(staging, { recursive: true, force: true });
|
|
7695
5757
|
}
|
|
7696
5758
|
}
|
|
7697
5759
|
|
|
@@ -8217,7 +6279,7 @@ Flags disponibles: jorgex-stack --help`
|
|
|
8217
6279
|
return;
|
|
8218
6280
|
}
|
|
8219
6281
|
try {
|
|
8220
|
-
const plan = JSON.parse(
|
|
6282
|
+
const plan = JSON.parse(fs20.readFileSync(flags.positional[0], "utf8"));
|
|
8221
6283
|
const result = await runQualityPlan(plan);
|
|
8222
6284
|
const serialized = serializeQualityReceipt(result.receipt);
|
|
8223
6285
|
if (flags.receipt === void 0) process.stdout.write(`${serialized}
|
|
@@ -8603,13 +6665,22 @@ Flags disponibles: jorgex-stack --help`
|
|
|
8603
6665
|
if (!flags.list) console.log("\nUsa: jorgex-stack restore <id>");
|
|
8604
6666
|
return;
|
|
8605
6667
|
}
|
|
8606
|
-
const
|
|
6668
|
+
const targetId = flags.positional[0];
|
|
6669
|
+
const expected = listBackups().find((b) => b.id === targetId)?.files.length;
|
|
6670
|
+
const restored = restoreBackup(targetId);
|
|
6671
|
+
if (expected !== void 0 && restored < expected) {
|
|
6672
|
+
console.error(
|
|
6673
|
+
`Restore incompleto del backup ${targetId}: restaurados ${restored}/${expected} archivos (omisiones de seguridad: symlinks, fuera de HOME o ilegibles). Revisa arriba y restaura a mano lo que falte.`
|
|
6674
|
+
);
|
|
6675
|
+
process.exitCode = 1;
|
|
6676
|
+
return;
|
|
6677
|
+
}
|
|
8607
6678
|
console.log(`Restaurados ${restored} archivos.`);
|
|
8608
6679
|
return;
|
|
8609
6680
|
}
|
|
8610
6681
|
}
|
|
8611
6682
|
}
|
|
8612
|
-
if (process.argv[1] !== void 0 && import.meta.url ===
|
|
6683
|
+
if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
8613
6684
|
await main().catch((err) => {
|
|
8614
6685
|
console.error(err instanceof Error ? err.message : String(err));
|
|
8615
6686
|
process.exitCode = 1;
|