bs9 1.4.3 ā 1.5.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/README.md +41 -13
- package/bin/bs9 +33 -10
- package/dist/bs9-cbcmk6jy. +389 -0
- package/dist/bs9-cd3btjpw. +312 -0
- package/dist/bs9-hp4w75sv. +281 -0
- package/dist/bs9-nfpea7ta. +269 -0
- package/dist/bs9-ws85s3p9. +270 -0
- package/dist/bs9-xaw2xcxz. +270 -0
- package/dist/bs9-xf46r11y. +269 -0
- package/dist/bs9-zmxbnn8g. +255 -0
- package/dist/bs9.js +1 -1
- package/package.json +1 -1
- package/src/commands/delete.ts +79 -2
- package/src/commands/deploy.ts +1 -7
- package/src/commands/doctor.ts +332 -0
- package/src/commands/inspect.ts +603 -0
- 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/start.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
|
|
|
16
16
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { getPlatformInfo } from "../platform/detect.js";
|
|
19
|
+
import { parseServiceArray, getMultipleServiceInfo, confirmAction } from "../utils/array-parser.js";
|
|
19
20
|
|
|
20
21
|
// Security: Host validation function
|
|
21
22
|
function isValidHost(host: string): boolean {
|
|
@@ -60,7 +61,53 @@ interface StartOptions {
|
|
|
60
61
|
https?: boolean;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
export async function startCommand(
|
|
64
|
+
export async function startCommand(files: string[], options: StartOptions): Promise<void> {
|
|
65
|
+
const platformInfo = getPlatformInfo();
|
|
66
|
+
|
|
67
|
+
// Handle multiple arguments
|
|
68
|
+
const file = files.length > 0 ? files.join(' ') : '';
|
|
69
|
+
|
|
70
|
+
// Handle multi-service operations
|
|
71
|
+
if (file.includes('[') || file === 'all') {
|
|
72
|
+
await handleMultiServiceStart(file, options);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Single service operation (existing logic)
|
|
77
|
+
await handleSingleServiceStart(file, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handleMultiServiceStart(file: string, options: StartOptions): Promise<void> {
|
|
81
|
+
const services = await parseServiceArray(file);
|
|
82
|
+
|
|
83
|
+
if (services.length === 0) {
|
|
84
|
+
console.log("ā No services found matching the pattern");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`š Starting ${services.length} services...`);
|
|
89
|
+
|
|
90
|
+
const results = await Promise.allSettled(
|
|
91
|
+
services.map(async (serviceName) => {
|
|
92
|
+
try {
|
|
93
|
+
// For multi-service, we need to find the service file
|
|
94
|
+
const serviceFile = findServiceFile(serviceName);
|
|
95
|
+
if (!serviceFile) {
|
|
96
|
+
throw new Error(`Service file not found for: ${serviceName}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await handleSingleServiceStart(serviceFile, { ...options, name: serviceName });
|
|
100
|
+
return { service: serviceName, status: 'success', error: null };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return { service: serviceName, status: 'failed', error: error instanceof Error ? error.message : String(error) };
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
displayBatchResults(results, 'start');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function handleSingleServiceStart(file: string, options: StartOptions): Promise<void> {
|
|
64
111
|
const platformInfo = getPlatformInfo();
|
|
65
112
|
|
|
66
113
|
// Security: Validate and sanitize file path
|
|
@@ -166,6 +213,51 @@ export async function startCommand(file: string, options: StartOptions): Promise
|
|
|
166
213
|
}
|
|
167
214
|
}
|
|
168
215
|
|
|
216
|
+
function findServiceFile(serviceName: string): string | null {
|
|
217
|
+
// Try to find the service file in common locations
|
|
218
|
+
const possiblePaths = [
|
|
219
|
+
join(process.cwd(), `${serviceName}.js`),
|
|
220
|
+
join(process.cwd(), `${serviceName}.ts`),
|
|
221
|
+
join(process.cwd(), 'src', `${serviceName}.js`),
|
|
222
|
+
join(process.cwd(), 'src', `${serviceName}.ts`),
|
|
223
|
+
join(process.cwd(), 'app', `${serviceName}.js`),
|
|
224
|
+
join(process.cwd(), 'app', `${serviceName}.ts`),
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
for (const path of possiblePaths) {
|
|
228
|
+
if (existsSync(path)) {
|
|
229
|
+
return path;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function displayBatchResults(results: PromiseSettledResult<{ service: string; status: string; error: string | null }>[], operation: string): void {
|
|
237
|
+
console.log(`\nš Batch ${operation} Results`);
|
|
238
|
+
console.log("=".repeat(50));
|
|
239
|
+
|
|
240
|
+
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 'success');
|
|
241
|
+
const failed = results.filter(r => r.status === 'fulfilled' && r.value.status === 'failed');
|
|
242
|
+
|
|
243
|
+
successful.forEach(result => {
|
|
244
|
+
if (result.status === 'fulfilled') {
|
|
245
|
+
console.log(`ā
${result.value.service} - ${operation} successful`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
failed.forEach(result => {
|
|
250
|
+
if (result.status === 'fulfilled') {
|
|
251
|
+
console.log(`ā ${result.value.service} - Failed: ${result.value.error}`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
console.log(`\nš Summary:`);
|
|
256
|
+
console.log(` Total: ${results.length} services`);
|
|
257
|
+
console.log(` Success: ${successful.length}/${results.length} (${((successful.length / results.length) * 100).toFixed(1)}%)`);
|
|
258
|
+
console.log(` Failed: ${failed.length}/${results.length} (${((failed.length / results.length) * 100).toFixed(1)}%)`);
|
|
259
|
+
}
|
|
260
|
+
|
|
169
261
|
async function createLinuxService(serviceName: string, execPath: string, host: string, port: string, protocol: string, options: StartOptions): Promise<void> {
|
|
170
262
|
// Phase 1: Generate hardened systemd unit
|
|
171
263
|
const unitContent = generateSystemdUnit({
|
package/src/commands/status.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { execSync } from "node:child_process";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { getPlatformInfo } from "../platform/detect.js";
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
|
+
import { parseServiceArray, getMultipleServiceInfo } from "../utils/array-parser.js";
|
|
16
17
|
|
|
17
18
|
interface StatusOptions {
|
|
18
19
|
watch?: boolean;
|
|
@@ -34,7 +35,56 @@ interface ServiceStatus {
|
|
|
34
35
|
lastRestart?: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export async function statusCommand(options: StatusOptions): Promise<void> {
|
|
38
|
+
export async function statusCommand(options: StatusOptions, names?: string[]): Promise<void> {
|
|
39
|
+
const platformInfo = getPlatformInfo();
|
|
40
|
+
|
|
41
|
+
// Handle multiple arguments
|
|
42
|
+
const name = names && names.length > 0 ? names.join(' ') : undefined;
|
|
43
|
+
|
|
44
|
+
// Handle multi-service status
|
|
45
|
+
if (name && (name.includes('[') || name === 'all')) {
|
|
46
|
+
await handleMultiServiceStatus(name, options);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Single service or all services status (existing logic)
|
|
51
|
+
await handleStatus(options, name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function handleMultiServiceStatus(name: string, options: StatusOptions): Promise<void> {
|
|
55
|
+
const services = await parseServiceArray(name);
|
|
56
|
+
|
|
57
|
+
if (services.length === 0) {
|
|
58
|
+
console.log("ā No services found matching the pattern");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(`š Multi-Service Status: ${name}`);
|
|
63
|
+
console.log("=".repeat(80));
|
|
64
|
+
|
|
65
|
+
const serviceInfo = await getMultipleServiceInfo(services);
|
|
66
|
+
|
|
67
|
+
if (serviceInfo.length === 0) {
|
|
68
|
+
console.log("ā No running services found");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
displayMultiServiceStatus(serviceInfo, name);
|
|
73
|
+
|
|
74
|
+
if (options.watch) {
|
|
75
|
+
console.log("\nš Watching for changes (Ctrl+C to stop)...");
|
|
76
|
+
setInterval(async () => {
|
|
77
|
+
console.clear();
|
|
78
|
+
console.log(`š Multi-Service Status: ${name}`);
|
|
79
|
+
console.log("=".repeat(80));
|
|
80
|
+
|
|
81
|
+
const updatedServiceInfo = await getMultipleServiceInfo(services);
|
|
82
|
+
displayMultiServiceStatus(updatedServiceInfo, name);
|
|
83
|
+
}, 2000);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handleStatus(options: StatusOptions, name?: string): Promise<void> {
|
|
38
88
|
const platformInfo = getPlatformInfo();
|
|
39
89
|
|
|
40
90
|
try {
|
|
@@ -48,6 +98,11 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
|
|
|
48
98
|
services = await getWindowsServices();
|
|
49
99
|
}
|
|
50
100
|
|
|
101
|
+
// Filter by specific service if provided
|
|
102
|
+
if (name) {
|
|
103
|
+
services = services.filter(service => service.name === name);
|
|
104
|
+
}
|
|
105
|
+
|
|
51
106
|
displayServices(services);
|
|
52
107
|
|
|
53
108
|
if (options.watch) {
|
|
@@ -66,15 +121,120 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
|
|
|
66
121
|
updatedServices = await getWindowsServices();
|
|
67
122
|
}
|
|
68
123
|
|
|
124
|
+
// Filter by specific service if provided
|
|
125
|
+
if (name) {
|
|
126
|
+
updatedServices = updatedServices.filter(service => service.name === name);
|
|
127
|
+
}
|
|
128
|
+
|
|
69
129
|
displayServices(updatedServices);
|
|
70
130
|
}, 2000);
|
|
71
131
|
}
|
|
72
|
-
} catch (
|
|
73
|
-
console.error("ā Failed to get service status:",
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error("ā Failed to get service status:", error);
|
|
74
134
|
process.exit(1);
|
|
75
135
|
}
|
|
76
136
|
}
|
|
77
137
|
|
|
138
|
+
function displayServices(services: ServiceStatus[]): void {
|
|
139
|
+
if (services.length === 0) {
|
|
140
|
+
console.log("š No BS9 services found");
|
|
141
|
+
console.log("š” Use 'bs9 start <file>' or 'bs9 deploy <file>' to create a service");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Header with better formatting
|
|
146
|
+
console.log(`${"SERVICE".padEnd(18)} ${"STATUS".padEnd(15)} ${"CPU".padEnd(10)} ${"MEMORY".padEnd(12)} ${"UPTIME".padEnd(12)} ${"TASKS".padEnd(8)} DESCRIPTION`);
|
|
147
|
+
console.log("ā".repeat(100));
|
|
148
|
+
|
|
149
|
+
// Sort services by status (running first, then by name)
|
|
150
|
+
const sortedServices = services.sort((a, b) => {
|
|
151
|
+
const aRunning = a.active === "active" && a.sub === "running";
|
|
152
|
+
const bRunning = b.active === "active" && a.sub === "running";
|
|
153
|
+
if (aRunning !== bRunning) return bRunning ? 1 : -1;
|
|
154
|
+
return a.name.localeCompare(b.name);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
for (const svc of sortedServices) {
|
|
158
|
+
// Better status formatting with indicators
|
|
159
|
+
let statusIndicator = "";
|
|
160
|
+
let status = `${svc.active}/${svc.sub}`;
|
|
161
|
+
|
|
162
|
+
if (svc.active === "active" && svc.sub === "running") {
|
|
163
|
+
statusIndicator = "ā
";
|
|
164
|
+
status = "running";
|
|
165
|
+
} else if (svc.active === "activating" && svc.sub.includes("auto-restart")) {
|
|
166
|
+
statusIndicator = "š";
|
|
167
|
+
status = "restarting";
|
|
168
|
+
} else if (svc.active === "failed" || svc.sub === "failed") {
|
|
169
|
+
statusIndicator = "ā";
|
|
170
|
+
status = "failed";
|
|
171
|
+
} else if (svc.active === "inactive") {
|
|
172
|
+
statusIndicator = "āøļø";
|
|
173
|
+
status = "stopped";
|
|
174
|
+
} else {
|
|
175
|
+
statusIndicator = "ā ļø";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const displayStatus = `${statusIndicator} ${status}`;
|
|
179
|
+
|
|
180
|
+
// Format memory and uptime better
|
|
181
|
+
const formattedMemory = svc.memory ? formatMemory(parseMemory(svc.memory)) : "-";
|
|
182
|
+
const formattedUptime = svc.uptime || "-";
|
|
183
|
+
const formattedCPU = svc.cpu || "-";
|
|
184
|
+
|
|
185
|
+
console.log(
|
|
186
|
+
`${svc.name.padEnd(18)} ${displayStatus.padEnd(15)} ${formattedCPU.padEnd(10)} ${formattedMemory.padEnd(12)} ${formattedUptime.padEnd(12)} ${(svc.tasks || "-").padEnd(8)} ${svc.description}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Enhanced summary with better formatting
|
|
191
|
+
console.log("\nš Service Summary:");
|
|
192
|
+
const totalServices = services.length;
|
|
193
|
+
const runningServices = services.filter(s => s.active === "active" && s.sub === "running").length;
|
|
194
|
+
const failedServices = services.filter(s => s.active === "failed" || s.sub === "failed").length;
|
|
195
|
+
const restartingServices = services.filter(s => s.active === "activating" && s.sub.includes("auto-restart")).length;
|
|
196
|
+
const totalMemory = services.reduce((sum, s) => sum + (s.memory ? parseMemory(s.memory) : 0), 0);
|
|
197
|
+
|
|
198
|
+
console.log(` š Status: ${runningServices} running, ${failedServices} failed, ${restartingServices} restarting`);
|
|
199
|
+
console.log(` š¦ Total: ${runningServices}/${totalServices} services running`);
|
|
200
|
+
console.log(` š¾ Memory: ${formatMemory(totalMemory)}`);
|
|
201
|
+
console.log(` š Last updated: ${new Date().toLocaleString()}`);
|
|
202
|
+
|
|
203
|
+
// Show failed services details with better formatting
|
|
204
|
+
if (failedServices > 0) {
|
|
205
|
+
console.log("\nšØ Failed Services:");
|
|
206
|
+
console.log("-".repeat(80));
|
|
207
|
+
|
|
208
|
+
services.filter(s => s.active === "failed" || s.sub === "failed").forEach(svc => {
|
|
209
|
+
console.log(` ā ${svc.name} - ${svc.description}`);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function displayMultiServiceStatus(serviceInfo: any[], pattern: string): void {
|
|
215
|
+
const running = serviceInfo.filter(s => s.status === 'active');
|
|
216
|
+
const failed = serviceInfo.filter(s => s.status === 'failed');
|
|
217
|
+
const inactive = serviceInfo.filter(s => s.status === 'inactive');
|
|
218
|
+
|
|
219
|
+
console.log(`\nš Services matching pattern: ${pattern}`);
|
|
220
|
+
console.log(` Total: ${serviceInfo.length} services`);
|
|
221
|
+
console.log(` Running: ${running.length}/${serviceInfo.length} (${((running.length / serviceInfo.length) * 100).toFixed(1)}%)`);
|
|
222
|
+
console.log(` Failed: ${failed.length}/${serviceInfo.length} (${((failed.length / serviceInfo.length) * 100).toFixed(1)}%)`);
|
|
223
|
+
console.log(` Inactive: ${inactive.length}/${serviceInfo.length} (${((inactive.length / serviceInfo.length) * 100).toFixed(1)}%)`);
|
|
224
|
+
|
|
225
|
+
if (serviceInfo.length > 0) {
|
|
226
|
+
console.log("\nš Service Details:");
|
|
227
|
+
console.log("-".repeat(80));
|
|
228
|
+
|
|
229
|
+
serviceInfo.forEach(service => {
|
|
230
|
+
const statusIcon = service.status === 'active' ? 'ā
' :
|
|
231
|
+
service.status === 'failed' ? 'ā' : 'āøļø';
|
|
232
|
+
|
|
233
|
+
console.log(`${statusIcon} ${service.name.padEnd(20)} PID: ${service.pid?.toString().padStart(8) || '-'.padStart(8)} PORT: ${service.port?.toString().padStart(6) || '-'.padStart(6)} STATUS: ${service.status.padEnd(10)}`);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
78
238
|
async function getLinuxServices(): Promise<ServiceStatus[]> {
|
|
79
239
|
const services: ServiceStatus[] = [];
|
|
80
240
|
const platformInfo = getPlatformInfo();
|
|
@@ -208,113 +368,6 @@ async function getWindowsServices(): Promise<ServiceStatus[]> {
|
|
|
208
368
|
return services;
|
|
209
369
|
}
|
|
210
370
|
|
|
211
|
-
function displayServices(services: ServiceStatus[]): void {
|
|
212
|
-
if (services.length === 0) {
|
|
213
|
-
console.log("š No BS9 services found");
|
|
214
|
-
console.log("š” Use 'bs9 start <file>' or 'bs9 deploy <file>' to create a service");
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Header with better formatting
|
|
219
|
-
console.log(`${"SERVICE".padEnd(18)} ${"STATUS".padEnd(15)} ${"CPU".padEnd(10)} ${"MEMORY".padEnd(12)} ${"UPTIME".padEnd(12)} ${"TASKS".padEnd(8)} DESCRIPTION`);
|
|
220
|
-
console.log("ā".repeat(100));
|
|
221
|
-
|
|
222
|
-
// Sort services by status (running first, then by name)
|
|
223
|
-
const sortedServices = services.sort((a, b) => {
|
|
224
|
-
const aRunning = a.active === "active" && a.sub === "running";
|
|
225
|
-
const bRunning = b.active === "active" && b.sub === "running";
|
|
226
|
-
if (aRunning !== bRunning) return bRunning ? 1 : -1;
|
|
227
|
-
return a.name.localeCompare(b.name);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
for (const svc of sortedServices) {
|
|
231
|
-
// Better status formatting with indicators
|
|
232
|
-
let statusIndicator = "";
|
|
233
|
-
let status = `${svc.active}/${svc.sub}`;
|
|
234
|
-
|
|
235
|
-
if (svc.active === "active" && svc.sub === "running") {
|
|
236
|
-
statusIndicator = "ā
";
|
|
237
|
-
status = "running";
|
|
238
|
-
} else if (svc.active === "activating" && svc.sub.includes("auto-restart")) {
|
|
239
|
-
statusIndicator = "š";
|
|
240
|
-
status = "restarting";
|
|
241
|
-
} else if (svc.active === "failed" || svc.sub === "failed") {
|
|
242
|
-
statusIndicator = "ā";
|
|
243
|
-
status = "failed";
|
|
244
|
-
} else if (svc.active === "inactive") {
|
|
245
|
-
statusIndicator = "āøļø";
|
|
246
|
-
status = "stopped";
|
|
247
|
-
} else {
|
|
248
|
-
statusIndicator = "ā ļø";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const displayStatus = `${statusIndicator} ${status}`;
|
|
252
|
-
|
|
253
|
-
// Format memory and uptime better
|
|
254
|
-
const formattedMemory = svc.memory ? formatMemory(parseMemory(svc.memory)) : "-";
|
|
255
|
-
const formattedUptime = svc.uptime || "-";
|
|
256
|
-
const formattedCPU = svc.cpu || "-";
|
|
257
|
-
|
|
258
|
-
console.log(
|
|
259
|
-
`${svc.name.padEnd(18)} ${displayStatus.padEnd(15)} ${formattedCPU.padEnd(10)} ${formattedMemory.padEnd(12)} ${formattedUptime.padEnd(12)} ${(svc.tasks || "-").padEnd(8)} ${svc.description}`
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Enhanced summary with better formatting
|
|
264
|
-
console.log("\nš Service Summary:");
|
|
265
|
-
const totalServices = services.length;
|
|
266
|
-
const runningServices = services.filter(s => s.active === "active" && s.sub === "running").length;
|
|
267
|
-
const failedServices = services.filter(s => s.active === "failed" || s.sub === "failed").length;
|
|
268
|
-
const restartingServices = services.filter(s => s.active === "activating" && s.sub.includes("auto-restart")).length;
|
|
269
|
-
const totalMemory = services.reduce((sum, s) => sum + (s.memory ? parseMemory(s.memory) : 0), 0);
|
|
270
|
-
|
|
271
|
-
console.log(` š Status: ${runningServices} running, ${failedServices} failed, ${restartingServices} restarting`);
|
|
272
|
-
console.log(` š¦ Total: ${runningServices}/${totalServices} services running`);
|
|
273
|
-
console.log(` š¾ Memory: ${formatMemory(totalMemory)}`);
|
|
274
|
-
console.log(` š Last updated: ${new Date().toLocaleString()}`);
|
|
275
|
-
|
|
276
|
-
// Show failed services details with better formatting
|
|
277
|
-
if (failedServices > 0) {
|
|
278
|
-
console.log("\nā Failed Services:");
|
|
279
|
-
const failed = services.filter(s => s.active === "failed" || s.sub === "failed");
|
|
280
|
-
for (const svc of failed) {
|
|
281
|
-
console.log(` ⢠${svc.name}: ${svc.active}/${svc.sub}`);
|
|
282
|
-
console.log(` š” Troubleshoot: bs9 logs ${svc.name} --tail 20`);
|
|
283
|
-
console.log(` š” Check: bs9 status ${svc.name}`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Show restarting services details
|
|
288
|
-
if (restartingServices > 0) {
|
|
289
|
-
console.log("\nš Restarting Services:");
|
|
290
|
-
const restarting = services.filter(s => s.active === "activating" && s.sub.includes("auto-restart"));
|
|
291
|
-
for (const svc of restarting) {
|
|
292
|
-
console.log(` ⢠${svc.name}: ${svc.active}/${svc.sub}`);
|
|
293
|
-
if (svc.exitCode) {
|
|
294
|
-
console.log(` ā Exit Code: ${svc.exitCode}`);
|
|
295
|
-
}
|
|
296
|
-
if (svc.lastRestart) {
|
|
297
|
-
console.log(` š Last Restart: ${svc.lastRestart}`);
|
|
298
|
-
}
|
|
299
|
-
console.log(` š” Troubleshoot: bs9 logs ${svc.name} --tail 20`);
|
|
300
|
-
console.log(` š” Check: bs9 status ${svc.name}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Show running services details
|
|
305
|
-
if (runningServices > 0) {
|
|
306
|
-
console.log("\nā
Running Services:");
|
|
307
|
-
const running = services.filter(s => s.active === "active" && s.sub === "running");
|
|
308
|
-
for (const svc of running) {
|
|
309
|
-
const memory = svc.memory ? formatMemory(parseMemory(svc.memory)) : "N/A";
|
|
310
|
-
const uptime = svc.uptime || "N/A";
|
|
311
|
-
const port = svc.port || "3000";
|
|
312
|
-
console.log(` ⢠${svc.name}: ${memory} memory, ${uptime} uptime`);
|
|
313
|
-
console.log(` š Access: http://localhost:${port}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
371
|
function formatCPU(nsec: number): string {
|
|
319
372
|
const ms = nsec / 1_000_000;
|
|
320
373
|
return `${ms.toFixed(1)}ms`;
|
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
|
+
}
|