// Functions related to intelligently installing the AO as a whole. Specific additional feature modules are each a file under ./features. import inquirer from 'inquirer' import path from 'path' import { execSync } from 'child_process' import { aoEnv, setAoEnv } from './settings.js' import { detectOS, updateSoftware, isInstalled } from './system.js' import { isFolder, isFile } from './files.js' import { aoIsInstalled } from './features/ao-server.js' import { asciiArt } from './console.js' import { headerStyle, heading2 } from './styles.js' import features from './features/index.js' const AO_MEMES_PATH = path.join(process.env.HOME, '.ao/memes') const commonPackages = ['curl', 'wget', 'git', 'make', 'sqlite3', 'python', 'autoconf-archive'] const debianPackages = ['build-essential', 'zlib1g-dev', 'libtool-bin', 'autoconf', 'automake autotools-dev', 'libgmp-dev', 'libsqlite3-dev', 'python3', 'python3-mako', 'libsodium-dev', 'pkg-config', 'libev-dev', 'libcurl4-gnutls-dev', 'libssl-dev', 'fakeroot', 'devscripts'] const archPackages = ['gmp', 'pkgconf', 'libev', 'python-mako', 'python-pip', 'net-tools', 'zlib', 'libsodium', 'gettext', 'nginx'] // plus base-devel which is a group of packages const fedoraPackages = ['autoconf', 'automake', 'python3', 'python3-mako', 'pkg-config', 'fakeroot', 'devscripts'] const minimalFeatures = [ 'ao-cli', 'tor', 'alchemy' ] const standardFeatures = [ 'bitcoin', 'lightning', 'jitsi', 'files', 'youtube-dl', 'themes', 'glossary' ] const fullFeatures = Object.keys(features).filter(feature => ![...minimalFeatures, ...standardFeatures].includes(feature)) // Friendly interactive install wizard walks you through the entire process of installing and configuring a version of the AO and its features export default async function aoInstallWizard() { asciiArt('AO Installer') console.log('Welcome to the AO installer. The Coalition of Invisible Colleges is currently in licensing negotiations between the Autonomous Organization and Zen to acquire rights to display Zen\'s welcome text here, which is better than this irony.') const level = await chooseInstallLevel() if(!level) { console.log('Install canceled.') return } console.log('Proceeding with', level, 'installation.') const version = await chooseAoVersion() if(!version) { console.log('Install canceled.') return } // Ask them how they would like to host the AO (private on this computer only, public via tor only, public website via HTTPS/SSL) updateSoftware() createAoDirectories() installRequired() setNodeVersion() if(!aoIsInstalled(version)) { installAo(version) } //configureAO() // set required ENV variables (are any still required? make all optional?) if(level === 'standard' || level === 'full') { //if(!features.bitcoin.isInstalled()) features.bitcoin.install() //if(!features.lightning.isInstalled()) features.lightning.install() console.log('Skipping manual, tor, bitcoin, lightning, jitsi, and configuration of themes, glossary, jubilee (coming soon)') } if(level === 'full') { console.log('Skipping youtube-dl, Signal, borg (coming soon)') //maybe can just loop through all feature modules here } console.log('Skipping SSL/Certbot (coming soon)') // Ask them at the beginning but do it here console.log('The AO is installed.') } // Asks if the user wants to do a minimal, standard, or full install and returns their answer async function chooseInstallLevel() { let answer try { answer = await inquirer.prompt({ name: 'level_menu', type: 'list', message: 'What kind of installation?', choices: [ { name: 'Minimal'.padEnd(11) + 'only core AO web server', value: 'minimal', short: 'minimal install' }, { name: 'Standard'.padEnd(11) + 'most AO features installed (recommended)', value: 'standard', short: 'standard install' }, { name: 'Full'.padEnd(11) + 'all AO features installed', value: 'full', short: 'full install' }, { name: 'Cancel', value: false } ] }) } catch(error) { if (error === 'EVENT_INTERRUPTED') { console.log('\nESC') return false } } return answer.level_menu } // Asks whether the user wants to install ao-svelte, ao-3, or ao-cli (only) and returns their choice export async function chooseAoVersion() { console.log(`\n${headerStyle('Choose AO Version')}`) console.log('Active version:', aoEnv('AO_VERSION')) let answer try { answer = await inquirer.prompt({ name: 'version_menu', type: 'list', message: 'Please choose:', choices: [ { name: 'ao-svelte'.padEnd(12) + 'new and mobile-first, currently in prototype phase', value: 'ao-svelte', short: 'ao-svelte' }, { name: 'ao-3'.padEnd(12) + 'the original, created in Vue 3, polished and bug-free', value: 'ao-3', short: 'ao-3' }, { name: 'ao-cli only'.padEnd(12), value: 'ao-cli' }, 'Cancel' ] }) } catch(error) { if (error === 'EVENT_INTERRUPTED') { console.log('\nESC') return false } } if(answer.version_menu === 'Cancel') { return false } setAoEnv('AO_VERSION', answer.version_menu) // todo: If the version has changed, install it now console.log('AO version choice saved.') return answer.version_menu } function checkAoDirectories() { return isFolder(AO_MEMES_PATH) } // Creates the directories to store the AO's database, memes, manual, and maybe other things, ~/.ao by standard export function createAoDirectories() { try { execSync('mkdir -p ' + AO_MEMES_PATH) } catch(error) { console.log('Error creating ~/.ao/memes directory. Maybe it already exists.') } } // Returns true if nvm is installed function checkNvm() { try { execSync('. ~/.nvm/nvm.sh && nvm -v') return true } catch(error) { console.log(error.stdout.toString()) return false } } function installNvm() { try { execSync('[ -z $NVM_DIR ]') execSync('source ~/Alchemy/ingredients/iron && install_nvm') console.log(`Installed nvm.`) return true } catch(err) { return false } } // Returns an object containing a list of requirement: installed (boolean) export function checkRequired() { const distro = detectOS() if(!distro) { console.log("Your OS was not recognized, so nothing was checked, sorry.") return false } let summary = {} console.log('Checking AO prerequisites for ' + distro.toTitleCase() + '...') // Check OS-specific requirements const checkEachPackage = (packages) => { packages.forEach(packageName => { summary[packageName] = isInstalled(packageName) }) } checkEachPackage(commonPackages) summary['nvm'] = checkNvm() switch(distro) { case 'debian': // Some of these might not be required checkEachPackage(debianPackages) break case 'arch': summary['base-devel'] = isInstalled('base-devel', true, true) checkEachPackage(archPackages) break case 'fedora': checkEachPackage(fedoraPackages) break } return summary } // Prints out a summary of the AO's installation status. Returns true if the AO is installed (Standard + ao-svelte or ao-3, not ao-cli-only) export function checkAo() { const width = 24 // Print status of each required package individually console.log(`\n${heading2('Required Software Packages')}`) const summary = checkRequired() Object.entries(summary).forEach(([packageName, isInstalled]) => { console.log(packageName.padEnd(width) + (isInstalled ? 'Installed' : 'Missing')) }) const prerequisitesInstalled = Object.entries(summary).every(([packageName, isInstalled]) => isInstalled) // Check for existence of required directories console.log(`\n${heading2('Folders & Config File')}`) const requiredDirectoriesExist = checkAoDirectories() console.log('~/.ao & ~/.ao/memes'.padEnd(width) + (requiredDirectoriesExist ? ' Created ' : 'Missing')) // Check for .env file const aoEnvFilePath = path.join(process.env.HOME, '.ao/.env') const hasEnvFile = isFile(aoEnvFilePath) console.log('~/.ao/.env'.padEnd(width) + (hasEnvFile ? 'Initialized' : 'Blank'), '\n') // Check for ao-server folder with node_modules folder (can do a better check?) console.log(`\n${heading2('AO Server + Client Version Installed')}`) const homePathsToCheck = ['ao-server', 'ao-svelte', 'ao-3'] let homePathsExist = [] homePathsToCheck.forEach(folderName => { const nodeModulesPath = path.join(process.env.HOME, folderName, 'node_modules') const exists = isFolder(nodeModulesPath) console.log(folderName.padEnd(width) + (exists ? 'Installed' : 'Not Installed')) if(exists) { homePathsExist.push(folderName) } }) const hasAnAo = (homePathsExist.includes('ao-server') && homePathsExist.includes('ao-svelte')) || homePathsExist.includes('ao-3') // Check which packages are installed, and based on three additive lists of requirements, determine Minimal, Standard, or Full install console.log(`\n${heading2('Optional Features')}`) let optionalInstalls = [] Object.entries(features).forEach(([shortname, feature]) => { const name = feature?.name || shortname const isInstalled = feature.hasOwnProperty('isInstalled') ? feature.isInstalled() : ['installed', 'enabled', 'running', 'synced'].includes(feature.status()) if(isInstalled) optionalInstalls.push(shortname) console.log(name.padEnd(width) + (isInstalled ? 'Installed' : 'Not Installed')) }) console.log(`\n${heading2('Summary')}`) let installAttained = null const otherPrereqs = requiredDirectoriesExist && hasAnAo && hasEnvFile if(otherPrereqs && prerequisitesInstalled && minimalFeatures.every(shortname => optionalInstalls.includes(shortname))) { installAttained = 'Minimal' } if(installAttained && standardFeatures.every(shortname => optionalInstalls.includes(shortname))) { installAttained = 'Standard' } if(installAttained === 'Standard' && fullFeatures.every(shortname => optionalInstalls.includes(shortname))) { installAttained = 'Full' } console.log(optionalInstalls.length + '/' + Object.keys(features).length, 'optional features installed.') if(!installAttained) { console.log("You have not installed the AO; the required packages were not detected.") } else { console.log('You have the packages installed for a', installAttained, 'install.') } console.log('Selected AO_VERSION is', aoEnv('AO_VERSION')) // Is it possible to check if npm i has already been called in all the node project folders? } // Installs core dependencies required by Alchemy and the AO export function installRequired() { const distro = detectOS() if(!distro) { console.log("Your OS was not recognized, so nothing was installed, sorry.") return false } console.log('Installing Alchemy and AO installation process core dependencies (fast if already installed)...') console.log(`(You may need to input your ${chalk.blue.bold("'sudo' password")} here)`) // Install on every OS installIfNotInstalled(commonPackages) installNvm() // Install OS-specific requirements switch(distro) { case 'debian': // Some of these might not be required installIfNotInstalled(debianPackages) break case 'arch': installIfNotInstalled('base-devel', true, true) installIfNotInstalled(archPackages) break case 'fedora': installIfNotInstalled(fedoraPackages) break } return true } // Sets node to the current version used by the AO function setNodeVersion() { execSync('source ~/Alchemy/ingredients/lead && source ~/Alchemy/ingredients/iron && set_node_to v16.13.0') }