@logboard/cli 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.
Files changed (114) hide show
  1. package/.env.example +37 -0
  2. package/README.md +200 -0
  3. package/bin/logboard +536 -0
  4. package/client/logger.js +309 -0
  5. package/config/index.js +142 -0
  6. package/config.js +2 -0
  7. package/controllers/AnalyticsController.js +46 -0
  8. package/controllers/ApiAnalyticsController.js +129 -0
  9. package/controllers/ApiKeyController.js +58 -0
  10. package/controllers/AuthController.js +131 -0
  11. package/controllers/HealthController.js +56 -0
  12. package/controllers/LogController.js +197 -0
  13. package/controllers/OrgController.js +152 -0
  14. package/controllers/RoleConfigController.js +20 -0
  15. package/controllers/SettingsController.js +39 -0
  16. package/controllers/StreamController.js +55 -0
  17. package/controllers/UiController.js +789 -0
  18. package/controllers/UserController.js +79 -0
  19. package/lib/batchWriter.js +57 -0
  20. package/lib/cleanup.js +67 -0
  21. package/lib/ejs.js +103 -0
  22. package/lib/emitter.js +5 -0
  23. package/lib/healthMonitor.js +245 -0
  24. package/lib/logger.js +21 -0
  25. package/lib/streams.js +32 -0
  26. package/lib/theme.js +77 -0
  27. package/lib/userStore.js +13 -0
  28. package/lib/utils.js +44 -0
  29. package/middleware/apiKey.js +82 -0
  30. package/middleware/auth.js +55 -0
  31. package/middleware/ipWhitelist.js +59 -0
  32. package/middleware/org.js +85 -0
  33. package/middleware/pageAccess.js +20 -0
  34. package/middleware/rateLimit.js +29 -0
  35. package/middleware/roles.js +11 -0
  36. package/package.json +77 -0
  37. package/routes/alerts.js +18 -0
  38. package/routes/analytics.js +26 -0
  39. package/routes/api-analytics.js +30 -0
  40. package/routes/api-keys.js +12 -0
  41. package/routes/archive.js +91 -0
  42. package/routes/audit.js +50 -0
  43. package/routes/auth.js +22 -0
  44. package/routes/bookmarks.js +13 -0
  45. package/routes/health.js +11 -0
  46. package/routes/logs.js +88 -0
  47. package/routes/metrics.js +66 -0
  48. package/routes/notifications.js +14 -0
  49. package/routes/orgs.js +98 -0
  50. package/routes/registration.js +202 -0
  51. package/routes/role-config.js +97 -0
  52. package/routes/saved-searches.js +12 -0
  53. package/routes/server.js +151 -0
  54. package/routes/settings.js +28 -0
  55. package/routes/status.js +21 -0
  56. package/routes/stream.js +11 -0
  57. package/routes/super.js +129 -0
  58. package/routes/ui.js +120 -0
  59. package/routes/users.js +13 -0
  60. package/server.js +172 -0
  61. package/services/AlertRulesService.js +323 -0
  62. package/services/AnalyticsService.js +665 -0
  63. package/services/ApiAnalyticsService.js +471 -0
  64. package/services/ApiKeyService.js +166 -0
  65. package/services/AuditService.js +249 -0
  66. package/services/AuthService.js +234 -0
  67. package/services/BookmarkService.js +49 -0
  68. package/services/GlobalSettingsService.js +44 -0
  69. package/services/LogService.js +1066 -0
  70. package/services/MetricsService.js +116 -0
  71. package/services/NotificationService.js +70 -0
  72. package/services/OrgService.js +217 -0
  73. package/services/ReportService.js +247 -0
  74. package/services/RoleConfigService.js +201 -0
  75. package/services/SavedSearchService.js +63 -0
  76. package/services/SettingsService.js +220 -0
  77. package/services/UserService.js +121 -0
  78. package/setup.js +132 -0
  79. package/views/404.ejs +8 -0
  80. package/views/alerts.ejs +190 -0
  81. package/views/analytics.ejs +209 -0
  82. package/views/api-analytics.ejs +660 -0
  83. package/views/api-keys.ejs +150 -0
  84. package/views/archive.ejs +123 -0
  85. package/views/audit.ejs +314 -0
  86. package/views/bookmarks.ejs +54 -0
  87. package/views/custom-dashboard.ejs +162 -0
  88. package/views/dashboard.ejs +186 -0
  89. package/views/diff.ejs +98 -0
  90. package/views/health.ejs +269 -0
  91. package/views/heatmap.ejs +126 -0
  92. package/views/insights.ejs +334 -0
  93. package/views/invite.ejs +74 -0
  94. package/views/live.ejs +299 -0
  95. package/views/login.ejs +64 -0
  96. package/views/logo.png +0 -0
  97. package/views/logs.ejs +754 -0
  98. package/views/notifications.ejs +58 -0
  99. package/views/partials/head.ejs +282 -0
  100. package/views/partials/sidebar.ejs +168 -0
  101. package/views/register.ejs +100 -0
  102. package/views/roles.ejs +279 -0
  103. package/views/saved-searches.ejs +51 -0
  104. package/views/service-map.ejs +142 -0
  105. package/views/settings.ejs +1159 -0
  106. package/views/sidebar.ejs +129 -0
  107. package/views/status.ejs +100 -0
  108. package/views/super-admin-admins.ejs +58 -0
  109. package/views/super-admin-analytics.ejs +49 -0
  110. package/views/super-admin-orgs.ejs +310 -0
  111. package/views/super-admin-profile.ejs +77 -0
  112. package/views/super-admin-settings.ejs +108 -0
  113. package/views/super-admin-system.ejs +46 -0
  114. package/views/users.ejs +153 -0
