Skip to content

Commit

Permalink
Introduce DotMake.CommandLine for easier System.CommandLine organization
Browse files Browse the repository at this point in the history
  • Loading branch information
simshaun committed Aug 18, 2024
1 parent 8da68ed commit 0324947
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 163 deletions.
31 changes: 20 additions & 11 deletions NodeSwap/Commands/AvailCommand.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
using System;
using System.CommandLine.Invocation;
using System.Threading.Tasks;
using DotMake.CommandLine;
using NodeSwap.Utils;

namespace NodeSwap.Commands;

public class AvailCommand(NodeJsWebApi nodeWeb) : ICommandHandler
[CliCommand(
Description =
"Discover Node.js versions available for download.",
Parent = typeof(RootCommand)
)]
public class AvailCommand(NodeJsWebApi nodeWeb)
{
public Task<int> InvokeAsync(InvocationContext context)
{
var versionPrefix = context.ParseResult.ValueForArgument("prefix");
[CliArgument(
Description =
"Can be specific like `22.6.0`, or fuzzy like `22.6` or `22`.")
]
public string Prefix { get; set; } = "";

public async Task<int> RunAsync()
{
try
{
var versions = nodeWeb.GetInstallableNodeVersions(versionPrefix?.ToString());
var versions = await nodeWeb.GetInstallableNodeVersions(Prefix);
if (versions.Count == 0)
{
Console.WriteLine("None found");
return Task.Factory.StartNew(() => 1);
return 1;
}

var consoleWidth = Console.WindowWidth;
Expand All @@ -29,13 +38,13 @@ public Task<int> InvokeAsync(InvocationContext context)
(v) => v.ToString().PadLeft(consoleWidth / numColumns, ' ')
);
Console.WriteLine();

return Task.Factory.StartNew(() => 0);
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message);
return Task.Factory.StartNew(() => 1);
await Console.Error.WriteLineAsync(e.Message);
return 1;
}

return 0;
}
}
165 changes: 97 additions & 68 deletions NodeSwap/Commands/InstallCommand.cs
Original file line number Diff line number Diff line change
@@ -1,118 +1,147 @@
using System;
using System.CommandLine.Invocation;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Timers;
using DotMake.CommandLine;
using NodeSwap.Utils;
using ShellProgressBar;

namespace NodeSwap.Commands;

[CliCommand(
Description = "Install a version of Node.js",
Parent = typeof(RootCommand)
)]
public class InstallCommand(GlobalContext globalContext, NodeJsWebApi nodeWeb, NodeJs nodeLocal)
: ICommandHandler
{
public async Task<int> InvokeAsync(InvocationContext context)
[CliArgument(Description = "`latest`, specific e.g. `22.6.0`, or fuzzy e.g. `22.6` or `22`.")]
public string Version { get; set; }

[CliOption(Description = "Re-install if installed already")]
public bool Force { get; set; }

public async Task<int> RunAsync()
{
var rawVersion = context.ParseResult.ValueForArgument("version");
if (rawVersion == null)
// Retrieve and validate version argument
if (string.IsNullOrEmpty(Version))
{
await Console.Error.WriteLineAsync("Missing version argument");
return 1;
}

Version version;
if (rawVersion.ToString()?.ToLower() == "latest")
{
try
{
version = nodeWeb.GetLatestNodeVersion();
}
catch (Exception)
{
await Console.Error.WriteLineAsync("Unable to determine latest Node.js version.");
return 1;
}
}
else if (rawVersion.ToString()?.Split(".").Length < 3)
// Determine the version to install
var version = await GetVersion(Version);
if (version == null) return 1;

// Check if the requested version is already installed
if (!Force && IsVersionInstalled(version))
{
try
{
version = nodeWeb.GetLatestNodeVersion(rawVersion.ToString());
}
catch (Exception)
{
await Console.Error.WriteLineAsync($"Unable to get latest Node.js version " +
$"with prefix {rawVersion}.");
return 1;
}
await Console.Error.WriteLineAsync($"{version} already installed");
return 1;
}
else

// Download and install Node.js
var downloadUrl = nodeWeb.GetDownloadUrl(version);
var zipPath = Path.Join(globalContext.StoragePath, Path.GetFileName(downloadUrl));
var downloadResult = await DownloadNodeJs(downloadUrl, zipPath);

if (!downloadResult) return 1;

// Extract the downloaded file
ExtractNodeJs(zipPath);

// Completion message
Console.WriteLine($"Done. To use, run `nodeswap use {version}`");
return 0;
}

private async Task<Version> GetVersion(string rawVersion)
{
try
{
try
{
version = VersionParser.Parse(rawVersion.ToString());
}
catch (ArgumentException)
{
await Console.Error.WriteLineAsync($"Invalid version argument: {rawVersion}");
return 1;
}
}
if (rawVersion.Equals("latest", StringComparison.CurrentCultureIgnoreCase))
return await nodeWeb.GetLatestNodeVersion();

//
// Is the requested version already installed?
//
if (rawVersion.Split(".").Length < 3)
return await nodeWeb.GetLatestNodeVersion(rawVersion);

if (nodeLocal.GetInstalledVersions().FindIndex(v => v.Version.Equals(version)) != -1)
return VersionParser.Parse(rawVersion);
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"{version} already installed");
return 1;
await Console.Error.WriteLineAsync($"Error determining version: {ex.Message}");
return null;
}
}

