dshmarket 1.47.0 → 1.49.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/UPDATE-API-V1.md +62 -0
- package/client/client.js +142 -35
- package/lib/dsh-cli.js +73 -2
- package/lib/dsh-install.js +74 -41
- package/lib/install.js +51 -3
- package/lib/pnpm-compat.js +6 -3
- package/lib/restart.js +15 -1
- package/lib/routes.js +199 -41
- package/lib/types/dsh-cli.d.ts +51 -0
- package/lib/types/dsh-install.d.ts +2 -2
- package/lib/types/install.d.ts +50 -3
- package/lib/types/restart.d.ts +4 -0
- package/package.json +1 -1
- package/src/client/MarketSection.tsx +26 -4
- package/src/client/index.ts +66 -38
- package/src/client/locales.ts +2 -2
- package/src/client/market-element.ts +60 -0
- package/src/client/section-gate.ts +79 -0
- package/src/dsh-cli.ts +73 -2
- package/src/dsh-install.ts +69 -44
- package/src/install.ts +52 -2
- package/src/pnpm-compat.ts +6 -3
- package/src/restart.ts +15 -1
- package/src/routes.ts +201 -40
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
|
@@ -105,7 +105,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
105
105
|
installing: "安装中…",
|
|
106
106
|
installedBadge: "✓ 已安装",
|
|
107
107
|
alreadyInstalled: "✓ 已安装",
|
|
108
|
-
restartBanner: "
|
|
108
|
+
restartBanner: "项变更需重启 DeepSeek Harness 后生效",
|
|
109
109
|
uninstall: "卸载",
|
|
110
110
|
confirmRemove: "确认卸载?",
|
|
111
111
|
uninstalling: "卸载中…",
|
|
@@ -638,7 +638,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
638
638
|
installing: "Installing…",
|
|
639
639
|
installedBadge: "✓ Installed",
|
|
640
640
|
alreadyInstalled: "✓ Installed",
|
|
641
|
-
restartBanner: "change(s)
|
|
641
|
+
restartBanner: "change(s) pending restart — restart DeepSeek Harness to apply",
|
|
642
642
|
uninstall: "Uninstall",
|
|
643
643
|
confirmRemove: "Uninstall?",
|
|
644
644
|
uninstalling: "Uninstalling…",
|
|
@@ -6348,6 +6348,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
6348
6348
|
const [buildsSkipped, setBuildsSkipped] = (0, react.useState)(null);
|
|
6349
6349
|
const [updatingAll, setUpdatingAll] = (0, react.useState)(false);
|
|
6350
6350
|
const [updatedNames, setUpdatedNames] = (0, react.useState)([]);
|
|
6351
|
+
const [restartNames, setRestartNames] = (0, react.useState)([]);
|
|
6351
6352
|
const [hotUrls, setHotUrls] = (0, react.useState)([]);
|
|
6352
6353
|
const [hotNames, setHotNames] = (0, react.useState)([]);
|
|
6353
6354
|
const [progressLine, setProgressLine] = (0, react.useState)(null);
|
|
@@ -6667,12 +6668,13 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
6667
6668
|
}
|
|
6668
6669
|
if (Array.isArray(saved.doneUrls) && saved.doneUrls.length > 0) setDoneUrls(saved.doneUrls);
|
|
6669
6670
|
if (Array.isArray(saved.updated) && saved.updated.length > 0) setUpdatedNames(saved.updated);
|
|
6671
|
+
if (Array.isArray(saved.restartNames) && saved.restartNames.length > 0) setRestartNames(saved.restartNames);
|
|
6670
6672
|
if (typeof saved.removed === "number" && saved.removed > 0) setRemovedCount(saved.removed);
|
|
6671
6673
|
if (typeof saved.toggled === "number" && saved.toggled > 0) setToggleRestart(saved.toggled);
|
|
6672
6674
|
}, [bootId]);
|
|
6673
6675
|
(0, react.useEffect)(() => {
|
|
6674
6676
|
if (bootId === null) return;
|
|
6675
|
-
if (doneUrls.length === 0 && updatedNames.length === 0 && removedCount === 0 && toggleRestart === 0) {
|
|
6677
|
+
if (doneUrls.length === 0 && updatedNames.length === 0 && restartNames.length === 0 && removedCount === 0 && toggleRestart === 0) {
|
|
6676
6678
|
sessionStorage.removeItem("dshm-restart");
|
|
6677
6679
|
return;
|
|
6678
6680
|
}
|
|
@@ -6680,6 +6682,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
6680
6682
|
boot: bootId,
|
|
6681
6683
|
doneUrls,
|
|
6682
6684
|
updated: updatedNames,
|
|
6685
|
+
restartNames,
|
|
6683
6686
|
removed: removedCount,
|
|
6684
6687
|
toggled: toggleRestart
|
|
6685
6688
|
}));
|
|
@@ -6687,6 +6690,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
6687
6690
|
bootId,
|
|
6688
6691
|
doneUrls,
|
|
6689
6692
|
updatedNames,
|
|
6693
|
+
restartNames,
|
|
6690
6694
|
removedCount,
|
|
6691
6695
|
toggleRestart
|
|
6692
6696
|
]);
|
|
@@ -7307,6 +7311,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7307
7311
|
if (status === 200 && body.ok) {
|
|
7308
7312
|
setRecords((list) => patch(list, updateRecordId, { state: "done" }));
|
|
7309
7313
|
setUpdatedNames((names) => names.concat(name));
|
|
7314
|
+
const activation = body.activation && typeof body.activation === "object" ? body.activation[name] : void 0;
|
|
7315
|
+
if (!activation || activation.state === "restart") setRestartNames((names) => names.includes(name) ? names : names.concat(name));
|
|
7310
7316
|
if (body.activation && typeof body.activation === "object") setActivations((prev) => ({
|
|
7311
7317
|
...prev,
|
|
7312
7318
|
...body.activation
|
|
@@ -7854,7 +7860,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7854
7860
|
});
|
|
7855
7861
|
}, [doGroupAction, groups]);
|
|
7856
7862
|
const selfName = installed["dshmarket"] !== void 0 ? "dshmarket" : "dsh-market";
|
|
7857
|
-
const updatableNames = Object.keys(installed).filter((name) => name !== selfName && !updatedNames.includes(name) && updates[name] && updates[name].updateAvailable);
|
|
7863
|
+
const updatableNames = Object.keys(installed).filter((name) => name !== selfName && !updatedNames.includes(name) && !effectiveDisabledSet.has(name) && updates[name] && updates[name].updateAvailable);
|
|
7858
7864
|
const batchUpdatableNames = updatableNames.filter((name) => updates[name]?.restoreRequired !== true);
|
|
7859
7865
|
const ignoredUpdateSet = (0, react.useMemo)(() => new Set(ignoredUpdateNames), [ignoredUpdateNames]);
|
|
7860
7866
|
const reminderUpdatableNames = updatableNames.filter((name) => !ignoredUpdateSet.has(name));
|
|
@@ -8102,7 +8108,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
8102
8108
|
webdavUrl,
|
|
8103
8109
|
webdavUser
|
|
8104
8110
|
]);
|
|
8105
|
-
const sessionPendingRestart = doneUrls.length +
|
|
8111
|
+
const sessionPendingRestart = doneUrls.length + restartNames.length + removedCount + toggleRestart + (backupRestored ? 1 : 0);
|
|
8106
8112
|
/**
|
|
8107
8113
|
* Plugins the HOST reports as restart-pending, independent of what this
|
|
8108
8114
|
* browser session happens to remember. Installing and then reloading the
|
|
@@ -10720,6 +10726,86 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10720
10726
|
});
|
|
10721
10727
|
}
|
|
10722
10728
|
//#endregion
|
|
10729
|
+
//#region src/client/market-element.ts
|
|
10730
|
+
/**
|
|
10731
|
+
* The market's panel, as an element, built from explicit dependencies.
|
|
10732
|
+
*
|
|
10733
|
+
* Two callers want the same thing and must not drift apart:
|
|
10734
|
+
*
|
|
10735
|
+
* - the `settings.section` this package registers, whose slot passes down a
|
|
10736
|
+
* host-chosen `preferredSubsectionId`;
|
|
10737
|
+
* - `market.render()`, for a host shell that renders the market inside its
|
|
10738
|
+
* own container (#602, from the Tauri desktop).
|
|
10739
|
+
*
|
|
10740
|
+
* A module function taking its dependencies rather than a closure over the
|
|
10741
|
+
* cordis context, so the wiring — which locale, which theme, which log
|
|
10742
|
+
* exporter reaches the panel — is something a test can assert instead of
|
|
10743
|
+
* something only a running host can reveal.
|
|
10744
|
+
*/
|
|
10745
|
+
/**
|
|
10746
|
+
* @param props - see {@link MarketElementProps}.
|
|
10747
|
+
* @returns the panel wrapped in its error boundary.
|
|
10748
|
+
*/
|
|
10749
|
+
function marketElement(props) {
|
|
10750
|
+
return (0, react.createElement)(MarketErrorBoundary, {
|
|
10751
|
+
text: props.crashText,
|
|
10752
|
+
actions: (0, react.createElement)("button", {
|
|
10753
|
+
type: "button",
|
|
10754
|
+
onClick: () => {
|
|
10755
|
+
props.exportLog();
|
|
10756
|
+
}
|
|
10757
|
+
}, props.t("exportLog"))
|
|
10758
|
+
}, (0, react.createElement)(MarketSection, {
|
|
10759
|
+
t: props.t,
|
|
10760
|
+
locale: props.locale,
|
|
10761
|
+
theme: props.theme,
|
|
10762
|
+
themeStore: props.themeStore,
|
|
10763
|
+
preferredSubsectionId: props.preferredSubsectionId
|
|
10764
|
+
}));
|
|
10765
|
+
}
|
|
10766
|
+
//#endregion
|
|
10767
|
+
//#region src/client/section-gate.ts
|
|
10768
|
+
/**
|
|
10769
|
+
* @param register - registers the entry and returns its disposer. Called
|
|
10770
|
+
* only when the entry should be visible; may be called again after a
|
|
10771
|
+
* retraction, which is why the disposer is required rather than optional.
|
|
10772
|
+
* @returns the gate.
|
|
10773
|
+
*/
|
|
10774
|
+
function createSectionGate(register) {
|
|
10775
|
+
let ready = false;
|
|
10776
|
+
let wanted = true;
|
|
10777
|
+
let removed = false;
|
|
10778
|
+
let dispose = null;
|
|
10779
|
+
const apply = () => {
|
|
10780
|
+
if (!ready) return;
|
|
10781
|
+
const shouldShow = wanted && !removed;
|
|
10782
|
+
if (shouldShow && dispose === null) {
|
|
10783
|
+
dispose = register();
|
|
10784
|
+
return;
|
|
10785
|
+
}
|
|
10786
|
+
if (!shouldShow && dispose !== null) {
|
|
10787
|
+
const stop = dispose;
|
|
10788
|
+
dispose = null;
|
|
10789
|
+
stop();
|
|
10790
|
+
}
|
|
10791
|
+
};
|
|
10792
|
+
return {
|
|
10793
|
+
available: () => {
|
|
10794
|
+
ready = true;
|
|
10795
|
+
apply();
|
|
10796
|
+
},
|
|
10797
|
+
setVisible: (visible) => {
|
|
10798
|
+
wanted = visible;
|
|
10799
|
+
apply();
|
|
10800
|
+
},
|
|
10801
|
+
visible: () => dispose !== null,
|
|
10802
|
+
retire: () => {
|
|
10803
|
+
removed = true;
|
|
10804
|
+
apply();
|
|
10805
|
+
}
|
|
10806
|
+
};
|
|
10807
|
+
}
|
|
10808
|
+
//#endregion
|
|
10723
10809
|
//#region src/client/SettingsCard.tsx
|
|
10724
10810
|
/**
|
|
10725
10811
|
* The market's card on the plugin configuration page (dsh >= 0.1.0-rc.7).
|
|
@@ -11210,8 +11296,32 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11210
11296
|
en
|
|
11211
11297
|
}), "dsh-market: dictionaries");
|
|
11212
11298
|
const t = ctx.locale.bind(NS);
|
|
11213
|
-
|
|
11214
|
-
|
|
11299
|
+
/**
|
|
11300
|
+
* The market's own panel, as an element — one builder for the settings
|
|
11301
|
+
* section this package registers and for `market.render()` (#602). Built
|
|
11302
|
+
* per call: the props are live (locale, theme, the host's preferred
|
|
11303
|
+
* subsection), and a cached element would freeze the first caller's.
|
|
11304
|
+
*/
|
|
11305
|
+
const buildMarketElement = (ownerProps = {}) => marketElement({
|
|
11306
|
+
t,
|
|
11307
|
+
locale: ctx.locale,
|
|
11308
|
+
theme: ctx.theme,
|
|
11309
|
+
themeStore: {
|
|
11310
|
+
subscribe: (cb) => ctx.on("theme/change", cb),
|
|
11311
|
+
getSnapshot: () => ctx.theme.getTheme()
|
|
11312
|
+
},
|
|
11313
|
+
crashText: {
|
|
11314
|
+
title: t("crashTitle"),
|
|
11315
|
+
hint: t("crashHint"),
|
|
11316
|
+
reload: t("crashReload"),
|
|
11317
|
+
details: t("crashDetails")
|
|
11318
|
+
},
|
|
11319
|
+
exportLog: () => {
|
|
11320
|
+
exportMarketLog().catch(() => {});
|
|
11321
|
+
},
|
|
11322
|
+
preferredSubsectionId: ownerProps.preferredSubsectionId
|
|
11323
|
+
});
|
|
11324
|
+
const sectionGate = createSectionGate(() => {
|
|
11215
11325
|
const off = ctx.slots.register({
|
|
11216
11326
|
name: "settings.section",
|
|
11217
11327
|
id: "market",
|
|
@@ -11219,32 +11329,31 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11219
11329
|
label: () => t("nav"),
|
|
11220
11330
|
locale: NS,
|
|
11221
11331
|
inject: () => ({ t })
|
|
11222
|
-
}, (ownerProps = {}) => (
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
details: t("crashDetails")
|
|
11228
|
-
},
|
|
11229
|
-
actions: (0, react.createElement)("button", {
|
|
11230
|
-
type: "button",
|
|
11231
|
-
onClick: () => {
|
|
11232
|
-
exportMarketLog().catch(() => {});
|
|
11233
|
-
}
|
|
11234
|
-
}, t("exportLog"))
|
|
11235
|
-
}, (0, react.createElement)(MarketSection, {
|
|
11236
|
-
t,
|
|
11237
|
-
locale: ctx.locale,
|
|
11238
|
-
theme: ctx.theme,
|
|
11239
|
-
themeStore: {
|
|
11240
|
-
subscribe: (cb) => ctx.on("theme/change", cb),
|
|
11241
|
-
getSnapshot: () => ctx.theme.getTheme()
|
|
11242
|
-
},
|
|
11243
|
-
preferredSubsectionId: ownerProps.preferredSubsectionId
|
|
11244
|
-
})));
|
|
11245
|
-
if (typeof off === "function") retireSection = off;
|
|
11246
|
-
return off;
|
|
11332
|
+
}, (ownerProps = {}) => buildMarketElement(ownerProps));
|
|
11333
|
+
return typeof off === "function" ? off : () => {};
|
|
11334
|
+
});
|
|
11335
|
+
ctx.slots.inject("settings.section", () => {
|
|
11336
|
+
sectionGate.available();
|
|
11247
11337
|
});
|
|
11338
|
+
const marketControl = {
|
|
11339
|
+
version: 1,
|
|
11340
|
+
setSettingsVisible: (visible) => {
|
|
11341
|
+
sectionGate.setVisible(visible);
|
|
11342
|
+
},
|
|
11343
|
+
settingsVisible: () => sectionGate.visible(),
|
|
11344
|
+
/**
|
|
11345
|
+
* The market's panel as an element, for a host that renders it inside
|
|
11346
|
+
* its own container. Same page, same React instance — this package's
|
|
11347
|
+
* bundle resolves react through the host's module table, so an element
|
|
11348
|
+
* returned here mounts anywhere in that tree.
|
|
11349
|
+
*
|
|
11350
|
+
* What it is NOT: a way to rearrange the market. It hands over the whole
|
|
11351
|
+
* panel, chrome included. Cutting the market into host-fillable regions
|
|
11352
|
+
* is a different design and has not been asked for by a second host yet.
|
|
11353
|
+
*/
|
|
11354
|
+
render: (props = {}) => buildMarketElement(props)
|
|
11355
|
+
};
|
|
11356
|
+
if (typeof ctx.provide === "function") ctx.provide("market", marketControl);
|
|
11248
11357
|
ctx.inject(["settingsScope"], (scoped) => {
|
|
11249
11358
|
scoped.slots.inject("settings.plugin.item", () => scoped.slots.register({
|
|
11250
11359
|
name: "settings.plugin.item",
|
|
@@ -11254,9 +11363,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11254
11363
|
}, () => (0, react.createElement)(SettingsCard, {
|
|
11255
11364
|
t,
|
|
11256
11365
|
onRemoved: () => {
|
|
11257
|
-
|
|
11258
|
-
retireSection = null;
|
|
11259
|
-
off?.();
|
|
11366
|
+
sectionGate.retire();
|
|
11260
11367
|
}
|
|
11261
11368
|
})));
|
|
11262
11369
|
});
|
package/lib/dsh-cli.js
CHANGED
|
@@ -192,6 +192,71 @@ 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
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Every `--config.<key>=<value>` override in the argv, repeated as the
|
|
235
|
+
* `PNPM_CONFIG_<KEY>` environment variable that pnpm 12 still reads.
|
|
236
|
+
*
|
|
237
|
+
* pnpm 12 ignores some `--config.<key>` overrides on the command line, in
|
|
238
|
+
* either spelling, without a word: `fetchTimeout` (#615; measured on
|
|
239
|
+
* 12.2.1, 12.3.0 and 12.4.1) and `auto-install-peers` (12.4.1 auto-installs
|
|
240
|
+
* the peer with the flag present and not with the variable), so the retries
|
|
241
|
+
* that carry them ran exactly like the first attempt. `PNPM_CONFIG_*` is
|
|
242
|
+
* honoured by 11.8, 11.21 and 12.4 alike, so the argument stays for the
|
|
243
|
+
* versions that read it and the variable carries the same value for the
|
|
244
|
+
* ones that do not. Scoped to the run that carries the flag; nothing is set
|
|
245
|
+
* otherwise, so a user's own values are untouched on every other run. Keys
|
|
246
|
+
* are accepted in either spelling, so respelling a constant (as #600 did
|
|
247
|
+
* for the release-age one) cannot silently drop the variable.
|
|
248
|
+
*/
|
|
249
|
+
export function pnpmConfigEnvForArgs(pluginArgs) {
|
|
250
|
+
const env = {};
|
|
251
|
+
for (const arg of pluginArgs) {
|
|
252
|
+
const match = /^--config\.([A-Za-z][A-Za-z0-9-]*)=(\S+)$/.exec(arg);
|
|
253
|
+
if (match === null)
|
|
254
|
+
continue;
|
|
255
|
+
const key = match[1].replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toUpperCase();
|
|
256
|
+
env[`PNPM_CONFIG_${key}`] = match[2];
|
|
257
|
+
}
|
|
258
|
+
return env;
|
|
259
|
+
}
|
|
195
260
|
function spawnEnv() {
|
|
196
261
|
// pnpm v10+ blocks forever on a silent interactive prompt without a TTY;
|
|
197
262
|
// CI mode forces it to act or fail instead of asking.
|
|
@@ -201,7 +266,13 @@ function spawnEnv() {
|
|
|
201
266
|
if (!parts.includes(bin))
|
|
202
267
|
parts.push(bin);
|
|
203
268
|
}
|
|
204
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
...process.env,
|
|
271
|
+
...proxyEnvForPnpm(process.env, activeRegion()),
|
|
272
|
+
...gitEnvForPnpm(process.env),
|
|
273
|
+
CI: 'true',
|
|
274
|
+
PATH: parts.join(separator),
|
|
275
|
+
};
|
|
205
276
|
}
|
|
206
277
|
const INSTALL_TIMEOUT_MS = Number(process.env.DSH_MARKET_INSTALL_TIMEOUT_MS) || 15 * 60 * 1000;
|
|
207
278
|
/**
|
|
@@ -695,7 +766,7 @@ export function runDshPlugin(profile, pluginArgs) {
|
|
|
695
766
|
// pnpm v10 blocks forever on a silent interactive prompt without a TTY
|
|
696
767
|
// (observed on re-add over a pinned git spec); CI mode forces it to act
|
|
697
768
|
// or fail instead of asking.
|
|
698
|
-
env: spawnEnv(),
|
|
769
|
+
env: { ...spawnEnv(), ...pnpmConfigEnvForArgs(pluginArgs) },
|
|
699
770
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
700
771
|
viaShell,
|
|
701
772
|
// Own process group on POSIX so cancel/timeout can kill the whole
|
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
|
}
|