@towles/tool 0.0.94 → 0.0.95
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/package.json +1 -1
- package/src/commands/agentboard2.ts +280 -0
- package/src/commands/agentboard.ts +0 -333
package/package.json
CHANGED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
import { colors } from "consola/utils";
|
|
7
|
+
import { BaseCommand } from "./base.js";
|
|
8
|
+
|
|
9
|
+
const PLUGIN_DIR = resolve(import.meta.dirname, "../../plugins/tt-agentboard2");
|
|
10
|
+
|
|
11
|
+
// Keybinding defaults
|
|
12
|
+
const DEFAULT_KEY = "a";
|
|
13
|
+
const TMUX_BINDINGS = { toggle: "t", focus: "s" } as const;
|
|
14
|
+
const RUN_SHELL_LINE = `run-shell '${PLUGIN_DIR}/agentboard2.tmux'`;
|
|
15
|
+
const MARKER = "# agentboard2";
|
|
16
|
+
|
|
17
|
+
function findTmuxConf(): string | null {
|
|
18
|
+
const candidates = [
|
|
19
|
+
resolve(process.env.HOME ?? "~", ".tmux.conf"),
|
|
20
|
+
resolve(process.env.HOME ?? "~", ".config/tmux/tmux.conf"),
|
|
21
|
+
];
|
|
22
|
+
for (const path of candidates) {
|
|
23
|
+
try {
|
|
24
|
+
const real = existsSync(path) ? path : null;
|
|
25
|
+
if (real) return real;
|
|
26
|
+
} catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
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 agentboard2 into tmux",
|
|
40
|
+
command: "<%= config.bin %> agentboard2 setup",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
description: "Uninstall from tmux",
|
|
44
|
+
command: "<%= config.bin %> agentboard2 uninstall",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
description: "Launch the server",
|
|
48
|
+
command: "<%= config.bin %> agentboard2 server",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
description: "Launch the TUI directly",
|
|
52
|
+
command: "<%= config.bin %> agentboard2 tui",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
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
|
+
};
|
|
63
|
+
|
|
64
|
+
async run(): Promise<void> {
|
|
65
|
+
const { args } = await this.parse(Agentboard2);
|
|
66
|
+
|
|
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
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
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
|
+
}
|
|
100
|
+
|
|
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 agentboard2 dependencies...");
|
|
105
|
+
execSync("pnpm install", { cwd: PLUGIN_DIR, stdio: "inherit" });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private setup(): void {
|
|
110
|
+
this.ensureDeps();
|
|
111
|
+
|
|
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
|
+
}
|
|
119
|
+
|
|
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
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if already installed
|
|
129
|
+
const content = readFileSync(editPath, "utf8");
|
|
130
|
+
if (content.includes("agentboard2.tmux")) {
|
|
131
|
+
consola.success("Already installed in tmux.conf");
|
|
132
|
+
this.reloadTmux();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
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`;
|
|
140
|
+
|
|
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
|
+
}
|
|
150
|
+
|
|
151
|
+
writeFileSync(editPath, newContent);
|
|
152
|
+
consola.success(`Added agentboard2 to ${editPath}`);
|
|
153
|
+
|
|
154
|
+
this.reloadTmux();
|
|
155
|
+
this.showKeys();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private uninstall(): void {
|
|
159
|
+
const confPath = findTmuxConf();
|
|
160
|
+
if (!confPath) {
|
|
161
|
+
consola.info("No tmux.conf found.");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let editPath = confPath;
|
|
166
|
+
try {
|
|
167
|
+
editPath = realpathSync(confPath);
|
|
168
|
+
} catch {
|
|
169
|
+
// keep confPath
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const content = readFileSync(editPath, "utf8");
|
|
173
|
+
if (!content.includes("agentboard2")) {
|
|
174
|
+
consola.info("agentboard2 not found in tmux.conf");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Remove the marker line and run-shell line
|
|
179
|
+
const newContent = content
|
|
180
|
+
.split("\n")
|
|
181
|
+
.filter((line) => !line.includes("agentboard2"))
|
|
182
|
+
.join("\n")
|
|
183
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
184
|
+
|
|
185
|
+
writeFileSync(editPath, newContent);
|
|
186
|
+
consola.success("Removed agentboard2 from tmux.conf");
|
|
187
|
+
this.reloadTmux();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Foreground command — blocks until server exits (Ctrl+C to stop)
|
|
191
|
+
private startServer(): void {
|
|
192
|
+
this.ensureDeps();
|
|
193
|
+
|
|
194
|
+
const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
|
|
195
|
+
consola.info("Starting agentboard2 server (foreground, Ctrl+C to stop)...");
|
|
196
|
+
|
|
197
|
+
execSync(`bun run ${serverEntry}`, {
|
|
198
|
+
stdio: "inherit",
|
|
199
|
+
cwd: PLUGIN_DIR,
|
|
200
|
+
env: {
|
|
201
|
+
...process.env,
|
|
202
|
+
AGENTBOARD2_DIR: PLUGIN_DIR,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Foreground command — blocks until TUI exits
|
|
208
|
+
private startTui(): void {
|
|
209
|
+
this.ensureDeps();
|
|
210
|
+
|
|
211
|
+
const tuiEntry = resolve(PLUGIN_DIR, "apps/tui/src/index.tsx");
|
|
212
|
+
|
|
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
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private showKeys(): void {
|
|
224
|
+
// Get tmux prefix and agentboard2 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 @agentboard2-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
|
|
242
|
+
}
|
|
243
|
+
|
|
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
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
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");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
import { Args, Flags } from "@oclif/core";
|
|
2
|
-
import { execSync, spawn } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { resolve, join } from "node:path";
|
|
5
|
-
import { networkInterfaces } from "node:os";
|
|
6
|
-
import consola from "consola";
|
|
7
|
-
import { colors } from "consola/utils";
|
|
8
|
-
import prompts from "prompts";
|
|
9
|
-
import { BaseCommand } from "./base.js";
|
|
10
|
-
|
|
11
|
-
function getLocalIp(): string {
|
|
12
|
-
const nets = networkInterfaces();
|
|
13
|
-
for (const ifaces of Object.values(nets)) {
|
|
14
|
-
if (!ifaces) continue;
|
|
15
|
-
for (const iface of ifaces) {
|
|
16
|
-
if (iface.family === "IPv4" && !iface.internal) {
|
|
17
|
-
return iface.address;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return "localhost";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export default class Agentboard extends BaseCommand {
|
|
25
|
-
static override aliases = ["ag"];
|
|
26
|
-
static override description = "Start AgentBoard — agentic workflow orchestration IDE";
|
|
27
|
-
|
|
28
|
-
static override examples = [
|
|
29
|
-
{
|
|
30
|
-
description: "Start AgentBoard on default port",
|
|
31
|
-
command: "<%= config.bin %> agentboard",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
description: "Start on a custom port",
|
|
35
|
-
command: "<%= config.bin %> ag --port 3000",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
description: "Start without opening browser",
|
|
39
|
-
command: "<%= config.bin %> ag --no-open",
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
description: "Attach to a running card tmux session",
|
|
43
|
-
command: "<%= config.bin %> ag attach 42",
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
description: "Selectively clear database (interactive)",
|
|
47
|
-
command: "<%= config.bin %> ag reset",
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
description: "Delete entire database without prompting",
|
|
51
|
-
command: "<%= config.bin %> ag reset --all",
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
static override flags = {
|
|
56
|
-
port: Flags.string({
|
|
57
|
-
char: "p",
|
|
58
|
-
description: "Port to serve on",
|
|
59
|
-
default: "4200",
|
|
60
|
-
}),
|
|
61
|
-
open: Flags.boolean({
|
|
62
|
-
description: "Open browser after starting",
|
|
63
|
-
default: true,
|
|
64
|
-
allowNo: true,
|
|
65
|
-
}),
|
|
66
|
-
"data-dir": Flags.string({
|
|
67
|
-
char: "d",
|
|
68
|
-
description: "Directory for AgentBoard data (SQLite DB, artifacts)",
|
|
69
|
-
env: "AGENTBOARD_DATA_DIR",
|
|
70
|
-
}),
|
|
71
|
-
lan: Flags.boolean({
|
|
72
|
-
description: "Listen on all interfaces (0.0.0.0) for LAN access. Default: localhost only.",
|
|
73
|
-
default: false,
|
|
74
|
-
}),
|
|
75
|
-
all: Flags.boolean({
|
|
76
|
-
description: "Reset entire database without prompting (for tt ag reset --all)",
|
|
77
|
-
default: false,
|
|
78
|
-
}),
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
static override args = {
|
|
82
|
-
subcommand: Args.string({
|
|
83
|
-
description: "Subcommand (attach, reset)",
|
|
84
|
-
required: false,
|
|
85
|
-
}),
|
|
86
|
-
cardId: Args.string({
|
|
87
|
-
description: "Card ID for attach subcommand",
|
|
88
|
-
required: false,
|
|
89
|
-
}),
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
async run(): Promise<void> {
|
|
93
|
-
const { args, flags } = await this.parse(Agentboard);
|
|
94
|
-
|
|
95
|
-
if (args.subcommand === "attach") {
|
|
96
|
-
if (!args.cardId) {
|
|
97
|
-
this.error("Card ID is required for attach subcommand");
|
|
98
|
-
}
|
|
99
|
-
execSync(`tmux attach-session -t card-${args.cardId}`, {
|
|
100
|
-
stdio: "inherit",
|
|
101
|
-
});
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (args.subcommand === "reset") {
|
|
106
|
-
const defaultDataDir = resolve(
|
|
107
|
-
process.env.XDG_CONFIG_HOME ?? resolve(process.env.HOME ?? "~", ".config"),
|
|
108
|
-
"towles-tool",
|
|
109
|
-
"agentboard",
|
|
110
|
-
);
|
|
111
|
-
const dataDir = flags["data-dir"] ? resolve(flags["data-dir"]) : defaultDataDir;
|
|
112
|
-
const dbPath = join(dataDir, "agentboard.db");
|
|
113
|
-
|
|
114
|
-
if (!existsSync(dbPath)) {
|
|
115
|
-
consola.info("No database found — nothing to reset.");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (flags.all) {
|
|
120
|
-
await this.resetEntireDatabase(dbPath);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
await this.selectiveClear(dbPath);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const agentboardDir = resolve(import.meta.dirname, "../../plugins/tt-agentboard");
|
|
129
|
-
const port = flags.port;
|
|
130
|
-
const defaultDataDir = resolve(
|
|
131
|
-
process.env.XDG_CONFIG_HOME ?? resolve(process.env.HOME ?? "~", ".config"),
|
|
132
|
-
"towles-tool",
|
|
133
|
-
"agentboard",
|
|
134
|
-
);
|
|
135
|
-
const dataDir = flags["data-dir"] ? resolve(flags["data-dir"]) : defaultDataDir;
|
|
136
|
-
const localIp = getLocalIp();
|
|
137
|
-
const dbPath = join(dataDir, "agentboard.db");
|
|
138
|
-
const isFirstRun = !existsSync(dbPath);
|
|
139
|
-
|
|
140
|
-
const lanMode = flags.lan;
|
|
141
|
-
const host = lanMode ? "0.0.0.0" : "127.0.0.1";
|
|
142
|
-
|
|
143
|
-
const lines = [`AgentBoard\n\n Local: http://localhost:${port}`];
|
|
144
|
-
if (lanMode) {
|
|
145
|
-
lines.push(` Network: http://${localIp}:${port}`);
|
|
146
|
-
} else {
|
|
147
|
-
lines.push(` Network: disabled (use --lan to enable)`);
|
|
148
|
-
}
|
|
149
|
-
lines.push(` Data: ${dataDir}`);
|
|
150
|
-
consola.box(lines.join("\n"));
|
|
151
|
-
|
|
152
|
-
if (isFirstRun) {
|
|
153
|
-
consola.info("First run detected — a new database will be created at startup.");
|
|
154
|
-
consola.info(
|
|
155
|
-
"Setup checklist:\n" +
|
|
156
|
-
" 1. Ensure tmux is installed (sudo apt install tmux / brew install tmux)\n" +
|
|
157
|
-
" 2. Set GITHUB_TOKEN for GitHub features (optional)\n" +
|
|
158
|
-
" 3. Open the board → Workspaces → Add a workspace slot\n" +
|
|
159
|
-
" 4. Create your first card and drag it to In Progress",
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const proc = spawn("pnpm", ["dev", "--port", port], {
|
|
164
|
-
cwd: agentboardDir,
|
|
165
|
-
stdio: "inherit",
|
|
166
|
-
env: {
|
|
167
|
-
...process.env,
|
|
168
|
-
NUXT_DEV_HOST: host,
|
|
169
|
-
AGENTBOARD_DATA_DIR: dataDir,
|
|
170
|
-
AGENTBOARD_LAN: lanMode ? "1" : "0",
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
if (flags.open) {
|
|
175
|
-
setTimeout(() => {
|
|
176
|
-
try {
|
|
177
|
-
execSync(`xdg-open http://localhost:${port}`, { stdio: "ignore" });
|
|
178
|
-
} catch {
|
|
179
|
-
consola.debug("Could not open browser automatically");
|
|
180
|
-
}
|
|
181
|
-
}, 2000);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
proc.on("exit", (code) => process.exit(code ?? 0));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private async resetEntireDatabase(dbPath: string): Promise<void> {
|
|
188
|
-
const walPath = `${dbPath}-wal`;
|
|
189
|
-
const shmPath = `${dbPath}-shm`;
|
|
190
|
-
consola.warn(`This will delete: ${dbPath}`);
|
|
191
|
-
const { unlinkSync } = await import("node:fs");
|
|
192
|
-
for (const f of [dbPath, walPath, shmPath]) {
|
|
193
|
-
if (existsSync(f)) {
|
|
194
|
-
unlinkSync(f);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
consola.success("Database reset. Start AgentBoard to create a fresh DB.");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private async selectiveClear(dbPath: string): Promise<void> {
|
|
201
|
-
const { createRequire } = await import("node:module");
|
|
202
|
-
const require = createRequire(import.meta.url);
|
|
203
|
-
const Database = require("better-sqlite3");
|
|
204
|
-
const sqlite = new Database(dbPath) as {
|
|
205
|
-
pragma(stmt: string): void;
|
|
206
|
-
prepare(sql: string): {
|
|
207
|
-
get(): unknown;
|
|
208
|
-
run(): { changes: number };
|
|
209
|
-
};
|
|
210
|
-
close(): void;
|
|
211
|
-
};
|
|
212
|
-
sqlite.pragma("foreign_keys = ON");
|
|
213
|
-
|
|
214
|
-
const counts = {
|
|
215
|
-
doneCards: sqlite.prepare("SELECT COUNT(*) as c FROM cards WHERE column = 'done'").get() as {
|
|
216
|
-
c: number;
|
|
217
|
-
},
|
|
218
|
-
failedCards: sqlite
|
|
219
|
-
.prepare("SELECT COUNT(*) as c FROM cards WHERE status = 'failed'")
|
|
220
|
-
.get() as { c: number },
|
|
221
|
-
allCards: sqlite.prepare("SELECT COUNT(*) as c FROM cards").get() as { c: number },
|
|
222
|
-
workflowRuns: sqlite.prepare("SELECT COUNT(*) as c FROM workflow_runs").get() as {
|
|
223
|
-
c: number;
|
|
224
|
-
},
|
|
225
|
-
cardEvents: sqlite.prepare("SELECT COUNT(*) as c FROM card_events").get() as { c: number },
|
|
226
|
-
agentLogs: sqlite.prepare("SELECT COUNT(*) as c FROM agent_logs").get() as { c: number },
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const choices = [
|
|
230
|
-
{
|
|
231
|
-
title: `Completed cards ${colors.dim(`(${counts.doneCards.c} cards in "done" column + events/runs)`)}`,
|
|
232
|
-
value: "done_cards",
|
|
233
|
-
disabled: counts.doneCards.c === 0,
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
title: `Failed cards ${colors.dim(`(${counts.failedCards.c} cards with "failed" status + events/runs)`)}`,
|
|
237
|
-
value: "failed_cards",
|
|
238
|
-
disabled: counts.failedCards.c === 0,
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
title: `Execution history ${colors.dim(`(${counts.workflowRuns.c} workflow runs, step runs, ${counts.agentLogs.c} agent logs)`)}`,
|
|
242
|
-
value: "execution_history",
|
|
243
|
-
disabled: counts.workflowRuns.c === 0,
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
title: `Event logs ${colors.dim(`(${counts.cardEvents.c} card events)`)}`,
|
|
247
|
-
value: "event_logs",
|
|
248
|
-
disabled: counts.cardEvents.c === 0,
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
title: `All cards ${colors.dim(`(${counts.allCards.c} cards — keeps repos, boards, slots)`)}`,
|
|
252
|
-
value: "all_cards",
|
|
253
|
-
disabled: counts.allCards.c === 0,
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
title: colors.red(`Everything (delete entire database)`),
|
|
257
|
-
value: "everything",
|
|
258
|
-
},
|
|
259
|
-
];
|
|
260
|
-
|
|
261
|
-
const result = await prompts(
|
|
262
|
-
{
|
|
263
|
-
name: "selected",
|
|
264
|
-
message: "What would you like to clear?",
|
|
265
|
-
type: "multiselect",
|
|
266
|
-
choices,
|
|
267
|
-
instructions: false,
|
|
268
|
-
hint: "- Space to select, Enter to confirm",
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
onCancel: () => {
|
|
272
|
-
consola.info(colors.dim("Canceled"));
|
|
273
|
-
process.exit(0);
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
const selected: string[] = result.selected;
|
|
279
|
-
if (!selected || selected.length === 0) {
|
|
280
|
-
consola.info("Nothing selected.");
|
|
281
|
-
sqlite.close();
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (selected.includes("everything")) {
|
|
286
|
-
sqlite.close();
|
|
287
|
-
await this.resetEntireDatabase(dbPath);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
let totalDeleted = 0;
|
|
292
|
-
|
|
293
|
-
if (selected.includes("done_cards")) {
|
|
294
|
-
// Cascade deletes handle events, dependencies, workflow_runs, step_runs, agent_logs
|
|
295
|
-
const deleted = sqlite.prepare("DELETE FROM cards WHERE column = 'done'").run();
|
|
296
|
-
consola.success(`Cleared ${deleted.changes} completed card(s)`);
|
|
297
|
-
totalDeleted += deleted.changes;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (selected.includes("failed_cards")) {
|
|
301
|
-
const deleted = sqlite.prepare("DELETE FROM cards WHERE status = 'failed'").run();
|
|
302
|
-
consola.success(`Cleared ${deleted.changes} failed card(s)`);
|
|
303
|
-
totalDeleted += deleted.changes;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (selected.includes("execution_history")) {
|
|
307
|
-
// agent_logs and step_runs cascade from workflow_runs
|
|
308
|
-
const deleted = sqlite.prepare("DELETE FROM workflow_runs").run();
|
|
309
|
-
consola.success(`Cleared ${deleted.changes} workflow run(s) and associated logs`);
|
|
310
|
-
totalDeleted += deleted.changes;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (selected.includes("event_logs")) {
|
|
314
|
-
const deleted = sqlite.prepare("DELETE FROM card_events").run();
|
|
315
|
-
consola.success(`Cleared ${deleted.changes} event log(s)`);
|
|
316
|
-
totalDeleted += deleted.changes;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (selected.includes("all_cards")) {
|
|
320
|
-
const deleted = sqlite.prepare("DELETE FROM cards").run();
|
|
321
|
-
consola.success(`Cleared ${deleted.changes} card(s)`);
|
|
322
|
-
totalDeleted += deleted.changes;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
sqlite.close();
|
|
326
|
-
|
|
327
|
-
if (totalDeleted === 0) {
|
|
328
|
-
consola.info("Nothing to clear.");
|
|
329
|
-
} else {
|
|
330
|
-
consola.success("Done.");
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|