@luolingp/request-doctor 0.0.0-stage → 0.11.1

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -3
  3. package/client.js +174 -0
  4. package/cordis.patch.yml +16 -0
  5. package/docs/00-/346/226/207/346/241/243/347/264/242/345/274/225.md +26 -0
  6. package/docs/01-/350/275/257/344/273/266/351/234/200/346/261/202/350/247/204/346/240/274/350/257/264/346/230/216.md +192 -0
  7. package/docs/02-/350/275/257/344/273/266/350/256/276/350/256/241/350/257/264/346/230/216.md +391 -0
  8. package/docs/03-/350/275/257/344/273/266/346/265/213/350/257/225/346/212/245/345/221/212.md +214 -0
  9. package/docs/04-/345/256/211/350/243/205/351/203/250/347/275/262/350/257/264/346/230/216.md +176 -0
  10. package/docs/05-/347/224/250/346/210/267/346/211/213/345/206/214.md +197 -0
  11. package/docs/06-/350/275/257/344/273/266/350/264/250/351/207/217/345/272/246/351/207/217/346/212/245/345/221/212.md +109 -0
  12. package/docs/quality-report.json +27 -0
  13. package/icon.svg +12 -0
  14. package/index.js +471 -0
  15. package/lib/arbitrate.js +45 -0
  16. package/lib/cache-store.js +79 -0
  17. package/lib/classify.js +389 -0
  18. package/lib/compat-env.js +490 -0
  19. package/lib/compat-repair.js +196 -0
  20. package/lib/compat.js +332 -0
  21. package/lib/diagnostics.js +77 -0
  22. package/lib/env-verify.js +160 -0
  23. package/lib/install-bundle.js +199 -0
  24. package/lib/install-rescue.js +161 -0
  25. package/lib/install-support.js +206 -0
  26. package/lib/model-repair.js +190 -0
  27. package/lib/package-fetch.js +181 -0
  28. package/lib/plugin-discovery.js +232 -0
  29. package/lib/repair-listeners.js +126 -0
  30. package/lib/restart-notice.js +21 -0
  31. package/lib/retry-ledger.js +67 -0
  32. package/lib/runtime-probe.js +171 -0
  33. package/lib/semver.js +159 -0
  34. package/lib/sleep.js +18 -0
  35. package/lib/stat-cache.js +67 -0
  36. package/lib/symbol-alias.js +44 -0
  37. package/lib/timers.js +39 -0
  38. package/lib/version-repair.js +499 -0
  39. package/lib/version-search.js +44 -0
  40. package/locale/en.json +17 -0
  41. package/locale/zh.json +17 -0
  42. package/package.json +51 -3
