mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
Initial commit
This commit is contained in:
25
setup/setup.bat
Normal file
25
setup/setup.bat
Normal file
@@ -0,0 +1,25 @@
|
||||
@echo off
|
||||
setlocal
|
||||
REM Wrapper to run setup via npm (Windows)
|
||||
REM Navigates to project root and runs npm run setup
|
||||
|
||||
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.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Navigating to project root...
|
||||
cd /d "%PROJECT_ROOT%"
|
||||
|
||||
echo Running setup script via npm...
|
||||
call npm run setup
|
||||
set EXITCODE=%ERRORLEVEL%
|
||||
echo.
|
||||
echo Setup finished with exit code %EXITCODE%.
|
||||
echo Press Enter to close.
|
||||
pause >NUL
|
||||
exit /b %EXITCODE%
|
||||
35
setup/setup.sh
Normal file
35
setup/setup.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Wrapper to run setup via npm (Linux/macOS)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
echo "=== Prerequisite Check ==="
|
||||
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
NPM_VERSION="$(npm -v 2>/dev/null || true)"
|
||||
echo "npm detected: ${NPM_VERSION}"
|
||||
else
|
||||
echo "[ERROR] npm not detected."
|
||||
echo " Install Node.js and npm from nodejs.org or your package manager"
|
||||
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}"
|
||||
else
|
||||
echo "[WARN] Git not detected."
|
||||
echo " Install (Linux): e.g. 'sudo apt install git' (or your distro equivalent)."
|
||||
fi
|
||||
|
||||
if [ ! -f "${PROJECT_ROOT}/package.json" ]; then
|
||||
echo "[ERROR] package.json not found at ${PROJECT_ROOT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Running setup script via npm ==="
|
||||
cd "${PROJECT_ROOT}"
|
||||
exec npm run setup
|
||||
214
setup/update/setup.mjs
Normal file
214
setup/update/setup.mjs
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Unified cross-platform setup script for Microsoft Rewards Script V2.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
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');
|
||||
|
||||
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');
|
||||
const example = path.join(SRC_DIR, 'accounts.example.jsonc');
|
||||
if (fs.existsSync(accounts)) {
|
||||
log('accounts.json already exists - skipping rename.');
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(example)) {
|
||||
log('Renaming accounts.example.jsonc to accounts.json...');
|
||||
fs.renameSync(example, accounts);
|
||||
} else {
|
||||
warn('Neither accounts.json nor accounts.example.jsonc found.');
|
||||
}
|
||||
}
|
||||
|
||||
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 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');
|
||||
|
||||
// Keep asking until user says yes
|
||||
for (;;) {
|
||||
const ans = (await prompt('Have you configured your accounts in accounts.json? (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.');
|
||||
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 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();
|
||||
} else {
|
||||
// Even if build exists, ensure browsers are installed once.
|
||||
await installPlaywrightBrowsers();
|
||||
}
|
||||
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');
|
||||
|
||||
const reviewConfig = (await prompt('Do you want to review config.jsonc now? (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');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await ensureNpmAvailable();
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install']);
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']);
|
||||
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, run: npm run start:schedule');
|
||||
}
|
||||
}
|
||||
|
||||
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('[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');
|
||||
}
|
||||
// After completing action, optionally pause if launched by double click on Windows (no TTY detection simple heuristic)
|
||||
if (process.platform === 'win32' && process.stdin.isTTY) {
|
||||
log('\nDone. 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);
|
||||
});
|
||||
412
setup/update/update.mjs
Normal file
412
setup/update/update.mjs
Normal file
@@ -0,0 +1,412 @@
|
||||
/* eslint-disable linebreak-style */
|
||||
/**
|
||||
* Smart Auto-Update Script
|
||||
*
|
||||
* Intelligently updates while preserving user settings:
|
||||
* - ALWAYS updates code files (*.ts, *.js, etc.)
|
||||
* - ONLY updates config.jsonc if remote has changes to it
|
||||
* - ONLY updates accounts.json if remote has changes to it
|
||||
* - KEEPS user passwords/emails/settings otherwise
|
||||
*
|
||||
* Usage:
|
||||
* node setup/update/update.mjs --git
|
||||
* node setup/update/update.mjs --docker
|
||||
*/
|
||||
|
||||
import { spawn, execSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 on parse errors
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
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: Check for existing conflicts FIRST
|
||||
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('\nAttempting automatic resolution...')
|
||||
|
||||
// Abort any ongoing operations
|
||||
abortAllGitOperations()
|
||||
|
||||
// Verify conflicts are cleared
|
||||
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 with update...\n')
|
||||
}
|
||||
|
||||
// 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: Fetch
|
||||
console.log('\nFetching latest changes...')
|
||||
await run('git', ['fetch', '--all', '--prune'])
|
||||
|
||||
// Step 3: Get current branch
|
||||
const currentBranch = exec('git branch --show-current')
|
||||
if (!currentBranch) {
|
||||
console.log('Could not determine current branch.')
|
||||
return 1
|
||||
}
|
||||
|
||||
// Step 4: Check which files changed in remote
|
||||
const remoteBranch = `origin/${currentBranch}`
|
||||
const filesChanged = exec(`git diff --name-only HEAD ${remoteBranch}`)
|
||||
|
||||
if (!filesChanged) {
|
||||
console.log('Already up to date!')
|
||||
return 0
|
||||
}
|
||||
|
||||
const changedFiles = filesChanged.split('\n').filter(f => f.trim())
|
||||
const configChanged = changedFiles.includes('src/config.jsonc')
|
||||
const accountsChanged = changedFiles.includes('src/accounts.json')
|
||||
|
||||
// Step 5: ALWAYS backup config and accounts (smart strategy!)
|
||||
const backupDir = join(process.cwd(), '.update-backup')
|
||||
mkdirSync(backupDir, { recursive: true })
|
||||
|
||||
const filesToRestore = []
|
||||
|
||||
if (existsSync('src/config.jsonc')) {
|
||||
console.log('\nBacking up config.jsonc...')
|
||||
writeFileSync(join(backupDir, 'config.jsonc'), readFileSync('src/config.jsonc', 'utf8'))
|
||||
// ALWAYS restore config unless user explicitly wants auto-update
|
||||
if (!userConfig.autoUpdateConfig) {
|
||||
filesToRestore.push('config.jsonc')
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync('src/accounts.json')) {
|
||||
console.log('Backing up accounts.json...')
|
||||
writeFileSync(join(backupDir, 'accounts.json'), readFileSync('src/accounts.json', 'utf8'))
|
||||
// ALWAYS restore accounts unless user explicitly wants auto-update
|
||||
if (!userConfig.autoUpdateAccounts) {
|
||||
filesToRestore.push('accounts.json')
|
||||
}
|
||||
}
|
||||
|
||||
// Show what will happen
|
||||
console.log('\nUpdate strategy:')
|
||||
console.log(` config.jsonc: ${userConfig.autoUpdateConfig ? 'WILL UPDATE from remote' : 'KEEPING YOUR LOCAL VERSION (always)'}`)
|
||||
console.log(` accounts.json: ${userConfig.autoUpdateAccounts ? 'WILL UPDATE from remote' : 'KEEPING YOUR LOCAL VERSION (always)'}`)
|
||||
console.log(' All other files: will update from remote')
|
||||
|
||||
// Step 6: Handle local changes intelligently
|
||||
// Check if there are uncommitted changes to config/accounts
|
||||
const localChanges = exec('git status --porcelain')
|
||||
const hasConfigChanges = localChanges && localChanges.includes('src/config.jsonc')
|
||||
const hasAccountChanges = localChanges && localChanges.includes('src/accounts.json')
|
||||
|
||||
if (hasConfigChanges && !userConfig.autoUpdateConfig) {
|
||||
console.log('\n✓ Detected local changes to config.jsonc - will preserve them')
|
||||
}
|
||||
|
||||
if (hasAccountChanges && !userConfig.autoUpdateAccounts) {
|
||||
console.log('✓ Detected local changes to accounts.json - will preserve them')
|
||||
}
|
||||
|
||||
// Step 7: Stash ALL changes (including untracked)
|
||||
const hasChanges = exec('git status --porcelain')
|
||||
let stashCreated = false
|
||||
if (hasChanges) {
|
||||
console.log('\nStashing local changes (including config/accounts)...')
|
||||
await run('git', ['stash', 'push', '-u', '-m', 'Auto-update backup with untracked files'])
|
||||
stashCreated = true
|
||||
}
|
||||
|
||||
// Step 8: Pull with strategy to handle diverged branches
|
||||
console.log('\nPulling latest code...')
|
||||
let pullCode = await run('git', ['pull', '--rebase'])
|
||||
|
||||
if (pullCode !== 0) {
|
||||
console.log('\n❌ Pull failed! Checking for conflicts...')
|
||||
|
||||
// Check if it's a conflict
|
||||
const postPullConflicts = hasUnresolvedConflicts()
|
||||
if (postPullConflicts.hasConflicts) {
|
||||
console.log('Conflicts detected during pull:')
|
||||
postPullConflicts.files.forEach(f => console.log(` - ${f}`))
|
||||
|
||||
// Abort the rebase/merge
|
||||
console.log('\nAborting failed pull...')
|
||||
abortAllGitOperations()
|
||||
|
||||
// Pop stash before giving up
|
||||
if (stashCreated) {
|
||||
console.log('Restoring stashed changes...')
|
||||
await run('git', ['stash', 'pop'])
|
||||
}
|
||||
|
||||
console.log('\n⚠️ Update failed due to conflicts.')
|
||||
console.log('Your local changes have been preserved.')
|
||||
console.log('\nTo force update (DISCARDS local changes), run:')
|
||||
console.log(' git fetch --all')
|
||||
console.log(' git reset --hard origin/main')
|
||||
console.log(' npm ci && npm run build')
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// Not a conflict, just a generic pull failure
|
||||
console.log('Pull failed for unknown reason.')
|
||||
if (stashCreated) await run('git', ['stash', 'pop'])
|
||||
return pullCode
|
||||
}
|
||||
|
||||
// Step 9: Restore user files based on preferences
|
||||
if (filesToRestore.length > 0) {
|
||||
console.log('\nRestoring your local files (per config preferences)...')
|
||||
for (const file of filesToRestore) {
|
||||
const content = readFileSync(join(backupDir, file), 'utf8')
|
||||
writeFileSync(join('src', file), content)
|
||||
console.log(` ✓ Restored ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 10: Restore stash (but skip config/accounts if we already restored them)
|
||||
if (stashCreated) {
|
||||
console.log('\nRestoring stashed changes...')
|
||||
// Pop stash but auto-resolve conflicts by keeping our versions
|
||||
const popCode = await run('git', ['stash', 'pop'])
|
||||
|
||||
if (popCode !== 0) {
|
||||
console.log('⚠️ Stash pop had conflicts - resolving automatically...')
|
||||
|
||||
// For config/accounts, keep our version (--ours)
|
||||
if (!userConfig.autoUpdateConfig) {
|
||||
await run('git', ['checkout', '--ours', 'src/config.jsonc'])
|
||||
await run('git', ['add', 'src/config.jsonc'])
|
||||
}
|
||||
|
||||
if (!userConfig.autoUpdateAccounts) {
|
||||
await run('git', ['checkout', '--ours', 'src/accounts.json'])
|
||||
await run('git', ['add', 'src/accounts.json'])
|
||||
}
|
||||
|
||||
// Drop the stash since we resolved manually
|
||||
await run('git', ['reset'])
|
||||
await run('git', ['stash', 'drop'])
|
||||
|
||||
console.log('✓ Conflicts auto-resolved')
|
||||
}
|
||||
}
|
||||
|
||||
// Step 9: 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
|
||||
}
|
||||
|
||||
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'])
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = new Set(process.argv.slice(2))
|
||||
const doGit = args.has('--git')
|
||||
const doDocker = args.has('--docker')
|
||||
|
||||
let code = 0
|
||||
if (doGit) {
|
||||
code = await updateGit()
|
||||
}
|
||||
if (doDocker && code === 0) {
|
||||
code = await updateDocker()
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Always exit with code, even from scheduler
|
||||
// The scheduler expects the update script to complete and exit
|
||||
// Otherwise the process hangs indefinitely and gets killed by watchdog
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Update script error:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user