oopsdb 1.0.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) 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 ADDED
@@ -0,0 +1,108 @@
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
+ ---
8
+
9
+ ## The Problem
10
+
11
+ 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.
12
+
13
+ Your data is gone. Your afternoon is gone. Your will to live is negotiable.
14
+
15
+ ## The Fix
16
+
17
+ ```bash
18
+ npm install -g oopsdb
19
+ oopsdb init # connect your DB (Supabase, Postgres, MySQL, SQLite)
20
+ oopsdb watch # auto-backup every 5 min
21
+ # ... AI nukes your DB ...
22
+ oopsdb restore # pick a snapshot, roll back instantly
23
+ ```
24
+
25
+ That's it. Three commands. Your data survives the AI apocalypse.
26
+
27
+ ## What It Does
28
+
29
+ - **Auto-backups** on a timer (`oopsdb watch`) — set it and forget it
30
+ - **Manual snapshots** (`oopsdb snapshot`) — before risky migrations or YOLO prompts
31
+ - **Interactive restore** (`oopsdb restore`) — pick any snapshot, roll back in seconds
32
+ - **Safety snapshots** — automatically backs up your current state before restoring, so you can't oops your oops
33
+ - **Encrypted at rest** — AES-256-CBC encryption on every backup file
34
+ - **Zero cloud, zero accounts** — everything stays on your machine
35
+ - **Streaming backups** — near-zero memory footprint regardless of DB size
36
+
37
+ ## Supported Databases
38
+
39
+ | Database | Backup Tool | Restore Tool |
40
+ |----------|------------|--------------|
41
+ | **Supabase** | `pg_dump` (with Supabase flags) | `psql` |
42
+ | PostgreSQL (Neon, local, other hosted) | `pg_dump` | `psql` |
43
+ | MySQL / MariaDB | `mysqldump` | `mysql` |
44
+ | SQLite | `sqlite3` | `sqlite3` |
45
+
46
+ ### Supabase (first-class support)
47
+
48
+ OopsDB has dedicated Supabase support. Just paste your connection string:
49
+
50
+ ```bash
51
+ oopsdb init
52
+ # → Select "Supabase"
53
+ # → Paste your connection string from Supabase Dashboard → Settings → Database
54
+ # → Done. SSL and Supabase-specific pg_dump flags are handled automatically.
55
+ ```
56
+
57
+ Supabase-specific flags applied automatically: `--no-owner`, `--no-privileges`, `--no-subscriptions`, `sslmode=require`.
58
+
59
+ ## Commands
60
+
61
+ ```
62
+ oopsdb init Set up your database connection
63
+ oopsdb watch Auto-backup every 5 minutes
64
+ oopsdb watch -i 1 Auto-backup every 1 minute (paranoid mode)
65
+ oopsdb snapshot One-time manual backup
66
+ oopsdb restore Interactive restore from any snapshot
67
+ oopsdb status View backup history and stats
68
+ oopsdb activate <key> Activate a Pro license
69
+ oopsdb deactivate Deactivate your license on this machine
70
+ oopsdb license Show current license status and plan
71
+ oopsdb secure Immutable cloud backups (Coming Soon)
72
+ oopsdb clean Remove all OopsDB data from project
73
+ ```
74
+
75
+ ## How It Works
76
+
77
+ 1. `oopsdb init` walks you through connecting your database. Credentials are encrypted and saved locally in `.oopsdb/config.json`.
78
+ 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.
79
+ 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.
80
+
81
+ ## Requirements
82
+
83
+ Your system needs the native database CLI tools:
84
+
85
+ - **PostgreSQL**: `pg_dump` + `psql`
86
+ - **MySQL**: `mysqldump` + `mysql`
87
+ - **SQLite**: `sqlite3`
88
+
89
+ OopsDB checks for these on `init` and gives install instructions if they're missing.
90
+
91
+ ## Security
92
+
93
+ - Credentials encrypted at rest (AES-256-CBC, machine-local key)
94
+ - Backup files encrypted at rest (AES-256-CBC, streaming encryption)
95
+ - Nothing leaves your machine — no cloud, no telemetry, no accounts
96
+ - Add `.oopsdb/` to `.gitignore` (already in ours)
97
+
98
+ ## Coming Soon: `oopsdb secure`
99
+
100
+ Immutable cloud backups that even a rogue AI can't delete.
101
+
102
+ 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.
103
+
104
+ Sign up for early access at [oopsdb.com/secure](https://oopsdb.com/secure).
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,3 @@
1
+ export declare function activateCommand(licenseKey: string): Promise<void>;
2
+ export declare function deactivateCommand(): Promise<void>;
3
+ export declare function licenseStatusCommand(): Promise<void>;
@@ -0,0 +1,89 @@
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.activateCommand = activateCommand;
7
+ exports.deactivateCommand = deactivateCommand;
8
+ exports.licenseStatusCommand = licenseStatusCommand;
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const ora_1 = __importDefault(require("ora"));
11
+ const license_1 = require("../utils/license");
12
+ async function activateCommand(licenseKey) {
13
+ console.log(chalk_1.default.bold('\n OopsDB — License Activation\n'));
14
+ if (!licenseKey || licenseKey.trim().length === 0) {
15
+ console.log(chalk_1.default.red(' Please provide your license key.'));
16
+ console.log(chalk_1.default.gray('\n Usage: ') + chalk_1.default.cyan('oopsdb activate <license-key>'));
17
+ console.log(chalk_1.default.gray(' Get your key at ') + chalk_1.default.cyan('https://oopsdb.com\n'));
18
+ return;
19
+ }
20
+ const spinner = (0, ora_1.default)('Activating license...').start();
21
+ try {
22
+ const license = await (0, license_1.activateLicense)(licenseKey.trim());
23
+ spinner.succeed('License activated!');
24
+ console.log(chalk_1.default.green('\n You\'re on the ' + chalk_1.default.bold(license.tier.toUpperCase()) + ' plan.'));
25
+ if (license.customerEmail) {
26
+ console.log(chalk_1.default.gray(' Email: ') + chalk_1.default.cyan(license.customerEmail));
27
+ }
28
+ console.log(chalk_1.default.gray(' Variant: ') + chalk_1.default.cyan(license.variantName || license.tier));
29
+ if (license.tier === 'pro') {
30
+ console.log(chalk_1.default.gray('\n You now have access to:'));
31
+ console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('PostgreSQL backups'));
32
+ console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('MySQL / MariaDB backups'));
33
+ console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Supabase backups'));
34
+ console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Unlimited snapshots'));
35
+ }
36
+ else if (license.tier === 'secure') {
37
+ console.log(chalk_1.default.gray('\n You now have access to:'));
38
+ console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Everything in Pro'));
39
+ console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Immutable cloud backups'));
40
+ console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Write-once retention'));
41
+ }
42
+ console.log(chalk_1.default.gray('\n Next: ') + chalk_1.default.cyan('oopsdb init') + chalk_1.default.gray(' to set up your database\n'));
43
+ }
44
+ catch (err) {
45
+ spinner.fail('Activation failed');
46
+ console.log(chalk_1.default.red(`\n ${err.message}`));
47
+ console.log(chalk_1.default.gray('\n Make sure your license key is correct.'));
48
+ console.log(chalk_1.default.gray(' Get help at ') + chalk_1.default.cyan('https://oopsdb.com\n'));
49
+ }
50
+ }
51
+ async function deactivateCommand() {
52
+ console.log(chalk_1.default.bold('\n OopsDB — License Deactivation\n'));
53
+ const license = (0, license_1.loadLicense)();
54
+ if (!license) {
55
+ console.log(chalk_1.default.yellow(' No active license found.\n'));
56
+ return;
57
+ }
58
+ const spinner = (0, ora_1.default)('Deactivating license...').start();
59
+ try {
60
+ await (0, license_1.deactivateLicense)();
61
+ spinner.succeed('License deactivated');
62
+ console.log(chalk_1.default.gray('\n You\'re back on the Free plan (SQLite only).'));
63
+ console.log(chalk_1.default.gray(' You can re-activate anytime with: ') + chalk_1.default.cyan('oopsdb activate <key>\n'));
64
+ }
65
+ catch (err) {
66
+ spinner.fail('Deactivation failed');
67
+ console.log(chalk_1.default.red(`\n ${err.message}\n`));
68
+ }
69
+ }
70
+ async function licenseStatusCommand() {
71
+ const license = (0, license_1.loadLicense)();
72
+ console.log(chalk_1.default.bold('\n OopsDB — License Status\n'));
73
+ if (!license) {
74
+ console.log(chalk_1.default.gray(' Plan: ') + chalk_1.default.white('Free'));
75
+ console.log(chalk_1.default.gray(' Access: ') + chalk_1.default.white('SQLite only'));
76
+ console.log(chalk_1.default.gray('\n Upgrade: ') + chalk_1.default.cyan('https://oopsdb.com') + chalk_1.default.gray(' → then run ') + chalk_1.default.cyan('oopsdb activate <key>\n'));
77
+ return;
78
+ }
79
+ console.log(chalk_1.default.gray(' Plan: ') + chalk_1.default.green(license.tier.toUpperCase()));
80
+ console.log(chalk_1.default.gray(' Key: ') + chalk_1.default.cyan(license.licenseKey.slice(0, 8) + '...' + license.licenseKey.slice(-4)));
81
+ console.log(chalk_1.default.gray(' Activated: ') + chalk_1.default.cyan(new Date(license.activatedAt).toLocaleDateString()));
82
+ if (license.customerEmail) {
83
+ console.log(chalk_1.default.gray(' Email: ') + chalk_1.default.cyan(license.customerEmail));
84
+ }
85
+ if (license.variantName) {
86
+ console.log(chalk_1.default.gray(' Variant: ') + chalk_1.default.cyan(license.variantName));
87
+ }
88
+ console.log();
89
+ }
@@ -0,0 +1,3 @@
1
+ export declare function cleanCommand(options: {
2
+ yes?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.cleanCommand = cleanCommand;
40
+ const inquirer = __importStar(require("inquirer"));
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const fs = __importStar(require("fs"));
43
+ const config_1 = require("../utils/config");
44
+ const dumper_1 = require("../utils/dumper");
45
+ async function cleanCommand(options) {
46
+ const configDir = (0, config_1.getConfigDir)();
47
+ if (!fs.existsSync(configDir)) {
48
+ console.log(chalk_1.default.yellow('\n Nothing to clean — no .oopsdb/ directory found.\n'));
49
+ return;
50
+ }
51
+ const snapshots = (0, dumper_1.listSnapshots)();
52
+ const dirSize = getDirSize(configDir);
53
+ console.log(chalk_1.default.bold('\n OopsDB Clean\n'));
54
+ console.log(chalk_1.default.gray(` Directory: ${configDir}`));
55
+ console.log(chalk_1.default.gray(` Snapshots: ${snapshots.length}`));
56
+ console.log(chalk_1.default.gray(` Total size: ${formatSize(dirSize)}\n`));
57
+ if (!options.yes) {
58
+ const { confirm } = await inquirer.prompt([
59
+ {
60
+ type: 'confirm',
61
+ name: 'confirm',
62
+ message: chalk_1.default.yellow('This will permanently delete all backups and config. Continue?'),
63
+ default: false,
64
+ },
65
+ ]);
66
+ if (!confirm) {
67
+ console.log(chalk_1.default.gray('\n Clean cancelled.\n'));
68
+ return;
69
+ }
70
+ }
71
+ fs.rmSync(configDir, { recursive: true, force: true });
72
+ console.log(chalk_1.default.green(`\n Removed ${configDir} (${snapshots.length} snapshot(s), ${formatSize(dirSize)})\n`));
73
+ }
74
+ function getDirSize(dir) {
75
+ let total = 0;
76
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
77
+ const full = `${dir}/${entry.name}`;
78
+ if (entry.isDirectory()) {
79
+ total += getDirSize(full);
80
+ }
81
+ else {
82
+ total += fs.statSync(full).size;
83
+ }
84
+ }
85
+ return total;
86
+ }
87
+ function formatSize(bytes) {
88
+ if (bytes < 1024)
89
+ return `${bytes} B`;
90
+ if (bytes < 1024 * 1024)
91
+ return `${Math.round(bytes / 1024)} KB`;
92
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
93
+ }
@@ -0,0 +1 @@
1
+ export declare function initCommand(): Promise<void>;
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.initCommand = initCommand;
40
+ const inquirer = __importStar(require("inquirer"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const ora_1 = __importDefault(require("ora"));
45
+ const config_1 = require("../utils/config");
46
+ const dumper_1 = require("../utils/dumper");
47
+ const preflight_1 = require("../utils/preflight");
48
+ const license_1 = require("../utils/license");
49
+ async function initCommand() {
50
+ console.log(chalk_1.default.bold('\n OopsDB Setup\n'));
51
+ console.log(chalk_1.default.gray(' Protect your database from AI-powered disasters.\n'));
52
+ const existing = (0, config_1.loadConfig)();
53
+ if (existing) {
54
+ const { overwrite } = await inquirer.prompt([
55
+ {
56
+ type: 'confirm',
57
+ name: 'overwrite',
58
+ message: 'OopsDB is already configured in this directory. Overwrite?',
59
+ default: false,
60
+ },
61
+ ]);
62
+ if (!overwrite) {
63
+ console.log(chalk_1.default.yellow('\n Setup cancelled.\n'));
64
+ return;
65
+ }
66
+ }
67
+ const { dbType } = await inquirer.prompt([
68
+ {
69
+ type: 'list',
70
+ name: 'dbType',
71
+ message: 'What database are you using?',
72
+ choices: [
73
+ { name: 'Supabase', value: 'supabase' },
74
+ { name: 'PostgreSQL (Neon, local, other hosted)', value: 'postgres' },
75
+ { name: 'MySQL / MariaDB', value: 'mysql' },
76
+ { name: 'SQLite (local file)', value: 'sqlite' },
77
+ ],
78
+ },
79
+ ]);
80
+ // Check license for non-SQLite databases
81
+ if ((0, license_1.requiresLicense)(dbType)) {
82
+ const tier = (0, license_1.getCurrentTier)();
83
+ if (tier === 'free') {
84
+ console.log(chalk_1.default.yellow('\n ⚠ ' + dbType.charAt(0).toUpperCase() + dbType.slice(1) + ' requires a Pro or Secure license.\n'));
85
+ console.log(chalk_1.default.white(' Free plan supports SQLite only.'));
86
+ console.log(chalk_1.default.gray('\n To upgrade:'));
87
+ console.log(chalk_1.default.cyan(' 1. ') + chalk_1.default.white('Get a license at ') + chalk_1.default.cyan('https://oopsdb.com'));
88
+ console.log(chalk_1.default.cyan(' 2. ') + chalk_1.default.white('Run: ') + chalk_1.default.cyan('oopsdb activate <your-license-key>'));
89
+ console.log(chalk_1.default.cyan(' 3. ') + chalk_1.default.white('Run: ') + chalk_1.default.cyan('oopsdb init') + chalk_1.default.gray(' again\n'));
90
+ return;
91
+ }
92
+ }
93
+ // Supabase and plain Postgres both use pg_dump/psql
94
+ const toolType = dbType === 'supabase' ? 'postgres' : dbType;
95
+ // Pre-flight: check that the required DB tools are installed
96
+ console.log(chalk_1.default.gray('\n Checking for required tools...\n'));
97
+ const toolsOk = await (0, preflight_1.preflightCheck)(toolType, 'both');
98
+ if (!toolsOk) {
99
+ console.log(chalk_1.default.red('\n Missing required database tools. Install them and re-run `oopsdb init`.\n'));
100
+ process.exit(1);
101
+ }
102
+ console.log();
103
+ let dbConfig;
104
+ if (dbType === 'supabase') {
105
+ dbConfig = await setupSupabase();
106
+ }
107
+ else if (dbType === 'sqlite') {
108
+ const { database } = await inquirer.prompt([
109
+ {
110
+ type: 'input',
111
+ name: 'database',
112
+ message: 'Path to your SQLite database file:',
113
+ validate: (input) => {
114
+ if (input.length === 0)
115
+ return 'Please enter a path';
116
+ const resolved = path.resolve(input);
117
+ if (!fs.existsSync(resolved))
118
+ return `File not found: ${resolved}`;
119
+ return true;
120
+ },
121
+ },
122
+ ]);
123
+ dbConfig = { type: 'sqlite', database: path.resolve(database) };
124
+ }
125
+ else {
126
+ const answers = await inquirer.prompt([
127
+ {
128
+ type: 'input',
129
+ name: 'host',
130
+ message: 'Database host:',
131
+ default: 'localhost',
132
+ },
133
+ {
134
+ type: 'input',
135
+ name: 'port',
136
+ message: 'Database port:',
137
+ default: dbType === 'postgres' ? '5432' : '3306',
138
+ validate: (input) => {
139
+ const port = parseInt(input, 10);
140
+ if (isNaN(port) || port < 1 || port > 65535)
141
+ return 'Port must be a number between 1 and 65535';
142
+ return true;
143
+ },
144
+ },
145
+ {
146
+ type: 'input',
147
+ name: 'user',
148
+ message: 'Database user:',
149
+ default: dbType === 'postgres' ? 'postgres' : 'root',
150
+ },
151
+ {
152
+ type: 'password',
153
+ name: 'password',
154
+ message: 'Database password:',
155
+ },
156
+ {
157
+ type: 'input',
158
+ name: 'database',
159
+ message: 'Database name:',
160
+ validate: (input) => (input.length > 0 ? true : 'Please enter a database name'),
161
+ },
162
+ ]);
163
+ dbConfig = {
164
+ type: dbType,
165
+ host: answers.host,
166
+ port: parseInt(answers.port, 10),
167
+ user: answers.user,
168
+ password: answers.password,
169
+ database: answers.database,
170
+ };
171
+ }
172
+ const config = {
173
+ db: dbConfig,
174
+ createdAt: new Date().toISOString(),
175
+ };
176
+ (0, config_1.saveConfig)(config);
177
+ console.log(chalk_1.default.green('\n Config saved to .oopsdb/config.json (encrypted)\n'));
178
+ const { takeSnapshot } = await inquirer.prompt([
179
+ {
180
+ type: 'confirm',
181
+ name: 'takeSnapshot',
182
+ message: 'Take an initial snapshot now?',
183
+ default: true,
184
+ },
185
+ ]);
186
+ if (takeSnapshot) {
187
+ const spinner = (0, ora_1.default)('Taking initial snapshot...').start();
188
+ try {
189
+ const file = await (0, dumper_1.createSnapshot)(dbConfig);
190
+ spinner.succeed(`Snapshot saved: ${file}`);
191
+ }
192
+ catch (err) {
193
+ spinner.fail(`Snapshot failed: ${err.message}`);
194
+ console.log(chalk_1.default.yellow('\n Tip: Make sure your database tools (pg_dump, mysqldump, sqlite3) are installed.\n'));
195
+ }
196
+ }
197
+ console.log(chalk_1.default.bold('\n You\'re all set! Next steps:\n'));
198
+ console.log(chalk_1.default.cyan(' oopsdb watch ') + chalk_1.default.gray('Start auto-backing up'));
199
+ console.log(chalk_1.default.cyan(' oopsdb snapshot ') + chalk_1.default.gray('Take a manual snapshot'));
200
+ console.log(chalk_1.default.cyan(' oopsdb restore ') + chalk_1.default.gray('Roll back to a backup'));
201
+ console.log(chalk_1.default.cyan(' oopsdb status ') + chalk_1.default.gray('See your backups'));
202
+ console.log();
203
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────'));
204
+ console.log(chalk_1.default.magenta(' New: ') + chalk_1.default.white('oopsdb secure'));
205
+ console.log(chalk_1.default.gray(' Immutable cloud backups that even a rogue AI can\'t delete.'));
206
+ console.log(chalk_1.default.gray(' Learn more: ') + chalk_1.default.cyan('oopsdb secure') + chalk_1.default.gray(' or ') + chalk_1.default.cyan('https://oopsdb.com/secure'));
207
+ console.log();
208
+ }
209
+ /**
210
+ * Parse a Supabase/Postgres connection string into DbConfig components.
211
+ * Supports: postgresql://user:password@host:port/database?sslmode=require
212
+ */
213
+ function parseConnectionString(connStr) {
214
+ const url = new URL(connStr);
215
+ return {
216
+ type: 'postgres',
217
+ supabase: true,
218
+ host: url.hostname,
219
+ port: parseInt(url.port, 10) || 5432,
220
+ user: decodeURIComponent(url.username),
221
+ password: decodeURIComponent(url.password),
222
+ database: url.pathname.slice(1) || 'postgres',
223
+ connectionString: connStr,
224
+ sslmode: url.searchParams.get('sslmode') || 'require',
225
+ };
226
+ }
227
+ async function setupSupabase() {
228
+ console.log(chalk_1.default.cyan(' Supabase Setup'));
229
+ console.log(chalk_1.default.gray(' Find your connection string in Supabase Dashboard → Settings → Database\n'));
230
+ const { method } = await inquirer.prompt([
231
+ {
232
+ type: 'list',
233
+ name: 'method',
234
+ message: 'How do you want to connect?',
235
+ choices: [
236
+ { name: 'Paste connection string (recommended)', value: 'connstring' },
237
+ { name: 'Enter details manually', value: 'manual' },
238
+ ],
239
+ },
240
+ ]);
241
+ if (method === 'connstring') {
242
+ const { connString } = await inquirer.prompt([
243
+ {
244
+ type: 'password',
245
+ name: 'connString',
246
+ message: 'Paste your Supabase connection string:',
247
+ validate: (input) => {
248
+ if (!input.startsWith('postgresql://') && !input.startsWith('postgres://')) {
249
+ return 'Connection string must start with postgresql:// or postgres://';
250
+ }
251
+ try {
252
+ new URL(input);
253
+ return true;
254
+ }
255
+ catch {
256
+ return 'Invalid connection string format';
257
+ }
258
+ },
259
+ },
260
+ ]);
261
+ const config = parseConnectionString(connString);
262
+ console.log(chalk_1.default.gray(`\n Parsed: ${config.host}:${config.port}/${config.database} (user: ${config.user})`));
263
+ console.log(chalk_1.default.gray(` SSL: ${config.sslmode}`));
264
+ console.log(chalk_1.default.green(' Supabase-specific flags: --no-owner --no-privileges --no-subscriptions\n'));
265
+ return config;
266
+ }
267
+ // Manual entry
268
+ const answers = await inquirer.prompt([
269
+ {
270
+ type: 'input',
271
+ name: 'host',
272
+ message: 'Supabase database host:',
273
+ default: 'db.YOUR_PROJECT_REF.supabase.co',
274
+ validate: (input) => input.length > 0 ? true : 'Please enter a host',
275
+ },
276
+ {
277
+ type: 'input',
278
+ name: 'port',
279
+ message: 'Port:',
280
+ default: '5432',
281
+ validate: (input) => {
282
+ const port = parseInt(input, 10);
283
+ if (isNaN(port) || port < 1 || port > 65535)
284
+ return 'Port must be 1-65535';
285
+ return true;
286
+ },
287
+ },
288
+ {
289
+ type: 'input',
290
+ name: 'user',
291
+ message: 'Database user:',
292
+ default: 'postgres',
293
+ },
294
+ {
295
+ type: 'password',
296
+ name: 'password',
297
+ message: 'Database password:',
298
+ },
299
+ {
300
+ type: 'input',
301
+ name: 'database',
302
+ message: 'Database name:',
303
+ default: 'postgres',
304
+ },
305
+ ]);
306
+ console.log(chalk_1.default.green('\n Supabase-specific flags will be applied automatically.\n'));
307
+ return {
308
+ type: 'postgres',
309
+ supabase: true,
310
+ host: answers.host,
311
+ port: parseInt(answers.port, 10),
312
+ user: answers.user,
313
+ password: answers.password,
314
+ database: answers.database,
315
+ sslmode: 'require',
316
+ };
317
+ }
@@ -0,0 +1 @@
1
+ export declare function restoreCommand(): Promise<void>;