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,154 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ COLORS & THEMES MODULE ║
4
+ * ║ Provides consistent color scheme for CLI output ║
5
+ * ║ Uses chalk for terminal color support ║
6
+ * ╚═══════════════════════════════════════════════════════════════╝
7
+ */
8
+
9
+ import chalk from 'chalk';
10
+
11
+ /**
12
+ * Color scheme definitions
13
+ * Each color is assigned a semantic meaning for consistent UX
14
+ */
15
+ export const colors = {
16
+ // ═══════════════════════════════════════════════════════════════
17
+ // STATUS INDICATORS - Service states with visual distinction
18
+ // ═══════════════════════════════════════════════════════════════
19
+ status: {
20
+ // New architecture states
21
+ live: chalk.greenBright, // ✓ Service is responsive and healthy
22
+ dead: chalk.gray, // ⚫ Service is not responding/dormant
23
+ waking: chalk.yellowBright, // ⟳ Service is responding slowly
24
+ failed: chalk.redBright, // ✗ Service failed to respond
25
+ unknown: chalk.magentaBright, // ? Unknown state
26
+
27
+ // Legacy states (for backwards compatibility)
28
+ ready: chalk.greenBright, // ✓ Service is running and healthy
29
+ sleeping: chalk.gray, // ⚫ Service is in sleep/cold state
30
+ pending: chalk.blueBright, // ⏳ Service is pending action
31
+ error: chalk.red, // ⚠️ Error occurred
32
+ },
33
+
34
+ // ═══════════════════════════════════════════════════════════════
35
+ // ACTION MESSAGES - Operation feedback
36
+ // ═══════════════════════════════════════════════════════════════
37
+ action: {
38
+ success: chalk.greenBright, // Operation completed successfully
39
+ warning: chalk.yellowBright, // Warning/caution message
40
+ info: chalk.cyanBright, // Informational message
41
+ debug: chalk.gray, // Debug/verbose output
42
+ prompt: chalk.blueBright, // User input prompt
43
+ },
44
+
45
+ // ═══════════════════════════════════════════════════════════════
46
+ // EMPHASIS - UI elements and emphasis
47
+ // ═══════════════════════════════════════════════════════════════
48
+ emphasis: {
49
+ primary: chalk.cyan, // Primary/main heading
50
+ secondary: chalk.magenta, // Secondary heading
51
+ accent: chalk.yellow, // Accent elements
52
+ muted: chalk.gray, // Muted/secondary text
53
+ highlight: chalk.inverse, // Highlighted text
54
+ },
55
+
56
+ // ═══════════════════════════════════════════════════════════════
57
+ // SPECIAL MARKERS - Icons and visual elements
58
+ // ═══════════════════════════════════════════════════════════════
59
+ markers: {
60
+ success: chalk.greenBright('✓'),
61
+ error: chalk.redBright('✗'),
62
+ warning: chalk.yellowBright('⚠'),
63
+ info: chalk.cyanBright('ℹ'),
64
+ arrow: chalk.dim('→'),
65
+ bullet: chalk.dim('•'),
66
+ spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
67
+ },
68
+ };
69
+
70
+ /**
71
+ * Formatting utilities for structured output
72
+ */
73
+ export const format = {
74
+ /**
75
+ * Format a service status line with color coding
76
+ * @param {string} serviceName - Name of the service
77
+ * @param {string} status - Current status (ready, sleeping, waking, etc)
78
+ * @param {string} message - Optional message to display
79
+ * @returns {string} Formatted status line
80
+ */
81
+ serviceStatus(serviceName, status, message = '') {
82
+ const statusColor = colors.status[status] || colors.status.unknown;
83
+ const statusText = statusColor(status.toUpperCase().padEnd(10));
84
+ const msgText = message ? ` ${colors.emphasis.muted(`(${message})`)}` : '';
85
+ return ` ${statusText} ${chalk.cyan(serviceName)}${msgText}`;
86
+ },
87
+
88
+ /**
89
+ * Format a table header row
90
+ * @param {string[]} columns - Column names
91
+ * @returns {string} Formatted header
92
+ */
93
+ tableHeader(columns) {
94
+ return chalk.inverse(
95
+ ' ' + columns.map(col => col.padEnd(20)).join(' ') + ' '
96
+ );
97
+ },
98
+
99
+ /**
100
+ * Format a table row
101
+ * @param {string[]} cells - Cell values
102
+ * @returns {string} Formatted row
103
+ */
104
+ tableRow(cells) {
105
+ return ' ' + cells.map(cell => cell.padEnd(20)).join(' ');
106
+ },
107
+
108
+ /**
109
+ * Format a section header
110
+ * @param {string} title - Section title
111
+ * @returns {string} Formatted header
112
+ */
113
+ section(title) {
114
+ return chalk.cyanBright.bold(`\n▶ ${title}\n`);
115
+ },
116
+
117
+ /**
118
+ * Format a success message
119
+ * @param {string} message - Message text
120
+ * @returns {string} Formatted message
121
+ */
122
+ success(message) {
123
+ return `${colors.markers.success} ${chalk.greenBright(message)}`;
124
+ },
125
+
126
+ /**
127
+ * Format an error message
128
+ * @param {string} message - Message text
129
+ * @returns {string} Formatted message
130
+ */
131
+ error(message) {
132
+ return `${colors.markers.error} ${chalk.redBright(message)}`;
133
+ },
134
+
135
+ /**
136
+ * Format an info message
137
+ * @param {string} message - Message text
138
+ * @returns {string} Formatted message
139
+ */
140
+ info(message) {
141
+ return `${colors.markers.info} ${chalk.cyanBright(message)}`;
142
+ },
143
+
144
+ /**
145
+ * Format a warning message
146
+ * @param {string} message - Message text
147
+ * @returns {string} Formatted message
148
+ */
149
+ warning(message) {
150
+ return `${colors.markers.warning} ${chalk.yellowBright(message)}`;
151
+ },
152
+ };
153
+
154
+ export default { colors, format };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ LOGGER & UTILITIES MODULE ║
4
+ * ║ Provides logging and utility functions ║
5
+ * ╚═══════════════════════════════════════════════════════════════╝
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import { colors, format } from './colors.js';
10
+
11
+ /**
12
+ * Logger class for consistent output formatting
13
+ * Provides methods for different log levels with color coding
14
+ */
15
+ export class Logger {
16
+ constructor(verbose = false) {
17
+ this.verbose = verbose;
18
+ this.spinnerState = 0;
19
+ }
20
+
21
+ /**
22
+ * Log a success message
23
+ * @param {string} message - Message to log
24
+ */
25
+ success(message) {
26
+ console.log(format.success(message));
27
+ }
28
+
29
+ /**
30
+ * Log an error message
31
+ * @param {string} message - Message to log
32
+ */
33
+ error(message) {
34
+ console.error(format.error(message));
35
+ }
36
+
37
+ /**
38
+ * Log an info message
39
+ * @param {string} message - Message to log
40
+ */
41
+ info(message) {
42
+ console.log(format.info(message));
43
+ }
44
+
45
+ /**
46
+ * Log a warning message
47
+ * @param {string} message - Message to log
48
+ */
49
+ warn(message) {
50
+ console.warn(format.warning(message));
51
+ }
52
+
53
+ /**
54
+ * Log a debug message (only if verbose mode enabled)
55
+ * @param {string} message - Message to log
56
+ */
57
+ debug(message) {
58
+ if (this.verbose) {
59
+ console.log(colors.emphasis.muted(`[DEBUG] ${message}`));
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Log a section header
65
+ * @param {string} title - Section title
66
+ */
67
+ section(title) {
68
+ console.log(format.section(title));
69
+ }
70
+
71
+ /**
72
+ * Start a spinner animation
73
+ * @param {string} message - Message while spinning
74
+ * @returns {Function} Function to stop spinner
75
+ */
76
+ spinner(message) {
77
+ const frames = colors.markers.spinner;
78
+ let index = 0;
79
+
80
+ const interval = setInterval(() => {
81
+ process.stdout.write(
82
+ `\r${chalk.blueBright(frames[index])} ${message}`
83
+ );
84
+ index = (index + 1) % frames.length;
85
+ }, 100);
86
+
87
+ return () => {
88
+ clearInterval(interval);
89
+ process.stdout.write('\r'); // Clear line
90
+ };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Utility functions for common operations
96
+ */
97
+ export const utils = {
98
+ /**
99
+ * Wait for a specified number of milliseconds
100
+ * @param {number} ms - Milliseconds to wait
101
+ * @returns {Promise<void>}
102
+ */
103
+ async wait(ms) {
104
+ return new Promise(resolve => setTimeout(resolve, ms));
105
+ },
106
+
107
+ /**
108
+ * Retry an async operation with exponential backoff
109
+ * @param {Function} fn - Async function to retry
110
+ * @param {number} maxAttempts - Maximum retry attempts
111
+ * @param {number} baseDelay - Base delay in milliseconds
112
+ * @returns {Promise<any>} Result from successful attempt
113
+ */
114
+ async retry(fn, maxAttempts = 5, baseDelay = 1000) {
115
+ let lastError;
116
+
117
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
118
+ try {
119
+ return await fn();
120
+ } catch (error) {
121
+ lastError = error;
122
+ if (attempt < maxAttempts) {
123
+ const delay = baseDelay * Math.pow(2, attempt - 1);
124
+ await this.wait(delay);
125
+ }
126
+ }
127
+ }
128
+
129
+ throw lastError;
130
+ },
131
+
132
+ /**
133
+ * Check if a URL is reachable
134
+ * @param {string} url - URL to check
135
+ * @param {number} timeout - Timeout in milliseconds
136
+ * @returns {Promise<boolean>} True if reachable
137
+ */
138
+ async isUrlReachable(url, timeout = 5000) {
139
+ try {
140
+ const controller = new AbortController();
141
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
142
+
143
+ const response = await fetch(url, {
144
+ method: 'GET',
145
+ signal: controller.signal,
146
+ });
147
+
148
+ clearTimeout(timeoutId);
149
+ return response.ok;
150
+ } catch (error) {
151
+ return false;
152
+ }
153
+ },
154
+
155
+ /**
156
+ * Format milliseconds to human-readable duration
157
+ * @param {number} ms - Milliseconds
158
+ * @returns {string} Formatted duration (e.g., "2.5s", "1m 30s")
159
+ */
160
+ formatDuration(ms) {
161
+ if (ms < 1000) return `${Math.round(ms)}ms`;
162
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
163
+
164
+ const minutes = Math.floor(ms / 60000);
165
+ const seconds = Math.round((ms % 60000) / 1000);
166
+ return `${minutes}m ${seconds}s`;
167
+ },
168
+
169
+ /**
170
+ * Parse duration string to milliseconds
171
+ * @param {string} duration - Duration string (e.g., "5m", "30s")
172
+ * @returns {number} Milliseconds
173
+ */
174
+ parseDuration(duration) {
175
+ const match = duration.match(/^(\d+)([smh])$/);
176
+ if (!match) throw new Error(`Invalid duration format: ${duration}`);
177
+
178
+ const [, amount, unit] = match;
179
+ const multipliers = { s: 1000, m: 60000, h: 3600000 };
180
+ return parseInt(amount, 10) * multipliers[unit];
181
+ },
182
+
183
+ /**
184
+ * Deep merge objects
185
+ * @param {Object} target - Target object
186
+ * @param {Object} source - Source object to merge
187
+ * @returns {Object} Merged object
188
+ */
189
+ deepMerge(target, source) {
190
+ const result = { ...target };
191
+
192
+ for (const key in source) {
193
+ if (source.hasOwnProperty(key)) {
194
+ if (
195
+ typeof source[key] === 'object' &&
196
+ source[key] !== null &&
197
+ !Array.isArray(source[key])
198
+ ) {
199
+ result[key] = this.deepMerge(result[key] || {}, source[key]);
200
+ } else {
201
+ result[key] = source[key];
202
+ }
203
+ }
204
+ }
205
+
206
+ return result;
207
+ },
208
+
209
+ /**
210
+ * Validate URL format
211
+ * @param {string} url - URL to validate
212
+ * @returns {boolean} True if valid URL
213
+ */
214
+ isValidUrl(url) {
215
+ try {
216
+ new URL(url);
217
+ return true;
218
+ } catch {
219
+ return false;
220
+ }
221
+ },
222
+ };
223
+
224
+ export default { Logger, utils };
@@ -0,0 +1,328 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ TEST FILE EXAMPLES ║
4
+ * ║ Unit and integration tests for wilfredwake ║
5
+ * ╚═══════════════════════════════════════════════════════════════╝
6
+ *
7
+ * Run tests with: npm test
8
+ * Watch mode: npm run test:watch
9
+ */
10
+
11
+ import test from 'node:test';
12
+ import assert from 'node:assert';
13
+ import ServiceRegistry from '../src/orchestrator/registry.js';
14
+ import { Orchestrator, ServiceState } from '../src/orchestrator/orchestrator.js';
15
+
16
+ /**
17
+ * Test Suite: Service Registry
18
+ */
19
+ test('ServiceRegistry - Load and validate YAML', async (t) => {
20
+ const registry = new ServiceRegistry();
21
+
22
+ const yaml = `
23
+ services:
24
+ dev:
25
+ auth:
26
+ url: https://auth.test
27
+ health: /health
28
+ wake: /wake
29
+ dependsOn: []
30
+ payment:
31
+ url: https://payment.test
32
+ health: /health
33
+ wake: /wake
34
+ dependsOn: [auth]
35
+ `;
36
+
37
+ await registry.loadFromString(yaml, 'yaml');
38
+ const services = registry.getServices('dev');
39
+
40
+ assert.equal(services.length, 2, 'Should load 2 services');
41
+ assert.equal(services[0].name, 'auth', 'First service should be auth');
42
+ });
43
+
44
+ test('ServiceRegistry - Resolve wake order with dependencies', async (t) => {
45
+ const registry = new ServiceRegistry();
46
+
47
+ const yaml = `
48
+ services:
49
+ dev:
50
+ auth:
51
+ url: https://auth.test
52
+ health: /health
53
+ wake: /wake
54
+ dependsOn: []
55
+ payment:
56
+ url: https://payment.test
57
+ health: /health
58
+ wake: /wake
59
+ dependsOn: [auth]
60
+ consumer:
61
+ url: https://consumer.test
62
+ health: /health
63
+ wake: /wake
64
+ dependsOn: [payment]
65
+ `;
66
+
67
+ await registry.loadFromString(yaml, 'yaml');
68
+ const order = registry.resolveWakeOrder('all', 'dev');
69
+
70
+ assert.equal(order.length, 3, 'Should resolve 3 services');
71
+ assert.equal(order[0].name, 'auth', 'Auth should be first');
72
+ assert.equal(order[1].name, 'payment', 'Payment should be second');
73
+ assert.equal(order[2].name, 'consumer', 'Consumer should be third');
74
+ });
75
+
76
+ test('ServiceRegistry - Detect circular dependencies', async (t) => {
77
+ const registry = new ServiceRegistry();
78
+
79
+ const yaml = `
80
+ services:
81
+ dev:
82
+ auth:
83
+ url: https://auth.test
84
+ health: /health
85
+ wake: /wake
86
+ dependsOn: [payment]
87
+ payment:
88
+ url: https://payment.test
89
+ health: /health
90
+ wake: /wake
91
+ dependsOn: [auth]
92
+ `;
93
+
94
+ await registry.loadFromString(yaml, 'yaml');
95
+
96
+ assert.throws(
97
+ () => registry.resolveWakeOrder('all', 'dev'),
98
+ /Circular dependency/,
99
+ 'Should detect circular dependency'
100
+ );
101
+ });
102
+
103
+ test('ServiceRegistry - Get service by name', async (t) => {
104
+ const registry = new ServiceRegistry();
105
+
106
+ const yaml = `
107
+ services:
108
+ dev:
109
+ auth:
110
+ url: https://auth.test
111
+ health: /health
112
+ wake: /wake
113
+ dependsOn: []
114
+ `;
115
+
116
+ await registry.loadFromString(yaml, 'yaml');
117
+ const service = registry.getService('auth', 'dev');
118
+
119
+ assert.ok(service, 'Should find service');
120
+ assert.equal(service.name, 'auth', 'Service name should match');
121
+ assert.equal(service.url, 'https://auth.test', 'Service URL should match');
122
+ });
123
+
124
+ test('ServiceRegistry - Get registry statistics', async (t) => {
125
+ const registry = new ServiceRegistry();
126
+
127
+ const yaml = `
128
+ services:
129
+ dev:
130
+ auth:
131
+ url: https://auth.test
132
+ health: /health
133
+ wake: /wake
134
+ dependsOn: []
135
+ payment:
136
+ url: https://payment.test
137
+ health: /health
138
+ wake: /wake
139
+ dependsOn: [auth]
140
+ staging:
141
+ auth:
142
+ url: https://auth-staging.test
143
+ health: /health
144
+ wake: /wake
145
+ dependsOn: []
146
+ `;
147
+
148
+ await registry.loadFromString(yaml, 'yaml');
149
+ const stats = registry.getStats();
150
+
151
+ assert.equal(stats.totalServices, 3, 'Should count 3 total services');
152
+ assert.equal(stats.environments.length, 2, 'Should have 2 environments');
153
+ });
154
+
155
+ /**
156
+ * Test Suite: Orchestrator
157
+ */
158
+ test('Orchestrator - Wake order respects dependencies', async (t) => {
159
+ const registry = new ServiceRegistry();
160
+
161
+ const yaml = `
162
+ services:
163
+ dev:
164
+ auth:
165
+ url: https://auth.test
166
+ health: /health
167
+ wake: /wake
168
+ dependsOn: []
169
+ payment:
170
+ url: https://payment.test
171
+ health: /health
172
+ wake: /wake
173
+ dependsOn: [auth]
174
+ `;
175
+
176
+ await registry.loadFromString(yaml, 'yaml');
177
+ const orchestrator = new Orchestrator(registry);
178
+
179
+ const order = registry.resolveWakeOrder('payment', 'dev');
180
+
181
+ assert.equal(order[0].name, 'auth', 'Auth should wake first');
182
+ assert.equal(order[1].name, 'payment', 'Payment should wake second');
183
+ });
184
+
185
+ test('Orchestrator - Set and get service state', async (t) => {
186
+ const registry = new ServiceRegistry();
187
+
188
+ const yaml = `
189
+ services:
190
+ dev:
191
+ auth:
192
+ url: https://auth.test
193
+ health: /health
194
+ wake: /wake
195
+ dependsOn: []
196
+ `;
197
+
198
+ await registry.loadFromString(yaml, 'yaml');
199
+ const orchestrator = new Orchestrator(registry);
200
+
201
+ orchestrator._setServiceState('auth', ServiceState.LIVE);
202
+
203
+ // Note: In real implementation, would have a getter method
204
+ assert.ok(orchestrator.serviceStates.has('auth'), 'Should store service state');
205
+ });
206
+
207
+ /**
208
+ * Test Suite: Error Handling
209
+ */
210
+ test('ServiceRegistry - Validate missing required fields', async (t) => {
211
+ const registry = new ServiceRegistry();
212
+
213
+ const invalidYaml = `
214
+ services:
215
+ dev:
216
+ auth:
217
+ url: https://auth.test
218
+ `;
219
+
220
+ try {
221
+ await registry.loadFromString(invalidYaml, 'yaml');
222
+ assert.fail('Should have thrown validation error');
223
+ } catch (error) {
224
+ assert.ok(error.message.includes('health'), 'Should error on missing health endpoint');
225
+ }
226
+ });
227
+
228
+ test('ServiceRegistry - Handle invalid YAML', async (t) => {
229
+ const registry = new ServiceRegistry();
230
+
231
+ const invalidYaml = `
232
+ services:
233
+ dev: [invalid]
234
+ `;
235
+
236
+ try {
237
+ await registry.loadFromString(invalidYaml, 'yaml');
238
+ assert.fail('Should have thrown error');
239
+ } catch (error) {
240
+ assert.ok(error.message.includes('must be an object'), 'Should error on invalid structure');
241
+ }
242
+ });
243
+
244
+ /**
245
+ * Test Suite: Configuration
246
+ */
247
+ test('Config - Validate URL format', async (t) => {
248
+ const { utils } = await import('../src/shared/logger.js');
249
+
250
+ assert.ok(utils.isValidUrl('http://localhost:3000'), 'Valid URL');
251
+ assert.ok(utils.isValidUrl('https://example.com'), 'Valid HTTPS URL');
252
+ assert.ok(!utils.isValidUrl('not-a-url'), 'Invalid URL');
253
+ });
254
+
255
+ test('Config - Parse duration strings', async (t) => {
256
+ const { utils } = await import('../src/shared/logger.js');
257
+
258
+ assert.equal(utils.parseDuration('5s'), 5000, 'Parse seconds');
259
+ assert.equal(utils.parseDuration('2m'), 120000, 'Parse minutes');
260
+ assert.equal(utils.parseDuration('1h'), 3600000, 'Parse hours');
261
+ });
262
+
263
+ test('Config - Format duration to readable string', async (t) => {
264
+ const { utils } = await import('../src/shared/logger.js');
265
+
266
+ assert.equal(utils.formatDuration(500), '500ms', 'Format milliseconds');
267
+ assert.equal(utils.formatDuration(5000), '5.0s', 'Format seconds');
268
+ assert.equal(utils.formatDuration(65000), '1m 5s', 'Format minutes');
269
+ });
270
+
271
+ /**
272
+ * Test Suite: Deep Merge
273
+ */
274
+ test('Utils - Deep merge objects', async (t) => {
275
+ const { utils } = await import('../src/shared/logger.js');
276
+
277
+ const target = { a: 1, b: { c: 2 } };
278
+ const source = { b: { d: 3 }, e: 4 };
279
+ const result = utils.deepMerge(target, source);
280
+
281
+ assert.equal(result.a, 1, 'Should preserve target properties');
282
+ assert.equal(result.b.c, 2, 'Should preserve nested target');
283
+ assert.equal(result.b.d, 3, 'Should add new nested properties');
284
+ assert.equal(result.e, 4, 'Should add new properties');
285
+ });
286
+
287
+ /**
288
+ * Test Suite: Retry Logic
289
+ */
290
+ test('Utils - Retry with exponential backoff', async (t) => {
291
+ const { utils } = await import('../src/shared/logger.js');
292
+
293
+ let attempts = 0;
294
+
295
+ const result = await utils.retry(
296
+ async () => {
297
+ attempts++;
298
+ if (attempts < 3) {
299
+ throw new Error('Fail');
300
+ }
301
+ return 'success';
302
+ },
303
+ 5,
304
+ 100
305
+ );
306
+
307
+ assert.equal(result, 'success', 'Should succeed on retry');
308
+ assert.equal(attempts, 3, 'Should make 3 attempts');
309
+ });
310
+
311
+ test('Utils - Retry timeout after max attempts', async (t) => {
312
+ const { utils } = await import('../src/shared/logger.js');
313
+
314
+ try {
315
+ await utils.retry(
316
+ async () => {
317
+ throw new Error('Always fails');
318
+ },
319
+ 3,
320
+ 10
321
+ );
322
+ assert.fail('Should have thrown error');
323
+ } catch (error) {
324
+ assert.ok(error.message.includes('Always fails'), 'Should throw last error');
325
+ }
326
+ });
327
+
328
+ console.log('\n✓ All tests completed');