@1e0zj/dsh-plugin-mall 0.4.5 → 0.4.7

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/src/installer.js CHANGED
@@ -16,7 +16,7 @@ import { createHash } from "node:crypto";
16
16
  import { dump, load } from "js-yaml";
17
17
  import { DEFAULT_PROFILE_BUNDLES, PROFILE_TEMPLATES, initProfile, resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
18
18
  import { describeBuildScripts, npmNameOf } from "./github.js";
19
- import { clearPendingApprovalPause, commitPendingSnapshot, createProfileSnapshot, describeRollbackRebuild, markPendingApprovalPause, markPendingSnapshot, pausedCandidateBeforeState, pendingApprovalPaused, pnpmGuardEnv, pnpmSpawnPlan, readValidatedPendingSnapshot, rollbackPendingSnapshot, validateInstalledProfile, validateRemoveCompletion } from "./guard.js";
19
+ import { clearPendingApprovalPause, commitPendingSnapshot, createProfileSnapshot, describeRollbackRebuild, markPendingApprovalPause, markPendingSnapshot, mcpEntryAuditForInstall, pausedCandidateBeforeState, pendingApprovalPaused, pnpmGuardEnv, pnpmSpawnPlan, readValidatedPendingSnapshot, rollbackPendingSnapshot, validateInstalledProfile, validateRemoveCompletion } from "./guard.js";
20
20
 
21
21
  // ── spec normalization ──────────────────────────────────────────────────────
22
22
 
@@ -1811,6 +1811,18 @@ function runInstallInner({ profile, spec, allowBuildScripts, approvedProof, pref
1811
1811
  /** Post-success accounting: reconcile bundles, register client rows, summarize. */
1812
1812
  const finalizeSuccess = () => {
1813
1813
  const bundles = reconcileBundles(profileDir, beforeDeps);
1814
+ // 装后终检(issue #14):预检的探装禁了 lifecycle 脚本,正式安装可能
1815
+ // 在用户审批后执行了它们——入口是否真的产生,只有此刻这棵真树说了算
1816
+ // (预检对带安装期脚本的候选只给了 warn)。此刻构建要么跑完要么本来
1817
+ // 就没有,终检不过就按失败结算,外层回滚到装前快照。
1818
+ const auditIssues = mcpEntryAuditForInstall({ profileDir, candidateName: preflight?.candidate?.name ?? npmNameOf(spec) });
1819
+ if (auditIssues.length > 0) {
1820
+ push(`\n[dsh-plugin-mall] ${auditIssues[0].title}: ${auditIssues[0].detail}\n`);
1821
+ // 只描述失败,不预言恢复:回滚此刻还没发生(在外层收尾里),成没成
1822
+ // 由它自己的三态结论去说。在这里写「已回滚」,回滚失败时 detail 会
1823
+ // 同时出现「已回滚」和「无法回滚」两句打架的话。
1824
+ return { status: "failed", detail: `pnpm installed ${spec}, but ${auditIssues[0].title}: 行 ${auditIssues[0].row ?? auditIssues[0].extra?.row} 的入口 ${auditIssues[0].file ?? auditIssues[0].extra?.file} 仍缺失,安装按失败结算。` };
1825
+ }
1814
1826
  const manifest = readManifest(profileDir);
1815
1827
  const currentDeps = new Set(Object.keys(manifest.dependencies ?? {}));
1816
1828
  const clientRows = [];
@@ -3665,6 +3677,94 @@ async function runTransactionFixtures() {
3665
3677
  }
3666
3678
  }
3667
3679
 
3680
+ // 4c. 装后终检(issue #14):预检的探装禁了 lifecycle 脚本,带安装期脚本
3681
+ // 的候选入口要等构建后才可能出现——预检对它们只 warn,这里是硬闸。
3682
+ // pnpm 成功、入口在真树里仍缺 → failed + 回滚(绝不提交一块砖);
3683
+ // 构建产出了入口 → completed。beforeExit 模拟 pnpm add 的落盘效果
3684
+ // (node_modules 实体化 + manifest 依赖写入)。
3685
+ {
3686
+ const fatalPatch = `
3687
+ - insert:
3688
+ - id: mcp-x
3689
+ name: '@deepseek-ai/dsh-mcp-client'
3690
+ config:
3691
+ transport: stdio
3692
+ command: node
3693
+ args:
3694
+ - !!js dshHomePath('profiles/p/node_modules/mcp-brick-pkg/dist/mcp/index.js')
3695
+ failOnStartupError: true
3696
+ `;
3697
+ const materializeCandidate = (profileDir, withEntry) => {
3698
+ const dir = join(profileDir, "node_modules", "mcp-brick-pkg");
3699
+ mkdirSync(dir, { recursive: true });
3700
+ if (withEntry) {
3701
+ mkdirSync(join(dir, "dist", "mcp"), { recursive: true });
3702
+ writeFileSync(join(dir, "dist", "mcp", "index.js"), "");
3703
+ }
3704
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "mcp-brick-pkg", version: "1.0.0", scripts: { postinstall: "node build.js" }, dsh: { bundle: { patch: "./cordis.patch.yml" } } }));
3705
+ writeFileSync(join(dir, "cordis.patch.yml"), fatalPatch);
3706
+ const manifest = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8"));
3707
+ manifest.dependencies = { ...(manifest.dependencies ?? {}), "mcp-brick-pkg": "1.0.0" };
3708
+ writeFileSync(join(profileDir, "package.json"), JSON.stringify(manifest, undefined, 2) + "\n");
3709
+ };
3710
+ const runAuditInstall = (profileDir, withEntry) => {
3711
+ const spawn = scriptedSpawn([{ code: 0, out: "Done in 1s\n", beforeExit: () => materializeCandidate(profileDir, withEntry) }]);
3712
+ return runInstall({ profile: "p", spec: "mcp-brick-pkg", preflight: preflightStub("mcp-brick-pkg"), _profileDir: profileDir, _spawn: spawn.spawnFn, _describe: async () => [] }).done;
3713
+ };
3714
+
3715
+ {
3716
+ const { profileDir, cleanup } = makeTempProfile("audit-missing");
3717
+ try {
3718
+ const outcome = await runAuditInstall(profileDir, false);
3719
+ const depGone = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")).dependencies?.["mcp-brick-pkg"] === undefined;
3720
+ check(
3721
+ "装后终检:入口仍缺 → failed 且回滚(不提交砖)",
3722
+ outcome.status === "failed" && /入口/.test(outcome.detail ?? "") && depGone && !existsSync(pendingMarkerPath(profileDir)),
3723
+ `status=${outcome.status} detail=${(outcome.detail ?? "").slice(0, 120)}`,
3724
+ );
3725
+ } finally {
3726
+ cleanup();
3727
+ }
3728
+ }
3729
+ {
3730
+ const { profileDir, cleanup } = makeTempProfile("audit-present");
3731
+ try {
3732
+ const outcome = await runAuditInstall(profileDir, true);
3733
+ check("装后终检:构建产出了入口 → completed", outcome.status === "completed", `status=${outcome.status} detail=${(outcome.detail ?? "").slice(0, 120)}`);
3734
+ } finally {
3735
+ cleanup();
3736
+ }
3737
+ }
3738
+
3739
+ // 终检失败 + 回滚做不成(marker 被外部抽掉):detail 绝不能同时出现
3740
+ // 「已回滚」和「无法回滚」——终检文案只说失败,恢复结论由外层三态拼。
3741
+ {
3742
+ const { profileDir, cleanup } = makeTempProfile("audit-rollback-fails");
3743
+ try {
3744
+ const spawn = scriptedSpawn([{
3745
+ code: 0,
3746
+ out: "Done in 1s\n",
3747
+ beforeExit: () => {
3748
+ materializeCandidate(profileDir, false);
3749
+ rmSync(pendingMarkerPath(profileDir), { force: true }); // 抽掉还原目标
3750
+ },
3751
+ }]);
3752
+ const outcome = await runInstall({ profile: "p", spec: "mcp-brick-pkg", preflight: preflightStub("mcp-brick-pkg"), _profileDir: profileDir, _spawn: spawn.spawnFn, _describe: async () => [] }).done;
3753
+ const detail = outcome.detail ?? "";
3754
+ check(
3755
+ "终检失败 + 回滚失败 → 不预言恢复,failed 且指名 marker",
3756
+ outcome.status === "failed"
3757
+ && /仍缺失/.test(detail)
3758
+ && /could NOT be restored|marker/.test(detail)
3759
+ && !/has been rolled back/.test(detail),
3760
+ `status=${outcome.status} detail=${detail.slice(0, 160)}`,
3761
+ );
3762
+ } finally {
3763
+ cleanup();
3764
+ }
3765
+ }
3766
+ }
3767
+
3668
3768
  // 4b-2. 取消了,但回滚做不成。marker 被外部删掉(外部清理、磁盘故障、
3669
3769
  // 或本轮压根没登记上),rollbackPendingSnapshot 返回 undefined 而不抛。
3670
3770
  //