dshmarket 1.46.1 → 1.48.0
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 +4 -0
- package/README.zh.md +4 -0
- package/UPDATE-API-V1.md +62 -0
- package/client/client.js +196 -35
- package/lib/accelerate.js +10 -0
- package/lib/changelog.js +20 -3
- package/lib/dsh-cli.js +45 -1
- package/lib/dsh-install.js +74 -41
- package/lib/hot.js +71 -5
- package/lib/install.js +39 -3
- package/lib/pnpm-compat.js +6 -3
- package/lib/routes.js +199 -41
- package/lib/types/dsh-cli.d.ts +34 -0
- package/lib/types/dsh-install.d.ts +2 -2
- package/lib/types/hot.d.ts +31 -0
- package/lib/types/install.d.ts +38 -3
- package/package.json +1 -1
- package/src/accelerate.ts +9 -0
- package/src/changelog.ts +21 -4
- package/src/client/MarketSection.tsx +1 -1
- package/src/client/index.ts +66 -38
- package/src/client/market-data.ts +74 -4
- package/src/client/market-element.ts +60 -0
- package/src/client/section-gate.ts +79 -0
- package/src/dsh-cli.ts +45 -1
- package/src/dsh-install.ts +69 -44
- package/src/hot.ts +64 -2
- package/src/install.ts +40 -2
- package/src/pnpm-compat.ts +6 -3
- package/src/routes.ts +201 -40
package/README.md
CHANGED
|
@@ -123,6 +123,10 @@ DSHM_REGISTRY_URL=https://your-mirror.example/plugins.json dsh web
|
|
|
123
123
|
|
|
124
124
|
[local-dsh](https://github.com/liangchen-harold/local-dsh) — a DeepSeek Harness desktop client that can run the model on your own machine: it bundles llama.cpp next to Node, pnpm and DSH, so a downloaded GGUF model answers without any external API. Built on Tauri; Apple Silicon Macs for now. [localdsh.com](https://localdsh.com)
|
|
125
125
|
|
|
126
|
+
### dsh desktop (MochiNek0)
|
|
127
|
+
|
|
128
|
+
[dsh-desktop](https://github.com/MochiNek0/dsh-desktop) — a cross-platform DeepSeek Harness desktop client built on Tauri (Windows, macOS, Linux). It uses the system webview, so the installer is a few megabytes: 2.3 MB on Windows, 5.8 MB on macOS. It starts `dsh web` in the background on launch and embeds it in a native window, sharing sessions and config with the CLI. This market sits first in the recommended list of its built-in plugin panel, one click to install. A Runtime panel enumerates and switches the machine's Node installs and installs or upgrades dsh, no administrator privileges required, and native notifications fire when a turn ends or dsh is waiting on you. [dsh-desktop.cc.cd](https://dsh-desktop.cc.cd/)
|
|
129
|
+
|
|
126
130
|
### DSH Get
|
|
127
131
|
|
|
128
132
|
[DSH Get](https://www.dshget.com/) — a searchable web directory for discovering DeepSeek Harness plugins: category filters, bilingual descriptions, install commands and per-plugin detail pages. Its normalized catalog snapshot is public at [bobby-sheng/dshget-data](https://github.com/bobby-sheng/dshget-data).
|
package/README.zh.md
CHANGED
|
@@ -120,6 +120,10 @@ DSHM_REGISTRY_URL=https://your-mirror.example/plugins.json dsh web
|
|
|
120
120
|
|
|
121
121
|
[local-dsh](https://github.com/liangchen-harold/local-dsh)——可以把模型跑在本机的 DeepSeek Harness 桌面客户端:发行包内置 llama.cpp 与 Node、pnpm、DSH,下载一个 GGUF 模型就能对话,不必接外部 API。基于 Tauri 构建;目前支持 Apple 芯片的 Mac。[localdsh.com](https://localdsh.com)
|
|
122
122
|
|
|
123
|
+
### dsh desktop(MochiNek0)
|
|
124
|
+
|
|
125
|
+
[dsh-desktop](https://github.com/MochiNek0/dsh-desktop)——基于 Tauri 构建的跨平台 DeepSeek Harness 桌面客户端(Windows / macOS / Linux),界面走系统 webview,安装包只有几 MB(Windows 2.3 MB、macOS 5.8 MB)。启动时自动在后台拉起 `dsh web` 并嵌入原生窗口,会话与配置和 CLI 共享;内置插件面板的推荐位第一条就是本市场,点一下即可装上。另有「运行环境」面板枚举与切换本机 Node、安装或升级 dsh,全程无需管理员权限;回合结束或 dsh 等待你确认时发出系统通知。[dsh-desktop.cc.cd](https://dsh-desktop.cc.cd/)
|
|
126
|
+
|
|
123
127
|
### DSH Get
|
|
124
128
|
|
|
125
129
|
[DSH Get](https://www.dshget.com/)——DeepSeek Harness 插件的网页检索目录:分类筛选、中英描述、安装命令与插件详情页;其规范化的目录快照公开在 [bobby-sheng/dshget-data](https://github.com/bobby-sheng/dshget-data)。
|
package/UPDATE-API-V1.md
CHANGED
|
@@ -33,6 +33,37 @@ supported features, restart owner and endpoint paths. A client must hide its
|
|
|
33
33
|
restart button when `restart.supported` is false. Desktop and supervised hosts
|
|
34
34
|
normally delegate restart to their owning shell or operator.
|
|
35
35
|
|
|
36
|
+
## Count what can be updated
|
|
37
|
+
|
|
38
|
+
```http
|
|
39
|
+
GET /dsh-market/api/v1/updates/summary
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"schema": "dsh-market/update-api/v1",
|
|
45
|
+
"checked": 12,
|
|
46
|
+
"updatable": 3,
|
|
47
|
+
"packages": [
|
|
48
|
+
{ "name": "dsh-mcp-connector", "source": "npm", "installedVersion": "1.1.0", "latestVersion": "1.2.0" }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`checked` is the denominator — how many installed plugins the answer looked
|
|
54
|
+
at. Read it, or a badge cannot tell "nothing to update" from "nothing was
|
|
55
|
+
looked at". The two are different answers and only one of them means the
|
|
56
|
+
profile is up to date.
|
|
57
|
+
|
|
58
|
+
`packages` carries the same objects the single-package endpoint below
|
|
59
|
+
returns, and lists only plugins that can be updated. A badge needs
|
|
60
|
+
`updatable`; a panel needs the rows; neither should have to filter the whole
|
|
61
|
+
profile itself.
|
|
62
|
+
|
|
63
|
+
`GET /dsh-market/api/v1/capabilities` reports this endpoint at
|
|
64
|
+
`endpoints.updatesSummary` and sets `features.updatesSummary`. Check that
|
|
65
|
+
rather than probing for the path.
|
|
66
|
+
|
|
36
67
|
## Check one installed package
|
|
37
68
|
|
|
38
69
|
```http
|
|
@@ -123,6 +154,37 @@ same-origin, no forwarding headers, no package mutation in progress, and a Host
|
|
|
123
154
|
whose lifecycle is not owned by Desktop or a supervisor. Clients must feature
|
|
124
155
|
detect it; they must not invent an alternative process-control path.
|
|
125
156
|
|
|
157
|
+
## Rendering the market's panel elsewhere (client-side)
|
|
158
|
+
|
|
159
|
+
A host shell that wants the market inside its own container — rather than in
|
|
160
|
+
the settings page — reads the client service this package publishes:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const market = ctx.reflect.get('market')
|
|
164
|
+
const element = market.render({ preferredSubsectionId: 'installed' })
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`render()` returns the market's own panel wrapped in its error boundary, as a
|
|
168
|
+
React element. Same page, same React instance: this package's client bundle
|
|
169
|
+
resolves react through the host's module table, so the element mounts
|
|
170
|
+
anywhere in that tree.
|
|
171
|
+
|
|
172
|
+
| member | |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `version` | `1` |
|
|
175
|
+
| `render(props?)` | the panel element; `preferredSubsectionId` is optional |
|
|
176
|
+
| `setSettingsVisible(visible)` | register or retract the market's own `settings.section` entry |
|
|
177
|
+
| `settingsVisible()` | whether that entry is registered right now |
|
|
178
|
+
|
|
179
|
+
`ctx.provide(name, value)` and `ctx.reflect.provide(name, value)` are the
|
|
180
|
+
same call — cordis's `Service.provide` delegates to the reflect layer — so
|
|
181
|
+
either idiom reaches this service.
|
|
182
|
+
|
|
183
|
+
What `render()` is **not**: a way to rearrange the market. It hands over the
|
|
184
|
+
whole panel, chrome included. Cutting the market into host-fillable regions
|
|
185
|
+
is a different design, and one host asking is not yet evidence that it fits
|
|
186
|
+
anyone else.
|
|
187
|
+
|
|
126
188
|
## Compatibility policy
|
|
127
189
|
|
|
128
190
|
- New optional response fields may be added within v1.
|
package/client/client.js
CHANGED
|
@@ -1591,10 +1591,20 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1591
1591
|
* dependency's spec pins a github repo AND the entry states one, the repos
|
|
1592
1592
|
* decide — the loose name/npm identities only apply when at least one side
|
|
1593
1593
|
* carries no repo evidence (npm installs, non-github entries).
|
|
1594
|
+
*
|
|
1595
|
+
* Repo evidence only ever decides by repository ROOT. A monorepo catalog
|
|
1596
|
+
* entry states `owner/repo#path:/pkg` while an npm-installed manifest
|
|
1597
|
+
* usually states the bare `owner/repo` (it rarely declares
|
|
1598
|
+
* `repository.directory`), and reading that asymmetry as a source conflict
|
|
1599
|
+
* kept a genuinely installed subpackage from ever reading as installed.
|
|
1594
1600
|
*/
|
|
1601
|
+
/** Repository root: the part before any `#path:/…` subpath selection. */
|
|
1602
|
+
function repoRoots(ids) {
|
|
1603
|
+
return new Set([...ids].map((id) => id.split("#path:/")[0]));
|
|
1604
|
+
}
|
|
1595
1605
|
function sameSourceConflict(plugin, spec, repoIdentities = []) {
|
|
1596
|
-
const entry = entryRepoIds(plugin);
|
|
1597
|
-
const dep = depRepoIds(spec, repoIdentities);
|
|
1606
|
+
const entry = repoRoots(entryRepoIds(plugin));
|
|
1607
|
+
const dep = repoRoots(depRepoIds(spec, repoIdentities));
|
|
1598
1608
|
if (entry.size === 0 || dep.size === 0) return false;
|
|
1599
1609
|
for (const id of dep) if (entry.has(id)) return false;
|
|
1600
1610
|
return true;
|
|
@@ -1645,6 +1655,56 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1645
1655
|
for (const id of entryIdentities(plugin)) if (dep.has(id)) return true;
|
|
1646
1656
|
return false;
|
|
1647
1657
|
}
|
|
1658
|
+
/**
|
|
1659
|
+
* The same memo, for the branch #262 left behind (#589).
|
|
1660
|
+
*
|
|
1661
|
+
* `looseMatchCount` above covers dependencies installed by version. A
|
|
1662
|
+
* `link:` or `file:` dependency takes the other branch, into
|
|
1663
|
+
* `findCatalogEntryForLocal`, which walks the whole catalog at least twice
|
|
1664
|
+
* per call — once to filter by name, once to collect `/tree/` repos — and
|
|
1665
|
+
* up to twice more when there are identities to probe. Both callers below
|
|
1666
|
+
* run once per rendered card. The reporter profiled ~300ms per repaint at
|
|
1667
|
+
* 24 cards against a 3,627-entry catalog where a version-pinned dependency
|
|
1668
|
+
* paid 1.1ms; a local benchmark measured ~38ms per render at that shape,
|
|
1669
|
+
* and ~1.4s at 96 cards with eight local dependencies.
|
|
1670
|
+
*
|
|
1671
|
+
* The inner key carries the EVIDENCE, not just the name. Installing a plugin
|
|
1672
|
+
* hands the next render a fresh identities array while the catalog array
|
|
1673
|
+
* stays the same, so a name-only key would answer the post-install question
|
|
1674
|
+
* with the pre-install result — which is the same-named-fork confusion #485
|
|
1675
|
+
* asked this matcher to stop making, reintroduced as a cache bug.
|
|
1676
|
+
*
|
|
1677
|
+
* A miss is cached as `null`, which is why the "not cached yet" sentinel
|
|
1678
|
+
* has to be `undefined`: `null` is a real answer here, and it costs the
|
|
1679
|
+
* same full scan to establish as a hit does. It is also the common case —
|
|
1680
|
+
* a checkout you are developing is usually not in the catalog at all.
|
|
1681
|
+
*
|
|
1682
|
+
* The invariant this rests on, stated because the WeakMap cannot enforce it:
|
|
1683
|
+
* the catalog array and the entries inside it are frozen once handed here. A
|
|
1684
|
+
* refetch replaces the array — which is what the outer key is for — but an
|
|
1685
|
+
* in-place `push`, `sort` or `reverse`, or editing a row's `url`, would keep
|
|
1686
|
+
* the key and change the answer. Order is load-bearing too: the matcher
|
|
1687
|
+
* returns the FIRST row that fits. Nothing in the client does any of this
|
|
1688
|
+
* today; `visiblePlugins` and `themePlugins` both sort copies.
|
|
1689
|
+
*/
|
|
1690
|
+
const localMatchCache = /* @__PURE__ */ new WeakMap();
|
|
1691
|
+
function cachedEntryForLocal(plugins, name, identities, hints) {
|
|
1692
|
+
let byKey = localMatchCache.get(plugins);
|
|
1693
|
+
if (byKey === void 0) {
|
|
1694
|
+
byKey = /* @__PURE__ */ new Map();
|
|
1695
|
+
localMatchCache.set(plugins, byKey);
|
|
1696
|
+
}
|
|
1697
|
+
const key = JSON.stringify([
|
|
1698
|
+
name,
|
|
1699
|
+
identities,
|
|
1700
|
+
hints
|
|
1701
|
+
]);
|
|
1702
|
+
const hit = byKey.get(key);
|
|
1703
|
+
if (hit !== void 0) return hit;
|
|
1704
|
+
const entry = findCatalogEntryForLocal(plugins, name, identities, hints);
|
|
1705
|
+
byKey.set(key, entry);
|
|
1706
|
+
return entry;
|
|
1707
|
+
}
|
|
1648
1708
|
/** The installed dependency name a registry entry corresponds to, or null. */
|
|
1649
1709
|
function matchInstalledName(plugin, installed, repoIdentities = {}, plugins, repoHints = {}) {
|
|
1650
1710
|
const ids = entryIdentities(plugin);
|
|
@@ -1653,7 +1713,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1653
1713
|
const repos = repoIdentities[name] ?? [];
|
|
1654
1714
|
if (/^(?:link|file):/i.test(specStr)) {
|
|
1655
1715
|
if (plugins === void 0) continue;
|
|
1656
|
-
const entry =
|
|
1716
|
+
const entry = cachedEntryForLocal(plugins, name, repos, repoHints[name] ?? []);
|
|
1657
1717
|
if (entry !== null && entry.url === plugin.url) return name;
|
|
1658
1718
|
continue;
|
|
1659
1719
|
}
|
|
@@ -2055,7 +2115,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
2055
2115
|
}
|
|
2056
2116
|
/** Catalog row for an installed dependency — strict for local link:/file: specs. */
|
|
2057
2117
|
function catalogEntryForInstalled(plugins, name, spec, repoIdentities = [], repoHints = []) {
|
|
2058
|
-
if (/^(?:link|file):/i.test(spec)) return
|
|
2118
|
+
if (/^(?:link|file):/i.test(spec)) return cachedEntryForLocal(plugins, name, repoIdentities, repoHints) ?? void 0;
|
|
2059
2119
|
return entryForDep(plugins, name, spec, repoIdentities, repoHints);
|
|
2060
2120
|
}
|
|
2061
2121
|
//#endregion
|
|
@@ -8614,7 +8674,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
8614
8674
|
children: updatingName === self ? t("updating") : status.restoreRequired === true ? t("restoreOnline") : t("marketUpdate")
|
|
8615
8675
|
});
|
|
8616
8676
|
})(),
|
|
8617
|
-
reminderBatchUpdatableNames.length >=
|
|
8677
|
+
reminderBatchUpdatableNames.length >= 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
8618
8678
|
variant: "primary",
|
|
8619
8679
|
size: "sm",
|
|
8620
8680
|
disabled: updatingAll || updatingName !== null || busyUrl !== null || removingName !== null,
|
|
@@ -10660,6 +10720,86 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10660
10720
|
});
|
|
10661
10721
|
}
|
|
10662
10722
|
//#endregion
|
|
10723
|
+
//#region src/client/market-element.ts
|
|
10724
|
+
/**
|
|
10725
|
+
* The market's panel, as an element, built from explicit dependencies.
|
|
10726
|
+
*
|
|
10727
|
+
* Two callers want the same thing and must not drift apart:
|
|
10728
|
+
*
|
|
10729
|
+
* - the `settings.section` this package registers, whose slot passes down a
|
|
10730
|
+
* host-chosen `preferredSubsectionId`;
|
|
10731
|
+
* - `market.render()`, for a host shell that renders the market inside its
|
|
10732
|
+
* own container (#602, from the Tauri desktop).
|
|
10733
|
+
*
|
|
10734
|
+
* A module function taking its dependencies rather than a closure over the
|
|
10735
|
+
* cordis context, so the wiring — which locale, which theme, which log
|
|
10736
|
+
* exporter reaches the panel — is something a test can assert instead of
|
|
10737
|
+
* something only a running host can reveal.
|
|
10738
|
+
*/
|
|
10739
|
+
/**
|
|
10740
|
+
* @param props - see {@link MarketElementProps}.
|
|
10741
|
+
* @returns the panel wrapped in its error boundary.
|
|
10742
|
+
*/
|
|
10743
|
+
function marketElement(props) {
|
|
10744
|
+
return (0, react.createElement)(MarketErrorBoundary, {
|
|
10745
|
+
text: props.crashText,
|
|
10746
|
+
actions: (0, react.createElement)("button", {
|
|
10747
|
+
type: "button",
|
|
10748
|
+
onClick: () => {
|
|
10749
|
+
props.exportLog();
|
|
10750
|
+
}
|
|
10751
|
+
}, props.t("exportLog"))
|
|
10752
|
+
}, (0, react.createElement)(MarketSection, {
|
|
10753
|
+
t: props.t,
|
|
10754
|
+
locale: props.locale,
|
|
10755
|
+
theme: props.theme,
|
|
10756
|
+
themeStore: props.themeStore,
|
|
10757
|
+
preferredSubsectionId: props.preferredSubsectionId
|
|
10758
|
+
}));
|
|
10759
|
+
}
|
|
10760
|
+
//#endregion
|
|
10761
|
+
//#region src/client/section-gate.ts
|
|
10762
|
+
/**
|
|
10763
|
+
* @param register - registers the entry and returns its disposer. Called
|
|
10764
|
+
* only when the entry should be visible; may be called again after a
|
|
10765
|
+
* retraction, which is why the disposer is required rather than optional.
|
|
10766
|
+
* @returns the gate.
|
|
10767
|
+
*/
|
|
10768
|
+
function createSectionGate(register) {
|
|
10769
|
+
let ready = false;
|
|
10770
|
+
let wanted = true;
|
|
10771
|
+
let removed = false;
|
|
10772
|
+
let dispose = null;
|
|
10773
|
+
const apply = () => {
|
|
10774
|
+
if (!ready) return;
|
|
10775
|
+
const shouldShow = wanted && !removed;
|
|
10776
|
+
if (shouldShow && dispose === null) {
|
|
10777
|
+
dispose = register();
|
|
10778
|
+
return;
|
|
10779
|
+
}
|
|
10780
|
+
if (!shouldShow && dispose !== null) {
|
|
10781
|
+
const stop = dispose;
|
|
10782
|
+
dispose = null;
|
|
10783
|
+
stop();
|
|
10784
|
+
}
|
|
10785
|
+
};
|
|
10786
|
+
return {
|
|
10787
|
+
available: () => {
|
|
10788
|
+
ready = true;
|
|
10789
|
+
apply();
|
|
10790
|
+
},
|
|
10791
|
+
setVisible: (visible) => {
|
|
10792
|
+
wanted = visible;
|
|
10793
|
+
apply();
|
|
10794
|
+
},
|
|
10795
|
+
visible: () => dispose !== null,
|
|
10796
|
+
retire: () => {
|
|
10797
|
+
removed = true;
|
|
10798
|
+
apply();
|
|
10799
|
+
}
|
|
10800
|
+
};
|
|
10801
|
+
}
|
|
10802
|
+
//#endregion
|
|
10663
10803
|
//#region src/client/SettingsCard.tsx
|
|
10664
10804
|
/**
|
|
10665
10805
|
* The market's card on the plugin configuration page (dsh >= 0.1.0-rc.7).
|
|
@@ -11150,8 +11290,32 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11150
11290
|
en
|
|
11151
11291
|
}), "dsh-market: dictionaries");
|
|
11152
11292
|
const t = ctx.locale.bind(NS);
|
|
11153
|
-
|
|
11154
|
-
|
|
11293
|
+
/**
|
|
11294
|
+
* The market's own panel, as an element — one builder for the settings
|
|
11295
|
+
* section this package registers and for `market.render()` (#602). Built
|
|
11296
|
+
* per call: the props are live (locale, theme, the host's preferred
|
|
11297
|
+
* subsection), and a cached element would freeze the first caller's.
|
|
11298
|
+
*/
|
|
11299
|
+
const buildMarketElement = (ownerProps = {}) => marketElement({
|
|
11300
|
+
t,
|
|
11301
|
+
locale: ctx.locale,
|
|
11302
|
+
theme: ctx.theme,
|
|
11303
|
+
themeStore: {
|
|
11304
|
+
subscribe: (cb) => ctx.on("theme/change", cb),
|
|
11305
|
+
getSnapshot: () => ctx.theme.getTheme()
|
|
11306
|
+
},
|
|
11307
|
+
crashText: {
|
|
11308
|
+
title: t("crashTitle"),
|
|
11309
|
+
hint: t("crashHint"),
|
|
11310
|
+
reload: t("crashReload"),
|
|
11311
|
+
details: t("crashDetails")
|
|
11312
|
+
},
|
|
11313
|
+
exportLog: () => {
|
|
11314
|
+
exportMarketLog().catch(() => {});
|
|
11315
|
+
},
|
|
11316
|
+
preferredSubsectionId: ownerProps.preferredSubsectionId
|
|
11317
|
+
});
|
|
11318
|
+
const sectionGate = createSectionGate(() => {
|
|
11155
11319
|
const off = ctx.slots.register({
|
|
11156
11320
|
name: "settings.section",
|
|
11157
11321
|
id: "market",
|
|
@@ -11159,32 +11323,31 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11159
11323
|
label: () => t("nav"),
|
|
11160
11324
|
locale: NS,
|
|
11161
11325
|
inject: () => ({ t })
|
|
11162
|
-
}, (ownerProps = {}) => (
|
|
11163
|
-
|
|
11164
|
-
title: t("crashTitle"),
|
|
11165
|
-
hint: t("crashHint"),
|
|
11166
|
-
reload: t("crashReload"),
|
|
11167
|
-
details: t("crashDetails")
|
|
11168
|
-
},
|
|
11169
|
-
actions: (0, react.createElement)("button", {
|
|
11170
|
-
type: "button",
|
|
11171
|
-
onClick: () => {
|
|
11172
|
-
exportMarketLog().catch(() => {});
|
|
11173
|
-
}
|
|
11174
|
-
}, t("exportLog"))
|
|
11175
|
-
}, (0, react.createElement)(MarketSection, {
|
|
11176
|
-
t,
|
|
11177
|
-
locale: ctx.locale,
|
|
11178
|
-
theme: ctx.theme,
|
|
11179
|
-
themeStore: {
|
|
11180
|
-
subscribe: (cb) => ctx.on("theme/change", cb),
|
|
11181
|
-
getSnapshot: () => ctx.theme.getTheme()
|
|
11182
|
-
},
|
|
11183
|
-
preferredSubsectionId: ownerProps.preferredSubsectionId
|
|
11184
|
-
})));
|
|
11185
|
-
if (typeof off === "function") retireSection = off;
|
|
11186
|
-
return off;
|
|
11326
|
+
}, (ownerProps = {}) => buildMarketElement(ownerProps));
|
|
11327
|
+
return typeof off === "function" ? off : () => {};
|
|
11187
11328
|
});
|
|
11329
|
+
ctx.slots.inject("settings.section", () => {
|
|
11330
|
+
sectionGate.available();
|
|
11331
|
+
});
|
|
11332
|
+
const marketControl = {
|
|
11333
|
+
version: 1,
|
|
11334
|
+
setSettingsVisible: (visible) => {
|
|
11335
|
+
sectionGate.setVisible(visible);
|
|
11336
|
+
},
|
|
11337
|
+
settingsVisible: () => sectionGate.visible(),
|
|
11338
|
+
/**
|
|
11339
|
+
* The market's panel as an element, for a host that renders it inside
|
|
11340
|
+
* its own container. Same page, same React instance — this package's
|
|
11341
|
+
* bundle resolves react through the host's module table, so an element
|
|
11342
|
+
* returned here mounts anywhere in that tree.
|
|
11343
|
+
*
|
|
11344
|
+
* What it is NOT: a way to rearrange the market. It hands over the whole
|
|
11345
|
+
* panel, chrome included. Cutting the market into host-fillable regions
|
|
11346
|
+
* is a different design and has not been asked for by a second host yet.
|
|
11347
|
+
*/
|
|
11348
|
+
render: (props = {}) => buildMarketElement(props)
|
|
11349
|
+
};
|
|
11350
|
+
if (typeof ctx.provide === "function") ctx.provide("market", marketControl);
|
|
11188
11351
|
ctx.inject(["settingsScope"], (scoped) => {
|
|
11189
11352
|
scoped.slots.inject("settings.plugin.item", () => scoped.slots.register({
|
|
11190
11353
|
name: "settings.plugin.item",
|
|
@@ -11194,9 +11357,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11194
11357
|
}, () => (0, react.createElement)(SettingsCard, {
|
|
11195
11358
|
t,
|
|
11196
11359
|
onRemoved: () => {
|
|
11197
|
-
|
|
11198
|
-
retireSection = null;
|
|
11199
|
-
off?.();
|
|
11360
|
+
sectionGate.retire();
|
|
11200
11361
|
}
|
|
11201
11362
|
})));
|
|
11202
11363
|
});
|
package/lib/accelerate.js
CHANGED
|
@@ -112,6 +112,16 @@ async function tryHeadCommit(repo, proxy, signal, ref) {
|
|
|
112
112
|
// metacharacters (`.` is legal in a branch name).
|
|
113
113
|
const quoted = ref.replace(/[.*+?^${}()|[\]\\]/gu, String.raw `\$&`);
|
|
114
114
|
for (const namespace of ['heads', 'tags']) {
|
|
115
|
+
// An annotated tag advertises two refs — the tag object and its
|
|
116
|
+
// peeled commit (`refs/tags/<ref>^{}`). pnpm resolves the tag to the
|
|
117
|
+
// peeled commit and codeload records that sha in the lockfile, so the
|
|
118
|
+
// peeled line is the one that must equal `current`; answering with
|
|
119
|
+
// the tag-object sha made annotated-tag installs report an update
|
|
120
|
+
// forever (#597). Lightweight tags have no peeled line and fall
|
|
121
|
+
// through to the direct one.
|
|
122
|
+
const peeled = new RegExp(String.raw `([0-9a-f]{40}) refs/${namespace}/${quoted}\^\{\}(?![^\s])`, 'u').exec(body);
|
|
123
|
+
if (peeled !== null)
|
|
124
|
+
return { kind: 'valid', sha: peeled[1] };
|
|
115
125
|
const found = new RegExp(String.raw `([0-9a-f]{40}) refs/${namespace}/${quoted}(?![^\s])`, 'u').exec(body);
|
|
116
126
|
if (found !== null)
|
|
117
127
|
return { kind: 'valid', sha: found[1] };
|
package/lib/changelog.js
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
import { fileFromTarball } from './catalog-npm.js';
|
|
21
21
|
import { marketFetch } from './net.js';
|
|
22
22
|
import { activeRegion, routesFor } from './regions.js';
|
|
23
|
-
import { profileDir, readInstalled, readLockCommits } from './profile.js';
|
|
24
|
-
import { lookupRepoFromUrl, repoOfTarget } from './sources.js';
|
|
23
|
+
import { profileDir, readInstalled, readInstalledRepoEvidence, readLockCommits } from './profile.js';
|
|
24
|
+
import { lookupRepoFromUrl, repoOf, repoOfTarget } from './sources.js';
|
|
25
25
|
import { checkUpdates } from './updates.js';
|
|
26
26
|
import { loadRegistry } from './registry.js';
|
|
27
27
|
const UPDATES_PACKAGE = 'dsh-plugin-updates';
|
|
@@ -198,7 +198,24 @@ export async function updateNotesFor(profile, explicitDir, name) {
|
|
|
198
198
|
if (key === null) {
|
|
199
199
|
try {
|
|
200
200
|
const registry = await loadRegistry();
|
|
201
|
-
const
|
|
201
|
+
const candidates = registry.plugins.filter(p => p.name === name);
|
|
202
|
+
let plugin;
|
|
203
|
+
if (candidates.length === 1) {
|
|
204
|
+
plugin = candidates[0];
|
|
205
|
+
}
|
|
206
|
+
else if (candidates.length > 1) {
|
|
207
|
+
// Same-named packages exist in the catalog; a bare name match can
|
|
208
|
+
// pick someone else's repo and answer "no notes" for a plugin that
|
|
209
|
+
// ships updates data under its own repository (#598). The installed
|
|
210
|
+
// package declares its repository, so prefer the catalog entry that
|
|
211
|
+
// agrees with it; only a unique agreement is trusted — an ambiguous
|
|
212
|
+
// name falls through to npm publish times, which are honest for any
|
|
213
|
+
// installed npm package.
|
|
214
|
+
const evidence = readInstalledRepoEvidence(profile, name, spec, explicitDir);
|
|
215
|
+
const matches = candidates.filter(p => evidence.identities.some(id => repoOf(p.url)?.toLowerCase() === id.split('#')[0].toLowerCase()));
|
|
216
|
+
if (matches.length === 1)
|
|
217
|
+
plugin = matches[0];
|
|
218
|
+
}
|
|
202
219
|
if (plugin !== undefined) {
|
|
203
220
|
key = plugin.url;
|
|
204
221
|
}
|
package/lib/dsh-cli.js
CHANGED
|
@@ -192,6 +192,44 @@ export function toolSearchDirs(platform = process.platform, env = process.env, h
|
|
|
192
192
|
// one place a user goes looking for an answer.
|
|
193
193
|
return [...new Set(dirs.filter(dir => dir.trim() !== ''))];
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Stop git asking for credentials down a channel nobody is listening on
|
|
197
|
+
* (#587).
|
|
198
|
+
*
|
|
199
|
+
* `CI=true` below is the same defence one layer up, and pnpm reads it. git
|
|
200
|
+
* does not — it has its own switch, and it was not set. The gap only opens
|
|
201
|
+
* when a spec reaches pnpm's git fetcher instead of the codeload tarball
|
|
202
|
+
* path `accelerate.ts` describes: `github:owner/repo#path:/sub` is one, and
|
|
203
|
+
* pnpm really does shell out to `git` for it, trying HTTPS first.
|
|
204
|
+
*
|
|
205
|
+
* git's credential prompt opens the controlling terminal, not stdin. There
|
|
206
|
+
* is no terminal here, so the question is never seen and never answered:
|
|
207
|
+
* the reporter caught `git.exe` alive for eight minutes having burned 0.05s
|
|
208
|
+
* of CPU, and only the fifteen-minute install timeout ended it. Refusing
|
|
209
|
+
* the prompt turns that into a fast, readable failure.
|
|
210
|
+
*
|
|
211
|
+
* It is a default, not an override: a value the caller set wins, and blank
|
|
212
|
+
* counts as unset because an empty `GIT_TERMINAL_PROMPT` is not a setting
|
|
213
|
+
* git can parse either. Credential helpers and `GIT_ASKPASS` are untouched
|
|
214
|
+
* and still answer first — this closes only the terminal fallback, which is
|
|
215
|
+
* precisely the branch that cannot work from a spawned child.
|
|
216
|
+
*
|
|
217
|
+
* Scope, stated plainly because it is narrower than the issue title
|
|
218
|
+
* suggests: this is the HTTPS half. pnpm falls back to `git@github.com:`
|
|
219
|
+
* when HTTPS fails, and the ssh side needs `GIT_SSH_COMMAND`, which
|
|
220
|
+
* overrides `core.sshCommand` and `GIT_SSH` — the two ordinary ways to
|
|
221
|
+
* choose an identity — and whose `BatchMode=yes` would disable
|
|
222
|
+
* `SSH_ASKPASS`, breaking key-passphrase installs that work today. That
|
|
223
|
+
* half needs a policy decision, so it is not made here. Note also that on
|
|
224
|
+
* POSIX `runDshPlugin` spawns detached, so the subtree has no controlling
|
|
225
|
+
* terminal and the prompt already dies instantly; the hang the issue
|
|
226
|
+
* reports needs Windows, where the spawn is not detached.
|
|
227
|
+
*/
|
|
228
|
+
export function gitEnvForPnpm(env = process.env) {
|
|
229
|
+
if ((env.GIT_TERMINAL_PROMPT ?? '').trim() !== '')
|
|
230
|
+
return {};
|
|
231
|
+
return { GIT_TERMINAL_PROMPT: '0' };
|
|
232
|
+
}
|
|
195
233
|
function spawnEnv() {
|
|
196
234
|
// pnpm v10+ blocks forever on a silent interactive prompt without a TTY;
|
|
197
235
|
// CI mode forces it to act or fail instead of asking.
|
|
@@ -201,7 +239,13 @@ function spawnEnv() {
|
|
|
201
239
|
if (!parts.includes(bin))
|
|
202
240
|
parts.push(bin);
|
|
203
241
|
}
|
|
204
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
...process.env,
|
|
244
|
+
...proxyEnvForPnpm(process.env, activeRegion()),
|
|
245
|
+
...gitEnvForPnpm(process.env),
|
|
246
|
+
CI: 'true',
|
|
247
|
+
PATH: parts.join(separator),
|
|
248
|
+
};
|
|
205
249
|
}
|
|
206
250
|
const INSTALL_TIMEOUT_MS = Number(process.env.DSH_MARKET_INSTALL_TIMEOUT_MS) || 15 * 60 * 1000;
|
|
207
251
|
/**
|
package/lib/dsh-install.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Locate the DSH host package in CLI and packaged Desktop runtimes. */
|
|
2
2
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
/** The entry with symlinks resolved, or unchanged when it cannot be read. */
|
|
5
5
|
function realpathOf(entry) {
|
|
6
6
|
try {
|
|
@@ -14,27 +14,77 @@ function realpathOf(entry) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
const DSH_PACKAGE = '@deepseek-ai/dsh';
|
|
17
|
+
const DESKTOP_PACKAGE = '@deepseek-ai/dsh-desktop';
|
|
18
|
+
const APPLICATION_ROOTS = ['app.asar.unpacked', 'app.asar', 'app'];
|
|
19
|
+
// The split runtime reported in #553. Only these local, identity-checked
|
|
20
|
+
// packages corroborate the shell; never resolve witnesses from a profile.
|
|
21
|
+
const DESKTOP_RUNTIME_PACKAGES = ['dsh-base', 'dsh-web-app', 'dsh-web', 'dsh-settings'];
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @returns the parsed manifest of `@deepseek-ai/dsh`, or null.
|
|
23
|
+
* A package's own manifest, or null when its identity cannot be confirmed.
|
|
20
24
|
*/
|
|
21
|
-
function readDshManifest(directory) {
|
|
25
|
+
function readDshManifest(directory, name = DSH_PACKAGE) {
|
|
22
26
|
try {
|
|
23
27
|
const manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
|
|
24
|
-
return manifest.name ===
|
|
28
|
+
return manifest.name === name ? { name, version: manifest.version } : null;
|
|
25
29
|
}
|
|
26
30
|
catch {
|
|
27
31
|
return null;
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
34
|
+
function entryDirectories(entry) {
|
|
35
|
+
if (entry === undefined)
|
|
36
|
+
return [];
|
|
37
|
+
const directories = [];
|
|
38
|
+
let directory = resolve(dirname(realpathOf(entry)));
|
|
39
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
40
|
+
directories.push(directory);
|
|
41
|
+
const parent = dirname(directory);
|
|
42
|
+
if (parent === directory)
|
|
43
|
+
break;
|
|
44
|
+
directory = parent;
|
|
45
|
+
}
|
|
46
|
+
return directories;
|
|
47
|
+
}
|
|
48
|
+
function desktopDirectories() {
|
|
49
|
+
const { resourcesPath } = process;
|
|
50
|
+
if (typeof resourcesPath !== 'string' || resourcesPath.length === 0)
|
|
51
|
+
return [];
|
|
52
|
+
return APPLICATION_ROOTS.map(root => join(resourcesPath, root));
|
|
53
|
+
}
|
|
54
|
+
function declaredVersion(manifest) {
|
|
55
|
+
return typeof manifest?.version === 'string' && manifest.version !== '' ? manifest.version : 'unknown';
|
|
56
|
+
}
|
|
57
|
+
function flatDesktopHost(directory) {
|
|
58
|
+
const shell = readDshManifest(directory, DESKTOP_PACKAGE);
|
|
59
|
+
if (shell === null)
|
|
60
|
+
return null;
|
|
61
|
+
const runtime = DESKTOP_RUNTIME_PACKAGES.map(name => {
|
|
62
|
+
const candidate = join(directory, 'node_modules', '@deepseek-ai', name);
|
|
63
|
+
// Bundled pnpm links are fine; a link to a mutable profile/global package
|
|
64
|
+
// is not evidence about the version of the embedded runtime.
|
|
65
|
+
const path = relative(realpathOf(directory), realpathOf(join(candidate, 'package.json')));
|
|
66
|
+
if (isAbsolute(path) || path === '..' || path.startsWith(`..${sep}`))
|
|
67
|
+
return null;
|
|
68
|
+
return readDshManifest(candidate, `@deepseek-ai/${name}`);
|
|
69
|
+
});
|
|
70
|
+
// A shell alone is not a dependency anchor. Either in-box bundle can
|
|
71
|
+
// establish the directory even if the other version witnesses are broken.
|
|
72
|
+
if (runtime[0] === null && runtime[1] === null)
|
|
73
|
+
return null;
|
|
74
|
+
const version = declaredVersion(shell);
|
|
75
|
+
// DSH split packages share a release line, but a desktop shell need not.
|
|
76
|
+
// Require agreement from every witness; missing/conflicting evidence is
|
|
77
|
+
// unknown, not the shell version or a majority vote among split packages.
|
|
78
|
+
const parsed = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
|
|
79
|
+
const corroborated = parsed !== null && !(parsed[1]?.split('.').some(part => /^0\d+$/.test(part)) ?? false)
|
|
80
|
+
&& runtime.every(manifest => manifest?.version === version);
|
|
81
|
+
return { directory, version: corroborated ? version : 'unknown' };
|
|
32
82
|
}
|
|
33
83
|
/**
|
|
34
84
|
* The version of the DSH host this market is running inside.
|
|
35
85
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
86
|
+
* CLI versions come from the host manifest. Flat Desktop shells additionally
|
|
87
|
+
* require agreement with their bundled split runtime, never profile packages.
|
|
38
88
|
*
|
|
39
89
|
* Worth reporting because the host version has repeatedly been the thing
|
|
40
90
|
* neither side could see. #293 turned on it (the reporter was on
|
|
@@ -51,17 +101,20 @@ function isDshPackage(directory) {
|
|
|
51
101
|
* legitimately land here).
|
|
52
102
|
*/
|
|
53
103
|
export function dshHostInfo(entry = process.argv[1]) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
104
|
+
const ancestors = entryDirectories(entry);
|
|
105
|
+
const applications = desktopDirectories();
|
|
106
|
+
// Keep the entire legacy search order ahead of newly supported shells.
|
|
107
|
+
for (const directory of [...ancestors, ...applications.map(root => join(root, 'node_modules', '@deepseek-ai', 'dsh'))]) {
|
|
108
|
+
const manifest = readDshManifest(directory);
|
|
109
|
+
if (manifest !== null)
|
|
110
|
+
return { directory, version: declaredVersion(manifest) };
|
|
111
|
+
}
|
|
112
|
+
for (const directory of [...ancestors, ...applications]) {
|
|
113
|
+
const host = flatDesktopHost(directory);
|
|
114
|
+
if (host !== null)
|
|
115
|
+
return host;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
65
118
|
}
|
|
66
119
|
/**
|
|
67
120
|
* Walk up from the CLI entry first, then inspect Electron's authoritative
|
|
@@ -84,25 +137,5 @@ export function dshHostInfo(entry = process.argv[1]) {
|
|
|
84
137
|
* why this survived: the case that worked is the one that gets tested.
|
|
85
138
|
*/
|
|
86
139
|
export function findDshInstallDir(entry = process.argv[1]) {
|
|
87
|
-
|
|
88
|
-
let directory = resolve(dirname(realpathOf(entry)));
|
|
89
|
-
for (let depth = 0; depth < 10; depth += 1) {
|
|
90
|
-
if (isDshPackage(directory))
|
|
91
|
-
return directory;
|
|
92
|
-
const parent = dirname(directory);
|
|
93
|
-
if (parent === directory)
|
|
94
|
-
break;
|
|
95
|
-
directory = parent;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
const electronProcess = process;
|
|
99
|
-
if (typeof electronProcess.resourcesPath !== 'string'
|
|
100
|
-
|| electronProcess.resourcesPath.length === 0)
|
|
101
|
-
return null;
|
|
102
|
-
for (const applicationRoot of ['app.asar.unpacked', 'app.asar', 'app']) {
|
|
103
|
-
const candidate = join(electronProcess.resourcesPath, applicationRoot, 'node_modules', '@deepseek-ai', 'dsh');
|
|
104
|
-
if (isDshPackage(candidate))
|
|
105
|
-
return candidate;
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
140
|
+
return dshHostInfo(entry)?.directory ?? null;
|
|
108
141
|
}
|