512 lines
18 KiB
C#
512 lines
18 KiB
C#
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 <output-path>");
|
|
return 1;
|
|
}
|
|
|
|
var outputPath = args[0];
|
|
|
|
var assembly = typeof(InitialSessionStateExtensions).Assembly;
|
|
|
|
var cmdletTypes = assembly.GetTypes()
|
|
.Where(t => t.GetCustomAttribute<CmdletAttribute>() != null)
|
|
.OrderBy(t => t.GetCustomAttribute<CmdletAttribute>()!.VerbName + "-" + t.GetCustomAttribute<CmdletAttribute>()!.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<CmdletAttribute>()!;
|
|
var name = $"{cmdletAttr.VerbName}-{cmdletAttr.NounName}";
|
|
|
|
var outputTypeAttr = type.GetCustomAttribute<OutputTypeAttribute>();
|
|
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<ParameterAttribute>() != null)
|
|
.OrderBy(p =>
|
|
{
|
|
var pa = p.GetCustomAttribute<ParameterAttribute>()!;
|
|
return pa.Position == int.MinValue ? int.MaxValue : pa.Position;
|
|
})
|
|
.ThenBy(p => p.Name);
|
|
|
|
foreach (var prop in properties)
|
|
{
|
|
var paramAttr = prop.GetCustomAttribute<ParameterAttribute>()!;
|
|
var aliasAttr = prop.GetCustomAttribute<AliasAttribute>();
|
|
var aliases = aliasAttr?.AliasNames?.ToArray() ?? Array.Empty<string>();
|
|
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<string>(cmdletTypes.Select(t =>
|
|
{
|
|
var a = t.GetCustomAttribute<CmdletAttribute>()!;
|
|
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<string>()
|
|
.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<JsonElement> { 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<string>();
|
|
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<string, Type>
|
|
{
|
|
["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<LANCommander.SDK.Enums.ScriptType>())
|
|
{
|
|
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;
|
|
}
|