Browse Source

modularized install and uninstall of features, cleaned up menus

main
deicidus 2 years ago
parent
commit
52a52c632a
  1. 7
      README.md
  2. 236
      index.js
  3. 3
      package.json
  4. 67
      scripts/api.js
  5. 14
      scripts/crypto.js
  6. 83
      scripts/features/ao-cli.js
  7. 20
      scripts/features/ao-server.js
  8. 20
      scripts/features/bitcoin.js
  9. 7
      scripts/features/borg.js
  10. 5
      scripts/features/certbot.js
  11. 5
      scripts/features/encryption.js
  12. 5
      scripts/features/files.js
  13. 5
      scripts/features/glossary.js
  14. 18
      scripts/features/index.js
  15. 5
      scripts/features/jitsi.js
  16. 5
      scripts/features/jubilee.js
  17. 23
      scripts/features/lightning.js
  18. 45
      scripts/features/manual.js
  19. 28
      scripts/features/nginx.js
  20. 5
      scripts/features/signal.js
  21. 5
      scripts/features/themes.js
  22. 20
      scripts/features/tor.js
  23. 4
      scripts/features/youtube-dl.js
  24. 1
      scripts/files.js
  25. 2
      scripts/manual.js
  26. 3
      scripts/priority.js
  27. 14
      scripts/session.js
  28. 4
      scripts/strings.js

7
README.md

