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.
- package/.env.example +52 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +400 -0
- package/README.md +831 -0
- package/bin/cli.js +133 -0
- package/index.js +222 -0
- package/package.json +41 -0
- package/src/cli/commands/health.js +238 -0
- package/src/cli/commands/init.js +192 -0
- package/src/cli/commands/status.js +180 -0
- package/src/cli/commands/wake.js +182 -0
- package/src/cli/config.js +263 -0
- package/src/config/services.yaml +49 -0
- package/src/orchestrator/orchestrator.js +397 -0
- package/src/orchestrator/registry.js +283 -0
- package/src/orchestrator/server.js +402 -0
- package/src/shared/colors.js +154 -0
- package/src/shared/logger.js +224 -0
- package/tests/cli.test.js +328 -0
|
@@ -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;
|