clawcontrol 0.2.0 → 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
@@ -1,14 +1,16 @@
1
1
  // src/index.tsx
2
2
  import { createRequire } from "module";
3
+ import { release } from "os";
3
4
  import { createCliRenderer } from "@opentui/core";
4
5
  import { createRoot } from "@opentui/react";
5
6
 
6
7
  // src/App.tsx
7
- import { useState as useState12, useCallback as useCallback4, useRef as useRef6 } from "react";
8
+ import { useState as useState13, useCallback as useCallback4, useRef as useRef8 } from "react";
8
9
  import { useRenderer as useRenderer2 } from "@opentui/react";
9
10
 
10
11
  // src/components/Home.tsx
11
- import { useState } from "react";
12
+ import { useState, useRef } from "react";
13
+ import { useKeyboard } from "@opentui/react";
12
14
 
13
15
  // src/theme.ts
14
16
  var palette = {
@@ -149,33 +151,61 @@ var COMMANDS = [
149
151
  { name: "/logs", description: "View deployment logs" },
150
152
  { name: "/dashboard", description: "Open OpenClaw dashboard in browser" },
151
153
  { name: "/destroy", description: "Destroy a deployment" },
154
+ { name: "/channels", description: "View configured channels" },
152
155
  { name: "/templates", description: "Manage deployment templates" },
153
156
  { name: "/help", description: "Show help" }
154
157
  ];
155
158
  function Home({ context }) {
156
159
  const [inputValue, setInputValue] = useState("");
157
160
  const [error, setError] = useState(null);
161
+ const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
162
+ const viewMap = {
163
+ "/new": "new",
164
+ "/list": "list",
165
+ "/deploy": "deploy",
166
+ "/status": "status",
167
+ "/ssh": "ssh",
168
+ "/logs": "logs",
169
+ "/dashboard": "dashboard",
170
+ "/destroy": "destroy",
171
+ "/channels": "channels",
172
+ "/templates": "templates",
173
+ "/help": "help"
174
+ };
175
+ const filteredCommands = inputValue.length >= 2 && inputValue.startsWith("/") ? COMMANDS.filter((cmd) => cmd.name.startsWith(inputValue.toLowerCase())) : [];
176
+ const stateRef = useRef({ selectedSuggestionIndex, filteredCommands, inputValue });
177
+ stateRef.current = { selectedSuggestionIndex, filteredCommands, inputValue };
158
178
  const handleCommand = (command) => {
159
179
  const cmd = command.trim().toLowerCase();
160
180
  setError(null);
161
- const viewMap = {
162
- "/new": "new",
163
- "/list": "list",
164
- "/deploy": "deploy",
165
- "/status": "status",
166
- "/ssh": "ssh",
167
- "/logs": "logs",
168
- "/dashboard": "dashboard",
169
- "/destroy": "destroy",
170
- "/templates": "templates",
171
- "/help": "help"
172
- };
173
181
  if (viewMap[cmd]) {
174
182
  context.navigateTo(viewMap[cmd]);
175
183
  } else if (cmd.startsWith("/")) {
176
184
  setError(`Unknown command: ${cmd}. Type /help for available commands.`);
177
185
  }
178
186
  };
187
+ useKeyboard((key) => {
188
+ const s = stateRef.current;
189
+ if (s.filteredCommands.length === 0) return;
190
+ if (key.name === "down") {
191
+ setSelectedSuggestionIndex(
192
+ (prev) => prev < s.filteredCommands.length - 1 ? prev + 1 : 0
193
+ );
194
+ } else if (key.name === "up") {
195
+ setSelectedSuggestionIndex(
196
+ (prev) => prev > 0 ? prev - 1 : s.filteredCommands.length - 1
197
+ );
198
+ } else if (key.name === "tab") {
199
+ if (s.selectedSuggestionIndex >= 0 && s.selectedSuggestionIndex < s.filteredCommands.length) {
200
+ const cmd = s.filteredCommands[s.selectedSuggestionIndex].name;
201
+ setInputValue("");
202
+ setSelectedSuggestionIndex(-1);
203
+ handleCommand(cmd);
204
+ }
205
+ } else if (key.name === "escape") {
206
+ setSelectedSuggestionIndex(-1);
207
+ }
208
+ });
179
209
  return /* @__PURE__ */ jsxs("box", { flexDirection: "column", width: "100%", height: "100%", children: [
180
210
  /* @__PURE__ */ jsx(
181
211
  "scrollbox",
@@ -278,9 +308,19 @@ function Home({ context }) {
278
308
  placeholder: "Type a command (e.g., /new)...",
279
309
  focused: true,
280
310
  width: "100%",
281
- onInput: (value) => setInputValue(value),
311
+ onInput: (value) => {
312
+ setInputValue(value);
313
+ const matches = value.length >= 2 && value.startsWith("/") ? COMMANDS.filter((cmd) => cmd.name.startsWith(value.toLowerCase())) : [];
314
+ setSelectedSuggestionIndex(matches.length > 0 ? 0 : -1);
315
+ },
282
316
  onSubmit: (value) => {
283
- if (typeof value === "string" && typeof value.trim === "function" && value.trim()) {
317
+ const s = stateRef.current;
318
+ if (s.selectedSuggestionIndex >= 0 && s.selectedSuggestionIndex < s.filteredCommands.length) {
319
+ const cmd = s.filteredCommands[s.selectedSuggestionIndex].name;
320
+ setInputValue("");
321
+ setSelectedSuggestionIndex(-1);
322
+ handleCommand(cmd);
323
+ } else if (typeof value === "string" && typeof value.trim === "function" && value.trim()) {
284
324
  handleCommand(value);
285
325
  setInputValue("");
286
326
  }
@@ -290,14 +330,22 @@ function Home({ context }) {
290
330
  }
291
331
  )
292
332
  }
293
- )
333
+ ),
334
+ filteredCommands.length > 0 && /* @__PURE__ */ jsx("box", { flexDirection: "column", paddingLeft: 1, backgroundColor: t.bg.elevated, children: filteredCommands.map((cmd, i) => {
335
+ const selected = i === selectedSuggestionIndex;
336
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "row", height: 1, overflow: "hidden", backgroundColor: selected ? t.selection.bg : t.bg.elevated, children: [
337
+ /* @__PURE__ */ jsx("text", { fg: selected ? t.selection.indicator : t.fg.muted, children: selected ? "> " : " " }),
338
+ /* @__PURE__ */ jsx("text", { fg: selected ? t.accent : t.fg.primary, width: 14, children: cmd.name }),
339
+ /* @__PURE__ */ jsx("text", { fg: t.fg.secondary, children: cmd.description })
340
+ ] }, cmd.name);
341
+ }) })
294
342
  ] })
