File: AtsTypeScriptCodeGenerator.cs
Web Access
Project: src\src\Aspire.Hosting.CodeGeneration.TypeScript\Aspire.Hosting.CodeGeneration.TypeScript.csproj (Aspire.Hosting.CodeGeneration.TypeScript)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Reflection;
using Aspire.Hosting.Ats;
 
namespace Aspire.Hosting.CodeGeneration.TypeScript;
 
/// <summary>
/// Represents a builder class to be generated with its capabilities.
/// Internal type replacing BuilderModel - used only within the generator.
/// </summary>
internal sealed class BuilderModel
{
    public required string TypeId { get; init; }
    public required string BuilderClassName { get; init; }
    public required List<AtsCapabilityInfo> Capabilities { get; init; }
    public bool IsInterface { get; init; }
    public AtsTypeRef? TargetType { get; init; }
}
 
/// <summary>
/// Generates a TypeScript SDK using the ATS (Aspire Type System) capability-based API.
/// Produces typed builder classes with fluent methods that use invokeCapability().
/// </summary>
/// <remarks>
/// <para>
/// <b>ATS to TypeScript Type Mapping</b>
/// </para>
/// <para>
/// The generator maps ATS types to TypeScript types according to the following rules:
/// </para>
/// <para>
/// <b>Primitive Types:</b>
/// <list type="table">
///   <listheader>
///     <term>ATS Type</term>
///     <description>TypeScript Type</description>
///   </listheader>
///   <item><term><c>string</c></term><description><c>string</c></description></item>
///   <item><term><c>number</c></term><description><c>number</c></description></item>
///   <item><term><c>boolean</c></term><description><c>boolean</c></description></item>
///   <item><term><c>any</c></term><description><c>unknown</c></description></item>
/// </list>
/// </para>
/// <para>
/// <b>Handle Types:</b>
/// Type IDs use the format <c>{AssemblyName}/{TypeName}</c>.
/// <list type="table">
///   <listheader>
///     <term>ATS Type ID</term>
///     <description>TypeScript Type</description>
///   </listheader>
///   <item><term><c>Aspire.Hosting/IDistributedApplicationBuilder</c></term><description><c>BuilderHandle</c></description></item>
///   <item><term><c>Aspire.Hosting/DistributedApplication</c></term><description><c>ApplicationHandle</c></description></item>
///   <item><term><c>Aspire.Hosting/DistributedApplicationExecutionContext</c></term><description><c>ExecutionContextHandle</c></description></item>
///   <item><term><c>Aspire.Hosting.Redis/RedisResource</c></term><description><c>RedisResourceBuilderHandle</c></description></item>
///   <item><term><c>Aspire.Hosting/ContainerResource</c></term><description><c>ContainerResourceBuilderHandle</c></description></item>
///   <item><term><c>Aspire.Hosting.ApplicationModel/IResource</c></term><description><c>IResourceHandle</c></description></item>
/// </list>
/// </para>
/// <para>
/// <b>Handle Type Naming Rules:</b>
/// <list type="bullet">
///   <item><description>Core types: Use type name + "Handle"</description></item>
///   <item><description>Interface types: Use interface name + "Handle" (keep the I prefix)</description></item>
///   <item><description>Resource types: Use type name + "BuilderHandle"</description></item>
/// </list>
/// </para>
/// <para>
/// <b>Special Types:</b>
/// <list type="table">
///   <listheader>
///     <term>ATS Type</term>
///     <description>TypeScript Type</description>
///   </listheader>
///   <item><term><c>callback</c></term><description><c>(context: EnvironmentContextHandle) =&gt; Promise&lt;void&gt;</c></description></item>
///   <item><term><c>T[]</c> (array)</term><description><c>T[]</c> (array of mapped type)</description></item>
/// </list>
/// </para>
/// <para>
/// <b>Builder Class Generation:</b>
/// <list type="bullet">
///   <item><description><c>Aspire.Hosting.Redis/RedisResource</c><c>RedisResourceBuilder</c> class with <c>RedisResourceBuilderPromise</c> thenable wrapper</description></item>
///   <item><description><c>Aspire.Hosting.ApplicationModel/IResource</c><c>ResourceBuilderBase</c> abstract class (interface types get "BuilderBase" suffix)</description></item>
///   <item><description>Concrete builders extend interface builders based on type hierarchy</description></item>
/// </list>
/// </para>
/// <para>
/// <b>Method Naming:</b>
/// <list type="bullet">
///   <item><description>Derived from capability ID: <c>Aspire.Hosting.Redis/addRedis</c><c>addRedis</c></description></item>
///   <item><description>Can be overridden via <c>[AspireExport(MethodName = "...")]</c></description></item>
///   <item><description>TypeScript uses camelCase (the canonical form from capability IDs)</description></item>
/// </list>
/// </para>
/// </remarks>
public sealed class AtsTypeScriptCodeGenerator : ICodeGenerator
{
    private TextWriter _writer = null!;
 
    // Mapping of typeId -> wrapper class name for all generated wrapper types
    // Used to resolve parameter types to wrapper classes instead of handle types
    private readonly Dictionary<string, string> _wrapperClassNames = new(StringComparer.Ordinal);
 
    // Set of type IDs that have Promise wrappers (types with chainable methods)
    // Used to determine return types for methods
    private readonly HashSet<string> _typesWithPromiseWrappers = new(StringComparer.Ordinal);
 
    // Set of generated options interfaces to avoid duplicates
    private readonly HashSet<string> _generatedOptionsInterfaces = new(StringComparer.Ordinal);
 
    // Collected options interfaces to generate (interface name -> list of optional params)
    private readonly Dictionary<string, List<AtsParameterInfo>> _optionsInterfacesToGenerate = new(StringComparer.Ordinal);
 
    // Mapping of enum type IDs to TypeScript enum names
    private readonly Dictionary<string, string> _enumTypeNames = new(StringComparer.Ordinal);
 
    /// <summary>
    /// Checks if an AtsTypeRef represents a handle type.
    /// </summary>
    private static bool IsHandleType(AtsTypeRef? typeRef) =>
        typeRef != null && typeRef.Category == AtsTypeCategory.Handle;
 
    /// <summary>
    /// Maps an AtsTypeRef to a TypeScript type using category-based dispatch.
    /// This is the preferred method - uses type metadata rather than string parsing.
    /// </summary>
    private string MapTypeRefToTypeScript(AtsTypeRef? typeRef)
    {
        if (typeRef == null)
        {
            return "unknown";
        }
 
        // Check for wrapper class first (handles custom types like ReferenceExpression)
        if (_wrapperClassNames.TryGetValue(typeRef.TypeId, out var wrapperClassName))
        {
            return wrapperClassName;
        }
 
        return typeRef.Category switch
        {
            AtsTypeCategory.Primitive => MapPrimitiveType(typeRef.TypeId),
            AtsTypeCategory.Enum => MapEnumType(typeRef.TypeId),
            AtsTypeCategory.Handle => GetWrapperOrHandleName(typeRef.TypeId),
            AtsTypeCategory.Dto => GetDtoInterfaceName(typeRef.TypeId),
            AtsTypeCategory.Callback => "Function",  // Callbacks handled separately with full signature
            AtsTypeCategory.Array => $"{MapTypeRefToTypeScript(typeRef.ElementType)}[]",
            AtsTypeCategory.List => $"AspireList<{MapTypeRefToTypeScript(typeRef.ElementType)}>",
            AtsTypeCategory.Dict => typeRef.IsReadOnly
                ? $"Record<{MapTypeRefToTypeScript(typeRef.KeyType)}, {MapTypeRefToTypeScript(typeRef.ValueType)}>"
                : $"AspireDict<{MapTypeRefToTypeScript(typeRef.KeyType)}, {MapTypeRefToTypeScript(typeRef.ValueType)}>",
            AtsTypeCategory.Union => MapUnionTypeToTypeScript(typeRef),
            AtsTypeCategory.Unknown => "any",  // Unknown types use 'any' since they're not in the ATS universe
            _ => "any"  // Fallback for any unhandled categories
        };
    }
 
    /// <summary>
    /// Maps primitive type IDs to TypeScript types.
    /// </summary>
    private static string MapPrimitiveType(string typeId) => typeId switch
    {
        AtsConstants.String or AtsConstants.Char => "string",
        AtsConstants.Number => "number",
        AtsConstants.Boolean => "boolean",
        AtsConstants.Void => "void",
        AtsConstants.Any => "any",
        AtsConstants.DateTime or AtsConstants.DateTimeOffset or
        AtsConstants.DateOnly or AtsConstants.TimeOnly => "string",
        AtsConstants.TimeSpan => "number",
        AtsConstants.Guid or AtsConstants.Uri => "string",
        AtsConstants.CancellationToken => "AbortSignal",
        _ => typeId
    };
 
    /// <summary>
    /// Maps an enum type ID to the generated TypeScript enum name.
    /// Throws if the enum type wasn't collected during scanning.
    /// </summary>
    private string MapEnumType(string typeId)
    {
        if (!_enumTypeNames.TryGetValue(typeId, out var enumName))
        {
            throw new InvalidOperationException(
                $"Enum type '{typeId}' was not found in the scanned enum types. " +
                $"This indicates the enum type was not discovered during assembly scanning.");
        }
        return enumName;
    }
 
    /// <summary>
    /// Maps a union type to TypeScript union syntax (T1 | T2 | ...).
    /// </summary>
    private string MapUnionTypeToTypeScript(AtsTypeRef typeRef)
    {
        if (typeRef.UnionTypes == null || typeRef.UnionTypes.Count == 0)
        {
            return "unknown";
        }
 
        var memberTypes = typeRef.UnionTypes
            .Select(MapTypeRefToTypeScript)
            .Distinct();
 
        return string.Join(" | ", memberTypes);
    }
 
    /// <summary>
    /// Gets the wrapper class name or handle type name for a handle type ID.
    /// Prefers wrapper class if one exists, otherwise generates a handle type name.
    /// </summary>
    private string GetWrapperOrHandleName(string typeId)
    {
        if (_wrapperClassNames.TryGetValue(typeId, out var wrapperClassName))
        {
            return wrapperClassName;
        }
        return GetHandleTypeName(typeId);
    }
 
    /// <summary>
    /// Gets a TypeScript interface name for a DTO type.
    /// </summary>
    private static string GetDtoInterfaceName(string typeId)
    {
        // Extract simple type name and use as interface name
        var simpleTypeName = ExtractSimpleTypeName(typeId);
        return simpleTypeName;
    }
 
    /// <summary>
    /// Maps a parameter to its TypeScript type, handling callbacks specially.
    /// For interface handle types, generates union types to accept both handles and wrapper classes.
    /// </summary>
    private string MapParameterToTypeScript(AtsParameterInfo param)
    {
        if (param.IsCallback)
        {
            return GenerateCallbackTypeSignature(param.CallbackParameters, param.CallbackReturnType);
        }
 
        var baseType = MapTypeRefToTypeScript(param.Type);
 
        // For interface handle types, use ResourceBuilderBase as the parameter type
        // All wrapper classes extend ResourceBuilderBase and have toJSON() for serialization
        if (IsInterfaceHandleType(param.Type))
        {
            return "ResourceBuilderBase";
        }
 
        return baseType;
    }
 
