bs9 1.4.6 → 1.5.3
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 +17 -1
- package/bin/bs9 +11 -10
- package/dist/bs9-cbcmk6jy. +389 -0
- package/dist/bs9-cd3btjpw. +312 -0
- package/dist/bs9-hp4w75sv. +281 -0
- package/dist/bs9-ws85s3p9. +270 -0
- package/dist/bs9-xaw2xcxz. +270 -0
- package/dist/bs9.js +1 -1
- package/package.json +12 -3
- package/src/commands/delete.ts +79 -2
- package/src/commands/restart.ts +77 -1
- package/src/commands/start.ts +93 -1
- package/src/commands/status.ts +163 -110
- package/src/commands/stop.ts +77 -1
- package/src/utils/array-parser.ts +131 -0
package/src/commands/stop.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { execSync } from "node:child_process";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { getPlatformInfo } from "../platform/detect.js";
|
|
15
|
+
import { parseServiceArray, confirmAction } from "../utils/array-parser.js";
|
|
15
16
|
|
|
16
17
|
// Security: Service name validation
|
|
17
18
|
function isValidServiceName(name: string): boolean {
|
|
@@ -21,7 +22,57 @@ function isValidServiceName(name: string): boolean {
|
|
|
21
22
|
return validPattern.test(name) && name.length <= 64 && !name.includes('..') && !name.includes('/');
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export async function stopCommand(
|
|
25
|
+
export async function stopCommand(names: string[]): Promise<void> {
|
|
26
|
+
// Handle multiple arguments
|
|
27
|
+
const name = names.length > 0 ? names.join(' ') : '';
|
|
28
|
+
|
|
29
|
+
// Handle multi-service operations
|
|
30
|
+
if (name.includes('[') || name === 'all') {
|
|
31
|
+
await handleMultiServiceStop(name);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Single service operation (existing logic)
|
|
36
|
+
await handleSingleServiceStop(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function handleMultiServiceStop(name: string): Promise<void> {
|
|
40
|
+
const services = await parseServiceArray(name);
|
|
41
|
+
|
|
42
|
+
if (services.length === 0) {
|
|
43
|
+
console.log("❌ No services found matching the pattern");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Safety confirmation for bulk operations
|
|
48
|
+
if (services.length > 1) {
|
|
49
|
+
console.log(`⚠️ About to stop ${services.length} services:`);
|
|
50
|
+
services.forEach(service => console.log(` - ${service}`));
|
|
51
|
+
|
|
52
|
+
const confirmed = await confirmAction('Are you sure? (y/N): ');
|
|
53
|
+
if (!confirmed) {
|
|
54
|
+
console.log('❌ Stop operation cancelled');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`🛑 Stopping ${services.length} services...`);
|
|
60
|
+
|
|
61
|
+
const results = await Promise.allSettled(
|
|
62
|
+
services.map(async (serviceName) => {
|
|
63
|
+
try {
|
|
64
|
+
await handleSingleServiceStop(serviceName);
|
|
65
|
+
return { service: serviceName, status: 'success', error: null };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return { service: serviceName, status: 'failed', error: error instanceof Error ? error.message : String(error) };
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
displayBatchResults(results, 'stop');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function handleSingleServiceStop(name: string): Promise<void> {
|
|
25
76
|
// Security: Validate service name
|
|
26
77
|
if (!isValidServiceName(name)) {
|
|
27
78
|
console.error(`❌ Security: Invalid service name: ${name}`);
|
|
@@ -48,3 +99,28 @@ export async function stopCommand(name: string): Promise<void> {
|
|
|
48
99
|
process.exit(1);
|
|
49
100
|
}
|
|
50
101
|
}
|
|
102
|
+
|
|
103
|
+
function displayBatchResults(results: PromiseSettledResult<{ service: string; status: string; error: string | null }>[], operation: string): void {
|
|
104
|
+
console.log(`\n📊 Batch ${operation} Results`);
|
|
105
|
+
console.log("=".repeat(50));
|
|
106
|
+
|
|
107
|
+
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 'success');
|
|
108
|
+
const failed = results.filter(r => r.status === 'fulfilled' && r.value.status === 'failed');
|
|
109
|
+
|
|
110
|
+
successful.forEach(result => {
|
|
111
|
+
if (result.status === 'fulfilled') {
|
|
112
|
+
console.log(`✅ ${result.value.service} - ${operation} successful`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
failed.forEach(result => {
|
|
117
|
+
if (result.status === 'fulfilled') {
|
|
118
|
+
console.log(`❌ ${result.value.service} - Failed: ${result.value.error}`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
console.log(`\n📈 Summary:`);
|
|
123
|
+
console.log(` Total: ${results.length} services`);
|
|
124
|
+
console.log(` Success: ${successful.length}/${results.length} (${((successful.length / results.length) * 100).toFixed(1)}%)`);
|
|
125
|
+
console.log(` Failed: ${failed.length}/${results.length} (${((failed.length / results.length) * 100).toFixed(1)}%)`);
|
|
126
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
|
|
4
|
+
export interface ServiceInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
status: string;
|
|
7
|
+
pid?: number;
|
|
8
|
+
port?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function parseServiceArray(input: string): Promise<string[]> {
|
|
12
|
+
if (!input) return [];
|
|
13
|
+
|
|
14
|
+
// Handle "all" keyword
|
|
15
|
+
if (input === 'all') {
|
|
16
|
+
return await getAllServices();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Handle array syntax [app1, app2, app-*]
|
|
20
|
+
const arrayMatch = input.match(/^\[(.*)\]$/);
|
|
21
|
+
if (!arrayMatch) {
|
|
22
|
+
return [input]; // Single service
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const services = arrayMatch[1].split(',').map(s => s.trim());
|
|
26
|
+
const expanded: string[] = [];
|
|
27
|
+
|
|
28
|
+
for (const service of services) {
|
|
29
|
+
if (service.includes('*')) {
|
|
30
|
+
// Pattern matching
|
|
31
|
+
const pattern = service.replace('*', '.*');
|
|
32
|
+
const matching = await getServicesByPattern(pattern);
|
|
33
|
+
expanded.push(...matching);
|
|
34
|
+
} else if (service === 'all') {
|
|
35
|
+
// All services
|
|
36
|
+
const allServices = await getAllServices();
|
|
37
|
+
expanded.push(...allServices);
|
|
38
|
+
} else {
|
|
39
|
+
// Specific service
|
|
40
|
+
expanded.push(service);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Remove duplicates and return
|
|
45
|
+
return [...new Set(expanded)];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getAllServices(): Promise<string[]> {
|
|
49
|
+
try {
|
|
50
|
+
const output = execSync("systemctl --user list-units --type=service --all --no-pager --no-legend", {
|
|
51
|
+
encoding: "utf-8"
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const services = output
|
|
55
|
+
.split('\n')
|
|
56
|
+
.filter(line => line.trim())
|
|
57
|
+
.map(line => line.split(' ')[0])
|
|
58
|
+
.filter(service => service.includes('bs9-'))
|
|
59
|
+
.map(service => service.replace('bs9-', ''));
|
|
60
|
+
|
|
61
|
+
return services;
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getServicesByPattern(pattern: string): Promise<string[]> {
|
|
68
|
+
try {
|
|
69
|
+
const allServices = await getAllServices();
|
|
70
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
71
|
+
return allServices.filter(service => regex.test(service));
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function getServiceInfo(serviceName: string): Promise<ServiceInfo | null> {
|
|
78
|
+
try {
|
|
79
|
+
const status = execSync(`systemctl --user is-active bs9-${serviceName}`, { encoding: "utf-8" }).trim();
|
|
80
|
+
const showOutput = execSync(`systemctl --user show bs9-${serviceName}`, { encoding: "utf-8" });
|
|
81
|
+
|
|
82
|
+
const pidMatch = showOutput.match(/MainPID=(\d+)/);
|
|
83
|
+
const portMatch = showOutput.match(/Environment=PORT=(\d+)/);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
name: serviceName,
|
|
87
|
+
status: status,
|
|
88
|
+
pid: pidMatch ? parseInt(pidMatch[1]) : undefined,
|
|
89
|
+
port: portMatch ? parseInt(portMatch[1]) : undefined
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function getMultipleServiceInfo(serviceNames: string[]): Promise<ServiceInfo[]> {
|
|
97
|
+
const results = await Promise.allSettled(
|
|
98
|
+
serviceNames.map(name => getServiceInfo(name))
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return results
|
|
102
|
+
.filter((result): result is PromiseFulfilledResult<ServiceInfo> =>
|
|
103
|
+
result.status === 'fulfilled' && result.value !== null
|
|
104
|
+
)
|
|
105
|
+
.map(result => result.value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function confirmAction(message: string): Promise<boolean> {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
process.stdout.write(message);
|
|
111
|
+
process.stdin.setRawMode(true);
|
|
112
|
+
process.stdin.resume();
|
|
113
|
+
process.stdin.setEncoding('utf8');
|
|
114
|
+
|
|
115
|
+
const onData = (key: string) => {
|
|
116
|
+
if (key === 'y' || key === 'Y') {
|
|
117
|
+
process.stdin.setRawMode(false);
|
|
118
|
+
process.stdin.pause();
|
|
119
|
+
process.stdin.off('data', onData);
|
|
120
|
+
resolve(true);
|
|
121
|
+
} else if (key === '\n' || key === '\r' || key === 'n' || key === 'N') {
|
|
122
|
+
process.stdin.setRawMode(false);
|
|
123
|
+
process.stdin.pause();
|
|
124
|
+
process.stdin.off('data', onData);
|
|
125
|
+
resolve(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
process.stdin.on('data', onData);
|
|
130
|
+
});
|
|
131
|
+
}
|