149 lines
5.4 KiB
TypeScript
149 lines
5.4 KiB
TypeScript
import path from "path";
|
|
import * as fs from 'fs/promises';
|
|
import { existsSync } from 'fs';
|
|
|
|
/*
|
|
* Lua addon naming conventions:
|
|
* <addon-name>.<major>.<minor>.<patch>
|
|
*/
|
|
|
|
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 `<name>.<major>.<minor>.<patch>`
|
|
* 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<string | undefined> {
|
|
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<string | undefined> {
|
|
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<void> {
|
|
return this.removeAllExtensions([name]);
|
|
}
|
|
|
|
/**
|
|
* Removes multiple addons by name.
|
|
*
|
|
* Scans the extension directory for folders matching the addon naming pattern
|
|
* `<name>.<major>.<minor>.<patch>` 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<void> {
|
|
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<void> {
|
|
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 });
|
|
}
|
|
|
|
} |