    /// <summary>
    /// Checks if a type reference is an interface handle type.
    /// Interface handles need union types to accept wrapper classes.
    /// </summary>
    private static bool IsInterfaceHandleType(AtsTypeRef? typeRef)
    {
        if (typeRef == null)
        {
            return false;
        }
        return typeRef.Category == AtsTypeCategory.Handle && typeRef.IsInterface;
    }
 
    /// <summary>
    /// Gets the TypeId from a capability's return type.
    /// </summary>
    private static string? GetReturnTypeId(AtsCapabilityInfo capability) => capability.ReturnType?.TypeId;
 
    /// <inheritdoc />
    public string Language => "TypeScript";
 
    /// <inheritdoc />
    public Dictionary<string, string> GenerateDistributedApplication(AtsContext context)
    {
        var files = new Dictionary<string, string>();
 
        // Add embedded resource files (transport.ts, base.ts)
        files["transport.ts"] = GetEmbeddedResource("transport.ts");
        files["base.ts"] = GetEmbeddedResource("base.ts");
 
        // Generate the capability-based aspire.ts SDK
        files["aspire.ts"] = GenerateAspireSdk(context);
 
        return files;
    }
 
    private static string GetEmbeddedResource(string name)
    {
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = $"Aspire.Hosting.CodeGeneration.TypeScript.Resources.{name}";
 
        using var stream = assembly.GetManifestResourceStream(resourceName)
            ?? throw new InvalidOperationException($"Embedded resource '{name}' not found.");
        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
 
    /// <summary>
    /// Gets a valid TypeScript method name from a capability method name.
    /// Handles dotted names like "EnvironmentContext.resource" by extracting just the final part.
    /// </summary>
    private static string GetTypeScriptMethodName(string methodName)
    {
        var dotIndex = methodName.LastIndexOf('.');
        return dotIndex >= 0 ? methodName[(dotIndex + 1)..] : methodName;
    }
 
    /// <summary>
    /// Generates the aspire.ts SDK file with capability-based API.
    /// </summary>
    private string GenerateAspireSdk(AtsContext context)
    {
        using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
        _writer = stringWriter;
 
        // Header
        WriteLine("""
            // aspire.ts - Capability-based Aspire SDK
            // This SDK uses the ATS (Aspire Type System) capability API.
            // Capabilities are endpoints like 'Aspire.Hosting/createBuilder'.
            //
            // GENERATED CODE - DO NOT EDIT
 
            import {
                AspireClient as AspireClientRpc,
                Handle,
                MarshalledHandle,
                CapabilityError,
                registerCallback,
                wrapIfHandle,
                registerHandleWrapper
            } from './transport.js';
 
            import {
                ResourceBuilderBase,
                ReferenceExpression,
                refExpr,
                AspireDict,
                AspireList
            } from './base.js';
            """);
        WriteLine();
 
        var capabilities = context.Capabilities;
        var dtoTypes = context.DtoTypes;
        var enumTypes = context.EnumTypes;
 
        // Get builder models (flattened - each builder has all its applicable capabilities)
        var allBuilders = CreateBuilderModels(capabilities);
        var entryPoints = GetEntryPointCapabilities(capabilities);
 
        // All builders (no special filtering)
        var builders = allBuilders;
 
        // Entry point methods that don't extend any type go on AspireClient
        var clientMethods = entryPoints
            .Where(c => string.IsNullOrEmpty(c.TargetTypeId))
            .ToList();
 
        // Collect all unique type IDs for handle type aliases
        // Exclude DTO types - they have their own interfaces, not handle aliases
        var dtoTypeIds = new HashSet<string>(dtoTypes.Select(d => d.TypeId));
        var typeIds = new HashSet<string>();
        foreach (var cap in capabilities)
        {
            if (!string.IsNullOrEmpty(cap.TargetTypeId) && !dtoTypeIds.Contains(cap.TargetTypeId))
            {
                typeIds.Add(cap.TargetTypeId);
            }
            if (IsHandleType(cap.ReturnType) && !dtoTypeIds.Contains(cap.ReturnType!.TypeId))
            {
                typeIds.Add(GetReturnTypeId(cap)!);
            }
            // Add parameter type IDs (for types like IResourceBuilder<IResource>)
            foreach (var param in cap.Parameters)
            {
                if (IsHandleType(param.Type) && !dtoTypeIds.Contains(param.Type!.TypeId))
                {
                    typeIds.Add(param.Type!.TypeId);
                }
                // Also collect callback parameter types
                if (param.IsCallback && param.CallbackParameters != null)
                {
                    foreach (var cbParam in param.CallbackParameters)
                    {
                        if (IsHandleType(cbParam.Type) && !dtoTypeIds.Contains(cbParam.Type.TypeId))
                        {
                            typeIds.Add(cbParam.Type.TypeId);
                        }
                    }
                }
            }
        }
 
        // Generate handle type aliases
        GenerateHandleTypeAliases(typeIds);
 
        // Generate enum types
        GenerateEnumTypes(enumTypes);
 
        // Generate DTO interfaces
        GenerateDtoInterfaces(dtoTypes);
 
        // Separate builders into categories:
        // 1. Resource builders: IResource*, ContainerResource, etc.
        // 2. Type classes: everything else (context types, wrapper types)
        var resourceBuilders = builders.Where(b => b.TargetType?.IsResourceBuilder == true).ToList();
        var typeClasses = builders.Where(b => b.TargetType?.IsResourceBuilder != true).ToList();
 
        // Build wrapper class name mapping for type resolution BEFORE generating options interfaces
        // This allows parameter types to use wrapper class names instead of handle types
        _wrapperClassNames.Clear();
        _typesWithPromiseWrappers.Clear();
        _generatedOptionsInterfaces.Clear();
        _optionsInterfacesToGenerate.Clear();
 
        foreach (var builder in resourceBuilders)
        {
            _wrapperClassNames[builder.TypeId] = builder.BuilderClassName;
            // All resource builders get Promise wrappers
            _typesWithPromiseWrappers.Add(builder.TypeId);
        }
        foreach (var typeClass in typeClasses)
        {
            _wrapperClassNames[typeClass.TypeId] = DeriveClassName(typeClass.TypeId);
            // Type classes with methods get Promise wrappers
            if (HasChainableMethods(typeClass))
            {
                _typesWithPromiseWrappers.Add(typeClass.TypeId);
            }
        }
        // Add ReferenceExpression (defined in base.ts, not generated)
        _wrapperClassNames[AtsConstants.ReferenceExpressionTypeId] = "ReferenceExpression";
 
        // Pre-scan all capabilities to collect options interfaces
        // This must happen AFTER wrapper class names are populated so types resolve correctly
        foreach (var builder in builders)
        {
            foreach (var cap in builder.Capabilities)
            {
                var (_, optionalParams) = SeparateParameters(cap.Parameters);
                if (optionalParams.Count > 0)
                {
                    RegisterOptionsInterface(cap.MethodName, optionalParams);
                }
            }
        }
 
        // Generate collected options interfaces
        GenerateOptionsInterfaces();
 
        // Generate type classes (context types and wrapper types)
        foreach (var typeClass in typeClasses)
        {
            GenerateTypeClass(typeClass);
        }
 
        // Generate resource builder classes
        foreach (var builder in resourceBuilders)
        {
            GenerateBuilderClass(builder);
        }
 
        // Generate AspireClient with remaining entry point methods
        GenerateAspireClient(clientMethods);
 
        // Generate connection helper
        GenerateConnectionHelper();
 
        // Generate global error handling
        GenerateGlobalErrorHandling();
 
        // Generate handle wrapper registrations (after all classes are defined)
        GenerateHandleWrapperRegistrations(typeClasses, resourceBuilders);
 
        return stringWriter.ToString();
    }
 
    private void WriteLine(string? text = null)
    {
        if (text != null)
        {
            _writer.WriteLine(text);
        }
        else
        {
            _writer.WriteLine();
        }
    }
 
    private void Write(string text)
    {
        _writer.Write(text);
    }
 
    private void GenerateHandleTypeAliases(HashSet<string> typeIds)
    {
        WriteLine("// ============================================================================");
        WriteLine("// Handle Type Aliases (Internal - not exported to users)");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var typeId in typeIds.OrderBy(t => t))
        {
            var handleName = GetHandleTypeName(typeId);
            var description = GetTypeDescription(typeId);
            WriteLine($"/** {description} */");
            // Internal type alias - not exported (users work with wrapper classes)
            WriteLine($"type {handleName} = Handle<'{typeId}'>;");
            WriteLine();
        }
    }
 
    /// <summary>
    /// Generates TypeScript enums from discovered enum types.
    /// </summary>
    private void GenerateEnumTypes(IReadOnlyList<AtsEnumTypeInfo> enumTypes)
    {
        if (enumTypes.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// Enum Types");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var enumType in enumTypes.OrderBy(e => e.Name))
        {
            // Track enum name for type mapping
            _enumTypeNames[enumType.TypeId] = enumType.Name;
 
            WriteLine($"/** Enum type for {enumType.Name} */");
            WriteLine($"export enum {enumType.Name} {{");
 
            foreach (var value in enumType.Values)
            {
                // Enums serialize as strings in JSON
                WriteLine($"    {value} = \"{value}\",");
            }
 
            WriteLine("}");
            WriteLine();
        }
    }
 
    /// <summary>
    /// Generates TypeScript interfaces for DTO types marked with [AspireDto].
    /// </summary>
    private void GenerateDtoInterfaces(IReadOnlyList<AtsDtoTypeInfo> dtoTypes)
    {
        if (dtoTypes.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// DTO Interfaces");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var dto in dtoTypes.OrderBy(d => d.Name))
        {
            var interfaceName = GetDtoInterfaceName(dto.TypeId);
 
            WriteLine($"/** DTO interface for {dto.Name} */");
            WriteLine($"export interface {interfaceName} {{");
 
            foreach (var prop in dto.Properties)
            {
                var tsType = MapTypeRefToTypeScript(prop.Type);
                // All DTO properties are optional in TypeScript to allow partial objects
                // Convert PascalCase to camelCase for TypeScript
                var propName = ToCamelCase(prop.Name);
                WriteLine($"    {propName}?: {tsType};");
            }
 
            WriteLine("}");
            WriteLine();
        }
    }
 
    /// <summary>
    /// Converts a PascalCase name to camelCase.
    /// </summary>
    private static string ToCamelCase(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            return name;
        }
        if (char.IsLower(name[0]))
        {
            return name;
        }
        return char.ToLowerInvariant(name[0]) + name[1..];
    }
 
    /// <summary>
    /// Converts a camelCase name to PascalCase.
    /// </summary>
    private static string ToPascalCase(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            return name;
        }
        if (char.IsUpper(name[0]))
        {
            return name;
        }
        return char.ToUpperInvariant(name[0]) + name[1..];
    }
 
