Merge pull request #687 from erri120/issue-684

CLI overhaul and Changelog autogen
This commit is contained in:
Timothy Baldridge 2020-04-06 20:57:28 -06:00 committed by GitHub
commit 9e6b1881de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 645 additions and 83 deletions

View File

@ -121,4 +121,67 @@ csharp_preserve_single_line_blocks = true
# CS4014: Task not awaited
dotnet_diagnostic.CS4014.severity = error
# CS1998: Async function does not contain await
dotnet_diagnostic.CS1998.severity = silent
dotnet_diagnostic.CS1998.severity = silent
###############################
# C# Nullability #
###############################
# CS8602: Dereference of a possibly null reference.
dotnet_diagnostic.CS8602.severity = error
# CS8600: Converting null literal or possible null value to non-nullable type.
dotnet_diagnostic.CS8600.severity = error
# CS8619: Nullability of reference types in value doesn't match target type.
dotnet_diagnostic.CS8619.severity = error
# CS8603: Possible null reference return.
dotnet_diagnostic.CS8603.severity = error
# CS8625: Cannot convert null literal to non-nullable reference type.
dotnet_diagnostic.CS8625.severity = error
# CS8653: A default expression introduces a null value for a type parameter.
dotnet_diagnostic.CS8653.severity = silent
# CS8601: Possible null reference assignment.
dotnet_diagnostic.CS8601.severity = error
# CS8604: Possible null reference argument.
dotnet_diagnostic.CS8604.severity = error
# CS8622: Nullability of reference types in type of parameter doesn't match the target delegate.
dotnet_diagnostic.CS8622.severity = error
# CS8610: Nullability of reference types in type of parameter doesn't match overridden member.
dotnet_diagnostic.CS8610.severity = error
# CS8618: Non-nullable field is uninitialized. Consider declaring as nullable.
dotnet_diagnostic.CS8618.severity = error
# CS8629: Nullable value type may be null.
dotnet_diagnostic.CS8629.severity = error
# CS8620: Argument cannot be used for parameter due to differences in the nullability of reference types.
dotnet_diagnostic.CS8620.severity = error
# CS8614: Nullability of reference types in type of parameter doesn't match implicitly implemented member.
dotnet_diagnostic.CS8614.severity = error
# CS8617: Nullability of reference types in type of parameter doesn't match implemented member.
dotnet_diagnostic.CS8617.severity = error
# CS8611: Nullability of reference types in type of parameter doesn't match partial method declaration.
dotnet_diagnostic.CS8611.severity = error
# CS8597: Thrown value may be null.
dotnet_diagnostic.CS8597.severity = error
# CS8609: Nullability of reference types in return type doesn't match overridden member.
dotnet_diagnostic.CS8609.severity = error
# CS8714: The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
dotnet_diagnostic.CS8714.severity = error
# CS8605: Unboxing a possibly null value.
dotnet_diagnostic.CS8605.severity = error

View File

