dsh-plugin-tool-management 0.1.2 → 0.2.0

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.
@@ -0,0 +1,240 @@
1
+ // dsh-plugin-tool-management —— AGENTS.md 预设库 + 切换 服务层。
2
+ //
3
+ // DSH 全局指令基线只有一个文件 ~/.dsh/AGENTS.md(USER_GLOBAL_FILE 固定),
4
+ // 没有内置的「多份全局 AGENTS.md 切换」机制。本服务在插件目录内维护一个
5
+ // 预设库(每套一个子目录 + AGENTS.md),「应用」= 把选中预设内容写入
6
+ // ~/.dsh/AGENTS.md,新会话生效(当前会话不变,DSH 本身如此)。
7
+ //
8
+ // 约定镜像 @deepseek-ai/dsh-agent-presets:id 即目录名,正则
9
+ // /^[a-z0-9][a-z0-9-]*$/;__last-applied__ 是备份槽,不算用户预设。
10
+ // 「当前生效」靠比对 ~/.dsh/AGENTS.md 的 sha256 与各预设 sha256 推断,
11
+ // 无状态文件——用户手改全局文件也能如实反映。
12
+ import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { createHash } from 'node:crypto';
15
+ const PRESET_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
16
+ const FILENAME = 'AGENTS.md';
17
+ const LAST_APPLIED_ID = '__last-applied__';
18
+ export function createAgentsMdService(_ctx, deps) {
19
+ const message = (e) => String((e && e.message) || e);
20
+ async function sha256OfFile(abs) {
21
+ try {
22
+ const buf = await readFile(abs, 'utf8');
23
+ return createHash('sha256').update(buf).digest('hex');
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ // 幂等初始化:仅当库完全无预设且全局 AGENTS.md 存在时,拷贝全局为 default
30
+ // 预设(首次运行引导)。用户已 create/apply 过任何预设则不插手,避免把
31
+ // 当前生效内容误存成 default 与真正生效的预设冲突。
32
+ async function ensureInit() {
33
+ const [presetsDir, globalPath] = await Promise.all([deps.presetsDir, deps.getGlobalAgentsMdPath()]);
34
+ let existing = [];
35
+ try {
36
+ existing = await readdir(presetsDir, { withFileTypes: true });
37
+ }
38
+ catch { /* 库不存在 */ }
39
+ const hasPreset = existing.some((e) => e.isDirectory() && PRESET_ID_RE.test(e.name) && e.name !== LAST_APPLIED_ID);
40
+ if (hasPreset)
41
+ return;
42
+ let globalContent;
43
+ try {
44
+ globalContent = await readFile(globalPath, 'utf8');
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ try {
50
+ await mkdir(presetsDir, { recursive: true });
51
+ const defaultDir = join(presetsDir, 'default');
52
+ await mkdir(defaultDir, { recursive: true });
53
+ await writeFile(join(defaultDir, FILENAME), globalContent, 'utf8');
54
+ }
55
+ catch { /* best effort */ }
56
+ }
57
+ async function list() {
58
+ try {
59
+ await ensureInit();
60
+ const [presetsDir, globalPath] = await Promise.all([deps.presetsDir, deps.getGlobalAgentsMdPath()]);
61
+ let entries = [];
62
+ try {
63
+ entries = await readdir(presetsDir, { withFileTypes: true });
64
+ }
65
+ catch {
66
+ // 库目录不存在视作空(首次运行尚未创建)
67
+ }
68
+ const globalHash = await sha256OfFile(globalPath);
69
+ const ids = entries
70
+ .filter((e) => e.isDirectory() && PRESET_ID_RE.test(e.name) && e.name !== LAST_APPLIED_ID)
71
+ .map((e) => e.name);
72
+ const presets = await Promise.all(ids.map(async (id) => {
73
+ const contentHash = await sha256OfFile(join(presetsDir, id, FILENAME));
74
+ return {
75
+ id,
76
+ active: globalHash !== null && contentHash !== null && globalHash === contentHash,
77
+ };
78
+ }));
79
+ presets.sort((a, b) => a.id.localeCompare(b.id));
80
+ return { ok: true, presets };
81
+ }
82
+ catch (e) {
83
+ return { ok: false, error: message(e) };
84
+ }
85
+ }
86
+ async function read(id) {
87
+ const safeId = String(id || '');
88
+ // id 即目录名,严格校验防目录穿越:仅允许小写字母/数字/连字符。
89
+ // __last-applied__ 含下划线会被拒(它是备份槽,不走 read,由 apply 内部直写)。
90
+ if (!PRESET_ID_RE.test(safeId))
91
+ return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
92
+ try {
93
+ const content = await readFile(join(deps.presetsDir, safeId, FILENAME), 'utf8');
94
+ return { ok: true, content };
95
+ }
96
+ catch {
97
+ return { ok: false, error: '预设不存在:' + safeId };
98
+ }
99
+ }
100
+ async function create(id, from) {
101
+ const safeId = String(id || '');
102
+ if (!PRESET_ID_RE.test(safeId))
103
+ return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
104
+ const dir = join(deps.presetsDir, safeId);
105
+ try {
106
+ await stat(dir);
107
+ return { ok: false, error: 'id 已存在:' + safeId };
108
+ }
109
+ catch { /* 不存在,继续 */ }
110
+ let content;
111
+ const srcId = String(from || '');
112
+ if (srcId) {
113
+ if (!PRESET_ID_RE.test(srcId))
114
+ return { ok: false, error: '非法 from id:' + srcId };
115
+ try {
116
+ content = await readFile(join(deps.presetsDir, srcId, FILENAME), 'utf8');
117
+ }
118
+ catch {
119
+ return { ok: false, error: '来源预设不存在:' + srcId };
120
+ }
121
+ }
122
+ else {
123
+ content = '# AGENTS.md\n\n(DSH 全局指令基线预设,待编辑)\n';
124
+ }
125
+ await mkdir(dir, { recursive: true });
126
+ await writeFile(join(dir, FILENAME), content, 'utf8');
127
+ return { ok: true, id: safeId };
128
+ }
129
+ async function update(id, content) {
130
+ const safeId = String(id || '');
131
+ if (!PRESET_ID_RE.test(safeId))
132
+ return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
133
+ const dir = join(deps.presetsDir, safeId);
134
+ try {
135
+ await stat(dir);
136
+ }
137
+ catch {
138
+ return { ok: false, error: '预设不存在:' + safeId };
139
+ }
140
+ await writeFile(join(dir, FILENAME), String(content ?? ''), 'utf8');
141
+ return { ok: true, id: safeId };
142
+ }
143
+ async function apply(id) {
144
+ const safeId = String(id || '');
145
+ if (!PRESET_ID_RE.test(safeId))
146
+ return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
147
+ let content;
148
+ try {
149
+ content = await readFile(join(deps.presetsDir, safeId, FILENAME), 'utf8');
150
+ }
151
+ catch {
152
+ return { ok: false, error: '预设不存在:' + safeId };
153
+ }
154
+ const globalPath = await deps.getGlobalAgentsMdPath();
155
+ // 备份当前全局内容(若存在)到 __last-applied__,再覆盖写入。
156
+ let prev = null;
157
+ try {
158
+ prev = await readFile(globalPath, 'utf8');
159
+ }
160
+ catch { /* 当前不存在,不备份 */ }
161
+ if (prev !== null) {
162
+ const backupDir = join(deps.presetsDir, LAST_APPLIED_ID);
163
+ await mkdir(backupDir, { recursive: true });
164
+ await writeFile(join(backupDir, FILENAME), prev, 'utf8');
165
+ }
166
+ await writeFile(globalPath, content, 'utf8');
167
+ return { ok: true, id: safeId, backedUp: prev !== null };
168
+ }
169
+ async function getCurrent() {
170
+ try {
171
+ await ensureInit();
172
+ const [globalPath, presetsDir] = await Promise.all([deps.getGlobalAgentsMdPath(), deps.presetsDir]);
173
+ let content;
174
+ try {
175
+ content = await readFile(globalPath, 'utf8');
176
+ }
177
+ catch {
178
+ return { ok: true, content: '', presetId: null, exists: false };
179
+ }
180
+ const globalHash = createHash('sha256').update(content).digest('hex');
181
+ let presetId = null;
182
+ let entries = [];
183
+ try {
184
+ entries = await readdir(presetsDir, { withFileTypes: true });
185
+ }
186
+ catch { /* 库不存在 */ }
187
+ for (const e of entries) {
188
+ if (!e.isDirectory() || !PRESET_ID_RE.test(e.name) || e.name === LAST_APPLIED_ID)
189
+ continue;
190
+ let c;
191
+ try {
192
+ c = await readFile(join(presetsDir, e.name, FILENAME), 'utf8');
193
+ }
194
+ catch {
195
+ continue;
196
+ }
197
+ if (createHash('sha256').update(c).digest('hex') === globalHash) {
198
+ presetId = e.name;
199
+ break;
200
+ }
201
+ }
202
+ return { ok: true, content, presetId, exists: true };
203
+ }
204
+ catch (e) {
205
+ return { ok: false, error: message(e) };
206
+ }
207
+ }
208
+ async function remove(id) {
209
+ const safeId = String(id || '');
210
+ if (safeId === LAST_APPLIED_ID)
211
+ return { ok: false, error: '备份槽不可删除' };
212
+ if (!PRESET_ID_RE.test(safeId))
213
+ return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
214
+ const dir = join(deps.presetsDir, safeId);
215
+ try {
216
+ await stat(dir);
217
+ }
218
+ catch {
219
+ return { ok: false, error: '预设不存在:' + safeId };
220
+ }
221
+ await rm(dir, { recursive: true, force: true });
222
+ return { ok: true, id: safeId };
223
+ }
224
+ // 从外部文本内容(如导入的 .md 文件)建预设:id 校验 + 重复检查 + 写 AGENTS.md。
225
+ async function importPreset(id, content) {
226
+ const safeId = String(id || '');
227
+ if (!PRESET_ID_RE.test(safeId))
228
+ return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
229
+ const dir = join(deps.presetsDir, safeId);
230
+ try {
231
+ await stat(dir);
232
+ return { ok: false, error: 'id 已存在:' + safeId };
233
+ }
234
+ catch { /* 不存在,继续 */ }
235
+ await mkdir(dir, { recursive: true });
236
+ await writeFile(join(dir, FILENAME), String(content ?? ''), 'utf8');
237
+ return { ok: true, id: safeId };
238
+ }
239
+ return { list, read, create, update, apply, getCurrent, remove, importPreset };
240
+ }