install.fairie/home/dot_local/bin/executable_installx
2024-05-28 06:55:42 +00:00

634 lines
28 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env zx
import osInfo from 'linux-os-info'
$.verbose = false
// Preserves color from subshells
process.env.CLICOLOR_FORCE = 3
let installOrder, osArch, osId, osType, pkgs, sysType
const cacheDir = os.homedir() + '/.cache/installx'
const customArgv = minimist(process.argv.slice(3), {
boolean: [
'all'
],
alias: {
a: 'all',
}
})
function log(message) {
console.log(`${chalk.greenBright.bold('installx ')} ${message}`)
}
async function getOsInfo() {
return osInfo({ mode: 'sync' })
}
function execPromise(command) {
return new Promise(function (resolve, reject) {
require('child_process').exec(command, (error, stdout, stderr) => {
if (error) {
reject(error)
return
}
resolve(stdout.trim())
})
})
}
function runSilentCommand(command) {
return require('child_process').execSync(`${command}`, { stdio: 'inherit', shell: true })
}
async function runScript(key, script) {
fs.writeFileSync(`${cacheDir}/${key}-raw`, script)
const [templatedScript, file, brief] = await Promise.all([
$`cat "${cacheDir}/${key}-raw" | chezmoi execute-template`,
$`cat "${cacheDir}/${key}-raw" | ( grep "^# @file" || [ "$?" == "1" ] ) | sed 's/^# @file //'`,
$`cat "${cacheDir}/${key}-raw" | ( grep "^# @brief" || [ "$?" == "1" ] ) | sed 's/^# @brief //'`
])
fs.writeFileSync(`${cacheDir}/${key}-glow`, (file.stdout ? `# ${file.stdout}\n\n` : '') + (brief.stdout ? `> ${brief.stdout}\n\n` : '') + '```sh\n' + templatedScript.stdout + "\n```")
fs.writeFileSync(`${cacheDir}/${key}`, templatedScript.stdout)
try {
runSilentCommand(`glow --width 120 "${cacheDir}/${key}-glow"`)
// TODO: Set process.env.DEBUG || true here because the asynchronous method is not logging properly / running slow
if (process.env.DEBUG) {
return await runSilentCommand(`bash "${cacheDir}/${key}" || gum log -sl error 'Error occurred while processing script for ${key}'`)
} else {
return await $`bash "${cacheDir}/${key}" || gum log -sl error 'Error occurred while processing script for ${key}'`.pipe(process.stdout)
}
} catch (e) {
console.error(`Failed to run script associated with ${key}`, e)
}
}
function getPkgData(pref, pkg, installer) {
if (installer) {
if (pkg[`${pref}:${installer}:${osId}:${osArch}`]) {
return `${pref}:${installer}:${osId}:${osArch}` // Handles case like `_bin:pipx:debian:x64:`
} else if (pkg[`${pref}:${osId}:${installer}:${osArch}`]) {
return `${pref}:${osId}:${installer}:${osArch}` // Handles case like `_bin:debian:pipx:x64:`
} else if (pkg[`${pref}:${installer}:${osType}:${osArch}`]) {
return `${pref}:${installer}:${osType}:${osArch}` // Handles case like `_bin:pipx:windows:x64:`
} else if (pkg[`${pref}:${osType}:${installer}:${osArch}`]) {
return `${pref}:${osType}:${installer}:${osArch}` // Handles case like `_bin:windows:pipx:x64:`
} else if (pkg[`${pref}:${installer}:${osId}`]) {
return `${pref}:${installer}:${osType}` // Handles case like `_bin:pipx:fedora:`
} else if (pkg[`${pref}:${osId}:${installer}`]) {
return `${pref}:${osType}:${installer}` // Handles case like `_bin:fedora:pipx:`
} else if (pkg[`${pref}:${installer}:${osType}`]) {
return `${pref}:${installer}:${osType}` // Handles case like `_bin:pipx:darwin:`
} else if (pkg[`${pref}:${osType}:${installer}`]) {
return `${pref}:${osType}:${installer}` // Handles case like `_bin:darwin:pipx:`
} else if (pkg[`${pref}:${installer}`]) {
return `${pref}:${installer}` // Handles case like `_bin:pipx:`
} else if (pkg[`${pref}`]) {
return `${pref}` // Handles case like `_bin:`
} else {
return false
}
} else {
if (pkg[`${pref}:${osId}:${osArch}`]) {
return `${pref}:${osId}:${osArch}` // Handles case like `pipx:debian:x64:`
} else if (pkg[`${pref}:${osType}:${osArch}`]) {
return `${pref}:${osType}:${osArch}` // Handles case like `pipx:windows:x64:`
} else if (pkg[`${pref}:${osId}`]) {
return `${pref}:${osType}` // Handles case like `pipx:fedora:`
} else if (pkg[`${pref}:${osType}`]) {
return `${pref}:${osType}` // Handles case like `pipx:darwin:`
} else if (pkg[`${pref}`]) {
return `${pref}` // Handles case like `pipx:`
} else {
return false
}
}
}
async function getSoftwareDefinitions() {
try {
return YAML.parse(fs.readFileSync(`${os.homedir()}/.local/share/chezmoi/software.yml`, 'utf8'))
} catch (e) {
throw Error('Failed to load software definitions', e)
}
}
async function getSystemType() {
if (process.platform === "win32") {
return "windows"
} else if (process.platform === "linux") {
if (which.sync('apk')) {
return "apk"
} else if (which.sync('apt-get')) {
return "apt"
} else if (which.sync('dnf')) {
return "dnf"
} else if (which.sync('pacman')) {
return "pacman"
} else if (which.sync('zypper')) {
return "zypper"
} else {
return "linux"
}
} else {
return process.platform
}
}
function expandDeps(keys) {
for (const i of keys) {
for (const pref of installOrder[sysType]) {
const installKey = getPkgData(pref, pkgs[i], false)
if (installKey) {
const installType = installKey.split(':')[0]
const depsKey = getPkgData('_deps', pkgs[i], installType)
if (depsKey) {
const deps = typeof pkgs[i][depsKey] === 'string' ? [pkgs[i][depsKey]] : pkgs[i][depsKey]
return [...keys, ...expandDeps(deps)]
}
}
}
return [...keys]
}
return [...keys]
}
async function createCaskLinks(caskMap) {
if (!caskMap) return
const caskApps = caskMap
.filter(x => {
// Filter out macOS apps that already have a _app installed
if (x.installType === 'cask' || (osId === 'darwin' && x._app)) {
const appField = getPkgData('_app', x, x.installType)
const binField = getPkgData('_bin', x, x.installType)
const sysDir = fs.existsSync(`/Applications/${x[appField]}`)
const homeDir = fs.existsSync(`${os.homedir()}/Applications/${x[appField]}`)
const binFile = fs.existsSync(`${os.homedir()}/.local/bin/cask/${x[binField]}`)
if (sysDir || homeDir) {
return !binFile
} else {
return false
}
} else {
return false
}
})
caskApps.length && await $`mkdir -p "$HOME/.local/bin/cask"`
for (const app of caskApps) {
const appField = getPkgData('_app', app, app.installType)
if (!appField) {
log(`${app.listKey} is missing an _app definition`)
return
}
const binField = getPkgData('_bin', app, app.installType)
if (!binField) {
log(`${app.listKey} is missing a _bin definition`)
return
}
if (fs.existsSync(`${os.homedir()}/Applications/${app[appField]}`)) {
fs.writeFileSync(`${os.homedir()}/.local/bin/cask/${app[binField]}`, `#!/usr/bin/env bash\nopen "$HOME/Applications/${app[appField]}" $*`)
await $`chmod +x '${os.homedir()}/.local/bin/cask/${app[binField]}'`
} else if (fs.existsSync(`/Applications/${app[appField]}`)) {
fs.writeFileSync(`${os.homedir()}/.local/bin/cask/${app[binField]}`, `#!/usr/bin/env bash\nopen "/Applications/${app[appField]}" $*`)
await $`chmod +x '${os.homedir()}/.local/bin/cask/${app[binField]}'`
} else {
log(`Unable to create bin link to ${app[appField]}`)
}
}
caskApps.length && log(`Finished creating Homebrew cask links in ~/.local/bin/cask`)
}
async function createFlatpakLinks(flatpakMap) {
const flatpakInstallations = await $`flatpak --installations`
const flatpakDir = flatpakInstallations.stdout.replace('\n', '')
const flatpakApps = flatpakMap
.filter(x => {
if (x.installType === 'flatpak') {
const binField = getPkgData('_bin', x, x.installType)
const binFile = fs.existsSync(`${os.homedir()}/.local/bin/flatpak/${x[binField]}`)
return !binFile
}
return false
})
flatpakApps.length && await $`mkdir -p "$HOME/.local/bin/flatpak"`
for (const app of flatpakApps) {
const binField = getPkgData('_bin', app, app.installType)
if (!binField) {
log(`${app.listKey} is missing a _bin definition`)
return
}
if (fs.existsSync(`${flatpakDir}/app/${app.installList[0]}`)) {
fs.writeFileSync(`${os.homedir()}/.local/bin/flatpak/${app[binField]}`, `#!/usr/bin/env bash\nflatpak run ${app.installList[0]} $*`)
await $`chmod +x '${os.homedir()}/.local/bin/flatpak/${app[binField]}'`
} else {
log(`Unable to create bin link to ${x.flatpak}`)
}
}
flatpakApps.length && log(`Finished creating Flatpak links in ~/.local/bin/flatpak`)
}
async function bundleInstall(brews, casks, caskMap) {
try {
const lines = []
casks.length && log(`Adding following casks to Brewfile for installation: ${casks.join(' ')}`)
for (const cask of casks) {
if (cask.indexOf('/') !== -1) {
lines.push(`tap "${cask.substring(0, cask.lastIndexOf('/'))}"`)
}
if (cask.indexOf(' --no-quarantine') === -1) {
lines.push(`cask "${cask}"`)
} else {
lines.push(`cask "${cask.replace(' --no-quarantine', '')}", args: { "no-quarantine": true }`)
}
}
brews.length && log(`Adding following brews to Brewfile for installation: ${brews.join(' ')}`)
for (const brew of brews) {
if (brew.indexOf('/') !== -1) {
lines.push(`tap "${brew.substring(0, brew.lastIndexOf('/'))}"`)
}
lines.push(`brew "${brew}"`)
}
log(`Creating Brewfile to install from`)
cd(await $`mktemp -d`)
fs.writeFileSync('Brewfile', lines.join('\n'))
log(`Installing packages via brew bundle`)
await $`brew bundle --file Brewfile`
log(`Finished installing via Brewfile`)
} catch (e) {
console.log('Error:', e)
log(`Error occurred while installing via Brewfile`)
}
try {
await createCaskLinks(caskMap)
} catch (e) {
console.log('Error:', e)
log(`Error occurred while creating cask bin links`)
}
}
async function forEachSeries(iterable) {
for (const x of iterable) {
await x
}
}
async function installPackages(pkgInstructions) {
const combined = {}
const promises = []
log(`Populating install order lists`)
for (const option of installOrder[sysType]) {
const instructions = pkgInstructions.filter(x => x.installType === option).filter((value, index, array) => array.indexOf(value) === index)
if (instructions.length) {
combined[option] = instructions
}
}
log(`Running Homebrew installation via Brewfile`)
if ((combined.brew && combined.brew.length) || (combined.cask && combined.cask.length)) {
promises.push(bundleInstall(combined.brew ? combined.brew.flatMap(x => x.installList.flatMap(i => i)) : [], combined.cask ? combined.cask.flatMap(x => x.installList.flatMap(i => i)) : [], combined.cask))
}
for (const key of Object.keys(combined)) {
if (key !== 'script') {
log(`Install orders for ${key}: ${combined[key].flatMap(i => i.installList).join(' ')}`)
}
switch (key) {
case 'ansible':
promises.push(forEachSeries(combined[key].flatMap(x => x.installList.flatMap(i => $`${key} 127.0.0.1 -v${process.env.DEBUG && 'vv'} -e '{ ansible_connection: "local", ansible_become_user: "root", ansible_user: "${process.env.USER}", ansible_family: "${osId.charAt(0).toUpperCase() + osId.slice(1)}", install_homebrew: False }' -m include_role -a name=${i}`))))
break
case 'apk':
promises.push($`sudo ${key} add ${combined[key].flatMap(x => x.installList).split(' ')}`)
break
case 'appimage':
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => {
if (x.substring(0, 4) === 'http') {
return $`zap install --select-first -q --from ${i}`
} else if ((x.match(/\//g) || []).length === 1) {
return $`zap install --select-first -q --github --from ${i}`
} else {
return $`zap install --select-first -q ${i}`
}
})))
break
case 'apt':
promises.push($`DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Options::=--force-confdef install -y ${combined[key].flatMap(x => x.installList).split(' ')}`)
break
case 'basher':
case 'baulk':
case 'cargo':
case 'crew':
case 'gem':
case 'go':
case 'pip':
case 'pipx':
case 'scoop': // Maybe needs forEachSeries
case 'winget': // Maybe needs forEachSeries
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => $`${key} install ${i}`)))
break
case 'npm':
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => $`volta install ${i}`)))
break
case 'binary':
// TODO
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => $`TMP="$(mktemp)" && curl -sSL ${i} > "$TMP" && sudo mv "$TMP" /usr/local/src/${x._bin} && chmod +x /usr/local/src/${x._bin}`)))
break
case 'brew':
case 'cask': // Handled above
break
case 'choco':
promises.push($`${key} install -y ${combined[key].flatMap(x => x.installList).join(' ')}`)
break
case 'dnf':
case 'yum':
case 'zypper':
promises.push($`sudo ${key} install -y ${combined[key].flatMap(x => x.installList).join(' ')}`)
break
case 'emerge':
case 'pkg_add':
promises.push($`sudo ${key} ${combined[key].flatMap(x => x.installList).join(' ')}`)
break
case 'eopkg':
case 'pkg-freebsd':
case 'pkg-termux':
case 'pkgin':
case 'port':
case 'snap': // TODO - snap testing.. combine with snap-classic and add appropriate logic
promises.push($`sudo ${key === 'pkg-freebsd' || key === 'pkg-termux' ? 'pkg' : key} install ${combined[key].flatMap(x => x.installList).join(' ')}`)
break
case 'flatpak':
promises.push(forEachSeries(combined[key].flatMap(x => x.installList.flatMap(i => $`sudo ${key} install -y flathub ${i}`))))
break
case 'github': // TODO
break
case 'nix-env': // TODO
case 'nix-pkg': // TODO
case 'nix-shell': // TODO
break
case 'pacman':
promises.push($`sudo ${key} -Sy --noconfirm --needed ${combined[key].flatMap(x => x.installList).join(' ')}`)
break
case 'pkg-darwin':
break
case 'sbopkg': // TODO
break
case 'script':
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => runScript(x.listKey, i))))
break
case 'whalebrew': // TODO
break
case 'xbps':
promises.push($`sudo xbps-install -S ${combined[key].flatMap(x => x.installList).join(' ')}`)
break
case 'yay':
promises.push($`yay -Sy --noconfirm --needed ${combined[key].flatMap(x => x.installList).join(' ')}`)
break
default:
log(`Unable to find install key instructions for ${key}`)
}
}
log(`Performing ${promises.length} installations`)
process.env.DEBUG && console.log('Queued installs:', promises)
const installs = await Promise.allSettled(promises)
log(`All of the installations have finished`)
process.env.DEBUG && console.log('Completed installs:', installs)
await postInstall(combined)
}
async function postInstall(combined) {
log(`Running post-install routine`)
const promises = []
Object.keys(combined).includes('flatpak') && promises.push(createFlatpakLinks(combined.flatpak))
const postInstalls = await Promise.allSettled(promises)
process.env.DEBUG && console.log('Post installs:', postInstalls)
}
async function acquireManagerList(type, command) {
if (which.sync(type, { nothrow: true })) {
if (fs.existsSync(`${cacheDir}/${type}`)) {
setTimeout(() => {
require('child_process').exec(`${command} > ${cacheDir}/${type}`)
}, 100)
} else {
require('child_process').execSync(`${command} > ${cacheDir}/${type}`)
}
return fs.readFileSync(`${cacheDir}/${type}`).toString().split('\n')
} else {
log(`${type} is not installed`)
return []
}
}
function pkgMap(pkgDefs) {
return pkgDefs
.map(i => {
for (const pref of installOrder[sysType]) {
const installKey = getPkgData(pref, pkgs[i], false)
if (installKey) {
return {
...pkgs[i],
listKey: i,
installKey,
installType: installKey.split(':')[0],
installList: typeof pkgs[i][installKey] === 'string' ? [pkgs[i][installKey]] : pkgs[i][installKey]
}
}
}
return {
...pkgs[i],
listKey: i,
installKey: false,
installType: false,
installList: []
}
})
.filter(x => x.installKey)
}
async function main() {
await $`mkdir -p ${cacheDir}`
log(`Acquiring software definitions and system information`)
const initData = await Promise.all([
getOsInfo(),
getSoftwareDefinitions(),
getSystemType()
])
osArch = initData[0].arch
osId = process.platform === 'win32' ? 'win32' : (process.platform === 'linux' ? initData[0].id : process.platform)
osType = process.platform === 'win32' ? 'windows' : process.platform
pkgs = initData[1].softwarePackages
sysType = initData[2]
installOrder = initData[1].installerPreference
log(`Populating lists of pre-installed packages`)
const listPromises = [
acquireManagerList('apt', `if command -v dpkg; then dpkg -l; fi`),
acquireManagerList('brew', `brew list -1`),
acquireManagerList('cargo', `cargo install --list | awk '/^[[:alnum:]]/ {print $1}'`),
acquireManagerList('dnf', `rpm -qa`),
acquireManagerList('flatpak', `flatpak --columns=app list`),
acquireManagerList('gem', `gem list | awk '{print $1}'`),
acquireManagerList('npm', `volta list --format plain | awk '{print $2}' | sed 's/@.*//'`),
acquireManagerList('pacman', `pacman -Qs`),
acquireManagerList('pip', `pip3 list | awk '{print $1}'`),
acquireManagerList('pipx', `pipx list --short | awk '{print $1}'`),
acquireManagerList('snap', `if command -v snapd; then snap list; fi`),
acquireManagerList('zap', `zap list`)
]
const lists = await Promise.all(listPromises)
const managerLists = {
appimage: lists[6],
apt: lists[0],
brew: lists[1],
cargo: lists[2],
cask: lists[1],
dnf: lists[3],
flatpak: lists[4],
gem: lists[5],
npm: lists[6],
pacman: lists[7],
pip: lists[8],
pipx: lists[9],
snap: lists[10],
zap: lists[11]
}
log(`Acquiring installation keys`)
const installKeys = Object.keys(pkgs)
.filter(i => expandDeps(argv._).includes(i))
log(`Constructing installation data`)
const installData = pkgMap(customArgv.all ? Object.keys(pkgs) : installKeys)
log(`Filtering install instructions`)
const installInstructions = installData
.map(x => {
return {
...x,
installList: x.installList.filter(y => {
if ((x.installType === 'brew' || x.installType === 'cask') && y.includes('/')) {
return managerLists[x.installType] ? !managerLists[x.installType].includes(y.substring(y.lastIndexOf('/') + 1, y.length)) : true
} else {
return managerLists[x.installType] ? !managerLists[x.installType].includes(y) : true
}
})
}
})
.filter(x => {
// Filter out packages already installed by by package managers
return x.installList.length
})
.filter(x => {
// Filter out packages that contain a deprecation note
return !x._deprecated
})
.filter(x => {
// Filter out macOS apps that already have a _app installed
if (x.installType === 'cask' || (osId === 'darwin' && x._app)) {
const appField = getPkgData('_app', x, x.installType)
const appCheck = fs.existsSync(`/Applications/${x[appField]}`) || fs.existsSync(`${os.homedir()}/Applications/${x[appField]}`)
appCheck && log(`Skipping installation of ${x.listKey} because the application is in an Applications folder`)
return !appCheck
} else {
return true
}
})
.filter(x => {
// Filter out packages that already have a bin in the PATH
const binField = getPkgData('_bin', x, x.installType)
const isArray = Array.isArray(x[binField])
if (typeof x[binField] === 'string' || isArray) {
isArray && log(`_bin field for ${x.listKey} is an array so the first entry will be used to check`)
const whichCheck = which.sync(typeof x[binField] === 'string' ? x[binField] : x[binField][0], { nothrow: true })
whichCheck && log(`Skipping installation of ${x.listKey} because its binary is available in the PATH`)
return !whichCheck
} else {
log(`Ignoring _bin check because the _bin field for ${x.listKey} is not a string or array`)
return true
}
})
.filter(x => {
// Filter out packages that do not pass _when check
const whenField = getPkgData('_when', x, x.installType)
if (x[whenField]) {
if (typeof x[whenField] === 'string') {
try {
runSilentCommand(`${x[whenField]}`)
return true
} catch (e) {
return false
}
} else {
log(`typeof _when for ${x.listKey} must be a string`)
return true
}
} else {
return true
}
})
log(`Running installation routine`)
await installPackages(installInstructions)
log(`Adding users / groups defined under _groups`)
const usersGroupsAdditions = installData
.flatMap(x => {
const groupsField = getPkgData('_groups', x, x.installType)
if (!groupsField) return Promise.resolve()
log(`Ensuring user(s) / group(s) created for ${x.listKey}`)
if (typeof typeof x[groupsField] !== 'string' && !Array.isArray(x[groupsField])) {
log(`Failed to parse _groups for ${x.installKey}. The _groups field must be either a string or string[].`)
return Promise.resolve()
} else {
const groups = typeof x[groupsField] === 'string' ? [x[groupsField]] : x[groupsField]
return groups.flatMap(y => {
return $`sudo "${os.homedir()}/.local/bin/add-usergroup" "${y}" "${y}" && sudo "${os.homedir()}/.local/bin/add-usergroup" "${process.env.USER}" "${y}"`
})
}
})
await Promise.allSettled(usersGroupsAdditions)
log(`Running post-install inline scripts`)
for (const x of installData) {
const postField = getPkgData('_post', x, x.installType)
if (postField) {
log(`Running post-install script for ${x.listKey}`)
const binField = getPkgData('_bin', x, x.installType)
const isArray = Array.isArray(x[binField])
if (typeof x[binField] === 'string' || isArray) {
isArray && log(`_bin field for ${x.listKey} is an array so the first entry will be used to check`)
const whichCheck = which.sync(typeof x[binField] === 'string' ? x[binField] : x[binField][0], { nothrow: true })
whichCheck && await runScript(x.listKey, x[postField])
} else {
console.error(`${x.listKey}'s _bin field should either be a string or an array of strings`)
}
}
}
log(`Running post-install scripts defined in ~/.local/bin/post-installx`)
for (const x of installData) {
const scriptPath = `${os.homedir()}/.local/bin/post-installx/post-${x.listKey}.sh`
const scriptExists = fs.existsSync(scriptPath)
if (scriptExists) {
log(`Running post-install script defined in ${scriptPath}`)
await runScript(`post-${x.listKey}.sh`, fs.readFileSync(scriptPath, 'utf8'))
}
}
//await Promise.allSettled(postScripts)
//await Promise.allSettled(postScriptFiles)
log(`Starting services flagged with _serviceEnabled`)
const systemctlInstalled = which.sync('systemctl', { nothrow: true })
const brewInstalled = which.sync('brew', { nothrow: true })
const servicePromises = installData
.filter(x => !!x._serviceEnabled)
.filter(x => !!x._service)
.flatMap(x => {
const serviceField = getPkgData('_service', x, x.installType)
if (!serviceField) return Promise.resolve()
const services = typeof x[serviceField] === 'string' ? [{ name: x[serviceField] }] : (Array.isArray(x[serviceField]) ? x[serviceField] : [{ name: x[serviceField].name, sudo: x[serviceField].sudo }])
return services.flatMap(y => {
const name = typeof y === 'string' ? y : y.name
const sudo = typeof y === 'string' ? null : y.sudo
if (osType === 'linux' && x.installType !== 'brew' && x.installType !== 'cask' && systemctlInstalled) {
return sudo === true ? $`sudo systemctl enable --now ${name}` : $`systemctl enable --now ${name}`
} else if (brewInstalled) {
return sudo === true ? $`sudo brew services restart ${name}` : $`brew services restart ${name}`
}
})
})
await Promise.allSettled(servicePromises)
log(`Installation process complete!`)
}
main()