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