@xiaoso/dsh-tool-plus 0.1.9 → 0.1.10-beta.2

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 (40) hide show
  1. package/README.en.md +12 -5
  2. package/README.md +11 -4
  3. package/THIRD_PARTY_NOTICES.md +36 -0
  4. package/lib/client.js +377 -906
  5. package/lib/index.mjs +885 -1087
  6. package/lib/{scrapers-mhvFrvjH.mjs → scrapers-DSArdJ6N.mjs} +1 -1
  7. package/lib/types/client/ToolPlusSection.d.ts +3 -3
  8. package/lib/types/client/forms.d.ts +6 -6
  9. package/lib/types/client/index.d.ts +29 -12
  10. package/lib/types/client/locales.d.ts +1 -1
  11. package/lib/types/client/preset-panel.d.ts +54 -42
  12. package/lib/types/config/settings.d.ts +202 -21
  13. package/lib/types/host/browser-probe-rpc.d.ts +2 -1
  14. package/lib/types/host/preset-host.d.ts +103 -0
  15. package/lib/types/host/preset-rpc.d.ts +37 -72
  16. package/lib/types/presets/compare.d.ts +14 -37
  17. package/lib/types/presets/conflicts.d.ts +59 -0
  18. package/lib/types/presets/delta.d.ts +23 -18
  19. package/lib/types/presets/plan.d.ts +55 -0
  20. package/lib/types/presets/rows.d.ts +84 -0
  21. package/lib/types/presets/status.d.ts +56 -47
  22. package/lib/types/tools/bash/background.d.ts +20 -5
  23. package/lib/types/tools/bash/streaming-output.d.ts +4 -2
  24. package/lib/types/tools/shared/browser-rpc-channel.d.ts +84 -42
  25. package/lib/types/web/client/labels.d.ts +1 -1
  26. package/lib/types/web/client/row-utils.d.ts +26 -1
  27. package/lib/{utils-eiUz9n8I.mjs → utils-DA8R1_Q8.mjs} +1 -1
  28. package/package.json +166 -151
  29. package/presets/tool-plus-ptc.patch.yml +164 -0
  30. package/presets/tool-plus-standard.patch.yml +158 -0
  31. package/lib/types/client/BashPlusCard.d.ts +0 -53
  32. package/lib/types/presets/install.d.ts +0 -44
  33. package/lib/types/presets/paths.d.ts +0 -41
  34. package/lib/types/presets/rewrite.d.ts +0 -86
  35. package/presets/install-presets.mjs +0 -143
  36. package/presets/package.json +0 -42
  37. package/presets/tool-plus-ptc/agent.cordis.yml +0 -284
  38. package/presets/tool-plus-ptc/preset.yml +0 -3
  39. package/presets/tool-plus-standard/agent.cordis.yml +0 -269
  40. package/presets/tool-plus-standard/preset.yml +0 -3
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * **Why this registers its own route instead of calling `connection.rpc.handle`.**
8
8
  * The documented helper is unusable from a plugin in a real web deployment on
9
- * dsh 0.1.5-rc.1: `HostConnectionService.rpc` builds its handle against
9
+ * dsh 0.1.5-rc.1 — and still on 0.1.7-rc.1, re-checked for this migration:
10
+ * `HostConnectionService.rpc` builds its handle against
10
11
  * `owner = this.ctx` (`rpc-host.ts: "Generic channel registry scoped to the
11
12
  * Context reading this service"`), and `register()` then evaluates
