dshmarket 1.66.2 → 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/lib/changelog.js +80 -36
- package/lib/check.js +15 -1
- package/lib/index.js +18 -4
- package/lib/install.js +12 -14
- package/lib/net.js +35 -23
- package/lib/official-desktop.js +5 -1
- package/lib/pnpm-compat.js +28 -0
- package/lib/profile.js +41 -34
- 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 +31 -28
- 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/index.ts +19 -5
- package/src/install.ts +12 -14
- package/src/net.ts +35 -22
- package/src/official-desktop.ts +4 -1
- package/src/pnpm-compat.ts +29 -1
- package/src/profile.ts +41 -34
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
|
@@ -8,7 +8,7 @@ import { closeSync, existsSync, lstatSync, openSync, readlinkSync, readSync, rmS
|
|
|
8
8
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
9
9
|
import { findDshInstallDir } from './dsh-install.js';
|
|
10
10
|
import { classifyPnpmFailure, HOST_NAMESPACE_RE } from './pnpm-compat.js';
|
|
11
|
-
import { conflictingEntryIds, dropFromManifest, hasDshManifest, hasLoadableEntry,
|
|
11
|
+
import { conflictingEntryIds, dropFromManifest, hasDshManifest, hasLoadableEntry, mergeDuplicateReleaseAgeExcludes, pluginSubdirs, profileDir, readInstalled, readManifestDeps, readProfileBundles, dropUnparseableBuildKeys } from './profile.js';
|
|
12
12
|
import { logEvent } from './log.js';
|
|
13
13
|
import { cleanOrphanedStore } from './store.js';
|
|
14
14
|
/**
|
|
@@ -119,18 +119,16 @@ 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
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// never fire. Every verb that resolves the graph is covered, not just add and
|
|
128
|
-
// remove: an `install` or an in-place `update` consults the same key.
|
|
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.
|
|
129
127
|
const verb = pluginArgs.find(argument => !argument.startsWith('-'));
|
|
130
128
|
if (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update') {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
logEvent('warn', 'install', `minimumReleaseAgeExclude held
|
|
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`);
|
|
134
132
|
}
|
|
135
133
|
}
|
|
136
134
|
let result = await run(profile, pluginArgs);
|
|
@@ -169,9 +167,9 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
|
|
|
169
167
|
// (#732) or extending a version union (#733). Either way the same
|
|
170
168
|
// rewrite fixes it, needs no option at all, and so also works on the
|
|
171
169
|
// desktop bridge that refuses options. Retry the SAME argv afterwards.
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
174
|
-
logEvent('warn', 'install', `pnpm
|
|
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`);
|
|
175
173
|
result = await run(profile, pluginArgs);
|
|
176
174
|
}
|
|
177
175
|
else if (options.releaseAgeBypass === false) {
|
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,42 +1222,45 @@ function splitReleaseAgeExclude(entry) {
|
|
|
1222
1222
|
return { name, selector };
|
|
1223
1223
|
}
|
|
1224
1224
|
/**
|
|
1225
|
-
* Make a profile's `minimumReleaseAgeExclude` readable again (#732
|
|
1225
|
+
* Make a profile's `minimumReleaseAgeExclude` readable again (#732).
|
|
1226
1226
|
*
|
|
1227
|
-
* pnpm
|
|
1228
|
-
*
|
|
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.
|
|
1229
1233
|
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
* union, a form its OWN validator rejects ("Invalid versions union … Use
|
|
1238
|
-
* exact versions only"), and evaluating one aborts the process on a single
|
|
1239
|
-
* 80 GiB allocation that takes the machine down for minutes (#733). The
|
|
1240
|
-
* entries are evaluated lazily, so this fires on any later install that
|
|
1241
|
-
* age-checks that package, with no pnpm error output to explain it.
|
|
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.)
|
|
1242
1241
|
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
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.
|
|
1251
1250
|
*
|
|
1252
|
-
*
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
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.
|
|
1255
1258
|
*
|
|
1256
|
-
* @returns the package names whose
|
|
1259
|
+
* @returns the package names whose rules were merged; empty when the file
|
|
1257
1260
|
* needed no repair or could not be repaired, in which case it is left
|
|
1258
1261
|
* byte-for-byte as it was.
|
|
1259
1262
|
*/
|
|
1260
|
-
export function
|
|
1263
|
+
export function mergeDuplicateReleaseAgeExcludes(profile, explicitDir) {
|
|
1261
1264
|
const file = join(profileDir(profile, explicitDir), 'pnpm-workspace.yaml');
|
|
1262
1265
|
let yaml;
|
|
1263
1266
|
try {
|
|
@@ -1310,12 +1313,16 @@ export function normalizeReleaseAgeExcludes(profile, explicitDir) {
|
|
|
1310
1313
|
const group = byName.get(name);
|
|
1311
1314
|
if (group === undefined)
|
|
1312
1315
|
return '';
|
|
1313
|
-
// A bare name already covers every version, so it stays bare
|
|
1314
|
-
//
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
const text =
|
|
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(' || ')}`;
|
|
1319
1326
|
// `@` cannot start a plain scalar in YAML, so a scoped name is written
|
|
1320
1327
|
// quoted — the way pnpm itself writes one.
|
|
1321
1328
|
const quoted = group.quoted || text.startsWith('@');
|
package/lib/types/net.d.ts
CHANGED
|
@@ -9,23 +9,34 @@
|
|
|
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
32
|
/**
|
|
23
33
|
* The proxy this process would use for the catalog, if any.
|
|
24
34
|
*
|
|
25
35
|
* The standard variables mirror `EnvHttpProxyAgent`'s own resolution
|
|
26
36
|
* deliberately, rather than picking the order that reads best, because the
|
|
27
|
-
* same answer does two jobs: it decides whether
|
|
28
|
-
*
|
|
37
|
+
* same answer does two jobs: it decides whether the request goes through
|
|
38
|
+
* the proxy agent or the direct one, and it is what the failure message
|
|
39
|
+
* CLAIMS was tried. A helper that
|
|
29
40
|
* named a proxy undici would not have used would put a false statement in
|
|
30
41
|
* every bug report. `npm_config_*` is an additional source on top of that:
|
|
31
42
|
* npm holds its proxy in its own config namespace (a machine set up with
|
|
@@ -46,11 +57,9 @@
|
|
|
46
57
|
*/
|
|
47
58
|
export declare function configuredProxy(): string | null;
|
|
48
59
|
/**
|
|
49
|
-
* Fetch through the proxy this machine is configured to use
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* ordinary case on the runtime's own path rather than routing it through a
|
|
53
|
-
* second HTTP stack for no reason.
|
|
60
|
+
* Fetch through the proxy this machine is configured to use, or directly
|
|
61
|
+
* through this module's own agent when it has none. Both paths pass the
|
|
62
|
+
* dispatcher described on this module.
|
|
54
63
|
*/
|
|
55
64
|
export declare function marketFetch(url: string, init?: {
|
|
56
65
|
signal?: AbortSignal;
|
|
@@ -22,5 +22,5 @@ export interface OfficialPluginManagerLike {
|
|
|
22
22
|
*/
|
|
23
23
|
export declare const OFFICIAL_PAGE_HINT = "\u5B98\u65B9\u684C\u9762\u5BA2\u6237\u7AEF\u7684\u8FD9\u4E2A\u63D2\u4EF6\u64CD\u4F5C\u9700\u8981\u5728\u300C\u8BBE\u7F6E \u2192 \u63D2\u4EF6\u300D\u91CC\u5B8C\u6210\uFF1B\u5E02\u573A\u65E0\u6CD5\u901A\u8FC7\u547D\u4EE4\u884C\u4FEE\u6539\u684C\u9762\u7AEF\u7684 profile\u3002 / On the official desktop app, do this from Settings \u2192 Plugins; the market cannot change the desktop profile through the command line.";
|
|
24
24
|
/** Never fall back to `dsh plugin --profile desktop`: that CLI is forbidden. */
|
|
25
|
-
export declare function createOfficialDesktopRuntime(managerLookup: () => OfficialPluginManagerLike | undefined, profileName: string, _profileDirectory
|
|
25
|
+
export declare function createOfficialDesktopRuntime(managerLookup: () => OfficialPluginManagerLike | undefined, profileName: string, _profileDirectory?: string): DesktopPluginRuntime;
|
|
26
26
|
export {};
|
|
@@ -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' | 'native-oom' | 'ssh-auth-failed';
|
|
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' | 'unused-patch' | 'missing-tarball-integrity' | 'windows-file-locked' | 'pnpm-unusable' | 'missing-local-dependency' | 'unparseable-build-key' | 'native-oom' | 'ssh-auth-failed';
|
|
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. */
|