dshmarket 1.66.1 → 1.66.3
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 +9 -0
- package/lib/changelog.js +80 -36
- package/lib/check.js +15 -1
- package/lib/index.js +18 -4
- package/lib/install.js +29 -12
- package/lib/net.js +35 -23
- package/lib/official-desktop.js +5 -1
- package/lib/pnpm-compat.js +28 -0
- package/lib/profile.js +59 -27
- package/lib/routes.js +9 -1
- package/lib/types/net.d.ts +24 -15
- package/lib/types/official-desktop.d.ts +1 -1
- package/lib/types/pnpm-compat.d.ts +1 -1
- package/lib/types/profile.d.ts +32 -18
- package/locale/en.json +3 -0
- package/locale/zh.json +6 -0
- package/package.json +4 -2
- package/src/changelog.ts +89 -36
- package/src/check.ts +15 -1
- package/src/client/MarketSection.tsx +9 -0
- package/src/client/locales.ts +4 -0
- package/src/index.ts +19 -5
- package/src/install.ts +26 -11
- package/src/net.ts +35 -22
- package/src/official-desktop.ts +4 -1
- package/src/pnpm-compat.ts +29 -1
- package/src/profile.ts +66 -28
- package/src/routes.ts +9 -1
package/client/client.js
CHANGED
|
@@ -272,6 +272,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
272
272
|
capRedInstallScriptScripts: "安装时会立刻运行 {0},不需要你打开插件",
|
|
273
273
|
capRedCoreTamper: "会改动 DSH 自带的组件",
|
|
274
274
|
capRedCoreTamperDetail: "会改动 DSH 自带的组件({0})",
|
|
275
|
+
capRedCoreOverride: "会覆盖 DSH 自带的组件({0})",
|
|
276
|
+
capRedCoreDisable: "会禁用 DSH 自带的组件({0})",
|
|
275
277
|
favoriteAdd: "加入收藏",
|
|
276
278
|
favoriteRemove: "取消收藏",
|
|
277
279
|
favoritesEmpty: "还没有收藏。在「发现」或「主题」里点书签图标即可收藏。",
|
|
@@ -920,6 +922,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
920
922
|
capRedInstallScriptScripts: "runs {0} the moment you install — you do not have to open the plugin",
|
|
921
923
|
capRedCoreTamper: "replaces parts of DSH itself",
|
|
922
924
|
capRedCoreTamperDetail: "replaces parts of DSH itself ({0})",
|
|
925
|
+
capRedCoreOverride: "overrides a part of DSH itself ({0})",
|
|
926
|
+
capRedCoreDisable: "disables a part of DSH itself ({0})",
|
|
923
927
|
favoriteAdd: "Add to favorites",
|
|
924
928
|
favoriteRemove: "Remove from favorites",
|
|
925
929
|
favoritesEmpty: "No favorites yet. Click the bookmark icon in Discover or Themes to add one.",
|
|
@@ -9764,6 +9768,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
9764
9768
|
if (literalIp !== null) return t("capRedLiteralIp").replace("{0}", literalIp[1]);
|
|
9765
9769
|
const installScript = /^runs code at install time(?: \((.+)\))?$/.exec(line);
|
|
9766
9770
|
if (installScript !== null) return installScript[1] === void 0 ? t("capRedInstallScript") : t("capRedInstallScriptScripts").replace("{0}", installScript[1]);
|
|
9771
|
+
const coreVerb = /^tampers with a core bundle \((overrides|disables) bundle (.+)\)$/.exec(line);
|
|
9772
|
+
if (coreVerb !== null) {
|
|
9773
|
+
const key = coreVerb[1] === "overrides" ? "capRedCoreOverride" : "capRedCoreDisable";
|
|
9774
|
+
return t(key).replace("{0}", coreVerb[2]);
|
|
9775
|
+
}
|
|
9767
9776
|
const coreTamper = /^tampers with a core bundle(?: \((.+)\))?$/.exec(line);
|
|
9768
9777
|
if (coreTamper !== null) return coreTamper[1] === void 0 ? t("capRedCoreTamper") : t("capRedCoreTamperDetail").replace("{0}", coreTamper[1]);
|
|
9769
9778
|
return line;
|
package/lib/changelog.js
CHANGED
|
@@ -21,7 +21,7 @@ import { fileFromTarball } from './catalog-npm.js';
|
|
|
21
21
|
import { marketFetch } from './net.js';
|
|
22
22
|
import { activeRegion, routesFor } from './regions.js';
|
|
23
23
|
import { profileDir, readInstalled, readInstalledRepoEvidence, readLockCommits } from './profile.js';
|
|
24
|
-
import { lookupRepoFromUrl, repoOf, repoOfTarget } from './sources.js';
|
|
24
|
+
import { lookupRepoFromUrl, parseSourceUrl, 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';
|
|
@@ -170,6 +170,55 @@ export async function npmPublishTimes(name) {
|
|
|
170
170
|
timesCache.set(name, { at: Date.now(), data });
|
|
171
171
|
return data;
|
|
172
172
|
}
|
|
173
|
+
/** A probe entry the dialog can actually render — an empty object is a miss. */
|
|
174
|
+
function usableNotes(entry) {
|
|
175
|
+
if (entry === undefined)
|
|
176
|
+
return false;
|
|
177
|
+
if (entry.release != null && (entry.release.body !== '' || entry.release.tag !== null))
|
|
178
|
+
return true;
|
|
179
|
+
return (entry.commits?.length ?? 0) > 0;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The catalog row for an npm install.
|
|
183
|
+
*
|
|
184
|
+
* The profile dependency key is the npm package name. The catalog `name`
|
|
185
|
+
* often is not: a monorepo entry is `repo#subpath`, and a scoped package's
|
|
186
|
+
* catalog name drops the scope (#746). `npm` is the identity the install
|
|
187
|
+
* used, so it wins; a bare `name` match remains for entries that publish
|
|
188
|
+
* under their catalog name. Several hits are disambiguated by the installed
|
|
189
|
+
* repository, and an ambiguous set is left unresolved (#598).
|
|
190
|
+
*/
|
|
191
|
+
function catalogEntryForNpmInstall(plugins, name, evidence) {
|
|
192
|
+
const byNpm = plugins.filter(p => p.npm === name);
|
|
193
|
+
const candidates = byNpm.length > 0 ? byNpm : plugins.filter(p => p.name === name);
|
|
194
|
+
if (candidates.length <= 1)
|
|
195
|
+
return candidates[0];
|
|
196
|
+
const identities = evidence().identities;
|
|
197
|
+
const matches = candidates.filter(p => identities.some(id => repoOf(p.url)?.toLowerCase() === id.split('#')[0].toLowerCase()));
|
|
198
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The catalog row for a `github:…#path:` install.
|
|
202
|
+
*
|
|
203
|
+
* `repoKeyOf` keeps only the repository root, while a monorepo entry's notes
|
|
204
|
+
* are stored under its `/tree/<ref>/<subpath>` url (#746). The spec's subpath
|
|
205
|
+
* selects the row. Two rows with one subpath are not guessed between.
|
|
206
|
+
*/
|
|
207
|
+
function catalogEntryForGitSubpath(plugins, spec) {
|
|
208
|
+
const identity = repoOfTarget(spec);
|
|
209
|
+
const marker = identity?.indexOf('#path:/') ?? -1;
|
|
210
|
+
if (identity === null || marker === -1)
|
|
211
|
+
return undefined;
|
|
212
|
+
const repo = identity.slice(0, marker);
|
|
213
|
+
const subpath = identity.slice(marker + '#path:/'.length);
|
|
214
|
+
const matches = plugins.filter(p => {
|
|
215
|
+
const source = parseSourceUrl(p.url);
|
|
216
|
+
return source?.subpath != null
|
|
217
|
+
&& source.repo.toLowerCase() === repo
|
|
218
|
+
&& source.subpath.toLowerCase() === subpath;
|
|
219
|
+
});
|
|
220
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
221
|
+
}
|
|
173
222
|
/**
|
|
174
223
|
* Resolve the notes for one installed plugin, or `{ kind: 'none' }`.
|
|
175
224
|
*
|
|
@@ -185,52 +234,46 @@ export async function updateNotesFor(profile, explicitDir, name) {
|
|
|
185
234
|
}
|
|
186
235
|
try {
|
|
187
236
|
const payload = await loadUpdateNotes();
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
237
|
+
// A Release-asset spec carries the repo in the URL itself. An npm
|
|
238
|
+
// version range carries nothing, so `specKey` stays null and the catalog
|
|
239
|
+
// lookup below is what finds the repository.
|
|
240
|
+
const specKey = repoKeyOf(spec) ?? lookupRepoFromUrl(spec);
|
|
241
|
+
let key = specKey;
|
|
242
|
+
let entry = key === null ? undefined : entryForRepo(payload, key);
|
|
243
|
+
// The catalog is how an npm install, or a `#path:` install whose notes
|
|
244
|
+
// live under a /tree/ url, finds a key the spec itself does not name.
|
|
245
|
+
// A root github spec already is that key. Loading the catalog on a root
|
|
246
|
+
// miss would wait out its timeout to answer "no notes".
|
|
247
|
+
const needsCatalog = specKey === null || (repoOfTarget(spec)?.includes('#path:/') ?? false);
|
|
248
|
+
if (!usableNotes(entry) && needsCatalog) {
|
|
199
249
|
try {
|
|
200
250
|
const registry = await loadRegistry();
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
}
|
|
251
|
+
const plugin = specKey === null
|
|
252
|
+
? catalogEntryForNpmInstall(registry.plugins, name, () => readInstalledRepoEvidence(profile, name, spec, explicitDir))
|
|
253
|
+
: catalogEntryForGitSubpath(registry.plugins, spec);
|
|
219
254
|
if (plugin !== undefined) {
|
|
220
|
-
|
|
255
|
+
const catalogEntry = entryForRepo(payload, plugin.url);
|
|
256
|
+
// An empty row does not become the key. Publish times for an npm
|
|
257
|
+
// install follow from `specKey` staying null, and a subpath install
|
|
258
|
+
// keeps the root key so the miss stays `none`.
|
|
259
|
+
if (usableNotes(catalogEntry)) {
|
|
260
|
+
key = plugin.url;
|
|
261
|
+
entry = catalogEntry;
|
|
262
|
+
}
|
|
221
263
|
}
|
|
222
264
|
}
|
|
223
265
|
catch {
|
|
224
|
-
// Catalog unavailable;
|
|
266
|
+
// Catalog unavailable; the tiers below still answer.
|
|
225
267
|
}
|
|
226
268
|
}
|
|
227
|
-
const entry = key === null ? undefined : entryForRepo(payload, key);
|
|
228
269
|
// Both tiers below need the installed sha for github-kind installs.
|
|
229
270
|
// For Release asset URLs and catalog lookups, checkUpdates doesn't
|
|
230
271
|
// recognize the spec, so we read the lockfile directly using the repo key.
|
|
272
|
+
// `repoOf` drops a `/tree/<ref>/<subpath>` suffix; slicing the url at `#`
|
|
273
|
+
// would look the commit up under `owner/repo/tree/...` and miss it.
|
|
231
274
|
let current = null;
|
|
232
275
|
if (key !== null && key.startsWith('https://github.com/')) {
|
|
233
|
-
const repo = key.slice('https://github.com/'.length).split('#')[0].toLowerCase();
|
|
276
|
+
const repo = (repoOf(key) ?? key.slice('https://github.com/'.length).split('#')[0]).toLowerCase();
|
|
234
277
|
current = readLockCommits(profile, activeProfileDir).get(`github.com/${repo}`) ?? null;
|
|
235
278
|
}
|
|
236
279
|
else {
|
|
@@ -243,8 +286,9 @@ export async function updateNotesFor(profile, explicitDir, name) {
|
|
|
243
286
|
if (entry?.commits !== undefined && entry.commits.length > 0) {
|
|
244
287
|
return { kind: 'commits', commits: sliceCommitsAt(entry.commits, current) };
|
|
245
288
|
}
|
|
246
|
-
if (
|
|
247
|
-
//
|
|
289
|
+
if (specKey === null) {
|
|
290
|
+
// The install spec did not name a repository. Publish times remain
|
|
291
|
+
// when the catalog row is missing or its probe has no notes.
|
|
248
292
|
return { kind: 'npm', npmTimes: await npmPublishTimes(name) };
|
|
249
293
|
}
|
|
250
294
|
// A github plugin whose repo answered nothing — releases 404 AND the log
|
package/lib/check.js
CHANGED
|
@@ -1008,7 +1008,21 @@ export function analyzeProfile(profileDirectory, options = {}) {
|
|
|
1008
1008
|
if (seenDeps.has(key))
|
|
1009
1009
|
continue;
|
|
1010
1010
|
seenDeps.add(key);
|
|
1011
|
-
|
|
1011
|
+
// A HOST-PLANE peer never takes its version from the shared workspace
|
|
1012
|
+
// root: `<profiles>/node_modules` is shared by every profile, and on a
|
|
1013
|
+
// machine that also runs a second DSH installation — a global npm CLI
|
|
1014
|
+
// beside the Desktop app — that tree is the OTHER installation's
|
|
1015
|
+
// closure. Reading a host version from it reported a healthy install as
|
|
1016
|
+
// "introduced host-compatibility risks … vs 0.1.5-rc.2" and offered a
|
|
1017
|
+
// rollback, while the running host was 0.1.7-rc.2 (#726). What the
|
|
1018
|
+
// plugin itself resolves still counts (a nested copy, a profile-local
|
|
1019
|
+
// one), and the located installation is asked as `host` below; with
|
|
1020
|
+
// neither, the version is unknown rather than wrong — the rule #676
|
|
1021
|
+
// settled for bundles. Non-host peers keep the hoisted fallback.
|
|
1022
|
+
const hostPlane = name.startsWith('@deepseek-ai/');
|
|
1023
|
+
const hoisted = hostPlane
|
|
1024
|
+
? readNodeModulesVersion(profileDirectory, name)
|
|
1025
|
+
: readProfileVisibleVersion(profileDirectory, name);
|
|
1012
1026
|
const nested = readNodeModulesVersion(pluginDir, name);
|
|
1013
1027
|
const host = dshInstall !== null ? readNodeModulesVersion(dshInstall, name) : null;
|
|
1014
1028
|
// Node resolves a plugin's peer from its OWN node_modules first
|
package/lib/index.js
CHANGED
|
@@ -158,12 +158,26 @@ export function apply(ctx, config) {
|
|
|
158
158
|
// every install (#702). A third-party shell announces itself through
|
|
159
159
|
// `desktopProfiles`, and this whole block runs only when that service
|
|
160
160
|
// is absent, so this never takes a third-party shell's profile.
|
|
161
|
+
const configured = config?.profile;
|
|
162
|
+
const configuredDesktop = configured !== undefined && configured.toLowerCase() === 'desktop';
|
|
161
163
|
const officialElectron = launched !== undefined && launched.name.toLowerCase() === 'desktop';
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
// An explicitly configured `profile: desktop` takes this branch too. The
|
|
165
|
+
// CLI refuses that profile by NAME, so routing it there — which is what
|
|
166
|
+
// an operator's own workaround for a host that hides `profileContext`
|
|
167
|
+
// does (#744) — makes every install fail for certain. The configuration
|
|
168
|
+
// still wins over the launcher for the name; it just cannot win a branch
|
|
169
|
+
// that is guaranteed to fail.
|
|
170
|
+
if (configuredDesktop || (configured === undefined && officialElectron)) {
|
|
171
|
+
const profileName = configured ?? launched.name;
|
|
172
|
+
// The launcher owns where a profile lives — and its directory rides
|
|
173
|
+
// along only when the launcher is the one that named this profile.
|
|
174
|
+
const profileDirectory = launched !== undefined && launched.name.toLowerCase() === profileName.toLowerCase()
|
|
175
|
+
? launched.dir
|
|
176
|
+
: undefined;
|
|
177
|
+
const runtime = createOfficialDesktopRuntime(() => hostCtx.get('pluginManager'), profileName, profileDirectory);
|
|
164
178
|
const resolved = {
|
|
165
|
-
profile:
|
|
166
|
-
profileDirectory:
|
|
179
|
+
profile: profileName,
|
|
180
|
+
...(profileDirectory === undefined ? {} : { profileDirectory }),
|
|
167
181
|
desktopHost: true,
|
|
168
182
|
allowRestart: false,
|
|
169
183
|
maxSnapshots: config?.maxSnapshots,
|
package/lib/install.js
CHANGED
|
@@ -119,6 +119,18 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
|
|
|
119
119
|
const marketFlags = options.marketFlags !== false;
|
|
120
120
|
/** The option a recovery step needed and this host does not accept (#732). */
|
|
121
121
|
let unavailableOption = null;
|
|
122
|
+
// Before the FIRST run, not after a failure: the shadowed duplicate rule
|
|
123
|
+
// (#732) hurts pnpm while it RESOLVES the dependency graph, so merging it
|
|
124
|
+
// first is what keeps the command from failing at all — and it costs one
|
|
125
|
+
// small file read. Every verb that resolves the graph is covered, not just
|
|
126
|
+
// add and remove: an `install` or an in-place `update` reads the same key.
|
|
127
|
+
const verb = pluginArgs.find(argument => !argument.startsWith('-'));
|
|
128
|
+
if (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update') {
|
|
129
|
+
const merged = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory);
|
|
130
|
+
if (merged.length > 0) {
|
|
131
|
+
logEvent('warn', 'install', `minimumReleaseAgeExclude held several rules for ${merged.join(', ')}, and pnpm honours only the FIRST per name (#732) — merged each package's rules into one, before running`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
122
134
|
let result = await run(profile, pluginArgs);
|
|
123
135
|
const ok = (r) => r.exitCode === 0 && !r.timedOut && !r.cancelled;
|
|
124
136
|
if (!ok(result) && !result.cancelled) {
|
|
@@ -149,23 +161,28 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
|
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
else if (failure?.code === 'release-age-violation'
|
|
152
|
-
&&
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// #732
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const mergedDuplicates = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory);
|
|
162
|
-
if (mergedDuplicates.length > 0) {
|
|
163
|
-
logEvent('warn', 'install', `pnpm appended a second minimumReleaseAgeExclude rule for ${mergedDuplicates.join(', ')} and honours only the first, shadowing its own entry (#732) — merged the duplicates and retrying once`);
|
|
164
|
+
&& (pluginArgs[0] === 'add' || pluginArgs[0] === 'remove')) {
|
|
165
|
+
// The repair ran before this command, so a rewrite here means pnpm wrote
|
|
166
|
+
// one of the two broken shapes DURING it — appending a shadowed rule
|
|
167
|
+
// (#732) or extending a version union (#733). Either way the same
|
|
168
|
+
// rewrite fixes it, needs no option at all, and so also works on the
|
|
169
|
+
// desktop bridge that refuses options. Retry the SAME argv afterwards.
|
|
170
|
+
const mergedNow = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory);
|
|
171
|
+
if (mergedNow.length > 0) {
|
|
172
|
+
logEvent('warn', 'install', `pnpm appended a second minimumReleaseAgeExclude rule for ${mergedNow.join(', ')} and honours only the first, shadowing its own entry (#732) — merged them into one and retrying once`);
|
|
164
173
|
result = await run(profile, pluginArgs);
|
|
165
174
|
}
|
|
175
|
+
else if (options.releaseAgeBypass === false) {
|
|
176
|
+
// The caller declined the bypass (#594), and the duplicates were not
|
|
177
|
+
// what failed: this violation is the profile's own policy doing its
|
|
178
|
+
// job, so the command stands as it is.
|
|
179
|
+
}
|
|
166
180
|
else if (!marketFlags) {
|
|
167
181
|
unavailableOption = RELEASE_AGE_OVERRIDE;
|
|
168
182
|
}
|
|
183
|
+
else if (pluginArgs.includes(RELEASE_AGE_OVERRIDE)) {
|
|
184
|
+
// Already carrying it (a caller's forced retry): nothing left to add.
|
|
185
|
+
}
|
|
169
186
|
else {
|
|
170
187
|
logEvent('warn', 'install', `a too-young release blocks pnpm's lockfile verification (#39) — retrying once with ${RELEASE_AGE_OVERRIDE}`);
|
|
171
188
|
result = await run(profile, [pluginArgs[0], RELEASE_AGE_OVERRIDE, ...pluginArgs.slice(1)]);
|
package/lib/net.js
CHANGED
|
@@ -9,24 +9,35 @@
|
|
|
9
9
|
* took 9.9s direct on a reporter's machine, seconds from the 15s timeout,
|
|
10
10
|
* while their proxy sat unused a millisecond away.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* produced no CONNECT at a local proxy, while undici's own fetch produced
|
|
16
|
-
* `CONNECT awesome-dsh-plugin.com:443`.
|
|
12
|
+
* Every market request therefore calls undici's own fetch and carries a
|
|
13
|
+
* dispatcher this module created. Two measurements say why, and they are
|
|
14
|
+
* not the same fact:
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* - On Node 25, `setGlobalDispatcher` from the undici package does not
|
|
17
|
+
* steer global fetch. With a dispatcher installed that way, a global
|
|
18
|
+
* fetch produced no CONNECT at a local proxy, while undici's own fetch
|
|
19
|
+
* produced `CONNECT awesome-dsh-plugin.com:443`.
|
|
20
|
+
* - On Node 22 the two stacks share one symbol (#742). Global fetch reads
|
|
21
|
+
* `Symbol.for('undici.globalDispatcher.1')`. The host's first import of
|
|
22
|
+
* undici 8 (`web_fetch`) finds `.2` empty, installs its dispatcher, and
|
|
23
|
+
* writes a `Dispatcher1Wrapper` onto `.1`. After that, global fetch
|
|
24
|
+
* returns gzip bodies with null headers, and `JSON.parse` fails on the
|
|
25
|
+
* catalog. undici 7's fetch reads `.1` too, so calling it with no
|
|
26
|
+
* dispatcher fails the same way.
|
|
27
|
+
*
|
|
28
|
+
* The dispatcher is `EnvHttpProxyAgent` when a proxy is configured, and a
|
|
29
|
+
* plain `Agent` otherwise. Only requests made here take it. The host's own
|
|
30
|
+
* networking stays as the host configured it.
|
|
21
31
|
*/
|
|
22
|
-
import { EnvHttpProxyAgent, fetch as undiciFetch } from 'undici';
|
|
32
|
+
import { Agent, EnvHttpProxyAgent, fetch as undiciFetch } from 'undici';
|
|
23
33
|
/**
|
|
24
34
|
* The proxy this process would use for the catalog, if any.
|
|
25
35
|
*
|
|
26
36
|
* The standard variables mirror `EnvHttpProxyAgent`'s own resolution
|
|
27
37
|
* deliberately, rather than picking the order that reads best, because the
|
|
28
|
-
* same answer does two jobs: it decides whether
|
|
29
|
-
*
|
|
38
|
+
* same answer does two jobs: it decides whether the request goes through
|
|
39
|
+
* the proxy agent or the direct one, and it is what the failure message
|
|
40
|
+
* CLAIMS was tried. A helper that
|
|
30
41
|
* named a proxy undici would not have used would put a false statement in
|
|
31
42
|
* every bug report. `npm_config_*` is an additional source on top of that:
|
|
32
43
|
* npm holds its proxy in its own config namespace (a machine set up with
|
|
@@ -70,29 +81,30 @@ function proxyFromEnv() {
|
|
|
70
81
|
return { http, https };
|
|
71
82
|
}
|
|
72
83
|
/**
|
|
73
|
-
* Built once and reused: an agent per request would drop connection reuse
|
|
74
|
-
*
|
|
84
|
+
* Built once and reused: an agent per request would drop connection reuse.
|
|
85
|
+
* The proxy agent also reads NO_PROXY, so a host that excludes its own
|
|
75
86
|
* registry mirror keeps being excluded.
|
|
76
87
|
*/
|
|
77
|
-
let
|
|
88
|
+
let proxyAgent = null;
|
|
89
|
+
let directAgent = null;
|
|
78
90
|
/**
|
|
79
|
-
* Fetch through the proxy this machine is configured to use
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* ordinary case on the runtime's own path rather than routing it through a
|
|
83
|
-
* second HTTP stack for no reason.
|
|
91
|
+
* Fetch through the proxy this machine is configured to use, or directly
|
|
92
|
+
* through this module's own agent when it has none. Both paths pass the
|
|
93
|
+
* dispatcher described on this module.
|
|
84
94
|
*/
|
|
85
95
|
export async function marketFetch(url, init) {
|
|
86
96
|
const { http, https } = proxyFromEnv();
|
|
87
|
-
if (http === null && https === null)
|
|
88
|
-
|
|
97
|
+
if (http === null && https === null) {
|
|
98
|
+
directAgent ??= new Agent();
|
|
99
|
+
return await undiciFetch(url, { ...init, dispatcher: directAgent });
|
|
100
|
+
}
|
|
89
101
|
// Pass the resolved proxies explicitly. EnvHttpProxyAgent itself reads
|
|
90
102
|
// only http(s)_proxy out of the environment, so a proxy that lives in
|
|
91
103
|
// npm_config_* must be handed over directly — otherwise the agent would
|
|
92
104
|
// silently go direct while configuredProxy() claims a proxy was used.
|
|
93
|
-
|
|
105
|
+
proxyAgent ??= new EnvHttpProxyAgent({
|
|
94
106
|
httpProxy: http ?? undefined,
|
|
95
107
|
httpsProxy: https ?? undefined,
|
|
96
108
|
});
|
|
97
|
-
return await undiciFetch(url, { ...init, dispatcher:
|
|
109
|
+
return await undiciFetch(url, { ...init, dispatcher: proxyAgent });
|
|
98
110
|
}
|
package/lib/official-desktop.js
CHANGED
|
@@ -20,7 +20,11 @@ function detail(error) {
|
|
|
20
20
|
return JSON.stringify(error);
|
|
21
21
|
}
|
|
22
22
|
/** Never fall back to `dsh plugin --profile desktop`: that CLI is forbidden. */
|
|
23
|
-
export function createOfficialDesktopRuntime(managerLookup, profileName,
|
|
23
|
+
export function createOfficialDesktopRuntime(managerLookup, profileName,
|
|
24
|
+
// Unused by the manager bridge — it takes the profile by NAME — and optional
|
|
25
|
+
// because an explicitly configured `profile: desktop` may be the only thing
|
|
26
|
+
// that names this profile, with no launcher directory to go with it (#744).
|
|
27
|
+
_profileDirectory) {
|
|
24
28
|
let disposed = false;
|
|
25
29
|
let active;
|
|
26
30
|
const runPlugin = (profile, argv) => {
|
package/lib/pnpm-compat.js
CHANGED
|
@@ -270,6 +270,34 @@ export function classifyPnpmFailure(output, exitCode) {
|
|
|
270
270
|
message: `profile 里的一个 pnpm 补丁打不上了${which}。pnpm 会继续把这个包装上,但装的是没打补丁的原版——通常下次启动才会以「插件加载失败」暴露出来。多半是包升级后挪动了补丁指向的文件(例如补丁改的是 client/client.js,而新版本发的是 lib/client.js)。请更新或删掉这个补丁文件,以及 profile package.json 里 pnpm.patchedDependencies 中对应的那一条 / a pnpm patch in this profile no longer applies${whichEn}. pnpm still installs the package, but unpatched — which usually surfaces at the next boot as "failed to load plugins" rather than here. The usual cause is the package moving the file the patch targets (for example a patch against client/client.js when the release now ships lib/client.js). Update or remove that patch file and its entry under pnpm.patchedDependencies in the profile's package.json`,
|
|
271
271
|
};
|
|
272
272
|
}
|
|
273
|
+
// #740 by @lws2004: the other half of the story above. `patchedDependencies`
|
|
274
|
+
// keys on `pkg@exactVersion`, and once the installed version moves past that
|
|
275
|
+
// key pnpm 12 treats the stale patch as a HARD error: it writes NOTHING at
|
|
276
|
+
// all. So an update of that plugin is refused in full — the version stays
|
|
277
|
+
// where it was, the market's own log holds only exit=1, and the user reads
|
|
278
|
+
// "the update did not apply" for what is really a patch that no longer
|
|
279
|
+
// matches. Unlike ERR_PNPM_PATCH_FAILED there is no unpatched install to
|
|
280
|
+
// discover later, which makes this the more confusing of the two. The patch
|
|
281
|
+
// is the user's, so the market names the entries and says where to fix them
|
|
282
|
+
// rather than guessing a retarget.
|
|
283
|
+
if (output.includes('ERR_PNPM_UNUSED_PATCH')) {
|
|
284
|
+
// `[^\n"]+`: the same sentence also arrives inside the ndjson stream,
|
|
285
|
+
// where it ends at a quote rather than at the line — matching to the line
|
|
286
|
+
// end there captured `"}}` as part of the package name.
|
|
287
|
+
const unused = /The following patches were not used:\s*([^\n"]+)/
|
|
288
|
+
.exec(withDecodedPnpmDiagnostics(output))?.[1];
|
|
289
|
+
const named = unused === undefined
|
|
290
|
+
? []
|
|
291
|
+
: unused.split(',').map(entry => entry.trim()).filter(entry => entry !== '');
|
|
292
|
+
const which = named.length === 0 ? '' : `(${named.join('、')})`;
|
|
293
|
+
const whichEn = named.length === 0 ? '' : ` (${named.join(', ')})`;
|
|
294
|
+
return {
|
|
295
|
+
code: 'unused-patch',
|
|
296
|
+
recoverable: false,
|
|
297
|
+
pkg: named.length === 1 ? named[0] : undefined,
|
|
298
|
+
message: `profile 里的 pnpm 补丁有一条已经用不上了${which}:patchedDependencies 是按「包名@精确版本」钉的,而这次要装的版本已经越过它,pnpm 12 因此判定整条命令失败——它什么都没写,所以看起来像「点了更新没反应」。要么把补丁更新到新版本、并把 profile package.json 里 pnpm.patchedDependencies 的键改成「包名@新版本」,要么在新版本已经自带这个修复时直接删掉这一条和对应的补丁文件 / a pnpm patch in this profile is no longer used${whichEn}: patchedDependencies keys on "package@exactVersion", and the version being installed has moved past that key, so pnpm 12 fails the WHOLE command and writes nothing at all — which is why the update looks like it simply did not apply. Either retarget the patch to the new version and change its pnpm.patchedDependencies key in the profile's package.json to "package@newVersion", or — when the new release already carries that fix — drop the entry and its patch file`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
273
301
|
if (output.includes('ERR_PNPM_ADDING_TO_ROOT')) {
|
|
274
302
|
return {
|
|
275
303
|
code: 'adding-to-root',
|
package/lib/profile.js
CHANGED
|
@@ -1222,27 +1222,41 @@ function splitReleaseAgeExclude(entry) {
|
|
|
1222
1222
|
return { name, selector };
|
|
1223
1223
|
}
|
|
1224
1224
|
/**
|
|
1225
|
-
*
|
|
1226
|
-
* (#732).
|
|
1225
|
+
* Make a profile's `minimumReleaseAgeExclude` readable again (#732).
|
|
1227
1226
|
*
|
|
1228
|
-
* pnpm
|
|
1229
|
-
* `
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
* EVERY later command in that profile: installs, updates and uninstalls
|
|
1235
|
-
* alike, including ones that have nothing to do with the package. Reported
|
|
1236
|
-
* as #732, where the market's own self-update planted exactly that pair and
|
|
1237
|
-
* every plugin operation on the desktop profile stopped working.
|
|
1227
|
+
* pnpm appends a second rule for a package that already has one, while its
|
|
1228
|
+
* `evaluateVersionPolicy` honours only the FIRST rule per package name — so
|
|
1229
|
+
* its own new entry is dead, the young version stays unexcluded, and every
|
|
1230
|
+
* later command in that profile fails lockfile verification with
|
|
1231
|
+
* ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION, including commands that have nothing
|
|
1232
|
+
* to do with that package.
|
|
1238
1233
|
*
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1234
|
+
* (#733 reported a separate, unexplained 80 GiB allocation abort on pnpm
|
|
1235
|
+
* 12.4.1 that its author first tied to the union spelling. Review on that
|
|
1236
|
+
* issue — and the reporter's own follow-up, which could no longer reproduce it
|
|
1237
|
+
* — settled that `name@a || b` is a documented pnpm form, that the validator's
|
|
1238
|
+
* "Use exact versions only" is about ranges and name patterns, and that the
|
|
1239
|
+
* abort is not this market's to fix. Do not "repair" a union into a bare name
|
|
1240
|
+
* on the strength of it: see below.)
|
|
1244
1241
|
*
|
|
1245
|
-
*
|
|
1242
|
+
* pnpm WRITES this key itself, and one of the forms it writes is what breaks a
|
|
1243
|
+
* profile (#732): pnpm 11.7.0 APPENDS a second rule for a package that already
|
|
1244
|
+
* has one, while its `evaluateVersionPolicy` honours only the FIRST rule per
|
|
1245
|
+
* package name. Its own new entry is therefore dead, the young version stays
|
|
1246
|
+
* unexcluded, and every later command in that profile fails lockfile
|
|
1247
|
+
* verification with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION — installs, updates
|
|
1248
|
+
* and uninstalls alike, including ones that have nothing to do with that
|
|
1249
|
+
* package. Merging the same-name rules into one makes the file readable again.
|
|
1250
|
+
*
|
|
1251
|
+
* The merge keeps the UNION of the versions the file already lists
|
|
1252
|
+
* (`name@1.2.3 || 1.4.0`), which is a documented pnpm spelling. What this
|
|
1253
|
+
* deliberately does NOT do is collapse a version list to a bare package name:
|
|
1254
|
+
* a bare name exempts EVERY version of that package from the cooldown, which
|
|
1255
|
+
* is wider than what the file says, and the market pins exact versions
|
|
1256
|
+
* precisely so a fresh install cannot silently land on an older release
|
|
1257
|
+
* (#594). A form the file cannot be read exactly from is left alone.
|
|
1258
|
+
*
|
|
1259
|
+
* @returns the package names whose rules were merged; empty when the file
|
|
1246
1260
|
* needed no repair or could not be repaired, in which case it is left
|
|
1247
1261
|
* byte-for-byte as it was.
|
|
1248
1262
|
*/
|
|
@@ -1274,40 +1288,58 @@ export function mergeDuplicateReleaseAgeExcludes(profile, explicitDir) {
|
|
|
1274
1288
|
const rule = splitReleaseAgeExclude(m[1]);
|
|
1275
1289
|
if (rule === null)
|
|
1276
1290
|
return [];
|
|
1277
|
-
entries.push({ rule, quoted: /^['"]/.test(m[1]) });
|
|
1291
|
+
entries.push({ rule, quoted: /^['"]/.test(m[1]), line });
|
|
1278
1292
|
}
|
|
1279
1293
|
const byName = new Map();
|
|
1280
1294
|
const order = [];
|
|
1281
|
-
for (const { rule, quoted } of entries) {
|
|
1295
|
+
for (const { rule, quoted, line } of entries) {
|
|
1282
1296
|
let group = byName.get(rule.name);
|
|
1283
1297
|
if (group === undefined) {
|
|
1284
|
-
group = { selectors: new Set(), allVersions: false, quoted: false, count: 0 };
|
|
1298
|
+
group = { selectors: new Set(), originals: [], lines: [], allVersions: false, quoted: false, count: 0 };
|
|
1285
1299
|
byName.set(rule.name, group);
|
|
1286
1300
|
order.push(rule.name);
|
|
1287
1301
|
}
|
|
1288
1302
|
group.count += 1;
|
|
1289
1303
|
group.quoted = group.quoted || quoted;
|
|
1304
|
+
group.originals.push(rule.selector ?? '');
|
|
1305
|
+
group.lines.push(line);
|
|
1290
1306
|
if (rule.selector === null)
|
|
1291
1307
|
group.allVersions = true;
|
|
1292
1308
|
else
|
|
1293
1309
|
group.selectors.add(rule.selector);
|
|
1294
1310
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
return [];
|
|
1298
|
-
const lines = order.map(name => {
|
|
1311
|
+
/** The one line this package's entry is written as. */
|
|
1312
|
+
const produced = (name) => {
|
|
1299
1313
|
const group = byName.get(name);
|
|
1300
1314
|
if (group === undefined)
|
|
1301
1315
|
return '';
|
|
1302
|
-
|
|
1316
|
+
// A bare name already covers every version, so it stays bare and nothing
|
|
1317
|
+
// is widened; one rule stays exactly as it was written, union included.
|
|
1318
|
+
// Several rules for one name are the #732 breakage — only the first is
|
|
1319
|
+
// ever read — so they become one, as the union of the versions the file
|
|
1320
|
+
// already lists.
|
|
1321
|
+
const text = group.allVersions
|
|
1322
|
+
? name
|
|
1323
|
+
: group.count === 1
|
|
1324
|
+
? `${name}@${group.originals[0] ?? ''}`
|
|
1325
|
+
: `${name}@${[...group.selectors].join(' || ')}`;
|
|
1303
1326
|
// `@` cannot start a plain scalar in YAML, so a scoped name is written
|
|
1304
1327
|
// quoted — the way pnpm itself writes one.
|
|
1305
1328
|
const quoted = group.quoted || text.startsWith('@');
|
|
1306
1329
|
return `${indent}- ${quoted ? `'${text.replaceAll("'", "''")}'` : text}`;
|
|
1330
|
+
};
|
|
1331
|
+
const rewritten = order.filter(name => {
|
|
1332
|
+
const group = byName.get(name);
|
|
1333
|
+
if (group === undefined)
|
|
1334
|
+
return false;
|
|
1335
|
+
return group.count > 1 || group.lines[0] !== produced(name);
|
|
1307
1336
|
});
|
|
1337
|
+
if (rewritten.length === 0)
|
|
1338
|
+
return [];
|
|
1339
|
+
const lines = order.map(produced);
|
|
1308
1340
|
const blockText = `minimumReleaseAgeExclude:${eol}${lines.join(eol)}${eol}`;
|
|
1309
1341
|
// A function replacement: `$&` and friends in a string replacement would
|
|
1310
1342
|
// be read as capture references.
|
|
1311
1343
|
writeFileSync(file, yaml.replace(blockRe, () => blockText));
|
|
1312
|
-
return
|
|
1344
|
+
return rewritten;
|
|
1313
1345
|
}
|
package/lib/routes.js
CHANGED
|
@@ -3691,7 +3691,15 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
3691
3691
|
&& capturedNpmVersion(lockfileCapture.snapshot, name) !== beforeVersion
|
|
3692
3692
|
? {
|
|
3693
3693
|
available: false,
|
|
3694
|
-
|
|
3694
|
+
// Name all three: the version on disk, the version
|
|
3695
|
+
// the lockfile records, and the one a rollback
|
|
3696
|
+
// would install (#732). The old wording named the
|
|
3697
|
+
// first two only by implication, and a user who
|
|
3698
|
+
// reads "the exact source cannot be proven" has
|
|
3699
|
+
// nothing to act on — the reproducer's way out was
|
|
3700
|
+
// to align package.json with the installed build,
|
|
3701
|
+
// which is what this now says.
|
|
3702
|
+
detail: `更新前 node_modules 里装的是 v${beforeVersion},但 pnpm-lock.yaml 里 ${name} 记的是 ${capturedNpmVersion(lockfileCapture.snapshot, name) ?? '(没有记录)'},两者不一致,无法证明精确来源,因此自动回滚不可用(要回滚的是 ${name}@${beforeVersion})。需要时请手工重新安装 ${name}@${beforeVersion},或先把 package.json 与 pnpm-lock.yaml 对齐到 v${beforeVersion}。 / The build on disk before the update was v${beforeVersion}, but pnpm-lock.yaml records ${capturedNpmVersion(lockfileCapture.snapshot, name) ?? 'nothing'} for ${name}: they disagree, so the exact source cannot be proven and automatic rollback is unavailable. Reinstall ${name}@${beforeVersion} manually if needed, or align package.json and pnpm-lock.yaml with v${beforeVersion} first.`,
|
|
3695
3703
|
lockfileBefore: lockfileCapture.snapshot,
|
|
3696
3704
|
}
|
|
3697
3705
|
: !supportsExactRollbackTarget(`${name}@${beforeVersion}`)
|