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,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
+ }