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.
@@ -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
- const serviceName = options.name || basename(fullPath, fullPath.endsWith('.ts') ? '.ts' : '.js').replace(/[^a-zA-Z0-9-_]/g, "_");
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
- console.warn(" - Run with sudo (not recommended for user services)");
36
- console.warn(" - Use port forwarding: `sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000`");
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 unitPath = join(homedir(), ".config/systemd/user", `${serviceName}.service`);
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
- const userSystemdDir = join(homedir(), ".config/systemd/user");
94
- if (!existsSync(userSystemdDir)) {
95
- mkdirSync(userSystemdDir, { recursive: true });
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, { encoding: "utf-8" });
183
+ writeFileSync(unitPath, unitContent);
101
184
  console.log(`✅ Systemd user unit written to: ${unitPath}`);
102
- } catch (err) {
103
- console.error(`❌ Failed to write systemd user unit: ${err}`);
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
- // Reload and start using user systemd
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
- execSync("systemctl --user daemon-reload", { stdio: "inherit" });
110
- execSync(`systemctl --user enable ${serviceName}`, { stdio: "inherit" });
111
- execSync(`systemctl --user start ${serviceName}`, { stdio: "inherit" });
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: http://localhost:${port}/healthz`);
114
- console.log(` Metrics: http://localhost:${port}/metrics`);
115
- } catch (err) {
116
- console.error(`❌ Failed to start user service: ${err}`);
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
  `;
@@ -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 doStatus = () => {
24
- try {
25
- // Get all bsn-managed user services
26
- const listOutput = execSync("systemctl --user list-units --type=service --no-pager --no-legend", { encoding: "utf-8" });
27
- const lines = listOutput.split("\n").filter(line => line.includes(".service"));
28
-
29
- // Debug: show all lines
30
- // console.error("All service lines:", lines);
31
-
32
- const services: ServiceStatus[] = [];
33
-
34
- for (const line of lines) {
35
- // Skip empty lines
36
- if (!line.trim()) continue;
37
-
38
- // Try to match the line format - handle both ● and regular spaces
39
- const match = line.match(/^(?:\s*([●\s○]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
40
- if (!match) {
41
- continue;
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
- // Get additional metrics
60
- try {
61
- const showOutput = execSync(`systemctl --user show ${name} -p CPUUsageNSec MemoryCurrent ActiveEnterTimestamp TasksCurrent`, { encoding: "utf-8" });
62
- const cpuMatch = showOutput.match(/CPUUsageNSec=(\d+)/);
63
- const memMatch = showOutput.match(/MemoryCurrent=(\d+)/);
64
- const timeMatch = showOutput.match(/ActiveEnterTimestamp=(.+)/);
65
- const tasksMatch = showOutput.match(/TasksCurrent=(\d+)/);
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
- services.push(status);
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
- // Display table
79
- console.clear();
80
- console.log("🔍 BSN Service Status\n");
75
+ const [, statusIndicator, name, loaded, active, sub, description] = match;
81
76
 
82
- if (services.length === 0) {
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
- // Header
88
- console.log(`${"SERVICE".padEnd(20)} ${"STATE".padEnd(12)} ${"CPU".padEnd(8)} ${"MEMORY".padEnd(10)} ${"UPTIME".padEnd(12)} ${"TASKS".padEnd(6)} DESCRIPTION`);
89
- console.log("-".repeat(90));
79
+ const status: ServiceStatus = {
80
+ name,
81
+ loaded,
82
+ active,
83
+ sub,
84
+ description,
85
+ };
90
86
 
91
- for (const svc of services) {
92
- const state = `${svc.active}/${svc.sub}`;
93
- console.log(
94
- `${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}`
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
- console.log("\n📊 SRE Metrics Summary:");
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 unit = 0;
136
- while (size >= 1024 && unit < units.length - 1) {
168
+ let unitIndex = 0;
169
+
170
+ while (size >= 1024 && unitIndex < units.length - 1) {
137
171
  size /= 1024;
138
- unit++;
172
+ unitIndex++;
139
173
  }
140
- return `${size.toFixed(1)}${units[unit]}`;
174
+
175
+ return `${size.toFixed(1)}${units[unitIndex]}`;
141
176
  }
142
177
 
143
- function parseMemory(str: string): number {
144
- const match = str.match(/^([\d.]+)(B|KB|MB|GB)$/);
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 mult = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 }[unit] || 1;
148
- return Number(value) * mult;
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
- try {
153
- const date = new Date(timestamp);
154
- const now = new Date();
155
- const diff = now.getTime() - date.getTime();
156
- const hours = Math.floor(diff / (1000 * 60 * 60));
157
- const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
158
- return `${hours}h ${minutes}m`;
159
- } catch {
160
- return "-";
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
  }
@@ -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
- execSync(`systemctl --user stop ${name}`, { stdio: "inherit" });
8
- console.log(`🛑 User service '${name}' stopped`);
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 user service '${name}': ${err}`);
38
+ console.error(`❌ Failed to stop service '${name}': ${err}`);
11
39
  process.exit(1);
12
40
  }
13
41
  }