Files
DcsMissionScriptingTools/dutchies-dcs-scripting-tools/src/lua-addons-manager.ts
T

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 });
}
}