12
13
  * `owner.webServer.register(route)`. The client-connection row declares
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Host half of the preset surface: read the deployment's declared presets and
3
+ * apply the panel's three actions through the harness's own config editor.
4
+ *
5
+ * WHY WE DO NOT WRITE FILES OURSELVES: dsh 0.1.7 removed the directory preset
6
+ * mechanism and the `agent_preset` tool with it. The only supported programmatic
7
+ * write path is `ctx.configEditor.edit()`, and it is a better one than we had —
8
+ * it takes the profile lock, serializes with HMR, validates the next config
9
+ * through the Loader's own waterfall, writes atomically, rolls back on a failed
10
+ * reconcile, and refuses when a home patch or `--patch` overlay would defeat the
11
+ * edit. It also gives us "revert to the bundled declaration" for free: handing
12
+ * back the inherited config makes it delete the override's `config` key.
13
+ *
14
+ * The service is resolved per call, so a deployment that mounts the editor
15
+ * after this plugin still works instead of caching an early `undefined`.
16
+ *
17
+ * Everything here is a thin adapter; the decisions live in `src/presets/**`
18
+ * (pure, fs-free, unit-testable) and this module only resolves services and
19
+ * shapes results.
20
+ * @module @xiaoso/dsh-tool-plus/host/preset-host
21
+ */
22
+ import { type PresetActionResultValue, type PresetCompareValue, type PresetStatusListValue } from '../tools/shared/browser-rpc-channel.ts';
23
+ import { type PresetActionKind } from '../presets/plan.ts';
24
+ /** The context slice this module needs: Cordis service lookup only. */
25
+ export interface PresetHostCtx {
26
+ get(name: string): unknown;
27
+ }
28
+ /** Structural view of one addressable Loader entry. */
29
+ interface EntryLike {
30
+ options: {
31
+ id: string;
32
+ name?: string;
33
+ config?: unknown;
34
+ };
35
+ }
36
+ /** One `configEditor.configuration()` row. */
37
+ interface ConfigurationRowLike {
38
+ entry: EntryLike;
39
+ /** The bundled layer's config (our shipped declaration). */
40
+ inherited: Record<string, unknown>;
41
+ /** The profile layer's config; `{}` when the user never overrode the row. */
42
+ override: Record<string, unknown>;
43
+ }
44
+ /**
45
+ * The slice of `@deepseek-ai/dsh-config-editor` this plugin uses. Declared
46
+ * structurally so the plugin keeps zero build-time coupling to a package it
47
+ * never bundles.
48
+ */
49
+ export interface ConfigEditorLike {
50
+ /** Absolute path of the profile patch this editor writes. */
51
+ readonly documentPath: string;
52
+ entries(): readonly EntryLike[];
53
+ configuration(): readonly ConfigurationRowLike[];
54
+ edit(entry: EntryLike, change: (current: Record<string, unknown>, inherited: Record<string, unknown>) => Record<string, unknown>): Promise<void>;
55
+ }
56
+ /** One roster entry as the registry reports it. */
57
+ interface RosterRowLike {
58
+ readonly id?: unknown;
59
+ readonly name?: unknown;
60
+ readonly description?: unknown;
61
+ readonly broken?: unknown;
62
+ readonly isDefault?: unknown;
63
+ }
64
+ /**
65
+ * The slice of the preset registry this plugin uses.
66
+ *
67
+ * `remoteExportList()` is preferred: it is the roster the official client reads,
68
+ * and unlike `list()` it also carries `isDefault`. `list()` remains the fallback
69
+ * for a deployment whose registry predates the exported roster.
70
+ */
71
+ export interface AgentPresetsLike {
72
+ list?(): Promise<readonly RosterRowLike[]>;
73
+ remoteExportList?(): Promise<{
74
+ readonly presets?: readonly RosterRowLike[];
75
+ } | undefined>;
76
+ }
77
+ /**
78
+ * Answer `presets/status`.
79
+ * @param ctx - context used to resolve the config editor and the registry.
80
+ * @returns the panel's full status payload.
81
+ */
82
+ export declare function listPresetStatuses(ctx: PresetHostCtx): Promise<PresetStatusListValue>;
83
+ /**
84
+ * Answer `presets/apply` by planning the action and, when it is worth writing,
85
+ * delegating the write to the harness's own config editor.
86
+ * @param ctx - context used to resolve the services.
87
+ * @param presetId - the preset to act on.
88
+ * @param action - `upgrade`, `align` or `revert`.
89
+ * @param templateId - the align target (required for `align`).
90
+ * @returns the action result, never throwing.
91
+ */
92
+ export declare function applyPresetAction(ctx: PresetHostCtx, presetId: string, action: PresetActionKind, templateId?: string): Promise<PresetActionResultValue>;
93
+ /**
94
+ * Answer `presets/compare`: one preset's effective plugins against one of our
95
+ * declared templates. Never throws; an unresolvable side reports `unreadable`
96
+ * (that is a state the panel renders as a red dot, not a failed call).
97
+ * @param ctx - context used to resolve the services.
98
+ * @param presetId - the preset being inspected.
99
+ * @param templateId - the template to compare it against.
100
+ * @returns the comparison.
101
+ */
102
+ export declare function comparePresetAgainstTemplate(ctx: PresetHostCtx, presetId: string, templateId: string): Promise<PresetCompareValue>;
103
+ export {};
@@ -1,102 +1,67 @@
1
1
  /**
2
2
  * Host half of the preset endpoints: answers `presets/status` (roster +
3
- * conflicts + template diff) and `presets/apply` (disable conflicting rows /
4
- * reset from our template) for the settings panel's 「全局」tab.
3
+ * conflicts + template diff), `presets/apply` (upgrade / align / revert) and
4
+ * `presets/compare` for the settings panel's 「全局」tab.
5
5
  *
6
- * These endpoints live on the **same** `/tool-plus` channel registration as
7
- * `browser/detect` and `rmSafe/status` (see `browser-probe-rpc.ts`): a
8
- * Connection channel has exactly one handler, so a second
9
- * `connection.rpc.handle('/tool-plus', …)` never takes effect. That is why this
10
- * module exports an endpoint handler instead of installing a channel of its
11
- * own.
6
+ * These endpoints live on the **same** `/tool-plus` route as `browser/detect`
7
+ * and `rmSafe/status` (see `browser-probe-rpc.ts`). That route is a plain
8
+ * `webServer.register({ kind: 'prefix', path: '/tool-plus' })` registration, NOT
9
+ * `connection.rpc.handle`: in this deployment the Connection handler resolves
10
+ * its owner through `this.ctx` without the `webServer` injection, so a
11
+ * `rpc.handle` registration never takes effect. The Connection service is still
12
+ * used for its loopback trust fence (`connection.requestRejection`).
12
13
  *
13
- * The preset analysis itself lives in `src/presets/**` and is **injected**, so
14
- * this module stays a thin wire adapter and its helpers are testable without a
15
- * Connection service or a filesystem.
16
- *
17
- * Trust policy is `loopback` (the channel's own policy): these endpoints read
18
- * and write files under the user's DSH home, so they must never be reachable
19
- * from a non-loopback host.
14
+ * This module is a wire adapter only: payload validation plus dispatch into an
15
+ * injected {@link PresetRpcDeps}. The service lookup, the configuration reads
16
+ * and the writes live in `../host/preset-host.ts`; the decision logic lives in
17
+ * `src/presets/**`. Trust policy is `loopback` — these endpoints read the user's
18
+ * profile configuration and the write path edits it, so they must never be
19
+ * reachable from a non-loopback host.
20
20
  * @module @xiaoso/dsh-tool-plus/host/preset-rpc
21
21
  */
