diff --git a/.agents/skills/doc-pr-reviewer/SKILL.md b/.agents/skills/doc-pr-reviewer/SKILL.md index 4e2d38763..ea66befaa 100644 --- a/.agents/skills/doc-pr-reviewer/SKILL.md +++ b/.agents/skills/doc-pr-reviewer/SKILL.md @@ -80,6 +80,40 @@ For each non-`narrative` claim: - `contradicted` — the source code says something different than the PR claims; include both texts 4. Do not infer from documentation, blog posts, or other branches. The branch you actually checked out and recorded for that repo (per the **Source of truth** section — the PR's matching branch, or the documented fallback when none matches) is the only source of truth. +## Current-version placeholder review + +When a docs PR adds or edits Aspire version strings, check whether the version +is intended to represent the current release or an intentionally fixed version. +The docs site provides placeholders for current-version values: + +| Placeholder | Use for | +|-------------|---------| +| `%ASPIRE_VERSION%` | Full current Aspire version, including patch (for example, `13.5.0`) | +| `%ASPIRE_VERSION_MAJOR_MINOR%` | Current Aspire major/minor display version (for example, `13.5`) | + +Flag hard-coded Aspire versions as a review finding when they appear in current +copy/paste guidance that should track the active release, including: + +- `Aspire.AppHost.Sdk` declarations in project files or file-based apps. +- `#:package Aspire.*@...` file-based app package directives. +- Aspire CLI or AppHost sample output that reports the current CLI/AppHost + version. +- Getting started, installation, or "use this today" examples that should move + with the release branch. + +Do **not** require placeholders for intentionally fixed versions, including: + +- What's-new or release-note pages that describe a specific historical release. +- Upgrade examples that deliberately compare old and new versions. +- CLI examples where the point is pinning a specific version with `--version`, + `-Version`, or `Aspire.ProjectTemplates::...`. +- Versioned schema URLs, package compatibility notes, minimum-version + requirements, third-party dependency versions, container image tags, or issue + reproduction snippets. + +If the intent is ambiguous, leave a `COMMENT` asking whether the version should +track the current release instead of requesting changes outright. + ## Phase A artifact When Phase A finishes, write down (in memory) a frozen Phase A result containing: diff --git a/.github/agents/release-verifier.agent.md b/.github/agents/release-verifier.agent.md index 6fbab2d2b..8efc9bc63 100644 --- a/.github/agents/release-verifier.agent.md +++ b/.github/agents/release-verifier.agent.md @@ -121,7 +121,22 @@ Scan the documentation for version strings that should have been updated for thi Determine the immediately prior version. For `13.2` the prior is `13.1`; for `13.0` the prior is `9.5` (or whatever the last release of the previous major is). Use the existing what's-new files to determine this. -#### 4b. Scan for stale version references +#### 4b. Verify shared version placeholders + +Read `src/frontend/config/aspire-versions.mjs` and verify the exported current +version constants match the release being verified: + +| Export | Expected value | +|--------|----------------| +| `currentAspireMajorMinorVersion` | `{VERSION}` (for example, `13.2`) | +| `currentAspireVersion` | `{NUGET_VERSION}` (for example, `13.2.0`) | + +These constants drive the `%ASPIRE_VERSION_MAJOR_MINOR%` and +`%ASPIRE_VERSION%` documentation placeholders. If either value is stale, flag it +as a **critical** failure because generated docs can show the wrong current +release version. + +#### 4c. Scan for stale version references Search the docs content tree for references to the prior NuGet version that appear **outside** of intentional historical context (e.g., upgrade-from examples that deliberately show the old version). @@ -139,7 +154,7 @@ grep -rn 'Aspire.AppHost.Sdk.*Version="{PRIOR_NUGET_VERSION}"' src/frontend/src/ --include="*.mdx" ``` -#### 4c. Evaluate each match +#### 4d. Evaluate each match For every match found: @@ -149,7 +164,7 @@ For every match found: - **Stale (should be updated)**: The reference is in current guidance, installation instructions, or sample code that a user would copy today. Flag these for update. 3. Log each stale reference with file path, line number, and surrounding context. -#### 4d. Verify new version references +#### 4e. Verify new version references Spot-check that key documentation pages reference the release version: diff --git a/src/frontend/astro.config.mjs b/src/frontend/astro.config.mjs index 215da4507..90ff53105 100644 --- a/src/frontend/astro.config.mjs +++ b/src/frontend/astro.config.mjs @@ -8,6 +8,8 @@ import { cookieConfig } from './config/cookie.config'; import { locales } from './config/locales.ts'; import { headAttrs } from './config/head.attrs.ts'; import { socialConfig } from './config/socials.config.ts'; +import { aspireVersionPlaceholdersIntegration } from './config/aspire-version-placeholders-integration.mjs'; +import { remarkAspireVersionPlaceholders } from './config/remark-aspire-version-placeholders.mjs'; import catppuccin from '@catppuccin/starlight'; import lunaria from './config/lunaria-starlight.mjs'; import mermaid from 'astro-mermaid'; @@ -48,7 +50,9 @@ export default defineConfig({ site: 'https://aspire.dev', trailingSlash: 'always', markdown: { - processor: unified(), + processor: unified({ + remarkPlugins: [remarkAspireVersionPlaceholders], + }), }, redirects: redirects, integrations: [ @@ -238,6 +242,7 @@ export default defineConfig({ }), jopSoftwarecookieconsent(cookieConfig), ...(isBuildTimingEnabled ? [buildTiming()] : []), + aspireVersionPlaceholdersIntegration(), ], build: { concurrency: buildConcurrency, diff --git a/src/frontend/config/aspire-version-placeholders-integration.mjs b/src/frontend/config/aspire-version-placeholders-integration.mjs new file mode 100644 index 000000000..ae1a1dc38 --- /dev/null +++ b/src/frontend/config/aspire-version-placeholders-integration.mjs @@ -0,0 +1,91 @@ +import { readdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { replaceAspireVersionPlaceholders } from './remark-aspire-version-placeholders.mjs'; + +// Per-page Markdown copies emitted by `starlight-page-actions` are the only +// generated artifacts that still contain raw `%ASPIRE_VERSION%` placeholders: +// that plugin `viteStaticCopy`s `src/content/docs/**/*.{md,mdx}` straight to +// `dist/**/*.md` through a regex-only transform, so it never runs through the +// `remarkAspireVersionPlaceholders` remark plugin. +// +// Everything else is already handled before it reaches `dist`: +// - `.html` pages -> rendered via the remark pipeline (placeholders replaced +// in the mdast before expressive-code renders code blocks) +// - `llms*.txt` -> `starlight-llms-txt` sources rendered HTML (`render(entry)`) +// - `reference/**.md` -> generated from API/sample data, not docs content +// +// So this post-build pass only needs to touch `.md` files. Scoping it this way +// (instead of walking every `.html`/`.txt` in `dist`) avoids re-reading the bulk +// of the output — including the large `llms-full.txt` assets — which is what +// previously exhausted the Node heap. +const placeholderCopyExtensions = new Set(['.md']); + +// Process the Markdown copies through a small worker pool rather than a single +// recursive `Promise.all` over the whole tree, so peak memory stays proportional +// to the concurrency limit instead of the number of files held open at once. +const DEFAULT_CONCURRENCY = 16; + +export function aspireVersionPlaceholdersIntegration() { + return { + name: 'aspire-version-placeholders', + hooks: { + 'astro:build:done': async ({ dir }) => { + await replaceAspireVersionPlaceholdersInDirectory(fileURLToPath(dir)); + }, + }, + }; +} + +export async function replaceAspireVersionPlaceholdersInDirectory( + directory, + concurrency = DEFAULT_CONCURRENCY +) { + const files = []; + await collectMarkdownCopies(directory, files); + + if (files.length === 0) { + return; + } + + // Normalize to a finite positive integer so a stray NaN/0/negative value can't + // collapse the worker pool to an empty array and silently skip every file. + const limit = Number.isFinite(concurrency) ? Math.floor(concurrency) : DEFAULT_CONCURRENCY; + const workerCount = Math.min(Math.max(1, limit), files.length); + let cursor = 0; + + const runWorker = async () => { + while (cursor < files.length) { + const filePath = files[cursor++]; + await replaceAspireVersionPlaceholdersInFile(filePath); + } + }; + + await Promise.all(Array.from({ length: workerCount }, runWorker)); +} + +async function collectMarkdownCopies(directory, files) { + const entries = await readdir(directory, { withFileTypes: true }); + + for (const entry of entries) { + const resolvedPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + await collectMarkdownCopies(resolvedPath, files); + continue; + } + + if (entry.isFile() && placeholderCopyExtensions.has(path.extname(entry.name))) { + files.push(resolvedPath); + } + } +} + +async function replaceAspireVersionPlaceholdersInFile(filePath) { + const content = await readFile(filePath, 'utf8'); + const updated = replaceAspireVersionPlaceholders(content); + + if (updated !== content) { + await writeFile(filePath, updated); + } +} diff --git a/src/frontend/config/aspire-versions.mjs b/src/frontend/config/aspire-versions.mjs new file mode 100644 index 000000000..8c27e3ae9 --- /dev/null +++ b/src/frontend/config/aspire-versions.mjs @@ -0,0 +1,7 @@ +export const currentAspireMajorMinorVersion = '13.5'; +export const currentAspireVersion = '13.5.0'; + +export const aspireVersionPlaceholders = Object.freeze({ + '%ASPIRE_VERSION_MAJOR_MINOR%': currentAspireMajorMinorVersion, + '%ASPIRE_VERSION%': currentAspireVersion, +}); diff --git a/src/frontend/config/remark-aspire-version-placeholders.mjs b/src/frontend/config/remark-aspire-version-placeholders.mjs new file mode 100644 index 000000000..0be1f1bd6 --- /dev/null +++ b/src/frontend/config/remark-aspire-version-placeholders.mjs @@ -0,0 +1,40 @@ +import { aspireVersionPlaceholders } from './aspire-versions.mjs'; + +const placeholderEntries = Object.entries(aspireVersionPlaceholders); + +export function replaceAspireVersionPlaceholders(value) { + return placeholderEntries.reduce( + (current, [placeholder, version]) => current.replaceAll(placeholder, version), + value + ); +} + +export function remarkAspireVersionPlaceholders() { + return (tree) => { + replaceNodeValues(tree); + }; +} + +function replaceNodeValues(node) { + if (!node || typeof node !== 'object') { + return; + } + + if (typeof node.value === 'string') { + node.value = replaceAspireVersionPlaceholders(node.value); + } + + if (Array.isArray(node.attributes)) { + for (const attribute of node.attributes) { + if (attribute && typeof attribute.value === 'string') { + attribute.value = replaceAspireVersionPlaceholders(attribute.value); + } + } + } + + if (Array.isArray(node.children)) { + for (const child of node.children) { + replaceNodeValues(child); + } + } +} diff --git a/src/frontend/config/sidebar/dashboard.topics.ts b/src/frontend/config/sidebar/dashboard.topics.ts index a108599c3..36b5de2e6 100644 --- a/src/frontend/config/sidebar/dashboard.topics.ts +++ b/src/frontend/config/sidebar/dashboard.topics.ts @@ -243,6 +243,27 @@ export const dashboardTopics: StarlightSidebarTopicsUserConfig = { }, slug: 'dashboard/security-considerations', }, + { + label: 'Troubleshooting', + translations: { + da: 'Fejlfinding', + de: 'Fehlerbehebung', + en: 'Troubleshooting', + es: 'Solución de problemas', + fr: 'Dépannage', + hi: 'समस्या निवारण', + id: 'Pemecahan masalah', + it: 'Risoluzione dei problemi', + ja: 'トラブルシューティング', + ko: '문제 해결', + 'pt-BR': 'Solução de problemas', + ru: 'Устранение неполадок', + tr: 'Sorun giderme', + uk: 'Усунення неполадок', + 'zh-CN': '故障排除', + }, + slug: 'dashboard/troubleshooting', + }, { label: 'Telemetry', translations: { diff --git a/src/frontend/config/sidebar/deployment.topics.ts b/src/frontend/config/sidebar/deployment.topics.ts index c682e7cab..4b2fdef53 100644 --- a/src/frontend/config/sidebar/deployment.topics.ts +++ b/src/frontend/config/sidebar/deployment.topics.ts @@ -150,6 +150,10 @@ export const deploymentTopics: StarlightSidebarTopicsUserConfig = { label: 'External Helm charts', slug: 'deployment/kubernetes/helm-charts', }, + { + label: 'Persistent volumes', + slug: 'deployment/kubernetes/persistent-volumes', + }, { label: 'Ingress & Gateway API', slug: 'deployment/kubernetes-ingress', diff --git a/src/frontend/config/sidebar/docs.topics.ts b/src/frontend/config/sidebar/docs.topics.ts index 252aad6fe..cf23dff1c 100644 --- a/src/frontend/config/sidebar/docs.topics.ts +++ b/src/frontend/config/sidebar/docs.topics.ts @@ -90,6 +90,10 @@ export const docsTopics: StarlightSidebarTopicsUserConfig = { label: "What's new", collapsed: true, items: [ + { + label: 'Aspire 13.5', + slug: 'whats-new/aspire-13-5', + }, { label: 'Aspire 13.4', slug: 'whats-new/aspire-13-4', @@ -936,6 +940,10 @@ export const docsTopics: StarlightSidebarTopicsUserConfig = { ja: '実行可能リソース', }, }, + { + label: 'Interactive terminals', + slug: 'app-host/with-terminal', + }, ], }, { diff --git a/src/frontend/config/sidebar/reference.topics.ts b/src/frontend/config/sidebar/reference.topics.ts index 501411893..42380fa31 100644 --- a/src/frontend/config/sidebar/reference.topics.ts +++ b/src/frontend/config/sidebar/reference.topics.ts @@ -538,6 +538,24 @@ export const referenceTopics: StarlightSidebarTopicsUserConfig[number] = { slug: 'reference/cli/commands/aspire-start', }, { label: 'aspire stop', slug: 'reference/cli/commands/aspire-stop' }, + { + label: 'aspire terminal', + collapsed: true, + items: [ + { + label: 'aspire terminal', + slug: 'reference/cli/commands/aspire-terminal', + }, + { + label: 'aspire terminal attach', + slug: 'reference/cli/commands/aspire-terminal-attach', + }, + { + label: 'aspire terminal ps', + slug: 'reference/cli/commands/aspire-terminal-ps', + }, + ], + }, { label: 'aspire update', slug: 'reference/cli/commands/aspire-update', diff --git a/src/frontend/src/content/docs/app-host/certificate-configuration.mdx b/src/frontend/src/content/docs/app-host/certificate-configuration.mdx index bf9d44d0f..89c559865 100644 --- a/src/frontend/src/content/docs/app-host/certificate-configuration.mdx +++ b/src/frontend/src/content/docs/app-host/certificate-configuration.mdx @@ -5,6 +5,7 @@ description: Configure HTTPS endpoints and certificate trust for Aspire resource --- import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; +import OsAwareTabs from '@components/OsAwareTabs.astro'; Aspire provides two complementary sets of certificate APIs: @@ -36,7 +37,31 @@ Many of the certificate features in Aspire rely on a development certificate. Be ### Using the Aspire CLI (recommended) -The preferred way to manage the development certificate is to use the [Aspire CLI](/get-started/install-cli/). When you run `aspire run`, the CLI automatically ensures the development certificate is created and trusted. No additional manual steps are required. +The preferred way to manage the development certificate is to use the [Aspire CLI](/get-started/install-cli/). When you run `aspire run` in an interactive session, the CLI automatically ensures the development certificate is created and trusted. No additional manual steps are required. + +For non-C# AppHosts (such as [TypeScript](/app-host/typescript-apphost/) or Python AppHosts), the `dotnet` first-run experience that normally creates the HTTPS development certificate never runs, because these AppHosts launch a prebuilt native binary instead of invoking `dotnet`. The Aspire CLI fills this gap when `aspire run` starts and no development certificate exists: + +- In an interactive session—and on Linux, where establishing trust doesn't require a prompt—the CLI creates *and* trusts the certificate, just as it does for C# AppHosts. +- In a non-interactive session on macOS or Windows (for example, in CI), the CLI can't show the macOS Keychain password prompt or the Windows trust dialog, so it *generates* the certificate without trusting it. This lets servers such as Kestrel load the certificate from the personal store, even though it isn't trusted. If the certificate can't be generated, a warning is displayed and the run continues. + +To opt out of automatic certificate generation, set the `ASPIRE_CLI_GENERATE_HTTPS_CERTIFICATE` environment variable to `false`. This mirrors the .NET SDK's `DOTNET_GENERATE_ASPNET_CERTIFICATE` opt-out: + + +
+ +```bash title="Disable automatic HTTPS certificate generation" +ASPIRE_CLI_GENERATE_HTTPS_CERTIFICATE=false aspire run +``` + +
+
+ +```powershell title="Disable automatic HTTPS certificate generation" +$env:ASPIRE_CLI_GENERATE_HTTPS_CERTIFICATE="false"; aspire run +``` + +
+
You can also manage certificates explicitly with the Aspire CLI: diff --git a/src/frontend/src/content/docs/app-host/configuration.mdx b/src/frontend/src/content/docs/app-host/configuration.mdx index c66c3dc32..f51efcebc 100644 --- a/src/frontend/src/content/docs/app-host/configuration.mdx +++ b/src/frontend/src/content/docs/app-host/configuration.mdx @@ -24,9 +24,12 @@ AppHost configuration is provided through launch profiles: ]} /> -In C# AppHosts, profiles live in `launchSettings.json`: +C# AppHosts come in two forms, and each stores launch profiles differently: -```json title="launchSettings.json" +- **Project-based AppHost** (the default `dotnet new aspire-apphost` template): profiles live in `Properties/launchSettings.json`. +- **File-based AppHost** (created with `aspire new` using the empty C# template): profiles live in `apphost.run.json`. In this template, `aspire.config.json` only points at the entry file — it does **not** contain a `profiles` block. + +```json title="Project-based AppHost — Properties/launchSettings.json" { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { @@ -45,6 +48,36 @@ In C# AppHosts, profiles live in `launchSettings.json`: } } ``` + +```json title="File-based AppHost — apphost.run.json" +{ + "profiles": { + "https": { + "applicationUrl": "https://localhost:17134;http://localhost:15170", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21030", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22057" + } + } + } +} +``` + +```json title="File-based AppHost — aspire.config.json" +{ + "appHost": { + "path": "apphost.cs" + } +} +``` + + In TypeScript AppHosts, profiles live in `aspire.config.json`: diff --git a/src/frontend/src/content/docs/app-host/resource-lifetimes.mdx b/src/frontend/src/content/docs/app-host/resource-lifetimes.mdx index 3335e6aa7..854913c50 100644 --- a/src/frontend/src/content/docs/app-host/resource-lifetimes.mdx +++ b/src/frontend/src/content/docs/app-host/resource-lifetimes.mdx @@ -5,6 +5,7 @@ description: Learn how session, persistent, resource-scoped, and parent-process import { Tabs, TabItem } from '@astrojs/starlight/components'; import { Image } from 'astro:assets'; +import LearnMore from '@components/LearnMore.astro'; import persistentContainer from '@assets/whats-new/aspire-9/persistent-container.png'; import persistentContainerDocker from '@assets/whats-new/aspire-9/persistent-container-docker-desktop.png'; @@ -38,7 +39,13 @@ Use persistent lifetime for resources that are expensive to initialize, need sta Persistent resources are automatically recreated when the AppHost detects meaningful configuration changes. If the configuration differs, the resource is recreated with the new settings. ::: -Persistent resources default to proxyless endpoints so a stable local endpoint can remain reachable after the AppHost stops. You can still configure endpoint proxy behavior explicitly. For example, set `isProxied: true` or `IsProxied = true` when you need Aspire's proxy for a specific endpoint, or disable endpoint proxy support when you intentionally want every endpoint on a resource to be proxyless. Persistent executable endpoints must have a concrete `port` or `targetPort`; automatically persisted random executable ports aren't supported. +Persistent containers use proxied endpoints by default, just like session containers. The proxy runs only while the AppHost is running, so the proxy address isn't reachable after the AppHost stops. Persistent executables and projects default to proxyless endpoints so their direct addresses stay stable and reachable even after the AppHost stops. + +You can still configure endpoint proxy behavior explicitly on any persistent resource. Set `isProxied: false` on an individual endpoint, or call `WithEndpointProxySupport(false)` to make every endpoint on a resource proxyless. When a proxyless endpoint doesn't specify a public `port`, Aspire allocates one before the resource is created. For persistent resources, Aspire stores the allocated port in user secrets when user secrets are available and reuses it on later AppHost runs. + + + For more information, see [Allocate ports for dynamic proxyless endpoints](/fundamentals/networking-overview/#allocate-ports-for-dynamic-proxyless-endpoints). + :::danger[Persistent container ≠ persistent data] Persistent container lifetime doesn't guarantee data durability. For details, see [Container lifetime vs. data durability](#container-lifetime-vs-data-durability). @@ -155,7 +162,7 @@ await builder.build().run(); -Configure a concrete `port` or `targetPort` for persistent executable endpoints; automatically persisted random executable ports aren't supported. +The preceding example configures a concrete `port` so the endpoint address is explicit. If you omit the public `port`, Aspire allocates one before the executable starts and reuses it from user secrets on later AppHost runs when user secrets are available. ## Configure a persistent project diff --git a/src/frontend/src/content/docs/app-host/with-terminal.mdx b/src/frontend/src/content/docs/app-host/with-terminal.mdx new file mode 100644 index 000000000..c2fa6ff52 --- /dev/null +++ b/src/frontend/src/content/docs/app-host/with-terminal.mdx @@ -0,0 +1,184 @@ +--- +title: Test TUI and shell apps using WithTerminal +seoTitle: Test terminal apps in your Aspire AppHost using WithTerminal +description: Use WithTerminal to expose an interactive terminal for a resource in your Aspire app model, then attach to it from the dashboard or the aspire terminal CLI to drive TUI and shell-based experiences. +--- + +import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; + +If you have a terminal user interface (TUI) application or a shell-based experience that you want to exercise while it runs under Aspire, add `WithTerminal(...)` to the resource. Aspire then exposes an interactive terminal session that you can attach to from the [Aspire dashboard](/dashboard/overview/) or from the [`aspire terminal`](/reference/cli/commands/aspire-terminal/) CLI command. + + + + +```csharp title="AppHost.cs" +#pragma warning disable ASPIRETERMINAL001 +var builder = DistributedApplication.CreateBuilder(args); + +var agent = builder.AddExecutable("agent", "my-agent", ".") + .WithTerminal(); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.ts" +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const agent = await builder.addExecutable("agent", "my-agent", ".") + .withTerminal(); + +await builder.build().run(); +``` + + + + +Once the app is running, open the resource's terminal page in the dashboard—or run `aspire terminal attach agent`—to interact with the process just as you would in a local shell. + + + +## When to use WithTerminal + +Reach for `WithTerminal` when a resource is interactive rather than a plain background service: + +- A **TUI application**—for example, an agent, a diagnostics console, or a curses-style tool—that draws a full-screen interface you want to see and drive. +- A **shell-based experience** where you want an interactive prompt inside a container or executable while it runs as part of your app model. +- Any resource you want to **poke at live** during development without leaving the Aspire dashboard or CLI. + +## The debugger is not attached automatically + +When you apply `WithTerminal`, Aspire runs the resource as a plain process and **does not automatically attach the debugger**. If you need to debug the resource, attach the debugger manually to the running process from your IDE. + + + +## Attach from multiple places at once + +Terminal sessions support multiple simultaneous viewers. You can open **two browser tabs pointing at the same terminal**—or a browser tab and the CLI together—and both stay responsive: input and output are mirrored to every attached peer. + +One peer holds the **primary** role and drives the terminal's dimensions, while the others attach as **viewers**. From the CLI you can join as a passive viewer with `aspire terminal attach --viewer`, and take control later with the `Ctrl+B T` hotkey. + +## Configure the terminal + +The terminal session is described by a set of options with sensible defaults: + +| Option | Default | Description | +| ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Columns` | `120` | The initial number of columns for the terminal grid. | +| `Rows` | `30` | The initial number of rows for the terminal grid. | +| `Shell` | `null` | The shell to launch for the session. When `null`, the default is used: for containers this is typically `/bin/sh`; for executables the process itself is the terminal program. | +| `ShowTerminalHost` | `false` | Whether the hidden per-replica terminal host resources appear in the dashboard and CLI resource lists. Set to `true` to diagnose terminal-host startup or connectivity issues. | + + + + +In C#, pass a callback to override any of these options: + +```csharp title="AppHost.cs" +#pragma warning disable ASPIRETERMINAL001 +var builder = DistributedApplication.CreateBuilder(args); + +var agent = builder.AddExecutable("agent", "my-agent", ".") + .WithTerminal(options => + { + options.Columns = 200; + options.Rows = 50; + }); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.ts" +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const agent = await builder.addExecutable("agent", "my-agent", ".") + .withTerminal(); + +await builder.build().run(); +``` + + + + + + +## Terminals and replicas + +Each replica of a resource gets its own independent terminal session. Aspire creates one terminal host per parent replica, so requesting three replicas yields three separate terminals. The order of `WithReplicas` and `WithTerminal` does not matter—the final replica count is always honored: + + + + +```csharp title="AppHost.cs" +#pragma warning disable ASPIRETERMINAL001 +var builder = DistributedApplication.CreateBuilder(args); + +// Three replicas, each with its own interactive terminal. +var agent = builder.AddExecutable("agent", "my-agent", ".") + .WithReplicas(3) + .WithTerminal(); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.ts" +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const agent = await builder.addExecutable("agent", "my-agent", ".") + .withReplicas(3) + .withTerminal(); + +await builder.build().run(); +``` + + + + +When a resource has more than one replica, choose which one to attach to with `aspire terminal attach --replica ` (indices are 0-based), or pick interactively when prompted. + + + +## View terminals in the dashboard + +When a resource has `WithTerminal` applied, its **Console Logs** tab in the [Aspire dashboard](/dashboard/overview/) is replaced by a live terminal session. You can drive the running process directly in the browser without leaving the dashboard. For example, you can type commands, scroll the scrollback buffer, and switch between replicas. Each replica appears as its own entry (for example, `agent-r0`, `agent-r1`, `agent-r2`) with an independent session. + +## Work with terminals from the CLI + +The `aspire terminal` command group lets you list and attach to terminal sessions from your shell. Because `WithTerminal` is experimental, these commands are hidden behind a feature flag. Enable them with: + +```bash title="Enable the aspire terminal commands" +aspire config set features.terminalCommandsEnabled true +``` + +Then: + +- [`aspire terminal ps`](/reference/cli/commands/aspire-terminal-ps/) lists every terminal-enabled resource in the running AppHost, with grid size, attached-peer count, and per-replica health. +- [`aspire terminal attach`](/reference/cli/commands/aspire-terminal-attach/) attaches your local terminal to a resource's interactive PTY session. + +## See also + +- [aspire terminal command](/reference/cli/commands/aspire-terminal/) +- [Executable resources](/app-host/executable-resources/) +- [Aspire dashboard overview](/dashboard/overview/) diff --git a/src/frontend/src/content/docs/community/index.mdx b/src/frontend/src/content/docs/community/index.mdx index 5a970503d..3b0ed1a3c 100644 --- a/src/frontend/src/content/docs/community/index.mdx +++ b/src/frontend/src/content/docs/community/index.mdx @@ -7,7 +7,7 @@ next: false description: Connect with the Aspire team and community across Discord, GitHub Discussions, livestreams, social channels, and contribution platforms for distributed apps. banner: content: | - Aspire 13.4 is here!See what's new + ✨ Aspire 13.5 is available!Explore the latest features and improvements editUrl: false giscus: false tableOfContents: false diff --git a/src/frontend/src/content/docs/da/index.mdx b/src/frontend/src/content/docs/da/index.mdx index 24254470d..fff3ca559 100644 --- a/src/frontend/src/content/docs/da/index.mdx +++ b/src/frontend/src/content/docs/da/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 er udgivet!Se hvad der er nyt i Aspire 13.4. + ✨ Aspire 13.5 er udgivet!Se hvad der er nyt i Aspire 13.5. hero: tagline: Din stack, forenklet.

