Browse Source

replaced inquirer with prompts, but it did not fix double typing issue with blessed

main
deicidus 2 years ago
parent
commit
0c7645bf1e
  1. 2
      README.md
  2. 59
      index.js
  3. 1004
      package-lock.json
  4. 5
      package.json
  5. 81
      scripts/ao.js
  6. 108
      scripts/api.js
  7. 53
      scripts/cards.js
  8. 49
      scripts/connect.js
  9. 79
      scripts/features/index.js
  10. 24
      scripts/features/manual.js
  11. 8
      scripts/keyboard.js
  12. 59
      scripts/priority.js
  13. 232
      scripts/shadowchat.js
  14. 21
      scripts/tests.js
  15. 67
      scripts/welcome.js
  16. 60
      scripts/wizard.js

2
README.md

@ -34,7 +34,7 @@ These features work right now:
* Add `ao` alias for `ao-cli` (under Features→ao-cli)
* Optionally enchant your 'cd' command to narrate your travels through the UNIX filesystem and occasionally remind you of your top priority (under Features→ao-cli)
* Easily add your existing systemctl services to the Features list so you can start and stop them from the AO Features menu
* Pedagogical codebase designed for teaching novice users. Code written to be read, with relevant contextual information mentioned in comments. Browse or download the code for ao-cli at the Repository link above.
* Pedagogical codebase designed for teaching novice developers. Code written to be read, with relevant contextual information mentioned in comments. Browse or download the code for ao-cli at the Repository link above.
## Upcoming Features

59
index.js

