gib-runs 2.3.2 → 2.3.5

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,69 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.3.5] - 2026-02-12
6
+
7
+ ### Added
8
+ - 🔄 **Environment Variable Replacement** - Automatic replacement of `${VAR_NAME}` patterns in HTML files
9
+ - Reads from `.env` file automatically
10
+ - Works with all environment variables (APP_NAME, API_KEY, etc)
11
+ - No configuration needed
12
+
13
+ ## [2.3.4] - 2026-02-12
14
+
15
+ ### Added - New Features 🎉
16
+ - 🔁 **Auto-Restart on Crash** - Server automatically restarts on unexpected errors
17
+ - Use `--auto-restart` flag to enable
18
+ - Attempts up to 5 restarts before giving up
19
+ - Resilient mode for production-like development
20
+ - Displays restart attempt count in console
21
+ - 📤 **File Upload Endpoint** - Built-in file upload support for development
22
+ - Use `--enable-upload` flag to enable
23
+ - POST files to `/upload` endpoint
24
+ - 10MB file size limit
25
+ - Files saved to `./uploads` directory
26
+ - Returns JSON response with file details
27
+ - 💚 **Health Check Endpoint** - Monitor server health and statistics
28
+ - Enabled by default (use `--no-health` to disable)
29
+ - Access via `GET /health` or `GET /_health`
30
+ - Returns JSON with uptime, memory usage, request count, reload count
31
+ - System information (CPU, memory, platform)
32
+ - Perfect for monitoring and debugging
33
+ - 📝 **Request Logging to File** - Log all requests to file for debugging
34
+ - Use `--log-to-file` flag to enable
35
+ - Logs saved to `gib-runs.log` in project root
36
+ - JSON format with timestamp, method, URL, IP, user-agent, status, duration
37
+ - Automatic log rotation at 10MB
38
+ - Old logs backed up with timestamp
39
+ - 🎨 **Custom Error Pages** - Beautiful, informative error pages
40
+ - Enabled by default (use `--no-error-page` to disable)
41
+ - Modern gradient design with detailed error information
42
+ - Shows error stack trace in development mode
43
+ - Covers all HTTP error codes (400, 401, 403, 404, 500, etc)
44
+ - Responsive design for mobile devices
45
+ - 🌍 **Environment Variable Support** - Automatic .env file loading
46
+ - Automatically loads `.env` file from project root
47
+ - Uses dotenv package
48
+ - No configuration needed, just create `.env` file
49
+ - Perfect for API keys, database URLs, etc
50
+ - 📡 **WebSocket Broadcasting API** - Send custom messages to all connected clients
51
+ - New `GibRuns.broadcast(message)` method
52
+ - Broadcast custom reload triggers or notifications
53
+ - Useful for custom build tools and integrations
54
+
55
+ ### Improved
56
+ - 🔧 **Better Error Handling** - More informative error messages with stack traces
57
+ - 📊 **Enhanced Health Monitoring** - More detailed system metrics
58
+ - 🎯 **Middleware Architecture** - Cleaner middleware loading and organization
59
+ - 📦 **Dependencies** - Added `dotenv` and `multer` for new features
60
+
61
+ ### Technical
62
+ - Version bumped to 2.3.4
63
+ - All existing tests passing
64
+ - Backward compatible with all previous versions
65
+ - New middleware files: `upload.js`, `health.js`, `logger.js`, `error-page.js`
66
+ - Enhanced GibRuns object with `wsClients`, `autoRestart`, `restartCount` properties
67
+
5
68
  ## [2.3.0] - 2026-02-10
6
69
 
