Skip to content

Commit 268504a

Browse files
committed
Release v0.1.4
- Fix fcdnx argument pass-through by allowing unmatched tokens as tool arguments - Select tool TFM from the highest installed .NET runtime instead of hard-coded net10.0 - Validate cached versions against current VersionConstraint and prerelease settings - Reuse the supplied NuGet.Config during package download, not only metadata resolution - Add regression coverage for CLI args, runtime-aware extraction, and cache constraint fall-through - Bump ToolHost and ToolHost.Cli package metadata to 0.1.4 and update release notes
1 parent ca9cf8a commit 268504a

21 files changed

Lines changed: 545 additions & 133 deletions

RELEASENOTES.ToolHost.Cli.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release Notes — FieldCure.ToolHost.Cli (`fcdnx`)
22

3+
## v0.1.4 (2026-05-19)
4+
5+
### Fixed
6+
7+
- **Tool arguments are now accepted and forwarded.** `fcdnx dotnetsay hello` no longer fails argument parsing before ToolHost starts. Unmatched tokens after the package id are treated as tool arguments and passed through to the launched process.
8+
- **Library re-pinned to `FieldCure.ToolHost` 0.1.4.** This pulls in runtime-aware tool TFM selection, cache constraint validation, and `--configfile` reuse during package download.
9+
310
## v0.1.3 (2026-05-19)
411

512
### Fixed (via embedded library)

RELEASENOTES.ToolHost.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Release Notes — FieldCure.ToolHost
22

3+
## v0.1.4 (2026-05-19)
4+
5+
### Fixed
6+
7+
- **Runtime TFM selection now follows the installed host runtime.** `NuGetToolExtractor` no longer assumes `net10.0` when selecting `tools/{tfm}/{rid}`. It derives the host framework from the highest installed `Microsoft.NETCore.App` runtime reported by `DotnetEnvironment`, falling back to the current process runtime only if runtime detection data is unavailable.
8+
- **Cache hits honor the current version request.** `CachedOnly` and `CachedWithRefresh` no longer return a pinned prerelease when prerelease is disallowed, or a pinned version outside the requested `VersionConstraint`.
9+
- **Downloads reuse the supplied NuGet.Config.** When the CLI is invoked with `--configfile`, the extractor now loads the same config file for package download that the resolver used for metadata resolution.
10+
311
## v0.1.3 (2026-05-19)
412

513
### Fixed

src/FieldCure.ToolHost.Cli/DnxCommand.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public static RootCommand Build()
8181
osOpt,
8282
policyOpt,
8383
};
84+
root.TreatUnmatchedTokensAsErrors = false;
8485

