feat: Refactor setup script for improved user guidance and quick launch functionality 3/3

This commit is contained in:
2025-11-11 13:11:30 +01:00
parent 82e5e71ffe
commit fefed7b061

View File

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