pulse-js-framework 1.7.4 → 1.7.6

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/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,17 @@ 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
+ * @param {boolean} options.fix - Auto-fix issues
710
+ * @param {boolean} options.dryRun - Show fixes without applying
711
+ * @param {boolean} options.quiet - Suppress output
712
+ * @returns {Object} Summary with totals
705
713
  */
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`);
714
+ async function lintFiles(files, options = {}) {
715
+ const { fix = false, dryRun = false, quiet = false } = options;
716
+ const timer = createTimer();
719
717
 
720
718
  let totalErrors = 0;
721
719
  let totalWarnings = 0;
@@ -725,7 +723,7 @@ export async function runLint(args) {
725
723
  const result = await lintFile(file, { fix });
726
724
  const relPath = relativePath(file);
727
725
 
728
- if (result.diagnostics.length > 0) {
726
+ if (result.diagnostics.length > 0 && !quiet) {
729
727
  log.info(`\n${relPath}`);
730
728
 
731
729
  for (const diag of result.diagnostics) {
@@ -740,21 +738,117 @@ export async function runLint(args) {
740
738
  }
741
739
  }
742
740
 
743
- // Summary
741
+ return {
742
+ errors: totalErrors,
743
+ warnings: totalWarnings,
744
+ info: totalInfo,
745
+ elapsed: timer.elapsed()
746
+ };
747
+ }
748
+
749
+ /**
750
+ * Main lint command handler
751
+ */
752
+ export async function runLint(args) {
753
+ const { options, patterns } = parseArgs(args);
754
+ const fix = options.fix || false;
755
+ const dryRun = options['dry-run'] || false;
756
+ const watchMode = options.watch || options.w || false;
757
+
758
+ // Dry-run only makes sense with --fix
759
+ if (dryRun && !fix) {
760
+ log.warn('Note: --dry-run has no effect without --fix');
761
+ }
762
+
763
+ // Find files to lint
764
+ const files = findPulseFiles(patterns);
765
+
766
+ if (files.length === 0) {
767
+ log.info('No .pulse files found to lint.');
768
+ return;
769
+ }
770
+
771
+ // Initial lint run
772
+ log.info(`Linting ${files.length} file(s)...${dryRun ? ' (dry-run)' : ''}\n`);
773
+ const summary = await lintFiles(files, { fix, dryRun });
774
+
775
+ // Print summary
776
+ printLintSummary(summary, files.length);
777
+
778
+ // Watch mode
779
+ if (watchMode) {
780
+ log.info('\nWatching for changes... (Ctrl+C to stop)\n');
781
+
782
+ const watchedDirs = new Set();
783
+ const debounceTimers = new Map();
784
+
785
+ // Collect directories to watch
786
+ for (const file of files) {
787
+ watchedDirs.add(dirname(file));
788
+ }
789
+
790
+ // Watch each directory
791
+ for (const dir of watchedDirs) {
792
+ watch(dir, { recursive: false }, (_eventType, filename) => {
793
+ if (!filename || !filename.endsWith('.pulse')) return;
794
+
795
+ const filePath = files.find(f => f.endsWith(filename));
796
+ if (!filePath) return;
797
+
798
+ // Debounce rapid changes
799
+ if (debounceTimers.has(filePath)) {
800
+ clearTimeout(debounceTimers.get(filePath));
801
+ }
802
+
803
+ debounceTimers.set(filePath, setTimeout(() => {
804
+ debounceTimers.delete(filePath);
805
+
806
+ log.info(`\n[${new Date().toLocaleTimeString()}] File changed: ${relativePath(filePath)}`);
807
+ lintFiles([filePath], { fix, dryRun }).then(result => {
808
+ printLintSummary(result, 1, true);
809
+ });
810
+ }, 100));
811
+ });
812
+ }
813
+
814
+ // Keep process running
815
+ return new Promise(() => {});
816
+ } else {
817
+ // Exit with error code if errors found
818
+ if (summary.errors > 0) {
819
+ process.exit(1);
820
+ }
821
+ }
822
+ }
823
+
824
+ /**
825
+ * Print lint summary
826
+ */
827
+ function printLintSummary(summary, fileCount, compact = false) {
828
+ const { errors, warnings, info, elapsed } = summary;
829
+ const timeStr = formatDuration(elapsed);
830
+
831
+ if (compact) {
832
+ const parts = [];
833
+ if (errors > 0) parts.push(`${errors} error(s)`);
834
+ if (warnings > 0) parts.push(`${warnings} warning(s)`);
835
+ if (parts.length === 0) {
836
+ log.success(`✓ Passed (${timeStr})`);
837
+ } else {
838
+ log.error(`✗ ${parts.join(', ')} (${timeStr})`);
839
+ }
840
+ return;
841
+ }
842
+
744
843
  log.info('\n' + '─'.repeat(60));
745
844
  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`);
