deicidus
2 years ago
35 changed files with 213 additions and 903 deletions
@ -1,43 +0,0 @@ |
|||||||
import { execSync } from 'child_process' |
|
||||||
import { lsFolder } from '../files.js'
|
|
||||||
export const ALCHEMY_FOLDER = process.env.HOME + '/Alchemy' |
|
||||||
|
|
||||||
function statusAlchemy() { |
|
||||||
return lsFolder(ALCHEMY_FOLDER).length >= 6 ? 'installed' : 'off' |
|
||||||
} |
|
||||||
|
|
||||||
function downloadAlchemy() { |
|
||||||
console.log('Beacon of Zen') |
|
||||||
} |
|
||||||
|
|
||||||
function updateAlchemy() { |
|
||||||
try { |
|
||||||
const result = execSync('cd ~/Alchemy && git pull') |
|
||||||
if(result.toString().includes('Already up to date.')) { |
|
||||||
console.log('Alchemy is already up to date.') |
|
||||||
} else { |
|
||||||
console.log('Alchemy updated.') |
|
||||||
return true |
|
||||||
} |
|
||||||
} catch(error) { |
|
||||||
console.log('Failed to update Alchemy scripts: ', error) |
|
||||||
} |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
function onMyCustomMenuItem() { |
|
||||||
console.log("Not implemented.") |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'Alchemy', |
|
||||||
description: 'scripts that transmute your system into gold', |
|
||||||
status: statusAlchemy, |
|
||||||
install: downloadAlchemy, |
|
||||||
update: updateAlchemy, |
|
||||||
// These menu items will show up oin Features->Alchemy. The key/(menu:value:) is arbitrary but must be the same in both places.
|
|
||||||
custom_script_1: onMyCustomMenuItem, |
|
||||||
menu: [ |
|
||||||
{ name: 'Menu item to trigger a very specific Alchemy script', value: 'custom_script_1' }
|
|
||||||
] |
|
||||||
} |
|
@ -1,140 +0,0 @@ |
|||||||
import { execSync } from 'child_process' |
|
||||||
import { fileURLToPath } from 'url' |
|
||||||
import path from 'path' |
|
||||||
import fs from 'fs' |
|
||||||
import { loadJsonFile } from '../files.js' |
|
||||||
|
|
||||||
// Can't include .json files without adding an experimental node flag, but we can use this workaround to use require, which works, instead
|
|
||||||
import { createRequire } from "module"; // Bring in the ability to create the 'require' method
|
|
||||||
const require = createRequire(import.meta.url); // construct the require method
|
|
||||||
const packageVersion = require("../../package.json").version |
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url) |
|
||||||
const __dirname = path.dirname(__filename) |
|
||||||
|
|
||||||
// Returns one of: off, installed, enabled, running, synced, error
|
|
||||||
function cliStatus() { |
|
||||||
try { |
|
||||||
const stdout = execSync('npm list -g @autonomousorganization/ao-cli') |
|
||||||
const isAoCliInstalled = stdout.includes('@autonomousorganization/ao-cli@') |
|
||||||
if(isAoCliInstalled) return 'installed' |
|
||||||
} catch(err) { |
|
||||||
return 'error' |
|
||||||
} |
|
||||||
return 'off' |
|
||||||
} |
|
||||||
|
|
||||||
// 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('npm i -g @autonomousorganization/ao-cli 2>&1') |
|
||||||
console.log('Installed ao-cli.') |
|
||||||
} catch(err) { |
|
||||||
console.log('Error installing ao-cli:', err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function getAoCliVersion() { |
|
||||||
const npmGlobalPackagesPath = execSync('npm root -g').toString().replace(/\n/, '') |
|
||||||
const aoGlobalPackagePath = path.join(npmGlobalPackagesPath, '@autonomousorganization/ao-cli/package.json') |
|
||||||
let jsonFileContents |
|
||||||
try { |
|
||||||
const contents = fs.readFileSync(aoGlobalPackagePath) |
|
||||||
jsonFileContents = JSON.parse(contents) |
|
||||||
} catch(err) { |
|
||||||
if(err.code === 'ENOENT') { |
|
||||||
console.log('The global ao-cli package.json file does not exist.') |
|
||||||
} else { |
|
||||||
console.log('Unknown error loading global ao-cli package.json file, aborting.', err) |
|
||||||
} |
|
||||||
return null |
|
||||||
} |
|
||||||
if(!jsonFileContents.hasOwnProperty('version')) { |
|
||||||
return null |
|
||||||
} |
|
||||||
return jsonFileContents.version |
|
||||||
} |
|
||||||
|
|
||||||
// Updates the globally-installed version of this package, ao-cli, using npm
|
|
||||||
async function selfUpdate() { |
|
||||||
try { |
|
||||||
const beforeVersionNumber = await getAoCliVersion() |
|
||||||
const result = execSync('npm update -g @autonomousorganization/ao-cli 2>&1') |
|
||||||
const afterVersionNumber = await getAoCliVersion() |
|
||||||
if(beforeVersionNumber === afterVersionNumber) { |
|
||||||
console.log("ao-cli version is already current.") |
|
||||||
} else { |
|
||||||
console.log('\nao-cli self-updated automatically from version', beforeVersionNumber, 'to version', afterVersionNumber, 'from the official npm repository.') |
|
||||||
} |
|
||||||
} catch (err) { |
|
||||||
console.log('Failed to update ao-cli: ', err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 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' |
|
||||||
} |
|
||||||
|
|
||||||
// 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('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) { |
|
||||||
console.log('Failed to add alias, sorry.') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
// Returns true if the cd hook function has already been addded to .bashrc
|
|
||||||
function checkCdHook() { |
|
||||||
try { |
|
||||||
execSync('grep "function cd \{ builtin cd.*--interstitial.* ; \}$" ~/\.bashrc') |
|
||||||
} catch(err) { |
|
||||||
return 'off' |
|
||||||
} |
|
||||||
return 'installed' |
|
||||||
} |
|
||||||
|
|
||||||
// Adds a line to .bashrc to make 'ao' an alias for 'ao-cli', to simplify using the AO from the command line
|
|
||||||
function installCdHook() { |
|
||||||
try { |
|
||||||
execSync('echo \'function cd { builtin cd "\$@" && node \~/ao-cli \-\-interstitial "\$\@" ; }\' >> $HOME/.bashrc') |
|
||||||
console.log('Added alias line to ~/.bashrc. Try typing \'cd\' in a terminal.') |
|
||||||
} catch(err) { |
|
||||||
console.log('Failed to add alias, sorry.') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function removeCdHook() { |
|
||||||
try { |
|
||||||
let result = execSync('sed -i \'/^function cd.*/d\' ' + process.env.HOME + '/.bashrc') |
|
||||||
console.log('sed:', result.toString()) |
|
||||||
} catch(error) { |
|
||||||
console.log(error.output.toString()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
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."), |
|
||||||
add_cd: installCdHook, |
|
||||||
remove_cd: removeCdHook, |
|
||||||
menu: [ |
|
||||||
{ name: () => checkAoAlias() === 'installed' ? 'Remove \'ao\' shortcut for \'ao-cli\'' : 'Install \'ao\' shortcut for \'ao-cli\'', |
|
||||||
value: () => checkAoAlias() === 'installed' ? 'remove_alias' : 'add_alias' }, |
|
||||||
{ name: () => checkCdHook() === 'installed' ? 'Remove \'cd\' fantasy hook' : 'Install \'cd\' fantasy hook', |
|
||||||
value: () => checkCdHook() === 'installed' ? 'remove_cd' : 'add_cd' }, |
|
||||||
] |
|
||||||
} |
|
@ -1,96 +0,0 @@ |
|||||||
import { execSync, exec } from 'child_process' |
|
||||||
import { isFolder } from '../files.js' |
|
||||||
import path from 'path' |
|
||||||
import { unlink } from 'node:fs' |
|
||||||
import SystemServiceManager from '../services.js' |
|
||||||
|
|
||||||
const AO_SERVER_PATH = path.join(process.env.HOME, 'ao-server') |
|
||||||
|
|
||||||
const aoServerServiceTemplate = |
|
||||||
`[Unit]
|
|
||||||
Description=ao-server daemon |
|
||||||
|
|
||||||
[Service] |
|
||||||
WorkingDirectory=${process.env.HOME}/ao-server |
|
||||||
ExecStart=npm run build && node --loader ts-node/esm ${process.env.HOME}/ao-server/src/server/app.ts |
|
||||||
User=${process.env.USER} |
|
||||||
Type=simple |
|
||||||
Restart=on-failure |
|
||||||
PrivateTmp=true |
|
||||||
|
|
||||||
[Install] |
|
||||||
WantedBy=multi-user.target` |
|
||||||
|
|
||||||
const aoServerServiceManager = new SystemServiceManager('ao-server', aoServerServiceTemplate) |
|
||||||
|
|
||||||
// Returns one of: off, installed, enabled, running, synced, error
|
|
||||||
function serviceStatus() { |
|
||||||
if(!aoIsInstalled()) { |
|
||||||
return 'off' |
|
||||||
} |
|
||||||
if(aoServerServiceManager.isInstalled()) { |
|
||||||
const isRunning = aoServerServiceManager.isRunning() |
|
||||||
switch(isRunning) { |
|
||||||
case true: |
|
||||||
return 'running' |
|
||||||
case 'error': |
|
||||||
return 'error' |
|
||||||
} |
|
||||||
return 'enabled' |
|
||||||
} else { |
|
||||||
return 'installed' |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Downloads ao-server to ~/ao-server. Returns false if it fails, which usually means the folder already exists (update instead).
|
|
||||||
export function downloadAoServer() { |
|
||||||
try { |
|
||||||
execSync('git clone http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-server.git 2>&1') |
|
||||||
} catch(err) { |
|
||||||
switch(err.code) { |
|
||||||
case 128: |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
// Return true if the ~/ao-server/.git folder exists
|
|
||||||
export function aoIsInstalled() { |
|
||||||
return isFolder(path.join(AO_SERVER_PATH, '.git')) |
|
||||||
} |
|
||||||
|
|
||||||
function installAo(version) { |
|
||||||
if(!version) { |
|
||||||
version = aoEnv('AO_VERSION') |
|
||||||
if(!version) { |
|
||||||
version = 'ao-svelte' |
|
||||||
setAoEnv('AO_VERSION', 'ao-svelte') |
|
||||||
console.log('No AO server/frontend version specified, defaulting to ao-svelte.') |
|
||||||
} |
|
||||||
} |
|
||||||
downloadAoServer() |
|
||||||
} |
|
||||||
|
|
||||||
export function updateAoServer() { |
|
||||||
console.log('Updating ao-server. If you are a developer, you may be asked for your SSH key now.') |
|
||||||
try { |
|
||||||
const stdout = execSync('cd ' + AO_SERVER_PATH + ' && git pull origin main 2>&1') |
|
||||||
if(stdout.includes('Already up to date.')) { |
|
||||||
console.log('Already up to date.') |
|
||||||
return |
|
||||||
} |
|
||||||
console.log('\nao-server was updated.') |
|
||||||
} catch(error) { |
|
||||||
console.log('git pull failed with error:', error) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
description: 'AO server instance on this computer', |
|
||||||
status: serviceStatus, |
|
||||||
install: installAo, |
|
||||||
isInstalled: aoIsInstalled, |
|
||||||
update: updateAoServer, |
|
||||||
submodules: [ aoServerServiceManager ] |
|
||||||
} |
|
@ -1,20 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
@ -1,7 +0,0 @@ |
|||||||
// BorgBackup module
|
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'Borg', |
|
||||||
description: 'encrypted-in-transit, deduplicated incremental backup (over tor)', |
|
||||||
status: () => null, |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export default { |
|
||||||
name: 'SSL/Certbot', |
|
||||||
description: 'HTTPS for public web AO', |
|
||||||
status: () => null, |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export default { |
|
||||||
name: 'Encryption', |
|
||||||
description: 'serverside secret messages', //encrypt messages to and from this computer',
|
|
||||||
status: () => null, |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export default { |
|
||||||
name: 'File hosting', |
|
||||||
description: 'file attachments on cards (sync p2p via tor with other AOs)', |
|
||||||
status: () => null, |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export default { |
|
||||||
name: 'Glossary', |
|
||||||
description: 'custom glossary', |
|
||||||
status: () => null, |
|
||||||
} |
|
@ -1,197 +0,0 @@ |
|||||||
// Re-export the features modules in this folder, which each add, remove, and admininster one AO feature (imported in project index.js)
|
|
||||||
// Also contains the Features menus to control these features in this directory
|
|
||||||
import chalk from 'chalk' |
|
||||||
import fs from 'fs' |
|
||||||
import { lsFolder } from '../files.js' |
|
||||||
import { fileURLToPath } from 'url' |
|
||||||
import path from 'path' |
|
||||||
import { headerStyle, greenChalk, styledStatus } from '../styles.js' |
|
||||||
import { spinner } from '../console.js' |
|
||||||
import SystemServiceManager, { getCustomServicesList, addCustomServiceInteractive, removeCustomService } from '../services.js' |
|
||||||
import { promptMenu } from '../welcome.js' |
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url) |
|
||||||
const __dirname = path.dirname(__filename) |
|
||||||
|
|
||||||
const loadFeatures = async () => { |
|
||||||
|
|
||||||
let filenames = lsFolder(path.join(__dirname)) |
|
||||||
let features = {} |
|
||||||
for(let i = 0; i < filenames.length; i++) { |
|
||||||
const filename = filenames[i] |
|
||||||
if(filename === 'index.js') continue |
|
||||||
const moduleShortname = filename.replace(/\.js$/, '') |
|
||||||
const path = './' + filename |
|
||||||
features[moduleShortname] = (await import(path)).default |
|
||||||
} |
|
||||||
return features |
|
||||||
} |
|
||||||
|
|
||||||
const features = await loadFeatures() |
|
||||||
export default features |
|
||||||
|
|
||||||
// Prints the Configure AO Features menu and executes the user's choice
|
|
||||||
let featuresChoices |
|
||||||
export async function featuresMenu(previousMenuChoice = 0) { |
|
||||||
const stopSpinner = spinner('Loading status...') |
|
||||||
let loadedFeatures = 0 |
|
||||||
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 status = feature.status() || 'Unknown' |
|
||||||
if(status !== 'Unknown') { |
|
||||||
loadedFeatures++ |
|
||||||
} |
|
||||||
const statusColumn = styledStatus(status, 25) |
|
||||||
const descriptionColumn = feature.description || '' |
|
||||||
const choice = { title: nameColumn + statusColumn + descriptionColumn, value: featureKey, short: featureKey } |
|
||||||
return choice |
|
||||||
}) |
|
||||||
featuresChoices.push({ title: '---', disabled: true }) |
|
||||||
const customServices = getCustomServicesList() |
|
||||||
customServices.forEach(serviceName => { |
|
||||||
const nameColumn = serviceName.padEnd(17) |
|
||||||
const service = new SystemServiceManager(serviceName) |
|
||||||
const status = service.status() || 'Unknown' |
|
||||||
const statusColumn = styledStatus(status, 25) |
|
||||||
const descriptionColumn = service.description || '' |
|
||||||
const choice = { title: nameColumn + statusColumn + descriptionColumn, value: 'service_' + serviceName, short: serviceName } |
|
||||||
featuresChoices.push(choice) |
|
||||||
}) |
|
||||||
featuresChoices.push( |
|
||||||
{ title: 'Add custom service', value: 'add_service' }, |
|
||||||
'Back to Main Menu' |
|
||||||
) |
|
||||||
} else { |
|
||||||
loadedFeatures = featuresChoices.filter(feature => { |
|
||||||
return typeof feature === 'object' && feature.hasOwnProperty('name') && !feature.name.includes('Unknown') |
|
||||||
}).length |
|
||||||
} |
|
||||||
stopSpinner('Loaded status for ' + loadedFeatures + '/' + (featuresChoices.length - 1) + ' features.') |
|
||||||
const answer = await promptMenu(featuresChoices, 'Configure AO Features', undefined, previousMenuChoice) |
|
||||||
if(answer.includes('service_')) { |
|
||||||
const serviceName = answer.features_menu.replace(/^service_/, '') |
|
||||||
const service = new SystemServiceManager(serviceName) |
|
||||||
const nameWithoutDot = serviceName.split('.')[0] + ' service' |
|
||||||
console.log('calling service menu with true') |
|
||||||
await oneFeatureMenu(nameWithoutDot, service, true) |
|
||||||
return answer.features_menu |
|
||||||
} |
|
||||||
switch(answer) { |
|
||||||
case 'add_service': |
|
||||||
console.log('Many Linux distributions run system services in the background. You can add an existing systemctl service to the AO Features menu to make it easier to start and stop your services. You must know the name of the service and it must already exist.') |
|
||||||
while(await addCustomServiceInteractive()) {} |
|
||||||
featuresChoices = null |
|
||||||
return true |
|
||||||
case 'Back to Main Menu': |
|
||||||
return false |
|
||||||
} |
|
||||||
const chosenFeature = features[answer] |
|
||||||
const chosenFeatureName = (chosenFeature.hasOwnProperty('name') && chosenFeature.name.length >= 1) ? chosenFeature.name : answer |
|
||||||
while(await oneFeatureMenu(chosenFeatureName, chosenFeature)) {} |
|
||||||
return answer |
|
||||||
} |
|
||||||
|
|
||||||
// Prints the menu options for a specific feature (including subfeatures)
|
|
||||||
function oneFeatureMenuChoices(name, feature, status, isCustom = false) { |
|
||||||
if(!status && status !== false) { |
|
||||||
status = typeof feature.status === 'function' ? feature.status() : feature.hasOwnProperty('status') ? feature.status : 'off' |
|
||||||
} |
|
||||||
let featureChoices = [] |
|
||||||
if(!status) { |
|
||||||
console.log("This AO subfeature module lacks a status() function, not sure which menu items to display.") |
|
||||||
return null |
|
||||||
} |
|
||||||
|
|
||||||
const installed = typeof feature.isInstall === 'function' ? feature.isInstalled : feature.hasOwnProperty('isInstalled') ? feature.isInstalled : status !== 'off' |
|
||||||
const running = installed && typeof feature.isRunning === 'function' ? feature.isRunning() : feature.hasOwnProperty('isRunning') ? feature.isRunning : false |
|
||||||
if(running && typeof feature.stop === 'function') { |
|
||||||
featureChoices.push({ title: 'Stop ' + name, value: feature.stop }) |
|
||||||
} else if(installed && !running && typeof feature.start === 'function') { |
|
||||||
featureChoices.push({ title: 'Start ' + name, value: feature.start }) |
|
||||||
} |
|
||||||
|
|
||||||
if(status === 'off') { |
|
||||||
if(typeof feature.install === 'function') { |
|
||||||
featureChoices.push({ title: 'Install ' + name, value: feature.install }) |
|
||||||
} |
|
||||||
} else { |
|
||||||
if(typeof feature.update === 'function') { |
|
||||||
featureChoices.push({ title: 'Update ' + name, value: feature.update }) |
|
||||||
} |
|
||||||
if(typeof feature.uninstall === 'function') { |
|
||||||
featureChoices.push({ title: 'Uninstall ' + name, value: feature.uninstall }) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if(isCustom) { |
|
||||||
featureChoices.push({ title: 'Remove from list', value: () => { |
|
||||||
const nameWithoutLabel = name.replace(/ service/, '') |
|
||||||
removeCustomService(nameWithoutLabel) |
|
||||||
featuresChoices = null |
|
||||||
}
|
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
if(feature.hasOwnProperty('menu')) { |
|
||||||
feature.menu.forEach(menuItem => { |
|
||||||
const menuItemName = typeof menuItem.name === 'function' ? menuItem.name() : menuItem.name |
|
||||||
if(menuItemName) { |
|
||||||
featureChoices.push({ title: menuItemName, value: menuItem.Value }) |
|
||||||
} |
|
||||||
// todo: uninstall option will go here also
|
|
||||||
}) |
|
||||||
} |
|
||||||
return featureChoices |
|
||||||
} |
|
||||||
|
|
||||||
// Prints the feature menu for a specific feature
|
|
||||||
// Each feature module can export functions for status, install, version, update, displayed 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'
|
|
||||||
// Each feature can also have subfeatures/featuare submodules. These are simply features not listed in the main AO Features list
|
|
||||||
// Instead, their (usually shorter list of) menu items are listed flattened alongside the feature's menu items (no submenus)
|
|
||||||
// This has better usability than submenus, giving users more contexual cues at the same time about where they are in the menus
|
|
||||||
export async function oneFeatureMenu(name, feature, isCustom = false) { |
|
||||||
console.log(`\n${headerStyle(name)}`) |
|
||||||
if(feature.description && feature.description?.length >= 1) { |
|
||||||
console.log('\n' + feature.description + '\n') |
|
||||||
} |
|
||||||
let featureChoices = [] |
|
||||||
const stopSpinner = spinner('Loading status...') |
|
||||||
const status = feature?.status() || false |
|
||||||
featureChoices = oneFeatureMenuChoices(name, feature, status, isCustom) |
|
||||||
if(feature.submodules && Array.isArray(feature.submodules)) { |
|
||||||
feature.submodules.forEach(subfeature => { |
|
||||||
const submoduleChoices = oneFeatureMenuChoices(subfeature.name, subfeature, undefined, isCustom) |
|
||||||
if(submoduleChoices && submoduleChoices.length >= 1) { |
|
||||||
featureChoices = featureChoices.concat(submoduleChoices) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
stopSpinner('Loaded ' + name + ' status: ' + (feature.status() || 'Unknown') + '\n') |
|
||||||
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 promptMenu(featureChoices, name) |
|
||||||
if(answer === 'Back to Features') { |
|
||||||
return false |
|
||||||
} |
|
||||||
if(typeof answer === 'function') { |
|
||||||
await answer() |
|
||||||
return true |
|
||||||
} else if(Object.keys(feature).includes(answer)) { |
|
||||||
await feature[answer]() |
|
||||||
return true |
|
||||||
} |
|
||||||
console.log('Not yet implemented') |
|
||||||
return true |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export default { |
|
||||||
name: 'Jitsi', |
|
||||||
description: 'secure video chat', |
|
||||||
status: () => null, |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export default { |
|
||||||
name: 'Jubilee', |
|
||||||
description: 'monthly points creation event', |
|
||||||
status: () => null, |
|
||||||
} |
|
@ -1,23 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
@ -1,184 +0,0 @@ |
|||||||
// Functions for downloading, updating, and displaying the AO Manual, a hierarchy of markdown files
|
|
||||||
import chalk from 'chalk' |
|
||||||
import { execSync, exec } from 'child_process' |
|
||||||
import { loadYamlMarkdownFile, lsFolder, isFolder } from '../files.js' |
|
||||||
import { repeatString, centerLines } from '../strings.js' |
|
||||||
import { promptMenu } from '../welcome.js' |
|
||||||
import { headerStyle, manualTitleStyle } from '../styles.js' |
|
||||||
import { basename } from 'path' |
|
||||||
import { marked } from 'marked' |
|
||||||
import TerminalRenderer from 'marked-terminal' |
|
||||||
|
|
||||||
export const AO_MANUAL_PATH = process.env.HOME + '/.ao/manual' |
|
||||||
|
|
||||||
export function manualStatus() { |
|
||||||
// There are at least eighteen items in the manual
|
|
||||||
if(lsFolder(AO_MANUAL_PATH).length >= 18) { |
|
||||||
return 'installed' |
|
||||||
} |
|
||||||
return 'off' |
|
||||||
} |
|
||||||
|
|
||||||
// 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 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.') |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Removes numbered prefix such as 12_ and .md suffix, replaces underscores with spaces, and adds titlecase
|
|
||||||
function formatManualTitleString(title) { |
|
||||||
// Remove .md suffix
|
|
||||||
if(/\.md$/.exec(title)) { |
|
||||||
title = title.substring(0, title.length - 3) |
|
||||||
} |
|
||||||
// Remove numbered prefix e.g., 12_
|
|
||||||
if(/^\d*_/.exec(title)) { |
|
||||||
title = title.split('_').slice(1).join('_') |
|
||||||
} |
|
||||||
// Replace underscores with spaces
|
|
||||||
title = title.replaceAll('_', ' ') |
|
||||||
|
|
||||||
return title.toTitleCase()
|
|
||||||
} |
|
||||||
|
|
||||||
marked.setOptions({ |
|
||||||
renderer: new TerminalRenderer({ |
|
||||||
showSectionPrefix: false, |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
// Given a path and file/folder name, it returns the appropriate manual title
|
|
||||||
// If it's a folder and there is an index.js inside that has a title: field, that overrides the title
|
|
||||||
// Otherwise it's the filename or foldername, minus anything before the first underscore (_), in titlecase
|
|
||||||
async function loadManualTitle(path, fileOrFolder) { |
|
||||||
// If it's a .md file, check inside for a title: field
|
|
||||||
if(/\.md$/.exec(fileOrFolder)) { |
|
||||||
const indexTitle = (await loadYamlMarkdownFile(path + fileOrFolder))?.meta?.title |
|
||||||
if(indexTitle) { |
|
||||||
return indexTitle |
|
||||||
}
|
|
||||||
} |
|
||||||
|
|
||||||
// If it's a folder, check for a title: field in index.md and return if exists
|
|
||||||
if(isFolder(path + fileOrFolder)) { |
|
||||||
const indexPath = path + fileOrFolder + '/index.md' |
|
||||||
const indexTitle = (await loadYamlMarkdownFile(indexPath))?.meta?.title |
|
||||||
if(indexTitle) { |
|
||||||
return indexTitle |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Fall back to using the file/folder name as the title
|
|
||||||
return formatManualTitleString(fileOrFolder) |
|
||||||
} |
|
||||||
|
|
||||||
// Prints the specified manual page to the screen
|
|
||||||
export async function printManualPage(path, injectedTitle = '') { |
|
||||||
if(isFolder(path)) { |
|
||||||
path += '/index.md' |
|
||||||
} |
|
||||||
const dict = await loadYamlMarkdownFile(path) |
|
||||||
const title = injectedTitle || dict?.meta?.title || formatManualTitleString(basename(path)) |
|
||||||
const formattedTitle = manualTitleStyle(title).centerInLine(title.length).centerInConsole() |
|
||||||
console.log('\n' + formattedTitle + '\n') |
|
||||||
const renderedMarkdown = marked(dict?.tail).wordWrap().centerInConsole() |
|
||||||
console.log(renderedMarkdown) |
|
||||||
} |
|
||||||
|
|
||||||
// Render the manual folder or a subfolder as a menu
|
|
||||||
// First the index.js is listed using the folder name or title loaded from inside the file as the menu item title
|
|
||||||
// Next, any other files not starting with a number are loaded and displayed in discovered/arbitrary order
|
|
||||||
// Next, items starting with 0_, then 1_, and so on are displayed in order. You can mix files and folders.
|
|
||||||
// Selecting a menu item renders it. For .md files it renders it and shows the same level Manual menu again.
|
|
||||||
// For folders, it goes into that folder and renders it as a manual menu folder
|
|
||||||
// This allows arbitrarily nested Manual menu folders to be explored in a standardized menu system
|
|
||||||
export async function manualFolderAsMenu(path, menuTitle, backOption, previousMenuChoice = 0) { |
|
||||||
if(!isFolder(path)) { |
|
||||||
return false |
|
||||||
} |
|
||||||
if(path[path.length - 1] != '/') { |
|
||||||
path += '/' |
|
||||||
} |
|
||||||
|
|
||||||
let menuItems = [] |
|
||||||
|
|
||||||
const folderContents = lsFolder(path) |
|
||||||
if(folderContents.some(fileOrFolder => fileOrFolder === 'index.md')) { |
|
||||||
const indexTitle = await loadManualTitle(path, 'index.md') |
|
||||||
|
|
||||||
let indexMenuItem = {} |
|
||||||
indexMenuItem[indexTitle] = 'index.md' |
|
||||||
menuItems.push(indexMenuItem) |
|
||||||
} |
|
||||||
|
|
||||||
let unNumberedItems = [] |
|
||||||
let numberedItems = [] |
|
||||||
const sortedFolderContents = folderContents.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) |
|
||||||
for(let i = 0; i < sortedFolderContents.length; i++) { |
|
||||||
const fileOrFolder = sortedFolderContents[i] |
|
||||||
if(fileOrFolder === 'index.md') { |
|
||||||
continue |
|
||||||
} |
|
||||||
const potentialNumber = fileOrFolder.split('_')[0] |
|
||||||
const initialNumber = parseInt(potentialNumber) |
|
||||||
const title = await loadManualTitle(path, fileOrFolder) |
|
||||||
const menuItem = {} |
|
||||||
menuItem[title] = fileOrFolder |
|
||||||
|
|
||||||
if(isNaN(initialNumber)) { |
|
||||||
unNumberedItems.push(menuItem) |
|
||||||
} else { |
|
||||||
numberedItems.push(menuItem) |
|
||||||
} |
|
||||||
} |
|
||||||
menuItems = menuItems.concat(unNumberedItems, numberedItems) |
|
||||||
|
|
||||||
const menuChoices = menuItems.map(menuItem => Object.keys(menuItem)[0]) |
|
||||||
menuChoices.push(backOption) |
|
||||||
if(previousMenuChoice >= menuChoices.length) { |
|
||||||
previousMenuChoice = 0 |
|
||||||
} |
|
||||||
const answer = await promptMenu(menuChoices, menuTitle, 'choose a topic', previousMenuChoice, undefined, true) |
|
||||||
const chosenMenuIndex = menuChoices.indexOf(answer) |
|
||||||
if(answer === backOption) { |
|
||||||
return false |
|
||||||
} |
|
||||||
const chosenPath = path + Object.values(menuItems.find(menuItem => Object.keys(menuItem)[0] === answer))[0] |
|
||||||
await printManualPage(chosenPath, answer) |
|
||||||
const newBackOption = backOption === 'Back to Main Menu' ? 'Back to Table of Contents' : 'Back to ' + menuTitle |
|
||||||
let previousChoice = 0 |
|
||||||
do { |
|
||||||
previousChoice = await manualFolderAsMenu(chosenPath, answer, newBackOption, previousChoice + 1) |
|
||||||
} |
|
||||||
while(previousChoice !== false) |
|
||||||
return chosenMenuIndex |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'Manual', |
|
||||||
description: 'AO user manual', |
|
||||||
status: manualStatus, |
|
||||||
install: downloadManual, |
|
||||||
isInstalled: () => manualStatus() === 'installed', |
|
||||||
update: updateManual |
|
||||||
} |
|
@ -1,28 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export default { |
|
||||||
name: 'Signal', |
|
||||||
description: 'secure notifications', |
|
||||||
status: () => null, |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
export default { |
|
||||||
name: 'Themes', |
|
||||||
description: 'custom themes', |
|
||||||
status: () => null, |
|
||||||
} |
|
@ -1,25 +0,0 @@ |
|||||||
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 function isInstalled() { |
|
||||||
return torStatus() === 'running' |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
description: 'connect AOs p2p', |
|
||||||
status: torStatus, |
|
||||||
isInstalled: isInstalled |
|
||||||
} |
|
@ -1,4 +0,0 @@ |
|||||||
export default { |
|
||||||
description: 'cache web videos', |
|
||||||
status: () => null, |
|
||||||
} |
|
Loading…
Reference in new issue