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.
Files changed (28) hide show
  1. package/README.md +49 -8
  2. package/dist/assets/{index-DgkcaXbc.js → index-DWOX2re7.js} +332 -332
  3. package/dist/index.html +1 -1
  4. package/dist-server/index.js +94 -94
  5. package/dist-server/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/better_sqlite3/src/better_sqlite3.o.d.raw +82 -0
  6. package/dist-server/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o.d +4 -0
  7. package/dist-server/node_modules/better-sqlite3/build/Release/.deps/Release/sqlite3.a.d +1 -0
  8. package/dist-server/node_modules/better-sqlite3/build/Release/better_sqlite3.node +0 -0
  9. package/dist-server/node_modules/better-sqlite3/build/Release/obj/gen/sqlite3/sqlite3.c +692 -176
  10. package/dist-server/node_modules/better-sqlite3/build/Release/obj/gen/sqlite3/sqlite3.h +17 -5
  11. package/dist-server/node_modules/better-sqlite3/build/Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o +0 -0
  12. package/dist-server/node_modules/better-sqlite3/build/Release/sqlite3.a +0 -0
  13. package/dist-server/node_modules/better-sqlite3/deps/download.sh +1 -1
  14. package/dist-server/node_modules/better-sqlite3/deps/sqlite3/sqlite3.c +692 -176
  15. package/dist-server/node_modules/better-sqlite3/deps/sqlite3/sqlite3.h +17 -5
  16. package/dist-server/node_modules/better-sqlite3/package.json +1 -1
  17. package/dist-server/node_modules/better-sqlite3/src/better_sqlite3.cpp +1 -1
  18. package/dist-server/node_modules/better-sqlite3/src/util/helpers.cpp +1 -1
  19. package/dist-server/node_modules/better-sqlite3/src/util/macros.cpp +8 -1
  20. package/menubar/ERDBProTray.app/Contents/Info.plist +20 -0
  21. package/menubar/ERDBProTray.app/Contents/MacOS/ERDBProTray +0 -0
  22. package/menubar/ERDBProTray.swift +129 -0
  23. package/menubar/erdbpro-tray +0 -0
  24. package/menubar/icon.svg +14 -0
  25. package/menubar/launch.sh +4 -0
  26. package/menubar/tray.sh +59 -0
  27. package/package.json +10 -8
  28. 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, dbUrl, background) {
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' : 'inherit',
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
- console.log(`\n🚀 ERD Builder Pro v${VERSION}`);
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
- startServer(port, dbUrl, background);
386
+ // Launch menubar tray icon (macOS only, if bundled)
387
+ setTimeout(() => { launchMenubar(port); }, 1500);
237
388
 
238
- if (options.open && !background) {
239
- setTimeout(() => { open(`http://localhost:${port}`).catch(() => {}); }, 2000);
389
+ // If --background flag: skip menu, server runs detached
390
+ if (background) {
391
+ process.exit(0);
240
392
  }
241
- if (options.open && background) {
242
- setTimeout(() => { open(`http://localhost:${port}`).catch(() => {}); }, 3000);
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
- // Launch menubar tray icon (macOS only, if bundled)
246
- setTimeout(() => {
247
- const launched = launchMenubar(port);
248
- }, 1500);
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