Browse Source

organized files, moved manual, improved manual

main
deicidus 2 years ago
parent
commit
d5650570f0
  1. 6
      README.md
  2. 585
      index.js
  3. 7
      manual/index.yml
  4. 608
      package-lock.json
  5. 8
      package.json
  6. 26
      scripts/bootstrap.js
  7. 10
      scripts/chalkStyles.js
  8. 38
      scripts/console.js
  9. 19
      scripts/features.js
  10. 40
      scripts/files.js
  11. 159
      scripts/manual.js
  12. 120
      scripts/settings.js
  13. 36
      scripts/strings.js
  14. 111
      scripts/system.js
  15. 6
      scripts/tests.js
  16. 18
      scripts/util.js
  17. 94
      scripts/welcome.js

6
README.md

@ -1,6 +1,6 @@
# ao-cli
A command-line interface (CLI) that helps you install, use, and configure the Autonomous Organization (AO). This package includes the command line tool `ao-cli` (alias `ao`) which makes it very easy to install and use the AO via the command line or a web browser. `ao-cli` is a Node/JavaScript CLI tool that wraps the functionality of Alchemy, AO administration, plus key AO features into one convenient interface.
A command-line interface (CLI) that helps you install, use, and configure the Autonomous Organization (AO). This package includes the command line tool `ao-cli` (alias `ao`) which makes it very easy to install and use the AO via the command line or a web browser. `ao-cli` is a Node/JavaScript CLI tool that wraps the functionality of Alchemy, AO administration, plus key AO features into one convenient interface. Command-line social networking.
To run immediately:
@ -14,5 +14,5 @@ Then you can run with `ao-cli`. (Inside the menu you will find an option to add
### Version History
0.0.2 Added browsable manual (must download ao-svelte)
0.0.1 Menus prototyped
0.0.5 Added browsable manual (must download ao-svelte)
0.0.1 Menus prototyped

585
index.js

@ -1,172 +1,58 @@
#!/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 inquirer from 'inquirer'
import { execSync } from 'child_process'
import { runAllTests, testDict } from './scripts/tests.js'
const MANUAL_PATH = '/home/deicidus/ao-svelte/static/manual'
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 } from './scripts/manual.js'
import { sleep } from './scripts/util.js'
import { tests } from './scripts/tests.js'
import { headerStyle } from './scripts/chalkStyles.js'
import './scripts/strings.js'
import { installAoAlias } from './scripts/features.js'
import { startPublicBootstrap } from './scripts/bootstrap.js'
// These should become .env variables that are loaded intelligently
const MANUAL_PATH = process.env.HOME + '/.ao/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(`${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) {}
}
// 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 mainMenuChoices = [
'Chat',
'Alchemy',
'Deck',
'Admin',
'Tests',
'Manual',
'Log Out',
'Exit',
]
const answer = await inquirer.prompt({
name: 'main_menu',
type: 'list',
message: 'Please choose:',
choices: [
'Alchemy',
'Deck',
'Admin',
'Tests',
'Manual',
'Log Out',
'Exit',
]
choices: mainMenuChoices,
pageSize: mainMenuChoices.length
})
switch(answer.main_menu) {
case 'Chat':
while(await chatMenu()) {}
break
case 'Alchemy':
while(await alchemyMenu()) {}
break
@ -181,22 +67,86 @@ async function mainMenu() {
break
case 'Manual':
await printManualPage(MANUAL_PATH) // Fencepost case - print overview page
let previousChoice
let previousChoice = 0
do {
previousChoice = await manualFolderAsMenu(MANUAL_PATH, 'AO User Manual', 'Back to Main Menu', previousChoice)
previousChoice = await manualFolderAsMenu(MANUAL_PATH, 'AO User Manual', 'Back to Main Menu', previousChoice + 1)
} while(previousChoice !== false)
break
case 'Log Out':
await spinnerWait('Logging out... (just kidding)')
break
case 'Exit':
console.log(chalk.yellow.bold(selectRandom(farewellMessages)))
farewell()
await sleep(310)
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
async function adminMenu() {
console.log(`\n${headerStyle('AO Admin Menu')}`)
@ -244,7 +194,7 @@ async function adminMenu() {
// 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]) => {
let testChoices = Object.entries(tests).map(([menuTitle, testFunction]) => {
return menuTitle
})
testChoices.push('Back to Main Menu')
@ -257,7 +207,7 @@ async function testsMenu() {
if(answer.tests_menu === 'Back to Main Menu') {
return false
}
const testFunction = testDict[answer.tests_menu]
const testFunction = tests[answer.tests_menu]
if(testFunction) await testFunction()
return true
}
@ -357,336 +307,6 @@ 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)
@ -694,7 +314,10 @@ async function main() {
// Loading screen, display some quick info during the fun animation
distro = detectOS()
if(!loadAoEnvFile()) {
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)

7
manual/index.yml

@ -1,7 +0,0 @@
# file.yml
YAML:
- A human-readable data serialization language
- https://en.wikipedia.org/wiki/YAML
yaml:
- A complete JavaScript implementation
- https://www.npmjs.com/package/yaml

608
package-lock.json generated

File diff suppressed because it is too large Load Diff

8
package.json

@ -19,13 +19,17 @@
"author": "Coalition of Invisible Colleges",
"license": "AGPL-3.0-or-later",
"dependencies": {
"ansimd": "^0.2.1",
"chalk": "^5.0.1",
"chalk-animation": "^2.0.2",
"crypto": "^1.0.1",
"envfile": "^6.17.0",
"figlet": "^1.5.2",
"gradient-string": "^2.0.1",
"hash.js": "^1.1.7",
"inquirer": "^8.2.4",
"marked": "^4.0.16",
"marked-terminal": "^5.1.1",
"mdlog": "^1.0.3",
"nanospinner": "^1.1.0",
"sha.js": "^2.4.11",
@ -33,10 +37,10 @@
"socket.io-client": "^4.5.1",
"superagent": "^7.1.6",
"uuid": "^8.3.2",
"wrap-ansi": "^8.0.1",
"yaml-head-loader": "^1.0.2"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {}
}
}

26
scripts/bootstrap.js vendored

@ -0,0 +1,26 @@
// The bootstrapping module uses the glossary in peers.json (later will use members from DB?)
// to look up tor addresses for the give shortname or SSH public key.
// We could just do all this in the AO, but the bootstrapper is for public / loose ties and the AO's explicit p2p is for close / private ties.
// The bootstrapper occasionally queries all of the tor addresses in your address book.
// If they are an AO with bootstrapping turned on, the AO server will respond with its public directory information.
// Since you have connected to them via their .onion address, it is assumed they are a known trusted party,
// so the information received will update your local directory information in your address book.
// Be careful to only connect to bootstrap servers you trust, with owners who will not add unsafe .onions to their own directory!
// An AO contacted at a tor address is considered a known party and an authority on announcing its own SSH key (if you trust the party).
// Therefore it works to receive an initial trusted .onion address, connect, get their directory, and use it to connect to others.
// You can copy the directory of each new peer, however these are marked with a hops: field to count how far away the trust gets.
// Maybe there should be a setting you announce to other nodes about whether they can share your .onion address or not (reshare)
// Start bootstrapping in the background
export function startPublicBootstrap() {
// Go through all the address book entries in my peers.json
// For each one that has a .onion address, do a fetch on it at the /bootstrap route
// If it responds with JSON containing directory information, increment the hops: field on all of it, and merge it with my file
// Must use entire new or old record. Use whichever one has fewer hops. Only replace if timestamp is newer.
// Again we are assuming that we know the owner of the .onion address and trust them, because a .onion is not spoofable.
}
// Kill the bootstrapping process
export function stopPublicBootstrap() {
}

10
scripts/chalkStyles.js

@ -0,0 +1,10 @@
import chalk from 'chalk'
// Chalk styles
export const greenChalk = chalk.hex('#008800')
export const headerStyle = chalk.blue.bold.underline
export const manualTitleStyle = greenChalk.bold.underline
// Preformatted phrases that can be used in backticked console.log strings
export const theAO = `the ${greenChalk.bold('AO')}`
export const theMenu = `the ${greenChalk.bold('Menu')}`

38
scripts/console.js

@ -0,0 +1,38 @@
import chalk from 'chalk'
import chalkAnimation from 'chalk-animation'
import gradient from 'gradient-string'
import figlet from 'figlet'
import { createSpinner } from 'nanospinner'
import { sleep } from './util.js'
import { selectRandom } from './util.js'
import { centerLines } from './strings.js'
// Displays a brief randomly-selected rainbow-animated phrase
const greetingMessages = ['Portaling!', 'You are a Unicorn!', "Here we go!", "Wow!", "AO Loading...", "Powering Up!", "Unicorn Portal", "Don't Panic!"]
export 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']
export 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
export function clearScreen() {
console.clear()
}
// Displays a spinner for 1.2 secconds with the given messages during and after the timer completes
export async function spinnerWait(waitingMessage, doneMessage, ms = 1200) {
const spinner = createSpinner(waitingMessage || 'Please wait...').start()
await sleep(ms)
spinner.success({ text: doneMessage || 'Done.' })
}

19
scripts/features.js

@ -0,0 +1,19 @@
// Functions to add and remove AO features
import { execSync } from 'child_process'
// Adds a line to .bashrc to make 'ao' an alias for 'ao-cli', to simplify using the AO from the command line
export 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.')
}
}
// Downloads the ao-manual repo to ~/.ao/manual/
export function downloadAoManual() {
console.log(execSync('git clone https://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-manual.git'))
}

40
scripts/files.js

@ -0,0 +1,40 @@
// Helpers for filesystem / folder manipulations and loading files
import fs from 'fs'
import readYamlAndMarkdown from 'yaml-head-loader'
// Loads the text of a file
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
export 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
export function lsFolder(path) {
try {
return fs.readdirSync(path).filter(fileOrFolderName => fileOrFolderName.length >= 1 && fileOrFolderName[0] !== '.')
} catch(err) {
return null
}
}
// Returns true if the path is a folder
export function isFolder(path) {
return Array.isArray(lsFolder(path))
}

159
scripts/manual.js

@ -1,21 +1,148 @@
import { parse } from 'yaml'
import fs from 'fs'
// Functions for loading the AO Manual, a hierarchy of markdown files
import chalk from 'chalk'
import inquirer from 'inquirer'
import { loadYamlMarkdownFile, lsFolder, isFolder } from './files.js'
import { repeatString, centerLines } from './strings.js'
import { headerStyle, manualTitleStyle } from './chalkStyles.js'
console.log("Parsing the manual file...")
// 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()
}
parse('3.14159')
// 3.14159
import { basename } from 'path'
import mdlogBuilder from 'mdlog'
import { marked } from 'marked'
import TerminalRenderer from 'marked-terminal'
marked.setOptions({
renderer: new TerminalRenderer({
showSectionPrefix: false,
})
})
parse('[ true, false, maybe, null ]\n')
// [ true, false, 'maybe', null ]
// 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)
}
const file = fs.readFileSync('./manual/index.yml', 'utf8')
const parsed = parse(file)
// { YAML:
// [ 'A human-readable data serialization language',
// 'https://en.wikipedia.org/wiki/YAML' ],
// yaml:
// [ 'A complete JavaScript implementation',
// 'https://www.npmjs.com/package/yaml' ] }
// Prints the specified manual page to the screen
export async function printManualPage(path, injectedTitle = '') {
if(isFolder(path)) {
path += '/index.md'
}
const dict = await loadYamlMarkdownFile(path)
const title = injectedTitle || dict?.meta?.title || formatManualTitleString(basename(path))
const formattedTitle = manualTitleStyle(title).centerInLine(title.length).centerInConsole()
console.log('\n' + formattedTitle + '\n')
const renderedMarkdown = marked(dict?.tail).wordWrap().centerInConsole()
console.log(renderedMarkdown)
}
console.log(parsed)
// 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
export 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)
if(previousMenuChoice >= menuChoices.length) {
previousMenuChoice = 0
}
console.log(`\n${headerStyle(menuTitle)}`)
const answer = await inquirer.prompt({
name: 'manual_menu',
type: 'rawlist',
message: 'Choose a topic:',
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, answer.manual_menu)
const newBackOption = backOption === 'Back to Main Menu' ? 'Back to Table of Contents' : 'Back to ' + menuTitle
let previousChoice = 0
do {
previousChoice = await manualFolderAsMenu(chosenPath, answer.manual_menu, newBackOption, previousChoice + 1)
}
while(previousChoice !== false)
return chosenMenuIndex
}

