mgcheck 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Migration Guardian Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,416 @@
1
+ # 🛡️ Migration Guardian (`mgcheck`)
2
+
3
+ **Catch unsafe database migrations before they hit production.**
4
+
5
+ Migration Guardian sits between your migration file and your real database — running it against a disposable shadow Postgres, analyzing it against known unsafe patterns, and reporting exactly what's dangerous and why.
6
+
7
+ Built for teams using **Prisma**, **Drizzle**, or **raw SQL** migrations with PostgreSQL. Works as a **CLI**, a **CI gate**, or an **MCP tool** that AI coding agents call automatically.
8
+
9
+ [![npm version](https://img.shields.io/npm/v/mgcheck.svg)](https://www.npmjs.com/package/mgcheck)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
11
+ [![CI](https://img.shields.io/github/actions/workflow/status/yourusername/mgcheck/ci.yml?branch=main)](https://github.com/yourusername/mgcheck/actions)
12
+
13
+ ---
14
+
15
+ ## Why?
16
+
17
+ AI coding agents (Claude Code, Cursor, etc.) write database migrations constantly. They get the *schema intent* right — but they don't know whether the operation is *safe to run on production*:
18
+
19
+ - `CREATE INDEX` without `CONCURRENTLY` → **locks the entire table** during index build
20
+ - `ALTER COLUMN TYPE` → **full table rewrite**, blocking all reads/writes
21
+ - `ADD COLUMN NOT NULL` without a default → **table rewrite on large tables**
22
+ - `DROP TABLE` / `DROP COLUMN` → **irreversible data loss**
23
+ - No `lock_timeout` → **lock queue death spiral** under contention
24
+
25
+ **Existing tools catch some of this. None of them close the loop:**
26
+
27
+ | Tool | Static Analysis | Shadow DB Execution | Auto-Fix + Re-verify | Open Source |
28
+ |---|:---:|:---:|:---:|:---:|
29
+ | Squawk | ✅ | ❌ | ❌ | ✅ |
30
+ | pgfence | ✅ | ❌ | ❌ | ✅ |
31
+ | Atlas lint | ✅ | ✅ | ❌ | ❌ (Pro tier) |
32
+ | **mgcheck** | ✅ | ✅ | ✅ (v1) | ✅ |
33
+
34
+ ---
35
+
36
+ ## Quick Start
37
+
38
+ ### 1. Zero-Install Interactive Setup (One Command)
39
+
40
+ Just run this in any project terminal:
41
+
42
+ ```bash
43
+ npx mgcheck
44
+ ```
45
+
46
+ It automatically:
47
+ 1. Detects your AI editors (**Antigravity**, **Cursor**, **Claude Desktop**).
48
+ 2. Asks: `Connect Migration Guardian to your AI agents? [Y/n]`
49
+ 3. Instantly configures the MCP server so your AI coding agent can automatically call `check_migration` and `analyze_migration` before touching your schema!
50
+
51
+ ---
52
+
53
+ ### 2. Manual CLI Usage
54
+
55
+ ```bash
56
+ # Check a single migration file against shadow Postgres
57
+ npx mgcheck run ./migrations/0007_add_status.sql
58
+
59
+ # Static analysis only (zero database required)
60
+ npx mgcheck analyze ./migrations/0007_add_status.sql
61
+
62
+ # Non-interactive auto-setup
63
+ npx mgcheck setup -y
64
+ ```
65
+
66
+ ### Example Output
67
+
68
+ ```
69
+ mgcheck v0.1.0 — Migration Guardian
70
+
71
+ 📁 Detected: Raw SQL migration
72
+ 📄 File: ./migrations/0007_add_status.sql
73
+
74
+ ⚡ Shadow Database
75
+ ├─ Provider: Docker (postgres:16-alpine)
76
+ ├─ Started in 2.3s
77
+ └─ Execution: ✅ Success (45ms, 3 statements)
78
+
79
+ 🔍 Rule Analysis
80
+
81
+ ❌ MG001: CREATE INDEX without CONCURRENTLY
82
+ Line 12: CREATE INDEX idx_users_email ON users (email);
83
+
84
+ Why this is dangerous:
85
+ CREATE INDEX without CONCURRENTLY acquires a SHARE lock on the table,
86
+ blocking all INSERT, UPDATE, and DELETE operations for the entire
87
+ duration of the index build. On a table with millions of rows, this
88
+ can block writes for minutes or hours.
89
+
90
+ Suggested fix:
91
+ CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
92
+ Note: CONCURRENTLY cannot run inside a transaction block.
93
+
94
+ ⚠️ MG009: No lock_timeout guard
95
+ No SET lock_timeout found before risky operations.
96
+
97
+ Suggested fix:
98
+ Prepend: SET lock_timeout = '5s';
99
+
100
+ ──────────────────────────────────────────
101
+ Result: ❌ FAILED (1 error, 1 warning)
102
+ ```
103
+
104
+ ---
105
+
106
+ ## CLI Reference
107
+
108
+ ### Commands
109
+
110
+ ```bash
111
+ mgcheck run <path> # Full check: analyze + execute against shadow DB
112
+ mgcheck analyze <path> # Static analysis only (no shadow DB required)
113
+ mgcheck setup # Auto-connect MCP to Antigravity, Cursor, & Claude
114
+ mgcheck mcp # Start the MCP server for AI agent stdio
115
+ mgcheck version # Print version
116
+ ```
117
+
118
+ ### Flags
119
+
120
+ | Flag | Description | Default |
121
+ |---|---|---|
122
+ | `--db-url <url>` | Use existing Postgres instead of Docker | — |
123
+ | `--seed <path>` | Base schema SQL to apply before target migration | — |
124
+ | `--docker-image <img>` | Postgres Docker image | `postgres:16-alpine` |
125
+ | `--provider <p>` | Shadow DB provider: `docker` \| `pglite` | `docker` |
126
+ | `--format <f>` | Output: `terminal` \| `json` \| `markdown` | `terminal` |
127
+ | `--ci` | Machine-readable JSON + exit codes only | `false` |
128
+ | `--confirm-destructive` | Allow destructive ops (DROP, TRUNCATE) | `false` |
129
+ | `--llm-fix` | *(v1)* Enable LLM-assisted fix generation | `false` |
130
+ | `--verbose` | Show detailed logs | `false` |
131
+ | `--config <path>` | Path to config file | auto-detect |
132
+
133
+ ### Exit Codes
134
+
135
+ | Code | Meaning |
136
+ |---|---|
137
+ | `0` | All checks passed |
138
+ | `1` | Execution failed or blocking rule violation |
139
+ | `2` | Warnings only (no blocking errors) |
140
+ | `3` | Internal error / misconfiguration |
141
+
142
+ ---
143
+
144
+ ## Safety Rules
145
+
146
+ mgcheck ships with 10 built-in safety rules, inspired by [Squawk](https://squawkhq.com/) and [pgfence](https://github.com/flvmnt/pgfence):
147
+
148
+ | Rule | Name | Detects | Severity | Safe Alternative |
149
+ |---|---|---|---|---|
150
+ | **MG001** | `create-index-not-concurrent` | `CREATE INDEX` without `CONCURRENTLY` | error | `CREATE INDEX CONCURRENTLY` |
151
+ | **MG002** | `alter-column-type` | `ALTER TABLE ... ALTER COLUMN ... TYPE` | error | Expand/contract: add new column → migrate → drop old |
152
+ | **MG003** | `add-column-not-null-no-default` | `ADD COLUMN ... NOT NULL` without `DEFAULT` | error | Add nullable → backfill → `SET NOT NULL` |
153
+ | **MG004** | `add-constraint-not-valid` | `ADD CONSTRAINT` without `NOT VALID` | warning | `NOT VALID` + separate `VALIDATE CONSTRAINT` |
154
+ | **MG005** | `drop-column` | `DROP COLUMN` | error | Requires `--confirm-destructive` |
155
+ | **MG006** | `drop-table` | `DROP TABLE` | error | Requires `--confirm-destructive` |
156
+ | **MG007** | `rename-column` | `RENAME COLUMN` | warning | Add new column + update code + drop old |
157
+ | **MG008** | `rename-table` | `RENAME TABLE` | warning | Create new table + view/alias |
158
+ | **MG009** | `missing-lock-timeout` | No `lock_timeout` before risky ops | warning | Prepend `SET lock_timeout = '5s';` |
159
+ | **MG010** | `missing-down-migration` | No rollback migration detected | warning | Create corresponding down migration |
160
+
161
+ ### Configuring Rules
162
+
163
+ Override severity or disable rules in `.mgcheckrc.json`:
164
+
165
+ ```json
166
+ {
167
+ "rules": {
168
+ "MG004": "error",
169
+ "MG009": "off",
170
+ "MG010": "off"
171
+ }
172
+ }
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Configuration
178
+
179
+ Create `.mgcheckrc.json` in your project root:
180
+
181
+ ```json
182
+ {
183
+ "shadowDb": {
184
+ "provider": "docker",
185
+ "dockerImage": "postgres:16-alpine",
186
+ "seedFile": "./prisma/migrations/0001_init/migration.sql"
187
+ },
188
+ "rules": {
189
+ "MG007": "error",
190
+ "MG010": "off"
191
+ },
192
+ "output": {
193
+ "format": "terminal",
194
+ "verbose": false
195
+ }
196
+ }
197
+ ```
198
+
199
+ ### Environment Variables
200
+
201
+ | Variable | Description |
202
+ |---|---|
203
+ | `MGCHECK_DB_URL` | Postgres connection string for shadow DB |
204
+ | `MGCHECK_DOCKER_IMAGE` | Docker image override |
205
+ | `MGCHECK_LLM_PROVIDER` | *(v1)* LLM provider (`openai`, `anthropic`, or base URL) |
206
+ | `MGCHECK_LLM_API_KEY` | *(v1)* LLM API key |
207
+ | `MGCHECK_LLM_MODEL` | *(v1)* Model name (e.g., `claude-sonnet-4-20250514`) |
208
+
209
+ ---
210
+
211
+ ## CI / GitHub Actions
212
+
213
+ ### Direct CLI Usage
214
+
215
+ ```yaml
216
+ name: Migration Safety Check
217
+ on:
218
+ pull_request:
219
+ paths:
220
+ - 'prisma/migrations/**'
221
+ - 'drizzle/**'
222
+
223
+ jobs:
224
+ check-migrations:
225
+ runs-on: ubuntu-latest
226
+ services:
227
+ postgres:
228
+ image: postgres:16-alpine
229
+ env:
230
+ POSTGRES_PASSWORD: test
231
+ options: >-
232
+ --health-cmd pg_isready
233
+ --health-interval 10s
234
+ --health-timeout 5s
235
+ --health-retries 5
236
+ ports:
237
+ - 5432:5432
238
+
239
+ steps:
240
+ - uses: actions/checkout@v4
241
+ - uses: actions/setup-node@v4
242
+ with:
243
+ node-version: '20'
244
+
245
+ - name: Check migrations
246
+ run: npx mgcheck run ./prisma/migrations/ --ci --format json
247
+ env:
248
+ MGCHECK_DB_URL: postgresql://postgres:test@localhost:5432/postgres
249
+ ```
250
+
251
+ ### GitHub Action (Prebuilt)
252
+
253
+ ```yaml
254
+ - name: Migration Guardian
255
+ uses: yourusername/mgcheck-action@v1
256
+ with:
257
+ migration-path: ./prisma/migrations/
258
+ seed-path: ./schema/base.sql
259
+ ```
260
+
261
+ ---
262
+
263
+ ## MCP Server (AI Agent Integration)
264
+
265
+ mgcheck exposes an MCP server so AI coding agents (Claude Code, Cursor, etc.) can verify migrations inline:
266
+
267
+ ### Setup
268
+
269
+ Add to your MCP configuration (e.g., `claude_desktop_config.json`):
270
+
271
+ ```json
272
+ {
273
+ "mcpServers": {
274
+ "mgcheck": {
275
+ "command": "npx",
276
+ "args": ["mgcheck", "mcp"],
277
+ "env": {
278
+ "MGCHECK_DOCKER_IMAGE": "postgres:16-alpine"
279
+ }
280
+ }
281
+ }
282
+ }
283
+ ```
284
+
285
+ ### Available Tools
286
+
287
+ | Tool | Description |
288
+ |---|---|
289
+ | `check_migration` | Full pipeline: analyze + execute against shadow DB |
290
+ | `analyze_migration` | Static analysis only (no Docker required) |
291
+
292
+ The agent can call `check_migration` with the SQL content and get back a structured report with violations, explanations, and suggested fixes — all before the migration ever touches a real database.
293
+
294
+ ---
295
+
296
+ ## Migration Format Support
297
+
298
+ | Format | Auto-Detected By | Status |
299
+ |---|---|---|
300
+ | **Raw SQL** (`.sql` files) | File extension | ✅ Supported |
301
+ | **Prisma** | `migration.sql` in `prisma/migrations/*/` | ✅ Supported |
302
+ | **Drizzle** | `meta/_journal.json` in directory | ✅ Supported |
303
+ | MySQL / SQLite | — | 🔮 Planned |
304
+
305
+ ---
306
+
307
+ ## How It Works
308
+
309
+ ```
310
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
311
+ │ Migration │ │ Shadow DB │ │ Rule │
312
+ │ Detector │────▶│ Executor │────▶│ Analyzer │
313
+ │ │ │ (Docker/ │ │ (10 rules) │
314
+ │ SQL/Prisma/ │ │ PGlite) │ │ │
315
+ │ Drizzle │ └──────────────┘ └──────────────┘
316
+ └──────────────┘ │ │
317
+ │ │
318
+ ▼ ▼
319
+ ┌──────────────────────────────┐
320
+ │ Report Builder │
321
+ │ • Terminal (pretty) │
322
+ │ • JSON (CI/machine) │
323
+ │ • Markdown (PR comments) │
324
+ └──────────────────────────────┘
325
+
326
+ ┌──────┴──────┐
327
+ │ v1: LLM Fix │
328
+ │ + Re-verify │
329
+ └─────────────┘
330
+ ```
331
+
332
+ 1. **Detect**: Auto-detect migration format (raw SQL, Prisma, Drizzle)
333
+ 2. **Analyze**: Parse SQL into AST, run 10 safety rules
334
+ 3. **Execute**: Spin up disposable Postgres, run migration, capture result
335
+ 4. **Report**: Clear pass/fail with plain-English explanations
336
+ 5. *(v1)* **Fix**: LLM generates corrected migration, re-verified from scratch
337
+
338
+ ---
339
+
340
+ ## Development
341
+
342
+ ```bash
343
+ # Clone and install
344
+ git clone https://github.com/yourusername/mgcheck.git
345
+ cd mgcheck
346
+ npm install
347
+
348
+ # Run in development
349
+ npm run dev -- run ./test/fixtures/migrations/unsafe-create-index.sql
350
+
351
+ # Run tests
352
+ npm run test:unit # No Docker needed
353
+ npm run test:integration # Requires Docker
354
+
355
+ # Build
356
+ npm run build
357
+
358
+ # Link locally
359
+ npm link
360
+ mgcheck run ./your-migration.sql
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Roadmap
366
+
367
+ ### v0 (Current) — Detection & Reporting
368
+ - [x] CLI tool with `run` and `analyze` commands
369
+ - [x] Auto-detect Raw SQL, Prisma, Drizzle formats
370
+ - [x] Shadow DB via Docker + PGlite fallback
371
+ - [x] 10 safety rules with plain-English explanations
372
+ - [x] Terminal, JSON, Markdown output formats
373
+ - [x] CI-compatible exit codes
374
+ - [x] MCP server for AI agent integration
375
+ - [x] GitHub Action
376
+
377
+ ### v1 — LLM Fix & Re-verify
378
+ - [ ] Pluggable LLM backend (Claude, GPT, etc.)
379
+ - [ ] Fix generation with structured output
380
+ - [ ] Fresh shadow DB re-verification of fixes
381
+ - [ ] Destructive operation guardrails (never auto-fix drops)
382
+ - [ ] Single-attempt cap (no infinite loops)
383
+
384
+ ### v2 — Multi-Database & Beyond
385
+ - [ ] MySQL support
386
+ - [ ] SQLite support
387
+ - [ ] Custom rule authoring API
388
+ - [ ] Schema drift detection
389
+ - [ ] Migration dependency graph
390
+
391
+ ---
392
+
393
+ ## Contributing
394
+
395
+ We welcome contributions! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
396
+
397
+ ```bash
398
+ # Fork the repo, then:
399
+ git checkout -b feat/my-feature
400
+ npm test
401
+ git commit -m "feat: add my feature"
402
+ git push origin feat/my-feature
403
+ # Open a Pull Request
404
+ ```
405
+
406
+ ---
407
+
408
+ ## License
409
+
410
+ MIT © [Your Name]
411
+
412
+ ---
413
+
414
+ <p align="center">
415
+ <strong>Stop shipping unsafe migrations. Start with <code>mgcheck</code>.</strong>
416
+ </p>
@@ -0,0 +1,13 @@
1
+ import {
2
+ LOG_FILE,
3
+ formatActivityEntry,
4
+ logActivity,
5
+ watchActivityLog
6
+ } from "./chunk-3G3R2NM3.js";
7
+ export {
8
+ LOG_FILE,
9
+ formatActivityEntry,
10
+ logActivity,
11
+ watchActivityLog
12
+ };
13
+ //# sourceMappingURL=activity-log-ETNHCZ7B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,9 @@
1
+ import {
2
+ applyToRealDatabase,
3
+ promptAndApplyIfConfirmed
4
+ } from "./chunk-7ENQ5WVM.js";
5
+ export {
6
+ applyToRealDatabase,
7
+ promptAndApplyIfConfirmed
8
+ };
9
+ //# sourceMappingURL=applier-TLGJ6SZ2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,73 @@
1
+ // src/core/activity-log.ts
2
+ import { existsSync, mkdirSync, appendFileSync, readFileSync, statSync } from "fs";
3
+ import { join } from "path";
4
+ import os from "os";
5
+ import chalk from "chalk";
6
+ var LOG_DIR = join(os.homedir(), ".mgcheck");
7
+ var LOG_FILE = join(LOG_DIR, "activity.log");
8
+ function logActivity(entry) {
9
+ if (!existsSync(LOG_DIR)) {
10
+ mkdirSync(LOG_DIR, { recursive: true });
11
+ }
12
+ const line = JSON.stringify(entry) + "\n";
13
+ appendFileSync(LOG_FILE, line, "utf-8");
14
+ }
15
+ function watchActivityLog(handler) {
16
+ if (!existsSync(LOG_DIR)) {
17
+ mkdirSync(LOG_DIR, { recursive: true });
18
+ }
19
+ if (!existsSync(LOG_FILE)) {
20
+ appendFileSync(LOG_FILE, "", "utf-8");
21
+ }
22
+ let lastSize = statSync(LOG_FILE).size;
23
+ const interval = setInterval(() => {
24
+ try {
25
+ const currentSize = statSync(LOG_FILE).size;
26
+ if (currentSize > lastSize) {
27
+ const fd = readFileSync(LOG_FILE, "utf-8");
28
+ const newContent = fd.substring(lastSize);
29
+ lastSize = currentSize;
30
+ const lines = newContent.split("\n").filter((l) => l.trim());
31
+ for (const line of lines) {
32
+ try {
33
+ const entry = JSON.parse(line);
34
+ handler(entry);
35
+ } catch {
36
+ }
37
+ }
38
+ }
39
+ } catch {
40
+ }
41
+ }, 500);
42
+ return () => {
43
+ clearInterval(interval);
44
+ };
45
+ }
46
+ function formatActivityEntry(entry) {
47
+ const time = chalk.dim(`[${entry.timestamp}]`);
48
+ const tool = chalk.cyan(entry.tool);
49
+ const lines = [];
50
+ lines.push(`${time} ${tool} called`);
51
+ if (entry.statements.length > 0) {
52
+ for (const stmt of entry.statements) {
53
+ const truncated = stmt.length > 80 ? stmt.substring(0, 77) + "..." : stmt;
54
+ lines.push(chalk.dim(" \u2502 ") + chalk.white(truncated));
55
+ }
56
+ }
57
+ if (entry.passed) {
58
+ lines.push(chalk.dim(" \u2514\u2500 ") + chalk.green("\u2705 PASSED") + chalk.dim(` (${entry.violations} violations)`));
59
+ } else {
60
+ lines.push(
61
+ chalk.dim(" \u2514\u2500 ") + chalk.red("\u274C BLOCKED") + chalk.dim(` (${entry.errors} error${entry.errors !== 1 ? "s" : ""}, ${entry.warnings} warning${entry.warnings !== 1 ? "s" : ""})`)
62
+ );
63
+ }
64
+ return lines.join("\n");
65
+ }
66
+
67
+ export {
68
+ LOG_FILE,
69
+ logActivity,
70
+ watchActivityLog,
71
+ formatActivityEntry
72
+ };
73
+ //# sourceMappingURL=chunk-3G3R2NM3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/activity-log.ts"],"sourcesContent":["import { existsSync, mkdirSync, appendFileSync, readFileSync, statSync, watchFile, unwatchFile } from 'node:fs';\nimport { join } from 'node:path';\nimport os from 'node:os';\nimport chalk from 'chalk';\n\nconst LOG_DIR = join(os.homedir(), '.mgcheck');\nconst LOG_FILE = join(LOG_DIR, 'activity.log');\n\nexport interface ActivityEntry {\n timestamp: string;\n tool: string;\n summary: string;\n violations: number;\n errors: number;\n warnings: number;\n passed: boolean;\n statements: string[];\n}\n\n/**\n * Append an activity entry to the shared log file.\n * Called by the MCP server when a tool is invoked.\n */\nexport function logActivity(entry: ActivityEntry): void {\n if (!existsSync(LOG_DIR)) {\n mkdirSync(LOG_DIR, { recursive: true });\n }\n const line = JSON.stringify(entry) + '\\n';\n appendFileSync(LOG_FILE, line, 'utf-8');\n}\n\n/**\n * Watch the activity log and call the handler for each new entry.\n * Returns a cleanup function to stop watching.\n */\nexport function watchActivityLog(handler: (entry: ActivityEntry) => void): () => void {\n if (!existsSync(LOG_DIR)) {\n mkdirSync(LOG_DIR, { recursive: true });\n }\n\n // Create log file if it doesn't exist\n if (!existsSync(LOG_FILE)) {\n appendFileSync(LOG_FILE, '', 'utf-8');\n }\n\n // Track current file size to only read new content\n let lastSize = statSync(LOG_FILE).size;\n\n const interval = setInterval(() => {\n try {\n const currentSize = statSync(LOG_FILE).size;\n if (currentSize > lastSize) {\n // Read only the new bytes\n const fd = readFileSync(LOG_FILE, 'utf-8');\n const newContent = fd.substring(lastSize);\n lastSize = currentSize;\n\n const lines = newContent.split('\\n').filter((l) => l.trim());\n for (const line of lines) {\n try {\n const entry: ActivityEntry = JSON.parse(line);\n handler(entry);\n } catch {\n // Skip malformed lines\n }\n }\n }\n } catch {\n // File may be temporarily unavailable\n }\n }, 500);\n\n return () => {\n clearInterval(interval);\n };\n}\n\n/**\n * Format an activity entry for terminal display.\n */\nexport function formatActivityEntry(entry: ActivityEntry): string {\n const time = chalk.dim(`[${entry.timestamp}]`);\n const tool = chalk.cyan(entry.tool);\n\n const lines: string[] = [];\n lines.push(`${time} ${tool} called`);\n\n // Show each statement\n if (entry.statements.length > 0) {\n for (const stmt of entry.statements) {\n const truncated = stmt.length > 80 ? stmt.substring(0, 77) + '...' : stmt;\n lines.push(chalk.dim(' │ ') + chalk.white(truncated));\n }\n }\n\n // Show result\n if (entry.passed) {\n lines.push(chalk.dim(' └─ ') + chalk.green('✅ PASSED') + chalk.dim(` (${entry.violations} violations)`));\n } else {\n lines.push(\n chalk.dim(' └─ ') +\n chalk.red('❌ BLOCKED') +\n chalk.dim(` (${entry.errors} error${entry.errors !== 1 ? 's' : ''}, ${entry.warnings} warning${entry.warnings !== 1 ? 's' : ''})`)\n );\n }\n\n return lines.join('\\n');\n}\n\nexport { LOG_FILE };\n"],"mappings":";AAAA,SAAS,YAAY,WAAW,gBAAgB,cAAc,gBAAwC;AACtG,SAAS,YAAY;AACrB,OAAO,QAAQ;AACf,OAAO,WAAW;AAElB,IAAM,UAAU,KAAK,GAAG,QAAQ,GAAG,UAAU;AAC7C,IAAM,WAAW,KAAK,SAAS,cAAc;AAiBtC,SAAS,YAAY,OAA4B;AACtD,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,cAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,EACxC;AACA,QAAM,OAAO,KAAK,UAAU,KAAK,IAAI;AACrC,iBAAe,UAAU,MAAM,OAAO;AACxC;AAMO,SAAS,iBAAiB,SAAqD;AACpF,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,cAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,EACxC;AAGA,MAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,mBAAe,UAAU,IAAI,OAAO;AAAA,EACtC;AAGA,MAAI,WAAW,SAAS,QAAQ,EAAE;AAElC,QAAM,WAAW,YAAY,MAAM;AACjC,QAAI;AACF,YAAM,cAAc,SAAS,QAAQ,EAAE;AACvC,UAAI,cAAc,UAAU;AAE1B,cAAM,KAAK,aAAa,UAAU,OAAO;AACzC,cAAM,aAAa,GAAG,UAAU,QAAQ;AACxC,mBAAW;AAEX,cAAM,QAAQ,WAAW,MAAM,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC;AAC3D,mBAAW,QAAQ,OAAO;AACxB,cAAI;AACF,kBAAM,QAAuB,KAAK,MAAM,IAAI;AAC5C,oBAAQ,KAAK;AAAA,UACf,QAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,GAAG;AAEN,SAAO,MAAM;AACX,kBAAc,QAAQ;AAAA,EACxB;AACF;AAKO,SAAS,oBAAoB,OAA8B;AAChE,QAAM,OAAO,MAAM,IAAI,IAAI,MAAM,SAAS,GAAG;AAC7C,QAAM,OAAO,MAAM,KAAK,MAAM,IAAI;AAElC,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,GAAG,IAAI,IAAI,IAAI,SAAS;AAGnC,MAAI,MAAM,WAAW,SAAS,GAAG;AAC/B,eAAW,QAAQ,MAAM,YAAY;AACnC,YAAM,YAAY,KAAK,SAAS,KAAK,KAAK,UAAU,GAAG,EAAE,IAAI,QAAQ;AACrE,YAAM,KAAK,MAAM,IAAI,WAAM,IAAI,MAAM,MAAM,SAAS,CAAC;AAAA,IACvD;AAAA,EACF;AAGA,MAAI,MAAM,QAAQ;AAChB,UAAM,KAAK,MAAM,IAAI,iBAAO,IAAI,MAAM,MAAM,eAAU,IAAI,MAAM,IAAI,KAAK,MAAM,UAAU,cAAc,CAAC;AAAA,EAC1G,OAAO;AACL,UAAM;AAAA,MACJ,MAAM,IAAI,iBAAO,IACf,MAAM,IAAI,gBAAW,IACrB,MAAM,IAAI,KAAK,MAAM,MAAM,SAAS,MAAM,WAAW,IAAI,MAAM,EAAE,KAAK,MAAM,QAAQ,WAAW,MAAM,aAAa,IAAI,MAAM,EAAE,GAAG;AAAA,IACrI;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}