jorgex-stack 1.9.62 → 1.9.63
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 +17 -30
- package/dist/browser-playwright.d.ts +2 -0
- package/dist/browser-playwright.js +955 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +9616 -1827
- package/dist/pi-ci-artifact.js +341 -35
- package/dist/quality-verifier.js +399 -29
- package/package.json +3 -2
- package/stack/mcp/servers.json +1 -1
- package/stack/system-prompt/browser-playwright.md +1 -1
- package/upstreams.json +8 -8
- package/dist/chunk-2CBCBOP7.js +0 -245
- package/dist/chunk-4USZMJ3I.js +0 -554
- package/dist/chunk-66WVKOU4.js +0 -693
- package/dist/chunk-B76ZJXXD.js +0 -989
- package/dist/chunk-BSHTDTJL.js +0 -781
- package/dist/chunk-KGL5VNVZ.js +0 -305
- package/dist/chunk-KKN4OYHD.js +0 -704
- package/dist/chunk-N5IC2JBJ.js +0 -467
- package/dist/chunk-NQZVJFV2.js +0 -33
- package/dist/claude-code-BBHLN5TD.js +0 -12
- package/dist/codex-ALGP2YBC.js +0 -12
- package/dist/official-engram-setup-4WIXM4EF.js +0 -34
- package/dist/opencode-VG2IZRRF.js +0 -32
- package/dist/pi-CKFSPGJU.js +0 -13
package/dist/chunk-BSHTDTJL.js
DELETED
|
@@ -1,781 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
createLocalCapabilityReport,
|
|
4
|
-
detectOpenCode,
|
|
5
|
-
hasManagedMarkdownSection,
|
|
6
|
-
hookScriptNames,
|
|
7
|
-
isCanonicalMcpServerEnabled,
|
|
8
|
-
loadCanonicalDefaults,
|
|
9
|
-
removeSystemPromptSections,
|
|
10
|
-
resolveAgentModel,
|
|
11
|
-
upsertJson
|
|
12
|
-
} from "./chunk-66WVKOU4.js";
|
|
13
|
-
import {
|
|
14
|
-
readTextIfExists,
|
|
15
|
-
registerOfficialSetupVerifier
|
|
16
|
-
} from "./chunk-B76ZJXXD.js";
|
|
17
|
-
import {
|
|
18
|
-
HOME,
|
|
19
|
-
samePath,
|
|
20
|
-
stackRoot
|
|
21
|
-
} from "./chunk-NQZVJFV2.js";
|
|
22
|
-
|
|
23
|
-
// src/adapters/opencode.ts
|
|
24
|
-
import path from "path";
|
|
25
|
-
import fs from "fs";
|
|
26
|
-
import { isDeepStrictEqual } from "util";
|
|
27
|
-
import { pathToFileURL } from "url";
|
|
28
|
-
var gitReadPrefix = "git --no-pager -c core.fsmonitor=false -c log.showSignature=false";
|
|
29
|
-
var gitReadCommands = [
|
|
30
|
-
"diff",
|
|
31
|
-
"diff --stat",
|
|
32
|
-
"diff --name-only",
|
|
33
|
-
"diff --cached",
|
|
34
|
-
"log",
|
|
35
|
-
"log --oneline -10"
|
|
36
|
-
].map((action) => `${gitReadPrefix} ${action} --no-ext-diff --no-textconv --end-of-options`);
|
|
37
|
-
function yamlString(value) {
|
|
38
|
-
return JSON.stringify(value);
|
|
39
|
-
}
|
|
40
|
-
function readMcpConfig(file) {
|
|
41
|
-
let content;
|
|
42
|
-
try {
|
|
43
|
-
content = fs.readFileSync(file, "utf8");
|
|
44
|
-
} catch (error) {
|
|
45
|
-
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
46
|
-
if (code === "ENOENT") return null;
|
|
47
|
-
throw new Error(`OpenCode: no se pudo leer la configuraci\xF3n MCP en ${file} (${code}).`);
|
|
48
|
-
}
|
|
49
|
-
if (content.trim() !== "") {
|
|
50
|
-
try {
|
|
51
|
-
if (objectValue(JSON.parse(content)) === null) throw new Error();
|
|
52
|
-
} catch {
|
|
53
|
-
throw new Error(`OpenCode: la configuraci\xF3n MCP en ${file} debe contener un objeto JSON v\xE1lido.`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return content;
|
|
57
|
-
}
|
|
58
|
-
function isManagedOptionalStdioServer(server, value) {
|
|
59
|
-
if (!server.optional || server.transport !== "stdio" || value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
const current = value;
|
|
63
|
-
const expectedCommand = [server.command, ...server.args ?? []];
|
|
64
|
-
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]);
|
|
65
|
-
}
|
|
66
|
-
var PRIMARY_MODEL = "openai/gpt-5.6-sol";
|
|
67
|
-
var PRIMARY_MODEL_ID = "gpt-5.6-sol";
|
|
68
|
-
var PRIMARY_LIMITS = { context: 872e3, input: 744e3, output: 128e3 };
|
|
69
|
-
var PRIMARY_MODEL_FIELD = "model";
|
|
70
|
-
var PRIMARY_PROVIDER_FIELD = "provider";
|
|
71
|
-
var PRIMARY_OPENAI_FIELD = "provider.openai";
|
|
72
|
-
var PRIMARY_MODELS_FIELD = "provider.openai.models";
|
|
73
|
-
var PRIMARY_SOL_FIELD = `provider.openai.models.${PRIMARY_MODEL_ID}`;
|
|
74
|
-
var PRIMARY_LIMIT_PREFIX = `provider.openai.models.${PRIMARY_MODEL_ID}.limit`;
|
|
75
|
-
function objectValue(value) {
|
|
76
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
77
|
-
}
|
|
78
|
-
function isCompatibleContext7Server(server, value) {
|
|
79
|
-
const current = objectValue(value);
|
|
80
|
-
if (server.transport !== "http" || typeof server.url !== "string" || current === null) return false;
|
|
81
|
-
return current.type === "remote" && current.url === server.url && (current.enabled === void 0 || current.enabled === true);
|
|
82
|
-
}
|
|
83
|
-
function canonicalContext7Server(server) {
|
|
84
|
-
const headers = {};
|
|
85
|
-
for (const [key, raw] of Object.entries(server.headers ?? {})) {
|
|
86
|
-
const envRef = /^\$\{(\w+)\}$/.exec(raw);
|
|
87
|
-
headers[key] = envRef ? `{env:${envRef[1]}}` : raw;
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
type: "remote",
|
|
91
|
-
url: server.url,
|
|
92
|
-
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
function isCanonicalContext7Server(server, value) {
|
|
96
|
-
return isCompatibleContext7Server(server, value) && isDeepStrictEqual(value, canonicalContext7Server(server));
|
|
97
|
-
}
|
|
98
|
-
function assertCompatibleContext7(server, value) {
|
|
99
|
-
if (value !== void 0 && !isCompatibleContext7Server(server, value)) {
|
|
100
|
-
throw new Error("OpenCode: MCP 'context7' entra en conflicto con una definici\xF3n existente (endpoint, tipo o estado nativo incompatible). Conserva la configuraci\xF3n y corrige el conflicto antes de reintentar.");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function ensureObject(parent, key, fieldPath) {
|
|
104
|
-
if (parent[key] === void 0) parent[key] = {};
|
|
105
|
-
const value = objectValue(parent[key]);
|
|
106
|
-
if (value === null) throw new Error(`OpenCode: '${fieldPath}' debe ser un objeto; corr\xEDgelo antes de reintentar sync.`);
|
|
107
|
-
return value;
|
|
108
|
-
}
|
|
109
|
-
function ensureOwnedPrimaryObject(parent, key, field, owned, changes) {
|
|
110
|
-
const created = parent[key] === void 0;
|
|
111
|
-
const value = ensureObject(parent, key, field);
|
|
112
|
-
if (created && owned?.has(field) !== true) changes.push({ field, owned: true });
|
|
113
|
-
return value;
|
|
114
|
-
}
|
|
115
|
-
function pruneEmpty(parent, key) {
|
|
116
|
-
const value = objectValue(parent[key]);
|
|
117
|
-
if (value !== null && Object.keys(value).length === 0) delete parent[key];
|
|
118
|
-
}
|
|
119
|
-
function hasOpenCodeManualApproval(configDir) {
|
|
120
|
-
const content = readTextIfExists(path.join(configDir, "opencode.json"));
|
|
121
|
-
if (content === null) return false;
|
|
122
|
-
try {
|
|
123
|
-
const root = JSON.parse(content);
|
|
124
|
-
const permission = objectValue(objectValue(root)?.permission);
|
|
125
|
-
const expected = loadCanonicalDefaults(stackRoot())["opencode"]?.["permission"];
|
|
126
|
-
return permission !== null && expected !== void 0 && isDeepStrictEqual(permission, expected);
|
|
127
|
-
} catch {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
var opencodeAdapter = {
|
|
132
|
-
id: "opencode",
|
|
133
|
-
name: "OpenCode",
|
|
134
|
-
excludedPluginBasenames: ["engram.ts"],
|
|
135
|
-
detect: detectOpenCode,
|
|
136
|
-
reportCapabilities(configDir) {
|
|
137
|
-
const prompt = readTextIfExists(path.join(configDir, "AGENTS.md"));
|
|
138
|
-
return createLocalCapabilityReport("opencode", [
|
|
139
|
-
...hasManagedMarkdownSection(prompt, "system-prompt") ? [{
|
|
140
|
-
id: "policy-guidance",
|
|
141
|
-
state: "prompt-only",
|
|
142
|
-
reason: "The managed policy prompt is advisory and cannot enforce the policy",
|
|
143
|
-
evidence: { source: "jorgex-stack-system-prompt", version: "1" }
|
|
144
|
-
}] : [],
|
|
145
|
-
...hasOpenCodeManualApproval(configDir) ? [{
|
|
146
|
-
id: "tool-approval",
|
|
147
|
-
state: "manual",
|
|
148
|
-
reason: "Canonical approval declarations require a human decision; runtime activation is not certified",
|
|
149
|
-
evidence: { source: "jorgex-stack-opencode-approval-policy", version: "1" }
|
|
150
|
-
}] : []
|
|
151
|
-
]);
|
|
152
|
-
},
|
|
153
|
-
paths(configDir) {
|
|
154
|
-
const isRealConfigDir = samePath(
|
|
155
|
-
configDir,
|
|
156
|
-
process.env.OPENCODE_CONFIG_DIR ?? path.join(HOME, ".config", "opencode")
|
|
157
|
-
);
|
|
158
|
-
const agentsHome = isRealConfigDir ? HOME : path.dirname(configDir);
|
|
159
|
-
return {
|
|
160
|
-
systemPromptFile: path.join(configDir, "AGENTS.md"),
|
|
161
|
-
agentsDir: path.join(configDir, "agents"),
|
|
162
|
-
skillsDir: path.join(agentsHome, ".agents", "skills"),
|
|
163
|
-
commandsDir: path.join(configDir, "commands"),
|
|
164
|
-
pluginsDir: path.join(configDir, "plugins"),
|
|
165
|
-
scriptsDir: path.join(configDir, "scripts"),
|
|
166
|
-
outputStylesDir: null,
|
|
167
|
-
profilesDir: null
|
|
168
|
-
};
|
|
169
|
-
},
|
|
170
|
-
renderAgent(agent, models) {
|
|
171
|
-
const lines = [`description: ${yamlString(agent.description)}`, `mode: ${agent.mode}`];
|
|
172
|
-
if (agent.mode === "subagent") {
|
|
173
|
-
const tierModel = resolveAgentModel(models, agent.name, agent.tier);
|
|
174
|
-
lines.push(`model: ${tierModel.model}`);
|
|
175
|
-
if (tierModel.variant) lines.push(`variant: ${tierModel.variant}`);
|
|
176
|
-
if (agent.readonly || agent.bash !== "full" || !agent.spawn) lines.push("permission:");
|
|
177
|
-
if (agent.readonly) lines.push(" edit: deny");
|
|
178
|
-
if (agent.bash === "none") lines.push(" bash: deny");
|
|
179
|
-
else if (agent.bash === "git-read") {
|
|
180
|
-
lines.push(' bash:\n "*": deny');
|
|
181
|
-
for (const command of gitReadCommands) {
|
|
182
|
-
lines.push(` ${yamlString(command)}: allow`, ` ${yamlString(`${command} *`)}: allow`);
|
|
183
|
-
}
|
|
184
|
-
const permission = objectValue(loadCanonicalDefaults(stackRoot())["opencode"]?.permission);
|
|
185
|
-
const bash = objectValue(permission?.bash);
|
|
186
|
-
if (bash === null) throw new Error("OpenCode: canonical Bash policy is required for git-read agents.");
|
|
187
|
-
for (const [pattern, decision] of Object.entries(bash)) {
|
|
188
|
-
if (decision === "deny") lines.push(` ${yamlString(pattern)}: deny`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (!agent.spawn) lines.push(" task: deny");
|
|
192
|
-
}
|
|
193
|
-
return [
|
|
194
|
-
{
|
|
195
|
-
file: `${agent.name}.md`,
|
|
196
|
-
content: `---
|
|
197
|
-
${lines.join("\n")}
|
|
198
|
-
---
|
|
199
|
-
${agent.body}${agent.bash === "git-read" ? `
|
|
200
|
-
|
|
201
|
-
Use only these read-only Git command prefixes; put refs and paths after --end-of-options:
|
|
202
|
-
${gitReadCommands.map((command) => `- \`${command}\``).join("\n")}
|
|
203
|
-
` : ""}`,
|
|
204
|
-
kind: "agent"
|
|
205
|
-
}
|
|
206
|
-
];
|
|
207
|
-
},
|
|
208
|
-
renderCommand(file, content) {
|
|
209
|
-
return { file, content: content.replace(/\{\{input\}\}/g, "$ARGUMENTS") };
|
|
210
|
-
},
|
|
211
|
-
planHooks(canonical, ctx) {
|
|
212
|
-
const actions = [];
|
|
213
|
-
const { scriptsDir } = this.paths(ctx.configDir);
|
|
214
|
-
const bashEntries = {};
|
|
215
|
-
for (const [event, entries] of Object.entries(canonical.hooks)) {
|
|
216
|
-
if (event !== "PostToolUse") {
|
|
217
|
-
ctx.warnings.push(`opencode: evento de hook '${event}' a\xFAn no soportado por el puente \u2014 omitido.`);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
for (const entry of entries) {
|
|
221
|
-
if (!(entry.matcher ?? "").split("|").some((p) => p.trim().toLowerCase() === "bash")) {
|
|
222
|
-
ctx.warnings.push(`opencode: matcher de hook '${entry.matcher}' no soportado \u2014 omitido.`);
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
const includes = entry["x-command-includes"] ?? "*";
|
|
226
|
-
for (const hook of entry.hooks) {
|
|
227
|
-
const match = /\{\{SCRIPTS_DIR\}\}[/\\]([\w./\\-]+)/.exec(hook.command);
|
|
228
|
-
if (!match) {
|
|
229
|
-
ctx.warnings.push(`opencode: hook sin {{SCRIPTS_DIR}} no traducible: ${hook.command}`);
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
const script = `scripts/${path.basename(match[1])}`;
|
|
233
|
-
(bashEntries[includes] ??= []).push(script);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
const hooksFile = path.join(ctx.configDir, "hooks.json");
|
|
238
|
-
const content = upsertJson(readTextIfExists(hooksFile), (root) => {
|
|
239
|
-
const afterValue = root["tool.execute.after"] ??= {};
|
|
240
|
-
if (afterValue === null || typeof afterValue !== "object" || Array.isArray(afterValue)) {
|
|
241
|
-
ctx.warnings.push("opencode: tool.execute.after no es un objeto; hooks gestionados omitidos.");
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
const after = afterValue;
|
|
245
|
-
const bashValue = after["bash"];
|
|
246
|
-
if (bashValue !== void 0 && !Array.isArray(bashValue) && (bashValue === null || typeof bashValue !== "object")) {
|
|
247
|
-
ctx.warnings.push("opencode: tool.execute.after.bash no es un array ni un mapa; hooks gestionados omitidos.");
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
const bash = Array.isArray(bashValue) ? { "*": bashValue } : bashValue ?? {};
|
|
251
|
-
after["bash"] = bash;
|
|
252
|
-
const managedScripts = new Set(Object.values(bashEntries).flat());
|
|
253
|
-
for (const [includes, scripts] of Object.entries(bash)) {
|
|
254
|
-
if (!Array.isArray(scripts)) continue;
|
|
255
|
-
const preserved = scripts.filter(
|
|
256
|
-
(script) => typeof script !== "string" || !managedScripts.has(script)
|
|
257
|
-
);
|
|
258
|
-
if (preserved.length > 0) bash[includes] = preserved;
|
|
259
|
-
else delete bash[includes];
|
|
260
|
-
}
|
|
261
|
-
for (const [includes, scripts] of Object.entries(bashEntries)) {
|
|
262
|
-
const current = bash[includes];
|
|
263
|
-
if (current !== void 0 && !Array.isArray(current)) {
|
|
264
|
-
ctx.warnings.push(`opencode: trigger bash '${includes}' no es un array; hook gestionado omitido.`);
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const list = bash[includes] ??= [];
|
|
268
|
-
for (const s of scripts) if (!list.includes(s)) list.push(s);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
actions.push({ kind: "write", target: hooksFile, content });
|
|
272
|
-
const scriptsSource = path.join(ctx.stackDir, "scripts");
|
|
273
|
-
if (fs.existsSync(scriptsSource)) {
|
|
274
|
-
for (const f of fs.readdirSync(scriptsSource)) {
|
|
275
|
-
actions.push({ kind: "copy", source: path.join(scriptsSource, f), target: path.join(scriptsDir, f) });
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return actions;
|
|
279
|
-
},
|
|
280
|
-
planMainConfig(canonical, ctx) {
|
|
281
|
-
const file = path.join(ctx.configDir, "opencode.json");
|
|
282
|
-
const { pluginsDir } = this.paths(ctx.configDir);
|
|
283
|
-
const original = readMcpConfig(file);
|
|
284
|
-
const contentSource = original === null || original.trim() === "" ? null : original;
|
|
285
|
-
const isFreshConfig = contentSource === null;
|
|
286
|
-
const mcpOwnership = [];
|
|
287
|
-
const primaryModelOwnership = [];
|
|
288
|
-
const content = upsertJson(contentSource, (root) => {
|
|
289
|
-
const rawMcp = root["mcp"];
|
|
290
|
-
if (rawMcp !== void 0 && objectValue(rawMcp) === null) {
|
|
291
|
-
throw new Error("OpenCode: la clave 'mcp' debe ser un objeto; corr\xEDgela antes de reintentar sync.");
|
|
292
|
-
}
|
|
293
|
-
const existingMcp = rawMcp;
|
|
294
|
-
const context7 = canonical.servers.context7;
|
|
295
|
-
if (context7 !== void 0) assertCompatibleContext7(context7, existingMcp?.["context7"]);
|
|
296
|
-
root["$schema"] ??= "https://opencode.ai/config.json";
|
|
297
|
-
if (root[PRIMARY_MODEL_FIELD] === void 0) {
|
|
298
|
-
root[PRIMARY_MODEL_FIELD] = PRIMARY_MODEL;
|
|
299
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_MODEL_FIELD) !== true) {
|
|
300
|
-
primaryModelOwnership.push({ field: PRIMARY_MODEL_FIELD, owned: true });
|
|
301
|
-
}
|
|
302
|
-
} else if (typeof root[PRIMARY_MODEL_FIELD] !== "string" || root[PRIMARY_MODEL_FIELD].trim() === "") {
|
|
303
|
-
throw new Error("OpenCode: 'model' debe ser un identificador provider/model no vac\xEDo; corr\xEDgelo antes de reintentar sync.");
|
|
304
|
-
}
|
|
305
|
-
const provider = ensureOwnedPrimaryObject(root, "provider", PRIMARY_PROVIDER_FIELD, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
306
|
-
const openai = ensureOwnedPrimaryObject(provider, "openai", PRIMARY_OPENAI_FIELD, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
307
|
-
const models = ensureOwnedPrimaryObject(openai, "models", PRIMARY_MODELS_FIELD, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
308
|
-
const sol = ensureOwnedPrimaryObject(models, PRIMARY_MODEL_ID, PRIMARY_SOL_FIELD, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
309
|
-
const limit = ensureOwnedPrimaryObject(sol, "limit", PRIMARY_LIMIT_PREFIX, ctx.ownedPrimaryModelFields, primaryModelOwnership);
|
|
310
|
-
for (const [key, value] of Object.entries(PRIMARY_LIMITS)) {
|
|
311
|
-
if (limit[key] !== void 0) continue;
|
|
312
|
-
limit[key] = value;
|
|
313
|
-
const field = `${PRIMARY_LIMIT_PREFIX}.${key}`;
|
|
314
|
-
if (ctx.ownedPrimaryModelFields?.has(field) !== true) {
|
|
315
|
-
primaryModelOwnership.push({ field, owned: true });
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const defaults = loadCanonicalDefaults(ctx.stackDir)["opencode"];
|
|
319
|
-
const canonicalPermission = defaults?.["permission"];
|
|
320
|
-
if (isFreshConfig) {
|
|
321
|
-
if (canonicalPermission !== void 0) {
|
|
322
|
-
root["permission"] = canonicalPermission;
|
|
323
|
-
ctx.warnings.push(
|
|
324
|
-
"OpenCode: fresh config allows ordinary reads, edits, web access and Bash; sensitive operations ask, while protected paths and obvious destruction are denied. Native matching is not a universal filesystem sandbox."
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
} else if (canonicalPermission !== void 0 && !isDeepStrictEqual(root["permission"], canonicalPermission)) {
|
|
328
|
-
if (ctx.upgradePermissions === true) {
|
|
329
|
-
root["permission"] = canonicalPermission;
|
|
330
|
-
} else {
|
|
331
|
-
ctx.warnings.push(
|
|
332
|
-
"OpenCode: permission block differs from the stack default and was left untouched; re-run with --upgrade-permissions to replace it (a backup is created first), or edit it by hand. Overwriting discards your own permission changes, including any extra hardenings."
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
const mcp = root["mcp"] ??= {};
|
|
337
|
-
const pluginState = inspectOpencodeEngramPlugin(ctx.configDir);
|
|
338
|
-
const officialPluginPresent = pluginState === "official";
|
|
339
|
-
const pluginUnknown = pluginState === "unknown";
|
|
340
|
-
for (const [name, server] of Object.entries(canonical.servers)) {
|
|
341
|
-
const existing = mcp[name];
|
|
342
|
-
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
343
|
-
if (name === "engram" && (officialPluginPresent || pluginUnknown)) {
|
|
344
|
-
if (officialPluginPresent && owned) mcpOwnership.push({ server: name, owned: false });
|
|
345
|
-
ctx.warnings.push(
|
|
346
|
-
officialPluginPresent ? "OpenCode: Engram ya est\xE1 integrado v\xEDa plugin oficial \u2014 no se registra el MCP para no duplicar ni reclamar ownership." : "OpenCode: plugin engram.ts ilegible \u2014 se conserva el MCP sin reclamar ownership hasta poder verificarlo."
|
|
347
|
-
);
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
if (name === "context7" && existing !== void 0) {
|
|
351
|
-
if (owned && !isCanonicalContext7Server(server, existing)) {
|
|
352
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
353
|
-
}
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
|
|
357
|
-
if (owned) {
|
|
358
|
-
if (isManagedOptionalStdioServer(server, existing)) delete mcp[name];
|
|
359
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
360
|
-
}
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
if (server.optional && existing !== void 0) {
|
|
364
|
-
if (!owned || !isManagedOptionalStdioServer(server, existing)) {
|
|
365
|
-
if (owned) mcpOwnership.push({ server: name, owned: false });
|
|
366
|
-
ctx.warnings.push(`OpenCode: MCP opcional '${name}' ya pertenece a la configuraci\xF3n del usuario; se conserva.`);
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (server.transport === "stdio") {
|
|
371
|
-
if (server.command === "{{ENGRAM_BIN}}" && ctx.engramBin === null) {
|
|
372
|
-
ctx.warnings.push(
|
|
373
|
-
"Engram no detectado: el MCP 'engram' no se registra. Inst\xE1lalo (github.com/Gentleman-Programming/engram) y re-ejecuta sync."
|
|
374
|
-
);
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
|
|
378
|
-
mcp[name] = { type: "local", command: [command, ...server.args ?? []] };
|
|
379
|
-
if (server.optional && existing === void 0 && !owned) mcpOwnership.push({ server: name, owned: true });
|
|
380
|
-
} else {
|
|
381
|
-
const previous = mcp[name];
|
|
382
|
-
const headers = {};
|
|
383
|
-
for (const [key, raw] of Object.entries(server.headers ?? {})) {
|
|
384
|
-
const envRef = /^\$\{(\w+)\}$/.exec(raw);
|
|
385
|
-
headers[key] = previous?.headers?.[key] || (envRef ? `{env:${envRef[1]}}` : raw);
|
|
386
|
-
}
|
|
387
|
-
mcp[name] = {
|
|
388
|
-
type: "remote",
|
|
389
|
-
url: server.url,
|
|
390
|
-
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
391
|
-
};
|
|
392
|
-
if (name === "context7" && existing === void 0 && !owned) {
|
|
393
|
-
mcpOwnership.push({ server: name, owned: true });
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (pluginsDir !== null) {
|
|
398
|
-
const plugin = root["plugin"];
|
|
399
|
-
if (Array.isArray(plugin)) {
|
|
400
|
-
const pluginsDirPrefix = pathToFileURL(pluginsDir).href + "/";
|
|
401
|
-
const kept = plugin.filter((url) => !url.startsWith(pluginsDirPrefix));
|
|
402
|
-
if (kept.length === 0) delete root["plugin"];
|
|
403
|
-
else root["plugin"] = kept;
|
|
404
|
-
if (kept.some((u) => /engram/i.test(u))) {
|
|
405
|
-
ctx.warnings.push(
|
|
406
|
-
"OpenCode: hay un plugin de Engram registrado como paquete \u2014 revisa que no conviva con el engram.ts del stack (duplicar\xEDa la integraci\xF3n)."
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
return [{
|
|
413
|
-
kind: "write",
|
|
414
|
-
target: file,
|
|
415
|
-
content,
|
|
416
|
-
...mcpOwnership.length > 0 ? { mcpOwnership } : {},
|
|
417
|
-
...primaryModelOwnership.length > 0 ? { primaryModelOwnership } : {}
|
|
418
|
-
}];
|
|
419
|
-
},
|
|
420
|
-
planUnmerge(mcp, hooks, ctx) {
|
|
421
|
-
const actions = [];
|
|
422
|
-
const { systemPromptFile, pluginsDir } = this.paths(ctx.configDir);
|
|
423
|
-
const prompt = readTextIfExists(systemPromptFile);
|
|
424
|
-
if (prompt !== null) {
|
|
425
|
-
const content = removeSystemPromptSections(prompt);
|
|
426
|
-
actions.push({ kind: "write", target: systemPromptFile, content });
|
|
427
|
-
}
|
|
428
|
-
const configFile = path.join(ctx.configDir, "opencode.json");
|
|
429
|
-
const config = readMcpConfig(configFile);
|
|
430
|
-
if (config !== null) {
|
|
431
|
-
const mcpOwnership = [];
|
|
432
|
-
const primaryModelOwnership = [];
|
|
433
|
-
const content = upsertJson(config, (root) => {
|
|
434
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_MODEL_FIELD) === true) {
|
|
435
|
-
if (root[PRIMARY_MODEL_FIELD] === PRIMARY_MODEL) delete root[PRIMARY_MODEL_FIELD];
|
|
436
|
-
primaryModelOwnership.push({ field: PRIMARY_MODEL_FIELD, owned: false });
|
|
437
|
-
}
|
|
438
|
-
const provider = objectValue(root["provider"]);
|
|
439
|
-
const openai = provider === null ? null : objectValue(provider["openai"]);
|
|
440
|
-
const models = openai === null ? null : objectValue(openai["models"]);
|
|
441
|
-
const sol = models === null ? null : objectValue(models[PRIMARY_MODEL_ID]);
|
|
442
|
-
const limit = sol === null ? null : objectValue(sol["limit"]);
|
|
443
|
-
if (limit !== null) {
|
|
444
|
-
for (const [key, value] of Object.entries(PRIMARY_LIMITS)) {
|
|
445
|
-
const field = `${PRIMARY_LIMIT_PREFIX}.${key}`;
|
|
446
|
-
if (ctx.ownedPrimaryModelFields?.has(field) !== true) continue;
|
|
447
|
-
if (limit[key] === value) delete limit[key];
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
if (sol !== null && ctx.ownedPrimaryModelFields?.has(PRIMARY_LIMIT_PREFIX) === true) pruneEmpty(sol, "limit");
|
|
451
|
-
if (models !== null && ctx.ownedPrimaryModelFields?.has(PRIMARY_SOL_FIELD) === true) pruneEmpty(models, PRIMARY_MODEL_ID);
|
|
452
|
-
if (openai !== null && ctx.ownedPrimaryModelFields?.has(PRIMARY_MODELS_FIELD) === true) pruneEmpty(openai, "models");
|
|
453
|
-
if (provider !== null && ctx.ownedPrimaryModelFields?.has(PRIMARY_OPENAI_FIELD) === true) pruneEmpty(provider, "openai");
|
|
454
|
-
if (ctx.ownedPrimaryModelFields?.has(PRIMARY_PROVIDER_FIELD) === true) pruneEmpty(root, "provider");
|
|
455
|
-
const managedFields = [
|
|
456
|
-
PRIMARY_PROVIDER_FIELD,
|
|
457
|
-
PRIMARY_OPENAI_FIELD,
|
|
458
|
-
PRIMARY_MODELS_FIELD,
|
|
459
|
-
PRIMARY_SOL_FIELD,
|
|
460
|
-
PRIMARY_LIMIT_PREFIX,
|
|
461
|
-
...Object.keys(PRIMARY_LIMITS).map((key) => `${PRIMARY_LIMIT_PREFIX}.${key}`)
|
|
462
|
-
];
|
|
463
|
-
for (const field of managedFields) {
|
|
464
|
-
if (ctx.ownedPrimaryModelFields?.has(field) === true) primaryModelOwnership.push({ field, owned: false });
|
|
465
|
-
}
|
|
466
|
-
const rawMcpBlock = root["mcp"];
|
|
467
|
-
if (rawMcpBlock !== void 0 && objectValue(rawMcpBlock) === null) {
|
|
468
|
-
throw new Error("OpenCode: la clave 'mcp' debe ser un objeto; corr\xEDgela antes de reintentar uninstall.");
|
|
469
|
-
}
|
|
470
|
-
const mcpBlock = rawMcpBlock;
|
|
471
|
-
if (mcpBlock !== void 0) {
|
|
472
|
-
const pluginState = inspectOpencodeEngramPlugin(ctx.configDir);
|
|
473
|
-
const officialPluginPresent = pluginState === "official";
|
|
474
|
-
const pluginUnknown = pluginState === "unknown";
|
|
475
|
-
for (const [name, server] of Object.entries(mcp.servers)) {
|
|
476
|
-
if (name === "engram" && (officialPluginPresent || pluginUnknown)) {
|
|
477
|
-
ctx.warnings.push(
|
|
478
|
-
officialPluginPresent ? "OpenCode: MCP 'engram' oficial (plugin) se conserva; usa el desinstalador oficial de Engram para retirarlo." : "OpenCode: plugin engram.ts ilegible \u2014 MCP 'engram' y su estado se conservan sin verificar."
|
|
479
|
-
);
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
if (name === "context7") {
|
|
483
|
-
const canonical = isCanonicalContext7Server(server, mcpBlock[name]);
|
|
484
|
-
const owned = ctx.ownedMcpServers?.has(name) === true;
|
|
485
|
-
if (owned) {
|
|
486
|
-
if (canonical) delete mcpBlock[name];
|
|
487
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
488
|
-
}
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
if (!server.optional) {
|
|
492
|
-
delete mcpBlock[name];
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
if (ctx.ownedMcpServers?.has(name) === true) {
|
|
496
|
-
if (isManagedOptionalStdioServer(server, mcpBlock[name])) delete mcpBlock[name];
|
|
497
|
-
mcpOwnership.push({ server: name, owned: false });
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
if (Object.keys(mcpBlock).length === 0) delete root["mcp"];
|
|
501
|
-
}
|
|
502
|
-
const plugin = root["plugin"];
|
|
503
|
-
if (Array.isArray(plugin) && pluginsDir !== null) {
|
|
504
|
-
const pluginsDirPrefix = pathToFileURL(pluginsDir).href + "/";
|
|
505
|
-
const kept = plugin.filter((url) => !url.startsWith(pluginsDirPrefix));
|
|
506
|
-
if (kept.length === 0) delete root["plugin"];
|
|
507
|
-
else root["plugin"] = kept;
|
|
508
|
-
}
|
|
509
|
-
});
|
|
510
|
-
actions.push({
|
|
511
|
-
kind: "write",
|
|
512
|
-
target: configFile,
|
|
513
|
-
content,
|
|
514
|
-
...mcpOwnership.length > 0 ? { mcpOwnership } : {},
|
|
515
|
-
...primaryModelOwnership.length > 0 ? { primaryModelOwnership } : {}
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
const hooksFile = path.join(ctx.configDir, "hooks.json");
|
|
519
|
-
const hooksJson = readTextIfExists(hooksFile);
|
|
520
|
-
if (hooksJson !== null) {
|
|
521
|
-
const ourScripts = hookScriptNames(hooks);
|
|
522
|
-
const content = upsertJson(hooksJson, (root) => {
|
|
523
|
-
const after = root["tool.execute.after"];
|
|
524
|
-
if (!after) return;
|
|
525
|
-
for (const tool of Object.keys(after)) {
|
|
526
|
-
const byCommand = after[tool];
|
|
527
|
-
for (const includes of Object.keys(byCommand)) {
|
|
528
|
-
byCommand[includes] = byCommand[includes].filter(
|
|
529
|
-
(script) => !ourScripts.some((s) => script.includes(s))
|
|
530
|
-
);
|
|
531
|
-
if (byCommand[includes].length === 0) delete byCommand[includes];
|
|
532
|
-
}
|
|
533
|
-
if (Object.keys(byCommand).length === 0) delete after[tool];
|
|
534
|
-
}
|
|
535
|
-
if (Object.keys(after).length === 0) delete root["tool.execute.after"];
|
|
536
|
-
});
|
|
537
|
-
actions.push({ kind: "write", target: hooksFile, content: content.trim() === "{}" ? "" : content });
|
|
538
|
-
}
|
|
539
|
-
return actions;
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
var OPENCODE_STACK_KEPT_PLUGINS = ["hooks.ts", "worktree.ts"];
|
|
543
|
-
function isOfficialOpencodePluginContent(content) {
|
|
544
|
-
return content.includes("ensureLocalReady") || content.includes("CONFIGURED_ENGRAM_URL") || content.includes("SESSION_ATTRIBUTED_WRITE_TOOLS") || content.includes("canonicalEngramToolName") || content.includes("localInstanceID");
|
|
545
|
-
}
|
|
546
|
-
function inspectOpencodePluginFile(file) {
|
|
547
|
-
let content;
|
|
548
|
-
try {
|
|
549
|
-
content = fs.readFileSync(file, "utf8");
|
|
550
|
-
} catch (error) {
|
|
551
|
-
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
552
|
-
if (code === "ENOENT") return "absent";
|
|
553
|
-
return "unknown";
|
|
554
|
-
}
|
|
555
|
-
return isOfficialOpencodePluginContent(content) ? "official" : "legacy-or-foreign";
|
|
556
|
-
}
|
|
557
|
-
function inspectOpencodeEngramPlugin(configDir) {
|
|
558
|
-
return inspectOpencodePluginFile(path.join(configDir, "plugins", "engram.ts"));
|
|
559
|
-
}
|
|
560
|
-
function isStackLegacyOpencodePluginContent(content) {
|
|
561
|
-
if (content.includes("{{ENGRAM_BIN}}") || content.includes("{{ENGRAM_PROTOCOL}}")) return true;
|
|
562
|
-
try {
|
|
563
|
-
const canon = fs.readFileSync(
|
|
564
|
-
path.join(stackRoot(), "plugins", "opencode", "engram.ts"),
|
|
565
|
-
"utf8"
|
|
566
|
-
);
|
|
567
|
-
if (content === canon) return true;
|
|
568
|
-
} catch {
|
|
569
|
-
}
|
|
570
|
-
return content.includes("resolveEngramBin") || content.includes("stripPrivateTags") || content.includes("declare const Bun");
|
|
571
|
-
}
|
|
572
|
-
function readOpencodePluginFile(configDir) {
|
|
573
|
-
return readTextIfExists(path.join(configDir, "plugins", "engram.ts"));
|
|
574
|
-
}
|
|
575
|
-
function readOpencodeConfigFile(file) {
|
|
576
|
-
let raw;
|
|
577
|
-
try {
|
|
578
|
-
raw = fs.readFileSync(file, "utf8");
|
|
579
|
-
} catch (error) {
|
|
580
|
-
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
581
|
-
if (code === "ENOENT") return { file, status: "absent" };
|
|
582
|
-
return { file, status: "unverifiable", reason: `${file}: ilegible (${code})` };
|
|
583
|
-
}
|
|
584
|
-
let parsed;
|
|
585
|
-
try {
|
|
586
|
-
parsed = JSON.parse(raw);
|
|
587
|
-
} catch {
|
|
588
|
-
return { file, status: "unverifiable", reason: `${file}: JSON malformado` };
|
|
589
|
-
}
|
|
590
|
-
const root = objectValue(parsed);
|
|
591
|
-
if (root === null) return { file, status: "unverifiable", reason: `${file}: no es un objeto JSON` };
|
|
592
|
-
return { file, status: "ok", parsed: root };
|
|
593
|
-
}
|
|
594
|
-
function readExistingOpencodeConfigs(configDir) {
|
|
595
|
-
const out = [];
|
|
596
|
-
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
597
|
-
const read = readOpencodeConfigFile(path.join(configDir, name));
|
|
598
|
-
if (read.status === "ok") out.push(read);
|
|
599
|
-
}
|
|
600
|
-
return out;
|
|
601
|
-
}
|
|
602
|
-
function readExistingTuiConfigs(configDir) {
|
|
603
|
-
const out = [];
|
|
604
|
-
for (const name of ["tui.json", "tui.jsonc"]) {
|
|
605
|
-
const read = readOpencodeConfigFile(path.join(configDir, name));
|
|
606
|
-
if (read.status === "ok") out.push(read);
|
|
607
|
-
}
|
|
608
|
-
return out;
|
|
609
|
-
}
|
|
610
|
-
function collectUnverifiableOpencodeConfigs(configDir) {
|
|
611
|
-
const reasons = [];
|
|
612
|
-
for (const name of ["opencode.json", "opencode.jsonc", "tui.json", "tui.jsonc"]) {
|
|
613
|
-
const read = readOpencodeConfigFile(path.join(configDir, name));
|
|
614
|
-
if (read.status === "unverifiable") reasons.push(read.reason);
|
|
615
|
-
}
|
|
616
|
-
return reasons;
|
|
617
|
-
}
|
|
618
|
-
function isExactOpencodeEngramMcpValue(value, engramBin) {
|
|
619
|
-
const record = objectValue(value);
|
|
620
|
-
if (record === null) return false;
|
|
621
|
-
if (record["type"] !== "local") return false;
|
|
622
|
-
const command = record["command"];
|
|
623
|
-
if (!Array.isArray(command) || command.length !== 3) return false;
|
|
624
|
-
if (command[1] !== "mcp" || command[2] !== "--tools=agent") return false;
|
|
625
|
-
if (typeof command[0] !== "string") return false;
|
|
626
|
-
if (engramBin === "") return false;
|
|
627
|
-
if (engramBin !== void 0) return command[0] === engramBin;
|
|
628
|
-
return command[0].includes("engram");
|
|
629
|
-
}
|
|
630
|
-
function checkOpencodeOfficialMcp(configDir, engramBin) {
|
|
631
|
-
for (const { parsed } of readExistingOpencodeConfigs(configDir)) {
|
|
632
|
-
const mcp = objectValue(parsed["mcp"]);
|
|
633
|
-
if (mcp !== null && isExactOpencodeEngramMcpValue(mcp["engram"], engramBin)) return true;
|
|
634
|
-
}
|
|
635
|
-
return false;
|
|
636
|
-
}
|
|
637
|
-
function checkOpencodeOfficialStatusline(configDir) {
|
|
638
|
-
for (const { parsed } of readExistingOpencodeConfigs(configDir)) {
|
|
639
|
-
const statusline = objectValue(parsed["statusline"]);
|
|
640
|
-
if (statusline !== null) {
|
|
641
|
-
const command = statusline["command"];
|
|
642
|
-
if (typeof command === "string" && command.includes("engram")) return true;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
for (const { parsed } of readExistingTuiConfigs(configDir)) {
|
|
646
|
-
const plugin = parsed["plugin"];
|
|
647
|
-
if (Array.isArray(plugin) && plugin.some((entry) => typeof entry === "string" && /statusline/i.test(entry))) {
|
|
648
|
-
return true;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
return false;
|
|
652
|
-
}
|
|
653
|
-
function checkOpencodeDuplicates(configDir) {
|
|
654
|
-
for (const { parsed } of readExistingOpencodeConfigs(configDir)) {
|
|
655
|
-
const plugin = parsed["plugin"];
|
|
656
|
-
if (Array.isArray(plugin) && plugin.some((entry) => typeof entry === "string" && /engram/i.test(entry))) {
|
|
657
|
-
return true;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
return false;
|
|
661
|
-
}
|
|
662
|
-
async function verifyOfficialSetup(args) {
|
|
663
|
-
const plugin = readOpencodePluginFile(args.configDir);
|
|
664
|
-
const hasPlugin = plugin !== null && isOfficialOpencodePluginContent(plugin);
|
|
665
|
-
const hasMcp = checkOpencodeOfficialMcp(args.configDir, args.engramBin);
|
|
666
|
-
const hasStatusline = checkOpencodeOfficialStatusline(args.configDir);
|
|
667
|
-
const duplicates = checkOpencodeDuplicates(args.configDir);
|
|
668
|
-
const unverifiable = collectUnverifiableOpencodeConfigs(args.configDir);
|
|
669
|
-
const passed = [];
|
|
670
|
-
const missing = [];
|
|
671
|
-
if (hasPlugin) passed.push("plugin");
|
|
672
|
-
else missing.push(plugin === null ? "plugin:missing" : "plugin:legacy-or-foreign");
|
|
673
|
-
if (hasMcp) passed.push("mcp");
|
|
674
|
-
else missing.push("mcp:missing");
|
|
675
|
-
if (hasStatusline) passed.push("statusline");
|
|
676
|
-
else missing.push("statusline:missing");
|
|
677
|
-
if (duplicates) missing.push("duplicates:detected");
|
|
678
|
-
if (unverifiable.length > 0) missing.push("config:unverifiable");
|
|
679
|
-
if (missing.length === 0) {
|
|
680
|
-
return { ok: true, layers: passed, duplicates: false };
|
|
681
|
-
}
|
|
682
|
-
const reason = duplicates && unverifiable.length === 0 ? `OpenCode: setup oficial Engram con plugin Engram duplicado; se conserva sin reclamar.` : unverifiable.length > 0 ? `OpenCode: setup oficial Engram no verificable (${unverifiable.join("; ")}).` : `OpenCode: setup oficial Engram incompleto (falta: ${missing.join(", ")}).`;
|
|
683
|
-
return {
|
|
684
|
-
ok: false,
|
|
685
|
-
layers: [...passed, ...missing],
|
|
686
|
-
duplicates,
|
|
687
|
-
reason
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
registerOfficialSetupVerifier("opencode", verifyOfficialSetup);
|
|
691
|
-
async function shouldRetireLegacyEngram(args) {
|
|
692
|
-
const plugin = readOpencodePluginFile(args.configDir);
|
|
693
|
-
if (plugin === null) {
|
|
694
|
-
return { retire: false, reason: "ambiguous: plugins/engram.ts ausente, sin reemplazo oficial verificable" };
|
|
695
|
-
}
|
|
696
|
-
if (isOfficialOpencodePluginContent(plugin)) {
|
|
697
|
-
const unverifiable = collectUnverifiableOpencodeConfigs(args.configDir);
|
|
698
|
-
if (unverifiable.length > 0) {
|
|
699
|
-
return { retire: false, reason: `ambiguous: config no verificable (${unverifiable.join("; ")}), se conserva` };
|
|
700
|
-
}
|
|
701
|
-
const hasMcp = checkOpencodeOfficialMcp(args.configDir);
|
|
702
|
-
const hasStatusline = checkOpencodeOfficialStatusline(args.configDir);
|
|
703
|
-
if (hasMcp && hasStatusline) {
|
|
704
|
-
return { retire: true, reason: "official verified: plugin + MCP + statusline en filesystem" };
|
|
705
|
-
}
|
|
706
|
-
return {
|
|
707
|
-
retire: false,
|
|
708
|
-
reason: `ambiguous: plugin oficial sin MCP/statusline verificables (mcp=${hasMcp}, statusline=${hasStatusline})`
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
if (isStackLegacyOpencodePluginContent(plugin)) {
|
|
712
|
-
return { retire: false, reason: "legacy Stack sin reemplazo oficial: setup pendiente, se conserva" };
|
|
713
|
-
}
|
|
714
|
-
return { retire: false, reason: "ambiguous: custom/foreign content en plugins/engram.ts, se conserva" };
|
|
715
|
-
}
|
|
716
|
-
async function transferEngramOwnership(args) {
|
|
717
|
-
const pluginPath = path.join(args.configDir, "plugins", "engram.ts");
|
|
718
|
-
const plugin = readTextIfExists(pluginPath);
|
|
719
|
-
const kept = [...OPENCODE_STACK_KEPT_PLUGINS];
|
|
720
|
-
if (plugin === null || !isOfficialOpencodePluginContent(plugin)) {
|
|
721
|
-
const detail = plugin === null ? "plugins/engram.ts ausente" : isStackLegacyOpencodePluginContent(plugin) ? "legacy Stack sin reemplazo oficial" : "custom/foreign content";
|
|
722
|
-
return {
|
|
723
|
-
ownershipRetired: false,
|
|
724
|
-
retired: false,
|
|
725
|
-
kept,
|
|
726
|
-
recreateOnSync: false,
|
|
727
|
-
preserveOfficialOnUninstall: true,
|
|
728
|
-
layers: [],
|
|
729
|
-
reason: `OpenCode: transferencia bloqueada (${detail}); se conserva el archivo.`
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
const unverifiable = collectUnverifiableOpencodeConfigs(args.configDir);
|
|
733
|
-
if (unverifiable.length > 0) {
|
|
734
|
-
return {
|
|
735
|
-
ownershipRetired: false,
|
|
736
|
-
retired: false,
|
|
737
|
-
kept,
|
|
738
|
-
recreateOnSync: false,
|
|
739
|
-
preserveOfficialOnUninstall: true,
|
|
740
|
-
layers: ["plugin", "config:unverifiable"],
|
|
741
|
-
reason: `OpenCode: transferencia bloqueada (config no verificable: ${unverifiable.join("; ")}); se conserva el archivo oficial.`
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
const hasMcp = checkOpencodeOfficialMcp(args.configDir);
|
|
745
|
-
const hasStatusline = checkOpencodeOfficialStatusline(args.configDir);
|
|
746
|
-
const layers = ["plugin", ...hasMcp ? ["mcp"] : ["mcp:missing"], ...hasStatusline ? ["statusline"] : ["statusline:missing"]];
|
|
747
|
-
if (!hasMcp || !hasStatusline) {
|
|
748
|
-
return {
|
|
749
|
-
ownershipRetired: false,
|
|
750
|
-
retired: false,
|
|
751
|
-
kept,
|
|
752
|
-
recreateOnSync: false,
|
|
753
|
-
preserveOfficialOnUninstall: true,
|
|
754
|
-
layers,
|
|
755
|
-
reason: `OpenCode: transferencia bloqueada (plugin oficial sin MCP/statusline verificables); se conserva el archivo oficial.`
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
return {
|
|
759
|
-
ownershipRetired: true,
|
|
760
|
-
retired: true,
|
|
761
|
-
kept,
|
|
762
|
-
recreateOnSync: false,
|
|
763
|
-
preserveOfficialOnUninstall: true,
|
|
764
|
-
layers: ["plugin", "mcp", "statusline"]
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
export {
|
|
769
|
-
opencodeAdapter,
|
|
770
|
-
isOfficialOpencodePluginContent,
|
|
771
|
-
inspectOpencodePluginFile,
|
|
772
|
-
inspectOpencodeEngramPlugin,
|
|
773
|
-
readOpencodeConfigFile,
|
|
774
|
-
collectUnverifiableOpencodeConfigs,
|
|
775
|
-
checkOpencodeOfficialMcp,
|
|
776
|
-
checkOpencodeOfficialStatusline,
|
|
777
|
-
checkOpencodeDuplicates,
|
|
778
|
-
verifyOfficialSetup,
|
|
779
|
-
shouldRetireLegacyEngram,
|
|
780
|
-
transferEngramOwnership
|
|
781
|
-
};
|