package/bin/logboard ADDED
@@ -0,0 +1,536 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * LogBoard CLI
6
+ * Usage:
7
+ * logboard start Start server in foreground
8
+ * logboard start --port 9900 --data ./data
9
+ * logboard stop Stop background service
10
+ * logboard restart Restart service
11
+ * logboard status Show running status + version
12
+ * logboard setup First-run: create data/, .env, default users
13
+ * logboard service install Register as OS service (auto-start on boot)
14
+ * logboard service uninstall Remove OS service
15
+ * logboard service start Start OS service
16
+ * logboard service stop Stop OS service
17
+ * logboard service logs Tail service logs
18
+ * logboard open Open LogBoard in default browser
19
+ * logboard --version / -v
20
+ * logboard --help / -h
21
+ */
22
+
23
+ const path = require('path');
24
+ const fs = require('fs');
25
+ const os = require('os');
26
+ const { execSync, spawn, spawnSync } = require('child_process');
27
+
28
+ // ── Paths ─────────────────────────────────────────────────────────────────
29
+ const BLORQ_DIR = path.resolve(__dirname, '..'); // root of logboard install
30
+ const PKG = require(path.join(BLORQ_DIR, 'package.json'));
31
+ const VERSION = PKG.version;
32
+ const PID_FILE = path.join(os.homedir(), '.logboard', 'logboard.pid');
33
+ const LOG_FILE = path.join(os.homedir(), '.logboard', 'logboard.log');
34
+ const SERVICE_ID = 'dev.logboard.server';
35
+
36
+ // Colours
37
+ const C = {
38
+ reset: '\x1b[0m',
39
+ bold: '\x1b[1m',
40
+ dim: '\x1b[2m',
41
+ red: '\x1b[31m',
42
+ green: '\x1b[32m',
43
+ yellow: '\x1b[33m',
44
+ blue: '\x1b[34m',
45
+ cyan: '\x1b[36m',
46
+ white: '\x1b[37m',
47
+ };
48
+
49
+ function c(color, text) { return C[color] + text + C.reset; }
50
+ function ok(msg) { console.log(c('green',' ✓ ') + msg); }
51
+ function info(msg) { console.log(c('blue', ' → ') + msg); }
52
+ function warn(msg) { console.log(c('yellow',' ⚠ ') + msg); }
53
+ function fail(msg) { console.error(c('red', ' ✗ ') + msg); }
54
+ function title(msg) { console.log('\n' + c('bold', c('cyan', msg)) + '\n'); }
55
+
56
+ function ensureDir(d) {
57
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
58
+ }
59
+
60
+ // ── Arg parsing ───────────────────────────────────────────────────────────
61
+ const args = process.argv.slice(2);
62
+ const command = args[0] || 'help';
63
+ const subCmd = args[1] || '';
64
+
65
+ function getFlag(flag, defaultVal) {
66
+ const i = args.indexOf(flag);
67
+ return i !== -1 && args[i + 1] ? args[i + 1] : defaultVal;
68
+ }
69
+
70
+ function hasFlag(flag) { return args.includes(flag); }
71
+
72
+ // ── Banner ────────────────────────────────────────────────────────────────
73
+ function banner() {
74
+ console.log('');
75
+ console.log(c('bold', c('cyan', ' ██████╗ ██╗ ██████╗ ██████╗ ██████╗ ')));
76
+ console.log(c('bold', c('cyan', ' ██╔══██╗██║ ██╔═══██╗██╔══██╗██╔═══██╗')));
77
+ console.log(c('bold', c('cyan', ' ██████╔╝██║ ██║ ██║██████╔╝██║ ██║')));
78
+ console.log(c('bold', c('cyan', ' ██╔══██╗██║ ██║ ██║██╔══██╗██║▄▄ ██║')));
79
+ console.log(c('bold', c('cyan', ' ██████╔╝███████╗╚██████╔╝██║ ██║╚██████╔╝')));
80
+ console.log(c('bold', c('cyan', ' ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚══▀▀═╝ ')));
81
+ console.log('');
82
+ console.log(c('dim', ' Production-grade log aggregator ') + c('cyan', 'v' + VERSION));
83
+ console.log('');
84
+ }
85
+
86
+ // ── PID helpers ───────────────────────────────────────────────────────────
87
+ function readPid() {
88
+ try { return parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10) || null; }
89
+ catch { return null; }
90
+ }
91
+
92
+ function isRunning(pid) {
93
+ if (!pid) return false;
94
+ try { process.kill(pid, 0); return true; }
95
+ catch { return false; }
96
+ }
97
+
98
+ function writePid(pid) {
99
+ ensureDir(path.dirname(PID_FILE));
100
+ fs.writeFileSync(PID_FILE, String(pid), 'utf8');
101
+ }
102
+
103
+ function clearPid() {
104
+ try { fs.unlinkSync(PID_FILE); } catch {}
105
+ }
106
+
107
+ // ── Platform detection ────────────────────────────────────────────────────
108
+ const PLATFORM = process.platform; // 'darwin' | 'linux' | 'win32'
109
+
110
+ // ── Commands ──────────────────────────────────────────────────────────────
111
+
112
+ // ─── start ────────────────────────────────────────────────────────────────
113
+ function cmdStart() {
114
+ banner();
115
+ const port = getFlag('--port', process.env.PORT || '9900');
116
+ const dataDir = getFlag('--data', process.env.DATA_DIR || path.join(BLORQ_DIR, 'data'));
117
+ const bg = hasFlag('--background') || hasFlag('-b');
118
+
119
+ // Ensure data dir + setup
120
+ ensureDir(dataDir);
121
+ const usersFile = path.join(dataDir, 'users.json');
122
+ if (!fs.existsSync(usersFile)) {
123
+ info('No data found — running first-time setup…');
124
+ spawnSync(process.execPath, [path.join(BLORQ_DIR, 'setup.js')], { stdio: 'inherit', cwd: BLORQ_DIR });
125
+ console.log('');
126
+ }
127
+
128
+ if (bg) {
129
+ // Background mode — detach process
130
+ const pid = readPid();
131
+ if (isRunning(pid)) {
132
+ fail('LogBoard is already running (PID ' + pid + ')');
133
+ process.exit(1);
134
+ }
135
+ ensureDir(path.dirname(LOG_FILE));
136
+ const out = fs.openSync(LOG_FILE, 'a');
137
+ const child = spawn(process.execPath, [path.join(BLORQ_DIR, 'server.js')], {
138
+ detached: true,
139
+ stdio: ['ignore', out, out],
140
+ cwd: BLORQ_DIR,
141
+ env: { ...process.env, PORT: port, DATA_DIR: dataDir },
142
+ });
143
+ child.unref();
144
+ writePid(child.pid);
145
+ ok('LogBoard started in background (PID ' + child.pid + ')');
146
+ ok('URL: ' + c('cyan', 'http://localhost:' + port));
147
+ info('Logs: ' + LOG_FILE);
148
+ info('Stop: logboard stop');
149
+ console.log('');
150
+ } else {
151
+ // Foreground
152
+ info('Starting LogBoard on ' + c('cyan', 'http://localhost:' + port));
153
+ info('Press Ctrl+C to stop\n');
154
+ const child = spawn(process.execPath, [path.join(BLORQ_DIR, 'server.js')], {
155
+ stdio: 'inherit',
156
+ cwd: BLORQ_DIR,
157
+ env: { ...process.env, PORT: port, DATA_DIR: dataDir },
158
+ });
159
+ child.on('exit', code => process.exit(code || 0));
160
+ }
161
+ }
162
+
163
+ // ─── stop ─────────────────────────────────────────────────────────────────
164
+ function cmdStop() {
165
+ const pid = readPid();
166
+ if (!isRunning(pid)) {
167
+ warn('LogBoard is not running');
168
+ clearPid();
169
+ process.exit(0);
170
+ }
171
+ try {
172
+ process.kill(pid, 'SIGTERM');
173
+ ok('Stopped LogBoard (PID ' + pid + ')');
174
+ clearPid();
175
+ } catch (e) {
176
+ fail('Could not stop LogBoard: ' + e.message);
177
+ process.exit(1);
178
+ }
179
+ }
180
+
181
+ // ─── restart ──────────────────────────────────────────────────────────────
182
+ function cmdRestart() {
183
+ const pid = readPid();
184
+ if (isRunning(pid)) {
185
+ info('Stopping current instance…');
186
+ try { process.kill(pid, 'SIGTERM'); clearPid(); } catch {}
187
+ // Wait briefly for clean exit
188
+ const deadline = Date.now() + 3000;
189
+ while (isRunning(pid) && Date.now() < deadline) { /* spin */ }
190
+ }
191
+ // Start fresh
192
+ info('Restarting…\n');
193
+ args.push('--background');
194
+ cmdStart();
195
+ }
196
+
197
+ // ─── status ───────────────────────────────────────────────────────────────
198
+ function cmdStatus() {
199
+ const pid = readPid();
200
+ const port = process.env.PORT || '9900';
201
+ console.log('');
202
+ console.log(c('bold', ' LogBoard') + ' ' + c('dim', 'v' + VERSION));
203
+ console.log('');
204
+ if (isRunning(pid)) {
205
+ console.log(' Status ' + c('green', '● running') + ' ' + c('dim', 'PID ' + pid));
206
+ console.log(' URL ' + c('cyan', 'http://localhost:' + port));
207
+ console.log(' Logs ' + c('dim', LOG_FILE));
208
+ } else {
209
+ console.log(' Status ' + c('red', '● stopped'));
210
+ console.log(' Run: ' + c('cyan', 'logboard start'));
211
+ }
212
+ console.log(' Data ' + c('dim', process.env.DATA_DIR || path.join(BLORQ_DIR, 'data')));
213
+ console.log(' Install ' + c('dim', BLORQ_DIR));
214
+ console.log(' Platform ' + c('dim', PLATFORM + ' ' + os.arch()));
215
+ console.log('');
216
+ }
217
+
218
+ // ─── open ─────────────────────────────────────────────────────────────────
219
+ function cmdOpen() {
220
+ const port = process.env.PORT || '9900';
221
+ const url = 'http://localhost:' + port;
222
+ const pid = readPid();
223
+ if (!isRunning(pid)) {
224
+ warn('LogBoard may not be running. Open anyway: ' + c('cyan', url));
225
+ }
226
+ info('Opening ' + url);
227
+ const open = PLATFORM === 'win32' ? 'start' : PLATFORM === 'darwin' ? 'open' : 'xdg-open';
228
+ try { execSync(open + ' ' + url, { stdio: 'ignore' }); }
229
+ catch { info('Open manually: ' + c('cyan', url)); }
230
+ }
231
+
232
+ // ─── service ──────────────────────────────────────────────────────────────
233
+ function cmdService() {
234
+ const action = subCmd || 'help';
235
+ switch (action) {
236
+ case 'install': return serviceInstall();
237
+ case 'uninstall': return serviceUninstall();
238
+ case 'start': return serviceStart();
239
+ case 'stop': return serviceStop();
240
+ case 'restart': return serviceRestart();
241
+ case 'logs': return serviceLogs();
242
+ default:
243
+ console.log('\nUsage: logboard service <install|uninstall|start|stop|restart|logs>\n');
244
+ }
245
+ }
246
+
247
+ // ── macOS launchd ─────────────────────────────────────────────────────────
248
+ function plistPath() {
249
+ return path.join(os.homedir(), 'Library', 'LaunchAgents', SERVICE_ID + '.plist');
250
+ }
251
+
252
+ function writePlist(port, dataDir) {
253
+ const nodePath = process.execPath;
254
+ const serverPath = path.join(BLORQ_DIR, 'server.js');
255
+ const logOut = path.join(os.homedir(), '.logboard', 'stdout.log');
256
+ const logErr = path.join(os.homedir(), '.logboard', 'stderr.log');
257
+ ensureDir(path.join(os.homedir(), '.logboard'));
258
+
259
+ return `<?xml version="1.0" encoding="UTF-8"?>
260
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
261
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
262
+ <plist version="1.0">
263
+ <dict>
264
+ <key>Label</key> <string>${SERVICE_ID}</string>
265
+ <key>ProgramArguments</key>
266
+ <array>
267
+ <string>${nodePath}</string>
268
+ <string>${serverPath}</string>
269
+ </array>
270
+ <key>EnvironmentVariables</key>
271
+ <dict>
272
+ <key>PORT</key> <string>${port}</string>
273
+ <key>DATA_DIR</key> <string>${dataDir}</string>
274
+ <key>NODE_ENV</key> <string>production</string>
275
+ </dict>
276
+ <key>WorkingDirectory</key> <string>${BLORQ_DIR}</string>
277
+ <key>RunAtLoad</key> <true/>
278
+ <key>KeepAlive</key> <true/>
279
+ <key>StandardOutPath</key> <string>${logOut}</string>
280
+ <key>StandardErrorPath</key> <string>${logErr}</string>
281
+ <key>ProcessType</key> <string>Background</string>
282
+ </dict>
283
+ </plist>`;
284
+ }
285
+
286
+ // ── Linux systemd ─────────────────────────────────────────────────────────
287
+ function systemdPath() { return path.join(os.homedir(), '.config', 'systemd', 'user', 'logboard.service'); }
288
+ function systemdPathSys() { return '/etc/systemd/system/logboard.service'; } // root install
289
+
290
+ function writeSystemd(port, dataDir) {
291
+ const nodePath = process.execPath;
292
+ const serverPath = path.join(BLORQ_DIR, 'server.js');
293
+ const logDir = path.join(os.homedir(), '.logboard');
294
+ ensureDir(logDir);
295
+
296
+ return `[Unit]
297
+ Description=LogBoard Log Aggregator v${VERSION}
298
+ After=network.target
299
+
300
+ [Service]
301
+ Type=simple
302
+ User=${os.userInfo().username}
303
+ WorkingDirectory=${BLORQ_DIR}
304
+ ExecStart=${nodePath} ${serverPath}
305
+ Restart=on-failure
306
+ RestartSec=5s
307
+ Environment=PORT=${port}
308
+ Environment=DATA_DIR=${dataDir}
309
+ Environment=NODE_ENV=production
310
+ StandardOutput=journal
311
+ StandardError=journal
312
+ SyslogIdentifier=logboard
313
+
314
+ [Install]
315
+ WantedBy=default.target
316
+ `;
317
+ }
318
+
319
+ // ── Windows NSSM / sc ────────────────────────────────────────────────────
320
+ function writeWindowsScript(port, dataDir) {
321
+ const nodePath = process.execPath;
322
+ const serverPath = path.join(BLORQ_DIR, 'server.js');
323
+ // Returns a batch script that installs via sc.exe
324
+ return `@echo off
325
+ echo Installing LogBoard as Windows service...
326
+ sc create LogBoard binPath= "${nodePath} ${serverPath}" start= auto DisplayName= "LogBoard Log Aggregator"
327
+ sc description LogBoard "LogBoard production-grade log aggregator"
328
+ sc start LogBoard
329
+ echo Done. LogBoard will start automatically on login.
330
+ `;
331
+ }
332
+
333
+ function serviceInstall() {
334
+ const port = getFlag('--port', process.env.PORT || '9900');
335
+ const dataDir = getFlag('--data', path.join(BLORQ_DIR, 'data'));
336
+ ensureDir(dataDir);
337
+
338
+ title('Installing LogBoard as a system service');
339
+
340
+ if (PLATFORM === 'darwin') {
341
+ const plist = plistPath();
342
+ ensureDir(path.dirname(plist));
343
+ fs.writeFileSync(plist, writePlist(port, dataDir), 'utf8');
344
+ try {
345
+ // Unload first (in case it already exists)
346
+ try { execSync('launchctl unload "' + plist + '" 2>/dev/null', { stdio: 'ignore' }); } catch {}
347
+ execSync('launchctl load "' + plist + '"');
348
+ ok('Service registered with launchd');
349
+ ok('LogBoard will start automatically on login');
350
+ ok('URL: ' + c('cyan', 'http://localhost:' + port));
351
+ console.log('');
352
+ info('Control: logboard service start|stop|restart|logs');
353
+ console.log('');
354
+ } catch (e) {
355
+ warn('launchctl failed: ' + e.message);
356
+ info('Plist written to: ' + plist);
357
+ info('Load manually: launchctl load "' + plist + '"');
358
+ }
359
+
360
+ } else if (PLATFORM === 'linux') {
361
+ const svcPath = systemdPath();
362
+ ensureDir(path.dirname(svcPath));
363
+ fs.writeFileSync(svcPath, writeSystemd(port, dataDir), 'utf8');
364
+ try {
365
+ execSync('systemctl --user daemon-reload');
366
+ execSync('systemctl --user enable logboard.service');
367
+ execSync('systemctl --user start logboard.service');
368
+ ok('Service enabled and started via systemd (user)');
369
+ ok('LogBoard starts automatically on login');
370
+ ok('URL: ' + c('cyan', 'http://localhost:' + port));
371
+ console.log('');
372
+ info('Logs: journalctl --user -u logboard -f');
373
+ info('Control: logboard service start|stop|restart|logs');
374
+ console.log('');
375
+ } catch (e) {
376
+ warn('systemctl failed: ' + e.message);
377
+ info('Unit file: ' + svcPath);
378
+ info('Try: systemctl --user daemon-reload && systemctl --user enable --now logboard');
379
+ }
380
+
381
+ } else if (PLATFORM === 'win32') {
382
+ const batchPath = path.join(BLORQ_DIR, 'install-service.bat');
383
+ fs.writeFileSync(batchPath, writeWindowsScript(port, dataDir), 'utf8');
384
+ warn('Run the generated script as Administrator:');
385
+ info(batchPath);
386
+ info('');
387
+ info('Or use NSSM (recommended): https://nssm.cc');
388
+ info(' nssm install LogBoard "' + process.execPath + '" "' + path.join(BLORQ_DIR, 'server.js') + '"');
389
+ info(' nssm start LogBoard');
390
+ } else {
391
+ warn('Unsupported platform: ' + PLATFORM);
392
+ info('Start manually: logboard start --background');
393
+ }
394
+ }
395
+
396
+ function serviceUninstall() {
397
+ title('Uninstalling LogBoard service');
398
+ if (PLATFORM === 'darwin') {
399
+ const plist = plistPath();
400
+ try { execSync('launchctl unload "' + plist + '" 2>/dev/null', { stdio: 'ignore' }); } catch {}
401
+ try { fs.unlinkSync(plist); } catch {}
402
+ ok('Service removed');
403
+ } else if (PLATFORM === 'linux') {
404
+ try { execSync('systemctl --user stop logboard 2>/dev/null', { stdio: 'ignore' }); } catch {}
405
+ try { execSync('systemctl --user disable logboard 2>/dev/null', { stdio: 'ignore' }); } catch {}
406
+ try { fs.unlinkSync(systemdPath()); } catch {}
407
+ try { execSync('systemctl --user daemon-reload'); } catch {}
408
+ ok('Service removed');
409
+ } else if (PLATFORM === 'win32') {
410
+ try { execSync('sc stop LogBoard', { stdio: 'ignore' }); } catch {}
411
+ try { execSync('sc delete LogBoard', { stdio: 'ignore' }); } catch {}
412
+ ok('Windows service removed');
413
+ }
414
+ }
415
+
416
+ function serviceStart() {
417
+ if (PLATFORM === 'darwin') {
418
+ execSync('launchctl load "' + plistPath() + '"');
419
+ } else if (PLATFORM === 'linux') {
420
+ execSync('systemctl --user start logboard');
421
+ } else if (PLATFORM === 'win32') {
422
+ execSync('sc start LogBoard');
423
+ }
424
+ ok('LogBoard service started');
425
+ }
426
+
427
+ function serviceStop() {
428
+ if (PLATFORM === 'darwin') {
429
+ try { execSync('launchctl unload "' + plistPath() + '"'); } catch {}
430
+ } else if (PLATFORM === 'linux') {
431
+ execSync('systemctl --user stop logboard');
432
+ } else if (PLATFORM === 'win32') {
433
+ execSync('sc stop LogBoard');
434
+ }
435
+ ok('LogBoard service stopped');
436
+ }
437
+
438
+ function serviceRestart() {
439
+ if (PLATFORM === 'darwin') {
440
+ try { execSync('launchctl unload "' + plistPath() + '" 2>/dev/null', { stdio:'ignore' }); } catch {}
441
+ execSync('launchctl load "' + plistPath() + '"');
442
+ } else if (PLATFORM === 'linux') {
443
+ execSync('systemctl --user restart logboard');
444
+ } else if (PLATFORM === 'win32') {
445
+ execSync('sc stop LogBoard && sc start LogBoard');
446
+ }
447
+ ok('LogBoard service restarted');
448
+ }
449
+
450
+ function serviceLogs() {
451
+ if (PLATFORM === 'linux') {
452
+ // Use journalctl
453
+ const child = spawn('journalctl', ['--user', '-u', 'logboard', '-f', '--no-pager', '-n', '50'], { stdio: 'inherit' });
454
+ child.on('error', () => tailFile());
455
+ } else {
456
+ tailFile();
457
+ }
458
+ }
459
+
460
+ function tailFile() {
461
+ const f = path.join(os.homedir(), '.logboard', 'stdout.log');
462
+ if (!fs.existsSync(f)) {
463
+ warn('No log file yet: ' + f);
464
+ return;
465
+ }
466
+ info('Tailing ' + f + ' (Ctrl+C to stop)\n');
467
+ // Simple tail -f implementation
468
+ let pos = fs.statSync(f).size;
469
+ setInterval(() => {
470
+ const stat = fs.statSync(f);
471
+ if (stat.size > pos) {
472
+ const fd = fs.openSync(f, 'r');
473
+ const buf = Buffer.alloc(stat.size - pos);
474
+ fs.readSync(fd, buf, 0, buf.length, pos);
475
+ fs.closeSync(fd);
476
+ process.stdout.write(buf);
477
+ pos = stat.size;
478
+ }
479
+ }, 250);
480
+ }
481
+
482
+ // ─── setup ────────────────────────────────────────────────────────────────
483
+ function cmdSetup() {
484
+ banner();
485
+ spawnSync(process.execPath, [path.join(BLORQ_DIR, 'setup.js')], { stdio: 'inherit', cwd: BLORQ_DIR });
486
+ }
487
+
488
+ // ─── help ─────────────────────────────────────────────────────────────────
489
+ function cmdHelp() {
490
+ banner();
491
+ console.log(c('bold',' Usage') + ' logboard <command> [options]\n');
492
+ console.log(c('bold',' Commands:'));
493
+ const cmds = [
494
+ ['start', 'Start LogBoard in foreground'],
495
+ ['start --background','Start LogBoard in background'],
496
+ ['start --port 9900', 'Start on custom port'],
497
+ ['stop', 'Stop background instance'],
498
+ ['restart', 'Restart background instance'],
499
+ ['status', 'Show running status'],
500
+ ['setup', 'First-run: create data/, .env, users'],
501
+ ['open', 'Open dashboard in browser'],
502
+ ['service install', 'Register as OS service (starts on boot)'],
503
+ ['service uninstall', 'Remove OS service'],
504
+ ['service start', 'Start the OS service'],
505
+ ['service stop', 'Stop the OS service'],
506
+ ['service logs', 'Tail service logs'],
507
+ ['--version', 'Print version'],
508
+ ['--help', 'Print this help'],
509
+ ];
510
+ const pad = 26;
511
+ cmds.forEach(([cmd, desc]) => {
512
+ console.log(' ' + c('cyan', cmd.padEnd(pad)) + c('dim', desc));
513
+ });
514
+ console.log('');
515
+ console.log(c('bold',' Environment variables:'));
516
+ console.log(' ' + c('cyan','PORT'.padEnd(pad)) + c('dim','Server port (default: 9900)'));
517
+ console.log(' ' + c('cyan','DATA_DIR'.padEnd(pad))+ c('dim','Where to store data/ files'));
518
+ console.log(' ' + c('cyan','JWT_SECRET'.padEnd(pad))+c('dim','JWT signing secret (required in prod)'));
519
+ console.log('');
520
+ console.log(' Docs: ' + c('cyan','https://github.com/logboard/logboard') + '\n');
521
+ }
522
+
523
+ // ── Dispatch ──────────────────────────────────────────────────────────────
524
+ switch (command) {
525
+ case 'start': cmdStart(); break;
526
+ case 'stop': cmdStop(); break;
527
+ case 'restart': cmdRestart(); break;
528
+ case 'status': cmdStatus(); break;
529
+ case 'setup': cmdSetup(); break;
530
+ case 'open': cmdOpen(); break;
531
+ case 'service': cmdService(); break;
532
+ case '--version': case '-v':
533
+ console.log('logboard v' + VERSION); break;
534
+ case '--help': case '-h': case 'help': default:
535
+ cmdHelp();
536
+ }