dsh-config-manager 0.1.76 → 0.1.78
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/adapters/plugin-data-declared.d.ts +12 -0
- package/lib/adapters/plugin-data-declared.js +57 -0
- package/lib/adapters/plugin-data.d.ts +4 -6
- package/lib/adapters/plugin-data.js +8 -36
- package/lib/adapters/plugins.d.ts +16 -0
- package/lib/adapters/plugins.js +223 -16
- package/lib/client.d.ts +104 -9
- package/lib/client.js +1800 -718
- package/lib/core/analyzer.js +3 -1
- package/lib/core/backup.d.ts +9 -0
- package/lib/core/backup.js +16 -1
- package/lib/core/messages.d.ts +11 -0
- package/lib/core/messages.js +22 -0
- package/lib/core/plugin-cli.d.ts +26 -0
- package/lib/core/plugin-cli.js +62 -2
- package/lib/core/rollback-attribution.d.ts +81 -0
- package/lib/core/rollback-attribution.js +236 -0
- package/lib/core/rollback-merge.d.ts +22 -0
- package/lib/core/rollback-merge.js +203 -0
- package/lib/core/rollback-point.d.ts +114 -0
- package/lib/core/rollback-point.js +534 -17
- package/lib/core/types.d.ts +28 -0
- package/lib/index.d.ts +26 -3
- package/lib/index.js +129 -16
- package/lib/routes/recovery.js +11 -0
- package/lib/schema/section-registry.js +5 -5
- package/lib/schema/types.d.ts +14 -0
- package/lib/ui/badge-tone.d.ts +16 -0
- package/lib/ui/badge-tone.js +20 -0
- package/lib/ui/i18n.d.ts +4 -4
- package/lib/ui/i18n.js +8 -8
- package/lib/ui/rollback-view.d.ts +95 -0
- package/lib/ui/rollback-view.js +55 -0
- package/lib/ui/selection-model.d.ts +140 -1
- package/lib/ui/selection-model.js +241 -9
- package/package.json +1 -1
- package/src/adapters/plugin-data-declared.ts +60 -0
- package/src/adapters/plugin-data.ts +7 -33
- package/src/adapters/plugins.test.ts +162 -0
- package/src/adapters/plugins.ts +236 -17
- package/src/adapters/units.test.ts +26 -0
- package/src/client/chat/ChatEntryView.tsx +147 -46
- package/src/client/chat/chat-entry-view.test.ts +277 -1
- package/src/client/chat/chat-entry-view.ts +98 -4
- package/src/client/chat/chat-node-registration.ts +71 -0
- package/src/client/chat/chat-slots.ts +30 -12
- package/src/client/common/ContentPicker.tsx +581 -175
- package/src/client/common/host-primitives.ts +64 -1
- package/src/client/common/ui.tsx +17 -3
- package/src/client/config-manager.module.css +630 -397
- package/src/client/css-scale-guard.test.ts +41 -3
- package/src/client/css-token-guard.test.ts +10 -4
- package/src/client/export/ExportView.tsx +2 -0
- package/src/client/index.ts +15 -3
- package/src/client/locales.ts +54 -10
- package/src/client/recovery/RollbackCard.tsx +56 -2
- package/src/client/recovery/RollbackDiffPane.tsx +5 -0
- package/src/client/recovery/RollbackFileChanges.tsx +46 -2
- package/src/client/recovery/recovery-locales.ts +22 -0
- package/src/client/recovery/rollback-file-changes.test.ts +1 -1
- package/src/core/analyzer.ts +3 -1
- package/src/core/backup.ts +16 -1
- package/src/core/messages.ts +22 -0
- package/src/core/plugin-cli.fs.test.ts +65 -3
- package/src/core/plugin-cli.ts +55 -2
- package/src/core/rollback-attribution.test.ts +120 -0
- package/src/core/rollback-attribution.ts +267 -0
- package/src/core/rollback-merge.test.ts +133 -0
- package/src/core/rollback-merge.ts +213 -0
- package/src/core/rollback-point.test.ts +412 -0
- package/src/core/rollback-point.ts +560 -20
- package/src/core/types.ts +29 -0
- package/src/index.facade.test.ts +60 -0
- package/src/index.ts +133 -19
- package/src/routes/recovery.ts +11 -0
- package/src/schema/section-registry.ts +5 -5
- package/src/schema/types.ts +14 -0
- package/src/ui/badge-tone.test.ts +23 -0
- package/src/ui/badge-tone.ts +27 -0
- package/src/ui/i18n.ts +8 -8
- package/src/ui/rollback-view.test.ts +27 -0
- package/src/ui/rollback-view.ts +139 -0
- package/src/ui/selection-model.test.ts +240 -2
- package/src/ui/selection-model.ts +349 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PluginDataSource } from '../core/plugin-data-registry.ts';
|
|
2
|
+
import type { HostContext } from '../core/types.ts';
|
|
3
|
+
/** 单次扫描最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
|
|
4
|
+
export declare const MAX_DECLARED_PACKAGES = 300;
|
|
5
|
+
/**
|
|
6
|
+
* 解析声明时用的 profile 目录名。
|
|
7
|
+
*
|
|
8
|
+
* 与 DSH 的 profile 解析同口径:空 / undefined = \`web\`(与改造前的私有实现一致)。
|
|
9
|
+
*/
|
|
10
|
+
export declare function declaredProfileName(profile: string | undefined): string;
|
|
11
|
+
/** 扫描本机已安装插件自述的数据目录声明(零副作用;读不到的一律跳过)。 */
|
|
12
|
+
export declare function scanDeclaredDataSources(ctx: HostContext, maxPackages?: number): Promise<PluginDataSource[]>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「已安装插件自述数据目录(dsh.dataDirs)」的**单一扫描实现**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一个模块:这份扫描有两个消费者 ——
|
|
5
|
+
* - \`pluginData\` / \`pluginAssets\` adapter(决定要收集哪些区域);
|
|
6
|
+
* - \`plugins\` adapter(导出选择器「按插件看」需要知道**每个插件自己声明的数据区域**,
|
|
7
|
+
* 才能在不读任何数据目录的前提下画出树骨架)。
|
|
8
|
+
* 两处各写一份必然漂移(同一条铁律:同一事实只有一份实现)。
|
|
9
|
+
*
|
|
10
|
+
* 语义(与改造前 plugin-data.ts 的私有实现逐字一致):
|
|
11
|
+
* - 读的是 **profile 实际安装**的那份 manifest(\`profiles/<profile>/node_modules/<pkg>/package.json\`),
|
|
12
|
+
* 不是全局目录 —— 同一个包在不同 profile 可以是不同版本,声明必须取本 profile 装的;
|
|
13
|
+
* - 读不到(未安装 / 无声明 / 形状非法)一律跳过:本机制是**附加**信息,绝不因为一个包读不出来
|
|
14
|
+
* 就影响内置来源或整次导出;
|
|
15
|
+
* - 本函数**不缓存**(缓存策略由调用方决定:pluginData 有实例级 TTL 缓存,plugins 有模块级缓存)。
|
|
16
|
+
*/
|
|
17
|
+
import { parseDeclaredDataDirs } from '../core/plugin-data-registry.js';
|
|
18
|
+
/** 单次扫描最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
|
|
19
|
+
export const MAX_DECLARED_PACKAGES = 300;
|
|
20
|
+
/**
|
|
21
|
+
* 解析声明时用的 profile 目录名。
|
|
22
|
+
*
|
|
23
|
+
* 与 DSH 的 profile 解析同口径:空 / undefined = \`web\`(与改造前的私有实现一致)。
|
|
24
|
+
*/
|
|
25
|
+
export function declaredProfileName(profile) {
|
|
26
|
+
return profile !== undefined && profile !== '' ? profile : 'web';
|
|
27
|
+
}
|
|
28
|
+
/** 扫描本机已安装插件自述的数据目录声明(零副作用;读不到的一律跳过)。 */
|
|
29
|
+
export async function scanDeclaredDataSources(ctx, maxPackages = MAX_DECLARED_PACKAGES) {
|
|
30
|
+
const base = 'profiles/' + declaredProfileName(ctx.profile);
|
|
31
|
+
const out = [];
|
|
32
|
+
let deps = [];
|
|
33
|
+
try {
|
|
34
|
+
const raw = await ctx.fs.readFile(base + '/package.json');
|
|
35
|
+
const parsed = JSON.parse(new TextDecoder().decode(raw));
|
|
36
|
+
const dep = parsed?.dependencies;
|
|
37
|
+
if (dep !== null && typeof dep === 'object')
|
|
38
|
+
deps = Object.keys(dep).slice(0, maxPackages);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
deps = [];
|
|
42
|
+
}
|
|
43
|
+
for (const name of deps) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = await ctx.fs.readFile(base + '/node_modules/' + name + '/package.json');
|
|
46
|
+
const manifest = JSON.parse(new TextDecoder().decode(raw));
|
|
47
|
+
const source = parseDeclaredDataDirs(name, manifest);
|
|
48
|
+
if (source !== null)
|
|
49
|
+
out.push(source);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// 未安装 / 无 manifest → 无声明,跳过
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=plugin-data-declared.js.map
|
|
@@ -47,13 +47,11 @@ export declare class PluginDataAdapter extends FileCollectionAdapter {
|
|
|
47
47
|
private allowedEntries;
|
|
48
48
|
private allSources;
|
|
49
49
|
/**
|
|
50
|
-
* 已安装插件的自述声明(dsh.dataDirs
|
|
50
|
+
* 已安装插件的自述声明(dsh.dataDirs)—— 本方法只做**缓存**,扫描实现见 plugin-data-declared.ts。
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
* 下(pnpm
|
|
54
|
-
*
|
|
55
|
-
* 读不到(未安装 / 无声明 / 形状非法)一律跳过:本机制是**附加**信息,绝不因为一个包读不出来
|
|
56
|
-
* 就影响内置来源或整次导出。
|
|
52
|
+
* 为什么缓存必须带 ctx 键(homeDir + profile):DSH 的插件装在 \`profiles/<profile>/node_modules\`
|
|
53
|
+
* 下(pnpm 隔离布局),而「这台机器装了某插件」不代表另一台装了 —— 缓存若按实例共享,
|
|
54
|
+
* 导入到「没装该插件」的目标机时会拿源机的声明去放行写入,白名单就变成「谁先导入谁说了算」。
|
|
57
55
|
*/
|
|
58
56
|
private declaredSources;
|
|
59
57
|
/**
|
|
@@ -30,13 +30,12 @@ import { normalizePath } from '../utils/paths.js';
|
|
|
30
30
|
import { engineMsg } from './messages.js';
|
|
31
31
|
import { listFilesDetailed } from './link-report.js';
|
|
32
32
|
import { sectionMeta } from '../schema/section-registry.js';
|
|
33
|
-
import { BUILTIN_PLUGIN_DATA_SOURCES, areaOfPath, areasForSection, isExcludedRel, normalizeHomeRel,
|
|
33
|
+
import { BUILTIN_PLUGIN_DATA_SOURCES, areaOfPath, areasForSection, isExcludedRel, normalizeHomeRel, } from '../core/plugin-data-registry.js';
|
|
34
|
+
import { declaredProfileName, scanDeclaredDataSources } from './plugin-data-declared.js';
|
|
34
35
|
import { osHomeDirOf } from '../core/backup.js';
|
|
35
36
|
import { FileCollectionAdapter } from './file-collection.js';
|
|
36
37
|
/** 声明来源缓存的 TTL(毫秒):一次导出/导入/执行里会多次解析,逐次读 15+ 个 package.json 是净开销 */
|
|
37
38
|
const DECLARED_TTL_MS = 5_000;
|
|
38
|
-
/** 单次解析里最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
|
|
39
|
-
const MAX_DECLARED_PACKAGES = 300;
|
|
40
39
|
export class PluginDataAdapter extends FileCollectionAdapter {
|
|
41
40
|
id;
|
|
42
41
|
displayName;
|
|
@@ -75,48 +74,21 @@ export class PluginDataAdapter extends FileCollectionAdapter {
|
|
|
75
74
|
return [...this.builtin, ...declared];
|
|
76
75
|
}
|
|
77
76
|
/**
|
|
78
|
-
* 已安装插件的自述声明(dsh.dataDirs
|
|
77
|
+
* 已安装插件的自述声明(dsh.dataDirs)—— 本方法只做**缓存**,扫描实现见 plugin-data-declared.ts。
|
|
79
78
|
*
|
|
80
|
-
*
|
|
81
|
-
* 下(pnpm
|
|
82
|
-
*
|
|
83
|
-
* 读不到(未安装 / 无声明 / 形状非法)一律跳过:本机制是**附加**信息,绝不因为一个包读不出来
|
|
84
|
-
* 就影响内置来源或整次导出。
|
|
79
|
+
* 为什么缓存必须带 ctx 键(homeDir + profile):DSH 的插件装在 \`profiles/<profile>/node_modules\`
|
|
80
|
+
* 下(pnpm 隔离布局),而「这台机器装了某插件」不代表另一台装了 —— 缓存若按实例共享,
|
|
81
|
+
* 导入到「没装该插件」的目标机时会拿源机的声明去放行写入,白名单就变成「谁先导入谁说了算」。
|
|
85
82
|
*/
|
|
86
83
|
async declaredSources(ctx) {
|
|
87
84
|
const now = Date.now();
|
|
88
|
-
const
|
|
89
|
-
const memoKey = ctx.homeDir + '|' + profile;
|
|
85
|
+
const memoKey = ctx.homeDir + '|' + declaredProfileName(ctx.profile);
|
|
90
86
|
if (this.declaredMemo !== undefined
|
|
91
87
|
&& this.declaredMemo.key === memoKey
|
|
92
88
|
&& now - this.declaredMemo.at < this.declaredTtlMs) {
|
|
93
89
|
return this.declaredMemo.sources;
|
|
94
90
|
}
|
|
95
|
-
const
|
|
96
|
-
const out = [];
|
|
97
|
-
let deps = [];
|
|
98
|
-
try {
|
|
99
|
-
const raw = await ctx.fs.readFile(base + '/package.json');
|
|
100
|
-
const parsed = JSON.parse(new TextDecoder().decode(raw));
|
|
101
|
-
const dep = parsed?.dependencies;
|
|
102
|
-
if (dep !== null && typeof dep === 'object')
|
|
103
|
-
deps = Object.keys(dep).slice(0, MAX_DECLARED_PACKAGES);
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
deps = [];
|
|
107
|
-
}
|
|
108
|
-
for (const name of deps) {
|
|
109
|
-
try {
|
|
110
|
-
const raw = await ctx.fs.readFile(base + '/node_modules/' + name + '/package.json');
|
|
111
|
-
const manifest = JSON.parse(new TextDecoder().decode(raw));
|
|
112
|
-
const source = parseDeclaredDataDirs(name, manifest);
|
|
113
|
-
if (source !== null)
|
|
114
|
-
out.push(source);
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
// 未安装 / 无 manifest → 无声明,跳过
|
|
118
|
-
}
|
|
119
|
-
}
|
|
91
|
+
const out = await scanDeclaredDataSources(ctx);
|
|
120
92
|
this.declaredMemo = { key: memoKey, at: now, sources: out };
|
|
121
93
|
return out;
|
|
122
94
|
}
|
|
@@ -72,6 +72,22 @@ export declare class PluginsAdapter implements ConfigAdapter<PluginsSection> {
|
|
|
72
72
|
* 不做任何写入(写入在 applyItem 内、且先于 pnpm-workspace.yaml)。
|
|
73
73
|
*/
|
|
74
74
|
private patchAvailability;
|
|
75
|
+
/**
|
|
76
|
+
* 读目标机 profile 的 `dsh.profile.bundles`(issue #95)。
|
|
77
|
+
*
|
|
78
|
+
* 相对路径与写入目标同源(`profiles/<profile>/package.json`),走 `ctx.target.fs` 而不是
|
|
79
|
+
* 宿主私有通道:与 pnpm-workspace.yaml / patch 文件同一条 I/O 纪律(受限、可被 MemFs 测试)。
|
|
80
|
+
*/
|
|
81
|
+
private targetBundles;
|
|
82
|
+
/**
|
|
83
|
+
* 把 host 自带的层声明进目标机 profile 的 `dsh.profile.bundles`(issue #95)。
|
|
84
|
+
*
|
|
85
|
+
* 为什么**不走安装通道**:这类层的包由 DSH 从自己的安装位置解析;按裸包名 `dsh plugin add`
|
|
86
|
+
* 会把 npm `dist-tags.latest` 的副本装进 profile,与 host 的版本线脱节 —— 真机故障:
|
|
87
|
+
* host 0.2.1-rc.2 + 包 0.2.1-alpha.2 的隔离组契约对不上 → `session-controller` 一直 pending
|
|
88
|
+
* → 新建会话 service-unavailable。声明不了时只报非致命 warning(§34.17),不拖垮其余配置。
|
|
89
|
+
*/
|
|
90
|
+
private declareHostBundle;
|
|
75
91
|
analyzeImport(data: PluginsSection, ctx: ImportContext): Promise<PlanItem[]>;
|
|
76
92
|
applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
|
|
77
93
|
validate(data: PluginsSection, msg?: MsgFunc): Promise<ValidationResult>;
|
package/lib/adapters/plugins.js
CHANGED
|
@@ -13,16 +13,18 @@
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { isDeepStrictEqual } from 'node:util';
|
|
15
15
|
import { sha256Hex } from '../utils/hashing.js';
|
|
16
|
-
import { installSpecFor, resolveProcessProfileName } from '../core/plugin-cli.js';
|
|
16
|
+
import { installSpecFor, isBundlesOnlyEntry, mergeBundleDeclaration, resolveProcessProfileName } from '../core/plugin-cli.js';
|
|
17
17
|
import { classifyPluginSpec, LOCAL_PLUGIN_DIR, resolveLocalPluginPath } from '../core/local-plugin-pack.js';
|
|
18
18
|
import { msgOf, zhMsg } from '../core/messages.js';
|
|
19
19
|
import { engineMsg } from './messages.js';
|
|
20
20
|
import { stringifyJsonSafe } from '../utils/json.js';
|
|
21
21
|
import { isPathSafe, normalizePath } from '../utils/paths.js';
|
|
22
22
|
import { isENOENT } from '../utils/guards.js';
|
|
23
|
-
import { PLUGIN_PATCH_REF_PREFIX } from '../core/backup.js';
|
|
23
|
+
import { PLUGIN_PATCH_REF_PREFIX, PLUGIN_PROFILE_MANIFEST_REF } from '../core/backup.js';
|
|
24
24
|
import { readEffectivePatchLines, resolveWriteLayer, USER_PATCH_FILE } from '../core/patch-layers.js';
|
|
25
25
|
import { parsePnpmPatchedDependencies, sanitizePnpmWorkspacePatches } from './pnpm-workspace.js';
|
|
26
|
+
import { BUILTIN_PLUGIN_DATA_SOURCES, areasForSection } from '../core/plugin-data-registry.js';
|
|
27
|
+
import { declaredProfileName, scanDeclaredDataSources } from './plugin-data-declared.js';
|
|
26
28
|
import { sectionMeta } from '../schema/section-registry.js';
|
|
27
29
|
import { validateJsonSection } from './json-section.js';
|
|
28
30
|
/**
|
|
@@ -126,6 +128,66 @@ function entriesOfRaw(raw) {
|
|
|
126
128
|
* 导出产物(真实 base64)走另一条分支,行为逐字不变。
|
|
127
129
|
*/
|
|
128
130
|
const previewTarballBytes = new WeakMap();
|
|
131
|
+
/**
|
|
132
|
+
* 「插件包名 → 它声明的数据区域」映射(只有 \`plugin:<包名>\` 单元用;导出选择器「按插件看」的树骨架)。
|
|
133
|
+
*
|
|
134
|
+
* 为什么挂 WeakMap 而不是给 PluginsSection 加字段:这是**展示用旁路信息** —— 既不是备份格式的
|
|
135
|
+
* 一部分(schema 校验与市场防线都不该看见它),也与「导出真的带什么」无关;挂在载荷对象上随对象
|
|
136
|
+
* 一起回收,载荷本身逐字节不变(与 previewTarballBytes 同一套理由)。
|
|
137
|
+
*/
|
|
138
|
+
const relatedAreasByData = new WeakMap();
|
|
139
|
+
/** 内置来源 + 自述声明的扫描缓存(TTL 内的同一 home/profile 只扫一次;/export-preview 一趟会问两次) */
|
|
140
|
+
const declaredSourcesMemo = { key: '', at: 0, sources: [] };
|
|
141
|
+
const DECLARED_MEMO_TTL_MS = 5_000;
|
|
142
|
+
/**
|
|
143
|
+
* 本机可归属的插件数据来源 = 内置来源表 + 已安装插件自述声明。
|
|
144
|
+
*
|
|
145
|
+
* 缓存键必须带 homeDir + profile:同一台机器换 profile 就是另一批已安装插件,拿旧声明画树
|
|
146
|
+
* 会让用户看到不属于当前 profile 的数据区域。
|
|
147
|
+
*/
|
|
148
|
+
async function pluginDataSources(ctx) {
|
|
149
|
+
const key = ctx.homeDir + '|' + declaredProfileName(ctx.profile);
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
if (declaredSourcesMemo.key === key && now - declaredSourcesMemo.at < DECLARED_MEMO_TTL_MS) {
|
|
152
|
+
return declaredSourcesMemo.sources;
|
|
153
|
+
}
|
|
154
|
+
const sources = [...BUILTIN_PLUGIN_DATA_SOURCES, ...await scanDeclaredDataSources(ctx)];
|
|
155
|
+
declaredSourcesMemo.key = key;
|
|
156
|
+
declaredSourcesMemo.at = now;
|
|
157
|
+
declaredSourcesMemo.sources = sources;
|
|
158
|
+
return sources;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 包名 → 该插件的数据区域引用。
|
|
162
|
+
*
|
|
163
|
+
* 口径与 pluginData / pluginAssets 的收集面**同源**(同一个 areasForSection):role=config 进
|
|
164
|
+
* pluginData、role=assets 进 pluginAssets;role=cache 的区域不属于任何分区,因此**绝不进树**
|
|
165
|
+
* (缓存是可再生产物,列出来只会诱导用户把它带走)。
|
|
166
|
+
*
|
|
167
|
+
* 只产出「注册表说它应该在哪儿」,不代表磁盘上真有 —— 界面必须把两者分开渲染。
|
|
168
|
+
*/
|
|
169
|
+
async function relatedAreasOf(ctx) {
|
|
170
|
+
const sources = await pluginDataSources(ctx);
|
|
171
|
+
const map = new Map();
|
|
172
|
+
for (const source of sources) {
|
|
173
|
+
const refs = [];
|
|
174
|
+
for (const section of ['pluginData', 'pluginAssets']) {
|
|
175
|
+
for (const entry of areasForSection([source], section)) {
|
|
176
|
+
refs.push({ section, relPath: entry.area.relPath, role: entry.area.role === 'assets' ? 'assets' : 'config' });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (refs.length === 0)
|
|
180
|
+
continue;
|
|
181
|
+
for (const pkg of source.packages) {
|
|
182
|
+
const cur = map.get(pkg);
|
|
183
|
+
if (cur === undefined)
|
|
184
|
+
map.set(pkg, [...refs]);
|
|
185
|
+
else
|
|
186
|
+
cur.push(...refs);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return map;
|
|
190
|
+
}
|
|
129
191
|
/**
|
|
130
192
|
* 零 spawn 推导本地源插件的体积(预览专用)。
|
|
131
193
|
*
|
|
@@ -179,7 +241,7 @@ async function measureLocalSources(plugins, ctx) {
|
|
|
179
241
|
* `tarballBytes` = 预览路径登记到的本地源体积(见 measureLocalSources,0 = 体积未知但仍随包携带);
|
|
180
242
|
* 缺省时体积与文件数完全按导出产物里的 base64 计算 —— 与改造前逐字一致。
|
|
181
243
|
*/
|
|
182
|
-
function unitsOf(data, tarballBytes) {
|
|
244
|
+
function unitsOf(data, tarballBytes, related) {
|
|
183
245
|
const wsId = 'plugins:pnpm-workspace';
|
|
184
246
|
const patchFileIds = (data.patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
|
|
185
247
|
const group = [wsId, ...patchFileIds];
|
|
@@ -187,6 +249,7 @@ function unitsOf(data, tarballBytes) {
|
|
|
187
249
|
for (const pl of data.plugins) {
|
|
188
250
|
const tarball = tarballOf(data, pl.name);
|
|
189
251
|
const measured = tarballBytes?.get(pl.name);
|
|
252
|
+
const areas = related?.get(pl.name);
|
|
190
253
|
units.push({
|
|
191
254
|
id: `plugin:${pl.name}`,
|
|
192
255
|
label: pl.name,
|
|
@@ -194,6 +257,8 @@ function unitsOf(data, tarballBytes) {
|
|
|
194
257
|
// base64 长度 → 原始字节数(仅用于展示体积,不求精确);预览路径没有字节,用 stat 量到的源体积
|
|
195
258
|
sizeBytes: tarball !== undefined ? Math.floor((tarball.base64.length * 3) / 4) : measured ?? 0,
|
|
196
259
|
...(tarball !== undefined || measured !== undefined ? { fileCount: 1 } : {}),
|
|
260
|
+
// 「按插件看」的树骨架:该插件声明的数据区域(零数据目录 I/O)
|
|
261
|
+
...(areas !== undefined && areas.length > 0 ? { relatedAreas: areas } : {}),
|
|
197
262
|
});
|
|
198
263
|
}
|
|
199
264
|
for (const line of data.patch) {
|
|
@@ -260,7 +325,7 @@ export class PluginsAdapter {
|
|
|
260
325
|
section: collected,
|
|
261
326
|
// 与 src/index.ts 对 JSON 分区现算体积的口径一致(utf8 字节数)
|
|
262
327
|
sizeBytes: new TextEncoder().encode(stringifyJsonSafe(data)).length,
|
|
263
|
-
items: unitsOf(data, tarballBytes),
|
|
328
|
+
items: unitsOf(data, tarballBytes, relatedAreasByData.get(data)),
|
|
264
329
|
};
|
|
265
330
|
}
|
|
266
331
|
/**
|
|
@@ -286,6 +351,9 @@ export class PluginsAdapter {
|
|
|
286
351
|
isBundle: p.isBundle ?? false,
|
|
287
352
|
inBundles: p.inBundles ?? [],
|
|
288
353
|
enabled: p.enabled,
|
|
354
|
+
// issue #95:只在 dsh.profile.bundles 里声明的层(源机无依赖声明)必须带上这个标记,
|
|
355
|
+
// 导入端据此走「只声明」而不是 npm 安装。缺省不写,普通依赖的字节与改造前逐字一致。
|
|
356
|
+
...(p.bundlesOnly === true ? { bundlesOnly: true } : {}),
|
|
289
357
|
});
|
|
290
358
|
}
|
|
291
359
|
}
|
|
@@ -401,16 +469,30 @@ export class PluginsAdapter {
|
|
|
401
469
|
const alive = new Set(keptPlugins.map((pl) => pl.name));
|
|
402
470
|
keptTarballs = localTarballs?.filter((t) => alive.has(t.packageName));
|
|
403
471
|
}
|
|
472
|
+
const data = {
|
|
473
|
+
version: 1,
|
|
474
|
+
plugins: keptPlugins,
|
|
475
|
+
patch: keptPatch,
|
|
476
|
+
pnpmWorkspace: keptWorkspace,
|
|
477
|
+
...(keptTarballs !== undefined ? { localTarballs: keptTarballs } : {}),
|
|
478
|
+
...(keptPatchFiles !== undefined ? { patchFiles: keptPatchFiles } : {}),
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* 「按插件看」的树骨架(导出选择器):登记每个插件声明的数据区域。
|
|
482
|
+
* 只登记「至少有一个区域」的插件 —— 其余插件单元保持无该字段,界面据此不显示空壳节点。
|
|
483
|
+
* 归属信息是**附加**信息:扫描失败不得影响导出/预览本身(与 pluginData 同一纪律)。
|
|
484
|
+
*/
|
|
485
|
+
try {
|
|
486
|
+
const related = await relatedAreasOf(ctx);
|
|
487
|
+
if (related.size > 0)
|
|
488
|
+
relatedAreasByData.set(data, related);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// 读不到声明就不登记(界面按「没有已知关联」渲染,绝不猜)
|
|
492
|
+
}
|
|
404
493
|
return {
|
|
405
494
|
sectionId: 'plugins',
|
|
406
|
-
data
|
|
407
|
-
version: 1,
|
|
408
|
-
plugins: keptPlugins,
|
|
409
|
-
patch: keptPatch,
|
|
410
|
-
pnpmWorkspace: keptWorkspace,
|
|
411
|
-
...(keptTarballs !== undefined ? { localTarballs: keptTarballs } : {}),
|
|
412
|
-
...(keptPatchFiles !== undefined ? { patchFiles: keptPatchFiles } : {}),
|
|
413
|
-
},
|
|
495
|
+
data,
|
|
414
496
|
counts: {
|
|
415
497
|
plugins: keptPlugins.length,
|
|
416
498
|
patchLines: keptPatch.length,
|
|
@@ -429,8 +511,9 @@ export class PluginsAdapter {
|
|
|
429
511
|
* 由选择模型保证勾选/取消同步。
|
|
430
512
|
*/
|
|
431
513
|
listUnits(section) {
|
|
432
|
-
// 预览产物没有 tarball 字节,体积来自 measureLocalSources 挂上的统计(导出产物走 base64
|
|
433
|
-
|
|
514
|
+
// 预览产物没有 tarball 字节,体积来自 measureLocalSources 挂上的统计(导出产物走 base64 分支);
|
|
515
|
+
// 「按插件看」的数据区域骨架同样由 collect() 登记在载荷对象上(两条路径逐字一致,units.test 钉住)
|
|
516
|
+
return unitsOf(section.data, previewTarballBytes.get(section.data), relatedAreasByData.get(section.data));
|
|
434
517
|
}
|
|
435
518
|
/**
|
|
436
519
|
* 导入后目标机**可能**存在的 patch 文件集合(相对 profile 目录)——issue #35。
|
|
@@ -463,6 +546,83 @@ export class PluginsAdapter {
|
|
|
463
546
|
}
|
|
464
547
|
return set;
|
|
465
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* 读目标机 profile 的 `dsh.profile.bundles`(issue #95)。
|
|
551
|
+
*
|
|
552
|
+
* 相对路径与写入目标同源(`profiles/<profile>/package.json`),走 `ctx.target.fs` 而不是
|
|
553
|
+
* 宿主私有通道:与 pnpm-workspace.yaml / patch 文件同一条 I/O 纪律(受限、可被 MemFs 测试)。
|
|
554
|
+
*/
|
|
555
|
+
async targetBundles(ctx) {
|
|
556
|
+
const rel = `${PNPM_PROFILE_DIR(ctx.target.profile)}/package.json`;
|
|
557
|
+
try {
|
|
558
|
+
if (!(await ctx.target.fs.exists(rel)))
|
|
559
|
+
return { state: 'absent' };
|
|
560
|
+
const text = new TextDecoder().decode(await ctx.target.fs.readFile(rel));
|
|
561
|
+
let parsed;
|
|
562
|
+
try {
|
|
563
|
+
parsed = JSON.parse(text);
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return { state: 'unreadable', reason: ctx.msg('adapter.pluginHostBundleBadJson') };
|
|
567
|
+
}
|
|
568
|
+
const bundles = parsed?.dsh?.profile?.bundles;
|
|
569
|
+
if (!Array.isArray(bundles) || bundles.some((entry) => typeof entry !== 'string')) {
|
|
570
|
+
return { state: 'unreadable', reason: ctx.msg('adapter.pluginHostBundleBadShape') };
|
|
571
|
+
}
|
|
572
|
+
return { state: 'ok', names: new Set(bundles), text };
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
return { state: 'unreadable', reason: err instanceof Error ? err.message : String(err) };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* 把 host 自带的层声明进目标机 profile 的 `dsh.profile.bundles`(issue #95)。
|
|
580
|
+
*
|
|
581
|
+
* 为什么**不走安装通道**:这类层的包由 DSH 从自己的安装位置解析;按裸包名 `dsh plugin add`
|
|
582
|
+
* 会把 npm `dist-tags.latest` 的副本装进 profile,与 host 的版本线脱节 —— 真机故障:
|
|
583
|
+
* host 0.2.1-rc.2 + 包 0.2.1-alpha.2 的隔离组契约对不上 → `session-controller` 一直 pending
|
|
584
|
+
* → 新建会话 service-unavailable。声明不了时只报非致命 warning(§34.17),不拖垮其余配置。
|
|
585
|
+
*/
|
|
586
|
+
async declareHostBundle(name, ctx) {
|
|
587
|
+
const msg = ctx.msg;
|
|
588
|
+
const rel = `${PNPM_PROFILE_DIR(ctx.target.profile)}/package.json`;
|
|
589
|
+
const view = await this.targetBundles(ctx);
|
|
590
|
+
if (view.state === 'absent') {
|
|
591
|
+
return { ok: false, warning: true, message: msg('adapter.pluginHostBundleNoManifest', { name }) };
|
|
592
|
+
}
|
|
593
|
+
if (view.state === 'unreadable') {
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
warning: true,
|
|
597
|
+
message: msg('adapter.pluginHostBundleManifestUnreadable', { name, reason: view.reason }),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (view.names.has(name)) {
|
|
601
|
+
return { ok: true, needsRestart: false, message: msg('adapter.pluginHostBundleSame', { name }) };
|
|
602
|
+
}
|
|
603
|
+
const merged = mergeBundleDeclaration(view.text, name);
|
|
604
|
+
if (merged === null) {
|
|
605
|
+
return {
|
|
606
|
+
ok: false,
|
|
607
|
+
warning: true,
|
|
608
|
+
message: msg('adapter.pluginHostBundleManifestUnreadable', {
|
|
609
|
+
name,
|
|
610
|
+
reason: msg('adapter.pluginHostBundleBadShape'),
|
|
611
|
+
}),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
await ctx.target.fs.writeFile(rel, new TextEncoder().encode(merged.text));
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
return {
|
|
619
|
+
ok: false,
|
|
620
|
+
warning: true,
|
|
621
|
+
message: msg('adapter.pluginHostBundleWriteFailed', { name, reason: err instanceof Error ? err.message : String(err) }),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
return { ok: true, needsRestart: true, message: msg('adapter.pluginHostBundleDeclared', { name }) };
|
|
625
|
+
}
|
|
466
626
|
async analyzeImport(data, ctx) {
|
|
467
627
|
const msg = ctx.msg;
|
|
468
628
|
const items = [];
|
|
@@ -554,8 +714,44 @@ export class PluginsAdapter {
|
|
|
554
714
|
}
|
|
555
715
|
// 插件:包名唯一键;同版本 Skip / 未装 Install / 版本不同 Conflict
|
|
556
716
|
const targetInstalled = await ctx.target.plugins.listInstalled();
|
|
717
|
+
// issue #95:只在 dsh.profile.bundles 里声明的层(源机没有依赖声明)**不装 npm 包** ——
|
|
718
|
+
// 它的名字只声明进目标机的 bundles,由 DSH 从自己的安装位置解析。只在真出现这类条目时才读
|
|
719
|
+
// 目标清单(其余场景零额外 I/O,行为与改造前逐字一致)。
|
|
720
|
+
const bundleView = data.plugins.some((p) => isBundlesOnlyEntry(p)) ? await this.targetBundles(ctx) : null;
|
|
557
721
|
for (const p of data.plugins) {
|
|
558
722
|
const id = `plugin:${p.name}`;
|
|
723
|
+
if (isBundlesOnlyEntry(p)) {
|
|
724
|
+
if (bundleView !== null && bundleView.state === 'ok' && bundleView.names.has(p.name)) {
|
|
725
|
+
items.push({
|
|
726
|
+
id, kind: 'Skip', adapter: 'plugins',
|
|
727
|
+
description: msg('adapter.pluginHostBundleSame', { name: p.name }),
|
|
728
|
+
detail: msg('adapter.pluginHostBundleDetail'),
|
|
729
|
+
severity: 'info',
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
else if (bundleView !== null && bundleView.state === 'ok') {
|
|
733
|
+
items.push({
|
|
734
|
+
id, kind: 'Create', adapter: 'plugins',
|
|
735
|
+
description: msg('adapter.pluginHostBundleDeclare', { name: p.name }),
|
|
736
|
+
detail: msg('adapter.pluginHostBundleDetail'),
|
|
737
|
+
severity: 'info',
|
|
738
|
+
target: { adapter: 'plugins', ref: PLUGIN_PROFILE_MANIFEST_REF },
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
// 清单缺 / 读不到 / 形态不符:**不猜也不重建**,如实告警(非致命,其余分区照常导入)
|
|
743
|
+
items.push({
|
|
744
|
+
id,
|
|
745
|
+
kind: 'Warning',
|
|
746
|
+
adapter: 'plugins',
|
|
747
|
+
description: bundleView === null || bundleView.state === 'absent'
|
|
748
|
+
? msg('adapter.pluginHostBundleNoManifest', { name: p.name })
|
|
749
|
+
: msg('adapter.pluginHostBundleManifestUnreadable', { name: p.name, reason: bundleView.reason }),
|
|
750
|
+
severity: 'warning',
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
559
755
|
const tp = targetInstalled.find((t) => t.name === p.name);
|
|
560
756
|
// T1:该插件在备份内自带 tarball(本地源 link:/file:)→ 目标机无需原始路径即可安装。
|
|
561
757
|
const hasTarball = tarballOf(data, p.name) !== undefined;
|
|
@@ -697,10 +893,16 @@ export class PluginsAdapter {
|
|
|
697
893
|
// 拖垮已成功导入的其余配置——与 workspaces 的「目标不可达 → warning」同款语义。
|
|
698
894
|
if (item.id.startsWith('plugin:')) {
|
|
699
895
|
const name = item.id.replace(/^plugin:/, '');
|
|
896
|
+
const data = ctx.sections.get('plugins');
|
|
897
|
+
// issue #95:host 自带的层(源机只在 dsh.profile.bundles 声明、没有依赖声明)走「只声明」,
|
|
898
|
+
// 绝不进 npm 安装通道(按裸包名装 = 把 dist-tag latest 的副本塞进 profile)。
|
|
899
|
+
const entry = data?.plugins.find((p) => p.name === name);
|
|
900
|
+
if (entry !== undefined && isBundlesOnlyEntry(entry)) {
|
|
901
|
+
return await this.declareHostBundle(name, ctx);
|
|
902
|
+
}
|
|
700
903
|
try {
|
|
701
904
|
// 非 registry 来源(github:/file: 等)按来源 spec 安装,registry 包按裸包名装 npm 最新版
|
|
702
|
-
|
|
703
|
-
let spec = data?.plugins.find((p) => p.name === name)?.spec;
|
|
905
|
+
let spec = entry?.spec;
|
|
704
906
|
// T1:备份内自带 tarball → 解包到 homeDir 内的本地插件缓存目录,把 spec 重写为绝对路径。
|
|
705
907
|
// 这是「换机后本地插件不再丢失」的关键:原始绝对路径在目标机不存在,解包出的 tgz 一定存在。
|
|
706
908
|
const tarball = data !== undefined ? tarballOf(data, name) : undefined;
|
|
@@ -780,6 +982,11 @@ export class PluginsAdapter {
|
|
|
780
982
|
if (typeof name !== 'string' || name === '') {
|
|
781
983
|
issues.push({ path: `plugins[${i}].name`, message: msg('adapter.validate.requiredField', { subject: 'plugins[].name' }), severity: 'error' });
|
|
782
984
|
}
|
|
985
|
+
// issue #95:`bundlesOnly` 决定走不走 npm 安装通道,写错类型必须拦下(缺省合法)
|
|
986
|
+
const bundlesOnly = p.bundlesOnly;
|
|
987
|
+
if (bundlesOnly !== undefined && typeof bundlesOnly !== 'boolean') {
|
|
988
|
+
issues.push({ path: `plugins[${i}].bundlesOnly`, message: msg('adapter.validate.boolean', { subject: 'plugins[].bundlesOnly' }), severity: 'error' });
|
|
989
|
+
}
|
|
783
990
|
}
|
|
784
991
|
}
|
|
785
992
|
if (section.patch !== undefined && !Array.isArray(section.patch)) {
|