dsh-plugin-manager-companion 0.1.0

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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +144 -0
  3. package/README.md +142 -0
  4. package/cordis.patch.yml +9 -0
  5. package/dist/about.d.ts +77 -0
  6. package/dist/about.js +179 -0
  7. package/dist/cli.d.ts +226 -0
  8. package/dist/cli.js +856 -0
  9. package/dist/client/AboutPage.d.ts +75 -0
  10. package/dist/client/ConsolePage.d.ts +79 -0
  11. package/dist/client/KindsPage.d.ts +21 -0
  12. package/dist/client/MarketplacePage.d.ts +36 -0
  13. package/dist/client/OfficialSlots.d.ts +35 -0
  14. package/dist/client/UpgradeRow.d.ts +108 -0
  15. package/dist/client/index.d.ts +26 -0
  16. package/dist/client/locales.d.ts +475 -0
  17. package/dist/client/pmSelect.d.ts +38 -0
  18. package/dist/client/shared.d.ts +928 -0
  19. package/dist/client/upgradeView.d.ts +278 -0
  20. package/dist/client/wire.d.ts +401 -0
  21. package/dist/client.js +9194 -0
  22. package/dist/diagnostics.d.ts +332 -0
  23. package/dist/diagnostics.js +2631 -0
  24. package/dist/envManager.d.ts +1047 -0
  25. package/dist/envManager.js +3214 -0
  26. package/dist/fix.d.ts +60 -0
  27. package/dist/fix.js +168 -0
  28. package/dist/guard.d.ts +133 -0
  29. package/dist/guard.js +232 -0
  30. package/dist/index.d.ts +121 -0
  31. package/dist/index.js +1150 -0
  32. package/dist/installSession.d.ts +111 -0
  33. package/dist/installSession.js +150 -0
  34. package/dist/kinds.d.ts +464 -0
  35. package/dist/kinds.js +1029 -0
  36. package/dist/marketView.d.ts +261 -0
  37. package/dist/marketView.js +406 -0
  38. package/dist/marketplace.d.ts +248 -0
  39. package/dist/marketplace.js +500 -0
  40. package/dist/match.d.ts +67 -0
  41. package/dist/match.js +203 -0
  42. package/dist/net.d.ts +108 -0
  43. package/dist/net.js +163 -0
  44. package/dist/official.d.ts +145 -0
  45. package/dist/official.js +205 -0
  46. package/dist/paths.d.ts +108 -0
  47. package/dist/paths.js +236 -0
  48. package/dist/presets.d.ts +299 -0
  49. package/dist/presets.js +578 -0
  50. package/dist/qualityGate.d.ts +66 -0
  51. package/dist/qualityGate.js +247 -0
  52. package/dist/rank.d.ts +88 -0
  53. package/dist/rank.js +164 -0
  54. package/dist/registry.d.ts +295 -0
  55. package/dist/registry.js +686 -0
  56. package/dist/rest.d.ts +122 -0
  57. package/dist/rest.js +219 -0
  58. package/dist/scan.d.ts +134 -0
  59. package/dist/scan.js +396 -0
  60. package/dist/settings.d.ts +447 -0
  61. package/dist/settings.js +263 -0
  62. package/dist/tags.d.ts +119 -0
  63. package/dist/tags.js +166 -0
  64. package/dist/tools.d.ts +131 -0
  65. package/dist/tools.js +377 -0
  66. package/dist/types.d.ts +651 -0
  67. package/dist/types.js +13 -0
  68. package/dist/upgrade.d.ts +428 -0
  69. package/dist/upgrade.js +1100 -0
  70. package/dist/upgradeView.d.ts +313 -0
  71. package/dist/upgradeView.js +273 -0
  72. package/docs/images/readme/01-console-health.png +0 -0
  73. package/docs/images/readme/02-console-envs.png +0 -0
  74. package/docs/images/readme/03-marketplace.png +0 -0
  75. package/docs/images/readme/04-official-plugin-page.png +0 -0
  76. package/package.json +104 -0