22
22
  import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection';
23
- import { type PresetActionPayload, type PresetComparePayloadValue } from '../tools/shared/browser-rpc-channel.ts';
24
- import type { PresetInstallResult } from '../presets/install.ts';
25
- import type { PresetComparison, PresetTemplate } from '../presets/compare.ts';
26
- import type { PresetRosterEntry, PresetStatus } from '../presets/status.ts';
27
- /** Payload shape of a `presets/compare` call (shared contract). */
28
- type PresetComparePayload = PresetComparePayloadValue;
23
+ import { type PresetActionPayload, type PresetActionResultValue, type PresetActionValue, type PresetComparePayloadValue, type PresetCompareValue, type PresetStatusListValue } from '../tools/shared/browser-rpc-channel.ts';
29
24
  /** Result shape of one Connection RPC call (official pattern in the gateway). */
30
25
  type ConnectionRpcResult = Awaited<ReturnType<ConnectionRpcHandler>>;
26
+ /** The context slice this handler needs: pass-through to the injected deps. */
27
+ export interface PresetEndpointCtx {
28
+ /** Cordis service lookup, forwarded to the host implementation. */
29
+ get(name: string): unknown;
30
+ }
31
31
  /**
32
- * Structural view of one official `ctx.agentPresets.list()` entry. Declared
33
- * here instead of importing `@deepseek-ai/dsh-agent-presets` so the plugin
34
- * keeps zero build-time coupling to a package it never bundles — the service
35
- * is reached through the context, and every field is validated on the way in.
32
+ * Everything the endpoints need from the host plane. Injected so this module
33
+ * stays a thin wire adapter whose parsing helpers are testable without a
34
+ * Connection service, a Loader, or a profile.
36
35
  */
37
- interface RosterPresetLike {
38
- readonly id?: unknown;
39
- readonly trust?: unknown;
40
- readonly path?: unknown;
41
- readonly name?: unknown;
42
- readonly description?: unknown;
43
- readonly broken?: unknown;
44
- }
45
- /** The only slice of the official agent-presets service this plugin uses. */
46
- export interface AgentPresetsServiceLike {
47
- list(): Promise<readonly RosterPresetLike[]>;
48
- }
49
- /** Install-time dependencies: the preset analysis owned by the host half. */
50
36
  export interface PresetRpcDeps {
51
- /** Build one status row per roster entry (reads the user root, never writes). */
52
- listStatuses: (roster: readonly PresetRosterEntry[]) => PresetStatus[];
53
- /** Apply one action to one preset (the only endpoint that writes). */
54
- applyAction: (id: string, action: PresetActionPayload['action'], templateId?: string) => PresetInstallResult;
55
- /** Templates this package ships (the panel's "compare with" options). */
56
- listTemplates: () => PresetTemplate[];
57
- /**
58
- * Read-only comparison between one roster preset and one of our templates.
59
- * Returns `status: 'unreadable'` instead of throwing when a file cannot be
60
- * read — that is a state the panel renders (red dot), not a failed call.
61
- */
62
- comparePreset: (preset: PresetRosterEntry, templateId: string) => PresetComparison;
37
+ /** Read every declared preset with its conflicts and template diff. */
38
+ listPresets: (ctx: PresetEndpointCtx) => Promise<PresetStatusListValue>;
39
+ /** Apply one action to one preset. The only path that writes. */
40
+ applyAction: (ctx: PresetEndpointCtx, id: string, action: PresetActionValue, templateId?: string) => Promise<PresetActionResultValue>;
41
+ /** Read-only comparison between one preset and one of our declarations. */
42
+ comparePreset: (ctx: PresetEndpointCtx, presetId: string, templateId: string) => Promise<PresetCompareValue>;
63
43
  }
64
44
  /**
65
45
  * Validate a `presets/compare` payload: two non-empty ids, nothing else.
66
46
  * @param payload - the raw RPC payload.
67
47
  * @returns the narrowed payload, or undefined when invalid.
68
48
  */
