gib-runs 2.3.2 → 2.3.4
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 +55 -0
- package/README.md +283 -4
- package/gib-run.js +29 -2
- package/index.js +89 -4
- 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,61 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.3.4] - 2026-02-12
|
|
6
|
+
|
|
7
|
+
### Added - New Features 🎉
|
|
8
|
+
- 🔁 **Auto-Restart on Crash** - Server automatically restarts on unexpected errors
|
|
9
|
+
- Use `--auto-restart` flag to enable
|
|
10
|
+
- Attempts up to 5 restarts before giving up
|
|
11
|
+
- Resilient mode for production-like development
|
|
12
|
+
- Displays restart attempt count in console
|
|
13
|
+
- 📤 **File Upload Endpoint** - Built-in file upload support for development
|
|
14
|
+
- Use `--enable-upload` flag to enable
|
|
15
|
+
- POST files to `/upload` endpoint
|
|
16
|
+
- 10MB file size limit
|
|
17
|
+
- Files saved to `./uploads` directory
|
|
18
|
+
- Returns JSON response with file details
|
|
19
|
+
- 💚 **Health Check Endpoint** - Monitor server health and statistics
|
|
20
|
+
- Enabled by default (use `--no-health` to disable)
|
|
21
|
+
- Access via `GET /health` or `GET /_health`
|
|
22
|
+
- Returns JSON with uptime, memory usage, request count, reload count
|
|
23
|
+
- System information (CPU, memory, platform)
|
|
24
|
+
- Perfect for monitoring and debugging
|
|
25
|
+
- 📝 **Request Logging to File** - Log all requests to file for debugging
|
|
26
|
+
- Use `--log-to-file` flag to enable
|
|
27
|
+
- Logs saved to `gib-runs.log` in project root
|
|
28
|
+
- JSON format with timestamp, method, URL, IP, user-agent, status, duration
|
|
29
|
+
- Automatic log rotation at 10MB
|
|
30
|
+
- Old logs backed up with timestamp
|
|
31
|
+
- 🎨 **Custom Error Pages** - Beautiful, informative error pages
|
|
32
|
+
- Enabled by default (use `--no-error-page` to disable)
|
|
33
|
+
- Modern gradient design with detailed error information
|
|
34
|
+
- Shows error stack trace in development mode
|
|
35
|
+
- Covers all HTTP error codes (400, 401, 403, 404, 500, etc)
|
|
36
|
+
- Responsive design for mobile devices
|
|
37
|
+
- 🌍 **Environment Variable Support** - Automatic .env file loading
|
|
38
|
+
- Automatically loads `.env` file from project root
|
|
39
|
+
- Uses dotenv package
|
|
40
|
+
- No configuration needed, just create `.env` file
|
|
41
|
+
- Perfect for API keys, database URLs, etc
|
|
42
|
+
- 📡 **WebSocket Broadcasting API** - Send custom messages to all connected clients
|
|
43
|
+
- New `GibRuns.broadcast(message)` method
|
|
44
|
+
- Broadcast custom reload triggers or notifications
|
|
45
|
+
- Useful for custom build tools and integrations
|
|
46
|
+
|
|
47
|
+
### Improved
|
|
48
|
+
- 🔧 **Better Error Handling** - More informative error messages with stack traces
|
|
49
|
+
- 📊 **Enhanced Health Monitoring** - More detailed system metrics
|
|
50
|
+
- 🎯 **Middleware Architecture** - Cleaner middleware loading and organization
|
|
51
|
+
- 📦 **Dependencies** - Added `dotenv` and `multer` for new features
|
|
52
|
+
|
|
53
|
+
### Technical
|
|
54
|
+
- Version bumped to 2.3.4
|
|
55
|
+
- All existing tests passing
|
|
56
|
+
- Backward compatible with all previous versions
|
|
57
|
+
- New middleware files: `upload.js`, `health.js`, `logger.js`, `error-page.js`
|
|
58
|
+
- Enhanced GibRuns object with `wsClients`, `autoRestart`, `restartCount` properties
|
|
59
|
+
|
|
5
60
|
## [2.3.0] - 2026-02-10
|
|
6
61
|
|
|
7
62
|
### 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.4
|
|
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,15 @@ 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.4 🎉
|
|
65
|
+
- 🔁 **Auto-Restart on Crash** - Automatically restart server on unexpected errors (resilient mode)
|
|
66
|
+
- 📤 **File Upload Endpoint** - Built-in file upload support for development (POST to /upload)
|
|
67
|
+
- 💚 **Health Check Endpoint** - Monitor server health and statistics (GET /health)
|
|
68
|
+
- 📝 **Request Logging to File** - Log all requests to file for debugging (gib-runs.log)
|
|
69
|
+
- 🎨 **Custom Error Pages** - Beautiful, informative error pages with stack traces
|
|
70
|
+
- 🌍 **Environment Variables** - Automatic .env file loading (dotenv support)
|
|
71
|
+
- 📡 **WebSocket Broadcasting** - Send custom messages to all connected clients
|
|
72
|
+
|
|
64
73
|
## 📦 Installation
|
|
65
74
|
|
|
66
75
|
### Global Installation (Recommended)
|
|
@@ -155,6 +164,11 @@ gib-runs dist --port=3000 --spa --cors --no-browser
|
|
|
155
164
|
| `--npm-script=SCRIPT` | Run npm script (dev, start, etc) | None |
|
|
156
165
|
| `--pm2` | Use PM2 process manager | `false` |
|
|
157
166
|
| `--pm2-name=NAME` | PM2 app name | `gib-runs-app` |
|
|
167
|
+
| `--auto-restart` | Auto-restart server on crash | `false` |
|
|
168
|
+
| `--enable-upload` | Enable file upload endpoint | `false` |
|
|
169
|
+
| `--no-health` | Disable health check endpoint | `false` |
|
|
170
|
+
| `--log-to-file` | Log requests to file | `false` |
|
|
171
|
+
| `--no-error-page` | Disable custom error pages | `false` |
|
|
158
172
|
| `-v, --version` | Show version | - |
|
|
159
173
|
| `-h, --help` | Show help | - |
|
|
160
174
|
|
|
@@ -372,7 +386,7 @@ gib-runs
|
|
|
372
386
|
Network URLs are **ALWAYS shown automatically** when you start the server:
|
|
373
387
|
|
|
374
388
|
```
|
|
375
|
-
🚀 GIB-RUNS v2.3.
|
|
389
|
+
🚀 GIB-RUNS v2.3.4
|
|
376
390
|
"Unlike Gibran, this actually works through merit"
|
|
377
391
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
378
392
|
📁 Root: /home/user/project
|
|
@@ -491,7 +505,7 @@ gib-runs --tunnel-service=tunnelto
|
|
|
491
505
|
### Example Output
|
|
492
506
|
|
|
493
507
|
```
|
|
494
|
-
🚀 GIB-RUNS v2.3.
|
|
508
|
+
🚀 GIB-RUNS v2.3.4
|
|
495
509
|
"Unlike Gibran, this actually works through merit"
|
|
496
510
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
497
511
|
📁 Root: /home/user/project
|
|
@@ -621,7 +635,7 @@ pm2 list
|
|
|
621
635
|
### Example Output
|
|
622
636
|
|
|
623
637
|
```
|
|
624
|
-
🚀 GIB-RUNS v2.3.
|
|
638
|
+
🚀 GIB-RUNS v2.3.4
|
|
625
639
|
"Unlike Gibran, this actually works through merit"
|
|
626
640
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
627
641
|
📁 Root: /home/user/project
|
|
@@ -648,6 +662,271 @@ pm2 list
|
|
|
648
662
|
|
|
649
663
|
**Unlike Gibran's career, these processes run on actual merit and capability!** 🔥
|
|
650
664
|
|
|
665
|
+
## 🆕 New Features in v2.3.4
|
|
666
|
+
|
|
667
|
+
### Auto-Restart on Crash
|
|
668
|
+
|
|
669
|
+
**Automatically restart your server when it crashes - resilient mode for development!**
|
|
670
|
+
|
|
671
|
+
```bash
|
|
672
|
+
# Enable auto-restart
|
|
673
|
+
gib-runs --auto-restart
|
|
674
|
+
|
|
675
|
+
# With other options
|
|
676
|
+
gib-runs --auto-restart --port=3000 --spa
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**How it works:**
|
|
680
|
+
- Server automatically restarts on unexpected errors
|
|
681
|
+
- Attempts up to 5 restarts before giving up
|
|
682
|
+
- Shows restart attempt count in console
|
|
683
|
+
- Perfect for unstable development environments
|
|
684
|
+
- Keeps your workflow uninterrupted
|
|
685
|
+
|
|
686
|
+
**Example output:**
|
|
687
|
+
```
|
|
688
|
+
✖ Server Error: ECONNRESET
|
|
689
|
+
🔄 Auto-restarting server (attempt 1/5)...
|
|
690
|
+
✓ Server restarted successfully
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### File Upload Endpoint
|
|
694
|
+
|
|
695
|
+
**Built-in file upload support for development - no need for separate upload server!**
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
# Enable file upload endpoint
|
|
699
|
+
gib-runs --enable-upload
|
|
700
|
+
|
|
701
|
+
# Files will be saved to ./uploads directory
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
**Usage:**
|
|
705
|
+
```html
|
|
706
|
+
<!-- HTML Form -->
|
|
707
|
+
<form action="/upload" method="POST" enctype="multipart/form-data">
|
|
708
|
+
<input type="file" name="file">
|
|
709
|
+
<button type="submit">Upload</button>
|
|
710
|
+
</form>
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
```javascript
|
|
714
|
+
// JavaScript Fetch API
|
|
715
|
+
const formData = new FormData();
|
|
716
|
+
formData.append('file', fileInput.files[0]);
|
|
717
|
+
|
|
718
|
+
fetch('/upload', {
|
|
719
|
+
method: 'POST',
|
|
720
|
+
body: formData
|
|
721
|
+
})
|
|
722
|
+
.then(res => res.json())
|
|
723
|
+
.then(data => {
|
|
724
|
+
console.log('Uploaded:', data.file);
|
|
725
|
+
// { filename: 'file-123456789.jpg', originalname: 'photo.jpg', size: 12345, path: '/path/to/uploads/...' }
|
|
726
|
+
});
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**Features:**
|
|
730
|
+
- 10MB file size limit
|
|
731
|
+
- Files saved to `./uploads` directory
|
|
732
|
+
- Automatic directory creation
|
|
733
|
+
- Unique filenames with timestamp
|
|
734
|
+
- JSON response with file details
|
|
735
|
+
- Error handling for invalid uploads
|
|
736
|
+
|
|
737
|
+
### Health Check Endpoint
|
|
738
|
+
|
|
739
|
+
**Monitor your server health and statistics - transparency in action!**
|
|
740
|
+
|
|
741
|
+
```bash
|
|
742
|
+
# Health check is enabled by default
|
|
743
|
+
gib-runs
|
|
744
|
+
|
|
745
|
+
# Disable if needed
|
|
746
|
+
gib-runs --no-health
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**Access health endpoint:**
|
|
750
|
+
```bash
|
|
751
|
+
# Via curl
|
|
752
|
+
curl http://localhost:8080/health
|
|
753
|
+
|
|
754
|
+
# Or in browser
|
|
755
|
+
http://localhost:8080/health
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**Response example:**
|
|
759
|
+
```json
|
|
760
|
+
{
|
|
761
|
+
"status": "healthy",
|
|
762
|
+
"uptime": 123.45,
|
|
763
|
+
"timestamp": "2026-02-12T09:00:00.000Z",
|
|
764
|
+
"server": {
|
|
765
|
+
"requests": 42,
|
|
766
|
+
"reloads": 5,
|
|
767
|
+
"memory": {
|
|
768
|
+
"rss": "45MB",
|
|
769
|
+
"heapUsed": "23MB",
|
|
770
|
+
"heapTotal": "35MB"
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
"system": {
|
|
774
|
+
"platform": "linux",
|
|
775
|
+
"arch": "x64",
|
|
776
|
+
"cpus": 8,
|
|
777
|
+
"freemem": "2048MB",
|
|
778
|
+
"totalmem": "16384MB",
|
|
779
|
+
"loadavg": [1.2, 1.5, 1.8]
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
**Use cases:**
|
|
785
|
+
- Monitor server performance
|
|
786
|
+
- Debug memory leaks
|
|
787
|
+
- Track request patterns
|
|
788
|
+
- Integration with monitoring tools
|
|
789
|
+
- Health checks for Docker containers
|
|
790
|
+
|
|
791
|
+
### Request Logging to File
|
|
792
|
+
|
|
793
|
+
**Log all requests to file for debugging - transparent and verifiable!**
|
|
794
|
+
|
|
795
|
+
```bash
|
|
796
|
+
# Enable file logging
|
|
797
|
+
gib-runs --log-to-file
|
|
798
|
+
|
|
799
|
+
# Logs saved to gib-runs.log in project root
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
**Log format (JSON):**
|
|
803
|
+
```json
|
|
804
|
+
{"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"}
|
|
805
|
+
{"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"}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
**Features:**
|
|
809
|
+
- JSON format for easy parsing
|
|
810
|
+
- Includes timestamp, method, URL, IP, user-agent, status, duration
|
|
811
|
+
- Automatic log rotation at 10MB
|
|
812
|
+
- Old logs backed up with timestamp
|
|
813
|
+
- Perfect for debugging and analytics
|
|
814
|
+
|
|
815
|
+
**Parse logs with jq:**
|
|
816
|
+
```bash
|
|
817
|
+
# Show all 404 errors
|
|
818
|
+
cat gib-runs.log | jq 'select(.status == 404)'
|
|
819
|
+
|
|
820
|
+
# Show slow requests (>100ms)
|
|
821
|
+
cat gib-runs.log | jq 'select(.duration | tonumber > 100)'
|
|
822
|
+
|
|
823
|
+
# Count requests by URL
|
|
824
|
+
cat gib-runs.log | jq -r '.url' | sort | uniq -c
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### Custom Error Pages
|
|
828
|
+
|
|
829
|
+
**Beautiful, informative error pages - unlike some political errors!**
|
|
830
|
+
|
|
831
|
+
```bash
|
|
832
|
+
# Custom error pages enabled by default
|
|
833
|
+
gib-runs
|
|
834
|
+
|
|
835
|
+
# Disable if needed
|
|
836
|
+
gib-runs --no-error-page
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
**Features:**
|
|
840
|
+
- Modern gradient design
|
|
841
|
+
- Detailed error information
|
|
842
|
+
- Shows error stack trace in development mode
|
|
843
|
+
- Covers all HTTP error codes (400, 401, 403, 404, 500, etc)
|
|
844
|
+
- Responsive design for mobile devices
|
|
845
|
+
- "Back to Home" button
|
|
846
|
+
- Professional appearance
|
|
847
|
+
|
|
848
|
+
**Error codes covered:**
|
|
849
|
+
- 400 Bad Request
|
|
850
|
+
- 401 Unauthorized
|
|
851
|
+
- 403 Forbidden
|
|
852
|
+
- 404 Not Found
|
|
853
|
+
- 405 Method Not Allowed
|
|
854
|
+
- 500 Internal Server Error
|
|
855
|
+
- 502 Bad Gateway
|
|
856
|
+
- 503 Service Unavailable
|
|
857
|
+
|
|
858
|
+
### Environment Variables
|
|
859
|
+
|
|
860
|
+
**Automatic .env file loading - no configuration needed!**
|
|
861
|
+
|
|
862
|
+
```bash
|
|
863
|
+
# Just create .env file in project root
|
|
864
|
+
echo "API_KEY=your-secret-key" > .env
|
|
865
|
+
echo "DATABASE_URL=postgres://localhost/mydb" >> .env
|
|
866
|
+
|
|
867
|
+
# Start server (automatically loads .env)
|
|
868
|
+
gib-runs
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Access in your code:**
|
|
872
|
+
```javascript
|
|
873
|
+
// Node.js
|
|
874
|
+
const apiKey = process.env.API_KEY;
|
|
875
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
876
|
+
|
|
877
|
+
console.log('API Key:', apiKey);
|
|
878
|
+
console.log('Database:', dbUrl);
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
**Features:**
|
|
882
|
+
- Automatic loading on server start
|
|
883
|
+
- No configuration needed
|
|
884
|
+
- Uses dotenv package
|
|
885
|
+
- Perfect for API keys, database URLs, etc
|
|
886
|
+
- Keeps secrets out of version control
|
|
887
|
+
|
|
888
|
+
### WebSocket Broadcasting API
|
|
889
|
+
|
|
890
|
+
**Send custom messages to all connected clients - programmatic control!**
|
|
891
|
+
|
|
892
|
+
```javascript
|
|
893
|
+
const gibRuns = require('gib-runs');
|
|
894
|
+
|
|
895
|
+
// Start server
|
|
896
|
+
const server = gibRuns.start({
|
|
897
|
+
port: 8080,
|
|
898
|
+
root: './public'
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Broadcast custom message to all clients
|
|
902
|
+
gibRuns.broadcast('custom-reload');
|
|
903
|
+
|
|
904
|
+
// Trigger reload from your build script
|
|
905
|
+
gibRuns.broadcast('reload');
|
|
906
|
+
|
|
907
|
+
// Send custom data
|
|
908
|
+
gibRuns.broadcast(JSON.stringify({ type: 'notification', message: 'Build complete!' }));
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
**Use cases:**
|
|
912
|
+
- Custom build tool integration
|
|
913
|
+
- Trigger reload from external scripts
|
|
914
|
+
- Send notifications to browser
|
|
915
|
+
- Custom live reload logic
|
|
916
|
+
- Integration with CI/CD pipelines
|
|
917
|
+
|
|
918
|
+
**Client-side handling:**
|
|
919
|
+
```javascript
|
|
920
|
+
// In your HTML/JavaScript
|
|
921
|
+
const ws = new WebSocket('ws://localhost:8080');
|
|
922
|
+
ws.onmessage = function(event) {
|
|
923
|
+
if (event.data === 'custom-reload') {
|
|
924
|
+
console.log('Custom reload triggered!');
|
|
925
|
+
location.reload();
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
```
|
|
929
|
+
|
|
651
930
|
## 🐛 Troubleshooting
|
|
652
931
|
|
|
653
932
|
### 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){
|
|
@@ -136,6 +147,11 @@ function entryPoint(staticHandler, file) {
|
|
|
136
147
|
* @param compression {boolean} Enable gzip compression (default: true)
|
|
137
148
|
* @param qrCode {boolean} Show QR code for network URLs (default: false)
|
|
138
149
|
* @param tunnel {boolean} Create public tunnel URL (default: false)
|
|
150
|
+
* @param autoRestart {boolean} Auto-restart server on crash (default: false)
|
|
151
|
+
* @param enableUpload {boolean} Enable file upload endpoint (default: false)
|
|
152
|
+
* @param enableHealth {boolean} Enable health check endpoint (default: true)
|
|
153
|
+
* @param logToFile {boolean} Log requests to file (default: false)
|
|
154
|
+
* @param customErrorPage {boolean} Use custom error pages (default: true)
|
|
139
155
|
*/
|
|
140
156
|
GibRuns.start = function(options) {
|
|
141
157
|
options = options || {};
|
|
@@ -177,6 +193,13 @@ GibRuns.start = function(options) {
|
|
|
177
193
|
var usePM2 = options.pm2 || false;
|
|
178
194
|
var pm2Name = options.pm2Name || 'gib-runs-app';
|
|
179
195
|
var testMode = options.test || false;
|
|
196
|
+
var autoRestart = options.autoRestart || false;
|
|
197
|
+
var enableUpload = options.enableUpload || false;
|
|
198
|
+
var enableHealth = options.enableHealth !== false;
|
|
199
|
+
var logToFile = options.logToFile || false;
|
|
200
|
+
var customErrorPage = options.customErrorPage !== false;
|
|
201
|
+
|
|
202
|
+
GibRuns.autoRestart = autoRestart;
|
|
180
203
|
|
|
181
204
|
if (httpsModule) {
|
|
182
205
|
try {
|
|
@@ -217,6 +240,27 @@ GibRuns.start = function(options) {
|
|
|
217
240
|
|
|
218
241
|
next();
|
|
219
242
|
});
|
|
243
|
+
|
|
244
|
+
// Add health check endpoint
|
|
245
|
+
if (enableHealth) {
|
|
246
|
+
app.use(require('./middleware/health')(GibRuns));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Add file upload endpoint
|
|
250
|
+
if (enableUpload) {
|
|
251
|
+
app.use(require('./middleware/upload')());
|
|
252
|
+
if (GibRuns.logLevel >= 1) {
|
|
253
|
+
console.log(chalk.cyan(' 📤 File Upload: ') + chalk.green('Enabled') + chalk.gray(' (POST to /upload)'));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Add request logger to file
|
|
258
|
+
if (logToFile) {
|
|
259
|
+
app.use(require('./middleware/logger')({ logFile: path.join(root, 'gib-runs.log') }));
|
|
260
|
+
if (GibRuns.logLevel >= 1) {
|
|
261
|
+
console.log(chalk.cyan(' 📝 File Logging: ') + chalk.green('Enabled') + chalk.gray(' (gib-runs.log)'));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
220
264
|
|
|
221
265
|
// Add logger. Level 2 logs only errors
|
|
222
266
|
if (GibRuns.logLevel === 2) {
|
|
@@ -298,6 +342,11 @@ GibRuns.start = function(options) {
|
|
|
298
342
|
app.use(staticServerHandler) // Custom static server
|
|
299
343
|
.use(entryPoint(staticServerHandler, file))
|
|
300
344
|
.use(serveIndex(root, { icons: true }));
|
|
345
|
+
|
|
346
|
+
// Add custom error page handler (must be last)
|
|
347
|
+
if (customErrorPage) {
|
|
348
|
+
app.use(require('./middleware/error-page')({ showStack: GibRuns.logLevel >= 2 }));
|
|
349
|
+
}
|
|
301
350
|
}
|
|
302
351
|
|
|
303
352
|
var server, protocol;
|
|
@@ -338,7 +387,17 @@ GibRuns.start = function(options) {
|
|
|
338
387
|
if (GibRuns.logLevel >= 3) {
|
|
339
388
|
console.error(chalk.gray(' Stack trace:'), e.stack);
|
|
340
389
|
}
|
|
341
|
-
|
|
390
|
+
|
|
391
|
+
// Auto-restart on crash if enabled
|
|
392
|
+
if (autoRestart && GibRuns.restartCount < 5) {
|
|
393
|
+
GibRuns.restartCount++;
|
|
394
|
+
console.log(chalk.yellow(' 🔄 Auto-restarting server (attempt ' + GibRuns.restartCount + '/5)...'));
|
|
395
|
+
setTimeout(function() {
|
|
396
|
+
GibRuns.start(options);
|
|
397
|
+
}, 2000);
|
|
398
|
+
} else {
|
|
399
|
+
GibRuns.shutdown();
|
|
400
|
+
}
|
|
342
401
|
}
|
|
343
402
|
});
|
|
344
403
|
|
|
@@ -353,7 +412,7 @@ GibRuns.start = function(options) {
|
|
|
353
412
|
// Show info about what's running
|
|
354
413
|
if (GibRuns.logLevel >= 1) {
|
|
355
414
|
console.log('\n' + chalk.cyan.bold('━'.repeat(60)));
|
|
356
|
-
console.log(chalk.cyan.bold(' 🚀 GIB-RUNS') + chalk.gray('
|
|
415
|
+
console.log(chalk.cyan.bold(' 🚀 GIB-RUNS') + chalk.gray(' v' + packageJson.version));
|
|
357
416
|
console.log(chalk.gray(' "Unlike Gibran, this actually works through merit"'));
|
|
358
417
|
console.log(chalk.cyan.bold('━'.repeat(60)));
|
|
359
418
|
console.log(chalk.white(' 📁 Root: ') + chalk.yellow(root));
|
|
@@ -367,6 +426,9 @@ GibRuns.start = function(options) {
|
|
|
367
426
|
console.log(chalk.white(' 🔄 PM2: ') + chalk.green(' Enabled') + chalk.gray(' (process manager)'));
|
|
368
427
|
}
|
|
369
428
|
console.log(chalk.white(' 🔄 Live Reload:') + chalk.green(' Enabled') + chalk.gray(' (watching for changes)'));
|
|
429
|
+
if (autoRestart) {
|
|
430
|
+
console.log(chalk.white(' 🔁 Auto-Restart:') + chalk.green(' Enabled') + chalk.gray(' (resilient mode)'));
|
|
431
|
+
}
|
|
370
432
|
console.log(chalk.cyan.bold('━'.repeat(60)));
|
|
371
433
|
console.log(chalk.gray(' Press Ctrl+C to stop\n'));
|
|
372
434
|
}
|
|
@@ -443,7 +505,7 @@ GibRuns.start = function(options) {
|
|
|
443
505
|
// Output with beautiful formatting
|
|
444
506
|
if (GibRuns.logLevel >= 1) {
|
|
445
507
|
console.log('\n' + chalk.cyan.bold('━'.repeat(60)));
|
|
446
|
-
console.log(chalk.cyan.bold(' 🚀 GIB-RUNS') + chalk.gray('
|
|
508
|
+
console.log(chalk.cyan.bold(' 🚀 GIB-RUNS') + chalk.gray(' v' + packageJson.version));
|
|
447
509
|
console.log(chalk.gray(' "Unlike Gibran, this actually works through merit"'));
|
|
448
510
|
console.log(chalk.cyan.bold('━'.repeat(60)));
|
|
449
511
|
console.log(chalk.white(' 📁 Root: ') + chalk.yellow(root));
|
|
@@ -466,6 +528,12 @@ GibRuns.start = function(options) {
|
|
|
466
528
|
if (https) {
|
|
467
529
|
console.log(chalk.white(' 🔒 HTTPS: ') + chalk.green(' Enabled') + chalk.gray(' (real security)'));
|
|
468
530
|
}
|
|
531
|
+
if (enableHealth) {
|
|
532
|
+
console.log(chalk.white(' 💚 Health: ') + chalk.green(' Enabled') + chalk.gray(' (GET /health)'));
|
|
533
|
+
}
|
|
534
|
+
if (autoRestart) {
|
|
535
|
+
console.log(chalk.white(' 🔁 Auto-Restart:') + chalk.green(' Enabled') + chalk.gray(' (resilient mode)'));
|
|
536
|
+
}
|
|
469
537
|
console.log(chalk.cyan.bold('━'.repeat(60)));
|
|
470
538
|
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
471
539
|
console.log(chalk.yellow(' 💡 Tip: Share network URLs with your team!\n'));
|
|
@@ -540,6 +608,7 @@ GibRuns.start = function(options) {
|
|
|
540
608
|
};
|
|
541
609
|
|
|
542
610
|
clients.push(ws);
|
|
611
|
+
GibRuns.wsClients = clients;
|
|
543
612
|
});
|
|
544
613
|
|
|
545
614
|
var ignored = [
|
|
@@ -599,6 +668,22 @@ GibRuns.start = function(options) {
|
|
|
599
668
|
return server;
|
|
600
669
|
};
|
|
601
670
|
|
|
671
|
+
/**
|
|
672
|
+
* Broadcast custom message to all connected WebSocket clients
|
|
673
|
+
* @param {string} message - Message to broadcast
|
|
674
|
+
*/
|
|
675
|
+
GibRuns.broadcast = function(message) {
|
|
676
|
+
if (GibRuns.wsClients && GibRuns.wsClients.length > 0) {
|
|
677
|
+
GibRuns.wsClients.forEach(function(ws) {
|
|
678
|
+
if (ws && ws.send) {
|
|
679
|
+
ws.send(message);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
685
|
+
};
|
|
686
|
+
|
|
602
687
|
GibRuns.shutdown = function() {
|
|
603
688
|
if (GibRuns.logLevel >= 1 && GibRuns.startTime) {
|
|
604
689
|
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.4",
|
|
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",
|