bs9 1.3.9 → 1.4.1

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # BS9 (Bun Sentinel 9) šŸš€
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
- [![Version](https://img.shields.io/badge/version-1.3.7-blue.svg)](https://github.com/xarhang/bs9)
4
+ [![Version](https://img.shields.io/badge/version-1.3.9-blue.svg)](https://github.com/xarhang/bs9)
5
5
  [![Security](https://img.shields.io/badge/security-Enterprise-green.svg)](SECURITY.md)
6
6
  [![Production Ready](https://img.shields.io/badge/production-Ready-brightgreen.svg)](PRODUCTION.md)
7
7
  [![Cross-Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey.svg)](https://github.com/bs9/bs9)
@@ -26,24 +26,25 @@ chmod +x ~/.local/bin/bs9
26
26
 
27
27
  ---
28
28
 
29
- ### Platform Support
30
-
31
- BS9 supports all major platforms with native service management:
29
+ ### 🌐 Platform Support
30
+ - **āœ… Auto-Detection**: Automatically detects platform and creates directories
31
+ - **āœ… Zero Configuration**: No manual setup required
32
+ - **āœ… Cross-Platform**: Same commands work on all platforms
32
33
 
33
34
  #### 🐧 Linux
34
35
  - **Service Manager**: Systemd (user-mode)
35
36
  - **Features**: Advanced security hardening, resource limits, sandboxing
36
- - **Commands**: All standard commands available
37
+ - **Commands**: All 21 commands available
37
38
 
38
39
  #### šŸŽ macOS
39
40
  - **Service Manager**: Launchd
40
41
  - **Features**: Native macOS integration, automatic recovery
41
- - **Commands**: Standard commands + `bs9 macos` for launchd management
42
+ - **Commands**: All commands + `bs9 macos` for launchd management
42
43
 
43
44
  #### 🪟 Windows
44
45
  - **Service Manager**: Windows Services
45
46
  - **Features**: PowerShell automation, event log integration
46
- - **Commands**: Standard commands + `bs9 windows` for service management
47
+ - **Commands**: All commands + `bs9 windows` for service management
47
48
 
48
49
  ```bash
49
50
  # Check your platform
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bs9",
3
- "version": "1.3.9",
3
+ "version": "1.4.1",
4
4
  "description": "Bun Sentinel 9 - High-performance, non-root process manager for Bun",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { execSync } from "node:child_process";
13
13
  import { join } from "node:path";
14
- import { getPlatformInfo } from "../platform/detect.js";
14
+ import { getPlatformInfo, initializePlatformDirectories } from "../platform/detect.js";
15
15
 
16
16
  interface ResurrectOptions {
17
17
  all?: boolean;
@@ -28,6 +28,9 @@ function isValidServiceName(name: string): boolean {
28
28
  }
29
29
 
30
30
  export async function resurrectCommand(name: string, options: ResurrectOptions): Promise<void> {
31
+ // Initialize platform directories
32
+ initializePlatformDirectories();
33
+
31
34
  const platformInfo = getPlatformInfo();
32
35
 
33
36
  // Handle resurrect all services
@@ -48,8 +51,7 @@ export async function resurrectCommand(name: string, options: ResurrectOptions):
48
51
  const escapedName = name.replace(/[^a-zA-Z0-9._-]/g, '');
49
52
 
50
53
  // Check if service exists in backup
51
- const backupDir = join(platformInfo.configDir, 'backups');
52
- const backupFile = join(backupDir, `${escapedName}.json`);
54
+ const backupFile = join(platformInfo.backupDir, `${escapedName}.json`);
53
55
 
54
56
  if (!require('node:fs').existsSync(backupFile)) {
55
57
  console.error(`āŒ No backup found for service '${name}'`);
@@ -115,16 +117,12 @@ async function resurrectAllServices(platformInfo: any, options: ResurrectOptions
115
117
  try {
116
118
  console.log("šŸ”„ Resurrecting all BS9 services from backup...");
117
119
 
120
+ // Initialize platform directories
121
+ initializePlatformDirectories();
122
+
118
123
  if (platformInfo.isLinux) {
119
- const backupDir = join(platformInfo.configDir, 'backups');
120
-
121
- if (!require('node:fs').existsSync(backupDir)) {
122
- console.log("ā„¹ļø No backup directory found");
123
- return;
124
- }
125
-
126
124
  // Get all backup files
127
- const backupFiles = require('node:fs').readdirSync(backupDir)
125
+ const backupFiles = require('node:fs').readdirSync(platformInfo.backupDir)
128
126
  .filter((file: string) => file.endsWith('.json'));
129
127
 
130
128
  if (backupFiles.length === 0) {
@@ -137,7 +135,7 @@ async function resurrectAllServices(platformInfo: any, options: ResurrectOptions
137
135
  for (const backupFile of backupFiles) {
138
136
  try {
139
137
  const serviceName = backupFile.replace('.json', '');
140
- const backupPath = join(backupDir, backupFile);
138
+ const backupPath = join(platformInfo.backupDir, backupFile);
141
139
  const backupConfig = JSON.parse(require('node:fs').readFileSync(backupPath, 'utf8'));
142
140
 
143
141
  // Restore service using backup configuration
@@ -161,11 +159,11 @@ async function resurrectAllServices(platformInfo: any, options: ResurrectOptions
161
159
 
162
160
  } else if (platformInfo.isMacOS) {
163
161
  console.log("šŸ“ To resurrect all services on macOS, you need to manually restore the plist files from:");
164
- console.log(` ${join(platformInfo.configDir, 'backups')}/*.plist`);
162
+ console.log(` ${platformInfo.backupDir}/*.plist`);
165
163
  console.log(" And then run: launchctl load ~/Library/LaunchAgents/bs9.*.plist");
166
164
  } else if (platformInfo.isWindows) {
167
165
  console.log("šŸ“ To resurrect all services on Windows, use PowerShell:");
168
- console.log(" Get-ChildItem -Path \"${join(platformInfo.configDir, 'backups')}\" | ForEach-Object { Restore-Service $_.Name }");
166
+ console.log(" Get-ChildItem -Path \"${platformInfo.backupDir}\" | ForEach-Object { Restore-Service $_.Name }");
169
167
  }
170
168
 
171
169
  console.log(`āœ… All BS9 services resurrection process completed`);
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { execSync } from "node:child_process";
13
13
  import { join, resolve } from "node:path";
14
- import { getPlatformInfo } from "../platform/detect.js";
14
+ import { getPlatformInfo, initializePlatformDirectories } from "../platform/detect.js";
15
15
 
16
16
  interface SaveOptions {
17
17
  all?: boolean;
@@ -28,6 +28,9 @@ function isValidServiceName(name: string): boolean {
28
28
  }
29
29
 
30
30
  export async function saveCommand(name: string, options: SaveOptions): Promise<void> {
31
+ // Initialize platform directories
32
+ initializePlatformDirectories();
33
+
31
34
  const platformInfo = getPlatformInfo();
32
35
 
33
36
  // Handle save all services
@@ -62,12 +65,8 @@ export async function saveCommand(name: string, options: SaveOptions): Promise<v
62
65
  // Parse service configuration to extract startup parameters
63
66
  const config = parseServiceConfig(serviceConfig, statusOutput);
64
67
 
65
- // Create backup directory if it doesn't exist
66
- const backupDir = join(platformInfo.configDir, 'backups');
67
- require('node:fs').mkdirSync(backupDir, { recursive: true });
68
-
69
68
  // Save configuration to backup
70
- const backupFile = join(backupDir, `${escapedName}.json`);
69
+ const backupFile = join(platformInfo.backupDir, `${escapedName}.json`);
71
70
  const backupData = {
72
71
  name: name,
73
72
  file: extractFileFromConfig(serviceConfig),
@@ -89,7 +88,7 @@ export async function saveCommand(name: string, options: SaveOptions): Promise<v
89
88
  if (options.backup) {
90
89
  // Create additional backup with timestamp
91
90
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
92
- const timestampedBackup = join(backupDir, `${escapedName}-${timestamp}.json`);
91
+ const timestampedBackup = join(platformInfo.backupDir, `${escapedName}-${timestamp}.json`);
93
92
  require('node:fs').writeFileSync(timestampedBackup, JSON.stringify(backupData, null, 2));
94
93
  console.log(`šŸ“¦ Additional backup created: ${timestampedBackup}`);
95
94
  }
@@ -118,6 +117,9 @@ async function saveAllServices(platformInfo: any, options: SaveOptions): Promise
118
117
  try {
119
118
  console.log("šŸ’¾ Saving all BS9 service configurations...");
120
119
 
120
+ // Initialize platform directories
121
+ initializePlatformDirectories();
122
+
121
123
  if (platformInfo.isLinux) {
122
124
  // Get all BS9 services
123
125
  const listOutput = execSync("systemctl --user list-units --type=service --no-pager --no-legend", { encoding: "utf-8" });
@@ -144,10 +146,6 @@ async function saveAllServices(platformInfo: any, options: SaveOptions): Promise
144
146
 
145
147
  console.log(`Found ${bs9Services.length} BS9 services to save...`);
146
148
 
147
- // Create backup directory
148
- const backupDir = join(platformInfo.configDir, 'backups');
149
- require('node:fs').mkdirSync(backupDir, { recursive: true });
150
-
151
149
  for (const serviceName of bs9Services) {
152
150
  try {
153
151
  const serviceFile = join(platformInfo.serviceDir, `${serviceName}.service`);
@@ -157,7 +155,7 @@ async function saveAllServices(platformInfo: any, options: SaveOptions): Promise
157
155
  const statusOutput = execSync(`systemctl --user show "${serviceName}"`, { encoding: "utf-8" });
158
156
  const config = parseServiceConfig(serviceConfig, statusOutput);
159
157
 
160
- const backupFile = join(backupDir, `${serviceName}.json`);
158
+ const backupFile = join(platformInfo.backupDir, `${serviceName}.json`);
161
159
  const backupData = {
162
160
  name: serviceName,
163
161
  file: extractFileFromConfig(serviceConfig),
@@ -27,6 +27,7 @@ interface ServiceStatus {
27
27
  memory?: string;
28
28
  uptime?: string;
29
29
  tasks?: string;
30
+ pid?: string;
30
31
  }
31
32
 
32
33
  export async function statusCommand(options: StatusOptions): Promise<void> {
@@ -93,20 +94,46 @@ async function getLinuxServices(): Promise<ServiceStatus[]> {
93
94
  description,
94
95
  };
95
96
 
96
- // Get additional metrics
97
+ // Get additional metrics with better error handling
97
98
  try {
98
- const showOutput = execSync(`systemctl --user show ${name} -p CPUUsageNSec MemoryCurrent ActiveEnterTimestamp TasksCurrent`, { encoding: "utf-8" });
99
+ // Get comprehensive service information
100
+ const showOutput = execSync(`systemctl --user show ${name}`, { encoding: "utf-8" });
101
+
102
+ // Extract CPU usage
99
103
  const cpuMatch = showOutput.match(/CPUUsageNSec=(\d+)/);
104
+ if (cpuMatch) {
105
+ status.cpu = formatCPU(Number(cpuMatch[1]));
106
+ }
107
+
108
+ // Extract memory usage
100
109
  const memMatch = showOutput.match(/MemoryCurrent=(\d+)/);
110
+ if (memMatch) {
111
+ const memoryBytes = Number(memMatch[1]);
112
+ status.memory = formatMemory(memoryBytes);
113
+ }
114
+
115
+ // Extract startup time and calculate uptime
101
116
  const timeMatch = showOutput.match(/ActiveEnterTimestamp=(.+)/);
117
+ if (timeMatch) {
118
+ status.uptime = formatUptime(timeMatch[1]);
119
+ }
120
+
121
+ // Extract task count
102
122
  const tasksMatch = showOutput.match(/TasksCurrent=(\d+)/);
123
+ if (tasksMatch) {
124
+ status.tasks = tasksMatch[1];
125
+ }
126
+
127
+ // Get process ID for additional info
128
+ const pidMatch = showOutput.match(/MainPID=(\d+)/);
129
+ if (pidMatch && pidMatch[1] !== "0") {
130
+ // We could get more detailed process info here if needed
131
+ status.pid = pidMatch[1];
132
+ }
103
133
 
104
- if (cpuMatch) status.cpu = formatCPU(Number(cpuMatch[1]));
105
- if (memMatch) status.memory = formatMemory(Number(memMatch[1]));
106
- if (timeMatch) status.uptime = formatUptime(timeMatch[1]);
107
- if (tasksMatch) status.tasks = tasksMatch[1];
108
- } catch {
109
- // Metrics might not be available
134
+ } catch (metricsError: any) {
135
+ // If metrics fail, at least we have basic status
136
+ console.warn(`āš ļø Could not get metrics for ${name}: ${metricsError?.message || metricsError}`);
110
137
  }
111
138
 
112
139
  services.push(status);
@@ -145,25 +172,84 @@ async function getWindowsServices(): Promise<ServiceStatus[]> {
145
172
  }
146
173
 
147
174
  function displayServices(services: ServiceStatus[]): void {
148
- // Header
149
- console.log(`${"SERVICE".padEnd(20)} ${"STATE".padEnd(12)} ${"CPU".padEnd(8)} ${"MEMORY".padEnd(10)} ${"UPTIME".padEnd(12)} ${"TASKS".padEnd(6)} DESCRIPTION`);
150
- console.log("-".repeat(90));
175
+ if (services.length === 0) {
176
+ console.log("šŸ“‹ No BS9 services found");
177
+ console.log("šŸ’” Use 'bs9 start <file>' to create a service");
178
+ return;
179
+ }
180
+
181
+ // Header with better formatting
182
+ console.log(`${"SERVICE".padEnd(18)} ${"STATUS".padEnd(15)} ${"CPU".padEnd(10)} ${"MEMORY".padEnd(12)} ${"UPTIME".padEnd(12)} ${"TASKS".padEnd(8)} DESCRIPTION`);
183
+ console.log("─".repeat(100));
151
184
 
152
- for (const svc of services) {
153
- const state = `${svc.active}/${svc.sub}`;
185
+ // Sort services by status (running first, then by name)
186
+ const sortedServices = services.sort((a, b) => {
187
+ const aRunning = a.active === "active" && a.sub === "running";
188
+ const bRunning = b.active === "active" && b.sub === "running";
189
+ if (aRunning !== bRunning) return bRunning ? 1 : -1;
190
+ return a.name.localeCompare(b.name);
191
+ });
192
+
193
+ for (const svc of sortedServices) {
194
+ // Better status formatting with colors/indicators
195
+ let statusIndicator = "";
196
+ let status = `${svc.active}/${svc.sub}`;
197
+
198
+ if (svc.active === "active" && svc.sub === "running") {
199
+ statusIndicator = "āœ…";
200
+ status = "running";
201
+ } else if (svc.active === "activating" && svc.sub.includes("auto-restart")) {
202
+ statusIndicator = "šŸ”„";
203
+ status = "restarting";
204
+ } else if (svc.active === "failed" || svc.sub === "failed") {
205
+ statusIndicator = "āŒ";
206
+ status = "failed";
207
+ } else if (svc.active === "inactive") {
208
+ statusIndicator = "āøļø";
209
+ status = "stopped";
210
+ } else {
211
+ statusIndicator = "āš ļø";
212
+ }
213
+
214
+ const displayStatus = `${statusIndicator} ${status}`;
215
+
154
216
  console.log(
155
- `${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}`
217
+ `${svc.name.padEnd(18)} ${displayStatus.padEnd(15)} ${(svc.cpu || "-").padEnd(10)} ${(svc.memory || "-").padEnd(12)} ${(svc.uptime || "-").padEnd(12)} ${(svc.tasks || "-").padEnd(8)} ${svc.description}`
156
218
  );
157
219
  }
158
220
 
159
- console.log("\nšŸ“Š SRE Metrics Summary:");
221
+ // Enhanced summary
222
+ console.log("\nšŸ“Š Service Summary:");
160
223
  const totalServices = services.length;
161
- const runningServices = services.filter(s => s.active === "active").length;
224
+ const runningServices = services.filter(s => s.active === "active" && s.sub === "running").length;
225
+ const failedServices = services.filter(s => s.active === "failed" || s.sub === "failed").length;
226
+ const restartingServices = services.filter(s => s.active === "activating" && s.sub.includes("auto-restart")).length;
162
227
  const totalMemory = services.reduce((sum, s) => sum + (s.memory ? parseMemory(s.memory) : 0), 0);
163
228
 
164
- console.log(` Services: ${runningServices}/${totalServices} running`);
165
- console.log(` Memory: ${formatMemory(totalMemory)}`);
166
- console.log(` Last updated: ${new Date().toISOString()}`);
229
+ console.log(` šŸ“ˆ Status: ${runningServices} running, ${failedServices} failed, ${restartingServices} restarting`);
230
+ console.log(` šŸ“¦ Total: ${runningServices}/${totalServices} services running`);
231
+ console.log(` šŸ’¾ Memory: ${formatMemory(totalMemory)}`);
232
+ console.log(` šŸ•’ Last updated: ${new Date().toLocaleString()}`);
233
+
234
+ // Show failed services details
235
+ if (failedServices > 0) {
236
+ console.log("\nāŒ Failed Services:");
237
+ const failed = services.filter(s => s.active === "failed" || s.sub === "failed");
238
+ for (const svc of failed) {
239
+ console.log(` • ${svc.name}: ${svc.active}/${svc.sub}`);
240
+ console.log(` šŸ’” Try: bs9 logs ${svc.name} --tail 20`);
241
+ }
242
+ }
243
+
244
+ // Show restarting services details
245
+ if (restartingServices > 0) {
246
+ console.log("\nšŸ”„ Restarting Services:");
247
+ const restarting = services.filter(s => s.active === "activating" && s.sub.includes("auto-restart"));
248
+ for (const svc of restarting) {
249
+ console.log(` • ${svc.name}: ${svc.active}/${svc.sub}`);
250
+ console.log(` šŸ’” Try: bs9 logs ${svc.name} --tail 20`);
251
+ }
252
+ }
167
253
  }
168
254
 
169
255
  function formatCPU(nsec: number): string {
@@ -9,7 +9,8 @@
9
9
  * https://github.com/xarhang/bs9
10
10
  */
11
11
 
12
- import { platform } from "node:os";
12
+ import { platform, homedir } from "node:os";
13
+ import { join } from "node:path";
13
14
 
14
15
  export type Platform = 'linux' | 'darwin' | 'win32';
15
16
 
@@ -22,10 +23,12 @@ export interface PlatformInfo {
22
23
  configDir: string;
23
24
  logDir: string;
24
25
  serviceDir: string;
26
+ backupDir: string;
25
27
  }
26
28
 
27
29
  export function getPlatformInfo(): PlatformInfo {
28
30
  const currentPlatform = platform() as Platform;
31
+ const userHome = homedir();
29
32
 
30
33
  const baseInfo: PlatformInfo = {
31
34
  platform: currentPlatform,
@@ -35,29 +38,33 @@ export function getPlatformInfo(): PlatformInfo {
35
38
  serviceManager: 'systemd',
36
39
  configDir: '',
37
40
  logDir: '',
38
- serviceDir: ''
41
+ serviceDir: '',
42
+ backupDir: ''
39
43
  };
40
44
 
41
45
  switch (currentPlatform) {
42
46
  case 'linux':
43
47
  baseInfo.serviceManager = 'systemd';
44
- baseInfo.configDir = `${process.env.HOME}/.config/bs9`;
45
- baseInfo.logDir = `${process.env.HOME}/.local/share/bs9/logs`;
46
- baseInfo.serviceDir = `${process.env.HOME}/.config/systemd/user`;
48
+ baseInfo.configDir = join(userHome, '.config', 'bs9');
49
+ baseInfo.logDir = join(userHome, '.local', 'share', 'bs9', 'logs');
50
+ baseInfo.serviceDir = join(userHome, '.config', 'systemd', 'user');
51
+ baseInfo.backupDir = join(baseInfo.configDir, 'backups');
47
52
  break;
48
53
 
49
54
  case 'darwin':
50
55
  baseInfo.serviceManager = 'launchd';
51
- baseInfo.configDir = `${process.env.HOME}/.bs9`;
52
- baseInfo.logDir = `${process.env.HOME}/.bs9/logs`;
53
- baseInfo.serviceDir = `${process.env.HOME}/Library/LaunchAgents`;
56
+ baseInfo.configDir = join(userHome, '.bs9');
57
+ baseInfo.logDir = join(userHome, '.bs9', 'logs');
58
+ baseInfo.serviceDir = join(userHome, 'Library', 'LaunchAgents');
59
+ baseInfo.backupDir = join(baseInfo.configDir, 'backups');
54
60
  break;
55
61
 
56
62
  case 'win32':
57
63
  baseInfo.serviceManager = 'windows-service';
58
- baseInfo.configDir = `${process.env.USERPROFILE}/.bs9`;
59
- baseInfo.logDir = `${process.env.USERPROFILE}/.bs9/logs`;
60
- baseInfo.serviceDir = `${process.env.USERPROFILE}/.bs9/services`;
64
+ baseInfo.configDir = join(userHome, '.bs9');
65
+ baseInfo.logDir = join(userHome, '.bs9', 'logs');
66
+ baseInfo.serviceDir = join(userHome, '.bs9', 'services');
67
+ baseInfo.backupDir = join(baseInfo.configDir, 'backups');
61
68
  break;
62
69
 
63
70
  default:
@@ -77,13 +84,13 @@ export function getPlatformSpecificCommands(): string[] {
77
84
 
78
85
  switch (currentPlatform) {
79
86
  case 'linux':
80
- return ['start', 'stop', 'restart', 'status', 'logs', 'monit', 'web', 'alert', 'export', 'deps', 'profile', 'loadbalancer', 'dbpool'];
87
+ return ['start', 'stop', 'restart', 'status', 'logs', 'monit', 'web', 'alert', 'export', 'deps', 'profile', 'delete', 'save', 'resurrect', 'loadbalancer', 'dbpool'];
81
88
 
82
89
  case 'darwin':
83
- return ['start', 'stop', 'restart', 'status', 'logs', 'monit', 'web', 'alert', 'export', 'deps', 'profile', 'loadbalancer', 'dbpool', 'macos'];
90
+ return ['start', 'stop', 'restart', 'status', 'logs', 'monit', 'web', 'alert', 'export', 'deps', 'profile', 'delete', 'save', 'resurrect', 'loadbalancer', 'dbpool', 'macos'];
84
91
 
85
92
  case 'win32':
86
- return ['start', 'stop', 'restart', 'status', 'logs', 'monit', 'web', 'alert', 'export', 'deps', 'profile', 'loadbalancer', 'dbpool', 'windows'];
93
+ return ['start', 'stop', 'restart', 'status', 'logs', 'monit', 'web', 'alert', 'export', 'deps', 'profile', 'delete', 'save', 'resurrect', 'loadbalancer', 'dbpool', 'windows'];
87
94
 
88
95
  default:
89
96
  return [];
@@ -144,3 +151,19 @@ Windows-specific:
144
151
  return `āŒ Platform ${currentPlatform} is not supported`;
145
152
  }
146
153
  }
154
+
155
+ // Auto-detect and initialize platform-specific directories
156
+ export function initializePlatformDirectories(): void {
157
+ const platformInfo = getPlatformInfo();
158
+
159
+ // Create directories if they don't exist
160
+ const fs = require('node:fs');
161
+
162
+ try {
163
+ fs.mkdirSync(platformInfo.configDir, { recursive: true });
164
+ fs.mkdirSync(platformInfo.logDir, { recursive: true });
165
+ fs.mkdirSync(platformInfo.backupDir, { recursive: true });
166
+ } catch (error) {
167
+ console.warn(`āš ļø Warning: Could not create platform directories: ${error}`);
168
+ }
169
+ }