import os from 'node:os'
import fs from 'node:fs'
import { execSync } from 'child_process'
/** Map of internal keys to Ableton Live .app bundle names. @type {Object.<string, string>} */
const LIVE_APP_BUNDLES = {
Live_12_Suite: 'Ableton Live 12 Suite.app',
Live_12_Standard: 'Ableton Live 12 Standard.app',
Live_12_Lite: 'Ableton Live 12 Lite.app',
Live_11_Suite: 'Ableton Live 11 Suite.app',
Live_11_Standard: 'Ableton Live 11 Standard.app',
Live_11_Lite: 'Ableton Live 11 Lite.app',
Live_10_Suite: 'Ableton Live 10 Suite.app',
Live_10_Standard: 'Ableton Live 10 Standard.app',
Live_10_Lite: 'Ableton Live 10 Lite.app',
}
/** Known Ableton preferences directory paths on macOS. @type {Object.<string, string>} */
const LIVE_PREFS_PATHS = {
Live_Preferences: '~/Library/Preferences/Ableton',
}
/**
* @typedef {Object} PluginInfoMacOSConfig
* @property {string} [default='/Library/Audio/Plug-Ins/'] - Base directory for system plug-ins.
* @property {Object} [vst2] - VST2 configuration.
* @property {boolean} [vst2.isEnabled] - Whether VST2 scanning is enabled.
* @property {boolean} [vst2.isCustomPathEnabled] - Whether the VST2 custom path is active.
* @property {string|null} [vst2.customPath] - Custom VST2 search path.
* @property {Object} [vst3] - VST3 configuration.
* @property {boolean} [vst3.isEnabled] - Whether VST3 scanning is enabled.
* @property {boolean} [vst3.isCustomPathEnabled] - Whether the VST3 custom path is active.
* @property {string|null} [vst3.customPath] - Custom VST3 search path.
* @property {boolean} [au.enabled=false] - Whether Audio Units are enabled.
*/
/**
* Discovers installed macOS audio plug-ins (VST2, VST3, and Audio Units).
* Must be instantiated on macOS (darwin); throws otherwise.
*/
export class PluginInfoMacOS {
/** macOS can have 64-bit vst plugins, including custom paths for them */
#_vst = []
#_vst3 = []
#_au = []
#_config = {
default: '/Library/Audio/Plug-Ins/',
vst2: {
enabled: false,
customEnabled: false,
customPath: null,
},
vst3: {
enabled: false,
customEnabled: false,
customPath: null,
},
customVst2: null,
customVst3: null,
au: {
enabled: false,
},
}
/**
* @param {PluginInfoMacOSConfig} config - Plug-in search configuration.
* @throws {Error} When not running on macOS.
*/
constructor(config) {
this.platform = os.platform()
if (this.platform !== 'darwin') {
throw new Error('PluginInfoMacOS can only be used on macOS')
}
this.#_config = { ...this.#_config, ...config }
;(async () => {
this.#_vst = await this.getVst2Plugins()
this.#_vst3 = await this.getVst3Plugins()
this.#_au = await this.getAudioUnitPlugins()
})()
}
/**
* Returns a snapshot of all discovered plug-in file paths, grouped by format.
* @type {{vst2: string[], vst3: string[], au: string[]}}
*/
get map() {
return {
vst2: this.#_vst,
vst3: this.#_vst3,
au: this.#_au,
}
}
/**
* Re-scans all plug-in directories, bypassing the in-memory cache.
* @returns {Promise<void>}
*/
async refresh() {
this.#_vst = await this.getVst2Plugins(false)
this.#_vst3 = await this.getVst3Plugins(false)
this.#_au = await this.getAudioUnitPlugins(false)
}
/**
* Returns the list of discovered VST2 plug-in file paths.
* @param {boolean} [cache=true] - When true, returns the cached result if available.
* @returns {Promise<string[]>} Absolute paths to `.vst` bundles.
*/
async getVst2Plugins(cache = true) {
if (cache && this.#_vst.length > 0) {
return this.#_vst
}
return await this.#_getPluginPaths('Vst')
}
/**
* Returns the list of discovered VST3 plug-in file paths.
* @param {boolean} [cache=true] - When true, returns the cached result if available.
* @returns {Promise<string[]>} Absolute paths to `.vst3` bundles.
*/
async getVst3Plugins(cache = true) {
if (cache && this.#_vst3.length > 0) {
return this.#_vst3
}
return await this.#_getPluginPaths('Vst3')
}
/**
* Returns the list of discovered Audio Unit plug-in file paths.
* @param {boolean} [cache=true] - When true, returns the cached result if available.
* @returns {Promise<string[]>} Absolute paths to `.component` bundles.
*/
async getAudioUnitPlugins(cache = true) {
if (cache && this.#_au.length > 0) {
return this.#_au
}
return await this.#_getPluginPaths('AudioUnit')
}
async #_getPluginPaths(pluginType) {
let searchPaths = []
if (pluginType === 'Vst') {
if (this.#_config.vst2.isEnabled) {
searchPaths = [this.#_config.default + 'VST/']
}
if (
this.#_config.vst2.isCustomPathEnabled &&
this.#_config.vst2.customPath
) {
searchPaths.push(this.#_config.vst2.customPath)
}
} else if (pluginType === 'Vst3') {
if (this.#_config.vst3.isEnabled) {
searchPaths = [this.#_config.default + 'VST3/']
}
if (
this.#_config.vst3.isCustomPathEnabled &&
this.#_config.vst3.customPath
) {
searchPaths.push(this.#_config.vst3.customPath)
}
} else if (pluginType === 'AudioUnit') {
searchPaths = [this.#_config.default + 'Components/']
}
let pluginFiles = []
for (const path of searchPaths) {
try {
const files = await fs.promises.readdir(path)
const pluginFilesInPath = files
.filter((file) =>
file.endsWith(this.#_getPluginExtension(pluginType)),
)
.map((file) => path + file)
pluginFiles.push(...pluginFilesInPath)
} catch (err) {
// Handle error if directory can't be read
if (err) throw err
}
}
return pluginFiles
}
#_getPluginExtension(pluginType) {
if (pluginType === 'Vst') {
return '.vst'
} else if (pluginType === 'Vst3') {
return '.vst3'
} else if (pluginType === 'AudioUnit') {
return '.component'
}
}
}
/**
* @typedef {Object} LiveVersion
* @property {string} version - The version string (e.g. `"12.0.5"`).
* @property {string|null} build - The build identifier, or `null` if not present.
*/
/**
* Retrieves information about Ableton Live installations on macOS.
* Must be instantiated on macOS (darwin); throws otherwise.
*/
export class AbletonInfoMacOS {
/**
* @throws {Error} When not running on macOS.
*/
constructor() {
// console.log('os', os.platform())
this.platform = os.platform()
if (this.platform !== 'darwin') {
throw new Error('AbletonInfoMacOS can only be used on macOS')
}
}
/**
* The default directory where Ableton Live is installed on macOS.
* @type {string}
*/
get defaultInstallRoot() {
return '/Applications/'
}
/**
* All installed Ableton Live versions found in the default install root.
* @type {LiveVersion[]}
*/
get installedVersions() {
return this.getInstalledLiveVersions()
}
/**
* Returns the macOS product version string (e.g. `"14.5"`).
* @returns {string} The macOS version.
*/
getMacOSVersion() {
return execSync('sw_vers -productVersion', { encoding: 'utf8' }).trim()
}
/**
* Scans a directory for installed Ableton Live app bundles and returns their versions.
* @param {string|null} [rootDirectory=null] - Directory to search; defaults to {@link defaultInstallRoot}.
* @returns {LiveVersion[]} Version info for each detected installation.
*/
getInstalledLiveVersions(rootDirectory = null) {
if (!rootDirectory) {
rootDirectory = this.defaultInstallRoot
}
let bundles = Object.values(LIVE_APP_BUNDLES).filter((appBundleName) => {
const appPath = `${rootDirectory}${appBundleName}`
try {
execSync(`test -d "${appPath}"`)
return true
} catch {
return false
}
})
return bundles.map((appBundleName) => {
const appPath = `${rootDirectory}${appBundleName}`
return this.getLiveVersionFromAppBundle(appPath)
})
}
/**
* Reads the version from an Ableton Live `.app` bundle's `Info.plist`.
* @param {string} appBundlePath - Absolute path to the `.app` bundle.
* @returns {LiveVersion} The version and build extracted from the bundle.
* @throws {Error} When the plist cannot be read or parsed.
*/
getLiveVersionFromAppBundle(appBundlePath) {
try {
const plistPath = `${appBundlePath}/Contents/Info.plist`
const rawVersion = execSync(
`plutil -extract CFBundleShortVersionString raw "${plistPath}"`,
{ encoding: 'utf8' },
).trim()
let pieces = rawVersion.split(' (')
return {
version: pieces[0].trim(),
build: pieces[1] ? pieces[1].replace(')', '').trim() : null,
}
} catch (error) {
throw new Error(`Failed to get version from app bundle: ${error.message}`)
}
}
}
export default AbletonInfoMacOS