opencode-resolve 0.1.20 → 0.2.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.ko.md +10 -46
- package/README.md +10 -46
- package/dist/agents.d.ts +3 -19
- package/dist/agents.js +45 -205
- package/dist/config.js +10 -47
- package/dist/hooks/index.js +217 -84
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/messages.d.ts +1 -1
- package/dist/messages.js +8 -48
- package/dist/state.d.ts +13 -0
- package/dist/state.js +9 -0
- package/dist/tools/index.d.ts +11 -2
- package/dist/tools/index.js +127 -21
- package/dist/types.d.ts +3 -5
- package/dist/utils.d.ts +0 -9
- package/dist/utils.js +8 -91
- package/opencode-resolve.example.json +0 -15
- package/opencode-resolve.reference.jsonc +56 -130
- package/package.json +1 -1
- package/scripts/cli.mjs +13 -9
- package/scripts/install-local.mjs +4 -18
- package/scripts/postinstall.mjs +132 -495
package/scripts/postinstall.mjs
CHANGED
|
@@ -20,48 +20,17 @@ const ADDITIVE_DEFAULTS = {
|
|
|
20
20
|
autoApprove: true,
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ re: /\b(gpt-4o|opus|sonnet|gemini-1\.5)\b/i, score: 1 },
|
|
29
|
-
{ re: /\b(o1|o3|o4|reason|reasoning|pro|max|ultra|gold|sota)\b/i, score: 2 },
|
|
30
|
-
{ re: /\b(5\.5|5\.4|5\.3|5\.2|5\.1|sonnet-4|opus-4|opus-5|max-5|next|2026)\b/i, score: 2 },
|
|
31
|
-
{ re: /\b(codex|coder|code|coding)\b/i, score: 1 },
|
|
32
|
-
]
|
|
23
|
+
// Tier classification by name keywords. Returns 0=bronze (cheap scout),
|
|
24
|
+
// 1=silver (workhorse coder), 2=gold (flagship reasoner). Within the same
|
|
25
|
+
// tier we tiebreak by numeric version so 5.1 ranks above 4.5.
|
|
26
|
+
const TIER_BRONZE_RE = /\b(mini|flash|nano|lite|haiku|air|small|gpt-3\.5)\b/i
|
|
27
|
+
const TIER_GOLD_RE = /\b(5\.5|5\.4|opus|o1-pro|o1|o3|o4|max|ultra|sota|reasoning|gpt-4-turbo)\b/i
|
|
33
28
|
|
|
34
29
|
// Provider-neutral agents — safe to enable for everyone (no profile-specific model needed).
|
|
35
30
|
const DEFAULT_ENABLED_AGENTS = [
|
|
36
31
|
"coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner",
|
|
37
32
|
"architect", "debugger", "researcher",
|
|
38
33
|
]
|
|
39
|
-
const GPT_ENABLED_AGENTS = [...DEFAULT_ENABLED_AGENTS, "gpt", "gpt-coder", "codex"]
|
|
40
|
-
const GLM_ENABLED_AGENTS = [...DEFAULT_ENABLED_AGENTS, "glm"]
|
|
41
|
-
|
|
42
|
-
// ZAI local MCP server bootstrap for GLM users.
|
|
43
|
-
// Do not copy provider secrets from OpenCode's auth store into opencode.json.
|
|
44
|
-
// The MCP process should receive credentials from the user's runtime environment.
|
|
45
|
-
const ZAI_MCP_SERVERS = {
|
|
46
|
-
"zai-mcp-server": {
|
|
47
|
-
type: "local",
|
|
48
|
-
command: ["npx", "-y", "@z_ai/mcp-server"],
|
|
49
|
-
environment: {
|
|
50
|
-
Z_AI_MODE: "ZAI",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const COMPANION_PLUGINS = [
|
|
56
|
-
{
|
|
57
|
-
pkg: "@tarquinen/opencode-dcp",
|
|
58
|
-
desc: "Dynamic Context Pruning — strips obsolete tool outputs so long resolver loops cost fewer tokens",
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
pkg: "@slkiser/opencode-quota",
|
|
62
|
-
desc: "Live token/quota usage indicator — supports GLM coding-plan, OpenAI Plus/Pro, Qwen, and more",
|
|
63
|
-
},
|
|
64
|
-
]
|
|
65
34
|
|
|
66
35
|
const OPENAI_MODEL_HINTS = [
|
|
67
36
|
"openai/gpt-5.5",
|
|
@@ -74,11 +43,13 @@ const OPENAI_MODEL_HINTS = [
|
|
|
74
43
|
]
|
|
75
44
|
|
|
76
45
|
const GLM_MODEL_HINTS = [
|
|
46
|
+
"zai-coding-plan/glm-5.2",
|
|
77
47
|
"zai-coding-plan/glm-5.1",
|
|
78
48
|
"zai-coding-plan/glm-4.5",
|
|
79
49
|
"zai-coding-plan/glm-4.5-air",
|
|
80
50
|
"zai-coding-plan/glm-5",
|
|
81
51
|
"zai-coding-plan/glm-4.7",
|
|
52
|
+
"zai/glm-5.2",
|
|
82
53
|
"zai/glm-5.1",
|
|
83
54
|
"zai/glm-4.5",
|
|
84
55
|
"zai/glm-4.5-air",
|
|
@@ -105,7 +76,6 @@ async function printSummaryBanner(version) {
|
|
|
105
76
|
const raw = await readFile(resolveConfigPath, "utf8")
|
|
106
77
|
const cfg = JSON.parse(raw)
|
|
107
78
|
const parts = []
|
|
108
|
-
if (cfg.profile) parts.push(`profile=${cfg.profile}`)
|
|
109
79
|
if (cfg.tier) parts.push(`tier=${cfg.tier}`)
|
|
110
80
|
const enabled = Array.isArray(cfg.enabled) ? cfg.enabled.length : Object.keys(cfg.agents ?? {}).length
|
|
111
81
|
if (enabled) parts.push(`${enabled} agents`)
|
|
@@ -131,7 +101,6 @@ async function printSummaryBanner(version) {
|
|
|
131
101
|
try {
|
|
132
102
|
await registerPlugin()
|
|
133
103
|
await refreshSelfPluginCache(pluginVersion)
|
|
134
|
-
await offerCompanionPlugins()
|
|
135
104
|
console.log(`[${packageName}] v${pluginVersion} install complete — restart OpenCode to load the plugin`)
|
|
136
105
|
await printSummaryBanner(pluginVersion)
|
|
137
106
|
} catch (error) {
|
|
@@ -154,17 +123,11 @@ async function registerPlugin() {
|
|
|
154
123
|
const scriptedAnswers = await readScriptedAnswersIfNeeded()
|
|
155
124
|
|
|
156
125
|
const probe = await readOpenCodeConfig()
|
|
157
|
-
const allModels = detectAllModels(probe)
|
|
158
|
-
const hasGLM = allModels.some((m) => isGLMModel(m))
|
|
159
126
|
const pluginNeeded = !isPluginRegisteredIn(probe)
|
|
160
|
-
const missingMCPNames = hasGLM
|
|
161
|
-
? Object.keys(ZAI_MCP_SERVERS).filter((name) => probe.mcp?.[name] === undefined)
|
|
162
|
-
: []
|
|
163
127
|
|
|
164
|
-
if (pluginNeeded
|
|
128
|
+
if (pluginNeeded) {
|
|
165
129
|
const fresh = await readOpenCodeConfig()
|
|
166
|
-
|
|
167
|
-
if (missingMCPNames.length > 0) applyMCPPatches(fresh, missingMCPNames)
|
|
130
|
+
applyPluginPatch(fresh)
|
|
168
131
|
await writeFile(opencodeConfigPath, `${JSON.stringify(fresh, null, 2)}\n`)
|
|
169
132
|
console.log(`[${packageName}] updated ${opencodeConfigPath}`)
|
|
170
133
|
} else {
|
|
@@ -233,7 +196,6 @@ async function runOpenCodePluginInstall() {
|
|
|
233
196
|
...process.env,
|
|
234
197
|
OPENCODE_RESOLVE_REFRESHING_CACHE: "1",
|
|
235
198
|
OPENCODE_RESOLVE_SKIP_POSTINSTALL: "1",
|
|
236
|
-
OPENCODE_RESOLVE_SKIP_COMPANIONS: "1",
|
|
237
199
|
},
|
|
238
200
|
})
|
|
239
201
|
child.on("exit", (code) => resolveSpawn(code === 0))
|
|
@@ -255,25 +217,13 @@ function applyPluginPatch(config) {
|
|
|
255
217
|
}
|
|
256
218
|
}
|
|
257
219
|
|
|
258
|
-
function applyMCPPatches(config, names) {
|
|
259
|
-
config.mcp ??= {}
|
|
260
|
-
for (const name of names) {
|
|
261
|
-
if (config.mcp[name] === undefined) {
|
|
262
|
-
config.mcp[name] = ZAI_MCP_SERVERS[name]
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
console.log(`[${packageName}] injected ZAI MCP server config: ${names.join(", ")}`)
|
|
266
|
-
console.log(`[${packageName}] note: API keys are not copied into opencode.json; export Z_AI_API_KEY if the MCP server requires it.`)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
220
|
async function handleExistingResolveConfig(opencodeConfig, scriptedAnswers) {
|
|
270
221
|
const action = await chooseExistingResolveConfigAction(scriptedAnswers)
|
|
271
222
|
if (action === "fresh") {
|
|
272
|
-
const existing = await readExistingResolveConfig()
|
|
273
223
|
await backupResolveConfig()
|
|
274
|
-
|
|
224
|
+
console.log(`[${packageName}] resetting resolve.json — running setup from scratch (model pins wiped)`)
|
|
275
225
|
await createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers, {
|
|
276
|
-
preservedModels:
|
|
226
|
+
preservedModels: undefined,
|
|
277
227
|
})
|
|
278
228
|
return
|
|
279
229
|
}
|
|
@@ -290,7 +240,7 @@ async function handleExistingResolveConfig(opencodeConfig, scriptedAnswers) {
|
|
|
290
240
|
async function chooseExistingResolveConfigAction(scriptedAnswers) {
|
|
291
241
|
if (readInstallerOption("configure_models") === "1") return "models"
|
|
292
242
|
const requested = readInstallerOption("reinstall").trim().toLowerCase()
|
|
293
|
-
if (["fresh", "reset", "recreate", "new"].includes(requested)) return "fresh"
|
|
243
|
+
if (["fresh", "reset", "recreate", "new", "nuke", "wipe", "full-reset", "scratch"].includes(requested)) return "fresh"
|
|
294
244
|
if (["update", "keep", "migrate", "preserve"].includes(requested)) return "update"
|
|
295
245
|
if (requested) {
|
|
296
246
|
console.warn(`[${packageName}] ignoring unknown reinstall mode ${JSON.stringify(requested)}; use "fresh" or "update".`)
|
|
@@ -301,6 +251,7 @@ async function chooseExistingResolveConfigAction(scriptedAnswers) {
|
|
|
301
251
|
if (!canPrompt) {
|
|
302
252
|
console.log(`[${packageName}] existing ${resolveConfigPath} found; preserving it and applying additive updates.`)
|
|
303
253
|
console.log(`[${packageName}] for model setup, run: ${packageName} setup --models`)
|
|
254
|
+
console.log(`[${packageName}] for a full reset (wipes the whole file incl. model pins), run: ${packageName} setup --reset`)
|
|
304
255
|
console.log(`[${packageName}] to force plugin cache reinstall without touching settings, run: ${packageName} setup --force-cache`)
|
|
305
256
|
return "update"
|
|
306
257
|
}
|
|
@@ -316,10 +267,11 @@ async function chooseExistingResolveConfigAction(scriptedAnswers) {
|
|
|
316
267
|
console.log("How do you want to handle it?")
|
|
317
268
|
console.log(" 1. keep — preserve existing settings, only add missing defaults (recommended)")
|
|
318
269
|
console.log(" 2. models — re-pick models only, keep the rest")
|
|
319
|
-
console.log(" 3. reset — back up the
|
|
270
|
+
console.log(" 3. reset — back up the file and wipe EVERYTHING (model pins included)")
|
|
320
271
|
const answer = await askChoice(rl, "Choice [1=keep, 2=models, 3=reset, default 1]: ", ["1", "2", "3"], "1")
|
|
321
272
|
if (answer === "2") return "models"
|
|
322
|
-
|
|
273
|
+
if (answer === "3") return "fresh"
|
|
274
|
+
return "update"
|
|
323
275
|
} finally {
|
|
324
276
|
rl.close()
|
|
325
277
|
}
|
|
@@ -361,7 +313,6 @@ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers, opti
|
|
|
361
313
|
: undefined
|
|
362
314
|
|
|
363
315
|
if (interactivePreset) {
|
|
364
|
-
resolveConfig.profile = interactivePreset.profile
|
|
365
316
|
if (interactivePreset.tier) resolveConfig.tier = interactivePreset.tier
|
|
366
317
|
else delete resolveConfig.tier
|
|
367
318
|
if (interactivePreset.enabled) resolveConfig.enabled = interactivePreset.enabled
|
|
@@ -375,42 +326,17 @@ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers, opti
|
|
|
375
326
|
return
|
|
376
327
|
}
|
|
377
328
|
|
|
378
|
-
|
|
379
|
-
|
|
329
|
+
// Non-interactive: agents inherit the top-level OpenCode model. No model
|
|
330
|
+
// pinning unless the user runs setup in a TTY and picks one.
|
|
380
331
|
const preset = buildModelPreset(currentModel, allModels)
|
|
381
332
|
|
|
382
|
-
if (hasGLM && !hasGPT) {
|
|
383
|
-
resolveConfig.profile = "glm"
|
|
384
|
-
resolveConfig.tier = "silver"
|
|
385
|
-
resolveConfig.agents = {
|
|
386
|
-
...resolveConfig.agents,
|
|
387
|
-
glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
|
|
388
|
-
}
|
|
389
|
-
} else if (hasGPT && !hasGLM) {
|
|
390
|
-
resolveConfig.profile = "gpt"
|
|
391
|
-
resolveConfig.tier = "gold"
|
|
392
|
-
resolveConfig.agents = {
|
|
393
|
-
...resolveConfig.agents,
|
|
394
|
-
gpt: { ...(resolveConfig.agents?.gpt ?? {}), enabled: true },
|
|
395
|
-
}
|
|
396
|
-
} else {
|
|
397
|
-
resolveConfig.profile = "mix"
|
|
398
|
-
if (hasGLM && hasGPT) {
|
|
399
|
-
resolveConfig.agents = {
|
|
400
|
-
...resolveConfig.agents,
|
|
401
|
-
gpt: { ...(resolveConfig.agents?.gpt ?? {}), enabled: true },
|
|
402
|
-
glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
333
|
if (preset && Object.keys(preset).length > 0) {
|
|
408
334
|
resolveConfig.models = mergePreservedModels(preset, preservedModels)
|
|
409
335
|
} else if (preservedModels) {
|
|
410
336
|
resolveConfig.models = { ...preservedModels }
|
|
411
337
|
} else {
|
|
412
338
|
const providerHint = currentModel ? ` (top-level model: ${currentModel})` : ""
|
|
413
|
-
console.log(`[${packageName}] no
|
|
339
|
+
console.log(`[${packageName}] no models detected in opencode.json — agents inherit the top-level model${providerHint}`)
|
|
414
340
|
console.log(`[${packageName}] to pin role-specific models, edit ${resolveConfigPath} ("models" section)`)
|
|
415
341
|
console.log(`[${packageName}] or rerun setup in a TTY: ${packageName} setup --models`)
|
|
416
342
|
}
|
|
@@ -437,17 +363,15 @@ async function reconfigureExistingModels(opencodeConfig, scriptedAnswers) {
|
|
|
437
363
|
: undefined
|
|
438
364
|
const preset = interactivePreset ?? {
|
|
439
365
|
label: getPresetLabel(currentModel),
|
|
440
|
-
profile: inferProfileFromModels(currentModel, allModels),
|
|
441
366
|
models: buildModelPreset(currentModel, allModels),
|
|
442
367
|
}
|
|
443
368
|
|
|
444
369
|
if (!preset.models || Object.keys(preset.models).length === 0) {
|
|
445
|
-
console.log(`[${packageName}] no
|
|
370
|
+
console.log(`[${packageName}] no models detected; existing model pins preserved`)
|
|
446
371
|
return
|
|
447
372
|
}
|
|
448
373
|
|
|
449
374
|
const updated = { ...existing }
|
|
450
|
-
updated.profile = preset.profile
|
|
451
375
|
if (preset.tier) updated.tier = preset.tier
|
|
452
376
|
else delete updated.tier
|
|
453
377
|
if (preset.enabled) updated.enabled = preset.enabled
|
|
@@ -466,12 +390,28 @@ function mergePreservedModels(generated, preserved) {
|
|
|
466
390
|
return { ...generated, ...preserved }
|
|
467
391
|
}
|
|
468
392
|
|
|
469
|
-
function
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
return
|
|
393
|
+
function inferModelStrengthHint(currentModel, allModels = []) {
|
|
394
|
+
// Non-interactive fallback: pin a single detected model to every role so
|
|
395
|
+
// agents don't fall through to an unrelated default. Returns {} when nothing
|
|
396
|
+
// is detectable (agents then inherit the top-level OpenCode model).
|
|
397
|
+
if (!currentModel) return {}
|
|
398
|
+
return {
|
|
399
|
+
bronze: currentModel,
|
|
400
|
+
silver: currentModel,
|
|
401
|
+
gold: currentModel,
|
|
402
|
+
fast: currentModel,
|
|
403
|
+
strong: currentModel,
|
|
404
|
+
mini: currentModel,
|
|
405
|
+
explorer: "bronze",
|
|
406
|
+
coder: "gold",
|
|
407
|
+
resolver: "gold",
|
|
408
|
+
reviewer: "gold",
|
|
409
|
+
"deep-reviewer": "gold",
|
|
410
|
+
planner: "gold",
|
|
411
|
+
architect: "gold",
|
|
412
|
+
debugger: "gold",
|
|
413
|
+
researcher: "bronze",
|
|
414
|
+
}
|
|
475
415
|
}
|
|
476
416
|
|
|
477
417
|
function detectOpenCodeModel(config) {
|
|
@@ -502,195 +442,16 @@ function detectOpenCodeModel(config) {
|
|
|
502
442
|
}
|
|
503
443
|
|
|
504
444
|
function buildModelPreset(currentModel, allModels = []) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const glmModel = glmModels[0]
|
|
510
|
-
const gptModel = gptModels[0]
|
|
511
|
-
|
|
512
|
-
if (glmModel && gptModel) {
|
|
513
|
-
const glmTiers = chooseThreeTier(glmModels, "glm", false)
|
|
514
|
-
const gptTiers = chooseThreeTier(gptModels, "gpt", false)
|
|
515
|
-
return {
|
|
516
|
-
mix: "gpt",
|
|
517
|
-
gpt: gptTiers.gold,
|
|
518
|
-
bronze: glmTiers.bronze,
|
|
519
|
-
silver: glmTiers.silver,
|
|
520
|
-
gold: gptTiers.gold,
|
|
521
|
-
"glm-bronze": glmTiers.bronze,
|
|
522
|
-
"glm-silver": glmTiers.silver,
|
|
523
|
-
"glm-gold": glmTiers.gold,
|
|
524
|
-
"gpt-bronze": gptTiers.bronze,
|
|
525
|
-
"gpt-silver": gptTiers.silver,
|
|
526
|
-
"gpt-gold": gptTiers.gold,
|
|
527
|
-
fast: "bronze",
|
|
528
|
-
strong: "gold",
|
|
529
|
-
mini: "bronze",
|
|
530
|
-
codex: "gpt-gold",
|
|
531
|
-
glm: glmTiers.gold,
|
|
532
|
-
explorer: "bronze",
|
|
533
|
-
coder: "silver",
|
|
534
|
-
resolver: "gold",
|
|
535
|
-
reviewer: "gold",
|
|
536
|
-
"deep-reviewer": "gold",
|
|
537
|
-
planner: "gold",
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (!currentModel) {
|
|
542
|
-
return {}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const lower = currentModel.toLowerCase()
|
|
546
|
-
|
|
547
|
-
// GLM / ZAI — GLM-only preset (no GPT dependency, avoids token-exhaustion errors)
|
|
548
|
-
if (lower.includes("glm") || lower.includes("zai")) {
|
|
549
|
-
return buildGLMOnlyPreset(currentModel, glmModels)
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
if (lower.includes("openai/") || lower.includes("gpt")) {
|
|
553
|
-
return buildGPTOnlyPreset(currentModel, gptModels)
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// OpenAI / GPT single-provider preset
|
|
557
|
-
if (lower.includes("openai/") || lower.includes("gpt")) {
|
|
558
|
-
return buildGPTOnlyPreset(currentModel)
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Unknown provider — keep model-neutral
|
|
562
|
-
return {}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function buildGLMOnlyPreset(model, glmModels) {
|
|
566
|
-
const models = glmModels && glmModels.length > 0 ? glmModels : [model]
|
|
567
|
-
const tiers = chooseThreeTier(models, "glm", false)
|
|
568
|
-
return {
|
|
569
|
-
glm: tiers.gold,
|
|
570
|
-
bronze: tiers.bronze,
|
|
571
|
-
silver: tiers.silver,
|
|
572
|
-
gold: tiers.gold,
|
|
573
|
-
fast: "bronze",
|
|
574
|
-
strong: "gold",
|
|
575
|
-
mini: "bronze",
|
|
576
|
-
coder: "gold",
|
|
577
|
-
resolver: "gold",
|
|
578
|
-
reviewer: "gold",
|
|
579
|
-
"deep-reviewer": "gold",
|
|
580
|
-
explorer: "bronze",
|
|
581
|
-
planner: "gold",
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function buildGPTOnlyPreset(model, gptModels) {
|
|
586
|
-
const models = gptModels && gptModels.length > 0 ? gptModels : [model]
|
|
587
|
-
const tiers = chooseThreeTier(models, "gpt", false)
|
|
588
|
-
return {
|
|
589
|
-
gpt: tiers.gold,
|
|
590
|
-
bronze: tiers.bronze,
|
|
591
|
-
silver: tiers.silver,
|
|
592
|
-
gold: tiers.gold,
|
|
593
|
-
fast: "bronze",
|
|
594
|
-
strong: "gold",
|
|
595
|
-
mini: "bronze",
|
|
596
|
-
codex: "gold",
|
|
597
|
-
coder: "silver",
|
|
598
|
-
resolver: "gold",
|
|
599
|
-
explorer: "bronze",
|
|
600
|
-
reviewer: "gold",
|
|
601
|
-
"deep-reviewer": "gold",
|
|
602
|
-
}
|
|
445
|
+
// Non-interactive path: pin a single detected model across all roles.
|
|
446
|
+
// The interactive TTY flow (buildGenericInteractivePreset) handles tier
|
|
447
|
+
// splitting; this is just the headless fallback.
|
|
448
|
+
return inferModelStrengthHint(currentModel, allModels)
|
|
603
449
|
}
|
|
604
450
|
|
|
605
451
|
async function buildInteractivePreset(currentModel, allModels, scriptedAnswers, opencodeConfig) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
if (currentModel) {
|
|
611
|
-
if (isGPTModel(currentModel)) choices.gpt = unique([currentModel, ...choices.gpt])
|
|
612
|
-
if (isGLMModel(currentModel)) choices.glm = unique([currentModel, ...choices.glm])
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const hasGPTOrGLM = allModels.some((m) => isGPTModel(m) || isGLMModel(m))
|
|
616
|
-
const providerCount = opencodeConfig ? detectProvidersFromConfig(opencodeConfig).length : 0
|
|
617
|
-
const offerAuto = !hasGPTOrGLM && providerCount > 0
|
|
618
|
-
const defaultChoice = offerAuto ? "4" : "1"
|
|
619
|
-
|
|
620
|
-
const rl = createPromptInterface(scriptedAnswers)
|
|
621
|
-
try {
|
|
622
|
-
console.log("")
|
|
623
|
-
console.log("──────────────────────────────────────────────────────────────")
|
|
624
|
-
console.log(` opencode-resolve setup`)
|
|
625
|
-
console.log(` Press enter at any prompt to accept the default in [brackets].`)
|
|
626
|
-
console.log("──────────────────────────────────────────────────────────────")
|
|
627
|
-
console.log("")
|
|
628
|
-
console.log(`[${packageName}] Step 1/2 — Choose resolve profile:`)
|
|
629
|
-
console.log(` 1. mix — neutral resolver plus optional Codex and GLM primary agents${offerAuto ? "" : " (recommended)"}`)
|
|
630
|
-
console.log(" 2. gpt — GPT/Codex-only, three-tier")
|
|
631
|
-
console.log(" 3. glm — GLM-only, three-tier")
|
|
632
|
-
if (offerAuto) {
|
|
633
|
-
console.log(` 4. auto — provider-agnostic tier setup from detected providers (recommended)`)
|
|
634
|
-
}
|
|
635
|
-
const validChoices = offerAuto ? ["1", "2", "3", "4"] : ["1", "2", "3"]
|
|
636
|
-
const profileAnswer = await askChoice(rl, `Profile [${validChoices.join(",")}, default ${defaultChoice}]: `, validChoices, defaultChoice)
|
|
637
|
-
|
|
638
|
-
if (profileAnswer === "4" && offerAuto) {
|
|
639
|
-
rl.close()
|
|
640
|
-
const generic = await buildGenericInteractivePreset(currentModel, opencodeConfig, scriptedAnswers)
|
|
641
|
-
if (generic) return generic
|
|
642
|
-
// fall through to mix if generic returned nothing
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const profile = profileAnswer === "2" ? "gpt" : profileAnswer === "3" ? "glm" : "mix"
|
|
646
|
-
|
|
647
|
-
if (profile === "gpt") {
|
|
648
|
-
const tiers = await askThreeTier(rl, "GPT/Codex", choices.gpt)
|
|
649
|
-
return {
|
|
650
|
-
label: "gpt-three-tier",
|
|
651
|
-
profile: "gpt",
|
|
652
|
-
tier: "gold",
|
|
653
|
-
enabled: GPT_ENABLED_AGENTS,
|
|
654
|
-
models: buildGPTThreeTierModels(tiers),
|
|
655
|
-
agents: { gpt: { enabled: true } },
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
if (profile === "glm") {
|
|
660
|
-
// zai and zai-coding-plan are distinct providers — show whatever the user actually
|
|
661
|
-
// has configured. No rewriting between them.
|
|
662
|
-
const tiers = await askThreeTier(rl, "GLM", choices.glm)
|
|
663
|
-
return {
|
|
664
|
-
label: "glm-three-tier",
|
|
665
|
-
profile: "glm",
|
|
666
|
-
tier: "gold",
|
|
667
|
-
enabled: GLM_ENABLED_AGENTS,
|
|
668
|
-
models: buildGLMThreeTierModels(tiers),
|
|
669
|
-
agents: { glm: { enabled: true } },
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const useGPT = await askYesNo(rl, "Enable dedicated GPT primary agent too? [Y/n]: ", true)
|
|
674
|
-
const useGLM = await askYesNo(rl, "Enable dedicated GLM primary agent too? [Y/n]: ", true)
|
|
675
|
-
const gptTiers = await askThreeTier(rl, "GPT", choices.gpt)
|
|
676
|
-
const glmTiers = await askThreeTier(rl, "GLM", choices.glm)
|
|
677
|
-
return {
|
|
678
|
-
label: "mix-three-tier",
|
|
679
|
-
profile: "mix",
|
|
680
|
-
enabled: unique([
|
|
681
|
-
...DEFAULT_ENABLED_AGENTS,
|
|
682
|
-
...(useGPT ? ["gpt"] : []),
|
|
683
|
-
...(useGLM ? ["glm"] : []),
|
|
684
|
-
]),
|
|
685
|
-
models: buildMixedThreeTierModels(gptTiers, glmTiers),
|
|
686
|
-
agents: {
|
|
687
|
-
gpt: { enabled: useGPT },
|
|
688
|
-
glm: { enabled: useGLM },
|
|
689
|
-
},
|
|
690
|
-
}
|
|
691
|
-
} finally {
|
|
692
|
-
rl.close()
|
|
693
|
-
}
|
|
452
|
+
// Single flow: detect providers from opencode.json, pick which API/model to
|
|
453
|
+
// pin, done. No profile selection — model-agnostic by design.
|
|
454
|
+
return buildGenericInteractivePreset(currentModel, opencodeConfig, scriptedAnswers)
|
|
694
455
|
}
|
|
695
456
|
|
|
696
457
|
function createPromptInterface(scriptedAnswers) {
|
|
@@ -712,8 +473,15 @@ async function askThreeTier(rl, label, models) {
|
|
|
712
473
|
const defaults = chooseThreeTier(choices, label.toLowerCase().includes("glm") ? "glm" : "gpt")
|
|
713
474
|
while (true) {
|
|
714
475
|
console.log("")
|
|
715
|
-
console.log(`[${packageName}] ${label} model choices:`)
|
|
716
|
-
choices.forEach((model, index) =>
|
|
476
|
+
console.log(`[${packageName}] ${label} model choices (★ = recommended default for that tier):`)
|
|
477
|
+
choices.forEach((model, index) => {
|
|
478
|
+
const tags = []
|
|
479
|
+
if (model === defaults.bronze) tags.push("★ bronze")
|
|
480
|
+
if (model === defaults.silver) tags.push("★ silver")
|
|
481
|
+
if (model === defaults.gold) tags.push("★ gold")
|
|
482
|
+
const suffix = tags.length > 0 ? ` ${tags.join(" / ")}` : ""
|
|
483
|
+
console.log(` ${index + 1}. ${model}${suffix}`)
|
|
484
|
+
})
|
|
717
485
|
const bronze = await askModel(rl, choices, `Pick ${label} bronze/scout [default ${defaults.bronze}]: `, defaults.bronze)
|
|
718
486
|
const silver = await askModel(rl, choices, `Pick ${label} silver/coder [default ${defaults.silver}]: `, defaults.silver)
|
|
719
487
|
const gold = await askModel(rl, choices, `Pick ${label} gold/reasoner [default ${defaults.gold}]: `, defaults.gold)
|
|
@@ -747,70 +515,6 @@ async function askYesNo(rl, question, defaultValue) {
|
|
|
747
515
|
return answer === "y" || answer === "yes"
|
|
748
516
|
}
|
|
749
517
|
|
|
750
|
-
function buildMixedThreeTierModels(gptTiers, glmTiers) {
|
|
751
|
-
return {
|
|
752
|
-
mix: "gpt-gold",
|
|
753
|
-
gpt: "gpt-gold",
|
|
754
|
-
glm: "glm-gold",
|
|
755
|
-
bronze: "glm-bronze",
|
|
756
|
-
silver: "glm-silver",
|
|
757
|
-
gold: "gpt-gold",
|
|
758
|
-
"gpt-bronze": gptTiers.bronze,
|
|
759
|
-
"gpt-silver": gptTiers.silver,
|
|
760
|
-
"gpt-gold": gptTiers.gold,
|
|
761
|
-
"glm-bronze": glmTiers.bronze,
|
|
762
|
-
"glm-silver": glmTiers.silver,
|
|
763
|
-
"glm-gold": glmTiers.gold,
|
|
764
|
-
fast: "bronze",
|
|
765
|
-
strong: "gold",
|
|
766
|
-
mini: "bronze",
|
|
767
|
-
codex: "gpt-gold",
|
|
768
|
-
explorer: "bronze",
|
|
769
|
-
coder: "silver",
|
|
770
|
-
resolver: "gold",
|
|
771
|
-
reviewer: "gold",
|
|
772
|
-
"deep-reviewer": "gold",
|
|
773
|
-
planner: "gold",
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function buildGPTThreeTierModels(tiers) {
|
|
778
|
-
return {
|
|
779
|
-
gpt: "gold",
|
|
780
|
-
bronze: tiers.bronze,
|
|
781
|
-
silver: tiers.silver,
|
|
782
|
-
gold: tiers.gold,
|
|
783
|
-
fast: "bronze",
|
|
784
|
-
strong: "gold",
|
|
785
|
-
mini: "bronze",
|
|
786
|
-
codex: "gold",
|
|
787
|
-
explorer: "bronze",
|
|
788
|
-
coder: "silver",
|
|
789
|
-
resolver: "gold",
|
|
790
|
-
reviewer: "gold",
|
|
791
|
-
"deep-reviewer": "gold",
|
|
792
|
-
planner: "gold",
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function buildGLMThreeTierModels(tiers) {
|
|
797
|
-
return {
|
|
798
|
-
glm: "gold",
|
|
799
|
-
bronze: tiers.bronze,
|
|
800
|
-
silver: tiers.silver,
|
|
801
|
-
gold: tiers.gold,
|
|
802
|
-
fast: "bronze",
|
|
803
|
-
strong: "gold",
|
|
804
|
-
mini: "bronze",
|
|
805
|
-
explorer: "bronze",
|
|
806
|
-
coder: "gold",
|
|
807
|
-
resolver: "gold",
|
|
808
|
-
reviewer: "gold",
|
|
809
|
-
"deep-reviewer": "gold",
|
|
810
|
-
planner: "gold",
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
518
|
function collectModelChoices(allModels, predicate, hints, includeFallbackHints = true) {
|
|
815
519
|
const detected = allModels.filter(predicate)
|
|
816
520
|
if (detected.length > 0) {
|
|
@@ -831,12 +535,28 @@ function chooseThreeTier(models, family) {
|
|
|
831
535
|
const fallback = family === "glm" ? GLM_MODEL_HINTS : OPENAI_MODEL_HINTS
|
|
832
536
|
return chooseThreeTier(fallback, family)
|
|
833
537
|
}
|
|
834
|
-
//
|
|
538
|
+
// Bucket by tier, then pick the STRONGEST within each bucket so defaults are
|
|
539
|
+
// the newest model of that class (e.g. gpt-5-mini beats gpt-4o-mini for bronze).
|
|
835
540
|
const sorted = sortModelsByStrength(choices)
|
|
541
|
+
return pickFromBuckets(sorted)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function pickFromBuckets(sorted) {
|
|
545
|
+
// bronze = cheapest scout (WEAKEST of bronze bucket — e.g. gpt-4o-mini over gpt-5-mini)
|
|
546
|
+
// silver = strongest workhorse coder (STRONGEST of silver bucket — e.g. gpt-5.3-codex)
|
|
547
|
+
// gold = flagship reasoner (STRONGEST of gold bucket — e.g. gpt-5.5)
|
|
548
|
+
const buckets = [[], [], []]
|
|
549
|
+
for (const m of sorted) buckets[inferModelTier(m)].push(m)
|
|
550
|
+
const weakestOf = (bucket) => bucket.length > 0 ? bucket[0] : null
|
|
551
|
+
const strongestOf = (bucket) => bucket.length > 0 ? bucket[bucket.length - 1] : null
|
|
552
|
+
const n = sorted.length
|
|
553
|
+
const fallbackBronze = sorted[0]
|
|
554
|
+
const fallbackGold = sorted[n - 1]
|
|
555
|
+
const fallbackSilver = n >= 2 ? sorted[Math.floor(n / 2)] : sorted[0]
|
|
836
556
|
return {
|
|
837
|
-
bronze:
|
|
838
|
-
silver:
|
|
839
|
-
gold:
|
|
557
|
+
bronze: weakestOf(buckets[0]) ?? fallbackBronze,
|
|
558
|
+
silver: strongestOf(buckets[1]) ?? fallbackSilver,
|
|
559
|
+
gold: strongestOf(buckets[2]) ?? fallbackGold,
|
|
840
560
|
}
|
|
841
561
|
}
|
|
842
562
|
|
|
@@ -846,6 +566,7 @@ function sortGLMModelChoices(models) {
|
|
|
846
566
|
|
|
847
567
|
function rankGLMModel(model) {
|
|
848
568
|
const lower = model.toLowerCase()
|
|
569
|
+
if (lower.includes("5.2")) return -1
|
|
849
570
|
if (lower.includes("5.1")) return 0
|
|
850
571
|
if (lower.includes("4.5") && !lower.includes("air") && !lower.includes("flash")) return 1
|
|
851
572
|
if (lower.includes("4.5-airx")) return 2
|
|
@@ -897,24 +618,35 @@ function readInstallerOption(name) {
|
|
|
897
618
|
}
|
|
898
619
|
|
|
899
620
|
function getPresetLabel(currentModel) {
|
|
900
|
-
|
|
901
|
-
const lower = currentModel.toLowerCase()
|
|
902
|
-
if (lower.includes("glm") || lower.includes("zai")) return "glm-only"
|
|
903
|
-
if (lower.includes("openai/") || lower.includes("gpt")) return "gpt-only"
|
|
904
|
-
return "inherited"
|
|
621
|
+
return currentModel ? "configured" : "inherited"
|
|
905
622
|
}
|
|
906
623
|
|
|
907
|
-
function
|
|
624
|
+
function inferModelTier(modelId) {
|
|
625
|
+
if (typeof modelId !== "string") return 1
|
|
626
|
+
if (TIER_BRONZE_RE.test(modelId)) return 0
|
|
627
|
+
if (TIER_GOLD_RE.test(modelId)) return 2
|
|
628
|
+
return 1
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function extractModelVersion(modelId) {
|
|
908
632
|
if (typeof modelId !== "string") return 0
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
633
|
+
const match = modelId.match(/\b(\d+(?:\.\d+)?)\b/)
|
|
634
|
+
return match ? Number.parseFloat(match[1]) : 0
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function inferModelStrength(modelId) {
|
|
638
|
+
// Composite ordering: tier first, then version. Used for sortModelsByStrength.
|
|
639
|
+
return inferModelTier(modelId) * 100 + extractModelVersion(modelId)
|
|
914
640
|
}
|
|
915
641
|
|
|
916
642
|
function sortModelsByStrength(models) {
|
|
917
|
-
return [...models].sort((a, b) =>
|
|
643
|
+
return [...models].sort((a, b) => {
|
|
644
|
+
const diff = inferModelStrength(a) - inferModelStrength(b)
|
|
645
|
+
if (diff !== 0) return diff
|
|
646
|
+
// Tie on strength: keep the canonical/shorter variant last so bucket "strongest"
|
|
647
|
+
// picks e.g. gpt-5.3-codex over gpt-5.3-codex-spark.
|
|
648
|
+
return b.length - a.length
|
|
649
|
+
})
|
|
918
650
|
}
|
|
919
651
|
|
|
920
652
|
function detectProvidersFromConfig(config) {
|
|
@@ -1057,23 +789,41 @@ async function buildGenericInteractivePreset(currentModel, opencodeConfig, scrip
|
|
|
1057
789
|
const shapeAns = await askChoice(rl, `Tier shape [1,2,3, default ${shapeDefault}]: `, ["1", "2", "3"], shapeDefault)
|
|
1058
790
|
const shape = shapeAns === "3" ? "three" : shapeAns === "2" ? "two" : "single"
|
|
1059
791
|
|
|
1060
|
-
// 3. Pick models
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
792
|
+
// 3. Pick models — use the same bucket-aware picks as askThreeTier so the
|
|
793
|
+
// generic flow defaults line up with the GPT/GLM tier shortcuts.
|
|
794
|
+
const tierPicks = pickFromBuckets(sorted)
|
|
795
|
+
const n = sorted.length
|
|
796
|
+
const strongest = sorted[n - 1]
|
|
797
|
+
const bronzeDefault = tierPicks.bronze
|
|
798
|
+
const silverDefault = tierPicks.silver
|
|
799
|
+
const goldDefault = tierPicks.gold
|
|
1064
800
|
console.log("")
|
|
1065
|
-
console.log(`[${packageName}] Step 3/3 — Pick models
|
|
1066
|
-
sorted.forEach((m, i) =>
|
|
801
|
+
console.log(`[${packageName}] Step 3/3 — Pick models (★ = recommended default for that tier):`)
|
|
802
|
+
sorted.forEach((m, i) => {
|
|
803
|
+
const tags = []
|
|
804
|
+
if (shape === "three") {
|
|
805
|
+
if (m === bronzeDefault) tags.push("★ bronze")
|
|
806
|
+
if (m === silverDefault) tags.push("★ silver")
|
|
807
|
+
if (m === goldDefault) tags.push("★ gold")
|
|
808
|
+
} else if (shape === "two") {
|
|
809
|
+
if (m === bronzeDefault) tags.push("★ silver")
|
|
810
|
+
if (m === goldDefault) tags.push("★ gold")
|
|
811
|
+
} else if (m === strongest) {
|
|
812
|
+
tags.push("★ all roles")
|
|
813
|
+
}
|
|
814
|
+
const suffix = tags.length > 0 ? ` ${tags.join(" / ")}` : ""
|
|
815
|
+
console.log(` ${i + 1}. ${m}${suffix}`)
|
|
816
|
+
})
|
|
1067
817
|
|
|
1068
818
|
while (true) {
|
|
1069
819
|
const tiers = { shape }
|
|
1070
820
|
if (shape === "three") {
|
|
1071
|
-
tiers.bronze = await askModel(rl, sorted, `Bronze (scout for explorer/researcher) [default ${
|
|
1072
|
-
tiers.silver = await askModel(rl, sorted, `Silver (coder/debugger) [default ${
|
|
1073
|
-
tiers.gold = await askModel(rl, sorted, `Gold (resolver/reviewer/deep-reviewer/planner/architect) [default ${
|
|
821
|
+
tiers.bronze = await askModel(rl, sorted, `Bronze (scout for explorer/researcher) [default ${bronzeDefault}]: `, bronzeDefault)
|
|
822
|
+
tiers.silver = await askModel(rl, sorted, `Silver (coder/debugger) [default ${silverDefault}]: `, silverDefault)
|
|
823
|
+
tiers.gold = await askModel(rl, sorted, `Gold (resolver/reviewer/deep-reviewer/planner/architect) [default ${goldDefault}]: `, goldDefault)
|
|
1074
824
|
} else if (shape === "two") {
|
|
1075
|
-
tiers.silver = await askModel(rl, sorted, `Silver (fast: coder/explorer/debugger/researcher) [default ${
|
|
1076
|
-
tiers.gold = await askModel(rl, sorted, `Gold (strong: resolver/reviewer/…/architect) [default ${
|
|
825
|
+
tiers.silver = await askModel(rl, sorted, `Silver (fast: coder/explorer/debugger/researcher) [default ${bronzeDefault}]: `, bronzeDefault)
|
|
826
|
+
tiers.gold = await askModel(rl, sorted, `Gold (strong: resolver/reviewer/…/architect) [default ${goldDefault}]: `, goldDefault)
|
|
1077
827
|
} else {
|
|
1078
828
|
tiers.gold = await askModel(rl, sorted, `Model for all roles [default ${strongest}]: `, strongest)
|
|
1079
829
|
}
|
|
@@ -1085,8 +835,7 @@ async function buildGenericInteractivePreset(currentModel, opencodeConfig, scrip
|
|
|
1085
835
|
const ok = await askYesNo(rl, `Confirm picks? [Y/n] (n re-asks all): `, true)
|
|
1086
836
|
if (ok) {
|
|
1087
837
|
return {
|
|
1088
|
-
label:
|
|
1089
|
-
profile: "mix",
|
|
838
|
+
label: `${shape}-tier`,
|
|
1090
839
|
enabled: DEFAULT_ENABLED_AGENTS,
|
|
1091
840
|
models: buildGenericResolveModels(tiers),
|
|
1092
841
|
agents: {},
|
|
@@ -1262,115 +1011,3 @@ function formatError(error) {
|
|
|
1262
1011
|
return error instanceof Error ? error.message : String(error)
|
|
1263
1012
|
}
|
|
1264
1013
|
|
|
1265
|
-
async function offerCompanionPlugins() {
|
|
1266
|
-
if (process.env.OPENCODE_RESOLVE_SKIP_COMPANIONS === "1") return
|
|
1267
|
-
|
|
1268
|
-
const config = await readOpenCodeConfig()
|
|
1269
|
-
const existing = collectPluginBaseNames(config.plugin ?? [])
|
|
1270
|
-
const missing = COMPANION_PLUGINS.filter((c) => !existing.has(c.pkg))
|
|
1271
|
-
|
|
1272
|
-
if (missing.length === 0) {
|
|
1273
|
-
console.log(`[${packageName}] recommended companion plugins already present — skipping`)
|
|
1274
|
-
return
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
1278
|
-
|
|
1279
|
-
if (!isInteractive) {
|
|
1280
|
-
console.log(`[${packageName}] recommended companion plugins not detected:`)
|
|
1281
|
-
for (const c of missing) {
|
|
1282
|
-
console.log(` - ${c.pkg} — ${c.desc}`)
|
|
1283
|
-
console.log(` install: opencode plugin ${c.pkg}@latest --global --force`)
|
|
1284
|
-
}
|
|
1285
|
-
return
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
1289
|
-
try {
|
|
1290
|
-
for (const c of missing) {
|
|
1291
|
-
console.log("")
|
|
1292
|
-
console.log(`[${packageName}] recommended companion: ${c.pkg}`)
|
|
1293
|
-
console.log(` ${c.desc}`)
|
|
1294
|
-
const raw = await rl.question(" install now? [Y/n] ")
|
|
1295
|
-
const answer = raw.trim().toLowerCase()
|
|
1296
|
-
const accepted = answer === "" || answer === "y" || answer === "yes"
|
|
1297
|
-
if (!accepted) {
|
|
1298
|
-
console.log(` skipped — install later via: opencode plugin ${c.pkg}@latest --global --force`)
|
|
1299
|
-
continue
|
|
1300
|
-
}
|
|
1301
|
-
const installed = await installCompanion(c.pkg)
|
|
1302
|
-
if (!installed) {
|
|
1303
|
-
console.warn(` ${c.pkg} install command failed — leave plugin list untouched, retry manually`)
|
|
1304
|
-
continue
|
|
1305
|
-
}
|
|
1306
|
-
await addCompanionToOpenCodeConfig(`${c.pkg}@latest`)
|
|
1307
|
-
console.log(` ${c.pkg} cached and registered — restart OpenCode to activate`)
|
|
1308
|
-
}
|
|
1309
|
-
} finally {
|
|
1310
|
-
rl.close()
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
function collectPluginBaseNames(plugins) {
|
|
1315
|
-
const names = new Set()
|
|
1316
|
-
for (const entry of plugins) {
|
|
1317
|
-
const raw = typeof entry === "string"
|
|
1318
|
-
? entry
|
|
1319
|
-
: Array.isArray(entry) && typeof entry[0] === "string"
|
|
1320
|
-
? entry[0]
|
|
1321
|
-
: null
|
|
1322
|
-
if (!raw) continue
|
|
1323
|
-
names.add(stripVersionSuffix(raw))
|
|
1324
|
-
}
|
|
1325
|
-
return names
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
function stripVersionSuffix(name) {
|
|
1329
|
-
if (name.startsWith("@")) {
|
|
1330
|
-
const slashIndex = name.indexOf("/")
|
|
1331
|
-
if (slashIndex === -1) return name
|
|
1332
|
-
const scope = name.slice(0, slashIndex)
|
|
1333
|
-
const rest = name.slice(slashIndex + 1)
|
|
1334
|
-
return `${scope}/${rest.split("@")[0]}`
|
|
1335
|
-
}
|
|
1336
|
-
return name.split("@")[0]
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
async function installCompanion(pkg) {
|
|
1340
|
-
return new Promise((resolveSpawn) => {
|
|
1341
|
-
const child = spawn("opencode", ["plugin", `${pkg}@latest`, "--global", "--force"], {
|
|
1342
|
-
stdio: "inherit",
|
|
1343
|
-
})
|
|
1344
|
-
child.on("exit", (code) => resolveSpawn(code === 0))
|
|
1345
|
-
child.on("error", () => resolveSpawn(false))
|
|
1346
|
-
})
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
async function addCompanionToOpenCodeConfig(pluginEntry) {
|
|
1350
|
-
const baseName = stripVersionSuffix(pluginEntry)
|
|
1351
|
-
const probe = await readOpenCodeConfig()
|
|
1352
|
-
const alreadyPresent = isCompanionPresent(probe, baseName)
|
|
1353
|
-
if (alreadyPresent) return
|
|
1354
|
-
|
|
1355
|
-
const fresh = await readOpenCodeConfig()
|
|
1356
|
-
if (!isCompanionPresent(fresh, baseName)) {
|
|
1357
|
-
fresh.plugin ??= []
|
|
1358
|
-
if (!Array.isArray(fresh.plugin)) {
|
|
1359
|
-
throw new Error(`${opencodeConfigPath}.plugin must be an array`)
|
|
1360
|
-
}
|
|
1361
|
-
fresh.plugin.push(pluginEntry)
|
|
1362
|
-
await writeFile(opencodeConfigPath, `${JSON.stringify(fresh, null, 2)}\n`)
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
function isCompanionPresent(config, baseName) {
|
|
1367
|
-
if (!Array.isArray(config.plugin)) return false
|
|
1368
|
-
return config.plugin.some((entry) => {
|
|
1369
|
-
const raw = typeof entry === "string"
|
|
1370
|
-
? entry
|
|
1371
|
-
: Array.isArray(entry) && typeof entry[0] === "string"
|
|
1372
|
-
? entry[0]
|
|
1373
|
-
: null
|
|
1374
|
-
return raw !== null && stripVersionSuffix(raw) === baseName
|
|
1375
|
-
})
|
|
1376
|
-
}
|