69
- export declare function parsePresetComparePayload(payload: unknown): PresetComparePayload | undefined;
49
+ export declare function parsePresetComparePayload(payload: unknown): PresetComparePayloadValue | undefined;
70
50
  /**
71
51
  * Validate a `presets/apply` payload. Anything that is not exactly
72
- * `{ id: non-empty string, action: 'upgrade' | 'reset' }` is rejected, so the
73
- * write endpoint never receives a half-formed request.
52
+ * `{ id: non-empty string, action: 'upgrade' | 'align' | 'revert', templateId?: non-empty string }`
53
+ * is rejected, so the write endpoint never receives a half-formed request.
74
54
  * @param payload - the raw RPC payload.
75
55
  * @returns the narrowed payload, or undefined when invalid.
76
56
  */
77
57
  export declare function parsePresetActionPayload(payload: unknown): PresetActionPayload | undefined;
78
58
  /**
79
- * Narrow one roster entry coming from the official service. Unknown trust is
80
- * treated as `user` (the conservative side: it is a writable preset only when
81
- * the caller proves otherwise — the caller matches ids against our templates).
82
- * @param preset - raw entry from `ctx.agentPresets.list()`.
83
- * @returns the entry in our own shape.
84
- */
85
- export declare function toRosterEntry(preset: RosterPresetLike): PresetRosterEntry;
86
- /** The context slice this handler needs: service lookup only. */
87
- export interface PresetEndpointCtx {
88
- /** Cordis service lookup; `agentPresets` is mounted by the harness. */
89
- get(name: string): unknown;
90
- }
91
- /**
92
- * Answer one preset endpoint on the shared `/tool-plus` channel.
59
+ * Answer one preset endpoint on the shared `/tool-plus` route.
93
60
  *
94
61
  * Returns `undefined` for an endpoint this module does not own, so the caller
95
- * (the single channel handler) can keep serving its own families. The service
96
- * is resolved per call: a deployment that mounts `agent-presets` after this
97
- * plugin still gets a working endpoint instead of a stale `undefined`.
98
- * @param ctx - context used to resolve the `agentPresets` service.
99
- * @param deps - injected preset analysis.
62
+ * (the single route handler) can keep serving its own families.
63
+ * @param ctx - context forwarded to the injected host implementation.
64
+ * @param deps - injected host implementation.
100
65
  * @param endpoint - the requested endpoint name.
101
66
  * @param payload - the raw RPC payload.
102
67
  * @returns the RPC result, or undefined when the endpoint is not ours.
@@ -1,28 +1,22 @@
1
1
  /**
2
- * 预设比较:把"某份预设"和"我们随包的某个模板"放在一起看。
2
+ * 预设比较:把"某份 preset 生效的 plugins"和"某个随包声明的 plugins"放在一起看。
3
3
  *
4
- * 面板现在是两块信息、两个状态点:
5
- * ①**工具行**:这份预设里我们那 4 个冲突行都禁用了吗(=`conflicts`);
6
- * ②**完整比较**:内容上和我们选的模板差多少(=`items`,弹窗里逐条展示)。
7
- * 这是只读操作,绝不写盘。比较一律**不跳过**冲突行的 `disabled` —— 第二个点
8
- * 要的是"完整的比较",工具开关的不同也必须出现在差异清单里,否则会出现
9
- * "说内容一致、却说工具行没配好"这种自相矛盾的读数。
4
+ * 面板是两块信息、两个状态点:
5
+ * ①**工具行**:这份 preset 里那 4 个冲突行都禁用了吗(=`conflicts`);
6
+ * ②**完整比较**:内容上和选中的模板差多少(=`items`,弹窗里逐条展示)。
7
+ * 只读,绝不写盘。比较一律**不跳过**冲突行的 `disabled` —— 第二个点要的是
8
+ * "完整的比较",工具开关的不同也必须出现在差异清单里,否则会出现"说内容一致、
9
+ * 却说工具行没配好"这种自相矛盾的读数。
10
10
  * @module @xiaoso/dsh-tool-plus/presets/compare
11
11
  */
12
- import { type PresetDeps } from './paths.ts';
13
12
  import { type PresetDeltaItem } from './delta.ts';
14
- /** 面板"对比模板"下拉的一项:模板 id(可读性靠 name)。 */
15
- export interface PresetTemplate {
16
- id: string;
17
- name?: string;
18
- }
19
13
  /** 一次比较的结果(面板两个状态点 + 弹窗的全部数据)。 */