@ -10,7 +10,7 @@ To install:
`npm i -g @autonomousorganization/ao-cli` `npm i -g @autonomousorganization/ao-cli`
Then you can run with `ao-cli`. (In the menus you will find an option to add `ao` as an alias.) Then you can run with `ao-cli`.
## Features ## Features
@ -21,18 +21,20 @@ These features work right now:
* Wraps the functionality of (some of) Zen's Alchemy suite of scripts (system configuration, AO installation) * Wraps the functionality of (some of) Zen's Alchemy suite of scripts (system configuration, AO installation)
* `ao-cli` can self-update to the newest version * `ao-cli` can self-update to the newest version
* Run AO unit tests to verify the up-to-spec functioning of the system's running AO API server * Run AO unit tests to verify the up-to-spec functioning of the system's running AO API server
* Easily view installed/running status of optional AO features
* Add `ao` alias for `ao-cli` (under Features→ao-cli)
## Upcoming Features ## Upcoming Features
These features are planned and many are mocked up in the menus: These features are planned and many are mocked up in the menus:
* Join the AO .onion bootstrapping network and find public AO chatrooms p2p over tor * Join the AO .onion bootstrapping network and find public AO chatrooms p2p over tor
* Easily install/uninstall and turn on/off optional AO features
* Operate essential AO client features (like creating and sending cards p2p via tor) * Operate essential AO client features (like creating and sending cards p2p via tor)
* Easily install and configure your AO server installation * Easily install and configure your AO server installation
* Easily use hardware-owner-only god-mode features for your AO server including resetting any password or deleting any member * Easily use hardware-owner-only god-mode features for your AO server including resetting any password or deleting any member
* Easily monitor your AO server status and start/stop the service * Easily monitor your AO server status and start/stop the service
* Easily switch between serving different AO frontends: `ao-svelte`, `ao-3` (Vue), or `ao-react` * Easily switch between serving different AO frontends: `ao-svelte`, `ao-3` (Vue), or `ao-react`
* Easily install/uninstall and turn on/off option AO features
* Easily update all your remote AOs at once * Easily update all your remote AOs at once
* Easily install your preferred flavor of Unix on any unsecured Windows computer given its IP address (j/k) * Easily install your preferred flavor of Unix on any unsecured Windows computer given its IP address (j/k)
* Full interactive wizard to walk you through setting up and connecting new AO hardware resources to your AO server * Full interactive wizard to walk you through setting up and connecting new AO hardware resources to your AO server
@ -49,6 +51,7 @@ These features are planned and many are mocked up in the menus:
## Version History ## Version History
* 0.0.9 Features menu loaded from module file for each feature; view top priority; menu cleanup
* 0.0.8 Added self-update feature and --version/-v arg * 0.0.8 Added self-update feature and --version/-v arg
* 0.0.6 User manual downloads and updates automatically from [official ao-manual repo](https://git.coalitionofinvisiblecolleges.org/autonomousorganization/ao-manual) * 0.0.6 User manual downloads and updates automatically from [official ao-manual repo](https://git.coalitionofinvisiblecolleges.org/autonomousorganization/ao-manual)
* 0.0.5 Added browsable manual * 0.0.5 Added browsable manual

236
index.js

@ -12,7 +12,9 @@ import { sleep } from './scripts/util.js'
import { tests } from './scripts/tests.js' import { tests } from './scripts/tests.js'
import { headerStyle, greenChalk } from './scripts/styles.js' import { headerStyle, greenChalk } from './scripts/styles.js'
import './scripts/strings.js' import './scripts/strings.js'
import { installAoAlias, getAoCliVersion, selfUpdate, downloadManual, updateManual } from './scripts/features.js'
// Import AO modular features
import * as features from './scripts/features/index.js'
import { startPublicBootstrap } from './scripts/bootstrap.js' import { startPublicBootstrap } from './scripts/bootstrap.js'
import { isLoggedIn, loginPrompt, logout } from './scripts/session.js' import { isLoggedIn, loginPrompt, logout } from './scripts/session.js'
import { AO_DEFAULT_HOSTNAME } from './scripts/api.js' import { AO_DEFAULT_HOSTNAME } from './scripts/api.js'
@ -38,7 +40,6 @@ async function mainMenu() {
let mainMenuChoices = [ let mainMenuChoices = [
'AO', 'AO',
'Features', 'Features',
'Admin',
'Alchemy', 'Alchemy',
'Manual', 'Manual',
'Exit' 'Exit'
@ -50,33 +51,32 @@ async function mainMenu() {
choices: mainMenuChoices, choices: mainMenuChoices,
pageSize: mainMenuChoices.length pageSize: mainMenuChoices.length
}) })
let previousChoice
switch(answer.main_menu) { switch(answer.main_menu) {
case 'AO': case 'AO':
while(await useAoMenu()) {} while(await useAoMenu()) {}
break break
case 'Features': case 'Features':
while(await featuresMenu()) {} do {
break previousChoice = await featuresMenu(previousChoice)
case 'Admin': } while(previousChoice !== false)
while(await adminMenu()) {}
break break
case 'Alchemy': case 'Alchemy':
while(await alchemyMenu()) {} while(await adminMenu()) {}
break break
case 'Manual': case 'Manual':
if(!isFolder(AO_MANUAL_PATH)) { if(!isFolder(AO_MANUAL_PATH)) {
console.log("Downloading the AO manual...") console.log("Downloading the AO manual...")
if(downloadManual()) { if(features.manual.install()) {
console.log("Downloaded the AO Manual from the official git repo via http and saved to", AO_MANUAL_PATH + '.') console.log("Downloaded the AO Manual from the official git repo via http and saved to", AO_MANUAL_PATH + '.')
} else { } else {
console.log('Failed to download the AO manual, sorry.') console.log('Failed to download the AO manual, sorry.')
return false return false
} }
} else { } else {
updateManual() features.manual.update()
} }
await printManualPage(AO_MANUAL_PATH) // Fencepost case - print overview page await printManualPage(AO_MANUAL_PATH) // Fencepost case - print overview page
let previousChoice
do { do {
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)
@ -93,7 +93,10 @@ async function mainMenu() {
async function useAoMenu() { async function useAoMenu() {
const loggedIn = isLoggedIn() const loggedIn = isLoggedIn()
console.log(`\n${headerStyle('AO')}\n`) console.log(`\n${headerStyle('AO')}\n`)
console.log('Top priority:', await getTopPriorityText()) if(loggedIn) {
console.log('Logged in as:', aoEnv('AO_CLI_SESSION_USERNAME'))
console.log('Top priority:', await getTopPriorityText())
}
let aoMenuChoices = [] let aoMenuChoices = []
if(loggedIn) { if(loggedIn) {
aoMenuChoices.push( aoMenuChoices.push(
@ -200,19 +203,14 @@ async function chatMenu() {
// 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('AO Admin Menu')}`) console.log(`\n${headerStyle('System Alchemy')}`)
const adminChoices = [ const adminChoices = [
'Install \'ao\' alias for \'ao-cli\'', 'Update system software',
'Update ao-cli',
'Check AO install',
'Update AO',
'Switch AO target server', 'Switch AO target server',
'Switch AO database',
'Import/Export state/decks', 'Import/Export state/decks',
'Check AO service',
'Watch logs now', 'Watch logs now',
'Start/Stop AO service',
'Tests', 'Tests',
'Update remote AOs',
'Back to Main Menu' 'Back to Main Menu'
] ]
const answer = await inquirer.prompt({ const answer = await inquirer.prompt({
@ -223,20 +221,13 @@ async function adminMenu() {
pageSize: adminChoices.length, pageSize: adminChoices.length,
}) })
switch(answer.admin_menu) { switch(answer.admin_menu) {
case 'Install \'ao\' alias for \'ao-cli\'': case 'Update system software':
installAoAlias() updateSoftware()
break
case 'Update ao-cli':
await selfUpdate()
break break
case 'Check AO install':
case 'Update AO':
case 'Switch AO target server': case 'Switch AO target server':
case 'Switch AO database':
case 'Import/Export state/decks': case 'Import/Export state/decks':
case 'Check AO service':
case 'Watch logs now': case 'Watch logs now':
case 'Start/Stop AO service': case 'Update remote AOs':
console.log("Not yet implemented.") console.log("Not yet implemented.")
break break
case 'Tests': case 'Tests':
@ -269,79 +260,148 @@ async function testsMenu() {
return true return true
} }
// Prints the AO Admin Menu and executes the user's choice // Returns a colored capitalized status word
async function alchemyMenu() { const styledStatus = (fullWord) => {
console.log(`\n${headerStyle('Alchemy')}`) const lookup = {
const alchemyChoices = [ off: ' ' + chalk.grey('Off') + ' ',
'Update software', installed: chalk.blue('Installed') + ' ',
'Install AO prerequisites', enabled: ' ' + greenChalk('Enabled') + ' ',
'Check bitcoin status', running: ' ' + greenChalk('Running') + ' ',
'Back to Main Menu' synced: ' ' + greenChalk('Synced') + ' ',
] error: ' ' + chalk.red('Error') + ' '
}
return lookup[fullWord.toLowerCase()] + ' '
}
// Prints the Configure AO Features menu and executes the user's choice
let featuresChoices
async function featuresMenu(previousMenuChoice = 0) {
console.log(`\n${headerStyle('Configure AO Features')}`)
if(!featuresChoices) {
featuresChoices = Object.entries(features).map(([featureKey, feature]) => {
let featureName = featureKey
if(feature.hasOwnProperty('name') && feature.name.length >= 1) {
featureName = feature.name
}
const nameColumn = featureName.padEnd(17)
const statusColumn = styledStatus(feature.status())
const descriptionColumn = feature.description || ''
const choice = { name: nameColumn + statusColumn + descriptionColumn, value: featureKey, short: featureKey}
return choice
})
featuresChoices.push(
'Back to Main Menu'
)
}
const answer = await inquirer.prompt({ const answer = await inquirer.prompt({
name: 'alchemy_menu', name: 'features_menu',
type: 'list', type: 'list',
message: 'Please choose:', message: 'Please choose:',
choices: alchemyChoices choices: featuresChoices,
default: previousMenuChoice,
pageSize: featuresChoices.length
}) })
switch(answer.alchemy_menu) { if(answer.features_menu === 'Back to Main Menu') {
case alchemyChoices[0]: return false
updateSoftware()
break
case alchemyChoices[1]:
installRequired()
break
case alchemyChoices[2]:
let stdout = execSync('source ~/Alchemy/ingredients/lead && source ~/Alchemy/ingredients/gold && bitcoin_is_synced')
console.log(`${stdout}`)
break
default:
return false
} }
return true while(await oneFeatureMenu(answer.features_menu, features[answer.features_menu])) {}
return answer.features_menu
} }
// Prints the Configure AO Features menu and executes the user's choice // Prints the menu options for a specific feature.
async function featuresMenu() { // Each feature module can export functions for status, install, version, update and these will be listed in the menu based on context
console.log(`\n${headerStyle('Configure AO Features')}`) // If the module also has a menu: field, these menu items will each be appended if the name is truthy when calculated
const status = { // Prints all the standard-named features plus features listed under 'menu'
off: ' ' + chalk.grey('Off') + ' ', async function oneFeatureMenu(name, feature) {
ins: chalk.yellow('Installed') + ' ', console.log(`\n${headerStyle(name)}`)
ena: ' ' + greenChalk('Enabled') + ' ', const featureChoices = []
run: ' ' + greenChalk('Running') + ' ', const status = feature?.status() || false
err: ' ' + chalk.red('Error') + ' ' if(!status) {
console.log("This AO feature module lacks a status() function, not sure which menu items to display.")
return false
}
if(status === 'off') {
if(feature.hasOwnProperty('install')) {
featureChoices.push({ name: 'Install ' + name, value: 'install' })
menuItemCount++
}
} else {
if(feature.hasOwnProperty('update')) {
featureChoices.push({ name: 'Update ' + name, value: 'update'})
}
if(feature.hasOwnProperty('uninstall')) {
featureChoices.push({ name: 'Uninstall ' + name, value: 'uninstall'})
}
}
if(feature.hasOwnProperty('menu')) {
feature.menu.forEach(menuItem => {
const menuItemName = typeof menuItem.name === 'function' ? menuItem.name() : menuItem.name
if(!menuItemName) {
return
}
const menuItemValue = typeof menuItem.value === 'function' ? menuItem.value() : menuItem.value
// todo: uninstall option will go here also
featureChoices.push({ name: menuItemName, value: menuItemValue })
})
}
if(featureChoices.length < 1) {
console.log("Nothing to do yet on this feature, please check back soon.")
return false
} }
let widest = 9 featureChoices.push(
const features = [ 'Back to Features'
`Tor ${status.run} connect AOs p2p`, )
`Bitcoin ${status.run} payments`, const answer = await inquirer.prompt({
`Lightning ${status.ins} payments`, name: 'feature_menu',
`nginx ${status.ins} host AO publicly over the world wide web`, type: 'list',
`SSL/Certbot ${status.ins} HTTPS for public web AO`, message: 'Please choose:',
`Jitsi ${status.err} secure video chat`, choices: featureChoices
`Signal ${status.off} notifications`, })
`File hosting ${status.err} file attachments on cards`, if(answer.feature_menu === 'Back to Features') {
`youtube-dl ${status.ins} cache web videos`, return false
`Borg ${status.ins} backup`, }
`Encryption ${status.err} serverside secret messages`, if(Object.keys(feature).includes(answer.feature_menu)) {
`Themes ${status.err} custom themes`, await feature[answer.feature_menu]()
`Glossary ${status.err} custom glossary`, return true
`Jubilee ${status.ena} monthly points creation event`, }
console.log('Not yet implemented')
return true
}
// Prints the AO Admin Menu and executes the user's choice
async function aoInstallMenu() {
console.log(`\n${headerStyle('Alchemy')}`)
const aoServerChoices = [
{ name: 'Install AO prerequisites', value: 'prereqs' },
'Check AO install',
'Update AO',
'Check AO service',
'Start/Stop AO service',
'Switch AO database',
'Switch AO version',
'Back to Main Menu' 'Back to Main Menu'
] ]
const answer = await inquirer.prompt({ const answer = await inquirer.prompt({
name: 'features_menu', name: 'install_menu',
type: 'list', type: 'list',
message: 'Please choose:', message: 'Please choose:',
choices: features, choices: aoServerChoices,
pageSize: features.length pageSize: aoServerChoices.length
}) })
switch(answer.features_menu) { switch(answer.install_menu) {
case 'Back to Main Menu': case 'prereqs':
return false installRequired()
default: break
console.log("Not yet implemented") case 'Check AO install':
case 'Update AO':
case 'Check AO service':
case 'Start/Stop AO service':
case 'Switch AO database':
case 'Switch AO version':
console.log("Not yet implemented.")
return true return true
default:
return false
} }
return true return true
} }
@ -368,7 +428,7 @@ async function handleArgs(args) {
switch (args[0]) { switch (args[0]) {
case '--version': case '--version':
case '-v': case '-v':
console.log(await getAoCliVersion()) console.log(await features['ao-cli'].version())
return false return false
} }
return true return true

3
package.json

@ -8,7 +8,8 @@
"scripts": { "scripts": {
"start": "node .", "start": "node .",
"version": "node . -v", "version": "node . -v",
"prettier": "npx prettier" "prettier": "npx prettier",
"debug": "node inspect ."
}, },
"keywords": [ "keywords": [
"AO", "AO",

67
scripts/api.js

@ -23,35 +23,32 @@ let currentSessionId = aoEnv('AO_CLI_SESSION_ID')
let currentSessionToken = aoEnv('AO_CLI_SESSION_TOKEN') let currentSessionToken = aoEnv('AO_CLI_SESSION_TOKEN')
// Performs a post request to the specified endpoint, sending the given payload // Performs a post request to the specified endpoint, sending the given payload
export function postRequest(endpoint, payload) { export async function postRequest(endpoint, payload) {
if (!currentSessionToken) { if (!currentSessionToken) {
console.log('Session token not set, API not ready.') console.log('Session token not set, API not ready.')
return new Promise(() => null) return new Promise(() => null)
} }
try { try {
let partialRequest = request if(payload) {
.post(HOSTNAME + endpoint) return await request
.set('authorization', currentSessionToken) .post(HOSTNAME + endpoint)
.set('session', currentSessionId) .send(payload)
.set('authorization', currentSessionToken)
if(payload) { .set('session', currentSessionId)
console.log('sending payload', payload) } else {
return partialRequest.send(payload).then(res => { return await request.post(HOSTNAME + endpoint)
//console.log("Response completed:", res) .set('authorization', currentSessionToken)
}).catch(err => { .set('session', currentSessionId)
//console.log('response failed:', err) }
})
} else {
return partialRequest
}
} catch (err) { } catch (err) {
console.log('request failed') console.log('request failed', err)
return null
} }
} }
// Performs a post request to the /event endpoint, sending the given JSON object as the event // Performs a post request to the /event endpoint, sending the given JSON object as the event
export function postEvent(event) { export async function postEvent(event) {
return postRequest('/event', event) return await postRequest('/event', event)
} }
// Attempts login with the given username and password combo. If successful, returns the generated session and token (login cookies). // Attempts login with the given username and password combo. If successful, returns the generated session and token (login cookies).
@ -191,17 +188,17 @@ export async function cacheMeme(taskId) {
// Cards feature // Cards feature
export async function getCard(taskId, alsoGetRelevant = 'subcards') { export async function getCard(taskId, alsoGetRelevant = 'subcards') {
taskId = taskId.trim().toLowerCase() taskId = taskId.trim().toLowerCase()
console.log('taskId to fetch is', taskId) let payload = { taskId: taskId }
const payload = { 'taskId': taskId }
const result = await postRequest('/fetchTaskByID', payload) // todo: change to flat text, not JSON const result = await postRequest('/fetchTaskByID', payload) // todo: change to flat text, not JSON
if(!result || !result.body) { if(!result || !result.body) {
console.log('Error fetching task.') console.log('Error fetching task.')
return null return null
} }
if(alsoGetRelevant) { if(alsoGetRelevant) {
let relevantResult = await getAllRelevantCards(result.body.taskId, alsoGetRelevant) let relevantCards = await getAllRelevantCards(result.body, alsoGetRelevant)
return [result.body, ...relevantCards]
} }
return result.body.concat(relevantResult.body) return [result.body]
} }
// Fetches all cards related to the given card object, i.e., cards that could be seen or navigated to immediately from that card // Fetches all cards related to the given card object, i.e., cards that could be seen or navigated to immediately from that card
@ -224,7 +221,7 @@ export async function getAllRelevantCards(
// Choose which taskIds we are going to request from the server // Choose which taskIds we are going to request from the server
switch (scope) { switch (scope) {
case 'priority': case 'priority':
taskIdsToFetch = new Set(seedTask.priorities.at(-1)) taskIdsToFetch = new Set([seedTask.priorities.at(-1)])
break break
case 'priorities': case 'priorities':
taskIdsToFetch = new Set(seedTask.priorities) taskIdsToFetch = new Set(seedTask.priorities)
@ -239,35 +236,31 @@ export async function getAllRelevantCards(
}) })
} }
} }
// Filter out the taskIds for tasks we already have // Filter out the taskIds for tasks we already have
taskIdsToFetch.filter(taskId => { taskIdsToFetch = [...taskIdsToFetch].filter(taskId => {
if (!taskId) { if (!taskId) {
return false return false
} }
const existingTask = existingTasks.get(taskId) const existingTask = existingTasks.get(taskId)
return !existingTask return !existingTask
}) })
if (taskIdsToFetch.length < 1) { if(taskIdsToFetch.length < 1) {
return [] return []
} }
// Fetch the cards // Fetch the cards
try { try {
const result = await postRequest('/fetchTaskByID', { taskId: taskIdsToFetch }) const result = await postRequest('/fetchTasks', { taskIds: taskIdsToFetch })
// Filter again (overlapping queries or intelligent server can cause duplicates to be returned)
const newTasksOnly = result.body.filter(
fetchedTask => !existingTasks.get(fetchedTask.taskId)
)
return newTasksOnly
} catch (error) { } catch (error) {
console.log('Error fetching relevant tasks:', { taskIdsToFetch, error }) console.log('Error fetching relevant tasks:', { taskIdsToFetch, error })
return null return null
} }
// Filter again (overlapping queries or intelligent server can cause duplicates to be returned)
const newTasksOnly = result.body.foundThisTaskList.filter(
fetchedTask =>
!existingTasks.some(
existingTask => existingTask.taskId === fetchedTask.taskId
)
)
return newTasksOnly
} }
export async function colorCard(taskId, color) { export async function colorCard(taskId, color) {

14
scripts/crypto.js

@ -27,18 +27,4 @@ export function decryptFromPrivate(priv, hiddenInfo) {
return crypto return crypto
.privateDecrypt(priv, Buffer.from(hiddenInfo, 'hex')) .privateDecrypt(priv, Buffer.from(hiddenInfo, 'hex'))
.toString('latin1') .toString('latin1')
}
//Old crypto.js file produces different hashes, need to fix before I'll be able to log into my accounts again
export function oldCreateHash(payload) {
let sha256 = crypto.createHash('sha256')
sha256.update(payload)
return sha256.digest('hex')
}
export function oldHmacHex(data, signingKey) {
let hmac = crypto.createHmac('sha256', signingKey)
hmac.update(data)
return hmac.digest('hex')
} }

83
scripts/features.js → scripts/features/ao-cli.js

@ -1,41 +1,40 @@
// Functions to add and remove AO features import { execSync } from 'child_process'
import { execSync, exec } from 'child_process'
import { AO_MANUAL_PATH } from './manual.js'
import { loadJsonFile } from './files.js'
import path from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import path from 'path'
import { loadJsonFile } from '../files.js'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
// It is possible to run ao-cli with npx @autonomousorganization/ao-cli. In this case, it can help you install it permanently. // Returns one of: off, installed, enabled, running, synced, error
export function installAoCli() { function cliStatus() {
try { try {
execSync('npm i -g @autonomousorganization/ao-cli 2>&1') const stdout = execSync('npm list -g @autonomousorganization/ao-cli')
console.log('Installed ao-cli.') const isAoCliInstalled = stdout.includes('@autonomousorganization/ao-cli@')
if(isAoCliInstalled) return 'installed'
} catch(err) { } catch(err) {
console.log('Error installing ao-cli:', err) return 'error'
} }
return 'off'
} }
// Adds a line to .bashrc to make 'ao' an alias for 'ao-cli', to simplify using the AO from the command line // It is possible to run ao-cli with npx @autonomousorganization/ao-cli. In this case, it can help you install it permanently.
export function installAoAlias() { function installAoCli() {
try { try {
execSync('grep "ao=\'ao-cli\'" ~/.bashrc') execSync('npm i -g @autonomousorganization/ao-cli 2>&1')
console.log('You can already type \'ao\' to launch ao-cli; the alias line already exists in ~/.bashrc.') console.log('Installed ao-cli.')
} catch(err) { } catch(err) {
execSync('echo alias ao=\'ao-cli\' >> .bashrc') console.log('Error installing ao-cli:', err)
console.log('Added alias line to ~/.bashrc. You can now type \'ao\' to launch ao-cli.')
} }
} }
export async function getAoCliVersion() { async function getAoCliVersion() {
const packageJson = await loadJsonFile(path.join(__dirname, '../package.json')) const packageJson = await loadJsonFile(path.join(__dirname, '../../package.json'))
return packageJson.version return packageJson.version
} }
// Updates the globally-installed version of this package, ao-cli, using npm // Updates the globally-installed version of this package, ao-cli, using npm
export async function selfUpdate() { async function selfUpdate() {
try { try {
const beforeVersionNumber = await getAoCliVersion() const beforeVersionNumber = await getAoCliVersion()
const result = execSync('npm update -g @autonomousorganization/ao-cli 2>&1') const result = execSync('npm update -g @autonomousorganization/ao-cli 2>&1')
@ -50,28 +49,36 @@ export async function selfUpdate() {
} }
} }
export async function updateManual() { // Returns true if the 'ao' alias for ao-cli has already been addded to .bashrc
exec('cd ' + process.env.HOME + '/.ao/manual && git pull origin main 2>&1', (error, stdout, stderr) => { function checkAoAlias() {
//console.log('error:', error, 'stdout:', stdout, 'stderr:', stderr) try {
if(error) { execSync('grep "ao=ao-cli" ~/.bashrc')
console.log('git pull failed with error:', error) } catch(err) {
} return 'off'
if(stdout.includes('Already up to date.')) { }
return return 'installed'
}
console.log('/nAO User Manual was updated.')
})
} }
// Downloads the ao-manual repo to ~/.ao/manual/ // Adds a line to .bashrc to make 'ao' an alias for 'ao-cli', to simplify using the AO from the command line
export function downloadManual() { function installAoAlias() {
try { try {
execSync('git clone http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-manual.git ' + AO_MANUAL_PATH + ' 2>&1') execSync('echo alias ao=ao-cli >> $HOME/.bashrc')
console.log('Added alias line to ~/.bashrc. You can now type \'ao\' to launch ao-cli.')
} catch(err) { } catch(err) {
switch(err.code) { console.log('Failed to add alias, sorry.')
case 128:
return false
}
} }
return true }
export default {
description: 'this AO command-line interface',
status: cliStatus,
install: installAoCli,
version: getAoCliVersion,
update: selfUpdate,
add_alias: installAoAlias,
remove_alias: () => console.log("Not implemented yet."),
menu: [
{ name: () => checkAoAlias() === 'installed' ? 'Remove \'ao\' shortcut for \'ao-cli\'' : 'Install \'ao\' shortcut for \'ao-cli\'',
value: () => checkAoAlias() === 'installed' ? 'remove_alias' : 'add_alias' }
]
} }

20
scripts/features/ao-server.js

@ -0,0 +1,20 @@
import { execSync } from 'child_process'
// Returns one of: off, installed, enabled, running, synced, error
function serviceStatus() {
try {
const stdout = execSync('systemctl status ao')
const isServiceRunning = stdout.includes('Active: active (running)')
if(isServiceRunning) return 'running'
else if(stdout.includes('error')) return 'error'
else if(stdout.includes('stopped')) return 'installed'
} catch(err) {
return 'error'
}
return 'off'
}
export default {
description: 'AO server instance on this computer',
status: serviceStatus
}

20
scripts/features/bitcoin.js

@ -0,0 +1,20 @@
import { execSync } from 'child_process'
// Returns one of: off, installed, enabled, running, synced, error
export function bitcoinStatus() {
try {
const stdout = execSync('source ~/Alchemy/ingredients/lead && source ~/Alchemy/ingredients/gold && bitcoin_is_synced')
const isSynced = stdout.includes('Bitcoin is synced!')
if(isSynced) return 'synced'
else if(stdout.includes('error')) return 'error'
} catch(err) {
return 'error'
}
return 'off'
}
export default {
name: 'Bitcoin',
description: 'payments',
status: bitcoinStatus
}

7
scripts/features/borg.js

@ -0,0 +1,7 @@
// BorgBackup module
export default {
name: 'Borg',
description: 'encrypted-in-transit, deduplicated incremental backup (over tor)',
status: () => 'off',
}

5
scripts/features/certbot.js

@ -0,0 +1,5 @@
export default {
name: 'SSL/Certbot',
description: 'HTTPS for public web AO',
status: () => 'Off',
}

5
scripts/features/encryption.js

@ -0,0 +1,5 @@
export default {
name: 'Encryption',
description: 'serverside secret messages', //encrypt messages to and from this computer',
status: () => 'off',
}

5
scripts/features/files.js

@ -0,0 +1,5 @@
export default {
name: 'File hosting',
description: 'file attachments on cards (sync p2p via tor with other AOs)',
status: () => 'off',
}

5
scripts/features/glossary.js

@ -0,0 +1,5 @@
export default {
name: 'Glossary',
description: 'custom glossary',
status: () => 'off',
}

18
scripts/features/index.js

@ -0,0 +1,18 @@
// Import the features modules in this folder, which each add, remove, and admininster one AO feature
export { default as 'ao-cli' } from './ao-cli.js'
export { default as 'ao-server' } from './ao-server.js'
export { default as bitcoin } from './bitcoin.js'
export { default as borg } from './borg.js'
export { default as certbot } from './certbot.js'
export { default as encryption } from './encryption.js'
export { default as files } from './files.js'
export { default as glossary } from './glossary.js'
export { default as jitsi } from './jitsi.js'
export { default as jubilee } from './jubilee.js'
export { default as lightning } from './lightning.js'
export { default as manual } from './manual.js'
export { default as nginx } from './nginx.js'
export { default as signal } from './signal.js'
export { default as themes } from './themes.js'
export { default as tor } from './tor.js'
export { default as 'youtube-dl' } from './youtube-dl.js'

5
scripts/features/jitsi.js

@ -0,0 +1,5 @@
export default {
name: 'Jitsi',
description: 'secure video chat',
status: () => 'off',
}

5
scripts/features/jubilee.js

@ -0,0 +1,5 @@
export default {
name: 'Jubilee',
description: 'monthly points creation event',
status: () => 'off',
}

23
scripts/features/lightning.js

@ -0,0 +1,23 @@
import { execSync } from 'child_process'
// Returns one of: off, installed, enabled, running, synced, error
export function lightningStatus() {
try {
const stdout = execSync('lightning-cli -V')
} catch(err) {
return 'off'
}
try {
const stdout = execSync('lightning-cli getinfo')
} catch(err) {
return 'installed'
}
return 'running'
}
export default {
description: 'payments',
status: lightningStatus
}

45
scripts/features/manual.js

@ -0,0 +1,45 @@
import { execSync, exec } from 'child_process'
import { lsFolder } from '../files.js'
import { AO_MANUAL_PATH } from '../manual.js'
export function manualStatus() {
// There are at least eighteen items in the manual
if(lsFolder(AO_MANUAL_PATH).length >= 18) {
return 'installed'
}
return 'off'
}
export async function updateManual() {
exec('cd ' + process.env.HOME + '/.ao/manual && git pull origin main 2>&1', (error, stdout, stderr) => {
//console.log('error:', error, 'stdout:', stdout, 'stderr:', stderr)
if(error) {
console.log('git pull failed with error:', error)
}
if(stdout.includes('Already up to date.')) {
return
}
console.log('/nAO User Manual was updated.')
})
}
// Downloads the ao-manual repo to ~/.ao/manual/. Returns false if it fails, which usually means the folder already exists (update instead).
export function downloadManual() {
try {
execSync('git clone http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-manual.git ' + AO_MANUAL_PATH + ' 2>&1')
} catch(err) {
switch(err.code) {
case 128:
return false
}
}
return true
}
export default {
name: 'Manual',
description: 'AO user manual',
status: manualStatus,
install: downloadManual,
update: updateManual
}

28
scripts/features/nginx.js

@ -0,0 +1,28 @@
import { execSync } from 'child_process'
// Returns one of: off, installed, enabled, running, synced, error
export function nginxStatus() {
try {
const stdout = execSync('nginx -v 2>&1')
} catch(err) {
return 'off'
}
try {
const stdout = execSync('systemctl status nginx')
if(stdout.includes('Active: active (running)')) {
return 'running'
} else if(stdout.includes('Active: inactive (dead)')) {
return 'installed'
}
} catch(err) {
return 'installed'
}
return 'installed'
}
export default {
description: 'host AO publicly over the world wide web',
status: nginxStatus
}

5
scripts/features/signal.js

@ -0,0 +1,5 @@
export default {
name: 'Signal',
description: 'secure notifications',
status: () => 'off',
}

5
scripts/features/themes.js

@ -0,0 +1,5 @@
export default {
name: 'Themes',
description: 'custom themes',
status: () => 'off',
}

20
scripts/features/tor.js

@ -0,0 +1,20 @@
import { execSync } from 'child_process'
// Returns one of: off, installed, enabled, running, synced, error
export function torStatus() {
try {
const stdout = execSync('systemctl status tor')
const isTorRunning = stdout.includes('Active: active (running)')
if(isTorRunning) return 'running'
else if(stdout.includes('error')) return 'error'
else if(stdout.includes('stopped')) return 'installed'
} catch(err) {
return 'error'
}
return 'off'
}
export default {
description: 'connect AOs p2p',
status: torStatus
}

4
scripts/features/youtube-dl.js

@ -0,0 +1,4 @@
export default {
description: 'cache web videos',
status: () => 'off',
}

1
scripts/files.js

@ -19,6 +19,7 @@ export async function loadJsonFile(path) {
const loadedText = await loadTextFile(path) const loadedText = await loadTextFile(path)
return JSON.parse(loadedText) return JSON.parse(loadedText)
} }
// Loads the given text file and returns a dictionary of its contents parsed into a .header dict and .tail markdown content // Loads the given text file and returns a dictionary of its contents parsed into a .header dict and .tail markdown content
export async function loadYamlMarkdownFile(path) { export async function loadYamlMarkdownFile(path) {
let text = await loadTextFile(path) let text = await loadTextFile(path)

2
scripts/manual.js

@ -25,8 +25,6 @@ function formatManualTitleString(title) {
return title.toTitleCase() return title.toTitleCase()
} }
marked.setOptions({ marked.setOptions({
renderer: new TerminalRenderer({ renderer: new TerminalRenderer({

3
scripts/priority.js

@ -9,7 +9,6 @@ export async function getTopPriorityText() {
return 'Not logged in' return 'Not logged in'
} }
const fetchedCards = await getCard(memberId, 'priority') const fetchedCards = await getCard(memberId, 'priority')
console.log('fetch result:', fetchedCards)
if(!fetchedCards || fetchedCards.length < 2) { if(!fetchedCards || fetchedCards.length < 2) {
return 'None' return 'None'
} }
@ -20,4 +19,4 @@ export async function getTopPriorityText() {
// Makes an API request to get the first prioritized card in the member card of the logged-in user // Makes an API request to get the first prioritized card in the member card of the logged-in user
async function getFirstPriorityCard() { async function getFirstPriorityCard() {
} }

14
scripts/session.js

@ -5,9 +5,10 @@ import { askQuestionText } from './welcome.js'
// Returns true if there is a session cookie for ao-cli saved in the AO .env file (=ready to make session requests) // Returns true if there is a session cookie for ao-cli saved in the AO .env file (=ready to make session requests)
export function isLoggedIn() { export function isLoggedIn() {
const username = aoEnv('AO_CLI_SESSION_USERNAME') const username = aoEnv('AO_CLI_SESSION_USERNAME')
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID')
const sessionId = aoEnv('AO_CLI_SESSION_ID') const sessionId = aoEnv('AO_CLI_SESSION_ID')
const sessionToken = aoEnv('AO_CLI_SESSION_TOKEN') const sessionToken = aoEnv('AO_CLI_SESSION_TOKEN')
return username && sessionId && sessionToken return username && memberId && sessionId && sessionToken
} }
// Interactive prompt to log in. Performs the login request. // Interactive prompt to log in. Performs the login request.
@ -46,17 +47,18 @@ export async function logout() {
try { try {
console.log('Logging out...') console.log('Logging out...')
const response = await apiLogout() const response = await apiLogout()
setAoEnv('AO_CLI_SESSION_USERNAME', null)
setAoEnv('AO_CLI_SESSION_MEMBERID', null)
setAoEnv('AO_CLI_SESSION_ID', null)
setAoEnv('AO_CLI_SESSION_TOKEN', null)
if(response.statusCode === 200) { if(response.statusCode === 200) {
setAoEnv('AO_CLI_SESSION_USERNAME', null)
setAoEnv('AO_CLI_SESSION_ID', null)
setAoEnv('AO_CLI_SESSION_TOKEN', null)
console.log('Logged out') console.log('Logged out')
} else { } else {
console.log('Logout failed. Response:', response) console.log('Server rejected logout. Forgetting session anyway. Response:', response)
return false return false
} }
} catch(err) { } catch(err) {
console.log(err) console.log(err)
} }
return true return true
} }

4
scripts/strings.js

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

Loading…
Cancel
Save