gib-runs 2.3.8 → 2.4.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/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.4.0] - 2026-02-15
6
+
7
+ ### Added
8
+ - 📜 **Request History Tracking** - New `/history` endpoint
9
+ - Tracks last 50 HTTP requests with full details
10
+ - Includes timestamp, method, URL, status, duration, and IP
11
+ - Perfect for debugging and monitoring
12
+ - 📊 **Detailed File Change Notifications** - Enhanced file watching
13
+ - Shows file size on changes: `File changed: app.js (12.45KB)`
14
+ - Separate notifications for file add/delete events
15
+ - ➕ File added (logLevel >= 2)
16
+ - ➖ File deleted (logLevel >= 2)
17
+ - 📝 **Comprehensive Logging** - Complete request and error logging
18
+ - All requests logged with timestamp and IP address
19
+ - Consistent ISO 8601 timestamps across all logs
20
+ - Works in static, npm-script, and exec modes
21
+
22
+ ### Changed
23
+ - 🔧 **Improved Error Handling** - Better error messages with timestamps
24
+ - Server errors include timestamp and stack trace
25
+ - Process errors in npm/exec mode properly logged
26
+ - Watcher errors show detailed information
27
+ - ⚡ **Enhanced Process Runner** - Better npm-script and exec mode
28
+ - Stderr properly captured and logged with timestamps
29
+ - Exit codes and signals logged with context
30
+ - Cleaner output formatting
31
+
5
32
  ## [2.3.8] - 2026-02-15
6
33
 
7
34
  ### Changed
