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. 45
      scripts/features/ao-server.js
  5. 122
      scripts/features/index.js
  6. 36
      scripts/forest.js
  7. 225
      scripts/services.js
  8. 18
      scripts/system.js
  9. 12
      scripts/welcome.js
  10. 14
      scripts/wizard.js

23
README.md

@ -21,33 +21,34 @@ Then you can run with `ao-cli`.
These features work right now: These features work right now:
* Browse the [AO User Manual](https://git.coalitionofinvisiblecolleges.org/autonomousorganization/ao-manual) and automatically download and keep it updated * 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 * 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 * `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 * 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) * 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) * 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 ## Upcoming Features
These features are planned and many are mocked up in the menus: 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 * 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 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 switch between serving different AO frontends: `ao-svelte`, `ao-3` (Vue), or `ao-react`
* Easily update all your remote AOs at once * 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) * 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 * 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 * 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) * 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 ## Important Locations
@ -63,9 +64,9 @@ These features are planned and many are mocked up in the menus:
## Version History ## Version History
* 0.1.4 Added fantasy hook feature to bring the AO MUD aesthetic into the terminal * 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.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.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.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 * 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 './scripts/strings.js'
// Import AO modular features // 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 manual, { printManualPage, manualFolderAsMenu, AO_MANUAL_PATH } from './scripts/features/manual.js'
import aoCli from './scripts/features/ao-cli.js' import aoCli from './scripts/features/ao-cli.js'
@ -93,6 +93,7 @@ async function adminMenu() {
console.log(`\n${headerStyle('System Alchemy')}`) console.log(`\n${headerStyle('System Alchemy')}`)
const adminChoices = [ const adminChoices = [
{ name: 'Check AO install', value: 'check_AO' }, { name: 'Check AO install', value: 'check_AO' },
'Update everything',
'Update system software', 'Update system software',
//'Update AO', //'Update AO',
//{ name: 'Install AO prerequisites', value: 'prereqs' }, // move to feature module? calls installRequired() //{ name: 'Install AO prerequisites', value: 'prereqs' }, // move to feature module? calls installRequired()
@ -127,6 +128,21 @@ async function adminMenu() {
case 'check_AO': case 'check_AO':
await checkAo() await checkAo()
break 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': case 'Update system software':
updateSoftware() updateSoftware()
break break
@ -163,7 +179,7 @@ async function handleArgs(args) {
case '-cd': case '-cd':
// Don't say a message every time or it will annoy everybody // Don't say a message every time or it will annoy everybody
if(randomInt(0, 6) === 0) { if(randomInt(0, 6) === 0) {
console.log(wander(args[1])) console.log(await wander(args[1]))
} }
return false return false
} }

2
scripts/features/ao-cli.js

@ -34,7 +34,7 @@ function installAoCli() {
} }
async function getAoCliVersion() { 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 // Updates the globally-installed version of this package, ao-cli, using npm

45
scripts/features/ao-server.js

@ -1,29 +1,45 @@
import { execSync, exec } from 'child_process' import { execSync, exec } from 'child_process'
import { isFolder } from '../files.js' import { isFolder } from '../files.js'
import path from 'path' 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 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 // Returns one of: off, installed, enabled, running, synced, error
function serviceStatus() { function serviceStatus() {
if(!aoIsInstalled()) { if(!aoIsInstalled()) {
return 'off' return 'off'
} }
let stdout if(aoServerServiceManager.isInstalled()) {
try { const isRunning = aoServerServiceManager.isRunning()
stdout = execSync('systemctl status ao') switch(isRunning) {
} catch(err) { case true:
stdout = err.output.toString() return 'running'
case 'error':
return 'error'
} }
if(stdout.includes('Unit ao.service could not be found.')) { return 'enabled'
} else {
return 'installed' 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). // 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() { export function updateAoServer() {
console.log('Updating ao-server. If you are a developer, you may be asked for your SSH key now.')
try { try {
const stdout = execSync('cd ' + AO_SERVER_PATH + ' && git pull origin main 2>&1') const stdout = execSync('cd ' + AO_SERVER_PATH + ' && git pull origin main 2>&1')
if(stdout.includes('Already up to date.')) { if(stdout.includes('Already up to date.')) {
console.log('Already up to date.')
return return
} }
console.log('\nao-server was updated.') console.log('\nao-server was updated.')
@ -73,5 +91,6 @@ export default {
status: serviceStatus, status: serviceStatus,
install: installAo, install: installAo,
isInstalled: aoIsInstalled, isInstalled: aoIsInstalled,
update: updateAoServer update: updateAoServer,
submodules: [ aoServerServiceManager ]
} }

122
scripts/features/index.js

@ -8,6 +8,7 @@ import { fileURLToPath } from 'url'
import path from 'path' import path from 'path'
import { headerStyle, greenChalk, styledStatus } from '../styles.js' import { headerStyle, greenChalk, styledStatus } from '../styles.js'
import { spinner } from '../console.js' import { spinner } from '../console.js'
import SystemServiceManager, { getCustomServicesList, addCustomServiceInteractive, removeCustomService } from '../services.js'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
@ -51,12 +52,24 @@ export async function featuresMenu(previousMenuChoice = 0) {
const choice = { name: nameColumn + statusColumn + descriptionColumn, value: featureKey, short: featureKey } const choice = { name: nameColumn + statusColumn + descriptionColumn, value: featureKey, short: featureKey }
return choice 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( featuresChoices.push(
{ name: 'Add custom service', value: 'add_service' },
'Back to Main Menu' 'Back to Main Menu'
) )
} else { } else {
loadedFeatures = featuresChoices.filter(feature => { loadedFeatures = featuresChoices.filter(feature => {
return typeof feature === 'object' && !feature.name.includes('Unknown') return typeof feature === 'object' && feature.hasOwnProperty('name') && !feature.name.includes('Unknown')
}).length }).length
} }
stopSpinner('Loaded status for ' + loadedFeatures + '/' + (featuresChoices.length - 1) + ' features.') stopSpinner('Loaded status for ' + loadedFeatures + '/' + (featuresChoices.length - 1) + ' features.')
@ -76,7 +89,20 @@ export async function featuresMenu(previousMenuChoice = 0) {
return false return false
} }
} }
if(answer.features_menu === 'Back to Main Menu') { 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 return false
} }
const chosenFeature = features[answer.features_menu] const chosenFeature = features[answer.features_menu]
@ -85,47 +111,82 @@ export async function featuresMenu(previousMenuChoice = 0) {
return answer.features_menu return answer.features_menu
} }
// Prints the menu options for a specific feature. // Prints the menu options for a specific feature (including subfeatures)
// Each feature module can export functions for status, install, version, update and these will be listed in the menu based on context function oneFeatureMenuChoices(name, feature, status, isCustom = false) {
// If the module also has a menu: field, these menu items will each be appended if the name is truthy when calculated if(!status && status !== false) {
// Prints all the standard-named features plus features listed under 'menu' status = typeof feature.status === 'function' ? feature.status() : feature.hasOwnProperty('status') ? feature.status : 'off'
export async function oneFeatureMenu(name, feature) {
console.log(`\n${headerStyle(name)}`)
if(feature.description && feature.description?.length >= 1) {
console.log('\n' + feature.description + '\n')
} }
const featureChoices = [] let featureChoices = []
const stopSpinner = spinner('Loading status...')
const status = feature?.status() || false
stopSpinner('Loaded ' + name + ' status: ' + (feature.status() || 'Unknown') + '\n')
if(!status) { if(!status) {
console.log("This AO feature module lacks a status() function, not sure which menu items to display.") console.log("This AO subfeature module lacks a status() function, not sure which menu items to display.")
return false 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(status === 'off') {
if(feature.hasOwnProperty('install')) { if(typeof feature.install === 'function') {
featureChoices.push({ name: 'Install ' + name, value: 'install' }) featureChoices.push({ name: 'Install ' + name, value: () => feature.install() })
menuItemCount++
} }
} else { } else {
if(feature.hasOwnProperty('update')) { if(typeof feature.update === 'function') {
featureChoices.push({ name: 'Update ' + name, value: 'update'}) featureChoices.push({ name: 'Update ' + name, value: () => feature.update() })
}
if(typeof feature.uninstall === 'function') {
featureChoices.push({ name: 'Uninstall ' + name, value: () => feature.uninstall() })
}
} }
if(feature.hasOwnProperty('uninstall')) {
featureChoices.push({ name: 'Uninstall ' + name, value: 'uninstall'}) if(isCustom) {
featureChoices.push({ name: 'Remove from list', value: () => {
removeCustomService(name)
featuresChoices = null
} }
})
} }
if(feature.hasOwnProperty('menu')) { if(feature.hasOwnProperty('menu')) {
feature.menu.forEach(menuItem => { feature.menu.forEach(menuItem => {
const menuItemName = typeof menuItem.name === 'function' ? menuItem.name() : menuItem.name const menuItemName = typeof menuItem.name === 'function' ? menuItem.name() : menuItem.name
if(!menuItemName) { if(menuItemName) {
return featureChoices.push({ name: menuItemName, value: menuItem.Value })
} }
const menuItemValue = typeof menuItem.value === 'function' ? menuItem.value() : menuItem.value
// todo: uninstall option will go here also // 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) { if(featureChoices.length < 1) {
console.log("Nothing to do yet on this feature, please check back soon.") console.log("Nothing to do yet on this feature, please check back soon.")
return false return false
@ -145,12 +206,17 @@ export async function oneFeatureMenu(name, feature) {
if (error === 'EVENT_INTERRUPTED') { if (error === 'EVENT_INTERRUPTED') {
console.log('\nESC') console.log('\nESC')
return false return false
} else {
console.log('Unknown error displaying Features menu:', error)
} }
} }
if(answer.feature_menu === 'Back to Features') { if(answer.feature_menu === 'Back to Features') {
return false 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]() await feature[answer.feature_menu]()
return true return true
} }

36
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 // 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 = [ const stepMessages = [
'You find a door and walk through.', 'You find a door and walk through.',
@ -11,6 +13,10 @@ const walkMessages = [
'You take a winding path near a river.', 'You take a winding path near a river.',
'A few meadows away is the place you are looking for.', 'A few meadows away is the place you are looking for.',
'A deer leads you a few galleries away to where you were going.', '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 = { const wanderMessages = {
up: [ up: [
@ -65,10 +71,24 @@ const wanderMessages = {
'You perform a basic transit spell to take you to your destination.', 'You perform a basic transit spell to take you to your destination.',
'Turning your mind inside out, you transmigrate zones.', 'Turning your mind inside out, you transmigrate zones.',
'You bilocate and, when the confusion clears, there you are.', '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' direction = !!direction ? direction.trim() : 'home'
switch(direction) { switch(direction) {
case '..': case '..':
@ -96,5 +116,15 @@ export default function wander(direction) {
direction = Math.min(parseInt(totalSlashes), 5) 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
}

18
scripts/system.js

@ -71,7 +71,6 @@ export function updateSoftware() {
break break
case 'arch': case 'arch':
execSync('sudo pacman -Syu --noconfirm') 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 break
case 'fedora': case 'fedora':
execSync('sudo dnf update -yqqq 2>/dev/null && && sudo dnf autoremove -yqqq && sudo dnf upgrade -yqqq') 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 } 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
}

12
scripts/welcome.js

@ -117,6 +117,16 @@ export async function askQuestionText(prompt = 'Please enter a string:', promptO
message: prompt message: prompt
} }
Object.assign(options, promptOptions) 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 return answer.text
} }

14
scripts/wizard.js

@ -10,6 +10,7 @@ import { asciiArt } from './console.js'
import { headerStyle, heading2, styledStatus } from './styles.js' import { headerStyle, heading2, styledStatus } from './styles.js'
import features from './features/index.js' import features from './features/index.js'
import { yesOrNo } from './welcome.js' import { yesOrNo } from './welcome.js'
import { isNpmPackageInstalled } from './system.js'
const AO_PATH = path.join(process.env.HOME, '.ao') const AO_PATH = path.join(process.env.HOME, '.ao')
const AO_MEMES_PATH = path.join(AO_PATH, 'memes') const AO_MEMES_PATH = path.join(AO_PATH, 'memes')
@ -221,18 +222,9 @@ export async function checkAo() {
let npmI = false let npmI = false
if(exists) { if(exists) {
homePathsExist.push(folderName) homePathsExist.push(folderName)
try { npmI = isNpmPackageInstalled(folderPath)
const stdout = execSync('cd ' + folderPath + ' && npm list 2>&1') if(npmI) {
npmInstalled.push(folderName) 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')) console.log(folderName.padEnd(width) + columnize(exists ? npmI ? 'Installed' : 'Downloaded' : 'Not Installed'))

Loading…
Cancel
Save