jorgex-stack 1.0.15 → 1.0.17
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 +511 -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 +52 -13
- package/stack/plugins/opencode/goal/opencode-hooks.ts +23 -8
- package/stack/plugins/opencode/goal-plugin.ts +17 -9
- package/stack/plugins/opencode/worktree.ts +2 -1
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,23 +1466,23 @@ 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
|
}
|
|
@@ -1335,12 +1498,14 @@ var ADAPTERS = {
|
|
|
1335
1498
|
"claude-code": claudeCodeAdapter,
|
|
1336
1499
|
codex: codexAdapter
|
|
1337
1500
|
};
|
|
1338
|
-
function makeContext(adapter, configDir) {
|
|
1501
|
+
function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
|
|
1339
1502
|
const models = loadModelMap()[adapter.id];
|
|
1340
1503
|
if (!models) return null;
|
|
1341
1504
|
return {
|
|
1342
1505
|
stackDir: stackRoot(),
|
|
1343
1506
|
configDir,
|
|
1507
|
+
mode: mode.mode,
|
|
1508
|
+
subagentConcurrency: mode.subagentConcurrency,
|
|
1344
1509
|
engramBin: detectEngram(),
|
|
1345
1510
|
models,
|
|
1346
1511
|
warnings: []
|
|
@@ -1364,7 +1529,7 @@ function diffPlan(plan) {
|
|
|
1364
1529
|
if (current === null) return { action, status: "create" };
|
|
1365
1530
|
return { action, status: current === action.content ? "unchanged" : "update" };
|
|
1366
1531
|
}
|
|
1367
|
-
if (!
|
|
1532
|
+
if (!fs14.existsSync(action.target)) return { action, status: "create" };
|
|
1368
1533
|
return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
|
|
1369
1534
|
});
|
|
1370
1535
|
}
|
|
@@ -1374,35 +1539,49 @@ function applyChanges(changes) {
|
|
|
1374
1539
|
else copyFile(action.source, action.target);
|
|
1375
1540
|
}
|
|
1376
1541
|
}
|
|
1377
|
-
function collectAllCurrentTargets() {
|
|
1542
|
+
function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
|
|
1378
1543
|
const targets = /* @__PURE__ */ new Set();
|
|
1379
1544
|
let complete = true;
|
|
1545
|
+
const warnings = [];
|
|
1380
1546
|
for (const adapter of Object.values(ADAPTERS)) {
|
|
1381
1547
|
const detection = adapter.detect();
|
|
1382
1548
|
if (!detection.installed) continue;
|
|
1383
|
-
const ctx = makeContext(adapter, detection.configDir);
|
|
1384
|
-
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
|
+
}
|
|
1385
1555
|
try {
|
|
1386
|
-
for (const action of buildPlan(adapter, ctx)) targets.add(
|
|
1387
|
-
} catch {
|
|
1556
|
+
for (const action of buildPlan(adapter, ctx)) targets.add(path19.resolve(action.target));
|
|
1557
|
+
} catch (error) {
|
|
1388
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
|
+
);
|
|
1389
1562
|
}
|
|
1390
1563
|
}
|
|
1391
|
-
return { targets, complete };
|
|
1564
|
+
return { targets, complete, warnings };
|
|
1392
1565
|
}
|
|
1393
1566
|
async function runInstall(opts) {
|
|
1394
1567
|
p.intro(`jorgex-stack ${opts.dryRun ? "install (dry-run)" : "install"}`);
|
|
1395
1568
|
const stackDir = stackRoot();
|
|
1396
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;
|
|
1397
1572
|
const modelMap = loadModelMap();
|
|
1398
|
-
ensureModelMapFile();
|
|
1573
|
+
if (useManifest) ensureModelMapFile();
|
|
1399
1574
|
p.log.info(engramBin ? `Engram detectado: ${engramBin} (se respeta, D7)` : "Engram NO detectado.");
|
|
1400
|
-
const
|
|
1401
|
-
const current = useManifest ? collectAllCurrentTargets() : { targets: /* @__PURE__ */ new Set(), complete: false };
|
|
1575
|
+
const current = useManifest ? collectAllCurrentTargets(modePreference) : { targets: /* @__PURE__ */ new Set(), complete: false, warnings: [] };
|
|
1402
1576
|
const canOrphan = useManifest && current.complete;
|
|
1403
1577
|
const canonicalMcp = loadCanonicalMcp(stackDir);
|
|
1404
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
|
+
}
|
|
1405
1583
|
let exitCode = 0;
|
|
1584
|
+
let successfulRuns = 0;
|
|
1406
1585
|
for (const id of opts.runtimes) {
|
|
1407
1586
|
const adapter = ADAPTERS[id];
|
|
1408
1587
|
if (!adapter) {
|
|
@@ -1423,6 +1602,8 @@ async function runInstall(opts) {
|
|
|
1423
1602
|
const ctx = {
|
|
1424
1603
|
stackDir,
|
|
1425
1604
|
configDir,
|
|
1605
|
+
mode: modePreference.mode,
|
|
1606
|
+
subagentConcurrency: modePreference.subagentConcurrency,
|
|
1426
1607
|
engramBin,
|
|
1427
1608
|
models,
|
|
1428
1609
|
warnings: []
|
|
@@ -1432,8 +1613,8 @@ async function runInstall(opts) {
|
|
|
1432
1613
|
let creates = diff.filter((d) => d.status === "create");
|
|
1433
1614
|
let updates = diff.filter((d) => d.status === "update");
|
|
1434
1615
|
let changes = [...creates, ...updates];
|
|
1435
|
-
const prevManifest =
|
|
1436
|
-
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) : [];
|
|
1437
1618
|
p.log.step(`${adapter.name} \u2192 ${configDir}`);
|
|
1438
1619
|
p.log.info(
|
|
1439
1620
|
`${diff.length} archivos gestionados: ${creates.length} nuevos, ${updates.length} modificados, ${diff.length - changes.length} sin cambios`
|
|
@@ -1448,13 +1629,17 @@ async function runInstall(opts) {
|
|
|
1448
1629
|
}
|
|
1449
1630
|
const writeManifest = () => {
|
|
1450
1631
|
if (!useManifest) return;
|
|
1451
|
-
const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) =>
|
|
1452
|
-
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])];
|
|
1453
1637
|
writeRuntimeManifest(id, { configDir, owned, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1454
1638
|
};
|
|
1455
1639
|
if (changes.length === 0 && orphans.length === 0) {
|
|
1456
1640
|
writeManifest();
|
|
1457
1641
|
p.log.success(`${adapter.name}: ya al d\xEDa (idempotente).`);
|
|
1642
|
+
successfulRuns++;
|
|
1458
1643
|
continue;
|
|
1459
1644
|
}
|
|
1460
1645
|
if (!opts.yes && process.stdout.isTTY) {
|
|
@@ -1470,12 +1655,12 @@ async function runInstall(opts) {
|
|
|
1470
1655
|
updates = diff.filter((d) => d.status === "update");
|
|
1471
1656
|
changes = [...creates, ...updates];
|
|
1472
1657
|
}
|
|
1473
|
-
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;
|
|
1474
1659
|
if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
1475
1660
|
applyChanges(changes);
|
|
1476
|
-
const pruneRoot = useManifest ? HOME :
|
|
1661
|
+
const pruneRoot = useManifest ? HOME : path19.dirname(configDir);
|
|
1477
1662
|
for (const orphan of orphans) {
|
|
1478
|
-
|
|
1663
|
+
fs14.rmSync(orphan, { force: true });
|
|
1479
1664
|
pruneEmptyDirs(orphan, pruneRoot);
|
|
1480
1665
|
}
|
|
1481
1666
|
const verifyCtx = { ...ctx, warnings: [] };
|
|
@@ -1487,15 +1672,19 @@ async function runInstall(opts) {
|
|
|
1487
1672
|
} else {
|
|
1488
1673
|
writeManifest();
|
|
1489
1674
|
p.log.success(`${adapter.name}: ${changes.length} archivos aplicados y verificados (idempotente).`);
|
|
1675
|
+
successfulRuns++;
|
|
1490
1676
|
}
|
|
1491
1677
|
}
|
|
1678
|
+
if (useManifest && !opts.dryRun && exitCode === 0 && successfulRuns > 0) {
|
|
1679
|
+
saveInstallModePreference(installModePreferenceFile(), modePreference);
|
|
1680
|
+
}
|
|
1492
1681
|
p.outro(opts.dryRun ? "Dry-run: no se ha escrito nada." : "Hecho.");
|
|
1493
1682
|
return exitCode;
|
|
1494
1683
|
}
|
|
1495
1684
|
|
|
1496
1685
|
// src/uninstall.ts
|
|
1497
|
-
import
|
|
1498
|
-
import
|
|
1686
|
+
import fs15 from "fs";
|
|
1687
|
+
import path20 from "path";
|
|
1499
1688
|
import * as p2 from "@clack/prompts";
|
|
1500
1689
|
async function runUninstall(opts) {
|
|
1501
1690
|
p2.intro(`jorgex-stack ${opts.dryRun ? "uninstall (dry-run)" : "uninstall"}`);
|
|
@@ -1525,7 +1714,7 @@ async function runUninstall(opts) {
|
|
|
1525
1714
|
if (!detection.installed) continue;
|
|
1526
1715
|
const keepCtx = makeContext(keep, detection.configDir);
|
|
1527
1716
|
if (!keepCtx) continue;
|
|
1528
|
-
for (const action of buildPlan(keep, keepCtx)) retained.add(
|
|
1717
|
+
for (const action of buildPlan(keep, keepCtx)) retained.add(path20.resolve(action.target));
|
|
1529
1718
|
}
|
|
1530
1719
|
for (const id of opts.runtimes) {
|
|
1531
1720
|
const adapter = ADAPTERS[id];
|
|
@@ -1543,15 +1732,15 @@ async function runUninstall(opts) {
|
|
|
1543
1732
|
if (!ctx) continue;
|
|
1544
1733
|
ctx.preserveEngram = !removeEngram;
|
|
1545
1734
|
const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
|
|
1546
|
-
const mergedTargets = new Set(unmerge.map((a) =>
|
|
1735
|
+
const mergedTargets = new Set(unmerge.map((a) => path20.resolve(a.target)));
|
|
1547
1736
|
const usingRealConfig = opts.targetDir === void 0;
|
|
1548
1737
|
const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
|
|
1549
|
-
const pruneRoot = usingRealConfig ? HOME :
|
|
1738
|
+
const pruneRoot = usingRealConfig ? HOME : path20.dirname(configDir);
|
|
1550
1739
|
const planTargets = [
|
|
1551
|
-
.../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) =>
|
|
1552
|
-
].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));
|
|
1553
1742
|
const deleteTargets = planTargets.filter(
|
|
1554
|
-
(t) => !retained.has(t) && !(ctx.preserveEngram &&
|
|
1743
|
+
(t) => !retained.has(t) && !(ctx.preserveEngram && path20.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
|
|
1555
1744
|
);
|
|
1556
1745
|
const sharedKept = planTargets.length - deleteTargets.length;
|
|
1557
1746
|
p2.log.step(`${adapter.name} \u2192 ${configDir}`);
|
|
@@ -1559,18 +1748,18 @@ async function runUninstall(opts) {
|
|
|
1559
1748
|
if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
|
|
1560
1749
|
if (opts.dryRun) continue;
|
|
1561
1750
|
const backup = createBackup(
|
|
1562
|
-
[...deleteTargets, ...unmerge.map((a) => a.target).filter((t) =>
|
|
1751
|
+
[...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs15.existsSync(t))],
|
|
1563
1752
|
`uninstall-${id}`
|
|
1564
1753
|
);
|
|
1565
1754
|
if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
1566
1755
|
for (const target of deleteTargets) {
|
|
1567
|
-
|
|
1756
|
+
fs15.rmSync(target, { force: true });
|
|
1568
1757
|
pruneEmptyDirs(target, pruneRoot);
|
|
1569
1758
|
}
|
|
1570
1759
|
for (const action of unmerge) {
|
|
1571
1760
|
if (action.kind !== "write") continue;
|
|
1572
1761
|
if (action.content.trim() === "") {
|
|
1573
|
-
|
|
1762
|
+
fs15.rmSync(action.target, { force: true });
|
|
1574
1763
|
} else {
|
|
1575
1764
|
writeText(action.target, action.content);
|
|
1576
1765
|
}
|
|
@@ -1583,8 +1772,8 @@ async function runUninstall(opts) {
|
|
|
1583
1772
|
}
|
|
1584
1773
|
|
|
1585
1774
|
// src/doctor.ts
|
|
1586
|
-
import
|
|
1587
|
-
import
|
|
1775
|
+
import path21 from "path";
|
|
1776
|
+
import fs16 from "fs";
|
|
1588
1777
|
import * as p3 from "@clack/prompts";
|
|
1589
1778
|
function engramVersion(bin) {
|
|
1590
1779
|
const out = runDetectedBin(bin, ["--version"], 5e3);
|
|
@@ -1592,7 +1781,7 @@ function engramVersion(bin) {
|
|
|
1592
1781
|
return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
|
|
1593
1782
|
}
|
|
1594
1783
|
function context7KeyConfigured(id, configDir) {
|
|
1595
|
-
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");
|
|
1596
1785
|
const content = readTextIfExists(file);
|
|
1597
1786
|
if (content === null) return null;
|
|
1598
1787
|
const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
|
|
@@ -1615,22 +1804,28 @@ async function runDoctor() {
|
|
|
1615
1804
|
p3.log.success(`Engram: ${version} (${engramBin})`);
|
|
1616
1805
|
}
|
|
1617
1806
|
}
|
|
1618
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
1619
|
-
const engramDb =
|
|
1620
|
-
if (
|
|
1621
|
-
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);
|
|
1622
1811
|
p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
|
|
1623
1812
|
}
|
|
1624
|
-
if (!
|
|
1813
|
+
if (!fs16.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
|
|
1625
1814
|
const manifest = readManifest();
|
|
1626
|
-
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
|
+
}
|
|
1627
1822
|
for (const adapter of Object.values(ADAPTERS)) {
|
|
1628
1823
|
const detection = adapter.detect();
|
|
1629
1824
|
if (!detection.installed) {
|
|
1630
1825
|
p3.log.warn(`${adapter.name}: no instalado en esta m\xE1quina.`);
|
|
1631
1826
|
continue;
|
|
1632
1827
|
}
|
|
1633
|
-
const ctx = makeContext(adapter, detection.configDir);
|
|
1828
|
+
const ctx = makeContext(adapter, detection.configDir, modePreference);
|
|
1634
1829
|
if (!ctx) continue;
|
|
1635
1830
|
let pending;
|
|
1636
1831
|
try {
|
|
@@ -1654,10 +1849,10 @@ async function runDoctor() {
|
|
|
1654
1849
|
p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
|
|
1655
1850
|
problems++;
|
|
1656
1851
|
}
|
|
1657
|
-
if (adapter.id === "codex" &&
|
|
1852
|
+
if (adapter.id === "codex" && fs16.existsSync(path21.join(detection.configDir, "hooks.json"))) {
|
|
1658
1853
|
p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
|
|
1659
1854
|
}
|
|
1660
|
-
if (adapter.id === "codex" &&
|
|
1855
|
+
if (adapter.id === "codex" && fs16.existsSync(path21.join(detection.configDir, "AGENTS.override.md"))) {
|
|
1661
1856
|
p3.log.warn(
|
|
1662
1857
|
"Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
|
|
1663
1858
|
);
|
|
@@ -1671,15 +1866,15 @@ async function runDoctor() {
|
|
|
1671
1866
|
}
|
|
1672
1867
|
|
|
1673
1868
|
// src/update.ts
|
|
1674
|
-
import
|
|
1675
|
-
import
|
|
1869
|
+
import fs19 from "fs";
|
|
1870
|
+
import path24 from "path";
|
|
1676
1871
|
import os3 from "os";
|
|
1677
1872
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
1678
1873
|
import * as p4 from "@clack/prompts";
|
|
1679
1874
|
|
|
1680
1875
|
// src/lib/github.ts
|
|
1681
|
-
import
|
|
1682
|
-
import
|
|
1876
|
+
import fs17 from "fs";
|
|
1877
|
+
import path22 from "path";
|
|
1683
1878
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1684
1879
|
import os2 from "os";
|
|
1685
1880
|
import { Readable } from "stream";
|
|
@@ -1749,19 +1944,19 @@ async function latestGithubCommit(repo) {
|
|
|
1749
1944
|
}
|
|
1750
1945
|
}
|
|
1751
1946
|
function validateExtractedTree(destDir) {
|
|
1752
|
-
const resolved =
|
|
1947
|
+
const resolved = path22.resolve(destDir);
|
|
1753
1948
|
const walk = (dir) => {
|
|
1754
1949
|
let entries;
|
|
1755
1950
|
try {
|
|
1756
|
-
entries =
|
|
1951
|
+
entries = fs17.readdirSync(dir, { withFileTypes: true });
|
|
1757
1952
|
} catch {
|
|
1758
1953
|
return false;
|
|
1759
1954
|
}
|
|
1760
1955
|
for (const entry of entries) {
|
|
1761
|
-
const full =
|
|
1956
|
+
const full = path22.join(dir, entry.name);
|
|
1762
1957
|
let stat;
|
|
1763
1958
|
try {
|
|
1764
|
-
stat =
|
|
1959
|
+
stat = fs17.lstatSync(full);
|
|
1765
1960
|
} catch {
|
|
1766
1961
|
return false;
|
|
1767
1962
|
}
|
|
@@ -1777,15 +1972,15 @@ function validateExtractedTree(destDir) {
|
|
|
1777
1972
|
}
|
|
1778
1973
|
function resolveTarBin() {
|
|
1779
1974
|
if (process.platform !== "win32") return "tar";
|
|
1780
|
-
const winTar =
|
|
1781
|
-
return
|
|
1975
|
+
const winTar = path22.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
|
|
1976
|
+
return fs17.existsSync(winTar) ? winTar : "tar";
|
|
1782
1977
|
}
|
|
1783
1978
|
async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
1784
1979
|
const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
|
|
1785
|
-
const tmp =
|
|
1980
|
+
const tmp = path22.join(os2.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
|
|
1786
1981
|
const fail = (reason) => {
|
|
1787
1982
|
try {
|
|
1788
|
-
|
|
1983
|
+
fs17.rmSync(destDir, { recursive: true, force: true });
|
|
1789
1984
|
} catch {
|
|
1790
1985
|
}
|
|
1791
1986
|
return { ok: false, reason };
|
|
@@ -1804,10 +1999,10 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
|
1804
1999
|
if (!res.body) return fail("respuesta HTTP sin cuerpo");
|
|
1805
2000
|
await pipeline(
|
|
1806
2001
|
Readable.fromWeb(res.body),
|
|
1807
|
-
|
|
2002
|
+
fs17.createWriteStream(tmp)
|
|
1808
2003
|
);
|
|
1809
|
-
|
|
1810
|
-
|
|
2004
|
+
fs17.rmSync(destDir, { recursive: true, force: true });
|
|
2005
|
+
fs17.mkdirSync(destDir, { recursive: true });
|
|
1811
2006
|
try {
|
|
1812
2007
|
execFileSync2(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
|
|
1813
2008
|
} catch (err) {
|
|
@@ -1815,35 +2010,35 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
|
1815
2010
|
const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
|
|
1816
2011
|
return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
|
|
1817
2012
|
}
|
|
1818
|
-
const resolvedDest =
|
|
1819
|
-
const validateRoot = validateSubdir ?
|
|
2013
|
+
const resolvedDest = path22.resolve(destDir);
|
|
2014
|
+
const validateRoot = validateSubdir ? path22.resolve(resolvedDest, validateSubdir) : resolvedDest;
|
|
1820
2015
|
if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
|
|
1821
2016
|
return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
|
|
1822
2017
|
}
|
|
1823
|
-
if (
|
|
2018
|
+
if (fs17.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
|
|
1824
2019
|
return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
|
|
1825
2020
|
}
|
|
1826
|
-
const validated =
|
|
2021
|
+
const validated = fs17.existsSync(validateRoot);
|
|
1827
2022
|
return { ok: true, validated };
|
|
1828
2023
|
} catch (err) {
|
|
1829
2024
|
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
1830
2025
|
return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
|
|
1831
2026
|
} finally {
|
|
1832
2027
|
try {
|
|
1833
|
-
|
|
2028
|
+
fs17.rmSync(tmp, { force: true });
|
|
1834
2029
|
} catch {
|
|
1835
2030
|
}
|
|
1836
2031
|
}
|
|
1837
2032
|
}
|
|
1838
2033
|
|
|
1839
2034
|
// src/lib/skill-update.ts
|
|
1840
|
-
import
|
|
1841
|
-
import
|
|
2035
|
+
import fs18 from "fs";
|
|
2036
|
+
import path23 from "path";
|
|
1842
2037
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1843
2038
|
var PROTECTED_SKILLS = /* @__PURE__ */ new Set(["agent-delegation", "work-lifecycle"]);
|
|
1844
2039
|
function sameTextContentNormalized(a, b) {
|
|
1845
|
-
const ba =
|
|
1846
|
-
const bb =
|
|
2040
|
+
const ba = fs18.readFileSync(a);
|
|
2041
|
+
const bb = fs18.readFileSync(b);
|
|
1847
2042
|
if (ba.equals(bb)) return true;
|
|
1848
2043
|
const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
|
|
1849
2044
|
const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
|
|
@@ -1851,10 +2046,10 @@ function sameTextContentNormalized(a, b) {
|
|
|
1851
2046
|
}
|
|
1852
2047
|
function diffSkillDirs(upstreamDir, localDir) {
|
|
1853
2048
|
const upstreamFiles = new Set(
|
|
1854
|
-
listFilesRecursive(upstreamDir).map((f) =>
|
|
2049
|
+
listFilesRecursive(upstreamDir).map((f) => path23.relative(upstreamDir, f))
|
|
1855
2050
|
);
|
|
1856
2051
|
const localFiles = new Set(
|
|
1857
|
-
listFilesRecursive(localDir).map((f) =>
|
|
2052
|
+
listFilesRecursive(localDir).map((f) => path23.relative(localDir, f))
|
|
1858
2053
|
);
|
|
1859
2054
|
const added = [];
|
|
1860
2055
|
const modified = [];
|
|
@@ -1862,7 +2057,7 @@ function diffSkillDirs(upstreamDir, localDir) {
|
|
|
1862
2057
|
for (const rel of upstreamFiles) {
|
|
1863
2058
|
if (!localFiles.has(rel)) {
|
|
1864
2059
|
added.push(rel);
|
|
1865
|
-
} else if (!sameTextContentNormalized(
|
|
2060
|
+
} else if (!sameTextContentNormalized(path23.join(upstreamDir, rel), path23.join(localDir, rel))) {
|
|
1866
2061
|
modified.push(rel);
|
|
1867
2062
|
}
|
|
1868
2063
|
}
|
|
@@ -1924,8 +2119,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
1924
2119
|
if (PROTECTED_SKILLS.has(name)) {
|
|
1925
2120
|
throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
|
|
1926
2121
|
}
|
|
1927
|
-
const upstreamsFile = upstreamsFilePath ??
|
|
1928
|
-
const raw =
|
|
2122
|
+
const upstreamsFile = upstreamsFilePath ?? path23.join(path23.dirname(stackRoot()), "upstreams.json");
|
|
2123
|
+
const raw = fs18.readFileSync(upstreamsFile, "utf8");
|
|
1929
2124
|
const data = JSON.parse(raw);
|
|
1930
2125
|
const skillEntry = data?.skills?.[name];
|
|
1931
2126
|
if (!skillEntry) {
|
|
@@ -1934,8 +2129,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
1934
2129
|
if (skillEntry.kind === "release") {
|
|
1935
2130
|
throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
|
|
1936
2131
|
}
|
|
1937
|
-
const skillsRoot = localSkillsRoot ??
|
|
1938
|
-
const localSkillDir =
|
|
2132
|
+
const skillsRoot = localSkillsRoot ?? path23.join(stackRoot(), "skills");
|
|
2133
|
+
const localSkillDir = path23.join(skillsRoot, name);
|
|
1939
2134
|
const localFiles = listFilesRecursive(localSkillDir);
|
|
1940
2135
|
if (localFiles.length > 0) {
|
|
1941
2136
|
createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
|
|
@@ -1944,24 +2139,24 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
1944
2139
|
try {
|
|
1945
2140
|
const upstreamFiles = listFilesRecursive(upstreamSkillDir);
|
|
1946
2141
|
for (const src of upstreamFiles) {
|
|
1947
|
-
const st =
|
|
2142
|
+
const st = fs18.lstatSync(src);
|
|
1948
2143
|
if (st.isSymbolicLink()) {
|
|
1949
2144
|
throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
|
|
1950
2145
|
}
|
|
1951
|
-
const rel =
|
|
1952
|
-
const dest =
|
|
1953
|
-
ensureDir(
|
|
2146
|
+
const rel = path23.relative(upstreamSkillDir, src);
|
|
2147
|
+
const dest = path23.join(stagingDir, rel);
|
|
2148
|
+
ensureDir(path23.dirname(dest));
|
|
1954
2149
|
copyFile(src, dest);
|
|
1955
2150
|
}
|
|
1956
2151
|
const oldDir = `${localSkillDir}.old-${process.pid}`;
|
|
1957
|
-
if (
|
|
1958
|
-
|
|
2152
|
+
if (fs18.existsSync(localSkillDir)) {
|
|
2153
|
+
fs18.renameSync(localSkillDir, oldDir);
|
|
1959
2154
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
2155
|
+
fs18.renameSync(stagingDir, localSkillDir);
|
|
2156
|
+
fs18.rmSync(oldDir, { recursive: true, force: true });
|
|
1962
2157
|
} catch (err) {
|
|
1963
2158
|
try {
|
|
1964
|
-
|
|
2159
|
+
fs18.rmSync(stagingDir, { recursive: true, force: true });
|
|
1965
2160
|
} catch {
|
|
1966
2161
|
}
|
|
1967
2162
|
throw err;
|
|
@@ -1975,8 +2170,8 @@ function rateLimitHint(prefix) {
|
|
|
1975
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.`;
|
|
1976
2171
|
}
|
|
1977
2172
|
function loadUpstreams() {
|
|
1978
|
-
const file =
|
|
1979
|
-
return JSON.parse(
|
|
2173
|
+
const file = path24.join(path24.dirname(stackRoot()), "upstreams.json");
|
|
2174
|
+
return JSON.parse(fs19.readFileSync(file, "utf8"));
|
|
1980
2175
|
}
|
|
1981
2176
|
function skillsToScan(maintainer, upstreams) {
|
|
1982
2177
|
return maintainer ? Object.keys(upstreams.skills) : [];
|
|
@@ -2088,8 +2283,8 @@ function isEngramRunning() {
|
|
|
2088
2283
|
return null;
|
|
2089
2284
|
}
|
|
2090
2285
|
}
|
|
2091
|
-
function isGitClone(projectRoot =
|
|
2092
|
-
return
|
|
2286
|
+
function isGitClone(projectRoot = path24.dirname(stackRoot())) {
|
|
2287
|
+
return fs19.existsSync(path24.join(projectRoot, ".git"));
|
|
2093
2288
|
}
|
|
2094
2289
|
var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
|
|
2095
2290
|
function resolvePnpm() {
|
|
@@ -2099,12 +2294,12 @@ function resolvePnpm() {
|
|
|
2099
2294
|
}
|
|
2100
2295
|
function cleanupTmp(dir) {
|
|
2101
2296
|
try {
|
|
2102
|
-
|
|
2297
|
+
fs19.rmSync(dir, { recursive: true, force: true });
|
|
2103
2298
|
} catch {
|
|
2104
2299
|
}
|
|
2105
2300
|
}
|
|
2106
2301
|
function updateStackGitClone() {
|
|
2107
|
-
const projectRoot =
|
|
2302
|
+
const projectRoot = path24.dirname(stackRoot());
|
|
2108
2303
|
const git = lookPath("git");
|
|
2109
2304
|
if (!git) throw new Error("git no encontrado en PATH.");
|
|
2110
2305
|
const pnpm = resolvePnpm();
|
|
@@ -2121,7 +2316,7 @@ function updateStackGlobal() {
|
|
|
2121
2316
|
execFileSync4(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
|
|
2122
2317
|
}
|
|
2123
2318
|
async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
2124
|
-
const root =
|
|
2319
|
+
const root = fs19.mkdtempSync(path24.join(os3.tmpdir(), "jorgex-skill-"));
|
|
2125
2320
|
try {
|
|
2126
2321
|
const result = await downloadRepoTarball(repo, sha, root, skillPath);
|
|
2127
2322
|
if (!result.ok) {
|
|
@@ -2129,15 +2324,15 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2129
2324
|
return { error: result.reason };
|
|
2130
2325
|
}
|
|
2131
2326
|
if (skillPath) {
|
|
2132
|
-
const sub =
|
|
2327
|
+
const sub = path24.resolve(path24.join(root, skillPath));
|
|
2133
2328
|
if (!isContainedIn(sub, root)) {
|
|
2134
2329
|
cleanupTmp(root);
|
|
2135
2330
|
return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
|
|
2136
2331
|
}
|
|
2137
|
-
if (
|
|
2332
|
+
if (fs19.existsSync(sub)) return { dir: sub, root };
|
|
2138
2333
|
const lastSeg = skillPath.split("/").pop();
|
|
2139
|
-
const sub2 =
|
|
2140
|
-
if (isContainedIn(sub2, root) &&
|
|
2334
|
+
const sub2 = path24.resolve(path24.join(root, lastSeg));
|
|
2335
|
+
if (isContainedIn(sub2, root) && fs19.existsSync(sub2)) {
|
|
2141
2336
|
if (!validateExtractedTree(sub2)) {
|
|
2142
2337
|
cleanupTmp(root);
|
|
2143
2338
|
return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
|
|
@@ -2156,11 +2351,11 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2156
2351
|
function pruneEngramDbBackups() {
|
|
2157
2352
|
try {
|
|
2158
2353
|
const dir = dataDir();
|
|
2159
|
-
if (!
|
|
2160
|
-
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);
|
|
2161
2356
|
for (const old of backups.slice(3)) {
|
|
2162
2357
|
try {
|
|
2163
|
-
|
|
2358
|
+
fs19.rmSync(path24.join(dir, old.name));
|
|
2164
2359
|
} catch {
|
|
2165
2360
|
}
|
|
2166
2361
|
}
|
|
@@ -2168,18 +2363,18 @@ function pruneEngramDbBackups() {
|
|
|
2168
2363
|
}
|
|
2169
2364
|
}
|
|
2170
2365
|
function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
2171
|
-
if (!
|
|
2172
|
-
const dir =
|
|
2173
|
-
const base =
|
|
2366
|
+
if (!fs19.existsSync(binPath)) return null;
|
|
2367
|
+
const dir = path24.dirname(binPath);
|
|
2368
|
+
const base = path24.basename(binPath);
|
|
2174
2369
|
const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
|
|
2175
2370
|
const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
|
|
2176
|
-
const resolvedDir =
|
|
2177
|
-
if (resolvedDir ===
|
|
2371
|
+
const resolvedDir = path24.resolve(dir);
|
|
2372
|
+
if (resolvedDir === path24.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
|
|
2178
2373
|
try {
|
|
2179
|
-
for (const entry of
|
|
2374
|
+
for (const entry of fs19.readdirSync(dir)) {
|
|
2180
2375
|
if (oldPattern.test(entry)) {
|
|
2181
2376
|
try {
|
|
2182
|
-
|
|
2377
|
+
fs19.rmSync(path24.join(dir, entry), { force: true });
|
|
2183
2378
|
} catch {
|
|
2184
2379
|
}
|
|
2185
2380
|
}
|
|
@@ -2187,8 +2382,8 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
|
2187
2382
|
} catch {
|
|
2188
2383
|
}
|
|
2189
2384
|
}
|
|
2190
|
-
const rotated =
|
|
2191
|
-
|
|
2385
|
+
const rotated = path24.join(dir, `${base}.old-${Date.now()}`);
|
|
2386
|
+
fs19.renameSync(binPath, rotated);
|
|
2192
2387
|
return rotated;
|
|
2193
2388
|
}
|
|
2194
2389
|
async function updateEngram(engramRepo, latestVersion) {
|
|
@@ -2197,19 +2392,19 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2197
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)."
|
|
2198
2393
|
);
|
|
2199
2394
|
}
|
|
2200
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
2201
|
-
const engramDb =
|
|
2202
|
-
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)) {
|
|
2203
2398
|
const doBackup = await p4.confirm({
|
|
2204
2399
|
message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
|
|
2205
2400
|
initialValue: true
|
|
2206
2401
|
});
|
|
2207
2402
|
if (!p4.isCancel(doBackup) && doBackup) {
|
|
2208
2403
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2209
|
-
const dest =
|
|
2404
|
+
const dest = path24.join(dataDir(), `engram-db-backup-${ts}.db`);
|
|
2210
2405
|
try {
|
|
2211
|
-
if (!
|
|
2212
|
-
|
|
2406
|
+
if (!fs19.existsSync(dataDir())) fs19.mkdirSync(dataDir(), { recursive: true });
|
|
2407
|
+
fs19.copyFileSync(engramDb, dest);
|
|
2213
2408
|
p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
|
|
2214
2409
|
pruneEngramDbBackups();
|
|
2215
2410
|
} catch (err) {
|
|
@@ -2259,10 +2454,10 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2259
2454
|
["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
|
|
2260
2455
|
{ stdio: "inherit" }
|
|
2261
2456
|
);
|
|
2262
|
-
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists:
|
|
2457
|
+
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs19.existsSync(bin ?? "") });
|
|
2263
2458
|
if (rollbackOk.action === "restore") {
|
|
2264
2459
|
try {
|
|
2265
|
-
|
|
2460
|
+
fs19.renameSync(rotated, bin);
|
|
2266
2461
|
p4.log.warn(rollbackOk.messages.onRestore);
|
|
2267
2462
|
} catch {
|
|
2268
2463
|
p4.log.warn(rollbackOk.messages.onRenameFail);
|
|
@@ -2270,10 +2465,10 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2270
2465
|
}
|
|
2271
2466
|
return true;
|
|
2272
2467
|
} catch (err) {
|
|
2273
|
-
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists:
|
|
2468
|
+
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs19.existsSync(bin ?? "") });
|
|
2274
2469
|
if (rollbackFail.action === "restore") {
|
|
2275
2470
|
try {
|
|
2276
|
-
|
|
2471
|
+
fs19.renameSync(rotated, bin);
|
|
2277
2472
|
p4.log.info(rollbackFail.messages.onRestore);
|
|
2278
2473
|
} catch {
|
|
2279
2474
|
p4.log.error(rollbackFail.messages.onRenameFail);
|
|
@@ -2531,7 +2726,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2531
2726
|
continue;
|
|
2532
2727
|
}
|
|
2533
2728
|
const { dir: tmpDir, root: tmpRoot } = tmpResult;
|
|
2534
|
-
const localSkillDir =
|
|
2729
|
+
const localSkillDir = path24.join(stackRoot(), "skills", skillInfo.name);
|
|
2535
2730
|
const diff = renderSkillDiff(tmpDir, localSkillDir);
|
|
2536
2731
|
if (diff) {
|
|
2537
2732
|
p4.log.info(`Diff de ${skillInfo.name}:
|
|
@@ -2590,7 +2785,7 @@ ${diff}`);
|
|
|
2590
2785
|
}
|
|
2591
2786
|
|
|
2592
2787
|
// src/models-picker.ts
|
|
2593
|
-
import
|
|
2788
|
+
import path25 from "path";
|
|
2594
2789
|
import * as p5 from "@clack/prompts";
|
|
2595
2790
|
var TIERS = ["strong", "standard", "cheap"];
|
|
2596
2791
|
var EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
@@ -2606,7 +2801,7 @@ function opencodeLiveModels(binPath) {
|
|
|
2606
2801
|
}
|
|
2607
2802
|
function agentsByTier() {
|
|
2608
2803
|
const grouped = { strong: [], standard: [], cheap: [] };
|
|
2609
|
-
for (const agent of loadCanonicalAgents(
|
|
2804
|
+
for (const agent of loadCanonicalAgents(path25.join(stackRoot(), "agents"))) {
|
|
2610
2805
|
if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
|
|
2611
2806
|
}
|
|
2612
2807
|
return grouped;
|
|
@@ -2765,16 +2960,16 @@ function cancelled() {
|
|
|
2765
2960
|
}
|
|
2766
2961
|
|
|
2767
2962
|
// src/lib/release.ts
|
|
2768
|
-
import
|
|
2769
|
-
import
|
|
2963
|
+
import fs20 from "fs";
|
|
2964
|
+
import path26 from "path";
|
|
2770
2965
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
2771
2966
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2772
2967
|
function findPackageJson() {
|
|
2773
|
-
let dir =
|
|
2968
|
+
let dir = path26.dirname(fileURLToPath2(import.meta.url));
|
|
2774
2969
|
for (let i = 0; i < 6; i++) {
|
|
2775
|
-
const candidate =
|
|
2776
|
-
if (
|
|
2777
|
-
dir =
|
|
2970
|
+
const candidate = path26.join(dir, "package.json");
|
|
2971
|
+
if (fs20.existsSync(candidate)) return candidate;
|
|
2972
|
+
dir = path26.dirname(dir);
|
|
2778
2973
|
}
|
|
2779
2974
|
throw new Error("No se encontr\xF3 package.json cerca del CLI.");
|
|
2780
2975
|
}
|
|
@@ -2783,7 +2978,7 @@ function readPackageVersion() {
|
|
|
2783
2978
|
}
|
|
2784
2979
|
function readPackageMetadata() {
|
|
2785
2980
|
const packageJson = findPackageJson();
|
|
2786
|
-
const raw =
|
|
2981
|
+
const raw = fs20.readFileSync(packageJson, "utf8");
|
|
2787
2982
|
const parsed = JSON.parse(raw);
|
|
2788
2983
|
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
2789
2984
|
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
@@ -2801,6 +2996,8 @@ function parseFlags(args) {
|
|
|
2801
2996
|
agents: [],
|
|
2802
2997
|
dryRun: false,
|
|
2803
2998
|
yes: false,
|
|
2999
|
+
mode: void 0,
|
|
3000
|
+
subagentConcurrency: void 0,
|
|
2804
3001
|
help: false,
|
|
2805
3002
|
version: false,
|
|
2806
3003
|
list: false,
|
|
@@ -2808,12 +3005,33 @@ function parseFlags(args) {
|
|
|
2808
3005
|
removeEngram: false,
|
|
2809
3006
|
positional: []
|
|
2810
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
|
+
};
|
|
2811
3013
|
for (let i = 0; i < args.length; i++) {
|
|
2812
3014
|
const arg = args[i];
|
|
2813
|
-
if (arg === "--agents" || arg === "-a")
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
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);
|
|
2817
3035
|
else if (arg === "--dry-run") flags.dryRun = true;
|
|
2818
3036
|
else if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
2819
3037
|
else if (arg === "--help" || arg === "-h") flags.help = true;
|
|
@@ -2825,6 +3043,67 @@ function parseFlags(args) {
|
|
|
2825
3043
|
}
|
|
2826
3044
|
return flags;
|
|
2827
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
|
+
}
|
|
2828
3107
|
function parseCliArgs(argv) {
|
|
2829
3108
|
const [first, ...rest] = argv;
|
|
2830
3109
|
const isCommand = COMMANDS.includes(first ?? "install");
|
|
@@ -2873,6 +3152,8 @@ Comandos:
|
|
|
2873
3152
|
|
|
2874
3153
|
Opciones:
|
|
2875
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
|
|
2876
3157
|
--target-dir <dir> Dir alternativo (pruebas de paridad; requiere 1 runtime)
|
|
2877
3158
|
--dry-run Muestra el plan sin escribir nada
|
|
2878
3159
|
--yes, -y No interactivo
|
|
@@ -2900,6 +3181,8 @@ async function main() {
|
|
|
2900
3181
|
switch (command) {
|
|
2901
3182
|
case "install":
|
|
2902
3183
|
case "sync": {
|
|
3184
|
+
const mode = await resolveInstallMode(flags);
|
|
3185
|
+
if (mode === null) return;
|
|
2903
3186
|
const runtimes = await resolveRuntimes(flags);
|
|
2904
3187
|
if (runtimes === null) return;
|
|
2905
3188
|
if (runtimes.length === 0) {
|
|
@@ -2907,7 +3190,7 @@ async function main() {
|
|
|
2907
3190
|
process.exitCode = 1;
|
|
2908
3191
|
return;
|
|
2909
3192
|
}
|
|
2910
|
-
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 });
|
|
2911
3194
|
return;
|
|
2912
3195
|
}
|
|
2913
3196
|
case "uninstall": {
|
|
@@ -2942,23 +3225,46 @@ async function main() {
|
|
|
2942
3225
|
}
|
|
2943
3226
|
const runtimes = await resolveRuntimes(flags);
|
|
2944
3227
|
if (runtimes === null) return;
|
|
2945
|
-
|
|
2946
|
-
|
|
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
|
+
});
|
|
2947
3243
|
if (code !== 0) {
|
|
2948
3244
|
process.exitCode = code;
|
|
2949
3245
|
return;
|
|
2950
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.");
|
|
2951
3249
|
}
|
|
2952
3250
|
const result = await runInteractiveUpdate(VERSION, flags.yes, flags.dryRun);
|
|
2953
3251
|
process.exitCode = result.exitCode;
|
|
2954
|
-
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) {
|
|
2955
3255
|
const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
|
|
2956
3256
|
if (!p6.isCancel(apply) && apply) {
|
|
2957
|
-
process.exitCode = await runInstall({
|
|
3257
|
+
process.exitCode = await runInstall({
|
|
3258
|
+
runtimes,
|
|
3259
|
+
targetDir: flags.targetDir,
|
|
3260
|
+
dryRun: false,
|
|
3261
|
+
yes: false,
|
|
3262
|
+
mode
|
|
3263
|
+
});
|
|
2958
3264
|
} else {
|
|
2959
3265
|
console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
|
|
2960
3266
|
}
|
|
2961
|
-
} 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)) {
|
|
2962
3268
|
console.log("Skills/stack actualizados. Ejecuta jorgex-stack sync para aplicarlos a los runtimes.");
|
|
2963
3269
|
}
|
|
2964
3270
|
return;
|
|
@@ -2976,7 +3282,17 @@ async function main() {
|
|
|
2976
3282
|
if (code === 0 && !flags.yes && process.stdout.isTTY) {
|
|
2977
3283
|
const apply = await p6.confirm({ message: "\xBFAplicar ahora los modelos a los agentes instalados? (sync)" });
|
|
2978
3284
|
if (!p6.isCancel(apply) && apply) {
|
|
2979
|
-
|
|
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 });
|
|
2980
3296
|
} else {
|
|
2981
3297
|
console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
|
|
2982
3298
|
}
|