mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
New structure 2
This commit is contained in:
@@ -41,15 +41,12 @@ npm start
|
||||
**Usage:**
|
||||
```bash
|
||||
# Run update manually
|
||||
node setup/update/update.mjs
|
||||
node scripts/installer/update.mjs
|
||||
```
|
||||
|
||||
**Automatic updates:** The bot checks for updates on startup (controlled by `update.enabled` in config.jsonc).
|
||||
|
||||
### update/setup.mjs
|
||||
**Interactive setup wizard** used by setup.bat/setup.sh.
|
||||
|
||||
This is typically not run directly - use the wrapper scripts instead.
|
||||
**Note:** Installer scripts have been moved to `scripts/installer/` directory. See `scripts/README.md` for details.
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
|
||||
3
setup/run.sh
Normal file
3
setup/run.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
nix develop --command bash -c "xvfb-run npm run start"
|
||||
@@ -1,175 +0,0 @@
|
||||
# Update System
|
||||
|
||||
## Overview
|
||||
|
||||
The bot uses a **simplified GitHub API-based update system** that:
|
||||
- ✅ Downloads latest code as ZIP archive
|
||||
- ✅ No Git required
|
||||
- ✅ No merge conflicts
|
||||
- ✅ Preserves user files automatically
|
||||
- ✅ Automatic dependency installation
|
||||
- ✅ TypeScript rebuild
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Automatic Updates**: If enabled in `config.jsonc`, the bot checks for updates on every startup
|
||||
2. **Download**: Latest code is downloaded as ZIP from GitHub
|
||||
3. **Protection**: User files (accounts, config, sessions) are backed up
|
||||
4. **Update**: Code files are replaced selectively
|
||||
5. **Restore**: Protected files are restored
|
||||
6. **Install**: Dependencies are installed (`npm ci`)
|
||||
7. **Build**: TypeScript is compiled
|
||||
8. **Restart**: Bot restarts automatically with new version
|
||||
|
||||
## Configuration
|
||||
|
||||
In `src/config.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"update": {
|
||||
"enabled": true, // Enable/disable updates
|
||||
"autoUpdateAccounts": false, // Protect accounts files (recommended: false)
|
||||
"autoUpdateConfig": false // Protect config.jsonc (recommended: false)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Protected Files
|
||||
|
||||
These files are **always protected** (never overwritten):
|
||||
- `sessions/` - Browser session data
|
||||
- `.playwright-chromium-installed` - Browser installation marker
|
||||
|
||||
These files are **conditionally protected** (based on config):
|
||||
- `src/accounts.jsonc` - Protected unless `autoUpdateAccounts: true`
|
||||
- `src/accounts.json` - Protected unless `autoUpdateAccounts: true`
|
||||
- `src/config.jsonc` - Protected unless `autoUpdateConfig: true`
|
||||
|
||||
## Manual Update
|
||||
|
||||
Run the update manually:
|
||||
|
||||
```bash
|
||||
node setup/update/update.mjs
|
||||
```
|
||||
|
||||
## Update Detection
|
||||
|
||||
The bot uses marker files to prevent restart loops:
|
||||
|
||||
- `.update-happened` - Created when files are actually updated
|
||||
- `.update-restart-count` - Tracks restart attempts (max 3)
|
||||
|
||||
If no updates are available, **no marker is created** and the bot won't restart.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Updates disabled
|
||||
```
|
||||
⚠️ Updates are disabled in config.jsonc
|
||||
```
|
||||
→ Set `update.enabled: true` in `src/config.jsonc`
|
||||
|
||||
### Download failed
|
||||
```
|
||||
❌ Download failed: [error]
|
||||
```
|
||||
→ Check your internet connection
|
||||
→ Verify GitHub is accessible
|
||||
|
||||
### Extraction failed
|
||||
```
|
||||
❌ Extraction failed: [error]
|
||||
```
|
||||
→ Ensure you have one of: `unzip`, `tar`, or PowerShell (Windows)
|
||||
|
||||
### Build failed
|
||||
```
|
||||
⚠️ Update completed with build warnings
|
||||
```
|
||||
→ Check TypeScript errors above
|
||||
→ May still work, but review errors
|
||||
|
||||
## Architecture
|
||||
|
||||
### File Structure
|
||||
```
|
||||
setup/update/
|
||||
├── update.mjs # Main update script (468 lines)
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
### Update Flow
|
||||
```
|
||||
Start
|
||||
↓
|
||||
Check config (enabled?)
|
||||
↓
|
||||
Read user preferences (autoUpdate flags)
|
||||
↓
|
||||
Backup protected files
|
||||
↓
|
||||
Download ZIP from GitHub
|
||||
↓
|
||||
Extract archive
|
||||
↓
|
||||
Copy files selectively (skip protected)
|
||||
↓
|
||||
Restore protected files
|
||||
↓
|
||||
Cleanup temporary files
|
||||
↓
|
||||
Create marker (.update-happened) if files changed
|
||||
↓
|
||||
Install dependencies (npm ci)
|
||||
↓
|
||||
Build TypeScript
|
||||
↓
|
||||
Exit (bot auto-restarts if marker exists)
|
||||
```
|
||||
|
||||
## Previous System
|
||||
|
||||
The old update system (799 lines) supported two methods:
|
||||
- Git method (required Git, had merge conflicts)
|
||||
- GitHub API method
|
||||
|
||||
**New system**: Only GitHub API method (simpler, more reliable)
|
||||
|
||||
## Anti-Loop Protection
|
||||
|
||||
The bot has built-in protection against infinite restart loops:
|
||||
|
||||
1. **Marker detection**: Bot only restarts if `.update-happened` exists
|
||||
2. **Restart counter**: Max 3 restart attempts (`.update-restart-count`)
|
||||
3. **Counter cleanup**: Removed after successful run without updates
|
||||
4. **No-update detection**: Marker NOT created if already up to date
|
||||
|
||||
This ensures the bot never gets stuck in an infinite update loop.
|
||||
|
||||
## Dependencies
|
||||
|
||||
No external dependencies required! The update system uses only Node.js built-in modules:
|
||||
- `node:child_process` - Run shell commands
|
||||
- `node:fs` - File system operations
|
||||
- `node:https` - Download files
|
||||
- `node:path` - Path manipulation
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- `0` - Success (updated or already up to date)
|
||||
- `1` - Error (download failed, extraction failed, etc.)
|
||||
|
||||
## NPM Scripts
|
||||
|
||||
- `npm run start` - Start bot (runs update check first if enabled)
|
||||
- `npm run dev` - Start in dev mode (skips update check)
|
||||
- `npm run build` - Build TypeScript manually
|
||||
|
||||
## Version Info
|
||||
|
||||
- Current version: **v2** (GitHub API only)
|
||||
- Previous version: v1 (Dual Git/GitHub API)
|
||||
- Lines of code: **468** (down from 799)
|
||||
- Complexity: **Simple** (down from Complex)
|
||||
@@ -1,232 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Microsoft Rewards Bot - First-Time Setup Script
|
||||
*
|
||||
* This script handles initial project setup:
|
||||
* 1. Creates accounts.jsonc from template
|
||||
* 2. Guides user through account configuration
|
||||
* 3. Installs dependencies (npm install)
|
||||
* 4. Builds TypeScript project (npm run build)
|
||||
* 5. Installs Playwright Chromium browser
|
||||
*
|
||||
* IMPORTANT: This script does NOT launch the bot automatically.
|
||||
* After setup, run: npm start
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
||||
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
|
||||
|
||||
function log(msg) { console.log(msg); }
|
||||
function warn(msg) { console.warn(msg); }
|
||||
function error(msg) { console.error(msg); }
|
||||
|
||||
function createAccountsFile() {
|
||||
const accounts = path.join(SRC_DIR, 'accounts.jsonc');
|
||||
const example = path.join(SRC_DIR, 'accounts.example.jsonc');
|
||||
|
||||
if (fs.existsSync(accounts)) {
|
||||
log('✓ accounts.jsonc already exists - skipping creation');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fs.existsSync(example)) {
|
||||
log('📝 Creating accounts.jsonc from template...');
|
||||
fs.copyFileSync(example, accounts);
|
||||
log('✓ Created accounts.jsonc');
|
||||
return false;
|
||||
} else {
|
||||
error('❌ Template file accounts.example.jsonc not found!');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function prompt(question) {
|
||||
return await new Promise(resolve => {
|
||||
process.stdout.write(question);
|
||||
const onData = (data) => {
|
||||
const ans = data.toString().trim();
|
||||
process.stdin.off('data', onData);
|
||||
resolve(ans);
|
||||
};
|
||||
process.stdin.on('data', onData);
|
||||
});
|
||||
}
|
||||
|
||||
async function guideAccountConfiguration() {
|
||||
log('\n<> ACCOUNT CONFIGURATION');
|
||||
log('════════════════════════════════════════════════════════════');
|
||||
log('1. Open file: src/accounts.jsonc');
|
||||
log('2. Add your Microsoft account credentials:');
|
||||
log(' - email: Your Microsoft account email');
|
||||
log(' - password: Your account password');
|
||||
log(' - totp: (Optional) 2FA secret for automatic authentication');
|
||||
log('3. Enable accounts by setting "enabled": true');
|
||||
log('4. Save the file');
|
||||
log('');
|
||||
log('📚 Full guide: docs/accounts.md');
|
||||
log('════════════════════════════════════════════════════════════\n');
|
||||
|
||||
for (;;) {
|
||||
const ans = (await prompt('Have you configured your accounts? (yes/no): ')).toLowerCase();
|
||||
if (['yes', 'y'].includes(ans)) break;
|
||||
if (['no', 'n'].includes(ans)) {
|
||||
log('\n⏸️ Please configure src/accounts.jsonc and save it, then answer yes.\n');
|
||||
continue;
|
||||
}
|
||||
log('Please answer yes or no.');
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand(cmd, args, opts = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
log(`Running: ${cmd} ${args.join(' ')}`);
|
||||
const child = spawn(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts });
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) return resolve();
|
||||
reject(new Error(`${cmd} exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureNpmAvailable() {
|
||||
try {
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['-v']);
|
||||
} catch (e) {
|
||||
throw new Error('npm not found in PATH. Install Node.js first.');
|
||||
}
|
||||
}
|
||||
|
||||
async function performSetup() {
|
||||
log('\n🚀 MICROSOFT REWARDS BOT - FIRST-TIME SETUP');
|
||||
log('════════════════════════════════════════════════════════════\n');
|
||||
|
||||
// Step 1: Create accounts file
|
||||
const accountsExisted = createAccountsFile();
|
||||
|
||||
// Step 2: Guide user through account configuration
|
||||
if (!accountsExisted) {
|
||||
await guideAccountConfiguration();
|
||||
} else {
|
||||
log('✓ Using existing accounts.jsonc\n');
|
||||
}
|
||||
|
||||
// Step 3: Configuration guidance
|
||||
log('\n⚙️ CONFIGURATION (src/config.jsonc)');
|
||||
log('════════════════════════════════════════════════════════════');
|
||||
log('Key settings you may want to adjust:');
|
||||
log(' • browser.headless: false = visible browser, true = background');
|
||||
log(' • execution.clusters: Number of parallel accounts (default: 1)');
|
||||
log(' • workers: Enable/disable specific tasks');
|
||||
log(' • humanization.enabled: Add natural delays (recommended: true)');
|
||||
log(' • scheduling.enabled: Automate with OS scheduler');
|
||||
log('');
|
||||
log('📚 Full configuration guide: docs/getting-started.md');
|
||||
log('════════════════════════════════════════════════════════════\n');
|
||||
|
||||
const reviewConfig = (await prompt('Review config.jsonc before continuing? (yes/no): ')).toLowerCase();
|
||||
if (['yes', 'y'].includes(reviewConfig)) {
|
||||
log('\n⏸️ Setup paused.');
|
||||
log('Please review and edit src/config.jsonc, then run: npm run setup\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Step 4: Install dependencies
|
||||
log('\n📦 Installing dependencies...');
|
||||
await ensureNpmAvailable();
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install']);
|
||||
|
||||
// Step 5: Build TypeScript
|
||||
log('\n🔨 Building TypeScript project...');
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']);
|
||||
|
||||
// Step 6: Install Playwright browsers
|
||||
await installPlaywrightBrowsers();
|
||||
|
||||
// Final message
|
||||
log('\n');
|
||||
log('═══════════════════════════════════════════════════════════');
|
||||
log('✅ SETUP COMPLETE!');
|
||||
log('═══════════════════════════════════════════════════════════');
|
||||
log('');
|
||||
log('📁 Configuration files:');
|
||||
log(' • Accounts: src/accounts.jsonc');
|
||||
log(' • Config: src/config.jsonc');
|
||||
log('');
|
||||
log('📚 Documentation:');
|
||||
log(' • Getting started: docs/getting-started.md');
|
||||
log(' • Full docs: docs/index.md');
|
||||
log('');
|
||||
log('🚀 TO START THE BOT:');
|
||||
log(' npm start');
|
||||
log('');
|
||||
log('⏰ FOR AUTOMATED SCHEDULING:');
|
||||
log(' See: docs/getting-started.md (Scheduling section)');
|
||||
log('═══════════════════════════════════════════════════════════\n');
|
||||
}
|
||||
|
||||
async function installPlaywrightBrowsers() {
|
||||
const PLAYWRIGHT_MARKER = path.join(PROJECT_ROOT, '.playwright-chromium-installed');
|
||||
// Idempotent: skip if marker exists
|
||||
if (fs.existsSync(PLAYWRIGHT_MARKER)) {
|
||||
log('Playwright chromium already installed (marker found).');
|
||||
return;
|
||||
}
|
||||
log('Ensuring Playwright chromium browser is installed...');
|
||||
try {
|
||||
await runCommand(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['playwright', 'install', 'chromium']);
|
||||
fs.writeFileSync(PLAYWRIGHT_MARKER, new Date().toISOString());
|
||||
log('Playwright chromium install complete.');
|
||||
} catch (e) {
|
||||
warn('Failed to install Playwright chromium automatically. You can manually run: npx playwright install chromium');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(SRC_DIR)) {
|
||||
error('❌ Cannot find src directory at ' + SRC_DIR);
|
||||
process.exit(1);
|
||||
}
|
||||
process.chdir(PROJECT_ROOT);
|
||||
|
||||
// Check if already setup (dist exists and accounts configured)
|
||||
const distExists = fs.existsSync(path.join(PROJECT_ROOT, 'dist', 'index.js'));
|
||||
const accountsExists = fs.existsSync(path.join(SRC_DIR, 'accounts.jsonc'));
|
||||
|
||||
if (distExists && accountsExists) {
|
||||
log('\n⚠️ Setup appears to be already complete.');
|
||||
log(' • Build output: dist/index.js exists');
|
||||
log(' • Accounts: src/accounts.jsonc exists\n');
|
||||
|
||||
const rerun = (await prompt('Run setup anyway? (yes/no): ')).toLowerCase();
|
||||
if (!['yes', 'y'].includes(rerun)) {
|
||||
log('\n💡 To start the bot: npm start');
|
||||
log('💡 To rebuild: npm run build\n');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
await performSetup();
|
||||
|
||||
// Pause if launched by double-click on Windows
|
||||
if (process.platform === 'win32' && process.stdin.isTTY) {
|
||||
log('Press Enter to close...');
|
||||
await prompt('');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Allow clean Ctrl+C
|
||||
process.on('SIGINT', () => { console.log('\nInterrupted.'); process.exit(1); });
|
||||
|
||||
main().catch(err => {
|
||||
error('\nSetup failed: ' + err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,742 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Microsoft Rewards Bot - Automatic Update System
|
||||
*
|
||||
* Uses GitHub API to download latest code as ZIP archive.
|
||||
* No Git required, no merge conflicts, always clean.
|
||||
*
|
||||
* Features:
|
||||
* - Downloads latest code from GitHub (ZIP)
|
||||
* - Preserves user files (accounts, config, sessions)
|
||||
* - Selective file copying
|
||||
* - Automatic dependency installation
|
||||
* - TypeScript rebuild
|
||||
*
|
||||
* Usage:
|
||||
* node setup/update/update.mjs # Run update
|
||||
* npm run start # Bot runs this automatically if enabled
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
import { cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { get as httpsGet } from 'node:https'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Strip JSON comments
|
||||
*/
|
||||
function stripJsonComments(input) {
|
||||
let result = ''
|
||||
let inString = false
|
||||
let stringChar = ''
|
||||
let inLineComment = false
|
||||
let inBlockComment = false
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const char = input[i]
|
||||
const next = input[i + 1]
|
||||
|
||||
if (inLineComment) {
|
||||
if (char === '\n' || char === '\r') {
|
||||
inLineComment = false
|
||||
result += char
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (inBlockComment) {
|
||||
if (char === '*' && next === '/') {
|
||||
inBlockComment = false
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (inString) {
|
||||
result += char
|
||||
if (char === '\\') {
|
||||
i++
|
||||
if (i < input.length) result += input[i]
|
||||
continue
|
||||
}
|
||||
if (char === stringChar) inString = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
inString = true
|
||||
stringChar = char
|
||||
result += char
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '/' && next === '/') {
|
||||
inLineComment = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '/' && next === '*') {
|
||||
inBlockComment = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
result += char
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse JSON config file
|
||||
*/
|
||||
function readJsonConfig(preferredPaths) {
|
||||
for (const candidate of preferredPaths) {
|
||||
if (!existsSync(candidate)) continue
|
||||
try {
|
||||
const raw = readFileSync(candidate, 'utf8').replace(/^\uFEFF/, '')
|
||||
return JSON.parse(stripJsonComments(raw))
|
||||
} catch {
|
||||
// Try next candidate
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Run shell command
|
||||
*/
|
||||
function run(cmd, args, opts = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(cmd, args, {
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
...opts
|
||||
})
|
||||
child.on('close', (code) => resolve(code ?? 0))
|
||||
child.on('error', () => resolve(1))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if command exists
|
||||
*/
|
||||
async function which(cmd) {
|
||||
const probe = process.platform === 'win32' ? 'where' : 'which'
|
||||
const code = await run(probe, [cmd], { stdio: 'ignore' })
|
||||
return code === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file via HTTPS
|
||||
*/
|
||||
function downloadFile(url, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = createWriteStream(dest)
|
||||
|
||||
httpsGet(url, (response) => {
|
||||
// Handle redirects
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
file.close()
|
||||
rmSync(dest, { force: true })
|
||||
downloadFile(response.headers.location, dest).then(resolve).catch(reject)
|
||||
return
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
file.close()
|
||||
rmSync(dest, { force: true })
|
||||
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`))
|
||||
return
|
||||
}
|
||||
|
||||
response.pipe(file)
|
||||
file.on('finish', () => {
|
||||
file.close()
|
||||
resolve()
|
||||
})
|
||||
}).on('error', (err) => {
|
||||
file.close()
|
||||
rmSync(dest, { force: true })
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ZIP file (cross-platform)
|
||||
*/
|
||||
async function extractZip(zipPath, destDir) {
|
||||
// Try unzip (Unix-like)
|
||||
if (await which('unzip')) {
|
||||
const code = await run('unzip', ['-q', '-o', zipPath, '-d', destDir], { stdio: 'ignore' })
|
||||
if (code === 0) return
|
||||
}
|
||||
|
||||
// Try tar (modern Windows/Unix)
|
||||
if (await which('tar')) {
|
||||
const code = await run('tar', ['-xf', zipPath, '-C', destDir], { stdio: 'ignore' })
|
||||
if (code === 0) return
|
||||
}
|
||||
|
||||
// Try PowerShell Expand-Archive (Windows)
|
||||
if (process.platform === 'win32') {
|
||||
const code = await run('powershell', [
|
||||
'-Command',
|
||||
`Expand-Archive -Path "${zipPath}" -DestinationPath "${destDir}" -Force`
|
||||
], { stdio: 'ignore' })
|
||||
if (code === 0) return
|
||||
}
|
||||
|
||||
throw new Error('No extraction tool found (unzip, tar, or PowerShell required)')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ENVIRONMENT DETECTION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Detect if running inside a Docker container
|
||||
* Checks multiple indicators for accuracy
|
||||
*/
|
||||
function isDocker() {
|
||||
try {
|
||||
// Method 1: Check for /.dockerenv file (most reliable)
|
||||
if (existsSync('/.dockerenv')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Method 2: Check /proc/1/cgroup for docker
|
||||
if (existsSync('/proc/1/cgroup')) {
|
||||
const cgroupContent = readFileSync('/proc/1/cgroup', 'utf8')
|
||||
if (cgroupContent.includes('docker') || cgroupContent.includes('/kubepods/')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Check environment variables
|
||||
if (process.env.DOCKER === 'true' ||
|
||||
process.env.CONTAINER === 'docker' ||
|
||||
process.env.KUBERNETES_SERVICE_HOST) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Method 4: Check /proc/self/mountinfo for overlay filesystem
|
||||
if (existsSync('/proc/self/mountinfo')) {
|
||||
const mountinfo = readFileSync('/proc/self/mountinfo', 'utf8')
|
||||
if (mountinfo.includes('docker') || mountinfo.includes('overlay')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} catch {
|
||||
// If any error occurs (e.g., on Windows), assume not Docker
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine update mode based on config and environment
|
||||
*/
|
||||
function getUpdateMode(configData) {
|
||||
const dockerMode = configData?.update?.dockerMode || 'auto'
|
||||
|
||||
if (dockerMode === 'force-docker') {
|
||||
return 'docker'
|
||||
}
|
||||
|
||||
if (dockerMode === 'force-host') {
|
||||
return 'host'
|
||||
}
|
||||
|
||||
// Auto-detect
|
||||
return isDocker() ? 'docker' : 'host'
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN UPDATE LOGIC
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check if update is available by comparing versions
|
||||
* Returns true if versions differ (allows both upgrades and downgrades)
|
||||
*/
|
||||
async function checkVersion() {
|
||||
try {
|
||||
// Read local version
|
||||
const localPkgPath = join(process.cwd(), 'package.json')
|
||||
if (!existsSync(localPkgPath)) {
|
||||
console.log('⚠️ Could not find local package.json')
|
||||
return { updateAvailable: false, localVersion: 'unknown', remoteVersion: 'unknown' }
|
||||
}
|
||||
|
||||
const localPkg = JSON.parse(readFileSync(localPkgPath, 'utf8'))
|
||||
const localVersion = localPkg.version
|
||||
|
||||
// Fetch remote version from GitHub
|
||||
const repoOwner = 'Obsidian-wtf'
|
||||
const repoName = 'Microsoft-Rewards-Bot'
|
||||
const branch = 'main'
|
||||
|
||||
// Add cache-buster to prevent GitHub from serving stale cached version
|
||||
const cacheBuster = Date.now()
|
||||
const pkgUrl = `https://raw.githubusercontent.com/${repoOwner}/${repoName}/refs/heads/${branch}/package.json?cb=${cacheBuster}`
|
||||
|
||||
console.log('🔍 Checking for updates...')
|
||||
console.log(` Local: ${localVersion}`)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Request with cache-busting headers
|
||||
const options = {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
'User-Agent': 'Microsoft-Rewards-Bot-Updater'
|
||||
}
|
||||
}
|
||||
|
||||
const request = httpsGet(pkgUrl, options, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.log(` ⚠️ Could not check remote version (HTTP ${res.statusCode})`)
|
||||
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
|
||||
return
|
||||
}
|
||||
|
||||
let data = ''
|
||||
res.on('data', chunk => data += chunk)
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const remotePkg = JSON.parse(data)
|
||||
const remoteVersion = remotePkg.version
|
||||
console.log(` Remote: ${remoteVersion}`)
|
||||
|
||||
// Any difference triggers update (upgrade or downgrade)
|
||||
const updateAvailable = localVersion !== remoteVersion
|
||||
resolve({ updateAvailable, localVersion, remoteVersion })
|
||||
} catch (err) {
|
||||
console.log(` ⚠️ Could not parse remote package.json: ${err.message}`)
|
||||
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 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}`)
|
||||
return { updateAvailable: false, localVersion: 'unknown', remoteVersion: 'unknown' }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform update using GitHub API (ZIP download)
|
||||
*/
|
||||
async function performUpdate() {
|
||||
// Step 0: Check if update is needed by comparing versions
|
||||
const versionCheck = await checkVersion()
|
||||
|
||||
if (!versionCheck.updateAvailable) {
|
||||
console.log(`✅ Already up to date (v${versionCheck.localVersion})`)
|
||||
return 0 // Exit without creating update marker
|
||||
}
|
||||
|
||||
// Step 0.5: Detect environment and determine update mode
|
||||
const configData = readJsonConfig([
|
||||
'src/config.jsonc',
|
||||
'config.jsonc',
|
||||
'src/config.json',
|
||||
'config.json'
|
||||
])
|
||||
|
||||
const updateMode = getUpdateMode(configData)
|
||||
const envIcon = updateMode === 'docker' ? '🐳' : '💻'
|
||||
|
||||
console.log(`\n📦 Update available: ${versionCheck.localVersion} → ${versionCheck.remoteVersion}`)
|
||||
console.log(`${envIcon} Environment: ${updateMode === 'docker' ? 'Docker container' : 'Host system'}`)
|
||||
console.log('⏳ Updating... (this may take a moment)\n')
|
||||
|
||||
// Step 1: Read user preferences (silent)
|
||||
const userConfig = {
|
||||
autoUpdateConfig: configData?.update?.autoUpdateConfig ?? false,
|
||||
autoUpdateAccounts: configData?.update?.autoUpdateAccounts ?? false
|
||||
}
|
||||
|
||||
// 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 },
|
||||
{ path: 'src/accounts.jsonc', protect: !userConfig.autoUpdateAccounts },
|
||||
{ path: 'src/accounts.json', protect: !userConfig.autoUpdateAccounts },
|
||||
{ path: 'sessions', protect: true, isDir: true },
|
||||
{ path: '.playwright-chromium-installed', protect: true }
|
||||
]
|
||||
|
||||
const backedUp = []
|
||||
for (const file of filesToProtect) {
|
||||
if (!file.protect) continue
|
||||
const srcPath = join(process.cwd(), file.path)
|
||||
if (!existsSync(srcPath)) continue
|
||||
|
||||
const destPath = join(backupDir, file.path)
|
||||
mkdirSync(dirname(destPath), { recursive: true })
|
||||
|
||||
try {
|
||||
if (file.isDir) {
|
||||
cpSync(srcPath, destPath, { recursive: true })
|
||||
} else {
|
||||
writeFileSync(destPath, readFileSync(srcPath))
|
||||
}
|
||||
backedUp.push(file)
|
||||
} 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
|
||||
process.stdout.write('📥 Downloading...')
|
||||
const repoOwner = 'Obsidian-wtf'
|
||||
const repoName = 'Microsoft-Rewards-Bot'
|
||||
const branch = 'main'
|
||||
const archiveUrl = `https://github.com/${repoOwner}/${repoName}/archive/refs/heads/${branch}.zip`
|
||||
|
||||
const archivePath = join(process.cwd(), '.update-download.zip')
|
||||
const extractDir = join(process.cwd(), '.update-extract')
|
||||
|
||||
try {
|
||||
await downloadFile(archiveUrl, archivePath)
|
||||
process.stdout.write(' ✓\n')
|
||||
} catch (err) {
|
||||
console.log(` ❌\n❌ Download failed: ${err.message}`)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Step 4: Extract archive
|
||||
process.stdout.write('📂 Extracting...')
|
||||
rmSync(extractDir, { recursive: true, force: true })
|
||||
mkdirSync(extractDir, { recursive: true })
|
||||
|
||||
try {
|
||||
await extractZip(archivePath, extractDir)
|
||||
process.stdout.write(' ✓\n')
|
||||
} catch (err) {
|
||||
console.log(` ❌\n❌ Extraction failed: ${err.message}`)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Step 5: Find extracted folder
|
||||
const extractedItems = readdirSync(extractDir)
|
||||
const extractedRepoDir = extractedItems.find(item => item.startsWith(repoName))
|
||||
if (!extractedRepoDir) {
|
||||
console.log('\n❌ Could not find extracted repository folder')
|
||||
return 1
|
||||
}
|
||||
|
||||
const sourceDir = join(extractDir, extractedRepoDir)
|
||||
|
||||
// Step 6: Copy files selectively
|
||||
process.stdout.write('📦 Updating files...')
|
||||
const itemsToUpdate = [
|
||||
'src',
|
||||
'docs',
|
||||
'setup',
|
||||
'public',
|
||||
'tests',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'tsconfig.json',
|
||||
'Dockerfile',
|
||||
'compose.yaml',
|
||||
'entrypoint.sh',
|
||||
'run.sh',
|
||||
'README.md',
|
||||
'LICENSE'
|
||||
]
|
||||
|
||||
for (const item of itemsToUpdate) {
|
||||
const srcPath = join(sourceDir, item)
|
||||
const destPath = join(process.cwd(), item)
|
||||
|
||||
if (!existsSync(srcPath)) continue
|
||||
|
||||
// Skip protected items
|
||||
const isProtected = backedUp.some(f => f.path === item || destPath.includes(f.path))
|
||||
if (isProtected) continue
|
||||
|
||||
try {
|
||||
if (existsSync(destPath)) {
|
||||
rmSync(destPath, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
if (statSync(srcPath).isDirectory()) {
|
||||
cpSync(srcPath, destPath, { recursive: true })
|
||||
} else {
|
||||
cpSync(srcPath, destPath)
|
||||
}
|
||||
} catch {
|
||||
// Silent failure - continue
|
||||
}
|
||||
}
|
||||
process.stdout.write(' ✓\n')
|
||||
|
||||
// Step 7: Restore protected files (silent)
|
||||
if (backedUp.length > 0) {
|
||||
for (const file of backedUp) {
|
||||
const backupPath = join(backupDir, file.path)
|
||||
if (!existsSync(backupPath)) continue
|
||||
|
||||
const destPath = join(process.cwd(), file.path)
|
||||
mkdirSync(dirname(destPath), { recursive: true })
|
||||
|
||||
try {
|
||||
if (file.isDir) {
|
||||
rmSync(destPath, { recursive: true, force: true })
|
||||
cpSync(backupPath, destPath, { recursive: true })
|
||||
} else {
|
||||
writeFileSync(destPath, readFileSync(backupPath))
|
||||
}
|
||||
} catch {
|
||||
// Silent failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: Cleanup temporary files (silent)
|
||||
rmSync(archivePath, { force: true })
|
||||
rmSync(extractDir, { recursive: true, force: true })
|
||||
rmSync(backupDir, { recursive: true, force: true })
|
||||
|
||||
// Step 9: Create update marker for bot restart detection
|
||||
const updateMarkerPath = join(process.cwd(), '.update-happened')
|
||||
writeFileSync(updateMarkerPath, JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
fromVersion: versionCheck.localVersion,
|
||||
toVersion: versionCheck.remoteVersion,
|
||||
method: 'github-api'
|
||||
}, null, 2))
|
||||
|
||||
// Step 10: Install dependencies & rebuild
|
||||
const hasNpm = await which('npm')
|
||||
if (!hasNpm) {
|
||||
console.log('⚠️ npm not found - please run: npm install && npm run build')
|
||||
return 0
|
||||
}
|
||||
|
||||
process.stdout.write('📦 Installing dependencies...')
|
||||
const installCode = await run('npm', ['ci', '--silent'], { stdio: 'ignore' })
|
||||
if (installCode !== 0) {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
|
||||
console.log(`\n✅ Updated successfully! (${versionCheck.localVersion} → ${versionCheck.remoteVersion})`)
|
||||
|
||||
// Different behavior for Docker vs Host
|
||||
if (updateMode === 'docker') {
|
||||
console.log('<27> Docker mode: Update complete')
|
||||
console.log(' Container will restart automatically if configured\n')
|
||||
// In Docker, don't restart - let orchestrator handle it
|
||||
// Just exit cleanly so Docker can restart the container
|
||||
return 0
|
||||
} else {
|
||||
console.log('<27>🔄 Restarting in same process...\n')
|
||||
// In host mode, signal restart needed
|
||||
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([
|
||||
'src/config.jsonc',
|
||||
'config.jsonc',
|
||||
'src/config.json',
|
||||
'config.json'
|
||||
])
|
||||
|
||||
if (configData?.update?.enabled === false) {
|
||||
console.log('\n⚠️ Updates are disabled in config.jsonc')
|
||||
console.log('To enable: set "update.enabled" to true in src/config.jsonc\n')
|
||||
return 0
|
||||
}
|
||||
|
||||
// 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('\nCleaning up and reverting...')
|
||||
cleanup()
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user