    /// <summary>
    /// Gets the options interface name for a method.
    /// Strips any type prefix (e.g., "TypeName.methodName" -> "MethodName").
    /// </summary>
    private static string GetOptionsInterfaceName(string methodName)
    {
        // Strip type prefix if present (e.g., "EndpointReference.getExpression" -> "getExpression")
        var simpleName = methodName.Contains('.')
            ? methodName[(methodName.LastIndexOf('.') + 1)..]
            : methodName;
        return $"{ToPascalCase(simpleName)}Options";
    }
 
    /// <summary>
    /// Separates parameters into required and optional lists.
    /// Required = not optional and not nullable.
    /// </summary>
    private static (List<AtsParameterInfo> Required, List<AtsParameterInfo> Optional) SeparateParameters(
        IEnumerable<AtsParameterInfo> parameters)
    {
        var required = new List<AtsParameterInfo>();
        var optional = new List<AtsParameterInfo>();
 
        foreach (var param in parameters)
        {
            if (param.IsOptional || param.IsNullable)
            {
                optional.Add(param);
            }
            else
            {
                required.Add(param);
            }
        }
 
        return (required, optional);
    }
 
    /// <summary>
    /// Registers an options interface to be generated later.
    /// Uses method name to create the interface name.
    /// </summary>
    private void RegisterOptionsInterface(string methodName, List<AtsParameterInfo> optionalParams)
    {
        if (optionalParams.Count == 0)
        {
            return;
        }
 
        var interfaceName = GetOptionsInterfaceName(methodName);
        if (_generatedOptionsInterfaces.Add(interfaceName))
        {
            _optionsInterfacesToGenerate[interfaceName] = optionalParams;
        }
    }
 
    /// <summary>
    /// Generates all collected options interfaces.
    /// </summary>
    private void GenerateOptionsInterfaces()
    {
        if (_optionsInterfacesToGenerate.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// Options Interfaces");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var (interfaceName, optionalParams) in _optionsInterfacesToGenerate.OrderBy(kvp => kvp.Key))
        {
            WriteLine($"export interface {interfaceName} {{");
            foreach (var param in optionalParams)
            {
                var tsType = MapParameterToTypeScript(param);
                WriteLine($"    {param.Name}?: {tsType};");
            }
            WriteLine("}");
            WriteLine();
        }
    }
 
    private static string GetTypeDescription(string typeId)
    {
        var typeName = ExtractSimpleTypeName(typeId);
        return $"Handle to {typeName}";
    }
 
    private void GenerateBuilderClass(BuilderModel builder)
    {
        WriteLine("// ============================================================================");
        WriteLine($"// {builder.BuilderClassName}");
        WriteLine("// ============================================================================");
        WriteLine();
 
        var handleType = GetHandleTypeName(builder.TypeId);
 
        // Generate builder class extending ResourceBuilderBase
        WriteLine($"export class {builder.BuilderClassName} extends ResourceBuilderBase<{handleType}> {{");
 
        // Constructor
        WriteLine($"    constructor(handle: {handleType}, client: AspireClientRpc) {{");
        WriteLine($"        super(handle, client);");
        WriteLine("    }");
        WriteLine();
 
        // Generate internal methods and public fluent methods
        // Capabilities are already flattened - no need to collect from parents
        // Filter out property getters and setters - they are not methods
        foreach (var capability in builder.Capabilities.Where(c =>
            c.CapabilityKind != AtsCapabilityKind.PropertyGetter &&
            c.CapabilityKind != AtsCapabilityKind.PropertySetter))
        {
            GenerateBuilderMethod(builder, capability);
        }
 
        WriteLine("}");
        WriteLine();
 
        // Generate thenable wrapper class
        GenerateThenableClass(builder);
    }
 
    private void GenerateBuilderMethod(BuilderModel builder, AtsCapabilityInfo capability)
    {
        var methodName = capability.MethodName;
        var internalMethodName = $"_{methodName}Internal";
 
        // Separate required and optional parameters
        var (requiredParams, optionalParams) = SeparateParameters(capability.Parameters);
        var hasOptionals = optionalParams.Count > 0;
        var optionsInterfaceName = GetOptionsInterfaceName(methodName);
 
        // Build parameter list for public method
        var publicParamDefs = new List<string>();
        foreach (var param in requiredParams)
        {
            var tsType = MapParameterToTypeScript(param);
            publicParamDefs.Add($"{param.Name}: {tsType}");
        }
        if (hasOptionals)
        {
            publicParamDefs.Add($"options?: {optionsInterfaceName}");
        }
        var publicParamsString = string.Join(", ", publicParamDefs);
 
        // Build parameter list for internal method (all params positional for callback registration)
        var internalParamDefs = new List<string>();
        foreach (var param in capability.Parameters)
        {
            var tsType = MapParameterToTypeScript(param);
            var optional = param.IsOptional || param.IsNullable ? "?" : "";
            internalParamDefs.Add($"{param.Name}{optional}: {tsType}");
        }
        var internalParamsString = string.Join(", ", internalParamDefs);
 
        // Use the actual target parameter name from the capability (e.g., "resource" for withReference)
        var targetParamName = capability.TargetParameterName ?? "builder";
 
        // Determine return type - use the builder's own type for fluent methods
        var returnHandle = capability.ReturnsBuilder
            ? GetHandleTypeName(builder.TypeId)
            : "void";
        var returnsBuilder = capability.ReturnsBuilder;
 
        // Check if this method returns a non-builder, non-void type (e.g., getEndpoint returns EndpointReference)
        var hasNonBuilderReturn = !returnsBuilder && capability.ReturnType != null;
        if (hasNonBuilderReturn)
        {
            // Generate a simple async method that returns the actual type
            var returnType = MapTypeRefToTypeScript(capability.ReturnType);
 
            if (!string.IsNullOrEmpty(capability.Description))
            {
                WriteLine($"    /** {capability.Description} */");
            }
            Write($"    async {methodName}(");
            Write(publicParamsString);
            WriteLine($"): Promise<{returnType}> {{");
 
            // Extract optional params from options object
            foreach (var param in optionalParams)
            {
                WriteLine($"        const {param.Name} = options?.{param.Name};");
            }
 
            // Handle callback registration if any
            var callbackParams2 = capability.Parameters.Where(p => p.IsCallback).ToList();
            foreach (var callbackParam in callbackParams2)
            {
                GenerateCallbackRegistration(callbackParam);
            }
 
            // Handle cancellation token registration if any
            var cancellationParams2 = capability.Parameters.Where(IsCancellationToken).ToList();
            foreach (var ctParam in cancellationParams2)
            {
                GenerateCancellationRegistration(ctParam);
            }
 
            // Build args object with conditional inclusion
            GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams, cancellationParams2);
 
            WriteLine($"        return await this._client.invokeCapability<{returnType}>(");
            WriteLine($"            '{capability.CapabilityId}',");
            WriteLine($"            rpcArgs");
            WriteLine("        );");
            WriteLine("    }");
            WriteLine();
            return;
        }
 
        // Generate internal async method for fluent builder methods
        WriteLine($"    /** @internal */");
        Write($"    async {internalMethodName}(");
        Write(internalParamsString);
        Write($"): Promise<{builder.BuilderClassName}> {{");
        WriteLine();
 
        // Handle callback registration if any
        var callbackParams = capability.Parameters.Where(p => p.IsCallback).ToList();
        foreach (var callbackParam in callbackParams)
        {
            GenerateCallbackRegistration(callbackParam);
        }
 
        // Handle cancellation token registration if any
        var cancellationParams = capability.Parameters.Where(IsCancellationToken).ToList();
        foreach (var ctParam in cancellationParams)
        {
            GenerateCancellationRegistration(ctParam);
        }
 
        // Build args object with conditional inclusion
        GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams, cancellationParams);
 
        if (returnsBuilder)
        {
            WriteLine($"        const result = await this._client.invokeCapability<{returnHandle}>(");
            WriteLine($"            '{capability.CapabilityId}',");
            WriteLine($"            rpcArgs");
            WriteLine("        );");
            WriteLine($"        return new {builder.BuilderClassName}(result, this._client);");
        }
        else
        {
            WriteLine($"        await this._client.invokeCapability<void>(");
            WriteLine($"            '{capability.CapabilityId}',");
            WriteLine($"            rpcArgs");
            WriteLine("        );");
            WriteLine($"        return this;");
        }
        WriteLine("    }");
        WriteLine();
 
        // Generate public fluent method (returns thenable wrapper)
        if (!string.IsNullOrEmpty(capability.Description))
        {
            WriteLine($"    /** {capability.Description} */");
        }
        var promiseClass = $"{builder.BuilderClassName}Promise";
        Write($"    {methodName}(");
        Write(publicParamsString);
        Write($"): {promiseClass} {{");
        WriteLine();
 
        // Extract optional params from options object and forward to internal method
        foreach (var param in optionalParams)
        {
            WriteLine($"        const {param.Name} = options?.{param.Name};");
        }
 
