dshmarket 1.66.1 → 1.66.2
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/install.js +32 -13
- package/lib/profile.js +53 -28
- package/lib/routes.js +9 -1
- package/lib/types/profile.d.ts +30 -19
- package/package.json +1 -1
- package/src/client/MarketSection.tsx +9 -0
- package/src/client/locales.ts +4 -0
- package/src/install.ts +29 -12
- package/src/profile.ts +60 -29
- 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/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, normalizeReleaseAgeExcludes, 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,6 +119,20 @@ 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 two shapes pnpm writes into
|
|
123
|
+
// `minimumReleaseAgeExclude` (a shadowed duplicate rule, #732; a version
|
|
124
|
+
// union, #733) hurt pnpm while it RESOLVES the dependency graph, and the
|
|
125
|
+
// union one aborts the process on an 80 GiB allocation with no error output
|
|
126
|
+
// at all — nothing to classify, so a repair that waited for a failure would
|
|
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.
|
|
129
|
+
const verb = pluginArgs.find(argument => !argument.startsWith('-'));
|
|
130
|
+
if (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update') {
|
|
131
|
+
const normalized = normalizeReleaseAgeExcludes(profile, profileDirectory);
|
|
132
|
+
if (normalized.length > 0) {
|
|
133
|
+
logEvent('warn', 'install', `minimumReleaseAgeExclude held a form pnpm cannot read back for ${normalized.join(', ')} (a shadowed duplicate rule, #732, or a version union, which pnpm 12.4.1 aborts on — #733) — rewrote each as one bare package name before running`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
122
136
|
let result = await run(profile, pluginArgs);
|
|
123
137
|
const ok = (r) => r.exitCode === 0 && !r.timedOut && !r.cancelled;
|
|
124
138
|
if (!ok(result) && !result.cancelled) {
|
|
@@ -149,23 +163,28 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
|
|
|
149
163
|
}
|
|
150
164
|
}
|
|
151
165
|
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`);
|
|
166
|
+
&& (pluginArgs[0] === 'add' || pluginArgs[0] === 'remove')) {
|
|
167
|
+
// The repair ran before this command, so a rewrite here means pnpm wrote
|
|
168
|
+
// one of the two broken shapes DURING it — appending a shadowed rule
|
|
169
|
+
// (#732) or extending a version union (#733). Either way the same
|
|
170
|
+
// rewrite fixes it, needs no option at all, and so also works on the
|
|
171
|
+
// desktop bridge that refuses options. Retry the SAME argv afterwards.
|
|
172
|
+
const normalized = normalizeReleaseAgeExcludes(profile, profileDirectory);
|
|
173
|
+
if (normalized.length > 0) {
|
|
174
|
+
logEvent('warn', 'install', `pnpm wrote a minimumReleaseAgeExclude entry it cannot read back for ${normalized.join(', ')} (#732/#733) — rewrote each as one bare package name and retrying once`);
|
|
164
175
|
result = await run(profile, pluginArgs);
|
|
165
176
|
}
|
|
177
|
+
else if (options.releaseAgeBypass === false) {
|
|
178
|
+
// The caller declined the bypass (#594), and the duplicates were not
|
|
179
|
+
// what failed: this violation is the profile's own policy doing its
|
|
180
|
+
// job, so the command stands as it is.
|
|
181
|
+
}
|
|
166
182
|
else if (!marketFlags) {
|
|
167
183
|
unavailableOption = RELEASE_AGE_OVERRIDE;
|
|
168
184
|
}
|
|
185
|
+
else if (pluginArgs.includes(RELEASE_AGE_OVERRIDE)) {
|
|
186
|
+
// Already carrying it (a caller's forced retry): nothing left to add.
|
|
187
|
+
}
|
|
169
188
|
else {
|
|
170
189
|
logEvent('warn', 'install', `a too-young release blocks pnpm's lockfile verification (#39) — retrying once with ${RELEASE_AGE_OVERRIDE}`);
|
|
171
190
|
result = await run(profile, [pluginArgs[0], RELEASE_AGE_OVERRIDE, ...pluginArgs.slice(1)]);
|
package/lib/profile.js
CHANGED
|
@@ -1222,31 +1222,42 @@ function splitReleaseAgeExclude(entry) {
|
|
|
1222
1222
|
return { name, selector };
|
|
1223
1223
|
}
|
|
1224
1224
|
/**
|
|
1225
|
-
*
|
|
1226
|
-
* (#732).
|
|
1225
|
+
* Make a profile's `minimumReleaseAgeExclude` readable again (#732, #733).
|
|
1227
1226
|
*
|
|
1228
|
-
* pnpm
|
|
1229
|
-
*
|
|
1230
|
-
* that package — and its own `evaluateVersionPolicy` then honours only the
|
|
1231
|
-
* FIRST rule per package name. The entry pnpm just wrote is therefore
|
|
1232
|
-
* shadowed by the older one, the young version stays unexcluded, and pnpm
|
|
1233
|
-
* fails lockfile verification with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION on
|
|
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 WRITES this key in forms it then mishandles, and two separate defects
|
|
1228
|
+
* come out of that:
|
|
1238
1229
|
*
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1230
|
+
* - pnpm 11.7.0 appends a second rule for a package that already has one,
|
|
1231
|
+
* while its `evaluateVersionPolicy` honours only the FIRST rule per name.
|
|
1232
|
+
* Its own new entry is therefore dead, and every later command in that
|
|
1233
|
+
* profile fails lockfile verification with
|
|
1234
|
+
* ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION — including commands that have
|
|
1235
|
+
* nothing to do with that package (#732).
|
|
1236
|
+
* - pnpm 12.4.1 folds the versions it approves into a `name@v1 || v2 || …`
|
|
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.
|
|
1244
1242
|
*
|
|
1245
|
-
*
|
|
1243
|
+
* Both are avoided by writing each such package as ONE BARE NAME. A bare name
|
|
1244
|
+
* cannot be shadowed (the first rule for it is already all of it), it stops
|
|
1245
|
+
* pnpm's auto-collect for that package outright, and it carries no union for
|
|
1246
|
+
* the parser to evaluate. It is also a wider statement than a version list —
|
|
1247
|
+
* the package stops being age-gated at all — so it is spent only on entries
|
|
1248
|
+
* pnpm has already written in one of those two broken forms, never on a
|
|
1249
|
+
* package the file lists as a single exact version. A pure duplicate of one
|
|
1250
|
+
* exact version collapses to that one line, which changes no policy at all.
|
|
1251
|
+
*
|
|
1252
|
+
* A file holding nothing but bare names and single exact versions is left
|
|
1253
|
+
* untouched, as is one whose block this cannot read exactly (a flow list, an
|
|
1254
|
+
* inline comment, a line it would have to guess at).
|
|
1255
|
+
*
|
|
1256
|
+
* @returns the package names whose entry was rewritten; empty when the file
|
|
1246
1257
|
* needed no repair or could not be repaired, in which case it is left
|
|
1247
1258
|
* byte-for-byte as it was.
|
|
1248
1259
|
*/
|
|
1249
|
-
export function
|
|
1260
|
+
export function normalizeReleaseAgeExcludes(profile, explicitDir) {
|
|
1250
1261
|
const file = join(profileDir(profile, explicitDir), 'pnpm-workspace.yaml');
|
|
1251
1262
|
let yaml;
|
|
1252
1263
|
try {
|
|
@@ -1274,40 +1285,54 @@ export function mergeDuplicateReleaseAgeExcludes(profile, explicitDir) {
|
|
|
1274
1285
|
const rule = splitReleaseAgeExclude(m[1]);
|
|
1275
1286
|
if (rule === null)
|
|
1276
1287
|
return [];
|
|
1277
|
-
entries.push({ rule, quoted: /^['"]/.test(m[1]) });
|
|
1288
|
+
entries.push({ rule, quoted: /^['"]/.test(m[1]), line });
|
|
1278
1289
|
}
|
|
1279
1290
|
const byName = new Map();
|
|
1280
1291
|
const order = [];
|
|
1281
|
-
for (const { rule, quoted } of entries) {
|
|
1292
|
+
for (const { rule, quoted, line } of entries) {
|
|
1282
1293
|
let group = byName.get(rule.name);
|
|
1283
1294
|
if (group === undefined) {
|
|
1284
|
-
group = { selectors: new Set(), allVersions: false, quoted: false, count: 0 };
|
|
1295
|
+
group = { selectors: new Set(), originals: [], lines: [], allVersions: false, quoted: false, count: 0 };
|
|
1285
1296
|
byName.set(rule.name, group);
|
|
1286
1297
|
order.push(rule.name);
|
|
1287
1298
|
}
|
|
1288
1299
|
group.count += 1;
|
|
1289
1300
|
group.quoted = group.quoted || quoted;
|
|
1301
|
+
group.originals.push(rule.selector ?? '');
|
|
1302
|
+
group.lines.push(line);
|
|
1290
1303
|
if (rule.selector === null)
|
|
1291
1304
|
group.allVersions = true;
|
|
1292
1305
|
else
|
|
1293
1306
|
group.selectors.add(rule.selector);
|
|
1294
1307
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
return [];
|
|
1298
|
-
const lines = order.map(name => {
|
|
1308
|
+
/** The one line this package's entry is written as. */
|
|
1309
|
+
const produced = (name) => {
|
|
1299
1310
|
const group = byName.get(name);
|
|
1300
1311
|
if (group === undefined)
|
|
1301
1312
|
return '';
|
|
1302
|
-
|
|
1313
|
+
// A bare name already covers every version, so it stays bare; a union or
|
|
1314
|
+
// several rules for one name cannot both be read, so they become one.
|
|
1315
|
+
const bare = group.allVersions
|
|
1316
|
+
|| group.originals.some(selector => selector.includes('||'))
|
|
1317
|
+
|| group.selectors.size > 1;
|
|
1318
|
+
const text = bare ? name : `${name}@${[...group.selectors][0]}`;
|
|
1303
1319
|
// `@` cannot start a plain scalar in YAML, so a scoped name is written
|
|
1304
1320
|
// quoted — the way pnpm itself writes one.
|
|
1305
1321
|
const quoted = group.quoted || text.startsWith('@');
|
|
1306
1322
|
return `${indent}- ${quoted ? `'${text.replaceAll("'", "''")}'` : text}`;
|
|
1323
|
+
};
|
|
1324
|
+
const rewritten = order.filter(name => {
|
|
1325
|
+
const group = byName.get(name);
|
|
1326
|
+
if (group === undefined)
|
|
1327
|
+
return false;
|
|
1328
|
+
return group.count > 1 || group.lines[0] !== produced(name);
|
|
1307
1329
|
});
|
|
1330
|
+
if (rewritten.length === 0)
|
|
1331
|
+
return [];
|
|
1332
|
+
const lines = order.map(produced);
|
|
1308
1333
|
const blockText = `minimumReleaseAgeExclude:${eol}${lines.join(eol)}${eol}`;
|
|
1309
1334
|
// A function replacement: `$&` and friends in a string replacement would
|
|
1310
1335
|
// be read as capture references.
|
|
1311
1336
|
writeFileSync(file, yaml.replace(blockRe, () => blockText));
|
|
1312
|
-
return
|
|
1337
|
+
return rewritten;
|
|
1313
1338
|
}
|
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}`)
|
package/lib/types/profile.d.ts
CHANGED
|
@@ -351,28 +351,39 @@ export declare function setAllowBuilds(profile: string, packages: string[], expl
|
|
|
351
351
|
*/
|
|
352
352
|
export declare function dropUnparseableBuildKeys(profile: string, explicitDir?: string): string[];
|
|
353
353
|
/**
|
|
354
|
-
*
|
|
355
|
-
* (#732).
|
|
354
|
+
* Make a profile's `minimumReleaseAgeExclude` readable again (#732, #733).
|
|
356
355
|
*
|
|
357
|
-
* pnpm
|
|
358
|
-
*
|
|
359
|
-
* that package — and its own `evaluateVersionPolicy` then honours only the
|
|
360
|
-
* FIRST rule per package name. The entry pnpm just wrote is therefore
|
|
361
|
-
* shadowed by the older one, the young version stays unexcluded, and pnpm
|
|
362
|
-
* fails lockfile verification with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION on
|
|
363
|
-
* EVERY later command in that profile: installs, updates and uninstalls
|
|
364
|
-
* alike, including ones that have nothing to do with the package. Reported
|
|
365
|
-
* as #732, where the market's own self-update planted exactly that pair and
|
|
366
|
-
* every plugin operation on the desktop profile stopped working.
|
|
356
|
+
* pnpm WRITES this key in forms it then mishandles, and two separate defects
|
|
357
|
+
* come out of that:
|
|
367
358
|
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
359
|
+
* - pnpm 11.7.0 appends a second rule for a package that already has one,
|
|
360
|
+
* while its `evaluateVersionPolicy` honours only the FIRST rule per name.
|
|
361
|
+
* Its own new entry is therefore dead, and every later command in that
|
|
362
|
+
* profile fails lockfile verification with
|
|
363
|
+
* ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION — including commands that have
|
|
364
|
+
* nothing to do with that package (#732).
|
|
365
|
+
* - pnpm 12.4.1 folds the versions it approves into a `name@v1 || v2 || …`
|
|
366
|
+
* union, a form its OWN validator rejects ("Invalid versions union … Use
|
|
367
|
+
* exact versions only"), and evaluating one aborts the process on a single
|
|
368
|
+
* 80 GiB allocation that takes the machine down for minutes (#733). The
|
|
369
|
+
* entries are evaluated lazily, so this fires on any later install that
|
|
370
|
+
* age-checks that package, with no pnpm error output to explain it.
|
|
373
371
|
*
|
|
374
|
-
*
|
|
372
|
+
* Both are avoided by writing each such package as ONE BARE NAME. A bare name
|
|
373
|
+
* cannot be shadowed (the first rule for it is already all of it), it stops
|
|
374
|
+
* pnpm's auto-collect for that package outright, and it carries no union for
|
|
375
|
+
* the parser to evaluate. It is also a wider statement than a version list —
|
|
376
|
+
* the package stops being age-gated at all — so it is spent only on entries
|
|
377
|
+
* pnpm has already written in one of those two broken forms, never on a
|
|
378
|
+
* package the file lists as a single exact version. A pure duplicate of one
|
|
379
|
+
* exact version collapses to that one line, which changes no policy at all.
|
|
380
|
+
*
|
|
381
|
+
* A file holding nothing but bare names and single exact versions is left
|
|
382
|
+
* untouched, as is one whose block this cannot read exactly (a flow list, an
|
|
383
|
+
* inline comment, a line it would have to guess at).
|
|
384
|
+
*
|
|
385
|
+
* @returns the package names whose entry was rewritten; empty when the file
|
|
375
386
|
* needed no repair or could not be repaired, in which case it is left
|
|
376
387
|
* byte-for-byte as it was.
|
|
377
388
|
*/
|
|
378
|
-
export declare function
|
|
389
|
+
export declare function normalizeReleaseAgeExcludes(profile: string, explicitDir?: string): string[];
|
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.66.
|
|
4
|
+
"version": "1.66.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -3980,6 +3980,15 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
3980
3980
|
? t('capRedInstallScript')
|
|
3981
3981
|
: t('capRedInstallScriptScripts').replace('{0}', installScript[1])
|
|
3982
3982
|
}
|
|
3983
|
+
// The parenthesised detail is not a name. The scanner emits exactly two
|
|
3984
|
+
// shapes — "overrides bundle <id>" and "disables bundle <id>" — and pasting
|
|
3985
|
+
// either through leaves the verb in English. That verb is the fact: the
|
|
3986
|
+
// install would replace a part of DSH, or switch one off.
|
|
3987
|
+
const coreVerb = /^tampers with a core bundle \((overrides|disables) bundle (.+)\)$/.exec(line)
|
|
3988
|
+
if (coreVerb !== null) {
|
|
3989
|
+
const key = coreVerb[1] === 'overrides' ? 'capRedCoreOverride' : 'capRedCoreDisable'
|
|
3990
|
+
return t(key).replace('{0}', coreVerb[2]!)
|
|
3991
|
+
}
|
|
3983
3992
|
const coreTamper = /^tampers with a core bundle(?: \((.+)\))?$/.exec(line)
|
|
3984
3993
|
if (coreTamper !== null) {
|
|
3985
3994
|
return coreTamper[1] === undefined
|
package/src/client/locales.ts
CHANGED
|
@@ -81,6 +81,8 @@ export const zh = {
|
|
|
81
81
|
capRedInstallScriptScripts: '安装时会立刻运行 {0},不需要你打开插件',
|
|
82
82
|
capRedCoreTamper: '会改动 DSH 自带的组件',
|
|
83
83
|
capRedCoreTamperDetail: '会改动 DSH 自带的组件({0})',
|
|
84
|
+
capRedCoreOverride: '会覆盖 DSH 自带的组件({0})',
|
|
85
|
+
capRedCoreDisable: '会禁用 DSH 自带的组件({0})',
|
|
84
86
|
favoriteAdd: '加入收藏',
|
|
85
87
|
favoriteRemove: '取消收藏',
|
|
86
88
|
favoritesEmpty: '还没有收藏。在「发现」或「主题」里点书签图标即可收藏。',
|
|
@@ -746,6 +748,8 @@ export const en: Record<MarketKey, string> = {
|
|
|
746
748
|
capRedInstallScriptScripts: 'runs {0} the moment you install — you do not have to open the plugin',
|
|
747
749
|
capRedCoreTamper: 'replaces parts of DSH itself',
|
|
748
750
|
capRedCoreTamperDetail: 'replaces parts of DSH itself ({0})',
|
|
751
|
+
capRedCoreOverride: 'overrides a part of DSH itself ({0})',
|
|
752
|
+
capRedCoreDisable: 'disables a part of DSH itself ({0})',
|
|
749
753
|
favoriteAdd: 'Add to favorites',
|
|
750
754
|
favoriteRemove: 'Remove from favorites',
|
|
751
755
|
favoritesEmpty: 'No favorites yet. Click the bookmark icon in Discover or Themes to add one.',
|
package/src/install.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
|
10
10
|
import type { InstallResult, PluginRunner } from './dsh-cli.ts'
|
|
11
11
|
import { findDshInstallDir } from './dsh-install.ts'
|
|
12
12
|
import { classifyPnpmFailure, HOST_NAMESPACE_RE, isTransientPnpmFailure } from './pnpm-compat.ts'
|
|
13
|
-
import { conflictingEntryIds, dropFromManifest, hasDshManifest, hasLoadableEntry,
|
|
13
|
+
import { conflictingEntryIds, dropFromManifest, hasDshManifest, hasLoadableEntry, normalizeReleaseAgeExcludes, pluginSubdirs, profileDir, readInstalled, readManifestDeps, readProfileBundles, dropUnparseableBuildKeys } from './profile.ts'
|
|
14
14
|
import { logEvent } from './log.ts'
|
|
15
15
|
import { cleanOrphanedStore } from './store.ts'
|
|
16
16
|
|
|
@@ -135,6 +135,20 @@ export async function withHoistRecovery(
|
|
|
135
135
|
const marketFlags = options.marketFlags !== false
|
|
136
136
|
/** The option a recovery step needed and this host does not accept (#732). */
|
|
137
137
|
let unavailableOption: string | null = null
|
|
138
|
+
// Before the FIRST run, not after a failure: the two shapes pnpm writes into
|
|
139
|
+
// `minimumReleaseAgeExclude` (a shadowed duplicate rule, #732; a version
|
|
140
|
+
// union, #733) hurt pnpm while it RESOLVES the dependency graph, and the
|
|
141
|
+
// union one aborts the process on an 80 GiB allocation with no error output
|
|
142
|
+
// at all — nothing to classify, so a repair that waited for a failure would
|
|
143
|
+
// never fire. Every verb that resolves the graph is covered, not just add and
|
|
144
|
+
// remove: an `install` or an in-place `update` consults the same key.
|
|
145
|
+
const verb = pluginArgs.find(argument => !argument.startsWith('-'))
|
|
146
|
+
if (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update') {
|
|
147
|
+
const normalized = normalizeReleaseAgeExcludes(profile, profileDirectory)
|
|
148
|
+
if (normalized.length > 0) {
|
|
149
|
+
logEvent('warn', 'install', `minimumReleaseAgeExclude held a form pnpm cannot read back for ${normalized.join(', ')} (a shadowed duplicate rule, #732, or a version union, which pnpm 12.4.1 aborts on — #733) — rewrote each as one bare package name before running`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
138
152
|
let result = await run(profile, pluginArgs)
|
|
139
153
|
const ok = (r: InstallResult): boolean => r.exitCode === 0 && !r.timedOut && !r.cancelled
|
|
140
154
|
if (!ok(result) && !result.cancelled) {
|
|
@@ -162,22 +176,25 @@ export async function withHoistRecovery(
|
|
|
162
176
|
}
|
|
163
177
|
} else if (
|
|
164
178
|
failure?.code === 'release-age-violation'
|
|
165
|
-
&& options.releaseAgeBypass !== false
|
|
166
179
|
&& (pluginArgs[0] === 'add' || pluginArgs[0] === 'remove')
|
|
167
|
-
&& !pluginArgs.includes(RELEASE_AGE_OVERRIDE)
|
|
168
180
|
) {
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
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`)
|
|
181
|
+
// The repair ran before this command, so a rewrite here means pnpm wrote
|
|
182
|
+
// one of the two broken shapes DURING it — appending a shadowed rule
|
|
183
|
+
// (#732) or extending a version union (#733). Either way the same
|
|
184
|
+
// rewrite fixes it, needs no option at all, and so also works on the
|
|
185
|
+
// desktop bridge that refuses options. Retry the SAME argv afterwards.
|
|
186
|
+
const normalized = normalizeReleaseAgeExcludes(profile, profileDirectory)
|
|
187
|
+
if (normalized.length > 0) {
|
|
188
|
+
logEvent('warn', 'install', `pnpm wrote a minimumReleaseAgeExclude entry it cannot read back for ${normalized.join(', ')} (#732/#733) — rewrote each as one bare package name and retrying once`)
|
|
178
189
|
result = await run(profile, pluginArgs)
|
|
190
|
+
} else if (options.releaseAgeBypass === false) {
|
|
191
|
+
// The caller declined the bypass (#594), and the duplicates were not
|
|
192
|
+
// what failed: this violation is the profile's own policy doing its
|
|
193
|
+
// job, so the command stands as it is.
|
|
179
194
|
} else if (!marketFlags) {
|
|
180
195
|
unavailableOption = RELEASE_AGE_OVERRIDE
|
|
196
|
+
} else if (pluginArgs.includes(RELEASE_AGE_OVERRIDE)) {
|
|
197
|
+
// Already carrying it (a caller's forced retry): nothing left to add.
|
|
181
198
|
} else {
|
|
182
199
|
logEvent('warn', 'install', `a too-young release blocks pnpm's lockfile verification (#39) — retrying once with ${RELEASE_AGE_OVERRIDE}`)
|
|
183
200
|
result = await run(profile, [pluginArgs[0], RELEASE_AGE_OVERRIDE, ...pluginArgs.slice(1)])
|
package/src/profile.ts
CHANGED
|
@@ -1269,31 +1269,42 @@ function splitReleaseAgeExclude(entry: string): ReleaseAgeExcludeRule | null {
|
|
|
1269
1269
|
}
|
|
1270
1270
|
|
|
1271
1271
|
/**
|
|
1272
|
-
*
|
|
1273
|
-
* (#732).
|
|
1272
|
+
* Make a profile's `minimumReleaseAgeExclude` readable again (#732, #733).
|
|
1274
1273
|
*
|
|
1275
|
-
* pnpm
|
|
1276
|
-
*
|
|
1277
|
-
* that package — and its own `evaluateVersionPolicy` then honours only the
|
|
1278
|
-
* FIRST rule per package name. The entry pnpm just wrote is therefore
|
|
1279
|
-
* shadowed by the older one, the young version stays unexcluded, and pnpm
|
|
1280
|
-
* fails lockfile verification with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION on
|
|
1281
|
-
* EVERY later command in that profile: installs, updates and uninstalls
|
|
1282
|
-
* alike, including ones that have nothing to do with the package. Reported
|
|
1283
|
-
* as #732, where the market's own self-update planted exactly that pair and
|
|
1284
|
-
* every plugin operation on the desktop profile stopped working.
|
|
1274
|
+
* pnpm WRITES this key in forms it then mishandles, and two separate defects
|
|
1275
|
+
* come out of that:
|
|
1285
1276
|
*
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1277
|
+
* - pnpm 11.7.0 appends a second rule for a package that already has one,
|
|
1278
|
+
* while its `evaluateVersionPolicy` honours only the FIRST rule per name.
|
|
1279
|
+
* Its own new entry is therefore dead, and every later command in that
|
|
1280
|
+
* profile fails lockfile verification with
|
|
1281
|
+
* ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION — including commands that have
|
|
1282
|
+
* nothing to do with that package (#732).
|
|
1283
|
+
* - pnpm 12.4.1 folds the versions it approves into a `name@v1 || v2 || …`
|
|
1284
|
+
* union, a form its OWN validator rejects ("Invalid versions union … Use
|
|
1285
|
+
* exact versions only"), and evaluating one aborts the process on a single
|
|
1286
|
+
* 80 GiB allocation that takes the machine down for minutes (#733). The
|
|
1287
|
+
* entries are evaluated lazily, so this fires on any later install that
|
|
1288
|
+
* age-checks that package, with no pnpm error output to explain it.
|
|
1291
1289
|
*
|
|
1292
|
-
*
|
|
1290
|
+
* Both are avoided by writing each such package as ONE BARE NAME. A bare name
|
|
1291
|
+
* cannot be shadowed (the first rule for it is already all of it), it stops
|
|
1292
|
+
* pnpm's auto-collect for that package outright, and it carries no union for
|
|
1293
|
+
* the parser to evaluate. It is also a wider statement than a version list —
|
|
1294
|
+
* the package stops being age-gated at all — so it is spent only on entries
|
|
1295
|
+
* pnpm has already written in one of those two broken forms, never on a
|
|
1296
|
+
* package the file lists as a single exact version. A pure duplicate of one
|
|
1297
|
+
* exact version collapses to that one line, which changes no policy at all.
|
|
1298
|
+
*
|
|
1299
|
+
* A file holding nothing but bare names and single exact versions is left
|
|
1300
|
+
* untouched, as is one whose block this cannot read exactly (a flow list, an
|
|
1301
|
+
* inline comment, a line it would have to guess at).
|
|
1302
|
+
*
|
|
1303
|
+
* @returns the package names whose entry was rewritten; empty when the file
|
|
1293
1304
|
* needed no repair or could not be repaired, in which case it is left
|
|
1294
1305
|
* byte-for-byte as it was.
|
|
1295
1306
|
*/
|
|
1296
|
-
export function
|
|
1307
|
+
export function normalizeReleaseAgeExcludes(profile: string, explicitDir?: string): string[] {
|
|
1297
1308
|
const file = join(profileDir(profile, explicitDir), 'pnpm-workspace.yaml')
|
|
1298
1309
|
let yaml: string
|
|
1299
1310
|
try { yaml = readFileSync(file, 'utf8') } catch { return [] }
|
|
@@ -1304,7 +1315,7 @@ export function mergeDuplicateReleaseAgeExcludes(profile: string, explicitDir?:
|
|
|
1304
1315
|
const eol = /\r\n/.test(yaml) ? '\r\n' : '\n'
|
|
1305
1316
|
const indentMatch = /^([ \t]+)-/.exec(block[1])
|
|
1306
1317
|
const indent = indentMatch === null ? ' ' : indentMatch[1]
|
|
1307
|
-
const entries: { rule: ReleaseAgeExcludeRule; quoted: boolean }[] = []
|
|
1318
|
+
const entries: { rule: ReleaseAgeExcludeRule; quoted: boolean; line: string }[] = []
|
|
1308
1319
|
for (const line of block[1].split(/\r?\n/)) {
|
|
1309
1320
|
if (line.trim() === '') continue
|
|
1310
1321
|
const m = /^[ \t]+-[ \t]*(.*?)[ \t]*$/.exec(line)
|
|
@@ -1312,36 +1323,56 @@ export function mergeDuplicateReleaseAgeExcludes(profile: string, explicitDir?:
|
|
|
1312
1323
|
if (m === null || m[1].includes('#')) return []
|
|
1313
1324
|
const rule = splitReleaseAgeExclude(m[1])
|
|
1314
1325
|
if (rule === null) return []
|
|
1315
|
-
entries.push({ rule, quoted: /^['"]/.test(m[1]) })
|
|
1326
|
+
entries.push({ rule, quoted: /^['"]/.test(m[1]), line })
|
|
1316
1327
|
}
|
|
1317
|
-
const byName = new Map<string, {
|
|
1328
|
+
const byName = new Map<string, {
|
|
1329
|
+
selectors: Set<string>
|
|
1330
|
+
originals: string[]
|
|
1331
|
+
lines: string[]
|
|
1332
|
+
allVersions: boolean
|
|
1333
|
+
quoted: boolean
|
|
1334
|
+
count: number
|
|
1335
|
+
}>()
|
|
1318
1336
|
const order: string[] = []
|
|
1319
|
-
for (const { rule, quoted } of entries) {
|
|
1337
|
+
for (const { rule, quoted, line } of entries) {
|
|
1320
1338
|
let group = byName.get(rule.name)
|
|
1321
1339
|
if (group === undefined) {
|
|
1322
|
-
group = { selectors: new Set(), allVersions: false, quoted: false, count: 0 }
|
|
1340
|
+
group = { selectors: new Set(), originals: [], lines: [], allVersions: false, quoted: false, count: 0 }
|
|
1323
1341
|
byName.set(rule.name, group)
|
|
1324
1342
|
order.push(rule.name)
|
|
1325
1343
|
}
|
|
1326
1344
|
group.count += 1
|
|
1327
1345
|
group.quoted = group.quoted || quoted
|
|
1346
|
+
group.originals.push(rule.selector ?? '')
|
|
1347
|
+
group.lines.push(line)
|
|
1328
1348
|
if (rule.selector === null) group.allVersions = true
|
|
1329
1349
|
else group.selectors.add(rule.selector)
|
|
1330
1350
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const lines = order.map(name => {
|
|
1351
|
+
/** The one line this package's entry is written as. */
|
|
1352
|
+
const produced = (name: string): string => {
|
|
1334
1353
|
const group = byName.get(name)
|
|
1335
1354
|
if (group === undefined) return ''
|
|
1336
|
-
|
|
1355
|
+
// A bare name already covers every version, so it stays bare; a union or
|
|
1356
|
+
// several rules for one name cannot both be read, so they become one.
|
|
1357
|
+
const bare = group.allVersions
|
|
1358
|
+
|| group.originals.some(selector => selector.includes('||'))
|
|
1359
|
+
|| group.selectors.size > 1
|
|
1360
|
+
const text = bare ? name : `${name}@${[...group.selectors][0]}`
|
|
1337
1361
|
// `@` cannot start a plain scalar in YAML, so a scoped name is written
|
|
1338
1362
|
// quoted — the way pnpm itself writes one.
|
|
1339
1363
|
const quoted = group.quoted || text.startsWith('@')
|
|
1340
1364
|
return `${indent}- ${quoted ? `'${text.replaceAll("'", "''")}'` : text}`
|
|
1365
|
+
}
|
|
1366
|
+
const rewritten = order.filter(name => {
|
|
1367
|
+
const group = byName.get(name)
|
|
1368
|
+
if (group === undefined) return false
|
|
1369
|
+
return group.count > 1 || group.lines[0] !== produced(name)
|
|
1341
1370
|
})
|
|
1371
|
+
if (rewritten.length === 0) return []
|
|
1372
|
+
const lines = order.map(produced)
|
|
1342
1373
|
const blockText = `minimumReleaseAgeExclude:${eol}${lines.join(eol)}${eol}`
|
|
1343
1374
|
// A function replacement: `$&` and friends in a string replacement would
|
|
1344
1375
|
// be read as capture references.
|
|
1345
1376
|
writeFileSync(file, yaml.replace(blockRe, () => blockText))
|
|
1346
|
-
return
|
|
1377
|
+
return rewritten
|
|
1347
1378
|
}
|
package/src/routes.ts
CHANGED
|
@@ -3866,7 +3866,15 @@ sendJson(response, 200, { updates })
|
|
|
3866
3866
|
&& capturedNpmVersion(lockfileCapture.snapshot, name) !== beforeVersion
|
|
3867
3867
|
? {
|
|
3868
3868
|
available: false,
|
|
3869
|
-
|
|
3869
|
+
// Name all three: the version on disk, the version
|
|
3870
|
+
// the lockfile records, and the one a rollback
|
|
3871
|
+
// would install (#732). The old wording named the
|
|
3872
|
+
// first two only by implication, and a user who
|
|
3873
|
+
// reads "the exact source cannot be proven" has
|
|
3874
|
+
// nothing to act on — the reproducer's way out was
|
|
3875
|
+
// to align package.json with the installed build,
|
|
3876
|
+
// which is what this now says.
|
|
3877
|
+
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.`,
|
|
3870
3878
|
lockfileBefore: lockfileCapture.snapshot,
|
|
3871
3879
|
}
|
|
3872
3880
|
: !supportsExactRollbackTarget(`${name}@${beforeVersion}`)
|