@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.
- package/README.md +2 -2
- package/bin/run.ts +4 -3
- package/package.json +10 -37
- package/src/cli.ts +19 -0
- package/src/commands/agentboard.ts +386 -214
- package/src/commands/auto-claude/index.ts +74 -91
- package/src/commands/auto-claude/list.ts +33 -43
- package/src/commands/auto-claude/retry.test.ts +10 -6
- package/src/commands/auto-claude/retry.ts +26 -39
- package/src/commands/auto-claude/status.ts +10 -17
- package/src/commands/config.test.ts +4 -10
- package/src/commands/config.ts +14 -28
- package/src/commands/doctor.ts +156 -178
- package/src/commands/gh/branch-clean.ts +28 -43
- package/src/commands/gh/branch.ts +22 -37
- package/src/commands/gh/index.ts +10 -0
- package/src/commands/gh/pr.ts +82 -100
- package/src/commands/graph/index.ts +59 -70
- package/src/commands/install.ts +91 -115
- package/src/commands/journal/daily-notes.ts +16 -24
- package/src/commands/journal/index.ts +10 -0
- package/src/commands/journal/meeting.ts +16 -34
- package/src/commands/journal/note.ts +16 -34
- package/src/commands/shared.ts +21 -0
- package/src/lib/auto-claude/templates.test.ts +16 -11
- package/src/lib/graph/parser.test.ts +11 -10
- package/src/utils/git/gh-cli-wrapper.test.ts +6 -5
- package/src/commands/base.ts +0 -32
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
117
|
+
let editPath = confPath;
|
|
118
|
+
try {
|
|
119
|
+
editPath = realpathSync(confPath);
|
|
120
|
+
} catch {
|
|
121
|
+
// keep confPath
|
|
122
|
+
}
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const insertLines = `\n${MARKER}\n${RUN_SHELL_LINE}\n`;
|
|
147
|
+
reloadTmux();
|
|
148
|
+
showKeys();
|
|
149
|
+
}
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
152
|
-
|
|
158
|
+
let editPath = confPath;
|
|
159
|
+
try {
|
|
160
|
+
editPath = realpathSync(confPath);
|
|
161
|
+
} catch {
|
|
162
|
+
// keep confPath
|
|
163
|
+
}
|
|
153
164
|
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// keep confPath
|
|
170
|
-
}
|
|
177
|
+
writeFileSync(editPath, newContent);
|
|
178
|
+
consola.success("Removed agentboard from tmux.conf");
|
|
179
|
+
reloadTmux();
|
|
180
|
+
}
|
|
171
181
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
consola.info("agentboard not found in tmux.conf");
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
182
|
+
function startServer(): void {
|
|
183
|
+
ensureDeps();
|
|
177
184
|
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
241
|
+
function tmuxContext(): string {
|
|
242
|
+
return tmuxDisplay("#{client_tty}|#{session_name}|#{window_id}");
|
|
243
|
+
}
|
|
210
244
|
|
|
211
|
-
|
|
245
|
+
function resetTmuxKeys(): void {
|
|
246
|
+
spawnSync("tmux", ["switch-client", "-T", "root"], { stdio: "pipe" });
|
|
247
|
+
}
|
|
212
248
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
`
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
});
|