Browse Source

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

main
deicidus 3 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) * 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) * 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 * 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 ## Upcoming Features

59
index.js

@ -1,13 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
import chalk from 'chalk' import chalk from 'chalk'
import inquirer from 'inquirer'
import { execSync } from 'child_process' import { execSync } from 'child_process'
// Import ao-cli core features // Import ao-cli core features
import { exitIfRoot, detectOS, updateSoftware } from './scripts/system.js' import { exitIfRoot, detectOS, updateSoftware } from './scripts/system.js'
import { checkAoEnvFile, aoEnv, setAoEnv, AO_ENV_FILE_PATH } from './scripts/settings.js' import { checkAoEnvFile, aoEnv, setAoEnv, AO_ENV_FILE_PATH } from './scripts/settings.js'
import { unicornPortal, asciiArt, clearScreen } from './scripts/console.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 wander from './scripts/forest.js'
import useAoMenu from './scripts/ao.js' import useAoMenu from './scripts/ao.js'
import aoInstallWizard, { chooseAoVersion, checkAo } from './scripts/wizard.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 manual, { printManualPage, manualFolderAsMenu, AO_MANUAL_PATH } from './scripts/features/manual.js'
import aoCli from './scripts/features/ao-cli.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 // Prints the AO Main Menu and executes the user's choice
async function mainMenu() { async function mainMenu() {
console.log(`\n${headerStyle('AO Main Menu')}\n`) //console.log(`\n${headerStyle('AO Main Menu')}\n`)
let mainMenuChoices = [ let mainMenuChoices = [
'AO', { title: 'AO', description: 'use AO features' },
'Alchemy', { title: 'Alchemy', description: 'transmute system to gold' },
'Configure', { title: 'Configure', description: 'configure AO features' },
'Manual', { title: 'Manual', description: 'read the manual' },
'Exit' { title: 'Exit', description: 'ESC on any menu' }
] ]
let answer const answer = await promptMenu(mainMenuChoices, 'AO Main Menu')
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' }
}
}
let previousChoice let previousChoice
switch(answer.main_menu) { switch(answer) {
case 'AO': case 'AO':
while(await useAoMenu()) {} while(await useAoMenu()) {}
break break
@ -79,7 +61,7 @@ async function mainMenu() {
previousChoice = await manualFolderAsMenu(AO_MANUAL_PATH, 'AO User Manual', 'Back to Main Menu', previousChoice + 1) previousChoice = await manualFolderAsMenu(AO_MANUAL_PATH, 'AO User Manual', 'Back to Main Menu', previousChoice + 1)
} while(previousChoice !== false) } while(previousChoice !== false)
break break
case 'Exit': default:
farewell() farewell()
await sleep(310) await sleep(310)
return false return false
@ -90,9 +72,8 @@ async function mainMenu() {
// Prints the AO Admin Menu and executes the user's choice // 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 // Maybe Alchemy menu should be installation and update, and admin menu should be more configuration & AO member admin
async function adminMenu() { async function adminMenu() {
console.log(`\n${headerStyle('System Alchemy')}`)
const adminChoices = [ const adminChoices = [
{ name: 'Check AO install', value: 'check_AO' }, { title: 'Check AO install', value: 'check_AO' },
'Update everything', 'Update everything',
'Update system software', 'Update system software',
//'Update AO', //'Update AO',
@ -109,22 +90,8 @@ async function adminMenu() {
//'Update remote AOs', //'Update remote AOs',
'Back to Main Menu' 'Back to Main Menu'
] ]
let answer const answer = await promptMenu(adminChoices, 'System Alchemy')
try { switch(answer) {
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) {
case 'check_AO': case 'check_AO':
await checkAo() await checkAo()
break 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 ." "debug": "node inspect ."
}, },
"keywords": [ "keywords": [
"cli",
"AO", "AO",
"Autonomous Organization", "Autonomous Organization",
"autonomous", "autonomous",
@ -27,6 +28,7 @@
"author": "Coalition of Invisible Colleges", "author": "Coalition of Invisible Colleges",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"blessed": "^0.1.81",
"chalk": "^5.0.1", "chalk": "^5.0.1",
"chalk-animation": "^2.0.2", "chalk-animation": "^2.0.2",
"crypto": "^1.0.1", "crypto": "^1.0.1",
@ -34,11 +36,10 @@
"figlet": "^1.5.2", "figlet": "^1.5.2",
"gradient-string": "^2.0.1", "gradient-string": "^2.0.1",
"hash.js": "^1.1.7", "hash.js": "^1.1.7",
"inquirer": "^8.2.4",
"inquirer-interrupted-prompt": "^1.0.2",
"marked": "^4.0.16", "marked": "^4.0.16",
"marked-terminal": "^5.1.1", "marked-terminal": "^5.1.1",
"nanospinner": "^1.1.0", "nanospinner": "^1.1.0",
"prompts": "^2.4.2",
"sha.js": "^2.4.11", "sha.js": "^2.4.11",
"socket-io": "^1.0.0", "socket-io": "^1.0.0",
"socket.io-client": "^4.5.1", "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. // 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. // 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. // 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 { aoEnv } from './settings.js'
import { isLoggedIn, loginPrompt, logout } from './session.js' import { isLoggedIn, loginPrompt, logout } from './session.js'
import { getTopPriorityText } from './priority.js' import { getTopPriorityText } from './priority.js'
import { startPublicBootstrap } from './bootstrap.js'
import { AO_DEFAULT_HOSTNAME } from './api.js' import { AO_DEFAULT_HOSTNAME } from './api.js'
import { headerStyle } from './styles.js'
import { cardMenu } from './cards.js' import { cardMenu } from './cards.js'
import { connectMenu } from './connect.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). // 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() { export default async function useAoMenu() {
@ -37,22 +37,8 @@ export default async function useAoMenu() {
loggedIn ? 'Log Out' : 'Log In', loggedIn ? 'Log Out' : 'Log In',
'Back to Main Menu' 'Back to Main Menu'
) )
let answer const answer = await promptMenu(aoMenuChoices, 'Use AO features', 'connect to an AO server to use AO features')
try { switch(answer) {
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) {
case 'Deck': 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']) //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()) {} while(await cardMenu()) {}
@ -71,62 +57,7 @@ export default async function useAoMenu() {
await logout() await logout()
//await spinnerWait('Logging out...') //await spinnerWait('Logging out...')
break break
case 'Back to Main Menu': default:
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':
return false return false
} }
return true 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(':') const [HOST, PORT] = HOSTNAME.split(':')
// The AO API server websocket endpoint this ao-cli client will attempt to connect to // The AO API server websocket endpoint this ao-cli client will attempt to connect to
const AO_SOCKET_URL = const AO_SOCKET_URL = 'http://' + AO_DEFAULT_HOSTNAME // was process.env.NODE_ENV === 'development' ? 'http://' + AO_DEFAULT_HOSTNAME : '/'
process.env.NODE_ENV === 'development' ? 'http://' + AO_DEFAULT_HOSTNAME : '/'
export const socket = io(AO_SOCKET_URL, { export const socket = io(AO_SOCKET_URL, {
autoConnect: false 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 // Load the current session cookies from the AO .env file
let currentMemberId = aoEnv('AO_CLI_SESSION_MEMBERID') let currentMemberId = aoEnv('AO_CLI_SESSION_MEMBERID')
let currentSessionId = aoEnv('AO_CLI_SESSION_ID') let currentSessionId = aoEnv('AO_CLI_SESSION_ID')
@ -81,7 +83,7 @@ export async function createSession(user, pass) {
.on('error', () => false) .on('error', () => false)
currentMemberId = result.body.memberId currentMemberId = result.body.memberId
currentSessionToken = token currentSessionToken = token
currentSessionId = session // Not used in this api.js yet currentSessionId = session
return { session, token, memberId: currentMemberId } 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 // 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) { export async function getAoBootstrapList(serverOnion = null) {
const result = await getRequest('/bootstrap', undefined, serverOnion, false) const result = await getRequest('/bootstrap', undefined, serverOnion, false)
@ -125,6 +164,15 @@ export async function bootstrap(serverOnion = null) {
return onionList 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) { export async function connectToAo(address, secret) {
return await postEvent({ return await postEvent({
type: 'ao-outbound-connected', 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( /*reaction(
() => { () => {
//return aoStore.state.socketState //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) // 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 { getCardByName, createCard, prioritizeCard } from './api.js'
import { headerStyle } from './styles.js' import { headerStyle } from './styles.js'
import { prioritiesMenu } from './priority.js' import { prioritiesMenu } from './priority.js'
import { aoEnv } from './settings.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 // The card menu is complex and so has been split into this separate file
export async function cardMenu() { export async function cardMenu() {
console.log(`\n${headerStyle('My Deck')}`)
const cardChoices = [ const cardChoices = [
{ name: 'Top priorities', value: 'priorities', short: 'priorities' }, // hand? (7) (add #s in parens) { title: 'Top priorities', value: 'priorities', short: 'priorities' }, // hand? (7) (add #s in parens)
{ name: 'Cards in hand', value: 'subcards', short: 'hand' }, // (current) deck? (60) { title: 'Cards in hand', value: 'subcards', short: 'hand' }, // (current) deck? (60)
{ name: 'Browse full deck', value: 'browse', short: 'browse' }, // archive? (10,000) { title: 'Browse full deck', value: 'browse', short: 'browse' }, // archive? (10,000)
'Back to AO Menu' 'Back to AO Menu'
] ]
try { const answer = await promptMenu(cardChoices, 'My Deck')
const answer = await inquirer.prompt({ switch(answer.card_menu) {
name: 'card_menu', case 'priorities':
type: 'list', while(await prioritiesMenu()) {}
message: 'Please choose:', break
choices: cardChoices, case 'subcards':
pageSize: cardChoices.length, while(await subcardsMenu()) {}
}) break
switch(answer.card_menu) { case 'browse':
case 'priorities': while(await browseMenu()) {}
while(await prioritiesMenu()) {} break
break default:
case 'subcards':
while(await subcardsMenu()) {}
break
case 'browse':
while(await browseMenu()) {}
break
default:
return false
}
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false return false
}
} }
return true 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 // 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) { export async function createCardInteractive(prioritized = true) {
console.log('createcardinteractive')
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID') const memberId = aoEnv('AO_CLI_SESSION_MEMBERID')
if(!memberId) { if(!memberId) {
console.log('Not logged in.') console.log('Not logged in.')
return false return false
} }
const answer = await inquirer.prompt({ const answer = await askQuestionText('New card or Enter to end:')
name: 'new_card_text',
type: 'input',
message: 'New card or Enter to end:',
})
if(answer.new_card_text.trim().length <= 0) { if(answer.new_card_text.trim().length <= 0) {
return false 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. // 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 { headerStyle } from './styles.js'
import { aoEnv, setAoEnv } from './settings.js' import { aoEnv, setAoEnv } from './settings.js'
import { isLoggedIn } from './session.js' import { isLoggedIn } from './session.js'
import { isInstalled } from './features/tor.js' import { isInstalled } from './features/tor.js'
import { connectToAo, getAoBootstrapList, bootstrap } from './api.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 // Prints a menu to connect your AO to other AOs and manage connections
export async function connectMenu() { export async function connectMenu() {
console.log(`\n${headerStyle('AO P2P')}`)
const PUBLIC_BOOTSTRAP_ENABLED = aoEnv('PUBLIC_BOOTSTRAP_ENABLED') 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) { if(PUBLIC_BOOTSTRAP_ENABLED) {
publicBootstrapMenuItem = { name: 'Disable p2p bootstrap', value: 'disable_bootstrap' } publicBootstrapMenuItem = { title: 'Disable p2p bootstrap', value: 'disable_bootstrap' }
} }
const connectChoices = [ const connectChoices = [
{ name: 'Connect to AO', value: 'connect', short: 'p2p connect'}, { title: 'Connect to AO', value: 'connect', short: 'p2p connect'},
{ name: 'View connections', value: 'connections' }, { title: 'View connections', value: 'connections' },
{ name: 'Bootstrap now', value: 'bootstrap' }, { title: 'Bootstrap now', value: 'bootstrap' },
publicBootstrapMenuItem, publicBootstrapMenuItem,
{ name: 'Back to AO Menu', value: false } { title: 'Back to AO Menu', value: false }
] ]
let answer const answer = await promptMenu(connectChoices, 'AO P2P')
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
}
}
switch(answer.connect_menu) { switch(answer.connect_menu) {
case 'connect': case 'connect':
await connectInteractive() await connectInteractive()
@ -110,24 +94,11 @@ async function connectInteractive() {
} }
return true return true
} }
let answer const answer = await askQuestionText('Enter connection string of other AO:') //todo: validate: validateConnectionString
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 [onion, secret] = answer.connection_string.split(':') const [onion, secret] = answer.connection_string.split(':')
console.log('onion is', onion, 'and secret is', secret) console.log('onion is', onion, 'and secret is', secret)
console.log('Attempting connect...') console.log('Attempting connect...')
const result = await connectToAo(onion, secret) const result = await connectToAo(onion, secret)
console.log('result is', result.body) console.log('result is', result.body)
return true 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) // 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 // Also contains the Features menus to control these features in this directory
import chalk from 'chalk' import chalk from 'chalk'
import inquirer from 'inquirer'
import fs from 'fs' import fs from 'fs'
import { lsFolder } from '../files.js' import { lsFolder } from '../files.js'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@ -9,6 +8,7 @@ import path from 'path'
import { headerStyle, greenChalk, styledStatus } from '../styles.js' import { headerStyle, greenChalk, styledStatus } from '../styles.js'
import { spinner } from '../console.js' import { spinner } from '../console.js'
import SystemServiceManager, { getCustomServicesList, addCustomServiceInteractive, removeCustomService } from '../services.js' import SystemServiceManager, { getCustomServicesList, addCustomServiceInteractive, removeCustomService } from '../services.js'
import { promptMenu } from '../welcome.js'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
@ -33,7 +33,6 @@ export default features
// Prints the Configure AO Features menu and executes the user's choice // Prints the Configure AO Features menu and executes the user's choice
let featuresChoices let featuresChoices
export async function featuresMenu(previousMenuChoice = 0) { export async function featuresMenu(previousMenuChoice = 0) {
console.log(`\n${headerStyle('Configure AO Features')}`)
const stopSpinner = spinner('Loading status...') const stopSpinner = spinner('Loading status...')
let loadedFeatures = 0 let loadedFeatures = 0
if(!featuresChoices) { if(!featuresChoices) {
@ -49,10 +48,10 @@ export async function featuresMenu(previousMenuChoice = 0) {
} }
const statusColumn = styledStatus(status, 25) const statusColumn = styledStatus(status, 25)
const descriptionColumn = feature.description || '' 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 return choice
}) })
featuresChoices.push(new inquirer.Separator()) featuresChoices.push({ title: '---', disabled: true })
const customServices = getCustomServicesList() const customServices = getCustomServicesList()
customServices.forEach(serviceName => { customServices.forEach(serviceName => {
const nameColumn = serviceName.padEnd(17) const nameColumn = serviceName.padEnd(17)
@ -60,11 +59,11 @@ export async function featuresMenu(previousMenuChoice = 0) {
const status = service.status() || 'Unknown' const status = service.status() || 'Unknown'
const statusColumn = styledStatus(status, 25) const statusColumn = styledStatus(status, 25)
const descriptionColumn = service.description || '' 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(choice)
}) })
featuresChoices.push( featuresChoices.push(
{ name: 'Add custom service', value: 'add_service' }, { title: 'Add custom service', value: 'add_service' },
'Back to Main Menu' 'Back to Main Menu'
) )
} else { } else {
@ -73,23 +72,8 @@ export async function featuresMenu(previousMenuChoice = 0) {
}).length }).length
} }
stopSpinner('Loaded status for ' + loadedFeatures + '/' + (featuresChoices.length - 1) + ' features.') stopSpinner('Loaded status for ' + loadedFeatures + '/' + (featuresChoices.length - 1) + ' features.')
let answer const answer = await promptMenu(featuresChoices, 'Configure AO Features', undefined, previousMenuChoice)
try { if(answer.includes('service_')) {
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 serviceName = answer.features_menu.replace(/^service_/, '') const serviceName = answer.features_menu.replace(/^service_/, '')
const service = new SystemServiceManager(serviceName) const service = new SystemServiceManager(serviceName)
const nameWithoutDot = serviceName.split('.')[0] + ' service' const nameWithoutDot = serviceName.split('.')[0] + ' service'
@ -97,7 +81,7 @@ export async function featuresMenu(previousMenuChoice = 0) {
await oneFeatureMenu(nameWithoutDot, service, true) await oneFeatureMenu(nameWithoutDot, service, true)
return answer.features_menu return answer.features_menu
} }
switch(answer.features_menu) { switch(answer) {
case 'add_service': 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.') 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()) {} while(await addCustomServiceInteractive()) {}
@ -106,10 +90,10 @@ export async function featuresMenu(previousMenuChoice = 0) {
case 'Back to Main Menu': case 'Back to Main Menu':
return false return false
} }
const chosenFeature = features[answer.features_menu] const chosenFeature = features[answer]
const chosenFeatureName = (chosenFeature.hasOwnProperty('name') && chosenFeature.name.length >= 1) ? chosenFeature.name : answer.features_menu const chosenFeatureName = (chosenFeature.hasOwnProperty('name') && chosenFeature.name.length >= 1) ? chosenFeature.name : answer
while(await oneFeatureMenu(chosenFeatureName, chosenFeature)) {} while(await oneFeatureMenu(chosenFeatureName, chosenFeature)) {}
return answer.features_menu return answer
} }
// Prints the menu options for a specific feature (including subfeatures) // 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 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 const running = installed && typeof feature.isRunning === 'function' ? feature.isRunning() : feature.hasOwnProperty('isRunning') ? feature.isRunning : false
if(running && typeof feature.stop === 'function') { 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') { } 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(status === 'off') {
if(typeof feature.install === 'function') { if(typeof feature.install === 'function') {
featureChoices.push({ name: 'Install ' + name, value: () => feature.install() }) featureChoices.push({ title: 'Install ' + name, value: feature.install })
} }
} else { } else {
if(typeof feature.update === 'function') { 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') { if(typeof feature.uninstall === 'function') {
featureChoices.push({ name: 'Uninstall ' + name, value: () => feature.uninstall() }) featureChoices.push({ title: 'Uninstall ' + name, value: feature.uninstall })
} }
} }
if(isCustom) { if(isCustom) {
featureChoices.push({ name: 'Remove from list', value: () => { featureChoices.push({ title: 'Remove from list', value: () => {
const nameWithoutLabel = name.replace(/ service/, '') const nameWithoutLabel = name.replace(/ service/, '')
removeCustomService(nameWithoutLabel) removeCustomService(nameWithoutLabel)
featuresChoices = null featuresChoices = null
@ -157,7 +141,7 @@ function oneFeatureMenuChoices(name, feature, status, isCustom = false) {
feature.menu.forEach(menuItem => { feature.menu.forEach(menuItem => {
const menuItemName = typeof menuItem.name === 'function' ? menuItem.name() : menuItem.name const menuItemName = typeof menuItem.name === 'function' ? menuItem.name() : menuItem.name
if(menuItemName) { if(menuItemName) {
featureChoices.push({ name: menuItemName, value: menuItem.Value }) featureChoices.push({ title: menuItemName, value: menuItem.Value })
} }
// todo: uninstall option will go here also // todo: uninstall option will go here also
}) })
@ -197,30 +181,15 @@ export async function oneFeatureMenu(name, feature, isCustom = false) {
featureChoices.push( featureChoices.push(
'Back to Features' 'Back to Features'
) )
let answer const answer = await promptMenu(featureChoices, name)
try { if(answer === 'Back to Features') {
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') {
return false return false
} }
if(typeof answer.feature_menu === 'function') { if(typeof answer === 'function') {
answer.feature_menu() await answer()
return true return true
} else if(Object.keys(feature).includes(answer.feature_menu)) { } else if(Object.keys(feature).includes(answer)) {
await feature[answer.feature_menu]() await feature[answer]()
return true return true
} }
console.log('Not yet implemented') 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 // Functions for downloading, updating, and displaying the AO Manual, a hierarchy of markdown files
import chalk from 'chalk' import chalk from 'chalk'
import inquirer from 'inquirer'
import { execSync, exec } from 'child_process' import { execSync, exec } from 'child_process'
import { loadYamlMarkdownFile, lsFolder, isFolder } from '../files.js' import { loadYamlMarkdownFile, lsFolder, isFolder } from '../files.js'
import { repeatString, centerLines } from '../strings.js' import { repeatString, centerLines } from '../strings.js'
import { promptMenu } from '../welcome.js'
import { headerStyle, manualTitleStyle } from '../styles.js' import { headerStyle, manualTitleStyle } from '../styles.js'
import { basename } from 'path' import { basename } from 'path'
import { marked } from 'marked' import { marked } from 'marked'
@ -158,25 +158,17 @@ export async function manualFolderAsMenu(path, menuTitle, backOption, previousMe
if(previousMenuChoice >= menuChoices.length) { if(previousMenuChoice >= menuChoices.length) {
previousMenuChoice = 0 previousMenuChoice = 0
} }
console.log(`\n${headerStyle(menuTitle)}`) const answer = await promptMenu(menuChoices, menuTitle, 'choose a topic', previousMenuChoice, undefined, true)
const answer = await inquirer.prompt({ const chosenMenuIndex = menuChoices.indexOf(answer)
name: 'manual_menu', if(answer === backOption) {
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 return false
} }
const chosenPath = path + Object.values(menuItems.find(menuItem => Object.keys(menuItem)[0] === answer.manual_menu))[0] const chosenPath = path + Object.values(menuItems.find(menuItem => Object.keys(menuItem)[0] === answer))[0]
await printManualPage(chosenPath, answer.manual_menu) await printManualPage(chosenPath, answer)
const newBackOption = backOption === 'Back to Main Menu' ? 'Back to Table of Contents' : 'Back to ' + menuTitle const newBackOption = backOption === 'Back to Main Menu' ? 'Back to Table of Contents' : 'Back to ' + menuTitle
let previousChoice = 0 let previousChoice = 0
do { do {
previousChoice = await manualFolderAsMenu(chosenPath, answer.manual_menu, newBackOption, previousChoice + 1) previousChoice = await manualFolderAsMenu(chosenPath, answer, newBackOption, previousChoice + 1)
} }
while(previousChoice !== false) while(previousChoice !== false)
return chosenMenuIndex return chosenMenuIndex
@ -189,4 +181,4 @@ export default {
install: downloadManual, install: downloadManual,
isInstalled: () => manualStatus() === 'installed', isInstalled: () => manualStatus() === 'installed',
update: updateManual update: updateManual
} }

8
scripts/keyboard.js

@ -1,10 +1,10 @@
// Hook to add keyboard shortcuts to all inquirer prompts // Hook to add keyboard shortcuts to all inquirer prompts
import inquirer from 'inquirer' //import inquirer from 'inquirer'
import InterruptedPrompt from 'inquirer-interrupted-prompt' //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. // 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. // 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. // 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. // Prioritize cards within other cards. Each card has a .priorities array of other taskIds.
import inquirer from 'inquirer'
import { headerStyle } from './styles.js' import { headerStyle } from './styles.js'
import { aoEnv } from './settings.js' import { aoEnv } from './settings.js'
import { getCard, prioritizeCard, completeCard, uncheckCard, refocusCard } from './api.js' import { getCard, prioritizeCard, completeCard, uncheckCard, refocusCard } from './api.js'
import { createCardInteractive } from './cards.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 // Prints the text (.name) of the first card prioritized in the logged-in users member card
export async function getTopPriorityText() { export async function getTopPriorityText() {
@ -65,7 +65,7 @@ export async function prioritiesMenu(taskId = null) {
return 'Missing card, repair your database' return 'Missing card, repair your database'
} }
return { return {
name: priorityCard.name, title: priorityCard.name,
value: { index: i, card: priorityCard }, value: { index: i, card: priorityCard },
short: priorityCard.name.substring(0, 70) + priorityCard.name.length >= 70 ? '...' : '' short: priorityCard.name.substring(0, 70) + priorityCard.name.length >= 70 ? '...' : ''
} }
@ -80,29 +80,15 @@ export async function prioritiesMenu(taskId = null) {
} }
}) })
if(!isNaN(firstIndexEchelonDecreases)) { if(!isNaN(firstIndexEchelonDecreases)) {
prioritiesChoices.splice(firstIndexEchelonDecreases, 0, new inquirer.Separator('───')) prioritiesChoices.splice(firstIndexEchelonDecreases, 0, { title: '───', disabled: true })
} }
} }
prioritiesChoices.push( prioritiesChoices.push(
{ name: 'Create priority', value: 'create_here', short: 'new priority' }, { title: 'Create priority', value: 'create_here', short: 'new priority' },
{ name: 'Back to Deck', value: false, short: 'back' } { title: 'Back to Deck', value: false, short: 'back' }
) )
let answer const answer = await promptMenu(prioritiesChoices, 'My Priorities', undefined, undefined, 'Upboated tasks will be inserted above this line')
try { switch(answer) {
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) {
case false: case false:
return false return false
case 'create_here': case 'create_here':
@ -119,7 +105,7 @@ export async function prioritiesMenu(taskId = null) {
const chosenTaskId = chosenTask.taskId const chosenTaskId = chosenTask.taskId
let previousAnswer let previousAnswer
do { do {
previousAnswer = await priorityCardMenu(chosenTask, answer.priorities_menu.index, priorityCards) previousAnswer = await priorityCardMenu(chosenTask, answer.index, priorityCards)
if(previousAnswer) { if(previousAnswer) {
const fetchedCards = await getCard(chosenTaskId, false) const fetchedCards = await getCard(chosenTaskId, false)
if(!fetchedCards || fetchedCards.length < 1) { if(!fetchedCards || fetchedCards.length < 1) {
@ -147,33 +133,18 @@ async function priorityCardMenu(card, index, allPriorities) {
return false return false
} }
const isChecked = card.claimed.includes(memberId) const isChecked = card.claimed.includes(memberId)
console.log(`\n${headerStyle('Priority: ' + card.name)}`)
let priorityChoices = [] let priorityChoices = []
if(index != 0) { if(index != 0) {
priorityChoices.push({ name: 'Upboat', value: 'upboat', short: 'upboat' }) priorityChoices.push({ title: 'Upboat', value: 'upboat', short: 'upboat' })
} }
priorityChoices.push( priorityChoices.push(
{ name: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' }, { title: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' },
{ name: 'Downboat', value: 'downboat', short: 'downboat' }, { title: 'Downboat', value: 'downboat', short: 'downboat' },
//{ name: 'Browse within', value: 'browse', short: 'browse' } //{ title: 'Browse within', value: 'browse', short: 'browse' }
{ name: 'Back to Priorities', value: false, short: 'back' } { title: 'Back to Priorities', value: false, short: 'back' }
) )
let answer const answer = await promptMenu(priorityChoices, 'Priority: ' + card.name)
try { switch(answer) {
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) {
case 'check': case 'check':
if(isChecked) { if(isChecked) {
await uncheckCard(taskId) 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 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 // 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 // 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 { createSession, logout } from './api.js'
import { promptMenu } from './welcome.js'
async function testLoginAndOut() { async function testLoginAndOut() {
const username = 'ao' const username = 'ao'
@ -56,24 +56,11 @@ export default async function testsMenu() {
return menuTitle return menuTitle
}) })
testChoices.push('Back to Main Menu') testChoices.push('Back to Main Menu')
let answer const answer = await promptMenu(testChoices, 'AO Unit Tests')
try { if(answer === 'Back to Main Menu') {
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') {
return false return false
} }
const testFunction = tests[answer.tests_menu] const testFunction = tests[answer.tests_menu]
if(testFunction) await testFunction() if(testFunction) await testFunction()
return true return true
} }

67
scripts/welcome.js

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

60
scripts/wizard.js

@ -1,5 +1,4 @@
// Functions related to intelligently installing the AO as a whole. Specific additional feature modules are each a file under ./features. // Functions related to intelligently installing the AO as a whole. Specific additional feature modules are each a file under ./features.
import inquirer from 'inquirer'
import path from 'path' import path from 'path'
import { execSync } from 'child_process' import { execSync } from 'child_process'
import { aoEnv, setAoEnv } from './settings.js' import { aoEnv, setAoEnv } from './settings.js'
@ -9,7 +8,7 @@ import { aoIsInstalled } from './features/ao-server.js'
import { asciiArt } from './console.js' import { asciiArt } from './console.js'
import { headerStyle, heading2, styledStatus } from './styles.js' import { headerStyle, heading2, styledStatus } from './styles.js'
import features from './features/index.js' import features from './features/index.js'
import { yesOrNo } from './welcome.js' import { yesOrNo, promptMenu } from './welcome.js'
import { isNpmPackageInstalled } from './system.js' import { isNpmPackageInstalled } from './system.js'
const AO_PATH = path.join(process.env.HOME, '.ao') const AO_PATH = path.join(process.env.HOME, '.ao')
@ -63,58 +62,27 @@ export default async function aoInstallWizard() {
// Asks if the user wants to do a minimal, standard, or full install and returns their answer // Asks if the user wants to do a minimal, standard, or full install and returns their answer
async function chooseInstallLevel() { async function chooseInstallLevel() {
let answer return await promptMenu([
try { { title: 'Minimal'.padEnd(11) + 'only core AO web server (or only ao-cli)', value: 'minimal', short: 'minimal install' },
answer = await inquirer.prompt({ { title: 'Standard'.padEnd(11) + 'most AO features installed (recommended)', value: 'standard', short: 'standard install' },
name: 'level_menu', { title: 'Full'.padEnd(11) + 'all AO features installed', value: 'full', short: 'full install' },
type: 'list', { title: 'Cancel', value: false }
message: 'What kind of installation?', ], 'What kind of installation?')
choices: [
{ name: 'Minimal'.padEnd(11) + 'only core AO web server (or only ao-cli)', value: 'minimal', short: 'minimal install' },
{ name: 'Standard'.padEnd(11) + 'most AO features installed (recommended)', value: 'standard', short: 'standard install' },
{ name: 'Full'.padEnd(11) + 'all AO features installed', value: 'full', short: 'full install' },
{ name: 'Cancel', value: false }
]
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
return answer.level_menu
} }
// Asks whether the user wants to install ao-svelte, ao-3, or ao-cli (only) and returns their choice // Asks whether the user wants to install ao-svelte, ao-3, or ao-cli (only) and returns their choice
export async function chooseAoVersion() { export async function chooseAoVersion() {
console.log(`\n${headerStyle('Choose AO Version')}`)
console.log('Active version:', aoEnv('AO_VERSION')) console.log('Active version:', aoEnv('AO_VERSION'))
let answer const answer = await promptMenu([
try { { title: 'ao-svelte'.padEnd(12) + 'new and mobile-first, currently in prototype phase', value: 'ao-svelte', short: 'ao-svelte' },
answer = await inquirer.prompt({ { title: 'ao-3'.padEnd(12) + 'the original, created in Vue 3, polished and bug-free', value: 'ao-3', short: 'ao-3' },
name: 'version_menu', { title: 'ao-cli only'.padEnd(12), value: 'ao-cli' },
type: 'list', { title: 'Cancel', value: false }
message: 'Please choose:', ], 'Choose AO Version:')
choices: [
{ name: 'ao-svelte'.padEnd(12) + 'new and mobile-first, currently in prototype phase', value: 'ao-svelte', short: 'ao-svelte' },
{ name: 'ao-3'.padEnd(12) + 'the original, created in Vue 3, polished and bug-free', value: 'ao-3', short: 'ao-3' },
{ name: 'ao-cli only'.padEnd(12), value: 'ao-cli' },
'Cancel'
]
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
if(answer.version_menu === 'Cancel') {
return false
}
setAoEnv('AO_VERSION', answer.version_menu) setAoEnv('AO_VERSION', answer.version_menu)
// todo: If the version has changed, install it now // todo: If the version has changed, install it now
console.log('AO version choice saved.') console.log('AO version choice saved.')
return answer.version_menu return answer
} }
function checkAoDirectories() { function checkAoDirectories() {

Loading…
Cancel
Save