@ -1,9 +1,178 @@
using System;
using System.Linq;
using System.Reflection;
using Alphaleonis.Win32.Filesystem;
using CommandLine;
using Wabbajack.CLI.Verbs;
using Wabbajack.Common;
namespace Wabbajack.CLI
{
public enum ExitCode
{
BadArguments = -1,
Ok = 0,
Error = 1
}
/// <summary>
/// Abstract class to mark attributes which need validating
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
internal abstract class AValidateAttribute : Attribute
{
/// <summary>
/// Custom message if validation failed. Use placeholder %1 to insert the value
/// </summary>
public string? CustomMessage { get; set; }
}
/// <summary>
/// Validating if the file exists
/// </summary>
internal class IsFileAttribute : AValidateAttribute
{
/// <summary>
/// Extension the file should have
/// </summary>
public string? Extension { get; set; }
}
/// <summary>
/// Validating if the directory exists
/// </summary>
internal class IsDirectoryAttribute : AValidateAttribute
{
/// <summary>
/// Create the directory if it does not exists
/// </summary>
public bool Create { get; set; }
}
internal static class CLIUtils
{
/// <summary>
/// Validates all Attributes of type <see cref="AValidateAttribute"/>
/// </summary>
/// <param name="verb">The verb to validate</param>
/// <returns></returns>
internal static bool HasValidArguments(AVerb verb)
{
var props = verb.GetType().GetProperties().Where(p =>
{
var hasAttr = p.HasAttribute(typeof(OptionAttribute))
&& p.HasAttribute(typeof(AValidateAttribute));
if (!hasAttr)
return false;
if (p.PropertyType != typeof(string))
return false;
var value = p.GetValue(verb);
if (value == null)
return false;
var stringValue = (string)value;
return !string.IsNullOrWhiteSpace(stringValue);
});
var valid = true;
props.Do(p =>
{
if (!valid)
return;
var valueObject = p.GetValue(verb);
// not really possible since we filtered them out but whatever
if (valueObject == null)
return;
var value = (string)valueObject;
var attribute = (AValidateAttribute)p.GetAttribute(typeof(AValidateAttribute));
var isFile = false;
if (p.HasAttribute(typeof(IsFileAttribute)))
{
var fileAttribute = (IsFileAttribute)attribute;
isFile = true;
if (!File.Exists(value))
valid = false;
else
{
if (!string.IsNullOrWhiteSpace(fileAttribute.Extension))
{
valid = value.EndsWith(fileAttribute.Extension);
if(!valid)
Exit($"The file {value} does not have the extension {fileAttribute.Extension}!",
ExitCode.BadArguments);
}
}
}
if (p.HasAttribute(typeof(IsDirectoryAttribute)))
{
var dirAttribute = (IsDirectoryAttribute)attribute;
var exists = Directory.Exists(value);
if (!exists)
{
if (dirAttribute.Create)
{
Log($"Directory {value} does not exist and will be created");
Directory.CreateDirectory(value);
}
else
valid = false;
}
}
if (valid)
return;
var message = string.IsNullOrWhiteSpace(attribute.CustomMessage)
? isFile
? $"The file {value} does not exist!"
: $"The folder {value} does not exist!"
: attribute.CustomMessage.Replace("%1", value);
var optionAttribute = (OptionAttribute)p.GetAttribute(typeof(OptionAttribute));
if (optionAttribute.Required)
Exit(message, ExitCode.BadArguments);
else
Log(message);
});
return valid;
}
/// <summary>
/// Gets an attribute of a specific type
/// </summary>
/// <param name="member"></param>
/// <param name="attribute"></param>
/// <returns></returns>
internal static Attribute GetAttribute(this MemberInfo member, Type attribute)
{
var attributes = member.GetCustomAttributes(attribute);
return attributes.ElementAt(0);
}
/// <summary>
/// Checks if a <see cref="MemberInfo"/> has a custom attribute
/// </summary>
/// <param name="member"></param>
/// <param name="attribute"></param>
/// <returns></returns>
internal static bool HasAttribute(this MemberInfo member, Type attribute)
{
var attributes = member.GetCustomAttributes(attribute);
return attributes.Count() == 1;
}
internal static void Log(string msg, bool newLine = true)
{
//TODO: maybe also write to a log file?
@ -13,7 +182,7 @@ namespace Wabbajack.CLI
Console.Write(msg);
}
internal static int Exit(string msg, int code)
internal static ExitCode Exit(string msg, ExitCode code)
{
Log(msg);
return code;

View File

@ -16,7 +16,8 @@ namespace Wabbajack.CLI
typeof(ChangeDownload),
typeof(ServerLog),
typeof(MyFiles),
typeof(DeleteFile)
typeof(DeleteFile),
typeof(Changelog)
};
}
}

View File

@ -19,6 +19,7 @@ namespace Wabbajack.CLI
(ServerLog opts) => opts.Execute(),
(MyFiles opts) => opts.Execute(),
(DeleteFile opts) => opts.Execute(),
(Changelog opts) => opts.Execute(),
errs => 1);
}
}

View File

@ -0,0 +1,7 @@
{
"profiles": {
"Wabbajack.CLI": {
"commandName": "Project"
}
}
}

View File

@ -1,6 +1,4 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace Wabbajack.CLI.Verbs
{
@ -8,10 +6,13 @@ namespace Wabbajack.CLI.Verbs
{
public int Execute()
{
return Run().Result;
if (!CLIUtils.HasValidArguments(this))
CLIUtils.Exit("The provided arguments are not valid! Check previous messages for more information",
ExitCode.BadArguments);
return (int)Run().Result;
}
protected abstract Task<int> Run();
protected abstract Task<ExitCode> Run();
}
}

