bs9 1.0.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,194 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { homedir } from "node:os";
6
+
7
+ interface AlertConfig {
8
+ enabled: boolean;
9
+ webhookUrl?: string;
10
+ thresholds: {
11
+ cpu: number; // percentage
12
+ memory: number; // percentage
13
+ errorRate: number; // percentage
14
+ uptime: number; // percentage
15
+ };
16
+ cooldown: number; // seconds between alerts
17
+ services: {
18
+ [serviceName: string]: {
19
+ enabled: boolean;
20
+ customThresholds?: Partial<AlertConfig['thresholds']>;
21
+ };
22
+ };
23
+ }
24
+
25
+ class AlertManager {
26
+ private configPath: string;
27
+ private config: AlertConfig;
28
+ private lastAlerts: Map<string, number> = new Map();
29
+
30
+ constructor() {
31
+ this.configPath = join(homedir(), ".config", "bs9", "alerts.json");
32
+ this.config = this.loadConfig();
33
+ }
34
+
35
+ private loadConfig(): AlertConfig {
36
+ const defaultConfig: AlertConfig = {
37
+ enabled: true,
38
+ thresholds: {
39
+ cpu: 80,
40
+ memory: 85,
41
+ errorRate: 5,
42
+ uptime: 95,
43
+ },
44
+ cooldown: 300, // 5 minutes
45
+ services: {},
46
+ };
47
+
48
+ if (existsSync(this.configPath)) {
49
+ try {
50
+ const content = readFileSync(this.configPath, 'utf-8');
51
+ return { ...defaultConfig, ...JSON.parse(content) };
52
+ } catch (error) {
53
+ console.error('Failed to load alert config, using defaults:', error);
54
+ }
55
+ }
56
+
57
+ // Create default config file
58
+ this.saveConfig(defaultConfig);
59
+ return defaultConfig;
60
+ }
61
+
62
+ private saveConfig(config: AlertConfig): void {
63
+ try {
64
+ const configDir = join(homedir(), ".config", "bs9");
65
+ if (!existsSync(configDir)) {
66
+ mkdirSync(configDir, { recursive: true });
67
+ }
68
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2));
69
+ } catch (error) {
70
+ console.error('Failed to save alert config:', error);
71
+ }
72
+ }
73
+
74
+ updateConfig(updates: Partial<AlertConfig>): void {
75
+ this.config = { ...this.config, ...updates };
76
+ this.saveConfig(this.config);
77
+ }
78
+
79
+ setServiceAlert(serviceName: string, enabled: boolean, customThresholds?: Partial<AlertConfig['thresholds']>): void {
80
+ this.config.services[serviceName] = {
81
+ enabled,
82
+ customThresholds,
83
+ };
84
+ this.saveConfig(this.config);
85
+ }
86
+
87
+ async checkAlerts(serviceName: string, metrics: {
88
+ cpu: number;
89
+ memory: number;
90
+ health: 'healthy' | 'unhealthy' | 'unknown';
91
+ uptime: number;
92
+ }): Promise<void> {
93
+ if (!this.config.enabled) return;
94
+
95
+ const serviceConfig = this.config.services[serviceName];
96
+ if (serviceConfig && !serviceConfig.enabled) return;
97
+
98
+ const thresholds = {
99
+ ...this.config.thresholds,
100
+ ...serviceConfig?.customThresholds,
101
+ };
102
+
103
+ const alerts: string[] = [];
104
+
105
+ // Check CPU threshold
106
+ if (metrics.cpu > thresholds.cpu) {
107
+ alerts.push(`CPU usage (${metrics.cpu}%) exceeds threshold (${thresholds.cpu}%)`);
108
+ }
109
+
110
+ // Check Memory threshold
111
+ if (metrics.memory > thresholds.memory) {
112
+ alerts.push(`Memory usage (${metrics.memory}%) exceeds threshold (${thresholds.memory}%)`);
113
+ }
114
+
115
+ // Check Uptime threshold
116
+ if (metrics.uptime < thresholds.uptime) {
117
+ alerts.push(`Uptime (${metrics.uptime}%) below threshold (${thresholds.uptime}%)`);
118
+ }
119
+
120
+ // Check health
121
+ if (metrics.health === 'unhealthy') {
122
+ alerts.push('Service health check failed');
123
+ }
124
+
125
+ if (alerts.length === 0) return;
126
+
127
+ // Check cooldown
128
+ const now = Date.now();
129
+ const lastAlert = this.lastAlerts.get(serviceName) || 0;
130
+
131
+ if (now - lastAlert < this.config.cooldown * 1000) {
132
+ return; // Still in cooldown period
133
+ }
134
+
135
+ // Send alert
136
+ await this.sendAlert(serviceName, alerts);
137
+ this.lastAlerts.set(serviceName, now);
138
+ }
139
+
140
+ private async sendAlert(serviceName: string, alerts: string[]): Promise<void> {
141
+ const message = `🚨 BS9 Alert for ${serviceName}:\n${alerts.join('\n')}`;
142
+
143
+ console.error(message);
144
+
145
+ if (this.config.webhookUrl) {
146
+ try {
147
+ const response = await fetch(this.config.webhookUrl, {
148
+ method: 'POST',
149
+ headers: {
150
+ 'Content-Type': 'application/json',
151
+ },
152
+ body: JSON.stringify({
153
+ service: serviceName,
154
+ alerts,
155
+ timestamp: new Date().toISOString(),
156
+ severity: 'warning',
157
+ }),
158
+ });
159
+
160
+ if (!response.ok) {
161
+ console.error(`Failed to send webhook alert: ${response.statusText}`);
162
+ }
163
+ } catch (error) {
164
+ console.error('Failed to send webhook alert:', error);
165
+ }
166
+ }
167
+ }
168
+
169
+ getConfig(): AlertConfig {
170
+ return { ...this.config };
171
+ }
172
+
173
+ testWebhook(): Promise<boolean> {
174
+ if (!this.config.webhookUrl) {
175
+ return Promise.resolve(false);
176
+ }
177
+
178
+ return fetch(this.config.webhookUrl, {
179
+ method: 'POST',
180
+ headers: {
181
+ 'Content-Type': 'application/json',
182
+ },
183
+ body: JSON.stringify({
184
+ test: true,
185
+ message: 'BS9 Alert System Test',
186
+ timestamp: new Date().toISOString(),
187
+ }),
188
+ })
189
+ .then(response => response.ok)
190
+ .catch(() => false);
191
+ }
192
+ }
193
+
194
+ export { AlertManager, AlertConfig };
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { AlertManager } from "../alerting/config.js";
4
+
5
+ interface AlertOptions {
6
+ enable?: boolean;
7
+ disable?: boolean;
8
+ webhook?: string;
9
+ cpu?: string;
10
+ memory?: string;
11
+ errorRate?: string;
12
+ uptime?: string;
13
+ cooldown?: string;
14
+ service?: string;
15
+ list?: boolean;
16
+ test?: boolean;
17
+ }
18
+
19
+ export async function alertCommand(options: AlertOptions): Promise<void> {
20
+ const alertManager = new AlertManager();
21
+
22
+ if (options.list) {
23
+ const config = alertManager.getConfig();
24
+ console.log('šŸ”” BS9 Alert Configuration');
25
+ console.log('='.repeat(40));
26
+ console.log(`Enabled: ${config.enabled}`);
27
+ if (config.webhookUrl) {
28
+ console.log(`Webhook: ${config.webhookUrl}`);
29
+ }
30
+ console.log('');
31
+ console.log('Thresholds:');
32
+ console.log(` CPU: ${config.thresholds.cpu}%`);
33
+ console.log(` Memory: ${config.thresholds.memory}%`);
34
+ console.log(` Error Rate: ${config.thresholds.errorRate}%`);
35
+ console.log(` Uptime: ${config.thresholds.uptime}%`);
36
+ console.log(` Cooldown: ${config.cooldown}s`);
37
+ console.log('');
38
+
39
+ if (Object.keys(config.services).length > 0) {
40
+ console.log('Service-specific configs:');
41
+ for (const [serviceName, serviceConfig] of Object.entries(config.services)) {
42
+ console.log(` ${serviceName}:`);
43
+ console.log(` Enabled: ${serviceConfig.enabled}`);
44
+ if (serviceConfig.customThresholds) {
45
+ console.log(` Custom thresholds:`, serviceConfig.customThresholds);
46
+ }
47
+ }
48
+ }
49
+ return;
50
+ }
51
+
52
+ if (options.test) {
53
+ console.log('🧪 Testing alert webhook...');
54
+ const success = await alertManager.testWebhook();
55
+ if (success) {
56
+ console.log('āœ… Webhook test successful');
57
+ } else {
58
+ console.log('āŒ Webhook test failed');
59
+ }
60
+ return;
61
+ }
62
+
63
+ const updates: any = {};
64
+
65
+ if (options.enable !== undefined) {
66
+ updates.enabled = options.enable;
67
+ }
68
+
69
+ if (options.webhook) {
70
+ updates.webhookUrl = options.webhook;
71
+ }
72
+
73
+ const thresholdUpdates: any = {};
74
+ if (options.cpu) thresholdUpdates.cpu = Number(options.cpu);
75
+ if (options.memory) thresholdUpdates.memory = Number(options.memory);
76
+ if (options.errorRate) thresholdUpdates.errorRate = Number(options.errorRate);
77
+ if (options.uptime) thresholdUpdates.uptime = Number(options.uptime);
78
+ if (options.cooldown) thresholdUpdates.cooldown = Number(options.cooldown);
79
+
80
+ if (Object.keys(thresholdUpdates).length > 0) {
81
+ updates.thresholds = {
82
+ ...alertManager.getConfig().thresholds,
83
+ ...thresholdUpdates,
84
+ };
85
+ }
86
+
87
+ if (options.service) {
88
+ const serviceName = options.service;
89
+ const enabled = options.enable !== false;
90
+ alertManager.setServiceAlert(serviceName, enabled, Object.keys(thresholdUpdates).length > 0 ? thresholdUpdates : undefined);
91
+ console.log(`āœ… Updated alert config for service: ${serviceName}`);
92
+ } else if (Object.keys(updates).length > 0) {
93
+ alertManager.updateConfig(updates);
94
+ console.log('āœ… Updated global alert configuration');
95
+ } else {
96
+ console.log('ā„¹ļø No changes specified. Use --help for options.');
97
+ }
98
+ }
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { MetricsStorage } from "../storage/metrics.js";
4
+ import { writeFileSync } from "node:fs";
5
+
6
+ interface ExportOptions {
7
+ format?: string;
8
+ hours?: string;
9
+ output?: string;
10
+ service?: string;
11
+ }
12
+
13
+ export async function exportCommand(options: ExportOptions): Promise<void> {
14
+ const storage = new MetricsStorage();
15
+ const format = options.format || 'json';
16
+ const hours = Number(options.hours) || 24;
17
+ const serviceName = options.service;
18
+
19
+ console.log(`šŸ“Š Exporting BS9 metrics (${format.toUpperCase()} format)`);
20
+
21
+ try {
22
+ let data: string;
23
+
24
+ if (serviceName) {
25
+ // Export specific service metrics
26
+ const serviceMetrics = storage.getServiceMetrics(serviceName, hours);
27
+ data = format === 'csv' ? serviceMetricsToCsv(serviceMetrics, serviceName) : JSON.stringify(serviceMetrics, null, 2);
28
+ console.log(`šŸ“ˆ Exporting metrics for service: ${serviceName}`);
29
+ } else {
30
+ // Export all metrics
31
+ data = storage.exportData(format as 'json' | 'csv');
32
+ console.log(`šŸ“ˆ Exporting all metrics for last ${hours} hours`);
33
+ }
34
+
35
+ const outputFile = options.output || `bs9-metrics-${Date.now()}.${format}`;
36
+ writeFileSync(outputFile, data);
37
+
38
+ console.log(`āœ… Metrics exported to: ${outputFile}`);
39
+ console.log(` Size: ${(data.length / 1024).toFixed(2)} KB`);
40
+ console.log(` Records: ${serviceName ? storage.getServiceMetrics(serviceName, hours).length : 'all services'}`);
41
+
42
+ } catch (error) {
43
+ console.error(`āŒ Failed to export metrics: ${error}`);
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ function serviceMetricsToCsv(metrics: any[], serviceName: string): string {
49
+ const headers = ['timestamp', 'service_name', 'cpu_ms', 'memory_bytes', 'uptime', 'tasks', 'health', 'state'];
50
+ const rows = [headers.join(',')];
51
+
52
+ for (const metric of metrics) {
53
+ const cpuMatch = metric.cpu.match(/([\d.]+)ms/);
54
+ const cpuMs = cpuMatch ? cpuMatch[1] : '0';
55
+
56
+ rows.push([
57
+ new Date().toISOString(),
58
+ serviceName,
59
+ cpuMs,
60
+ metric.memory.toString(),
61
+ metric.uptime,
62
+ metric.tasks.toString(),
63
+ metric.health,
64
+ metric.state,
65
+ ].join(','));
66
+ }
67
+
68
+ return rows.join('\n');
69
+ }
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync } from "node:child_process";
4
+
5
+ interface LogsOptions {
6
+ follow?: boolean;
7
+ lines?: string;
8
+ }
9
+
10
+ export async function logsCommand(name: string, options: LogsOptions): Promise<void> {
11
+ try {
12
+ const args = ["--no-pager"];
13
+ if (options.follow) args.push("-f");
14
+ if (options.lines) args.push("-n", options.lines);
15
+
16
+ const cmd = `journalctl --user ${args.join(" ")} -u ${name}.service`;
17
+ execSync(cmd, { stdio: "inherit" });
18
+ } catch (err) {
19
+ console.error(`āŒ Failed to fetch logs for user service '${name}': ${err}`);
20
+ process.exit(1);
21
+ }
22
+ }
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { setTimeout } from "node:timers/promises";
5
+
6
+ interface MonitOptions {
7
+ refresh?: string;
8
+ }
9
+
10
+ interface ServiceMetrics {
11
+ name: string;
12
+ loaded: string;
13
+ active: string;
14
+ sub: string;
15
+ state: string;
16
+ cpu: string;
17
+ memory: string;
18
+ uptime: string;
19
+ tasks: string;
20
+ description: string;
21
+ health?: string;
22
+ lastError?: string;
23
+ }
24
+
25
+ export async function monitCommand(options: MonitOptions): Promise<void> {
26
+ const refreshInterval = Number(options.refresh) || 2;
27
+
28
+ // Clear screen and setup
29
+ process.stdout.write('\x1b[2J\x1b[H');
30
+ console.log('šŸ” BS9 Real-time Monitoring Dashboard');
31
+ console.log('='.repeat(80));
32
+ console.log(`Refresh: ${refreshInterval}s | Press Ctrl+C to exit`);
33
+ console.log('');
34
+
35
+ const getMetrics = (): ServiceMetrics[] => {
36
+ try {
37
+ const listOutput = execSync("systemctl --user list-units --type=service --no-pager --no-legend", { encoding: "utf-8" });
38
+ const lines = listOutput.split("\n").filter(line => line.includes(".service"));
39
+
40
+ const services: ServiceMetrics[] = [];
41
+
42
+ for (const line of lines) {
43
+ if (!line.trim()) continue;
44
+
45
+ const match = line.match(/^(?:\s*([ā—\sā—‹]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
46
+ if (!match) continue;
47
+
48
+ const [, statusIndicator, name, loaded, active, sub, description] = match;
49
+
50
+ if (!description.includes("Bun Service:") && !description.includes("BS9 Service:")) continue;
51
+
52
+ const service: ServiceMetrics = {
53
+ name,
54
+ loaded,
55
+ active,
56
+ sub,
57
+ state: `${active}/${sub}`,
58
+ description,
59
+ cpu: '-',
60
+ memory: '-',
61
+ uptime: '-',
62
+ tasks: '-',
63
+ };
64
+
65
+ // Get additional metrics
66
+ try {
67
+ const showOutput = execSync(`systemctl --user show ${name} -p CPUUsageNSec MemoryCurrent ActiveEnterTimestamp TasksCurrent State`, { encoding: "utf-8" });
68
+ const cpuMatch = showOutput.match(/CPUUsageNSec=(\d+)/);
69
+ const memMatch = showOutput.match(/MemoryCurrent=(\d+)/);
70
+ const timeMatch = showOutput.match(/ActiveEnterTimestamp=(.+)/);
71
+ const tasksMatch = showOutput.match(/TasksCurrent=(\d+)/);
72
+ const stateMatch = showOutput.match(/State=(.+)/);
73
+
74
+ if (cpuMatch) {
75
+ const cpuNs = Number(cpuMatch[1]);
76
+ service.cpu = `${(cpuNs / 1000000).toFixed(1)}ms`;
77
+ }
78
+ if (memMatch) {
79
+ const memBytes = Number(memMatch[1]);
80
+ service.memory = formatMemory(memBytes);
81
+ }
82
+ if (timeMatch) {
83
+ service.uptime = formatUptime(timeMatch[1]);
84
+ }
85
+ if (tasksMatch) {
86
+ service.tasks = tasksMatch[1];
87
+ }
88
+ if (stateMatch) {
89
+ service.state = stateMatch[1].trim();
90
+ }
91
+ } catch {
92
+ // Ignore metrics errors
93
+ }
94
+
95
+ // Check health endpoint
96
+ try {
97
+ const portMatch = description.match(/port[=:]?\s*(\d+)/i);
98
+ if (portMatch) {
99
+ const port = portMatch[1];
100
+ const healthCheck = execSync(`curl -s -o /dev/null -w "%{http_code}" http://localhost:${port}/healthz`, { encoding: "utf-8", timeout: 1000 });
101
+ service.health = healthCheck === "200" ? "āœ… OK" : "āŒ FAIL";
102
+ }
103
+ } catch {
104
+ service.health = "āš ļø UNKNOWN";
105
+ }
106
+
107
+ services.push(service);
108
+ }
109
+
110
+ return services;
111
+ } catch (error) {
112
+ console.error('Error fetching metrics:', error);
113
+ return [];
114
+ }
115
+ };
116
+
117
+ const formatMemory = (bytes: number): string => {
118
+ if (bytes === 0) return '0B';
119
+ const k = 1024;
120
+ const sizes = ['B', 'KB', 'MB', 'GB'];
121
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
122
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`;
123
+ };
124
+
125
+ const formatUptime = (timestamp: string): string => {
126
+ try {
127
+ const date = new Date(timestamp);
128
+ const now = new Date();
129
+ const diff = now.getTime() - date.getTime();
130
+
131
+ const hours = Math.floor(diff / (1000 * 60 * 60));
132
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
133
+
134
+ if (hours > 0) {
135
+ return `${hours}h ${minutes}m`;
136
+ }
137
+ return `${minutes}m`;
138
+ } catch {
139
+ return '-';
140
+ }
141
+ };
142
+
143
+ const renderDashboard = () => {
144
+ // Clear screen
145
+ process.stdout.write('\x1b[2J\x1b[H');
146
+
147
+ // Header
148
+ console.log('šŸ” BS9 Real-time Monitoring Dashboard');
149
+ console.log('='.repeat(120));
150
+ console.log(`Refresh: ${refreshInterval}s | Last update: ${new Date().toLocaleTimeString()} | Press Ctrl+C to exit`);
151
+ console.log('');
152
+
153
+ const services = getMetrics();
154
+
155
+ if (services.length === 0) {
156
+ console.log('No BS9-managed services running.');
157
+ return;
158
+ }
159
+
160
+ // Table header
161
+ console.log('SERVICE'.padEnd(20) +
162
+ 'STATE'.padEnd(15) +
163
+ 'HEALTH'.padEnd(10) +
164
+ 'CPU'.padEnd(10) +
165
+ 'MEMORY'.padEnd(12) +
166
+ 'UPTIME'.padEnd(12) +
167
+ 'TASKS'.padEnd(8) +
168
+ 'DESCRIPTION');
169
+ console.log('-'.repeat(120));
170
+
171
+ // Service rows
172
+ for (const service of services) {
173
+ const stateColor = service.active === 'active' ? '\x1b[32m' : '\x1b[31m';
174
+ const resetColor = '\x1b[0m';
175
+
176
+ const state = `${stateColor}${service.sub}${resetColor}`;
177
+ const health = service.health || '-';
178
+
179
+ console.log(
180
+ service.name.padEnd(20) +
181
+ state.padEnd(15) +
182
+ health.padEnd(10) +
183
+ service.cpu.padEnd(10) +
184
+ service.memory.padEnd(12) +
185
+ service.uptime.padEnd(12) +
186
+ service.tasks.padEnd(8) +
187
+ service.description
188
+ );
189
+ }
190
+
191
+ // Summary
192
+ console.log('');
193
+ console.log('='.repeat(120));
194
+
195
+ const running = services.filter(s => s.active === 'active').length;
196
+ const totalMemory = services.reduce((sum, s) => {
197
+ if (s.memory !== '-') {
198
+ const match = s.memory.match(/([\d.]+)(B|KB|MB|GB)/);
199
+ if (match) {
200
+ const [, value, unit] = match;
201
+ const bytes = Number(value) * Math.pow(1024, ['B', 'KB', 'MB', 'GB'].indexOf(unit));
202
+ return sum + bytes;
203
+ }
204
+ }
205
+ return sum;
206
+ }, 0);
207
+
208
+ console.log(`šŸ“Š Summary: ${running}/${services.length} services running | Total Memory: ${formatMemory(totalMemory)} | Services: ${services.length}`);
209
+
210
+ // Alerts
211
+ const failed = services.filter(s => s.active !== 'active');
212
+ const unhealthy = services.filter(s => s.health === 'āŒ FAIL');
213
+
214
+ if (failed.length > 0 || unhealthy.length > 0) {
215
+ console.log('');
216
+ console.log('āš ļø ALERTS:');
217
+ if (failed.length > 0) {
218
+ console.log(` Failed services: ${failed.map(s => s.name).join(', ')}`);
219
+ }
220
+ if (unhealthy.length > 0) {
221
+ console.log(` Unhealthy services: ${unhealthy.map(s => s.name).join(', ')}`);
222
+ }
223
+ }
224
+ };
225
+
226
+ // Initial render
227
+ renderDashboard();
228
+
229
+ // Setup refresh loop
230
+ const refresh = async () => {
231
+ while (true) {
232
+ await setTimeout(refreshInterval * 1000);
233
+ renderDashboard();
234
+ }
235
+ };
236
+
237
+ // Handle Ctrl+C gracefully
238
+ process.on('SIGINT', () => {
239
+ console.log('\nšŸ‘‹ Monitoring stopped');
240
+ process.exit(0);
241
+ });
242
+
243
+ // Start monitoring
244
+ refresh().catch(error => {
245
+ console.error('Monitoring error:', error);
246
+ process.exit(1);
247
+ });
248
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync } from "node:child_process";
4
+
5
+ export async function restartCommand(name: string): Promise<void> {
6
+ try {
7
+ execSync(`systemctl --user restart ${name}`, { stdio: "inherit" });
8
+ console.log(`šŸ”„ User service '${name}' restarted`);
9
+ } catch (err) {
10
+ console.error(`āŒ Failed to restart user service '${name}': ${err}`);
11
+ process.exit(1);
12
+ }
13
+ }