@qianxude/tem 0.2.0 → 0.4.0

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.
@@ -0,0 +1,561 @@
1
+ import { Database } from '../../database/index.js';
2
+ import {
3
+ formatDuration,
4
+ formatTimestamp,
5
+ formatNumber,
6
+ formatPercent,
7
+ truncate,
8
+ } from '../utils/format.js';
9
+ import { renderTable, renderKeyValue } from '../utils/table.js';
10
+
11
+ interface BatchSummary {
12
+ id: string;
13
+ code: string;
14
+ type: string;
15
+ created_at: string;
16
+ completed_at: string | null;
17
+ total: number;
18
+ pending: number;
19
+ running: number;
20
+ completed: number;
21
+ failed: number;
22
+ }
23
+
24
+ interface TaskTiming {
25
+ avg_time: number;
26
+ }
27
+
28
+ interface RecentError {
29
+ error: string;
30
+ failed_at: string;
31
+ }
32
+
33
+ interface WatchOptions {
34
+ interval: number;
35
+ timeout: number;
36
+ noClear: boolean;
37
+ latest: boolean;
38
+ }
39
+
40
+ interface WatchState {
41
+ startTime: number;
42
+ lastUpdateTime: number;
43
+ completedCount: number;
44
+ shouldExit: boolean;
45
+ exitCode: number;
46
+ }
47
+
48
+ function openDatabase(dbPath: string): Database {
49
+ return new Database({ path: dbPath, busyTimeout: 5000 });
50
+ }
51
+
52
+ function getBatchSummary(db: Database, batchId: string): BatchSummary | null {
53
+ const rows = db.query<BatchSummary>(
54
+ `SELECT
55
+ b.id,
56
+ b.code,
57
+ b.type,
58
+ b.created_at,
59
+ b.completed_at,
60
+ COUNT(t.id) as total,
61
+ SUM(CASE WHEN t.status = 'pending' THEN 1 ELSE 0 END) as pending,
62
+ SUM(CASE WHEN t.status = 'running' THEN 1 ELSE 0 END) as running,
63
+ SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed,
64
+ SUM(CASE WHEN t.status = 'failed' THEN 1 ELSE 0 END) as failed
65
+ FROM batch b
66
+ LEFT JOIN task t ON t.batch_id = b.id
67
+ WHERE b.id = ?
68
+ GROUP BY b.id`,
69
+ [batchId]
70
+ );
71
+ return rows[0] || null;
72
+ }
73
+
74
+ function getBatchByCode(db: Database, code: string): { id: string } | null {
75
+ const rows = db.query<{ id: string }>(
76
+ 'SELECT id FROM batch WHERE code = ?',
77
+ [code]
78
+ );
79
+ return rows[0] || null;
80
+ }
81
+
82
+ function getLatestBatch(db: Database): { id: string; code: string } | null {
83
+ const rows = db.query<{ id: string; code: string }>(
84
+ 'SELECT id, code FROM batch ORDER BY created_at DESC LIMIT 1'
85
+ );
86
+ return rows[0] || null;
87
+ }
88
+
89
+ function getTaskTiming(db: Database, batchId: string): TaskTiming | null {
90
+ const rows = db.query<TaskTiming>(
91
+ `SELECT
92
+ AVG(
93
+ unixepoch(completed_at) - unixepoch(created_at)
94
+ ) * 1000 as avg_time
95
+ FROM task
96
+ WHERE batch_id = ?
97
+ AND status = 'completed'
98
+ AND completed_at IS NOT NULL`,
99
+ [batchId]
100
+ );
101
+ return rows[0] || null;
102
+ }
103
+
104
+ function getRecentErrors(db: Database, batchId: string, limit: number): RecentError[] {
105
+ return db.query<RecentError>(
106
+ `SELECT
107
+ error,
108
+ failed_at
109
+ FROM task
110
+ WHERE batch_id = ?
111
+ AND status = 'failed'
112
+ AND error IS NOT NULL
113
+ ORDER BY failed_at DESC
114
+ LIMIT ?`,
115
+ [batchId, limit]
116
+ );
117
+ }
118
+
119
+ function getStuckTaskCount(db: Database, batchId: string): number {
120
+ const rows = db.query<{ count: number }>(
121
+ `SELECT COUNT(*) as count
122
+ FROM task
123
+ WHERE batch_id = ?
124
+ AND status = 'running'
125
+ AND claimed_at < datetime('now', '-5 minutes')`,
126
+ [batchId]
127
+ );
128
+ return rows[0]?.count ?? 0;
129
+ }
130
+
131
+ function determineBatchStatus(summary: BatchSummary): string {
132
+ if (summary.failed > 0 && summary.pending === 0 && summary.running === 0) {
133
+ return 'failed';
134
+ }
135
+ if (summary.pending === 0 && summary.running === 0) {
136
+ return 'completed';
137
+ }
138
+ if (summary.running > 0) {
139
+ return 'running';
140
+ }
141
+ return 'pending';
142
+ }
143
+
144
+ function formatElapsed(startTime: number): string {
145
+ const elapsed = Date.now() - startTime;
146
+ return formatDuration(elapsed);
147
+ }
148
+
149
+ function calculateETA(
150
+ summary: BatchSummary,
151
+ avgTaskTimeMs: number | null,
152
+ state: WatchState
153
+ ): string {
154
+ const remaining = summary.pending + summary.running;
155
+ if (remaining === 0) return 'Complete';
156
+ if (!avgTaskTimeMs || avgTaskTimeMs <= 0) return 'Calculating...';
157
+
158
+ // Estimate based on throughput since watch started
159
+ const elapsed = Date.now() - state.startTime;
160
+ const completedSinceStart = summary.completed - state.completedCount;
161
+
162
+ if (completedSinceStart > 0 && elapsed > 5000) {
163
+ const throughput = completedSinceStart / (elapsed / 1000);
164
+ const etaSeconds = remaining / throughput;
165
+ return formatDuration(Math.round(etaSeconds * 1000));
166
+ }
167
+
168
+ // Fallback to average task time
169
+ const etaMs = remaining * avgTaskTimeMs;
170
+ return formatDuration(Math.round(etaMs));
171
+ }
172
+
173
+ function calculateThroughput(summary: BatchSummary, state: WatchState): string {
174
+ const elapsed = Date.now() - state.startTime;
175
+ if (elapsed < 1000) return '-';
176
+
177
+ const completedSinceStart = summary.completed - state.completedCount;
178
+ const throughput = completedSinceStart / (elapsed / 1000);
179
+
180
+ if (throughput < 1) {
181
+ return `${(1 / throughput).toFixed(1)}s/task`;
182
+ }
183
+ return `${throughput.toFixed(2)} tasks/sec`;
184
+ }
185
+
186
+ function renderProgressBar(completed: number, failed: number, total: number, width: number = 40): string {
187
+ if (total === 0) return `[${' '.repeat(width)}] 0%`;
188
+
189
+ const done = completed + failed;
190
+ const percent = (done / total) * 100;
191
+ const filled = Math.round((done / total) * width);
192
+ const failedCount = Math.round((failed / total) * width);
193
+ const completedCount = filled - failedCount;
194
+
195
+ const bar = '█'.repeat(completedCount) + '▓'.repeat(failedCount) + '░'.repeat(width - filled);
196
+ return `[${bar}] ${percent.toFixed(1)}%`;
197
+ }
198
+
199
+ function getStatusColor(status: string): string {
200
+ switch (status) {
201
+ case 'completed':
202
+ return '\x1b[32m'; // Green
203
+ case 'failed':
204
+ return '\x1b[31m'; // Red
205
+ case 'running':
206
+ return '\x1b[33m'; // Yellow
207
+ default:
208
+ return '\x1b[36m'; // Cyan
209
+ }
210
+ }
211
+
212
+ function resetColor(): string {
213
+ return '\x1b[0m';
214
+ }
215
+
216
+ function clearScreen(): void {
217
+ process.stdout.write('\x1b[2J\x1b[H');
218
+ }
219
+
220
+ function shouldContinueWatching(summary: BatchSummary): boolean {
221
+ const status = determineBatchStatus(summary);
222
+ return status !== 'completed' && status !== 'failed';
223
+ }
224
+
225
+ function renderWatchDisplay(
226
+ summary: BatchSummary,
227
+ timing: TaskTiming | null,
228
+ recentErrors: RecentError[],
229
+ stuckCount: number,
230
+ state: WatchState,
231
+ _options: WatchOptions
232
+ ): string {
233
+ const lines: string[] = [];
234
+ const status = determineBatchStatus(summary);
235
+ const statusColor = getStatusColor(status);
236
+ const elapsed = Date.now() - new Date(summary.created_at).getTime();
237
+
238
+ // Header
239
+ lines.push('╔══════════════════════════════════════════════════════════════════╗');
240
+ lines.push(`║ TEM Batch Watch${' '.repeat(51)}║`);
241
+ lines.push('╠══════════════════════════════════════════════════════════════════╣');
242
+
243
+ // Batch info
244
+ const headerData = [
245
+ { key: 'Batch', value: summary.code },
246
+ { key: 'Type', value: summary.type },
247
+ { key: 'Status', value: `${statusColor}${status.toUpperCase()}${resetColor()}` },
248
+ { key: 'Elapsed', value: formatDuration(elapsed) },
249
+ { key: 'Watching', value: formatElapsed(state.startTime) },
250
+ ];
251
+
252
+ for (const item of headerData) {
253
+ const line = `║ ${(item.key + ':').padEnd(12)} ${item.value.toString().padEnd(52)}║`;
254
+ lines.push(line);
255
+ }
256
+ lines.push('╠══════════════════════════════════════════════════════════════════╣');
257
+
258
+ // Progress bar
259
+ lines.push('║ Progress ║');
260
+ lines.push(`║ ${renderProgressBar(summary.completed, summary.failed, summary.total).padEnd(64)}║`);
261
+ lines.push('╠══════════════════════════════════════════════════════════════════╣');
262
+
263
+ // Stats table
264
+ lines.push('║ Statistics ║');
265
+ const total = summary.total || 1;
266
+ const statsData = [
267
+ { status: 'Pending', count: summary.pending, percent: formatPercent(summary.pending, total) },
268
+ { status: 'Running', count: summary.running, percent: formatPercent(summary.running, total) },
269
+ { status: 'Completed', count: summary.completed, percent: formatPercent(summary.completed, total) },
270
+ { status: 'Failed', count: summary.failed, percent: formatPercent(summary.failed, total) },
271
+ { status: 'Total', count: summary.total, percent: '100%' },
272
+ ];
273
+
274
+ for (const stat of statsData) {
275
+ const line = `║ ${stat.status.padEnd(12)} ${formatNumber(stat.count).padStart(10)} ${stat.percent.padStart(6)}${' '.repeat(28)}║`;
276
+ lines.push(line);
277
+ }
278
+ lines.push('╠══════════════════════════════════════════════════════════════════╣');
279
+
280
+ // Timing info
281
+ lines.push('║ Performance ║');
282
+ const throughput = calculateThroughput(summary, state);
283
+ const eta = calculateETA(summary, timing?.avg_time ?? null, state);
284
+ const perfData = [
285
+ { key: 'Throughput', value: throughput },
286
+ { key: 'ETA', value: eta },
287
+ ];
288
+
289
+ if (timing?.avg_time) {
290
+ perfData.push({ key: 'Avg Task', value: formatDuration(Math.round(timing.avg_time)) });
291
+ }
292
+
293
+ for (const item of perfData) {
294
+ const line = `║ ${(item.key + ':').padEnd(12)} ${item.value.toString().padEnd(52)}║`;
295
+ lines.push(line);
296
+ }
297
+
298
+ // Stuck tasks warning
299
+ if (stuckCount > 0) {
300
+ lines.push('╠══════════════════════════════════════════════════════════════════╣');
301
+ lines.push(`║ ⚠ WARNING: ${stuckCount} task(s) stuck > 5 minutes${' '.repeat(37)}║`);
302
+ }
303
+
304
+ // Recent errors
305
+ if (recentErrors.length > 0) {
306
+ lines.push('╠══════════════════════════════════════════════════════════════════╣');
307
+ lines.push('║ Recent Failures ║');
308
+ for (const error of recentErrors.slice(0, 3)) {
309
+ const errorMsg = truncate(error.error, 58);
310
+ const line = `║ • ${errorMsg.padEnd(60)}║`;
311
+ lines.push(line);
312
+ }
313
+ }
314
+
315
+ lines.push('╚══════════════════════════════════════════════════════════════════╝');
316
+ lines.push(`Last update: ${new Date().toISOString()}`);
317
+
318
+ return lines.join('\n');
319
+ }
320
+
321
+ function renderFinalReport(
322
+ summary: BatchSummary,
323
+ timing: TaskTiming | null,
324
+ recentErrors: RecentError[],
325
+ stuckCount: number,
326
+ state: WatchState
327
+ ): string {
328
+ const lines: string[] = [];
329
+ const status = determineBatchStatus(summary);
330
+ const elapsed = Date.now() - new Date(summary.created_at).getTime();
331
+
332
+ lines.push('');
333
+ lines.push('════════════════════════════════════════════════════════════════════');
334
+ lines.push(' BATCH COMPLETED - FINAL REPORT');
335
+ lines.push('════════════════════════════════════════════════════════════════════');
336
+ lines.push('');
337
+
338
+ // Overview
339
+ lines.push('Overview');
340
+ const overviewData = [
341
+ { key: 'Batch Code', value: summary.code },
342
+ { key: 'Type', value: summary.type },
343
+ { key: 'Status', value: status },
344
+ { key: 'Created', value: formatTimestamp(summary.created_at) },
345
+ { key: 'Duration', value: formatDuration(elapsed) },
346
+ ];
347
+ lines.push(renderKeyValue(overviewData));
348
+ lines.push('');
349
+
350
+ // Status breakdown
351
+ lines.push('Status Breakdown');
352
+ const total = summary.total || 1;
353
+ const breakdownData = [
354
+ { status: 'Total', count: formatNumber(summary.total), percent: '100%' },
355
+ { status: 'Pending', count: formatNumber(summary.pending), percent: formatPercent(summary.pending, total) },
356
+ { status: 'Running', count: formatNumber(summary.running), percent: formatPercent(summary.running, total) },
357
+ { status: 'Completed', count: formatNumber(summary.completed), percent: formatPercent(summary.completed, total) },
358
+ { status: 'Failed', count: formatNumber(summary.failed), percent: formatPercent(summary.failed, total) },
359
+ ];
360
+ lines.push(renderTable(
361
+ [
362
+ { header: 'Status', key: 'status', width: 12 },
363
+ { header: 'Count', key: 'count', width: 10, align: 'right' },
364
+ { header: 'Percent', key: 'percent', width: 10, align: 'right' },
365
+ ],
366
+ breakdownData
367
+ ));
368
+
369
+ // Timing
370
+ if (timing && timing.avg_time !== null) {
371
+ lines.push('');
372
+ lines.push('Timing Analysis');
373
+ const elapsedWatch = Date.now() - state.startTime;
374
+ const completedSinceStart = summary.completed - state.completedCount;
375
+ let throughput = '-';
376
+ if (elapsedWatch > 0 && completedSinceStart > 0) {
377
+ const tps = completedSinceStart / (elapsedWatch / 1000);
378
+ throughput = tps < 1 ? `${(1 / tps).toFixed(1)}s/task` : `${tps.toFixed(2)} tasks/sec`;
379
+ }
380
+
381
+ const timingData = [
382
+ { key: 'Avg Task Time', value: formatDuration(Math.round(timing.avg_time)) },
383
+ { key: 'Throughput', value: throughput },
384
+ ];
385
+ lines.push(renderKeyValue(timingData));
386
+ }
387
+
388
+ // Stuck tasks
389
+ if (stuckCount > 0) {
390
+ lines.push('');
391
+ lines.push(`⚠ WARNING: ${stuckCount} task(s) were stuck > 5 minutes`);
392
+ }
393
+
394
+ // Errors
395
+ if (recentErrors.length > 0) {
396
+ lines.push('');
397
+ lines.push('Recent Failures');
398
+ const errorData = recentErrors.slice(0, 5).map(e => ({
399
+ time: formatTimestamp(e.failed_at).slice(0, 19),
400
+ error: truncate(e.error, 60),
401
+ }));
402
+ lines.push(renderTable(
403
+ [
404
+ { header: 'Time', key: 'time', width: 20 },
405
+ { header: 'Error', key: 'error' },
406
+ ],
407
+ errorData
408
+ ));
409
+ }
410
+
411
+ lines.push('');
412
+ lines.push('════════════════════════════════════════════════════════════════════');
413
+
414
+ return lines.join('\n');
415
+ }
416
+
417
+ export async function watchCommand(
418
+ dbPath: string,
419
+ batchCode: string | undefined,
420
+ flags: Record<string, string | boolean>
421
+ ): Promise<void> {
422
+ const options: WatchOptions = {
423
+ interval: (parseInt(String(flags['interval'] || '5'), 10) || 5) * 1000,
424
+ timeout: (parseInt(String(flags['timeout'] || '3600'), 10) || 3600) * 1000,
425
+ noClear: flags['no-clear'] === true,
426
+ latest: flags['latest'] === true,
427
+ };
428
+
429
+ const db = openDatabase(dbPath);
430
+
431
+ // Resolve batch
432
+ let batchId: string;
433
+ let resolvedBatchCode: string;
434
+
435
+ try {
436
+ if (batchCode) {
437
+ const batch = getBatchByCode(db, batchCode);
438
+ if (!batch) {
439
+ console.error(`Error: Batch "${batchCode}" not found`);
440
+ process.exit(1);
441
+ }
442
+ batchId = batch.id;
443
+ resolvedBatchCode = batchCode;
444
+ } else if (options.latest) {
445
+ const batch = getLatestBatch(db);
446
+ if (!batch) {
447
+ console.error('Error: No batches found in database');
448
+ process.exit(1);
449
+ }
450
+ batchId = batch.id;
451
+ resolvedBatchCode = batch.code;
452
+ } else {
453
+ console.error('Error: Either provide a batch-code or use --latest flag');
454
+ process.exit(2);
455
+ }
456
+
457
+ // Get initial summary
458
+ let summary = getBatchSummary(db, batchId);
459
+ if (!summary) {
460
+ console.error(`Error: Could not load summary for batch "${resolvedBatchCode}"`);
461
+ process.exit(1);
462
+ }
463
+
464
+ // If already completed/failed, just print report and exit
465
+ const initialStatus = determineBatchStatus(summary);
466
+ if (initialStatus === 'completed' || initialStatus === 'failed') {
467
+ const timing = getTaskTiming(db, batchId);
468
+ const recentErrors = getRecentErrors(db, batchId, 5);
469
+ const stuckCount = getStuckTaskCount(db, batchId);
470
+ const state: WatchState = {
471
+ startTime: Date.now(),
472
+ lastUpdateTime: Date.now(),
473
+ completedCount: summary.completed,
474
+ shouldExit: false,
475
+ exitCode: 0,
476
+ };
477
+ console.log(renderFinalReport(summary, timing, recentErrors, stuckCount, state));
478
+ process.exit(0);
479
+ }
480
+
481
+ // Initialize watch state
482
+ const state: WatchState = {
483
+ startTime: Date.now(),
484
+ lastUpdateTime: Date.now(),
485
+ completedCount: summary.completed,
486
+ shouldExit: false,
487
+ exitCode: 0,
488
+ };
489
+
490
+ // Set up SIGINT handler
491
+ let sigintReceived = false;
492
+ process.on('SIGINT', () => {
493
+ if (sigintReceived) {
494
+ process.exit(130);
495
+ }
496
+ sigintReceived = true;
497
+ state.shouldExit = true;
498
+ state.exitCode = 130;
499
+ });
500
+
501
+ // Initial render
502
+ if (!options.noClear) clearScreen();
503
+ const timing = getTaskTiming(db, batchId);
504
+ const recentErrors = getRecentErrors(db, batchId, 3);
505
+ const stuckCount = getStuckTaskCount(db, batchId);
506
+ console.log(renderWatchDisplay(summary, timing, recentErrors, stuckCount, state, options));
507
+
508
+ // Watch loop
509
+ return new Promise((_resolve) => {
510
+ const intervalId = setInterval(() => {
511
+ // Check timeout
512
+ if (Date.now() - state.startTime > options.timeout) {
513
+ clearInterval(intervalId);
514
+ if (!options.noClear) clearScreen();
515
+ console.error('Watch timeout reached');
516
+ db.close();
517
+ process.exit(1);
518
+ }
519
+
520
+ // Check if should exit
521
+ if (state.shouldExit) {
522
+ clearInterval(intervalId);
523
+ if (!options.noClear) clearScreen();
524
+ console.log('\nWatch interrupted by user');
525
+ db.close();
526
+ process.exit(state.exitCode);
527
+ }
528
+
529
+ // Refresh data
530
+ summary = getBatchSummary(db, batchId);
531
+ if (!summary) {
532
+ clearInterval(intervalId);
533
+ console.error('Error: Could not refresh batch data');
534
+ db.close();
535
+ process.exit(1);
536
+ }
537
+
538
+ // Render update
539
+ if (!options.noClear) clearScreen();
540
+ const timing = getTaskTiming(db, batchId);
541
+ const recentErrors = getRecentErrors(db, batchId, 3);
542
+ const stuckCount = getStuckTaskCount(db, batchId);
543
+ console.log(renderWatchDisplay(summary, timing, recentErrors, stuckCount, state, options));
544
+
545
+ // Check if batch is done
546
+ if (!shouldContinueWatching(summary)) {
547
+ clearInterval(intervalId);
548
+ if (!options.noClear) clearScreen();
549
+ console.log(renderFinalReport(summary, timing, recentErrors, stuckCount, state));
550
+ db.close();
551
+ process.exit(0);
552
+ }
553
+
554
+ state.lastUpdateTime = Date.now();
555
+ }, options.interval);
556
+ });
557
+ } catch (error) {
558
+ db.close();
559
+ throw error;
560
+ }
561
+ }
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bun
2
+ import { reportCommand } from './commands/report.js';
3
+ import { listCommand } from './commands/list.js';
4
+ import { watchCommand } from './commands/watch.js';
5
+
6
+ const HELP_TEXT = `tem CLI - Batch diagnostics and reporting tool
7
+
8
+ Usage: tem <command> [options]
9
+
10
+ Commands:
11
+ report <db-path> [batch-code] Generate diagnostic report
12
+ list <db-path> List tasks with filtering
13
+ watch <db-path> [batch-code] Monitor a running batch
14
+
15
+ Options:
16
+ --help, -h Show this help message
17
+
18
+ Report command options:
19
+ --latest Use the most recently created batch
20
+ --limit-errors N Show top N error patterns (default: 10)
21
+
22
+ List command options:
23
+ --batch <code> Filter by batch code
24
+ --status <status> Filter by status (pending|running|completed|failed)
25
+ --type <type> Filter by task type
26
+ --limit <n> Limit results (default: 100)
27
+
28
+ Watch command options:
29
+ --latest Use the most recently created batch
30
+ --interval N Refresh interval in seconds (default: 5)
31
+ --timeout N Maximum watch time in seconds (default: 3600)
32
+ --no-clear Don't clear screen between updates
33
+
34
+ Examples:
35
+ tem report ./test.db # Summary of all batches
36
+ tem report ./test.db my-batch # Detailed report for batch
37
+ tem report ./test.db --latest # Report for latest batch
38
+ tem list ./test.db --batch my-batch --status failed --limit 20
39
+ tem watch ./test.db --latest # Watch latest batch
40
+ tem watch ./test.db my-batch # Watch specific batch
41
+ tem watch ./test.db --latest --interval 10 --timeout 300
42
+ `;
43
+
44
+ function showHelp(): void {
45
+ console.log(HELP_TEXT);
46
+ }
47
+
48
+ function parseArgs(args: string[]): {
49
+ command: string;
50
+ dbPath: string;
51
+ positional: string[];
52
+ flags: Record<string, string | boolean>;
53
+ } {
54
+ const flags: Record<string, string | boolean> = {};
55
+ const positional: string[] = [];
56
+
57
+ for (let i = 0; i < args.length; i++) {
58
+ const arg = args[i];
59
+
60
+ if (arg === '--help' || arg === '-h') {
61
+ flags.help = true;
62
+ } else if (arg === '--latest') {
63
+ flags.latest = true;
64
+ } else if (arg.startsWith('--')) {
65
+ const key = arg.slice(2);
66
+ const nextArg = args[i + 1];
67
+ if (nextArg && !nextArg.startsWith('--')) {
68
+ flags[key] = nextArg;
69
+ i++;
70
+ } else {
71
+ flags[key] = true;
72
+ }
73
+ } else {
74
+ positional.push(arg);
75
+ }
76
+ }
77
+
78
+ return {
79
+ command: positional[0] || '',
80
+ dbPath: positional[1] || '',
81
+ positional: positional.slice(2),
82
+ flags,
83
+ };
84
+ }
85
+
86
+ async function main(): Promise<void> {
87
+ const args = Bun.argv.slice(2);
88
+
89
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
90
+ showHelp();
91
+ process.exit(0);
92
+ }
93
+
94
+ const { command, dbPath, positional, flags } = parseArgs(args);
95
+
96
+ if (flags.help) {
97
+ showHelp();
98
+ process.exit(0);
99
+ }
100
+
101
+ if (!command) {
102
+ console.error('Error: No command specified');
103
+ showHelp();
104
+ process.exit(2);
105
+ }
106
+
107
+ if (!dbPath) {
108
+ console.error('Error: Database path required');
109
+ showHelp();
110
+ process.exit(2);
111
+ }
112
+
113
+ try {
114
+ switch (command) {
115
+ case 'report':
116
+ await reportCommand(dbPath, positional[0], flags);
117
+ break;
118
+ case 'list':
119
+ await listCommand(dbPath, flags);
120
+ break;
121
+ case 'watch':
122
+ await watchCommand(dbPath, positional[0], flags);
123
+ break;
124
+ default:
125
+ console.error(`Error: Unknown command "${command}"`);
126
+ showHelp();
127
+ process.exit(2);
128
+ }
129
+ } catch (error) {
130
+ console.error('Error:', error instanceof Error ? error.message : String(error));
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ main();