From 236bfc3510d07bc384867f849e2bbf7f74d41acf Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 1 Nov 2025 10:33:00 +0100 Subject: [PATCH 1/3] Initial setup for Microsoft Desired State Configuration (DSC) --- .../dotnet/Commands/CliCommandStrings.resx | 27 +++ src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs | 62 +++++++ src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs | 84 +++++++++ .../Commands/Tool/Dsc/ToolDscCommandParser.cs | 35 ++++ .../Commands/Tool/Dsc/ToolDscExportCommand.cs | 95 ++++++++++ .../Tool/Dsc/ToolDscExportCommandParser.cs | 25 +++ .../Commands/Tool/Dsc/ToolDscGetCommand.cs | 169 ++++++++++++++++++ .../Tool/Dsc/ToolDscGetCommandParser.cs | 33 ++++ .../Tool/Dsc/ToolDscManifestCommand.cs | 103 +++++++++++ .../Tool/Dsc/ToolDscManifestCommandParser.cs | 25 +++ .../Commands/Tool/Dsc/ToolDscSchemaCommand.cs | 62 +++++++ .../Tool/Dsc/ToolDscSchemaCommandParser.cs | 25 +++ .../Commands/Tool/Dsc/ToolDscSetCommand.cs | 79 ++++++++ .../Tool/Dsc/ToolDscSetCommandParser.cs | 33 ++++ .../Commands/Tool/Dsc/ToolDscTestCommand.cs | 90 ++++++++++ .../Tool/Dsc/ToolDscTestCommandParser.cs | 33 ++++ .../dotnet/Commands/Tool/ToolCommandParser.cs | 2 + .../Commands/xlf/CliCommandStrings.cs.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.de.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.es.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.fr.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.it.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.ja.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.ko.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.pl.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.pt-BR.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.ru.xlf | 45 +++++ .../Commands/xlf/CliCommandStrings.tr.xlf | 45 +++++ .../xlf/CliCommandStrings.zh-Hans.xlf | 45 +++++ .../xlf/CliCommandStrings.zh-Hant.xlf | 45 +++++ src/Cli/dotnet/dotnet.csproj | 13 ++ 31 files changed, 1580 insertions(+) create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscCommandParser.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscExportCommand.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscExportCommandParser.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommand.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommandParser.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscManifestCommand.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscManifestCommandParser.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommand.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommandParser.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommand.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommandParser.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommand.cs create mode 100644 src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommandParser.cs diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx index 6ca03e2f5fe6..95113e71d016 100644 --- a/src/Cli/dotnet/Commands/CliCommandStrings.resx +++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx @@ -2047,6 +2047,33 @@ Your project targets multiple frameworks. Specify which framework to run using ' Install or manage tools that extend the .NET experience. + + Manage tools using Microsoft Desired State Configuration (DSC). + + + Get the current state of installed tools. + + + Set the desired state of tools by installing or updating them. + + + Test if the current state matches the desired state. + + + Export the current state of all installed tools. + + + Get the JSON schema for DSC tool state. + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + JSON input representing the desired or requested tool state. + + + JSON + Add an additional NuGet package source to use during installation. diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs b/src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs new file mode 100644 index 000000000000..2a1d94ef11b0 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs @@ -0,0 +1,62 @@ +// 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; + +namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; + +internal record DscToolsState +{ + [JsonPropertyName("tools")] + public List Tools { get; set; } = new List(); +} + +internal record DscToolState +{ + [JsonPropertyName("packageId")] + public string? PackageId { get; set; } + + [JsonPropertyName("version")] + public string? Version { get; set; } + + [JsonPropertyName("commands")] + public List? 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; } +} + +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; +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs b/src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs new file mode 100644 index 000000000000..009b13cdbae5 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs @@ -0,0 +1,84 @@ +// 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.Utils; + +namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; + +internal static class DscWriter +{ + /// + /// Writes an error message to stderr in DSC JSON format. + /// + public static void WriteError(string message) + { + var errorMessage = new DscErrorMessage { Error = message }; + string json = JsonSerializer.Serialize(errorMessage); + Reporter.Error.WriteLine(json); + } + + /// + /// Writes a debug message to stderr in DSC JSON format. + /// + public static void WriteDebug(string message) + { + var debugMessage = new DscDebugMessage { Debug = message }; + string json = JsonSerializer.Serialize(debugMessage); + Reporter.Error.WriteLine(json); + } + + /// + /// Writes a trace message to stderr in DSC JSON format. + /// + public static void WriteTrace(string message) + { + var traceMessage = new DscTraceMessage { Trace = message }; + string json = JsonSerializer.Serialize(traceMessage); + Reporter.Error.WriteLine(json); + } + + /// + /// Writes the result state to stdout in DSC JSON format. + /// + 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); + } + + /// + /// Writes any object to stdout in JSON format. + /// + 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); + } + + /// + /// Reads input from either stdin (if input is "-") or from a file. + /// + public static string ReadInput(string input) + { + if (input == "-") + { + return Console.In.ReadToEnd(); + } + else + { + return File.ReadAllText(input); + } + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscCommandParser.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscCommandParser.cs new file mode 100644 index 000000000000..8cc5054004ad --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscCommandParser.cs @@ -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; + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscExportCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscExportCommand.cs new file mode 100644 index 000000000000..577b2bd13f20 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscExportCommand.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.CommandLine; +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 class ToolDscExportCommand : CommandBase +{ + public ToolDscExportCommand(ParseResult parseResult) + : base(parseResult) + { + } + + public override int Execute() + { + try + { + var state = new DscToolsState(); + + // Enumerate all global tools + DscWriter.WriteTrace("Enumerating global tools"); + var globalTools = EnumerateGlobalTools(); + foreach (var tool in globalTools) + { + state.Tools.Add(tool); + } + + DscWriter.WriteTrace($"Found {state.Tools.Count} global tools"); + + // TODO: Add support for local tools (requires scanning for dotnet-tools.json files) + // TODO: Add support for tool-path tools (requires configuration of tool paths to scan) + + DscWriter.WriteResult(state); + + return 0; + } + catch (Exception ex) + { + DscWriter.WriteError($"Unexpected error: {ex.Message}"); + return 1; + } + } + + private List EnumerateGlobalTools() + { + var tools = new List(); + + // Query the global tool package store (null = default global location) + var packageStoreQuery = ToolPackageFactory.CreateToolPackageStoreQuery(null); + + try + { + var packages = packageStoreQuery.EnumeratePackages(); + + foreach (var package in packages) + { + try + { + // Only include packages that have commands + if (package.Command != null) + { + tools.Add(new DscToolState + { + PackageId = package.Id.ToString(), + Version = package.Version.ToNormalizedString(), + Commands = new List { package.Command.Name.Value }, + Scope = DscToolScope.Global, + ToolPath = null, + ManifestPath = null, + Exist = true + }); + } + } + catch (Exception ex) + { + // If we can't read a specific package, log a warning and continue + DscWriter.WriteError($"Warning: Could not read tool {package.Id}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + DscWriter.WriteError($"Error enumerating global tools: {ex.Message}"); + } + + return tools; + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscExportCommandParser.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscExportCommandParser.cs new file mode 100644 index 000000000000..03a3b0d27016 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscExportCommandParser.cs @@ -0,0 +1,25 @@ +// 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; + +namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; + +internal static class ToolDscExportCommandParser +{ + private static readonly Command Command = ConstructCommand(); + + public static Command GetCommand() + { + return Command; + } + + private static Command ConstructCommand() + { + Command command = new("export", CliCommandStrings.ToolDscExportCommandDescription); + + command.SetAction((parseResult) => new ToolDscExportCommand(parseResult).Execute()); + + return command; + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommand.cs new file mode 100644 index 000000000000..b270bbba0008 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommand.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.CommandLine; +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 class ToolDscGetCommand : CommandBase +{ + private readonly string _input; + + public ToolDscGetCommand(ParseResult parseResult) + : base(parseResult) + { + _input = parseResult.GetValue(ToolDscGetCommandParser.InputOption); + } + + public override int Execute() + { + try + { + DscToolsState inputState = null; + + if (!string.IsNullOrEmpty(_input)) + { + try + { + string jsonInput = DscWriter.ReadInput(_input); + inputState = JsonSerializer.Deserialize(jsonInput); + DscWriter.WriteTrace($"Input JSON deserialized: {inputState?.Tools?.Count ?? 0} tools"); + } + catch (JsonException ex) + { + DscWriter.WriteError($"Failed to deserialize JSON: {ex.Message}"); + return 1; + } + } + + var resultState = new DscToolsState(); + + foreach (var tool in inputState?.Tools ?? Enumerable.Empty()) + { + if (string.IsNullOrEmpty(tool.PackageId)) + { + DscWriter.WriteError("Property 'packageId' is required for 'get' operation."); + return 1; + } + + var actualState = GetToolState(tool); + if (actualState != null) + { + resultState.Tools.Add(actualState); + } + } + + DscWriter.WriteResult(resultState); + + return 0; + } + catch (Exception ex) + { + DscWriter.WriteError($"Unexpected error: {ex.Message}"); + return 1; + } + } + + private DscToolState GetToolState(DscToolState requestedState) + { + // 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 + DscWriter.WriteTrace($"Local tool scope not yet implemented for {requestedState.PackageId}"); + return new DscToolState + { + PackageId = requestedState.PackageId, + 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(requestedState.PackageId); + + 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(); + + DscWriter.WriteTrace($"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 { package.Command.Name.Value } : null, + Scope = scope, + ToolPath = scope == DscToolScope.ToolPath ? requestedState.ToolPath : null, + ManifestPath = null, + Exist = true + }; + } + else + { + // Tool not found + DscWriter.WriteTrace($"Tool {requestedState.PackageId} not found in {scope} scope"); + + return new DscToolState + { + PackageId = requestedState.PackageId, + 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 + DscWriter.WriteError($"Error querying tool {requestedState.PackageId}: {ex.Message}"); + + return new DscToolState + { + PackageId = requestedState.PackageId, + Version = null, + Commands = null, + Scope = scope, + ToolPath = scope == DscToolScope.ToolPath ? requestedState.ToolPath : null, + ManifestPath = null, + Exist = false + }; + } + } + +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommandParser.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommandParser.cs new file mode 100644 index 000000000000..88dd47a3e7fe --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommandParser.cs @@ -0,0 +1,33 @@ +// 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 ToolDscGetCommandParser +{ + public static readonly Option InputOption = new("--input", "-i") + { + Description = CliCommandStrings.ToolDscInputOptionDescription, + HelpName = CliCommandStrings.ToolDscInputOptionName + }; + + private static readonly Command Command = ConstructCommand(); + + public static Command GetCommand() + { + return Command; + } + + private static Command ConstructCommand() + { + Command command = new("get", CliCommandStrings.ToolDscGetCommandDescription); + command.Options.Add(InputOption); + + command.SetAction((parseResult) => new ToolDscGetCommand(parseResult).Execute()); + + return command; + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscManifestCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscManifestCommand.cs new file mode 100644 index 000000000000..5df59cbfd0ab --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscManifestCommand.cs @@ -0,0 +1,103 @@ +// 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 System.Text.Json; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; + +internal class ToolDscManifestCommand : CommandBase +{ + public ToolDscManifestCommand(ParseResult parseResult) + : base(parseResult) + { + } + + public override int Execute() + { + try + { + var manifest = new Dictionary + { + ["$schema"] = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + ["type"] = "Microsoft.DotNet.Sdk/Tool", + ["description"] = "Manage .NET tools using Microsoft Desired State Configuration (DSC).", + ["version"] = "1.0.0", + ["tags"] = new[] { "dotnet", "tool", "sdk" }, + ["exitCodes"] = new Dictionary + { + ["0"] = "Success", + ["1"] = "Error" + }, + ["schema"] = new Dictionary + { + ["command"] = new Dictionary + { + ["executable"] = "dotnet", + ["args"] = new object[] { "tool", "dsc", "schema" } + } + }, + ["get"] = new Dictionary + { + ["executable"] = "dotnet", + ["args"] = new object[] + { + "tool", + "dsc", + "get", + new Dictionary + { + ["jsonInputArg"] = "--input", + ["mandatory"] = true + } + } + }, + ["set"] = new Dictionary + { + ["executable"] = "dotnet", + ["args"] = new object[] + { + "tool", + "dsc", + "set", + new Dictionary + { + ["jsonInputArg"] = "--input", + ["mandatory"] = true + } + } + }, + ["test"] = new Dictionary + { + ["executable"] = "dotnet", + ["args"] = new object[] + { + "tool", + "dsc", + "test", + new Dictionary + { + ["jsonInputArg"] = "--input", + ["mandatory"] = true + } + } + }, + ["export"] = new Dictionary + { + ["executable"] = "dotnet", + ["args"] = new object[] { "tool", "dsc", "export" } + } + }; + + DscWriter.WriteJson(manifest); + + return 0; + } + catch (Exception ex) + { + DscWriter.WriteError($"Error generating manifest: {ex.Message}"); + return 1; + } + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscManifestCommandParser.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscManifestCommandParser.cs new file mode 100644 index 000000000000..f597dfe9a3f3 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscManifestCommandParser.cs @@ -0,0 +1,25 @@ +// 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; + +namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; + +internal static class ToolDscManifestCommandParser +{ + private static readonly Command Command = ConstructCommand(); + + public static Command GetCommand() + { + return Command; + } + + private static Command ConstructCommand() + { + Command command = new("manifest", CliCommandStrings.ToolDscManifestCommandDescription); + + command.SetAction((parseResult) => new ToolDscManifestCommand(parseResult).Execute()); + + return command; + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommand.cs new file mode 100644 index 000000000000..b2eee3f4805a --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommand.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.CommandLine; +using System.Text.Json; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; + +internal class ToolDscSchemaCommand : CommandBase +{ + public ToolDscSchemaCommand(ParseResult parseResult) + : base(parseResult) + { + } + + public override int Execute() + { + try + { + // For now, return a basic schema + // TODO: Generate proper JSON schema when System.Text.Json.Schema is available + var schema = new + { + type = "object", + properties = new + { + tools = new + { + type = "array", + items = new + { + type = "object", + properties = new + { + packageId = new { type = "string" }, + version = new { type = "string" }, + commands = new { type = "array", items = new { type = "string" } }, + scope = new { type = "string", @enum = new[] { "Global", "Local", "ToolPath" } }, + toolPath = new { type = "string" }, + manifestPath = new { type = "string" }, + _exist = new { type = "boolean" } + }, + required = new[] { "packageId" } + } + } + } + }; + + DscWriter.WriteJson(schema, writeIndented: true); + + return 0; + } + catch (Exception ex) + { + DscWriter.WriteError($"Unexpected error: {ex.Message}"); + return 1; + } + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommandParser.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommandParser.cs new file mode 100644 index 000000000000..3743cb502f23 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommandParser.cs @@ -0,0 +1,25 @@ +// 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; + +namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; + +internal static class ToolDscSchemaCommandParser +{ + private static readonly Command Command = ConstructCommand(); + + public static Command GetCommand() + { + return Command; + } + + private static Command ConstructCommand() + { + Command command = new("schema", CliCommandStrings.ToolDscSchemaCommandDescription); + + command.SetAction((parseResult) => new ToolDscSchemaCommand(parseResult).Execute()); + + return command; + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommand.cs new file mode 100644 index 000000000000..81c37fc1ece6 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommand.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.CommandLine; +using System.Text.Json; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; + +internal class ToolDscSetCommand : CommandBase +{ + private readonly string _input; + + public ToolDscSetCommand(ParseResult parseResult) + : base(parseResult) + { + _input = parseResult.GetValue(ToolDscSetCommandParser.InputOption); + } + + public override int Execute() + { + try + { + DscToolsState inputState = null; + + if (!string.IsNullOrEmpty(_input)) + { + try + { + string jsonInput = DscWriter.ReadInput(_input); + inputState = JsonSerializer.Deserialize(jsonInput); + DscWriter.WriteTrace($"Input JSON deserialized: {inputState?.Tools?.Count ?? 0} tools"); + } + catch (JsonException ex) + { + DscWriter.WriteError($"Failed to deserialize JSON: {ex.Message}"); + return 1; + } + } + + foreach (var tool in inputState?.Tools ?? Enumerable.Empty()) + { + if (string.IsNullOrEmpty(tool.PackageId)) + { + DscWriter.WriteError("Property 'packageId' is required for 'set' operation."); + return 1; + } + + if (tool.Exist == false) + { + DscWriter.WriteError($"Removing tools is not supported. Use 'dotnet tool uninstall' instead."); + return 1; + } + + DscWriter.WriteDebug($"Setting desired state for tool: {tool.PackageId}"); + + // TODO: Implement actual tool installation/update logic + // This would involve: + // - Checking if tool is already installed + // - Installing tool if not present + // - Updating tool if version doesn't match + // - Handling different scopes (global, local, toolPath) + } + + var resultState = new DscToolsState(); + DscWriter.WriteResult(resultState); + + return 0; + } + catch (Exception ex) + { + DscWriter.WriteError($"Unexpected error: {ex.Message}"); + return 1; + } + } + +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommandParser.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommandParser.cs new file mode 100644 index 000000000000..6ae4331d9444 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommandParser.cs @@ -0,0 +1,33 @@ +// 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 ToolDscSetCommandParser +{ + public static readonly Option InputOption = new("--input", "-i") + { + Description = CliCommandStrings.ToolDscInputOptionDescription, + HelpName = CliCommandStrings.ToolDscInputOptionName + }; + + private static readonly Command Command = ConstructCommand(); + + public static Command GetCommand() + { + return Command; + } + + private static Command ConstructCommand() + { + Command command = new("set", CliCommandStrings.ToolDscSetCommandDescription); + command.Options.Add(InputOption); + + command.SetAction((parseResult) => new ToolDscSetCommand(parseResult).Execute()); + + return command; + } +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommand.cs new file mode 100644 index 000000000000..e445457a9ec8 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommand.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.CommandLine; +using System.Text.Json; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; + +internal class ToolDscTestCommand : CommandBase +{ + private readonly string _input; + + public ToolDscTestCommand(ParseResult parseResult) + : base(parseResult) + { + _input = parseResult.GetValue(ToolDscTestCommandParser.InputOption); + } + + public override int Execute() + { + try + { + DscToolsState inputState = null; + + if (!string.IsNullOrEmpty(_input)) + { + try + { + string jsonInput = DscWriter.ReadInput(_input); + inputState = JsonSerializer.Deserialize(jsonInput); + DscWriter.WriteTrace($"Input JSON deserialized: {inputState?.Tools?.Count ?? 0} tools"); + } + catch (JsonException ex) + { + DscWriter.WriteError($"Failed to deserialize JSON: {ex.Message}"); + return 1; + } + } + + var resultState = new DscToolsState(); + + foreach (var tool in inputState?.Tools ?? Enumerable.Empty()) + { + if (string.IsNullOrEmpty(tool.PackageId)) + { + DscWriter.WriteError("Property 'packageId' is required for 'test' operation."); + return 1; + } + + var testResult = TestToolState(tool); + resultState.Tools.Add(testResult); + } + + DscWriter.WriteResult(resultState); + + // Return exit code 0 if all tools exist as desired, 1 otherwise + // TODO: Implement proper comparison to determine if tools match desired state + return 0; + } + catch (Exception ex) + { + DscWriter.WriteError($"Unexpected error: {ex.Message}"); + return 1; + } + } + + private DscToolState TestToolState(DscToolState desiredState) + { + // TODO: Implement actual tool state comparison logic + // This would involve: + // - Getting the actual state of the tool + // - Comparing with desired state (version, commands, etc.) + // - Returning the actual state with _exist property + // The exit code indicates whether the system is in sync + + var result = new DscToolState + { + PackageId = desiredState.PackageId, + Version = desiredState.Version, + Scope = desiredState.Scope, + Exist = false // Placeholder - should query actual state + }; + + return result; + } + +} diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommandParser.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommandParser.cs new file mode 100644 index 000000000000..572e73b4abc2 --- /dev/null +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommandParser.cs @@ -0,0 +1,33 @@ +// 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 ToolDscTestCommandParser +{ + public static readonly Option InputOption = new("--input", "-i") + { + Description = CliCommandStrings.ToolDscInputOptionDescription, + HelpName = CliCommandStrings.ToolDscInputOptionName + }; + + private static readonly Command Command = ConstructCommand(); + + public static Command GetCommand() + { + return Command; + } + + private static Command ConstructCommand() + { + Command command = new("test", CliCommandStrings.ToolDscTestCommandDescription); + command.Options.Add(InputOption); + + command.SetAction((parseResult) => new ToolDscTestCommand(parseResult).Execute()); + + return command; + } +} diff --git a/src/Cli/dotnet/Commands/Tool/ToolCommandParser.cs b/src/Cli/dotnet/Commands/Tool/ToolCommandParser.cs index 0392bde15d3f..b0e93ef1a58d 100644 --- a/src/Cli/dotnet/Commands/Tool/ToolCommandParser.cs +++ b/src/Cli/dotnet/Commands/Tool/ToolCommandParser.cs @@ -4,6 +4,7 @@ #nullable disable using System.CommandLine; +using Microsoft.DotNet.Cli.Commands.Tool.Dsc; using Microsoft.DotNet.Cli.Commands.Tool.Execute; using Microsoft.DotNet.Cli.Commands.Tool.Install; using Microsoft.DotNet.Cli.Commands.Tool.List; @@ -39,6 +40,7 @@ private static Command ConstructCommand() command.Subcommands.Add(ToolSearchCommandParser.GetCommand()); command.Subcommands.Add(ToolRestoreCommandParser.GetCommand()); command.Subcommands.Add(ToolExecuteCommandParser.GetCommand()); + command.Subcommands.Add(ToolDscCommandParser.GetCommand()); command.SetAction((parseResult) => parseResult.HandleMissingCommand()); diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf index d1d8709624e6..ef306e017851 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf @@ -3218,6 +3218,51 @@ Chcete pokračovat? Stažení balíčku nástroje vyžaduje potvrzení. Spusťte v interaktivním režimu nebo potvrďte akci pomocí možnosti příkazového řádku --yes. {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. Spustí nástroj ze zdroje bez trvalé instalace. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf index 2a39befb9d3c..c24bfaf3c3f9 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf @@ -3218,6 +3218,51 @@ Vorgang fortsetzen? Der Download des Toolpakets muss bestätigt werden. Führen Sie den Vorgang im interaktiven Modus aus oder verwenden Sie zur Bestätigung die Befehlszeilenoption "--yes". {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. Führt ein Tool aus der Quelle aus, ohne es dauerhaft zu installieren. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf index 84f4aabb9069..353f5071c26c 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf @@ -3218,6 +3218,51 @@ Proceed? La descarga del paquete de herramientas necesita confirmación. Ejecute en modo interactivo o use la opción de línea de comandos "--yes" para confirmar. {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. Ejecuta una herramienta desde el origen sin instalarla permanentemente. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf index 6a637323a60e..3935070ab2a8 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf @@ -3218,6 +3218,51 @@ Voulez-vous continuer ? Le téléchargement du package d’outils doit être confirmé. Exécutez en mode interactif ou utilisez l’option de ligne de commande « --yes » pour confirmer. {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. Exécute un outil à partir de la source sans l’installer définitivement. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf index 1f255f43c794..050cc5eb7117 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf @@ -3218,6 +3218,51 @@ Procedere? Il download del pacchetto dello strumento richiede conferma. Esegui in modalità interattiva o usa l'opzione della riga di comando "--yes" per confermare. {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. Esegue uno strumento dall'origine senza installarlo permanentemente. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf index 7aa8108b6954..1a4b8d6972e8 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf @@ -3218,6 +3218,51 @@ Proceed? ツール パッケージのダウンロードには確認が必要です。対話モードで実行するか、"--yes" コマンド ライン オプションを使用して確認してください。 {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. ツールを永続的にインストールすることなく、ソースから実行します。 diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf index 6544ed0d7051..98c3a6edc685 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf @@ -3218,6 +3218,51 @@ Proceed? 도구 패키지 다운로드를 확인해야 합니다. 대화형 모드에서 실행하거나 "--yes" 명령줄 옵션을 사용하여 확인하세요. {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. 영구적으로 설치하지 않고 원본에서 도구를 실행합니다. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf index 6e7de9021244..8aace083331a 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf @@ -3218,6 +3218,51 @@ Kontynuować? Pobieranie pakietu narzędzi wymaga potwierdzenia. Uruchom w trybie interakcyjnym lub użyj opcji wiersza polecenia „--yes”, aby potwierdzić. {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. Wykonuje narzędzie ze źródła bez trwałego instalowania go. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf index 032d2caf149f..8813374a1da0 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf @@ -3218,6 +3218,51 @@ Continuar? O download do pacote de ferramentas precisa de confirmação. Execute no modo interativo ou use a opção de linha de comando "--yes" para confirmar. {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. Executa uma ferramenta da origem sem instalá-la permanentemente. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf index 05edfdd693ef..1d0d54365e3e 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf @@ -3218,6 +3218,51 @@ Proceed? Для скачивания пакета инструмента требуется подтверждение. Запустите в интерактивном режиме или используйте параметр командной строки "--yes" для подтверждения. {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. Выполняет инструмент из источника без его постоянной установки. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf index 228d3a11bd02..a0c8dac403a0 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf @@ -3218,6 +3218,51 @@ Devam edilsin mi? Araç paketinin indirilmesi için onay gerekiyor. Etkileşimli modda çalıştırın veya onaylamak için "--yes" komut satırı seçeneğini kullanın. {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. Aracı kalıcı olarak yüklemeden kaynaktan yürütür. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf index 8dce87da752d..5215b467d3ea 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf @@ -3218,6 +3218,51 @@ Proceed? 工具包下载需要确认。在交互模式下运行,或使用 “--yes” 命令行选项进行确认。 {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. 从源执行工具,而无需永久安装它。 diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf index 4402fa7bc229..d0cb397abbb9 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf @@ -3218,6 +3218,51 @@ Proceed? 工具套件下載需要確認。請以互動模式執行,或使用 "--yes" 命令列選項來確認。 {Locked="--yes"} + + Manage tools using Microsoft Desired State Configuration (DSC). + Manage tools using Microsoft Desired State Configuration (DSC). + + + + Export the current state of all installed tools. + Export the current state of all installed tools. + + + + Get the current state of installed tools. + Get the current state of installed tools. + + + + JSON input representing the desired or requested tool state. + JSON input representing the desired or requested tool state. + + + + JSON + JSON + + + + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + Get the Microsoft Desired State Configuration (DSC) resource manifest for dotnet tool. + + + + Get the JSON schema for DSC tool state. + Get the JSON schema for DSC tool state. + + + + Set the desired state of tools by installing or updating them. + Set the desired state of tools by installing or updating them. + + + + Test if the current state matches the desired state. + Test if the current state matches the desired state. + + Executes a tool from source without permanently installing it. 從來源執行工具,但不永久性地安裝。 diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index 2e63f9a3c5a8..9a45aeb11d75 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -107,4 +107,17 @@ OverwriteReadOnlyFiles="true" /> + + + + $(TargetName).dsc.resource.json + $(RepoRoot).dotnet\dotnet$([System.IO.Path]::GetExtension('$(DotnetToolCommand)')) + + + + + + + From d577313eb31d9200074b18106523ee53342a643f Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 1 Nov 2025 14:28:08 +0100 Subject: [PATCH 2/3] Add set command and refactor --- src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs | 35 ++++ src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs | 135 ++++++++++++++ .../Commands/Tool/Dsc/ToolDscGetCommand.cs | 125 +------------ .../Commands/Tool/Dsc/ToolDscSetCommand.cs | 97 +++++++---- .../Commands/Tool/Dsc/ToolDscTestCommand.cs | 164 +++++++++++++----- 5 files changed, 363 insertions(+), 193 deletions(-) diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs b/src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs index 2a1d94ef11b0..22205ddaa99d 100644 --- a/src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs +++ b/src/Cli/dotnet/Commands/Tool/Dsc/DscModels.cs @@ -2,6 +2,7 @@ // 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; @@ -34,6 +35,40 @@ internal record DscToolState [JsonPropertyName("_exist")] public bool? Exist { get; set; } + + /// + /// Parses packageId and version from the PackageId property. + /// Supports format: "packageId" or "packageId@version" + /// + 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 diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs b/src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs index 009b13cdbae5..d381692988fe 100644 --- a/src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs +++ b/src/Cli/dotnet/Commands/Tool/Dsc/DscWriter.cs @@ -2,7 +2,9 @@ // 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; @@ -81,4 +83,137 @@ public static string ReadInput(string input) return File.ReadAllText(input); } } + + /// + /// 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. + /// + public static DscToolsState? ReadAndDeserializeInput(string? input) + { + if (string.IsNullOrEmpty(input)) + { + return null; + } + + try + { + string jsonInput = ReadInput(input); + var inputState = JsonSerializer.Deserialize(jsonInput); + return inputState; + } + catch (JsonException ex) + { + WriteError($"Failed to deserialize JSON: {ex.Message}"); + Environment.Exit(1); + return null; // Unreachable, but satisfies compiler + } + } + + /// + /// Queries the actual state of a tool from the package store. + /// + 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 { 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 + }; + } + } } diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommand.cs index b270bbba0008..13cb49986589 100644 --- a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscGetCommand.cs @@ -5,9 +5,7 @@ using System.CommandLine; 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; @@ -25,34 +23,12 @@ public override int Execute() { try { - DscToolsState inputState = null; - - if (!string.IsNullOrEmpty(_input)) - { - try - { - string jsonInput = DscWriter.ReadInput(_input); - inputState = JsonSerializer.Deserialize(jsonInput); - DscWriter.WriteTrace($"Input JSON deserialized: {inputState?.Tools?.Count ?? 0} tools"); - } - catch (JsonException ex) - { - DscWriter.WriteError($"Failed to deserialize JSON: {ex.Message}"); - return 1; - } - } - + var inputState = DscWriter.ReadAndDeserializeInput(_input); var resultState = new DscToolsState(); foreach (var tool in inputState?.Tools ?? Enumerable.Empty()) { - if (string.IsNullOrEmpty(tool.PackageId)) - { - DscWriter.WriteError("Property 'packageId' is required for 'get' operation."); - return 1; - } - - var actualState = GetToolState(tool); + var actualState = DscWriter.QueryToolState(tool); if (actualState != null) { resultState.Tools.Add(actualState); @@ -69,101 +45,4 @@ public override int Execute() return 1; } } - - private DscToolState GetToolState(DscToolState requestedState) - { - // 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 - DscWriter.WriteTrace($"Local tool scope not yet implemented for {requestedState.PackageId}"); - return new DscToolState - { - PackageId = requestedState.PackageId, - 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(requestedState.PackageId); - - 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(); - - DscWriter.WriteTrace($"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 { package.Command.Name.Value } : null, - Scope = scope, - ToolPath = scope == DscToolScope.ToolPath ? requestedState.ToolPath : null, - ManifestPath = null, - Exist = true - }; - } - else - { - // Tool not found - DscWriter.WriteTrace($"Tool {requestedState.PackageId} not found in {scope} scope"); - - return new DscToolState - { - PackageId = requestedState.PackageId, - 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 - DscWriter.WriteError($"Error querying tool {requestedState.PackageId}: {ex.Message}"); - - return new DscToolState - { - PackageId = requestedState.PackageId, - Version = null, - Commands = null, - Scope = scope, - ToolPath = scope == DscToolScope.ToolPath ? requestedState.ToolPath : null, - ManifestPath = null, - Exist = false - }; - } - } - } diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommand.cs index 81c37fc1ece6..31848e5b3fb1 100644 --- a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSetCommand.cs @@ -5,7 +5,11 @@ using System.CommandLine; using System.Text.Json; +using Microsoft.DotNet.Cli.Commands.Tool.Install; +using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.EnvironmentAbstractions; +using NuGet.Versioning; namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; @@ -23,51 +27,87 @@ public override int Execute() { try { - DscToolsState inputState = null; + var inputState = DscWriter.ReadAndDeserializeInput(_input); + var resultState = new DscToolsState(); + bool hasFailures = false; - if (!string.IsNullOrEmpty(_input)) + foreach (var tool in inputState?.Tools ?? Enumerable.Empty()) { - try + // Skip tools that should not exist (Exist == false) + if (tool.Exist == false) { - string jsonInput = DscWriter.ReadInput(_input); - inputState = JsonSerializer.Deserialize(jsonInput); - DscWriter.WriteTrace($"Input JSON deserialized: {inputState?.Tools?.Count ?? 0} tools"); + DscWriter.WriteDebug($"Skipping tool {tool.PackageId}: _exist is false (tool removal not supported)"); + continue; } - catch (JsonException ex) + + // Only support Global scope for now + var scope = tool.Scope ?? DscToolScope.Global; + if (scope != DscToolScope.Global) { - DscWriter.WriteError($"Failed to deserialize JSON: {ex.Message}"); - return 1; + DscWriter.WriteDebug($"Skipping tool {tool.PackageId}: only Global scope is currently supported"); + continue; } - } - foreach (var tool in inputState?.Tools ?? Enumerable.Empty()) - { - if (string.IsNullOrEmpty(tool.PackageId)) + DscWriter.WriteDebug($"Setting desired state for tool: {tool.PackageId}"); + + // Parse packageId and version + var (packageId, versionRange) = tool.ParsePackageIdentity(); + + if (string.IsNullOrEmpty(packageId)) { - DscWriter.WriteError("Property 'packageId' is required for 'set' operation."); - return 1; + DscWriter.WriteError($"Invalid packageId: {tool.PackageId}"); + continue; } - if (tool.Exist == false) + // Install or update the tool + try { - DscWriter.WriteError($"Removing tools is not supported. Use 'dotnet tool uninstall' instead."); - return 1; + string installArgs = $"tool install -g {packageId}"; + if (versionRange != null) + { + installArgs += $" --version \"{versionRange.OriginalString}\""; + } + + var installParseResult = Parser.Parse($"dotnet {installArgs}"); + + var installCommand = new ToolInstallGlobalOrToolPathCommand( + installParseResult, + packageId: new PackageId(packageId), + createToolPackageStoreDownloaderUninstaller: null, + createShellShimRepository: null, + environmentPathInstruction: null, + reporter: null); + + int exitCode = installCommand.Execute(); + + if (exitCode != 0) + { + DscWriter.WriteError($"Failed to install/update tool {packageId}"); + hasFailures = true; + continue; + } + + DscWriter.WriteDebug($"Tool {packageId} is at desired state"); + } + catch (Exception ex) + { + DscWriter.WriteError($"Error installing/updating tool {packageId}: {ex.Message}"); + hasFailures = true; + continue; } - DscWriter.WriteDebug($"Setting desired state for tool: {tool.PackageId}"); + // Query final state after installation + var finalState = DscWriter.QueryToolState(new DscToolState + { + PackageId = packageId, + Scope = scope + }); - // TODO: Implement actual tool installation/update logic - // This would involve: - // - Checking if tool is already installed - // - Installing tool if not present - // - Updating tool if version doesn't match - // - Handling different scopes (global, local, toolPath) + resultState.Tools.Add(finalState); } - var resultState = new DscToolsState(); DscWriter.WriteResult(resultState); - - return 0; + return hasFailures ? 1 : 0; } catch (Exception ex) { @@ -75,5 +115,4 @@ public override int Execute() return 1; } } - } diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommand.cs index e445457a9ec8..741fa5378dee 100644 --- a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscTestCommand.cs @@ -6,6 +6,7 @@ using System.CommandLine; using System.Text.Json; using Microsoft.DotNet.Cli.Utils; +using NuGet.Versioning; namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; @@ -23,42 +24,29 @@ public override int Execute() { try { - DscToolsState inputState = null; - - if (!string.IsNullOrEmpty(_input)) - { - try - { - string jsonInput = DscWriter.ReadInput(_input); - inputState = JsonSerializer.Deserialize(jsonInput); - DscWriter.WriteTrace($"Input JSON deserialized: {inputState?.Tools?.Count ?? 0} tools"); - } - catch (JsonException ex) - { - DscWriter.WriteError($"Failed to deserialize JSON: {ex.Message}"); - return 1; - } - } - + var inputState = DscWriter.ReadAndDeserializeInput(_input); var resultState = new DscToolsState(); + bool allMatch = true; foreach (var tool in inputState?.Tools ?? Enumerable.Empty()) { - if (string.IsNullOrEmpty(tool.PackageId)) + var actualState = DscWriter.QueryToolState(tool); + bool matches = CompareToolStates(tool, actualState); + + actualState.Exist = matches; + + if (!matches) { - DscWriter.WriteError("Property 'packageId' is required for 'test' operation."); - return 1; + allMatch = false; } - var testResult = TestToolState(tool); - resultState.Tools.Add(testResult); + resultState.Tools.Add(actualState); } DscWriter.WriteResult(resultState); - // Return exit code 0 if all tools exist as desired, 1 otherwise - // TODO: Implement proper comparison to determine if tools match desired state - return 0; + // Return exit code 0 if all tools match desired state, 1 if any don't match + return allMatch ? 0 : 1; } catch (Exception ex) { @@ -67,24 +55,118 @@ public override int Execute() } } - private DscToolState TestToolState(DscToolState desiredState) + /// + /// Compares desired state with actual state to determine if they match. + /// + private bool CompareToolStates(DscToolState desired, DscToolState actual) { - // TODO: Implement actual tool state comparison logic - // This would involve: - // - Getting the actual state of the tool - // - Comparing with desired state (version, commands, etc.) - // - Returning the actual state with _exist property - // The exit code indicates whether the system is in sync - - var result = new DscToolState + // Parse packageId to handle packageId@version syntax + var (packageId, versionRange) = desired.ParsePackageIdentity(); + + // Check _exist property first + bool desiredExist = desired.Exist ?? true; // Default to true if not specified + bool actualExist = actual.Exist ?? false; + + if (desiredExist != actualExist) { - PackageId = desiredState.PackageId, - Version = desiredState.Version, - Scope = desiredState.Scope, - Exist = false // Placeholder - should query actual state - }; + DscWriter.WriteDebug($"Tool {packageId}: Exist mismatch (desired: {desiredExist}, actual: {actualExist})"); + return false; + } - return result; - } + // If tool should not exist and doesn't exist, that's a match + if (!desiredExist && !actualExist) + { + DscWriter.WriteDebug($"Tool {packageId}: Correctly does not exist"); + return true; + } + // If tool should exist but doesn't, already caught above + // Now check version if specified + if (versionRange != null) + { + // packageId@version syntax or Version property specified + if (string.IsNullOrEmpty(actual.Version)) + { + DscWriter.WriteDebug($"Tool {packageId}: Version mismatch (desired: {versionRange.OriginalString}, actual: none)"); + return false; + } + + // Check if actual version satisfies the version range + if (NuGetVersion.TryParse(actual.Version, out var actualVersion)) + { + if (!versionRange.Satisfies(actualVersion)) + { + DscWriter.WriteDebug($"Tool {packageId}: Version mismatch (desired: {versionRange.OriginalString}, actual: {actual.Version})"); + return false; + } + } + else + { + DscWriter.WriteDebug($"Tool {packageId}: Failed to parse actual version {actual.Version}"); + return false; + } + } + else if (!string.IsNullOrEmpty(desired.Version)) + { + // Fallback to string comparison for Version property (shouldn't happen if ParsePackageIdentity works) + if (string.IsNullOrEmpty(actual.Version)) + { + DscWriter.WriteDebug($"Tool {packageId}: Version mismatch (desired: {desired.Version}, actual: none)"); + return false; + } + + if (NuGetVersion.TryParse(desired.Version, out var desiredVersion) && + NuGetVersion.TryParse(actual.Version, out var actualVersion)) + { + if (!desiredVersion.Equals(actualVersion)) + { + DscWriter.WriteDebug($"Tool {packageId}: Version mismatch (desired: {desired.Version}, actual: {actual.Version})"); + return false; + } + } + else if (!string.Equals(desired.Version, actual.Version, StringComparison.OrdinalIgnoreCase)) + { + DscWriter.WriteDebug($"Tool {packageId}: Version mismatch (desired: {desired.Version}, actual: {actual.Version})"); + return false; + } + } + + // Check commands if specified + if (desired.Commands != null && desired.Commands.Any()) + { + if (actual.Commands == null || !actual.Commands.Any()) + { + DscWriter.WriteDebug($"Tool {desired.PackageId}: Commands mismatch (desired commands specified, actual: none)"); + return false; + } + + // Check if all desired commands are present in actual + var missingCommands = desired.Commands.Except(actual.Commands, StringComparer.OrdinalIgnoreCase).ToList(); + if (missingCommands.Any()) + { + DscWriter.WriteDebug($"Tool {desired.PackageId}: Missing commands: {string.Join(", ", missingCommands)}"); + return false; + } + } + + // Check scope if specified + if (desired.Scope.HasValue && actual.Scope.HasValue) + { + if (desired.Scope.Value != actual.Scope.Value) + { + DscWriter.WriteDebug($"Tool {desired.PackageId}: Scope mismatch (desired: {desired.Scope.Value}, actual: {actual.Scope.Value})"); + return false; + } + } + + if (!string.IsNullOrEmpty(desired.ToolPath) && !string.IsNullOrEmpty(actual.ToolPath)) + { + if (!string.Equals(desired.ToolPath, actual.ToolPath, StringComparison.OrdinalIgnoreCase)) + { + DscWriter.WriteDebug($"Tool {desired.PackageId}: ToolPath mismatch (desired: {desired.ToolPath}, actual: {actual.ToolPath})"); + return false; + } + } + return true; + } } From 1abe3a6f77cf73c6dcbf2a526886b54d8c77ebff Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 1 Nov 2025 14:32:59 +0100 Subject: [PATCH 3/3] Refactor schema --- .../Commands/Tool/Dsc/ToolDscSchemaCommand.cs | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommand.cs b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommand.cs index b2eee3f4805a..6de2f631e07d 100644 --- a/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Dsc/ToolDscSchemaCommand.cs @@ -4,7 +4,9 @@ #nullable disable using System.CommandLine; +using System.Reflection; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Cli.Commands.Tool.Dsc; @@ -20,8 +22,27 @@ public override int Execute() { try { - // For now, return a basic schema - // TODO: Generate proper JSON schema when System.Text.Json.Schema is available + // Generate schema dynamically from DscToolState model + var toolProperties = new Dictionary(); + var requiredProperties = new List(); + + foreach (var prop in typeof(DscToolState).GetProperties()) + { + var jsonPropertyAttr = prop.GetCustomAttribute(); + if (jsonPropertyAttr == null) continue; + + string propertyName = jsonPropertyAttr.Name; + var propertySchema = GetPropertySchema(prop); + + toolProperties[propertyName] = propertySchema; + + // packageId is required + if (propertyName == "packageId") + { + requiredProperties.Add(propertyName); + } + } + var schema = new { type = "object", @@ -33,17 +54,8 @@ public override int Execute() items = new { type = "object", - properties = new - { - packageId = new { type = "string" }, - version = new { type = "string" }, - commands = new { type = "array", items = new { type = "string" } }, - scope = new { type = "string", @enum = new[] { "Global", "Local", "ToolPath" } }, - toolPath = new { type = "string" }, - manifestPath = new { type = "string" }, - _exist = new { type = "boolean" } - }, - required = new[] { "packageId" } + properties = toolProperties, + required = requiredProperties.ToArray() } } } @@ -59,4 +71,38 @@ public override int Execute() return 1; } } + + private static object GetPropertySchema(PropertyInfo prop) + { + var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; + + // Check for enum + if (underlyingType.IsEnum) + { + var enumValues = Enum.GetNames(underlyingType); + return new { type = "string", @enum = enumValues }; + } + + // Check for List + if (underlyingType.IsGenericType && underlyingType.GetGenericTypeDefinition() == typeof(List<>)) + { + var itemType = underlyingType.GetGenericArguments()[0]; + if (itemType == typeof(string)) + { + return new { type = "array", items = new { type = "string" } }; + } + } + + // Map CLR types to JSON schema types + if (underlyingType == typeof(string)) + return new { type = "string" }; + if (underlyingType == typeof(bool)) + return new { type = "boolean" }; + if (underlyingType == typeof(int) || underlyingType == typeof(long)) + return new { type = "integer" }; + if (underlyingType == typeof(double) || underlyingType == typeof(float)) + return new { type = "number" }; + + return new { type = "string" }; + } }