Create multiple workflows for different actions

This commit is contained in:
2026-09-10 14:08:13 +02:00
parent 7a59237d45
commit 1d7389f3e8
24 changed files with 2508 additions and 839 deletions
+1 -1
View File
@@ -15,7 +15,7 @@
"publisher": "dutchie031",
"license": "MIT",
"engines": {
"vscode": "1.108.1"
"vscode": "^1.108.1"
},
"repository": {
"type": "git",
+271 -28
View File
@@ -3,37 +3,52 @@
import * as vscode from 'vscode';
import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger } from 'dcs-script-compiler';
import { Logger } from './logger';
import * as luaAddonsManager from './lua-addons-manager';
const luaWorkSpaceSettingKey = "Lua.workspace";
const librarySettingsKey = "library";
const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-transpiler');
let pluginPath : string | undefined;
const logger = new Logger();
const extensionName = 'dutchies-dcs-scripting-tools';
const publisherName = 'dutchie031';
const extensionSettingsFilter = `@ext:${publisherName}.${extensionName}`;
// State tracking for addon updates
let extensionPath: string | undefined;
let workspaceRoot: string | undefined;
let currentExtensionVersion: string | undefined;
let installedAddonPaths: Map<string, string> = new Map();
let isUpdatingAddons = false;
//TODO:
// - ENABLE/DISABLE with settings instead of commands (or both)
// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {
pluginPath = context.asAbsolutePath('lua-addons');
extensionPath = context.extensionPath;
workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
// Initialize extension version
try {
currentExtensionVersion = await luaAddonsManager.getExtensionVersion(extensionPath);
logger.debug(`Extension version: ${currentExtensionVersion}`);
} catch (err) {
logger.error(`Failed to determine extension version: ${err instanceof Error ? err.message : String(err)}`);
}
context.subscriptions.push(
vscode.commands.registerCommand('dutchies-dcs-scripting-tools.enable', async () => {
enableIntellisense();
await enableIntellisense();
})
);
context.subscriptions.push(
vscode.commands.registerCommand('dutchies-dcs-scripting-tools.disable', async () => {
disableIntellisense();
await disableIntellisense();
})
);
@@ -71,20 +86,28 @@ export function activate(context: vscode.ExtensionContext) {
const config = vscode.workspace.getConfiguration(extensionName);
const dcsTypesEnabled = config.get<boolean>('dcsTypes') || false;
if (dcsTypesEnabled) {
addPluginPathToSettings();
await updateLuaAddons();
} else {
removePluginPathFromSettings();
await disableIntellisense();
}
}
});
// Check and update lua-addons on activation if dcsTypes is enabled
await checkAndUpdateAddonsOnStartup();
logger.info('Dutchies DCS Scripting Tools extension activated');
}
// This method is called when your extension is deactivated
export function deactivate()
export async function deactivate()
{
removePluginPathFromSettings();
// Clean up addon paths from settings on deactivation
try {
await removeVersionedPluginPathsFromSettings();
} catch (err) {
logger.error(`Error cleaning up on deactivation: ${err instanceof Error ? err.message : String(err)}`);
}
logger.info('Dutchies DCS Scripting Tools extension deactivated');
}
@@ -162,11 +185,11 @@ async function enableIntellisense() {
config.update("dcsTypes", true, vscode.ConfigurationTarget.Workspace);
}
addPluginPathToSettings();
await updateLuaAddons();
await vscode.commands.executeCommand(
"lua.startServer"
);
vscode.window.showInformationMessage('Dutchies DCS Scripting Tools enabled.');
vscode.window.showInformationMessage(`DCS-Types installed. Version ${currentExtensionVersion}.`);
}
async function disableIntellisense() {
@@ -175,32 +198,252 @@ async function disableIntellisense() {
config.update("dcsTypes", false, vscode.ConfigurationTarget.Workspace);
}
removePluginPathFromSettings();
await removeVersionedPluginPathsFromSettings();
// Clean up all addon versions from workspace
if (workspaceRoot) {
try {
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath || '');
for (const addonName of discoveredAddons.keys()) {
await luaAddonsManager.deleteAllVersionsOfAddon(workspaceRoot, addonName);
logger.debug(`Deleted all versions of ${addonName} from workspace.`);
}
} catch (err) {
logger.error(`Failed to clean up addons: ${err instanceof Error ? err.message : String(err)}`);
}
}
await vscode.commands.executeCommand(
"lua.startServer"
);
vscode.window.showInformationMessage('Dutchies DCS Scripting Tools disabled.');
}
function addPluginPathToSettings() {
if (pluginPath) {
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
const librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
if (!librarySettings.includes(pluginPath)) {
librarySettings.push(pluginPath);
luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
/**
* Updates lua-addons by copying them from the extension bundle to the workspace
* with versioned naming, cleaning old versions, and updating Lua settings
*/
async function updateLuaAddons(): Promise<void> {
// Prevent concurrent update operations
if (isUpdatingAddons) {
logger.debug('Addon update already in progress, skipping.');
return;
}
if (!extensionPath || !workspaceRoot || !currentExtensionVersion) {
logger.warn('Cannot update addons: extension path, workspace root, or version not available.');
return;
}
isUpdatingAddons = true;
try {
logger.info('Starting lua-addons update...');
// Discover bundled addons
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath);
if (discoveredAddons.size === 0) {
logger.info('No lua-addons found in extension bundle.');
isUpdatingAddons = false;
return;
}
logger.debug(`Discovered ${discoveredAddons.size} lua-addon(s): ${Array.from(discoveredAddons.keys()).join(', ')}`);
// Copy addons with versioned names
const copiedAddons = await luaAddonsManager.copyLuaAddons(
extensionPath,
workspaceRoot,
currentExtensionVersion,
discoveredAddons
);
// Store paths for settings management
installedAddonPaths = copiedAddons;
logger.debug(`Copied ${copiedAddons.size} addon(s) to workspace.`);
// Clean up old versions
for (const addonName of discoveredAddons.keys()) {
await luaAddonsManager.deleteOldVersions(workspaceRoot, addonName, currentExtensionVersion);
logger.debug(`Cleaned old versions of ${addonName}.`);
}
// Update Lua settings with new paths
await addVersionedPluginPathsToSettings(copiedAddons);
logger.info(`Lua-addons update completed successfully (version ${currentExtensionVersion}).`);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
logger.error(`Failed to update lua-addons: ${errorMsg}`);
vscode.window.showErrorMessage(`Failed to update Dcs Lua Types: ${errorMsg}`);
} finally {
isUpdatingAddons = false;
}
}
function removePluginPathFromSettings() {
if (pluginPath) {
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
const librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
const libIndex = librarySettings.indexOf(pluginPath);
if (libIndex !== -1) {
librarySettings.splice(libIndex, 1);
luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
/**
* Checks on extension startup if lua-addons need to be updated
* Runs only if dcsTypes is enabled in the workspace
*/
async function checkAndUpdateAddonsOnStartup(): Promise<void> {
if (!extensionPath || !workspaceRoot || !currentExtensionVersion) {
logger.debug('Skipping addon version check on startup: missing initialization data.');
return;
}
try {
const config = vscode.workspace.getConfiguration(extensionName);
const dcsTypesEnabled = config.get<boolean>('dcsTypes') || false;
if (!dcsTypesEnabled) {
logger.debug('dcsTypes not enabled, skipping addon check on startup.');
return;
}
logger.debug('Checking lua-addons versions on startup...');
// Get installed addons
const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot);
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath);
// Check each discovered addon
let updateNeeded = false;
for (const addonName of discoveredAddons.keys()) {
const installed = installedAddons.get(addonName);
const installedVersion = installed?.version;
if (luaAddonsManager.requiresUpdate(installedVersion, currentExtensionVersion)) {
logger.info(
`Version mismatch for ${addonName}: installed=${installedVersion || 'none'}, current=${currentExtensionVersion}`
);
updateNeeded = true;
break;
}
}
if (updateNeeded) {
logger.info('Lua-addons update needed, prompting user...');
const userChoice = await vscode.window.showInformationMessage(
'DCS-Types are not up to date with the current extension version. Do you want to update them?',
{ modal: false },
'Update'
);
if (userChoice === 'Update') {
await updateLuaAddons();
vscode.window.showInformationMessage(`DCS-Types updated to version ${currentExtensionVersion}.`);
} else {
logger.info('User declined DCS-Types update.');
}
} else {
logger.debug('Lua-addons versions are current, no update needed.');
// Ensure paths are in settings even if no update needed
const pathsMap = new Map<string, string>();
for (const [name, info] of installedAddons.entries()) {
pathsMap.set(name, info.path);
}
installedAddonPaths = pathsMap;
await addVersionedPluginPathsToSettings(installedAddonPaths);
}
} catch (err) {
logger.error(
`Error checking lua-addons on startup: ${err instanceof Error ? err.message : String(err)}`
);
// Don't fail activation if check fails; user can manually enable/disable
}
}
/**
* Converts an absolute path to a workspace-relative path using ${workspaceFolder}
*/
function convertToWorkspaceFolderPath(absolutePath: string): string {
if (!workspaceRoot) {
return absolutePath;
}
// Normalize paths for comparison (handle both forward and back slashes)
const normalizedAbsolute = absolutePath.replace(/\\/g, '/');
const normalizedWorkspace = workspaceRoot.replace(/\\/g, '/');
if (normalizedAbsolute.startsWith(normalizedWorkspace)) {
const relativePath = normalizedAbsolute.substring(normalizedWorkspace.length);
// Remove leading slash if present
const cleanPath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
return `\${workspaceFolder}/${cleanPath}`;
}
return absolutePath;
}
/**
* Adds versioned addon paths to the Lua workspace library settings
* @param addonPaths - Map of addon name to installed path
*/
async function addVersionedPluginPathsToSettings(addonPaths?: Map<string, string>): Promise<void> {
const pathsToAdd = addonPaths || installedAddonPaths;
if (pathsToAdd.size === 0) {
logger.debug('No addon paths to add to settings.');
return;
}
try {
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
// Add each addon path if not already present
for (const addonPath of pathsToAdd.values()) {
const workspaceFolderPath = convertToWorkspaceFolderPath(addonPath);
if (!librarySettings.includes(workspaceFolderPath)) {
librarySettings.push(workspaceFolderPath);
logger.debug(`Added addon path to settings: ${workspaceFolderPath}`);
}
}
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
} catch (err) {
logger.error(`Failed to add addon paths to settings: ${err instanceof Error ? err.message : String(err)}`);
throw err;
}
}
/**
* Removes all addon paths from the Lua workspace library settings
*/
async function removeVersionedPluginPathsFromSettings(): Promise<void> {
try {
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
if (workspaceRoot) {
// Get all installed addons to know what paths to remove
const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot);
for (const addonInfo of installedAddons.values()) {
// Try both absolute and workspace-relative paths for compatibility
const absolutePath = addonInfo.path;
const relativePath = convertToWorkspaceFolderPath(absolutePath);
// Remove absolute path if present
let index = librarySettings.indexOf(absolutePath);
if (index !== -1) {
librarySettings.splice(index, 1);
logger.debug(`Removed addon path from settings: ${absolutePath}`);
}
// Remove relative path if present
index = librarySettings.indexOf(relativePath);
if (index !== -1) {
librarySettings.splice(index, 1);
logger.debug(`Removed addon path from settings: ${relativePath}`);
}
}
}
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
} catch (err) {
logger.error(`Failed to remove addon paths from settings: ${err instanceof Error ? err.message : String(err)}`);
throw err;
}
}
@@ -20,6 +20,14 @@ export class Logger {
this.outputChannel.appendLine(`[${new Date().toISOString()}][ERROR] ${message}`);
}
debug(message: string) {
this.outputChannel.appendLine(`[${new Date().toISOString()}][DEBUG] ${message}`);
}
warn(message: string) {
this.outputChannel.appendLine(`[${new Date().toISOString()}][WARN] ${message}`);
}
clear() {
this.outputChannel.clear();
}
@@ -0,0 +1,354 @@
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);
}
}
}
@@ -0,0 +1,310 @@
import * as assert from 'assert';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import { existsSync } from 'fs';
import * as luaAddonsManager from '../lua-addons-manager';
/**
* Integration tests for lua-addons-manager
* Uses temporary directories to avoid polluting the file system
*/
suite('lua-addons-manager', () => {
let tempDir: string;
suiteSetup(async () => {
tempDir = path.join(os.tmpdir(), `dcs-test-${Date.now()}-${Math.random()}`);
});
suiteTeardown(async () => {
if (existsSync(tempDir)) {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
suite('getExtensionVersion', () => {
test('should read version from package.json', async () => {
const testDir = path.join(tempDir, 'getExtensionVersion-1');
await fs.mkdir(testDir, { recursive: true });
const packageJson = path.join(testDir, 'package.json');
await fs.writeFile(packageJson, JSON.stringify({ version: '1.2.3' }));
const version = await luaAddonsManager.getExtensionVersion(testDir);
assert.strictEqual(version, '1.2.3');
});
test('should throw error if package.json not found', async () => {
const testDir = path.join(tempDir, 'getExtensionVersion-2');
await fs.mkdir(testDir, { recursive: true });
await assert.rejects(
() => luaAddonsManager.getExtensionVersion(testDir),
/Failed to read extension version/
);
});
test('should throw error if version field missing', async () => {
const testDir = path.join(tempDir, 'getExtensionVersion-3');
await fs.mkdir(testDir, { recursive: true });
const packageJson = path.join(testDir, 'package.json');
await fs.writeFile(packageJson, JSON.stringify({ name: 'test' }));
await assert.rejects(
() => luaAddonsManager.getExtensionVersion(testDir),
/Version field not found/
);
});
});
suite('discoverLuaAddons', () => {
test('should discover lua-addons directories', async () => {
const testDir = path.join(tempDir, 'discoverLuaAddons-1');
const luaAddonsDir = path.join(testDir, 'lua-addons');
await fs.mkdir(path.join(luaAddonsDir, 'addon1'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'addon2'), { recursive: true });
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
assert.strictEqual(addons.size, 2);
assert.strictEqual(addons.has('addon1'), true);
assert.strictEqual(addons.has('addon2'), true);
});
test('should return empty map if lua-addons directory does not exist', async () => {
const testDir = path.join(tempDir, 'discoverLuaAddons-2');
await fs.mkdir(testDir, { recursive: true });
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
assert.strictEqual(addons.size, 0);
});
test('should ignore non-directory entries', async () => {
const testDir = path.join(tempDir, 'discoverLuaAddons-3');
const luaAddonsDir = path.join(testDir, 'lua-addons');
await fs.mkdir(luaAddonsDir, { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'addon1'), { recursive: true });
await fs.writeFile(path.join(luaAddonsDir, 'file.txt'), 'test');
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
assert.strictEqual(addons.size, 1);
assert.strictEqual(addons.has('addon1'), true);
});
});
suite('getWorkspaceLuaAddonsDir', () => {
test('should return correct .vscode/lua-addons path', () => {
const workspaceRoot = '/path/to/workspace';
const result = luaAddonsManager.getWorkspaceLuaAddonsDir(workspaceRoot);
assert.strictEqual(result, path.join(workspaceRoot, '.vscode', 'lua-addons'));
});
});
suite('getInstalledVersions', () => {
test('should parse installed addon versions from folder names', async () => {
const testDir = path.join(tempDir, 'getInstalledVersions-1');
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'mission-utils.1.2.3'), { recursive: true });
const installed = await luaAddonsManager.getInstalledVersions(testDir);
assert.strictEqual(installed.size, 2);
assert.strictEqual(installed.has('dcs-types'), true);
assert.strictEqual(installed.has('mission-utils'), true);
assert.strictEqual(installed.get('dcs-types')?.version, '0.0.5');
assert.strictEqual(installed.get('mission-utils')?.version, '1.2.3');
});
test('should return empty map if directory does not exist', async () => {
const testDir = path.join(tempDir, 'getInstalledVersions-2');
await fs.mkdir(testDir, { recursive: true });
const installed = await luaAddonsManager.getInstalledVersions(testDir);
assert.strictEqual(installed.size, 0);
});
test('should ignore folders with invalid version format', async () => {
const testDir = path.join(tempDir, 'getInstalledVersions-3');
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'invalid-addon'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'bad-format.v1'), { recursive: true });
const installed = await luaAddonsManager.getInstalledVersions(testDir);
assert.strictEqual(installed.size, 1);
assert.strictEqual(installed.has('dcs-types'), true);
assert.strictEqual(installed.has('invalid-addon'), false);
assert.strictEqual(installed.has('bad-format'), false);
});
});
suite('copyLuaAddons', () => {
test('should copy addons with versioned folder names', async () => {
const testDir = path.join(tempDir, 'copyLuaAddons-1');
const sourceAddon1 = path.join(testDir, 'source-addons', 'addon1');
await fs.mkdir(sourceAddon1, { recursive: true });
await fs.writeFile(path.join(sourceAddon1, 'file1.lua'), 'content1');
const addonsToCopy = new Map<string, string>([
['addon1', sourceAddon1]
]);
const result = await luaAddonsManager.copyLuaAddons(
testDir,
testDir,
'1.0.0',
addonsToCopy
);
assert.strictEqual(result.size, 1);
const copiedPath = result.get('addon1');
assert.strictEqual(copiedPath !== undefined, true);
if (copiedPath) {
assert.strictEqual(copiedPath.includes('addon1.1.0.0'), true);
assert.strictEqual(existsSync(path.join(copiedPath, 'file1.lua')), true);
}
});
test('should create .vscode directory if it does not exist', async () => {
const testDir = path.join(tempDir, 'copyLuaAddons-2');
const sourceAddon = path.join(testDir, 'source', 'addon1');
await fs.mkdir(sourceAddon, { recursive: true });
const addonsToCopy = new Map<string, string>([
['addon1', sourceAddon]
]);
await luaAddonsManager.copyLuaAddons(
testDir,
testDir,
'1.0.0',
addonsToCopy
);
const vscodeDir = path.join(testDir, '.vscode');
assert.strictEqual(existsSync(vscodeDir), true);
});
test('should copy nested directories recursively', async () => {
const testDir = path.join(tempDir, 'copyLuaAddons-3');
const sourceAddon = path.join(testDir, 'source', 'addon1');
await fs.mkdir(path.join(sourceAddon, 'subdir'), { recursive: true });
await fs.writeFile(path.join(sourceAddon, 'file.lua'), 'root');
await fs.writeFile(path.join(sourceAddon, 'subdir', 'file.lua'), 'nested');
const addonsToCopy = new Map<string, string>([
['addon1', sourceAddon]
]);
const result = await luaAddonsManager.copyLuaAddons(
testDir,
testDir,
'1.0.0',
addonsToCopy
);
const copiedPath = result.get('addon1')!;
assert.strictEqual(existsSync(path.join(copiedPath, 'file.lua')), true);
assert.strictEqual(existsSync(path.join(copiedPath, 'subdir', 'file.lua')), true);
});
});
suite('deleteOldVersions', () => {
test('should delete old versions of an addon', async () => {
const testDir = path.join(tempDir, 'deleteOldVersions-1');
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.3'), { recursive: true });
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.3');
const remaining = await fs.readdir(luaAddonsDir);
assert.deepStrictEqual(remaining.sort(), ['addon1.0.0.3']);
});
test('should not delete other addons', async () => {
const testDir = path.join(tempDir, 'deleteOldVersions-2');
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'addon2.0.0.1'), { recursive: true });
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.2');
const remaining = await fs.readdir(luaAddonsDir);
assert.strictEqual(remaining.includes('addon1.0.0.2'), true);
assert.strictEqual(remaining.includes('addon2.0.0.1'), true);
assert.strictEqual(remaining.includes('addon1.0.0.1'), false);
});
test('should handle missing directory gracefully', async () => {
const testDir = path.join(tempDir, 'deleteOldVersions-3');
await fs.mkdir(testDir, { recursive: true });
// Should not throw
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.1');
});
});
suite('deleteAllVersionsOfAddon', () => {
test('should delete all versions of an addon', async () => {
const testDir = path.join(tempDir, 'deleteAllVersionsOfAddon-1');
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
await fs.mkdir(path.join(luaAddonsDir, 'addon2.0.0.1'), { recursive: true });
await luaAddonsManager.deleteAllVersionsOfAddon(testDir, 'addon1');
const remaining = await fs.readdir(luaAddonsDir);
assert.deepStrictEqual(remaining.sort(), ['addon2.0.0.1']);
});
});
suite('requiresUpdate', () => {
test('should return true if no version installed', () => {
const result = luaAddonsManager.requiresUpdate(undefined, '1.0.0');
assert.strictEqual(result, true);
});
test('should return true if versions do not match', () => {
const result = luaAddonsManager.requiresUpdate('1.0.0', '1.0.1');
assert.strictEqual(result, true);
});
test('should return false if versions match', () => {
const result = luaAddonsManager.requiresUpdate('1.0.0', '1.0.0');
assert.strictEqual(result, false);
});
});
suite('getInstalledAddons', () => {
test('should return installed addon information', async () => {
const testDir = path.join(tempDir, 'getInstalledAddons-1');
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
const installed = await luaAddonsManager.getInstalledAddons(testDir);
assert.strictEqual(installed.size, 1);
const addonInfo = installed.get('dcs-types');
assert.strictEqual(addonInfo !== undefined, true);
if (addonInfo) {
assert.strictEqual(addonInfo.name, 'dcs-types');
assert.strictEqual(addonInfo.version, '0.0.5');
assert.strictEqual(addonInfo.path.includes('dcs-types.0.0.5'), true);
}
});
});
});