jorgex-stack 1.0.14 → 1.0.16
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 +42 -0
- package/dist/cli.js +512 -195
- package/package.json +1 -1
- package/stack/modes/programmatic/AGENTS.addendum.md +12 -0
- package/stack/modes/programmatic/agent-delegation.addendum.md +8 -0
- package/stack/modes/programmatic/final-output.schema.json +43 -0
- package/stack/modes/programmatic/orchestrator.addendum.md +15 -0
- package/stack/modes/programmatic/subagent.addendum.md +12 -0
- package/stack/plugins/opencode/engram.ts +54 -27
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,8 @@ import * as p6 from "@clack/prompts";
|
|
|
5
5
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
6
6
|
|
|
7
7
|
// src/install.ts
|
|
8
|
-
import
|
|
9
|
-
import
|
|
8
|
+
import fs14 from "fs";
|
|
9
|
+
import path19 from "path";
|
|
10
10
|
import * as p from "@clack/prompts";
|
|
11
11
|
|
|
12
12
|
// src/adapters/opencode.ts
|
|
@@ -1104,53 +1104,136 @@ args = [${args}]`);
|
|
|
1104
1104
|
}
|
|
1105
1105
|
};
|
|
1106
1106
|
|
|
1107
|
-
// src/lib/
|
|
1107
|
+
// src/lib/install-mode.ts
|
|
1108
1108
|
import fs6 from "fs";
|
|
1109
1109
|
import path10 from "path";
|
|
1110
|
+
var INSTALL_MODES = /* @__PURE__ */ new Set(["human", "programmatic"]);
|
|
1111
|
+
var SUBAGENT_CONCURRENCIES = /* @__PURE__ */ new Set(["serial", "parallel"]);
|
|
1112
|
+
var DEFAULT_INSTALL_MODE_PREFERENCE = {
|
|
1113
|
+
mode: "human",
|
|
1114
|
+
subagentConcurrency: "serial"
|
|
1115
|
+
};
|
|
1116
|
+
function installModePreferenceFile() {
|
|
1117
|
+
return path10.join(dataDir(), "install-mode.json");
|
|
1118
|
+
}
|
|
1119
|
+
function hasInstallModePreference(file = installModePreferenceFile()) {
|
|
1120
|
+
return fs6.existsSync(file);
|
|
1121
|
+
}
|
|
1122
|
+
function isInstallMode(value) {
|
|
1123
|
+
return INSTALL_MODES.has(value);
|
|
1124
|
+
}
|
|
1125
|
+
function isSubagentConcurrency(value) {
|
|
1126
|
+
return SUBAGENT_CONCURRENCIES.has(value);
|
|
1127
|
+
}
|
|
1128
|
+
function normalizeInstallModePreference(value) {
|
|
1129
|
+
if (!value) {
|
|
1130
|
+
throw new Error("Preferencia de instalaci\xF3n vac\xEDa o corrupta.");
|
|
1131
|
+
}
|
|
1132
|
+
if (value.mode === "human") {
|
|
1133
|
+
if (value.subagentConcurrency !== "serial") {
|
|
1134
|
+
throw new Error("Preferencia de instalaci\xF3n inconsistente: human solo puede usar serial.");
|
|
1135
|
+
}
|
|
1136
|
+
return DEFAULT_INSTALL_MODE_PREFERENCE;
|
|
1137
|
+
}
|
|
1138
|
+
if (value.mode === "programmatic") {
|
|
1139
|
+
const subagentConcurrency = value.subagentConcurrency;
|
|
1140
|
+
if (subagentConcurrency === void 0 || !isSubagentConcurrency(subagentConcurrency)) {
|
|
1141
|
+
throw new Error("Preferencia de instalaci\xF3n inv\xE1lida o corrupta.");
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
mode: "programmatic",
|
|
1145
|
+
subagentConcurrency
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
throw new Error("Preferencia de instalaci\xF3n inv\xE1lida o corrupta.");
|
|
1149
|
+
}
|
|
1150
|
+
function loadInstallModePreference(file = installModePreferenceFile()) {
|
|
1151
|
+
if (!fs6.existsSync(file)) return DEFAULT_INSTALL_MODE_PREFERENCE;
|
|
1152
|
+
try {
|
|
1153
|
+
const raw = fs6.readFileSync(file, "utf8");
|
|
1154
|
+
return normalizeInstallModePreference(JSON.parse(raw));
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1157
|
+
throw new Error(`No se pudo leer la preferencia de instalaci\xF3n en ${file}: ${message}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function saveInstallModePreference(file, value) {
|
|
1161
|
+
writeText(file, JSON.stringify(value, null, 2) + "\n");
|
|
1162
|
+
}
|
|
1163
|
+
function parseInstallModePreferenceFlags(mode, subagentConcurrency) {
|
|
1164
|
+
if (mode !== void 0 && !isInstallMode(mode)) {
|
|
1165
|
+
return { error: `Modo inv\xE1lido: ${mode}` };
|
|
1166
|
+
}
|
|
1167
|
+
if (subagentConcurrency !== void 0 && !isSubagentConcurrency(subagentConcurrency)) {
|
|
1168
|
+
return { error: `Concurrencia de subagentes inv\xE1lida: ${subagentConcurrency}` };
|
|
1169
|
+
}
|
|
1170
|
+
if (subagentConcurrency !== void 0 && mode !== "programmatic") {
|
|
1171
|
+
return { error: "--subagent-concurrency requiere --mode programmatic." };
|
|
1172
|
+
}
|
|
1173
|
+
if (mode === "human") {
|
|
1174
|
+
if (subagentConcurrency !== void 0) {
|
|
1175
|
+
return { error: "--subagent-concurrency no se puede usar con --mode human." };
|
|
1176
|
+
}
|
|
1177
|
+
return { preference: DEFAULT_INSTALL_MODE_PREFERENCE };
|
|
1178
|
+
}
|
|
1179
|
+
if (mode === "programmatic") {
|
|
1180
|
+
return {
|
|
1181
|
+
preference: {
|
|
1182
|
+
mode,
|
|
1183
|
+
subagentConcurrency: subagentConcurrency ?? "serial"
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
return {};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/lib/backup.ts
|
|
1191
|
+
import fs7 from "fs";
|
|
1192
|
+
import path11 from "path";
|
|
1110
1193
|
import crypto from "crypto";
|
|
1111
1194
|
var KEEP_BACKUPS = 10;
|
|
1112
1195
|
function backupsRoot() {
|
|
1113
|
-
return
|
|
1196
|
+
return path11.join(dataDir(), "backups");
|
|
1114
1197
|
}
|
|
1115
1198
|
function compositeChecksum(files) {
|
|
1116
1199
|
const hash = crypto.createHash("sha256");
|
|
1117
1200
|
for (const file of [...files].sort()) {
|
|
1118
|
-
const content = crypto.createHash("sha256").update(
|
|
1201
|
+
const content = crypto.createHash("sha256").update(fs7.readFileSync(file)).digest("hex");
|
|
1119
1202
|
hash.update(`${file}:${content}
|
|
1120
1203
|
`);
|
|
1121
1204
|
}
|
|
1122
1205
|
return hash.digest("hex");
|
|
1123
1206
|
}
|
|
1124
1207
|
function createBackup(files, label, root = backupsRoot()) {
|
|
1125
|
-
const existing = [...new Set(files)].filter((f) =>
|
|
1208
|
+
const existing = [...new Set(files)].filter((f) => fs7.existsSync(f));
|
|
1126
1209
|
if (existing.length === 0) return null;
|
|
1127
1210
|
const checksum = compositeChecksum(existing);
|
|
1128
1211
|
const latest = listBackups(root)[0];
|
|
1129
1212
|
if (latest?.checksum === checksum) return latest;
|
|
1130
1213
|
const base = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${label}`;
|
|
1131
1214
|
let id = base;
|
|
1132
|
-
let dir =
|
|
1133
|
-
for (let n = 1;
|
|
1215
|
+
let dir = path11.join(root, id);
|
|
1216
|
+
for (let n = 1; fs7.existsSync(dir); n++) {
|
|
1134
1217
|
id = `${base}-${n}`;
|
|
1135
|
-
dir =
|
|
1218
|
+
dir = path11.join(root, id);
|
|
1136
1219
|
}
|
|
1137
|
-
ensureDir(
|
|
1220
|
+
ensureDir(path11.join(dir, "files"));
|
|
1138
1221
|
const entries = existing.map((original, i) => {
|
|
1139
|
-
const stored =
|
|
1140
|
-
|
|
1222
|
+
const stored = path11.join(dir, "files", `${String(i).padStart(4, "0")}-${path11.basename(original)}`);
|
|
1223
|
+
fs7.copyFileSync(original, stored);
|
|
1141
1224
|
return { original, stored };
|
|
1142
1225
|
});
|
|
1143
1226
|
const info = { id, label, createdAt: (/* @__PURE__ */ new Date()).toISOString(), files: entries, checksum };
|
|
1144
|
-
writeText(
|
|
1227
|
+
writeText(path11.join(dir, "manifest.json"), JSON.stringify(info, null, 2) + "\n");
|
|
1145
1228
|
pruneBackups(root);
|
|
1146
1229
|
return info;
|
|
1147
1230
|
}
|
|
1148
1231
|
function listBackups(root = backupsRoot()) {
|
|
1149
|
-
if (!
|
|
1232
|
+
if (!fs7.existsSync(root)) return [];
|
|
1150
1233
|
const infos = [];
|
|
1151
|
-
for (const entry of
|
|
1234
|
+
for (const entry of fs7.readdirSync(root, { withFileTypes: true })) {
|
|
1152
1235
|
if (!entry.isDirectory()) continue;
|
|
1153
|
-
const manifest = readTextIfExists(
|
|
1236
|
+
const manifest = readTextIfExists(path11.join(root, entry.name, "manifest.json"));
|
|
1154
1237
|
if (manifest === null) continue;
|
|
1155
1238
|
try {
|
|
1156
1239
|
infos.push(JSON.parse(manifest));
|
|
@@ -1165,10 +1248,10 @@ function restoreBackup(id, root = backupsRoot(), boundary = HOME) {
|
|
|
1165
1248
|
if (!info) throw new Error(`Backup no encontrado: ${id}`);
|
|
1166
1249
|
let restored = 0;
|
|
1167
1250
|
for (const { original, stored } of info.files) {
|
|
1168
|
-
if (!
|
|
1251
|
+
if (!fs7.existsSync(stored)) continue;
|
|
1169
1252
|
if (!isContainedIn(original, boundary)) continue;
|
|
1170
|
-
ensureDir(
|
|
1171
|
-
|
|
1253
|
+
ensureDir(path11.dirname(original));
|
|
1254
|
+
fs7.copyFileSync(stored, original);
|
|
1172
1255
|
restored++;
|
|
1173
1256
|
}
|
|
1174
1257
|
return restored;
|
|
@@ -1176,15 +1259,15 @@ function restoreBackup(id, root = backupsRoot(), boundary = HOME) {
|
|
|
1176
1259
|
function pruneBackups(root) {
|
|
1177
1260
|
const all = listBackups(root);
|
|
1178
1261
|
for (const old of all.slice(KEEP_BACKUPS)) {
|
|
1179
|
-
|
|
1262
|
+
fs7.rmSync(path11.join(root, old.id), { recursive: true, force: true });
|
|
1180
1263
|
}
|
|
1181
1264
|
}
|
|
1182
1265
|
|
|
1183
1266
|
// src/lib/manifest.ts
|
|
1184
|
-
import
|
|
1185
|
-
import
|
|
1267
|
+
import fs8 from "fs";
|
|
1268
|
+
import path12 from "path";
|
|
1186
1269
|
function manifestFile() {
|
|
1187
|
-
return
|
|
1270
|
+
return path12.join(dataDir(), "manifest.json");
|
|
1188
1271
|
}
|
|
1189
1272
|
function readManifest(file = manifestFile()) {
|
|
1190
1273
|
const raw = readTextIfExists(file);
|
|
@@ -1208,23 +1291,92 @@ function removeRuntimeManifest(id, file = manifestFile()) {
|
|
|
1208
1291
|
writeText(file, JSON.stringify(manifest, null, 2) + "\n");
|
|
1209
1292
|
}
|
|
1210
1293
|
function findOrphans(prevOwned, currentTargets, root = HOME) {
|
|
1211
|
-
return prevOwned.map((f) =>
|
|
1212
|
-
(f) => !currentTargets.has(f) &&
|
|
1294
|
+
return prevOwned.map((f) => path12.resolve(f)).filter(
|
|
1295
|
+
(f) => !currentTargets.has(f) && path12.basename(f) !== "engram.ts" && isContainedIn(f, root) && fs8.existsSync(f)
|
|
1213
1296
|
);
|
|
1214
1297
|
}
|
|
1215
1298
|
|
|
1216
1299
|
// src/components/system-prompt.ts
|
|
1217
|
-
import
|
|
1218
|
-
import
|
|
1219
|
-
|
|
1300
|
+
import path14 from "path";
|
|
1301
|
+
import fs10 from "fs";
|
|
1302
|
+
|
|
1303
|
+
// src/lib/mode-composition.ts
|
|
1304
|
+
import fs9 from "fs";
|
|
1305
|
+
import path13 from "path";
|
|
1306
|
+
var PROGRAMMATIC_ROOT = ["modes", "programmatic"];
|
|
1307
|
+
var PROGRAMMATIC_MARKER = "<!-- jorgex:programmatic-mode -->";
|
|
1308
|
+
var LEGACY_RESULT_CONTRACT_SECTION = /\n?##\s+Result contract[\s\S]*$/;
|
|
1309
|
+
var LEGACY_SKILL_DELEGATION_SECTION = /\n?##\s+Formato obligatorio[\s\S]*$/;
|
|
1310
|
+
var LEGACY_DELEGATION_LINE = /- For each `→ \[agent\]: \.\.\.` line, launch the corresponding specialist\./;
|
|
1311
|
+
var LEGACY_PROGRAMMATIC_PHRASES = [
|
|
1312
|
+
[/\bResult contract\b/g, "strict JSON handoff"],
|
|
1313
|
+
[/Status \/ Delegations \/ Risks/g, "status, delegations, and risks"],
|
|
1314
|
+
[LEGACY_DELEGATION_LINE, "- Process the JSON `delegations[]` array and launch the corresponding specialist."]
|
|
1315
|
+
];
|
|
1316
|
+
var normalize = (value) => value.replace(/\r\n/g, "\n");
|
|
1317
|
+
function loadProgrammaticAddendum(stackDir, fileName) {
|
|
1318
|
+
return normalize(fs9.readFileSync(path13.join(stackDir, ...PROGRAMMATIC_ROOT, fileName), "utf8")).trim();
|
|
1319
|
+
}
|
|
1320
|
+
function appendAddendum(base, addendum) {
|
|
1321
|
+
const normalizedBase = normalize(base);
|
|
1322
|
+
if (normalizedBase.includes(PROGRAMMATIC_MARKER)) return normalizedBase;
|
|
1323
|
+
return `${normalizedBase.trimEnd()}
|
|
1324
|
+
|
|
1325
|
+
${addendum}
|
|
1326
|
+
`;
|
|
1327
|
+
}
|
|
1328
|
+
function stripLegacyResultContract(body) {
|
|
1329
|
+
return LEGACY_PROGRAMMATIC_PHRASES.reduce(
|
|
1330
|
+
(text2, [pattern, replacement]) => text2.replace(pattern, replacement),
|
|
1331
|
+
normalize(body).replace(LEGACY_RESULT_CONTRACT_SECTION, "")
|
|
1332
|
+
).trimEnd();
|
|
1333
|
+
}
|
|
1334
|
+
function concurrencyRule(concurrency) {
|
|
1335
|
+
if (concurrency === "parallel") {
|
|
1336
|
+
return [
|
|
1337
|
+
"- Parallel delegation is allowed when safe.",
|
|
1338
|
+
"- Set max_parallel_subagents explicitly when the runtime supports it."
|
|
1339
|
+
].join("\n");
|
|
1340
|
+
}
|
|
1341
|
+
return [
|
|
1342
|
+
"- Launch one subagent at a time.",
|
|
1343
|
+
"- No parallel delegation."
|
|
1344
|
+
].join("\n");
|
|
1345
|
+
}
|
|
1346
|
+
function composeProgrammaticSystemPrompt(stackDir, content, mode) {
|
|
1347
|
+
if (mode !== "programmatic") return normalize(content);
|
|
1348
|
+
return appendAddendum(normalize(content), loadProgrammaticAddendum(stackDir, "AGENTS.addendum.md"));
|
|
1349
|
+
}
|
|
1350
|
+
function composeProgrammaticAgentBody(stackDir, agent, mode, concurrency) {
|
|
1351
|
+
if (mode !== "programmatic") return normalize(agent.body);
|
|
1352
|
+
const fileName = agent.mode === "primary" ? "orchestrator.addendum.md" : "subagent.addendum.md";
|
|
1353
|
+
let addendum = loadProgrammaticAddendum(stackDir, fileName);
|
|
1354
|
+
if (agent.mode === "primary") {
|
|
1355
|
+
addendum = addendum.replace("{{CONCURRENCY_RULE}}", concurrencyRule(concurrency ?? "serial"));
|
|
1356
|
+
}
|
|
1357
|
+
return appendAddendum(stripLegacyResultContract(agent.body), addendum);
|
|
1358
|
+
}
|
|
1359
|
+
function composeProgrammaticSkillBody(stackDir, skillPath, content, mode) {
|
|
1360
|
+
if (mode !== "programmatic") return normalize(content);
|
|
1361
|
+
if (skillPath !== path13.join("agent-delegation", "SKILL.md")) return normalize(content);
|
|
1362
|
+
const base = normalize(content).replace(
|
|
1363
|
+
"Las delegaciones van **en tu output final**, en el formato de abajo. El orquestador las lee y decide a qui\xE9n invocar.",
|
|
1364
|
+
"Las delegaciones van como strings en el JSON final `delegations[]`. El orquestador las lee y decide a qui\xE9n invocar."
|
|
1365
|
+
).replace(LEGACY_SKILL_DELEGATION_SECTION, "").trimEnd();
|
|
1366
|
+
return appendAddendum(base, loadProgrammaticAddendum(stackDir, "agent-delegation.addendum.md"));
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/components/system-prompt.ts
|
|
1370
|
+
var normalize2 = (s) => s.replace(/\r\n/g, "\n");
|
|
1220
1371
|
function planSystemPrompt(adapter, ctx) {
|
|
1221
1372
|
const target = adapter.paths(ctx.configDir).systemPromptFile;
|
|
1222
|
-
const agentsMd =
|
|
1373
|
+
const agentsMd = normalize2(fs10.readFileSync(path14.join(ctx.stackDir, "system-prompt", "AGENTS.md"), "utf8"));
|
|
1223
1374
|
const protocol = stripLeadingHtmlComments(
|
|
1224
|
-
|
|
1375
|
+
normalize2(fs10.readFileSync(path14.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8"))
|
|
1225
1376
|
);
|
|
1377
|
+
const composedAgentsMd = composeProgrammaticSystemPrompt(ctx.stackDir, agentsMd, ctx.mode);
|
|
1226
1378
|
let content = readTextIfExists(target);
|
|
1227
|
-
content = upsertMarkdownSection(content, "system-prompt",
|
|
1379
|
+
content = upsertMarkdownSection(content, "system-prompt", composedAgentsMd);
|
|
1228
1380
|
if (adapter.injectEngramProtocol(ctx)) {
|
|
1229
1381
|
content = upsertMarkdownSection(content, "engram-protocol", protocol);
|
|
1230
1382
|
} else {
|
|
@@ -1234,7 +1386,7 @@ function planSystemPrompt(adapter, ctx) {
|
|
|
1234
1386
|
}
|
|
1235
1387
|
|
|
1236
1388
|
// src/components/agents.ts
|
|
1237
|
-
import
|
|
1389
|
+
import path15 from "path";
|
|
1238
1390
|
function planAgents(adapter, ctx) {
|
|
1239
1391
|
const { agentsDir, commandsDir, outputStylesDir, skillsDir, profilesDir, scriptsDir } = adapter.paths(ctx.configDir);
|
|
1240
1392
|
const dirFor = {
|
|
@@ -1245,51 +1397,62 @@ function planAgents(adapter, ctx) {
|
|
|
1245
1397
|
profile: profilesDir
|
|
1246
1398
|
};
|
|
1247
1399
|
const scriptsBase = scriptsDir.replace(/\\/g, "/");
|
|
1248
|
-
return loadCanonicalAgents(
|
|
1249
|
-
|
|
1400
|
+
return loadCanonicalAgents(path15.join(ctx.stackDir, "agents")).flatMap((agent) => {
|
|
1401
|
+
const composedAgent = {
|
|
1402
|
+
...agent,
|
|
1403
|
+
body: composeProgrammaticAgentBody(ctx.stackDir, agent, ctx.mode, ctx.subagentConcurrency)
|
|
1404
|
+
};
|
|
1405
|
+
return adapter.renderAgent(composedAgent, ctx.models).flatMap((rendered) => {
|
|
1250
1406
|
const dir = dirFor[rendered.kind];
|
|
1251
1407
|
if (dir === null) {
|
|
1252
1408
|
ctx.warnings.push(`${adapter.name}: sin destino para '${rendered.kind}' (${rendered.file}) \u2014 omitido.`);
|
|
1253
1409
|
return [];
|
|
1254
1410
|
}
|
|
1255
1411
|
const content = rendered.content.replace(/\{\{SCRIPTS_DIR\}\}/g, scriptsBase);
|
|
1256
|
-
return [{ kind: "write", target:
|
|
1257
|
-
})
|
|
1258
|
-
);
|
|
1412
|
+
return [{ kind: "write", target: path15.join(dir, rendered.file), content }];
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1259
1415
|
}
|
|
1260
1416
|
|
|
1261
1417
|
// src/components/skills.ts
|
|
1262
|
-
import
|
|
1418
|
+
import path16 from "path";
|
|
1419
|
+
import fs11 from "fs";
|
|
1263
1420
|
function planSkills(adapter, ctx) {
|
|
1264
1421
|
const { skillsDir } = adapter.paths(ctx.configDir);
|
|
1265
|
-
const source =
|
|
1266
|
-
return listFilesRecursive(source).map((file) =>
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1422
|
+
const source = path16.join(ctx.stackDir, "skills");
|
|
1423
|
+
return listFilesRecursive(source).map((file) => {
|
|
1424
|
+
const relative = path16.relative(source, file);
|
|
1425
|
+
if (relative === path16.join("agent-delegation", "SKILL.md") && ctx.mode === "programmatic") {
|
|
1426
|
+
return {
|
|
1427
|
+
kind: "write",
|
|
1428
|
+
target: path16.join(skillsDir, relative),
|
|
1429
|
+
content: composeProgrammaticSkillBody(ctx.stackDir, relative, fs11.readFileSync(file, "utf8"), ctx.mode)
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
return { kind: "copy", source: file, target: path16.join(skillsDir, relative) };
|
|
1433
|
+
});
|
|
1271
1434
|
}
|
|
1272
1435
|
|
|
1273
1436
|
// src/components/commands.ts
|
|
1274
|
-
import
|
|
1275
|
-
import
|
|
1437
|
+
import path17 from "path";
|
|
1438
|
+
import fs12 from "fs";
|
|
1276
1439
|
function planCommands(adapter, ctx) {
|
|
1277
1440
|
const { commandsDir } = adapter.paths(ctx.configDir);
|
|
1278
|
-
const source =
|
|
1279
|
-
if (!
|
|
1441
|
+
const source = path17.join(ctx.stackDir, "commands");
|
|
1442
|
+
if (!fs12.existsSync(source)) return [];
|
|
1280
1443
|
const commandFiles = [
|
|
1281
1444
|
...listMarkdownFiles(source),
|
|
1282
|
-
...listMarkdownFiles(
|
|
1445
|
+
...listMarkdownFiles(path17.join(source, adapter.id))
|
|
1283
1446
|
];
|
|
1284
1447
|
return commandFiles.map(({ file, fullPath }) => {
|
|
1285
|
-
const raw =
|
|
1448
|
+
const raw = fs12.readFileSync(fullPath, "utf8").replace(/\r\n/g, "\n");
|
|
1286
1449
|
const rendered = adapter.renderCommand(file, raw);
|
|
1287
|
-
return { kind: "write", target:
|
|
1450
|
+
return { kind: "write", target: path17.join(commandsDir, rendered.file), content: rendered.content };
|
|
1288
1451
|
});
|
|
1289
1452
|
}
|
|
1290
1453
|
function listMarkdownFiles(dir) {
|
|
1291
|
-
if (!
|
|
1292
|
-
return
|
|
1454
|
+
if (!fs12.existsSync(dir)) return [];
|
|
1455
|
+
return fs12.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => ({ file: entry.name, fullPath: path17.join(dir, entry.name) }));
|
|
1293
1456
|
}
|
|
1294
1457
|
|
|
1295
1458
|
// src/components/hooks.ts
|
|
@@ -1303,26 +1466,27 @@ function planMcp(adapter, ctx) {
|
|
|
1303
1466
|
}
|
|
1304
1467
|
|
|
1305
1468
|
// src/components/plugins.ts
|
|
1306
|
-
import
|
|
1307
|
-
import
|
|
1469
|
+
import path18 from "path";
|
|
1470
|
+
import fs13 from "fs";
|
|
1308
1471
|
function planPlugins(adapter, ctx) {
|
|
1309
1472
|
const { pluginsDir } = adapter.paths(ctx.configDir);
|
|
1310
1473
|
if (pluginsDir === null) return [];
|
|
1311
|
-
const source =
|
|
1312
|
-
if (!
|
|
1474
|
+
const source = path18.join(ctx.stackDir, "plugins", adapter.id);
|
|
1475
|
+
if (!fs13.existsSync(source)) return [];
|
|
1313
1476
|
return listFilesRecursive(source).filter((f) => f.endsWith(".ts")).map((sourceFile) => {
|
|
1314
|
-
const target =
|
|
1315
|
-
const raw =
|
|
1477
|
+
const target = path18.join(pluginsDir, path18.relative(source, sourceFile));
|
|
1478
|
+
const raw = fs13.readFileSync(sourceFile, "utf8");
|
|
1316
1479
|
let content = raw;
|
|
1317
1480
|
if (content.includes('"{{ENGRAM_BIN}}"')) {
|
|
1318
1481
|
content = content.replace(/"\{\{ENGRAM_BIN\}\}"/g, JSON.stringify(ctx.engramBin ?? "engram"));
|
|
1319
1482
|
}
|
|
1320
1483
|
if (content.includes('"{{ENGRAM_PROTOCOL}}"')) {
|
|
1321
1484
|
const protocol = stripLeadingHtmlComments(
|
|
1322
|
-
|
|
1485
|
+
fs13.readFileSync(path18.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8").replace(/\r\n/g, "\n")
|
|
1323
1486
|
);
|
|
1324
1487
|
content = content.replace(/"\{\{ENGRAM_PROTOCOL\}\}"/g, JSON.stringify(protocol));
|
|
1325
1488
|
}
|
|
1489
|
+
content = content.replace(/(from\s+["'])(\.{1,2}\/[^"']+?)\.js(["'])/g, "$1$2.ts$3");
|
|
1326
1490
|
if (content === raw) return { kind: "copy", source: sourceFile, target };
|
|
1327
1491
|
return { kind: "write", target, content };
|
|
1328
1492
|
});
|
|
@@ -1334,12 +1498,14 @@ var ADAPTERS = {
|
|
|
1334
1498
|
"claude-code": claudeCodeAdapter,
|
|
1335
1499
|
codex: codexAdapter
|
|
1336
1500
|
};
|
|
1337
|
-
function makeContext(adapter, configDir) {
|
|
1501
|
+
function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
|
|
1338
1502
|
const models = loadModelMap()[adapter.id];
|
|
1339
1503
|
if (!models) return null;
|
|
1340
1504
|
return {
|
|
1341
1505
|
stackDir: stackRoot(),
|
|
1342
1506
|
configDir,
|
|
1507
|
+
mode: mode.mode,
|
|
1508
|
+
subagentConcurrency: mode.subagentConcurrency,
|
|
1343
1509
|
engramBin: detectEngram(),
|
|
1344
1510
|
models,
|
|
1345
1511
|
warnings: []
|
|
@@ -1363,7 +1529,7 @@ function diffPlan(plan) {
|
|
|
1363
1529
|
if (current === null) return { action, status: "create" };
|
|
1364
1530
|
return { action, status: current === action.content ? "unchanged" : "update" };
|
|
1365
1531
|
}
|
|
1366
|
-
if (!
|
|
1532
|
+
if (!fs14.existsSync(action.target)) return { action, status: "create" };
|
|
1367
1533
|
return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
|
|
1368
1534
|
});
|
|
1369
1535
|
}
|
|
@@ -1373,35 +1539,49 @@ function applyChanges(changes) {
|
|
|
1373
1539
|
else copyFile(action.source, action.target);
|
|
1374
1540
|
}
|
|
1375
1541
|
}
|
|
1376
|
-
function collectAllCurrentTargets() {
|
|
1542
|
+
function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
|
|
1377
1543
|
const targets = /* @__PURE__ */ new Set();
|
|
1378
1544
|
let complete = true;
|
|
1545
|
+
const warnings = [];
|
|
1379
1546
|
for (const adapter of Object.values(ADAPTERS)) {
|
|
1380
1547
|
const detection = adapter.detect();
|
|
1381
1548
|
if (!detection.installed) continue;
|
|
1382
|
-
const ctx = makeContext(adapter, detection.configDir);
|
|
1383
|
-
if (!ctx)
|
|
1549
|
+
const ctx = makeContext(adapter, detection.configDir, mode);
|
|
1550
|
+
if (!ctx) {
|
|
1551
|
+
complete = false;
|
|
1552
|
+
warnings.push(`${adapter.name}: limpieza de hu\xE9rfanos deshabilitada \u2014 falta contexto/model-map instalable para este runtime.`);
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1384
1555
|
try {
|
|
1385
|
-
for (const action of buildPlan(adapter, ctx)) targets.add(
|
|
1386
|
-
} catch {
|
|
1556
|
+
for (const action of buildPlan(adapter, ctx)) targets.add(path19.resolve(action.target));
|
|
1557
|
+
} catch (error) {
|
|
1387
1558
|
complete = false;
|
|
1559
|
+
warnings.push(
|
|
1560
|
+
`${adapter.name}: limpieza de hu\xE9rfanos deshabilitada \u2014 no se pudo construir el plan completo (${error instanceof Error ? error.message : String(error)}).`
|
|
1561
|
+
);
|
|
1388
1562
|
}
|
|
1389
1563
|
}
|
|
1390
|
-
return { targets, complete };
|
|
1564
|
+
return { targets, complete, warnings };
|
|
1391
1565
|
}
|
|
1392
1566
|
async function runInstall(opts) {
|
|
1393
1567
|
p.intro(`jorgex-stack ${opts.dryRun ? "install (dry-run)" : "install"}`);
|
|
1394
1568
|
const stackDir = stackRoot();
|
|
1395
1569
|
const engramBin = detectEngram();
|
|
1570
|
+
const modePreference = opts.mode === void 0 ? opts.targetDir === void 0 ? loadInstallModePreference() : DEFAULT_INSTALL_MODE_PREFERENCE : normalizeInstallModePreference(opts.mode);
|
|
1571
|
+
const useManifest = opts.targetDir === void 0;
|
|
1396
1572
|
const modelMap = loadModelMap();
|
|
1397
|
-
ensureModelMapFile();
|
|
1573
|
+
if (useManifest) ensureModelMapFile();
|
|
1398
1574
|
p.log.info(engramBin ? `Engram detectado: ${engramBin} (se respeta, D7)` : "Engram NO detectado.");
|
|
1399
|
-
const
|
|
1400
|
-
const current = useManifest ? collectAllCurrentTargets() : { targets: /* @__PURE__ */ new Set(), complete: false };
|
|
1575
|
+
const current = useManifest ? collectAllCurrentTargets(modePreference) : { targets: /* @__PURE__ */ new Set(), complete: false, warnings: [] };
|
|
1401
1576
|
const canOrphan = useManifest && current.complete;
|
|
1402
1577
|
const canonicalMcp = loadCanonicalMcp(stackDir);
|
|
1403
1578
|
const canonicalHooks = loadCanonicalHooks(stackDir);
|
|
1579
|
+
if (useManifest && (!current.complete || current.warnings.length > 0)) {
|
|
1580
|
+
p.log.warn("Limpieza de hu\xE9rfanos deshabilitada: no se pudo construir el plan completo de todos los runtimes.");
|
|
1581
|
+
for (const warning of current.warnings) p.log.warn(warning);
|
|
1582
|
+
}
|
|
1404
1583
|
let exitCode = 0;
|
|
1584
|
+
let successfulRuns = 0;
|
|
1405
1585
|
for (const id of opts.runtimes) {
|
|
1406
1586
|
const adapter = ADAPTERS[id];
|
|
1407
1587
|
if (!adapter) {
|
|
@@ -1422,6 +1602,8 @@ async function runInstall(opts) {
|
|
|
1422
1602
|
const ctx = {
|
|
1423
1603
|
stackDir,
|
|
1424
1604
|
configDir,
|
|
1605
|
+
mode: modePreference.mode,
|
|
1606
|
+
subagentConcurrency: modePreference.subagentConcurrency,
|
|
1425
1607
|
engramBin,
|
|
1426
1608
|
models,
|
|
1427
1609
|
warnings: []
|
|
@@ -1431,8 +1613,8 @@ async function runInstall(opts) {
|
|
|
1431
1613
|
let creates = diff.filter((d) => d.status === "create");
|
|
1432
1614
|
let updates = diff.filter((d) => d.status === "update");
|
|
1433
1615
|
let changes = [...creates, ...updates];
|
|
1434
|
-
const prevManifest =
|
|
1435
|
-
const orphans = prevManifest ? findOrphans(prevManifest.owned, current.targets) : [];
|
|
1616
|
+
const prevManifest = useManifest ? readManifest().runtimes[id] : void 0;
|
|
1617
|
+
const orphans = canOrphan && prevManifest ? findOrphans(prevManifest.owned, current.targets) : [];
|
|
1436
1618
|
p.log.step(`${adapter.name} \u2192 ${configDir}`);
|
|
1437
1619
|
p.log.info(
|
|
1438
1620
|
`${diff.length} archivos gestionados: ${creates.length} nuevos, ${updates.length} modificados, ${diff.length - changes.length} sin cambios`
|
|
@@ -1447,13 +1629,17 @@ async function runInstall(opts) {
|
|
|
1447
1629
|
}
|
|
1448
1630
|
const writeManifest = () => {
|
|
1449
1631
|
if (!useManifest) return;
|
|
1450
|
-
const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) =>
|
|
1451
|
-
const
|
|
1632
|
+
const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) => path19.resolve(a.target)));
|
|
1633
|
+
const keepTarget = (target) => !unmergeTargets.has(target);
|
|
1634
|
+
const liveOwned = plan.map((a) => path19.resolve(a.target)).filter(keepTarget);
|
|
1635
|
+
const previousOwned = (prevManifest?.owned ?? []).map((target) => path19.resolve(target)).filter(keepTarget);
|
|
1636
|
+
const owned = canOrphan ? liveOwned : [.../* @__PURE__ */ new Set([...previousOwned, ...liveOwned])];
|
|
1452
1637
|
writeRuntimeManifest(id, { configDir, owned, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1453
1638
|
};
|
|
1454
1639
|
if (changes.length === 0 && orphans.length === 0) {
|
|
1455
1640
|
writeManifest();
|
|
1456
1641
|
p.log.success(`${adapter.name}: ya al d\xEDa (idempotente).`);
|
|
1642
|
+
successfulRuns++;
|
|
1457
1643
|
continue;
|
|
1458
1644
|
}
|
|
1459
1645
|
if (!opts.yes && process.stdout.isTTY) {
|
|
@@ -1469,12 +1655,12 @@ async function runInstall(opts) {
|
|
|
1469
1655
|
updates = diff.filter((d) => d.status === "update");
|
|
1470
1656
|
changes = [...creates, ...updates];
|
|
1471
1657
|
}
|
|
1472
|
-
const backup = createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`);
|
|
1658
|
+
const backup = useManifest ? createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`) : null;
|
|
1473
1659
|
if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
1474
1660
|
applyChanges(changes);
|
|
1475
|
-
const pruneRoot = useManifest ? HOME :
|
|
1661
|
+
const pruneRoot = useManifest ? HOME : path19.dirname(configDir);
|
|
1476
1662
|
for (const orphan of orphans) {
|
|
1477
|
-
|
|
1663
|
+
fs14.rmSync(orphan, { force: true });
|
|
1478
1664
|
pruneEmptyDirs(orphan, pruneRoot);
|
|
1479
1665
|
}
|
|
1480
1666
|
const verifyCtx = { ...ctx, warnings: [] };
|
|
@@ -1486,15 +1672,19 @@ async function runInstall(opts) {
|
|
|
1486
1672
|
} else {
|
|
1487
1673
|
writeManifest();
|
|
1488
1674
|
p.log.success(`${adapter.name}: ${changes.length} archivos aplicados y verificados (idempotente).`);
|
|
1675
|
+
successfulRuns++;
|
|
1489
1676
|
}
|
|
1490
1677
|
}
|
|
1678
|
+
if (useManifest && !opts.dryRun && exitCode === 0 && successfulRuns > 0) {
|
|
1679
|
+
saveInstallModePreference(installModePreferenceFile(), modePreference);
|
|
1680
|
+
}
|
|
1491
1681
|
p.outro(opts.dryRun ? "Dry-run: no se ha escrito nada." : "Hecho.");
|
|
1492
1682
|
return exitCode;
|
|
1493
1683
|
}
|
|
1494
1684
|
|
|
1495
1685
|
// src/uninstall.ts
|
|
1496
|
-
import
|
|
1497
|
-
import
|
|
1686
|
+
import fs15 from "fs";
|
|
1687
|
+
import path20 from "path";
|
|
1498
1688
|
import * as p2 from "@clack/prompts";
|
|
1499
1689
|
async function runUninstall(opts) {
|
|
1500
1690
|
p2.intro(`jorgex-stack ${opts.dryRun ? "uninstall (dry-run)" : "uninstall"}`);
|
|
@@ -1524,7 +1714,7 @@ async function runUninstall(opts) {
|
|
|
1524
1714
|
if (!detection.installed) continue;
|
|
1525
1715
|
const keepCtx = makeContext(keep, detection.configDir);
|
|
1526
1716
|
if (!keepCtx) continue;
|
|
1527
|
-
for (const action of buildPlan(keep, keepCtx)) retained.add(
|
|
1717
|
+
for (const action of buildPlan(keep, keepCtx)) retained.add(path20.resolve(action.target));
|
|
1528
1718
|
}
|
|
1529
1719
|
for (const id of opts.runtimes) {
|
|
1530
1720
|
const adapter = ADAPTERS[id];
|
|
@@ -1542,15 +1732,15 @@ async function runUninstall(opts) {
|
|
|
1542
1732
|
if (!ctx) continue;
|
|
1543
1733
|
ctx.preserveEngram = !removeEngram;
|
|
1544
1734
|
const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
|
|
1545
|
-
const mergedTargets = new Set(unmerge.map((a) =>
|
|
1735
|
+
const mergedTargets = new Set(unmerge.map((a) => path20.resolve(a.target)));
|
|
1546
1736
|
const usingRealConfig = opts.targetDir === void 0;
|
|
1547
1737
|
const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
|
|
1548
|
-
const pruneRoot = usingRealConfig ? HOME :
|
|
1738
|
+
const pruneRoot = usingRealConfig ? HOME : path20.dirname(configDir);
|
|
1549
1739
|
const planTargets = [
|
|
1550
|
-
.../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) =>
|
|
1551
|
-
].filter((t) => !mergedTargets.has(t) &&
|
|
1740
|
+
.../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path20.resolve(a.target)), ...prevOwned.map((t) => path20.resolve(t))])
|
|
1741
|
+
].filter((t) => !mergedTargets.has(t) && fs15.existsSync(t));
|
|
1552
1742
|
const deleteTargets = planTargets.filter(
|
|
1553
|
-
(t) => !retained.has(t) && !(ctx.preserveEngram &&
|
|
1743
|
+
(t) => !retained.has(t) && !(ctx.preserveEngram && path20.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
|
|
1554
1744
|
);
|
|
1555
1745
|
const sharedKept = planTargets.length - deleteTargets.length;
|
|
1556
1746
|
p2.log.step(`${adapter.name} \u2192 ${configDir}`);
|
|
@@ -1558,18 +1748,18 @@ async function runUninstall(opts) {
|
|
|
1558
1748
|
if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
|
|
1559
1749
|
if (opts.dryRun) continue;
|
|
1560
1750
|
const backup = createBackup(
|
|
1561
|
-
[...deleteTargets, ...unmerge.map((a) => a.target).filter((t) =>
|
|
1751
|
+
[...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs15.existsSync(t))],
|
|
1562
1752
|
`uninstall-${id}`
|
|
1563
1753
|
);
|
|
1564
1754
|
if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
1565
1755
|
for (const target of deleteTargets) {
|
|
1566
|
-
|
|
1756
|
+
fs15.rmSync(target, { force: true });
|
|
1567
1757
|
pruneEmptyDirs(target, pruneRoot);
|
|
1568
1758
|
}
|
|
1569
1759
|
for (const action of unmerge) {
|
|
1570
1760
|
if (action.kind !== "write") continue;
|
|
1571
1761
|
if (action.content.trim() === "") {
|
|
1572
|
-
|
|
1762
|
+
fs15.rmSync(action.target, { force: true });
|
|
1573
1763
|
} else {
|
|
1574
1764
|
writeText(action.target, action.content);
|
|
1575
1765
|
}
|
|
@@ -1582,8 +1772,8 @@ async function runUninstall(opts) {
|
|
|
1582
1772
|
}
|
|
1583
1773
|
|
|
1584
1774
|
// src/doctor.ts
|
|
1585
|
-
import
|
|
1586
|
-
import
|
|
1775
|
+
import path21 from "path";
|
|
1776
|
+
import fs16 from "fs";
|
|
1587
1777
|
import * as p3 from "@clack/prompts";
|
|
1588
1778
|
function engramVersion(bin) {
|
|
1589
1779
|
const out = runDetectedBin(bin, ["--version"], 5e3);
|
|
@@ -1591,7 +1781,7 @@ function engramVersion(bin) {
|
|
|
1591
1781
|
return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
|
|
1592
1782
|
}
|
|
1593
1783
|
function context7KeyConfigured(id, configDir) {
|
|
1594
|
-
const file = id === "codex" ?
|
|
1784
|
+
const file = id === "codex" ? path21.join(configDir, "config.toml") : id === "claude-code" ? path21.join(path21.dirname(configDir), `${path21.basename(configDir)}.json`) : path21.join(configDir, "opencode.json");
|
|
1595
1785
|
const content = readTextIfExists(file);
|
|
1596
1786
|
if (content === null) return null;
|
|
1597
1787
|
const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
|
|
@@ -1614,22 +1804,28 @@ async function runDoctor() {
|
|
|
1614
1804
|
p3.log.success(`Engram: ${version} (${engramBin})`);
|
|
1615
1805
|
}
|
|
1616
1806
|
}
|
|
1617
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
1618
|
-
const engramDb =
|
|
1619
|
-
if (
|
|
1620
|
-
const sizeMb = (
|
|
1807
|
+
const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path21.join(HOME, ".engram");
|
|
1808
|
+
const engramDb = path21.join(engramDataDir, "engram.db");
|
|
1809
|
+
if (fs16.existsSync(engramDb)) {
|
|
1810
|
+
const sizeMb = (fs16.statSync(engramDb).size / 1024 / 1024).toFixed(1);
|
|
1621
1811
|
p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
|
|
1622
1812
|
}
|
|
1623
|
-
if (!
|
|
1813
|
+
if (!fs16.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
|
|
1624
1814
|
const manifest = readManifest();
|
|
1625
|
-
const
|
|
1815
|
+
const modePreference = loadInstallModePreference();
|
|
1816
|
+
const current = collectAllCurrentTargets(modePreference);
|
|
1817
|
+
if (!current.complete || current.warnings.length > 0) {
|
|
1818
|
+
p3.log.warn("Limpieza de hu\xE9rfanos deshabilitada: no se pudo construir el plan completo de todos los runtimes.");
|
|
1819
|
+
for (const warning of current.warnings) p3.log.warn(warning);
|
|
1820
|
+
problems++;
|
|
1821
|
+
}
|
|
1626
1822
|
for (const adapter of Object.values(ADAPTERS)) {
|
|
1627
1823
|
const detection = adapter.detect();
|
|
1628
1824
|
if (!detection.installed) {
|
|
1629
1825
|
p3.log.warn(`${adapter.name}: no instalado en esta m\xE1quina.`);
|
|
1630
1826
|
continue;
|
|
1631
1827
|
}
|
|
1632
|
-
const ctx = makeContext(adapter, detection.configDir);
|
|
1828
|
+
const ctx = makeContext(adapter, detection.configDir, modePreference);
|
|
1633
1829
|
if (!ctx) continue;
|
|
1634
1830
|
let pending;
|
|
1635
1831
|
try {
|
|
@@ -1653,10 +1849,10 @@ async function runDoctor() {
|
|
|
1653
1849
|
p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
|
|
1654
1850
|
problems++;
|
|
1655
1851
|
}
|
|
1656
|
-
if (adapter.id === "codex" &&
|
|
1852
|
+
if (adapter.id === "codex" && fs16.existsSync(path21.join(detection.configDir, "hooks.json"))) {
|
|
1657
1853
|
p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
|
|
1658
1854
|
}
|
|
1659
|
-
if (adapter.id === "codex" &&
|
|
1855
|
+
if (adapter.id === "codex" && fs16.existsSync(path21.join(detection.configDir, "AGENTS.override.md"))) {
|
|
1660
1856
|
p3.log.warn(
|
|
1661
1857
|
"Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
|
|
1662
1858
|
);
|
|
@@ -1670,15 +1866,15 @@ async function runDoctor() {
|
|
|
1670
1866
|
}
|
|
1671
1867
|
|
|
1672
1868
|
// src/update.ts
|
|
1673
|
-
import
|
|
1674
|
-
import
|
|
1869
|
+
import fs19 from "fs";
|
|
1870
|
+
import path24 from "path";
|
|
1675
1871
|
import os3 from "os";
|
|
1676
1872
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
1677
1873
|
import * as p4 from "@clack/prompts";
|
|
1678
1874
|
|
|
1679
1875
|
// src/lib/github.ts
|
|
1680
|
-
import
|
|
1681
|
-
import
|
|
1876
|
+
import fs17 from "fs";
|
|
1877
|
+
import path22 from "path";
|
|
1682
1878
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1683
1879
|
import os2 from "os";
|
|
1684
1880
|
import { Readable } from "stream";
|
|
@@ -1748,19 +1944,19 @@ async function latestGithubCommit(repo) {
|
|
|
1748
1944
|
}
|
|
1749
1945
|
}
|
|
1750
1946
|
function validateExtractedTree(destDir) {
|
|
1751
|
-
const resolved =
|
|
1947
|
+
const resolved = path22.resolve(destDir);
|
|
1752
1948
|
const walk = (dir) => {
|
|
1753
1949
|
let entries;
|
|
1754
1950
|
try {
|
|
1755
|
-
entries =
|
|
1951
|
+
entries = fs17.readdirSync(dir, { withFileTypes: true });
|
|
1756
1952
|
} catch {
|
|
1757
1953
|
return false;
|
|
1758
1954
|
}
|
|
1759
1955
|
for (const entry of entries) {
|
|
1760
|
-
const full =
|
|
1956
|
+
const full = path22.join(dir, entry.name);
|
|
1761
1957
|
let stat;
|
|
1762
1958
|
try {
|
|
1763
|
-
stat =
|
|
1959
|
+
stat = fs17.lstatSync(full);
|
|
1764
1960
|
} catch {
|
|
1765
1961
|
return false;
|
|
1766
1962
|
}
|
|
@@ -1776,15 +1972,15 @@ function validateExtractedTree(destDir) {
|
|
|
1776
1972
|
}
|
|
1777
1973
|
function resolveTarBin() {
|
|
1778
1974
|
if (process.platform !== "win32") return "tar";
|
|
1779
|
-
const winTar =
|
|
1780
|
-
return
|
|
1975
|
+
const winTar = path22.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
|
|
1976
|
+
return fs17.existsSync(winTar) ? winTar : "tar";
|
|
1781
1977
|
}
|
|
1782
1978
|
async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
1783
1979
|
const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
|
|
1784
|
-
const tmp =
|
|
1980
|
+
const tmp = path22.join(os2.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
|
|
1785
1981
|
const fail = (reason) => {
|
|
1786
1982
|
try {
|
|
1787
|
-
|
|
1983
|
+
fs17.rmSync(destDir, { recursive: true, force: true });
|
|
1788
1984
|
} catch {
|
|
1789
1985
|
}
|
|
1790
1986
|
return { ok: false, reason };
|
|
@@ -1803,10 +1999,10 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
|
1803
1999
|
if (!res.body) return fail("respuesta HTTP sin cuerpo");
|
|
1804
2000
|
await pipeline(
|
|
1805
2001
|
Readable.fromWeb(res.body),
|
|
1806
|
-
|
|
2002
|
+
fs17.createWriteStream(tmp)
|
|
1807
2003
|
);
|
|
1808
|
-
|
|
1809
|
-
|
|
2004
|
+
fs17.rmSync(destDir, { recursive: true, force: true });
|
|
2005
|
+
fs17.mkdirSync(destDir, { recursive: true });
|
|
1810
2006
|
try {
|
|
1811
2007
|
execFileSync2(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
|
|
1812
2008
|
} catch (err) {
|
|
@@ -1814,35 +2010,35 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
|
1814
2010
|
const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
|
|
1815
2011
|
return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
|
|
1816
2012
|
}
|
|
1817
|
-
const resolvedDest =
|
|
1818
|
-
const validateRoot = validateSubdir ?
|
|
2013
|
+
const resolvedDest = path22.resolve(destDir);
|
|
2014
|
+
const validateRoot = validateSubdir ? path22.resolve(resolvedDest, validateSubdir) : resolvedDest;
|
|
1819
2015
|
if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
|
|
1820
2016
|
return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
|
|
1821
2017
|
}
|
|
1822
|
-
if (
|
|
2018
|
+
if (fs17.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
|
|
1823
2019
|
return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
|
|
1824
2020
|
}
|
|
1825
|
-
const validated =
|
|
2021
|
+
const validated = fs17.existsSync(validateRoot);
|
|
1826
2022
|
return { ok: true, validated };
|
|
1827
2023
|
} catch (err) {
|
|
1828
2024
|
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
1829
2025
|
return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
|
|
1830
2026
|
} finally {
|
|
1831
2027
|
try {
|
|
1832
|
-
|
|
2028
|
+
fs17.rmSync(tmp, { force: true });
|
|
1833
2029
|
} catch {
|
|
1834
2030
|
}
|
|
1835
2031
|
}
|
|
1836
2032
|
}
|
|
1837
2033
|
|
|
1838
2034
|
// src/lib/skill-update.ts
|
|
1839
|
-
import
|
|
1840
|
-
import
|
|
2035
|
+
import fs18 from "fs";
|
|
2036
|
+
import path23 from "path";
|
|
1841
2037
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1842
2038
|
var PROTECTED_SKILLS = /* @__PURE__ */ new Set(["agent-delegation", "work-lifecycle"]);
|
|
1843
2039
|
function sameTextContentNormalized(a, b) {
|
|
1844
|
-
const ba =
|
|
1845
|
-
const bb =
|
|
2040
|
+
const ba = fs18.readFileSync(a);
|
|
2041
|
+
const bb = fs18.readFileSync(b);
|
|
1846
2042
|
if (ba.equals(bb)) return true;
|
|
1847
2043
|
const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
|
|
1848
2044
|
const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
|
|
@@ -1850,10 +2046,10 @@ function sameTextContentNormalized(a, b) {
|
|
|
1850
2046
|
}
|
|
1851
2047
|
function diffSkillDirs(upstreamDir, localDir) {
|
|
1852
2048
|
const upstreamFiles = new Set(
|
|
1853
|
-
listFilesRecursive(upstreamDir).map((f) =>
|
|
2049
|
+
listFilesRecursive(upstreamDir).map((f) => path23.relative(upstreamDir, f))
|
|
1854
2050
|
);
|
|
1855
2051
|
const localFiles = new Set(
|
|
1856
|
-
listFilesRecursive(localDir).map((f) =>
|
|
2052
|
+
listFilesRecursive(localDir).map((f) => path23.relative(localDir, f))
|
|
1857
2053
|
);
|
|
1858
2054
|
const added = [];
|
|
1859
2055
|
const modified = [];
|
|
@@ -1861,7 +2057,7 @@ function diffSkillDirs(upstreamDir, localDir) {
|
|
|
1861
2057
|
for (const rel of upstreamFiles) {
|
|
1862
2058
|
if (!localFiles.has(rel)) {
|
|
1863
2059
|
added.push(rel);
|
|
1864
|
-
} else if (!sameTextContentNormalized(
|
|
2060
|
+
} else if (!sameTextContentNormalized(path23.join(upstreamDir, rel), path23.join(localDir, rel))) {
|
|
1865
2061
|
modified.push(rel);
|
|
1866
2062
|
}
|
|
1867
2063
|
}
|
|
@@ -1923,8 +2119,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
1923
2119
|
if (PROTECTED_SKILLS.has(name)) {
|
|
1924
2120
|
throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
|
|
1925
2121
|
}
|
|
1926
|
-
const upstreamsFile = upstreamsFilePath ??
|
|
1927
|
-
const raw =
|
|
2122
|
+
const upstreamsFile = upstreamsFilePath ?? path23.join(path23.dirname(stackRoot()), "upstreams.json");
|
|
2123
|
+
const raw = fs18.readFileSync(upstreamsFile, "utf8");
|
|
1928
2124
|
const data = JSON.parse(raw);
|
|
1929
2125
|
const skillEntry = data?.skills?.[name];
|
|
1930
2126
|
if (!skillEntry) {
|
|
@@ -1933,8 +2129,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
1933
2129
|
if (skillEntry.kind === "release") {
|
|
1934
2130
|
throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
|
|
1935
2131
|
}
|
|
1936
|
-
const skillsRoot = localSkillsRoot ??
|
|
1937
|
-
const localSkillDir =
|
|
2132
|
+
const skillsRoot = localSkillsRoot ?? path23.join(stackRoot(), "skills");
|
|
2133
|
+
const localSkillDir = path23.join(skillsRoot, name);
|
|
1938
2134
|
const localFiles = listFilesRecursive(localSkillDir);
|
|
1939
2135
|
if (localFiles.length > 0) {
|
|
1940
2136
|
createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
|
|
@@ -1943,24 +2139,24 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
1943
2139
|
try {
|
|
1944
2140
|
const upstreamFiles = listFilesRecursive(upstreamSkillDir);
|
|
1945
2141
|
for (const src of upstreamFiles) {
|
|
1946
|
-
const st =
|
|
2142
|
+
const st = fs18.lstatSync(src);
|
|
1947
2143
|
if (st.isSymbolicLink()) {
|
|
1948
2144
|
throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
|
|
1949
2145
|
}
|
|
1950
|
-
const rel =
|
|
1951
|
-
const dest =
|
|
1952
|
-
ensureDir(
|
|
2146
|
+
const rel = path23.relative(upstreamSkillDir, src);
|
|
2147
|
+
const dest = path23.join(stagingDir, rel);
|
|
2148
|
+
ensureDir(path23.dirname(dest));
|
|
1953
2149
|
copyFile(src, dest);
|
|
1954
2150
|
}
|
|
1955
2151
|
const oldDir = `${localSkillDir}.old-${process.pid}`;
|
|
1956
|
-
if (
|
|
1957
|
-
|
|
2152
|
+
if (fs18.existsSync(localSkillDir)) {
|
|
2153
|
+
fs18.renameSync(localSkillDir, oldDir);
|
|
1958
2154
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
2155
|
+
fs18.renameSync(stagingDir, localSkillDir);
|
|
2156
|
+
fs18.rmSync(oldDir, { recursive: true, force: true });
|
|
1961
2157
|
} catch (err) {
|
|
1962
2158
|
try {
|
|
1963
|
-
|
|
2159
|
+
fs18.rmSync(stagingDir, { recursive: true, force: true });
|
|
1964
2160
|
} catch {
|
|
1965
2161
|
}
|
|
1966
2162
|
throw err;
|
|
@@ -1974,8 +2170,8 @@ function rateLimitHint(prefix) {
|
|
|
1974
2170
|
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.`;
|
|
1975
2171
|
}
|
|
1976
2172
|
function loadUpstreams() {
|
|
1977
|
-
const file =
|
|
1978
|
-
return JSON.parse(
|
|
2173
|
+
const file = path24.join(path24.dirname(stackRoot()), "upstreams.json");
|
|
2174
|
+
return JSON.parse(fs19.readFileSync(file, "utf8"));
|
|
1979
2175
|
}
|
|
1980
2176
|
function skillsToScan(maintainer, upstreams) {
|
|
1981
2177
|
return maintainer ? Object.keys(upstreams.skills) : [];
|
|
@@ -2087,8 +2283,8 @@ function isEngramRunning() {
|
|
|
2087
2283
|
return null;
|
|
2088
2284
|
}
|
|
2089
2285
|
}
|
|
2090
|
-
function isGitClone(projectRoot =
|
|
2091
|
-
return
|
|
2286
|
+
function isGitClone(projectRoot = path24.dirname(stackRoot())) {
|
|
2287
|
+
return fs19.existsSync(path24.join(projectRoot, ".git"));
|
|
2092
2288
|
}
|
|
2093
2289
|
var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
|
|
2094
2290
|
function resolvePnpm() {
|
|
@@ -2098,12 +2294,12 @@ function resolvePnpm() {
|
|
|
2098
2294
|
}
|
|
2099
2295
|
function cleanupTmp(dir) {
|
|
2100
2296
|
try {
|
|
2101
|
-
|
|
2297
|
+
fs19.rmSync(dir, { recursive: true, force: true });
|
|
2102
2298
|
} catch {
|
|
2103
2299
|
}
|
|
2104
2300
|
}
|
|
2105
2301
|
function updateStackGitClone() {
|
|
2106
|
-
const projectRoot =
|
|
2302
|
+
const projectRoot = path24.dirname(stackRoot());
|
|
2107
2303
|
const git = lookPath("git");
|
|
2108
2304
|
if (!git) throw new Error("git no encontrado en PATH.");
|
|
2109
2305
|
const pnpm = resolvePnpm();
|
|
@@ -2120,7 +2316,7 @@ function updateStackGlobal() {
|
|
|
2120
2316
|
execFileSync4(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
|
|
2121
2317
|
}
|
|
2122
2318
|
async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
2123
|
-
const root =
|
|
2319
|
+
const root = fs19.mkdtempSync(path24.join(os3.tmpdir(), "jorgex-skill-"));
|
|
2124
2320
|
try {
|
|
2125
2321
|
const result = await downloadRepoTarball(repo, sha, root, skillPath);
|
|
2126
2322
|
if (!result.ok) {
|
|
@@ -2128,15 +2324,15 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2128
2324
|
return { error: result.reason };
|
|
2129
2325
|
}
|
|
2130
2326
|
if (skillPath) {
|
|
2131
|
-
const sub =
|
|
2327
|
+
const sub = path24.resolve(path24.join(root, skillPath));
|
|
2132
2328
|
if (!isContainedIn(sub, root)) {
|
|
2133
2329
|
cleanupTmp(root);
|
|
2134
2330
|
return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
|
|
2135
2331
|
}
|
|
2136
|
-
if (
|
|
2332
|
+
if (fs19.existsSync(sub)) return { dir: sub, root };
|
|
2137
2333
|
const lastSeg = skillPath.split("/").pop();
|
|
2138
|
-
const sub2 =
|
|
2139
|
-
if (isContainedIn(sub2, root) &&
|
|
2334
|
+
const sub2 = path24.resolve(path24.join(root, lastSeg));
|
|
2335
|
+
if (isContainedIn(sub2, root) && fs19.existsSync(sub2)) {
|
|
2140
2336
|
if (!validateExtractedTree(sub2)) {
|
|
2141
2337
|
cleanupTmp(root);
|
|
2142
2338
|
return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
|
|
@@ -2155,11 +2351,11 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2155
2351
|
function pruneEngramDbBackups() {
|
|
2156
2352
|
try {
|
|
2157
2353
|
const dir = dataDir();
|
|
2158
|
-
if (!
|
|
2159
|
-
const backups =
|
|
2354
|
+
if (!fs19.existsSync(dir)) return;
|
|
2355
|
+
const backups = fs19.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs19.statSync(path24.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
2160
2356
|
for (const old of backups.slice(3)) {
|
|
2161
2357
|
try {
|
|
2162
|
-
|
|
2358
|
+
fs19.rmSync(path24.join(dir, old.name));
|
|
2163
2359
|
} catch {
|
|
2164
2360
|
}
|
|
2165
2361
|
}
|
|
@@ -2167,18 +2363,18 @@ function pruneEngramDbBackups() {
|
|
|
2167
2363
|
}
|
|
2168
2364
|
}
|
|
2169
2365
|
function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
2170
|
-
if (!
|
|
2171
|
-
const dir =
|
|
2172
|
-
const base =
|
|
2366
|
+
if (!fs19.existsSync(binPath)) return null;
|
|
2367
|
+
const dir = path24.dirname(binPath);
|
|
2368
|
+
const base = path24.basename(binPath);
|
|
2173
2369
|
const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
|
|
2174
2370
|
const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
|
|
2175
|
-
const resolvedDir =
|
|
2176
|
-
if (resolvedDir ===
|
|
2371
|
+
const resolvedDir = path24.resolve(dir);
|
|
2372
|
+
if (resolvedDir === path24.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
|
|
2177
2373
|
try {
|
|
2178
|
-
for (const entry of
|
|
2374
|
+
for (const entry of fs19.readdirSync(dir)) {
|
|
2179
2375
|
if (oldPattern.test(entry)) {
|
|
2180
2376
|
try {
|
|
2181
|
-
|
|
2377
|
+
fs19.rmSync(path24.join(dir, entry), { force: true });
|
|
2182
2378
|
} catch {
|
|
2183
2379
|
}
|
|
2184
2380
|
}
|
|
@@ -2186,8 +2382,8 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
|
2186
2382
|
} catch {
|
|
2187
2383
|
}
|
|
2188
2384
|
}
|
|
2189
|
-
const rotated =
|
|
2190
|
-
|
|
2385
|
+
const rotated = path24.join(dir, `${base}.old-${Date.now()}`);
|
|
2386
|
+
fs19.renameSync(binPath, rotated);
|
|
2191
2387
|
return rotated;
|
|
2192
2388
|
}
|
|
2193
2389
|
async function updateEngram(engramRepo, latestVersion) {
|
|
@@ -2196,19 +2392,19 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2196
2392
|
"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)."
|
|
2197
2393
|
);
|
|
2198
2394
|
}
|
|
2199
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
2200
|
-
const engramDb =
|
|
2201
|
-
if (
|
|
2395
|
+
const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path24.join(HOME, ".engram");
|
|
2396
|
+
const engramDb = path24.join(engramDataDir, "engram.db");
|
|
2397
|
+
if (fs19.existsSync(engramDb)) {
|
|
2202
2398
|
const doBackup = await p4.confirm({
|
|
2203
2399
|
message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
|
|
2204
2400
|
initialValue: true
|
|
2205
2401
|
});
|
|
2206
2402
|
if (!p4.isCancel(doBackup) && doBackup) {
|
|
2207
2403
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2208
|
-
const dest =
|
|
2404
|
+
const dest = path24.join(dataDir(), `engram-db-backup-${ts}.db`);
|
|
2209
2405
|
try {
|
|
2210
|
-
if (!
|
|
2211
|
-
|
|
2406
|
+
if (!fs19.existsSync(dataDir())) fs19.mkdirSync(dataDir(), { recursive: true });
|
|
2407
|
+
fs19.copyFileSync(engramDb, dest);
|
|
2212
2408
|
p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
|
|
2213
2409
|
pruneEngramDbBackups();
|
|
2214
2410
|
} catch (err) {
|
|
@@ -2258,10 +2454,10 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2258
2454
|
["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
|
|
2259
2455
|
{ stdio: "inherit" }
|
|
2260
2456
|
);
|
|
2261
|
-
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists:
|
|
2457
|
+
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs19.existsSync(bin ?? "") });
|
|
2262
2458
|
if (rollbackOk.action === "restore") {
|
|
2263
2459
|
try {
|
|
2264
|
-
|
|
2460
|
+
fs19.renameSync(rotated, bin);
|
|
2265
2461
|
p4.log.warn(rollbackOk.messages.onRestore);
|
|
2266
2462
|
} catch {
|
|
2267
2463
|
p4.log.warn(rollbackOk.messages.onRenameFail);
|
|
@@ -2269,10 +2465,10 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2269
2465
|
}
|
|
2270
2466
|
return true;
|
|
2271
2467
|
} catch (err) {
|
|
2272
|
-
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists:
|
|
2468
|
+
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs19.existsSync(bin ?? "") });
|
|
2273
2469
|
if (rollbackFail.action === "restore") {
|
|
2274
2470
|
try {
|
|
2275
|
-
|
|
2471
|
+
fs19.renameSync(rotated, bin);
|
|
2276
2472
|
p4.log.info(rollbackFail.messages.onRestore);
|
|
2277
2473
|
} catch {
|
|
2278
2474
|
p4.log.error(rollbackFail.messages.onRenameFail);
|
|
@@ -2530,7 +2726,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2530
2726
|
continue;
|
|
2531
2727
|
}
|
|
2532
2728
|
const { dir: tmpDir, root: tmpRoot } = tmpResult;
|
|
2533
|
-
const localSkillDir =
|
|
2729
|
+
const localSkillDir = path24.join(stackRoot(), "skills", skillInfo.name);
|
|
2534
2730
|
const diff = renderSkillDiff(tmpDir, localSkillDir);
|
|
2535
2731
|
if (diff) {
|
|
2536
2732
|
p4.log.info(`Diff de ${skillInfo.name}:
|
|
@@ -2589,7 +2785,7 @@ ${diff}`);
|
|
|
2589
2785
|
}
|
|
2590
2786
|
|
|
2591
2787
|
// src/models-picker.ts
|
|
2592
|
-
import
|
|
2788
|
+
import path25 from "path";
|
|
2593
2789
|
import * as p5 from "@clack/prompts";
|
|
2594
2790
|
var TIERS = ["strong", "standard", "cheap"];
|
|
2595
2791
|
var EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
@@ -2605,7 +2801,7 @@ function opencodeLiveModels(binPath) {
|
|
|
2605
2801
|
}
|
|
2606
2802
|
function agentsByTier() {
|
|
2607
2803
|
const grouped = { strong: [], standard: [], cheap: [] };
|
|
2608
|
-
for (const agent of loadCanonicalAgents(
|
|
2804
|
+
for (const agent of loadCanonicalAgents(path25.join(stackRoot(), "agents"))) {
|
|
2609
2805
|
if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
|
|
2610
2806
|
}
|
|
2611
2807
|
return grouped;
|
|
@@ -2764,16 +2960,16 @@ function cancelled() {
|
|
|
2764
2960
|
}
|
|
2765
2961
|
|
|
2766
2962
|
// src/lib/release.ts
|
|
2767
|
-
import
|
|
2768
|
-
import
|
|
2963
|
+
import fs20 from "fs";
|
|
2964
|
+
import path26 from "path";
|
|
2769
2965
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
2770
2966
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2771
2967
|
function findPackageJson() {
|
|
2772
|
-
let dir =
|
|
2968
|
+
let dir = path26.dirname(fileURLToPath2(import.meta.url));
|
|
2773
2969
|
for (let i = 0; i < 6; i++) {
|
|
2774
|
-
const candidate =
|
|
2775
|
-
if (
|
|
2776
|
-
dir =
|
|
2970
|
+
const candidate = path26.join(dir, "package.json");
|
|
2971
|
+
if (fs20.existsSync(candidate)) return candidate;
|
|
2972
|
+
dir = path26.dirname(dir);
|
|
2777
2973
|
}
|
|
2778
2974
|
throw new Error("No se encontr\xF3 package.json cerca del CLI.");
|
|
2779
2975
|
}
|
|
@@ -2782,7 +2978,7 @@ function readPackageVersion() {
|
|
|
2782
2978
|
}
|
|
2783
2979
|
function readPackageMetadata() {
|
|
2784
2980
|
const packageJson = findPackageJson();
|
|
2785
|
-
const raw =
|
|
2981
|
+
const raw = fs20.readFileSync(packageJson, "utf8");
|
|
2786
2982
|
const parsed = JSON.parse(raw);
|
|
2787
2983
|
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
2788
2984
|
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
@@ -2800,6 +2996,8 @@ function parseFlags(args) {
|
|
|
2800
2996
|
agents: [],
|
|
2801
2997
|
dryRun: false,
|
|
2802
2998
|
yes: false,
|
|
2999
|
+
mode: void 0,
|
|
3000
|
+
subagentConcurrency: void 0,
|
|
2803
3001
|
help: false,
|
|
2804
3002
|
version: false,
|
|
2805
3003
|
list: false,
|
|
@@ -2807,12 +3005,33 @@ function parseFlags(args) {
|
|
|
2807
3005
|
removeEngram: false,
|
|
2808
3006
|
positional: []
|
|
2809
3007
|
};
|
|
3008
|
+
const readValue = (index) => {
|
|
3009
|
+
const value = args[index + 1];
|
|
3010
|
+
if (value === void 0 || value.startsWith("-")) return [void 0, index];
|
|
3011
|
+
return [value, index + 1];
|
|
3012
|
+
};
|
|
2810
3013
|
for (let i = 0; i < args.length; i++) {
|
|
2811
3014
|
const arg = args[i];
|
|
2812
|
-
if (arg === "--agents" || arg === "-a")
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3015
|
+
if (arg === "--agents" || arg === "-a") {
|
|
3016
|
+
const [value, nextIndex] = readValue(i);
|
|
3017
|
+
flags.agents = (value ?? "").split(",").filter(Boolean);
|
|
3018
|
+
i = nextIndex;
|
|
3019
|
+
} else if (arg.startsWith("--agents=")) flags.agents = arg.slice(9).split(",").filter(Boolean);
|
|
3020
|
+
else if (arg === "--target-dir") {
|
|
3021
|
+
const [value, nextIndex] = readValue(i);
|
|
3022
|
+
flags.targetDir = value;
|
|
3023
|
+
i = nextIndex;
|
|
3024
|
+
} else if (arg.startsWith("--target-dir=")) flags.targetDir = arg.slice(13);
|
|
3025
|
+
else if (arg === "--mode") {
|
|
3026
|
+
const [value, nextIndex] = readValue(i);
|
|
3027
|
+
flags.mode = value ?? "";
|
|
3028
|
+
i = nextIndex;
|
|
3029
|
+
} else if (arg.startsWith("--mode=")) flags.mode = arg.slice(7);
|
|
3030
|
+
else if (arg === "--subagent-concurrency") {
|
|
3031
|
+
const [value, nextIndex] = readValue(i);
|
|
3032
|
+
flags.subagentConcurrency = value ?? "";
|
|
3033
|
+
i = nextIndex;
|
|
3034
|
+
} else if (arg.startsWith("--subagent-concurrency=")) flags.subagentConcurrency = arg.slice(23);
|
|
2816
3035
|
else if (arg === "--dry-run") flags.dryRun = true;
|
|
2817
3036
|
else if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
2818
3037
|
else if (arg === "--help" || arg === "-h") flags.help = true;
|
|
@@ -2824,6 +3043,67 @@ function parseFlags(args) {
|
|
|
2824
3043
|
}
|
|
2825
3044
|
return flags;
|
|
2826
3045
|
}
|
|
3046
|
+
async function resolveInstallMode(flags, promptIfMissing = true) {
|
|
3047
|
+
const explicit = parseInstallModePreferenceFlags(flags.mode, flags.subagentConcurrency);
|
|
3048
|
+
if (explicit.error) {
|
|
3049
|
+
console.error(explicit.error);
|
|
3050
|
+
process.exitCode = 1;
|
|
3051
|
+
return null;
|
|
3052
|
+
}
|
|
3053
|
+
if (explicit.preference) return explicit.preference;
|
|
3054
|
+
if (flags.targetDir !== void 0) {
|
|
3055
|
+
p6.log.info("--target-dir ignora la preferencia guardada y usa modo human por defecto; usa --mode programmatic si quieres otro modo.");
|
|
3056
|
+
return DEFAULT_INSTALL_MODE_PREFERENCE;
|
|
3057
|
+
}
|
|
3058
|
+
const preferenceFile = installModePreferenceFile();
|
|
3059
|
+
if (hasInstallModePreference(preferenceFile)) {
|
|
3060
|
+
try {
|
|
3061
|
+
return loadInstallModePreference(preferenceFile);
|
|
3062
|
+
} catch (error) {
|
|
3063
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3064
|
+
console.error(
|
|
3065
|
+
`${reason}
|
|
3066
|
+
Corrige o borra ${preferenceFile}, o vuelve a ejecutar con --mode human|programmatic.`
|
|
3067
|
+
);
|
|
3068
|
+
process.exitCode = 1;
|
|
3069
|
+
return null;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
if (!promptIfMissing) {
|
|
3073
|
+
console.error("No hay modo guardado; usa --mode expl\xEDcito para este sync.");
|
|
3074
|
+
process.exitCode = 1;
|
|
3075
|
+
return null;
|
|
3076
|
+
}
|
|
3077
|
+
if (flags.yes || !process.stdout.isTTY) return DEFAULT_INSTALL_MODE_PREFERENCE;
|
|
3078
|
+
const selected = await p6.select({
|
|
3079
|
+
message: "\xBFC\xF3mo quieres instalar el modo del stack?",
|
|
3080
|
+
options: [
|
|
3081
|
+
{ value: "human", label: "Human (comportamiento actual)" },
|
|
3082
|
+
{ value: "programmatic", label: "Programmatic (elige concurrencia)" }
|
|
3083
|
+
],
|
|
3084
|
+
initialValue: DEFAULT_INSTALL_MODE_PREFERENCE.mode
|
|
3085
|
+
});
|
|
3086
|
+
if (p6.isCancel(selected)) return null;
|
|
3087
|
+
if (selected === "human") {
|
|
3088
|
+
return {
|
|
3089
|
+
mode: "human",
|
|
3090
|
+
subagentConcurrency: "serial"
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
const concurrency = await p6.select({
|
|
3094
|
+
message: "Concurrencia de subagentes en modo programmatic",
|
|
3095
|
+
options: [
|
|
3096
|
+
{ value: "serial", label: "Serial (default)" },
|
|
3097
|
+
{ value: "parallel", label: "Parallel" }
|
|
3098
|
+
],
|
|
3099
|
+
initialValue: DEFAULT_INSTALL_MODE_PREFERENCE.subagentConcurrency
|
|
3100
|
+
});
|
|
3101
|
+
if (p6.isCancel(concurrency)) return null;
|
|
3102
|
+
return {
|
|
3103
|
+
mode: "programmatic",
|
|
3104
|
+
subagentConcurrency: concurrency
|
|
3105
|
+
};
|
|
3106
|
+
}
|
|
2827
3107
|
function parseCliArgs(argv) {
|
|
2828
3108
|
const [first, ...rest] = argv;
|
|
2829
3109
|
const isCommand = COMMANDS.includes(first ?? "install");
|
|
@@ -2872,6 +3152,8 @@ Comandos:
|
|
|
2872
3152
|
|
|
2873
3153
|
Opciones:
|
|
2874
3154
|
--agents, -a opencode,claude-code,codex Runtimes destino (default: detectados)
|
|
3155
|
+
--mode human|programmatic Modo de instalaci\xF3n (default: preferencia guardada o human)
|
|
3156
|
+
--subagent-concurrency serial|parallel Concurrencia de subagentes en modo programmatic
|
|
2875
3157
|
--target-dir <dir> Dir alternativo (pruebas de paridad; requiere 1 runtime)
|
|
2876
3158
|
--dry-run Muestra el plan sin escribir nada
|
|
2877
3159
|
--yes, -y No interactivo
|
|
@@ -2899,6 +3181,8 @@ async function main() {
|
|
|
2899
3181
|
switch (command) {
|
|
2900
3182
|
case "install":
|
|
2901
3183
|
case "sync": {
|
|
3184
|
+
const mode = await resolveInstallMode(flags);
|
|
3185
|
+
if (mode === null) return;
|
|
2902
3186
|
const runtimes = await resolveRuntimes(flags);
|
|
2903
3187
|
if (runtimes === null) return;
|
|
2904
3188
|
if (runtimes.length === 0) {
|
|
@@ -2906,7 +3190,7 @@ async function main() {
|
|
|
2906
3190
|
process.exitCode = 1;
|
|
2907
3191
|
return;
|
|
2908
3192
|
}
|
|
2909
|
-
process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: flags.yes });
|
|
3193
|
+
process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: flags.yes, mode });
|
|
2910
3194
|
return;
|
|
2911
3195
|
}
|
|
2912
3196
|
case "uninstall": {
|
|
@@ -2941,23 +3225,46 @@ async function main() {
|
|
|
2941
3225
|
}
|
|
2942
3226
|
const runtimes = await resolveRuntimes(flags);
|
|
2943
3227
|
if (runtimes === null) return;
|
|
2944
|
-
|
|
2945
|
-
|
|
3228
|
+
const preferenceFile = installModePreferenceFile();
|
|
3229
|
+
const explicitMode = flags.mode !== void 0 || flags.subagentConcurrency !== void 0;
|
|
3230
|
+
const hasSavedMode = hasInstallModePreference(preferenceFile);
|
|
3231
|
+
const canResolveMode = flags.targetDir !== void 0 || explicitMode || hasSavedMode;
|
|
3232
|
+
const mode = runtimes.length > 0 && canResolveMode ? await resolveInstallMode(flags, false) : DEFAULT_INSTALL_MODE_PREFERENCE;
|
|
3233
|
+
if (mode === null) return;
|
|
3234
|
+
const canSync = runtimes.length === 0 || canResolveMode;
|
|
3235
|
+
if (runtimes.length > 0 && canSync) {
|
|
3236
|
+
const code = await runInstall({
|
|
3237
|
+
runtimes,
|
|
3238
|
+
targetDir: flags.targetDir,
|
|
3239
|
+
dryRun: flags.dryRun,
|
|
3240
|
+
yes: true,
|
|
3241
|
+
mode
|
|
3242
|
+
});
|
|
2946
3243
|
if (code !== 0) {
|
|
2947
3244
|
process.exitCode = code;
|
|
2948
3245
|
return;
|
|
2949
3246
|
}
|
|
3247
|
+
} else if (runtimes.length > 0) {
|
|
3248
|
+
console.error("No hay modo guardado; se omite el sync previo y se contin\xFAa con update. Usa --mode expl\xEDcito si quieres sincronizar.");
|
|
2950
3249
|
}
|
|
2951
3250
|
const result = await runInteractiveUpdate(VERSION, flags.yes, flags.dryRun);
|
|
2952
3251
|
process.exitCode = result.exitCode;
|
|
2953
|
-
if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && !
|
|
3252
|
+
if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && !canSync) {
|
|
3253
|
+
p6.log.warn("Skills/stack actualizados, pero el sync con los runtimes sigue pendiente. Ejecuta jorgex-stack sync --mode human|programmatic.");
|
|
3254
|
+
} else if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
|
|
2954
3255
|
const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
|
|
2955
3256
|
if (!p6.isCancel(apply) && apply) {
|
|
2956
|
-
process.exitCode = await runInstall({
|
|
3257
|
+
process.exitCode = await runInstall({
|
|
3258
|
+
runtimes,
|
|
3259
|
+
targetDir: flags.targetDir,
|
|
3260
|
+
dryRun: false,
|
|
3261
|
+
yes: false,
|
|
3262
|
+
mode
|
|
3263
|
+
});
|
|
2957
3264
|
} else {
|
|
2958
3265
|
console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
|
|
2959
3266
|
}
|
|
2960
|
-
} else if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && (flags.yes || !process.stdout.isTTY)) {
|
|
3267
|
+
} else if (result.exitCode === 0 && result.appliedUpdates && runtimes.length > 0 && canSync && (flags.yes || !process.stdout.isTTY)) {
|
|
2961
3268
|
console.log("Skills/stack actualizados. Ejecuta jorgex-stack sync para aplicarlos a los runtimes.");
|
|
2962
3269
|
}
|
|
2963
3270
|
return;
|
|
@@ -2975,7 +3282,17 @@ async function main() {
|
|
|
2975
3282
|
if (code === 0 && !flags.yes && process.stdout.isTTY) {
|
|
2976
3283
|
const apply = await p6.confirm({ message: "\xBFAplicar ahora los modelos a los agentes instalados? (sync)" });
|
|
2977
3284
|
if (!p6.isCancel(apply) && apply) {
|
|
2978
|
-
|
|
3285
|
+
const preferenceFile = installModePreferenceFile();
|
|
3286
|
+
const explicitMode = flags.mode !== void 0 || flags.subagentConcurrency !== void 0;
|
|
3287
|
+
const hasSavedMode = hasInstallModePreference(preferenceFile);
|
|
3288
|
+
const canResolveMode = flags.targetDir !== void 0 || explicitMode || hasSavedMode;
|
|
3289
|
+
if (!canResolveMode) {
|
|
3290
|
+
p6.log.warn("Model-map guardado: se omite el sync con los runtimes porque falta un modo. Ejecuta jorgex-stack sync --mode human|programmatic.");
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
const mode = await resolveInstallMode(flags, false);
|
|
3294
|
+
if (mode === null) return;
|
|
3295
|
+
process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: false, mode });
|
|
2979
3296
|
} else {
|
|
2980
3297
|
console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
|
|
2981
3298
|
}
|