Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
*.ps1 text eol=lf
pnpm-lock.yaml text eol=lf

# Force LF for agent-skills artifacts so sha256 digests are byte-stable across
# Windows and Linux checkouts (referenced by /.well-known/agent-skills/index.json).
src/frontend/public/.well-known/agent-skills/**/*.md text eol=lf
src/frontend/public/.well-known/agent-skills/index.json text eol=lf

# Explicitly mark binary files to avoid corruption
*.png binary
*.jpg binary
Expand Down
1 change: 1 addition & 0 deletions Aspire.Dev.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<Folder Name="/tests/">
<Project Path="tests/AtsJsonGenerator.Tests/AtsJsonGenerator.Tests.csproj" />
<Project Path="tests/PackageJsonGenerator.Tests/PackageJsonGenerator.Tests.csproj" />
<Project Path="tests/StaticHost.Tests/StaticHost.Tests.csproj" />
</Folder>
<Project Path="src/frontend/frontend.esproj">
<Build />
Expand Down
18 changes: 10 additions & 8 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
"scripts": {
"git-env": "node ./scripts/write-git-env.mjs",
"check-data": "node ./scripts/check-data-files.mjs",
"compute-skill-digests": "node ./scripts/compute-skill-digests.mjs",
"verify-skill-digests": "node ./scripts/compute-skill-digests.mjs --check",
"twoslash-types": "tsx ./scripts/generate-twoslash-types.ts",
"dev": "pnpm git-env && pnpm check-data && astro dev",
"dev:host": "pnpm git-env && pnpm check-data && astro dev --host",
"start": "pnpm git-env && pnpm check-data && astro dev",
"start:host": "pnpm git-env && pnpm check-data && astro dev --host",
"build": "pnpm git-env && astro build",
"build:skip-search": "pnpm git-env && astro build --mode skip-search",
"build:production": "pnpm git-env && astro build --mode production",
"dev": "pnpm git-env && pnpm check-data && pnpm compute-skill-digests && astro dev",
"dev:host": "pnpm git-env && pnpm check-data && pnpm compute-skill-digests && astro dev --host",
"start": "pnpm git-env && pnpm check-data && pnpm compute-skill-digests && astro dev",
"start:host": "pnpm git-env && pnpm check-data && pnpm compute-skill-digests && astro dev --host",
"build": "pnpm git-env && pnpm compute-skill-digests && astro build",
"build:skip-search": "pnpm git-env && pnpm compute-skill-digests && astro build --mode skip-search",
"build:production": "pnpm git-env && pnpm compute-skill-digests && astro build --mode production",
"preview": "astro preview",
"preview:host": "astro preview --host",
"astro": "pnpm git-env && astro",
Expand All @@ -42,7 +44,7 @@
"test:e2e": "playwright test",
"test:e2e:install": "playwright install chromium",
"test:e2e:serve": "pnpm git-env && pnpm check-data && astro dev --host 127.0.0.1 --port 4321",
"lint": "pnpm git-env && pnpm exec astro sync && eslint . --max-warnings 0",
"lint": "pnpm git-env && pnpm exec astro sync && pnpm verify-skill-digests && eslint . --max-warnings 0",
"format": "prettier -w --cache --plugin prettier-plugin-astro .",
"update:all": "pnpm update:integrations && pnpm update:github-stats && pnpm update:samples",
"update:schemas": "tsx ./scripts/update-schemas.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
name: getting-started-with-aspire
description: "Use this skill when a developer asks how to begin using Aspire — installing the Aspire CLI, creating a new Aspire app, running it locally, or finding the authoritative docs, integration catalog, or API reference on aspire.dev. Use it for questions like \"how do I install aspire?\", \"how do I create a new aspire app?\", \"how do I run my aspire app?\", or \"where is the documentation for aspire?\". Do not use it for operating an existing Aspire AppHost (use the official `aspire` skill at github.com/microsoft/aspire/tree/main/.agents/skills/aspire), authoring AppHost code, or adding integrations to an existing app."
---

# Getting started with Aspire

[Aspire](https://aspire.dev) is a polyglot stack for building, running, debugging, and deploying distributed applications. The AppHost can be authored in C# or TypeScript today, with additional languages (Java, Go, Python, Rust, …) on the roadmap. The Aspire CLI (`aspire`) is the entry point for everything: scaffolding apps, running them locally, inspecting state, and deploying.

## When to use this skill

- The user is new to Aspire and wants to install the CLI.
- The user wants to scaffold a brand-new Aspire app.
- The user wants to know how to run the app they just created.
- The user is looking for the authoritative docs, integration catalog, or API reference.

## Don't use this skill for

- Operating an existing Aspire AppHost (resources, logs, traces, dashboard commands). That's the [official `aspire` skill](https://github.com/microsoft/aspire/tree/main/.agents/skills/aspire).
- Editing AppHost source code (C# or TypeScript) — consult the API reference on aspire.dev.

## Install the Aspire CLI

The official cross-platform installers are hosted on aspire.dev:

- **Windows (PowerShell):** `iex (irm https://aspire.dev/install.ps1)`
- **macOS / Linux (bash):** `curl -fsSL https://aspire.dev/install.sh | bash`

After install, verify with `aspire --version`. Do not install Aspire from NuGet/npm directly when the user wants the CLI — the install script is the supported path.

## Create a new Aspire app

```sh
aspire new
```

`aspire new` is **interactive**. It prompts for the template (for example `aspire-starter`, `apphost`, `apphost-ts`), the project name, the output location, and the language. It creates a subfolder for the new project, so run it from the parent directory where you want the project folder to live. Do not pass fabricated template flags; let the CLI prompt the user.

## Run the app

```sh
cd <project>
aspire start
```

`aspire start` launches the AppHost and the Aspire dashboard. Prefer `aspire start` over `dotnet run` for AppHosts — `aspire start` is the agent-friendly path; `aspire run` blocks the terminal.

## Where to learn more

- **Documentation hub:** <https://aspire.dev/docs/>
- **CLI reference:** <https://aspire.dev/reference/cli/>
- **Integration catalog:** <https://aspire.dev/integrations/>
- **C# API reference:** <https://aspire.dev/reference/api/csharp/>
- **TypeScript API reference:** <https://aspire.dev/reference/api/typescript/>
- **LLM-friendly corpus:** <https://aspire.dev/llms.txt>, <https://aspire.dev/llms-full.txt>
- **Per-page markdown:** every page on aspire.dev is also available as `<page>.md` (or via `Accept: text/markdown` content negotiation).
- **In-page search tool:** when running in a WebMCP-capable browser, the `search-aspire-docs` tool is registered on every aspire.dev page.
- **Source repository:** <https://github.com/microsoft/aspire>
13 changes: 13 additions & 0 deletions src/frontend/public/.well-known/agent-skills/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://agentskills.io/schema/v0.2.0/discovery.schema.json",
"version": "0.2.0",
"skills": [
{
"name": "getting-started-with-aspire",
"type": "skill-md",
"description": "Install the Aspire CLI, create a new Aspire app, run it locally, and find the authoritative docs on aspire.dev.",
"url": "/.well-known/agent-skills/getting-started-with-aspire/SKILL.md",
"digest": "sha256:a8ab4851cd19b2bdbe065976180b17b6e741c36bad6b3301668a1bd49e2b52fd"
}
]
}
1 change: 1 addition & 0 deletions src/frontend/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
User-agent: *
Content-Signal: ai-train=yes, search=yes, ai-input=yes
Allow: /

Sitemap: https://aspire.dev/sitemap-index.xml
106 changes: 106 additions & 0 deletions src/frontend/scripts/compute-skill-digests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// @ts-check
/**
* compute-skill-digests.mjs
*
* Recomputes `digest` fields in `public/.well-known/agent-skills/index.json` by
* sha256-hashing each referenced skill artifact's raw bytes. Run as a `prebuild`
* step so the published index.json always matches the served SKILL.md bytes.
*
* Usage:
* node scripts/compute-skill-digests.mjs # update digests in place
* node scripts/compute-skill-digests.mjs --check # exit non-zero if stale
*
* The tool deliberately reads files as raw bytes (no LF/CRLF normalization)
* — `.gitattributes` pins LF for these paths so the on-disk bytes are stable
* across Windows and Linux checkouts.
*/

import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const repoRoot = path.resolve(fileURLToPath(import.meta.url), '..', '..');
const publicRoot = path.join(repoRoot, 'public');
const indexPath = path.join(publicRoot, '.well-known', 'agent-skills', 'index.json');

const checkOnly = process.argv.includes('--check');

/**
* @param {string} filePath
* @returns {Promise<string>} sha256 hash as `sha256:<lowerhex>`
*/
async function digestFile(filePath) {
const bytes = await readFile(filePath);
const hash = createHash('sha256').update(bytes).digest('hex');
return `sha256:${hash}`;
}

/**
* Resolve a public URL like `/.well-known/foo/bar.md` to its on-disk path.
* Validates that the resolved path stays under `publicRoot` so a malicious or
* malformed `url` (e.g. containing `..` segments) cannot escape the public
* directory in dev/CI environments.
* @param {string} url
*/
function resolvePublicPath(url) {
if (!url.startsWith('/')) {
throw new Error(`Skill url must be absolute: ${url}`);
}
const resolved = path.resolve(publicRoot, url.slice(1));
const publicRootWithSep = publicRoot.endsWith(path.sep) ? publicRoot : publicRoot + path.sep;
if (resolved !== publicRoot && !resolved.startsWith(publicRootWithSep)) {
throw new Error(`Skill url escapes publicRoot: ${url}`);
}
return resolved;
}

const raw = await readFile(indexPath, 'utf8');
const original = raw;
/** @type {{ skills: Array<{ name: string; type: string; url: string; digest?: string }> }} */
const index = JSON.parse(raw);

if (!Array.isArray(index.skills)) {
throw new Error('index.json must contain a `skills` array');
}

let changed = false;
for (const skill of index.skills) {
if (skill.type !== 'skill-md') {
// Future skill types might use a different bytes-to-digest convention;
// bail loudly so we don't silently emit a wrong digest.
throw new Error(`Unsupported skill type "${skill.type}" for ${skill.name}`);
}
const filePath = resolvePublicPath(skill.url);
const fresh = await digestFile(filePath);
if (skill.digest !== fresh) {
changed = true;
if (!checkOnly) {
skill.digest = fresh;
} else {
console.error(
`[compute-skill-digests] STALE digest for ${skill.name}: index.json has ${skill.digest}, file is ${fresh}`
);
}
}
}

const next = JSON.stringify(index, null, 2) + '\n';

if (checkOnly) {
if (changed || next !== original) {
console.error(
'[compute-skill-digests] index.json is out of date. Run `node scripts/compute-skill-digests.mjs` to refresh.'
);
process.exit(1);
}
console.log('[compute-skill-digests] index.json is up to date.');
process.exit(0);
}

if (next !== original) {
await writeFile(indexPath, next, 'utf8');
console.log('[compute-skill-digests] Updated', path.relative(repoRoot, indexPath));
} else {
console.log('[compute-skill-digests] No changes.');
}
5 changes: 5 additions & 0 deletions src/frontend/src/components/starlight/Head.astro
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ function computeSourceUrl() {
})();
</script>

<!-- WebMCP: register a single `search-aspire-docs` tool for in-page agents. -->
<script>
import '@scripts/webmcp';
</script>

<Tooltips
interactive={false}
allowHTML={true}
Expand Down
37 changes: 37 additions & 0 deletions src/frontend/src/scripts/search/SearchProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Common search interface used by the WebMCP `search-aspire-docs` tool.
*
* Concrete providers (Pagefind today, Typesense in the near future) implement
* this interface so the WebMCP tool itself stays engine-agnostic. The tool
* never imports a specific provider directly — it goes through
* `getSearchProvider()` from `./index.ts`.
*/

export interface SearchResult {
/** Page title or section heading. */
title: string;
/** Absolute or root-relative URL on aspire.dev. */
url: string;
/** Short snippet that explains why the page matched. */
excerpt: string;
}

export interface SearchResponse {
results: SearchResult[];
/** True when the underlying engine isn't available in this environment. */
unavailable?: boolean;
/** Human-readable reason. Surface to agents in error states. */
reason?: string;
}

export interface SearchProvider {
/** Engine identifier (for telemetry / debugging). */
readonly id: string;
/** Lazy initialization. Idempotent. */
ensureReady(): Promise<void>;
/**
* Run a query and return ranked results. `limit` is a soft cap; providers
* may return fewer results when the corpus is small.
*/
search(query: string, limit: number): Promise<SearchResponse>;
}
24 changes: 24 additions & 0 deletions src/frontend/src/scripts/search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { pagefindProvider } from './pagefind-provider';
import type { SearchProvider } from './SearchProvider';
import { typesenseProvider } from './typesense-provider';

export type { SearchProvider, SearchResponse, SearchResult } from './SearchProvider';

/**
* Selects the active search provider based on the build-time
* `PUBLIC_SEARCH_PROVIDER` env var. Defaults to Pagefind, which is what
* Starlight ships today. Once the site migrates to Typesense, set
* `PUBLIC_SEARCH_PROVIDER=typesense` (or change the default below) — the
* WebMCP tool surface stays stable across the swap.
*/
export function getSearchProvider(): SearchProvider {
const raw = (import.meta.env.PUBLIC_SEARCH_PROVIDER as string | undefined) ?? 'pagefind';
const id = raw.toLowerCase();
switch (id) {
case 'typesense':
return typesenseProvider;
case 'pagefind':
default:
return pagefindProvider;
}
}
Loading
Loading