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