Skip to content

Commit 0324947

Browse files
committed
Introduce DotMake.CommandLine for easier System.CommandLine organization
1 parent 8da68ed commit 0324947

File tree

8 files changed

+185
-163
lines changed

8 files changed

+185
-163
lines changed

NodeSwap/Commands/AvailCommand.cs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
using System;
2-
using System.CommandLine.Invocation;
32
using System.Threading.Tasks;
3+
using DotMake.CommandLine;
44
using NodeSwap.Utils;
55

66
namespace NodeSwap.Commands;
77

8-
public class AvailCommand(NodeJsWebApi nodeWeb) : ICommandHandler
8+
[CliCommand(
9+
Description =
10+
"Discover Node.js versions available for download.",
11+
Parent = typeof(RootCommand)
12+
)]
13+
public class AvailCommand(NodeJsWebApi nodeWeb)
914
{
10-
public Task<int> InvokeAsync(InvocationContext context)
11-
{
12-
var versionPrefix = context.ParseResult.ValueForArgument("prefix");
15+
[CliArgument(
16+
Description =
17+
"Can be specific like `22.6.0`, or fuzzy like `22.6` or `22`.")
18+
]
19+
public string Prefix { get; set; } = "";
1320

21+
public async Task<int> RunAsync()
22+
{
1423
try
1524
{
16-
var versions = nodeWeb.GetInstallableNodeVersions(versionPrefix?.ToString());
25+
var versions = await nodeWeb.GetInstallableNodeVersions(Prefix);
1726
if (versions.Count == 0)
1827
{
1928
Console.WriteLine("None found");
20-
return Task.Factory.StartNew(() => 1);
29+
return 1;
2130
}
2231

2332
var consoleWidth = Console.WindowWidth;
@@ -29,13 +38,13 @@ public Task<int> InvokeAsync(InvocationContext context)
2938
(v) => v.ToString().PadLeft(consoleWidth / numColumns, ' ')
3039
);
3140
Console.WriteLine();
32-
33-
return Task.Factory.StartNew(() => 0);
3441
}
3542
catch (Exception e)
3643
{
37-
Console.Error.WriteLine(e.Message);
38-
return Task.Factory.StartNew(() => 1);
44+
await Console.Error.WriteLineAsync(e.Message);
45+
return 1;
3946
}
47+
48+
return 0;
4049
}
4150
}
Lines changed: 97 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,147 @@
11
using System;
2-
using System.CommandLine.Invocation;
32
using System.IO;
43
using System.IO.Compression;
5-
using System.Net;
4+
using System.Net.Http;
65
using System.Threading.Tasks;
76
using System.Timers;
7+
using DotMake.CommandLine;
88
using NodeSwap.Utils;
99
using ShellProgressBar;
1010

1111
namespace NodeSwap.Commands;
1212

