@ebowwa/pkg-ops 0.1.20 → 0.1.22
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/package.json +1 -2
- package/rust/src/lib.rs +3 -1
- package/src/bridge.ts +0 -506
- package/src/config.ts +0 -364
- package/src/health-server.ts +0 -280
- package/src/index.ts +0 -1187
- package/src/service-manager.ts +0 -216
- package/src/types.ts +0 -240
package/src/config.ts
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration management for PkgOps.
|
|
3
|
-
*
|
|
4
|
-
* Handles reading/writing config file at /etc/pkg-ops/config.json
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```typescript
|
|
8
|
-
* import { loadConfig, saveConfig, getConfigPath } from "@ebowwa/pkg-ops/config";
|
|
9
|
-
*
|
|
10
|
-
* const config = await loadConfig();
|
|
11
|
-
* console.log(config.packages);
|
|
12
|
-
*
|
|
13
|
-
* config.packages["@ebowwa/stack"] = { version: "0.7.13", service: "stack.service" };
|
|
14
|
-
* await saveConfig(config);
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { accessSync, constants, mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
19
|
-
import { dirname } from "node:path";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Types
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Metadata for a single installed version.
|
|
28
|
-
*/
|
|
29
|
-
export interface VersionMetadata {
|
|
30
|
-
/** ISO timestamp when this version was installed */
|
|
31
|
-
installedAt: string;
|
|
32
|
-
/** Size of the dist directory in bytes */
|
|
33
|
-
distSizeBytes: number | null;
|
|
34
|
-
/** Number of files in dist */
|
|
35
|
-
fileCount: number | null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface PackageConfig {
|
|
39
|
-
/** Currently active version (semver) */
|
|
40
|
-
version: string;
|
|
41
|
-
/** All installed versions with metadata */
|
|
42
|
-
versions: Record<string, VersionMetadata>;
|
|
43
|
-
/** Associated systemd service name (without .service suffix) */
|
|
44
|
-
service?: string;
|
|
45
|
-
/** Whether to auto-start the service after install */
|
|
46
|
-
autoStart?: boolean;
|
|
47
|
-
/** Custom environment variables for the service */
|
|
48
|
-
environment?: Record<string, string>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface PkgOpsConfig {
|
|
52
|
-
/** Managed packages */
|
|
53
|
-
packages: Record<string, PackageConfig>;
|
|
54
|
-
/** Health check HTTP port (default: 8914) */
|
|
55
|
-
healthPort?: number;
|
|
56
|
-
/** Working directory for installations (default: /root) */
|
|
57
|
-
workDir?: string;
|
|
58
|
-
/** Log level (default: "info") */
|
|
59
|
-
logLevel?: "debug" | "info" | "warn" | "error";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Constants
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
export const DEFAULT_CONFIG: PkgOpsConfig = {
|
|
67
|
-
packages: {},
|
|
68
|
-
healthPort: 8914,
|
|
69
|
-
workDir: "/root",
|
|
70
|
-
logLevel: "info",
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export const CONFIG_DIR = "/etc/pkg-ops";
|
|
74
|
-
export const CONFIG_PATH = `${CONFIG_DIR}/config.json`;
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Functions
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Get the config file path.
|
|
82
|
-
* Respects PKG_OPS_CONFIG env override.
|
|
83
|
-
*/
|
|
84
|
-
export function getConfigPath(): string {
|
|
85
|
-
return process.env.PKG_OPS_CONFIG ?? CONFIG_PATH;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Ensure config directory exists (requires sudo on most systems).
|
|
90
|
-
*/
|
|
91
|
-
export function ensureConfigDir(): void {
|
|
92
|
-
const configDir = dirname(getConfigPath());
|
|
93
|
-
if (!existsSync(configDir)) {
|
|
94
|
-
mkdirSync(configDir, { recursive: true, mode: constants.S_IRWXU | constants.S_IRGRP | constants.S_IXGRP });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Load configuration from file.
|
|
100
|
-
* Returns default config if file doesn't exist.
|
|
101
|
-
*/
|
|
102
|
-
export function loadConfig(): PkgOpsConfig {
|
|
103
|
-
const configPath = getConfigPath();
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
if (!existsSync(configPath)) {
|
|
107
|
-
return { ...DEFAULT_CONFIG };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const content = readFileSync(configPath, "utf-8");
|
|
111
|
-
const parsed = JSON.parse(content);
|
|
112
|
-
|
|
113
|
-
// Merge with defaults
|
|
114
|
-
return {
|
|
115
|
-
...DEFAULT_CONFIG,
|
|
116
|
-
...parsed,
|
|
117
|
-
};
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error(`Failed to load config from ${configPath}:`, error);
|
|
120
|
-
return { ...DEFAULT_CONFIG };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Save configuration to file.
|
|
126
|
-
* Creates directory if it doesn't exist.
|
|
127
|
-
*/
|
|
128
|
-
export function saveConfig(config: PkgOpsConfig): void {
|
|
129
|
-
const configPath = getConfigPath();
|
|
130
|
-
ensureConfigDir();
|
|
131
|
-
|
|
132
|
-
const content = JSON.stringify(config, null, 2);
|
|
133
|
-
writeFileSync(configPath, content, {
|
|
134
|
-
encoding: "utf-8",
|
|
135
|
-
mode: constants.S_IRUSR | constants.S_IWUSR | constants.S_IRGRP,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Update a package in the config.
|
|
141
|
-
*/
|
|
142
|
-
export function updatePackageConfig(
|
|
143
|
-
packageName: string,
|
|
144
|
-
packageConfig: PackageConfig
|
|
145
|
-
): PkgOpsConfig {
|
|
146
|
-
const config = loadConfig();
|
|
147
|
-
config.packages[packageName] = packageConfig;
|
|
148
|
-
saveConfig(config);
|
|
149
|
-
return config;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Remove a package from the config.
|
|
154
|
-
*/
|
|
155
|
-
export function removePackageConfig(packageName: string): PkgOpsConfig {
|
|
156
|
-
const config = loadConfig();
|
|
157
|
-
delete config.packages[packageName];
|
|
158
|
-
saveConfig(config);
|
|
159
|
-
return config;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Get a specific package config.
|
|
164
|
-
*/
|
|
165
|
-
export function getPackageConfig(packageName: string): PackageConfig | undefined {
|
|
166
|
-
const config = loadConfig();
|
|
167
|
-
return config.packages[packageName];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* List all managed packages.
|
|
172
|
-
*/
|
|
173
|
-
export function listManagedPackages(): Array<{ name: string; config: PackageConfig }> {
|
|
174
|
-
const config = loadConfig();
|
|
175
|
-
return Object.entries(config.packages).map(([name, cfg]) => ({ name, config: cfg }));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Validate package name (must be @ebowwa/* scope).
|
|
180
|
-
*/
|
|
181
|
-
export function isValidPackageName(name: string): boolean {
|
|
182
|
-
return name.startsWith("@ebowwa/");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Parse package specifier into name and version.
|
|
187
|
-
* @example
|
|
188
|
-
* parsePackageSpec("@ebowwa/stack@0.7.12") => { name: "@ebowwa/stack", version: "0.7.12" }
|
|
189
|
-
* parsePackageSpec("@ebowwa/stack") => { name: "@ebowwa/stack", version: "latest" }
|
|
190
|
-
*/
|
|
191
|
-
export function parsePackageSpec(spec: string): { name: string; version: string } {
|
|
192
|
-
const atIndex = spec.lastIndexOf("@");
|
|
193
|
-
if (atIndex <= 0) {
|
|
194
|
-
// No version specified or scoped package without version
|
|
195
|
-
return { name: spec, version: "latest" };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const name = spec.slice(0, atIndex);
|
|
199
|
-
const version = spec.slice(atIndex + 1);
|
|
200
|
-
|
|
201
|
-
if (!version) {
|
|
202
|
-
return { name, version: "latest" };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return { name, version };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// Multi-Version Helper Functions
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Get all installed versions for a package.
|
|
214
|
-
*/
|
|
215
|
-
export function getInstalledVersions(packageName: string): VersionMetadata[] {
|
|
216
|
-
const config = loadConfig();
|
|
217
|
-
const pkgConfig = config.packages[packageName];
|
|
218
|
-
if (!pkgConfig?.versions) {
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
221
|
-
return Object.entries(pkgConfig.versions).map(([version, meta]) => ({
|
|
222
|
-
version,
|
|
223
|
-
...meta,
|
|
224
|
-
}));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Check if a specific version is installed.
|
|
229
|
-
*/
|
|
230
|
-
export function isVersionInstalled(packageName: string, version: string): boolean {
|
|
231
|
-
const config = loadConfig();
|
|
232
|
-
const pkgConfig = config.packages[packageName];
|
|
233
|
-
return !!(pkgConfig?.versions?.[version]);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Get the active version for a package.
|
|
238
|
-
*/
|
|
239
|
-
export function getActiveVersion(packageName: string): string | null {
|
|
240
|
-
const config = loadConfig();
|
|
241
|
-
const pkgConfig = config.packages[packageName];
|
|
242
|
-
return pkgConfig?.version ?? null;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Add a new version to the package config.
|
|
247
|
-
*/
|
|
248
|
-
export function addPackageVersion(
|
|
249
|
-
packageName: string,
|
|
250
|
-
version: string,
|
|
251
|
-
metadata: Omit<VersionMetadata, "installedAt"> & { installedAt?: string }
|
|
252
|
-
): void {
|
|
253
|
-
const config = loadConfig();
|
|
254
|
-
|
|
255
|
-
if (!config.packages[packageName]) {
|
|
256
|
-
config.packages[packageName] = {
|
|
257
|
-
version,
|
|
258
|
-
versions: {},
|
|
259
|
-
service: packageName.replace("@ebowwa/", ""),
|
|
260
|
-
autoStart: true,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
config.packages[packageName].versions[version] = {
|
|
265
|
-
installedAt: metadata.installedAt ?? new Date().toISOString(),
|
|
266
|
-
distSizeBytes: metadata.distSizeBytes ?? null,
|
|
267
|
-
fileCount: metadata.fileCount ?? null,
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
saveConfig(config);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Remove a version from the package config.
|
|
275
|
-
* Returns true if the version was removed, false if it didn't exist.
|
|
276
|
-
*/
|
|
277
|
-
export function removePackageVersion(packageName: string, version: string): boolean {
|
|
278
|
-
const config = loadConfig();
|
|
279
|
-
const pkgConfig = config.packages[packageName];
|
|
280
|
-
|
|
281
|
-
if (!pkgConfig?.versions?.[version]) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
delete pkgConfig.versions[version];
|
|
286
|
-
|
|
287
|
-
// If removing the active version, switch to the most recent remaining version
|
|
288
|
-
if (pkgConfig.version === version) {
|
|
289
|
-
const remainingVersions = Object.keys(pkgConfig.versions).sort((a, b) => {
|
|
290
|
-
// Sort by installedAt descending (most recent first)
|
|
291
|
-
const aTime = pkgConfig.versions[a]?.installedAt ?? "";
|
|
292
|
-
const bTime = pkgConfig.versions[b]?.installedAt ?? "";
|
|
293
|
-
return bTime.localeCompare(aTime);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
if (remainingVersions.length > 0) {
|
|
297
|
-
pkgConfig.version = remainingVersions[0];
|
|
298
|
-
} else {
|
|
299
|
-
// No versions left, remove the package entirely
|
|
300
|
-
delete config.packages[packageName];
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
saveConfig(config);
|
|
305
|
-
return true;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Set the active version for a package.
|
|
310
|
-
* Returns true if successful, false if version not installed.
|
|
311
|
-
*/
|
|
312
|
-
export function setActiveVersion(packageName: string, version: string): boolean {
|
|
313
|
-
const config = loadConfig();
|
|
314
|
-
const pkgConfig = config.packages[packageName];
|
|
315
|
-
|
|
316
|
-
if (!pkgConfig?.versions?.[version]) {
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
pkgConfig.version = version;
|
|
321
|
-
saveConfig(config);
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Get the count of installed versions for a package.
|
|
327
|
-
*/
|
|
328
|
-
export function getVersionCount(packageName: string): number {
|
|
329
|
-
const config = loadConfig();
|
|
330
|
-
const pkgConfig = config.packages[packageName];
|
|
331
|
-
return Object.keys(pkgConfig?.versions ?? {}).length;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Get packages with multiple versions installed.
|
|
336
|
-
*/
|
|
337
|
-
export function getPackagesWithMultipleVersions(): Array<{
|
|
338
|
-
name: string;
|
|
339
|
-
activeVersion: string;
|
|
340
|
-
totalVersions: number;
|
|
341
|
-
versions: string[];
|
|
342
|
-
}> {
|
|
343
|
-
const config = loadConfig();
|
|
344
|
-
const result: Array<{
|
|
345
|
-
name: string;
|
|
346
|
-
activeVersion: string;
|
|
347
|
-
totalVersions: number;
|
|
348
|
-
versions: string[];
|
|
349
|
-
}> = [];
|
|
350
|
-
|
|
351
|
-
for (const [name, pkgConfig] of Object.entries(config.packages)) {
|
|
352
|
-
const versions = Object.keys(pkgConfig.versions ?? {});
|
|
353
|
-
if (versions.length > 1) {
|
|
354
|
-
result.push({
|
|
355
|
-
name,
|
|
356
|
-
activeVersion: pkgConfig.version,
|
|
357
|
-
totalVersions: versions.length,
|
|
358
|
-
versions: versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true })),
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return result;
|
|
364
|
-
}
|
package/src/health-server.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Health monitoring HTTP server.
|
|
3
|
-
*
|
|
4
|
-
* Provides an HTTP endpoint for health checks on port 8914 (configurable).
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```typescript
|
|
8
|
-
* import { HealthServer, startHealthServer } from "@ebowwa/pkg-ops/health-server";
|
|
9
|
-
*
|
|
10
|
-
* // Start health server
|
|
11
|
-
* const server = await startHealthServer(8914);
|
|
12
|
-
*
|
|
13
|
-
* // Custom health checks
|
|
14
|
-
* server.addCheck("custom", async () => ({
|
|
15
|
-
* healthy: true,
|
|
16
|
-
* message: "Custom check passed",
|
|
17
|
-
* }));
|
|
18
|
-
*
|
|
19
|
-
* // Stop server
|
|
20
|
-
* await server.stop();
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { createServer, type Server as HttpServer, type IncomingMessage, type ServerResponse } from "http";
|
|
25
|
-
import { getServiceManager, type ServiceStatus } from "./service-manager.js";
|
|
26
|
-
import { loadConfig, type PkgOpsConfig } from "./config.js";
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Types
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
export interface HealthCheckResult {
|
|
33
|
-
/** Whether the check passed */
|
|
34
|
-
healthy: boolean;
|
|
35
|
-
/** Human-readable message */
|
|
36
|
-
message: string;
|
|
37
|
-
/** Additional data */
|
|
38
|
-
data?: Record<string, unknown>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ServiceHealth {
|
|
42
|
-
/** Service name */
|
|
43
|
-
name: string;
|
|
44
|
-
/** Whether the service is healthy */
|
|
45
|
-
healthy: boolean;
|
|
46
|
-
/** Service status */
|
|
47
|
-
status: ServiceStatus;
|
|
48
|
-
/** Last check timestamp */
|
|
49
|
-
checkedAt: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface SystemHealth {
|
|
53
|
-
/** Overall health status */
|
|
54
|
-
healthy: boolean;
|
|
55
|
-
/** All services health */
|
|
56
|
-
services: ServiceHealth[];
|
|
57
|
-
/** System uptime in seconds */
|
|
58
|
-
uptime: number;
|
|
59
|
-
/** Timestamp of this check */
|
|
60
|
-
timestamp: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type HealthCheckFn = () => Promise<HealthCheckResult> | HealthCheckResult;
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// Health Server Class
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* HTTP server for health monitoring.
|
|
71
|
-
*/
|
|
72
|
-
export class HealthServer {
|
|
73
|
-
private server: HttpServer | null = null;
|
|
74
|
-
private checks = new Map<string, HealthCheckFn>();
|
|
75
|
-
private startTime = Date.now();
|
|
76
|
-
private port: number;
|
|
77
|
-
|
|
78
|
-
constructor(port: number = 8914) {
|
|
79
|
-
this.port = port;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Start the health server.
|
|
84
|
-
*/
|
|
85
|
-
async start(): Promise<void> {
|
|
86
|
-
if (this.server) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return new Promise((resolve, reject) => {
|
|
91
|
-
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
92
|
-
|
|
93
|
-
this.server.on("error", reject);
|
|
94
|
-
|
|
95
|
-
this.server.listen(this.port, () => {
|
|
96
|
-
console.log(`Health server listening on port ${this.port}`);
|
|
97
|
-
this.server?.off("error", reject);
|
|
98
|
-
resolve();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Stop the health server.
|
|
105
|
-
*/
|
|
106
|
-
async stop(): Promise<void> {
|
|
107
|
-
if (!this.server) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return new Promise((resolve) => {
|
|
112
|
-
this.server?.close(() => {
|
|
113
|
-
this.server = null;
|
|
114
|
-
resolve();
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Add a custom health check.
|
|
121
|
-
*/
|
|
122
|
-
addCheck(name: string, check: HealthCheckFn): void {
|
|
123
|
-
this.checks.set(name, check);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Remove a health check.
|
|
128
|
-
*/
|
|
129
|
-
removeCheck(name: string): void {
|
|
130
|
-
this.checks.delete(name);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Check health of all services or a specific service.
|
|
135
|
-
* Public method for CLI usage.
|
|
136
|
-
*/
|
|
137
|
-
async checkHealth(serviceName?: string): Promise<SystemHealth | ServiceHealth | null> {
|
|
138
|
-
if (serviceName) {
|
|
139
|
-
return this.getServiceHealth(serviceName);
|
|
140
|
-
}
|
|
141
|
-
return this.getSystemHealth();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
// Private Methods
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
149
|
-
const url = req.url ?? "/";
|
|
150
|
-
const method = req.method ?? "GET";
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
if (method === "GET" && url === "/health") {
|
|
154
|
-
const health = await this.getSystemHealth();
|
|
155
|
-
this.sendJson(res, 200, health);
|
|
156
|
-
} else if (method === "GET" && url.startsWith("/health/")) {
|
|
157
|
-
const serviceName = url.slice(8).replace(".service", "");
|
|
158
|
-
const health = await this.getServiceHealth(serviceName);
|
|
159
|
-
this.sendJson(res, health ? 200 : 404, health ?? { error: "Service not found" });
|
|
160
|
-
} else if (method === "GET" && url === "/ready") {
|
|
161
|
-
const health = await this.getSystemHealth();
|
|
162
|
-
this.sendJson(res, health.healthy ? 200 : 503, { ready: health.healthy });
|
|
163
|
-
} else if (method === "GET" && url === "/live") {
|
|
164
|
-
this.sendJson(res, 200, { alive: true });
|
|
165
|
-
} else {
|
|
166
|
-
this.sendJson(res, 404, { error: "Not found" });
|
|
167
|
-
}
|
|
168
|
-
} catch (error) {
|
|
169
|
-
console.error("Health check error:", error);
|
|
170
|
-
this.sendJson(res, 500, { error: "Internal server error" });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
private async getSystemHealth(): Promise<SystemHealth> {
|
|
175
|
-
const config = loadConfig();
|
|
176
|
-
const serviceManager = getServiceManager();
|
|
177
|
-
|
|
178
|
-
const services: ServiceHealth[] = [];
|
|
179
|
-
|
|
180
|
-
for (const [packageName, pkgConfig] of Object.entries(config.packages)) {
|
|
181
|
-
if (!pkgConfig.service) continue;
|
|
182
|
-
|
|
183
|
-
const status = await serviceManager.status(pkgConfig.service);
|
|
184
|
-
const healthy = status.active && status.subState === "running";
|
|
185
|
-
|
|
186
|
-
services.push({
|
|
187
|
-
name: pkgConfig.service,
|
|
188
|
-
healthy,
|
|
189
|
-
status,
|
|
190
|
-
checkedAt: new Date().toISOString(),
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Run custom checks
|
|
195
|
-
const customResults: Record<string, HealthCheckResult> = {};
|
|
196
|
-
for (const [name, check] of this.checks) {
|
|
197
|
-
try {
|
|
198
|
-
customResults[name] = await check();
|
|
199
|
-
} catch (error) {
|
|
200
|
-
customResults[name] = {
|
|
201
|
-
healthy: false,
|
|
202
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const allHealthy = services.every((s) => s.healthy) &&
|
|
208
|
-
Object.values(customResults).every((r) => r.healthy);
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
healthy: allHealthy,
|
|
212
|
-
services,
|
|
213
|
-
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
|
214
|
-
timestamp: new Date().toISOString(),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private async getServiceHealth(serviceName: string): Promise<ServiceHealth | null> {
|
|
219
|
-
const serviceManager = getServiceManager();
|
|
220
|
-
|
|
221
|
-
const exists = await serviceManager.exists(serviceName);
|
|
222
|
-
if (!exists) {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const status = await serviceManager.status(serviceName);
|
|
227
|
-
const healthy = status.active && status.subState === "running";
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
name: serviceName,
|
|
231
|
-
healthy,
|
|
232
|
-
status,
|
|
233
|
-
checkedAt: new Date().toISOString(),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private sendJson(res: ServerResponse, statusCode: number, data: unknown): void {
|
|
238
|
-
res.statusCode = statusCode;
|
|
239
|
-
res.setHeader("Content-Type", "application/json");
|
|
240
|
-
res.end(JSON.stringify(data, null, 2));
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
|
-
// Singleton Instance
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
|
|
248
|
-
let healthServer: HealthServer | null = null;
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Start the health server with config port.
|
|
252
|
-
*/
|
|
253
|
-
export async function startHealthServer(port?: number): Promise<HealthServer> {
|
|
254
|
-
const config = loadConfig();
|
|
255
|
-
const actualPort = port ?? config.healthPort ?? 8914;
|
|
256
|
-
|
|
257
|
-
if (!healthServer) {
|
|
258
|
-
healthServer = new HealthServer(actualPort);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
await healthServer.start();
|
|
262
|
-
return healthServer;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Stop the health server.
|
|
267
|
-
*/
|
|
268
|
-
export async function stopHealthServer(): Promise<void> {
|
|
269
|
-
if (healthServer) {
|
|
270
|
-
await healthServer.stop();
|
|
271
|
-
healthServer = null;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Get the health server instance.
|
|
277
|
-
*/
|
|
278
|
-
export function getHealthServer(): HealthServer | null {
|
|
279
|
-
return healthServer;
|
|
280
|
-
}
|