opencode-hub 1.0.7 → 1.0.9
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/oc-tui.js +132 -32
- package/package.json +1 -1
- package/plugin.js +14 -2
package/oc-tui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from "fs";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import { join, dirname } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
@@ -20,6 +20,38 @@ var PLUGINS_DIR = join(CONFIG_DIR, "plugin");
|
|
|
20
20
|
// Folder name helper: <creator>/<repo-name> to avoid collisions
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
|
|
23
|
+
function loadNpmPlugins() {
|
|
24
|
+
var ocPath = join(CONFIG_DIR, "opencode.json");
|
|
25
|
+
if (!existsSync(ocPath)) return [];
|
|
26
|
+
try {
|
|
27
|
+
var raw = readFileSync(ocPath, "utf-8");
|
|
28
|
+
var stripped = raw.replace(/\/\/[^\n]*/g, "");
|
|
29
|
+
var oc = JSON.parse(stripped);
|
|
30
|
+
var plugins = oc.plugin || [];
|
|
31
|
+
return plugins
|
|
32
|
+
.filter(function(p) { return typeof p === "string"; })
|
|
33
|
+
.map(function(p) {
|
|
34
|
+
var name = p.replace(/@[^@\/]+$/, "") || p;
|
|
35
|
+
var version = "";
|
|
36
|
+
try {
|
|
37
|
+
// First try config-local node_modules, then global npm node_modules
|
|
38
|
+
var pkgPath = join(CONFIG_DIR, "node_modules", name, "package.json");
|
|
39
|
+
if (!existsSync(pkgPath)) {
|
|
40
|
+
// Global npm fallback (Windows: AppData/Roaming/npm/node_modules, Unix: prefix/lib/node_modules)
|
|
41
|
+
var globalNpm = process.platform === "win32"
|
|
42
|
+
? join(homedir(), "AppData", "Roaming", "npm", "node_modules")
|
|
43
|
+
: join("/usr", "lib", "node_modules");
|
|
44
|
+
pkgPath = join(globalNpm, name, "package.json");
|
|
45
|
+
}
|
|
46
|
+
if (existsSync(pkgPath)) {
|
|
47
|
+
version = JSON.parse(readFileSync(pkgPath, "utf-8")).version || "";
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
return { name: name, version: version, raw: p };
|
|
51
|
+
});
|
|
52
|
+
} catch { return []; }
|
|
53
|
+
}
|
|
54
|
+
|
|
23
55
|
function getFolderName(plugin) {
|
|
24
56
|
var match = (plugin.url || "").match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
25
57
|
if (match) return match[1] + "/" + plugin.name;
|
|
@@ -74,20 +106,6 @@ function checkForUpdates() {
|
|
|
74
106
|
execSync("npm install -g opencode-ai@latest", { stdio: "inherit", timeout: 120000 });
|
|
75
107
|
process.stderr.write("\x1b[32m > Updated to " + latest + "\x1b[0m\n\n");
|
|
76
108
|
} catch (e) {}
|
|
77
|
-
|
|
78
|
-
// Auto-update NPM plugins
|
|
79
|
-
try {
|
|
80
|
-
var ocPath = join(CONFIG_DIR, "opencode.json");
|
|
81
|
-
if (existsSync(ocPath)) {
|
|
82
|
-
var oc = JSON.parse(readFileSync(ocPath, "utf-8"));
|
|
83
|
-
var pluginsArr = oc.plugin || oc.plugins || [];
|
|
84
|
-
var npmPlugs = pluginsArr.filter(function(p) { return typeof p === "string" && p.includes("@latest") && !p.startsWith("."); });
|
|
85
|
-
if (npmPlugs.length > 0) {
|
|
86
|
-
process.stderr.write("\x1b[33m > Auto-updating NPM plugins: " + npmPlugs.join(", ") + "\x1b[0m\n");
|
|
87
|
-
execSync("npm install --no-save " + npmPlugs.join(" "), { cwd: CONFIG_DIR, stdio: "ignore", timeout: 60000 });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} catch (e) {}
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
checkForUpdates();
|
|
@@ -224,10 +242,13 @@ function buildPluginList() {
|
|
|
224
242
|
var remoteHead = "";
|
|
225
243
|
var subject = "";
|
|
226
244
|
var updateAvail = false;
|
|
245
|
+
var latestTag = "";
|
|
246
|
+
var enabled = p.enabled !== false;
|
|
227
247
|
|
|
228
248
|
if (installed) {
|
|
229
249
|
localHead = gitText(["git", "rev-parse", "HEAD"], dir);
|
|
230
250
|
subject = gitText(["git", "log", "-1", "--format=%s"], dir);
|
|
251
|
+
latestTag = gitText(["git", "describe", "--tags", "--abbrev=0"], dir);
|
|
231
252
|
}
|
|
232
253
|
|
|
233
254
|
list.push({
|
|
@@ -235,10 +256,12 @@ function buildPluginList() {
|
|
|
235
256
|
folderName: folderName,
|
|
236
257
|
url: p.url,
|
|
237
258
|
autoUpdate: p.autoUpdate !== false,
|
|
259
|
+
enabled: enabled,
|
|
238
260
|
installed: installed,
|
|
239
261
|
deployed: deployed,
|
|
240
262
|
localHead: localHead,
|
|
241
263
|
remoteHead: remoteHead,
|
|
264
|
+
latestTag: latestTag,
|
|
242
265
|
subject: subject,
|
|
243
266
|
updateAvail: updateAvail,
|
|
244
267
|
hasBuild: !!(p.build || p.bundle),
|
|
@@ -293,6 +316,10 @@ function runPluginUpdate(pluginItem) {
|
|
|
293
316
|
try { execSync(repo.install.join(" "), { cwd: dir, timeout: 120000, stdio: "ignore" }); }
|
|
294
317
|
catch (e) { return "Install failed"; }
|
|
295
318
|
}
|
|
319
|
+
if (repo.postInstall) {
|
|
320
|
+
try { execSync(repo.postInstall.join(" "), { cwd: dir, timeout: 120000, stdio: "ignore" }); }
|
|
321
|
+
catch (e) { return "Post-install failed"; }
|
|
322
|
+
}
|
|
296
323
|
if (repo.build) {
|
|
297
324
|
try { execSync(repo.build.join(" "), { cwd: dir, timeout: 120000, stdio: "ignore" }); }
|
|
298
325
|
catch (e) { return "Build failed"; }
|
|
@@ -348,6 +375,7 @@ function showCur() { process.stderr.write(E + "?25h"); }
|
|
|
348
375
|
|
|
349
376
|
var items = buildList();
|
|
350
377
|
var pluginItems = buildPluginList();
|
|
378
|
+
var npmPluginItems = loadNpmPlugins();
|
|
351
379
|
var cursor = 0;
|
|
352
380
|
var pcursor = 0; // plugin page cursor
|
|
353
381
|
var mode = "list"; // "list" | "actions" | "input" | "pactions"
|
|
@@ -394,16 +422,23 @@ function getActions(item) {
|
|
|
394
422
|
|
|
395
423
|
function getPluginActions(pitem) {
|
|
396
424
|
var a = [];
|
|
425
|
+
if (!pitem.enabled) {
|
|
426
|
+
a.push({ key: "enable-plugin", label: "Enable plugin" });
|
|
427
|
+
a.push({ key: "cancel", label: "Cancel" });
|
|
428
|
+
return a;
|
|
429
|
+
}
|
|
397
430
|
if (pitem.updateAvail || !pitem.deployed) {
|
|
398
431
|
a.push({ key: "update", label: "Update now" });
|
|
399
432
|
}
|
|
400
433
|
if (pitem.autoUpdate) {
|
|
401
|
-
a.push({ key: "disable-auto", label: "
|
|
434
|
+
a.push({ key: "disable-auto", label: "Set to manual update" });
|
|
402
435
|
} else {
|
|
403
436
|
a.push({ key: "enable-auto", label: "Enable auto-update" });
|
|
404
437
|
}
|
|
405
|
-
a.push({ key: "
|
|
438
|
+
a.push({ key: "update", label: "Force rebuild & deploy" });
|
|
439
|
+
a.push({ key: "update-all", label: "Update all plugins" });
|
|
406
440
|
a.push({ key: "commits", label: "Select specific commit (Downgrade)" });
|
|
441
|
+
a.push({ key: "disable-plugin", label: "Disable plugin" });
|
|
407
442
|
a.push({ key: "cancel", label: "Cancel" });
|
|
408
443
|
return a;
|
|
409
444
|
}
|
|
@@ -627,23 +662,29 @@ function buildPluginItem(pushBody, i, pitem, nameW, cols, isSelected) {
|
|
|
627
662
|
var nameStyle = sel ? (BOLD + WHITE) : DIM;
|
|
628
663
|
|
|
629
664
|
var statusParts = [];
|
|
630
|
-
if (pitem.
|
|
665
|
+
if (!pitem.enabled) {
|
|
666
|
+
statusParts.push(RED + "disabled" + RST);
|
|
667
|
+
} else if (pitem.autoUpdate) {
|
|
631
668
|
statusParts.push(GREEN + "auto" + RST);
|
|
632
669
|
} else {
|
|
633
670
|
statusParts.push(YELLOW + "manual" + RST);
|
|
634
671
|
}
|
|
635
|
-
if (pitem.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
672
|
+
if (pitem.enabled) {
|
|
673
|
+
if (pitem.updateAvail) {
|
|
674
|
+
statusParts.push(CYAN + "UPDATE" + RST);
|
|
675
|
+
} else if (pitem.deployed) {
|
|
676
|
+
statusParts.push(GRAY + "ok" + RST);
|
|
677
|
+
} else {
|
|
678
|
+
statusParts.push(RED + "missing" + RST);
|
|
679
|
+
}
|
|
641
680
|
}
|
|
642
681
|
|
|
643
682
|
var statusStr = statusParts.join(GRAY + " | " + RST);
|
|
644
|
-
var
|
|
683
|
+
var versionStr = pitem.latestTag
|
|
684
|
+
? (GRAY + pitem.latestTag + RST)
|
|
685
|
+
: (pitem.localHead ? (GRAY + pitem.localHead.substring(0, 7) + RST) : (GRAY + "---" + RST));
|
|
645
686
|
|
|
646
|
-
pushBody(" " + bg + arrow + nameStyle + pad(trunc(pitem.name, nameW), nameW) + RST + bg + " " + statusStr + " " +
|
|
687
|
+
pushBody(" " + bg + arrow + nameStyle + pad(trunc(pitem.name, nameW), nameW) + RST + bg + " " + statusStr + " " + versionStr + RST, isSelected);
|
|
647
688
|
|
|
648
689
|
if (sel) {
|
|
649
690
|
var subInfo = GRAY + " " + trunc(pitem.subject || pitem.url, cols - 10) + RST;
|
|
@@ -701,15 +742,18 @@ function buildPlugins(pushBody, pushFoot, cols, barW) {
|
|
|
701
742
|
return;
|
|
702
743
|
}
|
|
703
744
|
|
|
704
|
-
var autoCount = 0, manualCount = 0, updateCount = 0;
|
|
745
|
+
var autoCount = 0, manualCount = 0, updateCount = 0, disabledCount = 0;
|
|
705
746
|
for (var p of pluginItems) {
|
|
706
|
-
if (p.
|
|
747
|
+
if (!p.enabled) disabledCount++;
|
|
748
|
+
else if (p.autoUpdate) autoCount++; else manualCount++;
|
|
707
749
|
if (p.updateAvail) updateCount++;
|
|
708
750
|
}
|
|
709
751
|
|
|
710
752
|
pushBody(" " + MAGENTA + "#" + GRAY + " Plugins " +
|
|
711
753
|
DIM + "(" + autoCount + " auto, " + manualCount + " manual" +
|
|
754
|
+
(disabledCount > 0 ? ", " + RED + disabledCount + " disabled" + DIM : "") +
|
|
712
755
|
(updateCount > 0 ? ", " + CYAN + updateCount + " updates" + DIM : "") +
|
|
756
|
+
(npmPluginItems.length > 0 ? ", " + GRAY + npmPluginItems.length + " npm" + DIM : "") +
|
|
713
757
|
")" + RST, false);
|
|
714
758
|
|
|
715
759
|
if (!pluginFetched) {
|
|
@@ -720,6 +764,16 @@ function buildPlugins(pushBody, pushFoot, cols, barW) {
|
|
|
720
764
|
buildPluginItem(pushBody, i, pluginItems[i], nameW, cols, i === pcursor);
|
|
721
765
|
}
|
|
722
766
|
|
|
767
|
+
if (npmPluginItems.length > 0) {
|
|
768
|
+
pushBody("", false);
|
|
769
|
+
pushBody(" " + MAGENTA + "#" + GRAY + " npm plugins" + RST, false);
|
|
770
|
+
for (var ni = 0; ni < npmPluginItems.length; ni++) {
|
|
771
|
+
var np = npmPluginItems[ni];
|
|
772
|
+
var nvstr = np.version ? (GRAY + "v" + np.version + RST) : (GRAY + "not installed" + RST);
|
|
773
|
+
pushBody(" " + DIM + np.name + RST + " " + nvstr, false);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
723
777
|
pushBody("", false);
|
|
724
778
|
|
|
725
779
|
if (message) {
|
|
@@ -734,9 +788,9 @@ function buildPlugins(pushBody, pushFoot, cols, barW) {
|
|
|
734
788
|
} else {
|
|
735
789
|
pushFoot(" " + DIM + "^v" + RST + "/" + DIM + "WS" + RST + " Move " +
|
|
736
790
|
DIM + "Enter" + RST + " Select " +
|
|
737
|
-
DIM + "F" + RST + " Fetch " +
|
|
738
|
-
DIM + "A" + RST + " Toggle auto " +
|
|
739
791
|
DIM + "U" + RST + " Update " +
|
|
792
|
+
DIM + "D" + RST + " Disable " +
|
|
793
|
+
DIM + "A" + RST + " Toggle auto " +
|
|
740
794
|
DIM + "Q" + RST + " Quit" + RST);
|
|
741
795
|
}
|
|
742
796
|
}
|
|
@@ -768,7 +822,7 @@ function render() {
|
|
|
768
822
|
pushHead("");
|
|
769
823
|
pushHead(" " + BOLD + CYAN + " OpenCode" + RST + GRAY + " Launcher" + RST);
|
|
770
824
|
pushHead(" " + GRAY + "-".repeat(barW) + RST);
|
|
771
|
-
var showPluginsTab = pluginItems.length > 0;
|
|
825
|
+
var showPluginsTab = pluginItems.length > 0 || npmPluginItems.length > 0;
|
|
772
826
|
var projTab = page === "projects" ? (BOLD + WHITE + BG_SEL + " Projects " + RST) : (GRAY + " Projects " + RST);
|
|
773
827
|
var plugTab = showPluginsTab ? (page === "plugins" ? (BOLD + WHITE + BG_SEL + " Plugins " + RST) : (GRAY + " Plugins " + RST)) : "";
|
|
774
828
|
pushHead(" " + projTab + (showPluginsTab ? " " + plugTab + " " + DIM + "<- ->" + RST : ""));
|
|
@@ -925,6 +979,19 @@ function handlePluginKey(key) {
|
|
|
925
979
|
flash(err ? p.name + ": " + err : p.name + " updated. Restart OpenCode to apply.");
|
|
926
980
|
}
|
|
927
981
|
}
|
|
982
|
+
else if (key === "d") {
|
|
983
|
+
if (pluginItems.length > 0) {
|
|
984
|
+
var p = pluginItems[pcursor];
|
|
985
|
+
var plugins = loadPlugins();
|
|
986
|
+
var match = plugins.find(function(r) { return r.name === p.name; });
|
|
987
|
+
if (match) { match.enabled = false; savePlugins(plugins); }
|
|
988
|
+
var deployedPath = join(PLUGINS_DIR, p.pluginFile);
|
|
989
|
+
if (existsSync(deployedPath)) { try { unlinkSync(deployedPath); } catch {} }
|
|
990
|
+
pluginItems = buildPluginList();
|
|
991
|
+
if (pcursor >= pluginItems.length) pcursor = Math.max(0, pluginItems.length - 1);
|
|
992
|
+
flash(p.name + " disabled. Restart OpenCode to unload.");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
928
995
|
else if (key === "q" || key === "escape") { cleanup(); process.exit(1); }
|
|
929
996
|
} else if (mode === "pactions") {
|
|
930
997
|
var pitem = pluginItems[pcursor];
|
|
@@ -933,7 +1000,7 @@ function handlePluginKey(key) {
|
|
|
933
1000
|
else if (key === "down" || key === "s") { pacursor = Math.min(acts.length - 1, pacursor + 1); }
|
|
934
1001
|
else if (key === "enter" || key === "space") {
|
|
935
1002
|
var action = acts[pacursor].key;
|
|
936
|
-
if (action === "update"
|
|
1003
|
+
if (action === "update") {
|
|
937
1004
|
flash("Updating " + pitem.name + "...");
|
|
938
1005
|
render();
|
|
939
1006
|
var err = runPluginUpdate(pitem);
|
|
@@ -942,6 +1009,19 @@ function handlePluginKey(key) {
|
|
|
942
1009
|
flash(err ? pitem.name + ": " + err : pitem.name + " updated. Restart OpenCode to apply.");
|
|
943
1010
|
mode = "list";
|
|
944
1011
|
}
|
|
1012
|
+
else if (action === "update-all") {
|
|
1013
|
+
var errors = [];
|
|
1014
|
+
for (var pi of pluginItems) {
|
|
1015
|
+
flash("Updating " + pi.name + "...");
|
|
1016
|
+
render();
|
|
1017
|
+
var e = runPluginUpdate(pi);
|
|
1018
|
+
if (e) errors.push(pi.name + ": " + e);
|
|
1019
|
+
}
|
|
1020
|
+
pluginItems = buildPluginList();
|
|
1021
|
+
if (pcursor >= pluginItems.length) pcursor = Math.max(0, pluginItems.length - 1);
|
|
1022
|
+
flash(errors.length > 0 ? errors.join("; ") : "All plugins updated. Restart OpenCode to apply.");
|
|
1023
|
+
mode = "list";
|
|
1024
|
+
}
|
|
945
1025
|
else if (action === "enable-auto" || action === "disable-auto") {
|
|
946
1026
|
var newVal = action === "enable-auto";
|
|
947
1027
|
pitem.autoUpdate = newVal;
|
|
@@ -951,6 +1031,26 @@ function handlePluginKey(key) {
|
|
|
951
1031
|
flash(pitem.name + ": auto-update " + (newVal ? "ON" : "OFF"));
|
|
952
1032
|
mode = "list";
|
|
953
1033
|
}
|
|
1034
|
+
else if (action === "disable-plugin") {
|
|
1035
|
+
var plugins = loadPlugins();
|
|
1036
|
+
var match = plugins.find(function(r) { return r.name === pitem.name; });
|
|
1037
|
+
if (match) { match.enabled = false; savePlugins(plugins); }
|
|
1038
|
+
var deployedPath = join(PLUGINS_DIR, pitem.pluginFile);
|
|
1039
|
+
if (existsSync(deployedPath)) { try { unlinkSync(deployedPath); } catch {} }
|
|
1040
|
+
pluginItems = buildPluginList();
|
|
1041
|
+
if (pcursor >= pluginItems.length) pcursor = Math.max(0, pluginItems.length - 1);
|
|
1042
|
+
flash(pitem.name + " disabled. Restart OpenCode to unload.");
|
|
1043
|
+
mode = "list";
|
|
1044
|
+
}
|
|
1045
|
+
else if (action === "enable-plugin") {
|
|
1046
|
+
var plugins = loadPlugins();
|
|
1047
|
+
var match = plugins.find(function(r) { return r.name === pitem.name; });
|
|
1048
|
+
if (match) { delete match.enabled; savePlugins(plugins); }
|
|
1049
|
+
pluginItems = buildPluginList();
|
|
1050
|
+
if (pcursor >= pluginItems.length) pcursor = Math.max(0, pluginItems.length - 1);
|
|
1051
|
+
flash(pitem.name + " enabled. Use Update to deploy.");
|
|
1052
|
+
mode = "list";
|
|
1053
|
+
}
|
|
954
1054
|
else if (action === "commits") {
|
|
955
1055
|
var dir = join(REPOS_DIR, pitem.folderName);
|
|
956
1056
|
if (!existsSync(dir)) { flash("Not installed locally yet"); mode = "list"; return; }
|
package/package.json
CHANGED
package/plugin.js
CHANGED
|
@@ -12,10 +12,10 @@ function findTuiScript() {
|
|
|
12
12
|
var sameDirPath = join(import.meta.dir, "oc-tui.js");
|
|
13
13
|
if (existsSync(sameDirPath)) return sameDirPath;
|
|
14
14
|
|
|
15
|
-
// 2. Find config dir, then check repos/intisy/opencode-
|
|
15
|
+
// 2. Find config dir, then check repos/intisy/opencode-hub/ (updater case)
|
|
16
16
|
var configDir = findConfigDir(import.meta.dir);
|
|
17
17
|
if (configDir) {
|
|
18
|
-
var repoPath = join(configDir, "repos", "intisy", "opencode-
|
|
18
|
+
var repoPath = join(configDir, "repos", "intisy", "opencode-hub", "oc-tui.js");
|
|
19
19
|
if (existsSync(repoPath)) return repoPath;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -52,6 +52,18 @@ function installOcCommand() {
|
|
|
52
52
|
var binDir = getBinDir();
|
|
53
53
|
if (!existsSync(binDir)) try { mkdirSync(binDir, { recursive: true }); } catch {}
|
|
54
54
|
|
|
55
|
+
// Always keep binDir/oc-tui.js in sync with the source (so `oc` always runs latest)
|
|
56
|
+
var binTuiPath = join(binDir, "oc-tui.js");
|
|
57
|
+
try {
|
|
58
|
+
var srcContent = readFileSync(tuiPath, "utf-8");
|
|
59
|
+
var dstContent = existsSync(binTuiPath) ? readFileSync(binTuiPath, "utf-8") : null;
|
|
60
|
+
if (srcContent !== dstContent) {
|
|
61
|
+
writeFileSync(binTuiPath, srcContent, "utf-8");
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
// Point shell launchers at the stable binDir copy
|
|
65
|
+
tuiPath = binTuiPath;
|
|
66
|
+
|
|
55
67
|
var tuiPathEscaped = tuiPath.replace(/\\/g, "\\\\");
|
|
56
68
|
|
|
57
69
|
if (process.platform === "win32") {
|