pulse-js-framework 1.7.4 → 1.7.5

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/analyze.js CHANGED
@@ -7,6 +7,7 @@ import { readFileSync, statSync, existsSync } from 'fs';
7
7
  import { join, dirname, basename, relative } from 'path';
8
8
  import { findPulseFiles, parseArgs, formatBytes, relativePath, resolveImportPath } from './utils/file-utils.js';
9
9
  import { log } from './logger.js';
10
+ import { createTimer, formatDuration, createSpinner, createBarChart, createTree, createTable, printSection } from './utils/cli-ui.js';
10
11
 
11
12
  /**
12
13
  * Analyze the bundle/project
@@ -384,74 +385,111 @@ function formatConsoleOutput(analysis, verbose = false) {
384
385
  lines.push(' PULSE BUNDLE ANALYSIS');
385
386
  lines.push('═'.repeat(60));
386
387
 
387
- // Summary
388
+ // Summary table
388
389
  lines.push('');
389
- lines.push(' SUMMARY');
390
- lines.push(' ' + ''.repeat(40));
391
- lines.push(` Total files: ${analysis.summary.totalFiles}`);
392
- lines.push(` .pulse files: ${analysis.summary.pulseFiles}`);
393
- lines.push(` .js files: ${analysis.summary.jsFiles}`);
394
- lines.push(` Total size: ${analysis.summary.totalSizeFormatted}`);
395
-
396
- // Complexity (top 5)
390
+ lines.push(createTable(
391
+ ['Metric', 'Value'],
392
+ [
393
+ ['Total files', String(analysis.summary.totalFiles)],
394
+ ['.pulse files', String(analysis.summary.pulseFiles)],
395
+ ['.js files', String(analysis.summary.jsFiles)],
396
+ ['Total size', analysis.summary.totalSizeFormatted]
397
+ ],
398
+ { align: ['left', 'right'] }
399
+ ));
400
+
401
+ // Complexity bar chart (top 5)
397
402
  if (analysis.complexity.length > 0) {
398
- lines.push('');
399
- lines.push(' COMPLEXITY (Top 5)');
400
- lines.push(' ' + '─'.repeat(40));
403
+ printSection('COMPONENT COMPLEXITY');
401
404
  const top5 = analysis.complexity.slice(0, 5);
402
- for (const comp of top5) {
403
- const bar = '█'.repeat(Math.min(comp.complexity, 20));
404
- lines.push(` ${comp.componentName.padEnd(20)} ${String(comp.complexity).padStart(3)} ${bar}`);
405
+ const chartData = top5.map(comp => ({
406
+ label: comp.componentName.slice(0, 15).padEnd(15),
407
+ value: comp.complexity,
408
+ color: comp.complexity > 20 ? 'red' : comp.complexity > 10 ? 'yellow' : 'green'
409
+ }));
410
+ lines.push(createBarChart(chartData, { maxWidth: 30, showValues: true }));
411
+
412
+ // Detailed metrics table for verbose mode
413
+ if (verbose) {
414
+ lines.push('');
415
+ lines.push(createTable(
416
+ ['Component', 'State', 'Actions', 'Depth', 'Directives', 'Score'],
417
+ top5.map(c => [
418
+ c.componentName,
419
+ String(c.stateCount),
420
+ String(c.actionCount),
421
+ String(c.viewDepth),
422
+ String(c.directiveCount),
423
+ String(c.complexity)
424
+ ]),
425
+ { align: ['left', 'right', 'right', 'right', 'right', 'right'] }
426
+ ));
405
427
  }
406
428
  }
407
429
 
408
- // Dead code
430
+ // Dead code warnings
409
431
  if (analysis.deadCode.length > 0) {
410
- lines.push('');
411
- lines.push(' DEAD CODE');
412
- lines.push(' ' + '─'.repeat(40));
432
+ printSection('DEAD CODE DETECTED');
413
433
  for (const dead of analysis.deadCode) {
414
434
  lines.push(` ⚠ ${dead.file}`);
415
- lines.push(` ${dead.message}`);
435
+ lines.push(` └─ ${dead.message}`);
416
436
  }
417
437
  }
418
438
 
419
- // File breakdown (verbose)
439
+ // File size breakdown with bar chart (verbose)
420
440
  if (verbose && analysis.fileBreakdown.length > 0) {
421
- lines.push('');
422
- lines.push(' FILE BREAKDOWN');
423
- lines.push(' ' + '─'.repeat(40));
424
- for (const file of analysis.fileBreakdown) {
425
- const size = file.sizeFormatted.padStart(10);
426
- lines.push(` ${size} ${file.path}`);
427
- }
441
+ printSection('FILE SIZE BREAKDOWN');
442
+ const top10Files = analysis.fileBreakdown.slice(0, 10);
443
+ const sizeChartData = top10Files.map(file => ({
444
+ label: basename(file.path).slice(0, 20).padEnd(20),
445
+ value: file.size,
446
+ color: file.type === 'pulse' ? 'cyan' : 'blue'
447
+ }));
448
+ lines.push(createBarChart(sizeChartData, { maxWidth: 25, showValues: true, unit: 'B' }));
428
449
  }
429
450
 
430
- // Import graph (verbose)
451
+ // Import graph as tree (verbose)
431
452
  if (verbose && analysis.importGraph.edges.length > 0) {
432
- lines.push('');
433
- lines.push(' IMPORT GRAPH');
434
- lines.push(' ' + '─'.repeat(40));
435
- for (const edge of analysis.importGraph.edges.slice(0, 20)) {
436
- lines.push(` ${edge.from} → ${edge.to}`);
437
- }
438
- if (analysis.importGraph.edges.length > 20) {
439
- lines.push(` ... and ${analysis.importGraph.edges.length - 20} more`);
453
+ printSection('IMPORT DEPENDENCY TREE');
454
+
455
+ // Build tree structure from edges
456
+ const tree = buildDependencyTree(analysis.importGraph);
457
+ if (tree) {
458
+ lines.push(createTree(tree));
440
459
  }
460
+
461
+ // Show edge count summary
462
+ lines.push(` Total dependencies: ${analysis.importGraph.edges.length}`);
441
463
  }
442
464
 
443
465
  // State usage (verbose)
444
466
  if (verbose && analysis.stateUsage.length > 0) {
445
- lines.push('');
446
- lines.push(' STATE VARIABLES');
447
- lines.push(' ' + '─'.repeat(40));
448
- for (const state of analysis.stateUsage.slice(0, 10)) {
449
- const shared = state.isShared ? ' (shared)' : '';
467
+ printSection('STATE VARIABLES');
468
+
469
+ // Show shared state as a table
470
+ const sharedState = analysis.stateUsage.filter(s => s.isShared);
471
+ if (sharedState.length > 0) {
472
+ lines.push(' Shared state (used in multiple files):');
473
+ lines.push(createTable(
474
+ ['Variable', 'Files'],
475
+ sharedState.slice(0, 5).map(s => [s.name, String(s.files.length)]),
476
+ { align: ['left', 'right'] }
477
+ ));
478
+ }
479
+
480
+ // State tree visualization
481
+ for (const state of analysis.stateUsage.slice(0, 5)) {
482
+ const shared = state.isShared ? ' \x1b[33m(shared)\x1b[0m' : '';
450
483
  lines.push(` ${state.name}${shared}`);
451
- for (const file of state.files) {
452
- lines.push(` └─ ${file}`);
484
+ for (let i = 0; i < state.files.length; i++) {
485
+ const isLast = i === state.files.length - 1;
486
+ const prefix = isLast ? '└── ' : '├── ';
487
+ lines.push(` ${prefix}${state.files[i]}`);
453
488
  }
454
489
  }
490
+ if (analysis.stateUsage.length > 5) {
491
+ lines.push(` ... and ${analysis.stateUsage.length - 5} more state variables`);
492
+ }
455
493
  }
456
494
 
457
495
  lines.push('');
@@ -460,6 +498,44 @@ function formatConsoleOutput(analysis, verbose = false) {
460
498
  return lines.join('\n');
461
499
  }
462
500
 
501
+ /**
502
+ * Build a dependency tree structure for visualization
503
+ */
504
+ function buildDependencyTree(importGraph) {
505
+ if (!importGraph.edges.length) return null;
506
+
507
+ // Find entry points (files that aren't imported by others)
508
+ const imported = new Set(importGraph.edges.map(e => e.to));
509
+ const entryPoints = importGraph.nodes.filter(n =>
510
+ !imported.has(n) && (n.includes('main') || n.includes('index') || n.includes('App'))
511
+ );
512
+
513
+ if (entryPoints.length === 0 && importGraph.nodes.length > 0) {
514
+ entryPoints.push(importGraph.nodes[0]);
515
+ }
516
+
517
+ // Build tree recursively (limit depth to avoid infinite loops)
518
+ function buildNode(name, visited = new Set(), depth = 0) {
519
+ if (depth > 5 || visited.has(name)) {
520
+ return { name: basename(name) + (visited.has(name) ? ' (circular)' : ' ...'), children: [] };
521
+ }
522
+
523
+ visited.add(name);
524
+ const children = importGraph.edges
525
+ .filter(e => e.from === name)
526
+ .slice(0, 5) // Limit children
527
+ .map(e => buildNode(e.to, new Set(visited), depth + 1));
528
+
529
+ return {
530
+ name: basename(name),
531
+ children
532
+ };
533
+ }
534
+
535
+ // Return first entry point's tree
536
+ return buildNode(entryPoints[0]);
537
+ }
538
+
463
539
  /**
464
540
  * Main analyze command handler
465
541
  */
