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.
@@ -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
  }
@@ -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) {