#!/usr/bin/env node import chalk from 'chalk' import inquirer from 'inquirer' import { execSync } from 'child_process' import { detectOS, updateSoftware, createAoDirectories, 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 { aoIsInstalled } from './scripts/features/ao-server.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' import { cardMenu } from './scripts/cards.js' // These should become .env variables that are loaded intelligently let distro let memberName // This does not work function exitIfRoot() { try { execSync('if [ "$EUID" -eq 0 ] then echo 1') 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', 'Alchemy', 'Configure', '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 'Configure': 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')) const topPriority = await getTopPriorityText() if(topPriority) { console.log('Top priority:', topPriority) } else { console.log('Error contacting server, is your AO server running? AO features might not work.') } } let aoMenuChoices = [] if(loggedIn) { aoMenuChoices.push( 'Deck', 'Chat', ) } 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 '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']) while(await cardMenu()) {} break case 'Chat': while(await chatMenu()) {} 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', 'AO install wizard', '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 'AO install wizard': await aoInstallWizard() break case 'Install other AO version': await chooseAoVersion() 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 } // Friendly interactive install wizard walks you through the entire process of installing and configuring a version of the AO and its features async function aoInstallWizard() { asciiArt('AO Installer') console.log('Welcome to the AO installer. The Coalition of Invisible Colleges is currently in licensing negotiations between the Autonomous Organization and Zen to acquire rights to display Zen\'s welcome text here, which is better than this irony.') const level = await chooseInstallLevel() if(!level) { console.log('Install canceled.') return } console.log('Proceeding with', level, 'installation.') const version = await chooseAoVersion() if(!version) { console.log('Install canceled.') return } // Ask them how they would like to host the AO (private on this computer only, public via tor only, public website via HTTPS/SSL) updateSoftware() createAoDirectories() installRequired() setNodeVersion() if(!aoIsInstalled(version)) { installAo(version) } //configureAO() // set required ENV variables (are any still required? make all optional?) if(level === 'standard' || level === 'full') { //if(!features.bitcoin.isInstalled()) features.bitcoin.install() //if(!features.lightning.isInstalled()) features.lightning.install() console.log('Skipping manual, tor, bitcoin, lightning, jitsi, and configuration of themes, glossary, jubilee (coming soon)') } if(level === 'full') { console.log('Skipping youtube-dl, Signal, borg (coming soon)') //maybe can just loop through all feature modules here } console.log('Skipping SSL/Certbot (coming soon)') // Ask them at the beginning but do it here console.log('The AO is installed.') } // Asks if the user wants to do a minimal, standard, or full install and returns their answer async function chooseInstallLevel() { const answer = await inquirer.prompt({ name: 'level_menu', type: 'list', message: 'What kind of installation?', choices: [ { name: 'Minimal'.padEnd(11) + 'only core AO web server', value: 'minimal', short: 'minimal install' }, { name: 'Standard'.padEnd(11) + 'most AO features installed (recommended)', value: 'standard', short: 'standard install' }, { name: 'Full'.padEnd(11) + 'all AO features installed', value: 'full', short: 'full install' }, { name: 'Cancel', value: false } ] }) return answer.level_menu } // Detects which version(s) of the AO are installed (ao-3, ao-react, or ao-v) // todo: Maybe should move this to an option under the ao-server feature menu function detectAoVersion() { return aoEnv(AO_VERSION) } // Asks whether the user wants to install ao-svelte, ao-3, or ao-cli (only) and returns their choice async function chooseAoVersion() { console.log(`\n${headerStyle('Choose AO Version')}`) console.log('Active version:', aoEnv('AO_VERSION')) const answer = await inquirer.prompt({ name: 'version_menu', type: 'list', message: 'Please choose:', choices: [ { name: 'ao-svelte'.padEnd(12) + 'new and mobile-first, currently in prototype phase', value: 'ao-svelte', short: 'ao-svelte' }, { name: 'ao-3'.padEnd(12) + 'the original, created in Vue 3, polished and bug-free', value: 'ao-3', short: 'ao-3' }, { name: 'ao-cli only'.padEnd(12), value: 'ao-cli' }, 'Cancel' ] }) if(answer.version_menu === 'Cancel') { return false } setAoEnv('AO_VERSION', answer.version_menu) // todo: If the version has changed, install it now console.log('AO version choice saved.') return answer.version_menu } // 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 } const chosenFeature = features[answer.features_menu] const chosenFeatureName = (chosenFeature.hasOwnProperty('name') && chosenFeature.name.length >= 1) ? chosenFeature.name : answer.features_menu while(await oneFeatureMenu(chosenFeatureName, chosenFeature)) {} 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 }) } // 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()