7
70
  ### Fixed - Critical
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![license](https://img.shields.io/npm/l/gib-runs.svg)](https://github.com/levouinse/gib-runs/blob/main/LICENSE)
4
4
  [![tests](https://img.shields.io/badge/tests-32%20passing-brightgreen.svg)](https://github.com/levouinse/gib-runs)
5
5
 
6
- # 🚀 GIB-RUNS
6
+ # 🚀 GIB-RUNS v2.3.5
7
7
 
8
8
  **Modern development server with live reload - Unlike some people, this actually runs on merit, not connections.**
9
9
 
@@ -61,6 +61,18 @@ The name is a playful nod to Indonesia's Vice President Gibran Rakabuming Raka,
61
61
  - 🚦 **Rate Limiting** - Protect against abuse (better protection than family connections)
62
62
  - 🌐 **Network Access** - True network binding that actually works (unlike some political promises)
63
63
 
64
+ ### New in v2.3.5 🎉
65
+ - 🔄 **Environment Variable Replacement** - Automatic replacement of `${VAR_NAME}` in HTML files from .env
66
+
67
+ ### New in v2.3.5 🎉
68
+ - 🔁 **Auto-Restart on Crash** - Automatically restart server on unexpected errors (resilient mode)
69
+ - 📤 **File Upload Endpoint** - Built-in file upload support for development (POST to /upload)
70
+ - 💚 **Health Check Endpoint** - Monitor server health and statistics (GET /health)
71
+ - 📝 **Request Logging to File** - Log all requests to file for debugging (gib-runs.log)
72
+ - 🎨 **Custom Error Pages** - Beautiful, informative error pages with stack traces
73
+ - 🌍 **Environment Variables** - Automatic .env file loading (dotenv support)
74
+ - 📡 **WebSocket Broadcasting** - Send custom messages to all connected clients
75
+
64
76
  ## 📦 Installation
65
77
 
66
78
  ### Global Installation (Recommended)
@@ -155,6 +167,11 @@ gib-runs dist --port=3000 --spa --cors --no-browser
155
167
  | `--npm-script=SCRIPT` | Run npm script (dev, start, etc) | None |
156
168
  | `--pm2` | Use PM2 process manager | `false` |
157
169
  | `--pm2-name=NAME` | PM2 app name | `gib-runs-app` |
170
+ | `--auto-restart` | Auto-restart server on crash | `false` |
171
+ | `--enable-upload` | Enable file upload endpoint | `false` |
172
+ | `--no-health` | Disable health check endpoint | `false` |
173
+ | `--log-to-file` | Log requests to file | `false` |
174
+ | `--no-error-page` | Disable custom error pages | `false` |
158
175
  | `-v, --version` | Show version | - |
159
176
  | `-h, --help` | Show help | - |
160
177
 
@@ -372,7 +389,7 @@ gib-runs
372
389
  Network URLs are **ALWAYS shown automatically** when you start the server:
373
390
 
374
391
  ```
375
- 🚀 GIB-RUNS v2.3.2
392
+ 🚀 GIB-RUNS v2.3.5
376
393
  "Unlike Gibran, this actually works through merit"
377
394
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
378
395
  📁 Root: /home/user/project
@@ -491,7 +508,7 @@ gib-runs --tunnel-service=tunnelto
491
508
  ### Example Output
492
509
 
493
510
  ```
494
- 🚀 GIB-RUNS v2.3.2
511
+ 🚀 GIB-RUNS v2.3.5
495
512
  "Unlike Gibran, this actually works through merit"
496
513
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
497
514
  📁 Root: /home/user/project
@@ -621,7 +638,7 @@ pm2 list
621
638
  ### Example Output
622
639
 
623
640
  ```
624
- 🚀 GIB-RUNS v2.3.2
641
+ 🚀 GIB-RUNS v2.3.5
625
642
  "Unlike Gibran, this actually works through merit"
626
643
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
627
644
  📁 Root: /home/user/project
@@ -648,6 +665,271 @@ pm2 list
648
665
 
649
666
  **Unlike Gibran's career, these processes run on actual merit and capability!** 🔥
650
667
 
668
+ ## 🆕 New Features in v2.3.5
669
+
670
+ ### Auto-Restart on Crash
671
+
672
+ **Automatically restart your server when it crashes - resilient mode for development!**
673
+
674
+ ```bash
675
+ # Enable auto-restart
676
+ gib-runs --auto-restart
677
+
678
+ # With other options
679
+ gib-runs --auto-restart --port=3000 --spa
680
+ ```
681
+
682
+ **How it works:**
683
+ - Server automatically restarts on unexpected errors
684
+ - Attempts up to 5 restarts before giving up
685
+ - Shows restart attempt count in console
686
+ - Perfect for unstable development environments
687
+ - Keeps your workflow uninterrupted
688
+
689
+ **Example output:**
690
+ ```
691
+ ✖ Server Error: ECONNRESET
692
+ 🔄 Auto-restarting server (attempt 1/5)...
693
+ ✓ Server restarted successfully
694
+ ```
695
+
696
+ ### File Upload Endpoint
697
+
698
+ **Built-in file upload support for development - no need for separate upload server!**
699
+
700
+ ```bash
701
+ # Enable file upload endpoint
702
+ gib-runs --enable-upload
703
+
704
+ # Files will be saved to ./uploads directory
705
+ ```
706
+
707
+ **Usage:**
708
+ ```html
709
+ <!-- HTML Form -->
710
+ <form action="/upload" method="POST" enctype="multipart/form-data">
711
+ <input type="file" name="file">
712
+ <button type="submit">Upload</button>
713
+ </form>
714
+ ```
715
+
716
+ ```javascript
717
+ // JavaScript Fetch API
718
+ const formData = new FormData();
719
+ formData.append('file', fileInput.files[0]);
720
+
721
+ fetch('/upload', {
722
+ method: 'POST',
723
+ body: formData
724
+ })
725
+ .then(res => res.json())
726
+ .then(data => {
727
+ console.log('Uploaded:', data.file);
728
+ // { filename: 'file-123456789.jpg', originalname: 'photo.jpg', size: 12345, path: '/path/to/uploads/...' }
729
+ });
730
+ ```
731
+
732
+ **Features:**
733
+ - 10MB file size limit
734
+ - Files saved to `./uploads` directory
735
+ - Automatic directory creation
736
+ - Unique filenames with timestamp
737
+ - JSON response with file details
738
+ - Error handling for invalid uploads
739
+
740
+ ### Health Check Endpoint
741
+
742
+ **Monitor your server health and statistics - transparency in action!**
743
+
744
+ ```bash
745
+ # Health check is enabled by default
746
+ gib-runs
747
+
748
+ # Disable if needed
749
+ gib-runs --no-health
750
+ ```
751
+
752
+ **Access health endpoint:**
753
+ ```bash
754
+ # Via curl
755
+ curl http://localhost:8080/health
756
+
757
+ # Or in browser
758
+ http://localhost:8080/health
759
+ ```
760
+
761
+ **Response example:**
762
+ ```json
763
+ {
764
+ "status": "healthy",
765
+ "uptime": 123.45,
766
+ "timestamp": "2026-02-12T09:00:00.000Z",
767
+ "server": {
768
+ "requests": 42,
769
+ "reloads": 5,
770
+ "memory": {
771
+ "rss": "45MB",
772
+ "heapUsed": "23MB",
773
+ "heapTotal": "35MB"
774
+ }
775
+ },
776
+ "system": {
777
+ "platform": "linux",
778
+ "arch": "x64",
779
+ "cpus": 8,
780
+ "freemem": "2048MB",
781
+ "totalmem": "16384MB",
782
+ "loadavg": [1.2, 1.5, 1.8]
783
+ }
784
+ }
785
+ ```
786
+
787
+ **Use cases:**
788
+ - Monitor server performance
789
+ - Debug memory leaks
790
+ - Track request patterns
791
+ - Integration with monitoring tools
792
+ - Health checks for Docker containers
793
+
794
+ ### Request Logging to File
795
+
796
+ **Log all requests to file for debugging - transparent and verifiable!**
797
+
798
+ ```bash
799
+ # Enable file logging
800
+ gib-runs --log-to-file
801
+
802
+ # Logs saved to gib-runs.log in project root
803
+ ```
804
+
805
+ **Log format (JSON):**
806
+ ```json
807
+ {"timestamp":"2026-02-12T09:00:00.000Z","method":"GET","url":"/index.html","ip":"127.0.0.1","userAgent":"Mozilla/5.0...","status":200,"duration":"5ms"}
808
+ {"timestamp":"2026-02-12T09:00:01.000Z","method":"GET","url":"/style.css","ip":"127.0.0.1","userAgent":"Mozilla/5.0...","status":200,"duration":"2ms"}
809
+ ```
810
+
811
+ **Features:**
812
+ - JSON format for easy parsing
813
+ - Includes timestamp, method, URL, IP, user-agent, status, duration
814
+ - Automatic log rotation at 10MB
815
+ - Old logs backed up with timestamp
816
+ - Perfect for debugging and analytics
817
+
818
+ **Parse logs with jq:**
819
+ ```bash
820
+ # Show all 404 errors
821
+ cat gib-runs.log | jq 'select(.status == 404)'
822
+
823
+ # Show slow requests (>100ms)
824
+ cat gib-runs.log | jq 'select(.duration | tonumber > 100)'
825
+
826
+ # Count requests by URL
827
+ cat gib-runs.log | jq -r '.url' | sort | uniq -c
828
+ ```
829
+
830
+ ### Custom Error Pages
831
+
832
+ **Beautiful, informative error pages - unlike some political errors!**
833
+
834
+ ```bash
835
+ # Custom error pages enabled by default
836
+ gib-runs
837
+
838
+ # Disable if needed
839
+ gib-runs --no-error-page
840
+ ```
841
+
842
+ **Features:**
843
+ - Modern gradient design
844
+ - Detailed error information
845
+ - Shows error stack trace in development mode
846
+ - Covers all HTTP error codes (400, 401, 403, 404, 500, etc)
847
+ - Responsive design for mobile devices
848
+ - "Back to Home" button
849
+ - Professional appearance
850
+
851
+ **Error codes covered:**
852
+ - 400 Bad Request
853
+ - 401 Unauthorized
854
+ - 403 Forbidden
855
+ - 404 Not Found
856
+ - 405 Method Not Allowed
857
+ - 500 Internal Server Error
858
+ - 502 Bad Gateway
859
+ - 503 Service Unavailable
860
+
861
+ ### Environment Variables
862
+
863
+ **Automatic .env file loading - no configuration needed!**
864
+
865
+ ```bash
866
+ # Just create .env file in project root
867
+ echo "API_KEY=your-secret-key" > .env
868
+ echo "DATABASE_URL=postgres://localhost/mydb" >> .env
869
+
870
+ # Start server (automatically loads .env)
871
+ gib-runs
872
+ ```
873
+
874
+ **Access in your code:**
875
+ ```javascript
876
+ // Node.js
877
+ const apiKey = process.env.API_KEY;
878
+ const dbUrl = process.env.DATABASE_URL;
879
+
880
+ console.log('API Key:', apiKey);
881
+ console.log('Database:', dbUrl);
882
+ ```
883
+
884
+ **Features:**
885
+ - Automatic loading on server start
886
+ - No configuration needed
887
+ - Uses dotenv package
888
+ - Perfect for API keys, database URLs, etc
889
+ - Keeps secrets out of version control
890
+
891
+ ### WebSocket Broadcasting API
892
+
893
+ **Send custom messages to all connected clients - programmatic control!**
894
+
895
+ ```javascript
896
+ const gibRuns = require('gib-runs');
897
+
898
+ // Start server
899
+ const server = gibRuns.start({
900
+ port: 8080,
901
+ root: './public'
902
+ });
903
+
904
+ // Broadcast custom message to all clients
905
+ gibRuns.broadcast('custom-reload');
906
+
907
+ // Trigger reload from your build script
908
+ gibRuns.broadcast('reload');
909
+
910
+ // Send custom data
911
+ gibRuns.broadcast(JSON.stringify({ type: 'notification', message: 'Build complete!' }));
912
+ ```
913
+
914
+ **Use cases:**
915
+ - Custom build tool integration
916
+ - Trigger reload from external scripts
917
+ - Send notifications to browser
918
+ - Custom live reload logic
919
+ - Integration with CI/CD pipelines
920
+
921
+ **Client-side handling:**
922
+ ```javascript
923
+ // In your HTML/JavaScript
924
+ const ws = new WebSocket('ws://localhost:8080');
925
+ ws.onmessage = function(event) {
926
+ if (event.data === 'custom-reload') {
927
+ console.log('Custom reload triggered!');
928
+ location.reload();
929
+ }
930
+ };
931
+ ```
932
+
651
933
  ## 🐛 Troubleshooting
652
934
 
653
935
  ### No reload on changes
package/gib-run.js CHANGED
@@ -140,6 +140,26 @@ for (var i = process.argv.length - 1; i >= 2; --i) {
140
140
  opts.tunnel = true;
141
141
  process.argv.splice(i, 1);
142
142
  }
143
+ else if (arg === "--auto-restart") {
144
+ opts.autoRestart = true;
145
+ process.argv.splice(i, 1);
146
+ }
147
+ else if (arg === "--enable-upload") {
148
+ opts.enableUpload = true;
149
+ process.argv.splice(i, 1);
150
+ }
151
+ else if (arg === "--no-health") {
152
+ opts.enableHealth = false;
153
+ process.argv.splice(i, 1);
154
+ }
155
+ else if (arg === "--log-to-file") {
156
+ opts.logToFile = true;
157
+ process.argv.splice(i, 1);
158
+ }
159
+ else if (arg === "--no-error-page") {
160
+ opts.customErrorPage = false;
161
+ process.argv.splice(i, 1);
162
+ }
143
163
  else if (arg.indexOf("--tunnel-service=") > -1) {
144
164
  opts.tunnelService = arg.substring(17);
145
165
  opts.tunnel = true;
@@ -245,7 +265,12 @@ for (var i = process.argv.length - 1; i >= 2; --i) {
245
265
  console.log(chalk.yellow(' --exec=COMMAND ') + chalk.gray('Run custom command'));
246
266
  console.log(chalk.yellow(' --npm-script=SCRIPT ') + chalk.gray('Run npm script (dev, start, etc)'));
247
267
  console.log(chalk.yellow(' --pm2 ') + chalk.gray('Use PM2 process manager'));
248
- console.log(chalk.yellow(' --pm2-name=NAME ') + chalk.gray('PM2 app name (default: gib-runs-app)\n'));
268
+ console.log(chalk.yellow(' --pm2-name=NAME ') + chalk.gray('PM2 app name (default: gib-runs-app)'));
269
+ console.log(chalk.yellow(' --auto-restart ') + chalk.gray('Auto-restart server on crash'));
270
+ console.log(chalk.yellow(' --enable-upload ') + chalk.gray('Enable file upload endpoint'));
271
+ console.log(chalk.yellow(' --no-health ') + chalk.gray('Disable health check endpoint'));
272
+ console.log(chalk.yellow(' --log-to-file ') + chalk.gray('Log requests to file'));
273
+ console.log(chalk.yellow(' --no-error-page ') + chalk.gray('Disable custom error pages\n'));
249
274
  console.log(chalk.gray(' Examples:\n'));
250
275
  console.log(chalk.gray(' gib-runs'));
251
276
  console.log(chalk.gray(' gib-runs --port=3000 --verbose'));
@@ -256,7 +281,9 @@ for (var i = process.argv.length - 1; i >= 2; --i) {
256
281
  console.log(chalk.gray(' gib-runs --npm-script=dev'));
257
282
  console.log(chalk.gray(' gib-runs --exec="npm run build && npm start"'));
258
283
  console.log(chalk.gray(' gib-runs --npm-script=dev --pm2 --pm2-name=my-app'));
259
- console.log(chalk.gray(' gib-runs --performance --rate-limit=50\n'));
284
+ console.log(chalk.gray(' gib-runs --performance --rate-limit=50'));
285
+ console.log(chalk.gray(' gib-runs --auto-restart --enable-upload'));
286
+ console.log(chalk.gray(' gib-runs --log-to-file --verbose\n'));
260
287
  process.exit();
261
288
  }
262
289
  else if (arg === "--test") {
package/index.js CHANGED
@@ -14,7 +14,15 @@ var fs = require('fs'),
14
14
  chokidar = require('chokidar'),
15
15
  chalk = require('chalk');
16
16
 
17
+ // Load environment variables from .env file
18
+ try {
19
+ require('dotenv').config({ path: path.join(process.cwd(), '.env') });
20
+ } catch (e) {
21
+ // dotenv not available, skip
22
+ }
23
+
17
24
  var INJECTED_CODE = fs.readFileSync(path.join(__dirname, "injected.html"), "utf8");
25
+ var packageJson = require('./package.json');
18
26
 
19
27
  var GibRuns = {
20
28
  server: null,
@@ -24,7 +32,10 @@ var GibRuns = {
24
32
  requestCount: 0,
25
33
  reloadCount: 0,
26
34
  tunnel: null,
27
- processRunner: null
35
+ processRunner: null,
36
+ wsClients: [],
37
+ autoRestart: false,
38
+ restartCount: 0
28
39
  };
29
40
 
30
41
  function escape(html){
@@ -63,6 +74,12 @@ function staticServer(root) {
63
74
  if (hasNoOrigin && (possibleExtensions.indexOf(x) > -1)) {
64
75
  // TODO: Sync file read here is not nice, but we need to determine if the html should be injected or not
65
76
  var contents = fs.readFileSync(filepath, "utf8");
77
+
78
+ // Replace environment variables like ${APP_NAME}
79
+ contents = contents.replace(/\$\{([A-Z_]+)\}/g, function(fullMatch, varName) {
80
+ return process.env[varName] || fullMatch;
81
+ });
82
+
66
83
  for (var i = 0; i < injectCandidates.length; ++i) {
67
84
  match = injectCandidates[i].exec(contents);
68
85
  if (match) {
@@ -89,7 +106,12 @@ function staticServer(root) {
89
106
  res.setHeader('Content-Length', len);
90
107
  var originalPipe = stream.pipe;
91
108
  stream.pipe = function(resp) {
92
- originalPipe.call(stream, es.replace(new RegExp(injectTag, "i"), INJECTED_CODE + injectTag)).pipe(resp);
109
+ // Replace ${ENV_VAR} then inject code
110
+ var envReplace = es.replace(/\$\{([A-Z_]+)\}/g, function(match, varName) {
111
+ return process.env[varName] || match;
112
+ });
113
+ var codeInject = es.replace(new RegExp(injectTag, "i"), INJECTED_CODE + injectTag);
114
+ originalPipe.call(stream, envReplace).pipe(codeInject).pipe(resp);
93
115
  };
94
116
  }
95
117
  }
@@ -136,6 +158,11 @@ function entryPoint(staticHandler, file) {
136
158
  * @param compression {boolean} Enable gzip compression (default: true)
137
159
  * @param qrCode {boolean} Show QR code for network URLs (default: false)
138
160
  * @param tunnel {boolean} Create public tunnel URL (default: false)
161
+ * @param autoRestart {boolean} Auto-restart server on crash (default: false)
162
+ * @param enableUpload {boolean} Enable file upload endpoint (default: false)
163
+ * @param enableHealth {boolean} Enable health check endpoint (default: true)
164
+ * @param logToFile {boolean} Log requests to file (default: false)
165
+ * @param customErrorPage {boolean} Use custom error pages (default: true)
139
166
  */
140
167
  GibRuns.start = function(options) {
141
168
  options = options || {};
@@ -177,6 +204,13 @@ GibRuns.start = function(options) {
177
204
  var usePM2 = options.pm2 || false;
178
205
  var pm2Name = options.pm2Name || 'gib-runs-app';
179
206
  var testMode = options.test || false;
207
+ var autoRestart = options.autoRestart || false;
208
+ var enableUpload = options.enableUpload || false;
209
+ var enableHealth = options.enableHealth !== false;
210
+ var logToFile = options.logToFile || false;
211
+ var customErrorPage = options.customErrorPage !== false;
212
+
213
+ GibRuns.autoRestart = autoRestart;
180
214
 
181
215
  if (httpsModule) {
182
216
  try {
@@ -217,6 +251,27 @@ GibRuns.start = function(options) {
217
251
 
218
252
  next();
219
253
  });
254
+
255
+ // Add health check endpoint
256
+ if (enableHealth) {
257
+ app.use(require('./middleware/health')(GibRuns));
258
+ }
259
+
260
+ // Add file upload endpoint
261
+ if (enableUpload) {
262
+ app.use(require('./middleware/upload')());
263
+ if (GibRuns.logLevel >= 1) {
264
+ console.log(chalk.cyan(' 📤 File Upload: ') + chalk.green('Enabled') + chalk.gray(' (POST to /upload)'));
265
+ }
266
+ }
267
+
268
+ // Add request logger to file
269
+ if (logToFile) {
270
+ app.use(require('./middleware/logger')({ logFile: path.join(root, 'gib-runs.log') }));
271
+ if (GibRuns.logLevel >= 1) {
272
+ console.log(chalk.cyan(' 📝 File Logging: ') + chalk.green('Enabled') + chalk.gray(' (gib-runs.log)'));
273
+ }
274
+ }
220
275
 
221
276
  // Add logger. Level 2 logs only errors
222
277
  if (GibRuns.logLevel === 2) {
@@ -298,6 +353,11 @@ GibRuns.start = function(options) {
298
353
  app.use(staticServerHandler) // Custom static server
299
354
  .use(entryPoint(staticServerHandler, file))
300
355
  .use(serveIndex(root, { icons: true }));
356
+
357
+ // Add custom error page handler (must be last)
358
+ if (customErrorPage) {
359
+ app.use(require('./middleware/error-page')({ showStack: GibRuns.logLevel >= 2 }));
360
+ }
301
361
  }
302
362
 
303
363
  var server, protocol;
@@ -338,7 +398,17 @@ GibRuns.start = function(options) {
338
398
  if (GibRuns.logLevel >= 3) {
339
399
  console.error(chalk.gray(' Stack trace:'), e.stack);
340
400
  }
341
- GibRuns.shutdown();
401
+
402
+ // Auto-restart on crash if enabled
403
+ if (autoRestart && GibRuns.restartCount < 5) {
404
+ GibRuns.restartCount++;
405
+ console.log(chalk.yellow(' 🔄 Auto-restarting server (attempt ' + GibRuns.restartCount + '/5)...'));
406
+ setTimeout(function() {
407
+ GibRuns.start(options);
408
+ }, 2000);
409
+ } else {
410
+ GibRuns.shutdown();
411
+ }
342
412
  }
343
413
  });
344
414
 
@@ -353,7 +423,7 @@ GibRuns.start = function(options) {
353
423
  // Show info about what's running
354
424
  if (GibRuns.logLevel >= 1) {
355
425
  console.log('\n' + chalk.cyan.bold('━'.repeat(60)));
356
- console.log(chalk.cyan.bold(' 🚀 GIB-RUNS') + chalk.gray(' v2.3.2'));
426
+ console.log(chalk.cyan.bold(' 🚀 GIB-RUNS') + chalk.gray(' v' + packageJson.version));
357
427
  console.log(chalk.gray(' "Unlike Gibran, this actually works through merit"'));
358
428
  console.log(chalk.cyan.bold('━'.repeat(60)));
359
429
  console.log(chalk.white(' 📁 Root: ') + chalk.yellow(root));
@@ -367,6 +437,9 @@ GibRuns.start = function(options) {
367
437
  console.log(chalk.white(' 🔄 PM2: ') + chalk.green(' Enabled') + chalk.gray(' (process manager)'));
368
438
  }
369
439
  console.log(chalk.white(' 🔄 Live Reload:') + chalk.green(' Enabled') + chalk.gray(' (watching for changes)'));
440
+ if (autoRestart) {
441
+ console.log(chalk.white(' 🔁 Auto-Restart:') + chalk.green(' Enabled') + chalk.gray(' (resilient mode)'));
442
+ }
370
443
  console.log(chalk.cyan.bold('━'.repeat(60)));
371
444
  console.log(chalk.gray(' Press Ctrl+C to stop\n'));
372
445
  }
@@ -443,7 +516,7 @@ GibRuns.start = function(options) {
443
516
  // Output with beautiful formatting
444
517
  if (GibRuns.logLevel >= 1) {
445
518
  console.log('\n' + chalk.cyan.bold('━'.repeat(60)));
446
- console.log(chalk.cyan.bold(' 🚀 GIB-RUNS') + chalk.gray(' v2.3.2'));
519
+ console.log(chalk.cyan.bold(' 🚀 GIB-RUNS') + chalk.gray(' v' + packageJson.version));
447
520
  console.log(chalk.gray(' "Unlike Gibran, this actually works through merit"'));
448
521
  console.log(chalk.cyan.bold('━'.repeat(60)));
449
522
  console.log(chalk.white(' 📁 Root: ') + chalk.yellow(root));
@@ -466,6 +539,12 @@ GibRuns.start = function(options) {
466
539
  if (https) {
467
540
  console.log(chalk.white(' 🔒 HTTPS: ') + chalk.green(' Enabled') + chalk.gray(' (real security)'));
468
541
  }
542
+ if (enableHealth) {
543
+ console.log(chalk.white(' 💚 Health: ') + chalk.green(' Enabled') + chalk.gray(' (GET /health)'));
544
+ }
545
+ if (autoRestart) {
546
+ console.log(chalk.white(' 🔁 Auto-Restart:') + chalk.green(' Enabled') + chalk.gray(' (resilient mode)'));
547
+ }
469
548
  console.log(chalk.cyan.bold('━'.repeat(60)));
470
549
  console.log(chalk.gray(' Press Ctrl+C to stop'));
471
550
  console.log(chalk.yellow(' 💡 Tip: Share network URLs with your team!\n'));
@@ -540,6 +619,7 @@ GibRuns.start = function(options) {
540
619
  };
541
620
 
542
621
  clients.push(ws);
622
+ GibRuns.wsClients = clients;
543
623
  });
544
624
 
545
625
  var ignored = [
@@ -599,6 +679,22 @@ GibRuns.start = function(options) {
599
679
  return server;
600
680
  };
601
681
 
682
+ /**
683
+ * Broadcast custom message to all connected WebSocket clients
684
+ * @param {string} message - Message to broadcast
685
+ */
686
+ GibRuns.broadcast = function(message) {
687
+ if (GibRuns.wsClients && GibRuns.wsClients.length > 0) {
688
+ GibRuns.wsClients.forEach(function(ws) {
689
+ if (ws && ws.send) {
690
+ ws.send(message);
691
+ }
692
+ });
693
+ return true;
694
+ }
695
+ return false;
696
+ };
697
+
602
698
  GibRuns.shutdown = function() {
603
699
  if (GibRuns.logLevel >= 1 && GibRuns.startTime) {
604
700
  var uptime = ((Date.now() - GibRuns.startTime) / 1000).toFixed(2);
package/lib/tunnel.js CHANGED
@@ -102,15 +102,36 @@ function startLocalTunnel(port, options) {
102
102
 
103
103
  tunnel.on('close', function() {
104
104
  console.log(chalk.yellow(' ⚠ Tunnel closed'));
105
+ activeTunnel = null;
106
+ tunnelUrl = null;
107
+ });
108
+
109
+ tunnel.on('error', function(err) {
110
+ console.error(chalk.red(' ✖ Tunnel connection error:'), err.message);
111
+ console.log(chalk.yellow(' ⚠ Server continues running locally'));
112
+ console.log(chalk.gray(' 💡 Tunnel may be unstable, try alternative services:'));
113
+ console.log(chalk.gray(' • gib-runs --tunnel-service=cloudflared'));
114
+ console.log(chalk.gray(' • gib-runs --tunnel-service=ngrok --tunnel-authtoken=YOUR_TOKEN'));
115
+ console.log(chalk.gray(' • Or continue using local network URLs\n'));
116
+
117
+ // Clean up tunnel reference
118
+ activeTunnel = null;
119
+ tunnelUrl = null;
105
120
  });
106
121
  }).catch(function(err) {
107
- console.error(chalk.red(' ✖ LocalTunnel error:'), err.message);
108
- console.log(chalk.yellow(' 💡 Install: npm install -g localtunnel'));
122
+ console.error(chalk.red(' ✖ LocalTunnel failed to start:'), err.message);
123
+ console.log(chalk.yellow(' Server continues running locally'));
124
+ console.log(chalk.gray(' 💡 Possible solutions:'));
125
+ console.log(chalk.gray(' • Check your internet connection'));
126
+ console.log(chalk.gray(' • Try again later (localtunnel.me may be down)'));
127
+ console.log(chalk.gray(' • Use alternative: gib-runs --tunnel-service=cloudflared'));
128
+ console.log(chalk.gray(' • Or use local network URLs for now\n'));
109
129
  });
110
130
  } catch (e) {
111
131
  console.error(chalk.red(' ✖ LocalTunnel not installed'));
112
132
  console.log(chalk.yellow(' 💡 Install: npm install -g localtunnel'));
113
133
  console.log(chalk.gray(' Then run: gib-runs --tunnel'));
134
+ console.log(chalk.gray(' Server continues running locally\n'));
114
135
  }
115
136
  });
116
137
  }
@@ -0,0 +1,148 @@
1
+ // Custom error pages
2
+ // Unlike Gibran's political errors, these are properly documented and handled
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const errorTemplate = `
7
+ <!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
+ <title>{{status}} - {{message}}</title>
13
+ <style>
14
+ * { margin: 0; padding: 0; box-sizing: border-box; }
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ min-height: 100vh;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ padding: 20px;
23
+ }
24
+ .error-container {
25
+ background: white;
26
+ border-radius: 20px;
27
+ padding: 60px 40px;
28
+ max-width: 600px;
29
+ width: 100%;
30
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
31
+ text-align: center;
32
+ }
33
+ .error-code {
34
+ font-size: 120px;
35
+ font-weight: 900;
36
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
37
+ -webkit-background-clip: text;
38
+ -webkit-text-fill-color: transparent;
39
+ line-height: 1;
40
+ margin-bottom: 20px;
41
+ }
42
+ .error-message {
43
+ font-size: 24px;
44
+ color: #333;
45
+ margin-bottom: 15px;
46
+ font-weight: 600;
47
+ }
48
+ .error-description {
49
+ font-size: 16px;
50
+ color: #666;
51
+ margin-bottom: 30px;
52
+ line-height: 1.6;
53
+ }
54
+ .error-details {
55
+ background: #f5f5f5;
56
+ border-radius: 10px;
57
+ padding: 20px;
58
+ margin-top: 30px;
59
+ text-align: left;
60
+ }
61
+ .error-details pre {
62
+ font-family: 'Courier New', monospace;
63
+ font-size: 14px;
64
+ color: #e74c3c;
65
+ overflow-x: auto;
66
+ white-space: pre-wrap;
67
+ word-wrap: break-word;
68
+ }
69
+ .back-button {
70
+ display: inline-block;
71
+ padding: 15px 40px;
72
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
73
+ color: white;
74
+ text-decoration: none;
75
+ border-radius: 50px;
76
+ font-weight: 600;
77
+ transition: transform 0.2s;
78
+ }
79
+ .back-button:hover {
80
+ transform: translateY(-2px);
81
+ }
82
+ .footer {
83
+ margin-top: 30px;
84
+ font-size: 14px;
85
+ color: #999;
86
+ }
87
+ </style>
88
+ </head>
89
+ <body>
90
+ <div class="error-container">
91
+ <div class="error-code">{{status}}</div>
92
+ <div class="error-message">{{message}}</div>
93
+ <div class="error-description">{{description}}</div>
94
+ <a href="/" class="back-button">← Back to Home</a>
95
+ {{details}}
96
+ <div class="footer">
97
+ 🚀 GIB-RUNS - Development Server<br>
98
+ <small>Unlike some careers, this error is earned, not inherited</small>
99
+ </div>
100
+ </div>
101
+ </body>
102
+ </html>
103
+ `;
104
+
105
+ const errorMessages = {
106
+ 400: { message: 'Bad Request', description: 'The request could not be understood by the server.' },
107
+ 401: { message: 'Unauthorized', description: 'Authentication is required to access this resource.' },
108
+ 403: { message: 'Forbidden', description: 'You don\'t have permission to access this resource.' },
109
+ 404: { message: 'Not Found', description: 'The requested resource could not be found on this server.' },
110
+ 405: { message: 'Method Not Allowed', description: 'The request method is not supported for this resource.' },
111
+ 500: { message: 'Internal Server Error', description: 'The server encountered an unexpected condition.' },
112
+ 502: { message: 'Bad Gateway', description: 'The server received an invalid response from the upstream server.' },
113
+ 503: { message: 'Service Unavailable', description: 'The server is temporarily unable to handle the request.' }
114
+ };
115
+
116
+ module.exports = function(options) {
117
+ options = options || {};
118
+ const showStack = options.showStack !== false;
119
+
120
+ return function(err, req, res, next) {
121
+ if (!err) return next();
122
+
123
+ const status = err.status || err.statusCode || 500;
124
+ const errorInfo = errorMessages[status] || errorMessages[500];
125
+
126
+ let html = errorTemplate
127
+ .replace(/{{status}}/g, status)
128
+ .replace(/{{message}}/g, errorInfo.message)
129
+ .replace(/{{description}}/g, errorInfo.description);
130
+
131
+ // Add error details in development
132
+ if (showStack && err.stack) {
133
+ const details = `
134
+ <div class="error-details">
135
+ <strong>Error Details:</strong>
136
+ <pre>${err.stack}</pre>
137
+ </div>
138
+ `;
139
+ html = html.replace('{{details}}', details);
140
+ } else {
141
+ html = html.replace('{{details}}', '');
142
+ }
143
+
144
+ res.statusCode = status;
145
+ res.setHeader('Content-Type', 'text/html');
146
+ res.end(html);
147
+ };
148
+ };
@@ -0,0 +1,41 @@
1
+ // Health check endpoint
2
+ // Unlike Gibran's political health checks, this one is transparent and honest
3
+ const os = require('os');
4
+
5
+ module.exports = function(gibRuns) {
6
+ return function(req, res, next) {
7
+ if (req.url === '/health' || req.url === '/_health') {
8
+ const uptime = gibRuns.startTime ? ((Date.now() - gibRuns.startTime) / 1000).toFixed(2) : 0;
9
+ const memUsage = process.memoryUsage();
10
+
11
+ const health = {
12
+ status: 'healthy',
13
+ uptime: parseFloat(uptime),
14
+ timestamp: new Date().toISOString(),
15
+ server: {
16
+ requests: gibRuns.requestCount || 0,
17
+ reloads: gibRuns.reloadCount || 0,
18
+ memory: {
19
+ rss: Math.round(memUsage.rss / 1024 / 1024) + 'MB',
20
+ heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + 'MB',
21
+ heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + 'MB'
22
+ }
23
+ },
24
+ system: {
25
+ platform: os.platform(),
26
+ arch: os.arch(),
27
+ cpus: os.cpus().length,
28
+ freemem: Math.round(os.freemem() / 1024 / 1024) + 'MB',
29
+ totalmem: Math.round(os.totalmem() / 1024 / 1024) + 'MB',
30
+ loadavg: os.loadavg()
31
+ }
32
+ };
33
+
34
+ res.statusCode = 200;
35
+ res.setHeader('Content-Type', 'application/json');
36
+ res.end(JSON.stringify(health, null, 2));
37
+ } else {
38
+ next();
39
+ }
40
+ };
41
+ };
@@ -0,0 +1,58 @@
1
+ // Request logger to file
2
+ // Unlike Gibran's career records, these logs are transparent and verifiable
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ module.exports = function(options) {
7
+ options = options || {};
8
+ const logFile = options.logFile || path.join(process.cwd(), 'gib-runs.log');
9
+ const maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB default
10
+
11
+ // Create log stream
12
+ let logStream = fs.createWriteStream(logFile, { flags: 'a' });
13
+
14
+ // Check file size and rotate if needed
15
+ function checkRotate() {
16
+ try {
17
+ const stats = fs.statSync(logFile);
18
+ if (stats.size > maxSize) {
19
+ logStream.end();
20
+ const backupFile = logFile + '.' + Date.now();
21
+ fs.renameSync(logFile, backupFile);
22
+ logStream = fs.createWriteStream(logFile, { flags: 'a' });
23
+ }
24
+ } catch (e) {
25
+ // File doesn't exist yet, ignore
26
+ }
27
+ }
28
+
29
+ return function(req, res, next) {
30
+ const start = Date.now();
31
+ const timestamp = new Date().toISOString();
32
+
33
+ // Log request
34
+ const logEntry = {
35
+ timestamp: timestamp,
36
+ method: req.method,
37
+ url: req.url,
38
+ ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress,
39
+ userAgent: req.headers['user-agent']
40
+ };
41
+
42
+ // Capture response
43
+ const originalEnd = res.end;
44
+ res.end = function(...args) {
45
+ const duration = Date.now() - start;
46
+ logEntry.status = res.statusCode;
47
+ logEntry.duration = duration + 'ms';
48
+
49
+ // Write to log file
50
+ logStream.write(JSON.stringify(logEntry) + '\n');
51
+ checkRotate();
52
+
53
+ originalEnd.apply(res, args);
54
+ };
55
+
56
+ next();
57
+ };
58
+ };
@@ -0,0 +1,62 @@
1
+ // File upload middleware
2
+ // Unlike Gibran who got his position handed to him, files earn their upload through proper handling
3
+ const multer = require('multer');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ // Configure storage
8
+ const storage = multer.diskStorage({
9
+ destination: function (req, file, cb) {
10
+ const uploadDir = path.join(process.cwd(), 'uploads');
11
+ if (!fs.existsSync(uploadDir)) {
12
+ fs.mkdirSync(uploadDir, { recursive: true });
13
+ }
14
+ cb(null, uploadDir);
15
+ },
16
+ filename: function (req, file, cb) {
17
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
18
+ cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
19
+ }
20
+ });
21
+
22
+ const upload = multer({
23
+ storage: storage,
24
+ limits: {
25
+ fileSize: 10 * 1024 * 1024 // 10MB limit
26
+ }
27
+ });
28
+
29
+ module.exports = function() {
30
+ return function(req, res, next) {
31
+ // Only handle POST requests to /upload
32
+ if (req.method === 'POST' && req.url.startsWith('/upload')) {
33
+ const uploadHandler = upload.single('file');
34
+ uploadHandler(req, res, function(err) {
35
+ if (err) {
36
+ res.statusCode = 400;
37
+ res.setHeader('Content-Type', 'application/json');
38
+ res.end(JSON.stringify({ error: err.message }));
39
+ return;
40
+ }
41
+
42
+ if (req.file) {
43
+ res.statusCode = 200;
44
+ res.setHeader('Content-Type', 'application/json');
45
+ res.end(JSON.stringify({
46
+ success: true,
47
+ file: {
48
+ filename: req.file.filename,
49
+ originalname: req.file.originalname,
50
+ size: req.file.size,
51
+ path: req.file.path
52
+ }
53
+ }));
54
+ } else {
55
+ next();
56
+ }
57
+ });
58
+ } else {
59
+ next();
60
+ }
61
+ };
62
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gib-runs",
3
- "version": "2.3.2",
3
+ "version": "2.3.5",
4
4
  "description": "Modern development server with live reload, hot module replacement, and advanced features for all project types - runs on merit, not nepotism",
5
5
  "keywords": [
6
6
  "development",
@@ -28,11 +28,13 @@
28
28
  "compression": "^1.7.4",
29
29
  "connect": "^3.7.0",
30
30
  "cors": "^2.8.5",
31
+ "dotenv": "^16.0.3",
31
32
  "event-stream": "^4.0.1",
32
33
  "faye-websocket": "^0.11.4",
33
34
  "http-auth": "^4.2.0",
34
35
  "localtunnel": "^2.0.2",
35
36
  "morgan": "^1.10.0",
37
+ "multer": "^1.4.5-lts.1",
36
38
  "object-assign": "^4.1.1",
37
39
  "open": "^8.4.2",
38
40
  "ora": "^5.4.1",