watchfix 0.1.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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/agents/base.d.ts +19 -0
- package/dist/agents/base.js +140 -0
- package/dist/agents/claude.d.ts +11 -0
- package/dist/agents/claude.js +6 -0
- package/dist/agents/codex.d.ts +11 -0
- package/dist/agents/codex.js +6 -0
- package/dist/agents/defaults.d.ts +21 -0
- package/dist/agents/defaults.js +21 -0
- package/dist/agents/gemini.d.ts +11 -0
- package/dist/agents/gemini.js +6 -0
- package/dist/agents/index.d.ts +19 -0
- package/dist/agents/index.js +63 -0
- package/dist/agents/types.d.ts +22 -0
- package/dist/agents/types.js +1 -0
- package/dist/cli/commands/clean.d.ts +9 -0
- package/dist/cli/commands/clean.js +173 -0
- package/dist/cli/commands/config.d.ts +7 -0
- package/dist/cli/commands/config.js +134 -0
- package/dist/cli/commands/fix.d.ts +12 -0
- package/dist/cli/commands/fix.js +391 -0
- package/dist/cli/commands/ignore.d.ts +7 -0
- package/dist/cli/commands/ignore.js +74 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.js +115 -0
- package/dist/cli/commands/logs.d.ts +9 -0
- package/dist/cli/commands/logs.js +106 -0
- package/dist/cli/commands/show.d.ts +8 -0
- package/dist/cli/commands/show.js +165 -0
- package/dist/cli/commands/status.d.ts +7 -0
- package/dist/cli/commands/status.js +110 -0
- package/dist/cli/commands/stop.d.ts +7 -0
- package/dist/cli/commands/stop.js +106 -0
- package/dist/cli/commands/version.d.ts +7 -0
- package/dist/cli/commands/version.js +36 -0
- package/dist/cli/commands/watch.d.ts +10 -0
- package/dist/cli/commands/watch.js +204 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +152 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.js +96 -0
- package/dist/config/schema.d.ts +375 -0
- package/dist/config/schema.js +99 -0
- package/dist/db/index.d.ts +15 -0
- package/dist/db/index.js +71 -0
- package/dist/db/queries.d.ts +45 -0
- package/dist/db/queries.js +111 -0
- package/dist/db/schema.d.ts +4 -0
- package/dist/db/schema.js +84 -0
- package/dist/fixer/context.d.ts +9 -0
- package/dist/fixer/context.js +361 -0
- package/dist/fixer/index.d.ts +37 -0
- package/dist/fixer/index.js +398 -0
- package/dist/fixer/lock.d.ts +7 -0
- package/dist/fixer/lock.js +49 -0
- package/dist/fixer/output.d.ts +21 -0
- package/dist/fixer/output.js +108 -0
- package/dist/fixer/queue.d.ts +15 -0
- package/dist/fixer/queue.js +53 -0
- package/dist/fixer/verifier.d.ts +44 -0
- package/dist/fixer/verifier.js +133 -0
- package/dist/utils/daemon.d.ts +22 -0
- package/dist/utils/daemon.js +143 -0
- package/dist/utils/duration.d.ts +2 -0
- package/dist/utils/duration.js +31 -0
- package/dist/utils/errors.d.ts +17 -0
- package/dist/utils/errors.js +20 -0
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +18 -0
- package/dist/utils/http.d.ts +6 -0
- package/dist/utils/http.js +61 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.js +85 -0
- package/dist/utils/process.d.ts +16 -0
- package/dist/utils/process.js +146 -0
- package/dist/watcher/index.d.ts +55 -0
- package/dist/watcher/index.js +234 -0
- package/dist/watcher/parser.d.ts +42 -0
- package/dist/watcher/parser.js +162 -0
- package/dist/watcher/patterns.d.ts +5 -0
- package/dist/watcher/patterns.js +92 -0
- package/dist/watcher/sources/command.d.ts +27 -0
- package/dist/watcher/sources/command.js +143 -0
- package/dist/watcher/sources/docker.d.ts +28 -0
- package/dist/watcher/sources/docker.js +183 -0
- package/dist/watcher/sources/file.d.ts +30 -0
- package/dist/watcher/sources/file.js +177 -0
- package/dist/watcher/sources/types.d.ts +27 -0
- package/dist/watcher/sources/types.js +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
const CLI_CHECK_TIMEOUT_MS = 5000;
|
|
3
|
+
const KILL_GRACE_PERIOD_MS = 5000;
|
|
4
|
+
function firstLine(text) {
|
|
5
|
+
return text.trim().split('\n')[0] ?? '';
|
|
6
|
+
}
|
|
7
|
+
function formatSpawnFailure(command, stderr, status) {
|
|
8
|
+
const trimmed = stderr.trim();
|
|
9
|
+
if (trimmed) {
|
|
10
|
+
return trimmed;
|
|
11
|
+
}
|
|
12
|
+
return `'${command}' exited with code ${status}`;
|
|
13
|
+
}
|
|
14
|
+
export function checkCliExists(command) {
|
|
15
|
+
try {
|
|
16
|
+
const result = spawnSync(command, ['--version'], {
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
timeout: CLI_CHECK_TIMEOUT_MS,
|
|
19
|
+
shell: process.platform === 'win32',
|
|
20
|
+
});
|
|
21
|
+
if (result.error) {
|
|
22
|
+
if (result.error.code === 'ENOENT') {
|
|
23
|
+
return { exists: false, error: `'${command}' not found in PATH` };
|
|
24
|
+
}
|
|
25
|
+
return { exists: false, error: result.error.message };
|
|
26
|
+
}
|
|
27
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
28
|
+
return {
|
|
29
|
+
exists: false,
|
|
30
|
+
error: formatSpawnFailure(command, result.stderr ?? '', result.status),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const stdout = result.stdout ?? '';
|
|
34
|
+
const version = firstLine(stdout);
|
|
35
|
+
return version
|
|
36
|
+
? { exists: true, version }
|
|
37
|
+
: { exists: true, version: firstLine(result.stderr ?? '') };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
return { exists: false, error: error.message };
|
|
42
|
+
}
|
|
43
|
+
return { exists: false, error: 'Unknown error' };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function spawnWithTimeout(command, args, options = {}, timeoutMs) {
|
|
47
|
+
return await new Promise((resolve) => {
|
|
48
|
+
const child = spawn(command, args, {
|
|
49
|
+
...options,
|
|
50
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
|
+
});
|
|
52
|
+
let stdout = '';
|
|
53
|
+
let stderr = '';
|
|
54
|
+
let timedOut = false;
|
|
55
|
+
let resolved = false;
|
|
56
|
+
const finalize = (result) => {
|
|
57
|
+
if (resolved) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
resolved = true;
|
|
61
|
+
resolve(result);
|
|
62
|
+
};
|
|
63
|
+
const timeoutId = setTimeout(() => {
|
|
64
|
+
timedOut = true;
|
|
65
|
+
child.kill('SIGTERM');
|
|
66
|
+
const killTimer = setTimeout(() => {
|
|
67
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
68
|
+
child.kill('SIGKILL');
|
|
69
|
+
}
|
|
70
|
+
}, KILL_GRACE_PERIOD_MS);
|
|
71
|
+
child.once('close', () => clearTimeout(killTimer));
|
|
72
|
+
}, timeoutMs);
|
|
73
|
+
if (child.stdout) {
|
|
74
|
+
child.stdout.on('data', (data) => {
|
|
75
|
+
stdout += String(data);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (child.stderr) {
|
|
79
|
+
child.stderr.on('data', (data) => {
|
|
80
|
+
stderr += String(data);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
child.on('close', (code) => {
|
|
84
|
+
clearTimeout(timeoutId);
|
|
85
|
+
finalize({
|
|
86
|
+
success: code === 0 && !timedOut,
|
|
87
|
+
stdout,
|
|
88
|
+
stderr,
|
|
89
|
+
exitCode: code ?? -1,
|
|
90
|
+
timedOut,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
child.on('error', (error) => {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
finalize({
|
|
96
|
+
success: false,
|
|
97
|
+
stdout,
|
|
98
|
+
stderr: error.message,
|
|
99
|
+
exitCode: -1,
|
|
100
|
+
timedOut: false,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function readCommandLine(pid) {
|
|
106
|
+
if (process.platform === 'win32') {
|
|
107
|
+
const output = spawnSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'CommandLine', '/format:list'], { encoding: 'utf8', timeout: CLI_CHECK_TIMEOUT_MS });
|
|
108
|
+
// Check for wmic failure - fall through to PowerShell
|
|
109
|
+
if (!output.error && output.status === 0) {
|
|
110
|
+
const text = `${output.stdout ?? ''}`;
|
|
111
|
+
const line = text
|
|
112
|
+
.split('\n')
|
|
113
|
+
.map((value) => value.trim())
|
|
114
|
+
.find((value) => value.toLowerCase().startsWith('commandline='));
|
|
115
|
+
if (line) {
|
|
116
|
+
return line.slice('commandline='.length);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// PowerShell fallback (always reachable now)
|
|
120
|
+
const powerShell = spawnSync('powershell', ['-Command', `(Get-Process -Id ${pid}).CommandLine`], { encoding: 'utf8', timeout: CLI_CHECK_TIMEOUT_MS });
|
|
121
|
+
return `${powerShell.stdout ?? ''}${powerShell.stderr ?? ''}`.trim();
|
|
122
|
+
}
|
|
123
|
+
const result = spawnSync('ps', ['-p', `${pid}`, '-o', 'args='], {
|
|
124
|
+
encoding: 'utf8',
|
|
125
|
+
timeout: CLI_CHECK_TIMEOUT_MS,
|
|
126
|
+
});
|
|
127
|
+
return `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
|
128
|
+
}
|
|
129
|
+
export function isOurProcess(pid, expectedRoot) {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const cmdline = readCommandLine(pid);
|
|
138
|
+
if (!cmdline) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return cmdline.includes('watchfix') && cmdline.includes(expectedRoot);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Config } from '../config/schema.js';
|
|
2
|
+
import type { Database } from '../db/index.js';
|
|
3
|
+
import type { ErrorStatus } from '../utils/errors.js';
|
|
4
|
+
import { Logger } from '../utils/logger.js';
|
|
5
|
+
import { type ParsedError } from './parser.js';
|
|
6
|
+
import type { LogEvent } from './sources/types.js';
|
|
7
|
+
export type WatcherDetectedEvent = {
|
|
8
|
+
errorId: number;
|
|
9
|
+
error: ParsedError;
|
|
10
|
+
previousId?: number;
|
|
11
|
+
previousStatus?: ErrorStatus;
|
|
12
|
+
};
|
|
13
|
+
export type WatcherDeduplicatedEvent = {
|
|
14
|
+
errorId: number;
|
|
15
|
+
error: ParsedError;
|
|
16
|
+
status: ErrorStatus;
|
|
17
|
+
};
|
|
18
|
+
export type WatcherSourceErrorEvent = {
|
|
19
|
+
source: string;
|
|
20
|
+
error: Error;
|
|
21
|
+
};
|
|
22
|
+
type WatcherEventName = 'error_detected' | 'error_deduplicated' | 'source_error';
|
|
23
|
+
type WatcherEventPayloads = {
|
|
24
|
+
error_detected: WatcherDetectedEvent;
|
|
25
|
+
error_deduplicated: WatcherDeduplicatedEvent;
|
|
26
|
+
source_error: WatcherSourceErrorEvent;
|
|
27
|
+
};
|
|
28
|
+
export declare class WatcherOrchestrator {
|
|
29
|
+
private readonly config;
|
|
30
|
+
private readonly db;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
private readonly sources;
|
|
33
|
+
private readonly eventQueue;
|
|
34
|
+
private readonly parser;
|
|
35
|
+
private readonly emitter;
|
|
36
|
+
private readonly errorQueue;
|
|
37
|
+
private processingPromise;
|
|
38
|
+
private started;
|
|
39
|
+
private noSourceWarningTimer;
|
|
40
|
+
constructor(config: Config, db: Database, options?: {
|
|
41
|
+
logger?: Logger;
|
|
42
|
+
});
|
|
43
|
+
on<T extends WatcherEventName>(event: T, handler: (payload: WatcherEventPayloads[T]) => void): void;
|
|
44
|
+
emit(event: LogEvent): void;
|
|
45
|
+
start(): Promise<void>;
|
|
46
|
+
stop(): Promise<void>;
|
|
47
|
+
private createSources;
|
|
48
|
+
private buildSource;
|
|
49
|
+
private startEventProcessor;
|
|
50
|
+
private queueErrorHandling;
|
|
51
|
+
private handleParsedError;
|
|
52
|
+
private startNoSourceWarnings;
|
|
53
|
+
private stopNoSourceWarnings;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { getErrorByHash, insertError, logActivity } from '../db/queries.js';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import { ErrorParser } from './parser.js';
|
|
5
|
+
import { CommandSource } from './sources/command.js';
|
|
6
|
+
import { DockerSource } from './sources/docker.js';
|
|
7
|
+
import { FileSource } from './sources/file.js';
|
|
8
|
+
const ACTIVE_STATUSES = new Set([
|
|
9
|
+
'pending',
|
|
10
|
+
'analyzing',
|
|
11
|
+
'suggested',
|
|
12
|
+
'fixing',
|
|
13
|
+
]);
|
|
14
|
+
const NO_SOURCE_WARN_INTERVAL_MS = 60_000;
|
|
15
|
+
class AsyncQueue {
|
|
16
|
+
buffer = [];
|
|
17
|
+
resolvers = [];
|
|
18
|
+
closed = false;
|
|
19
|
+
push(value) {
|
|
20
|
+
if (this.closed) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const resolver = this.resolvers.shift();
|
|
24
|
+
if (resolver) {
|
|
25
|
+
resolver({ value, done: false });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.buffer.push(value);
|
|
29
|
+
}
|
|
30
|
+
close() {
|
|
31
|
+
this.closed = true;
|
|
32
|
+
while (this.resolvers.length > 0) {
|
|
33
|
+
const resolver = this.resolvers.shift();
|
|
34
|
+
if (resolver) {
|
|
35
|
+
resolver({ value: undefined, done: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async *[Symbol.asyncIterator]() {
|
|
40
|
+
while (true) {
|
|
41
|
+
if (this.buffer.length > 0) {
|
|
42
|
+
yield this.buffer.shift();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (this.closed) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const result = await new Promise((resolve) => {
|
|
49
|
+
this.resolvers.push(resolve);
|
|
50
|
+
});
|
|
51
|
+
if (result.done) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
yield result.value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
class SerialTaskQueue {
|
|
59
|
+
pending = Promise.resolve();
|
|
60
|
+
run(task) {
|
|
61
|
+
const next = this.pending.then(() => task());
|
|
62
|
+
this.pending = next.then(() => undefined, () => undefined);
|
|
63
|
+
return next;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class WatcherOrchestrator {
|
|
67
|
+
config;
|
|
68
|
+
db;
|
|
69
|
+
logger;
|
|
70
|
+
sources = [];
|
|
71
|
+
eventQueue = new AsyncQueue();
|
|
72
|
+
parser;
|
|
73
|
+
emitter = new EventEmitter();
|
|
74
|
+
errorQueue = new SerialTaskQueue();
|
|
75
|
+
processingPromise = null;
|
|
76
|
+
started = false;
|
|
77
|
+
noSourceWarningTimer = null;
|
|
78
|
+
constructor(config, db, options) {
|
|
79
|
+
this.config = config;
|
|
80
|
+
this.db = db;
|
|
81
|
+
this.logger =
|
|
82
|
+
options?.logger ?? new Logger({ rootDir: config.project.root });
|
|
83
|
+
this.parser = new ErrorParser({
|
|
84
|
+
contextLinesBefore: config.logs.context_lines_before,
|
|
85
|
+
contextLinesAfter: config.logs.context_lines_after,
|
|
86
|
+
customMatch: config.patterns.match,
|
|
87
|
+
customIgnore: config.patterns.ignore,
|
|
88
|
+
logger: this.logger,
|
|
89
|
+
onError: (error) => this.queueErrorHandling(error),
|
|
90
|
+
});
|
|
91
|
+
this.createSources(config.logs.sources);
|
|
92
|
+
}
|
|
93
|
+
on(event, handler) {
|
|
94
|
+
this.emitter.on(event, handler);
|
|
95
|
+
}
|
|
96
|
+
emit(event) {
|
|
97
|
+
this.eventQueue.push(event);
|
|
98
|
+
}
|
|
99
|
+
async start() {
|
|
100
|
+
if (this.started) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.started = true;
|
|
104
|
+
this.processingPromise = this.startEventProcessor();
|
|
105
|
+
const results = await Promise.allSettled(this.sources.map((entry) => entry.source.start()));
|
|
106
|
+
let startedCount = 0;
|
|
107
|
+
results.forEach((result, index) => {
|
|
108
|
+
if (result.status === 'fulfilled') {
|
|
109
|
+
startedCount += 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const entry = this.sources[index];
|
|
113
|
+
const error = result.reason instanceof Error ? result.reason : new Error(String(result.reason));
|
|
114
|
+
this.logger.error(`Failed to start log source '${entry.name}': ${error.message}`);
|
|
115
|
+
this.emitter.emit('source_error', { source: entry.name, error });
|
|
116
|
+
});
|
|
117
|
+
if (startedCount === 0) {
|
|
118
|
+
this.logger.error('All log sources failed to start. Watcher will continue running.');
|
|
119
|
+
this.startNoSourceWarnings();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.stopNoSourceWarnings();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async stop() {
|
|
126
|
+
if (!this.started) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.started = false;
|
|
130
|
+
this.stopNoSourceWarnings();
|
|
131
|
+
await Promise.allSettled(this.sources.map((entry) => entry.source.stop()));
|
|
132
|
+
this.eventQueue.close();
|
|
133
|
+
if (this.processingPromise) {
|
|
134
|
+
await this.processingPromise;
|
|
135
|
+
this.processingPromise = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
createSources(configs) {
|
|
139
|
+
configs.forEach((config) => {
|
|
140
|
+
const source = this.buildSource(config);
|
|
141
|
+
source.on('line', (event) => this.eventQueue.push(event));
|
|
142
|
+
this.sources.push({ name: config.name, source });
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
buildSource(config) {
|
|
146
|
+
switch (config.type) {
|
|
147
|
+
case 'file':
|
|
148
|
+
return new FileSource(config, { logger: this.logger });
|
|
149
|
+
case 'docker':
|
|
150
|
+
return new DockerSource(config, { logger: this.logger });
|
|
151
|
+
case 'command':
|
|
152
|
+
return new CommandSource(config, {
|
|
153
|
+
logger: this.logger,
|
|
154
|
+
maxLineBuffer: this.config.logs.max_line_buffer,
|
|
155
|
+
});
|
|
156
|
+
default: {
|
|
157
|
+
const exhaustive = config;
|
|
158
|
+
throw new Error(`Unsupported log source type: ${String(exhaustive)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async startEventProcessor() {
|
|
163
|
+
for await (const event of this.eventQueue) {
|
|
164
|
+
try {
|
|
165
|
+
await this.parser.processLine(event);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
this.logger.error(`Failed to process log event: ${message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
queueErrorHandling(error) {
|
|
174
|
+
return this.errorQueue.run(() => this.handleParsedError(error));
|
|
175
|
+
}
|
|
176
|
+
async handleParsedError(error) {
|
|
177
|
+
try {
|
|
178
|
+
const existing = getErrorByHash(this.db, error.hash);
|
|
179
|
+
if (existing && ACTIVE_STATUSES.has(existing.status)) {
|
|
180
|
+
logActivity(this.db, 'error_deduplicated', existing.id, `status=${existing.status}`);
|
|
181
|
+
this.logger.info(`Deduplicated error ${error.hash} (status=${existing.status})`);
|
|
182
|
+
this.emitter.emit('error_deduplicated', {
|
|
183
|
+
errorId: existing.id,
|
|
184
|
+
error,
|
|
185
|
+
status: existing.status,
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const newId = insertError(this.db, {
|
|
190
|
+
hash: error.hash,
|
|
191
|
+
source: error.source,
|
|
192
|
+
timestamp: error.timestamp,
|
|
193
|
+
errorType: error.errorType,
|
|
194
|
+
message: error.message,
|
|
195
|
+
stackTrace: error.stackTrace,
|
|
196
|
+
rawLog: error.rawLog,
|
|
197
|
+
status: 'pending',
|
|
198
|
+
fixAttempts: 0,
|
|
199
|
+
suggestion: null,
|
|
200
|
+
fixResult: null,
|
|
201
|
+
});
|
|
202
|
+
const details = existing ? `recurrence_of=${existing.id}` : undefined;
|
|
203
|
+
logActivity(this.db, 'error_detected', newId, details);
|
|
204
|
+
this.logger.info(existing
|
|
205
|
+
? `Recurring error detected (${newId}) from ${error.source}`
|
|
206
|
+
: `New error detected (${newId}) from ${error.source}`);
|
|
207
|
+
this.emitter.emit('error_detected', {
|
|
208
|
+
errorId: newId,
|
|
209
|
+
error,
|
|
210
|
+
previousId: existing?.id,
|
|
211
|
+
previousStatus: existing?.status,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
216
|
+
this.logger.error(`Failed to persist parsed error: ${message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
startNoSourceWarnings() {
|
|
220
|
+
if (this.noSourceWarningTimer) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
this.noSourceWarningTimer = setInterval(() => {
|
|
224
|
+
this.logger.warn('No log sources are running. Still waiting for sources.');
|
|
225
|
+
}, NO_SOURCE_WARN_INTERVAL_MS);
|
|
226
|
+
}
|
|
227
|
+
stopNoSourceWarnings() {
|
|
228
|
+
if (!this.noSourceWarningTimer) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
clearInterval(this.noSourceWarningTimer);
|
|
232
|
+
this.noSourceWarningTimer = null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Logger } from '../utils/logger.js';
|
|
2
|
+
export type LogEvent = {
|
|
3
|
+
source: string;
|
|
4
|
+
line: string;
|
|
5
|
+
timestamp: Date;
|
|
6
|
+
};
|
|
7
|
+
export type ParsedError = {
|
|
8
|
+
source: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
errorType: string;
|
|
11
|
+
message: string;
|
|
12
|
+
stackTrace: string | null;
|
|
13
|
+
rawLog: string;
|
|
14
|
+
hash: string;
|
|
15
|
+
};
|
|
16
|
+
export declare class ErrorParser {
|
|
17
|
+
private readonly contextLinesBefore;
|
|
18
|
+
private readonly contextLinesAfter;
|
|
19
|
+
private readonly onError;
|
|
20
|
+
private readonly customMatch?;
|
|
21
|
+
private readonly customIgnore?;
|
|
22
|
+
private readonly logger?;
|
|
23
|
+
private readonly flushTimeoutMs;
|
|
24
|
+
private beforeBuffer;
|
|
25
|
+
private current?;
|
|
26
|
+
private flushTimer?;
|
|
27
|
+
private lastSource?;
|
|
28
|
+
constructor(options?: {
|
|
29
|
+
contextLinesBefore?: number;
|
|
30
|
+
contextLinesAfter?: number;
|
|
31
|
+
onError?: (error: ParsedError) => void | Promise<void>;
|
|
32
|
+
customMatch?: string[];
|
|
33
|
+
customIgnore?: string[];
|
|
34
|
+
logger?: Logger;
|
|
35
|
+
flushTimeoutMs?: number;
|
|
36
|
+
});
|
|
37
|
+
processLine(event: LogEvent): Promise<void>;
|
|
38
|
+
private startError;
|
|
39
|
+
private recordBeforeContext;
|
|
40
|
+
private resetFlushTimer;
|
|
41
|
+
private flushCurrent;
|
|
42
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { computeErrorHash } from '../utils/hash.js';
|
|
2
|
+
import { extractErrorType, matchesErrorPattern } from './patterns.js';
|
|
3
|
+
const MAX_LINE_LENGTH = 64 * 1024;
|
|
4
|
+
const TRUNCATION_SUFFIX = '... [truncated]';
|
|
5
|
+
const FLUSH_TIMEOUT_MS = 100;
|
|
6
|
+
const CONTINUATION_PATTERNS = [
|
|
7
|
+
/^at\s/,
|
|
8
|
+
/^\s+(at|in)\s/,
|
|
9
|
+
/^\s+File "/,
|
|
10
|
+
/^\s+\d+:\d+/,
|
|
11
|
+
/^\s+\.\.\./,
|
|
12
|
+
];
|
|
13
|
+
const isContinuationLine = (line) => CONTINUATION_PATTERNS.some((pattern) => pattern.test(line));
|
|
14
|
+
const truncateLine = (line) => {
|
|
15
|
+
if (line.length <= MAX_LINE_LENGTH) {
|
|
16
|
+
return line;
|
|
17
|
+
}
|
|
18
|
+
return `${line.slice(0, MAX_LINE_LENGTH)}${TRUNCATION_SUFFIX}`;
|
|
19
|
+
};
|
|
20
|
+
export class ErrorParser {
|
|
21
|
+
contextLinesBefore;
|
|
22
|
+
contextLinesAfter;
|
|
23
|
+
onError;
|
|
24
|
+
customMatch;
|
|
25
|
+
customIgnore;
|
|
26
|
+
logger;
|
|
27
|
+
flushTimeoutMs;
|
|
28
|
+
beforeBuffer = [];
|
|
29
|
+
current;
|
|
30
|
+
flushTimer;
|
|
31
|
+
lastSource;
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.contextLinesBefore = options?.contextLinesBefore ?? 10;
|
|
34
|
+
this.contextLinesAfter = options?.contextLinesAfter ?? 5;
|
|
35
|
+
this.onError = options?.onError ?? (() => undefined);
|
|
36
|
+
this.customMatch = options?.customMatch;
|
|
37
|
+
this.customIgnore = options?.customIgnore;
|
|
38
|
+
this.logger = options?.logger;
|
|
39
|
+
this.flushTimeoutMs = options?.flushTimeoutMs ?? FLUSH_TIMEOUT_MS;
|
|
40
|
+
}
|
|
41
|
+
async processLine(event) {
|
|
42
|
+
if (this.lastSource && this.lastSource !== event.source) {
|
|
43
|
+
if (this.current) {
|
|
44
|
+
await this.flushCurrent('source_change');
|
|
45
|
+
}
|
|
46
|
+
this.beforeBuffer = [];
|
|
47
|
+
}
|
|
48
|
+
this.lastSource = event.source;
|
|
49
|
+
const line = truncateLine(event.line);
|
|
50
|
+
const isError = matchesErrorPattern(line, this.customMatch, this.customIgnore);
|
|
51
|
+
if (!this.current) {
|
|
52
|
+
if (isError) {
|
|
53
|
+
this.startError(event, line);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.recordBeforeContext(line);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (isError) {
|
|
61
|
+
await this.flushCurrent('new_error');
|
|
62
|
+
this.startError(event, line);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (this.current.phase === 'stack') {
|
|
66
|
+
if (isContinuationLine(line)) {
|
|
67
|
+
this.current.stackLines.push(line);
|
|
68
|
+
this.current.rawLines.push(line);
|
|
69
|
+
this.resetFlushTimer();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (this.contextLinesAfter === 0) {
|
|
73
|
+
await this.flushCurrent('after_complete');
|
|
74
|
+
this.recordBeforeContext(line);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.current.phase = 'after';
|
|
78
|
+
this.current.afterRemaining = this.contextLinesAfter;
|
|
79
|
+
this.current.rawLines.push(line);
|
|
80
|
+
this.recordBeforeContext(line);
|
|
81
|
+
this.current.afterRemaining -= 1;
|
|
82
|
+
this.resetFlushTimer();
|
|
83
|
+
if (this.current.afterRemaining <= 0) {
|
|
84
|
+
await this.flushCurrent('after_complete');
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.current.rawLines.push(line);
|
|
89
|
+
this.recordBeforeContext(line);
|
|
90
|
+
this.current.afterRemaining -= 1;
|
|
91
|
+
this.resetFlushTimer();
|
|
92
|
+
if (this.current.afterRemaining <= 0) {
|
|
93
|
+
await this.flushCurrent('after_complete');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
startError(event, line) {
|
|
97
|
+
const contextBefore = this.contextLinesBefore > 0 ? [...this.beforeBuffer] : [];
|
|
98
|
+
this.beforeBuffer = [];
|
|
99
|
+
const errorType = extractErrorType(line);
|
|
100
|
+
this.current = {
|
|
101
|
+
source: event.source,
|
|
102
|
+
timestamp: event.timestamp,
|
|
103
|
+
message: line,
|
|
104
|
+
errorType,
|
|
105
|
+
rawLines: [...contextBefore, line],
|
|
106
|
+
stackLines: [],
|
|
107
|
+
phase: 'stack',
|
|
108
|
+
afterRemaining: this.contextLinesAfter,
|
|
109
|
+
};
|
|
110
|
+
this.resetFlushTimer();
|
|
111
|
+
}
|
|
112
|
+
recordBeforeContext(line) {
|
|
113
|
+
if (this.contextLinesBefore === 0) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.beforeBuffer.push(line);
|
|
117
|
+
if (this.beforeBuffer.length > this.contextLinesBefore) {
|
|
118
|
+
this.beforeBuffer.shift();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
resetFlushTimer() {
|
|
122
|
+
if (!this.current) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (this.flushTimer) {
|
|
126
|
+
clearTimeout(this.flushTimer);
|
|
127
|
+
}
|
|
128
|
+
this.flushTimer = setTimeout(() => {
|
|
129
|
+
void this.flushCurrent('timeout');
|
|
130
|
+
}, this.flushTimeoutMs);
|
|
131
|
+
}
|
|
132
|
+
async flushCurrent(reason) {
|
|
133
|
+
if (!this.current) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (this.flushTimer) {
|
|
137
|
+
clearTimeout(this.flushTimer);
|
|
138
|
+
this.flushTimer = undefined;
|
|
139
|
+
}
|
|
140
|
+
const current = this.current;
|
|
141
|
+
this.current = undefined;
|
|
142
|
+
const stackTrace = current.stackLines.length > 0 ? current.stackLines.join('\n') : null;
|
|
143
|
+
const rawLog = current.rawLines.join('\n');
|
|
144
|
+
const hash = computeErrorHash(current.source, current.errorType, current.message);
|
|
145
|
+
try {
|
|
146
|
+
await this.onError({
|
|
147
|
+
source: current.source,
|
|
148
|
+
timestamp: current.timestamp.toISOString(),
|
|
149
|
+
errorType: current.errorType,
|
|
150
|
+
message: current.message,
|
|
151
|
+
stackTrace,
|
|
152
|
+
rawLog,
|
|
153
|
+
hash,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (this.logger) {
|
|
158
|
+
this.logger.error(`ErrorParser failed to emit error (${reason}): ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
declare const BUILTIN_MATCH_PATTERNS: ReadonlyArray<RegExp>;
|
|
2
|
+
declare const BUILTIN_IGNORE_PATTERNS: ReadonlyArray<RegExp>;
|
|
3
|
+
declare const matchesErrorPattern: (line: string, customMatch?: string[], customIgnore?: string[]) => boolean;
|
|
4
|
+
declare const extractErrorType: (message: string) => string;
|
|
5
|
+
export { BUILTIN_IGNORE_PATTERNS, BUILTIN_MATCH_PATTERNS, extractErrorType, matchesErrorPattern, };
|