@noob-stupid/dsh-plugin-console 0.5.85 → 0.5.87

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.
@@ -0,0 +1,120 @@
1
+ // L2 · routes —— link 面板的**两个写动作**(0.5.87 加法)
2
+ // POST /plugin-console/link-repair 缺什么补什么(kind: deps 补依赖 / shims 造垫片 / all 两件都做)
3
+ // POST /plugin-console/unpin-dev-dir 取消钉住(两段式:先只读计划,confirm:true 才移除)
4
+ //
5
+ // 为什么单独一个文件:`routes/state.js` 已经 439 行(上限 600),而这两条动作与「只读清单」
6
+ // 是两种职责(写 vs 读)。判据一律在 domain 层(link-self-sufficiency.js / unpin-dev-dir.js),
7
+ // 本文件只做三件事:**白名单校验 → 调 domain → 如实回包**。
8
+ //
9
+ // 安全语义(与 routes/revoke-force.js、routes/disk.js 同一套):
10
+ // · 包名必须是合法 npm 包名(NPM_NAME_RE),且**必须真的是本 profile 里的 link:/file: 依赖** ——
11
+ // 不接受任意包名,也不对 registry 依赖动手(那不是"开发式链接");
12
+ // · 路径**绝不从请求体来** —— profileDir 由服务端按当前 profile 决定;
13
+ // · 未确认(confirm!==true)时只回计划,一个字节都不写。
14
+ import { dirname } from 'node:path'
15
+ import { NPM_NAME_RE } from '../domain/dep-source.js'
16
+ import { pathDepsOf } from '../domain/link-inventory.js'
17
+ import { repairLinkTarget } from '../domain/link-self-sufficiency.js'
18
+ import { applyUnpinDevDir, planUnpinDevDir } from '../domain/unpin-dev-dir.js'
19
+ import { sendError, sendJson } from '../infra/httpd.js'
20
+ import { dshHome, findPatchPath } from '../infra/paths.js'
21
+
22
+ /** 当前 profile 根目录(拿不到就 null —— 调用方必须当成"拒绝改动"处理)。 */
23
+ function profileDirOfCtx(ctx) {
24
+ try { return dirname(findPatchPath(ctx)) } catch { return null }
25
+ }
26
+
27
+ /** 请求体 → 合法包名(不合法就空串)。 */
28
+ function packageNameOf(body) {
29
+ const raw = body !== null && typeof body === 'object' && typeof body.packageName === 'string' ? body.packageName.trim() : ''
30
+ return NPM_NAME_RE.test(raw) ? raw : ''
31
+ }
32
+
33
+ /**
34
+ * POST /link-repair —— 把"缺依赖 / 缺 peer"这两条出路做成可点动作。
35
+ * body: { packageName, kind?: 'deps' | 'shims' | 'all' }
36
+ */
37
+ async function routeLinkRepair(req, res, rc) {
38
+ const body = rc.body ?? {}
39
+ const packageName = packageNameOf(body)
40
+ if (packageName === '') { sendError(res, 400, '包名无效(只接受合法 npm 包名)'); return }
41
+ const kind = body.kind === 'deps' ? 'deps' : body.kind === 'shims' ? 'shims' : 'all'
42
+ const profileDir = profileDirOfCtx(rc.ctx)
43
+ if (typeof profileDir !== 'string' || profileDir === '') { sendError(res, 400, '拿不到 profile 目录,拒绝改动'); return }
44
+ // 只对**确实是本 profile 里的路径型依赖**生效(不凭空对任意包名动手)
45
+ const dep = pathDepsOf(profileDir).find((x) => x.packageName === packageName) ?? null
46
+ if (dep === null) {
47
+ sendError(res, 400, `${packageName} 不是本 profile 里的 link:/file: 依赖 —— 这份清单只处理开发式链接,不改别的东西`)
48
+ return
49
+ }
50
+ const result = await repairLinkTarget({ profileDir, packageName, kind, spec: dep.spec, home: dshHome() })
51
+ const plan = result.plan ?? null
52
+ const payload = {
53
+ ok: result.ok === true,
54
+ kind: result.kind,
55
+ packageName,
56
+ ready: result.ready === true,
57
+ changed: result.changed === true,
58
+ installed: result.installed === null || result.installed === undefined
59
+ ? null
60
+ : { ok: result.installed.ok === true, error: result.installed.error ?? null },
61
+ created: Array.isArray(result.created) ? result.created : [],
62
+ skipped: Array.isArray(result.skipped) ? result.skipped : [],
63
+ target: plan?.target ?? null,
64
+ missingDeps: plan?.missingDeps ?? [],
65
+ missingPeers: plan?.missingPeers ?? [],
66
+ shimmablePeers: plan?.shimmablePeers ?? [],
67
+ note: result.note ?? null,
68
+ error: result.ok === true ? null : (result.note ?? '补齐失败'),
69
+ }
70
+ if (result.ok === true) sendJson(res, 200, payload)
71
+ else sendError(res, 500, payload.error, payload)
72
+ }
73
+
74
+ /**
75
+ * POST /unpin-dev-dir —— 取消钉住(摘掉清单里那条 link:/file: 依赖)。
76
+ * body: { packageName, confirm?: boolean }
77
+ * confirm !== true → 只读计划(面板先给人看会怎样)
78
+ * confirm === true → 备份清单 → pnpm remove → **读回三处** → 失败即回滚
79
+ */
80
+ async function routeUnpinDevDir(req, res, rc) {
81
+ const body = rc.body ?? {}
82
+ const packageName = packageNameOf(body)
83
+ if (packageName === '') { sendError(res, 400, '包名无效(只接受合法 npm 包名)'); return }
84
+ const profileDir = profileDirOfCtx(rc.ctx)
85
+ if (typeof profileDir !== 'string' || profileDir === '') { sendError(res, 400, '拿不到 profile 目录,拒绝改动'); return }
86
+ if (body.confirm !== true) {
87
+ const plan = planUnpinDevDir({ profileDir, packageName })
88
+ if (plan.ok !== true) {
89
+ sendError(res, plan.status ?? 400, plan.error ?? '取消钉住规划失败', { ok: false, packageName, spec: plan.spec ?? null })
90
+ return
91
+ }
92
+ sendJson(res, 200, {
93
+ ok: true, applied: false, packageName, spec: plan.spec, target: plan.target,
94
+ targetExists: plan.targetExists, inBundles: plan.inBundles, bundles: plan.bundles,
95
+ warnings: plan.warnings, steps: plan.steps,
96
+ })
97
+ return
98
+ }
99
+ // 真删:用与 /uninstall 同一条 pnpm 通道(rc.deps.pnpmRemove),保持"一个动作负责三处"
100
+ const injected = rc.deps !== null && typeof rc.deps === 'object' && typeof rc.deps.pnpmRemove === 'function' ? rc.deps.pnpmRemove : null
101
+ const runRemove = injected === null ? null : (args) => injected(args.profileDir, args.packageName)
102
+ const result = await applyUnpinDevDir({ profileDir, packageName, runRemove })
103
+ const payload = {
104
+ ok: result.ok === true,
105
+ applied: result.changed === true,
106
+ packageName,
107
+ spec: result.spec ?? null,
108
+ target: result.target ?? null,
109
+ inBundles: result.inBundles === true,
110
+ removed: result.removed ?? null,
111
+ backup: result.backup ?? null,
112
+ warnings: result.warnings ?? [],
113
+ note: result.note ?? null,
114
+ error: result.ok === true ? null : (result.error ?? '取消钉住失败'),
115
+ }
116
+ if (result.ok === true) sendJson(res, 200, payload)
117
+ else sendError(res, result.status ?? 400, payload.error, payload)
118
+ }
119
+
120
+ export { routeLinkRepair, routeUnpinDevDir }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.85",
3
+ "version": "0.5.87",
4
4
  "description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
5
5
  "repository": {
6
6
  "type": "git",