Add support for defining PowerShell modules
Adds a new section "Scripting" in the server UI where PowerShell modules can be defined. These can be used for defining a library of functions that can be used in any script. These modules are automatically synced to the launcher and imported upon script execution.
This commit is contained in:
parent
5a80d1e605
commit
f87756243e
27 changed files with 1482 additions and 4 deletions
|
|
@ -37,7 +37,7 @@ public partial class LibraryViewModel : GamesCollectionViewModel
|
|||
public override bool IsCollectionFiltered => !string.IsNullOrEmpty(SelectedCollection);
|
||||
public override string FilteredCollectionName => SelectedCollection ?? string.Empty;
|
||||
|
||||
public LibraryViewModel(IServiceProvider serviceProvider)
|
||||
public LibraryViewModel(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = serviceProvider.GetRequiredService<ILogger<LibraryViewModel>>();
|
||||
|
|
@ -67,6 +67,12 @@ public partial class LibraryViewModel : GamesCollectionViewModel
|
|||
var mediaClient = scope.ServiceProvider.GetRequiredService<MediaClient>();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DbContext>();
|
||||
|
||||
if (!IsOfflineMode)
|
||||
{
|
||||
var moduleClient = scope.ServiceProvider.GetRequiredService<ModuleClient>();
|
||||
await moduleClient.SyncAsync();
|
||||
}
|
||||
|
||||
var items = await libraryService.GetItemsAsync();
|
||||
var results = new List<GameItemViewModel>();
|
||||
var gameModels = new List<LANCommander.Launcher.Data.Models.Game>();
|
||||
|
|
|
|||
69
LANCommander.SDK/Clients/ModuleClient.cs
Normal file
69
LANCommander.SDK/Clients/ModuleClient.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading.Tasks;
|
||||
using LANCommander.SDK.Abstractions;
|
||||
using LANCommander.SDK.Factories;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LANCommander.SDK.Services
|
||||
{
|
||||
public class ModuleClient(
|
||||
ILogger<ModuleClient> logger,
|
||||
ApiRequestFactory apiRequestFactory,
|
||||
ISettingsProvider settingsProvider)
|
||||
{
|
||||
public async Task<IEnumerable<string>> GetAsync()
|
||||
{
|
||||
return await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute("/api/Modules")
|
||||
.GetAsync<IEnumerable<string>>();
|
||||
}
|
||||
|
||||
public string GetLocalPath()
|
||||
=> settingsProvider.CurrentValue.Modules.StoragePath;
|
||||
|
||||
public async Task<FileInfo> DownloadAsync(string destination)
|
||||
{
|
||||
return await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute("/api/Modules/Download")
|
||||
.DownloadAsync(destination);
|
||||
}
|
||||
|
||||
public async Task SyncAsync()
|
||||
{
|
||||
var destination = GetLocalPath();
|
||||
var archivePath = Path.Combine(Path.GetTempPath(), $"LANCommander.Modules.{Guid.NewGuid()}.zip");
|
||||
|
||||
try
|
||||
{
|
||||
await DownloadAsync(archivePath);
|
||||
|
||||
if (Directory.Exists(destination))
|
||||
Directory.Delete(destination, true);
|
||||
|
||||
Directory.CreateDirectory(destination);
|
||||
|
||||
ZipFile.ExtractToDirectory(archivePath, destination, true);
|
||||
|
||||
logger?.LogInformation("Synced PowerShell modules to {Destination}", destination);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Could not sync PowerShell modules");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(archivePath))
|
||||
File.Delete(archivePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@ public static class IServiceCollectionExtensions
|
|||
services.AddSingleton<LibraryClient>();
|
||||
services.AddSingleton<LobbyClient>();
|
||||
services.AddSingleton<MediaClient>();
|
||||
services.AddSingleton<ModuleClient>();
|
||||
services.AddSingleton<PlaySessionClient>();
|
||||
services.AddSingleton<ProfileClient>();
|
||||
services.AddSingleton<RedistributableClient>();
|
||||
|
|
|
|||
6
LANCommander.SDK/Models/Settings/ModuleSettings.cs
Normal file
6
LANCommander.SDK/Models/Settings/ModuleSettings.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace LANCommander.SDK.Models;
|
||||
|
||||
public class ModuleSettings
|
||||
{
|
||||
public string StoragePath { get; set; } = AppPaths.GetConfigPath("Modules");
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ public class Settings
|
|||
public GameSettings Games { get; set; } = new();
|
||||
public ToolSettings Tools { get; set; } = new();
|
||||
public MediaSettings Media { get; set; } = new();
|
||||
public ModuleSettings Modules { get; set; } = new();
|
||||
public DebugSettings Debug { get; set; } = new();
|
||||
public UpdateSettings Updates { get; set; } = new();
|
||||
public IPXRelaySettings IPXRelay { get; set; } = new();
|
||||
|
|
|
|||
|
|
@ -175,9 +175,17 @@ namespace LANCommander.SDK.PowerShell
|
|||
T result = default;
|
||||
|
||||
var initialSessionState = InitialSessionState.CreateDefault();
|
||||
|
||||
|
||||
initialSessionState.AddCustomCmdlets();
|
||||
|
||||
// Always import synced PowerShell modules so their functions are available to every script
|
||||
var modulesPath = AppPaths.GetConfigPath("Modules");
|
||||
if (Directory.Exists(modulesPath))
|
||||
{
|
||||
foreach (var moduleDirectory in Directory.GetDirectories(modulesPath))
|
||||
initialSessionState.ImportPSModule(new[] { moduleDirectory });
|
||||
}
|
||||
|
||||
DisableWow64Redirection();
|
||||
|
||||
using (Runspace runspace = RunspaceFactory.CreateRunspace(initialSessionState))
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ public static class IServiceCollectionExtensions
|
|||
services.AddScoped<GameService>();
|
||||
services.AddScoped<LibraryService>();
|
||||
services.AddScoped<ScriptService>();
|
||||
services.AddScoped<ModuleService>();
|
||||
services.AddScoped<GenreService>();
|
||||
services.AddScoped<PlatformService>();
|
||||
services.AddScoped<KeyService>();
|
||||
|
|
|
|||
28
LANCommander.Server.Services/Models/Module.cs
Normal file
28
LANCommander.Server.Services/Models/Module.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LANCommander.Server.Models
|
||||
{
|
||||
public enum FunctionVisibility
|
||||
{
|
||||
Public,
|
||||
Private
|
||||
}
|
||||
|
||||
public class ModuleFunction
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Name { get; set; }
|
||||
public FunctionVisibility Visibility { get; set; } = FunctionVisibility.Public;
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
public class Module
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Manifest { get; set; }
|
||||
public List<ModuleFunction> Functions { get; set; } = new();
|
||||
}
|
||||
|
||||
public record VerbGroup(string Name, IReadOnlyList<string> Verbs);
|
||||
}
|
||||
14
LANCommander.Server.Services/Models/ModuleManifest.cs
Normal file
14
LANCommander.Server.Services/Models/ModuleManifest.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace LANCommander.Server.Models
|
||||
{
|
||||
public class ModuleManifest
|
||||
{
|
||||
public string RootModule { get; set; } = string.Empty;
|
||||
public string ModuleVersion { get; set; } = "1.0.0";
|
||||
public string Guid { get; set; } = System.Guid.NewGuid().ToString();
|
||||
public string Author { get; set; } = string.Empty;
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
public string Copyright { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string PowerShellVersion { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
478
LANCommander.Server.Services/ModuleService.cs
Normal file
478
LANCommander.Server.Services/ModuleService.cs
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
using System.IO.Compression;
|
||||
using System.Management.Automation.Language;
|
||||
using System.Text.RegularExpressions;
|
||||
using LANCommander.SDK;
|
||||
using LANCommander.Server.Models;
|
||||
|
||||
namespace LANCommander.Server.Services
|
||||
{
|
||||
public sealed class ModuleService(SettingsProvider<Settings.Settings> settingsProvider)
|
||||
{
|
||||
private static readonly Regex FunctionNamePattern =
|
||||
new(@"^[A-Za-z][A-Za-z0-9]*-[A-Za-z][A-Za-z0-9]*$", RegexOptions.Compiled);
|
||||
|
||||
// Standard set of approved PowerShell verbs (Get-Verb), grouped by verb group.
|
||||
private static readonly IReadOnlyList<VerbGroup> ApprovedVerbGroups = new[]
|
||||
{
|
||||
new VerbGroup("Common", new[]
|
||||
{
|
||||
"Add", "Clear", "Close", "Copy", "Enter", "Exit", "Find", "Format", "Get", "Hide", "Join",
|
||||
"Lock", "Move", "New", "Open", "Optimize", "Pop", "Push", "Redo", "Remove", "Rename", "Reset",
|
||||
"Resize", "Search", "Select", "Set", "Show", "Skip", "Split", "Step", "Switch", "Undo", "Unlock", "Watch",
|
||||
}),
|
||||
new VerbGroup("Communications", new[]
|
||||
{
|
||||
"Connect", "Disconnect", "Read", "Receive", "Send", "Write",
|
||||
}),
|
||||
new VerbGroup("Data", new[]
|
||||
{
|
||||
"Backup", "Checkpoint", "Compare", "Compress", "Convert", "ConvertFrom", "ConvertTo", "Dismount",
|
||||
"Edit", "Expand", "Export", "Group", "Import", "Initialize", "Limit", "Merge", "Mount", "Out",
|
||||
"Publish", "Restore", "Save", "Sync", "Unpublish", "Update",
|
||||
}),
|
||||
new VerbGroup("Diagnostic", new[]
|
||||
{
|
||||
"Debug", "Measure", "Ping", "Repair", "Resolve", "Test", "Trace",
|
||||
}),
|
||||
new VerbGroup("Lifecycle", new[]
|
||||
{
|
||||
"Approve", "Assert", "Build", "Complete", "Confirm", "Deny", "Deploy", "Disable", "Enable",
|
||||
"Install", "Invoke", "Register", "Request", "Restart", "Resume", "Start", "Stop", "Submit",
|
||||
"Suspend", "Uninstall", "Unregister", "Wait",
|
||||
}),
|
||||
new VerbGroup("Security", new[]
|
||||
{
|
||||
"Block", "Grant", "Protect", "Revoke", "Unblock", "Unprotect",
|
||||
}),
|
||||
new VerbGroup("Other", new[]
|
||||
{
|
||||
"Use",
|
||||
}),
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> ApprovedVerbs =
|
||||
new(ApprovedVerbGroups.SelectMany(g => g.Verbs), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private string GetStoragePath()
|
||||
{
|
||||
var storagePath = settingsProvider.CurrentValue.Server.Scripts.Modules.StoragePath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(storagePath))
|
||||
{
|
||||
storagePath = AppPaths.GetConfigPath("Modules");
|
||||
|
||||
settingsProvider.Update(s =>
|
||||
{
|
||||
s.Server.Scripts.Modules.StoragePath = storagePath;
|
||||
});
|
||||
}
|
||||
|
||||
if (!Directory.Exists(storagePath))
|
||||
Directory.CreateDirectory(storagePath);
|
||||
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
private string GetModuleDirectory(string name) => Path.Combine(GetStoragePath(), name);
|
||||
|
||||
private static string GetManifestPath(string moduleDirectory, string name) =>
|
||||
Path.Combine(moduleDirectory, $"{name}.psd1");
|
||||
|
||||
private static string GetLoaderPath(string moduleDirectory, string name) =>
|
||||
Path.Combine(moduleDirectory, $"{name}.psm1");
|
||||
|
||||
private static string GetVisibilityDirectory(string moduleDirectory, FunctionVisibility visibility) =>
|
||||
Path.Combine(moduleDirectory, visibility == FunctionVisibility.Public ? "Public" : "Private");
|
||||
|
||||
public IEnumerable<Module> GetModules()
|
||||
{
|
||||
var storagePath = GetStoragePath();
|
||||
|
||||
return Directory
|
||||
.GetDirectories(storagePath)
|
||||
.Select(d => GetModule(Path.GetFileName(d)))
|
||||
.Where(m => m != null)
|
||||
.OrderBy(m => m.Name);
|
||||
}
|
||||
|
||||
public Module GetModule(string name)
|
||||
{
|
||||
var moduleDirectory = GetModuleDirectory(name);
|
||||
|
||||
if (!Directory.Exists(moduleDirectory))
|
||||
return null;
|
||||
|
||||
var manifestPath = GetManifestPath(moduleDirectory, name);
|
||||
|
||||
var module = new Module
|
||||
{
|
||||
Name = name,
|
||||
Manifest = File.Exists(manifestPath) ? File.ReadAllText(manifestPath) : string.Empty,
|
||||
Functions = new List<ModuleFunction>(),
|
||||
};
|
||||
|
||||
foreach (var visibility in new[] { FunctionVisibility.Public, FunctionVisibility.Private })
|
||||
{
|
||||
var directory = GetVisibilityDirectory(moduleDirectory, visibility);
|
||||
|
||||
if (!Directory.Exists(directory))
|
||||
continue;
|
||||
|
||||
foreach (var file in Directory.GetFiles(directory, "*.ps1").OrderBy(f => f))
|
||||
{
|
||||
module.Functions.Add(new ModuleFunction
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(file),
|
||||
Visibility = visibility,
|
||||
Content = File.ReadAllText(file),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
public bool ModuleExists(string name) => Directory.Exists(GetModuleDirectory(name));
|
||||
|
||||
public Module CreateScaffold(string name)
|
||||
{
|
||||
return new Module
|
||||
{
|
||||
Name = name,
|
||||
Manifest = GetDefaultManifest(name),
|
||||
Functions = new List<ModuleFunction>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "Get-Example",
|
||||
Visibility = FunctionVisibility.Public,
|
||||
Content = GetDefaultFunctionContent("Get-Example"),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public void SaveModule(Module module)
|
||||
{
|
||||
var moduleDirectory = GetModuleDirectory(module.Name);
|
||||
|
||||
if (!Directory.Exists(moduleDirectory))
|
||||
Directory.CreateDirectory(moduleDirectory);
|
||||
|
||||
File.WriteAllText(GetManifestPath(moduleDirectory, module.Name), module.Manifest ?? string.Empty);
|
||||
File.WriteAllText(GetLoaderPath(moduleDirectory, module.Name), GetLoaderScript());
|
||||
|
||||
var desiredFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var visibility in new[] { FunctionVisibility.Public, FunctionVisibility.Private })
|
||||
{
|
||||
var directory = GetVisibilityDirectory(moduleDirectory, visibility);
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
foreach (var function in module.Functions.Where(f => f.Visibility == visibility))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(function.Name))
|
||||
continue;
|
||||
|
||||
var path = Path.Combine(directory, $"{function.Name}.ps1");
|
||||
|
||||
File.WriteAllText(path, function.Content ?? string.Empty);
|
||||
desiredFiles.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Prune function files that were removed or moved between Public/Private
|
||||
foreach (var visibility in new[] { FunctionVisibility.Public, FunctionVisibility.Private })
|
||||
{
|
||||
var directory = GetVisibilityDirectory(moduleDirectory, visibility);
|
||||
|
||||
if (!Directory.Exists(directory))
|
||||
continue;
|
||||
|
||||
foreach (var file in Directory.GetFiles(directory, "*.ps1"))
|
||||
{
|
||||
if (!desiredFiles.Contains(file))
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RenameModule(string oldName, string newName)
|
||||
{
|
||||
if (string.Equals(oldName, newName, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
var module = GetModule(oldName);
|
||||
|
||||
if (module == null)
|
||||
return;
|
||||
|
||||
DeleteModule(oldName);
|
||||
|
||||
module.Name = newName;
|
||||
|
||||
SaveModule(module);
|
||||
}
|
||||
|
||||
public void DeleteModule(string name)
|
||||
{
|
||||
var moduleDirectory = GetModuleDirectory(name);
|
||||
|
||||
if (Directory.Exists(moduleDirectory))
|
||||
Directory.Delete(moduleDirectory, true);
|
||||
}
|
||||
|
||||
public string GetModulesArchive()
|
||||
{
|
||||
var storagePath = GetStoragePath();
|
||||
var archivePath = Path.Combine(Path.GetTempPath(), $"LANCommander.Modules.{Guid.NewGuid()}.zip");
|
||||
|
||||
ZipFile.CreateFromDirectory(storagePath, archivePath, CompressionLevel.Optimal, false);
|
||||
|
||||
return archivePath;
|
||||
}
|
||||
|
||||
public ModuleManifest ParseManifest(string manifest)
|
||||
{
|
||||
var result = new ModuleManifest();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(manifest))
|
||||
return result;
|
||||
|
||||
var ast = Parser.ParseInput(manifest, out _, out _);
|
||||
|
||||
var hashtable = ast
|
||||
.Find(a => a is HashtableAst, false) as HashtableAst;
|
||||
|
||||
if (hashtable == null)
|
||||
return result;
|
||||
|
||||
foreach (var pair in hashtable.KeyValuePairs)
|
||||
{
|
||||
if (pair.Item1 is not StringConstantExpressionAst keyAst)
|
||||
continue;
|
||||
|
||||
var value = GetScalarString(pair.Item2);
|
||||
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
switch (keyAst.Value)
|
||||
{
|
||||
case "RootModule":
|
||||
result.RootModule = value;
|
||||
break;
|
||||
case "ModuleVersion":
|
||||
result.ModuleVersion = value;
|
||||
break;
|
||||
case "GUID":
|
||||
result.Guid = value;
|
||||
break;
|
||||
case "Author":
|
||||
result.Author = value;
|
||||
break;
|
||||
case "CompanyName":
|
||||
result.CompanyName = value;
|
||||
break;
|
||||
case "Copyright":
|
||||
result.Copyright = value;
|
||||
break;
|
||||
case "Description":
|
||||
result.Description = value;
|
||||
break;
|
||||
case "PowerShellVersion":
|
||||
result.PowerShellVersion = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GenerateManifest(ModuleManifest manifest, IEnumerable<string> functionNames, IEnumerable<string> aliasNames)
|
||||
{
|
||||
var functions = functionNames?.Distinct().OrderBy(n => n).ToList() ?? new List<string>();
|
||||
var aliases = aliasNames?.Distinct().OrderBy(n => n).ToList() ?? new List<string>();
|
||||
|
||||
var builder = new System.Text.StringBuilder();
|
||||
|
||||
builder.Append("@{\n");
|
||||
builder.Append($" RootModule = '{Escape(manifest.RootModule)}'\n");
|
||||
builder.Append($" ModuleVersion = '{Escape(manifest.ModuleVersion)}'\n");
|
||||
builder.Append($" GUID = '{Escape(manifest.Guid)}'\n");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(manifest.Author))
|
||||
builder.Append($" Author = '{Escape(manifest.Author)}'\n");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(manifest.CompanyName))
|
||||
builder.Append($" CompanyName = '{Escape(manifest.CompanyName)}'\n");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(manifest.Copyright))
|
||||
builder.Append($" Copyright = '{Escape(manifest.Copyright)}'\n");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(manifest.Description))
|
||||
builder.Append($" Description = '{Escape(manifest.Description)}'\n");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(manifest.PowerShellVersion))
|
||||
builder.Append($" PowerShellVersion = '{Escape(manifest.PowerShellVersion)}'\n");
|
||||
|
||||
builder.Append($" FunctionsToExport = {FormatArray(functions)}\n");
|
||||
builder.Append(" CmdletsToExport = @()\n");
|
||||
builder.Append(" VariablesToExport = @()\n");
|
||||
builder.Append($" AliasesToExport = {FormatArray(aliases)}\n");
|
||||
builder.Append("}\n");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetExportedFunctions(Module module)
|
||||
{
|
||||
if (module?.Functions == null)
|
||||
return [];
|
||||
|
||||
return module.Functions
|
||||
.Where(f => f.Visibility == FunctionVisibility.Public && !string.IsNullOrWhiteSpace(f.Name))
|
||||
.Select(f => f.Name)
|
||||
.Distinct()
|
||||
.OrderBy(n => n)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetExportedAliases(Module module)
|
||||
{
|
||||
if (module?.Functions == null)
|
||||
return [];
|
||||
|
||||
var aliases = new List<string>();
|
||||
|
||||
foreach (var function in module.Functions)
|
||||
aliases.AddRange(GetAliasesFromScript(function.Content));
|
||||
|
||||
return aliases.Distinct().OrderBy(a => a).ToList();
|
||||
}
|
||||
|
||||
public IEnumerable<(string Name, string Synopsis, string Module)> GetPublicFunctionCompletions()
|
||||
{
|
||||
foreach (var module in GetModules())
|
||||
{
|
||||
foreach (var function in module.Functions.Where(f =>
|
||||
f.Visibility == FunctionVisibility.Public && !string.IsNullOrWhiteSpace(f.Name)))
|
||||
{
|
||||
yield return (function.Name, ExtractSynopsis(function.Content), module.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractSynopsis(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
return null;
|
||||
|
||||
var match = Regex.Match(content, @"\.SYNOPSIS\s*\r?\n\s*(?<synopsis>.+)");
|
||||
|
||||
return match.Success ? match.Groups["synopsis"].Value.Trim() : null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<VerbGroup> GetApprovedVerbGroups() =>
|
||||
ApprovedVerbGroups
|
||||
.Select(g => new VerbGroup(g.Name, g.Verbs.OrderBy(v => v, StringComparer.OrdinalIgnoreCase).ToArray()))
|
||||
.ToList();
|
||||
|
||||
public bool IsValidFunctionName(string name) =>
|
||||
!string.IsNullOrWhiteSpace(name) && FunctionNamePattern.IsMatch(name);
|
||||
|
||||
public bool IsApprovedVerb(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return false;
|
||||
|
||||
var dash = name.IndexOf('-');
|
||||
|
||||
if (dash <= 0)
|
||||
return false;
|
||||
|
||||
return ApprovedVerbs.Contains(name.Substring(0, dash));
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetAliasesFromScript(string script)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(script))
|
||||
yield break;
|
||||
|
||||
var ast = Parser.ParseInput(script, out _, out _);
|
||||
|
||||
foreach (var command in ast.FindAll(a => a is CommandAst, true).Cast<CommandAst>())
|
||||
{
|
||||
var name = command.GetCommandName();
|
||||
|
||||
if (!string.Equals(name, "Set-Alias", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(name, "New-Alias", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var elements = command.CommandElements;
|
||||
|
||||
for (var i = 1; i < elements.Count; i++)
|
||||
{
|
||||
if (elements[i] is CommandParameterAst parameter &&
|
||||
string.Equals(parameter.ParameterName, "Name", StringComparison.OrdinalIgnoreCase) &&
|
||||
i + 1 < elements.Count)
|
||||
{
|
||||
var value = GetScalarString(elements[i + 1]);
|
||||
|
||||
if (value != null)
|
||||
yield return value;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (elements[i] is StringConstantExpressionAst positional && i == 1)
|
||||
{
|
||||
yield return positional.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetScalarString(Ast valueAst)
|
||||
{
|
||||
return valueAst switch
|
||||
{
|
||||
StringConstantExpressionAst s => s.Value,
|
||||
ExpandableStringExpressionAst e => e.Value,
|
||||
ConstantExpressionAst c => c.Value?.ToString(),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatArray(IReadOnlyList<string> values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
return "@()";
|
||||
|
||||
return "@(" + string.Join(", ", values.Select(v => $"'{Escape(v)}'")) + ")";
|
||||
}
|
||||
|
||||
private static string Escape(string value) =>
|
||||
(value ?? string.Empty).Replace("'", "''");
|
||||
|
||||
private static string GetDefaultManifest(string name) =>
|
||||
$"@{{\n RootModule = '{name}.psm1'\n ModuleVersion = '1.0.0'\n GUID = '{Guid.NewGuid()}'\n FunctionsToExport = '*'\n}}\n";
|
||||
|
||||
private static string GetDefaultFunctionContent(string name) =>
|
||||
$"function {name} {{\n [CmdletBinding()]\n param()\n\n \"Hello from a LANCommander module\"\n}}\n";
|
||||
|
||||
// Loader that dot-sources every function file and exports only the public ones.
|
||||
private static string GetLoaderScript() =>
|
||||
"$Public = @(Get-ChildItem -Path \"$PSScriptRoot\\Public\\*.ps1\" -ErrorAction SilentlyContinue)\n" +
|
||||
"$Private = @(Get-ChildItem -Path \"$PSScriptRoot\\Private\\*.ps1\" -ErrorAction SilentlyContinue)\n" +
|
||||
"\n" +
|
||||
"foreach ($file in @($Public + $Private)) {\n" +
|
||||
" try { . $file.FullName }\n" +
|
||||
" catch { Write-Error \"Failed to import function $($file.FullName): $_\" }\n" +
|
||||
"}\n" +
|
||||
"\n" +
|
||||
"Export-ModuleMember -Function $Public.BaseName\n";
|
||||
}
|
||||
}
|
||||
|
|
@ -125,5 +125,62 @@ namespace LANCommander.Server.Services
|
|||
};
|
||||
});
|
||||
}
|
||||
|
||||
private string GetSnippetsStoragePath()
|
||||
{
|
||||
var storagePath = settingsProvider.CurrentValue.Server.Scripts.Snippets.StoragePath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(storagePath))
|
||||
{
|
||||
storagePath = AppPaths.GetConfigPath("Snippets");
|
||||
|
||||
settingsProvider.Update(s =>
|
||||
{
|
||||
s.Server.Scripts.Snippets.StoragePath = storagePath;
|
||||
});
|
||||
}
|
||||
|
||||
if (!Directory.Exists(storagePath))
|
||||
Directory.CreateDirectory(storagePath);
|
||||
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
private string GetSnippetPath(string group, string name) =>
|
||||
Path.Combine(GetSnippetsStoragePath(), group, $"{name}.ps1");
|
||||
|
||||
public Snippet GetSnippet(string group, string name)
|
||||
{
|
||||
var path = GetSnippetPath(group, name);
|
||||
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
return new Snippet
|
||||
{
|
||||
Group = group,
|
||||
Name = name,
|
||||
Content = File.ReadAllText(path),
|
||||
};
|
||||
}
|
||||
|
||||
public void SaveSnippet(Snippet snippet)
|
||||
{
|
||||
var path = GetSnippetPath(snippet.Group, snippet.Name);
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
|
||||
if (!Directory.Exists(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
File.WriteAllText(path, snippet.Content ?? string.Empty);
|
||||
}
|
||||
|
||||
public void DeleteSnippet(string group, string name)
|
||||
{
|
||||
var path = GetSnippetPath(group, name);
|
||||
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
LANCommander.Server.Settings/Models/ModuleSettings.cs
Normal file
8
LANCommander.Server.Settings/Models/ModuleSettings.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
using LANCommander.SDK;
|
||||
|
||||
namespace LANCommander.Server.Settings.Models;
|
||||
|
||||
public class ModuleSettings
|
||||
{
|
||||
public string StoragePath { get; set; } = AppPaths.GetConfigPath("Modules");
|
||||
}
|
||||
|
|
@ -7,4 +7,5 @@ public class ScriptSettings
|
|||
public bool EnableAutomaticRepackaging { get; set; } = false;
|
||||
public int RepackageEvery { get; set; } = 24;
|
||||
public SnippetSettings Snippets { get; set; } = new();
|
||||
public ModuleSettings Modules { get; set; } = new();
|
||||
}
|
||||
32
LANCommander.Server/Endpoints/ModulesEndpoints.cs
Normal file
32
LANCommander.Server/Endpoints/ModulesEndpoints.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using LANCommander.Server.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LANCommander.Server.Endpoints;
|
||||
|
||||
public static class ModulesEndpoints
|
||||
{
|
||||
public static void MapModulesEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/Modules").RequireAuthorization();
|
||||
|
||||
group.MapGet("/", GetAsync);
|
||||
group.MapGet("/Download", DownloadAsync);
|
||||
}
|
||||
|
||||
internal static IResult GetAsync([FromServices] ModuleService moduleService)
|
||||
{
|
||||
var names = moduleService.GetModules().Select(m => m.Name);
|
||||
|
||||
return TypedResults.Ok(names);
|
||||
}
|
||||
|
||||
internal static IResult DownloadAsync([FromServices] ModuleService moduleService)
|
||||
{
|
||||
var archivePath = moduleService.GetModulesArchive();
|
||||
|
||||
var stream = new FileStream(archivePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096,
|
||||
FileOptions.DeleteOnClose | FileOptions.Asynchronous);
|
||||
|
||||
return TypedResults.File(stream, "application/octet-stream", "Modules.zip");
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ public static class Endpoints
|
|||
endpoints.MapLauncherEndpoints();
|
||||
endpoints.MapLibraryEndpoints();
|
||||
endpoints.MapRedistributablesEndpoints();
|
||||
endpoints.MapModulesEndpoints();
|
||||
endpoints.MapToolsEndpoints();
|
||||
endpoints.MapIssueEndpoints();
|
||||
endpoints.MapSaveEndpoints();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public static class Filesystem
|
|||
settings.Value.Server.Launcher.StoragePath,
|
||||
settings.Value.Server.Backups.StoragePath,
|
||||
settings.Value.Server.Scripts.Snippets.StoragePath,
|
||||
settings.Value.Server.Scripts.Modules.StoragePath,
|
||||
];
|
||||
|
||||
foreach (var directory in directories)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,16 @@
|
|||
<MenuItem RouterLink="/Redistributables" Icon="@IconType.Outline.Block">Redistributables</MenuItem>
|
||||
<MenuItem RouterLink="/Tools" Icon="@IconType.Outline.Tool">Tools</MenuItem>
|
||||
<MenuItem RouterLink="/Servers" Icon="@IconType.Outline.Database">Servers</MenuItem>
|
||||
<SubMenu>
|
||||
<TitleTemplate>
|
||||
<Icon Type="@IconType.Outline.Code"/>
|
||||
<span>Scripting</span>
|
||||
</TitleTemplate>
|
||||
<ChildContent>
|
||||
<MenuItem RouterLink="/Scripting/Snippets">Snippets</MenuItem>
|
||||
<MenuItem RouterLink="/Scripting/Modules">Modules</MenuItem>
|
||||
</ChildContent>
|
||||
</SubMenu>
|
||||
<MenuDivider/>
|
||||
<SubMenu>
|
||||
<TitleTemplate>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
@inject ScriptService ScriptService
|
||||
@inject ModuleService ModuleService
|
||||
|
||||
<Flex Gap="FlexGap.Small" Wrap="FlexWrap.Wrap" Align="FlexAlign.Center" Style="margin-bottom: 16px;">
|
||||
@foreach (var group in Snippets.Select(s => s.Group).Distinct())
|
||||
{
|
||||
<Dropdown>
|
||||
<Overlay>
|
||||
<Menu>
|
||||
@foreach (var snippet in Snippets.Where(s => s.Group == group))
|
||||
{
|
||||
<MenuItem OnClick="() => InsertSnippet(snippet)">
|
||||
@snippet.Name
|
||||
</MenuItem>
|
||||
}
|
||||
</Menu>
|
||||
</Overlay>
|
||||
|
||||
<ChildContent>
|
||||
<Button Type="@ButtonType.Primary">@group</Button>
|
||||
</ChildContent>
|
||||
</Dropdown>
|
||||
}
|
||||
|
||||
@if (Variables != null && Variables.Any())
|
||||
{
|
||||
<Dropdown>
|
||||
<Overlay>
|
||||
<Menu>
|
||||
@foreach (var variable in Variables)
|
||||
{
|
||||
<MenuItem OnClick="() => InsertText(variable)">
|
||||
@variable
|
||||
</MenuItem>
|
||||
}
|
||||
</Menu>
|
||||
</Overlay>
|
||||
|
||||
<ChildContent>
|
||||
<Button Type="@ButtonType.Primary">Variables</Button>
|
||||
</ChildContent>
|
||||
</Dropdown>
|
||||
}
|
||||
</Flex>
|
||||
|
||||
<CodeInput @ref="_codeInput"
|
||||
Value="@Value"
|
||||
ValueChanged="@ValueChanged"
|
||||
Language="Languages.PowerShell"
|
||||
OnSave="OnSave"
|
||||
ModuleFunctions="@_moduleFunctions"
|
||||
Height="@Height" />
|
||||
|
||||
@code {
|
||||
[Parameter] public string? Value { get; set; }
|
||||
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
|
||||
[Parameter] public int Height { get; set; } = 500;
|
||||
[Parameter] public IEnumerable<string>? Variables { get; set; }
|
||||
[Parameter] public EventCallback OnSave { get; set; }
|
||||
|
||||
CodeInput? _codeInput;
|
||||
IEnumerable<Snippet> Snippets { get; set; } = [];
|
||||
List<ModuleFunctionCompletion> _moduleFunctions = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Snippets = ScriptService.GetSnippets();
|
||||
|
||||
_moduleFunctions = ModuleService.GetPublicFunctionCompletions()
|
||||
.Select(f => new ModuleFunctionCompletion(f.Name, string.IsNullOrWhiteSpace(f.Synopsis) ? null : f.Synopsis, f.Module))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task LayoutAsync()
|
||||
{
|
||||
if (_codeInput != null)
|
||||
await _codeInput.LayoutAsync();
|
||||
}
|
||||
|
||||
async Task InsertText(string text)
|
||||
{
|
||||
if (_codeInput != null)
|
||||
await _codeInput.InsertText(text);
|
||||
}
|
||||
|
||||
async Task InsertSnippet(Snippet snippet)
|
||||
{
|
||||
if (_codeInput != null)
|
||||
await _codeInput.InsertSnippet(snippet.Content);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
@using YamlDotNet.Serialization.NamingConventions
|
||||
@inherits FeedbackComponent<ScriptEditorOptions, Script>
|
||||
@inject ScriptService ScriptService
|
||||
@inject ModuleService ModuleService
|
||||
@inject ArchiveService ArchiveService
|
||||
@inject ServerService ServerService
|
||||
@inject RedistributableService RedistributableService
|
||||
|
|
@ -91,7 +92,7 @@
|
|||
<Checkbox @bind-Checked="_script.RequiresAdmin">Requires Admin</Checkbox>
|
||||
</Flex>
|
||||
|
||||
<CodeInput @ref="_codeInput" @bind-Value="_script.Contents" Language="Languages.PowerShell" OnSave="Save" ScriptType="@_script.Type.ToString()" OnReady="OnEditorReady" />
|
||||
<CodeInput @ref="_codeInput" @bind-Value="_script.Contents" Language="Languages.PowerShell" OnSave="Save" ScriptType="@_script.Type.ToString()" ModuleFunctions="@_moduleFunctions" OnReady="OnEditorReady" />
|
||||
|
||||
<PowerShellConsole @ref="_console" Id="_id" @bind-Active="_debugging"/>
|
||||
|
||||
|
|
@ -113,6 +114,7 @@
|
|||
CodeInput? _codeInput;
|
||||
PowerShellConsole? _console;
|
||||
IEnumerable<Snippet> Snippets { get; set; } = [];
|
||||
List<ModuleFunctionCompletion> _moduleFunctions = new();
|
||||
List<string> _variables = new();
|
||||
List<string> _optionKeys = new();
|
||||
string _redistributableName = string.Empty;
|
||||
|
|
@ -209,6 +211,10 @@
|
|||
|
||||
UpdateVariables();
|
||||
Snippets = ScriptService.GetSnippets();
|
||||
|
||||
_moduleFunctions = ModuleService.GetPublicFunctionCompletions()
|
||||
.Select(f => new ModuleFunctionCompletion(f.Name, string.IsNullOrWhiteSpace(f.Synopsis) ? null : f.Synopsis, f.Module))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
async Task OnEditorReady()
|
||||
|
|
|
|||
388
LANCommander.Server/UI/Pages/Scripting/Modules/Edit.razor
Normal file
388
LANCommander.Server/UI/Pages/Scripting/Modules/Edit.razor
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
@page "/Scripting/Modules/Add"
|
||||
@page "/Scripting/Modules/{Name}"
|
||||
@attribute [Authorize(Roles = RoleService.AdministratorRoleName)]
|
||||
@inject ModuleService ModuleService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IMessageService MessageService
|
||||
@inject ILogger<Edit> Logger
|
||||
|
||||
<style>
|
||||
.module-editor .monaco-editor,
|
||||
.module-editor .monaco-editor-container {
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<PageHeader Title="@(IsNew ? "Add Module" : Module.Name)">
|
||||
<PageHeaderExtra>
|
||||
<Flex Gap="FlexGap.Small">
|
||||
<Button OnClick="@(() => NavigationManager.NavigateTo("/Scripting/Modules"))">Back</Button>
|
||||
<Button OnClick="Save" Type="@ButtonType.Primary">Save</Button>
|
||||
</Flex>
|
||||
</PageHeaderExtra>
|
||||
</PageHeader>
|
||||
|
||||
<PageContent Class="module-editor">
|
||||
<Form Model="@Module" Layout="@FormLayout.Vertical">
|
||||
<Tabs ActiveKey="@_activeTab" ActiveKeyChanged="OnTabChanged">
|
||||
<TabPane Key="functions" Tab="Functions">
|
||||
@if (_activeTab == "functions")
|
||||
{
|
||||
<GridRow Gutter="16">
|
||||
<GridCol Span="6">
|
||||
<Flex Style="margin-bottom: 16px;">
|
||||
<Button Type="@ButtonType.Primary" Icon="@IconType.Outline.Plus" OnClick="AddFunction" Block>Add Function</Button>
|
||||
</Flex>
|
||||
|
||||
<Tree TItem="ModuleFunction" SelectedKeys="@_selectedFunctionKeys" OnClick="OnFunctionTreeClick">
|
||||
<ChildContent>
|
||||
<TreeNode Title="Public" Selectable="false" Expanded>
|
||||
<ChildContent>
|
||||
@foreach (var function in Module.Functions.Where(f => f.Visibility == FunctionVisibility.Public))
|
||||
{
|
||||
<TreeNode Key="@function.Id.ToString()" Title="@FunctionTitle(function)" DataItem="function" IsLeaf />
|
||||
}
|
||||
</ChildContent>
|
||||
</TreeNode>
|
||||
<TreeNode Title="Private" Selectable="false" Expanded>
|
||||
<ChildContent>
|
||||
@foreach (var function in Module.Functions.Where(f => f.Visibility == FunctionVisibility.Private))
|
||||
{
|
||||
<TreeNode Key="@function.Id.ToString()" Title="@FunctionTitle(function)" DataItem="function" IsLeaf />
|
||||
}
|
||||
</ChildContent>
|
||||
</TreeNode>
|
||||
</ChildContent>
|
||||
</Tree>
|
||||
</GridCol>
|
||||
|
||||
<GridCol Span="18">
|
||||
@if (_selectedFunction != null)
|
||||
{
|
||||
<FormItem Label="Function Name">
|
||||
<Flex Gap="FlexGap.Small" Align="FlexAlign.Center">
|
||||
<Flex Gap="FlexGap.Small" Align="FlexAlign.Center" Style="flex-grow: 1;">
|
||||
<Select TItem="VerbOption" TItemValue="string"
|
||||
DataSource="_verbOptions"
|
||||
@bind-Value="Verb"
|
||||
ValueName="@nameof(VerbOption.Verb)"
|
||||
LabelName="@nameof(VerbOption.Verb)"
|
||||
GroupName="@nameof(VerbOption.Group)"
|
||||
SortByGroup="SortDirection.Ascending"
|
||||
SortByLabel="SortDirection.Ascending"
|
||||
ShowSearch
|
||||
Style="width: 160px;" />
|
||||
<span>-</span>
|
||||
<Input @bind-Value="Noun" Placeholder="Noun" Style="flex-grow: 1;" />
|
||||
</Flex>
|
||||
<Select @bind-Value="_selectedFunction.Visibility" TItemValue="FunctionVisibility" TItem="FunctionVisibility" Style="width: 140px;">
|
||||
<SelectOptions>
|
||||
<SelectOption TItemValue="FunctionVisibility" TItem="FunctionVisibility" Value="FunctionVisibility.Public" Label="Public" />
|
||||
<SelectOption TItemValue="FunctionVisibility" TItem="FunctionVisibility" Value="FunctionVisibility.Private" Label="Private" />
|
||||
</SelectOptions>
|
||||
</Select>
|
||||
<Popconfirm OnConfirm="() => DeleteFunction(_selectedFunction)" Title="Are you sure you want to delete this function?">
|
||||
<Button Icon="@IconType.Outline.Delete" Danger />
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
</FormItem>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_selectedFunction.Name) && !ModuleService.IsValidFunctionName(_selectedFunction.Name))
|
||||
{
|
||||
<Alert Type="@AlertType.Error" Message="Function names must use Verb-Noun format (e.g. Get-Thing)." ShowIcon="true" Style="margin-bottom: 16px;" />
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(_selectedFunction.Name) && !ModuleService.IsApprovedVerb(_selectedFunction.Name))
|
||||
{
|
||||
<Alert Type="@AlertType.Warning" Message="That verb is not an approved PowerShell verb. Run Get-Verb to see the approved list." ShowIcon="true" Style="margin-bottom: 16px;" />
|
||||
}
|
||||
|
||||
<PowerShellScriptEditor @key="_selectedFunction" @bind-Value="_selectedFunction.Content" OnSave="Save" Height="400" />
|
||||
}
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
}
|
||||
</TabPane>
|
||||
<TabPane Key="manifest" Tab="Manifest">
|
||||
<FormItem Label="Module Name">
|
||||
<Input @bind-Value="@context.Name" Disabled="@(!IsNew)" />
|
||||
</FormItem>
|
||||
|
||||
<Flex Justify="FlexJustify.FlexEnd" Align="FlexAlign.Center" Gap="FlexGap.Small" Style="margin-bottom: 16px;">
|
||||
<span>Advanced</span>
|
||||
<Switch Checked="@_advancedManifest" OnChange="OnManifestModeChanged" />
|
||||
</Flex>
|
||||
|
||||
@if (_activeTab == "manifest" && _advancedManifest)
|
||||
{
|
||||
<CodeInput @bind-Value="@context.Manifest" Language="Languages.PowerShell" OnSave="Save" Height="400" />
|
||||
}
|
||||
else if (_activeTab == "manifest")
|
||||
{
|
||||
<Form Model="@_manifest" Layout="@FormLayout.Vertical" Context="manifestContext">
|
||||
<FormItem Label="Module Version">
|
||||
<Input @bind-Value="@_manifest.ModuleVersion" />
|
||||
</FormItem>
|
||||
<FormItem Label="GUID">
|
||||
<Input @bind-Value="@_manifest.Guid" />
|
||||
</FormItem>
|
||||
<FormItem Label="Author">
|
||||
<Input @bind-Value="@_manifest.Author" />
|
||||
</FormItem>
|
||||
<FormItem Label="Description">
|
||||
<TextArea @bind-Value="@_manifest.Description" />
|
||||
</FormItem>
|
||||
<FormItem Label="Exported Functions">
|
||||
@if (ExportedFunctions.Count == 0)
|
||||
{
|
||||
<AntDesign.Text Type="@TextElementType.Secondary">None. Mark functions as Public to export them.</AntDesign.Text>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var function in ExportedFunctions)
|
||||
{
|
||||
<Tag>@function</Tag>
|
||||
}
|
||||
}
|
||||
</FormItem>
|
||||
<FormItem Label="Exported Aliases">
|
||||
@if (ExportedAliases.Count == 0)
|
||||
{
|
||||
<AntDesign.Text Type="@TextElementType.Secondary">None detected.</AntDesign.Text>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var alias in ExportedAliases)
|
||||
{
|
||||
<Tag>@alias</Tag>
|
||||
}
|
||||
}
|
||||
</FormItem>
|
||||
</Form>
|
||||
}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Form>
|
||||
</PageContent>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Name { get; set; }
|
||||
|
||||
Module Module { get; set; } = new();
|
||||
ModuleManifest _manifest = new();
|
||||
ModuleFunction _selectedFunction;
|
||||
|
||||
string _verb = string.Empty;
|
||||
string _noun = string.Empty;
|
||||
string[] _selectedFunctionKeys = Array.Empty<string>();
|
||||
List<VerbOption> _verbOptions = new();
|
||||
|
||||
record VerbOption(string Verb, string Group);
|
||||
|
||||
string _activeTab = "functions";
|
||||
bool _advancedManifest;
|
||||
|
||||
bool _isNew = true;
|
||||
bool IsNew => _isNew;
|
||||
|
||||
IReadOnlyList<string> ExportedFunctions => ModuleService.GetExportedFunctions(Module);
|
||||
IReadOnlyList<string> ExportedAliases => ModuleService.GetExportedAliases(Module);
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Name))
|
||||
{
|
||||
var module = ModuleService.GetModule(Name);
|
||||
|
||||
if (module != null)
|
||||
{
|
||||
Module = module;
|
||||
_isNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_isNew && Module.Functions.Count == 0)
|
||||
{
|
||||
Module.Functions.Add(new ModuleFunction
|
||||
{
|
||||
Name = "Get-Example",
|
||||
Visibility = FunctionVisibility.Public,
|
||||
Content = "function Get-Example {\n [CmdletBinding()]\n param()\n\n \"Hello from a LANCommander module\"\n}\n",
|
||||
});
|
||||
}
|
||||
|
||||
_verbOptions = ModuleService.GetApprovedVerbGroups()
|
||||
.SelectMany(g => g.Verbs.Select(v => new VerbOption(v, g.Name)))
|
||||
.ToList();
|
||||
_manifest = ModuleService.ParseManifest(Module.Manifest);
|
||||
SelectFunction(Module.Functions.FirstOrDefault());
|
||||
}
|
||||
|
||||
void OnTabChanged(string key)
|
||||
{
|
||||
_activeTab = key;
|
||||
}
|
||||
|
||||
string FunctionTitle(ModuleFunction function) =>
|
||||
string.IsNullOrWhiteSpace(function.Name) ? "(unnamed)" : function.Name;
|
||||
|
||||
void AddFunction()
|
||||
{
|
||||
var function = new ModuleFunction
|
||||
{
|
||||
Name = string.Empty,
|
||||
Visibility = FunctionVisibility.Public,
|
||||
Content = "function Verb-Noun {\n [CmdletBinding()]\n param()\n\n}\n",
|
||||
};
|
||||
|
||||
Module.Functions.Add(function);
|
||||
SelectFunction(function);
|
||||
}
|
||||
|
||||
void SelectFunction(ModuleFunction function)
|
||||
{
|
||||
_selectedFunction = function;
|
||||
_selectedFunctionKeys = function == null ? Array.Empty<string>() : new[] { function.Id.ToString() };
|
||||
LoadNameParts();
|
||||
}
|
||||
|
||||
void DeleteFunction(ModuleFunction function)
|
||||
{
|
||||
Module.Functions.Remove(function);
|
||||
|
||||
if (_selectedFunction == function)
|
||||
SelectFunction(Module.Functions.FirstOrDefault());
|
||||
}
|
||||
|
||||
void LoadNameParts()
|
||||
{
|
||||
_verb = string.Empty;
|
||||
_noun = string.Empty;
|
||||
|
||||
if (_selectedFunction == null || string.IsNullOrEmpty(_selectedFunction.Name))
|
||||
return;
|
||||
|
||||
var dash = _selectedFunction.Name.IndexOf('-');
|
||||
|
||||
if (dash > 0)
|
||||
{
|
||||
_verb = _selectedFunction.Name.Substring(0, dash);
|
||||
_noun = _selectedFunction.Name.Substring(dash + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
_verb = _selectedFunction.Name;
|
||||
}
|
||||
}
|
||||
|
||||
string Verb
|
||||
{
|
||||
get => _verb;
|
||||
set { _verb = value; UpdateFunctionName(); }
|
||||
}
|
||||
|
||||
string Noun
|
||||
{
|
||||
get => _noun;
|
||||
set { _noun = value; UpdateFunctionName(); }
|
||||
}
|
||||
|
||||
void OnFunctionTreeClick(TreeEventArgs<ModuleFunction> args)
|
||||
{
|
||||
if (args.Node.DataItem != null)
|
||||
SelectFunction(args.Node.DataItem);
|
||||
}
|
||||
|
||||
void UpdateFunctionName()
|
||||
{
|
||||
if (_selectedFunction == null)
|
||||
return;
|
||||
|
||||
var name = string.IsNullOrEmpty(_noun) ? _verb : $"{_verb}-{_noun}";
|
||||
|
||||
_selectedFunction.Name = name;
|
||||
|
||||
if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(_selectedFunction.Content))
|
||||
{
|
||||
var declaration = new System.Text.RegularExpressions.Regex(
|
||||
@"function\s+[^\s{(]+", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
|
||||
if (declaration.IsMatch(_selectedFunction.Content))
|
||||
_selectedFunction.Content = declaration.Replace(_selectedFunction.Content, $"function {name}", 1);
|
||||
}
|
||||
}
|
||||
|
||||
void OnManifestModeChanged(bool advanced)
|
||||
{
|
||||
if (advanced)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_manifest.RootModule))
|
||||
_manifest.RootModule = $"{Module.Name}.psm1";
|
||||
|
||||
Module.Manifest = ModuleService.GenerateManifest(_manifest, ExportedFunctions, ExportedAliases);
|
||||
}
|
||||
else
|
||||
{
|
||||
_manifest = ModuleService.ParseManifest(Module.Manifest);
|
||||
}
|
||||
|
||||
_advancedManifest = advanced;
|
||||
}
|
||||
|
||||
void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Module.Name))
|
||||
{
|
||||
MessageService.Error("Name is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsNew && ModuleService.ModuleExists(Module.Name))
|
||||
{
|
||||
MessageService.Error("A module with that name already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var function in Module.Functions)
|
||||
{
|
||||
if (!ModuleService.IsValidFunctionName(function.Name))
|
||||
{
|
||||
MessageService.Error($"'{function.Name}' is not a valid function name. Use Verb-Noun format.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var duplicate = Module.Functions
|
||||
.GroupBy(f => f.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicate != null)
|
||||
{
|
||||
MessageService.Error($"Duplicate function name '{duplicate.Key}'. Function names must be unique.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_advancedManifest)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_manifest.RootModule))
|
||||
_manifest.RootModule = $"{Module.Name}.psm1";
|
||||
|
||||
Module.Manifest = ModuleService.GenerateManifest(_manifest, ExportedFunctions, ExportedAliases);
|
||||
}
|
||||
|
||||
ModuleService.SaveModule(Module);
|
||||
|
||||
_isNew = false;
|
||||
|
||||
MessageService.Success("Module saved!");
|
||||
|
||||
NavigationManager.NavigateTo("/Scripting/Modules");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageService.Error("Could not save module!");
|
||||
Logger?.LogError(ex, "Could not save module");
|
||||
}
|
||||
}
|
||||
}
|
||||
57
LANCommander.Server/UI/Pages/Scripting/Modules/Index.razor
Normal file
57
LANCommander.Server/UI/Pages/Scripting/Modules/Index.razor
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
@page "/Scripting/Modules"
|
||||
@attribute [Authorize(Roles = RoleService.AdministratorRoleName)]
|
||||
@inject ModuleService ModuleService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IMessageService MessageService
|
||||
@inject ILogger<Index> Logger
|
||||
|
||||
<PageHeader Title="Modules">
|
||||
<PageHeaderExtra>
|
||||
<Button OnClick="@(() => NavigationManager.NavigateTo("/Scripting/Modules/Add"))" Type="@ButtonType.Primary">Add Module</Button>
|
||||
</PageHeaderExtra>
|
||||
</PageHeader>
|
||||
|
||||
<PageContent>
|
||||
<Table TItem="Module" DataSource="@Modules" Size="@TableSize.Small" Context="module" Responsive>
|
||||
<PropertyColumn Property="m => m.Name" Title="Name" Sortable DefaultSortOrder="SortDirection.Ascending" />
|
||||
<ActionColumn Title="" Style="text-align: right">
|
||||
<Flex Gap="FlexGap.Small" Align="FlexAlign.Center" Justify="FlexJustify.End">
|
||||
<a href="@($"/Scripting/Modules/{module.Name}")" class="ant-btn ant-btn-primary">Edit</a>
|
||||
<Popconfirm OnConfirm="() => Delete(module)" Title="Are you sure you want to delete this module?">
|
||||
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
</ActionColumn>
|
||||
</Table>
|
||||
</PageContent>
|
||||
|
||||
@code {
|
||||
IEnumerable<Module> Modules { get; set; } = new List<Module>();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Load();
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
Modules = ModuleService.GetModules().ToList();
|
||||
}
|
||||
|
||||
void Delete(Module module)
|
||||
{
|
||||
try
|
||||
{
|
||||
ModuleService.DeleteModule(module.Name);
|
||||
|
||||
Load();
|
||||
|
||||
MessageService.Success("Module deleted!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageService.Error("Could not delete module!");
|
||||
Logger?.LogError(ex, "Could not delete module");
|
||||
}
|
||||
}
|
||||
}
|
||||
86
LANCommander.Server/UI/Pages/Scripting/Snippets/Edit.razor
Normal file
86
LANCommander.Server/UI/Pages/Scripting/Snippets/Edit.razor
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
@page "/Scripting/Snippets/Add"
|
||||
@page "/Scripting/Snippets/{Group}/{Name}"
|
||||
@attribute [Authorize(Roles = RoleService.AdministratorRoleName)]
|
||||
@inject ScriptService ScriptService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IMessageService MessageService
|
||||
@inject ILogger<Edit> Logger
|
||||
|
||||
<PageHeader Title="@(IsNew ? "Add Snippet" : $"{Snippet.Group} / {Snippet.Name}")">
|
||||
<PageHeaderExtra>
|
||||
<Flex Gap="FlexGap.Small">
|
||||
<Button OnClick="@(() => NavigationManager.NavigateTo("/Scripting/Snippets"))">Back</Button>
|
||||
<Button OnClick="Save" Type="@ButtonType.Primary">Save</Button>
|
||||
</Flex>
|
||||
</PageHeaderExtra>
|
||||
</PageHeader>
|
||||
|
||||
<PageContent>
|
||||
<Form Model="@Snippet" Layout="@FormLayout.Vertical">
|
||||
<FormItem Label="Group">
|
||||
<Input @bind-Value="@context.Group" />
|
||||
</FormItem>
|
||||
<FormItem Label="Name">
|
||||
<Input @bind-Value="@context.Name" />
|
||||
</FormItem>
|
||||
<FormItem Label="Content">
|
||||
<PowerShellScriptEditor @bind-Value="@context.Content" OnSave="Save" Height="500" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</PageContent>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Group { get; set; }
|
||||
[Parameter] public string Name { get; set; }
|
||||
|
||||
Snippet Snippet { get; set; } = new();
|
||||
|
||||
string _originalGroup;
|
||||
string _originalName;
|
||||
|
||||
bool IsNew => string.IsNullOrEmpty(_originalName);
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Name))
|
||||
{
|
||||
var snippet = ScriptService.GetSnippet(Group, Name);
|
||||
|
||||
if (snippet != null)
|
||||
{
|
||||
Snippet = snippet;
|
||||
_originalGroup = snippet.Group;
|
||||
_originalName = snippet.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Snippet.Group) || string.IsNullOrWhiteSpace(Snippet.Name))
|
||||
{
|
||||
MessageService.Error("Group and Name are required.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsNew && (_originalGroup != Snippet.Group || _originalName != Snippet.Name))
|
||||
ScriptService.DeleteSnippet(_originalGroup, _originalName);
|
||||
|
||||
ScriptService.SaveSnippet(Snippet);
|
||||
|
||||
_originalGroup = Snippet.Group;
|
||||
_originalName = Snippet.Name;
|
||||
|
||||
MessageService.Success("Snippet saved!");
|
||||
|
||||
NavigationManager.NavigateTo("/Scripting/Snippets");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageService.Error("Could not save snippet!");
|
||||
Logger?.LogError(ex, "Could not save snippet");
|
||||
}
|
||||
}
|
||||
}
|
||||
58
LANCommander.Server/UI/Pages/Scripting/Snippets/Index.razor
Normal file
58
LANCommander.Server/UI/Pages/Scripting/Snippets/Index.razor
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
@page "/Scripting/Snippets"
|
||||
@attribute [Authorize(Roles = RoleService.AdministratorRoleName)]
|
||||
@inject ScriptService ScriptService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IMessageService MessageService
|
||||
@inject ILogger<Index> Logger
|
||||
|
||||
<PageHeader Title="Snippets">
|
||||
<PageHeaderExtra>
|
||||
<Button OnClick="@(() => NavigationManager.NavigateTo("/Scripting/Snippets/Add"))" Type="@ButtonType.Primary">Add Snippet</Button>
|
||||
</PageHeaderExtra>
|
||||
</PageHeader>
|
||||
|
||||
<PageContent>
|
||||
<Table TItem="Snippet" DataSource="@Snippets" Size="@TableSize.Small" Context="snippet" Responsive>
|
||||
<PropertyColumn Property="s => s.Group" Title="Group" Sortable DefaultSortOrder="SortDirection.Ascending" />
|
||||
<PropertyColumn Property="s => s.Name" Title="Name" Sortable />
|
||||
<ActionColumn Title="" Style="text-align: right">
|
||||
<Flex Gap="FlexGap.Small" Align="FlexAlign.Center" Justify="FlexJustify.End">
|
||||
<a href="@($"/Scripting/Snippets/{snippet.Group}/{snippet.Name}")" class="ant-btn ant-btn-primary">Edit</a>
|
||||
<Popconfirm OnConfirm="() => Delete(snippet)" Title="Are you sure you want to delete this snippet?">
|
||||
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
</ActionColumn>
|
||||
</Table>
|
||||
</PageContent>
|
||||
|
||||
@code {
|
||||
IEnumerable<Snippet> Snippets { get; set; } = new List<Snippet>();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Load();
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
Snippets = ScriptService.GetSnippets().OrderBy(s => s.Group).ThenBy(s => s.Name).ToList();
|
||||
}
|
||||
|
||||
void Delete(Snippet snippet)
|
||||
{
|
||||
try
|
||||
{
|
||||
ScriptService.DeleteSnippet(snippet.Group, snippet.Name);
|
||||
|
||||
Load();
|
||||
|
||||
MessageService.Success("Snippet deleted!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageService.Error("Could not delete snippet!");
|
||||
Logger?.LogError(ex, "Could not delete snippet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,12 +17,14 @@
|
|||
[Parameter] public string? Value { get; set; }
|
||||
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
|
||||
[Parameter] public string? ScriptType { get; set; }
|
||||
[Parameter] public IEnumerable<ModuleFunctionCompletion>? ModuleFunctions { get; set; }
|
||||
|
||||
static bool _completionsRegistered;
|
||||
static bool _yamlCompletionsRegistered;
|
||||
bool _isEditorInitialized = false;
|
||||
string? _previousValue;
|
||||
string? _previousScriptType;
|
||||
IEnumerable<ModuleFunctionCompletion>? _previousModuleFunctions;
|
||||
IJSObjectReference? _module;
|
||||
|
||||
StandaloneCodeEditor _editor;
|
||||
|
|
@ -62,6 +64,11 @@
|
|||
await RunValidationAsync();
|
||||
}
|
||||
|
||||
if (_isEditorInitialized && _module != null && !ReferenceEquals(ModuleFunctions, _previousModuleFunctions))
|
||||
{
|
||||
await PushModuleFunctionsAsync();
|
||||
}
|
||||
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
|
|
@ -99,10 +106,29 @@
|
|||
if (Language.Id == "powershell")
|
||||
await _module.InvokeVoidAsync("SetupFileDropHandler");
|
||||
|
||||
if (Language.Id == "powershell")
|
||||
await PushModuleFunctionsAsync();
|
||||
|
||||
if (OnReady.HasDelegate)
|
||||
await OnReady.InvokeAsync();
|
||||
}
|
||||
|
||||
async Task PushModuleFunctionsAsync()
|
||||
{
|
||||
if (_module == null)
|
||||
return;
|
||||
|
||||
_previousModuleFunctions = ModuleFunctions;
|
||||
|
||||
await _module.InvokeVoidAsync("setModuleFunctions", ModuleFunctions ?? []);
|
||||
}
|
||||
|
||||
public async Task LayoutAsync()
|
||||
{
|
||||
if (_isEditorInitialized && _editor != null)
|
||||
await _editor.Layout();
|
||||
}
|
||||
|
||||
public async Task InsertText(string text)
|
||||
{
|
||||
var selection = await _editor.GetSelection();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
namespace LANCommander.UI.Components;
|
||||
|
||||
public record ModuleFunctionCompletion(string Name, string? Synopsis, string? Module);
|
||||
|
|
@ -52,8 +52,15 @@ const variables: VariableDefinition[] = [
|
|||
{ name: "$GameId", type: "string", description: "Game GUID identifier", scriptTypes: ["BeforeStart", "AfterStop", "GameStarted", "GameStopped"] },
|
||||
];
|
||||
|
||||
interface ModuleFunctionDefinition {
|
||||
name: string;
|
||||
synopsis: string | null;
|
||||
module: string | null;
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
let currentScriptType: string | null = null;
|
||||
let moduleFunctions: ModuleFunctionDefinition[] = [];
|
||||
|
||||
export function registerPowerShellCompletions(): void {
|
||||
if (registered || typeof monaco === "undefined") return;
|
||||
|
|
@ -67,6 +74,10 @@ export function setScriptType(scriptType: string | null): void {
|
|||
currentScriptType = scriptType;
|
||||
}
|
||||
|
||||
export function setModuleFunctions(functions: ModuleFunctionDefinition[]): void {
|
||||
moduleFunctions = functions ?? [];
|
||||
}
|
||||
|
||||
function getFilteredVariables(): VariableDefinition[] {
|
||||
if (!currentScriptType) return variables;
|
||||
|
||||
|
|
@ -177,6 +188,18 @@ function registerCompletionProvider(): void {
|
|||
|
||||
// Cmdlet name completions
|
||||
if (!paramMatch && !memberMatch) {
|
||||
for (const fn of moduleFunctions) {
|
||||
suggestions.push({
|
||||
label: fn.name,
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: fn.name,
|
||||
detail: fn.module ? `Module: ${fn.module}` : "Module function",
|
||||
documentation: fn.synopsis || undefined,
|
||||
range,
|
||||
sortText: `0_${fn.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < allCmdlets.length; i++) {
|
||||
const c = allCmdlets[i];
|
||||
const isBuiltin = i >= cmdlets.length;
|
||||
|
|
@ -228,6 +251,24 @@ function registerHoverProvider(): void {
|
|||
],
|
||||
};
|
||||
}
|
||||
|
||||
const fn = moduleFunctions.find((f) => f.name === match![0]);
|
||||
if (fn) {
|
||||
const body = (fn.module ? `**Module:** ${fn.module}` : "Module function") +
|
||||
(fn.synopsis ? `\n\n${fn.synopsis}` : "");
|
||||
return {
|
||||
range: {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: start,
|
||||
endColumn: end,
|
||||
},
|
||||
contents: [
|
||||
{ value: `**${fn.name}**` },
|
||||
{ value: body },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export { SplitPane } from "./Components/SplitPane/SplitPane";
|
|||
export { TimeProvider } from "./Components/LocalTime/TimeProvider";
|
||||
export { Terminal } from "./Components/Terminal/Terminal";
|
||||
export { DomHelper } from "./Components/DomHelper/DomHelper";
|
||||
export { registerPowerShellCompletions, setScriptType, validateScript, insertSnippet, getScriptTemplate } from "./Components/MonacoCodeEditor/PowerShellCompletionProvider";
|
||||
export { registerPowerShellCompletions, setScriptType, setModuleFunctions, validateScript, insertSnippet, getScriptTemplate } from "./Components/MonacoCodeEditor/PowerShellCompletionProvider";
|
||||
export { registerYamlCompletions } from "./Components/MonacoCodeEditor/YamlCompletionProvider";
|
||||
export { UploadManager } from "./Components/UploadManager/UploadManager";
|
||||
export { SetupFileDropHandler } from "./Components/CodeInput/FileDropHandler";
|
||||
Loading…
Add table
Add a link
Reference in a new issue