pulse-js-framework 1.7.5 → 1.7.8

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/index.js CHANGED
@@ -5,9 +5,10 @@
5
5
  */
6
6
 
7
7
  import { fileURLToPath } from 'url';
8
- import { dirname, join, resolve } from 'path';
9
- import { existsSync, mkdirSync, writeFileSync, readFileSync, cpSync } from 'fs';
8
+ import { dirname, join, resolve, relative } from 'path';
9
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, cpSync, watch } from 'fs';
10
10
  import { log } from './logger.js';
11
+ import { findPulseFiles, parseArgs } from './utils/file-utils.js';
11
12
 
12
13
  const __filename = fileURLToPath(import.meta.url);
13
14
  const __dirname = dirname(__filename);
@@ -24,14 +25,98 @@ const commands = {
24
25
  dev: runDev,
25
26
  build: runBuild,
26
27
  preview: runPreview,
27
- compile: compileFile,
28
+ compile: compileFiles,
28
29
  mobile: runMobile,
29
30
  lint: runLint,
30
31
  format: runFormat,
31
32
  analyze: runAnalyze,
32
- release: runReleaseCmd
33
+ release: runReleaseCmd,
34
+ 'docs-test': runDocsTestCmd
33
35
  };
34
36
 
37
+ // Command aliases for common typos
38
+ const commandAliases = {
39
+ 'complile': 'compile',
40
+ 'comile': 'compile',
41
+ 'complie': 'compile',
42
+ 'buid': 'build',
43
+ 'biuld': 'build',
44
+ 'buildd': 'build',
45
+ 'devv': 'dev',
46
+ 'lnt': 'lint',
47
+ 'lintt': 'lint',
48
+ 'fromat': 'format',
49
+ 'foramt': 'format',
50
+ 'formatt': 'format',
51
+ 'analize': 'analyze',
52
+ 'analzye': 'analyze',
53
+ 'anaylze': 'analyze',
54
+ 'crate': 'create',
55
+ 'craete': 'create',
56
+ 'preivew': 'preview',
57
+ 'preveiw': 'preview',
58
+ 'moble': 'mobile',
59
+ 'moblie': 'mobile',
60
+ 'relase': 'release',
61
+ 'realease': 'release'
62
+ };
63
+
64
+ /**
65
+ * Calculate Levenshtein distance between two strings
66
+ * Used for "Did you mean...?" suggestions
67
+ */
68
+ function levenshteinDistance(a, b) {
69
+ const matrix = [];
70
+
71
+ for (let i = 0; i <= b.length; i++) {
72
+ matrix[i] = [i];
73
+ }
74
+ for (let j = 0; j <= a.length; j++) {
75
+ matrix[0][j] = j;
76
+ }
77
+
78
+ for (let i = 1; i <= b.length; i++) {
79
+ for (let j = 1; j <= a.length; j++) {
80
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
81
+ matrix[i][j] = matrix[i - 1][j - 1];
82
+ } else {
83
+ matrix[i][j] = Math.min(
84
+ matrix[i - 1][j - 1] + 1,
85
+ matrix[i][j - 1] + 1,
86
+ matrix[i - 1][j] + 1
87
+ );
88
+ }
89
+ }
90
+ }
91
+
92
+ return matrix[b.length][a.length];
93
+ }
94
+
95
+ /**
96
+ * Suggest similar commands based on input
97
+ */
98
+ function suggestCommand(input) {
99
+ // Check aliases first
100
+ if (input in commandAliases) {
101
+ return commandAliases[input];
102
+ }
103
+
104
+ // Find closest command using Levenshtein distance
105
+ const allCommands = Object.keys(commands);
106
+ let closest = null;
107
+ let minDistance = Infinity;
108
+
109
+ for (const cmd of allCommands) {
110
+ const distance = levenshteinDistance(input.toLowerCase(), cmd.toLowerCase());
111
+ if (distance < minDistance && distance <= 3) { // Max 3 edits
112
+ minDistance = distance;
113
+ closest = cmd;
114
+ }
115
+ }
116
+
117
+ return closest;
118
+ }
119
+
35
120
  /**
36
121
  * Main entry point
37
122
  */
@@ -43,6 +128,13 @@ async function main() {
43
128
  await commands[command](args.slice(1));
44
129
  } else {
45
130
  log.error(`Unknown command: ${command}`);
131
+
132
+ // Try to suggest a similar command
133
+ const suggestion = suggestCommand(command);
134
+ if (suggestion) {
135
+ log.info(`Did you mean: pulse ${suggestion}?`);
136
+ }
137
+
46
138
  log.info('Run "pulse help" for usage information.');
47
139
  process.exit(1);
48
140
  }