Orkestrér frontends, APIs, containere og databaser ubesværet—ingen omskrivninger, ingen grænser. Udvid Aspire til at drive ethvert projekt.

image: diff --git a/src/frontend/src/content/docs/dashboard/explore.mdx b/src/frontend/src/content/docs/dashboard/explore.mdx index 53c5cb0f9..d1726d0e0 100644 --- a/src/frontend/src/content/docs/dashboard/explore.mdx +++ b/src/frontend/src/content/docs/dashboard/explore.mdx @@ -579,13 +579,17 @@ Selecting **View response** opens the text visualizer with the command's result Aspire dashboard text visualizer showing a command response opened from the notification center. + + The unread count resets when you open the notification center. To clear all notifications, select the **Dismiss all** button in the dialog. -For information on returning messages from custom commands, see [Custom resource commands](/fundamentals/custom-resource-commands/). +For information on returning command result payloads from custom commands, see [Custom resource commands](/fundamentals/custom-resource-commands/). ## Settings dialog diff --git a/src/frontend/src/content/docs/dashboard/troubleshooting.mdx b/src/frontend/src/content/docs/dashboard/troubleshooting.mdx new file mode 100644 index 000000000..aa3747e30 --- /dev/null +++ b/src/frontend/src/content/docs/dashboard/troubleshooting.mdx @@ -0,0 +1,64 @@ +--- +title: Troubleshoot the Aspire dashboard +description: Diagnose and resolve common Aspire dashboard issues including connection errors between the dashboard and the AppHost resource service. +--- + +import LearnMore from '@components/LearnMore.astro'; + +This article helps you diagnose and resolve common issues with the Aspire dashboard. + +## Connection errors between the dashboard and AppHost + +The dashboard displays resource information by connecting to a gRPC resource service hosted by the AppHost. When this connection fails, the dashboard may show an error message or display telemetry data without any resource list or console logs. + +### Symptoms + +- The dashboard loads but shows no resources in the resource list. +- Console logs are unavailable for resources. +- An error banner appears indicating the dashboard can't connect to the resource service. +- The dashboard log contains errors related to gRPC connection failures. + +### Viewing dashboard logs + +Enable verbose logging to get more detail about connection failures. Run the AppHost with debug-level output: + +```bash title="Run with debug output" +aspire run --log-level Debug +``` + +Review the console output for messages related to the resource service connection, such as gRPC status codes or TLS handshake failures. + +### Certificate and TLS errors + +When running with HTTPS, the dashboard and AppHost use certificates to secure the gRPC connection. Certificate issues are a common source of connection failures. + +**Common causes:** + +- The development certificate isn't trusted. Run `aspire certs trust` to trust the development certificate. +- The certificate has expired or is corrupted. Regenerate it with `aspire certs clean` followed by `aspire certs trust`. +- A custom certificate is specified but the file path or password is incorrect. Verify the `Dashboard:ResourceServiceClient:ClientCertificate` settings. + +### Firewall and network issues + +- Confirm that a firewall or some other security software isn't blocking the connection between the dashboard and the resource service endpoint. +- The resource service uses gRPC, which requires HTTP/2. Some firewalls, proxies, or network appliances don't support HTTP/2 traffic and may silently drop or downgrade the connection. Ensure that any intermediary between the dashboard and the AppHost allows HTTP/2. + +### Ping timeout errors when debugging the AppHost + +You may see the following error in the dashboard: + +``` +Status(StatusCode="Internal", Detail="Error starting gRPC call. +HttpRequestException: The HTTP/2 server didn't respond to a ping request +within the configured KeepAlivePingDelay. (HttpProtocolError)") +``` + +This means the dashboard's gRPC client sent an HTTP/2 PING frame to the AppHost's resource service, but didn't receive a response in time. + +The most likely cause is that a debugger is paused on the AppHost — when you hit a breakpoint, the process can't respond to HTTP/2 PING frames, triggering the timeout. This error resolves once execution resumes. + +If you see this error and you're running without a debugger attached, verify the AppHost process is still alive and responsive. + + +For the full list of configuration options, see [Aspire dashboard configuration](/dashboard/configuration/). + diff --git a/src/frontend/src/content/docs/de/index.mdx b/src/frontend/src/content/docs/de/index.mdx index 41dddee97..39570f21e 100644 --- a/src/frontend/src/content/docs/de/index.mdx +++ b/src/frontend/src/content/docs/de/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 wurde veröffentlicht!Was ist neu in Aspire 13.4. + ✨ Aspire 13.5 wurde veröffentlicht!Was ist neu in Aspire 13.5. hero: tagline: Dein Stack, vereinfacht.

Orchestriere Frontends, APIs, Container und Datenbanken mühelos—ohne Umschreiben, ohne Grenzen. Erweitere Aspire, um jedes Projekt anzutreiben.

image: diff --git a/src/frontend/src/content/docs/deployment/kubernetes.mdx b/src/frontend/src/content/docs/deployment/kubernetes.mdx index 1985873d1..003f03f72 100644 --- a/src/frontend/src/content/docs/deployment/kubernetes.mdx +++ b/src/frontend/src/content/docs/deployment/kubernetes.mdx @@ -50,6 +50,8 @@ When you publish or deploy, Aspire translates your application model into Kubern | Endpoints | Services | | Volumes | PersistentVolumes and PersistentVolumeClaims | +For durable storage that you model as first-class resources and bind to workloads, see [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/). + ## Publish vs deploy Both Kubernetes targets support `aspire publish`. The key difference is whether the target also supports `aspire deploy`: @@ -91,4 +93,9 @@ For more on the pipeline model and how publish and deploy relate, see [Pipelines description="Install pre-existing Helm charts alongside your application using AddHelmChart." href="/deployment/kubernetes/helm-charts/" /> + \ No newline at end of file diff --git a/src/frontend/src/content/docs/deployment/kubernetes/clusters.mdx b/src/frontend/src/content/docs/deployment/kubernetes/clusters.mdx index 551ec65a3..511f48eed 100644 --- a/src/frontend/src/content/docs/deployment/kubernetes/clusters.mdx +++ b/src/frontend/src/content/docs/deployment/kubernetes/clusters.mdx @@ -350,6 +350,7 @@ See [Install external Helm charts](/deployment/kubernetes/helm-charts/) for a fu - [Kubernetes integration](/integrations/compute/kubernetes/) - [Deploy to AKS](/deployment/kubernetes/aks/) - [Install external Helm charts](/deployment/kubernetes/helm-charts/) +- [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/) - [Pipelines and app topology](/deployment/pipelines/) - [Publishing and deployment overview](/deployment/deploy-with-aspire/) - [Kubernetes documentation](https://kubernetes.io/docs/) diff --git a/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx b/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx new file mode 100644 index 000000000..160e4d3a5 --- /dev/null +++ b/src/frontend/src/content/docs/deployment/kubernetes/persistent-volumes.mdx @@ -0,0 +1,243 @@ +--- +title: Persistent volumes on Kubernetes +seoTitle: 'Persistent volumes for Aspire on Kubernetes clusters' +description: Model durable storage as first-class persistent volume resources in your Aspire AppHost, then bind them to Kubernetes workloads with a storage class, capacity, and access modes. +--- + +import { Tabs, TabItem, Aside } from '@astrojs/starlight/components'; +import LearnMore from '@components/LearnMore.astro'; + +Stateful workloads — databases, message brokers, or any service that writes durable data — need storage that survives pod restarts and rescheduling. Call `AddPersistentVolume` on a Kubernetes environment to describe a durable disk once in your AppHost, configure its storage class, capacity, access modes, and annotations with fluent methods, then bind it to one or more workloads: + + + + +```csharp title="AppHost.cs" +var k8s = builder.AddKubernetesEnvironment("k8s"); + +var pgData = k8s.AddPersistentVolume("pg-data") + .WithStorageClass("managed-csi") + .WithCapacity("20Gi"); +``` + + + + + + +```typescript title="apphost.mts" +const k8s = await builder.addKubernetesEnvironment('k8s'); + +const pgData = await k8s.addPersistentVolume('pg-data'); +await pgData.withStorageClass('managed-csi'); +await pgData.withCapacity('20Gi'); +``` + + + + +At publish time, the volume renders as a `v1.PersistentVolumeClaim` in the generated Helm chart. Like [ingress and gateway resources](/deployment/kubernetes-ingress/), you configure the volume once and reference it from workloads, rather than relying on a single environment-wide storage shape for every mount. + +## Prerequisites + +The following prerequisites must be in place before you can use persistent volumes in a Kubernetes cluster: + +- The [Aspire.Hosting.Kubernetes](/integrations/compute/kubernetes/) hosting integration installed in your AppHost. +- A [Kubernetes environment](/integrations/compute/kubernetes/#add-kubernetes-environment) added to your AppHost. +- A cluster with a storage class that can provision the volumes you request. Most managed clusters ship a default storage class. + +## Add a persistent volume + +The full set of configuration methods lets you pin the storage class, capacity, access modes, and provisioner annotations. A complete AppHost that defines a volume looks like this: + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Kubernetes; + +var builder = DistributedApplication.CreateBuilder(args); + +var k8s = builder.AddKubernetesEnvironment("k8s"); + +var pgData = k8s.AddPersistentVolume("pg-data") + .WithStorageClass("managed-csi") + .WithCapacity("20Gi") + .WithAccessMode(PersistentVolumeAccessMode.ReadWriteOnce) + .WithVolumeAnnotation("disk.csi.azure.com/skuName", "Premium_LRS"); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.mts" +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const k8s = await builder.addKubernetesEnvironment('k8s'); + +const pgData = await k8s.addPersistentVolume('pg-data'); +await pgData.withStorageClass('managed-csi'); +await pgData.withCapacity('20Gi'); +await pgData.withVolumeAnnotation('disk.csi.azure.com/skuName', 'Premium_LRS'); + +await builder.build().run(); +``` + + + + +Every configuration method is optional. When you don't set a storage class, the cluster's default storage class provisions the backing disk. When you don't set a capacity or access mode, the environment's default storage size and read-write policy apply. + +The available configuration methods are: + +| C# | TypeScript | Description | +|---|---|---| +| `WithStorageClass(string)` | `withStorageClass` / `withPvStorageClassParam` | Sets `spec.storageClassName` on the PVC. In C#, the single method accepts a literal string or an Aspire parameter; in TypeScript, use `withStorageClass` for a literal and `withPvStorageClassParam` for a parameter. | +| `WithCapacity(string)` | `withCapacity` / `withPvCapacityParam` | Sets the requested storage on `spec.resources.requests.storage` (for example, `"20Gi"`). In TypeScript, use `withCapacity` for a literal and `withPvCapacityParam` for a parameter. | +| `WithAccessMode(PersistentVolumeAccessMode)` | `withAccessMode` | Adds an entry to `spec.accessModes`. Call multiple times to declare more than one mode. | +| `WithVolumeAnnotation(string, string)` | `withVolumeAnnotation` / `withVolumeAnnotationParam` | Adds a key-value pair to the PVC's `metadata.annotations`. Useful for CSI driver hints, dynamic provisioner parameters, or backup tooling tags. In TypeScript, use `withVolumeAnnotation` for a literal value and `withVolumeAnnotationParam` for a parameter. | + +The `PersistentVolumeAccessMode` values map directly to the Kubernetes access modes: + +| Access mode | Description | +|---|---| +| `ReadWriteOnce` | Mounted as read-write by a single node. Most common for block-storage-backed databases. | +| `ReadOnlyMany` | Mounted as read-only by many nodes simultaneously. | +| `ReadWriteMany` | Mounted as read-write by many nodes simultaneously. Typically used for shared file stores such as Azure Files or NFS. | +| `ReadWriteOncePod` | Mounted as read-write by a single pod. Requires Kubernetes 1.27 or later. | + +## Bind a volume to a workload + +After defining a volume, bind it to the workloads that mount it. There are two overloads, depending on whether the workload already declares a named volume. + +### Bind by name + +Use the name-match overload when the workload already declares a volume — for example, through `WithVolume("name", "/path")` or an integration helper such as Postgres' `WithDataVolume()`. The persistent volume's name must match the workload volume's name so the publisher can route the pod's `volumes[]` entry through the generated PVC: + + + + +```csharp title="AppHost.cs" +var pgData = k8s.AddPersistentVolume("pg-data") + .WithStorageClass("managed-csi") + .WithCapacity("20Gi"); + +builder.AddPostgres("pg") + .WithDataVolume("pg-data") + .WithPersistentVolume(pgData); +``` + + + + +```typescript title="apphost.mts" +const pgData = await k8s.addPersistentVolume('pg-data'); +await pgData.withStorageClass('managed-csi'); +await pgData.withCapacity('20Gi'); + +const pg = await builder.addPostgres('pg'); +await pg.withDataVolume({ name: 'pg-data' }); +await pg.withKubernetesPersistentVolume(pgData); +``` + + + + +### Bind with a mount path + +Use the mount-path overload when the workload doesn't already declare a named volume. It creates the mount itself, so it works for projects and any compute resource. Pass the mount path inside the container, and optionally mount read-only: + + + + +```csharp title="AppHost.cs" +var media = k8s.AddPersistentVolume("media") + .WithStorageClass("azurefile-csi") + .WithCapacity("100Gi") + .WithAccessMode(PersistentVolumeAccessMode.ReadWriteMany); + +builder.AddProject("api") + .WithPersistentVolume(media, "/srv/media"); +``` + + + + +```typescript title="apphost.mts" +const media = await k8s.addPersistentVolume('media'); +await media.withStorageClass('azurefile-csi'); +await media.withCapacity('100Gi'); + +const api = await builder.addProject('api'); +await api.withKubernetesPersistentVolumeMount(media, '/srv/media'); +``` + + + + + + +## Unbound volumes and default storage + +Volumes on a workload that aren't bound to a `KubernetesPersistentVolumeResource` continue to use the environment's default storage type. You can mix a first-class persistent volume and an unbound ephemeral volume on the same workload — each is resolved independently at publish time. + +## Generated output + +Binding a container to a persistent volume by name produces a `StatefulSet` whose pod spec references the generated claim, alongside the `PersistentVolumeClaim` itself: + +```yaml title="StatefulSet (excerpt)" +apiVersion: "apps/v1" +kind: "StatefulSet" +metadata: + name: "service-statefulset" +spec: + template: + spec: + containers: + - image: "nginx:latest" + name: "service" + volumeMounts: + - name: "data" + mountPath: "/var/lib/data" + volumes: + - name: "data" + persistentVolumeClaim: + claimName: "data" + replicas: 1 +``` + +```yaml title="PersistentVolumeClaim" +apiVersion: "v1" +kind: "PersistentVolumeClaim" +metadata: + name: "data" + annotations: + volume.beta.kubernetes.io/storage-provisioner: "disk.csi.azure.com" +spec: + storageClassName: "managed-csi" + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: "20Gi" +``` + + + To generate and inspect the Helm chart for yourself, see [Deploy to Kubernetes clusters](/deployment/kubernetes/clusters/). + + +## See also + +- [Kubernetes integration](/integrations/compute/kubernetes/) +- [Deploy to Kubernetes clusters](/deployment/kubernetes/clusters/) +- [Deploy to AKS](/deployment/kubernetes/aks/) +- [Persist data with volumes](/fundamentals/persist-data-volumes/) +- [Kubernetes persistent volumes documentation](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) diff --git a/src/frontend/src/content/docs/docs.mdx b/src/frontend/src/content/docs/docs.mdx index f2a4b5acb..8e9ed378b 100644 --- a/src/frontend/src/content/docs/docs.mdx +++ b/src/frontend/src/content/docs/docs.mdx @@ -7,7 +7,7 @@ next: description: "Browse the official Aspire documentation: get started with C# and TypeScript AppHosts, model distributed apps, deploy to Azure and Kubernetes, and observe in the dashboard." banner: content: | - 🆕 Aspire 13.4 is here!See what's new + ✨ Aspire 13.5 is available!Explore the latest features and improvements editUrl: false tableOfContents: false pageActions: false diff --git a/src/frontend/src/content/docs/es/index.mdx b/src/frontend/src/content/docs/es/index.mdx index 4cdede2f9..f7855d495 100644 --- a/src/frontend/src/content/docs/es/index.mdx +++ b/src/frontend/src/content/docs/es/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 ¡Aspire 13.4 ha sido lanzado!Ver qué hay de nuevo en Aspire 13.4. + ✨ ¡Aspire 13.5 está disponible!Explora las últimas características y mejoras. hero: tagline: Tu stack, simplificado.

