dshmarket 1.59.0 → 1.60.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/client/client.js +12 -1
- package/lib/check.js +13 -1
- package/lib/pnpm-compat.js +15 -0
- package/lib/routes.js +79 -5
- package/lib/types/pnpm-compat.d.ts +1 -1
- package/package.json +1 -1
- package/src/check.ts +13 -1
- package/src/client/MarketSection.tsx +5 -2
- package/src/pnpm-compat.ts +16 -1
- package/src/routes.ts +76 -5
package/client/client.js
CHANGED
|
@@ -7139,6 +7139,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7139
7139
|
* real switch state so hand-edited cordis.patch.yml toggles are visible.
|
|
7140
7140
|
*/
|
|
7141
7141
|
const [patchDisabledNames, setPatchDisabledNames] = (0, react.useState)([]);
|
|
7142
|
+
/** Bundle packages DSH's own plugin page turned off by leaving them out of dsh.profile.bundles (#696). */
|
|
7143
|
+
const [unbundledNames, setUnbundledNames] = (0, react.useState)([]);
|
|
7142
7144
|
const [groups, setGroups] = (0, react.useState)({});
|
|
7143
7145
|
const [groupOrder, setGroupOrder] = (0, react.useState)([]);
|
|
7144
7146
|
/** Installed-tab sub-view: flat list or groups (All-plugins was removed —
|
|
@@ -7309,6 +7311,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7309
7311
|
}
|
|
7310
7312
|
if (body.notes !== null && typeof body.notes === "object" && !Array.isArray(body.notes)) setNotes(body.notes);
|
|
7311
7313
|
if (Array.isArray(body.patchDisabled)) setPatchDisabledNames(body.patchDisabled);
|
|
7314
|
+
if (Array.isArray(body.unbundled)) setUnbundledNames(body.unbundled);
|
|
7312
7315
|
if (body.groups && typeof body.groups === "object") setGroups(body.groups);
|
|
7313
7316
|
if (Array.isArray(body.groupOrder)) setGroupOrder(body.groupOrder);
|
|
7314
7317
|
if (Array.isArray(body.favorites)) setFavoriteUrls(body.favorites.filter((url) => typeof url === "string"));
|
|
@@ -7324,7 +7327,15 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7324
7327
|
(0, react.useMemo)(() => new Set(disabledNames), [disabledNames]);
|
|
7325
7328
|
const favoriteUrlSet = (0, react.useMemo)(() => new Set(favoriteUrls), [favoriteUrls]);
|
|
7326
7329
|
/** Effective switch state: market disable list ∪ user-patch-layer disables. */
|
|
7327
|
-
const effectiveDisabledSet = (0, react.useMemo)(() => /* @__PURE__ */ new Set([
|
|
7330
|
+
const effectiveDisabledSet = (0, react.useMemo)(() => /* @__PURE__ */ new Set([
|
|
7331
|
+
...disabledNames,
|
|
7332
|
+
...patchDisabledNames,
|
|
7333
|
+
...unbundledNames
|
|
7334
|
+
]), [
|
|
7335
|
+
disabledNames,
|
|
7336
|
+
patchDisabledNames,
|
|
7337
|
+
unbundledNames
|
|
7338
|
+
]);
|
|
7328
7339
|
(0, react.useEffect)(() => {
|
|
7329
7340
|
if (tab !== "themes" && themesFullscreen) setThemesFullscreen(false);
|
|
7330
7341
|
}, [tab, themesFullscreen]);
|
package/lib/check.js
CHANGED
|
@@ -759,7 +759,19 @@ export function buildBundleLayers(profileDirectory, bundleNames, specs, dshInsta
|
|
|
759
759
|
// fatal verdict and rolled back a good update (#369) — while `dsh
|
|
760
760
|
// --dump-config` on the same profile exited 0. Unknown has to read as
|
|
761
761
|
// unknown; the profile's own bundles are still judged normally.
|
|
762
|
-
|
|
762
|
+
//
|
|
763
|
+
// The same holds for any official bundle while the installation itself
|
|
764
|
+
// cannot be located (#676): a desktop build ships more in-box bundles
|
|
765
|
+
// than the three named in INBOX_BUNDLES —
|
|
766
|
+
// `@deepseek-ai/dsh-experimental-agent-team-profile` among them — and a
|
|
767
|
+
// fixed list, or an install-path shape, cannot know which. Two
|
|
768
|
+
// reporters saw that bundle called "not installed — will fail to boot"
|
|
769
|
+
// while its three entries were active in the running host. Only
|
|
770
|
+
// DeepSeek publishes under `@deepseek-ai/`, so while the installation
|
|
771
|
+
// is out of sight such a bundle is unknown, not missing. When the
|
|
772
|
+
// installation IS located and the bundle is in neither it nor the
|
|
773
|
+
// profile, the fatal verdict below still applies.
|
|
774
|
+
if (hostProvided || (dshInstallDir === null && name.startsWith('@deepseek-ai/'))) {
|
|
763
775
|
layer.error = null;
|
|
764
776
|
layer.unresolvedInbox = true;
|
|
765
777
|
return layer;
|
package/lib/pnpm-compat.js
CHANGED
|
@@ -313,6 +313,21 @@ export function classifyPnpmFailure(output, exitCode) {
|
|
|
313
313
|
// pnpm's FETCHER, before anything lands in node_modules — so the package
|
|
314
314
|
// the user must approve is not installed yet, and pnpm's own hint names a
|
|
315
315
|
// commit-pinned codeload URL that changes on every push.
|
|
316
|
+
// #701: pnpm 12's native engine (pnpm-native) aborting on an allocation it
|
|
317
|
+
// cannot get — "memory allocation of 5368709120 bytes failed", Windows exit
|
|
318
|
+
// 3221226505 (0xC0000409, how a Rust abort ends there). Reported with an
|
|
319
|
+
// A/B: on pnpm 12.5.1 every run with `autoInstallPeers: false` in the
|
|
320
|
+
// workspace file (DSH writes it into every profile) aborted at ~5 GB peak
|
|
321
|
+
// RSS, every run without it passed at ~80 MB, with free memory to spare.
|
|
322
|
+
// Nothing about the plugin being installed; not something a retry of the
|
|
323
|
+
// same command changes on the reporter's data, so none is attempted.
|
|
324
|
+
if (/memory allocation of \d+ bytes failed/.test(output) || exitCode === 3221226505) {
|
|
325
|
+
return {
|
|
326
|
+
code: 'native-oom',
|
|
327
|
+
recoverable: false,
|
|
328
|
+
message: 'pnpm 12 的原生引擎在处理这个 profile 时耗尽了内存并中止——和要安装的插件无关。在 pnpm 修复之前,请改用 pnpm 11(npm install -g pnpm@11)后再试。 / pnpm 12\'s native engine ran out of memory on this profile and aborted — the plugin being installed is not the cause. Until pnpm fixes it, switch to pnpm 11 (npm install -g pnpm@11) and try again.',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
316
331
|
// #698: pnpm 10.26+ and 11.0–11.5 read an allowBuilds key as
|
|
317
332
|
// `name@<version union>`, so a git or archive source there fails the WHOLE
|
|
318
333
|
// workspace file — every pnpm command in the profile, not the one plugin.
|
package/lib/routes.js
CHANGED
|
@@ -177,6 +177,16 @@ function packageHasClientPart(profileDirectory, name) {
|
|
|
177
177
|
* the package, and the key written is the one pnpm printed, never text from
|
|
178
178
|
* the request.
|
|
179
179
|
*/
|
|
180
|
+
/** Whether an installed package's manifest declares a bundle. */
|
|
181
|
+
function declaresBundle(profileDirectory, name) {
|
|
182
|
+
try {
|
|
183
|
+
const manifest = JSON.parse(readFileSync(join(profileDirectory, 'node_modules', name, 'package.json'), 'utf8'));
|
|
184
|
+
return manifest.dsh?.bundle !== undefined;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
180
190
|
const prepareRefusals = new Map();
|
|
181
191
|
function blockedBuilds(result) {
|
|
182
192
|
if (Array.isArray(result.ignoredBuilds) && result.ignoredBuilds.length > 0)
|
|
@@ -365,6 +375,34 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
365
375
|
marketState.githubProxy = fresh.githubProxy;
|
|
366
376
|
setCustomGithubProxy(fresh.githubProxy ?? null);
|
|
367
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Follow an enable made OUTSIDE the market (#696).
|
|
380
|
+
*
|
|
381
|
+
* The market keeps its own disable list in state.json, and the self-heal
|
|
382
|
+
* guard below and the boot replay put any plugin on that list back down.
|
|
383
|
+
* DSH's own Settings → Plugins page enables a row by flipping it to
|
|
384
|
+
* `disabled: false` in the shared patch layer — it has never heard of
|
|
385
|
+
* state.json — so its enable was silently undone within a second and again
|
|
386
|
+
* on every boot, and the official switch looked broken.
|
|
387
|
+
*
|
|
388
|
+
* The patch layer is the one truth both managers share. A row explicitly
|
|
389
|
+
* enabled there while no row of the same package is disabled can only be a
|
|
390
|
+
* newer decision than the market's list (the market removes a name from
|
|
391
|
+
* its list BEFORE writing its own enable row), so the list follows it.
|
|
392
|
+
*
|
|
393
|
+
* @returns true when the package was re-enabled elsewhere and has been
|
|
394
|
+
* dropped from the market's disable list.
|
|
395
|
+
*/
|
|
396
|
+
function followOutsideEnable(name) {
|
|
397
|
+
const patch = readUserPatchState(userPatchPath);
|
|
398
|
+
const flags = packagePatchFlags(host, activeProfileDir, [name], patch);
|
|
399
|
+
if (!flags.forced.includes(name) || flags.disabled.includes(name))
|
|
400
|
+
return false;
|
|
401
|
+
disabled.delete(name);
|
|
402
|
+
writeMarketState(activeProfileDir, { disabled, groups, groupOrder });
|
|
403
|
+
logEvent('info', 'toggle', `${name}: its rows were re-enabled outside the market (#696) — following that instead of switching it back off`);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
368
406
|
// Client-only packages (dsh.client without dsh.bundle) are invisible to the
|
|
369
407
|
// bundle layer in every boot; the market shim-mounts them so their client
|
|
370
408
|
// bundles are actually served.
|
|
@@ -375,7 +413,9 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
375
413
|
// switched away from get live-disabled again (bundle trees are
|
|
376
414
|
// in-memory, so the disable never persists on its own). Client-only
|
|
377
415
|
// shims for disabled plugins were already skipped by mountClientOnlyDeps.
|
|
378
|
-
for (const name of disabled) {
|
|
416
|
+
for (const name of [...disabled]) {
|
|
417
|
+
if (followOutsideEnable(name))
|
|
418
|
+
continue;
|
|
379
419
|
if (await themes.setEntryDisabled(name, true))
|
|
380
420
|
logEvent('info', 'boot', `plugin kept off: ${name}`);
|
|
381
421
|
}
|
|
@@ -385,7 +425,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
385
425
|
// up for a plugin the user switched off, put it back down.
|
|
386
426
|
host.on?.('internal/plugin', (fiber) => {
|
|
387
427
|
const name = fiber.entry?.options?.name;
|
|
388
|
-
if (name !== undefined && disabled.has(name))
|
|
428
|
+
if (name !== undefined && disabled.has(name) && !followOutsideEnable(name))
|
|
389
429
|
void themes.setEntryDisabled(name, true);
|
|
390
430
|
});
|
|
391
431
|
let installing = false;
|
|
@@ -1806,8 +1846,20 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
1806
1846
|
return typeof manifest === 'object' && manifest !== null
|
|
1807
1847
|
&& manifest.dsh !== undefined;
|
|
1808
1848
|
};
|
|
1849
|
+
// A package that declares a bundle but is not in dsh.profile.bundles
|
|
1850
|
+
// is not composed at boot: DSH's own plugin page turns a package off
|
|
1851
|
+
// by removing it from that list, and nothing the market recorded
|
|
1852
|
+
// says so (#696). Reported as off, unless it is live right now — a
|
|
1853
|
+
// hot mount from this session still runs until the next restart.
|
|
1854
|
+
const composedBundles = new Set(readProfileBundles(activeProfileDir));
|
|
1855
|
+
const unbundled = Object.keys(installed).filter(name => {
|
|
1856
|
+
if (INBOX_BUNDLES.has(name) || composedBundles.has(name) || live.has(name))
|
|
1857
|
+
return false;
|
|
1858
|
+
const manifest = installedManifests.get(name);
|
|
1859
|
+
return typeof manifest === 'object' && manifest !== null && manifest.dsh?.bundle !== undefined;
|
|
1860
|
+
});
|
|
1809
1861
|
for (const name of Object.keys(installed)) {
|
|
1810
|
-
const result = activationAfterReplace(verifyActivation(config.profile, name, live, activeProfileDir, disabled.has(name) || patchFlags.disabled.includes(name)), replacedWhileLive.has(name));
|
|
1862
|
+
const result = activationAfterReplace(verifyActivation(config.profile, name, live, activeProfileDir, disabled.has(name) || patchFlags.disabled.includes(name) || unbundled.includes(name)), replacedWhileLive.has(name));
|
|
1811
1863
|
// A package with no dsh surface of its own, outside the bundle
|
|
1812
1864
|
// layer, that another installed package declares, is that package's
|
|
1813
1865
|
// library rather than a plugin that failed to start (#634). Saying
|
|
@@ -1843,6 +1895,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
1843
1895
|
favorites: readMarketState(activeProfileDir).favorites ?? [],
|
|
1844
1896
|
patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts },
|
|
1845
1897
|
patchDisabled: patchFlags.disabled,
|
|
1898
|
+
unbundled,
|
|
1846
1899
|
patchForced: patchFlags.forced,
|
|
1847
1900
|
bundles: readProfileBundles(activeProfileDir).filter(name => !INBOX_BUNDLES.has(name)),
|
|
1848
1901
|
});
|
|
@@ -2257,15 +2310,25 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
2257
2310
|
// disabled) is NOT dropped: #147 requires disabling it to leave the
|
|
2258
2311
|
// neighbour live, and the e2e fixture-cross re-enable breaks otherwise.
|
|
2259
2312
|
const disablesOthers = carrierDisableIds(activeProfileDir, name);
|
|
2313
|
+
// Turned off by DSH's own plugin page, which removes a package from
|
|
2314
|
+
// dsh.profile.bundles (#696): enabling it here has to put it back,
|
|
2315
|
+
// or the patch rows flip, the switch reads on, and nothing composes
|
|
2316
|
+
// it on the next boot. Unlike a carrier this does NOT force a
|
|
2317
|
+
// restart: the enable below still brings it up in this process, and
|
|
2318
|
+
// whether a restart is needed is whether it is live afterwards.
|
|
2319
|
+
const reBundle = enabled && disablesOthers.length === 0 && !INBOX_BUNDLES.has(name)
|
|
2320
|
+
&& declaresBundle(activeProfileDir, name) && !readProfileBundles(activeProfileDir).includes(name);
|
|
2260
2321
|
const isCarrier = disablesOthers.length > 0;
|
|
2261
2322
|
let bundleSwitch = { ok: true, reason: null };
|
|
2262
|
-
if (isCarrier) {
|
|
2323
|
+
if (isCarrier || reBundle) {
|
|
2263
2324
|
try {
|
|
2264
2325
|
if (enabled)
|
|
2265
2326
|
addProfileBundle(activeProfileDir, name);
|
|
2266
2327
|
else
|
|
2267
2328
|
removeProfileBundle(activeProfileDir, name);
|
|
2268
|
-
logEvent('info', 'toggle',
|
|
2329
|
+
logEvent('info', 'toggle', reBundle
|
|
2330
|
+
? `${name}: re-added to dsh.profile.bundles, which another manager had removed it from (#696)`
|
|
2331
|
+
: `${name}: disable-carrier ${enabled ? 're-added to' : 'removed from'} dsh.profile.bundles (disables: ${disablesOthers.join(', ')})`);
|
|
2269
2332
|
}
|
|
2270
2333
|
catch (error) {
|
|
2271
2334
|
bundleSwitch = { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -4632,6 +4695,12 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
4632
4695
|
// keep their partial state on purpose (the user sees the diff
|
|
4633
4696
|
// and decides).
|
|
4634
4697
|
const manifestBefore = readProfileManifestSnapshot(config.profile, activeProfileDir);
|
|
4698
|
+
// The lockfile too (#701): pnpm writes it before it links, so a
|
|
4699
|
+
// run that dies in between — a native crash, a kill — leaves a
|
|
4700
|
+
// lock that names a package the manifest never got. The update
|
|
4701
|
+
// route has always restored both; a fresh install restored only
|
|
4702
|
+
// the manifest and left the half for the next pnpm run to trip on.
|
|
4703
|
+
const lockfileBefore = captureProfileLockfile();
|
|
4635
4704
|
const pinned = target === pinnedTarget && pinnedTarget !== plainTarget;
|
|
4636
4705
|
let result = await (pinned ? runPluginKeepingReleaseAge : runPlugin)(config.profile, ['add', target]);
|
|
4637
4706
|
// Two ways a pinned add can fail that a bare add would not, and
|
|
@@ -4666,6 +4735,11 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
|
|
|
4666
4735
|
const rolledBack = restoreProfileManifest(config.profile, manifestBefore, activeProfileDir);
|
|
4667
4736
|
if (rolledBack.length > 0)
|
|
4668
4737
|
logEvent('warn', 'install', `${target}: rolled back manifest residue of the failed run: ${rolledBack.join(', ')}`);
|
|
4738
|
+
if (lockfileBefore.ok) {
|
|
4739
|
+
const lock = restoreProfileLockfile(lockfileBefore.snapshot);
|
|
4740
|
+
if (!lock.ok)
|
|
4741
|
+
logEvent('warn', 'install', `${target}: could not restore pnpm-lock.yaml after the failed run: ${lock.detail ?? 'unknown'}`);
|
|
4742
|
+
}
|
|
4669
4743
|
}
|
|
4670
4744
|
let ok = result.exitCode === 0 && !result.timedOut && !cancelled;
|
|
4671
4745
|
const cancelDiff = cancelled ? changedSince(beforeSpecs) : null;
|
|
@@ -33,7 +33,7 @@ export declare function pluginArgsFor(profileDir: string, pluginArgs: string[]):
|
|
|
33
33
|
*/
|
|
34
34
|
export declare const HOST_NAMESPACE_RE: RegExp;
|
|
35
35
|
export interface PnpmFailure {
|
|
36
|
-
code: 'adding-to-root' | 'not-a-workspace' | 'hoist-pattern-diff' | 'pnpm-missing' | 'release-age-violation' | 'ignored-builds' | 'git-prepare-not-allowed' | 'git-prepare-failed' | 'tarball-url-mismatch' | 'fetch-404' | 'no-matching-version' | 'transient-network' | 'fetch-timeout' | 'unexpected-store' | 'patch-failed' | 'missing-tarball-integrity' | 'windows-file-locked' | 'pnpm-unusable' | 'missing-local-dependency' | 'unparseable-build-key';
|
|
36
|
+
code: 'adding-to-root' | 'not-a-workspace' | 'hoist-pattern-diff' | 'pnpm-missing' | 'release-age-violation' | 'ignored-builds' | 'git-prepare-not-allowed' | 'git-prepare-failed' | 'tarball-url-mismatch' | 'fetch-404' | 'no-matching-version' | 'transient-network' | 'fetch-timeout' | 'unexpected-store' | 'patch-failed' | 'missing-tarball-integrity' | 'windows-file-locked' | 'pnpm-unusable' | 'missing-local-dependency' | 'unparseable-build-key' | 'native-oom';
|
|
37
37
|
/** Bilingual, actionable message shown to the user instead of the raw wall of text. */
|
|
38
38
|
message: string;
|
|
39
39
|
/** True when re-running `pnpm install` in the profile is the documented recovery. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshmarket",
|
|
3
3
|
"description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.60.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
package/src/check.ts
CHANGED
|
@@ -947,7 +947,19 @@ export function buildBundleLayers(
|
|
|
947
947
|
// fatal verdict and rolled back a good update (#369) — while `dsh
|
|
948
948
|
// --dump-config` on the same profile exited 0. Unknown has to read as
|
|
949
949
|
// unknown; the profile's own bundles are still judged normally.
|
|
950
|
-
|
|
950
|
+
//
|
|
951
|
+
// The same holds for any official bundle while the installation itself
|
|
952
|
+
// cannot be located (#676): a desktop build ships more in-box bundles
|
|
953
|
+
// than the three named in INBOX_BUNDLES —
|
|
954
|
+
// `@deepseek-ai/dsh-experimental-agent-team-profile` among them — and a
|
|
955
|
+
// fixed list, or an install-path shape, cannot know which. Two
|
|
956
|
+
// reporters saw that bundle called "not installed — will fail to boot"
|
|
957
|
+
// while its three entries were active in the running host. Only
|
|
958
|
+
// DeepSeek publishes under `@deepseek-ai/`, so while the installation
|
|
959
|
+
// is out of sight such a bundle is unknown, not missing. When the
|
|
960
|
+
// installation IS located and the bundle is in neither it nor the
|
|
961
|
+
// profile, the fatal verdict below still applies.
|
|
962
|
+
if (hostProvided || (dshInstallDir === null && name.startsWith('@deepseek-ai/'))) {
|
|
951
963
|
layer.error = null
|
|
952
964
|
layer.unresolvedInbox = true
|
|
953
965
|
return layer
|
|
@@ -1490,6 +1490,8 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
1490
1490
|
* real switch state so hand-edited cordis.patch.yml toggles are visible.
|
|
1491
1491
|
*/
|
|
1492
1492
|
const [patchDisabledNames, setPatchDisabledNames] = useState<string[]>([])
|
|
1493
|
+
/** Bundle packages DSH's own plugin page turned off by leaving them out of dsh.profile.bundles (#696). */
|
|
1494
|
+
const [unbundledNames, setUnbundledNames] = useState<string[]>([])
|
|
1493
1495
|
const [groups, setGroups] = useState<Record<string, string[]>>({})
|
|
1494
1496
|
const [groupOrder, setGroupOrder] = useState<string[]>([])
|
|
1495
1497
|
/** Installed-tab sub-view: flat list or groups (All-plugins was removed —
|
|
@@ -1669,6 +1671,7 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
1669
1671
|
setNotes(body.notes as Record<string, string>)
|
|
1670
1672
|
}
|
|
1671
1673
|
if (Array.isArray(body.patchDisabled)) setPatchDisabledNames(body.patchDisabled)
|
|
1674
|
+
if (Array.isArray(body.unbundled)) setUnbundledNames(body.unbundled)
|
|
1672
1675
|
if (body.groups && typeof body.groups === 'object') setGroups(body.groups)
|
|
1673
1676
|
if (Array.isArray(body.groupOrder)) setGroupOrder(body.groupOrder)
|
|
1674
1677
|
if (Array.isArray(body.favorites)) setFavoriteUrls(body.favorites.filter((url: unknown): url is string => typeof url === 'string'))
|
|
@@ -1697,8 +1700,8 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
1697
1700
|
const favoriteUrlSet = useMemo(() => new Set(favoriteUrls), [favoriteUrls])
|
|
1698
1701
|
/** Effective switch state: market disable list ∪ user-patch-layer disables. */
|
|
1699
1702
|
const effectiveDisabledSet = useMemo(
|
|
1700
|
-
() => new Set([...disabledNames, ...patchDisabledNames]),
|
|
1701
|
-
[disabledNames, patchDisabledNames],
|
|
1703
|
+
() => new Set([...disabledNames, ...patchDisabledNames, ...unbundledNames]),
|
|
1704
|
+
[disabledNames, patchDisabledNames, unbundledNames],
|
|
1702
1705
|
)
|
|
1703
1706
|
|
|
1704
1707
|
useEffect(() => {
|
package/src/pnpm-compat.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface PnpmFailure {
|
|
|
48
48
|
| 'ignored-builds' | 'git-prepare-not-allowed' | 'git-prepare-failed' | 'tarball-url-mismatch'
|
|
49
49
|
| 'fetch-404' | 'no-matching-version' | 'transient-network' | 'fetch-timeout'
|
|
50
50
|
| 'unexpected-store' | 'patch-failed' | 'missing-tarball-integrity' | 'windows-file-locked'
|
|
51
|
-
| 'pnpm-unusable' | 'missing-local-dependency' | 'unparseable-build-key'
|
|
51
|
+
| 'pnpm-unusable' | 'missing-local-dependency' | 'unparseable-build-key' | 'native-oom'
|
|
52
52
|
/** Bilingual, actionable message shown to the user instead of the raw wall of text. */
|
|
53
53
|
message: string
|
|
54
54
|
/** True when re-running `pnpm install` in the profile is the documented recovery. */
|
|
@@ -350,6 +350,21 @@ export function classifyPnpmFailure(output: string, exitCode?: number | null): P
|
|
|
350
350
|
// pnpm's FETCHER, before anything lands in node_modules — so the package
|
|
351
351
|
// the user must approve is not installed yet, and pnpm's own hint names a
|
|
352
352
|
// commit-pinned codeload URL that changes on every push.
|
|
353
|
+
// #701: pnpm 12's native engine (pnpm-native) aborting on an allocation it
|
|
354
|
+
// cannot get — "memory allocation of 5368709120 bytes failed", Windows exit
|
|
355
|
+
// 3221226505 (0xC0000409, how a Rust abort ends there). Reported with an
|
|
356
|
+
// A/B: on pnpm 12.5.1 every run with `autoInstallPeers: false` in the
|
|
357
|
+
// workspace file (DSH writes it into every profile) aborted at ~5 GB peak
|
|
358
|
+
// RSS, every run without it passed at ~80 MB, with free memory to spare.
|
|
359
|
+
// Nothing about the plugin being installed; not something a retry of the
|
|
360
|
+
// same command changes on the reporter's data, so none is attempted.
|
|
361
|
+
if (/memory allocation of \d+ bytes failed/.test(output) || exitCode === 3221226505) {
|
|
362
|
+
return {
|
|
363
|
+
code: 'native-oom',
|
|
364
|
+
recoverable: false,
|
|
365
|
+
message: 'pnpm 12 的原生引擎在处理这个 profile 时耗尽了内存并中止——和要安装的插件无关。在 pnpm 修复之前,请改用 pnpm 11(npm install -g pnpm@11)后再试。 / pnpm 12\'s native engine ran out of memory on this profile and aborted — the plugin being installed is not the cause. Until pnpm fixes it, switch to pnpm 11 (npm install -g pnpm@11) and try again.',
|
|
366
|
+
}
|
|
367
|
+
}
|
|
353
368
|
// #698: pnpm 10.26+ and 11.0–11.5 read an allowBuilds key as
|
|
354
369
|
// `name@<version union>`, so a git or archive source there fails the WHOLE
|
|
355
370
|
// workspace file — every pnpm command in the profile, not the one plugin.
|
package/src/routes.ts
CHANGED
|
@@ -244,6 +244,16 @@ function packageHasClientPart(profileDirectory: string, name: string): boolean {
|
|
|
244
244
|
* the package, and the key written is the one pnpm printed, never text from
|
|
245
245
|
* the request.
|
|
246
246
|
*/
|
|
247
|
+
/** Whether an installed package's manifest declares a bundle. */
|
|
248
|
+
function declaresBundle(profileDirectory: string, name: string): boolean {
|
|
249
|
+
try {
|
|
250
|
+
const manifest = JSON.parse(readFileSync(join(profileDirectory, 'node_modules', name, 'package.json'), 'utf8')) as { dsh?: { bundle?: unknown } }
|
|
251
|
+
return manifest.dsh?.bundle !== undefined
|
|
252
|
+
} catch {
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
247
257
|
const prepareRefusals = new Map<string, string | null>()
|
|
248
258
|
|
|
249
259
|
function blockedBuilds(result: { ignoredBuilds?: unknown; stdout: string; stderr: string }): string[] | undefined {
|
|
@@ -435,6 +445,34 @@ export function mountMarketRoutes(
|
|
|
435
445
|
setCustomGithubProxy(fresh.githubProxy ?? null)
|
|
436
446
|
}
|
|
437
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Follow an enable made OUTSIDE the market (#696).
|
|
450
|
+
*
|
|
451
|
+
* The market keeps its own disable list in state.json, and the self-heal
|
|
452
|
+
* guard below and the boot replay put any plugin on that list back down.
|
|
453
|
+
* DSH's own Settings → Plugins page enables a row by flipping it to
|
|
454
|
+
* `disabled: false` in the shared patch layer — it has never heard of
|
|
455
|
+
* state.json — so its enable was silently undone within a second and again
|
|
456
|
+
* on every boot, and the official switch looked broken.
|
|
457
|
+
*
|
|
458
|
+
* The patch layer is the one truth both managers share. A row explicitly
|
|
459
|
+
* enabled there while no row of the same package is disabled can only be a
|
|
460
|
+
* newer decision than the market's list (the market removes a name from
|
|
461
|
+
* its list BEFORE writing its own enable row), so the list follows it.
|
|
462
|
+
*
|
|
463
|
+
* @returns true when the package was re-enabled elsewhere and has been
|
|
464
|
+
* dropped from the market's disable list.
|
|
465
|
+
*/
|
|
466
|
+
function followOutsideEnable(name: string): boolean {
|
|
467
|
+
const patch = readUserPatchState(userPatchPath)
|
|
468
|
+
const flags = packagePatchFlags(host, activeProfileDir, [name], patch)
|
|
469
|
+
if (!flags.forced.includes(name) || flags.disabled.includes(name)) return false
|
|
470
|
+
disabled.delete(name)
|
|
471
|
+
writeMarketState(activeProfileDir, { disabled, groups, groupOrder })
|
|
472
|
+
logEvent('info', 'toggle', `${name}: its rows were re-enabled outside the market (#696) — following that instead of switching it back off`)
|
|
473
|
+
return true
|
|
474
|
+
}
|
|
475
|
+
|
|
438
476
|
// Client-only packages (dsh.client without dsh.bundle) are invisible to the
|
|
439
477
|
// bundle layer in every boot; the market shim-mounts them so their client
|
|
440
478
|
// bundles are actually served.
|
|
@@ -444,7 +482,8 @@ export function mountMarketRoutes(
|
|
|
444
482
|
// switched away from get live-disabled again (bundle trees are
|
|
445
483
|
// in-memory, so the disable never persists on its own). Client-only
|
|
446
484
|
// shims for disabled plugins were already skipped by mountClientOnlyDeps.
|
|
447
|
-
for (const name of disabled) {
|
|
485
|
+
for (const name of [...disabled]) {
|
|
486
|
+
if (followOutsideEnable(name)) continue
|
|
448
487
|
if (await themes.setEntryDisabled(name, true)) logEvent('info', 'boot', `plugin kept off: ${name}`)
|
|
449
488
|
}
|
|
450
489
|
})
|
|
@@ -454,7 +493,7 @@ export function mountMarketRoutes(
|
|
|
454
493
|
// up for a plugin the user switched off, put it back down.
|
|
455
494
|
host.on?.('internal/plugin', (fiber) => {
|
|
456
495
|
const name = fiber.entry?.options?.name
|
|
457
|
-
if (name !== undefined && disabled.has(name)) void themes.setEntryDisabled(name, true)
|
|
496
|
+
if (name !== undefined && disabled.has(name) && !followOutsideEnable(name)) void themes.setEntryDisabled(name, true)
|
|
458
497
|
})
|
|
459
498
|
let installing = false
|
|
460
499
|
let restarting = false
|
|
@@ -1948,10 +1987,21 @@ export function mountMarketRoutes(
|
|
|
1948
1987
|
return typeof manifest === 'object' && manifest !== null
|
|
1949
1988
|
&& (manifest as { dsh?: unknown }).dsh !== undefined
|
|
1950
1989
|
}
|
|
1990
|
+
// A package that declares a bundle but is not in dsh.profile.bundles
|
|
1991
|
+
// is not composed at boot: DSH's own plugin page turns a package off
|
|
1992
|
+
// by removing it from that list, and nothing the market recorded
|
|
1993
|
+
// says so (#696). Reported as off, unless it is live right now — a
|
|
1994
|
+
// hot mount from this session still runs until the next restart.
|
|
1995
|
+
const composedBundles = new Set(readProfileBundles(activeProfileDir))
|
|
1996
|
+
const unbundled = Object.keys(installed).filter(name => {
|
|
1997
|
+
if (INBOX_BUNDLES.has(name) || composedBundles.has(name) || live.has(name)) return false
|
|
1998
|
+
const manifest = installedManifests.get(name) as { dsh?: { bundle?: unknown } } | null | undefined
|
|
1999
|
+
return typeof manifest === 'object' && manifest !== null && manifest.dsh?.bundle !== undefined
|
|
2000
|
+
})
|
|
1951
2001
|
for (const name of Object.keys(installed)) {
|
|
1952
2002
|
const result = activationAfterReplace(
|
|
1953
2003
|
verifyActivation(config.profile, name, live, activeProfileDir,
|
|
1954
|
-
disabled.has(name) || patchFlags.disabled.includes(name)),
|
|
2004
|
+
disabled.has(name) || patchFlags.disabled.includes(name) || unbundled.includes(name)),
|
|
1955
2005
|
replacedWhileLive.has(name),
|
|
1956
2006
|
)
|
|
1957
2007
|
// A package with no dsh surface of its own, outside the bundle
|
|
@@ -1989,6 +2039,7 @@ export function mountMarketRoutes(
|
|
|
1989
2039
|
favorites: readMarketState(activeProfileDir).favorites ?? [],
|
|
1990
2040
|
patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts },
|
|
1991
2041
|
patchDisabled: patchFlags.disabled,
|
|
2042
|
+
unbundled,
|
|
1992
2043
|
patchForced: patchFlags.forced,
|
|
1993
2044
|
bundles: readProfileBundles(activeProfileDir).filter(name => !INBOX_BUNDLES.has(name)),
|
|
1994
2045
|
})
|
|
@@ -2399,13 +2450,23 @@ export function mountMarketRoutes(
|
|
|
2399
2450
|
// disabled) is NOT dropped: #147 requires disabling it to leave the
|
|
2400
2451
|
// neighbour live, and the e2e fixture-cross re-enable breaks otherwise.
|
|
2401
2452
|
const disablesOthers = carrierDisableIds(activeProfileDir, name)
|
|
2453
|
+
// Turned off by DSH's own plugin page, which removes a package from
|
|
2454
|
+
// dsh.profile.bundles (#696): enabling it here has to put it back,
|
|
2455
|
+
// or the patch rows flip, the switch reads on, and nothing composes
|
|
2456
|
+
// it on the next boot. Unlike a carrier this does NOT force a
|
|
2457
|
+
// restart: the enable below still brings it up in this process, and
|
|
2458
|
+
// whether a restart is needed is whether it is live afterwards.
|
|
2459
|
+
const reBundle = enabled && disablesOthers.length === 0 && !INBOX_BUNDLES.has(name)
|
|
2460
|
+
&& declaresBundle(activeProfileDir, name) && !readProfileBundles(activeProfileDir).includes(name)
|
|
2402
2461
|
const isCarrier = disablesOthers.length > 0
|
|
2403
2462
|
let bundleSwitch: { ok: boolean; reason: string | null } = { ok: true, reason: null }
|
|
2404
|
-
if (isCarrier) {
|
|
2463
|
+
if (isCarrier || reBundle) {
|
|
2405
2464
|
try {
|
|
2406
2465
|
if (enabled) addProfileBundle(activeProfileDir, name)
|
|
2407
2466
|
else removeProfileBundle(activeProfileDir, name)
|
|
2408
|
-
logEvent('info', 'toggle',
|
|
2467
|
+
logEvent('info', 'toggle', reBundle
|
|
2468
|
+
? `${name}: re-added to dsh.profile.bundles, which another manager had removed it from (#696)`
|
|
2469
|
+
: `${name}: disable-carrier ${enabled ? 're-added to' : 'removed from'} dsh.profile.bundles (disables: ${disablesOthers.join(', ')})`)
|
|
2409
2470
|
} catch (error) {
|
|
2410
2471
|
bundleSwitch = { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
2411
2472
|
logEvent('warn', 'toggle', `${name}: carrier bundle switch failed — ${bundleSwitch.reason}`)
|
|
@@ -4792,6 +4853,12 @@ sendJson(response, 200, { updates })
|
|
|
4792
4853
|
// keep their partial state on purpose (the user sees the diff
|
|
4793
4854
|
// and decides).
|
|
4794
4855
|
const manifestBefore = readProfileManifestSnapshot(config.profile, activeProfileDir)
|
|
4856
|
+
// The lockfile too (#701): pnpm writes it before it links, so a
|
|
4857
|
+
// run that dies in between — a native crash, a kill — leaves a
|
|
4858
|
+
// lock that names a package the manifest never got. The update
|
|
4859
|
+
// route has always restored both; a fresh install restored only
|
|
4860
|
+
// the manifest and left the half for the next pnpm run to trip on.
|
|
4861
|
+
const lockfileBefore = captureProfileLockfile()
|
|
4795
4862
|
const pinned = target === pinnedTarget && pinnedTarget !== plainTarget
|
|
4796
4863
|
let result = await (pinned ? runPluginKeepingReleaseAge : runPlugin)(config.profile, ['add', target])
|
|
4797
4864
|
// Two ways a pinned add can fail that a bare add would not, and
|
|
@@ -4825,6 +4892,10 @@ sendJson(response, 200, { updates })
|
|
|
4825
4892
|
if ((result.exitCode !== 0 || result.timedOut) && !cancelled) {
|
|
4826
4893
|
const rolledBack = restoreProfileManifest(config.profile, manifestBefore, activeProfileDir)
|
|
4827
4894
|
if (rolledBack.length > 0) logEvent('warn', 'install', `${target}: rolled back manifest residue of the failed run: ${rolledBack.join(', ')}`)
|
|
4895
|
+
if (lockfileBefore.ok) {
|
|
4896
|
+
const lock = restoreProfileLockfile(lockfileBefore.snapshot)
|
|
4897
|
+
if (!lock.ok) logEvent('warn', 'install', `${target}: could not restore pnpm-lock.yaml after the failed run: ${lock.detail ?? 'unknown'}`)
|
|
4898
|
+
}
|
|
4828
4899
|
}
|
|
4829
4900
|
let ok = result.exitCode === 0 && !result.timedOut && !cancelled
|
|
4830
4901
|
const cancelDiff = cancelled ? changedSince(beforeSpecs) : null
|