//
// Download it
//
private bool IsVersionInstalled(Version version)
{
return nodeLocal.GetInstalledVersions().FindIndex(v => v.Version.Equals(version)) != -1;
}

var downloadUrl = nodeWeb.GetDownloadUrl(version);
var zipPath = Path.Join(globalContext.StoragePath, Path.GetFileName(downloadUrl));
private async Task<bool> DownloadNodeJs(string downloadUrl, string zipPath)
{
var progressBar = new ProgressBar(100, "Download progress", new ProgressBarOptions
{
ProgressCharacter = '\u2593',
ForegroundColor = ConsoleColor.Yellow,
ForegroundColorDone = ConsoleColor.Green,
});

var webClient = new WebClient();
webClient.DownloadProgressChanged += (s, e) => { progressBar.Tick(e.ProgressPercentage); };
webClient.DownloadFileCompleted += (s, e) => { progressBar.Dispose(); };

try
{
await webClient.DownloadFileTaskAsync(downloadUrl, zipPath).ConfigureAwait(false);
var httpClient = new HttpClient();
using var response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();

var totalBytes = response.Content.Headers.ContentLength ?? -1L;
var canReportProgress = totalBytes != -1;

await using var fileStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None);
await using var contentStream = await response.Content.ReadAsStreamAsync();

var buffer = new byte[8192];
long totalRead = 0;
int bytesRead;

while ((bytesRead = await contentStream.ReadAsync(buffer)) > 0)
{
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
if (!canReportProgress) continue;
totalRead += bytesRead;
var progressPercentage = (int) (totalRead * 100 / totalBytes);
progressBar.Tick(progressPercentage);
}

progressBar.Dispose();
return true;
}
catch (Exception e)
{
await Console.Error.WriteLineAsync("Unable to download the Node.js zip file.");
if (e.InnerException == null) return 1;
if (e.InnerException == null) return false;
await Console.Error.WriteLineAsync(e.InnerException.Message);
await Console.Error.WriteLineAsync("You may need to run this command from an " +
"elevated prompt. (Run as Administrator)");
return 1;
await Console.Error.WriteLineAsync(
"You may need to run this command from an elevated prompt. (Run as Administrator)");
return false;
}
}