Orquesta frontends, APIs, contenedores y bases de datos sin esfuerzo—sin reescrituras, sin límites. Extiende Aspire para impulsar cualquier proyecto.

image: diff --git a/src/frontend/src/content/docs/extensibility/interaction-service.mdx b/src/frontend/src/content/docs/extensibility/interaction-service.mdx index 58b971a11..66b230947 100644 --- a/src/frontend/src/content/docs/extensibility/interaction-service.mdx +++ b/src/frontend/src/content/docs/extensibility/interaction-service.mdx @@ -1,10 +1,10 @@ --- -title: Interaction service (Preview) -seoTitle: Aspire interaction service (Preview) for AppHost authors +title: Interaction service +seoTitle: Aspire interaction service for AppHost authors description: Use the Aspire interaction service to prompt users for input, request confirmation, and display messages from AppHost extensions, integrations, and custom resources. --- -import { Aside, Steps } from '@astrojs/starlight/components'; +import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; import { Image } from 'astro:assets'; import messageDialog from '@assets/extensibility/interaction-service-message-dialog.png'; import messageBar from '@assets/extensibility/interaction-service-message-bar.png'; @@ -13,7 +13,7 @@ import multipleInput from '@assets/extensibility/interaction-service-multiple-in import multipleInputFilled from '@assets/extensibility/interaction-service-multiple-input-filled.png'; import multipleInputLogs from '@assets/extensibility/interaction-service-multiple-input-logs.png'; -The interaction service (`Aspire.Hosting.IInteractionService`) allows you to prompt users for input, request confirmation, and display messages. The interaction service works in two different contexts: +The interaction service (`Aspire.Hosting.IInteractionService`) allows you to prompt users for input, request confirmation, and display messages. It works in two different contexts: - **Aspire dashboard**: When running `aspire run` or launching the AppHost directly, interactions appear as dialogs and notifications in the dashboard UI. - **Aspire CLI**: When running `aspire publish` or `aspire deploy`, interactions are prompted through the command-line interface. @@ -22,19 +22,18 @@ This is useful for scenarios where you need to gather information from the user - ## The `IInteractionService` API -The `IInteractionService` interface is retrieved from the `DistributedApplication` dependency injection container. `IInteractionService` can be injected into types created from DI or resolved from `IServiceProvider`, which is usually available on a context argument passed to events. +The `IInteractionService` interface provides methods for prompting users. In C#, it's retrieved from the dependency injection container. In TypeScript, it's resolved from the command context's service provider. Always check if the service is available before use — if you attempt to call a method when it's not available, an exception is thrown. For example, the interaction service isn't available when a command is triggered from the CLI. -When you request `IInteractionService`, be sure to check if it's available for usage. If you attempt to use the interaction service when it's not available (`IInteractionService.IsAvailable` returns `false`), an exception is thrown. For example, the interaction service isn't available when a command is triggered from the CLI. + + -```csharp +```csharp title="AppHost.cs" var interactionService = serviceProvider.GetRequiredService(); if (interactionService.IsAvailable) { @@ -49,6 +48,28 @@ if (interactionService.IsAvailable) } ``` + + + +```typescript title="apphost.mts" +const interactionService = + await context.services().getInteractionService(); + +if (await interactionService.isAvailable()) { + const result = await interactionService.promptConfirmation( + "Delete confirmation", + "Are you sure you want to delete the data?", + ); + + if (!result.canceled) { + // Run your resource/command logic. + } +} +``` + + + + The interaction service has several methods that you use to interact with users or display messages. The behavior of these methods depends on the execution context: - **Dashboard context** (`aspire run` or direct AppHost launch): Interactions appear as modal dialogs, notifications, and form inputs in the [Aspire dashboard web interface](/dashboard/overview/). @@ -63,9 +84,10 @@ The following sections describe how to use these APIs effectively in both contex | `PromptConfirmationAsync` | Displays a confirmation dialog with options for the user to confirm or cancel an action. | Dashboard only | | `PromptInputAsync` | Prompts the user for a single input value, such as text or secret. | Dashboard, CLI | | `PromptInputsAsync` | Prompts the user for multiple input values in a single dialog (dashboard) or sequentially (CLI). | Dashboard, CLI | +| `PromptProgressAsync` | Displays a progress dialog with an indeterminate progress indicator during long-running operations. | Dashboard only | ## Where to use the interaction service @@ -134,7 +156,10 @@ Message display methods (`PromptMessageBoxAsync` and `PromptNotificationAsync`) Dialog messages provide important information that requires user attention. -The `IInteractionService.PromptMessageBoxAsync` method displays a message with customizable response options. +The `PromptMessageBoxAsync` method displays a message with customizable response options. + + + ```csharp title="AppHost.cs" var interactionService = services.GetRequiredService(); @@ -168,6 +193,33 @@ else } ``` + + + +```typescript title="apphost.mts" +const interactionService = + await context.services().getInteractionService(); + +const result = await interactionService.promptMessageBox( + "Apply migrations", + "The database schema is out of date.\nWould you like to apply pending migrations now?", + { + enableMessageMarkdown: true, + primaryButtonText: "Apply", + secondaryButtonText: "Skip", + showSecondaryButton: true, + }, +); + +if (!result.canceled) { + // User clicked "Apply" + // Run migrations. +} +``` + + + + **Dashboard view:** Aspire dashboard interface showing a message dialog with a title, message, and buttons. @@ -186,60 +238,65 @@ In the dashboard, notification messages appear stacked at the top, so you can sh The `PromptNotificationAsync` method displays informational messages with optional action links in the dashboard context. You don't have to await the result of a notification message if you don't need to. This is especially useful for notifications, since you might want to display a notification and continue without waiting for user to dismiss it. + + + ```csharp title="AppHost.cs" var interactionService = services.GetRequiredService(); -// Demonstrating various notification types with different intents -var tasks = new List -{ - interactionService.PromptNotificationAsync( - title: "Confirmation", - message: "Are you sure you want to proceed?", - options: new NotificationInteractionOptions - { - Intent = MessageIntent.Confirmation - }), - interactionService.PromptNotificationAsync( - title: "Success", - message: "Your operation completed successfully.", - options: new NotificationInteractionOptions - { - Intent = MessageIntent.Success, - LinkText = "View Details", - LinkUrl = "/" - }), - interactionService.PromptNotificationAsync( - title: "Warning", - message: "Your SSL certificate will expire soon.", - options: new NotificationInteractionOptions - { - Intent = MessageIntent.Warning, - LinkText = "Renew Certificate", - LinkUrl = "https://portal.azure.com/certificates" - }), - interactionService.PromptNotificationAsync( - title: "Information", - message: "There is an update available for your application.", - options: new NotificationInteractionOptions - { - Intent = MessageIntent.Information, - LinkText = "Update Now", - LinkUrl = "/" - }), - interactionService.PromptNotificationAsync( - title: "Error", - message: "An error occurred while processing your request.", - options: new NotificationInteractionOptions - { - Intent = MessageIntent.Error, - LinkText = "Troubleshoot", - LinkUrl = "/get-started/troubleshooting/" - }) -}; -await Task.WhenAll(tasks); +await interactionService.PromptNotificationAsync( + title: "Success", + message: "Your operation completed successfully.", + options: new NotificationInteractionOptions + { + Intent = MessageIntent.Success, + LinkText = "View Details", + LinkUrl = "/" + }); + +await interactionService.PromptNotificationAsync( + title: "Warning", + message: "Your SSL certificate will expire soon.", + options: new NotificationInteractionOptions + { + Intent = MessageIntent.Warning, + LinkText = "Renew Certificate", + LinkUrl = "https://portal.azure.com/certificates" + }); +``` + + + + +```typescript title="apphost.mts" +const interactionService = + await context.services().getInteractionService(); + +await interactionService.promptNotification( + "Success", + "Your operation completed successfully.", + { + intent: MessageIntent.Success, + linkText: "View Details", + linkUrl: "/", + }, +); + +await interactionService.promptNotification( + "Warning", + "Your SSL certificate will expire soon.", + { + intent: MessageIntent.Warning, + linkText: "Renew Certificate", + linkUrl: "https://portal.azure.com/certificates", + }, +); ``` -The previous example demonstrates several ways to use the notification API. Each approach displays different types of notifications, all of which are invoked in parallel and displayed in the dashboard around the same time. + + + +The previous example demonstrates how to use the notification API with different intents and optional action links. **Dashboard view:** @@ -257,6 +314,9 @@ Use the interaction service when you need the user to confirm an action before p For operations that can't be undone, such as deleting resources, always prompt for confirmation: + + + ```csharp title="AppHost.cs" var interactionService = services.GetRequiredService(); // Prompt for confirmation before resetting database @@ -278,6 +338,33 @@ if (resetConfirmation.Data) } ``` + + + +```typescript title="apphost.mts" +const interactionService = + await context.services().getInteractionService(); + +const resetConfirmation = await interactionService.promptConfirmation( + "Confirm Reset", + "Are you sure you want to reset the database? This action cannot be undone.", + { + intent: MessageIntent.Confirmation, + primaryButtonText: "Reset", + secondaryButtonText: "Cancel", + showSecondaryButton: true, + enableMessageMarkdown: true, + }, +); + +if (!resetConfirmation.canceled) { + // Perform the reset operation... +} +``` + + + + **Dashboard view:** Aspire dashboard interface showing a confirmation dialog with a title, message, and buttons for confirming or canceling the operation. @@ -288,7 +375,7 @@ The `PromptConfirmationAsync` method isn't available in CLI contexts. If you cal ## Prompt for user input -The interaction service API allows you to prompt users for input in various ways. You can collect single values or multiple values, with support for different input types including text, secrets, choices, booleans, and numbers. The presentation adapts automatically to the execution context. +The interaction service API allows you to prompt users for input in various ways. You can collect single values or multiple values, with support for different input types including text, secrets, choices, booleans, numbers, and files. The presentation adapts automatically to the execution context. | Type | Dashboard | CLI prompt | |--------------|---------------------------|-----------------------| @@ -297,6 +384,7 @@ The interaction service API allows you to prompt users for input in various ways | `Choice` | Dropdown box of options | Choice prompt | | `Boolean` | Checkbox | Boolean choice prompt | | `Number` | Number textbox | Text prompt | +| `File` | File picker dialog | File path prompt | Each `InteractionInput` has a `Name` and a `Label` property: @@ -313,6 +401,9 @@ It's possible to create wizard-like flows using the interaction service. By chai Consider the following example, which prompts the user for multiple input values: + + + ```csharp title="AppHost.cs" var interactionService = services.GetRequiredService(); var loggerService = services.GetRequiredService(); @@ -383,6 +474,64 @@ if (!appConfigurationInput.Canceled) } ``` + + + +```typescript title="apphost.mts" +const interactionService = + await context.services().getInteractionService(); + +const appName = await interactionService.createTextInput("AppName", { + label: "Application Name", + required: true, + placeholder: "my-app", +}); + +const environment = await interactionService.createChoiceInput( + "Environment", + { + choices: [ + { value: "dev", label: "Development" }, + { value: "staging", label: "Staging" }, + { value: "test", label: "Testing" }, + ], + options: { required: true }, + }, +); + +const instanceCount = await interactionService.createNumberInput( + "InstanceCount", + { + label: "Instance Count", + required: true, + placeholder: "1", + }, +); + +const enableMonitoring = await interactionService.createBooleanInput( + "EnableMonitoring", + { label: "Enable Monitoring" }, +); + +const result = await interactionService.promptInputs( + "Application Configuration", + "Configure your application deployment settings:", + [appName, environment, instanceCount, enableMonitoring], +); + +if (!(await result.canceled())) { + const name = await result.inputs().value("AppName"); + const env = await result.inputs().value("Environment"); + const count = await result.inputs().value("InstanceCount"); + const monitoring = await result.inputs().value("EnableMonitoring"); + + // Use the collected values as needed +} +``` + + + + **Dashboard view:** This renders on the dashboard as shown in the following image: @@ -438,13 +587,16 @@ Environment: The interaction service supports dynamic inputs so you can populate options based on earlier responses. This enables cascading dropdowns and other dependent prompts in both the dashboard and CLI experiences. + + + To configure a dynamic input, set the `InteractionInput.DynamicLoading` property to an instance of `InputLoadOptions`. `InputLoadOptions` has a range of properties: - **LoadCallback**: A callback function that populates or refreshes the input options. This function is invoked when the input needs to be loaded or refreshed. You can access the current values of other inputs through the `context.AllInputs` dictionary. - **DependsOnInputs**: A list of input names that the dynamic input depends on. When any of these inputs change, the `LoadCallback` is triggered to refresh the options for the dynamic input. If no dependencies are specified then the callback is triggered when the interaction starts. - **AlwaysLoadOnStart**: If set to `true`, the `LoadCallback` is always called when the interaction starts, even if the dynamic input has dependencies. -```csharp +```csharp title="AppHost.cs" var inputs = new List { new() @@ -491,15 +643,66 @@ if (!result.Canceled) } ``` -In the preceding example, the `DatabaseVersion` input is dynamically populated based on the selected `DatabaseType`. + + + +Use `withDynamicLoading` to populate options based on other inputs. The callback runs server-side, receives a `loadContext`, and updates the loading input through `loadContext.input()`: + +```typescript title="apphost.mts" +const interactionService = + await context.services().getInteractionService(); + +const dbType = await interactionService.createChoiceInput("DatabaseType", { + label: "Database Type", + choices: [ + { value: "postgres", label: "PostgreSQL" }, + { value: "mysql", label: "MySQL" }, + { value: "sqlserver", label: "SQL Server" }, + ], + options: { required: true }, +}); + +const dbVersion = await ( + await interactionService.createChoiceInput("DatabaseVersion", { + label: "Database Version", + options: { required: true }, + }) +).withDynamicLoading( + async (loadContext) => { + const selectedType = await loadContext.inputs().value("DatabaseType"); + const versions = await getAvailableVersions(selectedType); + await loadContext.input().setChoiceOptions(versions); + }, + { dependsOnInputs: ["DatabaseType"] }, +); + +const result = await interactionService.promptInputs( + "Database configuration", + "Select a database type and version", + [dbType, dbVersion], +); + +if (!(await result.canceled())) { + const version = await result.inputs().value("DatabaseVersion"); + // Use version-specific configuration... +} +``` + + + + +In the preceding example, the database version input is dynamically populated based on the selected database type. #### Input validation -Basic input validation is available by configuring `InteractionInput`. It provides options for requiring a value, or the maximum text length of `Text` or `SecretText` fields. +Basic input validation is available by configuring inputs with `Required` and `MaxLength` properties. For complex scenarios, you can provide a custom validation callback that runs server-side when the user submits the dialog. + + + -For complex scenarios, you can provide custom validation logic using the `InputsDialogInteractionOptions.ValidationCallback` property: +Use the `InputsDialogInteractionOptions.ValidationCallback` property: -```csharp +```csharp title="AppHost.cs" // Multiple inputs with custom validation var databaseInputs = new List { @@ -572,8 +775,156 @@ if (!dbResult.Canceled && dbResult.Data != null) } ``` + + + +Supply a `validationCallback` on the options object. The callback reads submitted values through `validationContext.inputs().value(name)` and registers per-field errors through `validationContext.addValidationError(field, message)`: + +```typescript title="apphost.mts" +const interactionService = + await context.services().getInteractionService(); + +const password = await interactionService.createSecretInput("password", { + label: "Password", + required: true, + placeholder: "Enter a strong password", +}); + +const confirmPassword = await interactionService.createSecretInput( + "confirmPassword", + { + label: "Confirm password", + required: true, + placeholder: "Confirm your password", + }, +); + +const result = await interactionService.promptInputs( + "Database configuration", + "Configure your PostgreSQL database connection:", + [password, confirmPassword], + { + validationCallback: async (validationContext) => { + const pw = await validationContext.inputs().value("password"); + const cpw = await validationContext + .inputs() + .value("confirmPassword"); + + if (pw && pw.length < 8) { + await validationContext.addValidationError( + "password", + "Password must be at least 8 characters long", + ); + } + if (pw !== cpw) { + await validationContext.addValidationError( + "confirmPassword", + "Passwords do not match", + ); + } + }, + }, +); + +if (!(await result.canceled())) { + // Use the validated configuration +} +``` + + + + Prompting the user for a password and confirming they match is referred to as "dual independent verification" input. This approach is common in scenarios where you want to ensure the user enters the same password twice to avoid typos or mismatches. +### Prompt for file upload + +The `File` input type enables users to select and upload files through the native OS/browser file picker in the dashboard, or by providing a file path in the CLI. Files are uploaded to the AppHost and stored on disk, with metadata available through the `InteractionFile` class. + + + + +```csharp title="AppHost.cs" +var interactionService = services.GetRequiredService(); + +var fileInput = new InteractionInput +{ + Name = "ConfigFile", + InputType = InputType.File, + Label = "Configuration file", + Placeholder = "Select a file to import", + Required = true, + FileFilter = ".json,.yaml,.yml", + MaxFileSize = 10 * 1024 * 1024 // 10 MB +}; + +var result = await interactionService.PromptInputAsync( + "Import configuration", + "Select a configuration file to import.", + fileInput, + cancellationToken: cancellationToken); + +if (result.Canceled) +{ + return CommandResults.Failure("Canceled"); +} + +var file = result.Data.Files?[0]; +if (file is null) +{ + return CommandResults.Failure("No file uploaded"); +} + +// Read all file content as bytes +var content = await file.ReadAllBytesAsync(cancellationToken); + +// Or open as a stream for large files +await using var stream = file.OpenRead(); +``` + + + + +```typescript title="apphost.mts" +const interactionService = + await context.services().getInteractionService(); + +const fileInput = await interactionService.createFileInput("ConfigFile", { + label: "Configuration file", + description: "Select a file to import", + required: true, + maxFileSize: 10 * 1024 * 1024, // 10 MB +}); + +const result = await interactionService.promptInput( + "Import configuration", + "Select a configuration file to import.", + fileInput, + { primaryButtonText: "Upload" }, +); + +if (result.canceled) { + return { success: false, message: "Canceled" }; +} + +const files = result.input?.files ?? []; +if (files.length === 0) { + return { success: false, message: "No file uploaded" }; +} + +const file = files[0]; +// file.name - original filename +// file.filePath - path to uploaded file on disk +``` + + + + +The `InteractionInput` for file uploads supports these additional properties: + +- **`FileFilter`**: Restricts selectable files using the same format as the HTML `accept` attribute (for example, `".pem,.pfx,.crt"` or `"image/*"`). +- **`MaxFileSize`**: Maximum file size in bytes. Files exceeding this limit are rejected with a validation error. +- **`AllowMultipleFiles`**: When `true`, the user can select more than one file. + ### Best practices for user input When prompting for user input, consider these best practices: @@ -589,6 +940,106 @@ When prompting for user input, consider these best practices: +## Display progress + +The interaction service can display a progress dialog during long-running operations. The progress dialog shows an indeterminate progress indicator with an optional title, message, and cancel button. This is useful for scenarios such as resource deployment, data processing, or Azure provisioning where the user should wait for an operation to complete. + +:::caution[Experimental API] +`PromptProgressAsync` and its associated types (`ProgressInteractionOptions`, `ProgressContext`) are experimental and require suppressing the [`ASPIREINTERACTION001`](/diagnostics/aspireinteraction001/) diagnostic. The API shape may change before it stabilizes. +::: + +:::note +The `PromptProgressAsync` method is only available in dashboard contexts. It throws an exception if called during `aspire publish` or `aspire deploy` operations. +::: + +### Display progress with a work callback + +The simplest approach is to provide a `Work` callback via `ProgressInteractionOptions`. The progress dialog opens when `PromptProgressAsync` is called and closes automatically when the callback completes: + + + + +```csharp title="AppHost.cs" +#pragma warning disable ASPIREINTERACTION001 + +var interactionService = services.GetRequiredService(); +var result = await interactionService.PromptProgressAsync( + "Please wait while resources are being downloaded...", + "Downloading resources", + new ProgressInteractionOptions + { + PrimaryButtonText = "Cancel", + Work = async progress => + { + await DownloadResourcesAsync(progress.CancellationToken); + } + }, + cancellationToken); + +if (result.Canceled) +{ + // User clicked the cancel button. +} +``` + + + + +```typescript title="apphost.mts" +const interactionService = + await context.services().getInteractionService(); + +const result = await interactionService.promptProgress( + "Please wait while resources are being downloaded...", + { + title: "Downloading resources", + options: { + primaryButtonText: "Cancel", + work: async () => { + await downloadResources(); + }, + }, + }, +); + +if (result.canceled) { + // User clicked the cancel button. +} +``` + + + + +When a `Work` callback is provided: + +- The dialog stays open while the callback runs. +- If a cancel button is shown (via `PrimaryButtonText`), clicking it triggers `ProgressContext.CancellationToken`, which you can observe inside the callback. +- The method returns an `InteractionResult` with `Canceled = true` if the user clicked cancel. + +### Display progress without a work callback + +If you don't provide a `Work` callback, the progress dialog stays open until the `cancellationToken` passed to `PromptProgressAsync` is canceled, or the user clicks the cancel button (if `PrimaryButtonText` is set). This pattern is useful when you manage the work yourself and want to close the dialog at a specific point: + +```csharp title="AppHost.cs" +#pragma warning disable ASPIREINTERACTION001 + +var interactionService = services.GetRequiredService(); +using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + +var progressTask = interactionService.PromptProgressAsync( + "Processing data...", + "Processing", + cancellationToken: cts.Token); + +// Do work, then close the dialog by canceling the token. +await DoWorkAsync(cancellationToken); +cts.Cancel(); + +await progressTask; +``` + +Because no `PrimaryButtonText` is set, the dialog has no cancel button and the user cannot dismiss it. + ## Interaction contexts The interaction service behaves differently depending on how your Aspire solution is launched: @@ -599,6 +1050,7 @@ When you run your application using `aspire run` or by directly launching the Ap - **Modal dialogs**: Message boxes and input prompts appear as overlay dialogs that require user interaction. - **Notification messages**: Informational messages appear as dismissible banners at the top of the dashboard. +- **Progress dialogs**: Long-running operations display a non-dismissable progress indicator with an optional cancel button. - **Rich UI**: Full support for interactive form elements, validation, and visual feedback. - **All methods available**: All interaction service methods are supported in dashboard contexts. @@ -608,10 +1060,10 @@ When you run `aspire publish` or `aspire deploy`, interactions are prompted thro - **Text prompts**: Only input prompts (`PromptInputAsync` and `PromptInputsAsync`) are available and appear as text-based prompts in the terminal. - **Sequential input**: Multiple inputs are requested one at a time rather than in a single dialog. -- **Limited functionality**: Message boxes, notifications, and confirmation dialogs aren't available and throw exceptions if called. +- **Limited functionality**: Message boxes, notifications, confirmation dialogs, and progress dialogs aren't available and throw exceptions if called. ## See also diff --git a/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx b/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx index e1ddf9ea4..8d5b8cdda 100644 --- a/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx +++ b/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx @@ -440,6 +440,8 @@ DTOs are input objects, not live resource handles. Keep `[AspireDto]` types JSON For an API with one optional options DTO, prefer a flat generated call shape such as `addMyResource('name', { port: 5432 })` instead of requiring `{ options: { port: 5432 } }`. If an existing C# API uses a rich model type that doesn't serialize cleanly, add a small DTO or options type for the exported API. +Both generators flatten the call shape when a method's optional parameters reduce to a single DTO named options. The Go generator applies this as of Aspire 13.5 and only when there's no coexisting cancellation token or callback. TypeScript has flattened single options DTOs since Aspire 13.3; it additionally still flattens when a trailing cancellation token is present, rendering the token as its own parameter. + ```csharp title="Flat options DTO" [AspireDto] public sealed class AddMyWorkerOptions @@ -463,13 +465,20 @@ public static IResourceBuilder AddMyWorker( } ``` -```typescript title="Flat generated call shape" +```typescript title="Flat generated call shape (TypeScript)" const worker = await builder.addMyWorker('worker', { imageTag: '1.2.3', args: ['--verbose'], }); ``` +```go title="Flat generated call shape (Go)" +worker, err := builder.AddMyWorker("worker", &aspire.AddMyWorkerOptions{ + ImageTag: aspire.StringPtr("1.2.3"), + Args: []string{"--verbose"}, +}) +``` + Collection-valued DTO inputs should also feel like plain JSON in the generated SDK: ```csharp title="Collection-valued DTO input" diff --git a/src/frontend/src/content/docs/fr/index.mdx b/src/frontend/src/content/docs/fr/index.mdx index cb1b05e1f..57736c3ab 100644 --- a/src/frontend/src/content/docs/fr/index.mdx +++ b/src/frontend/src/content/docs/fr/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 est disponible !Découvrez les nouveautés d'Aspire 13.4. + ✨ Aspire 13.5 est disponible !Découvrez les nouveautés d'Aspire 13.5. hero: tagline: Votre stack, simplifiée.

Orchestrez vos frontends, APIs, conteneurs et bases de données sans effort — sans réécriture, sans limites. Étendez Aspire pour propulser n'importe quel projet.

image: diff --git a/src/frontend/src/content/docs/fundamentals/custom-resource-commands.mdx b/src/frontend/src/content/docs/fundamentals/custom-resource-commands.mdx index 288af9e1b..4ce592584 100644 --- a/src/frontend/src/content/docs/fundamentals/custom-resource-commands.mdx +++ b/src/frontend/src/content/docs/fundamentals/custom-resource-commands.mdx @@ -156,7 +156,7 @@ The `WithCommand` API adds the appropriate annotations to the resource, which ar - `displayName`: The name of the command to display in the dashboard. - `executeCommand`: The callback that runs when the command is invoked. In C# the type is `Func>`; in TypeScript it's `(context: ExecuteCommandContext) => Promise`. - `updateState`: A callback that decides the command's enabled state in the dashboard. In C# it's `Func` and supplied via `CommandOptions.UpdateState`; in TypeScript it's `(context: UpdateCommandStateContext) => Promise` and supplied through the `updateState` field on `CommandOptions`. -- `iconName`: The name of the icon to display in the dashboard. The icon is optional, but when you do provide it, it should be a valid [Fluent UI Blazor icon name](https://www.fluentui-blazor.net/Icon#explorer). +- `iconName`: The name of the icon to display in the dashboard. The icon is optional. Use a valid [Fluent UI Blazor icon name](https://www.fluentui-blazor.net/Icon#explorer). If the name isn't recognized, the dashboard renders a question-mark circle (❓) icon as a fallback. - `iconVariant`: The variant of the icon to display in the dashboard, valid options are `Regular` (default) or `Filled`. ## Execute command logic @@ -629,7 +629,7 @@ The `ExecuteCommandResult` type carries the outcome of a command. It exposes the - **`Success` / `success`** _(boolean)_ — Whether the command completed successfully. - **`Canceled` / `canceled`** _(boolean)_ — Whether the command was canceled by the user. - **`Message` / `message`** _(string?)_ — A human-readable message that surfaces in the dashboard [notification center](/dashboard/explore/#notification-center) and CLI output. Used for both success messages and error messages. -- **`Data` / `data`** _(`CommandResultData?`)_ — Optional structured output (plain text, JSON, or Markdown). See [Return structured output from a command](#return-structured-output-from-a-command). +- **`Data` / `data`** _(`CommandResultData?`)_ — Optional structured output (plain text, JSON, or Markdown). See [Return a result from a command](#return-a-result-from-a-command). +## Command progress + +Commands can display a progress dialog in the dashboard while they execute. The progress dialog shows an indeterminate progress indicator with an optional title, message, and cancel button. Set `CommandOptions.Progress` to a `CommandProgressOptions` value with a `Message` to enable this behavior — the dialog only appears when `Message` is set. + + + + +```csharp title="AppHost.cs" +builder.AddProject("api") + .WithCommand( + name: "seed-database", + displayName: "Seed Database", + executeCommand: async context => + { + await SeedDatabaseAsync(context.CancellationToken); + return CommandResults.Success("Database seeded."); + }, + commandOptions: new CommandOptions + { + IconName = "DatabaseArrowDown", + Progress = new CommandProgressOptions + { + Title = "Seeding", + Message = "Populating database with sample data..." + } + }); +``` + + + + +```typescript title="apphost.mts" +await api.withCommand( + "seed-database", + "Seed Database", + async () => { + await seedDatabase(); + return { success: true, message: "Database seeded." }; + }, + { + commandOptions: { + iconName: "DatabaseArrowDown", + progress: { + title: "Seeding", + message: "Populating database with sample data...", + }, + }, + }, +); +``` + + + + +When `CommandProgressOptions.Message` is set, the dashboard automatically shows the progress dialog when the command starts and closes it when the command completes. By default, a **Cancel** button is shown. Clicking it cancels the command via `ExecuteCommandContext.CancellationToken`. + +To hide the cancel button, set `HideCancelButton` to `true`: + + + + +```csharp title="AppHost.cs" +var commandOptions = new CommandOptions +{ + Progress = new CommandProgressOptions + { + Message = "Finalizing...", + HideCancelButton = true + } +}; +``` + + + + +```typescript title="apphost.mts" +{ + commandOptions: { + progress: { + message: "Finalizing...", + hideCancelButton: true, + }, + }, +} +``` + + + + +For more control over progress behavior, including custom work callbacks and programmatic dismissal, use `IInteractionService.PromptProgressAsync` directly. For more information, see [Display progress](/extensibility/interaction-service/#display-progress). + ## Process-backed resource commands :::caution[Experimental API] diff --git a/src/frontend/src/content/docs/fundamentals/http-commands.mdx b/src/frontend/src/content/docs/fundamentals/http-commands.mdx index e4ee9f656..a3d17650d 100644 --- a/src/frontend/src/content/docs/fundamentals/http-commands.mdx +++ b/src/frontend/src/content/docs/fundamentals/http-commands.mdx @@ -137,7 +137,7 @@ The preceding code: ## Return the HTTP response body -By default, an HTTP command uses the HTTP status code to determine whether the command succeeded, and it doesn't return the response body as command result data. Set `HttpCommandOptions.ResultMode` to opt in to returning a non-empty response body from the endpoint. The body is surfaced as the command's structured output, just like [structured output from custom resource commands](/fundamentals/custom-resource-commands/#return-structured-output-from-a-command). +By default, an HTTP command uses the HTTP status code to determine whether the command succeeded, and it doesn't return the response body as command result data. Set `HttpCommandOptions.ResultMode` to opt in to returning a non-empty response body from the endpoint. The body is surfaced as the command's structured output, just like [structured output from custom resource commands](/fundamentals/custom-resource-commands/#return-a-result-from-a-command). The following example configures an HTTP command that returns a JSON response body: diff --git a/src/frontend/src/content/docs/fundamentals/networking-overview.mdx b/src/frontend/src/content/docs/fundamentals/networking-overview.mdx index b2fcaaf7f..148806a87 100644 --- a/src/frontend/src/content/docs/fundamentals/networking-overview.mdx +++ b/src/frontend/src/content/docs/fundamentals/networking-overview.mdx @@ -55,7 +55,7 @@ The inner loop is the process of developing and testing your app locally before - **Endpoints/Endpoint configurations**: Endpoints are the connections between your app and the services it depends on, such as databases, message queues, or APIs. Endpoints provide information such as the service name, host port, scheme, and environment variable. Aspire can create endpoints automatically from resource configuration, and you can also add them explicitly by calling `WithEndpoint`. - **Proxies**: Aspire automatically launches a proxy for each proxied service binding you add to your app, and assigns a port for the proxy to listen on. The proxy then forwards the requests to the port that your app listens on, which might be different from the proxy port. This way, you can avoid port conflicts and access your app and services using consistent and predictable URLs. -- **Proxyless endpoints**: Endpoints with `IsProxied` set to `false` connect directly to the resource instead of routing through an Aspire-managed proxy. Starting in Aspire 13.4, endpoints on persistent resources are proxyless by default. Proxyless endpoints must specify a target port so Aspire knows which port the resource listens on. +- **Proxyless endpoints**: Endpoints with `IsProxied` set to `false` connect directly to the resource instead of routing through an Aspire-managed proxy. Starting in Aspire 13.4, endpoints on persistent executables and projects are proxyless by default. Persistent containers use proxied endpoints by default, the same as session containers. - **Container networks**: Aspire creates and manages dedicated networks for container resources so containers can discover and communicate with each other during local development. ## How endpoints work @@ -230,9 +230,9 @@ The underlying service still listens on its own port, and Aspire makes that allo Most endpoints are proxied by default. A proxyless endpoint skips the Aspire-managed proxy and exposes the resource's listener directly. Use a proxyless endpoint when a proxy would interfere with the resource's lifetime or networking behavior. -Starting in Aspire 13.4, endpoints on persistent resources are proxyless by default. This lets the persistent resource keep using its direct endpoint across AppHost runs instead of depending on a new proxy instance each time. This also ensures that the ports your service is reachable on stay stable whether the AppHost is running or not. +Starting in Aspire 13.4, endpoints on persistent executables and projects are proxyless by default. This lets the persistent resource keep using its direct endpoint across AppHost runs instead of depending on a new proxy instance each time. This also ensures that the ports your service is reachable on stay stable whether the AppHost is running or not. Persistent containers use proxied endpoints by default, the same as session containers, so integrations that depend on endpoint allocation before startup continue to work. -Because there's no proxy to listen on one port and forward to another, every proxyless endpoint must include a target port. The target port tells Aspire which port the resource listens on. You can also set `port` when you need a specific host port, but `targetPort` is still required for proxyless endpoints. +The following example configures a proxyless HTTP endpoint for an nginx container: @@ -260,6 +260,50 @@ await web.withHttpEndpoint({ targetPort: 80, isProxied: false }); +### Allocate ports for dynamic proxyless endpoints + +Proxyless endpoints can omit the public host `port`. Starting in Aspire 13.5, Aspire allocates a public host port for each dynamic proxyless endpoint before workload resources are created. The allocated port is then available to configuration that resolves endpoint properties, such as `GetEndpoint(...).Property(EndpointProperty.Port)`. + +If you set `port`, Aspire uses that value as the public host port. If you omit `port`, Aspire allocates one from the proxyless endpoint port range. The default range is `10000-32767`, and you can override it with the `ASPIRE_PROXYLESS_ENDPOINT_PORT_RANGE` environment variable in `start-end` format. Container endpoints still require `targetPort` so Aspire knows which port inside the container receives traffic. + +This lets you write patterns like the following, where a container exposes its allocated public port in an environment variable: + + + + +```csharp title="AppHost.cs" +var database = builder.AddContainer("database", "image") + .WithEndpoint(name: "tcp", targetPort: 5432, isProxied: false); + +database.WithEnvironment("PUBLIC_PORT", database.GetEndpoint("tcp").Property(EndpointProperty.Port)); +``` + + + + +```typescript title="apphost.mts" twoslash +import { EndpointProperty, createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const database = await builder.addContainer('database', { + image: 'image', + tag: 'latest', +}); +await database.withEndpoint({ name: 'tcp', targetPort: 5432, isProxied: false }); + +await database.withEnvironment('PUBLIC_PORT', database.getEndpoint('tcp').property(EndpointProperty.Port)); +``` + + + + +In this example, `PUBLIC_PORT` is set to the endpoint's allocated public host port before the container is created. Because the endpoint is proxyless and doesn't specify `port`, Aspire allocates a port from the proxyless endpoint port range. + +:::note +For persistent resources, Aspire stores allocated proxyless endpoint ports in user secrets when user secrets are available and reuses them on later AppHost runs. If Aspire can't persist the allocated port, configure a fixed public `port` or use a proxied endpoint to avoid recreating the persistent resource on each run. +::: + ## Omit the host port For proxied endpoints, when you omit the host port, Aspire generates a random port for both host and service port. This is useful when you want to avoid port conflicts and don't care about the host or service port. Consider the following code: @@ -405,7 +449,7 @@ The `AllocatedEndpoint` property allows you to get or set the endpoint for a ser on. -The `Name` property identifies the service, whereas the `Port` and `TargetPort` properties specify the desired and listening ports, respectively. `TargetPort` is required for proxyless endpoints, including endpoints with `IsProxied` set to `false` and, starting in Aspire 13.4, endpoints on persistent resources by default. +The `Name` property identifies the service, whereas the `Port` and `TargetPort` properties specify the desired and listening ports, respectively. `TargetPort` is required for proxyless endpoints, including endpoints with `IsProxied` set to `false` and, starting in Aspire 13.4, endpoints on persistent executables and projects by default. For network communication, the `Protocol` property supports **TCP** and **UDP**, with potential for more in the future, and the `Transport` property indicates the transport protocol (**HTTP**, **HTTP2**, **HTTP3**). Lastly, if the service is URI-addressable, the `UriScheme` property provides the URI scheme for constructing the service URI. diff --git a/src/frontend/src/content/docs/fundamentals/persist-data-volumes.mdx b/src/frontend/src/content/docs/fundamentals/persist-data-volumes.mdx index b3608b682..8516303e3 100644 --- a/src/frontend/src/content/docs/fundamentals/persist-data-volumes.mdx +++ b/src/frontend/src/content/docs/fundamentals/persist-data-volumes.mdx @@ -272,3 +272,4 @@ You can apply the volume concepts in the preceding code to a variety of services - [Tutorial: Connect an ASP.NET Core app to SQL Server using Aspire and Entity Framework Core](/integrations/databases/efcore/sql-server/sql-server-get-started/) - [Aspire orchestration overview](/get-started/app-host/) +- [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/) diff --git a/src/frontend/src/content/docs/get-started/add-aspire-existing-app.mdx b/src/frontend/src/content/docs/get-started/add-aspire-existing-app.mdx index 98435156b..8886bbfc8 100644 --- a/src/frontend/src/content/docs/get-started/add-aspire-existing-app.mdx +++ b/src/frontend/src/content/docs/get-started/add-aspire-existing-app.mdx @@ -144,8 +144,8 @@ Use a file-based AppHost when you want a lightweight single-file orchestrator wi 3. Wire the resources in `apphost.cs`: ```csharp title="apphost.cs" - #:sdk Aspire.AppHost.Sdk@13.2.0 - #:package Aspire.Hosting.Redis@13.2.0 + #:sdk Aspire.AppHost.Sdk@%ASPIRE_VERSION% + #:package Aspire.Hosting.Redis@%ASPIRE_VERSION% #pragma warning disable ASPIRECSHARPAPPS001 @@ -398,10 +398,10 @@ Common examples include Node.js apps, Vite frontends, Python workers, and Uvicor ```csharp title="apphost.cs — Existing services with hosting integrations" -#:sdk Aspire.AppHost.Sdk@13.3.0 -#:package Aspire.Hosting.Redis@13.3.0 -#:package Aspire.Hosting.Python@13.3.0 -#:package Aspire.Hosting.JavaScript@13.3.0 +#:sdk Aspire.AppHost.Sdk@%ASPIRE_VERSION% +#:package Aspire.Hosting.Redis@%ASPIRE_VERSION% +#:package Aspire.Hosting.Python@%ASPIRE_VERSION% +#:package Aspire.Hosting.JavaScript@%ASPIRE_VERSION% var builder = DistributedApplication.CreateBuilder(args); @@ -478,9 +478,9 @@ aspire add redis ```csharp title="apphost.cs — Existing containers and shared infrastructure" -#:sdk Aspire.AppHost.Sdk@13.3.0 -#:package Aspire.Hosting.PostgreSQL@13.3.0 -#:package Aspire.Hosting.Redis@13.3.0 +#:sdk Aspire.AppHost.Sdk@%ASPIRE_VERSION% +#:package Aspire.Hosting.PostgreSQL@%ASPIRE_VERSION% +#:package Aspire.Hosting.Redis@%ASPIRE_VERSION% var builder = DistributedApplication.CreateBuilder(args); @@ -569,8 +569,8 @@ services: ```csharp title="apphost.cs" showLineNumbers -#:sdk Aspire.AppHost.Sdk@13.3.0 -#:package Aspire.Hosting.PostgreSQL@13.3.0 +#:sdk Aspire.AppHost.Sdk@%ASPIRE_VERSION% +#:package Aspire.Hosting.PostgreSQL@%ASPIRE_VERSION% var builder = DistributedApplication.CreateBuilder(args); diff --git a/src/frontend/src/content/docs/get-started/aspire-sdk.mdx b/src/frontend/src/content/docs/get-started/aspire-sdk.mdx index 0e602be5a..ad75e3510 100644 --- a/src/frontend/src/content/docs/get-started/aspire-sdk.mdx +++ b/src/frontend/src/content/docs/get-started/aspire-sdk.mdx @@ -17,7 +17,7 @@ The [📦 Aspire.AppHost.Sdk](https://www.nuget.org/packages/Aspire.AppHost.Sdk) The `Aspire.AppHost.Sdk` is defined in the top-level `Project` node's `Sdk` attribute: ```xml title='*.csproj file' {1} - + Exe @@ -35,7 +35,7 @@ The `Aspire.AppHost.Sdk` is defined in the top-level `Project` node's `Sdk` attr The `Aspire.AppHost.Sdk` is defined in a file-based app's source file using the `#:sdk` directive: ```csharp title='file-based app' {1} -#:sdk Aspire.AppHost.Sdk@13.3.0 +#:sdk Aspire.AppHost.Sdk@%ASPIRE_VERSION% var builder = DistributedApplication.CreateBuilder(args); @@ -115,8 +115,8 @@ When `AspireUseCliBundle` is `true`: - `Aspire.AppHost.Sdk` still sets AppHost properties and adds the implicit `Aspire.Hosting.AppHost` package. - Your AppHost uses the DCP and Dashboard versions installed with the Aspire CLI. - At build time, the SDK resolves the DCP and Dashboard executables in this priority order: - 1. Explicit `AspireCliBundlePath` / `AspireCliPath` properties in the project file. - 2. The `aspire` executable on `PATH`. + 1. Explicit `AspireCliBundlePath` / `AspireCliPath` properties in the project file. + 2. The `aspire` executable on `PATH`. - If the bundle cannot be found, the build emits error `ASPIRE009` with a message directing you to [get.aspire.dev](https://get.aspire.dev) to install the Aspire CLI. #### Enable the opt-in diff --git a/src/frontend/src/content/docs/get-started/install-cli.mdx b/src/frontend/src/content/docs/get-started/install-cli.mdx index b1d7abb1f..762b881eb 100644 --- a/src/frontend/src/content/docs/get-started/install-cli.mdx +++ b/src/frontend/src/content/docs/get-started/install-cli.mdx @@ -16,7 +16,7 @@ import { Tabs, } from '@astrojs/starlight/components'; -Aspire provides a command-line interface (CLI) tool to help you create and manage Aspire-based apps. The CLI streamlines your development workflow with an interactive-first experience. This guide shows you how to install the Aspire CLI on your system. Choose the package manager that fits your environment — Homebrew, npm, NuGet, WinGet, or mise — or use the install script for direct setup. +Aspire provides a command-line interface (CLI) tool to help you create and manage Aspire-based apps. The CLI streamlines your development workflow with an interactive-first experience. This guide shows you how to install the Aspire CLI on your system. Choose the package manager that fits your environment — Homebrew, npm, NuGet, WinGet, mise, or Nix — or use the install script for direct setup. :::tip[Using VS Code?] The [Aspire VS Code extension](/get-started/aspire-vscode-extension/) can install the CLI for you. From the Command Palette, run: @@ -95,6 +95,68 @@ Install the Aspire CLI with [mise](https://mise.jdx.dev/) when you manage develo mise use -g aspire ``` + + + +Install or run the Aspire CLI from the official Nix flake on Linux and macOS. Nix 2.4 or later with flakes enabled is required. + +Run the Aspire CLI directly without installing it: + +```bash title="Run with Nix" +nix run github:microsoft/aspire#aspire-cli +``` + +Add the Aspire CLI to your Nix profile for persistent access: + +```bash title="Add to Nix profile" +nix profile add github:microsoft/aspire#aspire-cli +``` + +To use the Aspire CLI in a project's `flake.nix`, consume the package output directly: + +```nix title="flake.nix — package output" +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/"; + aspire.url = "github:microsoft/aspire"; + aspire.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { nixpkgs, aspire, ... }: + let + systems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-darwin" + "x86_64-linux" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + devShells = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = pkgs.mkShell { + packages = [ + aspire.packages.${system}.aspire-cli + ]; + }; + } + ); + }; +} +``` + +The Aspire CLI version is pinned by the `aspire` flake input revision and the `eng/nix/versions.json` manifest at that commit. Pin a specific CLI version by locking the `aspire` input to a particular Git revision. + +:::note +When installed via Nix, `aspire update --self` prints the Nix update commands to run rather than downloading a new binary, because the Nix store is read-only. See [`aspire update`](/reference/cli/commands/aspire-update/) for details. +::: + @@ -136,7 +198,7 @@ aspire --version If that command works, you're presented with the version of the Aspire CLI tool: ```bash title="Aspire CLI — Output" data-disable-copy -13.4.0+{commitSHA} +%ASPIRE_VERSION%+{commitSHA} ``` The `+{commitSHA}` suffix indicates the specific commit from which the Aspire CLI was built. diff --git a/src/frontend/src/content/docs/hi/index.mdx b/src/frontend/src/content/docs/hi/index.mdx index 8b9de1f96..9b748eb8f 100644 --- a/src/frontend/src/content/docs/hi/index.mdx +++ b/src/frontend/src/content/docs/hi/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 रिलीज़ हो गया है!Aspire 13.4 में नया क्या है देखें। + ✨ Aspire 13.5 रिलीज़ हो गया है!Aspire 13.5 में नया क्या है देखें। hero: tagline: आपका स्टैक, सरल।

फ्रंटएंड, API, कंटेनर और डेटाबेस को आसानी से ऑर्केस्ट्रेट करें—बिना रीराइट, बिना सीमा। किसी भी प्रोजेक्ट को शक्ति देने के लिए Aspire को विस्तारित करें।

image: diff --git a/src/frontend/src/content/docs/id/index.mdx b/src/frontend/src/content/docs/id/index.mdx index 5e73b900b..c692b12be 100644 --- a/src/frontend/src/content/docs/id/index.mdx +++ b/src/frontend/src/content/docs/id/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 telah dirilis!Lihat apa yang baru di Aspire 13.4. + ✨ Aspire 13.5 telah dirilis!Lihat apa yang baru di Aspire 13.5. hero: tagline: Stack Anda, disederhanakan.

Orkestrasi frontend, API, container, dan database dengan mudah—tanpa menulis ulang, tanpa batasan. Perluas Aspire untuk mendukung proyek apa pun.

image: diff --git a/src/frontend/src/content/docs/index.mdx b/src/frontend/src/content/docs/index.mdx index f0fe93528..dc6fee6c5 100644 --- a/src/frontend/src/content/docs/index.mdx +++ b/src/frontend/src/content/docs/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - Aspire 13.4 is here!See what's new + ✨ Aspire 13.5 is available!Explore the latest features and improvements hero: tagline: Your stack, streamlined.

Orchestrate frontends, APIs, containers, and databases effortlessly—no rewrites, no limits. Extend Aspire to power any project. Free, open-source, and agent ready.

image: diff --git a/src/frontend/src/content/docs/integrations/ai/github-models/github-models-connect.mdx b/src/frontend/src/content/docs/integrations/ai/github-models/github-models-connect.mdx index f62b055b7..7b68bd694 100644 --- a/src/frontend/src/content/docs/integrations/ai/github-models/github-models-connect.mdx +++ b/src/frontend/src/content/docs/integrations/ai/github-models/github-models-connect.mdx @@ -19,6 +19,10 @@ import githubIcon from '@assets/icons/github-icon.png'; data-zoom-off /> +:::caution[Integration deprecated] +The GitHub Models service is **no longer available to new customers**. The `Aspire.Hosting.GitHub.Models` integration is sunset as of Aspire 13.5. All public APIs are marked `[Obsolete]` and the package no longer appears in `aspire add` output. The package will ship one final obsolete release on NuGet and will be removed entirely in a future version. For new and existing apps, use the [Azure AI Foundry integration](/integrations/cloud/azure/azure-ai-foundry/azure-ai-foundry-get-started/) instead, which provides access to a broad catalog of models — including OpenAI's GPT models — and supports local development with `RunAsFoundryLocal()`. See [microsoft/aspire#18402](https://github.com/microsoft/aspire/issues/18402) for details. +::: + This page describes how consuming apps connect to a GitHub Model resource that's already modeled in your AppHost. For the AppHost API surface — adding a model resource, API key parameters, organization configuration, and health checks — see [GitHub Models hosting integration](../github-models-host/). When you reference a GitHub Model resource from your AppHost, Aspire injects the connection information into the consuming app as environment variables. Your app can either read those environment variables directly — the pattern works the same from any language — or, in C#, use the Aspire client integrations for automatic dependency injection, health checks, and telemetry. diff --git a/src/frontend/src/content/docs/integrations/ai/github-models/github-models-get-started.mdx b/src/frontend/src/content/docs/integrations/ai/github-models/github-models-get-started.mdx index be3d232f2..e78f6db59 100644 --- a/src/frontend/src/content/docs/integrations/ai/github-models/github-models-get-started.mdx +++ b/src/frontend/src/content/docs/integrations/ai/github-models/github-models-get-started.mdx @@ -17,6 +17,10 @@ import githubIcon from '@assets/icons/github-icon.png'; data-zoom-off /> +:::caution[Integration deprecated] +The GitHub Models service is **no longer available to new customers**. The `Aspire.Hosting.GitHub.Models` integration is sunset as of Aspire 13.5. All public APIs are marked `[Obsolete]` and the package no longer appears in `aspire add` output. The package will ship one final obsolete release on NuGet and will be removed entirely in a future version. For new and existing apps, use the [Azure AI Foundry integration](/integrations/cloud/azure/azure-ai-foundry/azure-ai-foundry-get-started/) instead, which provides access to a broad catalog of models — including OpenAI's GPT models — and supports local development with `RunAsFoundryLocal()`. See [microsoft/aspire#18402](https://github.com/microsoft/aspire/issues/18402) for details. +::: + [GitHub Models](https://github.com/marketplace/models) provides access to a broad catalog of AI models — including OpenAI's GPT models, DeepSeek, Microsoft's Phi models, and more — through GitHub's infrastructure and your existing GitHub token. The Aspire GitHub Models integration lets you model a GitHub Model resource as a first-class resource in your AppHost, then hand the connection information to any consuming app — regardless of language. ## Why use GitHub Models with Aspire diff --git a/src/frontend/src/content/docs/integrations/ai/github-models/github-models-host.mdx b/src/frontend/src/content/docs/integrations/ai/github-models/github-models-host.mdx index a9053fabd..ff423f159 100644 --- a/src/frontend/src/content/docs/integrations/ai/github-models/github-models-host.mdx +++ b/src/frontend/src/content/docs/integrations/ai/github-models/github-models-host.mdx @@ -17,6 +17,10 @@ import githubIcon from '@assets/icons/github-icon.png'; data-zoom-off /> +:::caution[Integration deprecated] +The GitHub Models service is **no longer available to new customers**. The `Aspire.Hosting.GitHub.Models` integration is sunset as of Aspire 13.5. All public APIs are marked `[Obsolete]` and the package no longer appears in `aspire add` output. The package will ship one final obsolete release on NuGet and will be removed entirely in a future version. For new and existing apps, use the [Azure AI Foundry integration](/integrations/cloud/azure/azure-ai-foundry/azure-ai-foundry-get-started/) instead, which provides access to a broad catalog of models — including OpenAI's GPT models — and supports local development with `RunAsFoundryLocal()`. See [microsoft/aspire#18402](https://github.com/microsoft/aspire/issues/18402) for details. +::: + This article is the reference for the Aspire GitHub Models hosting integration. It enumerates the AppHost APIs — with examples for both `AppHost.cs` and `apphost.mts` — that you use to model GitHub Model resources in your [`AppHost`](/get-started/app-host/) project. If you're new to the GitHub Models integration, start with the [Get started with GitHub Models integrations](/integrations/ai/github-models/github-models-get-started/) guide. For how consuming apps read the connection information this page exposes, see [Connect to GitHub Models](../github-models-connect/). diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-ai-foundry/azure-ai-foundry-host.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-ai-foundry/azure-ai-foundry-host.mdx index 606e9fff3..eb0d27c5b 100644 --- a/src/frontend/src/content/docs/integrations/cloud/azure/azure-ai-foundry/azure-ai-foundry-host.mdx +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-ai-foundry/azure-ai-foundry-host.mdx @@ -188,7 +188,7 @@ The preceding code: - Adds an Azure AI Foundry resource named `foundry`. - Adds a Foundry deployment resource named `chat` using the generated `FoundryModel.OpenAI.Gpt5Mini` descriptor. -If you need a different generated model descriptor, use the corresponding nested type, such as `FoundryModel.Microsoft.Phi4Reasoning`: +If you need a different generated model descriptor, use the corresponding nested type. The `FoundryModel` class organizes models by provider family: `FoundryModel.Microsoft`, `FoundryModel.OpenAI`, `FoundryModel.Cohere`, `FoundryModel.MistralAI`, and others. For example, to use `FoundryModel.Microsoft.Phi4Reasoning`: @@ -663,9 +663,11 @@ await builder.build().run(); -When the AppHost starts up, the local foundry service is also started. This requires the local machine to have [Foundry Local](https://learn.microsoft.com/azure/ai-foundry/foundry-local/get-started) installed and running. +When the AppHost starts, Aspire automatically starts the Foundry Local service using the `foundry` CLI (`foundry service start`). It then discovers the service endpoint, downloads and loads the specified models via CLI commands, and stops the service (`foundry service stop`) when the AppHost shuts down or is disposed. You do not need to pre-start Foundry Local manually. -The `RunAsFoundryLocal` method configures the resource to run as an emulator. It downloads and loads the specified models locally. The method provides health checks for the local service and automatically manages the Foundry Local lifecycle. +This requires the `foundry` CLI to be installed and available on `PATH`. For installation instructions, see [Foundry Local get started](https://learn.microsoft.com/azure/ai-foundry/foundry-local/get-started). + +The `RunAsFoundryLocal` method configures the resource to use the local service. It downloads and loads the specified models locally, provides health checks for the local service, and fully manages the Foundry Local lifecycle — including start and stop — through the `foundry` CLI. +## Persistent volumes + +To create durable storage in a Kubernetes cluster, model it as a first-class `KubernetesPersistentVolumeResource` with `AddPersistentVolume`. Then configure its storage class, capacity, and access modes, and bind it to workloads. At publish time, the volume renders as a `v1.PersistentVolumeClaim`, and any workload bound to it is promoted to a `StatefulSet`. + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Kubernetes; + +var k8s = builder.AddKubernetesEnvironment("k8s"); + +var pgData = k8s.AddPersistentVolume("pg-data") + .WithStorageClass("managed-csi") + .WithCapacity("20Gi"); + +builder.AddPostgres("pg") + .WithDataVolume("pg-data") + .WithPersistentVolume(pgData); +``` + + + + +```typescript title="apphost.mts" +const k8s = await builder.addKubernetesEnvironment('k8s'); + +const pgData = await k8s.addPersistentVolume('pg-data'); +await pgData.withStorageClass('managed-csi'); +await pgData.withCapacity('20Gi'); + +const pg = await builder.addPostgres('pg'); +await pg.withDataVolume({ name: 'pg-data' }); +await pg.withKubernetesPersistentVolume(pgData); +``` + + + + + + For the full configuration surface, binding overloads, access modes, and generated output, see [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/). + + ## Customize individual resources Use `PublishAsKubernetesService` to modify the generated Kubernetes resources for individual services: @@ -356,6 +399,7 @@ For complete walkthroughs, see [Deploy to Kubernetes clusters](/deployment/kuber ## See also - [Deploy to Kubernetes](/deployment/kubernetes/) +- [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/) - [Expose services with Ingress and Gateway API](/deployment/kubernetes-ingress/) - [Configure Ingress on AKS](/deployment/kubernetes-ingress-aks/) - [Configure Gateway API on AKS](/deployment/kubernetes-gateway-aks/) diff --git a/src/frontend/src/content/docs/integrations/databases/efcore/seed-database.mdx b/src/frontend/src/content/docs/integrations/databases/efcore/seed-database.mdx index 985fe362e..f0515f5ec 100644 --- a/src/frontend/src/content/docs/integrations/databases/efcore/seed-database.mdx +++ b/src/frontend/src/content/docs/integrations/databases/efcore/seed-database.mdx @@ -108,7 +108,7 @@ GO You must ensure that this script is copied to the AppHost's output directory, so that Aspire can execute it. Add the following XML to your *.csproj* file: ```xml title="DatabaseContainers.AppHost.csproj" {11-15} - + Exe @@ -129,9 +129,9 @@ You must ensure that this script is copied to the AppHost's output directory, so - - - + + + diff --git a/src/frontend/src/content/docs/integrations/dotnet/csharp-file-based-apps.mdx b/src/frontend/src/content/docs/integrations/dotnet/csharp-file-based-apps.mdx index cd897a885..3481117a6 100644 --- a/src/frontend/src/content/docs/integrations/dotnet/csharp-file-based-apps.mdx +++ b/src/frontend/src/content/docs/integrations/dotnet/csharp-file-based-apps.mdx @@ -212,8 +212,8 @@ The available options are the same as those used with `AddProject`: You can also write the AppHost itself as a file-based C# app using the `#:sdk` directive: ```csharp title="apphost.cs" -#:sdk Aspire.AppHost.Sdk@13.1.0 -#:package Aspire.Hosting.Redis@13.1.0 +#:sdk Aspire.AppHost.Sdk@%ASPIRE_VERSION% +#:package Aspire.Hosting.Redis@%ASPIRE_VERSION% #pragma warning disable ASPIRECSHARPAPPS001 diff --git a/src/frontend/src/content/docs/integrations/frameworks/bun-apps.mdx b/src/frontend/src/content/docs/integrations/frameworks/bun-apps.mdx index 6d609e651..81ad967d4 100644 --- a/src/frontend/src/content/docs/integrations/frameworks/bun-apps.mdx +++ b/src/frontend/src/content/docs/integrations/frameworks/bun-apps.mdx @@ -4,6 +4,7 @@ seoTitle: Bun integration for Aspire AppHost description: Learn how to use the Aspire.Hosting.JavaScript Bun hosting APIs to orchestrate Bun applications alongside other resources in the Aspire app host. --- +import { Tabs, TabItem } from '@astrojs/starlight/components'; import { Image } from 'astro:assets'; import InstallPackage from '@components/InstallPackage.astro'; import bunIcon from '@assets/icons/bun-icon.png'; @@ -20,7 +21,7 @@ import bunIcon from '@assets/icons/bun-icon.png'; The Aspire Bun hosting integration enables you to run [Bun](https://bun.sh/) applications alongside your other Aspire resources in the app host. Bun apps participate in the same service discovery, health checks, OpenTelemetry export, and Aspire dashboard support as the rest of your solution. :::note -The `AddBunApp(...)` integration was previously implemented as part of the [📦 CommunityToolkit.Aspire.Hosting.Bun](https://www.nuget.org/packages/CommunityToolkit.Aspire.Hosting.Bun) package but is now built in to `Aspire.Hosting.JavaScript`. +As of Aspire 13.4, Bun hosting support is available in the official `Aspire.Hosting.JavaScript` package as `BunAppResource`. The `CommunityToolkit.Aspire.Hosting.Bun` package from the Community Toolkit is deprecated — use `Aspire.Hosting.JavaScript` and `AddBunApp` / `addBunApp` for Aspire 13.4+ applications. ::: ## Hosting integration @@ -31,7 +32,10 @@ To access the Bun hosting APIs in your [`AppHost`](/get-started/app-host/) proje ### Add Bun app -Add a Bun application to your app host using the `AddBunApp` extension method: +Add a Bun application to your AppHost using the `AddBunApp` / `addBunApp` extension method: + + + ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); @@ -45,16 +49,38 @@ builder.AddProject("apiservice") builder.Build().Run(); ``` -`AddBunApp` requires: + + + +```typescript title="apphost.mts" +import { createBuilder } from "./.aspire/modules/aspire.mjs"; + +const builder = await createBuilder(); + +const bunApp = await builder.addBunApp("bun-api", "../bun-app", "server.ts"); +await bunApp.withHttpEndpoint({ port: 3000, env: "PORT" }); + +await builder.build().run(); +``` + + + + +`AddBunApp` / `addBunApp` requires: - **name**: The name of the resource in the Aspire dashboard. - **appDirectory**: The path to the directory containing your Bun application, relative to the AppHost project. - **scriptPath**: The script to run relative to `appDirectory`, such as `server.ts`. +The resource is typed as `BunAppResource`. + ### Specify a custom entrypoint Pass a different `scriptPath` to run a different script: + + + ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); @@ -64,13 +90,26 @@ var bunApp = builder.AddBunApp("bun-api", "../bun-app", "src/http/server.ts") builder.Build().Run(); ``` -### Install packages before startup + + + +```typescript title="apphost.mts" +import { createBuilder } from "./.aspire/modules/aspire.mjs"; + +const builder = await createBuilder(); + +const bunApp = await builder.addBunApp("bun-api", "../bun-app", "src/http/server.ts"); +await bunApp.withHttpEndpoint({ port: 3000, env: "PORT" }); + +await builder.build().run(); +``` -When your Bun app includes a `package.json` file, Aspire uses Bun as the package manager and installs packages automatically before the application starts. + + ### Configure HTTP endpoints -Bun applications typically read the port from an environment variable. Use `WithHttpEndpoint` to declare the HTTP endpoint and bind it to a named environment variable: +Bun applications typically read the port from an environment variable. Use `WithHttpEndpoint` / `withHttpEndpoint` to declare the HTTP endpoint and bind it to a named environment variable: ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); @@ -97,6 +136,7 @@ console.log(`Server listening on port ${server.port}`); ## See also - [📦 Aspire.Hosting.JavaScript](https://www.nuget.org/packages/Aspire.Hosting.JavaScript) +- [JavaScript hosting integration](/integrations/frameworks/javascript/) - [Bun documentation](https://bun.sh/docs) - [Aspire integrations overview](/integrations/overview/) - [Aspire GitHub repo](https://github.com/microsoft/aspire) diff --git a/src/frontend/src/content/docs/integrations/frameworks/javascript.mdx b/src/frontend/src/content/docs/integrations/frameworks/javascript.mdx index 142a75e00..8801e3777 100644 --- a/src/frontend/src/content/docs/integrations/frameworks/javascript.mdx +++ b/src/frontend/src/content/docs/integrations/frameworks/javascript.mdx @@ -32,6 +32,7 @@ The integration exposes a number of app resource types: - `NodeAppResource`: Added with `AddNodeApp` / `addNodeApp` for running specific JavaScript files with Node.js - `ViteAppResource`: Added with `AddViteApp` / `addViteApp` for Vite applications with Vite-specific defaults - `NextJsAppResource`: Added with `AddNextJsApp` / `addNextJsApp` for Next.js applications with Next.js-specific run and publish defaults +- `BunAppResource`: Added with `AddBunApp` / `addBunApp` for applications running on the [Bun](https://bun.sh/) runtime — see the [Bun integration](/integrations/frameworks/bun-apps/) for details ## Framework examples diff --git a/src/frontend/src/content/docs/it/index.mdx b/src/frontend/src/content/docs/it/index.mdx index 24484b7ae..18e9971ca 100644 --- a/src/frontend/src/content/docs/it/index.mdx +++ b/src/frontend/src/content/docs/it/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 è disponibile!Scopri le novità di Aspire 13.4. + ✨ Aspire 13.5 è disponibile!Scopri le novità di Aspire 13.5. hero: tagline: Il tuo stack, semplificato.

Orchestra frontend, API, container e database senza sforzo—senza riscritture, senza limiti. Estendi Aspire per potenziare qualsiasi progetto.

image: diff --git a/src/frontend/src/content/docs/ja/docs.mdx b/src/frontend/src/content/docs/ja/docs.mdx index 280f120bf..4c4eb100c 100644 --- a/src/frontend/src/content/docs/ja/docs.mdx +++ b/src/frontend/src/content/docs/ja/docs.mdx @@ -7,7 +7,7 @@ next: description: Aspire を使って、C#、TypeScript、JavaScript、Python などの分散アプリケーションを設計・実行・観測・デプロイする方法を学べます。 banner: content: | - 🆕 Aspire 13.4 が登場!新機能を見る + 🆕 Aspire 13.5 が登場!新機能を見る editUrl: false tableOfContents: false pageActions: false diff --git a/src/frontend/src/content/docs/ja/index.mdx b/src/frontend/src/content/docs/ja/index.mdx index c3bbec264..b58265ffa 100644 --- a/src/frontend/src/content/docs/ja/index.mdx +++ b/src/frontend/src/content/docs/ja/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - Aspire 13.4 が登場しました!新機能を見る + ✨ Aspire 13.5 が登場しました!新機能を見る hero: tagline: あなたのスタックをもっとシンプルに。

フロントエンド、API、コンテナ、データベースを再実装不要、制限なしでオーケストレーションします。Aspire を拡張してあらゆるプロジェクトを強化しましょう。

image: diff --git a/src/frontend/src/content/docs/ko/index.mdx b/src/frontend/src/content/docs/ko/index.mdx index d7100f8f7..d3a36ce42 100644 --- a/src/frontend/src/content/docs/ko/index.mdx +++ b/src/frontend/src/content/docs/ko/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4이 출시되었습니다!Aspire 13.4의 새로운 기능 확인하기. + ✨ Aspire 13.5이 출시되었습니다!Aspire 13.5의 새로운 기능 확인하기. hero: tagline: 당신의 스택, 단순화.

프론트엔드, API, 컨테이너, 데이터베이스를 손쉽게 오케스트레이션—재작성 불필요, 무한한 가능성. Aspire를 확장하여 모든 프로젝트를 강화하세요.

image: diff --git a/src/frontend/src/content/docs/pt-br/index.mdx b/src/frontend/src/content/docs/pt-br/index.mdx index 44f44d8be..d9ff948c3 100644 --- a/src/frontend/src/content/docs/pt-br/index.mdx +++ b/src/frontend/src/content/docs/pt-br/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 foi lançado!Veja o que há de novo no Aspire 13.4. + ✨ Aspire 13.5 foi lançado!Veja o que há de novo no Aspire 13.5. hero: tagline: Seu stack, simplificado.

Orquestre frontends, APIs, contêineres e bancos de dados sem esforço—sem reescritas, sem limites. Estenda o Aspire para impulsionar qualquer projeto.

image: diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-doctor.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-doctor.mdx index d6880337d..f4a01d06f 100644 --- a/src/frontend/src/content/docs/reference/cli/commands/aspire-doctor.mdx +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-doctor.mdx @@ -33,7 +33,7 @@ This command is useful for troubleshooting when you encounter issues with Aspire - **AppHost checks**: Reports the AppHost Aspire SDK version when an AppHost project is present in the current directory - **SDK checks**: Verifies .NET SDK installation and version requirements - **Container checks**: Validates container runtime (Docker and/or Podman) availability, running status, and version. Reports which runtime is active and why (explicit configuration, auto-detected default, or auto-detected only runtime running) -- **Environment checks**: Validates environment variables and other settings, including the JavaScript toolchain required by TypeScript AppHosts (npm, pnpm, Yarn, or Bun) +- **Environment checks**: Reports operating system information (type, version, and Linux distro details when available) and validates environment variables and other settings, including the JavaScript toolchain required by TypeScript AppHosts (npm, pnpm, Yarn, or Bun) The command displays results with clear status indicators: @@ -88,10 +88,10 @@ Aspire Environment Check ======================== Aspire - ✅ Aspire CLI version 13.4.0 + ✅ Aspire CLI version %ASPIRE_VERSION% AppHost - ✅ AppHost version 13.4.0 (MyApp.AppHost.csproj) + ✅ AppHost version %ASPIRE_VERSION% (MyApp.AppHost.csproj) .NET SDK ✅ .NET 10.0.203 installed (arm64) @@ -100,9 +100,10 @@ Container Runtime ✅ Docker v28.5.1: running (auto-detected (default)) ← active Environment + ✅ Operating system: macOS 15.5.0 ✅ HTTPS development certificate is trusted -Summary: 5 passed, 0 warnings, 0 failed +Summary: 6 passed, 0 warnings, 0 failed ``` After the summary, table output also includes an `Aspire CLI Installations` @@ -113,11 +114,11 @@ When the CLI is out of date, the Aspire section shows a warning with the latest ```bash title="Aspire CLI" Aspire - ⚠️ Aspire CLI version 13.4.0-dev is out of date. Latest version is 13.4.0-preview.1.26262.10 + ⚠️ Aspire CLI version %ASPIRE_VERSION%-dev is out of date. Latest version is %ASPIRE_VERSION%-preview.1.26262.10 Run 'aspire update' to update Aspire CLI. AppHost - ✅ AppHost version 13.4.0 (MyApp.AppHost.csproj) + ✅ AppHost version %ASPIRE_VERSION% (MyApp.AppHost.csproj) ``` This warning snippet is abbreviated. In complete table output, a summary follows @@ -167,19 +168,31 @@ When using the `--format Json` option, the output includes a structured response "category": "aspire", "name": "cli-version", "status": "pass", - "message": "Aspire CLI version 13.4.0", + "message": "Aspire CLI version %ASPIRE_VERSION%", "metadata": { - "currentVersion": "13.4.0" + "currentVersion": "%ASPIRE_VERSION%" } }, { "category": "apphost", "name": "apphost-version", "status": "pass", - "message": "AppHost version 13.4.0 (MyApp.AppHost.csproj)", + "message": "AppHost version %ASPIRE_VERSION% (MyApp.AppHost.csproj)", "metadata": { "appHostPath": "MyApp.AppHost.csproj", - "version": "13.4.0" + "version": "%ASPIRE_VERSION%" + } + }, + { + "category": "environment", + "name": "operating-system", + "status": "pass", + "message": "Operating system: Linux Ubuntu 24.04", + "metadata": { + "osType": "Linux", + "displayName": "Linux Ubuntu", + "version": "24.04", + "description": "Ubuntu 24.04.2 LTS" } }, { @@ -190,7 +203,7 @@ When using the `--format Json` option, the output includes a structured response } ], "summary": { - "passed": 5, + "passed": 6, "warnings": 0, "failed": 0 } @@ -204,17 +217,26 @@ When the CLI is out of date, the `cli-version` check has `"status": "warning"` a "category": "aspire", "name": "cli-version", "status": "warning", - "message": "Aspire CLI version 13.4.0-dev is out of date. Latest version is 13.4.0-preview.1.26262.10", + "message": "Aspire CLI version %ASPIRE_VERSION%-dev is out of date. Latest version is %ASPIRE_VERSION%-preview.1.26262.10", "fix": "Run 'aspire update' to update Aspire CLI.", "metadata": { - "currentVersion": "13.4.0-dev", - "latestVersion": "13.4.0-preview.1.26262.10" + "currentVersion": "%ASPIRE_VERSION%-dev", + "latestVersion": "%ASPIRE_VERSION%-preview.1.26262.10" } } ``` The `apphost-version` check is only present in the JSON output when an AppHost project is discovered in the current directory. +The `operating-system` check is always present and includes the following metadata fields: + +| Field | Description | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `osType` | The platform family: `Windows`, `macOS`, or `Linux`. | +| `displayName` | A human-readable OS name, for example `Windows`, `macOS`, or `Linux Ubuntu`. | +| `version` | The OS version string. On Linux this is the `VERSION_ID` from `/etc/os-release` when available; on Windows and macOS it is the value from `Environment.OSVersion`. | +| `description` | A longer description of the OS. On Linux this is the `PRETTY_NAME` value from `/etc/os-release` (for example `Ubuntu 24.04.2 LTS`); on other platforms it is the runtime OS description string. | + ## See also - [aspire command](../aspire/) diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-init.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-init.mdx index 7981425e4..995e6a287 100644 --- a/src/frontend/src/content/docs/reference/cli/commands/aspire-init.mdx +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-init.mdx @@ -102,6 +102,14 @@ The following options are available: The AppHost language to scaffold (`csharp`, `typescript`). +- **`--skill-locations `** + + Comma-separated list of skill locations to configure. Use `all` to configure all supported locations or `none` to skip skill location configuration. When not specified, the command prompts interactively. Useful for non-interactive mode in scripts and CI environments. For the list of available locations, see the [`aspire agent init` command reference](../aspire-agent-init/#options). + +- **`--skills `** + + Comma-separated list of skills to install. Use `all` to install all available skills or `none` to skip skill installation. When not specified, the command prompts interactively. Useful for non-interactive mode in scripts and CI environments. For the list of available skills, see the [`aspire agent init` command reference](../aspire-agent-init/#options). + - - @@ -133,3 +141,15 @@ The following options are available: ```bash title="Aspire CLI" aspire init --channel daily ``` + +- Initialize Aspire support non-interactively without installing any agent skills: + + ```bash title="Aspire CLI" + aspire init --non-interactive --skills none + ``` + +- Initialize Aspire support non-interactively, installing all skills in the standard location: + + ```bash title="Aspire CLI" + aspire init --non-interactive --language csharp --skill-locations standard --skills all + ``` diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-new.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-new.mdx index b8cd93487..3e5d69bcd 100644 --- a/src/frontend/src/content/docs/reference/cli/commands/aspire-new.mdx +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-new.mdx @@ -122,6 +122,14 @@ The following options are available: Suppress the prompt to initialize MCP server configuration for agent environments after project creation. +- **`--skill-locations `** + + Comma-separated list of skill locations to configure. Use `all` to configure all supported locations or `none` to skip skill location configuration. When not specified, the command prompts interactively. Useful for non-interactive mode in scripts and CI environments. For the list of available locations, see the [`aspire agent init` command reference](../aspire-agent-init/#options). + +- **`--skills `** + + Comma-separated list of skills to install. Use `all` to install all available skills or `none` to skip skill installation. When not specified, the command prompts interactively. Useful for non-interactive mode in scripts and CI environments. For the list of available skills, see the [`aspire agent init` command reference](../aspire-agent-init/#options). + - - @@ -179,6 +187,18 @@ The `aspire-py-starter` template accepts the following additional options: aspire new aspire-starter --channel daily ``` +- Create a new project non-interactively without installing any agent skills: + + ```bash title="Aspire CLI" + aspire new aspire-starter --name myapp --skills none --non-interactive + ``` + +- Create a new project non-interactively and install all skills in all supported locations: + + ```bash title="Aspire CLI" + aspire new aspire-starter --name myapp --skill-locations all --skills all --non-interactive + ``` + ## Troubleshooting ### Language support not found diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-logs.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-logs.mdx index 2971234ed..a3b0ba7df 100644 --- a/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-logs.mdx +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-logs.mdx @@ -73,9 +73,10 @@ The following options are available: | `"quoted phrase"` | Single fragment containing spaces | | `field:value` | Field qualifier — value must match the named field | | `-field:value` | Negated qualifier — excludes matches | + | `field:>value` / `field:>=value` / `field: @@ -133,6 +134,12 @@ The following options are available: aspire otel logs --search "scope:Microsoft.EntityFrameworkCore severity:Warning" ``` +- Filter logs by timestamp: + + ```bash title="Aspire CLI" + aspire otel logs --search "timestamp:>=2026-06-01T12:00:00 severity:error" + ``` + - View logs from a standalone dashboard using the login URL (token is automatically exchanged for an API key): ```bash title="Aspire CLI" diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-spans.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-spans.mdx index 595fa1c69..628a9c0ca 100644 --- a/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-spans.mdx +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-spans.mdx @@ -73,10 +73,10 @@ The following options are available: | `"quoted phrase"` | Single fragment containing spaces | | `field:value` | Field qualifier — value must match the named field | | `-field:value` | Negated qualifier — excludes matches | - | `field:>N` / `field:>=N` / `field:value` / `field:>=value` / `field: @@ -134,6 +134,12 @@ The following options are available: aspire otel spans --search "@db.system:postgresql" ``` +- Filter spans by timestamp: + + ```bash title="Aspire CLI" + aspire otel spans --search "timestamp:>=2026-03-01T08:30:00 kind:server" + ``` + - View spans from a standalone dashboard using the login URL (token is automatically exchanged for an API key): ```bash title="Aspire CLI" diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-traces.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-traces.mdx index 2e174409e..27c494276 100644 --- a/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-traces.mdx +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-otel-traces.mdx @@ -71,10 +71,10 @@ The following options are available: | `"quoted phrase"` | Single fragment containing spaces | | `field:value` | Field qualifier — value must match the named field | | `-field:value` | Negated qualifier — excludes matches | - | `field:>N` / `field:>=N` / `field:value` / `field:>=value` / `field: @@ -132,6 +132,12 @@ The following options are available: aspire otel traces --search "@http.status_code:500" ``` +- Filter traces by timestamp: + + ```bash title="Aspire CLI" + aspire otel traces --search "timestamp:>2026-01-15T09:30:00 status:error" + ``` + - View traces from a standalone dashboard using the login URL (token is automatically exchanged for an API key): ```bash title="Aspire CLI" diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-run.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-run.mdx index 9e4c82ac6..0ea0a480c 100644 --- a/src/frontend/src/content/docs/reference/cli/commands/aspire-run.mdx +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-run.mdx @@ -35,7 +35,7 @@ The command performs the following steps to run an Aspire AppHost: ## Stopping the AppHost -To stop the running AppHost and exit, press (or send `SIGTERM` on Linux/macOS). The CLI requests a graceful shutdown of the AppHost and its resources. +To stop the running AppHost and exit, press (or send `SIGTERM` on Linux/macOS). The CLI requests a graceful shutdown of the AppHost and its resources. If shutdown takes longer than a moment, a `🛑 Stopping Aspire.` message is displayed shortly after you press to confirm that shutdown is in progress. If graceful shutdown is taking too long and you need to exit immediately, press a second time to terminate the process immediately. diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-terminal-attach.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-terminal-attach.mdx new file mode 100644 index 000000000..42ad9b713 --- /dev/null +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-terminal-attach.mdx @@ -0,0 +1,98 @@ +--- +title: aspire terminal attach command +seoTitle: aspire terminal attach command reference for the Aspire CLI +description: Learn about the aspire terminal attach command, which connects your local terminal to a resource's interactive PTY session exposed using WithTerminal. +sidebar: + badge: Preview +--- + +import Include from '@components/Include.astro'; + +## Name + +`aspire terminal attach` - Attach the local terminal to an interactive PTY session for a resource. + +## Synopsis + +```bash title="Aspire CLI" +aspire terminal attach [options] +``` + +## Description + +The `aspire terminal attach` command connects your local terminal to the interactive pseudo-terminal (PTY) session of a resource that was registered with [`WithTerminal()`](/app-host/with-terminal/). Once attached, you interact with the resource's terminal, as if it were running directly in your console. For example, you can drive its TUI or type into its shell. + +The command discovers the running AppHost, verifies it supports the `terminals.v1` capability, looks up the resource, and connects to one of its terminal replicas. When the resource has a single replica, that replica is selected automatically. When it has more than one, you're prompted to choose one—or you can pass `--replica`. + +Multiple peers can attach to the same session at once. By default, `attach` takes the **primary** role and drives the terminal's dimensions. Pass `--viewer` to join as a passive **viewer** instead. This technique is useful when another peer, such as the dashboard, is currently driving the session. + +While attached, the following hotkeys are available: + +| Hotkey | Action | +| ---------- | -------------------------------------------- | +| `Ctrl+B D` | Detach from the session and exit cleanly. | +| `Ctrl+B T` | Take control (request the primary role). | + +If the selected replica has already exited, the command attaches to the historical output buffer; no live input is sent. + +Because `WithTerminal` is experimental, this command is hidden behind a feature flag. Enable it with `aspire config set features.terminalCommandsEnabled true`. + +## Arguments + +The following arguments are available: + +- **``** + + The name of the resource to attach a terminal to. The resource must have been registered with `.WithTerminal()`. + +## Options + +The following options are available: + +- + +- **`-r, --replica `** + + The 0-based replica index to attach to. Required when the resource has more than one replica and the CLI is not running interactively. + +- **`--viewer`** + + Connect as a viewer (secondary) instead of taking primary control. Viewers see the terminal output but do not drive its dimensions. This option is useful when another peer (for example, the dashboard) is currently driving the session. Take control later with `Ctrl+B T`. + +- + +- + +- + +- + +- + +- + +## Examples + +- Attach to a resource's terminal: + + ```bash title="Aspire CLI" + aspire terminal attach agent + ``` + +- Attach to a specific replica of a multi-replica resource: + + ```bash title="Aspire CLI" + aspire terminal attach agent --replica 1 + ``` + +- Join an existing session as a passive viewer: + + ```bash title="Aspire CLI" + aspire terminal attach agent --viewer + ``` + +## See also + +- [Test TUI and shell apps using WithTerminal](/app-host/with-terminal/) +- [aspire terminal command](../aspire-terminal/) +- [aspire terminal ps command](../aspire-terminal-ps/) diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-terminal-ps.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-terminal-ps.mdx new file mode 100644 index 000000000..a30112bd4 --- /dev/null +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-terminal-ps.mdx @@ -0,0 +1,97 @@ +--- +title: aspire terminal ps command +seoTitle: aspire terminal ps command reference for the Aspire CLI +description: Learn about the aspire terminal ps command, which lists every WithTerminal-enabled resource in the connected AppHost with grid size, attached peers, and per-replica health. +sidebar: + badge: Preview +--- + +import Include from '@components/Include.astro'; + +## Name + +`aspire terminal ps` - List interactive terminal sessions in the connected AppHost. + +## Synopsis + +```bash title="Aspire CLI" +aspire terminal ps [options] +``` + +## Description + +The `aspire terminal ps` command lists every resource in the connected AppHost that was registered using [`WithTerminal()`](/app-host/with-terminal/). For each terminal it reports the current grid size, the number of attached peers, and per-replica health so you can see what's available before running [`aspire terminal attach`](/reference/cli/commands/aspire-terminal-attach/). + +Resources whose terminal host isn't reachable are still listed with a status that indicates they're unavailable, rather than being silently dropped. + +Because `WithTerminal` is experimental, this command is hidden behind a feature flag. Enable it with `aspire config set features.terminalCommandsEnabled true`. The connected AppHost must advertise the `terminals.v1` capability (Aspire.Hosting 13.4 or later). + +The default output is a human-readable table with the following columns: + +| Column | Description | +| ---------- | --------------------------------------------------------------------------- | +| `Resource` | The display name of the terminal-enabled resource. | +| `Replica` | The 0-based replica index (one row per replica). | +| `Status` | Whether the replica is `alive`, `exited ()`, or `host unreachable`. | +| `Size` | The current terminal grid size (`columns`x`rows`). | +| `Peers` | The number of peers currently attached to the replica. | +| `Restarts` | The number of times the replica's terminal host has restarted. | + +## Options + +The following options are available: + +- + +- **`--format `** + + Output format. `text` (default) renders a table; `json` emits structured output for scripting. The JSON output is an array of terminal entries with `resourceName`, `displayName`, `configuredColumns`, `configuredRows`, `isHostReachable`, and a `replicas` array. Each replica includes `replicaIndex`, `isAlive`, `exitCode`, `producerConnected`, `restartCount`, `currentColumns`, `currentRows`, and `attachedPeerCount`. When `--verbose` is set, each replica also includes a `peers` array of `{ peerId, displayName }`. + +- **`-v, --verbose`** + + Include per-peer details for every attached viewer (peer id and display name). + +- + +- + +- + +- + +- + +- + +## Examples + +- List terminal sessions in the running AppHost: + + ```bash title="Aspire CLI" + aspire terminal ps + ``` + + Example output: + + ```text title="Output" + Resource Replica Status Size Peers Restarts + agent 0 alive 120x30 1 0 + ``` + +- Include per-peer details: + + ```bash title="Aspire CLI" + aspire terminal ps --verbose + ``` + +- Emit JSON for scripting and pipe to `jq`: + + ```bash title="Aspire CLI" + aspire terminal ps --format json | jq -c '.[] | { resourceName, isHostReachable }' + ``` + +## See also + +- [Test TUI and shell apps using WithTerminal](/app-host/with-terminal/) +- [aspire terminal command](/reference/cli/commands/aspire-terminal/) +- [aspire terminal attach command](/reference/cli/commands/aspire-terminal-attach/) diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-terminal.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-terminal.mdx new file mode 100644 index 000000000..bf2861d06 --- /dev/null +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-terminal.mdx @@ -0,0 +1,62 @@ +--- +title: aspire terminal command +seoTitle: aspire terminal command reference for the Aspire CLI +description: Learn about the aspire terminal command, which lists and attaches to interactive terminal sessions for resources registered using WithTerminal. +sidebar: + badge: Preview +--- + +import Include from '@components/Include.astro'; + +## Name + +`aspire terminal` - Manage interactive terminal sessions for resources. + +## Synopsis + +```bash title="Aspire CLI" +aspire terminal [command] [options] +``` + +## Description + +The `aspire terminal` command provides subcommands for working with interactive terminal sessions exposed by resources that were registered using [`WithTerminal()`](/app-host/with-terminal/) in the AppHost. You can list which resources have a terminal and attach your local terminal to a running session. + +Because `WithTerminal` is experimental, the `aspire terminal` command group is hidden behind a feature flag. Enable it before use: + +```bash title="Enable the aspire terminal commands" +aspire config set features.terminalCommandsEnabled true +``` + +The connected AppHost must advertise the `terminals.v1` capability (Aspire.Hosting 13.4 or later). Against an older AppHost the subcommands report that terminals are not supported. + +## Options + +The following options are available: + +- + +- + +- + +- + +- + +- + +## Commands + +The following commands are available: + +| Command | Function | +| ---------------------------------------------------------- | -------------------------------------------------------------- | +| [`aspire terminal attach`](../aspire-terminal-attach/) | Attach the local terminal to a resource's interactive session. | +| [`aspire terminal ps`](../aspire-terminal-ps/) | List interactive terminal sessions in the connected AppHost. | + +## See also + +- [Test TUI and shell apps using WithTerminal](/app-host/with-terminal/) +- [aspire terminal attach command](../aspire-terminal-attach/) +- [aspire terminal ps command](../aspire-terminal-ps/) diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-update.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-update.mdx index e0466e41d..6037ab3c1 100644 --- a/src/frontend/src/content/docs/reference/cli/commands/aspire-update.mdx +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-update.mdx @@ -79,6 +79,15 @@ To update the CLI without being prompted, run `aspire update --self` before runn - When the CLI was installed with the **install script** or another **binary install**, `aspire update --self` downloads and installs the latest CLI binary directly. - When the CLI was installed as a **.NET tool**, the command prints the `dotnet tool update` command to run. +- When the CLI was installed via **Nix** (profile or flake), the command prints the Nix update commands to run instead of downloading a new binary. Because the Nix store is read-only, in-place binary updates are not possible: + + ```text title="Output" + To update the Aspire CLI when installed from Nix, update the profile package or flake input: + nix profile upgrade aspire-cli + nix flake update + ``` + + Run `nix profile upgrade aspire-cli` for a profile install, or `nix flake update ` for a project flake. ## Options diff --git a/src/frontend/src/content/docs/reference/cli/microsoft-collected-cli-telemetry.mdx b/src/frontend/src/content/docs/reference/cli/microsoft-collected-cli-telemetry.mdx index 48c954dbd..ef3c17590 100644 --- a/src/frontend/src/content/docs/reference/cli/microsoft-collected-cli-telemetry.mdx +++ b/src/frontend/src/content/docs/reference/cli/microsoft-collected-cli-telemetry.mdx @@ -11,6 +11,12 @@ The Aspire CLI collects usage telemetry to help Microsoft improve the product. T Aspire CLI telemetry is collected when using the CLI to run commands. Telemetry is gathered during normal CLI usage, unless you have [opted out](#how-to-opt-out) of telemetry collection. +### AI agent skill usage + +When you run `aspire agent init`, Aspire installs Aspire's AI skills and a `PostToolUse` hook into each detected agent client's user-level configuration (for example, GitHub Copilot CLI and Claude Code). The hook lets Aspire understand which of its AI skills and tools are actually used so the team can invest in the most valuable ones. + +After each agent tool use, the hook checks if an **Aspire** skill, Aspire MCP tool, or Aspire skill reference file was involved. If so, the hook inspects the event and forwards a low-cardinality usage event to the hidden `aspire agent telemetry` command. The command records the event through the same CLI telemetry pipeline. Tool uses that don't involve Aspire skills or tools are ignored. See [AI agent skill usage data](#ai-agent-skill-usage-data) for exactly what's recorded. + ## How to opt-out To opt-out of telemetry collection, set the `ASPIRE_CLI_TELEMETRY_OPTOUT` environment variable to `true`. This will disable telemetry collection for all CLI commands: @@ -49,6 +55,30 @@ Aspire CLI collects the following data: | 13.2 | CLI launch. | Includes command name and exit code. Option and argument values are not sent to Microsoft. | | 13.2 | Ensure .NET SDK install. | Includes .NET SDK version installed. | | 13.2 | CLI-related unhandled exceptions. | — | +| 13.5 | Coding agent detection. | When the CLI is invoked from a known coding agent environment (such as GitHub Copilot, Claude, Cursor, or others), the detected agent name is recorded. Only the known agent name is sent — no environment variable values or other identifying data are included. This data point is omitted entirely when no known coding agent is detected. | +| 13.5 | AI agent skill usage. | Recorded only for Aspire's own skills and tools. See [AI agent skill usage data](#ai-agent-skill-usage-data). | + +### AI agent skill usage data + +The agent telemetry hook installed by `aspire agent init` reports a single event each time one of Aspire's own AI skills or tools is used. Three event types are reported: + +- `skill_invocation` — an Aspire skill was invoked, or its `SKILL.md` was read. +- `tool_invocation` — an Aspire MCP tool was called. +- `reference_file_read` — a reference file bundled alongside an Aspire skill was read. + +Each event records only a subset of the following low-cardinality, Aspire-owned values (depending on the event type): + +| Field | Description | +|-------|-------------| +| Event type | One of `skill_invocation`, `tool_invocation`, or `reference_file_read`. | +| Client name | The agent client that produced the event, for example `copilot-cli`, `claude-code`, or `vscode`. | +| Session ID | The agent session identifier (an opaque GUID), used to group events from one session. | +| Skill name | The Aspire skill name, matched against the fixed list of skills Aspire ships (for example `aspire-deployment`). | +| Tool name | The Aspire MCP tool name (for example `aspire-list_resources`). | +| File reference | The skill-relative path of a reference file, for example `aspire/references/deploy.md`. | +| Event timestamp | The UTC time the event occurred. | + +To protect your privacy, the hook only forwards an event when the skill or tool name matches the fixed set Aspire ships, and a file reference is reported only as the path **after** the skill folder. Absolute paths, repository names, user names, file contents, and tool arguments are never recorded. Before sending, the `aspire agent telemetry` command validates each value, discards anything it doesn't recognize, and honors the same `ASPIRE_CLI_TELEMETRY_OPTOUT` opt-out. ## See also diff --git a/src/frontend/src/content/docs/reference/cli/search-filter.mdx b/src/frontend/src/content/docs/reference/cli/search-filter.mdx index a9f4f514a..d3cc7054c 100644 --- a/src/frontend/src/content/docs/reference/cli/search-filter.mdx +++ b/src/frontend/src/content/docs/reference/cli/search-filter.mdx @@ -28,10 +28,28 @@ Search queries support free-text fragments, field qualifiers, attribute qualifie | `field:"value with spaces"` | Quoted field qualifier value | `message:"connection failed"` | | `@attr:value` | Attribute qualifier — matches custom span/log attributes | `@http.method:GET` | | `-field:value` / `-@attr:value` | Negated qualifier — excludes matches | `-severity:debug` | -| `field:>N` / `field:>=N` / `field:100` | +| `field:>value` / `field:>=value` / `field:100` | All terms are AND'd: every fragment and qualifier must independently match. +## Timestamp qualifier + +Use the `timestamp` field qualifier to narrow structured logs, traces, and spans to a specific date/time range. The qualifier accepts ISO 8601 date or date/time strings together with a comparison operator: + +```bash title="Aspire CLI" +aspire otel logs --search "timestamp:>2026-01-15T09:30:00" +aspire otel traces --search "timestamp:<=2026-06-01T12:00:00Z" +aspire otel spans --search "timestamp:>=2026-01-01 timestamp:<2027-01-01" +``` + +The supported comparison operators are `>`, `>=`, `<`, and `<=`. + +Timestamp values are internally stored in UTC. The `timestamp` value is converted to UTC using these timezone rules: + +- A value with no timezone suffix (for example `2026-01-15` or `2026-01-15T09:30:00`) is treated as local time on the dashboard server and adjusted to UTC before comparison. +- A value with a UTC offset (for example `2026-06-01T12:00:00+05:00`) is adjusted to UTC before comparison. +- A value with the `Z` suffix is treated as UTC directly. + ## Console logs Matches against log line text content and resource name. Only free-text is supported (no structured qualifiers). @@ -45,36 +63,39 @@ aspire logs --follow --search "\"connection error\"" Supports free-text and structured qualifiers. Free-text matches against message, resource name, scope, trace/span IDs, severity, and all attribute keys/values. -Available field qualifiers: `severity`, `resource`, `scope`, `message`, `trace-id`, `span-id`, `event`. +Available field qualifiers: `severity`, `resource`, `scope`, `message`, `trace-id`, `span-id`, `event`, `timestamp`. ```bash title="Aspire CLI" aspire otel logs --search "severity:error \"connection failed\"" aspire otel logs --search "resource:api -severity:debug" aspire otel logs --search "scope:Microsoft.EntityFrameworkCore" +aspire otel logs --search "timestamp:>=2026-06-01T12:00:00 severity:error" ``` ## Traces Supports free-text and structured qualifiers. Free-text matches against trace name, resource name, and all span fields within the trace. -Available field qualifiers: `name`, `resource`, `trace-id`, `status`, `duration`. +Available field qualifiers: `name`, `resource`, `trace-id`, `status`, `duration`, `timestamp`. ```bash title="Aspire CLI" aspire otel traces --search "checkout" aspire otel traces --search "status:error duration:>500" aspire otel traces --search "@http.status_code:500" +aspire otel traces --search "timestamp:>2026-01-15T09:30:00 status:error" ``` ## Spans Supports free-text and structured qualifiers. Free-text matches against span name, resource name, scope, trace/span IDs, status, kind, and all attribute keys/values. -Available field qualifiers: `name`, `resource`, `scope`, `status`, `kind`, `trace-id`, `span-id`, `duration`. +Available field qualifiers: `name`, `resource`, `scope`, `status`, `kind`, `trace-id`, `span-id`, `duration`, `timestamp`. ```bash title="Aspire CLI" aspire otel spans --search "@http.method:GET duration:>100 status:error" aspire otel spans --search "@db.system:postgresql" aspire otel spans --search "-kind:internal" +aspire otel spans --search "timestamp:>=2026-03-01T08:30:00 kind:server" ``` ## See also diff --git a/src/frontend/src/content/docs/ru/index.mdx b/src/frontend/src/content/docs/ru/index.mdx index 30ec4c51e..0ee08694b 100644 --- a/src/frontend/src/content/docs/ru/index.mdx +++ b/src/frontend/src/content/docs/ru/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 выпущен!Что нового в Aspire 13.4. + ✨ Aspire 13.5 выпущен!Что нового в Aspire 13.5. hero: tagline: Ваш стек, упрощён.

Оркестрируйте фронтенды, API, контейнеры и базы данных без усилий—без переписывания, без ограничений. Расширяйте Aspire для любого проекта.

image: diff --git a/src/frontend/src/content/docs/tr/index.mdx b/src/frontend/src/content/docs/tr/index.mdx index 10ec9f4a2..aca886187 100644 --- a/src/frontend/src/content/docs/tr/index.mdx +++ b/src/frontend/src/content/docs/tr/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 yayınlandı!Aspire 13.4'deki yeniliklere bakın. + ✨ Aspire 13.5 yayınlandı!Aspire 13.5'deki yeniliklere bakın. hero: tagline: Yığınınız basitleştirildi.

Frontend'leri, API'leri, konteynerleri ve veritabanlarını zahmetsizce orkestre edin—yeniden yazma yok, sınır yok. Her projeyi güçlendirmek için Aspire'ı genişletin.

image: diff --git a/src/frontend/src/content/docs/uk/index.mdx b/src/frontend/src/content/docs/uk/index.mdx index 0b277f82a..f4d0ad623 100644 --- a/src/frontend/src/content/docs/uk/index.mdx +++ b/src/frontend/src/content/docs/uk/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 випущено!Що нового в Aspire 13.4. + ✨ Aspire 13.5 випущено!Що нового в Aspire 13.5. hero: tagline: Ваш стек спрощено.

Оркеструйте фронтенди, API, контейнери та бази даних без зусиль—без переписування, без обмежень. Розширюйте Aspire для будь-якого проєкту.

image: diff --git a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx index 2e0a67436..1a07dc941 100644 --- a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx +++ b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx @@ -521,7 +521,7 @@ await builder.build().run(); ### Resource commands return structured results -Custom resource commands can now return an `ExecuteCommandResult` with a structured `Message` payload that the dashboard renders in the notification center. The new `Logger` property on `ExecuteCommandContext` lets command implementations log directly to the resource's log stream. +Custom resource commands can now return an `ExecuteCommandResult` with structured `Data` payloads (`Text`, `Json`, or `Markdown`) that the dashboard renders from the notification center's **View response** action. The `CommandResults.Success(string, CommandResultData)` overload makes it easy to return Markdown tables and other rich output. The new `Logger` property on `ExecuteCommandContext` lets command implementations log directly to the resource's log stream. diff --git a/src/frontend/src/content/docs/whats-new/aspire-13-4.mdx b/src/frontend/src/content/docs/whats-new/aspire-13-4.mdx index 7ba057e48..d2e2c9d23 100644 --- a/src/frontend/src/content/docs/whats-new/aspire-13-4.mdx +++ b/src/frontend/src/content/docs/whats-new/aspire-13-4.mdx @@ -398,6 +398,7 @@ Aspire 13.4 includes a set of smaller CLI improvements and fixes: - `aspire update` now requires `--yes` in non-interactive mode, matching `aspire destroy`. - `aspire stop --all` output includes the AppHost name and, when needed, the PID for clearer multi-instance shutdown output. - `aspire new`, `aspire init`, `aspire run`, and `aspire update` include fixes for NuGet feed errors, output paths, generated files, disabled dashboards, detached shutdown, and AppHost package-reference cleanup. +- `aspire run` now handles AppHost shutdown more gracefully: backchannel log, resource-state, and publishing-activity streams complete immediately when a shutdown is already in progress, and a warning is shown if log capture is interrupted rather than surfacing an unhandled error. ## 🧩 App model and AppHost @@ -473,10 +474,11 @@ Persistent lifetimes now extend beyond containers to executables and projects. C @@ -787,7 +789,7 @@ Re-run `aspire run` (or your code-generation step) to regenerate references, and #### Persistent executable and project lifetimes -Support for persistent lifetimes on executables and projects updates the underlying DCP executable model (new `persistent`, `start`, and `stop` fields). Persistent resources default to proxyless endpoints, require concrete ports for executables, don't support replicas, and aren't compatible with Aspire IDE debugging sessions. +Support for persistent lifetimes on executables and projects updates the underlying DCP executable model (new `persistent`, `start`, and `stop` fields). Persistent executables and projects default to proxyless endpoints, require concrete ports for executables, don't support replicas, and aren't compatible with Aspire IDE debugging sessions. Persistent containers use proxied endpoints by default, matching session container behavior, so integrations that depend on endpoint allocation before startup work correctly without changes. If you adopt `WithPersistentLifetime()`, review endpoint and port configuration. Tooling pinned to the previous DCP executable schema may need to be updated. See [Configure resource lifetimes](/app-host/resource-lifetimes/). diff --git a/src/frontend/src/content/docs/whats-new/aspire-13-5.mdx b/src/frontend/src/content/docs/whats-new/aspire-13-5.mdx new file mode 100644 index 000000000..39914b986 --- /dev/null +++ b/src/frontend/src/content/docs/whats-new/aspire-13-5.mdx @@ -0,0 +1,215 @@ +--- +title: "What's new in Aspire 13.5" +description: "Explore Aspire 13.5: Interactive terminal sessions, polyglot IInteractionService, TypeScript AppHost stability, terminal commands with user inputs, custom health checks, distributed tracing improvements, and dashboard enhancements." +sidebar: + label: Aspire 13.5 + order: 0 +tableOfContents: + minHeadingLevel: 2 + maxHeadingLevel: 2 +--- + +import { + Steps, + Aside, + FileTree, + Icon, + Tabs, + TabItem, +} from '@astrojs/starlight/components'; +import LearnMore from '@components/LearnMore.astro'; +import OsAwareTabs from '@components/OsAwareTabs.astro'; + +Aspire 13.5 is here with focus on **developer experience**, **polyglot feature parity**, and **runtime stability**. This release brings **interactive terminal sessions** via `WithTerminal()`, **IInteractionService available across all polyglot AppHosts** (TypeScript, Python, Go, Java, and Rust), **user-defined arguments in resource commands** for interactive workflows, **TypeScript AppHost startup optimizations** and **stability fixes**, **custom health checks for TypeScript AppHosts**, **container file copying** in polyglot AppHosts, **promoted IInteractionService to stable**, a major **Foundry integration update** to use the CLI-based lifecycle, **distributed trace improvements** including timestamp filtering, **dashboard telemetry enhancements**, new **VS Code extension commands** including opening the Dashboard in a side panel, **Bun debugging support**, **resource command visibility** in the extension tree, **Aspire CLI available via npm**, and many more improvements and bug fixes across AppHost, CLI, Dashboard, and Extensions. + +We'd love to hear what you think. Drop by [ Discord](https://aka.ms/aspire-discord) to chat with the team and the community, or file feedback and issues on [ GitHub](https://github.com/microsoft/aspire/issues). + +This release introduces: + +- **Interactive terminal sessions** with `WithTerminal()` on AppHost resources enable live REPL and shell interaction through the Dashboard and CLI. +- **Polyglot IInteractionService parity** brings prompts, message boxes, notifications, and dynamic inputs to TypeScript, Python, Go, Java, and Rust AppHosts alongside C#. +- **User-defined resource command arguments** let Dashboard and CLI prompt for input before invoking commands like custom deploy or setup workflows. +- **TypeScript AppHost stability improvements** fix deadlocks, optimize startup by racing connection attempts against process exit, and validate compilation before execution. +- **Custom health checks in TypeScript AppHosts** enable resource-specific health monitoring in polyglot environments. +- **Container file copying in TypeScript AppHosts** brings parity with C# for copying host files into containers with ownership and permission controls. +- **IInteractionService promoted to stable** removes the ASPIREINTERACTION001 diagnostic requirement from production code. +- **CLI available via npm** (@microsoft/aspire-cli) provides an alternative installation path alongside the default distribution. +- **Embedded Aspire skills bundle fallback** ensures the CLI can bootstrap even when GitHub release asset acquisition is unavailable. +- **Faster TypeScript AppHost startup** skips fixed delays and races the connection retry loop, reducing time-to-ready. +- **Friendly error messages for health check failures** replace raw exception stacks with actionable diagnostics in the Dashboard. +- **Distributed trace timestamp filtering** enables precise telemetry searches by date and time. +- **VS Code Dashboard in side panel** lets you monitor your app without leaving VS Code. +- **Bun debugging support** in VS Code extension via WebKit Inspector Protocol. +- **Resource commands in VS Code tree view** display all available actions (Start, Stop, custom commands) under each resource. +- **Improved parameter display** in VS Code shows secret masks, missing value warnings, and consistent formatting across panels. +- **VS Code extension renamed to "Aspire"** and rebranded for clarity on the Marketplace. +- **AppHost discovery efficiency** in VS Code respects exclusion settings and debounces file changes to reduce background scanning. +- **Foundry Local integration CLI update** now uses the foundry CLI for lifecycle management, requiring foundry 1.1.0+. +- **Proxyless endpoint port allocation** assigns dynamic public host ports before resources are created, so endpoint property references resolve consistently. +- …and much more. + +## 🆙 Upgrade to Aspire 13.5 + + +
+ + + +For general purpose upgrade guidance, see [Upgrade Aspire](/whats-new/upgrade-aspire/). + +The easiest way to upgrade to Aspire 13.5 is using the [`aspire update` command](/reference/cli/commands/aspire-update/): + + + +1. Update the Aspire CLI itself: + + ```bash title="Aspire CLI — Update the CLI" + aspire update --self + ``` + +1. Update your projects (run from the root of your repository): + + ```bash title="Aspire CLI — Update all Aspire packages" + aspire update + ``` + + + +Or install the CLI from scratch: + + + + + ```bash title="Aspire CLI — Install Aspire CLI" + curl -sSL https://aspire.dev/install.sh | bash + ``` + + + + + ```powershell title="Aspire CLI — Install Aspire CLI" + irm https://aspire.dev/install.ps1 | iex + ``` + + + + + + For more details on installing the Aspire CLI, see [Install the CLI](/get-started/install-cli/). + + +## 🖥️ Interactive terminal sessions with WithTerminal() + +AppHost authors can now call `WithTerminal()` on a resource to enable interactive terminal sessions. The Dashboard and CLI can attach to and detach from the session at will, enabling live use of REPLs, shells, and other terminal programs running as Aspire resources. This is particularly powerful for resource orchestration workflows where you need to interact with services in real-time. + + + For details, see [WithTerminal() interactive terminal sessions](/app-host/with-terminal/). + + +## 🌐 IInteractionService available across polyglot AppHosts + +IInteractionService and all related interaction types (prompts, message boxes, notifications, dynamic inputs) are now available in **polyglot AppHosts** written in TypeScript, Python, Go, Java, and Rust—providing feature parity with C# AppHosts. This enables rich user interaction patterns across all supported languages. + + + For TypeScript examples, see [IInteractionService](/extensibility/interaction-service/). + + +## 🆙 IInteractionService promoted to stable + +The `IInteractionService` API is no longer marked experimental. The `ASPIREINTERACTION001` diagnostic has been removed, so you can use interaction features without compiler suppressions in production code. + +## ⚙️ Resource commands with user-defined arguments + +HTTP resource commands now support named arguments, allowing the Dashboard and CLI to prompt for user input before invoking them. TypeScript AppHosts achieve full parity through Aspire Type System (ATS) exports. This enables interactive workflows such as custom deployment or setup procedures. + +## 💚 Custom health checks for TypeScript AppHosts + +TypeScript AppHosts can now register custom health check callbacks using `builder.addHealthCheck()` and attach them to resources. Project resources also gain `withEndpointsInEnvironment()` to control which endpoints are injected into environment variables. + +## 🐳 TypeScript AppHosts support container file copying + +TypeScript AppHosts can now export `withContainerFiles` to copy host files into container resources, with full support for owner, group, and umask options—achieving parity with C# AppHosts. + +## ⚡ Faster TypeScript AppHost startup + +TypeScript AppHost startup no longer waits a fixed delay before the CLI attempts to connect. The CLI now races the RPC connection retry loop against process exit, reducing the time from `aspire run` to an active AppHost. + +## 🔧 TypeScript AppHost fixes + +- Fixed a deadlock in TypeScript AppHosts where async callbacks stored in `IOptions.Configure` were invoked during `BeforeStartEvent`. +- Fixed a startup reliability issue with `WithBrowserLogs()` where tracked browser sessions could fail even when the browser eventually became responsive. The CDP startup command timeout has been increased. +- Fixed failures when proxyless container endpoint references were accessed before container creation. +- Fixed `aspire run` failing for polyglot AppHosts using `*.dev.localhost` resource service URLs. + +## 📦 Aspire CLI available via npm + +The Aspire CLI is now available as an npm package (`@microsoft/aspire-cli`), providing an alternative installation method alongside the standard distribution. The update command and update notifier now detect and handle npm-installed versions. + +## 💬 CLI improvements and diagnostics + +- **OS information in `aspire doctor`**: The `aspire doctor` command now reports operating system details in its Environment section. Human-readable output shows the OS type and version; on Linux it includes distro details from `/etc/os-release`. JSON output adds a structured `operating-system` check with `osType`, `displayName`, `version`, and `description` metadata fields for tooling. +- **Embedded Aspire skills bundle fallback**: When GitHub release asset acquisition is unavailable, the CLI uses the embedded bundle and shows a non-fatal warning instead of failing the entire command. +- **Faster stale backchannel socket cleanup**: Stale AppHost backchannel socket files no longer block CLI commands like `aspire add`. The CLI automatically prunes orphaned sockets before probing. +- **Better TypeScript AppHost error messages**: When AppHost code generation fails due to version mismatches, the CLI provides enriched diagnostic output to help identify the cause. +- **Improved aspire agent init output**: The command now collects updated skill/location pairs and prints one compact summary instead of repeating success for every skill. +- **Improved aspire ls discovery**: Fixed multiple bugs in the settings discovery pipeline including settings.json compatibility and aspire.config.json handling. +- **TypeScript AppHosts honor --no-build**: TypeScript AppHosts now correctly skip the TypeScript compilation check when the --no-build flag is passed. +- **CLI shutdown responsiveness**: Multiple improvements to Ctrl+C/SIGTERM handling, including faster signal responsiveness during AppHost startup. + +## 📊 Dashboard and telemetry improvements + +- **Timestamp filter for telemetry**: Filter logs and traces by timestamp using a dedicated search qualifier in the Dashboard filter dialog. +- **Numeric equality operators**: The telemetry filter dialog now includes `==` and `!=` operators for exact numeric value matching. +- **Rich text visualizer improvements**: The TextVisualizerDialog now disables markdown formatting for JSON and XML content. +- **Dashboard startup log formatting**: Improved output with indented URLs, separated container access warnings, and restored legacy login URL for `dotnet watch` integration. +- **Friendly health check error messages**: Health check failures now display concise, actionable messages instead of raw exception stacks. +- **Fixed telemetry streaming with resource filters**: Streams now wait for resources to appear before returning empty results. +- **Fixed duplicate replica display names**: Dashboard now uses the last 8 characters of the service.instance.id GUID to prevent collisions. + +## 🧩 VS Code extension enhancements + +- **Open Aspire Dashboard in side panel**: A new command lets you open the Dashboard in a side-by-side panel instead of an external browser. +- **Bun debugging support**: VS Code extension now supports debugging Bun applications via the WebKit Inspector Protocol. +- **Resource commands in tree view**: Resource commands (Start, Stop, custom commands) are displayed as child items under each resource. +- **VS Code extension telemetry**: The extension now collects activation, debug session, and dashboard interaction signals for product improvement. +- **Support launchUrl in launchSettings.json**: The extension respects the `launchUrl` property as the serverReadyAction URI format. +- **Show discovered AppHosts in Aspire pane**: Idle AppHosts discovered via `aspire ls` now display in the VS Code Aspire pane with context menu actions. +- **Security hardening**: Terminal commands now use structured shell arguments to prevent command injection via malicious paths or resource names. +- **Parameter display improvements**: Consistent display across panels with secret masks, missing value warnings, and 80-character truncation for non-secrets. +- **VS Code extension branding**: Rebranded to "Aspire" with an updated icon and display name. +- **AppHost discovery efficiency**: Respects workspace exclusion settings, debounces file changes, and avoids overlapping discovery runs. + +## 🔌 Integration improvements + +- **Foundry Local CLI update**: The Foundry Local integration now uses the installed foundry CLI for lifecycle management instead of internal SDK APIs, requiring foundry CLI 1.1.0+. +- **DevTunnel region configuration**: Added a `Region` property to `DevTunnelOptions` for specifying the tunnel creation region. +- **Blazor gateway Docker Compose support**: Added Docker Compose publish support for Blazor gateway resources. +- **Redis TLS deadlock fix**: Fixed a deadlock during Redis container startup when TLS was enabled with persistent lifetime. + +## Breaking changes + +The following breaking changes are included in Aspire 13.5: + +1. **ServiceProvider renamed to Services**: The `ServiceProvider` property on context types is now obsolete and replaced with `Services`. The old property still works but generates a compiler warning. Update your code to use the new property name. + +2. **PublishAsConnectionString marked obsolete**: The `PublishAsConnectionString` extension methods are now obsolete. Callers should switch to `AddConnectionString` in publish-mode app model code. + +3. **aspire ps --resources flag removed**: The `--resources` and `--include-hidden` flags have been removed from `aspire ps`, which now focuses on AppHost-level summaries. Use `aspire describe` for detailed resource data. + +4. **GitHub Models integration deprecated**: The GitHub Models service is no longer available to new customers, so the `Aspire.Hosting.GitHub.Models` integration is sunset as of Aspire 13.5. All public APIs are marked `[Obsolete]`, and the package no longer appears in `aspire add` output. One final obsolete release will ship on NuGet, and the package will be removed entirely in a future version. Migrate to the [Azure AI Foundry integration](/integrations/cloud/azure/azure-ai-foundry/azure-ai-foundry-get-started/) instead. See [microsoft/aspire#18402](https://github.com/microsoft/aspire/issues/18402) for details. + +5. **Proxyless endpoint port allocation timing changed**: Proxyless endpoints without an explicit public `port` now receive one during service preparation, before workload resources are created. Executable proxyless endpoints that previously failed without a public port, and container proxyless endpoints that expected the public port to be assigned later during container startup, should expect Aspire to assign the port earlier. The default allocation range is `10000-32767` and can be overridden with `ASPIRE_PROXYLESS_ENDPOINT_PORT_RANGE=start-end`. Persistent resources reuse allocated ports from user secrets when available. + +6. **Go polyglot SDK: single optional `options` DTO is now passed directly**: When a C# exported API has exactly one optional parameter that is a DTO named `options` (with no coexisting cancellation token or callback), the Go polyglot code generator now passes the DTO type directly as a variadic parameter instead of wrapping it in a generated method-options struct. Go AppHosts that used the wrapper-struct form must update their call sites after regenerating the SDK. + + + +## Known issues + +No Aspire 13.5 known issues are documented yet. If you discover an issue, please [report it on GitHub](https://github.com/microsoft/aspire/issues). diff --git a/src/frontend/src/content/docs/zh-cn/index.mdx b/src/frontend/src/content/docs/zh-cn/index.mdx index c5a354b1c..c2e46874f 100644 --- a/src/frontend/src/content/docs/zh-cn/index.mdx +++ b/src/frontend/src/content/docs/zh-cn/index.mdx @@ -11,7 +11,7 @@ prev: false next: false banner: content: | - 🚀 Aspire 13.4 已发布!查看 Aspire 13.4 的新功能。 + ✨ Aspire 13.5 已发布!查看 Aspire 13.5 的新功能。 hero: tagline: 精简你的技术栈。

轻松编排前端、API、容器和数据库—无需重写,无限可能。扩展 Aspire 为任何项目赋能。

image: diff --git a/src/frontend/tests/unit/aspire-version-placeholders.vitest.test.ts b/src/frontend/tests/unit/aspire-version-placeholders.vitest.test.ts new file mode 100644 index 000000000..20ef2e16f --- /dev/null +++ b/src/frontend/tests/unit/aspire-version-placeholders.vitest.test.ts @@ -0,0 +1,158 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { describe, expect, test } from 'vitest'; +import { replaceAspireVersionPlaceholdersInDirectory } from '../../config/aspire-version-placeholders-integration.mjs'; +import { + currentAspireMajorMinorVersion, + currentAspireVersion, +} from '../../config/aspire-versions.mjs'; +import { + remarkAspireVersionPlaceholders, + replaceAspireVersionPlaceholders, +} from '../../config/remark-aspire-version-placeholders.mjs'; + +describe('Aspire version placeholders', () => { + test('replaces current Aspire major.minor and patch placeholders', () => { + expect( + replaceAspireVersionPlaceholders( + 'Aspire %ASPIRE_VERSION_MAJOR_MINOR% ships as %ASPIRE_VERSION%.' + ) + ).toBe(`Aspire ${currentAspireMajorMinorVersion} ships as ${currentAspireVersion}.`); + }); + + test('replaces placeholders in markdown text, code fences, and MDX attributes', () => { + const tree = { + type: 'root', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Aspire %ASPIRE_VERSION_MAJOR_MINOR%' }], + }, + { type: 'code', lang: 'bash', value: 'aspire %ASPIRE_VERSION%' }, + { + type: 'mdxJsxFlowElement', + name: 'Code', + attributes: [{ type: 'mdxJsxAttribute', name: 'code', value: '%ASPIRE_VERSION%' }], + children: [], + }, + ], + }; + + remarkAspireVersionPlaceholders()(tree); + + expect(tree.children[0].children[0].value).toBe( + `Aspire ${currentAspireMajorMinorVersion}` + ); + expect(tree.children[1].value).toBe(`aspire ${currentAspireVersion}`); + expect(tree.children[2].attributes[0].value).toBe(currentAspireVersion); + }); + + test('replaces placeholders only in Markdown copies, leaving other assets untouched', async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'aspire-version-placeholders-')); + + try { + const markdownPath = path.join(tempDir, 'example.md'); + const htmlPath = path.join(tempDir, 'example.html'); + const textPath = path.join(tempDir, 'example.txt'); + const mdxPath = path.join(tempDir, 'example.mdx'); + const jsonPath = path.join(tempDir, 'example.json'); + + const placeholderContent = 'Aspire %ASPIRE_VERSION_MAJOR_MINOR%: %ASPIRE_VERSION%'; + + await Promise.all([ + writeFile(markdownPath, placeholderContent), + writeFile(htmlPath, placeholderContent), + writeFile(textPath, placeholderContent), + writeFile(mdxPath, placeholderContent), + writeFile(jsonPath, '{"version":"%ASPIRE_VERSION%"}'), + ]); + + await replaceAspireVersionPlaceholdersInDirectory(tempDir); + + // Only the `.md` copy (which bypasses the remark pipeline) is rewritten. + await expect(readFile(markdownPath, 'utf8')).resolves.toBe( + `Aspire ${currentAspireMajorMinorVersion}: ${currentAspireVersion}` + ); + + // The post-build pass intentionally rewrites only `.md` files. In the real + // build `.html`/`.txt`/`.mdx` are produced through the remark pipeline (so + // they're already replaced before this pass runs) and `.json` is never a + // placeholder target; this test seeds them with raw placeholders to assert + // that this pass leaves every non-`.md` extension untouched. + await expect(readFile(htmlPath, 'utf8')).resolves.toBe(placeholderContent); + await expect(readFile(textPath, 'utf8')).resolves.toBe(placeholderContent); + await expect(readFile(mdxPath, 'utf8')).resolves.toBe(placeholderContent); + await expect(readFile(jsonPath, 'utf8')).resolves.toBe('{"version":"%ASPIRE_VERSION%"}'); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); + + test('replaces Markdown placeholders recursively under a bounded concurrency limit', async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'aspire-version-placeholders-')); + + try { + // Spread more `.md` files than the worker-pool concurrency across nested + // directories so the bounded recursive traversal is exercised, alongside + // non-`.md` assets that must be left untouched. + const placeholderContent = 'Aspire %ASPIRE_VERSION_MAJOR_MINOR% is %ASPIRE_VERSION%.'; + const ignoredExtensions = ['.html', '.txt', '.mdx', '.json']; + const markdownPaths: string[] = []; + const ignoredPaths: string[] = []; + + for (let depth = 0; depth < 4; depth++) { + const dir = path.join(tempDir, ...Array.from({ length: depth }, (_, i) => `level-${i}`)); + await mkdir(dir, { recursive: true }); + + for (let index = 0; index < 5; index++) { + const markdownPath = path.join(dir, `asset-${index}.md`); + await writeFile(markdownPath, placeholderContent); + markdownPaths.push(markdownPath); + + const extension = ignoredExtensions[index % ignoredExtensions.length]; + const ignoredPath = path.join(dir, `asset-${index}${extension}`); + await writeFile(ignoredPath, placeholderContent); + ignoredPaths.push(ignoredPath); + } + } + + await replaceAspireVersionPlaceholdersInDirectory(tempDir, 2); + + await Promise.all( + markdownPaths.map(async (markdownPath) => { + await expect(readFile(markdownPath, 'utf8')).resolves.toBe( + `Aspire ${currentAspireMajorMinorVersion} is ${currentAspireVersion}.` + ); + }) + ); + + await Promise.all( + ignoredPaths.map(async (ignoredPath) => { + await expect(readFile(ignoredPath, 'utf8')).resolves.toBe(placeholderContent); + }) + ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); + + test('falls back to a valid worker count when given a non-finite concurrency', async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'aspire-version-placeholders-')); + + try { + const markdownPath = path.join(tempDir, 'example.md'); + await writeFile(markdownPath, 'Aspire %ASPIRE_VERSION_MAJOR_MINOR%: %ASPIRE_VERSION%'); + + // A non-finite concurrency must not collapse the worker pool to an empty + // array and silently skip every file. + await replaceAspireVersionPlaceholdersInDirectory(tempDir, Number.NaN); + + await expect(readFile(markdownPath, 'utf8')).resolves.toBe( + `Aspire ${currentAspireMajorMinorVersion}: ${currentAspireVersion}` + ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); +});