@glrs-dev/cli 2.1.0 → 2.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +18 -4
  3. package/dist/vendor/harness-opencode/dist/agents/prompts/build.open.md +18 -4
  4. package/dist/vendor/harness-opencode/dist/agents/prompts/{qa-thorough.md → code-reviewer-thorough.md} +34 -19
  5. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.md +80 -0
  6. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.open.md +68 -0
  7. package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +2 -0
  8. package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +3 -0
  9. package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +23 -4
  10. package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +146 -87
  11. package/dist/vendor/harness-opencode/dist/agents/prompts/research-auto.md +1 -1
  12. package/dist/vendor/harness-opencode/dist/agents/prompts/research-local.md +1 -1
  13. package/dist/vendor/harness-opencode/dist/agents/prompts/research-web.md +1 -1
  14. package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +2 -0
  15. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +54 -0
  16. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +57 -0
  17. package/dist/vendor/harness-opencode/dist/agents/shared/index.ts +1 -0
  18. package/dist/vendor/harness-opencode/dist/agents/shared/ui-evaluation-ladder.md +50 -0
  19. package/dist/vendor/harness-opencode/dist/agents/shared/workflow-mechanics.md +5 -5
  20. package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +80 -0
  21. package/dist/vendor/harness-opencode/dist/{chunk-VJUETC6A.js → chunk-PDMXYZM4.js} +53 -1
  22. package/dist/vendor/harness-opencode/dist/cli.js +1333 -1646
  23. package/dist/vendor/harness-opencode/dist/commands/prompts/fresh.md +27 -24
  24. package/dist/vendor/harness-opencode/dist/commands/prompts/review.md +3 -3
  25. package/dist/vendor/harness-opencode/dist/commands/prompts/ship.md +2 -0
  26. package/dist/vendor/harness-opencode/dist/index.js +106 -627
  27. package/dist/vendor/harness-opencode/dist/skills/adversarial-review-rubric/SKILL.md +47 -0
  28. package/dist/vendor/harness-opencode/dist/skills/code-quality/SKILL.md +1 -1
  29. package/dist/vendor/harness-opencode/dist/skills/root-cause-diagnosis/SKILL.md +24 -0
  30. package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +166 -0
  31. package/dist/vendor/harness-opencode/package.json +1 -1
  32. package/package.json +1 -1
  33. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-assessor.md +0 -77
  34. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +0 -40
  35. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +0 -56
  36. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-scoper.md +0 -58
  37. package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.md +0 -68
  38. package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.open.md +0 -58
  39. package/dist/vendor/harness-opencode/dist/chunk-6CZPRUMJ.js +0 -869
  40. package/dist/vendor/harness-opencode/dist/chunk-DZG4D3OH.js +0 -54
  41. package/dist/vendor/harness-opencode/dist/chunk-OYRKOEXK.js +0 -88
  42. package/dist/vendor/harness-opencode/dist/commands/prompts/autopilot.md +0 -96
  43. package/dist/vendor/harness-opencode/dist/install-6775ZBDG.js +0 -13
  44. package/dist/vendor/harness-opencode/dist/paths-WZ23ZQOV.js +0 -18
