saymon-syswatch-linux 1.0.0 → 1.0.2

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/bin/gen-cert.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // bin/gen-cert.js
4
4
  // Generates a self-signed TLS cert for HTTPS mode.
5
5
  // Uses openssl — must be installed on the system.
6
- // Saves to: certs/key.pem + certs/cert.pem
6
+ // Saves to: <configDir>/certs/key.pem + cert.pem
7
7
  // ─────────────────────────────────────────────────────
8
8
  'use strict';
9
9
 
@@ -11,7 +11,8 @@ const { execSync } = require('child_process');
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
13
 
14
- const certsDir = path.join(__dirname, '..', 'certs');
14
+ const { CONFIG_DIR } = require('../src/config');
15
+ const certsDir = path.join(CONFIG_DIR, 'certs');
15
16
 
16
17
  function generateCert() {
17
18
  if (!fs.existsSync(certsDir)) fs.mkdirSync(certsDir, { recursive: true });
@@ -33,7 +34,7 @@ function generateCert() {
33
34
  -subj "/CN=syswatch/O=SysWatch/C=US"`,
34
35
  { stdio: 'pipe' }
35
36
  );
36
- console.log('✓ Certificates saved to certs/key.pem + certs/cert.pem');
37
+ console.log(`✓ Certificates saved to ${certsDir}/`);
37
38
  console.log(' Note: Browser will warn "Not secure" for self-signed certs — that is normal.');
38
39
  console.log(' For production, replace with Let\'s Encrypt certs.');
39
40
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saymon-syswatch-linux",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Linux system monitor & learning dashboard — HTTP/HTTPS, journal logs, services, processes",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -11,9 +11,28 @@
11
11
  "dev": "node src/server.js --dev",
12
12
  "setup-https": "node bin/gen-cert.js"
13
13
  },
14
- "keywords": ["linux", "monitor", "systemd", "journalctl", "sysadmin", "dashboard", "learning"],
14
+ "files": [
15
+ "bin/",
16
+ "src/",
17
+ "public/",
18
+ "syswatch.config.example.json",
19
+ "README.md"
20
+ ],
21
+ "keywords": [
22
+ "linux",
23
+ "monitor",
24
+ "systemd",
25
+ "journalctl",
26
+ "sysadmin",
27
+ "dashboard",
28
+ "learning"
29
+ ],
15
30
  "author": "Saymon",
16
31
  "license": "MIT",
32
+ "preferGlobal": true,
33
+ "os": [
34
+ "linux"
35
+ ],
17
36
  "dependencies": {
18
37
  "express": "^4.18.2",
19
38
  "cors": "^2.8.5",
@@ -25,4 +44,4 @@
25
44
  "engines": {
26
45
  "node": ">=18.0.0"
27
46
  }
28
- }
47
+ }
package/public/login.html CHANGED
@@ -31,9 +31,9 @@
31
31
  onkeydown="if(event.key==='Enter')login()">
32
32
  <button onclick="login()">Sign In →</button>
33
33
  <div class="err" id="err">Invalid password. Try again.</div>
34
- <div class="note" id="note">
35
- First time? Enter any password it becomes your login for this session.<br>
36
- To persist it, add the bcrypt hash to <code>syswatch.config.json</code>.
34
+ <div class="note">
35
+ Enter the password you set during <code>syswatch --setup</code>.<br>
36
+ To re-configure, run <code>sudo syswatch --setup</code> in the terminal.
37
37
  </div>
38
38
  </div>
39
39
  <script>
@@ -51,11 +51,8 @@ async function login() {
51
51
  document.getElementById('pw').focus();
52
52
  }
53
53
  }
54
- // check if auth is even enabled
55
54
  fetch('/auth/status').then(r=>r.json()).then(d=>{
56
55
  if (!d.authEnabled) window.location.href = '/';
57
- if (d.passwordHash === null)
58
- document.getElementById('note').style.display='block';
59
56
  });
60
57
  </script>
61
58
  </body>
package/src/config.js CHANGED
@@ -3,13 +3,26 @@
3
3
  // Reads config from (priority order):
4
4
  // 1. CLI arguments (--port 9000)
5
5
  // 2. ENV variables (SYSWATCH_PORT=9000)
6
- // 3. syswatch.config.json in cwd (if exists)
6
+ // 3. Config file at fixed path (see CONFIG_PATH below)
7
7
  // 4. Defaults below
8
8
  // ─────────────────────────────────────────────────────
9
9
  'use strict';
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
+ const os = require('os');
14
+
15
+ // ── fixed config path (same regardless of cwd) ────
16
+ // root/sudo → /etc/syswatch/ non-root → ~/.config/syswatch/
17
+ function getConfigDir() {
18
+ try {
19
+ if (process.getuid && process.getuid() === 0) return '/etc/syswatch';
20
+ } catch {}
21
+ return path.join(os.homedir(), '.config', 'syswatch');
22
+ }
23
+
24
+ const CONFIG_DIR = process.env.SYSWATCH_CONFIG_DIR || getConfigDir();
25
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'syswatch.config.json');
13
26
 
14
27
  // ── parse CLI args ─────────────────────────────────
15
28
  const argv = process.argv.slice(2);
@@ -21,10 +34,9 @@ const flag = (f) => argv.includes(f);
21
34
 
22
35
  // ── load optional config file ──────────────────────
23
36
  let fileConf = {};
24
- const cfgPath = path.join(process.cwd(), 'syswatch.config.json');
25
- if (fs.existsSync(cfgPath)) {
26
- try { fileConf = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); }
27
- catch (e) { console.warn('[syswatch] Warning: invalid syswatch.config.json'); }
37
+ if (fs.existsSync(CONFIG_PATH)) {
38
+ try { fileConf = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); }
39
+ catch (e) { console.warn('[syswatch] Warning: invalid syswatch.config.json at ' + CONFIG_PATH); }
28
40
  }
29
41
 
30
42
  // ── merge config ───────────────────────────────────
@@ -46,8 +58,8 @@ const config = {
46
58
  sessionHours: fileConf.sessionHours || 8,
47
59
 
48
60
  // TLS cert paths (used when httpsEnabled)
49
- certPath: fileConf.certPath || path.join(__dirname, '..', 'certs', 'cert.pem'),
50
- keyPath: fileConf.keyPath || path.join(__dirname, '..', 'certs', 'key.pem'),
61
+ certPath: fileConf.certPath || path.join(CONFIG_DIR, 'certs', 'cert.pem'),
62
+ keyPath: fileConf.keyPath || path.join(CONFIG_DIR, 'certs', 'key.pem'),
51
63
 
52
64
  // Features
53
65
  learnMode: fileConf.learnMode !== false, // default on
@@ -62,4 +74,7 @@ function _randomSecret() {
62
74
  catch { return 'syswatch-change-this-secret'; }
63
75
  }
64
76
 
77
+ config.CONFIG_PATH = CONFIG_PATH;
78
+ config.CONFIG_DIR = CONFIG_DIR;
79
+
65
80
  module.exports = config;
package/src/server.js CHANGED
@@ -98,12 +98,9 @@ function banner() {
98
98
  function afterBanner() {
99
99
  console.log(`
100
100
  📖 Learn mode: ${config.learnMode ? 'ON (toggle in UI)' : 'OFF'}
101
- 🔐 Auth: ${config.authEnabled ? 'ON (set password on first login)' : 'OFF (--no-auth)'}
101
+ 🔐 Auth: ${config.authEnabled ? 'ON' : 'OFF (--no-auth)'}
102
102
  🖥 Host: ${config.host}
103
103
  `);
104
- if (config.authEnabled && !config.passwordHash) {
105
- console.log(' ℹ No password set yet — you will create one on first login.\n');
106
- }
107
104
  }
108
105
 
109
106
  function displayHost() {
package/src/setup.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // ─────────────────────────────────────────────────────
2
2
  // src/setup.js
3
3
  // First-run interactive setup wizard.
4
- // Runs automatically if syswatch.config.json is missing.
5
- // Also callable via: npx syswatch --setup
4
+ // Runs automatically if config is missing.
5
+ // Also callable via: syswatch --setup
6
6
  // ─────────────────────────────────────────────────────
7
7
  'use strict';
8
8
 
@@ -10,35 +10,57 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const readline = require('readline');
12
12
  const crypto = require('crypto');
13
+ const os = require('os');
14
+
15
+ // ── config path (mirrors src/config.js logic) ─────
16
+ function getConfigDir() {
17
+ try {
18
+ if (process.getuid && process.getuid() === 0) return '/etc/syswatch';
19
+ } catch {}
20
+ return path.join(os.homedir(), '.config', 'syswatch');
21
+ }
13
22
 
14
- const CONFIG_PATH = path.join(process.cwd(), 'syswatch.config.json');
23
+ const CONFIG_DIR = process.env.SYSWATCH_CONFIG_DIR || getConfigDir();
24
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'syswatch.config.json');
15
25
 
16
26
  // ── helpers ───────────────────────────────────────
17
27
  function ask(rl, question) {
18
28
  return new Promise(resolve => rl.question(question, resolve));
19
29
  }
20
30
 
31
+ // Hide password input — uses raw stdin mode WITHOUT creating a readline interface
32
+ // so there is no conflict with the rl instance used for other questions.
21
33
  function askHidden(question) {
22
- // hide password input on linux/mac terminals
23
34
  return new Promise(resolve => {
24
35
  process.stdout.write(question);
25
- const stdin = process.openStdin();
26
- // try to hide input
27
- try { process.stdin.setRawMode(true); } catch {}
36
+
37
+ const isTTY = process.stdin.isTTY;
38
+ if (isTTY) {
39
+ try { process.stdin.setRawMode(true); } catch {}
40
+ }
41
+
28
42
  let input = '';
29
- process.stdin.resume();
30
- process.stdin.setEncoding('utf8');
43
+
31
44
  const onData = (ch) => {
32
- ch = ch + '';
33
- if (ch === '\n' || ch === '\r' || ch === '\u0004') {
34
- try { process.stdin.setRawMode(false); } catch {}
35
- process.stdin.pause();
45
+ const s = ch.toString();
46
+
47
+ if (s === '\n' || s === '\r' || s === '\u0004') {
48
+ // Enter / Ctrl-D → done
49
+ if (isTTY) {
50
+ try { process.stdin.setRawMode(false); } catch {}
51
+ }
36
52
  process.stdin.removeListener('data', onData);
53
+ process.stdin.pause();
37
54
  process.stdout.write('\n');
38
55
  resolve(input);
39
- } else if (ch === '\u0003') {
40
- process.exit();
41
- } else if (ch === '\u007f' || ch === '\b') {
56
+
57
+ } else if (s === '\u0003') {
58
+ // Ctrl-C exit
59
+ process.stdout.write('\n');
60
+ process.exit(0);
61
+
62
+ } else if (s === '\u007f' || s === '\b') {
63
+ // Backspace
42
64
  if (input.length > 0) {
43
65
  input = input.slice(0, -1);
44
66
  process.stdout.clearLine(0);
@@ -46,10 +68,13 @@ function askHidden(question) {
46
68
  process.stdout.write(question + '*'.repeat(input.length));
47
69
  }
48
70
  } else {
49
- input += ch;
71
+ input += s;
50
72
  process.stdout.write('*');
51
73
  }
52
74
  };
75
+
76
+ process.stdin.resume();
77
+ process.stdin.setEncoding('utf8');
53
78
  process.stdin.on('data', onData);
54
79
  });
55
80
  }
@@ -86,27 +111,20 @@ async function run(autoMode = false) {
86
111
  ]);
87
112
  console.log();
88
113
 
89
- const rl = readline.createInterface({
90
- input: process.stdin,
91
- output: process.stdout,
92
- });
93
-
94
114
  const config = {};
95
115
 
96
- // ── Step 1: Password ────────────────────────────
116
+ // ── Step 1: Password (raw stdin — no readline yet) ─
97
117
  print('Step 1/4 — Set your login password', 'bold');
98
118
  print(' (This protects your dashboard from others on the network)\n', 'dim');
99
119
 
100
120
  let password = '';
101
- let confirmed = '';
102
-
103
121
  while (true) {
104
- password = await askHidden(' Enter password: ');
122
+ password = await askHidden(' Enter password: ');
105
123
  if (password.length < 4) {
106
124
  print(' ✗ Password must be at least 4 characters. Try again.\n', 'red');
107
125
  continue;
108
126
  }
109
- confirmed = await askHidden(' Confirm password: ');
127
+ const confirmed = await askHidden(' Confirm password: ');
110
128
  if (password !== confirmed) {
111
129
  print(' ✗ Passwords do not match. Try again.\n', 'red');
112
130
  continue;
@@ -119,6 +137,12 @@ async function run(autoMode = false) {
119
137
  config.passwordHash = await bcrypt.hash(password, 10);
120
138
  print(' ✓ Password set!\n', 'green');
121
139
 
140
+ // ── readline for remaining non-secret questions ──
141
+ const rl = readline.createInterface({
142
+ input: process.stdin,
143
+ output: process.stdout,
144
+ });
145
+
122
146
  // ── Step 2: Port ────────────────────────────────
123
147
  print('Step 2/4 — HTTP Port', 'bold');
124
148
  print(' Default is 8080. Press Enter to keep it.\n', 'dim');
@@ -147,15 +171,18 @@ async function run(autoMode = false) {
147
171
  config.httpsPort = 8443;
148
172
  print(` ✓ HTTPS: ${config.https ? 'Enabled on port 8443' : 'Disabled'}\n`, 'green');
149
173
 
150
- // ── Generate JWT secret ─────────────────────────
151
- config.jwtSecret = crypto.randomBytes(32).toString('hex');
174
+ config.jwtSecret = crypto.randomBytes(32).toString('hex');
152
175
  config.sessionHours = 8;
153
- config.learnMode = true;
154
- config.maxLogLines = 200;
176
+ config.learnMode = true;
177
+ config.maxLogLines = 200;
155
178
 
156
179
  rl.close();
157
180
 
158
181
  // ── Write config file ───────────────────────────
182
+ if (!fs.existsSync(CONFIG_DIR)) {
183
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
184
+ }
185
+
159
186
  const finalConfig = {
160
187
  _note: "Auto-generated by syswatch --setup. Do NOT commit this file to git.",
161
188
  host: config.host,
@@ -177,23 +204,19 @@ async function run(autoMode = false) {
177
204
  box([
178
205
  '\x1b[1m\x1b[32m ✓ Setup complete!\x1b[0m',
179
206
  '',
180
- ` Config saved → syswatch.config.json`,
207
+ ` Config saved → ${CONFIG_PATH}`,
181
208
  '',
182
209
  ` 🌐 http://${config.host === '0.0.0.0' ? 'YOUR_IP' : 'localhost'}:${config.port}`,
183
210
  config.https ? ` 🔒 https://${config.host === '0.0.0.0' ? 'YOUR_IP' : 'localhost'}:${config.httpsPort}` : '',
184
211
  '',
185
- ' Start the server:',
186
- ' sudo node src/server.js',
187
- '',
188
- ' Or if installed globally:',
189
- ' sudo syswatch',
212
+ ' To start: sudo syswatch',
213
+ ' Re-setup: sudo syswatch --setup',
190
214
  ].filter(l => l !== ''));
