import path from "path"; import * as fs from 'fs/promises'; import { existsSync } from 'fs'; /* * Lua addon naming conventions: * ... */ export class LuaAddonsManager { /** Path where addons are installed */ private targetPath: string; /** Path where addon sources are located */ private sourceExtensionPath: string; private extensionContextPath: string; /** * Creates a new LuaAddonsManager instance. * * @param extensionPath - Directory where addons are installed * @param sourceExtensionPath - Directory where addon sources are located */ public constructor(luaTargetPath: string, extensionPath: string) { this.targetPath = luaTargetPath; this.sourceExtensionPath = path.join(extensionPath, "lua-addons"); this.extensionContextPath = extensionPath; } /** * Retrieves the version of an installed addon by name. * * Searches for a folder matching the naming pattern `...` * and returns the version string. * * @param name - The addon name to search for (without version suffix) * @returns The version string in format "major.minor.patch", or undefined if not found * @throws Never throws, returns undefined if addon not found */ async getExtensionVersion(name: string): Promise { const folders = await fs.readdir(this.targetPath, { withFileTypes: true }); for (const folder of folders) { if (!folder.isDirectory()) { continue; } const versionMatch = folder.name.match(/^(.+)\.(\d+)\.(\d+)\.(\d+)$/); if (!versionMatch) { continue; } const addonName = versionMatch[1]; if (addonName === name) { return `${versionMatch[2]}.${versionMatch[3]}.${versionMatch[4]}`; } } return undefined; } /** * Reads the version from the package.json file in the extension path. * * @returns The version string from package.json * @throws Error if package.json cannot be read or is missing the version field */ async getPackageVersion(): Promise { try { const packageJsonPath = path.join(this.extensionContextPath,'package.json'); const content = await fs.readFile(packageJsonPath, 'utf-8'); const packageJson = JSON.parse(content); const version = packageJson.version as string | undefined; if (!version) { throw new Error('Version field not found in package.json'); } return version; } catch (err) { throw new Error( `Failed to read extension version: ${err instanceof Error ? err.message : String(err)}` ); } } /** * Removes a single addon by name. * * Deletes the addon directory matching the given name, regardless of version. * * @param name - The addon name to remove (without version suffix) * @throws Error if the removal operation fails */ async removeExtension(name: string): Promise { return this.removeAllExtensions([name]); } /** * Removes multiple addons by name. * * Scans the extension directory for folders matching the addon naming pattern * `...` and removes those whose base name is in the * provided list. Removes all versions of matched addons. * * @param names - Array of addon names to remove (without version suffix) * @throws Error if the removal operation fails */ async removeAllExtensions(names: string[]): Promise { const folders = await fs.readdir(this.targetPath, { withFileTypes: true }); for (const folder of folders) { if (!folder.isDirectory()) { continue; } // Check if folder name matches versioning pattern: name.major.minor.patch const versionMatch = folder.name.match(/^(.+)\.(\d+)\.(\d+)\.(\d+)$/); if (!versionMatch) { continue; } // Extract addon name from the versioned folder name const addonName = versionMatch[1]; if (names.includes(addonName)) { const folderPath = path.join(this.targetPath, folder.name); await fs.rm(folderPath, { recursive: true, force: true }); } } } /** * Installs an addon from source to the target installation directory. * * Copies the addon directory from sourceExtensionPath to extensionPath. * The source directory name should match the addon name. * * @param name - The addon name/directory to install (without version suffix) * @throws Error if source directory not found or copy operation fails */ async installExtension(name: string): Promise { const sourceFolderPath = path.join(this.sourceExtensionPath, name); const version = await this.getPackageVersion(); const versionedName = `${name}.${version}`; const targetFolderPath = path.join(this.targetPath, versionedName); if (!existsSync(sourceFolderPath)) { throw new Error(`Source extension folder not found: ${sourceFolderPath}`); } await fs.mkdir(targetFolderPath, { recursive: true }); await fs.cp(sourceFolderPath, targetFolderPath, { recursive: true }); } }