bs9 1.0.0 β 1.1.0
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 +97 -9
- package/bin/bs9 +58 -7
- package/dist/bs9-064xs9r9. +148 -0
- package/dist/bs9-0gqcrp5t. +144 -0
- package/dist/bs9-nv7nseny. +197 -0
- package/dist/bs9-r6b9zpw0. +156 -0
- package/dist/bs9.js +2 -0
- package/package.json +3 -4
- package/src/commands/deps.ts +295 -0
- package/src/commands/profile.ts +338 -0
- package/src/commands/restart.ts +28 -3
- package/src/commands/start.ts +201 -21
- package/src/commands/status.ts +154 -109
- package/src/commands/stop.ts +31 -3
- package/src/commands/web.ts +29 -3
- package/src/database/pool.ts +335 -0
- package/src/loadbalancer/manager.ts +481 -0
- package/src/macos/launchd.ts +402 -0
- package/src/platform/detect.ts +137 -0
- package/src/windows/service.ts +391 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { startCommand } from "../src/commands/start.js";
|
|
5
|
+
import { stopCommand } from "../src/commands/stop.js";
|
|
6
|
+
import { restartCommand } from "../src/commands/restart.js";
|
|
7
|
+
import { statusCommand } from "../src/commands/status.js";
|
|
8
|
+
import { logsCommand } from "../src/commands/logs.js";
|
|
9
|
+
import { monitCommand } from "../src/commands/monit.js";
|
|
10
|
+
import { webCommand } from "../src/commands/web.js";
|
|
11
|
+
import { alertCommand } from "../src/commands/alert.js";
|
|
12
|
+
import { exportCommand } from "../src/commands/export.js";
|
|
13
|
+
import { depsCommand } from "../src/commands/deps.js";
|
|
14
|
+
import { profileCommand } from "../src/commands/profile.js";
|
|
15
|
+
import { loadbalancerCommand } from "../src/loadbalancer/manager.js";
|
|
16
|
+
import { windowsCommand } from "../src/windows/service.js";
|
|
17
|
+
import { launchdCommand } from "../src/macos/launchd.js";
|
|
18
|
+
import { dbpoolCommand } from "../src/database/pool.js";
|
|
19
|
+
|
|
20
|
+
const program = new Command();
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name("bs9")
|
|
24
|
+
.description("BS9 (Bun Sentinel 9) β Mission-critical process manager CLI")
|
|
25
|
+
.version("1.0.0");
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command("start")
|
|
29
|
+
.description("Start a process with hardened systemd unit")
|
|
30
|
+
.argument("<file>", "Script or executable to run")
|
|
31
|
+
.option("-n, --name <name>", "Service name (defaults to filename)")
|
|
32
|
+
.option("-p, --port <port>", "Port to expose (for metrics/health)", "3000")
|
|
33
|
+
.option("--env <env...>", "Environment variables (KEY=VALUE)")
|
|
34
|
+
.option("--no-otel", "Disable automatic OpenTelemetry injection")
|
|
35
|
+
.option("--no-prometheus", "Disable automatic Prometheus metrics")
|
|
36
|
+
.option("--build", "Build TypeScript to optimized JavaScript for production (AOT)")
|
|
37
|
+
.action(startCommand);
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command("stop")
|
|
41
|
+
.description("Stop a managed service")
|
|
42
|
+
.argument("<name>", "Service name")
|
|
43
|
+
.action(stopCommand);
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command("restart")
|
|
47
|
+
.description("Restart a managed service")
|
|
48
|
+
.argument("<name>", "Service name")
|
|
49
|
+
.action(restartCommand);
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command("status")
|
|
53
|
+
.description("Show status and SRE metrics for all services")
|
|
54
|
+
.option("-w, --watch", "Watch mode (refresh every 2s)")
|
|
55
|
+
.action(statusCommand);
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command("logs")
|
|
59
|
+
.description("Show logs for a service (via journalctl)")
|
|
60
|
+
.argument("<name>", "Service name")
|
|
61
|
+
.option("-f, --follow", "Follow logs")
|
|
62
|
+
.option("-n, --lines <number>", "Number of lines", "50")
|
|
63
|
+
.action(logsCommand);
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command("monit")
|
|
67
|
+
.description("Real-time terminal dashboard for all services")
|
|
68
|
+
.option("-r, --refresh <seconds>", "Refresh interval in seconds", "2")
|
|
69
|
+
.action(monitCommand);
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.command("web")
|
|
73
|
+
.description("Start web-based monitoring dashboard")
|
|
74
|
+
.option("-p, --port <port>", "Port for web dashboard", "8080")
|
|
75
|
+
.option("-d, --detach", "Run in background")
|
|
76
|
+
.action(webCommand);
|
|
77
|
+
|
|
78
|
+
program
|
|
79
|
+
.command("alert")
|
|
80
|
+
.description("Configure alert thresholds and webhooks")
|
|
81
|
+
.option("--enable", "Enable alerts")
|
|
82
|
+
.option("--disable", "Disable alerts")
|
|
83
|
+
.option("--webhook <url>", "Set webhook URL for alerts")
|
|
84
|
+
.option("--cpu <percentage>", "CPU threshold percentage")
|
|
85
|
+
.option("--memory <percentage>", "Memory threshold percentage")
|
|
86
|
+
.option("--errorRate <percentage>", "Error rate threshold percentage")
|
|
87
|
+
.option("--uptime <percentage>", "Uptime threshold percentage")
|
|
88
|
+
.option("--cooldown <seconds>", "Alert cooldown period in seconds")
|
|
89
|
+
.option("--service <name>", "Configure alerts for specific service")
|
|
90
|
+
.option("--list", "List current alert configuration")
|
|
91
|
+
.option("--test", "Test webhook connectivity")
|
|
92
|
+
.action(alertCommand);
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command("export")
|
|
96
|
+
.description("Export historical metrics data")
|
|
97
|
+
.option("-f, --format <format>", "Export format (json|csv)", "json")
|
|
98
|
+
.option("-h, --hours <hours>", "Hours of data to export", "24")
|
|
99
|
+
.option("-o, --output <file>", "Output file path")
|
|
100
|
+
.option("-s, --service <name>", "Export specific service metrics")
|
|
101
|
+
.action(exportCommand);
|
|
102
|
+
|
|
103
|
+
program
|
|
104
|
+
.command("deps")
|
|
105
|
+
.description("Visualize service dependencies")
|
|
106
|
+
.option("-f, --format <format>", "Output format (text|dot|json)", "text")
|
|
107
|
+
.option("-o, --output <file>", "Output file path")
|
|
108
|
+
.action(depsCommand);
|
|
109
|
+
|
|
110
|
+
program
|
|
111
|
+
.command("profile")
|
|
112
|
+
.description("Performance profiling for services")
|
|
113
|
+
.option("-d, --duration <seconds>", "Profiling duration", "60")
|
|
114
|
+
.option("-i, --interval <ms>", "Sampling interval", "1000")
|
|
115
|
+
.option("-s, --service <name>", "Service name to profile")
|
|
116
|
+
.option("-o, --output <file>", "Output file path")
|
|
117
|
+
.action(profileCommand);
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command("loadbalancer")
|
|
121
|
+
.description("Load balancer management")
|
|
122
|
+
.argument("<action>", "Action to perform")
|
|
123
|
+
.option("-p, --port <port>", "Load balancer port", "8080")
|
|
124
|
+
.option("-a, --algorithm <type>", "Load balancing algorithm", "round-robin")
|
|
125
|
+
.option("-b, --backends <list>", "Backend servers (host:port,host:port)")
|
|
126
|
+
.option("--health-check", "Enable health checking", true)
|
|
127
|
+
.option("--health-path <path>", "Health check path", "/healthz")
|
|
128
|
+
.option("--health-interval <ms>", "Health check interval", "10000")
|
|
129
|
+
.action(loadbalancerCommand);
|
|
130
|
+
|
|
131
|
+
program
|
|
132
|
+
.command("dbpool")
|
|
133
|
+
.description("Database connection pool management")
|
|
134
|
+
.argument("<action>", "Action to perform")
|
|
135
|
+
.option("--host <host>", "Database host", "localhost")
|
|
136
|
+
.option("--port <port>", "Database port", "5432")
|
|
137
|
+
.option("--database <name>", "Database name", "testdb")
|
|
138
|
+
.option("--username <user>", "Database username", "user")
|
|
139
|
+
.option("--password <pass>", "Database password", "password")
|
|
140
|
+
.option("--max-connections <num>", "Maximum connections", "10")
|
|
141
|
+
.option("--min-connections <num>", "Minimum connections", "2")
|
|
142
|
+
.option("--concurrency <num>", "Test concurrency", "10")
|
|
143
|
+
.option("--iterations <num>", "Test iterations", "100")
|
|
144
|
+
.action(dbpoolCommand);
|
|
145
|
+
|
|
146
|
+
// Platform-specific commands
|
|
147
|
+
const platform = process.platform;
|
|
148
|
+
|
|
149
|
+
if (platform === 'win32') {
|
|
150
|
+
program
|
|
151
|
+
.command("windows")
|
|
152
|
+
.description("Windows service management")
|
|
153
|
+
.argument("<action>", "Action to perform")
|
|
154
|
+
.option("--name <name>", "Service name")
|
|
155
|
+
.option("--file <path>", "Executable file path")
|
|
156
|
+
.option("--displayName <name>", "Display name")
|
|
157
|
+
.option("--description <desc>", "Service description")
|
|
158
|
+
.option("--workingDir <path>", "Working directory")
|
|
159
|
+
.option("--args <args>", "Command line arguments (JSON array)")
|
|
160
|
+
.option("--env <env>", "Environment variables (JSON object)")
|
|
161
|
+
.action(windowsCommand);
|
|
162
|
+
} else if (platform === 'darwin') {
|
|
163
|
+
program
|
|
164
|
+
.command("macos")
|
|
165
|
+
.description("macOS launchd service management")
|
|
166
|
+
.argument("<action>", "Action to perform")
|
|
167
|
+
.option("--name <name>", "Service name")
|
|
168
|
+
.option("--file <path>", "Executable file path")
|
|
169
|
+
.option("--workingDir <path>", "Working directory")
|
|
170
|
+
.option("--args <args>", "Command line arguments (JSON array)")
|
|
171
|
+
.option("--env <env>", "Environment variables (JSON object)")
|
|
172
|
+
.option("--autoStart", "Start service at load")
|
|
173
|
+
.option("--keepAlive", "Keep service alive")
|
|
174
|
+
.option("--logOut <path>", "Standard output log path")
|
|
175
|
+
.option("--logErr <path>", "Standard error log path")
|
|
176
|
+
.action(launchdCommand);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Add platform info command
|
|
180
|
+
program
|
|
181
|
+
.command("platform")
|
|
182
|
+
.description("Show platform information")
|
|
183
|
+
.action(() => {
|
|
184
|
+
const { getPlatformInfo, getPlatformHelp } = require("../src/platform/detect.js");
|
|
185
|
+
const info = getPlatformInfo();
|
|
186
|
+
console.log('π₯οΈ BS9 Platform Information');
|
|
187
|
+
console.log('='.repeat(50));
|
|
188
|
+
console.log(`Platform: ${info.platform}`);
|
|
189
|
+
console.log(`Service Manager: ${info.serviceManager}`);
|
|
190
|
+
console.log(`Config Directory: ${info.configDir}`);
|
|
191
|
+
console.log(`Log Directory: ${info.logDir}`);
|
|
192
|
+
console.log(`Service Directory: ${info.serviceDir}`);
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(getPlatformHelp());
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
program.parse();
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { startCommand } from "../src/commands/start.js";
|
|
5
|
+
import { stopCommand } from "../src/commands/stop.js";
|
|
6
|
+
import { restartCommand } from "../src/commands/restart.js";
|
|
7
|
+
import { statusCommand } from "../src/commands/status.js";
|
|
8
|
+
import { logsCommand } from "../src/commands/logs.js";
|
|
9
|
+
import { monitCommand } from "../src/commands/monit.js";
|
|
10
|
+
import { webCommand } from "../src/commands/web.js";
|
|
11
|
+
import { alertCommand } from "../src/commands/alert.js";
|
|
12
|
+
import { exportCommand } from "../src/commands/export.js";
|
|
13
|
+
import { depsCommand } from "../src/commands/deps.js";
|
|
14
|
+
import { profileCommand } from "../src/commands/profile.js";
|
|
15
|
+
import { windowsCommand } from "../src/windows/service.js";
|
|
16
|
+
import { loadbalancerCommand } from "../src/loadbalancer/manager.js";
|
|
17
|
+
import { dbpoolCommand } from "../src/database/pool.js";
|
|
18
|
+
|
|
19
|
+
const program = new Command();
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name("bs9")
|
|
23
|
+
.description("BS9 (Bun Sentinel 9) β Mission-critical process manager CLI")
|
|
24
|
+
.version("1.0.0");
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command("start")
|
|
28
|
+
.description("Start a process with hardened systemd unit")
|
|
29
|
+
.argument("<file>", "Script or executable to run")
|
|
30
|
+
.option("-n, --name <name>", "Service name (defaults to filename)")
|
|
31
|
+
.option("-p, --port <port>", "Port to expose (for metrics/health)", "3000")
|
|
32
|
+
.option("--env <env...>", "Environment variables (KEY=VALUE)")
|
|
33
|
+
.option("--no-otel", "Disable automatic OpenTelemetry injection")
|
|
34
|
+
.option("--no-prometheus", "Disable automatic Prometheus metrics")
|
|
35
|
+
.option("--build", "Build TypeScript to optimized JavaScript for production (AOT)")
|
|
36
|
+
.action(startCommand);
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command("stop")
|
|
40
|
+
.description("Stop a managed service")
|
|
41
|
+
.argument("<name>", "Service name")
|
|
42
|
+
.action(stopCommand);
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.command("restart")
|
|
46
|
+
.description("Restart a managed service")
|
|
47
|
+
.argument("<name>", "Service name")
|
|
48
|
+
.action(restartCommand);
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.command("status")
|
|
52
|
+
.description("Show status and SRE metrics for all services")
|
|
53
|
+
.option("-w, --watch", "Watch mode (refresh every 2s)")
|
|
54
|
+
.action(statusCommand);
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command("logs")
|
|
58
|
+
.description("Show logs for a service (via journalctl)")
|
|
59
|
+
.argument("<name>", "Service name")
|
|
60
|
+
.option("-f, --follow", "Follow logs")
|
|
61
|
+
.option("-n, --lines <number>", "Number of lines", "50")
|
|
62
|
+
.action(logsCommand);
|
|
63
|
+
|
|
64
|
+
program
|
|
65
|
+
.command("monit")
|
|
66
|
+
.description("Real-time terminal dashboard for all services")
|
|
67
|
+
.option("-r, --refresh <seconds>", "Refresh interval in seconds", "2")
|
|
68
|
+
.action(monitCommand);
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.command("web")
|
|
72
|
+
.description("Start web-based monitoring dashboard")
|
|
73
|
+
.option("-p, --port <port>", "Port for web dashboard", "8080")
|
|
74
|
+
.option("-d, --detach", "Run in background")
|
|
75
|
+
.action(webCommand);
|
|
76
|
+
|
|
77
|
+
program
|
|
78
|
+
.command("alert")
|
|
79
|
+
.description("Configure alert thresholds and webhooks")
|
|
80
|
+
.option("--enable", "Enable alerts")
|
|
81
|
+
.option("--disable", "Disable alerts")
|
|
82
|
+
.option("--webhook <url>", "Set webhook URL for alerts")
|
|
83
|
+
.option("--cpu <percentage>", "CPU threshold percentage")
|
|
84
|
+
.option("--memory <percentage>", "Memory threshold percentage")
|
|
85
|
+
.option("--errorRate <percentage>", "Error rate threshold percentage")
|
|
86
|
+
.option("--uptime <percentage>", "Uptime threshold percentage")
|
|
87
|
+
.option("--cooldown <seconds>", "Alert cooldown period in seconds")
|
|
88
|
+
.option("--service <name>", "Configure alerts for specific service")
|
|
89
|
+
.option("--list", "List current alert configuration")
|
|
90
|
+
.option("--test", "Test webhook connectivity")
|
|
91
|
+
.action(alertCommand);
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command("export")
|
|
95
|
+
.description("Export historical metrics data")
|
|
96
|
+
.option("-f, --format <format>", "Export format (json|csv)", "json")
|
|
97
|
+
.option("-h, --hours <hours>", "Hours of data to export", "24")
|
|
98
|
+
.option("-o, --output <file>", "Output file path")
|
|
99
|
+
.option("-s, --service <name>", "Export specific service metrics")
|
|
100
|
+
.action(exportCommand);
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command("deps")
|
|
104
|
+
.description("Visualize service dependencies")
|
|
105
|
+
.option("-f, --format <format>", "Output format (text|dot|json)", "text")
|
|
106
|
+
.option("-o, --output <file>", "Output file path")
|
|
107
|
+
.action(depsCommand);
|
|
108
|
+
|
|
109
|
+
program
|
|
110
|
+
.command("profile")
|
|
111
|
+
.description("Performance profiling for services")
|
|
112
|
+
.option("-d, --duration <seconds>", "Profiling duration", "60")
|
|
113
|
+
.option("-i, --interval <ms>", "Sampling interval", "1000")
|
|
114
|
+
.option("-s, --service <name>", "Service name to profile")
|
|
115
|
+
.option("-o, --output <file>", "Output file path")
|
|
116
|
+
.action(profileCommand);
|
|
117
|
+
|
|
118
|
+
program
|
|
119
|
+
.command("windows")
|
|
120
|
+
.description("Windows service management")
|
|
121
|
+
.argument("<action>", "Action to perform", "create|start|stop|delete|status")
|
|
122
|
+
.argument("[name]", "Service name")
|
|
123
|
+
.option("--file <path>", "Service file path")
|
|
124
|
+
.option("--start-type <type>", "Start type (auto|demand|disabled)", "auto")
|
|
125
|
+
.option("--auto-start", "Auto-start service", true)
|
|
126
|
+
.option("--env <key=value>", "Environment variables")
|
|
127
|
+
.action(windowsCommand);
|
|
128
|
+
|
|
129
|
+
program
|
|
130
|
+
.command("loadbalancer")
|
|
131
|
+
.description("Load balancer management")
|
|
132
|
+
.argument("<action>", "Action to perform", "start|status|config")
|
|
133
|
+
.option("-p, --port <port>", "Load balancer port", "8080")
|
|
134
|
+
.option("-a, --algorithm <type>", "Load balancing algorithm", "round-robin")
|
|
135
|
+
.option("-b, --backends <list>", "Backend servers (host:port,host:port)")
|
|
136
|
+
.option("--health-check", "Enable health checking", true)
|
|
137
|
+
.option("--health-path <path>", "Health check path", "/healthz")
|
|
138
|
+
.option("--health-interval <ms>", "Health check interval", "10000")
|
|
139
|
+
.action(loadbalancerCommand);
|
|
140
|
+
|
|
141
|
+
program
|
|
142
|
+
.command("dbpool")
|
|
143
|
+
.description("Database connection pool management")
|
|
144
|
+
.argument("<action>", "Action to perform", "start|test|stats")
|
|
145
|
+
.option("--host <host>", "Database host", "localhost")
|
|
146
|
+
.option("--port <port>", "Database port", "5432")
|
|
147
|
+
.option("--database <name>", "Database name", "testdb")
|
|
148
|
+
.option("--username <user>", "Database username", "user")
|
|
149
|
+
.option("--password <pass>", "Database password", "password")
|
|
150
|
+
.option("--max-connections <num>", "Maximum connections", "10")
|
|
151
|
+
.option("--min-connections <num>", "Minimum connections", "2")
|
|
152
|
+
.option("--concurrency <num>", "Test concurrency", "10")
|
|
153
|
+
.option("--iterations <num>", "Test iterations", "100")
|
|
154
|
+
.action(dbpoolCommand);
|
|
155
|
+
|
|
156
|
+
program.parse();
|
package/dist/bs9.js
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bs9",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Bun Sentinel 9 - High-performance, non-root process manager for Bun",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"bin": {
|
|
8
|
-
"bs9": "./bin/bs9"
|
|
9
|
-
"bsn": "./bin/bs9"
|
|
8
|
+
"bs9": "./bin/bs9"
|
|
10
9
|
},
|
|
11
10
|
"files": [
|
|
12
11
|
"bin",
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
"@opentelemetry/resources": "^1.23.0",
|
|
37
36
|
"@opentelemetry/sdk-node": "^0.50.0",
|
|
38
37
|
"@opentelemetry/semantic-conventions": "^1.23.0",
|
|
39
|
-
"commander": "^
|
|
38
|
+
"commander": "^14.0.2",
|
|
40
39
|
"pg": "^8.12.0",
|
|
41
40
|
"pino": "^9.3.1",
|
|
42
41
|
"prom-client": "^15.1.2"
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { setTimeout } from "node:timers/promises";
|
|
5
|
+
|
|
6
|
+
interface DepOptions {
|
|
7
|
+
format?: string;
|
|
8
|
+
output?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ServiceDependency {
|
|
12
|
+
name: string;
|
|
13
|
+
dependsOn: string[];
|
|
14
|
+
status: 'running' | 'stopped' | 'failed' | 'unknown';
|
|
15
|
+
health: 'healthy' | 'unhealthy' | 'unknown';
|
|
16
|
+
port?: number;
|
|
17
|
+
endpoints: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DependencyGraph {
|
|
21
|
+
services: ServiceDependency[];
|
|
22
|
+
edges: Array<{
|
|
23
|
+
from: string;
|
|
24
|
+
to: string;
|
|
25
|
+
type: 'http' | 'database' | 'message' | 'custom';
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function depsCommand(options: DepOptions): Promise<void> {
|
|
30
|
+
console.log('π BS9 Service Dependency Visualization');
|
|
31
|
+
console.log('='.repeat(80));
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const graph = await buildDependencyGraph();
|
|
35
|
+
|
|
36
|
+
if (options.format === 'dot') {
|
|
37
|
+
const dotOutput = generateDotGraph(graph);
|
|
38
|
+
if (options.output) {
|
|
39
|
+
await Bun.write(options.output, dotOutput);
|
|
40
|
+
console.log(`β
Dependency graph saved to: ${options.output}`);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(dotOutput);
|
|
43
|
+
}
|
|
44
|
+
} else if (options.format === 'json') {
|
|
45
|
+
const jsonOutput = JSON.stringify(graph, null, 2);
|
|
46
|
+
if (options.output) {
|
|
47
|
+
await Bun.write(options.output, jsonOutput);
|
|
48
|
+
console.log(`β
Dependency graph saved to: ${options.output}`);
|
|
49
|
+
} else {
|
|
50
|
+
console.log(jsonOutput);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
displayDependencyGraph(graph);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`β Failed to analyze dependencies: ${error}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function buildDependencyGraph(): Promise<DependencyGraph> {
|
|
63
|
+
const services: ServiceDependency[] = [];
|
|
64
|
+
const edges: Array<{from: string; to: string; type: 'http' | 'database' | 'message' | 'custom'}> = [];
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Get all BS9 services
|
|
68
|
+
const listOutput = execSync("systemctl --user list-units --type=service --no-pager --no-legend", { encoding: "utf-8" });
|
|
69
|
+
const lines = listOutput.split("\n").filter(line => line.includes(".service"));
|
|
70
|
+
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
if (!line.trim()) continue;
|
|
73
|
+
|
|
74
|
+
const match = line.match(/^(?:\s*([β\sβ]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
|
|
75
|
+
if (!match) continue;
|
|
76
|
+
|
|
77
|
+
const [, statusIndicator, name, loaded, active, sub, description] = match;
|
|
78
|
+
|
|
79
|
+
if (!description.includes("Bun Service:") && !description.includes("BS9 Service:")) continue;
|
|
80
|
+
|
|
81
|
+
const service: ServiceDependency = {
|
|
82
|
+
name,
|
|
83
|
+
dependsOn: [],
|
|
84
|
+
status: getServiceStatus(active, sub),
|
|
85
|
+
health: 'unknown',
|
|
86
|
+
endpoints: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Extract port from description
|
|
90
|
+
const portMatch = description.match(/port[=:]?\s*(\d+)/i);
|
|
91
|
+
if (portMatch) {
|
|
92
|
+
service.port = Number(portMatch[1]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Analyze service for dependencies
|
|
96
|
+
const deps = await analyzeServiceDependencies(name, service.port);
|
|
97
|
+
service.dependsOn = deps.dependsOn;
|
|
98
|
+
service.endpoints = deps.endpoints;
|
|
99
|
+
|
|
100
|
+
services.push(service);
|
|
101
|
+
|
|
102
|
+
// Add edges to graph
|
|
103
|
+
for (const dep of deps.dependsOn) {
|
|
104
|
+
edges.push({
|
|
105
|
+
from: name,
|
|
106
|
+
to: dep,
|
|
107
|
+
type: deps.type,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Error fetching services:', error);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { services, edges };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function analyzeServiceDependencies(serviceName: string, port?: number): Promise<{
|
|
120
|
+
dependsOn: string[];
|
|
121
|
+
endpoints: string[];
|
|
122
|
+
type: 'http' | 'database' | 'message' | 'custom';
|
|
123
|
+
}> {
|
|
124
|
+
const dependsOn: string[] = [];
|
|
125
|
+
const endpoints: string[] = [];
|
|
126
|
+
let type: 'http' | 'database' | 'message' | 'custom' = 'custom';
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Check if service has health endpoints
|
|
130
|
+
if (port) {
|
|
131
|
+
endpoints.push(`http://localhost:${port}/healthz`);
|
|
132
|
+
endpoints.push(`http://localhost:${port}/readyz`);
|
|
133
|
+
endpoints.push(`http://localhost:${port}/metrics`);
|
|
134
|
+
|
|
135
|
+
// Check for common dependency patterns
|
|
136
|
+
try {
|
|
137
|
+
const response = Bun.fetch(`http://localhost:${port}/dependencies`, { timeout: 1000 });
|
|
138
|
+
if (response.ok) {
|
|
139
|
+
const deps = await response.json();
|
|
140
|
+
if (Array.isArray(deps)) {
|
|
141
|
+
dependsOn.push(...deps);
|
|
142
|
+
type = 'http';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Service doesn't expose dependencies endpoint
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Analyze service files for dependency patterns
|
|
151
|
+
try {
|
|
152
|
+
const servicePath = `/home/xarhang/.config/systemd/user/${serviceName}.service`;
|
|
153
|
+
const serviceContent = Bun.file(servicePath).text();
|
|
154
|
+
|
|
155
|
+
// Look for environment variables that suggest dependencies
|
|
156
|
+
const envMatches = serviceContent.match(/Environment=([^\n]+)/g) || [];
|
|
157
|
+
for (const env of envMatches) {
|
|
158
|
+
if (env.includes('DATABASE_URL') || env.includes('DB_HOST')) {
|
|
159
|
+
dependsOn.push('database');
|
|
160
|
+
type = 'database';
|
|
161
|
+
}
|
|
162
|
+
if (env.includes('REDIS_URL') || env.includes('REDIS_HOST')) {
|
|
163
|
+
dependsOn.push('redis');
|
|
164
|
+
type = 'message';
|
|
165
|
+
}
|
|
166
|
+
if (env.includes('API_URL') || env.includes('SERVICE_URL')) {
|
|
167
|
+
const urlMatch = env.match(/https?:\/\/([^:\/]+)/);
|
|
168
|
+
if (urlMatch) {
|
|
169
|
+
dependsOn.push(urlMatch[1]);
|
|
170
|
+
type = 'http';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Service file not accessible
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`Error analyzing dependencies for ${serviceName}:`, error);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { dependsOn, endpoints, type };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getServiceStatus(active: string, sub: string): 'running' | 'stopped' | 'failed' | 'unknown' {
|
|
186
|
+
if (active === 'active' && sub === 'running') return 'running';
|
|
187
|
+
if (active === 'inactive') return 'stopped';
|
|
188
|
+
if (active === 'failed' || sub === 'failed') return 'failed';
|
|
189
|
+
return 'unknown';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function displayDependencyGraph(graph: DependencyGraph): void {
|
|
193
|
+
console.log('\nπ Service Dependencies:');
|
|
194
|
+
console.log('-'.repeat(60));
|
|
195
|
+
|
|
196
|
+
for (const service of graph.services) {
|
|
197
|
+
const statusIcon = getStatusIcon(service.status);
|
|
198
|
+
const healthIcon = getHealthIcon(service.health);
|
|
199
|
+
|
|
200
|
+
console.log(`${statusIcon} ${service.name} ${healthIcon}`);
|
|
201
|
+
|
|
202
|
+
if (service.dependsOn.length > 0) {
|
|
203
|
+
console.log(` ββ Depends on: ${service.dependsOn.join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (service.endpoints.length > 0) {
|
|
207
|
+
console.log(` ββ Endpoints: ${service.endpoints.slice(0, 2).join(', ')}${service.endpoints.length > 2 ? '...' : ''}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log('\nπ Dependency Relationships:');
|
|
214
|
+
console.log('-'.repeat(60));
|
|
215
|
+
|
|
216
|
+
if (graph.edges.length === 0) {
|
|
217
|
+
console.log('No dependencies found between services.');
|
|
218
|
+
} else {
|
|
219
|
+
for (const edge of graph.edges) {
|
|
220
|
+
const fromService = graph.services.find(s => s.name === edge.from);
|
|
221
|
+
const toService = graph.services.find(s => s.name === edge.to);
|
|
222
|
+
|
|
223
|
+
const fromIcon = fromService ? getStatusIcon(fromService.status) : 'β';
|
|
224
|
+
const toIcon = toService ? getStatusIcon(toService.status) : 'β';
|
|
225
|
+
|
|
226
|
+
console.log(`${fromIcon} ${edge.from} β ${edge.to} ${toIcon} (${edge.type})`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log('\nπ Summary:');
|
|
231
|
+
console.log(` Total Services: ${graph.services.length}`);
|
|
232
|
+
console.log(` Dependencies: ${graph.edges.length}`);
|
|
233
|
+
console.log(` Running: ${graph.services.filter(s => s.status === 'running').length}`);
|
|
234
|
+
console.log(` Failed: ${graph.services.filter(s => s.status === 'failed').length}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function generateDotGraph(graph: DependencyGraph): string {
|
|
238
|
+
let dot = 'digraph BS9_Dependencies {\n';
|
|
239
|
+
dot += ' rankdir=LR;\n';
|
|
240
|
+
dot += ' node [shape=box, style=filled];\n\n';
|
|
241
|
+
|
|
242
|
+
// Add nodes
|
|
243
|
+
for (const service of graph.services) {
|
|
244
|
+
const color = getNodeColor(service.status);
|
|
245
|
+
const label = `${service.name}\\n${service.status}`;
|
|
246
|
+
dot += ` "${service.name}" [label="${label}", fillcolor="${color}"];\n`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
dot += '\n';
|
|
250
|
+
|
|
251
|
+
// Add edges
|
|
252
|
+
for (const edge of graph.edges) {
|
|
253
|
+
const color = getEdgeColor(edge.type);
|
|
254
|
+
dot += ` "${edge.from}" -> "${edge.to}" [color="${color}", label="${edge.type}"];\n`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
dot += '}\n';
|
|
258
|
+
|
|
259
|
+
return dot;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getStatusIcon(status: string): string {
|
|
263
|
+
switch (status) {
|
|
264
|
+
case 'running': return 'β
';
|
|
265
|
+
case 'stopped': return 'βΈοΈ';
|
|
266
|
+
case 'failed': return 'β';
|
|
267
|
+
default: return 'β';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getHealthIcon(health: string): string {
|
|
272
|
+
switch (health) {
|
|
273
|
+
case 'healthy': return 'π’';
|
|
274
|
+
case 'unhealthy': return 'π΄';
|
|
275
|
+
default: return 'βͺ';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getNodeColor(status: string): string {
|
|
280
|
+
switch (status) {
|
|
281
|
+
case 'running': return 'lightgreen';
|
|
282
|
+
case 'stopped': return 'lightgray';
|
|
283
|
+
case 'failed': return 'lightcoral';
|
|
284
|
+
default: return 'lightyellow';
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getEdgeColor(type: string): string {
|
|
289
|
+
switch (type) {
|
|
290
|
+
case 'http': return 'blue';
|
|
291
|
+
case 'database': return 'green';
|
|
292
|
+
case 'message': return 'orange';
|
|
293
|
+
default: return 'gray';
|
|
294
|
+
}
|
|
295
|
+
}
|