@towles/tool 0.0.70 → 0.0.72

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.70",
3
+ "version": "0.0.72",
4
4
  "description": "One off quality of life scripts that I use on a daily basis.",
5
5
  "homepage": "https://github.com/ChrisTowles/towles-tool#readme",
6
6
  "bugs": {
@@ -4,6 +4,8 @@ import { existsSync } from "node:fs";
4
4
  import { resolve, join } from "node:path";
5
5
  import { networkInterfaces } from "node:os";
6
6
  import consola from "consola";
7
+ import { colors } from "consola/utils";
8
+ import prompts from "prompts";
7
9
  import { BaseCommand } from "./base.js";
8
10
 
9
11
  function getLocalIp(): string {
@@ -40,6 +42,14 @@ export default class Agentboard extends BaseCommand {
40
42
  description: "Attach to a running card tmux session",
41
43
  command: "<%= config.bin %> ag attach 42",
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
+ },
43
53
  ];
44
54
 
45
55
  static override flags = {
@@ -62,6 +72,10 @@ export default class Agentboard extends BaseCommand {
62
72
  description: "Listen on all interfaces (0.0.0.0) for LAN access. Default: localhost only.",
63
73
  default: false,
64
74
  }),
75
+ all: Flags.boolean({
76
+ description: "Reset entire database without prompting (for tt ag reset --all)",
77
+ default: false,
78
+ }),
65
79
  };
66
80
 
67
81
  static override args = {
@@ -96,22 +110,18 @@ export default class Agentboard extends BaseCommand {
96
110
  );
97
111
  const dataDir = flags["data-dir"] ? resolve(flags["data-dir"]) : defaultDataDir;
98
112
  const dbPath = join(dataDir, "agentboard.db");
99
- const walPath = `${dbPath}-wal`;
100
- const shmPath = `${dbPath}-shm`;
101
113
 
102
114
  if (!existsSync(dbPath)) {
103
115
  consola.info("No database found — nothing to reset.");
104
116
  return;
105
117
  }
106
118
 
107
- consola.warn(`This will delete: ${dbPath}`);
108
- for (const f of [dbPath, walPath, shmPath]) {
109
- if (existsSync(f)) {
110
- const { unlinkSync } = await import("node:fs");
111
- unlinkSync(f);
112
- }
119
+ if (flags.all) {
120
+ await this.resetEntireDatabase(dbPath);
121
+ return;
113
122
  }
114
- consola.success("Database reset. Start AgentBoard to create a fresh DB.");
123
+
124
+ await this.selectiveClear(dbPath);
115
125
  return;
116
126
  }
117
127
 
@@ -173,4 +183,151 @@ export default class Agentboard extends BaseCommand {
173
183
 
174
184
  proc.on("exit", (code) => process.exit(code ?? 0));
175
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
+ }
176
333
  }