An interactive command-line interface (CLI) tool to help you install, use, and administer an AO instance.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

228 lines
9.2 KiB

// 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 inquirer from 'inquirer'
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'
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) {
console.log(`\n${headerStyle('Configure AO Features')}`)
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 = { name: nameColumn + statusColumn + descriptionColumn, value: featureKey, short: featureKey }
return choice
})
featuresChoices.push(new inquirer.Separator())
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 = { name: nameColumn + statusColumn + descriptionColumn, value: 'service_' + serviceName, short: serviceName }
featuresChoices.push(choice)
})
featuresChoices.push(
{ name: '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.')
let answer
try {
answer = await inquirer.prompt({
name: 'features_menu',
type: 'list',
message: 'Please choose:',
choices: featuresChoices,
default: previousMenuChoice,
pageSize: featuresChoices.length
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
}
}
if(answer.features_menu.includes('service_')) {
const serviceName = answer.features_menu.replace(/^service_/, '')
const 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.features_menu) {
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.features_menu]
const chosenFeatureName = (chosenFeature.hasOwnProperty('name') && chosenFeature.name.length >= 1) ? chosenFeature.name : answer.features_menu
while(await oneFeatureMenu(chosenFeatureName, chosenFeature)) {}
return answer.features_menu
}
// 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({ name: 'Stop ' + name, value: () => feature.stop() })
} else if(installed && !running && typeof feature.start === 'function') {
featureChoices.push({ name: 'Start ' + name, value: () => feature.start() })
}
if(status === 'off') {
if(typeof feature.install === 'function') {
featureChoices.push({ name: 'Install ' + name, value: () => feature.install() })
}
} else {
if(typeof feature.update === 'function') {
featureChoices.push({ name: 'Update ' + name, value: () => feature.update() })
}
if(typeof feature.uninstall === 'function') {
featureChoices.push({ name: 'Uninstall ' + name, value: () => feature.uninstall() })
}
}
if(isCustom) {
featureChoices.push({ name: '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({ name: 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'
)
let answer
try {
answer = await inquirer.prompt({
name: 'feature_menu',
type: 'list',
message: 'Please choose:',
choices: featureChoices
})
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return false
} else {
console.log('Unknown error displaying Features menu:', error)
}
}
if(answer.feature_menu === 'Back to Features') {
return false
}
if(typeof answer.feature_menu === 'function') {
answer.feature_menu()
return true
} else if(Object.keys(feature).includes(answer.feature_menu)) {
await feature[answer.feature_menu]()
return true
}
console.log('Not yet implemented')
return true
}