#!/usr/bin/env node
import chalk from 'chalk'
import inquirer from 'inquirer'
import { execSync } from 'child_process'
import { detectOS , updateSoftware , 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 { 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'
// These should become .env variables that are loaded intelligently
let distro
let memberName
// This does not work
function exitIfRoot ( ) {
try {
execSync ( '[ "$EUID" -eq 0 ]' )
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' ,
'Features' ,
'Alchemy' ,
'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 'Features' :
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' ) )
console . log ( 'Top priority:' , await getTopPriorityText ( ) )
}
let aoMenuChoices = [ ]
if ( loggedIn ) {
aoMenuChoices . push (
'Chat' ,
'Deck' ,
)
}
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 'Chat' :
while ( await chatMenu ( ) ) { }
break
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' ] )
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' ,
'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 '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
}
// 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
}
while ( await oneFeatureMenu ( answer . features _menu , features [ answer . features _menu ] ) ) { }
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
} )
}
// Detects which version(s) of the AO are installed (ao-3, ao-react, or ao-v)
function detectAoVersion ( ) {
}
// 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 ( )