diff --git a/README.md b/README.md index 5321320..7ac3c30 100644 --- a/README.md +++ b/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 diff --git a/index.js b/index.js index 5f08fbc..5e86827 100644 --- a/index.js +++ b/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`) - 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 = [] 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', - 'Back to Main Menu' - ] +// 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: - return false + if(answer.features_menu === 'Back to Main Menu') { + 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 -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') + ' ' +// 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 } - 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`, + 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 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 diff --git a/package.json b/package.json index d2c0f57..c488415 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "scripts": { "start": "node .", "version": "node . -v", - "prettier": "npx prettier" + "prettier": "npx prettier", + "debug": "node inspect ." }, "keywords": [ "AO", diff --git a/scripts/api.js b/scripts/api.js index b80ea75..31b783a 100644 --- a/scripts/api.js +++ b/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 - .post(HOSTNAME + endpoint) - .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 - } + if(payload) { + return await request + .post(HOSTNAME + endpoint) + .send(payload) + .set('authorization', currentSessionToken) + .set('session', currentSessionId) + } else { + 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) @@ -239,35 +236,31 @@ export async function getAllRelevantCards( }) } } - + // Filter out the taskIds for tasks we already have - taskIdsToFetch.filter(taskId => { + taskIdsToFetch = [...taskIdsToFetch].filter(taskId => { if (!taskId) { return false } const existingTask = existingTasks.get(taskId) return !existingTask }) - if (taskIdsToFetch.length < 1) { + if(taskIdsToFetch.length < 1) { return [] } // 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) { diff --git a/scripts/crypto.js b/scripts/crypto.js index f93f86a..bcf85ca 100755 --- a/scripts/crypto.js +++ b/scripts/crypto.js @@ -27,18 +27,4 @@ export function decryptFromPrivate(priv, hiddenInfo) { return crypto .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') } \ No newline at end of file diff --git a/scripts/features.js b/scripts/features/ao-cli.js similarity index 52% rename from scripts/features.js rename to scripts/features/ao-cli.js index c9485dd..1625638 100644 --- a/scripts/features.js +++ b/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 - } - console.log('/nAO User Manual was updated.') - }) +// 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' + } + 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' } + ] } diff --git a/scripts/features/ao-server.js b/scripts/features/ao-server.js new file mode 100644 index 0000000..9136529 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/scripts/features/bitcoin.js b/scripts/features/bitcoin.js new file mode 100644 index 0000000..5e30f2b --- /dev/null +++ b/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 +} diff --git a/scripts/features/borg.js b/scripts/features/borg.js new file mode 100644 index 0000000..046d696 --- /dev/null +++ b/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', +} \ No newline at end of file diff --git a/scripts/features/certbot.js b/scripts/features/certbot.js new file mode 100644 index 0000000..03a402c --- /dev/null +++ b/scripts/features/certbot.js @@ -0,0 +1,5 @@ +export default { + name: 'SSL/Certbot', + description: 'HTTPS for public web AO', + status: () => 'Off', +} \ No newline at end of file diff --git a/scripts/features/encryption.js b/scripts/features/encryption.js new file mode 100644 index 0000000..7385e24 --- /dev/null +++ b/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', +} \ No newline at end of file diff --git a/scripts/features/files.js b/scripts/features/files.js new file mode 100644 index 0000000..3417f87 --- /dev/null +++ b/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', +} \ No newline at end of file diff --git a/scripts/features/glossary.js b/scripts/features/glossary.js new file mode 100644 index 0000000..73807d2 --- /dev/null +++ b/scripts/features/glossary.js @@ -0,0 +1,5 @@ +export default { + name: 'Glossary', + description: 'custom glossary', + status: () => 'off', +} diff --git a/scripts/features/index.js b/scripts/features/index.js new file mode 100644 index 0000000..580c23f --- /dev/null +++ b/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' diff --git a/scripts/features/jitsi.js b/scripts/features/jitsi.js new file mode 100644 index 0000000..9845d3e --- /dev/null +++ b/scripts/features/jitsi.js @@ -0,0 +1,5 @@ +export default { + name: 'Jitsi', + description: 'secure video chat', + status: () => 'off', +} diff --git a/scripts/features/jubilee.js b/scripts/features/jubilee.js new file mode 100644 index 0000000..53feed2 --- /dev/null +++ b/scripts/features/jubilee.js @@ -0,0 +1,5 @@ +export default { + name: 'Jubilee', + description: 'monthly points creation event', + status: () => 'off', +} diff --git a/scripts/features/lightning.js b/scripts/features/lightning.js new file mode 100644 index 0000000..de7f423 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/scripts/features/manual.js b/scripts/features/manual.js new file mode 100644 index 0000000..d7d2cd3 --- /dev/null +++ b/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 +} diff --git a/scripts/features/nginx.js b/scripts/features/nginx.js new file mode 100644 index 0000000..5e7232b --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/scripts/features/signal.js b/scripts/features/signal.js new file mode 100644 index 0000000..530dcb8 --- /dev/null +++ b/scripts/features/signal.js @@ -0,0 +1,5 @@ +export default { + name: 'Signal', + description: 'secure notifications', + status: () => 'off', +} diff --git a/scripts/features/themes.js b/scripts/features/themes.js new file mode 100644 index 0000000..e9e5500 --- /dev/null +++ b/scripts/features/themes.js @@ -0,0 +1,5 @@ +export default { + name: 'Themes', + description: 'custom themes', + status: () => 'off', +} diff --git a/scripts/features/tor.js b/scripts/features/tor.js new file mode 100644 index 0000000..744e4be --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/scripts/features/youtube-dl.js b/scripts/features/youtube-dl.js new file mode 100644 index 0000000..4056235 --- /dev/null +++ b/scripts/features/youtube-dl.js @@ -0,0 +1,4 @@ +export default { + description: 'cache web videos', + status: () => 'off', +} diff --git a/scripts/files.js b/scripts/files.js index 26badad..2615cf2 100644 --- a/scripts/files.js +++ b/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) diff --git a/scripts/manual.js b/scripts/manual.js index 25e1912..78c271e 100644 --- a/scripts/manual.js +++ b/scripts/manual.js @@ -25,8 +25,6 @@ function formatManualTitleString(title) { return title.toTitleCase() } - - marked.setOptions({ renderer: new TerminalRenderer({ diff --git a/scripts/priority.js b/scripts/priority.js index 86f0a1a..ddad035 100644 --- a/scripts/priority.js +++ b/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' } @@ -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 async function getFirstPriorityCard() { -} \ No newline at end of file +} diff --git a/scripts/session.js b/scripts/session.js index 52583f2..ca3cf0b 100644 --- a/scripts/session.js +++ b/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,17 +47,18 @@ export async function logout() { try { console.log('Logging out...') 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) { - setAoEnv('AO_CLI_SESSION_USERNAME', null) - setAoEnv('AO_CLI_SESSION_ID', null) - setAoEnv('AO_CLI_SESSION_TOKEN', null) 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) { console.log(err) } return true -} \ No newline at end of file +} diff --git a/scripts/strings.js b/scripts/strings.js index be5dd02..319f759 100644 --- a/scripts/strings.js +++ b/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) } @@ -33,4 +33,4 @@ export const centerLines = (str) => { const lines = str.split('\n') const centered = lines.map(line => line.centerInLine(line.length, process.stdout.columns)) return centered.join('\n') -} \ No newline at end of file +}