wilfredwake 1.0.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,283 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ SERVICE REGISTRY MANAGER ║
4
+ * ║ Loads and validates service configuration ║
5
+ * ║ Manages service definitions and dependencies ║
6
+ * ╚═══════════════════════════════════════════════════════════════╝
7
+ */
8
+
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import yaml from 'js-yaml';
12
+
13
+ /**
14
+ * Service Registry - Single source of truth for service definitions
15
+ * Loads services from YAML/JSON configuration files
16
+ * Validates service definitions and dependency chains
17
+ */
18
+ export class ServiceRegistry {
19
+ constructor() {
20
+ this.services = new Map();
21
+ this.registry = null;
22
+ this.lastLoadTime = null;
23
+ }
24
+
25
+ /**
26
+ * Load service registry from file
27
+ * Supports both YAML and JSON formats
28
+ *
29
+ * @param {string} filePath - Path to registry file
30
+ * @returns {Promise<void>}
31
+ */
32
+ async load(filePath) {
33
+ try {
34
+ const content = await fs.readFile(filePath, 'utf-8');
35
+
36
+ let registry;
37
+ if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
38
+ registry = yaml.load(content);
39
+ } else if (filePath.endsWith('.json')) {
40
+ registry = JSON.parse(content);
41
+ } else {
42
+ throw new Error('Unsupported file format. Use .yaml, .yml, or .json');
43
+ }
44
+
45
+ this._validateRegistry(registry);
46
+ this.registry = registry;
47
+ this._indexServices(registry);
48
+ this.lastLoadTime = Date.now();
49
+ } catch (error) {
50
+ throw new Error(
51
+ `Failed to load service registry from ${filePath}: ${error.message}`
52
+ );
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Load registry from string content
58
+ * @param {string} content - YAML or JSON content
59
+ * @param {string} format - Format type ('yaml' or 'json')
60
+ */
61
+ async loadFromString(content, format = 'yaml') {
62
+ try {
63
+ let registry;
64
+ if (format === 'yaml') {
65
+ registry = yaml.load(content);
66
+ } else if (format === 'json') {
67
+ registry = JSON.parse(content);
68
+ } else {
69
+ throw new Error('Unsupported format. Use "yaml" or "json"');
70
+ }
71
+
72
+ this._validateRegistry(registry);
73
+ this.registry = registry;
74
+ this._indexServices(registry);
75
+ this.lastLoadTime = Date.now();
76
+ } catch (error) {
77
+ throw new Error(`Failed to load registry: ${error.message}`);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get all services for a specific environment
83
+ * @param {string} environment - Environment name (dev, staging, prod)
84
+ * @returns {Array<Object>} Array of services
85
+ */
86
+ getServices(environment = 'dev') {
87
+ const env = this.registry?.services?.[environment] || {};
88
+ return Object.values(env);
89
+ }
90
+
91
+ /**
92
+ * Get a specific service by name
93
+ * @param {string} serviceName - Service name
94
+ * @param {string} environment - Environment name
95
+ * @returns {Object|null} Service definition
96
+ */
97
+ getService(serviceName, environment = 'dev') {
98
+ return this.registry?.services?.[environment]?.[serviceName] || null;
99
+ }
100
+
101
+ /**
102
+ * Get service dependencies
103
+ * @param {string} serviceName - Service name
104
+ * @param {string} environment - Environment name
105
+ * @returns {Array<string>} Array of dependent service names
106
+ */
107
+ getDependencies(serviceName, environment = 'dev') {
108
+ const service = this.getService(serviceName, environment);
109
+ return service?.dependsOn || [];
110
+ }
111
+
112
+ /**
113
+ * Resolve service wake order respecting dependencies
114
+ * Performs topological sort to determine wake sequence
115
+ *
116
+ * @param {string|Array<string>} target - Service name or array of names
117
+ * @param {string} environment - Environment name
118
+ * @returns {Array<Object>} Services in wake order
119
+ */
120
+ resolveWakeOrder(target, environment = 'dev') {
121
+ const services = this.getServices(environment);
122
+ let targetServices = [];
123
+
124
+ // ═══════════════════════════════════════════════════════════════
125
+ // RESOLVE TARGET SERVICES
126
+ // ═══════════════════════════════════════════════════════════════
127
+ if (target === 'all') {
128
+ // Wake all services
129
+ targetServices = Object.values(services);
130
+ } else if (Array.isArray(target)) {
131
+ // Wake specified services
132
+ targetServices = target
133
+ .map(name => this.getService(name, environment))
134
+ .filter(Boolean);
135
+ } else {
136
+ // Wake single service and its dependencies
137
+ const service = this.getService(target, environment);
138
+ if (service) {
139
+ targetServices = [service];
140
+ }
141
+ }
142
+
143
+ // ═══════════════════════════════════════════════════════════════
144
+ // TOPOLOGICAL SORT - Respect dependency order
145
+ // ═══════════════════════════════════════════════════════════════
146
+ const visited = new Set();
147
+ const tempVisited = new Set();
148
+ const result = [];
149
+
150
+ const visit = (serviceName) => {
151
+ if (visited.has(serviceName)) return;
152
+ if (tempVisited.has(serviceName)) {
153
+ throw new Error(`Circular dependency detected involving ${serviceName}`);
154
+ }
155
+
156
+ tempVisited.add(serviceName);
157
+ const service = this.getService(serviceName, environment);
158
+
159
+ if (service) {
160
+ // Visit dependencies first
161
+ for (const dep of service.dependsOn || []) {
162
+ visit(dep);
163
+ }
164
+ }
165
+
166
+ tempVisited.delete(serviceName);
167
+ visited.add(serviceName);
168
+ result.push(serviceName);
169
+ };
170
+
171
+ // Visit each target service
172
+ for (const service of targetServices) {
173
+ visit(service.name);
174
+ }
175
+
176
+ // Convert to service objects
177
+ return result
178
+ .map(name => this.getService(name, environment))
179
+ .filter(Boolean);
180
+ }
181
+
182
+ /**
183
+ * Validate registry structure
184
+ * @private
185
+ * @param {Object} registry - Registry object to validate
186
+ */
187
+ _validateRegistry(registry) {
188
+ if (!registry) {
189
+ throw new Error('Registry is empty or invalid');
190
+ }
191
+
192
+ if (!registry.services) {
193
+ throw new Error('Registry must contain "services" key');
194
+ }
195
+
196
+ // Validate each environment
197
+ for (const [env, services] of Object.entries(registry.services)) {
198
+ if (typeof services !== 'object' || Array.isArray(services)) {
199
+ throw new Error(
200
+ `Services in environment "${env}" must be an object`
201
+ );
202
+ }
203
+
204
+ // Validate each service
205
+ for (const [name, service] of Object.entries(services)) {
206
+ this._validateService(name, service, env);
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Validate individual service definition
213
+ * @private
214
+ * @param {string} name - Service name
215
+ * @param {Object} service - Service definition
216
+ * @param {string} environment - Environment name
217
+ */
218
+ _validateService(name, service, environment) {
219
+ if (!service.url) {
220
+ throw new Error(
221
+ `Service "${name}" in environment "${environment}" must have a URL`
222
+ );
223
+ }
224
+
225
+ if (!service.health) {
226
+ throw new Error(
227
+ `Service "${name}" must define a health check endpoint`
228
+ );
229
+ }
230
+
231
+ // 'wake' endpoint is optional; newer logic uses health-only checks
232
+ // Keep backwards compatibility if present, but do not require it.
233
+
234
+ if (service.dependsOn && !Array.isArray(service.dependsOn)) {
235
+ throw new Error(
236
+ `Service "${name}" dependsOn must be an array`
237
+ );
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Index services by name for quick lookup
243
+ * @private
244
+ * @param {Object} registry - Registry object
245
+ */
246
+ _indexServices(registry) {
247
+ this.services.clear();
248
+
249
+ for (const environment of Object.keys(registry.services || {})) {
250
+ const envServices = registry.services[environment];
251
+ for (const [name, service] of Object.entries(envServices)) {
252
+ const key = `${environment}:${name}`;
253
+ this.services.set(key, service);
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Get registry statistics
260
+ * @returns {Object} Statistics about loaded registry
261
+ */
262
+ getStats() {
263
+ const stats = {
264
+ totalServices: 0,
265
+ environments: [],
266
+ lastLoadTime: this.lastLoadTime,
267
+ };
268
+
269
+ if (this.registry?.services) {
270
+ for (const [env, services] of Object.entries(this.registry.services)) {
271
+ stats.environments.push({
272
+ name: env,
273
+ serviceCount: Object.keys(services).length,
274
+ });
275
+ stats.totalServices += Object.keys(services).length;
276
+ }
277
+ }
278
+
279
+ return stats;
280
+ }
281
+ }
282
+
283
+ export default ServiceRegistry;
@@ -0,0 +1,402 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ ORCHESTRATOR API SERVER ║
4
+ * ║ Express.js server providing REST API for service mgmt ║
5
+ * ║ Manages service waking, health checks, and state ║
6
+ * ╚═══════════════════════════════════════════════════════════════╝
7
+ */
8
+
9
+ import express from 'express';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import chalk from 'chalk';
13
+ import { ServiceRegistry } from './registry.js';
14
+ import { Orchestrator } from './orchestrator.js';
15
+
16
+ // ═══════════════════════════════════════════════════════════════
17
+ // SETUP
18
+ // ═══════════════════════════════════════════════════════════════
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const app = express();
22
+ const PORT = process.env.PORT || 3000;
23
+ const REGISTRY_FILE = process.env.REGISTRY_FILE || path.join(__dirname, '../config/services.yaml');
24
+
25
+ // ═══════════════════════════════════════════════════════════════
26
+ // GLOBAL STATE
27
+ // ═══════════════════════════════════════════════════════════════
28
+
29
+ let registry = null;
30
+ let orchestrator = null;
31
+ let requestCounter = 0;
32
+
33
+ // ═══════════════════════════════════════════════════════════════
34
+ // MIDDLEWARE
35
+ // ═══════════════════════════════════════════════════════════════
36
+
37
+ // Body parser
38
+ app.use(express.json());
39
+
40
+ // Request logging with colors
41
+ app.use((req, res, next) => {
42
+ const requestId = ++requestCounter;
43
+ const startTime = Date.now();
44
+
45
+ res.on('finish', () => {
46
+ const duration = Date.now() - startTime;
47
+ const statusColor =
48
+ res.statusCode < 300
49
+ ? chalk.greenBright
50
+ : res.statusCode < 400
51
+ ? chalk.yellowBright
52
+ : chalk.redBright;
53
+
54
+ console.log(
55
+ chalk.dim(`[${new Date().toLocaleTimeString()}]`) +
56
+ ` ${statusColor(res.statusCode)} ` +
57
+ chalk.cyan(`${req.method} ${req.path}`) +
58
+ chalk.gray(` (${duration}ms)`)
59
+ );
60
+ });
61
+
62
+ next();
63
+ });
64
+
65
+ // ═══════════════════════════════════════════════════════════════
66
+ // MIDDLEWARE - AUTHENTICATION
67
+ // ═══════════════════════════════════════════════════════════════
68
+
69
+ /**
70
+ * Token validation middleware
71
+ * Checks Authorization header if tokens are required
72
+ */
73
+ const validateToken = (req, res, next) => {
74
+ const token = req.headers.authorization?.replace('Bearer ', '');
75
+
76
+ // For now, we accept all requests
77
+ // In production, validate against registered tokens
78
+ if (process.env.REQUIRE_AUTH && !token) {
79
+ return res.status(401).json({
80
+ success: false,
81
+ error: 'Missing or invalid authentication token',
82
+ });
83
+ }
84
+
85
+ req.userId = token || 'anonymous';
86
+ next();
87
+ };
88
+
89
+ // ═══════════════════════════════════════════════════════════════
90
+ // API ROUTES
91
+ // ═══════════════════════════════════════════════════════════════
92
+
93
+ /**
94
+ * GET /health
95
+ * Health check endpoint for orchestrator itself
96
+ */
97
+ app.get('/health', (req, res) => {
98
+ res.json({
99
+ status: 'ok',
100
+ uptime: process.uptime(),
101
+ timestamp: new Date().toISOString(),
102
+ registry: registry ? registry.getStats() : null,
103
+ });
104
+ });
105
+
106
+ /**
107
+ * GET /api/status
108
+ * Get current status of services
109
+ * Query params:
110
+ * - environment: Environment name (dev, staging, prod)
111
+ * - service: Optional specific service name
112
+ */
113
+ app.get('/api/status', validateToken, async (req, res) => {
114
+ try {
115
+ const { environment = 'dev', service } = req.query;
116
+
117
+ if (!orchestrator) {
118
+ return res.status(503).json({
119
+ success: false,
120
+ error: 'Orchestrator not initialized',
121
+ });
122
+ }
123
+
124
+ const status = await orchestrator.getStatus(service, environment);
125
+
126
+ res.json({
127
+ success: true,
128
+ ...status,
129
+ });
130
+ } catch (error) {
131
+ console.error(chalk.redBright(`Error in /api/status: ${error.message}`));
132
+ res.status(500).json({
133
+ success: false,
134
+ error: error.message,
135
+ });
136
+ }
137
+ });
138
+
139
+ /**
140
+ * GET /api/health
141
+ * Get detailed health information for services
142
+ * Query params:
143
+ * - environment: Environment name
144
+ * - service: Optional specific service name
145
+ */
146
+ app.get('/api/health', validateToken, async (req, res) => {
147
+ try {
148
+ const { environment = 'dev', service } = req.query;
149
+
150
+ if (!orchestrator) {
151
+ return res.status(503).json({
152
+ success: false,
153
+ error: 'Orchestrator not initialized',
154
+ });
155
+ }
156
+
157
+ const health = await orchestrator.getHealth(service, environment);
158
+
159
+ res.json({
160
+ success: true,
161
+ ...health,
162
+ });
163
+ } catch (error) {
164
+ console.error(chalk.redBright(`Error in /api/health: ${error.message}`));
165
+ res.status(500).json({
166
+ success: false,
167
+ error: error.message,
168
+ });
169
+ }
170
+ });
171
+
172
+ /**
173
+ * POST /api/wake
174
+ * Wake services on demand
175
+ * Body:
176
+ * - target: "all" | "<service-name>" | "<group-name>"
177
+ * - environment: Environment name
178
+ * - wait: Wait for services to be ready (boolean)
179
+ * - timeout: Timeout in seconds
180
+ */
181
+ app.post('/api/wake', validateToken, async (req, res) => {
182
+ try {
183
+ const {
184
+ target,
185
+ environment = 'dev',
186
+ wait = true,
187
+ timeout = 300,
188
+ } = req.body;
189
+
190
+ if (!target) {
191
+ return res.status(400).json({
192
+ success: false,
193
+ error: 'Missing required field: target',
194
+ });
195
+ }
196
+
197
+ if (!orchestrator) {
198
+ return res.status(503).json({
199
+ success: false,
200
+ error: 'Orchestrator not initialized',
201
+ });
202
+ }
203
+
204
+ // ═══════════════════════════════════════════════════════════════
205
+ // PERFORM WAKE OPERATION
206
+ // ═══════════════════════════════════════════════════════════════
207
+ const result = await orchestrator.wake(target, environment, {
208
+ wait,
209
+ timeout: parseInt(timeout),
210
+ });
211
+
212
+ // ═══════════════════════════════════════════════════════════════
213
+ // RETURN RESULT
214
+ // ═══════════════════════════════════════════════════════════════
215
+ const statusCode = result.success ? 200 : 207; // 207 Partial Success if some failed
216
+
217
+ res.status(statusCode).json({
218
+ success: result.success,
219
+ error: result.error,
220
+ target,
221
+ environment,
222
+ services: result.services,
223
+ totalDuration: result.totalDuration,
224
+ timestamp: new Date().toISOString(),
225
+ });
226
+ } catch (error) {
227
+ console.error(chalk.redBright(`Error in /api/wake: ${error.message}`));
228
+ res.status(500).json({
229
+ success: false,
230
+ error: error.message,
231
+ });
232
+ }
233
+ });
234
+
235
+ /**
236
+ * GET /api/registry
237
+ * Get current service registry (read-only)
238
+ * Query params:
239
+ * - environment: Optional environment filter
240
+ */
241
+ app.get('/api/registry', validateToken, (req, res) => {
242
+ try {
243
+ const { environment } = req.query;
244
+
245
+ if (!registry) {
246
+ return res.status(503).json({
247
+ success: false,
248
+ error: 'Registry not loaded',
249
+ });
250
+ }
251
+
252
+ const response = {
253
+ success: true,
254
+ stats: registry.getStats(),
255
+ timestamp: new Date().toISOString(),
256
+ };
257
+
258
+ if (environment) {
259
+ response.services = registry.getServices(environment);
260
+ } else {
261
+ response.services = registry.registry.services;
262
+ }
263
+
264
+ res.json(response);
265
+ } catch (error) {
266
+ console.error(chalk.redBright(`Error in /api/registry: ${error.message}`));
267
+ res.status(500).json({
268
+ success: false,
269
+ error: error.message,
270
+ });
271
+ }
272
+ });
273
+
274
+ /**
275
+ * POST /api/reload
276
+ * Reload service registry
277
+ * Requires authentication
278
+ */
279
+ app.post('/api/reload', validateToken, async (req, res) => {
280
+ try {
281
+ console.log(chalk.yellowBright('Reloading service registry...'));
282
+
283
+ registry = new ServiceRegistry();
284
+ await registry.load(REGISTRY_FILE);
285
+
286
+ orchestrator = new Orchestrator(registry);
287
+
288
+ const stats = registry.getStats();
289
+ console.log(chalk.greenBright(`Registry reloaded: ${stats.totalServices} services`));
290
+
291
+ res.json({
292
+ success: true,
293
+ message: 'Registry reloaded successfully',
294
+ stats,
295
+ });
296
+ } catch (error) {
297
+ console.error(chalk.redBright(`Registry reload failed: ${error.message}`));
298
+ res.status(500).json({
299
+ success: false,
300
+ error: error.message,
301
+ });
302
+ }
303
+ });
304
+
305
+ // ═══════════════════════════════════════════════════════════════
306
+ // ERROR HANDLING
307
+ // ═══════════════════════════════════════════════════════════════
308
+
309
+ /**
310
+ * 404 Handler
311
+ */
312
+ app.use((req, res) => {
313
+ res.status(404).json({
314
+ success: false,
315
+ error: `Endpoint not found: ${req.method} ${req.path}`,
316
+ });
317
+ });
318
+
319
+ /**
320
+ * Global error handler
321
+ */
322
+ app.use((err, req, res, next) => {
323
+ console.error(chalk.redBright(`Server error: ${err.message}`));
324
+ res.status(500).json({
325
+ success: false,
326
+ error: 'Internal server error',
327
+ });
328
+ });
329
+
330
+ // ═══════════════════════════════════════════════════════════════
331
+ // INITIALIZATION & SERVER START
332
+ // ═══════════════════════════════════════════════════════════════
333
+
334
+ /**
335
+ * Initialize orchestrator and start server
336
+ */
337
+ async function initialize() {
338
+ try {
339
+ // ═══════════════════════════════════════════════════════════════
340
+ // LOAD REGISTRY
341
+ // ═══════════════════════════════════════════════════════════════
342
+ console.log(chalk.cyanBright.bold('╔════════════════════════════════════╗'));
343
+ console.log(chalk.cyanBright.bold('║ WILFREDWAKE ORCHESTRATOR ║'));
344
+ console.log(chalk.cyanBright.bold('╚════════════════════════════════════╝\n'));
345
+
346
+ console.log(chalk.yellowBright(`Loading service registry from: ${REGISTRY_FILE}`));
347
+
348
+ registry = new ServiceRegistry();
349
+ await registry.load(REGISTRY_FILE);
350
+
351
+ const stats = registry.getStats();
352
+ console.log(
353
+ chalk.greenBright(
354
+ `✓ Registry loaded: ${stats.totalServices} services\n`
355
+ )
356
+ );
357
+
358
+ // ═══════════════════════════════════════════════════════════════
359
+ // INITIALIZE ORCHESTRATOR
360
+ // ═══════════════════════════════════════════════════════════════
361
+ orchestrator = new Orchestrator(registry);
362
+
363
+ // ═══════════════════════════════════════════════════════════════
364
+ // START SERVER
365
+ // ═══════════════════════════════════════════════════════════════
366
+ app.listen(PORT, () => {
367
+ console.log(chalk.greenBright.bold(`✓ Orchestrator running on port ${PORT}\n`));
368
+
369
+ console.log(chalk.cyanBright('Available endpoints:'));
370
+ console.log(chalk.yellow(' GET /health # Health check'));
371
+ console.log(chalk.yellow(' GET /api/status # Service status'));
372
+ console.log(chalk.yellow(' GET /api/health # Service health'));
373
+ console.log(chalk.yellow(' POST /api/wake # Wake services'));
374
+ console.log(chalk.yellow(' GET /api/registry # View registry'));
375
+ console.log(chalk.yellow(' POST /api/reload # Reload registry\n'));
376
+
377
+ console.log(chalk.dim(`Environment: ${process.env.NODE_ENV || 'development'}`));
378
+ console.log(chalk.dim(`Registry file: ${REGISTRY_FILE}\n`));
379
+ });
380
+ } catch (error) {
381
+ console.error(
382
+ chalk.redBright(`Failed to initialize orchestrator: ${error.message}`)
383
+ );
384
+ process.exit(1);
385
+ }
386
+ }
387
+
388
+ // Start the server
389
+ initialize();
390
+
391
+ // Graceful shutdown
392
+ process.on('SIGTERM', () => {
393
+ console.log(chalk.yellowBright('\nReceived SIGTERM, shutting down gracefully...'));
394
+ process.exit(0);
395
+ });
396
+
397
+ process.on('SIGINT', () => {
398
+ console.log(chalk.yellowBright('\nReceived SIGINT, shutting down gracefully...'));
399
+ process.exit(0);
400
+ });
401
+
402
+ export default app;