projax 3.3.25 → 3.3.27

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.
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' http://localhost:* ws://localhost:*;" />
7
7
  <title>projax</title>
8
- <script type="module" crossorigin src="./assets/index-CgB-tTpV.js"></script>
9
- <link rel="stylesheet" crossorigin href="./assets/index-ChoTzPLo.css">
8
+ <script type="module" crossorigin src="./assets/index-ByHY-x-j.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index-CS-85xbL.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -853,7 +853,7 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
853
853
  };
854
854
  addProcess(processInfo);
855
855
  // Listen for process exit to clean up tracking
856
- child.on('exit', (code, signal) => {
856
+ child.on('exit', async (code, signal) => {
857
857
  // Remove from tracking when process exits
858
858
  removeProcess(child.pid);
859
859
  // Log exit information
@@ -863,6 +863,8 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
863
863
  else if (signal !== null) {
864
864
  fs.appendFileSync(logFile, `\n\n[Process killed by signal ${signal}]\n`);
865
865
  }
866
+ // Check if this was a test script and parse results
867
+ await checkAndParseTestResults(logFile, projectPath, scriptName);
866
868
  });
867
869
  // Unref so parent can exit and process runs independently
868
870
  child.unref();
@@ -922,3 +924,51 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
922
924
  resolve(child.pid);
923
925
  });
924
926
  }
927
+ /**
928
+ * Check log file for test output and parse results
929
+ */
930
+ async function checkAndParseTestResults(logFile, projectPath, scriptName) {
931
+ try {
932
+ // Only parse if script name suggests it's a test command
933
+ const testScriptPatterns = ['test', 'spec', 'jest', 'vitest', 'mocha', 'pytest', 'unittest'];
934
+ const isTestScript = testScriptPatterns.some(pattern => scriptName.toLowerCase().includes(pattern));
935
+ if (!isTestScript) {
936
+ return;
937
+ }
938
+ // Read the log file
939
+ if (!fs.existsSync(logFile)) {
940
+ return;
941
+ }
942
+ const logContent = fs.readFileSync(logFile, 'utf-8');
943
+ // Import test parser (dynamic to avoid circular dependency issues)
944
+ const testParserPath = path.join(__dirname, 'test-parser.js');
945
+ if (!fs.existsSync(testParserPath)) {
946
+ // Parser not available (might be in development mode)
947
+ return;
948
+ }
949
+ const { parseTestOutput, isTestOutput } = await Promise.resolve(`${testParserPath}`).then(s => __importStar(require(s)));
950
+ // Check if output contains test results
951
+ if (!isTestOutput(logContent)) {
952
+ return;
953
+ }
954
+ // Parse test results
955
+ const parsed = parseTestOutput(logContent);
956
+ if (!parsed) {
957
+ return;
958
+ }
959
+ // Get project from database
960
+ const db = (0, core_bridge_1.getDatabaseManager)();
961
+ const project = db.getProjectByPath(projectPath);
962
+ if (!project) {
963
+ return;
964
+ }
965
+ // Save test results to database
966
+ db.addTestResult(project.id, scriptName, parsed.passed, parsed.failed, parsed.skipped, parsed.total, parsed.duration, parsed.coverage, parsed.framework, logContent.slice(-5000) // Store last 5000 chars of output
967
+ );
968
+ console.log(`\nšŸ“Š Test results saved: ${parsed.passed} passed, ${parsed.failed} failed, ${parsed.skipped} skipped`);
969
+ }
970
+ catch (error) {
971
+ // Silently fail - test parsing is optional
972
+ console.error('Error parsing test results:', error);
973
+ }
974
+ }
@@ -853,7 +853,7 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
853
853
  };
854
854
  addProcess(processInfo);
855
855
  // Listen for process exit to clean up tracking
856
- child.on('exit', (code, signal) => {
856
+ child.on('exit', async (code, signal) => {
857
857
  // Remove from tracking when process exits
858
858
  removeProcess(child.pid);
859
859
  // Log exit information
@@ -863,6 +863,8 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
863
863
  else if (signal !== null) {
864
864
  fs.appendFileSync(logFile, `\n\n[Process killed by signal ${signal}]\n`);
865
865
  }
866
+ // Check if this was a test script and parse results
867
+ await checkAndParseTestResults(logFile, projectPath, scriptName);
866
868
  });
867
869
  // Unref so parent can exit and process runs independently
868
870
  child.unref();
@@ -922,3 +924,51 @@ function runScriptInBackground(projectPath, projectName, scriptName, args = [],
922
924
  resolve(child.pid);
923
925
  });
924
926
  }
