#!/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 { 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 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', 'Admin', 'Alchemy', 'Manual', 'Exit' ] const answer = await inquirer.prompt({ name: 'main_menu', type: 'list', message: 'Please choose:', choices: mainMenuChoices, pageSize: mainMenuChoices.length }) switch(answer.main_menu) { case 'AO': while(await useAoMenu()) {} break case 'Features': while(await featuresMenu()) {} break case 'Admin': while(await adminMenu()) {} break case 'Alchemy': while(await alchemyMenu()) {} 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 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`) 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('AO Admin Menu')}`) const adminChoices = [ 'Install \'ao\' alias for \'ao-cli\'', 'Update ao-cli', 'Check AO install', 'Update AO', 'Switch AO target server', 'Switch AO database', 'Import/Export state/decks', 'Check AO service', 'Watch logs now', 'Start/Stop AO service', 'Tests', '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\'': 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': 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 } // 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 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`, '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 } // 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()