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 +21 -0
- package/README.md +416 -0
- package/dist/activity-log-ETNHCZ7B.js +13 -0
- package/dist/activity-log-ETNHCZ7B.js.map +1 -0
- package/dist/applier-TLGJ6SZ2.js +9 -0
- package/dist/applier-TLGJ6SZ2.js.map +1 -0
- package/dist/chunk-3G3R2NM3.js +73 -0
- package/dist/chunk-3G3R2NM3.js.map +1 -0
- package/dist/chunk-4YTQB62X.js +261 -0
- package/dist/chunk-4YTQB62X.js.map +1 -0
- package/dist/chunk-7ENQ5WVM.js +97 -0
- package/dist/chunk-7ENQ5WVM.js.map +1 -0
- package/dist/chunk-7JBFSZBD.js +1212 -0
- package/dist/chunk-7JBFSZBD.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/installer-KGWDJ6OR.js +323 -0
- package/dist/installer-KGWDJ6OR.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +158 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +83 -0
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
|
+
[](https://www.npmjs.com/package/mgcheck)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](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 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -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":[]}
|