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.

415 lines
13 KiB

3 years ago
#!/usr/bin/env node
import chalk from 'chalk'
import inquirer from 'inquirer'
3 years ago
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 { installAoAlias, getAoCliVersion, selfUpdate, downloadManual, updateManual } from './scripts/features.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
3 years ago
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.`)
3 years ago
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',
'Admin',
'Alchemy',
'Manual',
'Exit'
]
3 years ago
const answer = await inquirer.prompt({
name: 'main_menu',
type: 'list',
message: 'Please choose:',
choices: mainMenuChoices,
pageSize: mainMenuChoices.length
3 years ago
})
switch(answer.main_menu) {
case 'AO':
while(await useAoMenu()) {}
break
case 'Features':
while(await featuresMenu()) {}
3 years ago
break
case 'Admin':
while(await adminMenu()) {}
break
case 'Alchemy':
while(await alchemyMenu()) {}
3 years ago
break
case 'Manual':
if(!isFolder(AO_MANUAL_PATH)) {
console.log("Downloading the AO manual...")
if(downloadManual()) {
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 {
updateManual()
}
await printManualPage(AO_MANUAL_PATH) // Fencepost case - print overview page
let previousChoice
3 years ago
do {
previousChoice = await manualFolderAsMenu(AO_MANUAL_PATH, 'AO User Manual', 'Back to Main Menu', previousChoice + 1)
} while(previousChoice !== false)
3 years ago
break
case 'Exit':
farewell()
3 years ago
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`)
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
}
3 years ago
// 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
3 years ago
async function adminMenu() {
console.log(`\n${headerStyle('AO Admin Menu')}`)
const adminChoices = [
'Install \'ao\' alias for \'ao-cli\'',
'Update ao-cli',
3 years ago
'Check AO install',
'Update AO',
'Switch AO target server',
3 years ago
'Switch AO database',
'Import/Export state/decks',
'Check AO service',
'Watch logs now',
'Start/Stop AO service',
'Tests',
3 years ago
'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 'Install \'ao\' alias for \'ao-cli\'':
3 years ago
installAoAlias()
break
case 'Update ao-cli':
await selfUpdate()
break
case 'Check AO install':
case 'Update AO':
case 'Switch AO target server':
case 'Switch AO database':
case 'Import/Export state/decks':
case 'Check AO service':
case 'Watch logs now':
case 'Start/Stop AO service':
3 years ago
console.log("Not yet implemented.")
break
case 'Tests':
while(await testsMenu()) {}
break
3 years ago
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]) => {
3 years ago
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]
3 years ago
if(testFunction) await testFunction()
return true
}
// Prints the AO Admin Menu and executes the user's choice
async function alchemyMenu() {
console.log(`\n${headerStyle('Alchemy')}`)
const alchemyChoices = [
'Update software',
'Install AO prerequisites',
'Check bitcoin status',
'Back to Main Menu'
]
const answer = await inquirer.prompt({
name: 'alchemy_menu',
type: 'list',
message: 'Please choose:',
choices: alchemyChoices
})
switch(answer.alchemy_menu) {
case alchemyChoices[0]:
updateSoftware()
break
case alchemyChoices[1]:
installRequired()
break
case alchemyChoices[2]:
let stdout = execSync('source ~/Alchemy/ingredients/lead && source ~/Alchemy/ingredients/gold && bitcoin_is_synced')
console.log(`${stdout}`)
break
default:
return false
}
return true
}
// Prints the Configure AO Features menu and executes the user's choice
async function featuresMenu() {
console.log(`\n${headerStyle('Configure AO Features')}`)
const status = {
off: ' ' + chalk.grey('Off') + ' ',
ins: chalk.yellow('Installed') + ' ',
ena: ' ' + greenChalk('Enabled') + ' ',
run: ' ' + greenChalk('Running') + ' ',
err: ' ' + chalk.red('Error') + ' '
}
let widest = 9
3 years ago
const features = [
`Tor ${status.run} connect AOs p2p`,
`Bitcoin ${status.run} payments`,
`Lightning ${status.ins} payments`,
`nginx ${status.ins} host AO publicly over the world wide web`,
`SSL/Certbot ${status.ins} HTTPS for public web AO`,
`Jitsi ${status.err} secure video chat`,
`Signal ${status.off} notifications`,
`File hosting ${status.err} file attachments on cards`,
`youtube-dl ${status.ins} cache web videos`,
`Borg ${status.ins} backup`,
`Encryption ${status.err} serverside secret messages`,
`Themes ${status.err} custom themes`,
`Glossary ${status.err} custom glossary`,
`Jubilee ${status.ena} monthly points creation event`,
3 years ago
'Back to Main Menu'
]
const answer = await inquirer.prompt({
name: 'features_menu',
type: 'list',
message: 'Please choose:',
choices: features,
pageSize: features.length
})
switch(answer.features_menu) {
case 'Back to Main Menu':
return false
default:
console.log("Not yet implemented")
return true
}
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 getAoCliVersion())
return false
}
return true
}
3 years ago
// 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)
}
3 years ago
// 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)
3 years ago
}
await unicornPortal(650)
// Main AO title screen and flavor text
clearScreen()
asciiArt()
await welcome()
// Main loop
while(await mainMenu()) {}
process.exit(0)
}
await main()