@@ -478,10 +554,14 @@ export async function runAnalyze(args) {
478
554
  process.exit(1);
479
555
  }
480
556
 
481
- log.info('Analyzing bundle...\n');
557
+ const timer = createTimer();
558
+ const spinner = createSpinner('Analyzing bundle...');
482
559
 
483
560
  try {
484
561
  const analysis = await analyzeBundle(root);
562
+ const elapsed = timer.elapsed();
563
+
564
+ spinner.success(`Analysis complete (${formatDuration(elapsed)})`);
485
565
 
486
566
  if (json) {
487
567
  log.info(JSON.stringify(analysis, null, 2));
@@ -494,7 +574,8 @@ export async function runAnalyze(args) {
494
574
  log.warn(`\nWarning: ${analysis.deadCode.length} potentially unused file(s) found.`);
495
575
  }
496
576
  } catch (error) {
497
- log.error('Analysis failed:', error.message);
577
+ spinner.fail('Analysis failed');
578
+ log.error(error.message);
498
579
  process.exit(1);
499
580
  }
500
581
  }
package/cli/build.js CHANGED
@@ -8,6 +8,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSy
8
8
  import { join, extname, relative, dirname } from 'path';
9
9
  import { compile } from '../compiler/index.js';
10
10
  import { log } from './logger.js';
