jorgex-stack 1.0.30 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -3
- package/dist/cli.js +918 -255
- package/package.json +1 -1
- package/stack/agents/README.md +2 -2
- package/stack/agents/orchestrator.md +1 -221
- package/stack/agents/test-analyzer.md +10 -13
- package/stack/mcp/servers.json +8 -0
- package/stack/skills/agent-delegation/SKILL.md +3 -3
- package/stack/skills/orchestrator/SKILL.md +228 -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/modes/programmatic/agent-delegation.addendum.md +0 -11
- 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",
|
|
@@ -546,8 +565,6 @@ var opencodeAdapter = {
|
|
|
546
565
|
${deny}`);
|
|
547
566
|
}
|
|
548
567
|
if (!agent.spawn) lines.push(" task: deny");
|
|
549
|
-
lines.push("tools:");
|
|
550
|
-
lines.push(` write: ${agent.readonly ? "false" : "true"}`);
|
|
551
568
|
}
|
|
552
569
|
return [
|
|
553
570
|
{
|
|
@@ -638,6 +655,7 @@ ${agent.body}`,
|
|
|
638
655
|
const original = readTextIfExists(file);
|
|
639
656
|
const contentSource = original === null || original.trim() === "" ? null : original;
|
|
640
657
|
const isFreshConfig = contentSource === null;
|
|
658
|
+
const mcpOwnership = [];
|
|
641
659
|
const content = upsertJson(contentSource, (root) => {
|
|
642
660
|
root["$schema"] ??= "https://opencode.ai/config.json";
|
|
643
661
|
const defaults = loadCanonicalDefaults(ctx.stackDir)["opencode"];
|
|
@@ -649,6 +667,22 @@ ${agent.body}`,
|
|
|
649
667
|
}
|
|
650
668
|
const mcp = root["mcp"] ??= {};
|
|
651
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
|
+
}
|
|
652
686
|
if (server.transport === "stdio") {
|
|
653
687
|
if (server.command === "{{ENGRAM_BIN}}" && ctx.engramBin === null) {
|
|
654
688
|
ctx.warnings.push(
|
|
@@ -658,6 +692,7 @@ ${agent.body}`,
|
|
|
658
692
|
}
|
|
659
693
|
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
660
694
|
mcp[name] = { type: "local", command: [command, ...server.args ?? []] };
|
|
695
|
+
if (server.optional && existing === void 0 && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
661
696
|
} else {
|
|
662
697
|
const previous = mcp[name];
|
|
663
698
|
const headers = {};
|
|
@@ -687,7 +722,7 @@ ${agent.body}`,
|
|
|
687
722
|
}
|
|
688
723
|
}
|
|
689
724
|
});
|
|
690
|
-
return [{ kind: "write", target: file, content }];
|
|
725
|
+
return [{ kind: "write", target: file, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} }];
|
|
691
726
|
},
|
|
692
727
|
planUnmerge(mcp, hooks, ctx) {
|
|
693
728
|
const actions = [];
|
|
@@ -696,15 +731,26 @@ ${agent.body}`,
|
|
|
696
731
|
if (prompt !== null) {
|
|
697
732
|
let content = removeMarkdownSection(prompt, "system-prompt");
|
|
698
733
|
content = removeMarkdownSection(content, "engram-protocol");
|
|
734
|
+
content = removeMarkdownSection(content, "browser");
|
|
699
735
|
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
700
736
|
}
|
|
701
737
|
const configFile = path7.join(ctx.configDir, "opencode.json");
|
|
702
738
|
const config = readTextIfExists(configFile);
|
|
703
739
|
if (config !== null) {
|
|
740
|
+
const mcpOwnership = [];
|
|
704
741
|
const content = upsertJson(config, (root) => {
|
|
705
742
|
const mcpBlock = root["mcp"];
|
|
706
743
|
if (mcpBlock) {
|
|
707
|
-
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
|
+
}
|
|
708
754
|
if (Object.keys(mcpBlock).length === 0) delete root["mcp"];
|
|
709
755
|
}
|
|
710
756
|
const plugin = root["plugin"];
|
|
@@ -715,7 +761,7 @@ ${agent.body}`,
|
|
|
715
761
|
else root["plugin"] = kept;
|
|
716
762
|
}
|
|
717
763
|
});
|
|
718
|
-
actions.push({ kind: "write", target: configFile, content });
|
|
764
|
+
actions.push({ kind: "write", target: configFile, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} });
|
|
719
765
|
}
|
|
720
766
|
const hooksFile = path7.join(ctx.configDir, "hooks.json");
|
|
721
767
|
const hooksJson = readTextIfExists(hooksFile);
|
|
@@ -775,6 +821,14 @@ function hasEngramPlugin(configDir) {
|
|
|
775
821
|
return false;
|
|
776
822
|
}
|
|
777
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
|
+
}
|
|
778
832
|
var claudeCodeAdapter = {
|
|
779
833
|
id: "claude-code",
|
|
780
834
|
name: "Claude Code",
|
|
@@ -803,16 +857,7 @@ description: ${yamlString2(agent.description)}
|
|
|
803
857
|
keep-coding-instructions: true
|
|
804
858
|
---
|
|
805
859
|
${agent.body}`;
|
|
806
|
-
|
|
807
|
-
const skill = `---
|
|
808
|
-
name: ${agent.name}
|
|
809
|
-
description: ${yamlString2(skillDescription)}
|
|
810
|
-
---
|
|
811
|
-
${agent.body}`;
|
|
812
|
-
return [
|
|
813
|
-
{ file: `${agent.name}.md`, content: style, kind: "output-style" },
|
|
814
|
-
{ file: `${agent.name}/SKILL.md`, content: skill, kind: "skill" }
|
|
815
|
-
];
|
|
860
|
+
return [{ file: `${agent.name}.md`, content: style, kind: "output-style" }];
|
|
816
861
|
}
|
|
817
862
|
const lines = [`name: ${agent.name}`, `description: ${yamlString2(agent.description)}`];
|
|
818
863
|
const tools = toolsFor(agent);
|
|
@@ -871,9 +916,26 @@ ${agent.body}`,
|
|
|
871
916
|
},
|
|
872
917
|
planMainConfig(canonical, ctx) {
|
|
873
918
|
const file = path8.join(path8.dirname(ctx.configDir), `${path8.basename(ctx.configDir)}.json`);
|
|
919
|
+
const mcpOwnership = [];
|
|
874
920
|
const content = upsertJson(readTextIfExists(file), (root) => {
|
|
875
921
|
const servers = root["mcpServers"] ??= {};
|
|
876
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
|
+
}
|
|
877
939
|
if (server.transport === "stdio") {
|
|
878
940
|
if (server.command === "{{ENGRAM_BIN}}" && hasEngramPlugin(ctx.configDir)) {
|
|
879
941
|
if (name in servers) delete servers[name];
|
|
@@ -890,6 +952,7 @@ ${agent.body}`,
|
|
|
890
952
|
}
|
|
891
953
|
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
892
954
|
servers[name] = { type: "stdio", command, args: server.args ?? [] };
|
|
955
|
+
if (server.optional && existing === void 0 && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
893
956
|
} else {
|
|
894
957
|
const previous = servers[name];
|
|
895
958
|
const headers = {};
|
|
@@ -905,7 +968,7 @@ ${agent.body}`,
|
|
|
905
968
|
}
|
|
906
969
|
}
|
|
907
970
|
});
|
|
908
|
-
return [{ kind: "write", target: file, content }];
|
|
971
|
+
return [{ kind: "write", target: file, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} }];
|
|
909
972
|
},
|
|
910
973
|
planUnmerge(mcp, hooks, ctx) {
|
|
911
974
|
const actions = [];
|
|
@@ -914,6 +977,7 @@ ${agent.body}`,
|
|
|
914
977
|
if (prompt !== null) {
|
|
915
978
|
let content = removeMarkdownSection(prompt, "system-prompt");
|
|
916
979
|
content = removeMarkdownSection(content, "engram-protocol");
|
|
980
|
+
content = removeMarkdownSection(content, "browser");
|
|
917
981
|
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
918
982
|
}
|
|
919
983
|
const settingsFile = path8.join(ctx.configDir, "settings.json");
|
|
@@ -927,13 +991,23 @@ ${agent.body}`,
|
|
|
927
991
|
const mainFile = path8.join(path8.dirname(ctx.configDir), `${path8.basename(ctx.configDir)}.json`);
|
|
928
992
|
const main2 = readTextIfExists(mainFile);
|
|
929
993
|
if (main2 !== null) {
|
|
994
|
+
const mcpOwnership = [];
|
|
930
995
|
const content = upsertJson(main2, (root) => {
|
|
931
996
|
const servers = root["mcpServers"];
|
|
932
997
|
if (!servers) return;
|
|
933
|
-
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
|
+
}
|
|
934
1008
|
if (Object.keys(servers).length === 0) delete root["mcpServers"];
|
|
935
1009
|
});
|
|
936
|
-
actions.push({ kind: "write", target: mainFile, content });
|
|
1010
|
+
actions.push({ kind: "write", target: mainFile, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} });
|
|
937
1011
|
}
|
|
938
1012
|
return actions;
|
|
939
1013
|
}
|
|
@@ -951,6 +1025,14 @@ function tomlMultiline(value) {
|
|
|
951
1025
|
${value.replace(/\r\n/g, "\n").trim()}
|
|
952
1026
|
'''`;
|
|
953
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
|
+
}
|
|
954
1036
|
function hasActiveEngramPlugin(configDir) {
|
|
955
1037
|
const config = readTextIfExists(path9.join(configDir, "config.toml"));
|
|
956
1038
|
if (config === null) return false;
|
|
@@ -995,16 +1077,7 @@ var codexAdapter = {
|
|
|
995
1077
|
`# Uso: codex --profile ${agent.name}`,
|
|
996
1078
|
`developer_instructions = ${tomlMultiline(agent.body)}`
|
|
997
1079
|
];
|
|
998
|
-
|
|
999
|
-
const skill = `---
|
|
1000
|
-
name: ${agent.name}
|
|
1001
|
-
description: ${JSON.stringify(skillDescription)}
|
|
1002
|
-
---
|
|
1003
|
-
${agent.body}`;
|
|
1004
|
-
return [
|
|
1005
|
-
{ file: `${agent.name}.config.toml`, content: profileLines.join("\n") + "\n", kind: "profile" },
|
|
1006
|
-
{ file: `${agent.name}/SKILL.md`, content: skill, kind: "skill" }
|
|
1007
|
-
];
|
|
1080
|
+
return [{ file: `${agent.name}.config.toml`, content: profileLines.join("\n") + "\n", kind: "profile" }];
|
|
1008
1081
|
}
|
|
1009
1082
|
const lines = [
|
|
1010
1083
|
`name = ${tomlString(agent.name)}`,
|
|
@@ -1063,6 +1136,7 @@ ${body}`
|
|
|
1063
1136
|
const original = readTextIfExists(file);
|
|
1064
1137
|
const contentSource = original === null || original.trim() === "" ? null : original;
|
|
1065
1138
|
let content = contentSource;
|
|
1139
|
+
const mcpOwnership = [];
|
|
1066
1140
|
if (contentSource === null) {
|
|
1067
1141
|
const defaults = loadCanonicalDefaults(ctx.stackDir)["codex"] ?? {};
|
|
1068
1142
|
for (const [key, value] of Object.entries(defaults)) {
|
|
@@ -1109,6 +1183,22 @@ ${body}`
|
|
|
1109
1183
|
}
|
|
1110
1184
|
for (const [name, server] of Object.entries(canonical.servers)) {
|
|
1111
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
|
+
}
|
|
1112
1202
|
if (server.transport === "stdio") {
|
|
1113
1203
|
if (server.command === "{{ENGRAM_BIN}}" && hasActiveEngramPlugin(ctx.configDir)) {
|
|
1114
1204
|
if (content !== null) content = removeTomlSection(content, section);
|
|
@@ -1124,9 +1214,9 @@ ${body}`
|
|
|
1124
1214
|
continue;
|
|
1125
1215
|
}
|
|
1126
1216
|
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
1127
|
-
const args = (server.args ?? []).map(tomlString).join(", ");
|
|
1128
1217
|
content = upsertTomlSection(content, section, `command = ${tomlString(command)}
|
|
1129
|
-
args = [${args}]`);
|
|
1218
|
+
args = [${(server.args ?? []).map(tomlString).join(", ")}]`);
|
|
1219
|
+
if (server.optional && existing === null && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
1130
1220
|
} else {
|
|
1131
1221
|
const previousSection = readTomlSection(content, section);
|
|
1132
1222
|
const prevUsesEnvHeaders = previousSection?.includes("env_http_headers") ?? false;
|
|
@@ -1147,7 +1237,7 @@ args = [${args}]`);
|
|
|
1147
1237
|
}
|
|
1148
1238
|
if (content === null) return [];
|
|
1149
1239
|
if (!content.endsWith("\n")) content += "\n";
|
|
1150
|
-
return [{ kind: "write", target: file, content }];
|
|
1240
|
+
return [{ kind: "write", target: file, content, ...mcpOwnership.length > 0 ? { mcpOwnership } : {} }];
|
|
1151
1241
|
},
|
|
1152
1242
|
planUnmerge(mcp, hooks, ctx) {
|
|
1153
1243
|
const actions = [];
|
|
@@ -1156,14 +1246,28 @@ args = [${args}]`);
|
|
|
1156
1246
|
if (prompt !== null) {
|
|
1157
1247
|
let content = removeMarkdownSection(prompt, "system-prompt");
|
|
1158
1248
|
content = removeMarkdownSection(content, "engram-protocol");
|
|
1249
|
+
content = removeMarkdownSection(content, "browser");
|
|
1159
1250
|
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
1160
1251
|
}
|
|
1161
1252
|
const configFile = path9.join(ctx.configDir, "config.toml");
|
|
1162
1253
|
const config = readTextIfExists(configFile);
|
|
1163
1254
|
if (config !== null) {
|
|
1164
1255
|
let content = config;
|
|
1165
|
-
|
|
1166
|
-
|
|
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 } : {} });
|
|
1167
1271
|
}
|
|
1168
1272
|
const hooksFile = path9.join(ctx.configDir, "hooks.json");
|
|
1169
1273
|
const hooksJson = readTextIfExists(hooksFile);
|
|
@@ -1379,13 +1483,6 @@ import path13 from "path";
|
|
|
1379
1483
|
var PROGRAMMATIC_ROOT = ["modes", "programmatic"];
|
|
1380
1484
|
var PROGRAMMATIC_MARKER = "<!-- jorgex:programmatic-mode -->";
|
|
1381
1485
|
var LEGACY_RESULT_CONTRACT_SECTION = /\n?##\s+Result contract[\s\S]*$/;
|
|
1382
|
-
var LEGACY_SKILL_DELEGATION_SECTION = /\n?##\s+Formato obligatorio[\s\S]*$/;
|
|
1383
|
-
var LEGACY_DELEGATION_LINE = /- For each `→ \[agent\]: \.\.\.` line, launch the corresponding specialist\./;
|
|
1384
|
-
var LEGACY_PROGRAMMATIC_PHRASES = [
|
|
1385
|
-
[/\bResult contract\b/g, "strict JSON handoff"],
|
|
1386
|
-
[/Status \/ Delegations \/ Risks/g, "status, delegations, and risks"],
|
|
1387
|
-
[LEGACY_DELEGATION_LINE, "- Process the JSON `delegations[]` array and launch the corresponding specialist."]
|
|
1388
|
-
];
|
|
1389
1486
|
var normalize = (value) => value.replace(/\r\n/g, "\n");
|
|
1390
1487
|
function loadProgrammaticAddendum(stackDir, fileName) {
|
|
1391
1488
|
return normalize(fs9.readFileSync(path13.join(stackDir, ...PROGRAMMATIC_ROOT, fileName), "utf8")).trim();
|
|
@@ -1399,10 +1496,7 @@ ${addendum}
|
|
|
1399
1496
|
`;
|
|
1400
1497
|
}
|
|
1401
1498
|
function stripLegacyResultContract(body) {
|
|
1402
|
-
return
|
|
1403
|
-
(text2, [pattern, replacement]) => text2.replace(pattern, replacement),
|
|
1404
|
-
normalize(body).replace(LEGACY_RESULT_CONTRACT_SECTION, "")
|
|
1405
|
-
).trimEnd();
|
|
1499
|
+
return normalize(body).replace(LEGACY_RESULT_CONTRACT_SECTION, "").replace(/\bResult contract\b/g, "strict JSON handoff").trimEnd();
|
|
1406
1500
|
}
|
|
1407
1501
|
function concurrencyRule(concurrency) {
|
|
1408
1502
|
if (concurrency === "parallel") {
|
|
@@ -1422,22 +1516,13 @@ function composeProgrammaticSystemPrompt(stackDir, content, mode) {
|
|
|
1422
1516
|
}
|
|
1423
1517
|
function composeProgrammaticAgentBody(stackDir, agent, mode, concurrency) {
|
|
1424
1518
|
if (mode !== "programmatic") return normalize(agent.body);
|
|
1425
|
-
const fileName = agent.mode === "primary" ? "orchestrator.addendum.md" : "subagent.addendum.md";
|
|
1426
|
-
let addendum = loadProgrammaticAddendum(stackDir, fileName);
|
|
1427
1519
|
if (agent.mode === "primary") {
|
|
1428
|
-
|
|
1520
|
+
const addendum2 = loadProgrammaticAddendum(stackDir, "orchestrator.addendum.md").replace("{{CONCURRENCY_RULE}}", concurrencyRule(concurrency ?? "serial"));
|
|
1521
|
+
return appendAddendum(agent.body, addendum2);
|
|
1429
1522
|
}
|
|
1523
|
+
const addendum = loadProgrammaticAddendum(stackDir, "subagent.addendum.md");
|
|
1430
1524
|
return appendAddendum(stripLegacyResultContract(agent.body), addendum);
|
|
1431
1525
|
}
|
|
1432
|
-
function composeProgrammaticSkillBody(stackDir, skillPath, content, mode) {
|
|
1433
|
-
if (mode !== "programmatic") return normalize(content);
|
|
1434
|
-
if (skillPath !== path13.join("agent-delegation", "SKILL.md")) return normalize(content);
|
|
1435
|
-
const base = normalize(content).replace(
|
|
1436
|
-
"Las delegaciones van **en tu output final**, en el formato de abajo. El orquestador las lee y decide a qui\xE9n invocar.",
|
|
1437
|
-
"Las delegaciones van como strings en el JSON final `delegations[]`. El orquestador las lee y decide a qui\xE9n invocar."
|
|
1438
|
-
).replace(LEGACY_SKILL_DELEGATION_SECTION, "").trimEnd();
|
|
1439
|
-
return appendAddendum(base, loadProgrammaticAddendum(stackDir, "agent-delegation.addendum.md"));
|
|
1440
|
-
}
|
|
1441
1526
|
|
|
1442
1527
|
// src/components/system-prompt.ts
|
|
1443
1528
|
var normalize2 = (s) => s.replace(/\r\n/g, "\n");
|
|
@@ -1448,6 +1533,10 @@ function planSystemPrompt(adapter, ctx) {
|
|
|
1448
1533
|
normalize2(fs10.readFileSync(path14.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8"))
|
|
1449
1534
|
);
|
|
1450
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");
|
|
1451
1540
|
let content = readTextIfExists(target);
|
|
1452
1541
|
content = upsertMarkdownSection(content, "system-prompt", composedAgentsMd);
|
|
1453
1542
|
if (adapter.injectEngramProtocol(ctx)) {
|
|
@@ -1455,18 +1544,18 @@ function planSystemPrompt(adapter, ctx) {
|
|
|
1455
1544
|
} else {
|
|
1456
1545
|
content = removeMarkdownSection(content, "engram-protocol");
|
|
1457
1546
|
}
|
|
1547
|
+
content = browser === "" ? removeMarkdownSection(content, "browser") : upsertMarkdownSection(content, "browser", browser);
|
|
1458
1548
|
return [{ kind: "write", target, content }];
|
|
1459
1549
|
}
|
|
1460
1550
|
|
|
1461
1551
|
// src/components/agents.ts
|
|
1462
1552
|
import path15 from "path";
|
|
1463
1553
|
function planAgents(adapter, ctx) {
|
|
1464
|
-
const { agentsDir, commandsDir, outputStylesDir,
|
|
1554
|
+
const { agentsDir, commandsDir, outputStylesDir, profilesDir, scriptsDir } = adapter.paths(ctx.configDir);
|
|
1465
1555
|
const dirFor = {
|
|
1466
1556
|
agent: agentsDir,
|
|
1467
1557
|
command: commandsDir,
|
|
1468
1558
|
"output-style": outputStylesDir,
|
|
1469
|
-
skill: skillsDir,
|
|
1470
1559
|
profile: profilesDir
|
|
1471
1560
|
};
|
|
1472
1561
|
const scriptsBase = scriptsDir.replace(/\\/g, "/");
|
|
@@ -1489,43 +1578,35 @@ function planAgents(adapter, ctx) {
|
|
|
1489
1578
|
|
|
1490
1579
|
// src/components/skills.ts
|
|
1491
1580
|
import path16 from "path";
|
|
1492
|
-
import fs11 from "fs";
|
|
1493
1581
|
function planSkills(adapter, ctx) {
|
|
1494
1582
|
const { skillsDir } = adapter.paths(ctx.configDir);
|
|
1495
1583
|
const source = path16.join(ctx.stackDir, "skills");
|
|
1496
1584
|
return listFilesRecursive(source).map((file) => {
|
|
1497
1585
|
const relative = path16.relative(source, file);
|
|
1498
|
-
if (relative === path16.join("agent-delegation", "SKILL.md") && ctx.mode === "programmatic") {
|
|
1499
|
-
return {
|
|
1500
|
-
kind: "write",
|
|
1501
|
-
target: path16.join(skillsDir, relative),
|
|
1502
|
-
content: composeProgrammaticSkillBody(ctx.stackDir, relative, fs11.readFileSync(file, "utf8"), ctx.mode)
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
1586
|
return { kind: "copy", source: file, target: path16.join(skillsDir, relative) };
|
|
1506
1587
|
});
|
|
1507
1588
|
}
|
|
1508
1589
|
|
|
1509
1590
|
// src/components/commands.ts
|
|
1510
1591
|
import path17 from "path";
|
|
1511
|
-
import
|
|
1592
|
+
import fs11 from "fs";
|
|
1512
1593
|
function planCommands(adapter, ctx) {
|
|
1513
1594
|
const { commandsDir } = adapter.paths(ctx.configDir);
|
|
1514
1595
|
const source = path17.join(ctx.stackDir, "commands");
|
|
1515
|
-
if (!
|
|
1596
|
+
if (!fs11.existsSync(source)) return [];
|
|
1516
1597
|
const commandFiles = [
|
|
1517
1598
|
...listMarkdownFiles(source),
|
|
1518
1599
|
...listMarkdownFiles(path17.join(source, adapter.id))
|
|
1519
1600
|
];
|
|
1520
1601
|
return commandFiles.map(({ file, fullPath }) => {
|
|
1521
|
-
const raw =
|
|
1602
|
+
const raw = fs11.readFileSync(fullPath, "utf8").replace(/\r\n/g, "\n");
|
|
1522
1603
|
const rendered = adapter.renderCommand(file, raw);
|
|
1523
1604
|
return { kind: "write", target: path17.join(commandsDir, rendered.file), content: rendered.content };
|
|
1524
1605
|
});
|
|
1525
1606
|
}
|
|
1526
1607
|
function listMarkdownFiles(dir) {
|
|
1527
|
-
if (!
|
|
1528
|
-
return
|
|
1608
|
+
if (!fs11.existsSync(dir)) return [];
|
|
1609
|
+
return fs11.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => ({ file: entry.name, fullPath: path17.join(dir, entry.name) }));
|
|
1529
1610
|
}
|
|
1530
1611
|
|
|
1531
1612
|
// src/components/hooks.ts
|
|
@@ -1540,22 +1621,22 @@ function planMcp(adapter, ctx) {
|
|
|
1540
1621
|
|
|
1541
1622
|
// src/components/plugins.ts
|
|
1542
1623
|
import path18 from "path";
|
|
1543
|
-
import
|
|
1624
|
+
import fs12 from "fs";
|
|
1544
1625
|
function planPlugins(adapter, ctx) {
|
|
1545
1626
|
const { pluginsDir } = adapter.paths(ctx.configDir);
|
|
1546
1627
|
if (pluginsDir === null) return [];
|
|
1547
1628
|
const source = path18.join(ctx.stackDir, "plugins", adapter.id);
|
|
1548
|
-
if (!
|
|
1629
|
+
if (!fs12.existsSync(source)) return [];
|
|
1549
1630
|
return listFilesRecursive(source).filter((f) => f.endsWith(".ts")).map((sourceFile) => {
|
|
1550
1631
|
const target = path18.join(pluginsDir, path18.relative(source, sourceFile));
|
|
1551
|
-
const raw =
|
|
1632
|
+
const raw = fs12.readFileSync(sourceFile, "utf8");
|
|
1552
1633
|
let content = raw;
|
|
1553
1634
|
if (content.includes('"{{ENGRAM_BIN}}"')) {
|
|
1554
1635
|
content = content.replace(/"\{\{ENGRAM_BIN\}\}"/g, JSON.stringify(ctx.engramBin ?? "engram"));
|
|
1555
1636
|
}
|
|
1556
1637
|
if (content.includes('"{{ENGRAM_PROTOCOL}}"')) {
|
|
1557
1638
|
const protocol = stripLeadingHtmlComments(
|
|
1558
|
-
|
|
1639
|
+
fs12.readFileSync(path18.join(ctx.stackDir, "system-prompt", "engram-protocol.md"), "utf8").replace(/\r\n/g, "\n")
|
|
1559
1640
|
);
|
|
1560
1641
|
content = content.replace(/"\{\{ENGRAM_PROTOCOL\}\}"/g, JSON.stringify(protocol));
|
|
1561
1642
|
}
|
|
@@ -1565,13 +1646,256 @@ function planPlugins(adapter, ctx) {
|
|
|
1565
1646
|
});
|
|
1566
1647
|
}
|
|
1567
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+)\s*$/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
|
+
|
|
1568
1849
|
// src/install.ts
|
|
1569
1850
|
var ADAPTERS = {
|
|
1570
1851
|
opencode: opencodeAdapter,
|
|
1571
1852
|
"claude-code": claudeCodeAdapter,
|
|
1572
1853
|
codex: codexAdapter
|
|
1573
1854
|
};
|
|
1574
|
-
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) {
|
|
1575
1899
|
const models = loadModelMap()[adapter.id];
|
|
1576
1900
|
if (!models) return null;
|
|
1577
1901
|
return {
|
|
@@ -1581,7 +1905,10 @@ function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE)
|
|
|
1581
1905
|
subagentConcurrency: mode.subagentConcurrency,
|
|
1582
1906
|
engramBin: detectEngram(),
|
|
1583
1907
|
models,
|
|
1584
|
-
warnings: []
|
|
1908
|
+
warnings: [],
|
|
1909
|
+
enabledMcpServers: enabledMcpServers(adapter.id, void 0, useBrowserPreferences),
|
|
1910
|
+
playwrightCliEnabled: useBrowserPreferences && loadPlaywrightCliPreference() === true,
|
|
1911
|
+
ownedMcpServers: ownedMcpServers(adapter.id, useBrowserPreferences)
|
|
1585
1912
|
};
|
|
1586
1913
|
}
|
|
1587
1914
|
function buildPlan(adapter, ctx) {
|
|
@@ -1602,14 +1929,16 @@ function diffPlan(plan) {
|
|
|
1602
1929
|
if (current === null) return { action, status: "create" };
|
|
1603
1930
|
return { action, status: current === action.content ? "unchanged" : "update" };
|
|
1604
1931
|
}
|
|
1605
|
-
if (!
|
|
1932
|
+
if (!fs15.existsSync(action.target)) return { action, status: "create" };
|
|
1606
1933
|
return { action, status: sameFileContent(action.source, action.target) ? "unchanged" : "update" };
|
|
1607
1934
|
});
|
|
1608
1935
|
}
|
|
1609
|
-
function applyChanges(changes) {
|
|
1936
|
+
function applyChanges(changes, onMcpOwnershipWritten) {
|
|
1610
1937
|
for (const { action } of changes) {
|
|
1611
|
-
if (action.kind === "write")
|
|
1612
|
-
|
|
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);
|
|
1613
1942
|
}
|
|
1614
1943
|
}
|
|
1615
1944
|
function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
|
|
@@ -1626,7 +1955,7 @@ function collectAllCurrentTargets(mode = DEFAULT_INSTALL_MODE_PREFERENCE) {
|
|
|
1626
1955
|
continue;
|
|
1627
1956
|
}
|
|
1628
1957
|
try {
|
|
1629
|
-
for (const action of buildPlan(adapter, ctx)) targets.add(
|
|
1958
|
+
for (const action of buildPlan(adapter, ctx)) targets.add(path21.resolve(action.target));
|
|
1630
1959
|
} catch (error) {
|
|
1631
1960
|
complete = false;
|
|
1632
1961
|
warnings.push(
|
|
@@ -1642,6 +1971,17 @@ async function runInstall(opts) {
|
|
|
1642
1971
|
const engramBin = detectEngram();
|
|
1643
1972
|
const modePreference = opts.mode === void 0 ? opts.targetDir === void 0 ? loadInstallModePreference() : DEFAULT_INSTALL_MODE_PREFERENCE : normalizeInstallModePreference(opts.mode);
|
|
1644
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;
|
|
1645
1985
|
const modelMap = loadModelMap();
|
|
1646
1986
|
if (useManifest) ensureModelMapFile();
|
|
1647
1987
|
p.log.info(engramBin ? `Engram detectado: ${engramBin} (se respeta, D7)` : "Engram NO detectado.");
|
|
@@ -1655,6 +1995,7 @@ async function runInstall(opts) {
|
|
|
1655
1995
|
}
|
|
1656
1996
|
let exitCode = 0;
|
|
1657
1997
|
let successfulRuns = 0;
|
|
1998
|
+
const successfulContexts = [];
|
|
1658
1999
|
for (const id of opts.runtimes) {
|
|
1659
2000
|
const adapter = ADAPTERS[id];
|
|
1660
2001
|
if (!adapter) {
|
|
@@ -1680,7 +2021,16 @@ async function runInstall(opts) {
|
|
|
1680
2021
|
subagentConcurrency: modePreference.subagentConcurrency,
|
|
1681
2022
|
engramBin,
|
|
1682
2023
|
models,
|
|
1683
|
-
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
|
+
}
|
|
1684
2034
|
};
|
|
1685
2035
|
let plan = buildPlan(adapter, ctx);
|
|
1686
2036
|
let diff = diffPlan(plan);
|
|
@@ -1696,24 +2046,30 @@ async function runInstall(opts) {
|
|
|
1696
2046
|
if (orphans.length > 0) p.log.info(`${orphans.length} hu\xE9rfanos de versiones previas a eliminar`);
|
|
1697
2047
|
for (const w of ctx.warnings) p.log.warn(w);
|
|
1698
2048
|
if (opts.dryRun) {
|
|
1699
|
-
|
|
1700
|
-
|
|
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`);
|
|
1701
2054
|
for (const o of orphans) p.log.message(` - ${o}`);
|
|
1702
2055
|
continue;
|
|
1703
2056
|
}
|
|
1704
2057
|
const writeManifest = () => {
|
|
1705
2058
|
if (!useManifest) return;
|
|
1706
|
-
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)));
|
|
1707
2060
|
const keepTarget = (target) => !unmergeTargets.has(target);
|
|
1708
|
-
const liveOwned = plan.map((a) =>
|
|
1709
|
-
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);
|
|
1710
2063
|
const owned = canOrphan ? liveOwned : [.../* @__PURE__ */ new Set([...previousOwned, ...liveOwned])];
|
|
1711
2064
|
writeRuntimeManifest(id, { configDir, owned, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1712
2065
|
};
|
|
1713
2066
|
if (changes.length === 0 && orphans.length === 0) {
|
|
1714
2067
|
writeManifest();
|
|
2068
|
+
if (useManifest) persistMcpOwnershipChanges(id, plan);
|
|
2069
|
+
persistDevtoolsSelection();
|
|
1715
2070
|
p.log.success(`${adapter.name}: ya al d\xEDa (idempotente).`);
|
|
1716
2071
|
successfulRuns++;
|
|
2072
|
+
successfulContexts.push({ adapter, ctx });
|
|
1717
2073
|
continue;
|
|
1718
2074
|
}
|
|
1719
2075
|
if (!opts.yes && process.stdout.isTTY) {
|
|
@@ -1731,10 +2087,10 @@ async function runInstall(opts) {
|
|
|
1731
2087
|
}
|
|
1732
2088
|
const backup = useManifest ? createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`) : null;
|
|
1733
2089
|
if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
1734
|
-
applyChanges(changes);
|
|
1735
|
-
const pruneRoot = useManifest ? HOME :
|
|
2090
|
+
applyChanges(changes, useManifest ? (action) => persistMcpOwnershipChanges(id, [action]) : void 0);
|
|
2091
|
+
const pruneRoot = useManifest ? HOME : path21.dirname(configDir);
|
|
1736
2092
|
for (const orphan of orphans) {
|
|
1737
|
-
|
|
2093
|
+
fs15.rmSync(orphan, { force: true });
|
|
1738
2094
|
pruneEmptyDirs(orphan, pruneRoot);
|
|
1739
2095
|
}
|
|
1740
2096
|
const verifyCtx = { ...ctx, warnings: [] };
|
|
@@ -1745,26 +2101,98 @@ async function runInstall(opts) {
|
|
|
1745
2101
|
exitCode = 1;
|
|
1746
2102
|
} else {
|
|
1747
2103
|
writeManifest();
|
|
2104
|
+
if (useManifest) persistMcpOwnershipChanges(id, plan);
|
|
2105
|
+
persistDevtoolsSelection();
|
|
1748
2106
|
p.log.success(`${adapter.name}: ${changes.length} archivos aplicados y verificados (idempotente).`);
|
|
1749
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
|
+
}
|
|
1750
2164
|
}
|
|
1751
2165
|
}
|
|
1752
2166
|
if (useManifest && !opts.dryRun && exitCode === 0 && successfulRuns > 0) {
|
|
1753
2167
|
saveInstallModePreference(installModePreferenceFile(), modePreference);
|
|
1754
2168
|
}
|
|
1755
|
-
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).");
|
|
1756
2170
|
return exitCode;
|
|
1757
2171
|
}
|
|
1758
2172
|
|
|
1759
2173
|
// src/uninstall.ts
|
|
1760
|
-
import
|
|
1761
|
-
import
|
|
2174
|
+
import fs16 from "fs";
|
|
2175
|
+
import path22 from "path";
|
|
1762
2176
|
import * as p2 from "@clack/prompts";
|
|
2177
|
+
function resolvePlaywrightUninstallPlan(input) {
|
|
2178
|
+
return {
|
|
2179
|
+
actions: input.removePackage ? ["remove"] : [],
|
|
2180
|
+
preserveBrowserData: true
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
1763
2183
|
async function runUninstall(opts) {
|
|
1764
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
|
+
}
|
|
1765
2192
|
const stackDir = stackRoot();
|
|
1766
2193
|
const mcp = loadCanonicalMcp(stackDir);
|
|
1767
2194
|
const hooks = loadCanonicalHooks(stackDir);
|
|
2195
|
+
let exitCode = 0;
|
|
1768
2196
|
let removeEngram = opts.removeEngram;
|
|
1769
2197
|
if (!removeEngram && !opts.yes && !opts.dryRun && process.stdout.isTTY) {
|
|
1770
2198
|
const answer = await p2.confirm({
|
|
@@ -1782,13 +2210,15 @@ async function runUninstall(opts) {
|
|
|
1782
2210
|
p2.log.info("Engram se conserva: memorias, binario y registro intactos (usa --remove-engram para desregistrarlo).");
|
|
1783
2211
|
}
|
|
1784
2212
|
const retained = /* @__PURE__ */ new Set();
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
+
}
|
|
1792
2222
|
}
|
|
1793
2223
|
for (const id of opts.runtimes) {
|
|
1794
2224
|
const adapter = ADAPTERS[id];
|
|
@@ -1802,60 +2232,106 @@ async function runUninstall(opts) {
|
|
|
1802
2232
|
p2.log.warn(`${adapter.name} no detectado \u2014 omitido.`);
|
|
1803
2233
|
continue;
|
|
1804
2234
|
}
|
|
1805
|
-
const ctx = makeContext(adapter, configDir);
|
|
2235
|
+
const ctx = makeContext(adapter, configDir, void 0, useBrowserPreferences);
|
|
1806
2236
|
if (!ctx) continue;
|
|
1807
2237
|
ctx.preserveEngram = !removeEngram;
|
|
1808
2238
|
const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
|
|
1809
|
-
const mergedTargets = new Set(unmerge.map((a) =>
|
|
2239
|
+
const mergedTargets = new Set(unmerge.map((a) => path22.resolve(a.target)));
|
|
1810
2240
|
const usingRealConfig = opts.targetDir === void 0;
|
|
1811
2241
|
const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
|
|
1812
|
-
const pruneRoot = usingRealConfig ? HOME :
|
|
2242
|
+
const pruneRoot = usingRealConfig ? HOME : path22.dirname(configDir);
|
|
1813
2243
|
const planTargets = [
|
|
1814
|
-
.../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) =>
|
|
1815
|
-
].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));
|
|
1816
2246
|
const deleteTargets = planTargets.filter(
|
|
1817
|
-
(t) => !retained.has(t) && !(ctx.preserveEngram &&
|
|
2247
|
+
(t) => !retained.has(t) && !(ctx.preserveEngram && path22.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
|
|
1818
2248
|
);
|
|
1819
2249
|
const sharedKept = planTargets.length - deleteTargets.length;
|
|
1820
2250
|
p2.log.step(`${adapter.name} \u2192 ${configDir}`);
|
|
1821
2251
|
p2.log.info(`${deleteTargets.length} archivos a borrar, ${unmerge.length} archivos compartidos a limpiar`);
|
|
1822
2252
|
if (sharedKept > 0) p2.log.info(`${sharedKept} archivos se conservan: otros runtimes instalados los siguen usando.`);
|
|
1823
2253
|
if (opts.dryRun) continue;
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
+
}
|
|
1828
2265
|
if (backup) p2.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
|
|
1829
2266
|
for (const target of deleteTargets) {
|
|
1830
|
-
|
|
2267
|
+
fs16.rmSync(target, { force: true });
|
|
1831
2268
|
pruneEmptyDirs(target, pruneRoot);
|
|
1832
2269
|
}
|
|
1833
2270
|
for (const action of unmerge) {
|
|
1834
2271
|
if (action.kind !== "write") continue;
|
|
1835
2272
|
if (action.content.trim() === "") {
|
|
1836
|
-
|
|
2273
|
+
fs16.rmSync(action.target, { force: true });
|
|
1837
2274
|
} else {
|
|
1838
2275
|
writeText(action.target, action.content);
|
|
1839
2276
|
}
|
|
2277
|
+
if (usingRealConfig) {
|
|
2278
|
+
for (const change of action.mcpOwnership ?? []) {
|
|
2279
|
+
saveDevtoolsMcpOwnership(devtoolsMcpPreferenceFile(), id, change.server, change.owned);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
1840
2282
|
}
|
|
1841
2283
|
if (usingRealConfig) removeRuntimeManifest(id);
|
|
1842
2284
|
p2.log.success(`${adapter.name}: stack retirado (lo tuyo queda intacto).`);
|
|
1843
2285
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
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;
|
|
1846
2311
|
}
|
|
1847
2312
|
|
|
1848
2313
|
// src/doctor.ts
|
|
1849
|
-
import
|
|
1850
|
-
import
|
|
2314
|
+
import path23 from "path";
|
|
2315
|
+
import fs17 from "fs";
|
|
1851
2316
|
import * as p3 from "@clack/prompts";
|
|
1852
2317
|
function engramVersion(bin) {
|
|
1853
2318
|
const out = runDetectedBin(bin, ["--version"], 5e3);
|
|
1854
2319
|
if (out === null) return null;
|
|
1855
2320
|
return /(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? out.trim().split("\n")[0] ?? null;
|
|
1856
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
|
+
}
|
|
1857
2333
|
function context7KeyConfigured(id, configDir) {
|
|
1858
|
-
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");
|
|
1859
2335
|
const content = readTextIfExists(file);
|
|
1860
2336
|
if (content === null) return null;
|
|
1861
2337
|
const match = /CONTEXT7_API_KEY"?\s*[:=]\s*"([^"]*)"/.exec(content);
|
|
@@ -1878,13 +2354,44 @@ async function runDoctor() {
|
|
|
1878
2354
|
p3.log.success(`Engram: ${version} (${engramBin})`);
|
|
1879
2355
|
}
|
|
1880
2356
|
}
|
|
1881
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
1882
|
-
const engramDb =
|
|
1883
|
-
if (
|
|
1884
|
-
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);
|
|
1885
2361
|
p3.log.info(`Engram DB: ${engramDb} (${sizeMb} MB de memorias \u2014 el stack no la toca JAM\xC1S).`);
|
|
1886
2362
|
}
|
|
1887
|
-
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
|
+
}
|
|
1888
2395
|
const manifest = readManifest();
|
|
1889
2396
|
const modePreference = loadInstallModePreference();
|
|
1890
2397
|
const current = collectAllCurrentTargets(modePreference);
|
|
@@ -1923,10 +2430,10 @@ async function runDoctor() {
|
|
|
1923
2430
|
p3.log.warn(`${adapter.name}: ${orphans.length} archivos hu\xE9rfanos de versiones previas \u2192 ejecuta 'sync'.`);
|
|
1924
2431
|
problems++;
|
|
1925
2432
|
}
|
|
1926
|
-
if (adapter.id === "codex" &&
|
|
2433
|
+
if (adapter.id === "codex" && fs17.existsSync(path23.join(detection.configDir, "hooks.json"))) {
|
|
1927
2434
|
p3.log.info("Codex: recuerda que los hooks requieren aprobaci\xF3n manual \u2014 verifica con /hooks dentro de codex.");
|
|
1928
2435
|
}
|
|
1929
|
-
if (adapter.id === "codex" &&
|
|
2436
|
+
if (adapter.id === "codex" && fs17.existsSync(path23.join(detection.configDir, "AGENTS.override.md"))) {
|
|
1930
2437
|
p3.log.warn(
|
|
1931
2438
|
"Codex: existe ~/.codex/AGENTS.override.md \u2014 tiene prioridad ABSOLUTA y tapa el AGENTS.md gestionado por el stack."
|
|
1932
2439
|
);
|
|
@@ -1940,17 +2447,17 @@ async function runDoctor() {
|
|
|
1940
2447
|
}
|
|
1941
2448
|
|
|
1942
2449
|
// src/update.ts
|
|
1943
|
-
import
|
|
1944
|
-
import
|
|
1945
|
-
import
|
|
1946
|
-
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";
|
|
1947
2454
|
import * as p4 from "@clack/prompts";
|
|
1948
2455
|
|
|
1949
2456
|
// src/lib/github.ts
|
|
1950
|
-
import
|
|
1951
|
-
import
|
|
1952
|
-
import { execFileSync as
|
|
1953
|
-
import
|
|
2457
|
+
import fs18 from "fs";
|
|
2458
|
+
import path24 from "path";
|
|
2459
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2460
|
+
import os3 from "os";
|
|
1954
2461
|
import { Readable } from "stream";
|
|
1955
2462
|
import { pipeline } from "stream/promises";
|
|
1956
2463
|
var cachedToken;
|
|
@@ -2018,19 +2525,19 @@ async function latestGithubCommit(repo) {
|
|
|
2018
2525
|
}
|
|
2019
2526
|
}
|
|
2020
2527
|
function validateExtractedTree(destDir) {
|
|
2021
|
-
const resolved =
|
|
2528
|
+
const resolved = path24.resolve(destDir);
|
|
2022
2529
|
const walk = (dir) => {
|
|
2023
2530
|
let entries;
|
|
2024
2531
|
try {
|
|
2025
|
-
entries =
|
|
2532
|
+
entries = fs18.readdirSync(dir, { withFileTypes: true });
|
|
2026
2533
|
} catch {
|
|
2027
2534
|
return false;
|
|
2028
2535
|
}
|
|
2029
2536
|
for (const entry of entries) {
|
|
2030
|
-
const full =
|
|
2537
|
+
const full = path24.join(dir, entry.name);
|
|
2031
2538
|
let stat;
|
|
2032
2539
|
try {
|
|
2033
|
-
stat =
|
|
2540
|
+
stat = fs18.lstatSync(full);
|
|
2034
2541
|
} catch {
|
|
2035
2542
|
return false;
|
|
2036
2543
|
}
|
|
@@ -2046,15 +2553,15 @@ function validateExtractedTree(destDir) {
|
|
|
2046
2553
|
}
|
|
2047
2554
|
function resolveTarBin() {
|
|
2048
2555
|
if (process.platform !== "win32") return "tar";
|
|
2049
|
-
const winTar =
|
|
2050
|
-
return
|
|
2556
|
+
const winTar = path24.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "tar.exe");
|
|
2557
|
+
return fs18.existsSync(winTar) ? winTar : "tar";
|
|
2051
2558
|
}
|
|
2052
2559
|
async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
2053
2560
|
const url = `https://codeload.github.com/${repo}/tar.gz/${sha}`;
|
|
2054
|
-
const tmp =
|
|
2561
|
+
const tmp = path24.join(os3.tmpdir(), `jorgex-tarball-${Date.now()}.tar.gz`);
|
|
2055
2562
|
const fail = (reason) => {
|
|
2056
2563
|
try {
|
|
2057
|
-
|
|
2564
|
+
fs18.rmSync(destDir, { recursive: true, force: true });
|
|
2058
2565
|
} catch {
|
|
2059
2566
|
}
|
|
2060
2567
|
return { ok: false, reason };
|
|
@@ -2073,46 +2580,46 @@ async function downloadRepoTarball(repo, sha, destDir, validateSubdir) {
|
|
|
2073
2580
|
if (!res.body) return fail("respuesta HTTP sin cuerpo");
|
|
2074
2581
|
await pipeline(
|
|
2075
2582
|
Readable.fromWeb(res.body),
|
|
2076
|
-
|
|
2583
|
+
fs18.createWriteStream(tmp)
|
|
2077
2584
|
);
|
|
2078
|
-
|
|
2079
|
-
|
|
2585
|
+
fs18.rmSync(destDir, { recursive: true, force: true });
|
|
2586
|
+
fs18.mkdirSync(destDir, { recursive: true });
|
|
2080
2587
|
try {
|
|
2081
|
-
|
|
2588
|
+
execFileSync3(resolveTarBin(), ["-xzf", tmp, "--strip-components=1", "-C", destDir], { stdio: "pipe" });
|
|
2082
2589
|
} catch (err) {
|
|
2083
2590
|
const e = err;
|
|
2084
2591
|
const detail = (e.stderr?.toString().trim() || e.message || "").split("\n")[0];
|
|
2085
2592
|
return fail(detail ? `tar fall\xF3: ${detail}` : "tar no disponible o fall\xF3 la extracci\xF3n");
|
|
2086
2593
|
}
|
|
2087
|
-
const resolvedDest =
|
|
2088
|
-
const validateRoot = validateSubdir ?
|
|
2594
|
+
const resolvedDest = path24.resolve(destDir);
|
|
2595
|
+
const validateRoot = validateSubdir ? path24.resolve(resolvedDest, validateSubdir) : resolvedDest;
|
|
2089
2596
|
if (validateRoot !== resolvedDest && !isContainedIn(validateRoot, resolvedDest)) {
|
|
2090
2597
|
return fail(`la ruta de validaci\xF3n "${validateSubdir}" escapa del destino`);
|
|
2091
2598
|
}
|
|
2092
|
-
if (
|
|
2599
|
+
if (fs18.existsSync(validateRoot) && !validateExtractedTree(validateRoot)) {
|
|
2093
2600
|
return fail("el \xE1rbol extra\xEDdo contiene symlinks o rutas fuera del destino");
|
|
2094
2601
|
}
|
|
2095
|
-
const validated =
|
|
2602
|
+
const validated = fs18.existsSync(validateRoot);
|
|
2096
2603
|
return { ok: true, validated };
|
|
2097
2604
|
} catch (err) {
|
|
2098
2605
|
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
2099
2606
|
return fail(timedOut ? "timeout de descarga (120s)" : err instanceof Error ? `fallo de red: ${err.message}` : "error desconocido");
|
|
2100
2607
|
} finally {
|
|
2101
2608
|
try {
|
|
2102
|
-
|
|
2609
|
+
fs18.rmSync(tmp, { force: true });
|
|
2103
2610
|
} catch {
|
|
2104
2611
|
}
|
|
2105
2612
|
}
|
|
2106
2613
|
}
|
|
2107
2614
|
|
|
2108
2615
|
// src/lib/skill-update.ts
|
|
2109
|
-
import
|
|
2110
|
-
import
|
|
2111
|
-
import { execFileSync as
|
|
2616
|
+
import fs19 from "fs";
|
|
2617
|
+
import path25 from "path";
|
|
2618
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
2112
2619
|
var PROTECTED_SKILLS = /* @__PURE__ */ new Set(["agent-delegation", "work-lifecycle", "xreview"]);
|
|
2113
2620
|
function sameTextContentNormalized(a, b) {
|
|
2114
|
-
const ba =
|
|
2115
|
-
const bb =
|
|
2621
|
+
const ba = fs19.readFileSync(a);
|
|
2622
|
+
const bb = fs19.readFileSync(b);
|
|
2116
2623
|
if (ba.equals(bb)) return true;
|
|
2117
2624
|
const sa = ba.toString("utf8").replace(/\r\n/g, "\n");
|
|
2118
2625
|
const sb = bb.toString("utf8").replace(/\r\n/g, "\n");
|
|
@@ -2120,10 +2627,10 @@ function sameTextContentNormalized(a, b) {
|
|
|
2120
2627
|
}
|
|
2121
2628
|
function diffSkillDirs(upstreamDir, localDir) {
|
|
2122
2629
|
const upstreamFiles = new Set(
|
|
2123
|
-
listFilesRecursive(upstreamDir).map((f) =>
|
|
2630
|
+
listFilesRecursive(upstreamDir).map((f) => path25.relative(upstreamDir, f))
|
|
2124
2631
|
);
|
|
2125
2632
|
const localFiles = new Set(
|
|
2126
|
-
listFilesRecursive(localDir).map((f) =>
|
|
2633
|
+
listFilesRecursive(localDir).map((f) => path25.relative(localDir, f))
|
|
2127
2634
|
);
|
|
2128
2635
|
const added = [];
|
|
2129
2636
|
const modified = [];
|
|
@@ -2131,7 +2638,7 @@ function diffSkillDirs(upstreamDir, localDir) {
|
|
|
2131
2638
|
for (const rel of upstreamFiles) {
|
|
2132
2639
|
if (!localFiles.has(rel)) {
|
|
2133
2640
|
added.push(rel);
|
|
2134
|
-
} else if (!sameTextContentNormalized(
|
|
2641
|
+
} else if (!sameTextContentNormalized(path25.join(upstreamDir, rel), path25.join(localDir, rel))) {
|
|
2135
2642
|
modified.push(rel);
|
|
2136
2643
|
}
|
|
2137
2644
|
}
|
|
@@ -2149,7 +2656,7 @@ function diffSkillDirs(upstreamDir, localDir) {
|
|
|
2149
2656
|
var DIFF_MAX_LINES = 400;
|
|
2150
2657
|
function renderSkillDiff(upstreamDir, localDir) {
|
|
2151
2658
|
try {
|
|
2152
|
-
|
|
2659
|
+
execFileSync4("git", ["diff", "--no-index", "--stat", "--", localDir, upstreamDir], {
|
|
2153
2660
|
stdio: "pipe",
|
|
2154
2661
|
encoding: "utf8"
|
|
2155
2662
|
});
|
|
@@ -2166,7 +2673,7 @@ function renderSkillDiff(upstreamDir, localDir) {
|
|
|
2166
2673
|
const stat = se.stdout;
|
|
2167
2674
|
let fullDiff = "";
|
|
2168
2675
|
try {
|
|
2169
|
-
|
|
2676
|
+
execFileSync4("git", ["diff", "--no-index", "--", localDir, upstreamDir], {
|
|
2170
2677
|
stdio: "pipe",
|
|
2171
2678
|
encoding: "utf8"
|
|
2172
2679
|
});
|
|
@@ -2193,8 +2700,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
2193
2700
|
if (PROTECTED_SKILLS.has(name)) {
|
|
2194
2701
|
throw new Error(`La skill "${name}" es propia del stack y no se actualiza desde upstream.`);
|
|
2195
2702
|
}
|
|
2196
|
-
const upstreamsFile = upstreamsFilePath ??
|
|
2197
|
-
const raw =
|
|
2703
|
+
const upstreamsFile = upstreamsFilePath ?? path25.join(path25.dirname(stackRoot()), "upstreams.json");
|
|
2704
|
+
const raw = fs19.readFileSync(upstreamsFile, "utf8");
|
|
2198
2705
|
const data = JSON.parse(raw);
|
|
2199
2706
|
const skillEntry = data?.skills?.[name];
|
|
2200
2707
|
if (!skillEntry) {
|
|
@@ -2203,8 +2710,8 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
2203
2710
|
if (skillEntry.kind === "release") {
|
|
2204
2711
|
throw new Error(`La skill "${name}" es de tipo release y no se actualiza con replaceSkill.`);
|
|
2205
2712
|
}
|
|
2206
|
-
const skillsRoot = localSkillsRoot ??
|
|
2207
|
-
const localSkillDir =
|
|
2713
|
+
const skillsRoot = localSkillsRoot ?? path25.join(stackRoot(), "skills");
|
|
2714
|
+
const localSkillDir = path25.join(skillsRoot, name);
|
|
2208
2715
|
const localFiles = listFilesRecursive(localSkillDir);
|
|
2209
2716
|
if (localFiles.length > 0) {
|
|
2210
2717
|
createBackup(localFiles, `skill-update-${name}`, backupsRoot2);
|
|
@@ -2213,24 +2720,24 @@ function replaceSkill(name, upstreamSkillDir, newCommit, opts) {
|
|
|
2213
2720
|
try {
|
|
2214
2721
|
const upstreamFiles = listFilesRecursive(upstreamSkillDir);
|
|
2215
2722
|
for (const src of upstreamFiles) {
|
|
2216
|
-
const st =
|
|
2723
|
+
const st = fs19.lstatSync(src);
|
|
2217
2724
|
if (st.isSymbolicLink()) {
|
|
2218
2725
|
throw new Error(`Symlink rechazado en upstream de skill "${name}": ${src}`);
|
|
2219
2726
|
}
|
|
2220
|
-
const rel =
|
|
2221
|
-
const dest =
|
|
2222
|
-
ensureDir(
|
|
2727
|
+
const rel = path25.relative(upstreamSkillDir, src);
|
|
2728
|
+
const dest = path25.join(stagingDir, rel);
|
|
2729
|
+
ensureDir(path25.dirname(dest));
|
|
2223
2730
|
copyFile(src, dest);
|
|
2224
2731
|
}
|
|
2225
2732
|
const oldDir = `${localSkillDir}.old-${process.pid}`;
|
|
2226
|
-
if (
|
|
2227
|
-
|
|
2733
|
+
if (fs19.existsSync(localSkillDir)) {
|
|
2734
|
+
fs19.renameSync(localSkillDir, oldDir);
|
|
2228
2735
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2736
|
+
fs19.renameSync(stagingDir, localSkillDir);
|
|
2737
|
+
fs19.rmSync(oldDir, { recursive: true, force: true });
|
|
2231
2738
|
} catch (err) {
|
|
2232
2739
|
try {
|
|
2233
|
-
|
|
2740
|
+
fs19.rmSync(stagingDir, { recursive: true, force: true });
|
|
2234
2741
|
} catch {
|
|
2235
2742
|
}
|
|
2236
2743
|
throw err;
|
|
@@ -2244,8 +2751,8 @@ function rateLimitHint(prefix) {
|
|
|
2244
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.`;
|
|
2245
2752
|
}
|
|
2246
2753
|
function loadUpstreams() {
|
|
2247
|
-
const file =
|
|
2248
|
-
return JSON.parse(
|
|
2754
|
+
const file = path26.join(path26.dirname(stackRoot()), "upstreams.json");
|
|
2755
|
+
return JSON.parse(fs20.readFileSync(file, "utf8"));
|
|
2249
2756
|
}
|
|
2250
2757
|
function skillsToScan(maintainer, upstreams) {
|
|
2251
2758
|
return maintainer ? Object.keys(upstreams.skills) : [];
|
|
@@ -2262,8 +2769,30 @@ async function latestNpmVersion(pkg) {
|
|
|
2262
2769
|
return null;
|
|
2263
2770
|
}
|
|
2264
2771
|
}
|
|
2265
|
-
|
|
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) {
|
|
2266
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
|
+
}
|
|
2267
2796
|
const upstreams = loadUpstreams();
|
|
2268
2797
|
const npmLatest = await latestNpmVersion("jorgex-stack");
|
|
2269
2798
|
if (npmLatest === null)
|
|
@@ -2288,6 +2817,13 @@ async function runUpdateCheck(localVersion) {
|
|
|
2288
2817
|
`engram: ${local} local, ${latest} disponible. Tu instalaci\xF3n NO se toca (D7) \u2014 actualiza t\xFA: github.com/${engramRepo}/releases`
|
|
2289
2818
|
);
|
|
2290
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
|
+
}
|
|
2291
2827
|
const checkSkillNames = skillsToScan(isGitClone(), upstreams);
|
|
2292
2828
|
if (checkSkillNames.length === 0) {
|
|
2293
2829
|
p4.log.info(
|
|
@@ -2347,7 +2883,7 @@ function isEngramRunning() {
|
|
|
2347
2883
|
const pgrep = lookPath("pgrep");
|
|
2348
2884
|
if (!pgrep) return null;
|
|
2349
2885
|
try {
|
|
2350
|
-
|
|
2886
|
+
execFileSync5(pgrep, ["-x", "engram"], { stdio: "ignore" });
|
|
2351
2887
|
return true;
|
|
2352
2888
|
} catch {
|
|
2353
2889
|
return false;
|
|
@@ -2357,8 +2893,8 @@ function isEngramRunning() {
|
|
|
2357
2893
|
return null;
|
|
2358
2894
|
}
|
|
2359
2895
|
}
|
|
2360
|
-
function isGitClone(projectRoot =
|
|
2361
|
-
return
|
|
2896
|
+
function isGitClone(projectRoot = path26.dirname(stackRoot())) {
|
|
2897
|
+
return fs20.existsSync(path26.join(projectRoot, ".git"));
|
|
2362
2898
|
}
|
|
2363
2899
|
var STACK_METHOD_CLONE = "git pull + pnpm install + pnpm build";
|
|
2364
2900
|
function resolvePnpm() {
|
|
@@ -2368,29 +2904,29 @@ function resolvePnpm() {
|
|
|
2368
2904
|
}
|
|
2369
2905
|
function cleanupTmp(dir) {
|
|
2370
2906
|
try {
|
|
2371
|
-
|
|
2907
|
+
fs20.rmSync(dir, { recursive: true, force: true });
|
|
2372
2908
|
} catch {
|
|
2373
2909
|
}
|
|
2374
2910
|
}
|
|
2375
2911
|
function updateStackGitClone() {
|
|
2376
|
-
const projectRoot =
|
|
2912
|
+
const projectRoot = path26.dirname(stackRoot());
|
|
2377
2913
|
const git = lookPath("git");
|
|
2378
2914
|
if (!git) throw new Error("git no encontrado en PATH.");
|
|
2379
2915
|
const pnpm = resolvePnpm();
|
|
2380
2916
|
p4.log.info("Ejecutando git pull\u2026");
|
|
2381
|
-
|
|
2917
|
+
execFileSync5(git, ["pull"], { cwd: projectRoot, stdio: "inherit" });
|
|
2382
2918
|
p4.log.info("Ejecutando pnpm install\u2026");
|
|
2383
|
-
|
|
2919
|
+
execFileSync5(pnpm, ["install"], { cwd: projectRoot, stdio: "inherit" });
|
|
2384
2920
|
p4.log.info("Ejecutando pnpm build\u2026");
|
|
2385
|
-
|
|
2921
|
+
execFileSync5(pnpm, ["build"], { cwd: projectRoot, stdio: "inherit" });
|
|
2386
2922
|
}
|
|
2387
2923
|
function updateStackGlobal() {
|
|
2388
2924
|
const pnpm = resolvePnpm();
|
|
2389
2925
|
p4.log.info("Ejecutando pnpm add -g jorgex-stack@latest\u2026");
|
|
2390
|
-
|
|
2926
|
+
execFileSync5(pnpm, ["add", "-g", "jorgex-stack@latest"], { stdio: "inherit" });
|
|
2391
2927
|
}
|
|
2392
2928
|
async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
2393
|
-
const root =
|
|
2929
|
+
const root = fs20.mkdtempSync(path26.join(os4.tmpdir(), "jorgex-skill-"));
|
|
2394
2930
|
try {
|
|
2395
2931
|
const result = await downloadRepoTarball(repo, sha, root, skillPath);
|
|
2396
2932
|
if (!result.ok) {
|
|
@@ -2398,15 +2934,15 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2398
2934
|
return { error: result.reason };
|
|
2399
2935
|
}
|
|
2400
2936
|
if (skillPath) {
|
|
2401
|
-
const sub =
|
|
2937
|
+
const sub = path26.resolve(path26.join(root, skillPath));
|
|
2402
2938
|
if (!isContainedIn(sub, root)) {
|
|
2403
2939
|
cleanupTmp(root);
|
|
2404
2940
|
return { error: `la ruta "${skillPath}" escapa del directorio temporal` };
|
|
2405
2941
|
}
|
|
2406
|
-
if (
|
|
2942
|
+
if (fs20.existsSync(sub)) return { dir: sub, root };
|
|
2407
2943
|
const lastSeg = skillPath.split("/").pop();
|
|
2408
|
-
const sub2 =
|
|
2409
|
-
if (isContainedIn(sub2, root) &&
|
|
2944
|
+
const sub2 = path26.resolve(path26.join(root, lastSeg));
|
|
2945
|
+
if (isContainedIn(sub2, root) && fs20.existsSync(sub2)) {
|
|
2410
2946
|
if (!validateExtractedTree(sub2)) {
|
|
2411
2947
|
cleanupTmp(root);
|
|
2412
2948
|
return { error: `el sub\xE1rbol "${lastSeg}" contiene symlinks o rutas fuera del destino` };
|
|
@@ -2425,11 +2961,11 @@ async function downloadSkillToTemp(repo, sha, skillPath) {
|
|
|
2425
2961
|
function pruneEngramDbBackups() {
|
|
2426
2962
|
try {
|
|
2427
2963
|
const dir = dataDir();
|
|
2428
|
-
if (!
|
|
2429
|
-
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);
|
|
2430
2966
|
for (const old of backups.slice(3)) {
|
|
2431
2967
|
try {
|
|
2432
|
-
|
|
2968
|
+
fs20.rmSync(path26.join(dir, old.name));
|
|
2433
2969
|
} catch {
|
|
2434
2970
|
}
|
|
2435
2971
|
}
|
|
@@ -2437,18 +2973,18 @@ function pruneEngramDbBackups() {
|
|
|
2437
2973
|
}
|
|
2438
2974
|
}
|
|
2439
2975
|
function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
2440
|
-
if (!
|
|
2441
|
-
const dir =
|
|
2442
|
-
const base =
|
|
2976
|
+
if (!fs20.existsSync(binPath)) return null;
|
|
2977
|
+
const dir = path26.dirname(binPath);
|
|
2978
|
+
const base = path26.basename(binPath);
|
|
2443
2979
|
const escapedBase = base.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
|
|
2444
2980
|
const oldPattern = new RegExp("^" + escapedBase + "\\.old-\\d+$");
|
|
2445
|
-
const resolvedDir =
|
|
2446
|
-
if (resolvedDir ===
|
|
2981
|
+
const resolvedDir = path26.resolve(dir);
|
|
2982
|
+
if (resolvedDir === path26.resolve(sweepRoot) || isContainedIn(resolvedDir, sweepRoot)) {
|
|
2447
2983
|
try {
|
|
2448
|
-
for (const entry of
|
|
2984
|
+
for (const entry of fs20.readdirSync(dir)) {
|
|
2449
2985
|
if (oldPattern.test(entry)) {
|
|
2450
2986
|
try {
|
|
2451
|
-
|
|
2987
|
+
fs20.rmSync(path26.join(dir, entry), { force: true });
|
|
2452
2988
|
} catch {
|
|
2453
2989
|
}
|
|
2454
2990
|
}
|
|
@@ -2456,8 +2992,8 @@ function rotateLockedBinary(binPath, sweepRoot = HOME) {
|
|
|
2456
2992
|
} catch {
|
|
2457
2993
|
}
|
|
2458
2994
|
}
|
|
2459
|
-
const rotated =
|
|
2460
|
-
|
|
2995
|
+
const rotated = path26.join(dir, `${base}.old-${Date.now()}`);
|
|
2996
|
+
fs20.renameSync(binPath, rotated);
|
|
2461
2997
|
return rotated;
|
|
2462
2998
|
}
|
|
2463
2999
|
async function updateEngram(engramRepo, latestVersion) {
|
|
@@ -2466,19 +3002,19 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2466
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)."
|
|
2467
3003
|
);
|
|
2468
3004
|
}
|
|
2469
|
-
const engramDataDir = process.env.ENGRAM_DATA_DIR ??
|
|
2470
|
-
const engramDb =
|
|
2471
|
-
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)) {
|
|
2472
3008
|
const doBackup = await p4.confirm({
|
|
2473
3009
|
message: `\xBFHacer backup de la DB de Engram antes de actualizar? (${engramDb})`,
|
|
2474
3010
|
initialValue: true
|
|
2475
3011
|
});
|
|
2476
3012
|
if (!p4.isCancel(doBackup) && doBackup) {
|
|
2477
3013
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2478
|
-
const dest =
|
|
3014
|
+
const dest = path26.join(dataDir(), `engram-db-backup-${ts}.db`);
|
|
2479
3015
|
try {
|
|
2480
|
-
if (!
|
|
2481
|
-
|
|
3016
|
+
if (!fs20.existsSync(dataDir())) fs20.mkdirSync(dataDir(), { recursive: true });
|
|
3017
|
+
fs20.copyFileSync(engramDb, dest);
|
|
2482
3018
|
p4.log.success(`DB respaldada en ${dest} (la DB original NO se modifica jam\xE1s).`);
|
|
2483
3019
|
pruneEngramDbBackups();
|
|
2484
3020
|
} catch (err) {
|
|
@@ -2491,7 +3027,7 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2491
3027
|
if (brew) {
|
|
2492
3028
|
let brewManages = false;
|
|
2493
3029
|
try {
|
|
2494
|
-
|
|
3030
|
+
execFileSync5(brew, ["list", "engram"], { stdio: "pipe" });
|
|
2495
3031
|
brewManages = true;
|
|
2496
3032
|
} catch {
|
|
2497
3033
|
}
|
|
@@ -2499,7 +3035,7 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2499
3035
|
anyChannelTried = true;
|
|
2500
3036
|
p4.log.info("Actualizando engram con brew\u2026");
|
|
2501
3037
|
try {
|
|
2502
|
-
|
|
3038
|
+
execFileSync5(brew, ["upgrade", "engram"], { stdio: "inherit" });
|
|
2503
3039
|
return true;
|
|
2504
3040
|
} catch (err) {
|
|
2505
3041
|
p4.log.error(`brew upgrade engram fall\xF3: ${err instanceof Error ? err.message : err}`);
|
|
@@ -2523,15 +3059,15 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2523
3059
|
}
|
|
2524
3060
|
p4.log.info(`Actualizando engram con go install (${latestVersion})\u2026`);
|
|
2525
3061
|
try {
|
|
2526
|
-
|
|
3062
|
+
execFileSync5(
|
|
2527
3063
|
go,
|
|
2528
3064
|
["install", `github.com/Gentleman-Programming/engram/cmd/engram@v${latestVersion}`],
|
|
2529
3065
|
{ stdio: "inherit" }
|
|
2530
3066
|
);
|
|
2531
|
-
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists:
|
|
3067
|
+
const rollbackOk = resolveEngramRollback({ installOk: true, rotated, bin, binExists: fs20.existsSync(bin ?? "") });
|
|
2532
3068
|
if (rollbackOk.action === "restore") {
|
|
2533
3069
|
try {
|
|
2534
|
-
|
|
3070
|
+
fs20.renameSync(rotated, bin);
|
|
2535
3071
|
p4.log.warn(rollbackOk.messages.onRestore);
|
|
2536
3072
|
} catch {
|
|
2537
3073
|
p4.log.warn(rollbackOk.messages.onRenameFail);
|
|
@@ -2539,10 +3075,10 @@ async function updateEngram(engramRepo, latestVersion) {
|
|
|
2539
3075
|
}
|
|
2540
3076
|
return true;
|
|
2541
3077
|
} catch (err) {
|
|
2542
|
-
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists:
|
|
3078
|
+
const rollbackFail = resolveEngramRollback({ installOk: false, rotated, bin, binExists: fs20.existsSync(bin ?? "") });
|
|
2543
3079
|
if (rollbackFail.action === "restore") {
|
|
2544
3080
|
try {
|
|
2545
|
-
|
|
3081
|
+
fs20.renameSync(rotated, bin);
|
|
2546
3082
|
p4.log.info(rollbackFail.messages.onRestore);
|
|
2547
3083
|
} catch {
|
|
2548
3084
|
p4.log.error(rollbackFail.messages.onRenameFail);
|
|
@@ -2627,14 +3163,26 @@ function buildEligibleSkillUpdates(skills) {
|
|
|
2627
3163
|
}
|
|
2628
3164
|
return result;
|
|
2629
3165
|
}
|
|
2630
|
-
|
|
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) {
|
|
2631
3170
|
if (dryRun || yes || !process.stdout.isTTY) {
|
|
2632
|
-
return { exitCode: await runUpdateCheck(localVersion), appliedUpdates: false };
|
|
3171
|
+
return { exitCode: await runUpdateCheck(localVersion, includeBrowserState), appliedUpdates: false, syncRequired: false };
|
|
2633
3172
|
}
|
|
2634
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
|
+
}
|
|
2635
3182
|
const upstreams = loadUpstreams();
|
|
2636
3183
|
let exitCode = 0;
|
|
2637
3184
|
let appliedUpdates = false;
|
|
3185
|
+
const updated = [];
|
|
2638
3186
|
const maintainer = isGitClone();
|
|
2639
3187
|
const spin = p4.spinner();
|
|
2640
3188
|
spin.start("Consultando versiones upstream\u2026");
|
|
@@ -2693,6 +3241,21 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2693
3241
|
});
|
|
2694
3242
|
}
|
|
2695
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
|
+
}
|
|
2696
3259
|
const typedSkillHeads = skillHeads;
|
|
2697
3260
|
const loggedRepos = /* @__PURE__ */ new Map();
|
|
2698
3261
|
for (const { name, repo, info, head } of typedSkillHeads) {
|
|
@@ -2734,7 +3297,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2734
3297
|
}
|
|
2735
3298
|
if (updateItems.length === 0) {
|
|
2736
3299
|
p4.outro("Todo al d\xEDa. No hay actualizaciones disponibles.");
|
|
2737
|
-
return { exitCode: 0, appliedUpdates: false };
|
|
3300
|
+
return { exitCode: 0, appliedUpdates: false, syncRequired: false };
|
|
2738
3301
|
}
|
|
2739
3302
|
const selected = await p4.multiselect({
|
|
2740
3303
|
message: "Selecciona qu\xE9 actualizar (espacio para marcar, intro para confirmar):",
|
|
@@ -2744,11 +3307,11 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2744
3307
|
});
|
|
2745
3308
|
if (p4.isCancel(selected)) {
|
|
2746
3309
|
p4.outro("Update cancelado.");
|
|
2747
|
-
return { exitCode: 0, appliedUpdates: false };
|
|
3310
|
+
return { exitCode: 0, appliedUpdates: false, syncRequired: false };
|
|
2748
3311
|
}
|
|
2749
3312
|
if (selected.length === 0) {
|
|
2750
3313
|
p4.outro("Nada seleccionado.");
|
|
2751
|
-
return { exitCode: 0, appliedUpdates: false };
|
|
3314
|
+
return { exitCode: 0, appliedUpdates: false, syncRequired: false };
|
|
2752
3315
|
}
|
|
2753
3316
|
const sel = selected;
|
|
2754
3317
|
if (stackNeedsUpdate && sel.includes("stack")) {
|
|
@@ -2769,6 +3332,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2769
3332
|
}
|
|
2770
3333
|
p4.log.success("Stack actualizado correctamente.");
|
|
2771
3334
|
appliedUpdates = true;
|
|
3335
|
+
updated.push("stack");
|
|
2772
3336
|
} catch (err) {
|
|
2773
3337
|
p4.log.error(`Stack: error al actualizar \u2014 ${err instanceof Error ? err.message : err}`);
|
|
2774
3338
|
exitCode = 1;
|
|
@@ -2800,7 +3364,7 @@ async function runInteractiveUpdate(localVersion, yes, dryRun = false) {
|
|
|
2800
3364
|
continue;
|
|
2801
3365
|
}
|
|
2802
3366
|
const { dir: tmpDir, root: tmpRoot } = tmpResult;
|
|
2803
|
-
const localSkillDir =
|
|
3367
|
+
const localSkillDir = path26.join(stackRoot(), "skills", skillInfo.name);
|
|
2804
3368
|
const diff = renderSkillDiff(tmpDir, localSkillDir);
|
|
2805
3369
|
if (diff) {
|
|
2806
3370
|
p4.log.info(`Diff de ${skillInfo.name}:
|
|
@@ -2824,6 +3388,7 @@ ${diff}`);
|
|
|
2824
3388
|
replaceSkill(skillInfo.name, tmpDir, skillInfo.head, {});
|
|
2825
3389
|
p4.log.success(`skill ${skillInfo.name}: actualizada y re-pineada a ${skillInfo.head.slice(0, 7)}.`);
|
|
2826
3390
|
appliedUpdates = true;
|
|
3391
|
+
updated.push("skill");
|
|
2827
3392
|
} catch (err) {
|
|
2828
3393
|
p4.log.error(`skill ${skillInfo.name}: error \u2014 ${err instanceof Error ? err.message : err}`);
|
|
2829
3394
|
exitCode = 1;
|
|
@@ -2831,6 +3396,27 @@ ${diff}`);
|
|
|
2831
3396
|
cleanupTmp(tmpRoot);
|
|
2832
3397
|
}
|
|
2833
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
|
+
}
|
|
2834
3420
|
if (engramNeedsUpdate && sel.includes("engram") && engramData?.version) {
|
|
2835
3421
|
const confirmEngram = await p4.confirm({
|
|
2836
3422
|
message: `Actualizar engram a v${engramData.version} (canal nativo: brew \u2192 go install \u2192 URL)`,
|
|
@@ -2851,15 +3437,17 @@ ${diff}`);
|
|
|
2851
3437
|
p4.log.warn("Engram actualizado, pero no se pudo verificar la versi\xF3n \u2014 comprueba con engram --version");
|
|
2852
3438
|
}
|
|
2853
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");
|
|
2854
3442
|
}
|
|
2855
3443
|
}
|
|
2856
3444
|
}
|
|
2857
3445
|
p4.outro(exitCode === 0 ? "Update completado." : "Update completado con errores (revisa arriba).");
|
|
2858
|
-
return { exitCode, appliedUpdates };
|
|
3446
|
+
return { exitCode, appliedUpdates, syncRequired: resolveUpdateSyncRequired(updated) };
|
|
2859
3447
|
}
|
|
2860
3448
|
|
|
2861
3449
|
// src/models-picker.ts
|
|
2862
|
-
import
|
|
3450
|
+
import path27 from "path";
|
|
2863
3451
|
import * as p5 from "@clack/prompts";
|
|
2864
3452
|
var TIERS = ["strong", "standard", "cheap"];
|
|
2865
3453
|
var EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
@@ -2883,7 +3471,7 @@ function opencodeLiveModels(binPath) {
|
|
|
2883
3471
|
}
|
|
2884
3472
|
function agentsByTier() {
|
|
2885
3473
|
const grouped = { strong: [], standard: [], cheap: [] };
|
|
2886
|
-
for (const agent of loadCanonicalAgents(
|
|
3474
|
+
for (const agent of loadCanonicalAgents(path27.join(stackRoot(), "agents"))) {
|
|
2887
3475
|
if (agent.mode === "subagent") grouped[agent.tier].push(agent.name);
|
|
2888
3476
|
}
|
|
2889
3477
|
return grouped;
|
|
@@ -3063,16 +3651,16 @@ function cancelled() {
|
|
|
3063
3651
|
}
|
|
3064
3652
|
|
|
3065
3653
|
// src/lib/release.ts
|
|
3066
|
-
import
|
|
3067
|
-
import
|
|
3068
|
-
import { execFileSync as
|
|
3654
|
+
import fs21 from "fs";
|
|
3655
|
+
import path28 from "path";
|
|
3656
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
3069
3657
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3070
3658
|
function findPackageJson() {
|
|
3071
|
-
let dir =
|
|
3659
|
+
let dir = path28.dirname(fileURLToPath2(import.meta.url));
|
|
3072
3660
|
for (let i = 0; i < 6; i++) {
|
|
3073
|
-
const candidate =
|
|
3074
|
-
if (
|
|
3075
|
-
dir =
|
|
3661
|
+
const candidate = path28.join(dir, "package.json");
|
|
3662
|
+
if (fs21.existsSync(candidate)) return candidate;
|
|
3663
|
+
dir = path28.dirname(dir);
|
|
3076
3664
|
}
|
|
3077
3665
|
throw new Error("No se encontr\xF3 package.json cerca del CLI.");
|
|
3078
3666
|
}
|
|
@@ -3081,7 +3669,7 @@ function readPackageVersion() {
|
|
|
3081
3669
|
}
|
|
3082
3670
|
function readPackageMetadata() {
|
|
3083
3671
|
const packageJson = findPackageJson();
|
|
3084
|
-
const raw =
|
|
3672
|
+
const raw = fs21.readFileSync(packageJson, "utf8");
|
|
3085
3673
|
const parsed = JSON.parse(raw);
|
|
3086
3674
|
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
3087
3675
|
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
@@ -3118,6 +3706,10 @@ function parseFlags(args) {
|
|
|
3118
3706
|
list: false,
|
|
3119
3707
|
check: false,
|
|
3120
3708
|
removeEngram: false,
|
|
3709
|
+
playwright: false,
|
|
3710
|
+
removePlaywright: false,
|
|
3711
|
+
devtools: false,
|
|
3712
|
+
noDevtools: false,
|
|
3121
3713
|
positional: [],
|
|
3122
3714
|
unknownFlags: []
|
|
3123
3715
|
};
|
|
@@ -3155,6 +3747,10 @@ function parseFlags(args) {
|
|
|
3155
3747
|
else if (arg === "--list") flags.list = true;
|
|
3156
3748
|
else if (arg === "--check") flags.check = true;
|
|
3157
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;
|
|
3158
3754
|
else if (arg.startsWith("-")) flags.unknownFlags.push(arg);
|
|
3159
3755
|
else flags.positional.push(arg);
|
|
3160
3756
|
}
|
|
@@ -3221,6 +3817,48 @@ Corrige o borra ${preferenceFile}, o vuelve a ejecutar con --mode human|programm
|
|
|
3221
3817
|
subagentConcurrency: concurrency
|
|
3222
3818
|
};
|
|
3223
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
|
+
}
|
|
3224
3862
|
function parseCliArgs(argv) {
|
|
3225
3863
|
const [first, ...rest] = argv;
|
|
3226
3864
|
const isCommand = COMMANDS.includes(first ?? "install");
|
|
@@ -3275,8 +3913,13 @@ Opciones:
|
|
|
3275
3913
|
--target-dir <dir> Dir alternativo (pruebas de paridad; requiere 1 runtime)
|
|
3276
3914
|
--dry-run Muestra el plan sin escribir nada
|
|
3277
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)
|
|
3278
3919
|
--remove-engram (uninstall) desregistra Engram de los runtimes;
|
|
3279
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
|
|
3280
3923
|
|
|
3281
3924
|
Ver PRD.md para el dise\xF1o completo.`);
|
|
3282
3925
|
}
|
|
@@ -3320,17 +3963,31 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3320
3963
|
process.exitCode = 1;
|
|
3321
3964
|
return;
|
|
3322
3965
|
}
|
|
3323
|
-
|
|
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) {
|
|
3324
3972
|
process.exitCode = 1;
|
|
3325
3973
|
return;
|
|
3326
3974
|
}
|
|
3327
|
-
|
|
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;
|
|
3328
3985
|
return;
|
|
3329
3986
|
}
|
|
3330
3987
|
case "uninstall": {
|
|
3331
3988
|
const runtimes = await resolveRuntimes(flags);
|
|
3332
3989
|
if (runtimes === null) return;
|
|
3333
|
-
if (runtimes.length === 0) {
|
|
3990
|
+
if (runtimes.length === 0 && !flags.removePlaywright) {
|
|
3334
3991
|
console.error("Ning\xFAn runtime detectado (opencode, claude-code, codex).");
|
|
3335
3992
|
process.exitCode = 1;
|
|
3336
3993
|
return;
|
|
@@ -3340,7 +3997,8 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3340
3997
|
targetDir: flags.targetDir,
|
|
3341
3998
|
dryRun: flags.dryRun,
|
|
3342
3999
|
yes: flags.yes,
|
|
3343
|
-
removeEngram: flags.removeEngram
|
|
4000
|
+
removeEngram: flags.removeEngram,
|
|
4001
|
+
removePlaywright: flags.removePlaywright
|
|
3344
4002
|
});
|
|
3345
4003
|
return;
|
|
3346
4004
|
}
|
|
@@ -3350,11 +4008,11 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3350
4008
|
}
|
|
3351
4009
|
case "update": {
|
|
3352
4010
|
if (flags.check) {
|
|
3353
|
-
process.exitCode = await runUpdateCheck(VERSION);
|
|
4011
|
+
process.exitCode = await runUpdateCheck(VERSION, flags.targetDir === void 0);
|
|
3354
4012
|
return;
|
|
3355
4013
|
}
|
|
3356
4014
|
if (flags.dryRun) {
|
|
3357
|
-
process.exitCode = await runUpdateCheck(VERSION);
|
|
4015
|
+
process.exitCode = await runUpdateCheck(VERSION, flags.targetDir === void 0);
|
|
3358
4016
|
return;
|
|
3359
4017
|
}
|
|
3360
4018
|
const runtimes = await resolveRuntimes(flags);
|
|
@@ -3381,11 +4039,16 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3381
4039
|
} else if (runtimes.length > 0) {
|
|
3382
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.");
|
|
3383
4041
|
}
|
|
3384
|
-
const result = await runInteractiveUpdate(
|
|
4042
|
+
const result = await runInteractiveUpdate(
|
|
4043
|
+
VERSION,
|
|
4044
|
+
flags.yes,
|
|
4045
|
+
flags.dryRun,
|
|
4046
|
+
flags.targetDir === void 0
|
|
4047
|
+
);
|
|
3385
4048
|
process.exitCode = result.exitCode;
|
|
3386
|
-
if (result.
|
|
4049
|
+
if (result.syncRequired && runtimes.length > 0 && (result.exitCode !== 0 || !canSync)) {
|
|
3387
4050
|
p6.log.warn("Skills/stack actualizados, pero el sync con los runtimes sigue pendiente. Ejecuta jorgex-stack sync --mode human|programmatic.");
|
|
3388
|
-
} else if (result.exitCode === 0 && result.
|
|
4051
|
+
} else if (result.exitCode === 0 && result.syncRequired && runtimes.length > 0 && canSync && !flags.yes && process.stdout.isTTY) {
|
|
3389
4052
|
const apply = await p6.confirm({ message: "\xBFRe-aplicar a los runtimes ahora? (sync)" });
|
|
3390
4053
|
if (!p6.isCancel(apply) && apply) {
|
|
3391
4054
|
process.exitCode = await runInstall({
|
|
@@ -3398,7 +4061,7 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3398
4061
|
} else {
|
|
3399
4062
|
console.log("Sin aplicar. Cuando quieras: jorgex-stack sync");
|
|
3400
4063
|
}
|
|
3401
|
-
} else if (result.exitCode === 0 && result.
|
|
4064
|
+
} else if (result.exitCode === 0 && result.syncRequired && runtimes.length > 0 && canSync && (flags.yes || !process.stdout.isTTY)) {
|
|
3402
4065
|
console.log("Skills/stack actualizados. Ejecuta jorgex-stack sync para aplicarlos a los runtimes.");
|
|
3403
4066
|
}
|
|
3404
4067
|
return;
|
|
@@ -3448,7 +4111,7 @@ Flags disponibles: jorgex-stack --help`
|
|
|
3448
4111
|
}
|
|
3449
4112
|
}
|
|
3450
4113
|
if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL2(process.argv[1]).href) {
|
|
3451
|
-
main().catch((err) => {
|
|
4114
|
+
await main().catch((err) => {
|
|
3452
4115
|
console.error(err instanceof Error ? err.message : String(err));
|
|
3453
4116
|
process.exitCode = 1;
|
|
3454
4117
|
});
|