845
+ if (errors > 0) parts.push(`${errors} error(s)`);
846
+ if (warnings > 0) parts.push(`${warnings} warning(s)`);
847
+ if (info > 0) parts.push(`${info} info`);
749
848
 
750
849
  if (parts.length === 0) {
751
- log.success(`✓ ${files.length} file(s) passed`);
850
+ log.success(`✓ ${fileCount} file(s) passed (${timeStr})`);
752
851
  } 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);
852
+ log.error(`✗ ${parts.join(', ')} in ${fileCount} file(s) (${timeStr})`);
759
853
  }
760
854
  }
package/cli/logger.js CHANGED
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * Pulse CLI Logger
3
- * Lightweight logger for CLI tools with support for verbose mode
3
+ * Adapter for CLI tools using the unified runtime logger
4
4
  * @module pulse-cli/logger
5
5
  */
6
6
 
7
+ import { createLogger, setLogLevel, LogLevel } from '../runtime/logger.js';
8
+
9
+ // Create CLI-namespaced logger
10
+ const cliLogger = createLogger('CLI');
11
+
7
12
  /** @type {boolean} */
8
13
  let verboseMode = false;
9
14
 
@@ -17,6 +22,12 @@ let verboseMode = false;
17
22
  */
18
23
  export function setVerbose(enabled) {
19
24
  verboseMode = enabled;
25
+ // Update global log level to include debug when verbose
26
+ if (enabled) {
27
+ setLogLevel(LogLevel.DEBUG);
28
+ } else {
29
+ setLogLevel(LogLevel.INFO);
30
+ }
20
31
  }
21
32
 
22
33
  /**
@@ -33,6 +44,7 @@ export function isVerbose() {
33
44
 
34
45
  /**
35
46
  * CLI Logger object with console-like API
47
+ * Uses the runtime logger under the hood for consistency
36
48
  * @namespace log
37
49
  */
