erdbpro 2.4.5 → 2.5.2-beta.1
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/README.md +49 -8
- package/dist/assets/{index-DgkcaXbc.js → index-DWOX2re7.js} +332 -332
- package/dist/index.html +1 -1
- package/dist-server/index.js +94 -94
- package/dist-server/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/better_sqlite3/src/better_sqlite3.o.d.raw +82 -0
- package/dist-server/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o.d +4 -0
- package/dist-server/node_modules/better-sqlite3/build/Release/.deps/Release/sqlite3.a.d +1 -0
- package/dist-server/node_modules/better-sqlite3/build/Release/better_sqlite3.node +0 -0
- package/dist-server/node_modules/better-sqlite3/build/Release/obj/gen/sqlite3/sqlite3.c +692 -176
- package/dist-server/node_modules/better-sqlite3/build/Release/obj/gen/sqlite3/sqlite3.h +17 -5
- package/dist-server/node_modules/better-sqlite3/build/Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o +0 -0
- package/dist-server/node_modules/better-sqlite3/build/Release/sqlite3.a +0 -0
- package/dist-server/node_modules/better-sqlite3/deps/download.sh +1 -1
- package/dist-server/node_modules/better-sqlite3/deps/sqlite3/sqlite3.c +692 -176
- package/dist-server/node_modules/better-sqlite3/deps/sqlite3/sqlite3.h +17 -5
- package/dist-server/node_modules/better-sqlite3/package.json +1 -1
- package/dist-server/node_modules/better-sqlite3/src/better_sqlite3.cpp +1 -1
- package/dist-server/node_modules/better-sqlite3/src/util/helpers.cpp +1 -1
- package/dist-server/node_modules/better-sqlite3/src/util/macros.cpp +8 -1
- package/menubar/ERDBProTray.app/Contents/Info.plist +20 -0
- package/menubar/ERDBProTray.app/Contents/MacOS/ERDBProTray +0 -0
- package/menubar/ERDBProTray.swift +129 -0
- package/menubar/erdbpro-tray +0 -0
- package/menubar/icon.svg +14 -0
- package/menubar/launch.sh +4 -0
- package/menubar/tray.sh +59 -0
- package/package.json +10 -8
- package/src/cli.mjs +197 -28
package/src/cli.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import open from 'open';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
+
import http from 'node:http';
|
|
8
|
+
import readline from 'node:readline';
|
|
7
9
|
import { spawn } from 'node:child_process';
|
|
8
10
|
import { fileURLToPath } from 'node:url';
|
|
9
11
|
|
|
@@ -18,6 +20,7 @@ const ROOT = (() => {
|
|
|
18
20
|
})();
|
|
19
21
|
const PKG_ROOT = ROOT;
|
|
20
22
|
const DATA_DIR = path.join(os.homedir(), '.erdbpro');
|
|
23
|
+
const LOG_DIR = path.join(DATA_DIR, 'logs');
|
|
21
24
|
const DB_PATH = path.join(DATA_DIR, 'data.db');
|
|
22
25
|
const PID_FILE = path.join(DATA_DIR, 'server.pid');
|
|
23
26
|
const DEFAULT_PORT = 3101;
|
|
@@ -29,6 +32,7 @@ const UPDATE_URL = 'https://registry.npmjs.org/erdbpro/latest';
|
|
|
29
32
|
|
|
30
33
|
function ensureDataDir() {
|
|
31
34
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
35
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
function checkNodeVersion() {
|
|
@@ -52,7 +56,9 @@ function isServerRunning() {
|
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
function startServer(port,
|
|
59
|
+
function startServer(port, background) {
|
|
60
|
+
const dbUrl = `file:${DB_PATH}`;
|
|
61
|
+
const dbVariant = 'sqlite';
|
|
56
62
|
const serverScript = path.join(PKG_ROOT, 'dist-server', 'index.js');
|
|
57
63
|
|
|
58
64
|
if (!fs.existsSync(serverScript)) {
|
|
@@ -73,20 +79,36 @@ function startServer(port, dbUrl, background) {
|
|
|
73
79
|
const env = {
|
|
74
80
|
...process.env,
|
|
75
81
|
DATABASE_URL: dbUrl,
|
|
82
|
+
DB_VARIANT: dbVariant,
|
|
76
83
|
NODE_ENV: 'production',
|
|
77
84
|
PORT: String(port),
|
|
78
85
|
HOST: '127.0.0.1',
|
|
86
|
+
ERD_INSTALL_MODE: 'cli',
|
|
79
87
|
};
|
|
80
88
|
|
|
89
|
+
// Always redirect server logs to file. Only connect stdio for detached mode.
|
|
90
|
+
const dateTag = new Date().toISOString().replace(/[:.]/g, '-');
|
|
91
|
+
const outLog = path.join(LOG_DIR, `server-${dateTag}.out.log`);
|
|
92
|
+
const errLog = path.join(LOG_DIR, `server-${dateTag}.err.log`);
|
|
93
|
+
const outFd = fs.openSync(outLog, 'a');
|
|
94
|
+
const errFd = fs.openSync(errLog, 'a');
|
|
95
|
+
|
|
81
96
|
const spawnOpts = {
|
|
82
97
|
env,
|
|
83
98
|
cwd: PKG_ROOT,
|
|
84
|
-
stdio: background ? 'ignore' :
|
|
99
|
+
stdio: background ? ['ignore', outFd, errFd] : [process.stdin, outFd, errFd],
|
|
85
100
|
detached: !!background,
|
|
86
101
|
};
|
|
87
102
|
|
|
88
103
|
const child = spawn(process.execPath, [serverScript], spawnOpts);
|
|
89
104
|
|
|
105
|
+
// Clean up fd handles in parent after spawn
|
|
106
|
+
if (!background) {
|
|
107
|
+
// In foreground mode we still own stdin; close the log fds in parent
|
|
108
|
+
outFd && fs.closeSync(outFd);
|
|
109
|
+
errFd && fs.closeSync(errFd);
|
|
110
|
+
}
|
|
111
|
+
|
|
90
112
|
// Always write PID file so menubar Quit can stop the server
|
|
91
113
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
92
114
|
|
|
@@ -105,6 +127,7 @@ function startServer(port, dbUrl, background) {
|
|
|
105
127
|
child.on('exit', (code) => {
|
|
106
128
|
if (code !== 0 && !background) {
|
|
107
129
|
console.error(`❌ Server exited with code ${code}`);
|
|
130
|
+
console.error(` Logs: ${errLog}`);
|
|
108
131
|
}
|
|
109
132
|
if (fs.existsSync(PID_FILE)) {
|
|
110
133
|
try { fs.unlinkSync(PID_FILE); } catch { /* stale */ }
|
|
@@ -123,9 +146,9 @@ function startServer(port, dbUrl, background) {
|
|
|
123
146
|
process.on('SIGTERM', cleanup);
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
function stopServer() {
|
|
149
|
+
function stopServer(silent = false) {
|
|
127
150
|
if (!isServerRunning()) {
|
|
128
|
-
console.log('ℹ️ No server running.');
|
|
151
|
+
if (!silent) console.log('ℹ️ No server running.');
|
|
129
152
|
return;
|
|
130
153
|
}
|
|
131
154
|
try {
|
|
@@ -133,9 +156,9 @@ function stopServer() {
|
|
|
133
156
|
process.kill(pid, 'SIGTERM');
|
|
134
157
|
// Also kill menubar app
|
|
135
158
|
try { spawn('pkill', ['-f', 'erdbpro-tray'], { stdio: 'ignore' }); } catch {}
|
|
136
|
-
console.log(`🛑 Server stopped (PID: ${pid})`);
|
|
159
|
+
if (!silent) console.log(`🛑 Server stopped (PID: ${pid})`);
|
|
137
160
|
} catch {
|
|
138
|
-
console.log('🛑 Server already stopped.');
|
|
161
|
+
if (!silent) console.log('🛑 Server already stopped.');
|
|
139
162
|
}
|
|
140
163
|
}
|
|
141
164
|
|
|
@@ -195,19 +218,154 @@ program
|
|
|
195
218
|
.description('ERD Builder Pro CLI — Database design workspace, one command away.')
|
|
196
219
|
.version(VERSION);
|
|
197
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Poll the server health endpoint until it responds or times out.
|
|
223
|
+
*/
|
|
224
|
+
async function waitForServer(port, timeout = 10000) {
|
|
225
|
+
const start = Date.now();
|
|
226
|
+
while (Date.now() - start < timeout) {
|
|
227
|
+
try {
|
|
228
|
+
await new Promise((resolve, reject) => {
|
|
229
|
+
const req = http.get(`http://127.0.0.1:${port}/api/health`, (res) => {
|
|
230
|
+
resolve(res);
|
|
231
|
+
res.resume();
|
|
232
|
+
});
|
|
233
|
+
req.on('error', reject);
|
|
234
|
+
req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')); });
|
|
235
|
+
});
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
await new Promise(r => setTimeout(r, 500));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Arrow-key navigable menu — no number typing, just ↑↓ Enter.
|
|
246
|
+
* Zero logs — all output is in ~/.erdbpro/logs/
|
|
247
|
+
*/
|
|
248
|
+
async function showMenu(port) {
|
|
249
|
+
const choices = [
|
|
250
|
+
{ label: 'Web UI (Open in Browser)', action: 'open' },
|
|
251
|
+
{ label: 'Hide to Background', action: 'hide' },
|
|
252
|
+
{ label: 'Exit', action: 'exit' },
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
let selected = 0;
|
|
256
|
+
let firstDraw = true;
|
|
257
|
+
let keepMenu = true;
|
|
258
|
+
|
|
259
|
+
function drawMenu() {
|
|
260
|
+
if (firstDraw) {
|
|
261
|
+
process.stdout.write('\x1b[s');
|
|
262
|
+
firstDraw = false;
|
|
263
|
+
} else {
|
|
264
|
+
process.stdout.write('\x1b[u\x1b[J');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log('========================================');
|
|
268
|
+
console.log(` ERD Builder Pro (v${VERSION})`);
|
|
269
|
+
console.log(` 🚀 Server: http://localhost:${port}`);
|
|
270
|
+
console.log('========================================');
|
|
271
|
+
console.log('');
|
|
272
|
+
|
|
273
|
+
for (let i = 0; i < choices.length; i++) {
|
|
274
|
+
const prefix = i === selected ? ` ▶` : ` `;
|
|
275
|
+
const label = i === selected ? `\x1b[1m${choices[i].label}\x1b[0m` : choices[i].label;
|
|
276
|
+
console.log(`${prefix} ${label}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log('');
|
|
280
|
+
process.stdout.write(' ↑↓ move Enter select q quit ');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log('');
|
|
284
|
+
drawMenu();
|
|
285
|
+
|
|
286
|
+
readline.emitKeypressEvents(process.stdin);
|
|
287
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
288
|
+
|
|
289
|
+
function awaitKeypress() {
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
function handler(_, key) {
|
|
292
|
+
process.stdin.removeListener('keypress', handler);
|
|
293
|
+
resolve(key);
|
|
294
|
+
}
|
|
295
|
+
process.stdin.on('keypress', handler);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
while (keepMenu) {
|
|
300
|
+
// ── Menu loop: ↑↓ navigate, Enter picks ──
|
|
301
|
+
let picked;
|
|
302
|
+
while (true) {
|
|
303
|
+
const key = await awaitKeypress();
|
|
304
|
+
if (key.name === 'up') {
|
|
305
|
+
selected = (selected - 1 + choices.length) % choices.length;
|
|
306
|
+
drawMenu();
|
|
307
|
+
} else if (key.name === 'down') {
|
|
308
|
+
selected = (selected + 1) % choices.length;
|
|
309
|
+
drawMenu();
|
|
310
|
+
} else if (key.name === 'return') {
|
|
311
|
+
picked = choices[selected].action;
|
|
312
|
+
break;
|
|
313
|
+
} else if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
314
|
+
picked = 'exit';
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (picked === 'exit') {
|
|
320
|
+
keepMenu = false;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Execute action ──
|
|
325
|
+
process.stdout.write('\x1b[u\x1b[J'); // clear menu area
|
|
326
|
+
|
|
327
|
+
if (picked === 'hide') {
|
|
328
|
+
const pid = fs.readFileSync(PID_FILE, 'utf8').trim();
|
|
329
|
+
console.log(`\n 🔒 Running in background (PID: ${pid})`);
|
|
330
|
+
console.log(` 🌐 URL: http://localhost:${port}`);
|
|
331
|
+
console.log(' 🛑 Stop: erdbpro stop');
|
|
332
|
+
console.log(` 📋 Logs: ${LOG_DIR}\n`);
|
|
333
|
+
cleanup();
|
|
334
|
+
process.exit(0);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (picked === 'open') {
|
|
338
|
+
console.log('\n 🌐 Opening browser...');
|
|
339
|
+
await open(`http://localhost:${port}`).catch(() => {});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Wait for Enter, then redraw menu (no extra text) ──
|
|
343
|
+
console.log('\n Press Enter to go back to menu...');
|
|
344
|
+
await awaitKeypress(); // eat the Enter
|
|
345
|
+
|
|
346
|
+
firstDraw = true;
|
|
347
|
+
selected = 0;
|
|
348
|
+
drawMenu();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function cleanup() {
|
|
352
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
cleanup();
|
|
356
|
+
}
|
|
357
|
+
|
|
198
358
|
program
|
|
199
359
|
.command('start')
|
|
200
360
|
.description('Start ERD Builder Pro server')
|
|
201
361
|
.option('-p, --port <number>', 'Port to listen on', String(DEFAULT_PORT))
|
|
202
|
-
.option('--db-url <url>', 'Database URL (default: SQLite in ~/.erdbpro/)')
|
|
203
362
|
.option('-b, --background', 'Run server in background (detached)')
|
|
363
|
+
.option('--open', 'Skip menu and open browser immediately')
|
|
204
364
|
.option('-f, --force', 'Force start even if server appears to be running')
|
|
205
|
-
.option('--no-open', 'Do not open browser on start')
|
|
206
365
|
.action(async (options) => {
|
|
207
366
|
checkNodeVersion();
|
|
208
367
|
await checkForUpdates();
|
|
209
368
|
const port = parseInt(options.port, 10);
|
|
210
|
-
const dbUrl = options.dbUrl || `file:${DB_PATH}`;
|
|
211
369
|
const background = !!options.background;
|
|
212
370
|
|
|
213
371
|
if (isServerRunning() && !options.force) {
|
|
@@ -217,35 +375,46 @@ program
|
|
|
217
375
|
}
|
|
218
376
|
|
|
219
377
|
if (isServerRunning() && options.force) {
|
|
220
|
-
stopServer();
|
|
378
|
+
stopServer(true);
|
|
221
379
|
await new Promise(r => setTimeout(r, 500));
|
|
222
380
|
}
|
|
223
381
|
|
|
224
382
|
ensureDataDir();
|
|
225
383
|
|
|
226
|
-
|
|
227
|
-
console.log(` 📁 Data: ${DATA_DIR}`);
|
|
228
|
-
console.log(` 🌐 URL: http://localhost:${port}`);
|
|
229
|
-
if (dbUrl.startsWith('file:')) {
|
|
230
|
-
console.log(` 🗄️ DB: SQLite (${dbUrl.replace('file:', '')})`);
|
|
231
|
-
} else {
|
|
232
|
-
console.log(` 🗄️ DB: PostgreSQL`);
|
|
233
|
-
}
|
|
234
|
-
console.log();
|
|
384
|
+
startServer(port, background);
|
|
235
385
|
|
|
236
|
-
|
|
386
|
+
// Launch menubar tray icon (macOS only, if bundled)
|
|
387
|
+
setTimeout(() => { launchMenubar(port); }, 1500);
|
|
237
388
|
|
|
238
|
-
|
|
239
|
-
|
|
389
|
+
// If --background flag: skip menu, server runs detached
|
|
390
|
+
if (background) {
|
|
391
|
+
process.exit(0);
|
|
240
392
|
}
|
|
241
|
-
|
|
242
|
-
|
|
393
|
+
|
|
394
|
+
// If --open flag: skip menu, open browser immediately
|
|
395
|
+
if (options.open) {
|
|
396
|
+
await waitForServer(port);
|
|
397
|
+
await open(`http://localhost:${port}`).catch(() => {});
|
|
398
|
+
return; // keep alive for SIGINT
|
|
243
399
|
}
|
|
244
400
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
401
|
+
// Default interactive mode: wait for server, then show clean menu
|
|
402
|
+
await waitForServer(port);
|
|
403
|
+
|
|
404
|
+
// On SIGINT (Ctrl+C), stop server cleanly
|
|
405
|
+
process.on('SIGINT', () => {
|
|
406
|
+
console.log('\n 👋 Goodbye!\n');
|
|
407
|
+
stopServer(true);
|
|
408
|
+
process.exit(0);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Menu handles all actions internally (open, terminal, hide, exit)
|
|
412
|
+
// Only returns when user selects Exit (or q/Ctrl+C)
|
|
413
|
+
await showMenu(port);
|
|
414
|
+
|
|
415
|
+
console.log('\n 👋 Goodbye!\n');
|
|
416
|
+
stopServer(true);
|
|
417
|
+
process.exit(0);
|
|
249
418
|
});
|
|
250
419
|
|
|
251
420
|
program
|