@@ -68,14 +160,23 @@ Commands:
68
160
  format [files] Format .pulse files consistently
69
161
  analyze Analyze bundle size and dependencies
70
162
  release <type> Create a new release (patch, minor, major)
163
+ docs-test Test documentation (syntax, imports, HTTP)
71
164
  version Show version number
72
165
  help Show this help message
73
166
 
167
+ Compile Options:
168
+ --watch, -w Watch files and recompile on changes
169
+ --dry-run Show what would be compiled without writing
170
+ --output, -o Output directory (default: same as input)
171
+
74
172
  Lint Options:
75
173
  --fix Auto-fix fixable issues
174
+ --watch, -w Watch files and re-lint on changes
175
+ --dry-run Show fixes without applying (use with --fix)
76
176
 
77
177
  Format Options:
78
- --check Check formatting without writing
178
+ --check Check formatting without writing (dry-run)
179
+ --watch, -w Watch files and re-format on changes
79
180
  --write Write formatted output (default)
80
181
 
81
182
  Analyze Options:
@@ -83,11 +184,16 @@ Analyze Options:
83
184
  --verbose Show detailed metrics
84
185
 
85
186
  Release Options:
86
- --dry-run Show what would be done without making changes
87
- --no-push Create commit and tag but don't push
88
- --title <text> Release title for changelog
89
- --skip-prompt Use empty changelog (for automation)
90
- --from-commits Auto-extract changelog from git commits since last tag
187
+ --dry-run Show what would be done without making changes
188
+ --no-push Create commit and tag but don't push
189
+ --title <text> Release title for changelog
190
+ --skip-prompt Use empty changelog (for automation)
191
+ --skip-docs-test Skip documentation tests before release
192
+ --from-commits Auto-extract changelog from git commits since last tag
193
+
194
+ Docs-test Options:
195
+ --verbose, -v Show detailed output
196
+ --no-http Skip HTTP server tests
91
197
 
92
198
  Examples:
93
199
  pulse create my-app
@@ -99,7 +205,10 @@ Examples:
99
205
  pulse mobile build android
100
206
  pulse mobile run ios
101
207
  pulse compile src/App.pulse
208
+ pulse compile src/ --watch
209
+ pulse compile "**/*.pulse" --dry-run
102
210
  pulse lint src/
211
+ pulse lint src/ --fix --dry-run
103
212
  pulse lint "**/*.pulse" --fix
104
213
  pulse format --check
105
214
  pulse format src/App.pulse
@@ -109,6 +218,8 @@ Examples:
109
218
  pulse release minor --title "New Features"
110
219
  pulse release major --dry-run
111
220
  pulse release patch --from-commits
221
+ pulse docs-test
222
+ pulse docs-test --verbose
112
223
 
113
224
  Documentation: https://github.com/vincenthirtz/pulse-js-framework
