bs9 1.3.7 ā 1.3.8
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 +37 -1
- package/bin/bs9 +21 -1
- package/dist/bs9-xvqzb6v3. +221 -0
- package/dist/bs9.js +1 -1
- package/package.json +1 -1
- package/src/commands/delete.ts +1 -1
- package/src/commands/resurrect.ts +171 -0
- package/src/commands/save.ts +249 -0
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)
|
|
@@ -98,6 +98,20 @@ bs9 alert --test # Test webhook
|
|
|
98
98
|
bs9 export --format json --hours 24 # Export metrics
|
|
99
99
|
bs9 export --service myapp --format csv --hours 24
|
|
100
100
|
bs9 export --service myapp --format csv
|
|
101
|
+
|
|
102
|
+
# Service management
|
|
103
|
+
bs9 delete myapp # Delete specific service
|
|
104
|
+
bs9 delete myapp --remove # Delete and remove config files
|
|
105
|
+
bs9 delete --all # Delete all services
|
|
106
|
+
bs9 delete --all --force # Force delete all services
|
|
107
|
+
bs9 delete myapp --timeout 60 # Custom graceful shutdown timeout
|
|
108
|
+
|
|
109
|
+
# Backup and restore
|
|
110
|
+
bs9 save myapp # Save service configuration
|
|
111
|
+
bs9 save --all # Save all services
|
|
112
|
+
bs9 save myapp --backup # Save with timestamped backup
|
|
113
|
+
bs9 resurrect myapp # Restore from backup
|
|
114
|
+
bs9 resurrect --all # Restore all services
|
|
101
115
|
```
|
|
102
116
|
|
|
103
117
|
---
|
|
@@ -123,6 +137,22 @@ bs9 export --service myapp --format csv
|
|
|
123
137
|
- **Cooldown Period**: Prevent alert spam
|
|
124
138
|
- **Alert Testing**: Webhook connectivity validation
|
|
125
139
|
|
|
140
|
+
### šļø Service Deletion & Cleanup
|
|
141
|
+
- **Individual Service Deletion**: Delete specific services by name
|
|
142
|
+
- **Bulk Deletion**: Remove all BS9 services at once
|
|
143
|
+
- **Configuration Cleanup**: Remove service configuration files
|
|
144
|
+
- **Force Deletion**: Ignore errors during deletion
|
|
145
|
+
- **Graceful Shutdown**: Configurable timeout for clean termination
|
|
146
|
+
- **Cross-Platform Support**: Works on Linux, macOS, and Windows
|
|
147
|
+
|
|
148
|
+
### š¾ Backup & Restore System
|
|
149
|
+
- **Service Configuration Backup**: Save service configurations to JSON
|
|
150
|
+
- **Bulk Backup**: Save all services at once
|
|
151
|
+
- **Timestamped Backups**: Create multiple backup versions
|
|
152
|
+
- **Service Resurrection**: Restore services from backup
|
|
153
|
+
- **Configuration Management**: Manage service configurations
|
|
154
|
+
- **Disaster Recovery**: Quick service restoration
|
|
155
|
+
|
|
126
156
|
### š³ Container & Orchestration
|
|
127
157
|
- **Docker Support**: Complete Dockerfile and docker-compose setup
|
|
128
158
|
- **Kubernetes**: Full K8s deployment with ServiceMonitor
|
|
@@ -303,6 +333,9 @@ kubectl get pods -n bs9-system
|
|
|
303
333
|
| - | `bs9 web` | Web-based dashboard |
|
|
304
334
|
| - | `bs9 alert` | Alert system with webhooks |
|
|
305
335
|
| - | `bs9 export` | Historical metrics |
|
|
336
|
+
| - | `bs9 delete` | Service deletion and cleanup |
|
|
337
|
+
| - | `bs9 save` | Service configuration backup |
|
|
338
|
+
| - | `bs9 resurrect` | Service restoration from backup |
|
|
306
339
|
|
|
307
340
|
---
|
|
308
341
|
|
|
@@ -453,6 +486,9 @@ BS9/
|
|
|
453
486
|
ā ā āāā monit.ts # Terminal dashboard
|
|
454
487
|
ā ā āāā web.ts # Web dashboard
|
|
455
488
|
ā ā āāā alert.ts # Alert management
|
|
489
|
+
ā ā āāā delete.ts # Service deletion
|
|
490
|
+
ā ā āāā save.ts # Service backup
|
|
491
|
+
ā ā āāā resurrect.ts # Service restoration
|
|
456
492
|
ā ā āāā export.ts # Data export
|
|
457
493
|
ā āāā web/ # Web dashboard
|
|
458
494
|
ā ā āāā dashboard.ts # Web server
|
package/bin/bs9
CHANGED
|
@@ -21,6 +21,8 @@ import { exportCommand } from "../src/commands/export.js";
|
|
|
21
21
|
import { depsCommand } from "../src/commands/deps.js";
|
|
22
22
|
import { profileCommand } from "../src/commands/profile.js";
|
|
23
23
|
import { deleteCommand } from "../src/commands/delete.js";
|
|
24
|
+
import { saveCommand } from "../src/commands/save.js";
|
|
25
|
+
import { resurrectCommand } from "../src/commands/resurrect.js";
|
|
24
26
|
import { loadbalancerCommand } from "../src/loadbalancer/manager.js";
|
|
25
27
|
import { windowsCommand } from "../src/windows/service.js";
|
|
26
28
|
import { launchdCommand } from "../src/macos/launchd.js";
|
|
@@ -138,9 +140,27 @@ program
|
|
|
138
140
|
.option("-a, --all", "Delete all services")
|
|
139
141
|
.option("-f, --force", "Force deletion without errors")
|
|
140
142
|
.option("-r, --remove", "Remove service configuration files")
|
|
141
|
-
.option("-t, --timeout <seconds>", "Timeout for graceful shutdown
|
|
143
|
+
.option("-t, --timeout <seconds>", "Timeout for graceful shutdown (default: 30)")
|
|
142
144
|
.action(deleteCommand);
|
|
143
145
|
|
|
146
|
+
program
|
|
147
|
+
.command("save")
|
|
148
|
+
.description("Save service configurations to backup")
|
|
149
|
+
.argument("[name]", "Service name (optional)")
|
|
150
|
+
.option("-a, --all", "Save all services")
|
|
151
|
+
.option("-f, --force", "Force save without errors")
|
|
152
|
+
.option("-b, --backup", "Create timestamped backup")
|
|
153
|
+
.action(saveCommand);
|
|
154
|
+
|
|
155
|
+
program
|
|
156
|
+
.command("resurrect")
|
|
157
|
+
.description("Resurrect services from backup")
|
|
158
|
+
.argument("[name]", "Service name (optional)")
|
|
159
|
+
.option("-a, --all", "Resurrect all services")
|
|
160
|
+
.option("-f, --force", "Force resurrection without errors")
|
|
161
|
+
.option("-c, --config <file>", "Configuration file to use")
|
|
162
|
+
.action(resurrectCommand);
|
|
163
|
+
|
|
144
164
|
program
|
|
145
165
|
.command("loadbalancer")
|
|
146
166
|
.description("Load balancer management")
|
|
@@ -0,0 +1,221 @@
|
|
|
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 { loadbalancerCommand } from "../src/loadbalancer/manager.js";
|
|
27
|
+
import { windowsCommand } from "../src/windows/service.js";
|
|
28
|
+
import { launchdCommand } from "../src/macos/launchd.js";
|
|
29
|
+
import { updateCommand } from "../src/commands/update.js";
|
|
30
|
+
import { dbpoolCommand } from "../src/database/pool.js";
|
|
31
|
+
import { advancedMonitoringCommand } from "../src/monitoring/advanced.js";
|
|
32
|
+
import { consulCommand } from "../src/discovery/consul.js";
|
|
33
|
+
|
|
34
|
+
const program = new Command();
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.name("bs9")
|
|
38
|
+
.description("BS9 (Bun Sentinel 9) ā Mission-critical process manager CLI")
|
|
39
|
+
.version(version);
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("start")
|
|
43
|
+
.description("Start a process with hardened systemd unit")
|
|
44
|
+
.argument("<file>", "Application file to start")
|
|
45
|
+
.option("-n, --name <name>", "Service name")
|
|
46
|
+
.option("-p, --port <port>", "Port number", "3000")
|
|
47
|
+
.option("-h, --host <host>", "Host address", "localhost")
|
|
48
|
+
.option("--https", "Use HTTPS protocol")
|
|
49
|
+
.option("-e, --env <env>", "Environment variables (can be used multiple times)", (value, previous) => [...(previous || []), value])
|
|
50
|
+
.option("--otel", "Enable OpenTelemetry instrumentation", true)
|
|
51
|
+
.option("--prometheus", "Enable Prometheus metrics", true)
|
|
52
|
+
.option("--build", "Build TypeScript to JavaScript before starting")
|
|
53
|
+
.action(startCommand);
|
|
54
|
+
|
|
55
|
+
program
|
|
56
|
+
.command("stop")
|
|
57
|
+
.description("Stop a managed service")
|
|
58
|
+
.argument("<name>", "Service name")
|
|
59
|
+
.action(stopCommand);
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command("restart")
|
|
63
|
+
.description("Restart a managed service")
|
|
64
|
+
.argument("<name>", "Service name")
|
|
65
|
+
.action(restartCommand);
|
|
66
|
+
|
|
67
|
+
program
|
|
68
|
+
.command("status")
|
|
69
|
+
.description("Show status and SRE metrics for all services")
|
|
70
|
+
.option("-w, --watch", "Watch mode (refresh every 2s)")
|
|
71
|
+
.action(statusCommand);
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command("logs")
|
|
75
|
+
.description("Show logs for a service (via journalctl)")
|
|
76
|
+
.argument("<name>", "Service name")
|
|
77
|
+
.option("-f, --follow", "Follow logs")
|
|
78
|
+
.option("-n, --lines <number>", "Number of lines", "50")
|
|
79
|
+
.action(logsCommand);
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command("monit")
|
|
83
|
+
.description("Real-time terminal dashboard for all services")
|
|
84
|
+
.option("-r, --refresh <seconds>", "Refresh interval in seconds", "2")
|
|
85
|
+
.action(monitCommand);
|
|
86
|
+
|
|
87
|
+
program
|
|
88
|
+
.command("web")
|
|
89
|
+
.description("Start web-based monitoring dashboard")
|
|
90
|
+
.option("-p, --port <port>", "Port for web dashboard", "8080")
|
|
91
|
+
.option("-d, --detach", "Run in background")
|
|
92
|
+
.action(webCommand);
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command("alert")
|
|
96
|
+
.description("Configure alert thresholds and webhooks")
|
|
97
|
+
.option("--enable", "Enable alerts")
|
|
98
|
+
.option("--disable", "Disable alerts")
|
|
99
|
+
.option("--webhook <url>", "Set webhook URL for alerts")
|
|
100
|
+
.option("--cpu <percentage>", "CPU threshold percentage")
|
|
101
|
+
.option("--memory <percentage>", "Memory threshold percentage")
|
|
102
|
+
.option("--errorRate <percentage>", "Error rate threshold percentage")
|
|
103
|
+
.option("--uptime <percentage>", "Uptime threshold percentage")
|
|
104
|
+
.option("--cooldown <seconds>", "Alert cooldown period in seconds")
|
|
105
|
+
.option("--service <name>", "Configure alerts for specific service")
|
|
106
|
+
.option("--list", "List current alert configuration")
|
|
107
|
+
.option("--test", "Test webhook connectivity")
|
|
108
|
+
.action(alertCommand);
|
|
109
|
+
|
|
110
|
+
program
|
|
111
|
+
.command("export")
|
|
112
|
+
.description("Export historical metrics data")
|
|
113
|
+
.option("-f, --format <format>", "Export format (json|csv)", "json")
|
|
114
|
+
.option("-h, --hours <hours>", "Hours of data to export", "24")
|
|
115
|
+
.option("-o, --output <file>", "Output file path")
|
|
116
|
+
.option("-s, --service <name>", "Export specific service metrics")
|
|
117
|
+
.action(exportCommand);
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command("deps")
|
|
121
|
+
.description("Visualize service dependencies")
|
|
122
|
+
.option("-f, --format <format>", "Output format (text|dot|json)", "text")
|
|
123
|
+
.option("-o, --output <file>", "Output file path")
|
|
124
|
+
.action(depsCommand);
|
|
125
|
+
|
|
126
|
+
program
|
|
127
|
+
.command("profile")
|
|
128
|
+
.description("Performance profiling for services")
|
|
129
|
+
.option("-d, --duration <seconds>", "Profiling duration", "60")
|
|
130
|
+
.option("-i, --interval <ms>", "Sampling interval", "1000")
|
|
131
|
+
.option("-s, --service <name>", "Service name to profile")
|
|
132
|
+
.option("--flamegraph", "Generate flame graph")
|
|
133
|
+
.option("--top <number>", "Show top N functions", "10")
|
|
134
|
+
.action(profileCommand);
|
|
135
|
+
|
|
136
|
+
program
|
|
137
|
+
.command("delete")
|
|
138
|
+
.description("Delete managed services")
|
|
139
|
+
.argument("[name]", "Service name (optional)")
|
|
140
|
+
.option("-a, --all", "Delete all services")
|
|
141
|
+
.option("-f, --force", "Force deletion without errors")
|
|
142
|
+
.option("-r, --remove", "Remove service configuration files")
|
|
143
|
+
.option("-t, --timeout <seconds>", "Timeout for graceful shutdown (default: 30)")
|
|
144
|
+
.action(deleteCommand);
|
|
145
|
+
|
|
146
|
+
program
|
|
147
|
+
.command("save")
|
|
148
|
+
.description("Save service configurations to backup")
|
|
149
|
+
.argument("[name]", "Service name (optional)")
|
|
150
|
+
.option("-a, --all", "Save all services")
|
|
151
|
+
.option("-f, --force", "Force save without errors")
|
|
152
|
+
.option("-b, --backup", "Create timestamped backup")
|
|
153
|
+
.action(saveCommand);
|
|
154
|
+
|
|
155
|
+
program
|
|
156
|
+
.command("resurrect")
|
|
157
|
+
.description("Resurrect services from backup")
|
|
158
|
+
.argument("[name]", "Service name (optional)")
|
|
159
|
+
.option("-a, --all", "Resurrect all services")
|
|
160
|
+
.option("-f, --force", "Force resurrection without errors")
|
|
161
|
+
.option("-c, --config <file>", "Configuration file to use")
|
|
162
|
+
.action(resurrectCommand);
|
|
163
|
+
|
|
164
|
+
program
|
|
165
|
+
.command("loadbalancer")
|
|
166
|
+
.description("Load balancer management")
|
|
167
|
+
.argument("<action>", "Action to perform")
|
|
168
|
+
.option("-p, --port <port>", "Load balancer port", "8080")
|
|
169
|
+
.option("-a, --algorithm <type>", "Load balancing algorithm", "round-robin")
|
|
170
|
+
.option("-b, --backends <list>", "Backend servers (host:port,host:port)")
|
|
171
|
+
.option("--health-check", "Enable health checking", true)
|
|
172
|
+
.option("--health-path <path>", "Health check path", "/healthz")
|
|
173
|
+
.option("--health-interval <ms>", "Health check interval", "10000")
|
|
174
|
+
.action(loadbalancerCommand);
|
|
175
|
+
|
|
176
|
+
program
|
|
177
|
+
.command("dbpool")
|
|
178
|
+
.description("Database connection pool management")
|
|
179
|
+
.argument("<action>", "Action to perform")
|
|
180
|
+
.option("--host <host>", "Database host", "localhost")
|
|
181
|
+
.option("--port <port>", "Database port", "5432")
|
|
182
|
+
.option("--database <name>", "Database name", "testdb")
|
|
183
|
+
.option("--username <user>", "Database username", "user")
|
|
184
|
+
.option("--password <pass>", "Database password", "password")
|
|
185
|
+
.option("--max-connections <num>", "Maximum connections", "10")
|
|
186
|
+
.option("--min-connections <num>", "Minimum connections", "2")
|
|
187
|
+
.option("--concurrency <num>", "Test concurrency", "10")
|
|
188
|
+
.option("--iterations <num>", "Test iterations", "100")
|
|
189
|
+
.action(dbpoolCommand);
|
|
190
|
+
|
|
191
|
+
program
|
|
192
|
+
.command("update")
|
|
193
|
+
.description("Update BS9 to latest version")
|
|
194
|
+
.option("--check", "Check for updates without installing")
|
|
195
|
+
.option("--force", "Force update even if already latest")
|
|
196
|
+
.option("--rollback", "Rollback to previous version")
|
|
197
|
+
.option("--version <version>", "Update to specific version")
|
|
198
|
+
.action(updateCommand);
|
|
199
|
+
|
|
200
|
+
program
|
|
201
|
+
.command("advanced")
|
|
202
|
+
.description("Advanced monitoring dashboard")
|
|
203
|
+
.option("--port <port>", "Dashboard port", "8090")
|
|
204
|
+
.action(advancedMonitoringCommand);
|
|
205
|
+
|
|
206
|
+
program
|
|
207
|
+
.command("consul")
|
|
208
|
+
.description("Consul service discovery")
|
|
209
|
+
.argument("<action>", "Action to perform")
|
|
210
|
+
.option("--consul-url <url>", "Consul URL", "http://localhost:8500")
|
|
211
|
+
.option("--name <name>", "Service name")
|
|
212
|
+
.option("--id <id>", "Service ID")
|
|
213
|
+
.option("--address <address>", "Service address")
|
|
214
|
+
.option("--port <port>", "Service port")
|
|
215
|
+
.option("--tags <tags>", "Service tags (comma-separated)")
|
|
216
|
+
.option("--health-check <url>", "Health check URL")
|
|
217
|
+
.option("--meta <json>", "Service metadata (JSON)")
|
|
218
|
+
.option("--service <service>", "Service name for discovery")
|
|
219
|
+
.action(consulCommand);
|
|
220
|
+
|
|
221
|
+
program.parse();
|
package/dist/bs9.js
CHANGED
package/package.json
CHANGED
package/src/commands/delete.ts
CHANGED
|
@@ -118,7 +118,7 @@ async function deleteAllServices(platformInfo: any, options: DeleteOptions): Pro
|
|
|
118
118
|
for (const line of lines) {
|
|
119
119
|
const match = line.match(/^(?:\s*([ā\sā]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
|
|
120
120
|
if (match) {
|
|
121
|
-
const [, serviceName] = match;
|
|
121
|
+
const [, , serviceName] = match; // Skip the status symbol, capture service name
|
|
122
122
|
|
|
123
123
|
// Only process BS9 services
|
|
124
124
|
if (match[5].includes("Bun Service:") || match[5].includes("BS9 Service:")) {
|
|
@@ -0,0 +1,171 @@
|
|
|
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 { execSync } from "node:child_process";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { getPlatformInfo } from "../platform/detect.js";
|
|
15
|
+
|
|
16
|
+
interface ResurrectOptions {
|
|
17
|
+
all?: boolean;
|
|
18
|
+
force?: boolean;
|
|
19
|
+
config?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Security: Service name validation
|
|
23
|
+
function isValidServiceName(name: string): boolean {
|
|
24
|
+
// Only allow alphanumeric, hyphens, underscores, and dots
|
|
25
|
+
// Prevent command injection and path traversal
|
|
26
|
+
const validPattern = /^[a-zA-Z0-9._-]+$/;
|
|
27
|
+
return validPattern.test(name) && name.length <= 64 && !name.includes('..') && !name.includes('/');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function resurrectCommand(name: string, options: ResurrectOptions): Promise<void> {
|
|
31
|
+
const platformInfo = getPlatformInfo();
|
|
32
|
+
|
|
33
|
+
// Handle resurrect all services
|
|
34
|
+
if (options.all) {
|
|
35
|
+
await resurrectAllServices(platformInfo, options);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Security: Validate service name
|
|
40
|
+
if (!isValidServiceName(name)) {
|
|
41
|
+
console.error(`ā Security: Invalid service name: ${name}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (platformInfo.isLinux) {
|
|
47
|
+
// Security: Use shell escaping to prevent injection
|
|
48
|
+
const escapedName = name.replace(/[^a-zA-Z0-9._-]/g, '');
|
|
49
|
+
|
|
50
|
+
// Check if service exists in backup
|
|
51
|
+
const backupDir = join(platformInfo.configDir, 'backups');
|
|
52
|
+
const backupFile = join(backupDir, `${escapedName}.json`);
|
|
53
|
+
|
|
54
|
+
if (!require('node:fs').existsSync(backupFile)) {
|
|
55
|
+
console.error(`ā No backup found for service '${name}'`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Load backup configuration
|
|
60
|
+
const backupConfig = JSON.parse(require('node:fs').readFileSync(backupFile, 'utf8'));
|
|
61
|
+
|
|
62
|
+
// Restore service using backup configuration
|
|
63
|
+
const { startCommand } = await import("./start.js");
|
|
64
|
+
await startCommand(backupConfig.file, {
|
|
65
|
+
name: backupConfig.name,
|
|
66
|
+
port: backupConfig.port?.toString(),
|
|
67
|
+
host: backupConfig.host,
|
|
68
|
+
env: backupConfig.env,
|
|
69
|
+
otel: backupConfig.otel,
|
|
70
|
+
prometheus: backupConfig.prometheus,
|
|
71
|
+
build: backupConfig.build,
|
|
72
|
+
https: backupConfig.https
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
console.log(`ā
Service '${name}' resurrected successfully from backup`);
|
|
76
|
+
|
|
77
|
+
} else if (platformInfo.isMacOS) {
|
|
78
|
+
const { launchdCommand } = await import("../macos/launchd.js");
|
|
79
|
+
await launchdCommand('resurrect', { name: `bs9.${name}` });
|
|
80
|
+
|
|
81
|
+
if (options.config) {
|
|
82
|
+
const plistFile = join(platformInfo.serviceDir, `bs9.${name}.plist`);
|
|
83
|
+
try {
|
|
84
|
+
require('node:fs').writeFileSync(plistFile, options.config);
|
|
85
|
+
console.log(`š Configuration restored: ${plistFile}`);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`ā Failed to restore configuration: ${error}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`ā
Service '${name}' resurrected successfully`);
|
|
92
|
+
|
|
93
|
+
} else if (platformInfo.isWindows) {
|
|
94
|
+
const { windowsCommand } = await import("../windows/service.js");
|
|
95
|
+
await windowsCommand('resurrect', { name: `BS9_${name}` });
|
|
96
|
+
|
|
97
|
+
console.log(`ā
Service '${name}' resurrected successfully`);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`ā Failed to resurrect service '${name}': ${err}`);
|
|
101
|
+
if (!options.force) {
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function resurrectAllServices(platformInfo: any, options: ResurrectOptions): Promise<void> {
|
|
108
|
+
try {
|
|
109
|
+
console.log("š Resurrecting all BS9 services from backup...");
|
|
110
|
+
|
|
111
|
+
if (platformInfo.isLinux) {
|
|
112
|
+
const backupDir = join(platformInfo.configDir, 'backups');
|
|
113
|
+
|
|
114
|
+
if (!require('node:fs').existsSync(backupDir)) {
|
|
115
|
+
console.log("ā¹ļø No backup directory found");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Get all backup files
|
|
120
|
+
const backupFiles = require('node:fs').readdirSync(backupDir)
|
|
121
|
+
.filter((file: string) => file.endsWith('.json'));
|
|
122
|
+
|
|
123
|
+
if (backupFiles.length === 0) {
|
|
124
|
+
console.log("ā¹ļø No backup files found to resurrect");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(`Found ${backupFiles.length} backup files to restore...`);
|
|
129
|
+
|
|
130
|
+
for (const backupFile of backupFiles) {
|
|
131
|
+
try {
|
|
132
|
+
const serviceName = backupFile.replace('.json', '');
|
|
133
|
+
const backupPath = join(backupDir, backupFile);
|
|
134
|
+
const backupConfig = JSON.parse(require('node:fs').readFileSync(backupPath, 'utf8'));
|
|
135
|
+
|
|
136
|
+
// Restore service using backup configuration
|
|
137
|
+
const { startCommand } = await import("./start.js");
|
|
138
|
+
await startCommand(backupConfig.file, {
|
|
139
|
+
name: backupConfig.name,
|
|
140
|
+
port: backupConfig.port?.toString(),
|
|
141
|
+
host: backupConfig.host,
|
|
142
|
+
env: backupConfig.env,
|
|
143
|
+
otel: backupConfig.otel,
|
|
144
|
+
prometheus: backupConfig.prometheus,
|
|
145
|
+
build: backupConfig.build,
|
|
146
|
+
https: backupConfig.https
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
console.log(` ā
Resurrected service: ${serviceName}`);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(` ā ļø Failed to resurrect service '${backupFile}': ${error}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
} else if (platformInfo.isMacOS) {
|
|
156
|
+
console.log("š To resurrect all services on macOS, you need to manually restore the plist files from:");
|
|
157
|
+
console.log(` ${join(platformInfo.configDir, 'backups')}/*.plist`);
|
|
158
|
+
console.log(" And then run: launchctl load ~/Library/LaunchAgents/bs9.*.plist");
|
|
159
|
+
} else if (platformInfo.isWindows) {
|
|
160
|
+
console.log("š To resurrect all services on Windows, use PowerShell:");
|
|
161
|
+
console.log(" Get-ChildItem -Path \"${join(platformInfo.configDir, 'backups')}\" | ForEach-Object { Restore-Service $_.Name }");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(`ā
All BS9 services resurrection process completed`);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error(`ā Failed to resurrect all services: ${err}`);
|
|
167
|
+
if (!options.force) {
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
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 { execSync } from "node:child_process";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { getPlatformInfo } from "../platform/detect.js";
|
|
15
|
+
|
|
16
|
+
interface SaveOptions {
|
|
17
|
+
all?: boolean;
|
|
18
|
+
force?: boolean;
|
|
19
|
+
backup?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Security: Service name validation
|
|
23
|
+
function isValidServiceName(name: string): boolean {
|
|
24
|
+
// Only allow alphanumeric, hyphens, underscores, and dots
|
|
25
|
+
// Prevent command injection and path traversal
|
|
26
|
+
const validPattern = /^[a-zA-Z0-9._-]+$/;
|
|
27
|
+
return validPattern.test(name) && name.length <= 64 && !name.includes('..') && !name.includes('/');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function saveCommand(name: string, options: SaveOptions): Promise<void> {
|
|
31
|
+
const platformInfo = getPlatformInfo();
|
|
32
|
+
|
|
33
|
+
// Handle save all services
|
|
34
|
+
if (options.all) {
|
|
35
|
+
await saveAllServices(platformInfo, options);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Security: Validate service name
|
|
40
|
+
if (!isValidServiceName(name)) {
|
|
41
|
+
console.error(`ā Security: Invalid service name: ${name}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (platformInfo.isLinux) {
|
|
47
|
+
// Security: Use shell escaping to prevent injection
|
|
48
|
+
const escapedName = name.replace(/[^a-zA-Z0-9._-]/g, '');
|
|
49
|
+
|
|
50
|
+
// Get service status and configuration
|
|
51
|
+
const statusOutput = execSync(`systemctl --user show "${escapedName}"`, { encoding: "utf-8" });
|
|
52
|
+
const serviceFile = join(platformInfo.serviceDir, `${escapedName}.service`);
|
|
53
|
+
|
|
54
|
+
if (!require('node:fs').existsSync(serviceFile)) {
|
|
55
|
+
console.error(`ā Service configuration not found for '${name}'`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Read service configuration
|
|
60
|
+
const serviceConfig = require('node:fs').readFileSync(serviceFile, 'utf8');
|
|
61
|
+
|
|
62
|
+
// Parse service configuration to extract startup parameters
|
|
63
|
+
const config = parseServiceConfig(serviceConfig, statusOutput);
|
|
64
|
+
|
|
65
|
+
// Create backup directory if it doesn't exist
|
|
66
|
+
const backupDir = join(platformInfo.configDir, 'backups');
|
|
67
|
+
require('node:fs').mkdirSync(backupDir, { recursive: true });
|
|
68
|
+
|
|
69
|
+
// Save configuration to backup
|
|
70
|
+
const backupFile = join(backupDir, `${escapedName}.json`);
|
|
71
|
+
const backupData = {
|
|
72
|
+
name: name,
|
|
73
|
+
file: extractFileFromConfig(serviceConfig),
|
|
74
|
+
port: config.port,
|
|
75
|
+
host: config.host,
|
|
76
|
+
env: config.env,
|
|
77
|
+
otel: config.otel,
|
|
78
|
+
prometheus: config.prometheus,
|
|
79
|
+
build: config.build,
|
|
80
|
+
https: config.https,
|
|
81
|
+
savedAt: new Date().toISOString(),
|
|
82
|
+
platform: platformInfo.platform
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
require('node:fs').writeFileSync(backupFile, JSON.stringify(backupData, null, 2));
|
|
86
|
+
|
|
87
|
+
console.log(`š¾ Service '${name}' configuration saved to: ${backupFile}`);
|
|
88
|
+
|
|
89
|
+
if (options.backup) {
|
|
90
|
+
// Create additional backup with timestamp
|
|
91
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
92
|
+
const timestampedBackup = join(backupDir, `${escapedName}-${timestamp}.json`);
|
|
93
|
+
require('node:fs').writeFileSync(timestampedBackup, JSON.stringify(backupData, null, 2));
|
|
94
|
+
console.log(`š¦ Additional backup created: ${timestampedBackup}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
} else if (platformInfo.isMacOS) {
|
|
98
|
+
const { launchdCommand } = await import("../macos/launchd.js");
|
|
99
|
+
await launchdCommand('save', { name: `bs9.${name}` });
|
|
100
|
+
|
|
101
|
+
console.log(`š¾ Service '${name}' configuration saved`);
|
|
102
|
+
|
|
103
|
+
} else if (platformInfo.isWindows) {
|
|
104
|
+
const { windowsCommand } = await import("../windows/service.js");
|
|
105
|
+
await windowsCommand('save', { name: `BS9_${name}` });
|
|
106
|
+
|
|
107
|
+
console.log(`š¾ Service '${name}' configuration saved`);
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`ā Failed to save service '${name}': ${err}`);
|
|
111
|
+
if (!options.force) {
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function saveAllServices(platformInfo: any, options: SaveOptions): Promise<void> {
|
|
118
|
+
try {
|
|
119
|
+
console.log("š¾ Saving all BS9 service configurations...");
|
|
120
|
+
|
|
121
|
+
if (platformInfo.isLinux) {
|
|
122
|
+
// Get all BS9 services
|
|
123
|
+
const listOutput = execSync("systemctl --user list-units --type=service --no-pager --no-legend", { encoding: "utf-8" });
|
|
124
|
+
const lines = listOutput.split("\n").filter(line => line.includes(".service"));
|
|
125
|
+
|
|
126
|
+
const bs9Services: string[] = [];
|
|
127
|
+
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
const match = line.match(/^(?:\s*([ā\sā]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
|
|
130
|
+
if (match) {
|
|
131
|
+
const [, , serviceName] = match; // Skip the status symbol, capture service name
|
|
132
|
+
|
|
133
|
+
// Only process BS9 services
|
|
134
|
+
if (match[5].includes("Bun Service:") || match[5].includes("BS9 Service:")) {
|
|
135
|
+
bs9Services.push(serviceName);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (bs9Services.length === 0) {
|
|
141
|
+
console.log("ā¹ļø No BS9 services found to save");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`Found ${bs9Services.length} BS9 services to save...`);
|
|
146
|
+
|
|
147
|
+
// Create backup directory
|
|
148
|
+
const backupDir = join(platformInfo.configDir, 'backups');
|
|
149
|
+
require('node:fs').mkdirSync(backupDir, { recursive: true });
|
|
150
|
+
|
|
151
|
+
for (const serviceName of bs9Services) {
|
|
152
|
+
try {
|
|
153
|
+
const serviceFile = join(platformInfo.serviceDir, `${serviceName}.service`);
|
|
154
|
+
|
|
155
|
+
if (require('node:fs').existsSync(serviceFile)) {
|
|
156
|
+
const serviceConfig = require('node:fs').readFileSync(serviceFile, 'utf8');
|
|
157
|
+
const statusOutput = execSync(`systemctl --user show "${serviceName}"`, { encoding: "utf-8" });
|
|
158
|
+
const config = parseServiceConfig(serviceConfig, statusOutput);
|
|
159
|
+
|
|
160
|
+
const backupFile = join(backupDir, `${serviceName}.json`);
|
|
161
|
+
const backupData = {
|
|
162
|
+
name: serviceName,
|
|
163
|
+
file: extractFileFromConfig(serviceConfig),
|
|
164
|
+
port: config.port,
|
|
165
|
+
host: config.host,
|
|
166
|
+
env: config.env,
|
|
167
|
+
otel: config.otel,
|
|
168
|
+
prometheus: config.prometheus,
|
|
169
|
+
build: config.build,
|
|
170
|
+
https: config.https,
|
|
171
|
+
savedAt: new Date().toISOString(),
|
|
172
|
+
platform: platformInfo.platform
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
require('node:fs').writeFileSync(backupFile, JSON.stringify(backupData, null, 2));
|
|
176
|
+
console.log(` š¾ Saved service: ${serviceName}`);
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(` ā ļø Failed to save service '${serviceName}': ${error}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
} else if (platformInfo.isMacOS) {
|
|
184
|
+
console.log("š To save all services on macOS, you need to manually backup the plist files from:");
|
|
185
|
+
console.log(` ${platformInfo.serviceDir}/bs9.*.plist`);
|
|
186
|
+
} else if (platformInfo.isWindows) {
|
|
187
|
+
console.log("š To save all services on Windows, use PowerShell:");
|
|
188
|
+
console.log(" Get-Service -Name \"BS9_*\" | ForEach-Object { Export-ServiceConfiguration $_.Name }");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(`ā
All BS9 services save process completed`);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(`ā Failed to save all services: ${err}`);
|
|
194
|
+
if (!options.force) {
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Helper function to parse service configuration
|
|
201
|
+
function parseServiceConfig(serviceConfig: string, statusOutput: string): any {
|
|
202
|
+
const config: any = {};
|
|
203
|
+
|
|
204
|
+
// Extract port from service config
|
|
205
|
+
const portMatch = serviceConfig.match(/--port[=\s]+(\d+)/);
|
|
206
|
+
if (portMatch) {
|
|
207
|
+
config.port = parseInt(portMatch[1]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Extract host from service config
|
|
211
|
+
const hostMatch = serviceConfig.match(/--host[=\s]+([^\s]+)/);
|
|
212
|
+
if (hostMatch) {
|
|
213
|
+
config.host = hostMatch[1];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Extract environment variables
|
|
217
|
+
const envMatches = serviceConfig.match(/--env[=\s]+([^\s]+)/g);
|
|
218
|
+
if (envMatches) {
|
|
219
|
+
config.env = envMatches.map((env: string) => env.replace(/--env[=\s]+/, ''));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Extract OpenTelemetry flag
|
|
223
|
+
config.otel = serviceConfig.includes('--otel') || serviceConfig.includes('--opentelemetry');
|
|
224
|
+
|
|
225
|
+
// Extract Prometheus flag
|
|
226
|
+
config.prometheus = serviceConfig.includes('--prometheus');
|
|
227
|
+
|
|
228
|
+
// Extract build flag
|
|
229
|
+
config.build = serviceConfig.includes('--build');
|
|
230
|
+
|
|
231
|
+
// Extract HTTPS flag
|
|
232
|
+
config.https = serviceConfig.includes('--https');
|
|
233
|
+
|
|
234
|
+
return config;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Helper function to extract file path from service config
|
|
238
|
+
function extractFileFromConfig(serviceConfig: string): string {
|
|
239
|
+
const execMatch = serviceConfig.match(/ExecStart=([^\n]+)/);
|
|
240
|
+
if (execMatch) {
|
|
241
|
+
const execLine = execMatch[1].trim();
|
|
242
|
+
// Extract the file path from the exec command
|
|
243
|
+
const fileMatch = execLine.match(/bun\s+([^\s]+)/);
|
|
244
|
+
if (fileMatch) {
|
|
245
|
+
return fileMatch[1];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return '';
|
|
249
|
+
}
|