@stolyarovmn/dsh-ui-registry-aggregator 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -52
- package/client.js +371 -21
- package/package.json +1 -1
- package/source-core.js +86 -10
package/README.md
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
# Registry Aggregator for DeepSeek Harness 0.2.x
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Registry Aggregator adds federated plugin discovery and update discovery around the native DeepSeek Harness Plugin Manager.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
This is the clean DSH `0.2.0` implementation line. It does **not** carry DSH `0.1.x` compatibility code.
|
|
6
|
+
|
|
7
|
+
Current stable version: `0.5.2`
|
|
8
|
+
Current validation baseline: DSH `v0.2.0-rc.2`
|
|
9
|
+
Declared DSH compatibility: `>=0.2.0-rc.2 <0.3.0`
|
|
10
|
+
|
|
11
|
+
Compatibility badges are evaluated against the DSH version that is actually running rather than a hardcoded RC value.
|
|
12
|
+
|
|
13
|
+
## Screenshot
|
|
14
|
+
|
|
15
|
+
Real Harness UI, manually verified on DSH `0.2.0-rc.2`:
|
|
16
|
+
|
|
17
|
+

|
|
7
18
|
|
|
8
19
|
## Install from npm
|
|
9
20
|
|
|
@@ -12,62 +23,84 @@ $DSH_VERSION = "0.2.0-rc.2"
|
|
|
12
23
|
$PROFILE = "dsh-020-test"
|
|
13
24
|
$env:DSH_HOME = "$env:USERPROFILE\.dsh-020-test"
|
|
14
25
|
|
|
15
|
-
pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" plugin --profile $PROFILE add "@stolyarovmn/dsh-ui-registry-aggregator@0.5.
|
|
26
|
+
pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" plugin --profile $PROFILE add "@stolyarovmn/dsh-ui-registry-aggregator@0.5.2"
|
|
16
27
|
pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" --profile $PROFILE
|
|
17
28
|
```
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
## What it adds
|
|
31
|
+
|
|
32
|
+
The native Plugin Manager remains responsible for installation, enable/disable, uninstall, and bundle lifecycle. Registry Aggregator adds discovery, compatibility evidence, metadata, and update orchestration.
|
|
33
|
+
|
|
34
|
+
### Main Plugins page
|
|
35
|
+
|
|
36
|
+
On DSH `0.2.0-rc.2`, Registry Aggregator extends the main Plugins list with:
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
- `Installed | Sources | Browse | Updates` navigation using the native Installed heading/count visual language;
|
|
39
|
+
- the original native Installed cards as the **Installed** view;
|
|
40
|
+
- **Sources**, **Browse**, and **Updates** directly on the main Plugins page;
|
|
41
|
+
- a numeric update count beside **Updates**;
|
|
42
|
+
- `Update → vX.Y.Z` directly on an installed package when a newer eligible version is available;
|
|
43
|
+
- **Update all** for eligible updates, processed sequentially with Registry Aggregator updating itself last;
|
|
44
|
+
- automatic teardown of the main-page surface when the Plugin Manager opens a bundle/detail page.
|
|
22
45
|
|
|
23
|
-
The
|
|
46
|
+
The supported `plugins.bundle.config` detail view remains available as a fallback inside **Installed → Registry Aggregator**.
|
|
24
47
|
|
|
25
|
-
|
|
48
|
+
> **DSH 0.2.0-rc.2 integration note:** stock rc.2 does not expose a list-level Plugin Manager slot. The main-page integration is therefore an explicit rc.2 compatibility bridge. It waits until the native Plugin Manager inventory has finished loading, does not use a `MutationObserver`, and removes itself on detail pages. When DSH exposes an appropriate list-level slot, this bridge should be replaced by the supported slot API.
|
|
49
|
+
|
|
50
|
+
### Sources
|
|
26
51
|
|
|
27
|
-
- native `plugins.bundle.config` integration;
|
|
28
|
-
- `Sources | Browse | Updates` navigation;
|
|
29
|
-
- live source configuration through `ctx.configForms.get('registry-aggregator')`; the bundle slot is presentation-only because DSH does not guarantee a single `form` for bundle-wide pages;
|
|
30
52
|
- built-in npm and GitHub sources;
|
|
31
53
|
- custom JSON and corporate catalog sources;
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
- compatibility
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
54
|
+
- enable/disable, add, remove, refresh, copy-address, package/repository count, health, and error state;
|
|
55
|
+
- live configuration through `ctx.configForms.get('registry-aggregator')`;
|
|
56
|
+
- authenticated Host RPC through the DSH Connection service;
|
|
57
|
+
- bounded responses, request timeouts, redirect validation, DNS pinning, and private-network blocking.
|
|
58
|
+
|
|
59
|
+
### Browse
|
|
60
|
+
|
|
61
|
+
- federated search across enabled npm, GitHub, custom JSON, and corporate sources;
|
|
62
|
+
- popular/default results when the query is empty;
|
|
63
|
+
- compact source, release, freshness, tag, compatibility, and page-size filters;
|
|
64
|
+
- combinable sorting by relevance, stars, downloads, freshness, and name;
|
|
65
|
+
- multi-criterion percentile-normalized composite ranking;
|
|
66
|
+
- 20 / 50 / 100 pagination;
|
|
67
|
+
- explicit package versions;
|
|
68
|
+
- GitHub stars and release/repository freshness;
|
|
69
|
+
- npm `30d | total` download evidence resolved lazily for visible npm rows;
|
|
70
|
+
- package artwork from the DSH manifest icon contract, with safe repository-local README artwork fallback for npm packages that expose GitHub metadata;
|
|
71
|
+
- `Compatible`, `Incompatible`, and `Not verified` evidence against the actual running DSH version;
|
|
72
|
+
- compatibility filtering;
|
|
73
|
+
- icon-only one-click installation through native `remote.pluginManager.inspect()` → `installBundle()`;
|
|
74
|
+
- known-incompatible packages are disabled before installation is attempted.
|
|
75
|
+
|
|
76
|
+
### Updates
|
|
77
|
+
|
|
78
|
+
- update discovery from native installed bundles plus latest npm manifests;
|
|
79
|
+
- per-package update through the native Plugin Manager;
|
|
80
|
+
- package Update uses a two-arrow glyph while content/list Refresh keeps the single-arrow glyph;
|
|
81
|
+
- native installed artwork is reused when available;
|
|
82
|
+
- sequential **Update all**;
|
|
83
|
+
- explicitly incompatible update targets remain disabled;
|
|
84
|
+
- Registry Aggregator updates itself last so it cannot interrupt the remaining queue.
|
|
60
85
|
|
|
61
86
|
## Architecture
|
|
62
87
|
|
|
63
88
|
```text
|
|
64
89
|
DSH Plugins
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
├─ Installed | Sources | Browse | Updates
|
|
91
|
+
│ ├─ Installed
|
|
92
|
+
│ │ ├─ native DSH package cards
|
|
93
|
+
│ │ └─ Update → vX.Y.Z
|
|
94
|
+
│ ├─ Sources
|
|
95
|
+
│ ├─ Browse
|
|
96
|
+
│ └─ Updates
|
|
97
|
+
│ └─ Update all
|
|
98
|
+
│
|
|
99
|
+
└─ Installed → Registry Aggregator
|
|
100
|
+
└─ plugins.bundle.config
|
|
101
|
+
├─ Sources
|
|
102
|
+
├─ Browse
|
|
103
|
+
└─ Updates
|
|
71
104
|
|
|
72
105
|
Sources UI
|
|
73
106
|
├─ ctx.configForms.get('registry-aggregator')
|
|
@@ -81,19 +114,33 @@ Sources UI
|
|
|
81
114
|
├─ custom-json
|
|
82
115
|
└─ corporate
|
|
83
116
|
|
|
84
|
-
|
|
117
|
+
Plugin lifecycle
|
|
85
118
|
└─ native DSH remote.pluginManager
|
|
86
119
|
├─ inspect(spec)
|
|
87
120
|
├─ installBundle(spec)
|
|
88
121
|
└─ listBundles() / plugin-manager/changed
|
|
89
|
-
|
|
90
|
-
Enable / disable / uninstall
|
|
91
|
-
└─ native DSH Plugin Manager
|
|
92
122
|
```
|
|
93
123
|
|
|
124
|
+
## Known limitations
|
|
125
|
+
|
|
126
|
+
- build-script approval remains in the native **Add plugin** dialog;
|
|
127
|
+
- **Update all** cancellation is not implemented; eligible updates run sequentially once started;
|
|
128
|
+
- automatic update discovery does not yet cover GitHub-only installed dependencies without npm package identity;
|
|
129
|
+
- the main Plugins-list surface is a version-pinned DSH `0.2.0-rc.2` compatibility bridge because rc.2 has no supported list-level Plugin Manager slot.
|
|
130
|
+
|
|
131
|
+
## Validation
|
|
132
|
+
|
|
133
|
+
`0.5.2` was promoted from the manually verified `0.5.2-rc.6` candidate after:
|
|
134
|
+
|
|
135
|
+
- JavaScript syntax checks;
|
|
136
|
+
- repository unit/client contract tests;
|
|
137
|
+
- `npm pack --dry-run` validation;
|
|
138
|
+
- isolated install smoke test against DSH `0.2.0-rc.2`;
|
|
139
|
+
- real Harness UI verification of the main Plugins-page Browse integration shown above.
|
|
140
|
+
|
|
94
141
|
## Test from GitHub
|
|
95
142
|
|
|
96
|
-
|
|
143
|
+
For unreleased development builds, use a commit SHA instead of publishing a test package:
|
|
97
144
|
|
|
98
145
|
```powershell
|
|
99
146
|
$DSH_VERSION = "0.2.0-rc.2"
|
|
@@ -105,8 +152,6 @@ pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" plugin --profile $PROFILE add "github:S
|
|
|
105
152
|
pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" --profile $PROFILE
|
|
106
153
|
```
|
|
107
154
|
|
|
108
|
-
Then open **Plugins → Installed → Registry Aggregator** and verify **Sources**, **Browse**, installation, compatibility filtering, download statistics, and **Updates** in the real Harness UI.
|
|
109
|
-
|
|
110
155
|
## Historical lines
|
|
111
156
|
|
|
112
157
|
- `dsh-0.1.7` — stable `v0.4.17` line for DSH `0.1.7-rc.2`.
|
package/client.js
CHANGED
|
@@ -86,6 +86,9 @@ window.__ModuleLoader__.load({
|
|
|
86
86
|
updateNone: 'Installed plugins are up to date',
|
|
87
87
|
updateNoneBody: 'No newer package versions were found in the connected registries.',
|
|
88
88
|
updateRefresh: 'Check for updates',
|
|
89
|
+
updatesAvailable: 'Updates: {count} available',
|
|
90
|
+
updateAll: 'Update all',
|
|
91
|
+
updatingAll: 'Updating all…',
|
|
89
92
|
downloads30d: 'npm downloads in the last 30 days',
|
|
90
93
|
downloadsTotal: 'npm total downloads',
|
|
91
94
|
filterSource: 'Source',
|
|
@@ -198,6 +201,9 @@ window.__ModuleLoader__.load({
|
|
|
198
201
|
updateNone: '已安装插件均为最新版本',
|
|
199
202
|
updateNoneBody: '在已连接的注册表中没有发现更高版本。',
|
|
200
203
|
updateRefresh: '检查更新',
|
|
204
|
+
updatesAvailable: '更新:{count} 个可用',
|
|
205
|
+
updateAll: '全部更新',
|
|
206
|
+
updatingAll: '正在全部更新…',
|
|
201
207
|
downloads30d: 'npm 最近 30 天下载量',
|
|
202
208
|
downloadsTotal: 'npm 总下载量',
|
|
203
209
|
filterSource: '来源',
|
|
@@ -241,6 +247,8 @@ window.__ModuleLoader__.load({
|
|
|
241
247
|
'.ra-root{box-sizing:border-box;min-width:0;max-width:100%;overflow-x:hidden;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;padding:2px 0 8px}',
|
|
242
248
|
'.ra-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));width:min(100%,440px);padding:4px;margin:12px 0 24px;border-radius:12px;background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-2))}',
|
|
243
249
|
'.ra-tab{height:34px;padding:0 14px;border:.5px solid transparent;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer}',
|
|
250
|
+
'.ra-tab-label{display:inline-flex;align-items:center;justify-content:center;gap:6px}',
|
|
251
|
+
'.ra-update-badge{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground);font-size:10px;line-height:16px;font-weight:700}',
|
|
244
252
|
'.ra-tab:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
245
253
|
'.ra-tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}',
|
|
246
254
|
'.ra-tab[aria-selected=true]{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
@@ -254,6 +262,7 @@ window.__ModuleLoader__.load({
|
|
|
254
262
|
'.ra-button:focus-visible,.ra-input:focus-visible,.ra-select:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}',
|
|
255
263
|
'.ra-button:disabled{opacity:.5;cursor:not-allowed}',
|
|
256
264
|
'.ra-button-primary{background:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground)}',
|
|
265
|
+
'.ra-update-all{display:inline-flex;align-items:center;justify-content:center;gap:6px}',
|
|
257
266
|
'.ra-panel{border:.5px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}',
|
|
258
267
|
'.ra-add-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 13px}',
|
|
259
268
|
'.ra-add-form{display:grid;grid-template-columns:minmax(160px,1fr) minmax(130px,180px) minmax(220px,1.2fr) auto;gap:10px;padding:12px 13px;border-top:.5px solid var(--dsw-alias-border-l4);align-items:end}',
|
|
@@ -615,6 +624,15 @@ window.__ModuleLoader__.load({
|
|
|
615
624
|
] })
|
|
616
625
|
}
|
|
617
626
|
|
|
627
|
+
function IconUpdate({ size = 16 }) {
|
|
628
|
+
return h(SvgIcon, { size, children: [
|
|
629
|
+
h('path', { d: 'M13.5 5.5C12.6 3.2 10.5 1.75 8 1.75C5.9 1.75 4.05 2.75 2.9 4.3L1.75 5.75', stroke: 'currentColor' }),
|
|
630
|
+
h('path', { d: 'M1.75 2.75V5.75H4.75', stroke: 'currentColor' }),
|
|
631
|
+
h('path', { d: 'M2.5 10.5C3.4 12.8 5.5 14.25 8 14.25C10.1 14.25 11.95 13.25 13.1 11.7L14.25 10.25', stroke: 'currentColor' }),
|
|
632
|
+
h('path', { d: 'M14.25 13.25V10.25H11.25', stroke: 'currentColor' }),
|
|
633
|
+
] })
|
|
634
|
+
}
|
|
635
|
+
|
|
618
636
|
function IconPlus({ size = 16 }) {
|
|
619
637
|
return h(SvgIcon, { size, children: [
|
|
620
638
|
h('path', { d: 'M8 2V14', stroke: 'currentColor' }),
|
|
@@ -1669,12 +1687,12 @@ window.__ModuleLoader__.load({
|
|
|
1669
1687
|
)
|
|
1670
1688
|
}
|
|
1671
1689
|
|
|
1672
|
-
function
|
|
1690
|
+
function useUpdateDiscovery(enabled = true) {
|
|
1673
1691
|
const [revision, setRevision] = React.useState(0)
|
|
1674
|
-
const [state, setState] = React.useState({ loading:
|
|
1675
|
-
const [operations, setOperations] = React.useState({})
|
|
1692
|
+
const [state, setState] = React.useState({ loading: enabled, items: [], error: '' })
|
|
1676
1693
|
|
|
1677
1694
|
React.useEffect(() => {
|
|
1695
|
+
if (!enabled) return undefined
|
|
1678
1696
|
let active = true
|
|
1679
1697
|
const controller = new AbortController()
|
|
1680
1698
|
const load = async () => {
|
|
@@ -1716,36 +1734,76 @@ window.__ModuleLoader__.load({
|
|
|
1716
1734
|
controller.abort()
|
|
1717
1735
|
if (typeof dispose === 'function') dispose()
|
|
1718
1736
|
}
|
|
1719
|
-
}, [revision])
|
|
1737
|
+
}, [enabled, revision])
|
|
1738
|
+
|
|
1739
|
+
const refresh = React.useCallback(() => setRevision(value => value + 1), [])
|
|
1740
|
+
return { state, refresh }
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function UpdatesView({ t, discovery }) {
|
|
1744
|
+
const { state, refresh } = discovery
|
|
1745
|
+
const [operations, setOperations] = React.useState({})
|
|
1746
|
+
const [bulkUpdating, setBulkUpdating] = React.useState(false)
|
|
1720
1747
|
|
|
1721
|
-
const runUpdate = async item => {
|
|
1748
|
+
const runUpdate = async (item, refreshAfter = true) => {
|
|
1722
1749
|
const key = item.bundle.name
|
|
1723
1750
|
const current = operations[key]
|
|
1724
|
-
if (['starting', 'installing', 'applying'].includes(current?.phase)) return
|
|
1751
|
+
if (['starting', 'installing', 'applying'].includes(current?.phase)) return false
|
|
1725
1752
|
setOperations(value => ({ ...value, [key]: { phase: 'starting', error: '' } }))
|
|
1726
1753
|
try {
|
|
1727
1754
|
await updateInstalledPlugin(item.bundle, item.availableVersion, progress => {
|
|
1728
1755
|
setOperations(value => ({ ...value, [key]: { ...(value[key] ?? {}), ...progress, error: '' } }))
|
|
1729
1756
|
})
|
|
1730
1757
|
setOperations(value => ({ ...value, [key]: { phase: 'done', error: '' } }))
|
|
1731
|
-
|
|
1758
|
+
if (refreshAfter) refresh()
|
|
1759
|
+
return true
|
|
1732
1760
|
} catch (error) {
|
|
1733
1761
|
setOperations(value => ({ ...value, [key]: { phase: 'failed', error: String(error?.message ?? error) } }))
|
|
1762
|
+
return false
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const runUpdateAll = async () => {
|
|
1767
|
+
if (bulkUpdating || state.loading) return
|
|
1768
|
+
const queue = state.items
|
|
1769
|
+
.filter(item => item.metadata?.compatibility !== 'unsupported')
|
|
1770
|
+
.sort((left, right) => Number(left.bundle.name === PACKAGE) - Number(right.bundle.name === PACKAGE))
|
|
1771
|
+
if (!queue.length) return
|
|
1772
|
+
setBulkUpdating(true)
|
|
1773
|
+
try {
|
|
1774
|
+
for (const item of queue) await runUpdate(item, false)
|
|
1775
|
+
} finally {
|
|
1776
|
+
setBulkUpdating(false)
|
|
1777
|
+
refresh()
|
|
1734
1778
|
}
|
|
1735
1779
|
}
|
|
1736
1780
|
|
|
1781
|
+
const updatableCount = state.items.filter(item => item.metadata?.compatibility !== 'unsupported').length
|
|
1782
|
+
|
|
1737
1783
|
return h('section', { className: 'ra-section', 'aria-labelledby': 'ra-updates-title' },
|
|
1738
1784
|
h('div', { className: 'ra-section-head' },
|
|
1739
1785
|
h('div', null,
|
|
1740
1786
|
h('h3', { id: 'ra-updates-title', className: 'ra-heading' }, t('updatesTitle')),
|
|
1741
1787
|
h('p', { className: 'ra-lead' }, t('updatesLead')),
|
|
1742
1788
|
),
|
|
1743
|
-
h(
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1789
|
+
h('div', { className: 'ra-actions' },
|
|
1790
|
+
state.items.length > 1 ? h('button', {
|
|
1791
|
+
type: 'button',
|
|
1792
|
+
className: 'ra-button ra-update-all',
|
|
1793
|
+
title: bulkUpdating ? t('updatingAll') : t('updateAll'),
|
|
1794
|
+
disabled: state.loading || bulkUpdating || updatableCount === 0,
|
|
1795
|
+
onClick: () => { void runUpdateAll() },
|
|
1796
|
+
},
|
|
1797
|
+
h(IconUpdate, { size: 14 }),
|
|
1798
|
+
h('span', null, bulkUpdating ? t('updatingAll') : t('updateAll')),
|
|
1799
|
+
) : null,
|
|
1800
|
+
h(IconButton, {
|
|
1801
|
+
label: t('updateRefresh'),
|
|
1802
|
+
disabled: state.loading || bulkUpdating,
|
|
1803
|
+
onClick: refresh,
|
|
1804
|
+
icon: h(IconRefresh, { size: 16, className: state.loading ? 'ra-spin' : undefined }),
|
|
1805
|
+
}),
|
|
1806
|
+
),
|
|
1749
1807
|
),
|
|
1750
1808
|
state.loading
|
|
1751
1809
|
? h('div', { className: 'ra-plugin-meta', role: 'status' }, t('updateChecking'))
|
|
@@ -1775,7 +1833,7 @@ window.__ModuleLoader__.load({
|
|
|
1775
1833
|
? t('updateRetry')
|
|
1776
1834
|
: format(t, 'updateAction', { version: item.availableVersion })
|
|
1777
1835
|
return h('li', { key, className: 'ra-update-row' },
|
|
1778
|
-
h('span', { className: 'ra-plugin-icon', 'aria-hidden': true }, h(
|
|
1836
|
+
h('span', { className: 'ra-plugin-icon', 'aria-hidden': true }, h(PluginArtwork, { src: item.bundle?.meta?.icon })),
|
|
1779
1837
|
h('div', { className: 'ra-update-main' },
|
|
1780
1838
|
h('div', { className: 'ra-update-title' },
|
|
1781
1839
|
h('span', null, key),
|
|
@@ -1790,13 +1848,13 @@ window.__ModuleLoader__.load({
|
|
|
1790
1848
|
h(IconButton, {
|
|
1791
1849
|
label,
|
|
1792
1850
|
state: done ? 'done' : failed ? 'error' : undefined,
|
|
1793
|
-
disabled: busy || done || incompatible,
|
|
1851
|
+
disabled: bulkUpdating || busy || done || incompatible,
|
|
1794
1852
|
onClick: () => { void runUpdate(item) },
|
|
1795
1853
|
icon: done
|
|
1796
1854
|
? h(IconCheck, { size: 16 })
|
|
1797
1855
|
: busy
|
|
1798
1856
|
? h(IconRefresh, { size: 16, className: 'ra-spin' })
|
|
1799
|
-
: h(
|
|
1857
|
+
: h(IconUpdate, { size: 16 }),
|
|
1800
1858
|
}),
|
|
1801
1859
|
)
|
|
1802
1860
|
}),
|
|
@@ -1806,6 +1864,7 @@ window.__ModuleLoader__.load({
|
|
|
1806
1864
|
|
|
1807
1865
|
function RegistryAggregator({ t, view }) {
|
|
1808
1866
|
const [tab, setTab] = React.useState('sources')
|
|
1867
|
+
const updateDiscovery = useUpdateDiscovery(view === 'page')
|
|
1809
1868
|
const formState = React.useSyncExternalStore(
|
|
1810
1869
|
listener => sourceConfigForm.subscribe(listener),
|
|
1811
1870
|
() => sourceConfigForm.getSnapshot(),
|
|
@@ -1816,10 +1875,15 @@ window.__ModuleLoader__.load({
|
|
|
1816
1875
|
mutate: (operations, expectedRevision) => sourceConfigForm.mutate(operations, expectedRevision),
|
|
1817
1876
|
}), [formState])
|
|
1818
1877
|
if (view !== 'page') return null
|
|
1878
|
+
const updateCount = updateDiscovery.state.items.length
|
|
1879
|
+
const updateLabel = h('span', { className: 'ra-tab-label' },
|
|
1880
|
+
t('updates'),
|
|
1881
|
+
updateCount > 0 ? h('span', { className: 'ra-update-badge', 'aria-hidden': true }, updateCount > 99 ? '99+' : String(updateCount)) : null,
|
|
1882
|
+
)
|
|
1819
1883
|
const tabs = [
|
|
1820
|
-
['sources', t('sources')],
|
|
1821
|
-
['browse', t('browse')],
|
|
1822
|
-
['updates', t('updates')],
|
|
1884
|
+
['sources', t('sources'), t('sources')],
|
|
1885
|
+
['browse', t('browse'), t('browse')],
|
|
1886
|
+
['updates', updateLabel, updateCount > 0 ? format(t, 'updatesAvailable', { count: updateCount }) : t('updates')],
|
|
1823
1887
|
]
|
|
1824
1888
|
const selectRelative = delta => {
|
|
1825
1889
|
const index = tabs.findIndex(item => item[0] === tab)
|
|
@@ -1829,18 +1893,19 @@ window.__ModuleLoader__.load({
|
|
|
1829
1893
|
const body = tab === 'browse'
|
|
1830
1894
|
? h(BrowseView, { t })
|
|
1831
1895
|
: tab === 'updates'
|
|
1832
|
-
? h(UpdatesView, { t })
|
|
1896
|
+
? h(UpdatesView, { t, discovery: updateDiscovery })
|
|
1833
1897
|
: h(SourcesView, { t, form })
|
|
1834
1898
|
|
|
1835
1899
|
return h('div', { className: 'ra-root' },
|
|
1836
1900
|
h('style', null, css),
|
|
1837
1901
|
h('div', { className: 'ra-tabs', role: 'tablist', 'aria-label': 'Registry Aggregator' },
|
|
1838
|
-
...tabs.map(([id, label]) => h('button', {
|
|
1902
|
+
...tabs.map(([id, label, ariaLabel]) => h('button', {
|
|
1839
1903
|
key: id,
|
|
1840
1904
|
type: 'button',
|
|
1841
1905
|
role: 'tab',
|
|
1842
1906
|
className: 'ra-tab',
|
|
1843
1907
|
'aria-selected': tab === id,
|
|
1908
|
+
'aria-label': ariaLabel,
|
|
1844
1909
|
tabIndex: tab === id ? 0 : -1,
|
|
1845
1910
|
onClick: () => setTab(id),
|
|
1846
1911
|
onKeyDown: event => {
|
|
@@ -1858,6 +1923,290 @@ window.__ModuleLoader__.load({
|
|
|
1858
1923
|
)
|
|
1859
1924
|
}
|
|
1860
1925
|
|
|
1926
|
+
|
|
1927
|
+
// Temporary DSH 0.2.0-rc.2 compatibility surface.
|
|
1928
|
+
// Stock rc.2 has no supported list-level Plugin Manager slot, so this bridge
|
|
1929
|
+
// is deliberately version-pinned and mounts only after the native Installed
|
|
1930
|
+
// group exists. It never observes React DOM mutations and removes itself as
|
|
1931
|
+
// soon as the Plugin Manager navigates to a detail page.
|
|
1932
|
+
function PluginsPageBridgeSurface({ t, installed, cards }) {
|
|
1933
|
+
const [tab, setTab] = React.useState('installed')
|
|
1934
|
+
const discovery = useUpdateDiscovery(true)
|
|
1935
|
+
const [operations, setOperations] = React.useState({})
|
|
1936
|
+
const [bulkUpdating, setBulkUpdating] = React.useState(false)
|
|
1937
|
+
const formState = React.useSyncExternalStore(
|
|
1938
|
+
listener => sourceConfigForm.subscribe(listener),
|
|
1939
|
+
() => sourceConfigForm.getSnapshot(),
|
|
1940
|
+
() => sourceConfigForm.getSnapshot(),
|
|
1941
|
+
)
|
|
1942
|
+
const form = React.useMemo(() => ({
|
|
1943
|
+
state: formState,
|
|
1944
|
+
mutate: (operations, expectedRevision) => sourceConfigForm.mutate(operations, expectedRevision),
|
|
1945
|
+
}), [formState])
|
|
1946
|
+
|
|
1947
|
+
const installedCount = installed.querySelector('[data-plugin-count]')?.textContent
|
|
1948
|
+
?? String(installed.querySelectorAll('[data-plugin-package]').length)
|
|
1949
|
+
const updateCount = discovery.state.items.length
|
|
1950
|
+
const updatableCount = discovery.state.items.filter(item => item.metadata?.compatibility !== 'unsupported').length
|
|
1951
|
+
|
|
1952
|
+
const runUpdate = async (item, refreshAfter = true) => {
|
|
1953
|
+
const key = item.bundle.name
|
|
1954
|
+
const current = operations[key]
|
|
1955
|
+
if (['starting', 'installing', 'applying'].includes(current?.phase)) return false
|
|
1956
|
+
setOperations(value => ({ ...value, [key]: { phase: 'starting', error: '' } }))
|
|
1957
|
+
try {
|
|
1958
|
+
await updateInstalledPlugin(item.bundle, item.availableVersion, progress => {
|
|
1959
|
+
setOperations(value => ({ ...value, [key]: { ...(value[key] ?? {}), ...progress, error: '' } }))
|
|
1960
|
+
})
|
|
1961
|
+
setOperations(value => ({ ...value, [key]: { phase: 'done', error: '' } }))
|
|
1962
|
+
if (refreshAfter) discovery.refresh()
|
|
1963
|
+
return true
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
setOperations(value => ({ ...value, [key]: { phase: 'failed', error: String(error?.message ?? error) } }))
|
|
1966
|
+
return false
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
const runUpdateAll = async () => {
|
|
1971
|
+
if (bulkUpdating || discovery.state.loading) return
|
|
1972
|
+
const queue = discovery.state.items
|
|
1973
|
+
.filter(item => item.metadata?.compatibility !== 'unsupported')
|
|
1974
|
+
.sort((left, right) => Number(left.bundle.name === PACKAGE) - Number(right.bundle.name === PACKAGE))
|
|
1975
|
+
if (!queue.length) return
|
|
1976
|
+
setBulkUpdating(true)
|
|
1977
|
+
try {
|
|
1978
|
+
for (const item of queue) await runUpdate(item, false)
|
|
1979
|
+
} finally {
|
|
1980
|
+
setBulkUpdating(false)
|
|
1981
|
+
discovery.refresh()
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
React.useEffect(() => {
|
|
1986
|
+
cards.style.display = tab === 'installed' ? '' : 'none'
|
|
1987
|
+
return () => { cards.style.display = '' }
|
|
1988
|
+
}, [cards, tab])
|
|
1989
|
+
|
|
1990
|
+
const updateSignature = discovery.state.items
|
|
1991
|
+
.map(item => [item.bundle?.name, item.bundle?.version, item.availableVersion, item.metadata?.compatibility].join(':'))
|
|
1992
|
+
.join('|')
|
|
1993
|
+
const operationSignature = Object.entries(operations)
|
|
1994
|
+
.map(([key, value]) => [key, value?.phase, value?.error].join(':'))
|
|
1995
|
+
.join('|')
|
|
1996
|
+
|
|
1997
|
+
React.useEffect(() => {
|
|
1998
|
+
const updateByName = new Map(discovery.state.items.map(item => [item.bundle?.name, item]))
|
|
1999
|
+
const created = []
|
|
2000
|
+
for (const card of installed.querySelectorAll('[data-plugin-package]')) {
|
|
2001
|
+
const packageName = card.getAttribute('data-plugin-package')
|
|
2002
|
+
const item = updateByName.get(packageName)
|
|
2003
|
+
const old = card.querySelector('[data-ra-installed-update]')
|
|
2004
|
+
if (old) old.remove()
|
|
2005
|
+
if (!item?.availableVersion) continue
|
|
2006
|
+
|
|
2007
|
+
const toggle = card.querySelector('[role="switch"]')
|
|
2008
|
+
const actionHost = toggle?.parentElement
|
|
2009
|
+
if (!actionHost) continue
|
|
2010
|
+
|
|
2011
|
+
const operation = operations[packageName] ?? {}
|
|
2012
|
+
const busy = bulkUpdating || ['starting', 'installing', 'applying'].includes(operation.phase)
|
|
2013
|
+
const incompatible = item.metadata?.compatibility === 'unsupported'
|
|
2014
|
+
const button = document.createElement('button')
|
|
2015
|
+
button.type = 'button'
|
|
2016
|
+
button.className = 'ra-installed-update'
|
|
2017
|
+
button.setAttribute('data-ra-installed-update', '')
|
|
2018
|
+
button.setAttribute('data-state', operation.phase === 'failed' ? 'error' : busy ? 'busy' : 'ready')
|
|
2019
|
+
button.textContent = 'Update → v' + item.availableVersion
|
|
2020
|
+
button.title = operation.error || (incompatible
|
|
2021
|
+
? format(t, 'incompatibleTitle', { version: item.metadata?.runtimeVersion ?? '?' })
|
|
2022
|
+
: format(t, 'updateAction', { version: 'v' + item.availableVersion }))
|
|
2023
|
+
button.disabled = busy || incompatible
|
|
2024
|
+
button.addEventListener('click', event => {
|
|
2025
|
+
event.preventDefault()
|
|
2026
|
+
event.stopPropagation()
|
|
2027
|
+
void runUpdate(item)
|
|
2028
|
+
})
|
|
2029
|
+
actionHost.insertBefore(button, toggle)
|
|
2030
|
+
created.push(button)
|
|
2031
|
+
}
|
|
2032
|
+
return () => {
|
|
2033
|
+
for (const button of created) button.remove()
|
|
2034
|
+
}
|
|
2035
|
+
}, [installed, updateSignature, operationSignature, bulkUpdating])
|
|
2036
|
+
|
|
2037
|
+
const bridgeTabs = [
|
|
2038
|
+
['installed', t('installed'), installedCount],
|
|
2039
|
+
['sources', t('sources'), ''],
|
|
2040
|
+
['browse', t('browse'), ''],
|
|
2041
|
+
['updates', t('updates'), updateCount > 0 ? String(updateCount) : ''],
|
|
2042
|
+
]
|
|
2043
|
+
const selectRelative = delta => {
|
|
2044
|
+
const index = bridgeTabs.findIndex(item => item[0] === tab)
|
|
2045
|
+
setTab(bridgeTabs[(index + delta + bridgeTabs.length) % bridgeTabs.length][0])
|
|
2046
|
+
}
|
|
2047
|
+
const body = tab === 'sources'
|
|
2048
|
+
? h(SourcesView, { t, form })
|
|
2049
|
+
: tab === 'browse'
|
|
2050
|
+
? h(BrowseView, { t })
|
|
2051
|
+
: tab === 'updates'
|
|
2052
|
+
? h(UpdatesView, { t, discovery })
|
|
2053
|
+
: null
|
|
2054
|
+
|
|
2055
|
+
return h('div', { className: 'ra-native-surface' },
|
|
2056
|
+
h('div', { className: 'ra-native-nav-row' },
|
|
2057
|
+
h('div', { className: 'ra-native-tabs', role: 'tablist', 'aria-label': 'Plugin Manager views' },
|
|
2058
|
+
...bridgeTabs.flatMap(([id, label, count], index) => [
|
|
2059
|
+
index === 0 ? null : h('span', { key: id + '-separator', className: 'ra-native-separator', 'aria-hidden': true }, '|'),
|
|
2060
|
+
h('button', {
|
|
2061
|
+
key: id,
|
|
2062
|
+
type: 'button',
|
|
2063
|
+
role: 'tab',
|
|
2064
|
+
className: 'ra-native-tab',
|
|
2065
|
+
'aria-selected': tab === id,
|
|
2066
|
+
tabIndex: tab === id ? 0 : -1,
|
|
2067
|
+
onClick: () => setTab(id),
|
|
2068
|
+
onKeyDown: event => {
|
|
2069
|
+
if (event.key === 'ArrowRight') {
|
|
2070
|
+
event.preventDefault()
|
|
2071
|
+
selectRelative(1)
|
|
2072
|
+
} else if (event.key === 'ArrowLeft') {
|
|
2073
|
+
event.preventDefault()
|
|
2074
|
+
selectRelative(-1)
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
h('span', null, label),
|
|
2079
|
+
count ? h('span', { className: 'ra-native-count' }, count) : null,
|
|
2080
|
+
),
|
|
2081
|
+
].filter(Boolean)),
|
|
2082
|
+
),
|
|
2083
|
+
tab === 'installed' && updateCount > 0
|
|
2084
|
+
? h('button', {
|
|
2085
|
+
type: 'button',
|
|
2086
|
+
className: 'ra-native-update-all',
|
|
2087
|
+
disabled: discovery.state.loading || bulkUpdating || updatableCount === 0,
|
|
2088
|
+
onClick: () => { void runUpdateAll() },
|
|
2089
|
+
},
|
|
2090
|
+
h(IconUpdate, { size: 14 }),
|
|
2091
|
+
h('span', null, bulkUpdating ? t('updatingAll') : t('updateAll')),
|
|
2092
|
+
)
|
|
2093
|
+
: null,
|
|
2094
|
+
),
|
|
2095
|
+
body ? h('div', { className: 'ra-native-body' }, body) : null,
|
|
2096
|
+
)
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function installPluginsPagePollingBridge(ctx) {
|
|
2100
|
+
const doc = window.document
|
|
2101
|
+
if (!doc?.querySelector) return () => {}
|
|
2102
|
+
|
|
2103
|
+
const HOST_ATTR = 'data-registry-aggregator-main-surface'
|
|
2104
|
+
const bridgeCss = [
|
|
2105
|
+
'[' + HOST_ATTR + ']{box-sizing:border-box;min-width:0;width:100%}',
|
|
2106
|
+
'[' + HOST_ATTR + '] .ra-native-surface{min-width:0;width:100%}',
|
|
2107
|
+
'[' + HOST_ATTR + '] .ra-native-nav-row{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:22px}',
|
|
2108
|
+
'[' + HOST_ATTR + '] .ra-native-tabs{display:flex;align-items:baseline;min-width:0;flex-wrap:wrap}',
|
|
2109
|
+
'[' + HOST_ATTR + '] .ra-native-tab{display:inline-flex;align-items:baseline;gap:8px;margin:0;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:14px;line-height:22px;font-weight:500;cursor:pointer}',
|
|
2110
|
+
'[' + HOST_ATTR + '] .ra-native-tab[aria-selected=true]{color:var(--dsw-alias-label-primary)}',
|
|
2111
|
+
'[' + HOST_ATTR + '] .ra-native-tab:hover{color:var(--dsw-alias-label-primary)}',
|
|
2112
|
+
'[' + HOST_ATTR + '] .ra-native-tab:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px;border-radius:var(--dsw-radius-xs)}',
|
|
2113
|
+
'[' + HOST_ATTR + '] .ra-native-count{color:var(--dsw-alias-label-caption);font-weight:400;font-variant-numeric:tabular-nums}',
|
|
2114
|
+
'[' + HOST_ATTR + '] .ra-native-separator{padding:0 10px;color:var(--dsw-alias-label-caption);font-size:12px;line-height:22px}',
|
|
2115
|
+
'[' + HOST_ATTR + '] .ra-native-update-all{display:inline-flex;align-items:center;justify-content:center;gap:6px;flex:none;height:30px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}',
|
|
2116
|
+
'[' + HOST_ATTR + '] .ra-native-update-all:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
2117
|
+
'[' + HOST_ATTR + '] .ra-native-update-all:disabled{opacity:.5;cursor:default}',
|
|
2118
|
+
'[' + HOST_ATTR + '] .ra-native-body{padding-top:16px}',
|
|
2119
|
+
'.ra-installed-update{position:relative;z-index:2;display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 9px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:18px;white-space:nowrap;cursor:pointer}',
|
|
2120
|
+
'.ra-installed-update:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
2121
|
+
'.ra-installed-update:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}',
|
|
2122
|
+
'.ra-installed-update:disabled{opacity:.5;cursor:default}',
|
|
2123
|
+
'.ra-installed-update[data-state=error]{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 30%,transparent)}',
|
|
2124
|
+
].join('')
|
|
2125
|
+
let host
|
|
2126
|
+
let root
|
|
2127
|
+
let createRoot
|
|
2128
|
+
let installedGroup
|
|
2129
|
+
let installedHead
|
|
2130
|
+
let cardsList
|
|
2131
|
+
let headDisplay = ''
|
|
2132
|
+
let cardsDisplay = ''
|
|
2133
|
+
let disposed = false
|
|
2134
|
+
|
|
2135
|
+
const disposeRoot = () => {
|
|
2136
|
+
if (root) {
|
|
2137
|
+
try { root.unmount() } catch {}
|
|
2138
|
+
root = undefined
|
|
2139
|
+
}
|
|
2140
|
+
if (installedHead?.isConnected) installedHead.style.display = headDisplay
|
|
2141
|
+
if (cardsList?.isConnected) cardsList.style.display = cardsDisplay
|
|
2142
|
+
if (installedGroup?.querySelectorAll) {
|
|
2143
|
+
for (const button of installedGroup.querySelectorAll('[data-ra-installed-update]')) button.remove()
|
|
2144
|
+
}
|
|
2145
|
+
if (host?.isConnected) host.remove()
|
|
2146
|
+
host = undefined
|
|
2147
|
+
installedGroup = undefined
|
|
2148
|
+
installedHead = undefined
|
|
2149
|
+
cardsList = undefined
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
const reconcile = () => {
|
|
2153
|
+
if (disposed) return
|
|
2154
|
+
const panel = doc.querySelector('section[data-plugin-panel]')
|
|
2155
|
+
if (!panel) {
|
|
2156
|
+
disposeRoot()
|
|
2157
|
+
return
|
|
2158
|
+
}
|
|
2159
|
+
if (panel.querySelector('[data-plugin-loading]')) return
|
|
2160
|
+
|
|
2161
|
+
const installed = panel.querySelector('section[data-plugin-group="bundles"]')
|
|
2162
|
+
if (!installed) {
|
|
2163
|
+
// Detail pages intentionally have no Installed group. Removing the
|
|
2164
|
+
// bridge here prevents the duplicate UI shown above bundle details.
|
|
2165
|
+
disposeRoot()
|
|
2166
|
+
return
|
|
2167
|
+
}
|
|
2168
|
+
const head = installed.firstElementChild
|
|
2169
|
+
const cards = Array.from(installed.children).find(element => element.tagName === 'UL')
|
|
2170
|
+
if (!head || !cards) return
|
|
2171
|
+
if (host?.isConnected && installedGroup === installed && host.parentElement === installed && head.nextElementSibling === host) return
|
|
2172
|
+
|
|
2173
|
+
disposeRoot()
|
|
2174
|
+
installedGroup = installed
|
|
2175
|
+
installedHead = head
|
|
2176
|
+
cardsList = cards
|
|
2177
|
+
headDisplay = head.style.display
|
|
2178
|
+
cardsDisplay = cards.style.display
|
|
2179
|
+
head.style.display = 'none'
|
|
2180
|
+
|
|
2181
|
+
host = doc.createElement('div')
|
|
2182
|
+
host.setAttribute(HOST_ATTR, '')
|
|
2183
|
+
host.setAttribute('aria-label', 'Registry Aggregator')
|
|
2184
|
+
head.insertAdjacentElement('afterend', host)
|
|
2185
|
+
|
|
2186
|
+
try {
|
|
2187
|
+
if (!createRoot) ({ createRoot } = require('react-dom/client'))
|
|
2188
|
+
if (typeof createRoot !== 'function') throw new Error('react-dom/client createRoot is unavailable')
|
|
2189
|
+
root = createRoot(host)
|
|
2190
|
+
root.render(h(React.Fragment, null,
|
|
2191
|
+
h('style', null, css + '\n' + bridgeCss),
|
|
2192
|
+
h(PluginsPageBridgeSurface, { t: ctx.locale.bind(NS), installed, cards }),
|
|
2193
|
+
))
|
|
2194
|
+
} catch {
|
|
2195
|
+
disposeRoot()
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
const timer = window.setInterval(reconcile, 750)
|
|
2200
|
+
const initial = window.setTimeout(reconcile, 900)
|
|
2201
|
+
|
|
2202
|
+
return () => {
|
|
2203
|
+
disposed = true
|
|
2204
|
+
window.clearInterval(timer)
|
|
2205
|
+
window.clearTimeout(initial)
|
|
2206
|
+
disposeRoot()
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
1861
2210
|
return {
|
|
1862
2211
|
inject: ['slots', 'locale', 'connection', 'configForms', 'remote', 'remote.pluginManager'],
|
|
1863
2212
|
apply(ctx) {
|
|
@@ -1865,6 +2214,7 @@ window.__ModuleLoader__.load({
|
|
|
1865
2214
|
remote = ctx.remote
|
|
1866
2215
|
sourceConfigForm = ctx.configForms.get(HOST_ENTRY)
|
|
1867
2216
|
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'registry-aggregator: locale')
|
|
2217
|
+
ctx.effect(() => ctx.configForms.whileServed([HOST_ENTRY], () => installPluginsPagePollingBridge(ctx)), 'registry-aggregator: DSH 0.2.0-rc.2 Plugins page polling bridge')
|
|
1868
2218
|
ctx.effect(() => ctx.configForms.whileServed([HOST_ENTRY], () =>
|
|
1869
2219
|
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
1870
2220
|
name: 'plugins.bundle.config',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stolyarovmn/dsh-ui-registry-aggregator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Federated plugin discovery and update UI for DeepSeek Harness 0.2.x, integrated with the native Plugin Manager.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stolyarovmn",
|
package/source-core.js
CHANGED
|
@@ -505,6 +505,13 @@ function cleanUrl(value) {
|
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
function manifestRepositoryUrl(manifest) {
|
|
509
|
+
const repository = manifest?.repository
|
|
510
|
+
if (typeof repository === 'string') return cleanUrl(repository)
|
|
511
|
+
if (repository && typeof repository === 'object' && !Array.isArray(repository)) return cleanUrl(repository.url)
|
|
512
|
+
return undefined
|
|
513
|
+
}
|
|
514
|
+
|
|
508
515
|
function npmPlugin(entry, source) {
|
|
509
516
|
const pkg = entry?.package
|
|
510
517
|
const name = text(pkg?.name)
|
|
@@ -722,6 +729,41 @@ function safeIconPath(value) {
|
|
|
722
729
|
return { path: clean, mediaType }
|
|
723
730
|
}
|
|
724
731
|
|
|
732
|
+
function readmeIconPath(markdown) {
|
|
733
|
+
const source = String(markdown ?? '')
|
|
734
|
+
const candidates = []
|
|
735
|
+
const add = (raw, label = '') => {
|
|
736
|
+
const candidate = text(raw)?.replace(/^<|>$/g, '').split(/[?#]/u)[0]
|
|
737
|
+
const icon = safeIconPath(candidate)
|
|
738
|
+
if (!icon) return
|
|
739
|
+
const signal = (String(label) + ' ' + icon.path).toLowerCase()
|
|
740
|
+
if (!/(^|[\/_.\s-])(icon|logo|avatar|brand|mark)([\/_.\s-]|$)/u.test(signal)) return
|
|
741
|
+
const lowerPath = icon.path.toLowerCase()
|
|
742
|
+
const basename = lowerPath.split('/').pop() ?? ''
|
|
743
|
+
const score = /^(icon|logo)([._-]|$)/u.test(basename)
|
|
744
|
+
? 3
|
|
745
|
+
: /(^|\/)(assets?|images?|img|media)\//u.test(lowerPath)
|
|
746
|
+
? 2
|
|
747
|
+
: 1
|
|
748
|
+
candidates.push({ ...icon, score })
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const htmlImage = /<img\b([^>]*?)>/giu
|
|
752
|
+
let match
|
|
753
|
+
while ((match = htmlImage.exec(source))) {
|
|
754
|
+
const attrs = match[1]
|
|
755
|
+
const src = /\bsrc\s*=\s*["']([^"']+)["']/iu.exec(attrs)?.[1]
|
|
756
|
+
const alt = /\balt\s*=\s*["']([^"']*)["']/iu.exec(attrs)?.[1] ?? ''
|
|
757
|
+
add(src, alt)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const markdownImage = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/gu
|
|
761
|
+
while ((match = markdownImage.exec(source))) add(match[2], match[1])
|
|
762
|
+
|
|
763
|
+
candidates.sort((left, right) => right.score - left.score)
|
|
764
|
+
return candidates[0]
|
|
765
|
+
}
|
|
766
|
+
|
|
725
767
|
function encodedPath(path) {
|
|
726
768
|
return path.split('/').map(part => encodeURIComponent(part)).join('/')
|
|
727
769
|
}
|
|
@@ -900,8 +942,10 @@ export async function resolvePluginMetadata(items, options = {}) {
|
|
|
900
942
|
}
|
|
901
943
|
|
|
902
944
|
const compatibility = dshCompatibility(manifest, options.runtimeVersion)
|
|
945
|
+
const repository = manifestRepositoryUrl(manifest) ?? cleanUrl(item?.repository)
|
|
903
946
|
const value = {
|
|
904
947
|
...(text(manifest?.version) ? { version: text(manifest.version) } : {}),
|
|
948
|
+
...(repository ? { repository } : {}),
|
|
905
949
|
compatibility: compatibility.status,
|
|
906
950
|
...(compatibility.peers.length ? { dshPeers: compatibility.peers } : {}),
|
|
907
951
|
runtimeVersion: text(options.runtimeVersion),
|
|
@@ -917,22 +961,54 @@ async function resolveNpmIcon(item, options) {
|
|
|
917
961
|
if (!packageName || !version) return undefined
|
|
918
962
|
const manifest = await npmManifest(item, options)
|
|
919
963
|
const icon = safeIconPath(manifest?.icon)
|
|
920
|
-
if (
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
964
|
+
if (icon) {
|
|
965
|
+
const packagePath = packageName.startsWith('@')
|
|
966
|
+
? packageName.split('/').map(part => encodeURIComponent(part)).join('/')
|
|
967
|
+
: encodeURIComponent(packageName)
|
|
968
|
+
const iconUrl = 'https://unpkg.com/' + packagePath + '@' + encodeURIComponent(version) + '/' + encodedPath(icon.path)
|
|
969
|
+
return iconDataFromUrl(iconUrl, icon.mediaType, options)
|
|
970
|
+
}
|
|
971
|
+
const parts = githubRepositoryParts(manifestRepositoryUrl(manifest))
|
|
972
|
+
return parts ? githubReadmeIcon(parts, options) : undefined
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async function githubReadmeIcon(parts, options) {
|
|
976
|
+
const source = { id: 'plugin-readme-icon', name: 'plugin README icon', type: 'github', enabled: true }
|
|
977
|
+
for (const filename of ['README.md', 'readme.md']) {
|
|
978
|
+
try {
|
|
979
|
+
const readmeUrl = 'https://raw.githubusercontent.com/' + encodeURIComponent(parts.owner) + '/' + encodeURIComponent(parts.repo) + '/HEAD/' + filename
|
|
980
|
+
const bytes = await fetchBinary(readmeUrl, source, {
|
|
981
|
+
...options,
|
|
982
|
+
authToken: undefined,
|
|
983
|
+
allowPrivateNetwork: false,
|
|
984
|
+
maxBytes: Math.min(options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES, 512 * 1024),
|
|
985
|
+
})
|
|
986
|
+
const icon = readmeIconPath(new TextDecoder().decode(bytes))
|
|
987
|
+
if (!icon) continue
|
|
988
|
+
const iconUrl = 'https://raw.githubusercontent.com/' + encodeURIComponent(parts.owner) + '/' + encodeURIComponent(parts.repo) + '/HEAD/' + encodedPath(icon.path)
|
|
989
|
+
return await iconDataFromUrl(iconUrl, icon.mediaType, options)
|
|
990
|
+
} catch {
|
|
991
|
+
// README artwork is optional evidence; try the next conventional filename.
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return undefined
|
|
926
995
|
}
|
|
927
996
|
|
|
928
997
|
async function resolveGithubIcon(item, options) {
|
|
929
998
|
const parts = githubRepositoryParts(item?.repository)
|
|
930
999
|
if (!parts) return undefined
|
|
931
|
-
|
|
1000
|
+
let manifest
|
|
1001
|
+
try {
|
|
1002
|
+
manifest = await githubManifest(item, options)
|
|
1003
|
+
} catch {
|
|
1004
|
+
manifest = undefined
|
|
1005
|
+
}
|
|
932
1006
|
const icon = safeIconPath(manifest?.icon)
|
|
933
|
-
if (
|
|
934
|
-
|
|
935
|
-
|
|
1007
|
+
if (icon) {
|
|
1008
|
+
const iconUrl = 'https://raw.githubusercontent.com/' + encodeURIComponent(parts.owner) + '/' + encodeURIComponent(parts.repo) + '/HEAD/' + encodedPath(icon.path)
|
|
1009
|
+
return iconDataFromUrl(iconUrl, icon.mediaType, options)
|
|
1010
|
+
}
|
|
1011
|
+
return githubReadmeIcon(parts, options)
|
|
936
1012
|
}
|
|
937
1013
|
|
|
938
1014
|
export async function resolvePluginIcons(items, options = {}) {
|