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.
@@ -0,0 +1,251 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createSnapshot = createSnapshot;
37
+ exports.restoreSnapshot = restoreSnapshot;
38
+ exports.listSnapshots = listSnapshots;
39
+ const child_process_1 = require("child_process");
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ const crypto = __importStar(require("crypto"));
43
+ const promises_1 = require("stream/promises");
44
+ const config_1 = require("./config");
45
+ const CIPHER_ALGO = 'aes-256-cbc';
46
+ function timestamp() {
47
+ return new Date().toISOString().replace(/[:.]/g, '-');
48
+ }
49
+ function shellEscape(s) {
50
+ return `'${s.replace(/'/g, "'\\''")}'`;
51
+ }
52
+ /**
53
+ * Creates an encrypted, streamed snapshot of the database.
54
+ *
55
+ * For Postgres/MySQL: spawns pg_dump/mysqldump and pipes stdout → cipher → file.
56
+ * For SQLite: uses the native .backup command (always writes to a file), then
57
+ * streams that temp file through the cipher into the final encrypted output.
58
+ *
59
+ * Memory footprint stays near-zero regardless of database size.
60
+ */
61
+ async function createSnapshot(config) {
62
+ const backupsDir = (0, config_1.getBackupsDir)();
63
+ const ts = timestamp();
64
+ if (config.type === 'sqlite') {
65
+ return createSqliteSnapshot(config, backupsDir, ts);
66
+ }
67
+ // Postgres and MySQL both emit SQL to stdout — stream it through the cipher
68
+ const ext = config.supabase ? 'supabase' : config.type === 'postgres' ? 'pg' : 'mysql';
69
+ const outFile = path.join(backupsDir, `${ext}_${ts}.sql.enc`);
70
+ const { cmd, args, env } = getDumpCommand(config);
71
+ const iv = crypto.randomBytes(16);
72
+ // Write the IV as the first 16 bytes of the file
73
+ const outStream = fs.createWriteStream(outFile);
74
+ outStream.write(iv);
75
+ const cipher = crypto.createCipheriv(CIPHER_ALGO, (0, config_1.getEncryptionKey)(), iv);
76
+ const child = (0, child_process_1.spawn)(cmd, args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
77
+ let stderr = '';
78
+ child.stderr.on('data', (chunk) => {
79
+ stderr += chunk.toString();
80
+ });
81
+ const exitPromise = new Promise((resolve, reject) => {
82
+ child.on('error', (err) => reject(err));
83
+ child.on('close', (code) => {
84
+ if (code !== 0) {
85
+ reject(new Error(stderr || `${cmd} exited with code ${code}`));
86
+ }
87
+ else {
88
+ resolve();
89
+ }
90
+ });
91
+ });
92
+ await Promise.all([
93
+ (0, promises_1.pipeline)(child.stdout, cipher, outStream),
94
+ exitPromise,
95
+ ]);
96
+ return outFile;
97
+ }
98
+ /**
99
+ * Restores a database from an encrypted snapshot file by streaming:
100
+ * file → decipher → psql/mysql stdin.
101
+ *
102
+ * For SQLite: deciphers to a temp file, then copies over the original.
103
+ */
104
+ async function restoreSnapshot(config, snapshotPath) {
105
+ if (config.type === 'sqlite') {
106
+ return restoreSqliteSnapshot(config, snapshotPath);
107
+ }
108
+ const { cmd, args, env } = getRestoreCommand(config);
109
+ const inStream = fs.createReadStream(snapshotPath);
110
+ // Read the first 16 bytes as IV
111
+ const iv = await readIV(snapshotPath);
112
+ const fileStream = fs.createReadStream(snapshotPath, { start: 16 });
113
+ const decipher = crypto.createDecipheriv(CIPHER_ALGO, (0, config_1.getEncryptionKey)(), iv);
114
+ const child = (0, child_process_1.spawn)(cmd, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
115
+ let stderr = '';
116
+ child.stderr.on('data', (chunk) => {
117
+ stderr += chunk.toString();
118
+ });
119
+ const exitPromise = new Promise((resolve, reject) => {
120
+ child.on('error', (err) => reject(err));
121
+ child.on('close', (code) => {
122
+ if (code !== 0) {
123
+ reject(new Error(stderr || `${cmd} exited with code ${code}`));
124
+ }
125
+ else {
126
+ resolve();
127
+ }
128
+ });
129
+ });
130
+ await Promise.all([
131
+ (0, promises_1.pipeline)(fileStream, decipher, child.stdin),
132
+ exitPromise,
133
+ ]);
134
+ }
135
+ function listSnapshots() {
136
+ const backupsDir = (0, config_1.getBackupsDir)();
137
+ if (!fs.existsSync(backupsDir))
138
+ return [];
139
+ return fs
140
+ .readdirSync(backupsDir)
141
+ .filter((f) => f.endsWith('.sql.enc') || f.endsWith('.db.enc'))
142
+ .map((f) => {
143
+ const fullPath = path.join(backupsDir, f);
144
+ const stat = fs.statSync(fullPath);
145
+ return { file: fullPath, time: stat.mtime, size: stat.size };
146
+ })
147
+ .sort((a, b) => b.time.getTime() - a.time.getTime());
148
+ }
149
+ // ─── SQLite helpers ──────────────────────────────────────────────────────────
150
+ async function createSqliteSnapshot(config, backupsDir, ts) {
151
+ const outFile = path.join(backupsDir, `sqlite_${ts}.db.enc`);
152
+ const tmpFile = path.join(backupsDir, `sqlite_${ts}.db.tmp`);
153
+ // sqlite3 .backup writes directly to a file — we can't stream its stdout.
154
+ // Use the native backup, then stream the result through the cipher.
155
+ const child = (0, child_process_1.spawn)('sqlite3', [config.database, `.backup '${tmpFile}'`]);
156
+ await new Promise((resolve, reject) => {
157
+ child.on('error', (err) => reject(err));
158
+ child.on('close', (code) => {
159
+ if (code !== 0)
160
+ reject(new Error(`sqlite3 backup exited with code ${code}`));
161
+ else
162
+ resolve();
163
+ });
164
+ });
165
+ // Stream the tmp file through cipher to the encrypted output
166
+ const iv = crypto.randomBytes(16);
167
+ const cipher = crypto.createCipheriv(CIPHER_ALGO, (0, config_1.getEncryptionKey)(), iv);
168
+ const outStream = fs.createWriteStream(outFile);
169
+ outStream.write(iv);
170
+ await (0, promises_1.pipeline)(fs.createReadStream(tmpFile), cipher, outStream);
171
+ // Remove the unencrypted temp file
172
+ fs.unlinkSync(tmpFile);
173
+ return outFile;
174
+ }
175
+ async function restoreSqliteSnapshot(config, snapshotPath) {
176
+ const iv = await readIV(snapshotPath);
177
+ const decipher = crypto.createDecipheriv(CIPHER_ALGO, (0, config_1.getEncryptionKey)(), iv);
178
+ const fileStream = fs.createReadStream(snapshotPath, { start: 16 });
179
+ const outStream = fs.createWriteStream(config.database);
180
+ await (0, promises_1.pipeline)(fileStream, decipher, outStream);
181
+ }
182
+ // ─── Command builders ────────────────────────────────────────────────────────
183
+ function getDumpCommand(config) {
184
+ const env = { ...process.env };
185
+ if (config.type === 'postgres') {
186
+ if (config.password)
187
+ env.PGPASSWORD = config.password;
188
+ if (config.sslmode)
189
+ env.PGSSLMODE = config.sslmode;
190
+ const args = [
191
+ '-h', config.host || 'localhost',
192
+ '-p', String(config.port || 5432),
193
+ '-U', config.user || 'postgres',
194
+ ];
195
+ // Supabase-specific: skip ownership/privileges that conflict with managed Postgres
196
+ if (config.supabase) {
197
+ args.push('--no-owner', '--no-privileges', '--no-subscriptions');
198
+ }
199
+ args.push(config.database);
200
+ return { cmd: 'pg_dump', args, env };
201
+ }
202
+ // mysql
203
+ const args = [
204
+ '-h', config.host || 'localhost',
205
+ '-P', String(config.port || 3306),
206
+ '-u', config.user || 'root',
207
+ ];
208
+ if (config.password)
209
+ args.push(`-p${config.password}`);
210
+ args.push(config.database);
211
+ return { cmd: 'mysqldump', args, env };
212
+ }
213
+ function getRestoreCommand(config) {
214
+ const env = { ...process.env };
215
+ if (config.type === 'postgres') {
216
+ if (config.password)
217
+ env.PGPASSWORD = config.password;
218
+ if (config.sslmode)
219
+ env.PGSSLMODE = config.sslmode;
220
+ return {
221
+ cmd: 'psql',
222
+ args: [
223
+ '-h', config.host || 'localhost',
224
+ '-p', String(config.port || 5432),
225
+ '-U', config.user || 'postgres',
226
+ config.database,
227
+ ],
228
+ env,
229
+ };
230
+ }
231
+ // mysql
232
+ const args = [
233
+ '-h', config.host || 'localhost',
234
+ '-P', String(config.port || 3306),
235
+ '-u', config.user || 'root',
236
+ ];
237
+ if (config.password)
238
+ args.push(`-p${config.password}`);
239
+ args.push(config.database);
240
+ return { cmd: 'mysql', args, env };
241
+ }
242
+ // ─── Utilities ───────────────────────────────────────────────────────────────
243
+ function readIV(filePath) {
244
+ return new Promise((resolve, reject) => {
245
+ const stream = fs.createReadStream(filePath, { start: 0, end: 15 });
246
+ const chunks = [];
247
+ stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
248
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
249
+ stream.on('error', reject);
250
+ });
251
+ }
@@ -0,0 +1,29 @@
1
+ export type Tier = 'free' | 'pro' | 'secure';
2
+ export interface LicenseInfo {
3
+ licenseKey: string;
4
+ instanceId: string;
5
+ tier: Tier;
6
+ activatedAt: string;
7
+ customerName?: string;
8
+ customerEmail?: string;
9
+ variantName?: string;
10
+ }
11
+ export declare function loadLicense(): LicenseInfo | null;
12
+ export declare function removeLicense(): void;
13
+ export declare function getCurrentTier(): Tier;
14
+ export declare function requiresLicense(dbType: string): boolean;
15
+ /**
16
+ * Activate a license key with LemonSqueezy.
17
+ * Returns the license info on success, throws on failure.
18
+ */
19
+ export declare function activateLicense(licenseKey: string): Promise<LicenseInfo>;
20
+ /**
21
+ * Deactivate the current license.
22
+ */
23
+ export declare function deactivateLicense(): Promise<void>;
24
+ /**
25
+ * Validate the current license is still active.
26
+ * Returns true if valid, false if invalid/expired.
27
+ * On network error, returns true (offline grace).
28
+ */
29
+ export declare function validateLicense(): Promise<boolean>;
@@ -0,0 +1,205 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadLicense = loadLicense;
37
+ exports.removeLicense = removeLicense;
38
+ exports.getCurrentTier = getCurrentTier;
39
+ exports.requiresLicense = requiresLicense;
40
+ exports.activateLicense = activateLicense;
41
+ exports.deactivateLicense = deactivateLicense;
42
+ exports.validateLicense = validateLicense;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ const https = __importStar(require("https"));
47
+ // License stored globally in ~/.oopsdb/license.json (not per-project)
48
+ const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.oopsdb');
49
+ const LICENSE_FILE = path.join(GLOBAL_CONFIG_DIR, 'license.json');
50
+ // LemonSqueezy license API
51
+ const LEMONSQUEEZY_API = 'https://api.lemonsqueezy.com/v1/licenses';
52
+ function ensureGlobalDir() {
53
+ if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
54
+ fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
55
+ }
56
+ }
57
+ function loadLicense() {
58
+ if (!fs.existsSync(LICENSE_FILE))
59
+ return null;
60
+ try {
61
+ return JSON.parse(fs.readFileSync(LICENSE_FILE, 'utf8'));
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ function saveLicense(license) {
68
+ ensureGlobalDir();
69
+ fs.writeFileSync(LICENSE_FILE, JSON.stringify(license, null, 2), 'utf8');
70
+ }
71
+ function removeLicense() {
72
+ if (fs.existsSync(LICENSE_FILE)) {
73
+ fs.unlinkSync(LICENSE_FILE);
74
+ }
75
+ }
76
+ function getCurrentTier() {
77
+ const license = loadLicense();
78
+ if (!license)
79
+ return 'free';
80
+ return license.tier;
81
+ }
82
+ function requiresLicense(dbType) {
83
+ return dbType !== 'sqlite';
84
+ }
85
+ function getInstanceName() {
86
+ return `${os.userInfo().username}@${os.hostname()}`;
87
+ }
88
+ /**
89
+ * Determine the tier from the LemonSqueezy variant name.
90
+ */
91
+ function tierFromVariant(variantName) {
92
+ const lower = variantName.toLowerCase();
93
+ if (lower.includes('secure'))
94
+ return 'secure';
95
+ if (lower.includes('pro'))
96
+ return 'pro';
97
+ return 'pro'; // default paid tier
98
+ }
99
+ /**
100
+ * Make a POST request to LemonSqueezy license API.
101
+ */
102
+ function lemonSqueezyRequest(endpoint, body) {
103
+ return new Promise((resolve, reject) => {
104
+ const postData = new URLSearchParams(body).toString();
105
+ const options = {
106
+ hostname: 'api.lemonsqueezy.com',
107
+ path: `/v1/licenses/${endpoint}`,
108
+ method: 'POST',
109
+ headers: {
110
+ 'Content-Type': 'application/x-www-form-urlencoded',
111
+ 'Content-Length': Buffer.byteLength(postData),
112
+ 'Accept': 'application/json',
113
+ },
114
+ };
115
+ const req = https.request(options, (res) => {
116
+ let data = '';
117
+ res.on('data', (chunk) => { data += chunk; });
118
+ res.on('end', () => {
119
+ try {
120
+ resolve(JSON.parse(data));
121
+ }
122
+ catch {
123
+ reject(new Error(`Invalid response from license server`));
124
+ }
125
+ });
126
+ });
127
+ req.on('error', (err) => {
128
+ reject(new Error(`Could not reach license server: ${err.message}`));
129
+ });
130
+ req.setTimeout(10000, () => {
131
+ req.destroy();
132
+ reject(new Error('License server request timed out'));
133
+ });
134
+ req.write(postData);
135
+ req.end();
136
+ });
137
+ }
138
+ /**
139
+ * Activate a license key with LemonSqueezy.
140
+ * Returns the license info on success, throws on failure.
141
+ */
142
+ async function activateLicense(licenseKey) {
143
+ const instanceName = getInstanceName();
144
+ const response = await lemonSqueezyRequest('activate', {
145
+ license_key: licenseKey,
146
+ instance_name: instanceName,
147
+ });
148
+ if (response.error) {
149
+ throw new Error(response.error);
150
+ }
151
+ if (!response.activated && !response.license_key) {
152
+ throw new Error(response.error || 'Activation failed. Check your license key.');
153
+ }
154
+ const variantName = response.meta?.variant_name || '';
155
+ const tier = tierFromVariant(variantName);
156
+ const license = {
157
+ licenseKey,
158
+ instanceId: response.instance?.id || '',
159
+ tier,
160
+ activatedAt: new Date().toISOString(),
161
+ customerName: response.meta?.customer_name,
162
+ customerEmail: response.meta?.customer_email,
163
+ variantName,
164
+ };
165
+ saveLicense(license);
166
+ return license;
167
+ }
168
+ /**
169
+ * Deactivate the current license.
170
+ */
171
+ async function deactivateLicense() {
172
+ const license = loadLicense();
173
+ if (!license) {
174
+ throw new Error('No active license found.');
175
+ }
176
+ const response = await lemonSqueezyRequest('deactivate', {
177
+ license_key: license.licenseKey,
178
+ instance_id: license.instanceId,
179
+ });
180
+ if (response.error && !response.deactivated) {
181
+ throw new Error(response.error);
182
+ }
183
+ removeLicense();
184
+ }
185
+ /**
186
+ * Validate the current license is still active.
187
+ * Returns true if valid, false if invalid/expired.
188
+ * On network error, returns true (offline grace).
189
+ */
190
+ async function validateLicense() {
191
+ const license = loadLicense();
192
+ if (!license)
193
+ return false;
194
+ try {
195
+ const response = await lemonSqueezyRequest('validate', {
196
+ license_key: license.licenseKey,
197
+ instance_id: license.instanceId,
198
+ });
199
+ return response.valid === true;
200
+ }
201
+ catch {
202
+ // Offline grace: if we can't reach the server, trust the local license
203
+ return true;
204
+ }
205
+ }
@@ -0,0 +1,2 @@
1
+ import { DbConfig } from './config';
2
+ export declare function preflightCheck(dbType: DbConfig['type'], mode?: 'dump' | 'restore' | 'both'): Promise<boolean>;
@@ -0,0 +1,101 @@
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.preflightCheck = preflightCheck;
7
+ const child_process_1 = require("child_process");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const TOOL_MAP = {
10
+ postgres: {
11
+ dump: {
12
+ command: 'pg_dump',
13
+ versionFlag: '--version',
14
+ name: 'pg_dump',
15
+ installHint: ' brew install postgresql (macOS)\n' +
16
+ ' sudo apt install postgresql-client (Ubuntu/Debian)\n' +
17
+ ' choco install postgresql (Windows)',
18
+ },
19
+ restore: {
20
+ command: 'psql',
21
+ versionFlag: '--version',
22
+ name: 'psql',
23
+ installHint: ' brew install postgresql (macOS)\n' +
24
+ ' sudo apt install postgresql-client (Ubuntu/Debian)\n' +
25
+ ' choco install postgresql (Windows)',
26
+ },
27
+ },
28
+ mysql: {
29
+ dump: {
30
+ command: 'mysqldump',
31
+ versionFlag: '--version',
32
+ name: 'mysqldump',
33
+ installHint: ' brew install mysql-client (macOS)\n' +
34
+ ' sudo apt install mysql-client (Ubuntu/Debian)\n' +
35
+ ' choco install mysql-cli (Windows)',
36
+ },
37
+ restore: {
38
+ command: 'mysql',
39
+ versionFlag: '--version',
40
+ name: 'mysql',
41
+ installHint: ' brew install mysql-client (macOS)\n' +
42
+ ' sudo apt install mysql-client (Ubuntu/Debian)\n' +
43
+ ' choco install mysql-cli (Windows)',
44
+ },
45
+ },
46
+ sqlite: {
47
+ dump: {
48
+ command: 'sqlite3',
49
+ versionFlag: '--version',
50
+ name: 'sqlite3',
51
+ installHint: ' brew install sqlite (macOS)\n' +
52
+ ' sudo apt install sqlite3 (Ubuntu/Debian)\n' +
53
+ ' choco install sqlite (Windows)',
54
+ },
55
+ restore: {
56
+ command: 'sqlite3',
57
+ versionFlag: '--version',
58
+ name: 'sqlite3',
59
+ installHint: ' brew install sqlite (macOS)\n' +
60
+ ' sudo apt install sqlite3 (Ubuntu/Debian)\n' +
61
+ ' choco install sqlite (Windows)',
62
+ },
63
+ },
64
+ };
65
+ function checkTool(tool) {
66
+ return new Promise((resolve) => {
67
+ (0, child_process_1.exec)(`${tool.command} ${tool.versionFlag}`, (error, stdout) => {
68
+ if (error) {
69
+ resolve(null);
70
+ }
71
+ else {
72
+ resolve(stdout.trim().split('\n')[0]);
73
+ }
74
+ });
75
+ });
76
+ }
77
+ async function preflightCheck(dbType, mode = 'both') {
78
+ const tools = TOOL_MAP[dbType];
79
+ const checks = [];
80
+ if (mode === 'dump' || mode === 'both')
81
+ checks.push(tools.dump);
82
+ if (mode === 'restore' || mode === 'both') {
83
+ // Avoid duplicate check for sqlite (same tool)
84
+ if (tools.restore.command !== tools.dump.command || mode === 'restore') {
85
+ checks.push(tools.restore);
86
+ }
87
+ }
88
+ let allGood = true;
89
+ for (const tool of checks) {
90
+ const version = await checkTool(tool);
91
+ if (version) {
92
+ console.log(chalk_1.default.green(` ✓ ${tool.name} found`) + chalk_1.default.gray(` (${version})`));
93
+ }
94
+ else {
95
+ console.log(chalk_1.default.red(` ✗ ${tool.name} not found`));
96
+ console.log(chalk_1.default.yellow(`\n Install it:\n${tool.installHint}\n`));
97
+ allGood = false;
98
+ }
99
+ }
100
+ return allGood;
101
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "oopsdb",
3
+ "version": "1.0.0",
4
+ "description": "Don't let AI nuke your database. Auto-backup and 1-click restore for developers using Claude Code, OpenClaw, 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
+ "chalk": "^4.1.2",
47
+ "commander": "^12.1.0",
48
+ "inquirer": "^8.2.6",
49
+ "ora": "^5.4.1"
50
+ },
51
+ "devDependencies": {
52
+ "@types/inquirer": "^8.2.10",
53
+ "@types/node": "^22.0.0",
54
+ "testcontainers": "^11.12.0",
55
+ "typescript": "^5.7.0",
56
+ "vitest": "^4.0.18"
57
+ }
58
+ }