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.
- package/README.md +105 -9
- package/bin/bs9 +91 -7
- package/dist/bs9-064xs9r9. +148 -0
- package/dist/bs9-0gqcrp5t. +144 -0
- package/dist/bs9-33vcpmb9. +181 -0
- package/dist/bs9-nv7nseny. +197 -0
- package/dist/bs9-r6b9zpw0. +156 -0
- package/dist/bs9.js +2 -0
- package/package.json +3 -4
- package/src/commands/deps.ts +295 -0
- package/src/commands/profile.ts +338 -0
- package/src/commands/restart.ts +28 -3
- package/src/commands/start.ts +201 -21
- package/src/commands/status.ts +154 -109
- package/src/commands/stop.ts +31 -3
- package/src/commands/update.ts +322 -0
- package/src/commands/web.ts +29 -3
- package/src/database/pool.ts +335 -0
- package/src/discovery/consul.ts +285 -0
- package/src/loadbalancer/manager.ts +481 -0
- package/src/macos/launchd.ts +402 -0
- package/src/monitoring/advanced.ts +341 -0
- package/src/platform/detect.ts +137 -0
- package/src/windows/service.ts +391 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// Security: Input validation functions
|
|
4
|
+
function isValidHost(host: string): boolean {
|
|
5
|
+
const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
6
|
+
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
7
|
+
const localhostRegex = /^(localhost|127\.0\.0\.1)$/;
|
|
8
|
+
|
|
9
|
+
return hostnameRegex.test(host) && host.length <= 253 ||
|
|
10
|
+
ipv4Regex.test(host) ||
|
|
11
|
+
localhostRegex.test(host);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isValidDatabaseName(name: string): boolean {
|
|
15
|
+
// Allow alphanumeric, underscores, and hyphens only
|
|
16
|
+
return /^[a-zA-Z0-9_-]+$/.test(name) && name.length <= 64;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isValidUsername(username: string): boolean {
|
|
20
|
+
// Allow alphanumeric, underscores, and hyphens only
|
|
21
|
+
return /^[a-zA-Z0-9_-]+$/.test(username) && username.length <= 32;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sanitizeSQL(sql: string): string {
|
|
25
|
+
// Basic SQL injection prevention
|
|
26
|
+
const dangerousPatterns = [
|
|
27
|
+
/drop\s+table/i,
|
|
28
|
+
/delete\s+from/i,
|
|
29
|
+
/truncate\s+table/i,
|
|
30
|
+
/exec\s*\(/i,
|
|
31
|
+
/xp_cmdshell/i,
|
|
32
|
+
/sp_executesql/i,
|
|
33
|
+
/union\s+select/i,
|
|
34
|
+
/insert\s+into/i,
|
|
35
|
+
/update\s+set/i
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const pattern of dangerousPatterns) {
|
|
39
|
+
if (pattern.test(sql)) {
|
|
40
|
+
throw new Error(`❌ Security: Dangerous SQL pattern detected`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return sql.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface DatabaseConnection {
|
|
48
|
+
id: string;
|
|
49
|
+
created: number;
|
|
50
|
+
lastUsed: number;
|
|
51
|
+
inUse: boolean;
|
|
52
|
+
host: string;
|
|
53
|
+
port: number;
|
|
54
|
+
database: string;
|
|
55
|
+
username: string;
|
|
56
|
+
checkedOut: boolean;
|
|
57
|
+
query: (sql: string, params?: any[]) => Promise<any[]>;
|
|
58
|
+
close: () => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DatabaseConfig {
|
|
62
|
+
host: string;
|
|
63
|
+
port: number;
|
|
64
|
+
database: string;
|
|
65
|
+
username: string;
|
|
66
|
+
password: string;
|
|
67
|
+
ssl?: boolean;
|
|
68
|
+
maxConnections?: number;
|
|
69
|
+
minConnections?: number;
|
|
70
|
+
acquireTimeoutMillis?: number;
|
|
71
|
+
idleTimeoutMillis?: number;
|
|
72
|
+
reapIntervalMillis?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface PoolStats {
|
|
76
|
+
totalConnections: number;
|
|
77
|
+
activeConnections: number;
|
|
78
|
+
idleConnections: number;
|
|
79
|
+
waitingClients: number;
|
|
80
|
+
maxConnections: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class DatabasePool {
|
|
84
|
+
private config: DatabaseConfig;
|
|
85
|
+
private connections: DatabaseConnection[] = [];
|
|
86
|
+
private waitingQueue: Array<{
|
|
87
|
+
resolve: (connection: DatabaseConnection) => void;
|
|
88
|
+
reject: (error: Error) => void;
|
|
89
|
+
timestamp: number;
|
|
90
|
+
}> = [];
|
|
91
|
+
private reapingInterval?: NodeJS.Timeout;
|
|
92
|
+
|
|
93
|
+
constructor(config: DatabaseConfig) {
|
|
94
|
+
// Security: Validate configuration
|
|
95
|
+
if (!isValidHost(config.host)) {
|
|
96
|
+
throw new Error(`❌ Security: Invalid database host: ${config.host}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isValidDatabaseName(config.database)) {
|
|
100
|
+
throw new Error(`❌ Security: Invalid database name: ${config.database}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!isValidUsername(config.username)) {
|
|
104
|
+
throw new Error(`❌ Security: Invalid database username: ${config.username}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const portNum = Number(config.port);
|
|
108
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
109
|
+
throw new Error(`❌ Security: Invalid port number: ${config.port}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.config = {
|
|
113
|
+
maxConnections: 10,
|
|
114
|
+
minConnections: 2,
|
|
115
|
+
acquireTimeoutMillis: 30000,
|
|
116
|
+
idleTimeoutMillis: 30000,
|
|
117
|
+
reapIntervalMillis: 1000,
|
|
118
|
+
...config,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.startReaper();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async createConnection(): Promise<DatabaseConnection> {
|
|
125
|
+
const id = `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
|
|
128
|
+
// Simulate database connection
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
created: now,
|
|
134
|
+
lastUsed: now,
|
|
135
|
+
inUse: false,
|
|
136
|
+
host: this.config.host,
|
|
137
|
+
port: this.config.port,
|
|
138
|
+
database: this.config.database,
|
|
139
|
+
username: this.config.username,
|
|
140
|
+
checkedOut: false,
|
|
141
|
+
query: async (sql: string, params?: any[]) => {
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, Math.random() * 50 + 10));
|
|
143
|
+
return [{ id: 1, data: 'mock_result' }];
|
|
144
|
+
},
|
|
145
|
+
close: async () => {
|
|
146
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async acquire(): Promise<DatabaseConnection> {
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
|
|
154
|
+
const availableConnection = this.connections.find(conn => !conn.inUse);
|
|
155
|
+
if (availableConnection) {
|
|
156
|
+
availableConnection.inUse = true;
|
|
157
|
+
availableConnection.lastUsed = now;
|
|
158
|
+
return availableConnection;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this.connections.length < this.config.maxConnections!) {
|
|
162
|
+
const newConnection = await this.createConnection();
|
|
163
|
+
newConnection.inUse = true;
|
|
164
|
+
this.connections.push(newConnection);
|
|
165
|
+
return newConnection;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const timeoutId = setTimeout(() => {
|
|
170
|
+
const index = this.waitingQueue.findIndex(item => item.resolve === resolve);
|
|
171
|
+
if (index !== -1) {
|
|
172
|
+
this.waitingQueue.splice(index, 1);
|
|
173
|
+
}
|
|
174
|
+
reject(new Error('Connection acquire timeout'));
|
|
175
|
+
}, this.config.acquireTimeoutMillis);
|
|
176
|
+
|
|
177
|
+
this.waitingQueue.push({
|
|
178
|
+
resolve: (connection) => {
|
|
179
|
+
clearTimeout(timeoutId);
|
|
180
|
+
resolve(connection);
|
|
181
|
+
},
|
|
182
|
+
reject: (error) => {
|
|
183
|
+
clearTimeout(timeoutId);
|
|
184
|
+
reject(error);
|
|
185
|
+
},
|
|
186
|
+
timestamp: now,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async release(connection: DatabaseConnection): Promise<void> {
|
|
192
|
+
connection.inUse = false;
|
|
193
|
+
connection.lastUsed = Date.now();
|
|
194
|
+
|
|
195
|
+
const waiting = this.waitingQueue.shift();
|
|
196
|
+
if (waiting) {
|
|
197
|
+
connection.inUse = true;
|
|
198
|
+
waiting.resolve(connection);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async startReaper(): Promise<void> {
|
|
203
|
+
this.reapingInterval = setInterval(async () => {
|
|
204
|
+
await this.reapIdleConnections();
|
|
205
|
+
}, this.config.reapIntervalMillis);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async reapIdleConnections(): Promise<void> {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const minConnections = this.config.minConnections!;
|
|
211
|
+
|
|
212
|
+
const idleConnections = this.connections.filter(conn =>
|
|
213
|
+
!conn.inUse &&
|
|
214
|
+
now - conn.lastUsed > this.config.idleTimeoutMillis! &&
|
|
215
|
+
this.connections.length > minConnections
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
for (const conn of idleConnections) {
|
|
219
|
+
const index = this.connections.indexOf(conn);
|
|
220
|
+
if (index !== -1) {
|
|
221
|
+
await conn.close();
|
|
222
|
+
this.connections.splice(index, 1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getStats(): PoolStats {
|
|
228
|
+
const activeConnections = this.connections.filter(conn => conn.inUse).length;
|
|
229
|
+
const idleConnections = this.connections.filter(conn => !conn.inUse).length;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
totalConnections: this.connections.length,
|
|
233
|
+
activeConnections,
|
|
234
|
+
idleConnections,
|
|
235
|
+
waitingClients: this.waitingQueue.length,
|
|
236
|
+
maxConnections: this.config.maxConnections!,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async close(): Promise<void> {
|
|
241
|
+
if (this.reapingInterval) {
|
|
242
|
+
clearInterval(this.reapingInterval);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const closePromises = this.connections.map(conn => conn.close());
|
|
246
|
+
await Promise.all(closePromises);
|
|
247
|
+
this.connections = [];
|
|
248
|
+
|
|
249
|
+
for (const waiting of this.waitingQueue) {
|
|
250
|
+
waiting.reject(new Error('Pool is closing'));
|
|
251
|
+
}
|
|
252
|
+
this.waitingQueue = [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async testConnection(): Promise<boolean> {
|
|
256
|
+
try {
|
|
257
|
+
const conn = await this.acquire();
|
|
258
|
+
await conn.query('SELECT 1');
|
|
259
|
+
await this.release(conn);
|
|
260
|
+
return true;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function dbpoolCommand(action: string, options: any): Promise<void> {
|
|
268
|
+
console.log('🗄️ BS9 Database Connection Pool Management');
|
|
269
|
+
console.log('='.repeat(80));
|
|
270
|
+
|
|
271
|
+
const config: DatabaseConfig = {
|
|
272
|
+
host: options.host || 'localhost',
|
|
273
|
+
port: parseInt(options.port) || 5432,
|
|
274
|
+
database: options.database || 'testdb',
|
|
275
|
+
username: options.username || 'user',
|
|
276
|
+
password: options.password || 'password',
|
|
277
|
+
maxConnections: parseInt(options.maxConnections) || 10,
|
|
278
|
+
minConnections: parseInt(options.minConnections) || 2,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
switch (action) {
|
|
283
|
+
case 'start':
|
|
284
|
+
console.log(`🚀 Starting database pool...`);
|
|
285
|
+
console.log(` Host: ${config.host}:${config.port}`);
|
|
286
|
+
console.log(` Database: ${config.database}`);
|
|
287
|
+
console.log(` Max Connections: ${config.maxConnections}`);
|
|
288
|
+
console.log(` Min Connections: ${config.minConnections}`);
|
|
289
|
+
|
|
290
|
+
const pool = new DatabasePool(config);
|
|
291
|
+
const isConnected = await pool.testConnection();
|
|
292
|
+
|
|
293
|
+
if (isConnected) {
|
|
294
|
+
console.log('✅ Database pool started successfully');
|
|
295
|
+
const stats = pool.getStats();
|
|
296
|
+
console.log(`📊 Initial Stats: ${stats.totalConnections} connections`);
|
|
297
|
+
console.log('✅ Pool management commands ready');
|
|
298
|
+
} else {
|
|
299
|
+
console.error('❌ Failed to connect to database');
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
|
|
304
|
+
case 'test':
|
|
305
|
+
console.log('🧪 Testing database pool...');
|
|
306
|
+
const testPool = new DatabasePool(config);
|
|
307
|
+
const connected = await testPool.testConnection();
|
|
308
|
+
console.log(connected ? '✅ Connection test passed' : '❌ Connection test failed');
|
|
309
|
+
await testPool.close();
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case 'stats':
|
|
313
|
+
console.log('📊 Database Pool Statistics');
|
|
314
|
+
console.log('-'.repeat(40));
|
|
315
|
+
const statsPool = new DatabasePool(config);
|
|
316
|
+
const stats = statsPool.getStats();
|
|
317
|
+
console.log(`Total Connections: ${stats.totalConnections}`);
|
|
318
|
+
console.log(`Active Connections: ${stats.activeConnections}`);
|
|
319
|
+
console.log(`Idle Connections: ${stats.idleConnections}`);
|
|
320
|
+
console.log(`Waiting Clients: ${stats.waitingClients}`);
|
|
321
|
+
console.log(`Max Connections: ${stats.maxConnections}`);
|
|
322
|
+
console.log(`Pool Utilization: ${((stats.activeConnections / stats.maxConnections) * 100).toFixed(1)}%`);
|
|
323
|
+
await statsPool.close();
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
default:
|
|
327
|
+
console.error(`❌ Unknown action: ${action}`);
|
|
328
|
+
console.log('Available actions: start, test, stats');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error(`❌ Failed to ${action} pool: ${error}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
interface ConsulService {
|
|
6
|
+
ID: string;
|
|
7
|
+
Name: string;
|
|
8
|
+
Tags: string[];
|
|
9
|
+
Address: string;
|
|
10
|
+
Port: number;
|
|
11
|
+
EnableTagOverride: boolean;
|
|
12
|
+
Meta: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ConsulHealthCheck {
|
|
16
|
+
HTTP?: string;
|
|
17
|
+
TCP?: string;
|
|
18
|
+
Interval: string;
|
|
19
|
+
Timeout: string;
|
|
20
|
+
DeregisterCriticalServiceAfter: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ConsulRegistration {
|
|
24
|
+
Name: string;
|
|
25
|
+
ID: string;
|
|
26
|
+
Tags: string[];
|
|
27
|
+
Address: string;
|
|
28
|
+
Port: number;
|
|
29
|
+
EnableTagOverride: boolean;
|
|
30
|
+
Check: ConsulHealthCheck;
|
|
31
|
+
Meta?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class ConsulServiceDiscovery {
|
|
35
|
+
private consulUrl: string;
|
|
36
|
+
private serviceName: string = '';
|
|
37
|
+
private serviceId: string = '';
|
|
38
|
+
private registeredServices: Map<string, ConsulService> = new Map();
|
|
39
|
+
|
|
40
|
+
constructor(consulUrl: string = 'http://localhost:8500') {
|
|
41
|
+
this.consulUrl = consulUrl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async registerService(config: {
|
|
45
|
+
name: string;
|
|
46
|
+
id: string;
|
|
47
|
+
address: string;
|
|
48
|
+
port: number;
|
|
49
|
+
tags?: string[];
|
|
50
|
+
healthCheck?: string;
|
|
51
|
+
meta?: Record<string, string>;
|
|
52
|
+
}): Promise<void> {
|
|
53
|
+
const registration: ConsulRegistration = {
|
|
54
|
+
Name: config.name,
|
|
55
|
+
ID: config.id,
|
|
56
|
+
Tags: config.tags || ['bs9', 'service'],
|
|
57
|
+
Address: config.address,
|
|
58
|
+
Port: config.port,
|
|
59
|
+
EnableTagOverride: false,
|
|
60
|
+
Check: {
|
|
61
|
+
HTTP: config.healthCheck || `http://${config.address}:${config.port}/healthz`,
|
|
62
|
+
Interval: '10s',
|
|
63
|
+
Timeout: '5s',
|
|
64
|
+
DeregisterCriticalServiceAfter: '30s'
|
|
65
|
+
},
|
|
66
|
+
Meta: config.meta || {}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(`${this.consulUrl}/v1/agent/service/register`, {
|
|
71
|
+
method: 'PUT',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify(registration)
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (response.ok) {
|
|
77
|
+
console.log(`✅ Service ${config.name} registered with Consul`);
|
|
78
|
+
this.registeredServices.set(config.id, {
|
|
79
|
+
ID: config.id,
|
|
80
|
+
Name: config.name,
|
|
81
|
+
Tags: registration.Tags,
|
|
82
|
+
Address: config.address,
|
|
83
|
+
Port: config.port,
|
|
84
|
+
EnableTagOverride: registration.EnableTagOverride,
|
|
85
|
+
Meta: registration.Meta || {}
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
throw new Error(`Failed to register service: ${response.statusText}`);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(`❌ Consul registration failed: ${error}`);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async deregisterService(serviceId: string): Promise<void> {
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(`${this.consulUrl}/v1/agent/service/deregister/${serviceId}`, {
|
|
99
|
+
method: 'PUT'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (response.ok) {
|
|
103
|
+
console.log(`✅ Service ${serviceId} deregistered from Consul`);
|
|
104
|
+
this.registeredServices.delete(serviceId);
|
|
105
|
+
} else {
|
|
106
|
+
throw new Error(`Failed to deregister service: ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`❌ Consul deregistration failed: ${error}`);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async discoverServices(serviceName?: string): Promise<ConsulService[]> {
|
|
115
|
+
try {
|
|
116
|
+
let url = `${this.consulUrl}/v1/agent/services`;
|
|
117
|
+
if (serviceName) {
|
|
118
|
+
url += `?service=${serviceName}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const response = await fetch(url);
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
throw new Error(`Failed to discover services: ${response.statusText}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const services: Record<string, ConsulService> = await response.json();
|
|
127
|
+
return Object.values(services).filter(service =>
|
|
128
|
+
service.Tags.includes('bs9')
|
|
129
|
+
);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(`❌ Service discovery failed: ${error}`);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async getServiceHealth(serviceName: string): Promise<any[]> {
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(`${this.consulUrl}/v1/health/service/${serviceName}`);
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new Error(`Failed to get service health: ${response.statusText}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return await response.json();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`❌ Health check failed: ${error}`);
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async watchService(serviceName: string, callback: (services: ConsulService[]) => void): Promise<void> {
|
|
151
|
+
let lastServices: ConsulService[] = [];
|
|
152
|
+
|
|
153
|
+
const checkServices = async () => {
|
|
154
|
+
try {
|
|
155
|
+
const services = await this.discoverServices(serviceName);
|
|
156
|
+
|
|
157
|
+
// Check if services have changed
|
|
158
|
+
const servicesChanged = JSON.stringify(services) !== JSON.stringify(lastServices);
|
|
159
|
+
|
|
160
|
+
if (servicesChanged) {
|
|
161
|
+
callback(services);
|
|
162
|
+
lastServices = services;
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`❌ Service watch error: ${error}`);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Initial check
|
|
170
|
+
await checkServices();
|
|
171
|
+
|
|
172
|
+
// Set up periodic checking
|
|
173
|
+
setInterval(checkServices, 5000);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getRegisteredServices(): ConsulService[] {
|
|
177
|
+
return Array.from(this.registeredServices.values());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async isConsulAvailable(): Promise<boolean> {
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch(`${this.consulUrl}/v1/status/leader`);
|
|
183
|
+
return response.ok;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function consulCommand(action: string, options: any): Promise<void> {
|
|
191
|
+
const consul = new ConsulServiceDiscovery(options.consulUrl);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
switch (action) {
|
|
195
|
+
case 'register':
|
|
196
|
+
if (!options.name || !options.id || !options.address || !options.port) {
|
|
197
|
+
console.error('❌ --name, --id, --address, and --port are required for register');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await consul.registerService({
|
|
202
|
+
name: options.name,
|
|
203
|
+
id: options.id,
|
|
204
|
+
address: options.address,
|
|
205
|
+
port: parseInt(options.port),
|
|
206
|
+
tags: options.tags ? options.tags.split(',') : undefined,
|
|
207
|
+
healthCheck: options.healthCheck,
|
|
208
|
+
meta: options.meta ? JSON.parse(options.meta) : undefined
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case 'deregister':
|
|
213
|
+
if (!options.id) {
|
|
214
|
+
console.error('❌ --id is required for deregister');
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
await consul.deregisterService(options.id);
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case 'discover':
|
|
221
|
+
const services = await consul.discoverServices(options.service);
|
|
222
|
+
console.log('🔍 Discovered Services:');
|
|
223
|
+
console.log('='.repeat(50));
|
|
224
|
+
|
|
225
|
+
if (services.length === 0) {
|
|
226
|
+
console.log('No BS9 services found in Consul');
|
|
227
|
+
} else {
|
|
228
|
+
services.forEach(service => {
|
|
229
|
+
console.log(`📦 ${service.Name} (${service.ID})`);
|
|
230
|
+
console.log(` Address: ${service.Address}:${service.Port}`);
|
|
231
|
+
console.log(` Tags: ${service.Tags.join(', ')}`);
|
|
232
|
+
console.log(` Health: ${service.Meta?.health || 'unknown'}`);
|
|
233
|
+
console.log('');
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
|
|
238
|
+
case 'health':
|
|
239
|
+
if (!options.service) {
|
|
240
|
+
console.error('❌ --service is required for health check');
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const health = await consul.getServiceHealth(options.service);
|
|
245
|
+
console.log(`🏥 Health Status for ${options.service}:`);
|
|
246
|
+
console.log('='.repeat(50));
|
|
247
|
+
|
|
248
|
+
if (health.length === 0) {
|
|
249
|
+
console.log('No health information available');
|
|
250
|
+
} else {
|
|
251
|
+
health.forEach(check => {
|
|
252
|
+
const status = check.Checks?.[0]?.Status || 'unknown';
|
|
253
|
+
const output = check.Checks?.[0]?.Output || 'No output';
|
|
254
|
+
|
|
255
|
+
console.log(`📦 ${check.Service.ID}`);
|
|
256
|
+
console.log(` Status: ${status}`);
|
|
257
|
+
console.log(` Node: ${check.Node.Node}`);
|
|
258
|
+
console.log(` Address: ${check.Service.Address}:${check.Service.Port}`);
|
|
259
|
+
console.log(` Output: ${output}`);
|
|
260
|
+
console.log('');
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case 'status':
|
|
266
|
+
const available = await consul.isConsulAvailable();
|
|
267
|
+
if (available) {
|
|
268
|
+
console.log('✅ Consul is available');
|
|
269
|
+
console.log(` URL: ${options.consulUrl || 'http://localhost:8500'}`);
|
|
270
|
+
} else {
|
|
271
|
+
console.log('❌ Consul is not available');
|
|
272
|
+
console.log(` URL: ${options.consulUrl || 'http://localhost:8500'}`);
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
default:
|
|
277
|
+
console.error(`❌ Unknown action: ${action}`);
|
|
278
|
+
console.log('Available actions: register, deregister, discover, health, status');
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(`❌ Consul command failed: ${error}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|