diff --git a/package.json b/package.json index 3260571..3d15372 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "microsoft-rewards-bot", - "version": "2.56.3", + "version": "2.56.2", "description": "Automate Microsoft Rewards points collection", "private": true, "main": "index.js", diff --git a/setup/update/update.mjs b/setup/update/update.mjs index 4380ee2..b6a68fb 100644 --- a/setup/update/update.mjs +++ b/setup/update/update.mjs @@ -202,6 +202,7 @@ async function extractZip(zipPath, destDir) { /** * Check if update is available by comparing versions + * Returns true if versions differ (allows both upgrades and downgrades) */ async function checkVersion() { try { @@ -222,10 +223,10 @@ async function checkVersion() { const pkgUrl = `https://raw.githubusercontent.com/${repoOwner}/${repoName}/refs/heads/${branch}/package.json` console.log('๐Ÿ” Checking for updates...') - console.log(` Local version: ${localVersion}`) + console.log(` Local: ${localVersion}`) return new Promise((resolve) => { - httpsGet(pkgUrl, (res) => { + const request = httpsGet(pkgUrl, (res) => { if (res.statusCode !== 200) { console.log(` โš ๏ธ Could not check remote version (HTTP ${res.statusCode})`) resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) @@ -238,8 +239,9 @@ async function checkVersion() { try { const remotePkg = JSON.parse(data) const remoteVersion = remotePkg.version - console.log(` Remote version: ${remoteVersion}`) + console.log(` Remote: ${remoteVersion}`) + // Any difference triggers update (upgrade or downgrade) const updateAvailable = localVersion !== remoteVersion resolve({ updateAvailable, localVersion, remoteVersion }) } catch (err) { @@ -247,10 +249,19 @@ async function checkVersion() { resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) } }) - }).on('error', (err) => { + }) + + // Timeout after 10 seconds + request.on('error', (err) => { console.log(` โš ๏ธ Network error: ${err.message}`) resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) }) + + request.setTimeout(10000, () => { + request.destroy() + console.log(' โš ๏ธ Request timeout') + resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) + }) }) } catch (err) { console.log(`โš ๏ธ Version check failed: ${err.message}`) @@ -262,26 +273,18 @@ async function checkVersion() { * Perform update using GitHub API (ZIP download) */ async function performUpdate() { - console.log('\n' + '='.repeat(70)) - console.log('๐Ÿš€ Microsoft Rewards Bot - Automatic Update') - console.log('='.repeat(70)) - // Step 0: Check if update is needed by comparing versions const versionCheck = await checkVersion() if (!versionCheck.updateAvailable) { - console.log('\nโœ… Already up to date!') - console.log(` Current version: ${versionCheck.localVersion}`) - console.log('='.repeat(70) + '\n') + console.log(`โœ… Already up to date (v${versionCheck.localVersion})`) return 0 // Exit without creating update marker } - console.log('\n๐Ÿ“ฅ New version available!') - console.log(` ${versionCheck.localVersion} โ†’ ${versionCheck.remoteVersion}`) - console.log(' Starting update process...\n') + console.log(`\n๐Ÿ“ฆ Update available: ${versionCheck.localVersion} โ†’ ${versionCheck.remoteVersion}`) + console.log('โณ Updating... (this may take a moment)\n') - // Step 1: Read user preferences - console.log('\n๐Ÿ“‹ Reading configuration...') + // Step 1: Read user preferences (silent) const configData = readJsonConfig([ 'src/config.jsonc', 'config.jsonc', @@ -293,14 +296,17 @@ async function performUpdate() { autoUpdateConfig: configData?.update?.autoUpdateConfig ?? false, autoUpdateAccounts: configData?.update?.autoUpdateAccounts ?? false } - - console.log(` โ€ข Auto-update config.jsonc: ${userConfig.autoUpdateConfig ? 'YES' : 'NO (protected)'}`) - console.log(` โ€ข Auto-update accounts: ${userConfig.autoUpdateAccounts ? 'YES' : 'NO (protected)'}`) - // Step 2: Backup protected files - console.log('\n๐Ÿ”’ Backing up protected files...') + // Step 2: Create backups (protected files + critical for rollback) const backupDir = join(process.cwd(), '.update-backup') + const rollbackDir = join(process.cwd(), '.update-rollback') + + // Clean previous backups + rmSync(backupDir, { recursive: true, force: true }) + rmSync(rollbackDir, { recursive: true, force: true }) + mkdirSync(backupDir, { recursive: true }) + mkdirSync(rollbackDir, { recursive: true }) const filesToProtect = [ { path: 'src/config.jsonc', protect: !userConfig.autoUpdateConfig }, @@ -326,14 +332,30 @@ async function performUpdate() { writeFileSync(destPath, readFileSync(srcPath)) } backedUp.push(file) - console.log(` โœ“ ${file.path}${file.isDir ? '/' : ''}`) - } catch (err) { - console.log(` โš ๏ธ Could not backup ${file.path}: ${err.message}`) + } catch { + // Silent failure - continue with update + } + } + + // Backup critical files for potential rollback + const criticalFiles = ['package.json', 'package-lock.json', 'dist'] + for (const file of criticalFiles) { + const srcPath = join(process.cwd(), file) + if (!existsSync(srcPath)) continue + const destPath = join(rollbackDir, file) + try { + if (statSync(srcPath).isDirectory()) { + cpSync(srcPath, destPath, { recursive: true }) + } else { + cpSync(srcPath, destPath) + } + } catch { + // Continue } } // Step 3: Download latest code from GitHub - console.log('\n๐ŸŒ Downloading latest code from GitHub...') + process.stdout.write('๐Ÿ“ฅ Downloading...') const repoOwner = 'Obsidian-wtf' const repoName = 'Microsoft-Rewards-Bot' const branch = 'main' @@ -342,28 +364,24 @@ async function performUpdate() { const archivePath = join(process.cwd(), '.update-download.zip') const extractDir = join(process.cwd(), '.update-extract') - console.log(` ${archiveUrl}`) - try { await downloadFile(archiveUrl, archivePath) - console.log(' โœ“ Download complete') + process.stdout.write(' โœ“\n') } catch (err) { - console.log(`\nโŒ Download failed: ${err.message}`) - console.log('Please check your internet connection and try again.') + console.log(` โŒ\nโŒ Download failed: ${err.message}`) return 1 } // Step 4: Extract archive - console.log('\n๐Ÿ“‚ Extracting archive...') + process.stdout.write('๐Ÿ“‚ Extracting...') rmSync(extractDir, { recursive: true, force: true }) mkdirSync(extractDir, { recursive: true }) try { await extractZip(archivePath, extractDir) - console.log(' โœ“ Extraction complete') + process.stdout.write(' โœ“\n') } catch (err) { - console.log(`\nโŒ Extraction failed: ${err.message}`) - console.log('Please ensure you have unzip, tar, or PowerShell available.') + console.log(` โŒ\nโŒ Extraction failed: ${err.message}`) return 1 } @@ -378,7 +396,7 @@ async function performUpdate() { const sourceDir = join(extractDir, extractedRepoDir) // Step 6: Copy files selectively - console.log('\n๐Ÿ“ฆ Updating files...') + process.stdout.write('๐Ÿ“ฆ Updating files...') const itemsToUpdate = [ 'src', 'docs', @@ -404,10 +422,7 @@ async function performUpdate() { // Skip protected items const isProtected = backedUp.some(f => f.path === item || destPath.includes(f.path)) - if (isProtected) { - console.log(` โญ๏ธ ${item} (protected)`) - continue - } + if (isProtected) continue try { if (existsSync(destPath)) { @@ -416,19 +431,17 @@ async function performUpdate() { if (statSync(srcPath).isDirectory()) { cpSync(srcPath, destPath, { recursive: true }) - console.log(` โœ“ ${item}/`) } else { cpSync(srcPath, destPath) - console.log(` โœ“ ${item}`) } - } catch (err) { - console.log(` โš ๏ธ Failed to update ${item}: ${err.message}`) + } catch { + // Silent failure - continue } } + process.stdout.write(' โœ“\n') - // Step 7: Restore protected files + // Step 7: Restore protected files (silent) if (backedUp.length > 0) { - console.log('\n๐Ÿ” Restoring protected files...') for (const file of backedUp) { const backupPath = join(backupDir, file.path) if (!existsSync(backupPath)) continue @@ -443,22 +456,18 @@ async function performUpdate() { } else { writeFileSync(destPath, readFileSync(backupPath)) } - console.log(` โœ“ ${file.path}${file.isDir ? '/' : ''}`) - } catch (err) { - console.log(` โš ๏ธ Failed to restore ${file.path}: ${err.message}`) + } catch { + // Silent failure } } } - // Step 8: Cleanup temporary files - console.log('\n๐Ÿงน Cleaning up...') + // Step 8: Cleanup temporary files (silent) rmSync(archivePath, { force: true }) rmSync(extractDir, { recursive: true, force: true }) rmSync(backupDir, { recursive: true, force: true }) - console.log(' โœ“ Temporary files removed') // Step 9: Create update marker for bot restart detection - // Version check already confirmed update is needed, so we always create marker here const updateMarkerPath = join(process.cwd(), '.update-happened') writeFileSync(updateMarkerPath, JSON.stringify({ timestamp: new Date().toISOString(), @@ -466,45 +475,137 @@ async function performUpdate() { toVersion: versionCheck.remoteVersion, method: 'github-api' }, null, 2)) - console.log(' โœ“ Update marker created') // Step 10: Install dependencies & rebuild const hasNpm = await which('npm') if (!hasNpm) { - console.log('\nโš ๏ธ npm not found, skipping dependencies and build') - console.log('Please run manually: npm install && npm run build') - console.log('\nโœ… Update complete!') - console.log('='.repeat(70) + '\n') + console.log('โš ๏ธ npm not found - please run: npm install && npm run build') return 0 } - console.log('\n๐Ÿ“ฆ Installing dependencies...') - const installCode = await run('npm', ['ci']) + process.stdout.write('๐Ÿ“ฆ Installing dependencies...') + const installCode = await run('npm', ['ci', '--silent'], { stdio: 'ignore' }) if (installCode !== 0) { - console.log(' โš ๏ธ npm ci failed, trying npm install...') - await run('npm', ['install']) + await run('npm', ['install', '--silent'], { stdio: 'ignore' }) + } + process.stdout.write(' โœ“\n') + + process.stdout.write('๐Ÿ”จ Building project...') + const buildCode = await run('npm', ['run', 'build'], { stdio: 'ignore' }) + + if (buildCode !== 0) { + // Build failed - rollback + process.stdout.write(' โŒ\n') + console.log('โš ๏ธ Build failed, rolling back to previous version...') + + // Restore from rollback + for (const file of criticalFiles) { + const srcPath = join(rollbackDir, file) + const destPath = join(process.cwd(), file) + if (!existsSync(srcPath)) continue + try { + rmSync(destPath, { recursive: true, force: true }) + if (statSync(srcPath).isDirectory()) { + cpSync(srcPath, destPath, { recursive: true }) + } else { + cpSync(srcPath, destPath) + } + } catch { + // Continue + } + } + + console.log('โœ… Rollback complete - using previous version') + rmSync(rollbackDir, { recursive: true, force: true }) + return 1 } - console.log('\n๐Ÿ”จ Building TypeScript project...') - const buildCode = await run('npm', ['run', 'build']) - - console.log('\n' + '='.repeat(70)) - if (buildCode === 0) { - console.log('โœ… Update completed successfully!') - console.log(' Bot will restart automatically with new version') - } else { - console.log('โš ๏ธ Update completed with build warnings') - console.log(' Please check for errors above') + process.stdout.write(' โœ“\n') + + // Step 11: Verify integrity (check if critical files exist) + process.stdout.write('๐Ÿ” Verifying integrity...') + const criticalPaths = [ + 'dist/index.js', + 'package.json', + 'src/index.ts' + ] + + let integrityOk = true + for (const path of criticalPaths) { + if (!existsSync(join(process.cwd(), path))) { + integrityOk = false + break + } } - console.log('='.repeat(70) + '\n') + + if (!integrityOk) { + process.stdout.write(' โŒ\n') + console.log('โš ๏ธ Integrity check failed, rolling back...') + + // Restore from rollback + for (const file of criticalFiles) { + const srcPath = join(rollbackDir, file) + const destPath = join(process.cwd(), file) + if (!existsSync(srcPath)) continue + try { + rmSync(destPath, { recursive: true, force: true }) + if (statSync(srcPath).isDirectory()) { + cpSync(srcPath, destPath, { recursive: true }) + } else { + cpSync(srcPath, destPath) + } + } catch { + // Continue + } + } + + console.log('โœ… Rollback complete - using previous version') + rmSync(rollbackDir, { recursive: true, force: true }) + return 1 + } + + process.stdout.write(' โœ“\n') + + // Clean rollback backup on success + rmSync(rollbackDir, { recursive: true, force: true }) - return buildCode + console.log(`\nโœ… Updated successfully! (${versionCheck.localVersion} โ†’ ${versionCheck.remoteVersion})`) + console.log('๐Ÿ”„ Restarting...\n') + + return 0 } // ============================================================================= // ENTRY POINT // ============================================================================= +/** + * Cleanup temporary files + */ +function cleanup() { + const tempDirs = [ + '.update-backup', + '.update-rollback', + '.update-extract', + '.update-download.zip' + ] + + for (const dir of tempDirs) { + const path = join(process.cwd(), dir) + try { + if (existsSync(path)) { + if (statSync(path).isDirectory()) { + rmSync(path, { recursive: true, force: true }) + } else { + rmSync(path, { force: true }) + } + } + } catch { + // Ignore cleanup errors + } + } +} + async function main() { // Check if updates are enabled in config const configData = readJsonConfig([ @@ -520,12 +621,31 @@ async function main() { return 0 } - const code = await performUpdate() - process.exit(code) + // Global timeout: 5 minutes max + const timeout = setTimeout(() => { + console.error('\nโฑ๏ธ Update timeout (5 min) - cleaning up...') + cleanup() + process.exit(1) + }, 5 * 60 * 1000) + + try { + const code = await performUpdate() + clearTimeout(timeout) + + // Final cleanup of temporary files + cleanup() + + process.exit(code) + } catch (err) { + clearTimeout(timeout) + cleanup() + throw err + } } main().catch((err) => { console.error('\nโŒ Update failed with error:', err) - console.error('\nPlease report this issue if it persists.') + console.error('\nCleaning up and reverting...') + cleanup() process.exit(1) }) diff --git a/src/index.ts b/src/index.ts index 32e441b..a3f48bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -959,15 +959,6 @@ async function main(): Promise { const updateHappened = fs.existsSync(updateMarkerPath) if (updateHappened) { - // Read marker for logging - try { - const markerContent = fs.readFileSync(updateMarkerPath, 'utf8') - const markerData = JSON.parse(markerContent) - log('main', 'UPDATE', `โœ… Updated ${markerData.fromVersion} โ†’ ${markerData.toVersion} - restarting...`, 'log', 'green') - } catch { - log('main', 'UPDATE', 'โœ… Update successful - restarting...', 'log', 'green') - } - // Remove marker file try { fs.unlinkSync(updateMarkerPath) @@ -975,16 +966,23 @@ async function main(): Promise { // Ignore cleanup errors } - // Restart the process with the same arguments - const { spawn } = await import('child_process') - const child = spawn(process.execPath, process.argv.slice(1), { - detached: true, - stdio: 'inherit' + // Clear Node's require cache to reload updated modules + Object.keys(require.cache).forEach(key => { + // Only clear cache for project files, not node_modules + if (key.includes('dist') || key.includes('src')) { + delete require.cache[key] + } }) - child.unref() - process.exit(0) - } else { - log('main', 'UPDATE', 'Already up to date, continuing with bot execution') + + // Recursive restart in same process + log('main', 'UPDATE', 'Reloading with new version...') + setTimeout(() => { + bootstrap().catch(e => { + log('main', 'MAIN-ERROR', 'Fatal after update: ' + (e instanceof Error ? e.message : e), 'error') + process.exit(1) + }) + }, 500) + return } } } catch (updateError) {