@ -1,13 +1,12 @@
#!/usr/bin/env node
import chalk from 'chalk'
import inquirer from 'inquirer'
import { execSync } from 'child_process'
// Import ao-cli core features
import { exitIfRoot, detectOS, updateSoftware } from './scripts/system.js'
import { checkAoEnvFile, aoEnv, setAoEnv, AO_ENV_FILE_PATH } from './scripts/settings.js'
import { unicornPortal, asciiArt, clearScreen } from './scripts/console.js'
import { welcome, exclaim, farewell, yesOrNo } from './scripts/welcome.js'
import { welcome, exclaim, farewell, yesOrNo, promptMenu } from './scripts/welcome.js'
import wander from './scripts/forest.js'
import useAoMenu from './scripts/ao.js'
import aoInstallWizard, { chooseAoVersion, checkAo } from './scripts/wizard.js'
@ -21,36 +20,19 @@ import features, { featuresMenu } from './scripts/features/index.js'
import manual, { printManualPage, manualFolderAsMenu, AO_MANUAL_PATH } from './scripts/features/manual.js'
import aoCli from './scripts/features/ao-cli.js'
// Enable keyboard shortcut interruption of inquirer menus
import './scripts/keyboard.js'
// Prints the AO Main Menu and executes the user's choice
async function mainMenu() {
console.log(`\n${headerStyle('AO Main Menu')}\n`)
//console.log(`\n${headerStyle('AO Main Menu')}\n`)
let mainMenuChoices = [
'AO',
'Alchemy',
'Configure',
'Manual',
'Exit'
{ title: 'AO', description: 'use AO features' },
{ title: 'Alchemy', description: 'transmute system to gold' },
{ title: 'Configure', description: 'configure AO features' },
{ title: 'Manual', description: 'read the manual' },
{ title: 'Exit', description: 'ESC on any menu' }
]
let answer
try {
answer = await inquirer.prompt({
name: 'main_menu',
type: 'list',
message: 'Please choose:',
choices: mainMenuChoices,
pageSize: mainMenuChoices.length
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
answer = { main_menu: 'Exit' }
}
}
const answer = await promptMenu(mainMenuChoices, 'AO Main Menu')
let previousChoice
switch(answer.main_menu) {
switch(answer) {
case 'AO':
while(await useAoMenu()) {}
break
@ -79,7 +61,7 @@ async function mainMenu() {
previousChoice = await manualFolderAsMenu(AO_MANUAL_PATH, 'AO User Manual', 'Back to Main Menu', previousChoice + 1)
} while(previousChoice !== false)
break
case 'Exit':
default:
farewell()
await sleep(310)
return false
@ -90,9 +72,8 @@ async function mainMenu() {
// 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 = [
{ name: 'Check AO install', value: 'check_AO' },
{ title: 'Check AO install', value: 'check_AO' },
'Update everything',
'Update system software',
//'Update AO',
@ -109,22 +90,8 @@ async function adminMenu() {
//'Update remote AOs',
'Back to Main Menu'
]
let answer
try {
answer = await inquirer.prompt({
name: 'admin_menu',
type: 'list',
message: 'Please choose:',
choices: adminChoices,
pageSize: adminChoices.length,
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
switch(answer.admin_menu) {
const answer = await promptMenu(adminChoices, 'System Alchemy')
switch(answer) {
case 'check_AO':
await checkAo()
break

1004
package-lock.json generated

File diff suppressed because it is too large Load Diff

5
package.json

@ -16,6 +16,7 @@
"debug": "node inspect ."
},
"keywords": [
"cli",
"AO",
"Autonomous Organization",
"autonomous",
@ -27,6 +28,7 @@
"author": "Coalition of Invisible Colleges",
"license": "AGPL-3.0-or-later",
"dependencies": {
"blessed": "^0.1.81",
"chalk": "^5.0.1",
"chalk-animation": "^2.0.2",
"crypto": "^1.0.1",
@ -34,11 +36,10 @@
"figlet": "^1.5.2",
"gradient-string": "^2.0.1",
"hash.js": "^1.1.7",
"inquirer": "^8.2.4",
"inquirer-interrupted-prompt": "^1.0.2",
"marked": "^4.0.16",
"marked-terminal": "^5.1.1",
"nanospinner": "^1.1.0",
"prompts": "^2.4.2",
"sha.js": "^2.4.11",
"socket-io": "^1.0.0",
"socket.io-client": "^4.5.1",

81
scripts/ao.js

@ -2,15 +2,15 @@
// The ao-cli client has no store so it makes frequent (hopefully very quick) calls to get exactly the information it needs.
// This requires us to make sure the AO API server's REST API is concise and efficient.
// The only places ao-cli should call out to an AO server (i.e., use the API in api.js) are in this Use AO menu and in the Tests menu.
import inquirer from 'inquirer'
import { aoEnv } from './settings.js'
import { isLoggedIn, loginPrompt, logout } from './session.js'
import { getTopPriorityText } from './priority.js'
import { startPublicBootstrap } from './bootstrap.js'
import { AO_DEFAULT_HOSTNAME } from './api.js'
import { headerStyle } from './styles.js'
import { cardMenu } from './cards.js'
import { connectMenu } from './connect.js'
import { headerStyle } from './styles.js'
import chatMenu from './shadowchat.js'
import { promptMenu } from './welcome.js'
// Prints the Use AO Menu and executes the user's choice. Using the AO as a client occurs only under this menu item (except Tests menu).
export default async function useAoMenu() {
@ -37,22 +37,8 @@ export default async function useAoMenu() {
loggedIn ? 'Log Out' : 'Log In',
'Back to Main Menu'
)
let answer
try {
answer = await inquirer.prompt({
name: 'ao_menu',
type: 'list',
message: 'Please choose:',
choices: aoMenuChoices,
pageSize: aoMenuChoices.length
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
switch(answer.ao_menu) {
const answer = await promptMenu(aoMenuChoices, 'Use AO features', 'connect to an AO server to use AO features')
switch(answer) {
case 'Deck':
//await todoList('My Todo List', ['Add full AO install process to ao-cli in convenient format', 'Add AO server unit tests to ao-cli', 'Get groceries', 'Play music every day'])
while(await cardMenu()) {}
@ -71,62 +57,7 @@ export default async function useAoMenu() {
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
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)")
//}
//answers['chat_menu'] = 'Enable p2p bootstrap'
}
const chatChoices = [
{ name: 'Join public chatroom', value: 'browse_chatrooms', short: 'public chatrooms' },
{ name: 'Join chatroom', value: 'join_chat', short: 'join chat' },
'Address Book',
'Back to AO Menu',
]
console.log(`\n${headerStyle('AO Public Chatrooms')}\n`)
let answer
try {
answer = await inquirer.prompt({
name: 'chat_menu',
type: 'list',
message: 'Please choose:',
choices: chatChoices
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
switch(answer.chat_menu) {
case 'browse_chatrooms':
console.log('Not yet implemented')
break
case 'join_chat':
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 AO Menu':
default:
return false
}
return true

108
scripts/api.js

@ -11,12 +11,14 @@ const HOSTNAME = aoEnv('AO_CLI_TARGET_HOSTNAME') || AO_DEFAULT_HOSTNAME
const [HOST, PORT] = HOSTNAME.split(':')
// The AO API server websocket endpoint this ao-cli client will attempt to connect to
const AO_SOCKET_URL =
process.env.NODE_ENV === 'development' ? 'http://' + AO_DEFAULT_HOSTNAME : '/'
const AO_SOCKET_URL = 'http://' + AO_DEFAULT_HOSTNAME // was process.env.NODE_ENV === 'development' ? 'http://' + AO_DEFAULT_HOSTNAME : '/'
export const socket = io(AO_SOCKET_URL, {
autoConnect: false
})
// The current connected state of the websocket, can be 'attemptingAuthentication' | 'authenticationSuccess' | 'authenticationFailed'
export let socketStatus
// Load the current session cookies from the AO .env file
let currentMemberId = aoEnv('AO_CLI_SESSION_MEMBERID')
let currentSessionId = aoEnv('AO_CLI_SESSION_ID')
@ -81,7 +83,7 @@ export async function createSession(user, pass) {
.on('error', () => false)
currentMemberId = result.body.memberId
currentSessionToken = token
currentSessionId = session // Not used in this api.js yet
currentSessionId = session
return { session, token, memberId: currentMemberId }
}
@ -97,6 +99,43 @@ export async function nameAo(newName) {
})
}
// When you call startSocketListeners, it will either attempt to connect and authenticate on a web socket, or fail and return.
// onAuthenticated is a function that will be called when the client authenticates on the web socket (logs in/connects).
// In your onAuthenticated function, you should trigger fetchState or other initial fetching of state from server.
// eventCallback is a function (ev) => {} that will be called whenever an event is received on the socket.
// In this way, initial state can be fetched and then updates received after that can be used to update the local state model.
export async function startSocketListeners(onAuthenticated, onEvent, verbose = true) {
if(typeof onAuthenticated !== 'function' || typeof onEvent !== 'function') {
console.log('startSocketListeners requires two callback functions as arguments.')
return
}
socket.connect()
socket.on('connect', () => {
if(verbose) console.log('websocket connected')
socketStatus = 'attemptingAuthentication'
if(!currentSessionId || !currentSessionToken) {
if(verbose) console.log('No current session, must log in to authenticate and use socket.')
return
}
socket.emit('authentication', {
session: currentSessionId,
token: currentSessionToken,
})
})
socket.on('authenticated', () => {
if(verbose) console.log('websocket authenticated')
socketStatus = 'authenticationSuccess'
socket.on('eventstream', onEvent)
onAuthenticated()
})
socket.on('disconnect', reason => {
if(verbose) console.log('websocket disconnected')
socketStatus = 'authenticationFailed'
socket.connect()
})
}
// Requests the public bootstrap list by making a public (not logged in) GET request to this or the specified server
export async function getAoBootstrapList(serverOnion = null) {
const result = await getRequest('/bootstrap', undefined, serverOnion, false)
@ -125,6 +164,15 @@ export async function bootstrap(serverOnion = null) {
return onionList
}
export async function shadowchat(room, message, username) {
return await postEvent({
type: 'shadowchat',
room: room,
name: username,
message: message,
})
}
export async function connectToAo(address, secret) {
return await postEvent({
type: 'ao-outbound-connected',
@ -865,60 +913,6 @@ export async function setQuorum(quorum) {
})
}
/*startSocketListeners() {
this.socket.connect()
this.socket.on('connect', () => {
console.log('connected', { 'aoStore.state': aoStore.state })
runInAction(() => {
aoStore.state.socketState = 'attemptingAuthentication'
const loadedSession = window.localStorage.getItem('session')
if(loadedSession) {
aoStore.state.session = loadedSession
}
const loadedToken = window.localStorage.getItem('token')
if(loadedToken) {
currentSessionToken = loadedToken
}
})
console.log(
'emit auth: session: ' +
window.localStorage.getItem('session') +
', token: ' +
window.localStorage.getItem('token')
)
this.socket.emit('authentication', {
session: window.localStorage.getItem('session'),
token: window.localStorage.getItem('token'),
})
})
this.socket.on('authenticated', () => {
console.log('authenticated')
this.fetchState().then(() => {
runInAction(() => {
aoStore.state.socketState = 'authenticationSuccess'
})
})
this.socket.on('eventstream', ev => {
console.log('AO: client/api.ts: socketListener: event:', ev)
aoStore.applyEvent(ev)
})
})
this.socket.on('disconnect', reason => {
console.log('disconnected')
runInAction(() => {
aoStore.state.socketState = 'authenticationFailed'
})
this.socket.connect()
})
}*/
/*reaction(
() => {
//return aoStore.state.socketState

53
scripts/cards.js

@ -1,45 +1,31 @@
// Cards module - everything related to cards should go here (database install is automatic for AO server so no feature module)
import inquirer from 'inquirer'
import { getCardByName, createCard, prioritizeCard } from './api.js'
import { headerStyle } from './styles.js'
import { prioritiesMenu } from './priority.js'
import { aoEnv } from './settings.js'
import { promptMenu, askQuestionText } from './welcome.js'
// The card menu is complex and so has been split into this separate file
export async function cardMenu() {
console.log(`\n${headerStyle('My Deck')}`)
const cardChoices = [
{ name: 'Top priorities', value: 'priorities', short: 'priorities' }, // hand? (7) (add #s in parens)
{ name: 'Cards in hand', value: 'subcards', short: 'hand' }, // (current) deck? (60)
{ name: 'Browse full deck', value: 'browse', short: 'browse' }, // archive? (10,000)
{ title: 'Top priorities', value: 'priorities', short: 'priorities' }, // hand? (7) (add #s in parens)
{ title: 'Cards in hand', value: 'subcards', short: 'hand' }, // (current) deck? (60)
{ title: 'Browse full deck', value: 'browse', short: 'browse' }, // archive? (10,000)
'Back to AO Menu'
]
try {
const answer = await inquirer.prompt({
name: 'card_menu',
type: 'list',
message: 'Please choose:',
choices: cardChoices,
pageSize: cardChoices.length,
})
switch(answer.card_menu) {
case 'priorities':
while(await prioritiesMenu()) {}
break
case 'subcards':
while(await subcardsMenu()) {}
break
case 'browse':
while(await browseMenu()) {}
break
default:
return false
}
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
const answer = await promptMenu(cardChoices, 'My Deck')
switch(answer.card_menu) {
case 'priorities':
while(await prioritiesMenu()) {}
break
case 'subcards':
while(await subcardsMenu()) {}
break
case 'browse':
while(await browseMenu()) {}
break
default:
return false
}
}
return true
}
@ -54,17 +40,12 @@ async function browseMenu() {
// Ask the user to create a card, checks if it already exists, and then creates it if it doesn't
export async function createCardInteractive(prioritized = true) {
console.log('createcardinteractive')
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID')
if(!memberId) {
console.log('Not logged in.')
return false
}
const answer = await inquirer.prompt({
name: 'new_card_text',
type: 'input',
message: 'New card or Enter to end:',
})
const answer = await askQuestionText('New card or Enter to end:')
if(answer.new_card_text.trim().length <= 0) {
return false
}

49
scripts/connect.js

@ -1,42 +1,26 @@
// Each AO API server can connect peer-to-peer over Tor. Tor addresses are unique and data is end-to-end encrypted.
import inquirer from 'inquirer'
import { headerStyle } from './styles.js'
import { aoEnv, setAoEnv } from './settings.js'
import { isLoggedIn } from './session.js'
import { isInstalled } from './features/tor.js'
import { connectToAo, getAoBootstrapList, bootstrap } from './api.js'
import { roger } from './welcome.js'
import { roger, promptMenu } from './welcome.js'
// Prints a menu to connect your AO to other AOs and manage connections
export async function connectMenu() {
console.log(`\n${headerStyle('AO P2P')}`)
const PUBLIC_BOOTSTRAP_ENABLED = aoEnv('PUBLIC_BOOTSTRAP_ENABLED')
let publicBootstrapMenuItem = { name: 'Enable p2p bootstrap', value: 'enable_bootstrap' }
let publicBootstrapMenuItem = { title: 'Enable p2p bootstrap', value: 'enable_bootstrap' }
if(PUBLIC_BOOTSTRAP_ENABLED) {
publicBootstrapMenuItem = { name: 'Disable p2p bootstrap', value: 'disable_bootstrap' }
publicBootstrapMenuItem = { title: 'Disable p2p bootstrap', value: 'disable_bootstrap' }
}
const connectChoices = [
{ name: 'Connect to AO', value: 'connect', short: 'p2p connect'},
{ name: 'View connections', value: 'connections' },
{ name: 'Bootstrap now', value: 'bootstrap' },
{ title: 'Connect to AO', value: 'connect', short: 'p2p connect'},
{ title: 'View connections', value: 'connections' },
{ title: 'Bootstrap now', value: 'bootstrap' },
publicBootstrapMenuItem,
{ name: 'Back to AO Menu', value: false }
{ title: 'Back to AO Menu', value: false }
]
let answer
try {
answer = await inquirer.prompt({
name: 'connect_menu',
type: 'list',
message: 'Please choose:',
choices: connectChoices,
pageSize: connectChoices.length,
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
const answer = await promptMenu(connectChoices, 'AO P2P')
switch(answer.connect_menu) {
case 'connect':
await connectInteractive()
@ -110,24 +94,11 @@ async function connectInteractive() {
}
return true
}
let answer
try {
answer = await inquirer.prompt({
name: 'connection_string',
type: 'input',
message: 'Enter connection string of other AO:',
validate: validateConnectionString
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('ESC')
return false
}
}
const answer = await askQuestionText('Enter connection string of other AO:') //todo: validate: validateConnectionString
const [onion, secret] = answer.connection_string.split(':')
console.log('onion is', onion, 'and secret is', secret)
console.log('Attempting connect...')
const result = await connectToAo(onion, secret)
console.log('result is', result.body)
return true
}
}

79
scripts/features/index.js

@ -1,7 +1,6 @@
// Re-export the features modules in this folder, which each add, remove, and admininster one AO feature (imported in project index.js)
// Also contains the Features menus to control these features in this directory
import chalk from 'chalk'
import inquirer from 'inquirer'
import fs from 'fs'
import { lsFolder } from '../files.js'
import { fileURLToPath } from 'url'
@ -9,6 +8,7 @@ import path from 'path'
import { headerStyle, greenChalk, styledStatus } from '../styles.js'
import { spinner } from '../console.js'
import SystemServiceManager, { getCustomServicesList, addCustomServiceInteractive, removeCustomService } from '../services.js'
import { promptMenu } from '../welcome.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
@ -33,7 +33,6 @@ export default features
// Prints the Configure AO Features menu and executes the user's choice
let featuresChoices
export async function featuresMenu(previousMenuChoice = 0) {
console.log(`\n${headerStyle('Configure AO Features')}`)
const stopSpinner = spinner('Loading status...')
let loadedFeatures = 0
if(!featuresChoices) {
@ -49,10 +48,10 @@ export async function featuresMenu(previousMenuChoice = 0) {
}
const statusColumn = styledStatus(status, 25)
const descriptionColumn = feature.description || ''
const choice = { name: nameColumn + statusColumn + descriptionColumn, value: featureKey, short: featureKey }
const choice = { title: nameColumn + statusColumn + descriptionColumn, value: featureKey, short: featureKey }
return choice
})
featuresChoices.push(new inquirer.Separator())
featuresChoices.push({ title: '---', disabled: true })
const customServices = getCustomServicesList()
customServices.forEach(serviceName => {
const nameColumn = serviceName.padEnd(17)
@ -60,11 +59,11 @@ export async function featuresMenu(previousMenuChoice = 0) {
const status = service.status() || 'Unknown'
const statusColumn = styledStatus(status, 25)
const descriptionColumn = service.description || ''
const choice = { name: nameColumn + statusColumn + descriptionColumn, value: 'service_' + serviceName, short: serviceName }
const choice = { title: nameColumn + statusColumn + descriptionColumn, value: 'service_' + serviceName, short: serviceName }
featuresChoices.push(choice)
})
featuresChoices.push(
{ name: 'Add custom service', value: 'add_service' },
{ title: 'Add custom service', value: 'add_service' },
'Back to Main Menu'
)
} else {
@ -73,23 +72,8 @@ export async function featuresMenu(previousMenuChoice = 0) {
}).length
}
stopSpinner('Loaded status for ' + loadedFeatures + '/' + (featuresChoices.length - 1) + ' features.')
let answer
try {
answer = await inquirer.prompt({
name: 'features_menu',
type: 'list',
message: 'Please choose:',
choices: featuresChoices,
default: previousMenuChoice,
pageSize: featuresChoices.length
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
if(answer.features_menu.includes('service_')) {
const answer = await promptMenu(featuresChoices, 'Configure AO Features', undefined, previousMenuChoice)
if(answer.includes('service_')) {
const serviceName = answer.features_menu.replace(/^service_/, '')
const service = new SystemServiceManager(serviceName)
const nameWithoutDot = serviceName.split('.')[0] + ' service'
@ -97,7 +81,7 @@ export async function featuresMenu(previousMenuChoice = 0) {
await oneFeatureMenu(nameWithoutDot, service, true)
return answer.features_menu
}
switch(answer.features_menu) {
switch(answer) {
case 'add_service':
console.log('Many Linux distributions run system services in the background. You can add an existing systemctl service to the AO Features menu to make it easier to start and stop your services. You must know the name of the service and it must already exist.')
while(await addCustomServiceInteractive()) {}
@ -106,10 +90,10 @@ export async function featuresMenu(previousMenuChoice = 0) {
case 'Back to Main Menu':
return false
}
const chosenFeature = features[answer.features_menu]
const chosenFeatureName = (chosenFeature.hasOwnProperty('name') && chosenFeature.name.length >= 1) ? chosenFeature.name : answer.features_menu
const chosenFeature = features[answer]
const chosenFeatureName = (chosenFeature.hasOwnProperty('name') && chosenFeature.name.length >= 1) ? chosenFeature.name : answer
while(await oneFeatureMenu(chosenFeatureName, chosenFeature)) {}
return answer.features_menu
return answer
}
// Prints the menu options for a specific feature (including subfeatures)
@ -126,26 +110,26 @@ function oneFeatureMenuChoices(name, feature, status, isCustom = false) {
const installed = typeof feature.isInstall === 'function' ? feature.isInstalled : feature.hasOwnProperty('isInstalled') ? feature.isInstalled : status !== 'off'
const running = installed && typeof feature.isRunning === 'function' ? feature.isRunning() : feature.hasOwnProperty('isRunning') ? feature.isRunning : false
if(running && typeof feature.stop === 'function') {
featureChoices.push({ name: 'Stop ' + name, value: () => feature.stop() })
featureChoices.push({ title: 'Stop ' + name, value: feature.stop })
} else if(installed && !running && typeof feature.start === 'function') {
featureChoices.push({ name: 'Start ' + name, value: () => feature.start() })
featureChoices.push({ title: 'Start ' + name, value: feature.start })
}
if(status === 'off') {
if(typeof feature.install === 'function') {
featureChoices.push({ name: 'Install ' + name, value: () => feature.install() })
featureChoices.push({ title: 'Install ' + name, value: feature.install })
}
} else {
if(typeof feature.update === 'function') {
featureChoices.push({ name: 'Update ' + name, value: () => feature.update() })
featureChoices.push({ title: 'Update ' + name, value: feature.update })
}
if(typeof feature.uninstall === 'function') {
featureChoices.push({ name: 'Uninstall ' + name, value: () => feature.uninstall() })
featureChoices.push({ title: 'Uninstall ' + name, value: feature.uninstall })
}
}
if(isCustom) {
featureChoices.push({ name: 'Remove from list', value: () => {
featureChoices.push({ title: 'Remove from list', value: () => {
const nameWithoutLabel = name.replace(/ service/, '')
removeCustomService(nameWithoutLabel)
featuresChoices = null
@ -157,7 +141,7 @@ function oneFeatureMenuChoices(name, feature, status, isCustom = false) {
feature.menu.forEach(menuItem => {
const menuItemName = typeof menuItem.name === 'function' ? menuItem.name() : menuItem.name
if(menuItemName) {
featureChoices.push({ name: menuItemName, value: menuItem.Value })
featureChoices.push({ title: menuItemName, value: menuItem.Value })
}
// todo: uninstall option will go here also
})
@ -197,30 +181,15 @@ export async function oneFeatureMenu(name, feature, isCustom = false) {
featureChoices.push(
'Back to Features'
)
let answer
try {
answer = await inquirer.prompt({
name: 'feature_menu',
type: 'list',
message: 'Please choose:',
choices: featureChoices
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
} else {
console.log('Unknown error displaying Features menu:', error)
}
}
if(answer.feature_menu === 'Back to Features') {
const answer = await promptMenu(featureChoices, name)
if(answer === 'Back to Features') {
return false
}
if(typeof answer.feature_menu === 'function') {
answer.feature_menu()
if(typeof answer === 'function') {
await answer()
return true
} else if(Object.keys(feature).includes(answer.feature_menu)) {
await feature[answer.feature_menu]()
} else if(Object.keys(feature).includes(answer)) {
await feature[answer]()
return true
}
console.log('Not yet implemented')

24
scripts/features/manual.js

@ -1,9 +1,9 @@
// Functions for downloading, updating, and displaying the AO Manual, a hierarchy of markdown files
import chalk from 'chalk'
import inquirer from 'inquirer'
import { execSync, exec } from 'child_process'
import { loadYamlMarkdownFile, lsFolder, isFolder } from '../files.js'
import { repeatString, centerLines } from '../strings.js'
import { promptMenu } from '../welcome.js'
import { headerStyle, manualTitleStyle } from '../styles.js'
import { basename } from 'path'
import { marked } from 'marked'
@ -158,25 +158,17 @@ export async function manualFolderAsMenu(path, menuTitle, backOption, previousMe
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) {
const answer = await promptMenu(menuChoices, menuTitle, 'choose a topic', previousMenuChoice, undefined, true)
const chosenMenuIndex = menuChoices.indexOf(answer)
if(answer === 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 chosenPath = path + Object.values(menuItems.find(menuItem => Object.keys(menuItem)[0] === answer))[0]
await printManualPage(chosenPath, answer)
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)
previousChoice = await manualFolderAsMenu(chosenPath, answer, newBackOption, previousChoice + 1)
}
while(previousChoice !== false)
return chosenMenuIndex
@ -189,4 +181,4 @@ export default {
install: downloadManual,
isInstalled: () => manualStatus() === 'installed',
update: updateManual
}
}

8
scripts/keyboard.js

@ -1,10 +1,10 @@
// Hook to add keyboard shortcuts to all inquirer prompts
import inquirer from 'inquirer'
import InterruptedPrompt from 'inquirer-interrupted-prompt'
//import inquirer from 'inquirer'
//import InterruptedPrompt from 'inquirer-interrupted-prompt'
InterruptedPrompt.replaceAllDefaults(inquirer)
//InterruptedPrompt.replaceAllDefaults(inquirer)
// Note that the above method can only detect one key per menu, apparently, and it can't tell you which one it detected in the callback.
// Might be worth looking into a more flexible UI library that integrates keyboard shortcuts with its menu library.
// Right now we are stuck with just using one key, the Escape key to go up a level in menus. Every menu must handle it or it can crash.
// The inquirer-interrupted-prompt documentation shows using a prompt with an array of prompt objects as args, but it didn't work for me.
// The inquirer-interrupted-prompt documentation shows using a prompt with an array of prompt objects as args, but it didn't work for me.

59
scripts/priority.js

@ -1,9 +1,9 @@
// Prioritize cards within other cards. Each card has a .priorities array of other taskIds.
import inquirer from 'inquirer'
import { headerStyle } from './styles.js'
import { aoEnv } from './settings.js'
import { getCard, prioritizeCard, completeCard, uncheckCard, refocusCard } from './api.js'
import { createCardInteractive } from './cards.js'
import { promptMenu } from './welcome.js'
// Prints the text (.name) of the first card prioritized in the logged-in users member card
export async function getTopPriorityText() {
@ -65,7 +65,7 @@ export async function prioritiesMenu(taskId = null) {
return 'Missing card, repair your database'
}
return {
name: priorityCard.name,
title: priorityCard.name,
value: { index: i, card: priorityCard },
short: priorityCard.name.substring(0, 70) + priorityCard.name.length >= 70 ? '...' : ''
}
@ -80,29 +80,15 @@ export async function prioritiesMenu(taskId = null) {
}
})
if(!isNaN(firstIndexEchelonDecreases)) {
prioritiesChoices.splice(firstIndexEchelonDecreases, 0, new inquirer.Separator('───'))
prioritiesChoices.splice(firstIndexEchelonDecreases, 0, { title: '───', disabled: true })
}
}
prioritiesChoices.push(
{ name: 'Create priority', value: 'create_here', short: 'new priority' },
{ name: 'Back to Deck', value: false, short: 'back' }
{ title: 'Create priority', value: 'create_here', short: 'new priority' },
{ title: 'Back to Deck', value: false, short: 'back' }
)
let answer
try {
answer = await inquirer.prompt({
name: 'priorities_menu',
type: 'rawlist',
message: 'Please choose:',
choices: prioritiesChoices,
loop: false
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
switch(answer.priorities_menu) {
const answer = await promptMenu(prioritiesChoices, 'My Priorities', undefined, undefined, 'Upboated tasks will be inserted above this line')
switch(answer) {
case false:
return false
case 'create_here':
@ -119,7 +105,7 @@ export async function prioritiesMenu(taskId = null) {
const chosenTaskId = chosenTask.taskId
let previousAnswer
do {
previousAnswer = await priorityCardMenu(chosenTask, answer.priorities_menu.index, priorityCards)
previousAnswer = await priorityCardMenu(chosenTask, answer.index, priorityCards)
if(previousAnswer) {
const fetchedCards = await getCard(chosenTaskId, false)
if(!fetchedCards || fetchedCards.length < 1) {
@ -147,33 +133,18 @@ async function priorityCardMenu(card, index, allPriorities) {
return false
}
const isChecked = card.claimed.includes(memberId)
console.log(`\n${headerStyle('Priority: ' + card.name)}`)
let priorityChoices = []
if(index != 0) {
priorityChoices.push({ name: 'Upboat', value: 'upboat', short: 'upboat' })
priorityChoices.push({ title: 'Upboat', value: 'upboat', short: 'upboat' })
}
priorityChoices.push(
{ name: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' },
{ name: 'Downboat', value: 'downboat', short: 'downboat' },
//{ name: 'Browse within', value: 'browse', short: 'browse' }
{ name: 'Back to Priorities', value: false, short: 'back' }
{ title: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' },
{ title: 'Downboat', value: 'downboat', short: 'downboat' },
//{ title: 'Browse within', value: 'browse', short: 'browse' }
{ title: 'Back to Priorities', value: false, short: 'back' }
)
let answer
try {
answer = await inquirer.prompt({
name: 'priority_card_menu',
type: 'list',
message: 'Please choose:',
choices: priorityChoices,
pageSize: priorityChoices.length,
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
switch(answer.priority_card_menu) {
const answer = await promptMenu(priorityChoices, 'Priority: ' + card.name)
switch(answer) {
case 'check':
if(isChecked) {
await uncheckCard(taskId)

232
scripts/shadowchat.js

@ -0,0 +1,232 @@
// AO shadowchat feature menu including bootstrap network server list browser, chatroom list on each server, and chatroom interface
// Called shadowchat because no record is kept of the chat messages, and all connections happen E2E over tor
// As this feature gets build, sensible standards must be developed around when tor addresses change hands, when users authenticate, etc
import { aoEnv } from './settings.js'
import { isLoggedIn } from './session.js'
import { startPublicBootstrap } from './bootstrap.js'
import { headerStyle } from './styles.js'
import { sleep } from './util.js'
import { askQuestionText, promptMenu } from './welcome.js'
import { AO_DEFAULT_HOSTNAME, startSocketListeners, socketStatus, socket, shadowchat } from './api.js'
import { fileURLToPath } from 'url'
import path from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Prints a menu that allows you to join the global AO chatrooms
export default 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)")
//}
//answers['chat_menu'] = 'Enable p2p bootstrap'
}
const chatChoices = [
{ title: 'Join public chatroom', value: 'browse_chatrooms', short: 'public chatrooms' },
{ title: 'Join chatroom', value: 'join_chat', short: 'join chat' },
'Address Book',
'Back to AO Menu',
]
const answer = await promptMenu(chatChoices, 'AO Public Chatrooms')
switch(answer) {
case 'browse_chatrooms':
const loggedIn = isLoggedIn()
if(!isLoggedIn) {
console.log('Please start AO server and log in first.')
return true
}
console.log('Logged in as:', aoEnv('AO_CLI_SESSION_USERNAME'))
const onAuthenticated = () => {
//console.log('Websocket connected and authenticated.')
}
const onEvent = (event) => {
console.log('\nEvent received on websocket:', event) // todo: still receiving excess task-resets every 5 minutes
}
if(socketStatus !== 'authenticationSuccess') {
await startSocketListeners(onAuthenticated, onEvent)
await sleep(400)
}
console.log('socketStatus is', socketStatus)
if(socketStatus !== 'authenticationSuccess') {
console.log('Failed to connect to websocket. Make sure your AO server is running, and if you have a custom config set your target server in ao-cli.')
return true
}
console.log('Websocket successfully connected to listen for events.')
while(await browseChatrooms()) {}
break
case 'join_chat':
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
default:
return false
}
return true
}
async function getLocalRooms() {
return new Promise((resolve, reject) => {
socket.on('rooms_list', event => {
socket.off('rooms_list')
resolve(event)
})
socket.emit('get_rooms')
})
}
async function browseChatrooms() {
if(socketStatus !== 'authenticationSuccess') {
console.log('Websocket is not connected, going back.')
return false
}
console.log('Getting list of chatrooms...')
const chatrooms = await getLocalRooms()
// Get list of all servers and display
// todo: check if websocket can work without authentication
// For each server, request its list of channels
console.log('Chatrooms on this server:', chatrooms)
//await chatInRoom('HUB')
return await chatroomScreen('HUB')
// Display the list as a menu with a heading for each server
}
async function chatInRoom(room) {
console.log('Joining room...')
socket.emit('join_room', room)
return new Promise(async (resolve, reject) => {
const leaveChatroom = () => {
console.log('Leaving room...')
socket.emit('leave_room', room)
socket.off('chat')
resolve()
}
socket.on('chat', event => {
const chatLine = event.name ? event.name + ': ' + event.message : 'Anon:' + event.message
console.log(/*new Date().toLocaleTimeString(), */'\n' + chatLine)
})
const username = aoEnv('AO_CLI_SESSION_USERNAME')
let message
do {
message = await askQuestionText('> ')
switch(message.toLowerCase()) {
case 'exit':
case 'leave':
case 'back':
case 'quit':
case false:
resolve(false)
default:
//socket.emit('chat', room, message, username || undefined)
shadowchat(room, message, username || undefined)
}
} while(message !== 'exit')
})
}
/*async function chatroomScreen(room) {
var ui = new inquirer.ui.BottomBar()
ui.render()
console.log(ui)
// pipe a Stream to the log zone
//outputStream.pipe(ui.log)
// Or simply write output
ui.log.write('something just happened.');
ui.log.write('Almost over, standby!');
// During processing, update the bottom bar content to display a loader
// or output a progress bar, etc
ui.updateBottomBar('new bottom bar content');
}*/
import blessed from 'blessed'
export async function chatroomScreen(room) {
const screen = blessed.screen({
smartCSR: true,
ignoreLocked: ['C-c'],
dockBorders: true,
title: room
})
const chatlog = blessed.log({
top: 0,
left: 0,
bottom: 2,
width: '100%',
content: '{bold}' + room + '{/bold}',
tags: true,
scrollable: true,
border: {
type: 'line'
},
style: {
fg: 'white',
bg: 'black',
border: {
fg: '#f0f0f0'
},
}
})
screen.append(chatlog);
const input = blessed.textbox({
bottom: 0,
left: 0,
right: 0,
height: 3,
content: 'Type message here',
inputOnFocus: true,
border: {
type: 'line'
},
style: {
fg: 'white',
bg: 'blue',
border: {
fg: '#f0f0f0'
},
}
})
input.on('submit', (event) => {
chatlog.log('new submission:', event)
input.clearValue()
input.focus()
})
input.on('keypress', (event) => {
chatlog.add('got keypress:', event)
})
screen.append(input)
//screen.focusPop()
chatlog.add('screen.focus:', screen.focus)
input.focus()
screen.render()
return new Promise(async (resolve, reject) => {
//screen.key(['C-c'], () => process.exit(0))
screen.key(['escape', 'C-c'], function(ch, key) {
chatlog.log('Exiting chatroom...')
screen.destroy()
resolve(false)
})
})
}

21
scripts/tests.js

@ -2,8 +2,8 @@
// The tests actually happen so your database will be modified (future: allow switching databases or automatically switch)
// The tests use an AO API file saved in the same directory; this file must be kept up-to-date
// 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 inquirer from 'inquirer'
import { createSession, logout } from './api.js'
import { promptMenu } from './welcome.js'
async function testLoginAndOut() {
const username = 'ao'
@ -56,24 +56,11 @@ export default async function testsMenu() {
return menuTitle
})
testChoices.push('Back to Main Menu')
let answer
try {
answer = await inquirer.prompt({
name: 'tests_menu',
type: 'list',
message: 'Please choose:',
choices: testChoices
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
if(answer.tests_menu === 'Back to Main Menu') {
const answer = await promptMenu(testChoices, 'AO Unit Tests')
if(answer === 'Back to Main Menu') {
return false
}
const testFunction = tests[answer.tests_menu]
if(testFunction) await testFunction()
return true
}
}

67
scripts/welcome.js

@ -1,7 +1,8 @@
import chalk from 'chalk'
import inquirer from 'inquirer'
//import inquirer from 'inquirer'
import prompts from 'prompts'
import { selectRandom } from './util.js'
import { greenChalk, theAO, theMenu } from './styles.js'
import { greenChalk, theAO, theMenu, headerStyle } from './styles.js'
// Different sets of messages that can be randomly selected from
const welcomeMessages = [
@ -100,33 +101,63 @@ export function farewell() {
// Asks the given yes or no answer returns true or false for their response
export async function yesOrNo(prompt = 'Yes or no?', defaultAnswer = true) {
const answer = await inquirer.prompt({
name: 'yes_or_no',
const answer = await prompts({
type: 'confirm',
name: 'value',
message: prompt,
default: defaultAnswer
initial: defaultAnswer
})
return answer.yes_or_no
return answer.value || 'ESC'
}
// Ask the user the given question and returns their textual response
export async function askQuestionText(prompt = 'Please enter a string:', promptOptions = {}) {
let options = {
name: 'text',
type: 'input',
type: 'text',
name: 'value',
message: prompt
}
Object.assign(options, promptOptions)
let answer
try {
answer = await inquirer.prompt(options)
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return ''
} else {
console.log('Unknown error while displaying askQuestionText prompt:', error)
const answer = await prompts(options)
return answer.value || 'ESC'
}
export async function promptMenu(choices, prompt = 'Please choose:', hint = '(Use arrow keys)', defaultValue = null, warningMessage = null, numbered = false) {
if(prompt !== 'Please choose:') {
prompt = `\n${headerStyle(prompt)}`
}
if(numbered) {
const prependNumber = (num, str) => num + '. ' + str
for(let i = 0; i < choices.length; i++) {
if(typeof choices[i] === 'string') {
choices[i] = prependNumber(i + 1, choices[i])
} else if(typeof choices[i].title === 'string') {
if(!choices[i].value) {
choices[i].value = choices[i].title
}
choices[i].title = prependNumber(i + 1, choices[i].title)
}
}
}
return answer.text
console.log()
const answer = await prompts({
type: 'select',
name: 'value',
message: prompt,
choices: choices,
hint: hint,
initial: defaultValue,
warn: warningMessage
})