@@ -1,869 +0,0 @@
1
- import {
2
- getOpenCodeCachePackageDir,
3
- inspectCachePin,
4
- readOurPackageVersion,
5
- refreshPluginCache
6
- } from "./chunk-VJUETC6A.js";
7
-
8
- // src/cli/install.ts
9
- import * as fs3 from "fs";
10
- import * as path3 from "path";
11
- import * as os2 from "os";
12
- import { fileURLToPath } from "url";
13
-
14
- // src/cli/merge-config.ts
15
- import * as fs from "fs";
16
- import * as path from "path";
17
- var UNION_ALLOWLIST = /* @__PURE__ */ new Set(["plugin"]);
18
- function isPlainObject(v) {
19
- return typeof v === "object" && v !== null && !Array.isArray(v) && Object.prototype.toString.call(v) === "[object Object]";
20
- }
21
- function deepClone(v) {
22
- if (v === null || typeof v !== "object") return v;
23
- if (Array.isArray(v)) return v.map(deepClone);
24
- const out = {};
25
- for (const k of Object.keys(v)) {
26
- out[k] = deepClone(v[k]);
27
- }
28
- return out;
29
- }
30
- function fmtPath(parts) {
31
- return parts.map((p) => /^[A-Za-z_$][\w$]*$/.test(p) ? p : `["${p.replace(/"/g, '\\"')}"]`).reduce((acc, part) => {
32
- if (acc === "") return part;
33
- if (part.startsWith("[")) return acc + part;
34
- return acc + "." + part;
35
- }, "");
36
- }
37
- function pluginName(entry) {
38
- if (typeof entry === "string") {
39
- const atIdx = entry.indexOf("@", 1);
40
- return atIdx > 0 ? entry.slice(0, atIdx) : entry;
41
- }
42
- if (Array.isArray(entry) && typeof entry[0] === "string") {
43
- const name = entry[0];
44
- const atIdx = name.indexOf("@", 1);
45
- return atIdx > 0 ? name.slice(0, atIdx) : name;
46
- }
47
- return null;
48
- }
49
- function mergeWalk(src, dst, pathParts, additions, warnings) {
50
- for (const key of Object.keys(src)) {
51
- const sv = src[key];
52
- const newPath = pathParts.concat([key]);
53
- const pathStr = fmtPath(newPath);
54
- if (!Object.prototype.hasOwnProperty.call(dst, key)) {
55
- dst[key] = deepClone(sv);
56
- additions.push(`added: ${pathStr}`);
57
- continue;
58
- }
59
- const dv = dst[key];
60
- if (isPlainObject(sv) && isPlainObject(dv)) {
61
- mergeWalk(sv, dv, newPath, additions, warnings);
62
- continue;
63
- }
64
- if (isPlainObject(sv) && !isPlainObject(dv)) {
65
- warnings.push(
66
- `WARN: scalar-vs-object: user has non-object at ${pathStr} where we ship an object; not migrating. To adopt: ${JSON.stringify(sv)}`
67
- );
68
- continue;
69
- }
70
- if (Array.isArray(sv)) {
71
- if (!Array.isArray(dv)) {
72
- warnings.push(
73
- `WARN: scalar-vs-array: user has non-array at ${pathStr} where we ship an array; not migrating. To adopt: ${JSON.stringify(sv)}`
74
- );
75
- continue;
76
- }
77
- const joined = newPath.join(".");
78
- if (UNION_ALLOWLIST.has(joined)) {
79
- for (const item of sv) {
80
- const srcName = pluginName(item);
81
- if (srcName) {
82
- const dstIdx = dv.findIndex(
83
- (x) => pluginName(x) === srcName
84
- );
85
- if (dstIdx >= 0) {
86
- const srcIsTuple = Array.isArray(item) && item.length >= 2;
87
- const dstIsTuple = Array.isArray(dv[dstIdx]) && dv[dstIdx].length >= 2;
88
- if (srcIsTuple && !dstIsTuple) {
89
- dv[dstIdx] = deepClone(item);
90
- additions.push(`upgraded: ${pathStr}[${JSON.stringify(srcName)}] to tuple form`);
91
- }
92
- } else {
93
- dv.push(deepClone(item));
94
- additions.push(`appended: ${pathStr}[${JSON.stringify(item)}]`);
95
- }
96
- } else {
97
- const needle = JSON.stringify(item);
98
- const alreadyPresent = dv.some(
99
- (x) => JSON.stringify(x) === needle
100
- );
101
- if (!alreadyPresent) {
102
- dv.push(deepClone(item));
103
- additions.push(`appended: ${pathStr}[${JSON.stringify(item)}]`);
104
- }
105
- }
106
- }
107
- }
108
- continue;
109
- }
110
- }
111
- }
112
- function mergeConfig(srcJson, dstPath, dryRun = false) {
113
- let dstText;
114
- try {
115
- dstText = fs.readFileSync(dstPath, "utf8");
116
- } catch (e) {
117
- throw new Error(`Failed to read dst ${dstPath}: ${e.message}`);
118
- }
119
- let dst;
120
- try {
121
- dst = JSON.parse(dstText);
122
- } catch (e) {
123
- throw new Error(
124
- `User config at ${dstPath} has invalid JSON: ${e.message}. Not touching the file.`
125
- );
126
- }
127
- if (!isPlainObject(dst)) {
128
- throw new Error(
129
- `User config at ${dstPath} is not a JSON object at the top level.`
130
- );
131
- }
132
- const additions = [];
133
- const warnings = [];
134
- mergeWalk(srcJson, dst, [], additions, warnings);
135
- if (additions.length === 0) {
136
- return { changed: false, warnings };
137
- }
138
- if (dryRun) {
139
- return { changed: true, bakPath: "(dry-run)", additions, warnings };
140
- }
141
- const suffix = `${Date.now()}-${process.pid}`;
142
- const bakPath = `${dstPath}.bak.${suffix}`;
143
- const tmpPath = `${dstPath}.merge.tmp.${suffix}`;
144
- try {
145
- fs.copyFileSync(dstPath, bakPath);
146
- } catch (e) {
147
- throw new Error(`Failed to write backup ${bakPath}: ${e.message}`);
148
- }
149
- const serialized = JSON.stringify(dst, null, 2) + "\n";
150
- try {
151
- fs.writeFileSync(tmpPath, serialized);
152
- } catch (e) {
153
- try {
154
- fs.unlinkSync(bakPath);
155
- } catch {
156
- }
157
- throw new Error(`Failed to write tempfile ${tmpPath}: ${e.message}`);
158
- }
159
- try {
160
- fs.renameSync(tmpPath, dstPath);
161
- } catch (e) {
162
- try {
163
- fs.unlinkSync(tmpPath);
164
- } catch {
165
- }
166
- try {
167
- fs.unlinkSync(bakPath);
168
- } catch {
169
- }
170
- throw new Error(`Failed to rename ${tmpPath} \u2192 ${dstPath}: ${e.message}`);
171
- }
172
- return { changed: true, bakPath, additions, warnings };
173
- }
174
- function seedConfig(srcJson, dstPath) {
175
- fs.mkdirSync(path.dirname(dstPath), { recursive: true });
176
- fs.writeFileSync(dstPath, JSON.stringify(srcJson, null, 2) + "\n");
177
- }
178
-
179
- // src/cli/plugin-check.ts
180
- import * as fs2 from "fs";
181
- import * as path2 from "path";
182
- import * as os from "os";
183
- import { select, checkbox, confirm } from "@inquirer/prompts";
184
- async function promptChoice(question, choices, defaultIndex = 0) {
185
- if (!process.stdin.isTTY) return defaultIndex;
186
- const answer = await select({
187
- message: question,
188
- choices: choices.map((label, i) => ({
189
- name: label,
190
- value: i
191
- })),
192
- default: defaultIndex
193
- });
194
- return answer;
195
- }
196
- async function promptMulti(question, choices) {
197
- if (!process.stdin.isTTY) {
198
- const defaults = /* @__PURE__ */ new Set();
199
- choices.forEach((c2, i) => {
200
- if (c2.defaultOn) defaults.add(i);
201
- });
202
- return defaults;
203
- }
204
- const answers = await checkbox({
205
- message: question,
206
- choices: choices.map((c2, i) => ({
207
- name: c2.label,
208
- value: i,
209
- checked: c2.defaultOn
210
- }))
211
- });
212
- return new Set(answers);
213
- }
214
-
215
- // src/cli/models-dev.ts
216
- var MODELS_DEV_URL = "https://models.dev/api.json";
217
- var FETCH_TIMEOUT_MS = 5e3;
218
- function combinedCost(m) {
219
- const input = m.cost?.input ?? 0;
220
- const output = m.cost?.output ?? 0;
221
- return input + output;
222
- }
223
- function suggestTiersFromModelsDev(provider) {
224
- const models = Object.values(provider.models).sort(
225
- (a, b) => combinedCost(b) - combinedCost(a)
226
- );
227
- if (models.length === 0) {
228
- throw new Error(`Provider "${provider.id}" has no models`);
229
- }
230
- const deep = models[0];
231
- const fast = models[models.length - 1];
232
- let mid;
233
- if (models.length <= 2) {
234
- mid = models.length === 1 ? deep : fast;
235
- } else {
236
- const midCost = (combinedCost(deep) + combinedCost(fast)) / 2;
237
- const candidates = models.filter(
238
- (m) => m.id !== deep.id && m.id !== fast.id
239
- );
240
- mid = candidates.reduce(
241
- (best, m) => Math.abs(combinedCost(m) - midCost) < Math.abs(combinedCost(best) - midCost) ? m : best
242
- );
243
- }
244
- const ref = (m) => `${provider.id}/${m.id}`;
245
- return {
246
- deep: ref(deep),
247
- mid: ref(mid),
248
- fast: ref(fast)
249
- };
250
- }
251
- function pickBedrockTierIds(provider) {
252
- const models = Object.values(provider.models);
253
- const mostRecent = (candidates) => {
254
- if (candidates.length === 0) return null;
255
- return [...candidates].sort((a, b) => {
256
- const aDate = a.last_updated ?? "";
257
- const bDate = b.last_updated ?? "";
258
- if (aDate !== bDate) return bDate.localeCompare(aDate);
259
- return b.id.localeCompare(a.id);
260
- })[0];
261
- };
262
- const pickFamily = (familyKeyword) => {
263
- const globalCandidates = models.filter(
264
- (m) => m.id.startsWith(`global.anthropic.claude-${familyKeyword}-`)
265
- );
266
- const globalPick = mostRecent(globalCandidates);
267
- if (globalPick) return globalPick;
268
- const nonPrefixedCandidates = models.filter(
269
- (m) => m.id.startsWith(`anthropic.claude-${familyKeyword}-`)
270
- );
271
- return mostRecent(nonPrefixedCandidates);
272
- };
273
- const opus = pickFamily("opus");
274
- const sonnet = pickFamily("sonnet");
275
- const haiku = pickFamily("haiku");
276
- if (!opus || !sonnet || !haiku) {
277
- return suggestTiersFromModelsDev(provider);
278
- }
279
- const ref = (m) => `${provider.id}/${m.id}`;
280
- return {
281
- deep: ref(opus),
282
- mid: ref(sonnet),
283
- fast: ref(haiku)
284
- };
285
- }
286
- async function fetchModelsDevProviders() {
287
- const controller = new AbortController();
288
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
289
- try {
290
- const res = await fetch(MODELS_DEV_URL, { signal: controller.signal });
291
- if (!res.ok) return null;
292
- const data = await res.json();
293
- if (!data || typeof data !== "object" || Array.isArray(data)) return null;
294
- const providers = [];
295
- for (const [key, rawValue] of Object.entries(
296
- data
297
- )) {
298
- if (!rawValue || typeof rawValue !== "object") continue;
299
- const value = rawValue;
300
- if (typeof value.id !== "string" || value.id !== key) continue;
301
- if (typeof value.name !== "string") continue;
302
- if (!value.models || typeof value.models !== "object") continue;
303
- providers.push(value);
304
- }
305
- if (providers.length === 0) return null;
306
- return providers;
307
- } catch {
308
- return null;
309
- } finally {
310
- clearTimeout(timer);
311
- }
312
- }
313
-
314
- // src/cli/install.ts
315
- var PLUGIN_NAME = "@glrs-dev/harness-plugin-opencode";
316
- var c = {
317
- reset: "\x1B[0m",
318
- green: "\x1B[32m",
319
- yellow: "\x1B[33m",
320
- blue: "\x1B[34m",
321
- dim: "\x1B[2m",
322
- bold: "\x1B[1m"
323
- };
324
- var ok = (msg) => console.log(`${c.green}\u2713${c.reset} ${msg}`);
325
- var info = (msg) => console.log(`${c.blue}\u2022${c.reset} ${msg}`);
326
- var warn = (msg) => console.log(`${c.yellow}!${c.reset} ${msg}`);
327
- var MODEL_PRESETS = [
328
- {
329
- label: "Anthropic API (direct)",
330
- providerId: "anthropic",
331
- deep: "anthropic/claude-opus-4-7",
332
- mid: "anthropic/claude-sonnet-4-6",
333
- fast: "anthropic/claude-haiku-4-5-20251001"
334
- },
335
- {
336
- label: "AWS Bedrock",
337
- providerId: "amazon-bedrock",
338
- deep: "amazon-bedrock/global.anthropic.claude-opus-4-7",
339
- mid: "amazon-bedrock/global.anthropic.claude-sonnet-4-6",
340
- fast: "amazon-bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0"
341
- },
342
- {
343
- label: "Google Vertex AI (Claude)",
344
- providerId: "google-vertex-anthropic",
345
- deep: "google-vertex-anthropic/claude-opus-4-7@default",
346
- mid: "google-vertex-anthropic/claude-sonnet-4-6@default",
347
- fast: "google-vertex-anthropic/claude-haiku-4-5@20251001"
348
- }
349
- ];
350
- var MCP_TOGGLES = [
351
- { name: "playwright", label: "Playwright \u2014 browser automation", defaultOn: false },
352
- { name: "linear", label: "Linear \u2014 issue tracker integration", defaultOn: false }
353
- ];
354
- function extractPluginOptions(config) {
355
- if (!config) return null;
356
- const plugins = config.plugin;
357
- if (!Array.isArray(plugins)) return null;
358
- for (const entry of plugins) {
359
- if (Array.isArray(entry) && entry.length >= 2 && (entry[0] === PLUGIN_NAME || String(entry[0]).startsWith(`${PLUGIN_NAME}@`))) {
360
- return entry[1];
361
- }
362
- }
363
- return null;
364
- }
365
- function readPackageVersion() {
366
- const here = path3.dirname(fileURLToPath(import.meta.url));
367
- const candidates = [
368
- path3.join(here, "..", "package.json"),
369
- path3.join(here, "..", "..", "package.json")
370
- ];
371
- for (const candidate of candidates) {
372
- try {
373
- const raw = fs3.readFileSync(candidate, "utf8");
374
- const parsed = JSON.parse(raw);
375
- if (parsed.name === PLUGIN_NAME && typeof parsed.version === "string") {
376
- return parsed.version;
377
- }
378
- } catch {
379
- }
380
- }
381
- throw new Error(
382
- `Could not locate ${PLUGIN_NAME}'s package.json to read version`
383
- );
384
- }
385
- function getOpencodeConfigPath() {
386
- const configHome = process.env["XDG_CONFIG_HOME"] ?? path3.join(os2.homedir(), ".config");
387
- return path3.join(configHome, "opencode", "opencode.json");
388
- }
389
- async function refreshPluginCacheIfStale() {
390
- try {
391
- const cacheDir = getOpenCodeCachePackageDir();
392
- const pin = await inspectCachePin(cacheDir);
393
- if (pin.kind !== "exact") return;
394
- const ourVersion = readOurPackageVersion(import.meta.url);
395
- if (pin.version === ourVersion) return;
396
- const result = await refreshPluginCache(pin.version, ourVersion);
397
- if (result.outcome === "refreshed") {
398
- ok(`Plugin cache updated: ${result.fromVersion} \u2192 ${result.toVersion}`);
399
- }
400
- } catch {
401
- }
402
- }
403
- function readExistingConfig(configPath) {
404
- if (!fs3.existsSync(configPath)) return null;
405
- try {
406
- return JSON.parse(fs3.readFileSync(configPath, "utf8"));
407
- } catch {
408
- return null;
409
- }
410
- }
411
- function detectModelProvider(existing) {
412
- const opts = extractPluginOptions(existing);
413
- const models = opts?.models ?? existing?.harness?.models;
414
- if (!models) return null;
415
- const deep = Array.isArray(models.deep) ? models.deep[0] : models.deep;
416
- if (typeof deep !== "string") return null;
417
- for (const preset of MODEL_PRESETS) {
418
- if (deep === preset.deep) return preset.label;
419
- }
420
- return `custom (${deep})`;
421
- }
422
- function detectEnabledMcps(existing) {
423
- const enabled = /* @__PURE__ */ new Set();
424
- const mcp = existing?.mcp;
425
- if (!mcp || typeof mcp !== "object") return enabled;
426
- for (const toggle of MCP_TOGGLES) {
427
- if (mcp[toggle.name]?.enabled === true) {
428
- enabled.add(toggle.name);
429
- }
430
- }
431
- return enabled;
432
- }
433
- function migrateHarnessKeyToPluginOptions(configPath) {
434
- try {
435
- if (!fs3.existsSync(configPath)) return;
436
- const raw = fs3.readFileSync(configPath, "utf8");
437
- const config = JSON.parse(raw);
438
- if (!config.harness || typeof config.harness !== "object") return;
439
- const plugins = Array.isArray(config.plugin) ? config.plugin : [];
440
- const pluginIdx = plugins.findIndex((entry) => {
441
- const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
442
- return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
443
- });
444
- if (pluginIdx < 0) return;
445
- const current = plugins[pluginIdx];
446
- const existingName = typeof current === "string" ? current : Array.isArray(current) ? current[0] : PLUGIN_NAME;
447
- const existingOpts = Array.isArray(current) && current.length >= 2 ? current[1] : {};
448
- const merged = { ...config.harness, ...existingOpts };
449
- plugins[pluginIdx] = [existingName, merged];
450
- delete config.harness;
451
- const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
452
- fs3.copyFileSync(configPath, bakPath);
453
- fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
454
- ok("Migrated legacy `harness` config into plugin options");
455
- info(`Backup: ${bakPath}`);
456
- } catch {
457
- }
458
- }
459
- function deepEqual(a, b) {
460
- if (a === b) return true;
461
- if (typeof a !== typeof b) return false;
462
- if (a === null || b === null) return a === b;
463
- if (typeof a !== "object") return false;
464
- const aObj = a;
465
- const bObj = b;
466
- const aKeys = Object.keys(aObj);
467
- const bKeys = Object.keys(bObj);
468
- if (aKeys.length !== bKeys.length) return false;
469
- for (const key of aKeys) {
470
- if (!bKeys.includes(key)) return false;
471
- if (!deepEqual(aObj[key], bObj[key])) return false;
472
- }
473
- return true;
474
- }
475
- function writePluginOption(configPath, subKey, value, opts) {
476
- try {
477
- if (!fs3.existsSync(configPath)) {
478
- return { changed: false };
479
- }
480
- const raw = fs3.readFileSync(configPath, "utf8");
481
- const config = JSON.parse(raw);
482
- if (!Array.isArray(config.plugin)) {
483
- return { changed: false };
484
- }
485
- const pluginIdx = config.plugin.findIndex((entry) => {
486
- const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
487
- return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
488
- });
489
- if (pluginIdx < 0) {
490
- return { changed: false };
491
- }
492
- const current = config.plugin[pluginIdx];
493
- const existingName = typeof current === "string" ? current : Array.isArray(current) ? current[0] : PLUGIN_NAME;
494
- const existingOpts = Array.isArray(current) && current.length >= 2 ? current[1] : {};
495
- if (deepEqual(existingOpts[subKey], value)) {
496
- return { changed: false };
497
- }
498
- const newOpts = { ...existingOpts, [subKey]: value };
499
- if (opts.dryRun) {
500
- info(`[dry-run] Would reconfigure ${subKey} in plugin options`);
501
- return { changed: true };
502
- }
503
- const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
504
- fs3.copyFileSync(configPath, bakPath);
505
- config.plugin[pluginIdx] = [existingName, newOpts];
506
- fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
507
- ok(`Reconfigured ${subKey}`);
508
- info(`Backup: ${bakPath}`);
509
- return { changed: true, bakPath };
510
- } catch {
511
- return { changed: false };
512
- }
513
- }
514
- function writeMcpToggles(configPath, enabledSet, opts) {
515
- try {
516
- if (!fs3.existsSync(configPath)) {
517
- return { changed: false };
518
- }
519
- const raw = fs3.readFileSync(configPath, "utf8");
520
- const config = JSON.parse(raw);
521
- const toggleNames = new Set(MCP_TOGGLES.map((t) => t.name));
522
- const existingMcp = config.mcp && typeof config.mcp === "object" ? { ...config.mcp } : {};
523
- const newMcp = {};
524
- let hasChanges = false;
525
- for (const [key, val] of Object.entries(existingMcp)) {
526
- if (!toggleNames.has(key)) {
527
- newMcp[key] = val;
528
- }
529
- }
530
- for (const toggleName of toggleNames) {
531
- if (enabledSet.has(toggleName)) {
532
- newMcp[toggleName] = { enabled: true };
533
- if (!deepEqual(existingMcp[toggleName], { enabled: true })) {
534
- hasChanges = true;
535
- }
536
- } else {
537
- if (existingMcp[toggleName] !== void 0) {
538
- hasChanges = true;
539
- }
540
- }
541
- }
542
- if (!hasChanges && Object.keys(newMcp).length === Object.keys(existingMcp).length) {
543
- const allKeysMatch = Object.keys(newMcp).every(
544
- (k) => deepEqual(newMcp[k], existingMcp[k])
545
- );
546
- if (allKeysMatch) {
547
- return { changed: false };
548
- }
549
- }
550
- if (opts.dryRun) {
551
- info(`[dry-run] Would reconfigure MCP toggles`);
552
- return { changed: true };
553
- }
554
- const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
555
- fs3.copyFileSync(configPath, bakPath);
556
- if (Object.keys(newMcp).length > 0) {
557
- config.mcp = newMcp;
558
- } else {
559
- delete config.mcp;
560
- }
561
- fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
562
- ok("Reconfigured MCPs");
563
- info(`Backup: ${bakPath}`);
564
- return { changed: true, bakPath };
565
- } catch {
566
- return { changed: false };
567
- }
568
- }
569
- async function install(opts = {}) {
570
- const { dryRun = false, pin = false, nonInteractive = false } = opts;
571
- const configPath = getOpencodeConfigPath();
572
- const pluginEntry = pin ? `${PLUGIN_NAME}@${readPackageVersion()}` : PLUGIN_NAME;
573
- const interactive = !nonInteractive && process.stdin.isTTY === true;
574
- const existing = readExistingConfig(configPath);
575
- const hasPlugin = existing ? (Array.isArray(existing.plugin) ? existing.plugin : []).some(
576
- (p) => {
577
- const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
578
- return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
579
- }
580
- ) : false;
581
- const existingProvider = detectModelProvider(existing);
582
- const existingMcps = detectEnabledMcps(existing);
583
- const existingOpts = extractPluginOptions(existing);
584
- let hasModels = !!(existingOpts?.models ?? existing?.harness?.models);
585
- console.log(`
586
- ${c.bold}${c.blue}@glrs-dev/harness-plugin-opencode${c.reset} setup
587
- `);
588
- if (hasPlugin) {
589
- ok("Plugin already registered");
590
- }
591
- if (existingProvider) {
592
- ok(`Models: ${existingProvider}`);
593
- }
594
- if (existingMcps.size > 0) {
595
- ok(`MCPs: ${[...existingMcps].join(", ")} enabled`);
596
- }
597
- let reconfigureModels = false;
598
- let reconfigureMcps = false;
599
- let newModelsValue = null;
600
- let newMcpEnabledSet = /* @__PURE__ */ new Set();
601
- if (hasPlugin && (existingProvider || hasModels)) {
602
- const unconfiguredMcps = MCP_TOGGLES.filter(
603
- (t) => !existingMcps.has(t.name) && !existing?.mcp?.[t.name]
604
- );
605
- if (interactive) {
606
- const reconfigure = await promptChoice(
607
- " Reconfigure models?",
608
- ["No, keep current config", "Yes, reconfigure models"],
609
- 0
610
- );
611
- if (reconfigure === 1) {
612
- reconfigureModels = true;
613
- hasModels = false;
614
- }
615
- if (existingMcps.size > 0) {
616
- const reconfigureMcpChoice = await promptChoice(
617
- " Reconfigure MCPs?",
618
- ["No, keep current config", "Yes, reconfigure MCPs"],
619
- 0
620
- );
621
- if (reconfigureMcpChoice === 1) {
622
- reconfigureMcps = true;
623
- }
624
- }
625
- if (!reconfigureModels && !reconfigureMcps && unconfiguredMcps.length === 0) {
626
- console.log(`
627
- ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
628
- `);
629
- return;
630
- }
631
- } else if (unconfiguredMcps.length === 0) {
632
- console.log(`
633
- ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
634
- `);
635
- return;
636
- }
637
- }
638
- const pluginOpts = {};
639
- if (interactive && !hasModels) {
640
- console.log();
641
- console.log(`${c.dim}Models${c.reset}`);
642
- info("Fetching available providers\u2026");
643
- const modelsDevProviders = await fetchModelsDevProviders();
644
- let preset = null;
645
- if (modelsDevProviders && modelsDevProviders.length > 0) {
646
- const providerChoices = modelsDevProviders.map((p) => p.name);
647
- providerChoices.push("Keep defaults (no model config)");
648
- providerChoices.push("Custom (enter model IDs manually)");
649
- const keepDefaultsIdx = providerChoices.length - 2;
650
- const providerIdx = await promptChoice(
651
- " Which model provider?",
652
- providerChoices,
653
- keepDefaultsIdx
654
- );
655
- if (providerIdx < modelsDevProviders.length) {
656
- const provider = modelsDevProviders[providerIdx];
657
- ok(`Provider: ${provider.name}`);
658
- const suggested = provider.id === "amazon-bedrock" ? pickBedrockTierIds(provider) : suggestTiersFromModelsDev(provider);
659
- const modelChoices = Object.keys(provider.models).map(
660
- (modelId) => `${provider.id}/${modelId}`
661
- );
662
- const tiers = [
663
- { tier: "deep", suggested: suggested.deep },
664
- { tier: "mid", suggested: suggested.mid },
665
- { tier: "fast", suggested: suggested.fast }
666
- ];
667
- const picked = {};
668
- for (const { tier, suggested: suggestedModel } of tiers) {
669
- const defaultIdx = modelChoices.indexOf(suggestedModel);
670
- const idx = await promptChoice(
671
- ` ${tier} model?`,
672
- modelChoices,
673
- defaultIdx >= 0 ? defaultIdx : 0
674
- );
675
- picked[tier] = modelChoices[idx];
676
- info(` ${tier} \u2192 ${picked[tier]}`);
677
- }
678
- preset = {
679
- label: provider.name,
680
- providerId: provider.id,
681
- deep: picked["deep"],
682
- mid: picked["mid"],
683
- fast: picked["fast"]
684
- };
685
- } else if (providerIdx === modelsDevProviders.length) {
686
- ok("Models: OpenCode defaults");
687
- pluginOpts._skipModels = true;
688
- }
689
- } else {
690
- warn("Could not reach Models.dev API \u2014 using built-in presets");
691
- const presetLabels = [...MODEL_PRESETS.map((p) => p.label), "Keep defaults (no model config)", "Custom (enter model IDs manually)"];
692
- const keepDefaultsOfflineIdx = presetLabels.length - 2;
693
- const choice = await promptChoice(
694
- " Which model provider?",
695
- presetLabels,
696
- keepDefaultsOfflineIdx
697
- );
698
- if (choice < MODEL_PRESETS.length) {
699
- preset = MODEL_PRESETS[choice];
700
- ok(`Provider: ${preset.label}`);
701
- } else if (choice === MODEL_PRESETS.length) {
702
- ok("Models: OpenCode defaults");
703
- pluginOpts._skipModels = true;
704
- }
705
- }
706
- if (preset) {
707
- pluginOpts.models = {
708
- deep: [preset.deep],
709
- mid: [preset.mid],
710
- fast: [preset.fast]
711
- };
712
- newModelsValue = {
713
- deep: [preset.deep],
714
- mid: [preset.mid],
715
- fast: [preset.fast]
716
- };
717
- ok(`Models configured`);
718
- const midExecIdx = await promptChoice(
719
- " Use a strict executor for build agents? (recommended for Kimi/Qwen/DeepSeek)",
720
- ["No (use mid model as reasoning builder)", "Yes (configure mid-execute model)"],
721
- 0
722
- );
723
- if (midExecIdx === 1) {
724
- const { input } = await import("@inquirer/prompts");
725
- const midExecModel = await input({
726
- message: " mid-execute model ID:",
727
- default: preset.mid
728
- });
729
- if (midExecModel) {
730
- pluginOpts.models["mid-execute"] = [midExecModel];
731
- newModelsValue["mid-execute"] = [midExecModel];
732
- info(` mid-execute \u2192 ${midExecModel} (strict executor prompts)`);
733
- }
734
- } else {
735
- info(` mid-execute: skipped (build agents use mid model with reasoning prompts)`);
736
- }
737
- } else if (!pluginOpts._skipModels) {
738
- info("Enter model IDs in <provider>/<model-id> format (e.g. amazon-bedrock/global.anthropic.claude-opus-4-7)");
739
- const { input } = await import("@inquirer/prompts");
740
- const deepModel = await input({ message: " deep (most capable):" });
741
- const midModel = await input({ message: " mid (balanced):" });
742
- const fastModel = await input({ message: " fast (cheapest):" });
743
- if (deepModel) {
744
- const resolvedMid = midModel || deepModel;
745
- pluginOpts.models = {
746
- deep: [deepModel],
747
- mid: [resolvedMid],
748
- fast: [fastModel || midModel || deepModel]
749
- };
750
- newModelsValue = {
751
- deep: [deepModel],
752
- mid: [resolvedMid],
753
- fast: [fastModel || midModel || deepModel]
754
- };
755
- ok("Models: custom");
756
- const midExecModel = await input({ message: " mid-execute (optional strict executor, press Enter to skip):" });
757
- if (midExecModel) {
758
- pluginOpts.models["mid-execute"] = [midExecModel];
759
- newModelsValue["mid-execute"] = [midExecModel];
760
- info(` mid-execute \u2192 ${midExecModel} (strict executor prompts)`);
761
- } else {
762
- info(` mid-execute: skipped (build agents use mid model with reasoning prompts)`);
763
- }
764
- } else {
765
- ok("Models: OpenCode defaults");
766
- }
767
- }
768
- delete pluginOpts._skipModels;
769
- console.log();
770
- }
771
- if (interactive && reconfigureMcps) {
772
- console.log(`${c.dim}Reconfigure MCP servers${c.reset}`);
773
- const currentEnabled = new Set(existingMcps);
774
- const selected = await promptMulti(
775
- " Select MCPs to enable:",
776
- MCP_TOGGLES.map((t) => ({ label: t.label, defaultOn: currentEnabled.has(t.name) }))
777
- );
778
- newMcpEnabledSet = new Set([...selected].map((i) => MCP_TOGGLES[i].name));
779
- const names = [...newMcpEnabledSet].join(", ");
780
- if (newMcpEnabledSet.size > 0) {
781
- ok(`MCPs to enable: ${names}`);
782
- } else {
783
- ok("MCPs: all disabled");
784
- }
785
- console.log();
786
- }
787
- const pluginValue = Object.keys(pluginOpts).length > 0 ? [pluginEntry, pluginOpts] : pluginEntry;
788
- const config = {
789
- $schema: "https://opencode.ai/config.json",
790
- plugin: [pluginValue]
791
- };
792
- if (interactive) {
793
- const unconfigured = MCP_TOGGLES.filter(
794
- (t) => !existingMcps.has(t.name) && !existing?.mcp?.[t.name]
795
- );
796
- if (unconfigured.length > 0) {
797
- console.log(`${c.dim}Optional MCP servers (serena, memory, git are always on)${c.reset}`);
798
- const selected = await promptMulti(
799
- " Enable additional MCPs?",
800
- unconfigured.map((t) => ({ label: t.label, defaultOn: t.defaultOn }))
801
- );
802
- if (selected.size > 0) {
803
- const mcp = {};
804
- for (const idx of selected) {
805
- const toggle = unconfigured[idx];
806
- mcp[toggle.name] = { enabled: true };
807
- }
808
- config.mcp = mcp;
809
- const names = [...selected].map((i) => unconfigured[i].name).join(", ");
810
- ok(`MCPs enabled: ${names}`);
811
- } else {
812
- ok("MCPs: defaults only");
813
- }
814
- console.log();
815
- }
816
- }
817
- if (reconfigureModels && newModelsValue) {
818
- writePluginOption(configPath, "models", newModelsValue, { dryRun });
819
- }
820
- if (reconfigureMcps) {
821
- writeMcpToggles(configPath, newMcpEnabledSet, { dryRun });
822
- }
823
- if (!fs3.existsSync(configPath)) {
824
- if (dryRun) {
825
- info(`[dry-run] Would create ${configPath}`);
826
- info(`[dry-run] Config: ${JSON.stringify(config, null, 2)}`);
827
- } else {
828
- seedConfig(config, configPath);
829
- ok(`Created ${configPath}`);
830
- }
831
- } else {
832
- try {
833
- const result = mergeConfig(config, configPath, dryRun);
834
- if (!result.changed) {
835
- ok("opencode.json is up to date");
836
- for (const w of result.warnings) warn(w);
837
- } else {
838
- if (dryRun) {
839
- info(`[dry-run] Would merge into ${configPath}:`);
840
- for (const a of result.additions) info(` ${a}`);
841
- } else {
842
- ok(`Updated ${configPath}`);
843
- info(`Backup: ${result.bakPath}`);
844
- for (const a of result.additions) info(` ${a}`);
845
- }
846
- for (const w of result.warnings) warn(w);
847
- }
848
- } catch (e) {
849
- console.error(`\x1B[31m\u2717\x1B[0m ${e.message}`);
850
- process.exit(1);
851
- }
852
- }
853
- if (!dryRun) {
854
- migrateHarnessKeyToPluginOptions(configPath);
855
- }
856
- if (!dryRun) {
857
- await refreshPluginCacheIfStale();
858
- }
859
- console.log(`
860
- ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
861
- `);
862
- }
863
-
864
- export {
865
- MODEL_PRESETS,
866
- writePluginOption,
867
- writeMcpToggles,
868
- install
869
- };