New structure 2

This commit is contained in:
2025-11-11 13:07:51 +01:00
parent 89bc226d6b
commit 82e5e71ffe
9 changed files with 123 additions and 112 deletions

View File

@@ -41,15 +41,12 @@ npm start
**Usage:**
```bash
# Run update manually
node setup/update/update.mjs
node scripts/installer/update.mjs
```
**Automatic updates:** The bot checks for updates on startup (controlled by `update.enabled` in config.jsonc).
### update/setup.mjs
**Interactive setup wizard** used by setup.bat/setup.sh.
This is typically not run directly - use the wrapper scripts instead.
**Note:** Installer scripts have been moved to `scripts/installer/` directory. See `scripts/README.md` for details.
## Quick Start Guide

3
setup/run.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
nix develop --command bash -c "xvfb-run npm run start"

View File

@@ -1,175 +0,0 @@
# Update System
## Overview
The bot uses a **simplified GitHub API-based update system** that:
- ✅ Downloads latest code as ZIP archive
- ✅ No Git required
- ✅ No merge conflicts
- ✅ Preserves user files automatically
- ✅ Automatic dependency installation
- ✅ TypeScript rebuild
## How It Works
1. **Automatic Updates**: If enabled in `config.jsonc`, the bot checks for updates on every startup
2. **Download**: Latest code is downloaded as ZIP from GitHub
3. **Protection**: User files (accounts, config, sessions) are backed up
4. **Update**: Code files are replaced selectively
5. **Restore**: Protected files are restored
6. **Install**: Dependencies are installed (`npm ci`)
7. **Build**: TypeScript is compiled
8. **Restart**: Bot restarts automatically with new version
## Configuration
In `src/config.jsonc`:
```jsonc
{
"update": {
"enabled": true, // Enable/disable updates
"autoUpdateAccounts": false, // Protect accounts files (recommended: false)
"autoUpdateConfig": false // Protect config.jsonc (recommended: false)
}
}
```
## Protected Files
These files are **always protected** (never overwritten):
- `sessions/` - Browser session data
- `.playwright-chromium-installed` - Browser installation marker
These files are **conditionally protected** (based on config):
- `src/accounts.jsonc` - Protected unless `autoUpdateAccounts: true`
- `src/accounts.json` - Protected unless `autoUpdateAccounts: true`
- `src/config.jsonc` - Protected unless `autoUpdateConfig: true`
## Manual Update
Run the update manually:
```bash
node setup/update/update.mjs
```
## Update Detection
The bot uses marker files to prevent restart loops:
- `.update-happened` - Created when files are actually updated
- `.update-restart-count` - Tracks restart attempts (max 3)
If no updates are available, **no marker is created** and the bot won't restart.
## Troubleshooting
### Updates disabled
```
⚠️ Updates are disabled in config.jsonc
```
→ Set `update.enabled: true` in `src/config.jsonc`
### Download failed
```
❌ Download failed: [error]
```
→ Check your internet connection
→ Verify GitHub is accessible
### Extraction failed
```
❌ Extraction failed: [error]
```
→ Ensure you have one of: `unzip`, `tar`, or PowerShell (Windows)
### Build failed
```
⚠️ Update completed with build warnings
```
→ Check TypeScript errors above
→ May still work, but review errors
## Architecture
### File Structure
```
setup/update/
├── update.mjs # Main update script (468 lines)
└── README.md # This file
```
### Update Flow
```
Start
Check config (enabled?)
Read user preferences (autoUpdate flags)
Backup protected files
Download ZIP from GitHub
Extract archive
Copy files selectively (skip protected)
Restore protected files
Cleanup temporary files
Create marker (.update-happened) if files changed
Install dependencies (npm ci)
Build TypeScript
Exit (bot auto-restarts if marker exists)
```
## Previous System
The old update system (799 lines) supported two methods:
- Git method (required Git, had merge conflicts)
- GitHub API method
**New system**: Only GitHub API method (simpler, more reliable)
## Anti-Loop Protection
The bot has built-in protection against infinite restart loops:
1. **Marker detection**: Bot only restarts if `.update-happened` exists
2. **Restart counter**: Max 3 restart attempts (`.update-restart-count`)
3. **Counter cleanup**: Removed after successful run without updates
4. **No-update detection**: Marker NOT created if already up to date
This ensures the bot never gets stuck in an infinite update loop.
## Dependencies
No external dependencies required! The update system uses only Node.js built-in modules:
- `node:child_process` - Run shell commands
- `node:fs` - File system operations
- `node:https` - Download files
- `node:path` - Path manipulation
## Exit Codes
- `0` - Success (updated or already up to date)
- `1` - Error (download failed, extraction failed, etc.)
## NPM Scripts
- `npm run start` - Start bot (runs update check first if enabled)
- `npm run dev` - Start in dev mode (skips update check)
- `npm run build` - Build TypeScript manually
## Version Info
- Current version: **v2** (GitHub API only)
- Previous version: v1 (Dual Git/GitHub API)
- Lines of code: **468** (down from 799)
- Complexity: **Simple** (down from Complex)

