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,192 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ INIT COMMAND - First-Time Setup ║
4
+ * ║ Guides user through configuration initialization ║
5
+ * ║ Sets orchestrator URL and API token ║
6
+ * ╚═══════════════════════════════════════════════════════════════╝
7
+ */
8
+
9
+ import readline from 'readline';
10
+ import chalk from 'chalk';
11
+ import ConfigManager from '../config.js';
12
+ import { Logger, utils } from '../../shared/logger.js';
13
+ import { format } from '../../shared/colors.js';
14
+
15
+ const logger = new Logger();
16
+
17
+ /**
18
+ * Initialize wilfredwake configuration
19
+ * Prompts user for orchestrator URL and API token
20
+ * Creates ~/.wilfredwake/config.json
21
+ *
22
+ * @param {Object} options - Command options
23
+ * @param {string} options.orchestrator - Orchestrator URL
24
+ * @param {string} options.token - API token
25
+ */
26
+ export async function initCommand(options) {
27
+ try {
28
+ // ═══════════════════════════════════════════════════════════════
29
+ // INITIALIZATION HEADER
30
+ // ═══════════════════════════════════════════════════════════════
31
+ console.log('\n' + chalk.cyanBright.bold('╔════════════════════════════════════╗'));
32
+ console.log(chalk.cyanBright.bold('║ WILFREDWAKE INITIALIZATION WIZARD ║'));
33
+ console.log(chalk.cyanBright.bold('╚════════════════════════════════════╝\n'));
34
+
35
+ logger.info('Welcome to wilfredwake! Let\'s set up your environment.');
36
+
37
+ const configManager = new ConfigManager();
38
+ await configManager.init();
39
+
40
+ // ═══════════════════════════════════════════════════════════════
41
+ // GATHER CONFIGURATION DETAILS
42
+ // ═══════════════════════════════════════════════════════════════
43
+
44
+ const answers = await _promptForConfig(options);
45
+
46
+ logger.section('Validating Configuration');
47
+
48
+ // ═══════════════════════════════════════════════════════════════
49
+ // VALIDATE ORCHESTRATOR URL
50
+ // ═══════════════════════════════════════════════════════════════
51
+ process.stdout.write(
52
+ chalk.dim(`Validating orchestrator at ${answers.orchestrator}... `)
53
+ );
54
+
55
+ const isReachable = await utils.isUrlReachable(
56
+ `${answers.orchestrator}/health`,
57
+ 5000
58
+ );
59
+
60
+ if (isReachable) {
61
+ logger.success('Orchestrator is reachable!');
62
+ } else {
63
+ logger.warn(
64
+ `Could not reach orchestrator. Make sure it's running at ${answers.orchestrator}`
65
+ );
66
+ }
67
+
68
+ // ═══════════════════════════════════════════════════════════════
69
+ // SAVE CONFIGURATION
70
+ // ═══════════════════════════════════════════════════════════════
71
+ logger.section('Saving Configuration');
72
+
73
+ const config = await configManager.loadConfig();
74
+ config.orchestratorUrl = answers.orchestrator;
75
+ if (answers.token) {
76
+ config.token = answers.token;
77
+ }
78
+ config.environment = answers.environment;
79
+
80
+ await configManager.saveConfig(config);
81
+
82
+ logger.success(`Configuration saved to ${configManager.configFile}`);
83
+
84
+ // ═══════════════════════════════════════════════════════════════
85
+ // COMPLETION MESSAGE
86
+ // ═══════════════════════════════════════════════════════════════
87
+ console.log('\n' + chalk.greenBright.bold('✓ Setup Complete!\n'));
88
+
89
+ console.log(chalk.cyan('Configuration Summary:'));
90
+ console.log(chalk.dim(` • Orchestrator: ${answers.orchestrator}`));
91
+ console.log(chalk.dim(` • Environment: ${answers.environment}`));
92
+ if (answers.token) {
93
+ console.log(chalk.dim(` • Token: ${answers.token.substring(0, 10)}...`));
94
+ }
95
+ console.log('');
96
+
97
+ console.log(chalk.cyan('Next steps:'));
98
+ console.log(chalk.yellow(' $ wilfredwake status # Check service status'));
99
+ console.log(chalk.yellow(' $ wilfredwake wake all # Wake all services'));
100
+ console.log(chalk.yellow(' $ wilfredwake --help # View all commands\n'));
101
+
102
+ process.exit(0);
103
+ } catch (error) {
104
+ logger.error(`Initialization failed: ${error.message}`);
105
+ process.exit(1);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Prompt user for configuration values
111
+ * @private
112
+ * @param {Object} options - Pre-provided options
113
+ * @returns {Promise<Object>} User answers
114
+ */
115
+ async function _promptForConfig(options) {
116
+ const rl = readline.createInterface({
117
+ input: process.stdin,
118
+ output: process.stdout,
119
+ });
120
+
121
+ const question = (query) =>
122
+ new Promise((resolve) => {
123
+ rl.question(chalk.blueBright(`? ${query}`), resolve);
124
+ });
125
+
126
+ try {
127
+ // ═══════════════════════════════════════════════════════════════
128
+ // ORCHESTRATOR URL
129
+ // ═══════════════════════════════════════════════════════════════
130
+ let orchestrator = options.orchestrator;
131
+
132
+ if (!orchestrator) {
133
+ console.log(
134
+ chalk.dim(
135
+ '\nFirst, we need the URL of your orchestrator service.'
136
+ )
137
+ );
138
+ console.log(
139
+ chalk.dim(
140
+ 'This is the always-on backend that manages your services.'
141
+ )
142
+ );
143
+ console.log('');
144
+
145
+ orchestrator = await question(
146
+ `Orchestrator URL ${chalk.gray('(default: http://localhost:3000)')} `
147
+ );
148
+ orchestrator = orchestrator || 'http://localhost:3000';
149
+ }
150
+
151
+ // Validate URL format
152
+ if (!utils.isValidUrl(orchestrator)) {
153
+ throw new Error(`Invalid URL format: ${orchestrator}`);
154
+ }
155
+
156
+ // ═══════════════════════════════════════════════════════════════
157
+ // ENVIRONMENT SELECTION
158
+ // ═══════════════════════════════════════════════════════════════
159
+ console.log('');
160
+ const envOptions = ['dev', 'staging', 'prod'];
161
+ let environment = 'dev';
162
+
163
+ console.log(chalk.dim('Select your working environment:'));
164
+ envOptions.forEach((env, idx) => {
165
+ console.log(chalk.gray(` ${idx + 1}. ${env}`));
166
+ });
167
+
168
+ const envChoice = await question('Environment (1-3, default: 1) ');
169
+ const envIdx = parseInt(envChoice || '1') - 1;
170
+
171
+ if (envIdx >= 0 && envIdx < envOptions.length) {
172
+ environment = envOptions[envIdx];
173
+ }
174
+
175
+ // ═══════════════════════════════════════════════════════════════
176
+ // API TOKEN (Optional)
177
+ // ═══════════════════════════════════════════════════════════════
178
+ console.log('');
179
+ const token = options.token ||
180
+ (await question(
181
+ `API Token ${chalk.gray('(optional, press Enter to skip)')} `
182
+ ));
183
+
184
+ return {
185
+ orchestrator: orchestrator.trim(),
186
+ token: token ? token.trim() : null,
187
+ environment,
188
+ };
189
+ } finally {
190
+ rl.close();
191
+ }
192
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ STATUS COMMAND - Check Service Status ║
4
+ * ║ Queries orchestrator for real-time service states ║
5
+ * ║ Displays status in table or JSON format ║
6
+ * ╚═══════════════════════════════════════════════════════════════╝
7
+ */
8
+
9
+ import axios from 'axios';
10
+ import ConfigManager from '../config.js';
11
+ import { Logger, utils } from '../../shared/logger.js';
12
+ import { colors, format } from '../../shared/colors.js';
13
+ import chalk from 'chalk';
14
+
15
+ const logger = new Logger();
16
+
17
+ /**
18
+ * Check status of services
19
+ * Defaults to showing all services if none specified
20
+ * Can also filter by specific service or service group
21
+ *
22
+ * @param {string} service - Optional service name to filter (defaults to 'all')
23
+ * @param {Object} options - Command options
24
+ * @param {string} options.env - Environment (dev, staging, prod)
25
+ * @param {string} options.format - Output format (table, json)
26
+ */
27
+ export async function statusCommand(service, options) {
28
+ try {
29
+ // ═══════════════════════════════════════════════════════════════
30
+ // LOAD CONFIGURATION
31
+ // ═══════════════════════════════════════════════════════════════
32
+ const configManager = new ConfigManager();
33
+ const config = await configManager.loadConfig();
34
+
35
+ const env = options.env || config.environment;
36
+ const outputFormat = options.format || config.preferences.outputFormat;
37
+ // DEFAULT: Show all services if none specified
38
+ const serviceFilter = service || 'all';
39
+
40
+ logger.section('Service Status Check');
41
+
42
+ // ═══════════════════════════════════════════════════════════════
43
+ // FETCH STATUS FROM ORCHESTRATOR
44
+ // ═══════════════════════════════════════════════════════════════
45
+ let stopSpinner;
46
+ try {
47
+ stopSpinner = logger.spinner('Fetching service status...');
48
+
49
+ const response = await axios.get(
50
+ `${config.orchestratorUrl}/api/status`,
51
+ {
52
+ params: {
53
+ environment: env,
54
+ service: serviceFilter !== 'all' ? serviceFilter : undefined,
55
+ },
56
+ timeout: 10000,
57
+ }
58
+ );
59
+
60
+ stopSpinner();
61
+ console.log(''); // New line after spinner
62
+
63
+ const services = response.data.services || [];
64
+
65
+ // ═══════════════════════════════════════════════════════════════
66
+ // DISPLAY RESULTS
67
+ // ═══════════════════════════════════════════════════════════════
68
+
69
+ if (outputFormat === 'json') {
70
+ _displayJsonStatus(services);
71
+ } else {
72
+ _displayTableStatus(services, env);
73
+ }
74
+
75
+ // ═══════════════════════════════════════════════════════════════
76
+ // SUMMARY STATISTICS
77
+ // ═══════════════════════════════════════════════════════════════
78
+ _displaySummary(services);
79
+
80
+ process.exit(0);
81
+ } catch (error) {
82
+ stopSpinner?.();
83
+ throw error;
84
+ }
85
+ } catch (error) {
86
+ logger.error(`Failed to fetch status: ${error.message}`);
87
+
88
+ if (error.code === 'ECONNREFUSED') {
89
+ console.log(
90
+ chalk.yellowBright(
91
+ '\n⚠ Could not connect to orchestrator. Make sure it\'s running.'
92
+ )
93
+ );
94
+ console.log(chalk.dim('Run: wilfredwake init'));
95
+ }
96
+
97
+ process.exit(1);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Display status in table format with colors
103
+ * @private
104
+ * @param {Array} services - Services array
105
+ * @param {string} environment - Environment name
106
+ */
107
+ function _displayTableStatus(services, environment) {
108
+ if (services.length === 0) {
109
+ logger.info('No services found in registry.');
110
+ return;
111
+ }
112
+
113
+ console.log(chalk.cyanBright.bold(`\n📊 Services (${environment.toUpperCase()})\n`));
114
+
115
+ // ═══════════════════════════════════════════════════════════════
116
+ // TABLE HEADER
117
+ // ═══════════════════════════════════════════════════════════════
118
+ const headers = ['Service', 'Status', 'Last Woken', 'URL'];
119
+ console.log(format.tableHeader(headers));
120
+
121
+ // ═══════════════════════════════════════════════════════════════
122
+ // TABLE ROWS
123
+ // ═══════════════════════════════════════════════════════════════
124
+ services.forEach((service) => {
125
+ const statusColor = colors.status[service.status] || colors.status.unknown;
126
+ const lastWoken = service.lastWakeTime
127
+ ? new Date(service.lastWakeTime).toLocaleString()
128
+ : 'Never';
129
+
130
+ const cells = [
131
+ chalk.cyan(service.name.padEnd(20)),
132
+ statusColor(service.status.toUpperCase().padEnd(20)),
133
+ chalk.yellow(lastWoken.padEnd(20)),
134
+ chalk.gray(service.url.substring(0, 20).padEnd(20)),
135
+ ];
136
+ console.log(format.tableRow(cells));
137
+ });
138
+
139
+ console.log(''); // Spacing
140
+ }
141
+
142
+ /**
143
+ * Display status in JSON format
144
+ * @private
145
+ * @param {Array} services - Services array
146
+ */
147
+ function _displayJsonStatus(services) {
148
+ console.log(JSON.stringify(services, null, 2));
149
+ }
150
+
151
+ /**
152
+ * Display summary statistics
153
+ * @private
154
+ * @param {Array} services - Services array
155
+ */
156
+ function _displaySummary(services) {
157
+ if (services.length === 0) return;
158
+
159
+ const stats = {
160
+ total: services.length,
161
+ ready: services.filter(s => s.status === 'ready').length,
162
+ sleeping: services.filter(s => s.status === 'sleeping').length,
163
+ waking: services.filter(s => s.status === 'waking').length,
164
+ failed: services.filter(s => s.status === 'failed').length,
165
+ };
166
+
167
+ console.log(chalk.magentaBright.bold('Summary'));
168
+ console.log(
169
+ ` ${colors.status.ready('✓')} Ready: ${colors.status.ready(
170
+ stats.ready
171
+ )} | ${colors.status.sleeping('⚫')} Sleeping: ${colors.status.sleeping(
172
+ stats.sleeping
173
+ )} | ${colors.status.waking('⟳')} Waking: ${colors.status.waking(
174
+ stats.waking
175
+ )} | ${colors.status.failed('✗')} Failed: ${colors.status.failed(
176
+ stats.failed
177
+ )}`
178
+ );
179
+ console.log(` Total: ${stats.total} services\n`);
180
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * ╔═══════════════════════════════════════════════════════════════╗
3
+ * ║ WAKE COMMAND - Wake Services On Demand ║
4
+ * ║ Initiates service wake with dependency ordering ║
5
+ * ║ Polls health checks until ready or timeout ║
6
+ * ╚═══════════════════════════════════════════════════════════════╝
7
+ */
8
+
9
+ import axios from 'axios';
10
+ import ConfigManager from '../config.js';
11
+ import { Logger, utils } from '../../shared/logger.js';
12
+ import { colors, format } from '../../shared/colors.js';
13
+ import chalk from 'chalk';
14
+
15
+ const logger = new Logger();
16
+
17
+ /**
18
+ * Wake services on demand
19
+ * Defaults to waking all services if none specified
20
+ * Can also wake specific service or service group
21
+ * Respects dependency order and waits for readiness
22
+ *
23
+ * @param {string} target - Wake target (defaults to 'all', or <service>, or <group>)
24
+ * @param {Object} options - Command options
25
+ * @param {string} options.env - Environment (dev, staging, prod)
26
+ * @param {boolean} options.wait - Wait for services to be ready
27
+ * @param {string} options.timeout - Timeout in seconds
28
+ */
29
+ export async function wakeCommand(target, options) {
30
+ try {
31
+ // ═══════════════════════════════════════════════════════════════
32
+ // LOAD CONFIGURATION
33
+ // ═══════════════════════════════════════════════════════════════
34
+ const configManager = new ConfigManager();
35
+ const config = await configManager.loadConfig();
36
+
37
+ const env = options.env || config.environment;
38
+ const shouldWait = options.wait !== false;
39
+ const timeout = parseInt(options.timeout || config.preferences.timeout);
40
+ // DEFAULT: Wake all services if none specified
41
+ const wakeTarget = target || 'all';
42
+
43
+ logger.section('Service Wake Request');
44
+
45
+ logger.info(
46
+ `Waking ${chalk.cyan(wakeTarget)} in ${chalk.yellow(env)} environment...`
47
+ );
48
+
49
+ // ═══════════════════════════════════════════════════════════════
50
+ // SEND WAKE REQUEST TO ORCHESTRATOR
51
+ // ═══════════════════════════════════════════════════════════════
52
+ let stopSpinner;
53
+ try {
54
+ stopSpinner = logger.spinner(
55
+ `Sending wake request for ${chalk.cyan(wakeTarget)}...`
56
+ );
57
+
58
+ const response = await axios.post(
59
+ `${config.orchestratorUrl}/api/wake`,
60
+ {
61
+ target: wakeTarget,
62
+ environment: env,
63
+ wait: shouldWait,
64
+ timeout,
65
+ },
66
+ {
67
+ timeout: (timeout + 10) * 1000,
68
+ headers: {
69
+ Authorization: config.token ? `Bearer ${config.token}` : undefined,
70
+ },
71
+ }
72
+ );
73
+
74
+ stopSpinner();
75
+ console.log(''); // New line after spinner
76
+
77
+ // ═══════════════════════════════════════════════════════════════
78
+ // PROCESS WAKE RESPONSE
79
+ // ═══════════════════════════════════════════════════════════════
80
+ const results = response.data;
81
+
82
+ _displayWakeResults(results);
83
+
84
+ // ═══════════════════════════════════════════════════════════════
85
+ // DISPLAY FINAL STATUS
86
+ // ═══════════════════════════════════════════════════════════════
87
+ if (shouldWait) {
88
+ _displayFinalStatus(results);
89
+ }
90
+
91
+ process.exit(results.success ? 0 : 1);
92
+ } catch (error) {
93
+ stopSpinner?.();
94
+ throw error;
95
+ }
96
+ } catch (error) {
97
+ if (error.response?.status === 404) {
98
+ logger.error(`Service not found: ${error.response.data.message}`);
99
+ } else if (error.code === 'ECONNREFUSED') {
100
+ logger.error('Could not connect to orchestrator. Is it running?');
101
+ console.log(chalk.dim('Run: wilfredwake init'));
102
+ } else {
103
+ logger.error(`Wake request failed: ${error.message}`);
104
+ }
105
+
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Display wake operation results
112
+ * @private
113
+ * @param {Object} results - Wake results from orchestrator
114
+ */
115
+ function _displayWakeResults(results) {
116
+ console.log(chalk.cyanBright.bold('\n🌅 Wake Operation Results\n'));
117
+
118
+ if (!results.services || results.services.length === 0) {
119
+ logger.info('No services to wake.');
120
+ return;
121
+ }
122
+
123
+ // ═══════════════════════════════════════════════════════════════
124
+ // SERVICE WAKE TIMELINE
125
+ // ═══════════════════════════════════════════════════════════════
126
+ console.log(chalk.cyan('Timeline:'));
127
+
128
+ results.services.forEach((service, index) => {
129
+ const duration = service.duration
130
+ ? ` (${utils.formatDuration(service.duration)})`
131
+ : '';
132
+
133
+ let statusIndicator;
134
+ if (service.status === 'live') {
135
+ statusIndicator = chalk.greenBright('✓');
136
+ } else if (service.status === 'failed') {
137
+ statusIndicator = chalk.redBright('✗');
138
+ } else {
139
+ statusIndicator = chalk.yellowBright('⟳');
140
+ }
141
+
142
+ const arrow = index < results.services.length - 1 ? '│' : '└';
143
+ console.log(
144
+ ` ${chalk.dim(arrow)} ${statusIndicator} ${chalk.cyan(
145
+ service.name
146
+ )}${chalk.yellow(duration)}`
147
+ );
148
+
149
+ if (service.error) {
150
+ console.log(` ${chalk.redBright(`Error: ${service.error}`)}`);
151
+ }
152
+ });
153
+
154
+ console.log('');
155
+ }
156
+
157
+ /**
158
+ * Display final status summary
159
+ * @private
160
+ * @param {Object} results - Wake results from orchestrator
161
+ */
162
+ function _displayFinalStatus(results) {
163
+ const succeeded = results.services.filter(s => s.status === 'live').length;
164
+ const failed = results.services.filter(s => s.status === 'failed').length;
165
+ const totalTime = utils.formatDuration(results.totalDuration || 0);
166
+
167
+ console.log(chalk.magentaBright.bold('✓ Wake Complete\n'));
168
+
169
+ console.log(` ${colors.status.live('✓')} Live: ${colors.status.live(succeeded)}`);
170
+ if (failed > 0) {
171
+ console.log(` ${colors.status.failed('✗')} Failed: ${colors.status.failed(failed)}`);
172
+ }
173
+ console.log(` ${chalk.yellow('⏱')} Total Time: ${chalk.yellow(totalTime)}\n`);
174
+
175
+ if (results.success) {
176
+ logger.success('All services are live!');
177
+ } else {
178
+ logger.warn('Some services failed to wake. Check errors above.');
179
+ }
180
+
181
+ console.log('');
182
+ }