dsh-m 0.2.3 → 0.2.4
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/docs/DESIGN.md +1 -1
- package/lib/core/market.js +187 -11
- package/lib/core/npm-integrity.js +28 -0
- package/lib/core/versions.js +11 -0
- package/package.json +1 -1
- package/registry.json +11 -11
package/docs/DESIGN.md
CHANGED
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
底层原语(本机实证):`dsh plugin --profile web add|remove|update`(转发 pnpm,作用于 `$DSH_HOME/profiles/web`)。**profile 的 `package.json` 就是唯一事实源**——不引入任何额外状态文件。
|
|
65
65
|
|
|
66
|
-
- **安装(npm 源)**:装最新版并以**精确版本锁定**(不用 `^` 范围;用户指定版本必须为精确 semver,经该精确版本 endpoint 查询)。安装前对 profile 的 `package.json` / `pnpm-lock.yaml` / `pnpm-workspace.yaml`
|
|
66
|
+
- **安装(npm 源)**:装最新版并以**精确版本锁定**(不用 `^` 范围;用户指定版本必须为精确 semver,经该精确版本 endpoint 查询)。安装前对 profile 的 `package.json` / `pnpm-lock.yaml` / `pnpm-workspace.yaml` 做**字节快照**;安装后核验 importer 依赖为该精确版本,并在 lockfile `packages` 条目中比对与 npm dist 一致的 `resolution.integrity`——缺失或不一致 **fail closed** 并执行 **best-effort dependency rollback**:原子恢复快照字节 + frozen 自愈阶梯(`pnpm install --frozen-lockfile` → 仍报 `CONFIG_MISMATCH` 时把 lockfile 记录的 overrides 对齐回 `package.json#pnpm.overrides` 再复验 → `CONFIG_MISMATCH` / `OUTDATED_LOCKFILE` 顽固失配才降级 `--no-frozen-lockfile` 重建并明确告知「lockfile 已重建」;全阶梯失败才报「可能需要人工修复」)。成功路径若发现安装链丢失了 manifest 顶层未知键(如 `pnpm.overrides`,2026-09-05 升级回滚事故),从快照找回并复验 frozen 一致性,输出带 `[dsh-m 自愈]` 报告。刚发布 ~1 分钟内的 `ERR_PNPM_NO_MATCHING_VERSION` 多为 packument CDN 滞后:退避重试 2 次(5s/15s),每次重试前拉一次完整 packument 预热。不声称 node_modules 与间接依赖已字节级回滚。
|
|
67
67
|
- **安装(GitHub 源)**:解析并**锁定 commit SHA**(`github:owner/repo#sha`),skillhub 同款。
|
|
68
68
|
- **已装识别**:读 profile `package.json` dependencies,与 registry 匹配 → 标注「市场安装」;不匹配的也列出,标注「非市场安装 / 来源未知」。卸载/升级对两类都可用。
|
|
69
69
|
- **卸载**:live-disable(先让 client bundle 下线,避免 404)→ 摘除该包在 profile 的补丁条目(`pnpm-workspace.yaml` 顶层 `patchedDependencies` 与 `package.json#pnpm.patchedDependencies`;依赖移除后残留条目会令 pnpm 以 `ERR_PNPM_UNUSED_PATCH` 整单失败,只精确匹配 `pkg` / `pkg@ver`,补丁文件本体保留并计入残留报告)→ `dsh plugin remove`。**不清理插件产生的数据/配置**,但把检测到的疑似残留路径(如 `~/.dsh/<plugin>.json`)列出报告。
|
package/lib/core/market.js
CHANGED
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
* 安装 / 升级。安装语义见 DESIGN.md §3(npm 精确锁定、GitHub 锁 SHA)。
|
|
4
4
|
* Task 3:服务端 query/category/offset/limit 过滤;只对当前页查 latest(并发 ≤8、
|
|
5
5
|
* TTL cache、共享全局 deadline);unavailable 返回结构化空页;host/cli namespace 贯穿。
|
|
6
|
+
* 2026-09-05 回滚缺陷加固:B1 成功路径保留 manifest 顶层未知键(如 pnpm.overrides)、
|
|
7
|
+
* 失败路径字节级回滚后 frozen 自愈阶梯(overrides 对齐 → no-frozen 重建,B2)、
|
|
8
|
+
* 新发布后 NO_MATCHING_VERSION 的退避重试 + packument 预热(B3)。
|
|
6
9
|
*/
|
|
7
10
|
import { existsSync } from 'node:fs';
|
|
11
|
+
import { Buffer } from 'node:buffer';
|
|
8
12
|
import { readFile } from 'node:fs/promises';
|
|
9
13
|
import { join } from 'node:path';
|
|
10
14
|
import { addDshPlugin, removeDshPlugin, removePatchedDependencyEntries, runCommand } from './dsh-cli.js';
|
|
11
15
|
import { dshHome, installTimeoutMs, webProfileDir } from './env.js';
|
|
12
|
-
import { assertNpmIntegrity, readPnpmLockIntegrity, restoreSnapshots, snapshotFiles } from './npm-integrity.js';
|
|
16
|
+
import { assertNpmIntegrity, atomicWriteFile, readPnpmLockIntegrity, readPnpmLockOverrides, restoreSnapshots, snapshotFiles, } from './npm-integrity.js';
|
|
13
17
|
import { listInstalledPlugins as defaultListInstalledPlugins, readProfileDeps, removeInstalledPlugin, } from './installed.js';
|
|
14
18
|
import { setLivePluginDisabled } from './live-plugin.js';
|
|
15
19
|
import { loadRegistry as defaultLoadRegistry, } from './registry.js';
|
|
16
|
-
import { githubLatestTag as defaultGithubLatestTag, isNewerVersion, npmLatest as defaultNpmLatest, npmVersion } from './versions.js';
|
|
20
|
+
import { githubLatestTag as defaultGithubLatestTag, isNewerVersion, npmLatest as defaultNpmLatest, npmPackument as defaultNpmPackument, npmVersion, } from './versions.js';
|
|
17
21
|
// ---------- 通用工具 ----------
|
|
18
22
|
const DEFAULT_DEADLINE_MS = 60_000;
|
|
19
23
|
const WITH_LATEST_MAX = 50;
|
|
@@ -400,6 +404,157 @@ export async function listInstalledWithMeta(cfg = {}, opts = {}, deps = {}) {
|
|
|
400
404
|
async function defaultRestoreInstall(profileDir) {
|
|
401
405
|
return runCommand('pnpm', ['--dir', profileDir, 'install', '--frozen-lockfile'], { timeoutMs: installTimeoutMs() });
|
|
402
406
|
}
|
|
407
|
+
async function defaultRebuildInstall(profileDir) {
|
|
408
|
+
return runCommand('pnpm', ['--dir', profileDir, 'install', '--no-frozen-lockfile'], { timeoutMs: installTimeoutMs() });
|
|
409
|
+
}
|
|
410
|
+
/** B3 默认退避:新发布 ~1 分钟内的升级失败多为 packument CDN 滞后(2026-09-05 实证)。 */
|
|
411
|
+
const NO_MATCHING_VERSION_RETRY_DELAYS_MS = [5_000, 15_000];
|
|
412
|
+
// ---------- B1/B2:manifest 保留与 frozen 自愈(2026-09-05 事故加固) ----------
|
|
413
|
+
const LOCKFILE_CONFIG_MISMATCH_RE = /ERR_PNPM_LOCKFILE_CONFIG_MISMATCH/;
|
|
414
|
+
const OUTDATED_LOCKFILE_RE = /ERR_PNPM_OUTDATED_LOCKFILE/;
|
|
415
|
+
const NO_MATCHING_VERSION_RE = /ERR_PNPM_NO_MATCHING_VERSION/;
|
|
416
|
+
function errText(err) {
|
|
417
|
+
return err instanceof Error ? err.message : String(err);
|
|
418
|
+
}
|
|
419
|
+
function sleep(ms) {
|
|
420
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
421
|
+
}
|
|
422
|
+
async function readManifestDoc(profileDir) {
|
|
423
|
+
try {
|
|
424
|
+
const parsed = JSON.parse(await readFile(join(profileDir, 'package.json'), 'utf8'));
|
|
425
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
426
|
+
return null;
|
|
427
|
+
return parsed;
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* B1(成功路径):安装链(pnpm / 宿主 CLI)若把升级前 manifest 的顶层键丢掉
|
|
435
|
+
* (如 `pnpm.overrides` 事故前态),从安装前字节快照里找回并原子写回。
|
|
436
|
+
* 只补「快照有、现在无」的键,绝不覆盖安装刚写入的 dependencies/dsh 变更。
|
|
437
|
+
* 返回找回的键名列表;无需修复返回 null。
|
|
438
|
+
*/
|
|
439
|
+
async function restoreManifestKeys(profileDir, snapshot) {
|
|
440
|
+
if (!snapshot?.existed || snapshot.bytes === null)
|
|
441
|
+
return null;
|
|
442
|
+
let prev;
|
|
443
|
+
try {
|
|
444
|
+
const parsed = JSON.parse(snapshot.bytes.toString('utf8'));
|
|
445
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
446
|
+
return null;
|
|
447
|
+
prev = parsed;
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const cur = await readManifestDoc(profileDir);
|
|
453
|
+
if (!cur)
|
|
454
|
+
return null;
|
|
455
|
+
const missing = Object.keys(prev).filter((k) => !(k in cur));
|
|
456
|
+
if (missing.length === 0)
|
|
457
|
+
return null;
|
|
458
|
+
for (const key of missing)
|
|
459
|
+
cur[key] = prev[key];
|
|
460
|
+
const bytes = Buffer.from(`${JSON.stringify(cur, null, 2)}\n`, 'utf8');
|
|
461
|
+
await atomicWriteFile(join(profileDir, 'package.json'), bytes);
|
|
462
|
+
return missing;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* B2 第一层自愈:frozen 校验报 overrides 失配时,把 lockfile 记录的 overrides
|
|
466
|
+
* 并入 manifest 的 `pnpm.overrides`(manifest 已有条目优先,绝不删用户手写的键)。
|
|
467
|
+
* lockfile 的 overrides 是上一次解析实际生效的钉版,以此为准可让 frozen 直接通过
|
|
468
|
+
* 而无需重建依赖树(与 2026-09-05 人工修复路径等价)。未做任何修改返回 null。
|
|
469
|
+
*/
|
|
470
|
+
async function restoreOverridesFromLock(profileDir) {
|
|
471
|
+
let lockText;
|
|
472
|
+
try {
|
|
473
|
+
lockText = await readFile(join(profileDir, 'pnpm-lock.yaml'), 'utf8');
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
const lockOverrides = readPnpmLockOverrides(lockText);
|
|
479
|
+
const doc = await readManifestDoc(profileDir);
|
|
480
|
+
if (!doc)
|
|
481
|
+
return null;
|
|
482
|
+
const pnpm = doc.pnpm !== null && typeof doc.pnpm === 'object' && !Array.isArray(doc.pnpm)
|
|
483
|
+
? doc.pnpm
|
|
484
|
+
: {};
|
|
485
|
+
const current = pnpm.overrides !== null && typeof pnpm.overrides === 'object' && !Array.isArray(pnpm.overrides)
|
|
486
|
+
? pnpm.overrides
|
|
487
|
+
: {};
|
|
488
|
+
const merged = { ...lockOverrides, ...current };
|
|
489
|
+
if (JSON.stringify(merged) === JSON.stringify(current))
|
|
490
|
+
return null;
|
|
491
|
+
doc.pnpm = { ...pnpm, overrides: merged };
|
|
492
|
+
const bytes = Buffer.from(`${JSON.stringify(doc, null, 2)}\n`, 'utf8');
|
|
493
|
+
await atomicWriteFile(join(profileDir, 'package.json'), bytes);
|
|
494
|
+
const restored = Object.keys(lockOverrides).filter((k) => !(k in current));
|
|
495
|
+
return restored.length > 0 ? `(还原自 lockfile:${restored.join(', ')})` : '(与 lockfile overrides 对齐)';
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* B2 frozen 自愈阶梯:frozen install → CONFIG_MISMATCH 时先 overrides 对齐再重试 →
|
|
499
|
+
* 仍失配(或 OUTDATED_LOCKFILE specifier 漂移,实机实证:override 钉直接依赖时回滚
|
|
500
|
+
* 快照本身即 specifier 不一致)则降级 `--no-frozen-lockfile` 重建一致性。自愈动作按序
|
|
501
|
+
* 记入 notes(最终呈现给用户);阶梯走完仍失败时抛最后一个错误。
|
|
502
|
+
*/
|
|
503
|
+
async function frozenInstallWithHeal(profileDir, d, notes) {
|
|
504
|
+
try {
|
|
505
|
+
await d.restoreInstall(profileDir);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
catch (frozenErr) {
|
|
509
|
+
const text = errText(frozenErr);
|
|
510
|
+
const isConfigMismatch = LOCKFILE_CONFIG_MISMATCH_RE.test(text);
|
|
511
|
+
if (!isConfigMismatch && !OUTDATED_LOCKFILE_RE.test(text))
|
|
512
|
+
throw frozenErr;
|
|
513
|
+
if (isConfigMismatch) {
|
|
514
|
+
const merged = await restoreOverridesFromLock(profileDir);
|
|
515
|
+
if (merged !== null) {
|
|
516
|
+
notes.push(`已把 lockfile overrides 还原进 manifest ${merged}`);
|
|
517
|
+
try {
|
|
518
|
+
await d.restoreInstall(profileDir);
|
|
519
|
+
notes.push('frozen 校验通过');
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
/* 对齐后仍失配 → 走重建降级 */
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
await d.rebuildInstall(profileDir);
|
|
529
|
+
}
|
|
530
|
+
catch (rebuildErr) {
|
|
531
|
+
throw new Error(`${text};lockfile 重建(--no-frozen-lockfile)也失败:${errText(rebuildErr)}`);
|
|
532
|
+
}
|
|
533
|
+
notes.push('lockfile 已重建(--no-frozen-lockfile 完成一致性安装)');
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* B3:ERR_PNPM_NO_MATCHING_VERSION 在刚发布的窗口内几乎都是 packument CDN 滞后
|
|
538
|
+
* (2026-09-05 实证:/latest 已新、完整 packument 仍旧)。按 retryDelaysMs 退避重试,
|
|
539
|
+
* 每次重试前拉一次完整 packument 预热/校验;其他错误与重试耗尽后原样抛出。
|
|
540
|
+
*/
|
|
541
|
+
async function addDshPluginWithRetry(spec, pkg, d, retryDelaysMs, timeoutMs, signal) {
|
|
542
|
+
let attempt = 0;
|
|
543
|
+
for (;;) {
|
|
544
|
+
try {
|
|
545
|
+
return await d.addDshPlugin(spec);
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
if (!NO_MATCHING_VERSION_RE.test(errText(err)) || attempt >= retryDelaysMs.length)
|
|
549
|
+
throw err;
|
|
550
|
+
const delay = retryDelaysMs[attempt] ?? 0;
|
|
551
|
+
attempt += 1;
|
|
552
|
+
if (delay > 0)
|
|
553
|
+
await sleep(delay);
|
|
554
|
+
await d.npmPackument(pkg, timeoutMs, signal).catch(() => undefined);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
403
558
|
/** 从 registry 收录条目安装(npm → 精确锁定最新版;github → 锁 HEAD SHA)。 */
|
|
404
559
|
export async function installFromRegistry(id, cfg = {}, opts = {}, deps) {
|
|
405
560
|
const loaded = await (deps?.loadRegistry ?? defaultLoadRegistry)(cfg, { namespace: opts.namespace ?? 'host' });
|
|
@@ -416,12 +571,15 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
|
|
|
416
571
|
const d = {
|
|
417
572
|
npmLatest: deps?.npmLatest ?? defaultNpmLatest,
|
|
418
573
|
npmVersion: deps?.npmVersion ?? npmVersion,
|
|
574
|
+
npmPackument: deps?.npmPackument ?? defaultNpmPackument,
|
|
419
575
|
addDshPlugin: deps?.addDshPlugin ?? addDshPlugin,
|
|
420
576
|
removeDshPlugin: deps?.removeDshPlugin ?? removeDshPlugin,
|
|
421
577
|
readProfileDeps: deps?.readProfileDeps ?? readProfileDeps,
|
|
422
578
|
readLockIntegrity: deps?.readLockIntegrity ?? readPnpmLockIntegrity,
|
|
423
579
|
restoreInstall: deps?.restoreInstall ?? defaultRestoreInstall,
|
|
580
|
+
rebuildInstall: deps?.rebuildInstall ?? defaultRebuildInstall,
|
|
424
581
|
};
|
|
582
|
+
const retryDelaysMs = deps?.retryDelaysMs ?? NO_MATCHING_VERSION_RETRY_DELAYS_MS;
|
|
425
583
|
const profileDir = deps?.profileDir ?? webProfileDir();
|
|
426
584
|
if (entry.source === 'npm' && entry.npm) {
|
|
427
585
|
const pkg = entry.npm;
|
|
@@ -441,14 +599,18 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
|
|
|
441
599
|
if (!expectedIntegrity)
|
|
442
600
|
throw new Error(`npm metadata 缺少 dist integrity:${pkg}@${version},拒绝安装`);
|
|
443
601
|
const spec = `${pkg}@${version}`;
|
|
444
|
-
// 安装前快照:失败时 best-effort
|
|
602
|
+
// 安装前快照:失败时 best-effort 依赖回滚与 B1 键找回的依据
|
|
445
603
|
const snapshots = await snapshotFiles([
|
|
446
604
|
join(profileDir, 'package.json'),
|
|
447
605
|
join(profileDir, 'pnpm-lock.yaml'),
|
|
448
606
|
join(profileDir, 'pnpm-workspace.yaml'),
|
|
449
607
|
]);
|
|
608
|
+
const healNotes = [];
|
|
609
|
+
// 区分 add 阶段与校验阶段失败:回滚报错不再把 pnpm 安装失败误标成「integrity 校验失败」
|
|
610
|
+
let phase = 'install';
|
|
450
611
|
try {
|
|
451
|
-
const res = await d.
|
|
612
|
+
const res = await addDshPluginWithRetry(spec, pkg, d, retryDelaysMs, timeoutMs, opts.signal);
|
|
613
|
+
phase = 'verify';
|
|
452
614
|
const depsNow = await d.readProfileDeps(profileDir);
|
|
453
615
|
if (depsNow[pkg] === undefined)
|
|
454
616
|
throw new Error(`安装后未在 profile 依赖中找到 ${pkg}`);
|
|
@@ -463,6 +625,17 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
|
|
|
463
625
|
}
|
|
464
626
|
const actual = d.readLockIntegrity(lockText, pkg, version);
|
|
465
627
|
assertNpmIntegrity(expectedIntegrity, actual, pkg, version);
|
|
628
|
+
// B1(成功路径):安装链若丢了 manifest 顶层键(如 pnpm.overrides),从快照找回并复验 frozen 一致性
|
|
629
|
+
const restoredKeys = await restoreManifestKeys(profileDir, snapshots[0]);
|
|
630
|
+
if (restoredKeys) {
|
|
631
|
+
healNotes.push(`安装链丢失了 manifest 顶层键(${restoredKeys.join(', ')}),已从安装前快照找回`);
|
|
632
|
+
try {
|
|
633
|
+
await frozenInstallWithHeal(profileDir, d, healNotes);
|
|
634
|
+
}
|
|
635
|
+
catch (healErr) {
|
|
636
|
+
healNotes.push(`frozen 一致性自愈未完成,profile 可能需要人工检查:${errText(healErr)}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
466
639
|
return {
|
|
467
640
|
id: entry.id,
|
|
468
641
|
pkg,
|
|
@@ -470,18 +643,19 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
|
|
|
470
643
|
version,
|
|
471
644
|
usedAllowAllBuilds: res.usedAllowAllBuilds,
|
|
472
645
|
needsRestart: true,
|
|
473
|
-
output: res.output.slice(-800),
|
|
646
|
+
output: res.output.slice(-800) + (healNotes.length > 0 ? `\n[dsh-m 自愈] ${healNotes.join(';')}` : ''),
|
|
474
647
|
};
|
|
475
648
|
}
|
|
476
649
|
catch (err) {
|
|
477
|
-
// best-effort rollback:原子恢复 manifest/lock/workspace
|
|
650
|
+
// best-effort rollback:原子恢复 manifest/lock/workspace 快照字节,再按 frozen 自愈阶梯收敛
|
|
478
651
|
let rollbackError = null;
|
|
652
|
+
const rollbackNotes = [];
|
|
479
653
|
try {
|
|
480
654
|
await restoreSnapshots(snapshots);
|
|
481
|
-
await
|
|
655
|
+
await frozenInstallWithHeal(profileDir, d, rollbackNotes);
|
|
482
656
|
}
|
|
483
657
|
catch (rerr) {
|
|
484
|
-
rollbackError =
|
|
658
|
+
rollbackError = errText(rerr);
|
|
485
659
|
// 原先不存在该依赖且恢复安装失败:尝试移除
|
|
486
660
|
const originallyAbsent = snapshots[0] && snapshots[0].existed && (() => {
|
|
487
661
|
try {
|
|
@@ -497,14 +671,16 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
|
|
|
497
671
|
rollbackError = null;
|
|
498
672
|
}
|
|
499
673
|
catch (rmErr) {
|
|
500
|
-
rollbackError = `${rollbackError};移除 ${pkg} 也失败(${
|
|
674
|
+
rollbackError = `${rollbackError};移除 ${pkg} 也失败(${errText(rmErr)})`;
|
|
501
675
|
}
|
|
502
676
|
}
|
|
503
677
|
}
|
|
678
|
+
const prefix = phase === 'install' ? '安装失败' : 'integrity 校验失败';
|
|
504
679
|
if (rollbackError) {
|
|
505
|
-
throw new Error(
|
|
680
|
+
throw new Error(`${prefix}(${errText(err)});依赖回滚也失败(${rollbackError}),profile 可能需要人工修复`);
|
|
506
681
|
}
|
|
507
|
-
|
|
682
|
+
const suffix = rollbackNotes.length > 0 ? `(${rollbackNotes.join(';')})` : '';
|
|
683
|
+
throw new Error(`${prefix},已回滚到安装前状态${suffix}:${errText(err)}`);
|
|
508
684
|
}
|
|
509
685
|
}
|
|
510
686
|
if (entry.github) {
|
|
@@ -99,6 +99,33 @@ export function assertNpmIntegrity(expected, actual, pkg, version) {
|
|
|
99
99
|
throw new Error(`integrity 不一致:${pkg}@${version} 期望 ${expected},lockfile 实际 ${actual}`);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* 解析 pnpm lockfile v9 顶层的 `overrides:` 映射(键可带引号,值为版本串)。
|
|
104
|
+
* 无该区块返回 {}。只认 pnpm 自己生成的两空格顶层缩进;单条目解析失败即停(宁可少配,
|
|
105
|
+
* 不可错配)。frozen 自愈用它把 lockfile 记录的 overrides 还原进 manifest(2026-09-05 事故)。
|
|
106
|
+
*/
|
|
107
|
+
export function readPnpmLockOverrides(lockText) {
|
|
108
|
+
const lines = lockText.split(/\r?\n/);
|
|
109
|
+
const start = lines.findIndex((l) => l.trim() === 'overrides:');
|
|
110
|
+
const result = {};
|
|
111
|
+
if (start === -1)
|
|
112
|
+
return result;
|
|
113
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
114
|
+
const line = lines[i];
|
|
115
|
+
if (line.trim() === '')
|
|
116
|
+
continue;
|
|
117
|
+
if (!/^\s{2}\S/.test(line))
|
|
118
|
+
break;
|
|
119
|
+
const m = /^\s{2}(?:'([^']+)'|"([^"]+)"|([^\s:][^:]*?))\s*:\s*(.+?)\s*$/.exec(line);
|
|
120
|
+
if (!m)
|
|
121
|
+
break;
|
|
122
|
+
const key = m[1] ?? m[2] ?? m[3];
|
|
123
|
+
if (key === undefined)
|
|
124
|
+
break;
|
|
125
|
+
result[key] = m[4];
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
102
129
|
async function atomicWriteFile(path, bytes) {
|
|
103
130
|
await mkdir(dirname(path), { recursive: true });
|
|
104
131
|
const tmp = `${path}.restore-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -113,6 +140,7 @@ async function atomicWriteFile(path, bytes) {
|
|
|
113
140
|
await rm(path, { force: true });
|
|
114
141
|
await rename(tmp, path);
|
|
115
142
|
}
|
|
143
|
+
export { atomicWriteFile };
|
|
116
144
|
/** 记录 profile 关键文件的字节快照(package.json / pnpm-lock.yaml / pnpm-workspace.yaml)。 */
|
|
117
145
|
export async function snapshotFiles(paths) {
|
|
118
146
|
return Promise.all(paths.map(async (path) => {
|
package/lib/core/versions.js
CHANGED
|
@@ -39,6 +39,17 @@ export async function npmVersion(pkg, version, timeoutMs = 20_000, signal) {
|
|
|
39
39
|
tarball: typeof data.dist?.tarball === 'string' ? data.dist.tarball : undefined,
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* 拉取完整 packument(NO_MATCHING_VERSION 退避重试前的预热/校验原语)。
|
|
44
|
+
* 返回该包已知的全部版本号;解析不出 versions 时返回空列表(不抛)。
|
|
45
|
+
*/
|
|
46
|
+
export async function npmPackument(pkg, timeoutMs = 20_000, signal) {
|
|
47
|
+
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
48
|
+
throw new Error(`无效 npm 包名: ${pkg}`);
|
|
49
|
+
const data = await fetchJsonLimited(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`, { timeoutMs, signal, maxBytes: 8 * 1024 * 1024 });
|
|
50
|
+
const versions = data?.versions !== null && typeof data?.versions === 'object' ? Object.keys(data.versions) : [];
|
|
51
|
+
return { versions };
|
|
52
|
+
}
|
|
42
53
|
export async function npmLatest(pkg, timeoutMs = 20_000, signal) {
|
|
43
54
|
// 允许 scoped 包名:@scope/name(isSafePkgName 同款字符集)
|
|
44
55
|
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
3
|
"plugins": [
|
|
4
|
+
{
|
|
5
|
+
"id": "dsh-m",
|
|
6
|
+
"name": "DSH Marketplace",
|
|
7
|
+
"description": "可自定义 Registry 内容的 DeepSeek Harness 插件市场:收录、安装、卸载、升级 DSH 插件,一站搞定。",
|
|
8
|
+
"category": "market",
|
|
9
|
+
"tags": ["市场", "插件管理"],
|
|
10
|
+
"source": "npm",
|
|
11
|
+
"npm": "dsh-m",
|
|
12
|
+
"github": "iasiv5/dsh-m",
|
|
13
|
+
"homepage": "https://github.com/iasiv5/dsh-m"
|
|
14
|
+
},
|
|
4
15
|
{
|
|
5
16
|
"id": "dsh-skins",
|
|
6
17
|
"name": "DSH Skins",
|
|
@@ -23,17 +34,6 @@
|
|
|
23
34
|
"github": "iasiv5/dsh-copilot-auth",
|
|
24
35
|
"homepage": "https://github.com/iasiv5/dsh-copilot-auth"
|
|
25
36
|
},
|
|
26
|
-
{
|
|
27
|
-
"id": "dsh-m",
|
|
28
|
-
"name": "DSH Marketplace",
|
|
29
|
-
"description": "可自定义 Registry 内容的 DeepSeek Harness 插件市场:收录、安装、卸载、升级 DSH 插件,一站搞定。",
|
|
30
|
-
"category": "market",
|
|
31
|
-
"tags": ["市场", "插件管理"],
|
|
32
|
-
"source": "npm",
|
|
33
|
-
"npm": "dsh-m",
|
|
34
|
-
"github": "iasiv5/dsh-m",
|
|
35
|
-
"homepage": "https://github.com/iasiv5/dsh-m"
|
|
36
|
-
},
|
|
37
37
|
{
|
|
38
38
|
"id": "dsh-skip-browser-auth",
|
|
39
39
|
"name": "DSH Skip Browser Auth",
|