@1e0zj/dsh-plugin-mall 0.4.7 → 0.4.14
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/README.md +4 -3
- package/package.json +1 -1
- package/src/cli.js +928 -68
- package/src/client.js +25 -4
- package/src/guard.js +174 -6
- package/src/index.js +603 -42
- package/src/installer.js +39 -18
- package/src/restart-protocol.js +258 -0
- package/src/terminal.js +11 -0
package/src/client.js
CHANGED
|
@@ -29,6 +29,7 @@ window.__ModuleLoader__.load({
|
|
|
29
29
|
".mkt_btnSm{padding:3px 10px;font-size:12px}",
|
|
30
30
|
".mkt_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}",
|
|
31
31
|
".mkt_notice{border-left:3px solid var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary);padding:7px 10px;font-size:12px;line-height:18px;overflow-wrap:anywhere}",
|
|
32
|
+
".mkt_noticeInfo{border-left-color:var(--dsw-alias-state-accent-primary);color:inherit}",
|
|
32
33
|
".mkt_list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;align-items:stretch;min-width:0}",
|
|
33
34
|
"@media (max-width:820px){.mkt_list{grid-template-columns:minmax(0,1fr)}}",
|
|
34
35
|
".mkt_listHead{grid-column:1/-1;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
|
|
@@ -395,6 +396,13 @@ window.__ModuleLoader__.load({
|
|
|
395
396
|
return h("div", { className: "mkt_notice", role: "alert" }, props.message);
|
|
396
397
|
}
|
|
397
398
|
|
|
399
|
+
// 状态性提示(区别于 ErrorNotice 的 role=alert):「重启已调度」这类
|
|
400
|
+
// 成功路径的进展信息,页面自动重连后随刷新自然消失。
|
|
401
|
+
function StatusNotice(props) {
|
|
402
|
+
if (!props.message) return null;
|
|
403
|
+
return h("div", { className: "mkt_notice mkt_noticeInfo", role: "status" }, props.message);
|
|
404
|
+
}
|
|
405
|
+
|
|
398
406
|
function RestartDialog(props) {
|
|
399
407
|
useEffect(function () {
|
|
400
408
|
function onKeyDown(event) {
|
|
@@ -435,7 +443,7 @@ window.__ModuleLoader__.load({
|
|
|
435
443
|
meta: clip(props.spec || "", 50),
|
|
436
444
|
actions: actions,
|
|
437
445
|
},
|
|
438
|
-
|
|
446
|
+
h("div", { className: "mkt_desc" }, "pnpm 已拦截下列安装脚本,尚未执行任何插件代码。允许后会先复核制品再运行;暂不允许时,重启 dsh 会自动撤回这次安装。"),
|
|
439
447
|
pkgs.map(function (p, index) {
|
|
440
448
|
var facts = [];
|
|
441
449
|
if (typeof p.weeklyDownloads === "number") facts.push("周下载 " + p.weeklyDownloads.toLocaleString());
|
|
@@ -601,7 +609,6 @@ window.__ModuleLoader__.load({
|
|
|
601
609
|
job.needsApproval && job.needsApproval.length > 0
|
|
602
610
|
? h(ApprovalRequest, {
|
|
603
611
|
spec: job.spec,
|
|
604
|
-
detail: job.detail,
|
|
605
612
|
needsApproval: job.needsApproval,
|
|
606
613
|
busy: props.approving === job.spec,
|
|
607
614
|
onApprove: function (names) {
|
|
@@ -764,6 +771,9 @@ window.__ModuleLoader__.load({
|
|
|
764
771
|
var _restarting = useState(false);
|
|
765
772
|
var restarting = _restarting[0];
|
|
766
773
|
var setRestarting = _restarting[1];
|
|
774
|
+
var _restartNote = useState(null);
|
|
775
|
+
var restartNote = _restartNote[0];
|
|
776
|
+
var setRestartNote = _restartNote[1];
|
|
767
777
|
var _restartConfirm = useState(false);
|
|
768
778
|
var restartConfirm = _restartConfirm[0];
|
|
769
779
|
var setRestartConfirm = _restartConfirm[1];
|
|
@@ -1026,10 +1036,18 @@ window.__ModuleLoader__.load({
|
|
|
1026
1036
|
clearRestartPing();
|
|
1027
1037
|
setRestarting(true);
|
|
1028
1038
|
setError(null);
|
|
1039
|
+
setRestartNote(null);
|
|
1029
1040
|
var requestedAt = Date.now();
|
|
1030
1041
|
var previousHostStartedAt = Number(hostStartedAt);
|
|
1031
|
-
|
|
1042
|
+
var scheduledLogPath = null;
|
|
1043
|
+
call("restart", {}).then(function (value) {
|
|
1032
1044
|
if (restartControlRef.current.mounted !== true) return;
|
|
1045
|
+
// 响应语义是「已调度交接」而不是「重启成功」:新宿主何时接管
|
|
1046
|
+
// 由下面的轮询判断。这里只把输出会出现在哪里告诉用户。
|
|
1047
|
+
scheduledLogPath = value && value.logPath ? String(value.logPath) : null;
|
|
1048
|
+
setRestartNote(value && value.mode === "visible"
|
|
1049
|
+
? "重启已调度——dsh 正在新的控制台窗口中启动,窗口里 Ctrl+C 可停止;输出同时写入 " + scheduledLogPath
|
|
1050
|
+
: "重启已调度,输出与日志:" + (scheduledLogPath || "<home>/guard/ 下"));
|
|
1033
1051
|
var tries = 0;
|
|
1034
1052
|
var ping = setInterval(function () {
|
|
1035
1053
|
if (restartControlRef.current.mounted !== true) {
|
|
@@ -1040,7 +1058,8 @@ window.__ModuleLoader__.load({
|
|
|
1040
1058
|
if (tries > 40) {
|
|
1041
1059
|
clearRestartPing();
|
|
1042
1060
|
setRestarting(false);
|
|
1043
|
-
reportError("restart", "2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。"
|
|
1061
|
+
reportError("restart", "2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。"
|
|
1062
|
+
+ (scheduledLogPath ? "输出与日志:" + scheduledLogPath : ""));
|
|
1044
1063
|
return;
|
|
1045
1064
|
}
|
|
1046
1065
|
// Prefer identity change over wall-clock ordering: NTP may move the
|
|
@@ -1063,6 +1082,7 @@ window.__ModuleLoader__.load({
|
|
|
1063
1082
|
if (restartControlRef.current.mounted !== true) return;
|
|
1064
1083
|
clearRestartPing();
|
|
1065
1084
|
setRestarting(false);
|
|
1085
|
+
setRestartNote(null);
|
|
1066
1086
|
reportError("restart", e);
|
|
1067
1087
|
});
|
|
1068
1088
|
}, [call, clearRestartPing, hostStartedAt, reportError]);
|
|
@@ -1128,6 +1148,7 @@ window.__ModuleLoader__.load({
|
|
|
1128
1148
|
"只看已验证插件")
|
|
1129
1149
|
),
|
|
1130
1150
|
h(ErrorNotice, { message: error && (error.scope === "search" || error.scope === "restart") ? error.message : null }),
|
|
1151
|
+
restarting ? h(StatusNotice, { message: restartNote }) : null,
|
|
1131
1152
|
h(InstalledPanel, {
|
|
1132
1153
|
installed: installed,
|
|
1133
1154
|
removing: removing,
|
package/src/guard.js
CHANGED
|
@@ -24,8 +24,10 @@ import {
|
|
|
24
24
|
import { homedir, tmpdir } from "node:os";
|
|
25
25
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
26
26
|
import { createRequire } from "node:module";
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
27
28
|
import { JSON_SCHEMA, Type, load } from "js-yaml";
|
|
28
29
|
import { satisfies, validRange } from "semver";
|
|
30
|
+
import { stripTerminalControlSequences } from "./terminal.js";
|
|
29
31
|
|
|
30
32
|
// Official patches (e.g. @deepseek-ai/dsh-base, dsh-web-app) mark raw JS
|
|
31
33
|
// expressions with the scalar tag `!!js`. Construct the loader's own marker —
|
|
@@ -1450,6 +1452,84 @@ export function validateInstalledProfile(profileDir) {
|
|
|
1450
1452
|
};
|
|
1451
1453
|
}
|
|
1452
1454
|
|
|
1455
|
+
/** Stable identity for one installed-profile blocker across a transaction. */
|
|
1456
|
+
function staticBlockerFingerprint(entry) {
|
|
1457
|
+
const canonical = (value) => {
|
|
1458
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
1459
|
+
if (value !== null && typeof value === "object") {
|
|
1460
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
1461
|
+
}
|
|
1462
|
+
return value;
|
|
1463
|
+
};
|
|
1464
|
+
return createHash("sha256").update(JSON.stringify(canonical(entry))).digest("hex");
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function staticBlockerFingerprints(validation) {
|
|
1468
|
+
return validation.issues
|
|
1469
|
+
.filter((entry) => entry.severity === "block")
|
|
1470
|
+
.map(staticBlockerFingerprint);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Validate the live profile relative to the state captured before a pending
|
|
1475
|
+
* transaction. An unchanged blocker that was already present did not come
|
|
1476
|
+
* from this install/remove and must not make the transaction roll back. New
|
|
1477
|
+
* markers carry the baseline; old markers do not, and deliberately retain the
|
|
1478
|
+
* old fail-closed behaviour.
|
|
1479
|
+
*/
|
|
1480
|
+
export function validatePendingProfile(profileDir, pending = readValidatedPendingSnapshot(profileDir)) {
|
|
1481
|
+
const validation = validateInstalledProfile(profileDir);
|
|
1482
|
+
if (!Array.isArray(pending?.baselineBlockers)) {
|
|
1483
|
+
return { ...validation, allIssues: validation.issues, preexistingIssues: [] };
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const candidateName = pending.preflight?.candidate?.name ?? pending.candidate?.name;
|
|
1487
|
+
const remaining = new Map();
|
|
1488
|
+
for (const fingerprint of pending.baselineBlockers) {
|
|
1489
|
+
remaining.set(fingerprint, (remaining.get(fingerprint) ?? 0) + 1);
|
|
1490
|
+
}
|
|
1491
|
+
const issues = [];
|
|
1492
|
+
const preexistingIssues = [];
|
|
1493
|
+
for (const entry of validation.issues) {
|
|
1494
|
+
if (entry.severity !== "block") {
|
|
1495
|
+
issues.push(entry);
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
// Updating the package that owns (or participates in) an old blocker must
|
|
1499
|
+
// prove that blocker was actually fixed. Only unrelated historical issues
|
|
1500
|
+
// receive baseline treatment.
|
|
1501
|
+
const candidateRelated = entry.package === candidateName
|
|
1502
|
+
|| (Array.isArray(entry.conflictsWith) && entry.conflictsWith.includes(candidateName));
|
|
1503
|
+
if (candidateRelated) {
|
|
1504
|
+
issues.push(entry);
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
const fingerprint = staticBlockerFingerprint(entry);
|
|
1508
|
+
const count = remaining.get(fingerprint) ?? 0;
|
|
1509
|
+
if (count > 0) {
|
|
1510
|
+
preexistingIssues.push(entry);
|
|
1511
|
+
remaining.set(fingerprint, count - 1);
|
|
1512
|
+
} else {
|
|
1513
|
+
issues.push(entry);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const blockers = issues.filter((entry) => entry.severity === "block");
|
|
1517
|
+
const warnings = issues.filter((entry) => entry.severity === "warn");
|
|
1518
|
+
const verdict = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "warning" : "safe";
|
|
1519
|
+
return {
|
|
1520
|
+
ok: blockers.length === 0,
|
|
1521
|
+
verdict,
|
|
1522
|
+
issues,
|
|
1523
|
+
allIssues: validation.issues,
|
|
1524
|
+
preexistingIssues,
|
|
1525
|
+
summary: blockers.length > 0
|
|
1526
|
+
? `发现 ${blockers.length} 个本次新增阻断问题、${preexistingIssues.length} 个既有阻断问题、${warnings.length} 个警告`
|
|
1527
|
+
: preexistingIssues.length > 0
|
|
1528
|
+
? `本次变更未新增阻断问题(保留 ${preexistingIssues.length} 个既有阻断问题)`
|
|
1529
|
+
: validation.summary,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1453
1533
|
/**
|
|
1454
1534
|
* Remove leftover `node_modules` entries for packages that are no longer part
|
|
1455
1535
|
* of the profile's declared dependencies. pnpm hoists each direct dependency
|
|
@@ -1723,7 +1803,7 @@ function spawnCapture(command, args, options, onOutput, { signal, treeKill = fal
|
|
|
1723
1803
|
let child;
|
|
1724
1804
|
const chunks = [];
|
|
1725
1805
|
const push = (value) => {
|
|
1726
|
-
const text = value
|
|
1806
|
+
const text = stripTerminalControlSequences(value);
|
|
1727
1807
|
chunks.push(text);
|
|
1728
1808
|
onOutput?.(text);
|
|
1729
1809
|
};
|
|
@@ -1831,7 +1911,7 @@ export async function preflightInstall({ profileDir, spec, onOutput, signal }) {
|
|
|
1831
1911
|
const result = await spawnCapture(
|
|
1832
1912
|
plan.command,
|
|
1833
1913
|
probeAddArgs(spec),
|
|
1834
|
-
{ cwd: probeDir, env: process.env, shell: plan.shell },
|
|
1914
|
+
{ cwd: probeDir, env: pnpmGuardEnv(process.env), shell: plan.shell },
|
|
1835
1915
|
onOutput,
|
|
1836
1916
|
{ signal, treeKill: plan.treeKill },
|
|
1837
1917
|
);
|
|
@@ -1998,6 +2078,9 @@ function assertStoredTransactionMatches(pending) {
|
|
|
1998
2078
|
if (pending.operation === "remove" && stored.metadata.packageName !== pending.metadata.packageName) {
|
|
1999
2079
|
throw new Error("pending remove packageName does not match snapshot.json — refusing to act on it (left untouched for manual inspection)");
|
|
2000
2080
|
}
|
|
2081
|
+
if (JSON.stringify(stored.baselineBlockers) !== JSON.stringify(pending.baselineBlockers)) {
|
|
2082
|
+
throw new Error("pending marker baseline blockers do not match snapshot.json — refusing to act on it (left untouched for manual inspection)");
|
|
2083
|
+
}
|
|
2001
2084
|
}
|
|
2002
2085
|
|
|
2003
2086
|
/**
|
|
@@ -2037,6 +2120,12 @@ function sanitizeSnapshot(marker, home) {
|
|
|
2037
2120
|
if (!Array.isArray(marker.dependencies) || marker.dependencies.some((name) => typeof name !== "string" || !NPM_PACKAGE_NAME_RE.test(name))) {
|
|
2038
2121
|
throw new Error("pending marker has missing or corrupt dependency metadata — refusing to act on it (left untouched for manual inspection)");
|
|
2039
2122
|
}
|
|
2123
|
+
if (marker.baselineBlockers !== undefined && (
|
|
2124
|
+
!Array.isArray(marker.baselineBlockers)
|
|
2125
|
+
|| marker.baselineBlockers.some((fingerprint) => typeof fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(fingerprint))
|
|
2126
|
+
)) {
|
|
2127
|
+
throw new Error("pending marker has corrupt baseline blocker metadata — refusing to act on it (left untouched for manual inspection)");
|
|
2128
|
+
}
|
|
2040
2129
|
validatePendingTransaction(marker);
|
|
2041
2130
|
const profileDir = resolve(String(marker.profileDir ?? ""));
|
|
2042
2131
|
const profilesRoot = resolve(join(home, "profiles"));
|
|
@@ -2090,7 +2179,12 @@ export function createProfileSnapshot(profileDir, metadata = {}) {
|
|
|
2090
2179
|
} catch {
|
|
2091
2180
|
/* manifest unreadable — the rest of the snapshot still captures the bytes */
|
|
2092
2181
|
}
|
|
2093
|
-
|
|
2182
|
+
// Static validation after an install must judge what THIS transaction
|
|
2183
|
+
// changed. Without this baseline, an unrelated legacy plugin that already
|
|
2184
|
+
// shadowed host modules made every later update look guilty and forced the
|
|
2185
|
+
// marketplace itself back to its old version on restart.
|
|
2186
|
+
const baselineBlockers = staticBlockerFingerprints(validateInstalledProfile(profileDir));
|
|
2187
|
+
const snapshot = { version: SNAPSHOT_VERSION, id, dir, profileDir, createdAt: Date.now(), files, dependencies, baselineBlockers, operation: normalizedMetadata.operation, metadata: normalizedMetadata };
|
|
2094
2188
|
writeFileSync(join(dir, "snapshot.json"), JSON.stringify(snapshot, undefined, 2) + "\n");
|
|
2095
2189
|
return snapshot;
|
|
2096
2190
|
}
|
|
@@ -2127,7 +2221,7 @@ export function markPendingSnapshot(snapshot, record = {}) {
|
|
|
2127
2221
|
if (existsSync(markerPath)) {
|
|
2128
2222
|
throw new Error(`profile already has a pending install marker at ${markerPath} — run \`dsh-plugin-guard guard recover\` (or let dsh startup recovery consume it) before installing again`);
|
|
2129
2223
|
}
|
|
2130
|
-
const pending = { ...snapshot, ...record, pendingAt: Date.now() };
|
|
2224
|
+
const pending = { ...snapshot, ...record, baselineBlockers: snapshot.baselineBlockers, pendingAt: Date.now() };
|
|
2131
2225
|
// Reject producer bugs before persisting them. The read/recovery boundary
|
|
2132
2226
|
// repeats this validation because the marker is attacker-controllable.
|
|
2133
2227
|
validatePendingTransaction(pending);
|
|
@@ -2212,6 +2306,13 @@ function addedDependencyNames(pending, originalDependencies = pending?.dependenc
|
|
|
2212
2306
|
export function pnpmGuardEnv(base = process.env) {
|
|
2213
2307
|
return {
|
|
2214
2308
|
...base,
|
|
2309
|
+
// Browser jobs render plain text rather than a terminal. Prevent pnpm from
|
|
2310
|
+
// emitting colours at the source; output capture also strips CSI controls
|
|
2311
|
+
// defensively in case a child ignores these conventional switches.
|
|
2312
|
+
NO_COLOR: "1",
|
|
2313
|
+
FORCE_COLOR: "0",
|
|
2314
|
+
npm_config_color: "false",
|
|
2315
|
+
NPM_CONFIG_COLOR: "false",
|
|
2215
2316
|
npm_config_auto_install_peers: "false",
|
|
2216
2317
|
NPM_CONFIG_AUTO_INSTALL_PEERS: "false",
|
|
2217
2318
|
};
|
|
@@ -2855,7 +2956,7 @@ export function recoverProfile(profileDir) {
|
|
|
2855
2956
|
rebuild: rolled?.rebuild,
|
|
2856
2957
|
};
|
|
2857
2958
|
}
|
|
2858
|
-
const validation =
|
|
2959
|
+
const validation = validatePendingProfile(profileDir, pending);
|
|
2859
2960
|
const candidateName = pending.preflight?.candidate?.name ?? pending.candidate?.name;
|
|
2860
2961
|
const removeValidation = isRemove
|
|
2861
2962
|
? validateRemoveCompletion(pending.profileDir, candidateName)
|
|
@@ -3958,6 +4059,58 @@ async function selfTest() {
|
|
|
3958
4059
|
if (existsSync(snap.dir)) throw new Error("recoverProfile commit should delete the snapshot dir");
|
|
3959
4060
|
}
|
|
3960
4061
|
|
|
4062
|
+
// A blocker that predates the transaction is still a real profile problem,
|
|
4063
|
+
// but it is not evidence that this update broke the profile. This is the
|
|
4064
|
+
// reported marketplace self-update failure: an unrelated legacy plugin
|
|
4065
|
+
// shadows host modules, so the new mall version used to be rolled back.
|
|
4066
|
+
{
|
|
4067
|
+
const p = join(root, "profiles", "preexisting-blocker");
|
|
4068
|
+
mkdirSync(join(p, "node_modules", "legacy"), { recursive: true });
|
|
4069
|
+
mkdirSync(join(p, "node_modules", "good"), { recursive: true });
|
|
4070
|
+
writeFileSync(join(p, "package.json"), JSON.stringify({ dependencies: { legacy: "1.0.0", good: "1.0.0" } }));
|
|
4071
|
+
writeFileSync(join(p, "cordis.patch.yml"), "[]\n");
|
|
4072
|
+
writeFileSync(join(p, "node_modules", "legacy", "package.json"), JSON.stringify({
|
|
4073
|
+
name: "legacy", version: "1.0.0", dependencies: { "@deepseek-ai/dsh-llm": "*" },
|
|
4074
|
+
}));
|
|
4075
|
+
writeFileSync(join(p, "node_modules", "good", "package.json"), JSON.stringify({ name: "good", version: "1.0.0" }));
|
|
4076
|
+
if (validateInstalledProfile(p).issues.filter((entry) => entry.code === "host-module-shadow").length !== 1) {
|
|
4077
|
+
throw new Error("pre-existing blocker fixture prerequisite failed");
|
|
4078
|
+
}
|
|
4079
|
+
const snap = createProfileSnapshot(p, { fixture: true });
|
|
4080
|
+
markPendingSnapshot(snap, { spec: "good@2.0.0", preflight: { candidate: { name: "good", version: "2.0.0", kind: "plain" } } });
|
|
4081
|
+
writeFileSync(join(p, "package.json"), JSON.stringify({ dependencies: { legacy: "1.0.0", good: "2.0.0" } }));
|
|
4082
|
+
writeFileSync(join(p, "node_modules", "good", "package.json"), JSON.stringify({ name: "good", version: "2.0.0" }));
|
|
4083
|
+
const relative = validatePendingProfile(p);
|
|
4084
|
+
if (!relative.ok || relative.preexistingIssues.length !== 1 || relative.issues.some((entry) => entry.severity === "block")) {
|
|
4085
|
+
throw new Error(`unchanged pre-existing blocker must not condemn this update: ${JSON.stringify(relative)}`);
|
|
4086
|
+
}
|
|
4087
|
+
const rec = recoverProfile(p);
|
|
4088
|
+
if (rec.action !== "committed") throw new Error(`unchanged pre-existing blocker should commit this update, got ${rec.action}`);
|
|
4089
|
+
if (readJson(join(p, "node_modules", "good", "package.json")).version !== "2.0.0") {
|
|
4090
|
+
throw new Error("committing past a pre-existing blocker must keep the updated candidate");
|
|
4091
|
+
}
|
|
4092
|
+
console.log("PASS pending 基线:既有宿主模块 blocker 不再回滚无关更新");
|
|
4093
|
+
|
|
4094
|
+
const candidateProfile = join(root, "profiles", "candidate-own-blocker");
|
|
4095
|
+
mkdirSync(join(candidateProfile, "node_modules", "legacy"), { recursive: true });
|
|
4096
|
+
writeFileSync(join(candidateProfile, "package.json"), JSON.stringify({ dependencies: { legacy: "1.0.0" } }));
|
|
4097
|
+
writeFileSync(join(candidateProfile, "cordis.patch.yml"), "[]\n");
|
|
4098
|
+
writeFileSync(join(candidateProfile, "node_modules", "legacy", "package.json"), JSON.stringify({
|
|
4099
|
+
name: "legacy", version: "1.0.0", dependencies: { "@deepseek-ai/dsh-llm": "*" },
|
|
4100
|
+
}));
|
|
4101
|
+
const ownSnap = createProfileSnapshot(candidateProfile, { fixture: true });
|
|
4102
|
+
markPendingSnapshot(ownSnap, { spec: "legacy@2.0.0", preflight: { candidate: { name: "legacy", version: "2.0.0", kind: "plain" } } });
|
|
4103
|
+
writeFileSync(join(candidateProfile, "node_modules", "legacy", "package.json"), JSON.stringify({
|
|
4104
|
+
name: "legacy", version: "2.0.0", dependencies: { "@deepseek-ai/dsh-llm": "*" },
|
|
4105
|
+
}));
|
|
4106
|
+
const own = validatePendingProfile(candidateProfile);
|
|
4107
|
+
if (own.ok || !own.issues.some((entry) => entry.code === "host-module-shadow")) {
|
|
4108
|
+
throw new Error("an updated candidate must not inherit an exemption for its own old blocker");
|
|
4109
|
+
}
|
|
4110
|
+
commitPendingSnapshot(candidateProfile);
|
|
4111
|
+
console.log("PASS pending 基线:候选自身的既有 blocker 必须在更新中修复");
|
|
4112
|
+
}
|
|
4113
|
+
|
|
3961
4114
|
// Approval-pause mark, part 1: a paused marker must NEVER commit on
|
|
3962
4115
|
// recovery — not even when the static validation would pass (the version
|
|
3963
4116
|
// sits there with its build scripts never approved; committing would drop
|
|
@@ -4602,6 +4755,21 @@ async function selfTest() {
|
|
|
4602
4755
|
if (env.npm_config_auto_install_peers !== "false" || env.NPM_CONFIG_AUTO_INSTALL_PEERS !== "false") {
|
|
4603
4756
|
throw new Error("pnpmGuardEnv must disable peer auto-install");
|
|
4604
4757
|
}
|
|
4758
|
+
if (env.NO_COLOR !== "1" || env.FORCE_COLOR !== "0"
|
|
4759
|
+
|| env.npm_config_color !== "false" || env.NPM_CONFIG_COLOR !== "false") {
|
|
4760
|
+
throw new Error("pnpmGuardEnv must disable terminal colours for browser job logs");
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
let streamed = "";
|
|
4764
|
+
const coloured = await spawnCapture(
|
|
4765
|
+
process.execPath,
|
|
4766
|
+
["-e", "process.stdout.write('\\u001b[96mprobe ok\\u001b[39m\\n')"],
|
|
4767
|
+
{ env: process.env, shell: false },
|
|
4768
|
+
(text) => { streamed += text; },
|
|
4769
|
+
);
|
|
4770
|
+
if (coloured.exitCode !== 0 || coloured.output !== "probe ok\n" || streamed !== "probe ok\n") {
|
|
4771
|
+
throw new Error(`spawnCapture must strip terminal controls from captured and streamed logs: ${JSON.stringify({ coloured, streamed })}`);
|
|
4772
|
+
}
|
|
4605
4773
|
}
|
|
4606
4774
|
|
|
4607
4775
|
// candidateRestoredCompatible: version vs the restored dependency spec.
|
|
@@ -5279,4 +5447,4 @@ if (process.argv[1]?.endsWith("guard.js") && process.argv.includes("--self-test"
|
|
|
5279
5447
|
console.error(`FAIL ${error.stack ?? error.message}`);
|
|
5280
5448
|
process.exitCode = 1;
|
|
5281
5449
|
});
|
|
5282
|
-
}
|
|
5450
|
+
}
|