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
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()
|
|
|