An interactive command-line interface (CLI) tool to help you install, use, and administer an AO instance.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

474 lines
15 KiB

#!/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()