20
14
  export interface PresetComparison {
21
- /** 任一侧读不到或形状不可识别 → `unreadable`,此时 `items` 为空。 */
15
+ /** 任一侧不是合法行列表 → `unreadable`,此时 `items` 为空。 */
22
16
  status: 'ok' | 'unreadable';
23
- /** 这份预设里还没禁用的冲突行(空 = 我们的工具已生效)。 */
17
+ /** 这份 preset 里还没禁用的冲突行(空 = 本插件的工具已生效)。 */
24
18
  conflicts: string[];
25
- /** 内容层面逐项一致(注释与排版差异不算,逐字节相同也不额外判断)。 */
19
+ /** 内容层面逐项一致(顺序不同不算差异,逐值比较)。 */
26
20
  identical: boolean;
27
21
  yoursCount: number;
28
22
  behindCount: number;
@@ -30,26 +24,9 @@ export interface PresetComparison {
30
24
  items: PresetDeltaItem[];
31
25
  }
32
26
  /**
33
- * 列出包里带模板的预设 id(面板"对比模板"下拉的选项)。只读目录,不抛。
34
- * @param deps - 路径注入(测试用)。
35
- * @returns 按 id 排序的模板清单(读不到目录时为空数组)。
36
- */
37
- export declare function listTemplates(deps?: PresetDeps): PresetTemplate[];
38
- /** 读一份模板的组合文件;不存在返回 undefined。 */
39
- export declare function readTemplateComposition(templateId: string, deps?: PresetDeps): string | undefined;
40
- /**
41
- * 比较两份组合文件文本(纯函数,只读)。
42
- * @param presetText - 所选预设的原文。
43
- * @param templateText - 所选模板的原文。
27
+ * 比较两份 `config.plugins`(纯函数,只读)。
28
+ * @param presetPlugins - 所选 preset 生效的 plugins。
29
+ * @param templatePlugins - 所选模板的 plugins(随包声明)。
44
30
  * @returns 两个状态点与弹窗需要的全部数据。
45
31
  */
46
- export declare function compareCompositions(presetText: string, templateText: string): PresetComparison;
47
- /**
48
- * 读盘版比较:预设一侧用 roster 给的组合文件路径,模板一侧按 id 去包里找。
49
- * 任一侧读不到都返回 `unreadable` —— 那是面板要渲染的红点,不是调用失败。
50
- * @param presetPath - 所选预设的组合文件绝对路径(来自 roster)。
51
- * @param templateId - 所选模板的 id。
52
- * @param deps - 路径注入(测试用)。
53
- * @returns 比较结果。
54
- */
55
- export declare function comparePresetFile(presetPath: string, templateId: string, deps?: PresetDeps): PresetComparison;
32
+ export declare function comparePlugins(presetPlugins: unknown, templatePlugins: unknown): PresetComparison;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 冲突不变式:一个 preset「接上了本插件的工具」当且仅当官方那 4 个工具行
3
+ * 处于**不存在**或 **`disabled: true`** 状态。
4
+ *
5
+ * 原因(这套设计的地基,别丢):`@xiaoso/dsh-tool-plus` 作为包在**宿主面**
6
+ * 加载时,它自己的 profile 补丁已经禁用了这 4 行并插入**一行全局** `tool-plus`;
7
+ * 注册表按 scope 分层、就近优先,所以宿主面那一份已经服务所有会话。若某个
8
+ * preset 又在 **agent 面**挂一份同名工具行,就会用**第二个 `apply()` 影子实例**
9
+ * 盖掉健康的宿主实例,而那个实例的 settings 注入永不触发(cfg 退化成
10
+ * replace-mode 读取 —— 2026-08-25 hashline 事故的根因)。
11
+ *
12
+ * 与旧文本实现的唯一区别是**载体**:以前逐行读 `agent.cordis.yml` 的文本,
13
+ * 现在直接看宿主交给我们的行对象。判定规则一字不改:
14
+ * - 只认**顶层**行;group 里嵌套的同名行不碰;
15
+ * - `disabled` 严格等于布尔 `true` 才算已处理 —— 缺席、`false`、字符串、
16
+ * `!!js` 表达式统统算冲突(`!!js` 在挂载时才求值,我们要的是无条件关闭,
17
+ * 所以和旧实现一样整值翻成 `true`);
18
+ * - 输出恒为新数组,输入不变;结果与输入逐值相同时 `changes` 为空(调用方以
19
+ * 此判定"不写盘")。
20
+ * @module @xiaoso/dsh-tool-plus/presets/conflicts
21
+ */
22
+ import { type PresetRow } from './rows.ts';
23
+ /** 官方 standard / ptc / cordis 会挂的 4 个工具行 id。 */
24
+ export declare const PRESET_CONFLICT_IDS: readonly string[];
25
+ /** 一次改写实际发生的一处改动。 */
26
+ export interface PresetChange {
27
+ /** 行 id(顶层 `- id: <id>`)。 */
28
+ id: string;
29
+ /** `disabled` = 该行原本没有 `disabled` 键,现在补上;`flipped` = 既有值被翻成 `true`。 */
30
+ action: 'disabled' | 'flipped';
31
+ }
32
+ /** 只读判定:给设置页状态用。 */
33
+ export interface PresetAnalysis {
34
+ /** 仍挂着冲突行的 id(缺 `disabled` 或值不是布尔 `true`),按 conflictIds 顺序。 */
35
+ conflicts: string[];
36
+ /** 无冲突且形状可识别。 */
37
+ clean: boolean;
38
+ /** `unrecognized` = 不是合法的顶层行列表,一律不处理也不改写。 */
39
+ shape: 'ok' | 'unrecognized';
40
+ }
41
+ /**
42
+ * 只读分析一份 `plugins` 值。
43
+ * @param plugins - 声明的子插件列表(来自宿主配置,形状未知)。
44
+ * @param conflictIds - 冲突行清单,默认 {@link PRESET_CONFLICT_IDS}。
45
+ * @returns 冲突清单、是否干净、形状是否可识别。
46
+ */
47
+ export declare function analyzePlugins(plugins: unknown, conflictIds?: readonly string[]): PresetAnalysis;
48
+ /** 改写结果:新列表 + 实际改动清单 + 改写后仍未解决的冲突(理论上为空)。 */
49
+ export interface PresetConflictRewrite {
50
+ plugins: PresetRow[];
51
+ changes: PresetChange[];
52
+ }
53
+ /**
54
+ * 把那 4 个仍挂着的冲突行翻成 `disabled: true`(不改别的行、不删行)。
55
+ * @param plugins - 合法顶层行列表。
56
+ * @param conflictIds - 冲突行清单,默认 {@link PRESET_CONFLICT_IDS}。
57
+ * @returns 新列表与实际改动清单(无改动时 `changes` 为空且列表为原引用)。
58
+ */
59
+ export declare function disableConflicts(plugins: readonly PresetRow[], conflictIds?: readonly string[]): PresetConflictRewrite;
@@ -1,16 +1,18 @@
1
1
  /**
2
- * 预设差异清单:只读地说清"用户这份和随包模板差在哪"。
2
+ * 差异清单:只读地说清"这份 preset 与另一份(通常是随包声明)差在哪"。
3
3
  *
4
4
  * 为什么需要它:`templateDiffers: boolean` 只能说"不一样",而面板上紧挨着的
5
- * 是"工具冲突行都已禁用、无需调整"。两句都对,但读起来打架 —— 前者说的是
6
- * **你改过内容**,后者说的是**我们自己那 4 个冲突行**。把差异逐条列出来,
7
- * 面板才不需要用户自己猜"到底哪里不同、更新会不会动它"。
5
+ * 是"冲突行都已禁用、无需调整"。两句都对,读起来却打架 —— 前者说的是
6
+ * **内容被改过**,后者说的是**我们自己那 4 个冲突行**。把差异逐条列出来,
7
+ * 用户才不用猜"到底哪里不同、对齐会不会动它"。
8
8
  *
9
- * 契约:①比**展开到叶子的点号路径**(`config.thresholdRatio`,列表项带 `[i]`)——
10
- * 真实漂移常藏在嵌套键里,只比顶层键会得出"没有差异"的错误结论;②冲突行的
11
- * `disabled` 由 `conflicts` 负责,不在差异里重复报;③形状不可识别 → 空清单,
12
- * 绝不猜;④顺序=用户这份的行序在前,模板独有的行补在后;⑤`total` 是全量,
13
- * `items` 可被 `limit` 截断(面板据此说"等 N 项")。
9
+ * 契约(自旧文本实现原样继承,语义未变):
10
+ * - 比**展开到叶子的点号路径**(`config.thresholdRatio`、`config.models[0].id`)——
11
+ * 真实漂移常藏在嵌套键里,只比顶层键会得出"没有差异"的错误结论;
12
+ * - 冲突行的 `disabled` 由 `conflicts` 负责,不在差异里重复报;
13
+ * - 形状不可识别 → 空清单,绝不猜;
14
+ * - 顺序 = 你这份的行序在前(含 group 内子行),对方独有的行补在后;
15
+ * - `total` 是全量,`items` 可被 `limit` 截断(面板据此说"等 N 项")。
14
16
  * @module @xiaoso/dsh-tool-plus/presets/delta
15
17
  */
16
18
  /** 一条差异:整行只在一侧、某个叶子只在一侧、或同一个叶子两边值不同。 */
@@ -18,13 +20,16 @@ export type PresetDeltaKind = 'changed' | 'only-yours' | 'only-template' | 'row-
18
20
  /** 差异清单的一项。`path` 为空串表示整行层面的差异。 */
19
21
  export interface PresetDeltaItem {
20
22
  kind: PresetDeltaKind;
21
- /** 顶层行的 id。 */
23
+ /**
24
+ * 行路径:顶层行的 id(`tool-web`),或 group 内子行(`delegation/tool-ralph`)。
25
+ * 用路径而不是裸 id,是因为嵌套行重名时裸 id 会指错行。
26
+ */
22
27
  row: string;
23
28
  /** 行内点号路径(如 `config.thresholdRatio`);整行差异时为空串。 */
24
29
  path: string;
25
30
  /** 你这份里的值(`only-template` / `row-only-template` 时不带)。 */
26
31
  yours?: string;
27
- /** 模板里的值(`only-yours` / `row-only-yours` 时不带)。 */
32
+ /** 对方的同名值(`only-yours` / `row-only-yours` 时不带)。 */
28
33
  template?: string;
29
34
  }
30
35
  /** 差异清单:`items` 已按 `limit` 截断,`total` 是全量条数。 */
@@ -32,21 +37,21 @@ export interface PresetDelta {
32
37
  items: PresetDeltaItem[];
33
38
  total: number;
34
39
  /**
35
- * 全量口径的两个分组:`yoursCount` = 你自己加/改的(更新不覆盖,重置会覆盖),
36
- * `behindCount` = 模板有而你没有的(模板已更新,重置可对齐)。
40
+ * 全量口径的两个分组:`yoursCount` = 你自己加/改的(对齐会覆盖它),
41
+ * `behindCount` = 对方有而你没有的(对方已更新,对齐可补上)。
37
42
  * 分组计数必须按**全量**算:`items` 被 `limit` 截断后数它会把"等 N 项"说错。
38
43
  */
39
44
  yoursCount: number;
40
45
  behindCount: number;
41
46
  }
42
47
  /**
43
- * 比较两份组合文件,列出差异(只读,不改任何字节)。
44
- * @param userText - 用户这份的 `agent.cordis.yml` 原文。
45
- * @param templateText - 随包模板的同一文件原文。
48
+ * 比较两份 `config.plugins`,列出差异(只读,不改任何值)。
49
+ * @param yours - 你这一侧的 plugins(形状未知)。
50
+ * @param theirs - 对方的 plugins(形状未知)。
46
51
  * @param options - `conflictIds`(默认 {@link PRESET_CONFLICT_IDS})与 `limit`(默认 6)。
47
- * @returns 差异清单;两侧任一不可识别时为空清单。
52
+ * @returns 差异清单;两侧任一形状不可识别时为空清单。
48
53
  */
49
- export declare function presetDelta(userText: string, templateText: string, options?: {
54
+ export declare function presetDelta(yours: unknown, theirs: unknown, options?: {
50
55
  conflictIds?: readonly string[];
51
56
  limit?: number;
52
57
  }): PresetDelta;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 动作判定:三个面板动作各自"要不要动手、动手算不算真改动"。**纯函数**。
3
+ *
4
+ * 这一层存在的理由:真正落盘的是宿主自己的 `ctx.configEditor.edit()`,它**每次
5
+ * 调用都会重写 profile 补丁文件**(哪怕内容一个字没变)。所以"是否值得写"必须
6
+ * 在调用它**之前**判定,否则面板每点一次都会白改一次 mtime、白触发一轮 HMR 重载。
7
+ * 判定所需的事实全部来自 {@link PresetFacts},因此这里可以纯函数测试,不需要
8
+ * 真的 profile、也不需要 cordis。
9
+ *
10
+ * 三个动作的语义(自旧目录机制映射而来,见 CHANGELOG):
11
+ * - `upgrade`(最小更新):只把仍挂着的官方冲突行翻成 `disabled: true`。不改别的行,
12
+ * 不动显示元数据 —— 幂等,已干净时一个字节都不写。
13
+ * - `align`(对齐模板):把生效的 plugins 整份换成所选模板声明的 plugins。
14
+ * *对齐到自己的随包声明* = 撤销覆盖,等价于 `revert`,这里直接归约过去。
15
+ * - `revert`(恢复随包):让宿主删掉 profile 补丁里那一行的 `config`,
16
+ * 于是这一行回落到随包声明。没有覆盖时是 no-op。
17
+ * @module @xiaoso/dsh-tool-plus/presets/plan
18
+ */
19
+ import type { PresetChange } from './conflicts.ts';
20
+ /** 面板可以请求的三个动作。 */
21
+ export type PresetActionKind = 'upgrade' | 'align' | 'revert';
22
+ /** 判定所需的全部事实。 */
23
+ export interface PresetPlanInput {
24
+ action: PresetActionKind;
25
+ /** 这个 preset 是不是本插件声明的(只有 ours 允许 align / revert)。 */
26
+ ours: boolean;
27
+ /** 用户是否在 profile 层写过覆盖。 */
28
+ customized: boolean;
29
+ /** 生效的完整 config(宿主的 `entry.options.config`);取不到时 undefined。 */
30
+ effective: Record<string, unknown> | undefined;
31
+ /** 随包声明那一层的完整 config(宿主的 `inherited`)。 */
32
+ inherited: Record<string, unknown> | undefined;
33
+ /** `align` 的目标模板 plugins。 */
34
+ templatePlugins?: unknown;
35
+ /** `align` 请求的模板 id(仅用于文案与校验)。 */
36
+ templateId?: string;
37
+ }
38
+ /** 判定结果。 */
39
+ export type PresetPlan = {
40
+ kind: 'noop';
41
+ reason: string;
42
+ } | {
43
+ kind: 'upgrade';
44
+ changes: PresetChange[];
45
+ } | {
46
+ kind: 'align';
47
+ } | {
48
+ kind: 'revert';
49
+ };
50
+ /**
51
+ * 判定一个动作该不该写、会改什么。
52
+ * @param input - 判定所需的事实。
53
+ * @returns 计划;`noop` 时调用方**不要**调用 `configEditor.edit`。
54
+ */
55
+ export declare function planPresetAction(input: PresetPlanInput): PresetPlan;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * preset 组合的**行模型**:官方声明式 preset 的 `config.plugins` 就是一棵
3
+ * Cordis entry list,本模块是它唯一的读写口径。
4
+ *
5
+ * 为什么不再有 YAML 文本层:旧机制里预设是磁盘上的 `agent.cordis.yml`,我们
6
+ * 只能逐行做文本手术(保缩进、保注释、保 CRLF)。0.1.7 起预设是 profile 配置
7
+ * 里的一条声明,宿主给我们的**已经是解析好的对象树**(`configEditor` 的
8
+ * `inherited` / `override`,以及 `entry.options.config`),再走文本就是自找
9
+ * 麻烦:`!!js` 标记、块标量、引号风格全都不该由我们操心。
10
+ *
11
+ * 三条纪律:
12
+ * - **形状判定照抄官方**:顶层必须是行列表,且每一行是带非空字符串 `name`
13
+ * 的映射(官方 `entryListProblem` 同款)。不满足 → `undefined`,一律不处理。
14
+ * - **只认顶层行**:`tool-bash`/`tool-fs` 这类 id 只按顶层匹配;group 里嵌套的
15
+ * 同名行属于那个 group 的私有组成,绝不被外层动作波及。
16
+ * - **绝不原地改**:所有"改写"都返回新对象,输入保持可比较(宿主按深相等判定
17
+ * 是否落盘,原地改会让幂等判定失去意义)。
18
+ * @module @xiaoso/dsh-tool-plus/presets/rows
19
+ */
20
+ /** 一行 Cordis 配置项的最小子集(其余键原样透传)。 */
21
+ export interface PresetRow {
22
+ id?: unknown;
23
+ name?: unknown;
24
+ group?: unknown;
25
+ disabled?: unknown;
26
+ isolate?: unknown;
27
+ config?: unknown;
28
+ [key: string]: unknown;
29
+ }
30
+ /** 行 id 的取值(非空字符串才认,与官方对 `name` 的要求同款)。 */
31
+ export declare function rowId(row: PresetRow): string | undefined;
32
+ /** 行模块名(官方 entry-list 要求每行都有非空 `name`)。 */
33
+ export declare function rowName(row: PresetRow): string | undefined;
34
+ /** 这一行是不是 group 载体(group 的子行在 `config` 里)。 */
35
+ export declare function isGroupRow(row: PresetRow): boolean;
36
+ /**
37
+ * 判定一份 `plugins` 值是不是合法的顶层行列表。规则与官方
38
+ * `entryListProblem` 一致:必须是数组,每一项必须是带非空 `name` 的映射。
39
+ * @param value - 待判定的值(来自宿主配置或测试注入)。
40
+ * @returns 合法时为 true。
41
+ */
42
+ export declare function isRowList(value: unknown): value is PresetRow[];
43
+ /**
44
+ * 展平一棵行树(含 group 嵌套),键为 `父/子` 形式的路径。
45
+ *
46
+ * 路径是**给面板看的标识**,也用于我们自己的差异比较;官方
47
+ * `compositionInventory()` 回的行没有路径(它压平后只剩 entryId/moduleName),
48
+ * 所以需要分组结构时只能从声明本身取。
49
+ * @param rows - 顶层行列表。
50
+ * @param prefix - 递归时的父路径。
51
+ * @returns 路径 → 行。
52
+ */
53
+ export declare function flattenRows(rows: readonly PresetRow[], prefix?: string): Map<string, PresetRow>;
54
+ /** 一个叶子:行内点号路径 → 规范化后的值字符串。 */
55
+ export interface RowLeaf {
56
+ /** 行内路径,如 `config.thresholdRatio` 或 `config.models[0].id`。 */
57
+ path: string;
58
+ /** 规范化值:标量走 `JSON.stringify`,所以 `true`/`"true"`/`1` 不会互相混淆。 */
59
+ value: string;
60
+ }
61
+ /**
62
+ * `!!js` 表达式标记:entry-list 方言把它解析成 `{ __jsExpr: <源码> }`(见
63
+ * `vendor/include/src/index.ts` 的 `JsExpr`)。它对我们是**标量**——一整段待求值的
64
+ * 源码,不是可以逐键比较的结构。漏了这一步会造出 `disabled.__jsExpr` 这种假叶子,
65
+ * 顺带让"冲突行的 disabled 归 conflicts 管"这条跳过规则失效。
66
+ * @param value - 待判定的值。
67
+ * @returns 是表达式标记时为 true。
68
+ */
69
+ export declare function isJsExpr(value: unknown): value is {
70
+ __jsExpr: string;
71
+ };
72
+ /**
73
+ * 收集一行自己的叶子路径(不含 `id`,不含 group 的子行)。
74
+ *
75
+ * 用规范化 JSON 而不是 `String()`:Loader 对 `disabled` 的语义是
76
+ * "布尔取 Boolean(value)",所以字符串 `"false"` 与布尔 `false` **语义相反**;
77
+ * 两者若都显示成 `false`,面板就会把一处真实差异藏起来。
78
+ * @param row - 目标行。
79
+ * @param base - 递归时的前缀。
80
+ * @returns 该行的叶子清单(顺序为键的声明顺序)。
81
+ */
82
+ export declare function rowLeaves(row: PresetRow, base?: string): RowLeaf[];
83
+ /** 规范化的深比较(两侧都是 YAML/JSON 值,无函数与原型差异)。 */
84
+ export declare function sameValue(a: unknown, b: unknown): boolean;