120
scripts/settings.js

@ -0,0 +1,120 @@
import { execSync } from 'child_process'
import fs from 'fs'
import { parse, stringify } from 'envfile'
export const AO_ENV_FILE_PATH = process.env.HOME + '/.ao/.env'
function createAoFolderIfDoesNotExist() {
}
// Check for an AO env file at ~/.ao/.env and returns true if it exists
export function checkAoEnvFile() {
try {
execSync(`[ -f "${AO_ENV_FILE_PATH}" ]`)
return true
} catch(err) {
return false
}
}
export function aoEnv(variable) {
let envFileContents = {}
try {
envFileContents = fs.readFileSync(AO_ENV_FILE_PATH)
} catch(err) {
if(err.code === 'ENOENT') {
console.log('The .env file does not exist, so the requested value', variable, 'is empty.')
} else {
console.log('Unknown error loading .env file in aoEnv, aborting.')
return null
}
}
const parsedFile = parse(envFileContents)
if(!parsedFile.hasOwnProperty(variable)) {
return null
}
// Convert ENV idiom to programmatic types
switch(parsedFile[variable]) {
case '1':
case 'true':
case 'TRUE':
case 'yes':
case 'YES':
return true
case '0':
case 'false':
case 'FALSE':
case 'no':
case 'NO':
return false
}
return parsedFile[variable]
}
// Sets and saves the given ENV=value to the global ~/.ao/.env file
// If value is null, the env variable will be deleted
// Returns true if a change was made, false if no change was made or if it failed
export function setAoEnv(variable, value) {
createAoFolderIfDoesNotExist()
if(typeof variable !== 'string') {
console.log('ENV variable name must be a string for setAoEnv')
return false
}
// Convert types to standard ENV file idiom
switch(value) {
case true:
case 'TRUE':
case 'yes':
case 'YES':
value = '1'
break
case false:
case 'FALSE':
case 'no':
case 'NO':
value = '0'
}
let envFileContents = {}
try {
envFileContents = fs.readFileSync(AO_ENV_FILE_PATH)
} catch(err) {
if(err.code === 'ENOENT') {
console.log('The .env file hasn\'t been created yet, creating.')
} else {
console.log('Unknown error loading .env file in setAoEnv, aborting. Error:', err)
return false
}
}
const parsedFile = parse(envFileContents)
if(parsedFile[variable] == value) {
console.log(variable, 'is already', value, 'so no change was made.')
return false
}
if(value === null) {
delete parsedFile.variable
} else {
parsedFile[variable] = value
}
const stringified = stringify(parsedFile)
fs.writeFileSync(AO_ENV_FILE_PATH, stringified)
// Confirm the variable was set in the .env file correctly
if(aoEnv(variable) != value) {
console.log('Value was not saved correctly, sorry.')
return false
}
return true
}
function setAndSaveEnvironmentVariable(variable, value, path) {
}

