oopsdb 1.5.0 → 1.6.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 -21
- package/README.md +146 -127
- package/dist/commands/lock.d.ts +1 -0
- package/dist/commands/lock.js +52 -0
- package/dist/commands/secure.js +12 -9
- package/dist/commands/shield.js +27 -4
- package/dist/commands/status.js +7 -0
- package/dist/commands/unlock.d.ts +1 -0
- package/dist/commands/unlock.js +77 -0
- package/dist/index.js +10 -0
- package/dist/utils/psql.d.ts +2 -0
- package/dist/utils/psql.js +34 -0
- package/package.json +69 -69
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 OopsDB 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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OopsDB 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
CHANGED
|
@@ -1,127 +1,146 @@
|
|
|
1
|
-
# OopsDB
|
|
2
|
-
|
|
3
|
-
**Don't let AI nuke your database.**
|
|
4
|
-
|
|
5
|
-
Auto-backup and 1-click restore for developers using Claude Code, Cursor, Windsurf, and other AI coding agents.
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<img src="website/assets/oopsdb_demo.gif" alt="OopsDB Demo Screen Recording" style="max-width: 100%; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1)">
|
|
9
|
-
</p>
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## The Problem
|
|
14
|
-
|
|
15
|
-
You're vibing. Claude Code is cranking through tasks. Then it decides the fastest way to fix a migration is `DROP TABLE users`. Or it runs `DELETE FROM orders` without a `WHERE` clause. Or it helpfully "cleans up" your SQLite file.
|
|
16
|
-
|
|
17
|
-
Your data is gone. Your afternoon is gone. Your will to live is negotiable.
|
|
18
|
-
|
|
19
|
-
## The Fix
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install -g oopsdb
|
|
23
|
-
oopsdb init # connect your DB (Supabase, Postgres, MySQL, SQLite)
|
|
24
|
-
oopsdb watch # auto-backup every 5 min
|
|
25
|
-
# ... AI nukes your DB ...
|
|
26
|
-
oopsdb restore # pick a snapshot, roll back instantly
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
That's it. Three commands. Your data survives the AI apocalypse.
|
|
30
|
-
|
|
31
|
-
## What It Does
|
|
32
|
-
|
|
33
|
-
- **Auto-backups** on a timer (`oopsdb watch`) — set it and forget it
|
|
34
|
-
- **Manual snapshots** (`oopsdb snapshot`) — before risky migrations or YOLO prompts
|
|
35
|
-
- **Interactive restore** (`oopsdb restore`) — pick any snapshot, roll back in seconds
|
|
36
|
-
- **Safety snapshots** — automatically backs up your current state before restoring, so you can't oops your oops
|
|
37
|
-
- **Encrypted at rest** — AES-256-CBC encryption on every backup file
|
|
38
|
-
- **Zero cloud, zero accounts** — everything stays on your machine
|
|
39
|
-
- **Streaming backups** — near-zero memory footprint regardless of DB size
|
|
40
|
-
|
|
41
|
-
## Supported Databases
|
|
42
|
-
|
|
43
|
-
| Database | Backup Tool | Restore Tool |
|
|
44
|
-
|----------|------------|--------------|
|
|
45
|
-
| **Supabase** | `pg_dump` (with Supabase flags) | `psql` |
|
|
46
|
-
| PostgreSQL (Neon, local, other hosted) | `pg_dump` | `psql` |
|
|
47
|
-
| MySQL / MariaDB | `mysqldump` | `mysql` |
|
|
48
|
-
| SQLite | `sqlite3` | `sqlite3` |
|
|
49
|
-
|
|
50
|
-
### Supabase (first-class support)
|
|
51
|
-
|
|
52
|
-
OopsDB has dedicated Supabase support. Just paste your connection string:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
oopsdb init
|
|
56
|
-
# → Select "Supabase"
|
|
57
|
-
# → Paste your connection string from Supabase Dashboard → Settings → Database
|
|
58
|
-
# → Done. SSL and Supabase-specific pg_dump flags are handled automatically.
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Supabase-specific flags applied automatically: `--no-owner`, `--no-privileges`, `--no-subscriptions`, `sslmode=require`.
|
|
62
|
-
|
|
63
|
-
## Commands
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
oopsdb init Set up your database connection
|
|
67
|
-
oopsdb watch Auto-backup every 5 minutes
|
|
68
|
-
oopsdb watch -i 1 Auto-backup every 1 minute (paranoid mode)
|
|
69
|
-
oopsdb snapshot One-time manual backup
|
|
70
|
-
oopsdb
|
|
71
|
-
oopsdb
|
|
72
|
-
oopsdb
|
|
73
|
-
oopsdb
|
|
74
|
-
oopsdb
|
|
75
|
-
oopsdb
|
|
76
|
-
oopsdb
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
1
|
+
# OopsDB
|
|
2
|
+
|
|
3
|
+
**Don't let AI nuke your database.**
|
|
4
|
+
|
|
5
|
+
Auto-backup and 1-click restore for developers using Claude Code, Cursor, Windsurf, and other AI coding agents.
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="website/assets/oopsdb_demo.gif" alt="OopsDB Demo Screen Recording" style="max-width: 100%; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1)">
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## The Problem
|
|
14
|
+
|
|
15
|
+
You're vibing. Claude Code is cranking through tasks. Then it decides the fastest way to fix a migration is `DROP TABLE users`. Or it runs `DELETE FROM orders` without a `WHERE` clause. Or it helpfully "cleans up" your SQLite file.
|
|
16
|
+
|
|
17
|
+
Your data is gone. Your afternoon is gone. Your will to live is negotiable.
|
|
18
|
+
|
|
19
|
+
## The Fix
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g oopsdb
|
|
23
|
+
oopsdb init # connect your DB (Supabase, Postgres, MySQL, SQLite)
|
|
24
|
+
oopsdb watch # auto-backup every 5 min
|
|
25
|
+
# ... AI nukes your DB ...
|
|
26
|
+
oopsdb restore # pick a snapshot, roll back instantly
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That's it. Three commands. Your data survives the AI apocalypse.
|
|
30
|
+
|
|
31
|
+
## What It Does
|
|
32
|
+
|
|
33
|
+
- **Auto-backups** on a timer (`oopsdb watch`) — set it and forget it
|
|
34
|
+
- **Manual snapshots** (`oopsdb snapshot`) — before risky migrations or YOLO prompts
|
|
35
|
+
- **Interactive restore** (`oopsdb restore`) — pick any snapshot, roll back in seconds
|
|
36
|
+
- **Safety snapshots** — automatically backs up your current state before restoring, so you can't oops your oops
|
|
37
|
+
- **Encrypted at rest** — AES-256-CBC encryption on every backup file
|
|
38
|
+
- **Zero cloud, zero accounts** — everything stays on your machine
|
|
39
|
+
- **Streaming backups** — near-zero memory footprint regardless of DB size
|
|
40
|
+
|
|
41
|
+
## Supported Databases
|
|
42
|
+
|
|
43
|
+
| Database | Backup Tool | Restore Tool |
|
|
44
|
+
|----------|------------|--------------|
|
|
45
|
+
| **Supabase** | `pg_dump` (with Supabase flags) | `psql` |
|
|
46
|
+
| PostgreSQL (Neon, local, other hosted) | `pg_dump` | `psql` |
|
|
47
|
+
| MySQL / MariaDB | `mysqldump` | `mysql` |
|
|
48
|
+
| SQLite | `sqlite3` | `sqlite3` |
|
|
49
|
+
|
|
50
|
+
### Supabase (first-class support)
|
|
51
|
+
|
|
52
|
+
OopsDB has dedicated Supabase support. Just paste your connection string:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
oopsdb init
|
|
56
|
+
# → Select "Supabase"
|
|
57
|
+
# → Paste your connection string from Supabase Dashboard → Settings → Database
|
|
58
|
+
# → Done. SSL and Supabase-specific pg_dump flags are handled automatically.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Supabase-specific flags applied automatically: `--no-owner`, `--no-privileges`, `--no-subscriptions`, `sslmode=require`.
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
oopsdb init Set up your database connection
|
|
67
|
+
oopsdb watch Auto-backup every 5 minutes
|
|
68
|
+
oopsdb watch -i 1 Auto-backup every 1 minute (paranoid mode)
|
|
69
|
+
oopsdb snapshot One-time manual backup
|
|
70
|
+
oopsdb shield Active query interceptor (blocks DROP/DELETE)
|
|
71
|
+
oopsdb restore Interactive restore from any snapshot
|
|
72
|
+
oopsdb status View backup history and stats
|
|
73
|
+
oopsdb secure Immutable cloud backups
|
|
74
|
+
oopsdb activate <key> Activate a Secure license
|
|
75
|
+
oopsdb deactivate Deactivate your license on this machine
|
|
76
|
+
oopsdb license Show current license status
|
|
77
|
+
oopsdb lock Lock schema with Postgres event triggers
|
|
78
|
+
oopsdb unlock Temporarily unlock schema for migrations
|
|
79
|
+
oopsdb clean Remove all OopsDB data from project
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## The Shield (Query Interceptor)
|
|
83
|
+
|
|
84
|
+
OopsDB Shield acts as a proxy between your application and your database. It monitors incoming SQL traffic and automatically takes a safety snapshot if it detects destructive commands like `DROP TABLE` or `DELETE` without a `WHERE` clause.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
oopsdb shield --port 5433
|
|
88
|
+
# Now connect your app to port 5433 instead of the default DB port.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Antigravity (PostgreSQL / Supabase)
|
|
92
|
+
|
|
93
|
+
OopsDB Antigravity provides database-level protection using native Postgres event triggers. Even if an AI agent bypasses the OopsDB proxy or connects directly to the DB, destructive DDL commands will be blocked at the database level.
|
|
94
|
+
|
|
95
|
+
- `oopsdb lock`: Creates a trigger that blocks `DROP`, `ALTER`, and `TRUNCATE` operations.
|
|
96
|
+
- `oopsdb unlock`: Takes a safety snapshot, drops the trigger, and allows schema modifications for 60 seconds before automatically re-locking.
|
|
97
|
+
|
|
98
|
+
## How It Works
|
|
99
|
+
|
|
100
|
+
1. `oopsdb init` walks you through connecting your database. Credentials are encrypted and saved locally in `.oopsdb/config.json`.
|
|
101
|
+
2. `oopsdb watch` runs the native dump tool (`pg_dump`, `mysqldump`, or `sqlite3 .backup`) at your chosen interval. Output is streamed through AES-256-CBC encryption directly to disk — memory usage stays flat even for large databases.
|
|
102
|
+
3. `oopsdb restore` shows your snapshots with timestamps and sizes. Pick one, confirm, and your database is rolled back. It takes a safety snapshot first, so you can always undo the undo.
|
|
103
|
+
|
|
104
|
+
## Quick Demo
|
|
105
|
+
|
|
106
|
+
Want to see OopsDB in action without touching your real database? We built a safe demo playground just for you.
|
|
107
|
+
|
|
108
|
+
1. Clone or download this repository.
|
|
109
|
+
2. Navigate to the `demo/` folder: `cd demo`
|
|
110
|
+
3. Generate the dummy database: `sqlite3 test.db < seed.sql`
|
|
111
|
+
4. Initialize OopsDB (it won't affect your global config because `.oopsdb` is gitignored here): `oopsdb init`
|
|
112
|
+
5. Try running `oopsdb shield` and firing a `DROP TABLE users` against it to watch the interceptor catch the query!
|
|
113
|
+
|
|
114
|
+
## Requirements
|
|
115
|
+
|
|
116
|
+
Your system needs the native database CLI tools:
|
|
117
|
+
|
|
118
|
+
- **PostgreSQL**: `pg_dump` + `psql`
|
|
119
|
+
- **MySQL**: `mysqldump` + `mysql`
|
|
120
|
+
- **SQLite**: `sqlite3`
|
|
121
|
+
|
|
122
|
+
OopsDB checks for these on `init` and gives install instructions if they're missing.
|
|
123
|
+
|
|
124
|
+
## Security
|
|
125
|
+
|
|
126
|
+
- Credentials encrypted at rest (AES-256-CBC, machine-local key)
|
|
127
|
+
- Backup files encrypted at rest (AES-256-CBC, streaming encryption)
|
|
128
|
+
- Nothing leaves your machine — no cloud, no telemetry, no accounts
|
|
129
|
+
- Add `.oopsdb/` to `.gitignore` (already in ours)
|
|
130
|
+
|
|
131
|
+
## Pricing
|
|
132
|
+
|
|
133
|
+
**Free** — Everything local. All databases, unlimited snapshots, encrypted backups. No limits, no accounts, no strings attached.
|
|
134
|
+
|
|
135
|
+
**Secure** ($9/mo) — Immutable cloud backups that even a rogue AI can't delete. Local backups are great until the AI decides to `rm -rf .oopsdb/`. `oopsdb secure` pushes encrypted snapshots to tamper-proof cloud storage with write-once retention policies. Even if your entire machine gets wiped, your backups survive.
|
|
136
|
+
|
|
137
|
+
Learn more at [oopsdb.com](https://oopsdb.com).
|
|
138
|
+
|
|
139
|
+
## Support
|
|
140
|
+
|
|
141
|
+
If OopsDB saved your database (and your afternoon), consider buying me a coffee!
|
|
142
|
+
[ko-fi.com/pintayo](https://ko-fi.com/pintayo)
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function lockCommand(): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.lockCommand = lockCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const config_1 = require("../utils/config");
|
|
10
|
+
const psql_1 = require("../utils/psql");
|
|
11
|
+
async function lockCommand() {
|
|
12
|
+
const config = (0, config_1.loadConfig)();
|
|
13
|
+
if (!config) {
|
|
14
|
+
console.log(chalk_1.default.red('\n No config found. Run `oopsdb init` first.\n'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (config.db.type !== 'postgres') {
|
|
19
|
+
console.log(chalk_1.default.yellow('\n OopsDB Antigravity (Lock/Unlock) is currently only supported for PostgreSQL (including Supabase).\n'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(chalk_1.default.bold('\n OopsDB Antigravity ' + chalk_1.default.green('LOCK')));
|
|
23
|
+
console.log(chalk_1.default.gray(` Securing ${config.db.database} at ${config.db.host || 'localhost'}...\n`));
|
|
24
|
+
const spinner = (0, ora_1.default)('Engaging database-level event triggers...').start();
|
|
25
|
+
const sql = `
|
|
26
|
+
CREATE OR REPLACE FUNCTION oopsdb_block_ddl()
|
|
27
|
+
RETURNS event_trigger AS $$
|
|
28
|
+
BEGIN
|
|
29
|
+
RAISE EXCEPTION 'OopsDB Antigravity Lock is ACTIVE. Schema changes (DROP, ALTER, etc.) are blocked. Run \`oopsdb unlock\` to modify the schema.';
|
|
30
|
+
END;
|
|
31
|
+
$$ LANGUAGE plpgsql;
|
|
32
|
+
|
|
33
|
+
DROP EVENT TRIGGER IF EXISTS oopsdb_protect_schema;
|
|
34
|
+
|
|
35
|
+
CREATE EVENT TRIGGER oopsdb_protect_schema
|
|
36
|
+
ON ddl_command_start
|
|
37
|
+
EXECUTE FUNCTION oopsdb_block_ddl();
|
|
38
|
+
`;
|
|
39
|
+
try {
|
|
40
|
+
await (0, psql_1.runPsqlCommand)(config.db, sql);
|
|
41
|
+
spinner.succeed('Antigravity Lock engaged.');
|
|
42
|
+
console.log(chalk_1.default.green('\n Your database schema is now bulletproof.'));
|
|
43
|
+
console.log(chalk_1.default.gray(' Any attempt to DROP, ALTER, or TRUNCATE will be rejected by the database engine itself.'));
|
|
44
|
+
console.log(chalk_1.default.gray(' Need to run migrations? Use ') + chalk_1.default.cyan('oopsdb unlock') + chalk_1.default.gray(' first.\n'));
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
spinner.fail(`Failed to engage lock: ${err.message}`);
|
|
48
|
+
// Suggest that they might need superuser depending on the environment
|
|
49
|
+
console.log(chalk_1.default.gray('\n Note: Creating event triggers in Postgres requires superuser privileges.'));
|
|
50
|
+
console.log(chalk_1.default.gray(' Ensure the user configured in OopsDB has sufficient permissions.\n'));
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/commands/secure.js
CHANGED
|
@@ -42,6 +42,7 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
42
42
|
const fs = __importStar(require("fs"));
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const config_1 = require("../utils/config");
|
|
45
|
+
const license_1 = require("../utils/license");
|
|
45
46
|
const dumper_1 = require("../utils/dumper");
|
|
46
47
|
async function secureCommand(options) {
|
|
47
48
|
console.log(chalk_1.default.bold('\n OopsDB Secure'));
|
|
@@ -50,7 +51,13 @@ async function secureCommand(options) {
|
|
|
50
51
|
if (!config) {
|
|
51
52
|
console.log(chalk_1.default.red(' No config found. Run `oopsdb init` first.\n'));
|
|
52
53
|
process.exit(1);
|
|
53
|
-
return;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const license = (0, license_1.loadLicense)();
|
|
57
|
+
if (!license || license.tier !== 'secure') {
|
|
58
|
+
console.log(chalk_1.default.yellow(' Secure features require a Secure license.'));
|
|
59
|
+
console.log(chalk_1.default.gray(' Get one at ') + chalk_1.default.cyan('https://oopsdb.com\n'));
|
|
60
|
+
return;
|
|
54
61
|
}
|
|
55
62
|
// Find the latest snapshot
|
|
56
63
|
const snapshots = (0, dumper_1.listSnapshots)();
|
|
@@ -63,14 +70,13 @@ async function secureCommand(options) {
|
|
|
63
70
|
const fileSizeInBytes = fs.statSync(latestFile).size;
|
|
64
71
|
const fileSizeMB = (fileSizeInBytes / (1024 * 1024)).toFixed(2);
|
|
65
72
|
console.log(chalk_1.default.blue(` Found latest snapshot: ${fileName} (${fileSizeMB} MB)`));
|
|
66
|
-
// In production, hit the live endpoint. If testing locally, hit the wrangler dev server.
|
|
67
73
|
const baseUrl = process.env.TEST_LOCAL_API ? 'http://localhost:8788' : 'https://oopsdb.com';
|
|
68
|
-
console.log(chalk_1.default.gray(` Requesting secure upload token
|
|
74
|
+
console.log(chalk_1.default.gray(` Requesting secure upload token...\n`));
|
|
69
75
|
let actualUploadUrl = '';
|
|
70
76
|
try {
|
|
71
77
|
const res = await fetch(`${baseUrl}/api/upload-url?fileName=${fileName}`, {
|
|
72
78
|
headers: {
|
|
73
|
-
'Authorization':
|
|
79
|
+
'Authorization': `Bearer ${license.licenseKey}`
|
|
74
80
|
}
|
|
75
81
|
});
|
|
76
82
|
if (!res.ok) {
|
|
@@ -84,7 +90,6 @@ async function secureCommand(options) {
|
|
|
84
90
|
}
|
|
85
91
|
catch (err) {
|
|
86
92
|
console.log(chalk_1.default.red(` Network error reaching backend: ${err.message}\n`));
|
|
87
|
-
console.log(chalk_1.default.yellow(` Tip: If testing locally, ensure you ran 'npx wrangler pages dev website' first and set TEST_LOCAL_API=1\n`));
|
|
88
93
|
return;
|
|
89
94
|
}
|
|
90
95
|
await uploadToS3(latestFile, actualUploadUrl);
|
|
@@ -104,9 +109,8 @@ async function uploadToS3(filePath, uploadUrl) {
|
|
|
104
109
|
}
|
|
105
110
|
});
|
|
106
111
|
if (!response.ok) {
|
|
107
|
-
spinner.fail(`Upload
|
|
108
|
-
console.log(chalk_1.default.
|
|
109
|
-
console.log(chalk_1.default.gray(` (This is expected until the Cloudflare backend endpoint is reachable)\n`));
|
|
112
|
+
spinner.fail(`Upload failed: ${response.status} ${response.statusText}`);
|
|
113
|
+
console.log(chalk_1.default.gray(` Check your connection or license status.\n`));
|
|
110
114
|
}
|
|
111
115
|
else {
|
|
112
116
|
spinner.succeed('Snapshot safely stored in immutable S3 Cloud Vault.');
|
|
@@ -115,6 +119,5 @@ async function uploadToS3(filePath, uploadUrl) {
|
|
|
115
119
|
}
|
|
116
120
|
catch (err) {
|
|
117
121
|
spinner.fail(`Upload failed: ${err.message}`);
|
|
118
|
-
console.log(chalk_1.default.gray(` (Ensure the backend feature is fully implemented to receive a valid S3 URL)\n`));
|
|
119
122
|
}
|
|
120
123
|
}
|
package/dist/commands/shield.js
CHANGED
|
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.shieldCommand = shieldCommand;
|
|
40
40
|
const net = __importStar(require("net"));
|
|
41
|
+
const tls = __importStar(require("tls"));
|
|
41
42
|
const chalk_1 = __importDefault(require("chalk"));
|
|
42
43
|
const ora_1 = __importDefault(require("ora"));
|
|
43
44
|
const config_1 = require("../utils/config");
|
|
@@ -52,14 +53,30 @@ async function shieldCommand(options) {
|
|
|
52
53
|
const proxyPort = parseInt(options.port || '5433', 10);
|
|
53
54
|
const targetPort = config.db.port || (config.db.type === 'postgres' ? 5432 : 3306);
|
|
54
55
|
const targetHost = config.db.host || 'localhost';
|
|
56
|
+
const isSsl = config.db.supabase || config.db.sslmode === 'require';
|
|
55
57
|
console.log(chalk_1.default.bold('\n OopsDB Shield ' + chalk_1.default.green('ACTIVE')));
|
|
56
58
|
console.log(chalk_1.default.gray(` Listening on port ${proxyPort} -> Forwarding to ${config.db.type} on ${targetPort}\n`));
|
|
59
|
+
if (isSsl) {
|
|
60
|
+
console.log(chalk_1.default.yellow(' [SSL Detected] Proxy running in Pass-Through mode.'));
|
|
61
|
+
console.log(chalk_1.default.gray(' Deep-packet inspection (destructive command detection) is DISABLED.'));
|
|
62
|
+
console.log(chalk_1.default.gray(' To enable interception on SSL connections, local certificates/MITM are required.\n'));
|
|
63
|
+
}
|
|
57
64
|
const DESTRUCTIVE_REGEX = /(DROP\s+TABLE|TRUNCATE\s+TABLE|DELETE\s+FROM)/i;
|
|
58
65
|
const server = net.createServer((clientSocket) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
let targetSocket;
|
|
67
|
+
if (isSsl) {
|
|
68
|
+
targetSocket = tls.connect({
|
|
69
|
+
host: targetHost,
|
|
70
|
+
port: targetPort,
|
|
71
|
+
servername: targetHost
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
targetSocket = net.createConnection({
|
|
76
|
+
host: targetHost,
|
|
77
|
+
port: targetPort
|
|
78
|
+
});
|
|
79
|
+
}
|
|
63
80
|
targetSocket.on('error', (err) => {
|
|
64
81
|
console.log(chalk_1.default.red(`\n Database connection error: ${err.message}`));
|
|
65
82
|
clientSocket.destroy(err);
|
|
@@ -71,6 +88,12 @@ async function shieldCommand(options) {
|
|
|
71
88
|
targetSocket.pipe(clientSocket);
|
|
72
89
|
// Handle traffic from Client to DB (with interception)
|
|
73
90
|
clientSocket.on('data', async (chunk) => {
|
|
91
|
+
// If SSL is active, we bypass deep-packet inspection because the traffic is encrypted
|
|
92
|
+
// (or we are acting as a simple TLS pass-through for the application)
|
|
93
|
+
if (isSsl) {
|
|
94
|
+
targetSocket.write(chunk);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
74
97
|
const dataString = chunk.toString();
|
|
75
98
|
if (DESTRUCTIVE_REGEX.test(dataString)) {
|
|
76
99
|
// Destructive command detected!
|
package/dist/commands/status.js
CHANGED
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.statusCommand = statusCommand;
|
|
40
40
|
const chalk_1 = __importDefault(require("chalk"));
|
|
41
41
|
const config_1 = require("../utils/config");
|
|
42
|
+
const license_1 = require("../utils/license");
|
|
42
43
|
const dumper_1 = require("../utils/dumper");
|
|
43
44
|
const path = __importStar(require("path"));
|
|
44
45
|
function formatSize(bytes) {
|
|
@@ -74,6 +75,12 @@ async function statusCommand() {
|
|
|
74
75
|
console.log(` Host: ${chalk_1.default.cyan(config.db.host + ':' + config.db.port)}`);
|
|
75
76
|
}
|
|
76
77
|
console.log(` Since: ${chalk_1.default.cyan(new Date(config.createdAt).toLocaleDateString())}`);
|
|
78
|
+
const license = (0, license_1.loadLicense)();
|
|
79
|
+
const tierLabel = license ? license.tier.toUpperCase() : 'FREE';
|
|
80
|
+
console.log(` Plan: ${chalk_1.default.green(tierLabel)}`);
|
|
81
|
+
if (tierLabel === 'FREE') {
|
|
82
|
+
console.log(chalk_1.default.yellow(' ⚠️ Local snapshots only. Rogue AI or disk failure could wipe these.'));
|
|
83
|
+
}
|
|
77
84
|
console.log();
|
|
78
85
|
console.log(chalk_1.default.gray(' Backups'));
|
|
79
86
|
console.log(` Location: ${chalk_1.default.cyan((0, config_1.getBackupsDir)())}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function unlockCommand(): Promise<void>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.unlockCommand = unlockCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const config_1 = require("../utils/config");
|
|
10
|
+
const dumper_1 = require("../utils/dumper");
|
|
11
|
+
const psql_1 = require("../utils/psql");
|
|
12
|
+
async function unlockCommand() {
|
|
13
|
+
const config = (0, config_1.loadConfig)();
|
|
14
|
+
if (!config) {
|
|
15
|
+
console.log(chalk_1.default.red('\n No config found. Run `oopsdb init` first.\n'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (config.db.type !== 'postgres') {
|
|
20
|
+
console.log(chalk_1.default.yellow('\n OopsDB Antigravity (Lock/Unlock) is currently only supported for PostgreSQL (including Supabase).\n'));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(chalk_1.default.bold('\n OopsDB Antigravity ' + chalk_1.default.yellow('UNLOCK')));
|
|
24
|
+
// 1. Take a snapshot
|
|
25
|
+
const snapSpinner = (0, ora_1.default)('Securing a fresh snapshot before unlocking...').start();
|
|
26
|
+
try {
|
|
27
|
+
await (0, dumper_1.createSnapshot)(config.db);
|
|
28
|
+
snapSpinner.succeed('Fresh safety snapshot secured.');
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
snapSpinner.fail(`Failed to take safety snapshot: ${err.message}`);
|
|
32
|
+
console.log(chalk_1.default.red('\n Aborting unlock. We must secure your data first.\n'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// 2. Drop the trigger
|
|
36
|
+
const sql = `DROP EVENT TRIGGER IF EXISTS oopsdb_protect_schema;`;
|
|
37
|
+
const unlockSpinner = (0, ora_1.default)('Dropping Antigravity locks...').start();
|
|
38
|
+
try {
|
|
39
|
+
await (0, psql_1.runPsqlCommand)(config.db, sql);
|
|
40
|
+
unlockSpinner.succeed('Lock removed.');
|
|
41
|
+
console.log(chalk_1.default.bgYellow.black.bold('\n WARNING ') + chalk_1.default.yellow(' Schema modifications are now ALLOWED.'));
|
|
42
|
+
console.log(chalk_1.default.gray(' You have 60 seconds to run your migrations.'));
|
|
43
|
+
// 3. Setup re-lock timeout
|
|
44
|
+
let timer = 60;
|
|
45
|
+
const interval = setInterval(() => {
|
|
46
|
+
process.stdout.write(`\r ${chalk_1.default.cyan(timer)} seconds remaining...`);
|
|
47
|
+
timer--;
|
|
48
|
+
if (timer < 0) {
|
|
49
|
+
clearInterval(interval);
|
|
50
|
+
process.stdout.write('\n');
|
|
51
|
+
reLock(config.db);
|
|
52
|
+
}
|
|
53
|
+
}, 1000);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
unlockSpinner.fail(`Failed to remove lock: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function reLock(dbConfig) {
|
|
60
|
+
const relockSpinner = (0, ora_1.default)('Time is up. Re-engaging Antigravity Lock...').start();
|
|
61
|
+
const sql = `
|
|
62
|
+
CREATE EVENT TRIGGER oopsdb_protect_schema
|
|
63
|
+
ON ddl_command_start
|
|
64
|
+
EXECUTE FUNCTION oopsdb_block_ddl();
|
|
65
|
+
`;
|
|
66
|
+
try {
|
|
67
|
+
await (0, psql_1.runPsqlCommand)(dbConfig, sql);
|
|
68
|
+
relockSpinner.succeed('Antigravity Lock re-engaged.');
|
|
69
|
+
console.log(chalk_1.default.green('\n Your database schema is bulletproof again.\n'));
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
relockSpinner.fail(`Failed to re-engage lock: ${err.message}`);
|
|
74
|
+
console.log(chalk_1.default.red('\n CRITICAL WARNING: Database remains unprotected! Run `oopsdb lock` manually.\n'));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,8 @@ const secure_1 = require("./commands/secure");
|
|
|
11
11
|
const clean_1 = require("./commands/clean");
|
|
12
12
|
const activate_1 = require("./commands/activate");
|
|
13
13
|
const shield_1 = require("./commands/shield");
|
|
14
|
+
const lock_1 = require("./commands/lock");
|
|
15
|
+
const unlock_1 = require("./commands/unlock");
|
|
14
16
|
// Read version from package.json at runtime
|
|
15
17
|
const pkg = require('../package.json');
|
|
16
18
|
const program = new commander_1.Command();
|
|
@@ -45,6 +47,14 @@ program
|
|
|
45
47
|
.description('Start a database proxy interceptor that prevents destructive queries')
|
|
46
48
|
.option('-p, --port <port>', 'Proxy port', '5433')
|
|
47
49
|
.action(shield_1.shieldCommand);
|
|
50
|
+
program
|
|
51
|
+
.command('lock')
|
|
52
|
+
.description('Bulletproof your Postgres database against schema deletion')
|
|
53
|
+
.action(lock_1.lockCommand);
|
|
54
|
+
program
|
|
55
|
+
.command('unlock')
|
|
56
|
+
.description('Temporarily allow schema migrations (auto-relocks after 60s)')
|
|
57
|
+
.action(unlock_1.unlockCommand);
|
|
48
58
|
program
|
|
49
59
|
.command('secure')
|
|
50
60
|
.description('Immutable cloud backups — tamper-proof, AI-proof')
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runPsqlCommand = runPsqlCommand;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
function runPsqlCommand(dbConfig, sql) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const env = { ...process.env };
|
|
8
|
+
if (dbConfig.password)
|
|
9
|
+
env.PGPASSWORD = dbConfig.password;
|
|
10
|
+
if (dbConfig.sslmode)
|
|
11
|
+
env.PGSSLMODE = dbConfig.sslmode;
|
|
12
|
+
const args = [
|
|
13
|
+
'-h', dbConfig.host || 'localhost',
|
|
14
|
+
'-p', String(dbConfig.port || 5432),
|
|
15
|
+
'-U', dbConfig.user || 'postgres',
|
|
16
|
+
'-c', sql,
|
|
17
|
+
dbConfig.database
|
|
18
|
+
];
|
|
19
|
+
const child = (0, child_process_1.spawn)('psql', args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
20
|
+
let stderr = '';
|
|
21
|
+
child.stderr.on('data', (chunk) => {
|
|
22
|
+
stderr += chunk.toString();
|
|
23
|
+
});
|
|
24
|
+
child.on('error', (err) => reject(err));
|
|
25
|
+
child.on('close', (code) => {
|
|
26
|
+
if (code !== 0) {
|
|
27
|
+
reject(new Error(stderr || `psql exited with code ${code}`));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
resolve();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "oopsdb",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Don't let AI nuke your database. Auto-backup and 1-click restore for developers using Claude Code, Cursor, Windsurf, and other AI coding agents.",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"oopsdb": "dist/index.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "tsc",
|
|
11
|
-
"dev": "ts-node src/index.ts",
|
|
12
|
-
"start": "node dist/index.js",
|
|
13
|
-
"prepublishOnly": "npm run build",
|
|
14
|
-
"test": "tsc && vitest run tests/unit.test.ts",
|
|
15
|
-
"test:e2e": "tsc && vitest run tests/e2e.test.ts",
|
|
16
|
-
"test:all": "tsc && vitest run"
|
|
17
|
-
},
|
|
18
|
-
"keywords": [
|
|
19
|
-
"database",
|
|
20
|
-
"backup",
|
|
21
|
-
"restore",
|
|
22
|
-
"rollback",
|
|
23
|
-
"claude-code",
|
|
24
|
-
"ai-safety",
|
|
25
|
-
"mysql",
|
|
26
|
-
"postgres",
|
|
27
|
-
"supabase",
|
|
28
|
-
"sqlite"
|
|
29
|
-
],
|
|
30
|
-
"author": "",
|
|
31
|
-
"license": "MIT",
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/pintayo/oopsdb.git"
|
|
35
|
-
},
|
|
36
|
-
"homepage": "https://oopsdb.com",
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=16.0.0"
|
|
39
|
-
},
|
|
40
|
-
"files": [
|
|
41
|
-
"dist",
|
|
42
|
-
"README.md",
|
|
43
|
-
"LICENSE"
|
|
44
|
-
],
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"@aws-sdk/client-s3": "^3.1005.0",
|
|
47
|
-
"@aws-sdk/s3-request-presigner": "^3.1005.0",
|
|
48
|
-
"chalk": "^4.1.2",
|
|
49
|
-
"commander": "^12.1.0",
|
|
50
|
-
"inquirer": "^8.2.6",
|
|
51
|
-
"ora": "^5.4.1"
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@cloudflare/workers-types": "^4.20260310.1",
|
|
55
|
-
"@types/inquirer": "^8.2.10",
|
|
56
|
-
"@types/node": "^22.0.0",
|
|
57
|
-
"dotenv": "^17.3.1",
|
|
58
|
-
"testcontainers": "^11.12.0",
|
|
59
|
-
"typescript": "^5.7.0",
|
|
60
|
-
"vitest": "^4.0.18"
|
|
61
|
-
},
|
|
62
|
-
"directories": {
|
|
63
|
-
"test": "tests"
|
|
64
|
-
},
|
|
65
|
-
"types": "./dist/index.d.ts",
|
|
66
|
-
"bugs": {
|
|
67
|
-
"url": "https://github.com/pintayo/oopsdb/issues"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "oopsdb",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Don't let AI nuke your database. Auto-backup and 1-click restore for developers using Claude Code, Cursor, Windsurf, and other AI coding agents.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"oopsdb": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "ts-node src/index.ts",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"test": "tsc && vitest run tests/unit.test.ts",
|
|
15
|
+
"test:e2e": "tsc && vitest run tests/e2e.test.ts",
|
|
16
|
+
"test:all": "tsc && vitest run"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"database",
|
|
20
|
+
"backup",
|
|
21
|
+
"restore",
|
|
22
|
+
"rollback",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"ai-safety",
|
|
25
|
+
"mysql",
|
|
26
|
+
"postgres",
|
|
27
|
+
"supabase",
|
|
28
|
+
"sqlite"
|
|
29
|
+
],
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/pintayo/oopsdb.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://oopsdb.com",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=16.0.0"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@aws-sdk/client-s3": "^3.1005.0",
|
|
47
|
+
"@aws-sdk/s3-request-presigner": "^3.1005.0",
|
|
48
|
+
"chalk": "^4.1.2",
|
|
49
|
+
"commander": "^12.1.0",
|
|
50
|
+
"inquirer": "^8.2.6",
|
|
51
|
+
"ora": "^5.4.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@cloudflare/workers-types": "^4.20260310.1",
|
|
55
|
+
"@types/inquirer": "^8.2.10",
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"dotenv": "^17.3.1",
|
|
58
|
+
"testcontainers": "^11.12.0",
|
|
59
|
+
"typescript": "^5.7.0",
|
|
60
|
+
"vitest": "^4.0.18"
|
|
61
|
+
},
|
|
62
|
+
"directories": {
|
|
63
|
+
"test": "tests"
|
|
64
|
+
},
|
|
65
|
+
"types": "./dist/index.d.ts",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/pintayo/oopsdb/issues"
|
|
68
|
+
}
|
|
69
|
+
}
|