private void ExtractNodeJs(string zipPath)
{
Console.WriteLine("Extracting...");
ConsoleSpinner.Instance.Update();

var timer = new Timer(250);
timer.Elapsed += (s, e) => ConsoleSpinner.Instance.Update();
timer.Enabled = true;
ZipFile.ExtractToDirectory(zipPath, globalContext.StoragePath);
timer.Enabled = false;
timer.Elapsed += (_, _) => ConsoleSpinner.Instance.Update();
timer.Start();

ZipFile.ExtractToDirectory(zipPath, globalContext.StoragePath, overwriteFiles: true);

timer.Stop();
ConsoleSpinner.Reset();
File.Delete(zipPath);

Console.WriteLine($"Done. To use, run `nodeswap use {version}`");
return 0;
}
}
15 changes: 8 additions & 7 deletions NodeSwap/Commands/ListCommand.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
using System;
using System.CommandLine.Invocation;
using System.Threading.Tasks;
using DotMake.CommandLine;

namespace NodeSwap.Commands;

public class ListCommand(NodeJs nodeJs) : ICommandHandler
[CliCommand(
Description = "List installed versions of Node.js.",
Parent = typeof(RootCommand)
)]
public class ListCommand(NodeJs nodeJs)
{
public Task<int> InvokeAsync(InvocationContext context)
public void Run()
{
var versions = nodeJs.GetInstalledVersions();
if (versions.Count == 0)
{
Console.WriteLine("None installed");
return Task.Factory.StartNew(() => 0);
return;
}

Console.WriteLine();
Expand All @@ -22,7 +25,5 @@ public Task<int> InvokeAsync(InvocationContext context)
Console.WriteLine($"{prefix}{v.Version}");
});
Console.WriteLine();

return Task.Factory.StartNew(() => 0);
}
}
6 changes: 6 additions & 0 deletions NodeSwap/Commands/RootCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using DotMake.CommandLine;

namespace NodeSwap.Commands;

[CliCommand]
public class RootCommand;
27 changes: 16 additions & 11 deletions NodeSwap/Commands/UninstallCommand.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
using System;
using System.CommandLine.Invocation;
using System.IO;
using System.Threading.Tasks;
using DotMake.CommandLine;

namespace NodeSwap.Commands;

public class UninstallCommand(NodeJs nodeLocal) : ICommandHandler
[CliCommand(
Description = "Uninstall a specific version of Node.js",
Parent = typeof(RootCommand)
)]
public class UninstallCommand(NodeJs nodeLocal)
{
public async Task<int> InvokeAsync(InvocationContext context)
[CliArgument(Description = "e.g. `22.6.0`. Run `list` command to see installed versions.")]
public string Version { get; set; }

public int Run()
{
var rawVersion = context.ParseResult.ValueForArgument("version");
if (rawVersion == null)
if (Version == null)
{
await Console.Error.WriteLineAsync($"Missing version argument");
Console.Error.WriteLine("Missing version argument");
return 1;
}

Version version;
try
{
version = VersionParser.StrictParse(rawVersion.ToString()!);
version = VersionParser.StrictParse(Version);
}
catch (ArgumentException)
{
await Console.Error.WriteLineAsync($"Invalid version argument: {rawVersion}");
Console.Error.WriteLine($"Invalid version argument: {Version}");
return 1;
}

Expand All @@ -34,7 +39,7 @@ public async Task<int> InvokeAsync(InvocationContext context)
var nodeVersion = nodeLocal.GetInstalledVersions().Find(v => v.Version.Equals(version));
if (nodeVersion == null)
{
await Console.Error.WriteLineAsync($"{version} not installed");
Console.Error.WriteLine($"{version} not installed");
return 1;
}

Expand All @@ -48,7 +53,7 @@ public async Task<int> InvokeAsync(InvocationContext context)
}
catch (IOException)
{
await Console.Error.WriteLineAsync($"Unable to delete {nodeVersion.Path}");
Console.Error.WriteLine($"Unable to delete {nodeVersion.Path}");
return 1;
}

Expand Down
Loading

0 comments on commit 0324947

Please sign in to comment.