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.
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { setTimeout } from "node:timers/promises";
5
+
6
+ interface ProfileOptions {
7
+ duration?: string;
8
+ output?: string;
9
+ service?: string;
10
+ interval?: string;
11
+ }
12
+
13
+ interface ProfileData {
14
+ timestamp: number;
15
+ cpu: number;
16
+ memory: number;
17
+ heapUsed: number;
18
+ heapTotal: number;
19
+ external: number;
20
+ eventLoopLag: number;
21
+ activeHandles: number;
22
+ activeRequests: number;
23
+ }
24
+
25
+ interface PerformanceProfile {
26
+ serviceName: string;
27
+ duration: number;
28
+ interval: number;
29
+ samples: ProfileData[];
30
+ summary: {
31
+ avgCpu: number;
32
+ maxCpu: number;
33
+ avgMemory: number;
34
+ maxMemory: number;
35
+ avgHeapUsed: number;
36
+ maxHeapUsed: number;
37
+ avgEventLoopLag: number;
38
+ maxEventLoopLag: number;
39
+ };
40
+ }
41
+
42
+ export async function profileCommand(options: ProfileOptions): Promise<void> {
43
+ const duration = Number(options.duration) || 60; // Default 60 seconds
44
+ const interval = Number(options.interval) || 1000; // Default 1 second
45
+ const serviceName = options.service;
46
+
47
+ if (!serviceName) {
48
+ console.error('❌ Service name is required. Use --service <name>');
49
+ process.exit(1);
50
+ }
51
+
52
+ console.log(`📊 Performance Profiling for Service: ${serviceName}`);
53
+ console.log(`⏱️ Duration: ${duration}s | Interval: ${interval}ms`);
54
+ console.log('='.repeat(80));
55
+
56
+ try {
57
+ // Check if service is running
58
+ const serviceStatus = checkServiceStatus(serviceName);
59
+ if (serviceStatus !== 'active') {
60
+ console.error(`❌ Service '${serviceName}' is not running (status: ${serviceStatus})`);
61
+ process.exit(1);
62
+ }
63
+
64
+ // Get service port for metrics
65
+ const port = getServicePort(serviceName);
66
+ if (!port) {
67
+ console.error(`❌ Could not determine port for service '${serviceName}'`);
68
+ process.exit(1);
69
+ }
70
+
71
+ console.log(`🔍 Collecting metrics from http://localhost:${port}/metrics`);
72
+ console.log('Press Ctrl+C to stop profiling early\n');
73
+
74
+ const profile = await collectPerformanceData(serviceName, port, duration, interval);
75
+
76
+ // Display results
77
+ displayProfileResults(profile);
78
+
79
+ // Save to file if requested
80
+ if (options.output) {
81
+ await saveProfileData(profile, options.output);
82
+ console.log(`💾 Profile data saved to: ${options.output}`);
83
+ }
84
+
85
+ } catch (error) {
86
+ console.error(`❌ Failed to profile service: ${error}`);
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ function checkServiceStatus(serviceName: string): string {
92
+ try {
93
+ const output = execSync(`systemctl --user is-active ${serviceName}`, { encoding: "utf-8" }).trim();
94
+ return output;
95
+ } catch {
96
+ return 'unknown';
97
+ }
98
+ }
99
+
100
+ function getServicePort(serviceName: string): number | null {
101
+ try {
102
+ const servicePath = `/home/xarhang/.config/systemd/user/${serviceName}.service`;
103
+ const serviceContent = execSync(`cat ${servicePath}`, { encoding: "utf-8" });
104
+
105
+ // Extract port from environment variables
106
+ const portMatch = serviceContent.match(/Environment=.*PORT=(\d+)/);
107
+ if (portMatch) {
108
+ return Number(portMatch[1]);
109
+ }
110
+
111
+ // Extract port from description
112
+ const descMatch = serviceContent.match(/port[=:]?\s*(\d+)/i);
113
+ if (descMatch) {
114
+ return Number(descMatch[1]);
115
+ }
116
+
117
+ return null;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ async function collectPerformanceData(
124
+ serviceName: string,
125
+ port: number,
126
+ duration: number,
127
+ interval: number
128
+ ): Promise<PerformanceProfile> {
129
+ const samples: ProfileData[] = [];
130
+ const startTime = Date.now();
131
+ const endTime = startTime + (duration * 1000);
132
+
133
+ while (Date.now() < endTime) {
134
+ try {
135
+ const sample = await collectSample(port);
136
+ samples.push(sample);
137
+
138
+ // Show progress
139
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
140
+ const progress = Math.round((elapsed / duration) * 100);
141
+ process.stdout.write(`\r⏳ Progress: ${elapsed}s/${duration}s (${progress}%) | Samples: ${samples.length}`);
142
+
143
+ await setTimeout(interval);
144
+ } catch (error) {
145
+ console.error(`\n⚠️ Failed to collect sample: ${error}`);
146
+ break;
147
+ }
148
+ }
149
+
150
+ console.log('\n'); // New line after progress
151
+
152
+ // Calculate summary
153
+ const summary = calculateSummary(samples);
154
+
155
+ return {
156
+ serviceName,
157
+ duration,
158
+ interval,
159
+ samples,
160
+ summary,
161
+ };
162
+ }
163
+
164
+ async function collectSample(port: number): Promise<ProfileData> {
165
+ const response = await fetch(`http://localhost:${port}/metrics`);
166
+ if (!response.ok) {
167
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
168
+ }
169
+
170
+ const metricsText = await response.text();
171
+ const metrics = parsePrometheusMetrics(metricsText);
172
+
173
+ return {
174
+ timestamp: Date.now(),
175
+ cpu: metrics.cpu || 0,
176
+ memory: metrics.memory || 0,
177
+ heapUsed: metrics.heapUsed || 0,
178
+ heapTotal: metrics.heapTotal || 0,
179
+ external: metrics.external || 0,
180
+ eventLoopLag: metrics.eventLoopLag || 0,
181
+ activeHandles: metrics.activeHandles || 0,
182
+ activeRequests: metrics.activeRequests || 0,
183
+ };
184
+ }
185
+
186
+ function parsePrometheusMetrics(metricsText: string): Record<string, number> {
187
+ const metrics: Record<string, number> = {};
188
+ const lines = metricsText.split('\n');
189
+
190
+ for (const line of lines) {
191
+ if (line.startsWith('#') || !line.trim()) continue;
192
+
193
+ const match = line.match(/^(.+)\s+([\d.]+)$/);
194
+ if (!match) continue;
195
+
196
+ const [, name, value] = match;
197
+
198
+ // Map Prometheus metrics to our internal names
199
+ switch (name) {
200
+ case 'process_cpu_seconds_total':
201
+ metrics.cpu = parseFloat(value);
202
+ break;
203
+ case 'process_resident_memory_bytes':
204
+ metrics.memory = parseFloat(value);
205
+ break;
206
+ case 'nodejs_heap_size_used_bytes':
207
+ metrics.heapUsed = parseFloat(value);
208
+ break;
209
+ case 'nodejs_heap_size_total_bytes':
210
+ metrics.heapTotal = parseFloat(value);
211
+ break;
212
+ case 'nodejs_external_memory_bytes':
213
+ metrics.external = parseFloat(value);
214
+ break;
215
+ case 'nodejs_eventloop_lag_seconds':
216
+ metrics.eventLoopLag = parseFloat(value) * 1000; // Convert to ms
217
+ break;
218
+ case 'nodejs_active_handles_total':
219
+ metrics.activeHandles = parseFloat(value);
220
+ break;
221
+ case 'nodejs_active_requests_total':
222
+ metrics.activeRequests = parseFloat(value);
223
+ break;
224
+ }
225
+ }
226
+
227
+ return metrics;
228
+ }
229
+
230
+ function calculateSummary(samples: ProfileData[]): PerformanceProfile['summary'] {
231
+ if (samples.length === 0) {
232
+ return {
233
+ avgCpu: 0, maxCpu: 0,
234
+ avgMemory: 0, maxMemory: 0,
235
+ avgHeapUsed: 0, maxHeapUsed: 0,
236
+ avgEventLoopLag: 0, maxEventLoopLag: 0,
237
+ };
238
+ }
239
+
240
+ const sum = samples.reduce((acc, sample) => ({
241
+ cpu: acc.cpu + sample.cpu,
242
+ memory: acc.memory + sample.memory,
243
+ heapUsed: acc.heapUsed + sample.heapUsed,
244
+ eventLoopLag: acc.eventLoopLag + sample.eventLoopLag,
245
+ }), { cpu: 0, memory: 0, heapUsed: 0, eventLoopLag: 0 });
246
+
247
+ const count = samples.length;
248
+
249
+ return {
250
+ avgCpu: sum.cpu / count,
251
+ maxCpu: Math.max(...samples.map(s => s.cpu)),
252
+ avgMemory: sum.memory / count,
253
+ maxMemory: Math.max(...samples.map(s => s.memory)),
254
+ avgHeapUsed: sum.heapUsed / count,
255
+ maxHeapUsed: Math.max(...samples.map(s => s.heapUsed)),
256
+ avgEventLoopLag: sum.eventLoopLag / count,
257
+ maxEventLoopLag: Math.max(...samples.map(s => s.eventLoopLag)),
258
+ };
259
+ }
260
+
261
+ function displayProfileResults(profile: PerformanceProfile): void {
262
+ console.log('\n📊 Performance Profile Results');
263
+ console.log('='.repeat(80));
264
+
265
+ console.log(`\n🔧 Service: ${profile.serviceName}`);
266
+ console.log(`⏱️ Duration: ${profile.duration}s | Samples: ${profile.samples.length}`);
267
+ console.log(`📏 Interval: ${profile.interval}ms`);
268
+
269
+ console.log('\n💾 Memory Usage:');
270
+ console.log(` Average: ${formatBytes(profile.summary.avgMemory)} | Peak: ${formatBytes(profile.summary.maxMemory)}`);
271
+ console.log(` Heap Avg: ${formatBytes(profile.summary.avgHeapUsed)} | Heap Peak: ${formatBytes(profile.summary.maxHeapUsed)}`);
272
+ console.log(` External: ${formatBytes(profile.samples[0]?.external || 0)}`);
273
+
274
+ console.log('\n⚡ Performance:');
275
+ console.log(` CPU Avg: ${profile.summary.avgCpu.toFixed(2)}s | Peak: ${profile.summary.maxCpu.toFixed(2)}s`);
276
+ console.log(` Event Loop Avg: ${profile.summary.avgEventLoopLag.toFixed(2)}ms | Peak: ${profile.summary.maxEventLoopLag.toFixed(2)}ms`);
277
+
278
+ console.log('\n🔗 Resources:');
279
+ const lastSample = profile.samples[profile.samples.length - 1];
280
+ if (lastSample) {
281
+ console.log(` Active Handles: ${lastSample.activeHandles} | Active Requests: ${lastSample.activeRequests}`);
282
+ }
283
+
284
+ // Performance recommendations
285
+ console.log('\n💡 Performance Insights:');
286
+
287
+ if (profile.summary.maxMemory > 500 * 1024 * 1024) { // > 500MB
288
+ console.log(' ⚠️ High memory usage detected - consider memory optimization');
289
+ }
290
+
291
+ if (profile.summary.maxEventLoopLag > 100) { // > 100ms
292
+ console.log(' ⚠️ High event loop lag detected - consider optimizing async operations');
293
+ }
294
+
295
+ if (profile.samples.length > 0) {
296
+ const firstSample = profile.samples[0];
297
+ if (firstSample.heapTotal > 0 && profile.summary.avgHeapUsed / firstSample.heapTotal > 0.8) { // > 80%
298
+ console.log(' ⚠️ High heap usage ratio - potential memory leak');
299
+ }
300
+ }
301
+
302
+ if (profile.samples.length < profile.duration * 1000 / profile.interval * 0.9) {
303
+ console.log(' ⚠️ Some samples failed to collect - check service health');
304
+ }
305
+
306
+ console.log('\n📈 Sample Timeline (last 10 samples):');
307
+ const recentSamples = profile.samples.slice(-10);
308
+ for (let i = 0; i < recentSamples.length; i++) {
309
+ const sample = recentSamples[i];
310
+ const time = new Date(sample.timestamp).toLocaleTimeString();
311
+ console.log(` ${time} | CPU: ${sample.cpu.toFixed(2)}s | Memory: ${formatBytes(sample.memory)} | Heap: ${formatBytes(sample.heapUsed)}`);
312
+ }
313
+ }
314
+
315
+ function formatBytes(bytes: number): string {
316
+ if (bytes === 0) return '0B';
317
+ const k = 1024;
318
+ const sizes = ['B', 'KB', 'MB', 'GB'];
319
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
320
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`;
321
+ }
322
+
323
+ async function saveProfileData(profile: PerformanceProfile, filename: string): Promise<void> {
324
+ const data = {
325
+ metadata: {
326
+ serviceName: profile.serviceName,
327
+ duration: profile.duration,
328
+ interval: profile.interval,
329
+ sampleCount: profile.samples.length,
330
+ timestamp: new Date().toISOString(),
331
+ },
332
+ summary: profile.summary,
333
+ samples: profile.samples,
334
+ };
335
+
336
+ const json = JSON.stringify(data, null, 2);
337
+ await Bun.write(filename, json);
338
+ }
@@ -1,13 +1,38 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { execSync } from "node:child_process";
4
+ import { getPlatformInfo } from "../platform/detect.js";
5
+
6
+ // Security: Service name validation
7
+ function isValidServiceName(name: string): boolean {
8
+ const validPattern = /^[a-zA-Z0-9._-]+$/;
9
+ return validPattern.test(name) && name.length <= 64 && !name.includes('..') && !name.includes('/');
10
+ }
4
11
 
5
12
  export async function restartCommand(name: string): Promise<void> {
13
+ // Security: Validate service name
14
+ if (!isValidServiceName(name)) {
15
+ console.error(`❌ Security: Invalid service name: ${name}`);
16
+ process.exit(1);
17
+ }
18
+
19
+ const platformInfo = getPlatformInfo();
20
+
6
21
  try {
7
- execSync(`systemctl --user restart ${name}`, { stdio: "inherit" });
8
- console.log(`🔄 User service '${name}' restarted`);
22
+ if (platformInfo.isLinux) {
23
+ // Security: Use shell escaping to prevent injection
24
+ const escapedName = name.replace(/[^a-zA-Z0-9._-]/g, '');
25
+ execSync(`systemctl --user restart "${escapedName}"`, { stdio: "inherit" });
26
+ console.log(`🔄 User service '${name}' restarted`);
27
+ } else if (platformInfo.isMacOS) {
28
+ const { launchdCommand } = await import("../macos/launchd.js");
29
+ await launchdCommand('restart', { name: `bs9.${name}` });
30
+ } else if (platformInfo.isWindows) {
31
+ const { windowsCommand } = await import("../windows/service.js");
32
+ await windowsCommand('restart', { name: `BS9_${name}` });
33
+ }
9
34
  } catch (err) {
10
- console.error(`❌ Failed to restart user service '${name}': ${err}`);
35
+ console.error(`❌ Failed to restart service '${name}': ${err}`);
11
36
  process.exit(1);
12
37
  }
13
38
  }
@@ -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
  `;