576b11106f
Apply HOME-based ~ expansion to summarize/drift/prune audit scripts for input/output path flags to match capture behavior and avoid literal ~/ path bugs. Architecture/protocol diagrams reviewed; no updates needed for this path-resolution change.
197 lines
6.0 KiB
JavaScript
197 lines
6.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { writeFile } from 'node:fs/promises';
|
|
import { resolve } from 'node:path';
|
|
import { parseArgs } from 'node:util';
|
|
import { queryAuditLogs } from '../src/audit/export.js';
|
|
import {
|
|
renderPhase0BaselineMarkdown,
|
|
summarizePhase0Baseline,
|
|
type Phase0BaselineSummaryOptions,
|
|
} from '../src/audit/phase0BaselineSummary.js';
|
|
|
|
const DEFAULT_EVENT_TYPES = ['run.state', 'run.cancel', 'reaction.match', 'reaction.skip'] as const;
|
|
|
|
function usage(): string {
|
|
return [
|
|
'Usage: node --import tsx/esm scripts/summarize-phase0-baseline.ts --audit <path> [options]',
|
|
'',
|
|
'Options:',
|
|
' --audit <path> Path to audit.log (required)',
|
|
' --since <ISO-8601|epoch_ms> Start time filter',
|
|
' --until <ISO-8601|epoch_ms> End time filter',
|
|
' --session <id[,id...]> Restrict to session IDs',
|
|
' --channel <name[,name...]> Restrict to channels',
|
|
' --sender <id[,id...]> Restrict to senders',
|
|
' --source <gateway|channel[,..]> Restrict to sources',
|
|
' --max-sessions <integer> Limit session rows in output (default: 20)',
|
|
' --max-channels <integer> Limit channel rows in output (default: 20)',
|
|
' --max-skip-reasons <integer> Limit skip reason rows in output (default: 10)',
|
|
' --format <markdown|json> Output format (default: markdown)',
|
|
' --out <path> Write output to file instead of stdout',
|
|
].join('\n');
|
|
}
|
|
|
|
function parseTime(value: string | undefined, flag: string): number | undefined {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
if (/^\d+$/.test(value)) {
|
|
const asNumber = Number(value);
|
|
if (Number.isFinite(asNumber)) {
|
|
return asNumber;
|
|
}
|
|
}
|
|
const parsed = Date.parse(value);
|
|
if (!Number.isFinite(parsed)) {
|
|
throw new Error(`Invalid ${flag} value "${value}". Use ISO-8601 or epoch milliseconds.`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function parseCsv(value: string | undefined): string[] | undefined {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
const values = value
|
|
.split(',')
|
|
.map((item) => item.trim())
|
|
.filter((item) => item.length > 0);
|
|
return values.length > 0 ? values : undefined;
|
|
}
|
|
|
|
function expandHomePath(pathValue: string): string {
|
|
if (!pathValue.startsWith('~')) {
|
|
return pathValue;
|
|
}
|
|
const home = process.env.HOME;
|
|
if (!home) {
|
|
return pathValue;
|
|
}
|
|
return resolve(home, pathValue.slice(1));
|
|
}
|
|
|
|
function parseOptionalNumber(raw: string | undefined, flag: string): number | undefined {
|
|
if (!raw) {
|
|
return undefined;
|
|
}
|
|
const parsed = Number(raw);
|
|
if (!Number.isFinite(parsed)) {
|
|
throw new Error(`Invalid ${flag} value "${raw}". Expected a number.`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function parseOptionalInteger(raw: string | undefined, flag: string): number | undefined {
|
|
const parsed = parseOptionalNumber(raw, flag);
|
|
if (parsed === undefined) {
|
|
return undefined;
|
|
}
|
|
if (!Number.isInteger(parsed)) {
|
|
throw new Error(`Invalid ${flag} value "${raw}". Expected an integer.`);
|
|
}
|
|
if (parsed < 0) {
|
|
throw new Error(`${flag} must be greater than or equal to 0.`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function parseSources(raw: string | undefined): Array<'gateway' | 'channel'> | undefined {
|
|
const values = parseCsv(raw);
|
|
if (!values) {
|
|
return undefined;
|
|
}
|
|
const parsed: Array<'gateway' | 'channel'> = [];
|
|
for (const value of values) {
|
|
if (value === 'gateway' || value === 'channel') {
|
|
parsed.push(value);
|
|
continue;
|
|
}
|
|
throw new Error(`Invalid source "${value}".`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const { values } = parseArgs({
|
|
options: {
|
|
audit: { type: 'string' },
|
|
since: { type: 'string' },
|
|
until: { type: 'string' },
|
|
session: { type: 'string' },
|
|
channel: { type: 'string' },
|
|
sender: { type: 'string' },
|
|
source: { type: 'string' },
|
|
'max-sessions': { type: 'string' },
|
|
'max-channels': { type: 'string' },
|
|
'max-skip-reasons': { type: 'string' },
|
|
format: { type: 'string' },
|
|
out: { type: 'string' },
|
|
help: { type: 'boolean', short: 'h' },
|
|
},
|
|
strict: true,
|
|
allowPositionals: false,
|
|
});
|
|
|
|
if (values.help) {
|
|
process.stdout.write(`${usage()}\n`);
|
|
return;
|
|
}
|
|
|
|
if (!values.audit) {
|
|
throw new Error('--audit is required.');
|
|
}
|
|
|
|
const format = values.format ?? 'markdown';
|
|
if (format !== 'markdown' && format !== 'json') {
|
|
throw new Error(`Invalid --format value "${format}".`);
|
|
}
|
|
|
|
const summaryOptions: Phase0BaselineSummaryOptions = {
|
|
sessionIds: parseCsv(values.session),
|
|
channels: parseCsv(values.channel),
|
|
senders: parseCsv(values.sender),
|
|
sources: parseSources(values.source),
|
|
maxSessions: parseOptionalInteger(values['max-sessions'], '--max-sessions') ?? 20,
|
|
maxChannels: parseOptionalInteger(values['max-channels'], '--max-channels') ?? 20,
|
|
maxSkipReasons: parseOptionalInteger(values['max-skip-reasons'], '--max-skip-reasons') ?? 10,
|
|
};
|
|
|
|
const startTime = parseTime(values.since, '--since');
|
|
const endTime = parseTime(values.until, '--until');
|
|
|
|
const auditPath = expandHomePath(values.audit);
|
|
const events = await queryAuditLogs(auditPath, {
|
|
start_time: startTime,
|
|
end_time: endTime,
|
|
event_types: [...DEFAULT_EVENT_TYPES],
|
|
});
|
|
|
|
const summary = summarizePhase0Baseline(events, summaryOptions);
|
|
|
|
const output = format === 'json'
|
|
? JSON.stringify({
|
|
generated_at: new Date().toISOString(),
|
|
event_count: events.length,
|
|
filters: {
|
|
since_ms: startTime,
|
|
until_ms: endTime,
|
|
},
|
|
options: summaryOptions,
|
|
summary,
|
|
}, null, 2)
|
|
: renderPhase0BaselineMarkdown(summary, summaryOptions);
|
|
|
|
if (values.out) {
|
|
await writeFile(expandHomePath(values.out), `${output}\n`, 'utf-8');
|
|
} else {
|
|
process.stdout.write(`${output}\n`);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
process.stderr.write(`${message}\n\n${usage()}\n`);
|
|
process.exitCode = 1;
|
|
});
|