/ scripts / quality-check.js
quality-check.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * Quality Check Script
  5   * Runs all quality checks and outputs results in JSON format for Cline integration
  6   */
  7  
  8  import { execSync } from 'child_process';
  9  import { writeFileSync, mkdirSync, existsSync } from 'fs';
 10  import { join } from 'path';
 11  import { fileURLToPath } from 'url';
 12  import { dirname } from 'path';
 13  
 14  const __filename = fileURLToPath(import.meta.url);
 15  const __dirname = dirname(__filename);
 16  const projectRoot = join(__dirname, '..');
 17  const outputDir = join(projectRoot, '.quality-reports');
 18  
 19  // Ensure output directory exists
 20  if (!existsSync(outputDir)) {
 21    mkdirSync(outputDir, { recursive: true });
 22  }
 23  
 24  console.log('๐Ÿ” Running comprehensive quality checks...\n');
 25  
 26  const results = {
 27    timestamp: new Date().toISOString(),
 28    passed: true,
 29    checks: {},
 30  };
 31  
 32  /**
 33   * Run a quality check and capture results
 34   */
 35  // Safe: command args are hardcoded strings, not user input; file paths from known directories
 36  /* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
 37  // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
 38  function runCheck(name, command, options = {}) {
 39    const {
 40      outputFile = null,
 41      required = true,
 42      successMessage = `โœ… ${name} passed`,
 43      failureMessage = `โŒ ${name} failed`,
 44    } = options;
 45  
 46    try {
 47      const output = execSync(command, {
 48        // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
 49        cwd: projectRoot,
 50        encoding: 'utf-8',
 51        stdio: outputFile ? 'pipe' : 'inherit',
 52      });
 53  
 54      if (outputFile && output) {
 55        writeFileSync(join(outputDir, outputFile), output);
 56      }
 57  
 58      console.log(successMessage);
 59      results.checks[name] = {
 60        status: 'passed',
 61        output: outputFile ? `.quality-reports/${outputFile}` : null,
 62      };
 63  
 64      return true;
 65    } catch (error) {
 66      console.error(failureMessage);
 67  
 68      if (error.stdout && outputFile) {
 69        writeFileSync(join(outputDir, outputFile), error.stdout);
 70      }
 71  
 72      results.checks[name] = {
 73        status: 'failed',
 74        output: outputFile ? `.quality-reports/${outputFile}` : null,
 75        error: error.message,
 76      };
 77  
 78      if (required) {
 79        results.passed = false;
 80      }
 81  
 82      return false;
 83    }
 84  }
 85  
 86  // 1. ESLint
 87  console.log('๐Ÿ“‹ Running ESLint...');
 88  runCheck('eslint', './node_modules/.bin/eslint src/ scripts/ --format=json', {
 89    outputFile: 'eslint.json',
 90    required: true,
 91    successMessage: 'โœ… ESLint: No errors found',
 92    failureMessage: 'โš ๏ธ  ESLint: Issues found (see .quality-reports/eslint.json)',
 93  });
 94  
 95  // 2. Prettier
 96  console.log('\n๐Ÿ’… Checking code formatting...');
 97  runCheck('prettier', './node_modules/.bin/prettier --check "src/**/*.js" "scripts/**/*.js"', {
 98    required: false,
 99    successMessage: 'โœ… Prettier: Code is formatted correctly',
100    failureMessage: 'โš ๏ธ  Prettier: Code needs formatting (run: npm run format)',
101  });
102  
103  // 3. Unit Tests
104  console.log('\n๐Ÿงช Running tests...');
105  const testsPassed = runCheck('tests', 'node --test src/**/*.test.js 2>&1', {
106    outputFile: 'tests.txt',
107    required: true,
108    successMessage: 'โœ… Tests: All passing',
109    failureMessage: 'โŒ Tests: Some tests failed',
110  });
111  
112  // 4. Coverage (only if tests passed)
113  if (testsPassed) {
114    console.log('\n๐Ÿ“Š Generating coverage report...');
115    runCheck(
116      'coverage',
117      './node_modules/.bin/c8 --reporter=json-summary --reporter=text node --test src/**/*.test.js 2>&1',
118      {
119        outputFile: 'coverage.txt',
120        required: false,
121        successMessage: 'โœ… Coverage: Report generated',
122        failureMessage: 'โš ๏ธ  Coverage: Could not generate report',
123      }
124    );
125  
126    // Read coverage summary if it exists
127    try {
128      const coveragePath = join(projectRoot, 'coverage', 'coverage-summary.json');
129      if (existsSync(coveragePath)) {
130        const { readFileSync } = await import('fs');
131        const coverage = JSON.parse(readFileSync(coveragePath, 'utf-8'));
132        const totalPct = coverage.total.lines.pct;
133  
134        results.checks.coverage.percentage = totalPct;
135  
136        if (totalPct >= 70) {
137          console.log(`   ๐Ÿ“ˆ Coverage: ${totalPct.toFixed(1)}% (โœ… target: 70%)`);
138        } else {
139          console.log(`   ๐Ÿ“‰ Coverage: ${totalPct.toFixed(1)}% (โš ๏ธ  target: 70%)`);
140          results.checks.coverage.warning = 'Coverage below 70% target';
141        }
142      }
143    } catch {
144      // Coverage summary not available
145    }
146  }
147  
148  // 5. Sage AI Review (optional - only if installed)
149  console.log('\n๐Ÿค– Checking for Sage AI review...');
150  const sagePath = join(projectRoot, 'node_modules', '@tigtech', 'sage');
151  if (existsSync(sagePath)) {
152    runCheck('sage', './node_modules/.bin/sage review --format json 2>&1', {
153      outputFile: 'sage.json',
154      required: false,
155      successMessage: 'โœ… Sage: AI review complete',
156      failureMessage: 'โš ๏ธ  Sage: Review generated warnings (see .quality-reports/sage.json)',
157    });
158  } else {
159    console.log('โ„น๏ธ  Sage not installed (optional - install with: npm install -D @tigtech/sage)');
160    results.checks.sage = { status: 'not_installed' };
161  }
162  
163  // Write summary
164  writeFileSync(join(outputDir, 'summary.json'), JSON.stringify(results, null, 2));
165  
166  console.log(`\n${'='.repeat(60)}`);
167  console.log('\n๐Ÿ“‹ Quality Check Summary');
168  console.log('='.repeat(60));
169  console.log(`\nTimestamp: ${results.timestamp}`);
170  console.log(`Overall Status: ${results.passed ? 'โœ… PASSED' : 'โŒ FAILED'}\n`);
171  
172  Object.entries(results.checks).forEach(([name, check]) => {
173    const icon =
174      check.status === 'passed'
175        ? 'โœ…'
176        : check.status === 'failed'
177          ? 'โŒ'
178          : check.status === 'not_installed'
179            ? 'โ„น๏ธ'
180            : 'โš ๏ธ';
181    console.log(`${icon} ${name}: ${check.status}`);
182    if (check.output) {
183      console.log(`   ๐Ÿ“„ Details: ${check.output}`);
184    }
185  });
186  
187  console.log(`\n${'='.repeat(60)}`);
188  console.log('\n๐Ÿ’ก Cline Integration:');
189  console.log('   Cline can read .quality-reports/summary.json to auto-fix issues');
190  console.log('   Tell Cline: "Fix the issues in .quality-reports/"');
191  console.log(`\n${'='.repeat(60)}\n`);
192  
193  // Exit with appropriate code
194  process.exit(results.passed ? 0 : 1);