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.
Files changed (3) hide show
  1. package/oc-tui.js +132 -32
  2. package/package.json +1 -1
  3. 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: "Disable auto-update" });
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: "force-update", label: "Force rebuild & deploy" });
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.autoUpdate) {
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.updateAvail) {
636
- statusParts.push(CYAN + "UPDATE" + RST);
637
- } else if (pitem.deployed) {
638
- statusParts.push(GRAY + "ok" + RST);
639
- } else {
640
- statusParts.push(RED + "missing" + RST);
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 commitStr = pitem.localHead ? (GRAY + pitem.localHead.substring(0, 7) + RST) : (GRAY + "---" + RST);
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 + " " + commitStr + RST, isSelected);
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.autoUpdate) autoCount++; else manualCount++;
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" || action === "force-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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-hub",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "TUI launcher for OpenCode - project switcher and plugin manager with oc command",
5
5
  "main": "plugin.js",
6
6
  "type": "module",
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-launcher/ (updater case)
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-launcher", "oc-tui.js");
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") {