@stolyarovmn/dsh-ui-registry-aggregator 0.5.0 → 0.5.1

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This branch is the clean DSH `0.2.0` implementation line. It does **not** carry compatibility code or UI workarounds from DSH `0.1.x`.
4
4
 
5
- Current stable version: `0.5.0`
5
+ Current stable version: `0.5.1`
6
6
  Current validation baseline: DSH `v0.2.0-rc.2`. Compatibility checks use the actual running DSH version from the active installation manifest rather than a hardcoded RC.
7
7
 
8
8
  ## Install from npm
@@ -12,7 +12,7 @@ $DSH_VERSION = "0.2.0-rc.2"
12
12
  $PROFILE = "dsh-020-test"
13
13
  $env:DSH_HOME = "$env:USERPROFILE\.dsh-020-test"
14
14
 
15
- pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" plugin --profile $PROFILE add "@stolyarovmn/dsh-ui-registry-aggregator@0.5.0"
15
+ pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" plugin --profile $PROFILE add "@stolyarovmn/dsh-ui-registry-aggregator@0.5.1"
16
16
  pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" --profile $PROFILE
17
17
  ```
18
18
 
@@ -40,7 +40,7 @@ Implemented in this milestone:
40
40
  - combinable multi-sort criteria for relevance, stars, downloads, freshness, and name with per-criterion direction; when several are active they contribute equally through percentile-normalized composite ranking, so every selected criterion can affect the order;
41
41
  - npm 30-day download enrichment plus lazy paired `30d | total` stats for visible npm rows; lifetime totals are summed from the package creation date (or npm's 2015-01-10 data floor) in bounded windows, and the UI never presents a partial lifetime sum as a real total;
42
42
  - package freshness distinguishes npm release time from GitHub repository push time, so GitHub metadata churn does not make every item look newly released;
43
- - best-effort package artwork discovery from the DSH top-level manifest `icon` field, returned to the Client as bounded data URLs with the same SVG/PNG/JPEG/WebP and 256 KiB policy as native DSH metadata;
43
+ - best-effort package artwork discovery prefers the official DSH top-level manifest `icon`; when it is absent, Browse can recover the GitHub repository from npm manifest metadata and lazily use a safe repository-local README logo/icon image. Installed Updates reuse the native Plugin Manager `BundleInfo.meta.icon`. All fetched artwork remains limited to SVG/PNG/JPEG/WebP and 256 KiB;
44
44
  - popular/default Browse results when the query is empty;
45
45
  - real npm/GitHub source marks and a package icon exposed through the DSH 0.2 manifest contract;
46
46
  - horizontal layout containment for narrow Plugin Manager detail panes;
@@ -48,11 +48,11 @@ Implemented in this milestone:
48
48
  - icon-only Browse install states using the same local DSH-style glyph language as the rest of the page;
49
49
  - explicit `v<version>` package versions plus lazy manifest evidence for GitHub/npm rows;
50
50
  - compatibility evidence against the actual running DSH version, following DSH peer semantics for `@deepseek-ai/dsh` and `@deepseek-ai/dsh-*`; Browse shows `Compatible`, `Incompatible`, or neutral `Not verified` as plain colored text, and can filter by compatibility;
51
- - live Updates discovery from native installed bundles plus latest npm manifests, with per-package update through `remote.pluginManager.installBundle`;
51
+ - live Updates discovery from native installed bundles plus latest npm manifests, with per-package update through `remote.pluginManager.installBundle`; package update actions use a distinct two-arrow Update glyph while content/list refresh keeps the single-arrow Refresh glyph;
52
52
  - no runtime import of Harness Client implementation packages;
53
53
  - Harness-provided React and DSH theme tokens.
54
54
 
55
- Known limitations in `0.5.0`:
55
+ Known limitations in `0.5.1`:
56
56
 
57
57
  - build-script approval UI remains in the native **Add plugin** dialog;
58
58
  - bulk **Update all** / cancel orchestration is not part of the DSH 0.2 line yet;
@@ -81,7 +81,7 @@ Sources UI
81
81
  ├─ custom-json
82
82
  └─ corporate
83
83
 
84
- Browse Install / Updates
84
+ Browse Install
85
85
  └─ native DSH remote.pluginManager
86
86
  ├─ inspect(spec)
87
87
  ├─ installBundle(spec)
@@ -105,7 +105,7 @@ pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" plugin --profile $PROFILE add "github:S
105
105
  pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" --profile $PROFILE
106
106
  ```
107
107
 
108
- Then open **Plugins → Installed → Registry Aggregator** and verify **Sources**, **Browse**, installation, compatibility filtering, download statistics, and **Updates** in the real Harness UI.
108
+ Then open **Plugins → Installed → Registry Aggregator → Sources** and verify source state in the real Harness UI.
109
109
 
110
110
  ## Historical lines
111
111
 
package/client.js CHANGED
@@ -615,6 +615,15 @@ window.__ModuleLoader__.load({
615
615
  ] })
616
616
  }
617
617
 
618
+ function IconUpdate({ size = 16 }) {
619
+ return h(SvgIcon, { size, children: [
620
+ 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' }),
621
+ h('path', { d: 'M1.75 2.75V5.75H4.75', stroke: 'currentColor' }),
622
+ 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' }),
623
+ h('path', { d: 'M14.25 13.25V10.25H11.25', stroke: 'currentColor' }),
624
+ ] })
625
+ }
626
+
618
627
  function IconPlus({ size = 16 }) {
619
628
  return h(SvgIcon, { size, children: [
620
629
  h('path', { d: 'M8 2V14', stroke: 'currentColor' }),
@@ -1775,7 +1784,7 @@ window.__ModuleLoader__.load({
1775
1784
  ? t('updateRetry')
1776
1785
  : format(t, 'updateAction', { version: item.availableVersion })
1777
1786
  return h('li', { key, className: 'ra-update-row' },
1778
- h('span', { className: 'ra-plugin-icon', 'aria-hidden': true }, h(IconPlugin, { size: 16 })),
1787
+ h('span', { className: 'ra-plugin-icon', 'aria-hidden': true }, h(PluginArtwork, { src: item.bundle?.meta?.icon })),
1779
1788
  h('div', { className: 'ra-update-main' },
1780
1789
  h('div', { className: 'ra-update-title' },
1781
1790
  h('span', null, key),
@@ -1796,7 +1805,7 @@ window.__ModuleLoader__.load({
1796
1805
  ? h(IconCheck, { size: 16 })
1797
1806
  : busy
1798
1807
  ? h(IconRefresh, { size: 16, className: 'ra-spin' })
1799
- : h(IconRefresh, { size: 16 }),
1808
+ : h(IconUpdate, { size: 16 }),
1800
1809
  }),
1801
1810
  )
1802
1811
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stolyarovmn/dsh-ui-registry-aggregator",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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 (!icon) return undefined
921
- const packagePath = packageName.startsWith('@')
922
- ? packageName.split('/').map(part => encodeURIComponent(part)).join('/')
923
- : encodeURIComponent(packageName)
924
- const iconUrl = 'https://unpkg.com/' + packagePath + '@' + encodeURIComponent(version) + '/' + encodedPath(icon.path)
925
- return iconDataFromUrl(iconUrl, icon.mediaType, options)
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
- const manifest = await githubManifest(item, options)
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 (!icon) return undefined
934
- const iconUrl = 'https://raw.githubusercontent.com/' + encodeURIComponent(parts.owner) + '/' + encodeURIComponent(parts.repo) + '/HEAD/' + encodedPath(icon.path)
935
- return iconDataFromUrl(iconUrl, icon.mediaType, options)
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 = {}) {