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.
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { setTimeout } from "node:timers/promises";
5
+
6
+ interface DepOptions {
7
+ format?: string;
8
+ output?: string;
9
+ }
10
+
11
+ interface ServiceDependency {
12
+ name: string;
13
+ dependsOn: string[];
14
+ status: 'running' | 'stopped' | 'failed' | 'unknown';
15
+ health: 'healthy' | 'unhealthy' | 'unknown';
16
+ port?: number;
17
+ endpoints: string[];
18
+ }
19
+
20
+ interface DependencyGraph {
21
+ services: ServiceDependency[];
22
+ edges: Array<{
23
+ from: string;
24
+ to: string;
25
+ type: 'http' | 'database' | 'message' | 'custom';
26
+ }>;
27
+ }
28
+
29
+ export async function depsCommand(options: DepOptions): Promise<void> {
30
+ console.log('šŸ”— BS9 Service Dependency Visualization');
31
+ console.log('='.repeat(80));
32
+
33
+ try {
34
+ const graph = await buildDependencyGraph();
35
+
36
+ if (options.format === 'dot') {
37
+ const dotOutput = generateDotGraph(graph);
38
+ if (options.output) {
39
+ await Bun.write(options.output, dotOutput);
40
+ console.log(`āœ… Dependency graph saved to: ${options.output}`);
41
+ } else {
42
+ console.log(dotOutput);
43
+ }
44
+ } else if (options.format === 'json') {
45
+ const jsonOutput = JSON.stringify(graph, null, 2);
46
+ if (options.output) {
47
+ await Bun.write(options.output, jsonOutput);
48
+ console.log(`āœ… Dependency graph saved to: ${options.output}`);
49
+ } else {
50
+ console.log(jsonOutput);
51
+ }
52
+ } else {
53
+ displayDependencyGraph(graph);
54
+ }
55
+
56
+ } catch (error) {
57
+ console.error(`āŒ Failed to analyze dependencies: ${error}`);
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ async function buildDependencyGraph(): Promise<DependencyGraph> {
63
+ const services: ServiceDependency[] = [];
64
+ const edges: Array<{from: string; to: string; type: 'http' | 'database' | 'message' | 'custom'}> = [];
65
+
66
+ try {
67
+ // Get all BS9 services
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
+ if (!line.trim()) continue;
73
+
74
+ const match = line.match(/^(?:\s*([ā—\sā—‹]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
75
+ if (!match) continue;
76
+
77
+ const [, statusIndicator, name, loaded, active, sub, description] = match;
78
+
79
+ if (!description.includes("Bun Service:") && !description.includes("BS9 Service:")) continue;
80
+
81
+ const service: ServiceDependency = {
82
+ name,
83
+ dependsOn: [],
84
+ status: getServiceStatus(active, sub),
85
+ health: 'unknown',
86
+ endpoints: [],
87
+ };
88
+
89
+ // Extract port from description
90
+ const portMatch = description.match(/port[=:]?\s*(\d+)/i);
91
+ if (portMatch) {
92
+ service.port = Number(portMatch[1]);
93
+ }
94
+
95
+ // Analyze service for dependencies
96
+ const deps = await analyzeServiceDependencies(name, service.port);
97
+ service.dependsOn = deps.dependsOn;
98
+ service.endpoints = deps.endpoints;
99
+
100
+ services.push(service);
101
+
102
+ // Add edges to graph
103
+ for (const dep of deps.dependsOn) {
104
+ edges.push({
105
+ from: name,
106
+ to: dep,
107
+ type: deps.type,
108
+ });
109
+ }
110
+ }
111
+
112
+ } catch (error) {
113
+ console.error('Error fetching services:', error);
114
+ }
115
+
116
+ return { services, edges };
117
+ }
118
+
119
+ async function analyzeServiceDependencies(serviceName: string, port?: number): Promise<{
120
+ dependsOn: string[];
121
+ endpoints: string[];
122
+ type: 'http' | 'database' | 'message' | 'custom';
123
+ }> {
124
+ const dependsOn: string[] = [];
125
+ const endpoints: string[] = [];
126
+ let type: 'http' | 'database' | 'message' | 'custom' = 'custom';
127
+
128
+ try {
129
+ // Check if service has health endpoints
130
+ if (port) {
131
+ endpoints.push(`http://localhost:${port}/healthz`);
132
+ endpoints.push(`http://localhost:${port}/readyz`);
133
+ endpoints.push(`http://localhost:${port}/metrics`);
134
+
135
+ // Check for common dependency patterns
136
+ try {
137
+ const response = Bun.fetch(`http://localhost:${port}/dependencies`, { timeout: 1000 });
138
+ if (response.ok) {
139
+ const deps = await response.json();
140
+ if (Array.isArray(deps)) {
141
+ dependsOn.push(...deps);
142
+ type = 'http';
143
+ }
144
+ }
145
+ } catch {
146
+ // Service doesn't expose dependencies endpoint
147
+ }
148
+ }
149
+
150
+ // Analyze service files for dependency patterns
151
+ try {
152
+ const servicePath = `/home/xarhang/.config/systemd/user/${serviceName}.service`;
153
+ const serviceContent = Bun.file(servicePath).text();
154
+
155
+ // Look for environment variables that suggest dependencies
156
+ const envMatches = serviceContent.match(/Environment=([^\n]+)/g) || [];
157
+ for (const env of envMatches) {
158
+ if (env.includes('DATABASE_URL') || env.includes('DB_HOST')) {
159
+ dependsOn.push('database');
160
+ type = 'database';
161
+ }
162
+ if (env.includes('REDIS_URL') || env.includes('REDIS_HOST')) {
163
+ dependsOn.push('redis');
164
+ type = 'message';
165
+ }
166
+ if (env.includes('API_URL') || env.includes('SERVICE_URL')) {
167
+ const urlMatch = env.match(/https?:\/\/([^:\/]+)/);
168
+ if (urlMatch) {
169
+ dependsOn.push(urlMatch[1]);
170
+ type = 'http';
171
+ }
172
+ }
173
+ }
174
+ } catch {
175
+ // Service file not accessible
176
+ }
177
+
178
+ } catch (error) {
179
+ console.error(`Error analyzing dependencies for ${serviceName}:`, error);
180
+ }
181
+
182
+ return { dependsOn, endpoints, type };
183
+ }
184
+
185
+ function getServiceStatus(active: string, sub: string): 'running' | 'stopped' | 'failed' | 'unknown' {
186
+ if (active === 'active' && sub === 'running') return 'running';
187
+ if (active === 'inactive') return 'stopped';
188
+ if (active === 'failed' || sub === 'failed') return 'failed';
189
+ return 'unknown';
190
+ }
191
+
192
+ function displayDependencyGraph(graph: DependencyGraph): void {
193
+ console.log('\nšŸ“Š Service Dependencies:');
194
+ console.log('-'.repeat(60));
195
+
196
+ for (const service of graph.services) {
197
+ const statusIcon = getStatusIcon(service.status);
198
+ const healthIcon = getHealthIcon(service.health);
199
+
200
+ console.log(`${statusIcon} ${service.name} ${healthIcon}`);
201
+
202
+ if (service.dependsOn.length > 0) {
203
+ console.log(` └─ Depends on: ${service.dependsOn.join(', ')}`);
204
+ }
205
+
206
+ if (service.endpoints.length > 0) {
207
+ console.log(` └─ Endpoints: ${service.endpoints.slice(0, 2).join(', ')}${service.endpoints.length > 2 ? '...' : ''}`);
208
+ }
209
+
210
+ console.log('');
211
+ }
212
+
213
+ console.log('\nšŸ”— Dependency Relationships:');
214
+ console.log('-'.repeat(60));
215
+
216
+ if (graph.edges.length === 0) {
217
+ console.log('No dependencies found between services.');
218
+ } else {
219
+ for (const edge of graph.edges) {
220
+ const fromService = graph.services.find(s => s.name === edge.from);
221
+ const toService = graph.services.find(s => s.name === edge.to);
222
+
223
+ const fromIcon = fromService ? getStatusIcon(fromService.status) : 'ā“';
224
+ const toIcon = toService ? getStatusIcon(toService.status) : 'ā“';
225
+
226
+ console.log(`${fromIcon} ${edge.from} → ${edge.to} ${toIcon} (${edge.type})`);
227
+ }
228
+ }
229
+
230
+ console.log('\nšŸ“ˆ Summary:');
231
+ console.log(` Total Services: ${graph.services.length}`);
232
+ console.log(` Dependencies: ${graph.edges.length}`);
233
+ console.log(` Running: ${graph.services.filter(s => s.status === 'running').length}`);
234
+ console.log(` Failed: ${graph.services.filter(s => s.status === 'failed').length}`);
235
+ }
236
+
237
+ function generateDotGraph(graph: DependencyGraph): string {
238
+ let dot = 'digraph BS9_Dependencies {\n';
239
+ dot += ' rankdir=LR;\n';
240
+ dot += ' node [shape=box, style=filled];\n\n';
241
+
242
+ // Add nodes
243
+ for (const service of graph.services) {
244
+ const color = getNodeColor(service.status);
245
+ const label = `${service.name}\\n${service.status}`;
246
+ dot += ` "${service.name}" [label="${label}", fillcolor="${color}"];\n`;
247
+ }
248
+
249
+ dot += '\n';
250
+
251
+ // Add edges
252
+ for (const edge of graph.edges) {
253
+ const color = getEdgeColor(edge.type);
254
+ dot += ` "${edge.from}" -> "${edge.to}" [color="${color}", label="${edge.type}"];\n`;
255
+ }
256
+
257
+ dot += '}\n';
258
+
259
+ return dot;
260
+ }
261
+
262
+ function getStatusIcon(status: string): string {
263
+ switch (status) {
264
+ case 'running': return 'āœ…';
265
+ case 'stopped': return 'āøļø';
266
+ case 'failed': return 'āŒ';
267
+ default: return 'ā“';
268
+ }
269
+ }
270
+
271
+ function getHealthIcon(health: string): string {
272
+ switch (health) {
273
+ case 'healthy': return '🟢';
274
+ case 'unhealthy': return 'šŸ”“';
275
+ default: return '⚪';
276
+ }
277
+ }
278
+
279
+ function getNodeColor(status: string): string {
280
+ switch (status) {
281
+ case 'running': return 'lightgreen';
282
+ case 'stopped': return 'lightgray';
283
+ case 'failed': return 'lightcoral';
284
+ default: return 'lightyellow';
285
+ }
286
+ }
287
+
288
+ function getEdgeColor(type: string): string {
289
+ switch (type) {
290
+ case 'http': return 'blue';
291
+ case 'database': return 'green';
292
+ case 'message': return 'orange';
293
+ default: return 'gray';
294
+ }
295
+ }
@@ -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
  }