295
343
  ] });
296
344
  }
297
345
 
298
346
  // src/components/NewDeployment.tsx
299
- import { useState as useState2, useRef, useEffect } from "react";
300
- import { useKeyboard } from "@opentui/react";
347
+ import { useState as useState2, useRef as useRef2, useEffect } from "react";
348
+ import { useKeyboard as useKeyboard2 } from "@opentui/react";
301
349
  import { appendFileSync } from "fs";
302
350
  import { homedir as homedir3 } from "os";
303
351
  import { join as join4 } from "path";
@@ -1428,7 +1476,7 @@ function NewDeployment({ context }) {
1428
1476
  setSelectedSavedKeyIndex(0);
1429
1477
  }
1430
1478
  }, [step]);
1431
- const stateRef = useRef({
1479
+ const stateRef = useRef2({
1432
1480
  name,
1433
1481
  provider,
1434
1482
  apiKey,
@@ -1558,7 +1606,7 @@ function NewDeployment({ context }) {
1558
1606
  setError(`Failed to ${s.editMode === "edit" ? "update" : "create"} deployment: ${err instanceof Error ? err.message : String(err)}`);
1559
1607
  }
1560
1608
  };
1561
- useKeyboard((key) => {
1609
+ useKeyboard2((key) => {
1562
1610
  const currentState = stateRef.current;
1563
1611
  debugLog(`useKeyboard: key=${key.name}, step=${currentState.step}`);
1564
1612
  if (currentState.step === "template_choice") {
@@ -2811,8 +2859,8 @@ function NewDeployment({ context }) {
2811
2859
  }
2812
2860
 
2813
2861
  // src/components/ListView.tsx
2814
- import { useState as useState3, useRef as useRef2 } from "react";
2815
- import { useKeyboard as useKeyboard2 } from "@opentui/react";
2862
+ import { useState as useState3, useRef as useRef3 } from "react";
2863
+ import { useKeyboard as useKeyboard3 } from "@opentui/react";
2816
2864
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "@opentui/react/jsx-runtime";
2817
2865
  function ListView({ context }) {
2818
2866
  const [viewState, setViewState] = useState3("listing");
@@ -2822,9 +2870,9 @@ function ListView({ context }) {
2822
2870
  const [deletedName, setDeletedName] = useState3("");
2823
2871
  const deployments = context.deployments;
2824
2872
  const selectedDeployment = deployments[selectedIndex];
2825
- const stateRef = useRef2({ viewState, selectedIndex });
2873
+ const stateRef = useRef3({ viewState, selectedIndex });
2826
2874
  stateRef.current = { viewState, selectedIndex };
2827
- useKeyboard2((key) => {
2875
+ useKeyboard3((key) => {
2828
2876
  const current = stateRef.current;
2829
2877
  if (deployments.length === 0) {
2830
2878
  context.navigateTo("home");
@@ -3122,7 +3170,7 @@ function ListView({ context }) {
3122
3170
 
3123
3171
  // src/components/DeployView.tsx
3124
3172
  import { useState as useState4 } from "react";
3125
- import { useKeyboard as useKeyboard3 } from "@opentui/react";
3173
+ import { useKeyboard as useKeyboard4 } from "@opentui/react";
3126
3174
  import { jsx as jsx4, jsxs as jsxs4 } from "@opentui/react/jsx-runtime";
3127
3175
  function DeployView({ context }) {
3128
3176
  const [selectedIndex, setSelectedIndex] = useState4(0);
@@ -3132,7 +3180,7 @@ function DeployView({ context }) {
3132
3180
  const deployed = deployments.filter((d) => d.state.status === "deployed");
3133
3181
  const allDeployments = [...notDeployed, ...deployed];
3134
3182
  const selectedDeployment = allDeployments[selectedIndex];
3135
- useKeyboard3((key) => {
3183
+ useKeyboard4((key) => {
3136
3184
  if (allDeployments.length === 0) {
3137
3185
  context.navigateTo("home");
3138
3186
  return;
@@ -3251,8 +3299,8 @@ function DeployView({ context }) {
3251
3299
  }
3252
3300
 
3253
3301
  // src/components/DeployingView.tsx
3254
- import { useState as useState5, useEffect as useEffect2, useCallback, useRef as useRef3 } from "react";
3255
- import { useKeyboard as useKeyboard4 } from "@opentui/react";
3302
+ import { useState as useState5, useEffect as useEffect2, useCallback, useRef as useRef4 } from "react";
3303
+ import { useKeyboard as useKeyboard5 } from "@opentui/react";
3256
3304
  import open from "open";
3257
3305
 
3258
3306
  // src/services/ssh.ts
@@ -4713,6 +4761,12 @@ function detectTerminal() {
4713
4761
  return { app: "konsole", canOpenTab: true };
4714
4762
  }
4715
4763
  }
4764
+ if (os === "win32") {
4765
+ if (env.WT_SESSION) {
4766
+ return { app: "windows-terminal", canOpenTab: true };
4767
+ }
4768
+ return { app: "powershell", canOpenTab: false };
4769
+ }
4716
4770
  if (os === "darwin") {
4717
4771
  return { app: "terminal.app", canOpenTab: true };
4718
4772
  }
@@ -4747,11 +4801,19 @@ function openTerminalWithCommand(command) {
4747
4801
  return openKonsole(command);
4748
4802
  case "xfce4-terminal":
4749
4803
  return openXfce4Terminal(command);
4804
+ case "windows-terminal":
4805
+ return openWindowsTerminal(command);
4806
+ case "powershell":
4807
+ return openPowerShell(command);
4808
+ case "cmd":
4809
+ return openCmd(command);
4750
4810
  default:
4751
4811
  if (os === "darwin") {
4752
4812
  return openTerminalApp(command);
4753
4813
  } else if (os === "linux") {
4754
4814
  return openLinuxFallback(command);
4815
+ } else if (os === "win32") {
4816
+ return openWindowsFallback(command);
4755
4817
  }
4756
4818
  return { success: false, error: `Unsupported terminal or OS: ${terminal.app}` };
4757
4819
  }
@@ -4947,6 +5009,9 @@ function openCursorTerminal(command) {
4947
5009
  }
4948
5010
  return openTerminalApp(command);
4949
5011
  }
5012
+ if (platform() === "win32") {
5013
+ return openWindowsFallback(command);
5014
+ }
4950
5015
  return openLinuxFallback(command);
4951
5016
  }
4952
5017
  function openVSCodeTerminal(command) {
@@ -5018,6 +5083,62 @@ function openLinuxFallback(command) {
5018
5083
  }
5019
5084
  return { success: false, error: "Could not find a supported terminal emulator" };
5020
5085
  }
5086
+ function createTempScriptWindows(command) {
5087
+ const scriptPath = join5(tmpdir(), `clawcontrol-${process.pid}-${Date.now()}.cmd`);
5088
+ const content = [
5089
+ "@echo off",
5090
+ command,
5091
+ `del "${scriptPath}"`,
5092
+ ""
5093
+ ].join("\r\n");
5094
+ writeFileSync5(scriptPath, content);
5095
+ return scriptPath;
5096
+ }
5097
+ function openWindowsTerminal(command) {
5098
+ const script = createTempScriptWindows(command);
5099
+ const proc = spawn("wt.exe", ["new-tab", "cmd", "/c", script], {
5100
+ stdio: "ignore",
5101
+ detached: true,
5102
+ shell: true
5103
+ });
5104
+ proc.unref();
5105
+ return { success: true };
5106
+ }
5107
+ function openPowerShell(command) {
5108
+ const proc = spawn("powershell.exe", [
5109
+ "-NoProfile",
5110
+ "Start-Process",
5111
+ "powershell",
5112
+ "-ArgumentList",
5113
+ `'-NoExit -Command "${command.replace(/"/g, '`"')}"'`
5114
+ ], {
5115
+ stdio: "ignore",
5116
+ detached: true,
5117
+ shell: true
5118
+ });
5119
+ proc.unref();
5120
+ return { success: true };
5121
+ }
5122
+ function openCmd(command) {
5123
+ const script = createTempScriptWindows(command);
5124
+ const proc = spawn("cmd.exe", ["/c", "start", "cmd", "/k", script], {
5125
+ stdio: "ignore",
5126
+ detached: true,
5127
+ shell: true
5128
+ });
5129
+ proc.unref();
5130
+ return { success: true };
5131
+ }
5132
+ function openWindowsFallback(command) {
5133
+ try {
5134
+ const result = spawnSync("where", ["wt.exe"], { stdio: "pipe", timeout: 2e3 });
5135
+ if (result.status === 0) {
5136
+ return openWindowsTerminal(command);
5137
+ }
5138
+ } catch {
5139
+ }
5140
+ return openPowerShell(command);
5141
+ }
5021
5142
  function getTerminalDisplayName(app) {
5022
5143
  const names = {
5023
5144
  "terminal.app": "Terminal.app",
@@ -5033,6 +5154,9 @@ function getTerminalDisplayName(app) {
5033
5154
  "konsole": "Konsole",
5034
5155
  "xfce4-terminal": "XFCE Terminal",
5035
5156
  "xterm": "XTerm",
5157
+ "windows-terminal": "Windows Terminal",
5158
+ "powershell": "PowerShell",
5159
+ "cmd": "Command Prompt",
5036
5160
  "unknown": "System Terminal"
5037
5161
  };
5038
5162
  return names[app];
@@ -5094,9 +5218,9 @@ function DeployingView({ context }) {
5094
5218
  setDeployState("deploying");
5095
5219
  addLog("Terminal session confirmed complete, continuing deployment...");
5096
5220
  }, [terminalResolve, addLog]);
5097
- const stateRef = useRef3({ deployState, terminalResolve });
5221
+ const stateRef = useRef4({ deployState, terminalResolve });
5098
5222
  stateRef.current = { deployState, terminalResolve };
5099
- useKeyboard4((key) => {
5223
+ useKeyboard5((key) => {
5100
5224
  const currentState = stateRef.current;
5101
5225
  if (currentState.deployState === "waiting_terminal") {
5102
5226
  if (key.name === "return") {
@@ -5344,7 +5468,7 @@ function DeployingView({ context }) {
5344
5468
 
5345
5469
  // src/components/StatusView.tsx
5346
5470
  import { useState as useState6 } from "react";
5347
- import { useKeyboard as useKeyboard5 } from "@opentui/react";
5471
+ import { useKeyboard as useKeyboard6 } from "@opentui/react";
5348
5472
  import { jsx as jsx6, jsxs as jsxs6 } from "@opentui/react/jsx-runtime";
5349
5473
  function StatusView({ context }) {
5350
5474
  const [selectedIndex, setSelectedIndex] = useState6(0);
@@ -5374,7 +5498,7 @@ function StatusView({ context }) {
5374
5498
  setHealthStatus((prev) => new Map(prev).set(name, health));
5375
5499
  setChecking(null);
5376
5500
  };
5377
- useKeyboard5((key) => {
5501
+ useKeyboard6((key) => {
5378
5502
  if (deployments.length === 0) {
5379
5503
  context.navigateTo("home");
5380
5504
  return;
@@ -5528,7 +5652,7 @@ function StatusView({ context }) {
5528
5652
 
5529
5653
  // src/components/SSHView.tsx
5530
5654
  import { useState as useState7, useCallback as useCallback2 } from "react";
5531
- import { useKeyboard as useKeyboard6 } from "@opentui/react";
5655
+ import { useKeyboard as useKeyboard7 } from "@opentui/react";
5532
5656
  import { jsx as jsx7, jsxs as jsxs7 } from "@opentui/react/jsx-runtime";
5533
5657
  function SSHView({ context }) {
5534
5658
  const [viewState, setViewState] = useState7("selecting");
@@ -5554,7 +5678,7 @@ function SSHView({ context }) {
5554
5678
  }
5555
5679
  }, []);
5556
5680
  const selectedDeployment = deployedDeployments[selectedIndex];
5557
- useKeyboard6((key) => {
5681
+ useKeyboard7((key) => {
5558
5682
  if (deployedDeployments.length === 0) {
5559
5683
  context.navigateTo("home");
5560
5684
  return;
@@ -5709,8 +5833,8 @@ function SSHView({ context }) {
5709
5833
  }
5710
5834
 
5711
5835
  // src/components/LogsView.tsx
5712
- import { useState as useState8, useEffect as useEffect3, useRef as useRef4 } from "react";
5713
- import { useKeyboard as useKeyboard7 } from "@opentui/react";
5836
+ import { useState as useState8, useEffect as useEffect3, useRef as useRef5 } from "react";
5837
+ import { useKeyboard as useKeyboard8 } from "@opentui/react";
5714
5838
  import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
5715
5839
  function parseLogLine(line) {
5716
5840
  if (!line.trim()) return null;
@@ -5736,8 +5860,8 @@ function LogsView({ context }) {
5736
5860
  const [error, setError] = useState8(null);
5737
5861
  const [autoRefresh, setAutoRefresh] = useState8(false);
5738
5862
  const [lastFetched, setLastFetched] = useState8(null);
5739
- const sshRef = useRef4(null);
5740
- const refreshIntervalRef = useRef4(null);
5863
+ const sshRef = useRef5(null);
5864
+ const refreshIntervalRef = useRef5(null);
5741
5865
  const deployedDeployments = context.deployments.filter(
5742
5866
  (d) => d.state.status === "deployed" && d.state.serverIp
5743
5867
  );
@@ -5788,7 +5912,7 @@ function LogsView({ context }) {
5788
5912
  setLogs([]);
5789
5913
  };
5790
5914
  const selectedDeployment = deployedDeployments[selectedIndex];
5791
- useKeyboard7((key) => {
5915
+ useKeyboard8((key) => {
5792
5916
  if (deployedDeployments.length === 0) {
5793
5917
  context.navigateTo("home");
5794
5918
  return;
@@ -5947,7 +6071,7 @@ function LogsView({ context }) {
5947
6071
 
5948
6072
  // src/components/DestroyView.tsx
5949
6073
  import { useState as useState9 } from "react";
5950
- import { useKeyboard as useKeyboard8 } from "@opentui/react";
6074
+ import { useKeyboard as useKeyboard9 } from "@opentui/react";
5951
6075
  import { jsx as jsx9, jsxs as jsxs9 } from "@opentui/react/jsx-runtime";
5952
6076
  function DestroyView({ context }) {
5953
6077
  const [viewState, setViewState] = useState9("selecting");
@@ -5998,7 +6122,7 @@ function DestroyView({ context }) {
5998
6122
  }
5999
6123
  };
6000
6124
  const selectedDeployment = deployments[selectedIndex];
6001
- useKeyboard8((key) => {
6125
+ useKeyboard9((key) => {
6002
6126
  if (deployments.length === 0) {
6003
6127
  context.navigateTo("home");
6004
6128
  return;
@@ -6189,10 +6313,10 @@ function DestroyView({ context }) {
6189
6313
  }
6190
6314
 
6191
6315
  // src/components/HelpView.tsx
6192
- import { useKeyboard as useKeyboard9 } from "@opentui/react";
6316
+ import { useKeyboard as useKeyboard10 } from "@opentui/react";
6193
6317
  import { jsx as jsx10, jsxs as jsxs10 } from "@opentui/react/jsx-runtime";
6194
6318
  function HelpView({ context }) {
6195
- useKeyboard9(() => {
6319
+ useKeyboard10(() => {
6196
6320
  context.navigateTo("home");
6197
6321
  });
6198
6322
  return /* @__PURE__ */ jsxs10("box", { flexDirection: "column", width: "100%", padding: 1, children: [
@@ -6319,8 +6443,8 @@ function HelpView({ context }) {
6319
6443
  }
6320
6444
 
6321
6445
  // src/components/TemplatesView.tsx
6322
- import { useState as useState10, useRef as useRef5, useEffect as useEffect4 } from "react";
6323
- import { useKeyboard as useKeyboard10 } from "@opentui/react";
6446
+ import { useState as useState10, useRef as useRef6, useEffect as useEffect4 } from "react";
6447
+ import { useKeyboard as useKeyboard11 } from "@opentui/react";
6324
6448
  import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
6325
6449
  var DO_DROPLET_SIZES2 = [
6326
6450
  { slug: "s-1vcpu-2gb", label: "1 vCPU, 2GB RAM, 50GB SSD", price: "$12/mo" },
@@ -6343,7 +6467,7 @@ function TemplatesView({ context }) {
6343
6467
  const [forkAiProvider, setForkAiProvider] = useState10("");
6344
6468
  const [forkAiProviderIndex, setForkAiProviderIndex] = useState10(0);
6345
6469
  const [forkModel, setForkModel] = useState10("");
6346
- const stateRef = useRef5({
6470
+ const stateRef = useRef6({
6347
6471
  viewState,
6348
6472
  selectedIndex,
6349
6473
  selectedTemplate,
@@ -6457,7 +6581,7 @@ function TemplatesView({ context }) {
6457
6581
  setViewState("viewing");
6458
6582
  }
6459
6583
  };
6460
- useKeyboard10((key) => {
6584
+ useKeyboard11((key) => {
6461
6585
  const s = stateRef.current;
6462
6586
  if (s.viewState === "listing") {
6463
6587
  if (key.name === "up") {
@@ -6866,7 +6990,7 @@ function TemplatesView({ context }) {
6866
6990
 
6867
6991
  // src/components/DashboardView.tsx
6868
6992
  import { useState as useState11, useCallback as useCallback3 } from "react";
6869
- import { useKeyboard as useKeyboard11, useRenderer } from "@opentui/react";
6993
+ import { useKeyboard as useKeyboard12, useRenderer } from "@opentui/react";
6870
6994
  import { spawnSync as spawnSync2 } from "child_process";
6871
6995
  import { platform as platform2 } from "os";
6872
6996
 
@@ -7035,7 +7159,7 @@ function DashboardView({ context }) {
7035
7159
  setViewState("error");
7036
7160
  }
7037
7161
  }, []);
7038
- useKeyboard11((key) => {
7162
+ useKeyboard12((key) => {
7039
7163
  if (viewState === "selecting") {
7040
7164
  if (deployedDeployments.length === 0) {
7041
7165
  if (key.name === "escape" || key.name === "return") {
@@ -7293,22 +7417,175 @@ function DashboardView({ context }) {
7293
7417
  return null;
7294
7418
  }
7295
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
+
7296
7573
  // src/App.tsx
7297
- import { jsx as jsx13 } from "@opentui/react/jsx-runtime";
7298
- function App() {
7574
+ import { jsx as jsx14, jsxs as jsxs14 } from "@opentui/react/jsx-runtime";
7575
+ function App({ lacksTrueColor: lacksTrueColor2 }) {
7299
7576
  const renderer = useRenderer2();
7300
- const [currentView, setCurrentView] = useState12("home");
7301
- const [selectedDeployment, setSelectedDeployment] = useState12(null);
7302
- const [deployments, setDeployments] = useState12(() => {
7577
+ const [currentView, setCurrentView] = useState13("home");
7578
+ const [selectedDeployment, setSelectedDeployment] = useState13(null);
7579
+ const [deployments, setDeployments] = useState13(() => {
7303
7580
  try {
7304
7581
  return getAllDeployments();
7305
7582
  } catch {
7306
7583
  return [];
7307
7584
  }
7308
7585
  });
7309
- const [selectedTemplate, setSelectedTemplate] = useState12(null);
7310
- const [editingDeployment, setEditingDeployment] = useState12(null);
7311
- const wasDraggingRef = useRef6(false);
7586
+ const [selectedTemplate, setSelectedTemplate] = useState13(null);
7587
+ const [editingDeployment, setEditingDeployment] = useState13(null);
7588
+ const wasDraggingRef = useRef8(false);
7312
7589
  const handleMouseDrag = useCallback4(() => {
7313
7590
  wasDraggingRef.current = true;
7314
7591
  }, []);
@@ -7350,34 +7627,36 @@ function App() {
7350
7627
  const renderView = () => {
7351
7628
  switch (currentView) {
7352
7629
  case "home":
7353
- return /* @__PURE__ */ jsx13(Home, { context });
7630
+ return /* @__PURE__ */ jsx14(Home, { context });
7354
7631
  case "new":
7355
- return /* @__PURE__ */ jsx13(NewDeployment, { context });
7632
+ return /* @__PURE__ */ jsx14(NewDeployment, { context });
7356
7633
  case "list":
7357
- return /* @__PURE__ */ jsx13(ListView, { context });
7634
+ return /* @__PURE__ */ jsx14(ListView, { context });
7358
7635
  case "deploy":
7359
- return /* @__PURE__ */ jsx13(DeployView, { context });
7636
+ return /* @__PURE__ */ jsx14(DeployView, { context });
7360
7637
  case "deploying":
7361
- return /* @__PURE__ */ jsx13(DeployingView, { context });
7638
+ return /* @__PURE__ */ jsx14(DeployingView, { context });
7362
7639
  case "status":
7363
- return /* @__PURE__ */ jsx13(StatusView, { context });
7640
+ return /* @__PURE__ */ jsx14(StatusView, { context });
7364
7641
  case "ssh":
7365
- return /* @__PURE__ */ jsx13(SSHView, { context });
7642
+ return /* @__PURE__ */ jsx14(SSHView, { context });
7366
7643
  case "logs":
7367
- return /* @__PURE__ */ jsx13(LogsView, { context });
7644
+ return /* @__PURE__ */ jsx14(LogsView, { context });
7368
7645
  case "dashboard":
7369
- return /* @__PURE__ */ jsx13(DashboardView, { context });
7646
+ return /* @__PURE__ */ jsx14(DashboardView, { context });
7370
7647
  case "destroy":
7371
- return /* @__PURE__ */ jsx13(DestroyView, { context });
7648
+ return /* @__PURE__ */ jsx14(DestroyView, { context });
7372
7649
  case "help":
7373
- return /* @__PURE__ */ jsx13(HelpView, { context });
7650
+ return /* @__PURE__ */ jsx14(HelpView, { context });
7374
7651
  case "templates":
7375
- return /* @__PURE__ */ jsx13(TemplatesView, { context });
7652
+ return /* @__PURE__ */ jsx14(TemplatesView, { context });
7653
+ case "channels":
7654
+ return /* @__PURE__ */ jsx14(ChannelsView, { context });
7376
7655
  default:
7377
- return /* @__PURE__ */ jsx13(Home, { context });
7656
+ return /* @__PURE__ */ jsx14(Home, { context });
7378
7657
  }
7379
7658
  };
7380
- return /* @__PURE__ */ jsx13(
7659
+ return /* @__PURE__ */ jsxs14(
7381
7660
  "scrollbox",
7382
7661
  {
7383
7662
  width: "100%",
@@ -7398,13 +7677,29 @@ function App() {
7398
7677
  verticalScrollbarOptions: {
7399
7678
  showArrows: false
7400
7679
  },
7401
- children: renderView()
7680
+ children: [
7681
+ lacksTrueColor2 && /* @__PURE__ */ jsx14(
7682
+ "box",
7683
+ {
7684
+ width: "100%",
7685
+ style: {
7686
+ paddingLeft: 1,
7687
+ paddingRight: 1,
7688
+ paddingTop: 0,
7689
+ paddingBottom: 0,
7690
+ backgroundColor: t.status.warning
7691
+ },
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." })
7693
+ }
7694
+ ),
7695
+ renderView()
7696
+ ]
7402
7697
  }
7403
7698
  );
7404
7699
  }
7405
7700
 
7406
7701
  // src/index.tsx
7407
- import { jsx as jsx14 } from "@opentui/react/jsx-runtime";
7702
+ import { jsx as jsx15 } from "@opentui/react/jsx-runtime";
7408
7703
  var args = process.argv.slice(2);
7409
7704
  if (args.includes("--version") || args.includes("-v")) {
7410
7705
  const require2 = createRequire(import.meta.url);
@@ -7433,11 +7728,14 @@ Documentation & source:
7433
7728
  `);
7434
7729
  process.exit(0);
7435
7730
  }
7731
+ var isAppleTerminal = process.env.TERM_PROGRAM === "Apple_Terminal";
7732
+ var darwinMajor = process.platform === "darwin" ? parseInt(release(), 10) : Infinity;
7733
+ var lacksTrueColor = isAppleTerminal && darwinMajor < 25;
7436
7734
  async function main() {
7437
7735
  const renderer = await createCliRenderer({
7438
7736
  useMouse: true
7439
7737
  });
7440
7738
  const root = createRoot(renderer);
7441
- root.render(/* @__PURE__ */ jsx14(App, {}));
7739
+ root.render(/* @__PURE__ */ jsx15(App, { lacksTrueColor }));
7442
7740
  }
7443
7741
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawcontrol",
3
- "version": "0.2.0",
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",