clawcontrol 0.2.1 → 0.2.2

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.
@@ -8,32 +8,96 @@ import { execFileSync } from "node:child_process";
8
8
  import { createRequire } from "node:module";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { dirname, resolve } from "node:path";
11
+ import { readFileSync } from "node:fs";
11
12
 
12
13
  const args = process.argv.slice(2);
13
14
 
14
- // Handle --version and --help without needing bun
15
+ // ── Helpers ──────────────────────────────────────────────────────────
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const pkg = require("../package.json");
19
+ const currentVersion = pkg.version;
20
+
21
+ /** Compare two semver strings. Returns -1, 0, or 1. */
22
+ function compareSemver(a, b) {
23
+ const pa = a.split(".").map(Number);
24
+ const pb = b.split(".").map(Number);
25
+ for (let i = 0; i < 3; i++) {
26
+ if (pa[i] < pb[i]) return -1;
27
+ if (pa[i] > pb[i]) return 1;
28
+ }
29
+ return 0;
30
+ }
31
+
32
+ /** Fetch the latest version from the npm registry. Returns version string or null. */
33
+ async function fetchLatestVersion(timeoutMs = 3000) {
34
+ try {
35
+ const controller = new AbortController();
36
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
37
+ const res = await fetch("https://registry.npmjs.org/clawcontrol/latest", {
38
+ signal: controller.signal,
39
+ });
40
+ clearTimeout(timer);
41
+ if (!res.ok) return null;
42
+ const data = await res.json();
43
+ return data.version ?? null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /** Run bun add -g clawcontrol@latest. Returns true on success. */
50
+ function performUpdate() {
51
+ try {
52
+ execFileSync("bun", ["add", "-g", "clawcontrol@latest"], {
53
+ stdio: "pipe",
54
+ });
55
+ // Verify the new version by re-reading package.json from disk
56
+ const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../package.json");
57
+ const newPkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
58
+ console.log(`\x1b[32m[update] Updated to v${newPkg.version}\x1b[0m`);
59
+ return true;
60
+ } catch (e) {
61
+ if (e.code === "ENOENT") {
62
+ console.error(
63
+ "\x1b[31m[update] Update failed: bun is not installed.\x1b[0m\n" +
64
+ " Install it: curl -fsSL https://bun.sh/install | bash"
65
+ );
66
+ } else {
67
+ const detail = e.stderr ? e.stderr.toString().trim() : e.message;
68
+ console.error(`\x1b[31m[update] Update failed: ${detail}\x1b[0m`);
69
+ }
70
+ return false;
71
+ }
72
+ }
73
+
74
+ // ── --version ────────────────────────────────────────────────────────
75
+
15
76
  if (args.includes("--version") || args.includes("-v")) {
16
- const require = createRequire(import.meta.url);
17
- const pkg = require("../package.json");
18
- console.log(`clawcontrol v${pkg.version}`);
77
+ console.log(`clawcontrol v${currentVersion}`);
19
78
  process.exit(0);
20
79
  }
21
80
 
81
+ // ── --help ───────────────────────────────────────────────────────────
82
+
22
83
  if (args.includes("--help") || args.includes("-h")) {
23
- const require = createRequire(import.meta.url);
24
- const pkg = require("../package.json");
25
84
  console.log(`
26
- clawcontrol v${pkg.version}
85
+ clawcontrol v${currentVersion}
27
86
  ${pkg.description}
28
87
 
29
88
  Usage:
30
89
  clawcontrol Launch the interactive TUI
31
90
  clawcontrol --help Show this help message
32
91
  clawcontrol --version Show the version number
92
+ clawcontrol --update Update to the latest version
33
93
 
34
94
  Options:
35
95
  -h, --help Show this help message
36
96
  -v, --version Show the version number
97
+ -u, --update Update clawcontrol to the latest version
98
+
99
+ Environment:
100
+ CLAWCONTROL_SKIP_UPDATE=1 Skip the automatic update check on startup
37
101
 
38
102
  Documentation & source:
39
103
  https://github.com/ipenywis/clawcontrol
@@ -41,6 +105,51 @@ Documentation & source:
41
105
  process.exit(0);
42
106
  }
43
107
 
108
+ // ── --update (manual) ────────────────────────────────────────────────
109
+
110
+ if (args.includes("--update") || args.includes("-u")) {
111
+ console.log("\x1b[36m[update] Checking for updates...\x1b[0m");
112
+ const latest = await fetchLatestVersion(10000); // generous 10s timeout for manual
113
+ if (latest === null) {
114
+ console.error("\x1b[31m[update] Could not reach npm registry.\x1b[0m");
115
+ process.exit(1);
116
+ }
117
+ if (compareSemver(currentVersion, latest) >= 0) {
118
+ console.log(`\x1b[32m[update] Already up to date (v${currentVersion})\x1b[0m`);
119
+ process.exit(0);
120
+ }
121
+ console.log(`\x1b[36m[update] New version available: v${currentVersion} -> v${latest}\x1b[0m`);
122
+ console.log("\x1b[36m[update] Updating clawcontrol...\x1b[0m");
123
+ const ok = performUpdate();
124
+ process.exit(ok ? 0 : 1);
125
+ }
126
+
127
+ // ── Auto-update on startup ───────────────────────────────────────────
128
+
129
+ if (process.env.CLAWCONTROL_SKIP_UPDATE !== "1") {
130
+ const latest = await fetchLatestVersion(3000);
131
+ if (latest !== null && compareSemver(currentVersion, latest) < 0) {
132
+ console.log(`\x1b[36m[update] New version available: v${currentVersion} -> v${latest}\x1b[0m`);
133
+ console.log("\x1b[36m[update] Updating clawcontrol...\x1b[0m");
134
+ const ok = performUpdate();
135
+ if (ok) {
136
+ console.log("\x1b[36m[update] Restarting with updated version...\x1b[0m");
137
+ try {
138
+ execFileSync(process.argv[0], process.argv.slice(1), {
139
+ stdio: "inherit",
140
+ env: { ...process.env, CLAWCONTROL_SKIP_UPDATE: "1" },
141
+ });
142
+ process.exit(0);
143
+ } catch (e) {
144
+ process.exit(e.status ?? 1);
145
+ }
146
+ }
147
+ // If update failed, fall through and run the current version
148
+ }
149
+ }
150
+
151
+ // ── Launch the TUI via bun ───────────────────────────────────────────
152
+
44
153
  const __dirname = dirname(fileURLToPath(import.meta.url));
45
154
  const script = resolve(__dirname, "../dist/index.js");
46
155
 
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { createCliRenderer } from "@opentui/core";
5
5
  import { createRoot } from "@opentui/react";
6
6
 
7
7
  // src/App.tsx
8
- import { useState as useState12, useCallback as useCallback4, useRef as useRef7 } from "react";
8
+ import { useState as useState13, useCallback as useCallback4, useRef as useRef8 } from "react";
9
9
  import { useRenderer as useRenderer2 } from "@opentui/react";
10
10
 
11
11
  // src/components/Home.tsx
@@ -151,6 +151,7 @@ var COMMANDS = [
151
151
  { name: "/logs", description: "View deployment logs" },
152
152
  { name: "/dashboard", description: "Open OpenClaw dashboard in browser" },
153
153
  { name: "/destroy", description: "Destroy a deployment" },
154
+ { name: "/channels", description: "View configured channels" },
154
155
  { name: "/templates", description: "Manage deployment templates" },
155
156
  { name: "/help", description: "Show help" }
156
157
  ];
@@ -167,6 +168,7 @@ function Home({ context }) {
167
168
  "/logs": "logs",
168
169
  "/dashboard": "dashboard",
169
170
  "/destroy": "destroy",
171
+ "/channels": "channels",
170
172
  "/templates": "templates",
171
173
  "/help": "help"
172
174
  };
@@ -7415,22 +7417,175 @@ function DashboardView({ context }) {
7415
7417
  return null;
7416
7418
  }
7417
7419
 
7420
+ // src/components/ChannelsView.tsx
7421
+ import { useState as useState12, useRef as useRef7 } from "react";
7422
+ import { useKeyboard as useKeyboard13 } from "@opentui/react";
7423
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
7424
+ function maskToken(token) {
7425
+ if (token.length <= 10) return "****";
7426
+ return token.slice(0, 6) + "..." + token.slice(-4);
7427
+ }
7428
+ function ChannelsView({ context }) {
7429
+ const [viewState, setViewState] = useState12("listing");
7430
+ const [selectedIndex, setSelectedIndex] = useState12(0);
7431
+ const [revealed, setRevealed] = useState12(false);
7432
+ const deployments = context.deployments;
7433
+ const selectedDeployment = deployments[selectedIndex];
7434
+ const stateRef = useRef7({ viewState, selectedIndex });
7435
+ stateRef.current = { viewState, selectedIndex };
7436
+ useKeyboard13((key) => {
7437
+ const current = stateRef.current;
7438
+ if (current.viewState === "listing") {
7439
+ if (deployments.length === 0) {
7440
+ if (key.name === "escape") {
7441
+ context.navigateTo("home");
7442
+ }
7443
+ return;
7444
+ }
7445
+ if (key.name === "up" && current.selectedIndex > 0) {
7446
+ setSelectedIndex(current.selectedIndex - 1);
7447
+ } else if (key.name === "down" && current.selectedIndex < deployments.length - 1) {
7448
+ setSelectedIndex(current.selectedIndex + 1);
7449
+ } else if (key.name === "return") {
7450
+ setRevealed(false);
7451
+ setViewState("detail");
7452
+ } else if (key.name === "escape") {
7453
+ context.navigateTo("home");
7454
+ }
7455
+ } else if (current.viewState === "detail") {
7456
+ if (key.name === "r") {
7457
+ setRevealed((prev) => !prev);
7458
+ } else if (key.name === "escape") {
7459
+ setRevealed(false);
7460
+ setViewState("listing");
7461
+ }
7462
+ }
7463
+ });
7464
+ if (deployments.length === 0) {
7465
+ return /* @__PURE__ */ jsxs13("box", { flexDirection: "column", width: "100%", padding: 1, children: [
7466
+ /* @__PURE__ */ jsxs13("box", { flexDirection: "row", marginBottom: 2, children: [
7467
+ /* @__PURE__ */ jsx13("text", { fg: t.accent, children: "/channels" }),
7468
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.secondary, children: " - Communication Channels" })
7469
+ ] }),
7470
+ /* @__PURE__ */ jsxs13(
7471
+ "box",
7472
+ {
7473
+ flexDirection: "column",
7474
+ borderStyle: "single",
7475
+ borderColor: t.border.default,
7476
+ padding: 1,
7477
+ children: [
7478
+ /* @__PURE__ */ jsx13("text", { fg: t.status.warning, children: "No deployments found!" }),
7479
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.secondary, marginTop: 1, children: "Run /new to create a deployment first." })
7480
+ ]
7481
+ }
7482
+ ),
7483
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.muted, marginTop: 2, children: "Esc: Back" })
7484
+ ] });
7485
+ }
7486
+ if (viewState === "detail" && selectedDeployment) {
7487
+ const dep = selectedDeployment;
7488
+ const agent = dep.config.openclawAgent;
7489
+ return /* @__PURE__ */ jsxs13("box", { flexDirection: "column", width: "100%", padding: 1, children: [
7490
+ /* @__PURE__ */ jsxs13("box", { flexDirection: "row", marginBottom: 2, children: [
7491
+ /* @__PURE__ */ jsx13("text", { fg: t.accent, children: "/channels" }),
7492
+ /* @__PURE__ */ jsxs13("text", { fg: t.fg.secondary, children: [
7493
+ " - ",
7494
+ dep.config.name
7495
+ ] })
7496
+ ] }),
7497
+ /* @__PURE__ */ jsxs13(
7498
+ "box",
7499
+ {
7500
+ flexDirection: "column",
7501
+ borderStyle: "single",
7502
+ borderColor: t.border.focus,
7503
+ padding: 1,
7504
+ marginBottom: 1,
7505
+ children: [
7506
+ /* @__PURE__ */ jsx13("text", { fg: t.accent, marginBottom: 1, children: "Channel Details" }),
7507
+ /* @__PURE__ */ jsxs13("box", { flexDirection: "row", children: [
7508
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.secondary, width: 20, children: "Deployment:" }),
7509
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.primary, children: dep.config.name })
7510
+ ] }),
7511
+ agent ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
7512
+ /* @__PURE__ */ jsxs13("box", { flexDirection: "row", children: [
7513
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.secondary, width: 20, children: "Channel:" }),
7514
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.primary, children: agent.channel })
7515
+ ] }),
7516
+ /* @__PURE__ */ jsxs13("box", { flexDirection: "row", children: [
7517
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.secondary, width: 20, children: "Bot Token:" }),
7518
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.primary, children: revealed ? agent.telegramBotToken : maskToken(agent.telegramBotToken) })
7519
+ ] }),
7520
+ /* @__PURE__ */ jsxs13("box", { flexDirection: "row", children: [
7521
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.secondary, width: 20, children: "Allowed Users:" }),
7522
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.primary, children: agent.telegramAllowFrom || "Any" })
7523
+ ] })
7524
+ ] }) : /* @__PURE__ */ jsx13("box", { flexDirection: "row", marginTop: 1, children: /* @__PURE__ */ jsx13("text", { fg: t.fg.muted, children: "No channel configured for this deployment." }) })
7525
+ ]
7526
+ }
7527
+ ),
7528
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.muted, marginTop: 1, children: agent ? "R: Reveal/hide secrets | Esc: Back to list" : "Esc: Back to list" })
7529
+ ] });
7530
+ }
7531
+ return /* @__PURE__ */ jsxs13("box", { flexDirection: "column", width: "100%", padding: 1, children: [
7532
+ /* @__PURE__ */ jsxs13("box", { flexDirection: "row", marginBottom: 2, children: [
7533
+ /* @__PURE__ */ jsx13("text", { fg: t.accent, children: "/channels" }),
7534
+ /* @__PURE__ */ jsxs13("text", { fg: t.fg.secondary, children: [
7535
+ " - Communication Channels (",
7536
+ deployments.length,
7537
+ ")"
7538
+ ] })
7539
+ ] }),
7540
+ /* @__PURE__ */ jsx13(
7541
+ "box",
7542
+ {
7543
+ flexDirection: "column",
7544
+ borderStyle: "single",
7545
+ borderColor: t.border.default,
7546
+ padding: 1,
7547
+ children: deployments.map((dep, index) => {
7548
+ const isSelected = index === selectedIndex;
7549
+ const agent = dep.config.openclawAgent;
7550
+ return /* @__PURE__ */ jsxs13(
7551
+ "box",
7552
+ {
7553
+ flexDirection: "row",
7554
+ backgroundColor: isSelected ? t.selection.bg : void 0,
7555
+ children: [
7556
+ /* @__PURE__ */ jsx13("text", { fg: isSelected ? t.selection.indicator : t.fg.primary, children: isSelected ? "> " : " " }),
7557
+ /* @__PURE__ */ jsx13("text", { fg: isSelected ? t.selection.fg : t.fg.primary, width: 22, children: dep.config.name }),
7558
+ agent ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
7559
+ /* @__PURE__ */ jsx13("text", { fg: isSelected ? t.fg.primary : t.fg.secondary, width: 12, children: agent.channel }),
7560
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.muted, children: agent.telegramAllowFrom ? `users: ${agent.telegramAllowFrom}` : "" })
7561
+ ] }) : /* @__PURE__ */ jsx13("text", { fg: t.fg.muted, children: "No channel" })
7562
+ ]
7563
+ },
7564
+ dep.config.name
7565
+ );
7566
+ })
7567
+ }
7568
+ ),
7569
+ /* @__PURE__ */ jsx13("text", { fg: t.fg.muted, marginTop: 2, children: "Up/Down: Select | Enter: Details | Esc: Back" })
7570
+ ] });
7571
+ }
7572
+
7418
7573
  // src/App.tsx
7419
- import { jsx as jsx13, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
7574
+ import { jsx as jsx14, jsxs as jsxs14 } from "@opentui/react/jsx-runtime";
7420
7575
  function App({ lacksTrueColor: lacksTrueColor2 }) {
7421
7576
  const renderer = useRenderer2();
7422
- const [currentView, setCurrentView] = useState12("home");
7423
- const [selectedDeployment, setSelectedDeployment] = useState12(null);
7424
- const [deployments, setDeployments] = useState12(() => {
7577
+ const [currentView, setCurrentView] = useState13("home");
7578
+ const [selectedDeployment, setSelectedDeployment] = useState13(null);
7579
+ const [deployments, setDeployments] = useState13(() => {
7425
7580
  try {
7426
7581
  return getAllDeployments();
7427
7582
  } catch {
7428
7583
  return [];
7429
7584
  }
7430
7585
  });
7431
- const [selectedTemplate, setSelectedTemplate] = useState12(null);
7432
- const [editingDeployment, setEditingDeployment] = useState12(null);
7433
- const wasDraggingRef = useRef7(false);
7586
+ const [selectedTemplate, setSelectedTemplate] = useState13(null);
7587
+ const [editingDeployment, setEditingDeployment] = useState13(null);
7588
+ const wasDraggingRef = useRef8(false);
7434
7589
  const handleMouseDrag = useCallback4(() => {
7435
7590
  wasDraggingRef.current = true;
7436
7591
  }, []);
@@ -7472,34 +7627,36 @@ function App({ lacksTrueColor: lacksTrueColor2 }) {
7472
7627
  const renderView = () => {
7473
7628
  switch (currentView) {
7474
7629
  case "home":
7475
- return /* @__PURE__ */ jsx13(Home, { context });
7630
+ return /* @__PURE__ */ jsx14(Home, { context });
7476
7631
  case "new":
7477
- return /* @__PURE__ */ jsx13(NewDeployment, { context });
7632
+ return /* @__PURE__ */ jsx14(NewDeployment, { context });
7478
7633
  case "list":
7479
- return /* @__PURE__ */ jsx13(ListView, { context });
7634
+ return /* @__PURE__ */ jsx14(ListView, { context });
7480
7635
  case "deploy":
7481
- return /* @__PURE__ */ jsx13(DeployView, { context });
7636
+ return /* @__PURE__ */ jsx14(DeployView, { context });
7482
7637
  case "deploying":
7483
- return /* @__PURE__ */ jsx13(DeployingView, { context });
7638
+ return /* @__PURE__ */ jsx14(DeployingView, { context });
7484
7639
  case "status":
7485
- return /* @__PURE__ */ jsx13(StatusView, { context });
7640
+ return /* @__PURE__ */ jsx14(StatusView, { context });
7486
7641
  case "ssh":
7487
- return /* @__PURE__ */ jsx13(SSHView, { context });
7642
+ return /* @__PURE__ */ jsx14(SSHView, { context });
7488
7643
  case "logs":
7489
- return /* @__PURE__ */ jsx13(LogsView, { context });
7644
+ return /* @__PURE__ */ jsx14(LogsView, { context });
7490
7645
  case "dashboard":
7491
- return /* @__PURE__ */ jsx13(DashboardView, { context });
7646
+ return /* @__PURE__ */ jsx14(DashboardView, { context });
7492
7647
  case "destroy":
7493
- return /* @__PURE__ */ jsx13(DestroyView, { context });
7648
+ return /* @__PURE__ */ jsx14(DestroyView, { context });
7494
7649
  case "help":
7495
- return /* @__PURE__ */ jsx13(HelpView, { context });
7650
+ return /* @__PURE__ */ jsx14(HelpView, { context });
7496
7651
  case "templates":
7497
- return /* @__PURE__ */ jsx13(TemplatesView, { context });
7652
+ return /* @__PURE__ */ jsx14(TemplatesView, { context });
7653
+ case "channels":
7654
+ return /* @__PURE__ */ jsx14(ChannelsView, { context });
7498
7655
  default:
7499
- return /* @__PURE__ */ jsx13(Home, { context });
7656
+ return /* @__PURE__ */ jsx14(Home, { context });
7500
7657
  }
7501
7658
  };
7502
- return /* @__PURE__ */ jsxs13(
7659
+ return /* @__PURE__ */ jsxs14(
7503
7660
  "scrollbox",
7504
7661
  {
7505
7662
  width: "100%",
@@ -7521,7 +7678,7 @@ function App({ lacksTrueColor: lacksTrueColor2 }) {
7521
7678
  showArrows: false
7522
7679
  },
7523
7680
  children: [
7524
- lacksTrueColor2 && /* @__PURE__ */ jsx13(
7681
+ lacksTrueColor2 && /* @__PURE__ */ jsx14(
7525
7682
  "box",
7526
7683
  {
7527
7684
  width: "100%",
@@ -7532,7 +7689,7 @@ function App({ lacksTrueColor: lacksTrueColor2 }) {
7532
7689
  paddingBottom: 0,
7533
7690
  backgroundColor: t.status.warning
7534
7691
  },
7535
- children: /* @__PURE__ */ jsx13("text", { fg: "#000000", children: "\u26A0 Your terminal does not support true color. Colors may look wrong. For full color support, use Ghostty, iTerm2, Kitty, or WezTerm \u2014 or upgrade to macOS 26+ for Terminal.app true color support." })
7692
+ children: /* @__PURE__ */ jsx14("text", { fg: "#000000", children: "\u26A0 Your terminal does not support true color. Colors may look wrong. For full color support, use Ghostty, iTerm2, Kitty, or WezTerm \u2014 or upgrade to macOS 26+ for Terminal.app true color support." })
7536
7693
  }
7537
7694
  ),
7538
7695
  renderView()
@@ -7542,7 +7699,7 @@ function App({ lacksTrueColor: lacksTrueColor2 }) {
7542
7699
  }
7543
7700
 
7544
7701
  // src/index.tsx
7545
- import { jsx as jsx14 } from "@opentui/react/jsx-runtime";
7702
+ import { jsx as jsx15 } from "@opentui/react/jsx-runtime";
7546
7703
  var args = process.argv.slice(2);
7547
7704
  if (args.includes("--version") || args.includes("-v")) {
7548
7705
  const require2 = createRequire(import.meta.url);
@@ -7579,6 +7736,6 @@ async function main() {
7579
7736
  useMouse: true
7580
7737
  });
7581
7738
  const root = createRoot(renderer);
7582
- root.render(/* @__PURE__ */ jsx14(App, { lacksTrueColor }));
7739
+ root.render(/* @__PURE__ */ jsx15(App, { lacksTrueColor }));
7583
7740
  }
7584
7741
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawcontrol",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "CLI tool for deploying and managing OpenClaw instances on VPS providers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",