fix: Revert version in package.json to 2.56.2; improve update logging and process restart handling

This commit is contained in:
2025-11-09 23:02:21 +01:00
parent 7e4c519b3b
commit 8b1d95a7c7
3 changed files with 213 additions and 95 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "microsoft-rewards-bot", "name": "microsoft-rewards-bot",
"version": "2.56.3", "version": "2.56.2",
"description": "Automate Microsoft Rewards points collection", "description": "Automate Microsoft Rewards points collection",
"private": true, "private": true,
"main": "index.js", "main": "index.js",

View File

@@ -202,6 +202,7 @@ async function extractZip(zipPath, destDir) {
/** /**
* Check if update is available by comparing versions * Check if update is available by comparing versions
* Returns true if versions differ (allows both upgrades and downgrades)
*/ */
async function checkVersion() { async function checkVersion() {
try { try {
@@ -222,10 +223,10 @@ async function checkVersion() {
const pkgUrl = `https://raw.githubusercontent.com/${repoOwner}/${repoName}/refs/heads/${branch}/package.json` const pkgUrl = `https://raw.githubusercontent.com/${repoOwner}/${repoName}/refs/heads/${branch}/package.json`
console.log('🔍 Checking for updates...') console.log('🔍 Checking for updates...')
console.log(` Local version: ${localVersion}`) console.log(` Local: ${localVersion}`)
return new Promise((resolve) => { return new Promise((resolve) => {
httpsGet(pkgUrl, (res) => { const request = httpsGet(pkgUrl, (res) => {
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
console.log(` ⚠️ Could not check remote version (HTTP ${res.statusCode})`) console.log(` ⚠️ Could not check remote version (HTTP ${res.statusCode})`)
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
@@ -238,8 +239,9 @@ async function checkVersion() {
try { try {
const remotePkg = JSON.parse(data) const remotePkg = JSON.parse(data)
const remoteVersion = remotePkg.version const remoteVersion = remotePkg.version
console.log(` Remote version: ${remoteVersion}`) console.log(` Remote: ${remoteVersion}`)
// Any difference triggers update (upgrade or downgrade)
const updateAvailable = localVersion !== remoteVersion const updateAvailable = localVersion !== remoteVersion
resolve({ updateAvailable, localVersion, remoteVersion }) resolve({ updateAvailable, localVersion, remoteVersion })
} catch (err) { } catch (err) {
@@ -247,10 +249,19 @@ async function checkVersion() {
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
} }
}) })
}).on('error', (err) => { })
// Timeout after 10 seconds
request.on('error', (err) => {
console.log(` ⚠️ Network error: ${err.message}`) console.log(` ⚠️ Network error: ${err.message}`)
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
}) })
request.setTimeout(10000, () => {
request.destroy()
console.log(' ⚠️ Request timeout')
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
})
}) })
} catch (err) { } catch (err) {
console.log(`⚠️ Version check failed: ${err.message}`) console.log(`⚠️ Version check failed: ${err.message}`)
@@ -262,26 +273,18 @@ async function checkVersion() {
* Perform update using GitHub API (ZIP download) * Perform update using GitHub API (ZIP download)
*/ */
async function performUpdate() { 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 // Step 0: Check if update is needed by comparing versions
const versionCheck = await checkVersion() const versionCheck = await checkVersion()
if (!versionCheck.updateAvailable) { if (!versionCheck.updateAvailable) {
console.log('\n✅ Already up to date!') console.log(`✅ Already up to date (v${versionCheck.localVersion})`)
console.log(` Current version: ${versionCheck.localVersion}`)
console.log('='.repeat(70) + '\n')
return 0 // Exit without creating update marker return 0 // Exit without creating update marker
} }
console.log('\n📥 New version available!') console.log(`\n📦 Update available: ${versionCheck.localVersion}${versionCheck.remoteVersion}`)
console.log(` ${versionCheck.localVersion}${versionCheck.remoteVersion}`) console.log('⏳ Updating... (this may take a moment)\n')
console.log(' Starting update process...\n')
// Step 1: Read user preferences // Step 1: Read user preferences (silent)
console.log('\n📋 Reading configuration...')
const configData = readJsonConfig([ const configData = readJsonConfig([
'src/config.jsonc', 'src/config.jsonc',
'config.jsonc', 'config.jsonc',
@@ -293,14 +296,17 @@ async function performUpdate() {
autoUpdateConfig: configData?.update?.autoUpdateConfig ?? false, autoUpdateConfig: configData?.update?.autoUpdateConfig ?? false,
autoUpdateAccounts: configData?.update?.autoUpdateAccounts ?? 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 // Step 2: Create backups (protected files + critical for rollback)
console.log('\n🔒 Backing up protected files...')
const backupDir = join(process.cwd(), '.update-backup') 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(backupDir, { recursive: true })
mkdirSync(rollbackDir, { recursive: true })
const filesToProtect = [ const filesToProtect = [
{ path: 'src/config.jsonc', protect: !userConfig.autoUpdateConfig }, { path: 'src/config.jsonc', protect: !userConfig.autoUpdateConfig },
@@ -326,14 +332,30 @@ async function performUpdate() {
writeFileSync(destPath, readFileSync(srcPath)) writeFileSync(destPath, readFileSync(srcPath))
} }
backedUp.push(file) backedUp.push(file)
console.log(`${file.path}${file.isDir ? '/' : ''}`) } catch {
} catch (err) { // Silent failure - continue with update
console.log(` ⚠️ Could not backup ${file.path}: ${err.message}`) }
}
// 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 // Step 3: Download latest code from GitHub
console.log('\n🌐 Downloading latest code from GitHub...') process.stdout.write('📥 Downloading...')
const repoOwner = 'Obsidian-wtf' const repoOwner = 'Obsidian-wtf'
const repoName = 'Microsoft-Rewards-Bot' const repoName = 'Microsoft-Rewards-Bot'
const branch = 'main' const branch = 'main'
@@ -342,28 +364,24 @@ async function performUpdate() {
const archivePath = join(process.cwd(), '.update-download.zip') const archivePath = join(process.cwd(), '.update-download.zip')
const extractDir = join(process.cwd(), '.update-extract') const extractDir = join(process.cwd(), '.update-extract')
console.log(` ${archiveUrl}`)
try { try {
await downloadFile(archiveUrl, archivePath) await downloadFile(archiveUrl, archivePath)
console.log(' ✓ Download complete') process.stdout.write(' ✓\n')
} catch (err) { } catch (err) {
console.log(`\n❌ Download failed: ${err.message}`) console.log(`\n❌ Download failed: ${err.message}`)
console.log('Please check your internet connection and try again.')
return 1 return 1
} }
// Step 4: Extract archive // Step 4: Extract archive
console.log('\n📂 Extracting archive...') process.stdout.write('📂 Extracting...')
rmSync(extractDir, { recursive: true, force: true }) rmSync(extractDir, { recursive: true, force: true })
mkdirSync(extractDir, { recursive: true }) mkdirSync(extractDir, { recursive: true })
try { try {
await extractZip(archivePath, extractDir) await extractZip(archivePath, extractDir)
console.log(' ✓ Extraction complete') process.stdout.write(' ✓\n')
} catch (err) { } catch (err) {
console.log(`\n❌ Extraction failed: ${err.message}`) console.log(`\n❌ Extraction failed: ${err.message}`)
console.log('Please ensure you have unzip, tar, or PowerShell available.')
return 1 return 1
} }
@@ -378,7 +396,7 @@ async function performUpdate() {
const sourceDir = join(extractDir, extractedRepoDir) const sourceDir = join(extractDir, extractedRepoDir)
// Step 6: Copy files selectively // Step 6: Copy files selectively
console.log('\n📦 Updating files...') process.stdout.write('📦 Updating files...')
const itemsToUpdate = [ const itemsToUpdate = [
'src', 'src',
'docs', 'docs',
@@ -404,10 +422,7 @@ async function performUpdate() {
// Skip protected items // Skip protected items
const isProtected = backedUp.some(f => f.path === item || destPath.includes(f.path)) const isProtected = backedUp.some(f => f.path === item || destPath.includes(f.path))
if (isProtected) { if (isProtected) continue
console.log(` ⏭️ ${item} (protected)`)
continue
}
try { try {
if (existsSync(destPath)) { if (existsSync(destPath)) {
@@ -416,19 +431,17 @@ async function performUpdate() {
if (statSync(srcPath).isDirectory()) { if (statSync(srcPath).isDirectory()) {
cpSync(srcPath, destPath, { recursive: true }) cpSync(srcPath, destPath, { recursive: true })
console.log(`${item}/`)
} else { } else {
cpSync(srcPath, destPath) cpSync(srcPath, destPath)
console.log(`${item}`)
} }
} catch (err) { } catch {
console.log(` ⚠️ Failed to update ${item}: ${err.message}`) // Silent failure - continue
} }
} }
process.stdout.write(' ✓\n')
// Step 7: Restore protected files // Step 7: Restore protected files (silent)
if (backedUp.length > 0) { if (backedUp.length > 0) {
console.log('\n🔐 Restoring protected files...')
for (const file of backedUp) { for (const file of backedUp) {
const backupPath = join(backupDir, file.path) const backupPath = join(backupDir, file.path)
if (!existsSync(backupPath)) continue if (!existsSync(backupPath)) continue
@@ -443,22 +456,18 @@ async function performUpdate() {
} else { } else {
writeFileSync(destPath, readFileSync(backupPath)) writeFileSync(destPath, readFileSync(backupPath))
} }
console.log(`${file.path}${file.isDir ? '/' : ''}`) } catch {
} catch (err) { // Silent failure
console.log(` ⚠️ Failed to restore ${file.path}: ${err.message}`)
} }
} }
} }
// Step 8: Cleanup temporary files // Step 8: Cleanup temporary files (silent)
console.log('\n🧹 Cleaning up...')
rmSync(archivePath, { force: true }) rmSync(archivePath, { force: true })
rmSync(extractDir, { recursive: true, force: true }) rmSync(extractDir, { recursive: true, force: true })
rmSync(backupDir, { recursive: true, force: true }) rmSync(backupDir, { recursive: true, force: true })
console.log(' ✓ Temporary files removed')
// Step 9: Create update marker for bot restart detection // 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') const updateMarkerPath = join(process.cwd(), '.update-happened')
writeFileSync(updateMarkerPath, JSON.stringify({ writeFileSync(updateMarkerPath, JSON.stringify({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@@ -466,45 +475,137 @@ async function performUpdate() {
toVersion: versionCheck.remoteVersion, toVersion: versionCheck.remoteVersion,
method: 'github-api' method: 'github-api'
}, null, 2)) }, null, 2))
console.log(' ✓ Update marker created')
// Step 10: Install dependencies & rebuild // Step 10: Install dependencies & rebuild
const hasNpm = await which('npm') const hasNpm = await which('npm')
if (!hasNpm) { if (!hasNpm) {
console.log('\n⚠️ npm not found, skipping dependencies and build') console.log('⚠️ npm not found - please run: npm install && npm run build')
console.log('Please run manually: npm install && npm run build')
console.log('\n✅ Update complete!')
console.log('='.repeat(70) + '\n')
return 0 return 0
} }
console.log('\n📦 Installing dependencies...') process.stdout.write('📦 Installing dependencies...')
const installCode = await run('npm', ['ci']) const installCode = await run('npm', ['ci', '--silent'], { stdio: 'ignore' })
if (installCode !== 0) { if (installCode !== 0) {
console.log(' ⚠️ npm ci failed, trying npm install...') await run('npm', ['install', '--silent'], { stdio: 'ignore' })
await run('npm', ['install']) }
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...') process.stdout.write(' ✓\n')
const buildCode = await run('npm', ['run', 'build'])
// Step 11: Verify integrity (check if critical files exist)
console.log('\n' + '='.repeat(70)) process.stdout.write('🔍 Verifying integrity...')
if (buildCode === 0) { const criticalPaths = [
console.log('✅ Update completed successfully!') 'dist/index.js',
console.log(' Bot will restart automatically with new version') 'package.json',
} else { 'src/index.ts'
console.log('⚠️ Update completed with build warnings') ]
console.log(' Please check for errors above')
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 // 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() { async function main() {
// Check if updates are enabled in config // Check if updates are enabled in config
const configData = readJsonConfig([ const configData = readJsonConfig([
@@ -520,12 +621,31 @@ async function main() {
return 0 return 0
} }
const code = await performUpdate() // Global timeout: 5 minutes max
process.exit(code) 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) => { main().catch((err) => {
console.error('\n❌ Update failed with error:', 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) process.exit(1)
}) })

View File

@@ -959,15 +959,6 @@ async function main(): Promise<void> {
const updateHappened = fs.existsSync(updateMarkerPath) const updateHappened = fs.existsSync(updateMarkerPath)
if (updateHappened) { 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 // Remove marker file
try { try {
fs.unlinkSync(updateMarkerPath) fs.unlinkSync(updateMarkerPath)
@@ -975,16 +966,23 @@ async function main(): Promise<void> {
// Ignore cleanup errors // Ignore cleanup errors
} }
// Restart the process with the same arguments // Clear Node's require cache to reload updated modules
const { spawn } = await import('child_process') Object.keys(require.cache).forEach(key => {
const child = spawn(process.execPath, process.argv.slice(1), { // Only clear cache for project files, not node_modules
detached: true, if (key.includes('dist') || key.includes('src')) {
stdio: 'inherit' delete require.cache[key]
}
}) })
child.unref()
process.exit(0) // Recursive restart in same process
} else { log('main', 'UPDATE', 'Reloading with new version...')
log('main', 'UPDATE', 'Already up to date, continuing with bot execution') 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) { } catch (updateError) {