191
215
  console.log();
192
216
 
193
217
  if (autoMode) {
194
- // auto-start after setup
195
218
  print(' Starting SysWatch now...\n', 'cyan');
196
- require('./server');
219
+ // server is already chained via .then(startServer) in server.js
197
220
  } else {
198
221
  process.exit(0);
199
222
  }
@@ -202,10 +225,10 @@ async function run(autoMode = false) {
202
225
  // ── check if config exists, if not → auto-run setup ─
203
226
  function checkAndSetup() {
204
227
  if (!fs.existsSync(CONFIG_PATH)) {
205
- print('\n ⚠ No syswatch.config.json found — running first-time setup...\n', 'yellow');
206
- return run(true); // autoMode = true means start server after setup
228
+ print(`\n ⚠ No config found at ${CONFIG_PATH} — running first-time setup...\n`, 'yellow');
229
+ return run(true);
207
230
  }
208
231
  return Promise.resolve();
209
232
  }
210
233
 
211
- module.exports = { run, checkAndSetup };
234
+ module.exports = { run, checkAndSetup, CONFIG_PATH, CONFIG_DIR };
@@ -14,6 +14,6 @@
14
14
  "learnMode": true,
15
15
  "maxLogLines": 200,
16
16
 
17
- "certPath": "./certs/cert.pem",
18
- "keyPath": "./certs/key.pem"
17
+ "certPath": "/etc/syswatch/certs/cert.pem",
18
+ "keyPath": "/etc/syswatch/certs/key.pem"
19
19
  }