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,402 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { existsSync, writeFileSync, mkdirSync } from "node:fs";
5
+ import { join, dirname } from "node:path";
6
+ import { homedir } from "node:os";
7
+
8
+ interface LaunchdServiceConfig {
9
+ label: string;
10
+ programArguments: string[];
11
+ workingDirectory: string;
12
+ environmentVariables: Record<string, string>;
13
+ runAtLoad: boolean;
14
+ keepAlive: boolean;
15
+ standardOutPath?: string;
16
+ standardErrorPath?: string;
17
+ startInterval?: number;
18
+ }
19
+
20
+ interface LaunchdServiceStatus {
21
+ label: string;
22
+ pid?: number;
23
+ status: 'running' | 'stopped' | 'loaded' | 'unloaded';
24
+ lastExitStatus?: number;
25
+ exitTime?: Date;
26
+ }
27
+
28
+ class LaunchdServiceManager {
29
+ private launchAgentsDir: string;
30
+ private configPath: string;
31
+
32
+ constructor() {
33
+ this.launchAgentsDir = join(homedir(), 'Library', 'LaunchAgents');
34
+ this.configPath = join(homedir(), '.bs9', 'launchd-services.json');
35
+ this.ensureDirectories();
36
+ }
37
+
38
+ private ensureDirectories(): void {
39
+ if (!existsSync(this.launchAgentsDir)) {
40
+ mkdirSync(this.launchAgentsDir, { recursive: true });
41
+ }
42
+
43
+ const configDir = dirname(this.configPath);
44
+ if (!existsSync(configDir)) {
45
+ mkdirSync(configDir, { recursive: true });
46
+ }
47
+ }
48
+
49
+ private loadConfigs(): Record<string, LaunchdServiceConfig> {
50
+ try {
51
+ if (existsSync(this.configPath)) {
52
+ const content = require('fs').readFileSync(this.configPath, 'utf-8');
53
+ return JSON.parse(content);
54
+ }
55
+ } catch (error) {
56
+ console.warn('Failed to load launchd configs:', error);
57
+ }
58
+ return {};
59
+ }
60
+
61
+ private saveConfigs(configs: Record<string, LaunchdServiceConfig>): void {
62
+ try {
63
+ writeFileSync(this.configPath, JSON.stringify(configs, null, 2));
64
+ } catch (error) {
65
+ console.error('Failed to save launchd configs:', error);
66
+ }
67
+ }
68
+
69
+ private generatePlist(config: LaunchdServiceConfig): string {
70
+ const plistContent = {
71
+ Label: config.label,
72
+ ProgramArguments: config.programArguments,
73
+ WorkingDirectory: config.workingDirectory,
74
+ EnvironmentVariables: config.environmentVariables,
75
+ RunAtLoad: config.runAtLoad,
76
+ KeepAlive: config.keepAlive,
77
+ StandardOutPath: config.standardOutPath,
78
+ StandardErrorPath: config.standardErrorPath,
79
+ StartInterval: config.startInterval
80
+ };
81
+
82
+ // Remove undefined values
83
+ Object.keys(plistContent).forEach(key => {
84
+ if ((plistContent as any)[key] === undefined) {
85
+ delete (plistContent as any)[key];
86
+ }
87
+ });
88
+
89
+ return `<?xml version="1.0" encoding="UTF-8"?>
90
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
91
+ <plist version="1.0">
92
+ <dict>
93
+ ${Object.entries(plistContent).map(([key, value]) => {
94
+ if (typeof value === 'boolean') {
95
+ return ` <key>${key}</key>\n <${value ? 'true' : 'false'}/>`;
96
+ } else if (typeof value === 'string') {
97
+ return ` <key>${key}</key>\n <string>${value}</string>`;
98
+ } else if (Array.isArray(value)) {
99
+ return ` <key>${key}</key>\n <array>\n${value.map(item => ` <string>${item}</string>`).join('\n')}\n </array>`;
100
+ } else if (typeof value === 'object' && value !== null) {
101
+ return ` <key>${key}</key>\n <dict>\n${Object.entries(value).map(([k, v]) => ` <key>${k}</key>\n <string>${v}</string>`).join('\n')}\n </dict>`;
102
+ }
103
+ return '';
104
+ }).join('\n')}
105
+ </dict>
106
+ </plist>`;
107
+ }
108
+
109
+ async createService(config: LaunchdServiceConfig): Promise<void> {
110
+ const configs = this.loadConfigs();
111
+ configs[config.label] = config;
112
+ this.saveConfigs(configs);
113
+
114
+ // Generate plist file
115
+ const plistPath = join(this.launchAgentsDir, `${config.label}.plist`);
116
+ writeFileSync(plistPath, this.generatePlist(config));
117
+
118
+ try {
119
+ // Load the service
120
+ execSync(`launchctl load "${plistPath}"`, { stdio: 'inherit' });
121
+ console.log(`✅ Launchd service '${config.label}' created and loaded successfully`);
122
+ } catch (error) {
123
+ console.error(`❌ Failed to create launchd service: ${error}`);
124
+ throw error;
125
+ }
126
+ }
127
+
128
+ async startService(label: string): Promise<void> {
129
+ try {
130
+ execSync(`launchctl start "${label}"`, { stdio: 'inherit' });
131
+ console.log(`✅ Launchd service '${label}' started successfully`);
132
+ } catch (error) {
133
+ console.error(`❌ Failed to start launchd service: ${error}`);
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ async stopService(label: string): Promise<void> {
139
+ try {
140
+ execSync(`launchctl stop "${label}"`, { stdio: 'inherit' });
141
+ console.log(`✅ Launchd service '${label}' stopped successfully`);
142
+ } catch (error) {
143
+ console.error(`❌ Failed to stop launchd service: ${error}`);
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ async unloadService(label: string): Promise<void> {
149
+ const plistPath = join(this.launchAgentsDir, `${label}.plist`);
150
+
151
+ try {
152
+ // Stop service first
153
+ try {
154
+ await this.stopService(label);
155
+ } catch {
156
+ // Service might not be running
157
+ }
158
+
159
+ // Unload service
160
+ execSync(`launchctl unload "${plistPath}"`, { stdio: 'inherit' });
161
+
162
+ // Remove plist file
163
+ require('fs').unlinkSync(plistPath);
164
+
165
+ // Remove from config
166
+ const configs = this.loadConfigs();
167
+ delete configs[label];
168
+ this.saveConfigs(configs);
169
+
170
+ console.log(`✅ Launchd service '${label}' unloaded and deleted successfully`);
171
+ } catch (error) {
172
+ console.error(`❌ Failed to unload launchd service: ${error}`);
173
+ throw error;
174
+ }
175
+ }
176
+
177
+ async getServiceStatus(label: string): Promise<LaunchdServiceStatus | null> {
178
+ try {
179
+ const output = execSync(`launchctl list "${label}"`, { encoding: 'utf-8' });
180
+
181
+ const lines = output.split('\n');
182
+ const dataLine = lines.find(line => line.includes(label));
183
+
184
+ if (dataLine) {
185
+ const parts = dataLine.trim().split(/\s+/);
186
+ const status: LaunchdServiceStatus = {
187
+ label: label,
188
+ status: 'loaded'
189
+ };
190
+
191
+ if (parts[0] !== '-') {
192
+ status.pid = parseInt(parts[0]);
193
+ status.status = 'running';
194
+ }
195
+
196
+ if (parts[1] !== '-') {
197
+ status.lastExitStatus = parseInt(parts[1]);
198
+ }
199
+
200
+ return status;
201
+ }
202
+ } catch (error) {
203
+ // Service might not exist
204
+ }
205
+
206
+ return null;
207
+ }
208
+
209
+ async listServices(): Promise<LaunchdServiceStatus[]> {
210
+ try {
211
+ const configs = this.loadConfigs();
212
+ const services: LaunchdServiceStatus[] = [];
213
+
214
+ for (const label of Object.keys(configs)) {
215
+ const status = await this.getServiceStatus(label);
216
+ if (status) {
217
+ services.push(status);
218
+ }
219
+ }
220
+
221
+ return services;
222
+ } catch (error) {
223
+ console.error('Failed to list launchd services:', error);
224
+ return [];
225
+ }
226
+ }
227
+
228
+ async enableAutoStart(label: string): Promise<void> {
229
+ const configs = this.loadConfigs();
230
+ const config = configs[label];
231
+
232
+ if (!config) {
233
+ throw new Error(`Service '${label}' not found`);
234
+ }
235
+
236
+ config.runAtLoad = true;
237
+ config.keepAlive = true;
238
+ this.saveConfigs(configs);
239
+
240
+ // Update plist file
241
+ const plistPath = join(this.launchAgentsDir, `${label}.plist`);
242
+ writeFileSync(plistPath, this.generatePlist(config));
243
+
244
+ // Reload service
245
+ try {
246
+ execSync(`launchctl unload "${plistPath}"`, { stdio: 'inherit' });
247
+ execSync(`launchctl load "${plistPath}"`, { stdio: 'inherit' });
248
+ console.log(`✅ Launchd service '${label}' set to auto-start`);
249
+ } catch (error) {
250
+ console.error(`❌ Failed to configure auto-start: ${error}`);
251
+ throw error;
252
+ }
253
+ }
254
+
255
+ async disableAutoStart(label: string): Promise<void> {
256
+ const configs = this.loadConfigs();
257
+ const config = configs[label];
258
+
259
+ if (!config) {
260
+ throw new Error(`Service '${label}' not found`);
261
+ }
262
+
263
+ config.runAtLoad = false;
264
+ config.keepAlive = false;
265
+ this.saveConfigs(configs);
266
+
267
+ // Update plist file
268
+ const plistPath = join(this.launchAgentsDir, `${label}.plist`);
269
+ writeFileSync(plistPath, this.generatePlist(config));
270
+
271
+ // Reload service
272
+ try {
273
+ execSync(`launchctl unload "${plistPath}"`, { stdio: 'inherit' });
274
+ execSync(`launchctl load "${plistPath}"`, { stdio: 'inherit' });
275
+ console.log(`✅ Launchd service '${label}' set to manual start`);
276
+ } catch (error) {
277
+ console.error(`❌ Failed to configure auto-start: ${error}`);
278
+ throw error;
279
+ }
280
+ }
281
+ }
282
+
283
+ export async function launchdCommand(action: string, options: any): Promise<void> {
284
+ console.log('🍎 BS9 macOS Launchd Service Management');
285
+ console.log('='.repeat(80));
286
+
287
+ const manager = new LaunchdServiceManager();
288
+
289
+ try {
290
+ switch (action) {
291
+ case 'create':
292
+ if (!options.name || !options.file) {
293
+ console.error('❌ --name and --file are required for create action');
294
+ process.exit(1);
295
+ }
296
+
297
+ const config: LaunchdServiceConfig = {
298
+ label: options.name,
299
+ programArguments: [options.file, ...(options.args || [])],
300
+ workingDirectory: options.workingDir || process.cwd(),
301
+ environmentVariables: options.env ? JSON.parse(options.env) : {},
302
+ runAtLoad: options.autoStart !== false,
303
+ keepAlive: options.keepAlive !== false,
304
+ standardOutPath: options.logOut || join(homedir(), '.bs9', 'logs', `${options.name}.out.log`),
305
+ standardErrorPath: options.logErr || join(homedir(), '.bs9', 'logs', `${options.name}.err.log`)
306
+ };
307
+
308
+ await manager.createService(config);
309
+ break;
310
+
311
+ case 'start':
312
+ if (!options.name) {
313
+ console.error('❌ --name is required for start action');
314
+ process.exit(1);
315
+ }
316
+ await manager.startService(options.name);
317
+ break;
318
+
319
+ case 'stop':
320
+ if (!options.name) {
321
+ console.error('❌ --name is required for stop action');
322
+ process.exit(1);
323
+ }
324
+ await manager.stopService(options.name);
325
+ break;
326
+
327
+ case 'restart':
328
+ if (!options.name) {
329
+ console.error('❌ --name is required for restart action');
330
+ process.exit(1);
331
+ }
332
+ await manager.stopService(options.name);
333
+ await manager.startService(options.name);
334
+ break;
335
+
336
+ case 'unload':
337
+ if (!options.name) {
338
+ console.error('❌ --name is required for unload action');
339
+ process.exit(1);
340
+ }
341
+ await manager.unloadService(options.name);
342
+ break;
343
+
344
+ case 'status':
345
+ if (options.name) {
346
+ const status = await manager.getServiceStatus(options.name);
347
+ if (status) {
348
+ console.log(`📊 Service Status: ${status.label}`);
349
+ console.log(` Status: ${status.status}`);
350
+ if (status.pid) console.log(` PID: ${status.pid}`);
351
+ if (status.lastExitStatus !== undefined) console.log(` Last Exit Status: ${status.lastExitStatus}`);
352
+ } else {
353
+ console.log(`❌ Service '${options.name}' not found`);
354
+ }
355
+ } else {
356
+ const services = await manager.listServices();
357
+ console.log('📋 BS9 macOS Services:');
358
+ console.log('-'.repeat(80));
359
+ console.log('LABEL'.padEnd(30) + 'STATUS'.padEnd(15) + 'PID'.padEnd(10) + 'EXIT STATUS');
360
+ console.log('-'.repeat(80));
361
+
362
+ for (const service of services) {
363
+ console.log(
364
+ service.label.padEnd(30) +
365
+ service.status.padEnd(15) +
366
+ (service.pid?.toString() || '-').padEnd(10) +
367
+ (service.lastExitStatus?.toString() || '-')
368
+ );
369
+ }
370
+
371
+ if (services.length === 0) {
372
+ console.log('No BS9 macOS services found.');
373
+ }
374
+ }
375
+ break;
376
+
377
+ case 'enable':
378
+ if (!options.name) {
379
+ console.error('❌ --name is required for enable action');
380
+ process.exit(1);
381
+ }
382
+ await manager.enableAutoStart(options.name);
383
+ break;
384
+
385
+ case 'disable':
386
+ if (!options.name) {
387
+ console.error('❌ --name is required for disable action');
388
+ process.exit(1);
389
+ }
390
+ await manager.disableAutoStart(options.name);
391
+ break;
392
+
393
+ default:
394
+ console.error(`❌ Unknown action: ${action}`);
395
+ console.log('Available actions: create, start, stop, restart, unload, status, enable, disable');
396
+ process.exit(1);
397
+ }
398
+ } catch (error) {
399
+ console.error(`❌ Failed to ${action} macOS service: ${error}`);
400
+ process.exit(1);
401
+ }
402
+ }
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { platform } from "node:os";
4
+
5
+ export type Platform = 'linux' | 'darwin' | 'win32';
6
+
7
+ export interface PlatformInfo {
8
+ platform: Platform;
9
+ isLinux: boolean;
10
+ isMacOS: boolean;
11
+ isWindows: boolean;
12
+ serviceManager: 'systemd' | 'launchd' | 'windows-service';
13
+ configDir: string;
14
+ logDir: string;
15
+ serviceDir: string;
16
+ }
17
+
18
+ export function getPlatformInfo(): PlatformInfo {
19
+ const currentPlatform = platform() as Platform;
20
+
21
+ const baseInfo: PlatformInfo = {
22
+ platform: currentPlatform,
23
+ isLinux: currentPlatform === 'linux',
24
+ isMacOS: currentPlatform === 'darwin',
25
+ isWindows: currentPlatform === 'win32',
26
+ serviceManager: 'systemd',
27
+ configDir: '',
28
+ logDir: '',
29
+ serviceDir: ''
30
+ };
31
+
32
+ switch (currentPlatform) {
33
+ case 'linux':
34
+ baseInfo.serviceManager = 'systemd';
35
+ baseInfo.configDir = `${process.env.HOME}/.config/bs9`;
36
+ baseInfo.logDir = `${process.env.HOME}/.local/share/bs9/logs`;
37
+ baseInfo.serviceDir = `${process.env.HOME}/.config/systemd/user`;
38
+ break;
39
+
40
+ case 'darwin':
41
+ baseInfo.serviceManager = 'launchd';
42
+ baseInfo.configDir = `${process.env.HOME}/.bs9`;
43
+ baseInfo.logDir = `${process.env.HOME}/.bs9/logs`;
44
+ baseInfo.serviceDir = `${process.env.HOME}/Library/LaunchAgents`;
45
+ break;
46
+
47
+ case 'win32':
48
+ baseInfo.serviceManager = 'windows-service';
49
+ baseInfo.configDir = `${process.env.USERPROFILE}/.bs9`;
50
+ baseInfo.logDir = `${process.env.USERPROFILE}/.bs9/logs`;
51
+ baseInfo.serviceDir = `${process.env.USERPROFILE}/.bs9/services`;
52
+ break;
53
+
54
+ default:
55
+ throw new Error(`Unsupported platform: ${currentPlatform}`);
56
+ }
57
+
58
+ return baseInfo;
59
+ }
60
+
61
+ export function isSupportedPlatform(): boolean {
62
+ const supportedPlatforms: Platform[] = ['linux', 'darwin', 'win32'];
63
+ return supportedPlatforms.includes(platform() as Platform);
64
+ }
65
+
66
+ export function getPlatformSpecificCommands(): string[] {
67
+ const currentPlatform = platform() as Platform;
68
+
69
+ switch (currentPlatform) {
70
+ case 'linux':
71
+ return ['start', 'stop', 'restart', 'status', 'logs', 'monit', 'web', 'alert', 'export', 'deps', 'profile', 'loadbalancer', 'dbpool'];
72
+
73
+ case 'darwin':
74
+ return ['start', 'stop', 'restart', 'status', 'logs', 'monit', 'web', 'alert', 'export', 'deps', 'profile', 'loadbalancer', 'dbpool', 'macos'];
75
+
76
+ case 'win32':
77
+ return ['start', 'stop', 'restart', 'status', 'logs', 'monit', 'web', 'alert', 'export', 'deps', 'profile', 'loadbalancer', 'dbpool', 'windows'];
78
+
79
+ default:
80
+ return [];
81
+ }
82
+ }
83
+
84
+ export function getPlatformHelp(): string {
85
+ const currentPlatform = platform() as Platform;
86
+
87
+ switch (currentPlatform) {
88
+ case 'linux':
89
+ return `
90
+ 🐧 Linux Platform Features:
91
+ • Systemd-based service management
92
+ • User-mode service execution
93
+ • Advanced security hardening
94
+ • Resource limits and sandboxing
95
+
96
+ Available Commands:
97
+ ${getPlatformSpecificCommands().join(', ')}
98
+ `;
99
+
100
+ case 'darwin':
101
+ return `
102
+ 🍎 macOS Platform Features:
103
+ • Launchd service management
104
+ • Native macOS integration
105
+ • Automatic service recovery
106
+ • Standard macOS logging
107
+
108
+ Available Commands:
109
+ ${getPlatformSpecificCommands().join(', ')}
110
+
111
+ macOS-specific:
112
+ • bs9 macos create - Create launchd service
113
+ • bs9 macos start - Start launchd service
114
+ • bs9 macos stop - Stop launchd service
115
+ `;
116
+
117
+ case 'win32':
118
+ return `
119
+ 🪟 Windows Platform Features:
120
+ • Windows service management
121
+ • PowerShell-based automation
122
+ • Event log integration
123
+ • Service recovery policies
124
+
125
+ Available Commands:
126
+ ${getPlatformSpecificCommands().join(', ')}
127
+
128
+ Windows-specific:
129
+ • bs9 windows create - Create Windows service
130
+ • bs9 windows start - Start Windows service
131
+ • bs9 windows stop - Stop Windows service
132
+ `;
133
+
134
+ default:
135
+ return `❌ Platform ${currentPlatform} is not supported`;
136
+ }
137
+ }