mpx-db 1.1.0 → 1.1.3
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 +19 -9
- package/package.json +4 -6
- package/src/cli.js +72 -0
- package/src/commands/connections.js +2 -0
- package/src/commands/data.js +4 -2
- package/src/commands/migrate.js +42 -10
- package/src/commands/query.js +1 -4
- package/src/commands/schema.js +1 -1
- package/src/db/base-adapter.js +17 -1
- package/src/db/mysql-adapter.js +6 -1
- package/src/db/postgres-adapter.js +12 -1
- package/src/db/sqlite-adapter.js +5 -5
- package/src/mcp.js +1 -1
- package/src/schema.js +13 -0
- package/src/update.js +72 -0
package/LICENSE
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
Mesaplex Dual License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 Mesaplex
|
|
4
4
|
|
|
5
|
-
|
|
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:
|
|
5
|
+
This software is available under two licensing options:
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
1. FREE TIER (Personal Use)
|
|
8
|
+
- Limited daily usage
|
|
9
|
+
- Basic features only
|
|
10
|
+
- Personal and non-commercial use only
|
|
11
|
+
|
|
12
|
+
Permission is granted to use this software for personal, non-commercial
|
|
13
|
+
purposes subject to the usage limits described in the documentation.
|
|
14
|
+
|
|
15
|
+
2. PRO LICENSE (Commercial Use)
|
|
16
|
+
- Unlimited usage
|
|
17
|
+
- All features unlocked
|
|
18
|
+
- JSON/CSV export
|
|
19
|
+
- CI/CD integration
|
|
20
|
+
- Commercial use allowed
|
|
21
|
+
- Priority support
|
|
22
|
+
|
|
23
|
+
To obtain a Pro license, visit: https://mesaplex.com
|
|
14
24
|
|
|
15
25
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
26
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mpx-db",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Database management CLI - Connect, query, migrate, and manage databases from the terminal",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node --test test
|
|
11
|
+
"test": "node --test test/*.test.js",
|
|
12
12
|
"test:watch": "node --test --watch test/**/*.test.js",
|
|
13
13
|
"dev": "node bin/mpx-db.js"
|
|
14
14
|
},
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"json-output"
|
|
29
29
|
],
|
|
30
30
|
"author": "Mesaplex <support@mesaplex.com>",
|
|
31
|
-
"license": "
|
|
31
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
34
|
"url": "git+https://github.com/mesaplexdev/mpx-db.git"
|
|
@@ -43,9 +43,7 @@
|
|
|
43
43
|
"better-sqlite3": "^12.6.2",
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"cli-table3": "^0.6.5",
|
|
46
|
-
"commander": "^12.1.0"
|
|
47
|
-
"inquirer": "^10.0.0",
|
|
48
|
-
"yaml": "^2.6.1"
|
|
46
|
+
"commander": "^12.1.0"
|
|
49
47
|
},
|
|
50
48
|
"peerDependencies": {
|
|
51
49
|
"mysql2": "^3.11.5",
|
package/src/cli.js
CHANGED
|
@@ -153,6 +153,78 @@ program
|
|
|
153
153
|
.option('-o, --output <file>', 'Output file path')
|
|
154
154
|
.action((target, table, options) => exportData(target, table, { ...globalOptions, ...options }));
|
|
155
155
|
|
|
156
|
+
// Update subcommand
|
|
157
|
+
program
|
|
158
|
+
.command('update')
|
|
159
|
+
.description('Check for updates and optionally install the latest version')
|
|
160
|
+
.option('--check', 'Only check for updates (do not install)')
|
|
161
|
+
.action(async (options) => {
|
|
162
|
+
const { checkForUpdate, performUpdate } = await import('./update.js');
|
|
163
|
+
const jsonMode = globalOptions.json;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const info = checkForUpdate();
|
|
167
|
+
|
|
168
|
+
if (jsonMode) {
|
|
169
|
+
const output = {
|
|
170
|
+
current: info.current,
|
|
171
|
+
latest: info.latest,
|
|
172
|
+
updateAvailable: info.updateAvailable,
|
|
173
|
+
isGlobal: info.isGlobal
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (!options.check && info.updateAvailable) {
|
|
177
|
+
try {
|
|
178
|
+
const result = performUpdate(info.isGlobal);
|
|
179
|
+
output.updated = true;
|
|
180
|
+
output.newVersion = result.version;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
output.updated = false;
|
|
183
|
+
output.error = err.message;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log(JSON.stringify(output, null, 2));
|
|
188
|
+
process.exit(0);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Human-readable output
|
|
193
|
+
if (!info.updateAvailable) {
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log(chalk.green.bold(`✓ mpx-db v${info.current} is up to date`));
|
|
196
|
+
console.log('');
|
|
197
|
+
process.exit(0);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(chalk.yellow.bold(`⬆ Update available: v${info.current} → v${info.latest}`));
|
|
203
|
+
|
|
204
|
+
if (options.check) {
|
|
205
|
+
console.log(chalk.gray(`Run ${chalk.cyan('mpx-db update')} to install`));
|
|
206
|
+
console.log('');
|
|
207
|
+
process.exit(0);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(chalk.gray(`Installing v${info.latest}${info.isGlobal ? ' (global)' : ''}...`));
|
|
212
|
+
|
|
213
|
+
const result = performUpdate(info.isGlobal);
|
|
214
|
+
console.log(chalk.green.bold(`✓ Updated to v${result.version}`));
|
|
215
|
+
console.log('');
|
|
216
|
+
process.exit(0);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (jsonMode) {
|
|
219
|
+
console.log(JSON.stringify({ error: err.message, code: 'ERR_UPDATE' }, null, 2));
|
|
220
|
+
} else {
|
|
221
|
+
console.error(chalk.red.bold('\n❌ Update check failed:'), err.message);
|
|
222
|
+
console.error('');
|
|
223
|
+
}
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
156
228
|
// MCP subcommand
|
|
157
229
|
program
|
|
158
230
|
.command('mcp')
|
|
@@ -120,6 +120,7 @@ export async function removeConnection(name, options = {}) {
|
|
|
120
120
|
name,
|
|
121
121
|
message: deleted ? 'Connection deleted' : 'Connection not found'
|
|
122
122
|
}, null, 2));
|
|
123
|
+
if (!deleted) process.exit(1);
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
125
126
|
|
|
@@ -128,5 +129,6 @@ export async function removeConnection(name, options = {}) {
|
|
|
128
129
|
console.log(chalk.green(`✓ Deleted connection "${name}"`));
|
|
129
130
|
} else {
|
|
130
131
|
console.log(chalk.yellow(`Connection "${name}" not found`));
|
|
132
|
+
process.exit(1);
|
|
131
133
|
}
|
|
132
134
|
}
|
package/src/commands/data.js
CHANGED
|
@@ -14,10 +14,12 @@ export async function exportData(target, tableName, options = {}) {
|
|
|
14
14
|
db = await createConnection(connectionString);
|
|
15
15
|
|
|
16
16
|
// Query all data
|
|
17
|
-
const rows = await db.query(`SELECT * FROM ${tableName}`);
|
|
17
|
+
const rows = await db.query(`SELECT * FROM ${db.quoteIdentifier(tableName)}`);
|
|
18
18
|
|
|
19
19
|
if (rows.length === 0) {
|
|
20
|
-
if (
|
|
20
|
+
if (options.json) {
|
|
21
|
+
console.log(JSON.stringify({ success: true, rows: [], rowCount: 0 }, null, 2));
|
|
22
|
+
} else if (!options.quiet) {
|
|
21
23
|
console.log(chalk.yellow('No data to export'));
|
|
22
24
|
}
|
|
23
25
|
return;
|
package/src/commands/migrate.js
CHANGED
|
@@ -7,6 +7,44 @@ import { resolveConnection } from './query.js';
|
|
|
7
7
|
|
|
8
8
|
const MIGRATIONS_DIR = './migrations';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Split SQL into statements on semicolons, respecting quoted strings.
|
|
12
|
+
*/
|
|
13
|
+
function splitStatements(sql) {
|
|
14
|
+
const statements = [];
|
|
15
|
+
let current = '';
|
|
16
|
+
let inSingle = false;
|
|
17
|
+
let inDouble = false;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < sql.length; i++) {
|
|
20
|
+
const ch = sql[i];
|
|
21
|
+
const prev = i > 0 ? sql[i - 1] : '';
|
|
22
|
+
|
|
23
|
+
if (ch === "'" && !inDouble && prev !== '\\') {
|
|
24
|
+
inSingle = !inSingle;
|
|
25
|
+
} else if (ch === '"' && !inSingle && prev !== '\\') {
|
|
26
|
+
inDouble = !inDouble;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (ch === ';' && !inSingle && !inDouble) {
|
|
30
|
+
const trimmed = current.trim();
|
|
31
|
+
if (trimmed.length > 0) {
|
|
32
|
+
statements.push(trimmed);
|
|
33
|
+
}
|
|
34
|
+
current = '';
|
|
35
|
+
} else {
|
|
36
|
+
current += ch;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const trimmed = current.trim();
|
|
41
|
+
if (trimmed.length > 0) {
|
|
42
|
+
statements.push(trimmed);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return statements;
|
|
46
|
+
}
|
|
47
|
+
|
|
10
48
|
/**
|
|
11
49
|
* Initialize migrations directory
|
|
12
50
|
*/
|
|
@@ -287,17 +325,14 @@ export async function runMigrations(target, options = {}) {
|
|
|
287
325
|
console.log(chalk.gray(`→ ${name}`));
|
|
288
326
|
}
|
|
289
327
|
|
|
290
|
-
// Execute migration (split by semicolon
|
|
328
|
+
// Execute migration (split by semicolon, respecting quoted strings)
|
|
291
329
|
// Remove comment lines first, then split
|
|
292
330
|
const cleanedSQL = migration.up
|
|
293
331
|
.split('\n')
|
|
294
332
|
.filter(line => !line.trim().startsWith('--'))
|
|
295
333
|
.join('\n');
|
|
296
334
|
|
|
297
|
-
const statements = cleanedSQL
|
|
298
|
-
.split(';')
|
|
299
|
-
.map(s => s.trim())
|
|
300
|
-
.filter(s => s.length > 0);
|
|
335
|
+
const statements = splitStatements(cleanedSQL);
|
|
301
336
|
|
|
302
337
|
for (const statement of statements) {
|
|
303
338
|
await db.execute(statement);
|
|
@@ -385,17 +420,14 @@ export async function rollbackMigration(target, options = {}) {
|
|
|
385
420
|
console.log(chalk.cyan(`Rolling back: ${last.name}`));
|
|
386
421
|
}
|
|
387
422
|
|
|
388
|
-
// Execute rollback (split by semicolon
|
|
423
|
+
// Execute rollback (split by semicolon, respecting quoted strings)
|
|
389
424
|
// Remove comment lines first, then split
|
|
390
425
|
const cleanedSQL = migration.down
|
|
391
426
|
.split('\n')
|
|
392
427
|
.filter(line => !line.trim().startsWith('--'))
|
|
393
428
|
.join('\n');
|
|
394
429
|
|
|
395
|
-
const statements = cleanedSQL
|
|
396
|
-
.split(';')
|
|
397
|
-
.map(s => s.trim())
|
|
398
|
-
.filter(s => s.length > 0);
|
|
430
|
+
const statements = splitStatements(cleanedSQL);
|
|
399
431
|
|
|
400
432
|
for (const statement of statements) {
|
|
401
433
|
await db.execute(statement);
|
package/src/commands/query.js
CHANGED
|
@@ -36,15 +36,12 @@ export async function handleQuery(target, sql, options = {}) {
|
|
|
36
36
|
rowCount: rows.length,
|
|
37
37
|
duration
|
|
38
38
|
}, null, 2));
|
|
39
|
-
} else {
|
|
39
|
+
} else if (!options.quiet) {
|
|
40
40
|
// Display results
|
|
41
41
|
if (rows.length === 0) {
|
|
42
42
|
console.log(chalk.yellow('No rows returned'));
|
|
43
43
|
} else {
|
|
44
44
|
displayTable(rows);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!options.quiet) {
|
|
48
45
|
console.log(chalk.gray(`\n${rows.length} row(s) in ${duration}ms`));
|
|
49
46
|
}
|
|
50
47
|
}
|
package/src/commands/schema.js
CHANGED
|
@@ -224,7 +224,7 @@ export async function dumpSchema(target, options = {}) {
|
|
|
224
224
|
return def;
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
-
sqlOutput += `CREATE TABLE ${table.name} (\n`;
|
|
227
|
+
sqlOutput += `CREATE TABLE "${table.name}" (\n`;
|
|
228
228
|
sqlOutput += cols.join(',\n');
|
|
229
229
|
sqlOutput += '\n);\n\n';
|
|
230
230
|
}
|
package/src/db/base-adapter.js
CHANGED
|
@@ -8,6 +8,22 @@ export class BaseAdapter {
|
|
|
8
8
|
this.connection = null;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Validate and quote a table/identifier name to prevent SQL injection.
|
|
13
|
+
* Rejects names with dangerous characters. Override quoteChar in subclasses.
|
|
14
|
+
*/
|
|
15
|
+
quoteIdentifier(name) {
|
|
16
|
+
if (!name || typeof name !== 'string') {
|
|
17
|
+
throw new Error('Invalid identifier: must be a non-empty string');
|
|
18
|
+
}
|
|
19
|
+
// Allow alphanumeric, underscores, dots (schema.table), hyphens
|
|
20
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_.\\-]*$/.test(name)) {
|
|
21
|
+
throw new Error(`Invalid identifier: "${name}" contains disallowed characters`);
|
|
22
|
+
}
|
|
23
|
+
const q = this._identifierQuote || '"';
|
|
24
|
+
return `${q}${name.replace(new RegExp(`\\${q}`, 'g'), q + q)}${q}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
/**
|
|
12
28
|
* Connect to database
|
|
13
29
|
*/
|
|
@@ -63,7 +79,7 @@ export class BaseAdapter {
|
|
|
63
79
|
* Get table row count
|
|
64
80
|
*/
|
|
65
81
|
async getRowCount(tableName) {
|
|
66
|
-
const result = await this.query(`SELECT COUNT(*) as count FROM ${tableName}`);
|
|
82
|
+
const result = await this.query(`SELECT COUNT(*) as count FROM ${this.quoteIdentifier(tableName)}`);
|
|
67
83
|
return result[0].count;
|
|
68
84
|
}
|
|
69
85
|
|
package/src/db/mysql-adapter.js
CHANGED
|
@@ -4,6 +4,11 @@ import { BaseAdapter } from './base-adapter.js';
|
|
|
4
4
|
* MySQL adapter using mysql2
|
|
5
5
|
*/
|
|
6
6
|
export class MySQLAdapter extends BaseAdapter {
|
|
7
|
+
constructor(connectionString) {
|
|
8
|
+
super(connectionString);
|
|
9
|
+
this._identifierQuote = '`';
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
async connect() {
|
|
8
13
|
try {
|
|
9
14
|
const mysql = await import('mysql2/promise');
|
|
@@ -62,7 +67,7 @@ export class MySQLAdapter extends BaseAdapter {
|
|
|
62
67
|
const tables = [];
|
|
63
68
|
for (const row of rows) {
|
|
64
69
|
const countResult = await this.query(
|
|
65
|
-
`SELECT COUNT(*) as count FROM
|
|
70
|
+
`SELECT COUNT(*) as count FROM ${this.quoteIdentifier(row.name)}`
|
|
66
71
|
);
|
|
67
72
|
tables.push({
|
|
68
73
|
name: row.name,
|
|
@@ -64,7 +64,7 @@ export class PostgresAdapter extends BaseAdapter {
|
|
|
64
64
|
const tables = [];
|
|
65
65
|
for (const row of rows) {
|
|
66
66
|
const countResult = await this.query(
|
|
67
|
-
`SELECT COUNT(*) as count FROM ${row.name}`
|
|
67
|
+
`SELECT COUNT(*) as count FROM ${this.quoteIdentifier(row.name)}`
|
|
68
68
|
);
|
|
69
69
|
tables.push({
|
|
70
70
|
name: row.name,
|
|
@@ -127,6 +127,17 @@ export class PostgresAdapter extends BaseAdapter {
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
async recordMigration(name) {
|
|
131
|
+
await this.execute(
|
|
132
|
+
'INSERT INTO mpx_migrations (name, applied_at) VALUES ($1, $2)',
|
|
133
|
+
[name, new Date().toISOString()]
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async removeMigration(name) {
|
|
138
|
+
await this.execute('DELETE FROM mpx_migrations WHERE name = $1', [name]);
|
|
139
|
+
}
|
|
140
|
+
|
|
130
141
|
async ensureMigrationsTable() {
|
|
131
142
|
await this.execute(`
|
|
132
143
|
CREATE TABLE IF NOT EXISTS mpx_migrations (
|
package/src/db/sqlite-adapter.js
CHANGED
|
@@ -10,9 +10,9 @@ export class SQLiteAdapter extends BaseAdapter {
|
|
|
10
10
|
super(connectionString);
|
|
11
11
|
|
|
12
12
|
// Extract file path from sqlite:// or sqlite3://
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// sqlite:///tmp/foo.db → /tmp/foo.db (absolute)
|
|
14
|
+
// sqlite://mydb.db → mydb.db (relative)
|
|
15
|
+
this.dbPath = connectionString.replace(/^sqlite3?:\/\//, '');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async connect() {
|
|
@@ -76,7 +76,7 @@ export class SQLiteAdapter extends BaseAdapter {
|
|
|
76
76
|
|
|
77
77
|
const tables = [];
|
|
78
78
|
for (const row of rows) {
|
|
79
|
-
const countResult = await this.query(`SELECT COUNT(*) as count FROM ${row.name}`);
|
|
79
|
+
const countResult = await this.query(`SELECT COUNT(*) as count FROM ${this.quoteIdentifier(row.name)}`);
|
|
80
80
|
tables.push({
|
|
81
81
|
name: row.name,
|
|
82
82
|
type: row.type,
|
|
@@ -88,7 +88,7 @@ export class SQLiteAdapter extends BaseAdapter {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
async getTableSchema(tableName) {
|
|
91
|
-
const rows = await this.query(`PRAGMA table_info(${tableName})`);
|
|
91
|
+
const rows = await this.query(`PRAGMA table_info(${this.quoteIdentifier(tableName)})`);
|
|
92
92
|
|
|
93
93
|
return rows.map(row => ({
|
|
94
94
|
name: row.name,
|
package/src/mcp.js
CHANGED
|
@@ -261,7 +261,7 @@ export async function startMCPServer() {
|
|
|
261
261
|
const db = await createConnection(connectionString);
|
|
262
262
|
|
|
263
263
|
try {
|
|
264
|
-
const rows = await db.query(`SELECT * FROM ${args.table}`);
|
|
264
|
+
const rows = await db.query(`SELECT * FROM ${db.quoteIdentifier(args.table)}`);
|
|
265
265
|
await db.disconnect();
|
|
266
266
|
|
|
267
267
|
return {
|
package/src/schema.js
CHANGED
|
@@ -560,6 +560,19 @@ export function getSchema() {
|
|
|
560
560
|
examples: [
|
|
561
561
|
{ command: 'mpx-db mcp', description: 'Start MCP stdio server' }
|
|
562
562
|
]
|
|
563
|
+
},
|
|
564
|
+
update: {
|
|
565
|
+
description: 'Check for updates and optionally install the latest version',
|
|
566
|
+
usage: 'mpx-db update [--check] [--json]',
|
|
567
|
+
flags: {
|
|
568
|
+
'--check': { description: 'Only check for updates (do not install)', default: false },
|
|
569
|
+
'--json': { description: 'Machine-readable JSON output', default: false }
|
|
570
|
+
},
|
|
571
|
+
examples: [
|
|
572
|
+
{ command: 'mpx-db update', description: 'Check and install updates' },
|
|
573
|
+
{ command: 'mpx-db update --check', description: 'Just check for updates' },
|
|
574
|
+
{ command: 'mpx-db update --check --json', description: 'Check for updates (JSON output)' }
|
|
575
|
+
]
|
|
563
576
|
}
|
|
564
577
|
},
|
|
565
578
|
mcpConfig: {
|
package/src/update.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command
|
|
3
|
+
*
|
|
4
|
+
* Checks npm for the latest version of mpx-db and offers to update.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check npm registry for latest version
|
|
18
|
+
* @returns {object} { current, latest, updateAvailable, isGlobal }
|
|
19
|
+
*/
|
|
20
|
+
export function checkForUpdate() {
|
|
21
|
+
const current = pkg.version;
|
|
22
|
+
|
|
23
|
+
let latest;
|
|
24
|
+
try {
|
|
25
|
+
latest = execSync('npm view mpx-db version', { encoding: 'utf8', timeout: 10000 }).trim();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error('Failed to check npm registry: ' + (err.message || 'unknown error'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const updateAvailable = latest !== current && compareVersions(latest, current) > 0;
|
|
31
|
+
|
|
32
|
+
// Detect if installed globally
|
|
33
|
+
let isGlobal = false;
|
|
34
|
+
try {
|
|
35
|
+
const globalDir = execSync('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
36
|
+
isGlobal = __dirname.startsWith(globalDir) || process.argv[1]?.includes('node_modules/.bin');
|
|
37
|
+
} catch {
|
|
38
|
+
// Can't determine, assume local
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { current, latest, updateAvailable, isGlobal };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Perform the update
|
|
46
|
+
* @param {boolean} isGlobal - Install globally
|
|
47
|
+
* @returns {object} { success, version }
|
|
48
|
+
*/
|
|
49
|
+
export function performUpdate(isGlobal) {
|
|
50
|
+
const cmd = isGlobal ? 'npm install -g mpx-db@latest' : 'npm install mpx-db@latest';
|
|
51
|
+
try {
|
|
52
|
+
execSync(cmd, { encoding: 'utf8', timeout: 60000, stdio: 'pipe' });
|
|
53
|
+
// Verify
|
|
54
|
+
const newVersion = execSync('npm view mpx-db version', { encoding: 'utf8', timeout: 10000 }).trim();
|
|
55
|
+
return { success: true, version: newVersion };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
throw new Error('Update failed: ' + (err.message || 'unknown error'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Compare semver strings. Returns >0 if a > b, <0 if a < b, 0 if equal.
|
|
63
|
+
*/
|
|
64
|
+
export function compareVersions(a, b) {
|
|
65
|
+
const pa = a.split('.').map(Number);
|
|
66
|
+
const pb = b.split('.').map(Number);
|
|
67
|
+
for (let i = 0; i < 3; i++) {
|
|
68
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
69
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
}
|