13+
[CliCommand(
14+
Description = "Install a version of Node.js",
15+
Parent = typeof(RootCommand)
16+
)]
1317
public class InstallCommand(GlobalContext globalContext, NodeJsWebApi nodeWeb, NodeJs nodeLocal)
14-
: ICommandHandler
1518
{
16-
public async Task<int> InvokeAsync(InvocationContext context)
19+
[CliArgument(Description = "`latest`, specific e.g. `22.6.0`, or fuzzy e.g. `22.6` or `22`.")]
20+
public string Version { get; set; }
21+
22+
[CliOption(Description = "Re-install if installed already")]
23+
public bool Force { get; set; }
24+
25+
public async Task<int> RunAsync()
1726
{
18-
var rawVersion = context.ParseResult.ValueForArgument("version");
19-
if (rawVersion == null)
27+
// Retrieve and validate version argument
28+
if (string.IsNullOrEmpty(Version))
2029
{
2130
await Console.Error.WriteLineAsync("Missing version argument");
2231
return 1;
2332
}
2433

25-
Version version;
26-
if (rawVersion.ToString()?.ToLower() == "latest")
27-
{
28-
try
29-
{
30-
version = nodeWeb.GetLatestNodeVersion();
31-
}
32-
catch (Exception)
33-
{
34-
await Console.Error.WriteLineAsync("Unable to determine latest Node.js version.");
35-
return 1;
36-
}
37-
}
38-
else if (rawVersion.ToString()?.Split(".").Length < 3)
34+
// Determine the version to install
35+
var version = await GetVersion(Version);
36+
if (version == null) return 1;
37+
38+
// Check if the requested version is already installed
39+
if (!Force && IsVersionInstalled(version))
3940
{
40-
try
41-
{
42-
version = nodeWeb.GetLatestNodeVersion(rawVersion.ToString());
43-
}
44-
catch (Exception)
45-
{
46-
await Console.Error.WriteLineAsync($"Unable to get latest Node.js version " +
47-
$"with prefix {rawVersion}.");
48-
return 1;
49-
}
41+
await Console.Error.WriteLineAsync($"{version} already installed");
42+
return 1;
5043
}
51-
else
44+
45+
// Download and install Node.js
46+
var downloadUrl = nodeWeb.GetDownloadUrl(version);
47+
var zipPath = Path.Join(globalContext.StoragePath, Path.GetFileName(downloadUrl));
48+
var downloadResult = await DownloadNodeJs(downloadUrl, zipPath);
49+
50+
if (!downloadResult) return 1;
51+
52+
// Extract the downloaded file
53+
ExtractNodeJs(zipPath);
54+
55+
// Completion message
56+
Console.WriteLine($"Done. To use, run `nodeswap use {version}`");
57+
return 0;
58+
}
59+
60+
private async Task<Version> GetVersion(string rawVersion)
61+
{
62+
try
5263
{
53-
try
54-
{
55-
version = VersionParser.Parse(rawVersion.ToString());
56-
}
57-
catch (ArgumentException)
58-
{
59-
await Console.Error.WriteLineAsync($"Invalid version argument: {rawVersion}");
60-
return 1;
61-
}
62-
}
64+
if (rawVersion.Equals("latest", StringComparison.CurrentCultureIgnoreCase))
65+
return await nodeWeb.GetLatestNodeVersion();
6366

64-
//
65-
// Is the requested version already installed?
66-
//
67+
if (rawVersion.Split(".").Length < 3)
68+
return await nodeWeb.GetLatestNodeVersion(rawVersion);
6769

68-
if (nodeLocal.GetInstalledVersions().FindIndex(v => v.Version.Equals(version)) != -1)
70+
return VersionParser.Parse(rawVersion);
71+
}
72+
catch (Exception ex)
6973
{
70-
await Console.Error.WriteLineAsync($"{version} already installed");
71-
return 1;
74+
await Console.Error.WriteLineAsync($"Error determining version: {ex.Message}");
75+
return null;
7276
}
77+
}
7378

74-
//
75-
// Download it
76-
//
79+
private bool IsVersionInstalled(Version version)
80+
{
81+
return nodeLocal.GetInstalledVersions().FindIndex(v => v.Version.Equals(version)) != -1;
82+
}
7783

78-
var downloadUrl = nodeWeb.GetDownloadUrl(version);
79-
var zipPath = Path.Join(globalContext.StoragePath, Path.GetFileName(downloadUrl));
84+
private async Task<bool> DownloadNodeJs(string downloadUrl, string zipPath)
85+
{
8086
var progressBar = new ProgressBar(100, "Download progress", new ProgressBarOptions
8187
{
8288
ProgressCharacter = '\u2593',
8389
ForegroundColor = ConsoleColor.Yellow,
8490
ForegroundColorDone = ConsoleColor.Green,
8591
});
8692

87-
var webClient = new WebClient();
88-
webClient.DownloadProgressChanged += (s, e) => { progressBar.Tick(e.ProgressPercentage); };
89-
webClient.DownloadFileCompleted += (s, e) => { progressBar.Dispose(); };
90-
9193
try
9294
{
93-
await webClient.DownloadFileTaskAsync(downloadUrl, zipPath).ConfigureAwait(false);
95+
var httpClient = new HttpClient();
96+
using var response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
97+
response.EnsureSuccessStatusCode();
98+
99+
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
100+
var canReportProgress = totalBytes != -1;
101+
102+
await using var fileStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None);
103+
await using var contentStream = await response.Content.ReadAsStreamAsync();
104+
105+
var buffer = new byte[8192];
106+
long totalRead = 0;
107+
int bytesRead;
108+
109+
while ((bytesRead = await contentStream.ReadAsync(buffer)) > 0)
110+
{
111+
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
112+
if (!canReportProgress) continue;
113+
totalRead += bytesRead;
114+
var progressPercentage = (int) (totalRead * 100 / totalBytes);
115+
progressBar.Tick(progressPercentage);
116+
}
117+
118+
progressBar.Dispose();
119+
return true;
94120
}
95121
catch (Exception e)
96122
{
97123
await Console.Error.WriteLineAsync("Unable to download the Node.js zip file.");
98-
if (e.InnerException == null) return 1;
124+
if (e.InnerException == null) return false;
99125
await Console.Error.WriteLineAsync(e.InnerException.Message);
100-
await Console.Error.WriteLineAsync("You may need to run this command from an " +
101-
"elevated prompt. (Run as Administrator)");
102-
return 1;
126+
await Console.Error.WriteLineAsync(
127+
"You may need to run this command from an elevated prompt. (Run as Administrator)");
128+
return false;
103129
}
130+
}
104131

