Added Required logic if not found, and added better update logic
This commit is contained in:
@@ -1,354 +1,149 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
/**
|
||||
* Manages lua-addons: discovery, versioning, copying, and cleanup.
|
||||
* All operations assume semver versioning (X.Y.Z format).
|
||||
* Addon folder names follow the pattern: `<addon-name>.<version>` (e.g., dcs-types.0.0.5)
|
||||
*/
|
||||
|
||||
interface LuaAddonInfo {
|
||||
name: string;
|
||||
sourceDir: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface InstalledAddonInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the extension's version from package.json
|
||||
* @param extensionRoot - Absolute path to the extension root directory
|
||||
* @returns The semantic version string (e.g., "0.0.5")
|
||||
* @throws Error if package.json cannot be read or version is not found
|
||||
*/
|
||||
export async function getExtensionVersion(extensionRoot: string): Promise<string> {
|
||||
try {
|
||||
const packageJsonPath = path.join(extensionRoot, '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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers all lua-addons in the bundled lua-addons directory
|
||||
* @param extensionPath - Absolute path to the extension installation directory
|
||||
* @returns Map of addon name to source directory path
|
||||
* @throws Error if lua-addons directory cannot be read
|
||||
*/
|
||||
export async function discoverLuaAddons(extensionPath: string): Promise<Map<string, string>> {
|
||||
const luaAddonsDir = path.join(extensionPath, 'lua-addons');
|
||||
const addons = new Map<string, string>();
|
||||
|
||||
try {
|
||||
// Check if lua-addons directory exists
|
||||
if (!existsSync(luaAddonsDir)) {
|
||||
return addons; // Empty map if no lua-addons
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const addonName = entry.name;
|
||||
const addonPath = path.join(luaAddonsDir, addonName);
|
||||
addons.set(addonName, addonPath);
|
||||
}
|
||||
}
|
||||
|
||||
return addons;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to discover lua-addons: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the workspace lua-addons directory path (.vscode/lua-addons)
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @returns Absolute path to .vscode/lua-addons
|
||||
*/
|
||||
export function getWorkspaceLuaAddonsDir(workspaceRoot: string): string {
|
||||
return path.join(workspaceRoot, '.vscode', 'lua-addons');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses installed addon versions from the workspace lua-addons directory
|
||||
* Expects folder names in format: `<addon-name>.<version>` (e.g., dcs-types.0.0.5)
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @returns Map of addon name to version string; returns empty map if directory doesn't exist
|
||||
* @throws Error if directory cannot be read
|
||||
*/
|
||||
export async function getInstalledVersions(
|
||||
workspaceRoot: string
|
||||
): Promise<Map<string, InstalledAddonInfo>> {
|
||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||
const installed = new Map<string, InstalledAddonInfo>();
|
||||
|
||||
try {
|
||||
// Return empty map if directory doesn't exist yet
|
||||
if (!existsSync(luaAddonsDir)) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const folderName = entry.name;
|
||||
const parsed = parseAddonFolderName(folderName);
|
||||
|
||||
if (parsed) {
|
||||
installed.set(parsed.name, {
|
||||
name: parsed.name,
|
||||
version: parsed.version,
|
||||
path: path.join(luaAddonsDir, folderName)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return installed;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to read installed versions: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies lua-addons from the extension bundle to the workspace with versioned folder names
|
||||
* @param extensionPath - Absolute path to the extension installation directory
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @param extensionVersion - The current extension version string
|
||||
* @param addonsToCopy - Map of addon name to source directory (from discoverLuaAddons)
|
||||
* @returns Map of addon name to installed path in workspace
|
||||
* @throws Error if copy fails or directory creation fails
|
||||
*/
|
||||
export async function copyLuaAddons(
|
||||
extensionPath: string,
|
||||
workspaceRoot: string,
|
||||
extensionVersion: string,
|
||||
addonsToCopy: Map<string, string>
|
||||
): Promise<Map<string, string>> {
|
||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||
const results = new Map<string, string>();
|
||||
|
||||
try {
|
||||
// Ensure .vscode directory exists
|
||||
const vscodeDir = path.join(workspaceRoot, '.vscode');
|
||||
await ensureDirectoryExists(vscodeDir);
|
||||
|
||||
// Ensure lua-addons directory exists
|
||||
await ensureDirectoryExists(luaAddonsDir);
|
||||
|
||||
// Copy each addon
|
||||
for (const [addonName, sourceDir] of addonsToCopy) {
|
||||
const versionedFolderName = `${addonName}.${extensionVersion}`;
|
||||
const destDir = path.join(luaAddonsDir, versionedFolderName);
|
||||
|
||||
// Remove destination if it already exists (shouldn't happen, but be safe)
|
||||
if (existsSync(destDir)) {
|
||||
await fs.rm(destDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Copy the addon
|
||||
await copyDirectory(sourceDir, destDir);
|
||||
results.set(addonName, destDir);
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to copy lua-addons: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all old versions of a specific addon
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @param addonName - Name of the addon (e.g., "dcs-types")
|
||||
* @param currentVersion - Version to keep (e.g., "0.0.5")
|
||||
* @throws Error if directory operations fail
|
||||
*/
|
||||
export async function deleteOldVersions(
|
||||
workspaceRoot: string,
|
||||
addonName: string,
|
||||
currentVersion: string
|
||||
): Promise<void> {
|
||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||
|
||||
try {
|
||||
// Return silently if directory doesn't exist
|
||||
if (!existsSync(luaAddonsDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const parsed = parseAddonFolderName(entry.name);
|
||||
|
||||
// Delete if it's an old version of this addon
|
||||
if (parsed && parsed.name === addonName && parsed.version !== currentVersion) {
|
||||
const oldPath = path.join(luaAddonsDir, entry.name);
|
||||
await fs.rm(oldPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to delete old versions of ${addonName}: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an addon needs to be updated (version mismatch)
|
||||
* @param installedVersion - Currently installed version or undefined if not installed
|
||||
* @param currentExtensionVersion - Current extension version to compare against
|
||||
* @returns true if no version is installed or version doesn't match
|
||||
*/
|
||||
export function requiresUpdate(
|
||||
installedVersion: string | undefined,
|
||||
currentExtensionVersion: string
|
||||
): boolean {
|
||||
return !installedVersion || installedVersion !== currentExtensionVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all versioned addon folders for a specific addon name
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @param addonName - Name of the addon to clean up completely
|
||||
* @throws Error if directory operations fail
|
||||
*/
|
||||
export async function deleteAllVersionsOfAddon(
|
||||
workspaceRoot: string,
|
||||
addonName: string
|
||||
): Promise<void> {
|
||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||
|
||||
try {
|
||||
// Return silently if directory doesn't exist
|
||||
if (!existsSync(luaAddonsDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const parsed = parseAddonFolderName(entry.name);
|
||||
|
||||
// Delete all versions of this addon
|
||||
if (parsed && parsed.name === addonName) {
|
||||
const addonPath = path.join(luaAddonsDir, entry.name);
|
||||
await fs.rm(addonPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to delete all versions of ${addonName}: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently installed addon info for all addons
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @returns Map of addon name to its latest installed version info
|
||||
*/
|
||||
export async function getInstalledAddons(
|
||||
workspaceRoot: string
|
||||
): Promise<Map<string, InstalledAddonInfo>> {
|
||||
return getInstalledVersions(workspaceRoot);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Private Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parses an addon folder name in format `<name>.<version>`
|
||||
* @param folderName - The folder name to parse
|
||||
* @returns Object with name and version, or null if format doesn't match
|
||||
*/
|
||||
function parseAddonFolderName(
|
||||
folderName: string
|
||||
): { name: string; version: string } | null {
|
||||
// Split only on the last dot to handle addon names with dots (unlikely, but safe)
|
||||
const lastDotIndex = folderName.lastIndexOf('.');
|
||||
if (lastDotIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = folderName.substring(0, lastDotIndex);
|
||||
const version = folderName.substring(lastDotIndex + 1);
|
||||
|
||||
// Validate semver format (basic check)
|
||||
if (!/^\d+\.\d+\.\d+/.test(version)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { name, version };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a directory exists, creating it if necessary
|
||||
* @param dirPath - Absolute path to the directory
|
||||
*/
|
||||
async function ensureDirectoryExists(dirPath: string): Promise<void> {
|
||||
try {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
} catch (err) {
|
||||
// Ignore EEXIST errors
|
||||
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copies a directory and its contents
|
||||
* @param srcDir - Source directory path
|
||||
* @param destDir - Destination directory path
|
||||
*/
|
||||
async function copyDirectory(srcDir: string, destDir: string): Promise<void> {
|
||||
await ensureDirectoryExists(destDir);
|
||||
|
||||
const entries = await fs.readdir(srcDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(srcDir, entry.name);
|
||||
const destPath = path.join(destDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
await fs.copyFile(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 });
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user