project-scan 1.0.1 → 1.0.3

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.
Files changed (2) hide show
  1. package/lib/test-runner.js +53 -23
  2. package/package.json +1 -1
@@ -9,6 +9,8 @@ const { execSync, spawnSync } = require('child_process');
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
+ const IS_WIN = process.platform === 'win32';
13
+
12
14
  // ─── Per-language runner configs ─────────────────────────────────────────────
13
15
 
14
16
  /**
@@ -100,9 +102,9 @@ const RUNNERS = [
100
102
  hasSourceExt(root, '.py');
101
103
  },
102
104
  run(root) {
103
- const cmd = commandExists('pytest') ? 'pytest -v --tb=short 2>&1'
104
- : commandExists('python3') ? 'python3 -m pytest -v --tb=short 2>&1'
105
- : commandExists('python') ? 'python -m pytest -v --tb=short 2>&1'
105
+ const cmd = commandExists('pytest') ? 'pytest -v --tb=short'
106
+ : commandExists('python3') ? 'python3 -m pytest -v --tb=short'
107
+ : commandExists('python') ? 'python -m pytest -v --tb=short'
106
108
  : null;
107
109
  return [runCommand({ dir: root, label: '(root)', cmd, suite: 'pytest', parseOutput: parsePytestOutput })];
108
110
  }
@@ -115,7 +117,7 @@ const RUNNERS = [
115
117
  run(root) {
116
118
  return [runCommand({
117
119
  dir: root, label: '(root)',
118
- cmd: commandExists('go') ? 'go test ./... -v 2>&1' : null,
120
+ cmd: commandExists('go') ? 'go test ./... -v' : null,
119
121
  suite: 'Go test',
120
122
  parseOutput: parseGoTestOutput,
121
123
  })];
@@ -129,7 +131,7 @@ const RUNNERS = [
129
131
  run(root) {
130
132
  return [runCommand({
131
133
  dir: root, label: '(root)',
132
- cmd: commandExists('cargo') ? 'cargo test 2>&1' : null,
134
+ cmd: commandExists('cargo') ? 'cargo test' : null,
133
135
  suite: 'Cargo test',
134
136
  parseOutput: parseCargoOutput,
135
137
  })];
@@ -141,7 +143,7 @@ const RUNNERS = [
141
143
  name: 'Maven',
142
144
  detect(root) { return fs.existsSync(path.join(root, 'pom.xml')); },
143
145
  run(root) {
144
- const cmd = commandExists('mvn') ? 'mvn test -B 2>&1' : null;
146
+ const cmd = commandExists('mvn') ? 'mvn test -B' : null;
145
147
  return [runCommand({ dir: root, label: '(root)', cmd, suite: 'Maven', parseOutput: parseMavenOutput })];
146
148
  }
147
149
  },
@@ -154,8 +156,13 @@ const RUNNERS = [
154
156
  fs.existsSync(path.join(root, 'build.gradle.kts'));
155
157
  },
156
158
  run(root) {
157
- const gradleCmd = fs.existsSync(path.join(root, 'gradlew')) ? './gradlew' : (commandExists('gradle') ? 'gradle' : null);
158
- return [runCommand({ dir: root, label: '(root)', cmd: gradleCmd ? `${gradleCmd} test 2>&1` : null, suite: 'Gradle', parseOutput: parseGradleOutput })];
159
+ // Windows uses gradlew.bat; Unix uses ./gradlew
160
+ const wrapperWin = path.join(root, 'gradlew.bat');
161
+ const wrapperUnix = path.join(root, 'gradlew');
162
+ const gradleCmd = fs.existsSync(IS_WIN ? wrapperWin : wrapperUnix)
163
+ ? (IS_WIN ? 'gradlew.bat' : './gradlew')
164
+ : (commandExists('gradle') ? 'gradle' : null);
165
+ return [runCommand({ dir: root, label: '(root)', cmd: gradleCmd ? `${gradleCmd} test` : null, suite: 'Gradle', parseOutput: parseGradleOutput })];
159
166
  }
160
167
  },
161
168
 
@@ -163,14 +170,16 @@ const RUNNERS = [
163
170
  {
164
171
  name: 'PHPUnit',
165
172
  detect(root) {
166
- return fs.existsSync(path.join(root, 'phpunit.xml')) ||
167
- fs.existsSync(path.join(root, 'phpunit.xml.dist')) ||
168
- fs.existsSync(path.join(root, 'vendor/bin/phpunit')) ||
173
+ return fs.existsSync(path.join(root, 'phpunit.xml')) ||
174
+ fs.existsSync(path.join(root, 'phpunit.xml.dist')) ||
175
+ fs.existsSync(path.join(root, 'vendor', 'bin', 'phpunit')) ||
169
176
  hasSourceExt(root, '.php');
170
177
  },
171
178
  run(root) {
172
- const cmd = fs.existsSync(path.join(root, 'vendor/bin/phpunit')) ? './vendor/bin/phpunit --testdox 2>&1'
173
- : commandExists('phpunit') ? 'phpunit --testdox 2>&1' : null;
179
+ const vendorBin = path.join(root, 'vendor', 'bin', 'phpunit');
180
+ const cmd = fs.existsSync(vendorBin)
181
+ ? `"${vendorBin}" --testdox`
182
+ : commandExists('phpunit') ? 'phpunit --testdox' : null;
174
183
  return [runCommand({ dir: root, label: '(root)', cmd, suite: 'PHPUnit', parseOutput: parsePHPUnitOutput })];
175
184
  }
176
185
  },
@@ -184,7 +193,7 @@ const RUNNERS = [
184
193
  fs.existsSync(path.join(root, 'spec'));
185
194
  },
186
195
  run(root) {
187
- const cmd = commandExists('rspec') ? 'rspec --format progress 2>&1' : (commandExists('bundle') ? 'bundle exec rspec --format progress 2>&1' : null);
196
+ const cmd = commandExists('rspec') ? 'rspec --format progress' : (commandExists('bundle') ? 'bundle exec rspec --format progress' : null);
188
197
  return [runCommand({ dir: root, label: '(root)', cmd, suite: 'RSpec', parseOutput: parseRSpecOutput })];
189
198
  }
190
199
  },
@@ -194,7 +203,7 @@ const RUNNERS = [
194
203
  name: 'Flutter/Dart test',
195
204
  detect(root) { return fs.existsSync(path.join(root, 'pubspec.yaml')); },
196
205
  run(root) {
197
- const cmd = commandExists('flutter') ? 'flutter test 2>&1' : (commandExists('dart') ? 'dart test 2>&1' : null);
206
+ const cmd = commandExists('flutter') ? 'flutter test' : (commandExists('dart') ? 'dart test' : null);
198
207
  return [runCommand({ dir: root, label: '(root)', cmd, suite: 'Flutter/Dart', parseOutput: parseDartOutput })];
199
208
  }
200
209
  },
@@ -208,7 +217,7 @@ const RUNNERS = [
208
217
  } catch { return false; }
209
218
  },
210
219
  run(root) {
211
- const cmd = commandExists('dotnet') ? 'dotnet test --logger "console;verbosity=normal" 2>&1' : null;
220
+ const cmd = commandExists('dotnet') ? 'dotnet test --logger "console;verbosity=normal"' : null;
212
221
  return [runCommand({ dir: root, label: '(root)', cmd, suite: 'dotnet test', parseOutput: parseDotnetOutput })];
213
222
  }
214
223
  },
@@ -252,17 +261,18 @@ function runAllTests(rootDir) {
252
261
 
253
262
  function runJest(dir, label) {
254
263
  const start = Date.now();
255
- if (!fs.existsSync(path.join(dir, 'node_modules', '.bin', 'jest')) &&
264
+ if (!fs.existsSync(path.join(dir, 'node_modules', '.bin', 'jest')) &&
265
+ !fs.existsSync(path.join(dir, 'node_modules', '.bin', 'jest.cmd')) &&
256
266
  !fs.existsSync(path.join(dir, 'node_modules', 'jest'))) {
257
- // Try npm test fallback
258
267
  const pkg = tryReadJSON(path.join(dir, 'package.json'));
259
268
  if (!pkg?.scripts?.test) {
260
269
  return { suite: `Jest (${label})`, status: 'skipped', passed: 0, failed: 0, skipped: 0, output: 'Jest not installed. Run npm install first.', duration: 0 };
261
270
  }
262
271
  }
263
272
 
273
+ // shell:true is required on Windows so npx.cmd is resolved correctly
264
274
  const result = spawnSync('npx', ['jest', '--no-coverage', '--json', '--forceExit'], {
265
- cwd: dir, encoding: 'utf8', timeout: 120000,
275
+ cwd: dir, encoding: 'utf8', timeout: 120000, shell: true,
266
276
  env: { ...process.env, FORCE_COLOR: '0' }
267
277
  });
268
278
 
@@ -329,7 +339,11 @@ function runCommand({ dir, label, cmd, suite, parseOutput }) {
329
339
  }
330
340
  const start = Date.now();
331
341
  try {
332
- const output = execSync(cmd, { cwd: dir, encoding: 'utf8', timeout: 120000, env: { ...process.env, FORCE_COLOR: '0' }, stdio: 'pipe' });
342
+ // shell:true is required on Windows; harmless on Unix
343
+ const output = execSync(cmd, {
344
+ cwd: dir, encoding: 'utf8', timeout: 120000, shell: true,
345
+ env: { ...process.env, FORCE_COLOR: '0' }, stdio: 'pipe'
346
+ });
333
347
  const duration = Date.now() - start;
334
348
  return { suite: `${suite} (${label})`, duration, ...parseOutput(output, true) };
335
349
  } catch (err) {
@@ -432,12 +446,28 @@ function parseDotnetOutput(output, exitOk) {
432
446
  return { status: exitOk ? 'passed' : 'failed', passed: p ? parseInt(p[1]) : 0, failed: f ? parseInt(f[1]) : 0, skipped: 0, output: output.slice(-3000) };
433
447
  }
434
448
 
435
- // ─── Utility helpers ──────────────────────────────────────────────────────────
449
+ // ─── Utility helpers ─────────────────────────────────────────────────────────
436
450
 
437
451
  function tryReadJSON(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } }
438
452
  function allDeps(pkg) { return [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {}), ...Object.keys(pkg.peerDependencies || {})]; }
439
- function commandExists(cmd) { try { execSync(`which ${cmd} 2>/dev/null || command -v ${cmd} 2>/dev/null`, { encoding: 'utf8' }).trim(); return true; } catch { return false; } }
440
- function hasBin(dir, bin) { return fs.existsSync(path.join(dir, 'node_modules', '.bin', bin)); }
453
+
454
+ /** Works on Windows (where and Get-Command) and Unix (which / command -v). */
455
+ function commandExists(cmd) {
456
+ try {
457
+ if (IS_WIN) {
458
+ execSync(`where ${cmd}`, { encoding: 'utf8', stdio: 'pipe' });
459
+ } else {
460
+ execSync(`command -v ${cmd}`, { encoding: 'utf8', stdio: 'pipe', shell: '/bin/sh' });
461
+ }
462
+ return true;
463
+ } catch { return false; }
464
+ }
465
+
466
+ function hasBin(dir, bin) {
467
+ // On Windows the bin is a .cmd wrapper
468
+ return fs.existsSync(path.join(dir, 'node_modules', '.bin', bin)) ||
469
+ fs.existsSync(path.join(dir, 'node_modules', '.bin', bin + '.cmd'));
470
+ }
441
471
  function hasSourceExt(root, ext) {
442
472
  try { return fs.readdirSync(root).some(f => f.endsWith(ext)); } catch { return false; }
443
473
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-scan",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Universal project summarizer & test runner — scans any project for languages, frameworks, databases, cloud services, CI/CD pipelines and runs tests. Works with JavaScript, TypeScript, Python, Go, Rust, Java, Kotlin, PHP, Ruby, Dart, C#, and more.",
5
5
  "main": "lib/scanner.js",
6
6
  "bin": {