Source: ableton-info-win64.js

import os from 'node:os'
import { execFile, exec, execSync } from 'node:child_process'
import fs from 'node:fs/promises'
import { readFileSync } from 'node:fs'

/**
 * Detects whether the current process is running inside Windows Subsystem for Linux (WSL).
 * @returns {boolean} `true` when running under WSL, `false` otherwise.
 */
export function isRunningInWsl() {
	const hasWslEnv =
		process.env.WSL_DISTRO_NAME || process.env.WSLENV ? true : false

	const isWsl = os.type() === 'Linux' && hasWslEnv
	return isWsl
}

/**
 * @typedef {Object} PluginInfoWin64Config
 * @property {Object} [vst2] - VST2 configuration overrides.
 * @property {boolean} [vst2.enabled] - Whether VST2 scanning is enabled.
 * @property {string[]} [vst2.systemPaths] - Default system search paths for VST2 plug-ins.
 * @property {boolean} [vst2.customEnabled] - Whether the VST2 custom path is active.
 * @property {string|null} [vst2.customPath] - Custom VST2 search path.
 * @property {Object} [vst3] - VST3 configuration overrides.
 * @property {boolean} [vst3.enabled] - Whether VST3 scanning is enabled.
 * @property {string[]} [vst3.systemPaths] - Default system search paths for VST3 plug-ins.
 * @property {boolean} [vst3.customEnabled] - Whether the VST3 custom path is active.
 * @property {string|null} [vst3.customPath] - Custom VST3 search path.
 */

/**
 * Discovers installed 64-bit Windows audio plug-ins (VST2 and VST3).
 * Must be instantiated on Windows or WSL; throws otherwise.
 */
export class PluginInfoWin64 {
	/** Windows can have 32-bit and 64-bit VST plugins, including custom paths for them */
	#_vst2 = []
	#_vst3 = []

