srv-it 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -34,7 +34,7 @@ srv-it
34
34
  srv-it 3000
35
35
  srv-it 8080 ./public
36
36
  srv-it ./public --open
37
- srv-it --style neon --style-css ./srv-listing.css
37
+ srv-it --style paper --style-css ./srv-listing.css
38
38
  ```
39
39
 
40
40
  ## Config defaults
@@ -43,7 +43,7 @@ You can set defaults globally and per-project:
43
43
 
44
44
  - global: `~/.srvrc.json`
45
45
  - project: `./srv.config.json`
46
- - override file: `srv --config ./my-srv.json`
46
+ - override file: `srv-it --config ./my-srv.json`
47
47
  - template: `./srv.config.example.json`
48
48
 
49
49
  Example `srv.config.json`:
@@ -74,23 +74,29 @@ Run:
74
74
  srv-it --help
75
75
  ```
76
76
 
77
- Highlights:
78
-
79
- - `-p, --port <number>`
80
- - `--host <host>`
81
- - `--open [path]`, `--no-open`
82
- - `--watch <path>` (repeat)
83
- - `--ignore <glob>` (repeat)
84
- - `--single`
85
- - `--cors`
86
- - `--no-css-inject`
87
- - `--no-dir-listing`
88
- - `--style <midnight|paper|neon>`
89
- - `--style-css <file>`
90
- - `-c` (create `srv.config.json` in served root if missing)
91
- - `--log-level <0-3>`
92
- - `--no-request-logging`
93
- - `--ssl-cert <file> --ssl-key <file> [--ssl-pass <file>]`
77
+ ### Supported Arguments
78
+
79
+ - `-h, --help`: show help output
80
+ - `-v, --version`: print the current version
81
+ - `-p, --port <number>`: set the server port
82
+ - `--host <host>`: set the bind host (default: `0.0.0.0`)
83
+ - `--open [path]`: open a browser to `/` or the provided path
84
+ - `--no-open`: disable automatic browser opening
85
+ - `--watch <path>` (repeat): add extra files/folders to watch for live reload
86
+ - `--ignore <glob>` (repeat): ignore matching watcher paths/globs
87
+ - `--no-css-inject`: use full page reload for CSS changes instead of hot CSS refresh
88
+ - `--cors`: enable CORS headers
89
+ - `--single`: serve `index.html` for unknown routes (SPA fallback)
90
+ - `--no-dir-listing`: disable generated directory listing pages
91
+ - `--style <midnight|paper>`: choose the directory listing style preset
92
+ - `--style-css <file>`: load custom CSS for directory listing pages
93
+ - `-c`: create `srv.config.json` in the served root if missing
94
+ - `--config <file>`: read additional config JSON file
95
+ - `--no-request-logging`: disable request logs
96
+ - `--log-level <0-3>`: set startup log verbosity
97
+ - `--ssl-cert <file>`: path to SSL certificate
98
+ - `--ssl-key <file>`: path to SSL private key
99
+ - `--ssl-pass <file>`: path to SSL passphrase file
94
100
 
95
101
  ## Notes
96
102
 
