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,263 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ CLI CONFIG MANAGEMENT ║
4
+ * ║ Handles ~/.wilfredwake/config.json read/write ║
5
+ * ║ Stores user preferences, tokens, and settings ║
6
+ * ╚═══════════════════════════════════════════════════════════════╝
7
+ */
8
+
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ /**
14
+ * Configuration manager for wilfredwake CLI
15
+ * Handles persistent storage in user's home directory
16
+ */
17
+ export class ConfigManager {
18
+ constructor() {
19
+ // ═══════════════════════════════════════════════════════════════
20
+ // CONFIG PATHS
21
+ // ═══════════════════════════════════════════════════════════════
22
+ this.configDir = path.join(os.homedir(), '.wilfredwake');
23
+ this.configFile = path.join(this.configDir, 'config.json');
24
+ this.cacheDir = path.join(this.configDir, 'cache');
25
+
26
+ // ═══════════════════════════════════════════════════════════════
27
+ // DEFAULT CONFIGURATION
28
+ // ═══════════════════════════════════════════════════════════════
29
+ this.defaultConfig = {
30
+ version: '1.0.0',
31
+ orchestratorUrl: 'http://localhost:3000',
32
+ token: null,
33
+ environment: 'dev',
34
+ userId: this._generateUserId(),
35
+ preferences: {
36
+ outputFormat: 'table', // table or json
37
+ verbose: false, // Enable debug output
38
+ autoWait: true, // Wait for services to be ready
39
+ timeout: 300, // Default timeout in seconds
40
+ colorOutput: true, // Enable colored output
41
+ notifyOnComplete: false, // Notify when wake completes
42
+ },
43
+ environments: {
44
+ dev: {
45
+ name: 'Development',
46
+ orchestratorUrl: 'http://localhost:3000',
47
+ },
48
+ staging: {
49
+ name: 'Staging',
50
+ orchestratorUrl: 'http://staging-orchestrator:3000',
51
+ },
52
+ prod: {
53
+ name: 'Production',
54
+ orchestratorUrl: 'http://prod-orchestrator:3000',
55
+ },
56
+ },
57
+ lastSyncTime: null,
58
+ cacheExpiry: 3600000, // 1 hour in milliseconds
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Generate a unique user ID for multi-developer support
64
+ * @private
65
+ * @returns {string} Unique user identifier
66
+ */
67
+ _generateUserId() {
68
+ return `user_${Date.now()}_${Math.random().toString(36).substring(7)}`;
69
+ }
70
+
71
+ /**
72
+ * Initialize configuration directory and files
73
+ * @returns {Promise<void>}
74
+ */
75
+ async init() {
76
+ try {
77
+ // Create .wilfredwake directory if it doesn't exist
78
+ await fs.mkdir(this.configDir, { recursive: true });
79
+
80
+ // Create cache directory
81
+ await fs.mkdir(this.cacheDir, { recursive: true });
82
+
83
+ // Create config file with defaults if it doesn't exist
84
+ const exists = await this._fileExists(this.configFile);
85
+ if (!exists) {
86
+ await this.saveConfig(this.defaultConfig);
87
+ }
88
+ } catch (error) {
89
+ throw new Error(`Failed to initialize configuration: ${error.message}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Load configuration from disk
95
+ * @returns {Promise<Object>} Configuration object
96
+ */
97
+ async loadConfig() {
98
+ try {
99
+ const exists = await this._fileExists(this.configFile);
100
+ if (!exists) {
101
+ await this.init();
102
+ return this.defaultConfig;
103
+ }
104
+
105
+ const content = await fs.readFile(this.configFile, 'utf-8');
106
+ const config = JSON.parse(content);
107
+
108
+ // Merge with defaults for any missing properties
109
+ return { ...this.defaultConfig, ...config };
110
+ } catch (error) {
111
+ throw new Error(`Failed to load configuration: ${error.message}`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Save configuration to disk
117
+ * @param {Object} config - Configuration object
118
+ * @returns {Promise<void>}
119
+ */
120
+ async saveConfig(config) {
121
+ try {
122
+ await fs.mkdir(this.configDir, { recursive: true });
123
+ const content = JSON.stringify(config, null, 2);
124
+ await fs.writeFile(this.configFile, content, 'utf-8');
125
+ } catch (error) {
126
+ throw new Error(`Failed to save configuration: ${error.message}`);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Update specific configuration value
132
+ * @param {string} key - Configuration key (supports dot notation: "preferences.verbose")
133
+ * @param {any} value - New value
134
+ * @returns {Promise<Object>} Updated configuration
135
+ */
136
+ async updateConfig(key, value) {
137
+ const config = await this.loadConfig();
138
+ const keys = key.split('.');
139
+ let current = config;
140
+
141
+ // Navigate to the target object
142
+ for (let i = 0; i < keys.length - 1; i++) {
143
+ if (!current[keys[i]]) {
144
+ current[keys[i]] = {};
145
+ }
146
+ current = current[keys[i]];
147
+ }
148
+
149
+ // Set the value
150
+ current[keys[keys.length - 1]] = value;
151
+
152
+ await this.saveConfig(config);
153
+ return config;
154
+ }
155
+
156
+ /**
157
+ * Get specific configuration value
158
+ * @param {string} key - Configuration key (supports dot notation)
159
+ * @returns {Promise<any>} Configuration value
160
+ */
161
+ async getConfig(key) {
162
+ const config = await this.loadConfig();
163
+ const keys = key.split('.');
164
+ let current = config;
165
+
166
+ for (const k of keys) {
167
+ current = current?.[k];
168
+ if (current === undefined) return null;
169
+ }
170
+
171
+ return current;
172
+ }
173
+
174
+ /**
175
+ * Cache data for quick retrieval
176
+ * @param {string} key - Cache key
177
+ * @param {any} data - Data to cache
178
+ * @returns {Promise<void>}
179
+ */
180
+ async setCacheData(key, data) {
181
+ try {
182
+ const cacheFile = path.join(this.cacheDir, `${key}.json`);
183
+ const cacheEntry = {
184
+ timestamp: Date.now(),
185
+ data,
186
+ };
187
+ await fs.writeFile(cacheFile, JSON.stringify(cacheEntry), 'utf-8');
188
+ } catch (error) {
189
+ // Silently fail on cache errors
190
+ console.debug(`Cache write failed for ${key}: ${error.message}`);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Retrieve cached data if not expired
196
+ * @param {string} key - Cache key
197
+ * @returns {Promise<any|null>} Cached data or null if expired/not found
198
+ */
199
+ async getCacheData(key) {
200
+ try {
201
+ const cacheFile = path.join(this.cacheDir, `${key}.json`);
202
+ const exists = await this._fileExists(cacheFile);
203
+
204
+ if (!exists) return null;
205
+
206
+ const content = await fs.readFile(cacheFile, 'utf-8');
207
+ const cacheEntry = JSON.parse(content);
208
+
209
+ const config = await this.loadConfig();
210
+ const isExpired =
211
+ Date.now() - cacheEntry.timestamp > config.cacheExpiry;
212
+
213
+ if (isExpired) {
214
+ await fs.unlink(cacheFile).catch(() => {});
215
+ return null;
216
+ }
217
+
218
+ return cacheEntry.data;
219
+ } catch (error) {
220
+ return null;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Clear all cache
226
+ * @returns {Promise<void>}
227
+ */
228
+ async clearCache() {
229
+ try {
230
+ const files = await fs.readdir(this.cacheDir);
231
+ for (const file of files) {
232
+ await fs.unlink(path.join(this.cacheDir, file));
233
+ }
234
+ } catch (error) {
235
+ console.debug(`Cache clear failed: ${error.message}`);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Check if a file exists
241
+ * @private
242
+ * @param {string} filePath - Path to check
243
+ * @returns {Promise<boolean>} True if file exists
244
+ */
245
+ async _fileExists(filePath) {
246
+ try {
247
+ await fs.access(filePath);
248
+ return true;
249
+ } catch {
250
+ return false;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Reset configuration to defaults
256
+ * @returns {Promise<void>}
257
+ */
258
+ async reset() {
259
+ await this.saveConfig(this.defaultConfig);
260
+ }
261
+ }
262
+
263
+ export default ConfigManager;
@@ -0,0 +1,49 @@
1
+ # ╔═══════════════════════════════════════════════════════════════╗
2
+ # ║ WILFREDWAKE SERVICE REGISTRY ║
3
+ # ║ Define all services, environments, and dependencies ║
4
+ # ║ This is the single source of truth for service mgmt ║
5
+ # ╚═══════════════════════════════════════════════════════════════╝
6
+
7
+ # WILFREDWAKE SERVICE REGISTRY (SIMPLIFIED)
8
+
9
+ # Environment and version
10
+ env: dev
11
+ version: "1.0"
12
+
13
+ # SERVICES DEFINITION (DEV ONLY)
14
+ services:
15
+ dev:
16
+ # Core backend service
17
+ backend:
18
+ url: https://pension-backend-rs4h.onrender.com
19
+ health: /api/health
20
+ dependsOn: []
21
+ description: "pension backend Service"
22
+
23
+ # Frontend service
24
+ frontend:
25
+ url: https://transactions-k6gk.onrender.com
26
+ health: /health
27
+ dependsOn: []
28
+ description: "frontend Service"
29
+
30
+ # Payment gateway (producer)
31
+ payment-gateway:
32
+ url: https://payment-gateway-7eta.onrender.com
33
+ health: /health
34
+ dependsOn: []
35
+ description: "Payment Event Producer"
36
+
37
+ # Notification consumer (event processor)
38
+ notification-consumer:
39
+ url: https://notification-service-consumer.onrender.com
40
+ health: /
41
+ dependsOn: []
42
+ description: "Notification Event Consumer"
43
+
44
+ # Notification producer
45
+ notification-producer:
46
+ url: https://notification-service-producer.onrender.com
47
+ health: /health
48
+ dependsOn: []
49
+ description: "Notification Producer Service"
@@ -0,0 +1,397 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ SERVICE ORCHESTRATOR ENGINE ║
4
+ * ║ Simplified wake logic using /health endpoint ║
5
+ * ║ Tracks last wake time and service readiness ║
6
+ * ╚═══════════════════════════════════════════════════════════════╝
7
+ */
8
+
9
+ import axios from 'axios';
10
+
11
+ /**
12
+ * Service state enumeration
13
+ */
14
+ export const ServiceState = {
15
+ DEAD: 'dead', // Service is not responding (initial state)
16
+ WAKING: 'waking', // Health check response slow (> 5s threshold)
17
+ LIVE: 'live', // Health check responds with 200 OK quickly
18
+ FAILED: 'failed', // Health check failed/error
19
+ UNKNOWN: 'unknown', // State cannot be determined
20
+ };
21
+
22
+ /**
23
+ * Orchestrator class - Manages service wake operations
24
+ * Uses simplified /health endpoint logic with timestamp tracking
25
+ */
26
+ export class Orchestrator {
27
+ constructor(registry) {
28
+ this.registry = registry;
29
+ this.serviceStates = new Map(); // Track service states
30
+ this.lastWakeTime = new Map(); // Track when each service was last woken
31
+ this.requestTimestamps = new Map(); // Track request/response times
32
+ this.HEALTH_CHECK_TIMEOUT = 5000; // 5 second threshold before marking as "waking"
33
+ }
34
+
35
+ /**
36
+ * Wake services with dependency ordering
37
+ * NEW LOGIC: Assume all services are dead initially
38
+ * Just call /health and track timestamps
39
+ *
40
+ * @param {string|Array<string>} target - Wake target (all, <service>, <group>)
41
+ * @param {string} environment - Environment name
42
+ * @param {Object} options - Wake options
43
+ * @param {boolean} options.wait - Wait for services to respond
44
+ * @param {number} options.timeout - Overall timeout in seconds
45
+ * @returns {Promise<Object>} Wake operation results
46
+ */
47
+ async wake(target, environment = 'dev', options = {}) {
48
+ const { wait = true, timeout = 300 } = options;
49
+
50
+ // ═══════════════════════════════════════════════════════════════
51
+ // RESOLVE WAKE ORDER
52
+ // ═══════════════════════════════════════════════════════════════
53
+ let services;
54
+ try {
55
+ services = this.registry.resolveWakeOrder(target, environment);
56
+ } catch (error) {
57
+ return {
58
+ success: false,
59
+ error: error.message,
60
+ services: [],
61
+ };
62
+ }
63
+
64
+ if (services.length === 0) {
65
+ return {
66
+ success: true,
67
+ error: null,
68
+ services: [],
69
+ totalDuration: 0,
70
+ };
71
+ }
72
+
73
+ // ═══════════════════════════════════════════════════════════════
74
+ // ASSUME ALL SERVICES ARE DEAD INITIALLY
75
+ // NEW: Instead of /wake endpoint, just check /health repeatedly
76
+ // ═══════════════════════════════════════════════════════════════
77
+ const results = [];
78
+ const startTime = Date.now();
79
+
80
+ for (const service of services) {
81
+ const serviceStartTime = Date.now();
82
+
83
+ try {
84
+ // NEW: Mark service as being woken and initiate health check sequence
85
+ this._logTimestamp(service.name, 'Wake initiated');
86
+
87
+ // NEW: Set state to DEAD initially (always assume dead)
88
+ this._setServiceState(service.name, ServiceState.DEAD);
89
+ this._recordLastWakeTime(service.name);
90
+
91
+ // Check health status (handles timeout internally)
92
+ const status = await this._checkHealthWithTimeout(
93
+ service,
94
+ timeout
95
+ );
96
+
97
+ const duration = Date.now() - serviceStartTime;
98
+
99
+ results.push({
100
+ name: service.name,
101
+ status,
102
+ url: service.url,
103
+ duration,
104
+ error: null,
105
+ lastWakeTime: this.lastWakeTime.get(service.name),
106
+ });
107
+
108
+ this._setServiceState(service.name, status);
109
+ } catch (error) {
110
+ const duration = Date.now() - serviceStartTime;
111
+
112
+ results.push({
113
+ name: service.name,
114
+ status: ServiceState.FAILED,
115
+ url: service.url,
116
+ duration,
117
+ error: error.message,
118
+ lastWakeTime: this.lastWakeTime.get(service.name),
119
+ });
120
+
121
+ this._setServiceState(service.name, ServiceState.FAILED);
122
+ }
123
+ }
124
+
125
+ const totalDuration = Date.now() - startTime;
126
+ const allLive = results.every(r => r.status === ServiceState.LIVE);
127
+
128
+ return {
129
+ success: allLive,
130
+ error: null,
131
+ services: results,
132
+ totalDuration,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Get status of services
138
+ * Returns current state of services (always assumes dead initially)
139
+ *
140
+ * @param {string} serviceName - Optional specific service
141
+ * @param {string} environment - Environment name
142
+ * @returns {Promise<Object>} Status information
143
+ */
144
+ async getStatus(serviceName, environment = 'dev') {
145
+ const services = serviceName
146
+ ? [this.registry.getService(serviceName, environment)].filter(Boolean)
147
+ : this.registry.getServices(environment);
148
+
149
+ const statusResults = [];
150
+
151
+ for (const service of services) {
152
+ const status = await this._checkHealthWithTimeout(service, 5); // Quick check
153
+
154
+ statusResults.push({
155
+ name: service.name,
156
+ status,
157
+ url: service.url,
158
+ lastWakeTime: this.lastWakeTime.get(service.name) || null,
159
+ });
160
+ }
161
+
162
+ return {
163
+ services: statusResults,
164
+ timestamp: new Date().toISOString(),
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Check health of services
170
+ * Performs health check without waking
171
+ *
172
+ * @param {string} serviceName - Optional specific service
173
+ * @param {string} environment - Environment name
174
+ * @returns {Promise<Object>} Health information
175
+ */
176
+ async getHealth(serviceName, environment = 'dev') {
177
+ const services = serviceName
178
+ ? [this.registry.getService(serviceName, environment)].filter(Boolean)
179
+ : this.registry.getServices(environment);
180
+
181
+ const healthResults = [];
182
+
183
+ for (const service of services) {
184
+ const health = await this._performHealthCheck(service);
185
+ healthResults.push({
186
+ name: service.name,
187
+ status: health.state,
188
+ url: service.url,
189
+ statusCode: health.statusCode,
190
+ responseTime: health.responseTime,
191
+ lastChecked: new Date().toISOString(),
192
+ lastWakeTime: this.lastWakeTime.get(service.name) || null,
193
+ error: health.error,
194
+ dependencies: service.dependsOn || [],
195
+ });
196
+ }
197
+
198
+ return {
199
+ services: healthResults,
200
+ timestamp: new Date().toISOString(),
201
+ };
202
+ }
203
+
204
+ /**
205
+ * REMOVED: _wakeService - No longer calls /wake endpoint
206
+ * NEW: Health-based checking with timeout threshold
207
+ */
208
+
209
+ /**
210
+ * Check health with timeout threshold
211
+ * NEW: If response takes > 5 seconds, mark as WAKING
212
+ * If responds quickly with 200, mark as LIVE
213
+ *
214
+ * @private
215
+ * @param {Object} service - Service definition
216
+ * @param {number} timeoutSeconds - Overall timeout
217
+ * @returns {Promise<string>} Service state
218
+ */
219
+ async _checkHealthWithTimeout(service, timeoutSeconds = 5) {
220
+ const startTime = Date.now();
221
+ const overallTimeoutMs = timeoutSeconds * 1000;
222
+
223
+ try {
224
+ const health = await this._performHealthCheck(service);
225
+ const responseTime = Date.now() - startTime;
226
+
227
+ // ═══════════════════════════════════════════════════════════════
228
+ // NEW LOGIC: Simple threshold-based state determination
229
+ // ═══════════════════════════════════════════════════════════════
230
+ if (health.state === ServiceState.LIVE) {
231
+ this._logTimestamp(
232
+ service.name,
233
+ `Health check OK in ${responseTime}ms`
234
+ );
235
+ return ServiceState.LIVE;
236
+ }
237
+
238
+ // If response is slow (> 5 seconds threshold), mark as waking
239
+ if (responseTime > this.HEALTH_CHECK_TIMEOUT) {
240
+ this._logTimestamp(
241
+ service.name,
242
+ `Health check slow (${responseTime}ms) - marking as waking`
243
+ );
244
+ return ServiceState.WAKING;
245
+ }
246
+
247
+ // If it's dead but response came back (non-200), still mark as waking
248
+ if (health.statusCode === 503 || health.statusCode >= 500) {
249
+ this._logTimestamp(service.name, `Health returned ${health.statusCode} - marking as waking`);
250
+ return ServiceState.WAKING;
251
+ }
252
+
253
+ return health.state || ServiceState.UNKNOWN;
254
+ } catch (error) {
255
+ this._logTimestamp(service.name, `Health check error: ${error.message}`);
256
+ return ServiceState.FAILED;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Perform health check on service
262
+ * NEW: Simple /health endpoint call with timeout tracking
263
+ *
264
+ * @private
265
+ * @param {Object} service - Service definition
266
+ * @returns {Promise<Object>} Health check result
267
+ */
268
+ async _performHealthCheck(service) {
269
+ const startTime = Date.now();
270
+
271
+ try {
272
+ const healthUrl = new URL(service.health, service.url).toString();
273
+
274
+ this._logTimestamp(service.name, `Requesting ${service.health}`);
275
+
276
+ const response = await axios.get(healthUrl, {
277
+ timeout: 10000,
278
+ validateStatus: () => true,
279
+ });
280
+
281
+ const responseTime = Date.now() - startTime;
282
+
283
+ // ═══════════════════════════════════════════════════════════════
284
+ // DETERMINE STATE FROM STATUS CODE
285
+ // ═══════════════════════════════════════════════════════════════
286
+ let state = ServiceState.UNKNOWN;
287
+
288
+ if (response.status < 300) {
289
+ state = ServiceState.LIVE; // Changed from READY
290
+ } else if (response.status === 503) {
291
+ state = ServiceState.DEAD; // Changed from SLEEPING
292
+ } else if (response.status >= 500) {
293
+ state = ServiceState.FAILED;
294
+ } else if (response.status >= 400) {
295
+ state = ServiceState.UNKNOWN;
296
+ }
297
+
298
+ this._logTimestamp(
299
+ service.name,
300
+ `Responded ${response.status} in ${responseTime}ms`
301
+ );
302
+
303
+ return {
304
+ state,
305
+ statusCode: response.status,
306
+ responseTime,
307
+ error: null,
308
+ uptime: response.data?.uptime || null,
309
+ };
310
+ } catch (error) {
311
+ const responseTime = Date.now() - startTime;
312
+
313
+ this._logTimestamp(
314
+ service.name,
315
+ `Health check failed: ${error.message}`
316
+ );
317
+
318
+ return {
319
+ state: ServiceState.DEAD, // Changed: assume dead on error
320
+ statusCode: null,
321
+ responseTime,
322
+ error: error.message,
323
+ uptime: null,
324
+ };
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Set service state
330
+ * @private
331
+ * @param {string} serviceName - Service name
332
+ * @param {string} state - New state
333
+ */
334
+ _setServiceState(serviceName, state) {
335
+ this.serviceStates.set(serviceName, state);
336
+ }
337
+
338
+ /**
339
+ * Record the time a service was last woken
340
+ * NEW: Track when each service wake was initiated
341
+ *
342
+ * @private
343
+ * @param {string} serviceName - Service name
344
+ */
345
+ _recordLastWakeTime(serviceName) {
346
+ const timestamp = new Date().toISOString();
347
+ this.lastWakeTime.set(serviceName, timestamp);
348
+ }
349
+
350
+ /**
351
+ * Log a timestamp for service event
352
+ * NEW: Detailed logging of request/response times
353
+ *
354
+ * @private
355
+ * @param {string} serviceName - Service name
356
+ * @param {string} message - Event message
357
+ */
358
+ _logTimestamp(serviceName, message) {
359
+ const timestamp = new Date().toLocaleString('en-US', {
360
+ year: 'numeric',
361
+ month: '2-digit',
362
+ day: '2-digit',
363
+ hour: '2-digit',
364
+ minute: '2-digit',
365
+ second: '2-digit',
366
+ hour12: false,
367
+ });
368
+
369
+ const key = `${serviceName}:${Date.now()}`;
370
+ this.requestTimestamps.set(key, {
371
+ service: serviceName,
372
+ timestamp,
373
+ message,
374
+ });
375
+
376
+ console.log(`[${timestamp}] ${serviceName}: ${message}`);
377
+ }
378
+
379
+ /**
380
+ * Wait for specified milliseconds
381
+ * @private
382
+ * @param {number} ms - Milliseconds to wait
383
+ */
384
+ async _wait(ms) {
385
+ return new Promise(resolve => setTimeout(resolve, ms));
386
+ }
387
+
388
+ /**
389
+ * Clear service states and wake times
390
+ * @returns {void}
391
+ */
392
+ clearStates() {
393
+ this.serviceStates.clear();
394
+ this.lastWakeTime.clear();
395
+ this.requestTimestamps.clear();
396
+ }
397
+ }