View File

@ -17,17 +17,20 @@ namespace Wabbajack.CLI.Verbs
[Verb("change-download", HelpText = "Move or Copy all used Downloads from a Modlist to another directory")]
public class ChangeDownload : AVerb
{
[IsDirectory(CustomMessage = "Downloads folder %1 does not exist!")]
[Option("input", Required = true, HelpText = "Input folder containing the downloads you want to move")]
public string Input { get; set; }
public string? Input { get; set; }
[IsDirectory(Create = true)]
[Option("output", Required = true, HelpText = "Output folder the downloads should be transferred to")]
public string Output { get; set; }
public string? Output { get; set; }
[IsFile(CustomMessage = "Modlist file %1 does not exist!")]
[Option("modlist", Required = true, HelpText = "The Modlist, can either be a .wabbajack or a modlist.txt file")]
public string Modlist { get; set; }
public string? Modlist { get; set; }
[Option("mods", Required = false, HelpText = "Mods folder location if the provided modlist file is an MO2 modlist.txt")]
public string Mods { get; set; }
public string? Mods { get; set; }
[Option("copy", Default = true, HelpText = "Whether to copy the files", SetName = "copy")]
public bool Copy { get; set; }
@ -55,27 +58,15 @@ namespace Wabbajack.CLI.Verbs
}
}
protected override async Task<int> Run()
protected override async Task<ExitCode> Run()
{
if (!File.Exists(Modlist))
return CLIUtils.Exit($"The file {Modlist} does not exist!", -1);
if (!Directory.Exists(Input))
return CLIUtils.Exit($"The input directory {Input} does not exist!", -1);
if (!Directory.Exists(Output))
{
CLIUtils.Log($"The output directory {Output} does not exist, it will be created.");
Directory.CreateDirectory(Output);
}
if (!Modlist.EndsWith(Consts.ModListExtension) && !Modlist.EndsWith("modlist.txt"))
return CLIUtils.Exit($"The file {Modlist} is not a valid modlist file!", -1);
if (Modlist != null && (!Modlist.EndsWith(Consts.ModListExtension) && !Modlist.EndsWith("modlist.txt")))
return CLIUtils.Exit($"The file {Modlist} is not a valid modlist file!", ExitCode.BadArguments);
if (Copy && Move)
return CLIUtils.Exit("You can't set both copy and move flags!", -1);
return CLIUtils.Exit("You can't set both copy and move flags!", ExitCode.BadArguments);
var isModlist = Modlist.EndsWith(Consts.ModListExtension);
var isModlist = Modlist != null && Modlist.EndsWith(Consts.ModListExtension);
var list = new List<TransferFile>();
@ -89,12 +80,12 @@ namespace Wabbajack.CLI.Verbs
}
catch (Exception e)
{
return CLIUtils.Exit($"Error while loading the Modlist!\n{e}", 1);
return CLIUtils.Exit($"Error while loading the Modlist!\n{e}", ExitCode.Error);
}
if (modlist == null)
{
return CLIUtils.Exit("The Modlist could not be loaded!", 1);
return CLIUtils.Exit("The Modlist could not be loaded!", ExitCode.Error);
}
CLIUtils.Log($"Modlist contains {modlist.Archives.Count} archives.");
@ -153,18 +144,18 @@ namespace Wabbajack.CLI.Verbs
else
{
if (!Directory.Exists(Mods))
return CLIUtils.Exit($"Mods directory {Mods} does not exist!", -1);
return CLIUtils.Exit($"Mods directory {Mods} does not exist!", ExitCode.BadArguments);
CLIUtils.Log($"Reading modlist.txt from {Modlist}");
string[] modlist = File.ReadAllLines(Modlist);
if (modlist == null || modlist.Length == 0)
return CLIUtils.Exit($"Provided modlist.txt file at {Modlist} is empty or could not be read!", -1);
return CLIUtils.Exit($"Provided modlist.txt file at {Modlist} is empty or could not be read!", ExitCode.BadArguments);
var mods = modlist.Where(s => s.StartsWith("+")).Select(s => s.Substring(1)).ToHashSet();
if (mods.Count == 0)
return CLIUtils.Exit("Counted mods from modlist.txt are 0!", -1);
return CLIUtils.Exit("Counted mods from modlist.txt are 0!", ExitCode.BadArguments);
CLIUtils.Log($"Found {mods.Count} mods in modlist.txt");

View File

@ -0,0 +1,346 @@
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using CommandLine;
using Markdig;
using Markdig.Syntax;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack.CLI.Verbs
{
[Verb("changelog", HelpText = "Generate a changelog using two different versions of the same Modlist.")]
public class Changelog : AVerb
{
[IsFile(CustomMessage = "Modlist %1 does not exist!", Extension = Consts.ModListExtension)]
[Option("original", Required = true, HelpText = "The original/previous modlist")]
public string? Original { get; set; }
[IsFile(CustomMessage = "Modlist %1 does not exist!", Extension = Consts.ModListExtension)]
[Option("update", Required = true, HelpText = "The current/updated modlist")]
public string? Update { get; set; }
[Option('o', "output", Required = false, HelpText = "The output file")]
public string? Output { get; set; }
[Option("changes-downloads", Required = false, Default = true, HelpText = "Include download changes")]
public bool IncludeDownloadChanges { get; set; }
[Option("changes-mods", Required = false, Default = false, HelpText = "Include mods changes")]
public bool IncludeModChanges { get; set; }
[Option("changes-loadorder", Required = false, Default = false, HelpText = "Include load order changes")]
public bool IncludeLoadOrderChanges { get; set; }
protected override async Task<ExitCode> Run()
{
if (Original == null)
return ExitCode.BadArguments;
if (Update == null)
return ExitCode.BadArguments;
ModList original, update;
try
{
original = AInstaller.LoadFromFile(Original);
}
catch (Exception e)
{
return CLIUtils.Exit($"Error while loading the original Modlist from {Original}!\n{e}", ExitCode.Error);
}
if(original == null)
return CLIUtils.Exit($"The Modlist from {Original} could not be loaded!", ExitCode.Error);
try
{
update = AInstaller.LoadFromFile(Update);
}
catch (Exception e)
{
return CLIUtils.Exit($"Error while loading the updated Modlist from {Update}!\n{e}", ExitCode.Error);
}
if(update == null)
return CLIUtils.Exit($"The Modlist from {Update} could not be loaded!", ExitCode.Error);
var downloadSizeChanges = original.DownloadSize - update.DownloadSize;
var installSizeChanges = original.InstallSize - update.InstallSize;
var versionRegex = new Regex(@"\s([0-9](\.|\s)?){1,4}");
var matchOriginal = versionRegex.Match(original.Name);
var matchUpdated = versionRegex.Match(update.Name);
if (!matchOriginal.Success || !matchUpdated.Success)
{
return CLIUtils.Exit(
!matchOriginal.Success
? "The name of the original modlist did not match the version check regex!"
: "The name of the updated modlist did not match the version check regex!", ExitCode.Error);
}
var version = matchUpdated.Value.Trim();
var mdText =
$"## {version}\n\n" +
$"**Build at:** `{File.GetCreationTime(Update)}`\n\n" +
"**Info**:\n\n" +
$"- Download Size change: {downloadSizeChanges.ToFileSizeString()} (Total: {update.DownloadSize.ToFileSizeString()})\n" +
$"- Install Size change: {installSizeChanges.ToFileSizeString()} (Total: {update.InstallSize.ToFileSizeString()})\n\n";
if (IncludeDownloadChanges)
{
var updatedArchives = update.Archives
.Where(a => original.Archives.All(x => x.Name != a.Name))
.Where(a =>
{
if (!(a.State is NexusDownloader.State nexusState))
return false;
return original.Archives.Any(x =>
{
if (!(x.State is NexusDownloader.State originalState))
return false;
if (nexusState.Name != originalState.Name)
return false;
if (nexusState.ModID != originalState.ModID)
return false;
if (!int.TryParse(nexusState.FileID, out var currentFileID))
return true;
if (int.TryParse(originalState.FileID, out var originalFileID))
{
return currentFileID > originalFileID;
}
return true;
});
}).ToList();
var newArchives = update.Archives
.Where(a => original.Archives.All(x => x.Name != a.Name))
.Where(a => updatedArchives.All(x => x != a))
.ToList();
var removedArchives = original.Archives
.Where(a => update.Archives.All(x => x.Name != a.Name))
.Where(a => updatedArchives.All(x => x != a))
.ToList();
if(newArchives.Any() || removedArchives.Any())
mdText += "**Download Changes**:\n\n";
updatedArchives.Do(a =>
{
mdText += $"- Updated [{GetModName(a)}]({a.State.GetManifestURL(a)})\n";
});
removedArchives.Do(a =>
{
mdText += $"- Removed [{GetModName(a)}]({a.State.GetManifestURL(a)})\n";
});
newArchives.Do(a =>
{
mdText += $"- Added [{GetModName(a)}]({a.State.GetManifestURL(a)})\n";
});
mdText += "\n";
}
if (IncludeLoadOrderChanges)
{
var originalLoadOrderFile = original.Directives
.Where(d => d is InlineFile)
.Where(d => d.To.EndsWith("loadorder.txt"))
.Cast<InlineFile>()
.First();
var updatedLoadOrderFile = update.Directives
.Where(d => d is InlineFile)
.Where(d => d.To.EndsWith("loadorder.txt"))
.Cast<InlineFile>()
.First();
var originalLoadOrder = GetTextFileFromModlist(Original, original, originalLoadOrderFile.SourceDataID).Split("\n");
var updatedLoadOrder = GetTextFileFromModlist(Update, update, updatedLoadOrderFile.SourceDataID).Split("\n");
var addedPlugins = updatedLoadOrder
.Where(p => originalLoadOrder.All(x => p != x))
.ToList();
var removedPlugins = originalLoadOrder
.Where(p => updatedLoadOrder.All(x => p != x))
.ToList();
if(addedPlugins.Any() || removedPlugins.Any())
mdText += "**Load Order Changes**:\n\n";
addedPlugins.Do(p =>
{
mdText += $"- Added {p}\n";
});
removedPlugins.Do(p =>
{
mdText += $"- Removed {p}\n";
});
mdText += "\n";
}
if (IncludeModChanges)
{
var originalModlistFile = original.Directives
.Where(d => d is InlineFile)
.Where(d => d.To.EndsWith("modlist.txt"))
.Cast<InlineFile>()
.First();
var updatedModlistFile = update.Directives
.Where(d => d is InlineFile)
.Where(d => d.To.EndsWith("modlist.txt"))
.Cast<InlineFile>()
.First();
var originalModlist = GetTextFileFromModlist(Original, original, originalModlistFile.SourceDataID).Split("\n");
var updatedModlist = GetTextFileFromModlist(Update, update, updatedModlistFile.SourceDataID).Split("\n");
var removedMods = originalModlist
.Where(m => m.StartsWith("+"))
.Where(m => updatedModlist.All(x => m != x))
.Select(m => m.Substring(1))
.ToList();
var addedMods = updatedModlist
.Where(m => m.StartsWith("+"))
.Where(m => originalModlist.All(x => m != x))
.Select(m => m.Substring(1))
.ToList();
if (removedMods.Any() || addedMods.Any())
mdText += "**Mod Changes**:\n\n";
addedMods.Do(m =>
{
mdText += $"- Added {m}\n";
});
removedMods.Do(m =>
{
mdText += $"- Removed {m}\n";
});
}
var output = string.IsNullOrWhiteSpace(Output)
? "changelog.md"
: Output;
if (File.Exists(output) && output.EndsWith("md"))
{
CLIUtils.Log($"Output file {output} already exists and is a markdown file. It will be updated with the newest version");
var markdown = File.ReadAllLines(output).ToList();
var lines = mdText.Split("\n");
if (lines.All(l => markdown.Contains(l)))
{
return CLIUtils.Exit("The output file is already up-to-date", ExitCode.Ok);
}
var doc = Markdown.Parse(File.ReadAllText(output));
var hasToc = false;
var tocLine = 0;
var headers = doc
.Where(b => b is HeadingBlock)
.Cast<HeadingBlock>()
.ToList();
if (headers.Count < 2)
{
return CLIUtils.Exit("The provided output file has less than 2 headers!", ExitCode.Error);
}
if (headers[0].Level == 1 && headers[1].Level == 2)
{
if (headers[1].Line - headers[0].Line > headers.Count - 1)
{
var listBlocks = doc
.Where(b => b.Line > headers[0].Line && b.Line < headers[1].Line)
.OfType<ListBlock>()
.ToList();
if (listBlocks.Count == 1)
{
hasToc = true;
tocLine = listBlocks[0].Line;
CLIUtils.Log($"Toc found at {tocLine}");
}
}
}
var firstHeader = headers
.First(h => h.Level >= 2);
var line = firstHeader.Line-1;
if (hasToc)
{
markdown.Insert(tocLine, $"- [{version}](#{ToTocLink(version)})");
line++;
}
markdown.InsertRange(line+1, lines);
File.WriteAllLines(output, markdown);
CLIUtils.Log($"Wrote {markdown.Count} lines to {output}");
return ExitCode.Ok;
}
var text = "# Changelog\n\n" +
$"- [{version}](#{ToTocLink(version)})\n\n" +
$"{mdText}";
File.WriteAllText(output, text);
CLIUtils.Log($"Wrote changelog to {output}");
return ExitCode.Ok;
}
private static string GetTextFileFromModlist(string archive, ModList modlist, string sourceID)
{
var installer = new MO2Installer(archive, modlist, "", "", null);
byte[] bytes = installer.LoadBytesFromPath(sourceID);
return Encoding.Default.GetString(bytes);
}
private static string ToTocLink(string header)
{
return header.Trim().Replace(" ", "").Replace(".", "");
}
private static string GetModName(Archive a)
{
var result = a.Name;
if (a.State is IMetaState metaState)
{
result = metaState.Name;
}
return result;
}
}
}

View File

@ -9,13 +9,12 @@ namespace Wabbajack.CLI.Verbs
public class Decrypt : AVerb
{
[Option('n', "name", Required = true, HelpText = @"Credential to encrypt and store in AppData\Local\Wabbajack")]
public string Name { get; set; }
public string? Name { get; set; }
[Option('o', "output", Required = true, HelpText = @"Output file for the decrypted data")]
public string Output { get; set; }
public string? Output { get; set; }
protected override async Task<int> Run()
protected override async Task<ExitCode> Run()
{
File.WriteAllBytes(Output, Utils.FromEncryptedData(Name));
return 0;

View File

@ -9,8 +9,9 @@ namespace Wabbajack.CLI.Verbs
public class DeleteFile : AVerb
{
[Option('n', "name", Required = true, HelpText = @"Full name (as returned by my-files) of the file")]
public string Name { get; set; }
protected override async Task<int> Run()
public string? Name { get; set; }
protected override async Task<ExitCode> Run()
{
Console.WriteLine(await AuthorAPI.DeleteFile(Name));
return 0;

View File

@ -1,5 +1,4 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
@ -14,16 +13,16 @@ namespace Wabbajack.CLI.Verbs
public class DownloadUrl : AVerb
{
[Option('u', "url", Required = true, HelpText = "Url to download")]
public Uri Url { get; set; }
public Uri? Url { get; set; }
[Option('o', "output", Required = true, HelpText = "Output file name")]
public string Output { get; set; }
public string? Output { get; set; }
protected override async Task<int> Run()
protected override async Task<ExitCode> Run()
{
var state = await DownloadDispatcher.Infer(Url);
if (state == null)
return CLIUtils.Exit($"Could not find download source for URL {Url}", 1);
return CLIUtils.Exit($"Could not find download source for URL {Url}", ExitCode.Error);
DownloadDispatcher.PrepareAll(new []{state});

View File

@ -9,12 +9,13 @@ namespace Wabbajack.CLI.Verbs
public class Encrypt : AVerb
{
[Option('n', "name", Required = true, HelpText = @"Credential to encrypt and store in AppData\Local\Wabbajack")]
public string Name { get; set; }
public string? Name { get; set; }
[IsFile(CustomMessage = "The input file %1 does not exist!")]
[Option('i', "input", Required = true, HelpText = @"Source data file name")]
public string Input { get; set; }
public string? Input { get; set; }
protected override async Task<int> Run()
protected override async Task<ExitCode> Run()
{
File.ReadAllBytes(Input).ToEcryptedData(Name);
return 0;

View File

@ -8,7 +8,7 @@ namespace Wabbajack.CLI.Verbs
[Verb("my-files", HelpText = "List files I have uploaded to the CDN (requires Author API key)")]
public class MyFiles : AVerb
{
protected override async Task<int> Run()
protected override async Task<ExitCode> Run()
{
var files = await AuthorAPI.GetMyFiles();
foreach (var file in files)

View File

@ -1,5 +1,4 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using CommandLine;
using Wabbajack.Lib.FileUploader;
@ -9,7 +8,7 @@ namespace Wabbajack.CLI.Verbs
[Verb("server-log", HelpText = @"Get the latest server log entries", Hidden = false)]
public class ServerLog : AVerb
{
protected override async Task<int> Run()
protected override async Task<ExitCode> Run()
{
Console.WriteLine(await AuthorAPI.GetServerLog());
return 0;

View File

@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CommandLine;
using Wabbajack.Lib.FileUploader;
@ -8,7 +7,7 @@ namespace Wabbajack.CLI.Verbs
[Verb("update-server-modlists", HelpText = "Tell the Build server to update curated modlists (Requires Author API key)")]
public class UpdateModlists : AVerb
{
protected override async Task<int> Run()
protected override async Task<ExitCode> Run()
{
CLIUtils.Log($"Job ID: {await AuthorAPI.UpdateServerModLists()}");
return 0;

View File

@ -1,7 +1,5 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CommandLine;
using Wabbajack.Common;
using Wabbajack.Lib.FileUploader;
namespace Wabbajack.CLI.Verbs
@ -9,7 +7,7 @@ namespace Wabbajack.CLI.Verbs
[Verb("update-nexus-cache", HelpText = "Tell the build server to update the Nexus cache (requires Author API key)")]
public class UpdateNexusCache : AVerb
{
protected override async Task<int> Run()
protected override async Task<ExitCode> Run()
{
CLIUtils.Log($"Job ID: {await AuthorAPI.UpdateNexusCache()}");
return 0;

View File

@ -1,41 +1,25 @@
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using CommandLine;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Validation;
using Wabbajack.VirtualFileSystem;
using File = Alphaleonis.Win32.Filesystem.File;
namespace Wabbajack.CLI.Verbs
{
[Verb("validate", HelpText = @"Validates a Modlist")]
public class Validate : AVerb
{
[IsFile(CustomMessage = "The modlist file %1 does not exist!", Extension = Consts.ModListExtension)]
[Option('i', "input", Required = true, HelpText = @"Modlist file")]
public string Input { get; set; }
public string? Input { get; set; }
/// <summary>
/// Runs the Validation of a Modlist
/// </summary>
/// <param name="opts"></param>
/// <returns>
/// <para>
/// <c>-1</c> bad Input
/// <c>0</c> valid modlist
/// <c>1</c> broken modlist
/// </para>
/// </returns>
protected override async Task<int> Run()
/// <returns></returns>
protected override async Task<ExitCode> Run()
{
if (!File.Exists(Input))
return CLIUtils.Exit($"The file {Input} does not exist!", -1);
if (!Input.EndsWith(Consts.ModListExtension))
return CLIUtils.Exit($"The file {Input} does not end with {Consts.ModListExtension}!", -1);
ModList modlist;
try
@ -44,12 +28,12 @@ namespace Wabbajack.CLI.Verbs
}
catch (Exception e)
{
return CLIUtils.Exit($"Error while loading the Modlist!\n{e}", 1);
return CLIUtils.Exit($"Error while loading the Modlist!\n{e}", ExitCode.Error);
}
if (modlist == null)
{
return CLIUtils.Exit($"The Modlist could not be loaded!", 1);
return CLIUtils.Exit($"The Modlist could not be loaded!", ExitCode.Error);
}
@ -61,7 +45,7 @@ namespace Wabbajack.CLI.Verbs
}
catch (Exception e)
{
return CLIUtils.Exit($"Error during Validation!\n{e}", 1);
return CLIUtils.Exit($"Error during Validation!\n{e}", ExitCode.Error);
}
return CLIUtils.Exit("The Modlist passed the Validation", 0);

View File

@ -12,10 +12,13 @@
<Description>An automated ModList installer</Description>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.7.82" />
<PackageReference Include="Markdig" Version="0.18.3" />
</ItemGroup>
<ItemGroup>

View File

@ -90,7 +90,7 @@ namespace Wabbajack.Common
public static string HashFileExtension => ".xxHash";
public static string MetaFileExtension => ".meta";
public static string ModListExtension = ".wabbajack";
public const string ModListExtension = ".wabbajack";
public static string LocalAppDataPath => Path.Combine(KnownFolders.LocalAppData.Path, "Wabbajack");
public static string MetricsKeyHeader => "x-metrics-key";