pulse-js-framework 1.7.11 → 1.7.13

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/cli/doctor.js ADDED
@@ -0,0 +1,702 @@
1
+ /**
2
+ * Pulse CLI - Doctor Command
3
+ * Project diagnostics and health checks
4
+ */
5
+
6
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
7
+ import { join, relative, resolve } from 'path';
8
+ import { execSync } from 'child_process';
9
+ import { log } from './logger.js';
10
+ import { findPulseFiles, parseArgs, formatBytes } from './utils/file-utils.js';
11
+ import { createTimer, formatDuration } from './utils/cli-ui.js';
12
+
13
+ /**
14
+ * Diagnostic check result
15
+ * @typedef {Object} CheckResult
16
+ * @property {string} name - Check name
17
+ * @property {'pass'|'warn'|'fail'|'info'} status - Check status
18
+ * @property {string} message - Result message
19
+ * @property {string} [suggestion] - Fix suggestion
20
+ */
21
+
22
+ /**
23
+ * Run all diagnostic checks
24
+ * @param {Object} options - Check options
25
+ * @returns {Promise<CheckResult[]>}
26
+ */
27
+ export async function runDiagnostics(options = {}) {
28
+ const { verbose = false, fix = false } = options;
29
+ const results = [];
30
+
31
+ // Environment checks
32
+ results.push(await checkNodeVersion());
33
+ results.push(await checkNpmVersion());
34
+
35
+ // Project structure checks
36
+ results.push(await checkPackageJson());
37
+ results.push(await checkDependencies());
38
+ results.push(await checkPulseFramework());
39
+ results.push(await checkViteConfig());
40
+ results.push(await checkProjectStructure());
41
+
42
+ // Code quality checks
43
+ results.push(await checkPulseFiles());
44
+ results.push(await checkGitStatus());
45
+
46
+ // Performance checks
47
+ if (verbose) {
48
+ results.push(await checkNodeModulesSize());
49
+ results.push(await checkBuildArtifacts());
50
+ }
51
+
52
+ return results;
53
+ }
54
+
55
+ /**
56
+ * Check Node.js version
57
+ */
58
+ async function checkNodeVersion() {
59
+ const name = 'Node.js Version';
60
+ try {
61
+ const version = process.version;
62
+ const major = parseInt(version.slice(1).split('.')[0], 10);
63
+
64
+ if (major < 18) {
65
+ return {
66
+ name,
67
+ status: 'fail',
68
+ message: `Node.js ${version} detected`,
69
+ suggestion: 'Pulse requires Node.js 18+. Please upgrade: https://nodejs.org'
70
+ };
71
+ } else if (major < 20) {
72
+ return {
73
+ name,
74
+ status: 'warn',
75
+ message: `Node.js ${version} (recommended: 20+)`,
76
+ suggestion: 'Consider upgrading to Node.js 20+ for better test coverage support'
77
+ };
78
+ }
79
+
80
+ return {
81
+ name,
82
+ status: 'pass',
83
+ message: `Node.js ${version}`
84
+ };
85
+ } catch (e) {
86
+ return {
87
+ name,
88
+ status: 'fail',
89
+ message: 'Could not detect Node.js version',
90
+ suggestion: 'Ensure Node.js is properly installed'
91
+ };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Check npm version
97
+ */
98
+ async function checkNpmVersion() {
99
+ const name = 'npm Version';
100
+ try {
101
+ const version = execSync('npm --version', { encoding: 'utf-8' }).trim();
102
+ const major = parseInt(version.split('.')[0], 10);
103
+
104
+ if (major < 8) {
105
+ return {
106
+ name,
107
+ status: 'warn',
108
+ message: `npm ${version} (recommended: 8+)`,
109
+ suggestion: 'Consider upgrading npm: npm install -g npm@latest'
110
+ };
111
+ }
112
+
113
+ return {
114
+ name,
115
+ status: 'pass',
116
+ message: `npm ${version}`
117
+ };
118
+ } catch (e) {
119
+ return {
120
+ name,
121
+ status: 'warn',
122
+ message: 'Could not detect npm version'
123
+ };
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Check package.json exists and is valid
129
+ */
130
+ async function checkPackageJson() {
131
+ const name = 'package.json';
132
+ const pkgPath = join(process.cwd(), 'package.json');
133
+
134
+ if (!existsSync(pkgPath)) {
135
+ return {
136
+ name,
137
+ status: 'fail',
138
+ message: 'package.json not found',
139
+ suggestion: 'Run "npm init" or "pulse create <name>" to create a project'
140
+ };
141
+ }
142
+
143
+ try {
144
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
145
+
146
+ const issues = [];
147
+
148
+ // Check for type: module
149
+ if (pkg.type !== 'module') {
150
+ issues.push('Missing "type": "module"');
151
+ }
152
+
153
+ // Check for required scripts
154
+ if (!pkg.scripts?.dev) {
155
+ issues.push('Missing "dev" script');
156
+ }
157
+ if (!pkg.scripts?.build) {
158
+ issues.push('Missing "build" script');
159
+ }
160
+
161
+ // Check for name
162
+ if (!pkg.name) {
163
+ issues.push('Missing "name" field');
164
+ }
165
+
166
+ if (issues.length > 0) {
167
+ return {
168
+ name,
169
+ status: 'warn',
170
+ message: `Issues found: ${issues.join(', ')}`,
171
+ suggestion: 'Update package.json to fix these issues'
172
+ };
173
+ }
174
+
175
+ return {
176
+ name,
177
+ status: 'pass',
178
+ message: `${pkg.name}@${pkg.version || '0.0.0'}`
179
+ };
180
+ } catch (e) {
181
+ return {
182
+ name,
183
+ status: 'fail',
184
+ message: 'Invalid package.json: ' + e.message,
185
+ suggestion: 'Fix the JSON syntax error in package.json'
186
+ };
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Check dependencies are installed
192
+ */
193
+ async function checkDependencies() {
194
+ const name = 'Dependencies';
195
+ const nodeModulesPath = join(process.cwd(), 'node_modules');
196
+
197
+ if (!existsSync(nodeModulesPath)) {
198
+ return {
199
+ name,
200
+ status: 'fail',
201
+ message: 'node_modules not found',
202
+ suggestion: 'Run "npm install" to install dependencies'
203
+ };
204
+ }
205
+
206
+ // Check for common issues
207
+ const pkgPath = join(process.cwd(), 'package.json');
208
+ if (!existsSync(pkgPath)) {
209
+ return {
210
+ name,
211
+ status: 'warn',
212
+ message: 'Cannot verify dependencies without package.json'
213
+ };
214
+ }
215
+
216
+ try {
217
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
218
+ const allDeps = {
219
+ ...pkg.dependencies,
220
+ ...pkg.devDependencies
221
+ };
222
+
223
+ const missing = [];
224
+ for (const dep of Object.keys(allDeps)) {
225
+ if (!existsSync(join(nodeModulesPath, dep))) {
226
+ missing.push(dep);
227
+ }
228
+ }
229
+
230
+ if (missing.length > 0) {
231
+ return {
232
+ name,
233
+ status: 'warn',
234
+ message: `Missing: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? ` (+${missing.length - 3} more)` : ''}`,
235
+ suggestion: 'Run "npm install" to install missing dependencies'
236
+ };
237
+ }
238
+
239
+ const depCount = Object.keys(allDeps).length;
240
+ return {
241
+ name,
242
+ status: 'pass',
243
+ message: `${depCount} dependencies installed`
244
+ };
245
+ } catch (e) {
246
+ return {
247
+ name,
248
+ status: 'warn',
249
+ message: 'Could not verify dependencies'
250
+ };
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Check Pulse framework is installed
256
+ */
257
+ async function checkPulseFramework() {
258
+ const name = 'Pulse Framework';
259
+ const pkgPath = join(process.cwd(), 'package.json');
260
+
261
+ if (!existsSync(pkgPath)) {
262
+ return {
263
+ name,
264
+ status: 'info',
265
+ message: 'Not a Node.js project'
266
+ };
267
+ }
268
+
269
+ try {
270
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
271
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
272
+
273
+ if (allDeps['pulse-js-framework']) {
274
+ const version = allDeps['pulse-js-framework'];
275
+ return {
276
+ name,
277
+ status: 'pass',
278
+ message: `pulse-js-framework ${version}`
279
+ };
280
+ }
281
+
282
+ // Check if this IS the framework
283
+ if (pkg.name === 'pulse-js-framework') {
284
+ return {
285
+ name,
286
+ status: 'pass',
287
+ message: `Framework source (${pkg.version})`
288
+ };
289
+ }
290
+
291
+ return {
292
+ name,
293
+ status: 'warn',
294
+ message: 'Pulse framework not found in dependencies',
295
+ suggestion: 'Run "npm install pulse-js-framework"'
296
+ };
297
+ } catch (e) {
298
+ return {
299
+ name,
300
+ status: 'warn',
301
+ message: 'Could not check framework'
302
+ };
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Check Vite configuration
308
+ */
309
+ async function checkViteConfig() {
310
+ const name = 'Vite Config';
311
+ const configFiles = [
312
+ 'vite.config.js',
313
+ 'vite.config.ts',
314
+ 'vite.config.mjs'
315
+ ];
316
+
317
+ let configPath = null;
318
+ for (const file of configFiles) {
319
+ const path = join(process.cwd(), file);
320
+ if (existsSync(path)) {
321
+ configPath = path;
322
+ break;
323
+ }
324
+ }
325
+
326
+ if (!configPath) {
327
+ return {
328
+ name,
329
+ status: 'info',
330
+ message: 'No Vite config found',
331
+ suggestion: 'Create vite.config.js for Vite integration'
332
+ };
333
+ }
334
+
335
+ try {
336
+ const content = readFileSync(configPath, 'utf-8');
337
+
338
+ // Check for Pulse plugin
339
+ if (!content.includes('pulse')) {
340
+ return {
341
+ name,
342
+ status: 'warn',
343
+ message: 'Vite config missing Pulse plugin',
344
+ suggestion: 'Add: import pulse from "pulse-js-framework/vite"'
345
+ };
346
+ }
347
+
348
+ return {
349
+ name,
350
+ status: 'pass',
351
+ message: relative(process.cwd(), configPath)
352
+ };
353
+ } catch (e) {
354
+ return {
355
+ name,
356
+ status: 'warn',
357
+ message: 'Could not read Vite config'
358
+ };
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Check project structure
364
+ */
365
+ async function checkProjectStructure() {
366
+ const name = 'Project Structure';
367
+ const issues = [];
368
+
369
+ // Check for src directory
370
+ if (!existsSync(join(process.cwd(), 'src'))) {
371
+ issues.push('No src/ directory');
372
+ }
373
+
374
+ // Check for index.html
375
+ if (!existsSync(join(process.cwd(), 'index.html'))) {
376
+ issues.push('No index.html');
377
+ }
378
+
379
+ // Check for main entry file
380
+ const mainFiles = ['src/main.js', 'src/main.ts', 'src/index.js', 'src/index.ts'];
381
+ const hasMain = mainFiles.some(f => existsSync(join(process.cwd(), f)));
382
+ if (!hasMain) {
383
+ issues.push('No main entry file in src/');
384
+ }
385
+
386
+ if (issues.length > 0) {
387
+ return {
388
+ name,
389
+ status: 'warn',
390
+ message: issues.join(', '),
391
+ suggestion: 'Run "pulse create <name>" to create a proper project structure'
392
+ };
393
+ }
394
+
395
+ return {
396
+ name,
397
+ status: 'pass',
398
+ message: 'Standard structure detected'
399
+ };
400
+ }
401
+
402
+ /**
403
+ * Check .pulse files for issues
404
+ */
405
+ async function checkPulseFiles() {
406
+ const name = 'Pulse Files';
407
+
408
+ try {
409
+ const files = findPulseFiles(['.']);
410
+
411
+ if (files.length === 0) {
412
+ return {
413
+ name,
414
+ status: 'info',
415
+ message: 'No .pulse files found'
416
+ };
417
+ }
418
+
419
+ // Quick syntax check
420
+ const { parse } = await import('../compiler/index.js');
421
+ let errors = 0;
422
+ const errorFiles = [];
423
+
424
+ for (const file of files) {
425
+ try {
426
+ const source = readFileSync(file, 'utf-8');
427
+ parse(source);
428
+ } catch (e) {
429
+ errors++;
430
+ errorFiles.push(relative(process.cwd(), file));
431
+ }
432
+ }
433
+
434
+ if (errors > 0) {
435
+ return {
436
+ name,
437
+ status: 'fail',
438
+ message: `${errors} file(s) with syntax errors`,
439
+ suggestion: `Run "pulse lint" to see details. Files: ${errorFiles.slice(0, 2).join(', ')}${errorFiles.length > 2 ? '...' : ''}`
440
+ };
441
+ }
442
+
443
+ return {
444
+ name,
445
+ status: 'pass',
446
+ message: `${files.length} file(s) OK`
447
+ };
448
+ } catch (e) {
449
+ return {
450
+ name,
451
+ status: 'warn',
452
+ message: 'Could not check .pulse files: ' + e.message
453
+ };
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Check git status
459
+ */
460
+ async function checkGitStatus() {
461
+ const name = 'Git Repository';
462
+
463
+ if (!existsSync(join(process.cwd(), '.git'))) {
464
+ return {
465
+ name,
466
+ status: 'info',
467
+ message: 'Not a git repository',
468
+ suggestion: 'Run "git init" to initialize version control'
469
+ };
470
+ }
471
+
472
+ try {
473
+ // Check for uncommitted changes
474
+ const status = execSync('git status --porcelain', { encoding: 'utf-8' });
475
+ const changes = status.trim().split('\n').filter(l => l.trim()).length;
476
+
477
+ if (changes > 0) {
478
+ return {
479
+ name,
480
+ status: 'info',
481
+ message: `${changes} uncommitted change(s)`
482
+ };
483
+ }
484
+
485
+ // Get current branch
486
+ const branch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
487
+
488
+ return {
489
+ name,
490
+ status: 'pass',
491
+ message: `On branch: ${branch}`
492
+ };
493
+ } catch (e) {
494
+ return {
495
+ name,
496
+ status: 'warn',
497
+ message: 'Could not check git status'
498
+ };
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Check node_modules size
504
+ */
505
+ async function checkNodeModulesSize() {
506
+ const name = 'node_modules Size';
507
+ const nodeModulesPath = join(process.cwd(), 'node_modules');
508
+
509
+ if (!existsSync(nodeModulesPath)) {
510
+ return {
511
+ name,
512
+ status: 'info',
513
+ message: 'node_modules not found'
514
+ };
515
+ }
516
+
517
+ try {
518
+ const size = getDirSize(nodeModulesPath);
519
+
520
+ if (size > 500 * 1024 * 1024) { // 500MB
521
+ return {
522
+ name,
523
+ status: 'warn',
524
+ message: `${formatBytes(size)} (large)`,
525
+ suggestion: 'Consider cleaning with: npm prune && npm dedupe'
526
+ };
527
+ }
528
+
529
+ return {
530
+ name,
531
+ status: 'pass',
532
+ message: formatBytes(size)
533
+ };
534
+ } catch (e) {
535
+ return {
536
+ name,
537
+ status: 'info',
538
+ message: 'Could not calculate size'
539
+ };
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Check build artifacts
545
+ */
546
+ async function checkBuildArtifacts() {
547
+ const name = 'Build Artifacts';
548
+ const distPath = join(process.cwd(), 'dist');
549
+
550
+ if (!existsSync(distPath)) {
551
+ return {
552
+ name,
553
+ status: 'info',
554
+ message: 'No dist/ directory',
555
+ suggestion: 'Run "pulse build" to create a production build'
556
+ };
557
+ }
558
+
559
+ try {
560
+ const size = getDirSize(distPath);
561
+ const files = countFiles(distPath);
562
+
563
+ return {
564
+ name,
565
+ status: 'pass',
566
+ message: `${files} file(s), ${formatBytes(size)}`
567
+ };
568
+ } catch (e) {
569
+ return {
570
+ name,
571
+ status: 'info',
572
+ message: 'Could not check build'
573
+ };
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Get directory size recursively
579
+ */
580
+ function getDirSize(dir) {
581
+ let size = 0;
582
+
583
+ try {
584
+ const entries = readdirSync(dir);
585
+ for (const entry of entries) {
586
+ const path = join(dir, entry);
587
+ try {
588
+ const stat = statSync(path);
589
+ if (stat.isDirectory()) {
590
+ size += getDirSize(path);
591
+ } else {
592
+ size += stat.size;
593
+ }
594
+ } catch (e) {
595
+ // Skip inaccessible files
596
+ }
597
+ }
598
+ } catch (e) {
599
+ // Skip inaccessible directories
600
+ }
601
+
602
+ return size;
603
+ }
604
+
605
+ /**
606
+ * Count files in directory
607
+ */
608
+ function countFiles(dir) {
609
+ let count = 0;
610
+
611
+ try {
612
+ const entries = readdirSync(dir);
613
+ for (const entry of entries) {
614
+ const path = join(dir, entry);
615
+ try {
616
+ const stat = statSync(path);
617
+ if (stat.isDirectory()) {
618
+ count += countFiles(path);
619
+ } else {
620
+ count++;
621
+ }
622
+ } catch (e) {
623
+ // Skip
624
+ }
625
+ }
626
+ } catch (e) {
627
+ // Skip
628
+ }
629
+
630
+ return count;
631
+ }
632
+
633
+ /**
634
+ * Format check result for display
635
+ */
636
+ function formatCheckResult(result) {
637
+ const icons = {
638
+ pass: '\x1b[32m✓\x1b[0m', // Green checkmark
639
+ warn: '\x1b[33m!\x1b[0m', // Yellow exclamation
640
+ fail: '\x1b[31m✗\x1b[0m', // Red X
641
+ info: '\x1b[34mi\x1b[0m' // Blue i
642
+ };
643
+
644
+ const icon = icons[result.status] || '?';
645
+ const name = result.name.padEnd(20);
646
+ const message = result.message;
647
+
648
+ return ` ${icon} ${name} ${message}`;
649
+ }
650
+
651
+ /**
652
+ * Main doctor command handler
653
+ */
654
+ export async function runDoctor(args) {
655
+ const { options } = parseArgs(args);
656
+ const verbose = options.verbose || options.v || false;
657
+ const json = options.json || false;
658
+
659
+ const timer = createTimer();
660
+
661
+ log.info('Pulse Doctor - Project Diagnostics\n');
662
+ log.info('Running checks...\n');
663
+
664
+ const results = await runDiagnostics({ verbose });
665
+
666
+ if (json) {
667
+ console.log(JSON.stringify(results, null, 2));
668
+ return;
669
+ }
670
+
671
+ // Display results
672
+ for (const result of results) {
673
+ log.info(formatCheckResult(result));
674
+ if (result.suggestion && (result.status === 'warn' || result.status === 'fail')) {
675
+ log.info(` \x1b[90m${result.suggestion}\x1b[0m`);
676
+ }
677
+ }
678
+
679
+ // Summary
680
+ const elapsed = formatDuration(timer.elapsed());
681
+ const passed = results.filter(r => r.status === 'pass').length;
682
+ const warnings = results.filter(r => r.status === 'warn').length;
683
+ const failures = results.filter(r => r.status === 'fail').length;
684
+
685
+ log.info('\n' + '─'.repeat(50));
686
+
687
+ if (failures > 0) {
688
+ log.error(`${failures} issue(s) require attention`);
689
+ }
690
+ if (warnings > 0) {
691
+ log.warn(`${warnings} warning(s)`);
692
+ }
693
+ if (failures === 0 && warnings === 0) {
694
+ log.success(`All ${passed} checks passed`);
695
+ }
696
+
697
+ log.info(`\nCompleted in ${elapsed}`);
698
+
699
+ if (failures > 0) {
700
+ process.exit(1);
701
+ }
702
+ }