36
scripts/strings.js

@ -0,0 +1,36 @@
// Extends the String type and other string helper functions
// 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()})
}
// Wraps the string to the console (or specified) width, ignoring any ansi formatting codes
import wrapAnsi from 'wrap-ansi'
String.prototype.wordWrap = function (width = 80) {
return wrapAnsi(this, width)
}
export 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
}
export const centerLines = (str) => {
const lines = str.split('\n')
const centered = lines.map(line => line.centerInLine(line.length, process.stdout.columns))
return centered.join('\n')
}

111
scripts/system.js

@ -0,0 +1,111 @@
// Functions related to OS and installing software
import { execSync } from 'child_process'
// Detects the operating system we are running on
export 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
export 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
}
// Installs core dependencies required by Alchemy and the AO
export 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
export function setNodeVersion() {
execSync('source ingredients/iron && set_node_to v16.13.0')
}

6
scripts/tests.js

@ -4,7 +4,7 @@
// Maybe in the future a precompiled api.js created from api.ts can be hosted so that ao-cli does not have to compile any TypeScript
import api from './api.js'
export async function testLoginAndOut() {
async function testLoginAndOut() {
const username = 'ao'
const password = 'ao'
try {
@ -39,11 +39,11 @@ export async function testLoginAndOut() {
return true
}
export async function runAllTests() {
async function runAllTests() {
await testLoginAndOut()
}
export const testDict = {
export const tests = {
"Run All Tests": runAllTests,
"Test Login/Logout": testLoginAndOut
}

18
scripts/util.js

@ -0,0 +1,18 @@
// General helper functions
// 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
export function selectRandom(arrayToChooseFrom) {
return arrayToChooseFrom[randomInt(0, arrayToChooseFrom.length - 1)]
}
// Waits for the given number of milliseconds (or a brief pause by default)
export function sleep(ms = 550) {
return new Promise((r) => setTimeout(r, ms))
}

94
scripts/welcome.js

@ -0,0 +1,94 @@
import chalk from 'chalk'
import { selectRandom } from './util.js'
import { greenChalk, theAO, theMenu } from './chalkStyles.js'
// Different sets of messages that can be randomly selected from
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}.`,
`You enter a gap between two hedges and, after struggling through the brush, emerge into a sunny estate garden. You've found the AO.`,
`You find a small animal burrow dug near the riverside. Crawling in, you find a network of caves that lead to the AO.`
]
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 rogerMessages = [
'You\'ve got it.',
'Aye-aye, captain!',
'The AO provides.',
'Roger.',
'Yokai.'
]
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.',
'Go for the low-hanging fruit.'
]
// Prints a random RPG-style welcome message to contextualize the AO experience and the main menu
export 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)
}
// Returns a random exclamatory remark
export function exclaim() {
return selectRandom(exclamationMessages)
}
// Returns a random obedient/affirmative remark denoting that a command was executed
export function roger() {
return selectRandom(rogerMessages)
}
export function farewell() {
console.log(chalk.yellow.bold(selectRandom(farewellMessages)))
}
Loading…
Cancel
Save