927
+ /**
928
+ * Check log file for test output and parse results
929
+ */
930
+ async function checkAndParseTestResults(logFile, projectPath, scriptName) {
931
+ try {
932
+ // Only parse if script name suggests it's a test command
933
+ const testScriptPatterns = ['test', 'spec', 'jest', 'vitest', 'mocha', 'pytest', 'unittest'];
934
+ const isTestScript = testScriptPatterns.some(pattern => scriptName.toLowerCase().includes(pattern));
935
+ if (!isTestScript) {
936
+ return;
937
+ }
938
+ // Read the log file
939
+ if (!fs.existsSync(logFile)) {
940
+ return;
941
+ }
942
+ const logContent = fs.readFileSync(logFile, 'utf-8');
943
+ // Import test parser (dynamic to avoid circular dependency issues)
944
+ const testParserPath = path.join(__dirname, 'test-parser.js');
945
+ if (!fs.existsSync(testParserPath)) {
946
+ // Parser not available (might be in development mode)
947
+ return;
948
+ }
949
+ const { parseTestOutput, isTestOutput } = await Promise.resolve(`${testParserPath}`).then(s => __importStar(require(s)));
950
+ // Check if output contains test results
951
+ if (!isTestOutput(logContent)) {
952
+ return;
953
+ }
954
+ // Parse test results
955
+ const parsed = parseTestOutput(logContent);
956
+ if (!parsed) {
957
+ return;
958
+ }
959
+ // Get project from database
960
+ const db = (0, core_bridge_1.getDatabaseManager)();
961
+ const project = db.getProjectByPath(projectPath);
962
+ if (!project) {
963
+ return;
964
+ }
965
+ // Save test results to database
966
+ db.addTestResult(project.id, scriptName, parsed.passed, parsed.failed, parsed.skipped, parsed.total, parsed.duration, parsed.coverage, parsed.framework, logContent.slice(-5000) // Store last 5000 chars of output
967
+ );
968
+ console.log(`\nšŸ“Š Test results saved: ${parsed.passed} passed, ${parsed.failed} failed, ${parsed.skipped} skipped`);
969
+ }
970
+ catch (error) {
971
+ // Silently fail - test parsing is optional
972
+ console.error('Error parsing test results:', error);
973
+ }
974
+ }
@@ -0,0 +1,17 @@
1
+ export interface ParsedTestResult {
2
+ framework: string | null;
3
+ passed: number;
4
+ failed: number;
5
+ skipped: number;
6
+ total: number;
7
+ duration: number | null;
8
+ coverage: number | null;
9
+ }
10
+ /**
11
+ * Parse test output from various frameworks and extract statistics
12
+ */
13
+ export declare function parseTestOutput(output: string): ParsedTestResult | null;
14
+ /**
15
+ * Check if output appears to be from a test command
16
+ */
17
+ export declare function isTestOutput(output: string): boolean;
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ // Test output parser for extracting test statistics from common test runners
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.parseTestOutput = parseTestOutput;
5
+ exports.isTestOutput = isTestOutput;
6
+ /**
7
+ * Parse test output from various frameworks and extract statistics
8
+ */
9
+ function parseTestOutput(output) {
10
+ const result = {
11
+ framework: null,
12
+ passed: 0,
13
+ failed: 0,
14
+ skipped: 0,
15
+ total: 0,
16
+ duration: null,
17
+ coverage: null,
18
+ };
19
+ // Detect framework and parse accordingly
20
+ if (output.includes('PASS') && output.includes('FAIL') || output.includes('Test Suites:')) {
21
+ // Jest/Vitest
22
+ result.framework = parseJestVitestOutput(output, result);
23
+ }
24
+ else if (output.includes('passing') && output.includes('failing')) {
25
+ // Mocha
26
+ result.framework = parseMochaOutput(output, result);
27
+ }
28
+ else if (output.includes('passed') && output.includes('failed') && output.includes('pytest')) {
29
+ // pytest
30
+ result.framework = parsePytestOutput(output, result);
31
+ }
32
+ else if (output.includes('OK') && output.includes('test')) {
33
+ // Python unittest
34
+ result.framework = parseUnittestOutput(output, result);
35
+ }
36
+ else {
37
+ // Try generic parsing
38
+ return parseGenericOutput(output);
39
+ }
40
+ // Calculate total if not set
41
+ if (result.total === 0) {
42
+ result.total = result.passed + result.failed + result.skipped;
43
+ }
44
+ return result.total > 0 ? result : null;
45
+ }
46
+ /**
47
+ * Parse Jest/Vitest output
48
+ */
49
+ function parseJestVitestOutput(output, result) {
50
+ // Jest format:
51
+ // Test Suites: 2 failed, 1 passed, 3 total
52
+ // Tests: 5 failed, 10 passed, 15 total
53
+ // Time: 2.345 s
54
+ const testSuitesMatch = output.match(/Test Suites:.*?(\d+)\s+passed/i);
55
+ const testsMatch = output.match(/Tests:\s+(?:(\d+)\s+failed,?\s*)?(?:(\d+)\s+skipped,?\s*)?(?:(\d+)\s+passed)/i);
56
+ const totalMatch = output.match(/Tests:.*?(\d+)\s+total/i);
57
+ const timeMatch = output.match(/Time:\s+([\d.]+)\s*s/i);
58
+ const coverageMatch = output.match(/All files\s*\|\s*([\d.]+)/);
59
+ if (testsMatch) {
60
+ result.failed = testsMatch[1] ? parseInt(testsMatch[1], 10) : 0;
61
+ result.skipped = testsMatch[2] ? parseInt(testsMatch[2], 10) : 0;
62
+ result.passed = testsMatch[3] ? parseInt(testsMatch[3], 10) : 0;
63
+ }
64
+ if (totalMatch) {
65
+ result.total = parseInt(totalMatch[1], 10);
66
+ }
67
+ if (timeMatch) {
68
+ result.duration = Math.floor(parseFloat(timeMatch[1]) * 1000);
69
+ }
70
+ if (coverageMatch) {
71
+ result.coverage = parseFloat(coverageMatch[1]);
72
+ }
73
+ // Determine if Jest or Vitest
74
+ return output.includes('vitest') ? 'vitest' : 'jest';
75
+ }
76
+ /**
77
+ * Parse Mocha output
78
+ */
79
+ function parseMochaOutput(output, result) {
80
+ // Mocha format:
81
+ // 10 passing (234ms)
82
+ // 2 failing
83
+ // 1 pending
84
+ const passingMatch = output.match(/(\d+)\s+passing/i);
85
+ const failingMatch = output.match(/(\d+)\s+failing/i);
86
+ const pendingMatch = output.match(/(\d+)\s+pending/i);
87
+ const timeMatch = output.match(/passing\s+\((\d+)ms\)/i);
88
+ if (passingMatch) {
89
+ result.passed = parseInt(passingMatch[1], 10);
90
+ }
91
+ if (failingMatch) {
92
+ result.failed = parseInt(failingMatch[1], 10);
93
+ }
94
+ if (pendingMatch) {
95
+ result.skipped = parseInt(pendingMatch[1], 10);
96
+ }
97
+ if (timeMatch) {
98
+ result.duration = parseInt(timeMatch[1], 10);
99
+ }
100
+ return 'mocha';
101
+ }
102
+ /**
103
+ * Parse pytest output
104
+ */
105
+ function parsePytestOutput(output, result) {
106
+ // pytest format:
107
+ // ===== 10 passed, 2 failed, 1 skipped in 2.34s =====
108
+ const statsMatch = output.match(/=+\s*(?:(\d+)\s+passed)?(?:,\s*(\d+)\s+failed)?(?:,\s*(\d+)\s+skipped)?(?:\s+in\s+([\d.]+)s)?/i);
109
+ if (statsMatch) {
110
+ result.passed = statsMatch[1] ? parseInt(statsMatch[1], 10) : 0;
111
+ result.failed = statsMatch[2] ? parseInt(statsMatch[2], 10) : 0;
112
+ result.skipped = statsMatch[3] ? parseInt(statsMatch[3], 10) : 0;
113
+ if (statsMatch[4]) {
114
+ result.duration = Math.floor(parseFloat(statsMatch[4]) * 1000);
115
+ }
116
+ }
117
+ // Coverage
118
+ const coverageMatch = output.match(/TOTAL\s+\d+\s+\d+\s+(\d+)%/);
119
+ if (coverageMatch) {
120
+ result.coverage = parseInt(coverageMatch[1], 10);
121
+ }
122
+ return 'pytest';
123
+ }
124
+ /**
125
+ * Parse Python unittest output
126
+ */
127
+ function parseUnittestOutput(output, result) {
128
+ // unittest format:
129
+ // Ran 15 tests in 2.345s
130
+ // OK
131
+ // or FAILED (failures=2, errors=1)
132
+ const ranMatch = output.match(/Ran\s+(\d+)\s+tests?\s+in\s+([\d.]+)s/i);
133
+ const failedMatch = output.match(/FAILED\s*\((?:failures=(\d+))?(?:,\s*errors=(\d+))?\)/i);
134
+ if (ranMatch) {
135
+ result.total = parseInt(ranMatch[1], 10);
136
+ result.duration = Math.floor(parseFloat(ranMatch[2]) * 1000);
137
+ }
138
+ if (failedMatch) {
139
+ const failures = failedMatch[1] ? parseInt(failedMatch[1], 10) : 0;
140
+ const errors = failedMatch[2] ? parseInt(failedMatch[2], 10) : 0;
141
+ result.failed = failures + errors;
142
+ result.passed = result.total - result.failed;
143
+ }
144
+ else if (output.includes('OK')) {
145
+ result.passed = result.total;
146
+ }
147
+ return 'unittest';
148
+ }
149
+ /**
150
+ * Generic parser for unrecognized frameworks
151
+ */
152
+ function parseGenericOutput(output) {
153
+ const result = {
154
+ framework: 'unknown',
155
+ passed: 0,
156
+ failed: 0,
157
+ skipped: 0,
158
+ total: 0,
159
+ duration: null,
160
+ coverage: null,
161
+ };
162
+ // Try to find common patterns
163
+ const patterns = [
164
+ { regex: /(\d+)\s+(?:test(?:s)?|spec(?:s)?)\s+passed/i, key: 'passed' },
165
+ { regex: /(\d+)\s+(?:test(?:s)?|spec(?:s)?)\s+failed/i, key: 'failed' },
166
+ { regex: /(\d+)\s+(?:test(?:s)?|spec(?:s)?)\s+skipped/i, key: 'skipped' },
167
+ { regex: /āœ“\s*(\d+)/i, key: 'passed' },
168
+ { regex: /āœ—\s*(\d+)/i, key: 'failed' },
169
+ ];
170
+ for (const pattern of patterns) {
171
+ const match = output.match(pattern.regex);
172
+ if (match && pattern.key in result) {
173
+ result[pattern.key] = parseInt(match[1], 10);
174
+ }
175
+ }
176
+ result.total = result.passed + result.failed + result.skipped;
177
+ return result.total > 0 ? result : null;
178
+ }
179
+ /**
180
+ * Check if output appears to be from a test command
181
+ */
182
+ function isTestOutput(output) {
183
+ const testIndicators = [
184
+ 'Test Suites:',
185
+ 'Tests:',
186
+ 'passing',
187
+ 'failing',
188
+ 'passed',
189
+ 'failed',
190
+ 'skipped',
191
+ 'pytest',
192
+ 'Ran',
193
+ 'test',
194
+ 'spec',
195
+ 'PASS',
196
+ 'FAIL',
197
+ ];
198
+ return testIndicators.some(indicator => output.includes(indicator));
199
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projax",
3
- "version": "3.3.25",
3
+ "version": "3.3.27",
4
4
  "description": "Cross-platform project management dashboard for tracking local development projects. Features CLI, Terminal UI, Desktop app, REST API, and built-in tools for test detection, port management, and script execution.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {