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
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 |
|
}
|
|
|