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 +63 -0
- package/README.md +286 -4
- package/gib-run.js +29 -2
- package/index.js +101 -5
- package/lib/tunnel.js +23 -2
- package/middleware/error-page.js +148 -0
- package/middleware/health.js +41 -0
- package/middleware/logger.js +58 -0
- package/middleware/upload.js +62 -0
- package/package.json +3 -1
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
|
[](https://github.com/levouinse/gib-runs/blob/main/LICENSE)
|
|
4
4
|
[](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.
|
|
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.
|
|
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.
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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
|
|
108
|
-
console.log(chalk.yellow('
|
|
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.
|
|
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",
|