using System.Management.Automation; using System.Reflection; using System.Text; using System.Text.Json; using LANCommander.SDK.PowerShell.Extensions; if (args.Length == 0) { Console.Error.WriteLine("Usage: LANCommander.CompletionGenerator "); return 1; } var outputPath = args[0]; var assembly = typeof(InitialSessionStateExtensions).Assembly; var cmdletTypes = assembly.GetTypes() .Where(t => t.GetCustomAttribute() != null) .OrderBy(t => t.GetCustomAttribute()!.VerbName + "-" + t.GetCustomAttribute()!.NounName); var sb = new StringBuilder(); sb.AppendLine("// Auto-generated by LANCommander.CompletionGenerator — do not edit manually"); sb.AppendLine(); sb.AppendLine("export interface CmdletParameter {"); sb.AppendLine(" name: string;"); sb.AppendLine(" type: string;"); sb.AppendLine(" mandatory: boolean;"); sb.AppendLine(" position: number | null;"); sb.AppendLine(" helpMessage: string | null;"); sb.AppendLine(" aliases: string[];"); sb.AppendLine("}"); sb.AppendLine(); sb.AppendLine("export interface CmdletDefinition {"); sb.AppendLine(" name: string;"); sb.AppendLine(" description: string | null;"); sb.AppendLine(" outputType: string | null;"); sb.AppendLine(" parameters: CmdletParameter[];"); sb.AppendLine("}"); sb.AppendLine(); sb.AppendLine("export const cmdlets: CmdletDefinition[] = ["); foreach (var type in cmdletTypes) { var cmdletAttr = type.GetCustomAttribute()!; var name = $"{cmdletAttr.VerbName}-{cmdletAttr.NounName}"; var outputTypeAttr = type.GetCustomAttribute(); string? outputType = null; if (outputTypeAttr?.Type?.Length > 0) outputType = GetFriendlyTypeName(outputTypeAttr.Type[0].Type); // Use XML doc summary if available via the Summary property pattern, otherwise use null var descriptionLines = type.GetCustomAttributes() .Where(a => a.GetType().Name == "DescriptionAttribute") .Select(a => a.GetType().GetProperty("Description")?.GetValue(a)?.ToString()) .FirstOrDefault(); // Fall back to XML summary comment — not available via reflection, use class summary convention string? description = descriptionLines; sb.AppendLine(" {"); sb.AppendLine($" name: {JsonEncode(name)},"); sb.AppendLine($" description: {JsonEncode(description)},"); sb.AppendLine($" outputType: {JsonEncode(outputType)},"); sb.AppendLine(" parameters: ["); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.GetCustomAttribute() != null) .OrderBy(p => { var pa = p.GetCustomAttribute()!; return pa.Position == int.MinValue ? int.MaxValue : pa.Position; }) .ThenBy(p => p.Name); foreach (var prop in properties) { var paramAttr = prop.GetCustomAttribute()!; var aliasAttr = prop.GetCustomAttribute(); var aliases = aliasAttr?.AliasNames?.ToArray() ?? Array.Empty(); var position = paramAttr.Position == int.MinValue ? (int?)null : paramAttr.Position; sb.AppendLine(" {"); sb.AppendLine($" name: {JsonEncode(prop.Name)},"); sb.AppendLine($" type: {JsonEncode(GetFriendlyTypeName(prop.PropertyType))},"); sb.AppendLine($" mandatory: {(paramAttr.Mandatory ? "true" : "false")},"); sb.AppendLine($" position: {(position.HasValue ? position.Value.ToString() : "null")},"); sb.AppendLine($" helpMessage: {JsonEncode(paramAttr.HelpMessage)},"); sb.AppendLine($" aliases: [{string.Join(", ", aliases.Select(a => JsonEncode(a)))}],"); sb.AppendLine(" },"); } sb.AppendLine(" ],"); sb.AppendLine(" },"); } sb.AppendLine("];"); sb.AppendLine(); // Generate built-in PowerShell cmdlet completions from core modules var builtinCmdletNames = new HashSet(cmdletTypes.Select(t => { var a = t.GetCustomAttribute()!; return $"{a.VerbName}-{a.NounName}"; })); Console.WriteLine("Enumerating built-in PowerShell cmdlets..."); sb.AppendLine("export const builtinCmdlets: CmdletDefinition[] = ["); var builtinCount = 0; try { // Use external pwsh process for full module metadata and help access var psScript = @" $commonParams = @( 'Verbose','Debug','ErrorAction','WarningAction','InformationAction', 'ErrorVariable','WarningVariable','InformationVariable','OutVariable', 'OutBuffer','PipelineVariable','ProgressAction','Confirm','WhatIf' ) $modules = @( 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Security', 'Microsoft.PowerShell.Archive' ) $results = @() # Get command names from core modules, then resolve each individually for full metadata $commandNames = Get-Command -CommandType Cmdlet,Function -Module $modules -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name -Unique foreach ($cmdName in $commandNames) { $cmd = Get-Command $cmdName -ErrorAction SilentlyContinue if (-not $cmd) { continue } $synopsis = $null try { $h = Get-Help $cmdName -ErrorAction SilentlyContinue if ($h -and $h.Synopsis) { $s = $h.Synopsis.Trim() # Skip if synopsis is just the cmdlet name, or looks like syntax (contains parameter notation) if ($s -ne $cmdName -and $s -notmatch '\[[-<]' -and $s -notmatch '-\w+\s+<') { $synopsis = $s } } } catch {} $outputType = $null if ($cmd.OutputType -and $cmd.OutputType.Count -gt 0 -and $cmd.OutputType[0].Type) { $outputType = $cmd.OutputType[0].Type.Name } $params = @() if ($cmd.Parameters) { foreach ($key in $cmd.Parameters.Keys) { if ($key -in $commonParams) { continue } $p = $cmd.Parameters[$key] $pAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | Select-Object -First 1 $mandatory = $false $position = $null $helpMsg = $null if ($pAttr) { $mandatory = [bool]$pAttr.Mandatory if ($pAttr.Position -ne [int]::MinValue) { $position = $pAttr.Position } if ($pAttr.HelpMessage) { $helpMsg = $pAttr.HelpMessage } } $aliases = @($p.Aliases) $params += @{ n = $key t = $p.ParameterType.Name m = $mandatory pos = $position h = $helpMsg a = $aliases } } } $results += @{ name = $cmdName desc = $synopsis out = $outputType params = $params } } $results | ConvertTo-Json -Depth 4 -Compress "; // Write the script to a temp file to avoid stdin encoding issues var tempScript = Path.GetTempFileName() + ".ps1"; File.WriteAllText(tempScript, psScript, new UTF8Encoding(false)); var psi = new System.Diagnostics.ProcessStartInfo { FileName = "pwsh", Arguments = $"-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"{tempScript}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; // Clear .NET SDK environment variables that can interfere with pwsh's module loading foreach (var key in Environment.GetEnvironmentVariables().Keys.Cast() .Where(k => k.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase) || k.StartsWith("MSBuild", StringComparison.OrdinalIgnoreCase))) { psi.Environment[key] = null; } using var process = System.Diagnostics.Process.Start(psi)!; var json = process.StandardOutput.ReadToEnd(); var stderr = process.StandardError.ReadToEnd(); process.WaitForExit(120_000); if (!string.IsNullOrWhiteSpace(stderr)) Console.Error.WriteLine($" pwsh stderr: {stderr.Trim()}"); try { File.Delete(tempScript); } catch { } if (process.ExitCode != 0) throw new Exception($"pwsh exited with code {process.ExitCode}"); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Handle both array and single-object responses var elements = root.ValueKind == JsonValueKind.Array ? root.EnumerateArray().ToList() : new List { root }; foreach (var cmd in elements.OrderBy(e => e.GetProperty("name").GetString())) { var name = cmd.GetProperty("name").GetString()!; if (builtinCmdletNames.Contains(name)) continue; string? description = null; if (cmd.TryGetProperty("desc", out var descEl) && descEl.ValueKind == JsonValueKind.String) description = descEl.GetString(); string? outputTypeName = null; if (cmd.TryGetProperty("out", out var outEl) && outEl.ValueKind == JsonValueKind.String) outputTypeName = MapTypeName(outEl.GetString()!); sb.AppendLine(" {"); sb.AppendLine($" name: {JsonEncode(name)},"); sb.AppendLine($" description: {JsonEncode(description)},"); sb.AppendLine($" outputType: {JsonEncode(outputTypeName)},"); sb.AppendLine(" parameters: ["); if (cmd.TryGetProperty("params", out var paramsEl) && paramsEl.ValueKind == JsonValueKind.Array) { // Sort: positional first, then alphabetical var paramList = paramsEl.EnumerateArray().ToList(); paramList.Sort((a, b) => { var posA = a.TryGetProperty("pos", out var pa) && pa.ValueKind == JsonValueKind.Number ? pa.GetInt32() : int.MaxValue; var posB = b.TryGetProperty("pos", out var pb) && pb.ValueKind == JsonValueKind.Number ? pb.GetInt32() : int.MaxValue; var cmp = posA.CompareTo(posB); if (cmp != 0) return cmp; return string.Compare( a.GetProperty("n").GetString(), b.GetProperty("n").GetString(), StringComparison.Ordinal); }); foreach (var param in paramList) { var paramName = param.GetProperty("n").GetString()!; var typeName = MapTypeName(param.GetProperty("t").GetString() ?? "object"); var mandatory = param.TryGetProperty("m", out var mEl) && mEl.ValueKind == JsonValueKind.True; string? posStr = "null"; if (param.TryGetProperty("pos", out var posEl) && posEl.ValueKind == JsonValueKind.Number) posStr = posEl.GetInt32().ToString(); string? helpMessage = null; if (param.TryGetProperty("h", out var hEl) && hEl.ValueKind == JsonValueKind.String) helpMessage = hEl.GetString(); var aliases = new List(); if (param.TryGetProperty("a", out var aEl) && aEl.ValueKind == JsonValueKind.Array) { foreach (var alias in aEl.EnumerateArray()) { if (alias.ValueKind == JsonValueKind.String) aliases.Add(alias.GetString()!); } } sb.AppendLine(" {"); sb.AppendLine($" name: {JsonEncode(paramName)},"); sb.AppendLine($" type: {JsonEncode(typeName)},"); sb.AppendLine($" mandatory: {(mandatory ? "true" : "false")},"); sb.AppendLine($" position: {posStr},"); sb.AppendLine($" helpMessage: {JsonEncode(helpMessage)},"); sb.AppendLine($" aliases: [{string.Join(", ", aliases.Select(a => JsonEncode(a)))}],"); sb.AppendLine(" },"); } } sb.AppendLine(" ],"); sb.AppendLine(" },"); builtinCount++; } } catch (Exception ex) { Console.Error.WriteLine($"Warning: Failed to enumerate built-in cmdlets: {ex.Message}"); Console.Error.WriteLine("Ensure 'pwsh' (PowerShell 7+) is installed and on PATH."); } sb.AppendLine("];"); sb.AppendLine(); // Generate type definitions for complex objects used as script variables var variableTypes = new Dictionary { ["GameManifest"] = typeof(LANCommander.SDK.Models.Manifest.Game), ["ToolManifest"] = typeof(LANCommander.SDK.Models.Manifest.Tool), ["RedistributableManifest"] = typeof(LANCommander.SDK.Models.Manifest.Redistributable), ["Server"] = typeof(LANCommander.SDK.Models.Server), ["Game"] = typeof(LANCommander.SDK.Models.Game), ["User"] = typeof(LANCommander.SDK.Models.User), ["Tool"] = typeof(LANCommander.SDK.Models.Tool), ["Redistributable"] = typeof(LANCommander.SDK.Models.Redistributable), }; sb.AppendLine("export interface TypeProperty {"); sb.AppendLine(" name: string;"); sb.AppendLine(" type: string;"); sb.AppendLine("}"); sb.AppendLine(); sb.AppendLine("export interface TypeDefinition {"); sb.AppendLine(" name: string;"); sb.AppendLine(" properties: TypeProperty[];"); sb.AppendLine("}"); sb.AppendLine(); sb.AppendLine("export const variableTypes: TypeDefinition[] = ["); foreach (var (typeName, clrType) in variableTypes.OrderBy(kv => kv.Key)) { sb.AppendLine(" {"); sb.AppendLine($" name: {JsonEncode(typeName)},"); sb.AppendLine(" properties: ["); var props = clrType.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead) .OrderBy(p => p.Name); foreach (var prop in props) { sb.AppendLine(" {"); sb.AppendLine($" name: {JsonEncode(prop.Name)},"); sb.AppendLine($" type: {JsonEncode(GetPropertyTypeName(prop.PropertyType))},"); sb.AppendLine(" },"); } sb.AppendLine(" ],"); sb.AppendLine(" },"); } sb.AppendLine("];"); sb.AppendLine(); // Generate ScriptType enum values sb.AppendLine("export const scriptTypeValues: string[] = ["); foreach (var value in Enum.GetNames()) { sb.AppendLine($" {JsonEncode(value)},"); } sb.AppendLine("];"); var directory = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); File.WriteAllText(outputPath, sb.ToString(), Encoding.UTF8); Console.WriteLine($"Generated completions for {cmdletTypes.Count()} LANCommander cmdlets, {builtinCount} built-in cmdlets, and {variableTypes.Count} variable types -> {outputPath}"); return 0; static string JsonEncode(string? value) { if (value == null) return "null"; return JsonSerializer.Serialize(value); } static string GetPropertyTypeName(Type type) { var underlying = Nullable.GetUnderlyingType(type); if (underlying != null) return GetPropertyTypeName(underlying) + "?"; if (type.IsArray) return GetPropertyTypeName(type.GetElementType()!) + "[]"; if (type.IsGenericType) { var genericDef = type.GetGenericTypeDefinition(); if (genericDef == typeof(IEnumerable<>) || genericDef == typeof(ICollection<>) || genericDef == typeof(List<>) || genericDef == typeof(IList<>)) { var elementType = type.GetGenericArguments()[0]; return GetPropertyTypeName(elementType) + "[]"; } } // Check non-generic IEnumerable if (type != typeof(string) && type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) { var elementType = type.GetInterfaces() .First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)) .GetGenericArguments()[0]; return GetPropertyTypeName(elementType) + "[]"; } if (type == typeof(string)) return "string"; if (type == typeof(int) || type == typeof(long)) return "int"; if (type == typeof(uint)) return "uint"; if (type == typeof(double) || type == typeof(float)) return "double"; if (type == typeof(bool)) return "bool"; if (type == typeof(Guid)) return "Guid"; if (type == typeof(DateTime)) return "DateTime"; if (type == typeof(Uri)) return "Uri"; if (type.IsEnum) return type.Name; return type.Name; } static string MapTypeName(string typeName) => typeName switch { "String" => "string", "Int32" => "int", "Int64" => "long", "UInt32" => "uint", "Double" => "double", "Single" => "float", "Boolean" => "bool", "Byte" => "byte", "Guid" => "Guid", "Uri" => "Uri", "DateTime" => "DateTime", "Object" => "object", "SwitchParameter" => "SwitchParameter", "SecureString" => "SecureString", "String[]" => "string[]", "Int32[]" => "int[]", "Object[]" => "object[]", "Byte[]" => "byte[]", "PSObject" => "object", "PSObject[]" => "object[]", "Hashtable" => "Hashtable", "ScriptBlock" => "ScriptBlock", "TimeSpan" => "TimeSpan", "PSCredential" => "PSCredential", _ => typeName, }; static string GetFriendlyTypeName(Type? type) { if (type == null) return "object"; // Handle nullable types var underlying = Nullable.GetUnderlyingType(type); if (underlying != null) return GetFriendlyTypeName(underlying) + "?"; // Handle arrays if (type.IsArray) return GetFriendlyTypeName(type.GetElementType()) + "[]"; // Handle SwitchParameter if (type == typeof(SwitchParameter)) return "SwitchParameter"; // Handle common types if (type == typeof(string)) return "string"; if (type == typeof(int)) return "int"; if (type == typeof(long)) return "long"; if (type == typeof(uint)) return "uint"; if (type == typeof(double)) return "double"; if (type == typeof(float)) return "float"; if (type == typeof(bool)) return "bool"; if (type == typeof(byte)) return "byte"; if (type == typeof(byte[])) return "byte[]"; if (type == typeof(Guid)) return "Guid"; if (type == typeof(Uri)) return "Uri"; if (type == typeof(object)) return "object"; // Handle SecureString if (type == typeof(System.Security.SecureString)) return "SecureString"; return type.Name; }