mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-08-03 09:04:16 +08:00
Compare commits
6 Commits
@sveltejs/
...
svelte-cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2571d81ad | ||
|
|
ed4272d55f | ||
|
|
ab14e46ec3 | ||
|
|
d6c694c778 | ||
|
|
a52c8b63bb | ||
|
|
8152ed9fd4 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/ai-tools" }],
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "sveltejs/ai-tools", "template": "\n- {summary} {ref}" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
|
||||
5
.changeset/fancy-paws-say.md
Normal file
5
.changeset/fancy-paws-say.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@sveltejs/opencode': patch
|
||||
---
|
||||
|
||||
chore: show warning for old versions
|
||||
5
.changeset/lucky-cars-invite.md
Normal file
5
.changeset/lucky-cars-invite.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@sveltejs/opencode': patch
|
||||
---
|
||||
|
||||
feat: add `autoupdate` option to reinstall the plugin when a new version is available
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/opencode/config.ts'
|
||||
- 'packages/opencode/config.js'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
## Summary
|
||||
Automatically generated update for OpenCode JSON schema.
|
||||
|
||||
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.ts`.
|
||||
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.js`.
|
||||
|
||||
## Changes
|
||||
- Updated `packages/opencode/schema.json` with latest JSON schema
|
||||
|
||||
@@ -34,11 +34,11 @@ The package also includes a TUI plugin for configuring these features interactiv
|
||||
}
|
||||
```
|
||||
|
||||
Restart OpenCode, then run `/svelte-plugin` or select **Configure Svelte plugin** from the command palette. Choose whether to edit the project or global configuration, then use the checkboxes and radio options to configure the plugin. Changes are saved automatically, and **Revert changes** restores the values from when the dialog was opened.
|
||||
Restart OpenCode, then run `/svelte-plugin` or select 'Configure Svelte plugin' from the command palette. Choose whether to edit the project or global configuration, then use the checkboxes and radio options to configure the plugin. Changes are saved automatically, and 'Revert changes' restores the values from when the dialog was opened.
|
||||
|
||||
## Configuration
|
||||
|
||||
By default, everything is enabled. The TUI plugin writes the same configuration files that you can create or edit manually:
|
||||
By default, the MCP server, subagent, skills, instructions, and automatic updates are enabled. The TUI plugin writes the same configuration files that you can create or edit manually:
|
||||
|
||||
- locally, in `.opencode/svelte.json`
|
||||
- globally, in `~/.config/opencode/svelte.json` (or, if you have specified the environment variable, in `$OPENCODE_CONFIG_DIR/svelte.json`)
|
||||
@@ -68,6 +68,13 @@ By default, everything is enabled. The TUI plugin writes the same configuration
|
||||
},
|
||||
"instructions": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"autoupdate": true
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic updates
|
||||
|
||||
The plugin checks npm for newer versions and warns you when one is available. OpenCode caches plugins, so it continues using the cached version until that cache is removed.
|
||||
|
||||
Automatic updates are enabled by default. After detecting a newer version, the plugin removes itself from the cache when OpenCode shuts down. OpenCode installs the latest version the next time it starts. Automatic updates only apply when the plugin is unpinned or explicitly uses the `latest` tag. Exact versions, ranges, and other dist-tags are left untouched because reinstalling them may resolve to the same version again. Set `"autoupdate": false` to only receive the warning.
|
||||
|
||||
@@ -10,7 +10,7 @@ If possible, we recommend that you instruct the LLM to execute MCP calls with th
|
||||
|
||||
## Installation
|
||||
|
||||
In VS Code, run the **Install plugin from source** command and use the repository URL:
|
||||
In VS Code, run the 'Install plugin from source' command and use the repository URL:
|
||||
|
||||
```text
|
||||
https://github.com/sveltejs/ai-tools
|
||||
|
||||
@@ -43,12 +43,12 @@
|
||||
],
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@changesets/changelog-github": "catalog:tooling",
|
||||
"@changesets/cli": "catalog:tooling",
|
||||
"@eslint/compat": "catalog:lint",
|
||||
"@eslint/js": "catalog:lint",
|
||||
"@modelcontextprotocol/inspector": "catalog:ai",
|
||||
"@sveltejs/adapter-vercel": "catalog:svelte",
|
||||
"@svitejs/changesets-changelog-github-compact": "catalog:tooling",
|
||||
"eslint": "catalog:lint",
|
||||
"eslint-config-prettier": "catalog:lint",
|
||||
"eslint-plugin-import": "catalog:lint",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @sveltejs/opencode
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: ship plugin as js files instead of ts ([#236](https://github.com/sveltejs/ai-tools/pull/236))
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -72,10 +72,17 @@ Create `svelte.json` to customize how the plugin configures MCP, the Svelte suba
|
||||
},
|
||||
"skills": {
|
||||
"enabled": ["svelte-code-writer", "svelte-core-bestpractices"]
|
||||
}
|
||||
},
|
||||
"autoupdate": true
|
||||
}
|
||||
```
|
||||
|
||||
### Auto update
|
||||
|
||||
The plugin checks npm for newer versions and warns you when one is available. OpenCode caches plugins, so a new version is only picked up once that cache is wiped.
|
||||
|
||||
Automatic updates are enabled by default. When a newer version is detected, the plugin removes itself from the OpenCode cache as OpenCode shuts down, so the latest version is installed on the next start. This only applies when the plugin is unpinned or explicitly uses the `latest` tag. Exact versions, ranges, and other dist-tags are left untouched because reinstalling them may resolve to the same version again. Set `"autoupdate": false` to only receive the warning.
|
||||
|
||||
### Defaults
|
||||
|
||||
If omitted, the plugin uses these defaults:
|
||||
@@ -86,6 +93,7 @@ If omitted, the plugin uses these defaults:
|
||||
- `subagent.agents`: `{}`
|
||||
- `instructions.enabled`: `true`
|
||||
- `skills.enabled`: `true`
|
||||
- `autoupdate`: `true`
|
||||
|
||||
### Configuration Options
|
||||
|
||||
@@ -100,6 +108,7 @@ If omitted, the plugin uses these defaults:
|
||||
| `subagent.agents.svelte-file-editor.maxSteps` | `number` | unlimited | Limit the number of steps the subagent can execute. |
|
||||
| `instructions.enabled` | `boolean` | `true` | Enable or disable automatic instruction-file injection. |
|
||||
| `skills.enabled` | `boolean \| string[]` | `true` | Enable all skills (`true`), disable all skills (`false`), or enable only specific skill names. |
|
||||
| `autoupdate` | `boolean` | `true` | Remove an unpinned/latest plugin from the cache on exit when a newer version is available. |
|
||||
|
||||
### Supported Skill Names
|
||||
|
||||
|
||||
@@ -8,4 +8,4 @@ export const agents = {
|
||||
prompt:
|
||||
"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.\n\nIf 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.\n\nIf the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.\n\n## Available MCP Tools\n\n### 1. list-sections\n\nLists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.\n\n### 2. get-documentation\n\nRetrieves 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.\n\n**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`\n\n### 3. svelte-autofixer\n\nAnalyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:\n\n- Using `$effect` instead of `$derived` for computations\n- Missing cleanup in effects\n- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)\n- Missing keys in `{#each}` blocks\n- And more\n\n## Workflow\n\nWhen invoked to work on a Svelte file:\n\n### 1. Gather Context (if needed)\n\nIf you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:\n\n1. Call `list-sections` to see available documentation\n2. Call `get-documentation` with relevant section names\n\n### 2. Read the Target File\n\nRead the file to understand the current implementation.\n\n### 3. Make Changes\n\nApply edits following Svelte 5 best practices:\n\n### 4. Validate Changes\n\nAfter editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.\n\n### 5. Fix Any Issues\n\nIf the autofixer reports problems, fix them and re-validate until no issues remain.\n\n## Output Format\n\nAfter completing your work, provide:\n\n1. Summary of changes made\n2. Any issues found and fixed by the autofixer\n3. Recommendations for further improvements (if any)",
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { PluginInput } from '@opencode-ai/plugin';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import * as v from 'valibot';
|
||||
|
||||
/** @typedef {import('@opencode-ai/plugin').PluginInput} PluginInput */
|
||||
|
||||
// Schema for individual agent configuration
|
||||
const agent_config_schema = v.object({
|
||||
model: v.pipe(
|
||||
@@ -28,19 +29,20 @@ const agent_config_schema = v.object({
|
||||
|
||||
const default_config = {
|
||||
mcp: {
|
||||
type: 'remote' as 'remote' | 'local',
|
||||
type: /** @type {'remote' | 'local'} */ ('remote'),
|
||||
enabled: true,
|
||||
},
|
||||
subagent: {
|
||||
enabled: true,
|
||||
agents: {} as Record<string, v.InferInput<typeof agent_config_schema>>,
|
||||
agents: /** @type {Record<string, v.InferInput<typeof agent_config_schema>>} */ ({}),
|
||||
},
|
||||
instructions: {
|
||||
enabled: true,
|
||||
},
|
||||
skills: {
|
||||
enabled: true as boolean | string[],
|
||||
enabled: /** @type {boolean | string[]} */ (true),
|
||||
},
|
||||
autoupdate: true,
|
||||
};
|
||||
|
||||
export const config_schema = v.object({
|
||||
@@ -89,27 +91,32 @@ export const config_schema = v.object({
|
||||
'Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills.',
|
||||
),
|
||||
),
|
||||
autoupdate: v.pipe(
|
||||
v.optional(v.boolean()),
|
||||
v.description(
|
||||
'When a new version of an unpinned or latest-tagged plugin is available, remove it from the opencode cache on exit so that the latest version is installed the next time opencode starts. Enabled by default; set it to false to only get a warning.',
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
export type McpConfig = v.InferInput<typeof config_schema>;
|
||||
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
|
||||
|
||||
const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode');
|
||||
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, 'svelte.json');
|
||||
|
||||
interface ConfigLoadResult {
|
||||
data: Record<string, unknown> | null;
|
||||
parse_error?: string;
|
||||
}
|
||||
/** @typedef {{ data: Record<string, unknown> | null, parse_error?: string }} ConfigLoadResult */
|
||||
|
||||
function get_config_paths() {
|
||||
// Global: ~/.config/opencode/svelte.json
|
||||
let global_path: string | null = null;
|
||||
/** @type {string | null} */
|
||||
let global_path = null;
|
||||
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
||||
global_path = GLOBAL_CONFIG_PATH;
|
||||
}
|
||||
|
||||
// Custom config directory: $OPENCODE_CONFIG_DIR/svelte.json
|
||||
let config_dir_path: string | null = null;
|
||||
/** @type {string | null} */
|
||||
let config_dir_path = null;
|
||||
const opencode_config_dir = process.env.OPENCODE_CONFIG_DIR;
|
||||
if (opencode_config_dir) {
|
||||
const config_json = join(opencode_config_dir, 'svelte.json');
|
||||
@@ -119,7 +126,8 @@ function get_config_paths() {
|
||||
}
|
||||
|
||||
// Project-local: ./.opencode/svelte.json (cwd)
|
||||
let project_path: string | null = null;
|
||||
/** @type {string | null} */
|
||||
let project_path = null;
|
||||
const project_config = join(process.cwd(), '.opencode', 'svelte.json');
|
||||
if (existsSync(project_config)) {
|
||||
project_path = project_config;
|
||||
@@ -129,8 +137,13 @@ function get_config_paths() {
|
||||
return [global_path, config_dir_path, project_path];
|
||||
}
|
||||
|
||||
function load_config_file(config_path: string): ConfigLoadResult {
|
||||
let file_content: string;
|
||||
/**
|
||||
* @param {string} config_path
|
||||
* @returns {ConfigLoadResult}
|
||||
*/
|
||||
function load_config_file(config_path) {
|
||||
/** @type {string} */
|
||||
let file_content;
|
||||
try {
|
||||
file_content = readFileSync(config_path, 'utf-8');
|
||||
} catch {
|
||||
@@ -144,7 +157,7 @@ function load_config_file(config_path: string): ConfigLoadResult {
|
||||
return { data: null, parse_error: 'Config file is empty or invalid' };
|
||||
}
|
||||
return { data: parsed };
|
||||
} catch (error: unknown) {
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
parse_error: error instanceof Error ? error.message : 'Failed to parse config',
|
||||
@@ -152,7 +165,11 @@ function load_config_file(config_path: string): ConfigLoadResult {
|
||||
}
|
||||
}
|
||||
|
||||
function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
|
||||
/**
|
||||
* @param {Partial<McpConfig>} user_config
|
||||
* @returns {McpConfig}
|
||||
*/
|
||||
function merge_with_defaults(user_config) {
|
||||
return {
|
||||
mcp: {
|
||||
...default_config.mcp,
|
||||
@@ -174,12 +191,15 @@ function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
|
||||
...default_config.skills,
|
||||
...user_config.skills,
|
||||
},
|
||||
autoupdate: user_config.autoupdate ?? default_config.autoupdate,
|
||||
};
|
||||
}
|
||||
|
||||
export function get_mcp_config(ctx: PluginInput) {
|
||||
/** @param {PluginInput} ctx */
|
||||
export function get_mcp_config(ctx) {
|
||||
const config_paths = get_config_paths();
|
||||
let merged: Partial<McpConfig> = {};
|
||||
/** @type {Partial<McpConfig>} */
|
||||
let merged = {};
|
||||
|
||||
// Iterate from lowest to highest priority, merging as we go
|
||||
for (const path of config_paths) {
|
||||
@@ -209,6 +229,7 @@ export function get_mcp_config(ctx: PluginInput) {
|
||||
},
|
||||
instructions: { ...merged.instructions, ...parsed.output.instructions },
|
||||
skills: { ...merged.skills, ...parsed.output.skills },
|
||||
autoupdate: parsed.output.autoupdate ?? merged.autoupdate,
|
||||
};
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
@@ -1,14 +1,24 @@
|
||||
import type { Plugin } from '@opencode-ai/plugin';
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { agents } from './agents.ts';
|
||||
import { get_mcp_config } from './config.ts';
|
||||
import { agents } from './agents.js';
|
||||
import { get_mcp_config } from './config.js';
|
||||
import { setup_updates } from './update.js';
|
||||
|
||||
/** @typedef {import('@opencode-ai/plugin').Plugin} Plugin */
|
||||
|
||||
const current_dir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const svelte_plugin: Plugin = async (ctx) => {
|
||||
/**
|
||||
* @param {Parameters<Plugin>[0]} ctx
|
||||
* @returns {ReturnType<Plugin>}
|
||||
*/
|
||||
export async function svelte_plugin(ctx) {
|
||||
const mcp_config = get_mcp_config(ctx);
|
||||
const dispose = setup_updates(ctx, mcp_config.autoupdate === true);
|
||||
|
||||
return {
|
||||
dispose,
|
||||
async config(input) {
|
||||
input.agent ??= {};
|
||||
input.mcp ??= {};
|
||||
@@ -24,15 +34,13 @@ export const svelte_plugin: Plugin = async (ctx) => {
|
||||
const mcp = input.mcp[name];
|
||||
if (
|
||||
(mcp?.type === 'remote' && mcp.url.includes('https://mcp.svelte.dev/mcp')) ||
|
||||
(mcp?.type === 'local' &&
|
||||
mcp.command.some((cmd: string) => cmd.includes('@sveltejs/mcp')))
|
||||
(mcp?.type === 'local' && mcp.command.some((cmd) => cmd.includes('@sveltejs/mcp')))
|
||||
) {
|
||||
// if we found the svelte MCP server, we store its name and break
|
||||
svelte_mcp_name = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const mcp_config = get_mcp_config(ctx);
|
||||
|
||||
if (mcp_config.instructions?.enabled !== false) {
|
||||
const instructions_dir = join(current_dir, 'instructions');
|
||||
@@ -75,7 +83,8 @@ export const svelte_plugin: Plugin = async (ctx) => {
|
||||
if (mcp_config.subagent?.enabled !== false) {
|
||||
for (const [agent_name, agent_data] of Object.entries(agents)) {
|
||||
// we add the editor subagent that will be used when editing Svelte files to prevent wasting context on the main agent
|
||||
const default_config: (typeof input.agent)[string] = {
|
||||
/** @type {(typeof input.agent)[string]} */
|
||||
const default_config = {
|
||||
color: '#ff3e00',
|
||||
mode: 'subagent',
|
||||
prompt: agent_data.prompt,
|
||||
@@ -109,4 +118,4 @@ export const svelte_plugin: Plugin = async (ctx) => {
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sveltejs/opencode",
|
||||
"version": "0.1.10",
|
||||
"version": "0.1.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/sveltejs/ai-tools#readme",
|
||||
@@ -9,24 +9,25 @@
|
||||
},
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit",
|
||||
"generate-schema": "node --import node-resolve-ts/register scripts/generate-schema.ts"
|
||||
"generate-schema": "node scripts/generate-schema.js"
|
||||
},
|
||||
"files": [
|
||||
"index.ts",
|
||||
"config.ts",
|
||||
"tui.tsx",
|
||||
"agents.ts",
|
||||
"index.js",
|
||||
"config.js",
|
||||
"update.js",
|
||||
"tui.jsx",
|
||||
"agents.js",
|
||||
"instructions",
|
||||
"skills"
|
||||
],
|
||||
"exports": {
|
||||
"./server": {
|
||||
"types": "./index.ts",
|
||||
"import": "./index.ts"
|
||||
"types": "./index.js",
|
||||
"import": "./index.js"
|
||||
},
|
||||
"./tui": {
|
||||
"types": "./tui.tsx",
|
||||
"import": "./tui.tsx"
|
||||
"types": "./tui.jsx",
|
||||
"import": "./tui.jsx"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
@@ -38,15 +39,16 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"valibot": "catalog:tooling",
|
||||
"@opentui/core": "catalog:opencode",
|
||||
"@opentui/keymap": "catalog:opencode",
|
||||
"@opentui/solid": "catalog:opencode",
|
||||
"solid-js": "catalog:opencode"
|
||||
"solid-js": "catalog:opencode",
|
||||
"valibot": "catalog:tooling",
|
||||
"verkit": "catalog:tooling"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode-ai/plugin": "catalog:opencode",
|
||||
"@valibot/to-json-schema": "catalog:tooling",
|
||||
"@types/node": "catalog:tooling"
|
||||
"@types/node": "catalog:tooling",
|
||||
"@valibot/to-json-schema": "catalog:tooling"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
},
|
||||
"required": [],
|
||||
"description": "Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills."
|
||||
},
|
||||
"autoupdate": {
|
||||
"type": "boolean",
|
||||
"description": "When a new version of an unpinned or latest-tagged plugin is available, remove it from the opencode cache on exit so that the latest version is installed the next time opencode starts. Enabled by default; set it to false to only get a warning."
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
|
||||
@@ -4,7 +4,8 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Read agent names from tools/agents/*.md files
|
||||
function get_agent_names(agents_dir: string) {
|
||||
/** @param {string} agents_dir */
|
||||
function get_agent_names(agents_dir) {
|
||||
if (!fs.existsSync(agents_dir)) return [];
|
||||
return fs
|
||||
.readdirSync(agents_dir, { withFileTypes: true })
|
||||
@@ -12,7 +13,8 @@ function get_agent_names(agents_dir: string) {
|
||||
.map((entry) => entry.name.replace(/\.md$/, ''));
|
||||
}
|
||||
|
||||
function get_skill_names(skills_dir: string) {
|
||||
/** @param {string} skills_dir */
|
||||
function get_skill_names(skills_dir) {
|
||||
if (!fs.existsSync(skills_dir)) return [];
|
||||
return fs
|
||||
.readdirSync(skills_dir, { withFileTypes: true })
|
||||
@@ -29,10 +31,13 @@ const json_schema = toJsonSchema(schema);
|
||||
// This is the JSON Schema equivalent of `"a" | "b" | (string & {})` —
|
||||
// editors will autocomplete the known names but any string is still valid.
|
||||
if (skill_names.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const enabled = (json_schema as any).properties?.skills?.properties?.enabled;
|
||||
const enabled = /** @type {any} */ (json_schema).properties?.skills?.properties?.enabled;
|
||||
if (enabled?.anyOf) {
|
||||
const array_branch = enabled.anyOf.find((s: Record<string, unknown>) => s.type === 'array');
|
||||
const array_branch = enabled.anyOf.find(
|
||||
/** @type {(schema: Record<string, unknown>) => boolean} */ (
|
||||
(schema) => schema.type === 'array'
|
||||
),
|
||||
);
|
||||
if (array_branch) {
|
||||
array_branch.items = {
|
||||
anyOf: [{ enum: skill_names }, { type: 'string' }],
|
||||
@@ -48,8 +53,7 @@ const agents_dir = path.resolve('../../tools/agents');
|
||||
const agent_names = get_agent_names(agents_dir);
|
||||
|
||||
if (agent_names.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const agents = (json_schema as any).properties?.subagent?.properties?.agents;
|
||||
const agents = /** @type {any} */ (json_schema).properties?.subagent?.properties?.agents;
|
||||
if (agents) {
|
||||
agents.propertyNames = {
|
||||
anyOf: [{ enum: agent_names }, { type: 'string' }],
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "@opentui/solid",
|
||||
"types": ["@types/node"]
|
||||
},
|
||||
"include": ["index.ts", "config.ts", "agents.ts", "tui.tsx", "scripts/*"],
|
||||
"include": ["index.js", "config.js", "update.js", "agents.js", "tui.jsx", "scripts/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import type { TuiPlugin, TuiPluginModule } from '@opencode-ai/plugin/tui';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import * as v from 'valibot';
|
||||
import { config_schema, type McpConfig } from './config.ts';
|
||||
import { config_schema } from './config.js';
|
||||
|
||||
/** @typedef {import('@opencode-ai/plugin/tui').TuiPlugin} TuiPlugin */
|
||||
/** @typedef {import('@opencode-ai/plugin/tui').TuiPluginApi} TuiPluginApi */
|
||||
/** @typedef {import('@opencode-ai/plugin/tui').TuiPluginModule} TuiPluginModule */
|
||||
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
|
||||
/** @typedef {'project' | 'global'} Scope */
|
||||
/** @typedef {Partial<McpConfig>} Config */
|
||||
|
||||
const plugin_id = 'svelte.configure';
|
||||
const skill_names = ['svelte-code-writer', 'svelte-core-bestpractices'] as const;
|
||||
const skill_names = ['svelte-code-writer', 'svelte-core-bestpractices'];
|
||||
const agent_name = 'svelte-file-editor';
|
||||
|
||||
type Scope = 'project' | 'global';
|
||||
type Config = Partial<McpConfig>;
|
||||
|
||||
function project_root(api: Parameters<TuiPlugin>[0]) {
|
||||
/** @param {TuiPluginApi} api */
|
||||
function project_root(api) {
|
||||
const worktree = api.state.path.worktree;
|
||||
return worktree && worktree !== '/' ? worktree : api.state.path.directory;
|
||||
}
|
||||
|
||||
function config_path(api: Parameters<TuiPlugin>[0], scope: Scope) {
|
||||
/**
|
||||
* @param {TuiPluginApi} api
|
||||
* @param {Scope} scope
|
||||
*/
|
||||
function config_path(api, scope) {
|
||||
if (scope === 'project') return join(project_root(api), '.opencode', 'svelte.json');
|
||||
return join(
|
||||
process.env.OPENCODE_CONFIG_DIR ?? join(homedir(), '.config', 'opencode'),
|
||||
@@ -27,26 +35,40 @@ function config_path(api: Parameters<TuiPlugin>[0], scope: Scope) {
|
||||
);
|
||||
}
|
||||
|
||||
async function read_config(path: string): Promise<Config> {
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Promise<Config>}
|
||||
*/
|
||||
async function read_config(path) {
|
||||
if (!existsSync(path)) return {};
|
||||
const parsed: unknown = JSON.parse(await readFile(path, 'utf8'));
|
||||
/** @type {unknown} */
|
||||
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
||||
const result = v.safeParse(config_schema, parsed);
|
||||
if (!result.success)
|
||||
throw new Error('The existing file does not match the Svelte plugin schema.');
|
||||
// Keep schema annotations and future fields that this version does not edit.
|
||||
return parsed as Config;
|
||||
return /** @type {Config} */ (parsed);
|
||||
}
|
||||
|
||||
async function save_config(path: string, config: Config) {
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {Config} config
|
||||
*/
|
||||
async function save_config(path, config) {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, `${JSON.stringify(config, null, '\t')}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function display(value: unknown, fallback = 'default') {
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @param {string} [fallback]
|
||||
*/
|
||||
function display(value, fallback = 'default') {
|
||||
return value === undefined ? fallback : String(value);
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
/** @type {TuiPlugin} */
|
||||
const tui = async (api) => {
|
||||
function open_scope() {
|
||||
if (!api.state.path.directory) {
|
||||
api.ui.toast({
|
||||
@@ -57,7 +79,7 @@ const tui: TuiPlugin = async (api) => {
|
||||
}
|
||||
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogSelect<Scope>
|
||||
<api.ui.DialogSelect
|
||||
title="Configure Svelte plugin"
|
||||
options={[
|
||||
{
|
||||
@@ -71,14 +93,16 @@ const tui: TuiPlugin = async (api) => {
|
||||
description: 'Write svelte.json in the OpenCode config directory',
|
||||
},
|
||||
]}
|
||||
onSelect={(option) => void open_config(option.value)}
|
||||
onSelect={(option) => void open_config(/** @type {Scope} */ (option.value))}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
async function open_config(scope: Scope) {
|
||||
/** @param {Scope} scope */
|
||||
async function open_config(scope) {
|
||||
const path = config_path(api, scope);
|
||||
let config: Config;
|
||||
/** @type {Config} */
|
||||
let config;
|
||||
try {
|
||||
config = await read_config(path);
|
||||
} catch (error) {
|
||||
@@ -90,7 +114,8 @@ const tui: TuiPlugin = async (api) => {
|
||||
return;
|
||||
}
|
||||
const original_config = structuredClone(config);
|
||||
let current_option: string | undefined;
|
||||
/** @type {string | undefined} */
|
||||
let current_option;
|
||||
|
||||
async function persist(show_toast = true) {
|
||||
try {
|
||||
@@ -102,7 +127,11 @@ const tui: TuiPlugin = async (api) => {
|
||||
}
|
||||
}
|
||||
|
||||
function prompt_agent_number(key: 'temperature' | 'top_p' | 'maxSteps', label: string) {
|
||||
/**
|
||||
* @param {'temperature' | 'top_p' | 'maxSteps'} key
|
||||
* @param {string} label
|
||||
*/
|
||||
function prompt_agent_number(key, label) {
|
||||
const agent = config.subagent?.agents?.[agent_name];
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogPrompt
|
||||
@@ -129,7 +158,7 @@ const tui: TuiPlugin = async (api) => {
|
||||
function open_agent() {
|
||||
const agent = config.subagent?.agents?.[agent_name];
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogSelect<string>
|
||||
<api.ui.DialogSelect
|
||||
title={`Configure ${agent_name}`}
|
||||
options={[
|
||||
{ title: 'Model', value: 'model', description: display(agent?.model) },
|
||||
@@ -144,11 +173,12 @@ const tui: TuiPlugin = async (api) => {
|
||||
]}
|
||||
onSelect={(option) => {
|
||||
if (option.value === 'back') return open_menu();
|
||||
if (option.value !== 'model')
|
||||
return prompt_agent_number(
|
||||
option.value as 'temperature' | 'top_p' | 'maxSteps',
|
||||
option.title,
|
||||
);
|
||||
if (
|
||||
option.value === 'temperature' ||
|
||||
option.value === 'top_p' ||
|
||||
option.value === 'maxSteps'
|
||||
)
|
||||
return prompt_agent_number(option.value, option.title);
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogPrompt
|
||||
title={`${agent_name}: model`}
|
||||
@@ -178,14 +208,16 @@ const tui: TuiPlugin = async (api) => {
|
||||
Array.isArray(skills) ? skills : skills === false ? [] : skill_names,
|
||||
);
|
||||
const all_skills_selected = skill_names.every((name) => selected_skills.has(name));
|
||||
function checked(value: boolean | undefined) {
|
||||
/** @param {boolean | undefined} value */
|
||||
function checked(value) {
|
||||
return value !== false ? '[x]' : '[ ]';
|
||||
}
|
||||
function radio(value: 'remote' | 'local') {
|
||||
/** @param {'remote' | 'local'} value */
|
||||
function radio(value) {
|
||||
return (config.mcp?.type ?? 'remote') === value ? '(*)' : '( )';
|
||||
}
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogSelect<string>
|
||||
<api.ui.DialogSelect
|
||||
title={`Svelte plugin (${scope})`}
|
||||
{...(current_option === undefined ? {} : { current: current_option })}
|
||||
skipFilter
|
||||
@@ -218,6 +250,12 @@ const tui: TuiPlugin = async (api) => {
|
||||
value: `skill:${name}`,
|
||||
category: 'Skills',
|
||||
})),
|
||||
{
|
||||
title: `${config.autoupdate !== false ? '[x]' : '[ ]'} Auto update`,
|
||||
value: 'autoupdate',
|
||||
category: 'Updates',
|
||||
description: 'Reinstall the plugin on the next start when a new version is out',
|
||||
},
|
||||
{
|
||||
title: 'Revert changes',
|
||||
value: 'revert',
|
||||
@@ -252,6 +290,7 @@ const tui: TuiPlugin = async (api) => {
|
||||
if (option.value === 'skills-all') {
|
||||
config.skills = { enabled: all_skills_selected ? [] : [...skill_names] };
|
||||
}
|
||||
if (option.value === 'autoupdate') config.autoupdate = config.autoupdate === false;
|
||||
if (option.value.startsWith('skill:')) {
|
||||
const name = option.value.slice('skill:'.length);
|
||||
if (selected_skills.has(name)) {
|
||||
@@ -285,7 +324,7 @@ const tui: TuiPlugin = async (api) => {
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
export default /** @satisfies {TuiPluginModule & { id: string }} */ ({
|
||||
id: plugin_id,
|
||||
tui,
|
||||
} satisfies TuiPluginModule & { id: string };
|
||||
});
|
||||
97
packages/opencode/update.js
Normal file
97
packages/opencode/update.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { exec } from 'node:child_process';
|
||||
import { rmSync } from 'node:fs';
|
||||
import { basename, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { compare } from 'verkit';
|
||||
import package_json from './package.json' with { type: 'json' };
|
||||
|
||||
/** @typedef {import('@opencode-ai/plugin').PluginInput} PluginInput */
|
||||
|
||||
const current_dir = dirname(fileURLToPath(import.meta.url));
|
||||
const name_segments = package_json.name.split('/');
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @param {number} levels
|
||||
*/
|
||||
function up(dir, levels) {
|
||||
for (let i = 0; i < levels; i++) dir = dirname(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* opencode installs every plugin in `<cache>/packages/<spec>/node_modules/<name>`, so removing
|
||||
* `<spec>` is enough to make it reinstall the plugin from scratch on the next start.
|
||||
*
|
||||
* We return `null` whenever we don't recognize that layout (for example when the plugin is linked
|
||||
* locally during development) so that we never delete a folder we don't own.
|
||||
*/
|
||||
export function get_install_dir(dir = current_dir) {
|
||||
// from `<cache>/packages/<spec>/node_modules/<name>` up to `<cache>/packages/<spec>`
|
||||
const install_dir = up(dir, name_segments.length + 1);
|
||||
// ...and from there up to `<cache>/packages`
|
||||
if (basename(up(install_dir, name_segments.length)) !== 'packages') return null;
|
||||
// Only unconstrained installs can pick up npm's latest version. Ranges and alternate tags may
|
||||
// resolve to the same installed version after every wipe.
|
||||
const package_name = name_segments.at(-1);
|
||||
if (!package_name || ![package_name, `${package_name}@latest`].includes(basename(install_dir))) {
|
||||
return null;
|
||||
}
|
||||
return install_dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks npm for a newer version of the plugin and warns the user about it. If `autoupdate` is
|
||||
* enabled we also delete the cached plugin once opencode shuts down, so the next start picks up the
|
||||
* new version.
|
||||
*
|
||||
* @param {PluginInput} ctx
|
||||
* @param {boolean} autoupdate
|
||||
* @returns {() => Promise<void>} the `dispose` hook
|
||||
*/
|
||||
export function setup_updates(ctx, autoupdate) {
|
||||
/** @type {string | null} */
|
||||
let stale_dir = null;
|
||||
let wiped = false;
|
||||
|
||||
function wipe() {
|
||||
if (wiped || !stale_dir) return;
|
||||
wiped = true;
|
||||
try {
|
||||
rmSync(stale_dir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// if we can't delete it there's nothing useful we can do at this point, the user will
|
||||
// just get the warning again on the next start
|
||||
}
|
||||
}
|
||||
|
||||
exec(`npm view ${package_json.name} version`, (_, version) => {
|
||||
const latest = version?.trim();
|
||||
if (!latest || compare(latest, package_json.version) !== 1) return;
|
||||
|
||||
stale_dir = autoupdate ? get_install_dir() : null;
|
||||
// `dispose` covers a graceful shutdown, `exit` is the safety net for everything else. We only
|
||||
// register it once we know we have something to delete to avoid piling up listeners.
|
||||
if (stale_dir) process.once('exit', wipe);
|
||||
|
||||
setTimeout(() => {
|
||||
ctx.client.tui.showToast({
|
||||
body: {
|
||||
title: 'Svelte: new plugin version available',
|
||||
message: `${package_json.name}@${latest} is available (you are using ${package_json.version}).\n\n${
|
||||
stale_dir
|
||||
? 'It will be installed automatically the next time you start OpenCode.'
|
||||
: 'Wipe the cache or update your OpenCode config to update.'
|
||||
}`,
|
||||
variant: 'warning',
|
||||
duration: 7000,
|
||||
},
|
||||
});
|
||||
}, 7000);
|
||||
});
|
||||
|
||||
return async () => {
|
||||
process.off('exit', wipe);
|
||||
wipe();
|
||||
};
|
||||
}
|
||||
35
packages/opencode/update.test.js
Normal file
35
packages/opencode/update.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { get_install_dir } from './update.js';
|
||||
|
||||
const cache_packages = join('/cache', 'packages');
|
||||
|
||||
/**
|
||||
* @param {string} spec
|
||||
*/
|
||||
function plugin_dir(spec) {
|
||||
return join(cache_packages, '@sveltejs', spec, 'node_modules', '@sveltejs', 'opencode');
|
||||
}
|
||||
|
||||
describe('get_install_dir', () => {
|
||||
test.each([
|
||||
['an unpinned install', 'opencode'],
|
||||
['the latest tag', 'opencode@latest'],
|
||||
])('returns the cache directory for %s', (_, spec) => {
|
||||
expect(get_install_dir(plugin_dir(spec))).toBe(join(cache_packages, '@sveltejs', spec));
|
||||
});
|
||||
|
||||
test.each([
|
||||
['an exact version', 'opencode@0.1.11'],
|
||||
['an exact version with a v prefix', 'opencode@v0.1.11'],
|
||||
['a range', 'opencode@^0.1.0'],
|
||||
['an alternate dist-tag', 'opencode@beta'],
|
||||
])('ignores %s', (_, spec) => {
|
||||
expect(get_install_dir(plugin_dir(spec))).toBeNull();
|
||||
});
|
||||
|
||||
test('ignores a matching layout outside the OpenCode package cache', () => {
|
||||
const dir = join('/workspace', 'node_modules', '@sveltejs', 'opencode');
|
||||
expect(get_install_dir(dir)).toBeNull();
|
||||
});
|
||||
});
|
||||
76
pnpm-lock.yaml
generated
76
pnpm-lock.yaml
generated
@@ -42,7 +42,7 @@ catalogs:
|
||||
version: 1.5.0
|
||||
eslint-plugin-svelte:
|
||||
specifier: ^3.19.0
|
||||
version: 3.14.0
|
||||
version: 3.19.0
|
||||
globals:
|
||||
specifier: ^17.0.0
|
||||
version: 17.2.0
|
||||
@@ -107,12 +107,12 @@ catalogs:
|
||||
specifier: ^1.19.3
|
||||
version: 1.19.3
|
||||
tooling:
|
||||
'@changesets/changelog-github':
|
||||
specifier: 1.0.0-next.6
|
||||
version: 1.0.0-next.6
|
||||
'@changesets/cli':
|
||||
specifier: ^2.29.7
|
||||
version: 2.29.8
|
||||
'@svitejs/changesets-changelog-github-compact':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
'@types/estree':
|
||||
specifier: ^1.0.8
|
||||
version: 1.0.8
|
||||
@@ -149,6 +149,9 @@ catalogs:
|
||||
valibot:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
verkit:
|
||||
specifier: ^0.1.2
|
||||
version: 0.1.2
|
||||
vite:
|
||||
specifier: ^7.0.4
|
||||
version: 7.3.1
|
||||
@@ -166,6 +169,9 @@ importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@changesets/changelog-github':
|
||||
specifier: catalog:tooling
|
||||
version: 1.0.0-next.6
|
||||
'@changesets/cli':
|
||||
specifier: catalog:tooling
|
||||
version: 2.29.8(@types/node@24.10.9)
|
||||
@@ -181,9 +187,6 @@ importers:
|
||||
'@sveltejs/adapter-vercel':
|
||||
specifier: catalog:svelte
|
||||
version: 6.3.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.1(@typescript-eslint/types@8.54.0))(vite@7.3.1(@types/node@24.10.9)(yaml@2.9.0)))(svelte@5.56.1(@typescript-eslint/types@8.54.0))(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(yaml@2.9.0)))(rollup@4.57.0)
|
||||
'@svitejs/changesets-changelog-github-compact':
|
||||
specifier: catalog:tooling
|
||||
version: 1.2.0
|
||||
eslint:
|
||||
specifier: catalog:lint
|
||||
version: 9.39.2
|
||||
@@ -412,6 +415,9 @@ importers:
|
||||
valibot:
|
||||
specifier: catalog:tooling
|
||||
version: 1.2.0(typescript@5.9.3)
|
||||
verkit:
|
||||
specifier: catalog:tooling
|
||||
version: 0.1.2
|
||||
devDependencies:
|
||||
'@opencode-ai/plugin':
|
||||
specifier: catalog:opencode
|
||||
@@ -605,6 +611,10 @@ packages:
|
||||
'@changesets/changelog-git@0.2.1':
|
||||
resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
|
||||
|
||||
'@changesets/changelog-github@1.0.0-next.6':
|
||||
resolution: {integrity: sha512-0ShCWgNt50xP0mryAYT8wtSkMUinG8uhxXgCgwHZ4UvJnR2f4ns8OsGAxYu8FIds7kHFdLOz7qX4YQ6AX4tVUA==}
|
||||
engines: {node: ^22.11 || ^24 || >=26}
|
||||
|
||||
'@changesets/cli@2.29.8':
|
||||
resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==}
|
||||
hasBin: true
|
||||
@@ -618,8 +628,9 @@ packages:
|
||||
'@changesets/get-dependents-graph@2.1.3':
|
||||
resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==}
|
||||
|
||||
'@changesets/get-github-info@0.6.0':
|
||||
resolution: {integrity: sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==}
|
||||
'@changesets/get-github-info@1.0.0-next.4':
|
||||
resolution: {integrity: sha512-Bosh+XOoFvLMzAj301tg6phbrEggBtvEccrnceEvYsKSM7PjcIa32Fy22SU5g+501HZywkdwcAlybdWF+e/zzw==}
|
||||
engines: {node: ^22.11 || ^24 || >=26}
|
||||
|
||||
'@changesets/get-release-plan@4.0.14':
|
||||
resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==}
|
||||
@@ -651,6 +662,10 @@ packages:
|
||||
'@changesets/types@6.1.0':
|
||||
resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==}
|
||||
|
||||
'@changesets/types@7.0.0-next.7':
|
||||
resolution: {integrity: sha512-1XyshLw+lRCg2DWxi1Qt3hbezjBostHzXvGXVgSzAeZ+fL+r2ikcMbpaQ98D9ivwKhYFf1FBbKbEc+XsBZsIJQ==}
|
||||
engines: {node: ^22.11 || ^24 || >=26}
|
||||
|
||||
'@changesets/write@0.4.0':
|
||||
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
|
||||
|
||||
@@ -2053,10 +2068,6 @@ packages:
|
||||
svelte: ^5.0.0
|
||||
vite: ^6.3.0 || ^7.0.0
|
||||
|
||||
'@svitejs/changesets-changelog-github-compact@1.2.0':
|
||||
resolution: {integrity: sha512-08eKiDAjj4zLug1taXSIJ0kGL5cawjVCyJkBb6EWSg5fEPX6L+Wtr0CH2If4j5KYylz85iaZiFlUItvgJvll5g==}
|
||||
engines: {node: ^14.13.1 || ^16.0.0 || >=18}
|
||||
|
||||
'@tmcp/adapter-valibot@0.1.5':
|
||||
resolution: {integrity: sha512-9P2wrVYPngemNK0UvPb/opC722/jfd09QxXmme1TRp/wPsl98vpSk/MXt24BCMqBRv4Dvs0xxJH4KHDcjXW52Q==}
|
||||
peerDependencies:
|
||||
@@ -2611,8 +2622,8 @@ packages:
|
||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
dataloader@1.4.0:
|
||||
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
|
||||
dataloader@2.2.3:
|
||||
resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==}
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
@@ -2698,10 +2709,6 @@ packages:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
dotenv@16.6.1:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv@17.2.3:
|
||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -4516,6 +4523,10 @@ packages:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
verkit@0.1.2:
|
||||
resolution: {integrity: sha512-WqkT8n3hqizuCu71W3bUzf5fjBmkbXcudsehe/NbxA8PgqoKnSOY5K0Ba2ckg1qaRaSpSz7as/n9K1R9JXjQKg==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
vite-plugin-devtools-json@1.0.0:
|
||||
resolution: {integrity: sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw==}
|
||||
peerDependencies:
|
||||
@@ -4989,6 +5000,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
|
||||
'@changesets/changelog-github@1.0.0-next.6':
|
||||
dependencies:
|
||||
'@changesets/get-github-info': 1.0.0-next.4
|
||||
'@changesets/types': 7.0.0-next.7
|
||||
|
||||
'@changesets/cli@2.29.8(@types/node@24.10.9)':
|
||||
dependencies:
|
||||
'@changesets/apply-release-plan': 7.0.14
|
||||
@@ -5043,12 +5059,9 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
semver: 7.7.3
|
||||
|
||||
'@changesets/get-github-info@0.6.0':
|
||||
'@changesets/get-github-info@1.0.0-next.4':
|
||||
dependencies:
|
||||
dataloader: 1.4.0
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dataloader: 2.2.3
|
||||
|
||||
'@changesets/get-release-plan@4.0.14':
|
||||
dependencies:
|
||||
@@ -5104,6 +5117,8 @@ snapshots:
|
||||
|
||||
'@changesets/types@6.1.0': {}
|
||||
|
||||
'@changesets/types@7.0.0-next.7': {}
|
||||
|
||||
'@changesets/write@0.4.0':
|
||||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
@@ -6280,13 +6295,6 @@ snapshots:
|
||||
vite: 7.3.1(@types/node@24.10.9)(yaml@2.9.0)
|
||||
vitefu: 1.1.1(vite@7.3.1(@types/node@24.10.9)(yaml@2.9.0))
|
||||
|
||||
'@svitejs/changesets-changelog-github-compact@1.2.0':
|
||||
dependencies:
|
||||
'@changesets/get-github-info': 0.6.0
|
||||
dotenv: 16.6.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@tmcp/adapter-valibot@0.1.5(tmcp@1.19.3(typescript@5.9.3))(valibot@1.2.0(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
@@ -6868,7 +6876,7 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.2
|
||||
|
||||
dataloader@1.4.0: {}
|
||||
dataloader@2.2.3: {}
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
@@ -6929,8 +6937,6 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dotenv@16.6.1: {}
|
||||
|
||||
dotenv@17.2.3: {}
|
||||
|
||||
dts-resolver@2.1.3: {}
|
||||
@@ -8918,6 +8924,8 @@ snapshots:
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
verkit@0.1.2: {}
|
||||
|
||||
vite-plugin-devtools-json@1.0.0(vite@7.3.1(@types/node@24.10.9)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
uuid: 11.1.0
|
||||
|
||||
@@ -8,12 +8,6 @@ catalogs:
|
||||
'@anthropic-ai/sdk': ^0.71.0
|
||||
'@mcp-ui/server': ^6.0.0
|
||||
'@modelcontextprotocol/inspector': ^0.19.0
|
||||
opencode:
|
||||
'@opencode-ai/plugin': 1.17.9
|
||||
'@opentui/core': ^0.4.3
|
||||
'@opentui/keymap': ^0.4.3
|
||||
'@opentui/solid': ^0.4.3
|
||||
solid-js: 1.9.12
|
||||
lint:
|
||||
'@eslint/compat': ^2.0.0
|
||||
'@eslint/js': ^9.36.0
|
||||
@@ -29,6 +23,12 @@ catalogs:
|
||||
prettier-plugin-svelte: ^3.3.3
|
||||
svelte-eslint-parser: ^1.7.1
|
||||
typescript-eslint: ^8.44.0
|
||||
opencode:
|
||||
'@opencode-ai/plugin': 1.17.9
|
||||
'@opentui/core': ^0.4.3
|
||||
'@opentui/keymap': ^0.4.3
|
||||
'@opentui/solid': ^0.4.3
|
||||
solid-js: 1.9.12
|
||||
svelte:
|
||||
'@sveltejs/adapter-vercel': ^6.0.0
|
||||
'@sveltejs/kit': ^2.42.2
|
||||
@@ -42,8 +42,8 @@ catalogs:
|
||||
'@tmcp/transport-stdio': ^0.4.2
|
||||
tmcp: ^1.19.3
|
||||
tooling:
|
||||
'@changesets/changelog-github': 1.0.0-next.6
|
||||
'@changesets/cli': ^2.29.7
|
||||
'@svitejs/changesets-changelog-github-compact': ^1.2.0
|
||||
'@types/estree': ^1.0.8
|
||||
'@types/node': ^24.3.1
|
||||
'@valibot/to-json-schema': ^1.5.0
|
||||
@@ -56,6 +56,7 @@ catalogs:
|
||||
tsdown: ^0.20.0
|
||||
typescript: ^5.0.0
|
||||
valibot: ^1.2.0
|
||||
verkit: ^0.1.2
|
||||
vite: ^7.0.4
|
||||
vite-plugin-devtools-json: ^1.0.0
|
||||
vitest: ^4.0.0
|
||||
|
||||
@@ -68,7 +68,7 @@ function parse_agent_md(content: string, file_path: string): AgentData | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate agents.ts module from tools/agents/*.md files
|
||||
* Generate agents.js module from tools/agents/*.md files
|
||||
*/
|
||||
async function sync_agents() {
|
||||
const agents_dir = path.join(TOOLS_DIR, 'agents');
|
||||
@@ -96,14 +96,14 @@ async function sync_agents() {
|
||||
),
|
||||
null,
|
||||
'\t',
|
||||
)} as const;`,
|
||||
)};`,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const dest = path.join(OPENCODE_PKG_DIR, 'agents.ts');
|
||||
const dest = path.join(OPENCODE_PKG_DIR, 'agents.js');
|
||||
await fs.writeFile(dest, output);
|
||||
|
||||
console.log(`Generated agents.ts with ${agents.length} agent(s)`);
|
||||
console.log(`Generated agents.js with ${agents.length} agent(s)`);
|
||||
}
|
||||
|
||||
await sync_skills();
|
||||
|
||||
@@ -3,13 +3,13 @@ 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.
|
||||
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 validate 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
|
||||
## Available MCP tools
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
@@ -35,30 +35,30 @@ Analyzes Svelte code and returns suggestions to fix issues. Pass the component c
|
||||
|
||||
When invoked to work on a Svelte file:
|
||||
|
||||
### 1. Gather Context (if needed)
|
||||
### 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
|
||||
### 2. Read the target file
|
||||
|
||||
Read the file to understand the current implementation.
|
||||
|
||||
### 3. Make Changes
|
||||
### 3. Make changes
|
||||
|
||||
Apply edits following Svelte 5 best practices:
|
||||
|
||||
### 4. Validate Changes
|
||||
### 4. Validate changes
|
||||
|
||||
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
|
||||
|
||||
### 5. Fix Any Issues
|
||||
### 5. Fix any issues
|
||||
|
||||
If the autofixer reports problems, fix them and re-validate until no issues remain.
|
||||
|
||||
## Output Format
|
||||
## Output format
|
||||
|
||||
After completing your work, provide:
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ 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
|
||||
## CLI tools
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
### List documentation sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
@@ -17,7 +15,7 @@ npx @sveltejs/mcp list-sections
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
### Get documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
@@ -31,7 +29,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
### Svelte autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
|
||||
Reference in New Issue
Block a user