Browse Source

added feature submodules and custom services feature

main
deicidus 2 years ago
parent
commit
f07c1734a9
  1. 23
      README.md
  2. 20
      index.js
  3. 2
      scripts/features/ao-cli.js
  4. 47
      scripts/features/ao-server.js
  5. 130
      scripts/features/index.js
  6. 38
      scripts/forest.js
  7. 225
      scripts/services.js
  8. 4
      scripts/settings.js
  9. 18
      scripts/system.js
  10. 14
      scripts/welcome.js
  11. 14
      scripts/wizard.js

23
README.md

@ -21,33 +21,34 @@ Then you can run with `ao-cli`.
These features work right now:
* Browse the [AO User Manual](https://git.coalitionofinvisiblecolleges.org/autonomousorganization/ao-manual) and automatically download and keep it updated
* Manages your AO configuration (.env) file for you
* Interactive install wizard installs the AO for you
* Wraps the functionality of (some of) Zen's Alchemy suite of scripts (system configuration, AO installation)
* Operate essential AO client features (view, create, and organize priorities)
* Easily view installed/running status of optional AO features (soon all features)
* Easily install/uninstall and turn on/off optional AO features
* Manages your AO configuration (.env) file for you
* `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)
* Detects your OS, with support for Debian/Ubuntu, Arch/Manjaro, and Fedora (MacOS planned)
* Wraps the functionality of (some of) Zen's Alchemy suite of scripts (system configuration, AO installation)
* Add `ao` alias for `ao-cli` (under Features→ao-cli)
* Enchant your 'cd' command to narrate your travels through the UNIX filesystem (under Features→ao-cli) (less annoying than it sounds, easy to disable)
* Easily install/uninstall and turn on/off optional AO features
* Easily monitor your AO server status and start/stop the service
* Easily add your existing systemctl services the Features list so you can start and stop them from the AO Features menu
## 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
* Operate essential AO client features (view and create priorities, create and send cards p2p via tor)
* Easily install and configure your AO server installation
* Join the AO .onion bootstrapping network and find public AO chatrooms p2p over tor
* 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 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
* Terminal spellbook to save and trade your favorite UNIX commands
* AO server using AO features via ao-cli command line switches (with optional sound notifications on server computer)
* More unit tests, optional feature modules, and AO client features
* More unit tests, optional feature modules, and AO client features, unit tests for each feature
## Important Locations
@ -63,9 +64,9 @@ These features are planned and many are mocked up in the menus:
## Version History
* 0.1.4 Added fantasy hook feature to bring the AO MUD aesthetic into the terminal
* 0.1.2 AO install wizard partway done, reorganized project repos, 'Check AO install' feature works
* 0.1.2 AO install wizard partway done, reorganized project repos, 'Check AO install' feature
* 0.1.0 View, create or recall, upboat and downboat priorities; partial AO install wizard
* 0.0.9 Features menu loaded from module file for each feature; view top priority; menu cleanup
* 0.0.9 Features menu loaded from module file for each feature; view top priority
* 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

20
index.js

@ -17,7 +17,7 @@ import { sleep, randomInt } from './scripts/util.js'
import './scripts/strings.js'
// Import AO modular features
import { featuresMenu } from './scripts/features/index.js'
import features, { featuresMenu } from './scripts/features/index.js'
import manual, { printManualPage, manualFolderAsMenu, AO_MANUAL_PATH } from './scripts/features/manual.js'
import aoCli from './scripts/features/ao-cli.js'
@ -93,6 +93,7 @@ async function adminMenu() {
console.log(`\n${headerStyle('System Alchemy')}`)
const adminChoices = [
{ name: 'Check AO install', value: 'check_AO' },
'Update everything',
'Update system software',
//'Update AO',
//{ name: 'Install AO prerequisites', value: 'prereqs' }, // move to feature module? calls installRequired()
@ -127,6 +128,21 @@ async function adminMenu() {
case 'check_AO':
await checkAo()
break
case 'Update everything':
// todo: go through the stuff in wizard and system and files and see if it can be moved to feature modules so it doesn't have to go here as an exceptional upgrade case
const featureEntries = Object.entries(features)
for(let i = 0; i < featureEntries.length; i++) {
const feature = featureEntries[i][1]
console.log('feature name is', feature.name)
const status = feature.status()
if((feature.hasOwnProperty('isInstalled') && feature.isInstalled()) || (['installed', 'enabled', 'running', 'synced'].includes(status))) {
console.log('Updating' + feature.name + '...')
if(feature.hasOwnProperty('upgrade')) {
await feature.upgrade()
}
}
}
break
case 'Update system software':
updateSoftware()
break
@ -163,7 +179,7 @@ async function handleArgs(args) {
case '-cd':
// Don't say a message every time or it will annoy everybody
if(randomInt(0, 6) === 0) {
console.log(wander(args[1]))
console.log(await wander(args[1]))
}
return false
}

2
scripts/features/ao-cli.js

@ -34,7 +34,7 @@ function installAoCli() {
}
async function getAoCliVersion() {
return packageVersion
return execSync('ao-cli --version').toString().replace(/\n$/, '')
}
// Updates the globally-installed version of this package, ao-cli, using npm

47
scripts/features/ao-server.js

@ -1,29 +1,45 @@
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'
}
let stdout
try {
stdout = execSync('systemctl status ao')
} catch(err) {
stdout = err.output.toString()
}
if(stdout.includes('Unit ao.service could not be found.')) {
if(aoServerServiceManager.isInstalled()) {
const isRunning = aoServerServiceManager.isRunning()
switch(isRunning) {
case true:
return 'running'
case 'error':
return 'error'
}
return 'enabled'
} else {
return 'installed'
}
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'
else if(stdout.includes('inactive (dead')) return 'installed'
return 'off'
}
// Downloads ao-server to ~/ao-server. Returns false if it fails, which usually means the folder already exists (update instead).
@ -57,9 +73,11 @@ function installAo(version) {
}
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.')
@ -73,5 +91,6 @@ export default {
status: serviceStatus,
install: installAo,
isInstalled: aoIsInstalled,
update: updateAoServer
update: updateAoServer,
submodules: [ aoServerServiceManager ]
}

130
scripts/features/index.js

@ -8,6 +8,7 @@ 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)
@ -48,15 +49,27 @@ export async function featuresMenu(previousMenuChoice = 0) {
}
const statusColumn = styledStatus(status, 25)
const descriptionColumn = feature.description || ''
const choice = { name: nameColumn + statusColumn + descriptionColumn, value: featureKey, short: featureKey}
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.name.includes('Unknown')
return typeof feature === 'object' && feature.hasOwnProperty('name') && !feature.name.includes('Unknown')
}).length
}
stopSpinner('Loaded status for ' + loadedFeatures + '/' + (featuresChoices.length - 1) + ' features.')
@ -76,8 +89,21 @@ export async function featuresMenu(previousMenuChoice = 0) {
return false
}
}
if(answer.features_menu === 'Back to Main Menu') {
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'
await oneFeatureMenu(nameWithoutDot, service, undefined, 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
@ -85,47 +111,82 @@ export async function featuresMenu(previousMenuChoice = 0) {
return answer.features_menu
}
// 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'
export async function oneFeatureMenu(name, feature) {
console.log(`\n${headerStyle(name)}`)
if(feature.description && feature.description?.length >= 1) {
console.log('\n' + feature.description + '\n')
// 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'
}
const featureChoices = []
const stopSpinner = spinner('Loading status...')
const status = feature?.status() || false
stopSpinner('Loaded ' + name + ' status: ' + (feature.status() || 'Unknown') + '\n')
let featureChoices = []
if(!status) {
console.log("This AO feature module lacks a status() function, not sure which menu items to display.")
return false
console.log("This AO subfeature module lacks a status() function, not sure which menu items to display.")
return null
}
const running = 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(!running && typeof feature.start === 'function') {
featureChoices.push({ name: 'Start ' + name, value: () => feature.start() })
}
if(status === 'off') {
if(feature.hasOwnProperty('install')) {
featureChoices.push({ name: 'Install ' + name, value: 'install' })
menuItemCount++
if(typeof feature.install === 'function') {
featureChoices.push({ name: 'Install ' + name, value: () => feature.install() })
}
} else {
if(feature.hasOwnProperty('update')) {
featureChoices.push({ name: 'Update ' + name, value: 'update'})
}
if(feature.hasOwnProperty('uninstall')) {
featureChoices.push({ name: 'Uninstall ' + name, value: 'uninstall'})
}
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: () => {
removeCustomService(name)
featuresChoices = null
}
})
}
if(feature.hasOwnProperty('menu')) {
feature.menu.forEach(menuItem => {
const menuItemName = typeof menuItem.name === 'function' ? menuItem.name() : menuItem.name
if(!menuItemName) {
return
if(menuItemName) {
featureChoices.push({ name: menuItemName, value: menuItem.Value })
}
const menuItemValue = typeof menuItem.value === 'function' ? menuItem.value() : menuItem.value
// todo: uninstall option will go here also
featureChoices.push({ name: menuItemName, value: menuItemValue })
})
}
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) {
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)
if(feature.submodules && Array.isArray(feature.submodules)) {
feature.submodules.forEach(subfeature => {
const submoduleChoices = oneFeatureMenuChoices(subfeature.name, subfeature)
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
@ -145,12 +206,17 @@ export async function oneFeatureMenu(name, feature) {
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(Object.keys(feature).includes(answer.feature_menu)) {
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
}

38
scripts/forest.js

@ -1,5 +1,7 @@
// Greeting text functions that can be hooked into your cd function so that moving between folders becomes an experience of the AO
import { selectRandom } from './util.js'
import { selectRandom, randomInt } from './util.js'
import { isLoggedIn } from './session.js'
import { getTopPriorityText } from './priority.js'
const stepMessages = [
'You find a door and walk through.',
@ -11,6 +13,10 @@ const walkMessages = [
'You take a winding path near a river.',
'A few meadows away is the place you are looking for.',
'A deer leads you a few galleries away to where you were going.',
'You thank the path for taking you to your destination.',
'You make the journey.',
'After some time, you arrive.',
'Winding your way through the trees, you find your way there.',
]
const wanderMessages = {
up: [
@ -65,10 +71,24 @@ const wanderMessages = {
'You perform a basic transit spell to take you to your destination.',
'Turning your mind inside out, you transmigrate zones.',
'You bilocate and, when the confusion clears, there you are.',
'Wandering for a way, you stop in a glade for lunch. Alighting, you walk onwards, and soon you arrive.',
'It\'s a long journey, but you make it there in one piece.',
'You\'re used to traveling anywhere in these woods very quickly.',
'Donning your seven-league boots, you make it there in a single bound.'
]
}
const priorityMessages = [
'You suddenly recall your top priority, to %s.',
'"Oh! I almost forgot!" you think to yourself, "I need to %s!"',
'You are suddenly reminded of your top priority, to %s.',
'For some reason, the scenery reminds you of your top priority, to %s.',
'You feel an urgent need to accomplish your next goal, to %s.',
'From deep within a surge of motivation replenishes you. You remember your task, to %s.',
'You draw upon your inner well of stillness and remember the task at hand, to %s.',
'This brings you one step closer to accomplishing your goal, to %s.'
]
export default function wander(direction) {
export default async function wander(direction) {
direction = !!direction ? direction.trim() : 'home'
switch(direction) {
case '..':
@ -96,5 +116,15 @@ export default function wander(direction) {
direction = Math.min(parseInt(totalSlashes), 5)
}
}
return selectRandom(wanderMessages[direction])
}
let narration = selectRandom(wanderMessages[direction])
// Remind them of their top priority sometimes
if(randomInt(0, 4) === 4 && isLoggedIn()) {
const priority = await getTopPriorityText()
if(priority !== 'Not logged in.') {
const madLibs = selectRandom(priorityMessages).replace(/%s/, priority)
narration += ' ' + madLibs
}
}
return narration
}

225
scripts/services.js

@ -0,0 +1,225 @@
// Tools for managing system services with systemctl (and eventually other options)
import { execSync } from 'child_process'
import path from 'path'
import fs from 'fs'
import { aoEnv, setAoEnv } from './settings.js'
import { isFile } from './files.js'
import { askQuestionText } from './welcome.js'
const SERVICE_FOLDER_PATH = '/etc/systemd/system'
// Manages a single systemctl service. serviceName is required. Implements the same functions as an AO feature module.
// serviceFileText is optional but if omitted, install(), uninstall(), and isInstalledToSpec() will not exist
// The only thing in this world that needs to be 'managed' is systemctl
export default class SystemServiceManager {
constructor(serviceName, serviceFileText = false) {
this.name = serviceName + '.service'
this.serviceFileText = serviceFileText
this.assertInitialized()
if(this.serviceFileText && this.serviceFileText.length >= 1) {
this.install = this.willBeInstall
this.uninstall = this.willBeUninstall
this.isInstalledToSpec = this.willBeIsInstalledToSpec
}
}
// Throws an exception if the service name has not been set
assertInitialized() {
if(!this.name || this.name.length < ('.service'.length + 1)) {
throw new Error('SystemServiceManager: Service name must be a string at least one character long.')
}
}
// Throws an exception if the serviceFileText has not been set
assertServiceFileText() {
if(!this.serviceFileText || this.serviceFileText.length < 1) {
return false
//throw new Error('SystemServiceManager: Attempt to use a function that requires serviceFileText, but serviceFileText is blank.')
}
return true
}
// Returns the full path of the service file, based on the service name
servicePath() {
this.assertInitialized()
return path.join(SERVICE_FOLDER_PATH, this.name)
}
status(verbose = true) {
if(this.isInstalled(verbose)) {
if(this.isRunning(verbose)) {
return 'running'
}
return 'installed'
}
console.log('its off')
return 'off'
}
// Returns true if the service file exists (does not check if it matches serviceFileText)
isInstalled(verbose = true) {
this.assertInitialized()
let error
try {
const stdout = execSync('systemctl cat -- ' + this.name + ' 2>&1')
if(stdout.length >= 1) {
return true
}
} catch(error) {
if(error.output.toString().includes('No files found for')) {
return false
}
}
if(verbose) console.log('Unknown status, assuming not installed' + (error ? error.output.toString() : '.'))
return false
}
// Return true if the file at the service file path matches the serviceFileText
// This function and isInstalled use different methods of getting this text but it should be identical, if not we will notice someday
willBeIsInstalledToSpec(verbose = true) {
console.log('isInstalled()')
this.assertInitialized()
this.assertServiceFileText()
const serviceFileText = fs.readFileSync(this.servicePath())
if(serviceFileText === this.serviceFileText) {
return true
} else {
if(verbose) console.log('Failed to write service file, sorry.')
return false
}
}
// Creates or overwrites the service file
willBeInstall(force = false, verbose = true) {
console.log('Creating service file. You may be asked for you sudo password.')
this.assertInitialized()
this.assertServiceFileText()
try {
const tempPath = path.join(process.env.HOME, '.ao/generatedservice.temp')
fs.writeFileSync(tempPath, this.serviceFileText, force ? { flag: "wx" } : undefined)
execSync('sudo mv ' + tempPath + ' ' + this.servicePath())
execSync('sudo systemctl daemon-reload')
return true
} catch(error) {
if(verbose) console.log('Service file already exists, or other error:', error)
}
// Clean up the temp file
try {
if(isFile(tempPath)) execSync('sudo rm ' + tempPath + ' 2>&1')
} catch(error) {} // If it failed, the temp file probably didn't exist
return false
}
// Deletes the service file
willBeUninstall(verbose = true) {
this.assertInitialized()
console.log('Deleting service file. You may be asked for you sudo password.')
try {
execSync('sudo rm ' + this.servicePath())
if(verbose) console.log("Deleted service file.")
execSync('sudo systemctl daemon-reload')
return true
} catch(error) {
if(verbose) console.log('Failed to delete service file:', error)
}
return false
}
// Returns true if the service is currently running without errors
isRunning(verbose = true) {
this.assertInitialized()
try {
const stdout = execSync('systemctl status -- ' + this.name)
if(stdout.includes('active (running)')) {
return true
}
} catch(error) {
error = error.output.toString()
if(error.includes(' could not be found.')) {
if(verbose) console.log('Warning: Checking whether a nonexistent service ' + this.name, 'is running. Check whether it is installed first.')
return false
} else if(error.includes('inactive (dead)')) {
return false
} else if(error.includes('failed')) {
return false
} else if(error.includes('stopped')) {
return false
} else {
if(verbose) console.log('Unknown error checking ' + this.name + ' running status:', error)
}
}
return false
}
// Attempts to start the service. Asks for sudo.
start(verbose = true) {
this.assertInitialized()
try {
const stdout = execSync('sudo systemctl start -- ' + this.name)
// todo: check for errors including wrong sudo password here
return true
} catch(error) {
if(verbose) console.log('Error starting service:', error)
}
return false
}
// Attempts to stop the service. Asks for sudo.
stop(verbose = true) {
this.assertInitialized()
try {
const stdout = execSync('sudo systemctl stop -- ' + this.name)
// todo: check for errors including wrong sudo password here
return true
} catch(error) {
if(verbose) console.log('Error stopping service:', error)
}
}
}
// Returns the saved list of custom services the user has saved in ao-cli's feature management interface
export function getCustomServicesList() {
let servicesList = aoEnv('AO_CLI_CUSTOM_SERVICES')
if(!servicesList) {
return []
}
return servicesList.split(',')
}
// Adds the given service name to the custom list of services saved for ao-cli in .env, and returns a new SystemServiceManager to control it
// Adding custom services is only for starting and stopping them, not for adding a service file (could add this but would need to make a UI)
function addCustomService(serviceName) {
const customServices = getCustomServicesList()
customServices.push(serviceName)
const serialized = customServices.join(',')
setAoEnv('AO_CLI_CUSTOM_SERVICES', serialized)
return new SystemServiceManager(serviceName)
}
export function removeCustomService(serviceName) {
let customServices = getCustomServicesList()
customServices = customServices.filter(service => service !== serviceName)
const serialized = customServices.join(',')
setAoEnv('AO_CLI_CUSTOM_SERVICES', serialized)
}
// Asks the user for a new service name and save it
export async function addCustomServiceInteractive() {
let serviceName = await askQuestionText('What is the name of the service?')
if(!serviceName || serviceName.length < 1) {
console.log('Nothing entered, going back.')
return false
}
serviceName = serviceName.toLowerCase()
if(getCustomServicesList().includes(serviceName)) {
console.log('You have already add that service to the list.')
return true
}
const service = new SystemServiceManager(serviceName)
if(!service.isInstalled()) {
console.log('That service does not exist, sorry.')
return true
}
addCustomService(serviceName)
return false
}

4
scripts/settings.js

@ -1,7 +1,7 @@
import { execSync } from 'child_process'
import fs from 'fs'
import fs from 'fs'
import { parse, stringify } from 'envfile'
export const AO_ENV_FILE_PATH = process.env.HOME + '/.ao/.env'
function createAoFolderIfDoesNotExist() {

18
scripts/system.js

@ -71,7 +71,6 @@ export function updateSoftware() {
break
case 'arch':
execSync('sudo pacman -Syu --noconfirm')
// for Manjaro, also do pamac upgrade -a && pamac update --aur --devel to do normal then build all AUR packages (check https://forum.manjaro.org/c/announcements/11 first)
break
case 'fedora':
execSync('sudo dnf update -yqqq 2>/dev/null && && sudo dnf autoremove -yqqq && sudo dnf upgrade -yqqq')
@ -179,3 +178,20 @@ function installIfNotInstalled(packageNameOrNames, verbose = true, group = false
})
return { installed: packagesInstalled, failed: packagesFailed }
}
// Returns true if npm install (npm i) has been run in the given path, otherwise false. Also returns false if the folder does not exist.
// Note that npm packages are totally different from (normal) packages installed the OS's bundled package manager. They are node packages.
export async function isNpmPackageInstalled(path, verbose = false) {
try {
const stdout = execSync('cd ' + folderPath + ' && npm list 2>&1')
return true
} catch(error) {
const missingLocalPackagesCount = error.output.toString().match(/npm ERR! missing:/g).length || 0
if(missingLocalPackagesCount) {
if(verbose) console.log(path, 'has', missingLocalPackagesCount, 'npm packages left to install.')
} else {
if(verbose) console.log('Error while checking if npm package at', path, 'is installed:', error)
}
}
return false
}

14
scripts/welcome.js

@ -117,6 +117,16 @@ export async function askQuestionText(prompt = 'Please enter a string:', promptO
message: prompt
}
Object.assign(options, promptOptions)
const answer = await inquirer.prompt(options)
let answer
try {
answer = await inquirer.prompt(options)
} catch(error) {
if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC')
return ''
} else {
console.log('Unknown error while displaying askQuestionText prompt:', error)
}
}
return answer.text
}
}

14
scripts/wizard.js

@ -10,6 +10,7 @@ import { asciiArt } from './console.js'
import { headerStyle, heading2, styledStatus } from './styles.js'
import features from './features/index.js'
import { yesOrNo } from './welcome.js'
import { isNpmPackageInstalled } from './system.js'
const AO_PATH = path.join(process.env.HOME, '.ao')
const AO_MEMES_PATH = path.join(AO_PATH, 'memes')
@ -221,18 +222,9 @@ export async function checkAo() {
let npmI = false
if(exists) {
homePathsExist.push(folderName)
try {
const stdout = execSync('cd ' + folderPath + ' && npm list 2>&1')
npmI = isNpmPackageInstalled(folderPath)
if(npmI) {
npmInstalled.push(folderName)
npmI = true
} catch(error) {
const missingLocalPackagesCount = error.output.toString().match(/npm ERR! missing:/g).length || 0
if(missingLocalPackagesCount) {
// Confirmation that not all dependencies have been installed
//console.log(folderName, 'has', missingLocalPackagesCount, 'npm packages left to install.')
} else {
// Unknown error
}
}
}
console.log(folderName.padEnd(width) + columnize(exists ? npmI ? 'Installed' : 'Downloaded' : 'Not Installed'))

Loading…
Cancel
Save