diff --git a/.changeset/remove-mcp-addon.md b/.changeset/remove-mcp-addon.md new file mode 100644 index 000000000..ac93d6a8b --- /dev/null +++ b/.changeset/remove-mcp-addon.md @@ -0,0 +1,5 @@ +--- +'sv': minor +--- + +feat(ai-tools): replace `mcp` add-on with `ai-tools` add-on that includes both MCP and skills setup diff --git a/.prettierignore b/.prettierignore index 3c73e5188..fc57e856b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ packages/sv/src/cli/tests/snapshots/* -packages/sv-utils/src/tests/**/output.ts \ No newline at end of file +packages/sv-utils/src/tests/**/output.ts +packages/sv/src/create/shared/+skills/* \ No newline at end of file diff --git a/documentation/docs/20-commands/20-sv-add.md b/documentation/docs/20-commands/20-sv-add.md index 6ce8c6389..cb26127de 100644 --- a/documentation/docs/20-commands/20-sv-add.md +++ b/documentation/docs/20-commands/20-sv-add.md @@ -49,10 +49,10 @@ Prevents installing dependencies ## Official add-ons +- [`ai-tools`](ai-tools) - [`better-auth`](better-auth) - [`drizzle`](drizzle) - [`eslint`](eslint) -- [`mcp`](mcp) - [`mdsvex`](mdsvex) - [`paraglide`](paraglide) - [`playwright`](playwright) diff --git a/documentation/docs/30-add-ons/17-mcp.md b/documentation/docs/30-add-ons/01-ai-tools.md similarity index 61% rename from documentation/docs/30-add-ons/17-mcp.md rename to documentation/docs/30-add-ons/01-ai-tools.md index 437977c67..5ff2f2f48 100644 --- a/documentation/docs/30-add-ons/17-mcp.md +++ b/documentation/docs/30-add-ons/01-ai-tools.md @@ -1,19 +1,20 @@ --- -title: mcp +title: ai-tools --- -[Svelte MCP](/docs/ai/overview) can help your LLM write better Svelte code. +[Svelte AI Tools](/docs/ai/overview) can help your LLM write better Svelte code. ## Usage ```sh -npx sv add mcp +npx sv add ai-tools ``` ## What you get - An MCP configuration for [local](https://svelte.dev/docs/ai/local-setup) or [remote](https://svelte.dev/docs/ai/remote-setup) setup - A [README for agents](https://agents.md/) to help you use the MCP server effectively +- [Skills](https://svelte.dev/docs/ai/skills) for clients that support them (claude code, opencode) ## Options @@ -22,7 +23,7 @@ npx sv add mcp The IDE you want to use like `'claude-code'`, `'cursor'`, `'gemini'`, `'opencode'`, `'vscode'`, `'other'`. ```sh -npx sv add mcp="ide:cursor,vscode" +npx sv add ai-tools="ide:cursor,vscode" ``` ### setup @@ -30,5 +31,5 @@ npx sv add mcp="ide:cursor,vscode" The setup you want to use. ```sh -npx sv add mcp="setup:local" +npx sv add ai-tools="setup:local" ``` diff --git a/packages/sv/src/addons/mcp.ts b/packages/sv/src/addons/ai-tools.ts similarity index 68% rename from packages/sv/src/addons/mcp.ts rename to packages/sv/src/addons/ai-tools.ts index 548f78e6f..c6afcdeaa 100644 --- a/packages/sv/src/addons/mcp.ts +++ b/packages/sv/src/addons/ai-tools.ts @@ -29,12 +29,26 @@ const options = defineAddonOptions() required: true, condition: ({ ide }) => !(ide.length === 1 && ide.includes('opencode')) }) + .add('skills', { + question: 'Do you want to install skills?', + type: 'select', + default: 'files', + options: [ + { value: 'files', label: 'Add files to the project' }, + { + value: 'none', + label: 'Skip', + hint: 'for Claude Code you can install the plugin instead: /plugin install svelte' + } + ], + condition: ({ ide }) => ide.some((i) => i !== 'opencode' && i !== 'other') + }) .build(); export default defineAddon({ - id: 'mcp', - shortDescription: 'Svelte MCP', - homepage: 'https://svelte.dev/docs/mcp', + id: 'ai-tools', + shortDescription: 'Svelte AI Tools', + homepage: 'https://svelte.dev/docs/ai', options, run: ({ sv, options }) => { const getLocalConfig = (o?: { @@ -72,14 +86,19 @@ export default defineAddon({ }; agentPath: string; configPath: string; + skillsPath?: string; + agentsPath?: string; + agentExtension?: string; customData?: Record; extraFiles?: Array<{ path: string; data: Record }>; } | { other: true } > = { 'claude-code': { - agentPath: 'CLAUDE.md', + agentPath: '.claude/CLAUDE.md', configPath: '.mcp.json', + skillsPath: '.claude/skills', + agentsPath: '.claude/agents', mcpOptions: { typeLocal: 'stdio', typeRemote: 'http', @@ -89,11 +108,13 @@ export default defineAddon({ cursor: { agentPath: 'AGENTS.md', configPath: '.cursor/mcp.json', + agentsPath: '.cursor/agents', mcpOptions: {} }, gemini: { agentPath: 'GEMINI.md', configPath: '.gemini/settings.json', + agentsPath: '.gemini/agents', schema: 'https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json', mcpOptions: {} @@ -115,6 +136,8 @@ export default defineAddon({ vscode: { agentPath: 'AGENTS.md', configPath: '.vscode/mcp.json', + agentsPath: '.github/agents', + agentExtension: '.agent.md', mcpOptions: { serversKey: 'servers' } @@ -127,8 +150,11 @@ export default defineAddon({ const filesAdded: string[] = []; const filesExistingAlready: string[] = []; - const sharedFiles = getSharedFiles().filter((file) => file.include.includes('mcp')); - const agentFile = sharedFiles.find((file) => file.name === 'AGENTS.md'); + const sharedFiles = getSharedFiles(); + const mcpFiles = sharedFiles.filter((file) => file.include.includes('mcp')); + const skillFiles = sharedFiles.filter((file) => file.include.includes('skills')); + const agentFiles = sharedFiles.filter((file) => file.include.includes('agents')); + const agentFile = mcpFiles.find((file) => file.name === 'AGENTS.md'); for (const ide of options.ide) { const value = configurator[ide]; @@ -136,7 +162,17 @@ export default defineAddon({ if (value === undefined) continue; if ('other' in value) continue; - const { mcpOptions, agentPath, configPath, schema, customData, extraFiles } = value; + const { + mcpOptions, + agentPath, + configPath, + skillsPath, + agentsPath, + agentExtension, + schema, + customData, + extraFiles + } = value; // We only add the agent file if it's not already added if (!filesAdded.includes(agentPath)) { @@ -184,12 +220,42 @@ export default defineAddon({ ); } } + + // Add skills for clients that support them (not opencode - plugin handles it) + if (skillsPath && options.skills === 'files') { + for (const file of skillFiles) { + const filePath = `${skillsPath}/${file.name}`; + sv.file(filePath, (content) => { + if (content) { + filesExistingAlready.push(filePath); + return false; + } + return file.contents; + }); + } + } + + // Add sub-agents for clients that support them (not opencode - plugin handles it) + if (agentsPath) { + for (const file of agentFiles) { + const ext = agentExtension ?? '.md'; + const name = file.name.replace(/\.md$/, ext); + const filePath = `${agentsPath}/${name}`; + sv.file(filePath, (content) => { + if (content) { + filesExistingAlready.push(filePath); + return false; + } + return file.contents; + }); + } + } } if (filesExistingAlready.length > 0) { log.warn( `${filesExistingAlready.map((path) => color.path(path)).join(', ')} already exists, we didn't touch ${filesExistingAlready.length > 1 ? 'them' : 'it'}. ` + - `See ${color.website('https://svelte.dev/docs/mcp/overview#Usage')} for manual setup.` + `See ${color.website('https://svelte.dev/docs/ai')} for manual setup.` ); } }, diff --git a/packages/sv/src/addons/index.ts b/packages/sv/src/addons/index.ts index de0303650..de0043919 100644 --- a/packages/sv/src/addons/index.ts +++ b/packages/sv/src/addons/index.ts @@ -1,8 +1,8 @@ import type { Addon, AddonDefinition } from '../core/config.ts'; +import aiTools from './ai-tools.ts'; import betterAuth from './better-auth.ts'; import drizzle from './drizzle.ts'; import eslint from './eslint.ts'; -import mcp from './mcp.ts'; import mdsvex from './mdsvex.ts'; import paraglide from './paraglide.ts'; import playwright from './playwright.ts'; @@ -24,7 +24,7 @@ type OfficialAddons = { mdsvex: Addon; paraglide: Addon; storybook: Addon; - mcp: Addon; + aiTools: Addon; }; // The order of addons here determines the order they are displayed inside the CLI @@ -41,7 +41,7 @@ export const officialAddons: OfficialAddons = { mdsvex, paraglide, storybook, - mcp + aiTools }; export function getAddonDetails(id: string): AddonDefinition { diff --git a/packages/sv/src/addons/tests/mcp/test.ts b/packages/sv/src/addons/tests/ai-tools/test.ts similarity index 75% rename from packages/sv/src/addons/tests/mcp/test.ts rename to packages/sv/src/addons/tests/ai-tools/test.ts index 3ab08956f..7e6ecd335 100644 --- a/packages/sv/src/addons/tests/mcp/test.ts +++ b/packages/sv/src/addons/tests/ai-tools/test.ts @@ -1,23 +1,31 @@ import fs from 'node:fs'; import path from 'node:path'; import { expect } from 'vitest'; -import mcp from '../../mcp.ts'; +import aiTools from '../../ai-tools.ts'; import { setupTest } from '../_setup/suite.ts'; const { test, testCases } = setupTest( - { mcp }, + { 'ai-tools': aiTools }, { kinds: [ { type: 'default-local', options: { - mcp: { ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], setup: 'local' } + 'ai-tools': { + ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], + setup: 'local', + skills: 'files' + } } }, { type: 'default-remote', options: { - mcp: { ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], setup: 'remote' } + 'ai-tools': { + ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], + setup: 'remote', + skills: 'files' + } } } ], @@ -45,12 +53,12 @@ const { test, testCases } = setupTest( } ); -test.concurrent.for(testCases)('mcp $kind.type $variant', (testCase, ctx) => { +test.concurrent.for(testCases)('ai-tools $kind.type $variant', (testCase, ctx) => { const cwd = ctx.cwd(testCase); const getContent = (filePath: string) => { - const cursorPath = path.resolve(cwd, filePath); - return fs.readFileSync(cursorPath, 'utf8'); + const fullPath = path.resolve(cwd, filePath); + return fs.readFileSync(fullPath, 'utf8'); }; const cursorMcpContent = getContent(`.cursor/mcp.json`); @@ -214,4 +222,22 @@ test.concurrent.for(testCases)('mcp $kind.type $variant', (testCase, ctx) => { } `); } + + // skills should be installed for claude-code only (opencode uses plugin) + const claudeSkillsDir = path.resolve(cwd, '.claude/skills'); + expect(fs.existsSync(claudeSkillsDir)).toBe(true); + expect(fs.existsSync(path.resolve(claudeSkillsDir, 'svelte-code-writer/SKILL.md'))).toBe(true); + expect(fs.existsSync(path.resolve(claudeSkillsDir, 'svelte-core-bestpractices/SKILL.md'))).toBe( + true + ); + + // opencode should NOT have skills (plugin handles it) + expect(fs.existsSync(path.resolve(cwd, '.opencode/skills'))).toBe(false); + + // sub-agents should be installed for all clients except opencode + expect(fs.existsSync(path.resolve(cwd, '.claude/agents/svelte-file-editor.md'))).toBe(true); + expect(fs.existsSync(path.resolve(cwd, '.cursor/agents/svelte-file-editor.md'))).toBe(true); + expect(fs.existsSync(path.resolve(cwd, '.gemini/agents/svelte-file-editor.md'))).toBe(true); + expect(fs.existsSync(path.resolve(cwd, '.github/agents/svelte-file-editor.agent.md'))).toBe(true); + expect(fs.existsSync(path.resolve(cwd, '.opencode/agents'))).toBe(false); }); diff --git a/packages/sv/src/cli/tests/cli.ts b/packages/sv/src/cli/tests/cli.ts index fe534e32a..c38baae82 100644 --- a/packages/sv/src/cli/tests/cli.ts +++ b/packages/sv/src/cli/tests/cli.ts @@ -32,7 +32,7 @@ describe('cli', () => { 'better-auth=demo:password,github', 'mdsvex', 'paraglide=languageTags:en,es+demo:yes', - 'mcp=ide:claude-code,cursor,gemini,opencode,vscode,other+setup:local' + 'ai-tools=ide:claude-code,cursor,gemini,opencode,vscode,other+setup:local+skills:files' // 'storybook' // No storybook addon during tests! ] }, @@ -93,10 +93,29 @@ describe('cli', () => { projectName ); const relativeFiles = fs.readdirSync(testOutputPath, { recursive: true }) as string[]; + + // Files from ai-tools repo (skills, agents) change independently - + // snapshot only file listings, not content + const aiToolsFiles: Record = {}; + const aiToolsPattern = /[\\/](skills|agents)[\\/]/; + for (const relativeFile of relativeFiles) { if (!fs.statSync(path.resolve(testOutputPath, relativeFile)).isFile()) continue; if (['.svg', '.env'].some((ext) => relativeFile.endsWith(ext))) continue; + const normalized = relativeFile.replace(/\\/g, '/'); + + // Group ai-tools files by directory for manifest comparison + if (aiToolsPattern.test(normalized)) { + const match = normalized.match(/(.+\/(?:skills|agents))\/(.*)/); + if (match) { + const [, base, rest] = match; + aiToolsFiles[base] ??= []; + aiToolsFiles[base].push(rest); + } + continue; + } + let generated = fs.readFileSync(path.resolve(testOutputPath, relativeFile), 'utf-8'); if (relativeFile === 'package.json') { const { data: generatedPackageJson } = parse.json(generated); @@ -119,6 +138,15 @@ describe('cli', () => { ); } + // Compare ai-tools file listings against sv-files-snapshots.md manifests + for (const [dir, files] of Object.entries(aiToolsFiles)) { + const manifest = files.sort().join('\n') + '\n'; + await expect(manifest).toMatchFileSnapshot( + path.resolve(snapPath, dir, 'sv-files-snapshots.md'), + `ai-tools manifest "${dir}" does not match snapshot` + ); + } + if (projectName === 'create-with-all-addons' && process.platform !== 'win32') { await exec('pnpm', ['install', '--no-frozen-lockfile'], { nodeOptions: { stdio: 'pipe', cwd: testOutputPath } diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/CLAUDE.md b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.claude/CLAUDE.md similarity index 95% rename from packages/sv/src/cli/tests/snapshots/create-with-all-addons/CLAUDE.md rename to packages/sv/src/cli/tests/snapshots/create-with-all-addons/.claude/CLAUDE.md index fcdbb88bb..92e91a6f2 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/CLAUDE.md +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.claude/CLAUDE.md @@ -2,7 +2,7 @@ - **Language**: TypeScript - **Package Manager**: npm -- **Add-ons**: prettier, eslint, vitest, playwright, tailwindcss, sveltekit-adapter, drizzle, better-auth, mdsvex, paraglide, mcp +- **Add-ons**: prettier, eslint, vitest, playwright, tailwindcss, sveltekit-adapter, drizzle, better-auth, mdsvex, paraglide, ai-tools --- diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.claude/agents/sv-files-snapshots.md b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.claude/agents/sv-files-snapshots.md new file mode 100644 index 000000000..ee6da1faf --- /dev/null +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.claude/agents/sv-files-snapshots.md @@ -0,0 +1 @@ +svelte-file-editor.md diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.claude/skills/sv-files-snapshots.md b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.claude/skills/sv-files-snapshots.md new file mode 100644 index 000000000..88296c9d5 --- /dev/null +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.claude/skills/sv-files-snapshots.md @@ -0,0 +1,11 @@ +svelte-code-writer/SKILL.md +svelte-core-bestpractices/SKILL.md +svelte-core-bestpractices/references/$inspect.md +svelte-core-bestpractices/references/@attach.md +svelte-core-bestpractices/references/@render.md +svelte-core-bestpractices/references/await-expressions.md +svelte-core-bestpractices/references/bind.md +svelte-core-bestpractices/references/each.md +svelte-core-bestpractices/references/hydratable.md +svelte-core-bestpractices/references/snippet.md +svelte-core-bestpractices/references/svelte-reactivity.md diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.cursor/agents/sv-files-snapshots.md b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.cursor/agents/sv-files-snapshots.md new file mode 100644 index 000000000..ee6da1faf --- /dev/null +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.cursor/agents/sv-files-snapshots.md @@ -0,0 +1 @@ +svelte-file-editor.md diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.gemini/agents/sv-files-snapshots.md b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.gemini/agents/sv-files-snapshots.md new file mode 100644 index 000000000..ee6da1faf --- /dev/null +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.gemini/agents/sv-files-snapshots.md @@ -0,0 +1 @@ +svelte-file-editor.md diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.github/agents/sv-files-snapshots.md b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.github/agents/sv-files-snapshots.md new file mode 100644 index 000000000..4ed244d57 --- /dev/null +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.github/agents/sv-files-snapshots.md @@ -0,0 +1 @@ +svelte-file-editor.agent.md diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/AGENTS.md b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/AGENTS.md index fcdbb88bb..92e91a6f2 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/AGENTS.md +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/AGENTS.md @@ -2,7 +2,7 @@ - **Language**: TypeScript - **Package Manager**: npm -- **Add-ons**: prettier, eslint, vitest, playwright, tailwindcss, sveltekit-adapter, drizzle, better-auth, mdsvex, paraglide, mcp +- **Add-ons**: prettier, eslint, vitest, playwright, tailwindcss, sveltekit-adapter, drizzle, better-auth, mdsvex, paraglide, ai-tools --- diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/GEMINI.md b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/GEMINI.md index fcdbb88bb..92e91a6f2 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/GEMINI.md +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/GEMINI.md @@ -2,7 +2,7 @@ - **Language**: TypeScript - **Package Manager**: npm -- **Add-ons**: prettier, eslint, vitest, playwright, tailwindcss, sveltekit-adapter, drizzle, better-auth, mdsvex, paraglide, mcp +- **Add-ons**: prettier, eslint, vitest, playwright, tailwindcss, sveltekit-adapter, drizzle, better-auth, mdsvex, paraglide, ai-tools --- diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/README.md b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/README.md index 579128e8d..8765042e4 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/README.md +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/README.md @@ -15,7 +15,7 @@ To recreate this project with the same configuration: ```sh # recreate this project -npx sv@0.0.0 create --template minimal --types ts --add prettier eslint vitest="usages:unit,component" playwright tailwindcss="plugins:typography,forms" sveltekit-adapter="adapter:node" drizzle="database:sqlite+sqlite:libsql" better-auth="demo:password,github" mdsvex paraglide="languageTags:en,es+demo:yes" mcp="ide:claude-code,cursor,gemini,opencode,vscode,other+setup:local" --no-install packages/sv/.test-output/cli/create-with-all-addons +npx sv@0.0.0 create --template minimal --types ts --add prettier eslint vitest="usages:unit,component" playwright tailwindcss="plugins:typography,forms" sveltekit-adapter="adapter:node" drizzle="database:sqlite+sqlite:libsql" better-auth="demo:password,github" mdsvex paraglide="languageTags:en,es+demo:yes" ai-tools="ide:claude-code,cursor,gemini,opencode,vscode,other+setup:local+skills:files" --no-install packages/sv/.test-output/cli/create-with-all-addons ``` ## Developing diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts deleted file mode 100644 index 9985ce113..000000000 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('home page has expected h1', async ({ page }) => { - await page.goto('/'); - await expect(page.locator('h1')).toBeVisible(); -}); diff --git a/packages/sv/src/core/common.ts b/packages/sv/src/core/common.ts index dce24eab7..ce632b8a9 100644 --- a/packages/sv/src/core/common.ts +++ b/packages/sv/src/core/common.ts @@ -288,7 +288,7 @@ export function updateAgent( packageManager: string, loadedAddons: LoadedAddon[] ): void { - const agentFiles = ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md']; + const agentFiles = ['AGENTS.md', 'GEMINI.md', '.claude/CLAUDE.md']; const languageLabel = language === 'typescript' diff --git a/packages/sv/src/create/index.ts b/packages/sv/src/create/index.ts index 8b487a481..8b6c3ab8d 100644 --- a/packages/sv/src/create/index.ts +++ b/packages/sv/src/create/index.ts @@ -23,7 +23,7 @@ export type File = { contents: string; }; -export type Condition = TemplateType | LanguageType | 'playground' | 'mcp'; +export type Condition = TemplateType | LanguageType | 'playground' | 'mcp' | 'skills' | 'agents'; export type Common = { files: Array<{ diff --git a/packages/sv/src/create/shared/+agents/svelte-file-editor.md b/packages/sv/src/create/shared/+agents/svelte-file-editor.md new file mode 100644 index 000000000..31185c0de --- /dev/null +++ b/packages/sv/src/create/shared/+agents/svelte-file-editor.md @@ -0,0 +1,67 @@ +--- +name: svelte-file-editor +description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools. +--- + +You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them. + +If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools. + +If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it. + +## Available MCP Tools + +### 1. list-sections + +Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available. + +### 2. get-documentation + +Retrieves full documentation for specified sections. Accepts a single section name or an array of section names. Use after `list-sections` to fetch relevant docs for the task at hand. + +**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions` + +### 3. svelte-autofixer + +Analyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like: + +- Using `$effect` instead of `$derived` for computations +- Missing cleanup in effects +- Svelte 4 syntax (`on:click`, `export let`, ``) +- Missing keys in `{#each}` blocks +- And more + +## Workflow + +When invoked to work on a Svelte file: + +### 1. Gather Context (if needed) + +If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools: + +1. Call `list-sections` to see available documentation +2. Call `get-documentation` with relevant section names + +### 2. Read the Target File + +Read the file to understand the current implementation. + +### 3. Make Changes + +Apply edits following Svelte 5 best practices: + +### 4. Validate Changes + +After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues. + +### 5. Fix Any Issues + +If the autofixer reports problems, fix them and re-validate until no issues remain. + +## Output Format + +After completing your work, provide: + +1. Summary of changes made +2. Any issues found and fixed by the autofixer +3. Recommendations for further improvements (if any) diff --git a/packages/sv/src/create/shared/+skills/svelte-code-writer/SKILL.md b/packages/sv/src/create/shared/+skills/svelte-code-writer/SKILL.md new file mode 100644 index 000000000..b50ccf902 --- /dev/null +++ b/packages/sv/src/create/shared/+skills/svelte-code-writer/SKILL.md @@ -0,0 +1,66 @@ +--- +name: svelte-code-writer +description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results. +--- + +# Svelte 5 Code Writer + +## CLI Tools + +You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`: + +### List Documentation Sections + +```bash +npx @sveltejs/mcp list-sections +``` + +Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. + +### Get Documentation + +```bash +npx @sveltejs/mcp get-documentation ",,..." +``` + +Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs. + +**Example:** + +```bash +npx @sveltejs/mcp get-documentation "$state,$derived,$effect" +``` + +### Svelte Autofixer + +```bash +npx @sveltejs/mcp svelte-autofixer "" [options] +``` + +Analyzes Svelte code and suggests fixes for common issues. + +**Options:** + +- `--async` - Enable async Svelte mode (default: false) +- `--svelte-version` - Target version: 4 or 5 (default: 5) + +**Examples:** + +```bash +# Analyze inline code (escape $ as \$) +npx @sveltejs/mcp svelte-autofixer '' + +# Analyze a file +npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte + +# Target Svelte 4 +npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4 +``` + +**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution. + +## Workflow + +1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics +2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues +3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component diff --git a/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/SKILL.md b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/SKILL.md new file mode 100644 index 000000000..ffa73ee76 --- /dev/null +++ b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/SKILL.md @@ -0,0 +1,176 @@ +--- +name: svelte-core-bestpractices +description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more. +--- + +## `$state` + +Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable. + +Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example. + +## `$derived` + +To compute something from state, use `$derived` rather than `$effect`: + +```js +// do this +let square = $derived(num * num); + +// don't do this +let square; + +$effect(() => { + square = num * num; +}); +``` + +> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`. + +Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes. + +If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this. + +## `$effect` + +Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects. + +- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md) +- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate +- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md) +- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md) + +Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server. + +## `$props` + +Treat props as though they will change. For example, values that depend on props should usually use `$derived`: + +```js +// @errors: 2451 +let { type } = $props(); + +// do this +let color = $derived(type === 'danger' ? 'red' : 'green'); + +// don't do this — `color` will not update if `type` changes +let color = type === 'danger' ? 'red' : 'green'; +``` + +## `$inspect.trace` + +`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update. + +## Events + +Any element attribute starting with `on` is treated as an event listener: + +```svelte + + + + + + + +``` + +If you need to attach listeners to `window` or `document` you can use `` and ``: + +```svelte + + +``` + +Avoid using `onMount` or `$effect` for this. + +## Snippets + +[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template. + +```svelte +{#snippet greeting(name)} +

hello {name}!

+{/snippet} + +{@render greeting('world')} +``` + +> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside ` + + + +``` + +On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations). + +## $inspect(...).with + +`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo: + +```svelte + + + +``` + +## $inspect.trace(...) + +This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire. + +```svelte + +``` + +`$inspect.trace` takes an optional first argument which will be used as the label. diff --git a/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/@attach.md b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/@attach.md new file mode 100644 index 000000000..5c113b16b --- /dev/null +++ b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/@attach.md @@ -0,0 +1,166 @@ +Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates. + +Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM. + +> [!NOTE] +> Attachments are available in Svelte 5.29 and newer. + +```svelte + + + +
...
+``` + +An element can have any number of attachments. + +## Attachment factories + +A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment (demo: + +```svelte + + + + + + +``` + +Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).) + +## Inline attachments + +Attachments can also be created inline (demo: + +```svelte + + { + const context = canvas.getContext('2d'); + + $effect(() => { + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + }); + }} +> +``` + +> [!NOTE] +> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state. + +## Conditional attachments + +Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage: + +```svelte +
...
+``` + +## Passing attachments to components + +When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments. + +This allows you to create _wrapper components_ that augment elements (demo: + +```svelte + + + + + +``` + +```svelte + + + + + + +``` + +## Controlling when attachments re-run + +Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`): + +```js +// @errors: 7006 2304 2552 +function foo(bar) { + return (node) => { + veryExpensiveSetupWork(node); + update(node, bar); + }; +} +``` + +In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect: + +```js +// @errors: 7006 2304 2552 +function foo(+++getBar+++) { + return (node) => { + veryExpensiveSetupWork(node); + ++++ $effect(() => { + update(node, getBar()); + });+++ + } +} +``` + +## Creating attachments programmatically + +To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey). + +## Converting actions to attachments + +If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components. diff --git a/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/@render.md b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/@render.md new file mode 100644 index 000000000..2e606855d --- /dev/null +++ b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/@render.md @@ -0,0 +1,35 @@ +To render a [snippet](snippet), use a `{@render ...}` tag. + +```svelte +{#snippet sum(a, b)} +

{a} + {b} = {a + b}

+{/snippet} + +{@render sum(1, 2)} +{@render sum(3, 4)} +{@render sum(5, 6)} +``` + +The expression can be an identifier like `sum`, or an arbitrary JavaScript expression: + +```svelte +{@render (cool ? coolSnippet : lameSnippet)()} +``` + +## Optional snippets + +If the snippet is potentially undefined — for example, because it's an incoming prop — then you can use optional chaining to only render it when it _is_ defined: + +```svelte +{@render children?.()} +``` + +Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content: + +```svelte +{#if children} + {@render children()} +{:else} +

fallback content

+{/if} +``` diff --git a/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/await-expressions.md b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/await-expressions.md new file mode 100644 index 000000000..18c223168 --- /dev/null +++ b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/await-expressions.md @@ -0,0 +1,180 @@ +As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable: + +- at the top level of your component's ` + + + + +

{a} + {b} = {await add(a, b)}

+``` + +...if you increment `a`, the contents of the `

` will _not_ immediately update to read this — + +```html +

2 + 2 = 3

+``` + +— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves. + +Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing. + +## Concurrency + +Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup... + +```svelte +

{await one()}

{await two()}

+``` + +...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential. + +This does not apply to sequential `await` expressions inside your ` + + + +{#if open} + + (open = false)} /> +{/if} +``` + +## Caveats + +As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum. + +## Breaking changes + +Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in very rare situations. diff --git a/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/bind.md b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/bind.md new file mode 100644 index 000000000..80b2f4c47 --- /dev/null +++ b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/bind.md @@ -0,0 +1,16 @@ +## Function bindings + +You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation: + +```svelte + value, (v) => (value = v.toLowerCase())} /> +``` + +In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`: + +```svelte +
...
+``` + +> [!NOTE] +> Function bindings are available in Svelte 5.9.0 and newer. diff --git a/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/each.md b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/each.md new file mode 100644 index 000000000..283b754f1 --- /dev/null +++ b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/each.md @@ -0,0 +1,42 @@ +## Keyed each blocks + +```svelte + +{#each expression as name (key)}...{/each} +``` + +```svelte + +{#each expression as name, index (key)}...{/each} +``` + +If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle. + +The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. + +```svelte +{#each items as item (item.id)} +
  • {item.name} x {item.qty}
  • +{/each} + + +{#each items as item, i (item.id)} +
  • {i + 1}: {item.name} x {item.qty}
  • +{/each} +``` + +You can freely use destructuring and rest patterns in each blocks. + +```svelte +{#each items as { id, name, qty }, i (id)} +
  • {i + 1}: {name} x {qty}
  • +{/each} + +{#each objects as { id, ...rest }} +
  • {id}
  • +{/each} + +{#each items as [id, ...rest]} +
  • {id}
  • +{/each} +``` diff --git a/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/hydratable.md b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/hydratable.md new file mode 100644 index 000000000..a7baf74d9 --- /dev/null +++ b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/hydratable.md @@ -0,0 +1,100 @@ +In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes: + +```svelte + + +

    {user.name}

    +``` + +That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions). + +To fix the example above: + +```svelte + + +

    {user.name}

    +``` + +This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration: + +```ts +import { hydratable } from 'svelte'; +const rand = hydratable('random', () => Math.random()); +``` + +If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries. + +## Serialization + +All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises: + +```svelte + + +{await promises.one} +{await promises.two} +``` + +## CSP + +`hydratable` adds an inline ` + +{#snippet hello(name)} +

    hello {name}! {message}!

    +{/snippet} + +{@render hello('alice')} +{@render hello('bob')} +``` + +...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings): + +```svelte +
    + {#snippet x()} + {#snippet y()}...{/snippet} + + + {@render y()} + {/snippet} + + + {@render y()} +
    + + +{@render x()} +``` + +Snippets can reference themselves and each other (demo: + +```svelte +{#snippet blastoff()} + 🚀 +{/snippet} + +{#snippet countdown(n)} + {#if n > 0} + {n}... + {@render countdown(n - 1)} + {:else} + {@render blastoff()} + {/if} +{/snippet} + +{@render countdown(10)} +``` + +## Passing snippets to components + +### Explicit props + +Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo: + +```svelte + + +{#snippet header()} + fruit + qty + price + total +{/snippet} + +{#snippet row(d)} + {d.name} + {d.qty} + {d.price} + {d.qty * d.price} +{/snippet} + + +``` + +Think about it like passing content instead of data to a component. The concept is similar to slots in web components. + +### Implicit props + +As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo: + +```svelte + +
    + {#snippet header()} + + + + + {/snippet} + + {#snippet row(d)} + + + + + {/snippet} +
    fruitqtypricetotal{d.name}{d.qty}{d.price}{d.qty * d.price}
    +``` + +### Implicit `children` snippet + +Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo: + +```svelte + + +``` + +```svelte + + + + + +``` + +> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name + +### Optional snippet props + +You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set... + +```svelte + + +{@render children?.()} +``` + +...or use an `#if` block to render fallback content: + +```svelte + + +{#if children} + {@render children()} +{:else} + fallback content +{/if} +``` + +## Typing snippets + +Snippets implement the `Snippet` interface imported from `'svelte'`: + +```svelte + +``` + +With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters. + +We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type: + +```svelte + +``` + +## Exporting snippets + +Snippets declared at the top level of a `.svelte` file can be exported from a ` + +{#snippet add(a, b)} + {a} + {b} = {a + b} +{/snippet} +``` + +> [!NOTE] +> This requires Svelte 5.5.0 or newer + +## Programmatic snippets + +Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases. + +## Snippets and slots + +In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5. diff --git a/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/svelte-reactivity.md b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/svelte-reactivity.md new file mode 100644 index 000000000..262e3617f --- /dev/null +++ b/packages/sv/src/create/shared/+skills/svelte-core-bestpractices/references/svelte-reactivity.md @@ -0,0 +1,61 @@ +## createSubscriber + +
    + +Available since 5.7.0 + +
    + +Returns a `subscribe` function that integrates external event-based systems with Svelte's reactivity. +It's particularly useful for integrating with web APIs like `MediaQuery`, `IntersectionObserver`, or `WebSocket`. + +If `subscribe` is called inside an effect (including indirectly, for example inside a getter), +the `start` callback will be called with an `update` function. Whenever `update` is called, the effect re-runs. + +If `start` returns a cleanup function, it will be called when the effect is destroyed. + +If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects +are active, and the returned teardown function will only be called when all effects are destroyed. + +It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery): + +```js +// @errors: 7031 +import { createSubscriber } from 'svelte/reactivity'; +import { on } from 'svelte/events'; + +export class MediaQuery { + #query; + #subscribe; + + constructor(query) { + this.#query = window.matchMedia(`(${query})`); + + this.#subscribe = createSubscriber((update) => { + // when the `change` event occurs, re-run any effects that read `this.current` + const off = on(this.#query, 'change', update); + + // stop listening when all the effects are destroyed + return () => off(); + }); + } + + get current() { + // This makes the getter reactive, if read in an effect + this.#subscribe(); + + // Return the current state of the query, whether or not we're in an effect + return this.#query.matches; + } +} +``` + +
    + +```dts +function createSubscriber( + start: (update: () => void) => (() => void) | void +): () => void; +``` + +
    diff --git a/scripts/update-dependencies.js b/scripts/update-dependencies.js index 0fc7fcfea..7682fa87a 100644 --- a/scripts/update-dependencies.js +++ b/scripts/update-dependencies.js @@ -148,7 +148,7 @@ await updatePackageFiles('packages/sv/src/create/templates', 'package.template.j // Update shared package.json files await updatePackageFiles('packages/sv/src/create/shared', 'package.json', 'shared'); -// Fetch the latest AGENTS.md from the MCP repo +// Fetch the latest AGENTS.md from the ai-tools repo const agents_response = await fetch( 'https://raw.githubusercontent.com/sveltejs/ai-tools/refs/heads/main/tools/instructions/AGENTS.md' ); @@ -156,3 +156,56 @@ fs.writeFileSync( path.resolve('packages', 'sv', 'src', 'create', 'shared', '+mcp', 'AGENTS.md'), await agents_response.text() ); + +// Fetch the latest skills from the ai-tools repo +const skillsBase = + 'https://raw.githubusercontent.com/sveltejs/ai-tools/refs/heads/main/tools/skills'; +const sharedSkillsBase = path.resolve('packages', 'sv', 'src', 'create', 'shared', '+skills'); + +/** @param {string} skillPath */ +async function fetchSkillFile(skillPath) { + const response = await fetch(`${skillsBase}/${skillPath}`); + const dest = path.resolve(sharedSkillsBase, skillPath); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, await response.text()); +} + +// Fetch skill files using the GitHub API to discover all files +const skillsApiBase = 'https://api.github.com/repos/sveltejs/ai-tools/contents/tools/skills'; + +/** @param {string} apiUrl */ +async function fetchSkillDir(apiUrl) { + const response = await fetch(apiUrl); + const entries = await response.json(); + for (const entry of entries) { + if (entry.type === 'file') { + const skillPath = entry.path.replace('tools/skills/', ''); + console.log(` - fetching skill: ${styleText('blue', skillPath)}`); + await fetchSkillFile(skillPath); + } else if (entry.type === 'dir') { + await fetchSkillDir(entry.url); + } + } +} + +console.log(`Fetching ${styleText(['cyanBright', 'bold'], 'skills')} from ai-tools repo`); +await fetchSkillDir(skillsApiBase); + +// Fetch the latest agents from the ai-tools repo +const agentsBase = + 'https://raw.githubusercontent.com/sveltejs/ai-tools/refs/heads/main/tools/agents'; +const sharedAgentsBase = path.resolve('packages', 'sv', 'src', 'create', 'shared', '+agents'); +const agentsApiBase = 'https://api.github.com/repos/sveltejs/ai-tools/contents/tools/agents'; + +console.log(`Fetching ${styleText(['cyanBright', 'bold'], 'agents')} from ai-tools repo`); +const agentsResponse = await fetch(agentsApiBase); +const agentEntries = await agentsResponse.json(); +for (const entry of agentEntries) { + if (entry.type === 'file') { + const agentName = entry.name; + console.log(` - fetching agent: ${styleText('blue', agentName)}`); + const response = await fetch(`${agentsBase}/${agentName}`); + fs.mkdirSync(sharedAgentsBase, { recursive: true }); + fs.writeFileSync(path.resolve(sharedAgentsBase, agentName), await response.text()); + } +}