Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2047,6 +2047,33 @@ Your project targets multiple frameworks. Specify which framework to run using '
<data name="ToolDefinition" xml:space="preserve">
<value>Install or manage tools that extend the .NET experience.</value>
</data>
<data name="ToolDscCommandDescription" xml:space="preserve">
<value>Manage tools using Microsoft Desired State Configuration (DSC).</value>
</data>
<data name="ToolDscGetCommandDescription" xml:space="preserve">
<value>Get the current state of installed tools.</value>
</data>
<data name="ToolDscSetCommandDescription" xml:space="preserve">
<value>Set the desired state of tools by installing or updating them.</value>
</data>
<data name="ToolDscTestCommandDescription" xml:space="preserve">
<value>Test if the current state matches the desired state.</value>
</data>
<data name="ToolDscExportCommandDescription" xml:space="preserve">
<value>Export the current state of all installed tools.</value>
</data>
<data name="ToolDscSchemaCommandDescription" xml:space="preserve">
<value>Get the JSON schema for DSC tool state.</value>
</data>
<data name="ToolDscManifestCommandDescription" xml:space="preserve">
<value>Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool.</value>
</data>
<data name="ToolDscInputOptionDescription" xml:space="preserve">
<value>JSON input representing the desired or requested tool state.</value>
</data>
<data name="ToolDscInputOptionName" xml:space="preserve">
<value>JSON</value>
</data>
<data name="ToolInstallAddSourceOptionDescription" xml:space="preserve">
<value>Add an additional NuGet package source to use during installation.</value>
</data>
Expand Down
97 changes: 97 additions & 0 deletions src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json.Serialization;
using NuGet.Versioning;

namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc;

internal record DscToolsState
{
[JsonPropertyName("tools")]
public List<DscToolState> Tools { get; set; } = new List<DscToolState>();
}

