openbroker 1.0.75 → 1.0.80
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/SKILL.md +6 -2
- package/bin/cli.ts +16 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/auto/audit-daemon.js +567 -0
- package/scripts/auto/audit.ts +552 -0
- package/scripts/auto/cli.ts +32 -0
- package/scripts/auto/report.ts +459 -0
- package/scripts/auto/runtime.ts +264 -79
- package/scripts/auto/types.ts +10 -0
- package/scripts/core/client.ts +245 -0
- package/scripts/core/ws.ts +25 -0
- package/scripts/info/funding-history.ts +5 -5
- package/scripts/info/search-markets.ts +30 -6
- package/scripts/info/spot.ts +23 -8
- package/scripts/operations/spot-order.ts +189 -0
- package/scripts/plugin/tools.ts +126 -6
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { AUDIT_DB_PATH } from './audit.js';
|
|
5
|
+
import { formatUsd, parseArgs } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_WATCH_INTERVAL_MS = 2000;
|
|
8
|
+
|
|
9
|
+
function printUsage() {
|
|
10
|
+
console.log(`
|
|
11
|
+
Usage: openbroker auto report <id> [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--run <run-id|latest> Specific run ID to inspect (default: latest)
|
|
15
|
+
--limit <n> Number of recent rows per section (default: 10)
|
|
16
|
+
--watch Refresh the report continuously
|
|
17
|
+
--watch-interval <ms> Refresh interval for --watch (default: 2000)
|
|
18
|
+
--json Output JSON
|
|
19
|
+
--help, -h Show this help
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
openbroker auto report hype-mm-v2-live-r4
|
|
23
|
+
openbroker auto report hype-mm-v2-live-r4 --limit 20
|
|
24
|
+
openbroker auto report hype-mm-v2-live-r4 --watch
|
|
25
|
+
openbroker auto report hype-mm-v2-live-r4 --run 123e4567... --json
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type RunRow = {
|
|
30
|
+
run_id: string;
|
|
31
|
+
automation_id: string;
|
|
32
|
+
script_path: string;
|
|
33
|
+
account_address: string | null;
|
|
34
|
+
wallet_address: string | null;
|
|
35
|
+
is_api_wallet: number;
|
|
36
|
+
dry_run: number;
|
|
37
|
+
verbose: number;
|
|
38
|
+
poll_interval_ms: number | null;
|
|
39
|
+
use_websocket: number;
|
|
40
|
+
pid: number;
|
|
41
|
+
started_at: number;
|
|
42
|
+
stopped_at: number | null;
|
|
43
|
+
status: string;
|
|
44
|
+
stop_reason: string | null;
|
|
45
|
+
initial_state_json: string | null;
|
|
46
|
+
persisted_state_json: string | null;
|
|
47
|
+
poll_count: number | null;
|
|
48
|
+
events_emitted: number | null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function parseJson<T>(value: string | null): T | null {
|
|
52
|
+
if (!value) return null;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(value) as T;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseNumber(raw: string | boolean | undefined, fallback: number): number {
|
|
61
|
+
const parsed = Number(raw);
|
|
62
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatTimestamp(timestamp: number | null): string {
|
|
66
|
+
return timestamp ? new Date(timestamp).toLocaleString() : '-';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatDurationMs(startedAt: number, stoppedAt: number | null): string {
|
|
70
|
+
const end = stoppedAt ?? Date.now();
|
|
71
|
+
const totalSeconds = Math.max(0, Math.round((end - startedAt) / 1000));
|
|
72
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
73
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
74
|
+
const seconds = totalSeconds % 60;
|
|
75
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
76
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
77
|
+
return `${seconds}s`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getPositionalArgs(rawArgs: string[]): string[] {
|
|
81
|
+
return rawArgs.filter((arg, index) => {
|
|
82
|
+
if (arg.startsWith('--')) return false;
|
|
83
|
+
if (index > 0 && rawArgs[index - 1]?.startsWith('--')) return false;
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function loadReport(
|
|
89
|
+
db: DatabaseSync,
|
|
90
|
+
automationId: string,
|
|
91
|
+
runSelector: string,
|
|
92
|
+
limit: number,
|
|
93
|
+
) {
|
|
94
|
+
const run = runSelector !== 'latest'
|
|
95
|
+
? db.prepare(`
|
|
96
|
+
SELECT *
|
|
97
|
+
FROM automation_runs
|
|
98
|
+
WHERE automation_id = ? AND run_id = ?
|
|
99
|
+
LIMIT 1
|
|
100
|
+
`).get(automationId, runSelector) as RunRow | undefined
|
|
101
|
+
: db.prepare(`
|
|
102
|
+
SELECT *
|
|
103
|
+
FROM automation_runs
|
|
104
|
+
WHERE automation_id = ?
|
|
105
|
+
ORDER BY started_at DESC
|
|
106
|
+
LIMIT 1
|
|
107
|
+
`).get(automationId) as RunRow | undefined;
|
|
108
|
+
|
|
109
|
+
if (!run) {
|
|
110
|
+
throw new Error(`No audit run found for automation "${automationId}"${runSelector !== 'latest' ? ` and run "${runSelector}"` : ''}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const countTables = {
|
|
114
|
+
logs: 'automation_logs',
|
|
115
|
+
events: 'automation_events',
|
|
116
|
+
actions: 'automation_actions',
|
|
117
|
+
snapshots: 'automation_snapshots',
|
|
118
|
+
orderUpdates: 'automation_order_updates',
|
|
119
|
+
fills: 'automation_fills',
|
|
120
|
+
userEvents: 'automation_user_events',
|
|
121
|
+
stateChanges: 'automation_state_changes',
|
|
122
|
+
publishes: 'automation_publishes',
|
|
123
|
+
errors: 'automation_errors',
|
|
124
|
+
notes: 'automation_notes',
|
|
125
|
+
metrics: 'automation_metrics',
|
|
126
|
+
} as const;
|
|
127
|
+
|
|
128
|
+
const counts = Object.fromEntries(
|
|
129
|
+
Object.entries(countTables).map(([key, table]) => {
|
|
130
|
+
const row = db.prepare(`SELECT count(*) AS c FROM ${table} WHERE run_id = ?`).get(run.run_id) as { c: number };
|
|
131
|
+
return [key, row.c];
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const fillSummary = db.prepare(`
|
|
136
|
+
SELECT
|
|
137
|
+
count(*) AS count,
|
|
138
|
+
COALESCE(sum(fee), 0) AS total_fee,
|
|
139
|
+
COALESCE(sum(closed_pnl), 0) AS total_closed_pnl,
|
|
140
|
+
COALESCE(sum(size * price), 0) AS total_volume
|
|
141
|
+
FROM automation_fills
|
|
142
|
+
WHERE run_id = ?
|
|
143
|
+
`).get(run.run_id) as {
|
|
144
|
+
count: number;
|
|
145
|
+
total_fee: number;
|
|
146
|
+
total_closed_pnl: number;
|
|
147
|
+
total_volume: number;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const firstSnapshot = db.prepare(`
|
|
151
|
+
SELECT timestamp, poll_count, equity, margin_used, margin_used_pct, positions_json
|
|
152
|
+
FROM automation_snapshots
|
|
153
|
+
WHERE run_id = ?
|
|
154
|
+
ORDER BY timestamp ASC
|
|
155
|
+
LIMIT 1
|
|
156
|
+
`).get(run.run_id) as Record<string, unknown> | undefined;
|
|
157
|
+
|
|
158
|
+
const latestSnapshot = db.prepare(`
|
|
159
|
+
SELECT timestamp, poll_count, equity, margin_used, margin_used_pct, positions_json
|
|
160
|
+
FROM automation_snapshots
|
|
161
|
+
WHERE run_id = ?
|
|
162
|
+
ORDER BY timestamp DESC
|
|
163
|
+
LIMIT 1
|
|
164
|
+
`).get(run.run_id) as Record<string, unknown> | undefined;
|
|
165
|
+
|
|
166
|
+
const actionBreakdown = db.prepare(`
|
|
167
|
+
SELECT
|
|
168
|
+
method,
|
|
169
|
+
sum(CASE WHEN phase = 'request' THEN 1 ELSE 0 END) AS requests,
|
|
170
|
+
sum(CASE WHEN phase = 'response' THEN 1 ELSE 0 END) AS responses,
|
|
171
|
+
sum(CASE WHEN phase = 'error' THEN 1 ELSE 0 END) AS errors
|
|
172
|
+
FROM automation_actions
|
|
173
|
+
WHERE run_id = ?
|
|
174
|
+
GROUP BY method
|
|
175
|
+
ORDER BY requests DESC, responses DESC, errors DESC, method ASC
|
|
176
|
+
`).all(run.run_id);
|
|
177
|
+
|
|
178
|
+
const recentLogs = db.prepare(`
|
|
179
|
+
SELECT timestamp, level, message
|
|
180
|
+
FROM automation_logs
|
|
181
|
+
WHERE run_id = ?
|
|
182
|
+
ORDER BY timestamp DESC
|
|
183
|
+
LIMIT ?
|
|
184
|
+
`).all(run.run_id, limit);
|
|
185
|
+
|
|
186
|
+
const recentErrors = db.prepare(`
|
|
187
|
+
SELECT timestamp, stage, error_json
|
|
188
|
+
FROM automation_errors
|
|
189
|
+
WHERE run_id = ?
|
|
190
|
+
ORDER BY timestamp DESC
|
|
191
|
+
LIMIT ?
|
|
192
|
+
`).all(run.run_id, limit);
|
|
193
|
+
|
|
194
|
+
const recentFills = db.prepare(`
|
|
195
|
+
SELECT timestamp, coin, side, size, price, fee, closed_pnl
|
|
196
|
+
FROM automation_fills
|
|
197
|
+
WHERE run_id = ?
|
|
198
|
+
ORDER BY timestamp DESC
|
|
199
|
+
LIMIT ?
|
|
200
|
+
`).all(run.run_id, limit);
|
|
201
|
+
|
|
202
|
+
const recentOrderUpdates = db.prepare(`
|
|
203
|
+
SELECT timestamp, coin, side, size, price, status, oid
|
|
204
|
+
FROM automation_order_updates
|
|
205
|
+
WHERE run_id = ?
|
|
206
|
+
ORDER BY timestamp DESC
|
|
207
|
+
LIMIT ?
|
|
208
|
+
`).all(run.run_id, limit);
|
|
209
|
+
|
|
210
|
+
const recentNotes = db.prepare(`
|
|
211
|
+
SELECT timestamp, kind, payload_json
|
|
212
|
+
FROM automation_notes
|
|
213
|
+
WHERE run_id = ?
|
|
214
|
+
ORDER BY timestamp DESC
|
|
215
|
+
LIMIT ?
|
|
216
|
+
`).all(run.run_id, limit);
|
|
217
|
+
|
|
218
|
+
const recentMetrics = db.prepare(`
|
|
219
|
+
SELECT timestamp, name, value, tags_json
|
|
220
|
+
FROM automation_metrics
|
|
221
|
+
WHERE run_id = ?
|
|
222
|
+
ORDER BY timestamp DESC
|
|
223
|
+
LIMIT ?
|
|
224
|
+
`).all(run.run_id, limit);
|
|
225
|
+
|
|
226
|
+
const report = {
|
|
227
|
+
automationId: run.automation_id,
|
|
228
|
+
runId: run.run_id,
|
|
229
|
+
status: run.status,
|
|
230
|
+
stopReason: run.stop_reason,
|
|
231
|
+
scriptPath: run.script_path,
|
|
232
|
+
startedAt: new Date(run.started_at).toISOString(),
|
|
233
|
+
stoppedAt: run.stopped_at ? new Date(run.stopped_at).toISOString() : null,
|
|
234
|
+
durationSec: Math.max(0, Math.round(((run.stopped_at ?? Date.now()) - run.started_at) / 1000)),
|
|
235
|
+
accountAddress: run.account_address,
|
|
236
|
+
walletAddress: run.wallet_address,
|
|
237
|
+
dryRun: run.dry_run === 1,
|
|
238
|
+
verbose: run.verbose === 1,
|
|
239
|
+
useWebSocket: run.use_websocket === 1,
|
|
240
|
+
pollIntervalMs: run.poll_interval_ms,
|
|
241
|
+
pid: run.pid,
|
|
242
|
+
initialState: parseJson<Record<string, unknown>>(run.initial_state_json),
|
|
243
|
+
persistedState: parseJson<Record<string, unknown>>(run.persisted_state_json),
|
|
244
|
+
runtimeStats: {
|
|
245
|
+
pollCount: run.poll_count,
|
|
246
|
+
eventsEmitted: run.events_emitted,
|
|
247
|
+
},
|
|
248
|
+
counts,
|
|
249
|
+
fills: {
|
|
250
|
+
count: fillSummary.count,
|
|
251
|
+
totalFees: fillSummary.total_fee,
|
|
252
|
+
totalClosedPnl: fillSummary.total_closed_pnl,
|
|
253
|
+
netAfterFees: fillSummary.total_closed_pnl - fillSummary.total_fee,
|
|
254
|
+
totalVolume: fillSummary.total_volume,
|
|
255
|
+
},
|
|
256
|
+
equity: {
|
|
257
|
+
first: firstSnapshot ? {
|
|
258
|
+
timestamp: firstSnapshot.timestamp,
|
|
259
|
+
pollCount: firstSnapshot.poll_count,
|
|
260
|
+
equity: firstSnapshot.equity,
|
|
261
|
+
marginUsed: firstSnapshot.margin_used,
|
|
262
|
+
marginUsedPct: firstSnapshot.margin_used_pct,
|
|
263
|
+
positions: parseJson(firstSnapshot.positions_json as string | null),
|
|
264
|
+
} : null,
|
|
265
|
+
latest: latestSnapshot ? {
|
|
266
|
+
timestamp: latestSnapshot.timestamp,
|
|
267
|
+
pollCount: latestSnapshot.poll_count,
|
|
268
|
+
equity: latestSnapshot.equity,
|
|
269
|
+
marginUsed: latestSnapshot.margin_used,
|
|
270
|
+
marginUsedPct: latestSnapshot.margin_used_pct,
|
|
271
|
+
positions: parseJson(latestSnapshot.positions_json as string | null),
|
|
272
|
+
} : null,
|
|
273
|
+
delta: firstSnapshot && latestSnapshot
|
|
274
|
+
? Number(latestSnapshot.equity) - Number(firstSnapshot.equity)
|
|
275
|
+
: null,
|
|
276
|
+
},
|
|
277
|
+
actionBreakdown,
|
|
278
|
+
recent: {
|
|
279
|
+
logs: recentLogs,
|
|
280
|
+
errors: recentErrors.map((row) => ({
|
|
281
|
+
...row,
|
|
282
|
+
error: parseJson(row.error_json as string | null),
|
|
283
|
+
})),
|
|
284
|
+
fills: recentFills,
|
|
285
|
+
orderUpdates: recentOrderUpdates,
|
|
286
|
+
notes: recentNotes.map((row) => ({
|
|
287
|
+
...row,
|
|
288
|
+
payload: parseJson(row.payload_json as string | null),
|
|
289
|
+
})),
|
|
290
|
+
metrics: recentMetrics.map((row) => ({
|
|
291
|
+
...row,
|
|
292
|
+
tags: parseJson(row.tags_json as string | null),
|
|
293
|
+
})),
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
return { run, report, counts, actionBreakdown, recentErrors, recentFills, recentLogs };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function renderTextReport(data: ReturnType<typeof loadReport>, watchMode = false, watchIntervalMs = DEFAULT_WATCH_INTERVAL_MS): void {
|
|
301
|
+
const { run, report, counts, actionBreakdown, recentErrors, recentFills, recentLogs } = data;
|
|
302
|
+
|
|
303
|
+
if (watchMode && process.stdout.isTTY) {
|
|
304
|
+
process.stdout.write('\x1Bc');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log('Open Broker - Automation Report');
|
|
308
|
+
console.log('===============================\n');
|
|
309
|
+
|
|
310
|
+
console.log(`Automation: ${report.automationId}`);
|
|
311
|
+
console.log(`Run ID: ${report.runId}`);
|
|
312
|
+
console.log(`Status: ${report.status}${report.stopReason ? ` (${report.stopReason})` : ''}`);
|
|
313
|
+
console.log(`Started: ${formatTimestamp(run.started_at)}`);
|
|
314
|
+
console.log(`Stopped: ${formatTimestamp(run.stopped_at)}`);
|
|
315
|
+
console.log(`Duration: ${formatDurationMs(run.started_at, run.stopped_at)}`);
|
|
316
|
+
console.log(`Script: ${run.script_path}`);
|
|
317
|
+
console.log(`Account: ${run.account_address ?? '-'}`);
|
|
318
|
+
console.log(`Mode: ${report.dryRun ? 'dry' : 'live'}${report.useWebSocket ? ', ws' : ', polling only'}`);
|
|
319
|
+
console.log(`Poll interval: ${run.poll_interval_ms ?? '-'} ms`);
|
|
320
|
+
if (watchMode) {
|
|
321
|
+
console.log(`Refresh: every ${watchIntervalMs} ms (Ctrl-C to stop)`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
console.log('\nCounts');
|
|
325
|
+
console.log('------');
|
|
326
|
+
for (const [key, value] of Object.entries(counts)) {
|
|
327
|
+
console.log(`${key.padEnd(14)} ${value}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log('\nEconomics');
|
|
331
|
+
console.log('---------');
|
|
332
|
+
console.log(`Fills: ${report.fills.count}`);
|
|
333
|
+
console.log(`Volume: ${formatUsd(report.fills.totalVolume)}`);
|
|
334
|
+
console.log(`Closed PnL: ${formatUsd(report.fills.totalClosedPnl)}`);
|
|
335
|
+
console.log(`Fees: ${formatUsd(report.fills.totalFees)}`);
|
|
336
|
+
console.log(`Net after fees: ${formatUsd(report.fills.netAfterFees)}`);
|
|
337
|
+
|
|
338
|
+
console.log('\nEquity');
|
|
339
|
+
console.log('------');
|
|
340
|
+
if (report.equity.first) {
|
|
341
|
+
console.log(`First snapshot: ${formatUsd(report.equity.first.equity)} @ ${formatTimestamp(Number(report.equity.first.timestamp))}`);
|
|
342
|
+
} else {
|
|
343
|
+
console.log('First snapshot: -');
|
|
344
|
+
}
|
|
345
|
+
if (report.equity.latest) {
|
|
346
|
+
console.log(`Latest snapshot:${formatUsd(report.equity.latest.equity)} @ ${formatTimestamp(Number(report.equity.latest.timestamp))}`);
|
|
347
|
+
} else {
|
|
348
|
+
console.log('Latest snapshot:-');
|
|
349
|
+
}
|
|
350
|
+
console.log(`Delta: ${report.equity.delta === null ? '-' : formatUsd(report.equity.delta)}`);
|
|
351
|
+
|
|
352
|
+
if (Array.isArray(actionBreakdown) && actionBreakdown.length > 0) {
|
|
353
|
+
console.log('\nActions');
|
|
354
|
+
console.log('-------');
|
|
355
|
+
for (const row of actionBreakdown as Array<Record<string, unknown>>) {
|
|
356
|
+
console.log(
|
|
357
|
+
`${String(row.method).padEnd(20)} req=${String(row.requests).padStart(3)} resp=${String(row.responses).padStart(3)} err=${String(row.errors).padStart(3)}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (recentErrors.length > 0) {
|
|
363
|
+
console.log('\nRecent Errors');
|
|
364
|
+
console.log('-------------');
|
|
365
|
+
for (const row of recentErrors as Array<Record<string, unknown>>) {
|
|
366
|
+
const parsed = parseJson<{ message?: string }>(row.error_json as string | null);
|
|
367
|
+
console.log(`${formatTimestamp(Number(row.timestamp))} ${String(row.stage)} ${parsed?.message || String(row.error_json)}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (recentFills.length > 0) {
|
|
372
|
+
console.log('\nRecent Fills');
|
|
373
|
+
console.log('------------');
|
|
374
|
+
for (const row of recentFills as Array<Record<string, unknown>>) {
|
|
375
|
+
console.log(
|
|
376
|
+
`${formatTimestamp(Number(row.timestamp))} ${String(row.side).toUpperCase()} ${String(row.coin)} ${row.size} @ ${row.price} pnl=${row.closed_pnl} fee=${row.fee}`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (recentLogs.length > 0) {
|
|
382
|
+
console.log('\nRecent Logs');
|
|
383
|
+
console.log('-----------');
|
|
384
|
+
for (const row of recentLogs as Array<Record<string, unknown>>) {
|
|
385
|
+
console.log(`${formatTimestamp(Number(row.timestamp))} ${String(row.level).toUpperCase()} ${String(row.message)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function main() {
|
|
391
|
+
const rawArgs = process.argv.slice(2);
|
|
392
|
+
const args = parseArgs(rawArgs);
|
|
393
|
+
|
|
394
|
+
if (args.help || args.h) {
|
|
395
|
+
printUsage();
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const positional = getPositionalArgs(rawArgs);
|
|
400
|
+
const automationId = positional[0];
|
|
401
|
+
if (!automationId) {
|
|
402
|
+
console.error('Error: automation ID is required');
|
|
403
|
+
printUsage();
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const runSelector = String(args.run || 'latest');
|
|
408
|
+
const limit = parseNumber(args.limit, 10);
|
|
409
|
+
const jsonOutput = args.json === true;
|
|
410
|
+
const watchMode = args.watch === true;
|
|
411
|
+
const watchIntervalMs = parseNumber(args['watch-interval'], DEFAULT_WATCH_INTERVAL_MS);
|
|
412
|
+
|
|
413
|
+
if (watchMode && jsonOutput) {
|
|
414
|
+
console.error('Error: --watch cannot be combined with --json');
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const db = new DatabaseSync(AUDIT_DB_PATH);
|
|
419
|
+
let stopRequested = false;
|
|
420
|
+
|
|
421
|
+
const cleanup = () => {
|
|
422
|
+
try {
|
|
423
|
+
db.close();
|
|
424
|
+
} catch {
|
|
425
|
+
// ignore close failures during shutdown
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const requestStop = () => {
|
|
430
|
+
stopRequested = true;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
process.once('SIGINT', requestStop);
|
|
434
|
+
process.once('SIGTERM', requestStop);
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
if (!watchMode) {
|
|
438
|
+
const data = loadReport(db, automationId, runSelector, limit);
|
|
439
|
+
if (jsonOutput) {
|
|
440
|
+
console.log(JSON.stringify(data.report, null, 2));
|
|
441
|
+
} else {
|
|
442
|
+
renderTextReport(data);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
while (!stopRequested) {
|
|
448
|
+
const data = loadReport(db, automationId, runSelector, limit);
|
|
449
|
+
renderTextReport(data, true, watchIntervalMs);
|
|
450
|
+
await new Promise((resolve) => setTimeout(resolve, watchIntervalMs));
|
|
451
|
+
}
|
|
452
|
+
} finally {
|
|
453
|
+
process.off('SIGINT', requestStop);
|
|
454
|
+
process.off('SIGTERM', requestStop);
|
|
455
|
+
cleanup();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await main();
|