Compare commits

...

2 Commits

Author SHA1 Message Date
jyc.dev
d2571d81ad chore: fix typos and heading casing in agent + skill docs (#240) 2026-07-26 14:56:11 +02:00
Paolo Ricciuti
ed4272d55f chore: show warning for old versions (#238)
Co-authored-by: Rich Harris <hello@rich-harris.dev>
2026-07-26 14:55:12 +02:00
16 changed files with 225 additions and 31 deletions

View File

@@ -0,0 +1,5 @@
---
'@sveltejs/opencode': patch
---
chore: show warning for old versions

View File

@@ -0,0 +1,5 @@
---
'@sveltejs/opencode': patch
---
feat: add `autoupdate` option to reinstall the plugin when a new version is available

View File

@@ -38,7 +38,7 @@ Restart OpenCode, then run `/svelte-plugin` or select 'Configure Svelte plugin'
## 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.

View File

@@ -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

View File

@@ -42,6 +42,7 @@ const default_config = {
skills: {
enabled: /** @type {boolean | string[]} */ (true),
},
autoupdate: true,
};
export const config_schema = v.object({
@@ -90,6 +91,12 @@ 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.',
),
),
});
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
@@ -184,6 +191,7 @@ function merge_with_defaults(user_config) {
...default_config.skills,
...user_config.skills,
},
autoupdate: user_config.autoupdate ?? default_config.autoupdate,
};
}
@@ -221,6 +229,7 @@ export function get_mcp_config(ctx) {
},
instructions: { ...merged.instructions, ...parsed.output.instructions },
skills: { ...merged.skills, ...parsed.output.skills },
autoupdate: parsed.output.autoupdate ?? merged.autoupdate,
};
} else {
setTimeout(() => {

View File

@@ -3,6 +3,7 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
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 */
@@ -13,7 +14,11 @@ const current_dir = dirname(fileURLToPath(import.meta.url));
* @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 ??= {};
@@ -36,7 +41,6 @@ export async function svelte_plugin(ctx) {
break;
}
}
const mcp_config = get_mcp_config(ctx);
if (mcp_config.instructions?.enabled !== false) {
const instructions_dir = join(current_dir, 'instructions');

View File

@@ -14,6 +14,7 @@
"files": [
"index.js",
"config.js",
"update.js",
"tui.jsx",
"agents.js",
"instructions",
@@ -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"
}
}

View File

@@ -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": [],

View File

@@ -5,6 +5,6 @@
"jsxImportSource": "@opentui/solid",
"types": ["@types/node"]
},
"include": ["index.js", "config.js", "agents.js", "tui.jsx", "scripts/*"],
"include": ["index.js", "config.js", "update.js", "agents.js", "tui.jsx", "scripts/*"],
"exclude": ["node_modules"]
}

View File

@@ -250,6 +250,12 @@ const tui = 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',
@@ -284,6 +290,7 @@ const tui = 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)) {

View 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();
};
}

View 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();
});
});

15
pnpm-lock.yaml generated
View File

@@ -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
@@ -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
@@ -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
@@ -3141,7 +3147,6 @@ packages:
glob@9.3.5:
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
engines: {node: '>=16 || 14 >=14.17'}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
@@ -4518,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:
@@ -8915,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

View File

@@ -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
@@ -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

View File

@@ -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:

View File

@@ -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]