package/README.md CHANGED
@@ -4,19 +4,19 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/gib-runs.svg)](https://www.npmjs.org/package/gib-runs)
5
5
  [![license](https://img.shields.io/npm/l/gib-runs.svg)](https://github.com/levouinse/gib-runs/blob/main/LICENSE)
6
6
 
7
- Modern development server with live reload and hot module replacement. Built for developers who value merit over connections.
7
+ A modern development server with live reload and hot module replacement. Built for developers who value capability over connections.
8
8
 
9
- > *"Unlike some people, this actually runs on capability, not nepotism."*
9
+ > *"Unlike some people, this actually runs on merit, not nepotism."*
10
10
 
11
11
  ## Why GIB-RUNS?
12
12
 
13
- The name playfully references Indonesia's Vice President Gibran Rakabuming Raka, who got his position thanks to his father, President Joko Widodo. But unlike certain political figures, this server:
13
+ Named after Indonesia's Vice President Gibran Rakabuming Raka, who got his position through family connections. But unlike certain political figures, this server:
14
14
 
15
- - ✅ Earned its position through actual features
16
- - ✅ Works hard without shortcuts
15
+ - ✅ Earned its position through real features
16
+ - ✅ Works without shortcuts or special treatment
17
17
  - ✅ Serves everyone equally
18
- - ✅ Transparent about what it does
19
- - ✅ Performance based on real metrics
18
+ - ✅ Transparent and honest about what it does
19
+ - ✅ Performance based on actual metrics
20
20
 
21
21
  *"When your dev server has more integrity than some vice presidents."*
22
22
 
@@ -26,6 +26,8 @@ The name playfully references Indonesia's Vice President Gibran Rakabuming Raka,
26
26
  - ⚡ **Hot CSS Injection** - Update styles without full page reload
27
27
  - 🎨 **Beautiful UI** - Modern status indicator with real-time feedback
28
28
  - 📊 **Performance Monitoring** - Track requests, reloads, and uptime
29
+ - 📝 **Comprehensive Logging** - All requests, errors, and file changes with timestamps and file sizes
30
+ - 📜 **Request History** - Track last 50 requests via `/history` endpoint
29
31
  - 🗜️ **Compression** - Built-in gzip compression
30
32
  - 🔒 **HTTPS/HTTP2** - Secure development with modern protocols
31
33
  - 🌐 **CORS Support** - Easy cross-origin development
@@ -34,7 +36,7 @@ The name playfully references Indonesia's Vice President Gibran Rakabuming Raka,
34
36
  - 🔌 **Proxy Support** - Proxy API requests during development
35
37
  - 📦 **Middleware** - Extend functionality with custom middleware
36
38
  - 🎭 **Mount Directories** - Serve multiple directories on different routes
37
- - 🚀 **NPM Scripts** - Run npm dev, start, or any script alongside server
39
+ - 🚀 **NPM Scripts** - Run npm scripts alongside the server
38
40
  - 🔄 **PM2 Integration** - Production-ready process management
39
41
  - 🌍 **Public Tunnels** - Share your dev server with anyone, anywhere
40
42
  - 📱 **Multi-Device** - Access from any device on your network
@@ -68,44 +70,16 @@ gib-runs --spa
68
70
  gib-runs --https=./config/https.conf.js
69
71
  ```
70
72
 
71
- ## CLI Options
73
+ ## Common Use Cases
72
74
 
73
- | Option | Description | Default |
74
- |--------|-------------|---------|
75
- | `--port=NUMBER` | Port to use | `8080` |
76
- | `--host=ADDRESS` | Address to bind to | `0.0.0.0` |
77
- | `--open=PATH` | Path to open in browser | `/` |
78
- | `--no-browser` | Suppress browser launch | `false` |
79
- | `-q, --quiet` | Suppress logging | `false` |
80
- | `-V, --verbose` | Verbose logging | `false` |
81
- | `--watch=PATH` | Paths to watch (comma-separated) | All files |
82
- | `--ignore=PATH` | Paths to ignore (comma-separated) | None |
83
- | `--no-css-inject` | Reload page on CSS change | `false` |
84
- | `--spa` | Single Page App mode | `false` |
85
- | `--cors` | Enable CORS | `false` |
86
- | `--https=PATH` | HTTPS config module | None |
87
- | `--proxy=ROUTE:URL` | Proxy requests | None |
88
- | `--middleware=PATH` | Custom middleware | None |
89
- | `--htpasswd=PATH` | HTTP auth file | None |
90
- | `--tunnel` | Create public tunnel | `false` |
91
- | `--tunnel-service=NAME` | Tunnel service (lt, cf, ngrok) | `localtunnel` |
92
- | `--npm-script=SCRIPT` | Run npm script (dev, start, etc) | None |
93
- | `--exec=COMMAND` | Run custom command | None |
94
- | `--pm2` | Use PM2 process manager | `false` |
95
- | `--auto-restart` | Auto-restart server on crash | `false` |
96
- | `--enable-upload` | Enable file upload endpoint | `false` |
97
- | `--log-to-file` | Log requests to file | `false` |
98
-
99
- ## Usage Examples
100
-
101
- ### Basic Server
75
+ ### Basic Development Server
102
76
 
103
77
  ```bash
104
- # Serve current directory on port 8080
78
+ # Serve current directory with live reload
105
79
  gib-runs
106
80
 
107
- # Serve with custom port
108
- gib-runs --port=3000
81
+ # Custom port and verbose logging
82
+ gib-runs --port=3000 --verbose
109
83
 
110
84
  # Serve specific directory
111
85
  gib-runs ./public
@@ -117,7 +91,7 @@ gib-runs ./public
117
91
  # SPA mode (redirects all routes to index.html)
118
92
  gib-runs --spa
119
93
 
120
- # With custom port
94
+ # SPA with custom port
121
95
  gib-runs --spa --port=8000
122
96
  ```
123
97
 
@@ -147,7 +121,7 @@ gib-runs --mount=/static:./assets --mount=/lib:./node_modules
147
121
  # Run npm dev script with live reload
148
122
  gib-runs --npm-script=dev
149
123
 
150
- # Run with PM2
124
+ # Run with PM2 process manager
151
125
  gib-runs --npm-script=dev --pm2
152
126
 
153
127
  # Run custom command
@@ -163,7 +137,7 @@ gib-runs --tunnel
163
137
  # Use Cloudflare Tunnel
164
138
  gib-runs --tunnel-service=cloudflared
165
139
 
166
- # Use Ngrok
140
+ # Use Ngrok (requires auth token)
167
141
  gib-runs --tunnel-service=ngrok --tunnel-authtoken=YOUR_TOKEN
168
142
  ```
169
143
 
@@ -187,15 +161,33 @@ Then run:
187
161
  gib-runs --https=./https.conf.js
188
162
  ```
189
163
 
190
- ### HTTP Authentication
191
-
192
- ```bash
193
- # Create htpasswd file
194
- htpasswd -c .htpasswd username
164
+ ## CLI Options
195
165
 
196
- # Use it
197
- gib-runs --htpasswd=.htpasswd
198
- ```
166
+ | Option | Description | Default |
167
+ |--------|-------------|---------|
168
+ | `--port=NUMBER` | Port to use | `8080` |
169
+ | `--host=ADDRESS` | Address to bind to | `0.0.0.0` |
170
+ | `--open=PATH` | Path to open in browser | `/` |
171
+ | `--no-browser` | Suppress browser launch | `false` |
172
+ | `-q, --quiet` | Suppress logging | `false` |
173
+ | `-V, --verbose` | Verbose logging | `false` |
174
+ | `--watch=PATH` | Paths to watch (comma-separated) | All files |
175
+ | `--ignore=PATH` | Paths to ignore (comma-separated) | None |
176
+ | `--no-css-inject` | Reload page on CSS change | `false` |
177
+ | `--spa` | Single Page App mode | `false` |
178
+ | `--cors` | Enable CORS | `false` |
179
+ | `--https=PATH` | HTTPS config module | None |
180
+ | `--proxy=ROUTE:URL` | Proxy requests | None |
181
+ | `--middleware=PATH` | Custom middleware | None |
182
+ | `--htpasswd=PATH` | HTTP auth file | None |
183
+ | `--tunnel` | Create public tunnel | `false` |
184
+ | `--tunnel-service=NAME` | Tunnel service (lt, cf, ngrok) | `localtunnel` |
185
+ | `--npm-script=SCRIPT` | Run npm script | None |
186
+ | `--exec=COMMAND` | Run custom command | None |
187
+ | `--pm2` | Use PM2 process manager | `false` |
188
+ | `--auto-restart` | Auto-restart server on crash | `false` |
189
+ | `--enable-upload` | Enable file upload endpoint | `false` |
190
+ | `--log-to-file` | Log requests to file | `false` |
199
191
 
200
192
  ## Node.js API
201
193
 
@@ -263,10 +255,37 @@ Or `.gib-runs.json` in your project root (overrides global config):
263
255
  }
264
256
  ```
265
257
 
266
- **Priority**: Project config > Global config > CLI arguments > Defaults
258
+ **Priority**: CLI arguments > Project config > Global config > Defaults
267
259
 
268
260
  ## Advanced Features
269
261
 
262
+ ### Request History
263
+
264
+ Track all HTTP requests for debugging:
265
+
266
+ ```bash
267
+ # View request history
268
+ curl http://localhost:8080/history
269
+ ```
270
+
271
+ Response:
272
+
273
+ ```json
274
+ {
275
+ "total": 10,
276
+ "requests": [
277
+ {
278
+ "timestamp": "2026-02-15T06:09:17.875Z",
279
+ "method": "GET",
280
+ "url": "/index.html",
281
+ "status": 200,
282
+ "duration": "5ms",
283
+ "ip": "127.0.0.1"
284
+ }
285
+ ]
286
+ }
287
+ ```
288
+
270
289
  ### Environment Variables
271
290
 
272
291
  Automatically loads `.env` file from project root:
@@ -357,7 +376,7 @@ cat gib-runs.log | jq 'select(.duration | tonumber > 100)'
357
376
  Create `middleware/custom.js`:
358
377
 
359
378
  ```javascript
360
- module.exports = function(req, res, next) {
379
+ module.exports = (req, res, next) => {
361
380
  console.log('Request:', req.url);
362
381
  next();
363
382
  };
@@ -379,7 +398,6 @@ Server automatically binds to `0.0.0.0` and shows all network URLs:
379
398
  📁 Root: /home/user/project
380
399
  🌐 Local: http://127.0.0.1:8080
381
400
  🔗 Network: http://192.168.1.100:8080
382
- 🔗 Network: http://10.0.0.5:8080
383
401
  🔄 Live Reload: Enabled
384
402
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
385
403
  ```
package/index.js CHANGED
@@ -1,27 +1,27 @@
1
1
  #!/usr/bin/env node
2
- var fs = require('fs'),
3
- connect = require('connect'),
4
- serveIndex = require('serve-index'),
5
- logger = require('morgan'),
6
- WebSocket = require('faye-websocket'),
7
- path = require('path'),
8
- http = require('http'),
9
- send = require('send'),
10
- open = require('open'),
11
- es = require("event-stream"),
12
- compression = require('compression'),
13
- os = require('os'),
14
- chokidar = require('chokidar'),
15
- chalk = require('chalk');
2
+ const fs = require('fs');
3
+ const connect = require('connect');
4
+ const serveIndex = require('serve-index');
5
+ const logger = require('morgan');
6
+ const WebSocket = require('faye-websocket');
7
+ const path = require('path');
8
+ const http = require('http');
9
+ const send = require('send');
10
+ const open = require('open');
11
+ const es = require('event-stream');
12
+ const compression = require('compression');
13
+ const os = require('os');
14
+ const chokidar = require('chokidar');
15
+ const chalk = require('chalk');
16
16
 
17
17
  try {
18
18
  require('dotenv').config({ path: path.join(process.cwd(), '.env') });
19
19
  } catch (e) {}
20
20
 
21
- var INJECTED_CODE = fs.readFileSync(path.join(__dirname, "injected.html"), "utf8");
22
- var packageJson = require('./package.json');
21
+ const INJECTED_CODE = fs.readFileSync(path.join(__dirname, 'injected.html'), 'utf8');
22
+ const packageJson = require('./package.json');
23
23
 
24
- var GibRuns = {
24
+ const GibRuns = {
25
25
  server: null,
26
26
  watcher: null,
27
27
  logLevel: 2,
@@ -35,81 +35,76 @@ var GibRuns = {
35
35
  restartCount: 0
36
36
  };
37
37
 
38
- function escape(html){
39
- return String(html)
40
- .replace(/&(?!\w+;)/g, '&')
41
- .replace(/</g, '&lt;')
42
- .replace(/>/g, '&gt;')
43
- .replace(/"/g, '&quot;');
44
- }
38
+ const escape = (html) => String(html)
39
+ .replace(/&(?!\w+;)/g, '&amp;')
40
+ .replace(/</g, '&lt;')
41
+ .replace(/>/g, '&gt;')
42
+ .replace(/"/g, '&quot;');
45
43
 
46
- // Based on connect.static(), but streamlined and with added code injecter
47
44
  function staticServer(root) {
48
- var isFile = false;
49
- try { // For supporting mounting files instead of just directories
45
+ let isFile = false;
46
+ try {
50
47
  isFile = fs.statSync(root).isFile();
51
48
  } catch (e) {
52
- if (e.code !== "ENOENT") throw e;
49
+ if (e.code !== 'ENOENT') throw e;
53
50
  }
54
- return function(req, res, next) {
55
- if (req.method !== "GET" && req.method !== "HEAD") return next();
56
- var reqpath = isFile ? "" : new URL(req.url, 'http://localhost').pathname;
57
- var hasNoOrigin = !req.headers.origin;
58
- var injectCandidates = [ new RegExp("</body>", "i"), new RegExp("</svg>"), new RegExp("</head>", "i")];
59
- var injectTag = null;
60
-
61
- function directory() {
62
- var pathname = new URL(req.originalUrl, 'http://localhost').pathname;
51
+
52
+ return (req, res, next) => {
53
+ if (req.method !== 'GET' && req.method !== 'HEAD') return next();
54
+
55
+ const reqpath = isFile ? '' : new URL(req.url, 'http://localhost').pathname;
56
+ const hasNoOrigin = !req.headers.origin;
57
+ const injectCandidates = [/<\/body>/i, /<\/svg>/i, /<\/head>/i];
58
+ let injectTag = null;
59
+
60
+ const directory = () => {
61
+ const pathname = new URL(req.originalUrl, 'http://localhost').pathname;
63
62
  res.statusCode = 301;
64
63
  res.setHeader('Location', pathname + '/');
65
64
  res.end('Redirecting to ' + escape(pathname) + '/');
66
- }
65
+ };
67
66
 
68
- function file(filepath /*, stat*/) {
69
- var x = path.extname(filepath).toLocaleLowerCase(), match,
70
- possibleExtensions = [ "", ".html", ".htm", ".xhtml", ".php", ".svg" ];
71
- if (hasNoOrigin && (possibleExtensions.indexOf(x) > -1)) {
72
- // TODO: Sync file read here is not nice, but we need to determine if the html should be injected or not
73
- var contents = fs.readFileSync(filepath, "utf8");
74
- for (var i = 0; i < injectCandidates.length; ++i) {
75
- match = injectCandidates[i].exec(contents);
76
- if (match) {
77
- injectTag = match[0];
78
- break;
79
- }
80
- }
81
- if (injectTag === null && GibRuns.logLevel >= 3) {
82
- console.warn(chalk.yellow("⚠ Failed to inject refresh script!"),
83
- "Couldn't find any of the tags", injectCandidates, "from", filepath);
67
+ const file = (filepath) => {
68
+ const ext = path.extname(filepath).toLowerCase();
69
+ const injectableExts = ['', '.html', '.htm', '.xhtml', '.php', '.svg'];
70
+
71
+ if (hasNoOrigin && injectableExts.includes(ext)) {
72
+ const contents = fs.readFileSync(filepath, 'utf8');
73
+ const match = injectCandidates.find(regex => regex.test(contents));
74
+
75
+ if (match) {
76
+ injectTag = contents.match(match)[0];
77
+ } else if (GibRuns.logLevel >= 3) {
78
+ console.warn(chalk.yellow('⚠ Failed to inject refresh script!'),
79
+ "Couldn't find any of the tags", injectCandidates, 'from', filepath);
84
80
  }
85
81
  }
86
- }
82
+ };
87
83
 
88
- function error(err) {
84
+ const error = (err) => {
89
85
  if (err.status === 404) return next();
90
86
  next(err);
91
- }
87
+ };
92
88
 
93
- function inject(stream) {
89
+ const inject = (stream) => {
94
90
  if (injectTag) {
95
- // We need to modify the length given to browser
96
- var len = INJECTED_CODE.length + res.getHeader('Content-Length');
91
+ const len = INJECTED_CODE.length + res.getHeader('Content-Length');
97
92
  res.setHeader('Content-Length', len);
98
- var originalPipe = stream.pipe;
99
- stream.pipe = function(resp) {
100
- // Replace environment variables ${VAR_NAME} with actual values
101
- var envReplacer = es.mapSync(function(data) {
102
- return data.toString().replace(/\$\{([^}]+)\}/g, function(match, varName) {
103
- return process.env[varName] || '';
104
- });
105
- });
106
- var codeInject = es.replace(new RegExp(injectTag, "i"), INJECTED_CODE + injectTag);
93
+ const originalPipe = stream.pipe;
94
+
95
+ stream.pipe = (resp) => {
96
+ const envReplacer = es.mapSync((data) =>
97
+ data.toString().replace(/\$\{([^}]+)\}/g, (match, varName) =>
98
+ process.env[varName] || ''
99
+ )
100
+ );
101
+ const codeInject = es.replace(new RegExp(injectTag, 'i'), INJECTED_CODE + injectTag);
107
102
  originalPipe.call(stream, envReplacer).pipe(codeInject).pipe(resp);
108
103
  };
109
104
  }
110
- }
105
+ };
111
106
 
112
- send(req, reqpath, { root: root })
107
+ send(req, reqpath, { root })
113
108
  .on('error', error)
114
109
  .on('directory', directory)
115
110
  .on('file', file)
@@ -118,16 +113,11 @@ function staticServer(root) {
118
113
  };
119
114
  }
120
115
 
121
- /**
122
- * Rewrite request URL and pass it back to the static handler.
123
- * @param staticHandler {function} Next handler
124
- * @param file {string} Path to the entry point file
125
- */
126
116
  function entryPoint(staticHandler, file) {
127
- if (!file) return function(req, res, next) { next(); };
117
+ if (!file) return (req, res, next) => next();
128
118
 
129
- return function(req, res, next) {
130
- req.url = "/" + file;
119
+ return (req, res, next) => {
120
+ req.url = '/' + file;
131
121
  staticHandler(req, res, next);
132
122
  };
133
123
  }
@@ -228,23 +218,29 @@ GibRuns.start = function(options) {
228
218
  app.use(compression());
229
219
  }
230
220
 
231
- // Request counter middleware
221
+ // Request counter and logging middleware
232
222
  if (serveStatic) {
233
223
  app.use(function(req, res, next) {
234
224
  GibRuns.requestCount++;
225
+ const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
226
+ const timestamp = new Date().toISOString();
235
227
 
236
- // Log requests in verbose mode
237
- if (GibRuns.logLevel >= 3) {
238
- var timestamp = new Date().toLocaleTimeString();
239
- console.log(chalk.gray(' [' + timestamp + '] ') +
228
+ // Log all incoming requests
229
+ if (GibRuns.logLevel >= 1) {
230
+ console.log(chalk.gray(` [${timestamp}] `) +
240
231
  chalk.cyan(req.method) + ' ' +
241
232
  chalk.white(req.url) + ' ' +
242
- chalk.gray('from ' + (req.headers['x-forwarded-for'] || req.connection.remoteAddress)));
233
+ chalk.gray(`from ${ip}`));
243
234
  }
244
235
 
245
236
  next();
246
237
  });
247
238
 
239
+ // Add request history tracking
240
+ const historyMiddleware = require('./middleware/history')();
241
+ app.use(historyMiddleware);
242
+ GibRuns.getRequestHistory = require('./middleware/history').getHistory;
243
+
248
244
  // Add health check endpoint
249
245
  if (enableHealth) {
250
246
  app.use(require('./middleware/health')(GibRuns));
@@ -377,25 +373,26 @@ GibRuns.start = function(options) {
377
373
 
378
374
  // Handle server startup errors
379
375
  server.addListener('error', function(e) {
376
+ const timestamp = new Date().toISOString();
380
377
  if (e.code === 'EADDRINUSE') {
381
378
  var serveURL = protocol + '://' + host + ':' + port;
382
- console.log(chalk.yellow("" + serveURL + " is already in use. Trying another port..."));
383
- if (GibRuns.logLevel >= 3) {
384
- console.log(chalk.gray(' 💡 Port ' + port + ' is occupied, searching for available port...'));
379
+ console.log(chalk.yellow(`[${timestamp}] ${serveURL} is already in use. Trying another port...`));
380
+ if (GibRuns.logLevel >= 2) {
381
+ console.log(chalk.gray(` 💡 Port ${port} is occupied, searching for available port...`));
385
382
  }
386
383
  setTimeout(function() {
387
384
  server.listen(0, host);
388
385
  }, 1000);
389
386
  } else {
390
- console.error(chalk.red(" ✖ Server Error: " + e.toString()));
391
- if (GibRuns.logLevel >= 3) {
387
+ console.error(chalk.red(`[${timestamp}] Server Error: ${e.toString()}`));
388
+ if (GibRuns.logLevel >= 2) {
392
389
  console.error(chalk.gray(' Stack trace:'), e.stack);
393
390
  }
394
391
 
395
392
  // Auto-restart on crash if enabled
396
393
  if (autoRestart && GibRuns.restartCount < 5) {
397
394
  GibRuns.restartCount++;
398
- console.log(chalk.yellow(' 🔄 Auto-restarting server (attempt ' + GibRuns.restartCount + '/5)...'));
395
+ console.log(chalk.yellow(` 🔄 [${timestamp}] Auto-restarting server (attempt ${GibRuns.restartCount}/5)...`));
399
396
  setTimeout(function() {
400
397
  GibRuns.start(options);
401
398
  }, 2000);
@@ -647,26 +644,48 @@ GibRuns.start = function(options) {
647
644
  });
648
645
  function handleChange(changePath) {
649
646
  GibRuns.reloadCount++;
650
- var cssChange = path.extname(changePath) === ".css" && !noCssInject;
651
- var relPath = path.relative(root, changePath);
652
- var timestamp = new Date().toLocaleTimeString();
647
+ const cssChange = path.extname(changePath) === ".css" && !noCssInject;
648
+ const relPath = path.relative(root, changePath);
649
+ const timestamp = new Date().toISOString();
650
+ const fileSize = fs.existsSync(changePath) ? fs.statSync(changePath).size : 0;
653
651
 
654
652
  if (GibRuns.logLevel >= 1) {
655
653
  if (cssChange) {
656
- console.log(chalk.magenta(' ⚡ [' + timestamp + '] CSS updated: ') + chalk.gray(relPath));
654
+ console.log(chalk.magenta(` ⚡ [${timestamp}] CSS updated: `) + chalk.gray(relPath) +
655
+ chalk.dim(` (${(fileSize / 1024).toFixed(2)}KB)`));
657
656
  } else {
658
- console.log(chalk.cyan(' 🔄 [' + timestamp + '] File changed: ') + chalk.gray(relPath));
657
+ console.log(chalk.cyan(` 🔄 [${timestamp}] File changed: `) + chalk.gray(relPath) +
658
+ chalk.dim(` (${(fileSize / 1024).toFixed(2)}KB)`));
659
659
  }
660
660
  }
661
+
661
662
  clients.forEach(function(ws) {
662
- if (ws)
663
- ws.send(cssChange ? 'refreshcss' : 'reload');
663
+ if (ws) ws.send(cssChange ? 'refreshcss' : 'reload');
664
664
  });
665
665
  }
666
+
667
+ function handleAdd(addPath) {
668
+ const relPath = path.relative(root, addPath);
669
+ const timestamp = new Date().toISOString();
670
+ if (GibRuns.logLevel >= 2) {
671
+ console.log(chalk.green(` ➕ [${timestamp}] File added: `) + chalk.gray(relPath));
672
+ }
673
+ handleChange(addPath);
674
+ }
675
+
676
+ function handleUnlink(unlinkPath) {
677
+ const relPath = path.relative(root, unlinkPath);
678
+ const timestamp = new Date().toISOString();
679
+ if (GibRuns.logLevel >= 2) {
680
+ console.log(chalk.red(` ➖ [${timestamp}] File deleted: `) + chalk.gray(relPath));
681
+ }
682
+ handleChange(unlinkPath);
683
+ }
684
+
666
685
  GibRuns.watcher
667
686
  .on("change", handleChange)
668
- .on("add", handleChange)
669
- .on("unlink", handleChange)
687
+ .on("add", handleAdd)
688
+ .on("unlink", handleUnlink)
670
689
  .on("addDir", handleChange)
671
690
  .on("unlinkDir", handleChange)
672
691
  .on("ready", function () {
@@ -674,7 +693,11 @@ GibRuns.start = function(options) {
674
693
  console.log(chalk.cyan(" ✓ Watching for file changes...\n"));
675
694
  })
676
695
  .on("error", function (err) {
677
- console.log(chalk.red(" ✖ Watcher Error: ") + err);
696
+ const timestamp = new Date().toISOString();
697
+ console.log(chalk.red(` ✖ [${timestamp}] Watcher Error: `) + err.message);
698
+ if (GibRuns.logLevel >= 2) {
699
+ console.error(err.stack);
700
+ }
678
701
  });
679
702
 
680
703
  return server;