11
+ import { createTimer, createProgressBar, formatDuration, createSpinner } from './utils/cli-ui.js';
11
12
 
12
13
  /**
13
14
  * Build project for production
@@ -15,21 +16,23 @@ import { log } from './logger.js';
15
16
  export async function buildProject(args) {
16
17
  const root = process.cwd();
17
18
  const outDir = join(root, 'dist');
19
+ const timer = createTimer();
18
20
 
19
21
  // Check if vite is available
20
22
  try {
21
23
  const viteConfig = join(root, 'vite.config.js');
22
24
  if (existsSync(viteConfig)) {
23
- log.info('Vite config detected, using Vite build...');
25
+ const spinner = createSpinner('Building with Vite...');
24
26
  const { build } = await import('vite');
25
27
  await build({ root });
28
+ spinner.success(`Built with Vite in ${timer.format()}`);
26
29
  return;
27
30
  }
28
31
  } catch (e) {
29
32
  // Vite not available, use built-in build
30
33
  }
31
34
 
32
- log.info('Building with Pulse compiler...');
35
+ log.info('Building with Pulse compiler...\n');
33
36
 
34
37
  // Create output directory
35
38
  if (!existsSync(outDir)) {
@@ -42,10 +45,19 @@ export async function buildProject(args) {
42
45
  copyDir(publicDir, outDir);
43
46
  }
44
47
 
45
- // Process source files
48
+ // Count files for progress bar
46
49
  const srcDir = join(root, 'src');
47
- if (existsSync(srcDir)) {
48
- processDirectory(srcDir, join(outDir, 'assets'));
50
+ const fileCount = existsSync(srcDir) ? countFiles(srcDir) : 0;
51
+
52
+ // Process source files with progress bar
53
+ if (existsSync(srcDir) && fileCount > 0) {
54
+ const progress = createProgressBar({
55
+ total: fileCount,
56
+ label: 'Compiling',
57
+ width: 25
58
+ });
59
+ processDirectory(srcDir, join(outDir, 'assets'), progress);
60
+ progress.done();
49
61
  }
50
62
 
51
63
  // Copy and process index.html
@@ -68,20 +80,45 @@ export async function buildProject(args) {
68
80
  // Bundle runtime
69
81
  bundleRuntime(outDir);
70
82
 
83
+ const elapsed = timer.elapsed();
71
84
  log.success(`
72
- Build complete!
85
+ Build complete in ${formatDuration(elapsed)}
73
86
 
74
- Output directory: ${relative(root, outDir)}
87
+ Output: ${relative(root, outDir)}/
88
+ Files: ${fileCount} processed
75
89
 
76
- To preview the build:
77
- npx serve dist
90
+ To preview:
91
+ pulse preview
78
92
  `);
79
93
  }
80
94
 
95
+ /**
96
+ * Count files in a directory recursively
97
+ */
98
+ function countFiles(dir) {
99
+ let count = 0;
100
+ const files = readdirSync(dir);
101
+
102
+ for (const file of files) {
103
+ const fullPath = join(dir, file);
104
+ const stat = statSync(fullPath);
105
+ if (stat.isDirectory()) {
106
+ count += countFiles(fullPath);
107
+ } else {
108
+ count++;
109
+ }
110
+ }
111
+
112
+ return count;
113
+ }
114
+
81
115
  /**
82
116
  * Process a directory of source files
117
+ * @param {string} srcDir - Source directory
118
+ * @param {string} outDir - Output directory
119
+ * @param {Object} [progress] - Progress bar instance
83
120
  */
