watchfix 0.3.0 → 0.5.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/HELP.md +416 -0
- package/dist/cli/commands/init.js +13 -0
- package/dist/cli/commands/manual.d.ts +1 -0
- package/dist/cli/commands/manual.js +14 -0
- package/dist/cli/index.js +8 -0
- package/dist/config/schema.d.ts +227 -36
- package/dist/config/schema.js +18 -2
- package/dist/watcher/sources/file.d.ts +3 -0
- package/dist/watcher/sources/file.js +34 -1
- package/dist/watcher/sources/ndjson.d.ts +16 -0
- package/dist/watcher/sources/ndjson.js +108 -0
- package/dist/watcher/sources/types.d.ts +8 -0
- package/package.json +2 -1
package/HELP.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# watchfix - Agent Reference
|
|
2
|
+
|
|
3
|
+
> Run `watchfix manual` to display this document.
|
|
4
|
+
|
|
5
|
+
## Quick Context
|
|
6
|
+
|
|
7
|
+
watchfix is a CLI tool that watches log files, detects errors via configurable patterns, and dispatches AI agents (Claude, Gemini, or Codex) to analyze and fix them automatically. Prerequisites: a `watchfix.yaml` config file in your project root, one of the supported AI CLIs installed and in PATH, and at least one log source configured. All state is stored locally in `.watchfix/errors.db` (SQLite).
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
### Setup Commands
|
|
12
|
+
|
|
13
|
+
#### `watchfix init`
|
|
14
|
+
|
|
15
|
+
Create a `watchfix.yaml` config file in the current directory.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
watchfix init [--agent <provider>] [--force]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Flag | Description |
|
|
22
|
+
|------|-------------|
|
|
23
|
+
| `--agent <provider>` | Set agent provider: `claude`, `gemini`, or `codex` (default: `claude`) |
|
|
24
|
+
| `--force` | Overwrite existing `watchfix.yaml` |
|
|
25
|
+
|
|
26
|
+
Output: writes `watchfix.yaml` and adds `.watchfix/` to `.gitignore`.
|
|
27
|
+
|
|
28
|
+
#### `watchfix config validate`
|
|
29
|
+
|
|
30
|
+
Validate the configuration file.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
watchfix config validate [-c <path>]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Exit code 0 if valid, 1 if invalid (error message on stderr).
|
|
37
|
+
|
|
38
|
+
### Watching Commands
|
|
39
|
+
|
|
40
|
+
#### `watchfix watch`
|
|
41
|
+
|
|
42
|
+
Start watching configured log sources for errors.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
watchfix watch [--daemon] [--autonomous]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Flags | Mode | Behavior |
|
|
49
|
+
|-------|------|----------|
|
|
50
|
+
| _(none)_ | Foreground, manual | Detects errors, queues for manual `fix` |
|
|
51
|
+
| `--autonomous` | Foreground, autonomous | Detects and auto-fixes without approval |
|
|
52
|
+
| `--daemon` | Background, manual | Runs detached; logs to activity log only |
|
|
53
|
+
| `--daemon --autonomous` | Background, autonomous | Runs detached; auto-fixes in background |
|
|
54
|
+
|
|
55
|
+
Exit code 2 if a watcher is already running.
|
|
56
|
+
|
|
57
|
+
#### `watchfix stop`
|
|
58
|
+
|
|
59
|
+
Stop a running background watcher.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
watchfix stop
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Exit code 2 if no watcher is running.
|
|
66
|
+
|
|
67
|
+
#### `watchfix status`
|
|
68
|
+
|
|
69
|
+
Show watcher state and pending errors.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
watchfix status
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Output format (plain text, one error per line):
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Watcher: running (pid 12345, mode: autonomous)
|
|
79
|
+
Uptime: 2h 15m
|
|
80
|
+
|
|
81
|
+
Errors:
|
|
82
|
+
#1 [pending] TypeError: Cannot read property 'x' of null (app)
|
|
83
|
+
#3 [suggested] ReferenceError: foo is not defined (api)
|
|
84
|
+
#7 [fixing] FATAL: connection refused (db)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
When no errors exist: `No errors recorded.`
|
|
88
|
+
When watcher is not running: `Watcher: not running`
|
|
89
|
+
|
|
90
|
+
### Error Management Commands
|
|
91
|
+
|
|
92
|
+
#### `watchfix show <id>`
|
|
93
|
+
|
|
94
|
+
Show full error details and analysis.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
watchfix show <id> [--json]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
| Flag | Description |
|
|
101
|
+
|------|-------------|
|
|
102
|
+
| `--json` | Output machine-readable JSON (see JSON Output Format below) |
|
|
103
|
+
|
|
104
|
+
Without `--json`, outputs human-readable text with error metadata, stack trace, analysis, fix result, and activity log.
|
|
105
|
+
|
|
106
|
+
#### `watchfix fix [id]`
|
|
107
|
+
|
|
108
|
+
Analyze and fix an error. Without an id, requires `--all`.
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
watchfix fix <id> [-y] [--analyze-only] [--reanalyze]
|
|
112
|
+
watchfix fix --all [--confirm-each] [--analyze-only] [--reanalyze]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Flag | Description |
|
|
116
|
+
|------|-------------|
|
|
117
|
+
| `-y, --yes` | Skip confirmation prompt |
|
|
118
|
+
| `--all` | Fix all pending/suggested errors sequentially |
|
|
119
|
+
| `--confirm-each` | With `--all`: prompt before each fix |
|
|
120
|
+
| `--analyze-only` | Stop after analysis, don't apply the fix |
|
|
121
|
+
| `--reanalyze` | Force re-analysis even if already in `suggested` status |
|
|
122
|
+
|
|
123
|
+
Exit code 3 if the error is not in an actionable status or not found.
|
|
124
|
+
|
|
125
|
+
#### `watchfix ignore <id>`
|
|
126
|
+
|
|
127
|
+
Mark an error as ignored. It will not be processed further.
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
watchfix ignore <id>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Utility Commands
|
|
134
|
+
|
|
135
|
+
#### `watchfix logs`
|
|
136
|
+
|
|
137
|
+
Show the activity log.
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
watchfix logs [--tail] [-n <count>]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Flag | Description |
|
|
144
|
+
|------|-------------|
|
|
145
|
+
| `--tail` | Follow the log (stream new entries) |
|
|
146
|
+
| `-n, --lines <count>` | Number of lines to show (default: 50) |
|
|
147
|
+
|
|
148
|
+
#### `watchfix clean`
|
|
149
|
+
|
|
150
|
+
Remove old context files from `.watchfix/context/`.
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
watchfix clean [--dry-run] [--force]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Flag | Description |
|
|
157
|
+
|------|-------------|
|
|
158
|
+
| `--dry-run` | Show what would be removed without deleting |
|
|
159
|
+
| `--force` | Skip confirmation prompt |
|
|
160
|
+
|
|
161
|
+
#### `watchfix version`
|
|
162
|
+
|
|
163
|
+
Show version, Node.js version, config status, and agent provider.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
watchfix version
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### `watchfix manual`
|
|
170
|
+
|
|
171
|
+
Output this reference document to stdout.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
watchfix manual
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Global Options
|
|
178
|
+
|
|
179
|
+
These flags work with all commands:
|
|
180
|
+
|
|
181
|
+
| Flag | Description |
|
|
182
|
+
|------|-------------|
|
|
183
|
+
| `-c, --config <path>` | Use alternate config file (default: `./watchfix.yaml`) |
|
|
184
|
+
| `--verbose` | Increase output verbosity |
|
|
185
|
+
| `-q, --quiet` | Suppress non-essential output |
|
|
186
|
+
| `-h, --help` | Show help for command |
|
|
187
|
+
| `-v, --version` | Show version and exit |
|
|
188
|
+
|
|
189
|
+
## Common Workflows
|
|
190
|
+
|
|
191
|
+
### Check if watchfix is set up
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Look for config file
|
|
195
|
+
ls watchfix.yaml
|
|
196
|
+
|
|
197
|
+
# Validate it
|
|
198
|
+
watchfix config validate
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Start watching and monitor
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Start watcher in foreground
|
|
205
|
+
watchfix watch
|
|
206
|
+
|
|
207
|
+
# Or in background (Linux/macOS)
|
|
208
|
+
watchfix watch --daemon
|
|
209
|
+
|
|
210
|
+
# Poll for errors
|
|
211
|
+
watchfix status
|
|
212
|
+
|
|
213
|
+
# Stream activity log
|
|
214
|
+
watchfix logs --tail
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Investigate and fix one error
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# See what errors exist
|
|
221
|
+
watchfix status
|
|
222
|
+
|
|
223
|
+
# Get full details (machine-readable)
|
|
224
|
+
watchfix show 3 --json
|
|
225
|
+
|
|
226
|
+
# Fix it (with confirmation)
|
|
227
|
+
watchfix fix 3
|
|
228
|
+
|
|
229
|
+
# Or skip confirmation
|
|
230
|
+
watchfix fix 3 -y
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Fix all pending errors
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
watchfix fix --all
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Fully automated operation
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# Start autonomous daemon
|
|
243
|
+
watchfix watch --daemon --autonomous
|
|
244
|
+
|
|
245
|
+
# Monitor progress
|
|
246
|
+
watchfix status
|
|
247
|
+
watchfix logs --tail
|
|
248
|
+
|
|
249
|
+
# Stop when done
|
|
250
|
+
watchfix stop
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Analyze without fixing
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Get analysis only (no code changes)
|
|
257
|
+
watchfix fix 3 --analyze-only
|
|
258
|
+
|
|
259
|
+
# View the analysis
|
|
260
|
+
watchfix show 3
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Exit Codes
|
|
264
|
+
|
|
265
|
+
| Code | Constant | Meaning |
|
|
266
|
+
|------|----------|---------|
|
|
267
|
+
| 0 | `SUCCESS` | Command completed successfully |
|
|
268
|
+
| 1 | `GENERAL_ERROR` | General error (invalid config, agent failure, etc.) |
|
|
269
|
+
| 2 | `WATCHER_CONFLICT` | Watcher state conflict (already running / not running) |
|
|
270
|
+
| 3 | `NOT_ACTIONABLE` | Target not actionable (error not found, wrong status, locked) |
|
|
271
|
+
| 4 | `SCHEMA_MISMATCH` | Database schema version mismatch (requires migration) |
|
|
272
|
+
| 130 | `INTERRUPTED` | Interrupted by user (SIGINT / Ctrl+C) |
|
|
273
|
+
|
|
274
|
+
## Error Statuses
|
|
275
|
+
|
|
276
|
+
### Status Lifecycle
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
pending → analyzing → suggested → fixing → fixed
|
|
280
|
+
→ failed (after max attempts)
|
|
281
|
+
Any status → ignored (via watchfix ignore)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Status Reference
|
|
285
|
+
|
|
286
|
+
| Status | Description | Actionable? |
|
|
287
|
+
|--------|-------------|-------------|
|
|
288
|
+
| `pending` | Detected, awaiting analysis | Yes — `fix` will start analysis |
|
|
289
|
+
| `analyzing` | Agent is currently analyzing | No — locked |
|
|
290
|
+
| `suggested` | Analysis complete, awaiting fix | Yes — `fix` will apply the suggestion |
|
|
291
|
+
| `fixing` | Agent is currently applying fix | No — locked |
|
|
292
|
+
| `fixed` | Fix applied and verified | No — terminal |
|
|
293
|
+
| `failed` | Max attempts exceeded | Manual retry only (`fix <id>`, excluded from `--all`) |
|
|
294
|
+
| `ignored` | User chose to ignore | No — terminal |
|
|
295
|
+
| `resolved` | Fixed by a previous fix to another error | No — terminal |
|
|
296
|
+
| `deferred` | Non-code issue (infrastructure/config) | No — shows remediation guidance |
|
|
297
|
+
|
|
298
|
+
### Deduplication
|
|
299
|
+
|
|
300
|
+
When a new error matches an existing one (by hash):
|
|
301
|
+
- If existing is `pending`, `analyzing`, `suggested`, or `fixing`: new error is dropped
|
|
302
|
+
- If existing is `fixed`, `failed`, or `ignored`: a new entry is created (error recurred)
|
|
303
|
+
|
|
304
|
+
## JSON Output Format
|
|
305
|
+
|
|
306
|
+
`watchfix show <id> --json` returns a JSON object with this shape:
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"error": {
|
|
311
|
+
"id": 1,
|
|
312
|
+
"hash": "abc123...",
|
|
313
|
+
"source": "app",
|
|
314
|
+
"timestamp": "2026-01-29T12:00:00.000Z",
|
|
315
|
+
"errorType": "TypeError",
|
|
316
|
+
"message": "Cannot read property 'x' of null",
|
|
317
|
+
"stackTrace": "TypeError: Cannot read property...\n at foo (src/app.ts:10:5)\n ...",
|
|
318
|
+
"rawLog": "[2026-01-29 12:00:00] ERROR TypeError: Cannot read property...",
|
|
319
|
+
"status": "suggested",
|
|
320
|
+
"suggestion": "{\"summary\":\"...\",\"root_cause\":\"...\",\"suggested_fix\":\"...\"}",
|
|
321
|
+
"fixResult": null,
|
|
322
|
+
"fixAttempts": 0,
|
|
323
|
+
"lockedBy": null,
|
|
324
|
+
"lockedAt": null,
|
|
325
|
+
"createdAt": "2026-01-29T12:00:01.000Z",
|
|
326
|
+
"updatedAt": "2026-01-29T12:00:05.000Z"
|
|
327
|
+
},
|
|
328
|
+
"analysis": {
|
|
329
|
+
"summary": "Null pointer access in foo()",
|
|
330
|
+
"root_cause": "Variable 'x' is not initialized before access",
|
|
331
|
+
"suggested_fix": "Add null check before accessing property",
|
|
332
|
+
"files_to_modify": ["src/app.ts"],
|
|
333
|
+
"confidence": "high",
|
|
334
|
+
"category": "code"
|
|
335
|
+
},
|
|
336
|
+
"fixResult": null,
|
|
337
|
+
"activityLog": [
|
|
338
|
+
{
|
|
339
|
+
"id": 1,
|
|
340
|
+
"timestamp": "2026-01-29T12:00:01.000Z",
|
|
341
|
+
"action": "error_detected",
|
|
342
|
+
"error_id": 1,
|
|
343
|
+
"details": "TypeError: Cannot read property 'x' of null"
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Key Fields
|
|
350
|
+
|
|
351
|
+
- `error.status` — current lifecycle status (see Error Statuses above)
|
|
352
|
+
- `error.suggestion` — raw JSON string of agent analysis (parse it to get the `analysis` object)
|
|
353
|
+
- `error.fixResult` — raw JSON string of fix result (null if not yet fixed)
|
|
354
|
+
- `analysis` — parsed suggestion object; `null` if not yet analyzed
|
|
355
|
+
- `analysis.category` — `"code"`, `"infrastructure"`, or `"configuration"`
|
|
356
|
+
- `analysis.confidence` — `"high"`, `"medium"`, or `"low"`
|
|
357
|
+
- `analysis.remediation_guidance` — present for `deferred` (non-code) errors
|
|
358
|
+
- `fixResult` — parsed fix result object; `null` if not yet fixed
|
|
359
|
+
- `fixResult.success` — boolean indicating if verification passed
|
|
360
|
+
- `fixResult.files_changed` — array of `{ "path": "...", "change": "..." }`
|
|
361
|
+
- `activityLog` — chronological list of actions taken on this error
|
|
362
|
+
|
|
363
|
+
## Configuration Quick Reference
|
|
364
|
+
|
|
365
|
+
watchfix uses a `watchfix.yaml` file in the project root. Run `watchfix init` to generate a template.
|
|
366
|
+
|
|
367
|
+
### Key Sections
|
|
368
|
+
|
|
369
|
+
```yaml
|
|
370
|
+
project:
|
|
371
|
+
name: my-app # Project identifier
|
|
372
|
+
root: . # Project root (paths resolve relative to this)
|
|
373
|
+
|
|
374
|
+
agent:
|
|
375
|
+
provider: claude # Required: claude | gemini | codex
|
|
376
|
+
timeout: 5m # Max agent execution time (default: 5m)
|
|
377
|
+
retries: 2 # Retries on agent timeout/crash (default: 2)
|
|
378
|
+
|
|
379
|
+
logs:
|
|
380
|
+
sources: # At least one source required
|
|
381
|
+
- name: app # Source identifier
|
|
382
|
+
type: file # file | docker | command
|
|
383
|
+
path: ./logs/app.log
|
|
384
|
+
context_lines_before: 10
|
|
385
|
+
context_lines_after: 5
|
|
386
|
+
|
|
387
|
+
verification:
|
|
388
|
+
test_commands: # Run after fix, in order; stop on first failure
|
|
389
|
+
- npm run lint
|
|
390
|
+
- npm test
|
|
391
|
+
health_checks: # HTTP GET, expect 2xx
|
|
392
|
+
- http://localhost:3000/health
|
|
393
|
+
|
|
394
|
+
patterns:
|
|
395
|
+
match: # Additional error patterns
|
|
396
|
+
- "FATAL:"
|
|
397
|
+
- "regex:OOM.*killed"
|
|
398
|
+
ignore: # Patterns to skip
|
|
399
|
+
- "DeprecationWarning"
|
|
400
|
+
|
|
401
|
+
limits:
|
|
402
|
+
max_attempts_per_error: 3
|
|
403
|
+
|
|
404
|
+
cleanup:
|
|
405
|
+
context_max_age_days: 7
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Source Types
|
|
409
|
+
|
|
410
|
+
| Type | Required Fields | Description |
|
|
411
|
+
|------|----------------|-------------|
|
|
412
|
+
| `file` | `path` | Watch a log file (supports `format: ndjson` for structured JSON logs) |
|
|
413
|
+
| `docker` | `container` | Watch Docker container logs |
|
|
414
|
+
| `command` | `run`, `interval` | Run a command periodically and scan output |
|
|
415
|
+
|
|
416
|
+
For full configuration details and NDJSON options, run `watchfix init` to see the annotated template.
|
|
@@ -27,6 +27,19 @@ logs:
|
|
|
27
27
|
type: file
|
|
28
28
|
path: ./logs/app.log
|
|
29
29
|
|
|
30
|
+
# Example NDJSON source (for structured JSON logs like Pino, Bunyan, Winston):
|
|
31
|
+
# - name: app-json
|
|
32
|
+
# type: file
|
|
33
|
+
# path: ./logs/app.ndjson
|
|
34
|
+
# format: ndjson
|
|
35
|
+
# ndjson:
|
|
36
|
+
# messageField: msg # Required: field containing log message
|
|
37
|
+
# timestampField: time # Optional: field with timestamp
|
|
38
|
+
# levelField: level # Optional: field with log level
|
|
39
|
+
# levelFilter: # Optional: only process these levels
|
|
40
|
+
# - error
|
|
41
|
+
# - fatal
|
|
42
|
+
|
|
30
43
|
# Example docker source:
|
|
31
44
|
# - name: api
|
|
32
45
|
# type: docker
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const manualCommand: () => Promise<void>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { UserError } from '../../utils/errors.js';
|
|
5
|
+
export const manualCommand = async () => {
|
|
6
|
+
const helpPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'HELP.md');
|
|
7
|
+
try {
|
|
8
|
+
const content = await fs.readFile(helpPath, 'utf8');
|
|
9
|
+
process.stdout.write(content);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new UserError('HELP.md not found. This file should be included in the watchfix package.');
|
|
13
|
+
}
|
|
14
|
+
};
|
package/dist/cli/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { logsCommand } from './commands/logs.js';
|
|
|
12
12
|
import { configValidateCommand } from './commands/config.js';
|
|
13
13
|
import { cleanCommand } from './commands/clean.js';
|
|
14
14
|
import { versionCommand } from './commands/version.js';
|
|
15
|
+
import { manualCommand } from './commands/manual.js';
|
|
15
16
|
import { EXIT_CODES, InternalError, UserError } from '../utils/errors.js';
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
const pkg = require('../../package.json');
|
|
@@ -105,6 +106,12 @@ const registerCommands = (program) => {
|
|
|
105
106
|
.action(async (options) => {
|
|
106
107
|
await versionCommand(options);
|
|
107
108
|
}));
|
|
109
|
+
program
|
|
110
|
+
.command('manual')
|
|
111
|
+
.description('Show detailed reference documentation')
|
|
112
|
+
.action(async () => {
|
|
113
|
+
await manualCommand();
|
|
114
|
+
});
|
|
108
115
|
};
|
|
109
116
|
const program = new Command();
|
|
110
117
|
program
|
|
@@ -114,6 +121,7 @@ addGlobalOptions(program);
|
|
|
114
121
|
program
|
|
115
122
|
.helpOption('-h, --help', 'Show help for command')
|
|
116
123
|
.version(pkg.version ?? '0.0.0', '-v, --version', 'Show version and exit');
|
|
124
|
+
program.addHelpText('after', '\nRun "watchfix manual" for detailed reference documentation.');
|
|
117
125
|
registerCommands(program);
|
|
118
126
|
const handleError = (error) => {
|
|
119
127
|
if (error instanceof UserError) {
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -1,29 +1,76 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
declare const durationSchema: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
3
|
+
declare const ndjsonConfigSchema: z.ZodObject<{
|
|
4
|
+
messageField: z.ZodString;
|
|
5
|
+
timestampField: z.ZodOptional<z.ZodString>;
|
|
6
|
+
levelField: z.ZodOptional<z.ZodString>;
|
|
7
|
+
levelFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
messageField: string;
|
|
10
|
+
timestampField?: string | undefined;
|
|
11
|
+
levelField?: string | undefined;
|
|
12
|
+
levelFilter?: string[] | undefined;
|
|
13
|
+
}, {
|
|
14
|
+
messageField: string;
|
|
15
|
+
timestampField?: string | undefined;
|
|
16
|
+
levelField?: string | undefined;
|
|
17
|
+
levelFilter?: string[] | undefined;
|
|
18
|
+
}>;
|
|
3
19
|
declare const fileSourceSchema: z.ZodObject<{
|
|
4
20
|
name: z.ZodString;
|
|
5
21
|
type: z.ZodLiteral<"file">;
|
|
6
22
|
path: z.ZodString;
|
|
23
|
+
format: z.ZodOptional<z.ZodEnum<["text", "ndjson"]>>;
|
|
24
|
+
ndjson: z.ZodOptional<z.ZodObject<{
|
|
25
|
+
messageField: z.ZodString;
|
|
26
|
+
timestampField: z.ZodOptional<z.ZodString>;
|
|
27
|
+
levelField: z.ZodOptional<z.ZodString>;
|
|
28
|
+
levelFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
29
|
+
}, "strip", z.ZodTypeAny, {
|
|
30
|
+
messageField: string;
|
|
31
|
+
timestampField?: string | undefined;
|
|
32
|
+
levelField?: string | undefined;
|
|
33
|
+
levelFilter?: string[] | undefined;
|
|
34
|
+
}, {
|
|
35
|
+
messageField: string;
|
|
36
|
+
timestampField?: string | undefined;
|
|
37
|
+
levelField?: string | undefined;
|
|
38
|
+
levelFilter?: string[] | undefined;
|
|
39
|
+
}>>;
|
|
7
40
|
}, "strip", z.ZodTypeAny, {
|
|
8
41
|
path: string;
|
|
9
|
-
name: string;
|
|
10
42
|
type: "file";
|
|
43
|
+
name: string;
|
|
44
|
+
ndjson?: {
|
|
45
|
+
messageField: string;
|
|
46
|
+
timestampField?: string | undefined;
|
|
47
|
+
levelField?: string | undefined;
|
|
48
|
+
levelFilter?: string[] | undefined;
|
|
49
|
+
} | undefined;
|
|
50
|
+
format?: "text" | "ndjson" | undefined;
|
|
11
51
|
}, {
|
|
12
52
|
path: string;
|
|
13
|
-
name: string;
|
|
14
53
|
type: "file";
|
|
54
|
+
name: string;
|
|
55
|
+
ndjson?: {
|
|
56
|
+
messageField: string;
|
|
57
|
+
timestampField?: string | undefined;
|
|
58
|
+
levelField?: string | undefined;
|
|
59
|
+
levelFilter?: string[] | undefined;
|
|
60
|
+
} | undefined;
|
|
61
|
+
format?: "text" | "ndjson" | undefined;
|
|
15
62
|
}>;
|
|
16
63
|
declare const dockerSourceSchema: z.ZodObject<{
|
|
17
64
|
name: z.ZodString;
|
|
18
65
|
type: z.ZodLiteral<"docker">;
|
|
19
66
|
container: z.ZodString;
|
|
20
67
|
}, "strip", z.ZodTypeAny, {
|
|
21
|
-
name: string;
|
|
22
68
|
type: "docker";
|
|
69
|
+
name: string;
|
|
23
70
|
container: string;
|
|
24
71
|
}, {
|
|
25
|
-
name: string;
|
|
26
72
|
type: "docker";
|
|
73
|
+
name: string;
|
|
27
74
|
container: string;
|
|
28
75
|
}>;
|
|
29
76
|
declare const commandSourceSchema: z.ZodObject<{
|
|
@@ -32,13 +79,13 @@ declare const commandSourceSchema: z.ZodObject<{
|
|
|
32
79
|
run: z.ZodString;
|
|
33
80
|
interval: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
34
81
|
}, "strip", z.ZodTypeAny, {
|
|
35
|
-
name: string;
|
|
36
82
|
type: "command";
|
|
83
|
+
name: string;
|
|
37
84
|
run: string;
|
|
38
85
|
interval: string;
|
|
39
86
|
}, {
|
|
40
|
-
name: string;
|
|
41
87
|
type: "command";
|
|
88
|
+
name: string;
|
|
42
89
|
run: string;
|
|
43
90
|
interval: string;
|
|
44
91
|
}>;
|
|
@@ -46,25 +93,56 @@ declare const logSourceSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
46
93
|
name: z.ZodString;
|
|
47
94
|
type: z.ZodLiteral<"file">;
|
|
48
95
|
path: z.ZodString;
|
|
96
|
+
format: z.ZodOptional<z.ZodEnum<["text", "ndjson"]>>;
|
|
97
|
+
ndjson: z.ZodOptional<z.ZodObject<{
|
|
98
|
+
messageField: z.ZodString;
|
|
99
|
+
timestampField: z.ZodOptional<z.ZodString>;
|
|
100
|
+
levelField: z.ZodOptional<z.ZodString>;
|
|
101
|
+
levelFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
102
|
+
}, "strip", z.ZodTypeAny, {
|
|
103
|
+
messageField: string;
|
|
104
|
+
timestampField?: string | undefined;
|
|
105
|
+
levelField?: string | undefined;
|
|
106
|
+
levelFilter?: string[] | undefined;
|
|
107
|
+
}, {
|
|
108
|
+
messageField: string;
|
|
109
|
+
timestampField?: string | undefined;
|
|
110
|
+
levelField?: string | undefined;
|
|
111
|
+
levelFilter?: string[] | undefined;
|
|
112
|
+
}>>;
|
|
49
113
|
}, "strip", z.ZodTypeAny, {
|
|
50
114
|
path: string;
|
|
51
|
-
name: string;
|
|
52
115
|
type: "file";
|
|
116
|
+
name: string;
|
|
117
|
+
ndjson?: {
|
|
118
|
+
messageField: string;
|
|
119
|
+
timestampField?: string | undefined;
|
|
120
|
+
levelField?: string | undefined;
|
|
121
|
+
levelFilter?: string[] | undefined;
|
|
122
|
+
} | undefined;
|
|
123
|
+
format?: "text" | "ndjson" | undefined;
|
|
53
124
|
}, {
|
|
54
125
|
path: string;
|
|
55
|
-
name: string;
|
|
56
126
|
type: "file";
|
|
127
|
+
name: string;
|
|
128
|
+
ndjson?: {
|
|
129
|
+
messageField: string;
|
|
130
|
+
timestampField?: string | undefined;
|
|
131
|
+
levelField?: string | undefined;
|
|
132
|
+
levelFilter?: string[] | undefined;
|
|
133
|
+
} | undefined;
|
|
134
|
+
format?: "text" | "ndjson" | undefined;
|
|
57
135
|
}>, z.ZodObject<{
|
|
58
136
|
name: z.ZodString;
|
|
59
137
|
type: z.ZodLiteral<"docker">;
|
|
60
138
|
container: z.ZodString;
|
|
61
139
|
}, "strip", z.ZodTypeAny, {
|
|
62
|
-
name: string;
|
|
63
140
|
type: "docker";
|
|
141
|
+
name: string;
|
|
64
142
|
container: string;
|
|
65
143
|
}, {
|
|
66
|
-
name: string;
|
|
67
144
|
type: "docker";
|
|
145
|
+
name: string;
|
|
68
146
|
container: string;
|
|
69
147
|
}>, z.ZodObject<{
|
|
70
148
|
name: z.ZodString;
|
|
@@ -72,13 +150,13 @@ declare const logSourceSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
72
150
|
run: z.ZodString;
|
|
73
151
|
interval: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
74
152
|
}, "strip", z.ZodTypeAny, {
|
|
75
|
-
name: string;
|
|
76
153
|
type: "command";
|
|
154
|
+
name: string;
|
|
77
155
|
run: string;
|
|
78
156
|
interval: string;
|
|
79
157
|
}, {
|
|
80
|
-
name: string;
|
|
81
158
|
type: "command";
|
|
159
|
+
name: string;
|
|
82
160
|
run: string;
|
|
83
161
|
interval: string;
|
|
84
162
|
}>]>;
|
|
@@ -117,29 +195,60 @@ declare const configSchema: z.ZodObject<{
|
|
|
117
195
|
stderr_is_progress?: boolean | undefined;
|
|
118
196
|
}>;
|
|
119
197
|
logs: z.ZodObject<{
|
|
120
|
-
sources: z.ZodEffects<z.ZodArray<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
198
|
+
sources: z.ZodEffects<z.ZodEffects<z.ZodArray<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
121
199
|
name: z.ZodString;
|
|
122
200
|
type: z.ZodLiteral<"file">;
|
|
123
201
|
path: z.ZodString;
|
|
202
|
+
format: z.ZodOptional<z.ZodEnum<["text", "ndjson"]>>;
|
|
203
|
+
ndjson: z.ZodOptional<z.ZodObject<{
|
|
204
|
+
messageField: z.ZodString;
|
|
205
|
+
timestampField: z.ZodOptional<z.ZodString>;
|
|
206
|
+
levelField: z.ZodOptional<z.ZodString>;
|
|
207
|
+
levelFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
208
|
+
}, "strip", z.ZodTypeAny, {
|
|
209
|
+
messageField: string;
|
|
210
|
+
timestampField?: string | undefined;
|
|
211
|
+
levelField?: string | undefined;
|
|
212
|
+
levelFilter?: string[] | undefined;
|
|
213
|
+
}, {
|
|
214
|
+
messageField: string;
|
|
215
|
+
timestampField?: string | undefined;
|
|
216
|
+
levelField?: string | undefined;
|
|
217
|
+
levelFilter?: string[] | undefined;
|
|
218
|
+
}>>;
|
|
124
219
|
}, "strip", z.ZodTypeAny, {
|
|
125
220
|
path: string;
|
|
126
|
-
name: string;
|
|
127
221
|
type: "file";
|
|
222
|
+
name: string;
|
|
223
|
+
ndjson?: {
|
|
224
|
+
messageField: string;
|
|
225
|
+
timestampField?: string | undefined;
|
|
226
|
+
levelField?: string | undefined;
|
|
227
|
+
levelFilter?: string[] | undefined;
|
|
228
|
+
} | undefined;
|
|
229
|
+
format?: "text" | "ndjson" | undefined;
|
|
128
230
|
}, {
|
|
129
231
|
path: string;
|
|
130
|
-
name: string;
|
|
131
232
|
type: "file";
|
|
233
|
+
name: string;
|
|
234
|
+
ndjson?: {
|
|
235
|
+
messageField: string;
|
|
236
|
+
timestampField?: string | undefined;
|
|
237
|
+
levelField?: string | undefined;
|
|
238
|
+
levelFilter?: string[] | undefined;
|
|
239
|
+
} | undefined;
|
|
240
|
+
format?: "text" | "ndjson" | undefined;
|
|
132
241
|
}>, z.ZodObject<{
|
|
133
242
|
name: z.ZodString;
|
|
134
243
|
type: z.ZodLiteral<"docker">;
|
|
135
244
|
container: z.ZodString;
|
|
136
245
|
}, "strip", z.ZodTypeAny, {
|
|
137
|
-
name: string;
|
|
138
246
|
type: "docker";
|
|
247
|
+
name: string;
|
|
139
248
|
container: string;
|
|
140
249
|
}, {
|
|
141
|
-
name: string;
|
|
142
250
|
type: "docker";
|
|
251
|
+
name: string;
|
|
143
252
|
container: string;
|
|
144
253
|
}>, z.ZodObject<{
|
|
145
254
|
name: z.ZodString;
|
|
@@ -147,39 +256,93 @@ declare const configSchema: z.ZodObject<{
|
|
|
147
256
|
run: z.ZodString;
|
|
148
257
|
interval: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
149
258
|
}, "strip", z.ZodTypeAny, {
|
|
150
|
-
name: string;
|
|
151
259
|
type: "command";
|
|
260
|
+
name: string;
|
|
152
261
|
run: string;
|
|
153
262
|
interval: string;
|
|
154
263
|
}, {
|
|
155
|
-
name: string;
|
|
156
264
|
type: "command";
|
|
265
|
+
name: string;
|
|
157
266
|
run: string;
|
|
158
267
|
interval: string;
|
|
159
268
|
}>]>, "many">, ({
|
|
160
269
|
path: string;
|
|
161
|
-
name: string;
|
|
162
270
|
type: "file";
|
|
163
|
-
} | {
|
|
164
271
|
name: string;
|
|
272
|
+
ndjson?: {
|
|
273
|
+
messageField: string;
|
|
274
|
+
timestampField?: string | undefined;
|
|
275
|
+
levelField?: string | undefined;
|
|
276
|
+
levelFilter?: string[] | undefined;
|
|
277
|
+
} | undefined;
|
|
278
|
+
format?: "text" | "ndjson" | undefined;
|
|
279
|
+
} | {
|
|
165
280
|
type: "docker";
|
|
281
|
+
name: string;
|
|
166
282
|
container: string;
|
|
167
283
|
} | {
|
|
168
|
-
name: string;
|
|
169
284
|
type: "command";
|
|
285
|
+
name: string;
|
|
170
286
|
run: string;
|
|
171
287
|
interval: string;
|
|
172
288
|
})[], ({
|
|
173
289
|
path: string;
|
|
174
|
-
name: string;
|
|
175
290
|
type: "file";
|
|
291
|
+
name: string;
|
|
292
|
+
ndjson?: {
|
|
293
|
+
messageField: string;
|
|
294
|
+
timestampField?: string | undefined;
|
|
295
|
+
levelField?: string | undefined;
|
|
296
|
+
levelFilter?: string[] | undefined;
|
|
297
|
+
} | undefined;
|
|
298
|
+
format?: "text" | "ndjson" | undefined;
|
|
299
|
+
} | {
|
|
300
|
+
type: "docker";
|
|
301
|
+
name: string;
|
|
302
|
+
container: string;
|
|
176
303
|
} | {
|
|
304
|
+
type: "command";
|
|
305
|
+
name: string;
|
|
306
|
+
run: string;
|
|
307
|
+
interval: string;
|
|
308
|
+
})[]>, ({
|
|
309
|
+
path: string;
|
|
310
|
+
type: "file";
|
|
177
311
|
name: string;
|
|
312
|
+
ndjson?: {
|
|
313
|
+
messageField: string;
|
|
314
|
+
timestampField?: string | undefined;
|
|
315
|
+
levelField?: string | undefined;
|
|
316
|
+
levelFilter?: string[] | undefined;
|
|
317
|
+
} | undefined;
|
|
318
|
+
format?: "text" | "ndjson" | undefined;
|
|
319
|
+
} | {
|
|
178
320
|
type: "docker";
|
|
321
|
+
name: string;
|
|
179
322
|
container: string;
|
|
180
323
|
} | {
|
|
324
|
+
type: "command";
|
|
181
325
|
name: string;
|
|
326
|
+
run: string;
|
|
327
|
+
interval: string;
|
|
328
|
+
})[], ({
|
|
329
|
+
path: string;
|
|
330
|
+
type: "file";
|
|
331
|
+
name: string;
|
|
332
|
+
ndjson?: {
|
|
333
|
+
messageField: string;
|
|
334
|
+
timestampField?: string | undefined;
|
|
335
|
+
levelField?: string | undefined;
|
|
336
|
+
levelFilter?: string[] | undefined;
|
|
337
|
+
} | undefined;
|
|
338
|
+
format?: "text" | "ndjson" | undefined;
|
|
339
|
+
} | {
|
|
340
|
+
type: "docker";
|
|
341
|
+
name: string;
|
|
342
|
+
container: string;
|
|
343
|
+
} | {
|
|
182
344
|
type: "command";
|
|
345
|
+
name: string;
|
|
183
346
|
run: string;
|
|
184
347
|
interval: string;
|
|
185
348
|
})[]>;
|
|
@@ -189,15 +352,22 @@ declare const configSchema: z.ZodObject<{
|
|
|
189
352
|
}, "strip", z.ZodTypeAny, {
|
|
190
353
|
sources: ({
|
|
191
354
|
path: string;
|
|
192
|
-
name: string;
|
|
193
355
|
type: "file";
|
|
194
|
-
} | {
|
|
195
356
|
name: string;
|
|
357
|
+
ndjson?: {
|
|
358
|
+
messageField: string;
|
|
359
|
+
timestampField?: string | undefined;
|
|
360
|
+
levelField?: string | undefined;
|
|
361
|
+
levelFilter?: string[] | undefined;
|
|
362
|
+
} | undefined;
|
|
363
|
+
format?: "text" | "ndjson" | undefined;
|
|
364
|
+
} | {
|
|
196
365
|
type: "docker";
|
|
366
|
+
name: string;
|
|
197
367
|
container: string;
|
|
198
368
|
} | {
|
|
199
|
-
name: string;
|
|
200
369
|
type: "command";
|
|
370
|
+
name: string;
|
|
201
371
|
run: string;
|
|
202
372
|
interval: string;
|
|
203
373
|
})[];
|
|
@@ -207,15 +377,22 @@ declare const configSchema: z.ZodObject<{
|
|
|
207
377
|
}, {
|
|
208
378
|
sources: ({
|
|
209
379
|
path: string;
|
|
210
|
-
name: string;
|
|
211
380
|
type: "file";
|
|
212
|
-
} | {
|
|
213
381
|
name: string;
|
|
382
|
+
ndjson?: {
|
|
383
|
+
messageField: string;
|
|
384
|
+
timestampField?: string | undefined;
|
|
385
|
+
levelField?: string | undefined;
|
|
386
|
+
levelFilter?: string[] | undefined;
|
|
387
|
+
} | undefined;
|
|
388
|
+
format?: "text" | "ndjson" | undefined;
|
|
389
|
+
} | {
|
|
214
390
|
type: "docker";
|
|
391
|
+
name: string;
|
|
215
392
|
container: string;
|
|
216
393
|
} | {
|
|
217
|
-
name: string;
|
|
218
394
|
type: "command";
|
|
395
|
+
name: string;
|
|
219
396
|
run: string;
|
|
220
397
|
interval: string;
|
|
221
398
|
})[];
|
|
@@ -295,15 +472,22 @@ declare const configSchema: z.ZodObject<{
|
|
|
295
472
|
logs: {
|
|
296
473
|
sources: ({
|
|
297
474
|
path: string;
|
|
298
|
-
name: string;
|
|
299
475
|
type: "file";
|
|
300
|
-
} | {
|
|
301
476
|
name: string;
|
|
477
|
+
ndjson?: {
|
|
478
|
+
messageField: string;
|
|
479
|
+
timestampField?: string | undefined;
|
|
480
|
+
levelField?: string | undefined;
|
|
481
|
+
levelFilter?: string[] | undefined;
|
|
482
|
+
} | undefined;
|
|
483
|
+
format?: "text" | "ndjson" | undefined;
|
|
484
|
+
} | {
|
|
302
485
|
type: "docker";
|
|
486
|
+
name: string;
|
|
303
487
|
container: string;
|
|
304
488
|
} | {
|
|
305
|
-
name: string;
|
|
306
489
|
type: "command";
|
|
490
|
+
name: string;
|
|
307
491
|
run: string;
|
|
308
492
|
interval: string;
|
|
309
493
|
})[];
|
|
@@ -349,15 +533,22 @@ declare const configSchema: z.ZodObject<{
|
|
|
349
533
|
logs: {
|
|
350
534
|
sources: ({
|
|
351
535
|
path: string;
|
|
352
|
-
name: string;
|
|
353
536
|
type: "file";
|
|
354
|
-
} | {
|
|
355
537
|
name: string;
|
|
538
|
+
ndjson?: {
|
|
539
|
+
messageField: string;
|
|
540
|
+
timestampField?: string | undefined;
|
|
541
|
+
levelField?: string | undefined;
|
|
542
|
+
levelFilter?: string[] | undefined;
|
|
543
|
+
} | undefined;
|
|
544
|
+
format?: "text" | "ndjson" | undefined;
|
|
545
|
+
} | {
|
|
356
546
|
type: "docker";
|
|
547
|
+
name: string;
|
|
357
548
|
container: string;
|
|
358
549
|
} | {
|
|
359
|
-
name: string;
|
|
360
550
|
type: "command";
|
|
551
|
+
name: string;
|
|
361
552
|
run: string;
|
|
362
553
|
interval: string;
|
|
363
554
|
})[];
|
|
@@ -389,5 +580,5 @@ declare const configSchema: z.ZodObject<{
|
|
|
389
580
|
} | undefined;
|
|
390
581
|
}>;
|
|
391
582
|
type Config = z.infer<typeof configSchema>;
|
|
392
|
-
export { commandSourceSchema, configSchema, dockerSourceSchema, durationSchema, fileSourceSchema, logSourceSchema, patternSchema, };
|
|
583
|
+
export { commandSourceSchema, configSchema, dockerSourceSchema, durationSchema, fileSourceSchema, logSourceSchema, ndjsonConfigSchema, patternSchema, };
|
|
393
584
|
export type { Config };
|
package/dist/config/schema.js
CHANGED
|
@@ -17,10 +17,18 @@ const durationSchema = z
|
|
|
17
17
|
};
|
|
18
18
|
return amount * msByUnit[unit] <= MAX_DURATION_MS;
|
|
19
19
|
}, 'Duration cannot exceed 24 hours');
|
|
20
|
+
const ndjsonConfigSchema = z.object({
|
|
21
|
+
messageField: z.string().min(1),
|
|
22
|
+
timestampField: z.string().min(1).optional(),
|
|
23
|
+
levelField: z.string().min(1).optional(),
|
|
24
|
+
levelFilter: z.array(z.string().min(1)).optional(),
|
|
25
|
+
});
|
|
20
26
|
const fileSourceSchema = z.object({
|
|
21
27
|
name: z.string().min(1),
|
|
22
28
|
type: z.literal('file'),
|
|
23
29
|
path: z.string().min(1),
|
|
30
|
+
format: z.enum(['text', 'ndjson']).optional(),
|
|
31
|
+
ndjson: ndjsonConfigSchema.optional(),
|
|
24
32
|
});
|
|
25
33
|
const dockerSourceSchema = z.object({
|
|
26
34
|
name: z.string().min(1),
|
|
@@ -59,7 +67,15 @@ const configSchema = z.object({
|
|
|
59
67
|
.refine((sources) => {
|
|
60
68
|
const names = sources.map((source) => source.name);
|
|
61
69
|
return names.length === new Set(names).size;
|
|
62
|
-
}, { message: 'Log source names must be unique' })
|
|
70
|
+
}, { message: 'Log source names must be unique' })
|
|
71
|
+
.refine((sources) => {
|
|
72
|
+
return sources.every((source) => {
|
|
73
|
+
if (source.type === 'file' && source.format === 'ndjson') {
|
|
74
|
+
return source.ndjson !== undefined;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
}, { message: 'ndjson config is required when format is "ndjson"' }),
|
|
63
79
|
context_lines_before: z.number().int().min(0).default(10),
|
|
64
80
|
context_lines_after: z.number().int().min(0).default(5),
|
|
65
81
|
max_line_buffer: z.number().int().min(100).default(10000),
|
|
@@ -102,4 +118,4 @@ const configSchema = z.object({
|
|
|
102
118
|
})
|
|
103
119
|
.default({}),
|
|
104
120
|
});
|
|
105
|
-
export { commandSourceSchema, configSchema, dockerSourceSchema, durationSchema, fileSourceSchema, logSourceSchema, patternSchema, };
|
|
121
|
+
export { commandSourceSchema, configSchema, dockerSourceSchema, durationSchema, fileSourceSchema, logSourceSchema, ndjsonConfigSchema, patternSchema, };
|
|
@@ -9,6 +9,8 @@ export declare class FileSource implements LogSource {
|
|
|
9
9
|
private readonly filePath;
|
|
10
10
|
private readonly logger;
|
|
11
11
|
private readonly emitter;
|
|
12
|
+
private readonly format;
|
|
13
|
+
private readonly ndjsonConfig;
|
|
12
14
|
private watcher;
|
|
13
15
|
private position;
|
|
14
16
|
private partialLine;
|
|
@@ -26,5 +28,6 @@ export declare class FileSource implements LogSource {
|
|
|
26
28
|
private readLoop;
|
|
27
29
|
private readNewLines;
|
|
28
30
|
private processChunk;
|
|
31
|
+
private handleLine;
|
|
29
32
|
}
|
|
30
33
|
export {};
|
|
@@ -3,11 +3,14 @@ import path from 'node:path';
|
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
4
|
import chokidar from 'chokidar';
|
|
5
5
|
import { Logger } from '../../utils/logger.js';
|
|
6
|
+
import { parseNdjsonLine } from './ndjson.js';
|
|
6
7
|
export class FileSource {
|
|
7
8
|
config;
|
|
8
9
|
filePath;
|
|
9
10
|
logger;
|
|
10
11
|
emitter;
|
|
12
|
+
format;
|
|
13
|
+
ndjsonConfig;
|
|
11
14
|
watcher = null;
|
|
12
15
|
position = 0;
|
|
13
16
|
partialLine = '';
|
|
@@ -23,6 +26,8 @@ export class FileSource {
|
|
|
23
26
|
this.logger =
|
|
24
27
|
options?.logger ?? new Logger({ terminalEnabled: false, verbosity: 'normal' });
|
|
25
28
|
this.emitter = new EventEmitter();
|
|
29
|
+
this.format = config.format ?? 'text';
|
|
30
|
+
this.ndjsonConfig = config.ndjson ?? null;
|
|
26
31
|
}
|
|
27
32
|
async start() {
|
|
28
33
|
if (this.watcher) {
|
|
@@ -164,12 +169,40 @@ export class FileSource {
|
|
|
164
169
|
const lines = combined.split('\n');
|
|
165
170
|
this.partialLine = lines.pop() ?? '';
|
|
166
171
|
for (const rawLine of lines) {
|
|
167
|
-
|
|
172
|
+
this.handleLine(rawLine.replace(/\r$/, ''));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
handleLine(line) {
|
|
176
|
+
if (this.format !== 'ndjson' || !this.ndjsonConfig) {
|
|
168
177
|
this.emitter.emit('line', {
|
|
169
178
|
source: this.config.name,
|
|
170
179
|
line,
|
|
171
180
|
timestamp: new Date(),
|
|
172
181
|
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const result = parseNdjsonLine(line, this.ndjsonConfig);
|
|
185
|
+
if (result.success) {
|
|
186
|
+
this.emitter.emit('line', {
|
|
187
|
+
source: this.config.name,
|
|
188
|
+
line: result.data.message,
|
|
189
|
+
timestamp: result.data.timestamp,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Handle different failure reasons
|
|
194
|
+
if (result.reason === 'filtered') {
|
|
195
|
+
// Line was filtered by level - silently skip
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (result.reason === 'missing_message') {
|
|
199
|
+
this.logger.debug(`NDJSON line missing message field "${this.ndjsonConfig.messageField}": ${line.slice(0, 100)}`);
|
|
173
200
|
}
|
|
201
|
+
// For parse errors or missing message, fall back to emitting raw line
|
|
202
|
+
this.emitter.emit('line', {
|
|
203
|
+
source: this.config.name,
|
|
204
|
+
line,
|
|
205
|
+
timestamp: new Date(),
|
|
206
|
+
});
|
|
174
207
|
}
|
|
175
208
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { NdjsonConfig } from './types.js';
|
|
2
|
+
export type ParsedNdjsonLine = {
|
|
3
|
+
message: string;
|
|
4
|
+
timestamp: Date;
|
|
5
|
+
};
|
|
6
|
+
export type NdjsonParseResult = {
|
|
7
|
+
success: true;
|
|
8
|
+
data: ParsedNdjsonLine;
|
|
9
|
+
} | {
|
|
10
|
+
success: false;
|
|
11
|
+
reason: 'parse_error' | 'missing_message' | 'filtered';
|
|
12
|
+
};
|
|
13
|
+
export declare function getNestedValue(obj: unknown, path: string): unknown;
|
|
14
|
+
export declare function parseTimestamp(value: unknown): Date | null;
|
|
15
|
+
export declare function shouldProcessLevel(value: unknown, levelFilter: string[] | undefined): boolean;
|
|
16
|
+
export declare function parseNdjsonLine(line: string, config: NdjsonConfig): NdjsonParseResult;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export function getNestedValue(obj, path) {
|
|
2
|
+
const parts = path.split('.');
|
|
3
|
+
let current = obj;
|
|
4
|
+
for (const part of parts) {
|
|
5
|
+
if (current === null || current === undefined) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
if (typeof current !== 'object') {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
current = current[part];
|
|
12
|
+
}
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
export function parseTimestamp(value) {
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
const date = new Date(value);
|
|
21
|
+
if (!Number.isNaN(date.getTime())) {
|
|
22
|
+
return date;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'number') {
|
|
27
|
+
// Handle Unix timestamps in seconds or milliseconds
|
|
28
|
+
// If the number is less than 10^12, treat as seconds; otherwise as milliseconds
|
|
29
|
+
const MS_THRESHOLD = 1e12;
|
|
30
|
+
const timestamp = value < MS_THRESHOLD ? value * 1000 : value;
|
|
31
|
+
const date = new Date(timestamp);
|
|
32
|
+
if (!Number.isNaN(date.getTime())) {
|
|
33
|
+
return date;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export function shouldProcessLevel(value, levelFilter) {
|
|
40
|
+
if (!levelFilter || levelFilter.length === 0) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (value === null || value === undefined) {
|
|
44
|
+
// If no level is present but filter is configured, skip the line
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// Handle string levels (most common)
|
|
48
|
+
if (typeof value === 'string') {
|
|
49
|
+
const lowerValue = value.toLowerCase();
|
|
50
|
+
return levelFilter.some((filter) => filter.toLowerCase() === lowerValue);
|
|
51
|
+
}
|
|
52
|
+
// Handle numeric levels (Bunyan style: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal)
|
|
53
|
+
if (typeof value === 'number') {
|
|
54
|
+
const bunyanLevels = {
|
|
55
|
+
10: 'trace',
|
|
56
|
+
20: 'debug',
|
|
57
|
+
30: 'info',
|
|
58
|
+
40: 'warn',
|
|
59
|
+
50: 'error',
|
|
60
|
+
60: 'fatal',
|
|
61
|
+
};
|
|
62
|
+
const levelName = bunyanLevels[value];
|
|
63
|
+
if (levelName) {
|
|
64
|
+
return levelFilter.some((filter) => filter.toLowerCase() === levelName.toLowerCase());
|
|
65
|
+
}
|
|
66
|
+
// Unknown numeric level - don't match
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
export function parseNdjsonLine(line, config) {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(line);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return { success: false, reason: 'parse_error' };
|
|
78
|
+
}
|
|
79
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
80
|
+
return { success: false, reason: 'parse_error' };
|
|
81
|
+
}
|
|
82
|
+
// Check level filter first (before extracting message)
|
|
83
|
+
if (config.levelField) {
|
|
84
|
+
const levelValue = getNestedValue(parsed, config.levelField);
|
|
85
|
+
if (!shouldProcessLevel(levelValue, config.levelFilter)) {
|
|
86
|
+
return { success: false, reason: 'filtered' };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Extract message
|
|
90
|
+
const messageValue = getNestedValue(parsed, config.messageField);
|
|
91
|
+
if (messageValue === null || messageValue === undefined) {
|
|
92
|
+
return { success: false, reason: 'missing_message' };
|
|
93
|
+
}
|
|
94
|
+
const message = String(messageValue);
|
|
95
|
+
// Extract timestamp
|
|
96
|
+
let timestamp = new Date();
|
|
97
|
+
if (config.timestampField) {
|
|
98
|
+
const timestampValue = getNestedValue(parsed, config.timestampField);
|
|
99
|
+
const parsedTimestamp = parseTimestamp(timestampValue);
|
|
100
|
+
if (parsedTimestamp) {
|
|
101
|
+
timestamp = parsedTimestamp;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
success: true,
|
|
106
|
+
data: { message, timestamp },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -3,10 +3,18 @@ export type LogEvent = {
|
|
|
3
3
|
line: string;
|
|
4
4
|
timestamp: Date;
|
|
5
5
|
};
|
|
6
|
+
export type NdjsonConfig = {
|
|
7
|
+
messageField: string;
|
|
8
|
+
timestampField?: string;
|
|
9
|
+
levelField?: string;
|
|
10
|
+
levelFilter?: string[];
|
|
11
|
+
};
|
|
6
12
|
export type FileSourceConfig = {
|
|
7
13
|
name: string;
|
|
8
14
|
type: 'file';
|
|
9
15
|
path: string;
|
|
16
|
+
format?: 'text' | 'ndjson';
|
|
17
|
+
ndjson?: NdjsonConfig;
|
|
10
18
|
};
|
|
11
19
|
export type DockerSourceConfig = {
|
|
12
20
|
name: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "watchfix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI tool that watches logs, detects errors, and dispatches AI agents to fix them",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"dist",
|
|
31
31
|
"LICENSE",
|
|
32
32
|
"README.md",
|
|
33
|
+
"HELP.md",
|
|
33
34
|
"package.json"
|
|
34
35
|
],
|
|
35
36
|
"dependencies": {
|