internal record DscToolState
{
[JsonPropertyName("packageId")]
public string? PackageId { get; set; }

[JsonPropertyName("version")]
public string? Version { get; set; }

[JsonPropertyName("commands")]
public List<string>? Commands { get; set; }

[JsonPropertyName("scope")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public DscToolScope? Scope { get; set; }

[JsonPropertyName("toolPath")]
public string? ToolPath { get; set; }

[JsonPropertyName("manifestPath")]
public string? ManifestPath { get; set; }

[JsonPropertyName("_exist")]
public bool? Exist { get; set; }

/// <summary>
/// Parses packageId and version from the PackageId property.
/// Supports format: "packageId" or "packageId@version"
/// </summary>
public (string PackageId, VersionRange? VersionRange) ParsePackageIdentity()
{
if (string.IsNullOrEmpty(PackageId))
{
return (string.Empty, null);
}

string[] parts = PackageId.Split('@');
string packageId = parts[0];

if (parts.Length > 1 && !string.IsNullOrEmpty(parts[1]))
{
// packageId@version format
if (VersionRange.TryParse(parts[1], out var versionRange))
{
return (packageId, versionRange);
}
}
else if (!string.IsNullOrEmpty(Version))
{
// Use separate Version property if available
if (VersionRange.TryParse(Version, out var versionRange))
{
return (packageId, versionRange);
}
}

return (packageId, null);
}
}

internal enum DscToolScope
{
Global,
Local,
ToolPath
}

internal record DscErrorMessage
{
[JsonPropertyName("error")]
public string Error { get; set; } = string.Empty;
}

internal record DscDebugMessage
{
[JsonPropertyName("debug")]
public string Debug { get; set; } = string.Empty;
}

internal record DscTraceMessage
{
[JsonPropertyName("trace")]
public string Trace { get; set; } = string.Empty;
}
219 changes: 219 additions & 0 deletions src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.EnvironmentAbstractions;

namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc;

internal static class DscWriter
{
/// <summary>
/// Writes an error message to stderr in DSC JSON format.
/// </summary>
public static void WriteError(string message)
{
var errorMessage = new DscErrorMessage { Error = message };
string json = JsonSerializer.Serialize(errorMessage);
Reporter.Error.WriteLine(json);
}

/// <summary>
/// Writes a debug message to stderr in DSC JSON format.
/// </summary>
public static void WriteDebug(string message)
{
var debugMessage = new DscDebugMessage { Debug = message };
string json = JsonSerializer.Serialize(debugMessage);
Reporter.Error.WriteLine(json);
}

/// <summary>
/// Writes a trace message to stderr in DSC JSON format.
/// </summary>
public static void WriteTrace(string message)
{
var traceMessage = new DscTraceMessage { Trace = message };
string json = JsonSerializer.Serialize(traceMessage);
Reporter.Error.WriteLine(json);
}

/// <summary>
/// Writes the result state to stdout in DSC JSON format.
/// </summary>
public static void WriteResult(DscToolsState state)
{
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};

string json = JsonSerializer.Serialize(state, options);
Reporter.Output.WriteLine(json);
}

/// <summary>
/// Writes any object to stdout in JSON format.
/// </summary>
public static void WriteJson(object obj, bool writeIndented = false)
{
var options = new JsonSerializerOptions
{
WriteIndented = writeIndented
};

string json = JsonSerializer.Serialize(obj, options);
Reporter.Output.WriteLine(json);
}

/// <summary>
/// Reads input from either stdin (if input is "-") or from a file.
/// </summary>
public static string ReadInput(string input)
{
if (input == "-")
{
return Console.In.ReadToEnd();
}
else
{
return File.ReadAllText(input);
}
}

/// <summary>
/// Reads and deserializes DSC tool state from input (file or stdin).
/// Returns null if input is not provided.
/// Exits with code 1 if deserialization fails.
/// </summary>
public static DscToolsState? ReadAndDeserializeInput(string? input)
{
if (string.IsNullOrEmpty(input))
{
return null;
}

try
{
string jsonInput = ReadInput(input);
var inputState = JsonSerializer.Deserialize<DscToolsState>(jsonInput);
return inputState;
}
catch (JsonException ex)
{
WriteError($"Failed to deserialize JSON: {ex.Message}");
Environment.Exit(1);
return null; // Unreachable, but satisfies compiler
}
}

/// <summary>
/// Queries the actual state of a tool from the package store.
/// </summary>
public static DscToolState QueryToolState(DscToolState requestedState)
{
// Parse packageId to handle packageId@version syntax
var (packageIdString, _) = requestedState.ParsePackageIdentity();

if (string.IsNullOrEmpty(packageIdString))
{
packageIdString = requestedState.PackageId ?? string.Empty;
}

// Determine the scope and tool path based on the requested tool
DirectoryPath? toolPath = null;
DscToolScope scope = requestedState.Scope ?? DscToolScope.Global;

if (scope == DscToolScope.ToolPath && !string.IsNullOrWhiteSpace(requestedState.ToolPath))
{
toolPath = new DirectoryPath(requestedState.ToolPath);
}
else if (scope == DscToolScope.Global)
{
// Global tools, use default location (null)
toolPath = null;
}
else if (scope == DscToolScope.Local)
{
// TODO: Local tools require querying dotnet-tools.json in current directory
// For now, return not found for local tools
WriteDebug($"Local tool scope not yet implemented for {packageIdString}");
return new DscToolState
{
PackageId = packageIdString,
Version = null,
Commands = null,
Scope = DscToolScope.Local,
ToolPath = null,
ManifestPath = null,
Exist = false
};
}

// Query the tool package store
var packageStoreQuery = ToolPackageFactory.CreateToolPackageStoreQuery(toolPath);
var packageId = new PackageId(packageIdString);

try
{
// Find the tool package
var installedPackages = packageStoreQuery.EnumeratePackages()
.Where(p => p.Id.Equals(packageId))
.ToList();

if (installedPackages.Any())
{
// Tool exists, get its details from the first (or only) matching package
var package = installedPackages.First();

WriteDebug($"Found tool {package.Id} version {package.Version.ToNormalizedString()}");

return new DscToolState
{
PackageId = package.Id.ToString(),
Version = package.Version.ToNormalizedString(),
Commands = package.Command != null ? new List<string> { package.Command.Name.Value } : null,
Scope = scope,
ToolPath = scope == DscToolScope.ToolPath ? requestedState.ToolPath : null,
ManifestPath = null,
Exist = true
};
}
else
{
// Tool not found
WriteDebug($"Tool {packageIdString} not found in {scope} scope");

return new DscToolState
{
PackageId = packageIdString,
Version = null,
Commands = null,
Scope = scope,
ToolPath = scope == DscToolScope.ToolPath ? requestedState.ToolPath : null,
ManifestPath = null,
Exist = false
};
}
}
catch (Exception ex)
{
// If there's an error querying the tool, return it as not found
WriteError($"Error querying tool {packageIdString}: {ex.Message}");

return new DscToolState
{
PackageId = packageIdString,
Version = null,
Commands = null,
Scope = scope,
ToolPath = scope == DscToolScope.ToolPath ? requestedState.ToolPath : null,
ManifestPath = null,
Exist = false
};
}
}
}
35 changes: 35 additions & 0 deletions src/Cli/dotnet/Commands/Tool/Dsc/ToolDscCommandParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using Microsoft.DotNet.Cli.Extensions;

namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc;

internal static class ToolDscCommandParser
{
public static readonly string DocsLink = "https://aka.ms/dotnet-tool-dsc";

private static readonly Command Command = ConstructCommand();

public static Command GetCommand()
{
return Command;
}

private static Command ConstructCommand()
{
DocumentedCommand command = new("dsc", DocsLink, CliCommandStrings.ToolDscCommandDescription);

command.Subcommands.Add(ToolDscGetCommandParser.GetCommand());
command.Subcommands.Add(ToolDscSetCommandParser.GetCommand());
command.Subcommands.Add(ToolDscTestCommandParser.GetCommand());
command.Subcommands.Add(ToolDscExportCommandParser.GetCommand());
command.Subcommands.Add(ToolDscSchemaCommandParser.GetCommand());
command.Subcommands.Add(ToolDscManifestCommandParser.GetCommand());

command.SetAction((parseResult) => parseResult.HandleMissingCommand());

return command;
}
}
Loading
Loading