84
- function processDirectory(srcDir, outDir) {
121
+ function processDirectory(srcDir, outDir, progress = null) {
85
122
  if (!existsSync(outDir)) {
86
123
  mkdirSync(outDir, { recursive: true });
87
124
  }
@@ -93,7 +130,7 @@ function processDirectory(srcDir, outDir) {
93
130
  const stat = statSync(srcPath);
94
131
 
95
132
  if (stat.isDirectory()) {
96
- processDirectory(srcPath, join(outDir, file));
133
+ processDirectory(srcPath, join(outDir, file), progress);
97
134
  } else if (file.endsWith('.pulse')) {
98
135
  // Compile .pulse files
99
136
  const source = readFileSync(srcPath, 'utf-8');
@@ -105,13 +142,13 @@ function processDirectory(srcDir, outDir) {
105
142
  if (result.success) {
106
143
  const outPath = join(outDir, file.replace('.pulse', '.js'));
107
144
  writeFileSync(outPath, result.code);
108
- log.info(` Compiled: ${file}`);
109
145
  } else {
110
146
  log.error(` Error compiling ${file}:`);
111
147
  for (const error of result.errors) {
112
148
  log.error(` ${error.message}`);
113
149
  }
114
150
  }
151
+ if (progress) progress.tick();
115
152
  } else if (file.endsWith('.js') || file.endsWith('.mjs')) {
116
153
  // Process JS files - rewrite imports
117
154
  let content = readFileSync(srcPath, 'utf-8');
@@ -130,11 +167,12 @@ function processDirectory(srcDir, outDir) {
130
167
 
131
168
  const outPath = join(outDir, file);
132
169
  writeFileSync(outPath, content);
133
- log.info(` Processed & minified: ${file}`);
170
+ if (progress) progress.tick();
134
171
  } else {
135
172
  // Copy other files
136
173
  const outPath = join(outDir, file);
137
174
  copyFileSync(srcPath, outPath);
175
+ if (progress) progress.tick();
138
176
  }
139
177
  }
140
178
  }
package/cli/format.js CHANGED
@@ -3,9 +3,11 @@
3
3
  * Formats .pulse files consistently
4
4
  */
5
5
 
6
- import { readFileSync, writeFileSync } from 'fs';
6
+ import { readFileSync, writeFileSync, watch } from 'fs';
7
+ import { dirname } from 'path';
7
8
  import { findPulseFiles, parseArgs, relativePath } from './utils/file-utils.js';
8
9
  import { log } from './logger.js';
10
+ import { createTimer, formatDuration } from './utils/cli-ui.js';
9
11
 
10
12
  /**
11
13
  * Default format options
@@ -641,6 +643,7 @@ export async function runFormat(args) {
641
643
  const { options, patterns } = parseArgs(args);
642
644
  const check = options.check || false;
643
645
  const write = !check; // Default to write unless --check is specified
646
+ const watchMode = options.watch || options.w || false;
644
647
 
645
648
  // Find files to format
646
649
  const files = findPulseFiles(patterns);
@@ -650,7 +653,57 @@ export async function runFormat(args) {
650
653
  return;
651
654
  }
652
655
 
653
- log.info(`${check ? 'Checking' : 'Formatting'} ${files.length} file(s)...\n`);
656
+ // Run initial format
657
+ const result = await runFormatOnFiles(files, { check, write, options });
658
+
659
+ // If watch mode, set up file watchers
660
+ if (watchMode) {
661
+ if (check) {
662
+ log.warn('--watch mode is not available with --check');
663
+ return;
664
+ }
665
+
666
+ log.info('\nWatching for changes... (Ctrl+C to stop)\n');
667
+
668
+ // Get unique directories to watch
669
+ const watchedDirs = new Set(files.map(f => dirname(f)));
670
+
671
+ // Debounce timer
672
+ let debounceTimer = null;
673
+ const debounceDelay = 100;
674
+
675
+ for (const dir of watchedDirs) {
676
+ watch(dir, { recursive: false }, (_eventType, filename) => {
677
+ if (!filename || !filename.endsWith('.pulse')) return;
678
+
679
+ // Debounce rapid changes
680
+ if (debounceTimer) clearTimeout(debounceTimer);
681
+ debounceTimer = setTimeout(() => {
682
+ const changedFiles = findPulseFiles(patterns);
683
+ runFormatOnFiles(changedFiles, { check: false, write: true, options, isRerun: true })
684
+ .then(() => {
685
+ log.info('Watching for changes...\n');
686
+ });
687
+ }, debounceDelay);
688
+ });
689
+ }
690
+
691
+ // Keep process alive
692
+ process.stdin.resume();
693
+ } else if (check && result.changedCount > 0) {
694
+ process.exit(1);
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Run format on a list of files
700
+ */
701
+ async function runFormatOnFiles(files, { check, write, options, isRerun = false }) {
702
+ const timer = createTimer();
703
+
704
+ if (!isRerun) {
705
+ log.info(`${check ? 'Checking' : 'Formatting'} ${files.length} file(s)...\n`);
706
+ }
654
707
 
655
708
  let changedCount = 0;
656
709
  let errorCount = 0;
@@ -675,12 +728,14 @@ export async function runFormat(args) {
675
728
  log.info(` ${relPath} - formatted`);
676
729
  }
677
730
  } else {
678
- if (!check) {
731
+ if (!check && !isRerun) {
679
732
  log.info(` ${relPath} - unchanged`);
680
733
  }
681
734
  }
682
735
  }
683
736
 
737
+ const elapsed = timer.elapsed();
738
+
684
739
  // Summary
685
740
  log.info('\n' + '─'.repeat(60));
686
741
 
@@ -690,16 +745,17 @@ export async function runFormat(args) {
690
745
 
691
746
  if (check) {
692
747
  if (changedCount > 0) {
693
- log.error(`✗ ${changedCount} file(s) need formatting`);
694
- process.exit(1);
748
+ log.error(`✗ ${changedCount} file(s) need formatting (${formatDuration(elapsed)})`);
695
749
  } else {
696
- log.success(`✓ All ${files.length} file(s) are properly formatted`);
750
+ log.success(`✓ All ${files.length} file(s) are properly formatted (${formatDuration(elapsed)})`);
697
751
  }
698
752
  } else {
699
753
  if (changedCount > 0) {
700
- log.success(`✓ ${changedCount} file(s) formatted`);
754
+ log.success(`✓ ${changedCount} file(s) formatted (${formatDuration(elapsed)})`);
701
755
  } else {
702
- log.success(`✓ All ${files.length} file(s) were already formatted`);
756
+ log.success(`✓ All ${files.length} file(s) were already formatted (${formatDuration(elapsed)})`);
703
757
  }
704
758
  }
759
+
760
+ return { changedCount, errorCount };
705
761
  }
package/cli/lint.js CHANGED
@@ -3,9 +3,11 @@
3
3
  * Validates .pulse files for errors and style issues
4
4
  */
5
5
 
6
- import { readFileSync, writeFileSync } from 'fs';
6
+ import { readFileSync, writeFileSync, watch } from 'fs';
7
+ import { dirname } from 'path';
7
8
  import { findPulseFiles, parseArgs, relativePath } from './utils/file-utils.js';
8
9
  import { log } from './logger.js';
10
+ import { createTimer, formatDuration } from './utils/cli-ui.js';
9
11
 
10
12
  /**
11
13
  * Lint rules configuration
@@ -701,21 +703,14 @@ export async function lintFile(filePath, options = {}) {
701
703
  }
702
704
 
703
705
  /**
704
- * Main lint command handler
706
+ * Lint files and return summary
707
+ * @param {string[]} files - Files to lint
708
+ * @param {Object} options - Lint options
709
+ * @returns {Object} Summary with totals
705
710
  */
706
- export async function runLint(args) {
707
- const { options, patterns } = parseArgs(args);
708
- const fix = options.fix || false;
709
-
710
- // Find files to lint
711
- const files = findPulseFiles(patterns);
712
-
713
- if (files.length === 0) {
714
- log.info('No .pulse files found to lint.');
715
- return;
716
- }
717
-
718
- log.info(`Linting ${files.length} file(s)...\n`);
711
+ async function lintFiles(files, options = {}) {
712
+ const { fix = false, quiet = false } = options;
713
+ const timer = createTimer();
719
714
 
720
715
  let totalErrors = 0;
721
716
  let totalWarnings = 0;
@@ -725,7 +720,7 @@ export async function runLint(args) {
725
720
  const result = await lintFile(file, { fix });
726
721
  const relPath = relativePath(file);
727
722
 
728
- if (result.diagnostics.length > 0) {
723
+ if (result.diagnostics.length > 0 && !quiet) {
729
724
  log.info(`\n${relPath}`);
730
725
 
731
726
  for (const diag of result.diagnostics) {
@@ -740,21 +735,111 @@ export async function runLint(args) {
740
735
  }
741
736
  }
742
737
 
743
- // Summary
738
+ return {
739
+ errors: totalErrors,
740
+ warnings: totalWarnings,
741
+ info: totalInfo,
742
+ elapsed: timer.elapsed()
743
+ };
744
+ }
745
+
746
+ /**
747
+ * Main lint command handler
748
+ */
749
+ export async function runLint(args) {
750
+ const { options, patterns } = parseArgs(args);
751
+ const fix = options.fix || false;
752
+ const watchMode = options.watch || options.w || false;
753
+
754
+ // Find files to lint
755
+ const files = findPulseFiles(patterns);
756
+
757
+ if (files.length === 0) {
758
+ log.info('No .pulse files found to lint.');
759
+ return;
760
+ }
761
+
762
+ // Initial lint run
763
+ log.info(`Linting ${files.length} file(s)...\n`);
764
+ const summary = await lintFiles(files, { fix });
765
+
766
+ // Print summary
767
+ printLintSummary(summary, files.length);
768
+
769
+ // Watch mode
770
+ if (watchMode) {
771
+ log.info('\nWatching for changes... (Ctrl+C to stop)\n');
772
+
773
+ const watchedDirs = new Set();
774
+ const debounceTimers = new Map();
775
+
776
+ // Collect directories to watch
777
+ for (const file of files) {
778
+ watchedDirs.add(dirname(file));
779
+ }
780
+
781
+ // Watch each directory
782
+ for (const dir of watchedDirs) {
783
+ watch(dir, { recursive: false }, (_eventType, filename) => {
784
+ if (!filename || !filename.endsWith('.pulse')) return;
785
+
786
+ const filePath = files.find(f => f.endsWith(filename));
787
+ if (!filePath) return;
788
+
789
+ // Debounce rapid changes
790
+ if (debounceTimers.has(filePath)) {
791
+ clearTimeout(debounceTimers.get(filePath));
792
+ }
793
+
794
+ debounceTimers.set(filePath, setTimeout(() => {
795
+ debounceTimers.delete(filePath);
796
+
797
+ log.info(`\n[${new Date().toLocaleTimeString()}] File changed: ${relativePath(filePath)}`);
798
+ lintFiles([filePath], { fix }).then(result => {
799
+ printLintSummary(result, 1, true);
800
+ });
801
+ }, 100));
802
+ });
803
+ }
804
+
805
+ // Keep process running
806
+ return new Promise(() => {});
807
+ } else {
808
+ // Exit with error code if errors found
809
+ if (summary.errors > 0) {
810
+ process.exit(1);
811
+ }
812
+ }
813
+ }
814
+
815
+ /**
816
+ * Print lint summary
817
+ */
818
+ function printLintSummary(summary, fileCount, compact = false) {
819
+ const { errors, warnings, info, elapsed } = summary;
820
+ const timeStr = formatDuration(elapsed);
821
+
822
+ if (compact) {
823
+ const parts = [];
824
+ if (errors > 0) parts.push(`${errors} error(s)`);
825
+ if (warnings > 0) parts.push(`${warnings} warning(s)`);
826
+ if (parts.length === 0) {
827
+ log.success(`✓ Passed (${timeStr})`);
828
+ } else {
829
+ log.error(`✗ ${parts.join(', ')} (${timeStr})`);
830
+ }
831
+ return;
832
+ }
833
+
744
834
  log.info('\n' + '─'.repeat(60));
745
835
  const parts = [];
746
- if (totalErrors > 0) parts.push(`${totalErrors} error(s)`);
747
- if (totalWarnings > 0) parts.push(`${totalWarnings} warning(s)`);
748
- if (totalInfo > 0) parts.push(`${totalInfo} info`);
836
+ if (errors > 0) parts.push(`${errors} error(s)`);
837
+ if (warnings > 0) parts.push(`${warnings} warning(s)`);
838
+ if (info > 0) parts.push(`${info} info`);
749
839
 
750
840
  if (parts.length === 0) {
751
- log.success(`✓ ${files.length} file(s) passed`);
841
+ log.success(`✓ ${fileCount} file(s) passed (${timeStr})`);
752
842
  } else {
753
- log.error(`✗ ${parts.join(', ')} in ${files.length} file(s)`);
754
- }
755
-
756
- // Exit with error code if errors found
757
- if (totalErrors > 0) {
758
- process.exit(1);
843
+ log.error(`✗ ${parts.join(', ')} in ${fileCount} file(s) (${timeStr})`);
759
844
  }
760
845
  }