132+
private void ExtractNodeJs(string zipPath)
133+
{
105134
Console.WriteLine("Extracting...");
106135
ConsoleSpinner.Instance.Update();
136+
107137
var timer = new Timer(250);
108-
timer.Elapsed += (s, e) => ConsoleSpinner.Instance.Update();
109-
timer.Enabled = true;
110-
ZipFile.ExtractToDirectory(zipPath, globalContext.StoragePath);
111-
timer.Enabled = false;
138+
timer.Elapsed += (_, _) => ConsoleSpinner.Instance.Update();
139+
timer.Start();
140+
141+
ZipFile.ExtractToDirectory(zipPath, globalContext.StoragePath, overwriteFiles: true);
142+
143+
timer.Stop();
112144
ConsoleSpinner.Reset();
113145
File.Delete(zipPath);
114-
115-
Console.WriteLine($"Done. To use, run `nodeswap use {version}`");
116-
return 0;
117146
}
118147
}

NodeSwap/Commands/ListCommand.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
using System;
2-
using System.CommandLine.Invocation;
3-
using System.Threading.Tasks;
2+
using DotMake.CommandLine;
43

54
namespace NodeSwap.Commands;
65

7-
public class ListCommand(NodeJs nodeJs) : ICommandHandler
6+
[CliCommand(
7+
Description = "List installed versions of Node.js.",
8+
Parent = typeof(RootCommand)
9+
)]
10+
public class ListCommand(NodeJs nodeJs)
811
{
9-
public Task<int> InvokeAsync(InvocationContext context)
12+
public void Run()
1013
{
1114
var versions = nodeJs.GetInstalledVersions();
1215
if (versions.Count == 0)
1316
{
1417
Console.WriteLine("None installed");
15-
return Task.Factory.StartNew(() => 0);
18+
return;
1619
}
1720

1821
Console.WriteLine();
@@ -22,7 +25,5 @@ public Task<int> InvokeAsync(InvocationContext context)
2225
Console.WriteLine($"{prefix}{v.Version}");
2326
});
2427
Console.WriteLine();
25-
26-
return Task.Factory.StartNew(() => 0);
2728
}
2829
}

NodeSwap/Commands/RootCommand.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using DotMake.CommandLine;
2+
3+
namespace NodeSwap.Commands;
4+
5+
[CliCommand]
6+
public class RootCommand;

NodeSwap/Commands/UninstallCommand.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
11
using System;
2-
using System.CommandLine.Invocation;
32
using System.IO;
4-
using System.Threading.Tasks;
3+
using DotMake.CommandLine;
54

65
namespace NodeSwap.Commands;
76

8-
public class UninstallCommand(NodeJs nodeLocal) : ICommandHandler
7+
[CliCommand(
8+
Description = "Uninstall a specific version of Node.js",
9+
Parent = typeof(RootCommand)
10+
)]
11+
public class UninstallCommand(NodeJs nodeLocal)
912
{
10-
public async Task<int> InvokeAsync(InvocationContext context)
13+
[CliArgument(Description = "e.g. `22.6.0`. Run `list` command to see installed versions.")]
14+
public string Version { get; set; }
15+
16+
public int Run()
1117
{
12-
var rawVersion = context.ParseResult.ValueForArgument("version");
13-
if (rawVersion == null)
18+
if (Version == null)
1419
{
15-
await Console.Error.WriteLineAsync($"Missing version argument");
20+
Console.Error.WriteLine("Missing version argument");
1621
return 1;
1722
}
1823

1924
Version version;
2025
try
2126
{
22-
version = VersionParser.StrictParse(rawVersion.ToString()!);
27+
version = VersionParser.StrictParse(Version);
2328
}
2429
catch (ArgumentException)
2530
{
26-
await Console.Error.WriteLineAsync($"Invalid version argument: {rawVersion}");
31+
Console.Error.WriteLine($"Invalid version argument: {Version}");
2732
return 1;
2833
}
2934

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

@@ -48,7 +53,7 @@ public async Task<int> InvokeAsync(InvocationContext context)
4853
}
4954
catch (IOException)
5055
{
51-
await Console.Error.WriteLineAsync($"Unable to delete {nodeVersion.Path}");
56+
Console.Error.WriteLine($"Unable to delete {nodeVersion.Path}");
5257
return 1;
5358
}
5459

0 commit comments

Comments
 (0)