@glrs-dev/cli 2.2.0 → 2.4.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/CHANGELOG.md +4 -0
- package/dist/{chunk-EM4MJBOD.js → chunk-2AZKRWC6.js} +4 -4
- package/dist/{chunk-UXBOTMDY.js → chunk-2P3ETOT2.js} +2 -2
- package/dist/chunk-2VMFXAJH.js +795 -0
- package/dist/chunk-5ZVUFNCP.js +140 -0
- package/dist/{chunk-W37UX3U2.js → chunk-6Y27RQQL.js} +2 -2
- package/dist/{chunk-RZWOWTKF.js → chunk-EKNRKZWR.js} +4 -4
- package/dist/{chunk-YGNDPKIW.js → chunk-HQUCVJ4G.js} +3 -1
- package/dist/{chunk-OABVEBWW.js → chunk-MBEVC327.js} +1 -1
- package/dist/{chunk-SB3MLROC.js → chunk-MCM47HH4.js} +8 -3
- package/dist/{chunk-F3AFRUT2.js → chunk-PTIO556V.js} +2 -2
- package/dist/{chunk-E2UNZIZT.js → chunk-R2WXQ54P.js} +1 -1
- package/dist/{chunk-I2KUXY3I.js → chunk-SMDIOB5B.js} +2 -2
- package/dist/{chunk-SPULDN7P.js → chunk-YY7EWHMA.js} +5 -3
- package/dist/cli.js +31 -20
- package/dist/commands/autopilot-interactive.d.ts +89 -0
- package/dist/commands/autopilot-interactive.js +248 -0
- package/dist/commands/autopilot-raw.d.ts +1 -0
- package/dist/commands/autopilot-raw.js +368 -0
- package/dist/commands/autopilot-tui.d.ts +7 -0
- package/dist/commands/autopilot-tui.js +7 -0
- package/dist/commands/autopilot.d.ts +39 -0
- package/dist/commands/autopilot.js +395 -0
- package/dist/commands/cleanup.js +3 -3
- package/dist/commands/create.js +4 -4
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +1549 -0
- package/dist/commands/debrief.d.ts +57 -0
- package/dist/commands/debrief.js +9 -0
- package/dist/commands/delete.js +3 -3
- package/dist/commands/go.js +2 -2
- package/dist/commands/list.js +3 -3
- package/dist/commands/loop.d.ts +42 -0
- package/dist/commands/loop.js +133 -0
- package/dist/commands/plan-picker.d.ts +15 -0
- package/dist/commands/plan-picker.js +76 -0
- package/dist/commands/scoper.d.ts +54 -0
- package/dist/commands/scoper.js +341 -0
- package/dist/commands/switch.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lib/auto-update.js +1 -1
- package/dist/lib/config.d.ts +3 -2
- package/dist/lib/config.js +1 -1
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +1 -1
- package/dist/lib/worktree.js +3 -3
- package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +16 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer-thorough.md +6 -7
- package/dist/vendor/harness-opencode/dist/agents/prompts/debriefer.md +55 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +2 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +104 -7
- package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +4 -2
- package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +129 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +0 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +0 -1
- package/dist/vendor/harness-opencode/dist/chunk-GILWWWMB.js +66 -0
- package/dist/vendor/harness-opencode/dist/cli.js +328 -687
- package/dist/vendor/harness-opencode/dist/index.js +123 -20
- package/dist/vendor/harness-opencode/dist/plugin-check-GJRD2OK6.js +14 -0
- package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +2 -1
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +10 -2
- package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +0 -80
- package/dist/vendor/harness-opencode/dist/bin/plan-check.sh +0 -255
|
@@ -6,25 +6,25 @@ import {
|
|
|
6
6
|
refreshPluginCache,
|
|
7
7
|
validateModelOverride
|
|
8
8
|
} from "./chunk-PDMXYZM4.js";
|
|
9
|
+
import {
|
|
10
|
+
promptChoice,
|
|
11
|
+
promptMulti
|
|
12
|
+
} from "./chunk-GILWWWMB.js";
|
|
9
13
|
|
|
10
14
|
// src/cli.ts
|
|
11
15
|
import {
|
|
12
16
|
binary,
|
|
13
17
|
command as command2,
|
|
14
18
|
flag,
|
|
15
|
-
|
|
16
|
-
optional as optional2,
|
|
17
|
-
positional as positional2,
|
|
18
|
-
restPositionals,
|
|
19
|
-
string,
|
|
19
|
+
positional,
|
|
20
20
|
subcommands,
|
|
21
21
|
run
|
|
22
22
|
} from "cmd-ts";
|
|
23
23
|
|
|
24
24
|
// src/cli/install.ts
|
|
25
|
-
import * as
|
|
26
|
-
import * as
|
|
27
|
-
import * as
|
|
25
|
+
import * as fs2 from "fs";
|
|
26
|
+
import * as path2 from "path";
|
|
27
|
+
import * as os from "os";
|
|
28
28
|
import { fileURLToPath } from "url";
|
|
29
29
|
|
|
30
30
|
// src/cli/merge-config.ts
|
|
@@ -192,42 +192,6 @@ function seedConfig(srcJson, dstPath) {
|
|
|
192
192
|
fs.writeFileSync(dstPath, JSON.stringify(srcJson, null, 2) + "\n");
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
// src/cli/plugin-check.ts
|
|
196
|
-
import * as fs2 from "fs";
|
|
197
|
-
import * as path2 from "path";
|
|
198
|
-
import * as os from "os";
|
|
199
|
-
import { select, checkbox, confirm } from "@inquirer/prompts";
|
|
200
|
-
async function promptChoice(question, choices, defaultIndex = 0) {
|
|
201
|
-
if (!process.stdin.isTTY) return defaultIndex;
|
|
202
|
-
const answer = await select({
|
|
203
|
-
message: question,
|
|
204
|
-
choices: choices.map((label, i) => ({
|
|
205
|
-
name: label,
|
|
206
|
-
value: i
|
|
207
|
-
})),
|
|
208
|
-
default: defaultIndex
|
|
209
|
-
});
|
|
210
|
-
return answer;
|
|
211
|
-
}
|
|
212
|
-
async function promptMulti(question, choices) {
|
|
213
|
-
if (!process.stdin.isTTY) {
|
|
214
|
-
const defaults = /* @__PURE__ */ new Set();
|
|
215
|
-
choices.forEach((c3, i) => {
|
|
216
|
-
if (c3.defaultOn) defaults.add(i);
|
|
217
|
-
});
|
|
218
|
-
return defaults;
|
|
219
|
-
}
|
|
220
|
-
const answers = await checkbox({
|
|
221
|
-
message: question,
|
|
222
|
-
choices: choices.map((c3, i) => ({
|
|
223
|
-
name: c3.label,
|
|
224
|
-
value: i,
|
|
225
|
-
checked: c3.defaultOn
|
|
226
|
-
}))
|
|
227
|
-
});
|
|
228
|
-
return new Set(answers);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
195
|
// src/cli/models-dev.ts
|
|
232
196
|
var MODELS_DEV_URL = "https://models.dev/api.json";
|
|
233
197
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
@@ -386,14 +350,14 @@ function extractPluginOptions(config) {
|
|
|
386
350
|
return null;
|
|
387
351
|
}
|
|
388
352
|
function readPackageVersion() {
|
|
389
|
-
const here =
|
|
353
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
390
354
|
const candidates = [
|
|
391
|
-
|
|
392
|
-
|
|
355
|
+
path2.join(here, "..", "package.json"),
|
|
356
|
+
path2.join(here, "..", "..", "package.json")
|
|
393
357
|
];
|
|
394
358
|
for (const candidate of candidates) {
|
|
395
359
|
try {
|
|
396
|
-
const raw =
|
|
360
|
+
const raw = fs2.readFileSync(candidate, "utf8");
|
|
397
361
|
const parsed = JSON.parse(raw);
|
|
398
362
|
if (parsed.name === PLUGIN_NAME && typeof parsed.version === "string") {
|
|
399
363
|
return parsed.version;
|
|
@@ -406,8 +370,8 @@ function readPackageVersion() {
|
|
|
406
370
|
);
|
|
407
371
|
}
|
|
408
372
|
function getOpencodeConfigPath() {
|
|
409
|
-
const configHome = process.env["XDG_CONFIG_HOME"] ??
|
|
410
|
-
return
|
|
373
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path2.join(os.homedir(), ".config");
|
|
374
|
+
return path2.join(configHome, "opencode", "opencode.json");
|
|
411
375
|
}
|
|
412
376
|
async function refreshPluginCacheIfStale() {
|
|
413
377
|
try {
|
|
@@ -424,9 +388,9 @@ async function refreshPluginCacheIfStale() {
|
|
|
424
388
|
}
|
|
425
389
|
}
|
|
426
390
|
function readExistingConfig(configPath) {
|
|
427
|
-
if (!
|
|
391
|
+
if (!fs2.existsSync(configPath)) return null;
|
|
428
392
|
try {
|
|
429
|
-
return JSON.parse(
|
|
393
|
+
return JSON.parse(fs2.readFileSync(configPath, "utf8"));
|
|
430
394
|
} catch {
|
|
431
395
|
return null;
|
|
432
396
|
}
|
|
@@ -467,14 +431,14 @@ function detectEnabledPluginToggles(existing) {
|
|
|
467
431
|
}
|
|
468
432
|
function migrateHarnessKeyToPluginOptions(configPath) {
|
|
469
433
|
try {
|
|
470
|
-
if (!
|
|
471
|
-
const raw =
|
|
434
|
+
if (!fs2.existsSync(configPath)) return;
|
|
435
|
+
const raw = fs2.readFileSync(configPath, "utf8");
|
|
472
436
|
const config = JSON.parse(raw);
|
|
473
437
|
if (!config.harness || typeof config.harness !== "object") return;
|
|
474
438
|
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
475
439
|
const pluginIdx = plugins.findIndex((entry) => {
|
|
476
440
|
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
477
|
-
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
|
|
441
|
+
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`) || String(name ?? "").includes("harness-opencode");
|
|
478
442
|
});
|
|
479
443
|
if (pluginIdx < 0) return;
|
|
480
444
|
const current = plugins[pluginIdx];
|
|
@@ -484,8 +448,8 @@ function migrateHarnessKeyToPluginOptions(configPath) {
|
|
|
484
448
|
plugins[pluginIdx] = [existingName, merged];
|
|
485
449
|
delete config.harness;
|
|
486
450
|
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
487
|
-
|
|
488
|
-
|
|
451
|
+
fs2.copyFileSync(configPath, bakPath);
|
|
452
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
489
453
|
ok("Migrated legacy `harness` config into plugin options");
|
|
490
454
|
info(`Backup: ${bakPath}`);
|
|
491
455
|
} catch {
|
|
@@ -509,17 +473,17 @@ function deepEqual(a, b) {
|
|
|
509
473
|
}
|
|
510
474
|
function writePluginOption(configPath, subKey, value, opts) {
|
|
511
475
|
try {
|
|
512
|
-
if (!
|
|
476
|
+
if (!fs2.existsSync(configPath)) {
|
|
513
477
|
return { changed: false };
|
|
514
478
|
}
|
|
515
|
-
const raw =
|
|
479
|
+
const raw = fs2.readFileSync(configPath, "utf8");
|
|
516
480
|
const config = JSON.parse(raw);
|
|
517
481
|
if (!Array.isArray(config.plugin)) {
|
|
518
482
|
return { changed: false };
|
|
519
483
|
}
|
|
520
484
|
const pluginIdx = config.plugin.findIndex((entry) => {
|
|
521
485
|
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
522
|
-
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
|
|
486
|
+
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`) || String(name ?? "").includes("harness-opencode");
|
|
523
487
|
});
|
|
524
488
|
if (pluginIdx < 0) {
|
|
525
489
|
return { changed: false };
|
|
@@ -536,9 +500,9 @@ function writePluginOption(configPath, subKey, value, opts) {
|
|
|
536
500
|
return { changed: true };
|
|
537
501
|
}
|
|
538
502
|
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
539
|
-
|
|
503
|
+
fs2.copyFileSync(configPath, bakPath);
|
|
540
504
|
config.plugin[pluginIdx] = [existingName, newOpts];
|
|
541
|
-
|
|
505
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
542
506
|
ok(`Reconfigured ${subKey}`);
|
|
543
507
|
info(`Backup: ${bakPath}`);
|
|
544
508
|
return { changed: true, bakPath };
|
|
@@ -548,10 +512,10 @@ function writePluginOption(configPath, subKey, value, opts) {
|
|
|
548
512
|
}
|
|
549
513
|
function writeMcpToggles(configPath, enabledSet, opts) {
|
|
550
514
|
try {
|
|
551
|
-
if (!
|
|
515
|
+
if (!fs2.existsSync(configPath)) {
|
|
552
516
|
return { changed: false };
|
|
553
517
|
}
|
|
554
|
-
const raw =
|
|
518
|
+
const raw = fs2.readFileSync(configPath, "utf8");
|
|
555
519
|
const config = JSON.parse(raw);
|
|
556
520
|
const toggleNames = new Set(MCP_TOGGLES.map((t) => t.name));
|
|
557
521
|
const existingMcp = config.mcp && typeof config.mcp === "object" ? { ...config.mcp } : {};
|
|
@@ -587,13 +551,13 @@ function writeMcpToggles(configPath, enabledSet, opts) {
|
|
|
587
551
|
return { changed: true };
|
|
588
552
|
}
|
|
589
553
|
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
590
|
-
|
|
554
|
+
fs2.copyFileSync(configPath, bakPath);
|
|
591
555
|
if (Object.keys(newMcp).length > 0) {
|
|
592
556
|
config.mcp = newMcp;
|
|
593
557
|
} else {
|
|
594
558
|
delete config.mcp;
|
|
595
559
|
}
|
|
596
|
-
|
|
560
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
597
561
|
ok("Reconfigured MCPs");
|
|
598
562
|
info(`Backup: ${bakPath}`);
|
|
599
563
|
return { changed: true, bakPath };
|
|
@@ -603,10 +567,10 @@ function writeMcpToggles(configPath, enabledSet, opts) {
|
|
|
603
567
|
}
|
|
604
568
|
function writePluginToggles(configPath, enabledSet, opts) {
|
|
605
569
|
try {
|
|
606
|
-
if (!
|
|
570
|
+
if (!fs2.existsSync(configPath)) {
|
|
607
571
|
return { changed: false };
|
|
608
572
|
}
|
|
609
|
-
const raw =
|
|
573
|
+
const raw = fs2.readFileSync(configPath, "utf8");
|
|
610
574
|
const config = JSON.parse(raw);
|
|
611
575
|
const toggleNames = new Set(PLUGIN_TOGGLES.map((t) => t.name));
|
|
612
576
|
const existingPlugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
@@ -634,7 +598,7 @@ function writePluginToggles(configPath, enabledSet, opts) {
|
|
|
634
598
|
return { changed: true };
|
|
635
599
|
}
|
|
636
600
|
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
637
|
-
|
|
601
|
+
fs2.copyFileSync(configPath, bakPath);
|
|
638
602
|
const newPlugins = existingPlugins.filter((entry) => {
|
|
639
603
|
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
640
604
|
return !(typeof name === "string" && toRemove.has(name));
|
|
@@ -643,7 +607,7 @@ function writePluginToggles(configPath, enabledSet, opts) {
|
|
|
643
607
|
newPlugins.push(name);
|
|
644
608
|
}
|
|
645
609
|
config.plugin = newPlugins;
|
|
646
|
-
|
|
610
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
647
611
|
ok("Reconfigured plugin add-ons");
|
|
648
612
|
info(`Backup: ${bakPath}`);
|
|
649
613
|
return { changed: true, bakPath };
|
|
@@ -660,7 +624,7 @@ async function install(opts = {}) {
|
|
|
660
624
|
const hasPlugin = existing ? (Array.isArray(existing.plugin) ? existing.plugin : []).some(
|
|
661
625
|
(p) => {
|
|
662
626
|
const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
|
|
663
|
-
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
|
|
627
|
+
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`) || String(name ?? "").includes("harness-opencode");
|
|
664
628
|
}
|
|
665
629
|
) : false;
|
|
666
630
|
const existingProvider = detectModelProvider(existing);
|
|
@@ -957,7 +921,7 @@ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
|
957
921
|
if (reconfigurePluginToggles) {
|
|
958
922
|
writePluginToggles(configPath, newPluginToggleEnabledSet, { dryRun });
|
|
959
923
|
}
|
|
960
|
-
if (!
|
|
924
|
+
if (!fs2.existsSync(configPath)) {
|
|
961
925
|
if (dryRun) {
|
|
962
926
|
info(`[dry-run] Would create ${configPath}`);
|
|
963
927
|
info(`[dry-run] Config: ${JSON.stringify(config, null, 2)}`);
|
|
@@ -1002,36 +966,36 @@ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
|
1002
966
|
}
|
|
1003
967
|
|
|
1004
968
|
// src/cli/uninstall.ts
|
|
1005
|
-
import * as
|
|
1006
|
-
import * as
|
|
1007
|
-
import * as
|
|
969
|
+
import * as fs3 from "fs";
|
|
970
|
+
import * as path3 from "path";
|
|
971
|
+
import * as os2 from "os";
|
|
1008
972
|
var PLUGIN_NAME2 = "@glrs-dev/harness-plugin-opencode";
|
|
1009
973
|
function getOpencodeConfigPath2() {
|
|
1010
|
-
const configHome = process.env["XDG_CONFIG_HOME"] ??
|
|
1011
|
-
return
|
|
974
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path3.join(os2.homedir(), ".config");
|
|
975
|
+
return path3.join(configHome, "opencode", "opencode.json");
|
|
1012
976
|
}
|
|
1013
977
|
function uninstall(opts = {}) {
|
|
1014
978
|
const { dryRun = false } = opts;
|
|
1015
979
|
const configPath = getOpencodeConfigPath2();
|
|
1016
|
-
const
|
|
980
|
+
const c4 = {
|
|
1017
981
|
reset: "\x1B[0m",
|
|
1018
982
|
green: "\x1B[32m",
|
|
1019
983
|
yellow: "\x1B[33m",
|
|
1020
984
|
blue: "\x1B[34m"
|
|
1021
985
|
};
|
|
1022
|
-
const
|
|
1023
|
-
const
|
|
1024
|
-
const warn2 = (msg) => console.log(`${
|
|
986
|
+
const ok3 = (msg) => console.log(`${c4.green}\u2713${c4.reset} ${msg}`);
|
|
987
|
+
const info3 = (msg) => console.log(`${c4.blue}\u2022${c4.reset} ${msg}`);
|
|
988
|
+
const warn2 = (msg) => console.log(`${c4.yellow}!${c4.reset} ${msg}`);
|
|
1025
989
|
console.log(`
|
|
1026
|
-
${
|
|
990
|
+
${c4.blue}Uninstalling ${PLUGIN_NAME2}${c4.reset}
|
|
1027
991
|
`);
|
|
1028
|
-
if (!
|
|
992
|
+
if (!fs3.existsSync(configPath)) {
|
|
1029
993
|
warn2(`No opencode.json found at ${configPath} \u2014 nothing to do`);
|
|
1030
994
|
return;
|
|
1031
995
|
}
|
|
1032
996
|
let raw;
|
|
1033
997
|
try {
|
|
1034
|
-
raw =
|
|
998
|
+
raw = fs3.readFileSync(configPath, "utf8");
|
|
1035
999
|
} catch (e) {
|
|
1036
1000
|
console.error(`\x1B[31m\u2717\x1B[0m Failed to read ${configPath}: ${e.message}`);
|
|
1037
1001
|
process.exit(1);
|
|
@@ -1053,12 +1017,12 @@ ${c3.blue}Uninstalling ${PLUGIN_NAME2}${c3.reset}
|
|
|
1053
1017
|
return;
|
|
1054
1018
|
}
|
|
1055
1019
|
if (dryRun) {
|
|
1056
|
-
|
|
1020
|
+
info3(`[dry-run] Would remove "${PLUGIN_NAME2}" from plugin array in ${configPath}`);
|
|
1057
1021
|
return;
|
|
1058
1022
|
}
|
|
1059
1023
|
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
1060
1024
|
try {
|
|
1061
|
-
|
|
1025
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
1062
1026
|
} catch (e) {
|
|
1063
1027
|
console.error(`\x1B[31m\u2717\x1B[0m Failed to write backup: ${e.message}`);
|
|
1064
1028
|
process.exit(1);
|
|
@@ -1066,32 +1030,32 @@ ${c3.blue}Uninstalling ${PLUGIN_NAME2}${c3.reset}
|
|
|
1066
1030
|
config.plugin = filtered;
|
|
1067
1031
|
const tmpPath = `${configPath}.uninstall.tmp.${Date.now()}-${process.pid}`;
|
|
1068
1032
|
try {
|
|
1069
|
-
|
|
1070
|
-
|
|
1033
|
+
fs3.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1034
|
+
fs3.renameSync(tmpPath, configPath);
|
|
1071
1035
|
} catch (e) {
|
|
1072
1036
|
try {
|
|
1073
|
-
|
|
1037
|
+
fs3.unlinkSync(tmpPath);
|
|
1074
1038
|
} catch {
|
|
1075
1039
|
}
|
|
1076
1040
|
console.error(`\x1B[31m\u2717\x1B[0m Failed to write config: ${e.message}`);
|
|
1077
1041
|
process.exit(1);
|
|
1078
1042
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1043
|
+
ok3(`Removed "${PLUGIN_NAME2}" from ${configPath}`);
|
|
1044
|
+
info3(`Backup: ${bakPath}`);
|
|
1081
1045
|
console.log(`
|
|
1082
1046
|
To fully remove the package: bun remove @glrs-dev/harness-plugin-opencode
|
|
1083
1047
|
`);
|
|
1084
1048
|
}
|
|
1085
1049
|
|
|
1086
1050
|
// src/cli/doctor.ts
|
|
1087
|
-
import * as
|
|
1088
|
-
import * as
|
|
1089
|
-
import * as
|
|
1051
|
+
import * as fs4 from "fs";
|
|
1052
|
+
import * as path4 from "path";
|
|
1053
|
+
import * as os3 from "os";
|
|
1090
1054
|
import { execSync } from "child_process";
|
|
1091
1055
|
var PLUGIN_NAME3 = "@glrs-dev/harness-plugin-opencode";
|
|
1092
1056
|
function getOpencodeConfigPath3() {
|
|
1093
|
-
const configHome = process.env["XDG_CONFIG_HOME"] ??
|
|
1094
|
-
return
|
|
1057
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path4.join(os3.homedir(), ".config");
|
|
1058
|
+
return path4.join(configHome, "opencode", "opencode.json");
|
|
1095
1059
|
}
|
|
1096
1060
|
function cmd(command3) {
|
|
1097
1061
|
try {
|
|
@@ -1104,29 +1068,29 @@ function which(bin) {
|
|
|
1104
1068
|
return cmd(`which ${bin}`) !== null;
|
|
1105
1069
|
}
|
|
1106
1070
|
function doctor() {
|
|
1107
|
-
const
|
|
1071
|
+
const c4 = {
|
|
1108
1072
|
reset: "\x1B[0m",
|
|
1109
1073
|
green: "\x1B[32m",
|
|
1110
1074
|
yellow: "\x1B[33m",
|
|
1111
1075
|
red: "\x1B[31m",
|
|
1112
1076
|
bold: "\x1B[1m"
|
|
1113
1077
|
};
|
|
1114
|
-
const
|
|
1115
|
-
const warn2 = (msg) => console.log(`${
|
|
1116
|
-
const fail = (msg) => console.log(`${
|
|
1078
|
+
const ok3 = (msg) => console.log(`${c4.green}\u2713${c4.reset} ${msg}`);
|
|
1079
|
+
const warn2 = (msg) => console.log(`${c4.yellow}!${c4.reset} ${msg}`);
|
|
1080
|
+
const fail = (msg) => console.log(`${c4.red}\u2717${c4.reset} ${msg}`);
|
|
1117
1081
|
console.log(`
|
|
1118
|
-
${
|
|
1082
|
+
${c4.bold}Doctor \u2014 ${PLUGIN_NAME3}${c4.reset}
|
|
1119
1083
|
`);
|
|
1120
1084
|
const ocVersion = cmd("opencode --version 2>/dev/null | head -1");
|
|
1121
1085
|
if (ocVersion) {
|
|
1122
|
-
|
|
1086
|
+
ok3(`opencode ${ocVersion}`);
|
|
1123
1087
|
} else {
|
|
1124
1088
|
fail("opencode CLI not found \u2014 install from https://opencode.ai");
|
|
1125
1089
|
}
|
|
1126
1090
|
const configPath = getOpencodeConfigPath3();
|
|
1127
|
-
if (
|
|
1091
|
+
if (fs4.existsSync(configPath)) {
|
|
1128
1092
|
try {
|
|
1129
|
-
const config = JSON.parse(
|
|
1093
|
+
const config = JSON.parse(fs4.readFileSync(configPath, "utf8"));
|
|
1130
1094
|
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1131
1095
|
let pluginOptions = null;
|
|
1132
1096
|
const hasPlugin = plugins.some((p) => {
|
|
@@ -1144,7 +1108,7 @@ ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
|
|
|
1144
1108
|
return false;
|
|
1145
1109
|
});
|
|
1146
1110
|
if (hasPlugin) {
|
|
1147
|
-
|
|
1111
|
+
ok3(`"${PLUGIN_NAME3}" present in opencode.json plugin array`);
|
|
1148
1112
|
} else {
|
|
1149
1113
|
warn2(`"${PLUGIN_NAME3}" NOT in opencode.json plugin array \u2014 run: bunx ${PLUGIN_NAME3} install`);
|
|
1150
1114
|
}
|
|
@@ -1181,20 +1145,20 @@ ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
|
|
|
1181
1145
|
}
|
|
1182
1146
|
}
|
|
1183
1147
|
if (invalid.length === 0) {
|
|
1184
|
-
|
|
1148
|
+
ok3("model overrides look valid");
|
|
1185
1149
|
} else {
|
|
1186
1150
|
for (const entry of invalid) {
|
|
1187
1151
|
fail(`invalid model override at ${entry.keyPath}: "${entry.value}"`);
|
|
1188
1152
|
if (entry.reason) {
|
|
1189
|
-
console.log(` ${
|
|
1153
|
+
console.log(` ${c4.yellow}reason:${c4.reset} ${entry.reason}`);
|
|
1190
1154
|
}
|
|
1191
1155
|
if (entry.suggestion) {
|
|
1192
1156
|
console.log(
|
|
1193
|
-
` ${
|
|
1157
|
+
` ${c4.yellow}fix:${c4.reset} remove this key, or replace with \`${entry.suggestion}\``
|
|
1194
1158
|
);
|
|
1195
1159
|
} else {
|
|
1196
1160
|
console.log(
|
|
1197
|
-
` ${
|
|
1161
|
+
` ${c4.yellow}fix:${c4.reset} remove this key, or run \`bunx ${PLUGIN_NAME3} install\` to pick a current preset`
|
|
1198
1162
|
);
|
|
1199
1163
|
}
|
|
1200
1164
|
}
|
|
@@ -1207,563 +1171,292 @@ ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
|
|
|
1207
1171
|
warn2(`No opencode.json at ${configPath} \u2014 run: bunx ${PLUGIN_NAME3} install`);
|
|
1208
1172
|
}
|
|
1209
1173
|
if (which("uvx")) {
|
|
1210
|
-
|
|
1174
|
+
ok3("uvx (serena + git MCPs)");
|
|
1211
1175
|
} else {
|
|
1212
1176
|
warn2("uvx not found \u2014 serena and git MCPs won't work. Install: brew install uv");
|
|
1213
1177
|
}
|
|
1214
1178
|
if (which("node") && which("npx")) {
|
|
1215
|
-
|
|
1179
|
+
ok3(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
|
|
1216
1180
|
} else {
|
|
1217
1181
|
warn2("node/npx not found \u2014 memory MCP won't work");
|
|
1218
1182
|
}
|
|
1219
|
-
const planCheckResult = cmd(`bunx ${PLUGIN_NAME3} plan-check --help 2>/dev/null`);
|
|
1220
|
-
if (planCheckResult !== null) {
|
|
1221
|
-
ok2("plan-check CLI invokable");
|
|
1222
|
-
} else {
|
|
1223
|
-
warn2("plan-check CLI not invokable \u2014 try: bun install");
|
|
1224
|
-
}
|
|
1225
1183
|
if (which("bun")) {
|
|
1226
|
-
|
|
1184
|
+
ok3(`bun ${cmd("bun --version") ?? ""}`);
|
|
1227
1185
|
} else if (which("npm")) {
|
|
1228
|
-
|
|
1186
|
+
ok3(`npm ${cmd("npm --version") ?? ""} (install bun for faster installs)`);
|
|
1229
1187
|
} else {
|
|
1230
1188
|
fail("Neither bun nor npm found \u2014 cannot install plugins");
|
|
1231
1189
|
}
|
|
1232
1190
|
console.log();
|
|
1233
1191
|
}
|
|
1234
1192
|
|
|
1235
|
-
// src/
|
|
1236
|
-
import {
|
|
1237
|
-
import
|
|
1238
|
-
import
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1256
|
-
if (!scriptPath) {
|
|
1257
|
-
console.error("plan-check: could not find plan-check.sh");
|
|
1258
|
-
process.exit(2);
|
|
1259
|
-
}
|
|
1260
|
-
try {
|
|
1261
|
-
execFileSync("bash", [scriptPath, ...args], {
|
|
1262
|
-
stdio: "inherit",
|
|
1263
|
-
encoding: "utf8"
|
|
1264
|
-
});
|
|
1265
|
-
} catch (e) {
|
|
1266
|
-
process.exit(e.status ?? 1);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// src/plan-paths.ts
|
|
1271
|
-
import { execFile } from "child_process";
|
|
1272
|
-
import * as fs6 from "fs/promises";
|
|
1273
|
-
import * as os5 from "os";
|
|
1274
|
-
import * as path6 from "path";
|
|
1275
|
-
function execFileP(file, args, opts = {}) {
|
|
1276
|
-
const { cwd, timeoutMs = 5e3 } = opts;
|
|
1277
|
-
return new Promise((resolve2, reject) => {
|
|
1278
|
-
const controller = new AbortController();
|
|
1279
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1280
|
-
execFile(
|
|
1281
|
-
file,
|
|
1282
|
-
args,
|
|
1283
|
-
{ signal: controller.signal, cwd, encoding: "utf8" },
|
|
1284
|
-
(err, stdout) => {
|
|
1285
|
-
clearTimeout(timer);
|
|
1286
|
-
if (err) {
|
|
1287
|
-
reject(err);
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
resolve2(stdout ?? "");
|
|
1291
|
-
}
|
|
1292
|
-
);
|
|
1293
|
-
});
|
|
1294
|
-
}
|
|
1295
|
-
function expandTilde(p) {
|
|
1296
|
-
if (p === "~") return os5.homedir();
|
|
1297
|
-
if (p.startsWith("~/")) return path6.join(os5.homedir(), p.slice(2));
|
|
1298
|
-
return p;
|
|
1193
|
+
// src/cli/configure.ts
|
|
1194
|
+
import { command } from "cmd-ts";
|
|
1195
|
+
import * as fs5 from "fs";
|
|
1196
|
+
import * as path5 from "path";
|
|
1197
|
+
import * as os4 from "os";
|
|
1198
|
+
var PLUGIN_NAME4 = "@glrs-dev/harness-plugin-opencode";
|
|
1199
|
+
var c2 = {
|
|
1200
|
+
reset: "\x1B[0m",
|
|
1201
|
+
green: "\x1B[32m",
|
|
1202
|
+
yellow: "\x1B[33m",
|
|
1203
|
+
blue: "\x1B[34m",
|
|
1204
|
+
dim: "\x1B[2m",
|
|
1205
|
+
bold: "\x1B[1m",
|
|
1206
|
+
cyan: "\x1B[36m"
|
|
1207
|
+
};
|
|
1208
|
+
var ok2 = (msg) => console.log(`${c2.green}\u2713${c2.reset} ${msg}`);
|
|
1209
|
+
var info2 = (msg) => console.log(`${c2.blue}\u2022${c2.reset} ${msg}`);
|
|
1210
|
+
function getOpencodeConfigPath4() {
|
|
1211
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path5.join(os4.homedir(), ".config");
|
|
1212
|
+
return path5.join(configHome, "opencode", "opencode.json");
|
|
1299
1213
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1214
|
+
function readConfig(configPath) {
|
|
1215
|
+
if (!fs5.existsSync(configPath)) return null;
|
|
1302
1216
|
try {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
{ cwd: worktreeDir }
|
|
1307
|
-
);
|
|
1308
|
-
} catch (err) {
|
|
1309
|
-
const msg = err instanceof Error ? err.message : "unknown error running `git rev-parse --git-common-dir`";
|
|
1310
|
-
throw new Error(
|
|
1311
|
-
`getRepoFolder: failed to resolve git-common-dir for ${worktreeDir}: ${msg}`
|
|
1312
|
-
);
|
|
1313
|
-
}
|
|
1314
|
-
const gitCommonDir = stdout.trim();
|
|
1315
|
-
if (!gitCommonDir) {
|
|
1316
|
-
throw new Error(
|
|
1317
|
-
`getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
|
|
1318
|
-
);
|
|
1217
|
+
return JSON.parse(fs5.readFileSync(configPath, "utf8"));
|
|
1218
|
+
} catch {
|
|
1219
|
+
return null;
|
|
1319
1220
|
}
|
|
1320
|
-
const absCommonDir = path6.isAbsolute(gitCommonDir) ? gitCommonDir : path6.resolve(worktreeDir, gitCommonDir);
|
|
1321
|
-
const repoRoot = path6.dirname(absCommonDir);
|
|
1322
|
-
return path6.basename(repoRoot);
|
|
1323
1221
|
}
|
|
1324
|
-
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1332
|
-
async function migratePlans(worktreeDir, planDir) {
|
|
1333
|
-
const oldDir = path6.join(worktreeDir, ".agent", "plans");
|
|
1334
|
-
const marker = path6.join(oldDir, ".migrated");
|
|
1335
|
-
try {
|
|
1336
|
-
await fs6.stat(oldDir);
|
|
1337
|
-
} catch {
|
|
1338
|
-
return;
|
|
1222
|
+
function extractPluginOptions2(config) {
|
|
1223
|
+
const plugins = config.plugin;
|
|
1224
|
+
if (!Array.isArray(plugins)) return null;
|
|
1225
|
+
for (const entry of plugins) {
|
|
1226
|
+
if (Array.isArray(entry) && entry.length >= 2 && (entry[0] === PLUGIN_NAME4 || String(entry[0]).startsWith(`${PLUGIN_NAME4}@`) || String(entry[0]).startsWith("file://"))) {
|
|
1227
|
+
return entry[1];
|
|
1228
|
+
}
|
|
1339
1229
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
var TIER_LABELS = {
|
|
1233
|
+
deep: "@plan, @prime, @architecture-advisor",
|
|
1234
|
+
mid: "@build, @docs-maintainer, @lib-reader",
|
|
1235
|
+
"mid-execute": "@build, @spec-reviewer, @code-reviewer (overrides mid when set)",
|
|
1236
|
+
"autopilot-execute": "autopilot --fast",
|
|
1237
|
+
fast: "@code-searcher"
|
|
1238
|
+
};
|
|
1239
|
+
var TIERS = ["deep", "mid", "mid-execute", "autopilot-execute", "fast"];
|
|
1240
|
+
async function configureModels(configPath, currentModels) {
|
|
1241
|
+
console.log(`
|
|
1242
|
+
${c2.bold}Current model configuration:${c2.reset}
|
|
1243
|
+
`);
|
|
1244
|
+
for (const tier2 of TIERS) {
|
|
1245
|
+
const model = currentModels[tier2]?.[0] ?? "(not set)";
|
|
1246
|
+
const label = TIER_LABELS[tier2] ?? tier2;
|
|
1247
|
+
console.log(` ${c2.cyan}${label}${c2.reset}`);
|
|
1248
|
+
console.log(` ${model}
|
|
1249
|
+
`);
|
|
1344
1250
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1251
|
+
console.log();
|
|
1252
|
+
const tierChoices = [
|
|
1253
|
+
...TIERS.map((t) => {
|
|
1254
|
+
const label = TIER_LABELS[t] ?? t;
|
|
1255
|
+
const model = currentModels[t]?.[0] ?? "(not set)";
|
|
1256
|
+
return `${label} \u2192 ${model}`;
|
|
1257
|
+
}),
|
|
1258
|
+
"\u2190 Back"
|
|
1259
|
+
];
|
|
1260
|
+
const tierIdx = await promptChoice("Which tier to change?", tierChoices, tierChoices.length - 1);
|
|
1261
|
+
if (tierIdx >= TIERS.length) return;
|
|
1262
|
+
const tier = TIERS[tierIdx];
|
|
1263
|
+
info2("Fetching available models\u2026");
|
|
1264
|
+
const providers = await fetchModelsDevProviders();
|
|
1265
|
+
if (!providers || providers.length === 0) {
|
|
1266
|
+
console.log(`${c2.yellow}!${c2.reset} Could not reach Models.dev API. Enter model ID manually.`);
|
|
1267
|
+
const { input } = await import("@inquirer/prompts");
|
|
1268
|
+
const modelId = await input({
|
|
1269
|
+
message: ` ${tier} model ID:`,
|
|
1270
|
+
default: currentModels[tier]?.[0] ?? ""
|
|
1271
|
+
});
|
|
1272
|
+
if (modelId) {
|
|
1273
|
+
const newModels2 = { ...currentModels, [tier]: [modelId] };
|
|
1274
|
+
writePluginOption(configPath, "models", newModels2, { dryRun: false });
|
|
1275
|
+
}
|
|
1349
1276
|
return;
|
|
1350
1277
|
}
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
try {
|
|
1360
|
-
await fs6.stat(dst);
|
|
1361
|
-
dstExists = true;
|
|
1362
|
-
} catch {
|
|
1363
|
-
dstExists = false;
|
|
1364
|
-
}
|
|
1365
|
-
if (!dstExists) {
|
|
1366
|
-
await fs6.rename(src, dst);
|
|
1367
|
-
continue;
|
|
1368
|
-
}
|
|
1369
|
-
const [srcBuf, dstBuf] = await Promise.all([
|
|
1370
|
-
fs6.readFile(src),
|
|
1371
|
-
fs6.readFile(dst)
|
|
1372
|
-
]);
|
|
1373
|
-
if (srcBuf.equals(dstBuf)) {
|
|
1374
|
-
await fs6.unlink(src);
|
|
1375
|
-
continue;
|
|
1278
|
+
const allModels = [];
|
|
1279
|
+
for (const provider of providers) {
|
|
1280
|
+
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
1281
|
+
allModels.push({
|
|
1282
|
+
id: `${provider.id}/${modelId}`,
|
|
1283
|
+
provider: provider.name,
|
|
1284
|
+
name: model.name ?? modelId
|
|
1285
|
+
});
|
|
1376
1286
|
}
|
|
1377
|
-
process.stderr.write(
|
|
1378
|
-
`[harness-opencode] migratePlans: conflict on ${name} \u2014 destination ${dst} exists with different content; leaving source ${src} in place. Resolve manually.
|
|
1379
|
-
`
|
|
1380
|
-
);
|
|
1381
1287
|
}
|
|
1382
|
-
|
|
1288
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
1289
|
+
for (const m of allModels) {
|
|
1290
|
+
if (!byProvider.has(m.provider)) byProvider.set(m.provider, []);
|
|
1291
|
+
byProvider.get(m.provider).push(m);
|
|
1292
|
+
}
|
|
1293
|
+
const providerNames = [...byProvider.keys()];
|
|
1294
|
+
providerNames.push("\u2190 Back");
|
|
1295
|
+
const providerIdx = await promptChoice(`Provider for ${tier}:`, providerNames, 0);
|
|
1296
|
+
if (providerIdx >= providerNames.length - 1) return;
|
|
1297
|
+
const providerModels = byProvider.get(providerNames[providerIdx]);
|
|
1298
|
+
const modelChoices = providerModels.map((m) => m.id);
|
|
1299
|
+
modelChoices.push("\u2190 Back");
|
|
1300
|
+
const currentModel = currentModels[tier]?.[0] ?? "";
|
|
1301
|
+
const currentIdx = modelChoices.indexOf(currentModel);
|
|
1302
|
+
const modelIdx = await promptChoice(
|
|
1303
|
+
`${tier} model:`,
|
|
1304
|
+
modelChoices,
|
|
1305
|
+
currentIdx >= 0 ? currentIdx : 0
|
|
1306
|
+
);
|
|
1307
|
+
if (modelIdx >= modelChoices.length - 1) return;
|
|
1308
|
+
const selectedModel = modelChoices[modelIdx];
|
|
1309
|
+
const newModels = { ...currentModels, [tier]: [selectedModel] };
|
|
1310
|
+
writePluginOption(configPath, "models", newModels, { dryRun: false });
|
|
1311
|
+
ok2(`${tier} \u2192 ${selectedModel}`);
|
|
1383
1312
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
// src/lib/opencode-server.ts
|
|
1395
|
-
import { execFile as execFile2 } from "child_process";
|
|
1396
|
-
import { promisify } from "util";
|
|
1397
|
-
import {
|
|
1398
|
-
createOpencodeServer,
|
|
1399
|
-
createOpencodeClient
|
|
1400
|
-
} from "@opencode-ai/sdk";
|
|
1401
|
-
var execFileP2 = promisify(execFile2);
|
|
1402
|
-
var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
|
|
1403
|
-
async function ensureOpencodeOnPath() {
|
|
1404
|
-
try {
|
|
1405
|
-
await execFileP2("opencode", ["--version"]);
|
|
1406
|
-
} catch {
|
|
1407
|
-
throw new Error(
|
|
1408
|
-
"opencode CLI not found on PATH.\n Install: https://opencode.ai\n Or: bunx opencode upgrade"
|
|
1409
|
-
);
|
|
1313
|
+
async function configureNotifications(configPath, currentNotifyUrl) {
|
|
1314
|
+
console.log(`
|
|
1315
|
+
${c2.bold}Notifications configuration:${c2.reset}
|
|
1316
|
+
`);
|
|
1317
|
+
if (currentNotifyUrl) {
|
|
1318
|
+
console.log(` Current webhook URL: ${c2.cyan}${currentNotifyUrl}${c2.reset}
|
|
1319
|
+
`);
|
|
1320
|
+
} else {
|
|
1321
|
+
console.log(` ${c2.dim}No webhook URL configured.${c2.reset}
|
|
1322
|
+
`);
|
|
1410
1323
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
};
|
|
1430
|
-
return { url: server.url, client, shutdown };
|
|
1431
|
-
}
|
|
1432
|
-
async function createSession(client, opts) {
|
|
1433
|
-
const session = await client.session.create({
|
|
1434
|
-
body: {
|
|
1435
|
-
directory: opts.cwd,
|
|
1436
|
-
...opts.agentName ? { agentID: opts.agentName } : {}
|
|
1437
|
-
}
|
|
1438
|
-
});
|
|
1439
|
-
return session.id;
|
|
1440
|
-
}
|
|
1441
|
-
async function sendAndWait(client, opts) {
|
|
1442
|
-
const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
|
|
1443
|
-
await client.session.chat({
|
|
1444
|
-
sessionID: opts.sessionId,
|
|
1445
|
-
body: { content: [{ type: "text", text: opts.message }] }
|
|
1446
|
-
});
|
|
1447
|
-
return waitForIdle(client, {
|
|
1448
|
-
sessionId: opts.sessionId,
|
|
1449
|
-
stallMs,
|
|
1450
|
-
abortSignal: opts.abortSignal
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
async function waitForIdle(client, opts) {
|
|
1454
|
-
const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
|
|
1455
|
-
return new Promise((resolve2) => {
|
|
1456
|
-
let stallTimer = null;
|
|
1457
|
-
let unsubscribe = null;
|
|
1458
|
-
let settled = false;
|
|
1459
|
-
const settle = (result) => {
|
|
1460
|
-
if (settled) return;
|
|
1461
|
-
settled = true;
|
|
1462
|
-
if (stallTimer) clearTimeout(stallTimer);
|
|
1463
|
-
if (unsubscribe) unsubscribe();
|
|
1464
|
-
resolve2(result);
|
|
1465
|
-
};
|
|
1466
|
-
const resetStall = () => {
|
|
1467
|
-
if (stallTimer) clearTimeout(stallTimer);
|
|
1468
|
-
stallTimer = setTimeout(() => settle({ kind: "stall", stallMs }), stallMs);
|
|
1469
|
-
};
|
|
1470
|
-
if (opts.abortSignal) {
|
|
1471
|
-
if (opts.abortSignal.aborted) {
|
|
1472
|
-
settle({ kind: "abort" });
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
opts.abortSignal.addEventListener("abort", () => settle({ kind: "abort" }), { once: true });
|
|
1476
|
-
}
|
|
1477
|
-
resetStall();
|
|
1478
|
-
const stream = client.event.subscribe();
|
|
1479
|
-
let streamDone = false;
|
|
1480
|
-
(async () => {
|
|
1481
|
-
try {
|
|
1482
|
-
for await (const event of stream) {
|
|
1483
|
-
if (settled) break;
|
|
1484
|
-
const props = event.properties ?? {};
|
|
1485
|
-
const eventSessionId = props["sessionID"];
|
|
1486
|
-
if (eventSessionId !== opts.sessionId) continue;
|
|
1487
|
-
resetStall();
|
|
1488
|
-
const type = event.type ?? "";
|
|
1489
|
-
if (type === "session.idle") {
|
|
1490
|
-
settle({ kind: "idle" });
|
|
1491
|
-
break;
|
|
1492
|
-
}
|
|
1493
|
-
if (type === "session.error") {
|
|
1494
|
-
const msg = props["message"] ?? "session error";
|
|
1495
|
-
settle({ kind: "error", message: msg });
|
|
1496
|
-
break;
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
} catch (err) {
|
|
1500
|
-
if (!settled) {
|
|
1501
|
-
settle({ kind: "error", message: err instanceof Error ? err.message : String(err) });
|
|
1502
|
-
}
|
|
1503
|
-
} finally {
|
|
1504
|
-
streamDone = true;
|
|
1505
|
-
}
|
|
1506
|
-
})();
|
|
1507
|
-
unsubscribe = () => {
|
|
1508
|
-
};
|
|
1324
|
+
const choices = [
|
|
1325
|
+
"Set Slack incoming webhook URL",
|
|
1326
|
+
"Set custom webhook URL",
|
|
1327
|
+
"Clear webhook URL",
|
|
1328
|
+
"\u2190 Back"
|
|
1329
|
+
];
|
|
1330
|
+
const choice = await promptChoice("Notifications:", choices, choices.length - 1);
|
|
1331
|
+
if (choice === choices.length - 1) return currentNotifyUrl;
|
|
1332
|
+
if (choice === 2) {
|
|
1333
|
+
writeNotifyUrl(configPath, void 0);
|
|
1334
|
+
ok2("Webhook URL cleared.");
|
|
1335
|
+
return void 0;
|
|
1336
|
+
}
|
|
1337
|
+
const { input } = await import("@inquirer/prompts");
|
|
1338
|
+
const prompt = choice === 0 ? " Slack incoming webhook URL (https://hooks.slack.com/...):" : " Webhook URL:";
|
|
1339
|
+
const url = await input({
|
|
1340
|
+
message: prompt,
|
|
1341
|
+
default: currentNotifyUrl ?? ""
|
|
1509
1342
|
});
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
const assistantMessages = messages.filter((m) => m.info.role === "assistant");
|
|
1515
|
-
if (assistantMessages.length === 0) return "";
|
|
1516
|
-
const last = assistantMessages[assistantMessages.length - 1];
|
|
1517
|
-
return last.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
1518
|
-
} catch {
|
|
1519
|
-
return "";
|
|
1343
|
+
if (url) {
|
|
1344
|
+
writeNotifyUrl(configPath, url);
|
|
1345
|
+
ok2(`Webhook URL set: ${url}`);
|
|
1346
|
+
return url;
|
|
1520
1347
|
}
|
|
1348
|
+
return currentNotifyUrl;
|
|
1521
1349
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
return withoutInline.includes(SENTINEL_TAG);
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
// src/autopilot/struggle.ts
|
|
1542
|
-
import * as fs7 from "fs";
|
|
1543
|
-
import * as path7 from "path";
|
|
1544
|
-
var StruggleDetector = class {
|
|
1545
|
-
_consecutiveStalls = 0;
|
|
1546
|
-
_threshold;
|
|
1547
|
-
constructor(threshold) {
|
|
1548
|
-
this._threshold = threshold;
|
|
1549
|
-
}
|
|
1550
|
-
/** Number of consecutive stall iterations recorded so far. */
|
|
1551
|
-
get consecutiveStalls() {
|
|
1552
|
-
return this._consecutiveStalls;
|
|
1553
|
-
}
|
|
1554
|
-
/**
|
|
1555
|
-
* Record the result of one iteration.
|
|
1556
|
-
* @param madeProgress - true if the agent made filesystem changes this iteration.
|
|
1557
|
-
*/
|
|
1558
|
-
record(madeProgress) {
|
|
1559
|
-
if (madeProgress) {
|
|
1560
|
-
this._consecutiveStalls = 0;
|
|
1350
|
+
function writeNotifyUrl(configPath, url) {
|
|
1351
|
+
try {
|
|
1352
|
+
if (!fs5.existsSync(configPath)) return;
|
|
1353
|
+
const raw = fs5.readFileSync(configPath, "utf8");
|
|
1354
|
+
const config = JSON.parse(raw);
|
|
1355
|
+
if (!Array.isArray(config.plugin)) return;
|
|
1356
|
+
const pluginIdx = config.plugin.findIndex((entry) => {
|
|
1357
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
1358
|
+
return name === PLUGIN_NAME4 || String(name ?? "").startsWith(`${PLUGIN_NAME4}@`) || String(name ?? "").includes("harness-opencode");
|
|
1359
|
+
});
|
|
1360
|
+
if (pluginIdx < 0) return;
|
|
1361
|
+
const current = config.plugin[pluginIdx];
|
|
1362
|
+
const pluginName2 = Array.isArray(current) ? current[0] : current;
|
|
1363
|
+
const existingOpts = Array.isArray(current) && current.length >= 2 ? { ...current[1] } : {};
|
|
1364
|
+
if (url === void 0) {
|
|
1365
|
+
delete existingOpts.notifyUrl;
|
|
1561
1366
|
} else {
|
|
1562
|
-
|
|
1367
|
+
existingOpts.notifyUrl = url;
|
|
1563
1368
|
}
|
|
1369
|
+
config.plugin[pluginIdx] = [pluginName2, existingOpts];
|
|
1370
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
1371
|
+
fs5.copyFileSync(configPath, bakPath);
|
|
1372
|
+
fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1375
|
+
process.stderr.write(`\x1B[33m\u26A0 Failed to write notifyUrl: ${msg}\x1B[0m
|
|
1376
|
+
`);
|
|
1564
1377
|
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Returns true if the agent has stalled for `threshold` consecutive
|
|
1567
|
-
* iterations without making progress.
|
|
1568
|
-
*/
|
|
1569
|
-
isStruggling() {
|
|
1570
|
-
return this._consecutiveStalls >= this._threshold;
|
|
1571
|
-
}
|
|
1572
|
-
};
|
|
1573
|
-
function checkKillSwitch(cwd) {
|
|
1574
|
-
const killSwitchFile = path7.join(cwd, KILL_SWITCH_PATH);
|
|
1575
|
-
return fs7.existsSync(killSwitchFile);
|
|
1576
1378
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
try {
|
|
1588
|
-
const raw = readFileSync6(candidate, "utf8");
|
|
1589
|
-
template = raw.replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
1590
|
-
break;
|
|
1591
|
-
} catch {
|
|
1379
|
+
var configureCmd = command({
|
|
1380
|
+
name: "configure",
|
|
1381
|
+
description: "Interactively edit opencode.json settings \u2014 models, MCPs, plugin add-ons.",
|
|
1382
|
+
args: {},
|
|
1383
|
+
handler: async () => {
|
|
1384
|
+
const configPath = getOpencodeConfigPath4();
|
|
1385
|
+
const config = readConfig(configPath);
|
|
1386
|
+
if (!config) {
|
|
1387
|
+
console.log(`No config found at ${configPath}. Run ${c2.green}glrs oc install${c2.reset} first.`);
|
|
1388
|
+
process.exit(1);
|
|
1592
1389
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const _sendAndWait = opts._deps?.sendAndWait ?? sendAndWait;
|
|
1621
|
-
const _getLastAssistantMessage = opts._deps?.getLastAssistantMessage ?? getLastAssistantMessage;
|
|
1622
|
-
const fullPrompt = buildFullPrompt(opts.prompt);
|
|
1623
|
-
const struggle = new StruggleDetector(struggleThreshold);
|
|
1624
|
-
const startTime = Date.now();
|
|
1625
|
-
const server = await _startServer({ cwd: opts.cwd });
|
|
1626
|
-
const abort = new AbortController();
|
|
1627
|
-
const timeoutHandle = setTimeout(() => {
|
|
1628
|
-
abort.abort();
|
|
1629
|
-
}, timeoutMs);
|
|
1630
|
-
try {
|
|
1631
|
-
const sessionId = await _createSession(server.client, {
|
|
1632
|
-
cwd: opts.cwd,
|
|
1633
|
-
agentName: "prime"
|
|
1634
|
-
});
|
|
1635
|
-
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
1636
|
-
if (checkKillSwitch(opts.cwd)) {
|
|
1637
|
-
return {
|
|
1638
|
-
exitReason: "kill-switch",
|
|
1639
|
-
iterations: iteration - 1,
|
|
1640
|
-
message: `Kill switch active (.agent/autopilot-disable exists). Stopping after ${iteration - 1} iteration(s).`
|
|
1641
|
-
};
|
|
1642
|
-
}
|
|
1643
|
-
if (Date.now() - startTime >= timeoutMs) {
|
|
1644
|
-
return {
|
|
1645
|
-
exitReason: "timeout",
|
|
1646
|
-
iterations: iteration - 1,
|
|
1647
|
-
message: `Total timeout (${timeoutMs}ms) exceeded after ${iteration - 1} iteration(s).`
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
const headBefore = await getHeadSha(opts.cwd);
|
|
1651
|
-
const result = await _sendAndWait(server.client, {
|
|
1652
|
-
sessionId,
|
|
1653
|
-
message: fullPrompt,
|
|
1654
|
-
stallMs,
|
|
1655
|
-
abortSignal: abort.signal
|
|
1656
|
-
});
|
|
1657
|
-
if (result.kind === "abort") {
|
|
1658
|
-
return {
|
|
1659
|
-
exitReason: "timeout",
|
|
1660
|
-
iterations: iteration,
|
|
1661
|
-
message: `Aborted after ${iteration} iteration(s) (total timeout exceeded).`
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
if (result.kind === "stall") {
|
|
1665
|
-
return {
|
|
1666
|
-
exitReason: "stall",
|
|
1667
|
-
iterations: iteration,
|
|
1668
|
-
message: `Iteration ${iteration} stalled for ${result.stallMs}ms with no idle signal.`
|
|
1669
|
-
};
|
|
1390
|
+
const opts = extractPluginOptions2(config);
|
|
1391
|
+
const models = opts?.models ?? {};
|
|
1392
|
+
let notifyUrl = opts?.notifyUrl;
|
|
1393
|
+
console.log(`
|
|
1394
|
+
${c2.bold}${c2.blue}glrs oc configure${c2.reset}
|
|
1395
|
+
`);
|
|
1396
|
+
while (true) {
|
|
1397
|
+
const deepModel = models.deep?.[0] ?? "(not set)";
|
|
1398
|
+
const midModel = models.mid?.[0] ?? "(not set)";
|
|
1399
|
+
const midExecModel = models["mid-execute"]?.[0] ?? "(not set)";
|
|
1400
|
+
const autopilotExecModel = models["autopilot-execute"]?.[0] ?? `(falls back to ${midExecModel})`;
|
|
1401
|
+
const fastModel = models.fast?.[0] ?? "(not set)";
|
|
1402
|
+
const mcpEnabled = Object.entries(config.mcp ?? {}).filter(([, v]) => v?.enabled).map(([k]) => k);
|
|
1403
|
+
const slackConfigured = notifyUrl?.includes("hooks.slack.com/") ?? false;
|
|
1404
|
+
const notifyLabel = notifyUrl ? slackConfigured ? "Slack" : "custom webhook" : "none";
|
|
1405
|
+
const sections = [
|
|
1406
|
+
`Models \u2014 deep: ${deepModel.split("/").pop()}, autopilot --fast: ${autopilotExecModel.split("/").pop()}`,
|
|
1407
|
+
`MCPs \u2014 ${mcpEnabled.length > 0 ? mcpEnabled.join(", ") : "none"}`,
|
|
1408
|
+
`Notifications \u2014 ${notifyLabel}`,
|
|
1409
|
+
"Done"
|
|
1410
|
+
];
|
|
1411
|
+
const choice = await promptChoice("What to configure?", sections, sections.length - 1);
|
|
1412
|
+
if (choice === sections.length - 1) {
|
|
1413
|
+
console.log(`
|
|
1414
|
+
${c2.bold}Done.${c2.reset} Restart opencode to pick up changes.
|
|
1415
|
+
`);
|
|
1416
|
+
break;
|
|
1670
1417
|
}
|
|
1671
|
-
if (
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1418
|
+
if (choice === 0) {
|
|
1419
|
+
await configureModels(configPath, models);
|
|
1420
|
+
const updated = readConfig(configPath);
|
|
1421
|
+
if (updated) {
|
|
1422
|
+
const updatedOpts = extractPluginOptions2(updated);
|
|
1423
|
+
if (updatedOpts?.models) {
|
|
1424
|
+
Object.assign(models, updatedOpts.models);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1677
1427
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1428
|
+
if (choice === 1) {
|
|
1429
|
+
const { promptMulti: promptMulti2 } = await import("./plugin-check-GJRD2OK6.js");
|
|
1430
|
+
const MCP_TOGGLES2 = [
|
|
1431
|
+
{ name: "playwright", label: "Playwright \u2014 browser automation" },
|
|
1432
|
+
{ name: "linear", label: "Linear \u2014 issue tracker" }
|
|
1433
|
+
];
|
|
1434
|
+
const currentMcps = new Set(
|
|
1435
|
+
Object.entries(config.mcp ?? {}).filter(([, v]) => v?.enabled).map(([k]) => k)
|
|
1436
|
+
);
|
|
1437
|
+
const selected = await promptMulti2(
|
|
1438
|
+
"Enable MCPs:",
|
|
1439
|
+
MCP_TOGGLES2.map((t) => ({ label: t.label, defaultOn: currentMcps.has(t.name) }))
|
|
1440
|
+
);
|
|
1441
|
+
const newEnabled = new Set([...selected].map((i) => MCP_TOGGLES2[i].name));
|
|
1442
|
+
writeMcpToggles(configPath, newEnabled, { dryRun: false });
|
|
1685
1443
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
if (struggle.isStruggling()) {
|
|
1689
|
-
return {
|
|
1690
|
-
exitReason: "struggle",
|
|
1691
|
-
iterations: iteration,
|
|
1692
|
-
message: `Agent made no filesystem progress for ${struggleThreshold} consecutive iteration(s). Stopping at iteration ${iteration}.`
|
|
1693
|
-
};
|
|
1444
|
+
if (choice === 2) {
|
|
1445
|
+
notifyUrl = await configureNotifications(configPath, notifyUrl);
|
|
1694
1446
|
}
|
|
1695
1447
|
}
|
|
1696
|
-
return {
|
|
1697
|
-
exitReason: "max-iterations",
|
|
1698
|
-
iterations: maxIterations,
|
|
1699
|
-
message: `Reached maximum iterations (${maxIterations}). Stopping.`
|
|
1700
|
-
};
|
|
1701
|
-
} finally {
|
|
1702
|
-
clearTimeout(timeoutHandle);
|
|
1703
|
-
await server.shutdown();
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
// src/autopilot/cli.ts
|
|
1708
|
-
var autopilotCmd = command({
|
|
1709
|
-
name: "autopilot",
|
|
1710
|
-
description: "Run the Ralph loop: send a prompt to PRIME repeatedly until it emits <autopilot-done> or a budget is exhausted.",
|
|
1711
|
-
args: {
|
|
1712
|
-
prompt: positional({
|
|
1713
|
-
type: stringType,
|
|
1714
|
-
displayName: "prompt",
|
|
1715
|
-
description: "The prompt to send to PRIME each iteration (e.g. a Linear issue ref or free-form task)."
|
|
1716
|
-
}),
|
|
1717
|
-
maxIterations: option({
|
|
1718
|
-
long: "max-iterations",
|
|
1719
|
-
type: optional(numberType),
|
|
1720
|
-
description: `Maximum number of loop iterations (default: ${MAX_ITERATIONS}).`
|
|
1721
|
-
}),
|
|
1722
|
-
timeout: option({
|
|
1723
|
-
long: "timeout",
|
|
1724
|
-
type: optional(numberType),
|
|
1725
|
-
description: `Total wall-clock timeout in milliseconds (default: ${TIMEOUT_MS} = 4 hours).`
|
|
1726
|
-
})
|
|
1727
|
-
},
|
|
1728
|
-
handler: async ({ prompt, maxIterations, timeout }) => {
|
|
1729
|
-
const cwd = process.cwd();
|
|
1730
|
-
process.stdout.write("\n\x1B[1mAutopilot \u2014 Ralph loop\x1B[0m\n");
|
|
1731
|
-
process.stdout.write(`Prompt: ${prompt.slice(0, 80)}${prompt.length > 80 ? "..." : ""}
|
|
1732
|
-
`);
|
|
1733
|
-
process.stdout.write(`Max iterations: ${maxIterations ?? MAX_ITERATIONS}
|
|
1734
|
-
`);
|
|
1735
|
-
process.stdout.write(`Timeout: ${((timeout ?? TIMEOUT_MS) / 36e5).toFixed(1)}h
|
|
1736
|
-
|
|
1737
|
-
`);
|
|
1738
|
-
const result = await runRalphLoop({
|
|
1739
|
-
prompt,
|
|
1740
|
-
cwd,
|
|
1741
|
-
maxIterations: maxIterations ?? void 0,
|
|
1742
|
-
timeoutMs: timeout ?? void 0
|
|
1743
|
-
});
|
|
1744
|
-
const icon = result.exitReason === "sentinel" ? "\x1B[32m\u2713\x1B[0m" : result.exitReason === "kill-switch" ? "\x1B[33m\u2298\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
|
|
1745
|
-
process.stdout.write(`
|
|
1746
|
-
${icon} ${result.message}
|
|
1747
|
-
`);
|
|
1748
|
-
process.stdout.write(` Iterations: ${result.iterations}
|
|
1749
|
-
|
|
1750
|
-
`);
|
|
1751
|
-
if (result.exitReason !== "sentinel" && result.exitReason !== "kill-switch") {
|
|
1752
|
-
process.exit(1);
|
|
1753
|
-
}
|
|
1754
|
-
process.exit(0);
|
|
1755
1448
|
}
|
|
1756
1449
|
});
|
|
1757
1450
|
|
|
1758
1451
|
// src/cli/cli-update.ts
|
|
1759
|
-
import * as
|
|
1760
|
-
import * as
|
|
1761
|
-
import * as
|
|
1452
|
+
import * as fs6 from "fs";
|
|
1453
|
+
import * as path6 from "path";
|
|
1454
|
+
import * as os5 from "os";
|
|
1762
1455
|
import { spawn } from "child_process";
|
|
1763
|
-
import { fileURLToPath as
|
|
1456
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1764
1457
|
var PACKAGE_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
1765
1458
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1766
|
-
var
|
|
1459
|
+
var c3 = {
|
|
1767
1460
|
reset: "\x1B[0m",
|
|
1768
1461
|
green: "\x1B[32m",
|
|
1769
1462
|
yellow: "\x1B[33m",
|
|
@@ -1785,12 +1478,12 @@ function isMajorBump(current, latest) {
|
|
|
1785
1478
|
return latest.major > current.major;
|
|
1786
1479
|
}
|
|
1787
1480
|
function getStateFilePath() {
|
|
1788
|
-
const cacheHome = process.env["XDG_CACHE_HOME"] ??
|
|
1789
|
-
return
|
|
1481
|
+
const cacheHome = process.env["XDG_CACHE_HOME"] ?? path6.join(os5.homedir(), ".cache");
|
|
1482
|
+
return path6.join(cacheHome, "harness-opencode", "cli-update.json");
|
|
1790
1483
|
}
|
|
1791
1484
|
function readState() {
|
|
1792
1485
|
try {
|
|
1793
|
-
const raw =
|
|
1486
|
+
const raw = fs6.readFileSync(getStateFilePath(), "utf8");
|
|
1794
1487
|
return JSON.parse(raw);
|
|
1795
1488
|
} catch {
|
|
1796
1489
|
return null;
|
|
@@ -1799,21 +1492,21 @@ function readState() {
|
|
|
1799
1492
|
function writeState(state) {
|
|
1800
1493
|
try {
|
|
1801
1494
|
const statePath = getStateFilePath();
|
|
1802
|
-
|
|
1803
|
-
|
|
1495
|
+
fs6.mkdirSync(path6.dirname(statePath), { recursive: true });
|
|
1496
|
+
fs6.writeFileSync(statePath, JSON.stringify(state));
|
|
1804
1497
|
} catch {
|
|
1805
1498
|
}
|
|
1806
1499
|
}
|
|
1807
1500
|
function readInstalledVersion() {
|
|
1808
|
-
const here =
|
|
1501
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
1809
1502
|
const candidates = [
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1503
|
+
path6.join(here, "..", "package.json"),
|
|
1504
|
+
path6.join(here, "..", "..", "package.json"),
|
|
1505
|
+
path6.join(here, "package.json")
|
|
1813
1506
|
];
|
|
1814
1507
|
for (const candidate of candidates) {
|
|
1815
1508
|
try {
|
|
1816
|
-
const raw =
|
|
1509
|
+
const raw = fs6.readFileSync(candidate, "utf8");
|
|
1817
1510
|
const parsed = JSON.parse(raw);
|
|
1818
1511
|
if (parsed.name === PACKAGE_NAME && typeof parsed.version === "string") {
|
|
1819
1512
|
return parsed.version;
|
|
@@ -1884,7 +1577,7 @@ function startUpdateCheck() {
|
|
|
1884
1577
|
action = () => {
|
|
1885
1578
|
process.stderr.write(
|
|
1886
1579
|
`
|
|
1887
|
-
${
|
|
1580
|
+
${c3.blue}\u2022${c3.reset} Updating ${PACKAGE_NAME} ${c3.dim}${currentVersionStr}${c3.reset} \u2192 ${c3.green}${latestStr}${c3.reset} in the background...
|
|
1888
1581
|
`
|
|
1889
1582
|
);
|
|
1890
1583
|
spawnBackgroundUpdate();
|
|
@@ -1899,8 +1592,8 @@ ${c2.blue}\u2022${c2.reset} Updating ${PACKAGE_NAME} ${c2.dim}${currentVersionSt
|
|
|
1899
1592
|
function printMajorNotice(current, latest) {
|
|
1900
1593
|
process.stderr.write(
|
|
1901
1594
|
`
|
|
1902
|
-
${
|
|
1903
|
-
${
|
|
1595
|
+
${c3.yellow}${c3.bold}Major update available:${c3.reset} ${current} \u2192 ${c3.green}${latest}${c3.reset}
|
|
1596
|
+
${c3.dim}Review the changelog before upgrading:${c3.reset}
|
|
1904
1597
|
bun update -g ${PACKAGE_NAME}
|
|
1905
1598
|
`
|
|
1906
1599
|
);
|
|
@@ -1977,57 +1670,6 @@ var doctorCmd = command2({
|
|
|
1977
1670
|
doctor();
|
|
1978
1671
|
}
|
|
1979
1672
|
});
|
|
1980
|
-
var planCheckCmd = command2({
|
|
1981
|
-
name: "plan-check",
|
|
1982
|
-
description: "Parse a plan file's plan-state fence (legacy markdown plans).",
|
|
1983
|
-
args: {
|
|
1984
|
-
run: option2({
|
|
1985
|
-
long: "run",
|
|
1986
|
-
type: optional2(string),
|
|
1987
|
-
description: "Print verify commands for pending items, one per line."
|
|
1988
|
-
}),
|
|
1989
|
-
check: option2({
|
|
1990
|
-
long: "check",
|
|
1991
|
-
type: optional2(string),
|
|
1992
|
-
description: "Structural validation; exits 1 if any item is invalid."
|
|
1993
|
-
}),
|
|
1994
|
-
rest: restPositionals({
|
|
1995
|
-
type: string,
|
|
1996
|
-
displayName: "plan-path",
|
|
1997
|
-
description: "Path to a plan markdown file. Required unless --run / --check is given."
|
|
1998
|
-
})
|
|
1999
|
-
},
|
|
2000
|
-
handler: ({ run: run2, check, rest }) => {
|
|
2001
|
-
const legacy = [];
|
|
2002
|
-
if (run2 !== void 0) {
|
|
2003
|
-
legacy.push("--run", run2);
|
|
2004
|
-
} else if (check !== void 0) {
|
|
2005
|
-
legacy.push("--check", check);
|
|
2006
|
-
} else {
|
|
2007
|
-
legacy.push(...rest);
|
|
2008
|
-
}
|
|
2009
|
-
planCheck(legacy);
|
|
2010
|
-
}
|
|
2011
|
-
});
|
|
2012
|
-
var planDirCmd = command2({
|
|
2013
|
-
name: "plan-dir",
|
|
2014
|
-
description: "Print the repo-shared plan directory for the current worktree (resolves + creates + migrates legacy).",
|
|
2015
|
-
args: {},
|
|
2016
|
-
handler: async () => {
|
|
2017
|
-
try {
|
|
2018
|
-
const cwd = process.cwd();
|
|
2019
|
-
const planDir = await getPlanDir(cwd);
|
|
2020
|
-
await migratePlans(cwd, planDir);
|
|
2021
|
-
process.stdout.write(planDir + "\n");
|
|
2022
|
-
process.exit(0);
|
|
2023
|
-
} catch (err) {
|
|
2024
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2025
|
-
process.stderr.write(`plan-dir: ${msg}
|
|
2026
|
-
`);
|
|
2027
|
-
process.exit(1);
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
});
|
|
2031
1673
|
var installPluginCmd = command2({
|
|
2032
1674
|
name: "install-plugin",
|
|
2033
1675
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
@@ -2053,10 +1695,9 @@ var cli = subcommands({
|
|
|
2053
1695
|
"install-plugin": installPluginCmd,
|
|
2054
1696
|
install: installCmd,
|
|
2055
1697
|
uninstall: uninstallCmd,
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
autopilot: autopilotCmd
|
|
1698
|
+
configure: configureCmd,
|
|
1699
|
+
doctor: doctorCmd
|
|
1700
|
+
// Note: `loop` and `autopilot` commands have moved to @glrs-dev/cli.
|
|
2060
1701
|
}
|
|
2061
1702
|
});
|
|
2062
1703
|
var printUpdate = startUpdateCheck();
|