@getmikk/watcher 1.2.0 → 1.3.1
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/README.md +265 -0
- package/package.json +31 -27
- package/src/daemon.ts +232 -232
- package/src/file-watcher.ts +93 -93
- package/src/incremental-analyzer.ts +192 -192
- package/src/index.ts +4 -4
- package/src/types.ts +25 -25
- package/tests/smoke.test.ts +5 -5
- package/tsconfig.json +14 -14
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# @getmikk/watcher
|
|
2
|
+
|
|
3
|
+
> Chokidar-powered file watcher daemon with incremental analysis, debouncing, race-condition protection, and atomic lock file updates.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@getmikk/watcher)
|
|
6
|
+
[](../../LICENSE)
|
|
7
|
+
|
|
8
|
+
`@getmikk/watcher` keeps the `mikk.lock.json` file in sync with your codebase in real time. When files change, the watcher debounces events, incrementally re-parses only the affected files, updates the dependency graph, recomputes Merkle hashes, and writes the lock file atomically — all without requiring a full re-analysis.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @getmikk/watcher
|
|
16
|
+
# or
|
|
17
|
+
bun add @getmikk/watcher
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Peer dependency:** `@getmikk/core`
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { WatcherDaemon } from '@getmikk/watcher'
|
|
28
|
+
|
|
29
|
+
const daemon = new WatcherDaemon({
|
|
30
|
+
projectRoot: process.cwd(),
|
|
31
|
+
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
32
|
+
exclude: ['node_modules', 'dist', '.mikk'],
|
|
33
|
+
debounceMs: 100,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
daemon.on((event) => {
|
|
37
|
+
switch (event.type) {
|
|
38
|
+
case 'file:changed':
|
|
39
|
+
console.log(`Changed: ${event.path}`)
|
|
40
|
+
break
|
|
41
|
+
case 'graph:updated':
|
|
42
|
+
console.log('Dependency graph rebuilt')
|
|
43
|
+
break
|
|
44
|
+
case 'sync:clean':
|
|
45
|
+
console.log('Lock file is in sync')
|
|
46
|
+
break
|
|
47
|
+
case 'sync:drifted':
|
|
48
|
+
console.log('Lock file has drifted')
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
await daemon.start()
|
|
54
|
+
// Lock file is now kept in sync automatically
|
|
55
|
+
|
|
56
|
+
// Later...
|
|
57
|
+
await daemon.stop()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Architecture
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Filesystem Events (Chokidar)
|
|
66
|
+
│
|
|
67
|
+
▼
|
|
68
|
+
┌─────────────┐
|
|
69
|
+
│ FileWatcher │ ← Hash computation, deduplication
|
|
70
|
+
└──────┬──────┘
|
|
71
|
+
│ FileChangeEvent[]
|
|
72
|
+
▼
|
|
73
|
+
┌──────────────────┐
|
|
74
|
+
│ WatcherDaemon │ ← Debouncing (100ms), batching
|
|
75
|
+
└──────┬───────────┘
|
|
76
|
+
│ Batch of events
|
|
77
|
+
▼
|
|
78
|
+
┌─────────────────────┐
|
|
79
|
+
│ IncrementalAnalyzer │ ← Re-parse, graph patch, hash update
|
|
80
|
+
└──────────┬──────────┘
|
|
81
|
+
│
|
|
82
|
+
▼
|
|
83
|
+
Atomic lock file write
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## API Reference
|
|
89
|
+
|
|
90
|
+
### WatcherDaemon
|
|
91
|
+
|
|
92
|
+
The main entry point — a long-running process that keeps the lock file in sync.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { WatcherDaemon } from '@getmikk/watcher'
|
|
96
|
+
|
|
97
|
+
const daemon = new WatcherDaemon(config)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**`WatcherConfig`:**
|
|
101
|
+
|
|
102
|
+
| Field | Type | Default | Description |
|
|
103
|
+
|-------|------|---------|-------------|
|
|
104
|
+
| `projectRoot` | `string` | — | Absolute path to the project |
|
|
105
|
+
| `include` | `string[]` | `['**/*.ts']` | Glob patterns for watched files |
|
|
106
|
+
| `exclude` | `string[]` | `['node_modules']` | Glob patterns to ignore |
|
|
107
|
+
| `debounceMs` | `number` | `100` | Debounce window in milliseconds |
|
|
108
|
+
|
|
109
|
+
**Methods:**
|
|
110
|
+
|
|
111
|
+
| Method | Description |
|
|
112
|
+
|--------|-------------|
|
|
113
|
+
| `start()` | Start watching. Creates PID file at `.mikk/watcher.pid` for single-instance enforcement |
|
|
114
|
+
| `stop()` | Stop watching. Cleans up PID file |
|
|
115
|
+
| `on(handler)` | Register event handler |
|
|
116
|
+
|
|
117
|
+
**Features:**
|
|
118
|
+
|
|
119
|
+
- **Debouncing** — Batches rapid file changes (e.g., save-all) into a single analysis pass
|
|
120
|
+
- **PID file** — Prevents multiple watcher instances via `.mikk/watcher.pid`
|
|
121
|
+
- **Atomic writes** — Lock file is written atomically to prevent corruption
|
|
122
|
+
- **Sync state** — Emits `sync:clean` or `sync:drifted` after each cycle
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### FileWatcher
|
|
127
|
+
|
|
128
|
+
Lower-level wrapper around Chokidar with hash-based change detection:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { FileWatcher } from '@getmikk/watcher'
|
|
132
|
+
|
|
133
|
+
const watcher = new FileWatcher(config)
|
|
134
|
+
|
|
135
|
+
watcher.on((event) => {
|
|
136
|
+
console.log(event.type) // 'added' | 'changed' | 'deleted'
|
|
137
|
+
console.log(event.path) // Absolute file path
|
|
138
|
+
console.log(event.oldHash) // Previous content hash (undefined for 'added')
|
|
139
|
+
console.log(event.newHash) // New content hash (undefined for 'deleted')
|
|
140
|
+
console.log(event.timestamp) // Event timestamp
|
|
141
|
+
console.log(event.affectedModuleIds) // Modules containing this file
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await watcher.start()
|
|
145
|
+
|
|
146
|
+
// Seed with known hashes to detect only actual content changes
|
|
147
|
+
watcher.setHash('/src/index.ts', 'abc123...')
|
|
148
|
+
|
|
149
|
+
await watcher.stop()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Hash-based deduplication:** Even if the OS reports a file change, the watcher computes a SHA-256 hash and only emits an event if the content actually changed. This prevents redundant re-analysis from editor auto-saves or format-on-save.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
### IncrementalAnalyzer
|
|
157
|
+
|
|
158
|
+
Incrementally updates the dependency graph and lock file for a batch of changed files:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { IncrementalAnalyzer } from '@getmikk/watcher'
|
|
162
|
+
|
|
163
|
+
const analyzer = new IncrementalAnalyzer(graph, lock, contract, projectRoot)
|
|
164
|
+
|
|
165
|
+
const result = await analyzer.analyzeBatch(events)
|
|
166
|
+
|
|
167
|
+
console.log(result.graph) // Updated DependencyGraph
|
|
168
|
+
console.log(result.lock) // Updated MikkLock
|
|
169
|
+
console.log(result.impactResult) // ImpactResult from @getmikk/core
|
|
170
|
+
console.log(result.mode) // 'incremental' | 'full'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**How it works:**
|
|
174
|
+
|
|
175
|
+
1. **Small batches (≤15 files)** → Incremental mode:
|
|
176
|
+
- Re-parse only changed files
|
|
177
|
+
- Patch the existing graph (remove old nodes/edges, add new ones)
|
|
178
|
+
- Recompute affected hashes only
|
|
179
|
+
- Run impact analysis on changed nodes
|
|
180
|
+
|
|
181
|
+
2. **Large batches (>15 files)** → Full re-analysis:
|
|
182
|
+
- Re-parse all files from scratch
|
|
183
|
+
- Rebuild entire graph
|
|
184
|
+
- Recompute all hashes
|
|
185
|
+
|
|
186
|
+
**Race-condition protection:** After parsing a file, the analyzer re-hashes it. If the hash changed during parsing (the file was modified again), it retries up to 3 times before falling back to the latest parsed version.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### Events
|
|
191
|
+
|
|
192
|
+
All events emitted through the `on()` handler:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
type WatcherEvent =
|
|
196
|
+
| { type: 'file:changed'; event: FileChangeEvent }
|
|
197
|
+
| { type: 'module:updated'; moduleId: string }
|
|
198
|
+
| { type: 'graph:updated'; stats: { nodes: number; edges: number } }
|
|
199
|
+
| { type: 'sync:clean' }
|
|
200
|
+
| { type: 'sync:drifted'; driftedModules: string[] }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**`FileChangeEvent`:**
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
type FileChangeEvent = {
|
|
207
|
+
type: 'added' | 'changed' | 'deleted'
|
|
208
|
+
path: string
|
|
209
|
+
oldHash?: string
|
|
210
|
+
newHash?: string
|
|
211
|
+
timestamp: number
|
|
212
|
+
affectedModuleIds: string[]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Usage with the CLI
|
|
219
|
+
|
|
220
|
+
The `mikk watch` command starts the watcher daemon:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
mikk watch
|
|
224
|
+
# Watching src/**/*.ts, src/**/*.tsx...
|
|
225
|
+
# [sync:clean] Lock file is up to date
|
|
226
|
+
# [file:changed] src/auth/login.ts
|
|
227
|
+
# [graph:updated] 142 nodes, 87 edges
|
|
228
|
+
# [sync:clean] Lock file updated
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Press `Ctrl+C` to stop.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Single-Instance Enforcement
|
|
236
|
+
|
|
237
|
+
The daemon writes a PID file to `.mikk/watcher.pid` on start and removes it on stop. If another watcher is already running, `start()` will throw an error. This prevents multiple watchers from fighting over the lock file.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
try {
|
|
241
|
+
await daemon.start()
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (err.message.includes('already running')) {
|
|
244
|
+
console.log('Another watcher is already running')
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Types
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import type {
|
|
255
|
+
FileChangeEvent,
|
|
256
|
+
WatcherConfig,
|
|
257
|
+
WatcherEvent,
|
|
258
|
+
} from '@getmikk/watcher'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
[Apache-2.0](../../LICENSE)
|
package/package.json
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@getmikk/watcher",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
},
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
}
|
|
27
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@getmikk/watcher",
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Ansh-dhanani/mikk"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"test": "bun test",
|
|
21
|
+
"dev": "tsc --watch"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@getmikk/core": "^1.3.1",
|
|
25
|
+
"chokidar": "^4.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"typescript": "^5.7.0",
|
|
29
|
+
"@types/node": "^22.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|