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