@bigtyphoon/melo 1.7.6 → 1.8.1

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,514 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Check and validate project configuration
5
+ * Reports errors and warnings for common configuration issues
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { Command } from 'commander';
11
+
12
+ interface CheckResult {
13
+ type: 'error' | 'warning' | 'info' | 'success';
14
+ message: string;
15
+ file?: string;
16
+ fix?: string;
17
+ }
18
+
19
+ export default function (program: Command) {
20
+ program.command('doctor')
21
+ .description('Check project configuration for errors and warnings')
22
+ .option('-f, --fix', 'attempt to fix minor issues automatically', false)
23
+ .action(function (opts: { fix: boolean }) {
24
+ runDoctor(opts.fix);
25
+ });
26
+ }
27
+
28
+ function runDoctor(autoFix: boolean) {
29
+ const cwd = process.cwd();
30
+ const results: CheckResult[] = [];
31
+
32
+ console.info('šŸ” Checking melo project configuration...\n');
33
+
34
+ // Check 1: Is this a valid melo project?
35
+ if (!isMeloProject(cwd)) {
36
+ results.push({
37
+ type: 'error',
38
+ message: 'Not a valid melo project.',
39
+ fix: 'Run "melo init" to create a new project.'
40
+ });
41
+ printResults(results);
42
+ process.exit(1);
43
+ }
44
+
45
+ results.push({
46
+ type: 'success',
47
+ message: 'Valid melo project structure found.'
48
+ });
49
+
50
+ // Check 2: Validate adminServer.json
51
+ results.push(...checkAdminServerJson(cwd));
52
+
53
+ // Check 3: Validate servers.json
54
+ results.push(...checkServersJson(cwd));
55
+
56
+ // Check 4: Validate app.ts
57
+ results.push(...checkAppTs(cwd));
58
+
59
+ // Check 5: Check server directories consistency
60
+ results.push(...checkServerDirectories(cwd));
61
+
62
+ // Check 6: Check for duplicate server IDs
63
+ results.push(...checkDuplicateServerIds(cwd));
64
+
65
+ // Check 7: Check port conflicts
66
+ results.push(...checkPortConflicts(cwd));
67
+
68
+ // Check 8: Check for missing handler/remote files
69
+ results.push(...checkMissingHandlers(cwd));
70
+
71
+ printResults(results);
72
+
73
+ const errors = results.filter(r => r.type === 'error').length;
74
+ const warnings = results.filter(r => r.type === 'warning').length;
75
+ const fixes = results.filter(r => r.fix).length;
76
+
77
+ console.info(`\nšŸ“Š Summary: ${errors} errors, ${warnings} warnings`);
78
+
79
+ if (errors === 0 && warnings === 0) {
80
+ console.info('āœ… All checks passed! Your project looks good.');
81
+ } else if (errors === 0) {
82
+ console.info('āš ļø Project has some warnings but should work.');
83
+ } else {
84
+ console.info('āŒ Please fix the errors above before starting the server.');
85
+ }
86
+
87
+ if (fixes > 0 && !autoFix) {
88
+ console.info(`\nšŸ’” Run "melo doctor --fix" to attempt automatic fixes for some issues.`);
89
+ }
90
+
91
+ process.exit(errors > 0 ? 1 : 0);
92
+ }
93
+
94
+ function isMeloProject(cwd: string): boolean {
95
+ const requiredPaths = ['app.ts', 'config', 'app/servers'];
96
+ return requiredPaths.every(p => fs.existsSync(path.join(cwd, p)));
97
+ }
98
+
99
+ function checkAdminServerJson(cwd: string): CheckResult[] {
100
+ const results: CheckResult[] = [];
101
+ const filePath = path.join(cwd, 'config', 'adminServer.json');
102
+
103
+ if (!fs.existsSync(filePath)) {
104
+ results.push({
105
+ type: 'error',
106
+ message: 'adminServer.json not found.',
107
+ file: 'config/adminServer.json',
108
+ fix: 'Create adminServer.json with at least master server configuration.'
109
+ });
110
+ return results;
111
+ }
112
+
113
+ let adminServers: any[];
114
+ try {
115
+ adminServers = JSON.parse(fs.readFileSync(filePath, 'utf8'));
116
+ } catch (e) {
117
+ results.push({
118
+ type: 'error',
119
+ message: 'adminServer.json is not valid JSON.',
120
+ file: 'config/adminServer.json',
121
+ fix: 'Fix JSON syntax errors.'
122
+ });
123
+ return results;
124
+ }
125
+
126
+ if (!Array.isArray(adminServers)) {
127
+ results.push({
128
+ type: 'error',
129
+ message: 'adminServer.json must be an array.',
130
+ file: 'config/adminServer.json'
131
+ });
132
+ return results;
133
+ }
134
+
135
+ // Check for master
136
+ const hasMaster = adminServers.some(s => s.type === 'master');
137
+ if (!hasMaster) {
138
+ results.push({
139
+ type: 'error',
140
+ message: 'Missing "master" server type in adminServer.json.',
141
+ file: 'config/adminServer.json',
142
+ fix: 'Add master server configuration.'
143
+ });
144
+ }
145
+
146
+ // Check for connector
147
+ const hasConnector = adminServers.some(s => s.type === 'connector');
148
+ if (!hasConnector) {
149
+ results.push({
150
+ type: 'warning',
151
+ message: 'Missing "connector" server type. Project may not accept client connections.',
152
+ file: 'config/adminServer.json'
153
+ });
154
+ }
155
+
156
+ // Check for duplicate types
157
+ const types = adminServers.map(s => s.type);
158
+ const duplicates = types.filter((item, index) => types.indexOf(item) !== index);
159
+ if (duplicates.length > 0) {
160
+ results.push({
161
+ type: 'error',
162
+ message: `Duplicate server types found: ${[...new Set(duplicates)].join(', ')}`,
163
+ file: 'config/adminServer.json'
164
+ });
165
+ }
166
+
167
+ // Check for missing tokens
168
+ adminServers.forEach(server => {
169
+ if (!server.token) {
170
+ results.push({
171
+ type: 'warning',
172
+ message: `Server type "${server.type}" is missing a token.`,
173
+ file: 'config/adminServer.json',
174
+ fix: 'Add a secure token for this server type.'
175
+ });
176
+ }
177
+ });
178
+
179
+ return results;
180
+ }
181
+
182
+ function checkServersJson(cwd: string): CheckResult[] {
183
+ const results: CheckResult[] = [];
184
+ const filePath = path.join(cwd, 'config', 'servers.json');
185
+
186
+ if (!fs.existsSync(filePath)) {
187
+ results.push({
188
+ type: 'error',
189
+ message: 'servers.json not found.',
190
+ file: 'config/servers.json'
191
+ });
192
+ return results;
193
+ }
194
+
195
+ let servers: any;
196
+ try {
197
+ servers = JSON.parse(fs.readFileSync(filePath, 'utf8'));
198
+ } catch (e) {
199
+ results.push({
200
+ type: 'error',
201
+ message: 'servers.json is not valid JSON.',
202
+ file: 'config/servers.json'
203
+ });
204
+ return results;
205
+ }
206
+
207
+ // Check environments
208
+ ['development', 'production'].forEach(env => {
209
+ if (!servers[env]) {
210
+ results.push({
211
+ type: 'warning',
212
+ message: `Missing "${env}" environment configuration.`,
213
+ file: 'config/servers.json'
214
+ });
215
+ }
216
+ });
217
+
218
+ // Check each environment
219
+ Object.keys(servers).forEach(env => {
220
+ const envConfig = servers[env];
221
+
222
+ // Check for master
223
+ if (!envConfig.master || envConfig.master.length === 0) {
224
+ results.push({
225
+ type: 'error',
226
+ message: `Missing master server in "${env}" environment.`,
227
+ file: 'config/servers.json'
228
+ });
229
+ }
230
+
231
+ // Validate server entries
232
+ Object.keys(envConfig).forEach(serverType => {
233
+ const serverList = envConfig[serverType];
234
+
235
+ if (!Array.isArray(serverList)) {
236
+ results.push({
237
+ type: 'error',
238
+ message: `Server type "${serverType}" in "${env}" must be an array.`,
239
+ file: 'config/servers.json'
240
+ });
241
+ return;
242
+ }
243
+
244
+ serverList.forEach((server: any, index: number) => {
245
+ if (!server.id) {
246
+ results.push({
247
+ type: 'error',
248
+ message: `Server #${index + 1} of type "${serverType}" in "${env}" is missing an ID.`,
249
+ file: 'config/servers.json'
250
+ });
251
+ }
252
+
253
+ if (!server.host) {
254
+ results.push({
255
+ type: 'warning',
256
+ message: `Server "${server.id || index}" in "${env}" is missing host.`,
257
+ file: 'config/servers.json'
258
+ });
259
+ }
260
+
261
+ if (!server.port && server.port !== 0) {
262
+ results.push({
263
+ type: 'error',
264
+ message: `Server "${server.id || index}" in "${env}" is missing port.`,
265
+ file: 'config/servers.json'
266
+ });
267
+ }
268
+
269
+ // Check frontend servers have clientPort
270
+ if (server.frontend && !server.clientPort && server.clientPort !== 0) {
271
+ results.push({
272
+ type: 'warning',
273
+ message: `Frontend server "${server.id}" in "${env}" is missing clientPort.`,
274
+ file: 'config/servers.json'
275
+ });
276
+ }
277
+ });
278
+ });
279
+ });
280
+
281
+ return results;
282
+ }
283
+
284
+ function checkAppTs(cwd: string): CheckResult[] {
285
+ const results: CheckResult[] = [];
286
+ const filePath = path.join(cwd, 'app.ts');
287
+
288
+ if (!fs.existsSync(filePath)) {
289
+ return results;
290
+ }
291
+
292
+ const content = fs.readFileSync(filePath, 'utf8');
293
+
294
+ // Check for app.start()
295
+ if (!content.includes('app.start()')) {
296
+ results.push({
297
+ type: 'error',
298
+ message: 'app.ts is missing app.start() call.',
299
+ file: 'app.ts'
300
+ });
301
+ }
302
+
303
+ // Check for melo import
304
+ if (!content.includes('@bigtyphoon/melo') && !content.includes('melo')) {
305
+ results.push({
306
+ type: 'error',
307
+ message: 'app.ts is missing melo import.',
308
+ file: 'app.ts'
309
+ });
310
+ }
311
+
312
+ // Check for app.createApp()
313
+ if (!content.includes('createApp')) {
314
+ results.push({
315
+ type: 'error',
316
+ message: 'app.ts should call melo.createApp().',
317
+ file: 'app.ts'
318
+ });
319
+ }
320
+
321
+ return results;
322
+ }
323
+
324
+ function checkServerDirectories(cwd: string): CheckResult[] {
325
+ const results: CheckResult[] = [];
326
+ const serversDir = path.join(cwd, 'app', 'servers');
327
+
328
+ if (!fs.existsSync(serversDir)) {
329
+ return results;
330
+ }
331
+
332
+ const serverTypes = fs.readdirSync(serversDir).filter(f => {
333
+ return fs.statSync(path.join(serversDir, f)).isDirectory();
334
+ });
335
+
336
+ // Check if each server type has handler or remote directory
337
+ serverTypes.forEach(serverType => {
338
+ const serverPath = path.join(serversDir, serverType);
339
+ const hasHandler = fs.existsSync(path.join(serverPath, 'handler'));
340
+ const hasRemote = fs.existsSync(path.join(serverPath, 'remote'));
341
+
342
+ if (!hasHandler && !hasRemote) {
343
+ results.push({
344
+ type: 'warning',
345
+ message: `Server type "${serverType}" has neither handler nor remote directory.`,
346
+ file: `app/servers/${serverType}`
347
+ });
348
+ }
349
+
350
+ // Check handler directory is not empty
351
+ if (hasHandler) {
352
+ const handlerFiles = fs.readdirSync(path.join(serverPath, 'handler'))
353
+ .filter(f => f.endsWith('.ts') || f.endsWith('.js'));
354
+ if (handlerFiles.length === 0) {
355
+ results.push({
356
+ type: 'warning',
357
+ message: `Handler directory for "${serverType}" is empty.`,
358
+ file: `app/servers/${serverType}/handler`
359
+ });
360
+ }
361
+ }
362
+ });
363
+
364
+ return results;
365
+ }
366
+
367
+ function checkDuplicateServerIds(cwd: string): CheckResult[] {
368
+ const results: CheckResult[] = [];
369
+ const filePath = path.join(cwd, 'config', 'servers.json');
370
+
371
+ if (!fs.existsSync(filePath)) {
372
+ return results;
373
+ }
374
+
375
+ let servers: any;
376
+ try {
377
+ servers = JSON.parse(fs.readFileSync(filePath, 'utf8'));
378
+ } catch (e) {
379
+ return results;
380
+ }
381
+
382
+ Object.keys(servers).forEach(env => {
383
+ const envConfig = servers[env];
384
+ const ids: string[] = [];
385
+
386
+ Object.keys(envConfig).forEach(serverType => {
387
+ const serverList = envConfig[serverType];
388
+ if (Array.isArray(serverList)) {
389
+ serverList.forEach((server: any) => {
390
+ if (server.id) {
391
+ if (ids.includes(server.id)) {
392
+ results.push({
393
+ type: 'error',
394
+ message: `Duplicate server ID "${server.id}" in "${env}" environment.`,
395
+ file: 'config/servers.json'
396
+ });
397
+ }
398
+ ids.push(server.id);
399
+ }
400
+ });
401
+ }
402
+ });
403
+ });
404
+
405
+ return results;
406
+ }
407
+
408
+ function checkPortConflicts(cwd: string): CheckResult[] {
409
+ const results: CheckResult[] = [];
410
+ const filePath = path.join(cwd, 'config', 'servers.json');
411
+
412
+ if (!fs.existsSync(filePath)) {
413
+ return results;
414
+ }
415
+
416
+ let servers: any;
417
+ try {
418
+ servers = JSON.parse(fs.readFileSync(filePath, 'utf8'));
419
+ } catch (e) {
420
+ return results;
421
+ }
422
+
423
+ Object.keys(servers).forEach(env => {
424
+ const envConfig = servers[env];
425
+ const ports: { port: number; server: string }[] = [];
426
+
427
+ Object.keys(envConfig).forEach(serverType => {
428
+ const serverList = envConfig[serverType];
429
+ if (Array.isArray(serverList)) {
430
+ serverList.forEach((server: any) => {
431
+ if (server.port) {
432
+ const existing = ports.find(p => p.port === server.port);
433
+ if (existing) {
434
+ results.push({
435
+ type: 'error',
436
+ message: `Port conflict: "${server.id}" and "${existing.server}" both use port ${server.port} in "${env}".`,
437
+ file: 'config/servers.json'
438
+ });
439
+ }
440
+ ports.push({ port: server.port, server: server.id });
441
+ }
442
+
443
+ if (server.clientPort) {
444
+ const existingClient = ports.find(p => p.port === server.clientPort);
445
+ if (existingClient) {
446
+ results.push({
447
+ type: 'error',
448
+ message: `Client port conflict: "${server.id}" and "${existingClient.server}" both use clientPort ${server.clientPort} in "${env}".`,
449
+ file: 'config/servers.json'
450
+ });
451
+ }
452
+ ports.push({ port: server.clientPort, server: server.id });
453
+ }
454
+ });
455
+ }
456
+ });
457
+ });
458
+
459
+ return results;
460
+ }
461
+
462
+ function checkMissingHandlers(cwd: string): CheckResult[] {
463
+ const results: CheckResult[] = [];
464
+ const adminServerPath = path.join(cwd, 'config', 'adminServer.json');
465
+
466
+ if (!fs.existsSync(adminServerPath)) {
467
+ return results;
468
+ }
469
+
470
+ let adminServers: any[];
471
+ try {
472
+ adminServers = JSON.parse(fs.readFileSync(adminServerPath, 'utf8'));
473
+ } catch (e) {
474
+ return results;
475
+ }
476
+
477
+ const serversDir = path.join(cwd, 'app', 'servers');
478
+
479
+ adminServers.forEach(server => {
480
+ if (server.type === 'master') return; // master doesn't need handler
481
+
482
+ const serverPath = path.join(serversDir, server.type);
483
+ if (!fs.existsSync(serverPath)) {
484
+ results.push({
485
+ type: 'warning',
486
+ message: `Server type "${server.type}" is defined in adminServer.json but has no directory.`,
487
+ file: `app/servers/${server.type}`,
488
+ fix: `Run "melo add-server ${server.type}" to create the server directory.`
489
+ });
490
+ }
491
+ });
492
+
493
+ return results;
494
+ }
495
+
496
+ function printResults(results: CheckResult[]) {
497
+ const icons = {
498
+ error: 'āŒ',
499
+ warning: 'āš ļø ',
500
+ info: 'ā„¹ļø ',
501
+ success: 'āœ…'
502
+ };
503
+
504
+ results.forEach(result => {
505
+ console.info(`${icons[result.type]} ${result.message}`);
506
+ if (result.file) {
507
+ console.info(` šŸ“ ${result.file}`);
508
+ }
509
+ if (result.fix) {
510
+ console.info(` šŸ’” ${result.fix}`);
511
+ }
512
+ console.info('');
513
+ });
514
+ }
@@ -25,3 +25,7 @@ export let SCRIPT_NOT_FOUND = ('Fail to find an appropriate script to run,\nplea
25
25
  export let MASTER_HA_NOT_FOUND = ('Fail to find an appropriate masterha config file, \nplease check the current work directory or the arguments passed to.\n' as any).red;
26
26
  export let COMMAND_ERROR = ('Illegal command format. Use `melo --help` to get more info.\n' as any).red;
27
27
  export let DAEMON_INFO = 'The application is running in the background now.\n';
28
+
29
+ // Add server type command
30
+ export let ADD_SERVER_TYPE_INFO = 'Successfully added server type "${serverName}".\n\nGenerated files:\n - app/servers/${serverName}/handler/${serverName}Handler.ts\n - app/servers/${serverName}/remote/${serverName}Remote.ts\n - Updated config/adminServer.json\n - Updated config/servers.json\n - Updated app.ts\n\nYou can now add server instances to config/servers.json.';
31
+ export let ADD_SERVER_TYPE_EXISTS = 'Server type already exists. Use --force to overwrite or choose a different name.';