@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.
- package/README.en.md +12 -5
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +36 -0
- package/lib/client.js +377 -906
- package/lib/index.mjs +885 -1087
- package/lib/{scrapers-mhvFrvjH.mjs → scrapers-DSArdJ6N.mjs} +1 -1
- package/lib/types/client/ToolPlusSection.d.ts +3 -3
- package/lib/types/client/forms.d.ts +6 -6
- package/lib/types/client/index.d.ts +29 -12
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/preset-panel.d.ts +54 -42
- package/lib/types/config/settings.d.ts +202 -21
- package/lib/types/host/browser-probe-rpc.d.ts +2 -1
- package/lib/types/host/preset-host.d.ts +103 -0
- package/lib/types/host/preset-rpc.d.ts +37 -72
- package/lib/types/presets/compare.d.ts +14 -37
- package/lib/types/presets/conflicts.d.ts +59 -0
- package/lib/types/presets/delta.d.ts +23 -18
- package/lib/types/presets/plan.d.ts +55 -0
- package/lib/types/presets/rows.d.ts +84 -0
- package/lib/types/presets/status.d.ts +56 -47
- package/lib/types/tools/bash/background.d.ts +20 -5
- package/lib/types/tools/bash/streaming-output.d.ts +4 -2
- package/lib/types/tools/shared/browser-rpc-channel.d.ts +84 -42
- package/lib/types/web/client/labels.d.ts +1 -1
- package/lib/types/web/client/row-utils.d.ts +26 -1
- package/lib/{utils-eiUz9n8I.mjs → utils-DA8R1_Q8.mjs} +1 -1
- package/package.json +166 -151
- package/presets/tool-plus-ptc.patch.yml +164 -0
- package/presets/tool-plus-standard.patch.yml +158 -0
- package/lib/types/client/BashPlusCard.d.ts +0 -53
- package/lib/types/presets/install.d.ts +0 -44
- package/lib/types/presets/paths.d.ts +0 -41
- package/lib/types/presets/rewrite.d.ts +0 -86
- package/presets/install-presets.mjs +0 -143
- package/presets/package.json +0 -42
- package/presets/tool-plus-ptc/agent.cordis.yml +0 -284
- package/presets/tool-plus-ptc/preset.yml +0 -3
- package/presets/tool-plus-standard/agent.cordis.yml +0 -269
- 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
|
|
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)
|
|
4
|
-
*
|
|
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`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `connection.rpc.handle
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
/**
|
|
52
|
-
|
|
53
|
-
/** Apply one action to one preset
|
|
54
|
-
applyAction: (id: string, action:
|
|
55
|
-
/**
|
|
56
|
-
|
|
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):
|
|
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' | '
|
|
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
|
-
*
|
|
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
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
34
|
-
* @param
|
|
35
|
-
* @
|
|
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
|
|
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
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 是"冲突行都已禁用、无需调整"。两句都对,读起来却打架 —— 前者说的是
|
|
6
|
+
* **内容被改过**,后者说的是**我们自己那 4 个冲突行**。把差异逐条列出来,
|
|
7
|
+
* 用户才不用猜"到底哪里不同、对齐会不会动它"。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
45
|
-
* @param
|
|
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(
|
|
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;
|