@towles/tool 0.0.96 → 0.0.104

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.
@@ -1,22 +1,24 @@
1
- import { Args } from "@oclif/core";
2
- import { execSync } from "node:child_process";
1
+ import { defineCommand } from "citty";
2
+ import { execSync, spawn, spawnSync } from "node:child_process";
3
3
  import { readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
  import consola from "consola";
6
6
  import { colors } from "consola/utils";
7
- import { BaseCommand } from "./base.js";
7
+ import { debugArg } from "./shared.js";
8
+
9
+ const SERVER_HOST = "127.0.0.1";
10
+ const SERVER_PORT = 4201;
8
11
 
9
12
  const PLUGIN_DIR = resolve(import.meta.dirname, "../../plugins/tt-agentboard");
10
13
 
11
14
  // Keybinding defaults
12
15
  const DEFAULT_KEY = "a";
13
16
  const TMUX_BINDINGS = { toggle: "t", focus: "s" } as const;
14
- const RUN_SHELL_LINE = `run-shell '${PLUGIN_DIR}/agentboard.tmux'`;
17
+ const RUN_SHELL_LINE = "run-shell 'tt agentboard init'";
15
18
  const MARKER = "# agentboard";
16
19
 
17
20
  function findTmuxConf(): string | null {
18
21
  const candidates = [
19
- resolve(process.env.HOME ?? "~", ".tmux.conf"),
20
22
  resolve(process.env.HOME ?? "~", ".config/tmux/tmux.conf"),
21
23
  ];
22
24
  for (const path of candidates) {
@@ -30,251 +32,421 @@ function findTmuxConf(): string | null {
30
32
  return null;
31
33
  }
32
34
 
33
- export default class Agentboard2 extends BaseCommand {
34
- static override aliases = ["ag2"];
35
- static override description = "AgentBoard2 — opensessions-style tmux TUI sidebar";
36
-
37
- static override examples = [
38
- {
39
- description: "Install agentboard into tmux",
40
- command: "<%= config.bin %> agentboard setup",
41
- },
42
- {
43
- description: "Uninstall from tmux",
44
- command: "<%= config.bin %> agentboard uninstall",
45
- },
46
- {
47
- description: "Launch the server",
48
- command: "<%= config.bin %> agentboard server",
49
- },
50
- {
51
- description: "Launch the TUI directly",
52
- command: "<%= config.bin %> agentboard tui",
53
- },
54
- ];
35
+ function ensureDeps(): void {
36
+ try {
37
+ execSync("bun --version", { stdio: "pipe" });
38
+ } catch {
39
+ consola.error("bun is required but not found. Install: https://bun.sh");
40
+ process.exit(1);
41
+ }
55
42
 
56
- static override args = {
57
- subcommand: Args.string({
58
- description: "Subcommand: setup, uninstall, server, tui, keys",
59
- required: false,
60
- options: ["setup", "uninstall", "server", "tui", "start", "keys"],
61
- }),
62
- };
43
+ const runtimeNodeModules = resolve(PLUGIN_DIR, "packages/runtime/node_modules");
44
+ if (!existsSync(runtimeNodeModules)) {
45
+ consola.info("Installing agentboard dependencies...");
46
+ execSync("bun install", { cwd: PLUGIN_DIR, stdio: "inherit" });
47
+ }
48
+ }
63
49
 
64
- async run(): Promise<void> {
65
- const { args } = await this.parse(Agentboard2);
50
+ function reloadTmux(): void {
51
+ try {
52
+ execSync(
53
+ "tmux source-file ~/.config/tmux/tmux.conf 2>/dev/null || tmux source-file ~/.tmux.conf 2>/dev/null",
54
+ {
55
+ stdio: "pipe",
56
+ },
57
+ );
58
+ consola.success("tmux config reloaded");
59
+ } catch {
60
+ consola.info("Reload tmux manually: tmux source-file ~/.config/tmux/tmux.conf");
61
+ }
62
+ }
66
63
 
67
- switch (args.subcommand) {
68
- case "setup":
69
- this.setup();
70
- break;
71
- case "uninstall":
72
- this.uninstall();
73
- break;
74
- case "server":
75
- this.startServer();
76
- break;
77
- case "tui":
78
- this.startTui();
79
- break;
80
- case "start":
81
- // For backwards compat, start = tui
82
- this.startTui();
83
- break;
84
- case "keys":
85
- this.showKeys();
86
- break;
87
- default:
88
- this.showKeys();
89
- break;
90
- }
64
+ function showKeys(): void {
65
+ let prefix = "C-a";
66
+ let key = DEFAULT_KEY;
67
+ try {
68
+ prefix = execSync("tmux show-option -gv prefix", {
69
+ encoding: "utf8",
70
+ stdio: ["pipe", "pipe", "pipe"],
71
+ }).trim();
72
+ const abKey = execSync(
73
+ `tmux show-option -gv @agentboard-key 2>/dev/null || echo ${DEFAULT_KEY}`,
74
+ {
75
+ encoding: "utf8",
76
+ stdio: ["pipe", "pipe", "pipe"],
77
+ },
78
+ ).trim();
79
+ if (abKey) key = abKey;
80
+ } catch {
81
+ // use defaults
91
82
  }
92
83
 
93
- private ensureDeps(): void {
94
- // Check bun is installed
95
- try {
96
- execSync("bun --version", { stdio: "pipe" });
97
- } catch {
98
- this.error("bun is required but not found. Install: https://bun.sh");
99
- }
84
+ const { toggle, focus } = TMUX_BINDINGS;
85
+ consola.box(
86
+ [
87
+ `${colors.bold("AgentBoard Keybindings")}\n`,
88
+ `${colors.cyan(`tmux (prefix = ${prefix}, C = Ctrl):`)}`,
89
+ ` ${prefix} ${key} ${toggle} toggle sidebar`,
90
+ ` ${prefix} ${key} ${focus} focus sidebar`,
91
+ ` ${prefix} ${key} 1-9 jump to session\n`,
92
+ `${colors.cyan("In sidebar:")}`,
93
+ ` Tab cycle sessions`,
94
+ ` j / ↓ move down`,
95
+ ` k / ↑ move up`,
96
+ ` Enter / l switch to selected session`,
97
+ ` 1-9 jump to session`,
98
+ ` d hide session`,
99
+ ` x kill session`,
100
+ ` r refresh`,
101
+ ` ? help`,
102
+ ` q quit`,
103
+ ].join("\n"),
104
+ );
105
+ }
100
106
 
101
- // Install deps if needed for runtime package
102
- const runtimeNodeModules = resolve(PLUGIN_DIR, "packages/runtime/node_modules");
103
- if (!existsSync(runtimeNodeModules)) {
104
- consola.info("Installing agentboard dependencies...");
105
- execSync("pnpm install", { cwd: PLUGIN_DIR, stdio: "inherit" });
106
- }
107
+ function setup(): void {
108
+ ensureDeps();
109
+
110
+ const confPath = findTmuxConf();
111
+ if (!confPath) {
112
+ consola.warn("No tmux.conf found. Add this line manually:");
113
+ consola.info(colors.cyan(` ${RUN_SHELL_LINE}`));
114
+ return;
107
115
  }
108
116
 
109
- private setup(): void {
110
- this.ensureDeps();
117
+ let editPath = confPath;
118
+ try {
119
+ editPath = realpathSync(confPath);
120
+ } catch {
121
+ // keep confPath
122
+ }
111
123
 
112
- // Find tmux.conf
113
- const confPath = findTmuxConf();
114
- if (!confPath) {
115
- consola.warn("No tmux.conf found. Add this line manually:");
116
- consola.info(colors.cyan(` ${RUN_SHELL_LINE}`));
117
- return;
118
- }
124
+ const content = readFileSync(editPath, "utf8");
125
+ if (content.includes(MARKER)) {
126
+ consola.success("Already installed in tmux.conf");
127
+ reloadTmux();
128
+ return;
129
+ }
119
130
 
120
- // If it's a symlink, resolve to the real file for editing
121
- let editPath = confPath;
122
- try {
123
- editPath = realpathSync(confPath);
124
- } catch {
125
- // keep confPath
126
- }
131
+ const tpmLine = "run '~/.config/tmux/plugins/tpm/tpm'";
132
+ const altTpmLine = "run-shell '~/.tmux/plugins/tpm/tpm'";
133
+ const insertLines = `\n${MARKER}\n${RUN_SHELL_LINE}\n`;
134
+
135
+ let newContent: string;
136
+ if (content.includes(tpmLine)) {
137
+ newContent = content.replace(tpmLine, `${insertLines}\n${tpmLine}`);
138
+ } else if (content.includes(altTpmLine)) {
139
+ newContent = content.replace(altTpmLine, `${insertLines}\n${altTpmLine}`);
140
+ } else {
141
+ newContent = content + insertLines;
142
+ }
127
143
 
128
- // Check if already installed
129
- const content = readFileSync(editPath, "utf8");
130
- if (content.includes("agentboard.tmux")) {
131
- consola.success("Already installed in tmux.conf");
132
- this.reloadTmux();
133
- return;
134
- }
144
+ writeFileSync(editPath, newContent);
145
+ consola.success(`Added agentboard to ${editPath}`);
135
146
 
136
- // Add run-shell line before TPM init
137
- const tpmLine = "run '~/.config/tmux/plugins/tpm/tpm'";
138
- const altTpmLine = "run-shell '~/.tmux/plugins/tpm/tpm'";
139
- const insertLines = `\n${MARKER}\n${RUN_SHELL_LINE}\n`;
147
+ reloadTmux();
148
+ showKeys();
149
+ }
140
150
 
141
- let newContent: string;
142
- if (content.includes(tpmLine)) {
143
- newContent = content.replace(tpmLine, `${insertLines}\n${tpmLine}`);
144
- } else if (content.includes(altTpmLine)) {
145
- newContent = content.replace(altTpmLine, `${insertLines}\n${altTpmLine}`);
146
- } else {
147
- // No TPM found, append to end
148
- newContent = content + insertLines;
149
- }
151
+ function uninstall(): void {
152
+ const confPath = findTmuxConf();
153
+ if (!confPath) {
154
+ consola.info("No tmux.conf found.");
155
+ return;
156
+ }
150
157
 
151
- writeFileSync(editPath, newContent);
152
- consola.success(`Added agentboard to ${editPath}`);
158
+ let editPath = confPath;
159
+ try {
160
+ editPath = realpathSync(confPath);
161
+ } catch {
162
+ // keep confPath
163
+ }
153
164
 
154
- this.reloadTmux();
155
- this.showKeys();
165
+ const content = readFileSync(editPath, "utf8");
166
+ if (!content.includes("agentboard")) {
167
+ consola.info("agentboard not found in tmux.conf");
168
+ return;
156
169
  }
157
170
 
158
- private uninstall(): void {
159
- const confPath = findTmuxConf();
160
- if (!confPath) {
161
- consola.info("No tmux.conf found.");
162
- return;
163
- }
171
+ const newContent = content
172
+ .split("\n")
173
+ .filter((line) => !line.includes("agentboard"))
174
+ .join("\n")
175
+ .replace(/\n{3,}/g, "\n\n");
164
176
 
165
- let editPath = confPath;
166
- try {
167
- editPath = realpathSync(confPath);
168
- } catch {
169
- // keep confPath
170
- }
177
+ writeFileSync(editPath, newContent);
178
+ consola.success("Removed agentboard from tmux.conf");
179
+ reloadTmux();
180
+ }
171
181
 
172
- const content = readFileSync(editPath, "utf8");
173
- if (!content.includes("agentboard")) {
174
- consola.info("agentboard not found in tmux.conf");
175
- return;
176
- }
182
+ function startServer(): void {
183
+ ensureDeps();
177
184
 
178
- // Remove the marker line and run-shell line
179
- const newContent = content
180
- .split("\n")
181
- .filter((line) => !line.includes("agentboard"))
182
- .join("\n")
183
- .replace(/\n{3,}/g, "\n\n");
185
+ const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
186
+ consola.info("Starting agentboard server (foreground, Ctrl+C to stop)...");
184
187
 
185
- writeFileSync(editPath, newContent);
186
- consola.success("Removed agentboard from tmux.conf");
187
- this.reloadTmux();
188
- }
188
+ execSync(`bun run ${serverEntry}`, {
189
+ stdio: "inherit",
190
+ cwd: PLUGIN_DIR,
191
+ env: {
192
+ ...process.env,
193
+ AGENTBOARD_DIR: PLUGIN_DIR,
194
+ },
195
+ });
196
+ }
189
197
 
190
- // Foreground command — blocks until server exits (Ctrl+C to stop)
191
- private startServer(): void {
192
- this.ensureDeps();
198
+ async function serverAlive(): Promise<boolean> {
199
+ try {
200
+ const res = await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/`, {
201
+ signal: AbortSignal.timeout(500),
202
+ });
203
+ return res.ok;
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
193
208
 
194
- const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
195
- consola.info("Starting agentboard server (foreground, Ctrl+C to stop)...");
209
+ async function ensureServerUp(): Promise<boolean> {
210
+ if (await serverAlive()) return true;
211
+
212
+ const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
213
+ consola.info("Starting agentboard server...");
214
+ const child = spawn("bun", ["run", serverEntry], {
215
+ stdio: "ignore",
216
+ cwd: PLUGIN_DIR,
217
+ detached: true,
218
+ env: { ...process.env, AGENTBOARD_DIR: PLUGIN_DIR },
219
+ });
220
+ child.unref();
221
+
222
+ for (let i = 0; i < 30; i++) {
223
+ await new Promise((r) => setTimeout(r, 100));
224
+ if (await serverAlive()) return true;
225
+ }
226
+ return false;
227
+ }
196
228
 
197
- execSync(`bun run ${serverEntry}`, {
198
- stdio: "inherit",
199
- cwd: PLUGIN_DIR,
200
- env: {
201
- ...process.env,
202
- AGENTBOARD2_DIR: PLUGIN_DIR,
203
- },
229
+ function tmuxDisplay(fmt: string): string {
230
+ try {
231
+ const r = spawnSync("tmux", ["display-message", "-p", fmt], {
232
+ encoding: "utf8",
233
+ stdio: ["pipe", "pipe", "pipe"],
204
234
  });
235
+ return (r.stdout ?? "").trim();
236
+ } catch {
237
+ return "";
205
238
  }
239
+ }
206
240
 
207
- // Foreground command — blocks until TUI exits
208
- private startTui(): void {
209
- this.ensureDeps();
241
+ function tmuxContext(): string {
242
+ return tmuxDisplay("#{client_tty}|#{session_name}|#{window_id}");
243
+ }
210
244
 
211
- const tuiEntry = resolve(PLUGIN_DIR, "apps/tui/src/index.tsx");
245
+ function resetTmuxKeys(): void {
246
+ spawnSync("tmux", ["switch-client", "-T", "root"], { stdio: "pipe" });
247
+ }
212
248
 
213
- execSync(`bun run ${tuiEntry}`, {
214
- stdio: "inherit",
215
- cwd: resolve(PLUGIN_DIR, "apps/tui"),
216
- env: {
217
- ...process.env,
218
- AGENTBOARD2_DIR: PLUGIN_DIR,
219
- },
249
+ function findSidebarPane(windowId: string): string | null {
250
+ try {
251
+ const r = spawnSync("tmux", ["list-panes", "-t", windowId, "-F", "#{pane_id} #{pane_title}"], {
252
+ encoding: "utf8",
253
+ stdio: ["pipe", "pipe", "pipe"],
220
254
  });
255
+ for (const line of (r.stdout ?? "").trim().split("\n")) {
256
+ const [paneId, title] = line.split(" ", 2);
257
+ if (title === "agentboard-sidebar" && paneId) return paneId;
258
+ }
259
+ } catch {}
260
+ return null;
261
+ }
262
+
263
+ function tmux(...args: string[]): void {
264
+ spawnSync("tmux", args, { stdio: "pipe" });
265
+ }
266
+
267
+ function init(): void {
268
+ const port = process.env.TT_AGENTBOARD_PORT ?? "4201";
269
+ const host = process.env.TT_AGENTBOARD_HOST ?? "127.0.0.1";
270
+
271
+ // Read tmux options with defaults
272
+ const keyResult = spawnSync("tmux", ["show-option", "-gqv", "@agentboard-key"], {
273
+ encoding: "utf8",
274
+ stdio: ["pipe", "pipe", "pipe"],
275
+ });
276
+ const key = (keyResult.stdout ?? "").trim() || DEFAULT_KEY;
277
+
278
+ // Export to tmux environment
279
+ tmux("set-environment", "-g", "TT_AGENTBOARD_PORT", port);
280
+ tmux("set-environment", "-g", "TT_AGENTBOARD_HOST", host);
281
+
282
+ // Bind keybindings via command table "agentboard"
283
+ tmux("bind-key", "-T", "prefix", key, "switch-client", "-T", "agentboard");
284
+ tmux("bind-key", "-T", "agentboard", TMUX_BINDINGS.toggle, "run-shell", "tt agentboard run --toggle");
285
+ tmux("bind-key", "-T", "agentboard", TMUX_BINDINGS.focus, "run-shell", "tt agentboard run --focus");
286
+
287
+ // Number keys 1-9 switch to session by index
288
+ for (let i = 1; i <= 9; i++) {
289
+ tmux(
290
+ "bind-key", "-T", "agentboard", String(i), "run-shell",
291
+ `curl -s -X POST 'http://${host}:${port}/switch-index?index=${i}' -d "$(tmux display-message -p '#{q:client_tty}|#{q:session_name}|#{q:window_id}')" >/dev/null 2>&1 || true`,
292
+ );
221
293
  }
222
294
 
223
- private showKeys(): void {
224
- // Get tmux prefix and agentboard key from tmux
225
- let prefix = "C-a";
226
- let key = DEFAULT_KEY;
227
- try {
228
- prefix = execSync("tmux show-option -gv prefix", {
229
- encoding: "utf8",
230
- stdio: ["pipe", "pipe", "pipe"],
231
- }).trim();
232
- const ab2Key = execSync(
233
- `tmux show-option -gv @agentboard-key 2>/dev/null || echo ${DEFAULT_KEY}`,
234
- {
235
- encoding: "utf8",
236
- stdio: ["pipe", "pipe", "pipe"],
237
- },
238
- ).trim();
239
- if (ab2Key) key = ab2Key;
240
- } catch {
241
- // use defaults
295
+ // Hooks (fallback for when server isn't running yet)
296
+ const hookPost = (path: string, body?: string) => {
297
+ const bodyArg = body ? ` -d \\"${body}\\"` : "";
298
+ return `run-shell -b "curl -s -X POST http://${host}:${port}${path}${bodyArg} >/dev/null 2>&1 || true"`;
299
+ };
300
+ const focusBody = "#{q:client_tty}|#{q:session_name}|#{q:window_id}";
301
+ const resizeBody = "#{q:pane_id}|#{q:session_name}|#{q:window_id}|#{q:pane_width}|#{q:window_width}";
302
+
303
+ tmux("set-hook", "-g", "client-session-changed", hookPost("/focus", focusBody));
304
+ tmux("set-hook", "-g", "after-select-window", hookPost("/ensure-sidebar", focusBody));
305
+ tmux("set-hook", "-g", "after-resize-pane", hookPost("/resize-sidebars", resizeBody));
306
+ }
307
+
308
+ async function runToggle(): Promise<void> {
309
+ if (!(await ensureServerUp())) process.exit(0);
310
+ const ctx = tmuxContext();
311
+ await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, { method: "POST", body: ctx }).catch(() => {});
312
+ resetTmuxKeys();
313
+ }
314
+
315
+ async function runFocus(): Promise<void> {
316
+ const windowId = tmuxDisplay("#{window_id}");
317
+ if (!windowId) process.exit(0);
318
+
319
+ // If sidebar already exists, just focus it
320
+ const existing = findSidebarPane(windowId);
321
+ if (existing) {
322
+ spawnSync("tmux", ["select-pane", "-t", existing], { stdio: "pipe" });
323
+ resetTmuxKeys();
324
+ return;
325
+ }
326
+
327
+ // Otherwise, ensure server + toggle sidebar on
328
+ if (!(await ensureServerUp())) process.exit(0);
329
+ const ctx = tmuxContext();
330
+ await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, { method: "POST", body: ctx }).catch(() => {});
331
+
332
+ // Wait for sidebar pane to appear
333
+ for (let i = 0; i < 20; i++) {
334
+ const paneId = findSidebarPane(windowId);
335
+ if (paneId) {
336
+ spawnSync("tmux", ["select-pane", "-t", paneId], { stdio: "pipe" });
337
+ resetTmuxKeys();
338
+ return;
242
339
  }
340
+ await new Promise((r) => setTimeout(r, 50));
341
+ }
342
+ resetTmuxKeys();
343
+ }
243
344
 
244
- const { toggle, focus } = TMUX_BINDINGS;
245
- consola.box(
246
- [
247
- `${colors.bold("AgentBoard2 Keybindings")}\n`,
248
- `${colors.cyan(`tmux (prefix = ${prefix}, C = Ctrl):`)}`,
249
- ` ${prefix} ${key} ${toggle} toggle sidebar`,
250
- ` ${prefix} ${key} ${focus} focus sidebar`,
251
- ` ${prefix} ${key} 1-9 jump to session\n`,
252
- `${colors.cyan("In sidebar:")}`,
253
- ` Tab cycle sessions`,
254
- ` j / ↓ move down`,
255
- ` k / ↑ move up`,
256
- ` Enter / l switch to selected session`,
257
- ` 1-9 jump to session`,
258
- ` d hide session`,
259
- ` x kill session`,
260
- ` t theme picker`,
261
- ` r refresh`,
262
- ` q quit`,
263
- ].join("\n"),
264
- );
345
+ async function restart(): Promise<void> {
346
+ ensureDeps();
347
+
348
+ // 1. Kill stash sessions left over from hidden sidebars
349
+ try {
350
+ const result = spawnSync("tmux", ["list-sessions", "-F", "#{session_name}"], {
351
+ encoding: "utf8",
352
+ stdio: ["pipe", "pipe", "pipe"],
353
+ });
354
+ const sessions = (result.stdout ?? "").trim().split("\n").filter(Boolean);
355
+ for (const name of sessions) {
356
+ if (name.startsWith("_ab_stash")) {
357
+ spawnSync("tmux", ["kill-session", "-t", name], { stdio: "pipe" });
358
+ consola.info(`Killed stash session: ${name}`);
359
+ }
360
+ }
361
+ } catch {
362
+ // no tmux or no sessions
265
363
  }
266
364
 
267
- private reloadTmux(): void {
268
- try {
269
- execSync(
270
- "tmux source-file ~/.config/tmux/tmux.conf 2>/dev/null || tmux source-file ~/.tmux.conf 2>/dev/null",
271
- {
272
- stdio: "pipe",
273
- },
274
- );
275
- consola.success("tmux config reloaded");
276
- } catch {
277
- consola.info("Reload tmux manually: tmux source-file ~/.config/tmux/tmux.conf");
365
+ // 2. Ensure server is running
366
+ if (!(await ensureServerUp())) {
367
+ consola.error("Failed to start agentboard server");
368
+ process.exit(1);
369
+ }
370
+ consola.success("Server is running");
371
+
372
+ // 3. Toggle sidebar on (the server spawns sidebars in all active windows)
373
+ try {
374
+ const res = await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, {
375
+ method: "POST",
376
+ body: "",
377
+ signal: AbortSignal.timeout(2000),
378
+ });
379
+ if (res.ok) {
380
+ consola.success("Sidebar toggled on for all sessions");
381
+ } else {
382
+ consola.warn(`Toggle returned: ${res.status}`);
278
383
  }
384
+ } catch (err) {
385
+ consola.error("Failed to toggle sidebar:", err);
279
386
  }
280
387
  }
388
+
389
+ function startTui(): void {
390
+ ensureDeps();
391
+
392
+ const tuiEntry = resolve(PLUGIN_DIR, "apps/tui/src/index.tsx");
393
+
394
+ execSync(`bun run ${tuiEntry}`, {
395
+ stdio: "inherit",
396
+ cwd: resolve(PLUGIN_DIR, "apps/tui"),
397
+ env: {
398
+ ...process.env,
399
+ AGENTBOARD_DIR: PLUGIN_DIR,
400
+ },
401
+ });
402
+ }
403
+
404
+ export default defineCommand({
405
+ meta: { name: "agentboard", description: "AgentBoard — tmux TUI sidebar" },
406
+ args: {
407
+ debug: debugArg,
408
+ subcommand: {
409
+ type: "positional",
410
+ required: false,
411
+ description: "Subcommand: setup, uninstall, server, tui, start, restart, run, keys",
412
+ },
413
+ toggle: { type: "boolean", description: "Toggle sidebar (used with 'run')" },
414
+ focus: { type: "boolean", description: "Focus sidebar (used with 'run')" },
415
+ },
416
+ async run({ args }) {
417
+ switch (args.subcommand) {
418
+ case "setup":
419
+ setup();
420
+ break;
421
+ case "uninstall":
422
+ uninstall();
423
+ break;
424
+ case "server":
425
+ startServer();
426
+ break;
427
+ case "tui":
428
+ startTui();
429
+ break;
430
+ case "start":
431
+ startTui();
432
+ break;
433
+ case "restart":
434
+ await restart();
435
+ break;
436
+ case "init":
437
+ init();
438
+ break;
439
+ case "run":
440
+ if (args.toggle) await runToggle();
441
+ else if (args.focus) await runFocus();
442
+ else consola.error("Usage: tt agentboard run --toggle | --focus");
443
+ break;
444
+ case "keys":
445
+ showKeys();
446
+ break;
447
+ default:
448
+ showKeys();
449
+ break;
450
+ }
451
+ },
452
+ });