diff --git a/src/TALXIS.CLI.Core/Contracts/Dataverse/ComponentTypeResolver.cs b/src/TALXIS.CLI.Core/Contracts/Dataverse/ComponentTypeResolver.cs deleted file mode 100644 index 1c705da..0000000 --- a/src/TALXIS.CLI.Core/Contracts/Dataverse/ComponentTypeResolver.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace TALXIS.CLI.Core.Contracts.Dataverse; - -/// -/// Maps between integer component-type codes and human-friendly names. -/// Supports both directions: code → name and name → code. -/// Platform types are hardcoded; SCF types can be loaded dynamically from the environment. -/// -public sealed class ComponentTypeResolver -{ - private readonly Dictionary _codeToName; - private readonly Dictionary _nameToCode; - - public ComponentTypeResolver() - { - _codeToName = new Dictionary(PlatformTypes); - _nameToCode = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var (code, name) in PlatformTypes) - _nameToCode[name] = code; - // Register common aliases - foreach (var (alias, code) in PlatformAliases) - _nameToCode[alias] = code; - } - - /// Resolves a friendly name to its integer type code. - public bool TryResolveCode(string nameOrCode, out int code) - { - if (int.TryParse(nameOrCode, out code)) - return _codeToName.ContainsKey(code) || code > 0; - return _nameToCode.TryGetValue(nameOrCode, out code); - } - - /// Returns all known friendly names for use in error messages. - public IEnumerable GetKnownNames() => _nameToCode.Keys.Order(); - - /// Resolves an integer type code to a friendly name. - public string ResolveName(int code) - => _codeToName.TryGetValue(code, out var name) ? name : code.ToString(); - - /// Well-known platform component types (static, same across all environments). - private static readonly Dictionary PlatformTypes = new() - { - [1] = "Entity", - [2] = "Attribute", - [3] = "Relationship", - [9] = "OptionSet", - [10] = "EntityRelationship", - [14] = "EntityKey", - [16] = "Privilege", - [20] = "Role", - [26] = "SavedQuery", - [29] = "Workflow", - [31] = "Report", - [36] = "EmailTemplate", - [59] = "SavedQueryVisualization", - [60] = "SystemForm", - [61] = "WebResource", - [62] = "SiteMap", - [63] = "ConnectionRole", - [66] = "CustomControl", - [70] = "FieldSecurityProfile", - [80] = "AppModule", - [91] = "PluginAssembly", - [92] = "SdkMessageProcessingStep", - [95] = "ServiceEndpoint", - [300] = "CanvasApp", - [371] = "Connector", - [380] = "EnvironmentVariableDefinition", - [381] = "EnvironmentVariableValue", - }; - - /// Common aliases developers might use on the command line. - private static readonly Dictionary PlatformAliases = new(StringComparer.OrdinalIgnoreCase) - { - ["Table"] = 1, - ["Column"] = 2, - ["Choice"] = 9, - ["View"] = 26, - ["Chart"] = 59, - ["Form"] = 60, - ["Dashboard"] = 60, - ["SecurityRole"] = 20, - ["Process"] = 29, - ["PluginStep"] = 92, - ["EnvironmentVariable"] = 380, - }; -} diff --git a/src/TALXIS.CLI.Core/Shared/BrowserLauncher.cs b/src/TALXIS.CLI.Core/Shared/BrowserLauncher.cs new file mode 100644 index 0000000..781e4e3 --- /dev/null +++ b/src/TALXIS.CLI.Core/Shared/BrowserLauncher.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using TALXIS.CLI.Core.Abstractions; +using TALXIS.CLI.Core.DependencyInjection; + +namespace TALXIS.CLI.Core; + +/// +/// Cross-platform utility to open a URL in the default browser. +/// Headless-aware: skips browser launch in CI/non-interactive environments. +/// +public static class BrowserLauncher +{ + /// + /// Opens in the default browser. + /// In headless/CI mode, logs a warning and returns without opening. + /// + public static void Open(Uri url, ILogger logger) + { + var detector = TxcServices.Get(); + if (detector.IsHeadless) + { + logger.LogWarning("Headless mode ({Reason}) — browser not opened.", detector.Reason); + return; + } + + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Process.Start(new ProcessStartInfo(url.AbsoluteUri) { UseShellExecute = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url.AbsoluteUri); + } + else + { + Process.Start("xdg-open", url.AbsoluteUri); + } + } + catch (Exception ex) + { + logger.LogWarning("Could not open browser: {Error}", ex.Message); + } + } +} diff --git a/src/TALXIS.CLI.Core/TALXIS.CLI.Core.csproj b/src/TALXIS.CLI.Core/TALXIS.CLI.Core.csproj index c63cc97..49d08f7 100644 --- a/src/TALXIS.CLI.Core/TALXIS.CLI.Core.csproj +++ b/src/TALXIS.CLI.Core/TALXIS.CLI.Core.csproj @@ -14,6 +14,7 @@ + diff --git a/src/TALXIS.CLI.Features.Environment/Component/Browse/BrowseUrlConstants.cs b/src/TALXIS.CLI.Features.Environment/Component/Browse/BrowseUrlConstants.cs new file mode 100644 index 0000000..f326ce2 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Component/Browse/BrowseUrlConstants.cs @@ -0,0 +1,22 @@ +namespace TALXIS.CLI.Features.Environment.Component.Browse; + +/// +/// Shared constants for browse URL construction. +/// +public static class BrowseUrlConstants +{ + /// Default Solution GUID — used when no solution context is specified for form/view/securityrole. + public const string DefaultSolutionId = "fd140aaf-4df4-11dd-bd17-0019b9312238"; + + /// + /// Extracts the hostname from an org URL for use in URL construction. + /// Accepts either a full URL or a bare hostname. + /// + public static string NormalizeOrgUrl(string orgUrl) + { + if (Uri.TryCreate(orgUrl, UriKind.Absolute, out var uri)) + return uri.Host; + // Already a bare hostname + return orgUrl.TrimEnd('/'); + } +} diff --git a/src/TALXIS.CLI.Features.Environment/Component/Browse/CanvasAppUrls.cs b/src/TALXIS.CLI.Features.Environment/Component/Browse/CanvasAppUrls.cs new file mode 100644 index 0000000..8c3bfea --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Component/Browse/CanvasAppUrls.cs @@ -0,0 +1,31 @@ +namespace TALXIS.CLI.Features.Environment.Component.Browse; + +/// +/// URL builders for the Power Apps canvas app player (apps.powerapps.com/play). +/// Supports screen navigation, custom parameters, and hidden navbar. +/// +public static class CanvasAppUrls +{ + private const string Base = "https://apps.powerapps.com/play"; + + /// + /// Open a canvas app in the Power Apps player. + /// + public static Uri Play(Guid environmentId, Guid appId, string? tenantId, + string? screenName = null, IDictionary? customParams = null, bool hideNavbar = false) + { + var qs = new List(); + if (!string.IsNullOrWhiteSpace(tenantId)) + qs.Add($"tenantId={Uri.EscapeDataString(tenantId)}"); + if (!string.IsNullOrWhiteSpace(screenName)) + qs.Add($"screenName={Uri.EscapeDataString(screenName)}"); + if (hideNavbar) + qs.Add("hidenavbar=true"); + if (customParams != null) + foreach (var (key, value) in customParams) + qs.Add($"{Uri.EscapeDataString(key)}={Uri.EscapeDataString(value)}"); + + var query = qs.Count > 0 ? "?" + string.Join("&", qs) : ""; + return new Uri($"{Base}/e/{environmentId}/a/{appId}{query}"); + } +} diff --git a/src/TALXIS.CLI.Features.Environment/Component/Browse/ComponentBrowseCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Component/Browse/ComponentBrowseCliCommand.cs new file mode 100644 index 0000000..d94bb99 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Component/Browse/ComponentBrowseCliCommand.cs @@ -0,0 +1,364 @@ +using DotMake.CommandLine; +using Microsoft.Extensions.Logging; +using TALXIS.CLI.Core; +using TALXIS.CLI.Core.Abstractions; +using TALXIS.CLI.Core.Contracts.Dataverse; +using TALXIS.CLI.Core.DependencyInjection; +using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; + +namespace TALXIS.CLI.Features.Environment.Component.Browse; + +/// +/// Opens the Power Platform web editor for a component instance. +/// Resolves the appropriate URL based on component type and opens it in the default browser. +/// In headless mode, only prints the URL without opening the browser. +/// +[CliReadOnly] +[CliCommand( + Name = "browse", + Description = "Open the web editor for a component in the connected live environment. Requires an active profile." +)] +public class ComponentBrowseCliCommand : ProfiledCliCommand +{ + protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(); + + [CliOption(Name = "--type", Description = "Component type (name, alias, or integer code). Run 'txc component type list' to see available types.", Required = true)] + public string Type { get; set; } = null!; + + [CliOption(Name = "--id", Description = "Component GUID. Mutually exclusive with --name.", Required = false)] + public string? Id { get; set; } + + [CliOption(Name = "--name", Description = "Component friendly name (resolved to GUID). Mutually exclusive with --id. Supported for: solution (unique name), entity (logical name).", Required = false)] + public string? Name { get; set; } + + [CliOption(Name = "--entity", Description = "Entity logical name. Required for form/view types. Also provides the backing entity name for SCF types.", Required = false)] + public string? Entity { get; set; } + + [CliOption(Name = "--solution", Description = "Solution unique name for solution-scoped URLs. Resolved to GUID.", Required = false)] + public string? Solution { get; set; } + + // ── App deep-link options (model-driven apps) ── + + [CliOption(Name = "--pagetype", Description = "UCI page type for deep-linking within an app: entityrecord, entitylist, dashboard, webresource, control, custom, inlinedialog, genux, search.", Required = false)] + public string? PageType { get; set; } + + [CliOption(Name = "--record", Description = "Record GUID to open in an entity form (pagetype=entityrecord).", Required = false)] + public string? Record { get; set; } + + [CliOption(Name = "--formid", Description = "Specific form GUID to use for entity record.", Required = false)] + public string? FormId { get; set; } + + [CliOption(Name = "--viewid", Description = "View GUID for entity list (pagetype=entitylist).", Required = false)] + public string? ViewId { get; set; } + + [CliOption(Name = "--dashboard", Description = "Dashboard GUID (pagetype=dashboard).", Required = false)] + public string? Dashboard { get; set; } + + [CliOption(Name = "--custom-page", Description = "Custom page logical name (pagetype=custom).", Required = false)] + public string? CustomPage { get; set; } + + [CliOption(Name = "--control", Description = "Full-page PCF control fully-qualified name (pagetype=control).", Required = false)] + public string? Control { get; set; } + + [CliOption(Name = "--webresource", Description = "Web resource logical name (pagetype=webresource).", Required = false)] + public string? WebResource { get; set; } + + [CliOption(Name = "--genux", Description = "Generative/Copilot AI page ID (pagetype=genux).", Required = false)] + public string? Genux { get; set; } + + [CliOption(Name = "--dialog-name", Description = "Inline dialog name (pagetype=inlinedialog).", Required = false)] + public string? DialogName { get; set; } + + [CliOption(Name = "--dialog-options", Description = "Inline dialog options JSON (pagetype=inlinedialog).", Required = false)] + public string? DialogOptions { get; set; } + + [CliOption(Name = "--data", Description = "Data parameter for control, genux, or webresource page types.", Required = false)] + public string? Data { get; set; } + + [CliOption(Name = "--extraqs", Description = "Pre-populate form fields as key=value pairs (URL-encoded automatically).", Required = false)] + public string? ExtraQs { get; set; } + + [CliOption(Name = "--navbar", Description = "Navigation bar mode: on, off, entity.", Required = false)] + public string? Navbar { get; set; } + + [CliOption(Name = "--cmdbar", Description = "Show command bar: true or false.", Required = false)] + public string? Cmdbar { get; set; } + + // ── Canvas app options ── + + [CliOption(Name = "--screen", Description = "Canvas app screen name to navigate to on launch.", Required = false)] + public string? Screen { get; set; } + + [CliOption(Name = "--param", Description = "Canvas app custom parameter (key=value). Can be specified multiple times.", Required = false)] + public List Param { get; set; } = new(); + + [CliOption(Name = "--hidenavbar", Description = "Canvas app: hide the Power Apps navigation bar.", Required = false)] + public bool HideNavbar { get; set; } + + // ── Power Automate options ── + + [CliOption(Name = "--flow-view", Description = "Flow page to open: editor (default), details, or runs.", Required = false)] + public string? FlowView { get; set; } + + [CliOption(Name = "--run", Description = "Specific flow run ID to open.", Required = false)] + public string? Run { get; set; } + + // ── Report options ── + + [CliOption(Name = "--report-action", Description = "Report viewer action: run (default) or filter.", Required = false)] + public string? ReportAction { get; set; } + + protected override async Task ExecuteAsync() + { + // Resolve component type + var def = ComponentDefinitionRegistry.GetByName(Type); + ComponentType? typeCode = def?.TypeCode; + if (typeCode is null && int.TryParse(Type, out var rawCode) && rawCode > 0) + typeCode = (ComponentType)rawCode; + if (typeCode is null) + { + Logger.LogError("Unknown component type '{Type}'. Run 'txc component type list' to see available types.", Type); + return ExitValidationError; + } + + // Resolve profile + connection + var configResolver = TxcServices.Get(); + var ctx = await configResolver.ResolveAsync(Profile, CancellationToken.None).ConfigureAwait(false); + var connection = ctx.Connection; + + if (connection.EnvironmentId is null) + { + Logger.LogError("Environment ID is not set on the connection. Run 'txc config connection check' to populate it."); + return ExitValidationError; + } + var environmentId = connection.EnvironmentId.Value; + + // Validate EnvironmentUrl for types that require it (UCI, reports, SCF record forms) + var needsOrgUrl = typeCode.Value is ComponentType.AppModule or ComponentType.Report + || (typeCode.Value is not ComponentType.CanvasApp and not ComponentType.Workflow); + string? orgUrl = null; + if (!string.IsNullOrWhiteSpace(connection.EnvironmentUrl) + && Uri.TryCreate(connection.EnvironmentUrl, UriKind.Absolute, out var orgUri)) + { + orgUrl = orgUri.Host; + } + else if (needsOrgUrl) + { + Logger.LogError("Environment URL is not set or invalid on the connection. Run 'txc config connection check' to populate it."); + return ExitValidationError; + } + + // Dispatch by component type + Uri? url = typeCode.Value switch + { + ComponentType.AppModule => BuildAppModuleUrl(orgUrl!), + ComponentType.CanvasApp => BuildCanvasAppUrl(environmentId, connection.TenantId), + ComponentType.Report => BuildReportUrl(orgUrl!), + ComponentType.Workflow => await BuildFlowUrlAsync(environmentId).ConfigureAwait(false), + _ => await BuildMakerEditorUrlAsync(typeCode.Value, environmentId, orgUrl).ConfigureAwait(false) + }; + + if (url is null) + return ExitValidationError; // error already logged + + OutputFormatter.WriteData(new { url = url.AbsoluteUri, type = def?.Name ?? typeCode.Value.ToString() }, + _ => OutputWriter.WriteLine(url.AbsoluteUri)); + + BrowserLauncher.Open(url, Logger); + return ExitSuccess; + } + + /// Build URL for model-driven app (shell or deep-link via pagetype). + private Uri? BuildAppModuleUrl(string orgUrl) + { + if (string.IsNullOrWhiteSpace(PageType)) + { + if (!string.IsNullOrWhiteSpace(Name)) + return PowerAppsUciUrls.AppByName(orgUrl, Name); + if (!string.IsNullOrWhiteSpace(Id) && Guid.TryParse(Id, out var appId)) + return PowerAppsUciUrls.AppById(orgUrl, appId); + Logger.LogError("Provide --name or --id for the app module."); + return null; + } + + var queryParams = new Dictionary(); + if (!string.IsNullOrWhiteSpace(Entity)) queryParams["etn"] = Entity; + if (!string.IsNullOrWhiteSpace(Record)) queryParams["id"] = Record; + if (!string.IsNullOrWhiteSpace(FormId)) queryParams["formid"] = FormId; + if (!string.IsNullOrWhiteSpace(ViewId)) { queryParams["viewid"] = ViewId; queryParams["viewtype"] = "1039"; } + if (!string.IsNullOrWhiteSpace(Dashboard)) queryParams["id"] = Dashboard; + if (!string.IsNullOrWhiteSpace(CustomPage)) queryParams["name"] = CustomPage; + if (!string.IsNullOrWhiteSpace(Control)) queryParams["controlName"] = Control; + if (!string.IsNullOrWhiteSpace(WebResource)) queryParams["webresourceName"] = WebResource; + if (!string.IsNullOrWhiteSpace(Genux)) queryParams["id"] = Genux; + if (!string.IsNullOrWhiteSpace(DialogName)) queryParams["name"] = DialogName; + if (!string.IsNullOrWhiteSpace(DialogOptions)) queryParams["dialogOptions"] = DialogOptions; + if (!string.IsNullOrWhiteSpace(Data)) queryParams["data"] = Data; + if (!string.IsNullOrWhiteSpace(ExtraQs)) queryParams["extraqs"] = ExtraQs; + if (!string.IsNullOrWhiteSpace(Navbar)) queryParams["navbar"] = Navbar; + if (!string.IsNullOrWhiteSpace(Cmdbar)) queryParams["cmdbar"] = Cmdbar; + + Guid? appId2 = null; + if (!string.IsNullOrWhiteSpace(Id) && Guid.TryParse(Id, out var parsed)) + appId2 = parsed; + + return PowerAppsUciUrls.DeepLink(orgUrl, Name, appId2, PageType, queryParams); + } + + /// Build URL for Power Automate flow — editor, details, runs, or specific run. + private async Task BuildFlowUrlAsync(Guid environmentId) + { + if (string.IsNullOrWhiteSpace(Id) || !Guid.TryParse(Id, out var flowId)) + { + Logger.LogError("--id is required for flows."); + return null; + } + + Guid? solutionId = null; + if (!string.IsNullOrWhiteSpace(Solution)) + { + var slnService = TxcServices.Get(); + var (sln, _) = await slnService.ShowAsync(Profile, Solution, CancellationToken.None).ConfigureAwait(false); + solutionId = sln.Id; + } + + if (!string.IsNullOrWhiteSpace(Run)) + return PowerAutomateUrls.FlowRun(environmentId, flowId, Run, solutionId); + + return (FlowView?.ToLowerInvariant()) switch + { + "details" => PowerAutomateUrls.FlowDetails(environmentId, flowId, solutionId), + "runs" => PowerAutomateUrls.FlowRuns(environmentId, flowId, solutionId), + _ => PowerAutomateUrls.FlowEditor(environmentId, flowId, solutionId) + }; + } + + /// Build URL for canvas app player. + private Uri? BuildCanvasAppUrl(Guid environmentId, string? tenantId) + { + if (string.IsNullOrWhiteSpace(Id) || !Guid.TryParse(Id, out var appId)) + { + Logger.LogError("--id is required for canvas apps."); + return null; + } + + var customParams = new Dictionary(); + foreach (var p in Param) + { + var idx = p.IndexOf('='); + if (idx > 0 && idx < p.Length - 1) + customParams[p[..idx]] = p[(idx + 1)..]; + } + + return CanvasAppUrls.Play(environmentId, appId, tenantId, Screen, + customParams.Count > 0 ? customParams : null, HideNavbar); + } + + /// Build URL for report viewer. + private Uri? BuildReportUrl(string orgUrl) + { + if (string.IsNullOrWhiteSpace(Id) || !Guid.TryParse(Id, out var reportId)) + { + Logger.LogError("--id is required for reports."); + return null; + } + return PowerAppsUciUrls.Report(orgUrl, reportId, ReportAction ?? "run"); + } + + /// Build URL for maker portal editor (existing component types). + private async Task BuildMakerEditorUrlAsync(ComponentType typeCode, Guid environmentId, string? orgUrl) + { + // Validate --id / --name + if (string.IsNullOrWhiteSpace(Id) && string.IsNullOrWhiteSpace(Name)) + { + Logger.LogError("Provide --id or --name ."); + return null; + } + if (!string.IsNullOrWhiteSpace(Id) && !string.IsNullOrWhiteSpace(Name)) + { + Logger.LogError("--id and --name are mutually exclusive."); + return null; + } + + Guid componentId; + if (!string.IsNullOrWhiteSpace(Id)) + { + if (!Guid.TryParse(Id, out componentId)) + { + Logger.LogError("Invalid GUID: '{Id}'.", Id); + return null; + } + } + else + { + var resolved = await ResolveNameToGuidAsync(typeCode, Name!).ConfigureAwait(false); + if (resolved is null) return null; + componentId = resolved.Value; + } + + // Resolve --solution + Guid? solutionId = null; + if (!string.IsNullOrWhiteSpace(Solution)) + { + var slnService = TxcServices.Get(); + var (sln, _) = await slnService.ShowAsync(Profile, Solution, CancellationToken.None).ConfigureAwait(false); + solutionId = sln.Id; + } + + if (typeCode is ComponentType.SystemForm or ComponentType.Form or ComponentType.SavedQuery + && string.IsNullOrWhiteSpace(Entity)) + { + Logger.LogError("--entity is required for form/view types."); + return null; + } + + var url = BuildEditorUrl(typeCode, environmentId, orgUrl, componentId, Entity, solutionId); + if (url is null) + Logger.LogError("Cannot build URL for type '{Type}' (code {Code}). For SCF types, provide --entity.", Type, (int)typeCode); + return url; + } + + /// Dispatches to the appropriate URL builder based on component type. + private static Uri? BuildEditorUrl(ComponentType typeCode, Guid envId, string? orgUrl, Guid componentId, string? entity, Guid? solutionId) + { + return typeCode switch + { + ComponentType.Solution => MakerPortalUrls.Solution(envId, componentId), + ComponentType.Entity => MakerPortalUrls.Entity(envId, componentId, solutionId), + ComponentType.SystemForm when entity != null => MakerPortalUrls.FormDesigner(envId, entity, componentId, solutionId), + ComponentType.Form when entity != null => MakerPortalUrls.FormDesigner(envId, entity, componentId, solutionId), + ComponentType.SavedQuery when entity != null => MakerPortalUrls.ViewDesigner(envId, entity, componentId, solutionId), + ComponentType.Bot => CopilotStudioUrls.BotEditor(envId, componentId, solutionId), + ComponentType.Dataflow => MakerPortalUrls.DataflowEditor(envId, componentId), + ComponentType.Role => MakerPortalUrls.SecurityRoleEditor(envId, componentId, solutionId), + // SCF / unknown — fallback to UCI record form + _ when orgUrl != null && entity != null => PowerAppsUciUrls.RecordForm(orgUrl, entity, componentId), + _ => null + }; + } + + private async Task ResolveNameToGuidAsync(ComponentType typeCode, string name) + { + switch (typeCode) + { + case ComponentType.Solution: + var slnService = TxcServices.Get(); + var (sln, _) = await slnService.ShowAsync(Profile, name, CancellationToken.None).ConfigureAwait(false); + return sln.Id; + + case ComponentType.Entity: + var metadataResolver = TxcServices.Get(); + return await metadataResolver.ResolveEntityIdAsync(Profile, name, CancellationToken.None).ConfigureAwait(false); + + case ComponentType.AppModule: + // AppModule name resolution handled in BuildAppModuleUrl + Logger.LogError("AppModule name resolution is handled via --name directly."); + return null; + + default: + Logger.LogError("--name is not supported for type '{Type}'. Use --id instead.", Type); + return null; + } + } +} diff --git a/src/TALXIS.CLI.Features.Environment/Component/Browse/CopilotStudioUrls.cs b/src/TALXIS.CLI.Features.Environment/Component/Browse/CopilotStudioUrls.cs new file mode 100644 index 0000000..6ad8902 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Component/Browse/CopilotStudioUrls.cs @@ -0,0 +1,16 @@ +namespace TALXIS.CLI.Features.Environment.Component.Browse; + +/// +/// URL builders for Copilot Studio (copilotstudio.microsoft.com). +/// Covers bot/agent editor. +/// +public static class CopilotStudioUrls +{ + private const string Base = "https://copilotstudio.microsoft.com"; + + /// Open the bot/agent editor in Copilot Studio. + public static Uri BotEditor(Guid environmentId, Guid botId, Guid? solutionId = null) + => solutionId.HasValue + ? new($"{Base}/environments/{environmentId}/bots/{botId}?solutionId={solutionId}") + : new($"{Base}/environments/{environmentId}/bots/{botId}"); +} diff --git a/src/TALXIS.CLI.Features.Environment/Component/Browse/MakerPortalUrls.cs b/src/TALXIS.CLI.Features.Environment/Component/Browse/MakerPortalUrls.cs new file mode 100644 index 0000000..8d9a731 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Component/Browse/MakerPortalUrls.cs @@ -0,0 +1,40 @@ +namespace TALXIS.CLI.Features.Environment.Component.Browse; + +/// +/// URL builders for the Power Apps maker portal (make.powerapps.com). +/// Covers solution editor, entity fields, form designer, view designer, +/// security role editor, and dataflow editor. +/// +public static class MakerPortalUrls +{ + private const string Base = "https://make.powerapps.com"; + + public static Uri Solution(Guid environmentId, Guid solutionId) + => new($"{Base}/environments/{environmentId}/solutions/{solutionId}"); + + public static Uri Entity(Guid environmentId, Guid metadataId, Guid? solutionId = null) + => solutionId.HasValue + ? new($"{Base}/environments/{environmentId}/solutions/{solutionId}/entities/{metadataId}/fields") + : new($"{Base}/environments/{environmentId}/entities/{metadataId}/fields"); + + public static Uri FormDesigner(Guid environmentId, string entityLogicalName, Guid formId, Guid? solutionId = null) + { + var slnId = solutionId ?? Guid.Parse(BrowseUrlConstants.DefaultSolutionId); + return new($"{Base}/e/{environmentId}/s/{slnId}/entity/{entityLogicalName}/form/edit/{formId}"); + } + + public static Uri ViewDesigner(Guid environmentId, string entityLogicalName, Guid viewId, Guid? solutionId = null) + { + var slnId = solutionId ?? Guid.Parse(BrowseUrlConstants.DefaultSolutionId); + return new($"{Base}/e/{environmentId}/s/{slnId}/entity/{entityLogicalName}/view/{viewId}"); + } + + public static Uri SecurityRoleEditor(Guid environmentId, Guid roleId, Guid? solutionId = null) + { + var slnId = solutionId ?? Guid.Parse(BrowseUrlConstants.DefaultSolutionId); + return new($"{Base}/e/{environmentId}/s/{slnId}/securityroles/{roleId}/roleeditor"); + } + + public static Uri DataflowEditor(Guid environmentId, Guid dataflowId) + => new($"{Base}/environments/{environmentId}/dataintegration/list/{dataflowId}/edit"); +} diff --git a/src/TALXIS.CLI.Features.Environment/Component/Browse/PowerAppsUciUrls.cs b/src/TALXIS.CLI.Features.Environment/Component/Browse/PowerAppsUciUrls.cs new file mode 100644 index 0000000..838a031 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Component/Browse/PowerAppsUciUrls.cs @@ -0,0 +1,51 @@ +namespace TALXIS.CLI.Features.Environment.Component.Browse; + +/// +/// URL builders for the Power Apps UCI runtime ({org}.crm{N}.dynamics.com/main.aspx). +/// Covers model-driven app shell, all UCI page types (entityrecord, entitylist, dashboard, +/// webresource, control, custom, inlinedialog, genux, search), SCF record forms, and reports. +/// +public static class PowerAppsUciUrls +{ + /// Open a model-driven app by its unique name. + public static Uri AppByName(string orgUrl, string uniqueName) + => new($"https://{BrowseUrlConstants.NormalizeOrgUrl(orgUrl)}/main.aspx?appname={Uri.EscapeDataString(uniqueName)}"); + + /// Open a model-driven app by its AppModuleId GUID. + public static Uri AppById(string orgUrl, Guid appModuleId) + => new($"https://{BrowseUrlConstants.NormalizeOrgUrl(orgUrl)}/main.aspx?appid={appModuleId}"); + + /// + /// Generic deep-link into a model-driven app via main.aspx. + /// Builds the URL from app identity + pagetype + arbitrary query parameters. + /// Supports all UCI page types: entityrecord, entitylist, dashboard, webresource, + /// control, custom, inlinedialog, genux, search, apps. + /// + public static Uri DeepLink(string orgUrl, string? appName, Guid? appId, string pageType, IDictionary queryParams) + { + var org = BrowseUrlConstants.NormalizeOrgUrl(orgUrl); + var qs = new List(); + + if (!string.IsNullOrWhiteSpace(appName)) + qs.Add($"appname={Uri.EscapeDataString(appName)}"); + else if (appId.HasValue) + qs.Add($"appid={appId.Value}"); + + qs.Add($"pagetype={Uri.EscapeDataString(pageType)}"); + + foreach (var (key, value) in queryParams) + qs.Add($"{Uri.EscapeDataString(key)}={Uri.EscapeDataString(value)}"); + + return new Uri($"https://{org}/main.aspx?{string.Join("&", qs)}"); + } + + /// + /// Open an SCF or unrecognized component type's backing entity record form. + /// + public static Uri RecordForm(string orgUrl, string entityLogicalName, Guid recordId) + => new($"https://{BrowseUrlConstants.NormalizeOrgUrl(orgUrl)}/main.aspx?forceUCI=1&newWindow=true&pagetype=entityrecord&etn={entityLogicalName}&id={recordId}"); + + /// Open a report in the Dynamics report viewer. + public static Uri Report(string orgUrl, Guid reportId, string action = "run") + => new($"https://{BrowseUrlConstants.NormalizeOrgUrl(orgUrl)}/crmreports/viewer/viewer.aspx?action={Uri.EscapeDataString(action)}&id=%7b{reportId}%7d"); +} diff --git a/src/TALXIS.CLI.Features.Environment/Component/Browse/PowerAutomateUrls.cs b/src/TALXIS.CLI.Features.Environment/Component/Browse/PowerAutomateUrls.cs new file mode 100644 index 0000000..b590885 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Component/Browse/PowerAutomateUrls.cs @@ -0,0 +1,34 @@ +namespace TALXIS.CLI.Features.Environment.Component.Browse; + +/// +/// URL builders for the Power Automate maker portal (make.powerautomate.com). +/// Covers flow editor, flow details, run history, and specific run inspection. +/// +public static class PowerAutomateUrls +{ + private const string Base = "https://make.powerautomate.com"; + + /// Flow base path — editor view (default landing page for a flow). + public static Uri FlowEditor(Guid environmentId, Guid flowId, Guid? solutionId = null) + => solutionId.HasValue + ? new($"{Base}/environments/{environmentId}/solutions/{solutionId}/flows/{flowId}") + : new($"{Base}/environments/{environmentId}/flows/{flowId}"); + + /// Flow details page — shows connection references, owners, and metadata. + public static Uri FlowDetails(Guid environmentId, Guid flowId, Guid? solutionId = null) + => solutionId.HasValue + ? new($"{Base}/environments/{environmentId}/solutions/{solutionId}/flows/{flowId}/details") + : new($"{Base}/environments/{environmentId}/flows/{flowId}/details"); + + /// Flow run history — lists all runs with status. + public static Uri FlowRuns(Guid environmentId, Guid flowId, Guid? solutionId = null) + => solutionId.HasValue + ? new($"{Base}/environments/{environmentId}/solutions/{solutionId}/flows/{flowId}/runs") + : new($"{Base}/environments/{environmentId}/flows/{flowId}/runs"); + + /// Specific flow run — shows the run's step-by-step execution details. + public static Uri FlowRun(Guid environmentId, Guid flowId, string runId, Guid? solutionId = null) + => solutionId.HasValue + ? new($"{Base}/environments/{environmentId}/solutions/{solutionId}/flows/{flowId}/runs/{Uri.EscapeDataString(runId)}") + : new($"{Base}/environments/{environmentId}/flows/{flowId}/runs/{Uri.EscapeDataString(runId)}"); +} diff --git a/src/TALXIS.CLI.Features.Environment/Component/ComponentCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Component/ComponentCliCommand.cs index 94f582f..6e69e4c 100644 --- a/src/TALXIS.CLI.Features.Environment/Component/ComponentCliCommand.cs +++ b/src/TALXIS.CLI.Features.Environment/Component/ComponentCliCommand.cs @@ -5,9 +5,10 @@ namespace TALXIS.CLI.Features.Environment.Component; [CliCommand( Name = "component", Alias = "comp", - Description = "Inspect components independent of a specific solution (layers, dependencies).", + Description = "Inspect and navigate components in the live environment (layers, dependencies, browser editor).", Children = new[] { + typeof(Browse.ComponentBrowseCliCommand), typeof(Layer.ComponentLayerCliCommand), typeof(Dependency.ComponentDependencyCliCommand), }, diff --git a/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyDeleteCheckCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyDeleteCheckCliCommand.cs index e2740cd..42c1cf3 100644 --- a/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyDeleteCheckCliCommand.cs +++ b/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyDeleteCheckCliCommand.cs @@ -4,6 +4,7 @@ using TALXIS.CLI.Core.Contracts.Dataverse; using TALXIS.CLI.Core.DependencyInjection; using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; namespace TALXIS.CLI.Features.Environment.Component.Dependency; @@ -41,13 +42,16 @@ protected override async Task ExecuteAsync() return ExitValidationError; } - var resolver = new ComponentTypeResolver(); - if (!resolver.TryResolveCode(typeName, out var typeCode)) + var def = ComponentDefinitionRegistry.GetByName(typeName); + if (def is null && int.TryParse(typeName, out var parsedCode)) + def = ComponentDefinitionRegistry.GetByType((ComponentType)parsedCode); + if (def is null) { - var known = string.Join(", ", resolver.GetKnownNames().Take(15)); + var known = string.Join(", ", ComponentDefinitionRegistry.GetAll().Select(d => d.Name).Take(15)); Logger.LogError("Unknown component type '{Type}'. Available types: {Known}.", typeName, known); return ExitValidationError; } + var typeCode = (int)def.TypeCode; var service = TxcServices.Get(); var deps = await service.CheckDeleteAsync(Profile, id, typeCode, CancellationToken.None).ConfigureAwait(false); diff --git a/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyListCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyListCliCommand.cs index ae167f8..996edd4 100644 --- a/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyListCliCommand.cs +++ b/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyListCliCommand.cs @@ -4,6 +4,7 @@ using TALXIS.CLI.Core.Contracts.Dataverse; using TALXIS.CLI.Core.DependencyInjection; using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; namespace TALXIS.CLI.Features.Environment.Component.Dependency; @@ -41,13 +42,16 @@ protected override async Task ExecuteAsync() return ExitValidationError; } - var resolver = new ComponentTypeResolver(); - if (!resolver.TryResolveCode(typeName, out var typeCode)) + var def = ComponentDefinitionRegistry.GetByName(typeName); + if (def is null && int.TryParse(typeName, out var parsedCode)) + def = ComponentDefinitionRegistry.GetByType((ComponentType)parsedCode); + if (def is null) { - var known = string.Join(", ", resolver.GetKnownNames().Take(15)); + var known = string.Join(", ", ComponentDefinitionRegistry.GetAll().Select(d => d.Name).Take(15)); Logger.LogError("Unknown component type '{Type}'. Available types: {Known}. Or use an integer code.", typeName, known); return ExitValidationError; } + var typeCode = (int)def.TypeCode; var service = TxcServices.Get(); var deps = await service.GetDependentsAsync(Profile, id, typeCode, CancellationToken.None).ConfigureAwait(false); diff --git a/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyRequiredCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyRequiredCliCommand.cs index 091eee2..fd05824 100644 --- a/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyRequiredCliCommand.cs +++ b/src/TALXIS.CLI.Features.Environment/Component/Dependency/ComponentDependencyRequiredCliCommand.cs @@ -4,6 +4,7 @@ using TALXIS.CLI.Core.Contracts.Dataverse; using TALXIS.CLI.Core.DependencyInjection; using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; namespace TALXIS.CLI.Features.Environment.Component.Dependency; @@ -41,13 +42,16 @@ protected override async Task ExecuteAsync() return ExitValidationError; } - var resolver = new ComponentTypeResolver(); - if (!resolver.TryResolveCode(typeName, out var typeCode)) + var def = ComponentDefinitionRegistry.GetByName(typeName); + if (def is null && int.TryParse(typeName, out var parsedCode)) + def = ComponentDefinitionRegistry.GetByType((ComponentType)parsedCode); + if (def is null) { - var known = string.Join(", ", resolver.GetKnownNames().Take(15)); + var known = string.Join(", ", ComponentDefinitionRegistry.GetAll().Select(d => d.Name).Take(15)); Logger.LogError("Unknown component type '{Type}'. Available types: {Known}.", typeName, known); return ExitValidationError; } + var typeCode = (int)def.TypeCode; var service = TxcServices.Get(); var deps = await service.GetRequiredAsync(Profile, id, typeCode, CancellationToken.None).ConfigureAwait(false); diff --git a/src/TALXIS.CLI.Features.Environment/Component/Dependency/DependencyOutputHelper.cs b/src/TALXIS.CLI.Features.Environment/Component/Dependency/DependencyOutputHelper.cs index 5561671..851e201 100644 --- a/src/TALXIS.CLI.Features.Environment/Component/Dependency/DependencyOutputHelper.cs +++ b/src/TALXIS.CLI.Features.Environment/Component/Dependency/DependencyOutputHelper.cs @@ -1,5 +1,6 @@ using TALXIS.CLI.Core; using TALXIS.CLI.Core.Contracts.Dataverse; +using TALXIS.Platform.Metadata; namespace TALXIS.CLI.Features.Environment.Component.Dependency; @@ -8,8 +9,6 @@ namespace TALXIS.CLI.Features.Environment.Component.Dependency; /// internal static class DependencyOutputHelper { - private static readonly ComponentTypeResolver Resolver = new(); - // OutputWriter usage is intentional — called from text-renderer callbacks. #pragma warning disable TXC003 public static void PrintDependencyTable( @@ -33,8 +32,8 @@ public static void PrintDependencyTable( foreach (var d in rows) { - var depType = Resolver.ResolveName(d.DependentComponentType); - var reqType = Resolver.ResolveName(d.RequiredComponentType); + var depType = ComponentDefinitionRegistry.GetByType((ComponentType)d.DependentComponentType)?.Name ?? d.DependentComponentType.ToString(); + var reqType = ComponentDefinitionRegistry.GetByType((ComponentType)d.RequiredComponentType)?.Name ?? d.RequiredComponentType.ToString(); var depKind = d.DependencyType switch { 1 => "Internal", diff --git a/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentAddCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentAddCliCommand.cs index ab357b6..d1c0757 100644 --- a/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentAddCliCommand.cs +++ b/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentAddCliCommand.cs @@ -4,6 +4,7 @@ using TALXIS.CLI.Core.Contracts.Dataverse; using TALXIS.CLI.Core.DependencyInjection; using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; namespace TALXIS.CLI.Features.Environment.Solution.Component; @@ -39,13 +40,16 @@ protected override async Task ExecuteAsync() return ExitValidationError; } - var resolver = new ComponentTypeResolver(); - if (!resolver.TryResolveCode(Type, out var typeCode)) + var def = ComponentDefinitionRegistry.GetByName(Type); + if (def is null && int.TryParse(Type, out var parsedCode)) + def = ComponentDefinitionRegistry.GetByType((ComponentType)parsedCode); + if (def is null) { - var known = string.Join(", ", resolver.GetKnownNames().Take(15)); + var known = string.Join(", ", ComponentDefinitionRegistry.GetAll().Select(d => d.Name).Take(15)); Logger.LogError("Unknown component type '{Type}'. Available types: {Known}. Or use an integer code.", Type, known); return ExitValidationError; } + var typeCode = (int)def.TypeCode; // Pre-check: reject managed solutions (can't add components to managed) var detailService = TxcServices.Get(); @@ -60,7 +64,7 @@ protected override async Task ExecuteAsync() var service = TxcServices.Get(); await service.AddAsync(Profile, options, CancellationToken.None).ConfigureAwait(false); - var typeName = resolver.ResolveName(typeCode); + var typeName = def.Name; OutputFormatter.WriteData( new { status = "added", solution = SolutionName, componentId = ComponentId, componentType = typeName }, _ => diff --git a/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentListCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentListCliCommand.cs index f824fc0..abe3883 100644 --- a/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentListCliCommand.cs +++ b/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentListCliCommand.cs @@ -4,6 +4,7 @@ using TALXIS.CLI.Core.Contracts.Dataverse; using TALXIS.CLI.Core.DependencyInjection; using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; namespace TALXIS.CLI.Features.Environment.Solution.Component; @@ -33,14 +34,16 @@ protected override async Task ExecuteAsync() int? typeFilter = null; if (!string.IsNullOrWhiteSpace(Type)) { - var resolver = new ComponentTypeResolver(); - if (!resolver.TryResolveCode(Type, out var code)) + var def = ComponentDefinitionRegistry.GetByName(Type); + if (def is null && int.TryParse(Type, out var parsedCode)) + def = ComponentDefinitionRegistry.GetByType((ComponentType)parsedCode); + if (def is null) { - var known = string.Join(", ", resolver.GetKnownNames().Take(15)); - Logger.LogError("Unknown component type '{Type}'. Available types: {Known}. Or use an integer code.", Type, known); + var known = string.Join(", ", ComponentDefinitionRegistry.GetAll().Select(d => d.Name).Take(15)); + Logger.LogError("Unknown component type '{Type}'. Available types: {Known}. Or use an integer code.", Type, known); return ExitValidationError; } - typeFilter = code; + typeFilter = (int)def.TypeCode; } var service = TxcServices.Get(); diff --git a/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentRemoveCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentRemoveCliCommand.cs index bab5fcc..47f8928 100644 --- a/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentRemoveCliCommand.cs +++ b/src/TALXIS.CLI.Features.Environment/Solution/Component/SolutionComponentRemoveCliCommand.cs @@ -5,6 +5,7 @@ using TALXIS.CLI.Core.Contracts.Dataverse; using TALXIS.CLI.Core.DependencyInjection; using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; namespace TALXIS.CLI.Features.Environment.Solution.Component; @@ -37,13 +38,16 @@ protected override async Task ExecuteAsync() return ExitValidationError; } - var resolver = new ComponentTypeResolver(); - if (!resolver.TryResolveCode(Type, out var typeCode)) + var def = ComponentDefinitionRegistry.GetByName(Type); + if (def is null && int.TryParse(Type, out var parsedCode)) + def = ComponentDefinitionRegistry.GetByType((ComponentType)parsedCode); + if (def is null) { - var known = string.Join(", ", resolver.GetKnownNames().Take(15)); + var known = string.Join(", ", ComponentDefinitionRegistry.GetAll().Select(d => d.Name).Take(15)); Logger.LogError("Unknown component type '{Type}'. Available types: {Known}. Or use an integer code.", Type, known); return ExitValidationError; } + var typeCode = (int)def.TypeCode; // Pre-check: reject managed solutions (can't remove components from managed) var detailService = TxcServices.Get(); @@ -58,7 +62,7 @@ protected override async Task ExecuteAsync() var service = TxcServices.Get(); await service.RemoveAsync(Profile, options, CancellationToken.None).ConfigureAwait(false); - var typeName = resolver.ResolveName(typeCode); + var typeName = def.Name; OutputFormatter.WriteData( new { status = "removed", solution = SolutionName, componentId = ComponentId, componentType = typeName }, _ => diff --git a/src/TALXIS.CLI.Features.Environment/Solution/SolutionUninstallCheckCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Solution/SolutionUninstallCheckCliCommand.cs index 2507641..e8696ea 100644 --- a/src/TALXIS.CLI.Features.Environment/Solution/SolutionUninstallCheckCliCommand.cs +++ b/src/TALXIS.CLI.Features.Environment/Solution/SolutionUninstallCheckCliCommand.cs @@ -4,6 +4,7 @@ using TALXIS.CLI.Core.Contracts.Dataverse; using TALXIS.CLI.Core.DependencyInjection; using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; namespace TALXIS.CLI.Features.Environment.Solution; @@ -50,7 +51,6 @@ private void PrintSafe() private void PrintBlocked(IReadOnlyList deps) { - var resolver = new ComponentTypeResolver(); OutputWriter.WriteLine($"Solution '{Name}' has {deps.Count} blocking dependency(ies):\n"); string header = $"{"Required Type",-25} | {"Required ID",-36} | {"Dependent Type",-25} | {"Dependent ID",-36} | Dep.Type"; @@ -59,8 +59,8 @@ private void PrintBlocked(IReadOnlyList deps) foreach (var d in deps) { - var reqType = resolver.ResolveName(d.RequiredComponentType); - var depType = resolver.ResolveName(d.DependentComponentType); + var reqType = ComponentDefinitionRegistry.GetByType((ComponentType)d.RequiredComponentType)?.Name ?? d.RequiredComponentType.ToString(); + var depType = ComponentDefinitionRegistry.GetByType((ComponentType)d.DependentComponentType)?.Name ?? d.DependentComponentType.ToString(); var depKind = d.DependencyType switch { 1 => "Internal", diff --git a/src/TALXIS.CLI.Features.Workspace/ComponentCliCommand.cs b/src/TALXIS.CLI.Features.Workspace/ComponentCliCommand.cs index 731ac0b..8fc8e64 100644 --- a/src/TALXIS.CLI.Features.Workspace/ComponentCliCommand.cs +++ b/src/TALXIS.CLI.Features.Workspace/ComponentCliCommand.cs @@ -2,13 +2,18 @@ namespace TALXIS.CLI.Features.Workspace; +/// +/// Workspace component operations — create and inspect components in the local repository. +/// Component type introspection (list, explain) is at the top level: txc component type list/explain. +/// [CliCommand( - Description = "Create or modify components of your solution", + Description = "Create and inspect components in your local workspace", Name = "component", Alias = "comp", Children = new[] { - typeof(ComponentCreateCliCommand) + typeof(ComponentCreateCliCommand), + typeof(ComponentParameterListCliCommand), }, ShortFormAutoGenerate = CliNameAutoGenerate.None)] public class ComponentCliCommand @@ -17,30 +22,4 @@ public void Run(CliContext context) { context.ShowHelp(); } - - [CliCommand( - Description = "Parameters for a specific component", - Children = new[] { typeof(ComponentParameterListCliCommand) }, - Name = "parameter", - ShortFormAutoGenerate = CliNameAutoGenerate.None)] - public class ComponentParameterCliCommand - { - public void Run(CliContext context) - { - context.ShowHelp(); - } - } - - [CliCommand( - Description = "Types of available components", - Children = new[] { typeof(ComponentTypeListCliCommand), typeof(ComponentTypeExplainCliCommand) }, - Name = "type", - ShortFormAutoGenerate = CliNameAutoGenerate.None)] - public class ComponentTypeCliCommand - { - public void Run(CliContext context) - { - context.ShowHelp(); - } - } } diff --git a/src/TALXIS.CLI.Features.Workspace/ComponentParameterListCliCommand.cs b/src/TALXIS.CLI.Features.Workspace/ComponentParameterListCliCommand.cs index ce45726..d0c0c19 100644 --- a/src/TALXIS.CLI.Features.Workspace/ComponentParameterListCliCommand.cs +++ b/src/TALXIS.CLI.Features.Workspace/ComponentParameterListCliCommand.cs @@ -13,8 +13,8 @@ namespace TALXIS.CLI.Features.Workspace; /// [CliReadOnly] [CliCommand( - Description = "Lists parameters for a specific component", - Name = "list" + Description = "Lists parameters for a specific component template. Use 'txc component type explain ' for type metadata.", + Name = "parameter-list" )] public class ComponentParameterListCliCommand : TxcLeafCommand { diff --git a/src/TALXIS.CLI.Features.Workspace/ComponentTypeExplainCliCommand.cs b/src/TALXIS.CLI.Features.Workspace/ComponentTypeExplainCliCommand.cs deleted file mode 100644 index 026efca..0000000 --- a/src/TALXIS.CLI.Features.Workspace/ComponentTypeExplainCliCommand.cs +++ /dev/null @@ -1,54 +0,0 @@ -using DotMake.CommandLine; -using Microsoft.Extensions.Logging; -using TALXIS.CLI.Logging; -using TALXIS.CLI.Core; -using TALXIS.CLI.Features.Workspace.TemplateEngine; - -namespace TALXIS.CLI.Features.Workspace; - -[CliReadOnly] -[CliCommand( - Description = "Explains a solution component type. Use names returned by 'component type list' command", - Name = "explain" -)] -public class ComponentTypeExplainCliCommand : TxcLeafCommand -{ - protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(ComponentTypeExplainCliCommand)); - - [CliArgument(Description = "Type of the component to explain")] - public required string Type { get; set; } - - protected override async Task ExecuteAsync() - { - if (string.IsNullOrWhiteSpace(Type)) - { - Logger.LogError("Please provide a component type"); - return ExitValidationError; - } - - using var scaffolder = new TemplateInvoker(); - var templates = await scaffolder.ListTemplatesAsync(); - var template = templates?.FirstOrDefault(t => string.Equals(t.Name, Type, StringComparison.OrdinalIgnoreCase) - || t.ShortNameList.Any(sn => string.Equals(sn, Type, StringComparison.OrdinalIgnoreCase))); - - if (template == null) - { - Logger.LogError("Component template {Type} not found", Type); - return ExitValidationError; - } - - var data = new - { - type = template.ShortNameList.FirstOrDefault(), - description = template.Description - }; - - OutputFormatter.WriteData(data, d => - { - OutputWriter.WriteLine($"Type: {d.type}"); - OutputWriter.WriteLine($"Description: {d.description}"); - }); - - return ExitSuccess; - } -} diff --git a/src/TALXIS.CLI.Features.Workspace/ComponentTypeListCliCommand.cs b/src/TALXIS.CLI.Features.Workspace/ComponentTypeListCliCommand.cs deleted file mode 100644 index da7367c..0000000 --- a/src/TALXIS.CLI.Features.Workspace/ComponentTypeListCliCommand.cs +++ /dev/null @@ -1,48 +0,0 @@ -using DotMake.CommandLine; -using Microsoft.Extensions.Logging; -using TALXIS.CLI.Core; -using TALXIS.CLI.Logging; -using TALXIS.CLI.Features.Workspace.TemplateEngine; - -namespace TALXIS.CLI.Features.Workspace; - -[CliReadOnly] -[CliCommand( - Description = "Lists available solution components", - Name = "list" -)] -public class ComponentTypeListCliCommand : TxcLeafCommand -{ - protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(ComponentTypeListCliCommand)); - - protected override async Task ExecuteAsync() - { - using var scaffolder = new TemplateInvoker(); - var templates = await scaffolder.ListTemplatesAsync(); - if (templates == null || !templates.Any()) - { - OutputFormatter.WriteList(Array.Empty().ToList().AsReadOnly(), items => - { - OutputWriter.WriteLine("No components available."); - }); - return ExitSuccess; - } - - var projected = templates.Select(t => new - { - shortName = t.ShortNameList.FirstOrDefault(), - description = t.Description - }).ToList(); - - OutputFormatter.WriteList(projected, items => - { - foreach (var item in items) - { - var desc = string.IsNullOrWhiteSpace(item.description) ? "" : $" - {item.description}"; - OutputWriter.WriteLine($"- {item.shortName}{desc}"); - } - }); - - return ExitSuccess; - } -} diff --git a/src/TALXIS.CLI.Features.Workspace/Metamodel/MetamodelCliCommand.cs b/src/TALXIS.CLI.Features.Workspace/Metamodel/MetamodelCliCommand.cs deleted file mode 100644 index 3079ae0..0000000 --- a/src/TALXIS.CLI.Features.Workspace/Metamodel/MetamodelCliCommand.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Reserved skeleton — intentionally NOT wired into any parent's `Children` array. -// -// Group parent for the future `txc workspace metamodel` family. `metamodel describe` -// and `metamodel list` will expose the metamodel that describes the shape of -// user-authored workspace artifacts — the grammar driving validation, the -// language server, and richer scaffolding beyond the current templating- -// engine path. -// -// Because it is unreachable from `WorkspaceCliCommand.Children`, DotMake will -// not surface it or its children to the CLI and the MCP adapter will not -// surface them as tools. Activating the group is a two-edit change: -// 1. Add `typeof(MetamodelCliCommand)` to `WorkspaceCliCommand.Children`. -// 2. Replace the `NotImplementedException` bodies under Metamodel/ with real -// implementations. -// -// Please read `CONTRIBUTING.md` before changing the shape of this class. - -using DotMake.CommandLine; - -namespace TALXIS.CLI.Features.Workspace.Metamodel; - -[CliCommand( - Description = "Reserved — not yet implemented.", - Name = "metamodel", - Children = new[] - { - typeof(MetamodelDescribeCliCommand), - typeof(MetamodelListCliCommand) - })] -public sealed class MetamodelCliCommand -{ - public void Run(CliContext context) - { - context.ShowHelp(); - } -} diff --git a/src/TALXIS.CLI.Features.Workspace/Metamodel/MetamodelDescribeCliCommand.cs b/src/TALXIS.CLI.Features.Workspace/Metamodel/MetamodelDescribeCliCommand.cs deleted file mode 100644 index ee9aa85..0000000 --- a/src/TALXIS.CLI.Features.Workspace/Metamodel/MetamodelDescribeCliCommand.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable TXC001 // Reserved stub — will inherit TxcLeafCommand when implemented -#pragma warning disable TXC004 // Reserved stub — safety annotation deferred until implemented -// Reserved skeleton — intentionally NOT wired into any parent's `Children` array. -// -// `txc workspace metamodel describe` will render a human-readable description -// of a named metamodel entity (component kind, relationship, validation rule...) -// that governs user-authored workspace artifacts. -// -// See MetamodelCliCommand.cs and CONTRIBUTING.md for the activation procedure -// and the design philosophy that governs this surface. - -using DotMake.CommandLine; - -namespace TALXIS.CLI.Features.Workspace.Metamodel; - -[CliCommand( - Description = "Reserved — not yet implemented.", - Name = "describe")] -public sealed class MetamodelDescribeCliCommand -{ - public Task RunAsync() - { - throw new NotImplementedException( - "`workspace metamodel describe` is reserved for a future metamodel " - + "inspection workflow and is not yet implemented. See CONTRIBUTING.md."); - } -} diff --git a/src/TALXIS.CLI.Features.Workspace/Metamodel/MetamodelListCliCommand.cs b/src/TALXIS.CLI.Features.Workspace/Metamodel/MetamodelListCliCommand.cs deleted file mode 100644 index cb65272..0000000 --- a/src/TALXIS.CLI.Features.Workspace/Metamodel/MetamodelListCliCommand.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable TXC001 // Reserved stub — will inherit TxcLeafCommand when implemented -#pragma warning disable TXC004 // Reserved stub — safety annotation deferred until implemented -// Reserved skeleton — intentionally NOT wired into any parent's `Children` array. -// -// `txc workspace metamodel list` will enumerate metamodel entities (component -// kinds, relationship types, validation rules...) that govern user-authored -// workspace artifacts, with lightweight filtering. -// -// See MetamodelCliCommand.cs and CONTRIBUTING.md for the activation procedure -// and the design philosophy that governs this surface. - -using DotMake.CommandLine; - -namespace TALXIS.CLI.Features.Workspace.Metamodel; - -[CliCommand( - Description = "Reserved — not yet implemented.", - Name = "list")] -public sealed class MetamodelListCliCommand -{ - public Task RunAsync() - { - throw new NotImplementedException( - "`workspace metamodel list` is reserved for a future metamodel " - + "enumeration workflow and is not yet implemented. See CONTRIBUTING.md."); - } -} diff --git a/src/TALXIS.CLI/Component/ComponentCliCommand.cs b/src/TALXIS.CLI/Component/ComponentCliCommand.cs new file mode 100644 index 0000000..96aa3cb --- /dev/null +++ b/src/TALXIS.CLI/Component/ComponentCliCommand.cs @@ -0,0 +1,22 @@ +using DotMake.CommandLine; + +namespace TALXIS.CLI.Component; + +/// +/// Top-level component group — metamodel introspection. +/// Platform knowledge about component types, independent of any workspace or environment. +/// +[CliCommand( + Name = "component", + Alias = "comp", + Description = "Inspect component type definitions, aliases, and metadata.", + Children = new[] { typeof(ComponentTypeCliCommand) }, + ShortFormAutoGenerate = CliNameAutoGenerate.None +)] +public class ComponentCliCommand +{ + public void Run(CliContext context) + { + context.ShowHelp(); + } +} diff --git a/src/TALXIS.CLI/Component/ComponentTypeCliCommand.cs b/src/TALXIS.CLI/Component/ComponentTypeCliCommand.cs new file mode 100644 index 0000000..9dcb184 --- /dev/null +++ b/src/TALXIS.CLI/Component/ComponentTypeCliCommand.cs @@ -0,0 +1,20 @@ +using DotMake.CommandLine; + +namespace TALXIS.CLI.Component; + +/// +/// Component type subgroup — list and explain registered component types. +/// +[CliCommand( + Name = "type", + Description = "Discover available component types, their aliases, and metadata.", + Children = new[] { typeof(ComponentTypeListCliCommand), typeof(ComponentTypeExplainCliCommand) }, + ShortFormAutoGenerate = CliNameAutoGenerate.None +)] +public class ComponentTypeCliCommand +{ + public void Run(CliContext context) + { + context.ShowHelp(); + } +} diff --git a/src/TALXIS.CLI/Component/ComponentTypeExplainCliCommand.cs b/src/TALXIS.CLI/Component/ComponentTypeExplainCliCommand.cs new file mode 100644 index 0000000..3abb615 --- /dev/null +++ b/src/TALXIS.CLI/Component/ComponentTypeExplainCliCommand.cs @@ -0,0 +1,109 @@ +using DotMake.CommandLine; +using Microsoft.Extensions.Logging; +using TALXIS.CLI.Core; +using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; + +namespace TALXIS.CLI.Component; + +/// +/// Explains a specific component type — shows full details. +/// Accepts canonical name, alias, enum name, or integer type code. +/// +[CliReadOnly] +[CliCommand( + Name = "explain", + Description = "Show detailed information about a component type." +)] +public class ComponentTypeExplainCliCommand : TxcLeafCommand +{ + protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(); + + [CliArgument(Description = "Component type name, alias, or integer code (e.g. 'Entity', 'Table', '1').")] + public string Type { get; set; } = null!; + + protected override Task ExecuteAsync() + { + if (string.IsNullOrWhiteSpace(Type)) + { + Logger.LogError("Component type is required. Run 'txc component type list' to see available types."); + return Task.FromResult(ExitValidationError); + } + + var def = ComponentDefinitionRegistry.GetByName(Type); + if (def is null) + { + Logger.LogError("Unknown component type '{Type}'. Run 'txc component type list' to see available types.", Type); + return Task.FromResult(ExitValidationError); + } + + var data = new + { + typeCode = (int)def.TypeCode, + name = def.Name, + aliases = def.Aliases ?? (IReadOnlyList)Array.Empty(), + serializedName = def.SerializedName, + directory = def.Directory, + filePattern = def.FilePattern, + identity = def.Identity, + supportsMerge = def.SupportsMerge, + isMergeable = def.IsMergeable, + isFileBacked = def.IsFileBacked, + hasSubfolders = def.HasSubfolders, + hasParent = def.HasParent, + rootComponent = def.RootComponent, + isCustomizable = def.IsCustomizable, + canBeDeleted = def.CanBeDeleted, + primaryKeyName = def.PrimaryKeyName, + exportKeyAttributes = def.ExportKeyAttributes + }; + + OutputFormatter.WriteData(data, _ => PrintExplanation(def)); + + return Task.FromResult(ExitSuccess); + } + +#pragma warning disable TXC003 + private static void PrintExplanation(ComponentDefinition d) + { + const int labelWidth = -28; + + OutputWriter.WriteLine($"{"Name:",labelWidth}{d.Name}"); + OutputWriter.WriteLine($"{"Type Code:",labelWidth}{(int)d.TypeCode}"); + if (d.Aliases is { Count: > 0 }) + OutputWriter.WriteLine($"{"Aliases:",labelWidth}{string.Join(", ", d.Aliases)}"); + OutputWriter.WriteLine($"{"Serialized Name:",labelWidth}{d.SerializedName}"); + OutputWriter.WriteLine($"{"Directory:",labelWidth}{d.Directory}"); + OutputWriter.WriteLine($"{"File Pattern:",labelWidth}{d.FilePattern}"); + OutputWriter.WriteLine($"{"Identity Strategy:",labelWidth}{d.Identity}"); + + OutputWriter.WriteLine(); + OutputWriter.WriteLine("Behavioral Flags:"); + OutputWriter.WriteLine($" {"Supports Merge:",labelWidth}{BoolStr(d.SupportsMerge)}"); + OutputWriter.WriteLine($" {"Is Mergeable:",labelWidth}{BoolStr(d.IsMergeable)}"); + OutputWriter.WriteLine($" {"Is File-Backed:",labelWidth}{BoolStr(d.IsFileBacked)}"); + OutputWriter.WriteLine($" {"Has Subfolders:",labelWidth}{BoolStr(d.HasSubfolders)}"); + OutputWriter.WriteLine($" {"Is Customizable:",labelWidth}{BoolStr(d.IsCustomizable)}"); + OutputWriter.WriteLine($" {"Can Be Deleted:",labelWidth}{BoolStr(d.CanBeDeleted)}"); + + if (d.HasParent) + { + OutputWriter.WriteLine(); + OutputWriter.WriteLine("Parent-Child:"); + OutputWriter.WriteLine($" {"Has Parent:",labelWidth}true"); + OutputWriter.WriteLine($" {"Root Component:",labelWidth}{d.RootComponent}"); + if (d.GroupParentComponentType.HasValue) + OutputWriter.WriteLine($" {"Group Parent Type:",labelWidth}{d.GroupParentComponentType}"); + if (!string.IsNullOrWhiteSpace(d.GroupParentComponentAttributeName)) + OutputWriter.WriteLine($" {"Group Parent Attr:",labelWidth}{d.GroupParentComponentAttributeName}"); + } + + if (!string.IsNullOrWhiteSpace(d.PrimaryKeyName)) + OutputWriter.WriteLine($"\n{"Primary Key:",labelWidth}{d.PrimaryKeyName}"); + if (!string.IsNullOrWhiteSpace(d.ExportKeyAttributes)) + OutputWriter.WriteLine($"{"Export Key Attrs:",labelWidth}{d.ExportKeyAttributes}"); + } +#pragma warning restore TXC003 + + private static string BoolStr(bool value) => value ? "true" : "false"; +} diff --git a/src/TALXIS.CLI/Component/ComponentTypeListCliCommand.cs b/src/TALXIS.CLI/Component/ComponentTypeListCliCommand.cs new file mode 100644 index 0000000..260d0db --- /dev/null +++ b/src/TALXIS.CLI/Component/ComponentTypeListCliCommand.cs @@ -0,0 +1,90 @@ +using DotMake.CommandLine; +using Microsoft.Extensions.Logging; +using TALXIS.CLI.Core; +using TALXIS.CLI.Logging; +using TALXIS.Platform.Metadata; + +namespace TALXIS.CLI.Component; + +/// +/// Lists all known component types from the . +/// Shows type code, canonical name, aliases, identity strategy, and directory. +/// +[CliReadOnly] +[CliCommand( + Name = "list", + Description = "List all known component types with their aliases and metadata." +)] +public class ComponentTypeListCliCommand : TxcLeafCommand +{ + protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(); + + [CliOption(Name = "--search", Description = "Filter types by substring match on name or alias.", Required = false)] + public string? Search { get; set; } + + protected override Task ExecuteAsync() + { + var allDefs = ComponentDefinitionRegistry.GetAll() + .OrderBy(d => (int)d.TypeCode) + .ToList(); + + if (!string.IsNullOrWhiteSpace(Search)) + { + allDefs = allDefs.Where(d => + d.Name.Contains(Search, StringComparison.OrdinalIgnoreCase) || + (d.Aliases?.Any(a => a.Contains(Search, StringComparison.OrdinalIgnoreCase)) == true)) + .ToList(); + } + + var projected = allDefs.Select(d => new + { + typeCode = (int)d.TypeCode, + name = d.Name, + aliases = d.Aliases ?? (IReadOnlyList)Array.Empty(), + identity = d.Identity, + directory = d.Directory + }).ToList(); + + OutputFormatter.WriteList(projected, items => PrintTypeTable(items)); + + return Task.FromResult(ExitSuccess); + } + +#pragma warning disable TXC003 + private static void PrintTypeTable(IReadOnlyList items) where T : notnull + { + var rows = items.Cast().ToList(); + if (rows.Count == 0) + { + OutputWriter.WriteLine("No component types found."); + return; + } + + // Format aliases as comma-separated for text display + static string FormatAliases(dynamic aliases) + { + var list = (IReadOnlyList)aliases; + return list.Count > 0 ? string.Join(", ", list) : ""; + } + + int codeWidth = 6; + int nameWidth = Math.Clamp(rows.Max(r => ((string)r.name).Length), 10, 35); + int aliasWidth = Math.Clamp(rows.Max(r => FormatAliases(r.aliases).Length), 7, 30); + int identityWidth = 10; + + string header = $"{"Code".PadRight(codeWidth)} | {"Name".PadRight(nameWidth)} | {"Aliases".PadRight(aliasWidth)} | {"Identity".PadRight(identityWidth)} | Directory"; + OutputWriter.WriteLine(header); + OutputWriter.WriteLine(new string('-', header.Length)); + foreach (var r in rows) + { + OutputWriter.WriteLine( + $"{((int)r.typeCode).ToString().PadRight(codeWidth)} | " + + $"{((string)r.name).PadRight(nameWidth)} | " + + $"{FormatAliases(r.aliases).PadRight(aliasWidth)} | " + + $"{r.identity.ToString().PadRight(identityWidth)} | " + + $"{(string)r.directory}"); + } + OutputWriter.WriteLine($"\n{rows.Count} component type(s)."); + } +#pragma warning restore TXC003 +} diff --git a/src/TALXIS.CLI/TxcCliCommand.cs b/src/TALXIS.CLI/TxcCliCommand.cs index 3adf67e..1c705bf 100644 --- a/src/TALXIS.CLI/TxcCliCommand.cs +++ b/src/TALXIS.CLI/TxcCliCommand.cs @@ -4,7 +4,7 @@ namespace TALXIS.CLI; [CliCommand( Description = "Tool for automating development loops in Power Platform", - Children = new[] { typeof(TALXIS.CLI.Features.Data.DataCliCommand), typeof(TALXIS.CLI.Features.Environment.EnvironmentCliCommand), typeof(TALXIS.CLI.Features.Workspace.WorkspaceCliCommand), typeof(TALXIS.CLI.Features.Config.ConfigCliCommand), typeof(TALXIS.CLI.Features.Docs.DocsCliCommand) }, + Children = new[] { typeof(TALXIS.CLI.Features.Data.DataCliCommand), typeof(TALXIS.CLI.Features.Environment.EnvironmentCliCommand), typeof(TALXIS.CLI.Features.Workspace.WorkspaceCliCommand), typeof(TALXIS.CLI.Features.Config.ConfigCliCommand), typeof(TALXIS.CLI.Features.Docs.DocsCliCommand), typeof(TALXIS.CLI.Component.ComponentCliCommand) }, ShortFormAutoGenerate = CliNameAutoGenerate.None )] public class TxcCliCommand diff --git a/tests/TALXIS.CLI.IntegrationTests/CliTests.cs b/tests/TALXIS.CLI.IntegrationTests/CliTests.cs index ae9def5..8ab1758 100644 --- a/tests/TALXIS.CLI.IntegrationTests/CliTests.cs +++ b/tests/TALXIS.CLI.IntegrationTests/CliTests.cs @@ -10,9 +10,9 @@ namespace TALXIS.CLI.IntegrationTests; public class CliTests { [Theory] - [InlineData("workspace component type list")] - [InlineData("workspace component type explain pp-entity")] - [InlineData("workspace component parameter list pp-entity")] + [InlineData("component type list")] + [InlineData("component type explain Entity")] + [InlineData("workspace component parameter-list pp-entity")] [InlineData("environment package import --help")] [InlineData("environment package uninstall --help")] [InlineData("environment solution import --help")] @@ -30,21 +30,21 @@ public async Task Command_ExecutesSuccessfully(string command) } [Fact] - public async Task WorkspaceComponentList_ContainsExpectedComponents() + public async Task ComponentTypeList_ContainsExpectedTypes() { - var output = await CliRunner.RunAsync("workspace component type list"); + var output = await CliRunner.RunAsync("component type list"); - Assert.Contains("pp-entity", output); + Assert.Contains("Entity", output); } [Fact] - public async Task WorkspaceComponentType_ReturnsComponentDetails() + public async Task ComponentTypeExplain_ReturnsTypeDetails() { - var output = await CliRunner.RunAsync("workspace component type explain pp-entity"); + var output = await CliRunner.RunAsync("component type explain Entity"); - // Output is JSON when stdout is redirected (piped) — the new TxcLeafCommand + // Output is JSON when stdout is redirected (piped) — the TxcLeafCommand // base auto-detects format, so integration tests see JSON instead of plain text. - Assert.Contains("pp-entity", output); - Assert.Contains("description", output); + Assert.Contains("Entity", output); + Assert.Contains("Table", output); // alias } } diff --git a/tests/TALXIS.CLI.IntegrationTests/EquivalenceTests.cs b/tests/TALXIS.CLI.IntegrationTests/EquivalenceTests.cs index 85f4c03..ef39b73 100644 --- a/tests/TALXIS.CLI.IntegrationTests/EquivalenceTests.cs +++ b/tests/TALXIS.CLI.IntegrationTests/EquivalenceTests.cs @@ -31,16 +31,16 @@ public static IEnumerable GetTestCases() { yield return new object[] { - "workspace component type list", - "workspace_component_type_list", + "component type list", + "component_type_list", new Dictionary() }; yield return new object[] { - "workspace component type explain pp-entity", - "workspace_component_type_explain", - new Dictionary { { "Type", "pp-entity" } } + "component type explain Entity", + "component_type_explain", + new Dictionary { { "Type", "Entity" } } }; yield return new object[] @@ -52,8 +52,8 @@ public static IEnumerable GetTestCases() yield return new object[] { - "workspace component parameter list pp-entity", - "workspace_component_parameter_list", + "workspace component parameter-list pp-entity", + "workspace_component_parameter-list", new Dictionary { { "ShortName", "pp-entity" } } }; } diff --git a/tests/TALXIS.CLI.IntegrationTests/McpTests.cs b/tests/TALXIS.CLI.IntegrationTests/McpTests.cs index 602adde..9043f39 100644 --- a/tests/TALXIS.CLI.IntegrationTests/McpTests.cs +++ b/tests/TALXIS.CLI.IntegrationTests/McpTests.cs @@ -33,7 +33,7 @@ public async Task ListTools_ReturnsAlwaysOnTools() public async Task ExecuteOperation_WorkspaceComponentList_ReturnsValidResponse() { var client = await McpTestClient.InstanceAsync; - var args = new Dictionary { { "operation", "workspace_component_type_list" } }; + var args = new Dictionary { { "operation", "component_type_list" } }; var result = await client.CallToolAsync("execute_operation", args); @@ -49,18 +49,17 @@ public async Task ExecuteOperation_WorkspaceComponentExplain_ReturnsComponentDet // First verify the tool exists in the catalog by listing component types. // This also triggers template package auto-installation if needed. - var listArgs = new Dictionary { { "operation", "workspace_component_type_list" } }; + var listArgs = new Dictionary { { "operation", "component_type_list" } }; var listResult = await client.CallToolAsync("execute_operation", listArgs); - // If component type list returned empty or error, templates aren't available — skip + // If component type list returned empty or error, registry isn't available — skip var listText = listResult.Content?.OfType().FirstOrDefault()?.Text ?? ""; if (listResult.IsError == true || listText == "[]" || string.IsNullOrWhiteSpace(listText)) { - // Template package not available on this runner return; } - var args = new Dictionary { { "operation", "workspace_component_type_explain" }, { "arguments", "{\"Type\": \"pp-entity\"}" } }; + var args = new Dictionary { { "operation", "component_type_explain" }, { "arguments", "{\"Type\": \"Entity\"}" } }; var result = await client.CallToolAsync("execute_operation", args); Assert.NotNull(result.Content); @@ -74,7 +73,7 @@ public async Task ExecuteOperation_WorkspaceComponentExplain_ReturnsComponentDet if (result.Content[0] is TextContentBlock textBlock) { - Assert.Contains("pp-entity", textBlock.Text); + Assert.Contains("Entity", textBlock.Text); } } }