diff --git a/.gitignore b/.gitignore index a5f17dc..4c1fd67 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ accounts.main.jsonc .playwright-chromium-installed *.log .update-backup/ +.update-download.zip +.update-extract/ +.update-happened +.update-restart-count diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..a424496 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,362 @@ +# NPM Commands Reference + +This guide explains all available npm commands and when to use them. + +## πŸš€ Essential Commands + +### `npm start` +**Start the bot** - Use this to run the Microsoft Rewards Bot after setup. + +```bash +npm start +``` + +**What it does:** +- Runs the compiled JavaScript from `dist/index.js` +- Checks for automatic updates (if enabled) +- Executes reward earning tasks +- Fastest way to run the bot + +**When to use:** +- Daily bot execution +- After setup is complete +- When you want to earn points + +--- + +### `npm run setup` +**First-time installation** - Run this only once when setting up the project. + +```bash +npm run setup +``` + +**What it does:** +1. Creates `accounts.jsonc` from template +2. Guides you through account configuration +3. Installs dependencies (`npm install`) +4. Builds TypeScript (`npm run build`) +5. Installs Playwright browsers + +**When to use:** +- First time installing the bot +- After fresh git clone +- To reconfigure accounts + +**Important:** This does NOT start the bot automatically. After setup, run `npm start`. + +--- + +## πŸ”¨ Development Commands + +### `npm run build` +**Build TypeScript to JavaScript** - Compiles the project. + +```bash +npm run build +``` + +**What it does:** +- Compiles `src/*.ts` files to `dist/*.js` +- Generates source maps for debugging +- Required before running `npm start` + +**When to use:** +- After modifying TypeScript source code +- Before starting the bot with `npm start` +- After pulling updates from git + +--- + +### `npm run dev` +**Development mode** - Run TypeScript directly without building. + +```bash +npm run dev +``` + +**What it does:** +- Runs TypeScript files directly with `ts-node` +- No build step required +- Slower but convenient for development +- Includes `-dev` flag for debug features + +**When to use:** +- During development/testing +- When making code changes +- Quick testing without full build + +--- + +### `npm run ts-start` +**TypeScript direct execution** - Like `dev` but without debug flags. + +```bash +npm run ts-start +``` + +**When to use:** +- Alternative to `npm run dev` +- Running TypeScript without full build + +--- + +## 🧹 Maintenance Commands + +### `npm run clean` +**Remove build artifacts** - Deletes the `dist` folder. + +```bash +npm run clean +``` + +**When to use:** +- Before fresh rebuild +- To clear stale compiled code +- Troubleshooting build issues + +--- + +### `npm run install-deps` +**Install all dependencies** - Fresh installation of dependencies and browsers. + +```bash +npm run install-deps +``` + +**What it does:** +- Runs `npm install` to install Node.js packages +- Installs Playwright Chromium browser + +**When to use:** +- After deleting `node_modules` +- Setting up on new machine +- Troubleshooting dependency issues + +--- + +### `npm run typecheck` +**Check TypeScript types** - Validates code without building. + +```bash +npm run typecheck +``` + +**When to use:** +- Checking for type errors +- Before committing code +- Part of CI/CD pipeline + +--- + +## πŸ§ͺ Testing & Quality + +### `npm test` +**Run unit tests** - Execute test suite. + +```bash +npm test +``` + +**When to use:** +- Verifying code changes +- Before submitting pull requests +- Continuous integration + +--- + +### `npm run lint` +**Check code style** - ESLint validation. + +```bash +npm run lint +``` + +**When to use:** +- Checking code formatting +- Before commits +- Maintaining code quality + +--- + +## πŸ“Š Dashboard Commands + +### `npm run dashboard` +**Start web dashboard only** - Web interface without bot execution. + +```bash +npm run dashboard +``` + +**What it does:** +- Launches web interface on http://localhost:3000 +- Provides monitoring and control panel +- Does NOT start reward earning + +**When to use:** +- Monitoring bot status +- Viewing logs remotely +- Configuring settings via UI + +--- + +### `npm run dashboard-dev` +**Dashboard development mode** - TypeScript version of dashboard. + +```bash +npm run dashboard-dev +``` + +**When to use:** +- Dashboard development/testing +- Quick dashboard testing without build + +--- + +## πŸ€– Account Creation + +### `npm run creator` +**Account creation wizard** - Create new Microsoft accounts. + +```bash +npm run creator +``` + +**When to use:** +- Creating new Microsoft accounts +- Bulk account creation +- Testing account setup + +--- + +## 🐳 Docker Commands + +### `npm run create-docker` +**Build Docker image** - Create containerized version. + +```bash +npm run create-docker +``` + +**When to use:** +- Deploying with Docker +- Creating container image +- Testing Docker setup + +--- + +## πŸ†˜ Troubleshooting Commands + +### `npm run kill-chrome-win` (Windows only) +**Force close Chrome browsers** - Kill stuck browser processes. + +```bash +npm run kill-chrome-win +``` + +**When to use:** +- Browser processes stuck +- Windows only +- Before restarting bot + +--- + +## πŸ“ Command Comparison + +| Command | Speed | Purpose | When to Use | +|---------|-------|---------|-------------| +| `npm start` | ⚑ Fast | Run bot | Daily use | +| `npm run dev` | 🐌 Slow | Development | Code changes | +| `npm run build` | ⏱️ Medium | Compile TS | Before start | +| `npm run setup` | ⏱️ Medium | First install | Once only | + +--- + +## Common Workflows + +### First-Time Setup +```bash +# 1. Run setup wizard +npm run setup + +# 2. Start the bot +npm start +``` + +### Daily Usage +```bash +npm start +``` + +### After Code Changes +```bash +# Method 1: Build then run (faster) +npm run build +npm start + +# Method 2: Direct run (slower) +npm run dev +``` + +### After Pulling Updates +```bash +# If dependencies changed +npm install + +# Rebuild +npm run build + +# Start bot +npm start +``` + +### Troubleshooting +```bash +# Clean install +npm run clean +rm -rf node_modules package-lock.json +npm run install-deps + +# Rebuild +npm run build + +# Test +npm start +``` + +--- + +## ❓ FAQ + +### Why does `npm run start` trigger updates? +The bot automatically checks for updates on startup (configurable in `config.jsonc`). To disable: +```jsonc +{ + "update": { + "enabled": false + } +} +``` + +### What's the difference between `npm start` and `npm run start`? +**No functional difference** - both run the same command. `npm start` is a shorthand for `npm run start`. + +### Should I use `npm start` or `npm run dev`? +- **Production/Daily use:** `npm start` (faster) +- **Development:** `npm run dev` (no build needed) + +### How do I completely reset the project? +```bash +npm run clean +rm -rf node_modules package-lock.json dist +npm run setup +``` + +--- + +## Need Help? + +- **Getting Started:** [docs/getting-started.md](getting-started.md) +- **Configuration:** [docs/config.md](config.md) +- **Troubleshooting:** [docs/troubleshooting.md](troubleshooting.md) +- **Discord:** https://discord.gg/k5uHkx9mne diff --git a/package.json b/package.json index da92fcb..3d15372 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,10 @@ "homepage": "https://github.com/Obsidian-wtf/Microsoft-Rewards-Bot#readme", "scripts": { "clean": "rimraf dist", - "pre-build": "npm i && npm run clean && node -e \"process.exit(process.env.SKIP_PLAYWRIGHT_INSTALL?0:1)\" || npx playwright install chromium", + "install-deps": "npm install && npx playwright install chromium", "typecheck": "tsc --noEmit", "build": "tsc", + "postbuild": "node -e \"console.log('\\nβœ… Build complete! Run \\\"npm start\\\" to launch the bot.\\n')\"", "test": "node --test --loader ts-node/esm tests/**/*.test.ts", "start": "node --enable-source-maps ./dist/index.js", "ts-start": "node --loader ts-node/esm ./src/index.ts", @@ -28,7 +29,6 @@ "dashboard": "node --enable-source-maps ./dist/index.js -dashboard", "dashboard-dev": "ts-node ./src/index.ts -dashboard", "lint": "eslint \"src/**/*.{ts,tsx}\"", - "prepare": "npm run build", "setup": "node ./setup/update/setup.mjs", "kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"", "create-docker": "docker build -t microsoft-rewards-bot ." diff --git a/setup/README.md b/setup/README.md new file mode 100644 index 0000000..e65bf14 --- /dev/null +++ b/setup/README.md @@ -0,0 +1,123 @@ +# Setup Scripts + +This folder contains setup and update scripts for the Microsoft Rewards Bot. + +## Files + +### setup.bat / setup.sh +**First-time installation scripts** for Windows (.bat) and Linux/macOS (.sh). + +**What they do:** +1. Check prerequisites (Node.js, npm) +2. Create `accounts.jsonc` from template +3. Guide you through account configuration +4. Install dependencies (`npm install`) +5. Build TypeScript project (`npm run build`) +6. Install Playwright Chromium browser + +**Usage:** +```bash +# Windows +.\setup\setup.bat + +# Linux/macOS +./setup/setup.sh +``` + +**Important:** These scripts do NOT start the bot automatically. After setup, run: +```bash +npm start +``` + +### update/update.mjs +**Automatic update script** that keeps your bot up-to-date with the latest version. + +**Features:** +- Two update methods: Git-based or GitHub API (no Git needed) +- Preserves your configuration and accounts +- No merge conflicts with GitHub API method +- Automatic dependency installation and rebuild + +**Usage:** +```bash +# Auto-detect method from config.jsonc +node setup/update/update.mjs + +# Force GitHub API method (recommended) +node setup/update/update.mjs --no-git + +# Force Git method +node setup/update/update.mjs --git +``` + +**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. + +## Quick Start Guide + +### First-time setup: + +**Windows:** +```batch +.\setup\setup.bat +``` + +**Linux/macOS:** +```bash +chmod +x setup/setup.sh +./setup/setup.sh +``` + +### Daily usage: + +```bash +# Start the bot +npm start + +# Start with TypeScript (development) +npm run dev + +# View dashboard +npm run dashboard +``` + +### Configuration: + +- **Accounts:** Edit `src/accounts.jsonc` +- **Settings:** Edit `src/config.jsonc` +- **Documentation:** See `docs/` folder + +## Troubleshooting + +### "npm not found" +Install Node.js from https://nodejs.org/ (v20 or newer recommended) + +### "Setup failed" +1. Delete `node_modules` folder +2. Delete `package-lock.json` file +3. Run setup again + +### "Build failed" +```bash +npm run clean +npm run build +``` + +### Update issues +If automatic updates fail, manually update: +```bash +git pull origin main +npm install +npm run build +``` + +## Need Help? + +- **Documentation:** `docs/index.md` +- **Getting Started:** `docs/getting-started.md` +- **Troubleshooting:** `docs/troubleshooting.md` +- **Discord:** https://discord.gg/k5uHkx9mne diff --git a/setup/setup.bat b/setup/setup.bat index 1ea6de2..af184f8 100644 --- a/setup/setup.bat +++ b/setup/setup.bat @@ -1,25 +1,76 @@ @echo off -setlocal -REM Wrapper to run setup via npm (Windows) -REM Navigates to project root and runs npm run setup +setlocal EnableDelayedExpansion + +REM ======================================== +REM Microsoft Rewards Bot - Setup (Windows) +REM ======================================== +REM This script performs first-time setup: +REM 1. Check prerequisites (Node.js, npm) +REM 2. Run setup wizard (accounts + config) +REM 3. Install dependencies +REM 4. Build TypeScript project +REM +REM After setup, run the bot with: npm start +REM ======================================== set SCRIPT_DIR=%~dp0 set PROJECT_ROOT=%SCRIPT_DIR%.. -if not exist "%PROJECT_ROOT%\package.json" ( - echo [ERROR] package.json not found in project root. +echo. +echo ======================================== +echo Microsoft Rewards Bot - Setup +echo ======================================== +echo. + +REM Check if Node.js/npm are installed +where npm >nul 2>nul +if errorlevel 1 ( + echo [ERROR] npm not found! + echo. + echo Please install Node.js from: https://nodejs.org/ + echo Recommended version: v20 or newer + echo. pause exit /b 1 ) -echo Navigating to project root... +for /f "tokens=*" %%i in ('npm -v 2^>nul') do set NPM_VERSION=%%i +echo [OK] npm detected: v!NPM_VERSION! +echo. + +REM Check if package.json exists +if not exist "%PROJECT_ROOT%\package.json" ( + echo [ERROR] package.json not found in project root. + echo Current directory: %CD% + echo Project root: %PROJECT_ROOT% + echo. + pause + exit /b 1 +) + +REM Navigate to project root cd /d "%PROJECT_ROOT%" -echo Running setup script via npm... +REM Run setup script +echo Running setup wizard... +echo. call npm run setup set EXITCODE=%ERRORLEVEL% + echo. -echo Setup finished with exit code %EXITCODE%. -echo Press Enter to close. -pause >NUL +if %EXITCODE% EQU 0 ( + echo ======================================== + echo Setup Complete! + echo ======================================== + echo. + echo To start the bot: npm start + echo. +) else ( + echo ======================================== + echo Setup Failed ^(Exit Code: %EXITCODE%^) + echo ======================================== + echo. +) + +pause exit /b %EXITCODE% \ No newline at end of file diff --git a/setup/setup.sh b/setup/setup.sh index 759d8cc..589b5c8 100644 --- a/setup/setup.sh +++ b/setup/setup.sh @@ -1,35 +1,84 @@ #!/usr/bin/env bash set -euo pipefail -# Wrapper to run setup via npm (Linux/macOS) +# ======================================== +# Microsoft Rewards Bot - Setup (Linux/macOS) +# ======================================== +# This script performs first-time setup: +# 1. Check prerequisites (Node.js, npm, Git) +# 2. Run setup wizard (accounts + config) +# 3. Install dependencies +# 4. Build TypeScript project +# +# After setup, run the bot with: npm start +# ======================================== + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -echo "=== Prerequisite Check ===" +echo "" +echo "========================================" +echo " Microsoft Rewards Bot - Setup" +echo "========================================" +echo "" + +# Check prerequisites +echo "=== Prerequisites Check ===" +echo "" if command -v npm >/dev/null 2>&1; then - NPM_VERSION="$(npm -v 2>/dev/null || true)" - echo "npm detected: ${NPM_VERSION}" + NPM_VERSION="$(npm -v 2>/dev/null || echo 'unknown')" + echo "[OK] npm detected: v${NPM_VERSION}" else - echo "[ERROR] npm not detected." - echo " Install Node.js and npm from nodejs.org or your package manager" + echo "[ERROR] npm not found!" + echo "" + echo "Please install Node.js from: https://nodejs.org/" + echo "Recommended version: v20 or newer" + echo "" + echo "Alternatively, use your package manager:" + echo " β€’ Ubuntu/Debian: sudo apt install nodejs npm" + echo " β€’ macOS: brew install node" + echo " β€’ Fedora: sudo dnf install nodejs npm" exit 1 fi if command -v git >/dev/null 2>&1; then - GIT_VERSION="$(git --version 2>/dev/null || true)" - echo "Git detected: ${GIT_VERSION}" + GIT_VERSION="$(git --version 2>/dev/null | cut -d' ' -f3)" + echo "[OK] Git detected: v${GIT_VERSION}" else - echo "[WARN] Git not detected." - echo " Install (Linux): e.g. 'sudo apt install git' (or your distro equivalent)." + echo "[WARN] Git not detected (optional for setup, required for updates)" + echo " β€’ Ubuntu/Debian: sudo apt install git" + echo " β€’ macOS: brew install git" + echo " β€’ Fedora: sudo dnf install git" fi if [ ! -f "${PROJECT_ROOT}/package.json" ]; then + echo "" echo "[ERROR] package.json not found at ${PROJECT_ROOT}" >&2 exit 1 fi -echo -echo "=== Running setup script via npm ===" +echo "" +echo "=== Running Setup Wizard ===" +echo "" + cd "${PROJECT_ROOT}" -exec npm run setup +npm run setup +EXITCODE=$? + +echo "" +if [ $EXITCODE -eq 0 ]; then + echo "========================================" + echo " Setup Complete!" + echo "========================================" + echo "" + echo "To start the bot: npm start" + echo "" +else + echo "========================================" + echo " Setup Failed (Exit Code: $EXITCODE)" + echo "========================================" + echo "" +fi + +exit $EXITCODE diff --git a/setup/update/README.md b/setup/update/README.md new file mode 100644 index 0000000..e785909 --- /dev/null +++ b/setup/update/README.md @@ -0,0 +1,175 @@ +# 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) diff --git a/setup/update/setup.mjs b/setup/update/setup.mjs index ae736d8..823c036 100644 --- a/setup/update/setup.mjs +++ b/setup/update/setup.mjs @@ -1,31 +1,25 @@ #!/usr/bin/env node /** - * Unified cross-platform setup script for Microsoft Rewards Script V2. + * Microsoft Rewards Bot - First-Time Setup Script * - * Features: - * - Renames accounts.example.jsonc -> accounts.json (idempotent) - * - Guides user through account configuration (email, password, TOTP, proxy) - * - Explains config.jsonc structure and key settings - * - Installs dependencies (npm install) - * - Builds TypeScript project (npm run build) - * - Installs Playwright Chromium browser (idempotent with marker) - * - Optional immediate start or manual start instructions + * 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 * - * V2 Updates: - * - Enhanced prompts for new config.jsonc structure - * - Explains humanization, scheduling, notifications - * - References updated documentation (docs/config.md, docs/accounts.md) - * - Improved user guidance for first-time setup + * 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'; -import { spawn } from 'child_process'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Project root = two levels up from setup/update directory const PROJECT_ROOT = path.resolve(__dirname, '..', '..'); const SRC_DIR = path.join(PROJECT_ROOT, 'src'); @@ -33,18 +27,23 @@ function log(msg) { console.log(msg); } function warn(msg) { console.warn(msg); } function error(msg) { console.error(msg); } -function renameAccountsIfNeeded() { - const accounts = path.join(SRC_DIR, 'accounts.json'); +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.json already exists - skipping rename.'); - return; + log('βœ“ accounts.jsonc already exists - skipping creation'); + return true; } + if (fs.existsSync(example)) { - log('Renaming accounts.example.jsonc to accounts.json...'); - fs.renameSync(example, accounts); + log('πŸ“ Creating accounts.jsonc from template...'); + fs.copyFileSync(example, accounts); + log('βœ“ Created accounts.jsonc'); + return false; } else { - warn('Neither accounts.json nor accounts.example.jsonc found.'); + error('❌ Template file accounts.example.jsonc not found!'); + return true; } } @@ -60,20 +59,25 @@ async function prompt(question) { }); } -async function loopForAccountsConfirmation() { - log('\nπŸ“ Please configure your Microsoft accounts:'); - log(' - Open: src/accounts.json'); - log(' - Add your email and password for each account'); - log(' - Optional: Add TOTP secret for 2FA (see docs/accounts.md)'); - log(' - Optional: Configure proxy settings per account'); - log(' - Save the file (Ctrl+S or Cmd+S)\n'); +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'); - // Keep asking until user says yes for (;;) { - const ans = (await prompt('Have you configured your accounts in accounts.json? (yes/no): ')).toLowerCase(); + const ans = (await prompt('Have you configured your accounts? (yes/no): ')).toLowerCase(); if (['yes', 'y'].includes(ans)) break; if (['no', 'n'].includes(ans)) { - log('Please configure accounts.json and save the file, then answer yes.'); + log('\n⏸️ Please configure src/accounts.jsonc and save it, then answer yes.\n'); continue; } log('Please answer yes or no.'); @@ -99,64 +103,72 @@ async function ensureNpmAvailable() { } } -async function startOnly() { - log('Starting program (npm run start)...'); - await ensureNpmAvailable(); - // Assume user already installed & built; if dist missing inform user. - const distIndex = path.join(PROJECT_ROOT, 'dist', 'index.js'); - if (!fs.existsSync(distIndex)) { - warn('Build output not found. Running build first.'); - await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']); - await installPlaywrightBrowsers(); +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 { - // Even if build exists, ensure browsers are installed once. - await installPlaywrightBrowsers(); + log('βœ“ Using existing accounts.jsonc\n'); } - await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'start']); -} - -async function fullSetup() { - renameAccountsIfNeeded(); - await loopForAccountsConfirmation(); - log('\nβš™οΈ Configuration Options (src/config.jsonc):'); - log(' - browser.headless: Set to true for background operation'); - log(' - execution.clusters: Number of parallel account processes'); - log(' - workers: Enable/disable specific tasks (dailySet, searches, etc.)'); - log(' - humanization: Add natural delays and behavior (recommended: enabled)'); - log(' - schedule: Configure automated daily runs'); - log(' - notifications: Discord webhooks, NTFY push alerts'); - log(' πŸ“š Full guide: docs/config.md\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('Do you want to review config.jsonc now? (yes/no): ')).toLowerCase(); + const reviewConfig = (await prompt('Review config.jsonc before continuing? (yes/no): ')).toLowerCase(); if (['yes', 'y'].includes(reviewConfig)) { - log('⏸️ Setup paused. Please review src/config.jsonc, then re-run this setup.'); - log(' Common settings to check:'); - log(' - browser.headless (false = visible browser, true = background)'); - log(' - execution.runOnZeroPoints (false = skip when no points available)'); - log(' - humanization.enabled (true = natural behavior, recommended)'); - log(' - schedule.enabled (false = manual runs, true = automated scheduling)'); - log('\n After editing config.jsonc, run: npm run setup'); + 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(); - log('\nβœ… Setup complete!'); - log(' - Accounts configured: src/accounts.json'); - log(' - Configuration: src/config.jsonc'); - log(' - Documentation: docs/index.md\n'); - - const start = (await prompt('Do you want to start the automation now? (yes/no): ')).toLowerCase(); - if (['yes', 'y'].includes(start)) { - await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'start']); - } else { - log('\nFinished setup. To start later, run: npm start'); - log('For automated scheduling, use your OS scheduler (see docs/schedule.md).'); - } + // 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() { @@ -178,30 +190,36 @@ async function installPlaywrightBrowsers() { async function main() { if (!fs.existsSync(SRC_DIR)) { - error('[ERROR] Cannot find src directory at ' + SRC_DIR); + error('❌ Cannot find src directory at ' + SRC_DIR); process.exit(1); } process.chdir(PROJECT_ROOT); - for (;;) { - log('============================'); - log(' Microsoft Rewards Setup '); - log('============================'); - log('Select an option:'); - log(' 1) Start program now (skip setup)'); - log(' 2) Full first-time setup'); - log(' 3) Exit'); - const choice = (await prompt('Enter choice (1/2/3): ')).trim(); - if (choice === '1') { await startOnly(); break; } - if (choice === '2') { await fullSetup(); break; } - if (choice === '3') { log('Exiting.'); process.exit(0); } - log('\nInvalid choice. Please select 1, 2 or 3.\n'); + // 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); + } } - // After completing action, optionally pause if launched by double click on Windows (no TTY detection simple heuristic) + + await performSetup(); + + // Pause if launched by double-click on Windows if (process.platform === 'win32' && process.stdin.isTTY) { - log('\nDone. Press Enter to close.'); + log('Press Enter to close...'); await prompt(''); } + process.exit(0); } diff --git a/setup/update/update.mjs b/setup/update/update.mjs index 8d44056..6b56a7c 100644 --- a/setup/update/update.mjs +++ b/setup/update/update.mjs @@ -1,32 +1,38 @@ -/* eslint-disable linebreak-style */ +#!/usr/bin/env node /** - * Smart Auto-Update Script v2 + * Microsoft Rewards Bot - Automatic Update System * - * Supports two update methods: - * 1. Git method (--git): Uses Git commands, requires Git installed - * 2. GitHub API method (--no-git): Downloads ZIP, no Git needed, no conflicts (RECOMMENDED) + * 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 * - * Intelligently updates while preserving user settings: - * - ALWAYS updates code files (*.ts, *.js, etc.) - * - Respects config.jsonc update preferences - * - ALWAYS preserves accounts files (unless explicitly configured) - * * Usage: - * node setup/update/update.mjs # Auto-detect method from config - * node setup/update/update.mjs --git # Force Git method - * node setup/update/update.mjs --no-git # Force GitHub API method - * node setup/update/update.mjs --docker # Update Docker containers + * node setup/update/update.mjs # Run update + * npm run start # Bot runs this automatically if enabled */ -import { execSync, spawn } from 'node:child_process' +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 result = '' let inString = false - let stringChar = "" + let stringChar = '' let inLineComment = false let inBlockComment = false @@ -35,7 +41,7 @@ function stripJsonComments(input) { const next = input[i + 1] if (inLineComment) { - if (char === "\n" || char === "\r") { + if (char === '\n' || char === '\r') { inLineComment = false result += char } @@ -43,7 +49,7 @@ function stripJsonComments(input) { } if (inBlockComment) { - if (char === "*" && next === "/") { + if (char === '*' && next === '/') { inBlockComment = false i++ } @@ -52,7 +58,7 @@ function stripJsonComments(input) { if (inString) { result += char - if (char === "\\") { + if (char === '\\') { i++ if (i < input.length) result += input[i] continue @@ -61,20 +67,20 @@ function stripJsonComments(input) { continue } - if (char === "\"" || char === "'") { + if (char === '"' || char === "'") { inString = true stringChar = char result += char continue } - if (char === "/" && next === "/") { + if (char === '/' && next === '/') { inLineComment = true i++ continue } - if (char === "/" && next === "*") { + if (char === '/' && next === '*') { inBlockComment = true i++ continue @@ -86,492 +92,48 @@ function stripJsonComments(input) { 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/, "") + const raw = readFileSync(candidate, 'utf8').replace(/^\uFEFF/, '') return JSON.parse(stripJsonComments(raw)) } catch { - // Try next candidate on parse errors + // 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 }) + 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 } -function exec(cmd) { - try { - return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() - } catch { - return null - } -} - -function hasUnresolvedConflicts() { - // Check for unmerged files - const unmerged = exec('git ls-files -u') - if (unmerged) { - return { hasConflicts: true, files: unmerged.split('\n').filter(Boolean) } - } - - // Check if in middle of merge/rebase - const gitDir = exec('git rev-parse --git-dir') - if (gitDir) { - const mergePath = join(gitDir, 'MERGE_HEAD') - const rebasePath = join(gitDir, 'rebase-merge') - const rebaseApplyPath = join(gitDir, 'rebase-apply') - - if (existsSync(mergePath) || existsSync(rebasePath) || existsSync(rebaseApplyPath)) { - return { hasConflicts: true, files: ['merge/rebase in progress'] } - } - } - - return { hasConflicts: false, files: [] } -} - -function abortAllGitOperations() { - console.log('Aborting any ongoing Git operations...') - - // Try to abort merge - exec('git merge --abort') - - // Try to abort rebase - exec('git rebase --abort') - - // Try to abort cherry-pick - exec('git cherry-pick --abort') - - console.log('Git operations aborted.') -} - -async function updateGit() { - const hasGit = await which('git') - if (!hasGit) { - console.log('Git not found. Skipping update.') - return 1 - } - - console.log('\n' + '='.repeat(60)) - console.log('Smart Git Update') - console.log('='.repeat(60)) - - // Step 0: Pre-flight checks - const conflictCheck = hasUnresolvedConflicts() - if (conflictCheck.hasConflicts) { - console.log('\n⚠️ ERROR: Git repository has unresolved conflicts!') - console.log('Conflicted files:') - conflictCheck.files.forEach(f => console.log(` - ${f}`)) - console.log('\nπŸ”§ Attempting automatic resolution...') - - abortAllGitOperations() - - const recheckConflicts = hasUnresolvedConflicts() - if (recheckConflicts.hasConflicts) { - console.log('\n❌ Could not automatically resolve conflicts.') - console.log('Manual intervention required. Please run:') - console.log(' git status') - console.log(' git reset --hard origin/main # WARNING: This will discard ALL local changes') - console.log('\nUpdate aborted for safety.') - return 1 - } - - console.log('βœ“ Conflicts cleared. Continuing...\n') - } - - // Pre-flight: Check if repo is clean enough to proceed - const isDirty = exec('git diff --quiet') - const hasUntracked = exec('git ls-files --others --exclude-standard') - if (isDirty === null && hasUntracked) { - console.log('ℹ️ Repository has local changes, will preserve user files during update.') - } - - // Step 1: Read config to get user preferences - let userConfig = { autoUpdateConfig: false, autoUpdateAccounts: false } - const configData = readJsonConfig([ - "src/config.jsonc", - "config.jsonc", - "src/config.json", - "config.json" - ]) - - if (!configData) { - console.log('Warning: Could not read config.jsonc, using defaults (preserve local files)') - } else if (configData.update) { - userConfig.autoUpdateConfig = configData.update.autoUpdateConfig ?? false - userConfig.autoUpdateAccounts = configData.update.autoUpdateAccounts ?? false - } - - console.log('\nUser preferences:') - console.log(` Auto-update config.jsonc: ${userConfig.autoUpdateConfig}`) - console.log(` Auto-update accounts.json: ${userConfig.autoUpdateAccounts}`) - - // Step 2: Get current branch - const currentBranch = exec('git branch --show-current') - if (!currentBranch) { - console.log('Could not determine current branch.') - return 1 - } - - // Fetch latest changes - console.log('\n🌐 Fetching latest changes...') - await run('git', ['fetch', '--all', '--prune']) - - // Step 3: Backup user files BEFORE any git operations - const backupDir = join(process.cwd(), '.update-backup') - mkdirSync(backupDir, { recursive: true }) - - const userFiles = [] - - if (existsSync('src/config.jsonc')) { - console.log('\nπŸ“¦ Backing up config.jsonc...') - const configContent = readFileSync('src/config.jsonc', 'utf8') - writeFileSync(join(backupDir, 'config.jsonc.bak'), configContent) - if (!userConfig.autoUpdateConfig) { - userFiles.push({ path: 'src/config.jsonc', content: configContent }) - } - } - - if (existsSync('src/accounts.jsonc')) { - console.log('πŸ“¦ Backing up accounts.jsonc...') - const accountsContent = readFileSync('src/accounts.jsonc', 'utf8') - writeFileSync(join(backupDir, 'accounts.jsonc.bak'), accountsContent) - if (!userConfig.autoUpdateAccounts) { - userFiles.push({ path: 'src/accounts.jsonc', content: accountsContent }) - } - } - - if (existsSync('src/accounts.json')) { - console.log('πŸ“¦ Backing up accounts.json...') - const accountsJsonContent = readFileSync('src/accounts.json', 'utf8') - writeFileSync(join(backupDir, 'accounts.json.bak'), accountsJsonContent) - if (!userConfig.autoUpdateAccounts) { - userFiles.push({ path: 'src/accounts.json', content: accountsJsonContent }) - } - } - - // Show what will happen - console.log('\nπŸ“‹ Update strategy:') - console.log(` config.jsonc: ${userConfig.autoUpdateConfig ? 'πŸ”„ WILL UPDATE from remote' : 'πŸ”’ KEEPING YOUR LOCAL VERSION'}`) - console.log(` accounts: ${userConfig.autoUpdateAccounts ? 'πŸ”„ WILL UPDATE from remote' : 'πŸ”’ KEEPING YOUR LOCAL VERSION (always)'}`) - console.log(' Other files: πŸ”„ will update from remote') - - // Step 4: Use merge strategy to avoid conflicts - // Instead of stash, we'll use a better approach: - // 1. Reset to remote (get clean state) - // 2. Then restore user files manually - - console.log('\nπŸ”„ Applying updates (using smart merge strategy)...') - - // Save current commit for potential rollback - const currentCommit = exec('git rev-parse HEAD') - - // Check if we're behind - const remoteBranch = `origin/${currentBranch}` - const behindCount = exec(`git rev-list --count HEAD..${remoteBranch}`) - - if (!behindCount || behindCount === '0') { - console.log('βœ“ Already up to date!') - // FIXED: Return 0 but DON'T create update marker (no restart needed) - return 0 - } - - console.log(`ℹ️ ${behindCount} commits behind remote`) - - // MARK: Update is happening - create marker file for bot to detect - const updateMarkerPath = join(process.cwd(), '.update-happened') - writeFileSync(updateMarkerPath, `Updated from ${currentCommit} to latest at ${new Date().toISOString()}`) - - // Use merge with strategy to accept remote changes for all files - // We'll restore user files afterwards - const mergeCode = await run('git', ['merge', '--strategy-option=theirs', remoteBranch]) - - if (mergeCode !== 0) { - console.log('\n⚠️ Merge failed, trying reset strategy...') - - // Abort merge - exec('git merge --abort') - - // Try reset + restore approach instead - const resetCode = await run('git', ['reset', '--hard', remoteBranch]) - - if (resetCode !== 0) { - console.log('\n❌ Update failed!') - console.log('πŸ”™ Rolling back to previous state...') - await run('git', ['reset', '--hard', currentCommit]) - - // Restore user files from backup - for (const file of userFiles) { - writeFileSync(file.path, file.content) - } - - console.log('βœ“ Rolled back successfully. Your files are safe.') - return 1 - } - } - - // Step 5: Restore user files - if (userFiles.length > 0) { - console.log('\nπŸ”’ Restoring your protected files...') - for (const file of userFiles) { - try { - writeFileSync(file.path, file.content) - console.log(` βœ“ Restored ${file.path}`) - } catch (err) { - console.log(` ⚠️ Failed to restore ${file.path}: ${err.message}`) - } - } - } - - // Clean the git state (remove any leftover merge markers or conflicts) - exec('git reset HEAD .') - exec('git checkout -- .') - - // Step 6: Install & build - const hasNpm = await which('npm') - if (!hasNpm) return 0 - - console.log('\nInstalling dependencies...') - await run('npm', ['ci']) - - console.log('\nBuilding project...') - const buildCode = await run('npm', ['run', 'build']) - - console.log('\n' + '='.repeat(60)) - console.log('Update completed!') - console.log('='.repeat(60) + '\n') - - return buildCode -} - /** - * Git-free update using GitHub API - * Downloads latest code as ZIP, extracts, and selectively copies files - * Preserves user config and accounts - */ -async function updateGitFree() { - console.log('\n' + '='.repeat(60)) - console.log('Git-Free Smart Update (GitHub API)') - console.log('='.repeat(60)) - - // Step 1: Read user preferences - let userConfig = { autoUpdateConfig: false, autoUpdateAccounts: false } - const configData = readJsonConfig([ - "src/config.jsonc", - "config.jsonc", - "src/config.json", - "config.json" - ]) - - if (configData?.update) { - userConfig.autoUpdateConfig = configData.update.autoUpdateConfig ?? false - userConfig.autoUpdateAccounts = configData.update.autoUpdateAccounts ?? false - } - - console.log('\nπŸ“‹ User preferences:') - console.log(` Auto-update config.jsonc: ${userConfig.autoUpdateConfig}`) - console.log(` Auto-update accounts: ${userConfig.autoUpdateAccounts}`) - - // Step 2: Backup user files - const backupDir = join(process.cwd(), '.update-backup-gitfree') - mkdirSync(backupDir, { recursive: true }) - - const filesToPreserve = [ - { src: 'src/config.jsonc', preserve: !userConfig.autoUpdateConfig }, - { src: 'src/accounts.jsonc', preserve: !userConfig.autoUpdateAccounts }, - { src: 'src/accounts.json', preserve: !userConfig.autoUpdateAccounts }, - { src: 'sessions', preserve: true, isDir: true }, - { src: '.update-backup', preserve: true, isDir: true } - ] - - console.log('\nπŸ“¦ Backing up protected files...') - for (const file of filesToPreserve) { - if (!file.preserve) continue - const srcPath = join(process.cwd(), file.src) - if (!existsSync(srcPath)) continue - - const destPath = join(backupDir, file.src) - mkdirSync(dirname(destPath), { recursive: true }) - - try { - if (file.isDir) { - cpSync(srcPath, destPath, { recursive: true }) - console.log(` βœ“ Backed up ${file.src}/ (directory)`) - } else { - writeFileSync(destPath, readFileSync(srcPath)) - console.log(` βœ“ Backed up ${file.src}`) - } - } catch (err) { - console.log(` ⚠️ Could not backup ${file.src}: ${err.message}`) - } - } - - // Step 3: Download latest code from GitHub - const repoOwner = 'Obsidian-wtf' // Change to your repo - 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') - - console.log(`\n🌐 Downloading latest code from GitHub...`) - console.log(` ${archiveUrl}`) - - try { - // Download with built-in https - await downloadFile(archiveUrl, archivePath) - console.log('βœ“ Download complete') - } catch (err) { - console.log(`❌ Download failed: ${err.message}`) - console.log('Please check your internet connection and try again.') - return 1 - } - - // Step 4: Extract archive - console.log('\nπŸ“‚ Extracting archive...') - rmSync(extractDir, { recursive: true, force: true }) - mkdirSync(extractDir, { recursive: true }) - - try { - // Use built-in unzip or cross-platform solution - await extractZip(archivePath, extractDir) - console.log('βœ“ Extraction complete') - } catch (err) { - console.log(`❌ Extraction failed: ${err.message}`) - return 1 - } - - // Step 5: Find extracted folder (GitHub adds repo name prefix) - const extractedItems = readdirSync(extractDir) - const extractedRepoDir = extractedItems.find(item => item.startsWith(repoName)) - if (!extractedRepoDir) { - console.log('❌ Could not find extracted repository folder') - return 1 - } - - const sourceDir = join(extractDir, extractedRepoDir) - - // Step 6: Copy files selectively - console.log('\nπŸ“‹ Updating files...') - const filesToUpdate = [ - 'src', - 'docs', - 'setup', - 'public', - 'tests', - 'package.json', - 'package-lock.json', - 'tsconfig.json', - 'Dockerfile', - 'compose.yaml', - 'README.md', - 'LICENSE' - ] - - for (const item of filesToUpdate) { - const srcPath = join(sourceDir, item) - const destPath = join(process.cwd(), item) - - if (!existsSync(srcPath)) continue - - // Skip if it's a protected file - const isProtected = filesToPreserve.some(f => - f.preserve && (destPath.includes(f.src) || f.src === item) - ) - if (isProtected) { - console.log(` ⏭️ Skipping ${item} (protected)`) - continue - } - - try { - // Remove old first - if (existsSync(destPath)) { - rmSync(destPath, { recursive: true, force: true }) - } - // Copy new - if (statSync(srcPath).isDirectory()) { - cpSync(srcPath, destPath, { recursive: true }) - console.log(` βœ“ Updated ${item}/ (directory)`) - } else { - cpSync(srcPath, destPath) - console.log(` βœ“ Updated ${item}`) - } - } catch (err) { - console.log(` ⚠️ Failed to update ${item}: ${err.message}`) - } - } - - // Step 7: Restore protected files - console.log('\nπŸ”’ Restoring protected files...') - for (const file of filesToPreserve) { - if (!file.preserve) continue - const backupPath = join(backupDir, file.src) - if (!existsSync(backupPath)) continue - - const destPath = join(process.cwd(), file.src) - mkdirSync(dirname(destPath), { recursive: true }) - - try { - if (file.isDir) { - rmSync(destPath, { recursive: true, force: true }) - cpSync(backupPath, destPath, { recursive: true }) - console.log(` βœ“ Restored ${file.src}/ (directory)`) - } else { - writeFileSync(destPath, readFileSync(backupPath)) - console.log(` βœ“ Restored ${file.src}`) - } - } catch (err) { - console.log(` ⚠️ Failed to restore ${file.src}: ${err.message}`) - } - } - - // Step 8: Cleanup - console.log('\n🧹 Cleaning up temporary files...') - rmSync(archivePath, { force: true }) - rmSync(extractDir, { recursive: true, force: true }) - console.log('βœ“ Cleanup complete') - - // MARK: Update happened - create marker file for bot to detect restart - const updateMarkerPath = join(process.cwd(), '.update-happened') - writeFileSync(updateMarkerPath, `Git-free update completed at ${new Date().toISOString()}`) - console.log('βœ“ Created update marker for bot restart detection') - - // Step 9: Install & build - const hasNpm = await which('npm') - if (!hasNpm) { - console.log('\nβœ“ Update completed! (npm not found, skipping dependencies)') - return 0 - } - - console.log('\nπŸ“¦ Installing dependencies...') - await run('npm', ['ci']) - - console.log('\nπŸ”¨ Building project...') - const buildCode = await run('npm', ['run', 'build']) - - console.log('\n' + '='.repeat(60)) - console.log('βœ“ Git-Free Update Completed Successfully!') - console.log('='.repeat(60) + '\n') - - return buildCode -} - -/** - * Download file using Node.js built-in https + * Download file via HTTPS */ function downloadFile(url, dest) { return new Promise((resolve, reject) => { @@ -594,7 +156,6 @@ function downloadFile(url, dest) { } response.pipe(file) - file.on('finish', () => { file.close() resolve() @@ -609,145 +170,298 @@ function downloadFile(url, dest) { /** * Extract ZIP file (cross-platform) - * Uses built-in or fallback methods */ async function extractZip(zipPath, destDir) { - // Try using unzip command (Unix-like systems) - const hasUnzip = await which('unzip') - if (hasUnzip) { + // 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 using tar (works on modern Windows 10+) - const hasTar = await which('tar') - if (hasTar) { + // 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 using PowerShell Expand-Archive (Windows) + // Try PowerShell Expand-Archive (Windows) if (process.platform === 'win32') { - const code = await run('powershell', ['-Command', `Expand-Archive -Path "${zipPath}" -DestinationPath "${destDir}" -Force`], { stdio: 'ignore' }) + const code = await run('powershell', [ + '-Command', + `Expand-Archive -Path "${zipPath}" -DestinationPath "${destDir}" -Force` + ], { stdio: 'ignore' }) if (code === 0) return } - throw new Error('No suitable extraction tool found (unzip, tar, or PowerShell)') + throw new Error('No extraction tool found (unzip, tar, or PowerShell required)') } -async function updateDocker() { - const hasDocker = await which('docker') - if (!hasDocker) return 1 - // Prefer compose v2 (docker compose) - await run('docker', ['compose', 'pull']) - return run('docker', ['compose', 'up', '-d']) -} +// ============================================================================= +// MAIN UPDATE LOGIC +// ============================================================================= -async function main() { - const args = new Set(process.argv.slice(2)) - const forceGit = args.has('--git') - const forceGitFree = args.has('--no-git') || args.has('--zip') - const doDocker = args.has('--docker') +/** + * 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 1: Read user preferences + console.log('\nπŸ“‹ Reading configuration...') + const configData = readJsonConfig([ + 'src/config.jsonc', + 'config.jsonc', + 'src/config.json', + 'config.json' + ]) - let code = 0 + const userConfig = { + 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)'}`) - // If no method specified, read from config - let useGitFree = forceGitFree - let useGit = forceGit + // Step 2: Backup protected files + console.log('\nπŸ”’ Backing up protected files...') + const backupDir = join(process.cwd(), '.update-backup') + mkdirSync(backupDir, { recursive: true }) - if (!forceGit && !forceGitFree && !doDocker) { - // Read config to determine preferred method - const configData = readJsonConfig([ - "src/config.jsonc", - "config.jsonc", - "src/config.json", - "config.json" - ]) + 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 - if (configData?.update) { - const updateEnabled = configData.update.enabled !== false - const method = configData.update.method || 'github-api' - - if (!updateEnabled) { - console.log('⚠️ Updates are disabled in config.jsonc (update.enabled = false)') - console.log('To enable updates, set "update.enabled" to true in your config.jsonc') - return 0 - } - - if (method === 'github-api' || method === 'api' || method === 'zip') { - console.log('πŸ“‹ Config prefers GitHub API method (update.method = "github-api")') - useGitFree = true - } else if (method === 'git') { - console.log('πŸ“‹ Config prefers Git method (update.method = "git")') - useGit = true + const destPath = join(backupDir, file.path) + mkdirSync(dirname(destPath), { recursive: true }) + + try { + if (file.isDir) { + cpSync(srcPath, destPath, { recursive: true }) } else { - console.log(`⚠️ Unknown update method "${method}" in config, defaulting to GitHub API`) - useGitFree = true + writeFileSync(destPath, readFileSync(srcPath)) } - } else { - // No config found or no update section, default to GitHub API - console.log('πŸ“‹ No update preferences in config, using GitHub API method (recommended)') - useGitFree = true + backedUp.push(file) + console.log(` βœ“ ${file.path}${file.isDir ? '/' : ''}`) + } catch (err) { + console.log(` ⚠️ Could not backup ${file.path}: ${err.message}`) } } - // Execute chosen method - if (useGitFree) { - console.log('πŸš€ Starting update with GitHub API method (no Git conflicts)...\n') - code = await updateGitFree() - } else if (useGit) { - // Check if git is available, fallback to git-free if not - const hasGit = await which('git') - if (!hasGit) { - console.log('⚠️ Git not found, falling back to GitHub API method\n') - code = await updateGitFree() - } else { - console.log('πŸš€ Starting update with Git method...\n') - code = await updateGit() + // Step 3: Download latest code from GitHub + console.log('\n🌐 Downloading latest code from GitHub...') + 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') + + console.log(` ${archiveUrl}`) + + try { + await downloadFile(archiveUrl, archivePath) + console.log(' βœ“ Download complete') + } catch (err) { + console.log(`\n❌ Download failed: ${err.message}`) + console.log('Please check your internet connection and try again.') + return 1 + } + + // Step 4: Extract archive + console.log('\nπŸ“‚ Extracting archive...') + rmSync(extractDir, { recursive: true, force: true }) + mkdirSync(extractDir, { recursive: true }) + + try { + await extractZip(archivePath, extractDir) + console.log(' βœ“ Extraction complete') + } catch (err) { + console.log(`\n❌ Extraction failed: ${err.message}`) + console.log('Please ensure you have unzip, tar, or PowerShell available.') + 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 + console.log('\nπŸ“¦ 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' + ] + + let updatedCount = 0 + 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) { + console.log(` ⏭️ ${item} (protected)`) + continue } - } else { - // No method chosen, show usage - console.log('Microsoft Rewards Bot - Update Script') - console.log('=' .repeat(60)) - console.log('') - console.log('Usage:') - console.log(' node setup/update/update.mjs # Auto-detect from config.jsonc') - console.log(' node setup/update/update.mjs --git # Force Git method') - console.log(' node setup/update/update.mjs --no-git # Force GitHub API method') - console.log(' node setup/update/update.mjs --docker # Update Docker containers') - console.log('') - console.log('Update methods:') - console.log(' β€’ GitHub API (--no-git): Downloads ZIP from GitHub') - console.log(' βœ“ No Git required') - console.log(' βœ“ No merge conflicts') - console.log(' βœ“ Works even if Git repo is broken') - console.log(' βœ“ Recommended for most users') - console.log('') - console.log(' β€’ Git (--git): Uses Git pull/merge') - console.log(' βœ“ Preserves Git history') - console.log(' βœ“ Faster for small changes') - console.log(' βœ— Requires Git installed') - console.log(' βœ— May have merge conflicts') - console.log('') - console.log('Configuration:') - console.log(' Edit src/config.jsonc to set your preferred method:') - console.log(' "update": {') - console.log(' "enabled": true,') - console.log(' "method": "github-api" // or "git"') - console.log(' }') - console.log('') + + try { + if (existsSync(destPath)) { + rmSync(destPath, { recursive: true, force: true }) + } + + if (statSync(srcPath).isDirectory()) { + cpSync(srcPath, destPath, { recursive: true }) + console.log(` βœ“ ${item}/`) + } else { + cpSync(srcPath, destPath) + console.log(` βœ“ ${item}`) + } + updatedCount++ + } catch (err) { + console.log(` ⚠️ Failed to update ${item}: ${err.message}`) + } + } + + // Step 7: Restore protected files + 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 + + 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)) + } + console.log(` βœ“ ${file.path}${file.isDir ? '/' : ''}`) + } catch (err) { + console.log(` ⚠️ Failed to restore ${file.path}: ${err.message}`) + } + } + } + + // Step 8: Cleanup temporary files + console.log('\n🧹 Cleaning up...') + rmSync(archivePath, { force: true }) + rmSync(extractDir, { recursive: true, force: true }) + rmSync(backupDir, { recursive: true, force: true }) + console.log(' βœ“ Temporary files removed') + + // Step 9: Check if anything was actually updated + if (updatedCount === 0) { + console.log('\nβœ… Already up to date!') + console.log('='.repeat(70) + '\n') + // No update marker - bot won't restart return 0 } - if (doDocker && code === 0) { - code = await updateDocker() + // Step 10: Create update marker for bot restart detection + const updateMarkerPath = join(process.cwd(), '.update-happened') + writeFileSync(updateMarkerPath, JSON.stringify({ + timestamp: new Date().toISOString(), + filesUpdated: updatedCount, + method: 'github-api' + }, null, 2)) + console.log(' βœ“ Update marker created') + + // Step 11: 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') + return 0 + } + + console.log('\nπŸ“¦ Installing dependencies...') + const installCode = await run('npm', ['ci']) + if (installCode !== 0) { + console.log(' ⚠️ npm ci failed, trying npm install...') + await run('npm', ['install']) } - // Return exit code to parent process + 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') + } + console.log('='.repeat(70) + '\n') + + return buildCode +} + +// ============================================================================= +// ENTRY POINT +// ============================================================================= + +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 + } + + const code = await performUpdate() process.exit(code) } main().catch((err) => { - console.error('Update script error:', err) + console.error('\n❌ Update failed with error:', err) + console.error('\nPlease report this issue if it persists.') process.exit(1) }) diff --git a/src/index.ts b/src/index.ts index 4ae295b..a3296ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -970,44 +970,79 @@ async function main(): Promise { const bootstrap = async () => { try { // Check for updates BEFORE initializing and running tasks - // CRITICAL: Only restart if update script explicitly indicates new version was installed - try { - const updateResult = await rewardsBot.runAutoUpdate().catch((e) => { - log('main', 'UPDATE', `Auto-update check failed: ${e instanceof Error ? e.message : String(e)}`, 'warn') - return -1 - }) - - // FIXED: Only restart on exit code 0 AND if update actually happened - // The update script returns 0 even when no update is needed, which causes infinite loop - // Solution: Check for marker file that update script creates when actual update happens - if (updateResult === 0) { - const updateMarkerPath = path.join(process.cwd(), '.update-happened') - const updateHappened = fs.existsSync(updateMarkerPath) - - if (updateHappened) { - // Remove marker file - try { - fs.unlinkSync(updateMarkerPath) - } catch { - // Ignore cleanup errors - } - - log('main', 'UPDATE', 'βœ… Update successful - restarting with new version...', 'log', 'green') - - // 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' - }) - child.unref() - process.exit(0) - } else { - log('main', 'UPDATE', 'Already up to date, continuing with bot execution') - } + // Anti-loop protection: Track restart attempts + const restartCounterPath = path.join(process.cwd(), '.update-restart-count') + let restartCount = 0 + if (fs.existsSync(restartCounterPath)) { + try { + const content = fs.readFileSync(restartCounterPath, 'utf8') + restartCount = parseInt(content, 10) || 0 + } catch { + restartCount = 0 + } + } + + // If we've restarted too many times (3+), something is wrong - skip update + if (restartCount >= 3) { + log('main', 'UPDATE', '⚠️ Too many restart attempts detected - skipping update to prevent loop', 'warn') + // Clean up counter file + try { + fs.unlinkSync(restartCounterPath) + } catch { + // Ignore + } + } else { + try { + const updateResult = await rewardsBot.runAutoUpdate().catch((e) => { + log('main', 'UPDATE', `Auto-update check failed: ${e instanceof Error ? e.message : String(e)}`, 'warn') + return -1 + }) + + if (updateResult === 0) { + const updateMarkerPath = path.join(process.cwd(), '.update-happened') + const updateHappened = fs.existsSync(updateMarkerPath) + + if (updateHappened) { + // Remove marker file + try { + fs.unlinkSync(updateMarkerPath) + } catch { + // Ignore cleanup errors + } + + // Increment restart counter + restartCount++ + try { + fs.writeFileSync(restartCounterPath, String(restartCount)) + } catch { + // Ignore + } + + log('main', 'UPDATE', `βœ… Update successful - restarting with new version (attempt ${restartCount}/3)...`, 'log', 'green') + + // 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' + }) + child.unref() + process.exit(0) + } else { + log('main', 'UPDATE', 'Already up to date, continuing with bot execution') + // Clean restart counter on successful non-update run + try { + if (fs.existsSync(restartCounterPath)) { + fs.unlinkSync(restartCounterPath) + } + } catch { + // Ignore + } + } + } + } catch (updateError) { + log('main', 'UPDATE', `Update check failed (continuing): ${updateError instanceof Error ? updateError.message : String(updateError)}`, 'warn') } - } catch (updateError) { - log('main', 'UPDATE', `Update check failed (continuing): ${updateError instanceof Error ? updateError.message : String(updateError)}`, 'warn') } await rewardsBot.initialize()