View File

@@ -1,232 +0,0 @@
#!/usr/bin/env node
/**
* Microsoft Rewards Bot - First-Time Setup Script
*
* This script handles initial project setup:
* 1. Creates accounts.jsonc from template
* 2. Guides user through account configuration
* 3. Installs dependencies (npm install)
* 4. Builds TypeScript project (npm run build)
* 5. Installs Playwright Chromium browser
*
* IMPORTANT: This script does NOT launch the bot automatically.
* After setup, run: npm start
*/
import { spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
function log(msg) { console.log(msg); }
function warn(msg) { console.warn(msg); }
function error(msg) { console.error(msg); }
function createAccountsFile() {
const accounts = path.join(SRC_DIR, 'accounts.jsonc');
const example = path.join(SRC_DIR, 'accounts.example.jsonc');
if (fs.existsSync(accounts)) {
log('✓ accounts.jsonc already exists - skipping creation');
return true;
}
if (fs.existsSync(example)) {
log('📝 Creating accounts.jsonc from template...');
fs.copyFileSync(example, accounts);
log('✓ Created accounts.jsonc');
return false;
} else {
error('❌ Template file accounts.example.jsonc not found!');
return true;
}
}
async function prompt(question) {
return await new Promise(resolve => {
process.stdout.write(question);
const onData = (data) => {
const ans = data.toString().trim();
process.stdin.off('data', onData);
resolve(ans);
};
process.stdin.on('data', onData);
});
}
async function guideAccountConfiguration() {
log('\n<> ACCOUNT CONFIGURATION');
log('════════════════════════════════════════════════════════════');
log('1. Open file: src/accounts.jsonc');
log('2. Add your Microsoft account credentials:');
log(' - email: Your Microsoft account email');
log(' - password: Your account password');
log(' - totp: (Optional) 2FA secret for automatic authentication');
log('3. Enable accounts by setting "enabled": true');
log('4. Save the file');
log('');
log('📚 Full guide: docs/accounts.md');
log('════════════════════════════════════════════════════════════\n');
for (;;) {
const ans = (await prompt('Have you configured your accounts? (yes/no): ')).toLowerCase();
if (['yes', 'y'].includes(ans)) break;
if (['no', 'n'].includes(ans)) {
log('\n⏸ Please configure src/accounts.jsonc and save it, then answer yes.\n');
continue;
}
log('Please answer yes or no.');
}
}
function runCommand(cmd, args, opts = {}) {
return new Promise((resolve, reject) => {
log(`Running: ${cmd} ${args.join(' ')}`);
const child = spawn(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts });
child.on('exit', (code) => {
if (code === 0) return resolve();
reject(new Error(`${cmd} exited with code ${code}`));
});
});
}
async function ensureNpmAvailable() {
try {
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['-v']);
} catch (e) {
throw new Error('npm not found in PATH. Install Node.js first.');
}
}
async function performSetup() {
log('\n🚀 MICROSOFT REWARDS BOT - FIRST-TIME SETUP');
log('════════════════════════════════════════════════════════════\n');
// Step 1: Create accounts file
const accountsExisted = createAccountsFile();
// Step 2: Guide user through account configuration
if (!accountsExisted) {
await guideAccountConfiguration();
} else {
log('✓ Using existing accounts.jsonc\n');
}
// Step 3: Configuration guidance
log('\n⚙ CONFIGURATION (src/config.jsonc)');
log('════════════════════════════════════════════════════════════');
log('Key settings you may want to adjust:');
log(' • browser.headless: false = visible browser, true = background');
log(' • execution.clusters: Number of parallel accounts (default: 1)');
log(' • workers: Enable/disable specific tasks');
log(' • humanization.enabled: Add natural delays (recommended: true)');
log(' • scheduling.enabled: Automate with OS scheduler');
log('');
log('📚 Full configuration guide: docs/getting-started.md');
log('════════════════════════════════════════════════════════════\n');
const reviewConfig = (await prompt('Review config.jsonc before continuing? (yes/no): ')).toLowerCase();
if (['yes', 'y'].includes(reviewConfig)) {
log('\n⏸ Setup paused.');
log('Please review and edit src/config.jsonc, then run: npm run setup\n');
process.exit(0);
}
// Step 4: Install dependencies
log('\n📦 Installing dependencies...');
await ensureNpmAvailable();
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install']);
// Step 5: Build TypeScript
log('\n🔨 Building TypeScript project...');
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']);
// Step 6: Install Playwright browsers
await installPlaywrightBrowsers();
// Final message
log('\n');
log('═══════════════════════════════════════════════════════════');
log('✅ SETUP COMPLETE!');
log('═══════════════════════════════════════════════════════════');
log('');
log('📁 Configuration files:');
log(' • Accounts: src/accounts.jsonc');
log(' • Config: src/config.jsonc');
log('');
log('📚 Documentation:');
log(' • Getting started: docs/getting-started.md');
log(' • Full docs: docs/index.md');
log('');
log('🚀 TO START THE BOT:');
log(' npm start');
log('');
log('⏰ FOR AUTOMATED SCHEDULING:');
log(' See: docs/getting-started.md (Scheduling section)');
log('═══════════════════════════════════════════════════════════\n');
}
async function installPlaywrightBrowsers() {
const PLAYWRIGHT_MARKER = path.join(PROJECT_ROOT, '.playwright-chromium-installed');
// Idempotent: skip if marker exists
if (fs.existsSync(PLAYWRIGHT_MARKER)) {
log('Playwright chromium already installed (marker found).');
return;
}
log('Ensuring Playwright chromium browser is installed...');
try {
await runCommand(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['playwright', 'install', 'chromium']);
fs.writeFileSync(PLAYWRIGHT_MARKER, new Date().toISOString());
log('Playwright chromium install complete.');
} catch (e) {
warn('Failed to install Playwright chromium automatically. You can manually run: npx playwright install chromium');
}
}
async function main() {
if (!fs.existsSync(SRC_DIR)) {
error('❌ Cannot find src directory at ' + SRC_DIR);
process.exit(1);
}
process.chdir(PROJECT_ROOT);
// Check if already setup (dist exists and accounts configured)
const distExists = fs.existsSync(path.join(PROJECT_ROOT, 'dist', 'index.js'));
const accountsExists = fs.existsSync(path.join(SRC_DIR, 'accounts.jsonc'));
if (distExists && accountsExists) {
log('\n⚠ Setup appears to be already complete.');
log(' • Build output: dist/index.js exists');
log(' • Accounts: src/accounts.jsonc exists\n');
const rerun = (await prompt('Run setup anyway? (yes/no): ')).toLowerCase();
if (!['yes', 'y'].includes(rerun)) {
log('\n💡 To start the bot: npm start');
log('💡 To rebuild: npm run build\n');
process.exit(0);
}
}
await performSetup();
// Pause if launched by double-click on Windows
if (process.platform === 'win32' && process.stdin.isTTY) {
log('Press Enter to close...');
await prompt('');
}
process.exit(0);
}
// Allow clean Ctrl+C
process.on('SIGINT', () => { console.log('\nInterrupted.'); process.exit(1); });
main().catch(err => {
error('\nSetup failed: ' + err.message);
process.exit(1);
});

