dsh-config-manager 0.1.76 → 0.1.77
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.js +92 -12
- package/lib/client.d.ts +104 -9
- package/lib/client.js +1796 -714
- package/lib/core/messages.d.ts +1 -0
- package/lib/core/messages.js +2 -0
- 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 +18 -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/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.ts +98 -13
- 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/messages.ts +2 -0
- 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 +19 -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/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
package/lib/ui/rollback-view.js
CHANGED
|
@@ -419,6 +419,54 @@ export function rollbackPreviewScope(files, removals) {
|
|
|
419
419
|
}
|
|
420
420
|
return { files: files.length, added, removed, removals: removals.length };
|
|
421
421
|
}
|
|
422
|
+
/** 从预览推出可勾选行(只读;绝不改任何判定) */
|
|
423
|
+
export function rollbackSelectionRows(preview) {
|
|
424
|
+
const files = preview?.workspace?.files;
|
|
425
|
+
if (!Array.isArray(files))
|
|
426
|
+
return [];
|
|
427
|
+
const out = [];
|
|
428
|
+
for (const file of files) {
|
|
429
|
+
if (file === null || typeof file !== 'object')
|
|
430
|
+
continue;
|
|
431
|
+
const path = typeof file.path === 'string' ? file.path : '';
|
|
432
|
+
if (path === '')
|
|
433
|
+
continue;
|
|
434
|
+
const row = { path, parts: splitRollbackPath(path), foreign: file.foreign === true };
|
|
435
|
+
if (row.foreign && typeof file.foreignSessionId === 'string' && file.foreignSessionId !== '') {
|
|
436
|
+
row.foreignSessionId = file.foreignSessionId;
|
|
437
|
+
}
|
|
438
|
+
out.push(row);
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 默认勾选 = **非 foreign** 的行(B 的自动隔离口径)。用户可在弹窗里逐个改回去。
|
|
444
|
+
*
|
|
445
|
+
* 为什么要在这里定而不在组件里:同一条默认值要在面板与聊天两个入口一致,
|
|
446
|
+
* 且「默认排除别的会话的改动」是产品决定,必须可测。
|
|
447
|
+
*/
|
|
448
|
+
export function rollbackDefaultSelection(rows) {
|
|
449
|
+
return rows.filter((row) => !row.foreign).map((row) => row.path);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 勾选集 → `onlyPaths`:
|
|
453
|
+
* · **全选**(含空清单)⇒ `null`(不传字段 = 走既有全量口径,老宿主也照样工作);
|
|
454
|
+
* · 真的少勾 ⇒ 把选中的清单原样传给宿主(空数组由调用方拦:宿主侧它是 400)。
|
|
455
|
+
*/
|
|
456
|
+
export function rollbackOnlyPaths(rows, selected) {
|
|
457
|
+
if (rows.length === 0)
|
|
458
|
+
return null;
|
|
459
|
+
const chosen = new Set(selected);
|
|
460
|
+
const picked = rows.filter((row) => chosen.has(row.path)).map((row) => row.path);
|
|
461
|
+
return picked.length === rows.length ? null : picked;
|
|
462
|
+
}
|
|
463
|
+
/** 该不该拦住提交:清单非空、却一个都没勾(宿主会把空数组当 400) */
|
|
464
|
+
export function rollbackSelectionBlocksSubmit(rows, selected) {
|
|
465
|
+
if (rows.length === 0)
|
|
466
|
+
return false;
|
|
467
|
+
const chosen = new Set(selected);
|
|
468
|
+
return !rows.some((row) => chosen.has(row.path));
|
|
469
|
+
}
|
|
422
470
|
/**
|
|
423
471
|
* 差异无法逐行展示时的字典键(本层**只回键**,文案在字典里)。
|
|
424
472
|
* 未知 / 缺失原因回落 null —— 调用方按「未知原因」出话,绝不回落成「没有差异」(那是另一件事)。
|
|
@@ -682,6 +730,13 @@ export function rollbackRewindRows(result) {
|
|
|
682
730
|
childSessionId,
|
|
683
731
|
detail: redactLines(segment.detail),
|
|
684
732
|
guidance: rollbackGuidanceRows(segment.guidance),
|
|
733
|
+
foreignSkipped: numberOrNull(segment.foreignSkipped) ?? 0,
|
|
734
|
+
selectionSkipped: numberOrNull(segment.selectionSkipped) ?? 0,
|
|
735
|
+
mergedCount: numberOrNull(segment.mergedCount) ?? 0,
|
|
736
|
+
conflictCount: numberOrNull(segment.conflictCount) ?? 0,
|
|
737
|
+
conflictPaths: redactLines(segment.conflictPaths),
|
|
738
|
+
unavailableCount: numberOrNull(segment.unavailableCount) ?? 0,
|
|
739
|
+
concurrentCount: numberOrNull(segment.concurrentCount) ?? 0,
|
|
685
740
|
};
|
|
686
741
|
});
|
|
687
742
|
}
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* 单元 id 由宿主侧 ConfigAdapter.listUnits() 产出,与导入侧 PlanItem.id 同一命名空间。
|
|
16
16
|
*/
|
|
17
17
|
import type { SectionId } from '../schema/types.ts';
|
|
18
|
-
import type { ImportPlan, PlanItem, PlanItemKind, Portability } from '../core/types.ts';
|
|
18
|
+
import type { ImportPlan, PlanItem, PlanItemKind, PluginAreaRef, Portability } from '../core/types.ts';
|
|
19
|
+
import type { ExportGroup } from './types.ts';
|
|
19
20
|
/** 一个可单独勾选的最小单元(来自 /export-preview 的 items)。 */
|
|
20
21
|
export interface SelectionUnit {
|
|
21
22
|
id: string;
|
|
@@ -63,6 +64,14 @@ export interface SelectionUnit {
|
|
|
63
64
|
* 而不是把 `session-<uuid>` 当标题(用户要求「始终用对话标题」)。文案由界面按当前语言解析。
|
|
64
65
|
*/
|
|
65
66
|
blank?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* 该插件**声明的**第三方数据区域(宿主下发;只有 `plugin:<包名>` 单元可能有值)。
|
|
69
|
+
*
|
|
70
|
+
* 用途:「按插件看」视图的树骨架 —— 静态知识(内置来源表 + 插件自述 dsh.dataDirs),
|
|
71
|
+
* **零数据目录 I/O**;体积与「磁盘上到底有没有」等该分区清单到货后再填。
|
|
72
|
+
* 纯展示字段,不参与勾选/导出契约(契约只认 id)。缺省 = 该插件没有已知关联(不猜)。
|
|
73
|
+
*/
|
|
74
|
+
relatedAreas?: readonly PluginAreaRef[];
|
|
66
75
|
}
|
|
67
76
|
/** 一个分区及其可勾选单元;units 为空 = 本分区不可细分(UI 只给整体开关)。 */
|
|
68
77
|
export interface SelectionSection {
|
|
@@ -91,6 +100,14 @@ export interface SelectionSection {
|
|
|
91
100
|
portability?: Portability;
|
|
92
101
|
/** 涉及秘密状态(如 credentialsStatus):安全提示用,绝不显示值。 */
|
|
93
102
|
sensitive?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* 分区所属的**导出分组**(`ui/types.ts` 的 EXPORT_GROUPS)。
|
|
105
|
+
*
|
|
106
|
+
* 用途:选择器在分组切换处插入标题行(General / 插件与扩展 / …)。渲染顺序**由调用方给的顺序
|
|
107
|
+
* 决定**(宿主目录本来已按分组排好,UI 不重排)。缺省 = 不渲染分组标题 ——
|
|
108
|
+
* 导入向导与市场通道的节点没有这个概念,行为逐像素不变。
|
|
109
|
+
*/
|
|
110
|
+
group?: ExportGroup;
|
|
94
111
|
}
|
|
95
112
|
/** 唯一的导出选择状态。 */
|
|
96
113
|
export interface Selection {
|
|
@@ -382,3 +399,125 @@ export interface ImportSelectionState {
|
|
|
382
399
|
export declare function effectiveImportSelection(plan: ImportPlan | null, zipPath: string | null, state: ImportSelectionState | null): Selection | null;
|
|
383
400
|
/** 生效的导入计划(选择裁剪后的子计划);plan 为空时返回 null。 */
|
|
384
401
|
export declare function effectiveImportPlan(plan: ImportPlan | null, zipPath: string | null, state: ImportSelectionState | null): ImportPlan | null;
|
|
402
|
+
/** 树上一个叶子的种类:插件清单条目 / 配置类数据区域 / 大文件区域 */
|
|
403
|
+
export type PluginLeafKind = 'plugin' | 'config' | 'assets';
|
|
404
|
+
export interface PluginTreeLeaf {
|
|
405
|
+
kind: PluginLeafKind;
|
|
406
|
+
section: SectionId;
|
|
407
|
+
/** 单元 id(present 时可勾;unread / absent 时只是占位,不可勾) */
|
|
408
|
+
unitId: string;
|
|
409
|
+
/** 宿主直出文本:插件包名 / 区域 home 相对路径 */
|
|
410
|
+
label: string;
|
|
411
|
+
/** 单元副标题(版本等;只有已读到清单时才有) */
|
|
412
|
+
detail?: string;
|
|
413
|
+
sizeBytes: number;
|
|
414
|
+
/** present=清单里确有该单元(可勾);unread=该分区清单还没读;absent=已读但没有这个单元 */
|
|
415
|
+
state: 'present' | 'unread' | 'absent';
|
|
416
|
+
}
|
|
417
|
+
export interface PluginTreeEntry {
|
|
418
|
+
/** 插件包名(= plugins 单元 id 的 `plugin:` 后缀) */
|
|
419
|
+
name: string;
|
|
420
|
+
/** 插件清单条目(该分区还没读到清单时为 undefined) */
|
|
421
|
+
main?: PluginTreeLeaf;
|
|
422
|
+
/** 该插件声明的数据区域(宿主静态表;可能为空数组) */
|
|
423
|
+
areas: PluginTreeLeaf[];
|
|
424
|
+
/** main + areas(渲染顺序:清单条目在前,区域按宿主声明顺序) */
|
|
425
|
+
leaves: PluginTreeLeaf[];
|
|
426
|
+
}
|
|
427
|
+
/** 「无法归属到具体插件」的桶:整块来自某个分区(不猜归属)。 */
|
|
428
|
+
export interface PluginTreeBucket {
|
|
429
|
+
section: SectionId;
|
|
430
|
+
/** 该分区的清单是否已读到(false = 还没读 / 读取失败,界面给「读取该分区」入口) */
|
|
431
|
+
loaded: boolean;
|
|
432
|
+
units: SelectionUnit[];
|
|
433
|
+
}
|
|
434
|
+
export interface PluginTree {
|
|
435
|
+
/** 插件清单是否已读到;未读到时 entries 为空(界面提示先读取插件分区) */
|
|
436
|
+
pluginsLoaded: boolean;
|
|
437
|
+
entries: PluginTreeEntry[];
|
|
438
|
+
/** 桶:无归属的分区单元(插件的补丁行 / pnpm-workspace.yaml;pluginFiles 整块) */
|
|
439
|
+
buckets: PluginTreeBucket[];
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* 合并进「按插件看」那棵树的分区(导出选择器里「插件与扩展」组的成员)。
|
|
443
|
+
*
|
|
444
|
+
* 这四项的归属键只有 npm 包名(plugin:<包名> ↔ 来源注册表的 packages);self(配置管理器设置)
|
|
445
|
+
* **不在**这里 —— 它不是插件,仍按普通分区行渲染。清单顺序仍由调用方给的节点顺序决定
|
|
446
|
+
* (宿主目录已按 EXPORT_GROUPS 排好,树只在第一个成员处渲染一次)。
|
|
447
|
+
*/
|
|
448
|
+
export declare const PLUGIN_TREE_SECTIONS: readonly SectionId[];
|
|
449
|
+
/** 该分区是否合并进「按插件看」的树(见 PLUGIN_TREE_SECTIONS)。 */
|
|
450
|
+
export declare function isPluginTreeSection(section: SectionId): boolean;
|
|
451
|
+
/** 把选择器分区模型投影成「按插件看」的树(纯函数)。 */
|
|
452
|
+
export declare function pluginTree(nodes: readonly SelectionSection[]): PluginTree;
|
|
453
|
+
/** 树上的搜索过滤:插件名命中 → 保留它全部叶子;否则只保留标签命中的叶子;桶按单元标签过滤。 */
|
|
454
|
+
export declare function filterPluginTree(tree: PluginTree, query: string): PluginTree;
|
|
455
|
+
/** 该插件节点下**真实可勾选**的叶子(unread / absent 不参与勾选)。 */
|
|
456
|
+
export declare function pluginEntryLeaves(entry: PluginTreeEntry): PluginTreeLeaf[];
|
|
457
|
+
/**
|
|
458
|
+
* 该插件名下**已知**的叶子(存在 + 还没读到清单的区域声明)。
|
|
459
|
+
*
|
|
460
|
+
* 计数与三态用它,**不用 pluginEntryLeaves(只含 present)**:否则「已选 n/m」会随清单到货跳动 ——
|
|
461
|
+
* 折叠时显示 1/1、展开(触发了读取)后变成 1/3,用户看到的是「数字自己变了」(2026-10-17 实测反馈)。
|
|
462
|
+
* absent(注册表说有、盘上确实没有)不计入:它永远不可勾,算进去只会让分母虚高。
|
|
463
|
+
*/
|
|
464
|
+
export declare function pluginEntryKnownLeaves(entry: PluginTreeEntry): PluginTreeLeaf[];
|
|
465
|
+
export declare function pluginEntryTotalCount(entry: PluginTreeEntry): number;
|
|
466
|
+
export declare function pluginEntrySelectedCount(sel: Selection, entry: PluginTreeEntry): number;
|
|
467
|
+
/**
|
|
468
|
+
* 插件节点三态(与分区/分组同一套口径,但分母用**已知**叶子)。
|
|
469
|
+
*
|
|
470
|
+
* 还有区域没读到(unread)时最多只能是 `partial`:那些单元此刻勾不上,
|
|
471
|
+
* 报「全选」会让用户以为已经全带上(真正补齐见 ContentPicker 的待补勾队列)。
|
|
472
|
+
*/
|
|
473
|
+
export declare function pluginEntryPickState(sel: Selection, entry: PluginTreeEntry): SectionPickState;
|
|
474
|
+
/**
|
|
475
|
+
* 插件节点级勾选:把它名下**存在的**单元一起勾 / 取消。
|
|
476
|
+
*
|
|
477
|
+
* 逐个走 toggleUnit:分区未勾时「勾一个单元 = 只勾这一个」的既有语义因此自动生效,
|
|
478
|
+
* 不会因为一键操作就把整个 pluginData / pluginAssets 分区静默全选。
|
|
479
|
+
*/
|
|
480
|
+
export declare function togglePluginEntry(sel: Selection, nodes: SelectionSection[], entry: PluginTreeEntry, checked: boolean): Selection;
|
|
481
|
+
/** 树上所有**真实可勾选**的叶子(各插件名下存在单元的合计;不含未归属 / 遗留数据桶)。 */
|
|
482
|
+
export declare function pluginTreeLeaves(tree: PluginTree): PluginTreeLeaf[];
|
|
483
|
+
export declare function pluginTreeLeafCount(tree: PluginTree): number;
|
|
484
|
+
/** 树上**已知**的叶子(存在 + 未读取;不含 absent)—— 一级区块的计数与三态用它,口径同插件节点。 */
|
|
485
|
+
export declare function pluginTreeKnownLeaves(tree: PluginTree): PluginTreeLeaf[];
|
|
486
|
+
export declare function pluginTreeTotalCount(tree: PluginTree): number;
|
|
487
|
+
export declare function pluginTreeSelectedCount(sel: Selection, tree: PluginTree): number;
|
|
488
|
+
/**
|
|
489
|
+
* 「插件」一级区块(把全部插件收在一个可折叠区块里)的三态。
|
|
490
|
+
*
|
|
491
|
+
* 只算**插件名下真实存在的叶子**:未归属 / 遗留数据是「无法归属的东西」,不参与这个勾选框 ——
|
|
492
|
+
* 否则「全选所有插件」会顺手把没人认领的散落配置一起带上。
|
|
493
|
+
*/
|
|
494
|
+
export declare function pluginTreePickState(sel: Selection, tree: PluginTree): SectionPickState;
|
|
495
|
+
/** 一级区块勾选:把所有插件名下存在的单元一起勾 / 取消(逐个走 toggleUnit;含未归属桶吗?不含)。 */
|
|
496
|
+
export declare function togglePluginTree(sel: Selection, nodes: SelectionSection[], tree: PluginTree, checked: boolean): Selection;
|
|
497
|
+
/**
|
|
498
|
+
* 展开「插件」一级区块前还需要读取哪些分区的清单(惰性)。
|
|
499
|
+
*
|
|
500
|
+
* **插件清单自己没读到也要报**(否则一级区块展开后只有一句「先读取插件分区」却没有入口)。
|
|
501
|
+
*/
|
|
502
|
+
export declare function pluginTreeUnreadSections(tree: PluginTree): SectionId[];
|
|
503
|
+
/** 一条待补勾请求(键 = 「插件」一级区块,或某个插件名)。 */
|
|
504
|
+
export interface PluginPendingCheck {
|
|
505
|
+
key: string;
|
|
506
|
+
kind: 'root' | 'entry';
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* 待补勾的出队判定(纯函数,node 可测)。
|
|
510
|
+
*
|
|
511
|
+
* 背景:清单是**惰性**读取的 —— 用户勾「插件」一级区块或某个插件时,它名下可能还有区域没读到
|
|
512
|
+
* (unread),那些单元此刻勾不上。若就此结束,用户会以为「全带上了」,实际包里少了数据目录/大文件。
|
|
513
|
+
* 调用方在点击时先勾已知的、把没到齐的键入队;清单到货(nodes 变化)后再调本函数补齐。
|
|
514
|
+
*
|
|
515
|
+
* 判据 = 该节点名下**已无 unread 叶子**(`pluginEntryUnreadSections` / `pluginsLoaded`):
|
|
516
|
+
* 到齐 → 出队并补齐;没到齐 → 留在队列里继续等(绝不猜成「已勾」)。
|
|
517
|
+
*/
|
|
518
|
+
export declare function applyPluginPendingChecks(sel: Selection, nodes: SelectionSection[], tree: PluginTree, pending: readonly PluginPendingCheck[]): {
|
|
519
|
+
selection: Selection;
|
|
520
|
+
remaining: PluginPendingCheck[];
|
|
521
|
+
};
|
|
522
|
+
/** 勾上该插件前还需要读取哪些分区的清单(展开/点击时触发惰性读取)。 */
|
|
523
|
+
export declare function pluginEntryUnreadSections(entry: PluginTreeEntry): SectionId[];
|
|
@@ -59,6 +59,8 @@ export function sectionsFromPreview(preview) {
|
|
|
59
59
|
...(u.projectKey !== undefined ? { projectKey: u.projectKey } : {}),
|
|
60
60
|
...(u.parentSessionId !== undefined ? { parentSessionId: u.parentSessionId } : {}),
|
|
61
61
|
...(u.blank === true ? { blank: true } : {}),
|
|
62
|
+
// 「按插件看」的树骨架(宿主下发的静态区域清单;缺省 = 该插件没有已知关联)
|
|
63
|
+
...(u.relatedAreas !== undefined && u.relatedAreas.length > 0 ? { relatedAreas: u.relatedAreas } : {}),
|
|
62
64
|
})),
|
|
63
65
|
}));
|
|
64
66
|
}
|
|
@@ -776,13 +778,243 @@ export function effectiveImportPlan(plan, zipPath, state) {
|
|
|
776
778
|
const selection = effectiveImportSelection(plan, zipPath, state);
|
|
777
779
|
return plan === null || selection === null ? null : buildSelectedPlan(plan, selection);
|
|
778
780
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
781
|
+
/**
|
|
782
|
+
* 合并进「按插件看」那棵树的分区(导出选择器里「插件与扩展」组的成员)。
|
|
783
|
+
*
|
|
784
|
+
* 这四项的归属键只有 npm 包名(plugin:<包名> ↔ 来源注册表的 packages);self(配置管理器设置)
|
|
785
|
+
* **不在**这里 —— 它不是插件,仍按普通分区行渲染。清单顺序仍由调用方给的节点顺序决定
|
|
786
|
+
* (宿主目录已按 EXPORT_GROUPS 排好,树只在第一个成员处渲染一次)。
|
|
787
|
+
*/
|
|
788
|
+
export const PLUGIN_TREE_SECTIONS = ['plugins', 'pluginFiles', 'pluginData', 'pluginAssets'];
|
|
789
|
+
/** 该分区是否合并进「按插件看」的树(见 PLUGIN_TREE_SECTIONS)。 */
|
|
790
|
+
export function isPluginTreeSection(section) {
|
|
791
|
+
return PLUGIN_TREE_SECTIONS.includes(section);
|
|
792
|
+
}
|
|
793
|
+
/** 插件单元 id 前缀(与 adapters/units.ts 的 `plugin:<包名>` 同一命名空间) */
|
|
794
|
+
const PLUGIN_UNIT_PREFIX = 'plugin:';
|
|
795
|
+
/** 把一条区域引用投影成叶子(按该分区清单是否到货决定 present / unread / absent)。 */
|
|
796
|
+
function areaLeaf(node, ref) {
|
|
797
|
+
const unitId = ref.section + ':' + ref.relPath;
|
|
798
|
+
const kind = ref.role === 'assets' ? 'assets' : 'config';
|
|
799
|
+
const unit = node?.units.find((u) => u.id === unitId);
|
|
800
|
+
if (unit === undefined) {
|
|
801
|
+
// 分区清单没读到 → unread(展开时才去读);读到了却没有这个单元 → absent(注册表说有、盘上没有)
|
|
802
|
+
return {
|
|
803
|
+
kind, section: ref.section, unitId, label: ref.relPath, sizeBytes: 0,
|
|
804
|
+
state: node === undefined || node.unitsUnread === true ? 'unread' : 'absent',
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
kind, section: ref.section, unitId, label: ref.relPath,
|
|
809
|
+
...(unit.detail !== undefined ? { detail: unit.detail } : {}),
|
|
810
|
+
sizeBytes: unit.sizeBytes,
|
|
811
|
+
state: 'present',
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/** 把选择器分区模型投影成「按插件看」的树(纯函数)。 */
|
|
815
|
+
export function pluginTree(nodes) {
|
|
816
|
+
const bySection = new Map();
|
|
817
|
+
for (const n of nodes)
|
|
818
|
+
bySection.set(n.section, n);
|
|
819
|
+
const pluginsNode = bySection.get('plugins');
|
|
820
|
+
const entries = [];
|
|
821
|
+
const extras = [];
|
|
822
|
+
for (const unit of pluginsNode?.units ?? []) {
|
|
823
|
+
if (!unit.id.startsWith(PLUGIN_UNIT_PREFIX)) {
|
|
824
|
+
// 补丁行 / pnpm-workspace.yaml:与插件无归属关系,进桶(在插件视图里也必须可见可选)
|
|
825
|
+
extras.push(unit);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const name = unit.id.slice(PLUGIN_UNIT_PREFIX.length);
|
|
829
|
+
const main = {
|
|
830
|
+
kind: 'plugin', section: 'plugins', unitId: unit.id, label: unit.label,
|
|
831
|
+
...(unit.detail !== undefined ? { detail: unit.detail } : {}),
|
|
832
|
+
sizeBytes: unit.sizeBytes,
|
|
833
|
+
state: 'present',
|
|
834
|
+
};
|
|
835
|
+
const areas = (unit.relatedAreas ?? []).map((ref) => areaLeaf(bySection.get(ref.section), ref));
|
|
836
|
+
entries.push({ name, main, areas, leaves: [main, ...areas] });
|
|
837
|
+
}
|
|
838
|
+
const buckets = [];
|
|
839
|
+
const pluginExtras = bySection.get('plugins');
|
|
840
|
+
if (pluginExtras !== undefined) {
|
|
841
|
+
buckets.push({ section: 'plugins', loaded: pluginExtras.unitsUnread !== true, units: extras });
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* 只留一个桶:pluginFiles。
|
|
845
|
+
*
|
|
846
|
+
* 它没有任何包名信息(白名单文件 + `plugin-config/<任意目录名>/`),按目录名猜包名 = 猜错就在树里
|
|
847
|
+
* 勾错东西 ⇒ 整块归「未归属」。**pluginData / pluginAssets 里没被插件声明认领的单元不再单列
|
|
848
|
+
* 「遗留数据」桶**(用户 2026-10-17 明确要求:不需要看到这些)—— 它们仍会随该分区整体勾选导出,
|
|
849
|
+
* 只是不在插件树里单开一行。
|
|
850
|
+
*/
|
|
851
|
+
for (const id of ['pluginFiles']) {
|
|
852
|
+
const node = bySection.get(id);
|
|
853
|
+
if (node === undefined)
|
|
854
|
+
continue;
|
|
855
|
+
buckets.push({ section: id, loaded: node.unitsUnread !== true, units: node.units });
|
|
856
|
+
}
|
|
857
|
+
return { pluginsLoaded: pluginsNode !== undefined && pluginsNode.unitsUnread !== true, entries, buckets };
|
|
858
|
+
}
|
|
859
|
+
/** 树上的搜索过滤:插件名命中 → 保留它全部叶子;否则只保留标签命中的叶子;桶按单元标签过滤。 */
|
|
860
|
+
export function filterPluginTree(tree, query) {
|
|
861
|
+
const q = query.trim().toLowerCase();
|
|
862
|
+
if (q === '')
|
|
863
|
+
return tree;
|
|
864
|
+
const hit = (text) => text.toLowerCase().includes(q);
|
|
865
|
+
const entries = tree.entries
|
|
866
|
+
.map((e) => (hit(e.name) ? e : { ...e, leaves: e.leaves.filter((l) => hit(l.label) || hit(l.detail ?? '')) }))
|
|
867
|
+
.filter((e) => hit(e.name) || e.leaves.length > 0);
|
|
868
|
+
const buckets = tree.buckets
|
|
869
|
+
.map((b) => ({ ...b, units: b.units.filter((u) => hit(u.label) || hit(u.detail ?? '')) }))
|
|
870
|
+
.filter((b) => b.units.length > 0);
|
|
871
|
+
return { ...tree, entries, buckets };
|
|
872
|
+
}
|
|
873
|
+
/** 该插件节点下**真实可勾选**的叶子(unread / absent 不参与勾选)。 */
|
|
874
|
+
export function pluginEntryLeaves(entry) {
|
|
875
|
+
return entry.leaves.filter((l) => l.state === 'present');
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* 该插件名下**已知**的叶子(存在 + 还没读到清单的区域声明)。
|
|
879
|
+
*
|
|
880
|
+
* 计数与三态用它,**不用 pluginEntryLeaves(只含 present)**:否则「已选 n/m」会随清单到货跳动 ——
|
|
881
|
+
* 折叠时显示 1/1、展开(触发了读取)后变成 1/3,用户看到的是「数字自己变了」(2026-10-17 实测反馈)。
|
|
882
|
+
* absent(注册表说有、盘上确实没有)不计入:它永远不可勾,算进去只会让分母虚高。
|
|
883
|
+
*/
|
|
884
|
+
export function pluginEntryKnownLeaves(entry) {
|
|
885
|
+
return entry.leaves.filter((l) => l.state !== 'absent');
|
|
886
|
+
}
|
|
887
|
+
export function pluginEntryTotalCount(entry) {
|
|
888
|
+
return pluginEntryKnownLeaves(entry).length;
|
|
889
|
+
}
|
|
890
|
+
export function pluginEntrySelectedCount(sel, entry) {
|
|
891
|
+
return pluginEntryLeaves(entry).reduce((n, l) => n + (unitChecked(sel, l.section, l.unitId) ? 1 : 0), 0);
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* 插件节点三态(与分区/分组同一套口径,但分母用**已知**叶子)。
|
|
895
|
+
*
|
|
896
|
+
* 还有区域没读到(unread)时最多只能是 `partial`:那些单元此刻勾不上,
|
|
897
|
+
* 报「全选」会让用户以为已经全带上(真正补齐见 ContentPicker 的待补勾队列)。
|
|
898
|
+
*/
|
|
899
|
+
export function pluginEntryPickState(sel, entry) {
|
|
900
|
+
const known = pluginEntryKnownLeaves(entry);
|
|
901
|
+
if (known.length === 0)
|
|
902
|
+
return 'none';
|
|
903
|
+
const picked = pluginEntrySelectedCount(sel, entry);
|
|
904
|
+
if (picked === 0)
|
|
905
|
+
return 'none';
|
|
906
|
+
return picked === known.length ? 'all' : 'partial';
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* 插件节点级勾选:把它名下**存在的**单元一起勾 / 取消。
|
|
910
|
+
*
|
|
911
|
+
* 逐个走 toggleUnit:分区未勾时「勾一个单元 = 只勾这一个」的既有语义因此自动生效,
|
|
912
|
+
* 不会因为一键操作就把整个 pluginData / pluginAssets 分区静默全选。
|
|
913
|
+
*/
|
|
914
|
+
export function togglePluginEntry(sel, nodes, entry, checked) {
|
|
915
|
+
let out = sel;
|
|
916
|
+
for (const leaf of pluginEntryLeaves(entry))
|
|
917
|
+
out = toggleUnit(out, nodes, leaf.unitId, checked);
|
|
918
|
+
return out;
|
|
919
|
+
}
|
|
920
|
+
/** 树上所有**真实可勾选**的叶子(各插件名下存在单元的合计;不含未归属 / 遗留数据桶)。 */
|
|
921
|
+
export function pluginTreeLeaves(tree) {
|
|
922
|
+
return tree.entries.flatMap((e) => pluginEntryLeaves(e));
|
|
923
|
+
}
|
|
924
|
+
export function pluginTreeLeafCount(tree) {
|
|
925
|
+
return pluginTreeLeaves(tree).length;
|
|
926
|
+
}
|
|
927
|
+
/** 树上**已知**的叶子(存在 + 未读取;不含 absent)—— 一级区块的计数与三态用它,口径同插件节点。 */
|
|
928
|
+
export function pluginTreeKnownLeaves(tree) {
|
|
929
|
+
return tree.entries.flatMap((e) => pluginEntryKnownLeaves(e));
|
|
930
|
+
}
|
|
931
|
+
export function pluginTreeTotalCount(tree) {
|
|
932
|
+
return pluginTreeKnownLeaves(tree).length;
|
|
933
|
+
}
|
|
934
|
+
export function pluginTreeSelectedCount(sel, tree) {
|
|
935
|
+
return pluginTreeLeaves(tree).reduce((n, l) => n + (unitChecked(sel, l.section, l.unitId) ? 1 : 0), 0);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* 「插件」一级区块(把全部插件收在一个可折叠区块里)的三态。
|
|
939
|
+
*
|
|
940
|
+
* 只算**插件名下真实存在的叶子**:未归属 / 遗留数据是「无法归属的东西」,不参与这个勾选框 ——
|
|
941
|
+
* 否则「全选所有插件」会顺手把没人认领的散落配置一起带上。
|
|
942
|
+
*/
|
|
943
|
+
export function pluginTreePickState(sel, tree) {
|
|
944
|
+
const known = pluginTreeKnownLeaves(tree);
|
|
945
|
+
if (known.length === 0)
|
|
946
|
+
return 'none';
|
|
947
|
+
const picked = pluginTreeSelectedCount(sel, tree);
|
|
948
|
+
if (picked === 0)
|
|
949
|
+
return 'none';
|
|
950
|
+
return picked === known.length ? 'all' : 'partial';
|
|
951
|
+
}
|
|
952
|
+
/** 一级区块勾选:把所有插件名下存在的单元一起勾 / 取消(逐个走 toggleUnit;含未归属桶吗?不含)。 */
|
|
953
|
+
export function togglePluginTree(sel, nodes, tree, checked) {
|
|
954
|
+
let out = sel;
|
|
955
|
+
for (const leaf of pluginTreeLeaves(tree))
|
|
956
|
+
out = toggleUnit(out, nodes, leaf.unitId, checked);
|
|
957
|
+
return out;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* 展开「插件」一级区块前还需要读取哪些分区的清单(惰性)。
|
|
961
|
+
*
|
|
962
|
+
* **插件清单自己没读到也要报**(否则一级区块展开后只有一句「先读取插件分区」却没有入口)。
|
|
963
|
+
*/
|
|
964
|
+
export function pluginTreeUnreadSections(tree) {
|
|
965
|
+
const out = tree.pluginsLoaded ? [] : ['plugins'];
|
|
966
|
+
for (const entry of tree.entries) {
|
|
967
|
+
for (const s of pluginEntryUnreadSections(entry))
|
|
968
|
+
if (!out.includes(s))
|
|
969
|
+
out.push(s);
|
|
970
|
+
}
|
|
971
|
+
// pluginFiles(未归属桶)没读到也要报:它的读取入口挂在「展开这一行」上
|
|
972
|
+
for (const bucket of tree.buckets) {
|
|
973
|
+
if (!bucket.loaded && !out.includes(bucket.section))
|
|
974
|
+
out.push(bucket.section);
|
|
975
|
+
}
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* 待补勾的出队判定(纯函数,node 可测)。
|
|
980
|
+
*
|
|
981
|
+
* 背景:清单是**惰性**读取的 —— 用户勾「插件」一级区块或某个插件时,它名下可能还有区域没读到
|
|
982
|
+
* (unread),那些单元此刻勾不上。若就此结束,用户会以为「全带上了」,实际包里少了数据目录/大文件。
|
|
983
|
+
* 调用方在点击时先勾已知的、把没到齐的键入队;清单到货(nodes 变化)后再调本函数补齐。
|
|
984
|
+
*
|
|
985
|
+
* 判据 = 该节点名下**已无 unread 叶子**(`pluginEntryUnreadSections` / `pluginsLoaded`):
|
|
986
|
+
* 到齐 → 出队并补齐;没到齐 → 留在队列里继续等(绝不猜成「已勾」)。
|
|
987
|
+
*/
|
|
988
|
+
export function applyPluginPendingChecks(sel, nodes, tree, pending) {
|
|
989
|
+
const remaining = [];
|
|
990
|
+
let out = sel;
|
|
991
|
+
for (const p of pending) {
|
|
992
|
+
if (p.kind === 'root') {
|
|
993
|
+
if (!tree.pluginsLoaded || tree.entries.some((e) => pluginEntryUnreadSections(e).length > 0)) {
|
|
994
|
+
remaining.push(p);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
out = togglePluginTree(out, nodes, tree, true);
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const entry = tree.entries.find((e) => e.name === p.key);
|
|
1001
|
+
if (entry === undefined)
|
|
1002
|
+
continue;
|
|
1003
|
+
if (pluginEntryUnreadSections(entry).length > 0) {
|
|
1004
|
+
remaining.push(p);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
out = togglePluginEntry(out, nodes, entry, true);
|
|
1008
|
+
}
|
|
1009
|
+
return { selection: out, remaining };
|
|
1010
|
+
}
|
|
1011
|
+
/** 勾上该插件前还需要读取哪些分区的清单(展开/点击时触发惰性读取)。 */
|
|
1012
|
+
export function pluginEntryUnreadSections(entry) {
|
|
1013
|
+
const out = [];
|
|
1014
|
+
for (const leaf of entry.leaves) {
|
|
1015
|
+
if (leaf.state === 'unread' && !out.includes(leaf.section))
|
|
1016
|
+
out.push(leaf.section);
|
|
1017
|
+
}
|
|
1018
|
+
return out;
|
|
1019
|
+
}
|
|
788
1020
|
//# sourceMappingURL=selection-model.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-config-manager",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.77",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Backup, restore, export, import, migrate and sync your DeepSeek Harness (DSH) configuration: settings, plugins, MCP servers, skills, agent presets and workspaces. One-click migration to another machine. DSH 配置一键备份、恢复、导出、导入、迁移与同步插件。",
|
|
6
6
|
"icon": "./icon.svg",
|
|
@@ -0,0 +1,60 @@
|
|
|
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.ts';
|
|
18
|
+
import type { PluginDataSource } from '../core/plugin-data-registry.ts';
|
|
19
|
+
import type { HostContext } from '../core/types.ts';
|
|
20
|
+
|
|
21
|
+
/** 单次扫描最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
|
|
22
|
+
export const MAX_DECLARED_PACKAGES = 300;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 解析声明时用的 profile 目录名。
|
|
26
|
+
*
|
|
27
|
+
* 与 DSH 的 profile 解析同口径:空 / undefined = \`web\`(与改造前的私有实现一致)。
|
|
28
|
+
*/
|
|
29
|
+
export function declaredProfileName(profile: string | undefined): string {
|
|
30
|
+
return profile !== undefined && profile !== '' ? profile : 'web';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 扫描本机已安装插件自述的数据目录声明(零副作用;读不到的一律跳过)。 */
|
|
34
|
+
export async function scanDeclaredDataSources(
|
|
35
|
+
ctx: HostContext,
|
|
36
|
+
maxPackages: number = MAX_DECLARED_PACKAGES,
|
|
37
|
+
): Promise<PluginDataSource[]> {
|
|
38
|
+
const base = 'profiles/' + declaredProfileName(ctx.profile);
|
|
39
|
+
const out: PluginDataSource[] = [];
|
|
40
|
+
let deps: string[] = [];
|
|
41
|
+
try {
|
|
42
|
+
const raw = await ctx.fs.readFile(base + '/package.json');
|
|
43
|
+
const parsed: unknown = JSON.parse(new TextDecoder().decode(raw));
|
|
44
|
+
const dep = (parsed as { dependencies?: unknown } | null)?.dependencies;
|
|
45
|
+
if (dep !== null && typeof dep === 'object') deps = Object.keys(dep).slice(0, maxPackages);
|
|
46
|
+
} catch {
|
|
47
|
+
deps = [];
|
|
48
|
+
}
|
|
49
|
+
for (const name of deps) {
|
|
50
|
+
try {
|
|
51
|
+
const raw = await ctx.fs.readFile(base + '/node_modules/' + name + '/package.json');
|
|
52
|
+
const manifest: unknown = JSON.parse(new TextDecoder().decode(raw));
|
|
53
|
+
const source = parseDeclaredDataDirs(name, manifest);
|
|
54
|
+
if (source !== null) out.push(source);
|
|
55
|
+
} catch {
|
|
56
|
+
// 未安装 / 无 manifest → 无声明,跳过
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
@@ -33,8 +33,8 @@ import type { RecursiveListing } from '../utils/recursive-walk.ts';
|
|
|
33
33
|
import { sectionMeta } from '../schema/section-registry.ts';
|
|
34
34
|
import {
|
|
35
35
|
BUILTIN_PLUGIN_DATA_SOURCES, areaOfPath, areasForSection, isExcludedRel, normalizeHomeRel,
|
|
36
|
-
parseDeclaredDataDirs,
|
|
37
36
|
} from '../core/plugin-data-registry.ts';
|
|
37
|
+
import { declaredProfileName, scanDeclaredDataSources } from './plugin-data-declared.ts';
|
|
38
38
|
import type { PluginDataArea, PluginDataSource, PluginDataSection } from '../core/plugin-data-registry.ts';
|
|
39
39
|
import { osHomeDirOf } from '../core/backup.ts';
|
|
40
40
|
import { FileCollectionAdapter } from './file-collection.ts';
|
|
@@ -50,9 +50,6 @@ export type PluginDataEntry = { source: PluginDataSource; area: PluginDataArea }
|
|
|
50
50
|
/** 声明来源缓存的 TTL(毫秒):一次导出/导入/执行里会多次解析,逐次读 15+ 个 package.json 是净开销 */
|
|
51
51
|
const DECLARED_TTL_MS = 5_000;
|
|
52
52
|
|
|
53
|
-
/** 单次解析里最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
|
|
54
|
-
const MAX_DECLARED_PACKAGES = 300;
|
|
55
|
-
|
|
56
53
|
export interface PluginDataAdapterOptions {
|
|
57
54
|
/** 内置来源(缺省 = BUILTIN_PLUGIN_DATA_SOURCES;测试注入假来源) */
|
|
58
55
|
readonly sources?: readonly PluginDataSource[];
|
|
@@ -111,18 +108,15 @@ export class PluginDataAdapter extends FileCollectionAdapter {
|
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
/**
|
|
114
|
-
* 已安装插件的自述声明(dsh.dataDirs
|
|
115
|
-
*
|
|
116
|
-
* 为什么读 profile 的 node_modules 而不是全局目录:DSH 的插件装在 \`profiles/<profile>/node_modules\`
|
|
117
|
-
* 下(pnpm 隔离布局),且同一个包在不同 profile 可以是不同版本 —— 声明必须取**本 profile 实际装的**那份。
|
|
111
|
+
* 已安装插件的自述声明(dsh.dataDirs)—— 本方法只做**缓存**,扫描实现见 plugin-data-declared.ts。
|
|
118
112
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
113
|
+
* 为什么缓存必须带 ctx 键(homeDir + profile):DSH 的插件装在 \`profiles/<profile>/node_modules\`
|
|
114
|
+
* 下(pnpm 隔离布局),而「这台机器装了某插件」不代表另一台装了 —— 缓存若按实例共享,
|
|
115
|
+
* 导入到「没装该插件」的目标机时会拿源机的声明去放行写入,白名单就变成「谁先导入谁说了算」。
|
|
121
116
|
*/
|
|
122
117
|
private async declaredSources(ctx: HostContext): Promise<PluginDataSource[]> {
|
|
123
118
|
const now = Date.now();
|
|
124
|
-
const
|
|
125
|
-
const memoKey = ctx.homeDir + '|' + profile;
|
|
119
|
+
const memoKey = ctx.homeDir + '|' + declaredProfileName(ctx.profile);
|
|
126
120
|
if (
|
|
127
121
|
this.declaredMemo !== undefined
|
|
128
122
|
&& this.declaredMemo.key === memoKey
|
|
@@ -130,27 +124,7 @@ export class PluginDataAdapter extends FileCollectionAdapter {
|
|
|
130
124
|
) {
|
|
131
125
|
return this.declaredMemo.sources;
|
|
132
126
|
}
|
|
133
|
-
const
|
|
134
|
-
const out: PluginDataSource[] = [];
|
|
135
|
-
let deps: string[] = [];
|
|
136
|
-
try {
|
|
137
|
-
const raw = await ctx.fs.readFile(base + '/package.json');
|
|
138
|
-
const parsed: unknown = JSON.parse(new TextDecoder().decode(raw));
|
|
139
|
-
const dep = (parsed as { dependencies?: unknown } | null)?.dependencies;
|
|
140
|
-
if (dep !== null && typeof dep === 'object') deps = Object.keys(dep).slice(0, MAX_DECLARED_PACKAGES);
|
|
141
|
-
} catch {
|
|
142
|
-
deps = [];
|
|
143
|
-
}
|
|
144
|
-
for (const name of deps) {
|
|
145
|
-
try {
|
|
146
|
-
const raw = await ctx.fs.readFile(base + '/node_modules/' + name + '/package.json');
|
|
147
|
-
const manifest: unknown = JSON.parse(new TextDecoder().decode(raw));
|
|
148
|
-
const source = parseDeclaredDataDirs(name, manifest);
|
|
149
|
-
if (source !== null) out.push(source);
|
|
150
|
-
} catch {
|
|
151
|
-
// 未安装 / 无 manifest → 无声明,跳过
|
|
152
|
-
}
|
|
153
|
-
}
|
|
127
|
+
const out = await scanDeclaredDataSources(ctx);
|
|
154
128
|
this.declaredMemo = { key: memoKey, at: now, sources: out };
|
|
155
129
|
return out;
|
|
156
130
|
}
|