Skip to content

Commit 66ba569

Browse files
authored
Merge pull request #2042 from nukeop/feat/plugin-auto-update
Feat/plugin auto update
2 parents 76e9053 + 6d00e1b commit 66ba569

36 files changed

Lines changed: 1325 additions & 426 deletions

packages/docs/plugins/getting-started.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Run `npm init` inside.
2929
"main": "index.ts",
3030
"nuclear": {
3131
"displayName": "Hello Plugin",
32-
"category": "Examples"
32+
"categories": ["other"]
3333
}
3434
}
3535
```
@@ -96,5 +96,5 @@ type Plugin = {
9696
* `name`, `version`, `description`, `author`
9797
* `main` (optional). If missing, the app tries `index.js`, `index.ts`, `index.tsx`, then `dist/index.*`.
9898
* `nuclear.displayName` (optional UI name)
99-
* `nuclear.category` (shown in the Plugins list)
99+
* `nuclear.categories` (shown in the Plugins list)
100100
* `nuclear.icon` and `nuclear.permissions` (optional; unknown permissions get a warning)

packages/docs/plugins/plugin-store.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ After installation, the plugin appears in the Installed tab. Toggle it on to ena
2424

2525
You can also install plugins from a local folder. In the Installed tab, click "Add Plugin" and select a folder containing a plugin (a directory with a `package.json` and an entry file).
2626

27-
Manually installed plugins have one extra feature: a reload button. Click it to re-read the plugin source from the original folder, recompile, and reload. This is useful during development. Store-installed plugins don't support reload; remove and reinstall to get a new version. Auto-update is planned for the future.
27+
Manually installed plugins have one extra feature: a reload button. Click it to re-read the plugin source from the original folder, recompile, and reload. This is useful during development. Store-installed plugins don't support reload; remove and reinstall to get a new version. Store plugins are updated automatically on startup (see Plugin updates below).
2828

2929
## Managing plugins
3030

@@ -44,7 +44,11 @@ Dev plugins show a reload button that re-reads the source from the original fold
4444

4545
## Plugin updates
4646

47-
There's no automatic update mechanism yet, but it's planned for the future. To update a store-installed plugin, remove it and reinstall from the store. Nuclear always fetches the latest GitHub release, so reinstalling picks up any new version the developer has published.
47+
Nuclear checks for updates when it starts. If a newer version is available in the registry and auto-update is enabled (the default), the update is downloaded and installed automatically. The old version's files remain on disk but the new version takes over.
48+
49+
To disable auto-update, go to Settings and turn off "Auto-update plugins" under the Plugins section.
50+
51+
Dev plugins are never auto-updated. To update a dev plugin, edit the source and click reload.
4852

4953
## Where plugins are stored
5054

packages/docs/plugins/publishing.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ The `nuclear` field holds Nuclear-specific config:
3030
{
3131
"nuclear": {
3232
"displayName": "My Plugin",
33-
"category": "metadata",
33+
"categories": ["metadata"],
3434
"icon": {
3535
"type": "link",
3636
"link": "https://example.com/icon.png"
@@ -43,12 +43,12 @@ The `nuclear` field holds Nuclear-specific config:
4343
| Property | Required | Description |
4444
|----------|----------|-------------|
4545
| `displayName` | no | Human-readable name shown in the UI. Falls back to `name`. |
46-
| `category` | no | One of: `streaming`, `metadata`, `lyrics`, `scrobbling`, `dashboard`, `other`. Required for registry submission. |
46+
| `categories` | no | Array of categories. Valid values: `streaming`, `metadata`, `lyrics`, `scrobbling`, `dashboard`, `playlists`, `discovery`, `other`. Required for registry submission. |
4747
| `icon` | no | Plugin icon. Only `{"type": "link", "link": "url"}` is supported. |
4848
| `permissions` | no | Informational list. No permissions are enforced yet; this is for future use. |
4949

5050
{% hint style="info" %}
51-
Categories are convenience tags for filtering in the store. Pick the one that best describes your plugin's primary function.
51+
A plugin can belong to multiple categories. Pick all that apply based on the provider types your plugin registers.
5252
{% endhint %}
5353

5454
### Full example
@@ -63,7 +63,7 @@ Categories are convenience tags for filtering in the store. Pick the one that be
6363
"main": "dist/index.js",
6464
"nuclear": {
6565
"displayName": "Discogs",
66-
"category": "metadata"
66+
"categories": ["metadata"]
6767
}
6868
}
6969
```
@@ -113,7 +113,10 @@ Set up CI to build and create the release automatically. Manual zip creation is
113113
"author": "nukeop",
114114
"repo": "NuclearPlayer/nuclear-plugin-discogs",
115115
"category": "metadata",
116+
"categories": ["metadata"],
116117
"tags": ["discogs", "metadata"],
118+
"version": "1.0.0",
119+
"downloadUrl": "https://github.com/NuclearPlayer/nuclear-plugin-discogs/releases/download/v1.0.0/plugin.zip",
117120
"addedAt": "2026-01-25T00:00:00Z"
118121
}
119122
```
@@ -130,7 +133,10 @@ Set up CI to build and create the release automatically. Manual zip creation is
130133
| `author` | yes | 1-64 chars. |
131134
| `repo` | yes | `owner/repo-name` format. |
132135
| `category` | yes | Must match your `package.json` `nuclear.category`. |
136+
| `categories` | yes | Array of categories matching your plugin's provider types. Same valid values as `nuclear.categories` in package.json. |
133137
| `tags` | no | Up to 10 tags, lowercase with hyphens, unique. |
138+
| `version` | no | Latest released semver version. Populated automatically by CI. |
139+
| `downloadUrl` | no | Direct URL to the latest `plugin.zip`. Populated automatically by CI. |
134140
| `addedAt` | yes | ISO 8601 datetime (e.g., `2026-01-25T00:00:00Z`). |
135141

136142
---
@@ -142,7 +148,7 @@ You don't need to update the registry to release new versions. Create a new GitH
142148
Only submit a registry PR if you need to change the plugin's metadata (description, category, tags, etc.).
143149

144150
{% hint style="info" %}
145-
There is no automatic update push to users who already have your plugin installed. They need to remove and reinstall to get the latest version.
151+
Nuclear checks for plugin updates on startup. If auto-update is enabled (it is by default), installed store plugins are automatically updated to the latest version. Users can disable this in Settings under Plugins.
146152
{% endhint %}
147153

148154
## Examples

packages/i18n/src/locales/en_US.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@
219219
"installed": {
220220
"empty-state": "No plugins installed",
221221
"empty-state-description": "Browse the plugin store to find streaming, metadata, and other plugins.",
222-
"empty-state-action": "Go to store"
222+
"empty-state-action": "Go to store",
223+
"update-available": "Update available",
224+
"by": "by"
223225
},
224226
"store": {
225227
"searchPlaceholder": "Search plugins...",
@@ -233,6 +235,9 @@
233235
"metadata": "Metadata",
234236
"lyrics": "Lyrics",
235237
"scrobbling": "Scrobbling",
238+
"dashboard": "Dashboard",
239+
"playlists": "Playlists",
240+
"discovery": "Discovery",
236241
"other": "Other"
237242
},
238243
"error": {
@@ -270,7 +275,11 @@
270275
}
271276
},
272277
"plugins": {
273-
"title": "Plugins"
278+
"title": "Plugins",
279+
"auto-update": {
280+
"title": "Auto-update plugins",
281+
"description": "Automatically download and install new versions of store plugins on startup"
282+
}
274283
},
275284
"themes": {
276285
"title": "Themes"

packages/player/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"react-i18next": "^16.0.0",
5454
"react-icons": "^5.6.0",
5555
"react-inspector": "9.0.0",
56+
"semver": "^7.7.4",
5657
"sonner": "^2.0.7",
5758
"uuid": "^13.0.0",
5859
"zod": "^4.0.0",
@@ -69,6 +70,7 @@
6970
"@types/lodash-es": "^4.17.12",
7071
"@types/react": "^18.3.23",
7172
"@types/react-dom": "^18.3.7",
73+
"@types/semver": "^7.7.1",
7274
"@types/uuid": "^11.0.0",
7375
"@typescript-eslint/eslint-plugin": "^8.57.0",
7476
"@typescript-eslint/parser": "^8.57.0",

packages/player/src/App.hydration.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { usePluginStore } from './stores/pluginStore';
55
import { useStartupStore } from './stores/startupStore';
66
import { PluginFsMock } from './test/mocks/plugin-fs';
77
import { resetInMemoryTauriStore } from './test/utils/inMemoryTauriStore';
8-
import { seedRegistryEntry } from './test/utils/seedPluginRegistry';
8+
import { seedRegistryEntry } from './test/utils/seedPlugins';
99
import { createPluginFolder } from './test/utils/testPluginFolder';
1010
import { PluginsWrapper } from './views/Plugins/Plugins.test-wrapper';
1111

packages/player/src/apis/pluginMarketplaceApi.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,30 @@ import { z } from 'zod';
22

33
import { ApiClient } from './ApiClient';
44

5+
const PluginCategorySchema = z.enum([
6+
'streaming',
7+
'metadata',
8+
'lyrics',
9+
'scrobbling',
10+
'dashboard',
11+
'playlists',
12+
'discovery',
13+
'other',
14+
]);
15+
516
const MarketplacePluginSchema = z.object({
617
id: z.string().min(1),
718
name: z.string().min(1),
819
description: z.string(),
920
author: z.string().min(1),
1021
repo: z.string().regex(/^[^/]+\/[^/]+$/),
11-
category: z.enum([
12-
'streaming',
13-
'metadata',
14-
'lyrics',
15-
'scrobbling',
16-
'dashboard',
17-
'other',
18-
]),
22+
// TODO: Remove category after registry migration to categories
23+
category: PluginCategorySchema.optional(),
24+
categories: z.array(PluginCategorySchema).optional(),
1925
tags: z.array(z.string()).optional(),
20-
addedAt: z.string().datetime(),
26+
version: z.string().min(1).optional(),
27+
downloadUrl: z.url().optional(),
28+
addedAt: z.iso.datetime(),
2129
});
2230

2331
const RegistrySchema = z.object({

packages/player/src/services/coreSettings.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,15 @@ export const CORE_SETTINGS: SettingDefinition[] = [
208208
default: false,
209209
widget: { type: 'toggle' },
210210
},
211+
{
212+
id: 'plugins.autoUpdate',
213+
title: 'preferences.plugins.auto-update.title',
214+
description: 'preferences.plugins.auto-update.description',
215+
category: 'plugins',
216+
kind: 'boolean',
217+
default: true,
218+
widget: { type: 'toggle' },
219+
},
211220
{
212221
id: 'integrations.mcp.enabled',
213222
title: 'preferences.integrations.mcp.enabled.title',

packages/player/src/services/plugins/PluginLoader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class PluginLoader {
5959
description: manifest.description,
6060
author: manifest.author,
6161
category: manifest.nuclear?.category,
62+
categories: manifest.nuclear?.categories ?? [],
6263
icon: manifest.nuclear?.icon,
6364
permissions: manifest.nuclear?.permissions || [],
6465
};

0 commit comments

Comments
 (0)