@@ -0,0 +1,1047 @@
1
+ /**
2
+ * 环境管理引擎 — 列出/启停/创建/重命名/删除本地 DSH 环境,跨环境写插件,备份导出/差异/恢复。
3
+ *
4
+ * 归属:A 类·重写(旧仓库 src/profiles.ts 与 src/index.ts 的 createProfile /
5
+ * renameProfile / removeProfile / copyPlugins / backupExport / backupDiff /
6
+ * backupRestore 仅作意图参考,未复制代码)。
7
+ * 旧实现参考:旧 src/profiles.ts(进程扫描、终端窗口、端口探测的意图)、
8
+ * 旧 src/index.ts:227-1210(环境生命周期与备份四分类差异的意图)。
9
+ * 官方复用:@deepseek-ai/dsh-app-boot(initProfile / PROFILE_TEMPLATES /
10
+ * DEFAULT_PROFILE_BUNDLES / readProfileManifest / writeProfileBundles)、
11
+ * @deepseek-ai/dsh-plugin-manager/operations(runPluginCommand —— 跨环境 pnpm 通道)、
12
+ * @deepseek-ai/dsh-atomic-write(withFileLock)、./paths.ts(路径、manifest、互斥队列)、
13
+ * ./official.ts(当前环境事实)。
14
+ * 前提检查:旧实现的三条前提都已消失或已证伪 ——
15
+ * 1. 「必须自己拼 bundle 模板 / 自己调 pnpm」:官方 PROFILE_TEMPLATES 与
16
+ * runPluginCommand 已覆盖,本模块不写 bundle 清单、不调 pnpm 二进制;
17
+ * 2. 「当前环境靠 argv 猜」(旧 issue #1):官方 profileContext 是权威事实,
18
+ * 本模块只读它,拿不到就如实报告「未知」;
19
+ * 3. 「pkill -f 'dsh --profile' 收尾」:会误杀命令行里恰好出现同一字符串的无关
20
+ * 进程。本模块只按 pid 精确 kill,且在 kill 前用 /proc(或 ps)复核该 pid
21
+ * 的命令行仍属于同名环境。
22
+ *
23
+ * 同一套引擎、作用域可切换:当前环境的写操作走官方 pluginManager 服务,其它环境走
24
+ * 官方 operations —— 差别只是传给 runPluginCommand 的 profile 参数,不是两套实现。
25
+ */
26
+ import type { Context } from '@deepseek-ai/cordis';
27
+ import type { PackageOperationContext, PackageOperationOptions } from '@deepseek-ai/dsh-plugin-manager/operations';
28
+ import type { PackageResult } from '@deepseek-ai/dsh-plugin-manager/types';
29
+ import { type OfficialCapabilities } from './official.ts';
30
+ import type { BackupFormat, BackupMissingEntry, EnvironmentBackup, EnvironmentBackupDiff, EnvironmentInfo, EnvironmentResult, EnvironmentRun } from './types.ts';
31
+ /**
32
+ * 进程扫描结果的缓存有效期。
33
+ *
34
+ * 一次页面加载会连续触发列表与详情,每次都做一次全表扫描(Windows 上是
35
+ * powershell CIM 查询,可达数秒)。运行状态变化频率低,3s 内共享一份扫描对读
36
+ * 路径不可感知;启停的判定用 scanRunsNow 拿即时事实。
37
+ */
38
+ export declare const SCAN_RUNS_TTL_MS = 3000;
39
+ /** 启动后等待端口就绪的上限。官方 web 面冷启动要解析整棵插件树,给足 30s。 */
40
+ export declare const START_READY_TIMEOUT_MS = 30000;
41
+ /** 收到 SIGTERM 后等待进程退出的上限。 */
42
+ export declare const STOP_TIMEOUT_MS = 5000;
43
+ /** 自动选端口时的起点(官方 web 默认端口的上方)。 */
44
+ export declare const DEFAULT_WEB_PORT = 3090;
45
+ /**
46
+ * 创建环境时的默认模板名。
47
+ *
48
+ * 官方 DEFAULT_PROFILE_BUNDLES 只有 @deepseek-ai/dsh-base(官方语义:故意最小,没有
49
+ * 任何 app),用它建出来的环境**必然没有 web 服务**:实测 startEnvironment 拉起后
50
+ * 干等 30s 报 timeout。官方 PROFILE_TEMPLATES.web 才是「建一个能访问的环境」的语义。
51
+ * 取名字时按官方键校验,官方常量里没有这个键就 fail loud(不悄悄退化成 base-only)。
52
+ */
53
+ export declare const DEFAULT_ENVIRONMENT_TEMPLATE = "web";
54
+ /**
55
+ * 环境操作的稳定错误码。取值同时出现在 EnvironmentResult.code 与
56
+ * EnvironmentError.code 上,UI 依此做本地化与按钮分派。
57
+ */
58
+ export type EnvironmentErrorCode =
59
+ /** 环境名不合法(路径穿越、Windows 保留名等)。 */
60
+ 'invalid-name'
61
+ /** 环境不存在。 */
62
+ | 'not-found'
63
+ /** 环境已存在。 */
64
+ | 'already-exists'
65
+ /** 官方内置环境(web/headless),只读。 */
66
+ | 'builtin'
67
+ /** 当前进程正在运行的环境,不可停止/删除/重命名。 */
68
+ | 'current'
69
+ /** 环境已有运行实例。 */
70
+ | 'running'
71
+ /** 环境没有运行实例。 */
72
+ | 'not-running'
73
+ /** 环境的层栈里没有任何能提供 web 服务的层,启动它不会有网页可访问。 */
74
+ | 'no-web-layer'
75
+ /** 调用方显式指定的端口已经被监听:不猜「应答来自谁」,直接拒绝、不发起启动。 */
76
+ | 'port-in-use'
77
+ /** 进程事实读不到(powershell/ps 不可用等):运行状态未知,拒绝在未知状态下做破坏性操作。 */
78
+ | 'facts-unavailable'
79
+ /** 未知的 bundle 模板名。 */
80
+ | 'unknown-template'
81
+ /** 不是由 dsh 以 profile 方式启动,官方跨环境通道拿不到 installAnchor。 */
82
+ | 'no-profile-context'
83
+ /** 官方 @deepseek-ai/dsh-plugin-manager/operations 子路径不可用。 */
84
+ | 'official-unavailable'
85
+ /** 官方包操作返回非零退出码。 */
86
+ | 'package-operation-failed'
87
+ /** 启动失败(含没有可用终端且后台启动也失败)。 */
88
+ | 'launch-failed'
89
+ /** 端口在超时上限内没有就绪。 */
90
+ | 'timeout'
91
+ /** SIGTERM 后进程仍在。 */
92
+ | 'kill-timeout'
93
+ /** 备份文档结构不合法。 */
94
+ | 'unsafe-backup'
95
+ /** 备份中存在不可恢复的条目(本地路径来源已消失)。 */
96
+ | 'unrestorable'
97
+ /** 参数为空(例如没有选中任何插件)。 */
98
+ | 'empty-selection'
99
+ /** 快照做不到"只含源环境现在的清单"(删不掉上一次物化留下的 node_modules 等)。 */
100
+ | 'snapshot-not-shallow'
101
+ /** 文件系统操作失败。 */
102
+ | 'io-failed';
103
+ /** 带稳定错误码的环境操作异常。 */
104
+ export declare class EnvironmentError extends Error {
105
+ readonly code: EnvironmentErrorCode;
106
+ /**
107
+ * @param code - 稳定错误码,见 EnvironmentErrorCode。
108
+ * @param message - 面向用户的原因说明。
109
+ */
110
+ constructor(code: EnvironmentErrorCode, message: string);
111
+ }
112
+ /** 读进程表的注入点,供测试替换(生产路径见 defaultProcessLines)。 */
113
+ export type ProcessLineReader = () => readonly string[];
114
+ /** scanRuns 的选项。 */
115
+ export interface ScanRunsOptions {
116
+ /** 缓存有效期;默认 SCAN_RUNS_TTL_MS。 */
117
+ readonly ttlMs?: number;
118
+ /** 时钟注入(测试)。 */
119
+ readonly now?: () => number;
120
+ /** 进程表读取器注入(测试)。默认按平台选择 /proc、ps 或 powershell。 */
121
+ readonly reader?: ProcessLineReader;
122
+ /** 绕过缓存强制重新扫描。 */
123
+ readonly fresh?: boolean;
124
+ }
125
+ /** 丢弃进程扫描缓存。启停成功后调用,让下一次读取立刻看到变化。 */
126
+ export declare function resetRunCache(): void;
127
+ /**
128
+ * 扫描进程表,找出每个环境的运行实例与端口(带缓存)。
129
+ *
130
+ * @param options - 缓存与注入选项。
131
+ * @returns 环境名到运行实例列表的映射;未运行的环境不出现在 map 里。
132
+ */
133
+ export declare function scanRuns(options?: ScanRunsOptions): ReadonlyMap<string, readonly EnvironmentRun[]>;
134
+ /**
135
+ * 立即扫描进程表,不经缓存。
136
+ *
137
+ * 启停的就绪与存活判定必须看到即时变化,否则刚起的实例会被 3s 陈旧缓存判成
138
+ * 「没起」,stop 也会对着一个已经退出的 pid 空转。调用方在状态变化后用
139
+ * resetRunCache 让读路径跟上。
140
+ *
141
+ * @param options - 注入选项。
142
+ * @returns 环境名到运行实例列表的映射。
143
+ */
144
+ /**
145
+ * 一次进程事实读取的结果。
146
+ *
147
+ * `readable: false` 与「读到了、但一个实例都没在跑」是**两件事**(审计 W-19):
148
+ * 旧实现把失败折叠成空表,调用方只能看到「这台机器上没有实例」,于是把不知道说成了知道。
149
+ * 不可读时调用方必须如实说「运行状态未知」,破坏性操作必须拒绝。
150
+ */
151
+ export interface ProcessFacts {
152
+ /** 环境名 → 运行实例;不可读时为空 Map(**不要据此判断「没在运行」**)。 */
153
+ readonly runs: ReadonlyMap<string, readonly EnvironmentRun[]>;
154
+ /** 进程事实是否读到。 */
155
+ readonly readable: boolean;
156
+ /** 不可读的原因(面向用户)。 */
157
+ readonly reason?: string;
158
+ }
159
+ /**
160
+ * 立即读取进程事实(含「读不到」这个状态),并写入缓存。
161
+ *
162
+ * @param options - 注入选项。
163
+ * @returns 事实与可读性。
164
+ */
165
+ export declare function processFactsNow(options?: ScanRunsOptions): ProcessFacts;
166
+ /**
167
+ * 读取进程事实(带缓存)。
168
+ *
169
+ * @param options - 缓存与注入选项。
170
+ * @returns 事实与可读性。
171
+ */
172
+ export declare function processFacts(options?: ScanRunsOptions): ProcessFacts;
173
+ /**
174
+ * 立即扫描进程表,不经缓存(只要实例映射;可读性请用 processFactsNow)。
175
+ *
176
+ * @param options - 注入选项。
177
+ * @returns 环境名到运行实例列表的映射。
178
+ */
179
+ export declare function scanRunsNow(options?: ScanRunsOptions): Map<string, readonly EnvironmentRun[]>;
180
+ /**
181
+ * 读某个 pid 的命令行。
182
+ *
183
+ * kill 前的复核用它:pid 可能已被回收,必须重新证明它仍是同名环境的实例。读不
184
+ * 到时返回 null —— 调用方必须据此放弃 kill,而不是照杀不误。
185
+ *
186
+ * @param pid - 目标进程。
187
+ * @returns 命令行原文;不可读时为 null。
188
+ */
189
+ export declare function readProcessCommand(pid: number): string | null;
190
+ /**
191
+ * 廉价存活探针:signal 0 只在进程消失时抛 ESRCH;EPERM 说明进程还在(别人持有)。
192
+ *
193
+ * @param pid - 目标进程。
194
+ * @returns 是否存活。
195
+ */
196
+ export declare function pidAlive(pid: number): boolean;
197
+ /** 需要「当前环境」事实的操作的共享选项。 */
198
+ export interface CurrentEnvironmentOptions {
199
+ /** host 上下文:当前环境从 ctx.profileContext 读(权威事实)。 */
200
+ readonly ctx?: Context;
201
+ /** 覆盖当前环境判定(测试注入)。 */
202
+ readonly current?: string | null;
203
+ /** 覆盖官方能力探测(测试注入)。 */
204
+ readonly capabilities?: OfficialCapabilities;
205
+ }
206
+ /**
207
+ * 当前环境名(UI 与其它模块复用)。
208
+ *
209
+ * @param ctx - host 上下文。
210
+ * @returns 环境名;未知时 null。
211
+ */
212
+ export declare function currentEnvironment(ctx?: Context): string | null;
213
+ /** listEnvironments 的选项。 */
214
+ export interface ListEnvironmentsOptions extends CurrentEnvironmentOptions {
215
+ /** 复用的进程扫描结果;省略时走 scanRuns 的缓存扫描。 */
216
+ readonly runs?: ReadonlyMap<string, readonly EnvironmentRun[]>;
217
+ /** 复用的进程事实(带可读性);省略时自行读取。 */
218
+ readonly facts?: ProcessFacts;
219
+ }
220
+ /**
221
+ * 列出 $DSH_HOME/profiles 下的环境及其只读事实。
222
+ *
223
+ * @param ctx - host 上下文;用于取官方认定的「当前环境」。
224
+ * @param options - 注入选项。
225
+ * @returns 按名称排序的环境列表。
226
+ */
227
+ export declare function listEnvironments(ctx?: Context, options?: ListEnvironmentsOptions): EnvironmentInfo[];
228
+ /** 官方 bundle 模板(创建环境时可选),直接来自官方 PROFILE_TEMPLATES。 */
229
+ export interface EnvironmentTemplate {
230
+ readonly name: string;
231
+ readonly bundles: readonly string[];
232
+ }
233
+ /**
234
+ * 可用的环境模板。
235
+ *
236
+ * @returns 官方模板名与它们的 bundle 清单(本仓库不维护任何 bundle 名单)。
237
+ */
238
+ export declare function environmentTemplates(): readonly EnvironmentTemplate[];
239
+ /**
240
+ * 官方 web 模板相对官方默认层栈多出来的那几层 —— 也就是「能提供 web 面」的包。
241
+ *
242
+ * 派生自官方常量(PROFILE_TEMPLATES / DEFAULT_PROFILE_BUNDLES):官方改了模板,
243
+ * 这里跟着变,不需要改代码,也不需要我们维护一份会漂移的名字表。
244
+ *
245
+ * @returns 官方 web app 层的包名;官方常量里没有该模板时返回空数组。
246
+ */
247
+ export declare function officialWebAppBundles(): readonly string[];
248
+ /** 一个环境的 web 层判定。 */
249
+ export type WebLayerPresence = 'present' | 'absent' | 'unknown';
250
+ /**
251
+ * 判断一个环境的层栈里有没有能提供 web 服务的层。
252
+ *
253
+ * 两条官方事实,先静态后动态:
254
+ * 1. 层名出现在官方 web 模板的 app 层里(官方常量派生,覆盖官方模板建的层栈);
255
+ * 2. 逐层用官方 resolveBundleDir 找到包目录、读它的 manifest:依赖或 peer 里出现
256
+ * WEB_SERVER_PACKAGE 的层就是 web 层(这条能认出官方模板之外的 web bundle)。
257
+ *
258
+ * 只有「每一层都能解析、且都不满足上面两条」才敢说 absent —— 任何一层的事实拿不到
259
+ * 就返回 unknown,调用方据此降级为「无法预判,仍按就绪探测等待」,绝不预判成 absent
260
+ * 去拒绝一个可能能跑的环境。
261
+ *
262
+ * @param dir - 环境目录。
263
+ * @param bundles - 该环境的 bundle 层栈。
264
+ * @param installAnchor - 官方安装锚点;省略时用该环境自己的 manifest 作解析锚点(官方第二锚点)。
265
+ * @returns 判定结果。
266
+ */
267
+ export declare function environmentWebLayer(dir: string, bundles: readonly string[], installAnchor?: string): WebLayerPresence;
268
+ /**
269
+ * 环境名的完整校验(含平台的额外约束)。
270
+ *
271
+ * @param name - 待校验的环境名。
272
+ * @returns 拒绝原因;合法时为 null。
273
+ */
274
+ export declare function environmentNameProblem(name: string): string | null;
275
+ /**
276
+ * 创建一个环境(骨架由官方 initProfile 写:manifest、空 patch 层、pnpm 设置)。
277
+ *
278
+ * 模板只接受官方 PROFILE_TEMPLATES 的名字;不存在的模板名是配置错误,直接失败,
279
+ * 而不是悄悄换成别的 bundle 列表。
280
+ *
281
+ * 省略模板时用 DEFAULT_ENVIRONMENT_TEMPLATE(官方 web 模板),**不是**官方
282
+ * DEFAULT_PROFILE_BUNDLES:后者只有 base、没有任何 app,建出来的环境必然起不来
283
+ * (实测 startEnvironment 干等 30s 超时)。
284
+ *
285
+ * @param name - 新环境名。
286
+ * @param template - 官方模板名;省略时用官方 web 模板。
287
+ * @returns 操作结果。
288
+ */
289
+ export declare function createEnvironment(name: string, template?: string): Promise<EnvironmentResult>;
290
+ /** renameEnvironment 的选项。 */
291
+ export type RenameEnvironmentOptions = CurrentEnvironmentOptions;
292
+ /**
293
+ * 重命名一个环境目录。拒绝内置环境、当前环境与运行中的环境。
294
+ *
295
+ * @param from - 原名。
296
+ * @param to - 新名。
297
+ * @param options - 当前环境事实。
298
+ * @returns 操作结果。
299
+ */
300
+ export declare function renameEnvironment(from: string, to: string, options?: RenameEnvironmentOptions): Promise<EnvironmentResult>;
301
+ /** removeEnvironment 的选项。 */
302
+ export type RemoveEnvironmentOptions = CurrentEnvironmentOptions;
303
+ /**
304
+ * 删除一个环境目录。拒绝内置环境、当前环境与运行中的环境。
305
+ *
306
+ * @param name - 环境名。
307
+ * @param options - 当前环境事实。
308
+ * @returns 操作结果。
309
+ */
310
+ export declare function removeEnvironment(name: string, options?: RemoveEnvironmentOptions): Promise<EnvironmentResult>;
311
+ /** 一次启动的目标描述。 */
312
+ export interface LaunchSpec {
313
+ readonly profile: string;
314
+ readonly port: number;
315
+ readonly mode: 'terminal' | 'background';
316
+ /** 要执行的可执行文件。 */
317
+ readonly command: string;
318
+ readonly args: readonly string[];
319
+ /** dsh 入口脚本绝对路径;走 PATH shim 时为 null。 */
320
+ readonly entry: string | null;
321
+ /**
322
+ * 是否需要经 shell 启动。
323
+ *
324
+ * Windows 上 PATH shim 是 `dsh.cmd`:Node ≥20.12 起 `spawn('x.cmd')` 不带 shell 会**抛 EINVAL**
325
+ * (审计 W-07 已在真 win32 Node 上实测),所以这条回退路径必须显式声明要 shell;
326
+ * 首选路径(process.execPath + bin.js)保持无 shell 的官方启动纪律。
327
+ */
328
+ readonly shell: boolean;
329
+ /** 环境目录。 */
330
+ readonly dir: string;
331
+ /** 面向用户的等价命令行(原样展示,不执行)。 */
332
+ readonly display: string;
333
+ }
334
+ /** 启动结果。 */
335
+ export interface LaunchOutcome {
336
+ readonly ok: boolean;
337
+ readonly detail: string;
338
+ /** 实际采用的启动方式;没有可用终端时会从 terminal 降级为 background。 */
339
+ readonly mode: 'terminal' | 'background';
340
+ /** 终端模式实际用的终端名。 */
341
+ readonly terminal?: string;
342
+ /** 后台模式捕获官方输出到的日志路径(里面有官方打印的带 token 地址)。 */
343
+ readonly logPath?: string;
344
+ /**
345
+ * 为什么最终是这个启动方式(降级原因)。
346
+ *
347
+ * 独立复验 N-03:`wt` 缺失 → 降级后台时,成功与失败两条文案都只说「启动方式:后台」,
348
+ * 用户不知道**为什么**不是终端窗口。这是「用户即将做的动作的后果」类信息(DESIGN §12.2
349
+ * 的保留清单),必须下发。
350
+ */
351
+ readonly reason?: string;
352
+ }
353
+ /** startEnvironment 的选项。 */
354
+ export interface StartEnvironmentOptions {
355
+ /** 启动方式;terminal 在当前平台没有可用终端时自动降级为后台。 */
356
+ readonly mode?: 'terminal' | 'background';
357
+ /** 指定端口;省略时从 portStart 起找一个空闲端口。 */
358
+ readonly port?: number;
359
+ /** 自动选端口的起点;默认 DEFAULT_WEB_PORT。 */
360
+ readonly portStart?: number;
361
+ /** 就绪上限;默认 START_READY_TIMEOUT_MS。 */
362
+ readonly readyTimeoutMs?: number;
363
+ /** 追加给被启动应用的参数。 */
364
+ readonly extraArgs?: readonly string[];
365
+ /** host 上下文:用于取官方 installAnchor 判定 web 层。 */
366
+ readonly ctx?: Context;
367
+ /** installAnchor 覆盖;省略时取 ctx.profileContext.installAnchor,再退到该环境自己的 manifest。 */
368
+ readonly installAnchor?: string;
369
+ /** 启动器注入(测试):替换终端/后台启动。 */
370
+ readonly launch?: (spec: LaunchSpec) => Promise<LaunchOutcome>;
371
+ /** HTTP 就绪探测注入(测试):返回状态码;null 表示没有应答。 */
372
+ readonly probe?: (port: number) => Promise<number | null>;
373
+ /** 时钟注入(测试)。 */
374
+ readonly now?: () => number;
375
+ /** 等待注入(测试)。 */
376
+ readonly sleep?: (ms: number) => Promise<void>;
377
+ }
378
+ /**
379
+ * 启动一个环境的实例。
380
+ *
381
+ * 四道关,越靠前越便宜:
382
+ * 1. 环境存在、不在运行中;
383
+ * 2. **web 层预检**:层栈里没有任何 web 服务层就立刻拒绝(实测 base-only 环境要干等
384
+ * 30s 才超时,用户拿到的是「端口未就绪」这种没法行动的信息);
385
+ * 3. 选空闲端口,按 mode 在终端窗口或后台启动;
386
+ * 4. 等**官方 HTTP 端点应答**(不是等 TCP 可连接 —— 实测 TCP 通了那一刻 GET / 还是
387
+ * 404,1000ms 后才是 401,提前报成功会把不可用的 url 交出去)。
388
+ *
389
+ * @param name - 环境名。
390
+ * @param options - 启动选项。
391
+ * @returns 操作结果;成功时 output 含实际启动方式与可用地址(带 token,见 startedMessage)。
392
+ */
393
+ export declare function startEnvironment(name: string, options?: StartEnvironmentOptions): Promise<EnvironmentResult>;
394
+ /**
395
+ * 被启动实例的入口。
396
+ *
397
+ * 优先复用本进程自己所属的安装:process.argv[1] 就是当前 dsh 的入口脚本,node 是
398
+ * process.execPath —— 同一个安装、同一个版本,不依赖 PATH(旧实现靠 PATH 找 dsh,
399
+ * 在 nvm 未加载的终端里会失败)。
400
+ *
401
+ * @returns 命令、前置参数与入口脚本路径。
402
+ */
403
+ /**
404
+ * 被启动实例的入口(可注入 argv/platform,供测试)。
405
+ *
406
+ * @param argv - 进程参数;默认 process.argv。
407
+ * @param platform - 平台;默认 process.platform。
408
+ * @returns 命令、前置参数、入口脚本与是否需要 shell。
409
+ */
410
+ export declare function dshEntryPoint(argv?: readonly string[], platform?: NodeJS.Platform): {
411
+ command: string;
412
+ args: readonly string[];
413
+ entry: string | null;
414
+ shell: boolean;
415
+ };
416
+ /**
417
+ * .cmd/.bat shim 的启动形态(显式 cmd、argv 直传、参数先校验)。
418
+ *
419
+ * @param spec - 启动描述(shell 为 true 时才有意义)。
420
+ * @returns 命令与参数。
421
+ * @throws 参数含 cmd 不安全字符时(绝不静默交给 cmd 拆错)。
422
+ */
423
+ export declare function windowsShimInvocation(spec: LaunchSpec): {
424
+ command: string;
425
+ args: readonly string[];
426
+ };
427
+ /**
428
+ * Windows 可见终端窗口的启动形态(Windows Terminal,官方 open-in-app 目录的 Win 终端项就是 wt:
429
+ * packages/host/open-in-app/src/catalog.ts:361)。
430
+ *
431
+ * 为什么不用 `cmd /c start "" cmd /k <命令行>`:那条串是裸拼接的展示文本,交给 cmd 会**重新
432
+ * 分词**;官方安装包默认在 `C:\Program Files\nodejs`,含空格时新窗口里只有「找不到命令」,
433
+ * 而失败要等 30s 就绪超时才暴露(审计 W-08)。wt 收的是 argv,程序与每个参数各自成段,
434
+ * 不再经过 shell 分词。
435
+ *
436
+ * @param spec - 启动描述。
437
+ * @returns 要执行的命令与参数(argv 形态)。
438
+ */
439
+ export declare function windowsTerminalInvocation(spec: LaunchSpec): {
440
+ command: string;
441
+ args: readonly string[];
442
+ };
443
+ /** stopEnvironment 的选项。 */
444
+ export interface StopEnvironmentOptions extends CurrentEnvironmentOptions {
445
+ /** 等待退出的上限;默认 STOP_TIMEOUT_MS。 */
446
+ readonly timeoutMs?: number;
447
+ /** 注入时钟(测试)。 */
448
+ readonly now?: () => number;
449
+ /** 注入等待(测试)。 */
450
+ readonly sleep?: (ms: number) => Promise<void>;
451
+ /**
452
+ * 注入「读某个 pid 的命令行」(测试)。
453
+ *
454
+ * 为什么需要它:kill 前的复核在 Windows 上走 powershell,POSIX 上读 /proc —— 在 Linux 上
455
+ * 伪装 process.platform='win32' 会让复核必然读不到(找不到 powershell),于是整条 Windows
456
+ * 分支无法在 Linux 回归里覆盖。这是与 probe/launch/sleep 同风格的测试接缝。
457
+ */
458
+ readonly readCommand?: (pid: number) => string | null;
459
+ }
460
+ /**
461
+ * 停止一个环境的实例 —— 只按 pid 精确 kill。
462
+ *
463
+ * 绝不 pkill -f "dsh --profile NAME":那会连带杀掉命令行里恰好出现同一字符串的
464
+ * 无关进程,也会杀掉同名的 pnpm 与一次性命令。这里的流程是:扫描,取同名环境的
465
+ * pid,逐个用 readProcessCommand 复核该 pid 仍然是同一环境的实例,再终止(POSIX
466
+ * SIGTERM;Windows taskkill /T /F 结束进程树,见 terminateInstance),最后轮询存活。
467
+ * 结果文案如实说明是哪种终止方式 —— Windows 上不存在「优雅停止」这回事。
468
+ *
469
+ * @param name - 环境名。
470
+ * @param options - 停止选项。
471
+ * @returns 操作结果。
472
+ */
473
+ export declare function stopEnvironment(name: string, options?: StopEnvironmentOptions): Promise<EnvironmentResult>;
474
+ /** 官方包操作运行器。默认动态 import 官方子路径;测试可注入。 */
475
+ export type PluginCommandRunner = (context: PackageOperationContext, args: readonly string[], options: PackageOperationOptions) => Promise<PackageResult>;
476
+ /** 跨环境写操作的共享选项。 */
477
+ export interface CrossEnvironmentOptions {
478
+ /** host 上下文:installAnchor 从 ctx.profileContext 取。 */
479
+ readonly ctx?: Context;
480
+ /** installAnchor 覆盖(测试,或应用自有 profile)。官方事实拿不到时必须显式给出。 */
481
+ readonly installAnchor?: string;
482
+ /** 官方 operations 运行器覆盖(测试注入,避免真的跑 pnpm)。 */
483
+ readonly runCommand?: PluginCommandRunner;
484
+ /** 输出回调(逐块)。 */
485
+ readonly onOutput?: (text: string, stream: 'stdout' | 'stderr') => void;
486
+ /** 输出上限;默认 64KiB。 */
487
+ readonly outputBytes?: number;
488
+ /** 官方 operations 的锁等待上限;默认 120s。 */
489
+ readonly lockWaitMs?: number;
490
+ }
491
+ /**
492
+ * 修复安装:把当前 profile **已声明**的依赖真正装进 node_modules。
493
+ *
494
+ * 为什么不能走 add:官方 inspect 把「已声明」当作「已安装」(already-installed 是官方
495
+ * PluginInspectProblem 闭集里的取值),于是对「声明了但没装」的包走 add 必被拒绝,
496
+ * 修复输出「拒绝安装:already-installed」——与诊断结论直接矛盾,用户点多少次都不会成功
497
+ * (write-auditor 真机验证发现,见 docs/private/write-path-audit.md §3·P3)。
498
+ *
499
+ * 官方 `dsh plugin --profile X install` 就是把参数转发给 pnpm 的官方通道(apps/cli/src/plugin.ts
500
+ * 也走同一个 runPluginCommand),修复安装用它。
501
+ *
502
+ * @param target - 包名(只用于文案;官方 install 按 package.json 全量收敛)。
503
+ * @param options - 共享选项。
504
+ * @returns 结果;官方通道不可用或 pnpm 非零退出时如实报失败,不静默降级。
505
+ */
506
+ export declare function repairDependencies(target: string, options?: CrossEnvironmentOptions): Promise<EnvironmentResult>;
507
+ /**
508
+ * 把源环境里已装的插件装到目标环境。
509
+ *
510
+ * 与当前环境的写操作走官方 pluginManager 服务不同,这里是同一套官方 pnpm 通道加换
511
+ * 一个 profile 参数:runPluginCommand({ profile, dir, installAnchor, cwd, home },
512
+ * ['add', spec])。整批只占一次进程内互斥(分批会让并发的安装/删除插进条目之间,
513
+ * 互相覆盖 manifest)。
514
+ *
515
+ * @param from - 源环境名。
516
+ * @param to - 目标环境名。
517
+ * @param names - 要复制的包名(按源 manifest 里记录的来源重装)。
518
+ * @param options - 共享选项。
519
+ * @returns 操作结果。
520
+ */
521
+ export declare function copyPlugins(from: string, to: string, names: readonly string[], options?: CrossEnvironmentOptions): Promise<EnvironmentResult>;
522
+ /** 测试环境名的后缀。一个真实环境对应一个测试环境(保真需要,§5.4 命名与归属)。 */
523
+ export declare const TRIAL_ENVIRONMENT_SUFFIX = "-dpmc";
524
+ /**
525
+ * 某个真实环境对应的测试环境名。
526
+ *
527
+ * @param realName - 真实环境名。
528
+ * @returns 测试环境名。
529
+ */
530
+ export declare function trialEnvironmentName(realName: string): string;
531
+ /**
532
+ * 是不是我们创建的测试环境(只看名字形态)。
533
+ *
534
+ * 删除路径先用它筛,再核对归属(§5.4 清理):不靠台账,台账会过期。
535
+ *
536
+ * @param name - 环境名。
537
+ * @returns 是否形如测试环境。
538
+ */
539
+ export declare function isTrialEnvironmentName(name: string): boolean;
540
+ /**
541
+ * 测试环境名对应的真实环境名;不是测试环境时 null。
542
+ *
543
+ * @param name - 环境名。
544
+ * @returns 真实环境名或 null。
545
+ */
546
+ export declare function trialEnvironmentOwner(name: string): string | null;
547
+ /**
548
+ * 环境指纹(DESIGN §5.4):五元组,**全部读盘**,不读我们的内存台账。
549
+ *
550
+ * 为什么不读台账:用户可能在终端里跑官方 dsh plugin add、或直接改文件,那些改动不会经过我们,
551
+ * 台账必然过期。所以每次要用测试环境之前重算一次、与记录比对。
552
+ */
553
+ export interface EnvironmentFingerprint {
554
+ /** package.json 内容 hash;缺失时 null(缺失与空文件是两件事)。 */
555
+ readonly manifestHash: string | null;
556
+ /** pnpm-lock.yaml 内容 hash;缺失时 null。 */
557
+ readonly lockfileHash: string | null;
558
+ /** cordis.patch.yml 内容 hash;缺失时 null。 */
559
+ readonly patchHash: string | null;
560
+ /** bundle 层栈。 */
561
+ readonly bundles: readonly string[];
562
+ /** 层栈来源:官方 listBundles() 还是本地 manifest(**口径不同,必须标**)。 */
563
+ readonly bundlesSource: 'official' | 'manifest';
564
+ /** 直接依赖名(排序)。 */
565
+ readonly dependencies: readonly string[];
566
+ /** 五元组的整体 hash(比对用)。 */
567
+ readonly hash: string;
568
+ }
569
+ /** environmentFingerprint 的选项。 */
570
+ export interface FingerprintOptions {
571
+ /** 官方层栈事实:ctx.pluginManager.listBundles()(只在测当前环境时可用)。 */
572
+ readonly listBundles?: () => Promise<readonly string[]>;
573
+ }
574
+ /**
575
+ * 算一个环境当前的指纹(读盘)。
576
+ *
577
+ * @param name - 环境名。
578
+ * @param options - 官方层栈来源(可注入;拿不到时退回 manifest 并如实标注口径)。
579
+ * @returns 指纹。
580
+ */
581
+ export declare function environmentFingerprint(name: string, options?: FingerprintOptions): Promise<EnvironmentFingerprint>;
582
+ /**
583
+ * 两份指纹是否同一环境状态。
584
+ *
585
+ * @param a - 之一。
586
+ * @param b - 之二。
587
+ * @returns 是否一致。
588
+ */
589
+ export declare function sameFingerprint(a: EnvironmentFingerprint, b: EnvironmentFingerprint): boolean;
590
+ /** 一次挂载验证的判定(三态;DESIGN §5.2:**退出码不参与判定**)。 */
591
+ export type BootVerdict =
592
+ /** 树挂载成功(stderr 只有官方的「缺任务」用法提示)。 */
593
+ {
594
+ readonly kind: 'mounted';
595
+ }
596
+ /** 挂载失败,带根因链。 */
597
+ | {
598
+ readonly kind: 'failed';
599
+ readonly reason: string;
600
+ readonly chain: readonly string[];
601
+ }
602
+ /** 判不出来(没有可识别特征、进程没起来、被超时杀掉等)。 */
603
+ | {
604
+ readonly kind: 'undetermined';
605
+ readonly reason: string;
606
+ };
607
+ /**
608
+ * 从 stderr 文本判定挂载结果(纯函数,可注入文本测试)。
609
+ *
610
+ * 判据(§5.2 实测):健康环境 stderr 只有一行 dsh: a task is required…,而**退出码仍是 1**,
611
+ * 所以退出码不能用作判据;失败是 plugin tree failed to load + cause 链。两者都没有 → 无法判定。
612
+ *
613
+ * @param stderr - 子进程的 stderr 文本。
614
+ * @returns 三态判定。
615
+ */
616
+ export declare function judgeBootStderr(stderr: string): BootVerdict;
617
+ /**
618
+ * 一次验证启动的超时上限。
619
+ *
620
+ * 官方 smoke 用 90s 是在等一个真实服务;我们只等**就绪行**(实测报文 <1s),
621
+ * 所以收在 15s:既是 20 倍余量,也把"判不出来"的等待从 30s 砍到 15s。
622
+ */
623
+ export declare const VERIFY_TIMEOUT_MS = 15000;
624
+ /**
625
+ * 验证启动的参数:**按层栈决定形态**(未知参数不能盲加——headless 类环境不认 --port)。
626
+ *
627
+ * @param prefixArgs - 启动器入口自己的参数(`dshEntryPoint().args`)。
628
+ * @param name - 环境名。
629
+ * @param webLayer - 该环境的 web 层判定(environmentWebLayer)。
630
+ * @returns 完整参数表。
631
+ */
632
+ export declare function verificationArgs(prefixArgs: readonly string[], name: string, webLayer: WebLayerPresence): readonly string[];
633
+ /** 一次验证启动收集到的原始信号(判定只看它,退出码不参与)。 */
634
+ export interface BootSignals {
635
+ /** 子进程 stderr 原文。 */
636
+ readonly stderr: string;
637
+ /** 子进程 stdout 原文(服务形态的就绪行在这里)。 */
638
+ readonly stdout?: string;
639
+ /** 就绪行里的地址(`dsh web: http://…`);没有信号时 null。 */
640
+ readonly readyUrl?: string | null;
641
+ /** 退出码;**仅供展示与排查,不参与判定**(§5.2)。 */
642
+ readonly exitCode: number | null;
643
+ /** 是否读到就绪行后主动杀了子进程(服务形态常驻,必须收工)。 */
644
+ readonly killedAfterReady?: boolean;
645
+ }
646
+ /**
647
+ * 从一次启动的原始信号给判定(纯函数,可注入文本测试)。
648
+ *
649
+ * 两类环境各用各的就绪信号,**谁先出现算谁**(谁先出现由启动器决定,见 defaultHeadlessRun):
650
+ * · 服务形态:stdout 的官方就绪行 → mounted;
651
+ * · headless 形态:stderr 的 `dsh: a task is required…` → mounted(走 judgeBootStderr)。
652
+ * 失败一律读 stderr(`plugin tree failed to load` / `cannot resolve profile bundle` + cause 链);
653
+ * 什么信号都没有 → undetermined(**不许当通过**)。
654
+ *
655
+ * @param signals - 原始信号。
656
+ * @returns 三态判定。
657
+ */
658
+ export declare function judgeBootSignals(signals: BootSignals): BootVerdict;
659
+ /** 试装的三种结论(§5.2:各有措辞、不得混;无法试装**不算通过**)。 */
660
+ export type TrialConclusion =
661
+ /** 基线起得来、装完候选包也起得来 → 通过。 */
662
+ 'passed'
663
+ /** 快照基线本身就起不来:不是候选包的问题。 */
664
+ | 'baseline-broken'
665
+ /** 基线好、装完候选包起不来:候选包导致的。 */
666
+ | 'candidate-broken'
667
+ /** 无法试装(例如禁用联网且本地 store 没有该包):**不能算通过**。 */
668
+ | 'cannot-trial';
669
+ /**
670
+ * 按受控对照四步给结论(§5.2)。
671
+ *
672
+ * 判定顺序刻意如此:先看基线,再看候选 —— 基线失败时不许赖候选包。
673
+ *
674
+ * @param baseline - 物化快照后的挂载判定;没做基线启动时 null。
675
+ * @param candidate - 装完候选包后的挂载判定;没走到这一步 null。
676
+ * @param cannotTrialReason - 提前失败的原因(如无法下载候选包);给了就只报「无法试装」。
677
+ * @returns 结论。
678
+ */
679
+ export declare function judgeTrialOutcome(baseline: BootVerdict | null, candidate: BootVerdict | null, cannotTrialReason?: string): TrialConclusion;
680
+ /** 快照深度(§5.2/§5.3):自动判定,可强制。 */
681
+ export type SnapshotDepth = 'shallow' | 'full';
682
+ /**
683
+ * 自动判定快照深度:层栈是否**全部由安装锚点提供**。
684
+ *
685
+ * 判据复用同一套事实(与指纹的 bundlesSource 同源:都是「层从哪里来」),不另起一套:
686
+ * · 某一层出现在 <dir>/node_modules 里 → 它是 profile 自己装的(第三方 bundle),
687
+ * 浅快照(只复制清单)复现不出来 → full;
688
+ * · 否则试官方解析 resolveBundleDir:能从安装锚点解析到 → 浅快照够用 → shallow。
689
+ * 拿不到锚点、或解析失败 → 一律 full(写多不写少)。
690
+ *
691
+ * @param dir - 真实环境目录。
692
+ * @param bundles - 该环境的层栈。
693
+ * @param installAnchor - 官方安装锚点(package.json 路径)。
694
+ * @returns 深度。
695
+ */
696
+ export declare function snapshotDepthFor(dir: string, bundles: readonly string[], installAnchor?: string): SnapshotDepth;
697
+ /** 快照物化的结果。 */
698
+ export interface SnapshotMaterialization {
699
+ readonly depth: SnapshotDepth;
700
+ /** 从真实环境复制过来的文件名。 */
701
+ readonly copied: readonly string[];
702
+ /** 物化前清掉的**上一次物化残留**(例如 node_modules):快照不能带着旧依赖。 */
703
+ readonly cleared: readonly string[];
704
+ /** 是否跑了官方 pnpm 通道(full 深度时)。 */
705
+ readonly installed: boolean;
706
+ /** 面向用户的说明。 */
707
+ readonly output: string;
708
+ }
709
+ /** materializeSnapshot 的选项。 */
710
+ export interface MaterializeSnapshotOptions extends CrossEnvironmentOptions {
711
+ /** 强制深度;省略 = 自动判定。 */
712
+ readonly depth?: SnapshotDepth;
713
+ /** 层栈来源(官方 listBundles());拿不到就用源环境的清单,够自动判定用。 */
714
+ readonly bundlesOf?: (name: string) => Promise<readonly string[]>;
715
+ }
716
+ /**
717
+ * 把真实环境物化成测试环境(§5.4 同步原则:不订阅变化,用时即时物化)。
718
+ *
719
+ * 浅快照只复制清单文件;full 深度再走**官方 runPluginCommand** 装依赖(绝不自己调 pnpm 二进制)。
720
+ * 官方通道不可用或失败时如实抛出 —— 调用方据此报「无法试装」,不许静默当成功。
721
+ *
722
+ * @param sourceName - 真实环境名。
723
+ * @param targetName - 测试环境名(应等于 trialEnvironmentName(sourceName))。
724
+ * @param options - 深度、层栈来源与官方通道注入。
725
+ * @returns 物化结果。
726
+ */
727
+ export declare function materializeSnapshot(sourceName: string, targetName: string, options?: MaterializeSnapshotOptions): Promise<SnapshotMaterialization>;
728
+ /** createTrialEnvironment 的选项。 */
729
+ export interface CreateTrialEnvironmentOptions {
730
+ /** 建立后是否立即物化快照(默认 true)。 */
731
+ readonly materialize?: boolean;
732
+ /** 物化选项。 */
733
+ readonly snapshot?: MaterializeSnapshotOptions;
734
+ }
735
+ /**
736
+ * 建立/复用某个真实环境的测试环境(§5.4:一个真实环境一个测试环境)。
737
+ *
738
+ * @param realName - 真实环境名。
739
+ * @param options - 物化选项。
740
+ * @returns 结果(output 含快照深度与文件清单)。
741
+ */
742
+ export declare function createTrialEnvironment(realName: string, options?: CreateTrialEnvironmentOptions): Promise<EnvironmentResult>;
743
+ /** removeTrialEnvironment 的选项。 */
744
+ export interface RemoveTrialEnvironmentOptions extends CurrentEnvironmentOptions {
745
+ /** 注入进程事实(测试);省略时自行读取(fresh)。 */
746
+ readonly facts?: ProcessFacts;
747
+ }
748
+ /**
749
+ * 删除一个测试环境(§5.4:**删除必须安全**)。
750
+ *
751
+ * 三重纪律:
752
+ * 1. 只删形如 <真实名>-dpmc 的环境(裸后缀不算:没有归属的目录不能进删除路径);
753
+ * 2. 运行中先拒 —— 不做「先停后删」的隐式动作,停是用户的显式决定;
754
+ * 3. 进程事实不可读 → 拒绝(**未知状态下绝不动磁盘**,与 stop/remove 同一条纪律)。
755
+ * 孤儿(真实环境已改名或删除)**可以删**:归属核对的是「名字形态 + 这是我们建的测试环境」,
756
+ * 不是「真实环境还在」。
757
+ *
758
+ * @param name - 测试环境名。
759
+ * @param options - 当前环境事实与进程事实注入。
760
+ * @returns 操作结果。
761
+ */
762
+ export declare function removeTrialEnvironment(name: string, options?: RemoveTrialEnvironmentOptions): Promise<EnvironmentResult>;
763
+ /** 清理候选:全部读盘得来(mtime 取目录自身)。 */
764
+ export interface TrialCleanupCandidate {
765
+ readonly name: string;
766
+ /** 归属的真实环境名(名字形态推出)。 */
767
+ readonly owner: string;
768
+ /** 目录 mtime(毫秒)。 */
769
+ readonly modifiedAt: number;
770
+ readonly running: boolean;
771
+ }
772
+ /** 默认保留天数(§5.3:默认开 / 14 天,可关可配)。 */
773
+ export declare const DEFAULT_TRIAL_RETENTION_DAYS = 14;
774
+ /** planTrialCleanup 的选项。 */
775
+ export interface TrialCleanupPlanOptions {
776
+ /** 当前时间(毫秒);注入供测试。 */
777
+ readonly now?: number;
778
+ /** 保留天数;null 表示用户关掉了自动清理。 */
779
+ readonly retainDays?: number | null;
780
+ }
781
+ /** 一份清理计划:删什么、留什么,各自带原因。 */
782
+ export interface TrialCleanupPlan {
783
+ readonly remove: readonly {
784
+ readonly name: string;
785
+ readonly reason: string;
786
+ }[];
787
+ readonly keep: readonly {
788
+ readonly name: string;
789
+ readonly reason: string;
790
+ }[];
791
+ }
792
+ /**
793
+ * 算一份测试环境清理计划(纯函数,便于正反用例测试)。
794
+ *
795
+ * 只按「到没到保留期」与「是否在跑」两个事实判;运行中的永远保留(删除安全优先)。
796
+ * `retainDays: null` = 用户关掉了自动清理:什么都不删,但仍把候选列出来给界面显示。
797
+ *
798
+ * @param candidates - 候选(读盘事实)。
799
+ * @param options - 时间与保留期。
800
+ * @returns 计划。
801
+ */
802
+ export declare function planTrialCleanup(candidates: readonly TrialCleanupCandidate[], options?: TrialCleanupPlanOptions): TrialCleanupPlan;
803
+ /**
804
+ * 列出所有测试环境候选(读盘;进程事实不可读时按「未知」处理 → 全部保留)。
805
+ *
806
+ * @param options - 进程事实注入(测试)。
807
+ * @returns 候选列表与进程事实的可读性。
808
+ */
809
+ export declare function listTrialEnvironments(options?: {
810
+ readonly facts?: ProcessFacts;
811
+ }): {
812
+ readonly candidates: readonly TrialCleanupCandidate[];
813
+ readonly factsReadable: boolean;
814
+ readonly reason?: string;
815
+ };
816
+ /**
817
+ * 执行清理(§5.4:删除动作**记日志**)。
818
+ *
819
+ * @param options - 计划选项 + 日志回调。
820
+ * @returns 结果(删了哪些、留了哪些)。
821
+ */
822
+ export declare function cleanupTrialEnvironments(options?: TrialCleanupPlanOptions & {
823
+ readonly facts?: ProcessFacts;
824
+ readonly log?: (line: string) => void;
825
+ }): Promise<EnvironmentResult & {
826
+ readonly removed: readonly string[];
827
+ }>;
828
+ /**
829
+ * 构建指纹(CODE-POLICY §7.8 / DEVELOPMENT §87):真机结论必须钉在一次具体构建上。
830
+ *
831
+ * 产物 md5 取本模块被打进的那份文件(安装到用户环境里也能算),git HEAD 只在能读到仓库时带上,
832
+ * 读不到就 null —— 如实标「不知道这份构建对应哪个 commit」,不编。
833
+ */
834
+ export interface BuildIdentity {
835
+ /** 本模块产物的 md5(dist/<file>.js)。 */
836
+ readonly artifactMd5: string | null;
837
+ /** 产物文件 mtime(ISO)。 */
838
+ readonly artifactMtime: string | null;
839
+ /** git HEAD;读不到仓库时为 null。 */
840
+ readonly gitHead: string | null;
841
+ }
842
+ /**
843
+ * 算当前构建的指纹。
844
+ *
845
+ * @returns 构建指纹。
846
+ */
847
+ export declare function buildIdentity(): BuildIdentity;
848
+ /** 一次无头验证的结果(判定 + 证据)。 */
849
+ export interface BootVerification {
850
+ readonly verdict: BootVerdict;
851
+ /** 实测耗时(毫秒)。 */
852
+ readonly elapsedMs: number;
853
+ /** 子进程 stderr 原文(截断到 8KiB,供展示与判定复核)。 */
854
+ readonly stderr: string;
855
+ /** 子进程 stdout 原文(截断到 8KiB);服务形态的就绪行在这里。老调用点可省略。 */
856
+ readonly stdout?: string;
857
+ /** 就绪行里的地址(服务形态);没有信号时 null。 */
858
+ readonly readyUrl?: string | null;
859
+ /** 是否读到就绪行后主动杀了子进程(服务形态常驻,必须收工)。 */
860
+ readonly killedAfterReady?: boolean;
861
+ /** 退出码;**仅供展示与排查,不参与判定**(§5.2)。 */
862
+ readonly exitCode: number | null;
863
+ /** 本次验证所在的构建指纹。 */
864
+ readonly build: BuildIdentity;
865
+ }
866
+ /** runHeadlessVerification 的选项。 */
867
+ export interface HeadlessVerificationOptions {
868
+ /** 超时上限;默认 VERIFY_TIMEOUT_MS(15s)。 */
869
+ readonly timeoutMs?: number;
870
+ /**
871
+ * 启动器注入(测试):省略时真起进程。
872
+ * stdout / readyUrl / killedAfterReady 是服务形态那一路的证据,可以省略(headless 形态用不到)。
873
+ */
874
+ readonly run?: (name: string) => Promise<{
875
+ readonly stderr: string;
876
+ readonly exitCode: number | null;
877
+ readonly stdout?: string;
878
+ readonly readyUrl?: string | null;
879
+ readonly killedAfterReady?: boolean;
880
+ }>;
881
+ }
882
+ /**
883
+ * 无头验证一个环境(§5.2 的形态,不得偏离)。
884
+ *
885
+ * 形态:`<dsh> --profile <name>`,**不给任何任务文本**(给了就是真跑一轮 agent,花用户的钱),
886
+ * stdin=/dev/null,捕获 stderr;判定只读 stderr 特征(退出码不参与)。
887
+ *
888
+ * @param name - 环境名。
889
+ * @param options - 超时与启动器注入。
890
+ * @returns 验证结果。
891
+ */
892
+ export declare function runHeadlessVerification(name: string, options?: HeadlessVerificationOptions): Promise<BootVerification>;
893
+ /** 边收边判的启动信号收集器(纯逻辑,可按 chunk 驱动测试)。 */
894
+ export interface BootSignalCollector {
895
+ /** 收一段 stderr。 */
896
+ pushStderr(chunk: string): void;
897
+ /**
898
+ * 收一段 stdout;返回**本次是否新认出就绪行**(调用方据此收工)。
899
+ *
900
+ * 两条纪律:
901
+ * · 就绪行可能跨 chunk(官方一行也是一个 write,但流式读取不保证对齐)→ 每次都在累积文本里找;
902
+ * · stderr 里已经出现失败特征时,**不再认**后来的就绪行(谁先出现算谁)。
903
+ */
904
+ pushStdout(chunk: string): boolean;
905
+ readonly readyUrl: string | null;
906
+ readonly stderr: string;
907
+ readonly stdout: string;
908
+ }
909
+ /**
910
+ * 造一个启动信号收集器。
911
+ *
912
+ * 为什么把它抽出来:就绪行的识别是"跨 chunk"和"顺序"两件事,真机跑一次证明不了边界,
913
+ * 而这两条边界恰恰是最容易写错的(先来的失败被后来的就绪行盖掉、半行就绪行被漏掉)。
914
+ *
915
+ * @returns 收集器。
916
+ */
917
+ export declare function createBootSignalCollector(): BootSignalCollector;
918
+ /** runTrialInstall 的选项。 */
919
+ export interface TrialInstallOptions extends Omit<MaterializeSnapshotOptions, 'depth'> {
920
+ /**
921
+ * 深度策略:auto(默认,先浅快照、基线明确失败再升级 full)/ shallow / full。
922
+ *
923
+ * 为什么 auto 不再先问「锚点能否解析」:实测这条谓词对**原装环境一律返回 full**
924
+ * (官方层由安装锚点供给,但 resolveBundleDir 在这台机器上解析不到它们),
925
+ * 于是 shallow 成了走不到的分支,而 shallow 真机又能挂载(526ms)。
926
+ * 改成**以启动为判据**(Lead 授权):先 shallow;基线**明确 failed** 才升级 full,
927
+ * 升级后仍不 mounted 才报 baseline-broken(文案写明两种快照都试过)。
928
+ * undetermined(超时/无可识别特征)**不升级** —— 那是「判不出来」,如实报。
929
+ */
930
+ readonly depth?: 'auto' | 'shallow' | 'full';
931
+ /** 官方层栈事实(算指纹用);只在测当前环境时可用。 */
932
+ readonly listBundles?: () => Promise<readonly string[]>;
933
+ /** 是否做基线启动(§5.2 四步的②);默认 true —— 关掉省 ~558ms,但失败时说不清是谁的问题。 */
934
+ readonly baseline?: boolean;
935
+ /** 是否允许联网拉取候选包(§5.3);false 时只用本地 store,冷包直接判「无法试装」。 */
936
+ readonly allowNetwork?: boolean;
937
+ /** 无头验证注入(测试)。 */
938
+ readonly verify?: (name: string) => Promise<BootVerification>;
939
+ /** 时钟注入(测试)。 */
940
+ readonly now?: () => number;
941
+ }
942
+ /** 试装结果:结论 + 证据 + 构建指纹(§7.8 真机结论必须钉在一次具体构建上)。 */
943
+ export interface TrialInstallResult {
944
+ readonly conclusion: TrialConclusion;
945
+ readonly output: string;
946
+ /** 本次结论对应的构建(产物 md5 + mtime + 能读到时的 git HEAD)。 */
947
+ readonly build: BuildIdentity;
948
+ /** 试装前的源环境指纹。 */
949
+ readonly sourceFingerprint: EnvironmentFingerprint;
950
+ /** 试装后再算的源环境指纹(§5.4 第 4 步)。 */
951
+ readonly sourceFingerprintAfter: EnvironmentFingerprint | null;
952
+ /** 试装期间源环境又变过(结论可能不适用)。 */
953
+ readonly changedDuringTrial: boolean;
954
+ readonly baseline: BootVerdict | null;
955
+ readonly candidate: BootVerdict | null;
956
+ readonly elapsedMs: number;
957
+ /** 结论实际基于哪种快照深度。 */
958
+ readonly depth: SnapshotDepth;
959
+ /** 是否发生过 shallow → full 的升级(只在基线明确失败时)。 */
960
+ readonly escalated: boolean;
961
+ /** 浅快照不给力的原因(升级时给出,取失败判定的第一行)。 */
962
+ readonly escalationReason?: string;
963
+ /** 卸包那一步的说明(task-84:让候选成为"新装",做了/没做/失败都如实写)。 */
964
+ readonly detached?: string;
965
+ /**
966
+ * 候选的激活事实(进没进层栈)。拿到就带上,供调用方与界面判断"这次到底验证到了没有"。
967
+ */
968
+ readonly activation?: TrialActivationFact;
969
+ }
970
+ /**
971
+ * 候选在试装环境里的激活事实("装上了"不等于"验证到了"的判据,可下发给界面)。
972
+ */
973
+ export interface TrialActivationFact {
974
+ /** 候选包名;认不出来时 undefined。 */
975
+ readonly name: string | undefined;
976
+ /** 装完之后测试环境的层栈(dsh.profile.bundles)。 */
977
+ readonly bundles: readonly string[];
978
+ /** 候选是否真的进了层栈(false = 挂载期不会加载它,这次验证不作数)。 */
979
+ readonly activated: boolean;
980
+ /** 是否为了让候选成为"新装"而先走了官方 remove。 */
981
+ readonly removedFirst: boolean;
982
+ /** 卸包那一步的说明(做了/没做/失败,都如实写)。 */
983
+ readonly detachNote: string;
984
+ }
985
+ /**
986
+ * 受控对照四步(§5.2):物化快照 → 基线启动 → 装候选包 → 二次启动。
987
+ *
988
+ * 缺一步结论就站不住,所以:基线失败一律报 baseline-broken(**不赖候选包**);
989
+ * 装不上候选包报 cannot-trial(**不算通过**);只有基线好、装完也好的才是 passed。
990
+ * 装候选包走**官方 runPluginCommand**(绝不自己调 pnpm);allowNetwork=false 时加 --offline,
991
+ * 冷包失败如实报「无法试装」。
992
+ *
993
+ * @param spec - 候选包 spec。
994
+ * @param realName - 真实环境名(测试环境由它派生)。
995
+ * @param options - 四步选项与注入。
996
+ * @returns 结果(含证据与构建指纹)。
997
+ */
998
+ export declare function runTrialInstall(spec: string, realName: string, options?: TrialInstallOptions): Promise<TrialInstallResult>;
999
+ /**
1000
+ * 备份文档格式标识(运行期常量)。
1001
+ *
1002
+ * 类型声明在 types.ts 的 BackupFormat(单一事实来源);这里用类型断言把运行期值
1003
+ * 绑到那个字面量上 —— 改一处漏另一处会编译失败。
1004
+ */
1005
+ export declare const BACKUP_FORMAT: BackupFormat;
1006
+ /** 备份契约的再导出:类型唯一事实来源是 types.ts,本模块不重复定义。 */
1007
+ export type { BackupMissingEntry, EnvironmentBackup, EnvironmentBackupDiff };
1008
+ /**
1009
+ * 导出环境备份。
1010
+ *
1011
+ * @param name - 环境名。
1012
+ * @returns 备份文档。
1013
+ * @throws {EnvironmentError} 名称不合法或环境不存在时(code 为 invalid-name / not-found)。
1014
+ */
1015
+ export declare function backupExport(name: string): EnvironmentBackup;
1016
+ /**
1017
+ * 对比备份与目标环境,分四类:缺失、已装、目标环境不存在、不可恢复。
1018
+ *
1019
+ * 恢复前必须先跑这一遍:差异既是用户确认的依据,也是恢复的输入(只装 missing)。
1020
+ *
1021
+ * @param backup - 备份文档。
1022
+ * @param target - 目标环境名。
1023
+ * @returns 差异。
1024
+ * @throws {EnvironmentError} 备份结构不合法(code 为 unsafe-backup)或目标名不合法时。
1025
+ */
1026
+ export declare function backupDiff(backup: EnvironmentBackup, target: string): EnvironmentBackupDiff;
1027
+ /** backupRestore 的选项。 */
1028
+ export interface RestoreEnvironmentOptions extends CrossEnvironmentOptions {
1029
+ /** 只算差异、不写入。 */
1030
+ readonly dryRun?: boolean;
1031
+ }
1032
+ /**
1033
+ * 按备份恢复一个环境。
1034
+ *
1035
+ * 三步:先算差异(缺失/已装/目标不存在/不可恢复),再用官方 operations 逐条重装
1036
+ * 缺失依赖,最后在官方文件锁下补回备份的 bundle 层栈。整批只占一次进程内互斥。
1037
+ *
1038
+ * 锁的用法按官方意图:装包由 runPluginCommand 自己持锁(我们再套一层会自锁 —— 同一
1039
+ * 把 package.json.lock);bundle 层栈的读-改-写由我们用 withFileLock 独占,避免与
1040
+ * 并发的安装互相覆盖。
1041
+ *
1042
+ * @param backup - 备份文档。
1043
+ * @param target - 目标环境名。
1044
+ * @param options - 恢复选项。
1045
+ * @returns 操作结果。
1046
+ */
1047
+ export declare function backupRestore(backup: EnvironmentBackup, target: string, options?: RestoreEnvironmentOptions): Promise<EnvironmentResult>;