bs9 1.4.0 β 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -5
- package/bin/bs9 +26 -0
- package/dist/bs9-bbkpq14n. +247 -0
- package/dist/bs9.js +1 -1
- package/package.json +1 -1
- package/src/commands/deploy.ts +248 -0
- package/src/commands/status.ts +168 -19
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# BS9 (Bun Sentinel 9) π
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://github.com/xarhang/bs9)
|
|
5
5
|
[](SECURITY.md)
|
|
6
6
|
[](PRODUCTION.md)
|
|
7
7
|
[](https://github.com/bs9/bs9)
|
|
@@ -34,7 +34,7 @@ chmod +x ~/.local/bin/bs9
|
|
|
34
34
|
#### π§ Linux
|
|
35
35
|
- **Service Manager**: Systemd (user-mode)
|
|
36
36
|
- **Features**: Advanced security hardening, resource limits, sandboxing
|
|
37
|
-
- **Commands**: All
|
|
37
|
+
- **Commands**: All 22 commands available
|
|
38
38
|
|
|
39
39
|
#### π macOS
|
|
40
40
|
- **Service Manager**: Launchd
|
|
@@ -55,6 +55,31 @@ bs9 macos create --name my-app --file app.js # macOS
|
|
|
55
55
|
bs9 windows create --name my-app --file app.js # Windows
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
### π Killer Feature: Zero-Config Deployment
|
|
59
|
+
|
|
60
|
+
**Why choose BS9 over PM2 or systemd?**
|
|
61
|
+
**One command does everything:**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Deploy with production-ready setup
|
|
65
|
+
bs9 deploy app.ts --name my-api --port 8080 --env NODE_ENV=production
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**What `bs9 deploy` does automatically:**
|
|
69
|
+
- β
Creates systemd service with security hardening
|
|
70
|
+
- β
Enables user services persistence (linger)
|
|
71
|
+
- β
Sets up health checks (`/healthz`, `/metrics`)
|
|
72
|
+
- β
Enables OpenTelemetry and Prometheus metrics
|
|
73
|
+
- β
Configures smart restart policies
|
|
74
|
+
- β
Performs health validation
|
|
75
|
+
- β
Shows management commands and access URLs
|
|
76
|
+
|
|
77
|
+
**Hot reload capabilities:**
|
|
78
|
+
```bash
|
|
79
|
+
# Update configuration without downtime
|
|
80
|
+
bs9 deploy app.ts --reload --env NEW_CONFIG=value
|
|
81
|
+
```
|
|
82
|
+
|
|
58
83
|
## π Complete CLI Commands
|
|
59
84
|
|
|
60
85
|
### Service Management
|
|
@@ -65,11 +90,44 @@ bs9 start app.js --host 0.0.0.0 --port 8080 # Custom host and port
|
|
|
65
90
|
bs9 start app.js --host 192.168.1.100 --https # Custom host with HTTPS
|
|
66
91
|
bs9 start app.ts --build --name myapp --port 8080 --env NODE_ENV=production --host 0.0.0.0 --https
|
|
67
92
|
|
|
68
|
-
#
|
|
93
|
+
# Stop service
|
|
69
94
|
bs9 stop myapp
|
|
95
|
+
|
|
96
|
+
# Restart service
|
|
70
97
|
bs9 restart myapp
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
|
|
99
|
+
# Enhanced status display with visual indicators
|
|
100
|
+
bs9 status # Show all services
|
|
101
|
+
bs9 status myapp # Show specific service
|
|
102
|
+
|
|
103
|
+
# View logs
|
|
104
|
+
bs9 logs myapp # Show logs
|
|
105
|
+
bs9 logs myapp --follow # Follow logs
|
|
106
|
+
bs9 logs myapp --tail 50 # Show last 50 lines
|
|
107
|
+
|
|
108
|
+
# Delete services
|
|
109
|
+
bs9 delete myapp # Delete specific service
|
|
110
|
+
bs9 delete myapp --remove # Delete and remove config files
|
|
111
|
+
bs9 delete --all # Delete all services
|
|
112
|
+
bs9 delete --all --force # Force delete all services
|
|
113
|
+
|
|
114
|
+
# Deploy applications (KILLER FEATURE)
|
|
115
|
+
bs9 deploy app.ts # Zero-config deployment
|
|
116
|
+
bs9 deploy app.ts --name my-api --port 8080 --env NODE_ENV=production
|
|
117
|
+
bs9 deploy app.ts --reload --env NEW_CONFIG=value # Hot reload with new config
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Backup & Recovery
|
|
121
|
+
```bash
|
|
122
|
+
# Save service configurations
|
|
123
|
+
bs9 save myapp # Save specific service
|
|
124
|
+
bs9 save --all # Save all services
|
|
125
|
+
bs9 save myapp --backup # Save with timestamped backup
|
|
126
|
+
|
|
127
|
+
# Restore services from backup
|
|
128
|
+
bs9 resurrect myapp # Restore specific service
|
|
129
|
+
bs9 resurrect --all # Restore all services
|
|
130
|
+
bs9 resurrect myapp --config custom.json # Restore with custom config
|
|
73
131
|
```
|
|
74
132
|
|
|
75
133
|
### Monitoring & Observability
|
|
@@ -107,6 +165,11 @@ bs9 delete --all # Delete all services
|
|
|
107
165
|
bs9 delete --all --force # Force delete all services
|
|
108
166
|
bs9 delete myapp --timeout 60 # Custom graceful shutdown timeout
|
|
109
167
|
|
|
168
|
+
# Deploy applications (KILLER FEATURE)
|
|
169
|
+
bs9 deploy app.ts # Zero-config deployment
|
|
170
|
+
bs9 deploy app.ts --name my-api --port 8080 --env NODE_ENV=production
|
|
171
|
+
bs9 deploy app.ts --reload --env NEW_CONFIG=value # Hot reload with new config
|
|
172
|
+
|
|
110
173
|
# Backup and restore
|
|
111
174
|
bs9 save myapp # Save service configuration
|
|
112
175
|
bs9 save --all # Save all services
|
|
@@ -119,12 +182,33 @@ bs9 resurrect --all # Restore all services
|
|
|
119
182
|
|
|
120
183
|
## π― Key Features
|
|
121
184
|
|
|
185
|
+
### π Zero-Config Deployment (KILLER FEATURE)
|
|
186
|
+
- **One-Command Setup**: `bs9 deploy app.ts` does everything automatically
|
|
187
|
+
- **Production Ready**: Security hardening, health checks, metrics enabled
|
|
188
|
+
- **Hot Reload**: Update configurations without downtime
|
|
189
|
+
- **Port Detection**: Automatic service discovery and access URLs
|
|
190
|
+
- **Environment Management**: Easy environment variable updates
|
|
191
|
+
|
|
192
|
+
### π Enhanced Status Display
|
|
193
|
+
- **Visual Indicators**: β
πββ οΈβΈοΈ for instant health assessment
|
|
194
|
+
- **Perfect Alignment**: All columns properly aligned with accurate data
|
|
195
|
+
- **Detailed Metrics**: CPU, Memory, Uptime, Tasks, Port information
|
|
196
|
+
- **Troubleshooting Hints**: Actionable commands for common issues
|
|
197
|
+
- **Service Sections**: Running, Restarting, Failed services clearly separated
|
|
198
|
+
|
|
122
199
|
### π Real-time Monitoring
|
|
123
200
|
- **Terminal Dashboard**: Live terminal UI with color-coded status
|
|
124
201
|
- **Web Dashboard**: Browser-based monitoring with auto-refresh
|
|
125
202
|
- **Health Checks**: Automatic `/healthz`, `/readyz`, `/metrics` endpoints
|
|
126
203
|
- **SRE Metrics**: CPU, Memory, Uptime, Task tracking
|
|
127
204
|
|
|
205
|
+
### πΎ Backup & Recovery System
|
|
206
|
+
- **Service Configuration Backup**: Complete JSON-based backup system
|
|
207
|
+
- **Timestamped Backups**: Version control for service configurations
|
|
208
|
+
- **Bulk Operations**: Save and restore all services at once
|
|
209
|
+
- **Cross-Platform**: Works on Linux, macOS, Windows
|
|
210
|
+
- **Disaster Recovery**: Quick system restoration from backups
|
|
211
|
+
|
|
128
212
|
### π Historical Metrics Storage
|
|
129
213
|
- **Local Storage**: JSON-based metrics storage in `~/.config/bs9/metrics/`
|
|
130
214
|
- **Data Export**: JSON and CSV export formats
|
package/bin/bs9
CHANGED
|
@@ -23,6 +23,7 @@ import { profileCommand } from "../src/commands/profile.js";
|
|
|
23
23
|
import { deleteCommand } from "../src/commands/delete.js";
|
|
24
24
|
import { saveCommand } from "../src/commands/save.js";
|
|
25
25
|
import { resurrectCommand } from "../src/commands/resurrect.js";
|
|
26
|
+
import { deployCommand } from "../src/commands/deploy.js";
|
|
26
27
|
import { loadbalancerCommand } from "../src/loadbalancer/manager.js";
|
|
27
28
|
import { windowsCommand } from "../src/windows/service.js";
|
|
28
29
|
import { launchdCommand } from "../src/macos/launchd.js";
|
|
@@ -161,6 +162,31 @@ program
|
|
|
161
162
|
.option("-c, --config <file>", "Configuration file to use")
|
|
162
163
|
.action(resurrectCommand);
|
|
163
164
|
|
|
165
|
+
program
|
|
166
|
+
.command("deploy")
|
|
167
|
+
.description("π Deploy applications with zero-config production setup")
|
|
168
|
+
.argument("<file>", "Application file to deploy")
|
|
169
|
+
.option("-n, --name <name>", "Service name")
|
|
170
|
+
.option("-p, --port <port>", "Port number", "3000")
|
|
171
|
+
.option("-h, --host <host>", "Host address", "localhost")
|
|
172
|
+
.option("-e, --env <env>", "Environment variables (multiple)", (value, previous) => [...(previous || []), value])
|
|
173
|
+
.option("-i, --instances <number>", "Number of instances")
|
|
174
|
+
.option("--memory <memory>", "Memory limit")
|
|
175
|
+
.option("--cpu <cpu>", "CPU limit")
|
|
176
|
+
.option("--log-level <level>", "Log level")
|
|
177
|
+
.option("--log-file <file>", "Log file path")
|
|
178
|
+
.option("--restart <policy>", "Restart policy")
|
|
179
|
+
.option("--no-health", "Skip health check")
|
|
180
|
+
.option("--no-metrics", "Disable metrics")
|
|
181
|
+
.option("--no-otel", "Disable OpenTelemetry")
|
|
182
|
+
.option("--no-prometheus", "Disable Prometheus")
|
|
183
|
+
.option("--https", "Enable HTTPS")
|
|
184
|
+
.option("--no-linger", "Don't enable linger")
|
|
185
|
+
.option("-f, --force", "Force deployment")
|
|
186
|
+
.option("--reload", "Reload existing service")
|
|
187
|
+
.option("--build", "Build TypeScript")
|
|
188
|
+
.action(deployCommand);
|
|
189
|
+
|
|
164
190
|
program
|
|
165
191
|
.command("loadbalancer")
|
|
166
192
|
.description("Load balancer management")
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
|
|
7
|
+
// Read version from package.json
|
|
8
|
+
const packageJsonPath = join(dirname(import.meta.path), '..', 'package.json');
|
|
9
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
10
|
+
const version = packageJson.version;
|
|
11
|
+
|
|
12
|
+
import { startCommand } from "../src/commands/start.js";
|
|
13
|
+
import { stopCommand } from "../src/commands/stop.js";
|
|
14
|
+
import { restartCommand } from "../src/commands/restart.js";
|
|
15
|
+
import { statusCommand } from "../src/commands/status.js";
|
|
16
|
+
import { logsCommand } from "../src/commands/logs.js";
|
|
17
|
+
import { monitCommand } from "../src/commands/monit.js";
|
|
18
|
+
import { webCommand } from "../src/commands/web.js";
|
|
19
|
+
import { alertCommand } from "../src/commands/alert.js";
|
|
20
|
+
import { exportCommand } from "../src/commands/export.js";
|
|
21
|
+
import { depsCommand } from "../src/commands/deps.js";
|
|
22
|
+
import { profileCommand } from "../src/commands/profile.js";
|
|
23
|
+
import { deleteCommand } from "../src/commands/delete.js";
|
|
24
|
+
import { saveCommand } from "../src/commands/save.js";
|
|
25
|
+
import { resurrectCommand } from "../src/commands/resurrect.js";
|
|
26
|
+
import { deployCommand } from "../src/commands/deploy.js";
|
|
27
|
+
import { loadbalancerCommand } from "../src/loadbalancer/manager.js";
|
|
28
|
+
import { windowsCommand } from "../src/windows/service.js";
|
|
29
|
+
import { launchdCommand } from "../src/macos/launchd.js";
|
|
30
|
+
import { updateCommand } from "../src/commands/update.js";
|
|
31
|
+
import { dbpoolCommand } from "../src/database/pool.js";
|
|
32
|
+
import { advancedMonitoringCommand } from "../src/monitoring/advanced.js";
|
|
33
|
+
import { consulCommand } from "../src/discovery/consul.js";
|
|
34
|
+
|
|
35
|
+
const program = new Command();
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.name("bs9")
|
|
39
|
+
.description("BS9 (Bun Sentinel 9) β Mission-critical process manager CLI")
|
|
40
|
+
.version(version);
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.command("start")
|
|
44
|
+
.description("Start a process with hardened systemd unit")
|
|
45
|
+
.argument("<file>", "Application file to start")
|
|
46
|
+
.option("-n, --name <name>", "Service name")
|
|
47
|
+
.option("-p, --port <port>", "Port number", "3000")
|
|
48
|
+
.option("-h, --host <host>", "Host address", "localhost")
|
|
49
|
+
.option("--https", "Use HTTPS protocol")
|
|
50
|
+
.option("-e, --env <env>", "Environment variables (can be used multiple times)", (value, previous) => [...(previous || []), value])
|
|
51
|
+
.option("--otel", "Enable OpenTelemetry instrumentation", true)
|
|
52
|
+
.option("--prometheus", "Enable Prometheus metrics", true)
|
|
53
|
+
.option("--build", "Build TypeScript to JavaScript before starting")
|
|
54
|
+
.action(startCommand);
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command("stop")
|
|
58
|
+
.description("Stop a managed service")
|
|
59
|
+
.argument("<name>", "Service name")
|
|
60
|
+
.action(stopCommand);
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command("restart")
|
|
64
|
+
.description("Restart a managed service")
|
|
65
|
+
.argument("<name>", "Service name")
|
|
66
|
+
.action(restartCommand);
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command("status")
|
|
70
|
+
.description("Show status and SRE metrics for all services")
|
|
71
|
+
.option("-w, --watch", "Watch mode (refresh every 2s)")
|
|
72
|
+
.action(statusCommand);
|
|
73
|
+
|
|
74
|
+
program
|
|
75
|
+
.command("logs")
|
|
76
|
+
.description("Show logs for a service (via journalctl)")
|
|
77
|
+
.argument("<name>", "Service name")
|
|
78
|
+
.option("-f, --follow", "Follow logs")
|
|
79
|
+
.option("-n, --lines <number>", "Number of lines", "50")
|
|
80
|
+
.action(logsCommand);
|
|
81
|
+
|
|
82
|
+
program
|
|
83
|
+
.command("monit")
|
|
84
|
+
.description("Real-time terminal dashboard for all services")
|
|
85
|
+
.option("-r, --refresh <seconds>", "Refresh interval in seconds", "2")
|
|
86
|
+
.action(monitCommand);
|
|
87
|
+
|
|
88
|
+
program
|
|
89
|
+
.command("web")
|
|
90
|
+
.description("Start web-based monitoring dashboard")
|
|
91
|
+
.option("-p, --port <port>", "Port for web dashboard", "8080")
|
|
92
|
+
.option("-d, --detach", "Run in background")
|
|
93
|
+
.action(webCommand);
|
|
94
|
+
|
|
95
|
+
program
|
|
96
|
+
.command("alert")
|
|
97
|
+
.description("Configure alert thresholds and webhooks")
|
|
98
|
+
.option("--enable", "Enable alerts")
|
|
99
|
+
.option("--disable", "Disable alerts")
|
|
100
|
+
.option("--webhook <url>", "Set webhook URL for alerts")
|
|
101
|
+
.option("--cpu <percentage>", "CPU threshold percentage")
|
|
102
|
+
.option("--memory <percentage>", "Memory threshold percentage")
|
|
103
|
+
.option("--errorRate <percentage>", "Error rate threshold percentage")
|
|
104
|
+
.option("--uptime <percentage>", "Uptime threshold percentage")
|
|
105
|
+
.option("--cooldown <seconds>", "Alert cooldown period in seconds")
|
|
106
|
+
.option("--service <name>", "Configure alerts for specific service")
|
|
107
|
+
.option("--list", "List current alert configuration")
|
|
108
|
+
.option("--test", "Test webhook connectivity")
|
|
109
|
+
.action(alertCommand);
|
|
110
|
+
|
|
111
|
+
program
|
|
112
|
+
.command("export")
|
|
113
|
+
.description("Export historical metrics data")
|
|
114
|
+
.option("-f, --format <format>", "Export format (json|csv)", "json")
|
|
115
|
+
.option("-h, --hours <hours>", "Hours of data to export", "24")
|
|
116
|
+
.option("-o, --output <file>", "Output file path")
|
|
117
|
+
.option("-s, --service <name>", "Export specific service metrics")
|
|
118
|
+
.action(exportCommand);
|
|
119
|
+
|
|
120
|
+
program
|
|
121
|
+
.command("deps")
|
|
122
|
+
.description("Visualize service dependencies")
|
|
123
|
+
.option("-f, --format <format>", "Output format (text|dot|json)", "text")
|
|
124
|
+
.option("-o, --output <file>", "Output file path")
|
|
125
|
+
.action(depsCommand);
|
|
126
|
+
|
|
127
|
+
program
|
|
128
|
+
.command("profile")
|
|
129
|
+
.description("Performance profiling for services")
|
|
130
|
+
.option("-d, --duration <seconds>", "Profiling duration", "60")
|
|
131
|
+
.option("-i, --interval <ms>", "Sampling interval", "1000")
|
|
132
|
+
.option("-s, --service <name>", "Service name to profile")
|
|
133
|
+
.option("--flamegraph", "Generate flame graph")
|
|
134
|
+
.option("--top <number>", "Show top N functions", "10")
|
|
135
|
+
.action(profileCommand);
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.command("delete")
|
|
139
|
+
.description("Delete managed services")
|
|
140
|
+
.argument("[name]", "Service name (optional)")
|
|
141
|
+
.option("-a, --all", "Delete all services")
|
|
142
|
+
.option("-f, --force", "Force deletion without errors")
|
|
143
|
+
.option("-r, --remove", "Remove service configuration files")
|
|
144
|
+
.option("-t, --timeout <seconds>", "Timeout for graceful shutdown (default: 30)")
|
|
145
|
+
.action(deleteCommand);
|
|
146
|
+
|
|
147
|
+
program
|
|
148
|
+
.command("save")
|
|
149
|
+
.description("Save service configurations to backup")
|
|
150
|
+
.argument("[name]", "Service name (optional)")
|
|
151
|
+
.option("-a, --all", "Save all services")
|
|
152
|
+
.option("-f, --force", "Force save without errors")
|
|
153
|
+
.option("-b, --backup", "Create timestamped backup")
|
|
154
|
+
.action(saveCommand);
|
|
155
|
+
|
|
156
|
+
program
|
|
157
|
+
.command("resurrect")
|
|
158
|
+
.description("Resurrect services from backup")
|
|
159
|
+
.argument("[name]", "Service name (optional)")
|
|
160
|
+
.option("-a, --all", "Resurrect all services")
|
|
161
|
+
.option("-f, --force", "Force resurrection without errors")
|
|
162
|
+
.option("-c, --config <file>", "Configuration file to use")
|
|
163
|
+
.action(resurrectCommand);
|
|
164
|
+
|
|
165
|
+
program
|
|
166
|
+
.command("deploy")
|
|
167
|
+
.description("π Deploy applications with zero-config production setup")
|
|
168
|
+
.argument("<file>", "Application file to deploy")
|
|
169
|
+
.option("-n, --name <name>", "Service name")
|
|
170
|
+
.option("-p, --port <port>", "Port number", "3000")
|
|
171
|
+
.option("-h, --host <host>", "Host address", "localhost")
|
|
172
|
+
.option("-e, --env <env>", "Environment variables (multiple)", (value, previous) => [...(previous || []), value])
|
|
173
|
+
.option("-i, --instances <number>", "Number of instances")
|
|
174
|
+
.option("--memory <memory>", "Memory limit")
|
|
175
|
+
.option("--cpu <cpu>", "CPU limit")
|
|
176
|
+
.option("--log-level <level>", "Log level")
|
|
177
|
+
.option("--log-file <file>", "Log file path")
|
|
178
|
+
.option("--restart <policy>", "Restart policy")
|
|
179
|
+
.option("--no-health", "Skip health check")
|
|
180
|
+
.option("--no-metrics", "Disable metrics")
|
|
181
|
+
.option("--no-otel", "Disable OpenTelemetry")
|
|
182
|
+
.option("--no-prometheus", "Disable Prometheus")
|
|
183
|
+
.option("--https", "Enable HTTPS")
|
|
184
|
+
.option("--no-linger", "Don't enable linger")
|
|
185
|
+
.option("-f, --force", "Force deployment")
|
|
186
|
+
.option("--reload", "Reload existing service")
|
|
187
|
+
.option("--build", "Build TypeScript")
|
|
188
|
+
.action(deployCommand);
|
|
189
|
+
|
|
190
|
+
program
|
|
191
|
+
.command("loadbalancer")
|
|
192
|
+
.description("Load balancer management")
|
|
193
|
+
.argument("<action>", "Action to perform")
|
|
194
|
+
.option("-p, --port <port>", "Load balancer port", "8080")
|
|
195
|
+
.option("-a, --algorithm <type>", "Load balancing algorithm", "round-robin")
|
|
196
|
+
.option("-b, --backends <list>", "Backend servers (host:port,host:port)")
|
|
197
|
+
.option("--health-check", "Enable health checking", true)
|
|
198
|
+
.option("--health-path <path>", "Health check path", "/healthz")
|
|
199
|
+
.option("--health-interval <ms>", "Health check interval", "10000")
|
|
200
|
+
.action(loadbalancerCommand);
|
|
201
|
+
|
|
202
|
+
program
|
|
203
|
+
.command("dbpool")
|
|
204
|
+
.description("Database connection pool management")
|
|
205
|
+
.argument("<action>", "Action to perform")
|
|
206
|
+
.option("--host <host>", "Database host", "localhost")
|
|
207
|
+
.option("--port <port>", "Database port", "5432")
|
|
208
|
+
.option("--database <name>", "Database name", "testdb")
|
|
209
|
+
.option("--username <user>", "Database username", "user")
|
|
210
|
+
.option("--password <pass>", "Database password", "password")
|
|
211
|
+
.option("--max-connections <num>", "Maximum connections", "10")
|
|
212
|
+
.option("--min-connections <num>", "Minimum connections", "2")
|
|
213
|
+
.option("--concurrency <num>", "Test concurrency", "10")
|
|
214
|
+
.option("--iterations <num>", "Test iterations", "100")
|
|
215
|
+
.action(dbpoolCommand);
|
|
216
|
+
|
|
217
|
+
program
|
|
218
|
+
.command("update")
|
|
219
|
+
.description("Update BS9 to latest version")
|
|
220
|
+
.option("--check", "Check for updates without installing")
|
|
221
|
+
.option("--force", "Force update even if already latest")
|
|
222
|
+
.option("--rollback", "Rollback to previous version")
|
|
223
|
+
.option("--version <version>", "Update to specific version")
|
|
224
|
+
.action(updateCommand);
|
|
225
|
+
|
|
226
|
+
program
|
|
227
|
+
.command("advanced")
|
|
228
|
+
.description("Advanced monitoring dashboard")
|
|
229
|
+
.option("--port <port>", "Dashboard port", "8090")
|
|
230
|
+
.action(advancedMonitoringCommand);
|
|
231
|
+
|
|
232
|
+
program
|
|
233
|
+
.command("consul")
|
|
234
|
+
.description("Consul service discovery")
|
|
235
|
+
.argument("<action>", "Action to perform")
|
|
236
|
+
.option("--consul-url <url>", "Consul URL", "http://localhost:8500")
|
|
237
|
+
.option("--name <name>", "Service name")
|
|
238
|
+
.option("--id <id>", "Service ID")
|
|
239
|
+
.option("--address <address>", "Service address")
|
|
240
|
+
.option("--port <port>", "Service port")
|
|
241
|
+
.option("--tags <tags>", "Service tags (comma-separated)")
|
|
242
|
+
.option("--health-check <url>", "Health check URL")
|
|
243
|
+
.option("--meta <json>", "Service metadata (JSON)")
|
|
244
|
+
.option("--service <service>", "Service name for discovery")
|
|
245
|
+
.action(consulCommand);
|
|
246
|
+
|
|
247
|
+
program.parse();
|
package/dist/bs9.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BS9 - Bun Sentinel 9
|
|
5
|
+
* High-performance, non-root process manager for Bun
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2026 BS9 (Bun Sentinel 9)
|
|
8
|
+
* Licensed under the MIT License
|
|
9
|
+
* https://github.com/xarhang/bs9
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, statSync } from "node:fs";
|
|
13
|
+
import { join, basename, resolve, dirname } from "node:path";
|
|
14
|
+
import { execSync } from "node:child_process";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { getPlatformInfo, initializePlatformDirectories } from "../platform/detect.js";
|
|
17
|
+
|
|
18
|
+
interface DeployOptions {
|
|
19
|
+
name?: string;
|
|
20
|
+
port?: string;
|
|
21
|
+
host?: string;
|
|
22
|
+
env?: string[];
|
|
23
|
+
instances?: number;
|
|
24
|
+
memory?: string;
|
|
25
|
+
cpu?: string;
|
|
26
|
+
logLevel?: string;
|
|
27
|
+
logFile?: string;
|
|
28
|
+
restart?: string;
|
|
29
|
+
health?: boolean;
|
|
30
|
+
metrics?: boolean;
|
|
31
|
+
otel?: boolean;
|
|
32
|
+
prometheus?: boolean;
|
|
33
|
+
https?: boolean;
|
|
34
|
+
linger?: boolean;
|
|
35
|
+
force?: boolean;
|
|
36
|
+
reload?: boolean;
|
|
37
|
+
build?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Security: Service name validation
|
|
41
|
+
function isValidServiceName(name: string): boolean {
|
|
42
|
+
const validPattern = /^[a-zA-Z0-9._-]+$/;
|
|
43
|
+
return validPattern.test(name) && name.length <= 64 && !name.includes('..') && !name.includes('/');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Security: Host validation
|
|
47
|
+
function isValidHost(host: string): boolean {
|
|
48
|
+
const localhostRegex = /^(localhost|127\.0\.0\.1|::1)$/;
|
|
49
|
+
const anyIPRegex = /^(0\.0\.0\.0|::)$/;
|
|
50
|
+
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
51
|
+
const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
52
|
+
|
|
53
|
+
return localhostRegex.test(host) || anyIPRegex.test(host) ||
|
|
54
|
+
(ipv4Regex.test(host) && host.split('.').every(part => parseInt(part, 10) <= 255)) ||
|
|
55
|
+
(hostnameRegex.test(host) && host.length <= 253);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Security: Port validation
|
|
59
|
+
function isValidPort(port: string): boolean {
|
|
60
|
+
const portNum = parseInt(port, 10);
|
|
61
|
+
return !isNaN(portNum) && portNum >= 1 && portNum <= 65535;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function deployCommand(file: string, options: DeployOptions): Promise<void> {
|
|
65
|
+
console.log("π BS9 Deploy - Zero-Config Production Deployment");
|
|
66
|
+
console.log("=" .repeat(50));
|
|
67
|
+
|
|
68
|
+
// Initialize platform directories
|
|
69
|
+
initializePlatformDirectories();
|
|
70
|
+
|
|
71
|
+
const platformInfo = getPlatformInfo();
|
|
72
|
+
|
|
73
|
+
// Step 1: Validate file
|
|
74
|
+
const fullPath = resolve(file);
|
|
75
|
+
if (!existsSync(fullPath)) {
|
|
76
|
+
console.error(`β File not found: ${fullPath}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Security: Validate file path
|
|
81
|
+
const fileName = basename(fullPath);
|
|
82
|
+
if (fileName.includes('..') || fileName.includes('/') || fileName.includes('\\')) {
|
|
83
|
+
console.error(`β Security: Invalid file path: ${file}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Step 2: Auto-generate service name if not provided
|
|
88
|
+
const serviceName = options.name || fileName.replace(/\.(js|ts|mjs|cjs)$/, '').replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
89
|
+
|
|
90
|
+
if (!isValidServiceName(serviceName)) {
|
|
91
|
+
console.error(`β Security: Invalid service name: ${serviceName}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Step 3: Enable linger for user services persistence
|
|
96
|
+
if (options.linger !== false && platformInfo.isLinux) {
|
|
97
|
+
try {
|
|
98
|
+
console.log("π§ Enabling user services persistence...");
|
|
99
|
+
execSync("loginctl enable-linger $USER", { stdio: "pipe" });
|
|
100
|
+
console.log("β
User services persistence enabled");
|
|
101
|
+
} catch (error: any) {
|
|
102
|
+
console.warn("β οΈ Could not enable linger (may require root):", error?.message || error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Step 4: Validate configuration
|
|
107
|
+
const host = options.host || "localhost";
|
|
108
|
+
const port = options.port || "3000";
|
|
109
|
+
|
|
110
|
+
if (!isValidHost(host)) {
|
|
111
|
+
console.error(`β Security: Invalid host: ${host}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isValidPort(port)) {
|
|
116
|
+
console.error(`β Security: Invalid port: ${port}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Step 5: Deploy or reload
|
|
121
|
+
try {
|
|
122
|
+
if (options.reload) {
|
|
123
|
+
await reloadService(serviceName, file, options);
|
|
124
|
+
} else {
|
|
125
|
+
await deployService(serviceName, file, options);
|
|
126
|
+
}
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
console.error(`β Deployment failed: ${err?.message || err}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function deployService(serviceName: string, file: string, options: DeployOptions): Promise<void> {
|
|
134
|
+
console.log(`π¦ Deploying: ${serviceName}`);
|
|
135
|
+
console.log(`π File: ${file}`);
|
|
136
|
+
console.log(`π Host: ${options.host || "localhost"}:${options.port || "3000"}`);
|
|
137
|
+
|
|
138
|
+
// Import start command to reuse logic
|
|
139
|
+
const { startCommand } = await import("./start.js");
|
|
140
|
+
|
|
141
|
+
// Deploy with all production defaults
|
|
142
|
+
await startCommand(file, {
|
|
143
|
+
name: serviceName,
|
|
144
|
+
port: options.port || "3000",
|
|
145
|
+
host: options.host || "localhost",
|
|
146
|
+
env: options.env || [],
|
|
147
|
+
otel: options.otel !== false, // Default to true
|
|
148
|
+
prometheus: options.prometheus !== false, // Default to true
|
|
149
|
+
build: options.build,
|
|
150
|
+
https: options.https
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Step 6: Health check
|
|
154
|
+
if (options.health !== false) {
|
|
155
|
+
await performHealthCheck(serviceName, options.port || "3000", options.host || "localhost");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Step 7: Show deployment summary
|
|
159
|
+
showDeploymentSummary(serviceName, options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function reloadService(serviceName: string, file: string, options: DeployOptions): Promise<void> {
|
|
163
|
+
console.log(`π Reloading: ${serviceName}`);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
// Update environment variables if provided
|
|
167
|
+
if (options.env && options.env.length > 0) {
|
|
168
|
+
console.log("π§ Updating environment variables...");
|
|
169
|
+
// This would update the service configuration
|
|
170
|
+
// For now, we'll restart with new env vars
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Restart service with new configuration
|
|
174
|
+
const { restartCommand } = await import("./restart.js");
|
|
175
|
+
await restartCommand(serviceName);
|
|
176
|
+
|
|
177
|
+
// Health check after reload
|
|
178
|
+
if (options.health !== false) {
|
|
179
|
+
await performHealthCheck(serviceName, options.port || "3000", options.host || "localhost");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log("β
Service reloaded successfully");
|
|
183
|
+
} catch (err: any) {
|
|
184
|
+
console.error(`β Reload failed: ${err?.message || err}`);
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function performHealthCheck(serviceName: string, port: string, host: string): Promise<void> {
|
|
190
|
+
console.log("π₯ Performing health check...");
|
|
191
|
+
|
|
192
|
+
const healthUrl = `http://${host}:${port}/healthz`;
|
|
193
|
+
const metricsUrl = `http://${host}:${port}/metrics`;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
// Wait a moment for service to start
|
|
197
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
198
|
+
|
|
199
|
+
// Check health endpoint
|
|
200
|
+
const healthResponse = await fetch(healthUrl);
|
|
201
|
+
if (healthResponse.ok) {
|
|
202
|
+
console.log(`β
Health check passed: ${healthUrl}`);
|
|
203
|
+
} else {
|
|
204
|
+
console.warn(`β οΈ Health check failed: ${healthResponse.status}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check metrics endpoint
|
|
208
|
+
const metricsResponse = await fetch(metricsUrl);
|
|
209
|
+
if (metricsResponse.ok) {
|
|
210
|
+
console.log(`β
Metrics endpoint available: ${metricsUrl}`);
|
|
211
|
+
} else {
|
|
212
|
+
console.warn(`β οΈ Metrics endpoint failed: ${metricsResponse.status}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
} catch (error: any) {
|
|
216
|
+
console.warn(`β οΈ Health check failed: ${error?.message || error}`);
|
|
217
|
+
console.log(`π‘ Make sure your app exposes /healthz and /metrics endpoints`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function showDeploymentSummary(serviceName: string, options: DeployOptions): void {
|
|
222
|
+
console.log("\n" + "=".repeat(50));
|
|
223
|
+
console.log("π DEPLOYMENT SUMMARY");
|
|
224
|
+
console.log("=".repeat(50));
|
|
225
|
+
console.log(`π¦ Service: ${serviceName}`);
|
|
226
|
+
console.log(`π URL: http://${options.host || "localhost"}:${options.port || "3000"}`);
|
|
227
|
+
console.log(`π₯ Health: http://${options.host || "localhost"}:${options.port || "3000"}/healthz`);
|
|
228
|
+
console.log(`π Metrics: http://${options.host || "localhost"}:${options.port || "3000"}/metrics`);
|
|
229
|
+
console.log(`π OpenTelemetry: ${options.otel !== false ? 'Enabled' : 'Disabled'}`);
|
|
230
|
+
console.log(`π Prometheus: ${options.prometheus !== false ? 'Enabled' : 'Disabled'}`);
|
|
231
|
+
|
|
232
|
+
console.log("\nπ§ MANAGEMENT COMMANDS:");
|
|
233
|
+
console.log(` bs9 status ${serviceName}`);
|
|
234
|
+
console.log(` bs9 logs ${serviceName} --follow`);
|
|
235
|
+
console.log(` bs9 restart ${serviceName}`);
|
|
236
|
+
console.log(` bs9 stop ${serviceName}`);
|
|
237
|
+
console.log(` bs9 delete ${serviceName}`);
|
|
238
|
+
console.log(` bs9 save ${serviceName}`);
|
|
239
|
+
|
|
240
|
+
console.log("\nπ KILLER FEATURES:");
|
|
241
|
+
console.log(" β
Zero-config systemd setup");
|
|
242
|
+
console.log(" β
Built-in health checks & restart policies");
|
|
243
|
+
console.log(" β
Hot reload with environment updates");
|
|
244
|
+
console.log(" β
Automatic metrics & observability");
|
|
245
|
+
console.log(" β
One-command deployment");
|
|
246
|
+
|
|
247
|
+
console.log("\nπ Your service is now running in production mode!");
|
|
248
|
+
}
|
package/src/commands/status.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { execSync } from "node:child_process";
|
|
13
|
+
import { join } from "node:path";
|
|
13
14
|
import { getPlatformInfo } from "../platform/detect.js";
|
|
14
15
|
import { readFileSync } from "node:fs";
|
|
15
16
|
|
|
@@ -27,6 +28,10 @@ interface ServiceStatus {
|
|
|
27
28
|
memory?: string;
|
|
28
29
|
uptime?: string;
|
|
29
30
|
tasks?: string;
|
|
31
|
+
pid?: string;
|
|
32
|
+
port?: string;
|
|
33
|
+
exitCode?: string;
|
|
34
|
+
lastRestart?: string;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export async function statusCommand(options: StatusOptions): Promise<void> {
|
|
@@ -72,6 +77,7 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
|
|
|
72
77
|
|
|
73
78
|
async function getLinuxServices(): Promise<ServiceStatus[]> {
|
|
74
79
|
const services: ServiceStatus[] = [];
|
|
80
|
+
const platformInfo = getPlatformInfo();
|
|
75
81
|
|
|
76
82
|
try {
|
|
77
83
|
const listOutput = execSync("systemctl --user list-units --type=service --no-pager --no-legend", { encoding: "utf-8" });
|
|
@@ -93,20 +99,78 @@ async function getLinuxServices(): Promise<ServiceStatus[]> {
|
|
|
93
99
|
description,
|
|
94
100
|
};
|
|
95
101
|
|
|
96
|
-
// Get additional metrics
|
|
102
|
+
// Get additional metrics with better error handling
|
|
97
103
|
try {
|
|
98
|
-
|
|
104
|
+
// Get comprehensive service information
|
|
105
|
+
const showOutput = execSync(`systemctl --user show ${name} --property=CPUUsageNSec --property=MemoryCurrent --property=ActiveEnterTimestamp --property=MainPID --property=TasksCurrent`, { encoding: "utf-8" });
|
|
106
|
+
|
|
107
|
+
// Extract CPU usage (nanoseconds to milliseconds)
|
|
99
108
|
const cpuMatch = showOutput.match(/CPUUsageNSec=(\d+)/);
|
|
109
|
+
if (cpuMatch) {
|
|
110
|
+
const cpuNsec = Number(cpuMatch[1]);
|
|
111
|
+
status.cpu = formatCPU(cpuNsec);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Extract memory usage (bytes to human readable)
|
|
100
115
|
const memMatch = showOutput.match(/MemoryCurrent=(\d+)/);
|
|
116
|
+
if (memMatch) {
|
|
117
|
+
const memoryBytes = Number(memMatch[1]);
|
|
118
|
+
status.memory = formatMemory(memoryBytes);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract startup time and calculate uptime
|
|
101
122
|
const timeMatch = showOutput.match(/ActiveEnterTimestamp=(.+)/);
|
|
123
|
+
if (timeMatch) {
|
|
124
|
+
status.uptime = formatUptime(timeMatch[1]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extract task count
|
|
102
128
|
const tasksMatch = showOutput.match(/TasksCurrent=(\d+)/);
|
|
129
|
+
if (tasksMatch) {
|
|
130
|
+
status.tasks = tasksMatch[1];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Get process ID for additional info
|
|
134
|
+
const pidMatch = showOutput.match(/MainPID=(\d+)/);
|
|
135
|
+
if (pidMatch && pidMatch[1] !== "0") {
|
|
136
|
+
status.pid = pidMatch[1];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Extract port from service file if available
|
|
140
|
+
const serviceFile = join(platformInfo.serviceDir, `${name}.service`);
|
|
141
|
+
if (require('node:fs').existsSync(serviceFile)) {
|
|
142
|
+
const serviceContent = require('node:fs').readFileSync(serviceFile, 'utf8');
|
|
143
|
+
const portMatch = serviceContent.match(/Environment=PORT=(\d+)/);
|
|
144
|
+
if (portMatch) {
|
|
145
|
+
status.port = portMatch[1];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Extract exit code if available
|
|
150
|
+
const exitCodeMatch = showOutput.match(/Result=(\d+)/);
|
|
151
|
+
if (exitCodeMatch) {
|
|
152
|
+
status.exitCode = exitCodeMatch[1];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Extract last restart time if available
|
|
156
|
+
const restartMatch = showOutput.match(/Result=since=(.+)/);
|
|
157
|
+
if (restartMatch) {
|
|
158
|
+
status.lastRestart = restartMatch[1];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Also try to get additional properties that might be available
|
|
162
|
+
const cpuMatchAlt = showOutput.match(/CPUUsageNSec=(\d+)/);
|
|
163
|
+
const memMatchAlt = showOutput.match(/MemoryCurrent=(\d+)/);
|
|
164
|
+
if (!status.cpu && cpuMatchAlt) {
|
|
165
|
+
status.cpu = formatCPU(Number(cpuMatchAlt[1]));
|
|
166
|
+
}
|
|
167
|
+
if (!status.memory && memMatchAlt) {
|
|
168
|
+
status.memory = formatMemory(Number(memMatchAlt[1]));
|
|
169
|
+
}
|
|
103
170
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (tasksMatch) status.tasks = tasksMatch[1];
|
|
108
|
-
} catch {
|
|
109
|
-
// Metrics might not be available
|
|
171
|
+
} catch (metricsError: any) {
|
|
172
|
+
// If metrics fail, at least we have basic status
|
|
173
|
+
console.warn(`β οΈ Could not get metrics for ${name}: ${metricsError?.message || metricsError}`);
|
|
110
174
|
}
|
|
111
175
|
|
|
112
176
|
services.push(status);
|
|
@@ -145,25 +209,110 @@ async function getWindowsServices(): Promise<ServiceStatus[]> {
|
|
|
145
209
|
}
|
|
146
210
|
|
|
147
211
|
function displayServices(services: ServiceStatus[]): void {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
212
|
+
if (services.length === 0) {
|
|
213
|
+
console.log("π No BS9 services found");
|
|
214
|
+
console.log("π‘ Use 'bs9 start <file>' or 'bs9 deploy <file>' to create a service");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Header with better formatting
|
|
219
|
+
console.log(`${"SERVICE".padEnd(18)} ${"STATUS".padEnd(15)} ${"CPU".padEnd(10)} ${"MEMORY".padEnd(12)} ${"UPTIME".padEnd(12)} ${"TASKS".padEnd(8)} DESCRIPTION`);
|
|
220
|
+
console.log("β".repeat(100));
|
|
151
221
|
|
|
152
|
-
|
|
153
|
-
|
|
222
|
+
// Sort services by status (running first, then by name)
|
|
223
|
+
const sortedServices = services.sort((a, b) => {
|
|
224
|
+
const aRunning = a.active === "active" && a.sub === "running";
|
|
225
|
+
const bRunning = b.active === "active" && b.sub === "running";
|
|
226
|
+
if (aRunning !== bRunning) return bRunning ? 1 : -1;
|
|
227
|
+
return a.name.localeCompare(b.name);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
for (const svc of sortedServices) {
|
|
231
|
+
// Better status formatting with indicators
|
|
232
|
+
let statusIndicator = "";
|
|
233
|
+
let status = `${svc.active}/${svc.sub}`;
|
|
234
|
+
|
|
235
|
+
if (svc.active === "active" && svc.sub === "running") {
|
|
236
|
+
statusIndicator = "β
";
|
|
237
|
+
status = "running";
|
|
238
|
+
} else if (svc.active === "activating" && svc.sub.includes("auto-restart")) {
|
|
239
|
+
statusIndicator = "π";
|
|
240
|
+
status = "restarting";
|
|
241
|
+
} else if (svc.active === "failed" || svc.sub === "failed") {
|
|
242
|
+
statusIndicator = "β";
|
|
243
|
+
status = "failed";
|
|
244
|
+
} else if (svc.active === "inactive") {
|
|
245
|
+
statusIndicator = "βΈοΈ";
|
|
246
|
+
status = "stopped";
|
|
247
|
+
} else {
|
|
248
|
+
statusIndicator = "β οΈ";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const displayStatus = `${statusIndicator} ${status}`;
|
|
252
|
+
|
|
253
|
+
// Format memory and uptime better
|
|
254
|
+
const formattedMemory = svc.memory ? formatMemory(parseMemory(svc.memory)) : "-";
|
|
255
|
+
const formattedUptime = svc.uptime || "-";
|
|
256
|
+
const formattedCPU = svc.cpu || "-";
|
|
257
|
+
|
|
154
258
|
console.log(
|
|
155
|
-
`${svc.name.padEnd(
|
|
259
|
+
`${svc.name.padEnd(18)} ${displayStatus.padEnd(15)} ${formattedCPU.padEnd(10)} ${formattedMemory.padEnd(12)} ${formattedUptime.padEnd(12)} ${(svc.tasks || "-").padEnd(8)} ${svc.description}`
|
|
156
260
|
);
|
|
157
261
|
}
|
|
158
262
|
|
|
159
|
-
|
|
263
|
+
// Enhanced summary with better formatting
|
|
264
|
+
console.log("\nπ Service Summary:");
|
|
160
265
|
const totalServices = services.length;
|
|
161
|
-
const runningServices = services.filter(s => s.active === "active").length;
|
|
266
|
+
const runningServices = services.filter(s => s.active === "active" && s.sub === "running").length;
|
|
267
|
+
const failedServices = services.filter(s => s.active === "failed" || s.sub === "failed").length;
|
|
268
|
+
const restartingServices = services.filter(s => s.active === "activating" && s.sub.includes("auto-restart")).length;
|
|
162
269
|
const totalMemory = services.reduce((sum, s) => sum + (s.memory ? parseMemory(s.memory) : 0), 0);
|
|
163
270
|
|
|
164
|
-
console.log(`
|
|
165
|
-
console.log(`
|
|
166
|
-
console.log(`
|
|
271
|
+
console.log(` π Status: ${runningServices} running, ${failedServices} failed, ${restartingServices} restarting`);
|
|
272
|
+
console.log(` π¦ Total: ${runningServices}/${totalServices} services running`);
|
|
273
|
+
console.log(` πΎ Memory: ${formatMemory(totalMemory)}`);
|
|
274
|
+
console.log(` π Last updated: ${new Date().toLocaleString()}`);
|
|
275
|
+
|
|
276
|
+
// Show failed services details with better formatting
|
|
277
|
+
if (failedServices > 0) {
|
|
278
|
+
console.log("\nβ Failed Services:");
|
|
279
|
+
const failed = services.filter(s => s.active === "failed" || s.sub === "failed");
|
|
280
|
+
for (const svc of failed) {
|
|
281
|
+
console.log(` β’ ${svc.name}: ${svc.active}/${svc.sub}`);
|
|
282
|
+
console.log(` π‘ Troubleshoot: bs9 logs ${svc.name} --tail 20`);
|
|
283
|
+
console.log(` π‘ Check: bs9 status ${svc.name}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Show restarting services details
|
|
288
|
+
if (restartingServices > 0) {
|
|
289
|
+
console.log("\nπ Restarting Services:");
|
|
290
|
+
const restarting = services.filter(s => s.active === "activating" && s.sub.includes("auto-restart"));
|
|
291
|
+
for (const svc of restarting) {
|
|
292
|
+
console.log(` β’ ${svc.name}: ${svc.active}/${svc.sub}`);
|
|
293
|
+
if (svc.exitCode) {
|
|
294
|
+
console.log(` β Exit Code: ${svc.exitCode}`);
|
|
295
|
+
}
|
|
296
|
+
if (svc.lastRestart) {
|
|
297
|
+
console.log(` π Last Restart: ${svc.lastRestart}`);
|
|
298
|
+
}
|
|
299
|
+
console.log(` π‘ Troubleshoot: bs9 logs ${svc.name} --tail 20`);
|
|
300
|
+
console.log(` π‘ Check: bs9 status ${svc.name}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Show running services details
|
|
305
|
+
if (runningServices > 0) {
|
|
306
|
+
console.log("\nβ
Running Services:");
|
|
307
|
+
const running = services.filter(s => s.active === "active" && s.sub === "running");
|
|
308
|
+
for (const svc of running) {
|
|
309
|
+
const memory = svc.memory ? formatMemory(parseMemory(svc.memory)) : "N/A";
|
|
310
|
+
const uptime = svc.uptime || "N/A";
|
|
311
|
+
const port = svc.port || "3000";
|
|
312
|
+
console.log(` β’ ${svc.name}: ${memory} memory, ${uptime} uptime`);
|
|
313
|
+
console.log(` π Access: http://localhost:${port}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
167
316
|
}
|
|
168
317
|
|
|
169
318
|
function formatCPU(nsec: number): string {
|