jorgex-stack 1.0.31 → 1.1.1
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 +35 -3
- package/dist/cli.js +902 -191
- package/package.json +1 -1
- package/stack/mcp/servers.json +8 -0
- package/stack/skills/playwright-cli/SKILL.md +420 -0
- package/stack/skills/playwright-cli/references/element-attributes.md +23 -0
- package/stack/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/stack/skills/playwright-cli/references/request-mocking.md +87 -0
- package/stack/skills/playwright-cli/references/running-code.md +241 -0
- package/stack/skills/playwright-cli/references/session-management.md +225 -0
- package/stack/skills/playwright-cli/references/storage-state.md +275 -0
- package/stack/skills/playwright-cli/references/test-generation.md +433 -0
- package/stack/skills/playwright-cli/references/tracing.md +139 -0
- package/stack/skills/playwright-cli/references/video-recording.md +143 -0
- package/stack/system-prompt/browser-chrome-devtools.md +3 -0
- package/stack/system-prompt/browser-playwright.md +5 -0
- package/upstreams.json +9 -4
- package/stack/skills/agent-browser/SKILL.md +0 -55
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 fs15 from "fs";
|
|
9
|
+
import path21 from "path";
|
|
10
10
|
import * as p from "@clack/prompts";
|
|
11
11
|
|
|
12
12
|
// src/adapters/opencode.ts
|
|
@@ -48,6 +48,10 @@ function loadCanonicalAgents(agentsDir) {
|
|
|
48
48
|
return parseCanonicalAgent(source, file);
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
+
var DEVTOOLS_MCP_SERVER = "chrome-devtools";
|
|
52
|
+
function isCanonicalMcpServerEnabled(name, server, enabledServers) {
|
|
53
|
+
return !server.optional || server.defaultEnabled === true || enabledServers?.has(name) === true;
|
|
54
|
+
}
|
|
51
55
|
function loadCanonicalMcp(stackDir) {
|
|
52
56
|
return JSON.parse(fs.readFileSync(path.join(stackDir, "mcp", "servers.json"), "utf8"));
|
|
53
57
|
}
|
|
@@ -221,7 +225,7 @@ function ensureModelMapFile() {
|
|
|
221
225
|
// src/lib/detect.ts
|
|
222
226
|
import path5 from "path";
|
|
223
227
|
import { existsSync as existsSync3, statSync } from "fs";
|
|
224
|
-
import { execFileSync
|
|
228
|
+
import { execFileSync } from "child_process";
|
|
225
229
|
function lookPath(cmd) {
|
|
226
230
|
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ".ps1", ""] : [""];
|
|
227
231
|
for (const dir of (process.env.PATH ?? "").split(path5.delimiter)) {
|
|
@@ -233,17 +237,24 @@ function lookPath(cmd) {
|
|
|
233
237
|
}
|
|
234
238
|
return null;
|
|
235
239
|
}
|
|
240
|
+
function planDetectedBinCommand(bin, args) {
|
|
241
|
+
if (process.platform !== "win32" || !/\.(cmd|bat)$/i.test(bin)) return { command: bin, args };
|
|
242
|
+
if ([bin, ...args].some((part) => /[&|<>()^%!"\r\n]/.test(part))) return null;
|
|
243
|
+
const quote = (part) => part === "" || /\s/.test(part) ? `"${part}"` : part;
|
|
244
|
+
return {
|
|
245
|
+
command: process.env.ComSpec ?? "cmd.exe",
|
|
246
|
+
args: ["/d", "/s", "/c", [quote(bin), ...args.map(quote)].join(" ")]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
236
249
|
function runDetectedBin(bin, args, timeoutMs) {
|
|
237
250
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
return execFileSync(bin, args, { encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"] });
|
|
251
|
+
const command = planDetectedBinCommand(bin, args);
|
|
252
|
+
if (command === null) return null;
|
|
253
|
+
return execFileSync(command.command, command.args, {
|
|
254
|
+
encoding: "utf8",
|
|
255
|
+
timeout: timeoutMs,
|
|
256
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
257
|
+
});
|
|
247
258
|
} catch {
|
|
248
259
|
return null;
|
|
249
260
|
}
|
|
@@ -505,6 +516,14 @@ var GIT_GUARD_SCRIPT = "block-destructive-git.cjs";
|
|
|
505
516
|
function yamlString(value) {
|
|
506
517
|
return JSON.stringify(value);
|
|
507
518
|
}
|
|
519
|
+
function isManagedOptionalStdioServer(server, value) {
|
|
520
|
+
if (!server.optional || server.transport !== "stdio" || value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
const current = value;
|
|
524
|
+
const expectedCommand = [server.command, ...server.args ?? []];
|
|
525
|
+
return Object.keys(current).length === 2 && current.type === "local" && Array.isArray(current.command) && current.command.length === expectedCommand.length && current.command.every((arg, index) => arg === expectedCommand[index]);
|
|
526
|
+
}
|
|
508
527
|
var opencodeAdapter = {
|
|
509
528
|
id: "opencode",
|
|
510
529
|
name: "OpenCode",
|
|
@@ -636,6 +655,7 @@ ${agent.body}`,
|
|
|
636
655
|
const original = readTextIfExists(file);
|
|
637
656
|
const contentSource = original === null || original.trim() === "" ? null : original;
|
|
638
657
|
const isFreshConfig = contentSource === null;
|
|
658
|
+
const mcpOwnership = [];
|
|
639
659
|
const content = upsertJson(contentSource, (root) => {
|
|
640
660
|
root["$schema"] ??= "https://opencode.ai/config.json";
|
|
641
661
|
const defaults = loadCanonicalDefaults(ctx.stackDir)["opencode"];
|
|
@@ -647,6 +667,22 @@ ${agent.body}`,
|
|
|
647
667
|
}
|
|
648
668
|
const mcp = root["mcp"] ??= {};
|
|
649
669
|
for (const [name, server] of Object.entries(canonical.servers)) {
|
|
670
|
+
const existing = mcp[name];
|
|
671
|
+
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
672
|
+
if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
|
|
673
|
+
if (owned) {
|
|
674
|
+
if (isManagedOptionalStdioServer(server, existing)) delete mcp[name];
|
|
675
|
+
mcpOwnership.push({ server: name, owned: false });
|
|
676
|
+
}
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (server.optional && existing !== void 0) {
|
|
680
|
+
if (!owned || !isManagedOptionalStdioServer(server, existing)) {
|
|
681
|
+
if (owned) mcpOwnership.push({ server: name, owned: false });
|
|
682
|
+
ctx.warnings.push(`OpenCode: MCP opcional '${name}' ya pertenece a la configuraci\xF3n del usuario; se conserva.`);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
650
686
|
if (server.transport === "stdio") {
|
|
651
687
|
if (server.command === "{{ENGRAM_BIN}}" && ctx.engramBin === null) {
|
|
652
688
|
ctx.warnings.push(
|
|
@@ -656,6 +692,7 @@ ${agent.body}`,
|
|
|
656
692
|
}
|
|
657
693
|
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
658
694
|
mcp[name] = { type: "local", command: [command, ...server.args ?? []] };
|
|
695
|
+
if (server.optional && existing === void 0 && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
659
696
|
} else {
|
|
660
697
|
const previous = mcp[name];
|
|
661
698
|
const headers = {};
|
|
@@ -685,7 +722,7 @@ ${agent.body}`,
|
|
|
685
722
|
}
|
|
686
723
|
}
|
|
687
724
|
});
|
|
688
|
-
return [{ kind: "write", target: file, content }];
|
|
725
|
+
return [{ kind: "write", target: file, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} }];
|
|
689
726
|
},
|
|
690
727
|
planUnmerge(mcp, hooks, ctx) {
|
|
691
728
|
const actions = [];
|
|
@@ -694,15 +731,26 @@ ${agent.body}`,
|
|
|
694
731
|
if (prompt !== null) {
|
|
695
732
|
let content = removeMarkdownSection(prompt, "system-prompt");
|
|
696
733
|
content = removeMarkdownSection(content, "engram-protocol");
|
|
734
|
+
content = removeMarkdownSection(content, "browser");
|
|
697
735
|
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
698
736
|
}
|
|
699
737
|
const configFile = path7.join(ctx.configDir, "opencode.json");
|
|
700
738
|
const config = readTextIfExists(configFile);
|
|
701
739
|
if (config !== null) {
|
|
740
|
+
const mcpOwnership = [];
|
|
702
741
|
const content = upsertJson(config, (root) => {
|
|
703
742
|
const mcpBlock = root["mcp"];
|
|
704
743
|
if (mcpBlock) {
|
|
705
|
-
for (const name of Object.
|
|
744
|
+
for (const [name, server] of Object.entries(mcp.servers)) {
|
|
745
|
+
if (!server.optional) {
|
|
746
|
+
delete mcpBlock[name];
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (ctx.ownedMcpServers?.has(name) === true) {
|
|
750
|
+
if (isManagedOptionalStdioServer(server, mcpBlock[name])) delete mcpBlock[name];
|
|
751
|
+
mcpOwnership.push({ server: name, owned: false });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
706
754
|
if (Object.keys(mcpBlock).length === 0) delete root["mcp"];
|
|
707
755
|
}
|
|
708
756
|
const plugin = root["plugin"];
|
|
@@ -713,7 +761,7 @@ ${agent.body}`,
|
|
|
713
761
|
else root["plugin"] = kept;
|
|
714
762
|
}
|
|
715
763
|
});
|
|
716
|
-
actions.push({ kind: "write", target: configFile, content });
|
|
764
|
+
actions.push({ kind: "write", target: configFile, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} });
|
|
717
765
|
}
|
|
718
766
|
const hooksFile = path7.join(ctx.configDir, "hooks.json");
|
|
719
767
|
const hooksJson = readTextIfExists(hooksFile);
|
|
@@ -773,6 +821,14 @@ function hasEngramPlugin(configDir) {
|
|
|
773
821
|
return false;
|
|
774
822
|
}
|
|
775
823
|
}
|
|
824
|
+
function isManagedOptionalStdioServer2(server, value) {
|
|
825
|
+
if (!server.optional || server.transport !== "stdio" || value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
const current = value;
|
|
829
|
+
const expectedArgs = server.args ?? [];
|
|
830
|
+
return Object.keys(current).length === 3 && current.type === "stdio" && current.command === server.command && Array.isArray(current.args) && current.args.length === expectedArgs.length && current.args.every((arg, index) => arg === expectedArgs[index]);
|
|
831
|
+
}
|
|
776
832
|
var claudeCodeAdapter = {
|
|
777
833
|
id: "claude-code",
|
|
778
834
|
name: "Claude Code",
|
|
@@ -860,9 +916,26 @@ ${agent.body}`,
|
|
|
860
916
|
},
|
|
861
917
|
planMainConfig(canonical, ctx) {
|
|
862
918
|
const file = path8.join(path8.dirname(ctx.configDir), `${path8.basename(ctx.configDir)}.json`);
|
|
919
|
+
const mcpOwnership = [];
|
|
863
920
|
const content = upsertJson(readTextIfExists(file), (root) => {
|
|
864
921
|
const servers = root["mcpServers"] ??= {};
|
|
865
922
|
for (const [name, server] of Object.entries(canonical.servers)) {
|
|
923
|
+
const existing = servers[name];
|
|
924
|
+
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
925
|
+
if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
|
|
926
|
+
if (owned) {
|
|
927
|
+
if (isManagedOptionalStdioServer2(server, existing)) delete servers[name];
|
|
928
|
+
mcpOwnership.push({ server: name, owned: false });
|
|
929
|
+
}
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (server.optional && existing !== void 0) {
|
|
933
|
+
if (!owned || !isManagedOptionalStdioServer2(server, existing)) {
|
|
934
|
+
if (owned) mcpOwnership.push({ server: name, owned: false });
|
|
935
|
+
ctx.warnings.push(`Claude Code: MCP opcional '${name}' ya pertenece a la configuraci\xF3n del usuario; se conserva.`);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
866
939
|
if (server.transport === "stdio") {
|
|
867
940
|
if (server.command === "{{ENGRAM_BIN}}" && hasEngramPlugin(ctx.configDir)) {
|
|
868
941
|
if (name in servers) delete servers[name];
|
|
@@ -879,6 +952,7 @@ ${agent.body}`,
|
|
|
879
952
|
}
|
|
880
953
|
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
881
954
|
servers[name] = { type: "stdio", command, args: server.args ?? [] };
|
|
955
|
+
if (server.optional && existing === void 0 && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
882
956
|
} else {
|
|
883
957
|
const previous = servers[name];
|
|
884
958
|
const headers = {};
|
|
@@ -894,7 +968,7 @@ ${agent.body}`,
|
|
|
894
968
|
}
|
|
895
969
|
}
|
|
896
970
|
});
|
|
897
|
-
return [{ kind: "write", target: file, content }];
|
|
971
|
+
return [{ kind: "write", target: file, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} }];
|
|
898
972
|
},
|
|
899
973
|
planUnmerge(mcp, hooks, ctx) {
|
|
900
974
|
const actions = [];
|
|
@@ -903,6 +977,7 @@ ${agent.body}`,
|
|
|
903
977
|
if (prompt !== null) {
|
|
904
978
|
let content = removeMarkdownSection(prompt, "system-prompt");
|
|
905
979
|
content = removeMarkdownSection(content, "engram-protocol");
|
|
980
|
+
content = removeMarkdownSection(content, "browser");
|
|
906
981
|
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
907
982
|
}
|
|
908
983
|
const settingsFile = path8.join(ctx.configDir, "settings.json");
|
|
@@ -916,13 +991,23 @@ ${agent.body}`,
|
|
|
916
991
|
const mainFile = path8.join(path8.dirname(ctx.configDir), `${path8.basename(ctx.configDir)}.json`);
|
|
917
992
|
const main2 = readTextIfExists(mainFile);
|
|
918
993
|
if (main2 !== null) {
|
|
994
|
+
const mcpOwnership = [];
|
|
919
995
|
const content = upsertJson(main2, (root) => {
|
|
920
996
|
const servers = root["mcpServers"];
|
|
921
997
|
if (!servers) return;
|
|
922
|
-
for (const name of Object.
|
|
998
|
+
for (const [name, server] of Object.entries(mcp.servers)) {
|
|
999
|
+
if (!server.optional) {
|
|
1000
|
+
delete servers[name];
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
if (ctx.ownedMcpServers?.has(name) === true) {
|
|
1004
|
+
if (isManagedOptionalStdioServer2(server, servers[name])) delete servers[name];
|
|
1005
|
+
mcpOwnership.push({ server: name, owned: false });
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
923
1008
|
if (Object.keys(servers).length === 0) delete root["mcpServers"];
|
|
924
1009
|
});
|
|
925
|
-
actions.push({ kind: "write", target: mainFile, content });
|
|
1010
|
+
actions.push({ kind: "write", target: mainFile, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} });
|
|
926
1011
|
}
|
|
927
1012
|
return actions;
|
|
928
1013
|
}
|
|
@@ -940,6 +1025,14 @@ function tomlMultiline(value) {
|
|
|
940
1025
|
${value.replace(/\r\n/g, "\n").trim()}
|
|
941
1026
|
'''`;
|
|
942
1027
|
}
|
|
1028
|
+
function stdioMcpSection(server) {
|
|
1029
|
+
const args = (server.args ?? []).map(tomlString).join(", ");
|
|
1030
|
+
return `command = ${tomlString(server.command)}
|
|
1031
|
+
args = [${args}]`;
|
|
1032
|
+
}
|
|
1033
|
+
function isManagedOptionalStdioServer3(server, section) {
|
|
1034
|
+
return server.optional === true && server.transport === "stdio" && section?.trim() === stdioMcpSection(server);
|
|
1035
|
+
}
|
|
943
1036
|
function hasActiveEngramPlugin(configDir) {
|
|
944
1037
|
const config = readTextIfExists(path9.join(configDir, "config.toml"));
|
|
945
1038
|
if (config === null) return false;
|
|
@@ -1043,6 +1136,7 @@ ${body}`
|
|
|
1043
1136
|
const original = readTextIfExists(file);
|
|
1044
1137
|
const contentSource = original === null || original.trim() === "" ? null : original;
|
|
1045
1138
|
let content = contentSource;
|
|
1139
|
+
const mcpOwnership = [];
|
|
1046
1140
|
if (contentSource === null) {
|
|
1047
1141
|
const defaults = loadCanonicalDefaults(ctx.stackDir)["codex"] ?? {};
|
|
1048
1142
|
for (const [key, value] of Object.entries(defaults)) {
|
|
@@ -1089,6 +1183,22 @@ ${body}`
|
|
|
1089
1183
|
}
|
|
1090
1184
|
for (const [name, server] of Object.entries(canonical.servers)) {
|
|
1091
1185
|
const section = `mcp_servers.${name}`;
|
|
1186
|
+
const existing = readTomlSection(content, section);
|
|
1187
|
+
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
1188
|
+
if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
|
|
1189
|
+
if (owned) {
|
|
1190
|
+
if (isManagedOptionalStdioServer3(server, existing)) content = removeTomlSection(content, section);
|
|
1191
|
+
mcpOwnership.push({ server: name, owned: false });
|
|
1192
|
+
}
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (server.optional && existing !== null) {
|
|
1196
|
+
if (!owned || !isManagedOptionalStdioServer3(server, existing)) {
|
|
1197
|
+
if (owned) mcpOwnership.push({ server: name, owned: false });
|
|
1198
|
+
ctx.warnings.push(`Codex: MCP opcional '${name}' ya pertenece a la configuraci\xF3n del usuario; se conserva.`);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1092
1202
|
if (server.transport === "stdio") {
|
|
1093
1203
|
if (server.command === "{{ENGRAM_BIN}}" && hasActiveEngramPlugin(ctx.configDir)) {
|
|
1094
1204
|
if (content !== null) content = removeTomlSection(content, section);
|
|
@@ -1104,9 +1214,9 @@ ${body}`
|
|
|
1104
1214
|
continue;
|
|
1105
1215
|
}
|
|
1106
1216
|
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
1107
|
-
const args = (server.args ?? []).map(tomlString).join(", ");
|
|
1108
1217
|
content = upsertTomlSection(content, section, `command = ${tomlString(command)}
|
|
1109
|
-
args = [${args}]`);
|
|
1218
|
+
args = [${(server.args ?? []).map(tomlString).join(", ")}]`);
|
|
1219
|
+
if (server.optional && existing === null && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
1110
1220
|
} else {
|
|
1111
1221
|
const previousSection = readTomlSection(content, section);
|
|
1112
1222
|
const prevUsesEnvHeaders = previousSection?.includes("env_http_headers") ?? false;
|
|
@@ -1127,7 +1237,7 @@ args = [${args}]`);
|
|
|
1127
1237
|
}
|
|
1128
1238
|
if (content === null) return [];
|
|
1129
1239
|
if (!content.endsWith("\n")) content += "\n";
|
|
1130
|
-
return [{ kind: "write", target: file, content }];
|
|
1240
|
+
return [{ kind: "write", target: file, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} }];
|
|
1131
1241
|
},
|
|
1132
1242
|
planUnmerge(mcp, hooks, ctx) {
|
|
1133
1243
|
const actions = [];
|
|
@@ -1136,14 +1246,28 @@ args = [${args}]`);
|
|
|
1136
1246
|
if (prompt !== null) {
|
|
1137
1247
|
let content = removeMarkdownSection(prompt, "system-prompt");
|
|
1138
1248
|
content = removeMarkdownSection(content, "engram-protocol");
|
|
1249
|
+
content = removeMarkdownSection(content, "browser");
|
|
1139
1250
|
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
1140
1251
|
}
|
|
1141
1252
|
const configFile = path9.join(ctx.configDir, "config.toml");
|
|
1142
1253
|
const config = readTextIfExists(configFile);
|
|
1143
1254
|
if (config !== null) {
|
|
1144
1255
|
let content = config;
|
|
1145
|
-
|
|
1146
|
-
|
|
1256
|
+
const mcpOwnership = [];
|
|
1257
|
+
for (const [name, server] of Object.entries(mcp.servers)) {
|
|
1258
|
+
const section = `mcp_servers.${name}`;
|
|
1259
|
+
if (!server.optional) {
|
|
1260
|
+
content = removeTomlSection(content, section);
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (ctx.ownedMcpServers?.has(name) === true) {
|
|
1264
|
+
if (isManagedOptionalStdioServer3(server, readTomlSection(content, section))) {
|
|
1265
|
+
content = removeTomlSection(content, section);
|
|
1266
|
+
}
|
|
1267
|
+
mcpOwnership.push({ server: name, owned: false });
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
actions.push({ kind: "write", target: configFile, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} });
|
|
1147
1271
|
}
|
|
1148
1272
|
const hooksFile = path9.join(ctx.configDir, "hooks.json");
|
|
1149
1273
|
const hooksJson = readTextIfExists(hooksFile);
|
|
@@ -1409,6 +1533,10 @@ function planSystemPrompt(adapter, ctx) {
|
|
|
1409
1533
|
normalize2(fs10.readFileSync(path14.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8"))
|
|
1410
1534
|
);
|
|
1411
1535
|
const composedAgentsMd = composeProgrammaticSystemPrompt(ctx.stackDir, agentsMd, ctx.mode);
|
|
1536
|
+
const browser = [
|
|
1537
|
+
ctx.playwrightCliEnabled ? "browser-playwright.md" : null,
|
|
1538
|
+
ctx.enabledMcpServers?.has(DEVTOOLS_MCP_SERVER) ? "browser-chrome-devtools.md" : null
|
|
1539
|
+
].filter((file) => file !== null).map((file) => normalize2(fs10.readFileSync(path14.join(ctx.stackDir, "system-prompt", file), "utf8"))).join("\n\n");
|
|
1412
1540
|
let content = readTextIfExists(target);
|
|
1413
1541
|
content = upsertMarkdownSection(content, "system-prompt", composedAgentsMd);
|
|
1414
1542
|
if (adapter.injectEngramProtocol(ctx)) {
|
|
@@ -1416,6 +1544,7 @@ function planSystemPrompt(adapter, ctx) {
|
|
|
1416
1544
|
} else {
|
|
1417
1545
|
content = removeMarkdownSection(content, "engram-protocol");
|
|
1418
1546
|
}
|
|
1547
|
+
content = browser === "" ? removeMarkdownSection(content, "browser") : upsertMarkdownSection(content, "browser", browser);
|
|
1419
1548
|
return [{ kind: "write", target, content }];
|
|
1420
1549
|
}
|
|
1421
1550
|
|
|
@@ -1517,13 +1646,256 @@ function planPlugins(adapter, ctx) {
|
|
|
1517
1646
|
});
|
|
1518
1647
|
}
|
|
1519
1648
|
|
|
1649
|
+
// src/lib/external-tools.ts
|
|
1650
|
+
import fs13 from "fs";
|
|
1651
|
+
import os2 from "os";
|
|
1652
|
+
import path19 from "path";
|
|
1653
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1654
|
+
var PLAYWRIGHT_CLI = {
|
|
1655
|
+
packageName: "@playwright/cli",
|
|
1656
|
+
bin: "playwright-cli",
|
|
1657
|
+
version: "0.1.17",
|
|
1658
|
+
browserInstallAction: "install-browser"
|
|
1659
|
+
};
|
|
1660
|
+
function parsePlaywrightCliVersion(output) {
|
|
1661
|
+
if (output === null) return null;
|
|
1662
|
+
return /^(?:playwright-cli\s+)?(\d+\.\d+\.\d+)$/i.exec(output.trim())?.[1] ?? null;
|
|
1663
|
+
}
|
|
1664
|
+
function resolvePlaywrightCliState({ binPath, versionOutput }) {
|
|
1665
|
+
if (binPath === null) return { status: "absent", binPath: null, detectedVersion: null };
|
|
1666
|
+
const detectedVersion = parsePlaywrightCliVersion(versionOutput);
|
|
1667
|
+
if (detectedVersion === null) return { status: "broken", binPath, detectedVersion: null };
|
|
1668
|
+
return {
|
|
1669
|
+
status: detectedVersion === PLAYWRIGHT_CLI.version ? "current" : "outdated",
|
|
1670
|
+
binPath,
|
|
1671
|
+
detectedVersion
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
function detectPlaywrightCli() {
|
|
1675
|
+
const binPath = lookPath(PLAYWRIGHT_CLI.bin);
|
|
1676
|
+
return resolvePlaywrightCliState({
|
|
1677
|
+
binPath,
|
|
1678
|
+
versionOutput: binPath ? runDetectedBin(binPath, ["--version"], 5e3) : null
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
function resolvePnpmBin() {
|
|
1682
|
+
const pnpm = lookPath("pnpm");
|
|
1683
|
+
if (pnpm !== null && !pnpm.toLowerCase().endsWith(".ps1")) return pnpm;
|
|
1684
|
+
const pnpmCmd = lookPath("pnpm.cmd");
|
|
1685
|
+
return pnpmCmd !== null && !pnpmCmd.toLowerCase().endsWith(".ps1") ? pnpmCmd : null;
|
|
1686
|
+
}
|
|
1687
|
+
function isPlaywrightBrowserReady(env = process.env, platform = process.platform, homeDir = os2.homedir()) {
|
|
1688
|
+
const configuredPath = env.PLAYWRIGHT_BROWSERS_PATH;
|
|
1689
|
+
const cacheDir = configuredPath ?? (platform === "win32" ? path19.join(env.LOCALAPPDATA ?? path19.join(homeDir, "AppData", "Local"), "ms-playwright") : platform === "darwin" ? path19.join(homeDir, "Library", "Caches", "ms-playwright") : path19.join(env.XDG_CACHE_HOME ?? path19.join(homeDir, ".cache"), "ms-playwright"));
|
|
1690
|
+
if (configuredPath === "0") return { status: "missing", path: cacheDir, errorCode: "DISABLED" };
|
|
1691
|
+
try {
|
|
1692
|
+
const ready = fs13.readdirSync(cacheDir, { withFileTypes: true }).some(
|
|
1693
|
+
(entry) => entry.isDirectory() && /^chromium(?:_headless_shell)?-/.test(entry.name)
|
|
1694
|
+
);
|
|
1695
|
+
return ready ? { status: "ready", path: cacheDir } : { status: "missing", path: cacheDir };
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
const errorCode = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
1698
|
+
return errorCode === "ENOENT" ? { status: "missing", path: cacheDir, errorCode } : { status: "unreadable", path: cacheDir, errorCode };
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function planPlaywrightCliCommand(action, pnpmBin) {
|
|
1702
|
+
const pinnedPackage = `${PLAYWRIGHT_CLI.packageName}@${PLAYWRIGHT_CLI.version}`;
|
|
1703
|
+
switch (action) {
|
|
1704
|
+
case "install":
|
|
1705
|
+
case "update":
|
|
1706
|
+
return { command: pnpmBin, args: ["add", "--global", pinnedPackage] };
|
|
1707
|
+
case "remove":
|
|
1708
|
+
return { command: pnpmBin, args: ["remove", "--global", PLAYWRIGHT_CLI.packageName] };
|
|
1709
|
+
case "install-browser":
|
|
1710
|
+
return { command: pnpmBin, args: ["dlx", pinnedPackage, PLAYWRIGHT_CLI.browserInstallAction] };
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
function executePlaywrightToolAction(action, pnpmBin = resolvePnpmBin()) {
|
|
1714
|
+
if (pnpmBin === null) return false;
|
|
1715
|
+
const command = planPlaywrightCliCommand(action, pnpmBin);
|
|
1716
|
+
const invocation = planDetectedBinCommand(command.command, command.args);
|
|
1717
|
+
if (invocation === null) return false;
|
|
1718
|
+
try {
|
|
1719
|
+
execFileSync2(invocation.command, invocation.args, { stdio: "inherit" });
|
|
1720
|
+
return true;
|
|
1721
|
+
} catch {
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/lib/tool-preferences.ts
|
|
1727
|
+
import fs14 from "fs";
|
|
1728
|
+
import path20 from "path";
|
|
1729
|
+
var PLAYWRIGHT_CLI_PREFERENCE_VERSION = 1;
|
|
1730
|
+
var DEVTOOLS_MCP_PREFERENCE_VERSION = 1;
|
|
1731
|
+
function readPreference(file) {
|
|
1732
|
+
try {
|
|
1733
|
+
return { raw: fs14.readFileSync(file, "utf8"), errorCode: null };
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
1736
|
+
return code === "ENOENT" ? { raw: null, errorCode: null } : { raw: null, errorCode: code };
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function playwrightCliPreferenceFile(stateDir = dataDir()) {
|
|
1740
|
+
return path20.join(stateDir, "playwright-cli.json");
|
|
1741
|
+
}
|
|
1742
|
+
function parsePlaywrightCliPreference(raw) {
|
|
1743
|
+
try {
|
|
1744
|
+
const value = JSON.parse(raw);
|
|
1745
|
+
if (value.version !== PLAYWRIGHT_CLI_PREFERENCE_VERSION || typeof value.enabled !== "boolean") return void 0;
|
|
1746
|
+
return value.enabled;
|
|
1747
|
+
} catch {
|
|
1748
|
+
return void 0;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function playwrightCliPreferenceError(file = playwrightCliPreferenceFile()) {
|
|
1752
|
+
const { raw, errorCode } = readPreference(file);
|
|
1753
|
+
if (errorCode !== null) {
|
|
1754
|
+
return `Playwright CLI: no se pudo leer la preferencia en ${file} (${errorCode}). Corrige o borra ese archivo antes de reintentar.`;
|
|
1755
|
+
}
|
|
1756
|
+
if (raw === null || parsePlaywrightCliPreference(raw) !== void 0) return null;
|
|
1757
|
+
return `Playwright CLI: preferencia inv\xE1lida en ${file}. Corrige o borra ese archivo antes de reintentar.`;
|
|
1758
|
+
}
|
|
1759
|
+
function loadPlaywrightCliPreference(file = playwrightCliPreferenceFile()) {
|
|
1760
|
+
const { raw } = readPreference(file);
|
|
1761
|
+
if (raw === null) return void 0;
|
|
1762
|
+
return parsePlaywrightCliPreference(raw);
|
|
1763
|
+
}
|
|
1764
|
+
function savePlaywrightCliPreference(file, enabled) {
|
|
1765
|
+
const error = playwrightCliPreferenceError(file);
|
|
1766
|
+
if (error !== null) throw new Error(error);
|
|
1767
|
+
writeText(file, JSON.stringify({ version: PLAYWRIGHT_CLI_PREFERENCE_VERSION, enabled }) + "\n");
|
|
1768
|
+
}
|
|
1769
|
+
function devtoolsMcpPreferenceFile(stateDir = dataDir()) {
|
|
1770
|
+
return path20.join(stateDir, "devtools-mcp.json");
|
|
1771
|
+
}
|
|
1772
|
+
function isRecord(value) {
|
|
1773
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1774
|
+
}
|
|
1775
|
+
function isRuntimeId(value) {
|
|
1776
|
+
return value === "claude-code" || value === "codex" || value === "opencode";
|
|
1777
|
+
}
|
|
1778
|
+
function parseDevtoolsMcpState(raw) {
|
|
1779
|
+
try {
|
|
1780
|
+
const value = JSON.parse(raw);
|
|
1781
|
+
if (!isRecord(value) || value.version !== DEVTOOLS_MCP_PREFERENCE_VERSION || !isRecord(value.enabled) || !isRecord(value.owned)) return null;
|
|
1782
|
+
const enabled = {};
|
|
1783
|
+
for (const [runtime, selected] of Object.entries(value.enabled)) {
|
|
1784
|
+
if (!isRuntimeId(runtime) || typeof selected !== "boolean") return null;
|
|
1785
|
+
enabled[runtime] = selected;
|
|
1786
|
+
}
|
|
1787
|
+
const owned = {};
|
|
1788
|
+
for (const [runtime, servers] of Object.entries(value.owned)) {
|
|
1789
|
+
if (!isRuntimeId(runtime) || !isRecord(servers)) return null;
|
|
1790
|
+
const managed = {};
|
|
1791
|
+
for (const [server, marked] of Object.entries(servers)) {
|
|
1792
|
+
if (marked !== true) return null;
|
|
1793
|
+
managed[server] = true;
|
|
1794
|
+
}
|
|
1795
|
+
if (Object.keys(managed).length > 0) owned[runtime] = managed;
|
|
1796
|
+
}
|
|
1797
|
+
return { version: DEVTOOLS_MCP_PREFERENCE_VERSION, enabled, owned };
|
|
1798
|
+
} catch {
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function loadDevtoolsMcpState(file) {
|
|
1803
|
+
const empty = { version: DEVTOOLS_MCP_PREFERENCE_VERSION, enabled: {}, owned: {} };
|
|
1804
|
+
const { raw } = readPreference(file);
|
|
1805
|
+
if (raw === null) return empty;
|
|
1806
|
+
return parseDevtoolsMcpState(raw) ?? empty;
|
|
1807
|
+
}
|
|
1808
|
+
function devtoolsMcpPreferenceError(file = devtoolsMcpPreferenceFile()) {
|
|
1809
|
+
const { raw, errorCode } = readPreference(file);
|
|
1810
|
+
if (errorCode !== null) {
|
|
1811
|
+
return `Chrome DevTools MCP: no se pudo leer la preferencia en ${file} (${errorCode}). Corrige o borra ese archivo antes de reintentar.`;
|
|
1812
|
+
}
|
|
1813
|
+
if (raw === null || parseDevtoolsMcpState(raw) !== null) return null;
|
|
1814
|
+
return `Chrome DevTools MCP: preferencia inv\xE1lida en ${file}. Corrige o borra ese archivo antes de reintentar.`;
|
|
1815
|
+
}
|
|
1816
|
+
function saveDevtoolsMcpState(file, state) {
|
|
1817
|
+
const error = devtoolsMcpPreferenceError(file);
|
|
1818
|
+
if (error !== null) throw new Error(error);
|
|
1819
|
+
writeText(file, JSON.stringify(state) + "\n");
|
|
1820
|
+
}
|
|
1821
|
+
function loadDevtoolsMcpPreference(file, runtime) {
|
|
1822
|
+
return loadDevtoolsMcpState(file).enabled[runtime] === true;
|
|
1823
|
+
}
|
|
1824
|
+
function saveDevtoolsMcpPreference(file, runtime, enabled) {
|
|
1825
|
+
const state = loadDevtoolsMcpState(file);
|
|
1826
|
+
state.enabled[runtime] = enabled;
|
|
1827
|
+
saveDevtoolsMcpState(file, state);
|
|
1828
|
+
}
|
|
1829
|
+
function loadDevtoolsMcpOwnership(file, runtime, server) {
|
|
1830
|
+
return loadDevtoolsMcpState(file).owned[runtime]?.[server] === true;
|
|
1831
|
+
}
|
|
1832
|
+
function saveDevtoolsMcpOwnership(file, runtime, server, owned) {
|
|
1833
|
+
const state = loadDevtoolsMcpState(file);
|
|
1834
|
+
if (owned) {
|
|
1835
|
+
(state.owned[runtime] ??= {})[server] = true;
|
|
1836
|
+
} else {
|
|
1837
|
+
delete state.owned[runtime]?.[server];
|
|
1838
|
+
if (state.owned[runtime] !== void 0 && Object.keys(state.owned[runtime]).length === 0) delete state.owned[runtime];
|
|
1839
|
+
}
|
|
1840
|
+
saveDevtoolsMcpState(file, state);
|
|
1841
|
+
}
|
|
1842
|
+
function browserPreferenceErrors(stateDir = dataDir()) {
|
|
1843
|
+
return [
|
|
1844
|
+
playwrightCliPreferenceError(playwrightCliPreferenceFile(stateDir)),
|
|
1845
|
+
devtoolsMcpPreferenceError(devtoolsMcpPreferenceFile(stateDir))
|
|
1846
|
+
].filter((error) => error !== null);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1520
1849
|
// src/install.ts
|
|
1521
1850
|
var ADAPTERS = {
|
|
1522
1851
|
opencode: opencodeAdapter,
|
|
1523
1852
|
"claude-code": claudeCodeAdapter,
|
|
1524
1853
|
codex: codexAdapter
|
|
1525
1854
|
};
|
|
1526
|
-
function
|
|
1855
|
+
function executePlaywrightToolAction2(action) {
|
|
1856
|
+
return executePlaywrightToolAction(action, resolvePnpmBin());
|
|
1857
|
+
}
|
|
1858
|
+
function resolvePlaywrightToolPlan(consent) {
|
|
1859
|
+
if (consent.command !== "install" || consent.targetDir) return { actions: [] };
|
|
1860
|
+
const approved = consent.interactive ? consent.yes ? consent.explicitToolSelection : consent.confirmed : consent.yes && consent.explicitToolSelection;
|
|
1861
|
+
return approved ? { actions: ["install", "install-browser"], persistEnabledOnSuccess: true } : { actions: [] };
|
|
1862
|
+
}
|
|
1863
|
+
async function runPlaywrightToolPlan(plan, deps) {
|
|
1864
|
+
for (const action of plan.actions) {
|
|
1865
|
+
try {
|
|
1866
|
+
if (!await deps.run(action)) return { ok: false, failedAction: action };
|
|
1867
|
+
} catch {
|
|
1868
|
+
return { ok: false, failedAction: action };
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
if (plan.persistEnabledOnSuccess) {
|
|
1872
|
+
try {
|
|
1873
|
+
deps.persistEnabled(true);
|
|
1874
|
+
} catch {
|
|
1875
|
+
return { ok: false, failedAction: "persist" };
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return { ok: true };
|
|
1879
|
+
}
|
|
1880
|
+
function enabledMcpServers(runtime, explicitDevtoolsEnabled, useBrowserPreferences = true) {
|
|
1881
|
+
const devtoolsEnabled = explicitDevtoolsEnabled ?? (useBrowserPreferences && loadDevtoolsMcpPreference(devtoolsMcpPreferenceFile(), runtime));
|
|
1882
|
+
return devtoolsEnabled ? /* @__PURE__ */ new Set([DEVTOOLS_MCP_SERVER]) : /* @__PURE__ */ new Set();
|
|
1883
|
+
}
|
|
1884
|
+
function ownedMcpServers(runtime, useBrowserPreferences = true) {
|
|
1885
|
+
if (!useBrowserPreferences) return /* @__PURE__ */ new Set();
|
|
1886
|
+
const file = devtoolsMcpPreferenceFile();
|
|
1887
|
+
return loadDevtoolsMcpOwnership(file, runtime, DEVTOOLS_MCP_SERVER) ? /* @__PURE__ */ new Set([DEVTOOLS_MCP_SERVER]) : /* @__PURE__ */ new Set();
|
|
1888
|
+
}
|
|
1889
|
+
function persistMcpOwnershipChanges(runtime, plan) {
|
|
1890
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1891
|
+
for (const action of plan) {
|
|
1892
|
+
if (action.kind !== "write") continue;
|
|
1893
|
+
for (const change of action.mcpOwnership ?? []) latest.set(change.server, change.owned);
|
|
1894
|
+
}
|
|
1895
|
+
const file = devtoolsMcpPreferenceFile();
|
|
1896
|
+
for (const [server, owned] of latest) saveDevtoolsMcpOwnership(file, runtime, server, owned);
|
|
1897
|
+
}
|
|
1898
|
+
function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE, useBrowserPreferences = true) {
|
|
1527
1899
|
const models = loadModelMap()[adapter.id];
|
|
1528
1900
|
if (!models) return null;
|
|
1529
1901
|
return {
|
|
@@ -1533,7 +1905,10 @@ function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE)
|
|
|
1533
1905
|
subagentConcurrency: mode.subagentConcurrency,
|
|
1534
1906
|
engramBin: detectEngram(),
|
|
1535
1907
|
models,
|
|
1536
|
-
warnings: []
|
|
1908
|
+
warnings: [],
|
|
1909
|
+
enabledMcpServers: enabledMcpServers(adapter.id, void 0, useBrowserPreferences),
|
|
1910
|
+
playwrightCliEnabled: useBrowserPreferences && loadPlaywrightCliPreference() === true,
|
|
1911
|
+
ownedMcpServers: ownedMcpServers(adapter.id, useBrowserPreferences)
|
|
1537
1912
|
};
|
|
1538
1913
|
}
|
|
1539
1914
|
function buildPlan(adapter, ctx) {
|
|
@@ -1554,14 +1929,16 @@ function diffPlan(plan) {
|
|
|
1554
1929
|
if (current === null) return { action, status: "create" };
|
|
1555
1930
|
return { action, status: current === action.content ? "unchanged" : "update" };
|
|
1556
1931
|
}
|
|
1557
|
-
if (!
|
|
1932
|
+
if (!fs15.existsSync(action.target)) return { action, status: "create" };
|
|
1558
1933
|
return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
|
|
1559
1934
|
});
|
|
1560
1935
|
}
|
|
1561
|
-
function applyChanges(changes) {
|
|
1936
|
+
function applyChanges(changes, onMcpOwnershipWritten) {
|
|
1562
1937
|
for (const { action } of changes) {
|
|
1563
|
-
if (action.kind === "write")
|
|
1564
|
-
|
|
1938
|
+
if (action.kind === "write") {
|
|
1939
|
+
writeText(action.target, action.content);
|
|
1940
|
+
if (action.mcpOwnership !== void 0) onMcpOwnershipWritten?.(action);
|
|
1941
|
+
} else copyFile(action.source, action.target);
|
|
1565
1942
|
}
|
|
1566
1943
|
}
|
|
1567
1944
|
function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
|
|
@@ -1578,7 +1955,7 @@ function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
|
|
|
1578
1955
|
continue;
|
|
1579
1956
|
}
|
|
1580
1957
|
try {
|
|
1581
|
-
for (const action of buildPlan(adapter, ctx)) targets.add(
|
|
1958
|
+
for (const action of buildPlan(adapter, ctx)) targets.add(path21.resolve(action.target));
|
|
1582
1959
|
} catch (error) {
|
|
1583
1960
|
complete = false;
|
|
1584
1961
|
warnings.push(
|
|
@@ -1594,6 +1971,17 @@ async function runInstall(opts) {
|
|
|
1594
1971
|
const engramBin = detectEngram();
|
|
1595
1972
|
const modePreference = opts.mode === void 0 ? opts.targetDir === void 0 ? loadInstallModePreference() : DEFAULT_INSTALL_MODE_PREFERENCE : normalizeInstallModePreference(opts.mode);
|
|
1596
1973
|
const useManifest = opts.targetDir === void 0;
|
|
1974
|
+
const preferenceErrors = useManifest ? browserPreferenceErrors() : [];
|
|
1975
|
+
if (preferenceErrors.length > 0) {
|
|
1976
|
+
for (const error of preferenceErrors) p.log.error(error);
|
|
1977
|
+
p.outro("Install cancelado: corrige las preferencias de navegador antes de reintentar.");
|
|
1978
|
+
return 1;
|
|
1979
|
+
}
|
|
1980
|
+
const toolPlan = opts.playwrightToolConsent === void 0 ? null : resolvePlaywrightToolPlan({
|
|
1981
|
+
...opts.playwrightToolConsent,
|
|
1982
|
+
targetDir: opts.targetDir !== void 0 || opts.playwrightToolConsent.targetDir
|
|
1983
|
+
});
|
|
1984
|
+
const projectPlaywrightPrompt = opts.dryRun && toolPlan?.persistEnabledOnSuccess === true;
|
|
1597
1985
|
const modelMap = loadModelMap();
|
|
1598
1986
|
if (useManifest) ensureModelMapFile();
|
|
1599
1987
|
p.log.info(engramBin ? `Engram detectado: ${engramBin} (se respeta, D7)` : "Engram NO detectado.");
|
|
@@ -1607,6 +1995,7 @@ async function runInstall(opts) {
|
|
|
1607
1995
|
}
|
|
1608
1996
|
let exitCode = 0;
|
|
1609
1997
|
let successfulRuns = 0;
|
|
1998
|
+
const successfulContexts = [];
|
|
1610
1999
|
for (const id of opts.runtimes) {
|
|
1611
2000
|
const adapter = ADAPTERS[id];
|
|
1612
2001
|
if (!adapter) {
|
|
@@ -1632,7 +2021,16 @@ async function runInstall(opts) {
|
|
|
1632
2021
|
subagentConcurrency: modePreference.subagentConcurrency,
|
|
1633
2022
|
engramBin,
|
|
1634
2023
|
models,
|
|
1635
|
-
warnings: []
|
|
2024
|
+
warnings: [],
|
|
2025
|
+
enabledMcpServers: enabledMcpServers(id, opts.devtoolsMcpSelection?.[id], useManifest),
|
|
2026
|
+
playwrightCliEnabled: projectPlaywrightPrompt || useManifest && loadPlaywrightCliPreference() === true,
|
|
2027
|
+
ownedMcpServers: ownedMcpServers(id, useManifest)
|
|
2028
|
+
};
|
|
2029
|
+
const persistDevtoolsSelection = () => {
|
|
2030
|
+
const selection = opts.devtoolsMcpSelection?.[id];
|
|
2031
|
+
if (useManifest && selection !== void 0) {
|
|
2032
|
+
saveDevtoolsMcpPreference(devtoolsMcpPreferenceFile(), id, selection);
|
|
2033
|
+
}
|
|
1636
2034
|
};
|
|
1637
2035
|
let plan = buildPlan(adapter, ctx);
|
|
1638
2036
|
let diff = diffPlan(plan);
|
|
@@ -1648,24 +2046,30 @@ async function runInstall(opts) {
|
|
|
1648
2046
|
if (orphans.length > 0) p.log.info(`${orphans.length} hu\xE9rfanos de versiones previas a eliminar`);
|
|
1649
2047
|
for (const w of ctx.warnings) p.log.warn(w);
|
|
1650
2048
|
if (opts.dryRun) {
|
|
1651
|
-
|
|
1652
|
-
|
|
2049
|
+
const preview = changes.slice(0, 40);
|
|
2050
|
+
const projectedPrompt = projectPlaywrightPrompt ? changes.find((change) => change.action.target === adapter.paths(configDir).systemPromptFile) : void 0;
|
|
2051
|
+
if (projectedPrompt && !preview.includes(projectedPrompt)) preview.push(projectedPrompt);
|
|
2052
|
+
for (const c of preview) p.log.message(` ${c.status === "create" ? "+" : "~"} ${c.action.target}`);
|
|
2053
|
+
if (changes.length > preview.length) p.log.message(` \u2026 y ${changes.length - preview.length} m\xE1s`);
|
|
1653
2054
|
for (const o of orphans) p.log.message(` - ${o}`);
|
|
1654
2055
|
continue;
|
|
1655
2056
|
}
|
|
1656
2057
|
const writeManifest = () => {
|
|
1657
2058
|
if (!useManifest) return;
|
|
1658
|
-
const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) =>
|
|
2059
|
+
const unmergeTargets = new Set(adapter.planUnmerge(canonicalMcp, canonicalHooks, ctx).map((a) => path21.resolve(a.target)));
|
|
1659
2060
|
const keepTarget = (target) => !unmergeTargets.has(target);
|
|
1660
|
-
const liveOwned = plan.map((a) =>
|
|
1661
|
-
const previousOwned = (prevManifest?.owned ?? []).map((target) =>
|
|
2061
|
+
const liveOwned = plan.map((a) => path21.resolve(a.target)).filter(keepTarget);
|
|
2062
|
+
const previousOwned = (prevManifest?.owned ?? []).map((target) => path21.resolve(target)).filter(keepTarget);
|
|
1662
2063
|
const owned = canOrphan ? liveOwned : [.../* @__PURE__ */ new Set([...previousOwned, ...liveOwned])];
|
|
1663
2064
|
writeRuntimeManifest(id, { configDir, owned, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1664
2065
|
};
|
|
1665
2066
|
if (changes.length === 0 && orphans.length === 0) {
|
|
1666
2067
|
writeManifest();
|
|
2068
|
+
if (useManifest) persistMcpOwnershipChanges(id, plan);
|
|
2069
|
+
persistDevtoolsSelection();
|
|
1667
2070
|
p.log.success(`${adapter.name}: ya al d\xEDa (idempotente).`);
|
|
1668
2071
|
successfulRuns++;
|
|
2072
|
+
successfulContexts.push({ adapter, ctx });
|
|
1669
2073
|
continue;
|
|
1670
2074
|
}
|
|
1671
2075
|
if (!opts.yes && process.stdout.isTTY) {
|
|
@@ -1683,10 +2087,10 @@ async function runInstall(opts) {
|
|
|
1683
2087
|
}
|
|
1684
2088
|
const backup = useManifest ? createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`) : null;
|
|
1685
2089
|
if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
1686
|
-
applyChanges(changes);
|
|
1687
|
-
const pruneRoot = useManifest ? HOME :
|
|
2090
|
+
applyChanges(changes, useManifest ? (action) => persistMcpOwnershipChanges(id, [action]) : void 0);
|
|
2091
|
+
const pruneRoot = useManifest ? HOME : path21.dirname(configDir);
|
|
1688
2092
|
for (const orphan of orphans) {
|
|
1689
|
-
|
|
2093
|
+
fs15.rmSync(orphan, { force: true });
|
|
1690
2094
|
pruneEmptyDirs(orphan, pruneRoot);
|
|
1691
2095
|
}
|
|
1692
2096
|
const verifyCtx = { ...ctx, warnings: [] };
|
|
@@ -1697,26 +2101,98 @@ async function runInstall(opts) {
|
|
|
1697
2101
|
exitCode = 1;
|
|
1698
2102
|
} else {
|
|
1699
2103
|
writeManifest();
|
|
2104
|
+
if (useManifest) persistMcpOwnershipChanges(id, plan);
|
|
2105
|
+
persistDevtoolsSelection();
|
|
1700
2106
|
p.log.success(`${adapter.name}: ${changes.length} archivos aplicados y verificados (idempotente).`);
|
|
1701
2107
|
successfulRuns++;
|
|
2108
|
+
successfulContexts.push({ adapter, ctx });
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
if (toolPlan?.actions.length) {
|
|
2112
|
+
if (opts.dryRun) {
|
|
2113
|
+
p.log.info("Playwright CLI: instalaci\xF3n global y navegador previstos (dry-run; no se ejecutan).");
|
|
2114
|
+
} else if (exitCode === 0) {
|
|
2115
|
+
const result = await runPlaywrightToolPlan(toolPlan, opts.playwrightToolDeps ?? {
|
|
2116
|
+
run: async (action) => executePlaywrightToolAction2(action),
|
|
2117
|
+
persistEnabled: (enabled) => savePlaywrightCliPreference(playwrightCliPreferenceFile(), enabled)
|
|
2118
|
+
});
|
|
2119
|
+
if (!result.ok) {
|
|
2120
|
+
const reason = result.failedAction === "install" ? "no se pudo instalar el paquete global" : result.failedAction === "install-browser" ? "no se pudo descargar el navegador" : "se instalaron los componentes, pero no se pudo guardar la preferencia";
|
|
2121
|
+
p.log.error(`Playwright CLI: ${reason}; la preferencia no se ha marcado como habilitada. Ejecuta 'jorgex-stack install --playwright' para reintentar.`);
|
|
2122
|
+
exitCode = 1;
|
|
2123
|
+
} else {
|
|
2124
|
+
let promptReconciliationFailed = false;
|
|
2125
|
+
for (const { adapter, ctx } of successfulContexts) {
|
|
2126
|
+
const browserCtx = { ...ctx, playwrightCliEnabled: true, warnings: [] };
|
|
2127
|
+
try {
|
|
2128
|
+
const browserChanges = diffPlan(planSystemPrompt(adapter, browserCtx)).filter((change) => change.status !== "unchanged");
|
|
2129
|
+
if (browserChanges.length === 0) continue;
|
|
2130
|
+
const browserUpdates = browserChanges.filter((change) => change.status === "update");
|
|
2131
|
+
const backup = useManifest ? createBackup(browserUpdates.map((change) => change.action.target), `install-browser-${adapter.id}`) : null;
|
|
2132
|
+
if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
2133
|
+
applyChanges(browserChanges);
|
|
2134
|
+
const dirty = diffPlan(planSystemPrompt(adapter, { ...browserCtx, warnings: [] })).filter((change) => change.status !== "unchanged");
|
|
2135
|
+
if (dirty.length > 0) {
|
|
2136
|
+
p.log.error(`${adapter.name}: verificaci\xF3n de la gu\xEDa de navegador FALL\xD3 (${dirty.length} acciones inestables).`);
|
|
2137
|
+
promptReconciliationFailed = true;
|
|
2138
|
+
}
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
p.log.error(`${adapter.name}: no se pudo actualizar la gu\xEDa de navegador (${error instanceof Error ? error.message : String(error)}).`);
|
|
2141
|
+
exitCode = 1;
|
|
2142
|
+
promptReconciliationFailed = true;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (promptReconciliationFailed) {
|
|
2146
|
+
exitCode = 1;
|
|
2147
|
+
p.log.error("Playwright CLI y navegador se han instalado y la preferencia est\xE1 activada, pero la gu\xEDa de navegador qued\xF3 en estado parcial. Ejecuta 'jorgex-stack sync' para repararla.");
|
|
2148
|
+
} else {
|
|
2149
|
+
p.log.success("Playwright CLI y navegador instalados.");
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
} else if (useManifest && opts.playwrightToolConsent?.command === "sync" && loadPlaywrightCliPreference() === true) {
|
|
2154
|
+
const cli = detectPlaywrightCli();
|
|
2155
|
+
if (cli.status !== "current") {
|
|
2156
|
+
p.log.warn("Playwright CLI sigue habilitado, pero el paquete no est\xE1 listo; sync no instala herramientas. Ejecuta 'jorgex-stack install --playwright'.");
|
|
2157
|
+
} else {
|
|
2158
|
+
const browserCache = isPlaywrightBrowserReady();
|
|
2159
|
+
if (browserCache.status === "unreadable") {
|
|
2160
|
+
p.log.warn(`Playwright CLI sigue habilitado, pero no se puede leer la cach\xE9 de navegadores en ${browserCache.path} (${browserCache.errorCode}). Revisa permisos o ejecuta 'jorgex-stack install --playwright'.`);
|
|
2161
|
+
} else if (browserCache.status === "missing") {
|
|
2162
|
+
p.log.warn("Playwright CLI sigue habilitado, pero falta el navegador; sync no descarga navegadores. Ejecuta 'jorgex-stack install --playwright'.");
|
|
2163
|
+
}
|
|
1702
2164
|
}
|
|
1703
2165
|
}
|
|
1704
2166
|
if (useManifest && !opts.dryRun && exitCode === 0 && successfulRuns > 0) {
|
|
1705
2167
|
saveInstallModePreference(installModePreferenceFile(), modePreference);
|
|
1706
2168
|
}
|
|
1707
|
-
p.outro(opts.dryRun ? "Dry-run: no se ha escrito nada." : "Hecho.");
|
|
2169
|
+
p.outro(opts.dryRun ? exitCode === 0 ? "Dry-run: no se ha escrito nada." : "Dry-run completado con errores (revisa arriba)." : exitCode === 0 ? "Hecho." : "Install completado con errores (revisa arriba).");
|
|
1708
2170
|
return exitCode;
|
|
1709
2171
|
}
|
|
1710
2172
|
|
|
1711
2173
|
// src/uninstall.ts
|
|
1712
|
-
import
|
|
1713
|
-
import
|
|
2174
|
+
import fs16 from "fs";
|
|
2175
|
+
import path22 from "path";
|
|
1714
2176
|
import * as p2 from "@clack/prompts";
|
|
2177
|
+
function resolvePlaywrightUninstallPlan(input) {
|
|
2178
|
+
return {
|
|
2179
|
+
actions: input.removePackage ? ["remove"] : [],
|
|
2180
|
+
preserveBrowserData: true
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
1715
2183
|
async function runUninstall(opts) {
|
|
1716
2184
|
p2.intro(`jorgex-stack ${opts.dryRun ? "uninstall (dry-run)" : "uninstall"}`);
|
|
2185
|
+
const useBrowserPreferences = opts.targetDir === void 0;
|
|
2186
|
+
const preferenceErrors = useBrowserPreferences ? browserPreferenceErrors() : [];
|
|
2187
|
+
if (preferenceErrors.length > 0) {
|
|
2188
|
+
for (const error of preferenceErrors) p2.log.error(error);
|
|
2189
|
+
p2.outro("Uninstall cancelado: corrige las preferencias de navegador antes de reintentar.");
|
|
2190
|
+
return 1;
|
|
2191
|
+
}
|
|
1717
2192
|
const stackDir = stackRoot();
|
|
1718
2193
|
const mcp = loadCanonicalMcp(stackDir);
|
|
1719
2194
|
const hooks = loadCanonicalHooks(stackDir);
|
|
2195
|
+
let exitCode = 0;
|
|
1720
2196
|
let removeEngram = opts.removeEngram;
|
|
1721
2197
|
if (!removeEngram && !opts.yes && !opts.dryRun && process.stdout.isTTY) {
|
|
1722
2198
|
const answer = await p2.confirm({
|
|
@@ -1734,13 +2210,15 @@ async function runUninstall(opts) {
|
|
|
1734
2210
|
p2.log.info("Engram se conserva: memorias, binario y registro intactos (usa --remove-engram para desregistrarlo).");
|
|
1735
2211
|
}
|
|
1736
2212
|
const retained = /* @__PURE__ */ new Set();
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2213
|
+
if (useBrowserPreferences) {
|
|
2214
|
+
for (const keep of Object.values(ADAPTERS)) {
|
|
2215
|
+
if (opts.runtimes.includes(keep.id)) continue;
|
|
2216
|
+
const detection = keep.detect();
|
|
2217
|
+
if (!detection.installed) continue;
|
|
2218
|
+
const keepCtx = makeContext(keep, detection.configDir);
|
|
2219
|
+
if (!keepCtx) continue;
|
|
2220
|
+
for (const action of buildPlan(keep, keepCtx)) retained.add(path22.resolve(action.target));
|
|
2221
|
+
}
|
|
1744
2222
|
}
|
|
1745
2223
|
for (const id of opts.runtimes) {
|
|
1746
2224
|
const adapter = ADAPTERS[id];
|
|
@@ -1754,60 +2232,106 @@ async function runUninstall(opts) {
|
|
|
1754
2232
|
p2.log.warn(`${adapter.name} no detectado \u2014 omitido.`);
|
|
1755
2233
|
continue;
|
|
1756
2234
|
}
|
|
1757
|
-
const ctx = makeContext(adapter, configDir);
|
|
2235
|
+
const ctx = makeContext(adapter, configDir, void 0, useBrowserPreferences);
|
|
1758
2236
|
if (!ctx) continue;
|
|
1759
2237
|
ctx.preserveEngram = !removeEngram;
|
|
1760
2238
|
const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
|
|
1761
|
-
const mergedTargets = new Set(unmerge.map((a) =>
|
|
2239
|
+
const mergedTargets = new Set(unmerge.map((a) => path22.resolve(a.target)));
|
|
1762
2240
|
const usingRealConfig = opts.targetDir === void 0;
|
|
1763
2241
|
const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
|
|
1764
|
-
const pruneRoot = usingRealConfig ? HOME :
|
|
2242
|
+
const pruneRoot = usingRealConfig ? HOME : path22.dirname(configDir);
|
|
1765
2243
|
const planTargets = [
|
|
1766
|
-
.../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) =>
|
|
1767
|
-
].filter((t) => !mergedTargets.has(t) &&
|
|
2244
|
+
.../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path22.resolve(a.target)), ...prevOwned.map((t) => path22.resolve(t))])
|
|
2245
|
+
].filter((t) => !mergedTargets.has(t) && fs16.existsSync(t));
|
|
1768
2246
|
const deleteTargets = planTargets.filter(
|
|
1769
|
-
(t) => !retained.has(t) && !(ctx.preserveEngram &&
|
|
2247
|
+
(t) => !retained.has(t) && !(ctx.preserveEngram && path22.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
|
|
1770
2248
|
);
|
|
1771
2249
|
const sharedKept = planTargets.length - deleteTargets.length;
|
|
1772
2250
|
p2.log.step(`${adapter.name} \u2192 ${configDir}`);
|
|
1773
2251
|
p2.log.info(`${deleteTargets.length} archivos a borrar, ${unmerge.length} archivos compartidos a limpiar`);
|
|
1774
2252
|
if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
|
|
1775
2253
|
if (opts.dryRun) continue;
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2254
|
+
let backup;
|
|
2255
|
+
try {
|
|
2256
|
+
backup = createBackup(
|
|
2257
|
+
[...deleteTargets, ...unmerge.map((a) => a.target).filter((t) => fs16.existsSync(t))],
|
|
2258
|
+
`uninstall-${id}`
|
|
2259
|
+
);
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
p2.log.error(`${adapter.name}: no se pudo respaldar una configuraci\xF3n ilegible en ${configDir} \u2014 ${error instanceof Error ? error.message : String(error)}.`);
|
|
2262
|
+
exitCode = 1;
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
1780
2265
|
if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
1781
2266
|
for (const target of deleteTargets) {
|
|
1782
|
-
|
|
2267
|
+
fs16.rmSync(target, { force: true });
|
|
1783
2268
|
pruneEmptyDirs(target, pruneRoot);
|
|
1784
2269
|
}
|
|
1785
2270
|
for (const action of unmerge) {
|
|
1786
2271
|
if (action.kind !== "write") continue;
|
|
1787
2272
|
if (action.content.trim() === "") {
|
|
1788
|
-
|
|
2273
|
+
fs16.rmSync(action.target, { force: true });
|
|
1789
2274
|
} else {
|
|
1790
2275
|
writeText(action.target, action.content);
|
|
1791
2276
|
}
|
|
2277
|
+
if (usingRealConfig) {
|
|
2278
|
+
for (const change of action.mcpOwnership ?? []) {
|
|
2279
|
+
saveDevtoolsMcpOwnership(devtoolsMcpPreferenceFile(), id, change.server, change.owned);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
1792
2282
|
}
|
|
1793
2283
|
if (usingRealConfig) removeRuntimeManifest(id);
|
|
1794
2284
|
p2.log.success(`${adapter.name}: stack retirado (lo tuyo queda intacto).`);
|
|
1795
2285
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
2286
|
+
const playwrightPlan = resolvePlaywrightUninstallPlan({
|
|
2287
|
+
removePackage: opts.targetDir === void 0 && opts.removePlaywright
|
|
2288
|
+
});
|
|
2289
|
+
if (opts.removePlaywright && opts.targetDir !== void 0) {
|
|
2290
|
+
p2.log.info("Playwright CLI: --target-dir conserva el paquete global y los datos del navegador.");
|
|
2291
|
+
} else if (playwrightPlan.actions.length > 0) {
|
|
2292
|
+
if (opts.dryRun) {
|
|
2293
|
+
p2.log.info("Playwright CLI: se retirar\xEDa solo el paquete global; los datos y navegadores se conservan.");
|
|
2294
|
+
} else if (executePlaywrightToolAction2("remove")) {
|
|
2295
|
+
try {
|
|
2296
|
+
savePlaywrightCliPreference(playwrightCliPreferenceFile(), false);
|
|
2297
|
+
p2.log.success("Playwright CLI: paquete global retirado; los datos y navegadores se conservan.");
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
p2.log.error(`Playwright CLI: paquete global retirado, pero no se pudo guardar la preferencia (${error instanceof Error ? error.message : String(error)}). Corrige la preferencia antes de reintentar.`);
|
|
2300
|
+
exitCode = 1;
|
|
2301
|
+
}
|
|
2302
|
+
} else {
|
|
2303
|
+
p2.log.error("Playwright CLI: no se pudo retirar el paquete global; los datos y la preferencia se conservan.");
|
|
2304
|
+
exitCode = 1;
|
|
2305
|
+
}
|
|
2306
|
+
} else {
|
|
2307
|
+
p2.log.info("Playwright CLI: paquete global y datos del navegador conservados (usa --remove-playwright para retirar solo el paquete).");
|
|
2308
|
+
}
|
|
2309
|
+
p2.outro(opts.dryRun ? "Dry-run: no se ha tocado nada." : exitCode === 0 ? "Hecho. Usa 'restore' si quieres volver atr\xE1s." : "Uninstall completado con errores (revisa arriba).");
|
|
2310
|
+
return exitCode;
|
|
1798
2311
|
}
|
|
1799
2312
|
|
|
1800
2313
|
// src/doctor.ts
|
|
1801
|
-
import
|
|
1802
|
-
import
|
|
2314
|
+
import path23 from "path";
|
|
2315
|
+
import fs17 from "fs";
|
|
1803
2316
|
import * as p3 from "@clack/prompts";
|
|
1804
2317
|
function engramVersion(bin) {
|
|
1805
2318
|
const out = runDetectedBin(bin, ["--version"], 5e3);
|
|
1806
2319
|
if (out === null) return null;
|
|
1807
2320
|
return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
|
|
1808
2321
|
}
|
|
2322
|
+
function resolvePlaywrightDoctorState(input) {
|
|
2323
|
+
if (input.enabled !== true) return { status: "disabled" };
|
|
2324
|
+
if (input.cli.status === "absent") return { status: "missing", missing: "package" };
|
|
2325
|
+
if (input.cli.status === "broken") return { status: "broken" };
|
|
2326
|
+
if (input.cli.status === "outdated") return { status: "outdated" };
|
|
2327
|
+
if (input.browserCache?.status === "unreadable") {
|
|
2328
|
+
return { status: "unreadable", path: input.browserCache.path, errorCode: input.browserCache.errorCode };
|
|
2329
|
+
}
|
|
2330
|
+
if (!input.browserReady) return { status: "missing", missing: "browser" };
|
|
2331
|
+
return { status: "healthy" };
|
|
2332
|
+
}
|
|
1809
2333
|
function context7KeyConfigured(id, configDir) {
|
|
1810
|
-
const file = id === "codex" ?
|
|
2334
|
+
const file = id === "codex" ? path23.join(configDir, "config.toml") : id === "claude-code" ? path23.join(path23.dirname(configDir), `${path23.basename(configDir)}.json`) : path23.join(configDir, "opencode.json");
|
|
1811
2335
|
const content = readTextIfExists(file);
|
|
1812
2336
|
if (content === null) return null;
|
|
1813
2337
|
const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
|
|
@@ -1830,13 +2354,44 @@ async function runDoctor() {
|
|
|
1830
2354
|
p3.log.success(`Engram: ${version} (${engramBin})`);
|
|
1831
2355
|
}
|
|
1832
2356
|
}
|
|
1833
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
1834
|
-
const engramDb =
|
|
1835
|
-
if (
|
|
1836
|
-
const sizeMb = (
|
|
2357
|
+
const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path23.join(HOME, ".engram");
|
|
2358
|
+
const engramDb = path23.join(engramDataDir, "engram.db");
|
|
2359
|
+
if (fs17.existsSync(engramDb)) {
|
|
2360
|
+
const sizeMb = (fs17.statSync(engramDb).size / 1024 / 1024).toFixed(1);
|
|
1837
2361
|
p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
|
|
1838
2362
|
}
|
|
1839
|
-
if (!
|
|
2363
|
+
if (!fs17.existsSync(modelMapFile())) p3.log.info("model-map: a\xFAn no creado (se crea en el primer install o con 'models').");
|
|
2364
|
+
const preferenceErrors = browserPreferenceErrors();
|
|
2365
|
+
if (preferenceErrors.length > 0) {
|
|
2366
|
+
for (const error of preferenceErrors) p3.log.error(error);
|
|
2367
|
+
problems += preferenceErrors.length;
|
|
2368
|
+
} else {
|
|
2369
|
+
const browserCache = isPlaywrightBrowserReady();
|
|
2370
|
+
const playwright = resolvePlaywrightDoctorState({
|
|
2371
|
+
enabled: loadPlaywrightCliPreference(),
|
|
2372
|
+
cli: detectPlaywrightCli(),
|
|
2373
|
+
browserReady: browserCache.status === "ready",
|
|
2374
|
+
browserCache
|
|
2375
|
+
});
|
|
2376
|
+
if (playwright.status === "disabled") {
|
|
2377
|
+
p3.log.info("Playwright CLI: deshabilitado (opcional). Usa 'install --playwright' para instalarlo de forma expl\xEDcita.");
|
|
2378
|
+
} else if (playwright.status === "healthy") {
|
|
2379
|
+
p3.log.success("Playwright CLI: paquete y navegador listos.");
|
|
2380
|
+
} else if (playwright.status === "missing") {
|
|
2381
|
+
const target = playwright.missing === "package" ? "el paquete global" : "el navegador de Playwright";
|
|
2382
|
+
p3.log.warn(`Playwright CLI: habilitado, pero falta ${target} \u2192 ejecuta 'jorgex-stack install --playwright'.`);
|
|
2383
|
+
problems++;
|
|
2384
|
+
} else if (playwright.status === "broken") {
|
|
2385
|
+
p3.log.error("Playwright CLI: el binario detectado no responde correctamente \u2192 ejecuta 'jorgex-stack install --playwright'.");
|
|
2386
|
+
problems++;
|
|
2387
|
+
} else if (playwright.status === "unreadable") {
|
|
2388
|
+
p3.log.error(`Playwright CLI: no se puede leer la cach\xE9 de navegadores en ${playwright.path} (${playwright.errorCode}) \u2192 revisa permisos o ejecuta 'jorgex-stack install --playwright'.`);
|
|
2389
|
+
problems++;
|
|
2390
|
+
} else {
|
|
2391
|
+
p3.log.warn("Playwright CLI: versi\xF3n distinta del pin aprobado \u2192 ejecuta 'jorgex-stack update' o 'install --playwright'.");
|
|
2392
|
+
problems++;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
1840
2395
|
const manifest = readManifest();
|
|
1841
2396
|
const modePreference = loadInstallModePreference();
|
|
1842
2397
|
const current = collectAllCurrentTargets(modePreference);
|
|
@@ -1875,10 +2430,10 @@ async function runDoctor() {
|
|
|
1875
2430
|
p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
|
|
1876
2431
|
problems++;
|
|
1877
2432
|
}
|
|
1878
|
-
if (adapter.id === "codex" &&
|
|
2433
|
+
if (adapter.id === "codex" && fs17.existsSync(path23.join(detection.configDir, "hooks.json"))) {
|
|
1879
2434
|
p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
|
|
1880
2435
|
}
|
|
1881
|
-
if (adapter.id === "codex" &&
|
|
2436
|
+
if (adapter.id === "codex" && fs17.existsSync(path23.join(detection.configDir, "AGENTS.override.md"))) {
|
|
1882
2437
|
p3.log.warn(
|
|
1883
2438
|
"Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
|
|
1884
2439
|
);
|
|
@@ -1892,17 +2447,17 @@ async function runDoctor() {
|
|
|
1892
2447
|
}
|
|
1893
2448
|
|
|
1894
2449
|
// src/update.ts
|
|
1895
|
-
import
|
|
1896
|
-
import
|
|
1897
|
-
import
|
|
1898
|
-
import { execFileSync as
|
|
2450
|
+
import fs20 from "fs";
|
|
2451
|
+
import path26 from "path";
|
|
2452
|
+
import os4 from "os";
|
|
2453
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
1899
2454
|
import * as p4 from "@clack/prompts";
|
|
1900
2455
|
|
|
1901
2456
|
// src/lib/github.ts
|
|
1902
|
-
import
|
|
1903
|
-
import
|
|
1904
|
-
import { execFileSync as
|
|
1905
|
-
import
|
|
2457
|
+
import fs18 from "fs";
|
|
2458
|
+
import path24 from "path";
|
|
2459
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2460
|
+
import os3 from "os";
|
|
1906
2461
|
import { Readable } from "stream";
|
|
1907
2462
|
import { pipeline } from "stream/promises";
|
|
1908
2463
|
var cachedToken;
|
|
@@ -1970,19 +2525,19 @@ async function latestGithubCommit(repo) {
|
|
|
1970
2525
|
}
|
|
1971
2526
|
}
|
|
1972
2527
|
function validateExtractedTree(destDir) {
|
|
1973
|
-
const resolved =
|
|
2528
|
+
const resolved = path24.resolve(destDir);
|
|
1974
2529
|
const walk = (dir) => {
|
|
1975
2530
|
let entries;
|
|
1976
2531
|
try {
|
|
1977
|
-
entries =
|
|
2532
|
+
entries = fs18.readdirSync(dir, { withFileTypes: true });
|
|
1978
2533
|
} catch {
|
|
1979
2534
|
return false;
|
|
1980
2535
|
}
|
|
1981
2536
|
for (const entry of entries) {
|
|
1982
|
-
const full =
|
|
2537
|
+
const full = path24.join(dir, entry.name);
|
|
1983
2538
|
let stat;
|
|
1984
2539
|
try {
|
|
1985
|
-
stat =
|
|
2540
|
+
stat = fs18.lstatSync(full);
|
|
1986
2541
|
} catch {
|
|
1987
2542
|
return false;
|
|
1988
2543
|
}
|
|
@@ -1998,15 +2553,15 @@ function validateExtractedTree(destDir) {
|
|
|
1998
2553
|
}
|
|
1999
2554
|
function resolveTarBin() {
|
|
2000
2555
|
if (process.platform !== "win32") return "tar";
|
|
2001
|
-
const winTar =
|
|
2002
|
-
return
|
|
2556
|
+
const winTar = path24.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
|
|
2557
|
+
return fs18.existsSync(winTar) ? winTar : "tar";
|
|
2003
2558
|
}
|
|
2004
2559
|
async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
2005
2560
|
const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
|
|
2006
|
-
const tmp =
|
|
2561
|
+
const tmp = path24.join(os3.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
|
|
2007
2562
|
const fail = (reason) => {
|
|
2008
2563
|
try {
|
|
2009
|
-
|
|
2564
|
+
fs18.rmSync(destDir, { recursive: true, force: true });
|
|
2010
2565
|
} catch {
|
|
2011
2566
|
}
|
|
2012
2567
|
return { ok: false, reason };
|
|
@@ -2025,46 +2580,46 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
|
2025
2580
|
if (!res.body) return fail("respuesta HTTP sin cuerpo");
|
|
2026
2581
|
await pipeline(
|
|
2027
2582
|
Readable.fromWeb(res.body),
|
|
2028
|
-
|
|
2583
|
+
fs18.createWriteStream(tmp)
|
|
2029
2584
|
);
|
|
2030
|
-
|
|
2031
|
-
|
|
2585
|
+
fs18.rmSync(destDir, { recursive: true, force: true });
|
|
2586
|
+
fs18.mkdirSync(destDir, { recursive: true });
|
|
2032
2587
|
try {
|
|
2033
|
-
|
|
2588
|
+
execFileSync3(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
|
|
2034
2589
|
} catch (err) {
|
|
2035
2590
|
const e = err;
|
|
2036
2591
|
const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
|
|
2037
2592
|
return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
|
|
2038
2593
|
}
|
|
2039
|
-
const resolvedDest =
|
|
2040
|
-
const validateRoot = validateSubdir ?
|
|
2594
|
+
const resolvedDest = path24.resolve(destDir);
|
|
2595
|
+
const validateRoot = validateSubdir ? path24.resolve(resolvedDest, validateSubdir) : resolvedDest;
|
|
2041
2596
|
if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
|
|
2042
2597
|
return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
|
|
2043
2598
|
}
|
|
2044
|
-
if (
|
|
2599
|
+
if (fs18.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
|
|
2045
2600
|
return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
|
|
2046
2601
|
}
|
|
2047
|
-
const validated =
|
|
2602
|
+
const validated = fs18.existsSync(validateRoot);
|
|
2048
2603
|
return { ok: true, validated };
|
|
2049
2604
|
} catch (err) {
|
|
2050
2605
|
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
2051
2606
|
return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
|
|
2052
2607
|
} finally {
|
|
2053
2608
|
try {
|
|
2054
|
-
|
|
2609
|
+
fs18.rmSync(tmp, { force: true });
|
|
2055
2610
|
} catch {
|
|
2056
2611
|
}
|
|
2057
2612
|
}
|
|
2058
2613
|
}
|
|
2059
2614
|
|
|
2060
2615
|
// src/lib/skill-update.ts
|
|
2061
|
-
import
|
|
2062
|
-
import
|
|
2063
|
-
import { execFileSync as
|
|
2616
|
+
import fs19 from "fs";
|
|
2617
|
+
import path25 from "path";
|
|
2618
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
2064
2619
|
var PROTECTED_SKILLS = /* @__PURE__ */ new Set(["agent-delegation", "work-lifecycle", "xreview"]);
|
|
2065
2620
|
function sameTextContentNormalized(a, b) {
|
|
2066
|
-
const ba =
|
|
2067
|
-
const bb =
|
|
2621
|
+
const ba = fs19.readFileSync(a);
|
|
2622
|
+
const bb = fs19.readFileSync(b);
|
|
2068
2623
|
if (ba.equals(bb)) return true;
|
|
2069
2624
|
const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
|
|
2070
2625
|
const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
|
|
@@ -2072,10 +2627,10 @@ function sameTextContentNormalized(a, b) {
|
|
|
2072
2627
|
}
|
|
2073
2628
|
function diffSkillDirs(upstreamDir, localDir) {
|
|
2074
2629
|
const upstreamFiles = new Set(
|
|
2075
|
-
listFilesRecursive(upstreamDir).map((f) =>
|
|
2630
|
+
listFilesRecursive(upstreamDir).map((f) => path25.relative(upstreamDir, f))
|
|
2076
2631
|
);
|
|
2077
2632
|
const localFiles = new Set(
|
|
2078
|
-
listFilesRecursive(localDir).map((f) =>
|
|
2633
|
+
listFilesRecursive(localDir).map((f) => path25.relative(localDir, f))
|
|
2079
2634
|
);
|
|
2080
2635
|
const added = [];
|
|
2081
2636
|
const modified = [];
|
|
@@ -2083,7 +2638,7 @@ function diffSkillDirs(upstreamDir, localDir) {
|
|
|
2083
2638
|
for (const rel of upstreamFiles) {
|
|
2084
2639
|
if (!localFiles.has(rel)) {
|
|
2085
2640
|
added.push(rel);
|
|
2086
|
-
} else if (!sameTextContentNormalized(
|
|
2641
|
+
} else if (!sameTextContentNormalized(path25.join(upstreamDir, rel), path25.join(localDir, rel))) {
|
|
2087
2642
|
modified.push(rel);
|
|
2088
2643
|
}
|
|
2089
2644
|
}
|
|
@@ -2101,7 +2656,7 @@ function diffSkillDirs(upstreamDir, localDir) {
|
|
|
2101
2656
|
var DIFF_MAX_LINES = 400;
|
|
2102
2657
|
function renderSkillDiff(upstreamDir, localDir) {
|
|
2103
2658
|
try {
|
|
2104
|
-
|
|
2659
|
+
execFileSync4("git", ["diff", "--no-index", "--stat", "--", localDir, upstreamDir], {
|
|
2105
2660
|
stdio: "pipe",
|
|
2106
2661
|
encoding: "utf8"
|
|
2107
2662
|
});
|
|
@@ -2118,7 +2673,7 @@ function renderSkillDiff(upstreamDir, localDir) {
|
|
|
2118
2673
|
const stat = se.stdout;
|
|
2119
2674
|
let fullDiff = "";
|
|
2120
2675
|
try {
|
|
2121
|
-
|
|
2676
|
+
execFileSync4("git", ["diff", "--no-index", "--", localDir, upstreamDir], {
|
|
2122
2677
|
stdio: "pipe",
|
|
2123
2678
|
encoding: "utf8"
|
|
2124
2679
|
});
|
|
@@ -2145,8 +2700,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
2145
2700
|
if (PROTECTED_SKILLS.has(name)) {
|
|
2146
2701
|
throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
|
|
2147
2702
|
}
|
|
2148
|
-
const upstreamsFile = upstreamsFilePath ??
|
|
2149
|
-
const raw =
|
|
2703
|
+
const upstreamsFile = upstreamsFilePath ?? path25.join(path25.dirname(stackRoot()), "upstreams.json");
|
|
2704
|
+
const raw = fs19.readFileSync(upstreamsFile, "utf8");
|
|
2150
2705
|
const data = JSON.parse(raw);
|
|
2151
2706
|
const skillEntry = data?.skills?.[name];
|
|
2152
2707
|
if (!skillEntry) {
|
|
@@ -2155,8 +2710,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
2155
2710
|
if (skillEntry.kind === "release") {
|
|
2156
2711
|
throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
|
|
2157
2712
|
}
|
|
2158
|
-
const skillsRoot = localSkillsRoot ??
|
|
2159
|
-
const localSkillDir =
|
|
2713
|
+
const skillsRoot = localSkillsRoot ?? path25.join(stackRoot(), "skills");
|
|
2714
|
+
const localSkillDir = path25.join(skillsRoot, name);
|
|
2160
2715
|
const localFiles = listFilesRecursive(localSkillDir);
|
|
2161
2716
|
if (localFiles.length > 0) {
|
|
2162
2717
|
createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
|
|
@@ -2165,24 +2720,24 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
2165
2720
|
try {
|
|
2166
2721
|
const upstreamFiles = listFilesRecursive(upstreamSkillDir);
|
|
2167
2722
|
for (const src of upstreamFiles) {
|
|
2168
|
-
const st =
|
|
2723
|
+
const st = fs19.lstatSync(src);
|
|
2169
2724
|
if (st.isSymbolicLink()) {
|
|
2170
2725
|
throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
|
|
2171
2726
|
}
|
|
2172
|
-
const rel =
|
|
2173
|
-
const dest =
|
|
2174
|
-
ensureDir(
|
|
2727
|
+
const rel = path25.relative(upstreamSkillDir, src);
|
|
2728
|
+
const dest = path25.join(stagingDir, rel);
|
|
2729
|
+
ensureDir(path25.dirname(dest));
|
|
2175
2730
|
copyFile(src, dest);
|
|
2176
2731
|
}
|
|
2177
2732
|
const oldDir = `${localSkillDir}.old-${process.pid}`;
|
|
2178
|
-
if (
|
|
2179
|
-
|
|
2733
|
+
if (fs19.existsSync(localSkillDir)) {
|
|
2734
|
+
fs19.renameSync(localSkillDir, oldDir);
|
|
2180
2735
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2736
|
+
fs19.renameSync(stagingDir, localSkillDir);
|
|
2737
|
+
fs19.rmSync(oldDir, { recursive: true, force: true });
|
|
2183
2738
|
} catch (err) {
|
|
2184
2739
|
try {
|
|
2185
|
-
|
|
2740
|
+
fs19.rmSync(stagingDir, { recursive: true, force: true });
|
|
2186
2741
|
} catch {
|
|
2187
2742
|
}
|
|
2188
2743
|
throw err;
|
|
@@ -2196,8 +2751,8 @@ function rateLimitHint(prefix) {
|
|
|
2196
2751
|
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.`;
|
|
2197
2752
|
}
|
|
2198
2753
|
function loadUpstreams() {
|
|
2199
|
-
const file =
|
|
2200
|
-
return JSON.parse(
|
|
2754
|
+
const file = path26.join(path26.dirname(stackRoot()), "upstreams.json");
|
|
2755
|
+
return JSON.parse(fs20.readFileSync(file, "utf8"));
|
|
2201
2756
|
}
|
|
2202
2757
|
function skillsToScan(maintainer, upstreams) {
|
|
2203
2758
|
return maintainer ? Object.keys(upstreams.skills) : [];
|
|
@@ -2214,8 +2769,30 @@ async function latestNpmVersion(pkg) {
|
|
|
2214
2769
|
return null;
|
|
2215
2770
|
}
|
|
2216
2771
|
}
|
|
2217
|
-
|
|
2772
|
+
function resolvePlaywrightUpdateCheck(input) {
|
|
2773
|
+
if (input.enabled !== true) return null;
|
|
2774
|
+
if (input.cli.status === "current") {
|
|
2775
|
+
return {
|
|
2776
|
+
level: "success",
|
|
2777
|
+
message: `Playwright CLI: ${PLAYWRIGHT_CLI.version} \u2014 al d\xEDa con el pin aprobado.`
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
const local = input.cli.detectedVersion ?? (input.cli.status === "absent" ? "no instalado" : "no verificable");
|
|
2781
|
+
return {
|
|
2782
|
+
level: "warn",
|
|
2783
|
+
message: `Playwright CLI: ${local} local, pin aprobado ${PLAYWRIGHT_CLI.version} \u2192 ejecuta 'jorgex-stack update' o 'jorgex-stack install --playwright'.`
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
async function runUpdateCheck(localVersion, includeBrowserState = true) {
|
|
2218
2787
|
p4.intro("jorgex-stack update --check");
|
|
2788
|
+
if (includeBrowserState) {
|
|
2789
|
+
const preferenceErrors = browserPreferenceErrors();
|
|
2790
|
+
if (preferenceErrors.length > 0) {
|
|
2791
|
+
for (const error of preferenceErrors) p4.log.error(error);
|
|
2792
|
+
p4.outro("Check cancelado: corrige las preferencias de navegador antes de reintentar.");
|
|
2793
|
+
return 1;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2219
2796
|
const upstreams = loadUpstreams();
|
|
2220
2797
|
const npmLatest = await latestNpmVersion("jorgex-stack");
|
|
2221
2798
|
if (npmLatest === null)
|
|
@@ -2240,6 +2817,13 @@ async function runUpdateCheck(localVersion) {
|
|
|
2240
2817
|
`engram: ${local} local, ${latest} disponible. Tu instalaci\xF3n NO se toca (D7) \u2014 actualiza t\xFA: github.com/${engramRepo}/releases`
|
|
2241
2818
|
);
|
|
2242
2819
|
}
|
|
2820
|
+
if (includeBrowserState && loadPlaywrightCliPreference() === true) {
|
|
2821
|
+
const playwright = resolvePlaywrightUpdateCheck({
|
|
2822
|
+
enabled: true,
|
|
2823
|
+
cli: detectPlaywrightCli()
|
|
2824
|
+
});
|
|
2825
|
+
if (playwright) p4.log[playwright.level](playwright.message);
|
|
2826
|
+
}
|
|
2243
2827
|
const checkSkillNames = skillsToScan(isGitClone(), upstreams);
|
|
2244
2828
|
if (checkSkillNames.length === 0) {
|
|
2245
2829
|
p4.log.info(
|
|
@@ -2299,7 +2883,7 @@ function isEngramRunning() {
|
|
|
2299
2883
|
const pgrep = lookPath("pgrep");
|
|
2300
2884
|
if (!pgrep) return null;
|
|
2301
2885
|
try {
|
|
2302
|
-
|
|
2886
|
+
execFileSync5(pgrep, ["-x", "engram"], { stdio: "ignore" });
|
|
2303
2887
|
return true;
|
|
2304
2888
|
} catch {
|
|
2305
2889
|
return false;
|
|
@@ -2309,8 +2893,8 @@ function isEngramRunning() {
|
|
|
2309
2893
|
return null;
|
|
2310
2894
|
}
|
|
2311
2895
|
}
|
|
2312
|
-
function isGitClone(projectRoot =
|
|
2313
|
-
return
|
|
2896
|
+
function isGitClone(projectRoot = path26.dirname(stackRoot())) {
|
|
2897
|
+
return fs20.existsSync(path26.join(projectRoot, ".git"));
|
|
2314
2898
|
}
|
|
2315
2899
|
var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
|
|
2316
2900
|
function resolvePnpm() {
|
|
@@ -2320,29 +2904,29 @@ function resolvePnpm() {
|
|
|
2320
2904
|
}
|
|
2321
2905
|
function cleanupTmp(dir) {
|
|
2322
2906
|
try {
|
|
2323
|
-
|
|
2907
|
+
fs20.rmSync(dir, { recursive: true, force: true });
|
|
2324
2908
|
} catch {
|
|
2325
2909
|
}
|
|
2326
2910
|
}
|
|
2327
2911
|
function updateStackGitClone() {
|
|
2328
|
-
const projectRoot =
|
|
2912
|
+
const projectRoot = path26.dirname(stackRoot());
|
|
2329
2913
|
const git = lookPath("git");
|
|
2330
2914
|
if (!git) throw new Error("git no encontrado en PATH.");
|
|
2331
2915
|
const pnpm = resolvePnpm();
|
|
2332
2916
|
p4.log.info("Ejecutando git pull\u2026");
|
|
2333
|
-
|
|
2917
|
+
execFileSync5(git, ["pull"], { cwd: projectRoot, stdio: "inherit" });
|
|
2334
2918
|
p4.log.info("Ejecutando pnpm install\u2026");
|
|
2335
|
-
|
|
2919
|
+
execFileSync5(pnpm, ["install"], { cwd: projectRoot, stdio: "inherit" });
|
|
2336
2920
|
p4.log.info("Ejecutando pnpm build\u2026");
|
|
2337
|
-
|
|
2921
|
+
execFileSync5(pnpm, ["build"], { cwd: projectRoot, stdio: "inherit" });
|
|
2338
2922
|
}
|
|
2339
2923
|
function updateStackGlobal() {
|
|
2340
2924
|
const pnpm = resolvePnpm();
|
|
2341
2925
|
p4.log.info("Ejecutando pnpm add -g jorgex-stack@latest\u2026");
|
|
2342
|
-
|
|
2926
|
+
execFileSync5(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
|
|
2343
2927
|
}
|
|
2344
2928
|
async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
2345
|
-
const root =
|
|
2929
|
+
const root = fs20.mkdtempSync(path26.join(os4.tmpdir(), "jorgex-skill-"));
|
|
2346
2930
|
try {
|
|
2347
2931
|
const result = await downloadRepoTarball(repo, sha, root, skillPath);
|
|
2348
2932
|
if (!result.ok) {
|
|
@@ -2350,15 +2934,15 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2350
2934
|
return { error: result.reason };
|
|
2351
2935
|
}
|
|
2352
2936
|
if (skillPath) {
|
|
2353
|
-
const sub =
|
|
2937
|
+
const sub = path26.resolve(path26.join(root, skillPath));
|
|
2354
2938
|
if (!isContainedIn(sub, root)) {
|
|
2355
2939
|
cleanupTmp(root);
|
|
2356
2940
|
return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
|
|
2357
2941
|
}
|
|
2358
|
-
if (
|
|
2942
|
+
if (fs20.existsSync(sub)) return { dir: sub, root };
|
|
2359
2943
|
const lastSeg = skillPath.split("/").pop();
|
|
2360
|
-
const sub2 =
|
|
2361
|
-
if (isContainedIn(sub2, root) &&
|
|
2944
|
+
const sub2 = path26.resolve(path26.join(root, lastSeg));
|
|
2945
|
+
if (isContainedIn(sub2, root) && fs20.existsSync(sub2)) {
|
|
2362
2946
|
if (!validateExtractedTree(sub2)) {
|
|
2363
2947
|
cleanupTmp(root);
|
|
2364
2948
|
return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
|
|
@@ -2377,11 +2961,11 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2377
2961
|
function pruneEngramDbBackups() {
|
|
2378
2962
|
try {
|
|
2379
2963
|
const dir = dataDir();
|
|
2380
|
-
if (!
|
|
2381
|
-
const backups =
|
|
2964
|
+
if (!fs20.existsSync(dir)) return;
|
|
2965
|
+
const backups = fs20.readdirSync(dir).filter((f) => f.startsWith("engram-db-backup-") && f.endsWith(".db")).map((f) => ({ name: f, mtime: fs20.statSync(path26.join(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
2382
2966
|
for (const old of backups.slice(3)) {
|
|
2383
2967
|
try {
|
|
2384
|
-
|
|
2968
|
+
fs20.rmSync(path26.join(dir, old.name));
|
|
2385
2969
|
} catch {
|
|
2386
2970
|
}
|
|
2387
2971
|
}
|
|
@@ -2389,18 +2973,18 @@ function pruneEngramDbBackups() {
|
|
|
2389
2973
|
}
|
|
2390
2974
|
}
|
|
2391
2975
|
function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
2392
|
-
if (!
|
|
2393
|
-
const dir =
|
|
2394
|
-
const base =
|
|
2976
|
+
if (!fs20.existsSync(binPath)) return null;
|
|
2977
|
+
const dir = path26.dirname(binPath);
|
|
2978
|
+
const base = path26.basename(binPath);
|
|
2395
2979
|
const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
|
|
2396
2980
|
const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
|
|
2397
|
-
const resolvedDir =
|
|
2398
|
-
if (resolvedDir ===
|
|
2981
|
+
const resolvedDir = path26.resolve(dir);
|
|
2982
|
+
if (resolvedDir === path26.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
|
|
2399
2983
|
try {
|
|
2400
|
-
for (const entry of
|
|
2984
|
+
for (const entry of fs20.readdirSync(dir)) {
|
|
2401
2985
|
if (oldPattern.test(entry)) {
|
|
2402
2986
|
try {
|
|
2403
|
-
|
|
2987
|
+
fs20.rmSync(path26.join(dir, entry), { force: true });
|
|
2404
2988
|
} catch {
|
|
2405
2989
|
}
|
|
2406
2990
|
}
|
|
@@ -2408,8 +2992,8 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
|
2408
2992
|
} catch {
|
|
2409
2993
|
}
|
|
2410
2994
|
}
|
|
2411
|
-
const rotated =
|
|
2412
|
-
|
|
2995
|
+
const rotated = path26.join(dir, `${base}.old-${Date.now()}`);
|
|
2996
|
+
fs20.renameSync(binPath, rotated);
|
|
2413
2997
|
return rotated;
|
|
2414
2998
|
}
|
|
2415
2999
|
async function updateEngram(engramRepo, latestVersion) {
|
|
@@ -2418,19 +3002,19 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2418
3002
|
"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)."
|
|
2419
3003
|
);
|
|
2420
3004
|
}
|
|
2421
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
2422
|
-
const engramDb =
|
|
2423
|
-
if (
|
|
3005
|
+
const engramDataDir = process.env.ENGRAM_DATA_DIR ?? path26.join(HOME, ".engram");
|
|
3006
|
+
const engramDb = path26.join(engramDataDir, "engram.db");
|
|
3007
|
+
if (fs20.existsSync(engramDb)) {
|
|
2424
3008
|
const doBackup = await p4.confirm({
|
|
2425
3009
|
message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
|
|
2426
3010
|
initialValue: true
|
|
2427
3011
|
});
|
|
2428
3012
|
if (!p4.isCancel(doBackup) && doBackup) {
|
|
2429
3013
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2430
|
-
const dest =
|
|
3014
|
+
const dest = path26.join(dataDir(), `engram-db-backup-${ts}.db`);
|
|
2431
3015
|
try {
|
|
2432
|
-
if (!
|
|
2433
|
-
|
|
3016
|
+
if (!fs20.existsSync(dataDir())) fs20.mkdirSync(dataDir(), { recursive: true });
|
|
3017
|
+
fs20.copyFileSync(engramDb, dest);
|
|
2434
3018
|
p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
|
|
2435
3019
|
pruneEngramDbBackups();
|
|
2436
3020
|
} catch (err) {
|
|
@@ -2443,7 +3027,7 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2443
3027
|
if (brew) {
|
|
2444
3028
|
let brewManages = false;
|
|
2445
3029
|
try {
|
|
2446
|
-
|
|
3030
|
+
execFileSync5(brew, ["list", "engram"], { stdio: "pipe" });
|
|
2447
3031
|
brewManages = true;
|
|
2448
3032
|
} catch {
|
|
2449
3033
|
}
|
|
@@ -2451,7 +3035,7 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2451
3035
|
anyChannelTried = true;
|
|
2452
3036
|
p4.log.info("Actualizando engram con brew\u2026");
|
|
2453
3037
|
try {
|
|
2454
|
-
|
|
3038
|
+
execFileSync5(brew, ["upgrade", "engram"], { stdio: "inherit" });
|
|
2455
3039
|
return true;
|
|
2456
3040
|
} catch (err) {
|
|
2457
3041
|
p4.log.error(`brew upgrade engram fall\xF3: ${err instanceof Error ? err.message : err}`);
|
|
@@ -2475,15 +3059,15 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2475
3059
|
}
|
|
2476
3060
|
p4.log.info(`Actualizando engram con go install (${latestVersion})\u2026`);
|
|
2477
3061
|
try {
|
|
2478
|
-
|
|
3062
|
+
execFileSync5(
|
|
2479
3063
|
go,
|
|
2480
3064
|
["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
|
|
2481
3065
|
{ stdio: "inherit" }
|
|
2482
3066
|
);
|
|
2483
|
-
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists:
|
|
3067
|
+
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs20.existsSync(bin ?? "") });
|
|
2484
3068
|
if (rollbackOk.action === "restore") {
|
|
2485
3069
|
try {
|
|
2486
|
-
|
|
3070
|
+
fs20.renameSync(rotated, bin);
|
|
2487
3071
|
p4.log.warn(rollbackOk.messages.onRestore);
|
|
2488
3072
|
} catch {
|
|
2489
3073
|
p4.log.warn(rollbackOk.messages.onRenameFail);
|
|
@@ -2491,10 +3075,10 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2491
3075
|
}
|
|
2492
3076
|
return true;
|
|
2493
3077
|
} catch (err) {
|
|
2494
|
-
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists:
|
|
3078
|
+
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs20.existsSync(bin ?? "") });
|
|
2495
3079
|
if (rollbackFail.action === "restore") {
|
|
2496
3080
|
try {
|
|
2497
|
-
|
|
3081
|
+
fs20.renameSync(rotated, bin);
|
|
2498
3082
|
p4.log.info(rollbackFail.messages.onRestore);
|
|
2499
3083
|
} catch {
|
|
2500
3084
|
p4.log.error(rollbackFail.messages.onRenameFail);
|
|
@@ -2579,14 +3163,26 @@ function buildEligibleSkillUpdates(skills) {
|
|
|
2579
3163
|
}
|
|
2580
3164
|
return result;
|
|
2581
3165
|
}
|
|
2582
|
-
|
|
3166
|
+
function resolveUpdateSyncRequired(updated) {
|
|
3167
|
+
return updated.some((item) => item === "stack" || item === "skill" || item.startsWith("skill:"));
|
|
3168
|
+
}
|
|
3169
|
+
async function runInteractiveUpdate(localVersion, yes, dryRun = false, includeBrowserState = true) {
|
|
2583
3170
|
if (dryRun || yes || !process.stdout.isTTY) {
|
|
2584
|
-
return { exitCode: await runUpdateCheck(localVersion), appliedUpdates: false };
|
|
3171
|
+
return { exitCode: await runUpdateCheck(localVersion, includeBrowserState), appliedUpdates: false, syncRequired: false };
|
|
2585
3172
|
}
|
|
2586
3173
|
p4.intro("jorgex-stack update");
|
|
3174
|
+
if (includeBrowserState) {
|
|
3175
|
+
const preferenceErrors = browserPreferenceErrors();
|
|
3176
|
+
if (preferenceErrors.length > 0) {
|
|
3177
|
+
for (const error of preferenceErrors) p4.log.error(error);
|
|
3178
|
+
p4.outro("Update cancelado: corrige las preferencias de navegador antes de reintentar.");
|
|
3179
|
+
return { exitCode: 1, appliedUpdates: false, syncRequired: false };
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
2587
3182
|
const upstreams = loadUpstreams();
|
|
2588
3183
|
let exitCode = 0;
|
|
2589
3184
|
let appliedUpdates = false;
|
|
3185
|
+
const updated = [];
|
|
2590
3186
|
const maintainer = isGitClone();
|
|
2591
3187
|
const spin = p4.spinner();
|
|
2592
3188
|
spin.start("Consultando versiones upstream\u2026");
|
|
@@ -2645,6 +3241,21 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2645
3241
|
});
|
|
2646
3242
|
}
|
|
2647
3243
|
}
|
|
3244
|
+
let playwrightNeedsUpdate = false;
|
|
3245
|
+
if (includeBrowserState && loadPlaywrightCliPreference() === true) {
|
|
3246
|
+
const playwright = detectPlaywrightCli();
|
|
3247
|
+
if (playwright.status === "current") {
|
|
3248
|
+
p4.log.success("Playwright CLI: al d\xEDa.");
|
|
3249
|
+
} else {
|
|
3250
|
+
playwrightNeedsUpdate = true;
|
|
3251
|
+
const current = playwright.detectedVersion ?? playwright.status;
|
|
3252
|
+
updateItems.push({
|
|
3253
|
+
value: "playwright-cli",
|
|
3254
|
+
label: `Playwright CLI: ${current} \u2192 pin aprobado`,
|
|
3255
|
+
hint: "pnpm add --global @playwright/cli@0.1.17"
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
2648
3259
|
const typedSkillHeads = skillHeads;
|
|
2649
3260
|
const loggedRepos = /* @__PURE__ */ new Map();
|
|
2650
3261
|
for (const { name, repo, info, head } of typedSkillHeads) {
|
|
@@ -2686,7 +3297,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2686
3297
|
}
|
|
2687
3298
|
if (updateItems.length === 0) {
|
|
2688
3299
|
p4.outro("Todo al d\xEDa. No hay actualizaciones disponibles.");
|
|
2689
|
-
return { exitCode: 0, appliedUpdates: false };
|
|
3300
|
+
return { exitCode: 0, appliedUpdates: false, syncRequired: false };
|
|
2690
3301
|
}
|
|
2691
3302
|
const selected = await p4.multiselect({
|
|
2692
3303
|
message: "Selecciona qu\xE9 actualizar (espacio para marcar, intro para confirmar):",
|
|
@@ -2696,11 +3307,11 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2696
3307
|
});
|
|
2697
3308
|
if (p4.isCancel(selected)) {
|
|
2698
3309
|
p4.outro("Update cancelado.");
|
|
2699
|
-
return { exitCode: 0, appliedUpdates: false };
|
|
3310
|
+
return { exitCode: 0, appliedUpdates: false, syncRequired: false };
|
|
2700
3311
|
}
|
|
2701
3312
|
if (selected.length === 0) {
|
|
2702
3313
|
p4.outro("Nada seleccionado.");
|
|
2703
|
-
return { exitCode: 0, appliedUpdates: false };
|
|
3314
|
+
return { exitCode: 0, appliedUpdates: false, syncRequired: false };
|
|
2704
3315
|
}
|
|
2705
3316
|
const sel = selected;
|
|
2706
3317
|
if (stackNeedsUpdate && sel.includes("stack")) {
|
|
@@ -2721,6 +3332,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2721
3332
|
}
|
|
2722
3333
|
p4.log.success("Stack actualizado correctamente.");
|
|
2723
3334
|
appliedUpdates = true;
|
|
3335
|
+
updated.push("stack");
|
|
2724
3336
|
} catch (err) {
|
|
2725
3337
|
p4.log.error(`Stack: error al actualizar \u2014 ${err instanceof Error ? err.message : err}`);
|
|
2726
3338
|
exitCode = 1;
|
|
@@ -2752,7 +3364,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2752
3364
|
continue;
|
|
2753
3365
|
}
|
|
2754
3366
|
const { dir: tmpDir, root: tmpRoot } = tmpResult;
|
|
2755
|
-
const localSkillDir =
|
|
3367
|
+
const localSkillDir = path26.join(stackRoot(), "skills", skillInfo.name);
|
|
2756
3368
|
const diff = renderSkillDiff(tmpDir, localSkillDir);
|
|
2757
3369
|
if (diff) {
|
|
2758
3370
|
p4.log.info(`Diff de ${skillInfo.name}:
|
|
@@ -2776,6 +3388,7 @@ ${diff}`);
|
|
|
2776
3388
|
replaceSkill(skillInfo.name, tmpDir, skillInfo.head, {});
|
|
2777
3389
|
p4.log.success(`skill ${skillInfo.name}: actualizada y re-pineada a ${skillInfo.head.slice(0, 7)}.`);
|
|
2778
3390
|
appliedUpdates = true;
|
|
3391
|
+
updated.push("skill");
|
|
2779
3392
|
} catch (err) {
|
|
2780
3393
|
p4.log.error(`skill ${skillInfo.name}: error \u2014 ${err instanceof Error ? err.message : err}`);
|
|
2781
3394
|
exitCode = 1;
|
|
@@ -2783,6 +3396,27 @@ ${diff}`);
|
|
|
2783
3396
|
cleanupTmp(tmpRoot);
|
|
2784
3397
|
}
|
|
2785
3398
|
}
|
|
3399
|
+
if (playwrightNeedsUpdate && sel.includes("playwright-cli")) {
|
|
3400
|
+
const confirmPlaywright = await p4.confirm({
|
|
3401
|
+
message: "Actualizar Playwright CLI global al pin aprobado con pnpm add --global?",
|
|
3402
|
+
initialValue: true
|
|
3403
|
+
});
|
|
3404
|
+
if (p4.isCancel(confirmPlaywright) || !confirmPlaywright) {
|
|
3405
|
+
p4.log.info("Playwright CLI: actualizaci\xF3n omitida.");
|
|
3406
|
+
} else {
|
|
3407
|
+
const packageUpdated = executePlaywrightToolAction2("update");
|
|
3408
|
+
const browserInstalled = packageUpdated && executePlaywrightToolAction2("install-browser");
|
|
3409
|
+
if (packageUpdated && browserInstalled) {
|
|
3410
|
+
p4.log.success("Playwright CLI actualizado al pin aprobado.");
|
|
3411
|
+
appliedUpdates = true;
|
|
3412
|
+
updated.push("playwright-cli");
|
|
3413
|
+
} else {
|
|
3414
|
+
const failedStep = packageUpdated ? "descargar el navegador" : "actualizar el paquete global";
|
|
3415
|
+
p4.log.error(`Playwright CLI: no se pudo ${failedStep}. Ejecuta 'jorgex-stack install --playwright' para reintentar el paquete y el navegador.`);
|
|
3416
|
+
exitCode = 1;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
2786
3420
|
if (engramNeedsUpdate && sel.includes("engram") && engramData?.version) {
|
|
2787
3421
|
const confirmEngram = await p4.confirm({
|
|
2788
3422
|
message: `Actualizar engram a v${engramData.version} (canal nativo: brew \u2192 go install \u2192 URL)`,
|
|
@@ -2803,15 +3437,17 @@ ${diff}`);
|
|
|
2803
3437
|
p4.log.warn("Engram actualizado, pero no se pudo verificar la versi\xF3n \u2014 comprueba con engram --version");
|
|
2804
3438
|
}
|
|
2805
3439
|
p4.log.info("Reinicia los clientes (Claude Code/OpenCode/Codex) para que sus MCP usen la versi\xF3n nueva.");
|
|
3440
|
+
appliedUpdates = true;
|
|
3441
|
+
updated.push("engram");
|
|
2806
3442
|
}
|
|
2807
3443
|
}
|
|
2808
3444
|
}
|
|
2809
3445
|
p4.outro(exitCode === 0 ? "Update completado." : "Update completado con errores (revisa arriba).");
|
|
2810
|
-
return { exitCode, appliedUpdates };
|
|
3446
|
+
return { exitCode, appliedUpdates, syncRequired: resolveUpdateSyncRequired(updated) };
|
|
2811
3447
|
}
|
|
2812
3448
|
|
|
2813
3449
|
// src/models-picker.ts
|
|
2814
|
-
import
|
|
3450
|
+
import path27 from "path";
|
|
2815
3451
|
import * as p5 from "@clack/prompts";
|
|
2816
3452
|
var TIERS = ["strong", "standard", "cheap"];
|
|
2817
3453
|
var EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
@@ -2835,7 +3471,7 @@ function opencodeLiveModels(binPath) {
|
|
|
2835
3471
|
}
|
|
2836
3472
|
function agentsByTier() {
|
|
2837
3473
|
const grouped = { strong: [], standard: [], cheap: [] };
|
|
2838
|
-
for (const agent of loadCanonicalAgents(
|
|
3474
|
+
for (const agent of loadCanonicalAgents(path27.join(stackRoot(), "agents"))) {
|
|
2839
3475
|
if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
|
|
2840
3476
|
}
|
|
2841
3477
|
return grouped;
|
|
@@ -3015,16 +3651,16 @@ function cancelled() {
|
|
|
3015
3651
|
}
|
|
3016
3652
|
|
|
3017
3653
|
// src/lib/release.ts
|
|
3018
|
-
import
|
|
3019
|
-
import
|
|
3020
|
-
import { execFileSync as
|
|
3654
|
+
import fs21 from "fs";
|
|
3655
|
+
import path28 from "path";
|
|
3656
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
3021
3657
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3022
3658
|
function findPackageJson() {
|
|
3023
|
-
let dir =
|
|
3659
|
+
let dir = path28.dirname(fileURLToPath2(import.meta.url));
|
|
3024
3660
|
for (let i = 0; i < 6; i++) {
|
|
3025
|
-
const candidate =
|
|
3026
|
-
if (
|
|
3027
|
-
dir =
|
|
3661
|
+
const candidate = path28.join(dir, "package.json");
|
|
3662
|
+
if (fs21.existsSync(candidate)) return candidate;
|
|
3663
|
+
dir = path28.dirname(dir);
|
|
3028
3664
|
}
|
|
3029
3665
|
throw new Error("No se encontr\xF3 package.json cerca del CLI.");
|
|
3030
3666
|
}
|
|
@@ -3033,7 +3669,7 @@ function readPackageVersion() {
|
|
|
3033
3669
|
}
|
|
3034
3670
|
function readPackageMetadata() {
|
|
3035
3671
|
const packageJson = findPackageJson();
|
|
3036
|
-
const raw =
|
|
3672
|
+
const raw = fs21.readFileSync(packageJson, "utf8");
|
|
3037
3673
|
const parsed = JSON.parse(raw);
|
|
3038
3674
|
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
3039
3675
|
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
@@ -3070,6 +3706,10 @@ function parseFlags(args) {
|
|
|
3070
3706
|
list: false,
|
|
3071
3707
|
check: false,
|
|
3072
3708
|
removeEngram: false,
|
|
3709
|
+
playwright: false,
|
|
3710
|
+
removePlaywright: false,
|
|
3711
|
+
devtools: false,
|
|
3712
|
+
noDevtools: false,
|
|
3073
3713
|
positional: [],
|
|
3074
3714
|
unknownFlags: []
|
|
3075
3715
|
};
|
|
@@ -3107,6 +3747,10 @@ function parseFlags(args) {
|
|
|
3107
3747
|
else if (arg === "--list") flags.list = true;
|
|
3108
3748
|
else if (arg === "--check") flags.check = true;
|
|
3109
3749
|
else if (arg === "--remove-engram") flags.removeEngram = true;
|
|
3750
|
+
else if (arg === "--playwright") flags.playwright = true;
|
|
3751
|
+
else if (arg === "--remove-playwright") flags.removePlaywright = true;
|
|
3752
|
+
else if (arg === "--devtools") flags.devtools = true;
|
|
3753
|
+
else if (arg === "--no-devtools") flags.noDevtools = true;
|
|
3110
3754
|
else if (arg.startsWith("-")) flags.unknownFlags.push(arg);
|
|
3111
3755
|
else flags.positional.push(arg);
|
|
3112
3756
|
}
|
|
@@ -3173,6 +3817,48 @@ Corrige o borra ${preferenceFile}, o vuelve a ejecutar con --mode human|programm
|
|
|
3173
3817
|
subagentConcurrency: concurrency
|
|
3174
3818
|
};
|
|
3175
3819
|
}
|
|
3820
|
+
async function resolvePlaywrightToolConsent(command, flags) {
|
|
3821
|
+
const interactive = Boolean(process.stdout.isTTY);
|
|
3822
|
+
let confirmed = false;
|
|
3823
|
+
if (command === "install" && interactive && !flags.yes && !flags.dryRun && flags.targetDir === void 0) {
|
|
3824
|
+
const answer = await p6.confirm({
|
|
3825
|
+
message: "Recomendado: \xBFinstalar Playwright CLI global y descargar sus navegadores?",
|
|
3826
|
+
initialValue: false
|
|
3827
|
+
});
|
|
3828
|
+
if (p6.isCancel(answer)) return null;
|
|
3829
|
+
confirmed = answer === true;
|
|
3830
|
+
}
|
|
3831
|
+
return {
|
|
3832
|
+
command,
|
|
3833
|
+
interactive,
|
|
3834
|
+
yes: flags.yes,
|
|
3835
|
+
targetDir: flags.targetDir !== void 0,
|
|
3836
|
+
explicitToolSelection: flags.playwright,
|
|
3837
|
+
confirmed
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
async function resolveDevtoolsMcpSelection(command, flags, runtimes) {
|
|
3841
|
+
if (flags.devtools && flags.noDevtools) {
|
|
3842
|
+
console.error("Usa solo uno de --devtools o --no-devtools.");
|
|
3843
|
+
process.exitCode = 1;
|
|
3844
|
+
return null;
|
|
3845
|
+
}
|
|
3846
|
+
if (flags.devtools || flags.noDevtools) {
|
|
3847
|
+
return Object.fromEntries(runtimes.map((runtime) => [runtime, flags.devtools]));
|
|
3848
|
+
}
|
|
3849
|
+
if (command !== "install" || flags.yes || flags.dryRun || flags.targetDir !== void 0 || !process.stdout.isTTY) {
|
|
3850
|
+
return {};
|
|
3851
|
+
}
|
|
3852
|
+
const file = devtoolsMcpPreferenceFile();
|
|
3853
|
+
const selected = await p6.multiselect({
|
|
3854
|
+
message: "Chrome DevTools MCP avanzado (full: ~29 tools y ~5.8\u20137.7k tokens de schemas). \xBFEn qu\xE9 runtimes activarlo?",
|
|
3855
|
+
options: runtimes.map((runtime) => ({ value: runtime, label: ADAPTERS[runtime]?.name ?? runtime })),
|
|
3856
|
+
initialValues: runtimes.filter((runtime) => loadDevtoolsMcpPreference(file, runtime))
|
|
3857
|
+
});
|
|
3858
|
+
if (p6.isCancel(selected)) return null;
|
|
3859
|
+
const enabled = new Set(selected);
|
|
3860
|
+
return Object.fromEntries(runtimes.map((runtime) => [runtime, enabled.has(runtime)]));
|
|
3861
|
+
}
|
|
3176
3862
|
function parseCliArgs(argv) {
|
|
3177
3863
|
const [first, ...rest] = argv;
|
|
3178
3864
|
const isCommand = COMMANDS.includes(first ?? "install");
|
|
@@ -3227,8 +3913,13 @@ Opciones:
|
|
|
3227
3913
|
--target-dir <dir> Dir alternativo (pruebas de paridad; requiere 1 runtime)
|
|
3228
3914
|
--dry-run Muestra el plan sin escribir nada
|
|
3229
3915
|
--yes, -y No interactivo
|
|
3916
|
+
--playwright Autoriza Playwright CLI global y sus navegadores (requerido con --yes/sin TTY)
|
|
3917
|
+
--devtools (install/sync) activa Chrome DevTools MCP para los runtimes destino (opt-in)
|
|
3918
|
+
--no-devtools (install/sync) desactiva Chrome DevTools MCP (incompatible con --devtools)
|
|
3230
3919
|
--remove-engram (uninstall) desregistra Engram de los runtimes;
|
|
3231
3920
|
memorias y binario quedan intactos igualmente
|
|
3921
|
+
--remove-playwright (uninstall) retira solo el paquete global de Playwright;
|
|
3922
|
+
nunca perfiles, cach\xE9 ni navegadores
|
|
3232
3923
|
|
|
3233
3924
|
Ver PRD.md para el dise\xF1o completo.`);
|
|
3234
3925
|
}
|
|
@@ -3272,17 +3963,31 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3272
3963
|
process.exitCode = 1;
|
|
3273
3964
|
return;
|
|
3274
3965
|
}
|
|
3275
|
-
|
|
3966
|
+
const devtoolsMcpSelection = await resolveDevtoolsMcpSelection(command, flags, runtimes);
|
|
3967
|
+
if (devtoolsMcpSelection === null) return;
|
|
3968
|
+
const playwrightToolConsent = await resolvePlaywrightToolConsent(command, flags);
|
|
3969
|
+
if (playwrightToolConsent === null) return;
|
|
3970
|
+
const hasOpenCodeModels = await ensureOpenCodeModelsForInstall(command, flags, runtimes);
|
|
3971
|
+
if (!hasOpenCodeModels) {
|
|
3276
3972
|
process.exitCode = 1;
|
|
3277
3973
|
return;
|
|
3278
3974
|
}
|
|
3279
|
-
|
|
3975
|
+
const installExitCode = await runInstall({
|
|
3976
|
+
runtimes,
|
|
3977
|
+
targetDir: flags.targetDir,
|
|
3978
|
+
dryRun: flags.dryRun,
|
|
3979
|
+
yes: flags.yes,
|
|
3980
|
+
mode,
|
|
3981
|
+
playwrightToolConsent,
|
|
3982
|
+
devtoolsMcpSelection
|
|
3983
|
+
});
|
|
3984
|
+
process.exitCode = installExitCode;
|
|
3280
3985
|
return;
|
|
3281
3986
|
}
|
|
3282
3987
|
case "uninstall": {
|
|
3283
3988
|
const runtimes = await resolveRuntimes(flags);
|
|
3284
3989
|
if (runtimes === null) return;
|
|
3285
|
-
if (runtimes.length === 0) {
|
|
3990
|
+
if (runtimes.length === 0 && !flags.removePlaywright) {
|
|
3286
3991
|
console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex).");
|
|
3287
3992
|
process.exitCode = 1;
|
|
3288
3993
|
return;
|
|
@@ -3292,7 +3997,8 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3292
3997
|
targetDir: flags.targetDir,
|
|
3293
3998
|
dryRun: flags.dryRun,
|
|
3294
3999
|
yes: flags.yes,
|
|
3295
|
-
removeEngram: flags.removeEngram
|
|
4000
|
+
removeEngram: flags.removeEngram,
|
|
4001
|
+
removePlaywright: flags.removePlaywright
|
|
3296
4002
|
});
|
|
3297
4003
|
return;
|
|
3298
4004
|
}
|
|
@@ -3302,11 +4008,11 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3302
4008
|
}
|
|
3303
4009
|
case "update": {
|
|
3304
4010
|
if (flags.check) {
|
|
3305
|
-
process.exitCode = await runUpdateCheck(VERSION);
|
|
4011
|
+
process.exitCode = await runUpdateCheck(VERSION, flags.targetDir === void 0);
|
|
3306
4012
|
return;
|
|
3307
4013
|
}
|
|
3308
4014
|
if (flags.dryRun) {
|
|
3309
|
-
process.exitCode = await runUpdateCheck(VERSION);
|
|
4015
|
+
process.exitCode = await runUpdateCheck(VERSION, flags.targetDir === void 0);
|
|
3310
4016
|
return;
|
|
3311
4017
|
}
|
|
3312
4018
|
const runtimes = await resolveRuntimes(flags);
|
|
@@ -3333,11 +4039,16 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3333
4039
|
} else if (runtimes.length > 0) {
|
|
3334
4040
|
console.error("No hay modo guardado; se omite el sync previo y se contin\xFAa con update. Usa --mode expl\xEDcito si quieres sincronizar.");
|
|
3335
4041
|
}
|
|
3336
|
-
const result = await runInteractiveUpdate(
|
|
4042
|
+
const result = await runInteractiveUpdate(
|
|
4043
|
+
VERSION,
|
|
4044
|
+
flags.yes,
|
|
4045
|
+
flags.dryRun,
|
|
4046
|
+
flags.targetDir === void 0
|
|
4047
|
+
);
|
|
3337
4048
|
process.exitCode = result.exitCode;
|
|
3338
|
-
if (result.
|
|
4049
|
+
if (result.syncRequired && runtimes.length > 0 && (result.exitCode !== 0 || !canSync)) {
|
|
3339
4050
|
p6.log.warn("Skills/stack actualizados, pero el sync con los runtimes sigue pendiente. Ejecuta jorgex-stack sync --mode human|programmatic.");
|
|
3340
|
-
} else if (result.exitCode === 0 && result.
|
|
4051
|
+
} else if (result.exitCode === 0 && result.syncRequired && runtimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
|
|
3341
4052
|
const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
|
|
3342
4053
|
if (!p6.isCancel(apply) && apply) {
|
|
3343
4054
|
process.exitCode = await runInstall({
|
|
@@ -3350,7 +4061,7 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3350
4061
|
} else {
|
|
3351
4062
|
console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
|
|
3352
4063
|
}
|
|
3353
|
-
} else if (result.exitCode === 0 && result.
|
|
4064
|
+
} else if (result.exitCode === 0 && result.syncRequired && runtimes.length > 0 && canSync && (flags.yes || !process.stdout.isTTY)) {
|
|
3354
4065
|
console.log("Skills/stack actualizados. Ejecuta jorgex-stack sync para aplicarlos a los runtimes.");
|
|
3355
4066
|
}
|
|
3356
4067
|
return;
|
|
@@ -3400,7 +4111,7 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3400
4111
|
}
|
|
3401
4112
|
}
|
|
3402
4113
|
if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL2(process.argv[1]).href) {
|
|
3403
|
-
main().catch((err) => {
|
|
4114
|
+
await main().catch((err) => {
|
|
3404
4115
|
console.error(err instanceof Error ? err.message : String(err));
|
|
3405
4116
|
process.exitCode = 1;
|
|
3406
4117
|
});
|