| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- #!/usr/bin/env node
- /**
- * GrabZilla 2.1 - Project State Verification Script
- *
- * Checks:
- * - Binaries exist and are executable
- * - Tests can run and pass
- * - App can launch (headless check)
- * - Dependencies are installed
- * - File structure is intact
- *
- * Outputs JSON with complete state assessment.
- *
- * Usage: node verify-project-state.js
- */
- const fs = require('fs')
- const path = require('path')
- const { execSync, spawn } = require('child_process')
- // ANSI colors for terminal output
- const colors = {
- reset: '\x1b[0m',
- green: '\x1b[32m',
- red: '\x1b[31m',
- yellow: '\x1b[33m',
- blue: '\x1b[34m',
- cyan: '\x1b[36m'
- }
- const log = {
- info: (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`),
- success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
- error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
- warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
- section: (msg) => console.log(`\n${colors.blue}▶${colors.reset} ${msg}`)
- }
- // State object to collect all verification results
- const state = {
- timestamp: new Date().toISOString(),
- status: 'unknown', // Will be: green, yellow, red
- binaries: {
- ytdlp: { exists: false, executable: false, version: null },
- ffmpeg: { exists: false, executable: false, version: null }
- },
- tests: {
- total: 0,
- passing: 0,
- failing: 0,
- passRate: 0,
- suites: {}
- },
- app: {
- launches: false,
- error: null
- },
- dependencies: {
- installed: false,
- count: 0
- },
- files: {
- critical: [],
- missing: []
- },
- health: {
- score: 0, // 0-100
- issues: [],
- recommendations: []
- }
- }
- /**
- * Check if binaries exist and are executable
- */
- function checkBinaries() {
- log.section('Checking Binaries')
- const binariesDir = path.join(__dirname, 'binaries')
- const platform = process.platform
- const ext = platform === 'win32' ? '.exe' : ''
- // Check yt-dlp
- const ytdlpPath = path.join(binariesDir, `yt-dlp${ext}`)
- state.binaries.ytdlp.exists = fs.existsSync(ytdlpPath)
- if (state.binaries.ytdlp.exists) {
- try {
- // Check if executable
- fs.accessSync(ytdlpPath, fs.constants.X_OK)
- state.binaries.ytdlp.executable = true
- // Get version
- const version = execSync(`"${ytdlpPath}" --version`, { encoding: 'utf8' }).trim()
- state.binaries.ytdlp.version = version
- log.success(`yt-dlp found and executable (v${version})`)
- } catch (error) {
- state.binaries.ytdlp.executable = false
- log.error(`yt-dlp found but not executable: ${error.message}`)
- state.health.issues.push('yt-dlp is not executable - run: chmod +x binaries/yt-dlp')
- }
- } else {
- log.error('yt-dlp not found')
- state.health.issues.push('yt-dlp missing - run: node setup.js')
- }
- // Check ffmpeg
- const ffmpegPath = path.join(binariesDir, `ffmpeg${ext}`)
- state.binaries.ffmpeg.exists = fs.existsSync(ffmpegPath)
- if (state.binaries.ffmpeg.exists) {
- try {
- // Check if executable
- fs.accessSync(ffmpegPath, fs.constants.X_OK)
- state.binaries.ffmpeg.executable = true
- // Get version (ffmpeg outputs to stderr)
- const output = execSync(`"${ffmpegPath}" -version 2>&1`, { encoding: 'utf8' })
- const versionMatch = output.match(/ffmpeg version ([^\s]+)/)
- state.binaries.ffmpeg.version = versionMatch ? versionMatch[1] : 'unknown'
- log.success(`ffmpeg found and executable (v${state.binaries.ffmpeg.version})`)
- } catch (error) {
- state.binaries.ffmpeg.executable = false
- log.error(`ffmpeg found but not executable: ${error.message}`)
- state.health.issues.push('ffmpeg is not executable - run: chmod +x binaries/ffmpeg')
- }
- } else {
- log.error('ffmpeg not found')
- state.health.issues.push('ffmpeg missing - run: node setup.js')
- }
- }
- /**
- * Check if dependencies are installed
- */
- function checkDependencies() {
- log.section('Checking Dependencies')
- const nodeModulesPath = path.join(__dirname, 'node_modules')
- state.dependencies.installed = fs.existsSync(nodeModulesPath)
- if (state.dependencies.installed) {
- try {
- const pkgJson = require('./package.json')
- const allDeps = {
- ...pkgJson.dependencies,
- ...pkgJson.devDependencies
- }
- state.dependencies.count = Object.keys(allDeps).length
- log.success(`Dependencies installed (${state.dependencies.count} packages)`)
- } catch (error) {
- log.error(`Error reading package.json: ${error.message}`)
- state.health.issues.push('Cannot read package.json')
- }
- } else {
- log.error('node_modules not found')
- state.health.issues.push('Dependencies not installed - run: npm install')
- }
- }
- /**
- * Check critical files exist
- */
- function checkCriticalFiles() {
- log.section('Checking Critical Files')
- const criticalFiles = [
- 'src/main.js',
- 'src/preload.js',
- 'src/download-manager.js',
- 'scripts/app.js',
- 'scripts/models/Video.js',
- 'scripts/models/AppState.js',
- 'scripts/services/metadata-service.js',
- 'scripts/utils/url-validator.js',
- 'scripts/utils/ipc-integration.js',
- 'index.html',
- 'styles/main.css',
- 'package.json'
- ]
- criticalFiles.forEach(file => {
- const fullPath = path.join(__dirname, file)
- if (fs.existsSync(fullPath)) {
- state.files.critical.push(file)
- } else {
- state.files.missing.push(file)
- log.error(`Missing critical file: ${file}`)
- state.health.issues.push(`Critical file missing: ${file}`)
- }
- })
- log.success(`${state.files.critical.length}/${criticalFiles.length} critical files present`)
- }
- /**
- * Run tests and capture results
- */
- async function runTests() {
- log.section('Running Tests')
- return new Promise((resolve) => {
- try {
- // Run tests and capture output
- const output = execSync('npm test 2>&1', {
- encoding: 'utf8',
- maxBuffer: 10 * 1024 * 1024 // 10MB buffer
- })
- // Parse test results
- parseTestOutput(output)
- resolve()
- } catch (error) {
- // Tests may "fail" but still provide output
- parseTestOutput(error.stdout || error.message)
- resolve()
- }
- })
- }
- /**
- * Parse test output to extract statistics
- */
- function parseTestOutput(output) {
- // Extract test counts
- const passedMatch = output.match(/(\d+) passed/)
- const failedMatch = output.match(/(\d+) failed/)
- if (passedMatch) {
- state.tests.passing = parseInt(passedMatch[1], 10)
- }
- if (failedMatch) {
- state.tests.failing = parseInt(failedMatch[1], 10)
- }
- state.tests.total = state.tests.passing + state.tests.failing
- state.tests.passRate = state.tests.total > 0
- ? Math.round((state.tests.passing / state.tests.total) * 100)
- : 0
- // Extract suite results
- const suiteMatches = output.matchAll(/([✓❌]) ([A-Za-z\s]+Tests)\s+(PASSED|FAILED)/g)
- for (const match of suiteMatches) {
- const [, icon, name, status] = match
- state.tests.suites[name.trim()] = status === 'PASSED'
- }
- log.info(`Test Results: ${state.tests.passing}/${state.tests.total} passing (${state.tests.passRate}%)`)
- if (state.tests.passRate < 95) {
- state.health.issues.push(`Test pass rate below 95%: ${state.tests.passRate}%`)
- }
- if (state.tests.failing > 2) {
- state.health.issues.push(`Too many failing tests: ${state.tests.failing}`)
- }
- }
- /**
- * Check if app can launch (simple check)
- */
- function checkAppLaunch() {
- log.section('Checking App Launch Capability')
- try {
- // Check if main.js can be required without errors
- const mainPath = path.join(__dirname, 'src', 'main.js')
- if (fs.existsSync(mainPath)) {
- const mainContent = fs.readFileSync(mainPath, 'utf8')
- // Check for critical patterns
- const hasCreateWindow = mainContent.includes('createWindow')
- const hasIpcHandlers = mainContent.includes('ipcMain.handle')
- const hasAppReady = mainContent.includes('app.whenReady')
- if (hasCreateWindow && hasIpcHandlers && hasAppReady) {
- state.app.launches = true
- log.success('App structure looks valid')
- } else {
- state.app.error = 'Missing critical app initialization code'
- log.warning('App may not launch correctly')
- state.health.issues.push('App missing critical initialization code')
- }
- } else {
- state.app.error = 'main.js not found'
- log.error('Cannot find src/main.js')
- state.health.issues.push('src/main.js not found')
- }
- } catch (error) {
- state.app.error = error.message
- log.error(`App check failed: ${error.message}`)
- state.health.issues.push(`App check error: ${error.message}`)
- }
- }
- /**
- * Calculate overall health score
- */
- function calculateHealthScore() {
- let score = 0
- // Binaries (30 points)
- if (state.binaries.ytdlp.exists && state.binaries.ytdlp.executable) score += 15
- if (state.binaries.ffmpeg.exists && state.binaries.ffmpeg.executable) score += 15
- // Dependencies (10 points)
- if (state.dependencies.installed) score += 10
- // Critical files (20 points)
- const fileRatio = state.files.critical.length / (state.files.critical.length + state.files.missing.length)
- score += Math.round(fileRatio * 20)
- // Tests (30 points)
- score += Math.round((state.tests.passRate / 100) * 30)
- // App launch (10 points)
- if (state.app.launches) score += 10
- state.health.score = Math.min(100, score)
- // Determine status
- if (state.health.score >= 90) {
- state.status = 'green'
- } else if (state.health.score >= 70) {
- state.status = 'yellow'
- } else {
- state.status = 'red'
- }
- }
- /**
- * Generate recommendations
- */
- function generateRecommendations() {
- // Binary recommendations
- if (!state.binaries.ytdlp.exists || !state.binaries.ffmpeg.exists) {
- state.health.recommendations.push('Run "node setup.js" to download missing binaries')
- }
- if (!state.binaries.ytdlp.executable || !state.binaries.ffmpeg.executable) {
- state.health.recommendations.push('Run "chmod +x binaries/*" to make binaries executable')
- }
- // Dependency recommendations
- if (!state.dependencies.installed) {
- state.health.recommendations.push('Run "npm install" to install dependencies')
- }
- // Test recommendations
- if (state.tests.passRate < 95) {
- state.health.recommendations.push('Fix failing tests to improve stability')
- }
- // File recommendations
- if (state.files.missing.length > 0) {
- state.health.recommendations.push('Restore missing critical files from git')
- }
- // Overall recommendations
- if (state.status === 'green') {
- state.health.recommendations.push('Project is healthy - ready for development')
- } else if (state.status === 'yellow') {
- state.health.recommendations.push('Project has minor issues - fix before major changes')
- } else {
- state.health.recommendations.push('Project has critical issues - fix immediately')
- }
- }
- /**
- * Print summary report
- */
- function printSummary() {
- console.log('\n' + '='.repeat(60))
- console.log(' GRABZILLA 2.1 - PROJECT STATE VERIFICATION')
- console.log('='.repeat(60))
- // Status badge
- const statusColor = state.status === 'green' ? colors.green
- : state.status === 'yellow' ? colors.yellow
- : colors.red
- const statusEmoji = state.status === 'green' ? '🟢'
- : state.status === 'yellow' ? '🟡'
- : '🔴'
- console.log(`\nStatus: ${statusEmoji} ${statusColor}${state.status.toUpperCase()}${colors.reset} (Health Score: ${state.health.score}/100)`)
- // Binaries
- console.log('\nBinaries:')
- console.log(` yt-dlp: ${state.binaries.ytdlp.exists && state.binaries.ytdlp.executable ? '✓' : '✗'} ${state.binaries.ytdlp.version || 'not found'}`)
- console.log(` ffmpeg: ${state.binaries.ffmpeg.exists && state.binaries.ffmpeg.executable ? '✓' : '✗'} ${state.binaries.ffmpeg.version || 'not found'}`)
- // Tests
- console.log('\nTests:')
- console.log(` Total: ${state.tests.total}`)
- console.log(` Passing: ${state.tests.passing} (${state.tests.passRate}%)`)
- console.log(` Failing: ${state.tests.failing}`)
- // Dependencies
- console.log('\nDependencies:')
- console.log(` Installed: ${state.dependencies.installed ? '✓' : '✗'} (${state.dependencies.count} packages)`)
- // Files
- console.log('\nCritical Files:')
- console.log(` Present: ${state.files.critical.length}`)
- console.log(` Missing: ${state.files.missing.length}`)
- // App
- console.log('\nApp Launch:')
- console.log(` Can Launch: ${state.app.launches ? '✓' : '✗'}`)
- if (state.app.error) {
- console.log(` Error: ${state.app.error}`)
- }
- // Issues
- if (state.health.issues.length > 0) {
- console.log('\n⚠ Issues Found:')
- state.health.issues.forEach((issue, i) => {
- console.log(` ${i + 1}. ${issue}`)
- })
- }
- // Recommendations
- console.log('\n📋 Recommendations:')
- state.health.recommendations.forEach((rec, i) => {
- console.log(` ${i + 1}. ${rec}`)
- })
- console.log('\n' + '='.repeat(60))
- console.log(`Verification completed at ${new Date().toLocaleString()}`)
- console.log('='.repeat(60) + '\n')
- }
- /**
- * Main verification function
- */
- async function verify() {
- console.log('\n🔍 Starting GrabZilla 2.1 verification...\n')
- checkBinaries()
- checkDependencies()
- checkCriticalFiles()
- await runTests()
- checkAppLaunch()
- calculateHealthScore()
- generateRecommendations()
- printSummary()
- // Export JSON
- const jsonPath = path.join(__dirname, 'project-state.json')
- fs.writeFileSync(jsonPath, JSON.stringify(state, null, 2))
- log.success(`Full state exported to: ${jsonPath}`)
- // Exit code based on status
- const exitCode = state.status === 'green' ? 0
- : state.status === 'yellow' ? 1
- : 2
- process.exit(exitCode)
- }
- // Run verification
- verify().catch(error => {
- console.error('Verification failed:', error)
- process.exit(3)
- })
|