@noob-stupid/dsh-plugin-console 0.5.37 → 0.5.39
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/lib/client.js +30 -0
- package/lib/server/domain/bundle-refs.js +9 -2
- package/lib/server/domain/dep-source.js +61 -2
- package/lib/server/domain/install-cleanup.js +23 -2
- package/lib/server/domain/install-job.js +35 -47
- package/lib/server/domain/install.js +3 -0
- package/lib/server/domain/lock-importer.js +411 -0
- package/lib/server/domain/manifest.js +31 -5
- package/lib/server/domain/patch.js +14 -3
- package/lib/server/domain/persist.js +401 -0
- package/lib/server/domain/plugin-actions.js +42 -3
- package/lib/server/domain/selfupdate.js +210 -12
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -113,6 +113,7 @@ window.__ModuleLoader__.load({
|
|
|
113
113
|
// 2026-09-27 加法:安装结果旁的「建议动作」执行框(短句进面板,长解释进悬浮 title)
|
|
114
114
|
actionBoxTitle: "建议动作",
|
|
115
115
|
actionPinLabel: "钉住该依赖(按 link: 记录)",
|
|
116
|
+
actionPersistLabel: "一键钉住(清单 + lock + 挂载行)",
|
|
116
117
|
actionReconcileLabel: "重建 pnpm-lock.yaml",
|
|
117
118
|
actionRun: "执行",
|
|
118
119
|
actionCopy: "复制",
|
|
@@ -279,11 +280,16 @@ window.__ModuleLoader__.load({
|
|
|
279
280
|
subpackagesEmpty: "未发现可安装的子包。",
|
|
280
281
|
recentFailures: "最近安装失败",
|
|
281
282
|
installed: "已安装并启用",
|
|
283
|
+
// 0.5.38 加法:三处没齐备(清单 / lock / 挂载行)时**绝不**说"已安装并启用"——
|
|
284
|
+
// 那种状态下任何一次 pnpm 操作或重启都会把它清掉(真机 dshmarket 现场)。
|
|
285
|
+
installedNotPersisted: "⚠ 已装上但未持久化(重启/重装会消失)",
|
|
282
286
|
installNote: "HMR 正在生效;若插件带界面,请刷新页面",
|
|
283
287
|
bundleNote: "该包是 bundle 层插件(如皮肤合集),重启 DSH 服务后生效",
|
|
284
288
|
// ── 安装诊断短句(2026-09-26 加法):面板只放短句;解释性长文留在服务端字段里,
|
|
285
289
|
// 前端只把它挂到 title 悬浮提示上,不铺进面板(用户明确要求)──
|
|
286
290
|
diagCardTitle: "安装诊断",
|
|
291
|
+
// 0.5.38 加法:持久化判据(服务端 domain/persist.js)——三处任一没就位就出这张卡
|
|
292
|
+
diagNotPersisted: "未持久化:清单 / lock / 挂载行有缺口",
|
|
287
293
|
// 分隔符也进字典:英文界面用半角标点,避免中文全角标点混进英文句子
|
|
288
294
|
diagColon: ":",
|
|
289
295
|
diagSemi: ";",
|
|
@@ -674,6 +680,7 @@ window.__ModuleLoader__.load({
|
|
|
674
680
|
safetyLockRepairBtn: "Rebuild lock (explicit)",
|
|
675
681
|
actionBoxTitle: "Suggested action",
|
|
676
682
|
actionPinLabel: "Pin this dependency (record as link:)",
|
|
683
|
+
actionPersistLabel: "Pin it in one click (manifest + lock + mount row)",
|
|
677
684
|
actionReconcileLabel: "Rebuild pnpm-lock.yaml",
|
|
678
685
|
actionRun: "Run",
|
|
679
686
|
actionCopy: "Copy",
|
|
@@ -838,11 +845,13 @@ window.__ModuleLoader__.load({
|
|
|
838
845
|
subpackagesEmpty: "No installable subpackages found.",
|
|
839
846
|
recentFailures: "Recent install failures",
|
|
840
847
|
installed: "Installed and enabled",
|
|
848
|
+
installedNotPersisted: "⚠ Installed but NOT persisted (it disappears on restart/reinstall)",
|
|
841
849
|
installNote: "HMR is applying; refresh the page if the plugin ships UI",
|
|
842
850
|
bundleNote: "This package is a bundle-layer plugin (e.g. skin packs); restart the DSH service to activate it",
|
|
843
851
|
// ── install-diagnosis one-liners (2026-09-26, additive): the panel shows short sentences only;
|
|
844
852
|
// the long server-side explanations go to the title tooltip, never into the panel body ──
|
|
845
853
|
diagCardTitle: "Install diagnosis",
|
|
854
|
+
diagNotPersisted: "Not persisted: a gap remains in manifest / lock / mount row",
|
|
846
855
|
// separators live in the dictionary too: English uses half-width punctuation
|
|
847
856
|
diagColon: ": ",
|
|
848
857
|
diagSemi: "; ",
|
|
@@ -3282,6 +3291,12 @@ window.__ModuleLoader__.load({
|
|
|
3282
3291
|
// 「预设:新建会话时选择」(点击安装的当下没有新会话,必须先说清楚下一步)。
|
|
3283
3292
|
setMessage(t("presetInstalledMsg") + ":" + data.presetNote + t("presetDot") + diagMessage(data));
|
|
3284
3293
|
setReloadHint(true);
|
|
3294
|
+
} else if (data.persisted === false) {
|
|
3295
|
+
// 0.5.38 加法(用户诉求「下载下来就不用 lock、重启也不会消失」):
|
|
3296
|
+
// 三处没齐备时**绝不报"已安装并启用"** —— 说清它还缺什么、后果是什么,
|
|
3297
|
+
// 并把服务端下发的「一键钉住」执行框带出来(下面的 suggestedAction 分支)。
|
|
3298
|
+
setMessage(t("installedNotPersisted") + ":" + data.packageName + (typeof data.persistNote === "string" && data.persistNote !== "" ? "。" + data.persistNote : "") + diagMessage(data));
|
|
3299
|
+
setReloadHint(true);
|
|
3285
3300
|
} else {
|
|
3286
3301
|
setMessage(t("installed") + ":" + data.packageName + "(" + (data.entryId ?? "") + ")。" + (data.curlNote ?? (data.bundle ? t("bundleNote") : t("installNote"))) + (data.bundleNote ? "。" + data.bundleNote : "") + (data.lockNote ? "。⚠️ " + data.lockNote : "") + (data.depNote ? "。" + data.depNote : "") + diagMessage(data));
|
|
3287
3302
|
}
|
|
@@ -3293,6 +3308,9 @@ window.__ModuleLoader__.load({
|
|
|
3293
3308
|
}
|
|
3294
3309
|
if (data.bundle) {
|
|
3295
3310
|
setRestartHint(true);
|
|
3311
|
+
} else if (data.persisted === false) {
|
|
3312
|
+
// 未持久化:不自动刷新(卡片上的「一键钉住」还没点,刷新会把结果冲掉)
|
|
3313
|
+
setReloadHint(true);
|
|
3296
3314
|
} else if ((data.suggestedAction !== null && data.suggestedAction !== undefined)
|
|
3297
3315
|
|| (typeof data.presetNote === "string" && data.presetNote !== "")) {
|
|
3298
3316
|
// 有建议动作时不自动刷新页面(2026-09-27 加法):旧行为 2.5 秒后 reload,
|
|
@@ -3428,6 +3446,17 @@ window.__ModuleLoader__.load({
|
|
|
3428
3446
|
title: channelNotes.join("\n"),
|
|
3429
3447
|
});
|
|
3430
3448
|
}
|
|
3449
|
+
// ⑤ 持久化判据(0.5.38 加法,服务端 domain/persist.js:{ missing, actual, note }):
|
|
3450
|
+
// 三处(清单 spec / pnpm-lock.yaml 条目 / 挂载行)任一没就位 → error 级诊断行,
|
|
3451
|
+
// 完整缺口清单挂 title。**装成功但没钉住**的作业从此不会只显示"已安装并启用"。
|
|
3452
|
+
if (view.status === "done" && view.persisted === false) {
|
|
3453
|
+
lines.push({
|
|
3454
|
+
key: "persist",
|
|
3455
|
+
text: t("diagNotPersisted") + (Array.isArray(view.persist?.missing) && view.persist.missing.length > 0 ? t("diagColon") + view.persist.missing.join(" / ") : ""),
|
|
3456
|
+
level: "error",
|
|
3457
|
+
title: typeof view.persistNote === "string" && view.persistNote !== "" ? view.persistNote : (typeof view.persist?.note === "string" ? view.persist.note : null),
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3431
3460
|
return lines;
|
|
3432
3461
|
};
|
|
3433
3462
|
/** 诊断短句 → 消息栏后缀(空串 = 没问题):只在既有文案**末尾追加**,一个字都不替换。 */
|
|
@@ -5731,6 +5760,7 @@ window.__ModuleLoader__.load({
|
|
|
5731
5760
|
if (action === null || typeof action.command !== "string" || state.closed === true) return null;
|
|
5732
5761
|
const running = state.phase === "running";
|
|
5733
5762
|
const label = action.kind === "pin-dependency" ? t("actionPinLabel")
|
|
5763
|
+
: action.kind === "persist-plugin" ? t("actionPersistLabel")
|
|
5734
5764
|
: action.kind === "reconcile-lock" ? t("actionReconcileLabel")
|
|
5735
5765
|
: action.kind === "allow-builds" ? t("actionAllowLabel")
|
|
5736
5766
|
: action.kind === "overwrite-preset" ? t("actionOverwritePresetLabel")
|
|
@@ -31,8 +31,15 @@ function readBundlePatchRefNames(profileDir, pkgName) {
|
|
|
31
31
|
if (/^- insert:\s*$/u.test(line)) { inInsert = true; continue }
|
|
32
32
|
if (/^- /u.test(line) && !/^ {4}- /u.test(line)) inInsert = false
|
|
33
33
|
if (!inInsert) continue
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// ★ 2026-10-04 改错(0.5.38):`name:` 的值**加不加引号都是合法 YAML** —— 真机 dsh-whale-widget
|
|
35
|
+
// 写的是不带引号的 `name: dsh-whale-widget`,旧判据只认带引号 ⇒ 引用清单为空 ⇒ 注册前的
|
|
36
|
+
// "引用包是否都可解析"这道防崩闸门对这类包**形同不存在**。现在两种形态都认。
|
|
37
|
+
const nameM = (lines[i + 1] ?? '').match(/^ {6}name:\s*(.+?)\s*$/u)
|
|
38
|
+
if (nameM) {
|
|
39
|
+
const raw = nameM[1].replace(/\s+#.*$/u, '').trim()
|
|
40
|
+
const value = /^(['"]).*\1$/u.test(raw) ? raw.slice(1, -1) : raw
|
|
41
|
+
if (value !== '') names.push(value)
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
44
|
return names
|
|
38
45
|
} catch { return [] }
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// 不经 tarball 完整性校验,lock 删掉重建、node_modules 清空重装都稳定通过。
|
|
21
21
|
|
|
22
22
|
import { existsSync, rmSync, mkdirSync, realpathSync } from 'node:fs'
|
|
23
|
-
import { basename, dirname, join } from 'node:path'
|
|
23
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
24
24
|
import { copyTree } from '../infra/fsx.js'
|
|
25
25
|
import { fetchJsonUrl } from '../infra/http.js'
|
|
26
26
|
import { dshHome } from '../infra/paths.js'
|
|
@@ -113,6 +113,65 @@ function linkSpecFor(dir) {
|
|
|
113
113
|
return `link:${String(dir).replace(/\\/gu, '/')}`
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* 依赖规格的**唯一规范形**(0.5.39 加法):`link:` / `file:` 这类**路径型**来源统一成 pnpm 实际写出的
|
|
118
|
+
* 形态 —— **正斜杠**(是绝对还是相对保持调用方给的选择:pnpm 的 lock 里由它自己决定,见
|
|
119
|
+
* lock-importer.js#lockVersionText)。其它 spec(版本号/范围/dist-tag/URL/git)只做 trim。
|
|
120
|
+
*
|
|
121
|
+
* 为什么必须归一(真机证据,2026-10-04):desktop profile 的 `@noob-stupid/dsh-connection-card-host`
|
|
122
|
+
* 清单里是 `link:D:\dsh-link\dsh-connection-card-host`(**反斜杠**),而所有写回路径都是逐字比较
|
|
123
|
+
* (`pinProfileDependency` / `setProfileDependency` / `declareProfileDependency` 的 `deps[name] === spec`)
|
|
124
|
+
* ⇒ 任何一次「钉住」都会把它改成正斜杠 —— 用户没要求的漂移,而且幂等性彻底失效(每次都"有变化")。
|
|
125
|
+
*/
|
|
126
|
+
function canonicalSourceSpec(spec) {
|
|
127
|
+
if (typeof spec !== 'string') return null
|
|
128
|
+
const s = spec.trim()
|
|
129
|
+
const m = /^(link:|file:)([\s\S]*)$/u.exec(s)
|
|
130
|
+
if (m === null) return s
|
|
131
|
+
let target = m[2].replace(/\\/gu, '/').replace(/\/{2,}/gu, '/')
|
|
132
|
+
// 去掉尾部多余的斜杠(但别把盘根 `C:/` 或 POSIX 根 `/` 也削掉)
|
|
133
|
+
if (target.length > 1 && /\/$/u.test(target) && !/:\/$/u.test(target)) target = target.replace(/\/+$/u, '')
|
|
134
|
+
return `${m[1]}${target}`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 路径型 spec 的目标目录(相对写法按 profile 目录解析);非路径型返回 null。 */
|
|
138
|
+
function sourceSpecTarget(profileDir, spec) {
|
|
139
|
+
const canonical = canonicalSourceSpec(spec)
|
|
140
|
+
if (canonical === null) return null
|
|
141
|
+
const m = /^(link:|file:)([\s\S]*)$/u.exec(canonical)
|
|
142
|
+
if (m === null || m[2] === '') return null
|
|
143
|
+
const raw = m[2]
|
|
144
|
+
try {
|
|
145
|
+
return isAbsolute(raw) ? raw : resolve(String(profileDir ?? ''), raw)
|
|
146
|
+
} catch {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 两种写法是否**指向同一个东西**(0.5.39 加法,Fix 2 的判据):等价即视为**完好、零写盘**。
|
|
153
|
+
* · 先比规范形(`link:C:/x` ↔ `link:C:\x` ↔ `link:C:/x/` 全部等价);
|
|
154
|
+
* · 路径型再比 realpath(相对↔绝对、Windows 大小写差异、符号链接指向同一目录都算等价)。
|
|
155
|
+
* 只用它做"要不要写"的判断;真正写盘时一律写**规范形**(canonicalSourceSpec)。
|
|
156
|
+
*/
|
|
157
|
+
function sameSourceSpec(profileDir, a, b) {
|
|
158
|
+
const na = canonicalSourceSpec(a)
|
|
159
|
+
const nb = canonicalSourceSpec(b)
|
|
160
|
+
if (na === null || nb === null) return false
|
|
161
|
+
if (na === nb) return true
|
|
162
|
+
if (!/^(?:link:|file:)/u.test(na) || !/^(?:link:|file:)/u.test(nb)) return false
|
|
163
|
+
const pa = sourceSpecTarget(profileDir, na)
|
|
164
|
+
const pb = sourceSpecTarget(profileDir, nb)
|
|
165
|
+
if (pa === null || pb === null) return false
|
|
166
|
+
try {
|
|
167
|
+
const ra = realpathSync(pa)
|
|
168
|
+
const rb = realpathSync(pb)
|
|
169
|
+
return process.platform === 'win32' ? ra.toLowerCase() === rb.toLowerCase() : ra === rb
|
|
170
|
+
} catch {
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
116
175
|
/**
|
|
117
176
|
* manifest 里的 `link:` 规格当前是否**真的**还生效(`node_modules/<包名>` 就是指向它的那个链接)。
|
|
118
177
|
* 为什么必须查:release / curl 通道更新包时是「先 rmSync 再 copyTree」,会把 `node_modules/<包名>`
|
|
@@ -198,4 +257,4 @@ function suggestedReconcileAction({ profileDir = null, profile = null, reason =
|
|
|
198
257
|
}
|
|
199
258
|
}
|
|
200
259
|
|
|
201
|
-
export { PLUGIN_SRC_DIR, NPM_NAME_RE, probeRegistryPackage, probeFailureKind, materializePackageForLink, linkSpecFor, linkSpecIsIntact, profileNameOf, suggestedAllowBuildsAction, suggestedPinAction, suggestedReconcileAction }
|
|
260
|
+
export { PLUGIN_SRC_DIR, NPM_NAME_RE, probeRegistryPackage, probeFailureKind, materializePackageForLink, linkSpecFor, linkSpecIsIntact, profileNameOf, suggestedAllowBuildsAction, suggestedPinAction, suggestedReconcileAction, canonicalSourceSpec, sameSourceSpec, sourceSpecTarget }
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
// L1 · domain —— install-cleanup.js(安装失败清场 +
|
|
1
|
+
// L1 · domain —— install-cleanup.js(安装失败清场 + 授权被拒/超时后的失败文案 + pnpm remove)
|
|
2
2
|
// 2026-09-27 从 install-job.js 原样搬出(install-job.js 触到 600 行硬顶,见 test-architecture-guard.mjs)。
|
|
3
3
|
// 只搬移、未改逻辑;install-job.js 继续 re-export 这两个名字,调用点与测试的 import 面一个字都没变。
|
|
4
|
+
// 2026-10-04:`pnpmRemove` 同样**原样搬入**(0.5.38 要给安装收口腾行数;卸载路径与清场同族)。
|
|
4
5
|
|
|
5
6
|
import { existsSync, readdirSync } from 'node:fs'
|
|
6
7
|
import { basename, dirname, join } from 'node:path'
|
|
7
8
|
import { AI_CONSENT_TIMEOUT_MS } from './install.js'
|
|
8
9
|
import { disposeDir, disposeNote } from '../infra/fsx.js'
|
|
10
|
+
import { buildPnpmEnv, runPnpmWithFallback } from '../infra/exec.js'
|
|
9
11
|
|
|
10
12
|
/** 安装失败清场:把本次尝试过的候选包目录与 pnpm `_tmp_` 半成品一起清掉,并**如实汇报**清了什么、什么没清掉
|
|
11
13
|
* (2026-09-20 真装演练:聚合仓库跑 19 分钟后失败,node_modules 留着 `…_tmp_56272_2` 半成品,面板只说"安装失败")。
|
|
@@ -59,4 +61,23 @@ function aiConsentFailureText(decision, leftovers, timeoutMs = AI_CONSENT_TIMEOU
|
|
|
59
61
|
return `${base}${cleanedNote}${trashedNote}${failedNote}`
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
/** 用 corepack pnpm 移除包(与安装同一管理器与凭据抑制环境)。
|
|
65
|
+
* 注意:pnpm remove 不支持 --registry 选项,去掉避免静默失败。
|
|
66
|
+
* 2026-10-04 从 install-job.js **原样搬入**(那边要给安装收口腾行数;install-job.js 继续 re-export)。 */
|
|
67
|
+
async function pnpmRemove(profileDir, packageName) {
|
|
68
|
+
const args = ['remove', packageName]
|
|
69
|
+
const opts = {
|
|
70
|
+
cwd: profileDir,
|
|
71
|
+
timeout: 120000,
|
|
72
|
+
windowsHide: true,
|
|
73
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
74
|
+
// 2026-09-26:env 统一走 buildPnpmEnv(在既有 COREPACK_NPM_REGISTRY / 禁交互之上追加
|
|
75
|
+
// npm_config_fetch_timeout / npm_config_fetch_retries / CI=true)。**不加** --fetch-timeout 类
|
|
76
|
+
// CLI 选项:实测 `pnpm remove --fetch-timeout=…` 会 `[ERROR] Unknown options` 直接失败。
|
|
77
|
+
env: buildPnpmEnv('https://registry.npmmirror.com'),
|
|
78
|
+
}
|
|
79
|
+
// 与 pnpmInstall 同一套跨平台定位(Linux 的 corepack 不在 <node bin>/node_modules 下)
|
|
80
|
+
await runPnpmWithFallback(args, { execOpts: opts })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { cleanupAttemptedCandidates, aiConsentFailureText, pnpmRemove }
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
// 分层 Step 8c-2 从 lib/index.js 搬出;形参由 ctx 收窄为 ports(调用方注入的窄接口),只搬移未改逻辑。
|
|
3
3
|
// 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
|
|
4
4
|
|
|
5
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
6
5
|
import { readFile } from 'node:fs/promises'
|
|
7
6
|
import { dirname, join, resolve } from 'node:path'
|
|
8
7
|
import { aiRepair } from './ai-run.js'
|
|
9
8
|
import { maybeAutoAdaptCompat } from './compat.js'
|
|
10
9
|
import { getReposDir } from './repoland.js'
|
|
11
|
-
import { aiConsentFailureText, cleanupAttemptedCandidates } from './install-cleanup.js'
|
|
10
|
+
import { aiConsentFailureText, cleanupAttemptedCandidates, pnpmRemove } from './install-cleanup.js'
|
|
12
11
|
import { GIT_CHANNEL_BUDGET_MS, GIT_MIN_ATTEMPT_MS, GIT_SPEC_TIMEOUT_MS, gitChannelBudgetMs, noteChannel, formatBudgetMs, tryGitChannel } from './git-channel.js'
|
|
13
12
|
import { DEFAULT_BUNDLES, AI_CONSENT_TIMEOUT_MS, PNPM_INSTALL_TIMEOUT_MS, addBundleToManifest, backfillMissingDeps, curlManualInstall, detectBundleOnly, ensureBundlePatchIntegrity, githubReleaseInstall, pnpmInstall, raceInstallChannels, readGithubAuth, syncAggregateSubpackageVersions } from './install.js'
|
|
14
13
|
import { readBundlePatchRefNames } from './bundle-refs.js' // 0.5.26 搬家:install.js 贴着 600 行硬顶
|
|
@@ -19,14 +18,14 @@ import { tryPresetSourceChannel } from './preset-source.js'
|
|
|
19
18
|
import { settlePresetOutcome } from './preset-in-package.js'
|
|
20
19
|
import { alreadyInstalledTarget, verifyInstalledEntry } from './install-verify.js'
|
|
21
20
|
import { applyNpmNameHint, expandSubpackageCandidates, fetchRepoPackage, fetchRepoPackageEx, fetchSubpackageNames, npmNameHintForRepo, packageProbeErrorText, subpackageCandidates } from './market.js'
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
21
|
+
import { ensurePersisted } from './persist.js'
|
|
22
|
+
import { readPatchState, syncNameFromNote } from './patch.js'
|
|
23
|
+
import { listEntries } from './runtime.js'
|
|
25
24
|
import { detectSkillRepo, runSkillInstallJob } from './skills.js'
|
|
26
25
|
import { orderedRegistries, readSources } from './sources.js'
|
|
27
26
|
import { reconcileLockfile } from './selfupdate.js'
|
|
28
27
|
import { fetchRepoMeta, runSuiteInstallJob, shouldRunSuiteInstall } from './suite.js'
|
|
29
|
-
import {
|
|
28
|
+
import { execFileAsync } from '../infra/exec.js'
|
|
30
29
|
import { cleanupStalePackageDir, startTrashCleanup, trashScanRoots } from '../infra/fsx.js'
|
|
31
30
|
import { findPatchPath, packageNameOf, resolvePackageJson } from '../infra/paths.js'
|
|
32
31
|
|
|
@@ -224,14 +223,6 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
224
223
|
// 源码通道(体积门禁 + 稀疏取源码 + 预设装配)的注入缝:与 marketProbes 同一风格,生产调用方不传。
|
|
225
224
|
const sourceDeps = deps.sourceDeps ?? {}
|
|
226
225
|
const aiConsentTimeoutMs = Number.isFinite(deps.aiConsentTimeoutMs) && deps.aiConsentTimeoutMs > 0 ? deps.aiConsentTimeoutMs : AI_CONSENT_TIMEOUT_MS
|
|
227
|
-
/** 「安装即声明」的收尾(三个调用点共用一个出口):清单写回结果 → job 上的可见字段。
|
|
228
|
-
* depNote / suggestedAction 也在这里落地:面板必须能看见"清单里**实际**写成了什么",以及"没写成时点哪个按钮"。 */
|
|
229
|
-
const applyDeclared = (declared) => {
|
|
230
|
-
if (declared.changed) job.declared = declared.version
|
|
231
|
-
if (declared.depNote !== null) job.depNote = declared.depNote
|
|
232
|
-
if (declared.suggested !== null) job.suggestedAction = declared.suggested
|
|
233
|
-
if (declared.lockSynced === false && declared.lockNote !== null) job.lockNote = declared.lockNote
|
|
234
|
-
}
|
|
235
226
|
try {
|
|
236
227
|
job.stage = 'preparing'
|
|
237
228
|
// 2026-09-06 事故(室友机器):对 deepseek-ai/deepseek-harness(框架本体仓库)点安装会按 bundle 规则
|
|
@@ -479,7 +470,10 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
479
470
|
// lock 对账(2026-09-21,用户报告的自更新缺陷同源):主包 + 本次被补装/对齐的聚合子包一起核对,
|
|
480
471
|
// 一次 pnpm add 把漂移的包全写进 lock;对不上就留 lockNote,让面板如实告知(不假装成功)。
|
|
481
472
|
try {
|
|
482
|
-
|
|
473
|
+
// 同一个离线测试缝(见下方 `deps.persistDeps` 注释):安装路径上"动 lock"的两处都归它,
|
|
474
|
+
// 否则负控里这一处会先偷偷把 lock 写齐,收口那一步就再也测不出"未持久化"。
|
|
475
|
+
const reconcile = typeof deps.persistDeps?.reconcile === 'function' ? deps.persistDeps.reconcile : reconcileLockfile
|
|
476
|
+
const lock = await reconcile({
|
|
483
477
|
profileDir,
|
|
484
478
|
packages: [installedName, ...syncedNames].map((name) => ({ name })),
|
|
485
479
|
registries,
|
|
@@ -501,7 +495,14 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
501
495
|
const alreadyServed = listEntries(ports).some((e) => String(e.moduleName).startsWith(installedName)
|
|
502
496
|
|| String(e.moduleName) === installedName
|
|
503
497
|
|| packageNameOf(e.moduleName) === installedName)
|
|
498
|
+
// 安装收口(0.5.38):**一次动作内**把该插件钉住 —— ① 清单 spec ② pnpm-lock.yaml 条目
|
|
499
|
+
// ③ 挂载行(cordis.patch.yml 的 insert 行 / dsh.profile.bundles 的 bundle 层)。
|
|
500
|
+
// 三处齐备才算 `persisted`;缺任何一处都**如实**报「未持久化」+ 一键钉住动作 —— 绝不报成功
|
|
501
|
+
// (用户真机现场:dshmarket 装了、bundle 行挂着,清单里却没有条目 ⇒ 重启/任何 pnpm 操作即消失)。
|
|
502
|
+
// 判据与写入的唯一承担者是 domain/persist.js(别处不许再各判一套)。
|
|
503
|
+
let mode = 'insert'
|
|
504
504
|
if (await detectBundleOnly(profileDir, installedName)) {
|
|
505
|
+
mode = 'bundle'
|
|
505
506
|
// 官方 dsh plugin add 行为:声明 dsh.bundle 的包追加为 profile bundle 层,
|
|
506
507
|
// 其 cordis.patch.yml 在下次启动时参与组合(含皮肤包与 web-ui-settings 这类有入口的包)
|
|
507
508
|
if (!alreadyServed) {
|
|
@@ -521,23 +522,27 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
521
522
|
} else {
|
|
522
523
|
job.bundleNote = (job.bundleNote ? job.bundleNote + ';' : '') + `${installedName} 已由已安装聚合包提供,仅更新包本身,不再注册重复行`
|
|
523
524
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
// registry 可解析性决定(非 registry 包写 link:,见 manifest.js#planDependencySpec),故必须传 registries。
|
|
527
|
-
applyDeclared(await declareProfileDependency(profileDir, installedName, null, { registries }))
|
|
528
|
-
job.status = 'done'
|
|
529
|
-
return
|
|
530
|
-
}
|
|
531
|
-
if (alreadyServed) {
|
|
525
|
+
} else if (alreadyServed) {
|
|
526
|
+
mode = 'served'
|
|
532
527
|
job.note = `${installedName} 已由已安装条目提供,仅更新包本身,不重复注册`
|
|
533
|
-
applyDeclared(await declareProfileDependency(profileDir, installedName, null, { registries }))
|
|
534
|
-
job.status = 'done'
|
|
535
|
-
return
|
|
536
528
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
529
|
+
// `deps.persistDeps` 是**离线测试缝**(与 marketProbes/sourceDeps 同一风格):正控/负控必须能
|
|
530
|
+
// 确定性地让 lock 那一步失败("故意让 lock 写入失败 ⇒ 必须报未持久化而不是成功"),
|
|
531
|
+
// 生产调用方不传第三个参数时形状与默认值一个字都不变。
|
|
532
|
+
const persist = await ensurePersisted({ profileDir, patchPath, packageName: installedName, mode, taken, entries: listEntries(ports), registries, deps: deps.persistDeps ?? {} })
|
|
533
|
+
const persistNotes = [persist.declared?.depNote, persist.declared?.lockNote, persist.lock?.lockNote, persist.lock?.depNote, ...persist.notes]
|
|
534
|
+
.filter((n) => typeof n === 'string' && n !== '')
|
|
535
|
+
if (persistNotes.length > 0) job.depNote = job.depNote === undefined ? persistNotes.join(';') : `${job.depNote};${persistNotes.join(';')}`
|
|
536
|
+
if (persist.declared?.changed === true) job.declared = persist.declared.spec
|
|
537
|
+
// entryId 的语义**与旧行为一致**:只有"用户补丁里的 insert 行"才算本作业注册的行(bundle 层提供的行
|
|
538
|
+
// 与运行时已有行都不算,旧代码在这两条路上也不设 entryId)——撤销安装的归属判定依赖这一点。
|
|
539
|
+
job.entryId = persist.after.parts.mount.via === 'patch-row' ? (persist.rowId ?? null) : null
|
|
540
|
+
job.persisted = persist.persisted
|
|
541
|
+
job.persist = { missing: persist.missing, actual: persist.after.actual, note: persist.note }
|
|
542
|
+
if (persist.persisted !== true) {
|
|
543
|
+
job.persistNote = persist.note
|
|
544
|
+
if (persist.suggested !== null && persist.suggested !== undefined && (job.suggestedAction === undefined || job.suggestedAction === null)) job.suggestedAction = persist.suggested
|
|
545
|
+
}
|
|
541
546
|
job.status = 'done'
|
|
542
547
|
} catch (error) {
|
|
543
548
|
job.status = 'failed'
|
|
@@ -546,23 +551,6 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
546
551
|
job.finishedAt = Date.now()
|
|
547
552
|
}
|
|
548
553
|
}
|
|
549
|
-
/** 用 corepack pnpm 移除包(与安装同一管理器与凭据抑制环境)。
|
|
550
|
-
* 注意:pnpm remove 不支持 --registry 选项,去掉避免静默失败。 */
|
|
551
|
-
async function pnpmRemove(profileDir, packageName) {
|
|
552
|
-
const args = ['remove', packageName]
|
|
553
|
-
const opts = {
|
|
554
|
-
cwd: profileDir,
|
|
555
|
-
timeout: 120000,
|
|
556
|
-
windowsHide: true,
|
|
557
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
558
|
-
// 2026-09-26:env 统一走 buildPnpmEnv(在既有 COREPACK_NPM_REGISTRY / 禁交互之上追加
|
|
559
|
-
// npm_config_fetch_timeout / npm_config_fetch_retries / CI=true)。**不加** --fetch-timeout 类
|
|
560
|
-
// CLI 选项:实测 `pnpm remove --fetch-timeout=…` 会 `[ERROR] Unknown options` 直接失败。
|
|
561
|
-
env: buildPnpmEnv('https://registry.npmmirror.com'),
|
|
562
|
-
}
|
|
563
|
-
// 与 pnpmInstall 同一套跨平台定位(Linux 的 corepack 不在 <node bin>/node_modules 下)
|
|
564
|
-
await runPnpmWithFallback(args, { execOpts: opts })
|
|
565
|
-
}
|
|
566
554
|
/** 读取用户额外 bundle(非官方模板)的补丁插入行 id 与包名,用于"额外插件"判定。 */
|
|
567
555
|
async function readExtraBundleRows(profileDir) {
|
|
568
556
|
const rows = new Set()
|
|
@@ -546,6 +546,9 @@ function installJobView(job) {
|
|
|
546
546
|
diagnosis: job.diagnosis ?? null,
|
|
547
547
|
// 装后校验与下载物摘要(2026-09-26 加法,见 install-verify.js):**只提示**,不改变既有成功判定
|
|
548
548
|
entryCheck: job.entryCheck ?? null, integrity: job.integrity ?? null,
|
|
549
|
+
// 持久化判据(0.5.38 加法,见 domain/persist.js):`persisted:false` ⇒ 面板必须报「未持久化」
|
|
550
|
+
// (重启/任何一次 pnpm 操作后可能消失)并给出「一键钉住」动作 —— 绝不报"安装成功"。
|
|
551
|
+
persisted: job.persisted ?? null, persistNote: job.persistNote ?? null, persist: job.persist ?? null,
|
|
549
552
|
}
|
|
550
553
|
}
|
|
551
554
|
|