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.
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/bin/bs9 +97 -0
- package/package.json +48 -0
- package/src/.gitkeep +0 -0
- package/src/alerting/config.ts +194 -0
- package/src/commands/alert.ts +98 -0
- package/src/commands/export.ts +69 -0
- package/src/commands/logs.ts +22 -0
- package/src/commands/monit.ts +248 -0
- package/src/commands/restart.ts +13 -0
- package/src/commands/start.ts +207 -0
- package/src/commands/status.ts +162 -0
- package/src/commands/stop.ts +13 -0
- package/src/commands/web.ts +49 -0
- package/src/docker/Dockerfile +44 -0
- package/src/injectors/otel.ts +66 -0
- package/src/k8s/bs9-deployment.yaml +197 -0
- package/src/storage/metrics.ts +204 -0
- package/src/web/dashboard.ts +286 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { join, basename, resolve, dirname } from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
|
|
10
|
+
interface StartOptions {
|
|
11
|
+
name?: string;
|
|
12
|
+
port?: string;
|
|
13
|
+
env?: string[];
|
|
14
|
+
otel?: boolean;
|
|
15
|
+
prometheus?: boolean;
|
|
16
|
+
build?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function startCommand(file: string, options: StartOptions): Promise<void> {
|
|
20
|
+
const fullPath = resolve(file);
|
|
21
|
+
if (!existsSync(fullPath)) {
|
|
22
|
+
console.error(`❌ File not found: ${fullPath}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const serviceName = options.name || basename(fullPath, fullPath.endsWith('.ts') ? '.ts' : '.js').replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
27
|
+
const port = options.port || "3000";
|
|
28
|
+
|
|
29
|
+
// Port warning for privileged ports
|
|
30
|
+
const portNum = Number(port);
|
|
31
|
+
if (portNum < 1024) {
|
|
32
|
+
console.warn(`⚠️ Port ${port} is privileged (< 1024).`);
|
|
33
|
+
console.warn(" Options:");
|
|
34
|
+
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`");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle TypeScript files and build option
|
|
40
|
+
let execPath = fullPath;
|
|
41
|
+
let isBuilt = false;
|
|
42
|
+
|
|
43
|
+
if (fullPath.endsWith('.ts')) {
|
|
44
|
+
if (options.build) {
|
|
45
|
+
// AOT: Build TypeScript to single executable
|
|
46
|
+
console.log("🔨 Building TypeScript for production...");
|
|
47
|
+
const buildDir = join(dirname(fullPath), ".bs9-build");
|
|
48
|
+
mkdirSync(buildDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
const outputFile = join(buildDir, `${serviceName}.js`);
|
|
51
|
+
try {
|
|
52
|
+
execSync(`bun build ${fullPath} --outdir ${buildDir} --target bun --minify --splitting`, { stdio: "inherit" });
|
|
53
|
+
execPath = outputFile;
|
|
54
|
+
isBuilt = true;
|
|
55
|
+
console.log(`✅ Built to: ${execPath}`);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(`❌ Build failed: ${err}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// JIT: Run TypeScript directly (default)
|
|
62
|
+
console.log("⚡ Running TypeScript in JIT mode");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Phase 2: Pre-start Security Audit
|
|
67
|
+
const auditResult = await securityAudit(execPath);
|
|
68
|
+
if (auditResult.critical.length > 0) {
|
|
69
|
+
console.error("🚨 Critical security issues found:");
|
|
70
|
+
auditResult.critical.forEach(issue => console.error(` - ${issue}`));
|
|
71
|
+
console.error("\nRefusing to start. Fix issues or use --force to override.");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (auditResult.warning.length > 0) {
|
|
76
|
+
console.warn("⚠️ Security warnings:");
|
|
77
|
+
auditResult.warning.forEach(issue => console.warn(` - ${issue}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Phase 1: Generate hardened systemd unit
|
|
81
|
+
const unitContent = generateSystemdUnit({
|
|
82
|
+
serviceName,
|
|
83
|
+
fullPath: execPath,
|
|
84
|
+
port,
|
|
85
|
+
env: options.env || [],
|
|
86
|
+
otel: options.otel ?? true,
|
|
87
|
+
prometheus: options.prometheus ?? true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const unitPath = join(homedir(), ".config/systemd/user", `${serviceName}.service`);
|
|
91
|
+
|
|
92
|
+
// 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}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
writeFileSync(unitPath, unitContent, { encoding: "utf-8" });
|
|
101
|
+
console.log(`✅ Systemd user unit written to: ${unitPath}`);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(`❌ Failed to write systemd user unit: ${err}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Reload and start using user systemd
|
|
108
|
+
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" });
|
|
112
|
+
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}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface SecurityAuditResult {
|
|
122
|
+
critical: string[];
|
|
123
|
+
warning: string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function securityAudit(filePath: string): Promise<SecurityAuditResult> {
|
|
127
|
+
const result: SecurityAuditResult = { critical: [], warning: [] };
|
|
128
|
+
const content = readFileSync(filePath, "utf-8");
|
|
129
|
+
const stat = statSync(filePath);
|
|
130
|
+
|
|
131
|
+
// Check file permissions
|
|
132
|
+
if (stat.mode & 0o002) {
|
|
133
|
+
result.critical.push("File is world-writable");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check for dangerous patterns
|
|
137
|
+
const dangerousPatterns = [
|
|
138
|
+
{ pattern: /eval\s*\(/, msg: "Use of eval() detected" },
|
|
139
|
+
{ pattern: /Function\s*\(/, msg: "Dynamic function construction detected" },
|
|
140
|
+
{ pattern: /child_process\.exec\s*\(/, msg: "Unsafe child_process.exec() detected" },
|
|
141
|
+
{ pattern: /require\s*\(\s*["']fs["']\s*\)/, msg: "Direct fs module usage (potential file system access)" },
|
|
142
|
+
{ pattern: /process\.env\.\w+\s*\+\s*["']/, msg: "Potential command injection via env concatenation" },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
for (const { pattern, msg } of dangerousPatterns) {
|
|
146
|
+
if (pattern.test(content)) {
|
|
147
|
+
result.critical.push(msg);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for network access patterns
|
|
152
|
+
if (content.includes("fetch(") || content.includes("http.request")) {
|
|
153
|
+
result.warning.push("Network access detected - ensure outbound rules are in place");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check for file system writes
|
|
157
|
+
if (content.includes("writeFileSync") || content.includes("createWriteStream")) {
|
|
158
|
+
result.warning.push("File system write access detected - ensure proper sandboxing");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface SystemdUnitOptions {
|
|
165
|
+
serviceName: string;
|
|
166
|
+
fullPath: string;
|
|
167
|
+
port: string;
|
|
168
|
+
env: string[];
|
|
169
|
+
otel: boolean;
|
|
170
|
+
prometheus: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function generateSystemdUnit(opts: SystemdUnitOptions): string {
|
|
174
|
+
const envVars = [
|
|
175
|
+
`PORT=${opts.port}`,
|
|
176
|
+
`NODE_ENV=production`,
|
|
177
|
+
`SERVICE_NAME=${opts.serviceName}`,
|
|
178
|
+
...opts.env,
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
if (opts.otel) {
|
|
182
|
+
envVars.push("OTEL_SERVICE_NAME=" + opts.serviceName);
|
|
183
|
+
envVars.push("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const envSection = envVars.map(e => `Environment=${e}`).join("\n");
|
|
187
|
+
const workingDir = dirname(opts.fullPath);
|
|
188
|
+
|
|
189
|
+
const bunPath = execSync("which bun", { encoding: "utf-8" }).trim();
|
|
190
|
+
return `[Unit]
|
|
191
|
+
Description=BS9 Service: ${opts.serviceName}
|
|
192
|
+
After=network.target
|
|
193
|
+
|
|
194
|
+
[Service]
|
|
195
|
+
Type=simple
|
|
196
|
+
Restart=on-failure
|
|
197
|
+
RestartSec=2s
|
|
198
|
+
TimeoutStartSec=30s
|
|
199
|
+
TimeoutStopSec=30s
|
|
200
|
+
WorkingDirectory=${workingDir}
|
|
201
|
+
ExecStart=${bunPath} run ${opts.fullPath}
|
|
202
|
+
${envSection}
|
|
203
|
+
|
|
204
|
+
[Install]
|
|
205
|
+
WantedBy=default.target
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
interface StatusOptions {
|
|
7
|
+
watch?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ServiceStatus {
|
|
11
|
+
name: string;
|
|
12
|
+
loaded: string;
|
|
13
|
+
active: string;
|
|
14
|
+
sub: string;
|
|
15
|
+
description: string;
|
|
16
|
+
cpu?: string;
|
|
17
|
+
memory?: string;
|
|
18
|
+
uptime?: string;
|
|
19
|
+
tasks?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
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
|
+
};
|
|
58
|
+
|
|
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
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
services.push(status);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Display table
|
|
79
|
+
console.clear();
|
|
80
|
+
console.log("🔍 BSN Service Status\n");
|
|
81
|
+
|
|
82
|
+
if (services.length === 0) {
|
|
83
|
+
console.log("No BSN-managed services running.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
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));
|
|
90
|
+
|
|
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
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
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);
|
|
110
|
+
}
|
|
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();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function formatCPU(nsec: number): string {
|
|
128
|
+
const ms = nsec / 1_000_000;
|
|
129
|
+
return `${ms.toFixed(1)}ms`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatMemory(bytes: number): string {
|
|
133
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
134
|
+
let size = bytes;
|
|
135
|
+
let unit = 0;
|
|
136
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
137
|
+
size /= 1024;
|
|
138
|
+
unit++;
|
|
139
|
+
}
|
|
140
|
+
return `${size.toFixed(1)}${units[unit]}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseMemory(str: string): number {
|
|
144
|
+
const match = str.match(/^([\d.]+)(B|KB|MB|GB)$/);
|
|
145
|
+
if (!match) return 0;
|
|
146
|
+
const [, value, unit] = match;
|
|
147
|
+
const mult = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 }[unit] || 1;
|
|
148
|
+
return Number(value) * mult;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
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
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export async function stopCommand(name: string): Promise<void> {
|
|
6
|
+
try {
|
|
7
|
+
execSync(`systemctl --user stop ${name}`, { stdio: "inherit" });
|
|
8
|
+
console.log(`🛑 User service '${name}' stopped`);
|
|
9
|
+
} catch (err) {
|
|
10
|
+
console.error(`❌ Failed to stop user service '${name}': ${err}`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
interface WebOptions {
|
|
7
|
+
port?: string;
|
|
8
|
+
detach?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function webCommand(options: WebOptions): Promise<void> {
|
|
12
|
+
const port = options.port || "8080";
|
|
13
|
+
const dashboardPath = `${import.meta.dir}/../web/dashboard.ts`;
|
|
14
|
+
|
|
15
|
+
console.log(`🌐 Starting BS9 Web Dashboard on port ${port}`);
|
|
16
|
+
|
|
17
|
+
if (options.detach) {
|
|
18
|
+
// Run in background
|
|
19
|
+
const child = spawn("bun", ["run", dashboardPath], {
|
|
20
|
+
detached: true,
|
|
21
|
+
stdio: 'ignore',
|
|
22
|
+
env: {
|
|
23
|
+
...process.env,
|
|
24
|
+
WEB_DASHBOARD_PORT: port,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
child.unref();
|
|
29
|
+
|
|
30
|
+
console.log(`✅ Web dashboard started in background`);
|
|
31
|
+
console.log(` URL: http://localhost:${port}`);
|
|
32
|
+
console.log(` Process ID: ${child.pid}`);
|
|
33
|
+
console.log(` Stop with: kill ${child.pid}`);
|
|
34
|
+
} else {
|
|
35
|
+
// Run in foreground
|
|
36
|
+
console.log(` URL: http://localhost:${port}`);
|
|
37
|
+
console.log(` Press Ctrl+C to stop`);
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
execSync(`WEB_DASHBOARD_PORT=${port} bun run ${dashboardPath}`, {
|
|
42
|
+
stdio: "inherit"
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(`❌ Failed to start web dashboard: ${error}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# BS9 Docker Image
|
|
2
|
+
FROM oven/bun:1.3.6-alpine
|
|
3
|
+
|
|
4
|
+
# Install system dependencies
|
|
5
|
+
RUN apk add --no-cache \
|
|
6
|
+
systemd \
|
|
7
|
+
curl \
|
|
8
|
+
bash \
|
|
9
|
+
&& rm -rf /var/cache/apk/*
|
|
10
|
+
|
|
11
|
+
# Create app directory
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
|
|
14
|
+
# Copy package files
|
|
15
|
+
COPY package.json bun.lockb ./
|
|
16
|
+
|
|
17
|
+
# Install dependencies
|
|
18
|
+
RUN bun install --frozen-lockfile --production
|
|
19
|
+
|
|
20
|
+
# Copy BS9 source code
|
|
21
|
+
COPY bin/ ./bin/
|
|
22
|
+
COPY src/ ./src/
|
|
23
|
+
|
|
24
|
+
# Create necessary directories
|
|
25
|
+
RUN mkdir -p /app/.config/bs9 /app/.config/systemd/user
|
|
26
|
+
|
|
27
|
+
# Make BS9 executable
|
|
28
|
+
RUN chmod +x /app/bin/bs9
|
|
29
|
+
|
|
30
|
+
# Add BS9 to PATH
|
|
31
|
+
ENV PATH="/app/bin:${PATH}"
|
|
32
|
+
|
|
33
|
+
# Expose web dashboard port
|
|
34
|
+
EXPOSE 8080
|
|
35
|
+
|
|
36
|
+
# Health check
|
|
37
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
38
|
+
CMD curl -f http://localhost:8080/ || exit 1
|
|
39
|
+
|
|
40
|
+
# Set up systemd for user mode
|
|
41
|
+
RUN echo "user" > /etc/hostname
|
|
42
|
+
|
|
43
|
+
# Default command
|
|
44
|
+
CMD ["/bin/bash"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
|
|
6
|
+
export function injectOpenTelemetry(entryFile: string, serviceName: string): void {
|
|
7
|
+
const wrapper = `
|
|
8
|
+
// BSN OpenTelemetry Auto-injection
|
|
9
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
10
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
11
|
+
import { Resource } from "@opentelemetry/resources";
|
|
12
|
+
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
|
|
13
|
+
|
|
14
|
+
const sdk = new NodeSDK({
|
|
15
|
+
resource: new Resource({
|
|
16
|
+
[SemanticResourceAttributes.SERVICE_NAME]: "${serviceName}",
|
|
17
|
+
[SemanticResourceAttributes.SERVICE_VERSION]: "1.0.0",
|
|
18
|
+
}),
|
|
19
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
sdk.start();
|
|
23
|
+
|
|
24
|
+
// Original user code below
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const originalContent = readFileSync(entryFile, "utf-8");
|
|
28
|
+
const newContent = wrapper + originalContent;
|
|
29
|
+
|
|
30
|
+
writeFileSync(entryFile, newContent, { encoding: "utf-8" });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function injectPrometheus(entryFile: string, port: string): void {
|
|
34
|
+
const prometheusInject = `
|
|
35
|
+
// BSN Prometheus Auto-injection
|
|
36
|
+
import promClient from "prom-client";
|
|
37
|
+
|
|
38
|
+
const register = new promClient.Registry();
|
|
39
|
+
promClient.collectDefaultMetrics({ register });
|
|
40
|
+
|
|
41
|
+
const httpRequestDuration = new promClient.Histogram({
|
|
42
|
+
name: "http_request_duration_seconds",
|
|
43
|
+
help: "Duration of HTTP requests in seconds",
|
|
44
|
+
labelNames: ["method", "route", "status_code"],
|
|
45
|
+
registers: [register],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const httpRequestTotal = new promClient.Counter({
|
|
49
|
+
name: "http_requests_total",
|
|
50
|
+
help: "Total number of HTTP requests",
|
|
51
|
+
labelNames: ["method", "route", "status_code"],
|
|
52
|
+
registers: [register],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Export for use in user code
|
|
56
|
+
globalThis.prometheus = { register, httpRequestDuration, httpRequestTotal };
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
// This would need to be injected at the right place in the user's code
|
|
60
|
+
// For now, we'll write it to a temporary file that the user can import
|
|
61
|
+
const metricsFile = join(dirname(entryFile), "bsn-metrics.js");
|
|
62
|
+
writeFileSync(metricsFile, prometheusInject, { encoding: "utf-8" });
|
|
63
|
+
|
|
64
|
+
console.log(`📊 Prometheus metrics written to: ${metricsFile}`);
|
|
65
|
+
console.log(` Add to your app: import "./bsn-metrics.js"`);
|
|
66
|
+
}
|