From fefed7b061a35e03b10c00a40df6b372dbc68dd7 Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Tue, 11 Nov 2025 13:11:30 +0100 Subject: [PATCH] feat: Refactor setup script for improved user guidance and quick launch functionality 3/3 --- scripts/installer/setup.mjs | 354 +++++++++++++++++++++--------------- 1 file changed, 205 insertions(+), 149 deletions(-) diff --git a/scripts/installer/setup.mjs b/scripts/installer/setup.mjs index 823c036..96785bd 100644 --- a/scripts/installer/setup.mjs +++ b/scripts/installer/setup.mjs @@ -1,232 +1,288 @@ #!/usr/bin/env node /** - * Microsoft Rewards Bot - First-Time Setup Script + * Microsoft Rewards Bot - Setup & Quick Launcher * - * This script handles initial project setup: + * SMART BEHAVIOR: + * + * First-time setup (if dist/, accounts.jsonc, or node_modules missing): * 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 + * 6. Offers to start the bot immediately * - * IMPORTANT: This script does NOT launch the bot automatically. - * After setup, run: npm start + * Already configured (all files present): + * → Detects complete setup + * → Offers to start bot directly (npm start) + * → User can choose: Start / Re-run setup / Exit + * + * Usage: + * npm run setup # Interactive setup/launcher + * node scripts/installer/setup.mjs */ -import { spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; +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'); +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 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'); - + 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; + 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; + 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; + error('❌ Template file accounts.example.jsonc not found!') + return true } } async function prompt(question) { return await new Promise(resolve => { - process.stdout.write(question); + process.stdout.write(question) const onData = (data) => { - const ans = data.toString().trim(); - process.stdin.off('data', onData); - resolve(ans); - }; - process.stdin.on('data', onData); - }); + 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; + 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('\n⏸️ Please configure src/accounts.jsonc and save it, then answer yes.\n') + continue } - log('Please answer yes or no.'); + 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 }); + 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}`)); - }); - }); + 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']); + await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['-v']) } catch (e) { - throw new Error('npm not found in PATH. Install Node.js first.'); + 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'); - + log('\n🚀 MICROSOFT REWARDS BOT - FIRST-TIME SETUP') + log('════════════════════════════════════════════════════════════\n') + // Step 1: Create accounts file - const accountsExisted = createAccountsFile(); - + const accountsExisted = createAccountsFile() + // Step 2: Guide user through account configuration if (!accountsExisted) { - await guideAccountConfiguration(); + await guideAccountConfiguration() } else { - log('✓ Using existing accounts.jsonc\n'); + 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(); + 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); + 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']); - + 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']); - + log('\n🔨 Building TypeScript project...') + await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']) + // Step 6: Install Playwright browsers - await installPlaywrightBrowsers(); - + 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'); + 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'); + 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('Playwright chromium already installed (marker found).') + return } - log('Ensuring Playwright chromium browser is installed...'); + 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.'); + 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'); + warn('Failed to install Playwright chromium automatically. You can manually run: npx playwright install chromium') + } +} + +/** + * Check if setup is complete + * @returns {boolean} True if project is already set up + */ +function isSetupComplete() { + const distExists = fs.existsSync(path.join(PROJECT_ROOT, 'dist', 'index.js')) + const accountsExists = fs.existsSync(path.join(SRC_DIR, 'accounts.jsonc')) + const nodeModulesExists = fs.existsSync(path.join(PROJECT_ROOT, 'node_modules')) + + return distExists && accountsExists && nodeModulesExists +} + +/** + * Launch the bot directly (for already-configured projects) + */ +async function launchBot() { + log('\n🚀 MICROSOFT REWARDS BOT - QUICK START') + log('════════════════════════════════════════════════════════════\n') + log('✓ Project already configured') + log('✓ Starting bot with npm start...\n') + + try { + await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['start']) + } catch (err) { + error('\n❌ Failed to start bot: ' + err.message) + log('\n💡 Try running manually: npm start\n') + process.exit(1) } } async function main() { if (!fs.existsSync(SRC_DIR)) { - error('❌ Cannot find src directory at ' + SRC_DIR); - process.exit(1); + error('❌ Cannot find src directory at ' + SRC_DIR) + process.exit(1) } - process.chdir(PROJECT_ROOT); + 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); + // Check if project is already fully set up + if (isSetupComplete()) { + log('\n✅ Setup detected as complete!') + log(' • Build: dist/index.js ✓') + log(' • Accounts: src/accounts.jsonc ✓') + log(' • Dependencies: node_modules ✓\n') + + const choice = (await prompt('What would you like to do?\n [1] Start the bot (npm start)\n [2] Re-run setup\n [3] Exit\nChoice (1-3): ')).trim() + + if (choice === '1' || choice === '') { + await launchBot() + process.exit(0) + } else if (choice === '2') { + log('\n🔄 Re-running full setup...\n') + // Continue to performSetup below + } else { + log('\n👋 Goodbye!\n') + process.exit(0) } } - await performSetup(); - + // Perform full setup + await performSetup() + + // After setup, ask if user wants to start + log('\n🎯 Setup complete! Would you like to start the bot now?') + const startNow = (await prompt('Start bot? (yes/no): ')).toLowerCase() + + if (['yes', 'y'].includes(startNow)) { + log('\n🚀 Starting bot...\n') + await launchBot() + } else { + log('\n💡 To start the bot later, run: npm start') + log('💡 Or re-run this script: npm run setup\n') + } + // Pause if launched by double-click on Windows if (process.platform === 'win32' && process.stdin.isTTY) { - log('Press Enter to close...'); - await prompt(''); + log('Press Enter to close...') + await prompt('') } - - process.exit(0); + + process.exit(0) } // Allow clean Ctrl+C -process.on('SIGINT', () => { console.log('\nInterrupted.'); process.exit(1); }); +process.on('SIGINT', () => { console.log('\nInterrupted.'); process.exit(1) }) main().catch(err => { - error('\nSetup failed: ' + err.message); - process.exit(1); -}); + error('\nSetup failed: ' + err.message) + process.exit(1) +})