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
package/src/commands/status.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
+
import { getPlatformInfo } from "../platform/detect.js";
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
6
|
|
|
6
7
|
interface StatusOptions {
|
|
@@ -20,108 +21,140 @@ interface ServiceStatus {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export async function statusCommand(options: StatusOptions): Promise<void> {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const [, statusIndicator, name, loaded, active, sub, description] = match;
|
|
45
|
-
|
|
46
|
-
// Only include services that look like BSN-managed (check for "Bun Service:" pattern)
|
|
47
|
-
if (!description.includes("Bun Service:") && !description.includes("BS9 Service:")) {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const status: ServiceStatus = {
|
|
52
|
-
name,
|
|
53
|
-
loaded,
|
|
54
|
-
active,
|
|
55
|
-
sub,
|
|
56
|
-
description,
|
|
57
|
-
};
|
|
24
|
+
const platformInfo = getPlatformInfo();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
let services: ServiceStatus[] = [];
|
|
28
|
+
|
|
29
|
+
if (platformInfo.isLinux) {
|
|
30
|
+
services = await getLinuxServices();
|
|
31
|
+
} else if (platformInfo.isMacOS) {
|
|
32
|
+
services = await getMacOSServices();
|
|
33
|
+
} else if (platformInfo.isWindows) {
|
|
34
|
+
services = await getWindowsServices();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
displayServices(services);
|
|
38
|
+
|
|
39
|
+
if (options.watch) {
|
|
40
|
+
console.log("\n🔄 Watching for changes (Ctrl+C to stop)...");
|
|
41
|
+
setInterval(async () => {
|
|
42
|
+
console.clear();
|
|
43
|
+
console.log("🔍 BS9 Service Status");
|
|
44
|
+
console.log("=".repeat(80));
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (cpuMatch) status.cpu = formatCPU(Number(cpuMatch[1]));
|
|
68
|
-
if (memMatch) status.memory = formatMemory(Number(memMatch[1]));
|
|
69
|
-
if (timeMatch) status.uptime = formatUptime(timeMatch[1]);
|
|
70
|
-
if (tasksMatch) status.tasks = tasksMatch[1];
|
|
71
|
-
} catch {
|
|
72
|
-
// Ignore metrics errors
|
|
46
|
+
let updatedServices: ServiceStatus[] = [];
|
|
47
|
+
if (platformInfo.isLinux) {
|
|
48
|
+
updatedServices = await getLinuxServices();
|
|
49
|
+
} else if (platformInfo.isMacOS) {
|
|
50
|
+
updatedServices = await getMacOSServices();
|
|
51
|
+
} else if (platformInfo.isWindows) {
|
|
52
|
+
updatedServices = await getWindowsServices();
|
|
73
53
|
}
|
|
74
54
|
|
|
75
|
-
|
|
76
|
-
}
|
|
55
|
+
displayServices(updatedServices);
|
|
56
|
+
}, 2000);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error("❌ Failed to get service status:", err);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function getLinuxServices(): Promise<ServiceStatus[]> {
|
|
65
|
+
const services: ServiceStatus[] = [];
|
|
66
|
+
|
|
67
|
+
try {
|
|
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
|
+
const match = line.match(/^(?:\s*([●\s○]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
|
|
73
|
+
if (!match) continue;
|
|
77
74
|
|
|
78
|
-
|
|
79
|
-
console.clear();
|
|
80
|
-
console.log("🔍 BSN Service Status\n");
|
|
75
|
+
const [, statusIndicator, name, loaded, active, sub, description] = match;
|
|
81
76
|
|
|
82
|
-
if (
|
|
83
|
-
console.log("No BSN-managed services running.");
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
77
|
+
if (!description.includes("Bun Service:") && !description.includes("BS9 Service:")) continue;
|
|
86
78
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
const status: ServiceStatus = {
|
|
80
|
+
name,
|
|
81
|
+
loaded,
|
|
82
|
+
active,
|
|
83
|
+
sub,
|
|
84
|
+
description,
|
|
85
|
+
};
|
|
90
86
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
);
|
|
87
|
+
// Get additional metrics
|
|
88
|
+
try {
|
|
89
|
+
const showOutput = execSync(`systemctl --user show ${name} -p CPUUsageNSec MemoryCurrent ActiveEnterTimestamp TasksCurrent`, { encoding: "utf-8" });
|
|
90
|
+
const cpuMatch = showOutput.match(/CPUUsageNSec=(\d+)/);
|
|
91
|
+
const memMatch = showOutput.match(/MemoryCurrent=(\d+)/);
|
|
92
|
+
const timeMatch = showOutput.match(/ActiveEnterTimestamp=(.+)/);
|
|
93
|
+
const tasksMatch = showOutput.match(/TasksCurrent=(\d+)/);
|
|
94
|
+
|
|
95
|
+
if (cpuMatch) status.cpu = formatCPU(Number(cpuMatch[1]));
|
|
96
|
+
if (memMatch) status.memory = formatMemory(Number(memMatch[1]));
|
|
97
|
+
if (timeMatch) status.uptime = formatUptime(timeMatch[1]);
|
|
98
|
+
if (tasksMatch) status.tasks = tasksMatch[1];
|
|
99
|
+
} catch {
|
|
100
|
+
// Metrics might not be available
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
const totalServices = services.length;
|
|
100
|
-
const runningServices = services.filter(s => s.active === "active").length;
|
|
101
|
-
const totalMemory = services.reduce((sum, s) => sum + (s.memory ? parseMemory(s.memory) : 0), 0);
|
|
102
|
-
|
|
103
|
-
console.log(` Services: ${runningServices}/${totalServices} running`);
|
|
104
|
-
console.log(` Memory: ${formatMemory(totalMemory)}`);
|
|
105
|
-
console.log(` Last updated: ${new Date().toISOString()}`);
|
|
106
|
-
|
|
107
|
-
} catch (err) {
|
|
108
|
-
console.error(`❌ Failed to get status: ${err}`);
|
|
109
|
-
process.exit(1);
|
|
103
|
+
services.push(status);
|
|
110
104
|
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (options.watch) {
|
|
114
|
-
console.log("🔄 Watch mode (Ctrl+C to exit)\n");
|
|
115
|
-
const interval = setInterval(doStatus, 2000);
|
|
116
|
-
process.on("SIGINT", () => {
|
|
117
|
-
clearInterval(interval);
|
|
118
|
-
console.log("\n👋 Exiting watch mode");
|
|
119
|
-
process.exit(0);
|
|
120
|
-
});
|
|
121
|
-
doStatus();
|
|
122
|
-
} else {
|
|
123
|
-
doStatus();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.warn("Failed to get Linux services:", error);
|
|
124
107
|
}
|
|
108
|
+
|
|
109
|
+
return services;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function getMacOSServices(): Promise<ServiceStatus[]> {
|
|
113
|
+
const services: ServiceStatus[] = [];
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const { launchdCommand } = await import("../macos/launchd.js");
|
|
117
|
+
// For now, return empty array - would need to implement status in launchd.ts
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.warn("Failed to get macOS services:", error);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return services;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function getWindowsServices(): Promise<ServiceStatus[]> {
|
|
126
|
+
const services: ServiceStatus[] = [];
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const { windowsCommand } = await import("../windows/service.js");
|
|
130
|
+
// For now, return empty array - would need to implement status in windows/service.ts
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.warn("Failed to get Windows services:", error);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return services;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function displayServices(services: ServiceStatus[]): void {
|
|
139
|
+
// Header
|
|
140
|
+
console.log(`${"SERVICE".padEnd(20)} ${"STATE".padEnd(12)} ${"CPU".padEnd(8)} ${"MEMORY".padEnd(10)} ${"UPTIME".padEnd(12)} ${"TASKS".padEnd(6)} DESCRIPTION`);
|
|
141
|
+
console.log("-".repeat(90));
|
|
142
|
+
|
|
143
|
+
for (const svc of services) {
|
|
144
|
+
const state = `${svc.active}/${svc.sub}`;
|
|
145
|
+
console.log(
|
|
146
|
+
`${svc.name.padEnd(20)} ${state.padEnd(12)} ${(svc.cpu || "-").padEnd(8)} ${(svc.memory || "-").padEnd(10)} ${(svc.uptime || "-").padEnd(12)} ${(svc.tasks || "-").padEnd(6)} ${svc.description}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log("\n📊 SRE Metrics Summary:");
|
|
151
|
+
const totalServices = services.length;
|
|
152
|
+
const runningServices = services.filter(s => s.active === "active").length;
|
|
153
|
+
const totalMemory = services.reduce((sum, s) => sum + (s.memory ? parseMemory(s.memory) : 0), 0);
|
|
154
|
+
|
|
155
|
+
console.log(` Services: ${runningServices}/${totalServices} running`);
|
|
156
|
+
console.log(` Memory: ${formatMemory(totalMemory)}`);
|
|
157
|
+
console.log(` Last updated: ${new Date().toISOString()}`);
|
|
125
158
|
}
|
|
126
159
|
|
|
127
160
|
function formatCPU(nsec: number): string {
|
|
@@ -132,31 +165,43 @@ function formatCPU(nsec: number): string {
|
|
|
132
165
|
function formatMemory(bytes: number): string {
|
|
133
166
|
const units = ["B", "KB", "MB", "GB"];
|
|
134
167
|
let size = bytes;
|
|
135
|
-
let
|
|
136
|
-
|
|
168
|
+
let unitIndex = 0;
|
|
169
|
+
|
|
170
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
137
171
|
size /= 1024;
|
|
138
|
-
|
|
172
|
+
unitIndex++;
|
|
139
173
|
}
|
|
140
|
-
|
|
174
|
+
|
|
175
|
+
return `${size.toFixed(1)}${units[unitIndex]}`;
|
|
141
176
|
}
|
|
142
177
|
|
|
143
|
-
function parseMemory(
|
|
144
|
-
const match =
|
|
178
|
+
function parseMemory(memStr: string): number {
|
|
179
|
+
const match = memStr.match(/^([\d.]+)(B|KB|MB|GB)$/);
|
|
145
180
|
if (!match) return 0;
|
|
181
|
+
|
|
146
182
|
const [, value, unit] = match;
|
|
147
|
-
const
|
|
148
|
-
|
|
183
|
+
const num = parseFloat(value);
|
|
184
|
+
|
|
185
|
+
switch (unit) {
|
|
186
|
+
case "KB": return num * 1024;
|
|
187
|
+
case "MB": return num * 1024 * 1024;
|
|
188
|
+
case "GB": return num * 1024 * 1024 * 1024;
|
|
189
|
+
default: return num;
|
|
190
|
+
}
|
|
149
191
|
}
|
|
150
192
|
|
|
151
193
|
function formatUptime(timestamp: string): string {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
194
|
+
const date = new Date(timestamp);
|
|
195
|
+
const now = new Date();
|
|
196
|
+
const diff = now.getTime() - date.getTime();
|
|
197
|
+
|
|
198
|
+
const seconds = Math.floor(diff / 1000);
|
|
199
|
+
const minutes = Math.floor(seconds / 60);
|
|
200
|
+
const hours = Math.floor(minutes / 60);
|
|
201
|
+
const days = Math.floor(hours / 24);
|
|
202
|
+
|
|
203
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
204
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
205
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
206
|
+
return `${seconds}s`;
|
|
162
207
|
}
|
package/src/commands/stop.ts
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getPlatformInfo } from "../platform/detect.js";
|
|
6
|
+
|
|
7
|
+
// Security: Service name validation
|
|
8
|
+
function isValidServiceName(name: string): boolean {
|
|
9
|
+
// Only allow alphanumeric, hyphens, underscores, and dots
|
|
10
|
+
// Prevent command injection and path traversal
|
|
11
|
+
const validPattern = /^[a-zA-Z0-9._-]+$/;
|
|
12
|
+
return validPattern.test(name) && name.length <= 64 && !name.includes('..') && !name.includes('/');
|
|
13
|
+
}
|
|
4
14
|
|
|
5
15
|
export async function stopCommand(name: string): Promise<void> {
|
|
16
|
+
// Security: Validate service name
|
|
17
|
+
if (!isValidServiceName(name)) {
|
|
18
|
+
console.error(`❌ Security: Invalid service name: ${name}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const platformInfo = getPlatformInfo();
|
|
23
|
+
|
|
6
24
|
try {
|
|
7
|
-
|
|
8
|
-
|
|
25
|
+
if (platformInfo.isLinux) {
|
|
26
|
+
// Security: Use shell escaping to prevent injection
|
|
27
|
+
const escapedName = name.replace(/[^a-zA-Z0-9._-]/g, '');
|
|
28
|
+
execSync(`systemctl --user stop "${escapedName}"`, { stdio: "inherit" });
|
|
29
|
+
console.log(`🛑 User service '${name}' stopped`);
|
|
30
|
+
} else if (platformInfo.isMacOS) {
|
|
31
|
+
const { launchdCommand } = await import("../macos/launchd.js");
|
|
32
|
+
await launchdCommand('stop', { name: `bs9.${name}` });
|
|
33
|
+
} else if (platformInfo.isWindows) {
|
|
34
|
+
const { windowsCommand } = await import("../windows/service.js");
|
|
35
|
+
await windowsCommand('stop', { name: `BS9_${name}` });
|
|
36
|
+
}
|
|
9
37
|
} catch (err) {
|
|
10
|
-
console.error(`❌ Failed to stop
|
|
38
|
+
console.error(`❌ Failed to stop service '${name}': ${err}`);
|
|
11
39
|
process.exit(1);
|
|
12
40
|
}
|
|
13
41
|
}
|
package/src/commands/web.ts
CHANGED
|
@@ -2,26 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
5
6
|
|
|
6
7
|
interface WebOptions {
|
|
7
8
|
port?: string;
|
|
8
9
|
detach?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
// Security: Port validation
|
|
13
|
+
function isValidPort(port: string): boolean {
|
|
14
|
+
const portNum = Number(port);
|
|
15
|
+
return !isNaN(portNum) && portNum >= 1 && portNum <= 65535;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Security: Generate secure session token
|
|
19
|
+
function generateSessionToken(): string {
|
|
20
|
+
return randomBytes(32).toString('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export async function webCommand(options: WebOptions): Promise<void> {
|
|
24
|
+
// Security: Validate port
|
|
12
25
|
const port = options.port || "8080";
|
|
26
|
+
if (!isValidPort(port)) {
|
|
27
|
+
console.error(`❌ Security: Invalid port number: ${port}. Must be 1-65535`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
const dashboardPath = `${import.meta.dir}/../web/dashboard.ts`;
|
|
14
32
|
|
|
33
|
+
// Security: Generate session token for authentication
|
|
34
|
+
const sessionToken = generateSessionToken();
|
|
35
|
+
|
|
15
36
|
console.log(`🌐 Starting BS9 Web Dashboard on port ${port}`);
|
|
37
|
+
console.log(`🔐 Session Token: ${sessionToken}`);
|
|
16
38
|
|
|
17
39
|
if (options.detach) {
|
|
18
|
-
// Run in background
|
|
40
|
+
// Run in background with security
|
|
19
41
|
const child = spawn("bun", ["run", dashboardPath], {
|
|
20
42
|
detached: true,
|
|
21
43
|
stdio: 'ignore',
|
|
22
44
|
env: {
|
|
23
45
|
...process.env,
|
|
24
46
|
WEB_DASHBOARD_PORT: port,
|
|
47
|
+
WEB_SESSION_TOKEN: sessionToken,
|
|
48
|
+
NODE_ENV: "production",
|
|
25
49
|
},
|
|
26
50
|
});
|
|
27
51
|
|
|
@@ -31,14 +55,16 @@ export async function webCommand(options: WebOptions): Promise<void> {
|
|
|
31
55
|
console.log(` URL: http://localhost:${port}`);
|
|
32
56
|
console.log(` Process ID: ${child.pid}`);
|
|
33
57
|
console.log(` Stop with: kill ${child.pid}`);
|
|
58
|
+
console.log(` 🔐 Use session token for API access`);
|
|
34
59
|
} else {
|
|
35
|
-
// Run in foreground
|
|
60
|
+
// Run in foreground with security
|
|
36
61
|
console.log(` URL: http://localhost:${port}`);
|
|
62
|
+
console.log(` 🔐 Session token: ${sessionToken}`);
|
|
37
63
|
console.log(` Press Ctrl+C to stop`);
|
|
38
64
|
console.log('');
|
|
39
65
|
|
|
40
66
|
try {
|
|
41
|
-
execSync(`WEB_DASHBOARD_PORT=${port} bun run ${dashboardPath}`, {
|
|
67
|
+
execSync(`WEB_DASHBOARD_PORT=${port} WEB_SESSION_TOKEN=${sessionToken} NODE_ENV=production bun run ${dashboardPath}`, {
|
|
42
68
|
stdio: "inherit"
|
|
43
69
|
});
|
|
44
70
|
} catch (error) {
|