View File

@@ -1,742 +0,0 @@
#!/usr/bin/env node
/**
* Microsoft Rewards Bot - Automatic Update System
*
* Uses GitHub API to download latest code as ZIP archive.
* No Git required, no merge conflicts, always clean.
*
* Features:
* - Downloads latest code from GitHub (ZIP)
* - Preserves user files (accounts, config, sessions)
* - Selective file copying
* - Automatic dependency installation
* - TypeScript rebuild
*
* Usage:
* node setup/update/update.mjs # Run update
* npm run start # Bot runs this automatically if enabled
*/
import { spawn } from 'node:child_process'
import { cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { get as httpsGet } from 'node:https'
import { dirname, join } from 'node:path'
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Strip JSON comments
*/
function stripJsonComments(input) {
let result = ''
let inString = false
let stringChar = ''
let inLineComment = false
let inBlockComment = false
for (let i = 0; i < input.length; i++) {
const char = input[i]
const next = input[i + 1]
if (inLineComment) {
if (char === '\n' || char === '\r') {
inLineComment = false
result += char
}
continue
}
if (inBlockComment) {
if (char === '*' && next === '/') {
inBlockComment = false
i++
}
continue
}
if (inString) {
result += char
if (char === '\\') {
i++
if (i < input.length) result += input[i]
continue
}
if (char === stringChar) inString = false
continue
}
if (char === '"' || char === "'") {
inString = true
stringChar = char
result += char
continue
}
if (char === '/' && next === '/') {
inLineComment = true
i++
continue
}
if (char === '/' && next === '*') {
inBlockComment = true
i++
continue
}
result += char
}
return result
}
/**
* Read and parse JSON config file
*/
function readJsonConfig(preferredPaths) {
for (const candidate of preferredPaths) {
if (!existsSync(candidate)) continue
try {
const raw = readFileSync(candidate, 'utf8').replace(/^\uFEFF/, '')
return JSON.parse(stripJsonComments(raw))
} catch {
// Try next candidate
}
}
return null
}
/**
* Run shell command
*/
function run(cmd, args, opts = {}) {
return new Promise((resolve) => {
const child = spawn(cmd, args, {
stdio: 'inherit',
shell: process.platform === 'win32',
...opts
})
child.on('close', (code) => resolve(code ?? 0))
child.on('error', () => resolve(1))
})
}
/**
* Check if command exists
*/
async function which(cmd) {
const probe = process.platform === 'win32' ? 'where' : 'which'
const code = await run(probe, [cmd], { stdio: 'ignore' })
return code === 0
}
/**
* Download file via HTTPS
*/
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = createWriteStream(dest)
httpsGet(url, (response) => {
// Handle redirects
if (response.statusCode === 302 || response.statusCode === 301) {
file.close()
rmSync(dest, { force: true })
downloadFile(response.headers.location, dest).then(resolve).catch(reject)
return
}
if (response.statusCode !== 200) {
file.close()
rmSync(dest, { force: true })
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`))
return
}
response.pipe(file)
file.on('finish', () => {
file.close()
resolve()
})
}).on('error', (err) => {
file.close()
rmSync(dest, { force: true })
reject(err)
})
})
}
/**
* Extract ZIP file (cross-platform)
*/
async function extractZip(zipPath, destDir) {
// Try unzip (Unix-like)
if (await which('unzip')) {
const code = await run('unzip', ['-q', '-o', zipPath, '-d', destDir], { stdio: 'ignore' })
if (code === 0) return
}
// Try tar (modern Windows/Unix)
if (await which('tar')) {
const code = await run('tar', ['-xf', zipPath, '-C', destDir], { stdio: 'ignore' })
if (code === 0) return
}
// Try PowerShell Expand-Archive (Windows)
if (process.platform === 'win32') {
const code = await run('powershell', [
'-Command',
`Expand-Archive -Path "${zipPath}" -DestinationPath "${destDir}" -Force`
], { stdio: 'ignore' })
if (code === 0) return
}
throw new Error('No extraction tool found (unzip, tar, or PowerShell required)')
}
// =============================================================================
// ENVIRONMENT DETECTION
// =============================================================================
/**
* Detect if running inside a Docker container
* Checks multiple indicators for accuracy
*/
function isDocker() {
try {
// Method 1: Check for /.dockerenv file (most reliable)
if (existsSync('/.dockerenv')) {
return true
}
// Method 2: Check /proc/1/cgroup for docker
if (existsSync('/proc/1/cgroup')) {
const cgroupContent = readFileSync('/proc/1/cgroup', 'utf8')
if (cgroupContent.includes('docker') || cgroupContent.includes('/kubepods/')) {
return true
}
}
// Method 3: Check environment variables
if (process.env.DOCKER === 'true' ||
process.env.CONTAINER === 'docker' ||
process.env.KUBERNETES_SERVICE_HOST) {
return true
}
// Method 4: Check /proc/self/mountinfo for overlay filesystem
if (existsSync('/proc/self/mountinfo')) {
const mountinfo = readFileSync('/proc/self/mountinfo', 'utf8')
if (mountinfo.includes('docker') || mountinfo.includes('overlay')) {
return true
}
}
return false
} catch {
// If any error occurs (e.g., on Windows), assume not Docker
return false
}
}
/**
* Determine update mode based on config and environment
*/
function getUpdateMode(configData) {
const dockerMode = configData?.update?.dockerMode || 'auto'
if (dockerMode === 'force-docker') {
return 'docker'
}
if (dockerMode === 'force-host') {
return 'host'
}
// Auto-detect
return isDocker() ? 'docker' : 'host'
}
// =============================================================================
// MAIN UPDATE LOGIC
// =============================================================================
/**
* Check if update is available by comparing versions
* Returns true if versions differ (allows both upgrades and downgrades)
*/
async function checkVersion() {
try {
// Read local version
const localPkgPath = join(process.cwd(), 'package.json')
if (!existsSync(localPkgPath)) {
console.log('⚠️ Could not find local package.json')
return { updateAvailable: false, localVersion: 'unknown', remoteVersion: 'unknown' }
}
const localPkg = JSON.parse(readFileSync(localPkgPath, 'utf8'))
const localVersion = localPkg.version
// Fetch remote version from GitHub
const repoOwner = 'Obsidian-wtf'
const repoName = 'Microsoft-Rewards-Bot'
const branch = 'main'
// Add cache-buster to prevent GitHub from serving stale cached version
const cacheBuster = Date.now()
const pkgUrl = `https://raw.githubusercontent.com/${repoOwner}/${repoName}/refs/heads/${branch}/package.json?cb=${cacheBuster}`
console.log('🔍 Checking for updates...')
console.log(` Local: ${localVersion}`)
return new Promise((resolve) => {
// Request with cache-busting headers
const options = {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
'User-Agent': 'Microsoft-Rewards-Bot-Updater'
}
}
const request = httpsGet(pkgUrl, options, (res) => {
if (res.statusCode !== 200) {
console.log(` ⚠️ Could not check remote version (HTTP ${res.statusCode})`)
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
return
}
let data = ''
res.on('data', chunk => data += chunk)
res.on('end', () => {
try {
const remotePkg = JSON.parse(data)
const remoteVersion = remotePkg.version
console.log(` Remote: ${remoteVersion}`)
// Any difference triggers update (upgrade or downgrade)
const updateAvailable = localVersion !== remoteVersion
resolve({ updateAvailable, localVersion, remoteVersion })
} catch (err) {
console.log(` ⚠️ Could not parse remote package.json: ${err.message}`)
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
}
})
})
// Timeout after 10 seconds
request.on('error', (err) => {
console.log(` ⚠️ Network error: ${err.message}`)
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
})
request.setTimeout(10000, () => {
request.destroy()
console.log(' ⚠️ Request timeout')
resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' })
})
})
} catch (err) {
console.log(`⚠️ Version check failed: ${err.message}`)
return { updateAvailable: false, localVersion: 'unknown', remoteVersion: 'unknown' }
}
}
/**
* Perform update using GitHub API (ZIP download)
*/
async function performUpdate() {
// Step 0: Check if update is needed by comparing versions
const versionCheck = await checkVersion()
if (!versionCheck.updateAvailable) {
console.log(`✅ Already up to date (v${versionCheck.localVersion})`)
return 0 // Exit without creating update marker
}
// Step 0.5: Detect environment and determine update mode
const configData = readJsonConfig([
'src/config.jsonc',
'config.jsonc',
'src/config.json',
'config.json'
])
const updateMode = getUpdateMode(configData)
const envIcon = updateMode === 'docker' ? '🐳' : '💻'
console.log(`\n📦 Update available: ${versionCheck.localVersion}${versionCheck.remoteVersion}`)
console.log(`${envIcon} Environment: ${updateMode === 'docker' ? 'Docker container' : 'Host system'}`)
console.log('⏳ Updating... (this may take a moment)\n')
// Step 1: Read user preferences (silent)
const userConfig = {
autoUpdateConfig: configData?.update?.autoUpdateConfig ?? false,
autoUpdateAccounts: configData?.update?.autoUpdateAccounts ?? false
}
// Step 2: Create backups (protected files + critical for rollback)
const backupDir = join(process.cwd(), '.update-backup')
const rollbackDir = join(process.cwd(), '.update-rollback')
// Clean previous backups
rmSync(backupDir, { recursive: true, force: true })
rmSync(rollbackDir, { recursive: true, force: true })
mkdirSync(backupDir, { recursive: true })
mkdirSync(rollbackDir, { recursive: true })
const filesToProtect = [
{ path: 'src/config.jsonc', protect: !userConfig.autoUpdateConfig },
{ path: 'src/accounts.jsonc', protect: !userConfig.autoUpdateAccounts },
{ path: 'src/accounts.json', protect: !userConfig.autoUpdateAccounts },
{ path: 'sessions', protect: true, isDir: true },
{ path: '.playwright-chromium-installed', protect: true }
]
const backedUp = []
for (const file of filesToProtect) {
if (!file.protect) continue
const srcPath = join(process.cwd(), file.path)
if (!existsSync(srcPath)) continue
const destPath = join(backupDir, file.path)
mkdirSync(dirname(destPath), { recursive: true })
try {
if (file.isDir) {
cpSync(srcPath, destPath, { recursive: true })
} else {
writeFileSync(destPath, readFileSync(srcPath))
}
backedUp.push(file)
} catch {
// Silent failure - continue with update
}
}
// Backup critical files for potential rollback
const criticalFiles = ['package.json', 'package-lock.json', 'dist']
for (const file of criticalFiles) {
const srcPath = join(process.cwd(), file)
if (!existsSync(srcPath)) continue
const destPath = join(rollbackDir, file)
try {
if (statSync(srcPath).isDirectory()) {
cpSync(srcPath, destPath, { recursive: true })
} else {
cpSync(srcPath, destPath)
}
} catch {
// Continue
}
}
// Step 3: Download latest code from GitHub
process.stdout.write('📥 Downloading...')
const repoOwner = 'Obsidian-wtf'
const repoName = 'Microsoft-Rewards-Bot'
const branch = 'main'
const archiveUrl = `https://github.com/${repoOwner}/${repoName}/archive/refs/heads/${branch}.zip`
const archivePath = join(process.cwd(), '.update-download.zip')
const extractDir = join(process.cwd(), '.update-extract')
try {
await downloadFile(archiveUrl, archivePath)
process.stdout.write(' ✓\n')
} catch (err) {
console.log(`\n❌ Download failed: ${err.message}`)
return 1
}
// Step 4: Extract archive
process.stdout.write('📂 Extracting...')
rmSync(extractDir, { recursive: true, force: true })
mkdirSync(extractDir, { recursive: true })
try {
await extractZip(archivePath, extractDir)
process.stdout.write(' ✓\n')
} catch (err) {
console.log(`\n❌ Extraction failed: ${err.message}`)
return 1
}
// Step 5: Find extracted folder
const extractedItems = readdirSync(extractDir)
const extractedRepoDir = extractedItems.find(item => item.startsWith(repoName))
if (!extractedRepoDir) {
console.log('\n❌ Could not find extracted repository folder')
return 1
}
const sourceDir = join(extractDir, extractedRepoDir)
// Step 6: Copy files selectively
process.stdout.write('📦 Updating files...')
const itemsToUpdate = [
'src',
'docs',
'setup',
'public',
'tests',
'package.json',
'package-lock.json',
'tsconfig.json',
'Dockerfile',
'compose.yaml',
'entrypoint.sh',
'run.sh',
'README.md',
'LICENSE'
]
for (const item of itemsToUpdate) {
const srcPath = join(sourceDir, item)
const destPath = join(process.cwd(), item)
if (!existsSync(srcPath)) continue
// Skip protected items
const isProtected = backedUp.some(f => f.path === item || destPath.includes(f.path))
if (isProtected) continue
try {
if (existsSync(destPath)) {
rmSync(destPath, { recursive: true, force: true })
}
if (statSync(srcPath).isDirectory()) {
cpSync(srcPath, destPath, { recursive: true })
} else {
cpSync(srcPath, destPath)
}
} catch {
// Silent failure - continue
}
}
process.stdout.write(' ✓\n')
// Step 7: Restore protected files (silent)
if (backedUp.length > 0) {
for (const file of backedUp) {
const backupPath = join(backupDir, file.path)
if (!existsSync(backupPath)) continue
const destPath = join(process.cwd(), file.path)
mkdirSync(dirname(destPath), { recursive: true })
try {
if (file.isDir) {
rmSync(destPath, { recursive: true, force: true })
cpSync(backupPath, destPath, { recursive: true })
} else {
writeFileSync(destPath, readFileSync(backupPath))
}
} catch {
// Silent failure
}
}
}
// Step 8: Cleanup temporary files (silent)
rmSync(archivePath, { force: true })
rmSync(extractDir, { recursive: true, force: true })
rmSync(backupDir, { recursive: true, force: true })
// Step 9: Create update marker for bot restart detection
const updateMarkerPath = join(process.cwd(), '.update-happened')
writeFileSync(updateMarkerPath, JSON.stringify({
timestamp: new Date().toISOString(),
fromVersion: versionCheck.localVersion,
toVersion: versionCheck.remoteVersion,
method: 'github-api'
}, null, 2))
// Step 10: Install dependencies & rebuild
const hasNpm = await which('npm')
if (!hasNpm) {
console.log('⚠️ npm not found - please run: npm install && npm run build')
return 0
}
process.stdout.write('📦 Installing dependencies...')
const installCode = await run('npm', ['ci', '--silent'], { stdio: 'ignore' })
if (installCode !== 0) {
await run('npm', ['install', '--silent'], { stdio: 'ignore' })
}
process.stdout.write(' ✓\n')
process.stdout.write('🔨 Building project...')
const buildCode = await run('npm', ['run', 'build'], { stdio: 'ignore' })
if (buildCode !== 0) {
// Build failed - rollback
process.stdout.write(' ❌\n')
console.log('⚠️ Build failed, rolling back to previous version...')
// Restore from rollback
for (const file of criticalFiles) {
const srcPath = join(rollbackDir, file)
const destPath = join(process.cwd(), file)
if (!existsSync(srcPath)) continue
try {
rmSync(destPath, { recursive: true, force: true })
if (statSync(srcPath).isDirectory()) {
cpSync(srcPath, destPath, { recursive: true })
} else {
cpSync(srcPath, destPath)
}
} catch {
// Continue
}
}
console.log('✅ Rollback complete - using previous version')
rmSync(rollbackDir, { recursive: true, force: true })
return 1
}
process.stdout.write(' ✓\n')
// Step 11: Verify integrity (check if critical files exist)
process.stdout.write('🔍 Verifying integrity...')
const criticalPaths = [
'dist/index.js',
'package.json',
'src/index.ts'
]
let integrityOk = true
for (const path of criticalPaths) {
if (!existsSync(join(process.cwd(), path))) {
integrityOk = false
break
}
}
if (!integrityOk) {
process.stdout.write(' ❌\n')
console.log('⚠️ Integrity check failed, rolling back...')
// Restore from rollback
for (const file of criticalFiles) {
const srcPath = join(rollbackDir, file)
const destPath = join(process.cwd(), file)
if (!existsSync(srcPath)) continue
try {
rmSync(destPath, { recursive: true, force: true })
if (statSync(srcPath).isDirectory()) {
cpSync(srcPath, destPath, { recursive: true })
} else {
cpSync(srcPath, destPath)
}
} catch {
// Continue
}
}
console.log('✅ Rollback complete - using previous version')
rmSync(rollbackDir, { recursive: true, force: true })
return 1
}
process.stdout.write(' ✓\n')
// Clean rollback backup on success
rmSync(rollbackDir, { recursive: true, force: true })
console.log(`\n✅ Updated successfully! (${versionCheck.localVersion}${versionCheck.remoteVersion})`)
// Different behavior for Docker vs Host
if (updateMode === 'docker') {
console.log('<27> Docker mode: Update complete')
console.log(' Container will restart automatically if configured\n')
// In Docker, don't restart - let orchestrator handle it
// Just exit cleanly so Docker can restart the container
return 0
} else {
console.log('<27>🔄 Restarting in same process...\n')
// In host mode, signal restart needed
return 0
}
}
// =============================================================================
// ENTRY POINT
// =============================================================================
/**
* Cleanup temporary files
*/
function cleanup() {
const tempDirs = [
'.update-backup',
'.update-rollback',
'.update-extract',
'.update-download.zip'
]
for (const dir of tempDirs) {
const path = join(process.cwd(), dir)
try {
if (existsSync(path)) {
if (statSync(path).isDirectory()) {
rmSync(path, { recursive: true, force: true })
} else {
rmSync(path, { force: true })
}
}
} catch {
// Ignore cleanup errors
}
}
}
async function main() {
// Check if updates are enabled in config
const configData = readJsonConfig([
'src/config.jsonc',
'config.jsonc',
'src/config.json',
'config.json'
])
if (configData?.update?.enabled === false) {
console.log('\n⚠ Updates are disabled in config.jsonc')
console.log('To enable: set "update.enabled" to true in src/config.jsonc\n')
return 0
}
// Global timeout: 5 minutes max
const timeout = setTimeout(() => {
console.error('\n⏱ Update timeout (5 min) - cleaning up...')
cleanup()
process.exit(1)
}, 5 * 60 * 1000)
try {
const code = await performUpdate()
clearTimeout(timeout)
// Final cleanup of temporary files
cleanup()
process.exit(code)
} catch (err) {
clearTimeout(timeout)
cleanup()
throw err
}
}
main().catch((err) => {
console.error('\n❌ Update failed with error:', err)
console.error('\nCleaning up and reverting...')
cleanup()
process.exit(1)
})