	#_config = {
		vst2: {
			enabled: false,
			systemPaths: [
				'C:\\Program Files\\VSTPlugins',
				'C:\\Program Files\\Steinberg\\VSTPlugins',
			],
			customEnabled: false,
			customPath: null,
		},
		vst3: {
			enabled: false,
			systemPaths: ['C:\\Program Files\\Common Files\\VST3'],
			customEnabled: false,
			customPath: null,
		},
	}

	/**
	 * @param {PluginInfoWin64Config} [config={}] - Optional overrides for default search paths and flags.
	 * @throws {Error} When not running on Windows or WSL.
	 */
	constructor(config = {}) {
		this.platform = os.platform()
		this.isWsl = isRunningInWsl()

		if (this.platform !== 'win32' && this.isWsl === false) {
			throw new Error('PluginInfoWin64 can only be used on Windows')
		}

		// Deep merge config
		if (config.vst2) {
			this.#_config.vst2 = { ...this.#_config.vst2, ...config.vst2 }
		}
		if (config.vst3) {
			this.#_config.vst3 = { ...this.#_config.vst3, ...config.vst3 }
		}

		;(async () => {
			this.#_vst2 = await this.getVst2Plugins()
			this.#_vst3 = await this.getVst3Plugins()
		})()
	}

	/**
	 * Returns a snapshot of all discovered plug-in file paths, grouped by format.
	 * @type {{vst2: string[], vst3: string[]}}
	 */
	get map() {
		return {
			vst2: this.#_vst2,
			vst3: this.#_vst3,
		}
	}

	/**
	 * Re-scans all plug-in directories, bypassing the in-memory cache.
	 * @returns {Promise<void>}
	 */
	async refresh() {
		this.#_vst2 = await this.getVst2Plugins(false)
		this.#_vst3 = await this.getVst3Plugins(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 `.dll` plug-in files.
	 */
	async getVst2Plugins(cache = true) {
		if (cache && this.#_vst2.length > 0) {
			return this.#_vst2
		}
		return await this.#_getPluginPaths('Vst2')
	}

	/**
	 * 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` plug-in files.
	 */
	async getVst3Plugins(cache = true) {
		if (cache && this.#_vst3.length > 0) {
			return this.#_vst3
		}
		return await this.#_getPluginPaths('Vst3')
	}

	async #_getPluginPaths(pluginType) {
		let searchPaths = []

		if (pluginType === 'Vst2') {
			if (this.#_config.vst2.enabled) {
				searchPaths.push(...this.#_config.vst2.systemPaths)
			}
			if (this.#_config.vst2.customEnabled && this.#_config.vst2.customPath) {
				searchPaths.push(this.#_config.vst2.customPath)
			}
		} else if (pluginType === 'Vst3') {
			if (this.#_config.vst3.enabled) {
				searchPaths.push(...this.#_config.vst3.systemPaths)
			}
			if (this.#_config.vst3.customEnabled && this.#_config.vst3.customPath) {
				searchPaths.push(this.#_config.vst3.customPath)
			}
		}

		let pluginFiles = []

		for (const path of searchPaths) {
			try {
				const files = await fs.readdir(path)
				const pluginFilesInPath = files
					.filter((file) =>
						file.endsWith(this.#_getPluginExtension(pluginType)),
					)
					.map((file) => {
						const separator = path.endsWith('\\') ? '' : '\\'
						return path + separator + file
					})
				pluginFiles.push(...pluginFilesInPath)
			} catch (err) {
				// Handle error if directory can't be read (e.g., doesn't exist)
				// Silently continue to the next path
			}
		}

		return pluginFiles
	}

	#_getPluginExtension(pluginType) {
		if (pluginType === 'Vst2') {
			return '.dll'
		} else if (pluginType === 'Vst3') {
			return '.vst3'
		}
	}
}

/**
 * @typedef {Object} WindowsVersionInfo
 * @property {number} majorVersion - The Windows major version number.
 * @property {number} buildNumber - The OS build number.
 * @property {string} productName - Human-readable product name (`"Windows 10"` or `"Windows 11"`).
 */

/**
 * Retrieves information about Ableton Live installations on 64-bit Windows.
 * Also works when running inside WSL. Throws on other platforms.
 */
export class AbletonInfoWin64 {
	/** Known Ableton Live paths on Windows. @type {Object} */
	LIVE_PATHS = {
		Live_Preferences: false, // ????
		Install_Root: 'C:\\ProgramData\\Ableton',
		Executables: {
			'Live 12 Suite':
				'C:\\ProgramData\\Ableton\\Live 12 Suite\\Program\\Ableton Live 12 Suite.exe',
			'Live 12 Standard':
				'C:\\ProgramData\\Ableton\\Live 12 Standard\\Program\\Ableton Live 12 Standard.exe',
			'Live 11 Suite':
				'C:\\ProgramData\\Ableton\\Live 11 Suite\\Program\\Ableton Live 11 Suite.exe',
			'Live 11 Standard':
				'C:\\ProgramData\\Ableton\\Live 11 Standard\\Program\\Ableton Live 11 Standard.exe',
			'Live 10 Suite':
				'C:\\ProgramData\\Ableton\\Live 10 Suite\\Program\\Ableton Live 10 Suite.exe',
			'Live 10 Standard':
				'C:\\ProgramData\\Ableton\\Live 10 Standard\\Program\\Ableton Live 10 Standard.exe',
		},
	}

	/**
	 * @param {Object} [config={}] - Reserved for future configuration options.
	 * @throws {Error} When not running on Windows or WSL.
	 */
	constructor(config = {}) {
		// console.log('os', os.platform())
		this.platform = os.platform()
		this.isWsl = isRunningInWsl()
		this.username = os.userInfo().username

		this.LIVE_PATHS.Live_Preferences = `C:\\Users\\${this.username}\\AppData\\Roaming\\Ableton`

		// console.log('wsl?', this.isWsl, 'platform', this.platform)

		if (this.platform !== 'win32' && this.isWsl === false) {
			throw new Error(
				'AbletonInfoWin64 can only be used on Windows operating systems',
			)
		}
	}

	// async get installedVersions() {
	// 	return await this._getAbletonInstallations()
	// }

	/**
	 * Enumerates Ableton Live installations found under `LIVE_PATHS.Install_Root`.
	 * @returns {Promise<string|Array>} An array of version promises, or an error message string
	 *   if the install root does not exist.
	 */
	async _getAbletonInstallations() {
		let dirs = []
		try {
			await fs.access(this.LIVE_PATHS.Install_Root)
		} catch (Err) {
			return `Install root path not found: ${this.LIVE_PATHS.Install_Root}, is Ableton Live installed?`
		}

		let arr = await fs.readdir(this.LIVE_PATHS.Install_Root)

		let versionMap = arr.map(async (version) => {
			if (this.LIVE_PATHS.Executables[version]) {
				return await this._getAbletonVersionFromExe(
					this.LIVE_PATHS.Executables[version],
				)
			}
		})

		return await versionMap
	}

	/**
	 * Returns Windows version information derived from `os.release()`.
	 * @returns {WindowsVersionInfo}
	 * @throws {Error} When the detected Windows build number is below 10240 (pre-Windows 10).
	 */
	getWindowsVersion() {
		let strRelease = os.release()
		let majorVersion = parseInt(strRelease.split('.')[0], 10)
		let buildNumber = parseInt(strRelease.split('.')[2], 10)

		if (buildNumber < 10240) {
			throw new Error(
				`Unsupported Windows version: ${strRelease}. This library only supports Windows 10 and 11.`,
			)
		}

		// Windows 10 has build numbers starting from 10240, while Windows 11 starts from 26000
		let productName = buildNumber >= 26000 ? 'Windows 11' : 'Windows 10'

		return {
			majorVersion,
			buildNumber,
			productName,
		}
	}

	/**
	 * Retrieves the product version of an Ableton Live executable via PowerShell.
	 * @param {string} exePath - Absolute path to the `.exe` file.
	 * @returns {Promise<string|null>} The version string, or `null` on failure.
	 */
	async getLiveVersionFromExe(exePath) {
		try {
			const command = `"(Get-Item '${exePath}').VersionInfo.ProductVersion"`
			return await runPowerShellCommand(command)
		} catch (Err) {
			console.error(`Error getting version from ${exePath}:`, Err)
			return null
		}
	}
}

/**
 * Executes a PowerShell command synchronously and returns its trimmed output.
 * @param {string} command - The PowerShell command string to execute.
 * @returns {string} The stdout output, trimmed of surrounding whitespace.
 * @throws {Error} When PowerShell exits with a non-zero status.
 */
export function runPowerShellCommand(command) {
	try {
		let result = execSync(
			`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command ${command}`,
			{ windowsHide: true },
		)

		return result.toString().trim()
	} catch (Err) {
		throw Err
	}
}