#!/usr/bin/env node
import chalk from 'chalk'
import inquirer from 'inquirer'
import { execSync } from 'child_process'
import { detectOS , updateSoftware , createAoDirectories , installRequired , setNodeVersion } from './scripts/system.js'
import { checkAoEnvFile , aoEnv , setAoEnv , AO _ENV _FILE _PATH } from './scripts/settings.js'
import { unicornPortal , asciiArt , clearScreen , spinnerWait } from './scripts/console.js'
import { welcome , exclaim , roger , farewell } from './scripts/welcome.js'
import { printManualPage , manualFolderAsMenu , AO _MANUAL _PATH } from './scripts/manual.js'
import { isFolder , loadJsonFile } from './scripts/files.js'
import { sleep } from './scripts/util.js'
import { tests } from './scripts/tests.js'
import { headerStyle , greenChalk } from './scripts/styles.js'
import './scripts/strings.js'
// Import AO modular features
import * as features from './scripts/features/index.js'
import { aoIsInstalled } from './scripts/features/ao-server.js'
import { startPublicBootstrap } from './scripts/bootstrap.js'
import { isLoggedIn , loginPrompt , logout } from './scripts/session.js'
import { AO _DEFAULT _HOSTNAME } from './scripts/api.js'
import { getTopPriorityText } from './scripts/priority.js'
import { cardMenu } from './scripts/cards.js'
// These should become .env variables that are loaded intelligently
let distro
let memberName
// This does not work
function exitIfRoot ( ) {
try {
execSync ( 'if [ "$EUID" -eq 0 ] then echo 1' )
console . log ( ` ${ chalk . red . bold ( exclaim ( ) ) } Seems you're running this script as a superuser. ` )
console . log ( 'That might cause some issues with permissions and whatnot. Run this script as your default user (without sudo) and I\'ll ask you when I need superuser permissions' )
process . exit ( 1 )
} catch ( err ) { }
}
// Prints the AO Main Menu and executes the user's choice
async function mainMenu ( ) {
console . log ( ` \n ${ headerStyle ( 'AO Main Menu' ) } \n ` )
let mainMenuChoices = [
'AO' ,
'Alchemy' ,
'Configure' ,
'Manual' ,
'Exit'
]
const answer = await inquirer . prompt ( {
name : 'main_menu' ,
type : 'list' ,
message : 'Please choose:' ,
choices : mainMenuChoices ,
pageSize : mainMenuChoices . length
} )
let previousChoice
switch ( answer . main _menu ) {
case 'AO' :
while ( await useAoMenu ( ) ) { }
break
case 'Configure' :
do {
previousChoice = await featuresMenu ( previousChoice )
} while ( previousChoice !== false )
break
case 'Alchemy' :
while ( await adminMenu ( ) ) { }
break
case 'Manual' :
if ( ! isFolder ( AO _MANUAL _PATH ) ) {
console . log ( "Downloading the AO manual..." )
if ( features . manual . install ( ) ) {
console . log ( "Downloaded the AO Manual from the official git repo via http and saved to" , AO _MANUAL _PATH + '.' )
} else {
console . log ( 'Failed to download the AO manual, sorry.' )
return false
}
} else {
features . manual . update ( )
}
await printManualPage ( AO _MANUAL _PATH ) // Fencepost case - print overview page
do {
previousChoice = await manualFolderAsMenu ( AO _MANUAL _PATH , 'AO User Manual' , 'Back to Main Menu' , previousChoice + 1 )
} while ( previousChoice !== false )
break
case 'Exit' :
farewell ( )
await sleep ( 310 )
return false
}
return true
}
// Prints the Use AO Menu and executes the user's choice
async function useAoMenu ( ) {
const loggedIn = isLoggedIn ( )
console . log ( ` \n ${ headerStyle ( 'AO' ) } \n ` )
if ( loggedIn ) {
console . log ( 'Logged in as:' , aoEnv ( 'AO_CLI_SESSION_USERNAME' ) )
const topPriority = await getTopPriorityText ( )
if ( topPriority ) {
console . log ( 'Top priority:' , topPriority )
} else {
console . log ( 'Error contacting server, is your AO server running? AO features might not work.' )
}
}
let aoMenuChoices = [ ]
if ( loggedIn ) {
aoMenuChoices . push (
'Deck' ,
'Chat' ,
)
}
aoMenuChoices . push (
loggedIn ? 'Log Out' : 'Log In' ,
'Back to Main Menu'
)
const answer = await inquirer . prompt ( {
name : 'ao_menu' ,
type : 'list' ,
message : 'Please choose:' ,
choices : aoMenuChoices ,
pageSize : aoMenuChoices . length
} )
switch ( answer . ao _menu ) {
case 'Deck' :
//await todoList('My Todo List', ['Add full AO install process to ao-cli in convenient format', 'Add AO server unit tests to ao-cli', 'Get groceries', 'Play music every day'])
while ( await cardMenu ( ) ) { }
break
case 'Chat' :
while ( await chatMenu ( ) ) { }
break
case 'Log In' :
console . log ( '\nao-cli will use the AO API to log into the AO server at' , ( aoEnv ( 'AO_CLI_TARGET_HOSTNAME' ) || AO _DEFAULT _HOSTNAME ) + '.' )
await loginPrompt ( )
break
case 'Log Out' :
await logout ( )
//await spinnerWait('Logging out...')
break
case 'Back to Main Menu' :
return false
}
return true
}
// Prints a menu that allows you to join the global AO chatrooms
let publicBootstrapStarted = false
async function chatMenu ( ) {
let answers = { }
const PUBLIC _BOOTSTRAP _ENABLED = aoEnv ( 'PUBLIC_BOOTSTRAP_ENABLED' )
if ( PUBLIC _BOOTSTRAP _ENABLED ) {
// They previously enabled public bootstrapping, so check to make sure it is working and then hide the option
// todo: start and then verify functioning of p2p boostrap method here
// if it's already started don't start it again
if ( ! publicBootstrapStarted ) {
console . log ( "\nBootstrapping public AO swarm..." )
startPublicBootstrap ( )
console . log ( "Bootstrapped (just kidding)" )
publicBootstrapStarted = true
}
//answers['chat_menu'] = 'Enable p2p bootstrap'
}
const publicBootstrapMenuItem = PUBLIC _BOOTSTRAP _ENABLED ? 'Disable p2p bootstrap' : 'Enable p2p bootstrap'
const chatChoices = [
publicBootstrapMenuItem ,
'Join public chatroom' ,
'Join chatroom' ,
'Address Book' ,
'Back to Main Menu' ,
]
console . log ( ` \n ${ headerStyle ( 'AO Public Chatrooms' ) } \n ` )
const answer = await inquirer . prompt ( {
name : 'chat_menu' ,
type : 'list' ,
message : 'Please choose:' ,
choices : chatChoices
} )
switch ( answer . chat _menu ) {
case 'Enable p2p bootstrap' :
console . log ( 'In order to join AO public chatrooms, AO uses the hyperswarm protocol. Joining hyperswarm may expose your IP address to other users. (For high-security installations, don\'t use public bootstrap: you can still add tor addresses to your address book manually and join those chatrooms by name.)' )
setAoEnv ( 'PUBLIC_BOOTSTRAP_ENABLED' , true )
//message: Type \'public\' and press Enter to enable:')
break
case 'Disable p2p bootstrap' :
setAoEnv ( 'PUBLIC_BOOTSTRAP_ENABLED' , false )
console . log ( roger ( ) , 'Disabled public bootstrapping.' )
if ( publicBootstrapStarted ) {
// stop the bootstrap thing here
publicBootstrapStarted = false
}
break
case 'Join public chatroom' :
console . log ( 'Not yet implemented' )
break
case 'Join chatroom' :
console . log ( 'Not yet implemented' )
break
case 'Address Book' :
console . log ( 'The point of this address book is to make it possible to type short, one-word names and have them resolve to tor addresses.' )
console . log ( 'Name a piece of data by saying name=data. For example, doge=5uc41o1...onion. Then \'doge\' will return the .onion address.' )
console . log ( 'Querying with any synonym in a chain will return the final meanings they all point to.' )
console . log ( 'Keys can have multiple values.' )
break
case 'Back to Main Menu' :
return false
}
return true
}
// Prints the AO Admin Menu and executes the user's choice
// Maybe Alchemy menu should be installation and update, and admin menu should be more configuration & AO member admin
async function adminMenu ( ) {
console . log ( ` \n ${ headerStyle ( 'System Alchemy' ) } ` )
const adminChoices = [
'Update system software' ,
'AO install wizard' ,
'Switch AO target server' ,
'Import/export state/decks' ,
'Watch logs now' ,
'Tests' ,
'Update remote AOs' ,
'Back to Main Menu'
]
const answer = await inquirer . prompt ( {
name : 'admin_menu' ,
type : 'list' ,
message : 'Please choose:' ,
choices : adminChoices ,
pageSize : adminChoices . length ,
} )
switch ( answer . admin _menu ) {
case 'Update system software' :
updateSoftware ( )
break
case 'AO install wizard' :
await aoInstallWizard ( )
break
case 'Install other AO version' :
await chooseAoVersion ( )
break
case 'Switch AO target server' :
case 'Import/Export state/decks' :
case 'Watch logs now' :
case 'Update remote AOs' :
console . log ( "Not yet implemented." )
break
case 'Tests' :
while ( await testsMenu ( ) ) { }
break
default :
return false
}
return true
}
// Friendly interactive install wizard walks you through the entire process of installing and configuring a version of the AO and its features
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 ( ) {
const 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 }
]
} )
return answer . level _menu
}
// Detects which version(s) of the AO are installed (ao-3, ao-react, or ao-v)
// todo: Maybe should move this to an option under the ao-server feature menu
function detectAoVersion ( ) {
return aoEnv ( AO _VERSION )
}
// Asks whether the user wants to install ao-svelte, ao-3, or ao-cli (only) and returns their choice
async function chooseAoVersion ( ) {
console . log ( ` \n ${ headerStyle ( 'Choose AO Version' ) } ` )
console . log ( 'Active version:' , aoEnv ( 'AO_VERSION' ) )
const 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'
]
} )
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
}
// Prints the AO Unit Tests Menu and executes the user's choice
async function testsMenu ( ) {
console . log ( ` \n ${ headerStyle ( 'AO Unit Tests' ) } ` )
let testChoices = Object . entries ( tests ) . map ( ( [ menuTitle , testFunction ] ) => {
return menuTitle
} )
testChoices . push ( 'Back to Main Menu' )
const answer = await inquirer . prompt ( {
name : 'tests_menu' ,
type : 'list' ,
message : 'Please choose:' ,
choices : testChoices
} )
if ( answer . tests _menu === 'Back to Main Menu' ) {
return false
}
const testFunction = tests [ answer . tests _menu ]
if ( testFunction ) await testFunction ( )
return true
}
// Returns a colored capitalized status word
const styledStatus = ( fullWord ) => {
const lookup = {
off : ' ' + chalk . grey ( 'Off' ) + ' ' ,
installed : chalk . blue ( 'Installed' ) + ' ' ,
enabled : ' ' + greenChalk ( 'Enabled' ) + ' ' ,
running : ' ' + greenChalk ( 'Running' ) + ' ' ,
synced : ' ' + greenChalk ( 'Synced' ) + ' ' ,
error : ' ' + chalk . red ( 'Error' ) + ' '
}
return lookup [ fullWord . toLowerCase ( ) ] + ' '
}
// Prints the Configure AO Features menu and executes the user's choice
let featuresChoices
async function featuresMenu ( previousMenuChoice = 0 ) {
console . log ( ` \n ${ headerStyle ( 'Configure AO Features' ) } ` )
if ( ! featuresChoices ) {
featuresChoices = Object . entries ( features ) . map ( ( [ featureKey , feature ] ) => {
let featureName = featureKey
if ( feature . hasOwnProperty ( 'name' ) && feature . name . length >= 1 ) {
featureName = feature . name
}
const nameColumn = featureName . padEnd ( 17 )
const statusColumn = styledStatus ( feature . status ( ) )
const descriptionColumn = feature . description || ''
const choice = { name : nameColumn + statusColumn + descriptionColumn , value : featureKey , short : featureKey }
return choice
} )
featuresChoices . push (
'Back to Main Menu'
)
}
const answer = await inquirer . prompt ( {
name : 'features_menu' ,
type : 'list' ,
message : 'Please choose:' ,
choices : featuresChoices ,
default : previousMenuChoice ,
pageSize : featuresChoices . length
} )
if ( answer . features _menu === 'Back to Main Menu' ) {
return false
}
const chosenFeature = features [ answer . features _menu ]
const chosenFeatureName = ( chosenFeature . hasOwnProperty ( 'name' ) && chosenFeature . name . length >= 1 ) ? chosenFeature . name : answer . features _menu
while ( await oneFeatureMenu ( chosenFeatureName , chosenFeature ) ) { }
return answer . features _menu
}
// Prints the menu options for a specific feature.
// Each feature module can export functions for status, install, version, update and these will be listed in the menu based on context
// If the module also has a menu: field, these menu items will each be appended if the name is truthy when calculated
// Prints all the standard-named features plus features listed under 'menu'
async function oneFeatureMenu ( name , feature ) {
console . log ( ` \n ${ headerStyle ( name ) } ` )
const featureChoices = [ ]
const status = feature ? . status ( ) || false
if ( ! status ) {
console . log ( "This AO feature module lacks a status() function, not sure which menu items to display." )
return false
}
if ( status === 'off' ) {
if ( feature . hasOwnProperty ( 'install' ) ) {
featureChoices . push ( { name : 'Install ' + name , value : 'install' } )
menuItemCount ++
}
} else {
if ( feature . hasOwnProperty ( 'update' ) ) {
featureChoices . push ( { name : 'Update ' + name , value : 'update' } )
}
if ( feature . hasOwnProperty ( 'uninstall' ) ) {
featureChoices . push ( { name : 'Uninstall ' + name , value : 'uninstall' } )
}
}
if ( feature . hasOwnProperty ( 'menu' ) ) {
feature . menu . forEach ( menuItem => {
const menuItemName = typeof menuItem . name === 'function' ? menuItem . name ( ) : menuItem . name
if ( ! menuItemName ) {
return
}
const menuItemValue = typeof menuItem . value === 'function' ? menuItem . value ( ) : menuItem . value
// todo: uninstall option will go here also
featureChoices . push ( { name : menuItemName , value : menuItemValue } )
} )
}
if ( featureChoices . length < 1 ) {
console . log ( "Nothing to do yet on this feature, please check back soon." )
return false
}
featureChoices . push (
'Back to Features'
)
const answer = await inquirer . prompt ( {
name : 'feature_menu' ,
type : 'list' ,
message : 'Please choose:' ,
choices : featureChoices
} )
if ( answer . feature _menu === 'Back to Features' ) {
return false
}
if ( Object . keys ( feature ) . includes ( answer . feature _menu ) ) {
await feature [ answer . feature _menu ] ( )
return true
}
console . log ( 'Not yet implemented' )
return true
}
// Prints the AO Admin Menu and executes the user's choice
async function aoInstallMenu ( ) {
console . log ( ` \n ${ headerStyle ( 'Alchemy' ) } ` )
const aoServerChoices = [
{ name : 'Install AO prerequisites' , value : 'prereqs' } ,
'Check AO install' ,
'Update AO' ,
'Check AO service' ,
'Start/Stop AO service' ,
'Switch AO database' ,
'Switch AO version' ,
'Back to Main Menu'
]
const answer = await inquirer . prompt ( {
name : 'install_menu' ,
type : 'list' ,
message : 'Please choose:' ,
choices : aoServerChoices ,
pageSize : aoServerChoices . length
} )
switch ( answer . install _menu ) {
case 'prereqs' :
installRequired ( )
break
case 'Check AO install' :
case 'Update AO' :
case 'Check AO service' :
case 'Start/Stop AO service' :
case 'Switch AO database' :
case 'Switch AO version' :
console . log ( "Not yet implemented." )
return true
default :
return false
}
return true
}
// Prints the given todoItems (array of strings) and allows items to be checked and unchecked
async function todoList ( title , todoItems ) {
console . log ( ` \n ${ headerStyle ( title ) } ` )
const answer = await inquirer . prompt ( {
name : 'todo_list' ,
type : 'checkbox' ,
message : 'Check or uncheck items with Spacebar:' ,
choices : todoItems
} )
}
// Returns false if a flag means the program should now terminate
// -v Print version info
async function handleArgs ( args ) {
switch ( args [ 0 ] ) {
case '--version' :
case '-v' :
console . log ( await features [ 'ao-cli' ] . version ( ) )
return false
}
return true
}
// Main entry point
async function main ( ) {
// Print version info etc. no matter what
const nodePath = process . argv [ 0 ]
const aoCliPath = process . argv [ 1 ]
const args = process . argv . slice ( 2 )
let shouldTerminate = ! await handleArgs ( args )
if ( shouldTerminate ) {
process . exit ( 0 )
}
// No root allowed (todo)
exitIfRoot ( )
// Loading screen, display some quick info during the fun animation
distro = aoEnv ( 'DISTRO' )
if ( ! distro ) {
distro = detectOS ( )
setAoEnv ( 'DISTRO' , distro )
}
if ( checkAoEnvFile ( ) ) {
console . log ( 'AO .env file exists at' , AO _ENV _FILE _PATH )
} else {
console . log ( 'AO .env file does not exist at' , AO _ENV _FILE _PATH )
}
await unicornPortal ( 650 )
// Main AO title screen and flavor text
clearScreen ( )
asciiArt ( )
await welcome ( )
// Main loop
while ( await mainMenu ( ) ) { }
process . exit ( 0 )
}
await main ( )