@@ -0,0 +1,199 @@
1
+ // 装一个远程的插件,不管它的版本检查过不过。
2
+ //
3
+ // 管理器的安装路径是 pnpm 加一次版本检查:检查不过就抛 incompatible-version,
4
+ // 顺手把 profile 回滚。远程包的字节在检查之前就落地了,所以没法像本地目录那样
5
+ // 提前改好 manifest——只能在管理器放弃之后,自己把这件事做完。
6
+ //
7
+ // 这里做的事就是 pnpm add 该做的:取包、取依赖、放进 node_modules、登记为 bundle。
8
+ // 差别只有一处:装之前先按当前运行时把声明对齐,装完再把兼容环境建好。
9
+ import { existsSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { alignManifest, installCompatEnvironment, readJson } from './compat-env.js';
12
+ import { fetchPackage, listVersions } from './package-fetch.js';
13
+ import { satisfies } from './semver.js';
14
+ import { safeCompare } from './version-search.js';
15
+ import { cachePackage, resolveClosure } from './version-repair.js';
16
+
17
+ // 一次性最多装这么多个依赖。正常插件几个,多了说明依赖树异常。
18
+ const MAX_DEPENDENCIES = 24;
19
+
20
+ // 把 "name"、"name@1.2.3"、"@scope/name@^1.0.0" 拆成包名和范围。
21
+ export function parseInstallSpec(spec) {
22
+ if (typeof spec !== 'string') return undefined;
23
+ const text = spec.trim();
24
+ if (text === '' || text.startsWith('-')) return undefined;
25
+ // 本地路径、git 地址、tarball 走别的路,这里只认注册表写法。
26
+ if (/^(?:file:|link:|git|https?:)/.test(text) || /[\\/]/.test(text.replace(/^@[^/]+\/[^/]+/, ''))) return undefined;
27
+
28
+ const scoped = text.startsWith('@');
29
+ const at = text.indexOf('@', scoped ? 1 : 0);
30
+ if (at < 0) return { name: text, range: undefined };
31
+ return { name: text.slice(0, at), range: text.slice(at + 1) };
32
+ }
33
+
34
+ // 选一个版本:满足范围的最新版本;范围没写就取最新。
35
+ export function chooseVersion(versions, range) {
36
+ const usable = versions.filter((version) => typeof version === 'string' && version !== '');
37
+ if (usable.length === 0) return undefined;
38
+ const ordered = [...usable].sort(safeCompare).reverse();
39
+ if (range === undefined) return ordered[0];
40
+ return ordered.find((version) => satisfies(version, range) === true) ?? ordered[0];
41
+ }
42
+
43
+ // 把一个远程插件装进 profile。
44
+ //
45
+ // 不做部分成功:中途任何一步失败都把这个包整个删掉,like pnpm 回滚一样,
46
+ // 不给 profile 留下一半装好的东西。
47
+ export async function installRemoteBundle(input) {
48
+ const parsed = parseInstallSpec(input.spec);
49
+ if (parsed === undefined) return { installed: false, reason: '不是注册表里的包名,无法远程安装' };
50
+
51
+ const { name, range } = parsed;
52
+ if (typeof input.profileDir !== 'string' || input.profileDir === '') {
53
+ return { installed: false, reason: '拿不到 profile 目录' };
54
+ }
55
+
56
+ const versions = await listVersions(name);
57
+ const version = chooseVersion(versions, range);
58
+ if (version === undefined) return { installed: false, reason: `注册表里找不到 ${name}` };
59
+
60
+ const target = join(input.profileDir, 'node_modules', ...name.split('/'));
61
+ try {
62
+ rmSync(target, { recursive: true, force: true });
63
+ const fetched = await fetchPackage(name, version, target);
64
+ if (!fetched.ok) return { installed: false, reason: fetched.error ?? '下载失败' };
65
+
66
+ // 不是 bundle 的包登记进去会让加载器出错,跟管理器自己的 not-bundle 判断一致。
67
+ const fetchedManifest = readJson(join(target, 'package.json'));
68
+ if (fetchedManifest?.dsh?.bundle === undefined) {
69
+ rmSync(target, { recursive: true, force: true });
70
+ return { installed: false, reason: `${name}@${version} 不是 bundle 插件:清单里没有 dsh.bundle` };
71
+ }
72
+
73
+ const dependencies = await installDependencies(target, input);
74
+ const requirements = declaredRangesOf(target);
75
+ // 声明对齐到当前运行时。管理器读的就是这份 manifest,对齐了它的检查才过得去。
76
+ const served = {};
77
+ for (const declared of Object.keys(requirements)) served[declared] = input.runtimeVersion;
78
+ alignManifest({ pluginDir: target, versions: served, runtimeVersion: input.runtimeVersion });
79
+
80
+ // 插件到底 import 了什么,得读它的源码才知道。没有这个只能建空环境,
81
+ // 装上了也起不来——缺的垫片一个都没建。
82
+ const imports = input.imports ?? discoverImports(target, input);
83
+ const environment = installCompatEnvironment({
84
+ pluginDir: target,
85
+ profileDir: input.profileDir,
86
+ requirements,
87
+ imports,
88
+ runtimeVersion: input.runtimeVersion,
89
+ resolveDir: input.resolveDir,
90
+ });
91
+ return {
92
+ installed: true,
93
+ name,
94
+ version,
95
+ path: target,
96
+ environment: environment.status,
97
+ // 环境没建成时把原因带出去,用户才知道卡在哪一环。
98
+ environmentReason: environment.error,
99
+ missing: environment.missing ?? [],
100
+ dependencies,
101
+ };
102
+ } catch (error) {
103
+ rmSync(target, { recursive: true, force: true });
104
+ return { installed: false, reason: String(error) };
105
+ }
106
+ }
107
+
108
+ // 读出新装插件的导入表。
109
+ //
110
+ // 调用方传了探测函数就用它;没传就退回「按声明的 DSH 依赖建垫片」——
111
+ // 声明过的包一定是它要用的,给它们建齐,至少不会因为少一层垫片而 import 失败。
112
+ function discoverImports(target, input) {
113
+ if (typeof input.factsOf === 'function' && typeof input.entryOf === 'function') {
114
+ try {
115
+ const manifest = readJson(join(target, 'package.json'));
116
+ const entry = input.entryOf(target, manifest ?? {});
117
+ if (entry !== undefined) {
118
+ const facts = input.factsOf(target, entry);
119
+ if (facts?.imports instanceof Map) return facts.imports;
120
+ }
121
+ } catch {
122
+ // 探测失败就退回声明
123
+ }
124
+ }
125
+ const map = new Map();
126
+ for (const declared of Object.keys(declaredRangesOf(target))) map.set(declared, []);
127
+ return map;
128
+ }
129
+
130
+ // 读出插件自己声明的 DSH 依赖范围。对齐声明时要用它当依据。
131
+ function declaredRangesOf(dir) {
132
+ const manifest = readJson(join(dir, 'package.json'));
133
+ if (manifest === undefined) return {};
134
+ const found = {};
135
+ for (const source of [manifest.peerDependencies, manifest.dependencies]) {
136
+ for (const [name, range] of Object.entries(source ?? {})) {
137
+ if (name.startsWith('@deepseek-ai/') && found[name] === undefined) found[name] = range;
138
+ }
139
+ }
140
+ return found;
141
+ }
142
+
143
+ // 把包自己的运行时依赖也放进 profile。
144
+ //
145
+ // 不装的话,它 import 的兄弟包要么找不到,要么落到别的版本上。
146
+ //
147
+ // 可选依赖一并处理:它们通常带平台判断,缺了不一定报错,但装上的体验才是完整的。
148
+ // 装不上就算了——可选的本义就是可以没有。
149
+ async function installDependencies(target, input) {
150
+ const manifest = readJson(join(target, 'package.json'));
151
+ if (manifest === undefined) return [];
152
+
153
+ const installed = [];
154
+ const queue = [
155
+ ...Object.entries(manifest.dependencies ?? {}),
156
+ ...Object.entries(manifest.optionalDependencies ?? {}),
157
+ ];
158
+ const optional = new Set(Object.keys(manifest.optionalDependencies ?? {}));
159
+ const seen = new Set();
160
+ while (queue.length > 0 && installed.length < MAX_DEPENDENCIES) {
161
+ const [name, range] = queue.shift();
162
+ if (seen.has(name)) continue;
163
+ seen.add(name);
164
+
165
+ const destination = join(input.profileDir, 'node_modules', ...name.split('/'));
166
+ if (existsSync(join(destination, 'package.json'))) continue;
167
+
168
+ const versions = await listVersions(name);
169
+ const version = chooseVersion(versions, /^[\d.]/.test(range) ? range : undefined);
170
+ if (version === undefined) {
171
+ // 可选依赖取不到是正常的:它可能只对别的平台发布。
172
+ if (optional.has(name) === false) installed.push(`${name}(取不到版本)`);
173
+ continue;
174
+ }
175
+ if ((await fetchPackage(name, version, destination)).ok) installed.push(`${name}@${version}`);
176
+ }
177
+ return installed;
178
+ }
179
+
180
+ // 把包登记进 profile 的 bundle 列表。
181
+ //
182
+ // 只登记这一处:DSH 的 bundle 列表和 dependencies 是两回事,现有 profile 里就有
183
+ // 一半 bundle 不在 dependencies 里。写 dependencies 反而会让下次 pnpm 操作去核对它。
184
+ export function registerBundle(profileDir, name) {
185
+ const path = join(profileDir, 'package.json');
186
+ const manifest = readJson(path);
187
+ if (manifest === undefined) return { registered: false, reason: '读不到 profile 清单' };
188
+
189
+ const bundles = manifest.dsh?.profile?.bundles;
190
+ if (!Array.isArray(bundles)) return { registered: false, reason: 'profile 清单里没有 bundle 列表' };
191
+ if (bundles.includes(name)) return { registered: true, changed: false };
192
+
193
+ bundles.push(name);
194
+ writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
195
+ return { registered: true, changed: true };
196
+ }
197
+
198
+ // 供外部复用:闭包取包。
199
+ export { cachePackage, resolveClosure };
@@ -0,0 +1,161 @@
1
+ // 让「版本不匹配」的插件装得上。
2
+ //
3
+ // 管理器把兼容性检查放在安装流程里,对不上的包直接拒掉,profile 原样回滚。用户看到的
4
+ // 就是那句「不兼容,可能崩溃或丢数据」,然后什么都没装上。
5
+ //
6
+ // 它自己留了一个正当出口:版本豁免。先按它的规则授权,让它把包装完(依赖、构建脚本仍由
7
+ // pnpm 处理),装完再把声明对齐、把兼容环境建好。跑的是当前实现,声明说的也是当前版本,
8
+ // 风险是真的被消掉了,不是被绕过。
9
+ //
10
+ // 为什么不在 installBundle 上套一层?试过了,不行:远程方法在装饰期就被抓成一个按对象
11
+ // 取值的访问器快照,插件侧无论改实例还是改原型,都进不了调用链。授权是它公开支持的那
12
+ // 条路,也是失败信息里点名指出的那条。
13
+ import { readFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { alignManifest, installCompatEnvironment } from './compat-env.js';
16
+ import { installRemoteBundle, parseInstallSpec, registerBundle } from './install-bundle.js';
17
+
18
+ // 从安装失败里取出「哪个包的哪个版本」。
19
+ export function refusedPackageOf(error) {
20
+ const first = error?.incompatible?.[0];
21
+ if (first?.name !== undefined && first?.version !== undefined) {
22
+ return { name: first.name, version: first.version, runtimeVersion: first.runtimeVersion };
23
+ }
24
+ // 有些路径只给一句 message,那就退回从 spec 里认。
25
+ const fromSpec = parseInstallSpec(error?.spec);
26
+ if (fromSpec === undefined) return undefined;
27
+ return { name: fromSpec.name, version: fromSpec.range, runtimeVersion: undefined };
28
+ }
29
+
30
+ // 装一个注册表里的包:先授权,再让管理器装,最后补上兼容形态。
31
+ export async function installWithExemption(input) {
32
+ const parsed = parseInstallSpec(input.spec);
33
+ if (parsed === undefined) return { installed: false, reason: '不是注册表里的包名' };
34
+
35
+ const facts = input.probe.runtimeFacts();
36
+ const profileDir = facts.profileDir ?? '';
37
+ const runtimeVersion = input.runtimeVersion ?? facts.runtimeVersion ?? '';
38
+ if (profileDir === '' || runtimeVersion === '') {
39
+ return { installed: false, reason: '拿不到 profile 目录或运行时版本' };
40
+ }
41
+
42
+ // 已经装着的包不用再装一遍。用户多半是想「让它能用」——就地修好就行。
43
+ if (isPresent(profileDir, parsed.name)) {
44
+ input.record('install-present', `${parsed.name} 已经在 profile 里,就地修复`);
45
+ return finish(input, parsed.name, profileDir, runtimeVersion);
46
+ }
47
+
48
+ // 版本可能没写。这种时候先让管理器去解析,它会告诉我们实际是哪个版本。
49
+ const first = await attemptInstall(input, input.spec);
50
+ if (first.ok) return finish(input, parsed.name, profileDir, runtimeVersion);
51
+
52
+ // 管理器说「已经装了」也走就地修复:用户要的是能用,不是再装一次。
53
+ if (first.error?.code === 'already-installed') {
54
+ return finish(input, parsed.name, profileDir, runtimeVersion);
55
+ }
56
+
57
+ const refused = refusedPackageOf(first.error) ?? { name: parsed.name, version: parsed.range };
58
+ if (refused.version === undefined) {
59
+ return { installed: false, reason: first.error?.message ?? '安装失败,且拿不到具体版本' };
60
+ }
61
+ const granted = await grantExemption(input, refused, runtimeVersion);
62
+ if (granted === false) return { installed: false, reason: '版本豁免授权失败' };
63
+
64
+ const second = await attemptInstall(input, input.spec);
65
+ if (second.ok) return finish(input, refused.name, profileDir, runtimeVersion);
66
+ return installByHand(input, facts, profileDir, runtimeVersion);
67
+ }
68
+
69
+ // 按管理器自己的规则授权。这是它给的出口,不是我们绕过去。
70
+ async function grantExemption(input, refused, runtimeVersion) {
71
+ const packageVersion = `${refused.name}@${refused.version}`;
72
+ try {
73
+ await input.pluginManager?.setVersionExemption?.(packageVersion, runtimeVersion, true, true);
74
+ input.record('install-exemption', `已为 ${packageVersion} 授权运行在 ${runtimeVersion}`);
75
+ return true;
76
+ } catch (failure) {
77
+ input.record('install-exemption-failed', `${packageVersion} 授权失败:${String(failure)}`);
78
+ return false;
79
+ }
80
+ }
81
+
82
+ // 管理器那条路彻底走不通时自己装。
83
+ async function installByHand(input, facts, profileDir, runtimeVersion) {
84
+ const byHand = await installRemoteBundle({
85
+ spec: input.spec,
86
+ profileDir,
87
+ runtimeVersion,
88
+ resolveDir: input.probe.runtimeDirOf(facts),
89
+ factsOf: input.factsOf,
90
+ entryOf: input.entryOf,
91
+ });
92
+ if (!byHand.installed) return { installed: false, reason: byHand.reason ?? '安装失败' };
93
+ registerBundle(profileDir, byHand.name);
94
+ return finish(input, byHand.name, profileDir, runtimeVersion);
95
+ }
96
+
97
+ // 这个包是不是已经躺在 profile 里了。
98
+ function isPresent(profileDir, name) {
99
+ try {
100
+ return readFileSync(join(profileDir, 'node_modules', ...name.split('/'), 'package.json'), 'utf8') !== '';
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ // 装一次,把失败收成结果而不是抛出去。
107
+ async function attemptInstall(input, spec) {
108
+ try {
109
+ const result = await input.pluginManager.installBundle(spec);
110
+ return { ok: result?.application !== 'failed', result, error: result?.error };
111
+ } catch (error) {
112
+ return { ok: false, error };
113
+ }
114
+ }
115
+
116
+ // 装完之后的收尾:声明对齐、环境建好、启用。
117
+ async function finish(input, name, profileDir, runtimeVersion) {
118
+ const facts = input.probe.runtimeFacts();
119
+ const dir = join(profileDir, 'node_modules', ...name.split('/'));
120
+
121
+ let manifest;
122
+ try {
123
+ manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
124
+ } catch {
125
+ return { installed: false, reason: `${name} 装完之后读不到清单` };
126
+ }
127
+
128
+ // 运行时自己发布的声明全部对齐到当前版本。管理器读的就是这份 manifest。
129
+ const served = {};
130
+ for (const key of Object.keys(manifest.peerDependencies ?? {})) {
131
+ if (/^@deepseek-ai\/dsh(?:[-/]|$)/.test(key)) served[key] = runtimeVersion;
132
+ }
133
+ const aligned = alignManifest({ pluginDir: dir, versions: served, runtimeVersion });
134
+ if (aligned.status === 'failed') {
135
+ input.record('install-align-failed', `${name} 对齐失败:${aligned.error}`);
136
+ }
137
+
138
+ const environment = installCompatEnvironment({
139
+ pluginDir: dir,
140
+ profileDir,
141
+ requirements: manifest.peerDependencies ?? {},
142
+ imports: new Map(),
143
+ runtimeVersion,
144
+ resolveDir: input.probe.runtimeDirOf(facts),
145
+ });
146
+
147
+ try {
148
+ await input.pluginManager?.setBundleEnabled?.(name, true);
149
+ } catch {
150
+ // 重载失败不影响已经装好的东西
151
+ }
152
+
153
+ input.record('install', `${name} 已装好,环境 ${environment.status}`);
154
+ return {
155
+ installed: true,
156
+ name,
157
+ version: manifest.version,
158
+ path: dir,
159
+ environment: environment.status,
160
+ };
161
+ }
@@ -0,0 +1,206 @@
1
+ // 让本来装不上的插件能装上。
2
+ //
3
+ // pluginManager.installBundle 先跑 pnpm,然后拿新包自己的 manifest 做版本检查,
4
+ // 对不上就抛 incompatible-version。抛的时候只回滚 package.json 和 pnpm-lock.yaml,
5
+ // 包其实已经躺在 node_modules 里了,但永远不会被选中——看起来就是安装失败,什么都没装上。
6
+ //
7
+ // 正确做法不是等检查失败后再去绕过它,那时候 profile 已经回滚了。而是在管理器读 manifest
8
+ // 之前,就让检查按它自己的规则通过:把兼容环境准备好,这样它读到的声明确实满足当前运行时。
9
+ //
10
+ // 环境建在插件目录里,并留一个标记记录做过什么,装失败时可以原样撤回。
11
+ import { existsSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { alignManifest, installCompatEnvironment, readJson, removeCompatEnvironment, restoreManifest } from './compat-env.js';
14
+
15
+ // 准备好的环境在这儿等安装落地。
16
+ export const STAGING_DIR = '.request-doctor-staging';
17
+
18
+ // 判断一个安装参数指向的是本地目录还是远程包。
19
+ //
20
+ // 参数可能是包名、file: 或 link: 路径、git 地址或者 tarball。只有本地目录能在安装前
21
+ // 先看一眼,也只有这种情况才能提前准备环境——远程包的字节还没落到盘上。
22
+ export function classifySpec(spec, profileDir) {
23
+ if (typeof spec !== 'string') return { kind: 'remote' };
24
+ const text = spec.trim();
25
+ const local = /^(?:file:|link:)?(?:\.{0,2}\/|[A-Za-z]:[\\/]|\/)/.test(text);
26
+ if (!local) return { kind: 'remote' };
27
+ const path = text.replace(/^(?:file:|link:)/, '');
28
+ const absolute = /^[A-Za-z]:[\\/]|^\//.test(path) ? path : join(profileDir, path);
29
+ return existsSync(join(absolute, 'package.json')) ? { kind: 'local', dir: absolute } : { kind: 'remote' };
30
+ }
31
+
32
+ // 在本地插件装上来之前,先给它把兼容环境备好。
33
+ //
34
+ // 就地改目标目录(安装时会从这里链接),留一个标记记下改了什么,装失败了能撤回。
35
+ export function prepareForInstall(input) {
36
+ const checked = inspectCandidate(input);
37
+ if (checked.reason !== undefined) return checked;
38
+
39
+ const { dir, requirements, imports } = checked;
40
+ resetPreviousAttempt(dir);
41
+
42
+ const result = installCompatEnvironment({
43
+ pluginDir: dir,
44
+ profileDir: input.profileDir,
45
+ requirements,
46
+ imports,
47
+ runtimeVersion: input.runtimeVersion,
48
+ resolveDir: input.resolveDir,
49
+ });
50
+
51
+ // 缺符号的包要先取真实旧版本,那是异步的,这一步只对齐声明把安装放行,
52
+ // 环境交给后续扫描去建——扫描里能等网络。
53
+ if (result.status === 'needs-old-version') {
54
+ const deferred = deferEnvironment({ dir, requirements, runtimeVersion: input.runtimeVersion });
55
+ if (deferred.prepared === true) {
56
+ const names = result.needed.map((item) => `@`).join(', ');
57
+ return { ...deferred, reason: `已放行安装;环境需要真实旧版本 ,由后续扫描取回后建立` };
58
+ }
59
+ return deferred;
60
+ }
61
+ if (result.status === 'refused' && onlyMissingRuntimePackages(result.missing)) {
62
+ return deferEnvironment({ dir, requirements, runtimeVersion: input.runtimeVersion });
63
+ }
64
+ if (result.status === 'refused') {
65
+ // 环境建不全,但安装本身不该被拦下来。
66
+ //
67
+ // 被拒绝通常是因为某个符号在当前运行时里改名或者没了。那是运行期的问题:
68
+ // 插件装上之后能正常加载,真的用到那个符号时才会报错,而且报得清楚。
69
+ // 卡在安装这一步,用户连插件都看不到,反而更难判断。
70
+ // 所以这里只对齐声明放行,缺什么一并带回去告诉用户。
71
+ const deferred = deferEnvironment({ dir, requirements, runtimeVersion: input.runtimeVersion });
72
+ if (deferred.prepared === true) {
73
+ return {
74
+ ...deferred,
75
+ missing: result.missing,
76
+ reason: `${result.error};已放行安装,这些符号要等插件真正用到时才会报错`,
77
+ };
78
+ }
79
+ return deferred;
80
+ }
81
+ if (result.status !== 'installed') {
82
+ return { prepared: false, dir, reason: result.error ?? `environment could not be built (${result.status})` };
83
+ }
84
+ return finalizePreparation({ dir, packages: result.packages, runtimeVersion: input.runtimeVersion });
85
+ }
86
+
87
+ // 动笔写任何东西之前,先把目标目录验一遍。
88
+ // 返回两种结果:拒绝的理由,或者建环境需要的那几样东西。
89
+ function inspectCandidate(input) {
90
+ const classified = classifySpec(input.spec, input.profileDir);
91
+ if (classified.kind !== 'local' || classified.dir === undefined) {
92
+ return {
93
+ prepared: false,
94
+ reason: 'only a local directory can be inspected before it is installed; a registry package is downloaded by the manager itself',
95
+ };
96
+ }
97
+ const dir = classified.dir;
98
+ const manifest = readJson(join(dir, 'package.json'));
99
+ if (manifest === undefined) return { prepared: false, dir, reason: 'package.json is unreadable' };
100
+
101
+ const requirements = input.unmetPeers?.(manifest, input.runtimeVersion) ?? {};
102
+ if (Object.keys(requirements).length === 0) {
103
+ return { prepared: false, dir, reason: 'the package already declares requirements this runtime satisfies' };
104
+ }
105
+ const entry = input.entryOf?.(dir, manifest);
106
+ if (entry === undefined) return { prepared: false, dir, reason: 'the package declares no resolvable entry module' };
107
+
108
+ const facts = input.factsOf?.(dir, entry);
109
+ if (facts === undefined || facts.imports.size === 0) {
110
+ return { prepared: false, dir, reason: 'the package imports no DSH package, so no environment can be built for it' };
111
+ }
112
+ return { dir, requirements, imports: facts.imports };
113
+ }
114
+
115
+ // 把上一次尝试留下的东西清干净。
116
+ // 旧垫片留着会被当成这次建的,后面判断就全错了。
117
+ function resetPreviousAttempt(dir) {
118
+ if (existsSync(join(dir, STAGING_DIR))) rmSync(join(dir, STAGING_DIR), { recursive: true, force: true });
119
+ removeCompatEnvironment(dir);
120
+ }
121
+
122
+ // 判断被拒绝的原因是不是清一色「profile 里看不见运行时的包」。
123
+ // 运行时装在归档里时就是这个情况。安装检查只看 manifest,所以光对齐声明就够了。
124
+ function onlyMissingRuntimePackages(missing) {
125
+ return missing.every((entry) => /not installed in the profile/.test(entry));
126
+ }
127
+
128
+ // 只对齐声明,环境留给下一轮扫描去建。
129
+ function deferEnvironment(input) {
130
+ const aligned = alignManifest({
131
+ pluginDir: input.dir,
132
+ versions: Object.fromEntries(Object.keys(input.requirements).map((pkg) => [pkg, input.runtimeVersion])),
133
+ runtimeVersion: input.runtimeVersion,
134
+ });
135
+ if (aligned.status === 'failed') {
136
+ restoreManifest(input.dir);
137
+ return { prepared: false, dir: input.dir, reason: aligned.error ?? 'the manifest could not be aligned' };
138
+ }
139
+ writeFileSync(
140
+ join(input.dir, STAGING_DIR),
141
+ `${JSON.stringify({ runtimeVersion: input.runtimeVersion, packages: [], deferred: true, requirements: input.requirements }, null, 2)}\n`,
142
+ 'utf8',
143
+ );
144
+ return {
145
+ prepared: true,
146
+ dir: input.dir,
147
+ packages: [],
148
+ deferred: true,
149
+ requirements: input.requirements,
150
+ reason: 'declaration aligned so the install can proceed; the environment is built by the next scan',
151
+ };
152
+ }
153
+
154
+ // 环境建好后,把声明对齐到它实际服务的版本,并记下这次准备。
155
+ // 管理器读的是包自己的 manifest,不写这一笔,环境明明在那儿,安装照样被拒。
156
+ function finalizePreparation(input) {
157
+ const aligned = alignManifest({
158
+ pluginDir: input.dir,
159
+ versions: Object.fromEntries(input.packages.map((pkg) => [pkg, input.runtimeVersion])),
160
+ runtimeVersion: input.runtimeVersion,
161
+ });
162
+ if (aligned.status === 'failed') {
163
+ abandonPreparation(input.dir);
164
+ return { prepared: false, dir: input.dir, reason: aligned.error ?? 'the manifest could not be aligned' };
165
+ }
166
+ writeFileSync(
167
+ join(input.dir, STAGING_DIR),
168
+ `${JSON.stringify({ runtimeVersion: input.runtimeVersion, packages: input.packages }, null, 2)}\n`,
169
+ 'utf8',
170
+ );
171
+ return { prepared: true, dir: input.dir, packages: input.packages };
172
+ }
173
+ // 读一下还没处理完的准备标记。
174
+ export function readStagedPreparation(dir) {
175
+ return readJson(join(dir, STAGING_DIR));
176
+ }
177
+
178
+ // 环境已经有了,回去把准备标记清掉。
179
+ export function clearStagedPreparation(dir) {
180
+ try {
181
+ rmSync(join(dir, STAGING_DIR), { force: true });
182
+ } catch {
183
+ /* retried on the next scan */
184
+ }
185
+ }
186
+
187
+ // 安装没成的话,把准备过的痕迹原样撤掉。
188
+ export function abandonPreparation(dir) {
189
+ const staging = join(dir, STAGING_DIR);
190
+ if (!existsSync(staging)) return false;
191
+ removeCompatEnvironment(dir);
192
+ // 这次安装对齐过 manifest,现在把原来的还回去,失败的尝试不该在候选目录里留下痕迹。
193
+ restoreManifest(dir);
194
+ try {
195
+ rmSync(staging, { force: true });
196
+ } catch {
197
+ /* reported through the return value */
198
+ }
199
+ return true;
200
+ }
201
+
202
+ // 管理器装完之后,确认准备还在不在。
203
+ // 它会换掉目录(file: 安装是拷贝不是链接),那样环境就没了,得从装好的那份重建。
204
+ export function preparationSurvived(dir) {
205
+ return existsSync(join(dir, STAGING_DIR));
206
+ }