38
50
  export const log = {
@@ -44,6 +56,7 @@ export const log = {
44
56
  * log.info('Starting server on port', 3000);
45
57
  */
46
58
  info(...args) {
59
+ // Use console directly for CLI output (no namespace prefix for info)
47
60
  console.log(...args);
48
61
  },
49
62
 
@@ -66,7 +79,7 @@ export const log = {
66
79
  * log.warn('Deprecated feature used');
67
80
  */
68
81
  warn(...args) {
69
- console.warn(...args);
82
+ cliLogger.warn(...args);
70
83
  },
71
84
 
72
85
  /**
@@ -77,7 +90,7 @@ export const log = {
77
90
  * log.error('Failed to compile:', error.message);
78
91
  */
79
92
  error(...args) {
80
- console.error(...args);
93
+ cliLogger.error(...args);
81
94
  },
82
95
 
83
96
  /**
@@ -89,7 +102,7 @@ export const log = {
89
102
  */
90
103
  debug(...args) {
91
104
  if (verboseMode) {
92
- console.log('[debug]', ...args);
105
+ cliLogger.debug(...args);
93
106
  }
94
107
  },
95
108
 
@@ -116,7 +129,22 @@ export const log = {
116
129
  */
117
130
  newline() {
118
131
  console.log();
132
+ },
133
+
134
+ /**
135
+ * Create a child logger with a sub-namespace
136
+ * @param {string} namespace - Child namespace
137
+ * @returns {import('../runtime/logger.js').Logger} Child logger instance
138
+ * @example
139
+ * const buildLog = log.child('Build');
140
+ * buildLog.info('Starting...'); // [CLI:Build] Starting...
141
+ */
142
+ child(namespace) {
143
+ return cliLogger.child(namespace);
119
144
  }
120
145
  };
121
146
 
147
+ // Re-export LogLevel for convenience
148
+ export { LogLevel };
149
+
122
150
  export default log;
package/cli/release.js CHANGED
@@ -10,13 +10,15 @@
10
10
  * - Push to remote
11
11
  */
12
12
 
13
- import { readFileSync, writeFileSync, existsSync } from 'fs';
13
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
14
14
  import { join, dirname } from 'path';
15
15
  import { fileURLToPath } from 'url';
16
16
  import { execSync } from 'child_process';
17
+ import { tmpdir } from 'os';
17
18
  import { createInterface } from 'readline';
18
19
  import https from 'https';
19
20
  import { log } from './logger.js';
21
+ import { runDocsTest } from './docs-test.js';
20
22
 
21
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
24
  const root = join(__dirname, '..');
@@ -525,14 +527,15 @@ function gitCommitTagPush(newVersion, title, changes, dryRun = false) {
525
527
  log.info(' Running: git add -A...');
526
528
  execSync('git add -A', { cwd: root, stdio: 'inherit' });
527
529
 
528
- // git commit with heredoc for proper message formatting
530
+ // git commit using temp file for cross-platform compatibility
529
531
  log.info(' Running: git commit...');
530
- const escapedMessage = commitMessage.replace(/'/g, "'\\''");
531
- execSync(`git commit -m $'${escapedMessage.replace(/\n/g, '\\n')}'`, {
532
- cwd: root,
533
- stdio: 'inherit',
534
- shell: '/bin/bash'
535
- });
532
+ const tempFile = join(tmpdir(), `pulse-release-${Date.now()}.txt`);
533
+ writeFileSync(tempFile, commitMessage, 'utf-8');
534
+ try {
535
+ execSync(`git commit -F "${tempFile}"`, { cwd: root, stdio: 'inherit' });
536
+ } finally {
537
+ unlinkSync(tempFile);
538
+ }
536
539
 
537
540
  // git tag
538
541
  log.info(` Running: git tag v${newVersion}...`);
@@ -559,12 +562,13 @@ Types:
559
562
  major Bump major version (1.0.0 -> 2.0.0)
560
563
 
561
564
  Options:
562
- --dry-run Show what would be done without making changes
563
- --no-push Create commit and tag but don't push
564
- --title <text> Release title (e.g., "Performance Improvements")
565
- --skip-prompt Use empty changelog (for automated releases)
566
- --from-commits Auto-extract changelog from git commits since last tag
567
- --yes, -y Auto-confirm all prompts
565
+ --dry-run Show what would be done without making changes
566
+ --no-push Create commit and tag but don't push
567
+ --title <text> Release title (e.g., "Performance Improvements")
568
+ --skip-prompt Use empty changelog (for automated releases)
569
+ --skip-docs-test Skip documentation validation before release
570
+ --from-commits Auto-extract changelog from git commits since last tag
571
+ --yes, -y Auto-confirm all prompts
568
572
  --changes <json> Pass changelog as JSON (e.g., '{"added":["Feature 1"],"fixed":["Bug 1"]}')
569
573
  --added <items> Comma-separated list of added features
570
574
  --changed <items> Comma-separated list of changes
@@ -599,6 +603,7 @@ export async function runRelease(args) {
599
603
  const skipPrompt = args.includes('--skip-prompt');
600
604
  const fromCommits = args.includes('--from-commits');
601
605
  const autoConfirm = args.includes('--yes') || args.includes('-y');
606
+ const skipDocsTest = args.includes('--skip-docs-test');
602
607
 
603
608
  let title = '';
604
609
  const titleIndex = args.indexOf('--title');
@@ -675,6 +680,30 @@ export async function runRelease(args) {
675
680
  process.exit(1);
676
681
  }
677
682
 
683
+ // Run documentation tests
684
+ if (!skipDocsTest) {
685
+ log.info('');
686
+ log.info('Running documentation tests...');
687
+
688
+ const docsTestResult = await runDocsTest({ verbose: false, httpTest: true });
689
+
690
+ if (!docsTestResult.success) {
691
+ log.error('Documentation tests failed. Fix errors before releasing.');
692
+ if (!autoConfirm) {
693
+ const proceed = await prompt('Continue anyway? (y/N) ');
694
+ if (proceed.toLowerCase() !== 'y') {
695
+ log.info('Aborted.');
696
+ process.exit(1);
697
+ }
698
+ } else {
699
+ log.error('Aborting release due to documentation errors.');
700
+ process.exit(1);
701
+ }
702
+ }
703
+ } else {
704
+ log.warn('Skipping documentation tests (--skip-docs-test)');
705
+ }
706
+
678
707
  // Collect changelog entries
679
708
  let changes = { added: [], changed: [], fixed: [], removed: [] };
680
709
 
@@ -786,12 +815,13 @@ export async function runRelease(args) {
786
815
  // Only commit and tag, no push
787
816
  const commitMessage = buildCommitMessage(newVersion, title, changes);
788
817
  execSync('git add -A', { cwd: root, stdio: 'inherit' });
789
- const escapedMessage = commitMessage.replace(/'/g, "'\\''");
790
- execSync(`git commit -m $'${escapedMessage.replace(/\n/g, '\\n')}'`, {
791
- cwd: root,
792
- stdio: 'inherit',
793
- shell: '/bin/bash'
794
- });
818
+ const tempFile = join(tmpdir(), `pulse-release-${Date.now()}.txt`);
819
+ writeFileSync(tempFile, commitMessage, 'utf-8');
820
+ try {
821
+ execSync(`git commit -F "${tempFile}"`, { cwd: root, stdio: 'inherit' });
822
+ } finally {
823
+ unlinkSync(tempFile);
824
+ }
795
825
  execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: root, stdio: 'inherit' });
796
826
  log.info(' Created commit and tag (--no-push specified)');
797
827
  } else {