// 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 , styledStatus } from './styles.js'
import features from './features/index.js'
import { yesOrNo } from './welcome.js'
const AO _PATH = path . join ( process . env . HOME , '.ao' )
const AO _MEMES _PATH = path . join ( AO _PATH , '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 (or only ao-cli)' , 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 async function checkAo ( ) {
const width = 19
const columnize = ( status ) => styledStatus ( status , width )
// Print status of each required package individually
console . log ( ` \n ${ heading2 ( 'Required Software Packages' ) } ` )
const summary = Object . entries ( checkRequired ( ) )
let installedRequirementCount = 0
summary . forEach ( ( [ packageName , isInstalled ] ) => {
if ( isInstalled ) installedRequirementCount ++
console . log ( packageName . padEnd ( width ) + columnize ( isInstalled ? 'Installed' : 'Missing' ) )
} )
const prerequisitesInstalled = summary . every ( ( [ packageName , isInstalled ] ) => isInstalled )
// Check for existence of required directories
console . log ( ` \n ${ heading2 ( 'Folders & Config File' ) } ` )
const requiredDirectoriesExist = checkAoDirectories ( )
console . log ( '~/.ao' . padEnd ( width ) + ( isFolder ( AO _PATH ) ? columnize ( 'Created' ) : columnize ( 'Missing' ) ) )
// Check for .env file
const aoEnvFilePath = path . join ( process . env . HOME , '.ao/.env' )
const hasEnvFile = isFile ( aoEnvFilePath )
console . log ( '~/.ao/.env' . padEnd ( width ) + columnize ( hasEnvFile ? 'Initialized' : 'Blank' ) )
console . log ( '~/.ao/memes' . padEnd ( width ) + columnize ( requiredDirectoriesExist ? 'Created' : 'Missing' ) )
// Check for node project folders and locally installed packages (npm i)
console . log ( ` \n ${ heading2 ( 'AO Server + Client Version Installed' ) } ` )
const homePathsToCheck = [ 'ao-server' , 'ao-svelte' , 'ao-3' ]
let homePathsExist = [ ]
let npmInstalled = [ ]
homePathsToCheck . forEach ( folderName => {
const folderPath = path . join ( process . env . HOME , folderName )
const exists = isFolder ( folderPath )
let npmI = false
if ( exists ) {
homePathsExist . push ( folderName )
try {
const stdout = execSync ( 'cd ' + folderPath + ' && npm list 2>&1' )
npmInstalled . push ( folderName )
npmI = true
} catch ( error ) {
const missingLocalPackagesCount = error . output . toString ( ) . match ( /npm ERR! missing:/g ) . length || 0
if ( missingLocalPackagesCount ) {
// Confirmation that not all dependencies have been installed
//console.log(folderName, 'has', missingLocalPackagesCount, 'npm packages left to install.')
} else {
// Unknown error
}
}
}
console . log ( folderName . padEnd ( width ) + columnize ( exists ? npmI ? 'Installed' : 'Downloaded' : 'Not Installed' ) )
} )
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 ) + columnize ( 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 ( installedRequirementCount + '/' + summary . length , 'required packages installed.' )
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 ( 'Recognized this set of packages as a' , installAttained , 'install.' )
}
console . log ( 'Selected AO_VERSION is' , aoEnv ( 'AO_VERSION' ) + '\n' )
if ( installAttained ) console . log ( 'The AO is installed.' )
else {
console . log ( 'The AO is not installed yet. Would you like to install it now?' )
const answer = await yesOrNo ( 'Start AO install wizard?' , true )
if ( answer ) {
await aoInstallWizard ( )
}
}
}
// 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' )
}