        // Forward all params to internal method
        var allParamNames = capability.Parameters.Select(p => p.Name);
        Write($"        return new {promiseClass}(this.{internalMethodName}(");
        Write(string.Join(", ", allParamNames));
        WriteLine("));");
        WriteLine("    }");
        WriteLine();
    }
 
    /// <summary>
    /// Generates an args object with conditional inclusion of optional parameters.
    /// </summary>
    private void GenerateArgsObjectWithConditionals(
        string targetParamName,
        List<AtsParameterInfo> requiredParams,
        List<AtsParameterInfo> optionalParams,
        List<AtsParameterInfo>? cancellationParams = null)
    {
        var cancellationParamNames = new HashSet<string>(cancellationParams?.Select(p => p.Name) ?? []);
 
        // Build the required args inline
        var requiredArgs = new List<string> { $"{targetParamName}: this._handle" };
        foreach (var param in requiredParams)
        {
            if (param.IsCallback)
            {
                requiredArgs.Add($"callback: {param.Name}Id");
            }
            else if (cancellationParamNames.Contains(param.Name))
            {
                // Use the registered cancellation ID
                requiredArgs.Add($"{param.Name}: {param.Name}Id");
            }
            else
            {
                requiredArgs.Add(param.Name);
            }
        }
 
        WriteLine($"        const rpcArgs: Record<string, unknown> = {{ {string.Join(", ", requiredArgs)} }};");
 
        // Conditionally add optional params
        foreach (var param in optionalParams)
        {
            var isCancellation = cancellationParamNames.Contains(param.Name);
            var argName = param.IsCallback || isCancellation ? $"{param.Name}Id" : param.Name;
            var paramName = param.Name;
            var rpcParamName = param.IsCallback ? "callback" : paramName;
            WriteLine($"        if ({paramName} !== undefined) rpcArgs.{rpcParamName} = {argName};");
        }
    }
 
    private void GenerateThenableClass(BuilderModel builder)
    {
        var promiseClass = $"{builder.BuilderClassName}Promise";
 
        WriteLine($"/**");
        WriteLine($" * Thenable wrapper for {builder.BuilderClassName} that enables fluent chaining.");
        WriteLine($" * @example");
        WriteLine($" * await builder.addSomething().withX().withY();");
        WriteLine($" */");
        WriteLine($"export class {promiseClass} implements PromiseLike<{builder.BuilderClassName}> {{");
        WriteLine($"    constructor(private _promise: Promise<{builder.BuilderClassName}>) {{}}");
        WriteLine();
 
        // Generate then() for PromiseLike interface
        WriteLine($"    then<TResult1 = {builder.BuilderClassName}, TResult2 = never>(");
        WriteLine($"        onfulfilled?: ((value: {builder.BuilderClassName}) => TResult1 | PromiseLike<TResult1>) | null,");
        WriteLine("        onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null");
        WriteLine("    ): PromiseLike<TResult1 | TResult2> {");
        WriteLine("        return this._promise.then(onfulfilled, onrejected);");
        WriteLine("    }");
        WriteLine();
 
        // Generate fluent methods that chain via .then()
        // Capabilities are already flattened - no need to collect from parents
        // Filter out property getters and setters - they are not methods
        foreach (var capability in builder.Capabilities.Where(c =>
            c.CapabilityKind != AtsCapabilityKind.PropertyGetter &&
            c.CapabilityKind != AtsCapabilityKind.PropertySetter))
        {
            var methodName = capability.MethodName;
 
            // Separate required and optional parameters
            var (requiredParams, optionalParams) = SeparateParameters(capability.Parameters);
            var hasOptionals = optionalParams.Count > 0;
            var optionsInterfaceName = GetOptionsInterfaceName(methodName);
 
            // Build parameter list using options pattern
            var publicParamDefs = new List<string>();
            foreach (var param in requiredParams)
            {
                var tsType = MapParameterToTypeScript(param);
                publicParamDefs.Add($"{param.Name}: {tsType}");
            }
            if (hasOptionals)
            {
                publicParamDefs.Add($"options?: {optionsInterfaceName}");
            }
            var paramsString = string.Join(", ", publicParamDefs);
 
            // Forward args to underlying object's method (which handles options extraction)
            var forwardArgs = new List<string>();
            foreach (var param in requiredParams)
            {
                forwardArgs.Add(param.Name);
            }
            if (hasOptionals)
            {
                forwardArgs.Add("options");
            }
            var argsString = string.Join(", ", forwardArgs);
 
            // Check if this method returns a non-builder type
            var hasNonBuilderReturn = !capability.ReturnsBuilder && capability.ReturnType != null;
 
            if (!string.IsNullOrEmpty(capability.Description))
            {
                WriteLine($"    /** {capability.Description} */");
            }
 
            if (hasNonBuilderReturn)
            {
                // For non-builder returns, call the public method directly
                var returnType = MapTypeRefToTypeScript(capability.ReturnType);
                Write($"    {methodName}(");
                Write(paramsString);
                WriteLine($"): Promise<{returnType}> {{");
                Write($"        return this._promise.then(obj => obj.{methodName}(");
                Write(argsString);
                WriteLine("));");
                WriteLine("    }");
            }
            else
            {
                // For fluent builder methods, call the public method which wraps the internal
                Write($"    {methodName}(");
                Write(paramsString);
                Write($"): {promiseClass} {{");
                WriteLine();
                // Forward to the public method on the underlying object, wrapping result in promise class
                Write($"        return new {promiseClass}(this._promise.then(obj => obj.{methodName}(");
                Write(argsString);
                WriteLine(")));");
                WriteLine("    }");
            }
            WriteLine();
        }
 
        WriteLine("}");
        WriteLine();
    }
 
    private void GenerateAspireClient(List<AtsCapabilityInfo> entryPoints)
    {
        // Entry point methods (capabilities with no TargetTypeId) are generated as standalone functions
        // They're generated in GenerateConnectionHelper after the createBuilder() function
        // This method now only handles the comment header
        if (entryPoints.Count > 0)
        {
            WriteLine("// ============================================================================");
            WriteLine("// Entry Point Functions");
            WriteLine("// ============================================================================");
            WriteLine();
 
            foreach (var capability in entryPoints)
            {
                GenerateEntryPointFunction(capability);
            }
        }
    }
 
    private void GenerateEntryPointFunction(AtsCapabilityInfo capability)
    {
        var methodName = capability.MethodName;
 
        // Build parameter list
        var paramDefs = new List<string> { "client: AspireClientRpc" };
        var paramArgs = new List<string>();
 
        foreach (var param in capability.Parameters)
        {
            var tsType = MapParameterToTypeScript(param);
            var optional = param.IsOptional || param.IsNullable ? "?" : "";
            paramDefs.Add($"{param.Name}{optional}: {tsType}");
            paramArgs.Add(param.Name);
        }
 
        var paramsString = string.Join(", ", paramDefs);
        var argsObject = paramArgs.Count > 0
            ? $"{{ {string.Join(", ", paramArgs)} }}"
            : "{}";
 
        // Determine return type - check if return type has a Promise wrapper
        var capReturnTypeId = GetReturnTypeId(capability);
        var returnPromiseWrapper = GetPromiseWrapperForReturnType(capability.ReturnType);
 
        // Generate JSDoc
        if (!string.IsNullOrEmpty(capability.Description))
        {
            WriteLine($"/**");
            WriteLine($" * {capability.Description}");
            WriteLine($" */");
        }
 
        // Generate function based on return type
        if (returnPromiseWrapper != null && !string.IsNullOrEmpty(capReturnTypeId))
        {
            // Return type has Promise wrapper - generate fluent function
            var returnWrapperClass = _wrapperClassNames.GetValueOrDefault(capReturnTypeId)
                ?? DeriveClassName(capReturnTypeId);
            var handleType = GetHandleTypeName(capReturnTypeId);
 
            Write($"export function {methodName}(");
            Write(paramsString);
            WriteLine($"): {returnPromiseWrapper} {{");
            WriteLine($"    const promise = client.invokeCapability<{handleType}>(");
            WriteLine($"        '{capability.CapabilityId}',");
            WriteLine($"        {argsObject}");
            WriteLine($"    ).then(handle => new {returnWrapperClass}(handle, client));");
            WriteLine($"    return new {returnPromiseWrapper}(promise);");
            WriteLine("}");
        }
        else
        {
            // No Promise wrapper - return plain value
            var returnType = !string.IsNullOrEmpty(capReturnTypeId)
                ? MapTypeRefToTypeScript(capability.ReturnType)
                : "void";
 
            Write($"export async function {methodName}(");
            Write(paramsString);
            WriteLine($"): Promise<{returnType}> {{");
            if (returnType == "void")
            {
                WriteLine($"    await client.invokeCapability<void>(");
            }
            else
            {
                WriteLine($"    return await client.invokeCapability<{returnType}>(");
            }
            WriteLine($"        '{capability.CapabilityId}',");
            WriteLine($"        {argsObject}");
            WriteLine("    );");
            WriteLine("}");
        }
        WriteLine();
    }
 
    private string GenerateCallbackTypeSignature(IReadOnlyList<AtsCallbackParameterInfo>? callbackParameters, AtsTypeRef? callbackReturnType)
    {
        // Build parameter list
        var paramList = new List<string>();
        if (callbackParameters is not null)
        {
            foreach (var param in callbackParameters)
            {
                var tsType = MapTypeRefToTypeScript(param.Type);
                paramList.Add($"{param.Name}: {tsType}");
            }
        }
 
        var paramsString = paramList.Count > 0 ? string.Join(", ", paramList) : "";
 
        // Determine return type
        var returnType = callbackReturnType == null || callbackReturnType.TypeId == AtsConstants.Void
            ? "void"
            : MapTypeRefToTypeScript(callbackReturnType);
 
        // Callbacks are always async in TypeScript
        return $"({paramsString}) => Promise<{returnType}>";
    }
 
    private void GenerateCallbackRegistration(AtsParameterInfo callbackParam)
    {
        var callbackParameters = callbackParam.CallbackParameters;
        var isOptional = callbackParam.IsOptional || callbackParam.IsNullable;
        var callbackName = callbackParam.Name;
 
        // Determine parameter signature for registerCallback
        string paramSignature;
        if (callbackParameters is null || callbackParameters.Count == 0)
        {
            paramSignature = "";
        }
        else if (callbackParameters.Count == 1)
        {
            paramSignature = $"{callbackParameters[0].Name}Data: unknown";
        }
        else
        {
            paramSignature = "argsData: unknown";
        }
 
        // For optional callbacks, wrap the registration in a conditional
        if (isOptional)
        {
            WriteLine($"        const {callbackName}Id = {callbackName} ? registerCallback(async ({paramSignature}) => {{");
        }
        else
        {
            WriteLine($"        const {callbackName}Id = registerCallback(async ({paramSignature}) => {{");
        }
 
        // Generate the callback body
        GenerateCallbackBody(callbackParam, callbackParameters);
 
        // Close the callback registration
        if (isOptional)
        {
            WriteLine("        }) : undefined;");
        }
        else
        {
            WriteLine("        });");
        }
    }
 
    /// <summary>
    /// Checks if a parameter is a CancellationToken type.
    /// </summary>
    private static bool IsCancellationToken(AtsParameterInfo param)
    {
        return param.Type?.TypeId == AtsConstants.CancellationToken;
    }
 
    /// <summary>
    /// Generates cancellation registration for a CancellationToken parameter.
    /// </summary>
    private void GenerateCancellationRegistration(AtsParameterInfo param)
    {
        var isOptional = param.IsOptional || param.IsNullable;
        var paramName = param.Name;
 
        // For optional cancellation tokens, wrap the registration in a conditional
        if (isOptional)
        {
            WriteLine($"        const {paramName}Id = {paramName} ? registerCancellation({paramName}) : undefined;");
        }
        else
        {
            WriteLine($"        const {paramName}Id = registerCancellation({paramName});");
        }
    }
 
    /// <summary>
    /// Generates the body of a callback function.
    /// </summary>
    private void GenerateCallbackBody(AtsParameterInfo callbackParam, IReadOnlyList<AtsCallbackParameterInfo>? callbackParameters)
    {
        var callbackName = callbackParam.Name;
 
        // Check if callback has a return type - if so, we need to return the value
        var hasReturnType = callbackParam.CallbackReturnType != null
            && callbackParam.CallbackReturnType.TypeId != AtsConstants.Void;
        var returnPrefix = hasReturnType ? "return " : "";
 
        if (callbackParameters is null || callbackParameters.Count == 0)
        {
            // No parameters - just call the callback
            WriteLine($"            {returnPrefix}await {callbackName}();");
        }
        else if (callbackParameters.Count == 1)
        {
            // Single parameter callback
            var cbParam = callbackParameters[0];
            var tsType = MapTypeRefToTypeScript(cbParam.Type);
            var cbTypeId = cbParam.Type.TypeId;
 
            if (_wrapperClassNames.TryGetValue(cbTypeId, out var wrapperClassName))
            {
                // For types with wrapper classes, create an instance of the wrapper
                var handleType = GetHandleTypeName(cbTypeId);
                WriteLine($"            const {cbParam.Name}Handle = wrapIfHandle({cbParam.Name}Data) as {handleType};");
                WriteLine($"            const {cbParam.Name} = new {wrapperClassName}({cbParam.Name}Handle, this._client);");
            }
            else
            {
                // For raw handle types, just wrap and cast
                WriteLine($"            const {cbParam.Name} = wrapIfHandle({cbParam.Name}Data) as {tsType};");
            }
 
            WriteLine($"            {returnPrefix}await {callbackName}({cbParam.Name});");
        }
        else
        {
            // Multi-parameter callback - .NET sends as { p0, p1, ... }
            var paramNames = callbackParameters.Select((p, i) => $"p{i}").ToList();
            var destructure = string.Join(", ", paramNames);
 
            WriteLine($"            const args = argsData as {{ {destructure}: unknown }};");
 
            var callArgs = new List<string>();
            for (var i = 0; i < callbackParameters.Count; i++)
            {
                var cbParam = callbackParameters[i];
                var tsType = MapTypeRefToTypeScript(cbParam.Type);
                var cbTypeId = cbParam.Type.TypeId;
 
                if (_wrapperClassNames.TryGetValue(cbTypeId, out var wrapperClassName))
                {
                    // For types with wrapper classes, create an instance of the wrapper
                    var handleType = GetHandleTypeName(cbTypeId);
                    WriteLine($"            const {cbParam.Name}Handle = wrapIfHandle(args.p{i}) as {handleType};");
                    WriteLine($"            const {cbParam.Name} = new {wrapperClassName}({cbParam.Name}Handle, this._client);");
                }
                else
                {
                    // For raw handle types, just wrap and cast
                    WriteLine($"            const {cbParam.Name} = wrapIfHandle(args.p{i}) as {tsType};");
                }
                callArgs.Add(cbParam.Name);
            }
 
            WriteLine($"            {returnPrefix}await {callbackName}({string.Join(", ", callArgs)});");
        }
    }
 
    private void GenerateConnectionHelper()
    {
        var builderHandle = GetHandleTypeName(AtsConstants.BuilderTypeId);
 
        WriteLine($$"""
            // ============================================================================
            // Connection Helper
            // ============================================================================
 
            /**
             * Creates and connects to the Aspire AppHost.
             * Reads connection info from environment variables set by `aspire run`.
             */
            export async function connect(): Promise<AspireClientRpc> {
                const socketPath = process.env.REMOTE_APP_HOST_SOCKET_PATH;
                if (!socketPath) {
                    throw new Error(
                        'REMOTE_APP_HOST_SOCKET_PATH environment variable not set. ' +
                        'Run this application using `aspire run`.'
                    );
                }
 
                const client = new AspireClientRpc(socketPath);
                await client.connect();
 
                // Exit the process if the server connection is lost
                client.onDisconnect(() => {
                    console.error('Connection to AppHost lost. Exiting...');
                    process.exit(1);
                });
 
                return client;
            }
 
            /**
             * Creates a new distributed application builder.
             * This is the entry point for building Aspire applications.
             *
             * @param options - Optional configuration options for the builder
             * @returns A DistributedApplicationBuilder instance
             *
             * @example
             * const builder = await createBuilder();
             * builder.addRedis("cache");
             * builder.addContainer("api", "mcr.microsoft.com/dotnet/samples:aspnetapp");
             * const app = await builder.build();
             * await app.run();
             */
            export async function createBuilder(options?: CreateBuilderOptions): Promise<DistributedApplicationBuilder> {
                const client = await connect();
 
                // Default args and projectDirectory if not provided
                const effectiveOptions: CreateBuilderOptions = {
                    ...options,
                    args: options?.args ?? process.argv.slice(2),
                    projectDirectory: options?.projectDirectory ?? process.env.ASPIRE_PROJECT_DIRECTORY ?? process.cwd()
                };
 
                const handle = await client.invokeCapability<{{builderHandle}}>(
                    'Aspire.Hosting/createBuilderWithOptions',
                    { options: effectiveOptions }
                );
                return new DistributedApplicationBuilder(handle, client);
            }
 
            // Re-export commonly used types
            export { Handle, CapabilityError, registerCallback } from './transport.js';
            export { refExpr, ReferenceExpression } from './base.js';
            """);
        WriteLine();
    }
 
    private void GenerateGlobalErrorHandling()
    {
        WriteLine("""
            // ============================================================================
            // Global Error Handling
            // ============================================================================
 
            /**
             * Set up global error handlers to ensure the process exits properly on errors.
             * Node.js doesn't exit on unhandled rejections by default, so we need to handle them.
             */
            process.on('unhandledRejection', (reason: unknown) => {
                const error = reason instanceof Error ? reason : new Error(String(reason));
 
                if (reason instanceof CapabilityError) {
                    console.error(`\n❌ Capability Error: ${error.message}`);
                    console.error(`   Code: ${(reason as CapabilityError).code}`);
                    if ((reason as CapabilityError).capability) {
                        console.error(`   Capability: ${(reason as CapabilityError).capability}`);
                    }
                } else {
                    console.error(`\n❌ Unhandled Error: ${error.message}`);
                    if (error.stack) {
                        console.error(error.stack);
                    }
                }
 
                process.exit(1);
            });
 
            process.on('uncaughtException', (error: Error) => {
                console.error(`\n❌ Uncaught Exception: ${error.message}`);
                if (error.stack) {
                    console.error(error.stack);
                }
                process.exit(1);
            });
            """);
    }
 
    /// <summary>
    /// Generates handle wrapper registrations for all type classes and builder classes.
    /// This allows callback handles to be wrapped as typed instances.
    /// </summary>
    private void GenerateHandleWrapperRegistrations(List<BuilderModel> typeClasses, List<BuilderModel> resourceBuilders)
    {
        WriteLine();
        WriteLine("// ============================================================================");
        WriteLine("// Handle Wrapper Registrations");
        WriteLine("// ============================================================================");
        WriteLine();
        WriteLine("// Register wrapper factories for typed handle wrapping in callbacks");
 
        // Register type classes (context types like EnvironmentCallbackContext)
        foreach (var typeClass in typeClasses)
        {
            var className = _wrapperClassNames.GetValueOrDefault(typeClass.TypeId) ?? DeriveClassName(typeClass.TypeId);
            var handleType = GetHandleTypeName(typeClass.TypeId);
            WriteLine($"registerHandleWrapper('{typeClass.TypeId}', (handle, client) => new {className}(handle as {handleType}, client));");
        }
 
        // Register resource builder classes
        foreach (var builder in resourceBuilders)
        {
            var className = _wrapperClassNames.GetValueOrDefault(builder.TypeId) ?? DeriveClassName(builder.TypeId);
            var handleType = GetHandleTypeName(builder.TypeId);
            WriteLine($"registerHandleWrapper('{builder.TypeId}', (handle, client) => new {className}(handle as {handleType}, client));");
        }
 
        WriteLine();
    }
 
    /// <summary>
    /// Generates a type class (context type or wrapper type).
    /// Uses property-like object pattern for exposed properties.
    /// For types with methods, also generates a Promise wrapper class for fluent chaining.
    /// </summary>
    private void GenerateTypeClass(BuilderModel model)
    {
        var handleType = GetHandleTypeName(model.TypeId);
        var className = DeriveClassName(model.TypeId);
        var hasMethods = HasChainableMethods(model);
 
        WriteLine("// ============================================================================");
        WriteLine($"// {className}");
        WriteLine("// ============================================================================");
        WriteLine();
 
        // Separate capabilities by type using CapabilityKind enum
        var getters = model.Capabilities.Where(c => c.CapabilityKind == AtsCapabilityKind.PropertyGetter).ToList();
        var setters = model.Capabilities.Where(c => c.CapabilityKind == AtsCapabilityKind.PropertySetter).ToList();
        var contextMethods = model.Capabilities.Where(c => c.CapabilityKind == AtsCapabilityKind.InstanceMethod).ToList();
        var otherMethods = model.Capabilities.Where(c => c.CapabilityKind == AtsCapabilityKind.Method).ToList();
 
        // Combine methods for thenable generation
        var allMethods = contextMethods.Concat(otherMethods).ToList();
 
        WriteLine($"/**");
        WriteLine($" * Type class for {className}.");
        WriteLine($" */");
        WriteLine($"export class {className} {{");
        WriteLine($"    constructor(private _handle: {handleType}, private _client: AspireClientRpc) {{}}");
        WriteLine();
        WriteLine($"    /** Serialize for JSON-RPC transport */");
        WriteLine($"    toJSON(): MarshalledHandle {{ return this._handle.toJSON(); }}");
        WriteLine();
 
        // Group getters and setters by property name to create property-like objects
        var properties = GroupPropertiesByName(getters, setters);
 
        // Generate property-like objects
        foreach (var prop in properties)
        {
            GeneratePropertyLikeObject(prop.PropertyName, prop.Getter, prop.Setter);
        }
 
        // Generate methods - use thenable pattern if this type has a Promise wrapper
        if (hasMethods)
        {
            foreach (var method in allMethods)
            {
                GenerateTypeClassMethod(model, method);
            }
        }
        else
        {
            // No Promise wrapper - generate plain async methods
            foreach (var method in contextMethods)
            {
                GenerateContextMethod(method);
            }
            foreach (var method in otherMethods)
            {
                GenerateWrapperMethod(method);
            }
        }
 
        WriteLine("}");
        WriteLine();
 
        // Generate thenable wrapper class if this type has methods
        if (hasMethods)
        {
            GenerateTypeClassThenableWrapper(model, allMethods);
        }
    }
 
    /// <summary>
    /// Groups getters and setters by property name.
    /// </summary>
    private static List<(string PropertyName, AtsCapabilityInfo? Getter, AtsCapabilityInfo? Setter)> GroupPropertiesByName(
        List<AtsCapabilityInfo> getters, List<AtsCapabilityInfo> setters)
    {
        var result = new List<(string PropertyName, AtsCapabilityInfo? Getter, AtsCapabilityInfo? Setter)>();
        var processedNames = new HashSet<string>();
 
        // Process getters
        foreach (var getter in getters)
        {
            var propName = ExtractPropertyName(getter.MethodName);
            if (processedNames.Contains(propName))
            {
                continue;
            }
            processedNames.Add(propName);
 
            // Find matching setter (setPropertyName for propertyName)
            var setterName = "set" + char.ToUpperInvariant(propName[0]) + propName[1..];
            var setter = setters.FirstOrDefault(s => ExtractPropertyName(s.MethodName).Equals(setterName, StringComparison.OrdinalIgnoreCase));
 
            result.Add((propName, getter, setter));
        }
 
        // Process any setters without matching getters
        foreach (var setter in setters)
        {
            var setterMethodName = ExtractPropertyName(setter.MethodName);
            // setPropertyName -> propertyName
            if (setterMethodName.StartsWith("set", StringComparison.OrdinalIgnoreCase) && setterMethodName.Length > 3)
            {
                var propName = char.ToLowerInvariant(setterMethodName[3]) + setterMethodName[4..];
                if (!processedNames.Contains(propName))
                {
                    processedNames.Add(propName);
                    result.Add((propName, null, setter));
                }
            }
        }
 
        return result;
    }
 
    /// <summary>
    /// Extracts the property name from a method name like "ClassName.propertyName" or "setPropertyName".
    /// </summary>
    private static string ExtractPropertyName(string methodName)
    {
        // Handle "ClassName.propertyName" format
        if (methodName.Contains('.'))
        {
            return methodName[(methodName.LastIndexOf('.') + 1)..];
        }
        return methodName;
    }
 
    /// <summary>
    /// Generates a property-like object with get and/or set methods.
    /// For dictionary types, generates a direct AspireDict field instead.
    /// </summary>
    private void GeneratePropertyLikeObject(string propertyName, AtsCapabilityInfo? getter, AtsCapabilityInfo? setter)
    {
        // Determine the return type from getter
        string returnType = "unknown";
        string? description = null;
 
        if (getter != null)
        {
            returnType = MapTypeRefToTypeScript(getter.ReturnType);
            description = getter.Description;
 
            // Check if this is a dictionary type - generate direct AspireDict field instead
            if (IsDictionaryType(getter.ReturnType))
            {
                GenerateDictionaryProperty(propertyName, getter);
                return;
            }
 
            // Check if return type is a wrapper class - use property-like object returning wrapper
            if (getter.ReturnType?.TypeId != null && _wrapperClassNames.TryGetValue(getter.ReturnType.TypeId, out var wrapperClassName))
            {
                GenerateWrapperPropertyObject(propertyName, getter, wrapperClassName);
                return;
            }
        }
 
        // Generate property-like object for scalar types
        if (!string.IsNullOrEmpty(description))
        {
            WriteLine($"    /** {description} */");
        }
 
        WriteLine($"    {propertyName} = {{");
 
        // Generate get method
        if (getter != null)
        {
            WriteLine($"        get: async (): Promise<{returnType}> => {{");
            WriteLine($"            return await this._client.invokeCapability<{returnType}>(");
            WriteLine($"                '{getter.CapabilityId}',");
            WriteLine($"                {{ context: this._handle }}");
            WriteLine("            );");
            WriteLine("        },");
        }
 
        // Generate set method
        if (setter != null)
        {
            var valueParam = setter.Parameters.FirstOrDefault(p => p.Name == "value");
            if (valueParam != null)
            {
                var valueType = MapTypeRefToTypeScript(valueParam.Type);
                WriteLine($"        set: async (value: {valueType}): Promise<void> => {{");
                WriteLine($"            await this._client.invokeCapability<void>(");
                WriteLine($"                '{setter.CapabilityId}',");
                WriteLine($"                {{ context: this._handle, value }}");
                WriteLine("            );");
                WriteLine("        }");
            }
        }
 
        WriteLine("    };");
        WriteLine();
    }
 
    /// <summary>
    /// Generates a property-like object that returns a wrapper class.
    /// </summary>
    private void GenerateWrapperPropertyObject(string propertyName, AtsCapabilityInfo getter, string wrapperClassName)
    {
        var handleType = GetHandleTypeName(getter.ReturnType!.TypeId);
 
        if (!string.IsNullOrEmpty(getter.Description))
        {
            WriteLine($"    /** {getter.Description} */");
        }
 
        WriteLine($"    {propertyName} = {{");
        WriteLine($"        get: async (): Promise<{wrapperClassName}> => {{");
        WriteLine($"            const handle = await this._client.invokeCapability<{handleType}>(");
        WriteLine($"                '{getter.CapabilityId}',");
        WriteLine($"                {{ context: this._handle }}");
        WriteLine("            );");
        WriteLine($"            return new {wrapperClassName}(handle, this._client);");
        WriteLine("        },");
        WriteLine("    };");
        WriteLine();
    }
 
    /// <summary>
    /// Checks if a type reference is a dictionary type.
    /// </summary>
    private static bool IsDictionaryType(AtsTypeRef? typeRef)
    {
        return typeRef?.Category == AtsTypeCategory.Dict;
    }
 
    /// <summary>
    /// Generates a direct AspireDict property for dictionary types.
    /// </summary>
    private void GenerateDictionaryProperty(string propertyName, AtsCapabilityInfo getter)
    {
        // Determine key and value types
        var keyType = "string";
        var valueType = "unknown";
 
        // Try to extract key and value types from Dict type
        if (getter.ReturnType?.KeyType != null)
        {
            keyType = MapTypeRefToTypeScript(getter.ReturnType.KeyType);
        }
        if (getter.ReturnType?.ValueType != null)
        {
            // Union types will be mapped correctly via MapTypeRefToTypeScript
            valueType = MapTypeRefToTypeScript(getter.ReturnType.ValueType);
        }
 
        var typeId = $"'{getter.CapabilityId.Replace(".get", "")}'";
        var getterCapabilityId = $"'{getter.CapabilityId}'";
 
        if (!string.IsNullOrEmpty(getter.Description))
        {
            WriteLine($"    /** {getter.Description} */");
        }
 
        // Generate a getter property that returns AspireDict
        // Pass the getter capability ID so AspireDict can lazily fetch the actual dictionary handle
        WriteLine($"    private _{propertyName}?: AspireDict<{keyType}, {valueType}>;");
        WriteLine($"    get {propertyName}(): AspireDict<{keyType}, {valueType}> {{");
        WriteLine($"        if (!this._{propertyName}) {{");
        WriteLine($"            this._{propertyName} = new AspireDict<{keyType}, {valueType}>(");
        WriteLine($"                this._handle,");
        WriteLine($"                this._client,");
        WriteLine($"                {typeId},");
        WriteLine($"                {getterCapabilityId}");
        WriteLine("            );");
        WriteLine("        }");
        WriteLine($"        return this._{propertyName};");
        WriteLine("    }");
        WriteLine();
    }
 
    /// <summary>
    /// Generates a context instance method (from ExposeMethods=true).
    /// </summary>
    private void GenerateContextMethod(AtsCapabilityInfo method)
    {
        // Use OwningTypeName if available to extract method name, otherwise parse from MethodName
        var methodName = !string.IsNullOrEmpty(method.OwningTypeName) && method.MethodName.Contains('.')
            ? method.MethodName[(method.MethodName.LastIndexOf('.') + 1)..]
            : method.MethodName;
 
        // Filter out target parameter
        var targetParamName = method.TargetParameterName ?? "context";
        var userParams = method.Parameters.Where(p => p.Name != targetParamName).ToList();
 
        // Separate required and optional parameters
        var (requiredParams, optionalParams) = SeparateParameters(userParams);
        var hasOptionals = optionalParams.Count > 0;
        var optionsInterfaceName = GetOptionsInterfaceName(methodName);
 
        // Build parameter list using options pattern
        var paramDefs = new List<string>();
        foreach (var param in requiredParams)
        {
            var tsType = MapParameterToTypeScript(param);
            paramDefs.Add($"{param.Name}: {tsType}");
        }
        if (hasOptionals)
        {
            paramDefs.Add($"options?: {optionsInterfaceName}");
        }
        var paramsString = string.Join(", ", paramDefs);
 
        // Determine return type
        var returnType = GetReturnTypeId(method) != null
            ? MapTypeRefToTypeScript(method.ReturnType)
            : "void";
 
        // Generate JSDoc
        if (!string.IsNullOrEmpty(method.Description))
        {
            WriteLine($"    /** {method.Description} */");
        }
 
        // Generate async method
        Write($"    async {methodName}(");
        Write(paramsString);
        WriteLine($"): Promise<{returnType}> {{");
 
        // Extract optional params from options object
        foreach (var param in optionalParams)
        {
            WriteLine($"        const {param.Name} = options?.{param.Name};");
        }
 
        // Build args object with conditional inclusion
        var requiredArgs = new List<string> { $"{targetParamName}: this._handle" };
        foreach (var param in requiredParams)
        {
            requiredArgs.Add(param.Name);
        }
        WriteLine($"        const rpcArgs: Record<string, unknown> = {{ {string.Join(", ", requiredArgs)} }};");
        foreach (var param in optionalParams)
        {
            WriteLine($"        if ({param.Name} !== undefined) rpcArgs.{param.Name} = {param.Name};");
        }
 
        if (returnType == "void")
        {
            WriteLine($"        await this._client.invokeCapability<void>(");
        }
        else
        {
            WriteLine($"        return await this._client.invokeCapability<{returnType}>(");
        }
        WriteLine($"            '{method.CapabilityId}',");
        WriteLine($"            rpcArgs");
        WriteLine("        );");
        WriteLine("    }");
        WriteLine();
    }
 
    /// <summary>
    /// Generates a method on a wrapper class.
    /// </summary>
    private void GenerateWrapperMethod(AtsCapabilityInfo capability)
    {
        var methodName = GetTypeScriptMethodName(capability.MethodName);
 
        // First arg is the handle (implicit via this._handle) - use metadata instead of string parsing
        var firstParamName = capability.TargetParameterName ?? "builder";
 
        // Filter out the implicit handle parameter
        var userParams = capability.Parameters.Where(p => p.Name != firstParamName).ToList();
 
        // Separate required and optional parameters
        var (requiredParams, optionalParams) = SeparateParameters(userParams);
        var hasOptionals = optionalParams.Count > 0;
        var optionsInterfaceName = GetOptionsInterfaceName(methodName);
 
        // Build parameter list using options pattern
        var paramDefs = new List<string>();
        foreach (var param in requiredParams)
        {
            var tsType = MapParameterToTypeScript(param);
            paramDefs.Add($"{param.Name}: {tsType}");
        }
        if (hasOptionals)
        {
            paramDefs.Add($"options?: {optionsInterfaceName}");
        }
        var paramsString = string.Join(", ", paramDefs);
 
        // Determine return type
        var returnType = MapTypeRefToTypeScript(capability.ReturnType);
 
        // Generate JSDoc
        if (!string.IsNullOrEmpty(capability.Description))
        {
            WriteLine($"    /** {capability.Description} */");
        }
 
        // Generate async method
        Write($"    async {methodName}(");
        Write(paramsString);
        WriteLine($"): Promise<{returnType}> {{");
 
        // Extract optional params from options object
        foreach (var param in optionalParams)
        {
            WriteLine($"        const {param.Name} = options?.{param.Name};");
        }
 
        // Build args object with conditional inclusion
        var requiredArgs = new List<string> { $"{firstParamName}: this._handle" };
        foreach (var param in requiredParams)
        {
            requiredArgs.Add(param.Name);
        }
        WriteLine($"        const rpcArgs: Record<string, unknown> = {{ {string.Join(", ", requiredArgs)} }};");
        foreach (var param in optionalParams)
        {
            WriteLine($"        if ({param.Name} !== undefined) rpcArgs.{param.Name} = {param.Name};");
        }
 
        if (returnType == "void")
        {
            WriteLine($"        await this._client.invokeCapability<void>(");
        }
        else
        {
            WriteLine($"        return await this._client.invokeCapability<{returnType}>(");
        }
        WriteLine($"            '{capability.CapabilityId}',");
        WriteLine($"            rpcArgs");
        WriteLine("        );");
        WriteLine("    }");
        WriteLine();
    }
 
    /// <summary>
    /// Generates a method on a type class using the thenable pattern.
    /// Generates both an internal async method and a public fluent method.
    /// </summary>
    private void GenerateTypeClassMethod(BuilderModel model, AtsCapabilityInfo capability)
    {
        var className = DeriveClassName(model.TypeId);
        var promiseClass = $"{className}Promise";
 
        // Use OwningTypeName if available to extract method name, otherwise parse from MethodName
        var methodName = !string.IsNullOrEmpty(capability.OwningTypeName) && capability.MethodName.Contains('.')
            ? capability.MethodName[(capability.MethodName.LastIndexOf('.') + 1)..]
            : GetTypeScriptMethodName(capability.MethodName);
 
        var internalMethodName = $"_{methodName}Internal";
 
        // Filter out target parameter
        var targetParamName = capability.TargetParameterName ?? "context";
        var userParams = capability.Parameters.Where(p => p.Name != targetParamName).ToList();
 
        // Separate required and optional parameters
        var (requiredParams, optionalParams) = SeparateParameters(userParams);
        var hasOptionals = optionalParams.Count > 0;
        var optionsInterfaceName = GetOptionsInterfaceName(methodName);
 
        // Build parameter list for public method
        var publicParamDefs = new List<string>();
        foreach (var param in requiredParams)
        {
            var tsType = MapParameterToTypeScript(param);
            publicParamDefs.Add($"{param.Name}: {tsType}");
        }
        if (hasOptionals)
        {
            publicParamDefs.Add($"options?: {optionsInterfaceName}");
        }
        var publicParamsString = string.Join(", ", publicParamDefs);
 
        // Build parameter list for internal method (all params positional)
        var internalParamDefs = new List<string>();
        foreach (var param in userParams)
        {
            var tsType = MapParameterToTypeScript(param);
            var optional = param.IsOptional || param.IsNullable ? "?" : "";
            internalParamDefs.Add($"{param.Name}{optional}: {tsType}");
        }
        var internalParamsString = string.Join(", ", internalParamDefs);
 
        // Check if return type has a Promise wrapper
        var returnPromiseWrapper = GetPromiseWrapperForReturnType(capability.ReturnType);
        var returnType = MapTypeRefToTypeScript(capability.ReturnType);
        var isVoid = capability.ReturnType == null || capability.ReturnType.TypeId == AtsConstants.Void;
 
        // Generate JSDoc
        if (!string.IsNullOrEmpty(capability.Description))
        {
            WriteLine($"    /** {capability.Description} */");
        }
 
        // If return type has a Promise wrapper, generate internal + fluent pattern
        if (returnPromiseWrapper != null)
        {
            var returnWrapperClass = _wrapperClassNames.GetValueOrDefault(capability.ReturnType!.TypeId)
                ?? DeriveClassName(capability.ReturnType.TypeId);
            var returnHandleType = GetHandleTypeName(capability.ReturnType.TypeId);
 
            // Generate internal async method
            WriteLine($"    /** @internal */");
            Write($"    async {internalMethodName}(");
            Write(internalParamsString);
            WriteLine($"): Promise<{returnWrapperClass}> {{");
 
            // Handle callback registration if any
            var callbackParams = userParams.Where(p => p.IsCallback).ToList();
            foreach (var callbackParam in callbackParams)
            {
                GenerateCallbackRegistration(callbackParam);
            }
 
            // Build args with conditional inclusion
            GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams);
 
            WriteLine($"        const result = await this._client.invokeCapability<{returnHandleType}>(");
            WriteLine($"            '{capability.CapabilityId}',");
            WriteLine($"            rpcArgs");
            WriteLine("        );");
            WriteLine($"        return new {returnWrapperClass}(result, this._client);");
            WriteLine("    }");
            WriteLine();
 
            // Generate public fluent method that returns thenable wrapper
            Write($"    {methodName}(");
            Write(publicParamsString);
            WriteLine($"): {returnPromiseWrapper} {{");
 
            // Extract optional params and forward
            foreach (var param in optionalParams)
            {
                WriteLine($"        const {param.Name} = options?.{param.Name};");
            }
 
            Write($"        return new {returnPromiseWrapper}(this.{internalMethodName}(");
            Write(string.Join(", ", userParams.Select(p => p.Name)));
            WriteLine("));");
            WriteLine("    }");
        }
        else if (isVoid)
        {
            // Void return - generate internal + fluent returning this type's Promise wrapper
            // Generate internal async method
            WriteLine($"    /** @internal */");
            Write($"    async {internalMethodName}(");
            Write(internalParamsString);
            WriteLine($"): Promise<{className}> {{");
 
            // Handle callback registration if any
            var callbackParams = userParams.Where(p => p.IsCallback).ToList();
            foreach (var callbackParam in callbackParams)
            {
                GenerateCallbackRegistration(callbackParam);
            }
 
            // Build args with conditional inclusion
            GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams);
 
            WriteLine($"        await this._client.invokeCapability<void>(");
            WriteLine($"            '{capability.CapabilityId}',");
            WriteLine($"            rpcArgs");
            WriteLine("        );");
            WriteLine($"        return this;");
            WriteLine("    }");
            WriteLine();
 
            // Generate public fluent method
            Write($"    {methodName}(");
            Write(publicParamsString);
            WriteLine($"): {promiseClass} {{");
 
            // Extract optional params and forward
            foreach (var param in optionalParams)
            {
                WriteLine($"        const {param.Name} = options?.{param.Name};");
            }
 
            Write($"        return new {promiseClass}(this.{internalMethodName}(");
            Write(string.Join(", ", userParams.Select(p => p.Name)));
            WriteLine("));");
            WriteLine("    }");
        }
        else
        {
            // Non-void, non-wrapper return - plain async method
            Write($"    async {methodName}(");
            Write(publicParamsString);
            WriteLine($"): Promise<{returnType}> {{");
 
            // Extract optional params from options object
            foreach (var param in optionalParams)
            {
                WriteLine($"        const {param.Name} = options?.{param.Name};");
            }
 
            // Handle callback registration if any
            var callbackParams = userParams.Where(p => p.IsCallback).ToList();
            foreach (var callbackParam in callbackParams)
            {
                GenerateCallbackRegistration(callbackParam);
            }
 
            // Build args with conditional inclusion
            GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams);
 
            WriteLine($"        return await this._client.invokeCapability<{returnType}>(");
            WriteLine($"            '{capability.CapabilityId}',");
            WriteLine($"            rpcArgs");
            WriteLine("        );");
            WriteLine("    }");
        }
        WriteLine();
    }
 
    /// <summary>
    /// Generates a thenable wrapper class for a type class.
    /// </summary>
    private void GenerateTypeClassThenableWrapper(BuilderModel model, List<AtsCapabilityInfo> methods)
    {
        var className = DeriveClassName(model.TypeId);
        var promiseClass = $"{className}Promise";
 
        WriteLine($"/**");
        WriteLine($" * Thenable wrapper for {className} that enables fluent chaining.");
        WriteLine($" */");
        WriteLine($"export class {promiseClass} implements PromiseLike<{className}> {{");
        WriteLine($"    constructor(private _promise: Promise<{className}>) {{}}");
        WriteLine();
 
        // Generate then() for PromiseLike interface
        WriteLine($"    then<TResult1 = {className}, TResult2 = never>(");
        WriteLine($"        onfulfilled?: ((value: {className}) => TResult1 | PromiseLike<TResult1>) | null,");
        WriteLine("        onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null");
        WriteLine("    ): PromiseLike<TResult1 | TResult2> {");
        WriteLine("        return this._promise.then(onfulfilled, onrejected);");
        WriteLine("    }");
        WriteLine();
 
        // Generate fluent methods that chain via .then()
        foreach (var capability in methods)
        {
            var methodName = !string.IsNullOrEmpty(capability.OwningTypeName) && capability.MethodName.Contains('.')
                ? capability.MethodName[(capability.MethodName.LastIndexOf('.') + 1)..]
                : GetTypeScriptMethodName(capability.MethodName);
 
            var targetParamName = capability.TargetParameterName ?? "context";
            var userParams = capability.Parameters.Where(p => p.Name != targetParamName).ToList();
 
            // Separate required and optional parameters
            var (requiredParams, optionalParams) = SeparateParameters(userParams);
            var hasOptionals = optionalParams.Count > 0;
            var optionsInterfaceName = GetOptionsInterfaceName(methodName);
 
            // Build parameter list using options pattern
            var publicParamDefs = new List<string>();
            foreach (var param in requiredParams)
            {
                var tsType = MapParameterToTypeScript(param);
                publicParamDefs.Add($"{param.Name}: {tsType}");
            }
            if (hasOptionals)
            {
                publicParamDefs.Add($"options?: {optionsInterfaceName}");
            }
            var paramsString = string.Join(", ", publicParamDefs);
 
            // Forward args to underlying object's public method
            var forwardArgs = new List<string>();
            foreach (var param in requiredParams)
            {
                forwardArgs.Add(param.Name);
            }
            if (hasOptionals)
            {
                forwardArgs.Add("options");
            }
            var argsString = string.Join(", ", forwardArgs);
 
            // Check if return type has a Promise wrapper
            var returnPromiseWrapper = GetPromiseWrapperForReturnType(capability.ReturnType);
            var returnType = MapTypeRefToTypeScript(capability.ReturnType);
            var isVoid = capability.ReturnType == null || capability.ReturnType.TypeId == AtsConstants.Void;
 
            if (!string.IsNullOrEmpty(capability.Description))
            {
                WriteLine($"    /** {capability.Description} */");
            }
 
            if (returnPromiseWrapper != null)
            {
                // Return type has Promise wrapper - forward to public method, wrap result
                Write($"    {methodName}(");
                Write(paramsString);
                WriteLine($"): {returnPromiseWrapper} {{");
                Write($"        return new {returnPromiseWrapper}(this._promise.then(obj => obj.{methodName}(");
                Write(argsString);
                WriteLine(")));");
                WriteLine("    }");
            }
            else if (isVoid)
            {
                // Void return - forward to public method, wrap result in this class's promise
                Write($"    {methodName}(");
                Write(paramsString);
                WriteLine($"): {promiseClass} {{");
                Write($"        return new {promiseClass}(this._promise.then(obj => obj.{methodName}(");
                Write(argsString);
                WriteLine(")));");
                WriteLine("    }");
            }
            else
            {
                // Non-void, non-wrapper return - plain Promise
                Write($"    {methodName}(");
                Write(paramsString);
                WriteLine($"): Promise<{returnType}> {{");
                Write($"        return this._promise.then(obj => obj.{methodName}(");
                Write(argsString);
                WriteLine("));");
                WriteLine("    }");
            }
            WriteLine();
        }
 
        WriteLine("}");
        WriteLine();
    }
 
    // ============================================================================
    // Builder Model Helpers (replaces AtsBuilderModelFactory)
    // ============================================================================
 
    /// <summary>
    /// Groups capabilities by ExpandedTargetTypes to create builder models.
    /// Uses expansion to map interface targets to their concrete implementations.
    /// Also creates builders for interface types (for use as return type wrappers).
    /// </summary>
    private static List<BuilderModel> CreateBuilderModels(IReadOnlyList<AtsCapabilityInfo> capabilities)
    {
        // Group capabilities by expanded target type IDs
        // A capability targeting IResource with ExpandedTargetTypes = [RedisResource]
        // will be assigned to Aspire.Hosting.Redis/RedisResource (the concrete type)
        var capabilitiesByTypeId = new Dictionary<string, List<AtsCapabilityInfo>>();
 
        // Track the AtsTypeRef for each typeId (from ExpandedTargetTypes or TargetType metadata)
        var typeRefsByTypeId = new Dictionary<string, AtsTypeRef>();
 
        // Also track interface types and their capabilities (for interface wrapper classes)
        var interfaceCapabilities = new Dictionary<string, List<AtsCapabilityInfo>>();
 
        foreach (var cap in capabilities)
        {
            var targetTypeRef = cap.TargetType;
            var targetTypeId = cap.TargetTypeId;
            if (targetTypeRef == null || string.IsNullOrEmpty(targetTypeId))
            {
                // Entry point methods - handled separately
                continue;
            }
 
            // Use category-based check instead of string parsing
            if (targetTypeRef.Category != AtsTypeCategory.Handle)
            {
                continue;
            }
 
            // Use expanded types if available, otherwise fall back to the original target
            var expandedTypes = cap.ExpandedTargetTypes;
            if (expandedTypes is { Count: > 0 })
            {
                // Flatten to concrete types
                foreach (var expandedType in expandedTypes)
                {
                    if (!capabilitiesByTypeId.TryGetValue(expandedType.TypeId, out var list))
                    {
                        list = [];
                        capabilitiesByTypeId[expandedType.TypeId] = list;
                        // Store the type ref for this expanded type
                        typeRefsByTypeId[expandedType.TypeId] = expandedType;
                    }
                    list.Add(cap);
                }
 
                // Also track the original interface type for wrapper class generation
                if (targetTypeRef.IsInterface)
                {
                    if (!interfaceCapabilities.TryGetValue(targetTypeId, out var interfaceList))
                    {
                        interfaceList = [];
                        interfaceCapabilities[targetTypeId] = interfaceList;
                        // Store the type ref for the interface
                        typeRefsByTypeId[targetTypeId] = targetTypeRef;
                    }
                    interfaceList.Add(cap);
                }
            }
            else
            {
                // No expansion - use original target (concrete type)
                if (!capabilitiesByTypeId.TryGetValue(targetTypeId, out var list))
                {
                    list = [];
                    capabilitiesByTypeId[targetTypeId] = list;
                    // Store the type ref for this target type
                    typeRefsByTypeId[targetTypeId] = targetTypeRef;
                }
                list.Add(cap);
            }
        }
 
        // Create a builder for each concrete type with its specific capabilities
        var builders = new List<BuilderModel>();
        foreach (var (typeId, typeCapabilities) in capabilitiesByTypeId)
        {
            var builderClassName = DeriveClassName(typeId);
 
            // Get the type ref from tracked metadata (based on target type, not return type)
            var typeRef = typeRefsByTypeId.GetValueOrDefault(typeId);
 
            // Deduplicate capabilities by CapabilityId to avoid duplicate methods
            var uniqueCapabilities = typeCapabilities
                .GroupBy(c => c.CapabilityId)
                .Select(g => g.First())
                .ToList();
 
            var builder = new BuilderModel
            {
                TypeId = typeId,
                BuilderClassName = builderClassName,
                Capabilities = uniqueCapabilities,
                IsInterface = typeRef?.IsInterface ?? false,
                TargetType = typeRef
            };
 
            builders.Add(builder);
        }
 
        // Also create builders for interface types (for use as return type wrappers)
        // These are needed when methods return interface types like IResourceWithConnectionString
        foreach (var (interfaceTypeId, caps) in interfaceCapabilities)
        {
            // Skip if already added (shouldn't happen, but be safe)
            if (capabilitiesByTypeId.ContainsKey(interfaceTypeId))
            {
                continue;
            }
 
            var builderClassName = DeriveClassName(interfaceTypeId);
 
            // Get the type ref from tracked metadata
            var typeRef = typeRefsByTypeId.GetValueOrDefault(interfaceTypeId);
 
            // Deduplicate capabilities
            var uniqueCapabilities = caps
                .GroupBy(c => c.CapabilityId)
                .Select(g => g.First())
                .ToList();
 
            var builder = new BuilderModel
            {
                TypeId = interfaceTypeId,
                BuilderClassName = builderClassName,
                Capabilities = uniqueCapabilities,
                IsInterface = true,
                TargetType = typeRef
            };
 
            builders.Add(builder);
        }
 
        // Also create builders for resource types referenced anywhere in capabilities
        // This handles types like RedisCommanderResource that appear in callback signatures,
        // return types, or parameter types but aren't capability targets
        var allReferencedTypeRefs = CollectAllReferencedTypes(capabilities);
 
        // Track all types we already have builders for (concrete + interface)
        var existingBuilderTypeIds = new HashSet<string>(capabilitiesByTypeId.Keys);
        foreach (var (interfaceTypeId, _) in interfaceCapabilities)
        {
            existingBuilderTypeIds.Add(interfaceTypeId);
        }
 
        foreach (var (typeId, typeRef) in allReferencedTypeRefs)
        {
            // Skip types we already have builders for (from concrete or interface lists)
            if (existingBuilderTypeIds.Contains(typeId))
            {
                continue;
            }
 
            // Only create builders for resource types (using metadata instead of string parsing)
            if (!typeRef.IsResourceBuilder)
            {
                continue;
            }
 
            var builderClassName = DeriveClassName(typeId);
            var builder = new BuilderModel
            {
                TypeId = typeId,
                BuilderClassName = builderClassName,
                Capabilities = [],  // No specific capabilities - uses base type methods
                IsInterface = typeRef.IsInterface,
                TargetType = typeRef
            };
            builders.Add(builder);
        }
 
        // Sort: concrete types first, then interfaces
        return builders
            .OrderBy(b => b.IsInterface)
            .ThenBy(b => b.BuilderClassName)
            .ToList();
    }
 
    /// <summary>
    /// Collects all type refs referenced in capabilities (return types, parameter types, callback types, etc.)
    /// Returns a dictionary mapping typeId to AtsTypeRef for use in builder creation.
    /// </summary>
    private static Dictionary<string, AtsTypeRef> CollectAllReferencedTypes(IReadOnlyList<AtsCapabilityInfo> capabilities)
    {
        var typeRefs = new Dictionary<string, AtsTypeRef>();
 
        void CollectFromTypeRef(AtsTypeRef? typeRef)
        {
            if (typeRef == null)
            {
                return;
            }
 
            if (!string.IsNullOrEmpty(typeRef.TypeId) && typeRef.Category == AtsTypeCategory.Handle)
            {
                typeRefs.TryAdd(typeRef.TypeId, typeRef);
            }
 
            // Also check nested types (generics, arrays, etc.)
            CollectFromTypeRef(typeRef.ElementType);
            CollectFromTypeRef(typeRef.KeyType);
            CollectFromTypeRef(typeRef.ValueType);
            if (typeRef.UnionTypes != null)
            {
                foreach (var unionType in typeRef.UnionTypes)
                {
                    CollectFromTypeRef(unionType);
                }
            }
        }
 
        foreach (var cap in capabilities)
        {
            // Check return type
            CollectFromTypeRef(cap.ReturnType);
 
            // Check parameter types
            foreach (var param in cap.Parameters)
            {
                CollectFromTypeRef(param.Type);
 
                // Check callback parameter types and return type
                if (param.IsCallback)
                {
                    if (param.CallbackParameters != null)
                    {
                        foreach (var cbParam in param.CallbackParameters)
                        {
                            CollectFromTypeRef(cbParam.Type);
                        }
                    }
                    CollectFromTypeRef(param.CallbackReturnType);
                }
            }
        }
 
        return typeRefs;
    }
 
    /// <summary>
    /// Gets entry point capabilities (those without TargetTypeId).
    /// </summary>
    private static List<AtsCapabilityInfo> GetEntryPointCapabilities(IReadOnlyList<AtsCapabilityInfo> capabilities)
    {
        return capabilities.Where(c => string.IsNullOrEmpty(c.TargetTypeId)).ToList();
    }
 
    /// <summary>
    /// Derives the class name from an ATS type ID.
    /// For interfaces like IResource, strips the leading 'I'.
    /// </summary>
    private static string DeriveClassName(string typeId)
    {
        var typeName = ExtractSimpleTypeName(typeId);
 
        // Strip leading 'I' from interface types
        if (typeName.StartsWith('I') && typeName.Length > 1 && char.IsUpper(typeName[1]))
        {
            return typeName[1..];
        }
 
        return typeName;
    }
 
    /// <summary>
    /// Gets the handle type alias name for a type ID.
    /// </summary>
    private static string GetHandleTypeName(string typeId)
    {
        var typeName = ExtractSimpleTypeName(typeId);
 
        // Sanitize generic types like "Dict<String,Object>" -> "DictStringObject"
        // and array types like "string[]" -> "stringArray"
        typeName = typeName
            .Replace("[]", "Array", StringComparison.Ordinal)
            .Replace("<", "", StringComparison.Ordinal)
            .Replace(">", "", StringComparison.Ordinal)
            .Replace(",", "", StringComparison.Ordinal);
 
        return $"{typeName}Handle";
    }
 
    /// <summary>
    /// Extracts the simple type name from a type ID.
    /// </summary>
    /// <example>
    /// "Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource" → "IResource"
    /// "Aspire.Hosting/Aspire.Hosting.DistributedApplication" → "DistributedApplication"
    /// </example>
    private static string ExtractSimpleTypeName(string typeId)
    {
        var slashIndex = typeId.LastIndexOf('/');
        var fullTypeName = slashIndex >= 0 ? typeId[(slashIndex + 1)..] : typeId;
 
        var dotIndex = fullTypeName.LastIndexOf('.');
        return dotIndex >= 0 ? fullTypeName[(dotIndex + 1)..] : fullTypeName;
    }
 
    /// <summary>
    /// Determines if a type has chainable methods and should have a Promise wrapper.
    /// Types with instance methods or wrapper methods get Promise wrappers.
    /// </summary>
    private static bool HasChainableMethods(BuilderModel model)
    {
        // Check for instance methods (from ExposeMethods=true) or wrapper methods
        return model.Capabilities.Any(c =>
            c.CapabilityKind == AtsCapabilityKind.InstanceMethod ||
            c.CapabilityKind == AtsCapabilityKind.Method);
    }
 
    /// <summary>
    /// Gets the Promise wrapper class name for a return type, if one exists.
    /// Returns null if the return type doesn't have a Promise wrapper.
    /// </summary>
    private string? GetPromiseWrapperForReturnType(AtsTypeRef? returnType)
    {
        if (returnType == null)
        {
            return null;
        }
 
        // Check if the return type has a Promise wrapper
        if (_typesWithPromiseWrappers.Contains(returnType.TypeId))
        {
            var className = _wrapperClassNames.GetValueOrDefault(returnType.TypeId)
                ?? DeriveClassName(returnType.TypeId);
            return $"{className}Promise";
        }
 
        return null;
    }
}