Browse Source

modularized install and uninstall of features, cleaned up menus

main
deicidus 3 years ago
parent
commit
52a52c632a
  1. 7
      README.md
  2. 230
      index.js
  3. 3
      package.json
  4. 53
      scripts/api.js
  5. 14
      scripts/crypto.js
  6. 79
      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. 1
      scripts/priority.js
  27. 8
      scripts/session.js
  28. 2
      scripts/strings.js

7
README.md

@ -10,7 +10,7 @@ To install:
`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
@ -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)
* `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
* Easily view installed/running status of optional AO features
* Add `ao` alias for `ao-cli` (under Features→ao-cli)
## Upcoming Features
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
* Easily install/uninstall and turn on/off optional AO features
* Operate essential AO client features (like creating and sending cards p2p via tor)
* 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 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 install/uninstall and turn on/off option AO features
* 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)
* 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
* 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.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

230
index.js

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

3
package.json

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

53
scripts/api.js

@ -23,35 +23,32 @@ let currentSessionId = aoEnv('AO_CLI_SESSION_ID')
let currentSessionToken = aoEnv('AO_CLI_SESSION_TOKEN')
// 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) {
console.log('Session token not set, API not ready.')
return new Promise(() => null)
}
try {
let partialRequest = request
if(payload) {
return await request
.post(HOSTNAME + endpoint)
.send(payload)
.set('authorization', currentSessionToken)
.set('session', currentSessionId)
if(payload) {
console.log('sending payload', payload)
return partialRequest.send(payload).then(res => {
//console.log("Response completed:", res)
}).catch(err => {
//console.log('response failed:', err)
})
} else {
return partialRequest
return await request.post(HOSTNAME + endpoint)
.set('authorization', currentSessionToken)
.set('session', currentSessionId)
}
} 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
export function postEvent(event) {
return postRequest('/event', event)
export async function postEvent(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).
@ -191,17 +188,17 @@ export async function cacheMeme(taskId) {
// Cards feature
export async function getCard(taskId, alsoGetRelevant = 'subcards') {
taskId = taskId.trim().toLowerCase()
console.log('taskId to fetch is', taskId)
const payload = { 'taskId': taskId }
let payload = { taskId: taskId }
const result = await postRequest('/fetchTaskByID', payload) // todo: change to flat text, not JSON
if(!result || !result.body) {
console.log('Error fetching task.')
return null
}
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
@ -224,7 +221,7 @@ export async function getAllRelevantCards(
// Choose which taskIds we are going to request from the server
switch (scope) {
case 'priority':
taskIdsToFetch = new Set(seedTask.priorities.at(-1))
taskIdsToFetch = new Set([seedTask.priorities.at(-1)])
break
case 'priorities':
taskIdsToFetch = new Set(seedTask.priorities)
@ -241,7 +238,7 @@ export async function getAllRelevantCards(
}
// Filter out the taskIds for tasks we already have
taskIdsToFetch.filter(taskId => {
taskIdsToFetch = [...taskIdsToFetch].filter(taskId => {
if (!taskId) {
return false
}
@ -254,20 +251,16 @@ export async function getAllRelevantCards(
// Fetch the cards
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) {
console.log('Error fetching relevant tasks:', { taskIdsToFetch, error })
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) {

14
scripts/crypto.js

@ -28,17 +28,3 @@ export function decryptFromPrivate(priv, hiddenInfo) {
.privateDecrypt(priv, Buffer.from(hiddenInfo, 'hex'))
.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')
}

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

@ -1,41 +1,40 @@
// Functions to add and remove AO features
import { execSync, exec } from 'child_process'
import { AO_MANUAL_PATH } from './manual.js'
import { loadJsonFile } from './files.js'
import path from 'path'
import { execSync } from 'child_process'
import { fileURLToPath } from 'url'
import path from 'path'
import { loadJsonFile } from '../files.js'
const __filename = fileURLToPath(import.meta.url)
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.
export function installAoCli() {
// Returns one of: off, installed, enabled, running, synced, error
function cliStatus() {
try {
execSync('npm i -g @autonomousorganization/ao-cli 2>&1')
console.log('Installed ao-cli.')
const stdout = execSync('npm list -g @autonomousorganization/ao-cli')
const isAoCliInstalled = stdout.includes('@autonomousorganization/ao-cli@')
if(isAoCliInstalled) return 'installed'
} 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
export function installAoAlias() {
// It is possible to run ao-cli with npx @autonomousorganization/ao-cli. In this case, it can help you install it permanently.
function installAoCli() {
try {
execSync('grep "ao=\'ao-cli\'" ~/.bashrc')
console.log('You can already type \'ao\' to launch ao-cli; the alias line already exists in ~/.bashrc.')
execSync('npm i -g @autonomousorganization/ao-cli 2>&1')
console.log('Installed ao-cli.')
} catch(err) {
execSync('echo alias ao=\'ao-cli\' >> .bashrc')
console.log('Added alias line to ~/.bashrc. You can now type \'ao\' to launch ao-cli.')
console.log('Error installing ao-cli:', err)
}
}
export async function getAoCliVersion() {
const packageJson = await loadJsonFile(path.join(__dirname, '../package.json'))
async function getAoCliVersion() {
const packageJson = await loadJsonFile(path.join(__dirname, '../../package.json'))
return packageJson.version
}
// Updates the globally-installed version of this package, ao-cli, using npm
export async function selfUpdate() {
async function selfUpdate() {
try {
const beforeVersionNumber = await getAoCliVersion()
const result = execSync('npm update -g @autonomousorganization/ao-cli 2>&1')
@ -50,28 +49,36 @@ export async function selfUpdate() {
}
}
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
// Returns true if the 'ao' alias for ao-cli has already been addded to .bashrc
function checkAoAlias() {
try {
execSync('grep "ao=ao-cli" ~/.bashrc')
} catch(err) {
return 'off'
}
console.log('/nAO User Manual was updated.')
})
return 'installed'
}
// Downloads the ao-manual repo to ~/.ao/manual/
export function downloadManual() {
// Adds a line to .bashrc to make 'ao' an alias for 'ao-cli', to simplify using the AO from the command line
function installAoAlias() {
try {
execSync('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) {
switch(err.code) {
case 128:
return false
console.log('Failed to add alias, sorry.')
}
}
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)
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
export async function loadYamlMarkdownFile(path) {
let text = await loadTextFile(path)

2
scripts/manual.js

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

1
scripts/priority.js

@ -9,7 +9,6 @@ export async function getTopPriorityText() {
return 'Not logged in'
}
const fetchedCards = await getCard(memberId, 'priority')
console.log('fetch result:', fetchedCards)
if(!fetchedCards || fetchedCards.length < 2) {
return 'None'
}

8
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)
export function isLoggedIn() {
const username = aoEnv('AO_CLI_SESSION_USERNAME')
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID')
const sessionId = aoEnv('AO_CLI_SESSION_ID')
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.
@ -46,13 +47,14 @@ export async function logout() {
try {
console.log('Logging out...')
const response = await apiLogout()
if(response.statusCode === 200) {
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) {
console.log('Logged out')
} else {
console.log('Logout failed. Response:', response)
console.log('Server rejected logout. Forgetting session anyway. Response:', response)
return false
}
} catch(err) {

2
scripts/strings.js

@ -1,4 +1,5 @@
// Extends the String type and other string helper functions
import wrapAnsi from 'wrap-ansi'
// Adds a .toTitleCase() function to every string
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
import wrapAnsi from 'wrap-ansi'
String.prototype.wordWrap = function (width = 80) {
return wrapAnsi(this, width)
}

Loading…
Cancel
Save