bs9 1.0.0 → 1.3.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 +105 -9
- package/bin/bs9 +91 -7
- package/dist/bs9-064xs9r9. +148 -0
- package/dist/bs9-0gqcrp5t. +144 -0
- package/dist/bs9-33vcpmb9. +181 -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/update.ts +322 -0
- package/src/commands/web.ts +29 -3
- package/src/database/pool.ts +335 -0
- package/src/discovery/consul.ts +285 -0
- package/src/loadbalancer/manager.ts +481 -0
- package/src/macos/launchd.ts +402 -0
- package/src/monitoring/advanced.ts +341 -0
- package/src/platform/detect.ts +137 -0
- package/src/windows/service.ts +391 -0
package/src/commands/start.ts
CHANGED
|
@@ -6,34 +6,101 @@ import { execSync } from "node:child_process";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
+
import { getPlatformInfo } from "../platform/detect.js";
|
|
10
|
+
|
|
11
|
+
// Security: Host validation function
|
|
12
|
+
function isValidHost(host: string): boolean {
|
|
13
|
+
// Allow localhost, 0.0.0.0, and valid IP addresses
|
|
14
|
+
const localhostRegex = /^(localhost|127\.0\.0\.1|::1)$/;
|
|
15
|
+
const anyIPRegex = /^(0\.0\.0\.0|::)$/;
|
|
16
|
+
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
17
|
+
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
|
18
|
+
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])?)*$/;
|
|
19
|
+
|
|
20
|
+
if (localhostRegex.test(host) || anyIPRegex.test(host)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (ipv4Regex.test(host)) {
|
|
25
|
+
const parts = host.split('.');
|
|
26
|
+
return parts.every(part => {
|
|
27
|
+
const num = parseInt(part, 10);
|
|
28
|
+
return num >= 0 && num <= 255;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (ipv6Regex.test(host)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (hostnameRegex.test(host) && host.length <= 253) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
9
42
|
|
|
10
43
|
interface StartOptions {
|
|
11
44
|
name?: string;
|
|
12
45
|
port?: string;
|
|
46
|
+
host?: string;
|
|
13
47
|
env?: string[];
|
|
14
48
|
otel?: boolean;
|
|
15
49
|
prometheus?: boolean;
|
|
16
50
|
build?: boolean;
|
|
51
|
+
https?: boolean;
|
|
17
52
|
}
|
|
18
53
|
|
|
19
54
|
export async function startCommand(file: string, options: StartOptions): Promise<void> {
|
|
55
|
+
const platformInfo = getPlatformInfo();
|
|
56
|
+
|
|
57
|
+
// Security: Validate and sanitize file path
|
|
20
58
|
const fullPath = resolve(file);
|
|
21
59
|
if (!existsSync(fullPath)) {
|
|
22
60
|
console.error(`❌ File not found: ${fullPath}`);
|
|
23
61
|
process.exit(1);
|
|
24
62
|
}
|
|
25
|
-
|
|
26
|
-
|
|
63
|
+
|
|
64
|
+
// Security: Prevent directory traversal and ensure file is within allowed paths
|
|
65
|
+
const allowedPaths = [process.cwd(), homedir()];
|
|
66
|
+
const isAllowedPath = allowedPaths.some(allowed => fullPath.startsWith(allowed));
|
|
67
|
+
if (!isAllowedPath) {
|
|
68
|
+
console.error(`❌ Security: File path outside allowed directories: ${fullPath}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Security: Validate and sanitize service name
|
|
73
|
+
const rawServiceName = options.name || basename(fullPath, fullPath.endsWith('.ts') ? '.ts' : '.js');
|
|
74
|
+
const serviceName = rawServiceName.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/^[^a-zA-Z]/, "_").substring(0, 64);
|
|
75
|
+
|
|
76
|
+
// Security: Validate port number
|
|
27
77
|
const port = options.port || "3000";
|
|
78
|
+
const portNum = Number(port);
|
|
79
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
80
|
+
console.error(`❌ Security: Invalid port number: ${port}. Must be 1-65535`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Security: Validate host
|
|
85
|
+
const host = options.host || "localhost";
|
|
86
|
+
if (!isValidHost(host)) {
|
|
87
|
+
console.error(`❌ Security: Invalid host: ${host}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const protocol = options.https ? "https" : "http";
|
|
28
92
|
|
|
29
93
|
// Port warning for privileged ports
|
|
30
|
-
const portNum = Number(port);
|
|
31
94
|
if (portNum < 1024) {
|
|
32
95
|
console.warn(`⚠️ Port ${port} is privileged (< 1024).`);
|
|
33
96
|
console.warn(" Options:");
|
|
34
97
|
console.warn(" - Use port >= 1024 (recommended)");
|
|
35
|
-
|
|
36
|
-
|
|
98
|
+
if (platformInfo.isWindows) {
|
|
99
|
+
console.warn(" - Run as Administrator (not recommended)");
|
|
100
|
+
} else {
|
|
101
|
+
console.warn(" - Run with sudo (not recommended for user services)");
|
|
102
|
+
console.warn(" - Use port forwarding: `sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000`");
|
|
103
|
+
}
|
|
37
104
|
}
|
|
38
105
|
|
|
39
106
|
// Handle TypeScript files and build option
|
|
@@ -77,43 +144,137 @@ export async function startCommand(file: string, options: StartOptions): Promise
|
|
|
77
144
|
auditResult.warning.forEach(issue => console.warn(` - ${issue}`));
|
|
78
145
|
}
|
|
79
146
|
|
|
147
|
+
// Platform-specific service creation
|
|
148
|
+
if (platformInfo.isLinux) {
|
|
149
|
+
await createLinuxService(serviceName, execPath, host, port, protocol, options);
|
|
150
|
+
} else if (platformInfo.isMacOS) {
|
|
151
|
+
await createMacOSService(serviceName, execPath, host, port, protocol, options);
|
|
152
|
+
} else if (platformInfo.isWindows) {
|
|
153
|
+
await createWindowsService(serviceName, execPath, host, port, protocol, options);
|
|
154
|
+
} else {
|
|
155
|
+
console.error(`❌ Platform ${platformInfo.platform} is not supported`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function createLinuxService(serviceName: string, execPath: string, host: string, port: string, protocol: string, options: StartOptions): Promise<void> {
|
|
80
161
|
// Phase 1: Generate hardened systemd unit
|
|
81
162
|
const unitContent = generateSystemdUnit({
|
|
82
163
|
serviceName,
|
|
83
164
|
fullPath: execPath,
|
|
165
|
+
host,
|
|
84
166
|
port,
|
|
167
|
+
protocol,
|
|
85
168
|
env: options.env || [],
|
|
86
169
|
otel: options.otel ?? true,
|
|
87
170
|
prometheus: options.prometheus ?? true,
|
|
88
171
|
});
|
|
89
172
|
|
|
90
|
-
const
|
|
173
|
+
const platformInfo = getPlatformInfo();
|
|
174
|
+
const unitPath = join(platformInfo.serviceDir, `${serviceName}.service`);
|
|
91
175
|
|
|
92
176
|
// Create user systemd directory if it doesn't exist
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log(`📁 Created user systemd directory: ${userSystemdDir}`);
|
|
177
|
+
if (!existsSync(platformInfo.serviceDir)) {
|
|
178
|
+
mkdirSync(platformInfo.serviceDir, { recursive: true });
|
|
179
|
+
console.log(`📁 Created user systemd directory: ${platformInfo.serviceDir}`);
|
|
97
180
|
}
|
|
98
181
|
|
|
99
182
|
try {
|
|
100
|
-
writeFileSync(unitPath, unitContent
|
|
183
|
+
writeFileSync(unitPath, unitContent);
|
|
101
184
|
console.log(`✅ Systemd user unit written to: ${unitPath}`);
|
|
102
|
-
|
|
103
|
-
|
|
185
|
+
|
|
186
|
+
execSync("systemctl --user daemon-reload");
|
|
187
|
+
execSync(`systemctl --user enable ${serviceName}`);
|
|
188
|
+
execSync(`systemctl --user start ${serviceName}`);
|
|
189
|
+
|
|
190
|
+
console.log(`🚀 Service '${serviceName}' started successfully`);
|
|
191
|
+
console.log(` Health: ${protocol}://${host}:${port}/healthz`);
|
|
192
|
+
console.log(` Metrics: ${protocol}://${host}:${port}/metrics`);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(`❌ Failed to start service: ${error}`);
|
|
104
195
|
process.exit(1);
|
|
105
196
|
}
|
|
197
|
+
}
|
|
106
198
|
|
|
107
|
-
|
|
199
|
+
async function createMacOSService(serviceName: string, execPath: string, host: string, port: string, protocol: string, options: StartOptions): Promise<void> {
|
|
200
|
+
const { launchdCommand } = await import("../macos/launchd.js");
|
|
201
|
+
|
|
202
|
+
const envVars: Record<string, string> = {
|
|
203
|
+
PORT: port,
|
|
204
|
+
HOST: host,
|
|
205
|
+
PROTOCOL: protocol,
|
|
206
|
+
NODE_ENV: "production",
|
|
207
|
+
SERVICE_NAME: serviceName,
|
|
208
|
+
...(options.env || []).reduce((acc, env) => {
|
|
209
|
+
const [key, value] = env.split('=');
|
|
210
|
+
if (key && value) acc[key] = value;
|
|
211
|
+
return acc;
|
|
212
|
+
}, {} as Record<string, string>)
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (options.otel) {
|
|
216
|
+
envVars.OTEL_SERVICE_NAME = serviceName;
|
|
217
|
+
envVars.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "http://localhost:4318/v1/traces";
|
|
218
|
+
}
|
|
219
|
+
|
|
108
220
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
221
|
+
await launchdCommand('create', {
|
|
222
|
+
name: `bs9.${serviceName}`,
|
|
223
|
+
file: execPath,
|
|
224
|
+
workingDir: dirname(execPath),
|
|
225
|
+
env: JSON.stringify(envVars),
|
|
226
|
+
autoStart: true,
|
|
227
|
+
keepAlive: true,
|
|
228
|
+
logOut: `${getPlatformInfo().logDir}/${serviceName}.out.log`,
|
|
229
|
+
logErr: `${getPlatformInfo().logDir}/${serviceName}.err.log`
|
|
230
|
+
});
|
|
231
|
+
|
|
112
232
|
console.log(`🚀 Service '${serviceName}' started successfully`);
|
|
113
|
-
console.log(` Health:
|
|
114
|
-
console.log(` Metrics:
|
|
115
|
-
} catch (
|
|
116
|
-
console.error(`❌ Failed to start
|
|
233
|
+
console.log(` Health: ${protocol}://${host}:${port}/healthz`);
|
|
234
|
+
console.log(` Metrics: ${protocol}://${host}:${port}/metrics`);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error(`❌ Failed to start macOS service: ${error}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function createWindowsService(serviceName: string, execPath: string, host: string, port: string, protocol: string, options: StartOptions): Promise<void> {
|
|
242
|
+
const { windowsCommand } = await import("../windows/service.js");
|
|
243
|
+
|
|
244
|
+
const envVars: Record<string, string> = {
|
|
245
|
+
PORT: port,
|
|
246
|
+
HOST: host,
|
|
247
|
+
PROTOCOL: protocol,
|
|
248
|
+
NODE_ENV: "production",
|
|
249
|
+
SERVICE_NAME: serviceName,
|
|
250
|
+
...(options.env || []).reduce((acc, env) => {
|
|
251
|
+
const [key, value] = env.split('=');
|
|
252
|
+
if (key && value) acc[key] = value;
|
|
253
|
+
return acc;
|
|
254
|
+
}, {} as Record<string, string>)
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (options.otel) {
|
|
258
|
+
envVars.OTEL_SERVICE_NAME = serviceName;
|
|
259
|
+
envVars.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "http://localhost:4318/v1/traces";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await windowsCommand('create', {
|
|
264
|
+
name: `BS9_${serviceName}`,
|
|
265
|
+
file: execPath,
|
|
266
|
+
displayName: `BS9 Service: ${serviceName}`,
|
|
267
|
+
description: `BS9 managed service: ${serviceName}`,
|
|
268
|
+
workingDir: dirname(execPath),
|
|
269
|
+
args: ['run', execPath],
|
|
270
|
+
env: JSON.stringify(envVars)
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
console.log(`🚀 Service '${serviceName}' started successfully`);
|
|
274
|
+
console.log(` Health: ${protocol}://${host}:${port}/healthz`);
|
|
275
|
+
console.log(` Metrics: ${protocol}://${host}:${port}/metrics`);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(`❌ Failed to start Windows service: ${error}`);
|
|
117
278
|
process.exit(1);
|
|
118
279
|
}
|
|
119
280
|
}
|
|
@@ -140,6 +301,9 @@ async function securityAudit(filePath: string): Promise<SecurityAuditResult> {
|
|
|
140
301
|
{ pattern: /child_process\.exec\s*\(/, msg: "Unsafe child_process.exec() detected" },
|
|
141
302
|
{ pattern: /require\s*\(\s*["']fs["']\s*\)/, msg: "Direct fs module usage (potential file system access)" },
|
|
142
303
|
{ pattern: /process\.env\.\w+\s*\+\s*["']/, msg: "Potential command injection via env concatenation" },
|
|
304
|
+
{ pattern: /require\s*\(\s*["']child_process["']\s*\)/, msg: "Child process module usage detected" },
|
|
305
|
+
{ pattern: /spawn\s*\(/, msg: "Process spawning detected" },
|
|
306
|
+
{ pattern: /execSync\s*\(/, msg: "Synchronous execution detected" },
|
|
143
307
|
];
|
|
144
308
|
|
|
145
309
|
for (const { pattern, msg } of dangerousPatterns) {
|
|
@@ -164,7 +328,9 @@ async function securityAudit(filePath: string): Promise<SecurityAuditResult> {
|
|
|
164
328
|
interface SystemdUnitOptions {
|
|
165
329
|
serviceName: string;
|
|
166
330
|
fullPath: string;
|
|
331
|
+
host: string;
|
|
167
332
|
port: string;
|
|
333
|
+
protocol: string;
|
|
168
334
|
env: string[];
|
|
169
335
|
otel: boolean;
|
|
170
336
|
prometheus: boolean;
|
|
@@ -173,6 +339,8 @@ interface SystemdUnitOptions {
|
|
|
173
339
|
function generateSystemdUnit(opts: SystemdUnitOptions): string {
|
|
174
340
|
const envVars = [
|
|
175
341
|
`PORT=${opts.port}`,
|
|
342
|
+
`HOST=${opts.host}`,
|
|
343
|
+
`PROTOCOL=${opts.protocol}`,
|
|
176
344
|
`NODE_ENV=production`,
|
|
177
345
|
`SERVICE_NAME=${opts.serviceName}`,
|
|
178
346
|
...opts.env,
|
|
@@ -190,6 +358,7 @@ function generateSystemdUnit(opts: SystemdUnitOptions): string {
|
|
|
190
358
|
return `[Unit]
|
|
191
359
|
Description=BS9 Service: ${opts.serviceName}
|
|
192
360
|
After=network.target
|
|
361
|
+
Documentation=https://github.com/bs9/bs9
|
|
193
362
|
|
|
194
363
|
[Service]
|
|
195
364
|
Type=simple
|
|
@@ -201,6 +370,17 @@ WorkingDirectory=${workingDir}
|
|
|
201
370
|
ExecStart=${bunPath} run ${opts.fullPath}
|
|
202
371
|
${envSection}
|
|
203
372
|
|
|
373
|
+
# Security hardening (user systemd compatible)
|
|
374
|
+
PrivateTmp=true
|
|
375
|
+
ProtectSystem=strict
|
|
376
|
+
ProtectHome=true
|
|
377
|
+
ReadWritePaths=${workingDir}
|
|
378
|
+
UMask=0022
|
|
379
|
+
|
|
380
|
+
# Resource limits
|
|
381
|
+
LimitNOFILE=65536
|
|
382
|
+
LimitNPROC=4096
|
|
383
|
+
|
|
204
384
|
[Install]
|
|
205
385
|
WantedBy=default.target
|
|
206
386
|
`;
|
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
|
}
|