package/license.md ADDED
@@ -0,0 +1,20 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Elouan Grimm.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "srv-it",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Static server with polished CLI UI, directory listing, and live reload",
5
5
  "keywords": [
6
6
  "server",
package/src/cli.js CHANGED
@@ -46,7 +46,7 @@ function getHelpText() {
46
46
  ` ${chalk.yellow('--cors')} Enable CORS`,
47
47
  ` ${chalk.yellow('--single')} SPA fallback to /index.html`,
48
48
  ` ${chalk.yellow('--no-dir-listing')} Disable directory listing`,
49
- ` ${chalk.yellow('--style <name>')} Listing style preset: midnight | paper | neon`,
49
+ ` ${chalk.yellow('--style <midnight|paper>')} Listing style preset`,
50
50
  ` ${chalk.yellow('--style-css <file>')} Custom CSS file for listing page`,
51
51
  ` ${chalk.yellow('-c')} Create srv.config.json in served root if missing`,
52
52
  ` ${chalk.yellow('--config <file>')} Read additional config JSON file`,
@@ -207,7 +207,7 @@ async function run() {
207
207
 
208
208
  if (options.logLevel >= 1) {
209
209
  const lines = [
210
- `${chalk.green.bold('srv is running')}`,
210
+ `${chalk.hex("#3b82f6").bold('srv-it is running')}`,
211
211
  '',
212
212
  `${chalk.bold('- Local:')} ${url}`,
213
213
  `${chalk.bold('- Root:')} ${options.root}`,
@@ -230,7 +230,8 @@ async function run() {
230
230
  boxen(content, {
231
231
  padding: 1,
232
232
  borderStyle: 'single',
233
- borderColor: 'cyan',
233
+ borderColor: '#3b82f6',
234
+ margin: 1,
234
235
  }),
235
236
  );
236
237
  }
package/src/server.js CHANGED
@@ -11,8 +11,19 @@ const os = require('node:os');
11
11
  const chalk = require('chalk');
12
12
  const { WebSocketServer } = require('ws');
13
13
 
14
+ const srvLogger = {
15
+ http: (...message) => console.info(chalk.bgBlue.bold(' HTTP '), ...message),
16
+ info: (...message) => console.info(chalk.bgMagenta.bold(' INFO '), ...message),
17
+ warn: (...message) => console.error(chalk.bgYellow.bold(' WARN '), ...message),
18
+ error: (...message) => console.error(chalk.bgRed.bold(' ERRR '), ...message),
19
+ log: console.log,
20
+ };
21
+
14
22
  const BASE_WATCH_IGNORES = ['**/.git/**', '**/node_modules/**'];
15
23
 
24
+ const DIRECTORY_FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#0c0a09" d="M3 5.5A2.5 2.5 0 0 1 5.5 3H10l2 2h6.5A2.5 2.5 0 0 1 21 7.5v9A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5z"/><path fill="#3b82f6" d="M3 8h18v8.5a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 16.5z"/></svg>';
25
+ const DIRECTORY_FAVICON_DATA_URL = `data:image/svg+xml,${encodeURIComponent(DIRECTORY_FAVICON_SVG)}`;
26
+
16
27
  function toGlobPath(value) {
17
28
  return value.replace(/\\/g, '/');
18
29
  }
@@ -31,15 +42,29 @@ const LIVE_RELOAD_SNIPPET = `\n<script>\n(function(){\n if(!('WebSocket' in win
31
42
  const STYLE_PRESETS = {
32
43
  midnight: {
33
44
  accent: '#3b82f6',
45
+ accentSoft: 'rgba(59, 130, 246, 0.16)',
46
+ bg: '#0c0a09',
47
+ bgRaised: '#1c1917',
48
+ bgSurface: '#292524',
49
+ border: '#292524',
34
50
  borderStrong: '#78716c',
51
+ text: '#e7e5e4',
52
+ textMuted: '#a8a29e',
53
+ textFaint: '#78716c',
54
+ textBright: '#f5f5f4',
35
55
  },
36
56
  paper: {
37
- accent: '#f59e0b',
38
- borderStrong: '#a8a29e',
39
- },
40
- neon: {
41
- accent: '#22c55e',
57
+ accent: '#d97706',
58
+ accentSoft: 'rgba(217, 119, 6, 0.16)',
59
+ bg: '#f5f5f4',
60
+ bgRaised: '#fafaf9',
61
+ bgSurface: '#e7e5e4',
62
+ border: '#d6d3d1',
42
63
  borderStrong: '#a8a29e',
64
+ text: '#292524',
65
+ textMuted: '#57534e',
66
+ textFaint: '#78716c',
67
+ textBright: '#1c1917',
43
68
  },
44
69
  };
45
70
 
@@ -106,6 +131,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
106
131
  <meta charset="utf-8" />
107
132
  <meta name="viewport" content="width=device-width,initial-scale=1" />
108
133
  <title>Index of ${pathnameValue}</title>
134
+ <link rel="icon" href="${DIRECTORY_FAVICON_DATA_URL}" />
109
135
  <style>
110
136
  :root {
111
137
  --stone-050: #fafaf9;
@@ -120,16 +146,17 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
120
146
  --stone-900: #1c1917;
121
147
  --stone-950: #0c0a09;
122
148
 
123
- --bg: var(--stone-950);
124
- --bg-raised: var(--stone-900);
125
- --bg-surface: var(--stone-800);
126
- --border: var(--stone-800);
149
+ --bg: ${palette.bg};
150
+ --bg-raised: ${palette.bgRaised};
151
+ --bg-surface: ${palette.bgSurface};
152
+ --border: ${palette.border};
127
153
  --border-strong: ${palette.borderStrong};
128
- --text: var(--stone-200);
129
- --text-muted: var(--stone-400);
130
- --text-faint: var(--stone-500);
131
- --text-bright: var(--stone-100);
154
+ --text: ${palette.text};
155
+ --text-muted: ${palette.textMuted};
156
+ --text-faint: ${palette.textFaint};
157
+ --text-bright: ${palette.textBright};
132
158
  --accent: ${palette.accent};
159
+ --accent-soft: ${palette.accentSoft};
133
160
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
134
161
  --font-mono: "JetBrains Mono", "SF Mono", "Fira Code", "Roboto Mono", "Cascadia Code", monospace;
135
162
  --line-height: 1.6;
@@ -163,7 +190,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
163
190
 
164
191
  h1 {
165
192
  margin-bottom: 0.25rem;
166
- color: var(--text-bright);
193
+ color: var(--accent);
167
194
  font-family: var(--font-mono);
168
195
  font-size: 1.1rem;
169
196
  line-height: var(--line-height-tight);
@@ -176,6 +203,8 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
176
203
  color: var(--text-muted);
177
204
  font-family: var(--font-mono);
178
205
  font-size: 0.9rem;
206
+ border-left: 2px solid var(--accent);
207
+ padding-left: 0.6rem;
179
208
  }
180
209
 
181
210
  .up {
@@ -183,10 +212,10 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
183
212
  margin-bottom: 0.75rem;
184
213
  color: var(--accent);
185
214
  text-decoration: none;
186
- border: 1px solid var(--border-strong);
215
+ border: 1px solid var(--accent);
187
216
  padding: 0.3rem 0.55rem;
188
217
  font-family: var(--font-mono);
189
- transition: background-color 0.18s ease, color 0.18s ease;
218
+ transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
190
219
  }
191
220
 
192
221
  .up:hover {
@@ -209,7 +238,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
209
238
  color: var(--text);
210
239
  border-bottom: 1px solid var(--border);
211
240
  font-family: var(--font-mono);
212
- transition: background-color 0.18s ease, color 0.18s ease;
241
+ transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
213
242
  }
214
243
 
215
244
  li:last-child a {
@@ -219,6 +248,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
219
248
  li a:hover {
220
249
  background-color: var(--bg-surface);
221
250
  color: var(--text-bright);
251
+ box-shadow: inset 3px 0 0 var(--accent);
222
252
  }
223
253
 
224
254
  .name {
@@ -229,10 +259,13 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
229
259
  }
230
260
 
231
261
  .type {
232
- color: var(--text-faint);
262
+ color: var(--accent);
233
263
  text-transform: uppercase;
234
264
  font-size: 0.72rem;
235
265
  letter-spacing: 0.07em;
266
+ padding: 0.12rem 0.35rem;
267
+ border: 1px solid var(--accent-soft);
268
+ background-color: var(--accent-soft);
236
269
  }
237
270
 
238
271
  @media (max-width: 768px) {
@@ -278,6 +311,7 @@ async function createSrvServer(options) {
278
311
  const root = path.resolve(options.root);
279
312
  const compress = getCompressionMiddleware();
280
313
  const clients = new Set();
314
+ const sockets = new Set();
281
315
 
282
316
  let customCss = '';
283
317
  if (options.styleCss) {
@@ -315,6 +349,12 @@ async function createSrvServer(options) {
315
349
  try {
316
350
  stat = await fsp.stat(filePath);
317
351
  } catch (error) {
352
+ if (pathnameValue === '/sw.js' && error && error.code === 'ENOENT') {
353
+ // Browsers often probe /sw.js by default; treat missing file as a silent no-op.
354
+ res.statusCode = 204;
355
+ res.end();
356
+ return;
357
+ }
318
358
  if (options.single) {
319
359
  filePath = path.join(root, 'index.html');
320
360
  stat = await fsp.stat(filePath);
@@ -385,18 +425,22 @@ async function createSrvServer(options) {
385
425
  res.statusCode = 500;
386
426
  res.end('Internal server error');
387
427
  if (options.logLevel >= 1) {
388
- console.error('[srv] request error:', error.message);
428
+ srvLogger.error('[srv-it] request error:', error.message);
389
429
  }
390
430
  } finally {
391
431
  if (!options.noRequestLogging) {
392
432
  const statusCode = res.statusCode;
433
+ const suppressRequestLog = pathnameValue === '/sw.js' && statusCode === 204;
434
+ if (suppressRequestLog) {
435
+ return;
436
+ }
393
437
  const elapsed = Date.now() - start;
394
438
  const sourceIp = (req.socket.remoteAddress || '-').replace('::ffff:', '');
395
439
  const now = new Date();
396
440
  const formattedTime = `${now.toLocaleDateString()} ${now.toLocaleTimeString()}`;
397
441
  const methodColor = method === 'GET' ? 'cyan' : 'magenta';
398
442
 
399
- console.log(
443
+ srvLogger.http(
400
444
  chalk.dim(formattedTime),
401
445
  chalk.yellow(sourceIp),
402
446
  chalk[methodColor](`${method} ${pathnameValue}`),
@@ -420,7 +464,7 @@ async function createSrvServer(options) {
420
464
  res.statusCode = 500;
421
465
  res.end('Internal server error');
422
466
  if (options.logLevel >= 1) {
423
- console.error('[srv] handler error:', error.message);
467
+ srvLogger.error('[srv-it] handler error:', error.message);
424
468
  }
425
469
  });
426
470
  });
@@ -430,12 +474,17 @@ async function createSrvServer(options) {
430
474
  res.statusCode = 500;
431
475
  res.end('Internal server error');
432
476
  if (options.logLevel >= 1) {
433
- console.error('[srv] handler error:', error.message);
477
+ srvLogger.error('[srv-it] handler error:', error.message);
434
478
  }
435
479
  });
436
480
  });
437
481
  }
438
482
 
483
+ server.on('connection', (socket) => {
484
+ sockets.add(socket);
485
+ socket.on('close', () => sockets.delete(socket));
486
+ });
487
+
439
488
  const wss = new WebSocketServer({ server, path: '/__srv_ws' });
440
489
  wss.on('connection', (socket) => {
441
490
  clients.add(socket);
@@ -474,7 +523,7 @@ async function createSrvServer(options) {
474
523
  }
475
524
  }
476
525
  if (options.logLevel >= 2) {
477
- console.log(`[srv] ${isCss ? 'css refresh' : 'reload'}: ${changePath}`);
526
+ srvLogger.info(`[srv-it] ${isCss ? 'css refresh' : 'reload'}: ${changePath}`);
478
527
  }
479
528
  };
480
529
 
@@ -487,27 +536,66 @@ async function createSrvServer(options) {
487
536
  if (error && error.code === 'ENOSPC') {
488
537
  liveReloadEnabled = false;
489
538
  await closeWatcher();
490
- console.error('[srv] live reload disabled: file watcher limit reached (ENOSPC).');
491
- console.error('[srv] use --ignore to exclude noisy paths, or raise inotify limits on Linux.');
539
+ srvLogger.error('[srv-it] live reload disabled: file watcher limit reached (ENOSPC).');
540
+ srvLogger.warn('[srv-it] use --ignore to exclude noisy paths, or raise inotify limits on Linux.');
492
541
  return;
493
542
  }
494
543
 
495
544
  if (options.logLevel >= 1) {
496
- console.error(`[srv] watcher error: ${error.message}`);
545
+ srvLogger.error(`[srv-it] watcher error: ${error.message}`);
497
546
  }
498
547
  });
499
548
 
549
+ let closingPromise;
500
550
  const closeAll = async () => {
501
- await closeWatcher();
502
- wss.close();
503
- await new Promise((resolve) => server.close(resolve));
551
+ if (closingPromise) {
552
+ return closingPromise;
553
+ }
554
+
555
+ closingPromise = (async () => {
556
+ await closeWatcher();
557
+
558
+ for (const client of clients) {
559
+ try {
560
+ client.terminate();
561
+ } catch (_error) {
562
+ // Ignore client termination errors during shutdown.
563
+ }
564
+ }
565
+
566
+ await new Promise((resolve) => wss.close(resolve));
567
+
568
+ for (const socket of sockets) {
569
+ socket.destroy();
570
+ }
571
+
572
+ await new Promise((resolve) => server.close(resolve));
573
+ })();
574
+
575
+ return closingPromise;
504
576
  };
505
577
 
506
- process.on('SIGINT', async () => {
507
- console.log('\n[srv] shutting down...');
508
- await closeAll();
509
- process.exit(0);
510
- });
578
+ let shuttingDown = false;
579
+ const onSigint = async () => {
580
+ if (shuttingDown) {
581
+ process.exit(130);
582
+ return;
583
+ }
584
+
585
+ shuttingDown = true;
586
+ process.stdout.write('\u001B[2K\r');
587
+ console.log(chalk.bgWhite.bold('\n[srv-it]') + ' shutting down...');
588
+
589
+ try {
590
+ await closeAll();
591
+ process.exit(0);
592
+ } catch (error) {
593
+ srvLogger.error('[srv-it] shutdown error:', error.message);
594
+ process.exit(1);
595
+ }
596
+ };
597
+
598
+ process.once('SIGINT', onSigint);
511
599
 
512
600
  await new Promise((resolve, reject) => {
513
601
  server.once('error', reject);