8586
root.SetAction((parseResult, ct) => RunAsync(
8687
parseResult,
@@ -117,9 +118,9 @@ private static async Task<int> RunAsync(
117118
CancellationToken ct)
118119
{
119120
var verbosity = parseResult.GetValue(verbosityOpt) ?? VerbosityMapper.DefaultLevel;
120-
LogLevel minLevel = VerbosityMapper.Map(verbosity);
121+
var minLevel = VerbosityMapper.Map(verbosity);
121122

122-
using ILoggerFactory loggerFactory = LoggerFactory.Create(b =>
123+
using var loggerFactory = LoggerFactory.Create(b =>
123124
{
124125
_ = b.SetMinimumLevel(minLevel);
125126
_ = b.AddSimpleConsole(o =>
@@ -130,8 +131,8 @@ private static async Task<int> RunAsync(
130131
});
131132
});
132133

133-
ILogger<DnxLiteRunner> runnerLogger = loggerFactory.CreateLogger<DnxLiteRunner>();
134-
ILogger cliLogger = loggerFactory.CreateLogger("fcdnx");
134+
var runnerLogger = loggerFactory.CreateLogger<DnxLiteRunner>();
135+
var cliLogger = loggerFactory.CreateLogger("fcdnx");
135136

136137
try
137138
{
@@ -179,7 +180,10 @@ private static async Task<int> RunAsync(
179180
IgnoreFailedSources = parseResult.GetValue(ignoreFailedSourcesOpt),
180181
};
181182
NuGetPackageResolver resolver = new(resolverOptions, indexStore, loggerFactory.CreateLogger<NuGetPackageResolver>());
182-
NuGetToolExtractor extractor = new(environment, loggerFactory.CreateLogger<NuGetToolExtractor>());
183+
NuGetToolExtractor extractor = new(
184+
environment,
185+
parseResult.GetValue(configFileOpt),
186+
loggerFactory.CreateLogger<NuGetToolExtractor>());
183187
FieldCure.ToolHost.Execution.ToolLauncher launcher = new(loggerFactory.CreateLogger<FieldCure.ToolHost.Execution.ToolLauncher>());
184188

185189
DnxLiteRunner runner = new(environment, resolver, extractor, launcher, runnerLogger);
@@ -194,7 +198,7 @@ private static async Task<int> RunAsync(
194198
AllowRollForward = parseResult.GetValue(allowRollForwardOpt),
195199
};
196200

197-
using Process process = await runner.StartAsync(invocation, ct).ConfigureAwait(false);
201+
using var process = await runner.StartAsync(invocation, ct).ConfigureAwait(false);
198202
return await ForwardStdioAsync(process, ct).ConfigureAwait(false);
199203
}
200204
catch (PackageNotFoundException ex)

src/FieldCure.ToolHost.Cli/FieldCure.ToolHost.Cli.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
<LangVersion>latest</LangVersion>
9-
<Version>0.1.3</Version>
9+
<Version>0.1.4</Version>
1010
<AssemblyName>FieldCure.ToolHost.Cli</AssemblyName>
1111
<RootNamespace>FieldCure.ToolHost.Cli</RootNamespace>
1212
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -28,7 +28,7 @@
2828
<PackageTags>dnx;dotnet-tool;nuget;fcdnx;tool-runner;sdk-less;cli</PackageTags>
2929
<PackageReadmeFile>README.md</PackageReadmeFile>
3030
<PackageIcon>icon.png</PackageIcon>
31-
<PackageReleaseNotes>v0.1.3Library re-pinned to FieldCure.ToolHost 0.1.3 (hotfix: PackageSource ctor argument order; --source / --add-source were silently broken in 0.1.0–0.1.2). CLI surface unchanged. See RELEASENOTES.ToolHost.Cli.md on GitHub for the full notes.</PackageReleaseNotes>
31+
<PackageReleaseNotes>v0.1.4CLI now accepts and forwards tool arguments, and repins to FieldCure.ToolHost 0.1.4 for runtime TFM selection, cache constraint, and NuGet.Config download fixes. See RELEASENOTES.ToolHost.Cli.md on GitHub for the full notes.</PackageReleaseNotes>
3232
</PropertyGroup>
3333

3434
<ItemGroup>
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.CommandLine;
2-
3-
namespace FieldCure.ToolHost.Cli;
1+
namespace FieldCure.ToolHost.Cli;
42

53
/// <summary>Entry point for the <c>fcdnx</c> CLI.</summary>
64
public static class Program
@@ -9,8 +7,8 @@ public static class Program
97
/// <param name="args">Raw command-line arguments.</param>
108
public static Task<int> Main(string[] args)
119
{
12-
RootCommand root = DnxCommand.Build();
13-
ParseResult parseResult = root.Parse(args);
10+
var root = DnxCommand.Build();
11+
var parseResult = root.Parse(args);
1412
return parseResult.InvokeAsync();
1513
}
1614
}

src/FieldCure.ToolHost/Authentication/CredentialProviderSetup.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.Extensions.Logging;
1+
using Microsoft.Extensions.Logging;
22

33
using NuGet.Credentials;
44

@@ -35,7 +35,7 @@ public static void Register(bool interactive, ILogger? logger = null)
3535
return;
3636
}
3737

38-
INuGetLogger nugetLogger = logger is null
38+
var nugetLogger = logger is null
3939
? NuGet.Common.NullLogger.Instance
4040
: new ForwardingLogger(logger);
4141

@@ -54,7 +54,7 @@ private sealed class ForwardingLogger : NuGetLoggerBase
5454
/// <summary>Translates a NuGet log level and forwards the message synchronously.</summary>
5555
public override void Log(NuGetLogMessage message)
5656
{
57-
LogLevel level = message.Level switch
57+
var level = message.Level switch
5858
{
5959
NuGetLogLevel.Debug => LogLevel.Debug,
6060
NuGetLogLevel.Verbose => LogLevel.Debug,

src/FieldCure.ToolHost/DnxLiteRunner.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Diagnostics;
1+
using System.Diagnostics;
22

33
using FieldCure.ToolHost.Execution;
44
using FieldCure.ToolHost.Extraction;
@@ -86,13 +86,13 @@ public async Task<Process> StartAsync(ToolInvocationRequest request, Cancellatio
8686
AllowPrerelease: request.AllowPrerelease,
8787
ExplicitVersion: request.ExplicitVersion);
8888

89-
PackageResolution resolution = await _resolver.ResolveAsync(resolutionRequest, ct).ConfigureAwait(false);
89+
var resolution = await _resolver.ResolveAsync(resolutionRequest, ct).ConfigureAwait(false);
9090

9191
_logger.LogInformation(
9292
"Resolved {PackageId} -> {Version} (source={Source}, cacheHit={CacheHit})",
9393
resolution.PackageId, resolution.Version.ToNormalizedString(), resolution.SourceUrl, resolution.WasCacheHit);
9494

95-
ExtractedToolLayout layout = await _extractor.EnsureExtractedAsync(resolution, ct).ConfigureAwait(false);
95+
var layout = await _extractor.EnsureExtractedAsync(resolution, ct).ConfigureAwait(false);
9696

9797
LaunchRequest launchRequest = new()
9898
{

src/FieldCure.ToolHost/DotnetEnvironment.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ public static async Task<DotnetEnvironment> DetectAsync(CancellationToken ct = d
4949
"The 'dotnet' muxer was not found on PATH and DOTNET_ROOT is not set. " +
5050
"Install the .NET runtime or set DOTNET_ROOT to its installation folder.");
5151

52-
IReadOnlyList<string> sdks = ParseSdkLines(await RunDotnetAsync(muxer, "--list-sdks", ct).ConfigureAwait(false));
53-
IReadOnlyList<string> runtimes = ParseRuntimeLines(await RunDotnetAsync(muxer, "--list-runtimes", ct).ConfigureAwait(false));
52+
var sdks = ParseSdkLines(await RunDotnetAsync(muxer, "--list-sdks", ct).ConfigureAwait(false));
53+
var runtimes = ParseRuntimeLines(await RunDotnetAsync(muxer, "--list-runtimes", ct).ConfigureAwait(false));
5454

55-
ISettings settings = Settings.LoadDefaultSettings(root: null);
55+
var settings = Settings.LoadDefaultSettings(root: null);
5656
var globalPackages = SettingsUtility.GetGlobalPackagesFolder(settings);
5757

5858
return new DotnetEnvironment
@@ -99,7 +99,7 @@ public static async Task<DotnetEnvironment> DetectAsync(CancellationToken ct = d
9999
internal static IReadOnlyList<string> ParseSdkLines(string output)
100100
{
101101
// Each line: "<version> [<install-path>]"
102-
List<string> versions = new();
102+
List<string> versions = [];
103103
foreach (var line in output.Split('\n'))
104104
{
105105
var trimmed = line.Trim();
@@ -121,7 +121,7 @@ internal static IReadOnlyList<string> ParseRuntimeLines(string output)
121121
{
122122
// Each line: "<framework> <version> [<install-path>]"
123123
// We only retain Microsoft.NETCore.App runtimes.
124-
List<string> versions = new();
124+
List<string> versions = [];
125125
foreach (var line in output.Split('\n'))
126126
{
127127
var trimmed = line.Trim();
@@ -157,11 +157,11 @@ private static async Task<string> RunDotnetAsync(string muxer, string arg, Cance
157157
};
158158
psi.ArgumentList.Add(arg);
159159

160-
using Process process = Process.Start(psi)
160+
using var process = Process.Start(psi)
161161
?? throw new InvalidOperationException($"Failed to start '{muxer} {arg}'.");
162162

163-
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync(ct);
164-
Task<string> stderrTask = process.StandardError.ReadToEndAsync(ct);
163+
var stdoutTask = process.StandardOutput.ReadToEndAsync(ct);
164+
var stderrTask = process.StandardError.ReadToEndAsync(ct);
165165
await process.WaitForExitAsync(ct).ConfigureAwait(false);
166166
var stdout = await stdoutTask.ConfigureAwait(false);
167167
var stderr = await stderrTask.ConfigureAwait(false);

src/FieldCure.ToolHost/Execution/DotnetToolSettings.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static DotnetToolSettings Parse(string xml)
4343
throw new InvalidDataException("DotnetToolSettings.xml is not well-formed XML.", ex);
4444
}
4545

46-
XElement root = doc.Root
46+
var root = doc.Root
4747
?? throw new InvalidDataException("DotnetToolSettings.xml has no root element.");
4848

4949
if (!string.Equals(root.Name.LocalName, "DotNetCliTool", StringComparison.Ordinal))
@@ -52,16 +52,16 @@ public static DotnetToolSettings Parse(string xml)
5252
$"DotnetToolSettings.xml root must be <DotNetCliTool>, found <{root.Name.LocalName}>.");
5353
}
5454

55-
XElement? commandsElement = root.Elements()
55+
var commandsElement = root.Elements()
5656
.FirstOrDefault(e => string.Equals(e.Name.LocalName, "Commands", StringComparison.Ordinal));
5757

5858
if (commandsElement is null)
5959
{
6060
throw new InvalidDataException("DotnetToolSettings.xml is missing the <Commands> element.");
6161
}
6262

63-
List<DotnetToolCommand> commands = new();
64-
foreach (XElement commandElement in commandsElement.Elements()
63+
List<DotnetToolCommand> commands = [];
64+
foreach (var commandElement in commandsElement.Elements()
6565
.Where(e => string.Equals(e.Name.LocalName, "Command", StringComparison.Ordinal)))
6666
{
6767
var name = RequireAttribute(commandElement, "Name");

src/FieldCure.ToolHost/Execution/ToolLauncher.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public Process Start(LaunchRequest request)
3333
$"Package has no commands declared in DotnetToolSettings.xml at '{request.Layout.ToolsFolder}'.");
3434
}
3535

36-
DotnetToolCommand command = request.Layout.ToolSettings.Commands[0];
36+
var command = request.Layout.ToolSettings.Commands[0];
3737
var entryPoint = Path.Combine(request.Layout.ToolsFolder, command.EntryPoint);
3838

3939
if (!File.Exists(entryPoint))
@@ -75,7 +75,7 @@ public Process Start(LaunchRequest request)
7575

7676
if (request.AdditionalEnvironment is { Count: > 0 } extras)
7777
{
78-
foreach (KeyValuePair<string, string?> kv in extras)
78+
foreach (var kv in extras)
7979
{
8080
if (kv.Value is null)
8181
{
@@ -92,7 +92,7 @@ public Process Start(LaunchRequest request)
9292

9393
try
9494
{
95-
Process? process = Process.Start(psi);
95+
var process = Process.Start(psi);
9696
if (process is null)
9797
{
9898
throw new ToolLaunchFailedException(

0 commit comments

Comments
 (0)