114
225
  `);
@@ -381,38 +492,209 @@ async function runReleaseCmd(args) {
381
492
  }
382
493
 
383
494
  /**
384
- * Compile a single .pulse file
495
+ * Run documentation tests
385
496
  */
386
- async function compileFile(args) {
387
- const inputFile = args[0];
497
+ async function runDocsTestCmd(args) {
498
+ const { runDocsTestCli } = await import('./docs-test.js');
499
+ await runDocsTestCli(args);
500
+ }
388
501
 
389
- if (!inputFile) {
390
- log.error('Please provide a file to compile.');
391
- log.info('Usage: pulse compile <file.pulse>');
502
+ /**
503
+ * Compile .pulse files to JavaScript
504
+ * Supports multiple files, watch mode, and dry-run
505
+ */
506
+ async function compileFiles(args) {
507
+ const { options, patterns } = parseArgs(args);
508
+ const watchMode = options.watch || options.w || false;
509
+ const dryRun = options['dry-run'] || false;
510
+ const outputDir = options.output || options.o || null;
511
+
512
+ // Find all .pulse files matching patterns
513
+ const files = findPulseFiles(patterns);
514
+
515
+ if (files.length === 0) {
516
+ log.error('No .pulse files found.');
517
+ log.info('Usage: pulse compile <file.pulse> [options]');
518
+ log.info(' pulse compile src/ --watch');
392
519
  process.exit(1);
393
520
  }
394
521
 
395
- if (!existsSync(inputFile)) {
396
- log.error(`File not found: ${inputFile}`);
397
- process.exit(1);
522
+ const { compile } = await import('../compiler/index.js');
523
+ const cwd = process.cwd();
524
+
525
+ /**
526
+ * Compile a single file and return result
527
+ */
528
+ function compileOneFile(inputFile) {
529
+ const relPath = relative(cwd, inputFile);
530
+ const source = readFileSync(inputFile, 'utf-8');
531
+ const result = compile(source);
532
+
533
+ // Determine output path
534
+ let outputFile;
535
+ if (outputDir) {
536
+ const baseName = inputFile.replace(/\.pulse$/, '.js').split(/[/\\]/).pop();
537
+ outputFile = join(outputDir, baseName);
538
+ } else {
539
+ outputFile = inputFile.replace(/\.pulse$/, '.js');
540
+ }
541
+ const relOutput = relative(cwd, outputFile);
542
+
543
+ return { inputFile, outputFile, relPath, relOutput, result, source };
398
544
  }
399
545
 
400
- const { compile } = await import('../compiler/index.js');
546
+ /**
547
+ * Process compilation results
548
+ */
549
+ function processResults(compilations) {
550
+ let successCount = 0;
551
+ let errorCount = 0;
552
+
553
+ for (const { relPath, relOutput, result } of compilations) {
554
+ if (result.success) {
555
+ successCount++;
556
+ } else {
557
+ errorCount++;
558
+ log.error(`\n${relPath}:`);
559
+ for (const error of result.errors) {
560
+ const loc = error.line ? `:${error.line}:${error.column || 1}` : '';
561
+ log.error(` ${relPath}${loc} ${error.message}`);
562
+ }
563
+ }
564
+ }
401
565
 
402
- const source = readFileSync(inputFile, 'utf-8');
403
- const result = compile(source);
566
+ return { successCount, errorCount };
567
+ }
404
568
 
405
- if (result.success) {
406
- const outputFile = inputFile.replace(/\.pulse$/, '.js');
407
- writeFileSync(outputFile, result.code);
408
- log.info(`Compiled: ${inputFile} -> ${outputFile}`);
409
- } else {
410
- log.error('Compilation failed:');
411
- for (const error of result.errors) {
412
- log.error(` ${error.message}`);
569
+ /**
570
+ * Run compilation on all files
571
+ */
572
+ async function runCompilation() {
573
+ const startTime = Date.now();
574
+ const compilations = files.map(compileOneFile);
575
+ const { successCount, errorCount } = processResults(compilations);
576
+
577
+ if (dryRun) {
578
+ // Dry run - show what would be done
579
+ log.info('\n[Dry Run] Would compile:');
580
+ for (const { relPath, relOutput, result } of compilations) {
581
+ if (result.success) {
582
+ log.info(` ${relPath} -> ${relOutput}`);
583
+ }
584
+ }
585
+ log.info(`\n${successCount} file(s) would be compiled, ${errorCount} error(s)`);
586
+ } else {
587
+ // Actually write files
588
+ let writtenCount = 0;
589
+ for (const { outputFile, relPath, relOutput, result } of compilations) {
590
+ if (result.success) {
591
+ // Ensure output directory exists
592
+ const outDir = dirname(outputFile);
593
+ if (!existsSync(outDir)) {
594
+ mkdirSync(outDir, { recursive: true });
595
+ }
596
+ writeFileSync(outputFile, result.code);
597
+ log.info(`Compiled: ${relPath} -> ${relOutput}`);
598
+ writtenCount++;
599
+ }
600
+ }
601
+
602
+ const duration = Date.now() - startTime;
603
+ if (files.length > 1) {
604
+ log.info(`\n${writtenCount} file(s) compiled in ${duration}ms`);
605
+ if (errorCount > 0) {
606
+ log.error(`${errorCount} file(s) failed`);
607
+ }
608
+ }
413
609
  }
414
- process.exit(1);
610
+
611
+ return errorCount === 0;
612
+ }
613
+
614
+ // Initial compilation
615
+ const success = await runCompilation();
616
+
617
+ if (!watchMode) {
618
+ if (!success) {
619
+ process.exit(1);
620
+ }
621
+ return;
415
622
  }
623
+
624
+ // Watch mode
625
+ log.info('\nWatching for changes... (Ctrl+C to stop)\n');
626
+
627
+ // Collect directories to watch
628
+ const dirsToWatch = new Set();
629
+ for (const file of files) {
630
+ dirsToWatch.add(dirname(file));
631
+ }
632
+
633
+ // Debounce mechanism
634
+ let debounceTimeout = null;
635
+ const changedFiles = new Set();
636
+
637
+ function handleChange(eventType, filename) {
638
+ if (!filename || !filename.endsWith('.pulse')) return;
639
+
640
+ // Find the full path of the changed file
641
+ for (const dir of dirsToWatch) {
642
+ const fullPath = join(dir, filename);
643
+ if (files.includes(fullPath)) {
644
+ changedFiles.add(fullPath);
645
+ break;
646
+ }
647
+ }
648
+
649
+ // Debounce: wait 100ms before processing
650
+ if (debounceTimeout) {
651
+ clearTimeout(debounceTimeout);
652
+ }
653
+ debounceTimeout = setTimeout(() => {
654
+ if (changedFiles.size > 0) {
655
+ log.info(`\nFile changed: ${[...changedFiles].map(f => relative(cwd, f)).join(', ')}`);
656
+ const filesToCompile = [...changedFiles];
657
+ changedFiles.clear();
658
+
659
+ // Recompile changed files
660
+ for (const file of filesToCompile) {
661
+ const { relPath, relOutput, outputFile, result } = compileOneFile(file);
662
+ if (result.success) {
663
+ if (!dryRun) {
664
+ writeFileSync(outputFile, result.code);
665
+ }
666
+ log.info(`${dryRun ? '[Dry Run] Would compile' : 'Compiled'}: ${relPath} -> ${relOutput}`);
667
+ } else {
668
+ log.error(`\n${relPath}:`);
669
+ for (const error of result.errors) {
670
+ const loc = error.line ? `:${error.line}:${error.column || 1}` : '';
671
+ log.error(` ${relPath}${loc} ${error.message}`);
672
+ }
673
+ }
674
+ }
675
+ }
676
+ }, 100);
677
+ }
678
+
679
+ // Start watching directories
680
+ const watchers = [];
681
+ for (const dir of dirsToWatch) {
682
+ try {
683
+ const watcher = watch(dir, { recursive: false }, handleChange);
684
+ watchers.push(watcher);
685
+ } catch (e) {
686
+ log.warn(`Could not watch directory: ${dir}`);
687
+ }
688
+ }
689
+
690
+ // Keep process running
691
+ process.on('SIGINT', () => {
692
+ log.info('\nStopping watch mode...');
693
+ for (const watcher of watchers) {
694
+ watcher.close();
695
+ }
696
+ process.exit(0);
697
+ });
416
698
  }
417
699
 
418
700
  // Run main
package/cli/lint.js CHANGED
@@ -706,10 +706,13 @@ export async function lintFile(filePath, options = {}) {
706
706
  * Lint files and return summary
707
707
  * @param {string[]} files - Files to lint
708
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
709
712
  * @returns {Object} Summary with totals
710
713
  */
711
714
  async function lintFiles(files, options = {}) {
712
- const { fix = false, quiet = false } = options;
715
+ const { fix = false, dryRun = false, quiet = false } = options;
713
716
  const timer = createTimer();
714
717
 
715
718
  let totalErrors = 0;
@@ -749,8 +752,14 @@ async function lintFiles(files, options = {}) {
749
752
  export async function runLint(args) {
750
753
  const { options, patterns } = parseArgs(args);
751
754
  const fix = options.fix || false;
755
+ const dryRun = options['dry-run'] || false;
752
756
  const watchMode = options.watch || options.w || false;
753
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
+
754
763
  // Find files to lint
755
764
  const files = findPulseFiles(patterns);
756
765
 
@@ -760,8 +769,8 @@ export async function runLint(args) {
760
769
  }
761
770
 
762
771
  // Initial lint run
763
- log.info(`Linting ${files.length} file(s)...\n`);
764
- const summary = await lintFiles(files, { fix });
772
+ log.info(`Linting ${files.length} file(s)...${dryRun ? ' (dry-run)' : ''}\n`);
773
+ const summary = await lintFiles(files, { fix, dryRun });
765
774
 
766
775
  // Print summary
767
776
  printLintSummary(summary, files.length);
@@ -795,7 +804,7 @@ export async function runLint(args) {
795
804
  debounceTimers.delete(filePath);
796
805
 
797
806
  log.info(`\n[${new Date().toLocaleTimeString()}] File changed: ${relativePath(filePath)}`);
798
- lintFiles([filePath], { fix }).then(result => {
807
+ lintFiles([filePath], { fix, dryRun }).then(result => {
799
808
  printLintSummary(result, 1, true);
800
809
  });
801
810
  }, 100));
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 {
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { TokenType, tokenize } from './lexer.js';
8
- import { ParserError, SUGGESTIONS, getDocsUrl } from '../core/errors.js';
8
+ import { ParserError, SUGGESTIONS, getDocsUrl } from '../runtime/errors.js';
9
9
 
10
10
  // AST Node types
11
11
  export const NodeType = {