#!/usr/bin/env node
import inquirer from 'inquirer'
import chalk from 'chalk'
import chalkAnimation from 'chalk-animation'
import gradient from 'gradient-string'
import figlet from 'figlet'
import { createSpinner } from 'nanospinner'
import { execSync } from 'child_process'
import { runAllTests , testDict } from './scripts/tests.js'
const MANUAL _PATH = '/home/deicidus/ao-svelte/static/manual'
let distro
let memberName
// Chalk styles
const greenChalk = chalk . hex ( '#008800' )
const headerStyle = chalk . blue . bold . underline
const manualTitleStyle = greenChalk . bold . underline
// Preformatted phrases that can be used in backticked console.log strings
const theAO = ` the ${ greenChalk . bold ( 'AO' ) } `
const theMenu = ` the ${ greenChalk . bold ( 'Menu' ) } `
// Different sets of messages that can be randomly selected from
const greetingMessages = [ 'Portaling!' , 'You are a Unicorn!' , "Here we go!" , "Wow!" , "AO Loading..." , "Powering Up!" , "Unicorn Portal" , "Don't Panic!" ]
const welcomeMessages = [
` You turn a corner and are suddenly back in the halls of ${ theAO } . ` ,
` You make the sign of ${ theAO } and are whisked away on a gust of divine wind. ` ,
` You take a closer look at the folder you are in and realize it is a room. ` ,
` You draw an alchemical symbol and open a doorway into ${ theAO } . You step through. ` ,
` "Ah, there you are again!" The old man greets you as you step through the portal. ` ,
` A line of doge-masked worshippers glide by. By the time you exit the trance, you are in ${ theAO } . Doge bless. ` ,
` You spraypaint an ${ greenChalk . bold ( 'A' ) } superimposed on an ${ greenChalk . bold ( 'O' ) } and step through the portal. ` ,
` You receive a phone call. You answer it. You are in ${ theAO } . ` ,
` You dab fiercely, and when you raise your head, you are in ${ theAO } . ` ,
` A ship arrives and takes you out to sea. The ship sinks and you somehow wash up safely in ${ theAO } . ` ,
` You are reading in the Library when you find a strange book. Whatever you read next in the book is ${ theAO } . ` ,
` You plant a magic seed and it grows into a great tree. Climbing up into its branches, you know ${ theAO } is here. ` ,
` In the late afternoon, a warm sunbeam highlights motes of dust over your book. These motes are ${ theAO } . ` ,
` A black cat crosses your path. Most people wouldn't notice, but you know you have entered the AO. ` ,
` Dipping your brush in ink, you draw a perfect circle. This is ${ theAO } . ` ,
` You are offered a choice between two pills. However, you have secretly built up an immunity to both pills, and trick your opponent into taking one. Inconceviably, you are in ${ theAO } . ` ,
` A young man with spiky hair and golden skin appears before you in a halo of light. He guides you to ${ theAO } . ` ,
]
const menuMessages = [
` You see ${ theMenu } : ` ,
` A page of aged paper wafts into your hand. On it is written ${ theMenu } : ` ,
` A minstrel walks by playing a haunting melody, and in its harmonies you hear the refrain of ${ theMenu } : ` ,
` You pick up an apple from a nearby table and take a bite. You suddenly recall the words of ${ theMenu } : ` ,
` With a screeching sound, a page containing ${ theMenu } slowly emerges, line-by-line, from a dot-matrix printer: ` ,
` In the shadows cast in the cavernous space, you see the forms of ${ theMenu } : ` ,
` With a low hum, standing waves appear on the reflecting pool at your feet. Impossibly, they spell out ${ theMenu } : ` ,
` You see a box of candies labeled 'Eat Me'. Why not? you think. You find ${ theMenu } printed on the wrapper: ` ,
` You see an ornate bottle labeled 'Drink Me'. Why not? you think. You take a sip and shrink to the size of a doormouse. Now you can read ${ theMenu } scratched into the wainscoating: ` ,
` Cretaceous jungle plants tower overhead, overgrown from the safari room. The ancient fractals of the branches prefigure the structure of ${ theMenu } : ` ,
]
const exclamationMessages = [
'Woah there!' ,
'Shiver me timbers!' ,
'iNtErEsTiNg!' ,
'Turing\'s ghost!' ,
'Frack!' ,
'Frell!' ,
'Good grief!' ,
'With great power comes great responsibility.' ,
]
const farewellMessages = [
'Goodbye!' ,
'Goodbye! Goodbye! Goodbye...!' ,
'The AO will always be with you.' ,
'Please return soon; the AO needs your virtue.' ,
'The AO will await your return.' ,
'Doge bless.' ,
'Please remember the AO throughout your day as a ward against evil.' ,
'Remember your PrioriTEA™.' ,
'Know thyself.' ,
'With great power comes great responsibility.' ,
'The AO is a state of mind.'
]
// Returns a random int between min and max (inclusive)
function randomInt ( min , max ) {
min = Math . ceil ( min ) ;
max = Math . floor ( max ) ;
return Math . floor ( Math . random ( ) * ( max - min + 1 ) ) + min ;
}
// Returns a random item from the given array
function selectRandom ( arrayToChooseFrom ) {
return arrayToChooseFrom [ randomInt ( 0 , arrayToChooseFrom . length - 1 ) ]
}
// Waits for the given number of milliseconds (or a brief pause by default)
const sleep = ( ms = 550 ) => new Promise ( ( r ) => setTimeout ( r , ms ) )
// This does not work
function exitIfRoot ( ) {
try {
execSync ( '[ "$EUID" -eq 0 ]' )
console . log ( ` ${ chalk . red . bold ( selectRandom ( exclamationMessages ) ) } 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 ) { }
}
// Check for an AO env file at ~/.ao/.env and returns true if it exists
function loadAoEnvFile ( ) {
try {
execSync ( '[ -f "~/.ao/.env" ]' )
console . log ( 'AO .env file exists at ~/.ao/.env' )
return true
} catch ( err ) {
console . log ( 'AO .env file does not exist at ~/.ao/.env' )
return false
}
}
// Displays a brief randomly-selected rainbow-animated phrase
async function unicornPortal ( ms ) {
const randomGreetingMessage = selectRandom ( greetingMessages )
const rainbowTitle = chalkAnimation . rainbow ( randomGreetingMessage + '\n' )
await sleep ( ms )
rainbowTitle . stop ( )
}
// Prints the given message to the screen in the given ASCII art style. Here is a list of decent styles:
const asciiFonts = [ 'Standard' , 'Digital' , 'Bubble' , 'Script' , 'Mini' , 'Banner' , 'Alphabet' , 'Avatar' , 'Chunky' , 'Computer' , 'Contessa' , 'Gothic' , 'Invita' , 'Lockergnome' , 'Madrid' , 'Morse' , 'Moscow' , 'Pawp' , 'Pepper' , 'Pyramid' , 'Rectangles' , 'Shadow' , 'Short' , 'Slant' , 'Small' , 'Stampatello' , 'Stop' , 'Straight' , 'Thick' , 'Thin' , 'Weird' ]
async function asciiArt ( message , style ) {
const randomFont = selectRandom ( asciiFonts )
let art = figlet . textSync ( message || 'Autonomous Organization' , { font : randomFont } )
art = centerLines ( art )
console . log ( gradient . pastel . multiline ( art ) )
}
// Clears the console
function clearScreen ( ) {
console . clear ( )
}
// Prints a random RPG-style welcome message to contextualize the AO experience and the main menu
async function welcome ( ) {
const randomWelcomeMessage = selectRandom ( welcomeMessages )
const randomMenuMessage = selectRandom ( menuMessages )
const welcomeMessage = ( ' ' + randomWelcomeMessage + ' ' + randomMenuMessage ) . wordWrap ( process . stdout . columns - 2 )
// todo: line breaks would be more accurate if there were a function to count the length of a string minus the formatting codes (regex)
// right now the invisible formatting characters are counted so lines are wrapped early
console . log ( '\n' + welcomeMessage )
}
// Prints the AO Main Menu and executes the user's choice
async function mainMenu ( ) {
console . log ( ` \n ${ headerStyle ( 'AO Main Menu' ) } \n ` )
const answer = await inquirer . prompt ( {
name : 'main_menu' ,
type : 'list' ,
message : 'Please choose:' ,
choices : [
'Alchemy' ,
'Deck' ,
'Admin' ,
'Tests' ,
'Manual' ,
'Log Out' ,
'Exit' ,
]
} )
switch ( answer . main _menu ) {
case 'Alchemy' :
while ( await alchemyMenu ( ) ) { }
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 'Admin' :
while ( await adminMenu ( ) ) { }
break
case 'Tests' :
while ( await testsMenu ( ) ) { }
break
case 'Manual' :
await printManualPage ( MANUAL _PATH ) // Fencepost case - print overview page
let previousChoice
do {
previousChoice = await manualFolderAsMenu ( MANUAL _PATH , 'AO User Manual' , 'Back to Main Menu' , previousChoice )
} while ( previousChoice !== false )
break
case 'Log Out' :
await spinnerWait ( 'Logging out... (just kidding)' )
break
case 'Exit' :
console . log ( chalk . yellow . bold ( selectRandom ( farewellMessages ) ) )
await sleep ( 310 )
return false
}
return true
}
// Prints the AO Admin Menu and executes the user's choice
async function adminMenu ( ) {
console . log ( ` \n ${ headerStyle ( 'AO Admin Menu' ) } ` )
const adminChoices = [
'Install \'ao\' alias for \'ao-cli\'' ,
'Check AO install' ,
'Update AO' ,
'Configure AO features' ,
'Switch AO database' ,
'Import/Export state/decks' ,
'Check AO service' ,
'Watch logs now' ,
'Start/Stop AO service' ,
'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 adminChoices [ 0 ] :
installAoAlias ( )
break
case adminChoices [ 1 ] :
case adminChoices [ 2 ] :
case adminChoices [ 3 ] :
while ( await featuresMenu ( ) ) { }
break
case adminChoices [ 4 ] :
case adminChoices [ 5 ] :
case adminChoices [ 6 ] :
case adminChoices [ 7 ] :
case adminChoices [ 8 ] :
console . log ( "Not yet implemented." )
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 ( testDict ) . 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 = testDict [ 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 features = [
'nginx host AO publicly over the world wide web' ,
'SSL/Certbot HTTPS for public web AO' ,
'Tor connect AOs p2p' ,
'Bitcoin payments' ,
'Lightning payments' ,
'Jitsi secure video chat' ,
'Signal notifications' ,
'File hosting file attachments on cards' ,
'youtube-dl cache web videos' ,
'Borg backup' ,
'Encryption serverside secret messages' ,
'Themes custom themes' ,
'Glossary custom glossary' ,
'Jubilee 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
}
// Ask the user for their name and returns it
async function askName ( ) {
const answer = await inquirer . prompt ( {
name : 'member_name' ,
type : 'input' ,
message : 'What username would you like?'
} )
return answer . member _name
}
// 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 ( ) {
}
// Detects the operating system we are running on
function detectOS ( ) {
let distro
try {
execSync ( '[ -f "/etc/debian_version" ]' )
distro = 'debian'
console . log ( ` ${ greenChalk ( 'Debian' ) } , Ubuntu, or Raspbian OS detected. ` )
} catch ( err ) { }
try {
execSync ( '[ -f "/etc/arch-release" ]' )
distro = 'arch'
console . log ( ` ${ greenChalk ( 'Arch or Manjaro-based' ) } OS detected. ` )
} catch ( err ) { }
try {
execSync ( '[ -f "/etc/fedora-release" ]' )
distro = 'fedora'
console . log ( ` ${ greenChalk ( 'Fedora' ) } OS detected. ` )
} catch ( err ) { }
try {
execSync ( '[ $(uname | grep -c "Darwin") -eq 1 ]' )
distro = 'mac'
console . log ( ` ${ greenChalk ( 'MacOS' ) } detected. ` )
} catch ( err ) { }
if ( ! distro ) {
console . log ( "Your OS was not recognized, sorry." )
process . exit ( 1 )
}
return distro
}
// Runs the correct command to update all your software for any recognized OS
function updateSoftware ( ) {
distro = detectOS ( )
if ( ! distro ) {
console . log ( "Your OS was not recognized, so nothing was updated, sorry." )
return false
}
console . log ( 'Updating your software from repositories...' )
console . log ( ` (You may need to input your ${ chalk . blue . bold ( "'sudo' password" ) } here) ` )
switch ( distro ) {
case 'debian' :
execSync ( 'sudo apt update && sudo apt autoremove && sudo apt upgrade' )
break
case 'arch' :
execSync ( 'sudo pacman -Syu --noconfirm' )
break
case 'fedora' :
execSync ( 'sudo dnf update && sudo dnf upgrade' )
break
case 'mac' :
execSync ( 'install && sudo brew update' )
break
}
return true
}
// Adds a line to .bashrc to make 'ao' an alias for 'ao-cli', to simplify using the AO from the command line
function installAoAlias ( ) {
try {
execSync ( 'grep "ao=\'ao-cli\'" ~/.bashrc' )
console . log ( 'You can already type \'ao\' to launch ao-cli; the alias line already exists in ~/.bashrc.' )
} catch ( err ) {
execSync ( 'echo alias ao=\'ao-cli\' >> .bashrc' )
console . log ( 'Added alias line to ~/.bashrc. You can now type \'ao\' to launch ao-cli.' )
}
}
// Installs core dependencies required by Alchemy and the AO
function installRequired ( ) {
distro = detectOS ( )
if ( ! distro ) {
console . log ( "Your OS was not recognized, so nothing was installed, sorry." )
return false
}
console . log ( 'Installing Alchemy and AO installation process core dependencies (fast if already installed)...' )
console . log ( ` (You may need to input your ${ chalk . blue . bold ( "'sudo' password" ) } here) ` )
// Install OS-specific requirements
switch ( distro ) {
case 'debian' :
execSync ( 'sudo apt install build-essential' )
// Some of these might not be required
execSync ( 'source ~/Alchemy/ingredients/lead && install_if_needed sqlite3 zlib1g-dev libtool-bin autoconf autoconf-archive automake autotools-dev libgmp-dev libsqlite3-dev python python3 python3-mako libsodium-dev build-essential pkg-config libev-dev libcurl4-gnutls-dev libssl-dev fakeroot devscripts' )
break
case 'arch' :
try {
execSync ( '[[ ! $(pacman -Qg base-devel) ]]' )
execSync ( 'sudo pacman -S base-devel --noconfirm' )
} catch ( err ) { }
execSync ( 'source ~/Alchemy/ingredients/lead && install_if_needed python gmp sqlite3 autoconf-archive pkgconf libev python-mako python-pip net-tools zlib libsodium gettext nginx' )
break
case 'fedora' :
execSync ( 'source ~/Alchemy/ingredients/lead && install_if_needed sqlite3 autoconf autoconf-archive automake python python3 python3-mako pkg-config fakeroot devscripts' )
break
}
// Install on every OS
execSync ( 'source ~/Alchemy/ingredients/lead && install_if_needed git wget make' )
try {
execSync ( '[ -z $NVM_DIR ]' )
execSync ( 'source ingredients/iron && install_nvm' )
console . log ( ` Installed nvm. ` )
} catch ( err ) { }
return true
}
// Sets node to the current version used by the AO
function setNodeVersion ( ) {
execSync ( 'source ingredients/iron && set_node_to v16.13.0' )
}
// Displays a spinner for 1.2 secconds with the given messages during and after the timer completes
async function spinnerWait ( waitingMessage , doneMessage , ms = 1200 ) {
const spinner = createSpinner ( waitingMessage || 'Please wait...' ) . start ( )
await sleep ( ms )
spinner . success ( { text : doneMessage || 'Done.' } )
}
// Loads the text of a file
import fs from 'fs'
import readYamlAndMarkdown from 'yaml-head-loader'
async function loadTextFile ( path ) {
return new Promise ( ( resolve , reject ) => {
fs . readFile ( path , 'utf8' , function ( err , data ) {
if ( err ) {
console . log ( 'Reading file failed:' , err )
return null
}
resolve ( data )
} )
} )
}
// Loads the given text file and returns a dictionary of its contents parsed into a .header dict and .tail markdown content
async function loadYamlMarkdownFile ( path ) {
let text = await loadTextFile ( path )
if ( ! text ) {
return null
}
let dict = readYamlAndMarkdown ( text )
return dict
}
// Loads and returns the list of contents of a folder as an array
function lsFolder ( path ) {
try {
return fs . readdirSync ( path )
} catch ( err ) {
return null
}
}
// Returns true if the path is a folder
function isFolder ( path ) {
return Array . isArray ( lsFolder ( path ) )
}
// Adds a .toTitleCase() function to every string
String . prototype . toTitleCase = function ( ) {
return this . replace ( /\w\S*/g , function ( txt ) { return txt . charAt ( 0 ) . toUpperCase ( ) + txt . substr ( 1 ) . toLowerCase ( ) } )
}
String . prototype . wordWrap = function ( width = 80 ) {
return this . replace ( new RegExp ( ` (?: \\ S(?:.{0, ${ width } } \\ S)?(?: \\ s+|-| $ )|(?: \\ S{ ${ width } })) ` , 'g' ) , s => ` ${ s } \n ` ) . slice ( 0 , - 1 )
}
const repeatString = ( str , n ) => {
return new Array ( 1 + ( n || 0 ) ) . join ( str )
}
String . prototype . centerInConsole = function ( width = 80 ) {
const consoleWidth = process . stdout . columns
const padding = Math . floor ( ( consoleWidth - width ) / 2 )
const lines = this . split ( '\n' )
const centered = lines . map ( line => repeatString ( " " , padding ) + line )
return centered . join ( '\n' )
}
// Centers a one-line string within the given number of characters by adding padding to the left
String . prototype . centerInLine = function ( lineWidth , width = 80 ) {
const padding = Math . floor ( ( width - lineWidth ) / 2 )
return repeatString ( " " , padding ) + this
}
const centerLines = ( str ) => {
const lines = str . split ( '\n' )
const centered = lines . map ( line => line . centerInLine ( line . length , process . stdout . columns ) )
return centered . join ( '\n' )
}
// Removes numbered prefix such as 12_ and .md suffix, replaces underscores with spaces, and adds titlecase
function formatManualTitleString ( title ) {
// Remove .md suffix
if ( /\.md$/ . exec ( title ) ) {
title = title . substring ( 0 , title . length - 3 )
}
// Remove numbered prefix e.g., 12_
if ( /^\d*_/ . exec ( title ) ) {
title = title . split ( '_' ) . slice ( 1 ) . join ( '_' )
}
// Replace underscores with spaces
title = title . replaceAll ( '_' , ' ' )
return title . toTitleCase ( )
}
import { basename } from 'path'
import mdlogBuilder from 'mdlog'
// Prints the specified manual page to the screen
async function printManualPage ( path ) {
if ( isFolder ( path ) ) {
path += '/index.md'
}
const dict = await loadYamlMarkdownFile ( path )
const title = dict ? . meta ? . title || formatManualTitleString ( basename ( path ) )
const formattedTitle = manualTitleStyle ( title ) . centerInLine ( title . length ) . centerInConsole ( )
console . log ( '\n' + formattedTitle + '\n' )
const renderedMarkdown = mdlogBuilder . convert ( dict ? . tail . wordWrap ( ) ) . join ( '\n' ) . centerInConsole ( )
console . log ( renderedMarkdown )
}
// Given a path and file/folder name, it returns the appropriate manual title
// If it's a folder and there is an index.js inside that has a title: field, that overrides the title
// Otherwise it's the filename or foldername, minus anything before the first underscore (_), in titlecase
async function loadManualTitle ( path , fileOrFolder ) {
// If it's a .md file, check inside for a title: field
if ( /\.md$/ . exec ( fileOrFolder ) ) {
const indexTitle = ( await loadYamlMarkdownFile ( path + fileOrFolder ) ) ? . meta ? . title
if ( indexTitle ) {
return indexTitle
}
}
// If it's a folder, check for a title: field in index.md and return if exists
if ( isFolder ( path + fileOrFolder ) ) {
const indexPath = path + fileOrFolder + '/index.md'
const indexTitle = ( await loadYamlMarkdownFile ( indexPath ) ) ? . meta ? . title
if ( indexTitle ) {
return indexTitle
}
}
// Fall back to using the file/folder name as the title
return formatManualTitleString ( fileOrFolder )
}
// Render the manual folder or a subfolder as a menu
// First the index.js is listed using the folder name or title loaded from inside the file as the menu item title
// Next, any other files not starting with a number are loaded and displayed in discovered/arbitrary order
// Next, items starting with 0_, then 1_, and so on are displayed in order. You can mix files and folders.
// Selecting a menu item renders it. For .md files it renders it and shows the same level Manual menu again.
// For folders, it goes into that folder and renders it as a manual menu folder
// This allows arbitrarily nested Manual menu folders to be explored in a standardized menu system
async function manualFolderAsMenu ( path , menuTitle , backOption , previousMenuChoice = 0 ) {
if ( ! isFolder ( path ) ) {
return false
}
if ( path [ path . length - 1 ] != '/' ) {
path += '/'
}
let menuItems = [ ]
const folderContents = lsFolder ( path )
if ( folderContents . some ( fileOrFolder => fileOrFolder === 'index.md' ) ) {
const indexTitle = await loadManualTitle ( path , 'index.md' )
let indexMenuItem = { }
indexMenuItem [ indexTitle ] = 'index.md'
menuItems . push ( indexMenuItem )
}
let unNumberedItems = [ ]
let numberedItems = [ ]
const sortedFolderContents = folderContents . sort ( ( a , b ) => a . localeCompare ( b , undefined , { numeric : true } ) )
for ( let i = 0 ; i < sortedFolderContents . length ; i ++ ) {
const fileOrFolder = sortedFolderContents [ i ]
if ( fileOrFolder === 'index.md' ) {
continue
}
const potentialNumber = fileOrFolder . split ( '_' ) [ 0 ]
const initialNumber = parseInt ( potentialNumber )
const title = await loadManualTitle ( path , fileOrFolder )
const menuItem = { }
menuItem [ title ] = fileOrFolder
if ( isNaN ( initialNumber ) ) {
unNumberedItems . push ( menuItem )
} else {
numberedItems . push ( menuItem )
}
}
menuItems = menuItems . concat ( unNumberedItems , numberedItems )
const menuChoices = menuItems . map ( menuItem => Object . keys ( menuItem ) [ 0 ] )
menuChoices . push ( backOption )
console . log ( ` \n ${ headerStyle ( menuTitle ) } ` )
const answer = await inquirer . prompt ( {
name : 'manual_menu' ,
type : 'rawlist' ,
message : 'Please choose:' ,
choices : menuChoices ,
pageSize : menuChoices . length ,
default : previousMenuChoice
} )
const chosenMenuIndex = menuChoices . indexOf ( answer . manual _menu )
if ( answer . manual _menu === backOption ) {
return false
}
const chosenPath = path + Object . values ( menuItems . find ( menuItem => Object . keys ( menuItem ) [ 0 ] === answer . manual _menu ) ) [ 0 ]
await printManualPage ( chosenPath )
const newBackOption = backOption === 'Back to Main Menu' ? 'Back to Table of Contents' : 'Back to ' + menuTitle
let previousChoice
do {
previousChoice = await manualFolderAsMenu ( chosenPath , answer . manual _menu , newBackOption , previousChoice )
}
while ( previousChoice !== false )
return chosenMenuIndex
}
// Main entry point
async function main ( ) {
// No root allowed (todo)
exitIfRoot ( )
// Loading screen, display some quick info during the fun animation
distro = detectOS ( )
if ( ! loadAoEnvFile ( ) ) {
}
await unicornPortal ( 650 )
// Main AO title screen and flavor text
clearScreen ( )
asciiArt ( )
await welcome ( )
// Main loop
while ( await mainMenu ( ) ) { }
process . exit ( 0 )
}
await main ( )