diff --git a/.github/skills/ci-test-failures/SKILL.md b/.github/skills/ci-test-failures/SKILL.md
index ad2213915e1..b7f5144bb5f 100644
--- a/.github/skills/ci-test-failures/SKILL.md
+++ b/.github/skills/ci-test-failures/SKILL.md
@@ -214,11 +214,23 @@ artifact_0_TestName_os/
├── testresults/
│ ├── TestName_net10.0_timestamp.trx # Test results XML
│ ├── Aspire.*.Tests_*.log # Console output
-│ └── recordings/ # Asciinema recordings (CLI E2E tests)
+│ ├── recordings/ # Asciinema recordings (CLI E2E tests)
+│ └── workspaces/ # Captured project workspaces (CLI E2E tests)
+│ └── TestClassName.MethodName/ # Full generated project for failed tests
+│ ├── apphost.ts
+│ ├── aspire.config.json
+│ ├── .modules/ # Generated SDK (aspire.js) - key for debugging
+│ └── ...
├── *.crash.dmp # Crash dump (if test crashed)
└── test.binlog # MSBuild binary log
```
+### CLI E2E Workspace Capture
+
+CLI E2E tests annotated with `[CaptureWorkspaceOnFailure]` automatically capture the full generated project workspace when a test fails. This includes the generated SDK (`.modules/aspire.js`), template output, and config files — critical for debugging template generation or `aspire run` failures.
+
+Look in `testresults/workspaces/{TestClassName.MethodName}/` inside the downloaded artifact.
+
## Parsing .trx Files
```powershell
diff --git a/.github/skills/cli-e2e-testing/SKILL.md b/.github/skills/cli-e2e-testing/SKILL.md
index b4cacde0f02..35fcf4f18e6 100644
--- a/.github/skills/cli-e2e-testing/SKILL.md
+++ b/.github/skills/cli-e2e-testing/SKILL.md
@@ -569,9 +569,31 @@ testresults/
├── Aspire.Cli.EndToEnd.Tests_*.log # Console output log
├── *.crash.dmp # Crash dump (if test crashed)
├── test.binlog # MSBuild binary log
-└── recordings/
- ├── CreateAndRunAspireStarterProject.cast # Asciinema recording
- └── ...
+├── recordings/
+│ ├── CreateAndRunAspireStarterProject.cast # Asciinema recording
+│ └── ...
+└── workspaces/ # Captured project workspaces (on failure)
+ └── TestClassName.MethodName/ # Full generated project for debugging
+ ├── apphost.ts
+ ├── aspire.config.json
+ ├── .modules/ # Generated SDK - check aspire.js for exports
+ └── ...
+```
+
+### Workspace Capture
+
+Tests annotated with `[CaptureWorkspaceOnFailure]` automatically copy the generated project workspace into the test artifacts when a test fails. This is invaluable for debugging template generation or `aspire run` failures — you can inspect the exact generated files including the SDK output in `.modules/aspire.js`.
+
+To add workspace capture to a new test:
+```csharp
+[Fact]
+[CaptureWorkspaceOnFailure]
+public async Task MyTemplateTest()
+{
+ var workspace = TemporaryWorkspace.Create(output);
+ // ... test code — workspace is automatically registered for capture ...
+}
+```
```
### One-Liner: Download Latest Recording
diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj
index 8a59cc48f27..109d9009369 100644
--- a/src/Aspire.Cli/Aspire.Cli.csproj
+++ b/src/Aspire.Cli/Aspire.Cli.csproj
@@ -212,6 +212,9 @@
false
+
+ false
+
diff --git a/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs b/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs
new file mode 100644
index 00000000000..7af3c455774
--- /dev/null
+++ b/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs
@@ -0,0 +1,160 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Configuration;
+using Aspire.Cli.Interaction;
+using Aspire.Cli.Projects;
+using Aspire.Cli.Resources;
+using Microsoft.Extensions.Logging;
+using Spectre.Console;
+
+namespace Aspire.Cli.Templating;
+
+internal sealed partial class CliTemplateFactory
+{
+ private async Task ApplyPythonStarterTemplateAsync(CallbackTemplate _, TemplateInputs inputs, System.CommandLine.ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ var projectName = inputs.Name;
+ if (string.IsNullOrWhiteSpace(projectName))
+ {
+ var defaultName = _executionContext.WorkingDirectory.Name;
+ projectName = await _prompter.PromptForProjectNameAsync(defaultName, cancellationToken);
+ }
+
+ if (string.IsNullOrWhiteSpace(inputs.Version))
+ {
+ _interactionService.DisplayError("Unable to determine Aspire version for the Python starter template.");
+ return new TemplateResult(ExitCodeConstants.InvalidCommand);
+ }
+
+ var aspireVersion = inputs.Version;
+ var outputPath = inputs.Output;
+ if (string.IsNullOrWhiteSpace(outputPath))
+ {
+ var defaultOutputPath = $"./{projectName}";
+ outputPath = await _prompter.PromptForOutputPath(defaultOutputPath, cancellationToken);
+ }
+ outputPath = Path.GetFullPath(outputPath, _executionContext.WorkingDirectory.FullName);
+
+ _logger.LogDebug("Applying Python starter template. ProjectName: {ProjectName}, OutputPath: {OutputPath}, AspireVersion: {AspireVersion}.", projectName, outputPath, aspireVersion);
+
+ var useLocalhostTld = await ResolveUseLocalhostTldAsync(parseResult, cancellationToken);
+ var useRedisCache = await ResolveUseRedisCacheAsync(parseResult, cancellationToken);
+
+ TemplateResult templateResult;
+ try
+ {
+ if (!Directory.Exists(outputPath))
+ {
+ Directory.CreateDirectory(outputPath);
+ }
+
+ templateResult = await _interactionService.ShowStatusAsync(
+ TemplatingStrings.CreatingNewProject,
+ async () =>
+ {
+ var projectNameLower = projectName.ToLowerInvariant();
+
+ var ports = GenerateRandomPorts();
+ var hostName = useLocalhostTld ? $"{projectNameLower}.dev.localhost" : "localhost";
+ var conditions = new Dictionary
+ {
+ ["redis"] = useRedisCache,
+ ["no-redis"] = !useRedisCache,
+ };
+ string ApplyAllTokens(string content) => ConditionalBlockProcessor.Process(
+ ApplyTokens(content, projectName, projectNameLower, aspireVersion, ports, hostName),
+ conditions);
+ _logger.LogDebug("Copying embedded Python starter template files to '{OutputPath}'.", outputPath);
+ await CopyTemplateTreeToDiskAsync("py-starter", outputPath, ApplyAllTokens, cancellationToken);
+
+ if (useRedisCache)
+ {
+ AddRedisPackageToConfig(outputPath, aspireVersion);
+ }
+
+ // Write channel to settings.json before restore so package resolution uses the selected channel.
+ if (!string.IsNullOrEmpty(inputs.Channel))
+ {
+ var config = AspireJsonConfiguration.Load(outputPath);
+ if (config is not null)
+ {
+ config.Channel = inputs.Channel;
+ config.Save(outputPath);
+ }
+ }
+
+ var appHostProject = _projectFactory.TryGetProject(new FileInfo(Path.Combine(outputPath, "apphost.ts")));
+ if (appHostProject is not IGuestAppHostSdkGenerator guestProject)
+ {
+ _interactionService.DisplayError("Automatic 'aspire restore' is unavailable for the new Python starter project because no TypeScript AppHost SDK generator was found.");
+ return new TemplateResult(ExitCodeConstants.FailedToBuildArtifacts, outputPath);
+ }
+
+ _logger.LogDebug("Generating SDK code for Python starter in '{OutputPath}'.", outputPath);
+ var restoreSucceeded = await guestProject.BuildAndGenerateSdkAsync(new DirectoryInfo(outputPath), cancellationToken);
+ if (!restoreSucceeded)
+ {
+ _interactionService.DisplayError("Automatic 'aspire restore' failed for the new Python starter project. Run 'aspire restore' in the project directory for more details.");
+ return new TemplateResult(ExitCodeConstants.FailedToBuildArtifacts, outputPath);
+ }
+
+ return new TemplateResult(ExitCodeConstants.Success, outputPath);
+ }, emoji: KnownEmojis.Rocket);
+
+ if (templateResult.ExitCode != ExitCodeConstants.Success)
+ {
+ return templateResult;
+ }
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ _interactionService.DisplayError($"Failed to create project files: {ex.Message}");
+ return new TemplateResult(ExitCodeConstants.FailedToCreateNewProject);
+ }
+
+ _interactionService.DisplaySuccess($"Created Python starter project at {outputPath.EscapeMarkup()}");
+ DisplayPostCreationInstructions(outputPath);
+
+ return templateResult;
+ }
+
+ private async Task ResolveUseRedisCacheAsync(System.CommandLine.ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ var redisCacheOptionSpecified = parseResult.Tokens.Any(token =>
+ string.Equals(token.Value, "--use-redis-cache", StringComparisons.CliInputOrOutput));
+ var useRedisCache = parseResult.GetValue(_useRedisCacheOption);
+ if (!redisCacheOptionSpecified)
+ {
+ if (!_hostEnvironment.SupportsInteractiveInput)
+ {
+ return false;
+ }
+
+ useRedisCache = await _interactionService.PromptForSelectionAsync(
+ TemplatingStrings.UseRedisCache_Prompt,
+ [TemplatingStrings.Yes, TemplatingStrings.No],
+ choice => choice,
+ cancellationToken) switch
+ {
+ var choice when string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput) => true,
+ var choice when string.Equals(choice, TemplatingStrings.No, StringComparisons.CliInputOrOutput) => false,
+ _ => throw new InvalidOperationException(TemplatingStrings.UseRedisCache_UnexpectedChoice)
+ };
+ }
+
+ if (useRedisCache ?? false)
+ {
+ _interactionService.DisplayMessage(KnownEmojis.CheckMark, TemplatingStrings.UseRedisCache_UsingRedisCache);
+ }
+
+ return useRedisCache ?? false;
+ }
+
+ private static void AddRedisPackageToConfig(string outputPath, string aspireVersion)
+ {
+ var config = AspireConfigFile.LoadOrCreate(outputPath);
+ config.AddOrUpdatePackage("Aspire.Hosting.Redis", aspireVersion);
+ config.Save(outputPath);
+ }
+}
diff --git a/src/Aspire.Cli/Templating/CliTemplateFactory.cs b/src/Aspire.Cli/Templating/CliTemplateFactory.cs
index 2df9982de00..f5e89c4cce1 100644
--- a/src/Aspire.Cli/Templating/CliTemplateFactory.cs
+++ b/src/Aspire.Cli/Templating/CliTemplateFactory.cs
@@ -37,6 +37,11 @@ internal sealed partial class CliTemplateFactory : ITemplateFactory
Description = TemplatingStrings.UseLocalhostTld_Description
};
+ private readonly Option _useRedisCacheOption = new("--use-redis-cache")
+ {
+ Description = TemplatingStrings.UseRedisCache_Description
+ };
+
private readonly ILanguageDiscovery _languageDiscovery;
private readonly IAppHostProjectFactory _projectFactory;
private readonly IScaffoldingService _scaffoldingService;
@@ -130,7 +135,20 @@ private IEnumerable GetTemplateDefinitions()
ApplyEmptyAppHostTemplateAsync,
runtime: TemplateRuntime.Cli,
languageId: KnownLanguageId.Java,
- isEmpty: true)
+ isEmpty: true),
+
+ new CallbackTemplate(
+ KnownTemplateId.PythonStarter,
+ "Starter App (FastAPI/React)",
+ projectName => $"./{projectName}",
+ cmd =>
+ {
+ AddOptionIfMissing(cmd, _localhostTldOption);
+ AddOptionIfMissing(cmd, _useRedisCacheOption);
+ },
+ ApplyPythonStarterTemplateAsync,
+ runtime: TemplateRuntime.Cli,
+ languageId: KnownLanguageId.TypeScript)
];
return templates.Where(IsTemplateAvailable);
diff --git a/src/Aspire.Cli/Templating/ConditionalBlockProcessor.cs b/src/Aspire.Cli/Templating/ConditionalBlockProcessor.cs
new file mode 100644
index 00000000000..cc8c96973aa
--- /dev/null
+++ b/src/Aspire.Cli/Templating/ConditionalBlockProcessor.cs
@@ -0,0 +1,105 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+
+namespace Aspire.Cli.Templating;
+
+///
+/// Processes conditional blocks in template content. Blocks are delimited by
+/// marker lines of the form {{#name}} / {{/name}}. When a block
+/// is included, the marker lines are stripped and the inner content is kept;
+/// when excluded, the marker lines and their content are removed entirely.
+/// Marker lines may contain leading comment characters (e.g. // {{#name}}
+/// or # {{#name}}) — the entire line is always removed.
+///
+///
+/// Blocks must not overlap or nest across different condition names. Each condition
+/// is processed independently in enumeration order. Overlapping blocks produce
+/// undefined behavior.
+///
+internal static partial class ConditionalBlockProcessor
+{
+ ///
+ /// Processes all conditional blocks for the given set of conditions. Each entry
+ /// in maps a block name to whether it should be included.
+ ///
+ /// The template content to process.
+ /// A set of block-name to include/exclude mappings.
+ /// The processed content with conditional blocks resolved.
+ internal static string Process(string content, IReadOnlyDictionary conditions)
+ {
+ foreach (var (blockName, include) in conditions)
+ {
+ content = ProcessBlock(content, blockName, include);
+ }
+
+ Debug.Assert(
+ !LeftoverMarkerPattern().IsMatch(content),
+ $"Template content contains unprocessed conditional markers. Ensure all block names are included in the conditions dictionary.");
+
+ return content;
+ }
+
+ [GeneratedRegex(@"\{\{[#/][a-zA-Z][\w-]*\}\}")]
+ private static partial Regex LeftoverMarkerPattern();
+
+ ///
+ /// Processes all occurrences of a single conditional block in the content.
+ ///
+ /// The template content to process.
+ /// The name of the conditional block (e.g. redis).
+ ///
+ /// When , the block content is kept and only the marker lines
+ /// are removed. When , the entire block (markers and content) is removed.
+ ///
+ /// The processed content.
+ internal static string ProcessBlock(string content, string blockName, bool include)
+ {
+ var startPattern = $"{{{{#{blockName}}}}}";
+ var endPattern = $"{{{{/{blockName}}}}}";
+
+ while (true)
+ {
+ var startIdx = content.IndexOf(startPattern, StringComparison.Ordinal);
+ if (startIdx < 0)
+ {
+ break;
+ }
+
+ var endIdx = content.IndexOf(endPattern, startIdx, StringComparison.Ordinal);
+ if (endIdx < 0)
+ {
+ throw new InvalidOperationException(
+ $"Template contains opening marker '{{{{#{blockName}}}}}' without a matching closing marker '{{{{/{blockName}}}}}'.");
+ }
+
+ // Find the full start marker line (including leading whitespace/comments and trailing newline).
+ var startLineBegin = content.LastIndexOf('\n', startIdx);
+ startLineBegin = startLineBegin < 0 ? 0 : startLineBegin + 1;
+ var startLineEnd = content.IndexOf('\n', startIdx);
+ startLineEnd = startLineEnd < 0 ? content.Length : startLineEnd + 1;
+
+ // Find the full end marker line.
+ var endLineBegin = content.LastIndexOf('\n', endIdx);
+ endLineBegin = endLineBegin < 0 ? 0 : endLineBegin + 1;
+ var endLineEnd = content.IndexOf('\n', endIdx);
+ endLineEnd = endLineEnd < 0 ? content.Length : endLineEnd + 1;
+
+ if (include)
+ {
+ // Keep the block content but remove the marker lines.
+ var blockContent = content[startLineEnd..endLineBegin];
+ content = string.Concat(content.AsSpan(0, startLineBegin), blockContent, content.AsSpan(endLineEnd));
+ }
+ else
+ {
+ // Remove everything from start marker line to end marker line (inclusive).
+ content = string.Concat(content.AsSpan(0, startLineBegin), content.AsSpan(endLineEnd));
+ }
+ }
+
+ return content;
+ }
+}
diff --git a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
index 3e0fd548a2b..a4788202094 100644
--- a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
+++ b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
@@ -158,17 +158,6 @@ private IEnumerable GetTemplatesCore(bool showAllTemplates, bool nonI
languageId: KnownLanguageId.CSharp
);
- yield return new CallbackTemplate(
- "aspire-py-starter",
- TemplatingStrings.AspirePyStarter_Description,
- projectName => $"./{projectName}",
- ApplyDevLocalhostTldOption,
- nonInteractive
- ? ApplySingleFileTemplateWithNoExtraArgsAsync
- : (template, inputs, parseResult, ct) => ApplySingleFileTemplate(template, inputs, parseResult, PromptForExtraAspirePythonStarterOptionsAsync, ct),
- languageId: KnownLanguageId.CSharp
- );
-
if (showAllTemplates)
{
yield return new CallbackTemplate(
@@ -290,16 +279,6 @@ private async Task PromptForExtraAspireSingleFileOptionsAsync(ParseRes
return extraArgs.ToArray();
}
- private async Task PromptForExtraAspirePythonStarterOptionsAsync(ParseResult result, CancellationToken cancellationToken)
- {
- var extraArgs = new List();
-
- await PromptForDevLocalhostTldOptionAsync(result, extraArgs, cancellationToken);
- await PromptForRedisCacheOptionAsync(result, extraArgs, cancellationToken);
-
- return extraArgs.ToArray();
- }
-
private async Task PromptForExtraAspireJsFrontendStarterOptionsAsync(ParseResult result, CancellationToken cancellationToken)
{
var extraArgs = new List();
diff --git a/src/Aspire.Cli/Templating/KnownTemplateId.cs b/src/Aspire.Cli/Templating/KnownTemplateId.cs
index 36216035a26..bf1efd806c7 100644
--- a/src/Aspire.Cli/Templating/KnownTemplateId.cs
+++ b/src/Aspire.Cli/Templating/KnownTemplateId.cs
@@ -32,4 +32,9 @@ internal static class KnownTemplateId
/// The template ID for the CLI Java empty AppHost template.
///
public const string JavaEmptyAppHost = "aspire-java-empty";
+
+ ///
+ /// The template ID for the Python starter template.
+ ///
+ public const string PythonStarter = "aspire-py-starter";
}
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/.dockerignore b/src/Aspire.Cli/Templating/Templates/py-starter/app/.dockerignore
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/.dockerignore
rename to src/Aspire.Cli/Templating/Templates/py-starter/app/.dockerignore
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/.python-version b/src/Aspire.Cli/Templating/Templates/py-starter/app/.python-version
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/.python-version
rename to src/Aspire.Cli/Templating/Templates/py-starter/app/.python-version
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/main.py b/src/Aspire.Cli/Templating/Templates/py-starter/app/main.py
similarity index 94%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/main.py
rename to src/Aspire.Cli/Templating/Templates/py-starter/app/main.py
index 4d0a2a5d1c4..572643d6d2f 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/main.py
+++ b/src/Aspire.Cli/Templating/Templates/py-starter/app/main.py
@@ -1,8 +1,8 @@
import contextlib
import datetime
-//#if UseRedisCache
+# {{#redis}}
import json
-//#endif
+# {{/redis}}
import logging
import os
import random
@@ -11,10 +11,10 @@
import fastapi.responses
import fastapi.staticfiles
import opentelemetry.instrumentation.fastapi as otel_fastapi
-//#if UseRedisCache
+# {{#redis}}
import opentelemetry.instrumentation.redis as otel_redis
import redis
-//#endif
+# {{/redis}}
import telemetry
@@ -27,7 +27,7 @@ async def lifespan(app):
app = fastapi.FastAPI(lifespan=lifespan)
otel_fastapi.FastAPIInstrumentor.instrument_app(app, exclude_spans=["send"])
-//#if UseRedisCache
+# {{#redis}}
# Create a global to store the Redis client.
redis_client = None
otel_redis.RedisInstrumentor().instrument()
@@ -54,7 +54,7 @@ def get_redis_client():
)
return redis_client
-//#endif
+# {{/redis}}
logger = logging.getLogger(__name__)
@@ -66,7 +66,7 @@ async def root():
return "API service is running. Navigate to /api/weatherforecast to see sample data."
@app.get("/api/weatherforecast")
-//#if UseRedisCache
+# {{#redis}}
async def weather_forecast(redis_client=fastapi.Depends(get_redis_client)):
"""Weather forecast endpoint."""
cache_key = "weatherforecast"
@@ -82,10 +82,11 @@ async def weather_forecast(redis_client=fastapi.Depends(get_redis_client)):
except Exception as e:
logger.warning(f"Redis cache read error: {e}")
-//#else
+# {{/redis}}
+# {{#no-redis}}
async def weather_forecast():
"""Weather forecast endpoint."""
-//#endif
+# {{/no-redis}}
# Generate fresh data if not in cache or cache unavailable.
summaries = [
"Freezing",
@@ -112,7 +113,7 @@ async def weather_forecast():
}
forecast.append(forecast_item)
-//#if UseRedisCache
+# {{#redis}}
# Cache the data
if redis_client:
try:
@@ -120,20 +121,21 @@ async def weather_forecast():
except Exception as e:
logger.warning(f"Redis cache write error: {e}")
-//#endif
+# {{/redis}}
return forecast
@app.get("/health", response_class=fastapi.responses.PlainTextResponse)
-//#if UseRedisCache
+# {{#redis}}
async def health_check(redis_client=fastapi.Depends(get_redis_client)):
"""Health check endpoint."""
if redis_client:
redis_client.ping()
-//#else
+# {{/redis}}
+# {{#no-redis}}
async def health_check():
"""Health check endpoint."""
-//#endif
+# {{/no-redis}}
return "Healthy"
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/pyproject.toml b/src/Aspire.Cli/Templating/Templates/py-starter/app/pyproject.toml
similarity index 92%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/pyproject.toml
rename to src/Aspire.Cli/Templating/Templates/py-starter/app/pyproject.toml
index c8f2d9e1138..5f47caff95f 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/pyproject.toml
+++ b/src/Aspire.Cli/Templating/Templates/py-starter/app/pyproject.toml
@@ -8,8 +8,8 @@ dependencies = [
"opentelemetry-distro>=0.59b0",
"opentelemetry-exporter-otlp-proto-grpc>=1.38.0",
"opentelemetry-instrumentation-fastapi>=0.59b0",
-//#if UseRedisCache
+# {{#redis}}
"opentelemetry-instrumentation-redis>=0.59b0",
"redis>=6.4.0",
-//#endif
-]
\ No newline at end of file
+# {{/redis}}
+]
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/telemetry.py b/src/Aspire.Cli/Templating/Templates/py-starter/app/telemetry.py
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/telemetry.py
rename to src/Aspire.Cli/Templating/Templates/py-starter/app/telemetry.py
diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts b/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts
new file mode 100644
index 00000000000..e51954bafab
--- /dev/null
+++ b/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts
@@ -0,0 +1,31 @@
+import { createBuilder } from './.modules/aspire.js';
+
+const builder = await createBuilder();
+
+// {{#redis}}
+// Add a Redis cache for the app to use.
+const cache = await builder
+ .addRedis("cache");
+
+// {{/redis}}
+// Run the Python FastAPI app and expose its HTTP endpoint externally.
+const app = await builder
+ .addUvicornApp("app", "./app", "main:app")
+ .withUv()
+ .withExternalHttpEndpoints()
+// {{#redis}}
+ .withReference(cache)
+ .waitFor(cache)
+// {{/redis}}
+ .withHttpHealthCheck("/health");
+
+// Run the Vite frontend after the API and inject the API URL for local proxying.
+const frontend = await builder
+ .addViteApp("frontend", "./frontend")
+ .withReference(app)
+ .waitFor(app);
+
+// Bundle the frontend build output into the API container for publish/deploy.
+await app.publishWithContainerFiles(frontend, "./static");
+
+await builder.build().run();
diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/aspire.config.json b/src/Aspire.Cli/Templating/Templates/py-starter/aspire.config.json
new file mode 100644
index 00000000000..89488be7ad4
--- /dev/null
+++ b/src/Aspire.Cli/Templating/Templates/py-starter/aspire.config.json
@@ -0,0 +1,27 @@
+{
+ "appHost": {
+ "path": "apphost.ts",
+ "language": "typescript/nodejs"
+ },
+ "packages": {
+ "Aspire.Hosting.JavaScript": "{{aspireVersion}}",
+ "Aspire.Hosting.Python": "{{aspireVersion}}"
+ },
+ "profiles": {
+ "https": {
+ "applicationUrl": "https://{{hostName}}:{{httpsPort}};http://{{hostName}}:{{httpPort}}",
+ "environmentVariables": {
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://{{hostName}}:{{otlpHttpsPort}}",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://{{hostName}}:{{resourceHttpsPort}}"
+ }
+ },
+ "http": {
+ "applicationUrl": "http://{{hostName}}:{{httpPort}}",
+ "environmentVariables": {
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://{{hostName}}:{{otlpHttpPort}}",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://{{hostName}}:{{resourceHttpPort}}",
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
+ }
+ }
+ }
+}
diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs b/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs
new file mode 100644
index 00000000000..e7e33edb3fd
--- /dev/null
+++ b/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs
@@ -0,0 +1,17 @@
+// @ts-check
+
+import { defineConfig } from 'eslint/config';
+import tseslint from 'typescript-eslint';
+
+export default defineConfig({
+ files: ['apphost.ts'],
+ extends: [tseslint.configs.base],
+ languageOptions: {
+ parserOptions: {
+ projectService: true,
+ },
+ },
+ rules: {
+ '@typescript-eslint/no-floating-promises': ['error', { checkThenables: true }],
+ },
+});
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/.dockerignore b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/.dockerignore
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/.dockerignore
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/.dockerignore
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/.gitignore b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/.gitignore
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/.gitignore
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/.gitignore
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/eslint.config.js b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/eslint.config.js
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/eslint.config.js
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/eslint.config.js
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/index.html b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/index.html
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/index.html
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/index.html
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/package-lock.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/package-lock.json
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/package-lock.json
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/package-lock.json
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/package.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/package.json
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/package.json
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/package.json
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/public/Aspire.png b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/public/Aspire.png
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/public/Aspire.png
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/public/Aspire.png
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/public/github.svg b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/public/github.svg
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/public/github.svg
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/public/github.svg
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.css b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/App.css
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.css
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/App.css
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.tsx b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/App.tsx
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.tsx
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/App.tsx
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/index.css b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/index.css
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/index.css
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/index.css
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/main.tsx b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/main.tsx
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/main.tsx
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/main.tsx
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/vite-env.d.ts b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/vite-env.d.ts
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/vite-env.d.ts
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/vite-env.d.ts
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.app.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.app.json
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.app.json
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.app.json
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.json
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.json
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.json
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.node.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.node.json
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.node.json
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.node.json
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/vite.config.ts b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/vite.config.ts
similarity index 100%
rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/vite.config.ts
rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/vite.config.ts
diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/package-lock.json b/src/Aspire.Cli/Templating/Templates/py-starter/package-lock.json
new file mode 100644
index 00000000000..c750c10ace7
--- /dev/null
+++ b/src/Aspire.Cli/Templating/Templates/py-starter/package-lock.json
@@ -0,0 +1,2042 @@
+{
+ "name": "{{projectNameLower}}",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "{{projectNameLower}}",
+ "dependencies": {
+ "vscode-jsonrpc": "^8.2.0"
+ },
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "eslint": "^10.0.3",
+ "nodemon": "^3.1.14",
+ "tsx": "^4.21.0",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.57.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
+ "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
+ "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
+ "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
+ "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
+ "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
+ "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
+ "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
+ "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
+ "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
+ "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
+ "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
+ "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
+ "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
+ "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
+ "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
+ "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
+ "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
+ "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
+ "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
+ "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
+ "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.3",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz",
+ "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz",
+ "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.1.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz",
+ "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz",
+ "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz",
+ "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.1.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz",
+ "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.57.1",
+ "@typescript-eslint/type-utils": "8.57.1",
+ "@typescript-eslint/utils": "8.57.1",
+ "@typescript-eslint/visitor-keys": "8.57.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.57.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz",
+ "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.57.1",
+ "@typescript-eslint/types": "8.57.1",
+ "@typescript-eslint/typescript-estree": "8.57.1",
+ "@typescript-eslint/visitor-keys": "8.57.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz",
+ "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.57.1",
+ "@typescript-eslint/types": "^8.57.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz",
+ "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.1",
+ "@typescript-eslint/visitor-keys": "8.57.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz",
+ "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz",
+ "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.1",
+ "@typescript-eslint/typescript-estree": "8.57.1",
+ "@typescript-eslint/utils": "8.57.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz",
+ "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz",
+ "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.57.1",
+ "@typescript-eslint/tsconfig-utils": "8.57.1",
+ "@typescript-eslint/types": "8.57.1",
+ "@typescript-eslint/visitor-keys": "8.57.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz",
+ "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.57.1",
+ "@typescript-eslint/types": "8.57.1",
+ "@typescript-eslint/typescript-estree": "8.57.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz",
+ "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
+ "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.4",
+ "@esbuild/android-arm": "0.27.4",
+ "@esbuild/android-arm64": "0.27.4",
+ "@esbuild/android-x64": "0.27.4",
+ "@esbuild/darwin-arm64": "0.27.4",
+ "@esbuild/darwin-x64": "0.27.4",
+ "@esbuild/freebsd-arm64": "0.27.4",
+ "@esbuild/freebsd-x64": "0.27.4",
+ "@esbuild/linux-arm": "0.27.4",
+ "@esbuild/linux-arm64": "0.27.4",
+ "@esbuild/linux-ia32": "0.27.4",
+ "@esbuild/linux-loong64": "0.27.4",
+ "@esbuild/linux-mips64el": "0.27.4",
+ "@esbuild/linux-ppc64": "0.27.4",
+ "@esbuild/linux-riscv64": "0.27.4",
+ "@esbuild/linux-s390x": "0.27.4",
+ "@esbuild/linux-x64": "0.27.4",
+ "@esbuild/netbsd-arm64": "0.27.4",
+ "@esbuild/netbsd-x64": "0.27.4",
+ "@esbuild/openbsd-arm64": "0.27.4",
+ "@esbuild/openbsd-x64": "0.27.4",
+ "@esbuild/openharmony-arm64": "0.27.4",
+ "@esbuild/sunos-x64": "0.27.4",
+ "@esbuild/win32-arm64": "0.27.4",
+ "@esbuild/win32-ia32": "0.27.4",
+ "@esbuild/win32-x64": "0.27.4"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz",
+ "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.3",
+ "@eslint/config-helpers": "^0.5.2",
+ "@eslint/core": "^1.1.1",
+ "@eslint/plugin-kit": "^0.6.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.1.1",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
+ "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.13.6",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
+ "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz",
+ "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.57.1",
+ "@typescript-eslint/parser": "8.57.1",
+ "@typescript-eslint/typescript-estree": "8.57.1",
+ "@typescript-eslint/utils": "8.57.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vscode-jsonrpc": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
+ "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/package.json b/src/Aspire.Cli/Templating/Templates/py-starter/package.json
new file mode 100644
index 00000000000..7878cd35dbe
--- /dev/null
+++ b/src/Aspire.Cli/Templating/Templates/py-starter/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "{{projectNameLower}}",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "lint": "eslint apphost.ts",
+ "predev": "npm run lint",
+ "dev": "aspire run",
+ "prebuild": "npm run lint",
+ "build": "tsc",
+ "watch": "tsc --watch"
+ },
+ "dependencies": {
+ "vscode-jsonrpc": "^8.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "eslint": "^10.0.3",
+ "nodemon": "^3.1.14",
+ "tsx": "^4.21.0",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.57.1"
+ }
+}
diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json b/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json
new file mode 100644
index 00000000000..1e44664428b
--- /dev/null
+++ b/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "strict": true
+ }
+}
diff --git a/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj b/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj
index edcfe1a96af..ffdcba714f2 100644
--- a/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj
+++ b/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj
@@ -45,16 +45,10 @@
$(IntermediateOutputPath)content\templates\aspire-apphost-singlefile\%(RecursiveDir)%(Filename)%(Extension)
-
-
- content/templates/aspire-py-starter/%(RecursiveDir)
- $(IntermediateOutputPath)content\templates\aspire-py-starter\%(RecursiveDir)%(Filename)%(Extension)
-
-
+ Exclude="$(MSBuildThisFileDirectory)templates\**\bin\**;$(MSBuildThisFileDirectory)templates\**\obj\**;$(MSBuildThisFileDirectory)templates\**\*.csproj;$(MSBuildThisFileDirectory)templates\aspire-apphost-singlefile\**\apphost.cs">
content/templates/%(RecursiveDir)
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/dotnetcli.host.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/dotnetcli.host.json
deleted file mode 100644
index 69f2bcb8b28..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/dotnetcli.host.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "$schema": "http://json.schemastore.org/dotnetcli.host",
- "symbolInfo": {
- "NoHttps": {
- "longName": "no-https",
- "shortName": ""
- },
- "UseRedisCache": {
- "longName": "use-redis-cache",
- "shortName": ""
- },
- "LocalhostTld": {
- "longName": "localhost-tld",
- "shortName": ""
- }
- },
- "usageExamples": []
-}
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/ide.host.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/ide.host.json
deleted file mode 100644
index d19d3fe8687..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/ide.host.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/ide.host",
- "unsupportedHosts": [
- {
- "id": "vs"
- }
- ],
- "symbolInfo": [
- {
- "id": "UseRedisCache",
- "isVisible": true,
- "persistenceScope": "templateGroup"
- },
- {
- "id": "LocalhostTld",
- "isVisible": true,
- "persistenceScope": "templateGroup"
- }
- ]
-}
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.cs.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.cs.json
deleted file mode 100644
index 43a941d3199..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.cs.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Úvodní aplikace Aspire (FastAPI/React)",
- "description": "Šablona projektu pro vytvoření aplikace Aspire s front-endem Reactu a službou back-endového rozhraní API Pythonu s volitelným ukládáním do mezipaměti Redis",
- "symbols/UseRedisCache/displayName": "Po_užít Redis pro ukládání do mezipaměti (vyžaduje Docker)",
- "symbols/UseRedisCache/description": "Nakonfiguruje, jestli se má aplikace nastavit tak, aby pro ukládání do mezipaměti používala Redis. K místnímu spouštění se vyžaduje podporovaný modul runtime kontejneru. Další podrobnosti najdete na https://aka.ms/aspire/containers.",
- "symbols/appHostHttpPort/description": "Číslo portu, který se má použít pro koncový bod HTTP v launchSettings.json projektu AppHost.",
- "symbols/appHostOtlpHttpPort/description": "Číslo portu, který se má použít pro koncový bod HTTP OTLP v launchSettings.json projektu AppHost.",
- "symbols/appHostResourceHttpPort/description": "Číslo portu, který se má použít pro koncový bod HTTP služby prostředků v launchSettings.json projektu AppHost.",
- "symbols/appHostHttpsPort/description": "Číslo portu, který se má použít pro koncový bod HTTPS v launchSettings.json projektu AppHost. Tato možnost se dá použít jenom v případě, že se nepoužívá parametr no-https.",
- "symbols/appHostOtlpHttpsPort/description": "Číslo portu, který se má použít pro koncový bod HTTPS OTLP v launchSettings.json projektu AppHost.",
- "symbols/appHostResourceHttpsPort/description": "Číslo portu, který se má použít pro koncový bod HTTPS služby prostředků v launchSettings.json projektu AppHost.",
- "symbols/NoHttps/description": "Určuje, jestli se má vypnout protokol HTTPS.",
- "symbols/LocalhostTld/displayName": "Použití TLD .dev.localhost v adrese URL aplikace",
- "symbols/LocalhostTld/description": "Určuje, jestli se má název projektu zkombinovat s TLD .dev.localhost v adrese URL aplikace pro místní vývoj, například https://myapp.dev.localhost:12345. Podporováno v .NET 10 a novějších verzích."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.de.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.de.json
deleted file mode 100644
index 9b84c7a820a..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.de.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Aspire-Starter-App (FastAPI/React)",
- "description": "Eine Projektvorlage zum Erstellen einer Aspire-App mit einem React-Front-End- und einem Python-Back-End-API-Dienst, optional mit Redis-Zwischenspeicherung.",
- "symbols/UseRedisCache/displayName": "_Use Redis für die Zwischenspeicherung (erfordert eine unterstützte Container-Runtime)",
- "symbols/UseRedisCache/description": "Legt fest, ob die Anwendung für die Verwendung von Redis zum Zwischenspeichern eingerichtet werden soll. Erfordert eine unterstützte Containerruntime für die lokale Ausführung. Weitere Informationen finden Sie unter https://aka.ms/aspire/containers.",
- "symbols/appHostHttpPort/description": "Portnummer, die für den HTTP-Endpunkt in launchSettings.json des AppHost-Projekts verwendet werden soll.",
- "symbols/appHostOtlpHttpPort/description": "Portnummer, die für den OTLP-HTTP-Endpunkt in launchSettings.json des AppHost-Projekts verwendet werden soll.",
- "symbols/appHostResourceHttpPort/description": "Portnummer, die für den HTTP-Endpunkt des Ressourcendiensts in launchSettings.json des AppHost-Projekts verwendet werden soll.",
- "symbols/appHostHttpsPort/description": "Portnummer, die für den HTTPS-Endpunkt in launchSettings.json des AppHost-Projekts verwendet werden soll. Diese Option ist nur anwendbar, wenn der Parameter no-https nicht verwendet wird.",
- "symbols/appHostOtlpHttpsPort/description": "Portnummer, die für den OTLP-HTTPS-Endpunkt in launchSettings.json des AppHost-Projekts verwendet werden soll.",
- "symbols/appHostResourceHttpsPort/description": "Portnummer, die für den HTTPS-Endpunkt des Ressourcendiensts in launchSettings.json des AppHost-Projekts verwendet werden soll.",
- "symbols/NoHttps/description": "Ob HTTPS deaktiviert werden soll.",
- "symbols/LocalhostTld/displayName": "Verwenden der .dev.localhost-TLD in der Anwendungs-URL",
- "symbols/LocalhostTld/description": "Gibt an, ob der Projektname mit der .dev.localhost-TLD in der Anwendungs-URL für die lokale Entwicklung kombiniert werden soll, z. B. https://myapp.dev.localhost:12345. Unterstützt unter .NET 10 und höher."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.en.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.en.json
deleted file mode 100644
index cbdb3adc4ae..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.en.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Aspire Starter App (FastAPI/React)",
- "description": "A project template for creating an Aspire app with a React frontend and a Python backend API service, with optional Redis caching.",
- "symbols/UseRedisCache/displayName": "_Use Redis for caching (requires a supported container runtime)",
- "symbols/UseRedisCache/description": "Configures whether to setup the application to use Redis for caching. Requires a supported container runtime to run locally, see https://aka.ms/aspire/containers for more details.",
- "symbols/appHostHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json of the AppHost project.",
- "symbols/appHostOtlpHttpPort/description": "Port number to use for the OTLP HTTP endpoint in launchSettings.json of the AppHost project.",
- "symbols/appHostResourceHttpPort/description": "Port number to use for the resource service HTTP endpoint in launchSettings.json of the AppHost project.",
- "symbols/appHostHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json of the AppHost project. This option is only applicable when the parameter no-https is not used.",
- "symbols/appHostOtlpHttpsPort/description": "Port number to use for the OTLP HTTPS endpoint in launchSettings.json of the AppHost project.",
- "symbols/appHostResourceHttpsPort/description": "Port number to use for the resource service HTTPS endpoint in launchSettings.json of the AppHost project.",
- "symbols/NoHttps/description": "Whether to turn off HTTPS.",
- "symbols/LocalhostTld/displayName": "Use the .dev.localhost TLD in the application URL",
- "symbols/LocalhostTld/description": "Whether to combine the project name with the .dev.localhost TLD in the application URL for local development, e.g. https://myapp.dev.localhost:12345. Supported on .NET 10 and later."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.es.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.es.json
deleted file mode 100644
index c918c1744da..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.es.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Aplicación starter de Starter (FastAPI/React)",
- "description": "Plantilla de proyecto para crear una aplicación Desi con un front-end de React y un servicio de API de back-end de Python, con almacenamiento en caché de Redis opcional.",
- "symbols/UseRedisCache/displayName": "_Use Redis para el almacenamiento en caché (requiere un runtime de contenedor compatible)",
- "symbols/UseRedisCache/description": "Configure si se va a configurar la aplicación para que use Redis para el almacenamiento en caché. Requiere un entorno de ejecución de contenedor compatible para ejecutarse localmente. Consulte https://aka.ms/aspire/containers para obtener más detalles.",
- "symbols/appHostHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP en launchSettings.json del proyecto AppHost.",
- "symbols/appHostOtlpHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP de OTLP en launchSettings.json del proyecto AppHost.",
- "symbols/appHostResourceHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP del servicio de recursos en launchSettings.json del proyecto AppHost.",
- "symbols/appHostHttpsPort/description": "Número de puerto que se va a usar para el punto de conexión HTTPS en launchSettings.json del proyecto AppHost. Esta opción solo es aplicable cuando no se usa el parámetro no-https.",
- "symbols/appHostOtlpHttpsPort/description": "Número de puerto que se va a usar para el punto de conexión HTTPS de OTLP en launchSettings.json del proyecto AppHost.",
- "symbols/appHostResourceHttpsPort/description": "Número de puerto a usar para el punto de conexión HTTPS del servicio de recursos en launchSettings.json del proyecto AppHost.",
- "symbols/NoHttps/description": "Si se va a desactivar HTTPS.",
- "symbols/LocalhostTld/displayName": "Usar el TLD .dev.localhost en la dirección URL de la aplicación",
- "symbols/LocalhostTld/description": "Si se debe combinar el nombre del proyecto con el TLD .dev.localhost en la URL de la aplicación para el desarrollo local, por ejemplo, https://myapp.dev.localhost:12345. Compatible con .NET 10 y versiones posteriores."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.fr.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.fr.json
deleted file mode 100644
index eb1bbcf8f3e..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.fr.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Application de démarrage Aspire (FastAPI/React)",
- "description": "Modèle de projet pour créer une application Aspire avec un front-end React et un service API back-end en Python, avec une mise en cache Redis optionnelle.",
- "symbols/UseRedisCache/displayName": "_Use Redis pour la mise en cache (nécessite un runtime de conteneur pris en charge)",
- "symbols/UseRedisCache/description": "Permet la configuration s’il faut configurer l’application afin qu’elle utilise Redis pour la mise en cache. Nécessite un runtime du conteneur pris en charge pour fonctionner localement, voir https://aka.ms/aspire/containers pour obtenir plus d’informations.",
- "symbols/appHostHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP dans launchSettings.json du projet AppHost.",
- "symbols/appHostOtlpHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP OTLP dans launchSettings.json du projet AppHost.",
- "symbols/appHostResourceHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP du service de ressources dans launchSettings.json du projet AppHost.",
- "symbols/appHostHttpsPort/description": "Numéro de port à utiliser pour le point de terminaison HTTPS dans launchSettings.json du projet AppHost. Cette option n'est applicable que lorsque le paramètre no-https n'est pas utilisé.",
- "symbols/appHostOtlpHttpsPort/description": "Numéro de port à utiliser pour le point de terminaison HTTPS OTLP dans launchSettings.json du projet AppHost.",
- "symbols/appHostResourceHttpsPort/description": "Numéro de port à utiliser pour le point de terminaison HTTPS du service de ressources dans launchSettings.json du projet AppHost.",
- "symbols/NoHttps/description": "Indique s’il faut désactiver HTTPS.",
- "symbols/LocalhostTld/displayName": "Utilisez le TLD .dev.localhost dans l’URL de l’application",
- "symbols/LocalhostTld/description": "Indique s’il faut combiner le nom du projet avec le fichier TLD .dev.localhost dans l’URL de l’application pour le développement local, par exemple https://myapp.dev.localhost:12345. Pris en charge sur .NET 10 et versions ultérieures."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.it.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.it.json
deleted file mode 100644
index bf1d2ad2ecd..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.it.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "App Starter Aspire (FastAPI/React)",
- "description": "Modello di progetto per la creazione di un'app Aspire con front-end React e servizio API back-end Python, con cache Redis opzionale.",
- "symbols/UseRedisCache/displayName": "_Usare Redis per la memorizzazione nella cache (richiede un runtime del contenitore supportato)",
- "symbols/UseRedisCache/description": "Configura se impostare l'applicazione per l'utilizzo di Redis per la memorizzazione nella cache. Richiede l'esecuzione locale di un runtime del contenitore supportato. Per altri dettagli, vedere https://aka.ms/aspire/containers.",
- "symbols/appHostHttpPort/description": "Numero di porta da usare per l'endpoint HTTP in launchSettings.json. del progetto AppHost.",
- "symbols/appHostOtlpHttpPort/description": "Numero di porta da usare per l'endpoint OTLP HTTP in launchSettings.json. del progetto AppHost.",
- "symbols/appHostResourceHttpPort/description": "Numero di porta da usare per l'endpoint HTTP del servizio risorse in launchSettings.json del progetto AppHost.",
- "symbols/appHostHttpsPort/description": "Numero di porta da usare per l'endpoint HTTPS in launchSettings.json. del progetto AppHost. Questa opzione è applicabile solo quando il parametro no-https non viene usato.",
- "symbols/appHostOtlpHttpsPort/description": "Numero di porta da usare per l'endpoint OTLP HTTPS in launchSettings.json. del progetto AppHost.",
- "symbols/appHostResourceHttpsPort/description": "Numero di porta da usare per l'endpoint HTTPS del servizio risorse in launchSettings.json del progetto AppHost.",
- "symbols/NoHttps/description": "Indica se disattivare HTTPS.",
- "symbols/LocalhostTld/displayName": "Usa il TLD .dev.localhost nell'URL dell'applicazione",
- "symbols/LocalhostTld/description": "Indica se combinare il nome del progetto con il dominio di primo livello .dev.localhost nell'URL dell'applicazione per lo sviluppo in locale, ad esempio https://myapp.dev.localhost:12345. Supportato in .NET 10 e versioni successive."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ja.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ja.json
deleted file mode 100644
index 6c0ced89797..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ja.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Aspire スターター アプリ (FastAPI/React)",
- "description": "React フロントエンドと Python バックエンド API サービスを使用して Aspire アプリを作成するためのプロジェクト テンプレート。必要に応じて、Redis キャッシュを使用します。",
- "symbols/UseRedisCache/displayName": "キャッシュ用に Redis を使用する (サポートされているコンテナー ランタイムが必要) (_U)",
- "symbols/UseRedisCache/description": "Redis をキャッシュに使用するようにアプリケーションを設定するかどうかを構成します。ローカルで実行するには、サポートされているコンテナー ランタイムが必要です。詳細については、https://aka.ms/aspire/containers を参照してください。",
- "symbols/appHostHttpPort/description": "AppHost プロジェクトの launchSettings.json の HTTP エンドポイントに使用するポート番号。",
- "symbols/appHostOtlpHttpPort/description": "AppHost プロジェクトの launchSettings.json で OTLP HTTP エンドポイントに使用するポート番号。",
- "symbols/appHostResourceHttpPort/description": "AppHost プロジェクトの launchSettings.json のリソース サービス HTTP エンドポイントに使用するポート番号。",
- "symbols/appHostHttpsPort/description": "AppHost プロジェクトの launchSettings.json の HTTPS エンドポイントに使用するポート番号。このオプションは、パラメーター no-https を使用しない場合にのみ適用されます。",
- "symbols/appHostOtlpHttpsPort/description": "AppHost プロジェクトの launchSettings.json で OTLP HTTPS エンドポイントに使用するポート番号。",
- "symbols/appHostResourceHttpsPort/description": "AppHost プロジェクトの launchSettings.json のリソース サービス HTTPS エンドポイントに使用するポート番号。",
- "symbols/NoHttps/description": "HTTPS をオフにするかどうか。",
- "symbols/LocalhostTld/displayName": "アプリケーション URL で .dev.localhost TLD を使用する",
- "symbols/LocalhostTld/description": "ローカル開発用のアプリケーション URL 内で、プロジェクト名を .dev.localhost TLD と組み合わせるかどうか (例: https://myapp.dev.localhost:12345)。.NET 10 以降でサポートされています。"
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ko.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ko.json
deleted file mode 100644
index 67d44613363..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ko.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Aspire 시작 앱(FastAPI/React)",
- "description": "선택적 Redis 캐싱을 사용하여 React 프런트 엔드와 Python 백 엔드 API 서비스로 Aspire 앱을 만들기 위한 프로젝트 템플릿입니다.",
- "symbols/UseRedisCache/displayName": "캐싱용 _Use Redis(지원되는 컨테이너 런타임 필요)",
- "symbols/UseRedisCache/description": "캐싱에 Redis를 사용하도록 애플리케이션을 설정할지 여부를 구성합니다. 로컬로 실행하려면 지원되는 컨테이너 런타임이 필요합니다. 자세한 내용은 https://aka.ms/aspire/containers를 참조하세요.",
- "symbols/appHostHttpPort/description": "AppHost 프로젝트의 launchSettings.json HTTP 엔드포인트에 사용할 포트 번호입니다.",
- "symbols/appHostOtlpHttpPort/description": "AppHost 프로젝트의 launchSettings.json OTLP HTTP 엔드포인트에 사용할 포트 번호입니다.",
- "symbols/appHostResourceHttpPort/description": "AppHost 프로젝트의 launchSettings.json 리소스 서비스 HTTP 엔드포인트에 사용할 포트 번호입니다.",
- "symbols/appHostHttpsPort/description": "AppHost 프로젝트의 launchSettings.json HTTPS 엔드포인트에 사용할 포트 번호입니다. 이 옵션은 no-https 매개 변수가 사용되지 않는 경우에만 적용됩니다.",
- "symbols/appHostOtlpHttpsPort/description": "AppHost 프로젝트의 launchSettings.json OTLP HTTPS 엔드포인트에 사용할 포트 번호입니다.",
- "symbols/appHostResourceHttpsPort/description": "AppHost 프로젝트의 launchSettings.json 리소스 서비스 HTTPS 엔드포인트에 사용할 포트 번호입니다.",
- "symbols/NoHttps/description": "HTTPS를 끌지 여부입니다.",
- "symbols/LocalhostTld/displayName": "애플리케이션 URL에서 .dev.localhost TLD를 사용하세요.",
- "symbols/LocalhostTld/description": "로컬 개발을 위한 애플리케이션 URL에서 프로젝트 이름과 .dev.localhost TLD를 결합할지 여부(예: https://myapp.dev.localhost:12345)입니다. .NET 10 이상에서 지원됩니다."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pl.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pl.json
deleted file mode 100644
index 47245ae7a9f..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pl.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Aplikacja startowa Aspire (FastAPI/React)",
- "description": "Szablon projektu do tworzenia aplikacji Aspire z frontonem React i wewnętrzną bazą danych interfejsu API w Pythonie, z opcjonalnym buforowaniem Redis.",
- "symbols/UseRedisCache/displayName": "_Skorzystaj z magazynu danych Redis na potrzeby buforowania (wymaga obsługiwanego środowiska uruchomieniowego kontenera)",
- "symbols/UseRedisCache/description": "Określa, czy konfigurować aplikację do korzystania z magazynu danych Redis na potrzeby buforowania. Wymaga obsługiwanego środowiska uruchomieniowego kontenera na potrzeby uruchomienia lokalnego. Aby uzyskać więcej informacji, zobacz https://aka.ms/aspire/containers.",
- "symbols/appHostHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP w pliku launchSettings.json projektu AppHost.",
- "symbols/appHostOtlpHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP OTLP w pliku launchSettings.json projektu AppHost.",
- "symbols/appHostResourceHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP usługi zasobów w pliku launchSettings.json projektu AppHost.",
- "symbols/appHostHttpsPort/description": "Numer portu do użycia dla punktu końcowego HTTPS w pliku launchSettings.json projektu AppHost. Ta opcja ma zastosowanie tylko wtedy, gdy parametr no-https nie jest używany.",
- "symbols/appHostOtlpHttpsPort/description": "Numer portu do użycia dla punktu końcowego HTTPS OTLP w pliku launchSettings.json projektu AppHost.",
- "symbols/appHostResourceHttpsPort/description": "Numer portu do użycia dla punktu końcowego HTTPS usługi zasobów w pliku launchSettings.json projektu AppHost.",
- "symbols/NoHttps/description": "Określa, czy wyłączyć protokół HTTPS.",
- "symbols/LocalhostTld/displayName": "Użyj pliku .dev.localhost TLD w adresie URL aplikacji",
- "symbols/LocalhostTld/description": "Określa, czy połączyć nazwę projektu z TLD .dev.localhost w adresie URL aplikacji na potrzeby lokalnego programowania, np. https://myapp.dev.localhost:12345. Obsługiwane na platformie .NET 10 lub nowszej."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pt-BR.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pt-BR.json
deleted file mode 100644
index d86ab883ff3..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pt-BR.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "App de Inicialização Aspire (FastAPI/React)",
- "description": "Um modelo de projeto para criar um aplicativo Aspire com um front-end do React e um serviço de API de back-end do Python, com cache Redis opcional.",
- "symbols/UseRedisCache/displayName": "_Usar o Redis para cache (requer um tempo de execução de contêiner compatível)",
- "symbols/UseRedisCache/description": "Configura se o aplicativo deve usar Redis para cache. Requer um runtime de contêiner com suporte para execução local; confira https://aka.ms/aspire/containers para obter mais detalhes.",
- "symbols/appHostHttpPort/description": "Número da porta a ser usado para o ponto de extremidade HTTP launchSettings.json do projeto AppHost.",
- "symbols/appHostOtlpHttpPort/description": "Número da porta a ser usado para o ponto de extremidade HTTP OTLP launchSettings.json do projeto AppHost.",
- "symbols/appHostResourceHttpPort/description": "Número da porta a ser usado para o ponto de extremidade HTTP do serviço de recurso launchSettings.json do projeto AppHost.",
- "symbols/appHostHttpsPort/description": "Número da porta a ser usado para o ponto de extremidade HTTPS launchSettings.json do projeto AppHost. Essa opção só é aplicável quando o parâmetro no-https não é usado.",
- "symbols/appHostOtlpHttpsPort/description": "Número da porta a ser usado para o ponto de extremidade HTTPS OTLP launchSettings.json do projeto AppHost.",
- "symbols/appHostResourceHttpsPort/description": "Número da porta a ser usado para o ponto de extremidade HTTPS do serviço de recurso launchSettings.json do projeto AppHost.",
- "symbols/NoHttps/description": "Se o HTTPS deve ser desativado.",
- "symbols/LocalhostTld/displayName": "Usar o TLD .dev.localhost na URL do aplicativo",
- "symbols/LocalhostTld/description": "Se quiser combinar o nome do projeto com o TLD .dev.localhost na URL do aplicativo para desenvolvimento local, por exemplo, https://myapp.dev.localhost:12345. Com suporte no .NET 10 e versões posteriores."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ru.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ru.json
deleted file mode 100644
index 7fe0cfa0546..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ru.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Майкрософт",
- "name": "Начальное приложение Aspire (FastAPI/React)",
- "description": "Шаблон проекта для создания приложения Aspire с интерфейсной частью React и службой API серверной части на Python с необязательным кэшированием Redis.",
- "symbols/UseRedisCache/displayName": "_Использовать Redis для кэширования (требуется поддерживаемая среда выполнения контейнера)",
- "symbols/UseRedisCache/description": "Определяет, указывать ли приложению, что для кэширования следует использовать Redis. Для локального запуска требуется поддерживаемая среда выполнения контейнера. Дополнительные сведения см. на странице https://aka.ms/aspire/containers.",
- "symbols/appHostHttpPort/description": "Номер порта, который будет использоваться для конечной точки HTTP в файле launchSettings.json проекта AppHost.",
- "symbols/appHostOtlpHttpPort/description": "Номер порта, который будет использоваться для конечной точки HTTP OTLP в файле launchSettings.json проекта AppHost.",
- "symbols/appHostResourceHttpPort/description": "Номер порта, который будет использоваться для конечной точки HTTP службы ресурсов в файле launchSettings.json проекта AppHost.",
- "symbols/appHostHttpsPort/description": "Номер порта, который будет использоваться для конечной точки HTTPS в файле launchSettings.json проекта AppHost. Этот параметр применим только в том случае, если NO-HTTP не используется.",
- "symbols/appHostOtlpHttpsPort/description": "Номер порта, который будет использоваться для конечной точки OTLP HTTPS в файле launchSettings.json проекта AppHost.",
- "symbols/appHostResourceHttpsPort/description": "Номер порта, который будет использоваться для конечной точки HTTPS службы ресурсов в файле launchSettings.json проекта AppHost.",
- "symbols/NoHttps/description": "Следует ли отключить HTTPS.",
- "symbols/LocalhostTld/displayName": "Используйте домен верхнего уровня .dev.localhost в URL-адресе приложения",
- "symbols/LocalhostTld/description": "Следует ли объединять имя проекта с доменом верхнего уровня .dev.localhost в URL-адресе приложения для локальной разработки, например https://myapp.dev.localhost:12345. Поддерживается в .NET 10 и более поздних версиях."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.tr.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.tr.json
deleted file mode 100644
index c93f2ed0898..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.tr.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Aspire Başlangıç Uygulaması (FastAPI/React)",
- "description": "İsteğe bağlı Redis önbelleğiyle bir React ön uç ve Python arka uç API hizmetini içeren bir Aspire uygulaması oluşturmak için proje şablonu.",
- "symbols/UseRedisCache/displayName": "Önbelleğe alma için Redis’i k_ullan (desteklenen bir kapsayıcı çalışma zamanı gerektirir)",
- "symbols/UseRedisCache/description": "Uygulamanın önbelleğe alma için Redis’i kullanmak üzere ayarlanıp ayarlanmayacağını yapılandırır. Yerel olarak çalıştırmak için desteklenen bir kapsayıcı çalışma zamanı gerektirir. Daha fazla ayrıntı için https://aka.ms/aspire/containers sayfasına bakın.",
- "symbols/appHostHttpPort/description": "AppHost projesinin HTTP uç noktası launchSettings.json bağlantı noktası numarası.",
- "symbols/appHostOtlpHttpPort/description": "AppHost projesinin OTLP HTTP uç noktası launchSettings.json bağlantı noktası numarası.",
- "symbols/appHostResourceHttpPort/description": "AppHost projesinin kaynak hizmeti HTTP uç noktası launchSettings.json bağlantı noktası numarası.",
- "symbols/appHostHttpsPort/description": "AppHost projesinin HTTPS uç noktası launchSettings.json bağlantı noktası numarası. Bu seçenek yalnızca no-https parametresi kullanılmadığında uygulanabilir.",
- "symbols/appHostOtlpHttpsPort/description": "AppHost projesinin OTLP HTTPS uç noktası launchSettings.json bağlantı noktası numarası.",
- "symbols/appHostResourceHttpsPort/description": "AppHost projesinin kaynak hizmeti HTTPS uç noktası launchSettings.json bağlantı noktası numarası.",
- "symbols/NoHttps/description": "HTTPS'nin kapatılıp kapatılmayacağı.",
- "symbols/LocalhostTld/displayName": "Uygulama URL'sinde .dev.localhost TLD'sini kullanın.",
- "symbols/LocalhostTld/description": "Yerel geliştirme için uygulama URL'sinde proje adının .dev.localhost TLD ile birleştirilip birleştirilmeyeceği, örneğin https://myapp.dev.localhost:12345. .NET 10 ve sonraki sürümlerde desteklenir."
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hans.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hans.json
deleted file mode 100644
index 154115b92ad..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hans.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Aspire 入门应用(FastAPI/React)",
- "description": "用于使用 React 前端和 Python 后端 API 服务创建 Aspire 应用的项目模板,可选择使用 Redis 缓存。",
- "symbols/UseRedisCache/displayName": "使用 Redis 进行缓存(需要受支持的容器运行时)(_U)",
- "symbols/UseRedisCache/description": "配置是否将应用程序设置为使用 Redis 进行缓存。需要支持的容器运行时才能在本地运行,有关详细信息,请访问 https://aka.ms/aspire/containers for more details。",
- "symbols/appHostHttpPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的 HTTP 终结点。",
- "symbols/appHostOtlpHttpPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的 OTLP HTTP 终结点。",
- "symbols/appHostResourceHttpPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的资源服务 HTTP 终结点。",
- "symbols/appHostHttpsPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的 HTTPS 终结点。仅当不使用参数 no-https 时,此选项才适用。",
- "symbols/appHostOtlpHttpsPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的 OTLP HTTPS 终结点。",
- "symbols/appHostResourceHttpsPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的资源服务 HTTPS 终结点。",
- "symbols/NoHttps/description": "是否禁用 HTTPS。",
- "symbols/LocalhostTld/displayName": "在应用程序 URL 中使用 .dev.localhost TLD",
- "symbols/LocalhostTld/description": "是否在应用程序 URL 中将项目名称与 .dev.localhost TLD 合并以用于本地开发,例如 https://myapp.dev.localhost:12345。在 .NET 10 及更高版本中受支持。"
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hant.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hant.json
deleted file mode 100644
index 053127d5ae8..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hant.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "author": "Microsoft",
- "name": "Aspire 入門應用程式 (FastAPI/React)",
- "description": "用於建立 Aspire 應用程式的專案範本,其中包含 React 前端與 Python 後端 API 服務,並可選用 Redis 快取功能。",
- "symbols/UseRedisCache/displayName": "使用 Redis 進行快取 (需要支援的容器執行階段)(_U)",
- "symbols/UseRedisCache/description": "設定是否要將應用程式設為使用 Redis 進行快取。需要支援的容器執行階段,才能在本機執行,如需詳細資料,請參閱 https://aka.ms/aspire/containers。",
- "symbols/appHostHttpPort/description": "要用於 AppHost 專案 launchSettings.json 中 HTTP 端點的連接埠號碼。",
- "symbols/appHostOtlpHttpPort/description": "要用於 AppHost 專案 launchSettings.json 中 OTLP HTTP 端點的連接埠號碼。",
- "symbols/appHostResourceHttpPort/description": "要用於 AppHost 專案 launchSettings.json 中資源服務 HTTP 端點的連接埠號碼。",
- "symbols/appHostHttpsPort/description": "要用於 AppHost 專案 launchSettings.json 中 HTTPS 端點的連接埠號碼。只有在未使用參數 no-https 時,才適用此選項。",
- "symbols/appHostOtlpHttpsPort/description": "要用於 AppHost 專案 launchSettings.json 中 OTLP HTTPS 端點的連接埠號碼。",
- "symbols/appHostResourceHttpsPort/description": "要用於 AppHost 專案 launchSettings.json 中資源服務 HTTPS 端點的連接埠號碼。",
- "symbols/NoHttps/description": "是否要關閉 HTTPS。",
- "symbols/LocalhostTld/displayName": "在應用程式 URL 中使用 .dev.localhost TLD",
- "symbols/LocalhostTld/description": "是否要在用於本機開發的應用程式 URL 中將專案名稱結合 .dev.localhost TLD,例如 https://myapp.dev.localhost:12345。在 .NET 10 及更新版本上支援。"
-}
\ No newline at end of file
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/template.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/template.json
deleted file mode 100644
index e6ae244d653..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/template.json
+++ /dev/null
@@ -1,245 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/template",
- "author": "Microsoft",
- "classifications": [
- "Common",
- "Aspire",
- "Cloud",
- "Web",
- "Web API",
- "API",
- "Service",
- "Python",
- "TypeScript",
- "React"
- ],
- "name": "Aspire Starter App (FastAPI/React)",
- "defaultName": "AspirePyApp",
- "description": "A project template for creating an Aspire app with a React frontend and a Python backend API service, with optional Redis caching.",
- "shortName": "aspire-py-starter",
- "sourceName": "AspirePyStarter.1",
- "preferNameDirectory": true,
- "tags": {
- "language": "Python",
- "type": "project"
- },
- "precedence": "9000",
- "identity": "Aspire.Starter.TypeScript.Python.!!REPLACE_WITH_LATEST_MAJOR_MINOR_VERSION!!",
- "thirdPartyNotices": "https://aka.ms/aspire/third-party-notices",
- "groupIdentity": "Aspire.Starter.TypeScript.Python",
- "guids": [
- "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
- "B2C3D4E5-F678-90AB-CDEF-123456789012"
- ],
- "sources": [
- {
- "source": "./",
- "target": "./"
- }
- ],
- "symbols": {
- "UseRedisCache": {
- "type": "parameter",
- "datatype": "bool",
- "defaultValue": "false",
- "displayName": "_Use Redis for caching (requires a supported container runtime)",
- "description": "Configures whether to setup the application to use Redis for caching. Requires a supported container runtime to run locally, see https://aka.ms/aspire/containers for more details."
- },
- "appHostHttpPort": {
- "type": "parameter",
- "datatype": "integer",
- "description": "Port number to use for the HTTP endpoint in launchSettings.json of the AppHost project."
- },
- "appHostHttpPortGenerated": {
- "type": "generated",
- "generator": "port",
- "parameters": {
- "low": 15000,
- "high": 15300
- }
- },
- "appHostHttpPortReplacer": {
- "type": "generated",
- "generator": "coalesce",
- "parameters": {
- "sourceVariableName": "appHostHttpPort",
- "fallbackVariableName": "appHostHttpPortGenerated"
- },
- "replaces": "15000"
- },
- "appHostOtlpHttpPort": {
- "type": "parameter",
- "datatype": "integer",
- "description": "Port number to use for the OTLP HTTP endpoint in launchSettings.json of the AppHost project."
- },
- "appHostOtlpHttpPortGenerated": {
- "type": "generated",
- "generator": "port",
- "parameters": {
- "low": 19000,
- "high": 19300
- }
- },
- "appHostOtlpHttpPortReplacer": {
- "type": "generated",
- "generator": "coalesce",
- "parameters": {
- "sourceVariableName": "appHostOtlpHttpPort",
- "fallbackVariableName": "appHostOtlpHttpPortGenerated"
- },
- "replaces": "19000"
- },
- "appHostResourceHttpPort": {
- "type": "parameter",
- "datatype": "integer",
- "description": "Port number to use for the resource service HTTP endpoint in launchSettings.json of the AppHost project."
- },
- "appHostResourceHttpPortGenerated": {
- "type": "generated",
- "generator": "port",
- "parameters": {
- "low": 20000,
- "high": 20300
- }
- },
- "appHostResourceHttpPortReplacer": {
- "type": "generated",
- "generator": "coalesce",
- "parameters": {
- "sourceVariableName": "appHostResourceHttpPort",
- "fallbackVariableName": "appHostResourceHttpPortGenerated"
- },
- "replaces": "20000"
- },
- "appHostHttpsPort": {
- "type": "parameter",
- "datatype": "integer",
- "description": "Port number to use for the HTTPS endpoint in launchSettings.json of the AppHost project. This option is only applicable when the parameter no-https is not used."
- },
- "appHostHttpsPortGenerated": {
- "type": "generated",
- "generator": "port",
- "parameters": {
- "low": 17000,
- "high": 17300
- }
- },
- "appHostHttpsPortReplacer": {
- "type": "generated",
- "generator": "coalesce",
- "parameters": {
- "sourceVariableName": "appHostHttpsPort",
- "fallbackVariableName": "appHostHttpsPortGenerated"
- },
- "replaces": "17000"
- },
- "appHostOtlpHttpsPort": {
- "type": "parameter",
- "datatype": "integer",
- "description": "Port number to use for the OTLP HTTPS endpoint in launchSettings.json of the AppHost project."
- },
- "appHostOtlpHttpsPortGenerated": {
- "type": "generated",
- "generator": "port",
- "parameters": {
- "low": 21000,
- "high": 21300
- }
- },
- "appHostOtlpHttpsPortReplacer": {
- "type": "generated",
- "generator": "coalesce",
- "parameters": {
- "sourceVariableName": "appHostOtlpHttpsPort",
- "fallbackVariableName": "appHostOtlpHttpsPortGenerated"
- },
- "replaces": "21000"
- },
- "appHostResourceHttpsPort": {
- "type": "parameter",
- "datatype": "integer",
- "description": "Port number to use for the resource service HTTPS endpoint in launchSettings.json of the AppHost project."
- },
- "appHostResourceHttpsPortGenerated": {
- "type": "generated",
- "generator": "port",
- "parameters": {
- "low": 22000,
- "high": 22300
- }
- },
- "appHostResourceHttpsPortReplacer": {
- "type": "generated",
- "generator": "coalesce",
- "parameters": {
- "sourceVariableName": "appHostResourceHttpsPort",
- "fallbackVariableName": "appHostResourceHttpsPortGenerated"
- },
- "replaces": "22000"
- },
- "HasHttpsProfile": {
- "type": "computed",
- "value": "(!NoHttps)"
- },
- "NoHttps": {
- "type": "parameter",
- "datatype": "bool",
- "defaultValue": "false",
- "description": "Whether to turn off HTTPS."
- },
- "hostIdentifier": {
- "type": "bind",
- "binding": "HostIdentifier"
- },
- "LocalhostTld": {
- "type": "parameter",
- "datatype": "bool",
- "defaultValue": "false",
- "displayName": "Use the .dev.localhost TLD in the application URL",
- "description": "Whether to combine the project name with the .dev.localhost TLD in the application URL for local development, e.g. https://myapp.dev.localhost:12345. Supported on .NET 10 and later."
- },
- "hostName": {
- "type": "derived",
- "valueSource": "name",
- "valueTransform": "lowerCaseInvariantWithHyphens",
- "replaces": "LocalhostTldHostNamePrefix"
- }
- },
- "forms": {
- "lowerCaseInvariantWithHyphens": {
- "identifier": "chain",
- "steps": [
- "lowerCaseInvariant",
- "replaceDnsInvalidCharsWithHyphens",
- "replaceRepeatedHyphens",
- "trimLeadingHyphens",
- "trimTrailingHyphens"
- ]
- },
- "replaceDnsInvalidCharsWithHyphens": {
- "identifier": "replace",
- "pattern": "[^a-z0-9-]",
- "replacement": "-"
- },
- "replaceRepeatedHyphens": {
- "identifier": "replace",
- "pattern": "-{2,}",
- "replacement": "-"
- },
- "trimLeadingHyphens": {
- "identifier": "replace",
- "pattern": "^-+",
- "replacement": ""
- },
- "trimTrailingHyphens": {
- "identifier": "replace",
- "pattern": "-+$",
- "replacement": ""
- }
- },
- "primaryOutputs": [
- {
- "path": "apphost.cs"
- }
- ]
-}
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.cs b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.cs
deleted file mode 100644
index 663ef2a5745..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-#:sdk Aspire.AppHost.Sdk@!!REPLACE_WITH_LATEST_VERSION!!
-#:package Aspire.Hosting.JavaScript@!!REPLACE_WITH_LATEST_VERSION!!
-#:package Aspire.Hosting.Python@!!REPLACE_WITH_LATEST_VERSION!!
-#if UseRedisCache
-#:package Aspire.Hosting.Redis@!!REPLACE_WITH_LATEST_VERSION!!
-#endif
-
-var builder = DistributedApplication.CreateBuilder(args);
-
-#if UseRedisCache
-var cache = builder.AddRedis("cache");
-
-#endif
-var app = builder.AddUvicornApp("app", "./app", "main:app")
- .WithUv()
- .WithExternalHttpEndpoints()
-#if UseRedisCache
- .WithReference(cache)
- .WaitFor(cache)
-#endif
- .WithHttpHealthCheck("/health");
-
-var frontend = builder.AddViteApp("frontend", "./frontend")
- .WithReference(app)
- .WaitFor(app);
-
-app.PublishWithContainerFiles(frontend, "./static");
-
-builder.Build().Run();
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.run.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.run.json
deleted file mode 100644
index 83df4bced7d..00000000000
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.run.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/launchsettings.json",
- "profiles": {
- //#if (HasHttpsProfile)
- "https": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- //#if (LocalhostTld)
- "applicationUrl": "https://LocalhostTldHostNamePrefix.dev.localhost:17000;http://LocalhostTldHostNamePrefix.dev.localhost:15000",
- //#else
- "applicationUrl": "https://localhost:17000;http://localhost:15000",
- //#endif
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "DOTNET_ENVIRONMENT": "Development",
- "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21000",
- "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22000"
- }
- },
- //#endif
- "http": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- //#if (LocalhostTld)
- "applicationUrl": "http://LocalhostTldHostNamePrefix.dev.localhost:15000",
- //#else
- "applicationUrl": "http://localhost:15000",
- //#endif
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "DOTNET_ENVIRONMENT": "Development",
- "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19000",
- "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20000"
- }
- }
- }
-}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CaptureWorkspaceOnFailureAttribute.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CaptureWorkspaceOnFailureAttribute.cs
new file mode 100644
index 00000000000..8dc12d480a0
--- /dev/null
+++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CaptureWorkspaceOnFailureAttribute.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+using Xunit;
+using Xunit.v3;
+
+namespace Aspire.Cli.EndToEnd.Tests.Helpers;
+
+///
+/// When applied to a test method, captures directories to
+/// testresults/workspaces/{testName}/ on test failure so the generated
+/// files are uploaded as CI artifacts for debugging.
+///
+/// Register paths to capture via TestContext.Current.KeyValueStorage:
+///
+/// - "WorkspacePath" — the primary workspace directory
+/// - "CapturePath:{label}" — additional directories to capture under the given label
+///
+/// Workspace capture is automatic when using .
+///
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+internal sealed class CaptureWorkspaceOnFailureAttribute : BeforeAfterTestAttribute
+{
+ public override void After(MethodInfo methodUnderTest, IXunitTest test)
+ {
+ if (TestContext.Current.TestState?.Result is not TestResult.Failed)
+ {
+ return;
+ }
+
+ var testName = $"{test.TestCase.TestClassName}.{methodUnderTest.Name}";
+
+ try
+ {
+ // Capture primary workspace
+ if (TestContext.Current.KeyValueStorage.TryGetValue("WorkspacePath", out var value) &&
+ value is string workspacePath &&
+ Directory.Exists(workspacePath))
+ {
+ CliE2ETestHelpers.CaptureDirectory(workspacePath, testName, label: null);
+ }
+
+ // Capture additional registered paths (e.g., "CapturePath:aspire-home" → ~/.aspire)
+ foreach (var kvp in TestContext.Current.KeyValueStorage)
+ {
+ if (kvp.Key.StartsWith("CapturePath:", StringComparison.Ordinal) &&
+ kvp.Value is string path &&
+ Directory.Exists(path))
+ {
+ var label = kvp.Key["CapturePath:".Length..];
+ CliE2ETestHelpers.CaptureDirectory(path, testName, label);
+ }
+ }
+ }
+ catch
+ {
+ // Don't fail the test because of capture issues.
+ }
+ }
+}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs
index 72fce5931f5..9312fba4c22 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs
@@ -46,6 +46,11 @@ internal static async Task PrepareDockerEnvironmentAsync(
await auto.TypeAsync($"cd /workspace/{workspace.WorkspaceRoot.Name}");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
+
+ // Set up EXIT trap to copy .aspire diagnostics to workspace for CI capture
+ await auto.TypeAsync($"trap 'cp -r ~/.aspire/logs /workspace/{workspace.WorkspaceRoot.Name}/.aspire-logs 2>/dev/null; cp -r ~/.aspire/packages /workspace/{workspace.WorkspaceRoot.Name}/.aspire-packages 2>/dev/null' EXIT");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
}
}
@@ -300,14 +305,29 @@ await auto.TypeAsync(
throw new InvalidOperationException("aspire start failed. Check terminal output for CLI logs.");
}
- // Extract dashboard URL and verify it's reachable
+ // Extract dashboard URL and verify it's reachable.
+ // First check if the apphost crashed (aspire start exits 0 but prints error).
await auto.TypeAsync(
$"DASHBOARD_URL=$(sed -n " +
- "'s/.*\"dashboardUrl\"[[:space:]]*:[[:space:]]*\"\\(https:\\/\\/localhost:[0-9]*\\).*/\\1/p' " +
+ "'s/.*\"dashboardUrl\"[[:space:]]*:[[:space:]]*\"\\(https\\?:\\/\\/localhost:[0-9]*\\).*/\\1/p' " +
$"{jsonFile} | head -1)");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
+ // If DASHBOARD_URL is empty, the apphost likely crashed — dump logs for diagnostics
+ await auto.TypeAsync(
+ "if [ -z \"$DASHBOARD_URL\" ]; then " +
+ "echo 'dashboard-url-empty'; " +
+ "echo '=== ASPIRE START JSON ==='; cat " + jsonFile + "; echo '=== END JSON ==='; " +
+ "echo '=== ALL LOGS ==='; ls -lt ~/.aspire/logs/ 2>/dev/null; echo '=== END LIST ==='; " +
+ "DETACH_LOG=$(ls -t ~/.aspire/logs/cli_*detach*.log 2>/dev/null | head -1); " +
+ "echo \"=== DETACH LOG: $DETACH_LOG ===\"; [ -n \"$DETACH_LOG\" ] && tail -200 \"$DETACH_LOG\"; echo '=== END DETACH ==='; " +
+ "CLI_LOG=$(ls -t ~/.aspire/logs/cli_*.log 2>/dev/null | head -1); " +
+ "echo \"=== CLI LOG: $CLI_LOG ===\"; [ -n \"$CLI_LOG\" ] && tail -100 \"$CLI_LOG\"; echo '=== END CLI ==='; " +
+ "fi");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
await auto.TypeAsync(
"curl -ksSL -o /dev/null -w 'dashboard-http-%{http_code}' \"$DASHBOARD_URL\" " +
"|| echo 'dashboard-http-failed'");
@@ -327,4 +347,24 @@ internal static async Task AspireStopAsync(
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
}
+
+ ///
+ /// Copies interesting diagnostic directories from ~/.aspire to the mounted workspace
+ /// so they are captured by . Call this before
+ /// exiting the container. Copies logs and NuGet restore output (libs directories).
+ ///
+ internal static async Task CaptureAspireDiagnosticsAsync(
+ this Hex1bTerminalAutomator auto,
+ SequenceCounter counter,
+ TemporaryWorkspace workspace)
+ {
+ var containerWorkspace = $"/workspace/{workspace.WorkspaceRoot.Name}";
+
+ // Copy CLI logs
+ await auto.TypeAsync($"cp -r ~/.aspire/logs {containerWorkspace}/.aspire-logs 2>/dev/null; " +
+ $"cp -r ~/.aspire/packages {containerWorkspace}/.aspire-packages 2>/dev/null; " +
+ "echo done");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+ }
}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs
index 0216971c0f0..17f25e78934 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs
@@ -376,4 +376,58 @@ internal static string ToContainerPath(string hostPath, TemporaryWorkspace works
var relativePath = Path.GetRelativePath(workspace.WorkspaceRoot.FullName, hostPath);
return $"/workspace/{workspace.WorkspaceRoot.Name}/" + relativePath.Replace('\\', '/');
}
+
+ ///
+ /// Copies a directory to testresults/workspaces/{testName}/{label} for CI artifact upload.
+ /// Renames dot-prefixed directories to underscore-prefixed (upload-artifact skips hidden files).
+ ///
+ internal static void CaptureDirectory(string sourcePath, string testName, string? label)
+ {
+ var destDir = Path.Combine(
+ AppContext.BaseDirectory,
+ "TestResults",
+ "workspaces",
+ testName);
+
+ if (label is not null)
+ {
+ destDir = Path.Combine(destDir, label);
+ }
+
+ using var logWriter = new StreamWriter(Path.Combine(
+ Directory.CreateDirectory(destDir).FullName,
+ "_capture.log"));
+
+ CopyDirectory(sourcePath, destDir, line => logWriter.WriteLine(line));
+ }
+
+ private static void CopyDirectory(string sourceDir, string destDir, Action? log)
+ {
+ Directory.CreateDirectory(destDir);
+
+ log?.Invoke($"DIR: {sourceDir} ({Directory.GetFiles(sourceDir).Length} files, {Directory.GetDirectories(sourceDir).Length} dirs)");
+
+ foreach (var file in Directory.GetFiles(sourceDir))
+ {
+ var destFile = Path.Combine(destDir, Path.GetFileName(file));
+ File.Copy(file, destFile, overwrite: true);
+ }
+
+ foreach (var dir in Directory.GetDirectories(sourceDir))
+ {
+ var dirName = Path.GetFileName(dir);
+
+ // Skip node_modules — too large for artifacts
+ if (dirName.Equals("node_modules", StringComparison.OrdinalIgnoreCase))
+ {
+ log?.Invoke($" SKIP: {dirName}");
+ continue;
+ }
+
+ // Rename dot-prefixed dirs to underscore-prefixed
+ // (upload-artifact uses include-hidden-files: false by default)
+ var destDirName = dirName.StartsWith('.') ? "_" + dirName[1..] : dirName;
+ CopyDirectory(dir, Path.Combine(destDir, destDirName), log);
+ }
+ }
}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs
index 729773b008d..9b80af4288f 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs
@@ -4,7 +4,6 @@
using Aspire.Cli.EndToEnd.Tests.Helpers;
using Aspire.Cli.Tests.Utils;
using Hex1b.Automation;
-using Hex1b.Input;
using Xunit;
namespace Aspire.Cli.EndToEnd.Tests;
@@ -16,6 +15,7 @@ namespace Aspire.Cli.EndToEnd.Tests;
public sealed class PythonReactTemplateTests(ITestOutputHelper output)
{
[Fact]
+ [CaptureWorkspaceOnFailure]
public async Task CreateAndRunPythonReactProject()
{
var repoRoot = CliE2ETestHelpers.GetRepoRoot();
@@ -32,16 +32,20 @@ public async Task CreateAndRunPythonReactProject()
await auto.PrepareDockerEnvironmentAsync(counter, workspace);
await auto.InstallAspireCliInDockerAsync(installMode, counter);
+ // Step 1: Create project using aspire new, selecting the FastAPI/React template
await auto.AspireNewAsync("AspirePyReactApp", counter, template: AspireTemplate.PythonReact, useRedisCache: false);
- // Run the project with aspire run
- await auto.TypeAsync("aspire run");
+ // Step 2: Navigate into the project directory so config resolution finds the
+ // project-level aspire.config.json (which has the packages section).
+ // See https://github.com/microsoft/aspire/issues/15623
+ await auto.TypeAsync("cd AspirePyReactApp");
await auto.EnterAsync();
- await auto.WaitUntilTextAsync("Press CTRL+C to stop the apphost and exit.", timeout: TimeSpan.FromMinutes(2));
-
- await auto.Ctrl().KeyAsync(Hex1bKey.C);
await auto.WaitForSuccessPromptAsync(counter);
+ // Step 3: Start and stop the project
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
await auto.TypeAsync("exit");
await auto.EnterAsync();
diff --git a/tests/Aspire.Cli.Tests/Templating/ConditionalBlockProcessorTests.cs b/tests/Aspire.Cli.Tests/Templating/ConditionalBlockProcessorTests.cs
new file mode 100644
index 00000000000..44969530ce4
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Templating/ConditionalBlockProcessorTests.cs
@@ -0,0 +1,457 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Templating;
+
+namespace Aspire.Cli.Tests.Templating;
+
+public class ConditionalBlockProcessorTests
+{
+ // ──────────────────────────────────────────────────────────────────────
+ // Basic include / exclude
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Include_KeepsContent_RemovesMarkers()
+ {
+ var input = """
+ before
+ // {{#feature}}
+ included content
+ // {{/feature}}
+ after
+ """;
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "feature", include: true);
+
+ Assert.Contains("included content", result);
+ Assert.DoesNotContain("{{#feature}}", result);
+ Assert.DoesNotContain("{{/feature}}", result);
+ Assert.Contains("before", result);
+ Assert.Contains("after", result);
+ }
+
+ [Fact]
+ public void Exclude_RemovesContentAndMarkers()
+ {
+ var input = """
+ before
+ // {{#feature}}
+ excluded content
+ // {{/feature}}
+ after
+ """;
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "feature", include: false);
+
+ Assert.DoesNotContain("excluded content", result);
+ Assert.DoesNotContain("{{#feature}}", result);
+ Assert.DoesNotContain("{{/feature}}", result);
+ Assert.Contains("before", result);
+ Assert.Contains("after", result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // Comment style variations
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Handles_DoubleSlash_CommentMarkers()
+ {
+ var input = "line1\n// {{#test}}\nkept\n// {{/test}}\nline2\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal("line1\nkept\nline2\n", result);
+ }
+
+ [Fact]
+ public void Handles_Hash_CommentMarkers()
+ {
+ var input = "line1\n# {{#test}}\nkept\n# {{/test}}\nline2\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal("line1\nkept\nline2\n", result);
+ }
+
+ [Fact]
+ public void Handles_Bare_Markers_WithoutCommentPrefix()
+ {
+ var input = "line1\n{{#test}}\nkept\n{{/test}}\nline2\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal("line1\nkept\nline2\n", result);
+ }
+
+ [Fact]
+ public void Handles_IndentedMarkers()
+ {
+ var input = "line1\n // {{#test}}\n kept\n // {{/test}}\nline2\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal("line1\n kept\nline2\n", result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // Multiple blocks of same type
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Include_MultipleBlocks_AllKept()
+ {
+ var input = "a\n// {{#x}}\nb\n// {{/x}}\nc\n// {{#x}}\nd\n// {{/x}}\ne\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "x", include: true);
+
+ Assert.Equal("a\nb\nc\nd\ne\n", result);
+ }
+
+ [Fact]
+ public void Exclude_MultipleBlocks_AllRemoved()
+ {
+ var input = "a\n// {{#x}}\nb\n// {{/x}}\nc\n// {{#x}}\nd\n// {{/x}}\ne\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "x", include: false);
+
+ Assert.Equal("a\nc\ne\n", result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // Edge cases: position in file
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Block_AtStartOfFile()
+ {
+ // Exercises LastIndexOf returning -1 for start-of-file boundary.
+ var input = "// {{#test}}\nfirst\n// {{/test}}\nafter\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal("first\nafter\n", result);
+ }
+
+ [Fact]
+ public void Block_AtEndOfFile_NoTrailingNewline()
+ {
+ // Exercises IndexOf returning content.Length for end-of-file boundary.
+ var input = "before\n// {{#test}}\nlast\n// {{/test}}";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal("before\nlast\n", result);
+ }
+
+ [Fact]
+ public void Block_IsEntireFile_Exclude()
+ {
+ var input = "// {{#test}}\nall content\n// {{/test}}\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false);
+
+ Assert.Equal("", result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // Edge cases: content variations
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void EmptyBlock_ProducesSameResultForIncludeAndExclude()
+ {
+ var input = "before\n// {{#test}}\n// {{/test}}\nafter\n";
+
+ var includeResult = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+ var excludeResult = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false);
+
+ Assert.Equal("before\nafter\n", includeResult);
+ Assert.Equal(includeResult, excludeResult);
+ }
+
+ [Fact]
+ public void MultilineBlockContent_Include()
+ {
+ var input = "before\n// {{#test}}\nline1\nline2\nline3\n// {{/test}}\nafter\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal("before\nline1\nline2\nline3\nafter\n", result);
+ }
+
+ [Fact]
+ public void ContentWithCurlyBraces_NotConfusedWithMarkers()
+ {
+ var input = "before\n// {{#test}}\nvar x = { key: \"value\" };\n// {{/test}}\nafter\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal("before\nvar x = { key: \"value\" };\nafter\n", result);
+ }
+
+ [Fact]
+ public void ContentWithTemplateTokens_NotConfusedWithMarkers()
+ {
+ var input = "before\n// {{#test}}\nname: {{projectName}}\n// {{/test}}\nafter\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal("before\nname: {{projectName}}\nafter\n", result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // No markers / no matching markers
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void NoMarkers_ReturnsUnchanged()
+ {
+ var input = "just some content\nwith no markers\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal(input, result);
+ }
+
+ [Fact]
+ public void DifferentBlockName_IgnoresNonMatchingMarkers()
+ {
+ var input = "before\n// {{#other}}\nkept\n// {{/other}}\nafter\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false);
+
+ Assert.Equal(input, result);
+ }
+
+ [Fact]
+ public void MissingEndMarker_ThrowsInvalidOperationException()
+ {
+ var input = "before\n// {{#test}}\ncontent\nafter\n";
+
+ Assert.Throws(
+ () => ConditionalBlockProcessor.ProcessBlock(input, "test", include: false));
+ }
+
+ [Fact]
+ public void MissingStartMarker_LeavesContentUnchanged()
+ {
+ var input = "before\ncontent\n// {{/test}}\nafter\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false);
+
+ Assert.Equal(input, result);
+ }
+
+ [Fact]
+ public void EmptyContent_ReturnsEmpty()
+ {
+ var result = ConditionalBlockProcessor.ProcessBlock("", "test", include: true);
+
+ Assert.Equal("", result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // Process() with multiple conditions
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Process_MultipleConditions_IncludesAndExcludesCorrectly()
+ {
+ var input = """
+ start
+ // {{#redis}}
+ redis stuff
+ // {{/redis}}
+ middle
+ // {{#no-redis}}
+ no redis stuff
+ // {{/no-redis}}
+ end
+ """;
+
+ var conditions = new Dictionary
+ {
+ ["redis"] = true,
+ ["no-redis"] = false,
+ };
+
+ var result = ConditionalBlockProcessor.Process(input, conditions);
+
+ Assert.Contains("redis stuff", result);
+ Assert.DoesNotContain("no redis stuff", result);
+ Assert.Contains("start", result);
+ Assert.Contains("middle", result);
+ Assert.Contains("end", result);
+ }
+
+ [Fact]
+ public void Process_InverseConditions_IncludesAndExcludesCorrectly()
+ {
+ var input = """
+ start
+ // {{#redis}}
+ redis stuff
+ // {{/redis}}
+ middle
+ // {{#no-redis}}
+ no redis stuff
+ // {{/no-redis}}
+ end
+ """;
+
+ var conditions = new Dictionary
+ {
+ ["redis"] = false,
+ ["no-redis"] = true,
+ };
+
+ var result = ConditionalBlockProcessor.Process(input, conditions);
+
+ // "no redis stuff" contains the substring "redis stuff", so assert on the full line.
+ Assert.DoesNotContain("// {{#redis}}", result);
+ Assert.Contains("no redis stuff", result);
+ Assert.Contains("start", result);
+ Assert.Contains("middle", result);
+ Assert.Contains("end", result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // Adjacent blocks
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void AdjacentBlocks_ExcludeBoth()
+ {
+ var input = "before\n// {{#a}}\nalpha\n// {{/a}}\n// {{#b}}\nbeta\n// {{/b}}\nafter\n";
+
+ var conditions = new Dictionary
+ {
+ ["a"] = false,
+ ["b"] = false,
+ };
+
+ var result = ConditionalBlockProcessor.Process(input, conditions);
+
+ Assert.DoesNotContain("alpha", result);
+ Assert.DoesNotContain("beta", result);
+ Assert.Equal("before\nafter\n", result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // Realistic template scenarios
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void PythonFile_WithRedisAndNoRedisBlocks()
+ {
+ var input = """
+ import os
+ # {{#redis}}
+ import redis
+ # {{/redis}}
+
+ # {{#redis}}
+ def get_client():
+ return redis.from_url(os.environ.get("CACHE_URI"))
+ # {{/redis}}
+
+ # {{#no-redis}}
+ def get_forecast():
+ return generate_fresh()
+ # {{/no-redis}}
+ """;
+
+ var conditions = new Dictionary
+ {
+ ["redis"] = true,
+ ["no-redis"] = false,
+ };
+
+ var result = ConditionalBlockProcessor.Process(input, conditions);
+
+ Assert.Contains("import redis", result);
+ Assert.Contains("def get_client():", result);
+ Assert.DoesNotContain("def get_forecast():", result);
+ Assert.DoesNotContain("{{#", result);
+ Assert.DoesNotContain("{{/", result);
+ }
+
+ [Fact]
+ public void PyprojectToml_DependenciesBlock()
+ {
+ var input = """
+ dependencies = [
+ "fastapi[standard]>=0.119.0",
+ "opentelemetry-distro>=0.59b0",
+ # {{#redis}}
+ "opentelemetry-instrumentation-redis>=0.59b0",
+ "redis>=6.4.0",
+ # {{/redis}}
+ ]
+ """;
+
+ var resultWith = ConditionalBlockProcessor.ProcessBlock(input, "redis", include: true);
+ Assert.Contains("redis>=6.4.0", resultWith);
+ Assert.DoesNotContain("{{#redis}}", resultWith);
+ // Verify the closing bracket is still there
+ Assert.Contains("]", resultWith);
+
+ var resultWithout = ConditionalBlockProcessor.ProcessBlock(input, "redis", include: false);
+ Assert.DoesNotContain("redis", resultWithout);
+ Assert.Contains("fastapi", resultWithout);
+ Assert.Contains("]", resultWithout);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // Whitespace / formatting preservation
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void PreservesIndentation_InKeptContent()
+ {
+ var input = " // {{#test}}\n indented content\n // {{/test}}\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true);
+
+ Assert.Equal(" indented content\n", result);
+ }
+
+ [Fact]
+ public void SurroundingBlankLines_PreservedWhenExcluding()
+ {
+ var input = "before\n\n// {{#test}}\ncontent\n// {{/test}}\n\nafter\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false);
+
+ // The processor removes the block and its markers but preserves surrounding blank lines.
+ Assert.Equal("before\n\n\nafter\n", result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────────
+ // Block name edge cases
+ // ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void BlockNameWithHyphen_WorksCorrectly()
+ {
+ var input = "before\n// {{#no-redis}}\ncontent\n// {{/no-redis}}\nafter\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "no-redis", include: true);
+
+ Assert.Equal("before\ncontent\nafter\n", result);
+ }
+
+ [Fact]
+ public void SimilarBlockNames_DoNotInterfere()
+ {
+ var input = "// {{#redis}}\nredis content\n// {{/redis}}\n// {{#redis-cluster}}\ncluster content\n// {{/redis-cluster}}\n";
+
+ var result = ConditionalBlockProcessor.ProcessBlock(input, "redis", include: false);
+
+ Assert.DoesNotContain("redis content", result);
+ Assert.Contains("cluster content", result);
+ Assert.Contains("{{#redis-cluster}}", result);
+ }
+}
diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs
index 3bf653368cc..b3b801f5668 100644
--- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs
+++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs
@@ -324,7 +324,7 @@ public async Task GetTemplates_SingleFileAppHostIsNotReturned()
// Assert
var templateNames = templates.Select(t => t.Name).ToList();
Assert.DoesNotContain("aspire-apphost-singlefile", templateNames);
- Assert.Contains("aspire-py-starter", templateNames);
+ Assert.DoesNotContain("aspire-py-starter", templateNames);
}
[Fact]
diff --git a/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs b/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs
index 6fa3c1c03c1..4ed489c2461 100644
--- a/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs
+++ b/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs
@@ -23,7 +23,6 @@ public partial class LocalhostTldHostnameTests(ITestOutputHelper testOutput) : T
{ "aspire-apphost", "my.service.name", "my-service-name" },
{ "aspire-apphost-singlefile", "-my.service..name-", "my-service-name" },
{ "aspire-starter", "Test_App.1", "test-app-1" },
- { "aspire-py-starter", "xn--Test_App_1", "xn-test-app-1" },
{ "aspire-ts-cs-starter", "My-App.Test", "my-app-test" }
};
@@ -36,7 +35,7 @@ public async Task LocalhostTld_GeneratesDnsCompliantHostnames(string templateNam
var targetFramework = templateName switch
{
- "aspire-apphost-singlefile" or "aspire-py-starter" => TestTargetFramework.None, // These templates do not support -f argument
+ "aspire-apphost-singlefile" => TestTargetFramework.None, // These templates do not support -f argument
_ => TestTargetFramework.Next // LocalhostTld only available on net10.0
};
@@ -57,7 +56,7 @@ public async Task LocalhostTld_GeneratesDnsCompliantHostnames(string templateNam
"aspire-ts-cs-starter" or "aspire-starter" => Path.Combine(project.RootDir, $"{projectName}.AppHost", "Properties", "launchSettings.json"),
"aspire" => Path.Combine(project.RootDir, $"{projectName}.AppHost", "Properties", "launchSettings.json"),
"aspire-apphost" => Path.Combine(project.RootDir, "Properties", "launchSettings.json"),
- "aspire-apphost-singlefile" or "aspire-py-starter" => Path.Combine(project.RootDir, "apphost.run.json"),
+ "aspire-apphost-singlefile" => Path.Combine(project.RootDir, "apphost.run.json"),
_ => throw new ArgumentException($"Unknown template: {templateName}")
};
diff --git a/tests/Shared/Docker/Dockerfile.e2e b/tests/Shared/Docker/Dockerfile.e2e
index 4ce1c4f2e6a..ec4a952cb4f 100644
--- a/tests/Shared/Docker/Dockerfile.e2e
+++ b/tests/Shared/Docker/Dockerfile.e2e
@@ -65,10 +65,12 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
rm -rf /var/lib/apt/lists/*
-# Install Python (needed for Python templates).
+# Install Python and uv (needed for Python templates).
RUN apt-get update -qq && \
apt-get install -y --no-install-recommends python3 python3-pip python3-venv && \
rm -rf /var/lib/apt/lists/*
+RUN curl -LsSf https://astral.sh/uv/install.sh | sh
+ENV PATH="/root/.local/bin:${PATH}"
# Install Java (needed for Java AppHost tests).
RUN apt-get update -qq && \
diff --git a/tests/Shared/TemporaryRepo.cs b/tests/Shared/TemporaryRepo.cs
index e61ff2d4c48..b68046d7a4e 100644
--- a/tests/Shared/TemporaryRepo.cs
+++ b/tests/Shared/TemporaryRepo.cs
@@ -68,6 +68,9 @@ internal static TemporaryWorkspace Create(ITestOutputHelper outputHelper)
var aspireDir = Directory.CreateDirectory(Path.Combine(path, ".aspire"));
File.WriteAllText(Path.Combine(aspireDir.FullName, "settings.json"), "{}");
+ // Register workspace path for CaptureWorkspaceOnFailure attribute
+ TestContext.Current?.KeyValueStorage["WorkspacePath"] = repoDirectory.FullName;
+
return new TemporaryWorkspace(outputHelper, repoDirectory);
}
}